summaryrefslogtreecommitdiffstats
path: root/devtools/server/tests/browser
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/server/tests/browser')
-rw-r--r--devtools/server/tests/browser/animation-data.html115
-rw-r--r--devtools/server/tests/browser/animation.html170
-rw-r--r--devtools/server/tests/browser/application-manifest-404-manifest.html10
-rw-r--r--devtools/server/tests/browser/application-manifest-basic.html10
-rw-r--r--devtools/server/tests/browser/application-manifest-invalid-json.html11
-rw-r--r--devtools/server/tests/browser/application-manifest-no-manifest.html9
-rw-r--r--devtools/server/tests/browser/application-manifest-warnings.html10
-rw-r--r--devtools/server/tests/browser/browser.toml213
-rw-r--r--devtools/server/tests/browser/browser_accessibility_highlighter_infobar.js73
-rw-r--r--devtools/server/tests/browser/browser_accessibility_infobar_audit_keyboard.js157
-rw-r--r--devtools/server/tests/browser/browser_accessibility_infobar_audit_text_label.js164
-rw-r--r--devtools/server/tests/browser/browser_accessibility_infobar_show.js181
-rw-r--r--devtools/server/tests/browser/browser_accessibility_keyboard_audit.js367
-rw-r--r--devtools/server/tests/browser/browser_accessibility_node.js166
-rw-r--r--devtools/server/tests/browser/browser_accessibility_node_audit.js116
-rw-r--r--devtools/server/tests/browser/browser_accessibility_node_events.js197
-rw-r--r--devtools/server/tests/browser/browser_accessibility_node_tabbing_order_highlighter.js92
-rw-r--r--devtools/server/tests/browser/browser_accessibility_simple.js106
-rw-r--r--devtools/server/tests/browser/browser_accessibility_simulator.js88
-rw-r--r--devtools/server/tests/browser/browser_accessibility_tabbing_order_highlighter.js101
-rw-r--r--devtools/server/tests/browser/browser_accessibility_text_label_audit.js1134
-rw-r--r--devtools/server/tests/browser/browser_accessibility_text_label_audit_frame.js48
-rw-r--r--devtools/server/tests/browser/browser_accessibility_walker.js170
-rw-r--r--devtools/server/tests/browser/browser_accessibility_walker_audit.js155
-rw-r--r--devtools/server/tests/browser/browser_actor_error.js94
-rw-r--r--devtools/server/tests/browser/browser_animation_actor-lifetime.js80
-rw-r--r--devtools/server/tests/browser/browser_animation_emitMutations.js72
-rw-r--r--devtools/server/tests/browser/browser_animation_getMultipleStates.js63
-rw-r--r--devtools/server/tests/browser/browser_animation_getPlayers.js39
-rw-r--r--devtools/server/tests/browser/browser_animation_getStateAfterFinished.js76
-rw-r--r--devtools/server/tests/browser/browser_animation_getSubTreeAnimations.js50
-rw-r--r--devtools/server/tests/browser/browser_animation_keepFinished.js55
-rw-r--r--devtools/server/tests/browser/browser_animation_playPauseIframe.js70
-rw-r--r--devtools/server/tests/browser/browser_animation_playPauseSeveral.js67
-rw-r--r--devtools/server/tests/browser/browser_animation_playerState.js159
-rw-r--r--devtools/server/tests/browser/browser_animation_reconstructState.js40
-rw-r--r--devtools/server/tests/browser/browser_animation_refreshTransitions.js97
-rw-r--r--devtools/server/tests/browser/browser_animation_setCurrentTime.js47
-rw-r--r--devtools/server/tests/browser/browser_animation_setPlaybackRate.js49
-rw-r--r--devtools/server/tests/browser/browser_animation_simple.js39
-rw-r--r--devtools/server/tests/browser/browser_animation_updatedState.js66
-rw-r--r--devtools/server/tests/browser/browser_application_manifest.js87
-rw-r--r--devtools/server/tests/browser/browser_canvasframe_helper_01.js170
-rw-r--r--devtools/server/tests/browser/browser_canvasframe_helper_02.js53
-rw-r--r--devtools/server/tests/browser/browser_canvasframe_helper_03.js129
-rw-r--r--devtools/server/tests/browser/browser_canvasframe_helper_04.js142
-rw-r--r--devtools/server/tests/browser/browser_canvasframe_helper_05.js134
-rw-r--r--devtools/server/tests/browser/browser_canvasframe_helper_06.js116
-rw-r--r--devtools/server/tests/browser/browser_compatibility_cssIssues.js137
-rw-r--r--devtools/server/tests/browser/browser_connectToFrame.js142
-rw-r--r--devtools/server/tests/browser/browser_debugger_server.js198
-rw-r--r--devtools/server/tests/browser/browser_document_devtools_basics.js103
-rw-r--r--devtools/server/tests/browser/browser_document_rdp_basics.js129
-rw-r--r--devtools/server/tests/browser/browser_getProcess.js129
-rw-r--r--devtools/server/tests/browser/browser_inspector-anonymous.js204
-rw-r--r--devtools/server/tests/browser/browser_inspector-iframe.js93
-rw-r--r--devtools/server/tests/browser/browser_inspector-insert.js158
-rw-r--r--devtools/server/tests/browser/browser_inspector-isScrollable.js34
-rw-r--r--devtools/server/tests/browser/browser_inspector-mutations-childlist.js282
-rw-r--r--devtools/server/tests/browser/browser_inspector-release.js54
-rw-r--r--devtools/server/tests/browser/browser_inspector-remove.js102
-rw-r--r--devtools/server/tests/browser/browser_inspector-retain.js157
-rw-r--r--devtools/server/tests/browser/browser_inspector-search.js347
-rw-r--r--devtools/server/tests/browser/browser_inspector-shadow.js231
-rw-r--r--devtools/server/tests/browser/browser_inspector-traversal.js350
-rw-r--r--devtools/server/tests/browser/browser_inspector-utils.js25
-rw-r--r--devtools/server/tests/browser/browser_layout_getGrids.js145
-rw-r--r--devtools/server/tests/browser/browser_layout_simple.js31
-rw-r--r--devtools/server/tests/browser/browser_memory_allocations_01.js107
-rw-r--r--devtools/server/tests/browser/browser_perf-01.js57
-rw-r--r--devtools/server/tests/browser/browser_perf-02.js37
-rw-r--r--devtools/server/tests/browser/browser_perf-04.js53
-rw-r--r--devtools/server/tests/browser/browser_perf-getSupportedFeatures.js23
-rw-r--r--devtools/server/tests/browser/browser_storage_cookies-duplicate-names.js134
-rw-r--r--devtools/server/tests/browser/browser_storage_dynamic_windows.js410
-rw-r--r--devtools/server/tests/browser/browser_storage_listings.js743
-rw-r--r--devtools/server/tests/browser/browser_storage_updates.js343
-rw-r--r--devtools/server/tests/browser/browser_style_utils_getFontPreviewData.js137
-rw-r--r--devtools/server/tests/browser/browser_styles_getRuleText.js34
-rw-r--r--devtools/server/tests/browser/browser_stylesheets_getTextEmpty.js53
-rw-r--r--devtools/server/tests/browser/director-script-target.html18
-rw-r--r--devtools/server/tests/browser/doc_accessibility.html19
-rw-r--r--devtools/server/tests/browser/doc_accessibility_audit.html10
-rw-r--r--devtools/server/tests/browser/doc_accessibility_infobar.html12
-rw-r--r--devtools/server/tests/browser/doc_accessibility_keyboard_audit.html150
-rw-r--r--devtools/server/tests/browser/doc_accessibility_text_label_audit.html463
-rw-r--r--devtools/server/tests/browser/doc_accessibility_text_label_audit_frame.html10
-rw-r--r--devtools/server/tests/browser/doc_allocations.html23
-rw-r--r--devtools/server/tests/browser/doc_compatibility.html28
-rw-r--r--devtools/server/tests/browser/doc_force_cc.html32
-rw-r--r--devtools/server/tests/browser/doc_force_gc.html31
-rw-r--r--devtools/server/tests/browser/doc_iframe.html17
-rw-r--r--devtools/server/tests/browser/doc_iframe2.html15
-rw-r--r--devtools/server/tests/browser/doc_iframe_content.html14
-rw-r--r--devtools/server/tests/browser/doc_innerHTML.html21
-rw-r--r--devtools/server/tests/browser/error-actor.js25
-rw-r--r--devtools/server/tests/browser/grid.html42
-rw-r--r--devtools/server/tests/browser/head.js337
-rw-r--r--devtools/server/tests/browser/inspector-helpers.js161
-rw-r--r--devtools/server/tests/browser/inspector-isScrollable-data.html79
-rw-r--r--devtools/server/tests/browser/inspector-search-data.html54
-rw-r--r--devtools/server/tests/browser/inspector-shadow.html117
-rw-r--r--devtools/server/tests/browser/inspector-traversal-data.html98
-rw-r--r--devtools/server/tests/browser/storage-cookies-same-name.html29
-rw-r--r--devtools/server/tests/browser/storage-dynamic-windows.html117
-rw-r--r--devtools/server/tests/browser/storage-helpers.js50
-rw-r--r--devtools/server/tests/browser/storage-listings.html123
-rw-r--r--devtools/server/tests/browser/storage-secured-iframe.html94
-rw-r--r--devtools/server/tests/browser/storage-unsecured-iframe.html28
-rw-r--r--devtools/server/tests/browser/storage-updates.html47
-rw-r--r--devtools/server/tests/browser/test-errors-actor.js72
-rw-r--r--devtools/server/tests/browser/test-window.xhtml5
112 files changed, 13380 insertions, 0 deletions
diff --git a/devtools/server/tests/browser/animation-data.html b/devtools/server/tests/browser/animation-data.html
new file mode 100644
index 0000000000..1ee654cb17
--- /dev/null
+++ b/devtools/server/tests/browser/animation-data.html
@@ -0,0 +1,115 @@
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>Animation Test Data</title>
+ <style>
+ .ball {
+ width: 80px;
+ height: 80px;
+ border-radius: 50%;
+ background: #f06;
+
+ position: absolute;
+ }
+
+ .still {
+ top: 0;
+ left: 10px;
+ }
+
+ .animated {
+ top: 100px;
+ left: 10px;
+
+ animation: simple-animation 2s infinite alternate;
+ }
+
+ .multi {
+ top: 200px;
+ left: 10px;
+
+ animation: simple-animation 2s infinite alternate,
+ other-animation 5s infinite alternate;
+ }
+
+ .delayed {
+ top: 300px;
+ left: 10px;
+ background: rebeccapurple;
+
+ animation: simple-animation 3s 60s 10;
+ }
+
+ .multi-finite {
+ top: 400px;
+ left: 10px;
+ background: yellow;
+
+ animation: simple-animation 3s,
+ other-animation 4s;
+ }
+
+ .short {
+ top: 500px;
+ left: 10px;
+ background: red;
+
+ animation: simple-animation 2s;
+ }
+
+ .long {
+ top: 600px;
+ left: 10px;
+ background: blue;
+
+ animation: simple-animation 120s;
+ }
+
+ .negative-delay {
+ top: 700px;
+ left: 10px;
+ background: gray;
+
+ animation: simple-animation 15s -10s;
+ animation-fill-mode: forwards;
+ }
+
+ .no-compositor {
+ top: 0;
+ right: 10px;
+ background: gold;
+
+ animation: no-compositor 10s cubic-bezier(.57,-0.02,1,.31) forwards;
+ }
+
+ @keyframes simple-animation {
+ 100% {
+ transform: translateX(300px);
+ }
+ }
+
+ @keyframes other-animation {
+ 100% {
+ background: blue;
+ }
+ }
+
+ @keyframes no-compositor {
+ 100% {
+ margin-right: 600px;
+ }
+ }
+ </style>
+</head>
+</body>
+ <div class="ball still"></div>
+ <div class="ball animated"></div>
+ <div class="ball multi"></div>
+ <div class="ball delayed"></div>
+ <div class="ball multi-finite"></div>
+ <div class="ball short"></div>
+ <div class="ball long"></div>
+ <div class="ball negative-delay"></div>
+ <div class="ball no-compositor"></div>
+</body>
+</html>
diff --git a/devtools/server/tests/browser/animation.html b/devtools/server/tests/browser/animation.html
new file mode 100644
index 0000000000..f7b83df283
--- /dev/null
+++ b/devtools/server/tests/browser/animation.html
@@ -0,0 +1,170 @@
+<!DOCTYPE html>
+<style>
+ .not-animated {
+ display: inline-block;
+
+ width: 50px;
+ height: 50px;
+ border-radius: 50%;
+ background: #eee;
+ }
+
+ .simple-animation {
+ display: inline-block;
+
+ width: 64px;
+ height: 64px;
+ border-radius: 50%;
+ background: red;
+
+ animation: move 200s infinite;
+ }
+
+ .multiple-animations {
+ display: inline-block;
+
+ width: 50px;
+ height: 50px;
+ border-radius: 50%;
+ background: #eee;
+
+ animation: move 200s infinite , glow 100s 5;
+ animation-timing-function: ease-out;
+ animation-direction: reverse;
+ animation-fill-mode: both;
+ }
+
+ .transition {
+ display: inline-block;
+
+ width: 50px;
+ height: 50px;
+ border-radius: 50%;
+ background: #f06;
+
+ transition: width 500s ease-out;
+ }
+ .transition.get-round {
+ width: 200px;
+ }
+
+ .long-animation {
+ display: inline-block;
+
+ width: 50px;
+ height: 50px;
+ border-radius: 50%;
+ background: gold;
+
+ animation: move 100s;
+ }
+
+ .short-animation {
+ display: inline-block;
+
+ width: 50px;
+ height: 50px;
+ border-radius: 50%;
+ background: purple;
+
+ animation: move 1s;
+ }
+
+ .delayed-animation {
+ display: inline-block;
+
+ width: 50px;
+ height: 50px;
+ border-radius: 50%;
+ background: rebeccapurple;
+
+ animation: move 200s 5s infinite;
+ }
+
+ .delayed-transition {
+ display: inline-block;
+
+ width: 50px;
+ height: 50px;
+ border-radius: 50%;
+ background: black;
+
+ transition: width 500s 3s;
+ }
+ .delayed-transition.get-round {
+ width: 200px;
+ }
+
+ .delayed-multiple-animations {
+ display: inline-block;
+
+ width: 50px;
+ height: 50px;
+ border-radius: 50%;
+ background: green;
+
+ animation: move .5s 1s 10, glow 1s .75s 30;
+ }
+
+ .multiple-animations-2 {
+ display: inline-block;
+
+ width: 50px;
+ height: 50px;
+ border-radius: 50%;
+ background: blue;
+
+ animation: move .5s, glow 100s 2s infinite, grow 300s 1s 100;
+ }
+
+ .all-transitions {
+ position: absolute;
+ top: 0;
+ right: 0;
+ width: 50px;
+ height: 50px;
+ background: blue;
+ transition: all .2s;
+ }
+ .all-transitions.expand {
+ width: 200px;
+ height: 100px;
+ }
+
+ @keyframes move {
+ 100% {
+ transform: translateY(100px);
+ }
+ }
+
+ @keyframes glow {
+ 100% {
+ background: yellow;
+ }
+ }
+
+ @keyframes grow {
+ 100% {
+ width: 100px;
+ }
+ }
+</style>
+<div class="not-animated"></div>
+<div class="simple-animation"></div>
+<div class="multiple-animations"></div>
+<div class="transition"></div>
+<div class="long-animation"></div>
+<div class="short-animation"></div>
+<div class="delayed-animation"></div>
+<div class="delayed-transition"></div>
+<div class="delayed-multiple-animations"></div>
+<div class="multiple-animations-2"></div>
+<div class="all-transitions"></div>
+<script type="text/javascript">
+ "use strict";
+ // Get the transitions started when the page loads
+ addEventListener("load", function() {
+ document.querySelector(".transition").classList.add("get-round");
+ document.querySelector(".delayed-transition").classList.add("get-round");
+ });
+</script>
diff --git a/devtools/server/tests/browser/application-manifest-404-manifest.html b/devtools/server/tests/browser/application-manifest-404-manifest.html
new file mode 100644
index 0000000000..fd182a69a6
--- /dev/null
+++ b/devtools/server/tests/browser/application-manifest-404-manifest.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Simple manifest</title>
+ <link rel="manifest" href="non-existing-manifest.json">
+</head>
+<body>
+ <p>This page links to a manifest URL that is a 404.</p>
+</body>
diff --git a/devtools/server/tests/browser/application-manifest-basic.html b/devtools/server/tests/browser/application-manifest-basic.html
new file mode 100644
index 0000000000..a8e11a645f
--- /dev/null
+++ b/devtools/server/tests/browser/application-manifest-basic.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Simple manifest</title>
+ <link rel="manifest" href='data:application/manifest+json,{"name": "FooApp"}'>
+</head>
+<body>
+ <pre><code>{ "name": "Foo App" }</code></pre>
+</body>
diff --git a/devtools/server/tests/browser/application-manifest-invalid-json.html b/devtools/server/tests/browser/application-manifest-invalid-json.html
new file mode 100644
index 0000000000..2717a97ddd
--- /dev/null
+++ b/devtools/server/tests/browser/application-manifest-invalid-json.html
@@ -0,0 +1,11 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Invalid JSON</title>
+ <link rel="manifest" href='data:application/manifest+json,foo:'>
+</head>
+<body>
+ <p>Invalid JSON:</p>
+ <pre><code>foo:</code></pre>
+</body>
diff --git a/devtools/server/tests/browser/application-manifest-no-manifest.html b/devtools/server/tests/browser/application-manifest-no-manifest.html
new file mode 100644
index 0000000000..5f0668aa50
--- /dev/null
+++ b/devtools/server/tests/browser/application-manifest-no-manifest.html
@@ -0,0 +1,9 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>No manifest</title>
+</head>
+<body>
+ <p>This page does not link to a manifest</p>
+</body>
diff --git a/devtools/server/tests/browser/application-manifest-warnings.html b/devtools/server/tests/browser/application-manifest-warnings.html
new file mode 100644
index 0000000000..57f8b9b4e7
--- /dev/null
+++ b/devtools/server/tests/browser/application-manifest-warnings.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Empty manifest</title>
+ <link rel="manifest" href='data:application/manifest+json,{"name": 0}'>
+</head>
+<body>
+ <pre><code>{ }</code></pre>
+</body>
diff --git a/devtools/server/tests/browser/browser.toml b/devtools/server/tests/browser/browser.toml
new file mode 100644
index 0000000000..e03ab5649a
--- /dev/null
+++ b/devtools/server/tests/browser/browser.toml
@@ -0,0 +1,213 @@
+[DEFAULT]
+tags = "devtools"
+subsuite = "devtools"
+skip-if = [
+ "http3", # Bug 1829298
+ "http2",
+]
+support-files = [
+ "head.js",
+ "animation.html",
+ "animation-data.html",
+ "application-manifest-404-manifest.html",
+ "application-manifest-basic.html",
+ "application-manifest-invalid-json.html",
+ "application-manifest-no-manifest.html",
+ "application-manifest-warnings.html",
+ "doc_accessibility_audit.html",
+ "doc_accessibility_infobar.html",
+ "doc_accessibility_keyboard_audit.html",
+ "doc_accessibility_text_label_audit_frame.html",
+ "doc_accessibility_text_label_audit.html",
+ "doc_accessibility.html",
+ "doc_allocations.html",
+ "doc_compatibility.html",
+ "doc_force_cc.html",
+ "doc_force_gc.html",
+ "doc_innerHTML.html",
+ "doc_iframe.html",
+ "doc_iframe_content.html",
+ "doc_iframe2.html",
+ "error-actor.js",
+ "grid.html",
+ "inspector-isScrollable-data.html",
+ "inspector-search-data.html",
+ "inspector-traversal-data.html",
+ "inspector-shadow.html",
+ "storage-cookies-same-name.html",
+ "storage-dynamic-windows.html",
+ "storage-listings.html",
+ "storage-unsecured-iframe.html",
+ "storage-updates.html",
+ "storage-secured-iframe.html",
+ "test-errors-actor.js",
+ "test-window.xhtml",
+ "inspector-helpers.js",
+ "storage-helpers.js",
+ "!/devtools/client/shared/test/shared-head.js",
+ "!/devtools/client/shared/test/telemetry-test-helpers.js",
+ "!/devtools/server/tests/chrome/hello-actor.js",
+]
+
+["browser_accessibility_highlighter_infobar.js"]
+
+["browser_accessibility_infobar_audit_keyboard.js"]
+
+["browser_accessibility_infobar_audit_text_label.js"]
+
+["browser_accessibility_infobar_show.js"]
+
+["browser_accessibility_keyboard_audit.js"]
+
+["browser_accessibility_node.js"]
+
+["browser_accessibility_node_audit.js"]
+
+["browser_accessibility_node_events.js"]
+
+["browser_accessibility_node_tabbing_order_highlighter.js"]
+
+["browser_accessibility_simple.js"]
+
+["browser_accessibility_simulator.js"]
+
+["browser_accessibility_tabbing_order_highlighter.js"]
+
+["browser_accessibility_text_label_audit.js"]
+
+["browser_accessibility_text_label_audit_frame.js"]
+
+["browser_accessibility_walker.js"]
+
+["browser_accessibility_walker_audit.js"]
+
+["browser_actor_error.js"]
+
+["browser_animation_actor-lifetime.js"]
+
+["browser_animation_emitMutations.js"]
+
+["browser_animation_getMultipleStates.js"]
+
+["browser_animation_getPlayers.js"]
+
+["browser_animation_getStateAfterFinished.js"]
+
+["browser_animation_getSubTreeAnimations.js"]
+
+["browser_animation_keepFinished.js"]
+
+["browser_animation_playPauseIframe.js"]
+
+["browser_animation_playPauseSeveral.js"]
+
+["browser_animation_playerState.js"]
+
+["browser_animation_reconstructState.js"]
+
+["browser_animation_refreshTransitions.js"]
+
+["browser_animation_setCurrentTime.js"]
+
+["browser_animation_setPlaybackRate.js"]
+
+["browser_animation_simple.js"]
+
+["browser_animation_updatedState.js"]
+
+["browser_application_manifest.js"]
+
+["browser_canvasframe_helper_01.js"]
+skip-if = ["true"] # Bug 1183605
+
+["browser_canvasframe_helper_02.js"]
+skip-if = ["true"] # iframe will not be loaded in xul:window with strict xhtml.
+
+["browser_canvasframe_helper_03.js"]
+skip-if = ["true"] # Bug 1183605
+
+["browser_canvasframe_helper_04.js"]
+skip-if = ["true"] # Bug 1183605
+
+["browser_canvasframe_helper_05.js"]
+skip-if = ["true"] # Bug 1183605
+
+["browser_canvasframe_helper_06.js"]
+skip-if = ["true"] # Bug 1183605
+
+["browser_compatibility_cssIssues.js"]
+
+["browser_connectToFrame.js"]
+
+["browser_debugger_server.js"]
+
+["browser_document_devtools_basics.js"]
+
+["browser_document_rdp_basics.js"]
+
+["browser_getProcess.js"]
+
+["browser_inspector-anonymous.js"]
+
+["browser_inspector-iframe.js"]
+
+["browser_inspector-insert.js"]
+
+["browser_inspector-isScrollable.js"]
+
+["browser_inspector-mutations-childlist.js"]
+
+["browser_inspector-release.js"]
+
+["browser_inspector-remove.js"]
+
+["browser_inspector-retain.js"]
+
+["browser_inspector-search.js"]
+
+["browser_inspector-shadow.js"]
+
+["browser_inspector-traversal.js"]
+
+["browser_inspector-utils.js"]
+
+["browser_layout_getGrids.js"]
+
+["browser_layout_simple.js"]
+
+["browser_memory_allocations_01.js"]
+
+["browser_perf-01.js"]
+skip-if = ["tsan"] # bug 1804081, profiler issues in TSAN
+
+["browser_perf-02.js"]
+skip-if = ["tsan"] # bug 1804081, profiler issues in TSAN
+
+["browser_perf-04.js"]
+skip-if = ["tsan"] # bug 1804081, profiler issues in TSAN
+
+["browser_perf-getSupportedFeatures.js"]
+skip-if = ["tsan"] # bug 1804081, profiler issues in TSAN
+
+["browser_storage_cookies-duplicate-names.js"]
+https_first_disabled = true
+
+["browser_storage_dynamic_windows.js"]
+https_first_disabled = true
+skip-if = [
+ "debug", # Bug 1715916 - test is having race conditions on slow hardware
+ "tsan", # high frequency intermittent
+ "win11_2009 && asan", # high frequency intermittent
+]
+
+["browser_storage_listings.js"]
+https_first_disabled = true
+
+["browser_storage_updates.js"]
+https_first_disabled = true
+
+["browser_style_utils_getFontPreviewData.js"]
+
+["browser_styles_getRuleText.js"]
+
+["browser_stylesheets_getTextEmpty.js"]
diff --git a/devtools/server/tests/browser/browser_accessibility_highlighter_infobar.js b/devtools/server/tests/browser/browser_accessibility_highlighter_infobar.js
new file mode 100644
index 0000000000..0979276230
--- /dev/null
+++ b/devtools/server/tests/browser/browser_accessibility_highlighter_infobar.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";
+
+// Test the accessible highlighter's infobar content.
+
+const {
+ truncateString,
+} = require("resource://devtools/shared/inspector/utils.js");
+const {
+ MAX_STRING_LENGTH,
+} = require("resource://devtools/server/actors/highlighters/utils/accessibility.js");
+
+add_task(async function () {
+ const { target, walker, parentAccessibility, a11yWalker } =
+ await initAccessibilityFrontsForUrl(
+ MAIN_DOMAIN + "doc_accessibility_infobar.html"
+ );
+
+ info("Button front checks");
+ await checkNameAndRole(walker, "#button", a11yWalker, "Accessible Button");
+
+ info("Front with long name checks");
+ await checkNameAndRole(
+ walker,
+ "#h1",
+ a11yWalker,
+ "Lorem ipsum dolor sit ame" + "\u2026" + "e et dolore magna aliqua."
+ );
+
+ await waitForA11yShutdown(parentAccessibility);
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+/**
+ * A helper function for testing the accessible's displayed name and roles.
+ *
+ * @param {Object} walker
+ * The DOM walker.
+ * @param {String} querySelector
+ * The selector for the node to retrieve accessible from.
+ * @param {Object} a11yWalker
+ * The accessibility walker.
+ * @param {String} expectedName
+ * Expected string content for displaying the accessible's name.
+ * We are testing this in particular because name can be truncated.
+ */
+async function checkNameAndRole(
+ walker,
+ querySelector,
+ a11yWalker,
+ expectedName
+) {
+ const node = await walker.querySelector(walker.rootNode, querySelector);
+ const accessibleFront = await a11yWalker.getAccessibleFor(node);
+
+ const { name, role } = accessibleFront;
+ const onHighlightEvent = a11yWalker.once("highlighter-event");
+
+ await a11yWalker.highlightAccessible(accessibleFront);
+ const { options } = await onHighlightEvent;
+ is(options.name, name, "Accessible highlight has correct name option");
+ is(options.role, role, "Accessible highlight has correct role option");
+
+ is(
+ `"${truncateString(name, MAX_STRING_LENGTH)}"`,
+ `"${expectedName}"`,
+ "Accessible has correct displayed name."
+ );
+}
diff --git a/devtools/server/tests/browser/browser_accessibility_infobar_audit_keyboard.js b/devtools/server/tests/browser/browser_accessibility_infobar_audit_keyboard.js
new file mode 100644
index 0000000000..73fc7127f4
--- /dev/null
+++ b/devtools/server/tests/browser/browser_accessibility_infobar_audit_keyboard.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";
+
+// Checks for the AccessibleHighlighter's infobar component and its keyboard
+// audit.
+
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: MAIN_DOMAIN + "doc_accessibility_infobar.html",
+ },
+ async function (browser) {
+ await SpecialPowers.spawn(browser, [], async function () {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ const {
+ HighlighterEnvironment,
+ } = require("resource://devtools/server/actors/highlighters.js");
+ const {
+ AccessibleHighlighter,
+ } = require("resource://devtools/server/actors/highlighters/accessible.js");
+ const {
+ LocalizationHelper,
+ } = require("resource://devtools/shared/l10n.js");
+ const L10N = new LocalizationHelper(
+ "devtools/shared/locales/accessibility.properties"
+ );
+
+ const {
+ accessibility: {
+ AUDIT_TYPE,
+ ISSUE_TYPE: {
+ [AUDIT_TYPE.KEYBOARD]: {
+ INTERACTIVE_NO_ACTION,
+ FOCUSABLE_NO_SEMANTICS,
+ },
+ },
+ SCORES: { FAIL, WARNING },
+ },
+ } = require("resource://devtools/shared/constants.js");
+
+ /**
+ * Checks for updated content for an infobar.
+ *
+ * @param {Object} infobar
+ * Accessible highlighter's infobar component.
+ * @param {Object} audit
+ * Audit information that is passed on highlighter show.
+ */
+ function checkKeyboard(infobar, audit) {
+ const { issue, score } = audit || {};
+ let expected = "";
+ if (issue) {
+ const { ISSUE_TO_INFOBAR_LABEL_MAP } =
+ infobar.audit.reports[AUDIT_TYPE.KEYBOARD].constructor;
+ expected = L10N.getStr(ISSUE_TO_INFOBAR_LABEL_MAP[issue]);
+ }
+
+ is(
+ infobar.getTextContent("keyboard"),
+ expected,
+ "infobar keyboard audit text content is correct"
+ );
+ if (score) {
+ ok(infobar.getElement("keyboard").classList.contains(score));
+ }
+ }
+
+ // Start testing. First, create highlighter environment and initialize.
+ const env = new HighlighterEnvironment();
+ env.initFromWindow(content.window);
+
+ // Wait for loading highlighter environment content to complete before creating the
+ // highlighter.
+ await new Promise(resolve => {
+ const doc = env.document;
+
+ function onContentLoaded() {
+ if (
+ doc.readyState === "interactive" ||
+ doc.readyState === "complete"
+ ) {
+ resolve();
+ } else {
+ doc.addEventListener("DOMContentLoaded", onContentLoaded, {
+ once: true,
+ });
+ }
+ }
+
+ onContentLoaded();
+ });
+
+ // Now, we can test the Infobar's audit content.
+ const node = content.document.createElement("div");
+ content.document.body.append(node);
+ const highlighter = new AccessibleHighlighter(env);
+ await highlighter.isReady;
+ const infobar = highlighter.accessibleInfobar;
+ const bounds = {
+ x: 0,
+ y: 0,
+ w: 250,
+ h: 100,
+ };
+
+ const tests = [
+ {
+ desc: "Infobar is shown with no keyboard audit content when no audit.",
+ },
+ {
+ desc: "Infobar is shown with no keyboard audit content when audit is null.",
+ audit: null,
+ },
+ {
+ desc:
+ "Infobar is shown with no keyboard audit content when empty " +
+ "keyboard audit.",
+ audit: { [AUDIT_TYPE.KEYBOARD]: null },
+ },
+ {
+ desc: "Infobar is shown with keyboard audit content for an error.",
+ audit: {
+ [AUDIT_TYPE.KEYBOARD]: {
+ score: FAIL,
+ issue: INTERACTIVE_NO_ACTION,
+ },
+ },
+ },
+ {
+ desc: "Infobar is shown with keyboard audit content for a warning.",
+ audit: {
+ [AUDIT_TYPE.KEYBOARD]: {
+ score: WARNING,
+ issue: FOCUSABLE_NO_SEMANTICS,
+ },
+ },
+ },
+ ];
+
+ for (const test of tests) {
+ const { desc, audit } = test;
+
+ info(desc);
+ highlighter.show(node, { ...bounds, audit });
+ checkKeyboard(infobar, audit && audit[AUDIT_TYPE.KEYBOARD]);
+ highlighter.hide();
+ }
+ });
+ }
+ );
+});
diff --git a/devtools/server/tests/browser/browser_accessibility_infobar_audit_text_label.js b/devtools/server/tests/browser/browser_accessibility_infobar_audit_text_label.js
new file mode 100644
index 0000000000..a4e2d895ee
--- /dev/null
+++ b/devtools/server/tests/browser/browser_accessibility_infobar_audit_text_label.js
@@ -0,0 +1,164 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Checks for the AccessibleHighlighter's infobar component and its text label
+// audit.
+
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: MAIN_DOMAIN + "doc_accessibility_infobar.html",
+ },
+ async function (browser) {
+ await SpecialPowers.spawn(browser, [], async function () {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ const {
+ HighlighterEnvironment,
+ } = require("resource://devtools/server/actors/highlighters.js");
+ const {
+ AccessibleHighlighter,
+ } = require("resource://devtools/server/actors/highlighters/accessible.js");
+ const {
+ LocalizationHelper,
+ } = require("resource://devtools/shared/l10n.js");
+ const L10N = new LocalizationHelper(
+ "devtools/shared/locales/accessibility.properties"
+ );
+
+ const {
+ accessibility: {
+ AUDIT_TYPE,
+ ISSUE_TYPE: {
+ [AUDIT_TYPE.TEXT_LABEL]: {
+ DIALOG_NO_NAME,
+ FORM_NO_VISIBLE_NAME,
+ TOOLBAR_NO_NAME,
+ },
+ },
+ SCORES: { BEST_PRACTICES, FAIL, WARNING },
+ },
+ } = require("resource://devtools/shared/constants.js");
+
+ /**
+ * Checks for updated content for an infobar.
+ *
+ * @param {Object} infobar
+ * Accessible highlighter's infobar component.
+ * @param {Object} audit
+ * Audit information that is passed on highlighter show.
+ */
+ function checkTextLabel(infobar, audit) {
+ const { issue, score } = audit || {};
+ let expected = "";
+ if (issue) {
+ const { ISSUE_TO_INFOBAR_LABEL_MAP } =
+ infobar.audit.reports[AUDIT_TYPE.TEXT_LABEL].constructor;
+ expected = L10N.getStr(ISSUE_TO_INFOBAR_LABEL_MAP[issue]);
+ }
+
+ is(
+ infobar.getTextContent("text-label"),
+ expected,
+ "infobar text label audit text content is correct"
+ );
+ if (score) {
+ ok(infobar.getElement("text-label").classList.contains(score));
+ }
+ }
+
+ // Start testing. First, create highlighter environment and initialize.
+ const env = new HighlighterEnvironment();
+ env.initFromWindow(content.window);
+
+ // Wait for loading highlighter environment content to complete before creating the
+ // highlighter.
+ await new Promise(resolve => {
+ const doc = env.document;
+
+ function onContentLoaded() {
+ if (
+ doc.readyState === "interactive" ||
+ doc.readyState === "complete"
+ ) {
+ resolve();
+ } else {
+ doc.addEventListener("DOMContentLoaded", onContentLoaded, {
+ once: true,
+ });
+ }
+ }
+
+ onContentLoaded();
+ });
+
+ // Now, we can test the Infobar's audit content.
+ const node = content.document.createElement("div");
+ content.document.body.append(node);
+ const highlighter = new AccessibleHighlighter(env);
+ await highlighter.isReady;
+ const infobar = highlighter.accessibleInfobar;
+ const bounds = {
+ x: 0,
+ y: 0,
+ w: 250,
+ h: 100,
+ };
+
+ const tests = [
+ {
+ desc: "Infobar is shown with no text label audit content when no audit.",
+ },
+ {
+ desc: "Infobar is shown with no text label audit content when audit is null.",
+ audit: null,
+ },
+ {
+ desc:
+ "Infobar is shown with no text label audit content when empty " +
+ "text label audit.",
+ audit: { [AUDIT_TYPE.TEXT_LABEL]: null },
+ },
+ {
+ desc: "Infobar is shown with text label audit content for an error.",
+ audit: {
+ [AUDIT_TYPE.TEXT_LABEL]: { score: FAIL, issue: TOOLBAR_NO_NAME },
+ },
+ },
+ {
+ desc: "Infobar is shown with text label audit content for a warning.",
+ audit: {
+ [AUDIT_TYPE.TEXT_LABEL]: {
+ score: WARNING,
+ issue: FORM_NO_VISIBLE_NAME,
+ },
+ },
+ },
+ {
+ desc: "Infobar is shown with text label audit content for best practices.",
+ audit: {
+ [AUDIT_TYPE.TEXT_LABEL]: {
+ score: BEST_PRACTICES,
+ issue: DIALOG_NO_NAME,
+ },
+ },
+ },
+ ];
+
+ for (const test of tests) {
+ const { desc, audit } = test;
+
+ info(desc);
+ highlighter.show(node, { ...bounds, audit });
+ checkTextLabel(infobar, audit && audit[AUDIT_TYPE.TEXT_LABEL]);
+ highlighter.hide();
+ }
+ });
+ }
+ );
+});
diff --git a/devtools/server/tests/browser/browser_accessibility_infobar_show.js b/devtools/server/tests/browser/browser_accessibility_infobar_show.js
new file mode 100644
index 0000000000..9fedf6d3b4
--- /dev/null
+++ b/devtools/server/tests/browser/browser_accessibility_infobar_show.js
@@ -0,0 +1,181 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Checks for the AccessibleHighlighter's and XULWindowHighlighter's infobar components.
+
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: MAIN_DOMAIN + "doc_accessibility_infobar.html",
+ },
+ async function (browser) {
+ await SpecialPowers.spawn(browser, [], async function () {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ const {
+ HighlighterEnvironment,
+ } = require("resource://devtools/server/actors/highlighters.js");
+ const {
+ AccessibleHighlighter,
+ } = require("resource://devtools/server/actors/highlighters/accessible.js");
+
+ /**
+ * Get whether or not infobar container is hidden.
+ *
+ * @param {Object} infobar
+ * Accessible highlighter's infobar component.
+ * @return {String|null} If the infobar container is hidden.
+ */
+ function isContainerHidden(infobar) {
+ return !!infobar
+ .getElement("infobar-container")
+ .getAttribute("hidden");
+ }
+
+ /**
+ * Get name of accessible object.
+ *
+ * @param {Object} infobar
+ * Accessible highlighter's infobar component.
+ * @return {String} The text content of the infobar-name element.
+ */
+ function getName(infobar) {
+ return infobar.getTextContent("infobar-name");
+ }
+
+ /**
+ * Get role of accessible object.
+ *
+ * @param {Object} infobar
+ * Accessible highlighter's infobar component.
+ * @return {String} The text content of the infobar-role element.
+ */
+ function getRole(infobar) {
+ return infobar.getTextContent("infobar-role");
+ }
+
+ /**
+ * Checks for updated content for an infobar with valid bounds.
+ *
+ * @param {Object} infobar
+ * Accessible highlighter's infobar component.
+ * @param {Object} options
+ * Options to pass for the highlighter's show method.
+ * Available options:
+ * - {String} role
+ * Role value of the accessible.
+ * - {String} name
+ * Name value of the accessible.
+ * - {Boolean} shouldBeHidden
+ * If the infobar component should be hidden.
+ */
+ function checkInfobar(infobar, { shouldBeHidden, role, name }) {
+ is(
+ isContainerHidden(infobar),
+ shouldBeHidden,
+ "Infobar's hidden state is correct."
+ );
+
+ if (shouldBeHidden) {
+ return;
+ }
+
+ is(getRole(infobar), role, "infobarRole text content is correct");
+ is(
+ getName(infobar),
+ `"${name}"`,
+ "infoBarName text content is correct"
+ );
+ }
+
+ /**
+ * Checks for updated content of an infobar with valid bounds.
+ *
+ * @param {Element} node
+ * Node to check infobar content on.
+ * @param {Object} highlighter
+ * Accessible highlighter.
+ */
+ function testInfobar(node, highlighter) {
+ const infobar = highlighter.accessibleInfobar;
+ const bounds = {
+ x: 0,
+ y: 0,
+ w: 250,
+ h: 100,
+ };
+
+ info("Check that infobar is shown with valid bounds.");
+ highlighter.show(node, {
+ ...bounds,
+ role: "button",
+ name: "Accessible Button",
+ });
+
+ checkInfobar(infobar, {
+ role: "button",
+ name: "Accessible Button",
+ shouldBeHidden: false,
+ });
+ highlighter.hide();
+
+ info("Check that infobar is hidden after .hide() is called.");
+ checkInfobar(infobar, { shouldBeHidden: true });
+
+ info("Check to make sure content is updated with new options.");
+ highlighter.show(node, {
+ ...bounds,
+ name: "Test link",
+ role: "link",
+ });
+ checkInfobar(infobar, {
+ name: "Test link",
+ role: "link",
+ shouldBeHidden: false,
+ });
+ highlighter.hide();
+ }
+
+ // Start testing. First, create highlighter environment and initialize.
+ const env = new HighlighterEnvironment();
+ env.initFromWindow(content.window);
+
+ // Wait for loading highlighter environment content to complete before creating the
+ // highlighter.
+ await new Promise(resolve => {
+ const doc = env.document;
+
+ function onContentLoaded() {
+ if (
+ doc.readyState === "interactive" ||
+ doc.readyState === "complete"
+ ) {
+ resolve();
+ } else {
+ doc.addEventListener("DOMContentLoaded", onContentLoaded, {
+ once: true,
+ });
+ }
+ }
+
+ onContentLoaded();
+ });
+
+ // Now, we can test the Infobar and XULWindowInfobar components with their
+ // respective highlighters.
+ const node = content.document.createElement("div");
+ content.document.body.append(node);
+
+ info("Checks for Infobar's show method");
+ const highlighter = new AccessibleHighlighter(env);
+ await highlighter.isReady;
+ testInfobar(node, highlighter);
+ });
+ }
+ );
+});
diff --git a/devtools/server/tests/browser/browser_accessibility_keyboard_audit.js b/devtools/server/tests/browser/browser_accessibility_keyboard_audit.js
new file mode 100644
index 0000000000..fee9814b6c
--- /dev/null
+++ b/devtools/server/tests/browser/browser_accessibility_keyboard_audit.js
@@ -0,0 +1,367 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Checks functionality around text label audit for the AccessibleActor.
+ */
+
+const {
+ accessibility: {
+ AUDIT_TYPE: { KEYBOARD },
+ SCORES: { FAIL, WARNING },
+ ISSUE_TYPE: {
+ [KEYBOARD]: {
+ FOCUSABLE_NO_SEMANTICS,
+ FOCUSABLE_POSITIVE_TABINDEX,
+ INTERACTIVE_NOT_FOCUSABLE,
+ MOUSE_INTERACTIVE_ONLY,
+ NO_FOCUS_VISIBLE,
+ },
+ },
+ },
+} = require("resource://devtools/shared/constants.js");
+
+add_task(async function () {
+ const { target, walker, parentAccessibility, a11yWalker } =
+ await initAccessibilityFrontsForUrl(
+ `${MAIN_DOMAIN}doc_accessibility_keyboard_audit.html`
+ );
+
+ const tests = [
+ [
+ "Focusable element (styled button) with no semantics.",
+ "#button-1",
+ { score: WARNING, issue: FOCUSABLE_NO_SEMANTICS },
+ ],
+ ["Element (styled button) with no semantics.", "#button-2", null],
+ [
+ "Container element for out of order focusable element.",
+ "#input-container",
+ null,
+ ],
+ [
+ "Interactive element with focus out of order (-1).",
+ "#input-1",
+ {
+ score: FAIL,
+ issue: INTERACTIVE_NOT_FOCUSABLE,
+ },
+ ],
+ [
+ "Interactive element with focus out of order (-1) when disabled.",
+ "#input-2",
+ null,
+ ],
+ ["Interactive element when disabled.", "#input-3", null],
+ ["Focusable interactive element.", "#input-4", null],
+ [
+ "Interactive accesible (link with no attributes) with no accessible actions.",
+ "#link-1",
+ null,
+ ],
+ ["Interactive accessible (link with valid href).", "#link-2", null],
+ ["Interactive accessible (link with # as href).", "#link-3", null],
+ [
+ "Interactive accessible (link with empty string as href).",
+ "#link-4",
+ null,
+ ],
+ ["Interactive accessible with no tabindex.", "#button-3", null],
+ [
+ "Interactive accessible with -1 tabindex.",
+ "#button-4",
+ {
+ score: FAIL,
+ issue: INTERACTIVE_NOT_FOCUSABLE,
+ },
+ ],
+ ["Interactive accessible with 0 tabindex.", "#button-5", null],
+ [
+ "Interactive accessible with 1 tabindex.",
+ "#button-6",
+ { score: WARNING, issue: FOCUSABLE_POSITIVE_TABINDEX },
+ ],
+ [
+ "Focusable ARIA button with no focus styling.",
+ "#focusable-1",
+ { score: WARNING, issue: NO_FOCUS_VISIBLE },
+ ],
+ ["Focusable ARIA button with focus styling.", "#focusable-2", null],
+ ["Focusable ARIA button with browser styling.", "#focusable-3", null],
+ [
+ "Not focusable, non-semantic element that has a click handler.",
+ "#mouse-only-1",
+ { score: FAIL, issue: MOUSE_INTERACTIVE_ONLY },
+ ],
+ [
+ "Focusable, non-semantic element that has a click handler.",
+ "#focusable-4",
+ { score: WARNING, issue: FOCUSABLE_NO_SEMANTICS },
+ ],
+ [
+ "Not focusable, ARIA button that has a click handler.",
+ "#button-7",
+ { score: FAIL, issue: INTERACTIVE_NOT_FOCUSABLE },
+ ],
+ ["Focusable, ARIA button with a click handler.", "#button-8", null],
+ ["Regular image, no keyboard checks should flag an issue.", "#img-1", null],
+ [
+ "Image with a longdesc (accessible will have showlongdesc action).",
+ "#img-2",
+ null,
+ ],
+ [
+ "Clickable image with a longdesc (accessible will have click and showlongdesc actions).",
+ "#img-3",
+ { score: FAIL, issue: MOUSE_INTERACTIVE_ONLY },
+ ],
+ [
+ "Clickable image (accessible will have click action).",
+ "#img-4",
+ { score: FAIL, issue: MOUSE_INTERACTIVE_ONLY },
+ ],
+ ["Focusable button with aria-haspopup.", "#buttonmenu-1", null],
+ [
+ "Not focusable aria button with aria-haspopup.",
+ "#buttonmenu-2",
+ {
+ score: FAIL,
+ issue: INTERACTIVE_NOT_FOCUSABLE,
+ },
+ ],
+ ["Focusable checkbox.", "#checkbox-1", null],
+ ["Focusable select element size > 1", "#listbox-1", null],
+ ["Focusable select element with one option", "#combobox-1", null],
+ ["Focusable select element with no options", "#combobox-2", null],
+ ["Focusable select element with two options", "#combobox-3", null],
+ [
+ "Non-focusable aria combobox with one aria option.",
+ "#editcombobox-1",
+ null,
+ ],
+ ["Non-focusable aria combobox with no options.", "#editcombobox-2", null],
+ ["Focusable aria combobox with no options.", "#editcombobox-3", null],
+ [
+ "Non-focusable aria switch",
+ "#switch-1",
+ {
+ score: FAIL,
+ issue: INTERACTIVE_NOT_FOCUSABLE,
+ },
+ ],
+ ["Focusable aria switch", "#switch-2", null],
+ [
+ "Combobox list that is visible (has focusable state)",
+ "#owned_listbox",
+ null,
+ ],
+ [
+ "Mouse interactive, label that contains form element (linked)",
+ "#label-1",
+ null,
+ ],
+ ["Mouse interactive label for external element (linked)", "#label-2", null],
+ ["Not interactive unlinked label", "#label-3", null],
+ [
+ "Not interactive unlinked label with folloing form element",
+ "#label-4",
+ null,
+ ],
+ ["Image inside an anchor (href)", "#img-5", null],
+ ["Image inside an anchor (onmousedown)", "#img-6", null],
+ ["Image inside an anchor (onclick)", "#img-7", null],
+ ["Image inside an anchor (onmouseup)", "#img-8", null],
+ [
+ "Section with a collapse action from aria-expanded attribute",
+ "#section-1",
+ null,
+ ],
+ ["Tabindex -1 should not report an element as focusable", "#main", null],
+ [
+ "Not keyboard focusable element with no focus styling.",
+ "#not-keyboard-focusable-1",
+ null,
+ ],
+ ["Interactive grid that is not focusable.", "#grid-1", null],
+ ["Focusable interactive grid.", "#grid-2", null],
+ [
+ "Non interactive ARIA table does not need to be focusable.",
+ "#table-1",
+ null,
+ ],
+ [
+ "Focusable ARIA table does not have interactive semantics",
+ "#table-2",
+ { score: "WARNING", issue: "FOCUSABLE_NO_SEMANTICS" },
+ ],
+ ["Non interactive table does not need to be focusable.", "#table-3", null],
+ [
+ "Focusable table does not have interactive semantics",
+ "#table-4",
+ { score: "WARNING", issue: "FOCUSABLE_NO_SEMANTICS" },
+ ],
+ [
+ "Article that is not focusable is not considered interactive",
+ "#article-1",
+ null,
+ ],
+ ["Focusable article is considered interactive", "#article-2", null],
+ [
+ "Column header that is not focusable is not considered interactive (ARIA grid)",
+ "#columnheader-1",
+ null,
+ ],
+ [
+ "Column header that is not focusable is not considered interactive (ARIA table)",
+ "#columnheader-2",
+ null,
+ ],
+ [
+ "Column header that is not focusable is not considered interactive (table)",
+ "#columnheader-3",
+ null,
+ ],
+ [
+ "Column header that is focusable is considered interactive (table)",
+ "#columnheader-4",
+ null,
+ ],
+ [
+ "Column header that is not focusable is not considered interactive (table as ARIA grid)",
+ "#columnheader-5",
+ null,
+ ],
+ [
+ "Column header that is focusable is considered interactive (table as ARIA grid)",
+ "#columnheader-6",
+ null,
+ ],
+ [
+ "Row header that is not focusable is not considered interactive",
+ "#rowheader-1",
+ null,
+ ],
+ [
+ "Row header that is not focusable is not considered interactive",
+ "#rowheader-2",
+ null,
+ ],
+ [
+ "Row header that is not focusable is not considered interactive",
+ "#rowheader-3",
+ null,
+ ],
+ [
+ "Row header that is focusable is considered interactive",
+ "#rowheader-4",
+ null,
+ ],
+ [
+ "Row header that is not focusable is not considered interactive (table as ARIA grid)",
+ "#rowheader-5",
+ null,
+ ],
+ [
+ "Row header that is focusable is considered interactive (table as ARIA grid)",
+ "#rowheader-6",
+ null,
+ ],
+ [
+ "Gridcell that is not focusable is not considered interactive (ARIA grid)",
+ "#gridcell-1",
+ null,
+ ],
+ [
+ "Gridcell that is focusable is considered interactive (ARIA grid)",
+ "#gridcell-2",
+ null,
+ ],
+ [
+ "Gridcell that is not focusable is not considered interactive (table as ARIA grid)",
+ "#gridcell-3",
+ null,
+ ],
+ [
+ "Gridcell that is focusable is considered interactive (table as ARIA grid)",
+ "#gridcell-4",
+ null,
+ ],
+ [
+ "Tab list that is not focusable is not considered interactive",
+ "#tablist-1",
+ null,
+ ],
+ ["Focusable tab list is considered interactive", "#tablist-2", null],
+ [
+ "Scrollbar that is not focusable is not considered interactive",
+ "#scrollbar-1",
+ null,
+ ],
+ ["Focusable scrollbar is considered interactive", "#scrollbar-2", null],
+ [
+ "Separator that is not focusable is not considered interactive",
+ "#separator-1",
+ null,
+ ],
+ ["Focusable separator is considered interactive", "#separator-2", null],
+ [
+ "Toolbar that is not focusable is not considered interactive",
+ "#toolbar-1",
+ null,
+ ],
+ ["Focusable toolbar is considered interactive", "#toolbar-2", null],
+ [
+ "Menu popup that is not focusable is not considered interactive",
+ "#menu-1",
+ null,
+ ],
+ ["Focusable menu popup is considered interactive", "#menu-2", null],
+ [
+ "Menubar that is not focusable is not considered interactive",
+ "#menubar-1",
+ null,
+ ],
+ ["Focusable menubar is considered interactive", "#menubar-2", null],
+ ];
+
+ for (const [description, selector, expected] of tests) {
+ info(description);
+ const node = await walker.querySelector(walker.rootNode, selector);
+ const front = await a11yWalker.getAccessibleFor(node);
+ const audit = await front.audit({ types: [KEYBOARD] });
+ Assert.deepEqual(
+ audit[KEYBOARD],
+ expected,
+ `Audit result for ${selector} is correct.`
+ );
+ }
+
+ info("Text leaf inside a link (jump action is propagated to the text link)");
+ let node = await walker.querySelector(walker.rootNode, "#link-5");
+ let parent = await a11yWalker.getAccessibleFor(node);
+ let front = (await parent.children())[0];
+ let audit = await front.audit({ types: [KEYBOARD] });
+ Assert.deepEqual(
+ audit[KEYBOARD],
+ null,
+ "Text leafs are excluded from semantics rule."
+ );
+
+ info("Combobox list that is invisible");
+ node = await walker.querySelector(walker.rootNode, "#combobox-1");
+ parent = await a11yWalker.getAccessibleFor(node);
+ front = (await parent.children())[0];
+ audit = await front.audit({ types: [KEYBOARD] });
+ Assert.deepEqual(
+ audit[KEYBOARD],
+ null,
+ "Combobox lists (invisible) are excluded from semantics rule."
+ );
+
+ await waitForA11yShutdown(parentAccessibility);
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/server/tests/browser/browser_accessibility_node.js b/devtools/server/tests/browser/browser_accessibility_node.js
new file mode 100644
index 0000000000..7aff9d9a5d
--- /dev/null
+++ b/devtools/server/tests/browser/browser_accessibility_node.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";
+
+// Checks for the AccessibleActor
+
+add_task(async function () {
+ const { target, walker, a11yWalker, parentAccessibility } =
+ await initAccessibilityFrontsForUrl(MAIN_DOMAIN + "doc_accessibility.html");
+ const modifiers =
+ Services.appinfo.OS === "Darwin" ? "\u2303\u2325" : "Alt+Shift+";
+
+ const buttonNode = await walker.querySelector(walker.rootNode, "#button");
+ const accessibleFront = await a11yWalker.getAccessibleFor(buttonNode);
+
+ checkA11yFront(accessibleFront, {
+ name: "Accessible Button",
+ role: "button",
+ childCount: 1,
+ });
+
+ await accessibleFront.hydrate();
+
+ checkA11yFront(accessibleFront, {
+ name: "Accessible Button",
+ role: "button",
+ value: "",
+ description: "Accessibility Test",
+ keyboardShortcut: modifiers + "b",
+ childCount: 1,
+ domNodeType: 1,
+ indexInParent: 1,
+ states: ["focusable", "opaque", "enabled", "sensitive"],
+ actions: ["Press"],
+ attributes: {
+ "margin-top": "0px",
+ display: "inline-block",
+ "text-align": "center",
+ "text-indent": "0px",
+ "margin-left": "0px",
+ tag: "button",
+ "margin-right": "0px",
+ id: "button",
+ "margin-bottom": "0px",
+ },
+ });
+
+ info("Children");
+ const children = await accessibleFront.children();
+ is(children.length, 1, "Accessible Front has correct number of children");
+ checkA11yFront(children[0], {
+ name: "Accessible Button",
+ role: "text leaf",
+ });
+
+ info("Relations");
+ const labelNode = await walker.querySelector(walker.rootNode, "#label");
+ const controlNode = await walker.querySelector(walker.rootNode, "#control");
+ const labelAccessibleFront = await a11yWalker.getAccessibleFor(labelNode);
+ const controlAccessibleFront = await a11yWalker.getAccessibleFor(controlNode);
+ const docAccessibleFront = await a11yWalker.getAccessibleFor(walker.rootNode);
+ const labelRelations = await labelAccessibleFront.getRelations();
+ is(labelRelations.length, 2, "Label has correct number of relations");
+ is(labelRelations[0].type, "label for", "Label has a label for relation");
+ is(labelRelations[0].targets.length, 1, "Label is a label for one target");
+ is(
+ labelRelations[0].targets[0],
+ controlAccessibleFront,
+ "Label is a label for control accessible front"
+ );
+ is(
+ labelRelations[1].type,
+ "containing document",
+ "Label has a containing document relation"
+ );
+ is(
+ labelRelations[1].targets.length,
+ 1,
+ "Label is contained by just one document"
+ );
+ is(
+ labelRelations[1].targets[0],
+ docAccessibleFront,
+ "Label's containing document is a root document"
+ );
+
+ const controlRelations = await controlAccessibleFront.getRelations();
+ is(controlRelations.length, 3, "Control has correct number of relations");
+ is(controlRelations[2].type, "details", "Control has a details relation");
+ is(controlRelations[2].targets.length, 1, "Control has one details target");
+ const detailsNode = await walker.querySelector(walker.rootNode, "#details");
+ const detailsAccessibleFront = await a11yWalker.getAccessibleFor(detailsNode);
+ is(
+ controlRelations[2].targets[0],
+ detailsAccessibleFront,
+ "Control has correct details target"
+ );
+
+ info("Snapshot");
+ const snapshot = await controlAccessibleFront.snapshot();
+ Assert.deepEqual(snapshot, {
+ name: "Label",
+ role: "textbox",
+ actions: ["Activate"],
+ value: "",
+ nodeCssSelector: "#control",
+ nodeType: 1,
+ description: "",
+ keyboardShortcut: "",
+ childCount: 0,
+ indexInParent: 1,
+ states: [
+ "focusable",
+ "autocompletion",
+ "selectable text",
+ "editable",
+ "opaque",
+ "single line",
+ "enabled",
+ "sensitive",
+ ],
+ children: [],
+ attributes: {
+ "margin-left": "0px",
+ "text-align": "start",
+ "text-indent": "0px",
+ id: "control",
+ tag: "input",
+ "margin-top": "0px",
+ "margin-bottom": "0px",
+ "margin-right": "0px",
+ display: "inline-block",
+ "explicit-name": "true",
+ },
+ });
+
+ // Check that we're using ARIA role tokens for landmarks implicit in native
+ // markup.
+ const headerNode = await walker.querySelector(walker.rootNode, "#header");
+ const headerAccessibleFront = await a11yWalker.getAccessibleFor(headerNode);
+ checkA11yFront(headerAccessibleFront, {
+ name: null,
+ role: "banner",
+ childCount: 1,
+ });
+ const navNode = await walker.querySelector(walker.rootNode, "#nav");
+ const navAccessibleFront = await a11yWalker.getAccessibleFor(navNode);
+ checkA11yFront(navAccessibleFront, {
+ name: null,
+ role: "navigation",
+ childCount: 1,
+ });
+ const footerNode = await walker.querySelector(walker.rootNode, "#footer");
+ const footerAccessibleFront = await a11yWalker.getAccessibleFor(footerNode);
+ checkA11yFront(footerAccessibleFront, {
+ name: null,
+ role: "contentinfo",
+ childCount: 1,
+ });
+
+ await waitForA11yShutdown(parentAccessibility);
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/server/tests/browser/browser_accessibility_node_audit.js b/devtools/server/tests/browser/browser_accessibility_node_audit.js
new file mode 100644
index 0000000000..a3115a2846
--- /dev/null
+++ b/devtools/server/tests/browser/browser_accessibility_node_audit.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";
+
+/**
+ * Checks functionality around audit for the AccessibleActor. This includes
+ * tests for the return value when calling the audit method, payload of the
+ * corresponding event as well as the AccesibleFront state being up to date.
+ */
+
+const {
+ accessibility: { AUDIT_TYPE, SCORES },
+} = require("resource://devtools/shared/constants.js");
+const EMPTY_AUDIT = Object.keys(AUDIT_TYPE).reduce((audit, key) => {
+ audit[key] = null;
+ return audit;
+}, {});
+
+const EXPECTED_CONTRAST_DATA = {
+ value: 21,
+ color: [0, 0, 0, 1],
+ backgroundColor: [255, 255, 255, 1],
+ isLargeText: true,
+ score: SCORES.AAA,
+};
+
+const EMPTY_CONTRAST_AUDIT = {
+ [AUDIT_TYPE.CONTRAST]: null,
+};
+
+const CONTRAST_AUDIT = {
+ [AUDIT_TYPE.CONTRAST]: EXPECTED_CONTRAST_DATA,
+};
+
+const FULL_AUDIT = {
+ ...EMPTY_AUDIT,
+ [AUDIT_TYPE.CONTRAST]: EXPECTED_CONTRAST_DATA,
+};
+
+async function checkAudit(a11yWalker, node, expected, options) {
+ const front = await a11yWalker.getAccessibleFor(node);
+ const [textLeafNode] = await front.children();
+
+ const onAudited = textLeafNode.once("audited");
+ const audit = await textLeafNode.audit(options);
+ const auditFromEvent = await onAudited;
+
+ Assert.deepEqual(audit, expected.audit, "Audit results are correct.");
+ Assert.deepEqual(textLeafNode.checks, expected.checks, "Checks are correct.");
+ Assert.deepEqual(
+ auditFromEvent,
+ expected.audit,
+ "Audit results from event are correct."
+ );
+}
+
+add_task(async function () {
+ const { target, walker, a11yWalker, parentAccessibility } =
+ await initAccessibilityFrontsForUrl(
+ MAIN_DOMAIN + "doc_accessibility_infobar.html"
+ );
+
+ const headerNode = await walker.querySelector(walker.rootNode, "#h1");
+ await checkAudit(
+ a11yWalker,
+ headerNode,
+ { audit: CONTRAST_AUDIT, checks: CONTRAST_AUDIT },
+ { types: [AUDIT_TYPE.CONTRAST] }
+ );
+ await checkAudit(a11yWalker, headerNode, {
+ audit: FULL_AUDIT,
+ checks: FULL_AUDIT,
+ });
+ await checkAudit(
+ a11yWalker,
+ headerNode,
+ { audit: CONTRAST_AUDIT, checks: FULL_AUDIT },
+ { types: [AUDIT_TYPE.CONTRAST] }
+ );
+ await checkAudit(
+ a11yWalker,
+ headerNode,
+ { audit: FULL_AUDIT, checks: FULL_AUDIT },
+ { types: [] }
+ );
+
+ const paragraphNode = await walker.querySelector(walker.rootNode, "#p");
+ await checkAudit(
+ a11yWalker,
+ paragraphNode,
+ { audit: EMPTY_CONTRAST_AUDIT, checks: EMPTY_CONTRAST_AUDIT },
+ { types: [AUDIT_TYPE.CONTRAST] }
+ );
+ await checkAudit(a11yWalker, paragraphNode, {
+ audit: EMPTY_AUDIT,
+ checks: EMPTY_AUDIT,
+ });
+ await checkAudit(
+ a11yWalker,
+ paragraphNode,
+ { audit: EMPTY_CONTRAST_AUDIT, checks: EMPTY_AUDIT },
+ { types: [AUDIT_TYPE.CONTRAST] }
+ );
+ await checkAudit(
+ a11yWalker,
+ paragraphNode,
+ { audit: EMPTY_AUDIT, checks: EMPTY_AUDIT },
+ { types: [] }
+ );
+
+ await waitForA11yShutdown(parentAccessibility);
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/server/tests/browser/browser_accessibility_node_events.js b/devtools/server/tests/browser/browser_accessibility_node_events.js
new file mode 100644
index 0000000000..77a1e7892f
--- /dev/null
+++ b/devtools/server/tests/browser/browser_accessibility_node_events.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";
+
+// Checks for the AccessibleActor events
+
+add_task(async function () {
+ const { target, walker, a11yWalker, parentAccessibility } =
+ await initAccessibilityFrontsForUrl(MAIN_DOMAIN + "doc_accessibility.html");
+ const modifiers =
+ Services.appinfo.OS === "Darwin" ? "\u2303\u2325" : "Alt+Shift+";
+
+ const rootNode = await walker.getRootNode();
+ const a11yDoc = await a11yWalker.getAccessibleFor(rootNode);
+ const buttonNode = await walker.querySelector(walker.rootNode, "#button");
+ const accessibleFront = await a11yWalker.getAccessibleFor(buttonNode);
+ const sliderNode = await walker.querySelector(walker.rootNode, "#slider");
+ const accessibleSliderFront = await a11yWalker.getAccessibleFor(sliderNode);
+ const browser = gBrowser.selectedBrowser;
+
+ checkA11yFront(accessibleFront, {
+ name: "Accessible Button",
+ role: "button",
+ childCount: 1,
+ });
+
+ await accessibleFront.hydrate();
+
+ checkA11yFront(accessibleFront, {
+ name: "Accessible Button",
+ role: "button",
+ value: "",
+ description: "Accessibility Test",
+ keyboardShortcut: modifiers + "b",
+ childCount: 1,
+ domNodeType: 1,
+ indexInParent: 1,
+ states: ["focusable", "opaque", "enabled", "sensitive"],
+ actions: ["Press"],
+ attributes: {
+ "margin-top": "0px",
+ display: "inline-block",
+ "text-align": "center",
+ "text-indent": "0px",
+ "margin-left": "0px",
+ tag: "button",
+ "margin-right": "0px",
+ id: "button",
+ "margin-bottom": "0px",
+ },
+ });
+
+ info("Name change event");
+ await emitA11yEvent(
+ accessibleFront,
+ "name-change",
+ (name, parent) => {
+ checkA11yFront(accessibleFront, { name: "Renamed" });
+ checkA11yFront(parent, {}, a11yDoc);
+ },
+ () =>
+ SpecialPowers.spawn(browser, [], () =>
+ content.document
+ .getElementById("button")
+ .setAttribute("aria-label", "Renamed")
+ )
+ );
+
+ info("Description change event");
+ await emitA11yEvent(
+ accessibleFront,
+ "description-change",
+ () => checkA11yFront(accessibleFront, { description: "" }),
+ () =>
+ SpecialPowers.spawn(browser, [], () =>
+ content.document
+ .getElementById("button")
+ .removeAttribute("aria-describedby")
+ )
+ );
+
+ info("State change event");
+ const expectedStates = ["unavailable", "opaque"];
+ await emitA11yEvent(
+ accessibleFront,
+ "states-change",
+ newStates => {
+ checkA11yFront(accessibleFront, { states: expectedStates });
+ SimpleTest.isDeeply(newStates, expectedStates, "States are updated");
+ },
+ () =>
+ SpecialPowers.spawn(browser, [], () =>
+ content.document.getElementById("button").setAttribute("disabled", true)
+ )
+ );
+
+ info("Attributes change event");
+ await emitA11yEvent(
+ accessibleFront,
+ "attributes-change",
+ newAttrs => {
+ checkA11yFront(accessibleFront, {
+ attributes: {
+ "container-live": "polite",
+ display: "inline-block",
+ "event-from-input": "false",
+ "explicit-name": "true",
+ id: "button",
+ live: "polite",
+ "margin-bottom": "0px",
+ "margin-left": "0px",
+ "margin-right": "0px",
+ "margin-top": "0px",
+ tag: "button",
+ "text-align": "center",
+ "text-indent": "0px",
+ },
+ });
+ is(newAttrs.live, "polite", "Attributes are updated");
+ },
+ () =>
+ SpecialPowers.spawn(browser, [], () =>
+ content.document
+ .getElementById("button")
+ .setAttribute("aria-live", "polite")
+ )
+ );
+
+ info("Value change event");
+ await accessibleSliderFront.hydrate();
+ checkA11yFront(accessibleSliderFront, { value: "5" });
+ await emitA11yEvent(
+ accessibleSliderFront,
+ "value-change",
+ () => checkA11yFront(accessibleSliderFront, { value: "6" }),
+ () =>
+ SpecialPowers.spawn(browser, [], () =>
+ content.document
+ .getElementById("slider")
+ .setAttribute("aria-valuenow", "6")
+ )
+ );
+
+ info("Reorder event");
+ is(accessibleSliderFront.childCount, 1, "Slider has only 1 child");
+ const [firstChild] = await accessibleSliderFront.children();
+ await firstChild.hydrate();
+ is(
+ firstChild.indexInParent,
+ 0,
+ "Slider's first child has correct index in parent"
+ );
+ await emitA11yEvent(
+ accessibleSliderFront,
+ "reorder",
+ childCount => {
+ is(childCount, 2, "Child count is updated");
+ is(accessibleSliderFront.childCount, 2, "Child count is updated");
+ is(
+ firstChild.indexInParent,
+ 1,
+ "Slider's first child has an updated index in parent"
+ );
+ },
+ () =>
+ SpecialPowers.spawn(browser, [], () => {
+ const doc = content.document;
+ const slider = doc.getElementById("slider");
+ const button = doc.createElement("button");
+ button.innerText = "Slider button";
+ content.document
+ .getElementById("slider")
+ .insertBefore(button, slider.firstChild);
+ })
+ );
+
+ await emitA11yEvent(
+ firstChild,
+ "index-in-parent-change",
+ indexInParent =>
+ is(
+ indexInParent,
+ 0,
+ "Slider's first child has an updated index in parent"
+ ),
+ () =>
+ SpecialPowers.spawn(browser, [], () =>
+ content.document.getElementById("slider").firstChild.remove()
+ )
+ );
+
+ await waitForA11yShutdown(parentAccessibility);
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/server/tests/browser/browser_accessibility_node_tabbing_order_highlighter.js b/devtools/server/tests/browser/browser_accessibility_node_tabbing_order_highlighter.js
new file mode 100644
index 0000000000..adb47c0ec6
--- /dev/null
+++ b/devtools/server/tests/browser/browser_accessibility_node_tabbing_order_highlighter.js
@@ -0,0 +1,92 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Checks for the NodeTabbingOrderHighlighter.
+
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: MAIN_DOMAIN + "doc_accessibility_infobar.html",
+ },
+ async function (browser) {
+ await SpecialPowers.spawn(browser, [], async function () {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ const {
+ HighlighterEnvironment,
+ } = require("resource://devtools/server/actors/highlighters.js");
+ const {
+ NodeTabbingOrderHighlighter,
+ } = require("resource://devtools/server/actors/highlighters/node-tabbing-order.js");
+
+ // Checks for updated content for an infobar.
+ async function testShowHide(highlighter, node, index) {
+ const shown = highlighter.show(node, { index });
+ const infoBarText = highlighter.getElement("infobar-text");
+
+ ok(shown, "Highlighter is shown.");
+ is(
+ parseInt(infoBarText.getTextContent(), 10),
+ index,
+ "infobar text content is correct"
+ );
+
+ highlighter.hide();
+ }
+
+ // Start testing. First, create highlighter environment and initialize.
+ const env = new HighlighterEnvironment();
+ env.initFromWindow(content.window);
+
+ // Wait for loading highlighter environment content to complete before
+ // creating the highlighter.
+ await new Promise(resolve => {
+ const doc = env.document;
+
+ function onContentLoaded() {
+ if (
+ doc.readyState === "interactive" ||
+ doc.readyState === "complete"
+ ) {
+ resolve();
+ } else {
+ doc.addEventListener("DOMContentLoaded", onContentLoaded, {
+ once: true,
+ });
+ }
+ }
+
+ onContentLoaded();
+ });
+
+ // Now, we can test the Infobar's index content.
+ const node = content.document.createElement("div");
+ content.document.body.append(node);
+ const highlighter = new NodeTabbingOrderHighlighter(env);
+ await highlighter.isReady;
+
+ info("Showing Node tabbing order highlighter with index");
+ await testShowHide(highlighter, node, 1);
+
+ info("Showing Node tabbing order highlighter with new index");
+ await testShowHide(highlighter, node, 9);
+
+ info(
+ "Showing and highlighting focused node with the Node tabbing order highlighter"
+ );
+ highlighter.show(node, { index: 1 });
+ highlighter.updateFocus(true);
+ const { classList } = highlighter.getElement("root");
+ ok(classList.contains("focused"), "Focus styling is applied");
+ highlighter.updateFocus(false);
+ ok(!classList.contains("focused"), "Focus styling is removed");
+ highlighter.hide();
+ });
+ }
+ );
+});
diff --git a/devtools/server/tests/browser/browser_accessibility_simple.js b/devtools/server/tests/browser/browser_accessibility_simple.js
new file mode 100644
index 0000000000..518d4dbb99
--- /dev/null
+++ b/devtools/server/tests/browser/browser_accessibility_simple.js
@@ -0,0 +1,106 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const PREF_ACCESSIBILITY_FORCE_DISABLED = "accessibility.force_disabled";
+
+function checkAccessibilityState(accessibility, parentAccessibility, expected) {
+ const { enabled } = accessibility;
+ const { canBeDisabled, canBeEnabled } = parentAccessibility;
+ is(enabled, expected.enabled, "Enabled state is correct.");
+ is(canBeDisabled, expected.canBeDisabled, "canBeDisabled state is correct.");
+ is(canBeEnabled, expected.canBeEnabled, "canBeEnabled state is correct.");
+}
+
+// Simple checks for the AccessibilityActor and AccessibleWalkerActor
+
+add_task(async function () {
+ const {
+ walker: domWalker,
+ target,
+ accessibility,
+ parentAccessibility,
+ a11yWalker,
+ } = await initAccessibilityFrontsForUrl(
+ "data:text/html;charset=utf-8,<title>test</title><div></div>",
+ { enableByDefault: false }
+ );
+
+ ok(accessibility, "The AccessibilityFront was created");
+ ok(accessibility.getWalker, "The getWalker method exists");
+ ok(accessibility.getSimulator, "The getSimulator method exists");
+
+ ok(accessibility.accessibleWalkerFront, "Accessible walker was initialized");
+
+ is(
+ a11yWalker,
+ accessibility.accessibleWalkerFront,
+ "The AccessibleWalkerFront was returned"
+ );
+
+ const a11ySimulator = accessibility.simulatorFront;
+ ok(accessibility.simulatorFront, "Accessible simulator was initialized");
+ is(
+ a11ySimulator,
+ accessibility.simulatorFront,
+ "The SimulatorFront was returned"
+ );
+
+ checkAccessibilityState(accessibility, parentAccessibility, {
+ enabled: false,
+ canBeDisabled: true,
+ canBeEnabled: true,
+ });
+
+ info("Force disable accessibility service: updates canBeEnabled flag");
+ let onEvent = parentAccessibility.once("can-be-enabled-change");
+ Services.prefs.setIntPref(PREF_ACCESSIBILITY_FORCE_DISABLED, 1);
+ await onEvent;
+ checkAccessibilityState(accessibility, parentAccessibility, {
+ enabled: false,
+ canBeDisabled: true,
+ canBeEnabled: false,
+ });
+
+ info("Clear force disable accessibility service: updates canBeEnabled flag");
+ onEvent = parentAccessibility.once("can-be-enabled-change");
+ Services.prefs.clearUserPref(PREF_ACCESSIBILITY_FORCE_DISABLED);
+ await onEvent;
+ checkAccessibilityState(accessibility, parentAccessibility, {
+ enabled: false,
+ canBeDisabled: true,
+ canBeEnabled: true,
+ });
+
+ info("Initialize accessibility service");
+ const initEvent = accessibility.once("init");
+ await parentAccessibility.enable();
+ await waitForA11yInit();
+ await initEvent;
+ checkAccessibilityState(accessibility, parentAccessibility, {
+ enabled: true,
+ canBeDisabled: true,
+ canBeEnabled: true,
+ });
+
+ const rootNode = await domWalker.getRootNode();
+ const a11yDoc = await accessibility.accessibleWalkerFront.getAccessibleFor(
+ rootNode
+ );
+ ok(a11yDoc, "Accessible document actor is created");
+
+ info("Shutdown accessibility service");
+ const shutdownEvent = accessibility.once("shutdown");
+ await waitForA11yShutdown(parentAccessibility);
+ await shutdownEvent;
+ checkAccessibilityState(accessibility, parentAccessibility, {
+ enabled: false,
+ canBeDisabled: true,
+ canBeEnabled: true,
+ });
+
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/server/tests/browser/browser_accessibility_simulator.js b/devtools/server/tests/browser/browser_accessibility_simulator.js
new file mode 100644
index 0000000000..47e3b898a3
--- /dev/null
+++ b/devtools/server/tests/browser/browser_accessibility_simulator.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 {
+ accessibility: {
+ SIMULATION_TYPE: { PROTANOPIA },
+ },
+} = require("resource://devtools/shared/constants.js");
+const {
+ simulation: {
+ COLOR_TRANSFORMATION_MATRICES: {
+ PROTANOPIA: PROTANOPIA_MATRIX,
+ NONE: DEFAULT_MATRIX,
+ },
+ },
+} = require("resource://devtools/server/actors/accessibility/constants.js");
+
+// Checks for the SimulatorActor
+
+async function setup() {
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.window.testColorMatrix = function (actual, expected) {
+ for (const idx in actual) {
+ is(
+ actual[idx].toFixed(3),
+ expected[idx].toFixed(3),
+ "Color matrix value is set correctly."
+ );
+ }
+ };
+ });
+ SimpleTest.registerCleanupFunction(async function () {
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.window.testColorMatrix = null;
+ });
+ });
+}
+
+async function testSimulate(simulator, matrix, type = null) {
+ const matrixApplied = await simulator.simulate({ types: type ? [type] : [] });
+ ok(matrixApplied, "Simulation color matrix is successfully applied.");
+
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [[type, matrix]],
+ ([simulationType, simulationMatrix]) => {
+ const { window } = content;
+ info(
+ `Test that color matrix is set to ${
+ simulationType || "default"
+ } simulation values.`
+ );
+ window.testColorMatrix(
+ window.docShell.getColorMatrix(),
+ simulationMatrix
+ );
+ }
+ );
+}
+
+add_task(async function () {
+ const { target, accessibility } = await initAccessibilityFrontsForUrl(
+ MAIN_DOMAIN + "doc_accessibility.html",
+ { enableByDefault: false }
+ );
+
+ const simulator = accessibility.simulatorFront;
+ if (!simulator) {
+ ok(false, "Missing simulator actor.");
+ return;
+ }
+
+ await setup();
+
+ info("Test that protanopia is successfully simulated.");
+ await testSimulate(simulator, PROTANOPIA_MATRIX, PROTANOPIA);
+
+ info(
+ "Test that simulations are successfully removed by setting default color matrix."
+ );
+ await testSimulate(simulator, DEFAULT_MATRIX);
+
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/server/tests/browser/browser_accessibility_tabbing_order_highlighter.js b/devtools/server/tests/browser/browser_accessibility_tabbing_order_highlighter.js
new file mode 100644
index 0000000000..fb99534318
--- /dev/null
+++ b/devtools/server/tests/browser/browser_accessibility_tabbing_order_highlighter.js
@@ -0,0 +1,101 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Checks for the TabbingOrderHighlighter.
+
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: MAIN_DOMAIN + "doc_accessibility_infobar.html",
+ },
+ async function (browser) {
+ await SpecialPowers.spawn(browser, [], async function () {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ const {
+ HighlighterEnvironment,
+ } = require("resource://devtools/server/actors/highlighters.js");
+ const {
+ TabbingOrderHighlighter,
+ } = require("resource://devtools/server/actors/highlighters/tabbing-order.js");
+
+ // Start testing. First, create highlighter environment and initialize.
+ const env = new HighlighterEnvironment();
+ env.initFromWindow(content.window);
+
+ // Wait for loading highlighter environment content to complete before
+ // creating the highlighter.
+ await new Promise(resolve => {
+ const doc = env.document;
+
+ function onContentLoaded() {
+ if (
+ doc.readyState === "interactive" ||
+ doc.readyState === "complete"
+ ) {
+ resolve();
+ } else {
+ doc.addEventListener("DOMContentLoaded", onContentLoaded, {
+ once: true,
+ });
+ }
+ }
+
+ onContentLoaded();
+ });
+
+ // Now, we can test the Infobar's index content.
+ const node = content.document.createElement("div");
+ content.document.body.append(node);
+ const highlighter = new TabbingOrderHighlighter(env);
+ await highlighter.isReady;
+
+ info("Showing tabbing order highlighter for all tabbable nodes");
+ const { contentDOMReference, index } = await highlighter.show(
+ content.document,
+ {
+ index: 0,
+ }
+ );
+
+ is(
+ contentDOMReference,
+ null,
+ "No current element when at the end of the tab order"
+ );
+ is(index, 2, "Current index is correct");
+ is(
+ highlighter._highlighters.size,
+ 2,
+ "Number of node tabbing order highlighters is correct"
+ );
+ for (let i = 0; i < highlighter._highlighters.size; i++) {
+ const nodeHighlighter = [...highlighter._highlighters.values()][i];
+ const infoBarText = nodeHighlighter.getElement("infobar-text");
+
+ is(
+ parseInt(infoBarText.getTextContent(), 10),
+ i + 1,
+ "infobar text content is correct"
+ );
+ }
+
+ info("Showing focus highlighting");
+ const input = content.document.getElementById("input");
+ highlighter.updateFocus({ node: input, focused: true });
+ const nodeHighlighter = highlighter._highlighters.get(input);
+ const { classList } = nodeHighlighter.getElement("root");
+ ok(classList.contains("focused"), "Focus styling is applied");
+ highlighter.updateFocus({ node: input, focused: false });
+ ok(!classList.contains("focused"), "Focus styling is removed");
+
+ highlighter.hide();
+ });
+ }
+ );
+});
diff --git a/devtools/server/tests/browser/browser_accessibility_text_label_audit.js b/devtools/server/tests/browser/browser_accessibility_text_label_audit.js
new file mode 100644
index 0000000000..55afbdf936
--- /dev/null
+++ b/devtools/server/tests/browser/browser_accessibility_text_label_audit.js
@@ -0,0 +1,1134 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Checks functionality around text label audit for the AccessibleActor.
+ */
+
+const {
+ accessibility: {
+ AUDIT_TYPE: { TEXT_LABEL },
+ SCORES: { BEST_PRACTICES, FAIL, WARNING },
+ ISSUE_TYPE: {
+ [TEXT_LABEL]: {
+ DIALOG_NO_NAME,
+ DOCUMENT_NO_TITLE,
+ EMBED_NO_NAME,
+ FIGURE_NO_NAME,
+ FORM_FIELDSET_NO_NAME,
+ FORM_FIELDSET_NO_NAME_FROM_LEGEND,
+ FORM_NO_NAME,
+ FORM_NO_VISIBLE_NAME,
+ FORM_OPTGROUP_NO_NAME_FROM_LABEL,
+ HEADING_NO_CONTENT,
+ HEADING_NO_NAME,
+ IFRAME_NO_NAME_FROM_TITLE,
+ IMAGE_NO_NAME,
+ INTERACTIVE_NO_NAME,
+ MATHML_GLYPH_NO_NAME,
+ TOOLBAR_NO_NAME,
+ },
+ },
+ },
+} = require("resource://devtools/shared/constants.js");
+
+add_task(async function () {
+ const { target, walker, a11yWalker, parentAccessibility } =
+ await initAccessibilityFrontsForUrl(
+ `${MAIN_DOMAIN}doc_accessibility_text_label_audit.html`
+ );
+
+ const tests = [
+ ["Button menu with inner content", "#buttonmenu-1", null],
+ ["Button menu nested inside a <label>", "#buttonmenu-2", null],
+ [
+ "Button menu with no name",
+ "#buttonmenu-3",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Button menu with aria-label", "#buttonmenu-4", null],
+ ["Button menu with <label>", "#buttonmenu-5", null],
+ ["Button menu with aria-labelledby", "#buttonmenu-6", null],
+ ["Paragraph with inner content", "#p1", null],
+ ["Empty paragraph", "#p2", null],
+ [
+ "<canvas> with no name",
+ "#canvas-1",
+ { score: FAIL, issue: IMAGE_NO_NAME },
+ ],
+ ["<canvas> with aria-label", "#canvas-2", null],
+ ["<canvas> with aria-labelledby", "#canvas-3", null],
+ [
+ "<canvas> with inner content",
+ "#canvas-4",
+ { score: FAIL, issue: IMAGE_NO_NAME },
+ ],
+ [
+ "Checkbox with no name",
+ "#checkbox-1",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ [
+ "Checkbox with unrelated label",
+ "#checkbox-2",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ ["Checkbox nested inside a <label>", "#checkbox-3", null],
+ ["Checkbox with a label", "#checkbox-4", null],
+ [
+ "Checkbox with aria-label",
+ "#checkbox-5",
+ { score: WARNING, issue: FORM_NO_VISIBLE_NAME },
+ ],
+ ["Checkbox with aria-labelledby visible label", "#checkbox-6", null],
+ [
+ "Empty aria checkbox",
+ "#checkbox-7",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ [
+ "Aria checkbox with aria-label",
+ "#checkbox-8",
+ { score: WARNING, issue: FORM_NO_VISIBLE_NAME },
+ ],
+ ["Aria checkbox with aria-labelledby visible label", "#checkbox-9", null],
+ ["Menuitem checkbox with inner content", "#menuitemcheckbox-1", null],
+ [
+ "Menuitem checkbox with unlabelled inner content",
+ "#menuitemcheckbox-2",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Empty menuitem checkbox",
+ "#menuitemcheckbox-3",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Menuitem checkbox with no textual inner content",
+ "#menuitemcheckbox-4",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Menuitem checkbox with labelled inner content",
+ "#menuitemcheckbox-5",
+ null,
+ ],
+ [
+ "Menuitem checkbox with white space inner content",
+ "#menuitemcheckbox-6",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Column header with inner content", "#columnheader-1", null],
+ [
+ "Empty column header",
+ "#columnheader-2",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Column header with white space inner content",
+ "#columnheader-3",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Column header with aria-label", "#columnheader-4", null],
+ [
+ "Column header with empty aria-label",
+ "#columnheader-5",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Column header with white space aria-label",
+ "#columnheader-6",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Column header with aria-labelledby", "#columnheader-7", null],
+ ["Aria column header with inner content", "#columnheader-8", null],
+ [
+ "Empty aria column header",
+ "#columnheader-9",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Aria column header with white space inner content",
+ "#columnheader-10",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Aria column header with aria-label", "#columnheader-11", null],
+ [
+ "Aria column header with empty aria-label",
+ "#columnheader-12",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Aria column header with white space aria-label",
+ "#columnheader-13",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Aria column header with aria-labelledby", "#columnheader-14", null],
+ ["Combobox with a <label>", "#combobox-1", null],
+ [
+ "Combobox with no label",
+ "#combobox-2",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ [
+ "Combobox with unrelated label",
+ "#combobox-3",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ ["Combobox nested inside a label", "#combobox-4", null],
+ [
+ "Combobox with aria-label",
+ "#combobox-5",
+ { score: WARNING, issue: FORM_NO_VISIBLE_NAME },
+ ],
+ ["Combobox with aria-labelledby a visible label", "#combobox-6", null],
+ ["Combobox option with inner content", "#combobox-option-1", null],
+ [
+ "Combobox option with no inner content",
+ "#combobox-option-2",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Combobox option with white string inner content",
+ "#combobox-option-3",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Combobox option with label attribute", "#combobox-option-4", null],
+ [
+ "Combobox option with empty label attribute",
+ "#combobox-option-5",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Combobox option with white string label attribute",
+ "#combobox-option-6",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Svg diagram with no name",
+ "#diagram-1",
+ { score: FAIL, issue: IMAGE_NO_NAME },
+ ],
+ [
+ "Svg diagram with empty aria-label",
+ "#diagram-2",
+ { score: FAIL, issue: IMAGE_NO_NAME },
+ ],
+ ["Svg diagram with aria-label", "#diagram-3", null],
+ ["Svg diagram with aria-labelledby", "#diagram-4", null],
+ [
+ "Svg diagram with aria-labelledby an element with empty content",
+ "#diagram-5",
+ { score: FAIL, issue: IMAGE_NO_NAME },
+ ],
+ [
+ "Dialog with no name",
+ "#dialog-1",
+ { score: BEST_PRACTICES, issue: DIALOG_NO_NAME },
+ ],
+ [
+ "Dialog with empty aria-label",
+ "#dialog-2",
+ { score: BEST_PRACTICES, issue: DIALOG_NO_NAME },
+ ],
+ ["Dialog with aria-label", "#dialog-3", null],
+ ["Dialog with aria-labelledby", "#dialog-4", null],
+ [
+ "Aria dialog with no name",
+ "#dialog-5",
+ { score: BEST_PRACTICES, issue: DIALOG_NO_NAME },
+ ],
+ [
+ "Aria dialog with empty aria-label",
+ "#dialog-6",
+ { score: BEST_PRACTICES, issue: DIALOG_NO_NAME },
+ ],
+ ["Aria dialog with aria-label", "#dialog-7", null],
+ ["Aria dialog with aria-labelledby", "#dialog-8", null],
+ [
+ "Dialog with aria-labelledby an element with empty content",
+ "#dialog-9",
+ { score: BEST_PRACTICES, issue: DIALOG_NO_NAME },
+ ],
+ [
+ "Aria dialog with aria-labelledby an element with empty content",
+ "#dialog-10",
+ { score: BEST_PRACTICES, issue: DIALOG_NO_NAME },
+ ],
+ [
+ "Edit combobox with no name",
+ "#editcombobox-1",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ [
+ "Edit combobox with aria-label",
+ "#editcombobox-2",
+ { score: WARNING, issue: FORM_NO_VISIBLE_NAME },
+ ],
+ [
+ "Edit combobox with aria-labelled a visible label",
+ "#editcombobox-3",
+ null,
+ ],
+ ["Input nested inside a <label>", "#entry-1", null],
+ ["Input with no name", "#entry-2", { score: FAIL, issue: FORM_NO_NAME }],
+ [
+ "Input with aria-label",
+ "#entry-3",
+ { score: WARNING, issue: FORM_NO_VISIBLE_NAME },
+ ],
+ [
+ "Input with unrelated <label>",
+ "#entry-4",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ ["Input with <label>", "#entry-5", null],
+ ["Input with aria-labelledby", "#entry-6", null],
+ [
+ "Aria textbox with no name",
+ "#entry-7",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ [
+ "Aria textbox with aria-label",
+ "#entry-8",
+ { score: WARNING, issue: FORM_NO_VISIBLE_NAME },
+ ],
+ ["Aria textbox with aria-labelledby", "#entry-9", null],
+ ["Figure with <figcaption>", "#figure-1", null],
+ [
+ "Figore with no <figcaption>",
+ "#figure-2",
+ { score: BEST_PRACTICES, issue: FIGURE_NO_NAME },
+ ],
+ ["Aria figure with aria-labelledby", "#figure-3", null],
+ [
+ "Aria figure with aria-labelledby an element with empty content",
+ "#figure-4",
+ { score: BEST_PRACTICES, issue: FIGURE_NO_NAME },
+ ],
+ [
+ "Aria figure with no name",
+ "#figure-5",
+ { score: BEST_PRACTICES, issue: FIGURE_NO_NAME },
+ ],
+ ["Image with no alt text", "#img-1", { score: FAIL, issue: IMAGE_NO_NAME }],
+ ["Image with aria-label", "#img-2", null],
+ ["Image with aria-labelledby", "#img-3", null],
+ ["Image with alt text", "#img-4", null],
+ [
+ "Image with aria-labelledby an element with empty content",
+ "#img-5",
+ { score: FAIL, issue: IMAGE_NO_NAME },
+ ],
+ [
+ "Aria image with no name",
+ "#img-6",
+ { score: FAIL, issue: IMAGE_NO_NAME },
+ ],
+ ["Aria image with aria-label", "#img-7", null],
+ ["Aria image with aria-labelledby", "#img-8", null],
+ [
+ "Aria image with empty aria-label",
+ "#img-9",
+ { score: FAIL, issue: IMAGE_NO_NAME },
+ ],
+ [
+ "Aria image with aria-labelledby an element with empty content",
+ "#img-10",
+ { score: FAIL, issue: IMAGE_NO_NAME },
+ ],
+ ["<optgroup> with label", "#optgroup-1", null],
+ [
+ "<optgroup> with empty label",
+ "#optgroup-2",
+ { score: FAIL, issue: FORM_OPTGROUP_NO_NAME_FROM_LABEL },
+ ],
+ [
+ "<optgroup> with no label",
+ "#optgroup-3",
+ { score: FAIL, issue: FORM_OPTGROUP_NO_NAME_FROM_LABEL },
+ ],
+ [
+ "<optgroup> with aria-label",
+ "#optgroup-4",
+ { score: FAIL, issue: FORM_OPTGROUP_NO_NAME_FROM_LABEL },
+ ],
+ [
+ "<optgroup> with aria-labelledby",
+ "#optgroup-5",
+ { score: FAIL, issue: FORM_OPTGROUP_NO_NAME_FROM_LABEL },
+ ],
+ ["<fieldset> with <legend>", "#fieldset-1", null],
+ [
+ "<fieldset> with empty <legend>",
+ "#fieldset-2",
+ { score: FAIL, issue: FORM_FIELDSET_NO_NAME },
+ ],
+ [
+ "<fieldset> with no <legend>",
+ "#fieldset-3",
+ { score: FAIL, issue: FORM_FIELDSET_NO_NAME },
+ ],
+ [
+ "<fieldset> with aria-label",
+ "#fieldset-4",
+ { score: WARNING, issue: FORM_FIELDSET_NO_NAME_FROM_LEGEND },
+ ],
+ [
+ "<fieldset> with aria-labelledby",
+ "#fieldset-5",
+ { score: WARNING, issue: FORM_FIELDSET_NO_NAME_FROM_LEGEND },
+ ],
+ ["Empty <h1>", "#heading-1", { score: FAIL, issue: HEADING_NO_NAME }],
+ ["<h1> with inner content", "#heading-2", null],
+ [
+ "<h1> with white space inner content",
+ "#heading-3",
+ { score: FAIL, issue: HEADING_NO_NAME },
+ ],
+ [
+ "<h1> with aria-label",
+ "#heading-4",
+ { score: WARNING, issue: HEADING_NO_CONTENT },
+ ],
+ [
+ "<h1> with aria-labelledby",
+ "#heading-5",
+ { score: WARNING, issue: HEADING_NO_CONTENT },
+ ],
+ ["<h1> with inner content and aria-label", "#heading-6", null],
+ ["<h1> with inner content and aria-labelledby", "#heading-7", null],
+ [
+ "Empty aria heading",
+ "#heading-8",
+ { score: FAIL, issue: HEADING_NO_NAME },
+ ],
+ ["Aria heading with content", "#heading-9", null],
+ [
+ "Aria heading with white space inner content",
+ "#heading-10",
+ { score: FAIL, issue: HEADING_NO_NAME },
+ ],
+ [
+ "Aria heading with aria-label",
+ "#heading-11",
+ { score: WARNING, issue: HEADING_NO_CONTENT },
+ ],
+ [
+ "Aria heading with aria-labelledby",
+ "#heading-12",
+ { score: WARNING, issue: HEADING_NO_CONTENT },
+ ],
+ ["Aria heading with inner content and aria-label", "#heading-13", null],
+ [
+ "Aria heading with inner content and aria-labelledby",
+ "#heading-14",
+ null,
+ ],
+ [
+ "Image map with no name",
+ "#imagemap-1",
+ { score: FAIL, issue: IMAGE_NO_NAME },
+ ],
+ ["Image map with aria-label", "#imagemap-2", null],
+ ["Image map with aria-labelledby", "#imagemap-3", null],
+ ["Image map with alt attribute", "#imagemap-4", null],
+ [
+ "Image map with aria-labelledby an element with empty content",
+ "#imagemap-5",
+ { score: FAIL, issue: IMAGE_NO_NAME },
+ ],
+ ["<iframe> with title", "#iframe-1", null],
+ [
+ "<iframe> with empty title",
+ "#iframe-2",
+ { score: FAIL, issue: IFRAME_NO_NAME_FROM_TITLE },
+ ],
+ [
+ "<iframe> with no title",
+ "#iframe-3",
+ { score: FAIL, issue: IFRAME_NO_NAME_FROM_TITLE },
+ ],
+ [
+ "<iframe> with aria-label",
+ "#iframe-4",
+ { score: FAIL, issue: IFRAME_NO_NAME_FROM_TITLE },
+ ],
+ [
+ "<iframe> with aria-label and title",
+ "#iframe-5",
+ { score: FAIL, issue: IFRAME_NO_NAME_FROM_TITLE },
+ ],
+ [
+ "<object> with image data type and no name",
+ "#object-1",
+ { score: FAIL, issue: IMAGE_NO_NAME },
+ ],
+ ["<object> with image data type and aria-label", "#object-2", null],
+ ["<object> with image data type and aria-labelledby", "#object-3", null],
+ ["<object> with non-image data type", "#object-4", null],
+ [
+ "<embed> with image data type and no name",
+ "#embed-1",
+ { score: FAIL, issue: IMAGE_NO_NAME },
+ ],
+ [
+ "<embed> with video data type and no name",
+ "#embed-2",
+ { score: FAIL, issue: EMBED_NO_NAME },
+ ],
+ ["<embed> with video data type and aria-label", "#embed-3", null],
+ ["<embed> with video data type and aria-labelledby", "#embed-4", null],
+ ["Link with no inner content", "#link-1", null],
+ ["Link with inner content", "#link-2", null],
+ [
+ "Link with href and no inner content",
+ "#link-3",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Link with href and inner content", "#link-4", null],
+ [
+ "Link with empty href and no inner content",
+ "#link-5",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Link with empty href and inner content", "#link-6", null],
+ [
+ "Link with # href and no inner content",
+ "#link-7",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Link with # href and inner content", "#link-8", null],
+ [
+ "Link with non empty href and no inner content",
+ "#link-9",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Link with non empty href and inner content", "#link-10", null],
+ ["Link with aria-label", "#link-11", null],
+ ["Link with aria-labelledby", "#link-12", null],
+ [
+ "Aria link with no inner content",
+ "#link-13",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Aria link with inner content", "#link-14", null],
+ ["Aria link with aria-label", "#link-15", null],
+ ["Aria link with aria-labelledby", "#link-16", null],
+ ["<select> with a visible <label>", "#listbox-1", null],
+ [
+ "<select> with no name",
+ "#listbox-2",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ [
+ "<select> with unrelated <label>",
+ "#listbox-3",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ ["<select> nested inside a <label>", "#listbox-4", null],
+ [
+ "<select> with aria-label",
+ "#listbox-5",
+ { score: WARNING, issue: FORM_NO_VISIBLE_NAME },
+ ],
+ ["<select> with aria-labelledby a visible element", "#listbox-6", null],
+ [
+ "MathML glyph with no name",
+ "#mglyph-1",
+ { score: FAIL, issue: MATHML_GLYPH_NO_NAME },
+ ],
+ ["MathML glyph with aria-label", "#mglyph-2", null],
+ ["MathML glyph with aria-labelledby", "#mglyph-3", null],
+ ["MathML glyph with alt text", "#mglyph-4", null],
+ [
+ "MathML glyph with empty alt text",
+ "#mglyph-5",
+ { score: FAIL, issue: MATHML_GLYPH_NO_NAME },
+ ],
+ [
+ "MathML glyph with aria-labelledby an element with no inner content",
+ "#mglyph-6",
+ { score: FAIL, issue: MATHML_GLYPH_NO_NAME },
+ ],
+ [
+ "Aria menu item with no name",
+ "#menuitem-1",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Aria menu item with empty aria-label",
+ "#menuitem-2",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Aria menu item with aria-label", "#menuitem-3", null],
+ ["Aria menu item with aria-labelledby", "#menuitem-4", null],
+ [
+ "Aria menu item with aria-labelledby element with empty inner content",
+ "#menuitem-5",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Aria menu item with inner content", "#menuitem-6", null],
+ ["Option with inner content", "#option-1", null],
+ [
+ "Option with no inner content",
+ "#option-2",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Option with white space inner ",
+ "#option-3",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Option with a label", "#option-4", null],
+ [
+ "Option with an empty label",
+ "#option-5",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Option with a white space label",
+ "#option-6",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Aria option with inner content", "#option-7", null],
+ [
+ "Aria option with no inner content",
+ "#option-8",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Aria option with white space inner content",
+ "#option-9",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Aria option with aria-label", "#option-10", null],
+ [
+ "Aria option with empty aria-label",
+ "#option-11",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Aria option with white space aria-label",
+ "#option-12",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Aria option with aria-labelledby", "#option-13", null],
+ [
+ "Aria option with aria-labelledby an element with empty content",
+ "#option-14",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Aria option with aria-labelledby an element with white space content",
+ "#option-15",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Empty aria treeitem",
+ "#treeitem-1",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Aria treeitem with empty aria-label",
+ "#treeitem-2",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Aria treeitem with aria-label", "#treeitem-3", null],
+ ["Aria treeitem with aria-labelledby", "#treeitem-4", null],
+ [
+ "Aria treeitem with aria-labelledby an element with empty content",
+ "#treeitem-5",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Aria treeitem with inner content", "#treeitem-6", null],
+ [
+ "Aria tab with no content",
+ "#tab-1",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Aria tab with empty aria-label",
+ "#tab-2",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Aria tab with aria-label", "#tab-3", null],
+ ["Aria tab with aria-labelledby", "#tab-4", null],
+ [
+ "Aria tab with aria-labelledby an element with empty content",
+ "#tab-5",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Aria tab with inner content", "#tab-6", null],
+ ["Password nested inside a <label>", "#password-1", null],
+ ["Password no name", "#password-2", { score: FAIL, issue: FORM_NO_NAME }],
+ [
+ "Password with aria-label",
+ "#password-3",
+ { score: WARNING, issue: FORM_NO_VISIBLE_NAME },
+ ],
+ [
+ "Password with unrelated label",
+ "#password-4",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ ["Password with <label>", "#password-5", null],
+ ["Password with aria-labelledby a visible element", "#password-6", null],
+ ["<progress> nested inside a label", "#progress-1", null],
+ [
+ "<progress> with no name",
+ "#progress-2",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ [
+ "<progress> with aria-label",
+ "#progress-3",
+ { score: WARNING, issue: FORM_NO_VISIBLE_NAME },
+ ],
+ [
+ "<progress> with unrelated <label>",
+ "#progress-4",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ ["<progress> with <label>", "#progress-5", null],
+ ["<progress> with aria-labelledby a visible element", "#progress-6", null],
+ [
+ "Aria progressbar nested inside a <label>",
+ "#progress-7",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ [
+ "Aria progressbar with aria-labelledby a visible element",
+ "#progress-8",
+ null,
+ ],
+ [
+ "Aria progressbar no name",
+ "#progress-9",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ [
+ "Aria progressbar with aria-label",
+ "#progress-10",
+ { score: WARNING, issue: FORM_NO_VISIBLE_NAME },
+ ],
+ [
+ "Aria progressbar with unrelated <label>",
+ "#progress-11",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ [
+ "Aria progressbar with <label>",
+ "#progress-12",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ [
+ "Aria progressbar with aria-labelledby a visible <label>",
+ "#progress-13",
+ null,
+ ],
+ ["Button with inner content", "#button-1", null],
+ [
+ "Image button with no name",
+ "#button-2",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Button with no name",
+ "#button-3",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Image button with empty alt text",
+ "#button-4",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Image button with alt text", "#button-5", null],
+ [
+ "Button with white space inner content",
+ "#button-6",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Button inside a <label>", "#button-7", null],
+ ["Button with aria-label", "#button-8", null],
+ [
+ "Button with unrelated <label>",
+ "#button-9",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Button with <label>", "#button-10", null],
+ ["Button with aria-labelledby a visile <label>", "#button-11", null],
+ [
+ "Aria button inside a label",
+ "#button-12",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Aria button with aria-labelled by a <label>", "#button-13", null],
+ [
+ "Aria button with no content",
+ "#button-14",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Aria button with aria-label", "#button-15", null],
+ [
+ "Aria button with unrelated <label>",
+ "#button-16",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Aria button with <label>",
+ "#button-17",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Aria button with aria-labelledby a visible <label>", "#button-18", null],
+ ["Radio nested inside a label", "#radiobutton-1", null],
+ [
+ "Radio with no name",
+ "#radiobutton-2",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ [
+ "Radio with aria-label",
+ "#radiobutton-3",
+ { score: WARNING, issue: FORM_NO_VISIBLE_NAME },
+ ],
+ [
+ "Radio with unrelated <label>",
+ "#radiobutton-4",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ ["Radio with visible label>", "#radiobutton-5", null],
+ ["Radio with aria-labelledby a visible <label>", "#radiobutton-6", null],
+ [
+ "Aria radio with no name",
+ "#radiobutton-7",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ [
+ "Aria radio with aria-label",
+ "#radiobutton-8",
+ { score: WARNING, issue: FORM_NO_VISIBLE_NAME },
+ ],
+ [
+ "Aria radio with aria-labelledby a visible element",
+ "#radiobutton-9",
+ null,
+ ],
+ ["Aria menuitemradio with inner content", "#menuitemradio-1", null],
+ [
+ "Aria menuitemradio with no inner content",
+ "#menuitemradio-2",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Aria menuitemradio with white space inner content",
+ "#menuitemradio-3",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Rowheader with inner content", "#rowheader-1", null],
+ [
+ "Rowheader with no inner content",
+ "#rowheader-2",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Rowheader with white space inner content",
+ "#rowheader-3",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Rowheader with aria-label", "#rowheader-4", null],
+ [
+ "Rowheader with empty aria-label",
+ "#rowheader-5",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Rowheader with white space aria-label",
+ "#rowheader-6",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Rowheader with aria-labelledby", "#rowheader-7", null],
+ ["Aria rowheader with inner content", "#rowheader-8", null],
+ [
+ "Aria rowheader with no inner content",
+ "#rowheader-9",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Aria rowheader with white space inner content",
+ "#rowheader-10",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Aria rowheader with aria-label", "#rowheader-11", null],
+ [
+ "Aria rowheader with empty aria-label",
+ "#rowheader-12",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Aria rowheader with white space aria-label",
+ "#rowheader-13",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Aria rowheader with aria-labelledby", "#rowheader-14", null],
+ ["Slider nested inside a <label>", "#slider-1", null],
+ ["Slider with no name", "#slider-2", { score: FAIL, issue: FORM_NO_NAME }],
+ [
+ "Slider with aria-label",
+ "#slider-3",
+ { score: WARNING, issue: FORM_NO_VISIBLE_NAME },
+ ],
+ [
+ "Slider with unrelated <label>",
+ "#slider-4",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ ["Slider with a visible <label>", "#slider-5", null],
+ ["Slider with aria-labelled by a visible <label>", "#slider-6", null],
+ [
+ "Aria slider with no name",
+ "#slider-7",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ [
+ "Aria slider with aria-label",
+ "#slider-8",
+ { score: WARNING, issue: FORM_NO_VISIBLE_NAME },
+ ],
+ ["Aria slider with aria-labelledby a visible element", "#slider-9", null],
+ ["Number input inside a label", "#spinbutton-1", null],
+ [
+ "Number input with no label",
+ "#spinbutton-2",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ [
+ "Number input with aria-label",
+ "#spinbutton-3",
+ { score: WARNING, issue: FORM_NO_VISIBLE_NAME },
+ ],
+ [
+ "Number input with unrelated <label>",
+ "#spinbutton-4",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ ["Number input with visible <label>", "#spinbutton-5", null],
+ [
+ "Number input with aria-labelled by a visible <label>",
+ "#spinbutton-6",
+ null,
+ ],
+ [
+ "Aria spinbutton with no name",
+ "#spinbutton-7",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ [
+ "Aria spinbutton with aria-label",
+ "#spinbutton-8",
+ { score: WARNING, issue: FORM_NO_VISIBLE_NAME },
+ ],
+ [
+ "Aria spinbutton with aria-labelledby a visible element",
+ "#spinbutton-9",
+ null,
+ ],
+ [
+ "Aria switch with no name",
+ "#switch-1",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ [
+ "Aria switch wtih aria-label",
+ "#switch-2",
+ { score: WARNING, issue: FORM_NO_VISIBLE_NAME },
+ ],
+ ["Aria switch with aria-labelledby a visible element", "#switch-3", null],
+ [
+ "Aria switch with unrelated <label>",
+ "#switch-4",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ [
+ "Aria switch nested inside a <label>",
+ "#switch-5",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ // See bug: https://bugzilla.mozilla.org/show_bug.cgi?id=559770
+ // ["Meter inside a label", "#meter-1", null],
+ // See bug: https://bugzilla.mozilla.org/show_bug.cgi?id=559770
+ // ["Meter with no name", "#meter-2", { score: FAIL, issue: FORM_NO_NAME }],
+ // See bug: https://bugzilla.mozilla.org/show_bug.cgi?id=559770
+ // ["Meter with aria-label", "#meter-3",
+ // { score: WARNING, issue: FORM_NO_VISIBLE_NAME}],
+ // See bug: https://bugzilla.mozilla.org/show_bug.cgi?id=559770
+ // ["Meter with unrelated <label>", "#meter-4", { score: FAIL, issue: FORM_NO_NAME }],
+ ["Meter with visible <label>", "#meter-5", null],
+ ["Meter with aria-labelledby a visible <label>", "#meter-6", null],
+ // See bug: https://bugzilla.mozilla.org/show_bug.cgi?id=559770
+ // ["Aria meter with no name", "#meter-7", { score: FAIL, issue: FORM_NO_NAME }],
+ // See bug: https://bugzilla.mozilla.org/show_bug.cgi?id=559770
+ // ["Aria meter with aria-label", "#meter-8",
+ // { score: WARNING, issue: FORM_NO_VISIBLE_NAME}],
+ ["Aria meter with aria-labelledby a visible element", "#meter-9", null],
+ ["Toggle button with inner content", "#togglebutton-1", null],
+ [
+ "Image toggle button with no name",
+ "#togglebutton-2",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Empty toggle button",
+ "#togglebutton-3",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Image toggle button with empty alt text",
+ "#togglebutton-4",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Image toggle button with alt text", "#togglebutton-5", null],
+ [
+ "Toggle button with white space inner content",
+ "#togglebutton-6",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Toggle button nested inside a label", "#togglebutton-7", null],
+ ["Toggle button with aria-label", "#togglebutton-8", null],
+ [
+ "Toggle button with unrelated <label>",
+ "#togglebutton-9",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Toggle button with <label>", "#togglebutton-10", null],
+ [
+ "Toggle button with aria-labelled by a visible <label>",
+ "#togglebutton-11",
+ null,
+ ],
+ [
+ "Aria toggle button nested inside a label",
+ "#togglebutton-12",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Aria toggle button with aria-labelled by and nested inside a label",
+ "#togglebutton-13",
+ null,
+ ],
+ [
+ "Aria toggle button with no name",
+ "#togglebutton-14",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Aria toggle button with aria-label", "#togglebutton-15", null],
+ [
+ "Aria toggle button with unrelated <label>",
+ "#togglebutton-16",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Aria toggle button with <label>",
+ "#togglebutton-17",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Aria toggle button with aria-labelledby a visible <label>",
+ "#togglebutton-18",
+ null,
+ ],
+ ["Non-unique aria toolbar with aria-label", "#toolbar-1", null],
+ [
+ "Non-unique aria toolbar with no name (",
+ "#toolbar-2",
+ { score: FAIL, issue: TOOLBAR_NO_NAME },
+ ],
+ [
+ "Non-unique aAria toolbar with aria-labelledby an element with empty content",
+ "#toolbar-3",
+ { score: FAIL, issue: TOOLBAR_NO_NAME },
+ ],
+ ["Non-unique aria toolbar with aria-labelledby", "#toolbar-4", null],
+ ["SVGElement with role=img that has a title", "#svg-1", null],
+ ["SVGElement without role=img that has a title", "#svg-2", null],
+ [
+ "SVGElement with role=img and no name",
+ "#svg-3",
+ { score: FAIL, issue: IMAGE_NO_NAME },
+ ],
+ [
+ "SVGElement with no name",
+ "#svg-4",
+ { score: FAIL, issue: IMAGE_NO_NAME },
+ ],
+ ["SVGElement with a name", "#svg-5", null],
+ [
+ "SVGElement with a name and with ownerSVGElement with a name",
+ "#svg-6",
+ null,
+ ],
+ ["SVGElement with a title", "#svg-7", null],
+ [
+ "SVGElement with a name and with ownerSVGElement with a title",
+ "#svg-8",
+ null,
+ ],
+ ["SVGElement with role=img that has a title", "#svg-9", null],
+ [
+ "SVGElement with a name and with ownerSVGElement with role=img that has a title",
+ "#svg-10",
+ null,
+ ],
+ [
+ "SVGElement with role=img and no title",
+ "#svg-11",
+ { score: FAIL, issue: IMAGE_NO_NAME },
+ ],
+ [
+ "SVGElement with a name and with ownerSVGElement with role=img and no title",
+ "#svg-12",
+ null,
+ ],
+ ];
+
+ for (const [description, selector, expected] of tests) {
+ info(description);
+ const node = await walker.querySelector(walker.rootNode, selector);
+ const front = await a11yWalker.getAccessibleFor(node);
+ const audit = await front.audit({ types: [TEXT_LABEL] });
+ Assert.deepEqual(
+ audit[TEXT_LABEL],
+ expected,
+ `Audit result for ${selector} is correct.`
+ );
+ }
+
+ info("Test document rule:");
+ const front = await a11yWalker.getAccessibleFor(walker.rootNode);
+ let audit = await front.audit({ types: [TEXT_LABEL] });
+ info("Document with no title");
+ Assert.deepEqual(
+ audit[TEXT_LABEL],
+ { score: FAIL, issue: DOCUMENT_NO_TITLE },
+ "Audit result for document is correct."
+ );
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.document.title = "Hello world";
+ });
+ audit = await front.audit({ types: [TEXT_LABEL] });
+ info("Document with title");
+ Assert.deepEqual(
+ audit[TEXT_LABEL],
+ null,
+ "Audit result for document is correct."
+ );
+
+ await waitForA11yShutdown(parentAccessibility);
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/server/tests/browser/browser_accessibility_text_label_audit_frame.js b/devtools/server/tests/browser/browser_accessibility_text_label_audit_frame.js
new file mode 100644
index 0000000000..fbd56cee60
--- /dev/null
+++ b/devtools/server/tests/browser/browser_accessibility_text_label_audit_frame.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";
+
+/**
+ * Checks functionality around text label audit for the AccessibleActor that is
+ * created for frame elements.
+ */
+
+const {
+ accessibility: {
+ AUDIT_TYPE: { TEXT_LABEL },
+ SCORES: { FAIL },
+ ISSUE_TYPE: {
+ [TEXT_LABEL]: { FRAME_NO_NAME },
+ },
+ },
+} = require("resource://devtools/shared/constants.js");
+
+add_task(async function () {
+ const { target, walker, a11yWalker, parentAccessibility } =
+ await initAccessibilityFrontsForUrl(
+ `${MAIN_DOMAIN}doc_accessibility_text_label_audit_frame.html`
+ );
+
+ const tests = [
+ ["Frame with no name", "#frame-1", { score: FAIL, issue: FRAME_NO_NAME }],
+ ["Frame with aria-label", "#frame-2", null],
+ ];
+
+ for (const [description, selector, expected] of tests) {
+ info(description);
+ const node = await walker.querySelector(walker.rootNode, selector);
+ const front = await a11yWalker.getAccessibleFor(node);
+ const audit = await front.audit({ types: [TEXT_LABEL] });
+ Assert.deepEqual(
+ audit[TEXT_LABEL],
+ expected,
+ `Audit result for ${selector} is correct.`
+ );
+ }
+
+ await waitForA11yShutdown(parentAccessibility);
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/server/tests/browser/browser_accessibility_walker.js b/devtools/server/tests/browser/browser_accessibility_walker.js
new file mode 100644
index 0000000000..282a49b19a
--- /dev/null
+++ b/devtools/server/tests/browser/browser_accessibility_walker.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";
+
+// Checks for the AccessibleWalkerActor
+
+add_task(async function () {
+ const { target, walker, a11yWalker, parentAccessibility } =
+ await initAccessibilityFrontsForUrl(MAIN_DOMAIN + "doc_accessibility.html");
+
+ ok(a11yWalker, "The AccessibleWalkerFront was returned");
+ const rootNode = await walker.getRootNode();
+ const a11yDoc = await a11yWalker.getAccessibleFor(rootNode);
+ ok(a11yDoc, "The AccessibleFront for root doc is created");
+
+ const children = await a11yWalker.children();
+ is(
+ children.length,
+ 1,
+ "AccessibleWalker only has 1 child - root doc accessible"
+ );
+ is(
+ a11yDoc,
+ children[0],
+ "Root accessible must be AccessibleWalker's only child"
+ );
+
+ const buttonNode = await walker.querySelector(walker.rootNode, "#button");
+ const accessibleFront = await a11yWalker.getAccessibleFor(buttonNode);
+
+ checkA11yFront(accessibleFront, {
+ name: "Accessible Button",
+ role: "button",
+ });
+
+ const ancestry = await a11yWalker.getAncestry(accessibleFront);
+ is(ancestry.length, 1, "Button is a direct child of a root document.");
+ is(
+ ancestry[0].accessible,
+ a11yDoc,
+ "Button's only ancestor is a root document"
+ );
+ is(
+ ancestry[0].children.length,
+ 8,
+ "Root doc should have correct number of children"
+ );
+ ok(
+ ancestry[0].children.includes(accessibleFront),
+ "Button accessible front is in root doc's children"
+ );
+
+ const browser = gBrowser.selectedBrowser;
+
+ // Ensure name-change event is emitted by walker when cached accessible's name
+ // gets updated (via DOM manipularion).
+ await emitA11yEvent(
+ a11yWalker,
+ "name-change",
+ (front, parent) => {
+ checkA11yFront(front, { name: "Renamed" }, accessibleFront);
+ checkA11yFront(parent, {}, a11yDoc);
+ },
+ () =>
+ SpecialPowers.spawn(browser, [], () =>
+ content.document
+ .getElementById("button")
+ .setAttribute("aria-label", "Renamed")
+ )
+ );
+
+ // Ensure reorder event is emitted by walker when DOM tree changes.
+ let docChildren = await a11yDoc.children();
+ is(docChildren.length, 8, "Root doc should have correct number of children");
+
+ await emitA11yEvent(
+ a11yWalker,
+ "reorder",
+ front => checkA11yFront(front, {}, a11yDoc),
+ () =>
+ SpecialPowers.spawn(browser, [], () => {
+ const input = content.document.createElement("input");
+ input.type = "text";
+ input.title = "This is a tooltip";
+ input.value = "New input";
+ content.document.body.appendChild(input);
+ })
+ );
+
+ docChildren = await a11yDoc.children();
+ is(docChildren.length, 9, "Root doc should have correct number of children");
+
+ let shown = await a11yWalker.highlightAccessible(docChildren[0]);
+ ok(shown, "AccessibleHighlighter highlighted the node");
+
+ shown = await a11yWalker.highlightAccessible(a11yDoc);
+ ok(shown, "AccessibleHighlighter highlights the document correctly.");
+ await a11yWalker.unhighlight();
+
+ info("Checking AccessibleWalker picker functionality");
+ ok(a11yWalker.pick, "AccessibleWalker pick method exists");
+ ok(a11yWalker.pickAndFocus, "AccessibleWalker pickAndFocus method exists");
+ ok(a11yWalker.cancelPick, "AccessibleWalker cancelPick method exists");
+
+ let onPickerEvent = a11yWalker.once("picker-accessible-hovered");
+ await a11yWalker.pick();
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#h1",
+ { type: "mousemove" },
+ browser
+ );
+ let acc = await onPickerEvent;
+ checkA11yFront(acc, { name: "Accessibility Test" }, docChildren[0]);
+
+ onPickerEvent = a11yWalker.once("picker-accessible-previewed");
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#h1",
+ { shiftKey: true },
+ browser
+ );
+ acc = await onPickerEvent;
+ checkA11yFront(acc, { name: "Accessibility Test" }, docChildren[0]);
+
+ onPickerEvent = a11yWalker.once("picker-accessible-canceled");
+ await BrowserTestUtils.synthesizeKey(
+ "VK_ESCAPE",
+ { type: "keydown" },
+ browser
+ );
+ await onPickerEvent;
+
+ onPickerEvent = a11yWalker.once("picker-accessible-hovered");
+ await a11yWalker.pick();
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#h1",
+ { type: "mousemove" },
+ browser
+ );
+ await onPickerEvent;
+
+ onPickerEvent = a11yWalker.once("picker-accessible-picked");
+ await BrowserTestUtils.synthesizeMouseAtCenter("#h1", {}, browser);
+ acc = await onPickerEvent;
+ checkA11yFront(acc, { name: "Accessibility Test" }, docChildren[0]);
+
+ await a11yWalker.cancelPick();
+
+ info("Checking tabbing order highlighter");
+ let { elm, index } = await a11yWalker.showTabbingOrder(rootNode, 0);
+ isnot(!!elm, "No current element when at the end of the tab order");
+ is(index, 3, "Current index is correct");
+ await a11yWalker.hideTabbingOrder();
+
+ ({ elm, index } = await a11yWalker.showTabbingOrder(buttonNode, 0));
+ isnot(!!elm, "No current element when at the end of the tab order");
+ is(index, 2, "Current index is correct");
+ await a11yWalker.hideTabbingOrder();
+
+ info(
+ "When targets follow the WindowGlobal lifecycle and handle only one document, " +
+ "only check that the panel refreshes correctly and emit its 'reloaded' event"
+ );
+ await reloadBrowser();
+
+ await waitForA11yShutdown(parentAccessibility);
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/server/tests/browser/browser_accessibility_walker_audit.js b/devtools/server/tests/browser/browser_accessibility_walker_audit.js
new file mode 100644
index 0000000000..289023043f
--- /dev/null
+++ b/devtools/server/tests/browser/browser_accessibility_walker_audit.js
@@ -0,0 +1,155 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If 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 {
+ accessibility: { AUDIT_TYPE, ISSUE_TYPE, SCORES },
+} = require("resource://devtools/shared/constants.js");
+
+// Checks for the AccessibleWalkerActor audit.
+add_task(async function () {
+ const { target, a11yWalker, parentAccessibility } =
+ await initAccessibilityFrontsForUrl(
+ MAIN_DOMAIN + "doc_accessibility_audit.html"
+ );
+
+ const accessibles = [
+ {
+ name: "",
+ role: "document",
+ childCount: 2,
+ checks: {
+ [AUDIT_TYPE.CONTRAST]: null,
+ [AUDIT_TYPE.KEYBOARD]: null,
+ [AUDIT_TYPE.TEXT_LABEL]: {
+ score: SCORES.FAIL,
+ issue: ISSUE_TYPE.DOCUMENT_NO_TITLE,
+ },
+ },
+ },
+ {
+ name:
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do " +
+ "eiusmod tempor incididunt ut labore et dolore magna aliqua.",
+ role: "paragraph",
+ childCount: 1,
+ checks: {
+ [AUDIT_TYPE.CONTRAST]: null,
+ [AUDIT_TYPE.KEYBOARD]: null,
+ [AUDIT_TYPE.TEXT_LABEL]: null,
+ },
+ },
+ {
+ name:
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do " +
+ "eiusmod tempor incididunt ut labore et dolore magna aliqua.",
+ role: "text leaf",
+ childCount: 0,
+ checks: {
+ [AUDIT_TYPE.CONTRAST]: {
+ value: 4.0,
+ color: [255, 0, 0, 1],
+ backgroundColor: [255, 255, 255, 1],
+ isLargeText: false,
+ score: SCORES.FAIL,
+ },
+ [AUDIT_TYPE.KEYBOARD]: null,
+ [AUDIT_TYPE.TEXT_LABEL]: null,
+ },
+ },
+ {
+ name: "",
+ role: "paragraph",
+ childCount: 1,
+ checks: {
+ [AUDIT_TYPE.CONTRAST]: null,
+ [AUDIT_TYPE.KEYBOARD]: null,
+ [AUDIT_TYPE.TEXT_LABEL]: null,
+ },
+ },
+ {
+ name: "Accessible Paragraph",
+ role: "text leaf",
+ childCount: 0,
+ checks: {
+ [AUDIT_TYPE.CONTRAST]: {
+ value: 4.0,
+ color: [255, 0, 0, 1],
+ backgroundColor: [255, 255, 255, 1],
+ isLargeText: false,
+ score: SCORES.FAIL,
+ },
+ [AUDIT_TYPE.KEYBOARD]: null,
+ [AUDIT_TYPE.TEXT_LABEL]: null,
+ },
+ },
+ ];
+ const total = accessibles.length;
+ const auditProgress = [
+ { total, percentage: 20, completed: 1 },
+ { total, percentage: 40, completed: 2 },
+ { total, percentage: 60, completed: 3 },
+ { total, percentage: 80, completed: 4 },
+ { total, percentage: 100, completed: 5 },
+ ];
+
+ function findAccessible(name, role) {
+ return accessibles.find(
+ accessible => accessible.name === name && accessible.role === role
+ );
+ }
+
+ async function checkWalkerAudit(walker, expectedSize, options) {
+ info("Checking AccessibleWalker audit functionality");
+ const expectedProgress = Array.from(auditProgress);
+ const ancestries = await new Promise((resolve, reject) => {
+ const auditEventHandler = ({ type, ancestries: response, progress }) => {
+ switch (type) {
+ case "error":
+ walker.off("audit-event", auditEventHandler);
+ reject();
+ break;
+ case "completed":
+ walker.off("audit-event", auditEventHandler);
+ resolve(response);
+ is(expectedProgress.length, 0, "All progress events fired");
+ break;
+ case "progress":
+ SimpleTest.isDeeply(
+ progress,
+ expectedProgress.shift(),
+ "Progress data is correct"
+ );
+ break;
+ default:
+ break;
+ }
+ };
+
+ walker.on("audit-event", auditEventHandler);
+ walker.startAudit(options);
+ });
+
+ is(ancestries.length, expectedSize, "The size of ancestries is correct");
+ for (const ancestry of ancestries) {
+ for (const { accessible, children } of ancestry) {
+ checkA11yFront(
+ accessible,
+ findAccessible(accessibles.name, accessibles.role)
+ );
+ for (const child of children) {
+ checkA11yFront(child, findAccessible(child.name, child.role));
+ }
+ }
+ }
+ }
+
+ await checkWalkerAudit(a11yWalker, 3);
+ await checkWalkerAudit(a11yWalker, 2, { types: [AUDIT_TYPE.CONTRAST] });
+
+ await waitForA11yShutdown(parentAccessibility);
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/server/tests/browser/browser_actor_error.js b/devtools/server/tests/browser/browser_actor_error.js
new file mode 100644
index 0000000000..0c28d77cca
--- /dev/null
+++ b/devtools/server/tests/browser/browser_actor_error.js
@@ -0,0 +1,94 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test that clients can catch errors in actors.
+ */
+
+const ACTORS_URL =
+ "chrome://mochitests/content/browser/devtools/server/tests/browser/error-actor.js";
+
+add_task(async function test_old_actor() {
+ DevToolsServer.init();
+ DevToolsServer.registerAllActors();
+
+ ActorRegistry.registerModule(ACTORS_URL, {
+ prefix: "error",
+ constructor: "ErrorActor",
+ type: { global: true },
+ });
+
+ const transport = DevToolsServer.connectPipe();
+ const gClient = new DevToolsClient(transport);
+ await gClient.connect();
+
+ const { errorActor } = await gClient.mainRoot.rootForm;
+ ok(errorActor, "Found the error actor.");
+
+ await Assert.rejects(
+ gClient.request({ to: errorActor, type: "error" }),
+ err =>
+ err.error == "unknownError" &&
+ /error occurred while processing 'error/.test(err.message),
+ "The request should be rejected"
+ );
+
+ await gClient.close();
+});
+
+const TEST_ERRORS_ACTOR_URL =
+ "chrome://mochitests/content/browser/devtools/server/tests/browser/test-errors-actor.js";
+add_task(async function test_protocoljs_actor() {
+ DevToolsServer.init();
+ DevToolsServer.registerAllActors();
+
+ info("Register the new TestErrorsActor");
+ require(TEST_ERRORS_ACTOR_URL);
+ ActorRegistry.registerModule(TEST_ERRORS_ACTOR_URL, {
+ prefix: "testErrors",
+ constructor: "TestErrorsActor",
+ type: { global: true },
+ });
+
+ info("Create a DevTools client/server pair");
+ const transport = DevToolsServer.connectPipe();
+ const gClient = new DevToolsClient(transport);
+ await gClient.connect();
+
+ info("Retrieve a TestErrorsFront instance");
+ const testErrorsFront = await gClient.mainRoot.getFront("testErrors");
+ ok(testErrorsFront, "has a TestErrorsFront instance");
+
+ await Assert.rejects(testErrorsFront.throwsComponentsException(), e => {
+ return new RegExp(
+ `NS_ERROR_NOT_IMPLEMENTED from: ${testErrorsFront.actorID} ` +
+ `\\(${TEST_ERRORS_ACTOR_URL}:\\d+:\\d+\\)`
+ ).test(e.message);
+ });
+ await Assert.rejects(testErrorsFront.throwsException(), e => {
+ // Not asserting the specific error message here, as it changes depending
+ // on the channel.
+ return new RegExp(
+ `Protocol error \\(TypeError\\):.* from: ${testErrorsFront.actorID} ` +
+ `\\(${TEST_ERRORS_ACTOR_URL}:\\d+:\\d+\\)`
+ ).test(e.message);
+ });
+ await Assert.rejects(testErrorsFront.throwsJSError(), e => {
+ return new RegExp(
+ `Protocol error \\(Error\\): JSError from: ${testErrorsFront.actorID} ` +
+ `\\(${TEST_ERRORS_ACTOR_URL}:\\d+:\\d+\\)`
+ ).test(e.message);
+ });
+ await Assert.rejects(testErrorsFront.throwsString(), e => {
+ return new RegExp(`ErrorString from: ${testErrorsFront.actorID}`).test(
+ e.message
+ );
+ });
+ await Assert.rejects(testErrorsFront.throwsObject(), e => {
+ return new RegExp(`foo from: ${testErrorsFront.actorID}`).test(e.message);
+ });
+
+ await gClient.close();
+});
diff --git a/devtools/server/tests/browser/browser_animation_actor-lifetime.js b/devtools/server/tests/browser/browser_animation_actor-lifetime.js
new file mode 100644
index 0000000000..ef157d31fc
--- /dev/null
+++ b/devtools/server/tests/browser/browser_animation_actor-lifetime.js
@@ -0,0 +1,80 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for Bug 1247243
+
+add_task(async function () {
+ info("Setting up inspector and animation actors.");
+ const { animations, walker } = await initAnimationsFrontForUrl(
+ MAIN_DOMAIN + "animation-data.html"
+ );
+
+ info("Testing animated node actor");
+ const animatedNodeActor = await walker.querySelector(
+ walker.rootNode,
+ ".animated"
+ );
+ await animations.getAnimationPlayersForNode(animatedNodeActor);
+
+ await assertNumberOfAnimationActors(
+ 1,
+ "AnimationActor have 1 AnimationPlayerActors"
+ );
+
+ info("Testing AnimationPlayerActors release");
+ const stillNodeActor = await walker.querySelector(walker.rootNode, ".still");
+ await animations.getAnimationPlayersForNode(stillNodeActor);
+ await assertNumberOfAnimationActors(
+ 0,
+ "AnimationActor does not have any AnimationPlayerActors anymore"
+ );
+
+ info("Testing multi animated node actor");
+ const multiNodeActor = await walker.querySelector(walker.rootNode, ".multi");
+ await animations.getAnimationPlayersForNode(multiNodeActor);
+ await assertNumberOfAnimationActors(
+ 2,
+ "AnimationActor has now 2 AnimationPlayerActors"
+ );
+
+ info("Testing single animated node actor");
+ await animations.getAnimationPlayersForNode(animatedNodeActor);
+ await assertNumberOfAnimationActors(
+ 1,
+ "AnimationActor has only one AnimationPlayerActors"
+ );
+
+ info("Testing AnimationPlayerActors release again");
+ await animations.getAnimationPlayersForNode(stillNodeActor);
+ await assertNumberOfAnimationActors(
+ 0,
+ "AnimationActor does not have any AnimationPlayerActors anymore"
+ );
+
+ async function assertNumberOfAnimationActors(expected, message) {
+ const actors = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [[animations.actorID]],
+ function (actorID) {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ const {
+ DevToolsServer,
+ } = require("resource://devtools/server/devtools-server.js");
+ // Convert actorID to current compartment string otherwise
+ // searchAllConnectionsForActor is confused and won't find the actor.
+ actorID = String(actorID);
+ const animationActors =
+ DevToolsServer.searchAllConnectionsForActor(actorID);
+ if (!animationActors) {
+ return 0;
+ }
+ return animationActors.actors.length;
+ }
+ );
+ is(actors, expected, message);
+ }
+});
diff --git a/devtools/server/tests/browser/browser_animation_emitMutations.js b/devtools/server/tests/browser/browser_animation_emitMutations.js
new file mode 100644
index 0000000000..796418c937
--- /dev/null
+++ b/devtools/server/tests/browser/browser_animation_emitMutations.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the AnimationsActor emits events about changed animations on a
+// node after getAnimationPlayersForNode was called on that node.
+
+add_task(async function () {
+ const { target, walker, animations } = await initAnimationsFrontForUrl(
+ MAIN_DOMAIN + "animation.html"
+ );
+
+ info("Retrieve a non-animated node");
+ const node = await walker.querySelector(walker.rootNode, ".not-animated");
+
+ info("Retrieve the animation player for the node");
+ const players = await animations.getAnimationPlayersForNode(node);
+ is(players.length, 0, "The node has no animation players");
+
+ info("Listen for new animations");
+ let onMutations = once(animations, "mutations");
+
+ info("Add a couple of animation on the node");
+ await node.modifyAttributes([
+ { attributeName: "class", newValue: "multiple-animations" },
+ ]);
+ let changes = await onMutations;
+
+ ok(true, "The mutations event was emitted");
+ is(changes.length, 2, "There are 2 changes in the mutation event");
+ ok(
+ changes.every(({ type }) => type === "added"),
+ "Both changes are additions"
+ );
+
+ const names = changes.map(c => c.player.initialState.name).sort();
+ is(names[0], "glow", "The animation 'glow' was added");
+ is(names[1], "move", "The animation 'move' was added");
+
+ info("Store the 2 new players for comparing later");
+ const p1 = changes[0].player;
+ const p2 = changes[1].player;
+
+ info("Listen for removed animations");
+ onMutations = once(animations, "mutations");
+
+ info("Remove the animation css class on the node");
+ await node.modifyAttributes([
+ { attributeName: "class", newValue: "not-animated" },
+ ]);
+
+ changes = await onMutations;
+
+ ok(true, "The mutations event was emitted");
+ is(changes.length, 2, "There are 2 changes in the mutation event");
+ ok(
+ changes.every(({ type }) => type === "removed"),
+ "Both are removals"
+ );
+ ok(
+ changes[0].player === p1 || changes[0].player === p2,
+ "The first removed player was one of the previously added players"
+ );
+ ok(
+ changes[1].player === p1 || changes[1].player === p2,
+ "The second removed player was one of the previously added players"
+ );
+
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/server/tests/browser/browser_animation_getMultipleStates.js b/devtools/server/tests/browser/browser_animation_getMultipleStates.js
new file mode 100644
index 0000000000..77e6a7722b
--- /dev/null
+++ b/devtools/server/tests/browser/browser_animation_getMultipleStates.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check that the duration, iterationCount and delay are retrieved correctly for
+// multiple animations.
+
+add_task(async function () {
+ const { target, walker, animations } = await initAnimationsFrontForUrl(
+ MAIN_DOMAIN + "animation.html"
+ );
+
+ await playerHasAnInitialState(walker, animations);
+
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+async function playerHasAnInitialState(walker, animations) {
+ let state = await getAnimationStateForNode(
+ walker,
+ animations,
+ ".delayed-multiple-animations",
+ 0
+ );
+
+ is(state.duration, 500, "The duration of the first animation is correct");
+ is(
+ state.iterationCount,
+ 10,
+ "The iterationCount of the first animation is correct"
+ );
+ is(state.delay, 1000, "The delay of the first animation is correct");
+
+ state = await getAnimationStateForNode(
+ walker,
+ animations,
+ ".delayed-multiple-animations",
+ 1
+ );
+
+ is(state.duration, 1000, "The duration of the second animation is correct");
+ is(
+ state.iterationCount,
+ 30,
+ "The iterationCount of the second animation is correct"
+ );
+ is(state.delay, 750, "The delay of the second animation is correct");
+}
+
+async function getAnimationStateForNode(
+ walker,
+ animations,
+ selector,
+ playerIndex
+) {
+ const node = await walker.querySelector(walker.rootNode, selector);
+ const players = await animations.getAnimationPlayersForNode(node);
+ const player = players[playerIndex];
+ const state = await player.getCurrentState();
+ return state;
+}
diff --git a/devtools/server/tests/browser/browser_animation_getPlayers.js b/devtools/server/tests/browser/browser_animation_getPlayers.js
new file mode 100644
index 0000000000..de78bab02f
--- /dev/null
+++ b/devtools/server/tests/browser/browser_animation_getPlayers.js
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check the output of getAnimationPlayersForNode
+
+add_task(async function () {
+ const { target, walker, animations } = await initAnimationsFrontForUrl(
+ MAIN_DOMAIN + "animation.html"
+ );
+
+ await theRightNumberOfPlayersIsReturned(walker, animations);
+
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+async function theRightNumberOfPlayersIsReturned(walker, animations) {
+ let node = await walker.querySelector(walker.rootNode, ".not-animated");
+ let players = await animations.getAnimationPlayersForNode(node);
+ is(players.length, 0, "0 players were returned for the unanimated node");
+
+ node = await walker.querySelector(walker.rootNode, ".simple-animation");
+ players = await animations.getAnimationPlayersForNode(node);
+ is(players.length, 1, "One animation player was returned");
+
+ node = await walker.querySelector(walker.rootNode, ".multiple-animations");
+ players = await animations.getAnimationPlayersForNode(node);
+ is(players.length, 2, "Two animation players were returned");
+
+ node = await walker.querySelector(walker.rootNode, ".transition");
+ players = await animations.getAnimationPlayersForNode(node);
+ is(
+ players.length,
+ 1,
+ "One animation player was returned for the transitioned node"
+ );
+}
diff --git a/devtools/server/tests/browser/browser_animation_getStateAfterFinished.js b/devtools/server/tests/browser/browser_animation_getStateAfterFinished.js
new file mode 100644
index 0000000000..038d7b4911
--- /dev/null
+++ b/devtools/server/tests/browser/browser_animation_getStateAfterFinished.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+"use strict";
+
+// Check that the right duration/iterationCount/delay are retrieved even when
+// the node has multiple animations and one of them already ended before getting
+// the player objects.
+// See devtools/server/actors/animation.js |getPlayerIndex| for more
+// information.
+
+add_task(async function () {
+ const { target, walker, animations } = await initAnimationsFrontForUrl(
+ MAIN_DOMAIN + "animation.html"
+ );
+
+ info("Retrieve a non animated node");
+ const node = await walker.querySelector(walker.rootNode, ".not-animated");
+
+ info("Apply the multiple-animations-2 class to start the animations");
+ await node.modifyAttributes([
+ { attributeName: "class", newValue: "multiple-animations-2" },
+ ]);
+
+ info(
+ "Get the list of players, by the time this executes, the first, " +
+ "short, animation should have ended."
+ );
+ let players = await animations.getAnimationPlayersForNode(node);
+ if (players.length === 3) {
+ info("The short animation hasn't ended yet, wait for a bit.");
+ // The animation lasts for 500ms, so 1000ms should do it.
+ await new Promise(resolve => setTimeout(resolve, 1000));
+
+ info("And get the list again");
+ players = await animations.getAnimationPlayersForNode(node);
+ }
+
+ is(players.length, 2, "2 animations remain on the node");
+
+ is(
+ players[0].state.duration,
+ 100000,
+ "The duration of the first animation is correct"
+ );
+ is(
+ players[0].state.delay,
+ 2000,
+ "The delay of the first animation is correct"
+ );
+ is(
+ players[0].state.iterationCount,
+ null,
+ "The iterationCount of the first animation is correct"
+ );
+
+ is(
+ players[1].state.duration,
+ 300000,
+ "The duration of the second animation is correct"
+ );
+ is(
+ players[1].state.delay,
+ 1000,
+ "The delay of the second animation is correct"
+ );
+ is(
+ players[1].state.iterationCount,
+ 100,
+ "The iterationCount of the second animation is correct"
+ );
+
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/server/tests/browser/browser_animation_getSubTreeAnimations.js b/devtools/server/tests/browser/browser_animation_getSubTreeAnimations.js
new file mode 100644
index 0000000000..0a8a420c18
--- /dev/null
+++ b/devtools/server/tests/browser/browser_animation_getSubTreeAnimations.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check that the AnimationsActor can retrieve all animations inside a node's
+// subtree (but not going into iframes).
+
+const URL = MAIN_DOMAIN + "animation.html";
+
+// Import inspector's shared head.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/inspector/test/shared-head.js",
+ this
+);
+
+add_task(async function () {
+ info("Creating a test document with 2 iframes containing animated nodes");
+
+ const { inspector, target, walker, animations } =
+ await initAnimationsFrontForUrl(
+ "data:text/html;charset=utf-8," +
+ "<iframe id='iframe' src='" +
+ URL +
+ "'></iframe>"
+ );
+
+ info("Try retrieving all animations from the root doc's <body> node");
+ const rootBody = await walker.querySelector(walker.rootNode, "body");
+ let players = await animations.getAnimationPlayersForNode(rootBody);
+ is(players.length, 0, "The node has no animation players");
+
+ info("Retrieve all animations from the iframe's <body> node");
+ const frameBody = await getNodeFrontInFrames(["#iframe", "body"], inspector);
+ const animationsForFrame = await frameBody.targetFront.getFront("animations");
+ players = await animationsForFrame.getAnimationPlayersForNode(frameBody);
+
+ // Testing for a hard-coded number of animations here would intermittently
+ // fail depending on how fast or slow the test is (indeed, the test page
+ // contains short transitions, and delayed animations). So just make sure we
+ // at least have the infinitely running animations.
+ Assert.greaterOrEqual(
+ players.length,
+ 4,
+ "All subtree animations were retrieved"
+ );
+
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/server/tests/browser/browser_animation_keepFinished.js b/devtools/server/tests/browser/browser_animation_keepFinished.js
new file mode 100644
index 0000000000..0adb98ad69
--- /dev/null
+++ b/devtools/server/tests/browser/browser_animation_keepFinished.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+"use strict";
+
+// Test that the AnimationsActor doesn't report finished animations as removed.
+// Indeed, animations that only have the "finished" playState can be modified
+// still, so we want the AnimationsActor to preserve the corresponding
+// AnimationPlayerActor.
+
+add_task(async function () {
+ const { target, walker, animations } = await initAnimationsFrontForUrl(
+ MAIN_DOMAIN + "animation.html"
+ );
+
+ info("Retrieve a non-animated node");
+ const node = await walker.querySelector(walker.rootNode, ".not-animated");
+
+ info("Retrieve the animation player for the node");
+ let players = await animations.getAnimationPlayersForNode(node);
+ is(players.length, 0, "The node has no animation players");
+
+ info("Listen for new animations");
+ let reportedMutations = [];
+ function onMutations(mutations) {
+ reportedMutations = [...reportedMutations, ...mutations];
+ }
+ animations.on("mutations", onMutations);
+
+ info("Add a short animation on the node");
+ await node.modifyAttributes([
+ { attributeName: "class", newValue: "short-animation" },
+ ]);
+
+ info("Wait for longer than the animation's duration");
+ await wait(2000);
+
+ players = await animations.getAnimationPlayersForNode(node);
+ is(players.length, 0, "The added animation is surely finished");
+
+ is(reportedMutations.length, 1, "Only one mutation was reported");
+ is(reportedMutations[0].type, "added", "The mutation was an addition");
+
+ animations.off("mutations", onMutations);
+
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+function wait(ms) {
+ return new Promise(resolve => {
+ setTimeout(resolve, ms);
+ });
+}
diff --git a/devtools/server/tests/browser/browser_animation_playPauseIframe.js b/devtools/server/tests/browser/browser_animation_playPauseIframe.js
new file mode 100644
index 0000000000..e10fceb0bc
--- /dev/null
+++ b/devtools/server/tests/browser/browser_animation_playPauseIframe.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check that the AnimationsActor can pause/play all animations even those
+// within iframes.
+
+const URL = MAIN_DOMAIN + "animation.html";
+
+// Import inspector's shared head.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/inspector/test/shared-head.js",
+ this
+);
+
+add_task(async function () {
+ info("Creating a test document with 2 iframes containing animated nodes");
+
+ const { inspector, target } = await initAnimationsFrontForUrl(
+ "data:text/html;charset=utf-8," +
+ "<iframe id='i1' src='" +
+ URL +
+ "'></iframe>" +
+ "<iframe id='i2' src='" +
+ URL +
+ "'></iframe>"
+ );
+
+ info("Getting the 2 iframe container nodes and animated nodes in them");
+ const nodeInFrame1 = await getNodeFrontInFrames(
+ ["#i1", ".simple-animation"],
+ inspector
+ );
+ const nodeInFrame2 = await getNodeFrontInFrames(
+ ["#i2", ".simple-animation"],
+ inspector
+ );
+
+ info("Pause all animations in the test document");
+ await toggleAndCheckStates(nodeInFrame1, "paused");
+ await toggleAndCheckStates(nodeInFrame2, "paused");
+
+ info("Play all animations in the test document");
+ await toggleAndCheckStates(nodeInFrame1, "running");
+ await toggleAndCheckStates(nodeInFrame2, "running");
+
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+async function toggleAndCheckStates(nodeFront, playState) {
+ const animations = await nodeFront.targetFront.getFront("animations");
+ const [player] = await animations.getAnimationPlayersForNode(nodeFront);
+
+ if (playState === "paused") {
+ await animations.pauseSome([player]);
+ } else {
+ await animations.playSome([player]);
+ }
+
+ info("Getting the AnimationPlayerFront for the test node");
+ await player.ready;
+ const state = await player.getCurrentState();
+ is(
+ state.playState,
+ playState,
+ "The playState of the test node is " + playState
+ );
+}
diff --git a/devtools/server/tests/browser/browser_animation_playPauseSeveral.js b/devtools/server/tests/browser/browser_animation_playPauseSeveral.js
new file mode 100644
index 0000000000..d478a801d0
--- /dev/null
+++ b/devtools/server/tests/browser/browser_animation_playPauseSeveral.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check that the AnimationsActor can pause/play a given list of animations at once.
+
+// List of selectors that match "all" animated nodes in the test page.
+// This list misses a bunch of animated nodes on purpose. Only the ones that
+// have infinite animations are listed. This is done to avoid intermittents
+// caused when finite animations are already done playing by the time the test
+// runs.
+const ALL_ANIMATED_NODES = [
+ ".simple-animation",
+ ".multiple-animations",
+ ".delayed-animation",
+];
+
+add_task(async function () {
+ const { target, walker, animations } = await initAnimationsFrontForUrl(
+ MAIN_DOMAIN + "animation.html"
+ );
+
+ info("Pause all animations in the test document");
+ await toggleAndCheckStates(walker, animations, ALL_ANIMATED_NODES, "paused");
+
+ info("Play all animations in the test document");
+ await toggleAndCheckStates(walker, animations, ALL_ANIMATED_NODES, "running");
+
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+async function toggleAndCheckStates(walker, animations, selectors, playState) {
+ info(
+ "Checking the playState of all the nodes that have infinite running " +
+ "animations"
+ );
+
+ for (const selector of selectors) {
+ const players = await getPlayersFor(walker, animations, selector);
+
+ if (playState === "paused") {
+ await animations.pauseSome(players);
+ } else {
+ await animations.playSome(players);
+ }
+
+ info("Getting the AnimationPlayerFront for node " + selector);
+ const player = players[0];
+ await checkPlayState(player, selector, playState);
+ }
+}
+
+async function getPlayersFor(walker, animations, selector) {
+ const node = await walker.querySelector(walker.rootNode, selector);
+ return animations.getAnimationPlayersForNode(node);
+}
+
+async function checkPlayState(player, selector, expectedState) {
+ const state = await player.getCurrentState();
+ is(
+ state.playState,
+ expectedState,
+ "The playState of node " + selector + " is " + expectedState
+ );
+}
diff --git a/devtools/server/tests/browser/browser_animation_playerState.js b/devtools/server/tests/browser/browser_animation_playerState.js
new file mode 100644
index 0000000000..e010b576b5
--- /dev/null
+++ b/devtools/server/tests/browser/browser_animation_playerState.js
@@ -0,0 +1,159 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check the animation player's initial state
+
+add_task(async function () {
+ const { target, walker, animations } = await initAnimationsFrontForUrl(
+ MAIN_DOMAIN + "animation.html"
+ );
+
+ await playerHasAnInitialState(walker, animations);
+ await playerStateIsCorrect(walker, animations);
+
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+async function playerHasAnInitialState(walker, animations) {
+ const node = await walker.querySelector(walker.rootNode, ".simple-animation");
+ const [player] = await animations.getAnimationPlayersForNode(node);
+
+ ok(player.initialState, "The player front has an initial state");
+ ok("startTime" in player.initialState, "Player's state has startTime");
+ ok("currentTime" in player.initialState, "Player's state has currentTime");
+ ok("playState" in player.initialState, "Player's state has playState");
+ ok("playbackRate" in player.initialState, "Player's state has playbackRate");
+ ok("name" in player.initialState, "Player's state has name");
+ ok("duration" in player.initialState, "Player's state has duration");
+ ok("delay" in player.initialState, "Player's state has delay");
+ ok(
+ "iterationCount" in player.initialState,
+ "Player's state has iterationCount"
+ );
+ ok("fill" in player.initialState, "Player's state has fill");
+ ok("easing" in player.initialState, "Player's state has easing");
+ ok("direction" in player.initialState, "Player's state has direction");
+ ok(
+ "isRunningOnCompositor" in player.initialState,
+ "Player's state has isRunningOnCompositor"
+ );
+ ok("type" in player.initialState, "Player's state has type");
+ ok(
+ "documentCurrentTime" in player.initialState,
+ "Player's state has documentCurrentTime"
+ );
+ ok("properties" in player.initialState, "Player's state has properties");
+}
+
+async function playerStateIsCorrect(walker, animations) {
+ info("Checking the state of the simple animation");
+
+ let player = await getAnimationPlayerForNode(
+ walker,
+ animations,
+ ".simple-animation",
+ 0
+ );
+ let state = await player.getCurrentState();
+ is(state.name, "move", "Name is correct");
+ is(state.duration, 200000, "Duration is correct");
+ // null = infinite count
+ is(state.iterationCount, null, "Iteration count is correct");
+ is(state.fill, "none", "Fill is correct");
+ is(state.easing, "linear", "Easing is correct");
+ is(state.direction, "normal", "Direction is correct");
+ is(state.playState, "running", "PlayState is correct");
+ is(state.playbackRate, 1, "PlaybackRate is correct");
+ is(state.type, "cssanimation", "Type is correct");
+
+ info("Checking the state of the transition");
+
+ player = await getAnimationPlayerForNode(
+ walker,
+ animations,
+ ".transition",
+ 0
+ );
+ state = await player.getCurrentState();
+ is(state.name, "width", "Transition name matches transition property");
+ is(state.duration, 500000, "Transition duration is correct");
+ // transitions run only once
+ is(state.iterationCount, 1, "Transition iteration count is correct");
+ is(state.fill, "backwards", "Transition fill is correct");
+ is(state.easing, "ease-out", "Transition easing is correct");
+ is(state.direction, "normal", "Transition direction is correct");
+ is(state.playState, "running", "Transition playState is correct");
+ is(state.playbackRate, 1, "Transition playbackRate is correct");
+ is(state.type, "csstransition", "Transition type is correct");
+ // check easing in properties
+ let properties = state.properties;
+ is(properties.length, 1, "Length of animated properties is correct");
+ let keyframes = properties[0].values;
+ is(keyframes.length, 2, "Transition length of keyframe is correct");
+ is(keyframes[0].easing, "linear", "Transition keyframes's easing is correct");
+
+ info("Checking the state of one of multiple animations on a node");
+
+ // Checking the 2nd player
+ player = await getAnimationPlayerForNode(
+ walker,
+ animations,
+ ".multiple-animations",
+ 1
+ );
+ state = await player.getCurrentState();
+ is(state.name, "glow", "The 2nd animation's name is correct");
+ is(state.duration, 100000, "The 2nd animation's duration is correct");
+ is(state.iterationCount, 5, "The 2nd animation's iteration count is correct");
+ is(state.fill, "both", "The 2nd animation's fill is correct");
+ is(state.easing, "linear", "The 2nd animation's easing is correct");
+ is(state.direction, "reverse", "The 2nd animation's direction is correct");
+ is(state.playState, "running", "The 2nd animation's playState is correct");
+ is(state.playbackRate, 1, "The 2nd animation's playbackRate is correct");
+ // chech easing in keyframe
+ properties = state.properties;
+ keyframes = properties[0].values;
+ is(keyframes.length, 2, "The 2nd animation's length of keyframe is correct");
+ is(
+ keyframes[0].easing,
+ "ease-out",
+ "The 2nd animation's easing of keyframes is correct"
+ );
+
+ info("Checking the state of an animation with delay");
+
+ player = await getAnimationPlayerForNode(
+ walker,
+ animations,
+ ".delayed-animation",
+ 0
+ );
+ state = await player.getCurrentState();
+ is(state.delay, 5000, "The animation delay is correct");
+
+ info("Checking the state of an transition with delay");
+
+ player = await getAnimationPlayerForNode(
+ walker,
+ animations,
+ ".delayed-transition",
+ 0
+ );
+ state = await player.getCurrentState();
+ is(state.delay, 3000, "The transition delay is correct");
+}
+
+async function getAnimationPlayerForNode(
+ walker,
+ animations,
+ nodeSelector,
+ index
+) {
+ const node = await walker.querySelector(walker.rootNode, nodeSelector);
+ const players = await animations.getAnimationPlayersForNode(node);
+ const player = players[index];
+ return player;
+}
diff --git a/devtools/server/tests/browser/browser_animation_reconstructState.js b/devtools/server/tests/browser/browser_animation_reconstructState.js
new file mode 100644
index 0000000000..d7174562d9
--- /dev/null
+++ b/devtools/server/tests/browser/browser_animation_reconstructState.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check that, even though the AnimationPlayerActor only sends the bits of its
+// state that change, the front reconstructs the whole state everytime.
+
+add_task(async function () {
+ const { target, walker, animations } = await initAnimationsFrontForUrl(
+ MAIN_DOMAIN + "animation.html"
+ );
+
+ await playerHasCompleteStateAtAllTimes(walker, animations);
+
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+async function playerHasCompleteStateAtAllTimes(walker, animations) {
+ const node = await walker.querySelector(walker.rootNode, ".simple-animation");
+ const [player] = await animations.getAnimationPlayersForNode(node);
+
+ // Get the list of state key names from the initialstate.
+ const keys = Object.keys(player.initialState);
+
+ // Get the state over and over again and check that the object returned
+ // contains all keys.
+ // Normally, only the currentTime will have changed in between 2 calls.
+ for (let i = 0; i < 10; i++) {
+ await player.refreshState();
+ keys.forEach(key => {
+ Assert.notStrictEqual(
+ typeof player.state[key],
+ "undefined",
+ "The state retrieved has key " + key
+ );
+ });
+ }
+}
diff --git a/devtools/server/tests/browser/browser_animation_refreshTransitions.js b/devtools/server/tests/browser/browser_animation_refreshTransitions.js
new file mode 100644
index 0000000000..a48ea90e3d
--- /dev/null
+++ b/devtools/server/tests/browser/browser_animation_refreshTransitions.js
@@ -0,0 +1,97 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// When a transition finishes, no "removed" event is sent because it may still
+// be used, but when it restarts again (transitions back), then a new
+// AnimationPlayerFront should be sent, and the old one should be removed.
+
+add_task(async function () {
+ const { target, walker, animations } = await initAnimationsFrontForUrl(
+ MAIN_DOMAIN + "animation.html"
+ );
+
+ info("Retrieve the test node");
+ const node = await walker.querySelector(walker.rootNode, ".all-transitions");
+
+ info("Retrieve the animation players for the node");
+ const players = await animations.getAnimationPlayersForNode(node);
+ is(players.length, 0, "The node has no animation players yet");
+
+ info("Play a transition by adding the expand class, wait for mutations");
+ let onMutations = expectMutationEvents(animations, 2);
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ const el = content.document.querySelector(".all-transitions");
+ el.classList.add("expand");
+ });
+ let reportedMutations = await onMutations;
+
+ is(reportedMutations.length, 2, "2 mutation events were received");
+ is(reportedMutations[0].type, "added", "The first event was 'added'");
+ is(reportedMutations[1].type, "added", "The second event was 'added'");
+
+ info("Wait for the transitions to be finished");
+ await waitForEnd(reportedMutations[0].player);
+ await waitForEnd(reportedMutations[1].player);
+
+ info("Play the transition back by removing the class, wait for mutations");
+ onMutations = expectMutationEvents(animations, 4);
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ const el = content.document.querySelector(".all-transitions");
+ el.classList.remove("expand");
+ });
+ reportedMutations = await onMutations;
+
+ is(reportedMutations.length, 4, "4 new mutation events were received");
+ is(
+ reportedMutations.filter(m => m.type === "removed").length,
+ 2,
+ "2 'removed' events were sent (for the old transitions)"
+ );
+ is(
+ reportedMutations.filter(m => m.type === "added").length,
+ 2,
+ "2 'added' events were sent (for the new transitions)"
+ );
+
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+function expectMutationEvents(animationsFront, nbOfEvents) {
+ return new Promise(resolve => {
+ let reportedMutations = [];
+ function onMutations(mutations) {
+ reportedMutations = [...reportedMutations, ...mutations];
+ info(
+ "Received " +
+ reportedMutations.length +
+ " mutation events, " +
+ "expecting " +
+ nbOfEvents
+ );
+ if (reportedMutations.length === nbOfEvents) {
+ animationsFront.off("mutations", onMutations);
+ resolve(reportedMutations);
+ }
+ }
+
+ info("Start listening for mutation events from the AnimationsFront");
+ animationsFront.on("mutations", onMutations);
+ });
+}
+
+async function waitForEnd(animationFront) {
+ let playState;
+ while (playState !== "finished") {
+ const state = await animationFront.getCurrentState();
+ playState = state.playState;
+ info(
+ "Wait for transition " +
+ animationFront.state.name +
+ " to finish, playState=" +
+ playState
+ );
+ }
+}
diff --git a/devtools/server/tests/browser/browser_animation_setCurrentTime.js b/devtools/server/tests/browser/browser_animation_setCurrentTime.js
new file mode 100644
index 0000000000..8f7228cdd8
--- /dev/null
+++ b/devtools/server/tests/browser/browser_animation_setCurrentTime.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check that the AnimationsActor allows changing many players' currentTimes at once.
+
+add_task(async function () {
+ const { target, walker, animations } = await initAnimationsFrontForUrl(
+ MAIN_DOMAIN + "animation.html"
+ );
+
+ await testSetCurrentTimes(walker, animations);
+
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+async function testSetCurrentTimes(walker, animations) {
+ ok(animations.setCurrentTimes, "The AnimationsActor has the right method");
+
+ info("Retrieve multiple animated node and its animation players");
+
+ const nodeMulti = await walker.querySelector(
+ walker.rootNode,
+ ".multiple-animations"
+ );
+ const players = await animations.getAnimationPlayersForNode(nodeMulti);
+
+ Assert.greater(players.length, 1, "Node has more than 1 animation player");
+
+ info("Try to set multiple current times at once");
+ // Assume that all animations were created at same time.
+ const createdTime = players[1].state.createdTime;
+ await animations.setCurrentTimes(players, createdTime + 500, true);
+
+ info("Get the states of players and verify their correctness");
+ for (let i = 0; i < players.length; i++) {
+ const state = await players[i].getCurrentState();
+ is(state.playState, "paused", `Player ${i + 1} is paused`);
+ is(
+ parseInt(state.currentTime.toPrecision(4), 10),
+ 500,
+ `Player ${i + 1} has the right currentTime`
+ );
+ }
+}
diff --git a/devtools/server/tests/browser/browser_animation_setPlaybackRate.js b/devtools/server/tests/browser/browser_animation_setPlaybackRate.js
new file mode 100644
index 0000000000..b14751b114
--- /dev/null
+++ b/devtools/server/tests/browser/browser_animation_setPlaybackRate.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check that a player's playbackRate can be changed, and that multiple players
+// can have their rates changed at the same time.
+
+add_task(async function () {
+ const { target, walker, animations } = await initAnimationsFrontForUrl(
+ MAIN_DOMAIN + "animation.html"
+ );
+
+ info("Retrieve an animated node");
+ let node = await walker.querySelector(walker.rootNode, ".simple-animation");
+
+ info("Retrieve the animation player for the node");
+ const [player] = await animations.getAnimationPlayersForNode(node);
+
+ info("Change the rate to 10");
+ await animations.setPlaybackRates([player], 10);
+
+ info("Query the state again");
+ let state = await player.getCurrentState();
+ is(state.playbackRate, 10, "The playbackRate was updated");
+
+ info("Change the rate back to 1");
+ await animations.setPlaybackRates([player], 1);
+
+ info("Query the state again");
+ state = await player.getCurrentState();
+ is(state.playbackRate, 1, "The playbackRate was changed back");
+
+ info("Retrieve several animation players and set their rates");
+ node = await walker.querySelector(walker.rootNode, "body");
+ const players = await animations.getAnimationPlayersForNode(node);
+
+ info("Change all animations in <body> to .5 rate");
+ await animations.setPlaybackRates(players, 0.5);
+
+ info("Query their states and check they are correct");
+ for (const animPlayer of players) {
+ const animPlayerState = await animPlayer.getCurrentState();
+ is(animPlayerState.playbackRate, 0.5, "The playbackRate was updated");
+ }
+
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/server/tests/browser/browser_animation_simple.js b/devtools/server/tests/browser/browser_animation_simple.js
new file mode 100644
index 0000000000..0dd8adfde9
--- /dev/null
+++ b/devtools/server/tests/browser/browser_animation_simple.js
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Simple checks for the AnimationsActor
+
+add_task(async function () {
+ const { target, walker, animations } = await initAnimationsFrontForUrl(
+ "data:text/html;charset=utf-8,<title>test</title><div></div>"
+ );
+
+ ok(animations, "The AnimationsFront was created");
+ ok(
+ animations.getAnimationPlayersForNode,
+ "The getAnimationPlayersForNode method exists"
+ );
+ ok(animations.pauseSome, "The pauseSome method exists");
+ ok(animations.playSome, "The playSome method exists");
+ ok(animations.setCurrentTimes, "The setCurrentTimes method exists");
+ ok(animations.setPlaybackRates, "The setPlaybackRates method exists");
+ ok(animations.setWalkerActor, "The setWalkerActor method exists");
+
+ let didThrow = false;
+ try {
+ await animations.getAnimationPlayersForNode(null);
+ } catch (e) {
+ didThrow = true;
+ }
+ ok(didThrow, "An exception was thrown for a missing NodeActor");
+
+ const invalidNode = await walker.querySelector(walker.rootNode, "title");
+ const players = await animations.getAnimationPlayersForNode(invalidNode);
+ ok(Array.isArray(players), "An array of players was returned");
+ is(players.length, 0, "0 players have been returned for the invalid node");
+
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/server/tests/browser/browser_animation_updatedState.js b/devtools/server/tests/browser/browser_animation_updatedState.js
new file mode 100644
index 0000000000..4b1420de52
--- /dev/null
+++ b/devtools/server/tests/browser/browser_animation_updatedState.js
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+"use strict";
+
+// Check the animation player's updated state
+
+add_task(async function () {
+ const { target, walker, animations } = await initAnimationsFrontForUrl(
+ MAIN_DOMAIN + "animation.html"
+ );
+
+ await playStateIsUpdatedDynamically(walker, animations);
+
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+async function playStateIsUpdatedDynamically(walker, animations) {
+ info("Getting the test node (which runs a very long animation)");
+ // The animation lasts for 100s, to avoid intermittents.
+ const node = await walker.querySelector(walker.rootNode, ".long-animation");
+
+ info("Getting the animation player front for this node");
+ const [player] = await animations.getAnimationPlayersForNode(node);
+
+ let state = await player.getCurrentState();
+ is(
+ state.playState,
+ "running",
+ "The playState is running while the animation is running"
+ );
+
+ info(
+ "Change the animation's currentTime to be near the end and wait for " +
+ "it to finish"
+ );
+ const onFinished = waitForAnimationPlayState(player, "finished");
+ // Set the currentTime to 98s, knowing that the animation lasts for 100s.
+ await animations.setCurrentTimes([player], 98 * 1000, false);
+ state = await onFinished;
+ is(
+ state.playState,
+ "finished",
+ "The animation has ended and the state has been updated"
+ );
+ Assert.greater(
+ state.currentTime,
+ player.initialState.currentTime,
+ "The currentTime has been updated"
+ );
+}
+
+async function waitForAnimationPlayState(player, playState) {
+ let state = {};
+ while (state.playState !== playState) {
+ state = await player.getCurrentState();
+ await wait(500);
+ }
+ return state;
+}
+
+function wait(ms) {
+ return new Promise(r => setTimeout(r, ms));
+}
diff --git a/devtools/server/tests/browser/browser_application_manifest.js b/devtools/server/tests/browser/browser_application_manifest.js
new file mode 100644
index 0000000000..c92a3c0a2f
--- /dev/null
+++ b/devtools/server/tests/browser/browser_application_manifest.js
@@ -0,0 +1,87 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Enable web manifest processing.
+Services.prefs.setBoolPref("dom.manifest.enabled", true);
+
+add_task(async function () {
+ info("Testing fetching a valid manifest");
+ const response = await fetchManifest("application-manifest-basic.html");
+
+ ok(
+ response.manifest && response.manifest.name == "FooApp",
+ "Returns an object populated with the manifest data"
+ );
+});
+
+add_task(async function () {
+ info("Testing fetching an existing manifest with invalid values");
+ const response = await fetchManifest("application-manifest-warnings.html");
+
+ ok(
+ response.manifest && response.manifest.moz_validation,
+ "Returns an object populated with the manifest data"
+ );
+
+ const warnings = response.manifest.moz_validation;
+ ok(
+ warnings.length === 1 &&
+ warnings[0].warn &&
+ warnings[0].warn.includes("name member to be a string"),
+ "The returned object contains the expected warning info"
+ );
+});
+
+add_task(async function () {
+ info("Testing fetching a manifest in a page that does not have one");
+ const response = await fetchManifest("application-manifest-no-manifest.html");
+
+ is(response.manifest, null, "Returns an object with a `null` manifest");
+ ok(!response.errorMessage, "Does not return an error message");
+});
+
+add_task(async function () {
+ info("Testing an error happening fetching a manifest");
+ // the page that we are testing contains an invalid URL for the manifest
+ const response = await fetchManifest(
+ "application-manifest-404-manifest.html"
+ );
+
+ is(response.manifest, null, "Returns an object with a `null` manifest");
+ ok(
+ response.errorMessage &&
+ response.errorMessage.toLowerCase().includes("404 - not found"),
+ "Returns the expected error message"
+ );
+});
+
+add_task(async function () {
+ info("Testing a validation error when fetching a manifest with invalid JSON");
+ const response = await fetchManifest(
+ "application-manifest-invalid-json.html"
+ );
+ ok(
+ response.manifest && response.manifest.moz_validation,
+ "Returns an object with validation data"
+ );
+ const validation = response.manifest.moz_validation;
+ ok(
+ validation.find(x => x.error && x.type === "json"),
+ "Has the expected error in the validation field"
+ );
+});
+
+async function fetchManifest(filename) {
+ const url = MAIN_DOMAIN + filename;
+ const target = await addTabTarget(url);
+
+ info("Initializing manifest front for tab");
+ const manifestFront = await target.getFront("manifest");
+
+ info("Fetching manifest");
+ const response = await manifestFront.fetchCanonicalManifest();
+
+ return response;
+}
diff --git a/devtools/server/tests/browser/browser_canvasframe_helper_01.js b/devtools/server/tests/browser/browser_canvasframe_helper_01.js
new file mode 100644
index 0000000000..14c947db7e
--- /dev/null
+++ b/devtools/server/tests/browser/browser_canvasframe_helper_01.js
@@ -0,0 +1,170 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Simple CanvasFrameAnonymousContentHelper tests.
+
+const TEST_URL =
+ "data:text/html;charset=utf-8,CanvasFrameAnonymousContentHelper test";
+
+add_task(async function () {
+ const tab = await addTab(TEST_URL);
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ const {
+ HighlighterEnvironment,
+ } = require("resource://devtools/server/actors/highlighters.js");
+ const {
+ CanvasFrameAnonymousContentHelper,
+ } = require("resource://devtools/server/actors/highlighters/utils/markup.js");
+ const doc = content.document;
+
+ const nodeBuilder = () => {
+ const root = doc.createElement("div");
+ const child = doc.createElement("div");
+ child.style = "width:200px;height:200px;background:red;";
+ child.id = "child-element";
+ child.className = "child-element";
+ child.textContent = "test element";
+ root.appendChild(child);
+ return root;
+ };
+
+ info("Building the helper");
+ const env = new HighlighterEnvironment();
+ env.initFromWindow(doc.defaultView);
+ const helper = new CanvasFrameAnonymousContentHelper(env, nodeBuilder);
+ await helper.initialize();
+
+ ok(
+ content.AnonymousContent.isInstance(helper.content),
+ "The helper owns the AnonymousContent object"
+ );
+ ok(
+ helper.getTextContentForElement,
+ "The helper has the getTextContentForElement method"
+ );
+ ok(
+ helper.setTextContentForElement,
+ "The helper has the setTextContentForElement method"
+ );
+ ok(
+ helper.setAttributeForElement,
+ "The helper has the setAttributeForElement method"
+ );
+ ok(
+ helper.getAttributeForElement,
+ "The helper has the getAttributeForElement method"
+ );
+ ok(
+ helper.removeAttributeForElement,
+ "The helper has the removeAttributeForElement method"
+ );
+ ok(
+ helper.addEventListenerForElement,
+ "The helper has the addEventListenerForElement method"
+ );
+ ok(
+ helper.removeEventListenerForElement,
+ "The helper has the removeEventListenerForElement method"
+ );
+ ok(helper.getElement, "The helper has the getElement method");
+ ok(helper.scaleRootElement, "The helper has the scaleRootElement method");
+
+ is(
+ helper.getTextContentForElement("child-element"),
+ "test element",
+ "The text content was retrieve correctly"
+ );
+ is(
+ helper.getAttributeForElement("child-element", "id"),
+ "child-element",
+ "The ID attribute was retrieve correctly"
+ );
+ is(
+ helper.getAttributeForElement("child-element", "class"),
+ "child-element",
+ "The class attribute was retrieve correctly"
+ );
+
+ const el = helper.getElement("child-element");
+ ok(el, "The DOMNode-like element was created");
+
+ is(
+ el.getTextContent(),
+ "test element",
+ "The text content was retrieve correctly"
+ );
+ is(
+ el.getAttribute("id"),
+ "child-element",
+ "The ID attribute was retrieve correctly"
+ );
+ is(
+ el.getAttribute("class"),
+ "child-element",
+ "The class attribute was retrieve correctly"
+ );
+
+ info("Test the toggle API");
+ el.classList.toggle("test"); // This will set the class
+ is(
+ el.getAttribute("class"),
+ "child-element test",
+ "After toggling the class 'test', the class attribute contained the 'test' class"
+ );
+ el.classList.toggle("test"); // This will remove the class
+ is(
+ el.getAttribute("class"),
+ "child-element",
+ "After toggling the class 'test' again, the class attribute removed the 'test' class"
+ );
+ el.classList.toggle("test", true); // This will set the class
+ is(
+ el.getAttribute("class"),
+ "child-element test",
+ "After toggling the class 'test' again and keeping force=true, the class attribute added the 'test' class"
+ );
+ el.classList.toggle("test", true); // This will keep the class set
+ is(
+ el.getAttribute("class"),
+ "child-element test",
+ "After toggling the class 'test' again and keeping force=true,the class attribute contained the 'test' class"
+ );
+ el.classList.toggle("test", false); // This will remove the class
+ is(
+ el.getAttribute("class"),
+ "child-element",
+ "After toggling the class 'test' again and keeping force=false, the class attribute removed the 'test' class"
+ );
+ el.classList.toggle("test", false); // This will keep the class removed
+ is(
+ el.getAttribute("class"),
+ "child-element",
+ "After toggling the class 'test' again and keeping force=false, the class attribute removed the 'test' class"
+ );
+
+ info("Destroying the helper");
+ helper.destroy();
+ env.destroy();
+
+ ok(
+ !helper.getTextContentForElement("child-element"),
+ "No text content was retrieved after the helper was destroyed"
+ );
+ ok(
+ !helper.getAttributeForElement("child-element", "id"),
+ "No ID attribute was retrieved after the helper was destroyed"
+ );
+ ok(
+ !helper.getAttributeForElement("child-element", "class"),
+ "No class attribute was retrieved after the helper was destroyed"
+ );
+ });
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/server/tests/browser/browser_canvasframe_helper_02.js b/devtools/server/tests/browser/browser_canvasframe_helper_02.js
new file mode 100644
index 0000000000..bd54a03933
--- /dev/null
+++ b/devtools/server/tests/browser/browser_canvasframe_helper_02.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 CanvasFrameAnonymousContentHelper does not insert content in
+// XUL windows.
+
+add_task(async function () {
+ const tab = await addTab(
+ "chrome://mochitests/content/browser/devtools/server/tests/browser/test-window.xhtml"
+ );
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ const {
+ HighlighterEnvironment,
+ } = require("resource://devtools/server/actors/highlighters.js");
+ const {
+ CanvasFrameAnonymousContentHelper,
+ } = require("resource://devtools/server/actors/highlighters/utils/markup.js");
+ const doc = content.document;
+
+ const nodeBuilder = () => {
+ const root = doc.createElement("div");
+ const child = doc.createElement("div");
+ child.style = "width:200px;height:200px;background:red;";
+ child.id = "child-element";
+ child.className = "child-element";
+ child.textContent = "test element";
+ root.appendChild(child);
+ return root;
+ };
+
+ info("Building the helper");
+ const env = new HighlighterEnvironment();
+ env.initFromWindow(doc.defaultView);
+ const helper = new CanvasFrameAnonymousContentHelper(env, nodeBuilder);
+
+ ok(!helper.content, "The AnonymousContent was not inserted in the window");
+ ok(
+ !helper.getTextContentForElement("child-element"),
+ "No text content is returned"
+ );
+
+ env.destroy();
+ helper.destroy();
+ });
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/server/tests/browser/browser_canvasframe_helper_03.js b/devtools/server/tests/browser/browser_canvasframe_helper_03.js
new file mode 100644
index 0000000000..52aa4b5a6f
--- /dev/null
+++ b/devtools/server/tests/browser/browser_canvasframe_helper_03.js
@@ -0,0 +1,129 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the CanvasFrameAnonymousContentHelper event handling mechanism.
+
+const TEST_URL =
+ "data:text/html;charset=utf-8,CanvasFrameAnonymousContentHelper test";
+
+add_task(async function () {
+ const tab = await addTab(TEST_URL);
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ const {
+ HighlighterEnvironment,
+ } = require("resource://devtools/server/actors/highlighters.js");
+ const {
+ CanvasFrameAnonymousContentHelper,
+ } = require("resource://devtools/server/actors/highlighters/utils/markup.js");
+ const doc = content.document;
+
+ const nodeBuilder = () => {
+ const root = doc.createElement("div");
+ const child = doc.createElement("div");
+ child.style =
+ "pointer-events:auto;width:200px;height:200px;background:red;";
+ child.id = "child-element";
+ child.className = "child-element";
+ root.appendChild(child);
+ return root;
+ };
+
+ info("Building the helper");
+ const env = new HighlighterEnvironment();
+ env.initFromWindow(doc.defaultView);
+ const helper = new CanvasFrameAnonymousContentHelper(env, nodeBuilder);
+ await helper.initialize();
+
+ const el = helper.getElement("child-element");
+
+ info("Adding an event listener on the inserted element");
+ let mouseDownHandled = 0;
+ function onMouseDown(e, id) {
+ is(
+ id,
+ "child-element",
+ "The mousedown event was triggered on the element"
+ );
+ ok(!e.originalTarget, "The originalTarget property isn't available");
+ mouseDownHandled++;
+ }
+ el.addEventListener("mousedown", onMouseDown);
+
+ function once(target, event) {
+ return new Promise(done => {
+ target.addEventListener(event, done, { once: true });
+ });
+ }
+
+ info("Synthesizing an event on the inserted element");
+ let onDocMouseDown = once(doc, "mousedown");
+ synthesizeMouseDown(100, 100, doc.defaultView);
+ await onDocMouseDown;
+
+ is(
+ mouseDownHandled,
+ 1,
+ "The mousedown event was handled once on the element"
+ );
+
+ info("Synthesizing an event somewhere else");
+ onDocMouseDown = once(doc, "mousedown");
+ synthesizeMouseDown(400, 400, doc.defaultView);
+ await onDocMouseDown;
+
+ is(
+ mouseDownHandled,
+ 1,
+ "The mousedown event was not handled on the element"
+ );
+
+ info("Removing the event listener");
+ el.removeEventListener("mousedown", onMouseDown);
+
+ info("Synthesizing another event after the listener has been removed");
+ // Using a document event listener to know when the event has been synthesized.
+ onDocMouseDown = once(doc, "mousedown");
+ synthesizeMouseDown(100, 100, doc.defaultView);
+ await onDocMouseDown;
+
+ is(
+ mouseDownHandled,
+ 1,
+ "The mousedown event hasn't been handled after the listener was removed"
+ );
+
+ info("Adding again the event listener");
+ el.addEventListener("mousedown", onMouseDown);
+
+ info("Destroying the helper");
+ env.destroy();
+ helper.destroy();
+
+ info("Synthesizing another event after the helper has been destroyed");
+ // Using a document event listener to know when the event has been synthesized.
+ onDocMouseDown = once(doc, "mousedown");
+ synthesizeMouseDown(100, 100, doc.defaultView);
+ await onDocMouseDown;
+
+ is(
+ mouseDownHandled,
+ 1,
+ "The mousedown event hasn't been handled after the helper was destroyed"
+ );
+
+ function synthesizeMouseDown(x, y, win) {
+ // We need to make sure the inserted anonymous content can be targeted by the
+ // event right after having been inserted, and so we need to force a sync
+ // reflow.
+ win.document.documentElement.offsetWidth;
+ EventUtils.synthesizeMouseAtPoint(x, y, { type: "mousedown" }, win);
+ }
+ });
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/server/tests/browser/browser_canvasframe_helper_04.js b/devtools/server/tests/browser/browser_canvasframe_helper_04.js
new file mode 100644
index 0000000000..85368ff2b5
--- /dev/null
+++ b/devtools/server/tests/browser/browser_canvasframe_helper_04.js
@@ -0,0 +1,142 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the CanvasFrameAnonymousContentHelper re-inserts the content when the
+// page reloads.
+
+const TEST_URL_1 =
+ "data:text/html;charset=utf-8,CanvasFrameAnonymousContentHelper test 1";
+const TEST_URL_2 =
+ "data:text/html;charset=utf-8,CanvasFrameAnonymousContentHelper test 2";
+
+add_task(async function () {
+ const tab = await addTab(TEST_URL_1);
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [TEST_URL_2],
+ async function (url2) {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ const {
+ HighlighterEnvironment,
+ } = require("resource://devtools/server/actors/highlighters.js");
+ const {
+ CanvasFrameAnonymousContentHelper,
+ } = require("resource://devtools/server/actors/highlighters/utils/markup.js");
+ let doc = content.document;
+
+ const nodeBuilder = () => {
+ const root = doc.createElement("div");
+ const child = doc.createElement("div");
+ child.style =
+ "pointer-events:auto;width:200px;height:200px;background:red;";
+ child.id = "child-element";
+ child.className = "child-element";
+ child.textContent = "test content";
+ root.appendChild(child);
+ return root;
+ };
+
+ info("Building the helper");
+ const env = new HighlighterEnvironment();
+ env.initFromWindow(doc.defaultView);
+ const helper = new CanvasFrameAnonymousContentHelper(env, nodeBuilder);
+ await helper.initialize();
+
+ info("Get an element from the helper");
+ const el = helper.getElement("child-element");
+
+ info("Try to access the element");
+ is(
+ el.getAttribute("class"),
+ "child-element",
+ "The attribute is correct before navigation"
+ );
+ is(
+ el.getTextContent(),
+ "test content",
+ "The text content is correct before navigation"
+ );
+
+ info("Add an event listener on the element");
+ let mouseDownHandled = 0;
+ const onMouseDown = (e, id) => {
+ is(
+ id,
+ "child-element",
+ "The mousedown event was triggered on the element"
+ );
+ mouseDownHandled++;
+ };
+ el.addEventListener("mousedown", onMouseDown);
+
+ const once = function once(target, event) {
+ return new Promise(done => {
+ target.addEventListener(event, done, { once: true });
+ });
+ };
+
+ const synthesizeMouseDown = function synthesizeMouseDown(x, y, win) {
+ // We need to make sure the inserted anonymous content can be targeted by the
+ // event right after having been inserted, and so we need to force a sync
+ // reflow.
+ win.document.documentElement.offsetWidth;
+ EventUtils.synthesizeMouseAtPoint(x, y, { type: "mousedown" }, win);
+ };
+
+ info("Synthesizing an event on the element");
+ let onDocMouseDown = once(doc, "mousedown");
+ synthesizeMouseDown(100, 100, doc.defaultView);
+ await onDocMouseDown;
+ is(
+ mouseDownHandled,
+ 1,
+ "The mousedown event was handled once before navigation"
+ );
+
+ info("Navigating to a new page");
+ const loaded = once(this, "load");
+ content.location = url2;
+ await loaded;
+
+ // Wait for the next event tick to make sure the remaining part of the
+ // test is not executed in the microtask checkpoint for load event
+ // itself. Otherwise the synthesizeMouseDown doesn't work.
+ await new Promise(r => content.setTimeout(r, 0));
+
+ // Update to the new document we just loaded
+ doc = content.document;
+
+ info("Try to access the element again");
+ is(
+ el.getAttribute("class"),
+ "child-element",
+ "The attribute is correct after navigation"
+ );
+ is(
+ el.getTextContent(),
+ "test content",
+ "The text content is correct after navigation"
+ );
+
+ info("Synthesizing an event on the element again");
+ onDocMouseDown = once(doc, "mousedown");
+ synthesizeMouseDown(100, 100, doc.defaultView);
+ await onDocMouseDown;
+ is(
+ mouseDownHandled,
+ 1,
+ "The mousedown event was not handled after navigation"
+ );
+
+ info("Destroying the helper");
+ env.destroy();
+ helper.destroy();
+ }
+ );
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/server/tests/browser/browser_canvasframe_helper_05.js b/devtools/server/tests/browser/browser_canvasframe_helper_05.js
new file mode 100644
index 0000000000..b542b14221
--- /dev/null
+++ b/devtools/server/tests/browser/browser_canvasframe_helper_05.js
@@ -0,0 +1,134 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test some edge cases of the CanvasFrameAnonymousContentHelper event handling
+// mechanism.
+
+const TEST_URL =
+ "data:text/html;charset=utf-8,CanvasFrameAnonymousContentHelper test";
+
+add_task(async function () {
+ const tab = await addTab(TEST_URL);
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ const {
+ HighlighterEnvironment,
+ } = require("resource://devtools/server/actors/highlighters.js");
+ const {
+ CanvasFrameAnonymousContentHelper,
+ } = require("resource://devtools/server/actors/highlighters/utils/markup.js");
+ const doc = content.document;
+
+ const nodeBuilder = () => {
+ const root = doc.createElement("div");
+
+ const parent = doc.createElement("div");
+ parent.style =
+ "pointer-events:auto;width:300px;height:300px;background:yellow;";
+ parent.id = "parent-element";
+ root.appendChild(parent);
+
+ const child = doc.createElement("div");
+ child.style =
+ "pointer-events:auto;width:200px;height:200px;background:red;";
+ child.id = "child-element";
+ parent.appendChild(child);
+
+ return root;
+ };
+
+ info("Building the helper");
+ const env = new HighlighterEnvironment();
+ env.initFromWindow(doc.defaultView);
+ const helper = new CanvasFrameAnonymousContentHelper(env, nodeBuilder);
+ await helper.initialize();
+
+ info("Getting the parent and child elements");
+ const parentEl = helper.getElement("parent-element");
+ const childEl = helper.getElement("child-element");
+
+ info("Adding an event listener on both elements");
+ let mouseDownHandled = [];
+ function onMouseDown(e, id) {
+ mouseDownHandled.push(id);
+ }
+ parentEl.addEventListener("mousedown", onMouseDown);
+ childEl.addEventListener("mousedown", onMouseDown);
+
+ function once(target, event) {
+ return new Promise(done => {
+ target.addEventListener(event, done, { once: true });
+ });
+ }
+
+ info("Synthesizing an event on the child element");
+ let onDocMouseDown = once(doc, "mousedown");
+ synthesizeMouseDown(100, 100, doc.defaultView);
+ await onDocMouseDown;
+
+ is(mouseDownHandled.length, 2, "The mousedown event was handled twice");
+ is(
+ mouseDownHandled[0],
+ "child-element",
+ "The mousedown event was handled on the child element"
+ );
+ is(
+ mouseDownHandled[1],
+ "parent-element",
+ "The mousedown event was handled on the parent element"
+ );
+
+ info("Synthesizing an event on the parent, outside of the child element");
+ mouseDownHandled = [];
+ onDocMouseDown = once(doc, "mousedown");
+ synthesizeMouseDown(250, 250, doc.defaultView);
+ await onDocMouseDown;
+
+ is(mouseDownHandled.length, 1, "The mousedown event was handled only once");
+ is(
+ mouseDownHandled[0],
+ "parent-element",
+ "The mousedown event was handled on the parent element"
+ );
+
+ info("Removing the event listener");
+ parentEl.removeEventListener("mousedown", onMouseDown);
+ childEl.removeEventListener("mousedown", onMouseDown);
+
+ info("Adding an event listener on the parent element only");
+ mouseDownHandled = [];
+ parentEl.addEventListener("mousedown", onMouseDown);
+
+ info("Synthesizing an event on the child element");
+ onDocMouseDown = once(doc, "mousedown");
+ synthesizeMouseDown(100, 100, doc.defaultView);
+ await onDocMouseDown;
+
+ is(mouseDownHandled.length, 1, "The mousedown event was handled once");
+ is(
+ mouseDownHandled[0],
+ "parent-element",
+ "The mousedown event did bubble to the parent element"
+ );
+
+ info("Removing the parent listener");
+ parentEl.removeEventListener("mousedown", onMouseDown);
+
+ env.destroy();
+ helper.destroy();
+
+ function synthesizeMouseDown(x, y, win) {
+ // We need to make sure the inserted anonymous content can be targeted by the
+ // event right after having been inserted, and so we need to force a sync
+ // reflow.
+ win.document.documentElement.offsetWidth;
+ EventUtils.synthesizeMouseAtPoint(x, y, { type: "mousedown" }, win);
+ }
+ });
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/server/tests/browser/browser_canvasframe_helper_06.js b/devtools/server/tests/browser/browser_canvasframe_helper_06.js
new file mode 100644
index 0000000000..e0222b33b1
--- /dev/null
+++ b/devtools/server/tests/browser/browser_canvasframe_helper_06.js
@@ -0,0 +1,116 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test support for event propagation stop in the
+// CanvasFrameAnonymousContentHelper event handling mechanism.
+
+const TEST_URL =
+ "data:text/html;charset=utf-8,CanvasFrameAnonymousContentHelper test";
+
+add_task(async function () {
+ const tab = await addTab(TEST_URL);
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ const {
+ HighlighterEnvironment,
+ } = require("resource://devtools/server/actors/highlighters.js");
+ const {
+ CanvasFrameAnonymousContentHelper,
+ } = require("resource://devtools/server/actors/highlighters/utils/markup.js");
+ const doc = content.document;
+
+ const nodeBuilder = () => {
+ const root = doc.createElement("div");
+
+ const parent = doc.createElement("div");
+ parent.style =
+ "pointer-events:auto;width:300px;height:300px;background:yellow;";
+ parent.id = "parent-element";
+ root.appendChild(parent);
+
+ const child = doc.createElement("div");
+ child.style =
+ "pointer-events:auto;width:200px;height:200px;background:red;";
+ child.id = "child-element";
+ parent.appendChild(child);
+
+ return root;
+ };
+
+ info("Building the helper");
+ const env = new HighlighterEnvironment();
+ env.initFromWindow(doc.defaultView);
+ const helper = new CanvasFrameAnonymousContentHelper(env, nodeBuilder);
+ await helper.initialize();
+
+ info("Getting the parent and child elements");
+ const parentEl = helper.getElement("parent-element");
+ const childEl = helper.getElement("child-element");
+
+ info("Adding an event listener on both elements");
+ let mouseDownHandled = [];
+
+ function onParentMouseDown(e, id) {
+ mouseDownHandled.push(id);
+ }
+ parentEl.addEventListener("mousedown", onParentMouseDown);
+
+ function onChildMouseDown(e, id) {
+ mouseDownHandled.push(id);
+ e.stopPropagation();
+ }
+ childEl.addEventListener("mousedown", onChildMouseDown);
+
+ function once(target, event) {
+ return new Promise(done => {
+ target.addEventListener(event, done, { once: true });
+ });
+ }
+
+ info("Synthesizing an event on the child element");
+ let onDocMouseDown = once(doc, "mousedown");
+ synthesizeMouseDown(100, 100, doc.defaultView);
+ await onDocMouseDown;
+
+ is(mouseDownHandled.length, 1, "The mousedown event was handled only once");
+ is(
+ mouseDownHandled[0],
+ "child-element",
+ "The mousedown event was handled on the child element"
+ );
+
+ info("Synthesizing an event on the parent, outside of the child element");
+ mouseDownHandled = [];
+ onDocMouseDown = once(doc, "mousedown");
+ synthesizeMouseDown(250, 250, doc.defaultView);
+ await onDocMouseDown;
+
+ is(mouseDownHandled.length, 1, "The mousedown event was handled only once");
+ is(
+ mouseDownHandled[0],
+ "parent-element",
+ "The mousedown event was handled on the parent element"
+ );
+
+ info("Removing the event listener");
+ parentEl.removeEventListener("mousedown", onParentMouseDown);
+ childEl.removeEventListener("mousedown", onChildMouseDown);
+
+ env.destroy();
+ helper.destroy();
+
+ function synthesizeMouseDown(x, y, win) {
+ // We need to make sure the inserted anonymous content can be targeted by the
+ // event right after having been inserted, and so we need to force a sync
+ // reflow.
+ win.document.documentElement.offsetWidth;
+ EventUtils.synthesizeMouseAtPoint(x, y, { type: "mousedown" }, win);
+ }
+ });
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/server/tests/browser/browser_compatibility_cssIssues.js b/devtools/server/tests/browser/browser_compatibility_cssIssues.js
new file mode 100644
index 0000000000..4cd244688c
--- /dev/null
+++ b/devtools/server/tests/browser/browser_compatibility_cssIssues.js
@@ -0,0 +1,137 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check the output of getNodeCssIssues
+
+const {
+ COMPATIBILITY_ISSUE_TYPE,
+} = require("resource://devtools/shared/constants.js");
+const URL = MAIN_DOMAIN + "doc_compatibility.html";
+
+const CHROME_81 = {
+ id: "chrome",
+ version: "81",
+};
+
+const CHROME_ANDROID = {
+ id: "chrome_android",
+ version: "81",
+};
+
+const EDGE_81 = {
+ id: "edge",
+ version: "81",
+};
+
+const FIREFOX_1 = {
+ id: "firefox",
+ version: "1",
+};
+
+const FIREFOX_60 = {
+ id: "firefox",
+ version: "60",
+};
+
+const FIREFOX_69 = {
+ id: "firefox",
+ version: "69",
+};
+
+const FIREFOX_MOBILE = {
+ id: "firefox_android",
+ version: "68",
+};
+
+const SAFARI_13 = {
+ id: "safari",
+ version: "13",
+};
+
+const SAFARI_MOBILE = {
+ id: "safari_ios",
+ version: "13.4",
+};
+
+const TARGET_BROWSERS = [
+ FIREFOX_1,
+ FIREFOX_60,
+ FIREFOX_69,
+ FIREFOX_MOBILE,
+ CHROME_81,
+ CHROME_ANDROID,
+ SAFARI_13,
+ SAFARI_MOBILE,
+ EDGE_81,
+];
+
+const ISSUE_USER_SELECT = {
+ type: COMPATIBILITY_ISSUE_TYPE.CSS_PROPERTY_ALIASES,
+ property: "user-select",
+ aliases: ["-moz-user-select"],
+ url: "https://developer.mozilla.org/docs/Web/CSS/user-select",
+ specUrl: "https://drafts.csswg.org/css-ui/#content-selection",
+ deprecated: false,
+ experimental: false,
+ prefixNeeded: true,
+ unsupportedBrowsers: [
+ CHROME_81,
+ CHROME_ANDROID,
+ SAFARI_13,
+ SAFARI_MOBILE,
+ EDGE_81,
+ ],
+};
+
+const ISSUE_CLIP = {
+ type: COMPATIBILITY_ISSUE_TYPE.CSS_PROPERTY,
+ property: "clip",
+ url: "https://developer.mozilla.org/docs/Web/CSS/clip",
+ specUrl: "https://drafts.fxtf.org/css-masking/#clip-property",
+ deprecated: true,
+ experimental: false,
+ unsupportedBrowsers: [],
+};
+
+async function testNodeCssIssues(selector, walker, compatibility, expected) {
+ const node = await walker.querySelector(walker.rootNode, selector);
+ const cssCompatibilityIssues = await compatibility.getNodeCssIssues(
+ node,
+ TARGET_BROWSERS
+ );
+ info("Ensure result is correct");
+ Assert.deepEqual(
+ cssCompatibilityIssues,
+ expected,
+ "Expected CSS browser compat data is correct."
+ );
+}
+
+add_task(async function () {
+ const { inspector, walker, target } = await initInspectorFront(URL);
+ const compatibility = await inspector.getCompatibilityFront();
+
+ info('Test CSS properties linked with the "div" tag');
+ await testNodeCssIssues("div", walker, compatibility, []);
+
+ info('Test CSS properties linked with class "class-user-select"');
+ await testNodeCssIssues(".class-user-select", walker, compatibility, [
+ ISSUE_USER_SELECT,
+ ]);
+
+ info("Test CSS properties linked with multiple classes and id");
+ await testNodeCssIssues(
+ "div#id-clip.class-clip.class-user-select",
+ walker,
+ compatibility,
+ [ISSUE_CLIP, ISSUE_USER_SELECT]
+ );
+
+ info("Repeated incompatible CSS rule should be only reported once");
+ await testNodeCssIssues(".duplicate", walker, compatibility, [ISSUE_CLIP]);
+
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/server/tests/browser/browser_connectToFrame.js b/devtools/server/tests/browser/browser_connectToFrame.js
new file mode 100644
index 0000000000..568eb1acc1
--- /dev/null
+++ b/devtools/server/tests/browser/browser_connectToFrame.js
@@ -0,0 +1,142 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test `connectToFrame` method
+ */
+
+"use strict";
+
+const {
+ connectToFrame,
+} = require("resource://devtools/server/connectors/frame-connector.js");
+
+add_task(async function () {
+ // Create a minimal browser with a message manager
+ const browser = document.createXULElement("browser");
+ browser.setAttribute("type", "content");
+ document.body.appendChild(browser);
+
+ await TestUtils.waitForCondition(
+ () => browser.browsingContext.currentWindowGlobal,
+ "browser has no window global"
+ );
+
+ // Register a test actor in the child process so that we can know if and when
+ // this fake actor is destroyed.
+ await SpecialPowers.spawn(browser, [], () => {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ const {
+ DevToolsServer,
+ } = require("resource://devtools/server/devtools-server.js");
+ const {
+ ActorRegistry,
+ } = require("resource://devtools/server/actors/utils/actor-registry.js");
+
+ DevToolsServer.init();
+
+ const { Actor } = require("resource://devtools/shared/protocol/Actor.js");
+ class ConnectToFrameTestActor extends Actor {
+ constructor(conn, tab) {
+ super(conn, { typeName: "connectToFrameTest", methods: [] });
+ dump("instantiate test actor\n");
+ this.requestTypes = {
+ hello: this.hello,
+ };
+ }
+ hello() {
+ return { msg: "world" };
+ }
+
+ destroy() {
+ SpecialPowers.notifyObserversInParentProcess(
+ null,
+ "devtools-test-actor-destroyed",
+ ""
+ );
+ }
+ }
+
+ ActorRegistry.addTargetScopedActor(
+ {
+ constructorName: "ConnectToFrameTestActor",
+ constructorFun: ConnectToFrameTestActor,
+ },
+ "connectToFrameTestActor"
+ );
+ });
+
+ // Instantiate a minimal server
+ DevToolsServer.init();
+ if (!DevToolsServer.createRootActor) {
+ DevToolsServer.registerAllActors();
+ }
+
+ async function initAndCloseFirstClient() {
+ // Fake a first connection to a browser
+ const transport = DevToolsServer.connectPipe();
+ const conn = transport._serverConnection;
+ const client = new DevToolsClient(transport);
+ const actor = await connectToFrame(conn, browser);
+ ok(actor.connectToFrameTestActor, "Got the test actor");
+
+ // Ensure sending at least one request to our actor,
+ // otherwise it won't be instantiated, nor be destroyed...
+ await client.request({
+ to: actor.connectToFrameTestActor,
+ type: "hello",
+ });
+
+ // Connect a second client in parallel to assert that it received a distinct set of
+ // target actors
+ await initAndCloseSecondClient(actor.connectToFrameTestActor);
+
+ ok(
+ DevToolsServer.initialized,
+ "DevToolsServer isn't destroyed until all clients are disconnected"
+ );
+
+ // Ensure that our test actor got cleaned up;
+ // its destroy method should be called
+ const onActorDestroyed = TestUtils.topicObserved(
+ "devtools-test-actor-destroyed"
+ );
+
+ // Then close the client. That should end up cleaning our test actor
+ await client.close();
+
+ await onActorDestroyed;
+
+ // This test loads a frame in the parent process, so that we end up sharing the same
+ // DevToolsServer instance
+ ok(
+ !DevToolsServer.initialized,
+ "DevToolsServer is destroyed when all clients are disconnected"
+ );
+ }
+
+ async function initAndCloseSecondClient(firstActor) {
+ // Then fake a second one, that should spawn a new set of target-scoped actors
+ const transport = DevToolsServer.connectPipe();
+ const conn = transport._serverConnection;
+ const client = new DevToolsClient(transport);
+ const actor = await connectToFrame(conn, browser);
+ ok(
+ actor.connectToFrameTestActor,
+ "Got a test actor for the second connection"
+ );
+ isnot(
+ actor.connectToFrameTestActor,
+ firstActor,
+ "We get different actor instances between two connections"
+ );
+ return client.close();
+ }
+
+ await initAndCloseFirstClient();
+
+ DevToolsServer.destroy();
+ browser.remove();
+});
diff --git a/devtools/server/tests/browser/browser_debugger_server.js b/devtools/server/tests/browser/browser_debugger_server.js
new file mode 100644
index 0000000000..8b36076b34
--- /dev/null
+++ b/devtools/server/tests/browser/browser_debugger_server.js
@@ -0,0 +1,198 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test basic features of DevToolsServer
+
+add_task(async function () {
+ // When running some other tests before, they may not destroy the main server.
+ // Do it manually before running our tests.
+ if (DevToolsServer.initialized) {
+ DevToolsServer.destroy();
+ }
+
+ await testDevToolsServerInitialized();
+ await testDevToolsServerKeepAlive();
+});
+
+async function testDevToolsServerInitialized() {
+ const tab = await addTab("data:text/html;charset=utf-8,foo");
+
+ ok(
+ !DevToolsServer.initialized,
+ "By default, the DevToolsServer isn't initialized in parent process"
+ );
+ await assertServerInitialized(
+ tab,
+ false,
+ "By default, the DevToolsServer isn't initialized not in content process"
+ );
+ await assertDevToolsOpened(
+ tab,
+ false,
+ "By default, the DevTools are reported as closed"
+ );
+
+ const commands = await CommandsFactory.forTab(tab);
+
+ ok(
+ DevToolsServer.initialized,
+ "Creating the commands will initialize the DevToolsServer in parent process"
+ );
+ await assertServerInitialized(
+ tab,
+ false,
+ "Creating the commands isn't enough to initialize the DevToolsServer in content process"
+ );
+ await assertDevToolsOpened(
+ tab,
+ false,
+ "DevTools are still reported as closed after having created the commands"
+ );
+
+ await commands.targetCommand.startListening();
+
+ await assertServerInitialized(
+ tab,
+ true,
+ "Initializing the TargetCommand will initialize the DevToolsServer in content process"
+ );
+ await assertDevToolsOpened(
+ tab,
+ true,
+ "Initializing the TargetCommand will start reporting the DevTools as opened"
+ );
+
+ await commands.destroy();
+
+ // Disconnecting the client will remove all connections from both server, in parent and content process.
+ ok(
+ !DevToolsServer.initialized,
+ "Destroying the commands destroys the DevToolsServer in the parent process"
+ );
+ await assertServerInitialized(
+ tab,
+ false,
+ "But destroying the commands ends up destroying the DevToolsServer in the content process"
+ );
+ await assertDevToolsOpened(
+ tab,
+ false,
+ "Destroying the commands will report DevTools as being closed"
+ );
+
+ gBrowser.removeCurrentTab();
+ DevToolsServer.destroy();
+}
+
+async function testDevToolsServerKeepAlive() {
+ const tab = await addTab("data:text/html;charset=utf-8,foo");
+
+ await assertServerInitialized(
+ tab,
+ false,
+ "Server not started in content process"
+ );
+ await assertDevToolsOpened(tab, false, "DevTools are reported as closed");
+
+ const commands = await CommandsFactory.forTab(tab);
+ await commands.targetCommand.startListening();
+
+ await assertServerInitialized(tab, true, "Server started in content process");
+ await assertDevToolsOpened(tab, true, "DevTools are reported as opened");
+
+ info("Set DevToolsServer.keepAlive to true in the content process");
+ DevToolsServer.keepAlive = true;
+ await setContentServerKeepAlive(tab, true);
+
+ info("Destroy the commands, the content server should be kept alive");
+ await commands.destroy();
+
+ await assertServerInitialized(
+ tab,
+ true,
+ "Server still running in content process"
+ );
+ await assertDevToolsOpened(
+ tab,
+ false,
+ "DevTools are reported as close, even if the server is still running because there is no more client connected"
+ );
+
+ ok(
+ DevToolsServer.initialized,
+ "Destroying the commands never destroys the DevToolsServer in the parent process when keepAlive is true"
+ );
+
+ info("Set DevToolsServer.keepAlive back to false");
+ DevToolsServer.keepAlive = false;
+ await setContentServerKeepAlive(tab, false);
+
+ info("Create and destroy a commands again");
+ const newCommands = await CommandsFactory.forTab(tab);
+ await newCommands.targetCommand.startListening();
+
+ await newCommands.destroy();
+
+ await assertServerInitialized(
+ tab,
+ false,
+ "Server stopped in content process"
+ );
+ await assertDevToolsOpened(
+ tab,
+ false,
+ "DevTools are reported as closed after destroying the second commands"
+ );
+
+ ok(
+ !DevToolsServer.initialized,
+ "When turning keepAlive to false, the server in the parent process is destroyed"
+ );
+
+ gBrowser.removeCurrentTab();
+ DevToolsServer.destroy();
+}
+
+async function assertServerInitialized(tab, expected, message) {
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [expected, message],
+ function (_expected, _message) {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ const {
+ DevToolsServer,
+ } = require("resource://devtools/server/devtools-server.js");
+ is(DevToolsServer.initialized, _expected, _message);
+ }
+ );
+}
+
+async function assertDevToolsOpened(tab, expected, message) {
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [expected, message],
+ function (_expected, _message) {
+ is(ChromeUtils.isDevToolsOpened(), _expected, _message);
+ }
+ );
+}
+
+async function setContentServerKeepAlive(tab, keepAlive, message) {
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [keepAlive],
+ function (_keepAlive) {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ const {
+ DevToolsServer,
+ } = require("resource://devtools/server/devtools-server.js");
+ DevToolsServer.keepAlive = _keepAlive;
+ }
+ );
+}
diff --git a/devtools/server/tests/browser/browser_document_devtools_basics.js b/devtools/server/tests/browser/browser_document_devtools_basics.js
new file mode 100644
index 0000000000..1d15420559
--- /dev/null
+++ b/devtools/server/tests/browser/browser_document_devtools_basics.js
@@ -0,0 +1,103 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Document the basics of DevTools backend via Fronts in a test.
+ */
+
+"use strict";
+
+const TEST_URL = "data:text/html,new-tab";
+
+add_task(async () => {
+ // Allow logging all RDP packets
+ await pushPref("devtools.debugger.log", true);
+ // Really all of them
+ await pushPref("devtools.debugger.log.verbose", true);
+
+ // Instantiate a DevTools server
+ DevToolsServer.init();
+ DevToolsServer.registerAllActors();
+
+ // Instantiate a client connected to this server
+ const transport = DevToolsServer.connectPipe();
+ const client = new DevToolsClient(transport);
+
+ // This will trigger some handshake with the server
+ await client.connect();
+
+ // You need to call listTabs once to retrieve the existing list of Tab Descriptor actors...
+ const tabs = await client.mainRoot.listTabs();
+
+ // ... which will let you receive the 'tabListChanged' event.
+ // This is an empty RDP packet, you need to re-call listTabs to get the full new updated list of actors.
+ const onTabListUpdated = client.mainRoot.once("tabListChanged");
+
+ // Open a new tab.
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_URL,
+ });
+
+ await onTabListUpdated;
+
+ // The new list of Tab descriptors should contain the newly opened tab
+ const newTabs = await client.mainRoot.listTabs();
+ is(newTabs.length, tabs.length + 1);
+
+ const tabDescriptorActor = newTabs.pop();
+ is(tabDescriptorActor.url, TEST_URL);
+
+ // Query the Tab Descriptor actor to retrieve its related Watcher actor.
+ // Each Descriptor actor has a dedicated watcher which will be scoped to the context of the descriptor.
+ // Here the watcher will focus on the related tab.
+ const watcherActor = await tabDescriptorActor.getWatcher();
+
+ // The call to Watcher Actor's watchTargets will emit target-available-form RDP events.
+ // One per available target. It will emit one for each immediatly available target,
+ // but also for any available later. That, until you call unwatchTarget method.
+ //
+ // Here I'm listening to "target-available" to get a Front instance, which helps call RDP methods.
+ // But this isn't an RDP event. This is a frontend-only thing.
+ const onTopTargetAvailable = watcherActor.once("target-available");
+
+ // watchTargets accepts "frame", "process" and "worker"
+ // When debugging a web page you want to listen to frame and worker targets.
+ // "frame" would better be named "WindowGlobal" as it will notify you about all the WindowGlobal of the page.
+ // Each top level documents and any iframe documents will have a related WindowGlobal,
+ // if any of these documents navigate, a new WindowGlobal will be instantiated.
+ // If you care about workers, listen to worker targets as well.
+ await watcherActor.watchTargets("frame");
+
+ // This is a trivial example so we have a unique WindowGlobal target for the top level document
+ const topTarget = await onTopTargetAvailable;
+ is(topTarget.url, TEST_URL);
+
+ // Similarly to watchTarget, the next call to watchResources will emit new resources right away as well as later.
+ const onConsoleMessages = topTarget.once("resource-available-form");
+
+ // If you want to observe anything, you have to use Watcher Actor's watchrResources API.
+ // The list of all available resources is here:
+ // https://searchfox.org/mozilla-central/source/devtools/server/actors/resources/index.js#9
+ // And you might have a look at each ResourceWatcher subclass to learn more about the fields exposed by each resource type:
+ // https://searchfox.org/mozilla-central/source/devtools/server/actors/resources
+ await watcherActor.watchResources(["console-message"]);
+
+ // You may use many useful actors on each target actor, like console, thread, ...
+ // You can get the full list of available actors in:
+ // https://searchfox.org/mozilla-central/source/devtools/server/actors/utils/actor-registry.js#176
+ // And then look into the mentioned path for implementation.
+ const webConsoleActor = await topTarget.getFront("console");
+
+ // Call the Console API in order to force emitting a console-message resource
+ await webConsoleActor.evaluateJSAsync({ text: "console.log('42')" });
+
+ // Wait for the related console-message resource
+ const resources = await onConsoleMessages;
+
+ // Note that resource-available-form comes with a "resources" attribute which is an array of resources
+ // which may contain various resource types.
+ is(resources[0].message.arguments[0], "42");
+
+ await client.close();
+});
diff --git a/devtools/server/tests/browser/browser_document_rdp_basics.js b/devtools/server/tests/browser/browser_document_rdp_basics.js
new file mode 100644
index 0000000000..552837ff7c
--- /dev/null
+++ b/devtools/server/tests/browser/browser_document_rdp_basics.js
@@ -0,0 +1,129 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Document the basics of RDP packets via a test.
+ */
+
+"use strict";
+
+const TEST_URL = "data:text/html,new-tab";
+
+add_task(async () => {
+ // Allow logging all RDP packets
+ await pushPref("devtools.debugger.log", true);
+ // Really all of them
+ await pushPref("devtools.debugger.log.verbose", true);
+
+ // Instantiate a DevTools server
+ DevToolsServer.init();
+ DevToolsServer.registerAllActors();
+
+ // Instantiate a client connected to this server
+ const transport = DevToolsServer.connectPipe();
+ const client = new DevToolsClient(transport);
+
+ // This will trigger some handshake with the server
+ await client.connect();
+
+ // Ignore this gross hack, this is to be able to emit raw RDP packet via client.request
+ // (a Front is instantiated by DevToolsClient which would be confused with us sending
+ // RDP packets for the Root actor)
+ client.mainRoot.destroy();
+
+ // You need to call listTabs once to retrieve the existing list of Tab Descriptor actors...
+ const { tabs } = await client.request({ to: "root", type: "listTabs" });
+
+ // ... which will let you receive the 'tabListChanged' event.
+ // This is an empty RDP packet, you need to re-call listTabs to get the full new updated list of actors.
+ const onTabListUpdated = client.once("tabListChanged");
+
+ // Open a new tab.
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_URL,
+ });
+
+ await onTabListUpdated;
+
+ // The new list of Tab descriptors should contain the newly opened tab
+ const { tabs: newTabs } = await client.request({
+ to: "root",
+ type: "listTabs",
+ });
+ is(newTabs.length, tabs.length + 1);
+
+ const tabDescriptorActor = newTabs.pop();
+ is(tabDescriptorActor.url, TEST_URL);
+
+ // Query the Tab Descriptor actor to retrieve its related Watcher actor.
+ // Each Descriptor actor has a dedicated watcher which will be scoped to the context of the descriptor.
+ // Here the watcher will focus on the related tab.
+ //
+ // You want to pass isServerTargetSwitchingEnabled set to true in order to be notified about the top level document,
+ // as well as navigations to subsequent documents.
+ const watcherActor = await client.request({
+ to: tabDescriptorActor.actor,
+ type: "getWatcher",
+ isServerTargetSwitchingEnabled: true,
+ });
+
+ // The call to Watcher Actor's watchTargets will emit target-available-form RDP events.
+ // One per available target. It will emit one for each immediatly available target,
+ // but also for any available later. That, until you call unwatchTarget method.
+ const onTopTargetAvailable = client.once("target-available-form");
+
+ // watchTargets accepts "frame", "process" and "worker"
+ // When debugging a web page you want to listen to frame and worker targets.
+ // "frame" would better be named "WindowGlobal" as it will notify you about all the WindowGlobal of the page.
+ // Each top level documents and any iframe documents will have a related WindowGlobal,
+ // if any of these documents navigate, a new WindowGlobal will be instantiated.
+ // If you care about workers, listen to worker targets as well.
+ await client.request({
+ to: watcherActor.actor,
+ type: "watchTargets",
+ targetType: "frame",
+ });
+
+ // This is a trivial example so we have a unique WindowGlobal target for the top level document
+ const { target: topTarget } = await onTopTargetAvailable;
+ is(topTarget.url, TEST_URL);
+
+ // Similarly to watchTarget, the next call to watchResources will emit new resources right away as well as later.
+ const onConsoleMessages = client.once("resource-available-form");
+
+ // If you want to observe anything, you have to use Watcher Actor's watchrResources API.
+ // The list of all available resources is here:
+ // https://searchfox.org/mozilla-central/source/devtools/server/actors/resources/index.js#9
+ // And you might have a look at each ResourceWatcher subclass to learn more about the fields exposed by each resource type:
+ // https://searchfox.org/mozilla-central/source/devtools/server/actors/resources
+ await client.request({
+ to: watcherActor.actor,
+ type: "watchResources",
+ resourceTypes: ["console-message"],
+ });
+
+ // You may use many useful actors on each target actor, like console, thread, ...
+ // You can get the full list of available actors in:
+ // https://searchfox.org/mozilla-central/source/devtools/server/actors/utils/actor-registry.js#176
+ // And then look into the mentioned path for implementation.
+ //
+ // The "target form" contains the list of all these actor IDs
+ const webConsoleActorID = topTarget.consoleActor;
+
+ // Call the Console API in order to force emitting a console-message resource
+ await client.request({
+ to: webConsoleActorID,
+ type: "evaluateJSAsync",
+ text: "console.log('42')",
+ });
+
+ // Wait for the related console-message resource
+ const { resources } = await onConsoleMessages;
+
+ // Note that resource-available-form comes with a "resources" attribute which is an array of resources
+ // which may contain various resource types.
+ is(resources[0].message.arguments[0], "42");
+
+ await client.close();
+});
diff --git a/devtools/server/tests/browser/browser_getProcess.js b/devtools/server/tests/browser/browser_getProcess.js
new file mode 100644
index 0000000000..30c9fff589
--- /dev/null
+++ b/devtools/server/tests/browser/browser_getProcess.js
@@ -0,0 +1,129 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test `RootActor.getProcess` method
+ */
+
+"use strict";
+
+add_task(async () => {
+ let client, tab;
+
+ function connect() {
+ // Fake a first connection to the content process
+ const transport = DevToolsServer.connectPipe();
+ client = new DevToolsClient(transport);
+ return client.connect();
+ }
+
+ async function listProcess() {
+ const onNewProcess = new Promise(resolve => {
+ // Call listProcesses in order to start receiving new process notifications
+ client.mainRoot.on("processListChanged", function listener() {
+ client.off("processListChanged", listener);
+ ok(true, "Received processListChanged event");
+ resolve();
+ });
+ });
+ await client.mainRoot.listProcesses();
+ await createNewProcess();
+ return onNewProcess;
+ }
+
+ async function createNewProcess() {
+ tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "data:text/html,new-process",
+ forceNewProcess: true,
+ });
+ }
+
+ async function getProcess() {
+ // Note that we can't assert process count as the number of processes
+ // is affected by previous tests.
+ const processes = await client.mainRoot.listProcesses();
+ const { osPid } = tab.linkedBrowser.browsingContext.currentWindowGlobal;
+ const descriptor = processes.find(process => process.id == osPid);
+ ok(descriptor, "Got the new process descriptor");
+
+ // Connect to the first content process available
+ const content = processes.filter(p => !p.isParentProcessDescriptor)[0];
+
+ const processDescriptor = await client.mainRoot.getProcess(content.id);
+ const front = await processDescriptor.getTarget();
+ const targetForm = front.targetForm;
+ ok(targetForm.consoleActor, "Got the console actor");
+ ok(targetForm.threadActor, "Got the thread actor");
+
+ // Process target are no longer really used/supported beyond listing their workers
+ // from RootFront.
+ const { workers } = await front.listWorkers();
+ is(workers.length, 0, "listWorkers worked and reported no workers");
+
+ return [front, content.id];
+ }
+
+ // Assert that calling client.getProcess against the same process id is
+ // returning the same actor.
+ async function getProcessAgain(firstTargetFront, id) {
+ const processDescriptor = await client.mainRoot.getProcess(id);
+ const front = await processDescriptor.getTarget();
+ is(
+ front,
+ firstTargetFront,
+ "Second call to getProcess with the same id returns the same form"
+ );
+ }
+
+ function processScript() {
+ /* eslint-env mozilla/process-script */
+ const listener = function () {
+ Services.obs.removeObserver(listener, "devtools:loader:destroy");
+ sendAsyncMessage("test:getProcess-destroy", null);
+ };
+ Services.obs.addObserver(listener, "devtools:loader:destroy");
+ }
+
+ async function closeClient() {
+ const onLoaderDestroyed = new Promise(done => {
+ const processListener = function () {
+ Services.ppmm.removeMessageListener(
+ "test:getProcess-destroy",
+ processListener
+ );
+ done();
+ };
+ Services.ppmm.addMessageListener(
+ "test:getProcess-destroy",
+ processListener
+ );
+ });
+ const script = `data:,(${encodeURI(processScript)})()`;
+ Services.ppmm.loadProcessScript(script, true);
+ await client.close();
+
+ await onLoaderDestroyed;
+ Services.ppmm.removeDelayedProcessScript(script);
+ info("Loader destroyed in the content process");
+ }
+
+ // Instantiate a minimal server
+ DevToolsServer.init();
+ DevToolsServer.allowChromeProcess = true;
+ if (!DevToolsServer.createRootActor) {
+ DevToolsServer.registerAllActors();
+ }
+
+ await connect();
+ await listProcess();
+
+ const [front, contentId] = await getProcess();
+
+ await getProcessAgain(front, contentId);
+
+ await closeClient();
+
+ BrowserTestUtils.removeTab(tab);
+ DevToolsServer.destroy();
+});
diff --git a/devtools/server/tests/browser/browser_inspector-anonymous.js b/devtools/server/tests/browser/browser_inspector-anonymous.js
new file mode 100644
index 0000000000..024b7af1bb
--- /dev/null
+++ b/devtools/server/tests/browser/browser_inspector-anonymous.js
@@ -0,0 +1,204 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for Bug 777674
+
+add_task(async function () {
+ await SpecialPowers.pushPermissions([
+ { type: "allowXULXBL", allow: true, context: MAIN_DOMAIN },
+ ]);
+
+ const { walker } = await initInspectorFront(
+ MAIN_DOMAIN + "inspector-traversal-data.html"
+ );
+
+ await testXBLAnonymousInHTMLDocument(walker);
+ await testNativeAnonymous(walker);
+ await testNativeAnonymousStartingNode(walker);
+
+ await testPseudoElements(walker);
+ await testEmptyWithPseudo(walker);
+ await testShadowAnonymous(walker);
+});
+
+async function testXBLAnonymousInHTMLDocument(walker) {
+ info("Testing XBL anonymous in an HTML document.");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ const XUL_NS =
+ "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ const rawToolbarbutton = content.document.createElementNS(
+ XUL_NS,
+ "toolbarbutton"
+ );
+ content.document.documentElement.appendChild(rawToolbarbutton);
+ });
+
+ const toolbarbutton = await walker.querySelector(
+ walker.rootNode,
+ "toolbarbutton"
+ );
+ const children = await walker.children(toolbarbutton);
+
+ is(toolbarbutton.numChildren, 0, "XBL content is not visible in HTML doc");
+ is(children.nodes.length, 0, "XBL content is not returned in HTML doc");
+}
+
+async function testNativeAnonymous(walker) {
+ info("Testing native anonymous content with walker.");
+
+ const select = await walker.querySelector(walker.rootNode, "select");
+ const children = await walker.children(select);
+
+ is(select.numChildren, 2, "No native anon content for form control");
+ is(children.nodes.length, 2, "No native anon content for form control");
+}
+
+async function testNativeAnonymousStartingNode(walker) {
+ info("Tests attaching an element that a walker can't see.");
+
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [[walker.actorID]],
+ async function (actorID) {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ const {
+ DevToolsServer,
+ } = require("resource://devtools/server/devtools-server.js");
+
+ const {
+ DocumentWalker,
+ } = require("resource://devtools/server/actors/inspector/document-walker.js");
+ const nodeFilterConstants = require("resource://devtools/shared/dom-node-filter-constants.js");
+
+ const docwalker = new DocumentWalker(
+ content.document.querySelector("select"),
+ content,
+ {
+ filter: () => {
+ return nodeFilterConstants.FILTER_ACCEPT;
+ },
+ }
+ );
+ const scrollbar = docwalker.lastChild();
+ is(scrollbar.tagName, "scrollbar", "An anonymous child has been fetched");
+
+ // Convert actorID to current compartment string otherwise
+ // searchAllConnectionsForActor is confused and won't find the actor.
+ actorID = String(actorID);
+ const serverWalker = DevToolsServer.searchAllConnectionsForActor(actorID);
+ const node = await serverWalker.attachElement(scrollbar);
+
+ ok(node, "A response has arrived");
+ ok(node.node, "A node is in the response");
+ is(
+ node.node.rawNode.tagName,
+ "SELECT",
+ "The node has changed to a parent that the walker recognizes"
+ );
+ }
+ );
+}
+
+async function testPseudoElements(walker) {
+ info("Testing pseudo elements with walker.");
+
+ // Markup looks like: <div><::before /><span /><::after /></div>
+ const pseudo = await walker.querySelector(walker.rootNode, "#pseudo");
+ const children = await walker.children(pseudo);
+
+ is(
+ pseudo.numChildren,
+ 1,
+ "::before/::after are not counted if there is a child"
+ );
+ is(children.nodes.length, 3, "Correct number of children");
+
+ const before = children.nodes[0];
+ ok(before.isAnonymous, "Child is anonymous");
+ ok(before._form.isNativeAnonymous, "Child is native anonymous");
+
+ const span = children.nodes[1];
+ ok(!span.isAnonymous, "Child is not anonymous");
+
+ const after = children.nodes[2];
+ ok(after.isAnonymous, "Child is anonymous");
+ ok(after._form.isNativeAnonymous, "Child is native anonymous");
+}
+
+async function testEmptyWithPseudo(walker) {
+ info("Testing elements with no childrent, except for pseudos.");
+
+ info("Checking an element whose only child is a pseudo element");
+ const pseudo = await walker.querySelector(walker.rootNode, "#pseudo-empty");
+ const children = await walker.children(pseudo);
+
+ is(
+ pseudo.numChildren,
+ 1,
+ "::before/::after are is counted if there are no other children"
+ );
+ is(children.nodes.length, 1, "Correct number of children");
+
+ const before = children.nodes[0];
+ ok(before.isAnonymous, "Child is anonymous");
+ ok(before._form.isNativeAnonymous, "Child is native anonymous");
+}
+
+async function testShadowAnonymous(walker) {
+ info("Testing shadow DOM content.");
+
+ const host = await walker.querySelector(walker.rootNode, "#shadow");
+ const children = await walker.children(host);
+
+ // #shadow-root, ::before, light dom
+ is(host.numChildren, 3, "Children of the shadow root are counted");
+ is(children.nodes.length, 3, "Children returned from walker");
+
+ const before = children.nodes[1];
+ is(
+ before._form.nodeName,
+ "_moz_generated_content_before",
+ "Should be the ::before pseudo-element"
+ );
+ ok(before.isAnonymous, "::before is anonymous");
+ ok(before._form.isNativeAnonymous, "::before is native anonymous");
+ info(JSON.stringify(before._form));
+
+ const shadow = children.nodes[0];
+ const shadowChildren = await walker.children(shadow);
+ // <h3>...</h3>, <select multiple></select>
+ is(shadow.numChildren, 2, "Children of the shadow root are counted");
+ is(shadowChildren.nodes.length, 2, "Children returned from walker");
+
+ // <h3>Shadow <em>DOM</em></h3>
+ const shadowChild1 = shadowChildren.nodes[0];
+ ok(!shadowChild1.isAnonymous, "Shadow child is not anonymous");
+ ok(
+ !shadowChild1._form.isNativeAnonymous,
+ "Shadow child is not native anonymous"
+ );
+
+ const shadowSubChildren = await walker.children(shadowChild1);
+ is(shadowChild1.numChildren, 2, "Subchildren of the shadow root are counted");
+ is(shadowSubChildren.nodes.length, 2, "Subchildren are returned from walker");
+
+ // <em>DOM</em>
+ const shadowSubChild = shadowSubChildren.nodes[1];
+ ok(
+ !shadowSubChild.isAnonymous,
+ "Subchildren of shadow root are not anonymous"
+ );
+ ok(
+ !shadowSubChild._form.isNativeAnonymous,
+ "Subchildren of shadow root is not native anonymous"
+ );
+
+ // <select multiple></select>
+ const shadowChild2 = shadowChildren.nodes[1];
+ ok(!shadowChild2.isAnonymous, "Child is anonymous");
+ ok(!shadowChild2._form.isNativeAnonymous, "Child is not native anonymous");
+}
diff --git a/devtools/server/tests/browser/browser_inspector-iframe.js b/devtools/server/tests/browser/browser_inspector-iframe.js
new file mode 100644
index 0000000000..e9c3fd93a1
--- /dev/null
+++ b/devtools/server/tests/browser/browser_inspector-iframe.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const BYPASS_WALKERFRONT_CHILDREN_IFRAME_GUARD_PREF =
+ "devtools.testing.bypass-walker-children-iframe-guard";
+
+add_task(async function testIframe() {
+ info("Check that dedicated walker is used for retrieving iframe children");
+
+ const TEST_URI = `https://example.com/document-builder.sjs?html=${encodeURIComponent(`
+ <h1>Test iframe</h1>
+ <iframe src="https://example.com/document-builder.sjs?html=Hello"></iframe>
+`)}`;
+
+ const { walker } = await initInspectorFront(TEST_URI);
+ const iframeNodeFront = await walker.querySelector(walker.rootNode, "iframe");
+
+ is(
+ iframeNodeFront.useChildTargetToFetchChildren,
+ isEveryFrameTargetEnabled(),
+ "useChildTargetToFetchChildren has expected value"
+ );
+ is(
+ iframeNodeFront.numChildren,
+ 1,
+ "numChildren is set to 1 (for the #document node)"
+ );
+
+ const res = await walker.children(iframeNodeFront);
+ is(
+ res.nodes.length,
+ 1,
+ "Retrieving the iframe children return an array with one element"
+ );
+ const documentNodeFront = res.nodes[0];
+ is(
+ documentNodeFront.nodeName,
+ "#document",
+ "The child is the #document element"
+ );
+ if (isEveryFrameTargetEnabled()) {
+ Assert.notStrictEqual(
+ documentNodeFront.walkerFront,
+ walker,
+ "The child walker is different from the top level document one when EFT is enabled"
+ );
+ }
+ is(
+ documentNodeFront.parentNode(),
+ iframeNodeFront,
+ "The child parent was set to the original iframe nodeFront"
+ );
+});
+
+add_task(async function testIframeBlockedByCSP() {
+ info("Check that iframe blocked by CSP don't have any children");
+
+ const TEST_URI = `https://example.com/document-builder.sjs?html=${encodeURIComponent(`
+ <h1>Test CSP-blocked iframe</h1>
+ <iframe src="https://example.org/document-builder.sjs?html=Hello"></iframe>
+`)}&headers=content-security-policy:default-src 'self'`;
+
+ const { walker } = await initInspectorFront(TEST_URI);
+ const iframeNodeFront = await walker.querySelector(walker.rootNode, "iframe");
+
+ is(
+ iframeNodeFront.useChildTargetToFetchChildren,
+ false,
+ "useChildTargetToFetchChildren is false"
+ );
+ is(iframeNodeFront.numChildren, 0, "numChildren is set to 0");
+
+ info("Test calling WalkerFront#children with the safe guard removed");
+ await pushPref(BYPASS_WALKERFRONT_CHILDREN_IFRAME_GUARD_PREF, true);
+
+ let res = await walker.children(iframeNodeFront);
+ is(
+ res.nodes.length,
+ 0,
+ "Retrieving the iframe children return an empty array"
+ );
+
+ info("Test calling WalkerFront#children again, but with the safe guard");
+ Services.prefs.clearUserPref(BYPASS_WALKERFRONT_CHILDREN_IFRAME_GUARD_PREF);
+ res = await walker.children(iframeNodeFront);
+ is(
+ res.nodes.length,
+ 0,
+ "Retrieving the iframe children return an empty array"
+ );
+});
diff --git a/devtools/server/tests/browser/browser_inspector-insert.js b/devtools/server/tests/browser/browser_inspector-insert.js
new file mode 100644
index 0000000000..d3f2ea482d
--- /dev/null
+++ b/devtools/server/tests/browser/browser_inspector-insert.js
@@ -0,0 +1,158 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function () {
+ const { walker } = await initInspectorFront(
+ MAIN_DOMAIN + "inspector-traversal-data.html"
+ );
+
+ await testRearrange(walker);
+ await testInsertInvalidInput(walker);
+});
+
+async function testRearrange(walker) {
+ const longlist = await walker.querySelector(walker.rootNode, "#longlist");
+ let children = await walker.children(longlist);
+ const nodeA = children.nodes[0];
+ is(nodeA.id, "a", "Got the expected node.");
+
+ // Move nodeA to the end of the list.
+ await walker.insertBefore(nodeA, longlist, null);
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ ok(
+ !content.document.querySelector("#a").nextSibling,
+ "a should now be at the end of the list."
+ );
+ });
+
+ children = await walker.children(longlist);
+ is(
+ nodeA,
+ children.nodes[children.nodes.length - 1],
+ "a should now be the last returned child."
+ );
+
+ // Now move it to the middle of the list.
+ const nextNode = children.nodes[13];
+ await walker.insertBefore(nodeA, longlist, nextNode);
+
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [[nextNode.actorID]],
+ async function (actorID) {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ const {
+ DevToolsServer,
+ } = require("resource://devtools/server/devtools-server.js");
+ const {
+ DocumentWalker,
+ } = require("resource://devtools/server/actors/inspector/document-walker.js");
+ const sibling = new DocumentWalker(
+ content.document.querySelector("#a"),
+ content
+ ).nextSibling();
+ // Convert actorID to current compartment string otherwise
+ // searchAllConnectionsForActor is confused and won't find the actor.
+ actorID = String(actorID);
+ const nodeActor = DevToolsServer.searchAllConnectionsForActor(actorID);
+ is(
+ sibling,
+ nodeActor.rawNode,
+ "Node should match the expected next node."
+ );
+ }
+ );
+
+ children = await walker.children(longlist);
+ is(nodeA, children.nodes[13], "a should be where we expect it.");
+ is(nextNode, children.nodes[14], "next node should be where we expect it.");
+}
+
+async function testInsertInvalidInput(walker) {
+ const longlist = await walker.querySelector(walker.rootNode, "#longlist");
+ const children = await walker.children(longlist);
+ const nodeA = children.nodes[0];
+ const nextSibling = children.nodes[1];
+
+ // Now move it to the original location and make sure no mutation happens.
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [[longlist.actorID]],
+ async function (actorID) {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ const {
+ DevToolsServer,
+ } = require("resource://devtools/server/devtools-server.js");
+ // Convert actorID to current compartment string otherwise
+ // searchAllConnectionsForActor is confused and won't find the actor.
+ actorID = String(actorID);
+ const nodeActor = DevToolsServer.searchAllConnectionsForActor(actorID);
+ content.hasMutated = false;
+ content.observer = new content.MutationObserver(() => {
+ content.hasMutated = true;
+ });
+ content.observer.observe(nodeActor.rawNode, {
+ childList: true,
+ });
+ }
+ );
+
+ await walker.insertBefore(nodeA, longlist, nodeA);
+ let hasMutated = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async function () {
+ const state = content.hasMutated;
+ content.hasMutated = false;
+ return state;
+ }
+ );
+ ok(!hasMutated, "hasn't mutated");
+
+ await walker.insertBefore(nodeA, longlist, nextSibling);
+ hasMutated = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async function () {
+ const state = content.hasMutated;
+ content.hasMutated = false;
+ return state;
+ }
+ );
+ ok(!hasMutated, "still hasn't mutated after inserting before nextSibling");
+
+ await walker.insertBefore(nodeA, longlist);
+ hasMutated = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async function () {
+ const state = content.hasMutated;
+ content.hasMutated = false;
+ return state;
+ }
+ );
+ ok(hasMutated, "has mutated after inserting with null sibling");
+
+ await walker.insertBefore(nodeA, longlist);
+ hasMutated = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async function () {
+ const state = content.hasMutated;
+ content.hasMutated = false;
+ return state;
+ }
+ );
+ ok(!hasMutated, "hasn't mutated after inserting with null sibling again");
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ content.observer.disconnect();
+ });
+}
diff --git a/devtools/server/tests/browser/browser_inspector-isScrollable.js b/devtools/server/tests/browser/browser_inspector-isScrollable.js
new file mode 100644
index 0000000000..e28fc01ce9
--- /dev/null
+++ b/devtools/server/tests/browser/browser_inspector-isScrollable.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 URL = MAIN_DOMAIN + "inspector-isScrollable-data.html";
+
+const CASES = [
+ { id: "body", expected: false },
+ { id: "no_children", expected: false },
+ { id: "one_child_no_overflow", expected: false },
+ { id: "margin_left_overflow", expected: true },
+ { id: "transform_overflow", expected: true },
+ { id: "nested_overflow", expected: true },
+ { id: "intermediate_overflow", expected: true },
+ { id: "multiple_overflow_at_different_depths", expected: true },
+ { id: "overflow_hidden", expected: false },
+ { id: "scrollbar_none", expected: false },
+];
+
+add_task(async function () {
+ info(
+ "Test that elements with scrollbars have a true value for isScrollable, and elements without scrollbars have a false value."
+ );
+ const { walker } = await initInspectorFront(URL);
+
+ for (const { id, expected } of CASES) {
+ info(`Checking element id ${id}.`);
+
+ const el = await walker.querySelector(walker.rootNode, `#${id}`);
+ is(el.isScrollable, expected, `${id} has expected value for isScrollable.`);
+ }
+});
diff --git a/devtools/server/tests/browser/browser_inspector-mutations-childlist.js b/devtools/server/tests/browser/browser_inspector-mutations-childlist.js
new file mode 100644
index 0000000000..6818c9c8dc
--- /dev/null
+++ b/devtools/server/tests/browser/browser_inspector-mutations-childlist.js
@@ -0,0 +1,282 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/server/tests/browser/inspector-helpers.js",
+ this
+);
+
+function loadSelector(walker, selector) {
+ return walker.querySelectorAll(walker.rootNode, selector).then(nodeList => {
+ return nodeList.items();
+ });
+}
+
+function loadSelectors(walker, selectors) {
+ return Promise.all(Array.from(selectors, sel => loadSelector(walker, sel)));
+}
+
+function doMoves(movesArg) {
+ return SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [movesArg],
+ function (moves) {
+ function setParent(nodeSelector, newParentSelector) {
+ const node = content.document.querySelector(nodeSelector);
+ if (newParentSelector) {
+ const newParent = content.document.querySelector(newParentSelector);
+ newParent.appendChild(node);
+ } else {
+ node.remove();
+ }
+ }
+ for (const move of moves) {
+ setParent(move[0], move[1]);
+ }
+ }
+ );
+}
+
+/**
+ * Test a set of tree rearrangements and make sure they cause the expected changes.
+ */
+
+var gDummySerial = 0;
+
+function mutationTest(testSpec) {
+ return async function () {
+ const { walker } = await initInspectorFront(
+ MAIN_DOMAIN + "inspector-traversal-data.html"
+ );
+ await loadSelectors(walker, testSpec.load || ["html"]);
+ walker.autoCleanup = !!testSpec.autoCleanup;
+ if (testSpec.preCheck) {
+ testSpec.preCheck();
+ }
+ const onMutations = walker.once("mutations");
+
+ await doMoves(testSpec.moves || []);
+
+ // Some of these moves will trigger no mutation events,
+ // so do a dummy change to the root node to trigger
+ // a mutation event anyway.
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [[gDummySerial++]],
+ function (serial) {
+ content.document.documentElement.setAttribute("data-dummy", serial);
+ }
+ );
+
+ let mutations = await onMutations;
+
+ // Filter out our dummy mutation.
+ mutations = mutations.filter(change => {
+ if (change.type == "attributes" && change.attributeName == "data-dummy") {
+ return false;
+ }
+ return true;
+ });
+ await assertOwnershipTrees(walker);
+ if (testSpec.postCheck) {
+ testSpec.postCheck(walker, mutations);
+ }
+ };
+}
+
+// Verify that our dummy mutation works.
+add_task(
+ mutationTest({
+ autoCleanup: false,
+ postCheck(walker, mutations) {
+ is(mutations.length, 0, "Dummy mutation is filtered out.");
+ },
+ })
+);
+
+// Test a simple move to a different location in the sibling list for the same
+// parent.
+add_task(
+ mutationTest({
+ autoCleanup: false,
+ load: ["#longlist div"],
+ moves: [["#a", "#longlist"]],
+ postCheck(walker, mutations) {
+ const remove = mutations[0];
+ is(remove.type, "childList", "First mutation should be a childList.");
+ ok(!!remove.removed.length, "First mutation should be a removal.");
+ const add = mutations[1];
+ is(
+ add.type,
+ "childList",
+ "Second mutation should be a childList removal."
+ );
+ ok(!!add.added.length, "Second mutation should be an addition.");
+ const a = add.added[0];
+ is(a.id, "a", "Added node should be #a");
+ is(a.parentNode(), remove.target, "Should still be a child of longlist.");
+ is(
+ remove.target,
+ add.target,
+ "First and second mutations should be against the same node."
+ );
+ },
+ })
+);
+
+// Test a move to another location that is within our ownership tree.
+add_task(
+ mutationTest({
+ autoCleanup: false,
+ load: ["#longlist div", "#longlist-sibling"],
+ moves: [["#a", "#longlist-sibling"]],
+ postCheck(walker, mutations) {
+ const remove = mutations[0];
+ is(remove.type, "childList", "First mutation should be a childList.");
+ ok(!!remove.removed.length, "First mutation should be a removal.");
+ const add = mutations[1];
+ is(
+ add.type,
+ "childList",
+ "Second mutation should be a childList removal."
+ );
+ ok(!!add.added.length, "Second mutation should be an addition.");
+ const a = add.added[0];
+ is(a.id, "a", "Added node should be #a");
+ is(a.parentNode(), add.target, "Should still be a child of longlist.");
+ is(
+ add.target.id,
+ "longlist-sibling",
+ "long-sibling should be the target."
+ );
+ },
+ })
+);
+
+// Move an unseen node with a seen parent into our ownership tree - should generate a
+// childList pair with no adds or removes.
+add_task(
+ mutationTest({
+ autoCleanup: false,
+ load: ["#longlist"],
+ moves: [["#longlist-sibling", "#longlist"]],
+ postCheck(walker, mutations) {
+ is(mutations.length, 2, "Should generate two mutations");
+ is(mutations[0].type, "childList", "Should be childList mutations.");
+ is(mutations[0].added.length, 0, "Should have no adds.");
+ is(mutations[0].removed.length, 0, "Should have no removes.");
+ is(mutations[1].type, "childList", "Should be childList mutations.");
+ is(mutations[1].added.length, 0, "Should have no adds.");
+ is(mutations[1].removed.length, 0, "Should have no removes.");
+ },
+ })
+);
+
+// Move an unseen node with an unseen parent into our ownership tree. Should only
+// generate one childList mutation with no adds or removes.
+add_task(
+ mutationTest({
+ autoCleanup: false,
+ load: ["#longlist div"],
+ moves: [["#longlist-sibling-firstchild", "#longlist"]],
+ postCheck(walker, mutations) {
+ is(mutations.length, 1, "Should generate two mutations");
+ is(mutations[0].type, "childList", "Should be childList mutations.");
+ is(mutations[0].added.length, 0, "Should have no adds.");
+ is(mutations[0].removed.length, 0, "Should have no removes.");
+ },
+ })
+);
+
+// Move a node between unseen nodes, should generate no mutations.
+add_task(
+ mutationTest({
+ autoCleanup: false,
+ load: ["html"],
+ moves: [["#longlist-sibling", "#longlist"]],
+ postCheck(walker, mutations) {
+ is(mutations.length, 0, "Should generate no mutations.");
+ },
+ })
+);
+
+// Orphan a node and don't clean it up
+add_task(
+ mutationTest({
+ autoCleanup: false,
+ load: ["#longlist div"],
+ moves: [["#longlist", null]],
+ postCheck(walker, mutations) {
+ is(mutations.length, 1, "Should generate one mutation.");
+ const change = mutations[0];
+ is(change.type, "childList", "Should be a childList.");
+ is(change.removed.length, 1, "Should have removed a child.");
+ const ownership = clientOwnershipTree(walker);
+ is(ownership.orphaned.length, 1, "Should have one orphaned subtree.");
+ is(
+ ownershipTreeSize(ownership.orphaned[0]),
+ 1 + 26 + 26,
+ "Should have orphaned longlist, and 26 children, and 26 singleTextChilds"
+ );
+ },
+ })
+);
+
+// Orphan a node, and do clean it up.
+add_task(
+ mutationTest({
+ autoCleanup: true,
+ load: ["#longlist div"],
+ moves: [["#longlist", null]],
+ postCheck(walker, mutations) {
+ is(mutations.length, 1, "Should generate one mutation.");
+ const change = mutations[0];
+ is(change.type, "childList", "Should be a childList.");
+ is(change.removed.length, 1, "Should have removed a child.");
+ const ownership = clientOwnershipTree(walker);
+ is(ownership.orphaned.length, 0, "Should have no orphaned subtrees.");
+ },
+ })
+);
+
+// Orphan a node by moving it into the tree but out of our visible subtree.
+add_task(
+ mutationTest({
+ autoCleanup: false,
+ load: ["#longlist div"],
+ moves: [["#longlist", "#longlist-sibling"]],
+ postCheck(walker, mutations) {
+ is(mutations.length, 1, "Should generate one mutation.");
+ const change = mutations[0];
+ is(change.type, "childList", "Should be a childList.");
+ is(change.removed.length, 1, "Should have removed a child.");
+ const ownership = clientOwnershipTree(walker);
+ is(ownership.orphaned.length, 1, "Should have one orphaned subtree.");
+ is(
+ ownershipTreeSize(ownership.orphaned[0]),
+ 1 + 26 + 26,
+ "Should have orphaned longlist, 26 children, and 26 singleTextChilds."
+ );
+ },
+ })
+);
+
+// Orphan a node by moving it into the tree but out of our visible subtree,
+// and clean it up.
+add_task(
+ mutationTest({
+ autoCleanup: true,
+ load: ["#longlist div"],
+ moves: [["#longlist", "#longlist-sibling"]],
+ postCheck(walker, mutations) {
+ is(mutations.length, 1, "Should generate one mutation.");
+ const change = mutations[0];
+ is(change.type, "childList", "Should be a childList.");
+ is(change.removed.length, 1, "Should have removed a child.");
+ const ownership = clientOwnershipTree(walker);
+ is(ownership.orphaned.length, 0, "Should have no orphaned subtrees.");
+ },
+ })
+);
diff --git a/devtools/server/tests/browser/browser_inspector-release.js b/devtools/server/tests/browser/browser_inspector-release.js
new file mode 100644
index 0000000000..5546da605a
--- /dev/null
+++ b/devtools/server/tests/browser/browser_inspector-release.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/server/tests/browser/inspector-helpers.js",
+ this
+);
+
+add_task(async function loadNewChild() {
+ const { target, walker } = await initInspectorFront(
+ MAIN_DOMAIN + "inspector-traversal-data.html"
+ );
+
+ let originalOwnershipSize = 0;
+ let longlist = null;
+ let firstChild = null;
+ const list = await walker.querySelectorAll(walker.rootNode, "#longlist div");
+ // Make sure we have the 26 children of longlist in our ownership tree.
+ is(list.length, 26, "Expect 26 div children.");
+ // Make sure we've read in all those children and incorporated them
+ // in our ownership tree.
+ const items = await list.items();
+ originalOwnershipSize = await assertOwnershipTrees(walker);
+
+ // Here is how the ownership tree is summed up:
+ // #document 1
+ // <html> 1
+ // <body> 1
+ // <div id=longlist> 1
+ // <div id=a>a</div> 26*2 (each child plus it's singleTextChild)
+ // ...
+ // <div id=z>z</div>
+ // -----
+ // 56
+ is(originalOwnershipSize, 56, "Correct number of items in ownership tree");
+ firstChild = items[0].actorID;
+ // Now get the longlist and release it from the ownership tree.
+ const node = await walker.querySelector(walker.rootNode, "#longlist");
+ longlist = node.actorID;
+ await walker.releaseNode(node);
+ // Our ownership size should now be 53 fewer
+ // (we forgot about #longlist + 26 children + 26 singleTextChild nodes)
+ const newOwnershipSize = await assertOwnershipTrees(walker);
+ is(
+ newOwnershipSize,
+ originalOwnershipSize - 53,
+ "Ownership tree should be lower"
+ );
+ // Now verify that some nodes have gone away
+ await checkMissing(target, longlist);
+ await checkMissing(target, firstChild);
+});
diff --git a/devtools/server/tests/browser/browser_inspector-remove.js b/devtools/server/tests/browser/browser_inspector-remove.js
new file mode 100644
index 0000000000..8338e40ea2
--- /dev/null
+++ b/devtools/server/tests/browser/browser_inspector-remove.js
@@ -0,0 +1,102 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/server/tests/browser/inspector-helpers.js",
+ this
+);
+
+add_task(async function testRemoveSubtree() {
+ const { target, walker } = await initInspectorFront(
+ MAIN_DOMAIN + "inspector-traversal-data.html"
+ );
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ function ignoreNode(node) {
+ // Duplicate the walker logic to skip blank nodes...
+ return (
+ node.nodeType === content.Node.TEXT_NODE &&
+ !/[^\s]/.test(node.nodeValue)
+ );
+ }
+
+ let nextSibling = content.document.querySelector("#longlist").nextSibling;
+ while (nextSibling && ignoreNode(nextSibling)) {
+ nextSibling = nextSibling.nextSibling;
+ }
+
+ let previousSibling =
+ content.document.querySelector("#longlist").previousSibling;
+ while (previousSibling && ignoreNode(previousSibling)) {
+ previousSibling = previousSibling.previousSibling;
+ }
+ content.nextSibling = nextSibling;
+ content.previousSibling = previousSibling;
+ });
+
+ let originalOwnershipSize = 0;
+ const longlist = await walker.querySelector(walker.rootNode, "#longlist");
+ const longlistID = longlist.actorID;
+ await walker.children(longlist);
+ originalOwnershipSize = await assertOwnershipTrees(walker);
+ // Here is how the ownership tree is summed up:
+ // #document 1
+ // <html> 1
+ // <body> 1
+ // <div id=longlist> 1
+ // <div id=a>a</div> 26*2 (each child plus it's singleTextChild)
+ // ...
+ // <div id=z>z</div>
+ // -----
+ // 56
+ is(originalOwnershipSize, 56, "Correct number of items in ownership tree");
+
+ const onMutation = waitForMutation(walker, isChildList);
+ const siblings = await walker.removeNode(longlist);
+
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [[siblings.previousSibling.actorID, siblings.nextSibling.actorID]],
+ function ([previousActorID, nextActorID]) {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ const {
+ DevToolsServer,
+ } = require("resource://devtools/server/devtools-server.js");
+
+ // Convert actorID to current compartment string otherwise
+ // searchAllConnectionsForActor is confused and won't find the actor.
+ previousActorID = String(previousActorID);
+ nextActorID = String(nextActorID);
+ const previous =
+ DevToolsServer.searchAllConnectionsForActor(previousActorID);
+ const next = DevToolsServer.searchAllConnectionsForActor(nextActorID);
+
+ is(
+ previous.rawNode,
+ content.previousSibling,
+ "Should have returned the previous sibling."
+ );
+ is(
+ next.rawNode,
+ content.nextSibling,
+ "Should have returned the next sibling."
+ );
+ }
+ );
+ await onMutation;
+ // Our ownership size should now be 51 fewer (we forgot about #longlist + 26
+ // children + 26 singleTextChild nodes, but learned about #longlist's
+ // prev/next sibling)
+ const newOwnershipSize = await assertOwnershipTrees(walker);
+ is(
+ newOwnershipSize,
+ originalOwnershipSize - 51,
+ "Ownership tree should be lower"
+ );
+ // Now verify that some nodes have gone away
+ return checkMissing(target, longlistID);
+});
diff --git a/devtools/server/tests/browser/browser_inspector-retain.js b/devtools/server/tests/browser/browser_inspector-retain.js
new file mode 100644
index 0000000000..43d156675e
--- /dev/null
+++ b/devtools/server/tests/browser/browser_inspector-retain.js
@@ -0,0 +1,157 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/server/tests/browser/inspector-helpers.js",
+ this
+);
+
+// Retain a node, and a second-order child (in another document, for kicks)
+// Release the parent of the top item, which should cause one retained orphan.
+
+// Then unretain the top node, which should retain the orphan.
+
+// Then change the source of the iframe, which should kill that orphan.
+
+add_task(async function testRetain() {
+ // The test does not make sense when EFT is enabled, as different documents will have
+ // different walkers.
+ if (isEveryFrameTargetEnabled()) {
+ return;
+ }
+
+ const { walker } = await initInspectorFront(
+ MAIN_DOMAIN + "inspector-traversal-data.html"
+ );
+
+ // Get the toplevel body element and retain it.
+ const bodyFront = await walker.querySelector(walker.rootNode, "body");
+ await walker.retainNode(bodyFront);
+ // Get an element in the child frame and retain it.
+ const frame = await walker.querySelector(walker.rootNode, "#childFrame");
+ const children = await walker.children(frame, { maxNodes: 1 });
+ const childDoc = children.nodes[0];
+ const childListFront = await walker.querySelector(childDoc, "#longlist");
+ const originalOwnershipSize = await assertOwnershipTrees(walker);
+ // and retain it.
+ await walker.retainNode(childListFront);
+ // OK, try releasing the parent of the first retained.
+ await walker.releaseNode(bodyFront.parentNode());
+ const clientTree = clientOwnershipTree(walker);
+
+ // That request should have freed the parent of the first retained
+ // but moved the rest into the retained orphaned tree.
+ is(
+ ownershipTreeSize(clientTree.root) +
+ ownershipTreeSize(clientTree.retained[0]) +
+ 1,
+ originalOwnershipSize,
+ "Should have only lost one item overall."
+ );
+ is(walker._retainedOrphans.size, 1, "Should have retained one orphan");
+ ok(
+ walker._retainedOrphans.has(bodyFront),
+ "Should have retained the expected node."
+ );
+ // Unretain the body, which should promote the childListFront to a retained orphan.
+ await walker.unretainNode(bodyFront);
+ await assertOwnershipTrees(walker);
+
+ is(
+ walker._retainedOrphans.size,
+ 1,
+ "Should still only have one retained orphan."
+ );
+ ok(
+ !walker._retainedOrphans.has(bodyFront),
+ "Should have dropped the body node."
+ );
+ ok(
+ walker._retainedOrphans.has(childListFront),
+ "Should have retained the child node."
+ );
+
+ // Change the source of the iframe, which should kill the retained orphan.
+ const onMutations = waitForMutation(walker, isUnretained);
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ content.document.querySelector("#childFrame").src =
+ "data:text/html,<html>new child</html>";
+ });
+ await onMutations;
+
+ await assertOwnershipTrees(walker);
+ is(walker._retainedOrphans.size, 0, "Should have no more retained orphans.");
+});
+
+// Get a hold of a node, remove it from the doc and retain it at the same time.
+// We should always win that race (even though the mutation happens before the
+// retain request), because we haven't issued `getMutations` yet.
+add_task(async function testWinRace() {
+ const { walker } = await initInspectorFront(
+ MAIN_DOMAIN + "inspector-traversal-data.html"
+ );
+
+ const front = await walker.querySelector(walker.rootNode, "#a");
+ const onMutation = waitForMutation(walker, isChildList);
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ const contentNode = content.document.querySelector("#a");
+ contentNode.remove();
+ });
+ // Now wait for that mutation and retain response to come in.
+ await walker.retainNode(front);
+ await onMutation;
+
+ await assertOwnershipTrees(walker);
+ is(walker._retainedOrphans.size, 1, "Should have a retained orphan.");
+ ok(
+ walker._retainedOrphans.has(front),
+ "Should have retained our expected node."
+ );
+ await walker.unretainNode(front);
+
+ // Make sure we're clear for the next test.
+ await assertOwnershipTrees(walker);
+ is(walker._retainedOrphans.size, 0, "Should have no more retained orphans.");
+});
+
+// Same as above, but issue the request right after the 'new-mutations' event, so that
+// we *lose* the race.
+add_task(async function testLoseRace() {
+ const { walker } = await initInspectorFront(
+ MAIN_DOMAIN + "inspector-traversal-data.html"
+ );
+
+ const front = await walker.querySelector(walker.rootNode, "#z");
+ const onMutation = walker.once("new-mutations");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ const contentNode = content.document.querySelector("#z");
+ contentNode.remove();
+ });
+ await onMutation;
+
+ // Verify that we have an outstanding request (no good way to tell that it's a
+ // getMutations request, but there's nothing else it would be).
+ is(walker._requests.length, 1, "Should have an outstanding request.");
+ try {
+ await walker.retainNode(front);
+ ok(false, "Request should not have succeeded!");
+ } catch (err) {
+ // XXX: Switched to from ok() to todo_is() in Bug 1467712. Follow up in
+ // 1500960
+ // This is throwing because of
+ // `gInspectee.querySelector("#z").parentNode = null;` two blocks above...
+ // Even if you fix that, the test is still failing because "#a" was removed
+ // by the previous test. I am switching this to "#z" because I think that
+ // was the original intent. Still not failing with the expected error message
+ // Needs more work.
+ // ok(err, "noSuchActor", "Should have lost the race.");
+ is(
+ walker._retainedOrphans.size,
+ 0,
+ "Should have no more retained orphans."
+ );
+ // Don't re-throw the error.
+ }
+});
diff --git a/devtools/server/tests/browser/browser_inspector-search.js b/devtools/server/tests/browser/browser_inspector-search.js
new file mode 100644
index 0000000000..21cf745ce1
--- /dev/null
+++ b/devtools/server/tests/browser/browser_inspector-search.js
@@ -0,0 +1,347 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/server/tests/browser/inspector-helpers.js",
+ this
+);
+
+// Test for Bug 835896
+// WalkerSearch specific tests. This is to make sure search results are
+// coming back as expected.
+// See also test_inspector-search-front.html.
+
+add_task(async function () {
+ const { walker } = await initInspectorFront(
+ MAIN_DOMAIN + "inspector-search-data.html"
+ );
+
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [[walker.actorID]],
+ async function (actorID) {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ const {
+ DevToolsServer,
+ } = require("resource://devtools/server/devtools-server.js");
+ const {
+ DocumentWalker: _documentWalker,
+ } = require("resource://devtools/server/actors/inspector/document-walker.js");
+
+ // Convert actorID to current compartment string otherwise
+ // searchAllConnectionsForActor is confused and won't find the actor.
+ actorID = String(actorID);
+ const walkerActor = DevToolsServer.searchAllConnectionsForActor(actorID);
+ const walkerSearch = walkerActor.walkerSearch;
+ const {
+ WalkerSearch,
+ WalkerIndex,
+ } = require("resource://devtools/server/actors/utils/walker-search.js");
+
+ info("Testing basic index APIs exist.");
+ const index = new WalkerIndex(walkerActor);
+ Assert.greater(
+ index.data.size,
+ 0,
+ "public index is filled after getting"
+ );
+
+ index.clearIndex();
+ ok(!index._data, "private index is empty after clearing");
+ Assert.greater(
+ index.data.size,
+ 0,
+ "public index is filled after getting"
+ );
+
+ index.destroy();
+
+ info("Testing basic search APIs exist.");
+
+ ok(walkerSearch, "walker search exists on the WalkerActor");
+ ok(walkerSearch.search, "walker search has `search` method");
+ ok(walkerSearch.index, "walker search has `index` property");
+ is(
+ walkerSearch.walker,
+ walkerActor,
+ "referencing the correct WalkerActor"
+ );
+
+ const walkerSearch2 = new WalkerSearch(walkerActor);
+ ok(walkerSearch2, "a new search instance can be created");
+ ok(walkerSearch2.search, "new search instance has `search` method");
+ ok(walkerSearch2.index, "new search instance has `index` property");
+ isnot(
+ walkerSearch2,
+ walkerSearch,
+ "new search instance differs from the WalkerActor's"
+ );
+
+ walkerSearch2.destroy();
+
+ info("Testing search with an empty query.");
+ let results = walkerSearch.search("");
+ is(results.length, 0, "No results when searching for ''");
+
+ results = walkerSearch.search(null);
+ is(results.length, 0, "No results when searching for null");
+
+ results = walkerSearch.search(undefined);
+ is(results.length, 0, "No results when searching for undefined");
+
+ results = walkerSearch.search(10);
+ is(results.length, 0, "No results when searching for 10");
+
+ const inspectee = content.document;
+ const testData = [
+ {
+ desc: "Search for tag with one result.",
+ search: "body",
+ expected: [{ node: inspectee.body, type: "tag" }],
+ },
+ {
+ desc: "Search for tag with multiple results",
+ search: "h2",
+ expected: [
+ { node: inspectee.querySelectorAll("h2")[0], type: "tag" },
+ { node: inspectee.querySelectorAll("h2")[1], type: "tag" },
+ { node: inspectee.querySelectorAll("h2")[2], type: "tag" },
+ ],
+ },
+ {
+ desc: "Search for selector with multiple results",
+ search: "body > h2",
+ expected: [
+ { node: inspectee.querySelectorAll("h2")[0], type: "selector" },
+ { node: inspectee.querySelectorAll("h2")[1], type: "selector" },
+ { node: inspectee.querySelectorAll("h2")[2], type: "selector" },
+ ],
+ },
+ {
+ desc: "Search for selector with multiple results",
+ search: ":root h2",
+ expected: [
+ { node: inspectee.querySelectorAll("h2")[0], type: "selector" },
+ { node: inspectee.querySelectorAll("h2")[1], type: "selector" },
+ { node: inspectee.querySelectorAll("h2")[2], type: "selector" },
+ ],
+ },
+ {
+ desc: "Search for selector with multiple results",
+ search: "* h2",
+ expected: [
+ { node: inspectee.querySelectorAll("h2")[0], type: "selector" },
+ { node: inspectee.querySelectorAll("h2")[1], type: "selector" },
+ { node: inspectee.querySelectorAll("h2")[2], type: "selector" },
+ ],
+ },
+ {
+ desc: "Search with multiple matches in a single tag expecting a single result",
+ search: "💩",
+ expected: [
+ { node: inspectee.getElementById("💩"), type: "attributeValue" },
+ ],
+ },
+ {
+ desc: "Search that has tag and text results",
+ search: "h1",
+ expected: [
+ { node: inspectee.querySelector("h1"), type: "tag" },
+ {
+ node: inspectee.querySelector("h1 + p").childNodes[0],
+ type: "text",
+ },
+ {
+ node: inspectee.querySelector("h1 + p > strong").childNodes[0],
+ type: "text",
+ },
+ ],
+ },
+ {
+ desc: "Search for XPath with one result",
+ search: "//strong",
+ expected: [
+ { node: inspectee.querySelector("strong"), type: "xpath" },
+ ],
+ },
+ {
+ desc: "Search for XPath with multiple results",
+ search: "//h2",
+ expected: [
+ { node: inspectee.querySelectorAll("h2")[0], type: "xpath" },
+ { node: inspectee.querySelectorAll("h2")[1], type: "xpath" },
+ { node: inspectee.querySelectorAll("h2")[2], type: "xpath" },
+ ],
+ },
+ {
+ desc: "Search for XPath via containing text",
+ search: "//*[contains(text(), 'p tag')]",
+ expected: [{ node: inspectee.querySelector("p"), type: "xpath" }],
+ },
+ {
+ desc: "Search for XPath matching text node",
+ search: "//strong/text()",
+ expected: [
+ {
+ node: inspectee.querySelector("strong").firstChild,
+ type: "xpath",
+ },
+ ],
+ },
+ {
+ desc: "Search using XPath grouping expression",
+ search: "(//*)[2]",
+ expected: [{ node: inspectee.querySelector("head"), type: "xpath" }],
+ },
+ {
+ desc: "Search using XPath function",
+ search: "id('arrows')",
+ expected: [
+ { node: inspectee.querySelector("#arrows"), type: "xpath" },
+ ],
+ },
+ ];
+
+ const isDeeply = (a, b, msg) => {
+ return is(JSON.stringify(a), JSON.stringify(b), msg);
+ };
+ for (const { desc, search, expected } of testData) {
+ info("Running test: " + desc);
+ results = walkerSearch.search(search);
+ isDeeply(
+ results,
+ expected,
+ "Search returns correct results with '" + search + "'"
+ );
+ }
+
+ info("Testing ::before and ::after element matching");
+
+ const beforeElt = new _documentWalker(
+ inspectee.querySelector("#pseudo"),
+ inspectee.defaultView
+ ).firstChild();
+ const afterElt = new _documentWalker(
+ inspectee.querySelector("#pseudo"),
+ inspectee.defaultView
+ ).lastChild();
+ const styleText = inspectee.querySelector("style").childNodes[0];
+
+ // ::before
+ results = walkerSearch.search("::before");
+ isDeeply(
+ results,
+ [{ node: beforeElt, type: "tag" }],
+ "Tag search works for pseudo element"
+ );
+
+ results = walkerSearch.search("_moz_generated_content_before");
+ is(results.length, 0, "No results for anon tag name");
+
+ results = walkerSearch.search("before element");
+ isDeeply(
+ results,
+ [
+ { node: styleText, type: "text" },
+ { node: beforeElt, type: "text" },
+ ],
+ "Text search works for pseudo element"
+ );
+
+ // ::after
+ results = walkerSearch.search("::after");
+ isDeeply(
+ results,
+ [{ node: afterElt, type: "tag" }],
+ "Tag search works for pseudo element"
+ );
+
+ results = walkerSearch.search("_moz_generated_content_after");
+ is(results.length, 0, "No results for anon tag name");
+
+ results = walkerSearch.search("after element");
+ isDeeply(
+ results,
+ [
+ { node: styleText, type: "text" },
+ { node: afterElt, type: "text" },
+ ],
+ "Text search works for pseudo element"
+ );
+
+ info("Testing search before and after a mutation.");
+ const expected = [
+ { node: inspectee.querySelectorAll("h3")[0], type: "tag" },
+ { node: inspectee.querySelectorAll("h3")[1], type: "tag" },
+ { node: inspectee.querySelectorAll("h3")[2], type: "tag" },
+ ];
+
+ results = walkerSearch.search("h3");
+ isDeeply(results, expected, "Search works with tag results");
+
+ function mutateDocumentAndWaitForMutation(mutationFn) {
+ // eslint-disable-next-line new-cap
+ return new Promise(resolve => {
+ info("Listening to markup mutation on the inspectee");
+ const observer = new inspectee.defaultView.MutationObserver(resolve);
+ observer.observe(inspectee, { childList: true, subtree: true });
+ mutationFn();
+ });
+ }
+ await mutateDocumentAndWaitForMutation(() => {
+ expected[0].node.remove();
+ });
+
+ results = walkerSearch.search("h3");
+ isDeeply(
+ results,
+ [expected[1], expected[2]],
+ "Results are updated after removal"
+ );
+
+ // eslint-disable-next-line new-cap
+ await new Promise(resolve => {
+ info("Waiting for a mutation to happen");
+ const observer = new inspectee.defaultView.MutationObserver(() => {
+ resolve();
+ });
+ observer.observe(inspectee, { attributes: true, subtree: true });
+ inspectee.body.setAttribute("h3", "true");
+ });
+
+ results = walkerSearch.search("h3");
+ isDeeply(
+ results,
+ [
+ { node: inspectee.body, type: "attributeName" },
+ expected[1],
+ expected[2],
+ ],
+ "Results are updated after addition"
+ );
+
+ // eslint-disable-next-line new-cap
+ await new Promise(resolve => {
+ info("Waiting for a mutation to happen");
+ const observer = new inspectee.defaultView.MutationObserver(() => {
+ resolve();
+ });
+ observer.observe(inspectee, {
+ attributes: true,
+ childList: true,
+ subtree: true,
+ });
+ inspectee.body.removeAttribute("h3");
+ expected[1].node.remove();
+ expected[2].node.remove();
+ });
+
+ results = walkerSearch.search("h3");
+ is(results.length, 0, "Results are updated after removal");
+ }
+ );
+});
diff --git a/devtools/server/tests/browser/browser_inspector-shadow.js b/devtools/server/tests/browser/browser_inspector-shadow.js
new file mode 100644
index 0000000000..7675593c96
--- /dev/null
+++ b/devtools/server/tests/browser/browser_inspector-shadow.js
@@ -0,0 +1,231 @@
+"use strict";
+
+const URL = MAIN_DOMAIN + "inspector-shadow.html";
+
+add_task(async function () {
+ info("Test that a shadow host has a shadow root");
+ const { walker } = await initInspectorFront(URL);
+
+ const el = await walker.querySelector(walker.rootNode, "#empty");
+ const children = await walker.children(el);
+
+ is(el.displayName, "test-empty", "#empty exists");
+ ok(el.isShadowHost, "#empty is a shadow host");
+
+ const shadowRoot = children.nodes[0];
+ ok(shadowRoot.isShadowRoot, "#empty has a shadow-root child");
+ is(children.nodes.length, 1, "#empty has no other children");
+});
+
+add_task(async function () {
+ info("Test that a shadow host has its children too");
+ const { walker } = await initInspectorFront(URL);
+
+ const el = await walker.querySelector(walker.rootNode, "#one-child");
+ const children = await walker.children(el);
+
+ is(
+ children.nodes.length,
+ 2,
+ "#one-child has two children " + "(shadow root + another child)"
+ );
+ ok(children.nodes[0].isShadowRoot, "First child is a shadow-root");
+ is(children.nodes[1].displayName, "h1", "Second child is <h1>");
+});
+
+add_task(async function () {
+ info("Test that shadow-root has its children");
+ const { walker } = await initInspectorFront(URL);
+
+ const el = await walker.querySelector(walker.rootNode, "#shadow-children");
+ ok(el.isShadowHost, "#shadow-children is a shadow host");
+
+ const children = await walker.children(el);
+ ok(
+ children.nodes.length === 1 && children.nodes[0].isShadowRoot,
+ "#shadow-children has only one child and it's a shadow-root"
+ );
+
+ const shadowRoot = children.nodes[0];
+ const shadowChildren = await walker.children(shadowRoot);
+ is(shadowChildren.nodes.length, 2, "shadow-root has two children");
+ is(shadowChildren.nodes[0].displayName, "h1", "First child is <h1>");
+ is(shadowChildren.nodes[1].displayName, "p", "Second child is <p>");
+});
+
+add_task(async function () {
+ info("Test that shadow root has its children and slotted nodes");
+ const { walker } = await initInspectorFront(URL);
+
+ const el = await walker.querySelector(walker.rootNode, "#named-slot");
+ ok(el.isShadowHost, "#named-slot is a shadow host");
+
+ const children = await walker.children(el);
+ is(children.nodes.length, 2, "#named-slot has two children");
+ const shadowRoot = children.nodes[0];
+ ok(shadowRoot.isShadowRoot, "#named-slot has a shadow-root child");
+
+ const slotted = children.nodes[1];
+ is(
+ slotted.getAttribute("slot"),
+ "slot1",
+ "#named-slot as a child that is slotted"
+ );
+
+ const shadowChildren = await walker.children(shadowRoot);
+ is(
+ shadowChildren.nodes[0].displayName,
+ "h1",
+ "shadow-root first child is a regular <h1> tag"
+ );
+ is(
+ shadowChildren.nodes[1].displayName,
+ "slot",
+ "shadow-root second child is a slot"
+ );
+
+ const slottedChildren = await walker.children(shadowChildren.nodes[1]);
+ is(
+ slottedChildren.nodes[0],
+ slotted,
+ "The slot has the slotted node as a child"
+ );
+});
+
+add_task(async function () {
+ info("Test pseudoelements in shadow host");
+ const { walker } = await initInspectorFront(URL);
+
+ const el = await walker.querySelector(walker.rootNode, "#host-pseudo");
+ const children = await walker.children(el);
+
+ ok(children.nodes[0].isShadowRoot, "#host-pseudo 1st child is a shadow root");
+ ok(
+ children.nodes[1].isBeforePseudoElement,
+ "#host-pseudo 2nd child is ::before"
+ );
+ ok(
+ children.nodes[2].isAfterPseudoElement,
+ "#host-pseudo 3rd child is ::after"
+ );
+});
+
+add_task(async function () {
+ info("Test pseudoelements in slotted nodes");
+ const { walker } = await initInspectorFront(URL);
+
+ const el = await walker.querySelector(walker.rootNode, "#slot-pseudo");
+ const shadowRoot = (await walker.children(el)).nodes[0];
+ ok(shadowRoot.isShadowRoot, "#slot-pseudo has a shadow-root child");
+
+ const shadowChildren = await walker.children(shadowRoot);
+ is(shadowChildren.nodes[1].displayName, "slot", "shadow-root has a slot");
+
+ const slottedChildren = await walker.children(shadowChildren.nodes[1]);
+ ok(slottedChildren.nodes[0].isBeforePseudoElement, "slot has ::before");
+ ok(
+ slottedChildren.nodes[slottedChildren.nodes.length - 1]
+ .isAfterPseudoElement,
+ "slot has ::after"
+ );
+});
+
+add_task(async function () {
+ info("Test open/closed modes in shadow roots");
+ const { walker } = await initInspectorFront(URL);
+
+ const openEl = await walker.querySelector(walker.rootNode, "#mode-open");
+ const openShadowRoot = (await walker.children(openEl)).nodes[0];
+ const closedEl = await walker.querySelector(walker.rootNode, "#mode-closed");
+ const closedShadowRoot = (await walker.children(closedEl)).nodes[0];
+
+ is(
+ openShadowRoot.shadowRootMode,
+ "open",
+ "#mode-open has a shadow root with open mode"
+ );
+ is(
+ closedShadowRoot.shadowRootMode,
+ "closed",
+ "#mode-closed has a shadow root with closed mode"
+ );
+});
+
+add_task(async function () {
+ info("Test that slotted inline text nodes appear in the Shadow DOM tree");
+ const { walker } = await initInspectorFront(URL);
+
+ const el = await walker.querySelector(walker.rootNode, "#slot-inline-text");
+ const hostChildren = await walker.children(el);
+ const originalSlot = hostChildren.nodes[1];
+ is(
+ originalSlot.displayName,
+ "#text",
+ "Shadow host as a text node to be slotted"
+ );
+
+ const shadowRoot = hostChildren.nodes[0];
+ const shadowChildren = await walker.children(shadowRoot);
+ const slot = shadowChildren.nodes[0];
+ is(slot.displayName, "slot", "shadow-root has a slot child");
+ ok(!slot._form.inlineTextChild, "Slotted node is not an inline text");
+
+ const slotChildren = await walker.children(slot);
+ const slotted = slotChildren.nodes[0];
+ is(slotted.displayName, "#text", "Slotted node is a text node");
+ is(
+ slotted._form.nodeValue,
+ originalSlot._form.nodeValue,
+ "Slotted content is the same as original's"
+ );
+});
+
+add_task(async function () {
+ info("Test UA widgets when showAllAnonymousContent is true");
+ await SpecialPowers.pushPrefEnv({
+ set: [["devtools.inspector.showAllAnonymousContent", true]],
+ });
+
+ const { walker } = await initInspectorFront(URL);
+
+ let el = await walker.querySelector(walker.rootNode, "#video-controls");
+ let hostChildren = await walker.children(el);
+ is(hostChildren.nodes.length, 3, "#video-controls tag has 3 children");
+ const shadowRoot = hostChildren.nodes[0];
+ ok(shadowRoot.isShadowRoot, "#video-controls has a shadow-root child");
+
+ el = await walker.querySelector(
+ walker.rootNode,
+ "#video-controls-with-children"
+ );
+ hostChildren = await walker.children(el);
+ is(
+ hostChildren.nodes.length,
+ 4,
+ "#video-controls-with-children has 4 children"
+ );
+});
+
+add_task(async function () {
+ info("Test UA widgets when showAllAnonymousContent is false");
+ await SpecialPowers.pushPrefEnv({
+ set: [["devtools.inspector.showAllAnonymousContent", false]],
+ });
+
+ const { walker } = await initInspectorFront(URL);
+
+ let el = await walker.querySelector(walker.rootNode, "#video-controls");
+ let hostChildren = await walker.children(el);
+ is(hostChildren.nodes.length, 0, "#video-controls tag has no children");
+
+ el = await walker.querySelector(
+ walker.rootNode,
+ "#video-controls-with-children"
+ );
+ hostChildren = await walker.children(el);
+ is(
+ hostChildren.nodes.length,
+ 1,
+ "#video-controls-with-children has one child"
+ );
+});
diff --git a/devtools/server/tests/browser/browser_inspector-traversal.js b/devtools/server/tests/browser/browser_inspector-traversal.js
new file mode 100644
index 0000000000..786521cc18
--- /dev/null
+++ b/devtools/server/tests/browser/browser_inspector-traversal.js
@@ -0,0 +1,350 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/server/tests/browser/inspector-helpers.js",
+ this
+);
+
+const checkActorIDs = [];
+
+add_task(async function loadNewChild() {
+ const { walker } = await initInspectorFront(
+ MAIN_DOMAIN + "inspector-traversal-data.html"
+ );
+
+ // Make sure that refetching the root document of the walker returns the same
+ // actor as the getWalker returned.
+ const root = await walker.document();
+ Assert.strictEqual(
+ root,
+ walker.rootNode,
+ "Re-fetching the document node should match the root document node."
+ );
+ checkActorIDs.push(root.actorID);
+ await assertOwnershipTrees(walker);
+});
+
+add_task(async function testInnerHTML() {
+ const { walker } = await initInspectorFront(
+ MAIN_DOMAIN + "inspector-traversal-data.html"
+ );
+
+ const docElement = await walker.documentElement();
+ const longstring = await walker.innerHTML(docElement);
+ const innerHTML = await longstring.string();
+ const actualInnerHTML = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ function () {
+ return content.document.documentElement.innerHTML;
+ }
+ );
+ Assert.strictEqual(innerHTML, actualInnerHTML, "innerHTML should match");
+});
+
+add_task(async function testOuterHTML() {
+ const { walker } = await initInspectorFront(
+ MAIN_DOMAIN + "inspector-traversal-data.html"
+ );
+
+ const docElement = await walker.documentElement();
+ const longstring = await walker.outerHTML(docElement);
+ const outerHTML = await longstring.string();
+ const actualOuterHTML = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ function () {
+ return content.document.documentElement.outerHTML;
+ }
+ );
+ Assert.strictEqual(outerHTML, actualOuterHTML, "outerHTML should match");
+});
+
+add_task(async function testSetOuterHTMLNode() {
+ const { walker } = await initInspectorFront(
+ MAIN_DOMAIN + "inspector-traversal-data.html"
+ );
+ const newHTML = '<p id="edit-html-done">after edit</p>';
+ let node = await walker.querySelector(walker.rootNode, "#edit-html");
+ await walker.setOuterHTML(node, newHTML);
+ node = await walker.querySelector(walker.rootNode, "#edit-html-done");
+ const longstring = await walker.outerHTML(node);
+ const outerHTML = await longstring.string();
+ is(outerHTML, newHTML, "outerHTML has been updated");
+ node = await walker.querySelector(walker.rootNode, "#edit-html");
+ ok(!node, "The node with the old ID cannot be selected anymore");
+});
+
+add_task(async function testQuerySelector() {
+ const { walker } = await initInspectorFront(
+ MAIN_DOMAIN + "inspector-traversal-data.html"
+ );
+ let node = await walker.querySelector(walker.rootNode, "#longlist");
+ is(
+ node.getAttribute("data-test"),
+ "exists",
+ "should have found the right node"
+ );
+ await assertOwnershipTrees(walker);
+ node = await walker.querySelector(walker.rootNode, "unknownqueryselector");
+ ok(!node, "Should not find a node here.");
+ await assertOwnershipTrees(walker);
+});
+
+add_task(async function testQuerySelectors() {
+ const { target, walker } = await initInspectorFront(
+ MAIN_DOMAIN + "inspector-traversal-data.html"
+ );
+ const nodeList = await walker.querySelectorAll(
+ walker.rootNode,
+ "#longlist div"
+ );
+ is(nodeList.length, 26, "Expect 26 div children.");
+ await assertOwnershipTrees(walker);
+ const firstNode = await nodeList.item(0);
+ checkActorIDs.push(firstNode.actorID);
+ is(firstNode.id, "a", "First child should be a");
+ await assertOwnershipTrees(walker);
+ let nodes = await nodeList.items();
+ is(nodes.length, 26, "Expect 26 nodes");
+ is(nodes[0], firstNode, "First node should be reused.");
+ ok(nodes[0]._parent, "Parent node should be set.");
+ ok(nodes[0]._next || nodes[0]._prev, "Siblings should be set.");
+ ok(
+ nodes[25]._next || nodes[25]._prev,
+ "Siblings of " + nodes[25] + " should be set."
+ );
+ await assertOwnershipTrees(walker);
+ nodes = await nodeList.items(-1);
+ is(nodes.length, 1, "Expect 1 node");
+ is(nodes[0].id, "z", "Expect it to be the last node.");
+ checkActorIDs.push(nodes[0].actorID);
+ // Save the node list ID so we can ensure it was destroyed.
+ const nodeListID = nodeList.actorID;
+ await assertOwnershipTrees(walker);
+ await nodeList.release();
+ ok(!nodeList.actorID, "Actor should have been destroyed.");
+ await assertOwnershipTrees(walker);
+ await checkMissing(target, nodeListID);
+});
+
+// Helper to check the response of requests that return hasFirst/hasLast/nodes
+// node lists (like `children` and `siblings`)
+async function checkArray(walker, children, first, last, ids) {
+ is(
+ children.hasFirst,
+ first,
+ "Should " + (first ? "" : "not ") + " have the first node."
+ );
+ is(
+ children.hasLast,
+ last,
+ "Should " + (last ? "" : "not ") + " have the last node."
+ );
+ is(
+ children.nodes.length,
+ ids.length,
+ "Should have " + ids.length + " children listed."
+ );
+ let responseIds = "";
+ for (const node of children.nodes) {
+ responseIds += node.id;
+ }
+ is(responseIds, ids, "Correct nodes were returned.");
+ await assertOwnershipTrees(walker);
+}
+
+add_task(async function testNoChildren() {
+ const { walker } = await initInspectorFront(
+ MAIN_DOMAIN + "inspector-traversal-data.html"
+ );
+ const empty = await walker.querySelector(walker.rootNode, "#empty");
+ await assertOwnershipTrees(walker);
+ const children = await walker.children(empty);
+ await checkArray(walker, children, true, true, "");
+});
+
+add_task(async function testLongListTraversal() {
+ const { walker } = await initInspectorFront(
+ MAIN_DOMAIN + "inspector-traversal-data.html"
+ );
+ const longList = await walker.querySelector(walker.rootNode, "#longlist");
+ // First call with no options, expect all children.
+ await assertOwnershipTrees(walker);
+ let children = await walker.children(longList);
+ await checkArray(walker, children, true, true, "abcdefghijklmnopqrstuvwxyz");
+ const allChildren = children.nodes;
+ await assertOwnershipTrees(walker);
+ // maxNodes should limit us to the first 5 nodes.
+ await assertOwnershipTrees(walker);
+ children = await walker.children(longList, { maxNodes: 5 });
+ await checkArray(walker, children, true, false, "abcde");
+ await assertOwnershipTrees(walker);
+ // maxNodes with the second item centered should still give us the first 5 nodes.
+ children = await walker.children(longList, {
+ maxNodes: 5,
+ center: allChildren[1],
+ });
+ await checkArray(walker, children, true, false, "abcde");
+ // maxNodes with a center in the middle of the list should put that item in the middle
+ const center = allChildren[13];
+ is(center.id, "n", "Make sure I know how to count letters.");
+ children = await walker.children(longList, { maxNodes: 5, center });
+ await checkArray(walker, children, false, false, "lmnop");
+ // maxNodes with the second-to-last item centered should give us the last 5 nodes.
+ children = await walker.children(longList, {
+ maxNodes: 5,
+ center: allChildren[24],
+ });
+ await checkArray(walker, children, false, true, "vwxyz");
+ // maxNodes with a start in the middle should start at that node and fetch 5
+ const start = allChildren[13];
+ is(start.id, "n", "Make sure I know how to count letters.");
+ children = await walker.children(longList, { maxNodes: 5, start });
+ await checkArray(walker, children, false, false, "nopqr");
+ // maxNodes near the end should only return what's left
+ children = await walker.children(longList, {
+ maxNodes: 5,
+ start: allChildren[24],
+ });
+ await checkArray(walker, children, false, true, "yz");
+});
+
+add_task(async function testObjectNodeChildren() {
+ const { walker } = await initInspectorFront(
+ MAIN_DOMAIN + "inspector-traversal-data.html"
+ );
+ const object = await walker.querySelector(walker.rootNode, "object");
+ const children = await walker.children(object);
+ await checkArray(walker, children, true, true, "1");
+});
+
+add_task(async function testNextSibling() {
+ const { walker } = await initInspectorFront(
+ MAIN_DOMAIN + "inspector-traversal-data.html"
+ );
+ const y = await walker.querySelector(walker.rootNode, "#y");
+ is(y.id, "y", "Got the right node.");
+ const z = await walker.nextSibling(y);
+ is(z.id, "z", "nextSibling got the next node.");
+ const nothing = await walker.nextSibling(z);
+ is(nothing, null, "nextSibling on the last node returned null.");
+});
+
+add_task(async function testPreviousSibling() {
+ const { walker } = await initInspectorFront(
+ MAIN_DOMAIN + "inspector-traversal-data.html"
+ );
+ const b = await walker.querySelector(walker.rootNode, "#b");
+ is(b.id, "b", "Got the right node.");
+ const a = await walker.previousSibling(b);
+ is(a.id, "a", "nextSibling got the next node.");
+ const nothing = await walker.previousSibling(a);
+ is(nothing, null, "previousSibling on the first node returned null.");
+});
+
+add_task(async function testFrameTraversal() {
+ const { walker } = await initInspectorFront(
+ MAIN_DOMAIN + "inspector-traversal-data.html"
+ );
+ const childFrame = await walker.querySelector(walker.rootNode, "#childFrame");
+ const children = await walker.children(childFrame);
+ const nodes = children.nodes;
+ is(nodes.length, 1, "There should be only one child of the iframe");
+ is(
+ nodes[0].nodeType,
+ Node.DOCUMENT_NODE,
+ "iframe child should be a document node"
+ );
+ await walker.querySelector(nodes[0], "#z");
+});
+
+add_task(async function testLongValue() {
+ const { walker } = await initInspectorFront(
+ MAIN_DOMAIN + "inspector-traversal-data.html"
+ );
+
+ SimpleTest.registerCleanupFunction(async function () {
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ const WalkerActor = require("resource://devtools/server/actors/inspector/walker.js");
+ WalkerActor.setValueSummaryLength(
+ WalkerActor.DEFAULT_VALUE_SUMMARY_LENGTH
+ );
+ });
+ });
+
+ const longstringText = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ function () {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ const testSummaryLength = 10;
+ const WalkerActor = require("resource://devtools/server/actors/inspector/walker.js");
+
+ WalkerActor.setValueSummaryLength(testSummaryLength);
+ return content.document.getElementById("longstring").firstChild.nodeValue;
+ }
+ );
+
+ const node = await walker.querySelector(walker.rootNode, "#longstring");
+ ok(!node.inlineTextChild, "Text is too long to be inlined");
+ // Now we need to get the text node child...
+ const children = await walker.children(node, { maxNodes: 1 });
+ const textNode = children.nodes[0];
+ is(textNode.nodeType, Node.TEXT_NODE, "Value should be a text node");
+ const value = await textNode.getNodeValue();
+ const valueStr = await value.string();
+ is(
+ valueStr,
+ longstringText,
+ "Full node value should match the string from the document."
+ );
+});
+
+add_task(async function testShortValue() {
+ const { walker } = await initInspectorFront(
+ MAIN_DOMAIN + "inspector-traversal-data.html"
+ );
+ const shortstringText = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ function () {
+ return content.document.getElementById("shortstring").firstChild
+ .nodeValue;
+ }
+ );
+
+ const node = await walker.querySelector(walker.rootNode, "#shortstring");
+ ok(!!node.inlineTextChild, "Text is short enough to be inlined");
+ // Now we need to get the text node child...
+ const children = await walker.children(node, { maxNodes: 1 });
+ const textNode = children.nodes[0];
+ is(textNode.nodeType, Node.TEXT_NODE, "Value should be a text node");
+ const value = await textNode.getNodeValue();
+ const valueStr = await value.string();
+ is(
+ valueStr,
+ shortstringText,
+ "Full node value should match the string from the document."
+ );
+});
+
+add_task(async function testReleaseWalker() {
+ const { target, walker } = await initInspectorFront(
+ MAIN_DOMAIN + "inspector-traversal-data.html"
+ );
+ checkActorIDs.push(walker.actorID);
+
+ await walker.release();
+ for (const id of checkActorIDs) {
+ await checkMissing(target, id);
+ }
+});
diff --git a/devtools/server/tests/browser/browser_inspector-utils.js b/devtools/server/tests/browser/browser_inspector-utils.js
new file mode 100644
index 0000000000..b81eeb0178
--- /dev/null
+++ b/devtools/server/tests/browser/browser_inspector-utils.js
@@ -0,0 +1,25 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/server/tests/browser/inspector-helpers.js",
+ this
+);
+
+const COLOR_WHITE = [255, 255, 255, 1];
+
+add_task(async function loadNewChild() {
+ const { walker } = await initInspectorFront(
+ `data:text/html,<style>body{color:red;background-color:white;}body::before{content:"test";}</style>`
+ );
+
+ const body = await walker.querySelector(walker.rootNode, "body");
+ const color = await body.getBackgroundColor();
+ Assert.deepEqual(
+ color.value,
+ COLOR_WHITE,
+ "Background color is calculated correctly for an element with a pseudo child."
+ );
+});
diff --git a/devtools/server/tests/browser/browser_layout_getGrids.js b/devtools/server/tests/browser/browser_layout_getGrids.js
new file mode 100644
index 0000000000..ce40cf7a22
--- /dev/null
+++ b/devtools/server/tests/browser/browser_layout_getGrids.js
@@ -0,0 +1,145 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check the output of getGrids for the LayoutActor
+
+const GRID_FRAGMENT_DATA = {
+ areas: [
+ {
+ columnEnd: 3,
+ columnStart: 2,
+ name: "header",
+ rowEnd: 2,
+ rowStart: 1,
+ type: "explicit",
+ },
+ {
+ columnEnd: 2,
+ columnStart: 1,
+ name: "sidebar",
+ rowEnd: 3,
+ rowStart: 2,
+ type: "explicit",
+ },
+ {
+ columnEnd: 3,
+ columnStart: 2,
+ name: "content",
+ rowEnd: 3,
+ rowStart: 2,
+ type: "explicit",
+ },
+ ],
+ cols: {
+ lines: [
+ {
+ breadth: 0,
+ names: ["col-1", "col-start-1", "sidebar-start"],
+ number: 1,
+ start: 0,
+ type: "explicit",
+ },
+ {
+ breadth: 0,
+ names: ["col-2", "header-start", "sidebar-end", "content-start"],
+ number: 2,
+ start: 100,
+ type: "explicit",
+ },
+ {
+ breadth: 0,
+ names: ["header-end", "content-end"],
+ number: 3,
+ start: 200,
+ type: "explicit",
+ },
+ ],
+ tracks: [
+ {
+ breadth: 100,
+ start: 0,
+ state: "static",
+ type: "explicit",
+ },
+ {
+ breadth: 100,
+ start: 100,
+ state: "static",
+ type: "explicit",
+ },
+ ],
+ },
+ rows: {
+ lines: [
+ {
+ breadth: 0,
+ names: ["header-start"],
+ number: 1,
+ start: 0,
+ type: "explicit",
+ },
+ {
+ breadth: 0,
+ names: ["header-end", "sidebar-start", "content-start"],
+ number: 2,
+ start: 100,
+ type: "explicit",
+ },
+ {
+ breadth: 0,
+ names: ["sidebar-end", "content-end"],
+ number: 3,
+ start: 200,
+ type: "explicit",
+ },
+ ],
+ tracks: [
+ {
+ breadth: 100,
+ start: 0,
+ state: "static",
+ type: "explicit",
+ },
+ {
+ breadth: 100,
+ start: 100,
+ state: "static",
+ type: "explicit",
+ },
+ ],
+ },
+};
+
+add_task(async function () {
+ const { target, walker, layout } = await initLayoutFrontForUrl(
+ MAIN_DOMAIN + "grid.html"
+ );
+ const grids = await layout.getGrids(walker.rootNode);
+ const grid = grids[0];
+ const { gridFragments } = grid;
+
+ is(grids.length, 1, "One grid was returned.");
+ is(gridFragments.length, 1, "One grid fragment was returned.");
+ ok(Array.isArray(gridFragments), "An array of grid fragments was returned.");
+ Assert.deepEqual(
+ gridFragments[0],
+ GRID_FRAGMENT_DATA,
+ "Got the correct grid fragment data."
+ );
+
+ info("Get the grid container node front.");
+
+ try {
+ const nodeFront = await walker.getNodeFromActor(grids[0].actorID, [
+ "containerEl",
+ ]);
+ ok(nodeFront, "Got the grid container node front.");
+ } catch (e) {
+ ok(false, "Did not get grid container node front.");
+ }
+
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/server/tests/browser/browser_layout_simple.js b/devtools/server/tests/browser/browser_layout_simple.js
new file mode 100644
index 0000000000..d4caba572e
--- /dev/null
+++ b/devtools/server/tests/browser/browser_layout_simple.js
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Simple checks for the LayoutActor and GridActor
+
+add_task(async function () {
+ const { target, walker, layout } = await initLayoutFrontForUrl(
+ "data:text/html;charset=utf-8,<title>test</title><div></div>"
+ );
+
+ ok(layout, "The LayoutFront was created");
+ ok(layout.getGrids, "The getGrids method exists");
+
+ let didThrow = false;
+ try {
+ await layout.getGrids(null);
+ } catch (e) {
+ didThrow = true;
+ }
+ ok(didThrow, "An exception was thrown for a missing NodeActor in getGrids");
+
+ const invalidNode = await walker.querySelector(walker.rootNode, "title");
+ const grids = await layout.getGrids(invalidNode);
+ ok(Array.isArray(grids), "An array of grids was returned");
+ is(grids.length, 0, "0 grids have been returned for the invalid node");
+
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/server/tests/browser/browser_memory_allocations_01.js b/devtools/server/tests/browser/browser_memory_allocations_01.js
new file mode 100644
index 0000000000..cc6d5b0f58
--- /dev/null
+++ b/devtools/server/tests/browser/browser_memory_allocations_01.js
@@ -0,0 +1,107 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function () {
+ const target = await addTabTarget("data:text/html;charset=utf-8,test-doc");
+ const memory = await target.getFront("memory");
+
+ await memory.attach();
+
+ await memory.startRecordingAllocations();
+ ok(true, "Can start recording allocations");
+
+ // Allocate some objects.
+ const [line1, line2, line3] = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ function () {
+ // Use eval to ensure allocating the object in the page's compartment
+ return content.eval(
+ "(" +
+ function () {
+ let alloc1, alloc2, alloc3;
+
+ /* eslint-disable max-nested-callbacks */
+ (function outer() {
+ (function middle() {
+ (function inner() {
+ alloc1 = {};
+ alloc1.line = Error().lineNumber;
+ alloc2 = [];
+ alloc2.line = Error().lineNumber;
+ // eslint-disable-next-line new-parens
+ alloc3 = new (function () {})();
+ alloc3.line = Error().lineNumber;
+ })();
+ })();
+ })();
+ /* eslint-enable max-nested-callbacks */
+
+ return [alloc1.line, alloc2.line, alloc3.line];
+ } +
+ ")()"
+ );
+ }
+ );
+
+ const response = await memory.getAllocations();
+
+ await memory.stopRecordingAllocations();
+ ok(true, "Can stop recording allocations");
+
+ // Filter out allocations by library and test code, and get only the
+ // allocations that occurred in our test case above.
+
+ function isTestAllocation(alloc) {
+ const frame = response.frames[alloc];
+ return (
+ frame &&
+ frame.functionDisplayName === "inner" &&
+ (frame.line === line1 || frame.line === line2 || frame.line === line3)
+ );
+ }
+
+ const testAllocations = response.allocations.filter(isTestAllocation);
+ Assert.greaterOrEqual(
+ testAllocations.length,
+ 3,
+ "Should find our 3 test allocations (plus some allocations for the error " +
+ "objects used to get line numbers)"
+ );
+
+ // For each of the test case's allocations, ensure that the parent frame
+ // indices are correct. Also test that we did get an allocation at each
+ // line we expected (rather than a bunch on the first line and none on the
+ // others, etc).
+
+ const expectedLines = new Set([line1, line2, line3]);
+ is(expectedLines.size, 3, "We are expecting 3 allocations");
+
+ for (const alloc of testAllocations) {
+ const innerFrame = response.frames[alloc];
+ ok(innerFrame, "Should get the inner frame");
+ is(innerFrame.functionDisplayName, "inner");
+ expectedLines.delete(innerFrame.line);
+
+ const middleFrame = response.frames[innerFrame.parent];
+ ok(middleFrame, "Should get the middle frame");
+ is(middleFrame.functionDisplayName, "middle");
+
+ const outerFrame = response.frames[middleFrame.parent];
+ ok(outerFrame, "Should get the outer frame");
+ is(outerFrame.functionDisplayName, "outer");
+
+ // Not going to test the rest of the frames because they are Task.jsm
+ // and promise frames and it gets gross. Plus, I wouldn't want this test
+ // to start failing if they changed their implementations in a way that
+ // added or removed stack frames here.
+ }
+
+ is(expectedLines.size, 0, "Should have found all the expected lines");
+
+ await memory.detach();
+
+ await target.destroy();
+});
diff --git a/devtools/server/tests/browser/browser_perf-01.js b/devtools/server/tests/browser/browser_perf-01.js
new file mode 100644
index 0000000000..96afc8151e
--- /dev/null
+++ b/devtools/server/tests/browser/browser_perf-01.js
@@ -0,0 +1,57 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+// This test is at the edge of timing out, probably because of LUL
+// initialization on Linux. This is also happening only once, which is why only
+// this test needs it: for other tests LUL is already initialized because
+// they're running in the same Firefox instance.
+// See also bug 1635442.
+requestLongerTimeout(2);
+
+/**
+ * Run through a series of basic recording actions for the perf actor.
+ */
+add_task(async function () {
+ const { front, client } = await initPerfFront();
+
+ // Assert the initial state.
+ is(
+ await front.isSupportedPlatform(),
+ true,
+ "This test only runs on supported platforms."
+ );
+ is(await front.isActive(), false, "The profiler is not active yet.");
+
+ // Start the profiler.
+ const profilerStarted = once(front, "profiler-started");
+ await front.startProfiler();
+ await profilerStarted;
+ is(await front.isActive(), true, "The profiler was started.");
+
+ // Stop the profiler and assert the results.
+ const profilerStopped1 = once(front, "profiler-stopped");
+ const profile = await front.getProfileAndStopProfiler();
+ await profilerStopped1;
+ is(await front.isActive(), false, "The profiler was stopped.");
+ ok("threads" in profile, "The actor was used to record a profile.");
+
+ // Restart the profiler.
+ await front.startProfiler();
+ is(await front.isActive(), true, "The profiler was re-started.");
+
+ // Stop and discard.
+ const profilerStopped2 = once(front, "profiler-stopped");
+ await front.stopProfilerAndDiscardProfile();
+ await profilerStopped2;
+ is(
+ await front.isActive(),
+ false,
+ "The profiler was stopped and the profile discarded."
+ );
+
+ // Clean up.
+ await front.destroy();
+ await client.close();
+});
diff --git a/devtools/server/tests/browser/browser_perf-02.js b/devtools/server/tests/browser/browser_perf-02.js
new file mode 100644
index 0000000000..c7276d8a3f
--- /dev/null
+++ b/devtools/server/tests/browser/browser_perf-02.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";
+
+/**
+ * Test what happens when other tools control the profiler.
+ */
+add_task(async function () {
+ const { front, client } = await initPerfFront();
+
+ // Simulate other tools by getting an independent handle on the Gecko Profiler.
+ // eslint-disable-next-line mozilla/use-services
+ const geckoProfiler = Cc["@mozilla.org/tools/profiler;1"].getService(
+ Ci.nsIProfiler
+ );
+
+ is(await front.isActive(), false, "The profiler hasn't been started yet.");
+
+ // Start the profiler.
+ await front.startProfiler();
+ is(await front.isActive(), true, "The profiler was started.");
+
+ // Stop the profiler manually through the Gecko Profiler interface.
+ const profilerStopped = once(front, "profiler-stopped");
+ geckoProfiler.StopProfiler();
+ await profilerStopped;
+ is(
+ await front.isActive(),
+ false,
+ "The profiler was stopped by another tool."
+ );
+
+ // Clean up.
+ await front.destroy();
+ await client.close();
+});
diff --git a/devtools/server/tests/browser/browser_perf-04.js b/devtools/server/tests/browser/browser_perf-04.js
new file mode 100644
index 0000000000..9fba77d053
--- /dev/null
+++ b/devtools/server/tests/browser/browser_perf-04.js
@@ -0,0 +1,53 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+/**
+ * Run through a series of basic recording actions for the perf actor.
+ */
+add_task(async function () {
+ const { front, client } = await initPerfFront();
+
+ // Assert the initial state.
+ is(
+ await front.isSupportedPlatform(),
+ true,
+ "This test only runs on supported platforms."
+ );
+ is(await front.isActive(), false, "The profiler is not active yet.");
+
+ // Getting the active Browser ID to assert in the "profiler-started" event.
+ const win = Services.wm.getMostRecentWindow("navigator:browser");
+ const activeTabID = win.gBrowser.selectedBrowser.browsingContext.browserId;
+
+ front.once(
+ "profiler-started",
+ (entries, interval, features, duration, activeTID) => {
+ is(entries, 1024, "Should apply entries by startProfiler");
+ is(interval, 0.1, "Should apply interval by startProfiler");
+ is(typeof features, "number", "Should apply features by startProfiler");
+ is(duration, 2, "Should apply duration by startProfiler");
+ is(
+ activeTID,
+ activeTabID,
+ "Should apply active browser ID by startProfiler"
+ );
+ }
+ );
+
+ // Start the profiler.
+ await front.startProfiler({
+ entries: 1000,
+ duration: 2,
+ interval: 0.1,
+ features: ["js", "stackwalk"],
+ });
+
+ is(await front.isActive(), true, "The profiler is active.");
+
+ // clean up
+ await front.stopProfilerAndDiscardProfile();
+ await front.destroy();
+ await client.close();
+});
diff --git a/devtools/server/tests/browser/browser_perf-getSupportedFeatures.js b/devtools/server/tests/browser/browser_perf-getSupportedFeatures.js
new file mode 100644
index 0000000000..331d6d329c
--- /dev/null
+++ b/devtools/server/tests/browser/browser_perf-getSupportedFeatures.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";
+
+add_task(async function () {
+ const { front, client } = await initPerfFront();
+
+ info("Get the supported features from the perf actor.");
+ const features = await front.getSupportedFeatures();
+
+ ok(Array.isArray(features), "The features are an array.");
+ ok(!!features.length, "There are many features supported.");
+ ok(
+ features.includes("js"),
+ "All platforms support the js feature, and it's in this list."
+ );
+
+ // clean up
+ await front.stopProfilerAndDiscardProfile();
+ await front.destroy();
+ await client.close();
+});
diff --git a/devtools/server/tests/browser/browser_storage_cookies-duplicate-names.js b/devtools/server/tests/browser/browser_storage_cookies-duplicate-names.js
new file mode 100644
index 0000000000..0342e1b896
--- /dev/null
+++ b/devtools/server/tests/browser/browser_storage_cookies-duplicate-names.js
@@ -0,0 +1,134 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the storage panel is able to display multiple cookies with the same
+// name (and different paths).
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/server/tests/browser/storage-helpers.js",
+ this
+);
+
+const l10n = new Localization(["devtools/client/storage.ftl"], true);
+const sessionString = l10n.formatValueSync("storage-expires-session");
+
+const TESTDATA = {
+ "http://test1.example.org": [
+ {
+ name: "name",
+ value: "value1",
+ expires: 0,
+ path: "/",
+ host: "test1.example.org",
+ hostOnly: true,
+ isSecure: false,
+ },
+ {
+ name: "name",
+ value: "value2",
+ expires: 0,
+ path: "/path2/",
+ host: "test1.example.org",
+ hostOnly: true,
+ isSecure: false,
+ },
+ {
+ name: "name",
+ value: "value3",
+ expires: 0,
+ path: "/path3/",
+ host: "test1.example.org",
+ hostOnly: true,
+ isSecure: false,
+ },
+ ],
+};
+
+add_task(async function () {
+ const { commands } = await openTabAndSetupStorage(
+ MAIN_DOMAIN + "storage-cookies-same-name.html"
+ );
+
+ const { resourceCommand } = commands;
+ const { TYPES } = resourceCommand;
+ const data = {};
+ await resourceCommand.watchResources(
+ [TYPES.COOKIE, TYPES.LOCAL_STORAGE, TYPES.SESSION_STORAGE],
+ {
+ async onAvailable(resources) {
+ for (const resource of resources) {
+ const { resourceType } = resource;
+ if (!data[resourceType]) {
+ data[resourceType] = { hosts: {}, dataByHost: {} };
+ }
+
+ for (const host in resource.hosts) {
+ if (!data[resourceType].hosts[host]) {
+ data[resourceType].hosts[host] = [];
+ }
+ // For indexed DB, we have some values, the database names. Other are empty arrays.
+ const hostValues = resource.hosts[host];
+ data[resourceType].hosts[host].push(...hostValues);
+ data[resourceType].dataByHost[host] =
+ await resource.getStoreObjects(host, null, { sessionString });
+ }
+ }
+ },
+ }
+ );
+
+ ok(data.cookies, "Cookies storage actor is present");
+
+ await testCookies(data.cookies);
+ await clearStorage();
+
+ // Forcing GC/CC to get rid of docshells and windows created by this test.
+ forceCollections();
+ await commands.destroy();
+ forceCollections();
+});
+
+function testCookies({ hosts, dataByHost }) {
+ const numHosts = Object.keys(hosts).length;
+ is(numHosts, 1, "Correct number of host entries for cookies");
+ return testCookiesObjects(0, hosts, dataByHost);
+}
+
+var testCookiesObjects = async function (index, hosts, dataByHost) {
+ const host = Object.keys(hosts)[index];
+ const data = dataByHost[host];
+ is(
+ data.total,
+ TESTDATA[host].length,
+ "Number of cookies in host " + host + " matches"
+ );
+ for (const item of data.data) {
+ let found = false;
+ for (const toMatch of TESTDATA[host]) {
+ if (
+ item.name === toMatch.name &&
+ item.host === toMatch.host &&
+ item.path === toMatch.path
+ ) {
+ found = true;
+ ok(true, "Found cookie " + item.name + " in response");
+ is(item.value.str, toMatch.value, "The value matches.");
+ is(item.expires, toMatch.expires, "The expiry time matches.");
+ is(item.path, toMatch.path, "The path matches.");
+ is(item.host, toMatch.host, "The host matches.");
+ is(item.isSecure, toMatch.isSecure, "The isSecure value matches.");
+ is(item.hostOnly, toMatch.hostOnly, "The hostOnly value matches.");
+ break;
+ }
+ }
+ ok(found, "cookie " + item.name + " should exist in response");
+ }
+
+ ok(!!TESTDATA[host], "Host is present in the list : " + host);
+ if (index == Object.keys(hosts).length - 1) {
+ return;
+ }
+ await testCookiesObjects(++index, hosts, dataByHost);
+};
diff --git a/devtools/server/tests/browser/browser_storage_dynamic_windows.js b/devtools/server/tests/browser/browser_storage_dynamic_windows.js
new file mode 100644
index 0000000000..0417cf0f09
--- /dev/null
+++ b/devtools/server/tests/browser/browser_storage_dynamic_windows.js
@@ -0,0 +1,410 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/server/tests/browser/storage-helpers.js",
+ this
+);
+
+// beforeReload references an object representing the initialized state of the
+// storage actor.
+const beforeReload = {
+ cookies: {
+ "http://test1.example.org": ["c1", "cs2", "c3", "uc1"],
+ "http://sectest1.example.org": ["uc1", "cs2"],
+ },
+ "indexed-db": {
+ "http://test1.example.org": [
+ JSON.stringify(["idb1", "obj1"]),
+ JSON.stringify(["idb1", "obj2"]),
+ JSON.stringify(["idb2", "obj3"]),
+ ],
+ "http://sectest1.example.org": [],
+ },
+ "local-storage": {
+ "http://test1.example.org": ["ls1", "ls2"],
+ "http://sectest1.example.org": ["iframe-u-ls1"],
+ },
+ "session-storage": {
+ "http://test1.example.org": ["ss1"],
+ "http://sectest1.example.org": ["iframe-u-ss1", "iframe-u-ss2"],
+ },
+};
+
+// afterIframeAdded references the items added when an iframe containing storage
+// items is added to the page.
+const afterIframeAdded = {
+ cookies: {
+ "https://sectest1.example.org": [
+ getCookieId("cs2", ".example.org", "/"),
+ getCookieId(
+ "sc1",
+ "sectest1.example.org",
+ "/browser/devtools/server/tests/browser"
+ ),
+ ],
+ "http://sectest1.example.org": [
+ getCookieId(
+ "sc1",
+ "sectest1.example.org",
+ "/browser/devtools/server/tests/browser"
+ ),
+ ],
+ },
+ "indexed-db": {
+ // empty because indexed db creation happens after the page load, so at
+ // the time of window-ready, there was no indexed db present.
+ "https://sectest1.example.org": [],
+ },
+ "local-storage": {
+ "https://sectest1.example.org": ["iframe-s-ls1"],
+ },
+ "session-storage": {
+ "https://sectest1.example.org": ["iframe-s-ss1"],
+ },
+};
+
+// afterIframeRemoved references the items deleted when an iframe containing
+// storage items is removed from the page.
+const afterIframeRemoved = {
+ cookies: {
+ "http://sectest1.example.org": [],
+ },
+ "indexed-db": {
+ "http://sectest1.example.org": [],
+ },
+ "local-storage": {
+ "http://sectest1.example.org": [],
+ },
+ "session-storage": {
+ "http://sectest1.example.org": [],
+ },
+};
+
+add_task(async function () {
+ const { commands } = await openTabAndSetupStorage(
+ MAIN_DOMAIN + "storage-dynamic-windows.html"
+ );
+
+ const { resourceCommand } = commands;
+ const { TYPES } = resourceCommand;
+ const allResources = {};
+ const onAvailable = resources => {
+ for (const resource of resources) {
+ is(
+ resource.targetFront.targetType,
+ commands.targetCommand.TYPES.FRAME,
+ "Each storage resource has a valid 'targetFront' attribute"
+ );
+ // Because we have iframes, we have distinct targets, each spawning their own storage resource
+ if (allResources[resource.resourceType]) {
+ allResources[resource.resourceType].push(resource);
+ } else {
+ allResources[resource.resourceType] = [resource];
+ }
+ }
+ };
+ const parentProcessStorages = [TYPES.COOKIE, TYPES.INDEXED_DB];
+ const contentProcessStorages = [TYPES.LOCAL_STORAGE, TYPES.SESSION_STORAGE];
+ const allStorages = [...parentProcessStorages, ...contentProcessStorages];
+ await resourceCommand.watchResources(allStorages, { onAvailable });
+ is(
+ Object.keys(allStorages).length,
+ allStorages.length,
+ "Got all the storage resources"
+ );
+
+ // Do a copy of all the initial storages as test function may spawn new resources for the same
+ // type and override the initial ones.
+ // We do not call unwatchResources as it would clear its cache and next call
+ // to watchResources with ignoreExistingResources would break and reprocess all resources again.
+ const initialResources = Object.assign({}, allResources);
+
+ testWindowsBeforeReload(initialResources);
+
+ await testAddIframe(commands, initialResources, {
+ contentProcessStorages,
+ parentProcessStorages,
+ allStorages,
+ });
+
+ await testRemoveIframe(commands, initialResources, {
+ contentProcessStorages,
+ parentProcessStorages,
+ allStorages,
+ });
+
+ await clearStorage();
+
+ // Forcing GC/CC to get rid of docshells and windows created by this test.
+ forceCollections();
+ await commands.destroy();
+ forceCollections();
+});
+
+function testWindowsBeforeReload(resources) {
+ for (const storageType in beforeReload) {
+ ok(resources[storageType], `${storageType} storage actor is present`);
+
+ const hosts = {};
+ for (const resource of resources[storageType]) {
+ for (const [hostType, hostValues] of Object.entries(resource.hosts)) {
+ if (!hosts[hostType]) {
+ hosts[hostType] = [];
+ }
+
+ hosts[hostType].push(hostValues);
+ }
+ }
+
+ // If this test is run with chrome debugging enabled we get an extra
+ // key for "chrome". We don't want the test to fail in this case, so
+ // ignore it.
+ if (storageType == "indexedDB") {
+ delete hosts.chrome;
+ }
+
+ is(
+ Object.keys(hosts).length,
+ Object.keys(beforeReload[storageType]).length,
+ `Number of hosts for ${storageType} match`
+ );
+ for (const host in beforeReload[storageType]) {
+ ok(hosts[host], `Host ${host} is present`);
+ }
+ }
+}
+
+/**
+ * Wait for new storage resources to be created of the given types.
+ */
+async function waitForNewResourcesAndUpdates(commands, resourceTypes) {
+ // When fission is off, we don't expect any new resource
+ if (resourceTypes.length === 0) {
+ return { newResources: [], updates: [] };
+ }
+ const { resourceCommand } = commands;
+ let resolve;
+ const promise = new Promise(r => (resolve = r));
+ const allResources = {};
+ const allUpdates = {};
+ const onAvailable = resources => {
+ for (const resource of resources) {
+ if (resource.resourceType in allResources) {
+ ok(false, `Got multiple ${resource.resourceTypes} resources`);
+ }
+ allResources[resource.resourceType] = resource;
+ ok(true, `Got resource for ${resource.resourceType}`);
+
+ // Stop watching for resources when we got them all
+ if (Object.keys(allResources).length == resourceTypes.length) {
+ resourceCommand.unwatchResources(resourceTypes, {
+ onAvailable,
+ });
+ }
+
+ // But also listen for updates on each new resource
+ resource.once("single-store-update").then(update => {
+ ok(true, `Got updates for ${resource.resourceType}`);
+ allUpdates[resource.resourceType] = update;
+
+ // Resolve only once we got all the updates, for all the resources
+ if (Object.keys(allUpdates).length == resourceTypes.length) {
+ resolve({ newResources: allResources, updates: allUpdates });
+ }
+ });
+ }
+ };
+ await resourceCommand.watchResources(resourceTypes, {
+ onAvailable,
+ ignoreExistingResources: true,
+ });
+ return promise;
+}
+
+/**
+ * Wait for single-store-update events on all the given storage resources.
+ */
+function waitForResourceUpdates(resources, resourceTypes) {
+ const allUpdates = {};
+ const promises = [];
+ for (const type of resourceTypes) {
+ // Resolves once any of the many resources for the given storage type updates
+ const promise = Promise.any(
+ resources[type].map(resource => resource.once("single-store-update"))
+ );
+ promise.then(update => {
+ ok(true, `Got updates for ${type}`);
+ allUpdates[type] = update;
+ });
+ promises.push(promise);
+ }
+ return Promise.all(promises).then(() => allUpdates);
+}
+
+async function testAddIframe(
+ commands,
+ resources,
+ { contentProcessStorages, parentProcessStorages, allStorages }
+) {
+ info("Testing if new iframe addition works properly");
+
+ // If Fission or EFT is enabled:
+ // * we get new resources alongside single-store-update events for content process storages
+ // * only single-store-update events for previous resources for parent process storages
+ // Otherwise if fission is disables:
+ // * we get single-store-update events for all previous resources
+ const onResources = waitForNewResourcesAndUpdates(
+ commands,
+ isFissionEnabled() || isEveryFrameTargetEnabled()
+ ? contentProcessStorages
+ : []
+ );
+ // If fission or EFT is enabled, we only get update for parent process storages.
+ // The content process storage resources are notified via brand new resource instances.
+ const storagesWithUpdates =
+ isFissionEnabled() || isEveryFrameTargetEnabled()
+ ? parentProcessStorages
+ : allStorages;
+ const onUpdates = waitForResourceUpdates(resources, storagesWithUpdates);
+
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [ALT_DOMAIN_SECURED],
+ secured => {
+ const doc = content.document;
+
+ const iframe = doc.createElement("iframe");
+ iframe.src = secured + "storage-secured-iframe.html";
+
+ doc.querySelector("body").appendChild(iframe);
+ }
+ );
+
+ info("Wait for all resources");
+ const { newResources, updates } = await onResources;
+ info("Wait for all updates");
+ const previousResourceUpdates = await onUpdates;
+
+ if (isFissionEnabled() || isEveryFrameTargetEnabled()) {
+ for (const resourceType of contentProcessStorages) {
+ const resource = newResources[resourceType];
+ const expected = afterIframeAdded[resourceType];
+ // The resource only comes with hosts, without any values.
+ // Each host will be an empty array.
+ Assert.deepEqual(
+ Object.keys(resource.hosts),
+ Object.keys(expected),
+ `List of hosts for resource ${resourceType} is correct`
+ );
+ for (const host in resource.hosts) {
+ is(
+ resource.hosts[host].length,
+ 0,
+ "For new resources, each host has no value and is an empty array"
+ );
+ }
+ const update = updates[resourceType];
+ const storageKey = resourceTypeToStorageKey(resourceType);
+ Assert.deepEqual(
+ update.added[storageKey],
+ expected,
+ "We get an update after the resource, with the host values"
+ );
+ }
+ }
+
+ for (const resourceType of storagesWithUpdates) {
+ const expected = afterIframeAdded[resourceType];
+ const update = previousResourceUpdates[resourceType];
+ const storageKey = resourceTypeToStorageKey(resourceType);
+ Assert.deepEqual(
+ update.added[storageKey],
+ expected,
+ `We get an update after the resource ${resourceType}, with the host values`
+ );
+ }
+
+ return newResources;
+}
+
+async function testRemoveIframe(
+ commands,
+ resources,
+ { contentProcessStorages, parentProcessStorages, allStorages }
+) {
+ info("Testing if iframe removal works properly");
+
+ // If fission or EFT is enabled, we only get update for parent process storages.
+ // The content process storage resources are wiped via their related target destruction.
+ const onUpdates = waitForResourceUpdates(
+ resources,
+ isFissionEnabled() || isEveryFrameTargetEnabled()
+ ? parentProcessStorages
+ : allStorages
+ );
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ for (const iframe of content.document.querySelectorAll("iframe")) {
+ if (iframe.src.startsWith("http:")) {
+ iframe.remove();
+ break;
+ }
+ }
+ });
+
+ info("Wait for all updates");
+ const previousResourceUpdates = await onUpdates;
+
+ const storagesWithUpdates =
+ isFissionEnabled() || isEveryFrameTargetEnabled()
+ ? parentProcessStorages
+ : allStorages;
+ for (const resourceType of storagesWithUpdates) {
+ const expected = afterIframeRemoved[resourceType];
+ const update = previousResourceUpdates[resourceType];
+ const storageKey = resourceTypeToStorageKey(resourceType);
+ Assert.deepEqual(
+ update.deleted[storageKey],
+ expected,
+ `We get an update after the resource ${resourceType}, with the host values`
+ );
+ }
+
+ // With Fission or EFT, the iframe target is destroyed,
+ // which ends up destroying the related resources
+ if (isFissionEnabled() || isEveryFrameTargetEnabled()) {
+ const destroyedResourceTypes = [];
+ for (const storageType in resources) {
+ for (const resource of resources[storageType]) {
+ if (resource.isDestroyed()) {
+ destroyedResourceTypes.push(resource.resourceType);
+ }
+ }
+ }
+ Assert.deepEqual(
+ destroyedResourceTypes.sort(),
+ contentProcessStorages.sort(),
+ "Content process storage resources have been destroyed [local and session storages]"
+ );
+ }
+}
+
+/**
+ * single-store-update emits objects using attributes with old "storage key" namings,
+ * which is different from resource type namings.
+ */
+function resourceTypeToStorageKey(resourceType) {
+ if (resourceType == "local-storage") {
+ return "localStorage";
+ }
+ if (resourceType == "session-storage") {
+ return "sessionStorage";
+ }
+ if (resourceType == "indexed-db") {
+ return "indexedDB";
+ }
+ return resourceType;
+}
diff --git a/devtools/server/tests/browser/browser_storage_listings.js b/devtools/server/tests/browser/browser_storage_listings.js
new file mode 100644
index 0000000000..40365ede85
--- /dev/null
+++ b/devtools/server/tests/browser/browser_storage_listings.js
@@ -0,0 +1,743 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/server/tests/browser/storage-helpers.js",
+ this
+);
+
+const l10n = new Localization(["devtools/client/storage.ftl"], true);
+const sessionString = l10n.formatValueSync("storage-expires-session");
+
+const storeMap = {
+ cookies: {
+ "http://test1.example.org": [
+ {
+ name: "c1",
+ value: "foobar",
+ expires: 2000000000000,
+ path: "/browser",
+ host: "test1.example.org",
+ hostOnly: true,
+ isSecure: false,
+ },
+ {
+ name: "cs2",
+ value: "sessionCookie",
+ path: "/",
+ host: ".example.org",
+ expires: 0,
+ hostOnly: false,
+ isSecure: false,
+ },
+ {
+ name: "c3",
+ value: "foobar-2",
+ expires: 2000000001000,
+ path: "/",
+ host: "test1.example.org",
+ hostOnly: true,
+ isSecure: true,
+ },
+ ],
+
+ "http://sectest1.example.org": [
+ {
+ name: "cs2",
+ value: "sessionCookie",
+ path: "/",
+ host: ".example.org",
+ expires: 0,
+ hostOnly: false,
+ isSecure: false,
+ },
+ {
+ name: "sc1",
+ value: "foobar",
+ path: "/browser/devtools/server/tests/browser",
+ host: "sectest1.example.org",
+ expires: 0,
+ hostOnly: true,
+ isSecure: false,
+ },
+ ],
+
+ "https://sectest1.example.org": [
+ {
+ name: "uc1",
+ value: "foobar",
+ host: ".example.org",
+ path: "/",
+ expires: 0,
+ hostOnly: false,
+ isSecure: true,
+ },
+ {
+ name: "cs2",
+ value: "sessionCookie",
+ path: "/",
+ host: ".example.org",
+ expires: 0,
+ hostOnly: false,
+ isSecure: false,
+ },
+ {
+ name: "sc1",
+ value: "foobar",
+ path: "/browser/devtools/server/tests/browser",
+ host: "sectest1.example.org",
+ expires: 0,
+ hostOnly: true,
+ isSecure: false,
+ },
+ ],
+ },
+ "local-storage": {
+ "http://test1.example.org": [
+ {
+ name: "ls1",
+ value: "foobar",
+ },
+ {
+ name: "ls2",
+ value: "foobar-2",
+ },
+ ],
+ "http://sectest1.example.org": [
+ {
+ name: "iframe-u-ls1",
+ value: "foobar",
+ },
+ ],
+ "https://sectest1.example.org": [
+ {
+ name: "iframe-s-ls1",
+ value: "foobar",
+ },
+ ],
+ },
+ "session-storage": {
+ "http://test1.example.org": [
+ {
+ name: "ss1",
+ value: "foobar-3",
+ },
+ ],
+ "http://sectest1.example.org": [
+ {
+ name: "iframe-u-ss1",
+ value: "foobar1",
+ },
+ {
+ name: "iframe-u-ss2",
+ value: "foobar2",
+ },
+ ],
+ "https://sectest1.example.org": [
+ {
+ name: "iframe-s-ss1",
+ value: "foobar-2",
+ },
+ ],
+ },
+};
+
+const IDBValues = {
+ listStoresResponse: {
+ "http://test1.example.org": [
+ ["idb1 (default)", "obj1"],
+ ["idb1 (default)", "obj2"],
+ ["idb2 (default)", "obj3"],
+ ],
+ "http://sectest1.example.org": [],
+ "https://sectest1.example.org": [
+ ["idb-s1 (default)", "obj-s1"],
+ ["idb-s2 (default)", "obj-s2"],
+ ],
+ },
+ dbDetails: {
+ "http://test1.example.org": [
+ {
+ db: "idb1 (default)",
+ origin: "http://test1.example.org",
+ version: 1,
+ objectStores: 2,
+ },
+ {
+ db: "idb2 (default)",
+ origin: "http://test1.example.org",
+ version: 1,
+ objectStores: 1,
+ },
+ ],
+ "http://sectest1.example.org": [],
+ "https://sectest1.example.org": [
+ {
+ db: "idb-s1 (default)",
+ origin: "https://sectest1.example.org",
+ version: 1,
+ objectStores: 1,
+ },
+ {
+ db: "idb-s2 (default)",
+ origin: "https://sectest1.example.org",
+ version: 1,
+ objectStores: 1,
+ },
+ ],
+ },
+ objectStoreDetails: {
+ "http://test1.example.org": {
+ "idb1 (default)": [
+ {
+ objectStore: "obj1",
+ keyPath: "id",
+ autoIncrement: false,
+ indexes: [
+ {
+ name: "name",
+ keyPath: "name",
+ unique: false,
+ multiEntry: false,
+ },
+ {
+ name: "email",
+ keyPath: "email",
+ unique: true,
+ multiEntry: false,
+ },
+ ],
+ },
+ {
+ objectStore: "obj2",
+ keyPath: "id2",
+ autoIncrement: false,
+ indexes: [],
+ },
+ ],
+ "idb2 (default)": [
+ {
+ objectStore: "obj3",
+ keyPath: "id3",
+ autoIncrement: false,
+ indexes: [
+ {
+ name: "name2",
+ keyPath: "name2",
+ unique: true,
+ multiEntry: false,
+ },
+ ],
+ },
+ ],
+ },
+ "http://sectest1.example.org": {},
+ "https://sectest1.example.org": {
+ "idb-s1 (default)": [
+ {
+ objectStore: "obj-s1",
+ keyPath: "id",
+ autoIncrement: false,
+ indexes: [],
+ },
+ ],
+ "idb-s2 (default)": [
+ {
+ objectStore: "obj-s2",
+ keyPath: "id3",
+ autoIncrement: true,
+ indexes: [
+ {
+ name: "name2",
+ keyPath: "name2",
+ unique: true,
+ multiEntry: false,
+ },
+ ],
+ },
+ ],
+ },
+ },
+ entries: {
+ "http://test1.example.org": {
+ "idb1 (default)#obj1": [
+ {
+ name: 1,
+ value: {
+ id: 1,
+ name: "foo",
+ email: "foo@bar.com",
+ },
+ },
+ {
+ name: 2,
+ value: {
+ id: 2,
+ name: "foo2",
+ email: "foo2@bar.com",
+ },
+ },
+ {
+ name: 3,
+ value: {
+ id: 3,
+ name: "foo2",
+ email: "foo3@bar.com",
+ },
+ },
+ ],
+ "idb1 (default)#obj2": [
+ {
+ name: 1,
+ value: {
+ id2: 1,
+ name: "foo",
+ email: "foo@bar.com",
+ extra: "baz",
+ },
+ },
+ ],
+ "idb2 (default)#obj3": [],
+ },
+ "http://sectest1.example.org": {},
+ "https://sectest1.example.org": {
+ "idb-s1 (default)#obj-s1": [
+ {
+ name: 6,
+ value: {
+ id: 6,
+ name: "foo",
+ email: "foo@bar.com",
+ },
+ },
+ {
+ name: 7,
+ value: {
+ id: 7,
+ name: "foo2",
+ email: "foo2@bar.com",
+ },
+ },
+ ],
+ "idb-s2 (default)#obj-s2": [
+ {
+ name: 13,
+ value: {
+ id2: 13,
+ name2: "foo",
+ email: "foo@bar.com",
+ },
+ },
+ ],
+ },
+ },
+};
+
+async function testStores(commands) {
+ const { resourceCommand } = commands;
+ const { TYPES } = resourceCommand;
+ /**
+ * Data is a dictionary whose keys are storage types (their resourceType)
+ * while values are objects with following attributes:
+ * - hosts: dictionary of storage values (values are specific to each storage type)
+ * keyed by host names.
+ * - dataByHost: dictionary of storage objects keyed by host names.
+ * storages objects are returned by StorageActor.getStoreObjects.
+ * For IndexedDB it is different, instead it is still a dictionary
+ * keyed by host names, but each value is yet another sub dictionary with
+ * a special "main" attribute, with global store objects.
+ * Then, there will be one key per idb database, with their store objects
+ * as value.
+ */
+ const data = {};
+ await resourceCommand.watchResources(
+ [
+ TYPES.COOKIE,
+ TYPES.LOCAL_STORAGE,
+ TYPES.SESSION_STORAGE,
+ TYPES.INDEXED_DB,
+ ],
+ {
+ async onAvailable(resources) {
+ for (const resource of resources) {
+ const { resourceType } = resource;
+ if (!data[resourceType]) {
+ data[resourceType] = { hosts: {}, dataByHost: {} };
+ }
+
+ for (const host in resource.hosts) {
+ if (!data[resourceType].hosts[host]) {
+ data[resourceType].hosts[host] = [];
+ }
+ // For indexed DB, we have some values, the database names. Other are empty arrays.
+ const hostValues = resource.hosts[host];
+ data[resourceType].hosts[host].push(...hostValues);
+
+ // For INDEXED_DB, it is slightly more complex, as we may have 3 store per host,
+ if (resourceType == TYPES.INDEXED_DB) {
+ if (!data[resourceType].dataByHost[host]) {
+ data[resourceType].dataByHost[host] = {};
+ }
+ data[resourceType].dataByHost[host].main =
+ await resource.getStoreObjects(host, null, {
+ sessionString,
+ });
+ for (const name of resource.hosts[host]) {
+ const objName = JSON.parse(name).slice(0, 1);
+ data[resourceType].dataByHost[host][objName] =
+ await resource.getStoreObjects(
+ host,
+ [JSON.stringify(objName)],
+ { sessionString }
+ );
+ data[resourceType].dataByHost[host][name] =
+ await resource.getStoreObjects(host, [name], {
+ sessionString,
+ });
+ }
+ } else {
+ data[resourceType].dataByHost[host] =
+ await resource.getStoreObjects(host, null, { sessionString });
+ }
+ }
+ }
+ },
+ }
+ );
+
+ await testCookies(data.cookies);
+ await testLocalStorage(data["local-storage"]);
+ await testSessionStorage(data["session-storage"]);
+ await testIndexedDB(data["indexed-db"]);
+}
+
+function testCookies({ hosts, dataByHost }) {
+ is(
+ Object.keys(hosts).length,
+ 3,
+ "Correct number of host entries for cookies"
+ );
+ return testCookiesObjects(0, hosts, dataByHost);
+}
+
+async function testCookiesObjects(index, hosts, dataByHost) {
+ const host = Object.keys(hosts)[index];
+ ok(!!storeMap.cookies[host], "Host is present in the list : " + host);
+ const data = dataByHost[host];
+ let cookiesLength = 0;
+ for (const secureCookie of storeMap.cookies[host]) {
+ if (secureCookie.isSecure) {
+ ++cookiesLength;
+ }
+ }
+ // Any secure cookies did not get stored in the database.
+ is(
+ data.total,
+ storeMap.cookies[host].length - cookiesLength,
+ "Number of cookies in host " + host + " matches"
+ );
+ for (const item of data.data) {
+ let found = false;
+ for (const toMatch of storeMap.cookies[host]) {
+ if (item.name == toMatch.name) {
+ found = true;
+ ok(true, "Found cookie " + item.name + " in response");
+ is(item.value.str, toMatch.value, "The value matches.");
+ is(item.expires, toMatch.expires, "The expiry time matches.");
+ is(item.path, toMatch.path, "The path matches.");
+ is(item.host, toMatch.host, "The host matches.");
+ is(item.isSecure, toMatch.isSecure, "The isSecure value matches.");
+ is(item.hostOnly, toMatch.hostOnly, "The hostOnly value matches.");
+ break;
+ }
+ }
+ ok(found, "cookie " + item.name + " should exist in response");
+ }
+
+ if (index == Object.keys(hosts).length - 1) {
+ return;
+ }
+ await testCookiesObjects(++index, hosts, dataByHost);
+}
+
+function testLocalStorage({ hosts, dataByHost }) {
+ is(
+ Object.keys(hosts).length,
+ 3,
+ "Correct number of host entries for local storage"
+ );
+ return testLocalStorageObjects(0, hosts, dataByHost);
+}
+
+var testLocalStorageObjects = async function (index, hosts, dataByHost) {
+ const host = Object.keys(hosts)[index];
+ ok(
+ !!storeMap["local-storage"][host],
+ "Host is present in the list : " + host
+ );
+ const data = dataByHost[host];
+ is(
+ data.total,
+ storeMap["local-storage"][host].length,
+ "Number of local storage items in host " + host + " matches"
+ );
+ for (const item of data.data) {
+ let found = false;
+ for (const toMatch of storeMap["local-storage"][host]) {
+ if (item.name == toMatch.name) {
+ found = true;
+ ok(true, "Found local storage item " + item.name + " in response");
+ is(item.value.str, toMatch.value, "The value matches.");
+ break;
+ }
+ }
+ ok(found, "local storage item " + item.name + " should exist in response");
+ }
+
+ if (index == Object.keys(hosts).length - 1) {
+ return;
+ }
+ await testLocalStorageObjects(++index, hosts, dataByHost);
+};
+
+function testSessionStorage({ hosts, dataByHost }) {
+ is(
+ Object.keys(hosts).length,
+ 3,
+ "Correct number of host entries for session storage"
+ );
+ return testSessionStorageObjects(0, hosts, dataByHost);
+}
+
+async function testSessionStorageObjects(index, hosts, dataByHost) {
+ const host = Object.keys(hosts)[index];
+ ok(
+ !!storeMap["session-storage"][host],
+ "Host is present in the list : " + host
+ );
+ const data = dataByHost[host];
+ is(
+ data.total,
+ storeMap["session-storage"][host].length,
+ "Number of session storage items in host " + host + " matches"
+ );
+ for (const item of data.data) {
+ let found = false;
+ for (const toMatch of storeMap["session-storage"][host]) {
+ if (item.name == toMatch.name) {
+ found = true;
+ ok(true, "Found session storage item " + item.name + " in response");
+ is(item.value.str, toMatch.value, "The value matches.");
+ break;
+ }
+ }
+ ok(
+ found,
+ "session storage item " + item.name + " should exist in response"
+ );
+ }
+
+ if (index == Object.keys(hosts).length - 1) {
+ return;
+ }
+ await testSessionStorageObjects(++index, hosts, dataByHost);
+}
+
+async function testIndexedDB({ hosts, dataByHost }) {
+ is(
+ Object.keys(hosts).length,
+ 3,
+ "Correct number of host entries for indexed db"
+ );
+
+ for (const host in hosts) {
+ for (const item of hosts[host]) {
+ const parsedItem = JSON.parse(item);
+ let found = false;
+ for (const toMatch of IDBValues.listStoresResponse[host]) {
+ if (toMatch[0] == parsedItem[0] && toMatch[1] == parsedItem[1]) {
+ found = true;
+ break;
+ }
+ }
+ ok(found, item + " should exist in list stores response");
+ }
+ }
+
+ await testIndexedDBs(0, hosts, dataByHost);
+ await testObjectStores(0, hosts, dataByHost);
+ await testIDBEntries(0, hosts, dataByHost);
+}
+
+async function testIndexedDBs(index, hosts, dataByHost) {
+ const host = Object.keys(hosts)[index];
+ const data = dataByHost[host].main;
+ is(
+ data.total,
+ IDBValues.dbDetails[host].length,
+ "Number of indexed db in host " + host + " matches"
+ );
+ for (const item of data.data) {
+ let found = false;
+ for (const toMatch of IDBValues.dbDetails[host]) {
+ if (item.uniqueKey == toMatch.db) {
+ found = true;
+ ok(true, "Found indexed db " + item.uniqueKey + " in response");
+ is(item.origin, toMatch.origin, "The origin matches.");
+ is(item.version, toMatch.version, "The version matches.");
+ is(
+ item.objectStores,
+ toMatch.objectStores,
+ "The number of object stores matches."
+ );
+ break;
+ }
+ }
+ ok(found, "indexed db " + item.uniqueKey + " should exist in response");
+ }
+
+ ok(!!IDBValues.dbDetails[host], "Host is present in the list : " + host);
+ if (index == Object.keys(hosts).length - 1) {
+ return;
+ }
+ await testIndexedDBs(++index, hosts, dataByHost);
+}
+
+async function testObjectStores(ix, hosts, dataByHost) {
+ const host = Object.keys(hosts)[ix];
+ const matchItems = (data, db) => {
+ is(
+ data.total,
+ IDBValues.objectStoreDetails[host][db].length,
+ "Number of object stores in host " + host + " matches"
+ );
+ for (const item of data.data) {
+ let found = false;
+ for (const toMatch of IDBValues.objectStoreDetails[host][db]) {
+ if (item.objectStore == toMatch.objectStore) {
+ found = true;
+ ok(true, "Found object store " + item.objectStore + " in response");
+ is(item.keyPath, toMatch.keyPath, "The keyPath matches.");
+ is(
+ item.autoIncrement,
+ toMatch.autoIncrement,
+ "The autoIncrement matches."
+ );
+ // We might already have parsed the JSON value, in which case this will no longer be a string
+ item.indexes =
+ typeof item.indexes == "string"
+ ? JSON.parse(item.indexes)
+ : item.indexes;
+ is(
+ item.indexes.length,
+ toMatch.indexes.length,
+ "Number of indexes match"
+ );
+ for (const index of item.indexes) {
+ let indexFound = false;
+ for (const toMatchIndex of toMatch.indexes) {
+ if (toMatchIndex.name == index.name) {
+ indexFound = true;
+ ok(true, "Found index " + index.name);
+ is(
+ index.keyPath,
+ toMatchIndex.keyPath,
+ "The keyPath of index matches."
+ );
+ is(index.unique, toMatchIndex.unique, "The unique matches");
+ is(
+ index.multiEntry,
+ toMatchIndex.multiEntry,
+ "The multiEntry matches"
+ );
+ break;
+ }
+ }
+ ok(indexFound, "Index " + index + " should exist in response");
+ }
+ break;
+ }
+ }
+ ok(found, "indexed db " + item.name + " should exist in response");
+ }
+ };
+
+ ok(
+ !!IDBValues.objectStoreDetails[host],
+ "Host is present in the list : " + host
+ );
+ for (const name of hosts[host]) {
+ const objName = JSON.parse(name).slice(0, 1);
+ matchItems(dataByHost[host][objName], objName[0]);
+ }
+ if (ix == Object.keys(hosts).length - 1) {
+ return;
+ }
+ await testObjectStores(++ix, hosts, dataByHost);
+}
+
+async function testIDBEntries(index, hosts, dataByHost) {
+ const host = Object.keys(hosts)[index];
+ const matchItems = (data, obj) => {
+ is(
+ data.total,
+ IDBValues.entries[host][obj].length,
+ "Number of items in object store " + obj + " matches"
+ );
+ for (const item of data.data) {
+ let found = false;
+ for (const toMatch of IDBValues.entries[host][obj]) {
+ if (item.name == toMatch.name) {
+ found = true;
+ ok(true, "Found indexed db item " + item.name + " in response");
+ const value = JSON.parse(item.value.str);
+ is(
+ Object.keys(value).length,
+ Object.keys(toMatch.value).length,
+ "Number of entries in the value matches"
+ );
+ for (const key in value) {
+ is(
+ value[key],
+ toMatch.value[key],
+ "value of " + key + " value key matches"
+ );
+ }
+ break;
+ }
+ }
+ ok(found, "indexed db item " + item.name + " should exist in response");
+ }
+ };
+
+ ok(!!IDBValues.entries[host], "Host is present in the list : " + host);
+ for (const name of hosts[host]) {
+ const parsed = JSON.parse(name);
+ matchItems(dataByHost[host][name], parsed[0] + "#" + parsed[1]);
+ }
+ if (index == Object.keys(hosts).length - 1) {
+ return;
+ }
+ await testObjectStores(++index, hosts, dataByHost);
+}
+
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.documentCookies.maxage", 0]],
+ });
+
+ const { commands } = await openTabAndSetupStorage(
+ MAIN_DOMAIN + "storage-listings.html"
+ );
+
+ await testStores(commands);
+
+ await clearStorage();
+
+ // Forcing GC/CC to get rid of docshells and windows created by this test.
+ forceCollections();
+ await commands.destroy();
+ forceCollections();
+});
diff --git a/devtools/server/tests/browser/browser_storage_updates.js b/devtools/server/tests/browser/browser_storage_updates.js
new file mode 100644
index 0000000000..50926538a5
--- /dev/null
+++ b/devtools/server/tests/browser/browser_storage_updates.js
@@ -0,0 +1,343 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Ensure that storage updates are detected and that the correct information is
+// contained inside the storage actors.
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/server/tests/browser/storage-helpers.js",
+ this
+);
+
+const l10n = new Localization(["devtools/client/storage.ftl"], true);
+const sessionString = l10n.formatValueSync("storage-expires-session");
+
+const TESTS = [
+ // index 0
+ {
+ async action(win) {
+ await addCookie("c1", "foobar1");
+ await addCookie("c2", "foobar2");
+ await localStorageSetItem("l1", "foobar1");
+ },
+ snapshot: {
+ cookies: [
+ {
+ name: "c1",
+ value: "foobar1",
+ },
+ {
+ name: "c2",
+ value: "foobar2",
+ },
+ ],
+ "local-storage": [
+ {
+ name: "l1",
+ value: "foobar1",
+ },
+ ],
+ },
+ },
+
+ // index 1
+ {
+ async action() {
+ await addCookie("c1", "new_foobar1");
+ await localStorageSetItem("l2", "foobar2");
+ },
+ snapshot: {
+ cookies: [
+ {
+ name: "c1",
+ value: "new_foobar1",
+ },
+ {
+ name: "c2",
+ value: "foobar2",
+ },
+ ],
+ "local-storage": [
+ {
+ name: "l1",
+ value: "foobar1",
+ },
+ {
+ name: "l2",
+ value: "foobar2",
+ },
+ ],
+ },
+ },
+
+ // index 2
+ {
+ async action() {
+ await removeCookie("c2");
+ await localStorageRemoveItem("l1");
+ await localStorageSetItem("l3", "foobar3");
+ },
+ snapshot: {
+ cookies: [
+ {
+ name: "c1",
+ value: "new_foobar1",
+ },
+ ],
+ "local-storage": [
+ {
+ name: "l2",
+ value: "foobar2",
+ },
+ {
+ name: "l3",
+ value: "foobar3",
+ },
+ ],
+ },
+ },
+
+ // index 3
+ {
+ async action() {
+ await removeCookie("c1");
+ await addCookie("c3", "foobar3");
+ await localStorageRemoveItem("l2");
+ await sessionStorageSetItem("s1", "foobar1");
+ await sessionStorageSetItem("s2", "foobar2");
+ await localStorageSetItem("l3", "new_foobar3");
+ },
+ snapshot: {
+ cookies: [
+ {
+ name: "c3",
+ value: "foobar3",
+ },
+ ],
+ "local-storage": [
+ {
+ name: "l3",
+ value: "new_foobar3",
+ },
+ ],
+ "session-storage": [
+ {
+ name: "s1",
+ value: "foobar1",
+ },
+ {
+ name: "s2",
+ value: "foobar2",
+ },
+ ],
+ },
+ },
+
+ // index 4
+ {
+ async action() {
+ await sessionStorageRemoveItem("s1");
+ },
+ snapshot: {
+ cookies: [
+ {
+ name: "c3",
+ value: "foobar3",
+ },
+ ],
+ "local-storage": [
+ {
+ name: "l3",
+ value: "new_foobar3",
+ },
+ ],
+ "session-storage": [
+ {
+ name: "s2",
+ value: "foobar2",
+ },
+ ],
+ },
+ },
+
+ // index 5
+ {
+ async action() {
+ await clearCookies();
+ },
+ snapshot: {
+ cookies: [],
+ "local-storage": [
+ {
+ name: "l3",
+ value: "new_foobar3",
+ },
+ ],
+ "session-storage": [
+ {
+ name: "s2",
+ value: "foobar2",
+ },
+ ],
+ },
+ },
+
+ // index 6
+ {
+ async action() {
+ await clearLocalAndSessionStores();
+ },
+ snapshot: {
+ cookies: [],
+ "local-storage": [],
+ "session-storage": [],
+ },
+ },
+];
+
+add_task(async function () {
+ const { commands } = await openTabAndSetupStorage(
+ MAIN_DOMAIN + "storage-updates.html"
+ );
+
+ for (let i = 0; i < TESTS.length; i++) {
+ const test = TESTS[i];
+ await runTest(test, commands, i);
+ }
+
+ await commands.destroy();
+});
+
+async function runTest({ action, snapshot }, commands, index) {
+ info("Running test at index " + index);
+ await action();
+ await checkStores(commands, snapshot);
+}
+
+async function checkStores(commands, snapshot) {
+ const { resourceCommand } = commands;
+ const { TYPES } = resourceCommand;
+ const actual = {};
+ await resourceCommand.watchResources(
+ [TYPES.COOKIE, TYPES.LOCAL_STORAGE, TYPES.SESSION_STORAGE],
+ {
+ async onAvailable(resources) {
+ for (const resource of resources) {
+ actual[resource.resourceType] = await resource.getStoreObjects(
+ TEST_DOMAIN,
+ null,
+ {
+ sessionString,
+ }
+ );
+ }
+ },
+ }
+ );
+
+ for (const [type, entries] of Object.entries(snapshot)) {
+ const store = actual[type].data;
+
+ is(
+ store.length,
+ entries.length,
+ `The number of entries in ${type} is correct`
+ );
+
+ for (const entry of entries) {
+ checkStoreValue(entry.name, entry.value, store);
+ }
+ }
+}
+
+function checkStoreValue(name, value, store) {
+ for (const entry of store) {
+ if (entry.name === name) {
+ ok(true, `There is an entry for "${name}"`);
+
+ // entry.value is a longStringActor so we need to read it's value using
+ // entry.value.str.
+ is(entry.value.str, value, `Value for ${name} is correct`);
+ return;
+ }
+ }
+ ok(false, `There is an entry for "${name}"`);
+}
+
+async function addCookie(name, value) {
+ info(`addCookie("${name}", "${value}")`);
+
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [[name, value]],
+ ([iName, iValue]) => {
+ content.wrappedJSObject.window.addCookie(iName, iValue);
+ }
+ );
+}
+
+async function removeCookie(name) {
+ info(`removeCookie("${name}")`);
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [name], iName => {
+ content.wrappedJSObject.window.removeCookie(iName);
+ });
+}
+
+async function localStorageSetItem(name, value) {
+ info(`localStorageSetItem("${name}", "${value}")`);
+
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [[name, value]],
+ ([iName, iValue]) => {
+ content.window.localStorage.setItem(iName, iValue);
+ }
+ );
+}
+
+async function localStorageRemoveItem(name) {
+ info(`localStorageRemoveItem("${name}")`);
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [name], iName => {
+ content.window.localStorage.removeItem(iName);
+ });
+}
+
+async function sessionStorageSetItem(name, value) {
+ info(`sessionStorageSetItem("${name}", "${value}")`);
+
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [[name, value]],
+ ([iName, iValue]) => {
+ content.window.sessionStorage.setItem(iName, iValue);
+ }
+ );
+}
+
+async function sessionStorageRemoveItem(name) {
+ info(`sessionStorageRemoveItem("${name}")`);
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [name], iName => {
+ content.window.sessionStorage.removeItem(iName);
+ });
+}
+
+async function clearCookies() {
+ info(`clearCookies()`);
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.wrappedJSObject.window.clearCookies();
+ });
+}
+
+async function clearLocalAndSessionStores() {
+ info(`clearLocalAndSessionStores()`);
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.wrappedJSObject.window.clearLocalAndSessionStores();
+ });
+}
diff --git a/devtools/server/tests/browser/browser_style_utils_getFontPreviewData.js b/devtools/server/tests/browser/browser_style_utils_getFontPreviewData.js
new file mode 100644
index 0000000000..7145e90446
--- /dev/null
+++ b/devtools/server/tests/browser/browser_style_utils_getFontPreviewData.js
@@ -0,0 +1,137 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that getFontPreviewData of the style utils generates font previews.
+
+const TEST_URI = "data:text/html,<title>Test getFontPreviewData</title>";
+
+add_task(async function () {
+ await addTab(TEST_URI);
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ const {
+ getFontPreviewData,
+ } = require("resource://devtools/server/actors/utils/style-utils.js");
+
+ const font = Services.appinfo.OS === "WINNT" ? "Arial" : "Liberation Sans";
+ let fontPreviewData = getFontPreviewData(font, content.document);
+ ok(
+ fontPreviewData?.dataURL,
+ "Returned a font preview with a valid dataURL"
+ );
+
+ // Create <img> element and load the generated preview into it
+ // to check whether the image is valid and get its dimensions
+ const image = content.document.createElement("img");
+ let imageLoaded = new Promise(loaded =>
+ image.addEventListener("load", loaded, { once: true })
+ );
+ image.src = fontPreviewData.dataURL;
+ await imageLoaded;
+
+ const { naturalWidth: widthImage1, naturalHeight: heightImage1 } = image;
+
+ Assert.greater(widthImage1, 0, "Preview width is greater than 0");
+ Assert.greater(heightImage1, 0, "Preview height is greater than 0");
+
+ // Create a preview with different text and compare
+ // its dimensions with the first one
+ fontPreviewData = getFontPreviewData(font, content.document, {
+ previewText: "Abcdef",
+ });
+
+ ok(
+ fontPreviewData?.dataURL,
+ "Returned a font preview with a valid dataURL"
+ );
+
+ imageLoaded = new Promise(loaded =>
+ image.addEventListener("load", loaded, { once: true })
+ );
+ image.src = fontPreviewData.dataURL;
+ await imageLoaded;
+
+ const { naturalWidth: widthImage2, naturalHeight: heightImage2 } = image;
+
+ // Check whether the width is greater than with the default parameters
+ // and that the height is the same
+ Assert.greater(
+ widthImage2,
+ widthImage1,
+ "Preview width is greater than with default parameters"
+ );
+ Assert.strictEqual(
+ heightImage2,
+ heightImage1,
+ "Preview height is the same as with default parameters"
+ );
+
+ // Create a preview with smaller font size and compare
+ // its dimensions with the first one
+ fontPreviewData = getFontPreviewData(font, content.document, {
+ previewFontSize: 20,
+ });
+
+ ok(
+ fontPreviewData?.dataURL,
+ "Returned a font preview with a valid dataURL"
+ );
+
+ imageLoaded = new Promise(loaded =>
+ image.addEventListener("load", loaded, { once: true })
+ );
+ image.src = fontPreviewData.dataURL;
+ await imageLoaded;
+
+ const { naturalWidth: widthImage3, naturalHeight: heightImage3 } = image;
+
+ // Check whether the width and height are smaller than with the default parameters
+ Assert.less(
+ widthImage3,
+ widthImage1,
+ "Preview width is smaller than with default parameters"
+ );
+ Assert.less(
+ heightImage3,
+ heightImage1,
+ "Preview height is smaller than with default parameters"
+ );
+
+ // Create a preview with multiple lines and compare
+ // its dimensions with the first one
+ fontPreviewData = getFontPreviewData(font, content.document, {
+ previewText: "Abc\ndef",
+ });
+
+ ok(
+ fontPreviewData?.dataURL,
+ "Returned a font preview with a valid dataURL"
+ );
+
+ imageLoaded = new Promise(loaded =>
+ image.addEventListener("load", loaded, { once: true })
+ );
+ image.src = fontPreviewData.dataURL;
+ await imageLoaded;
+
+ const { naturalWidth: widthImage4, naturalHeight: heightImage4 } = image;
+
+ // Check whether the width is the same as with the default parameters
+ // and that the height is greater
+ Assert.strictEqual(
+ widthImage4,
+ widthImage1,
+ "Preview width is the same as with default parameters"
+ );
+ Assert.greater(
+ heightImage4,
+ heightImage1,
+ "Preview height is greater than with default parameters"
+ );
+ });
+});
diff --git a/devtools/server/tests/browser/browser_styles_getRuleText.js b/devtools/server/tests/browser/browser_styles_getRuleText.js
new file mode 100644
index 0000000000..e775bcbb28
--- /dev/null
+++ b/devtools/server/tests/browser/browser_styles_getRuleText.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that StyleRuleActor.getRuleText returns the contents of the CSS rule.
+
+const CSS_RULE = `#test {
+ background-color: #f06;
+}`;
+
+const CONTENT = `
+ <style type='text/css'>
+ ${CSS_RULE}
+ </style>
+ <div id="test"></div>
+`;
+
+const TEST_URI = `data:text/html;charset=utf-8,${encodeURIComponent(CONTENT)}`;
+
+add_task(async function () {
+ const { inspector, target, walker } = await initInspectorFront(TEST_URI);
+
+ const pageStyle = await inspector.getPageStyle();
+ const element = await walker.querySelector(walker.rootNode, "#test");
+ const entries = await pageStyle.getApplied(element, { inherited: false });
+
+ const rule = entries[1].rule;
+ const text = await rule.getRuleText();
+
+ is(text, CSS_RULE, "CSS rule text content matches");
+
+ await target.destroy();
+});
diff --git a/devtools/server/tests/browser/browser_stylesheets_getTextEmpty.js b/devtools/server/tests/browser/browser_stylesheets_getTextEmpty.js
new file mode 100644
index 0000000000..a8c069e950
--- /dev/null
+++ b/devtools/server/tests/browser/browser_stylesheets_getTextEmpty.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that StyleSheetsActor.getText handles empty text correctly.
+
+const CSS_CONTENT = "body { background-color: #f06; }";
+const TEST_URI = `data:text/html;charset=utf-8,<style>${encodeURIComponent(
+ CSS_CONTENT
+)}</style>`;
+
+add_task(async function () {
+ const tab = await addTab(TEST_URI);
+
+ const commands = await CommandsFactory.forTab(tab);
+ await commands.targetCommand.startListening();
+ const target = commands.targetCommand.targetFront;
+
+ const styleSheetsFront = await target.getFront("stylesheets");
+ ok(styleSheetsFront, "The StyleSheetsFront was created.");
+
+ const sheets = [];
+ await commands.resourceCommand.watchResources(
+ [commands.resourceCommand.TYPES.STYLESHEET],
+ {
+ onAvailable: resources => sheets.push(...resources),
+ }
+ );
+ is(sheets.length, 1, "watchResources returned the correct number of sheets");
+
+ const { resourceId } = sheets[0];
+
+ is(
+ await getStyleSheetText(styleSheetsFront, resourceId),
+ CSS_CONTENT,
+ "The stylesheet has expected initial text"
+ );
+ info("Update stylesheet content via the styleSheetsFront");
+ await styleSheetsFront.update(resourceId, "", false);
+ is(
+ await getStyleSheetText(styleSheetsFront, resourceId),
+ "",
+ "Stylesheet is now empty, as expected"
+ );
+
+ await commands.destroy();
+});
+
+async function getStyleSheetText(styleSheetsFront, resourceId) {
+ const longStringFront = await styleSheetsFront.getText(resourceId);
+ return longStringFront.string();
+}
diff --git a/devtools/server/tests/browser/director-script-target.html b/devtools/server/tests/browser/director-script-target.html
new file mode 100644
index 0000000000..c436a5446c
--- /dev/null
+++ b/devtools/server/tests/browser/director-script-target.html
@@ -0,0 +1,18 @@
+<html>
+ <head>
+ <script>
+ /* exported globalAccessibleVar */
+ "use strict";
+ // change the eval function to ensure the window object
+ // in the debug-script is correctly wrapped
+ // eslint-disable-next-line no-eval
+ window.eval = function() {
+ return "unsecure-eval-called";
+ };
+ var globalAccessibleVar = "global-value";
+ </script>
+ </head>
+ <body>
+ <h1>debug script target</h1>
+ </body>
+</html>
diff --git a/devtools/server/tests/browser/doc_accessibility.html b/devtools/server/tests/browser/doc_accessibility.html
new file mode 100644
index 0000000000..845dd7c562
--- /dev/null
+++ b/devtools/server/tests/browser/doc_accessibility.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+<body>
+ <h1 id="h1">Accessibility Test</h1>
+ <button id="button" aria-describedby="h1" accesskey="b">Accessible Button</button>
+ <div id="slider" role="slider" aria-valuenow="5"
+ aria-valuemin="0" aria-valuemax="7">slider</div>
+ <label id="label" for="control">Label
+ <input id="control" aria-details="details">
+ </label>
+ <div id="details">details</div>
+ <header id="header">header</header>
+ <nav id="nav">nav</nav>
+ <footer id="footer">footer</footer>
+</body>
+</html>
diff --git a/devtools/server/tests/browser/doc_accessibility_audit.html b/devtools/server/tests/browser/doc_accessibility_audit.html
new file mode 100644
index 0000000000..0667e0569e
--- /dev/null
+++ b/devtools/server/tests/browser/doc_accessibility_audit.html
@@ -0,0 +1,10 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+<body style="color: red;">
+ <p id="p1">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
+ <p id="p2">Accessible Paragraph</p>
+</body>
+</html>
diff --git a/devtools/server/tests/browser/doc_accessibility_infobar.html b/devtools/server/tests/browser/doc_accessibility_infobar.html
new file mode 100644
index 0000000000..8f3c66911c
--- /dev/null
+++ b/devtools/server/tests/browser/doc_accessibility_infobar.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+<body>
+ <h1 id="h1">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</h1>
+ <button id="button">Accessible Button</button>
+ <p id="p" style="font-size: 0;">This is a paragraph that has no bounds.</p>
+ <label>Enter text: <input id="input" type="text"></text></label>
+</body>
+</html>
diff --git a/devtools/server/tests/browser/doc_accessibility_keyboard_audit.html b/devtools/server/tests/browser/doc_accessibility_keyboard_audit.html
new file mode 100644
index 0000000000..00c002efe9
--- /dev/null
+++ b/devtools/server/tests/browser/doc_accessibility_keyboard_audit.html
@@ -0,0 +1,150 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <style>
+ #focusable-1 {
+ outline: none;
+ }
+
+ #focusable-2:focus {
+ outline: none;
+ border: 1px solid black;
+ }
+ </style>
+ </head>
+<body>
+ <div id="button-1" class="Button" tabindex="0">I should really be a button</div>
+ <div id="button-2" class="Button">I should really be a button</div>
+ <div id="input-container"><input id="input-1" type="text" tabindex="-1" /></div>
+ <input id="input-2" type="text" tabindex="-1" disabled />
+ <input id="input-3" type="text" disabled />
+ <input id="input-4" type="text" />
+ <a id="link-1">Though a link, I'm not interactive.</a>
+ <a id="link-2" href="example.com">I'm a proper link.</a>
+ <a id="link-3" href="#">Link 3</a>
+ <a id="link-4" href="">Link 4</a>
+ <a id="link-5" href="https://example.com">Website</a>
+ <button id="button-3">Button with no tabindex</button>
+ <button id="button-4" tabindex="-1">Button with -1 tabindex</button>
+ <button id="button-5" tabindex="0">Button with 0 tabindex</button>
+ <button id="button-6" tabindex="1">Button with 1 tabindex</button>
+ <div id="focusable-1" role="button" tabindex="0">Focusable with no focus style.</div>
+ <div id="focusable-2" role="button" tabindex="0">Focusable with focus style.</div>
+ <div id="focusable-3" role="button" tabindex="0">Focusable with focus style.</div>
+ <div id="mouse-only-1" onclick="console.log('foo');">Button for mouse only</div>
+ <div id="focusable-4" onclick="console.log('foo');" tabindex="0">Button no semantics</div>
+ <div id="button-7" onclick="console.log('foo');" role="button">Semantic button not focusable</div>
+ <div id="button-8" onclick="console.log('foo');" role="button" tabindex="0">Button</div>
+ <img id="img-1" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" alt="alt text">
+ <img id="img-2" longdesc="https://example.com" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" alt="alt text">
+ <img id="img-3" longdesc="https://example.com" onclick="console.log('foo');" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" alt="alt text">
+ <img id="img-4" onclick="console.log('foo');" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" alt="alt text">
+ <button id="buttonmenu-1" aria-haspopup="true">I have a popup</button>
+ <div role="button" id="buttonmenu-2" aria-haspopup="true">I have a popup</div>
+ <input id="checkbox-1" type="checkbox" name="hello" />
+ <select id="listbox-1" size="2">
+ <option id="lb_orange">orange</option>
+ <option id="lb_apple">apple</option>
+ </select>
+ <select id="combobox-1"></select>
+ <select id="combobox-2"><option>One</option></select>
+ <select id="combobox-3">
+ <option id="cb_orange">orange</option>
+ <option id="cb_apple">apple</option>
+ </select>
+ <div id="editcombobox-1" role="combobox"><span role="option">One</span></div>
+ <span id="editcombobox-2"role="combobox"></span>
+ <span id="editcombobox-3"role="combobox" tabindex="0"></span>
+ <span id="switch-1" role="switch"></span>
+ <span id="switch-2" role="switch" tabindex="0"></span>
+ <div aria-label="Tag" role="combobox" aria-expanded="true" aria-owns="owned_listbox" aria-haspopup="listbox">
+ <input type="text" aria-autocomplete="list" aria-controls="owned_listbox" aria-activedescendant="selected_option">
+ </div>
+ <ul role="listbox" id="owned_listbox">
+ <li role="option">Zebra</li>
+ <li role="option" id="selected_option">Zoom</li>
+ </ul>
+ <label id="label-1">hello<input type="checkbox" name="world" /></label>
+ <label id="label-2" for="checkbox-1">hello</label>
+ <label id="label-3">hello</label>
+ <label id="label-4">hello</label><input type="checkbox" name="world" />
+ <a href="about:mozilla" target="_blank" rel="opener">
+ <img id="img-5" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" alt="alt text">
+ </a>
+ <a onmousedown="">
+ <img id="img-6" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" alt="alt text">
+ </a>
+ <a onclick="">
+ <img id="img-7" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" alt="alt text">
+ </a>
+ <a onmouseup="">
+ <img id="img-8" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" alt="alt text">
+ </a>
+ <section id="section-1" class="collapsible-section top-sites animation-enabled" aria-expanded="true"></section>
+ <main id="main" tabindex="-1">Main content</main>
+ <div id="not-keyboard-focusable-1" tabindex="-1">Not keyboard fqocusable with no focus style.</div>
+ <div id="grid-1" role="grid" aria-label="Interactive grid"></div>
+ <div id="grid-2" tabindex="0" role="grid" aria-label="Interactive grid"></div>
+ <div id="table-1" role="table" aria-label="Non-interactive ARIA table"></div>
+ <div id="table-2" tabindex="0" role="table" aria-label="Non-interactive ARIA table"></div>
+ <table id="table-3" aria-label="Non-interactive table"></table>
+ <table id="table-4" tabindex="0" aria-label="Non-interactive table"></table>
+ <div id="article-1" role="article"></div>
+ <div id="article-2" role="article" tabindex="0"></div>
+ <div role="grid" aria-label="Interactive grid">
+ <div id="columnheader-1" role="columnheader"></div>
+ <div id="rowheader-1" role="rowheader"></div>
+ <div id="gridcell-1" role="gridcell"></div>
+ <div id="gridcell-2" role="gridcell" tabindex="0"></div>
+ </div>
+ <div role="table" aria-label="Non-interactive table">
+ <div id="columnheader-2" role="columnheader"></div>
+ <div id="rowheader-2" role="rowheader"></div>
+ </div>
+ <table>
+ <tr>
+ <th id="columnheader-3">Animals</th>
+ </tr>
+ <tr>
+ <th id="columnheader-4" tabindex="0">Hippopotamus</th>
+ </tr>
+ <tr>
+ <th id="rowheader-3">Horse</th>
+ <td>Mare</td>
+ </tr>
+ <tr>
+ <th id="rowheader-4" tabindex="0">Chicken</th>
+ <td>Hen</td>
+ </tr>
+ </table>
+ <table role="grid">
+ <tr>
+ <th id="columnheader-5">Animals</th>
+ </tr>
+ <tr>
+ <th id="columnheader-6" tabindex="0">Hippopotamus</th>
+ </tr>
+ <tr>
+ <th id="rowheader-5">Horse</th>
+ <td id="gridcell-3">Mare</td>
+ </tr>
+ <tr>
+ <th id="rowheader-6" tabindex="0">Chicken</th>
+ <td id="gridcell-4" tabindex="0">Hen</td>
+ </tr>
+ </table>
+ <div id="tablist-1" role="tablist"></div>
+ <div id="tablist-2" role="tablist" tabindex="0"></div>
+ <div id="scrollbar-1" role="scrollbar"></div>
+ <div id="scrollbar-2" role="scrollbar" tabindex="0"></div>
+ <div id="separator-1" role="separator"></div>
+ <div id="separator-2" role="separator" tabindex="0"></div>
+ <div id="toolbar-1" role="toolbar"></div>
+ <div id="toolbar-2" role="toolbar" tabindex="0"></div>
+ <div id="menu-1" role="menu"></div>
+ <div id="menu-2" role="menu" tabindex="0"></div>
+ <div id="menubar-1" role="menubar"></div>
+ <div id="menubar-2" role="menubar" tabindex="0"></div>
+</body>
+</html>
diff --git a/devtools/server/tests/browser/doc_accessibility_text_label_audit.html b/devtools/server/tests/browser/doc_accessibility_text_label_audit.html
new file mode 100644
index 0000000000..982cc5c243
--- /dev/null
+++ b/devtools/server/tests/browser/doc_accessibility_text_label_audit.html
@@ -0,0 +1,463 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+<body>
+ <button id="buttonmenu-1" aria-haspopup="true">I have a popup</button>
+ <label>I have a popup<button id="buttonmenu-2" aria-haspopup="true"></button></label>
+ <button id="buttonmenu-3" aria-haspopup="true"></button>
+ <button id="buttonmenu-4" aria-haspopup="true" aria-label="I have a popup"></button>
+ <label for="buttonmenu-5">I have a popup </label><button id="buttonmenu-5" aria-haspopup="true"></button>
+ <label id="buttonmenu-6-label">I have a popup </label><button id="buttonmenu-6" aria-haspopup="true" aria-labelledby="buttonmenu-6-label"></button>
+ <p id="p1">I am a paragraph</p>
+ <p id="p2"></p>
+ <canvas id="canvas-1"></canvas>
+ <canvas id="canvas-2" aria-label="Canvas label"></canvas>
+ <canvas id="canvas-3" aria-labelledby="canvas-3-heading">
+ <h2 id="canvas-3-heading">Shapes</h2>
+ </canvas>
+ <canvas id="canvas-4">
+ <h2>Shapes</h2>
+ </canvas>
+ <input id="checkbox-1" type="checkbox" name="world" />
+ <label>hello</label><input id="checkbox-2" type="checkbox" name="world" />
+ <label>hello<input id="checkbox-3" type="checkbox" name="world" /></label>
+ <label for="checkbox-4">hello</label><input id="checkbox-4" type="checkbox" name="world" />
+ <input id="checkbox-5" type="checkbox" name="world" aria-label="hello" />
+ <label id="checkbox-6-label">hello</label><input id="checkbox-6" type="checkbox" name="world" aria-labelledby="checkbox-6-label" />
+ <div id="checkbox-7" role="checkbox"></div>
+ <div id="checkbox-8" aria-label="hello" role="checkbox"></div>
+ <div id="checkbox-9-label">hello</div><div id="checkbox-9" aria-labelledby="checkbox-9-label" role="checkbox"></div>
+ <div role="menu">
+ <div id="menuitemcheckbox-1" role="menuitemcheckbox">hello</div>
+ <div id="menuitemcheckbox-2" role="menuitemcheckbox"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" /></div>
+ <div id="menuitemcheckbox-3" role="menuitemcheckbox"></div>
+ <div id="menuitemcheckbox-4" role="menuitemcheckbox"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" alt="" /></div>
+ <div id="menuitemcheckbox-5" role="menuitemcheckbox"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" alt="hello" /></div>
+ <div id="menuitemcheckbox-6" role="menuitemcheckbox">&nbsp;</div>
+ </div>
+ <p id="columnheader-7-label">Budget</p>
+ <p id="rowheader-7-label">Toy Story 3</p>
+ <table>
+ <thead>
+ <tr>
+ <th id="columnheader-1" scope="col">Film Title</th>
+ <th id="columnheader-2" scope="col"></th>
+ <th id="columnheader-3" scope="col">&nbsp;</th>
+ <th id="columnheader-4" scope="col" aria-label="Worldwide Gross"></th>
+ <th id="columnheader-5" scope="col" aria-label=""></th>
+ <th id="columnheader-6" scope="col" aria-label=" "></th>
+ <th id="columnheader-7" scope="col" aria-labelledby="columnheader-7-label"></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr><th id="rowheader-1" scope="row">Toy Story 3</th></tr>
+ <tr><th id="rowheader-2" scope="row"></th></tr>
+ <tr><th id="rowheader-3" scope="row">&nbsp;</th></tr>
+ <tr><th id="rowheader-4" scope="row" aria-label="Alladin"></th></tr>
+ <tr><th id="rowheader-5" scope="row" aria-label=""></th></tr>
+ <tr><th id="rowheader-6" scope="row" aria-label=" "></th></tr>
+ <tr><th id="rowheader-7" scope="row" aria-labelledby="columnheader-7-label"></th></tr>
+ </tbody>
+ </table>
+ <div role="columnheader" id="columnheader-8">Film Title</div>
+ <div role="columnheader" id="columnheader-9"></div>
+ <div role="columnheader" id="columnheader-10">&nbsp;</div>
+ <div role="columnheader" id="columnheader-11" aria-label="Worldwide Gross"></div>
+ <div role="columnheader" id="columnheader-12" aria-label=""></div>
+ <div role="columnheader" id="columnheader-13" aria-label=" "></div>
+ <div role="columnheader" id="columnheader-14" aria-labelledby="columnheader-7-label"></div>
+ <label for="combobox-1">Choose a pet:</label>
+ <select id="combobox-1">
+ <option id="combobox-option-1" value="">--Please choose an option--</option>
+ <option id="combobox-option-2" value="dog"></option>
+ <option id="combobox-option-3" value="cat">&nbsp;</option>
+ <option id="combobox-option-4" value="" label="--Please choose an option--"></option>
+ <option id="combobox-option-5" value="dog" label=""></option>
+ <option id="combobox-option-6" value="cat" label=" "></option>
+ </select>
+ <select id="combobox-2"></select>
+ <label>Choose a pet:</label><select id="combobox-3"></select>
+ <label>Choose a pet:<select id="combobox-4"></select></label>
+ <select id="combobox-5" aria-label="Choose a pet:"></select>
+ <label id="combobox-6-label">Choose a pet:</label><select id="combobox-6" aria-labelledby="combobox-6-label"></select>
+ <svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="diagram-1"
+ xmlns:xlink="http://www.w3.org/1999/xlink"></svg>
+ <svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="diagram-2" aria-label=""
+ xmlns:xlink="http://www.w3.org/1999/xlink"></svg>
+ <svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="diagram-3" aria-label="empty drawing"
+ xmlns:xlink="http://www.w3.org/1999/xlink"></svg>
+ <div id="diagram-4-label">Empty drawing</div>
+ <svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="diagram-4" aria-labelledby="diagram-4-label"
+ xmlns:xlink="http://www.w3.org/1999/xlink"></svg>
+ <div id="diagram-5-label"></div>
+ <svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="diagram-5" aria-labelledby="diagram-5-label"
+ xmlns:xlink="http://www.w3.org/1999/xlink"></svg>
+ <dialog id="dialog-1" open>
+ <p>Greetings, one and all!</p>
+ </dialog>
+ <dialog id="dialog-2" aria-label="" open>
+ <p>Greetings, one and all!</p>
+ </dialog>
+ <dialog id="dialog-3" aria-label="Greetings" open>
+ <p>Greetings, one and all!</p>
+ </dialog>
+ <dialog id="dialog-4" aria-labelledby="dialog-4-label" open>
+ <p id="dialog-4-label">Greetings, one and all!</p>
+ </dialog>
+ <div role="dialog" id="dialog-5">
+ <p>Greetings, one and all!</p>
+ </div>
+ <div role="dialog" id="dialog-6" aria-label="">
+ <p>Greetings, one and all!</p>
+ </div>
+ <div role="dialog" id="dialog-7" aria-label="Greetings">
+ <p>Greetings, one and all!</p>
+ </div>
+ <div role="dialog" id="dialog-8" aria-labelledby="dialog-8-label">
+ <p id="dialog-8-label">Greetings, one and all!</p>
+ </div>
+ <dialog id="dialog-9" aria-labelledby="dialog-9-label" open>
+ <p id="dialog-9-label"></p>
+ </dialog>
+ <div role="dialog" id="dialog-10" aria-labelledby="dialog-10-label">
+ <p id="dialog-10-label"></p>
+ </div>
+ <div id="editcombobox-1" role="combobox"></div>
+ <div id="editcombobox-2" aria-label="Choose a pet:" role="combobox"></div>
+ <div id="editcombobox-3-label">Choose a pet:</div><div id="editcombobox-3" aria-labelledby="editcombobox-3-label" role="combobox"></div>
+ <label>Customer name: <input id="entry-1"></label>
+ <input id="entry-2">
+ <input id="entry-3" aria-label="Customer name:">
+ <label>Customer name: </label><input id="entry-4">
+ <label for="entry-5">Customer name: </label><input id="entry-5">
+ <label id="entry-6-label">Customer name: </label><input id="entry-6" aria-labelledby="entry-6-label">
+ <div id="entry-7" role="textbox"></div>
+ <div id="entry-8" aria-label="Customer name:" role="textbox"></div>
+ <div id="entry-9-label">Customer name: </div><div id="entry-9" aria-labelledby="entry-9-label" role="textbox"></div>
+ <figure id="figure-1">
+ <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" alt="alt text">
+ <figcaption>Figure 1: The four layers of awesome.</figcaption>
+ </figure>
+ <figure id="figure-2">
+ <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" alt="alt text">
+ </figure>
+ <div id="figure-3" role="figure" aria-labelledby="caption-figure-3">
+ <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" alt="alt text">
+ <p id="caption-figure-3">Figure 1: The caption</p>
+ </div>
+ <div id="figure-4" role="figure" aria-labelledby="caption-figure-4">
+ <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" alt="alt text">
+ <p id="caption-figure-4"></p>
+ </div>
+ <div id="figure-5" role="figure">
+ <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" alt="alt text">
+ </div>
+ <img id="img-1" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=">
+ <img id="img-2" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" aria-label="alt text">
+ <p id="img-3-label">Label</p>
+ <img id="img-3" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" aria-labelledby="img-3-label">
+ <img id="img-4" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" alt="alt text">
+ <p id="img-5-label"></p>
+ <img id="img-5" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" aria-labelledby="img-5-label">
+ <div id="img-6" role="img"></div>
+ <div id="img-7" role="img" aria-label="alt text"></div>
+ <p id="img-8-label">Label</p>
+ <div id="img-8" role="img" aria-labelledby="img-8-label"></div>
+ <div id="img-9" role="img" aria-label=""></div>
+ <p id="img-10-label"></p>
+ <div id="img-10" role="img" aria-labelledby="img-10-label"></div>
+ <select>
+ <optgroup id="optgroup-1" label="Group 1">
+ <option>Option 1.1</option>
+ </optgroup>
+ <optgroup id="optgroup-2" label="">
+ <option>Option 2.1</option>
+ </optgroup>
+ <optgroup id="optgroup-3">
+ <option>Option 3.1</option>
+ </optgroup>
+ <optgroup id="optgroup-4" aria-label="Group 4">
+ <option>Option 4.1</option>
+ </optgroup>
+ <optgroup id="optgroup-5" aria-labelledby="optgroup-5-label">
+ <option id="optgroup-5-label">Option 5.1</option>
+ </optgroup>
+ </select>
+ <fieldset id="fieldset-1"><legend>Choose your favorite monster</legend></fieldset>
+ <fieldset id="fieldset-2"><legend></legend></fieldset>
+ <fieldset id="fieldset-3"></fieldset>
+ <fieldset id="fieldset-4" aria-label="Choose your favorite monster"></fieldset>
+ <p id="fieldset-5-label">Choose your favorite monster</p>
+ <fieldset id="fieldset-5" aria-labelledby="fieldset-5-label"></fieldset>
+ <h1 id="heading-1"></h1>
+ <h1 id="heading-2">Heading</h1>
+ <h1 id="heading-3">&nbsp;</h1>
+ <h1 id="heading-4" aria-label="Heading"></h1>
+ <h1 id="heading-5" aria-labelledby="heading-5-label"></h1>
+ <p id="heading-5-label">Heading</p>
+ <h1 id="heading-6" aria-label="Heading">H</h1>
+ <h1 id="heading-7" aria-labelledby="heading-7-label">H</h1>
+ <p id="heading-7-label">Heading</p>
+ <div role="heading" aria-level="1" id="heading-8"></div>
+ <div role="heading" aria-level="1" id="heading-9">Heading</div>
+ <div role="heading" aria-level="1" id="heading-10">&nbsp;</div>
+ <div role="heading" aria-level="1" id="heading-11" aria-label="Heading"></div>
+ <div role="heading" aria-level="1" id="heading-12" aria-labelledby="heading-12-label"></div>
+ <p id="heading-12-label">Heading</p>
+ <div role="heading" aria-level="1" id="heading-13" aria-label="Heading">H</div>
+ <div role="heading" aria-level="1" id="heading-14" aria-labelledby="heading-14-label">H</div>
+ <p id="heading-14-label">Heading</p>
+ <map name="imagemap">
+ <area alt="One" shape="rect" coords="0,0,14,28" href="1.html">
+ <area shape="rect" coords="14,0,28,28" href="2.html">
+ </map>
+ <img id="imagemap-1" usemap="#imagemap" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=">
+ <img id="imagemap-2" usemap="#imagemap" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" aria-label="image map name">
+ <p id="imagemap-3-label">image map name</p>
+ <img id="imagemap-3" usemap="#imagemap" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" aria-labelledby="imagemap-3-label">
+ <img id="imagemap-4" usemap="#imagemap" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" alt="image map name">
+ <p id="imagemap-5-label"></p>
+ <img id="imagemap-5" usemap="#imagemap" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" aria-labelledby="img-5-label">
+ <iframe id="iframe-1" title="IFrame Title" src="https://example.com"></iframe>
+ <iframe id="iframe-2" title="" src="https://example.com"></iframe>
+ <iframe id="iframe-3" src="https://example.com"></iframe>
+ <iframe id="iframe-4" aria-label="Bad Title" src="https://example.com"></iframe>
+ <iframe id="iframe-5" aria-label="Bad Title" title="Good Title" src="https://example.com"></iframe>
+ <object id="object-1" type="image/png" data="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII="></object>
+ <object id="object-2" aria-label="Image object" type="image/png" data="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII="></object>
+ <p id="object-3-label">Image object</p>
+ <object id="object-3" aria-labelledby="object-3-label" type="image/png" data="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII="></object>
+ <object id="object-4" type="text/html" data="https://example.com"></object>
+ <embed id="embed-1" type="image/png" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=">
+ <embed id="embed-2" type="video/webm" src="data:video/webm,xxx">
+ <embed id="embed-3" aria-label="Embedded video" type="video/webm" src="data:video/webm,xxx">
+ <p id="embed-4-label">Embedded video</p>
+ <embed id="embed-4" aria-labelledby="embed-4-label" type="video/webm" src="data:video/webm,xxx">
+ <a id="link-1"></a>
+ <a id="link-2">Hello world</a>
+ <a id="link-3" href></a>
+ <a id="link-4" href>Hello world</a>
+ <a id="link-5" href=""></a>
+ <a id="link-6" href="">Hello world</a>
+ <a id="link-7" href="#"></a>
+ <a id="link-8" href="#">Hello world</a>
+ <a id="link-9" href="https://example.com"></a>
+ <a id="link-10" href="https://example.com">Hello world</a>
+ <a id="link-11" aria-label="Hello world" href="https://example.com"></a>
+ <p id="link-12-label">Hello world</p>
+ <a id="link-12" aria-labelledby="link-12-label" href="https://example.com"></a>
+ <div role="link" id="link-13"></div>
+ <div role="link" id="link-14">Hello world</div>
+ <div role="link" id="link-15" aria-label="Hello world"></div>
+ <p id="link-16-label">Hello world</p>
+ <div role="link" id="link-16" aria-labelledby="link-16-label"></div>
+ <p id="mglyph-3-label">Label</p>
+ <p id="mglyph-6-label"></p>
+ <math>
+ <mi><mglyph id="mglyph-1" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII="/></mi>
+ <mi><mglyph id="mglyph-2" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" aria-label="alt text"/></mi>
+ <mi><mglyph id="mglyph-3" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" aria-labelledby="mglyph-3-label"/></mi>
+ <mi><mglyph id="mglyph-4" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" alt="alt text"/></mi>
+ <mi><mglyph id="mglyph-5" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" alt=""/></mi>
+ <mi><mglyph id="mglyph-6" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" aria-labelledby="mglyph-6-label"/></mi>
+ </math>
+ <span id="menuitem-1" role="menuitem"></span>
+ <span id="menuitem-2" aria-label="" role="menuitem"></span>
+ <span id="menuitem-3" aria-label="Menu Item" role="menuitem"></span>
+ <p id="menuitem-4-label">Menu Item</p>
+ <span id="menuitem-4" aria-labelledby="menuitem-4-label" role="menuitem"></span>
+ <p id="menuitem-5-label"></p>
+ <span id="menuitem-5" aria-labelledby="menuitem-5-label" role="menuitem"></span>
+ <span id="menuitem-6" role="menuitem">Menu Item</span>
+ <label for="listbox-1">Choose a pet:</label>
+ <select id="listbox-1" size="6">
+ <option id="option-1" value="">--Please choose an option--</option>
+ <option id="option-2" value="dog"></option>
+ <option id="option-3" value="cat">&nbsp;</option>
+ <option id="option-4" value="" label="--Please choose an option--"></option>
+ <option id="option-5" value="dog" label=""></option>
+ <option id="option-6" value="cat" label=" "></option>
+ </select>
+ <select id="listbox-2" size="2"></select>
+ <label>Choose a pet:</label><select id="listbox-3" size="2"></select>
+ <label>Choose a pet:<select id="listbox-4" size="2"></select></label>
+ <select id="listbox-5" aria-label="Choose a pet:" size="2"></select>
+ <label id="listbox-6-label">Choose a pet:</label><select id="listbox-6" aria-labelledby="listbox-6-label" size="2"></select>
+ <div role="listbox">
+ <div role="option" id="option-7">--Please choose an option--</div>
+ <div role="option" id="option-8"></div>
+ <div role="option" id="option-9">&nbsp;</div>
+ <div role="option" id="option-10" aria-label="--Please choose an option--"></div>
+ <div role="option" id="option-11" aria-label=""></div>
+ <div role="option" id="option-12" aria-label=" "></div>
+ <p id="option-13-label">--Please choose an option--</p>
+ <div role="option" id="option-13" aria-labelledby="option-13-label"></div>
+ <p id="option-14-label"></p>
+ <div role="option" id="option-14" aria-labelledby="option-14-label"></div>
+ <p id="option-15-label"> </p>
+ <div role="option" id="option-15" aria-labelledby="option-15-label"></div>
+ </div>
+ <span id="treeitem-1" role="treeitem"></span>
+ <span id="treeitem-2" aria-label="" role="treeitem"></span>
+ <span id="treeitem-3" aria-label="Tree Item" role="treeitem"></span>
+ <p id="treeitem-4-label">Tree Item</p>
+ <span id="treeitem-4" aria-labelledby="treeitem-4-label" role="treeitem"></span>
+ <p id="treeitem-5-label"></p>
+ <span id="treeitem-5" aria-labelledby="treeitem-5-label" role="treeitem"></span>
+ <span id="treeitem-6" role="treeitem">Tree Item</span>
+ <div role="tablist">
+ <span id="tab-1" role="tab"></span>
+ <span id="tab-2" aria-label="" role="tab"></span>
+ <span id="tab-3" aria-label="Tab" role="tab"></span>
+ <p id="tab-4-label">Tab</p>
+ <span id="tab-4" aria-labelledby="tab-4-label" role="tab"></span>
+ <p id="tab-5-label"></p>
+ <span id="tab-5" aria-labelledby="tab-5-label" role="tab"></span>
+ <span id="tab-6" role="tab">Tab</span>
+ </div>
+ <label>Password: <input type="password" id="password-1"></label>
+ <input type="password" id="password-2">
+ <input type="password" id="password-3" aria-label="Password:">
+ <label>Password: </label><input type="password" id="password-4">
+ <label for="password-5">Password: </label><input type="password" id="password-5">
+ <label id="password-6-label">Password: </label><input type="password" id="password-6" aria-labelledby="password-6-label">
+ <label>Progress: <progress id="progress-1"></progress></label>
+ <progress id="progress-2"></progress>
+ <progress id="progress-3" aria-label="Progress:"></progress>
+ <label>Progress: </label><progress id="progress-4"></progress>
+ <label for="progress-5">Progress: </label><progress id="progress-5"></progress>
+ <label id="progress-6-label">Progress: </label><progress id="progress-6" aria-labelledby="progress-6-label"></progress>
+ <label>Progress: <div role="progressbar" id="progress-7"></div></label>
+ <label id="progress-8-label">Progress: <div role="progressbar" id="progress-8" aria-labelledby="progress-8-label"></div></label>
+ <div role="progressbar" id="progress-9"></div>
+ <div role="progressbar" id="progress-10" aria-label="Progress:"></div>
+ <label>Progress: </label><div role="progressbar" id="progress-11"></div>
+ <label for="progress-12">Progress: </label><div role="progressbar" id="progress-12"></div>
+ <label id="progress-13-label">Progress: </label><div role="progressbar" id="progress-13" aria-labelledby="progress-13-label"></div>
+ <button id="button-1">hello</button>
+ <button id="button-2"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" /></button>
+ <button id="button-3"></button>
+ <button id="button-4"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" alt="" /></button>
+ <button id="button-5"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" alt="hello" /></button>
+ <button id="button-6">&nbsp;</button>
+ <label>Button: <button id="button-7"></button></label>
+ <button id="button-8" aria-label="Button:"></button>
+ <label>Button: </label><button id="button-9"></button>
+ <label for="button-10">Button: </label><button id="button-10"></button>
+ <label id="button-11-label">Button: </label><button id="button-11" aria-labelledby="button-11-label"></button>
+ <label>Button: <div role="button" id="button-12"></div></label>
+ <label id="button-13-label">Button: <div role="button" id="button-13" aria-labelledby="button-13-label"></div></label>
+ <div role="button" id="button-14"></div>
+ <div role="button" id="button-15" aria-label="Button:"></div>
+ <label>Button: </label><div role="button" id="button-16"></div>
+ <label for="button-17">Button: </label><div role="button" id="button-17"></div>
+ <label id="button-18-label">Button: </label><div role="button" id="button-18" aria-labelledby="button-18-label"></div>
+ <label>Radio label: <input type="radio" id="radiobutton-1"></label>
+ <input type="radio" id="radiobutton-2">
+ <input type="radio" id="radiobutton-3" aria-label="Radio label:">
+ <label>Radio label: </label><input type="radio" id="radiobutton-4">
+ <label for="radiobutton-5">Radio label: </label><input type="radio" id="radiobutton-5">
+ <label id="radiobutton-6-label">Radio label: </label><input type="radio" id="radiobutton-6" aria-labelledby="radiobutton-6-label">
+ <div id="radiobutton-7" role="radio"></div>
+ <div id="radiobutton-8" aria-label="Radio label:" role="radio"></div>
+ <div id="radiobutton-9-label">Radio label: </div><div id="radiobutton-9" aria-labelledby="radiobutton-9-label" role="radio"></div>
+ <div role="menu">
+ <div id="menuitemradio-1" role="menuitemradio">hello</div>
+ <div id="menuitemradio-2" role="menuitemradio"></div>
+ <div id="menuitemradio-3" role="menuitemradio">&nbsp;</div>
+ </div>
+ <div role="rowheader" id="rowheader-8">Toy Story 3</div>
+ <div role="rowheader" id="rowheader-9"></div>
+ <div role="rowheader" id="rowheader-10">&nbsp;</div>
+ <div role="rowheader" id="rowheader-11" aria-label="Alladin"></div>
+ <div role="rowheader" id="rowheader-12" aria-label=""></div>
+ <div role="rowheader" id="rowheader-13" aria-label=" "></div>
+ <div role="rowheader" id="rowheader-14" aria-labelledby="columnheader-7-label"></div>
+ <label>Slider label: <input type="range" id="slider-1"></label>
+ <input type="range" id="slider-2">
+ <input type="range" id="slider-3" aria-label="Slider label:">
+ <label>Slider label: </label><input type="range" id="slider-4">
+ <label for="slider-5">Slider label: </label><input type="range" id="slider-5">
+ <label id="slider-6-label">Slider label: </label><input type="range" id="slider-6" aria-labelledby="slider-6-label">
+ <div id="slider-7" role="slider"></div>
+ <div id="slider-8" aria-label="Slider label:" role="slider"></div>
+ <div id="slider-9-label">Slider label: </div><div id="slider-9" aria-labelledby="slider-9-label" role="slider"></div>
+ <label>Spin button label: <input type="number" id="spinbutton-1"></label>
+ <input type="number" id="spinbutton-2">
+ <input type="number" id="spinbutton-3" aria-label="Spin button label:">
+ <label>Spin button label: </label><input type="number" id="spinbutton-4">
+ <label for="spinbutton-5">Spin button label: </label><input type="number" id="spinbutton-5">
+ <label id="spinbutton-6-label">Spin button label: </label><input type="number" id="spinbutton-6" aria-labelledby="spinbutton-6-label">
+ <div id="spinbutton-7" role="spinbutton"></div>
+ <div id="spinbutton-8" aria-label="Spin button label:" role="spinbutton"></div>
+ <div id="spinbutton-9-label">Spin button label: </div><div id="spinbutton-9" aria-labelledby="spinbutton-9-label" role="spinbutton"></div>
+ <div id="switch-1" role="switch"></div>
+ <div id="switch-2" aria-label="hello" role="switch"></div>
+ <div id="switch-3-label">hello</div><div id="switch-3" aria-labelledby="switch-3-label" role="switch"></div>
+ <label for="switch-4">hello</label><div id="switch-4" role="switch"></div>
+ <label>hello<div id="switch-5" role="switch"></div></label>
+ <label>Meter label: <meter id="meter-1"></meter></label>
+ <meter id="meter-2"></meter>
+ <meter id="meter-3" aria-label="Meter label:"></meter>
+ <label>Meter label: </label><meter id="meter-4"></meter>
+ <label for="meter-5">Meter label: </label><meter id="meter-5"></meter>
+ <label id="meter-6-label">Meter label: </label><meter id="meter-6" aria-labelledby="meter-6-label"></meter>
+ <div id="meter-7" role="meter"></div>
+ <div id="meter-8" aria-label="Meter label:" role="meter"></div>
+ <div id="meter-9-label">Meter label: </div><div id="meter-9" aria-labelledby="meter-9-label" role="meter"></div>
+ <button aria-pressed="true" id="togglebutton-1" >hello</button>
+ <button aria-pressed="true" id="togglebutton-2"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" /></button>
+ <button aria-pressed="true" id="togglebutton-3"></button>
+ <button aria-pressed="true" id="togglebutton-4"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" alt="" /></button>
+ <button aria-pressed="true" id="togglebutton-5"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" alt="hello" /></button>
+ <button aria-pressed="true" id="togglebutton-6">&nbsp;</button>
+ <label>Button: <button aria-pressed="true" id="togglebutton-7"></button></label>
+ <button aria-pressed="true" id="togglebutton-8" aria-label="Button:"></button>
+ <label>Button: </label><button aria-pressed="true" id="togglebutton-9"></button>
+ <label for="togglebutton-10">Button: </label><button aria-pressed="true" id="togglebutton-10"></button>
+ <label id="togglebutton-11-label">Button: </label><button aria-pressed="true" id="togglebutton-11" aria-labelledby="togglebutton-11-label"></button>
+ <label>Button: <div role="button" aria-pressed="true" id="togglebutton-12"></div></label>
+ <label id="togglebutton-13-label">Button: <div role="button" aria-pressed="true" id="togglebutton-13" aria-labelledby="togglebutton-13-label"></div></label>
+ <div role="button" aria-pressed="true" id="togglebutton-14"></div>
+ <div role="button" aria-pressed="true" id="togglebutton-15" aria-label="Button:"></div>
+ <label>Button: </label><div role="button" aria-pressed="true" id="togglebutton-16"></div>
+ <label for="togglebutton-17">Button: </label><div role="button" aria-pressed="true" id="togglebutton-17"></div>
+ <label id="togglebutton-18-label">Button: </label><div role="button" aria-pressed="true" id="togglebutton-18" aria-labelledby="togglebutton-18-label"></div>
+ <span id="toolbar-1" role="toolbar" aria-label="Toolbar"></span>
+ <span id="toolbar-2" role="toolbar"></span>
+ <p id="toolbar-3-label"></p>
+ <span id="toolbar-3" role="toolbar" aria-labelledby="toolbar-3-label"></span>
+ <p id="toolbar-4-label">Toolbar</p>
+ <span id="toolbar-4" role="toolbar" aria-labelledby="toolbar-4-label"></span>
+ <svg id="svg-1" role="img" viewbox="0 0 100 10" height="10px">
+ <title id="siteLogoTitle">Site Logo</title>
+ <rect x="0" y="00" width="100" height="10" fill="red"></rect>
+ </svg>
+ <svg id="svg-2" viewbox="0 0 100 10" height="10px">
+ <title id="siteLogoTitle">Site Logo</title>
+ <rect x="0" y="00" width="100" height="10" fill="red"></rect>
+ </svg>
+ <svg id="svg-3" role="img" viewbox="0 0 100 10" height="10px">
+ <rect x="0" y="00" width="100" height="10" fill="red"></rect>
+ </svg>
+ <svg id="svg-4" viewbox="0 0 100 10" height="10px">
+ <rect x="0" y="00" width="100" height="10" fill="red"></rect>
+ </svg>
+ <svg id="svg-5" aria-label="foo" viewbox="0 0 100 10" height="10px">
+ <rect id="svg-6" aria-label="bar" x="0" y="00" width="100" height="10" fill="red"></rect>
+ </svg>
+ <svg id="svg-7" viewbox="0 0 100 10" height="10px">
+ <title id="siteLogoTitle">Site Logo</title>
+ <rect id="svg-8" aria-label="foo" x="0" y="00" width="100" height="10" fill="red"></rect>
+ </svg>
+ <svg id="svg-9" role="img" viewbox="0 0 100 10" height="10px">
+ <title id="siteLogoTitle">Site Logo</title>
+ <rect aria-label="foo" id="svg-10" x="0" y="00" width="100" height="10" fill="red"></rect>
+ </svg>
+ <svg id="svg-11" role="img" viewbox="0 0 100 10" height="10px">
+ <rect aria-label="foo" id="svg-12" x="0" y="00" width="100" height="10" fill="red"></rect>
+ </svg>
+</body>
+</html>
diff --git a/devtools/server/tests/browser/doc_accessibility_text_label_audit_frame.html b/devtools/server/tests/browser/doc_accessibility_text_label_audit_frame.html
new file mode 100644
index 0000000000..34a32abeb2
--- /dev/null
+++ b/devtools/server/tests/browser/doc_accessibility_text_label_audit_frame.html
@@ -0,0 +1,10 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <frameset cols="50%,50%">
+ <frame id="frame-1" src="https://example.com"></frame>
+ <frame id="frame-2" aria-label="Label" src="https://example.com"></frame>
+ </frameset>
+</html>
diff --git a/devtools/server/tests/browser/doc_allocations.html b/devtools/server/tests/browser/doc_allocations.html
new file mode 100644
index 0000000000..a5c9ea6d41
--- /dev/null
+++ b/devtools/server/tests/browser/doc_allocations.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+</head>
+<body>
+<script>
+"use strict";
+
+window.allocs = [];
+window.onload = function() {
+ function allocator() {
+ for (let i = 0; i < 1000; i++) {
+ window.allocs.push({});
+ }
+ }
+
+ window.setInterval(allocator, 1);
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/server/tests/browser/doc_compatibility.html b/devtools/server/tests/browser/doc_compatibility.html
new file mode 100644
index 0000000000..82cee286b5
--- /dev/null
+++ b/devtools/server/tests/browser/doc_compatibility.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<style>
+ div {
+ color: lime;
+ }
+
+ #id-clip {
+ clip: rect(10px, 10px, 10px, 10px);
+ }
+
+ .class-clip {
+ clip: rect(5px, 5px, 5px, 5px);
+ }
+
+ .class-user-select {
+ -moz-user-select: all;
+ }
+
+ .duplicate {
+ clip: rect(10px, 10px, 10px, 10px);
+ clip: rect(5px, 5px, 5px, 5px);
+ clip: rect(2px, 2px, 2px, 2px);
+ }
+</style>
+<div></div>
+<div class="class-user-select"></div>
+<div id="id-clip" class="class-clip class-user-select"></div>
+<div class="duplicate"></div>
diff --git a/devtools/server/tests/browser/doc_force_cc.html b/devtools/server/tests/browser/doc_force_cc.html
new file mode 100644
index 0000000000..22b1eb4071
--- /dev/null
+++ b/devtools/server/tests/browser/doc_force_cc.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"/>
+ <title>Performance tool + cycle collection test page</title>
+ </head>
+
+ <body>
+ <script type="text/javascript">
+ "use strict";
+
+ /* global test */
+ window.test = function() {
+ document.body.expando1 = { cycle: document.body };
+ SpecialPowers.Cu.forceCC();
+
+ document.body.expando2 = { cycle: document.body };
+ SpecialPowers.Cu.forceCC();
+
+ document.body.expando3 = { cycle: document.body };
+ SpecialPowers.Cu.forceCC();
+
+ setTimeout(window.test, 100);
+ };
+ test();
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/server/tests/browser/doc_force_gc.html b/devtools/server/tests/browser/doc_force_gc.html
new file mode 100644
index 0000000000..7dee110501
--- /dev/null
+++ b/devtools/server/tests/browser/doc_force_gc.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"/>
+ <title>Performance tool + garbage collection test page</title>
+ </head>
+
+ <body>
+ <script type="text/javascript">
+ "use strict";
+
+ var x = 1;
+ /* global test */
+ window.test = function() {
+ SpecialPowers.Cu.forceGC();
+ document.body.style.borderTop = x + "px solid red";
+ x = 1 ^ x;
+ // flush pending reflows
+ document.body.innerHeight;
+
+ // Prevent this script from being garbage collected.
+ setTimeout(window.test, 100);
+ };
+ test();
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/server/tests/browser/doc_iframe.html b/devtools/server/tests/browser/doc_iframe.html
new file mode 100644
index 0000000000..445361f7fa
--- /dev/null
+++ b/devtools/server/tests/browser/doc_iframe.html
@@ -0,0 +1,17 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>iframe test page</title>
+ </head>
+
+ <body>
+ <iframe id="better-not-ask" src="data:text/html,<iframe src='data:text/html,foo'></iframe>"></iframe>
+ <!-- This page is loaded on an example.org subdomain, so we switch to .com -->
+ <iframe id="remote-frame" src="http://example.com/browser/devtools/server/tests/browser/doc_iframe_content.html"></iframe>
+ </body>
+
+</html>
diff --git a/devtools/server/tests/browser/doc_iframe2.html b/devtools/server/tests/browser/doc_iframe2.html
new file mode 100644
index 0000000000..2255490f26
--- /dev/null
+++ b/devtools/server/tests/browser/doc_iframe2.html
@@ -0,0 +1,15 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Sub document page</title>
+ </head>
+
+ <body>
+ Iframe document
+ </body>
+
+</html>
diff --git a/devtools/server/tests/browser/doc_iframe_content.html b/devtools/server/tests/browser/doc_iframe_content.html
new file mode 100644
index 0000000000..6f80e4dd6d
--- /dev/null
+++ b/devtools/server/tests/browser/doc_iframe_content.html
@@ -0,0 +1,14 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Frame for browser_resource_list-remote-frames.js</title>
+ </head>
+
+ <body>
+ <div>Remote frame content</div>
+ </body>
+</html>
diff --git a/devtools/server/tests/browser/doc_innerHTML.html b/devtools/server/tests/browser/doc_innerHTML.html
new file mode 100644
index 0000000000..e58b32f51e
--- /dev/null
+++ b/devtools/server/tests/browser/doc_innerHTML.html
@@ -0,0 +1,21 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Performance tool + innerHTML test page</title>
+ </head>
+
+ <body>
+ <script type="text/javascript">
+ "use strict";
+ window.test = function() {
+ document.body.innerHTML = "<h1>LOL</h1>";
+ };
+ setInterval(window.test, 100);
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/server/tests/browser/error-actor.js b/devtools/server/tests/browser/error-actor.js
new file mode 100644
index 0000000000..3872d8ad96
--- /dev/null
+++ b/devtools/server/tests/browser/error-actor.js
@@ -0,0 +1,25 @@
+/* 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");
+
+/**
+ * Test actor designed to check that clients are properly notified of errors when calling
+ * methods on old style actors.
+ */
+class ErrorActor extends Actor {
+ constructor(conn, tab) {
+ super(conn, { typeName: "error", methods: [] });
+ this.tab = tab;
+ this.requestTypes = {
+ error: this.onError,
+ };
+ }
+ onError() {
+ throw new Error("error");
+ }
+}
+
+exports.ErrorActor = ErrorActor;
diff --git a/devtools/server/tests/browser/grid.html b/devtools/server/tests/browser/grid.html
new file mode 100644
index 0000000000..3bd0e1ec26
--- /dev/null
+++ b/devtools/server/tests/browser/grid.html
@@ -0,0 +1,42 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8"/>
+ <title>Grid test page</title>
+ <style type='text/css'>
+ #grid {
+ display: grid;
+ grid-template-columns: [col-1 col-start-1] 100px [col-2] 100px;
+ grid-template-rows: 100px 100px;
+ grid-template-areas: ". header"
+ "sidebar content";
+ }
+ #cell1 {
+ grid-column: 1;
+ grid-row: 1;
+ }
+ #cell2 {
+ grid-column: 2;
+ grid-row: 1;
+ }
+ #cell3 {
+ grid-column: 1;
+ grid-row: 2;
+ }
+ #cell4 {
+ grid-column: 2;
+ grid-row: 2;
+ }
+ </style>
+</head>
+<body>
+ <div id="grid">
+ <div id="cell1">cell1</div>
+ <div id="cell2">cell2</div>
+ <div id="cell3">cell3</div>
+ <div id="cell4">cell4</div>
+ </div>
+</body>
+</html>
diff --git a/devtools/server/tests/browser/head.js b/devtools/server/tests/browser/head.js
new file mode 100644
index 0000000000..aba6d578f2
--- /dev/null
+++ b/devtools/server/tests/browser/head.js
@@ -0,0 +1,337 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If 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 {
+ ActorRegistry,
+} = require("resource://devtools/server/actors/utils/actor-registry.js");
+const {
+ DevToolsServer,
+} = require("resource://devtools/server/devtools-server.js");
+
+const PATH = "browser/devtools/server/tests/browser/";
+const TEST_DOMAIN = "http://test1.example.org";
+const MAIN_DOMAIN = `${TEST_DOMAIN}/${PATH}`;
+const ALT_DOMAIN = "http://sectest1.example.org/" + PATH;
+const ALT_DOMAIN_SECURED = "https://sectest1.example.org:443/" + PATH;
+
+// GUID to be used as a separator in compound keys. This must match the same
+// constant in devtools/server/actors/resources/storage/index.js,
+// devtools/client/storage/ui.js and devtools/client/storage/test/head.js
+const SEPARATOR_GUID = "{9d414cc5-8319-0a04-0586-c0a6ae01670a}";
+
+// All tests are asynchronous.
+waitForExplicitFinish();
+
+// does almost the same thing as addTab, but directly returns an object
+async function addTabTarget(url) {
+ info(`Adding a new tab with URL: ${url}`);
+ const tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, url));
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ info(`Tab added a URL ${url} loaded`);
+ return createAndAttachTargetForTab(tab);
+}
+
+async function initAnimationsFrontForUrl(url) {
+ const { inspector, walker, target } = await initInspectorFront(url);
+ const animations = await target.getFront("animations");
+
+ return { inspector, walker, animations, target };
+}
+
+async function initLayoutFrontForUrl(url) {
+ const { inspector, walker, target } = await initInspectorFront(url);
+ const layout = await walker.getLayoutInspector();
+
+ return { inspector, walker, layout, target };
+}
+
+async function initAccessibilityFrontsForUrl(
+ url,
+ { enableByDefault = true } = {}
+) {
+ const { inspector, walker, target } = await initInspectorFront(url);
+ const parentAccessibility = await target.client.mainRoot.getFront(
+ "parentaccessibility"
+ );
+ const accessibility = await target.getFront("accessibility");
+ const a11yWalker = accessibility.accessibleWalkerFront;
+ if (enableByDefault) {
+ await parentAccessibility.enable();
+ }
+
+ return {
+ inspector,
+ walker,
+ accessibility,
+ parentAccessibility,
+ a11yWalker,
+ target,
+ };
+}
+
+function initDevToolsServer() {
+ try {
+ // Sometimes devtools server does not get destroyed correctly by previous
+ // tests.
+ DevToolsServer.destroy();
+ } catch (e) {
+ info(`DevToolsServer destroy error: ${e}\n${e.stack}`);
+ }
+ DevToolsServer.init();
+ DevToolsServer.registerAllActors();
+}
+
+async function initPerfFront() {
+ initDevToolsServer();
+ const client = new DevToolsClient(DevToolsServer.connectPipe());
+ await waitUntilClientConnected(client);
+ const front = await client.mainRoot.getFront("perf");
+ return { front, client };
+}
+
+async function initInspectorFront(url) {
+ const target = await addTabTarget(url);
+ const inspector = await target.getFront("inspector");
+ const walker = inspector.walker;
+
+ return { inspector, walker, target };
+}
+
+/**
+ * Wait until a DevToolsClient is connected.
+ * @param {DevToolsClient} client
+ * @return {Promise} Resolves when connected.
+ */
+function waitUntilClientConnected(client) {
+ return client.once("connected");
+}
+
+/**
+ * Wait for eventName on target.
+ * @param {Object} target An observable object that either supports on/off or
+ * addEventListener/removeEventListener
+ * @param {String} eventName
+ * @param {Boolean} useCapture Optional, for addEventListener/removeEventListener
+ * @return A promise that resolves when the event has been handled
+ */
+function once(target, eventName, useCapture = false) {
+ info("Waiting for event: '" + eventName + "' on " + target + ".");
+
+ return new Promise(resolve => {
+ for (const [add, remove] of [
+ ["addEventListener", "removeEventListener"],
+ ["addListener", "removeListener"],
+ ["on", "off"],
+ ]) {
+ if (add in target && remove in target) {
+ target[add](
+ eventName,
+ function onEvent(...aArgs) {
+ info("Got event: '" + eventName + "' on " + target + ".");
+ target[remove](eventName, onEvent, useCapture);
+ resolve(...aArgs);
+ },
+ useCapture
+ );
+ break;
+ }
+ }
+ });
+}
+
+/**
+ * Forces GC, CC and Shrinking GC to get rid of disconnected docshells and
+ * windows.
+ */
+function forceCollections() {
+ Cu.forceGC();
+ Cu.forceCC();
+ Cu.forceShrinkingGC();
+}
+
+registerCleanupFunction(function tearDown() {
+ Services.cookies.removeAll();
+
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeCurrentTab();
+ }
+});
+
+function idleWait(time) {
+ return DevToolsUtils.waitForTime(time);
+}
+
+function busyWait(time) {
+ const start = Date.now();
+ let stack;
+ while (Date.now() - start < time) {
+ stack = Components.stack; // eslint-disable-line no-unused-vars
+ }
+}
+
+/**
+ * Waits until a predicate returns true.
+ *
+ * @param function predicate
+ * Invoked once in a while until it returns true.
+ * @param number interval [optional]
+ * How often the predicate is invoked, in milliseconds.
+ */
+function waitUntil(predicate, interval = 10) {
+ if (predicate()) {
+ return Promise.resolve(true);
+ }
+ return new Promise(resolve => {
+ setTimeout(function () {
+ waitUntil(predicate).then(() => resolve(true));
+ }, interval);
+ });
+}
+
+function waitForMarkerType(
+ front,
+ types,
+ predicate,
+ unpackFun = (name, data) => data.markers,
+ eventName = "timeline-data"
+) {
+ types = [].concat(types);
+ predicate =
+ predicate ||
+ function () {
+ return true;
+ };
+ let filteredMarkers = [];
+
+ return new Promise(resolve => {
+ info("Waiting for markers of type: " + types);
+
+ function handler(name, data) {
+ if (typeof name === "string" && name !== "markers") {
+ return;
+ }
+
+ const markers = unpackFun(name, data);
+ info("Got markers");
+
+ filteredMarkers = filteredMarkers.concat(
+ markers.filter(m => types.includes(m.name))
+ );
+
+ if (
+ types.every(t => filteredMarkers.some(m => m.name === t)) &&
+ predicate(filteredMarkers)
+ ) {
+ front.off(eventName, handler);
+ resolve(filteredMarkers);
+ }
+ }
+ front.on(eventName, handler);
+ });
+}
+
+function getCookieId(name, domain, path) {
+ return `${name}${SEPARATOR_GUID}${domain}${SEPARATOR_GUID}${path}`;
+}
+
+/**
+ * Trigger DOM activity and wait for the corresponding accessibility event.
+ * @param {Object} emitter Devtools event emitter, usually a front.
+ * @param {Sting} name Accessibility event in question.
+ * @param {Function} handler Accessibility event handler function with checks.
+ * @param {Promise} task A promise that resolves when DOM activity is done.
+ */
+async function emitA11yEvent(emitter, name, handler, task) {
+ const promise = emitter.once(name, handler);
+ await task();
+ await promise;
+}
+
+/**
+ * Check that accessibilty front is correct and its attributes are also
+ * up-to-date.
+ * @param {Object} front Accessibility front to be tested.
+ * @param {Object} expected A map of a11y front properties to be verified.
+ * @param {Object} expectedFront Expected accessibility front.
+ */
+function checkA11yFront(front, expected, expectedFront) {
+ ok(front, "The accessibility front is created");
+
+ if (expectedFront) {
+ is(front, expectedFront, "Matching accessibility front");
+ }
+
+ // Clone the front so we could modify some values for comparison.
+ front = Object.assign(front);
+ for (const key in expected) {
+ if (key === "checks") {
+ const { CONTRAST } = front[key];
+ // Contrast values are rounded to two digits after the decimal point.
+ if (CONTRAST && CONTRAST.value) {
+ CONTRAST.value = parseFloat(CONTRAST.value.toFixed(2));
+ }
+ }
+
+ if (["actions", "states", "attributes", "checks"].includes(key)) {
+ SimpleTest.isDeeply(
+ front[key],
+ expected[key],
+ `Accessible Front has correct ${key}`
+ );
+ } else {
+ is(front[key], expected[key], `accessibility front has correct ${key}`);
+ }
+ }
+}
+
+function getA11yInitOrShutdownPromise() {
+ return new Promise(resolve => {
+ const observe = (subject, topic, data) => {
+ Services.obs.removeObserver(observe, "a11y-init-or-shutdown");
+ resolve(data);
+ };
+ Services.obs.addObserver(observe, "a11y-init-or-shutdown");
+ });
+}
+
+/**
+ * Wait for accessibility service to shut down. We consider it shut down when
+ * an "a11y-init-or-shutdown" event is received with a value of "0".
+ */
+async function waitForA11yShutdown(parentAccessibility) {
+ await parentAccessibility.disable();
+ if (!Services.appinfo.accessibilityEnabled) {
+ return;
+ }
+
+ await getA11yInitOrShutdownPromise().then(data =>
+ data === "0" ? Promise.resolve() : Promise.reject()
+ );
+}
+
+/**
+ * Wait for accessibility service to initialize. We consider it initialized when
+ * an "a11y-init-or-shutdown" event is received with a value of "1".
+ */
+async function waitForA11yInit() {
+ if (Services.appinfo.accessibilityEnabled) {
+ return;
+ }
+
+ await getA11yInitOrShutdownPromise().then(data =>
+ data === "1" ? Promise.resolve() : Promise.reject()
+ );
+}
diff --git a/devtools/server/tests/browser/inspector-helpers.js b/devtools/server/tests/browser/inspector-helpers.js
new file mode 100644
index 0000000000..0c05432b98
--- /dev/null
+++ b/devtools/server/tests/browser/inspector-helpers.js
@@ -0,0 +1,161 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* exported assertOwnershipTrees, checkMissing, waitForMutation,
+ isSrcChange, isUnretained, isChildList */
+
+function serverOwnershipTree(walkerArg) {
+ return SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [[walkerArg.actorID]],
+ function (actorID) {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ const {
+ DevToolsServer,
+ } = require("resource://devtools/server/devtools-server.js");
+ const {
+ DocumentWalker,
+ } = require("resource://devtools/server/actors/inspector/document-walker.js");
+
+ // Convert actorID to current compartment string otherwise
+ // searchAllConnectionsForActor is confused and won't find the actor.
+ actorID = String(actorID);
+ const serverWalker = DevToolsServer.searchAllConnectionsForActor(actorID);
+
+ function sortOwnershipChildrenContentScript(children) {
+ return children.sort((a, b) => a.name.localeCompare(b.name));
+ }
+
+ function serverOwnershipSubtree(walker, node) {
+ const actor = walker.getNode(node);
+ if (!actor) {
+ return undefined;
+ }
+
+ const children = [];
+ const docwalker = new DocumentWalker(node, content);
+ let child = docwalker.firstChild();
+ while (child) {
+ const item = serverOwnershipSubtree(walker, child);
+ if (item) {
+ children.push(item);
+ }
+ child = docwalker.nextSibling();
+ }
+ return {
+ name: actor.actorID,
+ children: sortOwnershipChildrenContentScript(children),
+ };
+ }
+ return {
+ root: serverOwnershipSubtree(serverWalker, serverWalker.rootDoc),
+ orphaned: [...serverWalker._orphaned].map(o =>
+ serverOwnershipSubtree(serverWalker, o.rawNode)
+ ),
+ retained: [...serverWalker._retainedOrphans].map(o =>
+ serverOwnershipSubtree(serverWalker, o.rawNode)
+ ),
+ };
+ }
+ );
+}
+
+function sortOwnershipChildren(children) {
+ return children.sort((a, b) => a.name.localeCompare(b.name));
+}
+
+function clientOwnershipSubtree(node) {
+ return {
+ name: node.actorID,
+ children: sortOwnershipChildren(
+ node.treeChildren().map(child => clientOwnershipSubtree(child))
+ ),
+ };
+}
+
+function clientOwnershipTree(walker) {
+ return {
+ root: clientOwnershipSubtree(walker.rootNode),
+ orphaned: [...walker._orphaned].map(o => clientOwnershipSubtree(o)),
+ retained: [...walker._retainedOrphans].map(o => clientOwnershipSubtree(o)),
+ };
+}
+
+function ownershipTreeSize(tree) {
+ let size = 1;
+ for (const child of tree.children) {
+ size += ownershipTreeSize(child);
+ }
+ return size;
+}
+
+async function assertOwnershipTrees(walker) {
+ const serverTree = await serverOwnershipTree(walker);
+ const clientTree = clientOwnershipTree(walker);
+ is(
+ JSON.stringify(clientTree, null, " "),
+ JSON.stringify(serverTree, null, " "),
+ "Server and client ownership trees should match."
+ );
+
+ return ownershipTreeSize(clientTree.root);
+}
+
+// Verify that an actorID is inaccessible both from the client library and the server.
+async function checkMissing({ client }, actorID) {
+ const front = client.getFrontByID(actorID);
+ ok(
+ !front,
+ "Front shouldn't be accessible from the client for actorID: " + actorID
+ );
+
+ try {
+ await client.request({
+ to: actorID,
+ type: "request",
+ });
+ ok(false, "The actor wasn't missing as the request worked");
+ } catch (e) {
+ is(
+ e.error,
+ "noSuchActor",
+ "node list actor should no longer be contactable."
+ );
+ }
+}
+
+// Load mutations aren't predictable, so keep accumulating mutations until
+// the one we're looking for shows up.
+function waitForMutation(walker, test, mutations = []) {
+ return new Promise(resolve => {
+ for (const change of mutations) {
+ if (test(change)) {
+ resolve(mutations);
+ }
+ }
+
+ walker.once("mutations", newMutations => {
+ waitForMutation(walker, test, mutations.concat(newMutations)).then(
+ finalMutations => {
+ resolve(finalMutations);
+ }
+ );
+ });
+ });
+}
+
+function isSrcChange(change) {
+ return change.type === "attributes" && change.attributeName === "src";
+}
+
+function isUnretained(change) {
+ return change.type === "unretained";
+}
+
+function isChildList(change) {
+ return change.type === "childList";
+}
diff --git a/devtools/server/tests/browser/inspector-isScrollable-data.html b/devtools/server/tests/browser/inspector-isScrollable-data.html
new file mode 100644
index 0000000000..07caabd894
--- /dev/null
+++ b/devtools/server/tests/browser/inspector-isScrollable-data.html
@@ -0,0 +1,79 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Inspector test of isScrollable</title>
+ <style>
+ /* "e" is our custom tag name for "element" */
+ e {
+ background: lightgray;
+ display: inline-block;
+ margin: 10px;
+ padding: 0;
+ border: 0;
+ width: 100px;
+ height: 100px;
+ overflow: auto;
+ }
+
+ /* "c" is our custom tag name for "child" */
+ c {
+ display: block;
+ background: green;
+ }
+
+ .fixedSize {
+ width: 10px;
+ height: 10px;
+ }
+
+ .target {
+ background: red;
+ }
+ </style>
+</head>
+<body id="body">
+<e id="no_children"></e>
+
+<e id="one_child_no_overflow">
+ <c></c>
+</e>
+
+<e id="margin_left_overflow">
+ <c class="target" style="margin-left:100px">abcd</c>
+</e>
+
+<e id="transform_overflow">
+ <c class="target" style="transform: translate(50px)">abcd</c>
+</e>
+
+<e id="nested_overflow">
+ <c>
+ <c class="target" style="margin-left:100px">abcd</c>
+ </c>
+</e>
+
+<e id="intermediate_overflow">
+ <c class="fixedSize target" style="margin-left:100px">
+ <c></c>
+ </c>
+</e>
+
+<e id="multiple_overflow_at_different_depths">
+ <c class="fixedSize target" style="margin-left:100px">
+ <c></c>
+ </c>
+ <c style="margin-left:100px">
+ <c class="target">abcd</c>
+ </c>
+</e>
+
+<e id="overflow_hidden" style="overflow:hidden">
+ <c class="target" style="margin-left:100px">abcd</c>
+</e>
+
+<e id="scrollbar_none" style="scrollbar-width:none">
+ <c class="target" style="margin-left:100px">abcd</c>
+</e>
+</body>
+</html>
diff --git a/devtools/server/tests/browser/inspector-search-data.html b/devtools/server/tests/browser/inspector-search-data.html
new file mode 100644
index 0000000000..784dcb7c9b
--- /dev/null
+++ b/devtools/server/tests/browser/inspector-search-data.html
@@ -0,0 +1,54 @@
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>Inspector Search Test Data</title>
+ <style>
+ #pseudo {
+ display: block;
+ margin: 0;
+ }
+ #pseudo:before {
+ content: "before element";
+ }
+ #pseudo:after {
+ content: "after element";
+ }
+ </style>
+ <script type="text/javascript">
+ "use strict";
+
+ window.onload = function() {
+ window.opener.postMessage("ready", "*");
+ };
+ </script>
+</head>
+</body>
+ <!-- A comment
+ spread across multiple lines -->
+
+ <img width="100" height="100" src="large-image.jpg" />
+
+ <h1 id="pseudo">Heading 1</h1>
+ <p>A p tag with the text 'h1' inside of it.
+ <strong>A strong h1 result</strong>
+ </p>
+
+ <div id="arrows" northwest="↖" northeast="↗" southeast="↘" southwest="↙">
+ Unicode arrows
+ </div>
+
+ <h2>Heading 2</h2>
+ <h2>Heading 2</h2>
+ <h2>Heading 2</h2>
+
+ <h3>Heading 3</h3>
+ <h3>Heading 3</h3>
+ <h3>Heading 3</h3>
+
+ <h4>Heading 4</h4>
+ <h4>Heading 4</h4>
+ <h4>Heading 4</h4>
+
+ <div class="💩" id="💩" 💩="💩"></div>
+</body>
+</html>
diff --git a/devtools/server/tests/browser/inspector-shadow.html b/devtools/server/tests/browser/inspector-shadow.html
new file mode 100644
index 0000000000..eb600548e2
--- /dev/null
+++ b/devtools/server/tests/browser/inspector-shadow.html
@@ -0,0 +1,117 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Inspector (empty page)</title>
+ <script>
+ "use strict";
+
+ window.onload = function() {
+ customElements.define("test-empty", class extends HTMLElement {
+ constructor() {
+ super();
+ this.attachShadow({mode: "open"});
+ }
+ });
+
+ customElements.define("test-empty-closed", class extends HTMLElement {
+ constructor() {
+ super();
+ this.attachShadow({mode: "closed"});
+ }
+ });
+
+ customElements.define("test-children", class extends HTMLElement {
+ constructor() {
+ super();
+ this.attachShadow({mode: "open"});
+ this.shadowRoot.innerHTML = `
+ <h1>One child</h1>
+ <p>A second child</p>`;
+ }
+ });
+
+ customElements.define("test-named-slot", class extends HTMLElement {
+ constructor() {
+ super();
+ this.attachShadow({mode: "open"});
+ this.shadowRoot.innerHTML = `
+ <h1>With slot</h1>
+ <slot name="slot1"></slot>`;
+ }
+ });
+
+ customElements.define("test-slot", class extends HTMLElement {
+ constructor() {
+ super();
+ this.attachShadow({mode: "open"});
+ this.shadowRoot.innerHTML = `
+ <style>
+ slot::before { content: "[SLOT BEFORE]"; color: red; }
+ slot::after { content: "[SLOT AFTER]"; color: blue; }
+ </style>
+ <slot></slot>`;
+ }
+ });
+
+ customElements.define("test-simple-slot", class extends HTMLElement {
+ constructor() {
+ super();
+ this.attachShadow({ mode: "open"});
+ this.shadowRoot.innerHTML = "<slot></slot>";
+ }
+ });
+ };
+ </script>
+ <style>
+ #host-pseudo::before { content: "[HOST BEFORE]"; color: red; }
+ #host-pseudo::after { content: "[HOST AFTER]"; color: blue; }
+ </style>
+</head>
+<body>
+ <test-empty id="empty"></test-empty>
+
+ <hr>
+
+ <test-empty id="one-child">
+ <h1>One child</h1>
+ </test-empty>
+
+ <hr>
+
+ <test-children id="shadow-children"></test-children>
+
+ <hr>
+
+ <test-named-slot id="named-slot">
+ <p class="slotted" slot="slot1">Slotted</p>
+ </test-named-slot>
+
+ <hr>
+
+ <test-slot id="slot-pseudo">
+ <span class="has-before">Slotted</span>
+ </test-slot>
+
+ <hr>
+
+ <test-empty id="host-pseudo"></test-empty>
+
+ <hr>
+
+ <test-empty id="mode-open"></test-empty>
+ <test-empty-closed id="mode-closed"></test-empty-closed>
+
+ <hr>
+
+ <test-simple-slot id="slot-inline-text">
+ Lorem ipsum
+ </test-simple-slot>
+
+ <hr>
+ <video id="video-controls" controls></video>
+ <video id="video-controls-with-children" controls>
+ <div>some content</div>
+ </video>
+</body>
+</html>
diff --git a/devtools/server/tests/browser/inspector-traversal-data.html b/devtools/server/tests/browser/inspector-traversal-data.html
new file mode 100644
index 0000000000..6f025747ec
--- /dev/null
+++ b/devtools/server/tests/browser/inspector-traversal-data.html
@@ -0,0 +1,98 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>Inspector Traversal Test Data</title>
+ <style type="text/css">
+ #pseudo::before {
+ content: "before";
+ }
+ #pseudo::after {
+ content: "after";
+ }
+ #pseudo-empty::before {
+ content: "before an empty element";
+ }
+ #shadow::before {
+ content: "Testing ::before on a shadow host";
+ }
+ </style>
+ <script type="text/javascript">
+ "use strict";
+
+ window.onload = function() {
+ // Set up a basic shadow DOM
+ const host = document.querySelector("#shadow");
+ const root = host.attachShadow({ mode: "open" });
+
+ const h3 = document.createElement("h3");
+ h3.append("Shadow ");
+
+ const em = document.createElement("em");
+ em.append("DOM");
+
+ const select = document.createElement("select");
+ select.setAttribute("multiple", "");
+ h3.appendChild(em);
+ root.appendChild(h3);
+ root.appendChild(select);
+
+ // Put a copy of the body in an iframe to test frame traversal.
+ const body = document.querySelector("body");
+ const data = "data:text/html,<html>" + body.outerHTML + "<html>";
+ const iframe = document.createElement("iframe");
+ iframe.setAttribute("id", "childFrame");
+ iframe.src = data;
+ body.appendChild(iframe);
+ };
+ </script>
+</head>
+<body style="background-color:white">
+ <h1>Inspector Actor Tests</h1>
+ <span id="longstring">longlonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglong</span>
+ <span id="shortstring">short</span>
+ <span id="empty"></span>
+ <div id="longlist" data-test="exists">
+ <div id="a">a</div>
+ <div id="b">b</div>
+ <div id="c">c</div>
+ <div id="d">d</div>
+ <div id="e">e</div>
+ <div id="f">f</div>
+ <div id="g">g</div>
+ <div id="h">h</div>
+ <div id="i">i</div>
+ <div id="j">j</div>
+ <div id="k">k</div>
+ <div id="l">l</div>
+ <div id="m">m</div>
+ <div id="n">n</div>
+ <div id="o">o</div>
+ <div id="p">p</div>
+ <div id="q">q</div>
+ <div id="r">r</div>
+ <div id="s">s</div>
+ <div id="t">t</div>
+ <div id="u">u</div>
+ <div id="v">v</div>
+ <div id="w">w</div>
+ <div id="x">x</div>
+ <div id="y">y</div>
+ <div id="z">z</div>
+ </div>
+ <div id="longlist-sibling">
+ <div id="longlist-sibling-firstchild"></div>
+ </div>
+ <p id="edit-html"></p>
+
+ <select multiple><option>one</option><option>two</option></select>
+ <div id="pseudo"><span>middle</span></div>
+ <div id="pseudo-empty"></div>
+ <div id="shadow">light dom</div>
+ <object>
+ <div id="1"></div>
+ </object>
+ <div class="node-to-duplicate"></div>
+ <div id="scroll-into-view" style="margin-top: 1000px;">scroll</div>
+</body>
+</html>
diff --git a/devtools/server/tests/browser/storage-cookies-same-name.html b/devtools/server/tests/browser/storage-cookies-same-name.html
new file mode 100644
index 0000000000..235c8a451f
--- /dev/null
+++ b/devtools/server/tests/browser/storage-cookies-same-name.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Storage inspector cookies with duplicate names</title>
+</head>
+<body onload="createCookies()">
+<script type="application/javascript">
+"use strict";
+// eslint-disable-next-line no-unused-vars
+function createCookies() {
+ document.cookie = "name=value1;path=/;";
+ document.cookie = "name=value2;path=/path2/;";
+ document.cookie = "name=value3;path=/path3/;";
+}
+
+window.removeCookie = function (name) {
+ document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT";
+};
+
+window.clearCookies = function () {
+ const cookies = document.cookie;
+ for (const cookie of cookies.split(";")) {
+ window.removeCookie(cookie.split("=")[0]);
+ }
+};
+</script>
+</body>
+</html>
diff --git a/devtools/server/tests/browser/storage-dynamic-windows.html b/devtools/server/tests/browser/storage-dynamic-windows.html
new file mode 100644
index 0000000000..22df8a255e
--- /dev/null
+++ b/devtools/server/tests/browser/storage-dynamic-windows.html
@@ -0,0 +1,117 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 965872 - Storage inspector actor with cookies, local storage and session storage.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Storage inspector test for listing hosts and storages</title>
+</head>
+<body>
+<iframe src="http://sectest1.example.org/browser/devtools/server/tests/browser/storage-unsecured-iframe.html"></iframe>
+<script type="application/javascript">
+"use strict";
+const partialHostname = location.hostname.match(/^[^.]+(\..*)$/)[1];
+const cookieExpiresTime1 = 2000000000000;
+const cookieExpiresTime2 = 2000000001000;
+// Setting up some cookies to eat.
+document.cookie = "c1=foobar; expires=" +
+ new Date(cookieExpiresTime1).toGMTString() + "; path=/browser";
+document.cookie = "cs2=sessionCookie; path=/; domain=" + partialHostname;
+document.cookie = "c3=foobar-2; expires=" +
+ new Date(cookieExpiresTime2).toGMTString() + "; path=/";
+// ... and some local storage items ..
+localStorage.setItem("ls1", "foobar");
+localStorage.setItem("ls2", "foobar-2");
+// ... and finally some session storage items too
+sessionStorage.setItem("ss1", "foobar-3");
+
+const idbGenerator = async function () {
+ let request = indexedDB.open("idb1", 1);
+ request.onerror = function() {
+ throw new Error("error opening db connection");
+ };
+ const db = await new Promise(done => {
+ request.onupgradeneeded = event => {
+ const dbResult = event.target.result;
+ const store1 = dbResult.createObjectStore("obj1", { keyPath: "id" });
+ store1.createIndex("name", "name", { unique: false });
+ store1.createIndex("email", "email", { unique: true });
+ dbResult.createObjectStore("obj2", { keyPath: "id2" });
+ store1.transaction.oncomplete = () => {
+ done(dbResult);
+ };
+ };
+ });
+
+ // Prevents AbortError
+ await new Promise(done => {
+ request.onsuccess = done;
+ });
+
+ const transaction = db.transaction(["obj1", "obj2"], "readwrite");
+ const store1 = transaction.objectStore("obj1");
+ const store2 = transaction.objectStore("obj2");
+ store1.add({id: 1, name: "foo", email: "foo@bar.com"});
+ store1.add({id: 2, name: "foo2", email: "foo2@bar.com"});
+ store1.add({id: 3, name: "foo2", email: "foo3@bar.com"});
+ store2.add({
+ id2: 1,
+ name: "foo",
+ email: "foo@bar.com",
+ extra: "baz"
+ });
+ // Prevents AbortError during close()
+ await new Promise(success => {
+ transaction.oncomplete = success;
+ });
+
+ db.close();
+
+ request = indexedDB.open("idb2", 1);
+ const db2 = await new Promise(done => {
+ request.onupgradeneeded = event => {
+ const db2Result = event.target.result;
+ const store3 = db2Result.createObjectStore("obj3", { keyPath: "id3" });
+ store3.createIndex("name2", "name2", { unique: true });
+ store3.transaction.oncomplete = () => {
+ done(db2Result);
+ }
+ };
+ });
+ // Prevents AbortError during close()
+ await new Promise(done => {
+ request.onsuccess = done;
+ });
+ db2.close();
+
+ console.log("added cookies and stuff from main page");
+};
+
+function deleteDB(dbName) {
+ return new Promise(resolve => {
+ dump("removing database " + dbName + " from " + document.location + "\n");
+ indexedDB.deleteDatabase(dbName).onsuccess = resolve;
+ });
+}
+
+window.setup = async function () {
+ await idbGenerator();
+};
+
+window.clear = async function () {
+ document.cookie = "c1=; expires=Thu, 01 Jan 1970 00:00:00 GMT";
+ document.cookie = "c3=; expires=Thu, 01 Jan 1970 00:00:00 GMT";
+ document.cookie = "cs2=; expires=Thu, 01 Jan 1970 00:00:00 GMT";
+
+ localStorage.clear();
+
+ await deleteDB("idb1");
+ await deleteDB("idb2");
+
+ dump("removed cookies, localStorage and indexedDB data from " +
+ document.location + "\n");
+};
+</script>
+</body>
+</html>
diff --git a/devtools/server/tests/browser/storage-helpers.js b/devtools/server/tests/browser/storage-helpers.js
new file mode 100644
index 0000000000..1315c77b31
--- /dev/null
+++ b/devtools/server/tests/browser/storage-helpers.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This file assumes head.js is loaded in the global scope.
+/* import-globals-from head.js */
+
+/* exported openTabAndSetupStorage, clearStorage */
+
+"use strict";
+
+/**
+ * This generator function opens the given url in a new tab, then sets up the
+ * page by waiting for all cookies, indexedDB items etc. to be created.
+ *
+ * @param url {String} The url to be opened in the new tab
+ *
+ * @return {Promise} A promise that resolves after storage inspector is ready
+ */
+async function openTabAndSetupStorage(url) {
+ await addTab(url);
+
+ // Setup the async storages in main window and for all its iframes
+ const browsingContexts =
+ gBrowser.selectedBrowser.browsingContext.getAllBrowsingContextsInSubtree();
+ for (const browsingContext of browsingContexts) {
+ await SpecialPowers.spawn(browsingContext, [], async function () {
+ if (content.wrappedJSObject.setup) {
+ await content.wrappedJSObject.setup();
+ }
+ });
+ }
+
+ // selected tab is set in addTab
+ const commands = await CommandsFactory.forTab(gBrowser.selectedTab);
+ await commands.targetCommand.startListening();
+ const target = commands.targetCommand.targetFront;
+ return { commands, target };
+}
+
+async function clearStorage() {
+ const browsingContexts =
+ gBrowser.selectedBrowser.browsingContext.getAllBrowsingContextsInSubtree();
+ for (const browsingContext of browsingContexts) {
+ await SpecialPowers.spawn(browsingContext, [], async function () {
+ if (content.wrappedJSObject.clear) {
+ await content.wrappedJSObject.clear();
+ }
+ });
+ }
+}
diff --git a/devtools/server/tests/browser/storage-listings.html b/devtools/server/tests/browser/storage-listings.html
new file mode 100644
index 0000000000..98ac182bd0
--- /dev/null
+++ b/devtools/server/tests/browser/storage-listings.html
@@ -0,0 +1,123 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 965872 - Storage inspector actor with cookies, local storage and session storage.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Storage inspector test for listing hosts and storages</title>
+</head>
+<body>
+<iframe src="http://sectest1.example.org/browser/devtools/server/tests/browser/storage-unsecured-iframe.html"></iframe>
+<iframe src="https://sectest1.example.org:443/browser/devtools/server/tests/browser/storage-secured-iframe.html"></iframe>
+<script type="application/javascript">
+"use strict";
+const partialHostname = location.hostname.match(/^[^.]+(\..*)$/)[1];
+const cookieExpiresTime1 = 2000000000000;
+const cookieExpiresTime2 = 2000000001000;
+// Setting up some cookies to eat.
+document.cookie = "c1=foobar; expires=" +
+ new Date(cookieExpiresTime1).toGMTString() + "; path=/browser";
+document.cookie = "cs2=sessionCookie; path=/; domain=" + partialHostname;
+document.cookie = "c3=foobar-2; secure=true; expires=" +
+ new Date(cookieExpiresTime2).toGMTString() + "; path=/";
+// ... and some local storage items ..
+localStorage.setItem("ls1", "foobar");
+localStorage.setItem("ls2", "foobar-2");
+// ... and finally some session storage items too
+sessionStorage.setItem("ss1", "foobar-3");
+console.log("added cookies and stuff from main page");
+
+const idbGenerator = async function () {
+ let request = indexedDB.open("idb1", 1);
+ request.onerror = function() {
+ throw new Error("error opening db connection");
+ };
+ const db = await new Promise(done => {
+ request.onupgradeneeded = event => {
+ const dbResult = event.target.result;
+ const store1 = dbResult.createObjectStore("obj1", { keyPath: "id" });
+ store1.createIndex("name", "name", { unique: false });
+ store1.createIndex("email", "email", { unique: true });
+ dbResult.createObjectStore("obj2", { keyPath: "id2" });
+ store1.transaction.oncomplete = () => {
+ done(dbResult);
+ };
+ };
+ });
+
+ // Prevents AbortError
+ await new Promise(done => {
+ request.onsuccess = done;
+ });
+
+ const transaction = db.transaction(["obj1", "obj2"], "readwrite");
+ const store1 = transaction.objectStore("obj1");
+ const store2 = transaction.objectStore("obj2");
+ store1.add({id: 1, name: "foo", email: "foo@bar.com"});
+ store1.add({id: 2, name: "foo2", email: "foo2@bar.com"});
+ store1.add({id: 3, name: "foo2", email: "foo3@bar.com"});
+ store2.add({
+ id2: 1,
+ name: "foo",
+ email: "foo@bar.com",
+ extra: "baz"
+ });
+ // Prevents AbortError during close()
+ await new Promise(success => {
+ transaction.oncomplete = success;
+ });
+
+ db.close();
+
+ request = indexedDB.open("idb2", 1);
+ const db2 = await new Promise(done => {
+ request.onupgradeneeded = event => {
+ const db2Result = event.target.result;
+ const store3 = db2Result.createObjectStore("obj3", { keyPath: "id3" });
+ store3.createIndex("name2", "name2", { unique: true });
+ store3.transaction.oncomplete = () => {
+ done(db2Result);
+ }
+ };
+ });
+ // Prevents AbortError during close()
+ await new Promise(done => {
+ request.onsuccess = done;
+ });
+ db2.close();
+
+ dump("added cookies and stuff from main page\n");
+};
+
+function deleteDB(dbName) {
+ return new Promise(resolve => {
+ dump("removing database " + dbName + " from " + document.location + "\n");
+ indexedDB.deleteDatabase(dbName).onsuccess = resolve;
+ });
+}
+
+window.setup = async function () {
+ await idbGenerator();
+};
+
+window.clear = async function () {
+ document.cookie = "c1=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/browser";
+ document.cookie =
+ "c3=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; secure=true";
+ document.cookie =
+ "cs2=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=" +
+ partialHostname;
+
+ localStorage.clear();
+ sessionStorage.clear();
+
+ await deleteDB("idb1");
+ await deleteDB("idb2");
+
+ dump("removed cookies, localStorage, sessionStorage and indexedDB data " +
+ "from " + document.location + "\n");
+};
+</script>
+</body>
+</html>
diff --git a/devtools/server/tests/browser/storage-secured-iframe.html b/devtools/server/tests/browser/storage-secured-iframe.html
new file mode 100644
index 0000000000..c2fe4ed485
--- /dev/null
+++ b/devtools/server/tests/browser/storage-secured-iframe.html
@@ -0,0 +1,94 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Iframe for testing multiple host detetion in storage actor
+-->
+<head>
+ <meta charset="utf-8">
+</head>
+<body>
+<script type="application/javascript">
+"use strict";
+document.cookie = "sc1=foobar;";
+localStorage.setItem("iframe-s-ls1", "foobar");
+sessionStorage.setItem("iframe-s-ss1", "foobar-2");
+
+const idbGenerator = async function () {
+ let request = indexedDB.open("idb-s1", 1);
+ request.onerror = function() {
+ throw new Error("error opening db connection");
+ };
+ const db = await new Promise(done => {
+ request.onupgradeneeded = event => {
+ const dbResult = event.target.result;
+ const store1 = dbResult.createObjectStore("obj-s1", { keyPath: "id" });
+ store1.transaction.oncomplete = () => {
+ done(dbResult);
+ };
+ };
+ });
+ await new Promise(done => {
+ request.onsuccess = done;
+ });
+
+ let transaction = db.transaction(["obj-s1"], "readwrite");
+ const store1 = transaction.objectStore("obj-s1");
+ store1.add({id: 6, name: "foo", email: "foo@bar.com"});
+ store1.add({id: 7, name: "foo2", email: "foo2@bar.com"});
+ await new Promise(success => {
+ transaction.oncomplete = success;
+ });
+
+ db.close();
+
+ request = indexedDB.open("idb-s2", 1);
+ const db2 = await new Promise(done => {
+ request.onupgradeneeded = event => {
+ const db2Result = event.target.result;
+ const store3 =
+ db2Result.createObjectStore("obj-s2", { keyPath: "id3", autoIncrement: true });
+ store3.createIndex("name2", "name2", { unique: true });
+ store3.transaction.oncomplete = () => {
+ done(db2Result);
+ };
+ };
+ });
+ await new Promise(done => {
+ request.onsuccess = done;
+ });
+
+ transaction = db2.transaction(["obj-s2"], "readwrite");
+ const store3 = transaction.objectStore("obj-s2");
+ store3.add({id3: 16, name2: "foo", email: "foo@bar.com"});
+ await new Promise(success => {
+ transaction.oncomplete = success;
+ });
+
+ db2.close();
+ dump("added cookies and stuff from secured iframe\n");
+}
+
+function deleteDB(dbName) {
+ return new Promise(resolve => {
+ dump("removing database " + dbName + " from " + document.location + "\n");
+ indexedDB.deleteDatabase(dbName).onsuccess = resolve;
+ });
+}
+
+window.setup = async function () {
+ await idbGenerator();
+};
+
+window.clear = async function () {
+ document.cookie = "sc1=; expires=Thu, 01 Jan 1970 00:00:00 GMT";
+
+ localStorage.clear();
+
+ await deleteDB("idb-s1");
+ await deleteDB("idb-s2");
+
+ console.log("removed cookies and stuff from secured iframe");
+}
+</script>
+</body>
+</html>
diff --git a/devtools/server/tests/browser/storage-unsecured-iframe.html b/devtools/server/tests/browser/storage-unsecured-iframe.html
new file mode 100644
index 0000000000..db70c9c692
--- /dev/null
+++ b/devtools/server/tests/browser/storage-unsecured-iframe.html
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Iframe for testing multiple host detetion in storage actor
+-->
+<head>
+ <meta charset="utf-8">
+</head>
+<body>
+<script>
+"use strict";
+
+document.cookie = "uc1=foobar; domain=.example.org; path=/; secure=true";
+localStorage.setItem("iframe-u-ls1", "foobar");
+sessionStorage.setItem("iframe-u-ss1", "foobar1");
+sessionStorage.setItem("iframe-u-ss2", "foobar2");
+console.log("added cookies and stuff from unsecured iframe");
+
+window.clear = function () {
+ document.cookie = "uc1=; expires=Thu, 01 Jan 1970 00:00:00 GMT";
+ localStorage.clear();
+ sessionStorage.clear();
+ console.log("removed cookies and stuff from unsecured iframe");
+};
+
+</script>
+</body>
+</html>
diff --git a/devtools/server/tests/browser/storage-updates.html b/devtools/server/tests/browser/storage-updates.html
new file mode 100644
index 0000000000..594c28ce0f
--- /dev/null
+++ b/devtools/server/tests/browser/storage-updates.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 965872 - Storage inspector actor with cookies, local storage and session storage.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Storage inspector blank html for tests</title>
+</head>
+<body>
+<script type="application/javascript">
+"use strict";
+window.addCookie = function(name, value, path, domain, expires, secure) {
+ let cookieString = name + "=" + value + ";";
+ if (path) {
+ cookieString += "path=" + path + ";";
+ }
+ if (domain) {
+ cookieString += "domain=" + domain + ";";
+ }
+ if (expires) {
+ cookieString += "expires=" + expires + ";";
+ }
+ if (secure) {
+ cookieString += "secure=true;";
+ }
+ document.cookie = cookieString;
+};
+
+window.removeCookie = function(name) {
+ document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT";
+};
+
+window.clearLocalAndSessionStores = function() {
+ localStorage.clear();
+ sessionStorage.clear();
+};
+
+window.clearCookies = function() {
+ const cookies = document.cookie;
+ for (const cookie of cookies.split(";")) {
+ window.removeCookie(cookie.split("=")[0]);
+ }
+};
+</script>
+</body>
+</html>
diff --git a/devtools/server/tests/browser/test-errors-actor.js b/devtools/server/tests/browser/test-errors-actor.js
new file mode 100644
index 0000000000..e476324be4
--- /dev/null
+++ b/devtools/server/tests/browser/test-errors-actor.js
@@ -0,0 +1,72 @@
+/* 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 testErrorsSpec = protocol.generateActorSpec({
+ typeName: "testErrors",
+
+ methods: {
+ throwsComponentsException: {
+ request: {},
+ response: {},
+ },
+ throwsException: {
+ request: {},
+ response: {},
+ },
+ throwsJSError: {
+ request: {},
+ response: {},
+ },
+ throwsString: {
+ request: {},
+ response: {},
+ },
+ throwsObject: {
+ request: {},
+ response: {},
+ },
+ },
+});
+
+class TestErrorsActor extends protocol.Actor {
+ constructor(conn) {
+ super(conn, testErrorsSpec);
+ }
+
+ throwsComponentsException() {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ }
+
+ throwsException() {
+ return this.a.b.c;
+ }
+
+ throwsJSError() {
+ throw new Error("JSError");
+ }
+
+ throwsString() {
+ // eslint-disable-next-line no-throw-literal
+ throw "ErrorString";
+ }
+
+ throwsObject() {
+ // eslint-disable-next-line no-throw-literal
+ throw {
+ error: "foo",
+ };
+ }
+}
+exports.TestErrorsActor = TestErrorsActor;
+
+class TestErrorsFront extends protocol.FrontClassWithSpec(testErrorsSpec) {
+ constructor(client) {
+ super(client);
+ this.formAttributeName = "testErrorsActor";
+ }
+}
+protocol.registerFront(TestErrorsFront);
diff --git a/devtools/server/tests/browser/test-window.xhtml b/devtools/server/tests/browser/test-window.xhtml
new file mode 100644
index 0000000000..33e70e2dee
--- /dev/null
+++ b/devtools/server/tests/browser/test-window.xhtml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<xul:window xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Test page">
+</xul:window>