diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /devtools/server/tests | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/server/tests')
445 files changed, 41065 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"> </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"> </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"> </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"> </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"> </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"> </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"> </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"> </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"> </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"> </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"> </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"> </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"> </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> diff --git a/devtools/server/tests/chrome/Debugger.Source.prototype.element-2.js b/devtools/server/tests/chrome/Debugger.Source.prototype.element-2.js new file mode 100644 index 0000000000..7260431428 --- /dev/null +++ b/devtools/server/tests/chrome/Debugger.Source.prototype.element-2.js @@ -0,0 +1,4 @@ +"use strict"; + +// eslint-disable-next-line no-debugger +debugger; diff --git a/devtools/server/tests/chrome/Debugger.Source.prototype.element.html b/devtools/server/tests/chrome/Debugger.Source.prototype.element.html new file mode 100644 index 0000000000..6959ad970d --- /dev/null +++ b/devtools/server/tests/chrome/Debugger.Source.prototype.element.html @@ -0,0 +1,25 @@ +<head> + <!-- Static (not dynamically inserted) inline script. --> + <script id='franz'> + /* exported franz */ + "use strict"; + + function franz() { + // eslint-disable-next-line no-debugger + debugger; + } + </script> + + <!-- Static out-of-line script element. --> + <script id='heinrich' src='Debugger.Source.prototype.element.js'></script> +</head> + +<!-- HTML requires some body element onfoo attributes to add handlers to the + *window*, not the element --- but Debugger.Source.prototype.element should + return the element. Here, that rule should apply to the body's 'onresize' + handler. (For the reason for the 'cancelable' check, see the code that + sends the event.) --> +<body onresize='if (event.cancelable) debugger;'> + <!-- Ordinary content element with event handler. --> + <div id='heidi' onclick='heinrichFun();'>Heidi</div> +</body> diff --git a/devtools/server/tests/chrome/Debugger.Source.prototype.element.js b/devtools/server/tests/chrome/Debugger.Source.prototype.element.js new file mode 100644 index 0000000000..095398ddad --- /dev/null +++ b/devtools/server/tests/chrome/Debugger.Source.prototype.element.js @@ -0,0 +1,7 @@ +/* exported heinrichFun */ +/* global franz */ +"use strict"; + +function heinrichFun() { + franz(); +} diff --git a/devtools/server/tests/chrome/chrome.toml b/devtools/server/tests/chrome/chrome.toml new file mode 100644 index 0000000000..b67b1ee971 --- /dev/null +++ b/devtools/server/tests/chrome/chrome.toml @@ -0,0 +1,150 @@ +[DEFAULT] +tags = "devtools" +skip-if = ["os == 'android'"] +support-files = [ + "doc_Debugger.Source.prototype.introductionType.xhtml", + "Debugger.Source.prototype.element.js", + "Debugger.Source.prototype.element-2.js", + "Debugger.Source.prototype.element.html", + "hello-actor.js", + "iframe1_makeGlobalObjectReference.html", + "iframe2_makeGlobalObjectReference.html", + "inspector_css-properties.html", + "inspector_display-type.html", + "inspector_getImageData.html", + "inspector_getOffsetParent.html", + "inspector-delay-image-response.sjs", + "inspector-eyedropper.html", + "inspector-helpers.js", + "inspector-search-data.html", + "inspector-styles-data.css", + "inspector-styles-data.html", + "inspector-template.html", + "inspector-traversal-data.html", + "large-image.jpg", + "memory-helpers.js", + "nonchrome_unsafeDereference.html", + "suspendTimeouts_content.html", + "suspendTimeouts_content.js", + "suspendTimeouts_worker.js", + "small-image.gif", + "test_suspendTimeouts.js", + "webconsole-helpers.js", + "inactive-property-helper/*.mjs", +] + +["test_Debugger.Script.prototype.global.html"] + +["test_Debugger.Source.prototype.elementAttribute.html"] + +["test_Debugger.Source.prototype.introductionScript.html"] + +["test_Debugger.Source.prototype.introductionType.html"] + +["test_animation-type-longhand.html"] + +["test_css-logic-specificity.html"] + +["test_css-logic.html"] + +["test_css-properties.html"] + +["test_device.html"] + +["test_executeInGlobal-outerized_this.html"] + +["test_highlighter_paused_debugger.html"] + +["test_inspector-changeattrs.html"] + +["test_inspector-changevalue.html"] + +["test_inspector-display-type.html"] + +["test_inspector-duplicate-node.html"] + +["test_inspector-hide.html"] + +["test_inspector-inactive-property-helper.html"] + +["test_inspector-mutations-attr.html"] + +["test_inspector-mutations-events.html"] + +["test_inspector-mutations-value.html"] + +["test_inspector-pick-color.html"] + +["test_inspector-pseudoclass-lock.html"] + +["test_inspector-reload.html"] + +["test_inspector-resize.html"] + +["test_inspector-resolve-url.html"] + +["test_inspector-scroll-into-view.html"] + +["test_inspector-search-front.html"] + +["test_inspector-template.html"] + +["test_inspector_getImageData-wait-for-load.html"] + +["test_inspector_getImageData.html"] + +["test_inspector_getImageDataFromURL.html"] + +["test_inspector_getNodeFromActor.html"] + +["test_inspector_getOffsetParent.html"] + +["test_makeGlobalObjectReference.html"] + +["test_memory.html"] + +["test_memory_allocations_02.html"] + +["test_memory_allocations_03.html"] + +["test_memory_allocations_04.html"] + +["test_memory_allocations_05.html"] + +["test_memory_allocations_06.html"] + +["test_memory_allocations_07.html"] + +["test_memory_attach_01.html"] + +["test_memory_attach_02.html"] + +["test_memory_census.html"] + +["test_memory_gc_01.html"] + +["test_memory_gc_events.html"] + +["test_overflowing-body.html"] + +["test_overflowing-children.html"] + +["test_preference.html"] + +["test_styles-applied.html"] + +["test_styles-computed.html"] + +["test_styles-layout.html"] + +["test_styles-matched.html"] + +["test_styles-modify.html"] + +["test_styles-svg.html"] + +["test_suspendTimeouts.html"] + +["test_unsafeDereference.html"] + +["test_webconsole-node-grip.html"] diff --git a/devtools/server/tests/chrome/doc_Debugger.Source.prototype.introductionType.xhtml b/devtools/server/tests/chrome/doc_Debugger.Source.prototype.introductionType.xhtml new file mode 100644 index 0000000000..b037190c9a --- /dev/null +++ b/devtools/server/tests/chrome/doc_Debugger.Source.prototype.introductionType.xhtml @@ -0,0 +1,7 @@ +<?xml version="1.0"?> +<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'> +<script id='xulie'> +/* eslint-disable strict, no-unused-vars, no-debugger */ +function xulScriptFunc() { debugger; } +</script> +</window> diff --git a/devtools/server/tests/chrome/hello-actor.js b/devtools/server/tests/chrome/hello-actor.js new file mode 100644 index 0000000000..eabb4a6773 --- /dev/null +++ b/devtools/server/tests/chrome/hello-actor.js @@ -0,0 +1,28 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* exported HelloActor */ +"use strict"; + +const protocol = require("resource://devtools/shared/protocol.js"); + +const helloSpec = protocol.generateActorSpec({ + typeName: "helloActor", + + methods: { + count: { + request: {}, + response: { count: protocol.RetVal("number") }, + }, + }, +}); + +class HelloActor extends protocol.Actor { + constructor(conn) { + super(conn, helloSpec); + this.counter = 0; + } + + count() { + return ++this.counter; + } +} diff --git a/devtools/server/tests/chrome/iframe1_makeGlobalObjectReference.html b/devtools/server/tests/chrome/iframe1_makeGlobalObjectReference.html new file mode 100644 index 0000000000..bab5a70765 --- /dev/null +++ b/devtools/server/tests/chrome/iframe1_makeGlobalObjectReference.html @@ -0,0 +1 @@ +<html>The word 'smorgasbord' spoken by an adorably plump child, symbolizing prosperity</html> diff --git a/devtools/server/tests/chrome/iframe2_makeGlobalObjectReference.html b/devtools/server/tests/chrome/iframe2_makeGlobalObjectReference.html new file mode 100644 index 0000000000..b297ca8a2b --- /dev/null +++ b/devtools/server/tests/chrome/iframe2_makeGlobalObjectReference.html @@ -0,0 +1 @@ +<html>Her retrospection, in hindsight, was prescient.</html> diff --git a/devtools/server/tests/chrome/inactive-property-helper/align-content.mjs b/devtools/server/tests/chrome/inactive-property-helper/align-content.mjs new file mode 100644 index 0000000000..a871081fad --- /dev/null +++ b/devtools/server/tests/chrome/inactive-property-helper/align-content.mjs @@ -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/. */ + +// InactivePropertyHelper `align-content` test cases. + +export default [ + { + info: "align-content is inactive on block elements (until bug 1105571 is fixed)", + property: "align-content", + tagName: "div", + rules: ["div { align-content: center; }"], + isActive: false, + }, + { + info: "align-content is active on flex containers", + property: "align-content", + tagName: "div", + rules: ["div { align-content: center; display: flex; }"], + isActive: true, + }, + { + info: "align-content is active on grid containers", + property: "align-content", + tagName: "div", + rules: ["div { align-content: center; display: grid; }"], + isActive: true, + }, + { + info: "align-content is inactive on flex items", + property: "align-content", + createTestElement: rootNode => { + const container = document.createElement("div"); + const element = document.createElement("span"); + container.append(element); + rootNode.append(container); + return element; + }, + rules: ["div { display: flex; }", "span { align-content: center; }"], + ruleIndex: 1, + isActive: false, + }, + { + info: "align-content is inactive on grid items", + property: "align-content", + createTestElement: rootNode => { + const container = document.createElement("div"); + const element = document.createElement("span"); + container.append(element); + rootNode.append(container); + return element; + }, + rules: ["div { display: grid; }", "span { align-content: center; }"], + ruleIndex: 1, + isActive: false, + }, + { + info: "align-content:baseline is active on flex items", + property: "align-content", + createTestElement: rootNode => { + const container = document.createElement("div"); + const element = document.createElement("span"); + container.append(element); + rootNode.append(container); + return element; + }, + rules: ["div { display: flex; }", "span { align-content: baseline; }"], + ruleIndex: 1, + isActive: true, + }, + { + info: "align-content:baseline is active on grid items", + property: "align-content", + createTestElement: rootNode => { + const container = document.createElement("div"); + const element = document.createElement("span"); + container.append(element); + rootNode.append(container); + return element; + }, + rules: ["div { display: grid; }", "span { align-content: baseline; }"], + ruleIndex: 1, + isActive: true, + }, + { + info: "align-content:baseline is active on table cells", + property: "align-content", + tagName: "div", + rules: ["div { display: table-cell; align-content: baseline; }"], + isActive: true, + }, +]; diff --git a/devtools/server/tests/chrome/inactive-property-helper/border-image.mjs b/devtools/server/tests/chrome/inactive-property-helper/border-image.mjs new file mode 100644 index 0000000000..85c57418a4 --- /dev/null +++ b/devtools/server/tests/chrome/inactive-property-helper/border-image.mjs @@ -0,0 +1,162 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// InactivePropertyHelper `border-image` test cases. +export default [ + { + info: "border-image is active on another element then a table element or internal table element where border-collapse is not set to collapse", + property: "border-image", + tagName: "div", + rules: ["div { border-image: linear-gradient(red, yellow) 10; }"], + isActive: true, + }, + { + info: "border-image is active on another element then a table element or internal table element where border-collapse is set to collapse", + property: "border-image", + tagName: "div", + rules: [ + "div { border-image: linear-gradient(red, yellow) 10; border-collapse: collapse;}", + ], + isActive: true, + }, + { + info: "border-image is active on a td element with no table parent and the browser is not crashing", + property: "border-image", + tagName: "td", + rules: [ + "td { border-image: linear-gradient(red, yellow) 10; border-collapse: collapse;}", + ], + isActive: true, + }, + createTableElementsToTestBorderImage({ + useDivTagWithDisplayTableStyle: false, + borderCollapse: true, + borderCollapsePropertyIsInherited: false, + isActive: true, + }), + createTableElementsToTestBorderImage({ + useDivTagWithDisplayTableStyle: false, + borderCollapse: false, + borderCollapsePropertyIsInherited: false, + isActive: true, + }), + createTableElementsToTestBorderImage({ + useDivTagWithDisplayTableStyle: false, + borderCollapse: true, + borderCollapsePropertyIsInherited: true, + isActive: false, + }), + createTableElementsToTestBorderImage({ + useDivTagWithDisplayTableStyle: false, + borderCollapse: false, + borderCollapsePropertyIsInherited: true, + isActive: true, + }), + createTableElementsToTestBorderImage({ + useDivTagWithDisplayTableStyle: true, + borderCollapse: true, + borderCollapsePropertyIsInherited: false, + isActive: true, + }), + createTableElementsToTestBorderImage({ + useDivTagWithDisplayTableStyle: true, + borderCollapse: false, + borderCollapsePropertyIsInherited: false, + isActive: true, + }), + createTableElementsToTestBorderImage({ + useDivTagWithDisplayTableStyle: true, + borderCollapse: true, + borderCollapsePropertyIsInherited: true, + isActive: false, + }), + createTableElementsToTestBorderImage({ + useDivTagWithDisplayTableStyle: true, + borderCollapse: false, + borderCollapsePropertyIsInherited: true, + isActive: true, + }), +]; + +/** + * @param {Object} testParameters + * @param {bool} testParameters.useDivTagWithDisplayTableStyle use generic divs using display property instead of actual table/tr/td tags + * @param {bool} testParameters.borderCollapse is `border-collapse` property set to `collapse` ( instead of `separate`) + * @param {bool} testParameters.borderCollapsePropertyIsInherited should the border collapse property be inherited from the table parent (instead of directly set on the internal table element) + * @param {bool} testParameters.isActive is the border-image property actve on the element + * @returns + */ +function createTableElementsToTestBorderImage({ + useDivTagWithDisplayTableStyle, + borderCollapse, + borderCollapsePropertyIsInherited, + isActive, +}) { + return { + info: `border-image is ${ + isActive ? "active" : "inactive" + } on an internal table element where border-collapse is${ + borderCollapse ? "" : " not" + } set to collapse${ + borderCollapsePropertyIsInherited + ? " by being inherited from its table parent" + : "" + } when the table and its internal elements are ${ + useDivTagWithDisplayTableStyle ? "not " : "" + }using semantic tags (table, tr, td, ...)`, + property: "border-image", + createTestElement: rootNode => { + const table = useDivTagWithDisplayTableStyle + ? document.createElement("div") + : document.createElement("table"); + if (useDivTagWithDisplayTableStyle) { + table.style.display = "table"; + } + if (borderCollapsePropertyIsInherited) { + table.style.borderCollapse = `${ + borderCollapse ? "collapse" : "separate" + }`; + } + rootNode.appendChild(table); + + const tbody = useDivTagWithDisplayTableStyle + ? document.createElement("div") + : document.createElement("tbody"); + if (useDivTagWithDisplayTableStyle) { + tbody.style.display = "table-row-group"; + } + table.appendChild(tbody); + + const tr = useDivTagWithDisplayTableStyle + ? document.createElement("div") + : document.createElement("tr"); + if (useDivTagWithDisplayTableStyle) { + tr.style.display = "table-row"; + } + tbody.appendChild(tr); + + const td = useDivTagWithDisplayTableStyle + ? document.createElement("div") + : document.createElement("td"); + if (useDivTagWithDisplayTableStyle) { + td.style.display = "table-cell"; + td.classList.add("td"); + } + tr.appendChild(td); + + return td; + }, + rules: [ + `td, .td { + border-image: linear-gradient(red, yellow) 10; + ${ + !borderCollapsePropertyIsInherited + ? `border-collapse: ${borderCollapse ? "collapse" : "separate"};` + : "" + } + }`, + ], + isActive, + }; +} diff --git a/devtools/server/tests/chrome/inactive-property-helper/cue-pseudo-element.mjs b/devtools/server/tests/chrome/inactive-property-helper/cue-pseudo-element.mjs new file mode 100644 index 0000000000..7a55425632 --- /dev/null +++ b/devtools/server/tests/chrome/inactive-property-helper/cue-pseudo-element.mjs @@ -0,0 +1,371 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// InactivePropertyHelper `cue-pseudo-element` test cases. + +// "background", +// "background-attachment", +// "background-blend-mode", +// "background-clip", +// "background-color", +// "background-image", +// "background-origin", +// "background-position", +// "background-position-x", +// "background-position-y", +// "background-repeat", +// "background-size", +// "color", +// "font", +// "font-family", +// "font-size", +// "font-stretch", +// "font-style", +// "font-variant", +// "font-variant-alternates", +// "font-variant-caps", +// "font-variant-east-asian", +// "font-variant-ligatures", +// "font-variant-numeric", +// "font-variant-position", +// "font-weight", +// "line-height", +// "opacity", +// "outline", +// "outline-color", +// "outline-offset", +// "outline-style", +// "outline-width", +// "ruby-position", +// "text-combine-upright", +// "text-decoration", +// "text-decoration-color", +// "text-decoration-line", +// "text-decoration-style", +// "text-decoration-thickness", +// "text-shadow", +// "visibility", +// "white-space", + +export default [ + { + info: "background is active on ::cue", + property: "background", + tagName: "video", + rules: ["video::cue { background: linear-gradient(white, black); }"], + isActive: true, + }, + { + info: "background-attachment is active on ::cue", + property: "background-attachment", + tagName: "video", + rules: ["video::cue { background-attachment: fixed; }"], + isActive: true, + }, + { + info: "background-blend-mode is active on ::cue", + property: "background-blend-mode", + tagName: "video", + rules: ["video::cue { background-blend-mode: difference; }"], + isActive: true, + }, + { + info: "background-clip is active on ::cue", + property: "background-clip", + tagName: "video", + rules: ["video::cue { background-clip: padding-box; }"], + isActive: true, + }, + { + info: "background-color is active on ::cue", + property: "background-color", + tagName: "video", + rules: ["video::cue { background-color: red; }"], + isActive: true, + }, + { + info: "background-image is active on ::cue", + property: "background-image", + tagName: "video", + rules: [ + "video::cue { background-image: url('https://example.com/image.png'); }", + ], + isActive: true, + }, + { + info: "background-origin is active on ::cue", + property: "background-origin", + tagName: "video", + rules: ["video::cue { background-origin: padding-box; }"], + isActive: true, + }, + { + info: "background-position is active on ::cue", + property: "background-position", + tagName: "video", + rules: ["video::cue { background-position: 0 0; }"], + isActive: true, + }, + { + info: "background-position-x is active on ::cue", + property: "background-position-x", + tagName: "video", + rules: ["video::cue { background-position-x: 0; }"], + isActive: true, + }, + { + info: "background-position-y is active on ::cue", + property: "background-position-y", + tagName: "video", + rules: ["video::cue { background-position-y: 0; }"], + isActive: true, + }, + { + info: "background-repeat is active on ::cue", + property: "background-repeat", + tagName: "video", + rules: ["video::cue { background-repeat: repeat-y; }"], + isActive: true, + }, + { + info: "background-size is active on ::cue", + property: "background-size", + tagName: "video", + rules: ["video::cue { background-size: 100% 100%; }"], + isActive: true, + }, + { + info: "color is active on ::cue", + property: "color", + tagName: "video", + rules: ["video::cue { color: red; }"], + isActive: true, + }, + { + info: "font is active on ::cue", + property: "font", + tagName: "video", + rules: ["video::cue { font: 1em sans-serif; }"], + isActive: true, + }, + { + info: "font-family is active on ::cue", + property: "font-family", + tagName: "video", + rules: ["video::cue { font-family: sans-serif; }"], + isActive: true, + }, + { + info: "font-size is active on ::cue", + property: "font-size", + tagName: "video", + rules: ["video::cue { font-size: 1em; }"], + isActive: true, + }, + { + info: "font-stretch is active on ::cue", + property: "font-stretch", + tagName: "video", + rules: ["video::cue { font-stretch: ultra-condensed; }"], + isActive: true, + }, + { + info: "font-style is active on ::cue", + property: "font-style", + tagName: "video", + rules: ["video::cue { font-style: italic; }"], + isActive: true, + }, + { + info: "font-variant is active on ::cue", + property: "font-variant", + tagName: "video", + rules: ["video::cue { font-variant: small-caps; }"], + isActive: true, + }, + { + info: "font-variant-alternates is active on ::cue", + property: "font-variant-alternates", + tagName: "video", + rules: ["video::cue { font-variant-alternates: slashed-zero; }"], + isActive: true, + }, + { + info: "font-variant-caps is active on ::cue", + property: "font-variant-caps", + tagName: "video", + rules: ["video::cue { font-variant-caps: all-small-caps; }"], + isActive: true, + }, + { + info: "font-variant-east-asian is active on ::cue", + property: "font-variant-east-asian", + tagName: "video", + rules: ["video::cue { font-variant-east-asian: ruby; }"], + isActive: true, + }, + { + info: "font-variant-ligatures is active on ::cue", + property: "font-variant-ligatures", + tagName: "video", + rules: ["video::cue { font-variant-ligatures: common-ligatures; }"], + isActive: true, + }, + { + info: "font-variant-numeric is active on ::cue", + property: "font-variant-numeric", + tagName: "video", + rules: ["video::cue { font-variant-numeric: ordinal; }"], + isActive: true, + }, + { + info: "font-variant-position is active on ::cue", + property: "font-variant-position", + tagName: "video", + rules: ["video::cue { font-variant-position: sub; }"], + isActive: true, + }, + { + info: "font-weight is active on ::cue", + property: "font-weight", + tagName: "video", + rules: ["video::cue { font-weight: bold; }"], + isActive: true, + }, + { + info: "line-height is active on ::cue", + property: "line-height", + tagName: "video", + rules: ["video::cue { line-height: 1.2; }"], + isActive: true, + }, + { + info: "opacity is active on ::cue", + property: "opacity", + tagName: "video", + rules: ["video::cue { opacity: 0.8; }"], + isActive: true, + }, + { + info: "outline is active on ::cue", + property: "outline", + tagName: "video", + rules: ["video::cue { outline: 1px solid red; }"], + isActive: true, + }, + { + info: "outline-color is active on ::cue", + property: "outline-color", + tagName: "video", + rules: ["video::cue { outline-color: red; }"], + isActive: true, + }, + { + info: "outline-offset is active on ::cue", + property: "outline-offset", + tagName: "video", + rules: ["video::cue { outline-offset: 1px; }"], + isActive: true, + }, + { + info: "outline-style is active on ::cue", + property: "outline-style", + tagName: "video", + rules: ["video::cue { outline-style: solid; }"], + isActive: true, + }, + { + info: "outline-width is active on ::cue", + property: "outline-width", + tagName: "video", + rules: ["video::cue { outline-width: 1px; }"], + isActive: true, + }, + { + info: "ruby-position is active on ::cue", + property: "ruby-position", + tagName: "video", + rules: ["video::cue { ruby-position: over; }"], + isActive: true, + }, + { + info: "text-combine-upright is active on ::cue", + property: "text-combine-upright", + tagName: "video", + rules: ["video::cue { text-combine-upright: all; }"], + isActive: true, + }, + { + info: "text-decoration is active on ::cue", + property: "text-decoration", + tagName: "video", + rules: ["video::cue { text-decoration: 1px underline red; }"], + isActive: true, + }, + { + info: "text-decoration-color is active on ::cue", + property: "text-decoration-color", + tagName: "video", + rules: ["video::cue { text-decoration-color: red; }"], + isActive: true, + }, + { + info: "text-decoration-line is active on ::cue", + property: "text-decoration-line", + tagName: "video", + rules: ["video::cue { text-decoration-line: underline; }"], + isActive: true, + }, + { + info: "text-decoration-style is active on ::cue", + property: "text-decoration-style", + tagName: "video", + rules: ["video::cue { text-decoration-style: wavy; }"], + isActive: true, + }, + { + info: "text-decoration-thickness is active on ::cue", + property: "text-decoration-thickness", + tagName: "video", + rules: ["video::cue { text-decoration-thickness: 1px; }"], + isActive: true, + }, + { + info: "text-shadow is active on ::cue", + property: "text-shadow", + tagName: "video", + rules: ["video::cue { text-shadow: 1px 1px 1px red; }"], + isActive: true, + }, + { + info: "visibility is active on ::cue", + property: "visibility", + tagName: "video", + rules: ["video::cue { visibility: hidden; }"], + isActive: true, + }, + { + info: "white-space is active on ::cue", + property: "white-space", + tagName: "video", + rules: ["video::cue { white-space: nowrap; }"], + isActive: true, + }, + { + info: "border is inactive on ::cue", + property: "border", + tagName: "video", + rules: ["video::cue { border: 1px solid red; }"], + isActive: false, + expectedMsgId: "inactive-css-cue-pseudo-element-not-supported", + }, + { + info: "display is inactive on ::cue", + property: "display", + tagName: "video", + rules: ["video::cue { display: grid; }"], + isActive: false, + expectedMsgId: "inactive-css-cue-pseudo-element-not-supported", + }, +]; diff --git a/devtools/server/tests/chrome/inactive-property-helper/first-letter-pseudo-element.mjs b/devtools/server/tests/chrome/inactive-property-helper/first-letter-pseudo-element.mjs new file mode 100644 index 0000000000..ebce0d292a --- /dev/null +++ b/devtools/server/tests/chrome/inactive-property-helper/first-letter-pseudo-element.mjs @@ -0,0 +1,32 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// InactivePropertyHelper `first-letter-pseudo-element` test cases. + +// "content", + +export default [ + { + info: "content is inactive on ::first-letter", + property: "content", + tagName: "div", + rules: ["div::first-letter { content: 'invalid'; }"], + isActive: false, + expectedMsgId: "inactive-css-first-letter-pseudo-element-not-supported", + }, + { + info: "color is active on ::first-letter", + property: "color", + tagName: "div", + rules: ["div::first-letter { color: green; }"], + isActive: true, + }, + { + info: "display is active on ::first-letter", + property: "display", + tagName: "div", + rules: ["div::first-letter { display: grid; }"], + isActive: true, + }, +]; diff --git a/devtools/server/tests/chrome/inactive-property-helper/first-line-pseudo-element.mjs b/devtools/server/tests/chrome/inactive-property-helper/first-line-pseudo-element.mjs new file mode 100644 index 0000000000..68948a16bc --- /dev/null +++ b/devtools/server/tests/chrome/inactive-property-helper/first-line-pseudo-element.mjs @@ -0,0 +1,50 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// InactivePropertyHelper `first-line-pseudo-element` test cases. + +// "direction", +// "text-orientation", +// "writing-mode", + +export default [ + { + info: "direction is inactive on ::first-line", + property: "direction", + tagName: "div", + rules: ["div::first-line { direction: rtl; }"], + isActive: false, + expectedMsgId: "inactive-css-first-line-pseudo-element-not-supported", + }, + { + info: "text-orientation is inactive on ::first-line", + property: "text-orientation", + tagName: "div", + rules: ["div::first-line { text-orientation: sideways; }"], + isActive: false, + expectedMsgId: "inactive-css-first-line-pseudo-element-not-supported", + }, + { + info: "writing-mode is inactive on ::first-line", + property: "writing-mode", + tagName: "div", + rules: ["div::first-line { writing-mode: vertical-rl; }"], + isActive: false, + expectedMsgId: "inactive-css-first-line-pseudo-element-not-supported", + }, + { + info: "color is active on ::first-line", + property: "color", + tagName: "div", + rules: ["div::first-line { color: green; }"], + isActive: true, + }, + { + info: "display is active on ::first-line", + property: "display", + tagName: "div", + rules: ["div::first-line { display: grid; }"], + isActive: true, + }, +]; diff --git a/devtools/server/tests/chrome/inactive-property-helper/flex-grid-item-properties.mjs b/devtools/server/tests/chrome/inactive-property-helper/flex-grid-item-properties.mjs new file mode 100644 index 0000000000..79c587798a --- /dev/null +++ b/devtools/server/tests/chrome/inactive-property-helper/flex-grid-item-properties.mjs @@ -0,0 +1,229 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// InactivePropertyHelper `align-self`, `place-self`, and `order` test cases. +export default [ + { + info: "align-self is inactive on block element", + property: "align-self", + tagName: "div", + rules: ["div { align-self: center; }"], + isActive: false, + }, + { + info: "align-self is inactive on flex container", + property: "align-self", + tagName: "div", + rules: ["div { align-self: center; display: flex;}"], + isActive: false, + }, + { + info: "align-self is inactive on inline-flex container", + property: "align-self", + tagName: "div", + rules: ["div { align-self: center; display: inline-flex;}"], + isActive: false, + }, + { + info: "align-self is inactive on grid container", + property: "align-self", + tagName: "div", + rules: ["div { align-self: center; display: grid;}"], + isActive: false, + }, + { + info: "align-self is inactive on inline grid container", + property: "align-self", + tagName: "div", + rules: ["div { align-self: center; display: inline-grid;}"], + isActive: false, + }, + { + info: "align-self is inactive on inline element", + property: "align-self", + tagName: "span", + rules: ["span { align-self: center; }"], + isActive: false, + }, + { + info: "align-self is active on flex item", + property: "align-self", + createTestElement: rootNode => { + const container = document.createElement("div"); + const element = document.createElement("span"); + container.append(element); + rootNode.append(container); + return element; + }, + rules: [ + "div { display: flex; align-items: start; }", + "span { align-self: center; }", + ], + ruleIndex: 1, + isActive: true, + }, + { + info: "align-self is active on grid item", + property: "align-self", + createTestElement: rootNode => { + const container = document.createElement("div"); + const element = document.createElement("span"); + container.append(element); + rootNode.append(container); + return element; + }, + rules: [ + "div { display: grid; align-items: start; }", + "span { align-self: center; }", + ], + ruleIndex: 1, + isActive: true, + }, + { + info: "place-self is inactive on block element", + property: "place-self", + tagName: "div", + rules: ["div { place-self: center; }"], + isActive: false, + }, + { + info: "place-self is inactive on flex container", + property: "place-self", + tagName: "div", + rules: ["div { place-self: center; display: flex;}"], + isActive: false, + }, + { + info: "place-self is inactive on inline-flex container", + property: "place-self", + tagName: "div", + rules: ["div { place-self: center; display: inline-flex;}"], + isActive: false, + }, + { + info: "place-self is inactive on grid container", + property: "place-self", + tagName: "div", + rules: ["div { place-self: center; display: grid;}"], + isActive: false, + }, + { + info: "place-self is inactive on inline grid container", + property: "place-self", + tagName: "div", + rules: ["div { place-self: center; display: inline-grid;}"], + isActive: false, + }, + { + info: "place-self is inactive on inline element", + property: "place-self", + tagName: "span", + rules: ["span { place-self: center; }"], + isActive: false, + }, + { + info: "place-self is active on flex item", + property: "place-self", + createTestElement: rootNode => { + const container = document.createElement("div"); + const element = document.createElement("span"); + container.append(element); + rootNode.append(container); + return element; + }, + rules: [ + "div { display: flex; align-items: start; }", + "span { place-self: center; }", + ], + ruleIndex: 1, + isActive: true, + }, + { + info: "place-self is active on grid item", + property: "place-self", + createTestElement: rootNode => { + const container = document.createElement("div"); + const element = document.createElement("span"); + container.append(element); + rootNode.append(container); + return element; + }, + rules: [ + "div { display: grid; align-items: start; }", + "span { place-self: center; }", + ], + ruleIndex: 1, + isActive: true, + }, + { + info: "order is inactive on block element", + property: "order", + tagName: "div", + rules: ["div { order: 1; }"], + isActive: false, + }, + { + info: "order is inactive on flex container", + property: "order", + tagName: "div", + rules: ["div { order: 1; display: flex;}"], + isActive: false, + }, + { + info: "order is inactive on inline-flex container", + property: "order", + tagName: "div", + rules: ["div { order: 1; display: inline-flex;}"], + isActive: false, + }, + { + info: "order is inactive on grid container", + property: "order", + tagName: "div", + rules: ["div { order: 1; display: grid;}"], + isActive: false, + }, + { + info: "order is inactive on inline grid container", + property: "order", + tagName: "div", + rules: ["div { order: 1; display: inline-grid;}"], + isActive: false, + }, + { + info: "order is inactive on inline element", + property: "order", + tagName: "span", + rules: ["span { order: 1; }"], + isActive: false, + }, + { + info: "order is active on flex item", + property: "order", + createTestElement: rootNode => { + const container = document.createElement("div"); + const element = document.createElement("span"); + container.append(element); + rootNode.append(container); + return element; + }, + rules: ["div { display: flex; }", "span { order: 1; }"], + ruleIndex: 1, + isActive: true, + }, + { + info: "order is active on grid item", + property: "order", + createTestElement: rootNode => { + const container = document.createElement("div"); + const element = document.createElement("span"); + container.append(element); + rootNode.append(container); + return element; + }, + rules: ["div { display: grid; }", "span { order: 1; }"], + ruleIndex: 1, + isActive: true, + }, +]; diff --git a/devtools/server/tests/chrome/inactive-property-helper/float.mjs b/devtools/server/tests/chrome/inactive-property-helper/float.mjs new file mode 100644 index 0000000000..4c502e3cca --- /dev/null +++ b/devtools/server/tests/chrome/inactive-property-helper/float.mjs @@ -0,0 +1,76 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// InactivePropertyHelper `float` test cases. +export default [ + { + info: "display: inline is inactive on a floated element", + property: "display", + tagName: "div", + rules: ["div { display: inline; float: right; }"], + isActive: false, + }, + { + info: "display: block is active on a floated element", + property: "display", + tagName: "div", + rules: ["div { display: block; float: right;}"], + isActive: true, + }, + { + info: "display: inline-grid is inactive on a floated element", + property: "display", + createTestElement: rootNode => { + const container = document.createElement("div"); + container.classList.add("test"); + rootNode.append(container); + return container; + }, + rules: [ + "div { float: left; display:block; }", + ".test { display: inline-grid ;}", + ], + isActive: false, + }, + { + info: "display: table-footer-group is inactive on a floated element", + property: "display", + createTestElement: rootNode => { + const container = document.createElement("div"); + container.style.display = "table"; + const footer = document.createElement("div"); + footer.classList.add("table-footer"); + container.append(footer); + rootNode.append(container); + return footer; + }, + rules: [".table-footer { display: table-footer-group; float: left;}"], + isActive: false, + }, + createGridPlacementOnFloatedItemTest("grid-row"), + createGridPlacementOnFloatedItemTest("grid-column"), + createGridPlacementOnFloatedItemTest("grid-area", "foo"), +]; + +function createGridPlacementOnFloatedItemTest(property, value = "2") { + return { + info: `grid placement property ${property} is active on a floated grid item`, + property, + createTestElement: rootNode => { + const grid = document.createElement("div"); + grid.style.display = "grid"; + grid.style.gridTemplateRows = "repeat(5, 1fr)"; + grid.style.gridTemplateColumns = "repeat(5, 1fr)"; + grid.style.gridTemplateAreas = "'foo foo foo'"; + rootNode.appendChild(grid); + + const item = document.createElement("span"); + grid.appendChild(item); + + return item; + }, + rules: [`span { ${property}: ${value}; float: left; }`], + isActive: true, + }; +} diff --git a/devtools/server/tests/chrome/inactive-property-helper/gap.mjs b/devtools/server/tests/chrome/inactive-property-helper/gap.mjs new file mode 100644 index 0000000000..83befcba0d --- /dev/null +++ b/devtools/server/tests/chrome/inactive-property-helper/gap.mjs @@ -0,0 +1,133 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// InactivePropertyHelper `gap` test cases. +export default [ + { + info: "column-gap is inactive on non-grid and non-flex container", + property: "column-gap", + tagName: "div", + rules: ["div { column-gap: 10px; display: block; }"], + isActive: false, + }, + { + info: "column-gap is active on grid container", + property: "column-gap", + tagName: "div", + rules: ["div { column-gap: 10px; display: grid; }"], + isActive: true, + }, + { + info: "column-gap is active on flex container", + property: "column-gap", + tagName: "div", + rules: ["div { column-gap: 10px; display: flex; }"], + isActive: true, + }, + { + info: "column-gap is inactive on non-multi-col container", + property: "column-gap", + tagName: "div", + rules: ["div { column-gap: 10px; column-count: auto; }"], + isActive: false, + }, + { + info: "column-gap is active on multi-column container", + property: "column-gap", + tagName: "div", + rules: ["div { column-gap: 10px; column-count: 2; }"], + isActive: true, + }, + { + info: "row-gap is inactive on non-grid and non-flex container", + property: "row-gap", + tagName: "div", + rules: ["div { row-gap: 10px; display: block; }"], + isActive: false, + }, + { + info: "row-gap is active on grid container", + property: "row-gap", + tagName: "div", + rules: ["div { row-gap: 10px; display: grid; }"], + isActive: true, + }, + { + info: "row-gap is active on flex container", + property: "row-gap", + tagName: "div", + rules: ["div { row-gap: 10px; display: flex; }"], + isActive: true, + }, + { + info: "gap is inactive on non-grid and non-flex container", + property: "gap", + tagName: "div", + rules: ["div { gap: 10px; display: block; }"], + isActive: false, + }, + { + info: "gap is active on flex container", + property: "gap", + tagName: "div", + rules: ["div { gap: 10px; display: flex; }"], + isActive: true, + }, + { + info: "gap is active on grid container", + property: "gap", + tagName: "div", + rules: ["div { gap: 10px; display: grid; }"], + isActive: true, + }, + { + info: "gap is inactive on non-multi-col container", + property: "gap", + tagName: "div", + rules: ["div { gap: 10px; column-count: auto; }"], + isActive: false, + }, + { + info: "gap is active on multi-col container", + property: "gap", + tagName: "div", + rules: ["div { gap: 10px; column-count: 2; }"], + isActive: true, + }, + { + info: "grid-gap is inactive on non-grid and non-flex container", + property: "grid-gap", + tagName: "div", + rules: ["div { grid-gap: 10px; display: block; }"], + isActive: false, + }, + { + info: "grid-gap is active on flex container", + property: "grid-gap", + tagName: "div", + rules: ["div { grid-gap: 10px; display: flex; }"], + isActive: true, + }, + { + info: "grid-gap is active on grid container", + property: "grid-gap", + tagName: "div", + rules: ["div { grid-gap: 10px; display: grid; }"], + isActive: true, + }, + { + info: "grid-gap is inactive on non-multi-col container", + property: "grid-gap", + tagName: "div", + rules: ["div { grid-gap: 10px; column-count: auto; }"], + isActive: false, + }, + { + info: "grid-gap is active on multi-col container", + property: "grid-gap", + tagName: "div", + rules: ["div { grid-gap: 10px; column-count: 2; }"], + isActive: true, + }, +]; diff --git a/devtools/server/tests/chrome/inactive-property-helper/grid-container-properties.mjs b/devtools/server/tests/chrome/inactive-property-helper/grid-container-properties.mjs new file mode 100644 index 0000000000..1fca234733 --- /dev/null +++ b/devtools/server/tests/chrome/inactive-property-helper/grid-container-properties.mjs @@ -0,0 +1,43 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// InactivePropertyHelper test cases: +// `grid-auto-columns`, `grid-auto-flow`, `grid-auto-rows`, `grid-template`, +// `grid-template-areas`, `grid-template-columns`, `grid-template-rows`, +// and `justify-items`. +let tests = []; + +for (const { propertyName, propertyValue } of [ + { propertyName: "grid-auto-columns", propertyValue: "100px" }, + { propertyName: "grid-auto-flow", propertyValue: "columns" }, + { propertyName: "grid-auto-rows", propertyValue: "100px" }, + { propertyName: "grid-template", propertyValue: "auto / auto" }, + { propertyName: "grid-template-areas", propertyValue: "a b c" }, + { propertyName: "grid-template-columns", propertyValue: "100px 1fr" }, + { propertyName: "grid-template-rows", propertyValue: "100px 1fr" }, + { propertyName: "justify-items", propertyValue: "center" }, +]) { + tests = tests.concat(createTestsForProp(propertyName, propertyValue)); +} + +function createTestsForProp(propertyName, propertyValue) { + return [ + { + info: `${propertyName} is active on a grid container`, + property: propertyName, + tagName: "div", + rules: [`div { display:grid; ${propertyName}: ${propertyValue}; }`], + isActive: true, + }, + { + info: `${propertyName} is inactive on a non-grid container`, + property: propertyName, + tagName: "div", + rules: [`div { ${propertyName}: ${propertyValue}; }`], + isActive: false, + }, + ]; +} + +export default tests; diff --git a/devtools/server/tests/chrome/inactive-property-helper/grid-with-absolute-properties.mjs b/devtools/server/tests/chrome/inactive-property-helper/grid-with-absolute-properties.mjs new file mode 100644 index 0000000000..fd963e0d3b --- /dev/null +++ b/devtools/server/tests/chrome/inactive-property-helper/grid-with-absolute-properties.mjs @@ -0,0 +1,71 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// InactivePropertyHelper test cases: +// `grid-area`, `grid-column`, `grid-column-end`, `grid-column-start`, +// `grid-row`, `grid-row-end`, `grid-row-start`, `justify-self`, `align-self` +// and `place-self`. +let tests = []; + +for (const { propertyName, propertyValue } of [ + { propertyName: "grid-area", propertyValue: "2 / 1 / span 2 / span 3" }, + { propertyName: "grid-column", propertyValue: 2 }, + { propertyName: "grid-column-end", propertyValue: "span 3" }, + { propertyName: "grid-column-start", propertyValue: 2 }, + { propertyName: "grid-row", propertyValue: "1 / span 2" }, + { propertyName: "grid-row-end", propertyValue: "span 3" }, + { propertyName: "grid-row-start", propertyValue: 2 }, + { propertyName: "justify-self", propertyValue: "start" }, + { propertyName: "align-self", propertyValue: "auto" }, + { propertyName: "place-self", propertyValue: "auto center" }, +]) { + tests = tests.concat(createTestsForProp(propertyName, propertyValue)); +} + +function createTestsForProp(propertyName, propertyValue) { + return [ + { + info: `${propertyName} is active on a grid item`, + property: `${propertyName}`, + createTestElement, + rules: [ + `#grid-container { display:grid; grid:auto/100px 100px; }`, + `#grid-item { ${propertyName}: ${propertyValue}; }`, + ], + ruleIndex: 1, + isActive: true, + }, + { + info: `${propertyName} is active on an absolutely positioned grid item`, + property: `${propertyName}`, + createTestElement, + rules: [ + `#grid-container { display:grid; grid:auto/100px 100px; position: relative }`, + `#grid-item { ${propertyName}: ${propertyValue}; position: absolute; }`, + ], + ruleIndex: 1, + isActive: true, + }, + { + info: `${propertyName} is inactive on a non-grid item`, + property: `${propertyName}`, + tagName: `div`, + rules: [`div { ${propertyName}: ${propertyValue}; }`], + isActive: false, + }, + ]; +} + +function createTestElement(rootNode) { + const container = document.createElement("div"); + container.id = "grid-container"; + const element = document.createElement("div"); + element.id = "grid-item"; + container.append(element); + rootNode.append(container); + + return element; +} + +export default tests; diff --git a/devtools/server/tests/chrome/inactive-property-helper/highlight-pseudo-elements.mjs b/devtools/server/tests/chrome/inactive-property-helper/highlight-pseudo-elements.mjs new file mode 100644 index 0000000000..bcb5b8763c --- /dev/null +++ b/devtools/server/tests/chrome/inactive-property-helper/highlight-pseudo-elements.mjs @@ -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/. */ + +// InactivePropertyHelper `highlight-pseudo-elements` test cases. + +// "background", +// "background-color", +// "color", +// "fill-color", +// "stroke-color", +// "stroke-width", +// "text-decoration", +// "text-shadow", +// "text-underline-offset", +// "text-underline-position", + +export default [ + { + info: "width is inactive on ::selection", + property: "width", + tagName: "span", + rules: ["span::selection { width: 10px; }"], + isActive: false, + // `width` is also inactive on inline element, so make sure we get the warning + // because we're using it in a highlight pseudo. + expectedMsgId: "inactive-css-highlight-pseudo-elements-not-supported", + }, + { + info: "display is inactive on ::highlight", + property: "display", + tagName: "span", + rules: ["span::highlight(result) { display: grid; }"], + isActive: false, + expectedMsgId: "inactive-css-highlight-pseudo-elements-not-supported", + }, + { + // accept background shorthand, even if it might hold inactive values + info: "background is active on ::selection", + property: "background", + tagName: "span", + rules: ["span::selection { background: red; }"], + isActive: true, + }, + { + info: "border-color is inactive on ::selection", + property: "border-color", + tagName: "span", + rules: ["span::selection { border-color: red; }"], + isActive: false, + // `width` is also inactive on inline element, so make sure we get the warning + // because we're using it in a highlight pseudo. + expectedMsgId: "inactive-css-highlight-pseudo-elements-not-supported", + }, + { + info: "background-color is active on ::selection", + property: "background-color", + tagName: "span", + rules: ["span::selection { background-color: red; }"], + isActive: true, + }, + { + info: "color is active on ::selection", + property: "color", + tagName: "span", + rules: ["span::selection { color: red; }"], + isActive: true, + }, + { + info: "text-decoration is active on ::selection", + property: "text-decoration", + tagName: "span", + rules: [ + "span::selection { text-decoration: double overline #FF3028 4px; }", + ], + isActive: true, + }, + { + info: "text-decoration-color is active on ::selection", + property: "text-decoration-color", + tagName: "span", + rules: ["span::selection { text-decoration-color: #FF3028; }"], + isActive: true, + }, + { + info: "text-decoration-line is active on ::selection", + property: "text-decoration-line", + tagName: "span", + rules: ["span::selection { text-decoration-line: overline; }"], + isActive: true, + }, + { + info: "text-decoration-style is active on ::selection", + property: "text-decoration-style", + tagName: "span", + rules: ["span::selection { text-decoration-style: double; }"], + isActive: true, + }, + { + info: "text-decoration-thickness is active on ::selection", + property: "text-decoration-thickness", + tagName: "span", + rules: ["span::selection { text-decoration-thickness: 4px; }"], + isActive: true, + }, + { + info: "text-shadow is active on ::selection", + property: "text-shadow", + tagName: "span", + rules: ["span::selection { text-shadow: text-shadow: #FC0 1px 0 10px; }"], + isActive: true, + }, + { + info: "text-underline-offset is active on ::selection", + property: "text-underline-offset", + tagName: "span", + rules: ["span::selection { text-underline-offset: 10px; }"], + isActive: true, + }, + { + info: "text-underline-position is active on ::selection", + property: "text-underline-position", + tagName: "span", + rules: ["span::selection { text-underline-position: under; }"], + isActive: true, + }, + { + info: "-webkit-text-fill-color is active on ::selection", + property: "-webkit-text-fill-color", + tagName: "span", + rules: ["span::selection { -webkit-text-fill-color: red; }"], + isActive: true, + }, + { + info: "-webkit-text-stroke-color is active on ::selection", + property: "-webkit-text-stroke-color", + tagName: "span", + rules: ["span::selection { -webkit-text-stroke-color: red; }"], + isActive: true, + }, + { + info: "-webkit-text-stroke-width is active on ::selection", + property: "-webkit-text-stroke-width", + tagName: "span", + rules: ["span::selection { -webkit-text-stroke-width: 4px; }"], + isActive: true, + }, + { + info: "-webkit-text-stroke is active on ::selection", + property: "-webkit-text-stroke", + tagName: "span", + rules: ["span::selection { -webkit-text-stroke: 4px navy; }"], + isActive: true, + }, +]; diff --git a/devtools/server/tests/chrome/inactive-property-helper/margin-padding.mjs b/devtools/server/tests/chrome/inactive-property-helper/margin-padding.mjs new file mode 100644 index 0000000000..7c1d348512 --- /dev/null +++ b/devtools/server/tests/chrome/inactive-property-helper/margin-padding.mjs @@ -0,0 +1,260 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// InactivePropertyHelper `align-content` test cases. + +export default [ + { + info: "margin is active on block containers", + property: "margin", + tagName: "div", + rules: ["div { margin: 10px; }"], + isActive: true, + }, + { + info: "margin is active on flex containers", + property: "margin", + tagName: "div", + rules: ["div { display: flex; margin: 10px; }"], + isActive: true, + }, + { + info: "margin is active on grid containers", + property: "margin", + tagName: "div", + rules: ["div { display: grid; margin: 10px; }"], + isActive: true, + }, + { + info: "margin is active on tables", + property: "margin", + tagName: "div", + rules: ["div { display: table; margin: 10px; }"], + isActive: true, + }, + { + info: "margin is active on inline tables", + property: "margin", + tagName: "div", + rules: ["div { display: inline-table; margin: 10px; }"], + isActive: true, + }, + { + info: "margin is active on table captions", + property: "margin", + tagName: "div", + rules: ["div { display: table-caption; margin: 10px; }"], + isActive: true, + }, + { + info: "margin is inactive on table cells", + property: "margin", + tagName: "div", + rules: ["div { display: table-cell; margin: 10px; }"], + isActive: false, + }, + { + info: "margin-block is inactive on table cells", + property: "margin-block", + tagName: "div", + rules: ["div { display: table-cell; margin-block: 10px; }"], + isActive: false, + }, + { + info: "margin-block-start is inactive on table cells", + property: "margin-block-start", + tagName: "div", + rules: ["div { display: table-cell; margin-block-start: 10px; }"], + isActive: false, + }, + { + info: "margin-block-end is inactive on table cells", + property: "margin-block-end", + tagName: "div", + rules: ["div { display: table-cell; margin-block-end: 10px; }"], + isActive: false, + }, + { + info: "margin-block is inactive on table cells", + property: "margin-block", + tagName: "div", + rules: ["div { display: table-cell; margin-block: 10px; }"], + isActive: false, + }, + { + info: "margin-bottom is inactive on table rows", + property: "margin-bottom", + tagName: "div", + rules: ["div { display: table-row; margin-bottom: 10px; }"], + isActive: false, + }, + { + info: "margin-inline-start is inactive on table rows", + property: "margin-inline-start", + tagName: "div", + rules: ["div { display: table-row; margin-inline-start: 10px; }"], + isActive: false, + }, + { + info: "margin-inline-end is inactive on table rows", + property: "margin-inline-end", + tagName: "div", + rules: ["div { display: table-row; margin-inline-end: 10px; }"], + isActive: false, + }, + { + info: "margin-inline is inactive on table rows", + property: "margin-inline", + tagName: "div", + rules: ["div { display: table-row; margin-inline: 10px; }"], + isActive: false, + }, + { + info: "margin-left is inactive on table columns", + property: "margin-left", + tagName: "div", + rules: ["div { display: table-column; margin-left: 10px; }"], + isActive: false, + }, + { + info: "margin-right is inactive on table row groups", + property: "margin-right", + tagName: "div", + rules: ["div { display: table-row-group; margin-right: 10px; }"], + isActive: false, + }, + { + info: "margin-top is inactive on table column groups", + property: "margin-top", + tagName: "div", + rules: ["div { display: table-column-group; margin-top: 10px; }"], + isActive: false, + }, + { + info: "padding is active on block containers", + property: "padding", + tagName: "div", + rules: ["div { padding: 10px; }"], + isActive: true, + }, + { + info: "padding is active on flex containers", + property: "padding", + tagName: "div", + rules: ["div { display: flex; padding: 10px; }"], + isActive: true, + }, + { + info: "padding is active on grid containers", + property: "padding", + tagName: "div", + rules: ["div { display: grid; padding: 10px; }"], + isActive: true, + }, + { + info: "padding is active on tables", + property: "padding", + tagName: "div", + rules: ["div { display: table; padding: 10px; }"], + isActive: true, + }, + { + info: "padding is active on inline tables", + property: "padding", + tagName: "div", + rules: ["div { display: inline-table; padding: 10px; }"], + isActive: true, + }, + { + info: "padding is active on table captions", + property: "padding", + tagName: "div", + rules: ["div { display: table-caption; padding: 10px; }"], + isActive: true, + }, + { + info: "padding is active on table cells", + property: "padding", + tagName: "div", + rules: ["div { display: table-cell; padding: 10px; }"], + isActive: true, + }, + { + info: "padding-block is active on table cells", + property: "padding-block", + tagName: "div", + rules: ["div { display: table-cell; padding-block: 10px; }"], + isActive: true, + }, + { + info: "padding-block-start is active on table cells", + property: "padding-block-start", + tagName: "div", + rules: ["div { display: table-cell; padding-block-start: 10px; }"], + isActive: true, + }, + { + info: "padding-block-end is active on table cells", + property: "padding-block-end", + tagName: "div", + rules: ["div { display: table-cell; padding-block-end: 10px; }"], + isActive: true, + }, + { + info: "padding-block is active on table cells", + property: "padding-block", + tagName: "div", + rules: ["div { display: table-cell; padding-block: 10px; }"], + isActive: true, + }, + { + info: "padding-bottom is inactive on table rows", + property: "padding-bottom", + tagName: "div", + rules: ["div { display: table-row; padding-bottom: 10px; }"], + isActive: false, + }, + { + info: "padding-inline-start is inactive on table rows", + property: "padding-inline-start", + tagName: "div", + rules: ["div { display: table-row; padding-inline-start: 10px; }"], + isActive: false, + }, + { + info: "padding-inline-end is inactive on table rows", + property: "padding-inline-end", + tagName: "div", + rules: ["div { display: table-row; padding-inline-end: 10px; }"], + isActive: false, + }, + { + info: "padding-inline is inactive on table rows", + property: "padding-inline", + tagName: "div", + rules: ["div { display: table-row; padding-inline: 10px; }"], + isActive: false, + }, + { + info: "padding-left is inactive on table columns", + property: "padding-left", + tagName: "div", + rules: ["div { display: table-column; padding-left: 10px; }"], + isActive: false, + }, + { + info: "padding-right is inactive on table row groups", + property: "padding-right", + tagName: "div", + rules: ["div { display: table-row-group; padding-right: 10px; }"], + isActive: false, + }, + { + info: "padding-top is inactive on table column groups", + property: "padding-top", + tagName: "div", + rules: ["div { display: table-column-group; padding-top: 10px; }"], + isActive: false, + }, +]; diff --git a/devtools/server/tests/chrome/inactive-property-helper/max-min-width-height.mjs b/devtools/server/tests/chrome/inactive-property-helper/max-min-width-height.mjs new file mode 100644 index 0000000000..4bb5623f6e --- /dev/null +++ b/devtools/server/tests/chrome/inactive-property-helper/max-min-width-height.mjs @@ -0,0 +1,366 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// InactivePropertyHelper `width`, `min-width`, `max-width`, `height`, `min-height`, +// `max-height` test cases. +export default [ + { + info: "width is inactive on a non-replaced inline element", + property: "width", + tagName: "span", + rules: ["span { width: 500px; }"], + isActive: false, + }, + { + info: "min-width is inactive on a non-replaced inline element", + property: "min-width", + tagName: "span", + rules: ["span { min-width: 500px; }"], + isActive: false, + }, + { + info: "max-width is inactive on a non-replaced inline element", + property: "max-width", + tagName: "span", + rules: ["span { max-width: 500px; }"], + isActive: false, + }, + { + info: "width is inactive on an tr element", + property: "width", + tagName: "tr", + rules: ["tr { width: 500px; }"], + isActive: false, + }, + { + info: "min-width is inactive on an tr element", + property: "min-width", + tagName: "tr", + rules: ["tr { min-width: 500px; }"], + isActive: false, + }, + { + info: "max-width is inactive on an tr element", + property: "max-width", + tagName: "tr", + rules: ["tr { max-width: 500px; }"], + isActive: false, + }, + { + info: "width is inactive on an thead element", + property: "width", + tagName: "thead", + rules: ["thead { width: 500px; }"], + isActive: false, + }, + { + info: "min-width is inactive on an thead element", + property: "min-width", + tagName: "thead", + rules: ["thead { min-width: 500px; }"], + isActive: false, + }, + { + info: "max-width is inactive on an thead element", + property: "max-width", + tagName: "thead", + rules: ["thead { max-width: 500px; }"], + isActive: false, + }, + { + info: "width is inactive on an tfoot element", + property: "width", + tagName: "tfoot", + rules: ["tfoot { width: 500px; }"], + isActive: false, + }, + { + info: "min-width is inactive on an tfoot element", + property: "min-width", + tagName: "tfoot", + rules: ["tfoot { min-width: 500px; }"], + isActive: false, + }, + { + info: "max-width is inactive on an tfoot element", + property: "max-width", + tagName: "tfoot", + rules: ["tfoot { max-width: 500px; }"], + isActive: false, + }, + { + info: "width is active on a replaced inline element", + property: "width", + tagName: "img", + rules: ["img { width: 500px; }"], + isActive: true, + }, + { + info: "width is active on an inline input element", + property: "width", + tagName: "input", + rules: ["input { display: inline; width: 500px; }"], + isActive: true, + }, + { + info: "width is active on an inline select element", + property: "width", + tagName: "select", + rules: ["select { display: inline; width: 500px; }"], + isActive: true, + }, + { + info: "width is active on a textarea element", + property: "width", + tagName: "textarea", + rules: ["textarea { width: 500px; }"], + isActive: true, + }, + { + info: "min-width is active on a replaced inline element", + property: "min-width", + tagName: "img", + rules: ["img { min-width: 500px; }"], + isActive: true, + }, + { + info: "max-width is active on a replaced inline element", + property: "max-width", + tagName: "img", + rules: ["img { max-width: 500px; }"], + isActive: true, + }, + { + info: "width is active on a block element", + property: "width", + tagName: "div", + rules: ["div { width: 500px; }"], + isActive: true, + }, + { + info: "min-width is active on a block element", + property: "min-width", + tagName: "div", + rules: ["div { min-width: 500px; }"], + isActive: true, + }, + { + info: "max-width is active on a block element", + property: "max-width", + tagName: "div", + rules: ["div { max-width: 500px; }"], + isActive: true, + }, + { + info: "height is inactive on a non-replaced inline element", + property: "height", + tagName: "span", + rules: ["span { height: 500px; }"], + isActive: false, + }, + { + info: "min-height is inactive on a non-replaced inline element", + property: "min-height", + tagName: "span", + rules: ["span { min-height: 500px; }"], + isActive: false, + }, + { + info: "max-height is inactive on a non-replaced inline element", + property: "max-height", + tagName: "span", + rules: ["span { max-height: 500px; }"], + isActive: false, + }, + { + info: "height is inactive on colgroup element", + property: "height", + tagName: "colgroup", + rules: ["colgroup { height: 500px; }"], + isActive: false, + }, + { + info: "min-height is inactive on colgroup element", + property: "min-height", + tagName: "colgroup", + rules: ["colgroup { min-height: 500px; }"], + isActive: false, + }, + { + info: "max-height is inactive on colgroup element", + property: "max-height", + tagName: "colgroup", + rules: ["colgroup { max-height: 500px; }"], + isActive: false, + }, + { + info: "height is inactive on col element", + property: "height", + tagName: "col", + rules: ["col { height: 500px; }"], + isActive: false, + }, + { + info: "min-height is inactive on col element", + property: "min-height", + tagName: "col", + rules: ["col { min-height: 500px; }"], + isActive: false, + }, + { + info: "max-height is inactive on col element", + property: "max-height", + tagName: "col", + rules: ["col { max-height: 500px; }"], + isActive: false, + }, + { + info: "height is active on a replaced inline element", + property: "height", + tagName: "img", + rules: ["img { height: 500px; }"], + isActive: true, + }, + { + info: "height is active on an inline input element", + property: "height", + tagName: "input", + rules: ["input { display: inline; height: 500px; }"], + isActive: true, + }, + { + info: "height is active on an inline select element", + property: "height", + tagName: "select", + rules: ["select { display: inline; height: 500px; }"], + isActive: true, + }, + { + info: "height is active on a textarea element", + property: "height", + tagName: "textarea", + rules: ["textarea { height: 500px; }"], + isActive: true, + }, + { + info: "min-height is active on a replaced inline element", + property: "min-height", + tagName: "img", + rules: ["img { min-height: 500px; }"], + isActive: true, + }, + { + info: "max-height is active on a replaced inline element", + property: "max-height", + tagName: "img", + rules: ["img { max-height: 500px; }"], + isActive: true, + }, + { + info: "height is active on a block element", + property: "height", + tagName: "div", + rules: ["div { height: 500px; }"], + isActive: true, + }, + { + info: "min-height is active on a block element", + property: "min-height", + tagName: "div", + rules: ["div { min-height: 500px; }"], + isActive: true, + }, + { + info: "max-height is active on a block element", + property: "max-height", + tagName: "div", + rules: ["div { max-height: 500px; }"], + isActive: true, + }, + { + info: "height is active on an svg <rect> element.", + property: "height", + createTestElement: main => { + main.innerHTML = ` + <svg width=100 height=100> + <rect width=100 fill=green></rect> + </svg> + `; + return main.querySelector("rect"); + }, + rules: ["rect { height: 100px; }"], + isActive: true, + }, + createTableElementTestCase("width", false, "table-row"), + createTableElementTestCase("width", false, "table-row-group"), + createTableElementTestCase("width", true, "table-column"), + createTableElementTestCase("width", true, "table-column-group"), + createTableElementTestCase("height", false, "table-column"), + createTableElementTestCase("height", false, "table-column-group"), + createTableElementTestCase("height", true, "table-row"), + createTableElementTestCase("height", true, "table-row-group"), + createVerticalTableElementTestCase("width", true, "table-row"), + createVerticalTableElementTestCase("width", true, "table-row-group"), + createVerticalTableElementTestCase("width", false, "table-column"), + createVerticalTableElementTestCase("width", false, "table-column-group"), + createVerticalTableElementTestCase("height", true, "table-column"), + createVerticalTableElementTestCase("height", true, "table-column-group"), + createVerticalTableElementTestCase("height", false, "table-row"), + createVerticalTableElementTestCase("height", false, "table-row-group"), + { + info: "width's inactivity status for a row takes the table's writing mode into account", + property: "width", + createTestElement: rootNode => { + const table = document.createElement("table"); + table.style.writingMode = "vertical-lr"; + rootNode.appendChild(table); + + const tbody = document.createElement("tbody"); + table.appendChild(tbody); + + const tr = document.createElement("tr"); + tbody.appendChild(tr); + + const td = document.createElement("td"); + tr.appendChild(td); + + return tr; + }, + rules: ["tr { writing-mode: horizontal-tb; width: 360px; }"], + isActive: true, + }, +]; + +function createTableElementTestCase(property, isActive, displayType) { + return { + info: `${property} is ${ + isActive ? "active" : "inactive" + } on a ${displayType}`, + property, + tagName: "div", + rules: [`div { display: ${displayType}; ${property}: 100px; }`], + isActive, + }; +} + +function createVerticalTableElementTestCase(property, isActive, displayType) { + return { + info: `${property} is ${ + isActive ? "active" : "inactive" + } on a vertical ${displayType}`, + property, + createTestElement: rootNode => { + const container = document.createElement("div"); + container.style.writingMode = "vertical-lr"; + rootNode.append(container); + + const element = document.createElement("span"); + container.append(element); + + return element; + }, + rules: [`span { display: ${displayType}; ${property}: 100px; }`], + isActive, + }; +} diff --git a/devtools/server/tests/chrome/inactive-property-helper/multicol-container-properties.mjs b/devtools/server/tests/chrome/inactive-property-helper/multicol-container-properties.mjs new file mode 100644 index 0000000000..6bc4e9dd13 --- /dev/null +++ b/devtools/server/tests/chrome/inactive-property-helper/multicol-container-properties.mjs @@ -0,0 +1,39 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// InactivePropertyHelper test cases: +// `column-fill`, `column-rule`, `column-rule-color`, `column-rule-style`, +// and `column-rule-width`. +let tests = []; + +for (const { propertyName, propertyValue } of [ + { propertyName: "column-fill", propertyValue: "auto" }, + { propertyName: "column-rule", propertyValue: "1px solid black" }, + { propertyName: "column-rule-color", propertyValue: "black" }, + { propertyName: "column-rule-style", propertyValue: "solid" }, + { propertyName: "column-rule-width", propertyValue: "1px" }, +]) { + tests = tests.concat(createTestsForProp(propertyName, propertyValue)); +} + +function createTestsForProp(propertyName, propertyValue) { + return [ + { + info: `${propertyName} is active on a multi-column container`, + property: propertyName, + tagName: "div", + rules: [`div { columns:2; ${propertyName}: ${propertyValue}; }`], + isActive: true, + }, + { + info: `${propertyName} is inactive on a non-multi-column container`, + property: propertyName, + tagName: "div", + rules: [`div { ${propertyName}: ${propertyValue}; }`], + isActive: false, + }, + ]; +} + +export default tests; diff --git a/devtools/server/tests/chrome/inactive-property-helper/place-items-content.mjs b/devtools/server/tests/chrome/inactive-property-helper/place-items-content.mjs new file mode 100644 index 0000000000..f554a785a7 --- /dev/null +++ b/devtools/server/tests/chrome/inactive-property-helper/place-items-content.mjs @@ -0,0 +1,159 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// InactivePropertyHelper `place-items` and `place-content` test cases. +export default [ + { + info: "place-items is inactive on block element", + property: "place-items", + tagName: "div", + rules: ["div { place-items: center; }"], + isActive: false, + }, + { + info: "place-items is inactive on inline element", + property: "place-items", + tagName: "span", + rules: ["span { place-items: center; }"], + isActive: false, + }, + { + info: "place-items is inactive on flex item", + property: "place-items", + createTestElement: rootNode => { + const container = document.createElement("div"); + const element = document.createElement("span"); + container.append(element); + rootNode.append(container); + return element; + }, + rules: [ + "div { display: flex; align-items: start; }", + "span { place-items: center; }", + ], + ruleIndex: 1, + isActive: false, + }, + { + info: "place-items is inactive on grid item", + property: "place-items", + createTestElement: rootNode => { + const container = document.createElement("div"); + const element = document.createElement("span"); + container.append(element); + rootNode.append(container); + return element; + }, + rules: [ + "div { display: grid; align-items: start; }", + "span { place-items: center; }", + ], + ruleIndex: 1, + isActive: false, + }, + { + info: "place-items is active on flex container", + property: "place-items", + tagName: "div", + rules: ["div { place-items: center; display: flex;}"], + isActive: true, + }, + { + info: "place-items is active on inline-flex container", + property: "place-items", + tagName: "div", + rules: ["div { place-items: center; display: inline-flex;}"], + isActive: true, + }, + { + info: "place-items is active on grid container", + property: "place-items", + tagName: "div", + rules: ["div { place-items: center; display: grid;}"], + isActive: true, + }, + { + info: "place-items is active on inline grid container", + property: "place-items", + tagName: "div", + rules: ["div { place-items: center; display: inline-grid;}"], + isActive: true, + }, + { + info: "place-content is inactive on block element", + property: "place-content", + tagName: "div", + rules: ["div { place-content: center; }"], + isActive: false, + }, + { + info: "place-content is inactive on inline element", + property: "place-content", + tagName: "span", + rules: ["span { place-content: center; }"], + isActive: false, + }, + { + info: "place-content is inactive on flex item", + property: "place-content", + createTestElement: rootNode => { + const container = document.createElement("div"); + const element = document.createElement("span"); + container.append(element); + rootNode.append(container); + return element; + }, + rules: [ + "div { display: flex; align-items: start; }", + "span { place-content: center; }", + ], + ruleIndex: 1, + isActive: false, + }, + { + info: "place-content is inactive on grid item", + property: "place-content", + createTestElement: rootNode => { + const container = document.createElement("div"); + const element = document.createElement("span"); + container.append(element); + rootNode.append(container); + return element; + }, + rules: [ + "div { display: grid; align-items: start; }", + "span { place-content: center; }", + ], + ruleIndex: 1, + isActive: false, + }, + { + info: "place-content is active on flex container", + property: "place-content", + tagName: "div", + rules: ["div { place-content: center; display: flex;}"], + isActive: true, + }, + { + info: "place-content is active on inline-flex container", + property: "place-content", + tagName: "div", + rules: ["div { place-content: center; display: inline-flex;}"], + isActive: true, + }, + { + info: "place-content is active on grid container", + property: "place-content", + tagName: "div", + rules: ["div { place-content: center; display: grid;}"], + isActive: true, + }, + { + info: "place-content is active on inline grid container", + property: "place-content", + tagName: "div", + rules: ["div { place-content: center; display: inline-grid;}"], + isActive: true, + }, +]; diff --git a/devtools/server/tests/chrome/inactive-property-helper/placeholder-pseudo-element.mjs b/devtools/server/tests/chrome/inactive-property-helper/placeholder-pseudo-element.mjs new file mode 100644 index 0000000000..6c9a81472b --- /dev/null +++ b/devtools/server/tests/chrome/inactive-property-helper/placeholder-pseudo-element.mjs @@ -0,0 +1,122 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// InactivePropertyHelper `placeholder-pseudo-element` test cases. + +//"baseline-source", +//"direction", +//"dominant-baseline", +//"line-height", +//"text-orientation", +//"vertical-align", +//"writing-mode", +//"alignment-baseline", +//"baseline-shift", +//"initial-letter", +//"text-box-trim", + +export default [ + { + info: "baseline-source is inactive on ::placeholder", + property: "baseline-source", + tagName: "input", + rules: ["input::placeholder { baseline-source: first; }"], + isActive: false, + expectedMsgId: "inactive-css-placeholder-pseudo-element-not-supported", + }, + { + info: "direction is inactive on ::placeholder", + property: "direction", + tagName: "input", + rules: ["input::placeholder { direction: rtl; }"], + isActive: false, + expectedMsgId: "inactive-css-placeholder-pseudo-element-not-supported", + }, + { + info: "dominant-baseline is inactive on ::placeholder", + property: "dominant-baseline", + tagName: "input", + rules: ["input::placeholder { dominant-baseline: central; }"], + isActive: false, + expectedMsgId: "inactive-css-placeholder-pseudo-element-not-supported", + }, + { + info: "line-height is inactive on ::placeholder", + property: "line-height", + tagName: "input", + rules: ["input::placeholder { line-height: 2em; }"], + isActive: false, + expectedMsgId: "inactive-css-placeholder-pseudo-element-not-supported", + }, + { + info: "text-orientation is inactive on ::placeholder", + property: "text-orientation", + tagName: "input", + rules: ["input::placeholder { text-orientation: sideways; }"], + isActive: false, + expectedMsgId: "inactive-css-placeholder-pseudo-element-not-supported", + }, + { + info: "vertical-align is inactive on ::placeholder", + property: "vertical-align", + tagName: "input", + rules: ["input::placeholder { vertical-align: super; }"], + isActive: false, + expectedMsgId: "inactive-css-placeholder-pseudo-element-not-supported", + }, + { + info: "writing-mode is inactive on ::placeholder", + property: "writing-mode", + tagName: "input", + rules: ["input::placeholder { writing-mode: vertical-rl; }"], + isActive: false, + expectedMsgId: "inactive-css-placeholder-pseudo-element-not-supported", + }, + { + info: "alignment-baseline is inactive on ::placeholder", + property: "alignment-baseline", + tagName: "input", + rules: ["input::placeholder { alignment-baseline: central; }"], + isActive: false, + expectedMsgId: "inactive-css-placeholder-pseudo-element-not-supported", + }, + { + info: "baseline-shift is inactive on ::placeholder", + property: "baseline-shift", + tagName: "input", + rules: ["input::placeholder { baseline-shift: super; }"], + isActive: false, + expectedMsgId: "inactive-css-placeholder-pseudo-element-not-supported", + }, + { + info: "initial-letter is inactive on ::placeholder", + property: "initial-letter", + tagName: "input", + rules: ["input::placeholder { initial-letter: 2em; }"], + isActive: false, + expectedMsgId: "inactive-css-placeholder-pseudo-element-not-supported", + }, + { + info: "text-box-trim is inactive on ::placeholder", + property: "text-box-trim", + tagName: "input", + rules: ["input::placeholder { text-box-trim: both; }"], + isActive: false, + expectedMsgId: "inactive-css-placeholder-pseudo-element-not-supported", + }, + { + info: "color is active on ::placeholder", + property: "color", + tagName: "input", + rules: ["input::placeholder { color: green; }"], + isActive: true, + }, + { + info: "display is active on ::placeholder", + property: "display", + tagName: "input", + rules: ["input::placeholder { display: grid; }"], + isActive: true, + }, +]; diff --git a/devtools/server/tests/chrome/inactive-property-helper/positioned.mjs b/devtools/server/tests/chrome/inactive-property-helper/positioned.mjs new file mode 100644 index 0000000000..0386c278c5 --- /dev/null +++ b/devtools/server/tests/chrome/inactive-property-helper/positioned.mjs @@ -0,0 +1,82 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// InactivePropertyHelper positioned elements test cases. + +// These are the properties we care about, those that are inactive when the element isn't +// positioned. +const PROPERTIES = [ + { property: "z-index", value: "2" }, + { property: "top", value: "20px" }, + { property: "right", value: "20px" }, + { property: "bottom", value: "20px" }, + { property: "left", value: "20px" }, +]; + +// These are all of the different position values and whether the above properties are +// active or not for each. +const POSITIONS = [ + { position: "unset", isActive: false }, + { position: "initial", isActive: false }, + { position: "inherit", isActive: false }, + { position: "static", isActive: false }, + { position: "absolute", isActive: true }, + { position: "relative", isActive: true }, + { position: "fixed", isActive: true }, + { position: "sticky", isActive: true }, +]; + +function makeTestCase(property, value, position, isActive) { + return { + info: `${property} is ${ + isActive ? "" : "in" + }active when position is ${position}`, + property, + tagName: "div", + rules: [`div { ${property}: ${value}; position: ${position}; }`], + isActive, + }; +} + +// Make the test cases for all the combinations of PROPERTIES and POSITIONS +const mainTests = []; + +for (const { property, value } of PROPERTIES) { + for (const { position, isActive } of POSITIONS) { + mainTests.push(makeTestCase(property, value, position, isActive)); + } +} + +// Add a few test cases to check that z-index actually works inside grids and flexboxes. +mainTests.push({ + info: "z-index is active even on unpositioned elements if they are grid items", + property: "z-index", + createTestElement: rootNode => { + const container = document.createElement("div"); + const element = document.createElement("span"); + container.append(element); + rootNode.append(container); + return element; + }, + rules: ["div { display: grid; }", "span { z-index: 3; }"], + ruleIndex: 1, + isActive: true, +}); + +mainTests.push({ + info: "z-index is active even on unpositioned elements if they are flex items", + property: "z-index", + createTestElement: rootNode => { + const container = document.createElement("div"); + const element = document.createElement("span"); + container.append(element); + rootNode.append(container); + return element; + }, + rules: ["div { display: flex; }", "span { z-index: 3; }"], + ruleIndex: 1, + isActive: true, +}); + +export default mainTests; diff --git a/devtools/server/tests/chrome/inactive-property-helper/scroll-padding.mjs b/devtools/server/tests/chrome/inactive-property-helper/scroll-padding.mjs new file mode 100644 index 0000000000..acb2899be2 --- /dev/null +++ b/devtools/server/tests/chrome/inactive-property-helper/scroll-padding.mjs @@ -0,0 +1,159 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// InactivePropertyHelper `scroll-padding-*` test cases. + +export default [ + { + info: "scroll-padding is active on element with auto-overflow", + property: "scroll-padding", + tagName: "div", + rules: ["div { overflow: auto; scroll-padding: 10px; }"], + isActive: true, + }, + { + info: "scroll-padding is active on element with scrollable overflow", + property: "scroll-padding", + tagName: "div", + rules: ["div { overflow: scroll; scroll-padding: 10px; }"], + isActive: true, + }, + { + info: "scroll-padding is active on element with hidden overflow", + property: "scroll-padding", + tagName: "div", + rules: ["div { overflow: hidden; scroll-padding: 10px; }"], + isActive: true, + }, + { + info: "scroll-padding is inactive on element with visible overflow", + property: "scroll-padding", + tagName: "div", + rules: ["div { scroll-padding: 10px; }"], + isActive: false, + }, + { + info: "scroll-padding is inactive on element with clipped overflow", + property: "scroll-padding", + tagName: "div", + rules: ["div { overflow: clip; scroll-padding: 10px; }"], + isActive: false, + }, + { + info: "scroll-padding is inactive on element with horizontally clipped overflow", + property: "scroll-padding", + tagName: "div", + rules: ["div { overflow-x: clip; scroll-padding: 10px; }"], + isActive: false, + }, + { + info: "scroll-padding is inactive on element with vertically clipped overflow", + property: "scroll-padding", + tagName: "div", + rules: ["div { overflow-y: clip; scroll-padding: 10px; }"], + isActive: false, + }, + { + info: "scroll-padding-top is inactive on element with visible overflow", + property: "scroll-padding-top", + tagName: "div", + rules: ["div { scroll-padding-top: 10px; }"], + isActive: false, + }, + { + info: "scroll-padding-top is inactive on element with horizontally clipped overflow", + property: "scroll-padding-top", + tagName: "div", + rules: ["div { overflow-x: clip; scroll-padding-top: 10px; }"], + isActive: false, + }, + { + info: "scroll-padding-top is inactive on element with vertically clipped overflow", + property: "scroll-padding-top", + tagName: "div", + rules: ["div { overflow-y: clip; scroll-padding-top: 10px; }"], + isActive: false, + }, + { + info: "scroll-padding-top is active on element with horizontally clipped but vertical auto-overflow (as 'clip' is computed to 'hidden')", + property: "scroll-padding-top", + tagName: "div", + rules: [ + "div { overflow-x: clip; overflow-y: auto; scroll-padding-top: 10px; }", + ], + isActive: true, + }, + { + info: "scroll-padding-top is active on element with vertically clipped but horizontal auto-overflow (as 'clip' is computed to 'hidden')", + property: "scroll-padding-top", + tagName: "div", + rules: [ + "div { overflow-x: auto; overflow-y: clip; scroll-padding-top: 10px; }", + ], + isActive: true, + }, + { + info: "scroll-padding-right is inactive on element with visible overflow", + property: "scroll-padding-right", + tagName: "div", + rules: ["div { scroll-padding-right: 10px; }"], + isActive: false, + }, + { + info: "scroll-padding-bottom is inactive on element with visible overflow", + property: "scroll-padding-bottom", + tagName: "div", + rules: ["div { scroll-padding-bottom: 10px; }"], + isActive: false, + }, + { + info: "scroll-padding-left is inactive on element with visible overflow", + property: "scroll-padding-left", + tagName: "div", + rules: ["div { scroll-padding-left: 10px; }"], + isActive: false, + }, + { + info: "scroll-padding-block is inactive on element with visible overflow", + property: "scroll-padding-block", + tagName: "div", + rules: ["div { scroll-padding-block: 10px; }"], + isActive: false, + }, + { + info: "scroll-padding-block-end is inactive on element with visible overflow", + property: "scroll-padding-block-end", + tagName: "div", + rules: ["div { scroll-padding-block-end: 10px; }"], + isActive: false, + }, + { + info: "scroll-padding-block-start is inactive on element with visible overflow", + property: "scroll-padding-block-start", + tagName: "div", + rules: ["div { scroll-padding-block-start: 10px; }"], + isActive: false, + }, + { + info: "scroll-padding-inline is inactive on element with visible overflow", + property: "scroll-padding-inline", + tagName: "div", + rules: ["div { scroll-padding-inline: 10px; }"], + isActive: false, + }, + { + info: "scroll-padding-inline-end is inactive on element with visible overflow", + property: "scroll-padding-inline-end", + tagName: "div", + rules: ["div { scroll-padding-inline-end: 10px; }"], + isActive: false, + }, + { + info: "scroll-padding-inline-start is inactive on element with visible overflow", + property: "scroll-padding-inline-start", + tagName: "div", + rules: ["div { scroll-padding-inline-start: 10px; }"], + isActive: false, + }, +]; diff --git a/devtools/server/tests/chrome/inactive-property-helper/table-cell.mjs b/devtools/server/tests/chrome/inactive-property-helper/table-cell.mjs new file mode 100644 index 0000000000..bda1f27015 --- /dev/null +++ b/devtools/server/tests/chrome/inactive-property-helper/table-cell.mjs @@ -0,0 +1,21 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// InactivePropertyHelper `empty-cells` test cases. +export default [ + { + info: "empty-cells is inactive on block element", + property: "empty-cells", + tagName: "div", + rules: ["div { empty-cells: hide; }"], + isActive: false, + }, + { + info: "empty-cells is active on table cell element", + property: "empty-cells", + tagName: "div", + rules: ["div { display: table-cell; empty-cells: hide; }"], + isActive: true, + }, +]; diff --git a/devtools/server/tests/chrome/inactive-property-helper/table.mjs b/devtools/server/tests/chrome/inactive-property-helper/table.mjs new file mode 100644 index 0000000000..596698522c --- /dev/null +++ b/devtools/server/tests/chrome/inactive-property-helper/table.mjs @@ -0,0 +1,28 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// InactivePropertyHelper `table-layout` test cases. +export default [ + { + info: "table-layout is inactive on block element", + property: "table-layout", + tagName: "div", + rules: ["div { table-layout: fixed; }"], + isActive: false, + }, + { + info: "table-layout is active on table element", + property: "table-layout", + tagName: "div", + rules: ["div { display: table; table-layout: fixed; }"], + isActive: true, + }, + { + info: "table-layout is active on inline table element", + property: "table-layout", + tagName: "div", + rules: ["div { display: inline-table; table-layout: fixed; }"], + isActive: true, + }, +]; diff --git a/devtools/server/tests/chrome/inactive-property-helper/text-overflow.mjs b/devtools/server/tests/chrome/inactive-property-helper/text-overflow.mjs new file mode 100644 index 0000000000..ada2211b3a --- /dev/null +++ b/devtools/server/tests/chrome/inactive-property-helper/text-overflow.mjs @@ -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/. */ + +// InactivePropertyHelper `text-overflow` test cases. +export default [ + { + info: "text-overflow is inactive when overflow is not set", + property: "text-overflow", + tagName: "div", + rules: ["div { text-overflow: ellipsis; }"], + isActive: false, + }, + { + info: "text-overflow is active when overflow is set to hidden", + property: "text-overflow", + tagName: "div", + rules: ["div { text-overflow: ellipsis; overflow: hidden; }"], + isActive: true, + }, + { + info: "text-overflow is active when overflow is set to auto", + property: "text-overflow", + tagName: "div", + rules: ["div { text-overflow: ellipsis; overflow: auto; }"], + isActive: true, + }, + { + info: "text-overflow is active when overflow is set to scroll", + property: "text-overflow", + tagName: "div", + rules: ["div { text-overflow: ellipsis; overflow: scroll; }"], + isActive: true, + }, + { + info: "text-overflow is inactive when overflow is set to visible", + property: "text-overflow", + tagName: "div", + rules: ["div { text-overflow: ellipsis; overflow: visible; }"], + isActive: false, + }, + { + info: "text-overflow is active when overflow-x is set to hidden on horizontal writing mode", + property: "text-overflow", + tagName: "div", + rules: [ + "div { writing-mode: lr; text-overflow: ellipsis; overflow-x: hidden; }", + ], + isActive: true, + }, + { + info: "text-overflow is inactive when overflow-x is set to visible on horizontal writing mode", + property: "text-overflow", + tagName: "div", + rules: [ + "div { writing-mode: lr; text-overflow: ellipsis; overflow-x: visible; }", + ], + isActive: false, + }, + { + info: "text-overflow is active when overflow-y is set to hidden on vertical writing mode", + property: "text-overflow", + tagName: "div", + rules: [ + "div { writing-mode: vertical-lr; text-overflow: ellipsis; overflow-y: hidden; }", + ], + isActive: true, + }, + { + info: "text-overflow is inactive when overflow-y is set to visible on vertical writing mode", + property: "text-overflow", + tagName: "div", + rules: [ + "div { writing-mode: vertical-lr; text-overflow: ellipsis; overflow-y: visible; }", + ], + isActive: false, + }, + { + info: "as soon as overflow:hidden is set, text-overflow is active whatever the box type", + property: "text-overflow", + tagName: "span", + rules: ["span { text-overflow: ellipsis; overflow: hidden; }"], + isActive: true, + }, + { + info: "as soon as overflow:hidden is set, text-overflow is active whatever the box type", + property: "text-overflow", + tagName: "legend", + rules: ["legend { text-overflow: ellipsis; overflow: hidden; }"], + isActive: true, + }, +]; diff --git a/devtools/server/tests/chrome/inactive-property-helper/text-wrap.mjs b/devtools/server/tests/chrome/inactive-property-helper/text-wrap.mjs new file mode 100644 index 0000000000..58751aa764 --- /dev/null +++ b/devtools/server/tests/chrome/inactive-property-helper/text-wrap.mjs @@ -0,0 +1,86 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// InactivePropertyHelper `text-wrap: balance` test cases. +const LOREM_IPSUM = ` + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Donec a diam lectus. Sed sit amet ipsum mauris. + Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. +`; + +export default [ + { + info: "text-wrap: balance; is inactive when line number exceeds threshold", + property: "text-wrap", + createTestElement: rootNode => { + const element = document.createElement("div"); + element.textContent = LOREM_IPSUM; + rootNode.append(element); + return element; + }, + tagName: "div", + rules: ["div { text-wrap: balance; width: 100px; }"], + isActive: false, + }, + { + info: "text-wrap: balance; is active when line number is below threshold", + property: "text-wrap", + createTestElement: rootNode => { + const element = document.createElement("div"); + element.textContent = LOREM_IPSUM; + rootNode.append(element); + return element; + }, + tagName: "div", + rules: ["div { text-wrap: balance; width: 300px; }"], + isActive: true, + }, + { + info: "text-wrap: balance is inactive when element is fragmented", + property: "text-wrap", + createTestElement: rootNode => { + const element = document.createElement("div"); + element.textContent = ` + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Donec a diam lectus. Sed sit amet ipsum mauris. + Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. + `; + rootNode.append(element); + return element; + }, + tagName: "div", + rules: ["div { text-wrap: balance; column-count: 2; }"], + isActive: false, + }, + { + info: "text-wrap: balance; does not throw if element is not a block", + property: "text-wrap", + createTestElement: rootNode => { + const element = document.createElement("div"); + element.textContent = LOREM_IPSUM; + rootNode.append(element); + return element; + }, + tagName: "div", + rules: ["div { text-wrap: balance; display: inline; }"], + isActive: true, + }, + { + info: "text-wrap: initial; is active", + property: "text-wrap", + createTestElement: rootNode => { + const element = document.createElement("div"); + element.textContent = ` + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Donec a diam lectus. Sed sit amet ipsum mauris. + Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. + `; + rootNode.append(element); + return element; + }, + tagName: "div", + rules: ["div { text-wrap: initial; width: 100px; }"], + isActive: true, + }, +]; diff --git a/devtools/server/tests/chrome/inactive-property-helper/vertical-align.mjs b/devtools/server/tests/chrome/inactive-property-helper/vertical-align.mjs new file mode 100644 index 0000000000..e9873d4865 --- /dev/null +++ b/devtools/server/tests/chrome/inactive-property-helper/vertical-align.mjs @@ -0,0 +1,56 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// InactivePropertyHelper `vertical-align` test cases. +export default [ + { + info: "vertical-align is inactive on a block element", + property: "vertical-align", + tagName: "div", + rules: ["div { vertical-align: top; }"], + isActive: false, + }, + { + info: "vertical-align is inactive on a span with display block", + property: "vertical-align", + tagName: "span", + rules: ["span { vertical-align: top; display: block;}"], + isActive: false, + }, + { + info: "vertical-align is active on a div with display inline-block", + property: "vertical-align", + tagName: "div", + rules: ["div { vertical-align: top; display: inline-block;}"], + isActive: true, + }, + { + info: "vertical-align is active on a table-cell", + property: "vertical-align", + tagName: "div", + rules: ["div { vertical-align: top; display: table-cell;}"], + isActive: true, + }, + { + info: "vertical-align is active on a block element ::first-letter", + property: "vertical-align", + tagName: "div", + rules: ["div::first-letter { vertical-align: top; }"], + isActive: true, + }, + { + info: "vertical-align is active on a block element ::first-line", + property: "vertical-align", + tagName: "div", + rules: ["div::first-line { vertical-align: top; }"], + isActive: true, + }, + { + info: "vertical-align is active on an inline element", + property: "vertical-align", + tagName: "span", + rules: ["span { vertical-align: top; }"], + isActive: true, + }, +]; diff --git a/devtools/server/tests/chrome/inactive-property-helper/width-height-ruby.mjs b/devtools/server/tests/chrome/inactive-property-helper/width-height-ruby.mjs new file mode 100644 index 0000000000..0dda222e0b --- /dev/null +++ b/devtools/server/tests/chrome/inactive-property-helper/width-height-ruby.mjs @@ -0,0 +1,147 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// InactivePropertyHelper `width & height on ruby element` test cases. +export default [ + { + info: "width is inactive on ruby element", + property: "width", + tagName: "ruby", + rules: ["ruby { width: 10px; }"], + isActive: false, + }, + { + info: "min-width is inactive on ruby element", + property: "min-width", + tagName: "ruby", + rules: ["ruby { min-width: 10px; }"], + isActive: false, + }, + { + info: "max-width is inactive on ruby element", + property: "max-width", + tagName: "ruby", + rules: ["ruby { max-width: 10px; }"], + isActive: false, + }, + { + info: "height is inactive on ruby element", + property: "height", + tagName: "ruby", + rules: ["ruby { height: 10px; }"], + isActive: false, + }, + { + info: "min-height is inactive on ruby element", + property: "min-height", + tagName: "ruby", + rules: ["ruby { min-height: 10px; }"], + isActive: false, + }, + { + info: "max-height is inactive on ruby element", + property: "max-height", + tagName: "ruby", + rules: ["ruby { max-height: 10px; }"], + isActive: false, + }, + { + info: "width is active on div element", + property: "width", + tagName: "div", + rules: ["div { width: 10px; }"], + isActive: true, + }, + { + info: "min-width is active on div element", + property: "min-width", + tagName: "div", + rules: ["div { min-width: 10px; }"], + isActive: true, + }, + { + info: "max-width is active on div element", + property: "max-width", + tagName: "div", + rules: ["div { max-width: 10px; }"], + isActive: true, + }, + { + info: "height is active on div element", + property: "height", + tagName: "div", + rules: ["div { height: 10px; }"], + isActive: true, + }, + { + info: "min-height is active on div element", + property: "min-height", + tagName: "div", + rules: ["div { min-height: 10px; }"], + isActive: true, + }, + { + info: "max-height is active on div element", + property: "max-height", + tagName: "div", + rules: ["div { max-height: 10px; }"], + isActive: true, + }, + { + info: "width is inactive on div element with display ruby", + property: "width", + tagName: "div", + rules: ["div { width: 10px; display: ruby;}"], + isActive: false, + }, + { + info: "height is inactive on div element with display ruby", + property: "height", + tagName: "div", + rules: ["div { height: 10px; display: ruby;}"], + isActive: false, + }, + { + info: "width is active on ruby element with display block", + property: "width", + tagName: "ruby", + rules: ["ruby { width: 10px; display: block;}"], + isActive: true, + }, + { + info: "height is active on ruby element with display block", + property: "height", + tagName: "ruby", + rules: ["ruby { height: 10px; display: block;}"], + isActive: true, + }, + { + info: "width is inactive on ruby-text element", + property: "width", + tagName: "rt", + rules: ["rt { width: 10px;}"], + isActive: false, + }, + { + info: "height is inactive on ruby-text element", + property: "height", + tagName: "rt", + rules: ["rt { height: 10px;}"], + isActive: false, + }, + { + info: "width is inactive on div elements with display ruby-text", + property: "width", + tagName: "div", + rules: ["div { width: 10px; display: ruby-text;}"], + isActive: false, + }, + { + info: "height is inactive on div elements with display ruby-text", + property: "height", + tagName: "div", + rules: ["div { height: 10px; display: ruby-text;}"], + isActive: false, + }, +]; diff --git a/devtools/server/tests/chrome/inspector-delay-image-response.sjs b/devtools/server/tests/chrome/inspector-delay-image-response.sjs new file mode 100644 index 0000000000..633d7e3aa6 --- /dev/null +++ b/devtools/server/tests/chrome/inspector-delay-image-response.sjs @@ -0,0 +1,46 @@ +/** + * Adapted from https://searchfox.org/mozilla-central/source/layout/reftests/backgrounds/delay-image-response.sjs + */ +"use strict"; + +// A 1x1 PNG image. +// Source: https://commons.wikimedia.org/wiki/File:1x1.png (Public Domain) +const IMAGE = atob( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAA" + + "ACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=" +); + +// To avoid GC. +let timer = null; + +function handleRequest(request, response) { + const query = {}; + request.queryString.split("&").forEach(function (val) { + const [name, value] = val.split("="); + query[name] = unescape(value); + }); + + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "image/png", false); + + // If there is no delay, we write the image and leave. + if (!("delay" in query)) { + response.write(IMAGE); + return; + } + + // If there is a delay, we create a timer which, when it fires, will write + // image and leave. + response.processAsync(); + const nsITimer = Ci.nsITimer; + + timer = Cc["@mozilla.org/timer;1"].createInstance(nsITimer); + timer.initWithCallback( + function () { + response.write(IMAGE); + response.finish(); + }, + query.delay, + nsITimer.TYPE_ONE_SHOT + ); +} diff --git a/devtools/server/tests/chrome/inspector-eyedropper.html b/devtools/server/tests/chrome/inspector-eyedropper.html new file mode 100644 index 0000000000..f5bd3a1cb8 --- /dev/null +++ b/devtools/server/tests/chrome/inspector-eyedropper.html @@ -0,0 +1,20 @@ +<html> +<head> + <meta charset="UTF-8"> + <title>Inspector Eyedropper tests</title> + <style> + html { + background: black; + } + </style> + <script type="text/javascript"> + "use strict"; + + window.onload = function() { + window.opener.postMessage("ready", "*"); + }; + </script> +</head> +</body> +</body> +</html> diff --git a/devtools/server/tests/chrome/inspector-helpers.js b/devtools/server/tests/chrome/inspector-helpers.js new file mode 100644 index 0000000000..0b7edd8035 --- /dev/null +++ b/devtools/server/tests/chrome/inspector-helpers.js @@ -0,0 +1,133 @@ +/* exported attachURL, promiseDone, + promiseOnce, + addTest, addAsyncTest, + runNextTest, _documentWalker */ +"use strict"; + +const { require } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/Loader.sys.mjs" +); +const { + CommandsFactory, +} = require("resource://devtools/shared/commands/commands-factory.js"); +const { + DevToolsServer, +} = require("resource://devtools/server/devtools-server.js"); +const { BrowserTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/BrowserTestUtils.sys.mjs" +); +const { + DocumentWalker: _documentWalker, +} = require("resource://devtools/server/actors/inspector/document-walker.js"); + +// Always log packets when running tests. +Services.prefs.setBoolPref("devtools.debugger.log", true); +SimpleTest.registerCleanupFunction(function () { + Services.prefs.clearUserPref("devtools.debugger.log"); +}); + +if (!DevToolsServer.initialized) { + DevToolsServer.init(); + DevToolsServer.registerAllActors(); + SimpleTest.registerCleanupFunction(function () { + DevToolsServer.destroy(); + }); +} + +var gAttachCleanups = []; + +SimpleTest.registerCleanupFunction(function () { + for (const cleanup of gAttachCleanups) { + cleanup(); + } +}); + +/** + * Open a tab, load the url, wait for it to signal its readiness, + * connect to this tab via DevTools protocol and return. + * + * Returns an object with a few helpful attributes: + * - commands {Object}: The commands object defined by modules from devtools/shared/commands + * - target {TargetFront}: The current top-level target front. + * - doc {HtmlDocument}: the tab's document that got opened + */ +async function attachURL(url) { + // Get the current browser window + const gBrowser = + Services.wm.getMostRecentWindow("navigator:browser").gBrowser; + + // open the url in a new tab, save a reference to the new inner window global object + // and wait for it to load. The tests rely on this window object to send a "ready" + // event to its opener (the test page). This window reference is used within + // the test tab, to reference the webpage being tested against, which is in another + // tab. + const windowOpened = BrowserTestUtils.waitForNewTab(gBrowser, url); + const win = window.open(url, "_blank"); + await windowOpened; + + const commands = await CommandsFactory.forTab(gBrowser.selectedTab); + await commands.targetCommand.startListening(); + + const cleanup = async function () { + await commands.destroy(); + if (win) { + win.close(); + } + }; + + gAttachCleanups.push(cleanup); + return { + commands, + target: commands.targetCommand.targetFront, + doc: win.document, + }; +} + +function promiseOnce(target, event) { + return new Promise(resolve => { + target.on(event, (...args) => { + if (args.length === 1) { + resolve(args[0]); + } else { + resolve(args); + } + }); + }); +} + +function promiseDone(currentPromise) { + currentPromise.catch(err => { + ok(false, "Promise failed: " + err); + if (err.stack) { + dump(err.stack); + } + SimpleTest.finish(); + }); +} + +var _tests = []; +function addTest(test) { + _tests.push(test); +} + +function addAsyncTest(generator) { + _tests.push(() => generator().catch(ok.bind(null, false))); +} + +function runNextTest() { + if (!_tests.length) { + SimpleTest.finish(); + return; + } + const fn = _tests.shift(); + try { + fn(); + } catch (ex) { + info( + "Test function " + + (fn.name ? "'" + fn.name + "' " : "") + + "threw an exception: " + + ex + ); + } +} diff --git a/devtools/server/tests/chrome/inspector-search-data.html b/devtools/server/tests/chrome/inspector-search-data.html new file mode 100644 index 0000000000..784dcb7c9b --- /dev/null +++ b/devtools/server/tests/chrome/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/chrome/inspector-styles-data.css b/devtools/server/tests/chrome/inspector-styles-data.css new file mode 100644 index 0000000000..5c3652f522 --- /dev/null +++ b/devtools/server/tests/chrome/inspector-styles-data.css @@ -0,0 +1,3 @@ +.external-rule { + cursor: crosshair; +} diff --git a/devtools/server/tests/chrome/inspector-styles-data.html b/devtools/server/tests/chrome/inspector-styles-data.html new file mode 100644 index 0000000000..334b268bfd --- /dev/null +++ b/devtools/server/tests/chrome/inspector-styles-data.html @@ -0,0 +1,85 @@ +<html> +<script> + "use strict"; + + window.onload = () => { + window.opener.postMessage("ready", "*"); + }; +</script> +<style> + .inheritable-rule { + font-size: 15px; + } + /* Has to be on one line, is such for test */ + .column-rule { font-size: 20px; } .column-rule { font-size: 25px; } + .uninheritable-rule { + background-color: #f06; + } + @media screen { + #mediaqueried { + background-color: #f06; + } + } + #svgcontent rect { + fill: rgb(1,2,3); + } + + #layout-element, + #layout-auto-margin-element { + width: 50px; + height: 50px; + padding: 3px 5px 7px 5px; + border: 5px solid red; + margin: 10px 20px 30px 0; + box-sizing: border-box; + position: absolute; + z-index: 2; + } + + #layout-auto-margin-element { + margin: 10px auto; + } +</style> +<link type="text/css" rel="stylesheet" href="inspector-styles-data.css"></link> +<body> + <h1>Style Actor Tests</h1> + <!-- Inheritance checks --> + <div id="inheritable-rule-uninheritable-style" class="inheritable-rule" style="background-color: purple"> + <div id="inheritable-rule-inheritable-style" class="inheritable-rule" style="color: blue"> + <div id="uninheritable-rule-uninheritable-style" class="uninheritable-rule" style="background-color: green"> + <div id="uninheritable-rule-inheritable-style" class="uninheritable-rule" style="color: red"> + <div id="test-node"> + Here is the test node. + </div> + </div> + </div> + </div> + </div> + + <!-- Computed checks --> + <div id="computed-parent" class="external-rule inheritable-rule uninheritable-rule" style="color: red;"> + <div id="computed-test-node" class="external-rule"> + Here is the test node. + </div> + </div> + + <!-- Matched checks --> + <div id="matched-parent" class="external-rule inheritable-rule column-rule uninheritable-rule" style="color: red;"> + <div id="matched-test-node" style="font-size: 10px" class="external-rule"> + Here is the test node. + </div> + </div> + + <div id="mediaqueried"> + Screen mediaqueried. + </div> + + <div id="svgcontent"> + <svg><rect></rect></svg> + </div> + + <div id="layout-element">I can has layout</div> + <div id="layout-auto-margin-element">I can has layout too</div> + +</body> +</html> diff --git a/devtools/server/tests/chrome/inspector-template.html b/devtools/server/tests/chrome/inspector-template.html new file mode 100644 index 0000000000..13c9d5c7d3 --- /dev/null +++ b/devtools/server/tests/chrome/inspector-template.html @@ -0,0 +1,17 @@ +<html> +<body> + <template> + <p>template content</p> + </template> + <div></div> + <script> + "use strict"; + + const template = document.querySelector("template"); + const clone = document.importNode(template.content, true); + document.querySelector("div").appendChild(clone); + + window.opener.postMessage("ready", "*"); + </script> +</body> +</html> diff --git a/devtools/server/tests/chrome/inspector-traversal-data.html b/devtools/server/tests/chrome/inspector-traversal-data.html new file mode 100644 index 0000000000..e294796467 --- /dev/null +++ b/devtools/server/tests/chrome/inspector-traversal-data.html @@ -0,0 +1,103 @@ +<!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"); + if (host.attachShadow) { + 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.onload = function() { + window.opener.postMessage("ready", "*"); + }; + 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/chrome/inspector_css-properties.html b/devtools/server/tests/chrome/inspector_css-properties.html new file mode 100644 index 0000000000..8cc6368cd1 --- /dev/null +++ b/devtools/server/tests/chrome/inspector_css-properties.html @@ -0,0 +1,12 @@ +<html> +<head> +<body> + <script type="text/javascript"> + "use strict"; + + window.onload = function() { + window.opener.postMessage("ready", "*"); + }; + </script> +</body> +</html> diff --git a/devtools/server/tests/chrome/inspector_display-type.html b/devtools/server/tests/chrome/inspector_display-type.html new file mode 100644 index 0000000000..7bd0da6709 --- /dev/null +++ b/devtools/server/tests/chrome/inspector_display-type.html @@ -0,0 +1,17 @@ +<html> +<head> +<body> + <div id="inline-block" style="display: inline-block"> + HELLO WORLD + </div> + <div id="grid" style="display: grid"></div> + <div id="block" style="position: fixed"></div> + <script> + "use strict"; + + window.onload = () => { + window.opener.postMessage("ready", "*"); + }; + </script> +</body> +</html> diff --git a/devtools/server/tests/chrome/inspector_getImageData.html b/devtools/server/tests/chrome/inspector_getImageData.html new file mode 100644 index 0000000000..754798df44 --- /dev/null +++ b/devtools/server/tests/chrome/inspector_getImageData.html @@ -0,0 +1,23 @@ +<html> +<head> +<body> + <img class="custom"> + <img class="big-horizontal" src="large-image.jpg" style="width:500px;"> + <canvas class="big-vertical" style="width:500px;"></canvas> + <img class="small" src="small-image.gif"> + <img class="data" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII="> + <script> + "use strict"; + + window.onload = () => { + const canvas = document.querySelector("canvas"), ctx = canvas.getContext("2d"); + canvas.width = 1000; + canvas.height = 2000; + ctx.fillStyle = "red"; + ctx.fillRect(0, 0, 1000, 2000); + + window.opener.postMessage("ready", "*"); + }; + </script> +</body> +</html> diff --git a/devtools/server/tests/chrome/inspector_getOffsetParent.html b/devtools/server/tests/chrome/inspector_getOffsetParent.html new file mode 100644 index 0000000000..72aac5f70b --- /dev/null +++ b/devtools/server/tests/chrome/inspector_getOffsetParent.html @@ -0,0 +1,18 @@ +<html> +<head> +<body> + <div id="relative_parent" style="position: relative"> + <div id="absolute_child" style="position: absolute"></div> + </div> + <div id="static"></div> + <div id="no_parent" style="position: absolute"></div> + <div id="fixed" style="position: fixed"></div> + <script> + "use strict"; + + window.onload = () => { + window.opener.postMessage("ready", "*"); + }; + </script> +</body> +</html> diff --git a/devtools/server/tests/chrome/large-image.jpg b/devtools/server/tests/chrome/large-image.jpg Binary files differnew file mode 100644 index 0000000000..bda383e594 --- /dev/null +++ b/devtools/server/tests/chrome/large-image.jpg diff --git a/devtools/server/tests/chrome/memory-helpers.js b/devtools/server/tests/chrome/memory-helpers.js new file mode 100644 index 0000000000..e4db689134 --- /dev/null +++ b/devtools/server/tests/chrome/memory-helpers.js @@ -0,0 +1,72 @@ +/* exported Task, startServerAndGetSelectedTabMemory, destroyServerAndFinish, + waitForTime, waitUntil */ +"use strict"; + +const { require } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/Loader.sys.mjs" +); +const { + CommandsFactory, +} = require("resource://devtools/shared/commands/commands-factory.js"); + +// Always log packets when running tests. +Services.prefs.setBoolPref("devtools.debugger.log", true); +var gReduceTimePrecision = Services.prefs.getBoolPref( + "privacy.reduceTimerPrecision" +); +Services.prefs.setBoolPref("privacy.reduceTimerPrecision", false); +SimpleTest.registerCleanupFunction(function () { + Services.prefs.clearUserPref("devtools.debugger.log"); + Services.prefs.setBoolPref( + "privacy.reduceTimerPrecision", + gReduceTimePrecision + ); +}); + +async function getTargetForSelectedTab() { + const browserWindow = Services.wm.getMostRecentWindow("navigator:browser"); + const commands = await CommandsFactory.forTab( + browserWindow.gBrowser.selectedTab + ); + await commands.targetCommand.startListening(); + const isEveryFrameTargetEnabled = Services.prefs.getBoolPref( + "devtools.every-frame-target.enabled", + false + ); + if (!isEveryFrameTargetEnabled) { + return commands.targetCommand.targetFront; + } + + // If EFT is enabled, we need to retrieve the target of the test document + const targets = await commands.targetCommand.getAllTargets([ + commands.targetCommand.TYPES.FRAME, + ]); + + return targets.find(t => t.url !== "chrome://mochikit/content/harness.xhtml"); +} + +async function startServerAndGetSelectedTabMemory() { + const target = await getTargetForSelectedTab(); + const memory = await target.getFront("memory"); + return { memory, target }; +} + +async function destroyServerAndFinish(target) { + await target.destroy(); + SimpleTest.finish(); +} + +function waitForTime(ms) { + return new Promise((resolve, reject) => { + setTimeout(resolve, ms); + }); +} + +function waitUntil(predicate) { + if (predicate()) { + return Promise.resolve(true); + } + return new Promise(resolve => + setTimeout(() => waitUntil(predicate).then(() => resolve(true)), 10) + ); +} diff --git a/devtools/server/tests/chrome/nonchrome_unsafeDereference.html b/devtools/server/tests/chrome/nonchrome_unsafeDereference.html new file mode 100644 index 0000000000..15e9fd9160 --- /dev/null +++ b/devtools/server/tests/chrome/nonchrome_unsafeDereference.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> +<script> +"use strict"; + +var xhr = new XMLHttpRequest(); +xhr.timeout = 1742; +xhr.expando = "Expando!"; +</script> +</html> diff --git a/devtools/server/tests/chrome/small-image.gif b/devtools/server/tests/chrome/small-image.gif Binary files differnew file mode 100644 index 0000000000..e702427a53 --- /dev/null +++ b/devtools/server/tests/chrome/small-image.gif diff --git a/devtools/server/tests/chrome/suspendTimeouts_content.html b/devtools/server/tests/chrome/suspendTimeouts_content.html new file mode 100644 index 0000000000..f3969fc10c --- /dev/null +++ b/devtools/server/tests/chrome/suspendTimeouts_content.html @@ -0,0 +1 @@ +<script src='suspendTimeouts_content.js'></script> diff --git a/devtools/server/tests/chrome/suspendTimeouts_content.js b/devtools/server/tests/chrome/suspendTimeouts_content.js new file mode 100644 index 0000000000..cb41653cff --- /dev/null +++ b/devtools/server/tests/chrome/suspendTimeouts_content.js @@ -0,0 +1,75 @@ +"use strict"; + +// To make it easier to follow, this code is arranged so that the functions are +// arranged in the order they are called. + +const worker = new Worker("suspendTimeouts_worker.js"); +worker.onerror = error => { + const message = `error from worker: ${error.filename}:${error.lineno}: ${error.message}`; + throw new Error(message); +}; + +// Create a message channel. Send one end to the worker, and return the other to +// the mochitest. +/* exported create_channel */ +function create_channel() { + const { port1, port2 } = new MessageChannel(); + info(`sending port to worker`); + worker.postMessage({ mochitestPort: port1 }, [port1]); + return port2; +} + +// Provoke the worker into sending us a message, and then refuse to receive said +// message, causing it to be delayed for later delivery. +// +// The worker will also post a message to the MessagePort we sent it earlier. +// That message should not be delayed, as it is handled by the mochitest window, +// not the content window. Its receipt signals that the test can assume that the +// runnable for step 2) is in the main thread's event queue, so the test can +// prepare for step 3). +/* exported start_worker */ +function start_worker() { + worker.onmessage = handle_echo; + + // This should prevent worker.onmessage from being called, until + // resumeTimeouts is called. + // + // This function is provided by test_suspendTimeouts.js. + // eslint-disable-next-line no-undef + suspendTimeouts(); + + // The worker should echo this message back to us and to the mochitest. + worker.postMessage("HALLOOOOOO"); // suitable message for echoing + info(`posted message to worker`); +} + +var resumeTimeouts_has_returned = false; + +// Resume timeouts. After this call, the worker's message should not be +// delivered to our onmessage handler until control returns to the event loop. +/* exported resume_timeouts */ +function resume_timeouts() { + // This function is provided by test_suspendTimeouts.js. + // eslint-disable-next-line no-undef + resumeTimeouts(); // onmessage handlers should not run from this call. + + resumeTimeouts_has_returned = true; + + // When this JavaScript invocation returns to the main thread's event loop, + // only then should onmessage handlers be invoked. +} + +// The buggy code calls this handler from the resumeTimeouts call, before the +// main thread returns to the event loop. The correct code calls this only once +// the JavaScript invocation that called resumeTimeouts has run to completion. +function handle_echo({ data }) { + ok( + resumeTimeouts_has_returned, + "worker message delivered from main event loop" + ); + + // Finish the mochitest. + // This function is set and defined by test_suspendTimeouts.js + // eslint-disable-next-line no-undef + finish(); +} diff --git a/devtools/server/tests/chrome/suspendTimeouts_worker.js b/devtools/server/tests/chrome/suspendTimeouts_worker.js new file mode 100644 index 0000000000..e008f7d0d3 --- /dev/null +++ b/devtools/server/tests/chrome/suspendTimeouts_worker.js @@ -0,0 +1,12 @@ +"use strict"; + +// Once content sends us a port connected to the mochitest, we simply echo every +// message we receive back to content and the mochitest. +onmessage = ({ data: { mochitestPort } }) => { + onmessage = ({ data }) => { + // Send a message to both content and the mochitest, which the main thread's + // event loop will attempt to deliver as step 2). + postMessage(`worker echo to content: ${data}`); + mochitestPort.postMessage(`worker echo to port: ${data}`); + }; +}; diff --git a/devtools/server/tests/chrome/test_Debugger.Script.prototype.global.html b/devtools/server/tests/chrome/test_Debugger.Script.prototype.global.html new file mode 100644 index 0000000000..d403d6b4a3 --- /dev/null +++ b/devtools/server/tests/chrome/test_Debugger.Script.prototype.global.html @@ -0,0 +1,48 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=958646 + +Debugger.Script.prototype.global should return innerize globals, not WindowProxies. +--> +<head> + <meta charset="utf-8"> + <title>Debugger.Script.prototype.global should return inner windows</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +</head> +<body> +<pre id="test"> +<script> +"use strict"; + +const {addDebuggerToGlobal} = ChromeUtils.importESModule("resource://gre/modules/jsdebugger.sys.mjs"); +addDebuggerToGlobal(globalThis); + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + + const iframe = document.createElement("iframe"); + iframe.src = "data:text/html,<script>function glorp() { }<\/script>"; + iframe.onload = firstOnLoadHandler; + document.body.appendChild(iframe); + + function firstOnLoadHandler() { + const dbg = new Debugger(); + const iframeDO = dbg.addDebuggee(iframe.contentWindow); + + // For sanity: check that the debuggee global is the inner window, + // and that the outer window gets a distinct D.O. + const iframeWindowProxyDO = iframeDO.makeDebuggeeValue(iframe.contentWindow); + ok(iframeDO !== iframeWindowProxyDO); + + // The real test: Debugger.Script.prototype.global returns inner windows. + ok(iframeDO.getOwnPropertyDescriptor("glorp").value.script.global === iframeDO); + + SimpleTest.finish(); + } +}; +</script> +</pre> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_Debugger.Source.prototype.elementAttribute.html b/devtools/server/tests/chrome/test_Debugger.Source.prototype.elementAttribute.html new file mode 100644 index 0000000000..cb9c2bbcdc --- /dev/null +++ b/devtools/server/tests/chrome/test_Debugger.Source.prototype.elementAttribute.html @@ -0,0 +1,159 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=941876 + +Debugger.Source.prototype.element and .elementAttributeName should report the DOM +element to which code is attached (if any), and how. +--> +<head> + <meta charset="utf-8"> + <title>Debugger.Source.prototype.element should return owning element</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +</head> +<body> +<pre id="test"> +<script> +"use strict"; + +const {addSandboxedDebuggerToGlobal} = ChromeUtils.importESModule("resource://gre/modules/jsdebugger.sys.mjs"); +addSandboxedDebuggerToGlobal(globalThis); + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + + let log = ""; + let doc, dieter, ulrich, isolde, albrecht; + let dbg, iframeDO; + + // Create an iframe to debug. + // We can't use a data: URL here, because we want to test script elements + // that refer to the JavaScript via 'src' attributes, and data: documents + // can't refer to those. So we use a separate HTML document. + const iframe = document.createElement("iframe"); + iframe.src = "Debugger.Source.prototype.element.html"; + iframe.onload = onLoadHandler; + document.body.appendChild(iframe); + + function onLoadHandler() { + log += "l"; + + // Now that the iframe's window has been created, we can add + // it as a debuggee. + dbg = new Debugger(); + dbg.onDebuggerStatement = franzDebuggerHandler; + iframeDO = dbg.addDebuggee(iframe.contentWindow); + iframeDO.makeDebuggeeValue.bind(iframeDO); + + // Send a click event to heidi. + doc = iframe.contentWindow.document; + doc.getElementById("heidi").dispatchEvent(new Event("click")); + } + + function franzDebuggerHandler(frame) { + log += "f"; + + // The top stack frame should be franz, belonging to the script element. + ok(frame.callee.displayName === "franz", "top frame is franz"); + ok(frame.script.source.elementAttributeName === undefined, + "top frame source doesn't belong to an attribute"); + + // The second stack frame should belong to heinrich. + ok(frame.older.script.source.elementAttributeName === undefined, + "second frame source doesn't belong to an attribute"); + + // The next stack frame should belong to heidi's onclick handler. + ok(frame.older.older.script.source.elementAttributeName === "onclick", + "third frame source belongs to 'onclick' attribute"); + + // Try a dynamically inserted inline script element. + ulrich = doc.createElement("script"); + ulrich.text = "debugger;"; + dbg.onDebuggerStatement = ulrichDebuggerHandler; + doc.body.appendChild(ulrich); + } + + function ulrichDebuggerHandler(frame) { + log += "u"; + + // The top frame should be ulrich's text. + ok(frame.script.source.elementAttributeName === undefined, + "top frame is not on an attribute of ulrich"); + + // Try a dynamically inserted out-of-line script element. + isolde = doc.createElement("script"); + isolde.setAttribute("src", "Debugger.Source.prototype.element-2.js"); + isolde.setAttribute("id", "idolde, my dear"); + dbg.onDebuggerStatement = isoldeDebuggerHandler; + doc.body.appendChild(isolde); + } + + function isoldeDebuggerHandler(frame) { + log += "i"; + + ok(frame.script.source.elementAttributeName === undefined, + "top frame source is not an attribute of isolde"); + info("frame.script.source.elementAttributeName is: " + + uneval(frame.script.source.elementAttributeName)); + + // Try a dynamically created div element with a handler. + dieter = doc.createElement("div"); + dieter.setAttribute("id", "dieter"); + dieter.setAttribute("ondrag", "debugger;"); + dbg.onDebuggerStatement = dieterDebuggerHandler; + dieter.dispatchEvent(new Event("drag")); + } + + function dieterDebuggerHandler(frame) { + log += "d"; + + // The top frame should belong to dieter's ondrag handler. + ok(frame.script.source.elementAttributeName === "ondrag", + "second event's handler is on dieter's 'ondrag' element"); + + // Try sending an 'onresize' event to the window. + // + // Note that we only want Debugger to see the events we send, not any + // genuine resize events accidentally generated by the test harness (see bug + // 1162067). So we mark our events as cancelable; that seems to be the only + // bit chrome can fiddle on an Event that content code will see and that + // won't affect propagation. Then, the content event only runs its + // 'debugger' statement when the event is cancelable. It's a kludge. + dbg.onDebuggerStatement = resizeDebuggerHandler; + iframe.contentWindow.dispatchEvent(new Event("resize", { cancelable: true })); + } + + function resizeDebuggerHandler(frame) { + log += "e"; + + // The top frame should belong to the body's 'onresize' handler, even + // though we sent the message to the window and it was handled. + ok(frame.script.source.elementAttributeName === "onresize", + "onresize event handler is on body element's 'onresize' attribute"); + + // In SVG, the event and the attribute that holds that event's handler + // have different names. Debugger.Source.prototype.elementAttributeName + // should report (as one might infer) the attribute name, not the event + // name. + albrecht = doc.createElementNS("http://www.w3.org/2000/svg", "svg"); + albrecht.setAttribute("onload", "debugger;"); + dbg.onDebuggerStatement = SVGLoadHandler; + albrecht.dispatchEvent(new Event("SVGLoad")); + } + + function SVGLoadHandler(frame) { + log += "s"; + + // The top frame's source should be on albrecht's 'onload' attribute. + ok(frame.script.source.elementAttributeName === "onload", + "SVGLoad event handler is on albrecht's 'onload' attribute"); + + ok(log === "lfuides", "all tests actually ran"); + SimpleTest.finish(); + } +}; +</script> +</pre> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_Debugger.Source.prototype.introductionScript.html b/devtools/server/tests/chrome/test_Debugger.Source.prototype.introductionScript.html new file mode 100644 index 0000000000..09c23b5253 --- /dev/null +++ b/devtools/server/tests/chrome/test_Debugger.Source.prototype.introductionScript.html @@ -0,0 +1,96 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=969786 + +Debugger.Source.prototype.introductionScript and .introductionOffset should +behave when 'eval' is called with no scripted frames active at all. +--> +<head> + <meta charset="utf-8"> + <title>Debugger.Source.prototype.introductionScript with no caller</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +</head> +<body> +<pre id="test"> +<script> +"use strict"; + +const {addDebuggerToGlobal} = ChromeUtils.importESModule("resource://gre/modules/jsdebugger.sys.mjs"); +addDebuggerToGlobal(globalThis); + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + + let dbg, iframeDO, doc; + + // Create an iframe to debug. + const iframe = document.createElement("iframe"); + iframe.src = "data:text/html,<div>Hi!</div>"; + iframe.onload = onLoadHandler; + document.body.appendChild(iframe); + + function onLoadHandler() { + // Now that the iframe's window has been created, we can add + // it as a debuggee. + dbg = new Debugger(); + iframeDO = dbg.addDebuggee(iframe.contentWindow); + + doc = iframe.contentWindow.document; + const script = doc.createElement("script"); + script.text = "setTimeout(eval.bind(null, 'debugger;'), 0);"; + dbg.onDebuggerStatement = timerHandler; + doc.body.appendChild(script); + } + + function timerHandler(frame) { + // The top stack frame's source should have an undefined + // introduction script and introduction offset. + const source = frame.script.source; + ok(source.introductionScript === undefined, + "setTimeout eval introductionScript is undefined"); + ok(source.introductionOffset === undefined, + "setTimeout eval introductionOffset is undefined"); + + // Check that the above isn't just some quirk of iframes, or the + // browser milieu destroying information: an eval script should indeed + // have proper introduction information. + const script2 = doc.createElement("script"); + script2.text = "eval('debugger;');"; + iframeDO.makeDebuggeeValue(script2); + + dbg.onDebuggerStatement = evalHandler; + doc.body.appendChild(script2); + } + + function evalHandler(frame) { + // The top stack frame's source should be introduced by the script that + // called eval. + const source = frame.script.source; + const frame2 = frame.older; + const frame3 = frame2.older; + + ok(source.introductionType === "eval", + "top frame's source was introduced by 'eval'"); + ok(source.introductionScript === frame2.script, + "eval frame's introduction script is the older frame's script"); + ok(source.introductionOffset === frame2.offset, + "eval frame's introduction offset is current offset in older frame"); + + // The frame that called eval, in turn, was introduced at the call that + // inserted the script element into the document. + ok(frame2.script.source.introductionType === "injectedScript", + "older frame has no introduction type"); + ok(frame2.script.source.introductionScript === frame3.script, + "older frame has introduction script"); + ok(frame2.script.source.introductionOffset === frame3.offset, + "older frame has introduction offset"); + + SimpleTest.finish(); + } +}; +</script> +</pre> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_Debugger.Source.prototype.introductionType.html b/devtools/server/tests/chrome/test_Debugger.Source.prototype.introductionType.html new file mode 100644 index 0000000000..1057d4b94f --- /dev/null +++ b/devtools/server/tests/chrome/test_Debugger.Source.prototype.introductionType.html @@ -0,0 +1,159 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=935203 + +Debugger.Source.prototype.introductionType should return 'eventHandler' for +JavaScrip appearing in an inline event handler attribute. +--> +<head> + <meta charset="utf-8"> + <title>Debugger.Source.prototype.introductionType should identify event handlers</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="inspector-helpers.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +</head> +<body> +<pre id="test"> +<script> +"use strict"; + +const {addSandboxedDebuggerToGlobal} = ChromeUtils.importESModule("resource://gre/modules/jsdebugger.sys.mjs"); +addSandboxedDebuggerToGlobal(globalThis); + +let dbg; +let iframeDO, doc; +let Tootles; + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + runNextTest(); +}; + +addTest(function setup() { + // Create an iframe to debug. + const iframe = document.createElement("iframe"); + iframe.srcdoc = "<div id='Tootles' onclick='debugger;'>I'm a DIV!</div>" + + "<script id='Auddie'>function auddie() { debugger; }<\/script>"; + iframe.onload = onLoadHandler; + document.body.appendChild(iframe); + + function onLoadHandler() { + // Now that the iframe's window has been created, we can add + // it as a debuggee. + dbg = new Debugger(); + iframeDO = dbg.addDebuggee(iframe.contentWindow); + doc = iframe.contentWindow.document; + Tootles = doc.getElementById("Tootles"); + iframeDO.makeDebuggeeValue(Tootles); + + runNextTest(); + } +}); + +// Check the introduction type of in-markup event handler code. +// Send a click event to Tootles, whose handler has a 'debugger' statement, +// and check that script's introduction type. +addTest(function ClickOnTootles() { + dbg.onDebuggerStatement = TootlesClickDebugger; + Tootles.dispatchEvent(new Event("click")); + + function TootlesClickDebugger(frame) { + // some sanity checks + is(frame.script.source.elementAttributeName, "onclick", + "top frame source belongs to 'onclick' attribute"); + + // And, the actual point of this test: + is(frame.script.source.introductionType, "eventHandler", + "top frame source's introductionType is 'eventHandler'"); + + runNextTest(); + } +}); + +// Check the introduction type of dynamically added event handler code. +// Add a drag event handler to Tootles as a string, and then send +// Tootles a drag event. +addTest(function DragTootles() { + dbg.onDebuggerStatement = TootlesDragDebugger; + Tootles.setAttribute("ondrag", "debugger;"); + Tootles.dispatchEvent(new Event("drag")); + + function TootlesDragDebugger(frame) { + // sanity checks + is(frame.script.source.elementAttributeName, "ondrag", + "top frame source belongs to 'ondrag' attribute"); + + // And, the actual point of this test: + is(frame.script.source.introductionType, "eventHandler", + "top frame source's introductionType is 'eventHandler'"); + + runNextTest(); + } +}); + +// Check the introduction type of an in-markup script element. +addTest(function checkAuddie() { + const fnDO = iframeDO.getOwnPropertyDescriptor("auddie").value; + iframeDO.makeDebuggeeValue(doc.getElementById("Auddie")); + + is(fnDO.class, "Function", + "Script element 'Auddie' defined function 'auddie'."); + is(fnDO.script.source.elementAttributeName, undefined, + "Function auddie's script doesn't belong to any attribute of 'Auddie'"); + is(fnDO.script.source.introductionType, "inlineScript", + "Function auddie's script's source was introduced by a script element"); + + runNextTest(); +}); + +// Check the introduction type of a dynamically inserted script element. +addTest(function InsertRover() { + dbg.onDebuggerStatement = RoverDebugger; + const rover = doc.createElement("script"); + rover.text = "debugger;"; + doc.body.appendChild(rover); + iframeDO.makeDebuggeeValue(rover); + + function RoverDebugger(frame) { + // sanity checks + ok(frame.script.source.elementAttributeName === undefined, + "Rover script doesn't belong to an attribute of Rover"); + + // Check the introduction type. + ok(frame.script.source.introductionType === "injectedScript", + "Rover script's introduction type is 'injectedScript'"); + + runNextTest(); + } +}); + +// Creates a chrome document with a XUL script element, and check its introduction type. +addTest(function XULDocumentScript() { + const frame = document.createElement("iframe"); + frame.src = "doc_Debugger.Source.prototype.introductionType.xhtml"; + frame.onload = docLoaded; + info("Appending iframe containing a document with a XUL script tag"); + document.body.appendChild(frame); + + function docLoaded() { + info("Loaded chrome document"); + const xulFrameDO = dbg.addDebuggee(frame.contentWindow); + const xulFnDO = xulFrameDO.getOwnPropertyDescriptor("xulScriptFunc").value; + is(typeof xulFnDO, "object", "XUL script element defined 'xulScriptFunc'"); + is(xulFnDO.class, "Function", + "XUL global 'xulScriptFunc' is indeed a function"); + + // A XUL script elements' code gets shared amongst all + // instantiations of the document, so there's no specific DOM element + // we can attribute the code to. + + is(xulFnDO.script.source.introductionType, "inlineScript", + "xulScriptFunc's introduction type is 'inlineScript'"); + runNextTest(); + } +}); +</script> +</pre> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_animation-type-longhand.html b/devtools/server/tests/chrome/test_animation-type-longhand.html new file mode 100644 index 0000000000..97f5b1e469 --- /dev/null +++ b/devtools/server/tests/chrome/test_animation-type-longhand.html @@ -0,0 +1,42 @@ +<!DOCTYPE html> +<meta charset="UTF-8"> +<title>Test animation-type-longhand</title> +<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> +<body> +<script> + "use strict"; + + // This test checks the content of animation type for longhands table that + // * every longhand property is included + // * nothing else is included + // * no property is mapped to more than one animation type + window.onload = function() { + const {require} = ChromeUtils.importESModule("resource://devtools/shared/loader/Loader.sys.mjs"); + const { ANIMATION_TYPE_FOR_LONGHANDS } = + require("devtools/server/actors/animation-type-longhand"); + const InspectorUtils = SpecialPowers.InspectorUtils; + + const all_longhands = InspectorUtils.getCSSPropertyNames({ + includeShorthands: false, + includeExperimentals: true, + }); + + const unseen_longhands = new Set(all_longhands); + const seen_longhands = new Set(); + for (const [, names] of ANIMATION_TYPE_FOR_LONGHANDS) { + for (const name of names) { + ok(!seen_longhands.has(name), + `${name} should have only one animation type`); + ok(unseen_longhands.has(name), + `${name} is an unseen longhand property`); + unseen_longhands.delete(name); + seen_longhands.add(name); + } + } + is(unseen_longhands.size, 0, + "All longhands should be mapped to some animation type: " + [...unseen_longhands].join(", ")); + + SimpleTest.finish(); + }; +</script> +</body> diff --git a/devtools/server/tests/chrome/test_css-logic-specificity.html b/devtools/server/tests/chrome/test_css-logic-specificity.html new file mode 100644 index 0000000000..b5d5c76c0c --- /dev/null +++ b/devtools/server/tests/chrome/test_css-logic-specificity.html @@ -0,0 +1,84 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test that css-logic calculates CSS specificity properly +--> +<meta charset="utf-8"> +<title>Test css-logic specificity</title> +<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> +<body style="background:blue;"> +<script> + "use strict"; + + window.onload = function() { + const {require} = ChromeUtils.importESModule("resource://devtools/shared/loader/Loader.sys.mjs"); + const {CssLogic, CssSelector} = require("devtools/server/actors/inspector/css-logic"); + + const TEST_DATA = [ + {text: "*", expected: 0}, + {text: "LI", expected: 1}, + {text: "UL LI", expected: 2}, + {text: "UL OL + LI", expected: 3}, + {text: "H1 + [REL=\"up\"]", expected: 1025}, + {text: "UL OL LI.red", expected: 1027}, + {text: "LI.red.level", expected: 2049}, + {text: ".red .level", expected: 2048}, + {text: "#x34y", expected: 1048576}, + {text: "#s12:not(FOO)", expected: 1048577}, + {text: "body#home div#warning p.message", expected: 2098179}, + {text: "* body#home div#warning p.message", expected: 2098179}, + {text: "#footer :not(nav) li", expected: 1048578}, + {text: "bar:nth-child(n)", expected: 1025}, + {text: "li::marker", expected: 2}, + {text: "a:hover", expected: 1025}, + ]; + + function createDocument() { + let text = TEST_DATA.map(i=>i.text).join(","); + text = '<style>' + text + " {color:red;}</style>"; + document.body.innerHTML = text; + } + + function getExpectedSpecificity(selectorText) { + return TEST_DATA.filter(i => i.text === selectorText)[0].expected; + } + + SimpleTest.waitForExplicitFinish(); + + createDocument(); + const cssLogic = new CssLogic(); + + cssLogic.highlight(document.body); + + // There could be more stylesheets due to e.g, accessiblecaret, so find the + // right one. + info(`Sheets: ${cssLogic.sheets.map(s => s.href).join(", ")}`); + + const cssSheet = cssLogic.sheets.find(s => s.href == location.href); + const cssRule = cssSheet.domSheet.cssRules[0]; + const selectors = CssLogic.getSelectors(cssRule); + + is(selectors.length, TEST_DATA.length, "Should be the right rule"); + + info("Iterating over the test selectors: " + selectors.join(", ")); + for (let i = 0; i < selectors.length; i++) { + const selectorText = selectors[i]; + info("Testing selector " + selectorText); + + const selector = new CssSelector(cssRule, selectorText, i); + const expected = getExpectedSpecificity(selectorText); + const specificity = selector.cssRule.selectorSpecificityAt(selector.selectorIndex); + is(specificity, expected, + 'Selector "' + selectorText + '" has a specificity of ' + expected); + } + + info("Testing specificity of element.style"); + const colorProp = cssLogic.getPropertyInfo("background"); + is(colorProp.matchedSelectors[0].specificity, 0x40000000, + "Element styles have specificity of 0x40000000 (1073741824)."); + + SimpleTest.finish(); + }; + </script> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_css-logic.html b/devtools/server/tests/chrome/test_css-logic.html new file mode 100644 index 0000000000..6378f5a9e7 --- /dev/null +++ b/devtools/server/tests/chrome/test_css-logic.html @@ -0,0 +1,73 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id= +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug </title> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> + <script type="application/javascript" src="inspector-helpers.js"></script> + <script type="application/javascript"> +"use strict"; + +const {CssLogic} = require("devtools/server/actors/inspector/css-logic"); + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + runNextTest(); +}; + +addTest(function getComputedStyle() { + const node = document.querySelector("#computed-style"); + is(CssLogic.getComputedStyle(node).getPropertyValue("width"), + "50px", "Computed style on a normal node works (width)"); + is(CssLogic.getComputedStyle(node).getPropertyValue("height"), + "10px", "Computed style on a normal node works (height)"); + + const firstChild = new _documentWalker(node, window).firstChild(); + is(CssLogic.getComputedStyle(firstChild).getPropertyValue("content"), + "\"before\"", "Computed style on a ::before node works (content)"); + const lastChild = new _documentWalker(node, window).lastChild(); + is(CssLogic.getComputedStyle(lastChild).getPropertyValue("content"), + "\"after\"", "Computed style on a ::after node works (content)"); + + runNextTest(); +}); + +addTest(function getBindingElementAndPseudo() { + const node = document.querySelector("#computed-style"); + let {bindingElement, pseudo} = CssLogic.getBindingElementAndPseudo(node); + + is(bindingElement, node, + "Binding element is the node itself for a normal node"); + ok(!pseudo, "Pseudo is null for a normal node"); + + const firstChild = new _documentWalker(node, window).firstChild(); + ({ bindingElement, pseudo } = CssLogic.getBindingElementAndPseudo(firstChild)); + is(bindingElement, node, + "Binding element is the parent for a pseudo node"); + is(pseudo, "::before", "Pseudo is correct for a ::before node"); + + const lastChild = new _documentWalker(node, window).lastChild(); + ({ bindingElement, pseudo } = CssLogic.getBindingElementAndPseudo(lastChild)); + is(bindingElement, node, + "Binding element is the parent for a pseudo node"); + is(pseudo, "::after", "Pseudo is correct for a ::after node"); + + runNextTest(); +}); + + </script> +</head> +<body> + <style type="text/css"> + #computed-style { width: 50px; height: 10px; } + #computed-style::before { content: "before"; } + #computed-style::after { content: "after"; } + </style> + <div id="computed-style"></div> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_css-properties.html b/devtools/server/tests/chrome/test_css-properties.html new file mode 100644 index 0000000000..3cbc3a4aa7 --- /dev/null +++ b/devtools/server/tests/chrome/test_css-properties.html @@ -0,0 +1,72 @@ +<!DOCTYPE HTML> +<html> +<!-- +Bug 1265798 - Replace inIDOMUtils.cssPropertyIsShorthand +--> +<head> + <meta charset="utf-8"> + <title>Test CSS Properties Actor</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> + <script type="application/javascript" src="inspector-helpers.js"></script> + <script type="application/javascript"> +"use strict"; + +window.onload = function() { + function toSortedString(array) { + return JSON.stringify(array.sort()); + } + + const runCssPropertiesTests = async function(url) { + info(`Opening tab with CssPropertiesActor support.`); + // Open a new tab. The only property we are interested in is `target`. + const { target } = await attachURL(url); + const { cssProperties } = await target.getFront("cssProperties"); + + ok(cssProperties.isKnown("border"), + "The `border` shorthand property is known."); + ok(cssProperties.isKnown("display"), + "The `display` property is known."); + ok(!cssProperties.isKnown("foobar"), + "A fake property is not known."); + ok(cssProperties.isKnown("--foobar"), + "A CSS variable properly evaluates."); + ok(cssProperties.isKnown("--foob\\{ar"), + "A CSS variable with escaped character properly evaluates."); + ok(cssProperties.isKnown("--fübar"), + "A CSS variable unicode properly evaluates."); + ok(!cssProperties.isKnown("--foo bar"), + "A CSS variable with spaces fails"); + + is(toSortedString(cssProperties.getValues("margin")), + toSortedString(["auto", "inherit", "initial", "unset", "revert", "revert-layer"]), + "Can get values for the CSS margin."); + is(cssProperties.getValues("foobar").length, 0, + "Unknown values return an empty array."); + + const bgColorValues = cssProperties.getValues("background-color"); + ok(bgColorValues.includes("blanchedalmond"), + "A property with color values includes blanchedalmond."); + ok(bgColorValues.includes("papayawhip"), + "A property with color values includes papayawhip."); + ok(bgColorValues.includes("rgb"), + "A property with color values includes non-colors."); + }; + + addAsyncTest(async function setup() { + const url = document.getElementById("cssProperties").href; + await runCssPropertiesTests(url); + + runNextTest(); + }); + + SimpleTest.waitForExplicitFinish(); + runNextTest(); +}; + </script> +</head> +<body> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1265798">Mozilla Bug 1265798</a> + <a id="cssProperties" target="_blank" href="inspector_css-properties.html">Test Document</a> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_device.html b/devtools/server/tests/chrome/test_device.html new file mode 100644 index 0000000000..117e50b5ca --- /dev/null +++ b/devtools/server/tests/chrome/test_device.html @@ -0,0 +1,79 @@ +<!DOCTYPE HTML> +<html> +<!-- +Bug 895360 - [app manager] Device meta data actor +--> +<head> + <meta charset="utf-8"> + <title>Mozilla Bug</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +</head> +<body> +<pre id="test"> +<script> +"use strict"; + +window.onload = function() { + const {require} = ChromeUtils.importESModule("resource://devtools/shared/loader/Loader.sys.mjs"); + const {DevToolsClient} = require("devtools/client/devtools-client"); + const {DevToolsServer} = require("devtools/server/devtools-server"); + + SimpleTest.waitForExplicitFinish(); + + DevToolsServer.init(); + DevToolsServer.registerAllActors(); + + const client = new DevToolsClient(DevToolsServer.connectPipe()); + client.connect().then(function onConnect() { + return client.mainRoot.getFront("device"); + }).then(function(d) { + let desc; + const appInfo = Services.appinfo; + const utils = window.windowUtils; + + const localDesc = { + appid: appInfo.ID, + vendor: appInfo.vendor, + name: appInfo.name, + version: appInfo.version, + appbuildid: appInfo.appBuildID, + platformbuildid: appInfo.platformBuildID, + platformversion: appInfo.platformVersion, + geckobuildid: appInfo.platformBuildID, + geckoversion: appInfo.platformVersion, + useragent: window.navigator.userAgent, + locale: Services.locale.appLocaleAsBCP47, + os: appInfo.OS, + processor: appInfo.XPCOMABI.split("-")[0], + compiler: appInfo.XPCOMABI.split("-")[1], + dpi: utils.displayDPI, + width: window.screen.width, + height: window.screen.height, + }; + + function checkValues() { + for (const key in localDesc) { + is(desc[key], localDesc[key], "valid field (" + key + ")"); + } + + const currProfD = Services.dirsvc.get("ProfD", Ci.nsIFile); + const profileDir = currProfD.path; + ok(profileDir.includes(!!desc.profile.length && desc.profile), + "valid profile name"); + + client.close().then(() => { + DevToolsServer.destroy(); + SimpleTest.finish(); + }); + } + + d.getDescription().then(function(v) { + desc = v; + }).then(checkValues); + }); +}; +</script> +</pre> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_executeInGlobal-outerized_this.html b/devtools/server/tests/chrome/test_executeInGlobal-outerized_this.html new file mode 100644 index 0000000000..6a846596b2 --- /dev/null +++ b/devtools/server/tests/chrome/test_executeInGlobal-outerized_this.html @@ -0,0 +1,73 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=837060 + +When we use Debugger.Object.prototype.executeInGlobal, the 'this' value seen +by the evaluated code should be the WindowProxy, not the inner window +object. +--> +<head> + <meta charset="utf-8"> + <title>Mozilla Bug 837060</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +</head> +<body> +<pre id="test"> +<script> +"use strict"; + +const {addDebuggerToGlobal} = ChromeUtils.importESModule("resource://gre/modules/jsdebugger.sys.mjs"); +addDebuggerToGlobal(globalThis); + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + + const iframe = document.createElement("iframe"); + iframe.src = "data:text/html,<script>var me = 'page 1';<\/script>"; + iframe.onload = firstOnLoadHandler; + document.body.appendChild(iframe); + + function firstOnLoadHandler() { + const dbg = new Debugger(); + const page1DO = dbg.addDebuggee(iframe.contentWindow); + iframe.src = "data:text/html,<script>var me = 'page 2';<\/script>"; + iframe.onload = function() { + const page2DO = dbg.addDebuggee(iframe.contentWindow); + ok(page1DO !== page2DO, "the two pages' globals get distinct D.O's"); + ok(page1DO.unsafeDereference() === page2DO.unsafeDereference(), + "unwrapping page1DO and page2DO outerizes both, yielding the same outer window"); + + is(page1DO.executeInGlobal("me").return, + "page 1", "page1DO continues to refer to original page"); + is(page2DO.executeInGlobal("me").return, "page 2", + "page2DO refers to current page"); + + is(page1DO.executeInGlobal("this === window").return, true, + "page 1: Debugger.Object.prototype.executeInGlobal should outerize 'this'"); + is(page1DO.executeInGlobalWithBindings("this === window", {x: 2}).return, true, + "page 1: Debugger.Object.prototype.executeInGlobal should outerize 'this'"); + + is(page2DO.executeInGlobal("this === window").return, true, + "page 2: Debugger.Object.prototype.executeInGlobal should outerize 'this'"); + is(page2DO.executeInGlobalWithBindings("this === window", {x: 2}).return, true, + "page 2: Debugger.Object.prototype.executeInGlobal should outerize 'this'"); + + // Debugger doesn't let one use outer windows as globals. You have to innerize. + const outerDO = page1DO.makeDebuggeeValue(page1DO.unsafeDereference()); + ok(outerDO !== page1DO, + "outer window gets its own D.O, distinct from page 1's global"); + ok(outerDO !== page2DO, + "outer window gets its own D.O, distinct from page 2's global"); + SimpleTest.doesThrow(() => outerDO.executeInGlobal("me"), + "outer window D.Os can't be used as globals"); + + SimpleTest.finish(); + }; + } +}; +</script> +</pre> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_highlighter_paused_debugger.html b/devtools/server/tests/chrome/test_highlighter_paused_debugger.html new file mode 100644 index 0000000000..82dc939dd6 --- /dev/null +++ b/devtools/server/tests/chrome/test_highlighter_paused_debugger.html @@ -0,0 +1,88 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test the PausedDebuggerOverlay highlighter. +--> +<head> + <meta charset="utf-8"> + <title>PausedDebuggerOverlay test</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +</head> +<body> +<pre id="test"> +<script> +"use strict"; + +window.onload = async function() { + SimpleTest.waitForExplicitFinish(); + + const {require} = ChromeUtils.importESModule("resource://devtools/shared/loader/Loader.sys.mjs"); + require("devtools/server/actors/inspector/inspector"); + const {HighlighterEnvironment} = require("devtools/server/actors/highlighters"); + const {PausedDebuggerOverlay} = require("devtools/server/actors/highlighters/paused-debugger"); + + const env = new HighlighterEnvironment(); + env.initFromWindow(window); + + const highlighter = new PausedDebuggerOverlay(env); + await highlighter.isReady; + const anonymousContent = highlighter.markup.content; + + const id = elementID => `${highlighter.ID_CLASS_PREFIX}${elementID}`; + + function isHidden(elementID) { + const attr = anonymousContent.root.getElementById(id(elementID)).getAttribute("hidden"); + return typeof attr === "string" && attr == "true"; + } + + function getReason() { + return anonymousContent.root.getElementById(id("reason")).textContent; + } + + function isOverlayShown() { + const attr = anonymousContent.root.getElementById(id("root")).getAttribute("overlay"); + return typeof attr === "string" && attr == "true"; + } + + info("Test that the various elements with IDs exist"); + ok(highlighter.getElement("root"), "The root wrapper element exists"); + ok(highlighter.getElement("toolbar"), "The toolbar element exists"); + ok(highlighter.getElement("reason"), "The reason label element exists"); + + info("Test that the highlighter is hidden by default"); + ok(isHidden("root"), "The highlighter is hidden"); + + info("Show the highlighter with overlay and toolbar"); + let didShow = highlighter.show("breakpoint"); + ok(didShow, "Calling show returned true"); + ok(!isHidden("root"), "The highlighter is shown"); + ok(isOverlayShown(), "The overlay is shown"); + is( + getReason(), + "Paused on breakpoint", + "The reason displayed in the toolbar is correct" + ); + + info("Call show again with another reason"); + didShow = highlighter.show("debuggerStatement"); + ok(didShow, "Calling show returned true too"); + ok(!isHidden("root"), "The highlighter is still shown"); + is(getReason(), "Paused on debugger statement", + "The reason displayed in the toolbar is correct again"); + ok(isOverlayShown(), "The overlay is still shown too"); + + info("Call show again but with no reason"); + highlighter.show(); + ok(isOverlayShown(), "The overlay is shown however"); + + info("Hide the highlighter"); + highlighter.hide(); + ok(isHidden("root"), "The highlighter is now hidden"); + + SimpleTest.finish(); +}; +</script> +</pre> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_inspector-changeattrs.html b/devtools/server/tests/chrome/test_inspector-changeattrs.html new file mode 100644 index 0000000000..94c4c3dc1b --- /dev/null +++ b/devtools/server/tests/chrome/test_inspector-changeattrs.html @@ -0,0 +1,90 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id= +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug </title> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> + <script type="application/javascript" src="inspector-helpers.js"></script> + <script type="application/javascript"> +"use strict"; + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + runNextTest(); +}; + +let gInspectee = null; +let gWalker = null; + +addTest(async function setup() { + const url = document.getElementById("inspectorContent").href; + const { target, doc } = await attachURL(url); + gInspectee = doc; + const inspector = await target.getFront("inspector"); + gWalker = inspector.walker; + runNextTest(); +}); + +addTest(function testChangeAttrs() { + const attrNode = gInspectee.querySelector("#a"); + let attrFront; + promiseDone(gWalker.querySelector(gWalker.rootNode, "#a").then(front => { + attrFront = front; + dump("attrFront is: " + attrFront + "\n"); + // Add a few attributes. + const list = attrFront.startModifyingAttributes(); + list.setAttribute("data-newattr", "newvalue"); + list.setAttribute("data-newattr2", "newvalue"); + return list.apply(); + }).then(() => { + // We're only going to test that the change hit the document. + // There are other tests that make sure changes are propagated + // to the client. + is(attrNode.getAttribute("data-newattr"), "newvalue", + "Node should have the first new attribute"); + is(attrNode.getAttribute("data-newattr2"), "newvalue", + "Node should have the second new attribute."); + }).then(() => { + // Change an attribute. + const list = attrFront.startModifyingAttributes(); + list.setAttribute("data-newattr", "changedvalue"); + return list.apply(); + }).then(() => { + is(attrNode.getAttribute("data-newattr"), "changedvalue", + "Node should have the changed first value."); + is(attrNode.getAttribute("data-newattr2"), "newvalue", + "Second value should remain unchanged."); + }).then(() => { + const list = attrFront.startModifyingAttributes(); + list.removeAttribute("data-newattr2"); + return list.apply(); + }).then(() => { + is(attrNode.getAttribute("data-newattr"), "changedvalue", + "Node should have the changed first value."); + ok(!attrNode.hasAttribute("data-newattr2"), "Second value should be removed."); + }).then(runNextTest)); +}); + +addTest(function cleanup() { + gWalker = null; + gInspectee = null; + runNextTest(); +}); + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=">Mozilla Bug </a> +<a id="inspectorContent" target="_blank" href="inspector-traversal-data.html">Test Document</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_inspector-changevalue.html b/devtools/server/tests/chrome/test_inspector-changevalue.html new file mode 100644 index 0000000000..f5aee52881 --- /dev/null +++ b/devtools/server/tests/chrome/test_inspector-changevalue.html @@ -0,0 +1,68 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id= +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug </title> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> + <script type="application/javascript" src="inspector-helpers.js"></script> + <script type="application/javascript"> +"use strict"; + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + runNextTest(); +}; + +let gInspectee = null; +let gWalker = null; + +addTest(async function setup() { + const url = document.getElementById("inspectorContent").href; + const { target, doc } = await attachURL(url); + gInspectee = doc; + const inspector = await target.getFront("inspector"); + gWalker = inspector.walker; + runNextTest(); +}); + +addTest(function testChangeValue() { + const contentNode = gInspectee.querySelector("#a").firstChild; + let nodeFront; + promiseDone(gWalker.querySelector(gWalker.rootNode, "#a").then(front => { + // Get the text child + return gWalker.children(front, { maxNodes: 1 }); + }).then(children => { + nodeFront = children.nodes[0]; + is(nodeFront.nodeType, Node.TEXT_NODE); + return nodeFront.setNodeValue("newvalue"); + }).then(() => { + // We're only going to test that the change hit the document. + // There are other tests that make sure changes are propagated + // to the client. + is(contentNode.nodeValue, "newvalue", "Node should have a new value."); + }).then(runNextTest)); +}); + +addTest(function cleanup() { + gWalker = null; + gInspectee = null; + runNextTest(); +}); + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=">Mozilla Bug </a> +<a id="inspectorContent" target="_blank" href="inspector-traversal-data.html">Test Document</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_inspector-display-type.html b/devtools/server/tests/chrome/test_inspector-display-type.html new file mode 100644 index 0000000000..a8bbedc22a --- /dev/null +++ b/devtools/server/tests/chrome/test_inspector-display-type.html @@ -0,0 +1,81 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1431900 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1431900</title> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> + <script type="application/javascript" src="inspector-helpers.js"></script> + <script type="application/javascript"> +"use strict"; + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + runNextTest(); +}; + +var gWalker; + +addTest(async function setup() { + const url = document.getElementById("inspectorContent").href; + const { target } = await attachURL(url); + const inspector = await target.getFront("inspector"); + gWalker = inspector.walker; + runNextTest(); +}); + +addAsyncTest(async function testInlineBlockDisplayType() { + info("Test getting the display type of an inline block element."); + const node = await gWalker.querySelector(gWalker.rootNode, "#inline-block"); + const displayType = node.displayType; + is(displayType, "inline-block", "The node has a display type of 'inline-block'."); + runNextTest(); +}); + +addAsyncTest(async function testInlineTextChildDisplayType() { + info("Test getting the display type of an inline text child."); + const node = await gWalker.querySelector(gWalker.rootNode, "#inline-block"); + const children = await gWalker.children(node); + const inlineTextChild = children.nodes[0]; + const displayType = inlineTextChild.displayType; + ok(!displayType, "No display type for inline text child."); + runNextTest(); +}); + +addAsyncTest(async function testGridDisplayType() { + info("Test getting the display type of an grid container."); + const node = await gWalker.querySelector(gWalker.rootNode, "#grid"); + const displayType = node.displayType; + is(displayType, "grid", "The node has a display type of 'grid'."); + runNextTest(); +}); + +addAsyncTest(async function testBlockDisplayType() { + info("Test getting the display type of a block element."); + const node = await gWalker.querySelector(gWalker.rootNode, "#block"); + const displayType = await node.displayType; + is(displayType, "block", "The node has a display type of 'block'."); + runNextTest(); +}); + +addTest(function() { + gWalker = null; + runNextTest(); +}); + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1431900">Mozilla Bug 1431900</a> +<a id="inspectorContent" target="_blank" href="inspector_display-type.html">Test Document</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_inspector-duplicate-node.html b/devtools/server/tests/chrome/test_inspector-duplicate-node.html new file mode 100644 index 0000000000..205e11629e --- /dev/null +++ b/devtools/server/tests/chrome/test_inspector-duplicate-node.html @@ -0,0 +1,61 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1208864 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1208864</title> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> + <script type="application/javascript" src="inspector-helpers.js"></script> + <script type="application/javascript"> +"use strict"; + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + runNextTest(); +}; + +let gWalker = null; + +addTest(async function setup() { + const url = document.getElementById("inspectorContent").href; + const { target } = await attachURL(url); + const inspector = await target.getFront("inspector"); + gWalker = inspector.walker; + runNextTest(); +}); + +addTest(async function testDuplicateNode() { + const className = ".node-to-duplicate"; + let matches = await gWalker.querySelectorAll(gWalker.rootNode, className); + is(matches.length, 1, "There should initially be one node to duplicate."); + + const nodeFront = await gWalker.querySelector(gWalker.rootNode, className); + await gWalker.duplicateNode(nodeFront); + + matches = await gWalker.querySelectorAll(gWalker.rootNode, className); + is(matches.length, 2, "The node should now be duplicated."); + + runNextTest(); +}); + +addTest(function cleanup() { + gWalker = null; + runNextTest(); +}); + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1208864">Mozilla Bug 1208864</a> +<a id="inspectorContent" target="_blank" href="inspector-traversal-data.html">Test Document</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_inspector-hide.html b/devtools/server/tests/chrome/test_inspector-hide.html new file mode 100644 index 0000000000..e699400ee0 --- /dev/null +++ b/devtools/server/tests/chrome/test_inspector-hide.html @@ -0,0 +1,71 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id= +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug </title> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> + <script type="application/javascript" src="inspector-helpers.js"></script> + <script type="application/javascript"> +"use strict"; + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + runNextTest(); +}; + +let gWalker = null; +let gInspectee = null; + +addTest(async function setup() { + const url = document.getElementById("inspectorContent").href; + const { target, doc } = await attachURL(url); + const inspector = await target.getFront("inspector"); + gInspectee = doc; + gWalker = inspector.walker; + runNextTest(); +}); + +addTest(function testRearrange() { + let listFront = null; + const listNode = gInspectee.querySelector("#longlist"); + + promiseDone(gWalker.querySelector(gWalker.rootNode, "#longlist").then(front => { + listFront = front; + }).then(() => { + const computed = gInspectee.defaultView.getComputedStyle(listNode); + is(computed.visibility, "visible", "Node should be visible to start with"); + return gWalker.hideNode(listFront); + }).then(response => { + const computed = gInspectee.defaultView.getComputedStyle(listNode); + is(computed.visibility, "hidden", "Node should be hidden"); + return gWalker.unhideNode(listFront); + }).then(() => { + const computed = gInspectee.defaultView.getComputedStyle(listNode); + is(computed.visibility, "visible", "Node should be visible again."); + }).then(runNextTest)); +}); + +addTest(function cleanup() { + gWalker = null; + gInspectee = null; + runNextTest(); +}); + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=">Mozilla Bug </a> +<a id="inspectorContent" target="_blank" href="inspector-traversal-data.html">Test Document</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_inspector-inactive-property-helper.html b/devtools/server/tests/chrome/test_inspector-inactive-property-helper.html new file mode 100644 index 0000000000..86c783c035 --- /dev/null +++ b/devtools/server/tests/chrome/test_inspector-inactive-property-helper.html @@ -0,0 +1,124 @@ +<!DOCTYPE HTML> +<html> + <head> + <meta charset="utf-8"> + <title>Test for InactivePropertyHelper</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> + <script type="application/javascript"> +"use strict"; +SimpleTest.waitForExplicitFinish(); + +(async function() { + const { require } = ChromeUtils.importESModule("resource://devtools/shared/loader/Loader.sys.mjs"); + const { isPropertyUsed } = require("devtools/server/actors/utils/inactive-property-helper"); + + const INACTIVE_CSS_PREF = "devtools.inspector.inactive.css.enabled"; + const CUSTOM_HIGHLIGHT_API = "dom.customHighlightAPI.enabled"; + const TEXT_WRAP_BALANCE = "layout.css.text-wrap-balance.enabled"; + + Services.prefs.setBoolPref(INACTIVE_CSS_PREF, true); + Services.prefs.setBoolPref(CUSTOM_HIGHLIGHT_API, true); + Services.prefs.setBoolPref(TEXT_WRAP_BALANCE, true); + + SimpleTest.registerCleanupFunction(() => { + Services.prefs.clearUserPref(INACTIVE_CSS_PREF); + Services.prefs.clearUserPref(CUSTOM_HIGHLIGHT_API); + Services.prefs.clearUserPref(TEXT_WRAP_BALANCE); + }); + + const FOLDER = "./inactive-property-helper"; + + // Each file should `export default` an array of objects, representing each test case. + // A single test case is an object of the following shape: + // - {String} info: a summary of the test case + // - {String} property: the CSS property that should be tested + // - {String|undefined} tagName: the tagName of the element we're going to test. + // Optional only if there's a createTestElement property. + // - {Function|undefined} createTestElement: A function that takes a node as a parameter + // where elements used for the test case will + // be appended. The function should return the + // element that will be passed to + // isPropertyUsed. + // Optional only if there's a tagName property + // - {Array<String>} rules: An array of the rules that will be applied on the element. + // This can't be empty as isPropertyUsed need a rule. + // - {Integer|undefined} ruleIndex: If there are multiples rules in `rules`, the index + // of the one that should be tested in isPropertyUsed. + // - {Boolean} isActive: should the property be active (isPropertyUsed `used` result). + const testFiles = [ + "align-content.mjs", + "border-image.mjs", + "cue-pseudo-element.mjs", + "first-letter-pseudo-element.mjs", + "first-line-pseudo-element.mjs", + "flex-grid-item-properties.mjs", + "float.mjs", + "gap.mjs", + "grid-container-properties.mjs", + "grid-with-absolute-properties.mjs", + "multicol-container-properties.mjs", + "highlight-pseudo-elements.mjs", + "margin-padding.mjs", + "max-min-width-height.mjs", + "place-items-content.mjs", + "placeholder-pseudo-element.mjs", + "positioned.mjs", + "scroll-padding.mjs", + "vertical-align.mjs", + "table.mjs", + "table-cell.mjs", + "text-overflow.mjs", + "text-wrap.mjs", + "width-height-ruby.mjs", + ].map(file => `${FOLDER}/${file}`); + + // Import all the test cases + const tests = + (await Promise.all(testFiles.map(f => import(f).then(data => data.default)))).flat(); + + for (const { + info: summary, + property, + tagName, + createTestElement, + rules, + ruleIndex, + isActive, + expectedMsgId, + } of tests) { + // Create an element which will contain the test elements. + const main = document.createElement("main"); + document.firstElementChild.appendChild(main); + + // Apply the CSS rules to the document. + const style = document.createElement("style"); + main.append(style); + for (const dataRule of rules) { + style.sheet.insertRule(dataRule); + } + const rule = style.sheet.cssRules[ruleIndex || 0]; + + // Create the test elements + let el; + if (createTestElement) { + el = createTestElement(main); + } else { + el = document.createElement(tagName); + main.append(el); + } + + const { used, msgId } = isPropertyUsed(el, getComputedStyle(el), rule, property); + ok(used === isActive, summary); + if (expectedMsgId) { + is(msgId, expectedMsgId, `${summary} - returned expected msgId`); + } + + main.remove(); + } + SimpleTest.finish(); +})(); + </script> + </head> + <body></body> +</html> diff --git a/devtools/server/tests/chrome/test_inspector-mutations-attr.html b/devtools/server/tests/chrome/test_inspector-mutations-attr.html new file mode 100644 index 0000000000..9430db65bd --- /dev/null +++ b/devtools/server/tests/chrome/test_inspector-mutations-attr.html @@ -0,0 +1,169 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id= +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug </title> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> + <script type="application/javascript" src="inspector-helpers.js"></script> + <script type="application/javascript"> +"use strict"; + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + runNextTest(); +}; + +let gInspectee = null; +let gWalker = null; +let attrNode; +let attrFront; + +addTest(async function setup() { + const url = document.getElementById("inspectorContent").href; + const { target, doc } = await attachURL(url); + const inspector = await target.getFront("inspector"); + gInspectee = doc; + gWalker = inspector.walker; + runNextTest(); +}); + +addTest(setupAttrTest); +addTest(testAddAttribute); +addTest(testChangeAttribute); +addTest(testRemoveAttribute); +addTest(testQueuedMutations); +addTest(setupFrameAttrTest); +addTest(testAddAttribute); +addTest(testChangeAttribute); +addTest(testRemoveAttribute); +addTest(testQueuedMutations); + +function setupAttrTest() { + attrNode = gInspectee.querySelector("#a"); + promiseDone(gWalker.querySelector(gWalker.rootNode, "#a").then(node => { + attrFront = node; + }).then(runNextTest)); +} + +function setupFrameAttrTest() { + const frame = gInspectee.querySelector("#childFrame"); + attrNode = frame.contentDocument.querySelector("#a"); + + promiseDone(gWalker.querySelector(gWalker.rootNode, "#childFrame").then(childFrame => { + return childFrame.walkerFront.children(childFrame); + }).then(children => { + const nodes = children.nodes; + is(nodes.length, 1, "There should be only one child of the iframe"); + const [iframeNode] = nodes; + is(iframeNode.nodeType, Node.DOCUMENT_NODE, "iframe child should be a document node"); + return iframeNode.walkerFront.querySelector(iframeNode, "#a"); + }).then(node => { + attrFront = node; + }).then(runNextTest)); +} + +function testAddAttribute() { + attrNode.setAttribute("data-newattr", "newvalue"); + attrNode.setAttribute("data-newattr2", "newvalue"); + attrFront.walkerFront.once("mutations", () => { + is(attrFront.attributes.length, 3, "Should have id and two new attributes."); + is(attrFront.getAttribute("data-newattr"), "newvalue", + "Node front should have the first new attribute"); + is(attrFront.getAttribute("data-newattr2"), "newvalue", + "Node front should have the second new attribute."); + runNextTest(); + }); +} + +function testChangeAttribute() { + attrNode.setAttribute("data-newattr", "changedvalue1"); + attrNode.setAttribute("data-newattr", "changedvalue2"); + attrNode.setAttribute("data-newattr", "changedvalue3"); + attrFront.walkerFront.once("mutations", mutations => { + is(mutations.length, 1, + "Only one mutation is sent for multiple queued attribute changes"); + is(attrFront.attributes.length, 3, "Should have id and two new attributes."); + is(attrFront.getAttribute("data-newattr"), "changedvalue3", + "Node front should have the changed first value"); + is(attrFront.getAttribute("data-newattr2"), "newvalue", + "Second value should remain unchanged."); + runNextTest(); + }); +} + +function testRemoveAttribute() { + attrNode.removeAttribute("data-newattr2"); + attrFront.walkerFront.once("mutations", () => { + is(attrFront.attributes.length, 2, "Should have id and one remaining attribute."); + is(attrFront.getAttribute("data-newattr"), "changedvalue3", + "Node front should still have the first value"); + ok(!attrFront.hasAttribute("data-newattr2"), "Second value should be removed."); + runNextTest(); + }); +} + +function testQueuedMutations() { + // All modifications to each attribute should be queued in one mutation event. + + attrNode.removeAttribute("data-newattr"); + attrNode.setAttribute("data-newattr", "1"); + attrNode.removeAttribute("data-newattr"); + attrNode.setAttribute("data-newattr", "2"); + attrNode.removeAttribute("data-newattr"); + + for (let i = 0; i <= 1000; i++) { + attrNode.setAttribute("data-newattr2", i); + } + + attrNode.removeAttribute("data-newattr3"); + attrNode.setAttribute("data-newattr3", "1"); + attrNode.removeAttribute("data-newattr3"); + attrNode.setAttribute("data-newattr3", "2"); + attrNode.removeAttribute("data-newattr3"); + attrNode.setAttribute("data-newattr3", "3"); + + // This shouldn't be added in the attribute set, since it's a new + // attribute that's been added and removed. + attrNode.setAttribute("data-newattr4", "4"); + attrNode.removeAttribute("data-newattr4"); + + attrFront.walkerFront.once("mutations", mutations => { + is(mutations.length, 4, + "Only one mutation each is sent for multiple queued attribute changes"); + is(attrFront.attributes.length, 3, + "Should have id, data-newattr2, and data-newattr3."); + + is(attrFront.getAttribute("data-newattr2"), "1000", + "Node front should still have the correct value"); + is(attrFront.getAttribute("data-newattr3"), "3", + "Node front should still have the correct value"); + ok(!attrFront.hasAttribute("data-newattr"), "Attribute value should be removed."); + ok(!attrFront.hasAttribute("data-newattr4"), "Attribute value should be removed."); + + runNextTest(); + }); +} + +addTest(function cleanup() { + gInspectee = null; + gWalker = null; + runNextTest(); +}); + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=">Mozilla Bug </a> +<a id="inspectorContent" target="_blank" href="inspector-traversal-data.html">Test Document</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_inspector-mutations-events.html b/devtools/server/tests/chrome/test_inspector-mutations-events.html new file mode 100644 index 0000000000..b48952c4d9 --- /dev/null +++ b/devtools/server/tests/chrome/test_inspector-mutations-events.html @@ -0,0 +1,187 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1157469 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1157469</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> + <script type="application/javascript" src="inspector-helpers.js"></script> + <script type="application/javascript"> +"use strict"; + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + + const prevPrefValue = Services.prefs.getBoolPref("devtools.chrome.enabled"); + Services.prefs.setBoolPref("devtools.chrome.enabled", true); + + let inspectee = null; + let inspector = null; + let walker = null; + const eventListener1 = function() {}; + const eventListener2 = function() {}; + let eventNode1; + let eventNode2; + let eventFront1; + let eventFront2; + + addAsyncTest(async function setup() { + info("Setting up inspector and walker actors."); + const url = document.getElementById("inspectorContent").href; + const { target, doc } = await attachURL(url); + inspectee = doc; + inspector = await target.getFront("inspector"); + walker = inspector.walker; + + runNextTest(); + }); + + addAsyncTest(async function setupEventTest() { + eventNode1 = inspectee.querySelector("#a"); + eventNode2 = inspectee.querySelector("#b"); + + eventFront1 = await walker.querySelector(walker.rootNode, "#a"); + eventFront2 = await walker.querySelector(walker.rootNode, "#b"); + + runNextTest(); + }); + + addAsyncTest(async function testChangeEventListenerOnSingleNode() { + checkNodesHaveNoEventListener(); + + info("add event listener on a single node"); + eventNode1.addEventListener("click", eventListener1); + + let mutations = await waitForMutations(); + is(mutations.length, 1, "one mutation expected"); + is(mutations[0].target, eventFront1, "mutation targets eventFront1"); + is(mutations[0].type, "events", "mutation type is events"); + is(mutations[0].hasEventListeners, true, + "mutation target should have event listeners"); + is(eventFront1.hasEventListeners, true, "eventFront1 should have event listeners"); + + info("remove event listener on a single node"); + eventNode1.removeEventListener("click", eventListener1); + + mutations = await waitForMutations(); + is(mutations.length, 1, "one mutation expected"); + is(mutations[0].target, eventFront1, "mutation targets eventFront1"); + is(mutations[0].type, "events", "mutation type is events"); + is(mutations[0].hasEventListeners, false, + "mutation target should have no event listeners"); + is(eventFront1.hasEventListeners, false, + "eventFront1 should have no event listeners"); + + info("perform several event listener changes on a single node"); + eventNode1.addEventListener("click", eventListener1); + eventNode1.addEventListener("click", eventListener2); + eventNode1.removeEventListener("click", eventListener1); + eventNode1.removeEventListener("click", eventListener2); + + mutations = await waitForMutations(); + is(mutations.length, 1, "one mutation expected"); + is(mutations[0].target, eventFront1, "mutation targets eventFront1"); + is(mutations[0].type, "events", "mutation type is events"); + is(mutations[0].hasEventListeners, false, + "no event listener expected on mutation target"); + is(eventFront1.hasEventListeners, false, "no event listener expected on node"); + + runNextTest(); + }); + + addAsyncTest(async function testChangeEventsOnSeveralNodes() { + checkNodesHaveNoEventListener(); + + info("add event listeners on both nodes"); + eventNode1.addEventListener("click", eventListener1); + eventNode2.addEventListener("click", eventListener2); + + let mutations = await waitForMutations(); + is(mutations.length, 2, "two mutations expected, one for each modified node"); + // first mutation + is(mutations[0].target, eventFront1, "first mutation targets eventFront1"); + is(mutations[0].type, "events", "mutation type is events"); + is(mutations[0].hasEventListeners, true, + "mutation target should have event listeners"); + is(eventFront1.hasEventListeners, true, "eventFront1 should have event listeners"); + // second mutation + is(mutations[1].target, eventFront2, "second mutation targets eventFront2"); + is(mutations[1].type, "events", "mutation type is events"); + is(mutations[1].hasEventListeners, true, + "mutation target should have event listeners"); + is(eventFront2.hasEventListeners, true, "eventFront1 should have event listeners"); + + info("remove event listeners on both nodes"); + eventNode1.removeEventListener("click", eventListener1); + eventNode2.removeEventListener("click", eventListener2); + + mutations = await waitForMutations(); + is(mutations.length, 2, "one mutation registered for event listener change"); + // first mutation + is(mutations[0].target, eventFront1, "first mutation targets eventFront1"); + is(mutations[0].type, "events", "mutation type is events"); + is(mutations[0].hasEventListeners, false, + "mutation target should have no event listeners"); + is(eventFront1.hasEventListeners, false, + "eventFront2 should have no event listeners"); + // second mutation + is(mutations[1].target, eventFront2, "second mutation targets eventFront2"); + is(mutations[1].type, "events", "mutation type is events"); + is(mutations[1].hasEventListeners, false, + "mutation target should have no event listeners"); + is(eventFront2.hasEventListeners, false, + "eventFront2 should have no event listeners"); + + runNextTest(); + }); + + addAsyncTest(async function testRemoveMissingEvent() { + checkNodesHaveNoEventListener(); + + info("try to remove an event listener not previously added"); + eventNode1.removeEventListener("click", eventListener1); + + info("set any attribute on the node to trigger a mutation"); + eventNode1.setAttribute("data-attr", "somevalue"); + + const mutations = await waitForMutations(); + is(mutations.length, 1, "expect only one mutation"); + isnot(mutations.type, "events", "mutation type should not be events"); + + Services.prefs.setBoolPref("devtools.chrome.enabled", prevPrefValue); + runNextTest(); + }); + + function checkNodesHaveNoEventListener() { + is(eventFront1.hasEventListeners, false, + "eventFront1 hasEventListeners should be false"); + is(eventFront2.hasEventListeners, false, + "eventFront2 hasEventListeners should be false"); + } + + function waitForMutations() { + return new Promise(resolve => { + walker.once("mutations", mutations => { + resolve(mutations); + }); + }); + } + + runNextTest(); +}; + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1157469">Mozilla Bug 1157469</a> +<a id="inspectorContent" target="_blank" href="inspector-traversal-data.html">Test Document</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_inspector-mutations-value.html b/devtools/server/tests/chrome/test_inspector-mutations-value.html new file mode 100644 index 0000000000..14e93b9d1c --- /dev/null +++ b/devtools/server/tests/chrome/test_inspector-mutations-value.html @@ -0,0 +1,163 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id= +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug </title> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> + <script type="application/javascript" src="inspector-helpers.js"></script> + <script type="application/javascript"> +"use strict"; + +const WalkerActor = require("devtools/server/actors/inspector/walker"); + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + runNextTest(); +}; + +const testSummaryLength = 10; +WalkerActor.setValueSummaryLength(testSummaryLength); +SimpleTest.registerCleanupFunction(function() { + WalkerActor.setValueSummaryLength(WalkerActor.DEFAULT_VALUE_SUMMARY_LENGTH); +}); + +let gInspectee = null; +let gWalker = null; +let valueNode; +var valueFront; +var longStringFront; +var longString = "stringstringstringstringstringstringstringstringstringstringstring"; +var shortString = "str"; +var shortString2 = "str2"; + +addTest(async function setup() { + const url = document.getElementById("inspectorContent").href; + const { target, doc } = await attachURL(url); + const inspector = await target.getFront("inspector"); + gInspectee = doc; + gWalker = inspector.walker; + runNextTest(); +}); + +addTest(setupValueTest); +addTest(testKeepLongValue); +addTest(testSetShortValue); +addTest(testKeepShortValue); +addTest(testSetLongValue); +addTest(setupFrameValueTest); +addTest(testKeepLongValue); +addTest(testSetShortValue); +addTest(testKeepShortValue); +addTest(testSetLongValue); + +function setupValueTest() { + valueNode = gInspectee.querySelector("#longstring").firstChild; + promiseDone(gWalker.querySelector(gWalker.rootNode, "#longstring").then(node => { + longStringFront = node; + return gWalker.children(node); + }).then(children => { + valueFront = children.nodes[0]; + }).then(runNextTest)); +} + +function setupFrameValueTest() { + const frame = gInspectee.querySelector("#childFrame"); + valueNode = frame.contentDocument.querySelector("#longstring").firstChild; + + promiseDone(gWalker.querySelector(gWalker.rootNode, "#childFrame").then(childFrame => { + return gWalker.children(childFrame); + }).then(children => { + const nodes = children.nodes; + is(nodes.length, 1, "There should be only one child of the iframe"); + const [node] =nodes; + is(node.nodeType, Node.DOCUMENT_NODE, "iframe child should be a document node"); + return node.walkerFront.querySelector(node, "#longstring"); + }).then(node => { + longStringFront = node; + return longStringFront.walkerFront.children(node); + }).then(children => { + valueFront = children.nodes[0]; + }).then(runNextTest)); +} + +function checkNodeFrontValue(front, expectedValue) { + return front.getNodeValue().then(longstring => { + return longstring.string(); + }).then(str => { + is(str, expectedValue, "Node value is as expected"); + }); +} + +function testKeepLongValue() { + // After first setup we should have a long string in the node + ok(!longStringFront.inlineTextChild, "Text node is too long to be inlined."); + + valueNode.nodeValue = longString; + valueFront.walkerFront.once("mutations", (changes) => { + ok(!longStringFront.inlineTextChild, "Text node is too long to be inlined."); + ok(!changes.some(change => change.type === "inlineTextChild"), + "No inline text child mutation was fired."); + checkNodeFrontValue(valueFront, longString).then(runNextTest); + }); +} + +function testSetShortValue() { + ok(!longStringFront.inlineTextChild, "Text node is too long to be inlined."); + + valueNode.nodeValue = shortString; + valueFront.walkerFront.once("mutations", (changes) => { + ok(!!longStringFront.inlineTextChild, "Text node is short enough to be inlined."); + ok(changes.some(change => change.type === "inlineTextChild"), + "An inlineTextChild mutation was fired."); + checkNodeFrontValue(valueFront, shortString).then(runNextTest); + }); +} + +function testKeepShortValue() { + ok(!!longStringFront.inlineTextChild, "Text node is short enough to be inlined."); + + valueNode.nodeValue = shortString2; + valueFront.walkerFront.once("mutations", (changes) => { + ok(!!longStringFront.inlineTextChild, "Text node is short enough to be inlined."); + ok(!changes.some(change => change.type === "inlineTextChild"), + "No inline text child mutation was fired."); + checkNodeFrontValue(valueFront, shortString2).then(runNextTest); + }); +} + +function testSetLongValue() { + ok(!!longStringFront.inlineTextChild, "Text node is short enough to be inlined."); + + valueNode.nodeValue = longString; + valueFront.walkerFront.once("mutations", (changes) => { + ok(!longStringFront.inlineTextChild, "Text node is too long to be inlined."); + ok(changes.some(change => change.type === "inlineTextChild"), + "An inlineTextChild mutation was fired."); + checkNodeFrontValue(valueFront, longString).then(runNextTest); + }); +} + +addTest(function cleanup() { + gInspectee = null; + gWalker = null; + runNextTest(); +}); + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=">Mozilla Bug </a> +<a id="inspectorContent" target="_blank" href="inspector-traversal-data.html">Test Document</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_inspector-pick-color.html b/devtools/server/tests/chrome/test_inspector-pick-color.html new file mode 100644 index 0000000000..74aa3c50ce --- /dev/null +++ b/devtools/server/tests/chrome/test_inspector-pick-color.html @@ -0,0 +1,94 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test that the inspector actor has the pickColorFromPage and cancelPickColorFromPage +methods and that when a color is picked the color-picked event is emitted and that when +the eyedropper is dimissed, the color-pick-canceled event is emitted. +https://bugzilla.mozilla.org/show_bug.cgi?id=1262439 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1262439</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> + <script type="application/javascript" src="inspector-helpers.js"></script> + <script type="application/javascript"> +"use strict"; + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + + let win = null; + let inspector = null; + + addAsyncTest(async function() { + info("Setting up inspector actor"); + + const url = document.getElementById("inspectorContent").href; + const { target, doc } = await attachURL(url); + inspector = await target.getFront("inspector"); + win = doc.defaultView; + runNextTest(); + }); + + addAsyncTest(async function() { + info("Start picking a color from the page"); + await inspector.pickColorFromPage(); + + info("Click in the page and make sure a color-picked event is received"); + const onColorPicked = waitForEvent("color-picked"); + win.document.body.click(); + const color = await onColorPicked; + + is(color, "#000000", "The color-picked event was received with the right color"); + + runNextTest(); + }); + + addAsyncTest(async function() { + info("Start picking a color from the page"); + await inspector.pickColorFromPage(); + + info("Use the escape key to dismiss the eyedropper"); + const onPickCanceled = waitForEvent("color-pick-canceled"); + + const keyboardEvent = new win.KeyboardEvent("keydown", { + bubbles: true, + cancelable: true, + view: win, + keyCode: 27 + }); + win.document.dispatchEvent(keyboardEvent); + + await onPickCanceled; + ok(true, "The color-pick-canceled event was received"); + + runNextTest(); + }); + + addAsyncTest(async function() { + info("Start picking a color from the page"); + await inspector.pickColorFromPage(); + + info("And cancel the color picking"); + await inspector.cancelPickColorFromPage(); + + runNextTest(); + }); + + function waitForEvent(name) { + return new Promise(resolve => inspector.once(name, resolve)); + } + + runNextTest(); +}; + </script> +</head> +<body> +<a id="inspectorContent" target="_blank" href="inspector-eyedropper.html">Test Document</a> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_inspector-pseudoclass-lock.html b/devtools/server/tests/chrome/test_inspector-pseudoclass-lock.html new file mode 100644 index 0000000000..949066255d --- /dev/null +++ b/devtools/server/tests/chrome/test_inspector-pseudoclass-lock.html @@ -0,0 +1,185 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id= +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug </title> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> + <script type="application/javascript" src="inspector-helpers.js"></script> + <script type="application/javascript"> +"use strict"; + +const { PSEUDO_CLASSES } = require("devtools/shared/css/constants"); + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + runNextTest(); +}; + +let gInspectee = null; +let gWalker = null; + +async function setup(callback) { + const url = document.getElementById("inspectorContent").href; + const { target, doc } = await attachURL(url); + gInspectee = doc; + const inspector = await target.getFront("inspector"); + ok(inspector.walker, "getWalker() should return an actor."); + gWalker = inspector.walker; + runNextTest(); +} + +function teardown() { + gWalker = null; + gInspectee = null; +} + +function checkChange(change, expectation) { + is(change.type, "pseudoClassLock", "Expect a pseudoclass lock change."); + const target = change.target; + if (expectation.id) { + is(target.id, expectation.id, "Expect a change on node id " + expectation.id); + } + if (expectation.nodeName) { + is(target.nodeName, expectation.nodeName, + "Expect a change on node name " + expectation.nodeName); + } + + is(target.pseudoClassLocks.length, expectation.pseudos.length, + "Expect " + expectation.pseudos.length + " pseudoclass locks."); + for (let i = 0; i < expectation.pseudos.length; i++) { + const pseudo = expectation.pseudos[i]; + const enabled = expectation.enabled === undefined ? true : expectation.enabled[i]; + ok(target.hasPseudoClassLock(pseudo), "Expect lock: " + pseudo); + const rawNode = target.rawNode(); + ok(InspectorUtils.hasPseudoClassLock(rawNode, pseudo), + "Expect lock in dom: " + pseudo); + + is(rawNode.matches(pseudo), enabled, + `Target should match pseudoclass, '${pseudo}', if enabled (with .matches())`); + } + + for (const pseudo of PSEUDO_CLASSES) { + if (!expectation.pseudos.some(expected => pseudo === expected)) { + ok(!target.hasPseudoClassLock(pseudo), "Don't expect lock: " + pseudo); + ok(!InspectorUtils.hasPseudoClassLock(target.rawNode(), pseudo), + "Don't expect lock in dom: " + pseudo); + } + } +} + +function checkMutations(mutations, expectations) { + is(mutations.length, expectations.length, "Should get the right number of mutations."); + for (let i = 0; i < mutations.length; i++) { + checkChange(mutations[i], expectations[i]); + } +} + +addTest(function testPseudoClassLock() { + let contentNode; + let nodeFront; + setup(() => { + contentNode = gInspectee.querySelector("#b"); + return promiseDone(gWalker.querySelector(gWalker.rootNode, "#b").then(front => { + nodeFront = front; + // Lock the pseudoclass alone, no parents. + gWalker.addPseudoClassLock(nodeFront, ":active"); + // Expect a single pseudoClassLock mutation. + return promiseOnce(gWalker, "mutations"); + }).then(mutations => { + is(mutations.length, 1, "Should get one mutation"); + is(mutations[0].target, nodeFront, "Should be the node we tried to apply to"); + checkChange(mutations[0], { + id: "b", + nodeName: "DIV", + pseudos: [":active"], + }); + }).then(() => { + // Now add :hover, this time with parents. + gWalker.addPseudoClassLock(nodeFront, ":hover", {parents: true}); + return promiseOnce(gWalker, "mutations"); + }).then(mutations => { + const expectedMutations = [{ + id: "b", + nodeName: "DIV", + pseudos: [":hover", ":active"], + }, + { + id: "longlist", + nodeName: "DIV", + pseudos: [":hover"], + }, + { + nodeName: "BODY", + pseudos: [":hover"], + }, + { + nodeName: "HTML", + pseudos: [":hover"], + }]; + checkMutations(mutations, expectedMutations); + }).then(() => { + // Now remove the :hover on all parents + gWalker.removePseudoClassLock(nodeFront, ":hover", {parents: true}); + return promiseOnce(gWalker, "mutations"); + }).then(mutations => { + const expectedMutations = [{ + id: "b", + nodeName: "DIV", + // Should still have :active on the original node. + pseudos: [":active"], + }, + { + id: "longlist", + nodeName: "DIV", + pseudos: [], + }, + { + nodeName: "BODY", + pseudos: [], + }, + { + nodeName: "HTML", + pseudos: [], + }]; + checkMutations(mutations, expectedMutations); + }).then(() => { + gWalker.addPseudoClassLock(nodeFront, ":hover", {enabled: false}); + return promiseOnce(gWalker, "mutations"); + }).then(mutations => { + is(mutations.length, 1, "Should get one mutation"); + is(mutations[0].target, nodeFront, "Should be the node we tried to apply to"); + checkChange(mutations[0], { + id: "b", + nodeName: "DIV", + pseudos: [":hover", ":active"], + enabled: [false, true], + }); + }).then(() => { + // Now shut down the walker and make sure that clears up the remaining lock. + return gWalker.release(); + }).then(() => { + ok(!InspectorUtils.hasPseudoClassLock(contentNode, ":active"), + "Pseudoclass should have been removed during destruction."); + teardown(); + }).then(runNextTest)); + }); +}); + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=">Mozilla Bug </a> +<a id="inspectorContent" target="_blank" href="inspector-traversal-data.html">Test Document</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_inspector-reload.html b/devtools/server/tests/chrome/test_inspector-reload.html new file mode 100644 index 0000000000..09bd31cf75 --- /dev/null +++ b/devtools/server/tests/chrome/test_inspector-reload.html @@ -0,0 +1,90 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id= +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug </title> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> + <script type="application/javascript" src="inspector-helpers.js"></script> + <script type="application/javascript"> +"use strict"; + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + runNextTest(); +}; + +let gInspectee = null; +let gWalker = null; +let gResourceCommand = null; +let gCommands = null; + +addTest(async function setup() { + const url = document.getElementById("inspectorContent").href; + const { commands, doc } = await attachURL(url); + const target = commands.targetCommand.targetFront; + const inspector = await target.getFront("inspector"); + gInspectee = doc; + const walker = inspector.walker; + gWalker = await inspector.getWalker(); + gResourceCommand = commands.resourceCommand; + gCommands = commands; + + ok(walker === gWalker, "getWalker() twice should return the same walker."); + runNextTest(); +}); + +addTest(async function testReload() { + const oldRootID = gWalker.rootNode.actorID; + + info("Start watching for root nodes and wait for the initial root node"); + let rootNodeResolve; + let rootNodePromise = new Promise(r => (rootNodeResolve = r)); + const onAvailable = rootNodeFront => rootNodeResolve(rootNodeFront); + await gResourceCommand.watchResources([gResourceCommand.TYPES.ROOT_NODE], { + onAvailable, + }); + await rootNodePromise; + + info("Retrieve the node front for the selector `#a`"); + const nodeFront = await gWalker.querySelector(gWalker.rootNode, "#a"); + ok(nodeFront.actorID, "Node front has a valid actor ID"); + + info("Reload the page and wait for the newRoot mutation"); + rootNodePromise = new Promise(r => (rootNodeResolve = r)); + + gInspectee.defaultView.location.reload(); + await rootNodePromise; + gWalker = (await gCommands.targetCommand.targetFront.getFront("inspector")).walker; + + info("Retrieve the (new) node front for the selector `#a`"); + const newNodeFront = await gWalker.querySelector(gWalker.rootNode, "#a"); + ok(newNodeFront.actorID, "Got a new actor ID"); + ok(gWalker.rootNode.actorID != oldRootID, "Root node should have changed."); + + runNextTest(); +}); + +addTest(function cleanup() { + gWalker = null; + gInspectee = null; + gResourceCommand = null; + runNextTest(); +}); + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=">Mozilla Bug </a> +<a id="inspectorContent" target="_blank" href="inspector-traversal-data.html">Test Document</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_inspector-resize.html b/devtools/server/tests/chrome/test_inspector-resize.html new file mode 100644 index 0000000000..e0cf9abade --- /dev/null +++ b/devtools/server/tests/chrome/test_inspector-resize.html @@ -0,0 +1,69 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test that the inspector actor emits "resize" events when the page is resized. +https://bugzilla.mozilla.org/show_bug.cgi?id=1222409 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1222409</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> + <script type="application/javascript" src="inspector-helpers.js"></script> + <script type="application/javascript"> +"use strict"; + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + + let win = null; + let inspector = null; + + addAsyncTest(async function setup() { + info("Setting up inspector and walker actors."); + + const url = document.getElementById("inspectorContent").href; + + const { target, doc } = await attachURL(url); + inspector = await target.getFront("inspector"); + win = doc.defaultView; + runNextTest(); + }); + + addAsyncTest(async function() { + const walker = inspector.walker; + + // We can't receive events from the walker if we haven't first executed a + // method on the actor to initialize it. + await walker.querySelector(walker.rootNode, "img"); + + const {outerWidth, outerHeight} = win; + // eslint-disable-next-line new-cap + const onResize = new Promise(resolve => { + walker.once("resize", () => { + resolve(); + }); + }); + win.resizeTo(800, 600); + await onResize; + + ok(true, "The resize event was emitted"); + win.resizeTo(outerWidth, outerHeight); + + runNextTest(); + }); + + runNextTest(); +}; + </script> +</head> +<body> +<a id="inspectorContent" target="_blank" href="inspector-search-data.html">Test Document</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_inspector-resolve-url.html b/devtools/server/tests/chrome/test_inspector-resolve-url.html new file mode 100644 index 0000000000..ddf68f56ed --- /dev/null +++ b/devtools/server/tests/chrome/test_inspector-resolve-url.html @@ -0,0 +1,87 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=921102 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 921102</title> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> + <script type="application/javascript" src="inspector-helpers.js"></script> + <script type="application/javascript"> +"use strict"; + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + runNextTest(); +}; + +let gInspector; +let gDoc; + +addTest(async function() { + const url = document.getElementById("inspectorContent").href; + const { target, doc } = await attachURL(url); + gInspector = await target.getFront("inspector"); + gDoc = doc; + runNextTest(); +}); + +addTest(function() { + info("Resolve a relative URL without providing a context node"); + gInspector.resolveRelativeURL("test.png?id=4#wow").then(url => { + is(url, "chrome://mochitests/content/chrome/devtools/server/tests/" + + "chrome/test.png?id=4#wow"); + runNextTest(); + }); +}); + +addTest(function() { + info("Resolve an absolute URL without providing a context node"); + gInspector.resolveRelativeURL("chrome://mochitests/content/chrome/" + + "devtools/server/").then(url => { + is(url, "chrome://mochitests/content/chrome/devtools/server/"); + runNextTest(); + }); +}); + +addTest(function() { + info("Resolve a relative URL providing a context node"); + const node = gDoc.querySelector(".big-horizontal"); + gInspector.resolveRelativeURL("test.png?id=4#wow", node).then(url => { + is(url, "chrome://mochitests/content/chrome/devtools/server/tests/" + + "chrome/test.png?id=4#wow"); + runNextTest(); + }); +}); + +addTest(function() { + info("Resolve an absolute URL providing a context node"); + const node = gDoc.querySelector(".big-horizontal"); + gInspector.resolveRelativeURL("chrome://mochitests/content/chrome/" + + "devtools/server/", node).then(url => { + is(url, "chrome://mochitests/content/chrome/devtools/server/"); + runNextTest(); + }); +}); + +addTest(function() { + gInspector = null; + gDoc = null; + runNextTest(); +}); + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=921102">Mozilla Bug 921102</a> +<a id="inspectorContent" target="_blank" href="inspector_getImageData.html">Test Document</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_inspector-scroll-into-view.html b/devtools/server/tests/chrome/test_inspector-scroll-into-view.html new file mode 100644 index 0000000000..a107f9ba4a --- /dev/null +++ b/devtools/server/tests/chrome/test_inspector-scroll-into-view.html @@ -0,0 +1,60 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=901250 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 901250</title> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> + <script type="application/javascript" src="inspector-helpers.js"></script> + <script type="application/javascript"> +"use strict"; + +window.onload = async function() { + SimpleTest.waitForExplicitFinish(); + + const url = document.getElementById("inspectorContent").href; + const { target, doc } = await attachURL(url); + const inspector = await target.getFront("inspector"); + const walker = inspector.walker; + + const id = "#scroll-into-view"; + let rect = doc.querySelector(id).getBoundingClientRect(); + const nodeFront = await walker.querySelector(walker.rootNode, id); + let inViewport = rect.x >= 0 && + rect.y >= 0 && + rect.y <= doc.defaultView.innerHeight && + rect.x <= doc.defaultView.innerWidth; + + ok(!inViewport, "Element is not in viewport initially"); + + await nodeFront.scrollIntoView(); + + await new Promise(res => SimpleTest.executeSoon(res)); + + rect = doc.querySelector(id).getBoundingClientRect(); + inViewport = rect.x >= 0 && + rect.y >= 0 && + rect.y <= doc.defaultView.innerHeight && + rect.x <= doc.defaultView.innerWidth; + ok(inViewport, "Element is in viewport after calling nodeFront.scrollIntoView"); + + await target.destroy(); + SimpleTest.finish(); +}; + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=901250">Mozilla Bug 901250</a> +<a id="inspectorContent" target="_blank" href="inspector-traversal-data.html">Test Document</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_inspector-search-front.html b/devtools/server/tests/chrome/test_inspector-search-front.html new file mode 100644 index 0000000000..a78700e8e6 --- /dev/null +++ b/devtools/server/tests/chrome/test_inspector-search-front.html @@ -0,0 +1,163 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=835896 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 835896</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> + <script type="application/javascript" src="inspector-helpers.js"></script> + <script type="application/javascript"> +"use strict"; + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + + let walkerFront = null; + let inspectorCommand = null; + + // WalkerFront and Inspector Command specific tests. These aren't to exercise search + // edge cases so much as to test the state the Front maintains between + // searches. + + addAsyncTest(async function setup() { + info("Setting up inspector and walker actors."); + + const url = document.getElementById("inspectorContent").href; + + const { commands } = await attachURL(url); + const target = commands.targetCommand.targetFront; + const inspector = await target.getFront("inspector"); + + walkerFront = inspector.walker; + inspectorCommand = commands.inspectorCommand; + + runNextTest(); + }); + + addAsyncTest(async function testWalkerFrontDefaults() { + info("Testing search API using WalkerFront and Inspector Command."); + const nodes = await walkerFront.querySelectorAll(walkerFront.rootNode, "h2"); + const fronts = await nodes.items(); + + const commandResult = await inspectorCommand.findNextNode(""); + ok(!commandResult, "Null result on front when searching for ''"); + + let results = await inspectorCommand.findNextNode("h2"); + isDeeply(results, { + node: fronts[0], + resultsIndex: 0, + resultsLength: 3, + }, "Default options work"); + + results = await inspectorCommand.findNextNode("h2", { }); + isDeeply(results, { + node: fronts[1], + resultsIndex: 1, + resultsLength: 3, + }, "Search works with empty options"); + + // Clear search data to remove result state on the front + await inspectorCommand.findNextNode(""); + runNextTest(); + }); + + addAsyncTest(async function testMultipleSearches() { + info("Testing search API using WalkerFront and Inspector Command (reverse=false)"); + const nodes = await walkerFront.querySelectorAll(walkerFront.rootNode, "h2"); + const fronts = await nodes.items(); + + let results = await inspectorCommand.findNextNode("h2"); + isDeeply(results, { + node: fronts[0], + resultsIndex: 0, + resultsLength: 3, + }, "Search works with multiple results (reverse=false)"); + + results = await inspectorCommand.findNextNode("h2"); + isDeeply(results, { + node: fronts[1], + resultsIndex: 1, + resultsLength: 3, + }, "Search works with multiple results (reverse=false)"); + + results = await inspectorCommand.findNextNode("h2"); + isDeeply(results, { + node: fronts[2], + resultsIndex: 2, + resultsLength: 3, + }, "Search works with multiple results (reverse=false)"); + + results = await inspectorCommand.findNextNode("h2"); + isDeeply(results, { + node: fronts[0], + resultsIndex: 0, + resultsLength: 3, + }, "Search works with multiple results (reverse=false)"); + + // Clear search data to remove result state on the front + await inspectorCommand.findNextNode(""); + runNextTest(); + }); + + addAsyncTest(async function testMultipleSearchesReverse() { + info("Testing search API using WalkerFront and Inspector Command (reverse=true)"); + const nodes = await walkerFront.querySelectorAll(walkerFront.rootNode, "h2"); + const fronts = await nodes.items(); + + let results = await inspectorCommand.findNextNode("h2", {reverse: true}); + isDeeply(results, { + node: fronts[2], + resultsIndex: 2, + resultsLength: 3, + }, "Search works with multiple results (reverse=true)"); + + results = await inspectorCommand.findNextNode("h2", {reverse: true}); + isDeeply(results, { + node: fronts[1], + resultsIndex: 1, + resultsLength: 3, + }, "Search works with multiple results (reverse=true)"); + + results = await inspectorCommand.findNextNode("h2", {reverse: true}); + isDeeply(results, { + node: fronts[0], + resultsIndex: 0, + resultsLength: 3, + }, "Search works with multiple results (reverse=true)"); + + results = await inspectorCommand.findNextNode("h2", {reverse: true}); + isDeeply(results, { + node: fronts[2], + resultsIndex: 2, + resultsLength: 3, + }, "Search works with multiple results (reverse=true)"); + + results = await inspectorCommand.findNextNode("h2", {reverse: false}); + isDeeply(results, { + node: fronts[0], + resultsIndex: 0, + resultsLength: 3, + }, "Search works with multiple results (reverse=false)"); + + // Clear search data to remove result state on the command + await inspectorCommand.findNextNode(""); + runNextTest(); + }); + + runNextTest(); +}; + </script> +</head> +<body> +<a id="inspectorContent" target="_blank" href="inspector-search-data.html">Test Document</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_inspector-template.html b/devtools/server/tests/chrome/test_inspector-template.html new file mode 100644 index 0000000000..6fbc7742c6 --- /dev/null +++ b/devtools/server/tests/chrome/test_inspector-template.html @@ -0,0 +1,66 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1078374 +Display template tag content in inspector. +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug </title> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> + <script type="application/javascript" src="inspector-helpers.js"></script> + <script type="application/javascript"> +"use strict"; + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + + let gWalker = null; + + addAsyncTest(async function setup() { + const url = document.getElementById("inspectorContent").href; + + const { target } = await attachURL(url); + const inspector = await target.getFront("inspector"); + gWalker = inspector.walker; + + runNextTest(); + }); + + addAsyncTest(async function testWalker() { + const nodeFront = await gWalker.querySelector(gWalker.rootNode, "template"); + + let children = await gWalker.children(nodeFront); + is(children.nodes.length, 1, "Found one child under the template element"); + + const docFragment = children.nodes[0]; + is(docFragment.nodeName, "#document-fragment", + "First child under <template> is a document-fragment"); + + children = await gWalker.children(docFragment); + is(children.nodes.length, 1, "Found one child under the template element"); + + const p = children.nodes[0]; + is(p.nodeName, "P", + "First child under the document-fragment is a p element"); + + runNextTest(); + }); + + runNextTest(); +}; + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=">Mozilla Bug </a> +<a id="inspectorContent" target="_blank" href="inspector-template.html">Test Document</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_inspector_getImageData-wait-for-load.html b/devtools/server/tests/chrome/test_inspector_getImageData-wait-for-load.html new file mode 100644 index 0000000000..129116b913 --- /dev/null +++ b/devtools/server/tests/chrome/test_inspector_getImageData-wait-for-load.html @@ -0,0 +1,133 @@ +<!DOCTYPE HTML> +<html> +<!-- +Tests for InspectorActor.getImageData() in following cases: + * Image takes too long to load (the method rejects after a timeout). + * Image is loading when the method is called and the load finishes before + timeout. + * Image fails to load. + +https://bugzilla.mozilla.org/show_bug.cgi?id=1192536 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1192536</title> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> + <script type="application/javascript" src="inspector-helpers.js"></script> + <script type="application/javascript"> +"use strict"; + +const PATH = "https://example.com/chrome/devtools/server/tests/chrome/"; +const BASE_IMAGE = PATH + "inspector-delay-image-response.sjs"; +const DELAYED_IMAGE = BASE_IMAGE + "?delay=300"; +const TIMEOUT_IMAGE = BASE_IMAGE + "?delay=50000"; +const NONEXISTENT_IMAGE = PATH + "this-does-not-exist.png"; + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + runNextTest(); +}; + +function pushPref(preferenceName, value) { + return new Promise(resolve => { + const options = {"set": [[preferenceName, value]]}; + SpecialPowers.pushPrefEnv(options, resolve); + }); +} + +let gImg = null; +let gNodeFront = null; +let gWalker = null; + +addTest(async function setup() { + const url = document.getElementById("inspectorContent").href; + const { target, doc } = await attachURL(url); + const inspector = await target.getFront("inspector"); + gWalker = inspector.walker; + gNodeFront = await gWalker.querySelector(gWalker.rootNode, "img.custom"); + gImg = doc.querySelector("img.custom"); + ok(gNodeFront, "Got the image NodeFront."); + ok(gImg, "Got the image Node."); + runNextTest(); +}); + +addTest(async function testTimeout() { + info("Testing that the method aborts if the image takes too long to load."); + + // imageToImageData() only times out when flags.testing is not set. + await pushPref("devtools.testing", false); + + gImg.src = TIMEOUT_IMAGE; + + info("Calling getImageData()."); + ensureRejects(gNodeFront.getImageData(), "Timeout image").then(runNextTest); +}); + +addTest(async function testNonExistentImage() { + info("Testing that non-existent image causes a rejection."); + + // This test shouldn't hit the timeout. + await pushPref("devtools.testing", true); + + gImg.src = NONEXISTENT_IMAGE; + + info("Calling getImageData()."); + ensureRejects(gNodeFront.getImageData(), "Non-existent image").then(runNextTest); +}); + +addTest(async function testDelayedImage() { + info("Testing that the method waits for an image to load."); + + // This test shouldn't hit the timeout. + await pushPref("devtools.testing", true); + + gImg.src = DELAYED_IMAGE; + + info("Calling getImageData()."); + checkImageData(gNodeFront.getImageData()).then(runNextTest); +}); + +addTest(function cleanup() { + gImg = null; + gNodeFront = null; + gWalker = null; + runNextTest(); +}); + +/** + * Asserts that the given promise rejects. + */ +function ensureRejects(promise, desc) { + return promise.then(() => { + ok(false, desc + ": promise resolved unexpectedly."); + }, () => { + ok(true, desc + ": promise rejected as expected."); + }); +} + +/** + * Waits for the call to getImageData() the resolve and checks that the image + * size is reported correctly. + */ +function checkImageData(promise, { width, height } = { width: 1, height: 1 }) { + return promise.then(({ size }) => { + is(size.naturalWidth, width, "The width is correct."); + is(size.naturalHeight, height, "The height is correct."); + }); +} + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1192536">Mozilla Bug 1192536</a> +<a id="inspectorContent" target="_blank" href="inspector_getImageData.html">Test Document</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_inspector_getImageData.html b/devtools/server/tests/chrome/test_inspector_getImageData.html new file mode 100644 index 0000000000..d95b0e5fd3 --- /dev/null +++ b/devtools/server/tests/chrome/test_inspector_getImageData.html @@ -0,0 +1,142 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=932937 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 932937</title> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> + <script type="application/javascript" src="inspector-helpers.js"></script> + <script type="application/javascript"> +"use strict"; + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + runNextTest(); +}; + +let gWalker = null; + +addTest(async function setup() { + const url = document.getElementById("inspectorContent").href; + const { target } = await attachURL(url); + const inspector = await target.getFront("inspector"); + gWalker = inspector.walker; + runNextTest(); +}); + +addTest(async function testLargeImage() { + // Select the image node from the test page + const img = await gWalker.querySelector(gWalker.rootNode, ".big-horizontal"); + ok(img, "Image node found in the test page"); + ok(img.getImageData, "Image node has the getImageData function"); + const imageData = await img.getImageData(100); + ok(imageData.data, "Image data actor was sent back"); + ok(imageData.size, "Image size info was sent back too"); + is(imageData.size.naturalWidth, 5333, "Natural width of the image correct"); + is(imageData.size.naturalHeight, 3000, "Natural width of the image correct"); + ok(imageData.size.resized, "Image was resized"); + const str = await imageData.data.string(); + ok(str, "We have an image data string!"); + testResizing(imageData, str); +}); + +addTest(async function testLargeCanvas() { + // Select the canvas node from the test page + const canvas = await gWalker.querySelector(gWalker.rootNode, ".big-vertical"); + ok(canvas, "Image node found in the test page"); + ok(canvas.getImageData, "Image node has the getImageData function"); + const imageData = await canvas.getImageData(350); + ok(imageData.data, "Image data actor was sent back"); + ok(imageData.size, "Image size info was sent back too"); + is(imageData.size.naturalWidth, 1000, "Natural width of the image correct"); + is(imageData.size.naturalHeight, 2000, "Natural width of the image correct"); + ok(imageData.size.resized, "Image was resized"); + const str = await imageData.data.string(); + ok(str, "We have an image data string!"); + testResizing(imageData, str); +}); + +addTest(async function testSmallImage() { + // Select the small image node from the test page + const img = await gWalker.querySelector(gWalker.rootNode, ".small"); + ok(img, "Image node found in the test page"); + ok(img.getImageData, "Image node has the getImageData function"); + const imageData = await img.getImageData(); + ok(imageData.data, "Image data actor was sent back"); + ok(imageData.size, "Image size info was sent back too"); + is(imageData.size.naturalWidth, 245, "Natural width of the image correct"); + is(imageData.size.naturalHeight, 240, "Natural width of the image correct"); + ok(!imageData.size.resized, "Image was NOT resized"); + const str = await imageData.data.string(); + ok(str, "We have an image data string!"); + testResizing(imageData, str); +}); + +addTest(async function testDataImage() { + // Select the data image node from the test page + const img = await gWalker.querySelector(gWalker.rootNode, ".data"); + ok(img, "Image node found in the test page"); + ok(img.getImageData, "Image node has the getImageData function"); + const imageData = await img.getImageData(14); + ok(imageData.data, "Image data actor was sent back"); + ok(imageData.size, "Image size info was sent back too"); + is(imageData.size.naturalWidth, 28, "Natural width of the image correct"); + is(imageData.size.naturalHeight, 28, "Natural width of the image correct"); + ok(imageData.size.resized, "Image was resized"); + const str = await imageData.data.string(); + ok(str, "We have an image data string!"); + testResizing(imageData, str); +}); + +addTest(async function testNonImgOrCanvasElements() { + const body = await gWalker.querySelector(gWalker.rootNode, "body"); + await ensureRejects(body.getImageData(), "Invalid element"); + runNextTest(); +}); + +addTest(function cleanup() { + gWalker = null; + runNextTest(); +}); + +/** + * Checks if the server told the truth about resizing the image + */ +function testResizing(imageData, str) { + const img = document.createElement("img"); + img.addEventListener("load", () => { + const resized = !(img.naturalWidth == imageData.size.naturalWidth && + img.naturalHeight == imageData.size.naturalHeight); + is(imageData.size.resized, resized, "Server told the truth about resizing"); + runNextTest(); + }); + img.src = str; +} + +/** + * Asserts that the given promise rejects. + */ +function ensureRejects(promise, desc) { + return promise.then(() => { + ok(false, desc + ": promise resolved unexpectedly."); + }, () => { + ok(true, desc + ": promise rejected as expected."); + }); +} + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=932937">Mozilla Bug 932937</a> +<a id="inspectorContent" target="_blank" href="inspector_getImageData.html">Test Document</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_inspector_getImageDataFromURL.html b/devtools/server/tests/chrome/test_inspector_getImageDataFromURL.html new file mode 100644 index 0000000000..451c49dcc3 --- /dev/null +++ b/devtools/server/tests/chrome/test_inspector_getImageDataFromURL.html @@ -0,0 +1,116 @@ +<!DOCTYPE HTML> +<html> +<!-- +Tests for InspectorActor.getImageDataFromURL() in following cases: + * Normal case, image loads after a small delay. + * Image takes too long to load (the method rejects after a timeout). + * Image fails to load. + +https://bugzilla.mozilla.org/show_bug.cgi?id=1192536 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1192536</title> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> + <script type="application/javascript" src="inspector-helpers.js"></script> + <script type="application/javascript"> +"use strict"; + +const PATH = "https://example.com/chrome/devtools/server/tests/chrome/"; +const BASE_IMAGE = PATH + "inspector-delay-image-response.sjs"; +const DELAYED_IMAGE = BASE_IMAGE + "?delay=300"; +const TIMEOUT_IMAGE = BASE_IMAGE + "?delay=50000"; +const NONEXISTENT_IMAGE = PATH + "this-does-not-exist.png"; + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + runNextTest(); +}; + +function pushPref(preferenceName, value) { + return new Promise(resolve => { + const options = {"set": [[preferenceName, value]]}; + SpecialPowers.pushPrefEnv(options, resolve); + }); +} + +let gInspector = null; + +addTest(async function setup() { + const url = document.getElementById("inspectorContent").href; + const { target } = await attachURL(url); + gInspector = await target.getFront("inspector"); + runNextTest(); +}); + +addTest(async function testTimeout() { + info("Testing that the method aborts if the image takes too long to load."); + + // imageToImageData() only times out when flags.testing is not set. + await pushPref("devtools.testing", false); + + ensureRejects(gInspector.getImageDataFromURL(TIMEOUT_IMAGE), + "Image that loads for too long").then(runNextTest); +}); + +addTest(async function testNonExistentImage() { + info("Testing that non-existent image causes a rejection."); + + // This test shouldn't hit the timeout. + await pushPref("devtools.testing", true); + + ensureRejects(gInspector.getImageDataFromURL(NONEXISTENT_IMAGE), + "Non-existent image").then(runNextTest); +}); + +addTest(async function testNormalImage() { + info("Testing that the method waits for an image to load."); + + // This test shouldn't hit the timeout. + await pushPref("devtools.testing", true); + + checkImageData(gInspector.getImageDataFromURL(DELAYED_IMAGE)).then(runNextTest); +}); + +addTest(function cleanup() { + gInspector = null; + runNextTest(); +}); + +/** + * Asserts that the given promise rejects. + */ +function ensureRejects(promise, desc) { + return promise.then(() => { + ok(false, desc + ": promise resolved unexpectedly."); + }, () => { + ok(true, desc + ": promise rejected as expected."); + }); +} + +/** + * Waits for the call to getImageData() the resolve and checks that the image + * size is reported correctly. + */ +function checkImageData(promise, { width, height } = { width: 1, height: 1 }) { + return promise.then(({ size }) => { + is(size.naturalWidth, width, "The width is correct."); + is(size.naturalHeight, height, "The height is correct."); + }); +} + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1192536">Mozilla Bug 1192536</a> +<a id="inspectorContent" target="_blank" href="inspector_getImageData.html">Test Document</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_inspector_getNodeFromActor.html b/devtools/server/tests/chrome/test_inspector_getNodeFromActor.html new file mode 100644 index 0000000000..c3c5d32af9 --- /dev/null +++ b/devtools/server/tests/chrome/test_inspector_getNodeFromActor.html @@ -0,0 +1,84 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1155653 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1155653</title> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> + <script type="application/javascript" src="inspector-helpers.js"></script> + <script type="application/javascript"> +"use strict"; + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + runNextTest(); +}; + +let gWalker; + +addTest(async function() { + const url = document.getElementById("inspectorContent").href; + const { target } = await attachURL(url); + const inspector = await target.getFront("inspector"); + gWalker = inspector.walker; + runNextTest(); +}); + +addTest(function() { + info("Try to get a NodeFront from an invalid actorID"); + gWalker.getNodeFromActor("invalid", ["node"]).then(node => { + ok(!node, "The node returned is null"); + runNextTest(); + }); +}); + +addTest(function() { + info("Try to get a NodeFront from a valid actorID but invalid path"); + gWalker.getNodeFromActor(gWalker.actorID, ["invalid", "path"]).then(node => { + ok(!node, "The node returned is null"); + runNextTest(); + }); +}); + +addTest(function() { + info("Try to get a NodeFront from a valid actorID and valid path"); + gWalker.getNodeFromActor(gWalker.actorID, ["rootDoc"]).then(rootDocNode => { + ok(rootDocNode, "A node was returned"); + is(rootDocNode, gWalker.rootNode, "The right node was returned"); + runNextTest(); + }); +}); + +addTest(function() { + info("Try to get a NodeFront from a valid actorID and valid complex path"); + gWalker.getNodeFromActor(gWalker.actorID, + ["targetActor", "window", "document", "body"]).then(bodyNode => { + ok(bodyNode, "A node was returned"); + gWalker.querySelector(gWalker.rootNode, "body").then(node => { + is(bodyNode, node, "The body node was returned"); + runNextTest(); + }); + }); +}); + +addTest(function() { + gWalker = null; + runNextTest(); +}); + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1155653">Mozilla Bug 1155653</a> +<a id="inspectorContent" target="_blank" href="inspector_getImageData.html">Test Document</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_inspector_getOffsetParent.html b/devtools/server/tests/chrome/test_inspector_getOffsetParent.html new file mode 100644 index 0000000000..09da7d55d1 --- /dev/null +++ b/devtools/server/tests/chrome/test_inspector_getOffsetParent.html @@ -0,0 +1,129 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1345119 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1345119</title> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> + <script type="application/javascript" src="inspector-helpers.js"></script> + <script type="application/javascript"> +"use strict"; + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + runNextTest(); +}; + +var gWalker; +var gHTMLNode; +var gBodyNode; + +addTest(async function setup() { + const url = document.getElementById("inspectorContent").href; + const { target } = await attachURL(url); + const inspector = await target.getFront("inspector"); + gWalker = inspector.walker; + gBodyNode = await gWalker.querySelector(gWalker.rootNode, "body"); + gHTMLNode = await gWalker.querySelector(gWalker.rootNode, "html"); + runNextTest(); +}); + +addTest(function() { + info("Try to get the offset parent for a dead node (null)"); + gWalker.getOffsetParent(null).then(offsetParent => { + ok(!offsetParent, "No offset parent found"); + runNextTest(); + }); +}); + +addTest(function() { + info("Try to get the offset parent for a node that is absolutely positioned inside a " + + "relative node"); + gWalker.querySelector(gWalker.rootNode, "#absolute_child").then(node => { + return gWalker.getOffsetParent(node); + }).then(offsetParent => { + ok(offsetParent, "The node has an offset parent"); + gWalker.querySelector(gWalker.rootNode, "#relative_parent").then(parent => { + ok(offsetParent === parent, "The offset parent is the correct node"); + runNextTest(); + }); + }); +}); + +addTest(function() { + info("Try to get the offset parent for a node that is absolutely positioned outside a" + + " relative node"); + gWalker.querySelector(gWalker.rootNode, "#no_parent").then(node => { + return gWalker.getOffsetParent(node); + }).then(offsetParent => { + ok(offsetParent === gBodyNode || offsetParent === gHTMLNode, + "The node's offset parent is the body or html node"); + runNextTest(); + }); +}); + +addTest(function() { + info("Try to get the offset parent for a relatively positioned node"); + gWalker.querySelector(gWalker.rootNode, "#relative_parent").then(node => { + return gWalker.getOffsetParent(node); + }).then(offsetParent => { + ok(offsetParent === gBodyNode || offsetParent === gHTMLNode, + "The node's offset parent is the body or html node"); + runNextTest(); + }); +}); + +addTest(function() { + info("Try to get the offset parent for a statically positioned node"); + gWalker.querySelector(gWalker.rootNode, "#static").then(node => { + return gWalker.getOffsetParent(node); + }).then(offsetParent => { + ok(offsetParent === gBodyNode || offsetParent === gHTMLNode, + "The node's offset parent is the body or html node"); + runNextTest(); + }); +}); + +addTest(function() { + info("Try to get the offset parent for a fixed positioned node"); + gWalker.querySelector(gWalker.rootNode, "#fixed").then(node => { + return gWalker.getOffsetParent(node); + }).then(offsetParent => { + ok(offsetParent === gBodyNode || offsetParent === gHTMLNode, + "The node's offset parent is the body or html node"); + runNextTest(); + }); +}); + +addTest(function() { + info("Try to get the offset parent for the body"); + gWalker.querySelector(gWalker.rootNode, "body").then(node => { + return gWalker.getOffsetParent(node); + }).then(offsetParent => { + ok(!offsetParent, "The body has no offset parent"); + runNextTest(); + }); +}); + +addTest(function() { + gWalker = null; + gBodyNode = null; + runNextTest(); +}); + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1345119">Mozilla Bug 1345119</a> +<a id="inspectorContent" target="_blank" href="inspector_getOffsetParent.html">Test Document</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_makeGlobalObjectReference.html b/devtools/server/tests/chrome/test_makeGlobalObjectReference.html new file mode 100644 index 0000000000..d800798427 --- /dev/null +++ b/devtools/server/tests/chrome/test_makeGlobalObjectReference.html @@ -0,0 +1,96 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=914405 + +Debugger.prototype.makeGlobalObjectReference should dereference WindowProxy +(outer window) objects. +--> +<head> + <meta charset="utf-8"> + <title>Mozilla Bug 914405</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +</head> +<body> +<pre id="test"> +<script> +"use strict"; + +const {addSandboxedDebuggerToGlobal} = ChromeUtils.importESModule("resource://gre/modules/jsdebugger.sys.mjs"); +addSandboxedDebuggerToGlobal(globalThis); + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + + // Load one of our iframes over http to force it in a different compartment + // from the current window and the other iframe. + const iframe = document.createElement("iframe"); + const baseURL = "http://mochi.test:8888/chrome/devtools/server/tests/chrome/"; + iframe.src = baseURL + "iframe1_makeGlobalObjectReference.html"; + iframe.onload = iframeOnLoad; + document.body.appendChild(iframe); + + function iframeOnLoad() { + const dbg = new Debugger(); + + // 'o' for 'outer window' + const g1o = iframe.contentWindow; + ok(!dbg.hasDebuggee(g1o), "iframe is not initially a debuggee"); + + // Like addDebuggee, makeGlobalObjectReference innerizes. + // 'i' stands for 'inner window'. + // 'DO' stands for 'Debugger.Object'. + const g1iDO = dbg.makeGlobalObjectReference(g1o); + ok(!dbg.hasDebuggee(g1o), + "makeGlobalObjectReference does not add g1 as debuggee, designated via outer"); + ok(!dbg.hasDebuggee(g1iDO), + "makeGlobalObjectReference does not add g1 as debuggee, designated via D.O "); + + // Wrapping an object automatically outerizes it, so dereferencing an + // inner object D.O gets you an outer object. + // ('===' does distinguish inner and outer objects.) + // (That's a capital '=', if you must know.) + ok(g1iDO.unsafeDereference() === g1o, "g1iDO has the right referent"); + + // However, Debugger.Objects do distinguish inner and outer windows. + const g1oDO = g1iDO.makeDebuggeeValue(g1o); + ok(g1iDO !== g1oDO, "makeDebuggeeValue doesn't innerize"); + ok(g1iDO.unsafeDereference() === g1oDO.unsafeDereference(), + "unsafeDereference() outerizes," + + " so inner and outer window D.Os both dereference to outer"); + + ok(dbg.addDebuggee(g1o) === g1iDO, "addDebuggee returns the inner window's D.O"); + ok(dbg.hasDebuggee(g1o), "addDebuggee adds the correct global"); + ok(dbg.hasDebuggee(g1iDO), + "hasDebuggee can take a D.O referring to the inner window"); + ok(dbg.hasDebuggee(g1oDO), + "hasDebuggee can take a D.O referring to the outer window"); + + const iframe2 = document.createElement("iframe"); + iframe2.src = "iframe2_makeGlobalObjectReference.html"; + iframe2.onload = iframe2OnLoad; + document.body.appendChild(iframe2); + + function iframe2OnLoad() { + // makeGlobalObjectReference dereferences CCWs. + const g2o = iframe2.contentWindow; + g2o.g1o = g1o; + + const g2iDO = dbg.addDebuggee(g2o); + const g2g1oDO = g2iDO.getOwnPropertyDescriptor("g1o").value; + ok(g2g1oDO !== g1oDO, "g2's cross-compartment wrapper for g1o gets its own D.O"); + ok(g2g1oDO.unwrap() === g1oDO, + "unwrapping g2's cross-compartment wrapper for g1o gets the right D.O"); + ok(dbg.makeGlobalObjectReference(g2g1oDO) === g1iDO, + "makeGlobalObjectReference unwraps cross-compartment wrappers, and innerizes"); + + SimpleTest.finish(); + } + } +}; + +</script> +</pre> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_memory.html b/devtools/server/tests/chrome/test_memory.html new file mode 100644 index 0000000000..79ba29c913 --- /dev/null +++ b/devtools/server/tests/chrome/test_memory.html @@ -0,0 +1,39 @@ +<!DOCTYPE HTML> +<html> +<!-- +Bug 923275 - Add a memory monitor widget to the developer toolbar +--> +<head> + <meta charset="utf-8"> + <title>Memory monitoring actor test</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +</head> +<body> +<pre id="test"> +<script src="memory-helpers.js" type="application/javascript"></script> +<script> +"use strict"; + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + + (async function() { + const { memory, target } = await startServerAndGetSelectedTabMemory(); + const measurement = await memory.measure(); + ok(measurement.total > 0, "total memory is valid"); + ok(measurement.domSize > 0, "domSize is valid"); + ok(measurement.styleSize > 0, "styleSize is valid"); + ok(measurement.jsObjectsSize > 0, "jsObjectsSize is valid"); + ok(measurement.jsStringsSize > 0, "jsStringsSize is valid"); + ok(measurement.jsOtherSize > 0, "jsOtherSize is valid"); + ok(measurement.otherSize > 0, "otherSize is valid"); + ok(measurement.jsMilliseconds, "jsMilliseconds is valid"); + ok(measurement.nonJSMilliseconds, "nonJSMilliseconds is valid"); + await destroyServerAndFinish(target); + })(); +}; +</script> +</pre> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_memory_allocations_02.html b/devtools/server/tests/chrome/test_memory_allocations_02.html new file mode 100644 index 0000000000..632903bc04 --- /dev/null +++ b/devtools/server/tests/chrome/test_memory_allocations_02.html @@ -0,0 +1,80 @@ +<!DOCTYPE HTML> +<html> +<!-- +Bug 1132764 - Test controlling the maximum allocations log length over the RDP. +--> +<head> + <meta charset="utf-8"> + <title>Memory monitoring actor test</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +</head> +<body> +<pre id="test"> +<script src="memory-helpers.js" type="application/javascript"></script> +<script> +"use strict"; + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + + (async function() { + const { memory, target } = await startServerAndGetSelectedTabMemory(); + await memory.attach(); + + const allocs = []; + let eventsFired = 0; + let intervalId = null; + function onAlloc() { + eventsFired++; + } + function startAllocating() { + intervalId = setInterval(() => { + for (let i = 100000; --i;) { + allocs.push({}); + } + }, 1); + } + function stopAllocating() { + clearInterval(intervalId); + } + + memory.on("allocations", onAlloc); + + await memory.startRecordingAllocations({ + drainAllocationsTimeout: 10, + }); + + await waitUntil(() => eventsFired > 5); + ok(eventsFired > 5, + "Some allocation events fired without allocating much via auto drain"); + await memory.stopRecordingAllocations(); + + // Set a really high auto drain timer so we can test if + // it fires on GC + eventsFired = 0; + const startTime = performance.now(); + const drainTimer = 1000000; + await memory.startRecordingAllocations({ + drainAllocationsTimeout: drainTimer, + }); + + startAllocating(); + await waitUntil(() => { + Cu.forceGC(); + return eventsFired > 1; + }); + stopAllocating(); + ok(performance.now() - drainTimer < startTime, + "Allocation events fired on GC before timer"); + await memory.stopRecordingAllocations(); + + memory.off("allocations", onAlloc); + await memory.detach(); + destroyServerAndFinish(target); + })(); +}; +</script> +</pre> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_memory_allocations_03.html b/devtools/server/tests/chrome/test_memory_allocations_03.html new file mode 100644 index 0000000000..ca6a1ec1b4 --- /dev/null +++ b/devtools/server/tests/chrome/test_memory_allocations_03.html @@ -0,0 +1,80 @@ +<!DOCTYPE HTML> +<html> +<!-- +Bug 1067491 - Test that frames keep the same index while we are recording. +--> +<head> + <meta charset="utf-8"> + <title>Memory monitoring actor test</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +</head> +<body> +<pre id="test"> +<script src="memory-helpers.js" type="application/javascript"></script> +<script> +"use strict"; + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + + (async function() { + const { memory, target } = await startServerAndGetSelectedTabMemory(); + await memory.attach(); + + await memory.startRecordingAllocations(); + + // Allocate twice with the exact same stack (hence setTimeout rather than + // allocating directly in the generator), but with getAllocations() calls in + // between. + + const allocs = []; + function allocator() { + allocs.push({}); + } + + setTimeout(allocator, 1); + await waitForTime(2); + const first = await memory.getAllocations(); + + setTimeout(allocator, 1); + await waitForTime(2); + const second = await memory.getAllocations(); + + await memory.stopRecordingAllocations(); + + // Assert that each frame in the first response has the same index in the + // second response. This isn't commutative, so we don't check that all + // of the second response's frames are the same in the first response, + // because there might be new allocations that happen after the first query + // but before the second. + + function assertSameFrame(a, b) { + info(" First frame = " + JSON.stringify(a, null, 4)); + info(" Second frame = " + JSON.stringify(b, null, 4)); + + is(!!a, !!b); + if (!a || !b) { + return; + } + + is(a.source, b.source); + is(a.line, b.line); + is(a.column, b.column); + is(a.functionDisplayName, b.functionDisplayName); + is(a.parent, b.parent); + } + + for (let i = 0; i < first.frames.length; i++) { + info("Checking frames at index " + i + ":"); + assertSameFrame(first.frames[i], second.frames[i]); + } + + await memory.detach(); + destroyServerAndFinish(target); + })(); +}; +</script> +</pre> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_memory_allocations_04.html b/devtools/server/tests/chrome/test_memory_allocations_04.html new file mode 100644 index 0000000000..8bb64c591c --- /dev/null +++ b/devtools/server/tests/chrome/test_memory_allocations_04.html @@ -0,0 +1,62 @@ +<!DOCTYPE HTML> +<html> +<!-- +Bug 1068171 - Test controlling the memory actor's allocation sampling probability. +--> +<head> + <meta charset="utf-8"> + <title>Memory monitoring actor test</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +</head> +<body> +<pre id="test"> +<script src="memory-helpers.js" type="application/javascript"></script> +<script> +"use strict"; + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + + (async function() { + const { memory, target } = await startServerAndGetSelectedTabMemory(); + await memory.attach(); + + const allocs = []; + function allocator() { + for (let i = 0; i < 100; i++) { + allocs.push({}); + } + } + + const testProbability = async function(p, expected) { + info("probability = " + p); + await memory.startRecordingAllocations({ + probability: p, + }); + allocator(); + const response = await memory.getAllocations(); + await memory.stopRecordingAllocations(); + return response.allocations.length; + }; + + is((await testProbability(0.0)), 0, + "With probability = 0.0, we shouldn't get any allocations."); + + ok((await testProbability(1.0)) >= 100, + "With probability = 1.0, we should get all 100 allocations (plus " + + "whatever allocations the actor and SpiderMonkey make)."); + + // We don't test any other probabilities because the test would be + // non-deterministic. We don't have a way to control the PRNG like we do in + // jit-tests + // (js/src/jit-test/tests/debug/Memory-allocationsSamplingProbability-*.js). + + await memory.detach(); + destroyServerAndFinish(target); + })(); +}; +</script> +</pre> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_memory_allocations_05.html b/devtools/server/tests/chrome/test_memory_allocations_05.html new file mode 100644 index 0000000000..590b3358e4 --- /dev/null +++ b/devtools/server/tests/chrome/test_memory_allocations_05.html @@ -0,0 +1,93 @@ +<!DOCTYPE HTML> +<html> +<!-- +Bug 1068144 - Test getting the timestamps for allocations. +--> +<head> + <meta charset="utf-8"> + <title>Memory monitoring actor test</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +</head> +<body> +<pre id="test"> +<script src="memory-helpers.js" type="application/javascript"></script> +<script> +"use strict"; + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + + (async function() { + const { memory, target } = await startServerAndGetSelectedTabMemory(); + await memory.attach(); + + const allocs = []; + function allocator() { + allocs.push({}); + } + + // Using setTimeout results in wildly varying delays that make it hard to + // test our timestamps and results in intermittent failures. Instead, we + // actually spin an empty loop for a whole millisecond. + function actuallyWaitOneWholeMillisecond() { + const start = window.performance.now(); + // eslint-disable-next-line curly + while (window.performance.now() - start < 1.000); + } + + await memory.startRecordingAllocations(); + + allocator(); + actuallyWaitOneWholeMillisecond(); + allocator(); + actuallyWaitOneWholeMillisecond(); + allocator(); + + const response = await memory.getAllocations(); + await memory.stopRecordingAllocations(); + + ok(response.allocationsTimestamps, "The response should have timestamps."); + is(response.allocationsTimestamps.length, response.allocations.length, + "There should be a timestamp for every allocation."); + + const allocatorIndices = response.allocations + .map(function(a, idx) { + const frame = response.frames[a]; + if (frame && frame.functionDisplayName === "allocator") { + return idx; + } + return null; + }) + .filter(function(idx) { + return idx !== null; + }); + + is(allocatorIndices.length, 3, + "Should have our 3 allocations from the `allocator` timeouts."); + + let lastTimestamp; + for (let i = 0; i < 3; i++) { + const timestamp = response.allocationsTimestamps[allocatorIndices[i]]; + info("timestamp", timestamp); + ok(timestamp, "We should have a timestamp for the `allocator` allocation."); + + if (lastTimestamp) { + const delta = timestamp - lastTimestamp; + info("delta since last timestamp", delta); + // ms + ok(delta >= 1, + "The timestamp should be about 1 ms after the last timestamp."); + } + + lastTimestamp = timestamp; + } + + await memory.detach(); + destroyServerAndFinish(target); + })(); +}; +</script> +</pre> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_memory_allocations_06.html b/devtools/server/tests/chrome/test_memory_allocations_06.html new file mode 100644 index 0000000000..d6223dd062 --- /dev/null +++ b/devtools/server/tests/chrome/test_memory_allocations_06.html @@ -0,0 +1,51 @@ +<!DOCTYPE HTML> +<html> +<!-- +Bug 1132764 - Test controlling the maximum allocations log length over the RDP. +--> +<head> + <meta charset="utf-8"> + <title>Memory monitoring actor test</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +</head> +<body> +<pre id="test"> +<script src="memory-helpers.js" type="application/javascript"></script> +<script> +"use strict"; + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + + (async function() { + const { memory, target } = await startServerAndGetSelectedTabMemory(); + await memory.attach(); + + const allocs = []; + function allocator() { + allocs.push({}); + } + + await memory.startRecordingAllocations({ + maxLogLength: 1, + }); + + allocator(); + allocator(); + allocator(); + + const response = await memory.getAllocations(); + await memory.stopRecordingAllocations(); + + is(response.allocations.length, 1, + "There should only be one entry in the allocations log."); + + await memory.detach(); + destroyServerAndFinish(target); + })(); +}; +</script> +</pre> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_memory_allocations_07.html b/devtools/server/tests/chrome/test_memory_allocations_07.html new file mode 100644 index 0000000000..ce5ba4d2ad --- /dev/null +++ b/devtools/server/tests/chrome/test_memory_allocations_07.html @@ -0,0 +1,58 @@ +<!DOCTYPE HTML> +<html> +<!-- +Bug 1192335 - Test getting the byte sizes for allocations. +--> +<head> + <meta charset="utf-8"> + <title>Memory monitoring actor test</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +</head> +<body> +<pre id="test"> +<script src="memory-helpers.js" type="application/javascript"></script> +<script> +"use strict"; + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + + (async function() { + const { memory, target } = await startServerAndGetSelectedTabMemory(); + await memory.attach(); + + const allocs = []; + function allocator() { + allocs.push({}); + } + + await memory.startRecordingAllocations(); + + allocator(); + allocator(); + allocator(); + + const response = await memory.getAllocations(); + await memory.stopRecordingAllocations(); + + ok(response.allocationSizes, "The response should have bytesizes."); + is(response.allocationSizes.length, response.allocations.length, + "There should be a bytesize for every allocation."); + ok(response.allocationSizes.length >= 3, + "There are atleast 3 allocations."); + ok(response.allocationSizes.every(isPositiveNumber), + "every bytesize is a positive number"); + + await memory.detach(); + destroyServerAndFinish(target); + })(); +}; + +function isPositiveNumber(n) { + return typeof n === "number" && n > 0; +} +</script> +</pre> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_memory_attach_01.html b/devtools/server/tests/chrome/test_memory_attach_01.html new file mode 100644 index 0000000000..89f1818292 --- /dev/null +++ b/devtools/server/tests/chrome/test_memory_attach_01.html @@ -0,0 +1,33 @@ +<!DOCTYPE HTML> +<html> +<!-- +Bug 960671 - Test attaching and detaching from a memory actor. +--> +<head> + <meta charset="utf-8"> + <title>Memory monitoring actor test</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +</head> +<body> +<pre id="test"> +<script src="memory-helpers.js" type="application/javascript"></script> +<script> +"use strict"; + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + + (async function() { + const { memory, target } = await startServerAndGetSelectedTabMemory(); + await memory.attach(); + ok(true, "Shouldn't have gotten an error attaching."); + await memory.detach(); + ok(true, "Shouldn't have gotten an error detaching."); + destroyServerAndFinish(target); + })(); +}; +</script> +</pre> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_memory_attach_02.html b/devtools/server/tests/chrome/test_memory_attach_02.html new file mode 100644 index 0000000000..89e23f5ed6 --- /dev/null +++ b/devtools/server/tests/chrome/test_memory_attach_02.html @@ -0,0 +1,44 @@ +<!DOCTYPE HTML> +<html> +<!-- +Bug 960671 - Test attaching and detaching while in the wrong state. +--> +<head> + <meta charset="utf-8"> + <title>Memory monitoring actor test</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +</head> +<body> +<pre id="test"> +<script src="memory-helpers.js" type="application/javascript"></script> +<script> +"use strict"; + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + + (async function() { + const { memory, target } = await startServerAndGetSelectedTabMemory(); + + let e = null; + try { + await memory.detach(); + } catch (ee) { + e = ee; + } + ok(e, "Should have hit the wrongState error"); + + await memory.attach(); + + await memory.attach(); + ok(true, "We can call attach many times, the duplicates will be ignored"); + + await memory.detach(); + destroyServerAndFinish(target); + })(); +}; +</script> +</pre> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_memory_census.html b/devtools/server/tests/chrome/test_memory_census.html new file mode 100644 index 0000000000..3c351740d3 --- /dev/null +++ b/devtools/server/tests/chrome/test_memory_census.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html> +<!-- +Bug 1067491 - Test taking a census over the RDP. +--> +<head> + <meta charset="utf-8"> + <title>Memory monitoring actor test</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +</head> +<body> +<pre id="test"> +<script src="memory-helpers.js" type="application/javascript"></script> +<script> +"use strict"; + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + + (async function() { + const { memory, target } = await startServerAndGetSelectedTabMemory(); + await memory.attach(); + + const census = await memory.takeCensus(); + is(typeof census, "object"); + + await memory.detach(); + destroyServerAndFinish(target); + })(); +}; +</script> +</pre> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_memory_gc_01.html b/devtools/server/tests/chrome/test_memory_gc_01.html new file mode 100644 index 0000000000..8b2f049602 --- /dev/null +++ b/devtools/server/tests/chrome/test_memory_gc_01.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<html> +<!-- +Bug 1067491 - Test forcing a gc. +--> +<head> + <meta charset="utf-8"> + <title>Memory monitoring actor test</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +</head> +<body> +<pre id="test"> +<script src="memory-helpers.js" type="application/javascript"></script> +<script> +"use strict"; + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + + (async function() { + const { memory, target } = await startServerAndGetSelectedTabMemory(); + + let beforeGC, afterGC; + + do { + let objects = []; + for (let i = 0; i < 1000; i++) { + const o = {}; + o[Math.random()] = 1; + objects.push(o); + } + objects = null; + + beforeGC = (await memory.measure()).total; + + await memory.forceGarbageCollection(); + + afterGC = (await memory.measure()).total; + } while (beforeGC < afterGC); + + ok(true, "The amount of memory after GC should eventually decrease"); + + destroyServerAndFinish(target); + })(); +}; +</script> +</pre> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_memory_gc_events.html b/devtools/server/tests/chrome/test_memory_gc_events.html new file mode 100644 index 0000000000..5db1607c91 --- /dev/null +++ b/devtools/server/tests/chrome/test_memory_gc_events.html @@ -0,0 +1,44 @@ +<!DOCTYPE HTML> +<html> +<!-- +Bug 1137527 - Test receiving GC events from the memory actor. +--> +<head> + <meta charset="utf-8"> + <title>Memory monitoring actor test</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +</head> +<body> +<pre id="test"> +<script src="memory-helpers.js" type="application/javascript"></script> +<script> +"use strict"; + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + + const EventEmitter = require("devtools/shared/event-emitter"); + + (async function() { + const { memory, target } = await startServerAndGetSelectedTabMemory(); + await memory.attach(); + + const gotGcEvent = new Promise(resolve => { + EventEmitter.on(memory, "garbage-collection", gcData => { + ok(gcData, "Got GC data"); + resolve(); + }); + }); + + memory.forceGarbageCollection(); + await gotGcEvent; + + await memory.detach(); + destroyServerAndFinish(target); + })(); +}; +</script> +</pre> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_overflowing-body.html b/devtools/server/tests/chrome/test_overflowing-body.html new file mode 100644 index 0000000000..1fe52e0011 --- /dev/null +++ b/devtools/server/tests/chrome/test_overflowing-body.html @@ -0,0 +1,42 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test InspectorUtils.GetOverflowingChildrenOfElement applied to the body element +--> +<head> +<meta charset="utf-8"> +<title>Test InspectorUtils.GetOverflowingChildrenOfElement on the body element</title> +<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> +<style> +body { + overflow: auto; + margin: 0; +} +.tallBox { + overflow: auto; + background: lavender; + width: 200px; + height: 110vh; +} +</style> +<script> +'use strict'; + +SimpleTest.waitForExplicitFinish(); +const InspectorUtils = SpecialPowers.InspectorUtils; + +function runTests() { + const body = document.documentElement; + const overflowing_children = InspectorUtils.getOverflowingChildrenOfElement(body); + + is(overflowing_children.length, 1, `body has the expected number of children.`); + + SimpleTest.finish(); +}; +window.onload = runTests; +</script> +</head> +<body> +<div class="tallBox"><div> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_overflowing-children.html b/devtools/server/tests/chrome/test_overflowing-children.html new file mode 100644 index 0000000000..8ba81bec3d --- /dev/null +++ b/devtools/server/tests/chrome/test_overflowing-children.html @@ -0,0 +1,131 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test InspectorUtils.GetOverflowingChildrenOfElement in various cases +--> +<head> +<meta charset="utf-8"> +<title>Test InspectorUtils.GetOverflowingChildrenOfElement</title> +<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> +<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> + +<script> +'use strict'; + +SimpleTest.waitForExplicitFinish(); +const InspectorUtils = SpecialPowers.InspectorUtils; + +const CASES = [ + {id: "no_children", expected: 0}, + {id: "one_child_no_overflow", expected: 0}, + {id: "margin_left_overflow", expected: 1}, + {id: "transform_overflow", expected: 1}, + {id: "nested_overflow", expected: 1}, + {id: "intermediate_overflow", expected: 1}, + {id: "multiple_overflow_at_different_depths", expected: 2}, +]; + +function runTests() { + // Assign each child element to an inner id so each of them can be identified for testing. + Array.from(document.getElementsByTagName('c')).forEach((e, i) => {e.id = `inner${i}`}); + + for (const {id, expected} of CASES) { + info(`Checking element id ${id}.`); + + const element = document.getElementById(id); + if (!element) { + ok(false, `Expected to find element with id ${id}.`); + continue; + } + const overflowing_children = InspectorUtils.getOverflowingChildrenOfElement(element); + + is(overflowing_children.length, expected, `${id} has the expected number of children.`); + + // Check that each child has the "target" class. Otherwise, we're getting the + // wrong children. We don't check each child with a test function, because we + // don't want to needlessly inflate the number of test functions in the log. + // But if we find a child that *doesn't* have the class "target", we report + // that as a test failure. + for (const child of overflowing_children) { + // child is a Node, but not necessarily an Element. We want to get the containing + // Element so that we can use its classList, tagName, and id properties. + let e = child; + if (child.nodeType !== Node.ELEMENT_NODE) { + e = child.parentElement; + } + if (!e.classList.contains("target")) { + ok(false, `${id} is reporting this unexpected child as a target: ${e.tagName} id=${e.id}`); + } + } + } + + SimpleTest.finish(); +}; +window.onload = runTests; +</script> +</head> +<body onload="runTests()"> + +<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> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_preference.html b/devtools/server/tests/chrome/test_preference.html new file mode 100644 index 0000000000..b4d23a24aa --- /dev/null +++ b/devtools/server/tests/chrome/test_preference.html @@ -0,0 +1,128 @@ +<!DOCTYPE HTML> +<html> +<!-- +Bug 943251 - Test preferences actor +--> +<head> + <meta charset="utf-8"> + <title>Test Preference Actor</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +</head> +<body> +<pre id="test"> +<script> +"use strict"; + +function runTests() { + const {require} = ChromeUtils.importESModule("resource://devtools/shared/loader/Loader.sys.mjs"); + const {DevToolsClient} = require("devtools/client/devtools-client"); + const {DevToolsServer} = require("devtools/server/devtools-server"); + + SimpleTest.waitForExplicitFinish(); + + DevToolsServer.init(); + DevToolsServer.registerAllActors(); + + const client = new DevToolsClient(DevToolsServer.connectPipe()); + client.connect().then(function onConnect() { + return client.mainRoot.getFront("preference"); + }).then(function(p) { + const prefs = {}; + + const localPref = { + boolPref: true, + intPref: 0x1234, + charPref: "Hello World", + }; + + function checkValues() { + is(prefs.boolPref, localPref.boolPref, "read/write bool pref"); + is(prefs.intPref, localPref.intPref, "read/write int pref"); + is(prefs.charPref, localPref.charPref, "read/write string pref"); + + ["test.all.bool", "test.all.int", "test.all.string"].forEach(function(key) { + let expectedValue; + switch (Services.prefs.getPrefType(key)) { + case Ci.nsIPrefBranch.PREF_STRING: + expectedValue = Services.prefs.getCharPref(key); + break; + case Ci.nsIPrefBranch.PREF_INT: + expectedValue = Services.prefs.getIntPref(key); + break; + case Ci.nsIPrefBranch.PREF_BOOL: + expectedValue = Services.prefs.getBoolPref(key); + break; + default: + ok(false, "unexpected pref type (" + key + ")"); + break; + } + + is(prefs.allPrefs[key].value, expectedValue, + "valid preference value (" + key + ")"); + is(prefs.allPrefs[key].hasUserValue, Services.prefs.prefHasUserValue(key), + "valid hasUserValue (" + key + ")"); + }); + + ["test.bool", "test.int", "test.string"].forEach(function(key) { + ok(!prefs.allPrefs.hasOwnProperty(key), "expect no pref (" + key + ")"); + is(Services.prefs.getPrefType(key), Ci.nsIPrefBranch.PREF_INVALID, + "pref (" + key + ") is clear"); + }); + + client.close().then(() => { + DevToolsServer.destroy(); + SimpleTest.finish(); + }); + } + + function checkUndefined() { + let next = p.getCharPref("test.undefined"); + next = next.then( + () => ok(false, "getCharPref should've thrown for an undefined preference"), + (ex) => { + const messageRe = new RegExp( + "Protocol error \\(Error\\): preference is not of the right type: " + + `test.undefined from: ${p.actorID} ` + + "\\(resource://devtools/server/actors/preference.js:\\d+:\\d+\\)" + ); + ok(messageRe.test(ex.message), "Error message matches the expected format"); + } + ); + return next; + } + + function updatePrefsProperty(key) { + return function(value) { + prefs[key] = value; + }; + } + + p.getAllPrefs().then(updatePrefsProperty("allPrefs")) + .then(() => p.setBoolPref("test.bool", localPref.boolPref)) + .then(() => p.setIntPref("test.int", localPref.intPref)) + .then(() => p.setCharPref("test.string", localPref.charPref)) + .then(() => p.getBoolPref("test.bool")).then(updatePrefsProperty("boolPref")) + .then(() => p.getIntPref("test.int")).then(updatePrefsProperty("intPref")) + .then(() => p.getCharPref("test.string")).then(updatePrefsProperty("charPref")) + .then(() => p.clearUserPref("test.bool")) + .then(() => p.clearUserPref("test.int")) + .then(() => p.clearUserPref("test.string")) + .then(() => checkUndefined()) + .then(checkValues); + }); +} + +window.onload = function() { + SpecialPowers.pushPrefEnv({ + "set": [ + ["test.all.bool", true], + ["test.all.int", 0x4321], + ["test.all.string", "allizom"], + ], + }, runTests); +}; +</script> +</pre> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_styles-applied.html b/devtools/server/tests/chrome/test_styles-applied.html new file mode 100644 index 0000000000..0910e9d7bc --- /dev/null +++ b/devtools/server/tests/chrome/test_styles-applied.html @@ -0,0 +1,155 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id= +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug </title> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> + <script type="application/javascript" src="inspector-helpers.js"></script> + <script type="application/javascript"> +"use strict"; + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + runNextTest(); +}; + +let gWalker = null; +let gStyles = null; + +addTest(async function setup() { + const url = document.getElementById("inspectorContent").href; + const { commands, target } = await attachURL(url); + + // We need an active resource command before initializing the inspector front. + const resourceCommand = commands.resourceCommand; + // We listen to any random resource, we only need to trigger the resource command + // onTargetAvailable callback so the `resourceCommand` attribute is set on the target front. + await resourceCommand.watchResources([resourceCommand.TYPES.DOCUMENT_EVENT], { onAvailable: () => {} }); + + const inspector = await target.getFront("inspector"); + gWalker = inspector.walker; + gStyles = await inspector.getPageStyle(); + + runNextTest(); +}); + +addTest(async function inheritedUserStyles() { + const node = await gWalker.querySelector(gWalker.rootNode, "#test-node") + const applied = await gStyles.getApplied(node, { inherited: true, filter: "user" }); + + ok(!applied[0].inherited, "Entry 0 should be uninherited"); + is(applied[0].rule.type, 100, "Entry 0 should be an element style"); + ok(!!applied[0].rule.href, "Element styles should have a URL"); + is(applied[0].rule.cssText, "", "Entry 0 should be an empty style"); + + is(applied[1].inherited.id, "uninheritable-rule-inheritable-style", + "Entry 1 should be inherited from the parent"); + is(applied[1].rule.type, 100, "Entry 1 should be an element style"); + is(applied[1].rule.cssText, "color: red;", + "Entry 1 should have the expected cssText"); + + is(applied[2].inherited.id, "inheritable-rule-inheritable-style", + "Entry 2 should be inherited from the parent's parent"); + is(applied[2].rule.type, 100, "Entry 2 should be an element style"); + is(applied[2].rule.cssText, "color: blue;", + "Entry 2 should have the expected cssText"); + + is(applied[3].inherited.id, "inheritable-rule-inheritable-style", + "Entry 3 should be inherited from the parent's parent"); + is(applied[3].rule.type, 1, "Entry 3 should be a rule style"); + is(applied[3].rule.cssText, "font-size: 15px;", + "Entry 3 should have the expected cssText"); + ok(!applied[3].matchedDesugaredSelectors, + "Shouldn't get matchedDesugaredSelectors with this request."); + + is(applied[4].inherited.id, "inheritable-rule-uninheritable-style", + "Entry 4 should be inherited from the parent's parent"); + is(applied[4].rule.type, 1, "Entry 4 should be an rule style"); + is(applied[4].rule.cssText, "font-size: 15px;", + "Entry 4 should have the expected cssText"); + ok(!applied[4].matchedDesugaredSelectors, "Shouldn't get matchedDesugaredSelectors with this request."); + + is(applied.length, 5, "Should have 5 rules."); + + runNextTest(); +}); + +addTest(async function inheritedSystemStyles() { + const node = await gWalker.querySelector(gWalker.rootNode, "#test-node"); + const applied = await gStyles.getApplied(node, { inherited: true, filter: "ua" }); + // If our system stylesheets are prone to churn, this might be a fragile + // test. If you're here because of that I apologize, file a bug + // and we can find a different way to test. + + ok(!applied[1].inherited, "Entry 1 should not be inherited"); + ok(applied[1].rule.parentStyleSheet.system, "Entry 1 should be a system style"); + is(applied[1].rule.type, 1, "Entry 1 should be a rule style"); + is(applied.length, 9, "Should have the expected number of rules."); + + runNextTest(); +}); + +addTest(async function noInheritedStyles() { + const node = await gWalker.querySelector(gWalker.rootNode, "#test-node") + const applied = await gStyles.getApplied(node, { inherited: false, filter: "user" }); + ok(!applied[0].inherited, "Entry 0 should be uninherited"); + is(applied[0].rule.type, 100, "Entry 0 should be an element style"); + is(applied[0].rule.cssText, "", "Entry 0 should be an empty style"); + is(applied.length, 1, "Should have 1 rule."); + + runNextTest(); +}); + +addTest(async function matchedSelectors() { + const node = await gWalker.querySelector(gWalker.rootNode, "#test-node"); + const applied = await gStyles.getApplied(node, { + inherited: true, filter: "user", matchedSelectors: true, + }); + is(applied[3].matchedDesugaredSelectors[0], ".inheritable-rule", + "Entry 3 should have a matched selector"); + is(applied[4].matchedDesugaredSelectors[0], ".inheritable-rule", + "Entry 4 should have a matched selector"); + + runNextTest(); +}); + +addTest(async function testMediaQuery() { + const node = await gWalker.querySelector(gWalker.rootNode, "#mediaqueried") + const applied = await gStyles.getApplied(node, { + inherited: false, + filter: "user", + matchedSelectors: true, + }); + + const ruleWithMedia = applied[1].rule; + is(ruleWithMedia.type, 1, "Entry 1 is a rule style"); + is(ruleWithMedia.ancestorData[0].type, "media", "Entry 1's rule ancestor data holds the media rule data..."); + is(ruleWithMedia.ancestorData[0].value, "screen", "...with the expected value"); + + runNextTest(); +}); + +addTest(function cleanup() { + gStyles = null; + gWalker = null; + runNextTest(); +}); + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=">Mozilla Bug </a> +<a id="inspectorContent" target="_blank" href="inspector-styles-data.html">Test Document</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_styles-computed.html b/devtools/server/tests/chrome/test_styles-computed.html new file mode 100644 index 0000000000..9aa962108a --- /dev/null +++ b/devtools/server/tests/chrome/test_styles-computed.html @@ -0,0 +1,130 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id= +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug </title> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> + <script type="application/javascript" src="inspector-helpers.js"></script> + <script type="application/javascript"> +"use strict"; + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + runNextTest(); +}; + +let gWalker = null; +let gStyles = null; + +addTest(async function setup() { + const url = document.getElementById("inspectorContent").href; + const { target } = await attachURL(url); + const inspector = await target.getFront("inspector"); + gWalker = inspector.walker; + gStyles = await inspector.getPageStyle(); + runNextTest(); +}); + +addTest(function testComputed() { + promiseDone( + gWalker.querySelector(gWalker.rootNode, "#computed-test-node").then(node => { + return gStyles.getComputed(node, {}); + }).then(computed => { + // Test a smattering of properties that include some system-defined + // props, some props that were defined in this node's stylesheet, + // and some default props. + is(computed["white-space-collapse"].value, "collapse", "Default value should appear"); + is(computed.display.value, "block", "System stylesheet item should appear"); + is(computed.cursor.value, "crosshair", "Included stylesheet rule should appear"); + is(computed.color.value, "rgb(255, 0, 0)", + "Inherited style attribute should appear"); + is(computed["font-size"].value, "15px", "Inherited inline rule should appear"); + + // We didn't request markMatched, so these shouldn't be set + ok(!computed.cursor.matched, "Didn't ask for matched, shouldn't get it"); + ok(!computed.color.matched, "Didn't ask for matched, shouldn't get it"); + ok(!computed["font-size"].matched, "Didn't ask for matched, shouldn't get it"); + }).then(runNextTest) + ); +}); + +addTest(function testComputedUserMatched() { + promiseDone( + gWalker.querySelector(gWalker.rootNode, "#computed-test-node").then(node => { + return gStyles.getComputed(node, { filter: "user", markMatched: true }); + }).then(computed => { + ok(!computed["white-space-collapse"].matched, "Default style shouldn't match"); + ok(!computed.display.matched, "Only user styles should match"); + ok(computed.cursor.matched, "Asked for matched, should get it"); + ok(computed.color.matched, "Asked for matched, should get it"); + ok(computed["font-size"].matched, "Asked for matched, should get it"); + }).then(runNextTest) + ); +}); + +addTest(function testComputedSystemMatched() { + promiseDone( + gWalker.querySelector(gWalker.rootNode, "#computed-test-node").then(node => { + return gStyles.getComputed(node, { filter: "ua", markMatched: true }); + }).then(computed => { + ok(!computed["white-space-collapse"].matched, "Default style shouldn't match"); + ok(computed.display.matched, "System stylesheets should match"); + ok(computed.cursor.matched, "Asked for matched, should get it"); + ok(computed.color.matched, "Asked for matched, should get it"); + ok(computed["font-size"].matched, "Asked for matched, should get it"); + }).then(runNextTest) + ); +}); + +addTest(function testComputedUserOnlyMatched() { + promiseDone( + gWalker.querySelector(gWalker.rootNode, "#computed-test-node").then(node => { + return gStyles.getComputed(node, { filter: "user", onlyMatched: true }); + }).then(computed => { + ok(!("white-space-collapse" in computed), "Default style shouldn't exist"); + ok(!("display" in computed), "System stylesheets shouldn't exist"); + ok(("cursor" in computed), "User items should exist."); + ok(("color" in computed), "User items should exist."); + ok(("font-size" in computed), "User items should exist."); + }).then(runNextTest) + ); +}); + +addTest(function testComputedSystemOnlyMatched() { + promiseDone( + gWalker.querySelector(gWalker.rootNode, "#computed-test-node").then(node => { + return gStyles.getComputed(node, { filter: "ua", onlyMatched: true }); + }).then(computed => { + ok(!("white-space-collapse" in computed), "Default style shouldn't exist"); + ok(("display" in computed), "System stylesheets should exist"); + ok(("cursor" in computed), "User items should exist."); + ok(("color" in computed), "User items should exist."); + ok(("font-size" in computed), "User items should exist."); + }).then(runNextTest) + ); +}); + +addTest(function cleanup() { + gStyles = null; + gWalker = null; + runNextTest(); +}); + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=">Mozilla Bug </a> +<a id="inspectorContent" target="_blank" href="inspector-styles-data.html">Test Document</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_styles-layout.html b/devtools/server/tests/chrome/test_styles-layout.html new file mode 100644 index 0000000000..f0441edd13 --- /dev/null +++ b/devtools/server/tests/chrome/test_styles-layout.html @@ -0,0 +1,110 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf-8"> +<title>Test for Bug 1175040 - PageStyleActor.getLayout</title> +<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +<script type="application/javascript" src="inspector-helpers.js"></script> +<script type="application/javascript"> +"use strict"; + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + runNextTest(); +}; + +let gWalker = null; +let gStyles = null; + +addTest(async function() { + const url = document.getElementById("inspectorContent").href; + const { target } = await attachURL(url); + const inspector = await target.getFront("inspector"); + gWalker = inspector.walker; + gStyles = await inspector.getPageStyle(); + runNextTest(); +}); + +addTest(function() { + ok(gStyles.getLayout, "The PageStyleActor has a getLayout method"); + runNextTest(); +}); + +addAsyncTest(async function() { + const node = await gWalker.querySelector(gWalker.rootNode, "#layout-element"); + const layout = await gStyles.getLayout(node, {}); + + const properties = ["width", "height", + "margin-top", "margin-right", "margin-bottom", + "margin-left", "padding-top", "padding-right", + "padding-bottom", "padding-left", "border-top-width", + "border-right-width", "border-bottom-width", + "border-left-width", "z-index", "box-sizing", "display", + "position"]; + for (const prop of properties) { + ok((prop in layout), "The layout object returned has " + prop); + } + + runNextTest(); +}); + +addAsyncTest(async function() { + const node = await gWalker.querySelector(gWalker.rootNode, "#layout-element"); + const layout = await gStyles.getLayout(node, {}); + + const expected = { + "box-sizing": "border-box", + "position": "absolute", + "z-index": "2", + "display": "block", + "width": 50, + "height": 50, + "margin-top": "10px", + "margin-right": "20px", + "margin-bottom": "30px", + "margin-left": "0px", + }; + + for (const name in expected) { + is(layout[name], expected[name], "The " + name + " property is correct"); + } + + runNextTest(); +}); + +addAsyncTest(async function() { + const node = await gWalker.querySelector(gWalker.rootNode, + "#layout-auto-margin-element"); + + let layout = await gStyles.getLayout(node, {}); + ok(!("autoMargins" in layout), + "By default, getLayout doesn't return auto margins"); + + layout = await gStyles.getLayout(node, {autoMargins: true}); + ok(("autoMargins" in layout), + "getLayout does return auto margins when asked to"); + is(layout.autoMargins.left, "auto", "The left margin is auto"); + is(layout.autoMargins.right, "auto", "The right margin is auto"); + ok(!layout.autoMargins.bottom, "The bottom margin is not auto"); + ok(!layout.autoMargins.top, "The top margin is not auto"); + + runNextTest(); +}); + +addTest(function() { + gStyles = gWalker = null; + runNextTest(); +}); + +</script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1175040">Mozilla Bug 1175040</a> +<a id="inspectorContent" target="_blank" href="inspector-styles-data.html">Test Document</a> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_styles-matched.html b/devtools/server/tests/chrome/test_styles-matched.html new file mode 100644 index 0000000000..42e1ec7885 --- /dev/null +++ b/devtools/server/tests/chrome/test_styles-matched.html @@ -0,0 +1,110 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id= +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug </title> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> + <script type="application/javascript" src="inspector-helpers.js"></script> + <script type="application/javascript"> +"use strict"; + +const CssLogic = require("devtools/shared/inspector/css-logic"); + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + runNextTest(); +}; + +let gWalker = null; +let gStyles = null; +let gInspectee = null; + +addTest(async function setup() { + const url = document.getElementById("inspectorContent").href; + const { commands, target, doc } = await attachURL(url); + gInspectee = doc; + + // We need an active resource command before initializing the inspector front. + const resourceCommand = commands.resourceCommand; + // We listen to any random resource, we only need to trigger the resource command + // onTargetAvailable callback so the `resourceCommand` attribute is set on the target front. + await resourceCommand.watchResources([resourceCommand.TYPES.DOCUMENT_EVENT], { onAvailable: () => {} }); + + const inspector = await target.getFront("inspector"); + gWalker = inspector.walker; + gStyles = await inspector.getPageStyle(); + runNextTest(); +}); + +addTest(function testMatchedStyles() { + promiseDone(gWalker.querySelector(gWalker.rootNode, "#matched-test-node").then(node => { + return gStyles.getMatchedSelectors(node, "font-size", {}); + }).then(matched => { + is(matched[0].sourceText, "this.style", "First match comes from the element style"); + is(matched[0].selector, "@element.style", "Element style has a special selector"); + is(matched[0].value, "10px", "First match has the expected value"); + is(matched[0].status, CssLogic.STATUS.BEST, "First match is the best match"); + is(matched[0].rule.type, 100, "First match is an element style"); + is(matched[0].rule.href, gInspectee.defaultView.location.href, + "Node style comes from this document"); + + is(matched[1].sourceText, ".column-rule", + "Second match comes from a rule"); + is(matched[1].selector, ".column-rule", + "Second match comes from highest line number"); + is(matched[1].value, "25px", "Second match comes from highest column"); + is(matched[1].status, CssLogic.STATUS.PARENT_MATCH, + "Second match is from the parent"); + is(matched[1].rule.parentStyleSheet.href, null, + "Inline stylesheet shouldn't have an href"); + is(matched[1].rule.parentStyleSheet.nodeHref, gInspectee.defaultView.location.href, + "Inline stylesheet's nodeHref should match the current document"); + ok(!matched[1].rule.parentStyleSheet.system, + "Inline stylesheet shouldn't be a system stylesheet."); + + // matched[2] is only there to test matched[1]; do not need to test + + is(matched[3].value, "15px", "Third match has the expected value"); + }).then(runNextTest)); +}); + +addTest(function testSystemStyles() { + let testNode = null; + + promiseDone(gWalker.querySelector(gWalker.rootNode, "#matched-test-node").then(node => { + testNode = node; + return gStyles.getMatchedSelectors(testNode, "display", { filter: "user" }); + }).then(matched => { + is(matched.length, 0, "No user selectors apply to this rule."); + return gStyles.getMatchedSelectors(testNode, "display", { filter: "ua" }); + }).then(matched => { + is(matched[0].selector, "div", "Should match system div selector"); + is(matched[0].value, "block"); + }).then(runNextTest)); +}); + +addTest(function cleanup() { + gStyles = null; + gWalker = null; + gInspectee = null; + runNextTest(); +}); + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=">Mozilla Bug </a> +<a id="inspectorContent" target="_blank" href="inspector-styles-data.html">Test Document</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_styles-modify.html b/devtools/server/tests/chrome/test_styles-modify.html new file mode 100644 index 0000000000..e615ec4425 --- /dev/null +++ b/devtools/server/tests/chrome/test_styles-modify.html @@ -0,0 +1,110 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id= +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug </title> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> + <script type="application/javascript" src="inspector-helpers.js"></script> + <script type="application/javascript"> +"use strict"; + +const {isCssPropertyKnown} = require("devtools/server/actors/css-properties"); + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + runNextTest(); +}; + +var gWalker = null; +var gStyles = null; +var gInspectee = null; + +addAsyncTest(async function setup() { + const url = document.getElementById("inspectorContent").href; + + const { target, doc } = await attachURL(url); + const inspector = await target.getFront("inspector"); + gInspectee = doc; + + gWalker = inspector.walker; + gStyles = await inspector.getPageStyle(); + + runNextTest(); +}); + +addAsyncTest(async function modifyProperties() { + const localNode = gInspectee.querySelector("#inheritable-rule-inheritable-style"); + + const node = await gWalker.querySelector(gWalker.rootNode, + "#inheritable-rule-inheritable-style"); + + const applied = await gStyles.getApplied(node, + { inherited: false, filter: "user" }); + + const elementStyle = applied[0].rule; + is(elementStyle.cssText, localNode.style.cssText, "Got expected css text"); + + // Change an existing property... + await setProperty(elementStyle, 0, "color", "black"); + // Create a new property + await setProperty(elementStyle, 1, "background-color", "green"); + + // Create a new property and then change it immediately. + await setProperty(elementStyle, 2, "border", "1px solid black"); + await setProperty(elementStyle, 2, "border", "2px solid black"); + + is(elementStyle.cssText, + "color: black; background-color: green; border: 2px solid black;", + "Should have expected cssText"); + is(elementStyle.cssText, localNode.style.cssText, + "Local node and style front match."); + + // Remove all the properties + await removeProperty(elementStyle, 0, "color"); + await removeProperty(elementStyle, 0, "background-color"); + await removeProperty(elementStyle, 0, "border"); + + is(elementStyle.cssText, "", "Should have expected cssText"); + is(elementStyle.cssText, localNode.style.cssText, + "Local node and style front match."); + + runNextTest(); +}); + +async function setProperty(rule, index, name, value) { + const changes = rule.startModifyingProperties(isCssPropertyKnown); + changes.setProperty(index, name, value); + await changes.apply(); +} + +async function removeProperty(rule, index, name) { + const changes = rule.startModifyingProperties(isCssPropertyKnown); + changes.removeProperty(index, name); + await changes.apply(); +} + +addTest(function cleanup() { + gStyles = null; + gWalker = null; + gInspectee = null; + runNextTest(); +}); + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=">Mozilla Bug </a> +<a id="inspectorContent" target="_blank" href="inspector-styles-data.html">Test Document</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_styles-svg.html b/devtools/server/tests/chrome/test_styles-svg.html new file mode 100644 index 0000000000..b03bc868b7 --- /dev/null +++ b/devtools/server/tests/chrome/test_styles-svg.html @@ -0,0 +1,61 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=921191 +Bug 921191 - allow inspection/editing of SVG elements' CSS properties +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug </title> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> + <script type="application/javascript" src="inspector-helpers.js"></script> + <script type="application/javascript"> +"use strict"; + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + runNextTest(); +}; + +let gWalker = null; +let gStyles = null; + +addTest(async function setup() { + const url = document.getElementById("inspectorContent").href; + const { target } = await attachURL(url); + const inspector = await target.getFront("inspector"); + gWalker = inspector.walker; + gStyles = await inspector.getPageStyle(); + runNextTest(); +}); + +addTest(function inheritedUserStyles() { + promiseDone(gWalker.querySelector(gWalker.rootNode, "#svgcontent rect").then(node => { + return gStyles.getApplied(node, { inherited: true, filter: "user" }); + }).then(applied => { + is(applied.length, 2, "Should have 2 rules"); + is(applied[1].rule.cssText, "fill: rgb(1, 2, 3);", "cssText is right"); + }).then(runNextTest)); +}); + +addTest(function cleanup() { + gStyles = null; + gWalker = null; + runNextTest(); +}); + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=921191">Mozilla Bug 921191</a> +<a id="inspectorContent" target="_blank" href="inspector-styles-data.html">Test Document</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_suspendTimeouts.html b/devtools/server/tests/chrome/test_suspendTimeouts.html new file mode 100644 index 0000000000..65a168986f --- /dev/null +++ b/devtools/server/tests/chrome/test_suspendTimeouts.html @@ -0,0 +1,20 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1426467 + +When we use windowUtils.resumeTimeouts to resume timeouts in a window, that call +should not immediately dispatch `onmessage` handlers for messages from workers. +--> +<head> + <meta charset="utf-8"> + <title>Mozilla Bug 1426467</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +</head> +<body> +<pre id="test"> +<script src='test_suspendTimeouts.js'></script> +</pre> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_suspendTimeouts.js b/devtools/server/tests/chrome/test_suspendTimeouts.js new file mode 100644 index 0000000000..614ac60cdb --- /dev/null +++ b/devtools/server/tests/chrome/test_suspendTimeouts.js @@ -0,0 +1,139 @@ +"use strict"; + +// The debugger uses nsIDOMWindowUtils::suspendTimeouts and ...::resumeTimeouts +// to ensure that content event handlers do not run while a JavaScript +// invocation is stepping or paused at a breakpoint. If a worker thread sends +// messages to the content while the content is paused, those messages must not +// run until the JavaScript invocation interrupted by the debugger has completed. +// +// Bug 1426467 is that calling nsIDOMWindowUtils::resumeTimeouts actually +// delivers deferred messages itself, calling the content's 'onmessage' handler. +// But the debugger calls suspend/resume around each individual interruption of +// the debuggee -- each step, say -- meaning that hitting the "step into" button +// causes you to step from the debuggee directly into an onmessage handler, +// since the onmessage handler is the next function call the debugger sees. +// +// In other words, delivering deferred messages from resumeTimeouts, as it is +// used by the debugger, breaks the run-to-completion rule. They must not be +// delivered until after the JavaScript invocation at hand is complete. That's +// what this test checks. +// +// For this test to detect the bug, the following steps must take place in +// order: +// +// 1) The content page must call suspendTimeouts. +// 2) A runnable conveying a message from the worker thread must attempt to +// deliver the message, see that the content page has suspended such things, +// and hold the message for later delivery. +// 3) The content page must call resumeTimeouts. +// +// In a correct implementation, the message from the worker thread is delivered +// only after the main thread returns to the event loop after calling +// resumeTimeouts in step 3). In the buggy implementation, the onmessage handler +// is called directly from the call to resumeTimeouts, so that the onmessage +// handlers run in the midst of whatever JavaScript invocation resumed timeouts +// (say, stepping in the debugger), in violation of the run-to-completion rule. +// +// In this specific bug, the handlers are called from resumeTimeouts, but +// really, running them any time before that invocation returns to the main +// event loop would be a bug. +// +// Posting the message and calling resumeTimeouts take place in different +// threads, but if 2) and 3) don't occur in that order, the worker's message +// will never be delayed and the test will pass spuriously. But the worker +// can't communicate with the content page directly, to let it know that it +// should proceed with step 3): the purpose of suspendTimeouts is to pause +// all such communication. +// +// So instead, the content page creates a MessageChannel, and passes one +// MessagePort to the worker and the other to this mochitest (which has its +// own window, separate from the one calling suspendTimeouts). The worker +// notifies the mochitest when it has posted the message, and then the +// mochitest calls into the content to carry out step 3). + +// To help you follow all the callbacks and event handlers, this code pulls out +// event handler functions so that control flows from top to bottom. + +window.onload = function () { + // This mochitest is not complete until we call SimpleTest.finish. Don't just + // exit as soon as we return to the main event loop. + SimpleTest.waitForExplicitFinish(); + + const iframe = document.createElement("iframe"); + iframe.src = + "http://mochi.test:8888/chrome/devtools/server/tests/chrome/suspendTimeouts_content.html"; + iframe.onload = iframe_onload_handler; + document.body.appendChild(iframe); + + function iframe_onload_handler() { + const content = iframe.contentWindow.wrappedJSObject; + + const windowUtils = iframe.contentWindow.windowUtils; + + // Hand over the suspend and resume functions to the content page, along + // with some testing utilities. + content.suspendTimeouts = function () { + SimpleTest.info("test_suspendTimeouts", "calling suspendTimeouts"); + windowUtils.suspendTimeouts(); + }; + content.resumeTimeouts = function () { + windowUtils.resumeTimeouts(); + SimpleTest.info("test_suspendTimeouts", "resumeTimeouts called"); + }; + content.info = function (message) { + SimpleTest.info("suspendTimeouts_content.js", message); + }; + content.ok = SimpleTest.ok; + content.finish = finish; + + SimpleTest.info( + "Disappointed with National Tautology Day? Well, it is what it is." + ); + + // Once the worker has sent a message to its parent (which should get delayed), + // it sends us a message directly on this channel. + const workerPort = content.create_channel(); + workerPort.onmessage = handle_worker_echo; + + // Have content send the worker a message that it should echo back to both + // content and us. The echo to content should get delayed; the echo to us + // should cause our handle_worker_echo to be called. + content.start_worker(); + + function handle_worker_echo({ data }) { + info(`mochitest received message from worker: ${data}`); + + // As it turns out, it's not correct to assume that, if the worker posts a + // message to its parent via the global `postMessage` function, and then + // posts a message to the mochitest via the MessagePort, those two + // messages will be delivered in the order they were sent. + // + // - Messages sent via the worker's global's postMessage go through two + // ThrottledEventQueues (one in the worker, and one on the parent), and + // eventually find their way into the thread's primary event queue, + // which is a PrioritizedEventQueue. + // + // - Messages sent via a MessageChannel whose ports are owned by different + // threads are passed as IPDL messages. + // + // There's basically no reliable way to ensure that delivery to content + // has been attempted and the runnable deferred; there are too many + // variables affecting the order in which things are processed. Delaying + // for a second is the best I could think of. + // + // Fortunately, this tactic failing can only cause spurious test passes + // (the runnable never gets deferred, so things work by accident), not + // spurious failures. Without some kind of trustworthy notification that + // the runnable has been deferred, perhaps via some special white-box + // testing API, we can't do better. + setTimeout(() => { + content.resume_timeouts(); + }, 1000); + } + + function finish(message) { + SimpleTest.info("suspendTimeouts_content.js", "called finish"); + SimpleTest.finish(); + } + } +}; diff --git a/devtools/server/tests/chrome/test_unsafeDereference.html b/devtools/server/tests/chrome/test_unsafeDereference.html new file mode 100644 index 0000000000..eca1e7d43e --- /dev/null +++ b/devtools/server/tests/chrome/test_unsafeDereference.html @@ -0,0 +1,53 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=837723 + +When we use Debugger.Object.prototype.unsafeDereference to get a non-D.O +reference to a content object in chrome, that reference should be via an +xray wrapper. +--> +<head> + <meta charset="utf-8"> + <title>Mozilla Bug 837723</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +</head> +<body> +<pre id="test"> +<script> +"use strict"; + +const {addDebuggerToGlobal} = ChromeUtils.importESModule("resource://gre/modules/jsdebugger.sys.mjs"); +addDebuggerToGlobal(globalThis); + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + + const iframe = document.createElement("iframe"); + iframe.src = "http://mochi.test:8888/chrome/devtools/server/tests/chrome/nonchrome_unsafeDereference.html"; + + iframe.onload = function() { + const dbg = new Debugger(); + const contentDO = dbg.addDebuggee(iframe.contentWindow); + const xhrDesc = contentDO.getOwnPropertyDescriptor("xhr"); + + isnot(xhrDesc, undefined, "xhr should be visible as property of content global"); + isnot(xhrDesc.value, undefined, "xhr should have a value"); + + const xhr = xhrDesc.value.unsafeDereference(); + + is(typeof xhr, "object", "we should be able to deference xhr's value's D.O"); + is(xhr.timeout, 1742, "chrome should see the xhr's 'timeout' property"); + is(xhr.expando, undefined, "chrome should not see the xhr's 'expando' property"); + + SimpleTest.finish(); + }; + + document.body.appendChild(iframe); +}; + +</script> +</pre> +</body> +</html> diff --git a/devtools/server/tests/chrome/test_webconsole-node-grip.html b/devtools/server/tests/chrome/test_webconsole-node-grip.html new file mode 100644 index 0000000000..0c54f65964 --- /dev/null +++ b/devtools/server/tests/chrome/test_webconsole-node-grip.html @@ -0,0 +1,66 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>DOMNode Object actor test</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> + <script type="application/javascript" src="webconsole-helpers.js"></script> + <script> +"use strict"; + +const TEST_URL = "data:text/html,<html><body>Hello</body></html>"; + +window.onload = async function() { + SimpleTest.waitForExplicitFinish(); + + try { + const commands = await addTabAndCreateCommands(TEST_URL); + await testNotInTreeElementNode(commands); + await testInTreeElementNode(commands); + await testNotInTreeTextNode(commands); + await testInTreeTextNode(commands); + } catch (e) { + ok(false, `Error thrown: ${e.message}`); + } + SimpleTest.finish(); +}; + +async function testNotInTreeElementNode(commands) { + info("Testing isConnected property on a ElementNode not in the DOM tree"); + const {result} = await commands.scriptCommand.execute("document.createElement(\"div\")"); + is(result.getGrip().preview.isConnected, false, + "isConnected is false since we only created the element"); +} + +async function testInTreeElementNode(commands) { + info("Testing isConnected property on a ElementNode in the DOM tree"); + const {result} = await commands.scriptCommand.execute("document.body"); + is(result.getGrip().preview.isConnected, true, + "isConnected is true as expected, since the element was retrieved from the DOM tree"); +} + +async function testNotInTreeTextNode(commands) { + info("Testing isConnected property on a TextNode not in the DOM tree"); + const {result} = await commands.scriptCommand.execute("document.createTextNode(\"Hello\")"); + is(result.getGrip().preview.isConnected, false, + "isConnected is false since we only created the element"); +} + +async function testInTreeTextNode(commands) { + info("Testing isConnected property on a TextNode in the DOM tree"); + const {result} = await commands.scriptCommand.execute("document.body.firstChild"); + is(result.getGrip().preview.isConnected, true, + "isConnected is true as expected, since the element was retrieved from the DOM tree"); +} + + </script> +</head> +<body> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> +</body> +</html> diff --git a/devtools/server/tests/chrome/webconsole-helpers.js b/devtools/server/tests/chrome/webconsole-helpers.js new file mode 100644 index 0000000000..8be8554e35 --- /dev/null +++ b/devtools/server/tests/chrome/webconsole-helpers.js @@ -0,0 +1,54 @@ +/* exported addTabAndCreateCommands */ +"use strict"; + +const { require } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/Loader.sys.mjs" +); +const { + DevToolsServer, +} = require("resource://devtools/server/devtools-server.js"); +const { + CommandsFactory, +} = require("resource://devtools/shared/commands/commands-factory.js"); + +// Always log packets when running tests. +Services.prefs.setBoolPref("devtools.debugger.log", true); +SimpleTest.registerCleanupFunction(function () { + Services.prefs.clearUserPref("devtools.debugger.log"); +}); + +if (!DevToolsServer.initialized) { + DevToolsServer.init(); + DevToolsServer.registerAllActors(); + SimpleTest.registerCleanupFunction(function () { + DevToolsServer.destroy(); + }); +} + +/** + * Open a tab, load the url, find the tab with the devtools server, + * and attach the console to it. + * + * @param {string} url : url to navigate to + * @return {Promise} Promise resolving when commands are initialized + * The Promise resolves with the commands. + */ +async function addTabAndCreateCommands(url) { + const tab = await addTab(url); + const commands = await CommandsFactory.forTab(tab); + await commands.targetCommand.startListening(); + return commands; +} + +/** + * Naive implementaion of addTab working from a mochitest-chrome test. + */ +async function addTab(url) { + const { gBrowser } = Services.wm.getMostRecentWindow("navigator:browser"); + const { BrowserTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/BrowserTestUtils.sys.mjs" + ); + const tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, url)); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + return tab; +} diff --git a/devtools/server/tests/xpcshell/.eslintrc.js b/devtools/server/tests/xpcshell/.eslintrc.js new file mode 100644 index 0000000000..b3d0382a56 --- /dev/null +++ b/devtools/server/tests/xpcshell/.eslintrc.js @@ -0,0 +1,9 @@ +"use strict"; + +module.exports = { + // Extend from the common devtools xpcshell eslintrc config. + extends: "../../../.eslintrc.xpcshell.js", + rules: { + "no-debugger": 0, + }, +}; diff --git a/devtools/server/tests/xpcshell/addons/web-extension-upgrade/manifest.json b/devtools/server/tests/xpcshell/addons/web-extension-upgrade/manifest.json new file mode 100644 index 0000000000..cad9442b80 --- /dev/null +++ b/devtools/server/tests/xpcshell/addons/web-extension-upgrade/manifest.json @@ -0,0 +1,10 @@ +{ + "manifest_version": 2, + "name": "Test Addons Actor Upgrade", + "version": "1.0", + "browser_specific_settings": { + "gecko": { + "id": "test-addons-actor@mozilla.org" + } + } +} diff --git a/devtools/server/tests/xpcshell/addons/web-extension/manifest.json b/devtools/server/tests/xpcshell/addons/web-extension/manifest.json new file mode 100644 index 0000000000..47f07671e5 --- /dev/null +++ b/devtools/server/tests/xpcshell/addons/web-extension/manifest.json @@ -0,0 +1,10 @@ +{ + "manifest_version": 2, + "name": "Test Addons Actor", + "version": "1.0", + "browser_specific_settings": { + "gecko": { + "id": "test-addons-actor@mozilla.org" + } + } +} diff --git a/devtools/server/tests/xpcshell/addons/web-extension2/manifest.json b/devtools/server/tests/xpcshell/addons/web-extension2/manifest.json new file mode 100644 index 0000000000..e1ba91f4fb --- /dev/null +++ b/devtools/server/tests/xpcshell/addons/web-extension2/manifest.json @@ -0,0 +1,10 @@ +{ + "manifest_version": 2, + "name": "Test Addons Actor 2", + "version": "1.0", + "browser_specific_settings": { + "gecko": { + "id": "test-addons-actor2@mozilla.org" + } + } +} diff --git a/devtools/server/tests/xpcshell/completions.js b/devtools/server/tests/xpcshell/completions.js new file mode 100644 index 0000000000..5e77e4e886 --- /dev/null +++ b/devtools/server/tests/xpcshell/completions.js @@ -0,0 +1,23 @@ +"use strict"; +/* exported global doRet doThrow */ + +function ret() { + return 2; +} + +function throws() { + throw new Error("yo"); +} + +function doRet() { + debugger; + const r = ret(); + return r; +} + +function doThrow() { + debugger; + try { + throws(); + } catch (e) {} +} diff --git a/devtools/server/tests/xpcshell/head_dbg.js b/devtools/server/tests/xpcshell/head_dbg.js new file mode 100644 index 0000000000..7161d5eaea --- /dev/null +++ b/devtools/server/tests/xpcshell/head_dbg.js @@ -0,0 +1,984 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint no-unused-vars: ["error", {"vars": "local"}] */ +/* eslint-disable no-shadow */ + +"use strict"; +var CC = Components.Constructor; + +// Populate AppInfo before anything (like the shared loader) accesses +// System.appinfo, which is a lazy getter. +const appInfo = ChromeUtils.importESModule( + "resource://testing-common/AppInfo.sys.mjs" +); +appInfo.updateAppInfo({ + ID: "devtools@tests.mozilla.org", + name: "devtools-tests", + version: "1", + platformVersion: "42", + crashReporter: true, +}); + +const { require, loader } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/Loader.sys.mjs" +); +const { worker } = ChromeUtils.import( + "resource://devtools/shared/loader/worker-loader.js" +); + +const { NetUtil } = ChromeUtils.importESModule( + "resource://gre/modules/NetUtil.sys.mjs" +); + +// Always log packets when running tests. runxpcshelltests.py will throw +// the output away anyway, unless you give it the --verbose flag. +Services.prefs.setBoolPref("devtools.debugger.log", false); +// Enable remote debugging for the relevant tests. +Services.prefs.setBoolPref("devtools.debugger.remote-enabled", true); + +const makeDebugger = require("resource://devtools/server/actors/utils/make-debugger.js"); +const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js"); +const { + ActorRegistry, +} = require("resource://devtools/server/actors/utils/actor-registry.js"); +const { + DevToolsServer, +} = require("resource://devtools/server/devtools-server.js"); +const { DevToolsServer: WorkerDevToolsServer } = worker.require( + "resource://devtools/server/devtools-server.js" +); +const { + DevToolsClient, +} = require("resource://devtools/client/devtools-client.js"); +const { ObjectFront } = require("resource://devtools/client/fronts/object.js"); +const { + LongStringFront, +} = require("resource://devtools/client/fronts/string.js"); +const { + createCommandsDictionary, +} = require("resource://devtools/shared/commands/index.js"); +const { + CommandsFactory, +} = require("resource://devtools/shared/commands/commands-factory.js"); + +const { addDebuggerToGlobal } = ChromeUtils.importESModule( + "resource://gre/modules/jsdebugger.sys.mjs" +); + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); +const { getAppInfo } = ChromeUtils.importESModule( + "resource://testing-common/AppInfo.sys.mjs" +); + +const systemPrincipal = Cc["@mozilla.org/systemprincipal;1"].createInstance( + Ci.nsIPrincipal +); + +var { loadSubScript, loadSubScriptWithOptions } = Services.scriptloader; + +/** + * The logic here must resemble the logic of --start-debugger-server as closely + * as possible. DevToolsStartup.sys.mjs uses a distinct loader that results in + * the existence of two isolated module namespaces. In practice, this can cause + * bugs such as bug 1837185. + */ +function getDistinctDevToolsServer() { + const { + useDistinctSystemPrincipalLoader, + releaseDistinctSystemPrincipalLoader, + } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs" + ); + const requester = {}; + const distinctLoader = useDistinctSystemPrincipalLoader(requester); + registerCleanupFunction(() => { + releaseDistinctSystemPrincipalLoader(requester); + }); + + const { DevToolsServer: DistinctDevToolsServer } = distinctLoader.require( + "resource://devtools/server/devtools-server.js" + ); + return DistinctDevToolsServer; +} + +/** + * Initializes any test that needs to work with add-ons. + * + * Should be called once per test script that needs to use AddonTestUtils (and + * not once per test task!). + */ +async function startupAddonsManager() { + // Create a directory for extensions. + const profileDir = do_get_profile().clone(); + profileDir.append("extensions"); + + AddonTestUtils.init(globalThis); + AddonTestUtils.overrideCertDB(); + AddonTestUtils.appInfo = getAppInfo(); + + await AddonTestUtils.promiseStartupManager(); +} + +async function createTargetForFakeTab(title) { + const client = await startTestDevToolsServer(title); + + const tabs = await listTabs(client); + const tabDescriptor = findTab(tabs, title); + + // These xpcshell tests use mocked actors (xpcshell-test/testactors) + // which still don't support watcher actor. + // Because of that we still can't enable server side targets and target swiching. + tabDescriptor.disableTargetSwitching(); + + return tabDescriptor.getTarget(); +} + +async function createTargetForMainProcess() { + const commands = await CommandsFactory.forMainProcess(); + return commands.descriptorFront.getTarget(); +} + +/** + * Create a MemoryFront for a fake test tab. + */ +async function createTabMemoryFront() { + const target = await createTargetForFakeTab("test_memory"); + + // MemoryFront requires the HeadSnapshotActor actor to be available + // as a global actor. This isn't registered by startTestDevToolsServer which + // only register the target actors and not the browser ones. + DevToolsServer.registerActors({ browser: true }); + + const memoryFront = await target.getFront("memory"); + await memoryFront.attach(); + + registerCleanupFunction(async () => { + await memoryFront.detach(); + + // On XPCShell, the target isn't for a local tab and so target.destroy + // won't close the client. So do it so here. It will automatically destroy the target. + await target.client.close(); + }); + + return { target, memoryFront }; +} + +/** + * Same as createTabMemoryFront but attaches the MemoryFront to the MemoryActor + * scoped to the full runtime rather than to a tab. + */ +async function createMainProcessMemoryFront() { + const target = await createTargetForMainProcess(); + + const memoryFront = await target.getFront("memory"); + await memoryFront.attach(); + + registerCleanupFunction(async () => { + await memoryFront.detach(); + // For XPCShell, the main process target actor is ContentProcessTargetActor + // which doesn't expose any `detach` method. So that the target actor isn't + // destroyed when calling target.destroy. + // Close the client to cleanup everything. + await target.client.close(); + }); + + return { client: target.client, memoryFront }; +} + +function createLongStringFront(conn, form) { + // CAUTION -- do not replicate in the codebase. Instead, use marshalling + // This code is simulating how the LongStringFront would be created by protocol.js + // We should not use it like this in the codebase, this is done only for testing + // purposes until we can return a proper LongStringFront from the server. + const front = new LongStringFront(conn, form); + front.actorID = form.actor; + front.manage(front); + return front; +} + +function createTestGlobal(name, options) { + const principal = Cc["@mozilla.org/systemprincipal;1"].createInstance( + Ci.nsIPrincipal + ); + // NOTE: The Sandbox constructor behaves differently based on the argument + // length. + const sandbox = options + ? Cu.Sandbox(principal, options) + : Cu.Sandbox(principal); + sandbox.__name = name; + // Expose a few mocks to better represent a Window object. + // These attributes will be used by DOCUMENT_EVENT resource listener. + sandbox.performance = { timing: {} }; + sandbox.document = { + readyState: "complete", + defaultView: sandbox, + }; + return sandbox; +} + +function connect(client) { + dump("Connecting client.\n"); + return client.connect(); +} + +function close(client) { + dump("Closing client.\n"); + return client.close(); +} + +function listTabs(client) { + dump("Listing tabs.\n"); + return client.mainRoot.listTabs(); +} + +function findTab(tabs, title) { + dump("Finding tab with title '" + title + "'.\n"); + for (const tab of tabs) { + if (tab.title === title) { + return tab; + } + } + return null; +} + +function waitForNewSource(threadFront, url) { + dump("Waiting for new source with url '" + url + "'.\n"); + return waitForEvent(threadFront, "newSource", function (packet) { + return packet.source.url === url; + }); +} + +function attachThread(targetFront, options = {}) { + dump("Attaching to thread.\n"); + return targetFront.attachThread(options); +} + +function resume(threadFront) { + dump("Resuming thread.\n"); + return threadFront.resume(); +} + +async function addWatchpoint(threadFront, frame, variable, property, type) { + const path = `${variable}.${property}`; + info(`Add an ${path} ${type} watchpoint`); + const environment = await frame.getEnvironment(); + const obj = environment.bindings.variables[variable]; + const objFront = threadFront.pauseGrip(obj.value); + return objFront.addWatchpoint(property, path, type); +} + +function getSources(threadFront) { + dump("Getting sources.\n"); + return threadFront.getSources(); +} + +function findSource(sources, url) { + dump("Finding source with url '" + url + "'.\n"); + for (const source of sources) { + if (source.url === url) { + return source; + } + } + return null; +} + +function waitForPause(threadFront) { + dump("Waiting for pause.\n"); + return waitForEvent(threadFront, "paused"); +} + +function waitForProperty(dbg, property) { + return new Promise(resolve => { + Object.defineProperty(dbg, property, { + set(newValue) { + resolve(newValue); + }, + }); + }); +} + +function setBreakpoint(threadFront, location) { + dump("Setting breakpoint.\n"); + return threadFront.setBreakpoint(location, {}); +} + +function getPrototypeAndProperties(objClient) { + dump("getting prototype and properties.\n"); + + return objClient.getPrototypeAndProperties(); +} + +function dumpn(msg) { + dump("DBG-TEST: " + msg + "\n"); +} + +function testExceptionHook(ex) { + try { + do_report_unexpected_exception(ex); + } catch (e) { + return { throw: e }; + } + return undefined; +} + +// Convert an nsIScriptError 'logLevel' value into an appropriate string. +function scriptErrorLogLevel(message) { + switch (message.logLevel) { + case Ci.nsIConsoleMessage.info: + return "info"; + case Ci.nsIConsoleMessage.warn: + return "warning"; + default: + Assert.equal(message.logLevel, Ci.nsIConsoleMessage.error); + return "error"; + } +} + +// Register a console listener, so console messages don't just disappear +// into the ether. +var errorCount = 0; +var listener = { + observe(message) { + try { + let string; + errorCount++; + try { + // If we've been given an nsIScriptError, then we can print out + // something nicely formatted, for tools like Emacs to pick up. + message.QueryInterface(Ci.nsIScriptError); + dumpn( + message.sourceName + + ":" + + message.lineNumber + + ": " + + scriptErrorLogLevel(message) + + ": " + + message.errorMessage + ); + string = message.errorMessage; + } catch (e1) { + // Be a little paranoid with message, as the whole goal here is to lose + // no information. + try { + string = "" + message.message; + } catch (e2) { + string = "<error converting error message to string>"; + } + } + + // Make sure we exit all nested event loops so that the test can finish. + while ( + DevToolsServer && + DevToolsServer.xpcInspector && + DevToolsServer.xpcInspector.eventLoopNestLevel > 0 + ) { + DevToolsServer.xpcInspector.exitNestedEventLoop(); + } + + // In the world before bug 997440, exceptions were getting lost because of + // the arbitrary JSContext being used in nsXPCWrappedJS::CallMethod. + // In the new world, the wanderers have returned. However, because of the, + // currently very-broken, exception reporting machinery in + // nsXPCWrappedJS these get reported as errors to the console, even if + // there's actually JS on the stack above that will catch them. If we + // throw an error here because of them our tests start failing. So, we'll + // just dump the message to the logs instead, to make sure the information + // isn't lost. + dumpn("head_dbg.js observed a console message: " + string); + } catch (_) { + // Swallow everything to avoid console reentrancy errors. We did our best + // to log above, but apparently that didn't cut it. + } + }, +}; + +Services.console.registerListener(listener); + +function addTestGlobal(name, server = DevToolsServer) { + const global = createTestGlobal(name); + server.addTestGlobal(global); + return global; +} + +// List the DevToolsClient |client|'s tabs, look for one whose title is +// |title|. +async function getTestTab(client, title) { + const tabs = await client.mainRoot.listTabs(); + for (const tab of tabs) { + if (tab.title === title) { + return tab; + } + } + return null; +} +/** + * Attach to the client's tab whose title is specified + * @param {Object} client + * @param {Object} title + * @returns commands + */ +async function attachTestTab(client, title) { + const descriptorFront = await getTestTab(client, title); + + // These xpcshell tests use mocked actors (xpcshell-test/testactors) + // which still don't support watcher actor. + // Because of that we still can't enable server side targets and target swiching. + descriptorFront.disableTargetSwitching(); + + const commands = await createCommandsDictionary(descriptorFront); + await commands.targetCommand.startListening(); + return commands; +} + +/** + * Attach to the client's tab whose title is specified, and then attach to + * that tab's thread. + * @param {Object} client + * @param {Object} title + * @returns {Object} + * targetFront + * threadFront + * commands + */ +async function attachTestThread(client, title) { + const commands = await attachTestTab(client, title); + const targetFront = commands.targetCommand.targetFront; + const threadFront = await targetFront.getFront("thread"); + await targetFront.attachThread({ + autoBlackBox: true, + }); + Assert.equal(threadFront.state, "attached", "Thread front is attached"); + return { targetFront, threadFront, commands }; +} + +/** + * Initialize the testing devtools server. + */ +function initTestDevToolsServer(server = DevToolsServer) { + if (server === WorkerDevToolsServer) { + const { createRootActor } = worker.require("xpcshell-test/testactors"); + server.setRootActor(createRootActor); + } else { + const { createRootActor } = require("xpcshell-test/testactors"); + server.setRootActor(createRootActor); + } + + // Allow incoming connections. + server.init(function () { + return true; + }); +} + +/** + * Initialize the testing devtools server with a tab whose title is |title|. + */ +async function startTestDevToolsServer(title, server = DevToolsServer) { + initTestDevToolsServer(server); + addTestGlobal(title); + DevToolsServer.registerActors({ target: true }); + + const transport = DevToolsServer.connectPipe(); + const client = new DevToolsClient(transport); + + await connect(client); + return client; +} + +async function finishClient(client) { + await client.close(); + DevToolsServer.destroy(); + do_test_finished(); +} + +/** + * Takes a relative file path and returns the absolute file url for it. + */ +function getFileUrl(name, allowMissing = false) { + const file = do_get_file(name, allowMissing); + return Services.io.newFileURI(file).spec; +} + +/** + * Returns the full path of the file with the specified name in a + * platform-independent and URL-like form. + */ +function getFilePath( + name, + allowMissing = false, + usePlatformPathSeparator = false +) { + const file = do_get_file(name, allowMissing); + let path = Services.io.newFileURI(file).spec; + let filePrePath = "file://"; + if ("nsILocalFileWin" in Ci && file instanceof Ci.nsILocalFileWin) { + filePrePath += "/"; + } + + path = path.slice(filePrePath.length); + + if (usePlatformPathSeparator && path.match(/^\w:/)) { + path = path.replace(/\//g, "\\"); + } + + return path; +} + +/** + * Returns the full text contents of the given file. + */ +function readFile(fileName) { + const f = do_get_file(fileName); + const s = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + s.init(f, -1, -1, false); + try { + return NetUtil.readInputStreamToString(s, s.available()); + } finally { + s.close(); + } +} + +function writeFile(fileName, content) { + const file = do_get_file(fileName, true); + const stream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance( + Ci.nsIFileOutputStream + ); + stream.init(file, -1, -1, 0); + try { + do { + const numWritten = stream.write(content, content.length); + content = content.slice(numWritten); + } while (content.length); + } finally { + stream.close(); + } +} + +function StubTransport() {} +StubTransport.prototype.ready = function () {}; +StubTransport.prototype.send = function () {}; +StubTransport.prototype.close = function () {}; + +// Create async version of the object where calling each method +// is equivalent of calling it with asyncall. Mainly useful for +// destructuring objects with methods that take callbacks. +const Async = target => new Proxy(target, Async); +Async.get = (target, name) => + typeof target[name] === "function" + ? asyncall.bind(null, target[name], target) + : target[name]; + +// Calls async function that takes callback and errorback and returns +// returns promise representing result. +const asyncall = (fn, self, ...args) => + new Promise((...etc) => fn.call(self, ...args, ...etc)); + +const Test = task => () => { + add_task(task); + run_next_test(); +}; + +const assert = Assert.ok.bind(Assert); + +/** + * Create a promise that is resolved on the next occurence of the given event. + * + * @param ThreadFront threadFront + * @param String event + * @param Function predicate + * @returns Promise + */ +function waitForEvent(front, type, predicate) { + if (!predicate) { + return front.once(type); + } + + return new Promise(function (resolve) { + function listener(packet) { + if (!predicate(packet)) { + return; + } + front.off(type, listener); + resolve(packet); + } + front.on(type, listener); + }); +} + +/** + * Execute the action on the next tick and return a promise that is resolved on + * the next pause. + * + * When using promises and Task.jsm, we often want to do an action that causes a + * pause and continue the task once the pause has ocurred. Unfortunately, if we + * do the action that causes the pause within the task's current tick we will + * pause before we have a chance to yield the promise that waits for the pause + * and we enter a dead lock. The solution is to create the promise that waits + * for the pause, schedule the action to run on the next tick of the event loop, + * and finally yield the promise. + * + * @param Function action + * @param ThreadFront threadFront + * @returns Promise + */ +function executeOnNextTickAndWaitForPause(action, threadFront) { + const paused = waitForPause(threadFront); + executeSoon(action); + return paused; +} + +function evalCallback(debuggeeGlobal, func) { + Cu.evalInSandbox("(" + func + ")()", debuggeeGlobal, "1.8", "test.js", 1); +} + +/** + * Interrupt JS execution for the specified thread. + * + * @param ThreadFront threadFront + * @returns Promise + */ +function interrupt(threadFront) { + dumpn("Interrupting."); + return threadFront.interrupt(); +} + +/** + * Resume JS execution for the specified thread and then wait for the next pause + * event. + * + * @param DevToolsClient client + * @param ThreadFront threadFront + * @returns Promise + */ +async function resumeAndWaitForPause(threadFront) { + const paused = waitForPause(threadFront); + await resume(threadFront); + return paused; +} + +/** + * Resume JS execution for a single step and wait for the pause after the step + * has been taken. + * + * @param ThreadFront threadFront + * @returns Promise + */ +function stepIn(threadFront) { + dumpn("Stepping in."); + const paused = waitForPause(threadFront); + return threadFront.stepIn().then(() => paused); +} + +/** + * Resume JS execution for a step over and wait for the pause after the step + * has been taken. + * + * @param ThreadFront threadFront + * @returns Promise + */ +async function stepOver(threadFront, frameActor) { + dumpn("Stepping over."); + await threadFront.stepOver(frameActor); + return waitForPause(threadFront); +} + +/** + * Resume JS execution for a step out and wait for the pause after the step + * has been taken. + * + * @param DevToolsClient client + * @param ThreadFront threadFront + * @returns Promise + */ +async function stepOut(threadFront, frameActor) { + dumpn("Stepping out."); + await threadFront.stepOut(frameActor); + return waitForPause(threadFront); +} + +/** + * Restart specific frame and wait for the pause after the restart + * has been taken. + * + * @param DevToolsClient client + * @param ThreadFront threadFront + * @returns Promise + */ +async function restartFrame(threadFront, frameActor) { + dumpn("Restarting frame."); + await threadFront.restart(frameActor); + return waitForPause(threadFront); +} + +/** + * Get the list of `count` frames currently on stack, starting at the index + * `first` for the specified thread. + * + * @param ThreadFront threadFront + * @param Number first + * @param Number count + * @returns Promise + */ +function getFrames(threadFront, first, count) { + dumpn("Getting frames."); + return threadFront.getFrames(first, count); +} + +/** + * Black box the specified source. + * + * @param SourceFront sourceFront + * @returns Promise + */ +async function blackBox(sourceFront, range = null) { + dumpn("Black boxing source: " + sourceFront.actor); + const pausedInSource = await sourceFront.blackBox(range); + ok(true, "blackBox didn't throw"); + return pausedInSource; +} + +/** + * Stop black boxing the specified source. + * + * @param SourceFront sourceFront + * @returns Promise + */ +async function unBlackBox(sourceFront, range = null) { + dumpn("Un-black boxing source: " + sourceFront.actor); + await sourceFront.unblackBox(range); + ok(true, "unblackBox didn't throw"); +} + +/** + * Get a source at the specified url. + * + * @param ThreadFront threadFront + * @param string url + * @returns Promise<SourceFront> + */ +async function getSource(threadFront, url) { + const source = await getSourceForm(threadFront, url); + if (source) { + return threadFront.source(source); + } + + throw new Error("source not found"); +} + +async function getSourceById(threadFront, id) { + const form = await getSourceFormById(threadFront, id); + return threadFront.source(form); +} + +async function getSourceForm(threadFront, url) { + const { sources } = await threadFront.getSources(); + return sources.find(s => s.url === url); +} + +async function getSourceFormById(threadFront, id) { + const { sources } = await threadFront.getSources(); + return sources.find(source => source.actor == id); +} + +async function checkFramesLength(threadFront, expectedFrames) { + const frameResponse = await threadFront.getFrames(0, null); + Assert.equal( + frameResponse.frames.length, + expectedFrames, + "Thread front has the expected number of frames" + ); +} + +/** + * Do a reload which clears the thread debugger + * + * @param TabFront tabFront + * @returns Promise<response> + */ +function reload(tabFront) { + return tabFront.reload({}); +} + +/** + * Returns an array of stack location strings given a thread and a sample. + * + * @param object thread + * @param object sample + * @returns object + */ +function getInflatedStackLocations(thread, sample) { + const stackTable = thread.stackTable; + const frameTable = thread.frameTable; + const stringTable = thread.stringTable; + const SAMPLE_STACK_SLOT = thread.samples.schema.stack; + const STACK_PREFIX_SLOT = stackTable.schema.prefix; + const STACK_FRAME_SLOT = stackTable.schema.frame; + const FRAME_LOCATION_SLOT = frameTable.schema.location; + + // Build the stack from the raw data and accumulate the locations in + // an array. + let stackIndex = sample[SAMPLE_STACK_SLOT]; + const locations = []; + while (stackIndex !== null) { + const stackEntry = stackTable.data[stackIndex]; + const frame = frameTable.data[stackEntry[STACK_FRAME_SLOT]]; + locations.push(stringTable[frame[FRAME_LOCATION_SLOT]]); + stackIndex = stackEntry[STACK_PREFIX_SLOT]; + } + + // The profiler tree is inverted, so reverse the array. + return locations.reverse(); +} + +async function setupTestFromUrl(url) { + do_test_pending(); + + const { createRootActor } = require("xpcshell-test/testactors"); + DevToolsServer.setRootActor(createRootActor); + DevToolsServer.init(() => true); + + const global = createTestGlobal("test"); + DevToolsServer.addTestGlobal(global); + + const devToolsClient = new DevToolsClient(DevToolsServer.connectPipe()); + await connect(devToolsClient); + + const tabs = await listTabs(devToolsClient); + const descriptorFront = findTab(tabs, "test"); + + // These xpcshell tests use mocked actors (xpcshell-test/testactors) + // which still don't support watcher actor. + // Because of that we still can't enable server side targets and target swiching. + descriptorFront.disableTargetSwitching(); + + const targetFront = await descriptorFront.getTarget(); + + const threadFront = await attachThread(targetFront); + + const sourceUrl = getFileUrl(url); + const promise = waitForNewSource(threadFront, sourceUrl); + loadSubScript(sourceUrl, global); + const { source } = await promise; + + const sourceFront = threadFront.source(source); + return { global, devToolsClient, threadFront, sourceFront }; +} + +/** + * Run the given test function twice, one with a regular DevToolsServer, + * testing against a fake tab. And another one against a WorkerDevToolsServer, + * testing the worker codepath. + * + * @param Function test + * Test function to run twice. + * This test function is called with a dictionary: + * - Sandbox debuggee + * The custom JS debuggee created for this test. This is a Sandbox using system + * principals by default. + * - ThreadFront threadFront + * A reference to a ThreadFront instance that is attached to the debuggee. + * - DevToolsClient client + * A reference to the DevToolsClient used to communicated with the RDP server. + * @param Object options + * Optional arguments to tweak test environment + * - JSPrincipal principal + * Principal to use for the debuggee. Defaults to systemPrincipal. + * - boolean doNotRunWorker + * If true, do not run this tests in worker debugger context. Defaults to false. + * - bool wantXrays + * Whether the debuggee wants Xray vision with respect to same-origin objects + * outside the sandbox. Defaults to true. + * - bool waitForFinish + * Whether to wait for a call to threadFrontTestFinished after the test + * function finishes. + */ +function threadFrontTest(test, options = {}) { + const { + principal = systemPrincipal, + doNotRunWorker = false, + wantXrays = true, + waitForFinish = false, + } = options; + + async function runThreadFrontTestWithServer(server, test) { + // Setup a server and connect a client to it. + initTestDevToolsServer(server); + + // Create a custom debuggee and register it to the server. + // We are using a custom Sandbox as debuggee. Create a new zone because + // debugger and debuggee must be in different compartments. + const debuggee = Cu.Sandbox(principal, { freshZone: true, wantXrays }); + const scriptName = "debuggee.js"; + debuggee.__name = scriptName; + server.addTestGlobal(debuggee); + + const client = new DevToolsClient(server.connectPipe()); + await client.connect(); + + // Attach to the fake tab target and retrieve the ThreadFront instance. + // Automatically resume as the thread is paused by default after attach. + const { targetFront, threadFront, commands } = await attachTestThread( + client, + scriptName + ); + + // Cross the client/server boundary to retrieve the target actor & thread + // actor instances, used by some tests. + const rootActor = client.transport._serverConnection.rootActor; + const targetActor = + rootActor._parameters.tabList.getTargetActorForTab("debuggee.js"); + const { threadActor } = targetActor; + + // Run the test function + const args = { + threadActor, + threadFront, + debuggee, + client, + server, + targetFront, + commands, + isWorkerServer: server === WorkerDevToolsServer, + }; + if (waitForFinish) { + // Use dispatchToMainThread so that the test function does not have to + // finish executing before the test itself finishes. + const promise = new Promise( + resolve => (threadFrontTestFinished = resolve) + ); + Services.tm.dispatchToMainThread(() => test(args)); + await promise; + } else { + await test(args); + } + + // Cleanup the client after the test ran + await client.close(); + + server.removeTestGlobal(debuggee); + + // Also cleanup the created server + server.destroy(); + } + + return async () => { + dump(">>> Run thread front test against a regular DevToolsServer\n"); + await runThreadFrontTestWithServer(DevToolsServer, test); + + // Skip tests that fail in the worker context + if (!doNotRunWorker) { + dump(">>> Run thread front test against a worker DevToolsServer\n"); + await runThreadFrontTestWithServer(WorkerDevToolsServer, test); + } + }; +} + +// This callback is used in tandem with the waitForFinish option of +// threadFrontTest to support thread front tests that use promises to +// asynchronously finish the tests, instead of using async/await. +// Newly written tests should avoid using this. See bug 1596114 for migrating +// existing tests to async/await and removing this functionality. +let threadFrontTestFinished; diff --git a/devtools/server/tests/xpcshell/hello-actor.js b/devtools/server/tests/xpcshell/hello-actor.js new file mode 100644 index 0000000000..f4fc63cb86 --- /dev/null +++ b/devtools/server/tests/xpcshell/hello-actor.js @@ -0,0 +1,23 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint no-unused-vars: ["error", {"vars": "local"}] */ + +"use strict"; + +const protocol = require("resource://devtools/shared/protocol.js"); + +const helloSpec = protocol.generateActorSpec({ + typeName: "helloActor", + + methods: { + hello: {}, + }, +}); + +class HelloActor extends protocol.Actor { + constructor(conn) { + super(conn, helloSpec); + } + + hello() {} +} diff --git a/devtools/server/tests/xpcshell/post_init_global_actors.js b/devtools/server/tests/xpcshell/post_init_global_actors.js new file mode 100644 index 0000000000..4ec5fb8078 --- /dev/null +++ b/devtools/server/tests/xpcshell/post_init_global_actors.js @@ -0,0 +1,22 @@ +/* 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"); + +class PostInitGlobalActor extends Actor { + constructor(conn) { + super(conn, { typeName: "postInitGlobal", methods: [] }); + + this.requestTypes = { + ping: this.onPing, + }; + } + + onPing() { + return { message: "pong" }; + } +} + +exports.PostInitGlobalActor = PostInitGlobalActor; diff --git a/devtools/server/tests/xpcshell/post_init_target_scoped_actors.js b/devtools/server/tests/xpcshell/post_init_target_scoped_actors.js new file mode 100644 index 0000000000..9b0b4c053e --- /dev/null +++ b/devtools/server/tests/xpcshell/post_init_target_scoped_actors.js @@ -0,0 +1,22 @@ +/* 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"); + +class PostInitTargetScopedActor extends Actor { + constructor(conn) { + super(conn, { typeName: "postInitTargetScoped", methods: [] }); + + this.requestTypes = { + ping: this.onPing, + }; + } + + onPing() { + return { message: "pong" }; + } +} + +exports.PostInitTargetScopedActor = PostInitTargetScopedActor; diff --git a/devtools/server/tests/xpcshell/pre_init_global_actors.js b/devtools/server/tests/xpcshell/pre_init_global_actors.js new file mode 100644 index 0000000000..f5e14aaaa9 --- /dev/null +++ b/devtools/server/tests/xpcshell/pre_init_global_actors.js @@ -0,0 +1,22 @@ +/* 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"); + +class PreInitGlobalActor extends Actor { + constructor(conn) { + super(conn, { typeName: "preInitGlobal", methods: [] }); + + this.requestTypes = { + ping: this.onPing, + }; + } + + onPing() { + return { message: "pong" }; + } +} + +exports.PreInitGlobalActor = PreInitGlobalActor; diff --git a/devtools/server/tests/xpcshell/pre_init_target_scoped_actors.js b/devtools/server/tests/xpcshell/pre_init_target_scoped_actors.js new file mode 100644 index 0000000000..360d4b52a0 --- /dev/null +++ b/devtools/server/tests/xpcshell/pre_init_target_scoped_actors.js @@ -0,0 +1,22 @@ +/* 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"); + +class PreInitTargetScopedActor extends Actor { + constructor(conn) { + super(conn, { typeName: "preInitTargetScoped", methods: [] }); + + this.requestTypes = { + ping: this.onPing, + }; + } + + onPing() { + return { message: "pong" }; + } +} + +exports.PreInitTargetScopedActor = PreInitTargetScopedActor; diff --git a/devtools/server/tests/xpcshell/registertestactors-lazy.js b/devtools/server/tests/xpcshell/registertestactors-lazy.js new file mode 100644 index 0000000000..ef04e7a8d2 --- /dev/null +++ b/devtools/server/tests/xpcshell/registertestactors-lazy.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var { + RetVal, + Actor, + FrontClassWithSpec, + generateActorSpec, +} = require("resource://devtools/shared/protocol.js"); + +const lazySpec = generateActorSpec({ + typeName: "lazy", + + methods: { + hello: { + response: { str: RetVal("string") }, + }, + }, +}); + +class LazyActor extends Actor { + constructor(conn, id) { + super(conn, lazySpec); + + Services.obs.notifyObservers(null, "actor", "instantiated"); + } + + hello(str) { + return "world"; + } +} +exports.LazyActor = LazyActor; + +Services.obs.notifyObservers(null, "actor", "loaded"); + +class LazyFront extends FrontClassWithSpec(lazySpec) { + constructor(client) { + super(client); + } +} +exports.LazyFront = LazyFront; diff --git a/devtools/server/tests/xpcshell/setBreakpoint-on-column-in-gcd-script.js b/devtools/server/tests/xpcshell/setBreakpoint-on-column-in-gcd-script.js new file mode 100644 index 0000000000..575915c4fd --- /dev/null +++ b/devtools/server/tests/xpcshell/setBreakpoint-on-column-in-gcd-script.js @@ -0,0 +1,7 @@ +"use strict"; + +function f() {} + +(function () { + var a = 1; var b = 2; var c = 3; +})(); diff --git a/devtools/server/tests/xpcshell/setBreakpoint-on-column-minified.js b/devtools/server/tests/xpcshell/setBreakpoint-on-column-minified.js new file mode 100644 index 0000000000..1fbf8ef16e --- /dev/null +++ b/devtools/server/tests/xpcshell/setBreakpoint-on-column-minified.js @@ -0,0 +1,8 @@ +"use strict"; + +function other(){ var a = 1; } function test(){ var a = 1; var b = 2; var c = 3; } + +function f() { + other(); + test(); +}
\ No newline at end of file diff --git a/devtools/server/tests/xpcshell/setBreakpoint-on-column-with-no-offsets-in-gcd-script.js b/devtools/server/tests/xpcshell/setBreakpoint-on-column-with-no-offsets-in-gcd-script.js new file mode 100644 index 0000000000..adce39193d --- /dev/null +++ b/devtools/server/tests/xpcshell/setBreakpoint-on-column-with-no-offsets-in-gcd-script.js @@ -0,0 +1,7 @@ +"use strict"; + +function f() {} + +(function () { + var a = 1; var c = 3; +})(); diff --git a/devtools/server/tests/xpcshell/setBreakpoint-on-column-with-no-offsets.js b/devtools/server/tests/xpcshell/setBreakpoint-on-column-with-no-offsets.js new file mode 100644 index 0000000000..5faefc3c88 --- /dev/null +++ b/devtools/server/tests/xpcshell/setBreakpoint-on-column-with-no-offsets.js @@ -0,0 +1,5 @@ +"use strict"; + +function f() { + var a = 1; var c = 3; +} diff --git a/devtools/server/tests/xpcshell/setBreakpoint-on-column.js b/devtools/server/tests/xpcshell/setBreakpoint-on-column.js new file mode 100644 index 0000000000..d92231e651 --- /dev/null +++ b/devtools/server/tests/xpcshell/setBreakpoint-on-column.js @@ -0,0 +1,5 @@ +"use strict"; + +function f() { + var a = 1; var b = 2; var c = 3; +} diff --git a/devtools/server/tests/xpcshell/setBreakpoint-on-line-in-gcd-script.js b/devtools/server/tests/xpcshell/setBreakpoint-on-line-in-gcd-script.js new file mode 100644 index 0000000000..fb96be8aba --- /dev/null +++ b/devtools/server/tests/xpcshell/setBreakpoint-on-line-in-gcd-script.js @@ -0,0 +1,9 @@ +"use strict"; + +function f() {} + +(function () { + var a = 1; + var b = 2; + var c = 3; +})(); diff --git a/devtools/server/tests/xpcshell/setBreakpoint-on-line-with-multiple-offsets.js b/devtools/server/tests/xpcshell/setBreakpoint-on-line-with-multiple-offsets.js new file mode 100644 index 0000000000..b30ebb5049 --- /dev/null +++ b/devtools/server/tests/xpcshell/setBreakpoint-on-line-with-multiple-offsets.js @@ -0,0 +1,7 @@ +"use strict"; + +function f() { + for (var i = 0; i < 1; ++i) { + ; + } +} diff --git a/devtools/server/tests/xpcshell/setBreakpoint-on-line-with-multiple-statements.js b/devtools/server/tests/xpcshell/setBreakpoint-on-line-with-multiple-statements.js new file mode 100644 index 0000000000..d92231e651 --- /dev/null +++ b/devtools/server/tests/xpcshell/setBreakpoint-on-line-with-multiple-statements.js @@ -0,0 +1,5 @@ +"use strict"; + +function f() { + var a = 1; var b = 2; var c = 3; +} diff --git a/devtools/server/tests/xpcshell/setBreakpoint-on-line-with-no-offsets-in-gcd-script.js b/devtools/server/tests/xpcshell/setBreakpoint-on-line-with-no-offsets-in-gcd-script.js new file mode 100644 index 0000000000..b03d400794 --- /dev/null +++ b/devtools/server/tests/xpcshell/setBreakpoint-on-line-with-no-offsets-in-gcd-script.js @@ -0,0 +1,9 @@ +"use strict"; + +function f() {} + +(function () { + var a = 1; + + var c = 3; +})(); diff --git a/devtools/server/tests/xpcshell/setBreakpoint-on-line-with-no-offsets.js b/devtools/server/tests/xpcshell/setBreakpoint-on-line-with-no-offsets.js new file mode 100644 index 0000000000..1268cf8db0 --- /dev/null +++ b/devtools/server/tests/xpcshell/setBreakpoint-on-line-with-no-offsets.js @@ -0,0 +1,7 @@ +"use strict"; + +function f() { + var a = 1; + + var c = 3; +} diff --git a/devtools/server/tests/xpcshell/setBreakpoint-on-line.js b/devtools/server/tests/xpcshell/setBreakpoint-on-line.js new file mode 100644 index 0000000000..1b15e2a5e7 --- /dev/null +++ b/devtools/server/tests/xpcshell/setBreakpoint-on-line.js @@ -0,0 +1,7 @@ +"use strict"; + +function f() { + var a = 1; + var b = 2; + var c = 3; +} diff --git a/devtools/server/tests/xpcshell/source-03.js b/devtools/server/tests/xpcshell/source-03.js new file mode 100644 index 0000000000..af623a2eb2 --- /dev/null +++ b/devtools/server/tests/xpcshell/source-03.js @@ -0,0 +1,7 @@ +/* eslint-disable */ + +function init() { + var a = foo(); +} + +function foo() {} diff --git a/devtools/server/tests/xpcshell/source-map-data/sourcemapped.coffee b/devtools/server/tests/xpcshell/source-map-data/sourcemapped.coffee new file mode 100644 index 0000000000..73a400a219 --- /dev/null +++ b/devtools/server/tests/xpcshell/source-map-data/sourcemapped.coffee @@ -0,0 +1,6 @@ +foo = (n) -> + return "foo" + i for i in [0...n] + +[first, second, third] = foo(3) + +debugger
\ No newline at end of file diff --git a/devtools/server/tests/xpcshell/source-map-data/sourcemapped.map b/devtools/server/tests/xpcshell/source-map-data/sourcemapped.map new file mode 100644 index 0000000000..dcee3c33c3 --- /dev/null +++ b/devtools/server/tests/xpcshell/source-map-data/sourcemapped.map @@ -0,0 +1,10 @@ +{ + "version": 3, + "file": "sourcemapped.js", + "sourceRoot": "", + "sources": [ + "sourcemapped.coffee" + ], + "names": [], + "mappings": ";AAAA;CAAA,KAAA,yBAAA;CAAA;CAAA,CAAA,CAAA,MAAO;CACL,IAAA,GAAA;AAAA,CAAA,EAAA,MAA0B,qDAA1B;CAAA,EAAe,EAAR,QAAA;CAAP,IADI;CAAN,EAAM;;CAAN,CAGA,CAAyB,IAAA;;CAEzB,UALA;CAAA" +}
\ No newline at end of file diff --git a/devtools/server/tests/xpcshell/sourcemapped.js b/devtools/server/tests/xpcshell/sourcemapped.js new file mode 100644 index 0000000000..94d130903b --- /dev/null +++ b/devtools/server/tests/xpcshell/sourcemapped.js @@ -0,0 +1,16 @@ +// Generated by CoffeeScript 1.6.1 +(function () { + var first, foo, second, third, _ref; + + foo = function (n) { + var i, _i; + for (i = _i = 0; 0 <= n ? _i < n : _i > n; i = 0 <= n ? ++_i : --_i) { + return "foo" + i; + } + }; + + _ref = foo(3), first = _ref[0], second = _ref[1], third = _ref[2]; + + debugger; + +}).call(this); diff --git a/devtools/server/tests/xpcshell/stepping-async.js b/devtools/server/tests/xpcshell/stepping-async.js new file mode 100644 index 0000000000..0ee37883bc --- /dev/null +++ b/devtools/server/tests/xpcshell/stepping-async.js @@ -0,0 +1,31 @@ +"use strict"; +/* exported stuff */ + +async function timer() { + return Promise.resolve(); +} + +function oops() { + return `oops`; +} + +async function inner() { + oops(); + await timer(); + Promise.resolve().then(async () => { + Promise.resolve().then(() => { + oops(); + }); + oops(); + }); + oops(); +} + +async function stuff() { + debugger; + const task = inner(); + oops(); + await task; + oops(); + debugger; +} diff --git a/devtools/server/tests/xpcshell/stepping.js b/devtools/server/tests/xpcshell/stepping.js new file mode 100644 index 0000000000..2134bea38d --- /dev/null +++ b/devtools/server/tests/xpcshell/stepping.js @@ -0,0 +1,36 @@ +"use strict"; +/* exported global arithmetic composition chaining nested */ + +const obj = { b }; + +function a() { + return obj; +} + +function b() { + return 2; +} + +function arithmetic() { + debugger; + a() + b(); +} + +function composition() { + debugger; + b(a()); +} + +function chaining() { + debugger; + a().b(); +} + +function c() { + return b(); +} + +function nested() { + debugger; + c(); +} diff --git a/devtools/server/tests/xpcshell/test_MemoryActor_saveHeapSnapshot_01.js b/devtools/server/tests/xpcshell/test_MemoryActor_saveHeapSnapshot_01.js new file mode 100644 index 0000000000..7df3cbd2ba --- /dev/null +++ b/devtools/server/tests/xpcshell/test_MemoryActor_saveHeapSnapshot_01.js @@ -0,0 +1,22 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that we can tell the memory actor to take a heap snapshot over the RDP +// and then create a HeapSnapshot instance from the resulting file. + +add_task(async () => { + const { memoryFront } = await createTabMemoryFront(); + + const snapshotFilePath = await memoryFront.saveHeapSnapshot(); + ok( + !!(await IOUtils.stat(snapshotFilePath)), + "Should have the heap snapshot file" + ); + const snapshot = ChromeUtils.readHeapSnapshot(snapshotFilePath); + ok( + HeapSnapshot.isInstance(snapshot), + "And we should be able to read a HeapSnapshot instance from the file" + ); +}); diff --git a/devtools/server/tests/xpcshell/test_MemoryActor_saveHeapSnapshot_02.js b/devtools/server/tests/xpcshell/test_MemoryActor_saveHeapSnapshot_02.js new file mode 100644 index 0000000000..91593d845f --- /dev/null +++ b/devtools/server/tests/xpcshell/test_MemoryActor_saveHeapSnapshot_02.js @@ -0,0 +1,24 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that we can properly stream heap snapshot files over the RDP as bulk +// data. + +add_task(async () => { + const { memoryFront } = await createTabMemoryFront(); + + const snapshotFilePath = await memoryFront.saveHeapSnapshot({ + forceCopy: true, + }); + ok( + !!(await IOUtils.stat(snapshotFilePath)), + "Should have the heap snapshot file" + ); + const snapshot = ChromeUtils.readHeapSnapshot(snapshotFilePath); + ok( + HeapSnapshot.isInstance(snapshot), + "And we should be able to read a HeapSnapshot instance from the file" + ); +}); diff --git a/devtools/server/tests/xpcshell/test_MemoryActor_saveHeapSnapshot_03.js b/devtools/server/tests/xpcshell/test_MemoryActor_saveHeapSnapshot_03.js new file mode 100644 index 0000000000..b212abbced --- /dev/null +++ b/devtools/server/tests/xpcshell/test_MemoryActor_saveHeapSnapshot_03.js @@ -0,0 +1,22 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that we can save full runtime heap snapshots when attached to the +// ParentProcessTargetActor or a ContentProcessTargetActor. + +add_task(async () => { + const { memoryFront } = await createMainProcessMemoryFront(); + + const snapshotFilePath = await memoryFront.saveHeapSnapshot(); + ok( + !!(await IOUtils.stat(snapshotFilePath)), + "Should have the heap snapshot file" + ); + const snapshot = ChromeUtils.readHeapSnapshot(snapshotFilePath); + ok( + HeapSnapshot.isInstance(snapshot), + "And we should be able to read a HeapSnapshot instance from the file" + ); +}); diff --git a/devtools/server/tests/xpcshell/test_add_actors.js b/devtools/server/tests/xpcshell/test_add_actors.js new file mode 100644 index 0000000000..8077109d71 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_add_actors.js @@ -0,0 +1,107 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Get the object, from the server side, for a given actor ID +function getActorInstance(connID, actorID) { + return DevToolsServer._connections[connID].getActor(actorID); +} + +/** + * The purpose of these tests is to verify that it's possible to add actors + * both before and after the DevToolsServer has been initialized, so addons + * that add actors don't have to poll the object for its initialization state + * in order to add actors after initialization but rather can add actors anytime + * regardless of the object's state. + */ +add_task(async function () { + ActorRegistry.registerModule("resource://test/pre_init_global_actors.js", { + prefix: "preInitGlobal", + constructor: "PreInitGlobalActor", + type: { global: true }, + }); + ActorRegistry.registerModule( + "resource://test/pre_init_target_scoped_actors.js", + { + prefix: "preInitTargetScoped", + constructor: "PreInitTargetScopedActor", + type: { target: true }, + } + ); + + const client = await startTestDevToolsServer("example tab"); + + ActorRegistry.registerModule("resource://test/post_init_global_actors.js", { + prefix: "postInitGlobal", + constructor: "PostInitGlobalActor", + type: { global: true }, + }); + ActorRegistry.registerModule( + "resource://test/post_init_target_scoped_actors.js", + { + prefix: "postInitTargetScoped", + constructor: "PostInitTargetScopedActor", + type: { target: true }, + } + ); + + let actors = await client.mainRoot.rootForm; + const tabs = await client.mainRoot.listTabs(); + const tabDescriptor = tabs[0]; + + // These xpcshell tests use mocked actors (xpcshell-test/testactors) + // which still don't support watcher actor. + // Because of that we still can't enable server side targets and target swiching. + tabDescriptor.disableTargetSwitching(); + + const tabTarget = await tabDescriptor.getTarget(); + + Assert.equal(tabs.length, 1); + + let reply = await client.request({ + to: actors.preInitGlobalActor, + type: "ping", + }); + Assert.equal(reply.message, "pong"); + + reply = await client.request({ + to: tabTarget.targetForm.preInitTargetScopedActor, + type: "ping", + }); + Assert.equal(reply.message, "pong"); + + reply = await client.request({ + to: actors.postInitGlobalActor, + type: "ping", + }); + Assert.equal(reply.message, "pong"); + + reply = await client.request({ + to: tabTarget.targetForm.postInitTargetScopedActor, + type: "ping", + }); + Assert.equal(reply.message, "pong"); + + // Consider that there is only one connection, and the first one is ours + const connID = Object.keys(DevToolsServer._connections)[0]; + const postInitGlobalActor = getActorInstance( + connID, + actors.postInitGlobalActor + ); + const preInitGlobalActor = getActorInstance( + connID, + actors.preInitGlobalActor + ); + actors = await client.mainRoot.getRoot(); + Assert.equal( + postInitGlobalActor, + getActorInstance(connID, actors.postInitGlobalActor) + ); + Assert.equal( + preInitGlobalActor, + getActorInstance(connID, actors.preInitGlobalActor) + ); + + await client.close(); +}); diff --git a/devtools/server/tests/xpcshell/test_addon_debugging_connect.js b/devtools/server/tests/xpcshell/test_addon_debugging_connect.js new file mode 100644 index 0000000000..221e73d256 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_addon_debugging_connect.js @@ -0,0 +1,158 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { ExtensionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/ExtensionXPCShellUtils.sys.mjs" +); + +const lazy = {}; +ChromeUtils.defineESModuleGetters(lazy, { + ExtensionParent: "resource://gre/modules/ExtensionParent.sys.mjs", +}); + +const { createAppInfo, promiseStartupManager } = AddonTestUtils; + +AddonTestUtils.init(this); +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42"); + +ExtensionTestUtils.init(this); + +function watchFrameUpdates(front) { + const collected = []; + + const listener = data => { + collected.push(data); + }; + + front.on("frameUpdate", listener); + let unsubscribe = () => { + unsubscribe = null; + front.off("frameUpdate", listener); + return collected; + }; + + return unsubscribe; +} + +function promiseFrameUpdate(front, matcher = () => true) { + return new Promise(resolve => { + const listener = data => { + if (matcher(data)) { + resolve(); + front.off("frameUpdate", listener); + } + }; + + front.on("frameUpdate", listener); + }); +} + +// Bug 1302702 - Test connect to a webextension addon +add_task( + { + // This test needs to run only when the extension are running in a separate + // child process, otherwise attachThread would pause the main process and this + // test would get stuck. + skip_if: () => !WebExtensionPolicy.useRemoteWebExtensions, + }, + async function test_webextension_addon_debugging_connect() { + await promiseStartupManager(); + + // Install and start a test webextension. + const extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "temporary", + background() { + const { browser } = this; + browser.test.log("background script executed"); + // window is available in background scripts + // eslint-disable-next-line no-undef + browser.test.sendMessage("background page ready", window.location.href); + }, + }); + await extension.startup(); + const bgPageURL = await extension.awaitMessage("background page ready"); + + const commands = await CommandsFactory.forAddon(extension.id); + + // Connect to the target addon actor and wait for the updated list of frames. + const addonTarget = await commands.descriptorFront.getTarget(); + ok(addonTarget, "Got an RDP target"); + + const { frames } = await addonTarget.listFrames(); + const backgroundPageFrame = frames + .filter(frame => { + return ( + frame.url && frame.url.endsWith("/_generated_background_page.html") + ); + }) + .pop(); + ok(backgroundPageFrame, "Found the frame for the background page"); + + const threadFront = await addonTarget.attachThread(); + + ok(threadFront, "Got a threadFront for the target addon"); + equal(threadFront.paused, false, "The addon threadActor isn't paused"); + + equal( + lazy.ExtensionParent.DebugUtils.debugBrowserPromises.size, + 1, + "The expected number of debug browser has been created by the addon actor" + ); + + const unwatchFrameUpdates = watchFrameUpdates(addonTarget); + + const promiseBgPageFrameUpdate = promiseFrameUpdate(addonTarget, data => { + return data.frames?.some(frame => frame.url === bgPageURL); + }); + + // Reload the addon through the RDP protocol. + await addonTarget.reload(); + info("Wait background page to be fully reloaded"); + await extension.awaitMessage("background page ready"); + info("Wait background page frameUpdate event"); + await promiseBgPageFrameUpdate; + + equal( + lazy.ExtensionParent.DebugUtils.debugBrowserPromises.size, + 1, + "The number of debug browser has not been changed after an addon reload" + ); + + const frameUpdates = unwatchFrameUpdates(); + const [frameUpdate] = frameUpdates; + + equal( + frameUpdates.length, + 1, + "Expect 1 frameUpdate events to have been received" + ); + equal( + frameUpdate.frames?.length, + 1, + "Expect 1 frame in the frameUpdate event " + ); + Assert.deepEqual( + { + url: frameUpdate.frames[0].url, + }, + { + url: bgPageURL, + }, + "Got the expected frame update when the addon background page was loaded back" + ); + + await commands.destroy(); + + // Check that if we close the debugging client without uninstalling the addon, + // the webextension debugging actor should release the debug browser. + equal( + lazy.ExtensionParent.DebugUtils.debugBrowserPromises.size, + 0, + "The debug browser has been released when the RDP connection has been closed" + ); + + await extension.unload(); + } +); diff --git a/devtools/server/tests/xpcshell/test_addon_events.js b/devtools/server/tests/xpcshell/test_addon_events.js new file mode 100644 index 0000000000..262a604953 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_addon_events.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { AddonManager } = ChromeUtils.importESModule( + "resource://gre/modules/AddonManager.sys.mjs" +); +add_task(async function testReloadExitedAddon() { + await startupAddonsManager(); + + DevToolsServer.init(); + DevToolsServer.registerAllActors(); + + const client = new DevToolsClient(DevToolsServer.connectPipe()); + await client.connect(); + + // Retrieve the current list of addons to be notified of the next list update. + // We will also call listAddons every time we receive the event "addonListChanged" for + // the same reason. + await client.mainRoot.listAddons(); + + info("Install the addon"); + const addonFile = do_get_file("addons/web-extension", false); + + let installedAddon; + await expectAddonListChanged(client, async () => { + installedAddon = await AddonManager.installTemporaryAddon(addonFile); + }); + ok(true, "Received onAddonListChanged when installing addon"); + + info("Disable the addon"); + await expectAddonListChanged(client, () => installedAddon.disable()); + ok(true, "Received onAddonListChanged when disabling addon"); + + info("Enable the addon"); + await expectAddonListChanged(client, () => installedAddon.enable()); + ok(true, "Received onAddonListChanged when enabling addon"); + + info("Put the addon in pending uninstall mode"); + await expectAddonListChanged(client, () => installedAddon.uninstall(true)); + ok(true, "Received onAddonListChanged when addon moves to pending uninstall"); + + info("Cancel uninstall for addon"); + await expectAddonListChanged(client, () => installedAddon.cancelUninstall()); + ok(true, "Received onAddonListChanged when addon uninstall is canceled"); + + info("Completely uninstall the addon"); + await expectAddonListChanged(client, () => installedAddon.uninstall()); + ok(true, "Received onAddonListChanged when addon is uninstalled"); + + await close(client); +}); + +async function expectAddonListChanged(client, predicate) { + const onAddonListChanged = client.mainRoot.once("addonListChanged"); + await predicate(); + await onAddonListChanged; + await client.mainRoot.listAddons(); +} diff --git a/devtools/server/tests/xpcshell/test_addon_reload.js b/devtools/server/tests/xpcshell/test_addon_reload.js new file mode 100644 index 0000000000..e0054f03cc --- /dev/null +++ b/devtools/server/tests/xpcshell/test_addon_reload.js @@ -0,0 +1,116 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { AddonManager } = ChromeUtils.importESModule( + "resource://gre/modules/AddonManager.sys.mjs" +); + +function promiseAddonEvent(event) { + return new Promise(resolve => { + const listener = { + [event](...args) { + AddonManager.removeAddonListener(listener); + resolve(args); + }, + }; + + AddonManager.addAddonListener(listener); + }); +} + +function promiseWebExtensionStartup() { + const { Management } = ChromeUtils.importESModule( + "resource://gre/modules/Extension.sys.mjs" + ); + + return new Promise(resolve => { + const listener = (evt, extension) => { + Management.off("ready", listener); + resolve(extension); + }; + + Management.on("ready", listener); + }); +} + +async function reloadAddon(addonFront) { + // The add-on will be re-installed after a successful reload. + const onInstalled = promiseAddonEvent("onInstalled"); + await addonFront.reload(); + await onInstalled; +} + +function getSupportFile(path) { + const allowMissing = false; + return do_get_file(path, allowMissing); +} + +add_task(async function testReloadExitedAddon() { + await startupAddonsManager(); + + DevToolsServer.init(); + DevToolsServer.registerAllActors(); + + const client = new DevToolsClient(DevToolsServer.connectPipe()); + await client.connect(); + + // Install our main add-on to trigger reloads on. + const addonFile = getSupportFile("addons/web-extension"); + const [installedAddon] = await Promise.all([ + AddonManager.installTemporaryAddon(addonFile), + promiseWebExtensionStartup(), + ]); + + // Install a decoy add-on. + const addonFile2 = getSupportFile("addons/web-extension2"); + const [installedAddon2] = await Promise.all([ + AddonManager.installTemporaryAddon(addonFile2), + promiseWebExtensionStartup(), + ]); + + const addonFront = await client.mainRoot.getAddon({ id: installedAddon.id }); + + await Promise.all([reloadAddon(addonFront), promiseWebExtensionStartup()]); + + // Uninstall the decoy add-on, which should cause its actor to exit. + const onUninstalled = promiseAddonEvent("onUninstalled"); + installedAddon2.uninstall(); + await onUninstalled; + + // Try to re-list all add-ons after a reload. + // This was throwing an exception because of the exited actor. + const newAddonFront = await client.mainRoot.getAddon({ + id: installedAddon.id, + }); + equal(newAddonFront.id, addonFront.id); + + // The fronts should be the same after the reload + equal(newAddonFront, addonFront); + + const onAddonListChanged = client.mainRoot.once("addonListChanged"); + + // Install an upgrade version of the first add-on. + const addonUpgradeFile = getSupportFile("addons/web-extension-upgrade"); + const [upgradedAddon] = await Promise.all([ + AddonManager.installTemporaryAddon(addonUpgradeFile), + promiseWebExtensionStartup(), + ]); + + // Waiting for addonListChanged unsolicited event + await onAddonListChanged; + + // re-list all add-ons after an upgrade. + const upgradedAddonFront = await client.mainRoot.getAddon({ + id: upgradedAddon.id, + }); + equal(upgradedAddonFront.id, addonFront.id); + // The fronts should be the same after the upgrade. + equal(upgradedAddonFront, addonFront); + + // The addon metadata has been updated. + equal(upgradedAddonFront.name, "Test Addons Actor Upgrade"); + + await close(client); +}); diff --git a/devtools/server/tests/xpcshell/test_addons_actor.js b/devtools/server/tests/xpcshell/test_addons_actor.js new file mode 100644 index 0000000000..ba9fda6c3d --- /dev/null +++ b/devtools/server/tests/xpcshell/test_addons_actor.js @@ -0,0 +1,55 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +async function connect() { + DevToolsServer.init(); + DevToolsServer.registerAllActors(); + + const client = new DevToolsClient(DevToolsServer.connectPipe()); + await client.connect(); + + const addons = await client.mainRoot.getFront("addons"); + return [client, addons]; +} + +// The AddonsManager test helper can only be called once per test script. +// This `setup` task will run first. +add_task(async function setup() { + await startupAddonsManager(); +}); + +add_task(async function testSuccessfulInstall() { + const [client, addons] = await connect(); + + const allowMissing = false; + const usePlatformSeparator = true; + const addonPath = getFilePath( + "addons/web-extension", + allowMissing, + usePlatformSeparator + ); + const installedAddon = await addons.installTemporaryAddon(addonPath, false); + equal(installedAddon.id, "test-addons-actor@mozilla.org"); + // The returned object is currently not a proper actor. + equal(installedAddon.actor, false); + + const addonList = await client.mainRoot.listAddons(); + ok(addonList && addonList.map(a => a.name), "Received list of add-ons"); + const addon = addonList.find(a => a.id === installedAddon.id); + ok(addon, "Test add-on appeared in root install list"); + + await close(client); +}); + +add_task(async function testNonExistantPath() { + const [client, addons] = await connect(); + + await Assert.rejects( + addons.installTemporaryAddon("some-non-existant-path", false), + /Could not install add-on.*Component returned failure/ + ); + + await close(client); +}); diff --git a/devtools/server/tests/xpcshell/test_animation_name.js b/devtools/server/tests/xpcshell/test_animation_name.js new file mode 100644 index 0000000000..e88911334c --- /dev/null +++ b/devtools/server/tests/xpcshell/test_animation_name.js @@ -0,0 +1,93 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that AnimationPlayerActor.getName returns the right name depending on +// the type of an animation and the various properties available on it. + +const { + AnimationPlayerActor, +} = require("resource://devtools/server/actors/animation.js"); + +function run_test() { + // Mock a window with just the properties the AnimationPlayerActor uses. + const window = {}; + window.MutationObserver = class { + constructor() { + this.observe = () => {}; + } + }; + window.Animation = class { + constructor() { + this.effect = { target: getMockNode() }; + } + }; + + window.CSSAnimation = class extends window.Animation {}; + window.CSSTransition = class extends window.Animation {}; + + // Helper to get a mock DOM node. + function getMockNode() { + return { + ownerDocument: { + defaultView: window, + }, + }; + } + + // Objects in this array should contain the following properties: + // - desc {String} For logging + // - animation {Object} An animation object instantiated from one of the mock + // window animation constructors. + // - props {Objet} Properties of this object will be added to the animation + // object. + // - expectedName {String} The expected name returned by + // AnimationPlayerActor.getName. + const TEST_DATA = [ + { + desc: "Animation with an id", + animation: new window.Animation(), + props: { id: "animation-id" }, + expectedName: "animation-id", + }, + { + desc: "Animation without an id", + animation: new window.Animation(), + props: {}, + expectedName: "", + }, + { + desc: "CSSTransition with an id", + animation: new window.CSSTransition(), + props: { id: "transition-with-id", transitionProperty: "width" }, + expectedName: "transition-with-id", + }, + { + desc: "CSSAnimation with an id", + animation: new window.CSSAnimation(), + props: { id: "animation-with-id", animationName: "move" }, + expectedName: "animation-with-id", + }, + { + desc: "CSSTransition without an id", + animation: new window.CSSTransition(), + props: { transitionProperty: "width" }, + expectedName: "width", + }, + { + desc: "CSSAnimation without an id", + animation: new window.CSSAnimation(), + props: { animationName: "move" }, + expectedName: "move", + }, + ]; + + for (const { desc, animation, props, expectedName } of TEST_DATA) { + info(desc); + for (const key in props) { + animation[key] = props[key]; + } + const actor = new AnimationPlayerActor({}, animation); + Assert.equal(actor.getName(), expectedName); + } +} diff --git a/devtools/server/tests/xpcshell/test_animation_type.js b/devtools/server/tests/xpcshell/test_animation_type.js new file mode 100644 index 0000000000..261b5ef2ac --- /dev/null +++ b/devtools/server/tests/xpcshell/test_animation_type.js @@ -0,0 +1,72 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test the output of AnimationPlayerActor.getType(). + +const { + ANIMATION_TYPES, + AnimationPlayerActor, +} = require("resource://devtools/server/actors/animation.js"); + +function run_test() { + // Mock a window with just the properties the AnimationPlayerActor uses. + const window = {}; + window.MutationObserver = class { + constructor() { + this.observe = () => {}; + } + }; + window.Animation = class { + constructor() { + this.effect = { target: getMockNode() }; + } + }; + + window.CSSAnimation = class extends window.Animation {}; + window.CSSTransition = class extends window.Animation {}; + + // Helper to get a mock DOM node. + function getMockNode() { + return { + ownerDocument: { + defaultView: window, + }, + }; + } + + // Objects in this array should contain the following properties: + // - desc {String} For logging + // - animation {Object} An animation object instantiated from one of the mock + // window animation constructors. + // - expectedType {String} The expected type returned by + // AnimationPlayerActor.getType. + const TEST_DATA = [ + { + desc: "Test CSSAnimation type", + animation: new window.CSSAnimation(), + expectedType: ANIMATION_TYPES.CSS_ANIMATION, + }, + { + desc: "Test CSSTransition type", + animation: new window.CSSTransition(), + expectedType: ANIMATION_TYPES.CSS_TRANSITION, + }, + { + desc: "Test ScriptAnimation type", + animation: new window.Animation(), + expectedType: ANIMATION_TYPES.SCRIPT_ANIMATION, + }, + { + desc: "Test unknown type", + animation: { effect: { target: getMockNode() } }, + expectedType: ANIMATION_TYPES.UNKNOWN, + }, + ]; + + for (const { desc, animation, expectedType } of TEST_DATA) { + info(desc); + const actor = new AnimationPlayerActor({}, animation); + Assert.equal(actor.getType(), expectedType); + } +} diff --git a/devtools/server/tests/xpcshell/test_attach.js b/devtools/server/tests/xpcshell/test_attach.js new file mode 100644 index 0000000000..fb7d232e76 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_attach.js @@ -0,0 +1,28 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { ThreadFront } = require("resource://devtools/client/fronts/thread.js"); +const { + WindowGlobalTargetFront, +} = require("resource://devtools/client/fronts/targets/window-global.js"); + +/** + * Very naive test that checks threadClearTest helper. + * It ensures that the thread front is correctly attached. + */ +add_task( + threadFrontTest(({ threadFront, debuggee, client, targetFront }) => { + ok(true, "Thread actor was able to attach"); + ok(threadFront instanceof ThreadFront, "Thread Front is valid"); + Assert.equal(threadFront.state, "attached", "Thread Front is resumed"); + Assert.equal( + Cu.getSandboxMetadata(debuggee), + undefined, + "Debuggee client is valid (getSandboxMetadata did not fail)" + ); + ok(client instanceof DevToolsClient, "Client is valid"); + ok(targetFront instanceof WindowGlobalTargetFront, "TargetFront is valid"); + }) +); diff --git a/devtools/server/tests/xpcshell/test_blackboxing-01.js b/devtools/server/tests/xpcshell/test_blackboxing-01.js new file mode 100644 index 0000000000..6c549b908e --- /dev/null +++ b/devtools/server/tests/xpcshell/test_blackboxing-01.js @@ -0,0 +1,155 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test basic black boxing. + */ + +var gDebuggee; +var gThreadFront; + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + gThreadFront = threadFront; + gDebuggee = debuggee; + await testBlackBox(); + }) +); + +const BLACK_BOXED_URL = "http://example.com/blackboxme.js"; +const SOURCE_URL = "http://example.com/source.js"; + +const testBlackBox = async function () { + const packet = await executeOnNextTickAndWaitForPause(evalCode, gThreadFront); + + const bpSource = await getSourceById(gThreadFront, packet.frame.where.actor); + + await setBreakpoint(gThreadFront, { sourceUrl: bpSource.url, line: 2 }); + await resume(gThreadFront); + + let sourceForm = await getSourceForm(gThreadFront, BLACK_BOXED_URL); + + Assert.ok( + !sourceForm.isBlackBoxed, + "By default the source is not black boxed." + ); + + // Test that we can step into `doStuff` when we are not black boxed. + await runTest( + async function onSteppedLocation(location) { + const source = await getSourceFormById(gThreadFront, location.actor); + Assert.equal(source.url, BLACK_BOXED_URL); + Assert.equal(location.line, 2); + }, + async function onDebuggerStatementFrames(frames) { + for (const frame of frames) { + const source = await getSourceFormById(gThreadFront, frame.where.actor); + Assert.ok(!source.isBlackBoxed); + } + } + ); + + const blackboxedSource = await getSource(gThreadFront, BLACK_BOXED_URL); + await blackBox(blackboxedSource); + sourceForm = await getSourceForm(gThreadFront, BLACK_BOXED_URL); + Assert.ok(sourceForm.isBlackBoxed); + + // Test that we step through `doStuff` when we are black boxed and its frame + // doesn't show up. + await runTest( + async function onSteppedLocation(location) { + const source = await getSourceFormById(gThreadFront, location.actor); + Assert.equal(source.url, SOURCE_URL); + Assert.equal(location.line, 4); + }, + async function onDebuggerStatementFrames(frames) { + for (const frame of frames) { + const source = await getSourceFormById(gThreadFront, frame.where.actor); + if (source.url == BLACK_BOXED_URL) { + Assert.ok(source.isBlackBoxed); + } else { + Assert.ok(!source.isBlackBoxed); + } + } + } + ); + + await unBlackBox(blackboxedSource); + sourceForm = await getSourceForm(gThreadFront, BLACK_BOXED_URL); + Assert.ok(!sourceForm.isBlackBoxed); + + // Test that we can step into `doStuff` again. + await runTest( + async function onSteppedLocation(location) { + const source = await getSourceFormById(gThreadFront, location.actor); + Assert.equal(source.url, BLACK_BOXED_URL); + Assert.equal(location.line, 2); + }, + async function onDebuggerStatementFrames(frames) { + for (const frame of frames) { + const source = await getSourceFormById(gThreadFront, frame.where.actor); + Assert.ok(!source.isBlackBoxed); + } + } + ); +}; + +function evalCode() { + /* eslint-disable mozilla/var-only-at-top-level, no-undef */ + // prettier-ignore + Cu.evalInSandbox( + "" + function doStuff(k) { // line 1 + var arg = 15; // line 2 - Step in here + k(arg); // line 3 + }, // line 4 + gDebuggee, + "1.8", + BLACK_BOXED_URL, + 1 + ); + + // prettier-ignore + Cu.evalInSandbox( + "" + function runTest() { // line 1 + doStuff( // line 2 - Break here + function (n) { // line 3 - Step through `doStuff` to here + (() => {})(); // line 4 + debugger; // line 5 + } // line 6 + ); // line 7 + } + "\n" // line 8 + + "debugger;", // line 9 + gDebuggee, + "1.8", + SOURCE_URL, + 1 + ); +} + +const runTest = async function (onSteppedLocation, onDebuggerStatementFrames) { + let packet = await executeOnNextTickAndWaitForPause( + gDebuggee.runTest, + gThreadFront + ); + Assert.equal(packet.why.type, "breakpoint"); + + await stepIn(gThreadFront); + + const location = await getCurrentLocation(); + await onSteppedLocation(location); + + packet = await resumeAndWaitForPause(gThreadFront); + Assert.equal(packet.why.type, "debuggerStatement"); + + const { frames } = await getFrames(gThreadFront, 0, 100); + await onDebuggerStatementFrames(frames); + + return resume(gThreadFront); +}; + +const getCurrentLocation = async function () { + const response = await getFrames(gThreadFront, 0, 1); + return response.frames[0].where; +}; diff --git a/devtools/server/tests/xpcshell/test_blackboxing-02.js b/devtools/server/tests/xpcshell/test_blackboxing-02.js new file mode 100644 index 0000000000..66efaee6c8 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_blackboxing-02.js @@ -0,0 +1,95 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that we don't hit breakpoints in black boxed sources, and that when we + * unblack box the source again, the breakpoint hasn't disappeared and we will + * hit it again. + */ + +const BLACK_BOXED_URL = "http://example.com/blackboxme.js"; +const SOURCE_URL = "http://example.com/source.js"; + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + // Set up + await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + threadFront.setBreakpoint({ sourceUrl: BLACK_BOXED_URL, line: 2 }, {}); + await threadFront.resume(); + + // Test the breakpoint in the black boxed source + const { error, sources } = await threadFront.getSources(); + Assert.ok(!error, "Should not get an error: " + error); + const sourceFront = threadFront.source( + sources.filter(s => s.url == BLACK_BOXED_URL)[0] + ); + + await blackBox(sourceFront); + + const packet1 = await executeOnNextTickAndWaitForPause( + debuggee.runTest, + threadFront + ); + + Assert.equal( + packet1.why.type, + "debuggerStatement", + "We should pass over the breakpoint since the source is black boxed." + ); + + await threadFront.resume(); + + // Test the breakpoint in the unblack boxed source + await unBlackBox(sourceFront); + + const packet2 = await executeOnNextTickAndWaitForPause( + debuggee.runTest, + threadFront + ); + + Assert.equal( + packet2.why.type, + "breakpoint", + "We should hit the breakpoint again" + ); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + /* eslint-disable no-undef */ + // prettier-ignore + Cu.evalInSandbox( + "" + function doStuff(k) { // line 1 + const arg = 15; // line 2 - Break here + k(arg); // line 3 + }, // line 4 + debuggee, + "1.8", + BLACK_BOXED_URL, + 1 + ); + // prettier-ignore + Cu.evalInSandbox( + "" + function runTest() { // line 1 + doStuff( // line 2 + function(n) { // line 3 + debugger; // line 5 + } // line 6 + ); // line 7 + } // line 8 + + "\n debugger;", // line 9 + debuggee, + "1.8", + SOURCE_URL, + 1 + ); + /* eslint-enable no-undef */ +} diff --git a/devtools/server/tests/xpcshell/test_blackboxing-03.js b/devtools/server/tests/xpcshell/test_blackboxing-03.js new file mode 100644 index 0000000000..f97c8e70f4 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_blackboxing-03.js @@ -0,0 +1,115 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that we don't stop at debugger statements inside black boxed sources. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + // Set up + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const source = await getSourceById(threadFront, packet.frame.where.actor); + threadFront.setBreakpoint({ sourceUrl: source.url, line: 4 }, {}); + await threadFront.resume(); + + // Test the debugger statement in the black boxed source + await threadFront.getSources(); + const sourceFront = await getSource(threadFront, BLACK_BOXED_URL); + + await blackBox(sourceFront); + + const packet2 = await executeOnNextTickAndWaitForPause( + debuggee.runTest, + threadFront + ); + + Assert.equal( + packet2.why.type, + "breakpoint", + "We should pass over the debugger statement." + ); + + threadFront.removeBreakpoint({ sourceUrl: source.url, line: 4 }, {}); + + await threadFront.resume(); + + // Test the debugger statement in the unblack boxed source + await unBlackBox(sourceFront); + + const packet3 = await executeOnNextTickAndWaitForPause( + debuggee.runTest, + threadFront + ); + + Assert.equal( + packet3.why.type, + "debuggerStatement", + "We should stop at the debugger statement again" + ); + await threadFront.resume(); + + // Test the debugger statement in the black boxed range + threadFront.setBreakpoint({ sourceUrl: source.url, line: 4 }, {}); + + await blackBox(sourceFront, { + start: { line: 1, column: 0 }, + end: { line: 9, column: 0 }, + }); + + const packet4 = await executeOnNextTickAndWaitForPause( + debuggee.runTest, + threadFront + ); + + Assert.equal( + packet4.why.type, + "breakpoint", + "We should pass over the debugger statement." + ); + + threadFront.removeBreakpoint({ sourceUrl: source.url, line: 4 }, {}); + await unBlackBox(sourceFront); + await threadFront.resume(); + }) +); + +const BLACK_BOXED_URL = "http://example.com/blackboxme.js"; +const SOURCE_URL = "http://example.com/source.js"; + +function evalCode(debuggee) { + /* eslint-disable no-multi-spaces, no-undef */ + // prettier-ignore + Cu.evalInSandbox( + "" + function doStuff(k) { // line 1 + debugger; // line 2 - Break here + k(100); // line 3 + }, // line 4 + debuggee, + "1.8", + BLACK_BOXED_URL, + 1 + ); + // prettier-ignore + Cu.evalInSandbox( + "" + function runTest() { // line 1 + doStuff( // line 2 + function(n) { // line 3 + Math.abs(n); // line 4 - Break here + } // line 5 + ); // line 6 + } // line 7 + + "\n debugger;", // line 8 + debuggee, + "1.8", + SOURCE_URL, + 1 + ); + /* eslint-enable no-multi-spaces, no-undef */ +} diff --git a/devtools/server/tests/xpcshell/test_blackboxing-04.js b/devtools/server/tests/xpcshell/test_blackboxing-04.js new file mode 100644 index 0000000000..13345c40e8 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_blackboxing-04.js @@ -0,0 +1,70 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test behavior of blackboxing sources we are currently paused in. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + // Set up + await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + threadFront.setBreakpoint({ sourceUrl: BLACK_BOXED_URL, line: 2 }, {}); + + // Test black boxing a source while pausing in the source + const { error, sources } = await threadFront.getSources(); + Assert.ok(!error, "Should not get an error: " + error); + const sourceFront = threadFront.source( + sources.filter(s => s.url == BLACK_BOXED_URL)[0] + ); + + const pausedInSource = await blackBox(sourceFront); + Assert.ok( + pausedInSource, + "We should be notified that we are currently paused in this source" + ); + await threadFront.resume(); + }) +); + +const BLACK_BOXED_URL = "http://example.com/blackboxme.js"; +const SOURCE_URL = "http://example.com/source.js"; + +function evalCode(debuggee) { + /* eslint-disable no-multi-spaces, no-undef */ + // prettier-ignore + Cu.evalInSandbox( + "" + + function doStuff(k) { // line 1 + debugger; // line 2 + k(100); // line 3 + }, // line 4 + debuggee, + "1.8", + BLACK_BOXED_URL, + 1 + ); + // prettier-ignore + Cu.evalInSandbox( + "" + + function runTest() { // line 1 + doStuff( // line 2 + function(n) { // line 3 + return n; // line 4 + } // line 5 + ); // line 6 + } + // line 7 + "\n runTest();", // line 8 + debuggee, + "1.8", + SOURCE_URL, + 1 + ); + /* eslint-enable no-multi-spaces, no-undef */ +} diff --git a/devtools/server/tests/xpcshell/test_blackboxing-05.js b/devtools/server/tests/xpcshell/test_blackboxing-05.js new file mode 100644 index 0000000000..388c87da88 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_blackboxing-05.js @@ -0,0 +1,97 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test exceptions inside black boxed sources. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee, commands }) => { + await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const { error } = await threadFront.getSources(); + Assert.ok(!error, "Should not get an error: " + error); + + const sourceFront = await getSource(threadFront, BLACK_BOXED_URL); + await blackBox(sourceFront); + await commands.threadConfigurationCommand.updateConfiguration({ + pauseOnExceptions: true, + ignoreCaughtExceptions: false, + }); + + const packet = await resumeAndWaitForPause(threadFront); + const source = await getSourceById(threadFront, packet.frame.where.actor); + + Assert.equal( + source.url, + SOURCE_URL, + "We shouldn't pause while in the black boxed source." + ); + + await unBlackBox(sourceFront); + await blackBox(sourceFront, { + start: { line: 1, column: 0 }, + end: { line: 4, column: 0 }, + }); + + await threadFront.resume(); + + await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const packet2 = await resumeAndWaitForPause(threadFront); + const source2 = await getSourceById(threadFront, packet2.frame.where.actor); + + Assert.equal( + source2.url, + SOURCE_URL, + "We shouldn't pause while in the black boxed source." + ); + + await threadFront.resume(); + }) +); + +const BLACK_BOXED_URL = "http://example.com/blackboxme.js"; +const SOURCE_URL = "http://example.com/source.js"; + +function evalCode(debuggee) { + /* eslint-disable no-multi-spaces, no-unreachable, no-undef */ + // prettier-ignore + Cu.evalInSandbox( + "" + + function doStuff(k) { // line 1 + throw new Error("error msg"); // line 2 + k(100); // line 3 + }, // line 4 + debuggee, + "1.8", + BLACK_BOXED_URL, + 1 + ); + // prettier-ignore + Cu.evalInSandbox( + "" + + function runTest() { // line 1 + doStuff( // line 2 + function(n) { // line 3 + debugger; // line 4 + } // line 5 + ); // line 6 + } + // line 7 + "\ndebugger;\n" + // line 8 + "try { runTest() } catch (ex) { }", // line 9 + debuggee, + "1.8", + SOURCE_URL, + 1 + ); + /* eslint-enable no-multi-spaces, no-unreachable, no-undef */ +} diff --git a/devtools/server/tests/xpcshell/test_blackboxing-08.js b/devtools/server/tests/xpcshell/test_blackboxing-08.js new file mode 100644 index 0000000000..d20d8b3966 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_blackboxing-08.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test blackbox ranges + */ + +async function testFinish({ threadFront, devToolsClient }) { + await threadFront.resume(); + await close(devToolsClient); + + do_test_finished(); +} + +async function invokeAndPause({ global, threadFront }, expression) { + return executeOnNextTickAndWaitForPause( + () => Cu.evalInSandbox(expression, global), + threadFront + ); +} + +function run_test() { + return (async function () { + const dbg = await setupTestFromUrl("stepping.js"); + const { threadFront } = dbg; + + await invokeAndPause(dbg, `chaining()`); + + const { sources } = await getSources(threadFront); + const sourceFront = threadFront.source(sources[0]); + + await setBreakpoint(threadFront, { sourceUrl: sourceFront.url, line: 7 }); + await setBreakpoint(threadFront, { sourceUrl: sourceFront.url, line: 11 }); + + // 1. lets blackbox function a, and assert that we pause in b + const range = { start: { line: 6, column: 0 }, end: { line: 8, colum: 1 } }; + blackBox(sourceFront, range); + const paused = await resumeAndWaitForPause(threadFront); + equal(paused.frame.where.line, 11, "paused inside of b"); + await threadFront.resume(); + + // 2. lets unblackbox function a, and assert that we pause in a + unBlackBox(sourceFront, range); + await invokeAndPause(dbg, `chaining()`); + const paused2 = await resumeAndWaitForPause(threadFront); + equal(paused2.frame.where.line, 7, "paused inside of a"); + + await testFinish(dbg); + })(); +} diff --git a/devtools/server/tests/xpcshell/test_breakpoint-01.js b/devtools/server/tests/xpcshell/test_breakpoint-01.js new file mode 100644 index 0000000000..be46d97cfb --- /dev/null +++ b/devtools/server/tests/xpcshell/test_breakpoint-01.js @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check basic breakpoint functionality. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + info("Wait for the debugger statement to be hit"); + const packet1 = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + const source = await getSourceById(threadFront, packet1.frame.where.actor); + const location = { sourceUrl: source.url, line: debuggee.line0 + 3 }; + + threadFront.setBreakpoint(location, {}); + + const packet2 = await resumeAndWaitForPause(threadFront); + + info("Paused at the breakpoint"); + Assert.equal(packet2.frame.where.actor, source.actor); + Assert.equal(packet2.frame.where.line, location.line); + Assert.equal(packet2.why.type, "breakpoint"); + + info("Check that the breakpoint worked."); + Assert.equal(debuggee.a, 1); + Assert.equal(debuggee.b, undefined); + + await threadFront.resume(); + }) +); + +function evaluateTestCode(debuggee) { + /* + * Be sure to run debuggee code in its own HTML 'task', so that when we call + * the onDebuggerStatement hook, the test's own microtasks don't get suspended + * along with the debuggee's. + */ + do_timeout(0, () => { + // prettier-ignore + Cu.evalInSandbox( + "var line0 = Error().lineNumber;\n" + + "debugger;\n" + // line0 + 1 + "var a = 1;\n" + // line0 + 2 + "var b = 2;\n", // line0 + 3 + debuggee + ); + }); +} diff --git a/devtools/server/tests/xpcshell/test_breakpoint-03.js b/devtools/server/tests/xpcshell/test_breakpoint-03.js new file mode 100644 index 0000000000..f598660a98 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_breakpoint-03.js @@ -0,0 +1,74 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint-disable no-shadow, max-nested-callbacks */ + +"use strict"; + +/** + * Check that setting a breakpoint on a line without code will skip + * forward when we know the script isn't GCed (the debugger is connected, + * so it's kept alive). + */ + +var test_no_skip_breakpoint = async function (source, location, debuggee) { + const [response, bpClient] = await source.setBreakpoint( + Object.assign({}, location, { noSliding: true }) + ); + + Assert.ok(!response.actualLocation); + Assert.equal(bpClient.location.line, debuggee.line0 + 3); + await bpClient.remove(); +}; + +add_task( + threadFrontTest(({ threadFront, debuggee }) => { + return new Promise(resolve => { + threadFront.once("paused", async function (packet) { + const location = { line: debuggee.line0 + 3 }; + const source = await getSourceById( + threadFront, + packet.frame.where.actor + ); + // First, make sure that we can disable sliding with the + // `noSliding` option. + await test_no_skip_breakpoint(source, location, debuggee); + + // Now make sure that the breakpoint properly slides forward one line. + const [response, bpClient] = await source.setBreakpoint(location); + Assert.ok(!!response.actualLocation); + Assert.equal(response.actualLocation.source.actor, source.actor); + Assert.equal(response.actualLocation.line, location.line + 1); + + threadFront.once("paused", function (packet) { + // Check the return value. + Assert.equal(packet.frame.where.actor, source.actor); + Assert.equal(packet.frame.where.line, location.line + 1); + Assert.equal(packet.why.type, "breakpoint"); + Assert.equal(packet.why.actors[0], bpClient.actor); + // Check that the breakpoint worked. + Assert.equal(debuggee.a, 1); + Assert.equal(debuggee.b, undefined); + + // Remove the breakpoint. + bpClient.remove(function (response) { + threadFront.resume().then(resolve); + }); + }); + + threadFront.resume(); + }); + + // Use `evalInSandbox` to make the debugger treat it as normal + // globally-scoped code, where breakpoint sliding rules apply. + // prettier-ignore + Cu.evalInSandbox( + "var line0 = Error().lineNumber;\n" + + "debugger;\n" + // line0 + 1 + "var a = 1;\n" + // line0 + 2 + "// A comment.\n" + // line0 + 3 + "var b = 2;", // line0 + 4 + debuggee + ); + }); + }) +); diff --git a/devtools/server/tests/xpcshell/test_breakpoint-04.js b/devtools/server/tests/xpcshell/test_breakpoint-04.js new file mode 100644 index 0000000000..8b7137f85d --- /dev/null +++ b/devtools/server/tests/xpcshell/test_breakpoint-04.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that setting a breakpoint in a line in a child script works. + */ + +add_task( + threadFrontTest(async ({ threadFront, client, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + const source = await getSourceById(threadFront, packet.frame.where.actor); + const location = { sourceUrl: source.url, line: debuggee.line0 + 3 }; + + //Pause at debugger statement. + Assert.equal(packet.frame.where.line, debuggee.line0 + 5); + Assert.equal(packet.why.type, "debuggerStatement"); + + threadFront.setBreakpoint(location, {}); + await client.waitForRequestsToSettle(); + await resume(threadFront); + + const packet2 = await waitForPause(threadFront); + // Check the return value. + Assert.equal(packet2.frame.where.actor, source.actor); + Assert.equal(packet2.frame.where.line, location.line); + Assert.equal(packet2.why.type, "breakpoint"); + // Check that the breakpoint worked. + Assert.equal(debuggee.a, 1); + Assert.equal(debuggee.b, undefined); + + // Remove the breakpoint. + threadFront.removeBreakpoint(location); + await client.waitForRequestsToSettle(); + + await resume(threadFront); + }) +); + +function evaluateTestCode(debuggee) { + // prettier-ignore + Cu.evalInSandbox( + "var line0 = Error().lineNumber;\n" + + "function foo() {\n" + // line0 + 1 + " this.a = 1;\n" + // line0 + 2 + " this.b = 2;\n" + // line0 + 3 + "}\n" + // line0 + 4 + "debugger;\n" + // line0 + 5 + "foo();\n", // line0 + 6 + debuggee + ); +} diff --git a/devtools/server/tests/xpcshell/test_breakpoint-05.js b/devtools/server/tests/xpcshell/test_breakpoint-05.js new file mode 100644 index 0000000000..f678b285b1 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_breakpoint-05.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint-disable no-shadow, max-nested-callbacks */ + +"use strict"; + +/** + * Check that setting a breakpoint in a line without code in a child script + * will skip forward. + */ + +add_task( + threadFrontTest(({ threadFront, debuggee }) => { + return new Promise(resolve => { + threadFront.once("paused", async function (packet) { + const source = await getSourceById( + threadFront, + packet.frame.where.actor + ); + const location = { line: debuggee.line0 + 3 }; + + source.setBreakpoint(location).then(function ([response, bpClient]) { + // Check that the breakpoint has properly skipped forward one line. + Assert.equal(response.actualLocation.source.actor, source.actor); + Assert.equal(response.actualLocation.line, location.line + 1); + + threadFront.once("paused", function (packet) { + // Check the return value. + Assert.equal(packet.frame.where.actor, source.actor); + Assert.equal(packet.frame.where.line, location.line + 1); + Assert.equal(packet.why.type, "breakpoint"); + Assert.equal(packet.why.actors[0], bpClient.actor); + // Check that the breakpoint worked. + Assert.equal(debuggee.a, 1); + Assert.equal(debuggee.b, undefined); + + // Remove the breakpoint. + bpClient.remove(function (response) { + threadFront.resume().then(resolve); + }); + }); + + // Continue until the breakpoint is hit. + threadFront.resume(); + }); + }); + + // prettier-ignore + Cu.evalInSandbox( + "var line0 = Error().lineNumber;\n" + + "function foo() {\n" + // line0 + 1 + " this.a = 1;\n" + // line0 + 2 + " // A comment.\n" + // line0 + 3 + " this.b = 2;\n" + // line0 + 4 + "}\n" + // line0 + 5 + "debugger;\n" + // line0 + 6 + "foo();\n", // line0 + 7 + debuggee + ); + }); + }) +); diff --git a/devtools/server/tests/xpcshell/test_breakpoint-06.js b/devtools/server/tests/xpcshell/test_breakpoint-06.js new file mode 100644 index 0000000000..79ddcdc3d4 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_breakpoint-06.js @@ -0,0 +1,68 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint-disable no-shadow, max-nested-callbacks */ + +"use strict"; + +/** + * Check that setting a breakpoint in a line without code in a deeply-nested + * child script will skip forward. + */ + +add_task( + threadFrontTest(({ threadFront, debuggee }) => { + return new Promise(resolve => { + threadFront.once("paused", async function (packet) { + const source = await getSourceById( + threadFront, + packet.frame.where.actor + ); + const location = { line: debuggee.line0 + 5 }; + + source.setBreakpoint(location).then(function ([response, bpClient]) { + // Check that the breakpoint has properly skipped forward one line. + Assert.equal(response.actualLocation.source.actor, source.actor); + Assert.equal(response.actualLocation.line, location.line + 1); + + threadFront.once("paused", function (packet) { + // Check the return value. + Assert.equal(packet.frame.where.actor, source.actor); + Assert.equal(packet.frame.where.line, location.line + 1); + Assert.equal(packet.why.type, "breakpoint"); + Assert.equal(packet.why.actors[0], bpClient.actor); + // Check that the breakpoint worked. + Assert.equal(debuggee.a, 1); + Assert.equal(debuggee.b, undefined); + + // Remove the breakpoint. + bpClient.remove(function (response) { + threadFront.resume().then(resolve); + }); + }); + + // Continue until the breakpoint is hit. + threadFront.resume(); + }); + }); + + // prettier-ignore + Cu.evalInSandbox( + "var line0 = Error().lineNumber;\n" + + "function foo() {\n" + // line0 + 1 + " function bar() {\n" + // line0 + 2 + " function baz() {\n" + // line0 + 3 + " this.a = 1;\n" + // line0 + 4 + " // A comment.\n" + // line0 + 5 + " this.b = 2;\n" + // line0 + 6 + " }\n" + // line0 + 7 + " baz();\n" + // line0 + 8 + " }\n" + // line0 + 9 + " bar();\n" + // line0 + 10 + "}\n" + // line0 + 11 + "debugger;\n" + // line0 + 12 + "foo();\n", // line0 + 13 + debuggee + ); + }); + }) +); diff --git a/devtools/server/tests/xpcshell/test_breakpoint-07.js b/devtools/server/tests/xpcshell/test_breakpoint-07.js new file mode 100644 index 0000000000..e6391747bb --- /dev/null +++ b/devtools/server/tests/xpcshell/test_breakpoint-07.js @@ -0,0 +1,65 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint-disable no-shadow, max-nested-callbacks */ + +"use strict"; + +/** + * Check that setting a breakpoint in a line without code in the second child + * script will skip forward. + */ + +add_task( + threadFrontTest(({ threadFront, debuggee }) => { + return new Promise(resolve => { + threadFront.once("paused", async function (packet) { + const source = await getSourceById( + threadFront, + packet.frame.where.actor + ); + const location = { line: debuggee.line0 + 6 }; + + source.setBreakpoint(location).then(function ([response, bpClient]) { + // Check that the breakpoint has properly skipped forward one line. + Assert.equal(response.actualLocation.source.actor, source.actor); + Assert.equal(response.actualLocation.line, location.line + 1); + + threadFront.once("paused", function (packet) { + // Check the return value. + Assert.equal(packet.frame.where.actor, source.actor); + Assert.equal(packet.frame.where.line, location.line + 1); + Assert.equal(packet.why.type, "breakpoint"); + Assert.equal(packet.why.actors[0], bpClient.actor); + // Check that the breakpoint worked. + Assert.equal(debuggee.a, 1); + Assert.equal(debuggee.b, undefined); + + // Remove the breakpoint. + bpClient.remove(function (response) { + threadFront.resume().then(resolve); + }); + }); + + // Continue until the breakpoint is hit. + threadFront.resume(); + }); + }); + + // prettier-ignore + Cu.evalInSandbox( + "var line0 = Error().lineNumber;\n" + + "function foo() {\n" + // line0 + 1 + " bar();\n" + // line0 + 2 + "}\n" + // line0 + 3 + "function bar() {\n" + // line0 + 4 + " this.a = 1;\n" + // line0 + 5 + " // A comment.\n" + // line0 + 6 + " this.b = 2;\n" + // line0 + 7 + "}\n" + // line0 + 8 + "debugger;\n" + // line0 + 9 + "foo();\n", // line0 + 10 + debuggee + ); + }); + }) +); diff --git a/devtools/server/tests/xpcshell/test_breakpoint-08.js b/devtools/server/tests/xpcshell/test_breakpoint-08.js new file mode 100644 index 0000000000..bff0cc3b52 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_breakpoint-08.js @@ -0,0 +1,75 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint-disable no-shadow, max-nested-callbacks */ + +"use strict"; + +/** + * Check that setting a breakpoint in a line without code in a child script + * will skip forward, in a file with two scripts. + */ + +add_task( + threadFrontTest(({ threadFront, debuggee }) => { + return new Promise(resolve => { + threadFront.once("paused", async function (packet) { + const line = debuggee.line0 + 3; + const source = await getSourceById( + threadFront, + packet.frame.where.actor + ); + + // this test has been disabled for a long time so the functionality doesn't work + const response = await threadFront.setBreakpoint( + { sourceUrl: source.url, line }, + {} + ); + // check that the breakpoint has properly skipped forward one line. + assert.equal(response.actuallocation.source.actor, source.actor); + // This is wrong - location is not defined, but the test has been disabled + // for a long time and currently doesn't work. + // eslint-disable-next-line no-undef + Assert.equal(response.actualLocation.line, location.line + 1); + + threadFront.once("paused", function (packet) { + // Check the return value. + Assert.equal(packet.frame.where.actor, source.actor); + // eslint-disable-next-line no-undef + Assert.equal(packet.frame.where.line, location.line + 1); + Assert.equal(packet.why.type, "breakpoint"); + Assert.equal(packet.why.actors[0], response.bpClient.actor); + // Check that the breakpoint worked. + Assert.equal(debuggee.a, 1); + Assert.equal(debuggee.b, undefined); + + // Remove the breakpoint. + response.bpClient.remove(function (response) { + threadFront.resume().then(resolve); + }); + }); + + // Continue until the breakpoint is hit. + threadFront.resume(); + }); + + // prettier-ignore + Cu.evalInSandbox("var line0 = Error().lineNumber;\n" + + "function foo() {\n" + // line0 + 1 + " this.a = 1;\n" + // line0 + 2 + " // A comment.\n" + // line0 + 3 + " this.b = 2;\n" + // line0 + 4 + "}\n", // line0 + 5 + debuggee, + "1.7", + "script1.js"); + + // prettier-ignore + Cu.evalInSandbox("var line1 = Error().lineNumber;\n" + + "debugger;\n" + // line1 + 1 + "foo();\n", // line1 + 2 + debuggee, + "1.7", + "script2.js"); + }); + }) +); diff --git a/devtools/server/tests/xpcshell/test_breakpoint-09.js b/devtools/server/tests/xpcshell/test_breakpoint-09.js new file mode 100644 index 0000000000..90b334102d --- /dev/null +++ b/devtools/server/tests/xpcshell/test_breakpoint-09.js @@ -0,0 +1,72 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint-disable no-shadow, max-nested-callbacks */ + +"use strict"; + +/** + * Check that removing a breakpoint works. + */ + +let done = false; + +add_task( + threadFrontTest(async ({ threadFront, client, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + + const source = await getSourceById(threadFront, packet.frame.where.actor); + const location = { sourceUrl: source.url, line: debuggee.line0 + 2 }; + + //Pause at debugger statement. + Assert.equal(packet.frame.where.line, debuggee.line0 + 7); + Assert.equal(packet.why.type, "debuggerStatement"); + + threadFront.setBreakpoint(location, {}); + await client.waitForRequestsToSettle(); + + await resume(threadFront); + + const packet2 = await waitForPause(threadFront); + + // Check the return value. + Assert.equal(packet2.frame.where.actor, source.actorID); + Assert.equal(packet2.frame.where.line, location.line); + Assert.equal(packet2.why.type, "breakpoint"); + // Check that the breakpoint worked. + Assert.equal(debuggee.a, undefined); + + // Remove the breakpoint. + threadFront.removeBreakpoint(location); + await client.waitForRequestsToSettle(); + + done = true; + threadFront.once("paused", function (packet) { + // The breakpoint should not be hit again. + threadFront.resume().then(function () { + Assert.ok(false); + }); + }); + + await resume(threadFront); + }) +); + +function evaluateTestCode(debuggee) { + // prettier-ignore + Cu.evalInSandbox("var line0 = Error().lineNumber;\n" + + "function foo(stop) {\n" + // line0 + 1 + " this.a = 1;\n" + // line0 + 2 + " if (stop) return;\n" + // line0 + 3 + " delete this.a;\n" + // line0 + 4 + " foo(true);\n" + // line0 + 5 + "}\n" + // line0 + 6 + "debugger;\n" + // line0 + 7 + "foo();\n", // line0 + 8 + debuggee); + if (!done) { + Assert.ok(false); + } +} diff --git a/devtools/server/tests/xpcshell/test_breakpoint-10.js b/devtools/server/tests/xpcshell/test_breakpoint-10.js new file mode 100644 index 0000000000..fd114f173d --- /dev/null +++ b/devtools/server/tests/xpcshell/test_breakpoint-10.js @@ -0,0 +1,81 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that setting a breakpoint in a line with multiple entry points + * triggers no matter which entry point we reach. + */ + +add_task( + threadFrontTest(async ({ threadFront, client, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + const source = await getSourceById(threadFront, packet.frame.where.actor); + const location = { + sourceUrl: source.url, + line: debuggee.line0 + 3, + column: 5, + }; + + //Pause at debugger statement. + Assert.equal(packet.frame.where.line, debuggee.line0 + 1); + Assert.equal(packet.why.type, "debuggerStatement"); + + threadFront.setBreakpoint(location, {}); + await client.waitForRequestsToSettle(); + + await resume(threadFront); + + const packet2 = await waitForPause(threadFront); + // Check the return value. + Assert.equal(packet2.why.type, "breakpoint"); + // Check that the breakpoint worked. + Assert.equal(debuggee.i, 0); + // Check pause location + Assert.equal(packet2.frame.where.line, debuggee.line0 + 3); + Assert.equal(packet2.frame.where.column, 5); + + // Remove the breakpoint. + threadFront.removeBreakpoint(location); + await client.waitForRequestsToSettle(); + + const location2 = { + sourceUrl: source.url, + line: debuggee.line0 + 3, + column: 12, + }; + threadFront.setBreakpoint(location2, {}); + await client.waitForRequestsToSettle(); + + await resume(threadFront); + const packet3 = await waitForPause(threadFront); + // Check the return value. + Assert.equal(packet3.why.type, "breakpoint"); + // Check that the breakpoint worked. + Assert.equal(debuggee.i, 1); + // Check execution location + Assert.equal(packet3.frame.where.line, debuggee.line0 + 3); + Assert.equal(packet3.frame.where.column, 12); + + // Remove the breakpoint. + threadFront.removeBreakpoint(location2); + await client.waitForRequestsToSettle(); + + await resume(threadFront); + }) +); + +function evaluateTestCode(debuggee) { + // prettier-ignore + Cu.evalInSandbox("var line0 = Error().lineNumber;\n" + + "debugger;\n" + // line0 + 1 + "var a, i = 0;\n" + // line0 + 2 + "for (i = 1; i <= 2; i++) {\n" + // line0 + 3 + " a = i;\n" + // line0 + 4 + "}\n", // line0 + 5 + debuggee); +} diff --git a/devtools/server/tests/xpcshell/test_breakpoint-11.js b/devtools/server/tests/xpcshell/test_breakpoint-11.js new file mode 100644 index 0000000000..a29cd2f768 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_breakpoint-11.js @@ -0,0 +1,77 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Make sure that setting a breakpoint in a line with bytecodes in multiple + * scripts, sets the breakpoint in all of them (bug 793214). + */ + +add_task( + threadFrontTest(async ({ threadFront, client, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + const source = await getSourceById(threadFront, packet.frame.where.actor); + const location = { + sourceUrl: source.url, + line: debuggee.line0 + 2, + column: 8, + }; + + //Pause at debugger statement. + Assert.equal(packet.frame.where.line, debuggee.line0 + 1); + Assert.equal(packet.why.type, "debuggerStatement"); + + threadFront.setBreakpoint(location, {}); + await resume(threadFront); + + const packet2 = await waitForPause(threadFront); + + // Check the return value. + Assert.equal(packet2.why.type, "breakpoint"); + // Check that the breakpoint worked. + Assert.equal(debuggee.a, undefined); + // Check execution location + Assert.equal(packet2.frame.where.line, debuggee.line0 + 2); + Assert.equal(packet2.frame.where.column, 8); + + // Remove the breakpoint. + threadFront.removeBreakpoint(location); + + const location2 = { + sourceUrl: source.url, + line: debuggee.line0 + 2, + column: 32, + }; + threadFront.setBreakpoint(location2, {}); + + await resume(threadFront); + const packet3 = await waitForPause(threadFront); + + // Check the return value. + Assert.equal(packet3.why.type, "breakpoint"); + // Check that the breakpoint worked. + Assert.equal(debuggee.a.b, 1); + Assert.equal(debuggee.res, undefined); + // Check execution location + Assert.equal(packet3.frame.where.line, debuggee.line0 + 2); + Assert.equal(packet3.frame.where.column, 32); + + // Remove the breakpoint. + threadFront.removeBreakpoint(location2); + + await resume(threadFront); + }) +); + +function evaluateTestCode(debuggee) { + // prettier-ignore + Cu.evalInSandbox("var line0 = Error().lineNumber;\n" + + "debugger;\n" + // line0 + 1 + "var a = { b: 1, f: function() { return 2; } };\n" + // line0+2 + "var res = a.f();\n", // line0 + 3 + debuggee); +} diff --git a/devtools/server/tests/xpcshell/test_breakpoint-12.js b/devtools/server/tests/xpcshell/test_breakpoint-12.js new file mode 100644 index 0000000000..44b524f1cf --- /dev/null +++ b/devtools/server/tests/xpcshell/test_breakpoint-12.js @@ -0,0 +1,93 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint-disable no-shadow, max-nested-callbacks */ + +"use strict"; + +/** + * Make sure that setting a breakpoint twice in a line without bytecodes works + * as expected. + */ + +const NUM_BREAKPOINTS = 10; +var gBpActor; +var gCount; + +add_task( + threadFrontTest(({ threadFront, debuggee }) => { + return new Promise(resolve => { + threadFront.once("paused", async function (packet) { + const source = await getSourceById( + threadFront, + packet.frame.where.actor + ); + const location = { line: debuggee.line0 + 3 }; + + source.setBreakpoint(location).then(function ([response, bpClient]) { + // Check that the breakpoint has properly skipped forward one line. + Assert.equal(response.actualLocation.source.actor, source.actor); + Assert.equal(response.actualLocation.line, location.line + 1); + gBpActor = response.actor; + + // Set more breakpoints at the same location. + set_breakpoints(source, location); + }); + }); + + /* eslint-disable no-multi-spaces */ + Cu.evalInSandbox( + "var line0 = Error().lineNumber;\n" + + "function foo() {\n" + // line0 + 1 + " this.a = 1;\n" + // line0 + 2 + " // A comment.\n" + // line0 + 3 + " this.b = 2;\n" + // line0 + 4 + "}\n" + // line0 + 5 + "debugger;\n" + // line0 + 6 + "foo();\n", // line0 + 7 + debuggee + ); + /* eslint-enable no-multi-spaces */ + + // Set many breakpoints at the same location. + function set_breakpoints(source, location) { + Assert.notEqual(gCount, NUM_BREAKPOINTS); + source.setBreakpoint(location).then(function ([response, bpClient]) { + // Check that the breakpoint has properly skipped forward one line. + Assert.equal(response.actualLocation.source.actor, source.actor); + Assert.equal(response.actualLocation.line, location.line + 1); + // Check that the same breakpoint actor was returned. + Assert.equal(response.actor, gBpActor); + + if (++gCount < NUM_BREAKPOINTS) { + set_breakpoints(source, location); + return; + } + + // After setting all the breakpoints, check that only one has effectively + // remained. + threadFront.once("paused", function (packet) { + // Check the return value. + Assert.equal(packet.frame.where.actor, source.actor); + Assert.equal(packet.frame.where.line, location.line + 1); + Assert.equal(packet.why.type, "breakpoint"); + Assert.equal(packet.why.actors[0], bpClient.actor); + // Check that the breakpoint worked. + Assert.equal(debuggee.a, 1); + Assert.equal(debuggee.b, undefined); + + threadFront.once("paused", function (packet) { + // We don't expect any more pauses after the breakpoint was hit once. + Assert.ok(false); + }); + threadFront.resume().then(function () { + // Give any remaining breakpoints a chance to trigger. + do_timeout(1000, resolve); + }); + }); + // Continue until the breakpoint is hit. + threadFront.resume(); + }); + } + }); + }) +); diff --git a/devtools/server/tests/xpcshell/test_breakpoint-13.js b/devtools/server/tests/xpcshell/test_breakpoint-13.js new file mode 100644 index 0000000000..2265f3449a --- /dev/null +++ b/devtools/server/tests/xpcshell/test_breakpoint-13.js @@ -0,0 +1,78 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that execution doesn't pause twice while stepping, when encountering + * either a breakpoint or a debugger statement. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + + const source = await getSourceById(threadFront, packet.frame.where.actor); + await threadFront.setBreakpoint( + { sourceUrl: source.url, line: 3, column: 6 }, + {} + ); + + info("Check that the stepping worked."); + const packet1 = await stepIn(threadFront); + Assert.equal(packet1.frame.where.line, 6); + Assert.equal(packet1.why.type, "resumeLimit"); + + info("Entered the foo function call frame."); + const packet2 = await stepIn(threadFront); + Assert.equal(packet2.frame.where.line, 3); + Assert.equal(packet2.why.type, "resumeLimit"); + + info("Check that the breakpoint wasn't the reason for this pause"); + const packet3 = await stepIn(threadFront); + Assert.equal(packet3.frame.where.line, 4); + Assert.equal(packet3.why.type, "resumeLimit"); + Assert.equal(packet3.why.frameFinished.return.type, "undefined"); + + info("Check that the debugger statement wasn't the reason for this pause."); + const packet4 = await stepIn(threadFront); + Assert.equal(debuggee.a, 1); + Assert.equal(debuggee.b, undefined); + Assert.equal(packet4.frame.where.line, 7); + Assert.equal(packet4.why.type, "resumeLimit"); + + info("Check that the debugger statement wasn't the reason for this pause."); + const packet5 = await stepIn(threadFront); + Assert.equal(packet5.frame.where.line, 8); + Assert.equal(packet5.why.type, "resumeLimit"); + + info("Remove the breakpoint and finish."); + await stepIn(threadFront); + threadFront.removeBreakpoint({ sourceUrl: source.url, line: 3 }); + + await resume(threadFront); + }) +); + +function evaluateTestCode(debuggee) { + /* eslint-disable */ + Cu.evalInSandbox( + ` + function foo() { + this.a = 1; // <-- breakpoint set here + } + debugger; + foo(); + debugger; + var b = 2; + `, + debuggee, + "1.8", + "test_breakpoint-13.js", + 1 + ); + /* eslint-enable */ +} diff --git a/devtools/server/tests/xpcshell/test_breakpoint-14.js b/devtools/server/tests/xpcshell/test_breakpoint-14.js new file mode 100644 index 0000000000..835edb1385 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_breakpoint-14.js @@ -0,0 +1,90 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint-disable no-shadow */ + +"use strict"; + +/** + * Check that a breakpoint or a debugger statement cause execution to pause even + * in a stepped-over function. + */ + +add_task( + threadFrontTest(async ({ threadFront, client, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + const source = await getSourceById(threadFront, packet.frame.where.actor); + const location = { + sourceUrl: source.url, + line: debuggee.line0 + 2, + }; + + //Pause at debugger statement. + Assert.equal(packet.frame.where.line, debuggee.line0 + 4); + Assert.equal(packet.why.type, "debuggerStatement"); + + threadFront.setBreakpoint(location, {}); + + const testCallbacks = [ + function (packet) { + // Check that the stepping worked. + Assert.equal(packet.frame.where.line, debuggee.line0 + 5); + Assert.equal(packet.why.type, "resumeLimit"); + }, + function (packet) { + // Reached the breakpoint. + Assert.equal(packet.frame.where.line, location.line); + Assert.equal(packet.why.type, "breakpoint"); + Assert.notEqual(packet.why.type, "resumeLimit"); + }, + function (packet) { + // The frame is about to be popped while stepping. + Assert.equal(packet.frame.where.line, debuggee.line0 + 3); + Assert.notEqual(packet.why.type, "breakpoint"); + Assert.equal(packet.why.type, "resumeLimit"); + Assert.equal(packet.why.frameFinished.return.type, "undefined"); + }, + function (packet) { + // Check that the debugger statement wasn't the reason for this pause. + Assert.equal(debuggee.a, 1); + Assert.equal(debuggee.b, undefined); + Assert.equal(packet.frame.where.line, debuggee.line0 + 6); + Assert.notEqual(packet.why.type, "debuggerStatement"); + Assert.equal(packet.why.type, "resumeLimit"); + }, + function (packet) { + // Check that the debugger statement wasn't the reason for this pause. + Assert.equal(packet.frame.where.line, debuggee.line0 + 7); + Assert.notEqual(packet.why.type, "debuggerStatement"); + Assert.equal(packet.why.type, "resumeLimit"); + }, + ]; + + for (const callback of testCallbacks) { + const waiter = waitForPause(threadFront); + threadFront.stepOver(); + const packet = await waiter; + callback(packet); + } + + // Remove the breakpoint and finish. + threadFront.removeBreakpoint(location); + + await threadFront.resume(); + }) +); + +function evaluateTestCode(debuggee) { + // prettier-ignore + Cu.evalInSandbox("var line0 = Error().lineNumber;\n" + + "function foo() {\n" + // line0 + 1 + " this.a = 1;\n" + // line0 + 2 <-- Breakpoint is set here. + "}\n" + // line0 + 3 + "debugger;\n" + // line0 + 4 + "foo();\n" + // line0 + 5 + "debugger;\n" + // line0 + 6 + "var b = 2;\n", // line0 + 7 + debuggee); +} diff --git a/devtools/server/tests/xpcshell/test_breakpoint-16.js b/devtools/server/tests/xpcshell/test_breakpoint-16.js new file mode 100644 index 0000000000..a42306eee1 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_breakpoint-16.js @@ -0,0 +1,70 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint-disable no-shadow, max-nested-callbacks */ + +"use strict"; + +/** + * Check that we can set breakpoints in columns, not just lines. + */ + +add_task( + threadFrontTest(async ({ threadFront, client, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + const source = await getSourceById(threadFront, packet.frame.where.actor); + const location = { + sourceUrl: source.url, + line: debuggee.line0 + 1, + column: 55, + }; + + let timesBreakpointHit = 0; + threadFront.setBreakpoint(location, {}); + + while (timesBreakpointHit < 3) { + await resume(threadFront); + const packet = await waitForPause(threadFront); + await testAssertions( + packet, + debuggee, + source, + location, + timesBreakpointHit + ); + + timesBreakpointHit++; + } + + threadFront.removeBreakpoint(location); + await threadFront.resume(); + }) +); + +function evaluateTestCode(debuggee) { + // prettier-ignore + Cu.evalInSandbox( + "var line0 = Error().lineNumber;\n" + + "(function () { debugger; this.acc = 0; for (var i = 0; i < 3; i++) this.acc++; }());", + debuggee + ); +} + +async function testAssertions( + packet, + debuggee, + source, + location, + timesBreakpointHit +) { + Assert.equal(packet.why.type, "breakpoint"); + Assert.equal(packet.frame.where.actor, source.actor); + Assert.equal(packet.frame.where.line, location.line); + Assert.equal(packet.frame.where.column, location.column); + + Assert.equal(debuggee.acc, timesBreakpointHit); + const environment = await packet.frame.getEnvironment(); + Assert.equal(environment.bindings.variables.i.value, timesBreakpointHit); +} diff --git a/devtools/server/tests/xpcshell/test_breakpoint-17.js b/devtools/server/tests/xpcshell/test_breakpoint-17.js new file mode 100644 index 0000000000..c52e6547ef --- /dev/null +++ b/devtools/server/tests/xpcshell/test_breakpoint-17.js @@ -0,0 +1,130 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint-disable no-shadow */ + +"use strict"; + +/** + * Test that when we add 2 breakpoints to the same line at different columns and + * then remove one of them, we don't remove them both. + */ + +const code = + "(" + + function (global) { + global.foo = function () { + Math.abs(-1); + Math.log(0.5); + debugger; + }; + debugger; + } + + "(this))"; + +const firstLocation = { + line: 3, + column: 4, +}; + +const secondLocation = { + line: 3, + column: 18, +}; + +add_task( + threadFrontTest(({ threadFront, debuggee }) => { + return new Promise(resolve => { + threadFront.on("paused", async packet => { + const [first, second] = await set_breakpoints(packet, threadFront); + test_different_actors(first, second); + await test_remove_one(first, second, threadFront, debuggee); + resolve(); + }); + + Cu.evalInSandbox(code, debuggee, "1.8", "http://example.com/", 1); + }); + }) +); + +async function set_breakpoints(packet, threadFront) { + const source = await getSourceById(threadFront, packet.frame.where.actor); + return new Promise(resolve => { + let first, second; + + source + .setBreakpoint(firstLocation) + .then(function ([{ actualLocation }, breakpointClient]) { + Assert.ok(!actualLocation, "Should not get an actualLocation"); + first = breakpointClient; + + source + .setBreakpoint(secondLocation) + .then(function ([{ actualLocation }, breakpointClient]) { + Assert.ok(!actualLocation, "Should not get an actualLocation"); + second = breakpointClient; + + resolve([first, second]); + }); + }); + }); +} + +function test_different_actors(first, second) { + Assert.notEqual( + first.actor, + second.actor, + "Each breakpoint should have a different actor" + ); +} + +function test_remove_one(first, second, threadFront, debuggee) { + return new Promise(resolve => { + first.remove(function ({ error }) { + Assert.ok(!error, "Should not get an error removing a breakpoint"); + + let hitSecond; + threadFront.on("paused", function _onPaused({ why, frame }) { + if (why.type == "breakpoint") { + hitSecond = true; + Assert.equal( + why.actors.length, + 1, + "Should only be paused because of one breakpoint actor" + ); + Assert.equal( + why.actors[0], + second.actor, + "Should be paused because of the correct breakpoint actor" + ); + Assert.equal( + frame.where.line, + secondLocation.line, + "Should be at the right line" + ); + Assert.equal( + frame.where.column, + secondLocation.column, + "Should be at the right column" + ); + threadFront.resume(); + return; + } + + if (why.type == "debuggerStatement") { + threadFront.off("paused", _onPaused); + Assert.ok( + hitSecond, + "We should still hit `second`, but not `first`." + ); + + resolve(); + return; + } + + Assert.ok(false, "Should never get here"); + }); + + threadFront.resume().then(() => debuggee.foo()); + }); + }); +} diff --git a/devtools/server/tests/xpcshell/test_breakpoint-18.js b/devtools/server/tests/xpcshell/test_breakpoint-18.js new file mode 100644 index 0000000000..b2c86458d0 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_breakpoint-18.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that we only break on offsets that are entry points for the line we are + * breaking on. Bug 907278. + */ + +add_task( + threadFrontTest(async ({ threadFront, client, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + + const source = await getSourceById(threadFront, packet.frame.where.actor); + const location = { sourceUrl: source.url, line: 3 }; + threadFront.setBreakpoint(location, {}); + await client.waitForRequestsToSettle(); + + debuggee.console = { log: x => void x }; + + await resume(threadFront); + + const packet2 = await executeOnNextTickAndWaitForPause( + debuggee.test, + threadFront + ); + Assert.equal(packet2.why.type, "breakpoint"); + + const packet3 = await resumeAndWaitForPause(threadFront); + testDbgStatement(packet3); + + await resume(threadFront); + }) +); + +function evaluateTestCode(debuggee) { + Cu.evalInSandbox( + "debugger;\n" + + function test() { + console.log("foo bar"); + debugger; + }, + debuggee, + "1.8", + "http://example.com/", + 1 + ); +} + +function testDbgStatement({ why }) { + // Should continue to the debugger statement. + Assert.equal(why.type, "debuggerStatement"); + // Not break on another offset from the same line (that isn't an entry point + // to the line) + Assert.notEqual(why.type, "breakpoint"); +} diff --git a/devtools/server/tests/xpcshell/test_breakpoint-19.js b/devtools/server/tests/xpcshell/test_breakpoint-19.js new file mode 100644 index 0000000000..013acdfaf1 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_breakpoint-19.js @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Make sure that setting a breakpoint in a not-yet-existing script doesn't throw + * an error (see bug 897567). Also make sure that this breakpoint works. + */ + +const URL = "test.js"; + +function setUpCode(debuggee) { + /* eslint-disable mozilla/var-only-at-top-level, no-unused-vars */ + // prettier-ignore + Cu.evalInSandbox( + "" + function test() { // 1 + var a = 1; // 2 + debugger; // 3 + } + // 4 + "\ndebugger;", // 5 + debuggee, + "1.8", + URL + ); + /* eslint-enable mozilla/var-only-at-top-level, no-unused-vars */ +} + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + setBreakpoint(threadFront, { sourceUrl: URL, line: 2 }); + + await executeOnNextTickAndWaitForPause( + () => setUpCode(debuggee), + threadFront + ); + await resume(threadFront); + + const packet = await executeOnNextTickAndWaitForPause( + debuggee.test, + threadFront + ); + equal(packet.why.type, "breakpoint"); + }) +); diff --git a/devtools/server/tests/xpcshell/test_breakpoint-20.js b/devtools/server/tests/xpcshell/test_breakpoint-20.js new file mode 100644 index 0000000000..886d44164d --- /dev/null +++ b/devtools/server/tests/xpcshell/test_breakpoint-20.js @@ -0,0 +1,109 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Verify that when two of the "same" source are loaded concurrently (like e10s + * frame scripts), breakpoints get hit in scripts defined by all sources. + */ + +var gDebuggee; + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + gDebuggee = debuggee; + await testBreakpoint(threadFront); + }) +); + +const testBreakpoint = async function (threadFront) { + evalSetupCode(); + + // Load the test source once. + + evalTestCode(); + equal( + gDebuggee.functions.length, + 1, + "The test code should have added a function." + ); + + // Set a breakpoint in the test source. + + const source = await getSource(threadFront, "test.js"); + setBreakpoint(threadFront, { sourceUrl: source.url, line: 3 }); + + // Load the test source again. + + evalTestCode(); + equal( + gDebuggee.functions.length, + 2, + "The test code should have added another function." + ); + + // Should hit our breakpoint in a script defined by the first instance of the + // test source. + + const bpPause1 = await executeOnNextTickAndWaitForPause( + gDebuggee.functions[0], + threadFront + ); + equal( + bpPause1.why.type, + "breakpoint", + "Should pause because of hitting our breakpoint (not debugger statement)." + ); + const dbgStmtPause1 = await executeOnNextTickAndWaitForPause( + () => resume(threadFront), + threadFront + ); + equal( + dbgStmtPause1.why.type, + "debuggerStatement", + "And we should hit the debugger statement after the pause." + ); + await resume(threadFront); + + // Should also hit our breakpoint in a script defined by the second instance + // of the test source. + + const bpPause2 = await executeOnNextTickAndWaitForPause( + gDebuggee.functions[1], + threadFront + ); + equal( + bpPause2.why.type, + "breakpoint", + "Should pause because of hitting our breakpoint (not debugger statement)." + ); + const dbgStmtPause2 = await executeOnNextTickAndWaitForPause( + () => resume(threadFront), + threadFront + ); + equal( + dbgStmtPause2.why.type, + "debuggerStatement", + "And we should hit the debugger statement after the pause." + ); +}; + +function evalSetupCode() { + Cu.evalInSandbox("this.functions = [];", gDebuggee, "1.8", "setup.js", 1); +} + +function evalTestCode() { + Cu.evalInSandbox( + ` // 1 + this.functions.push(function () { // 2 + var setBreakpointHere = 1; // 3 + debugger; // 4 + }); // 5 + `, + gDebuggee, + "1.8", + "test.js", + 1 + ); +} diff --git a/devtools/server/tests/xpcshell/test_breakpoint-21.js b/devtools/server/tests/xpcshell/test_breakpoint-21.js new file mode 100644 index 0000000000..da7a87f91c --- /dev/null +++ b/devtools/server/tests/xpcshell/test_breakpoint-21.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Bug 1122064 - make sure that scripts introduced via onNewScripts + * properly populate the `ScriptStore` with all there nested child + * scripts, so you can set breakpoints on deeply nested scripts + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + // Populate the `ScriptStore` so that we only test that the script + // is added through `onNewScript` + await getSources(threadFront); + + let packet = await executeOnNextTickAndWaitForPause(() => { + evalCode(debuggee); + }, threadFront); + const source = await getSourceById(threadFront, packet.frame.where.actor); + const location = { + sourceUrl: source.url, + line: debuggee.line0 + 8, + }; + + setBreakpoint(threadFront, location); + + await resume(threadFront); + packet = await waitForPause(threadFront); + Assert.equal(packet.why.type, "breakpoint"); + Assert.equal(packet.frame.where.actor, source.actor); + Assert.equal(packet.frame.where.line, location.line); + + await resume(threadFront); + }) +); + +function evalCode(debuggee) { + // Start a new script + /* eslint-disable mozilla/var-only-at-top-level, max-nested-callbacks, no-unused-vars */ + // prettier-ignore + Cu.evalInSandbox( + "var line0 = Error().lineNumber;\n(" + function () { + debugger; + var a = (function () { + return (function () { + return (function () { + return (function () { + return (function () { + var x = 10; // This line gets a breakpoint + return 1; + })(); + })(); + })(); + })(); + })(); + } + ")()", + debuggee + ); + /* eslint-enable mozilla/var-only-at-top-level, max-nested-callbacks, no-unused-vars */ +} diff --git a/devtools/server/tests/xpcshell/test_breakpoint-22.js b/devtools/server/tests/xpcshell/test_breakpoint-22.js new file mode 100644 index 0000000000..067dfa3fa2 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_breakpoint-22.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Bug 1333219 - make that setBreakpoint fails when script is not found + * at the specified line. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + // Populate the `ScriptStore` so that we only test that the script + // is added through `onNewScript` + await getSources(threadFront); + + const packet = await executeOnNextTickAndWaitForPause(() => { + evalCode(debuggee); + }, threadFront); + const source = await getSourceById(threadFront, packet.frame.where.actor); + + const location = { + line: debuggee.line0 + 2, + }; + + const [res] = await setBreakpoint(source, location); + ok(!res.error); + + const location2 = { + line: debuggee.line0 + 7, + }; + + await source.setBreakpoint(location2).then( + () => { + do_throw("no code shall not be found the specified line or below it"); + }, + reason => { + Assert.equal(reason.error, "noCodeAtLineColumn"); + ok(reason.message); + } + ); + + await resume(threadFront); + }) +); + +function evalCode(debuggee) { + // Start a new script + Cu.evalInSandbox( + ` +var line0 = Error().lineNumber; +function some_function() { + // breakpoint is valid here -- it slides one line below (line0 + 2) +} +debugger; +// no breakpoint is allowed after the EOF (line0 + 6) +`, + debuggee + ); +} diff --git a/devtools/server/tests/xpcshell/test_breakpoint-23.js b/devtools/server/tests/xpcshell/test_breakpoint-23.js new file mode 100644 index 0000000000..8f07190ea9 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_breakpoint-23.js @@ -0,0 +1,35 @@ +/* eslint-disable max-nested-callbacks */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Bug 1552453 - Verify that breakpoints are hit for evaluated + * scripts that contain a source url pragma. + */ +add_task( + threadFrontTest(async ({ commands, threadFront }) => { + await threadFront.setBreakpoint( + { sourceUrl: "http://example.com/code.js", line: 2, column: 1 }, + {} + ); + + info("Create a new script with the displayUrl code.js"); + const onNewSource = waitForEvent(threadFront, "newSource"); + await commands.scriptCommand.execute( + "function f() {\n return 5; \n}\n//# sourceURL=http://example.com/code.js" + ); + const sourcePacket = await onNewSource; + + equal(sourcePacket.source.url, "http://example.com/code.js"); + + info("Evaluate f() and pause at line 2"); + const onExecutionDone = commands.scriptCommand.execute("f()"); + const pausedPacket = await waitForPause(threadFront); + equal(pausedPacket.why.type, "breakpoint"); + equal(pausedPacket.frame.where.line, 2); + resume(threadFront); + await onExecutionDone; + }) +); diff --git a/devtools/server/tests/xpcshell/test_breakpoint-24.js b/devtools/server/tests/xpcshell/test_breakpoint-24.js new file mode 100644 index 0000000000..a240a237f0 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_breakpoint-24.js @@ -0,0 +1,239 @@ +/* eslint-disable max-nested-callbacks */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Bug 1441183 - Verify that the debugger advances to a new location + * when encountering debugger statements and brakpoints + * + * Bug 1613165 - Verify that debugger statement is not disabled by + * adding/removing a breakpoint + */ +add_task( + threadFrontTest(async props => { + await testDebuggerStatements(props); + await testBreakpoints(props); + await testBreakpointsAndDebuggerStatements(props); + await testLoops(props); + await testRemovingBreakpoint(props); + await testAddingBreakpoint(props); + }) +); + +// Ensure that we advance to the next line when we +// step to a debugger statement and resume. +async function testDebuggerStatements({ commands, threadFront }) { + commands.scriptCommand.execute(`function foo(stop) { + debugger; + debugger; + debugger; + } + foo(); + //# sourceURL=http://example.com/code.js`); + + await performActions(threadFront, [ + [ + "paused at first debugger statement", + { line: 2, type: "debuggerStatement" }, + "stepOver", + ], + [ + "paused at the second debugger statement", + { line: 3, type: "resumeLimit" }, + "resume", + ], + [ + "paused at the third debugger statement", + { line: 4, type: "debuggerStatement" }, + "resume", + ], + ]); +} + +// Ensure that we advance to the next line when we hit a breakpoint +// on a line with a debugger statement and resume. +async function testBreakpointsAndDebuggerStatements({ commands, threadFront }) { + commands.scriptCommand.execute(`function foo(stop) { + debugger; + debugger; + debugger; + } + foo(); + //# sourceURL=http://example.com/testBreakpointsAndDebuggerStatements.js`); + + threadFront.setBreakpoint( + { + sourceUrl: "http://example.com/testBreakpointsAndDebuggerStatements.js", + line: 3, + }, + {} + ); + + await performActions(threadFront, [ + [ + "paused at first debugger statement", + { line: 2, type: "debuggerStatement" }, + "resume", + ], + [ + "paused at the breakpoint at the second debugger statement", + { line: 3, type: "breakpoint" }, + "resume", + ], + [ + "pause at the third debugger statement", + { line: 4, type: "debuggerStatement" }, + "resume", + ], + ]); +} + +// Ensure that we advance to the next line when we step to +// a line with a breakpoint and resume. +async function testBreakpoints({ commands, threadFront }) { + commands.scriptCommand.execute(`function foo(stop) { + debugger; + a(); + debugger; + } + function a() {} + foo(); + //# sourceURL=http://example.com/testBreakpoints.js`); + + threadFront.setBreakpoint( + { sourceUrl: "http://example.com/testBreakpoints.js", line: 3, column: 6 }, + {} + ); + + await performActions(threadFront, [ + [ + "paused at first debugger statement", + { line: 2, type: "debuggerStatement" }, + "stepOver", + ], + ["paused at a()", { line: 3, type: "resumeLimit" }, "resume"], + [ + "pause at the second debugger satement", + { line: 4, type: "debuggerStatement" }, + "resume", + ], + ]); +} + +// Ensure that we advance to the next line when we step to +// a line with a breakpoint and resume. +async function testLoops({ commands, threadFront }) { + commands.scriptCommand.execute(`function foo(stop) { + let i = 0; + debugger; + while (i++ < 2) { + debugger; + } + debugger; + } + foo(); + //# sourceURL=http://example.com/testLoops.js`); + + await performActions(threadFront, [ + [ + "paused at first debugger statement", + { line: 3, type: "debuggerStatement" }, + "resume", + ], + [ + "pause at the second debugger satement", + { line: 5, type: "debuggerStatement" }, + "resume", + ], + [ + "pause at the second debugger satement (2nd time)", + { line: 5, type: "debuggerStatement" }, + "resume", + ], + [ + "pause at the third debugger satement", + { line: 7, type: "debuggerStatement" }, + "resume", + ], + ]); +} + +// Bug 1613165 - ensure that if you pause on a breakpoint on a line with +// debugger statement, remove the breakpoint, and try to pause on the +// debugger statement before pausing anywhere else, debugger pauses instead of +// skipping debugger statement. +async function testRemovingBreakpoint({ commands, threadFront }) { + commands.scriptCommand.execute(`function foo(stop) { + debugger; + } + foo(); + foo(); + //# sourceURL=http://example.com/testRemovingBreakpoint.js`); + + const location = { + sourceUrl: "http://example.com/testRemovingBreakpoint.js", + line: 2, + column: 6, + }; + + threadFront.setBreakpoint(location, {}); + + info("paused at the breakpoint at the first debugger statement"); + const packet = await waitForEvent(threadFront, "paused"); + Assert.equal(packet.frame.where.line, 2); + Assert.equal(packet.why.type, "breakpoint"); + threadFront.removeBreakpoint(location); + + info("paused at the first debugger statement"); + const packet2 = await resumeAndWaitForPause(threadFront); + Assert.equal(packet2.frame.where.line, 2); + Assert.equal(packet2.why.type, "debuggerStatement"); + await threadFront.resume(); +} + +// Bug 1613165 - ensure if you pause on a debugger statement, add a +// breakpoint on the same line, and try to pause on the breakpoint +// before pausing anywhere else, debugger pauses on that line instead of +// skipping breakpoint. +async function testAddingBreakpoint({ commands, threadFront }) { + commands.scriptCommand.execute(`function foo(stop) { + debugger; + } + foo(); + foo(); + //# sourceURL=http://example.com/testAddingBreakpoint.js`); + + const location = { + sourceUrl: "http://example.com/testAddingBreakpoint.js", + line: 2, + column: 6, + }; + + info("paused at the first debugger statement"); + const packet = await waitForEvent(threadFront, "paused"); + Assert.equal(packet.frame.where.line, 2); + Assert.equal(packet.why.type, "debuggerStatement"); + threadFront.setBreakpoint(location, {}); + + info("paused at the breakpoint at the first debugger statement"); + const packet2 = await resumeAndWaitForPause(threadFront); + Assert.equal(packet2.frame.where.line, 2); + Assert.equal(packet2.why.type, "breakpoint"); + await threadFront.resume(); +} + +async function performActions(threadFront, actions) { + for (const action of actions) { + await performAction(threadFront, action); + } +} + +async function performAction(threadFront, [description, result, action]) { + info(description); + const packet = await waitForEvent(threadFront, "paused"); + Assert.equal(packet.frame.where.line, result.line); + Assert.equal(packet.why.type, result.type); + await threadFront[action](); +} diff --git a/devtools/server/tests/xpcshell/test_breakpoint-25.js b/devtools/server/tests/xpcshell/test_breakpoint-25.js new file mode 100644 index 0000000000..f155234c96 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_breakpoint-25.js @@ -0,0 +1,79 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Ensure that the debugger resume page execution when the connection drops + * and when the target is detached. + */ + +add_task( + threadFrontTest(({ threadFront, debuggee, targetFront }) => { + return new Promise(resolve => { + (async () => { + await executeOnNextTickAndWaitForPause(evalCode, threadFront); + + ok(true, "The page is paused"); + ok(!debuggee.foo, "foo is still false after we hit the breakpoint"); + + await targetFront.detach(); + + // Closing the connection will force the thread actor to resume page + // execution + ok(debuggee.foo, "foo is true after target's detach request"); + + resolve(); + })(); + + function evalCode() { + /* eslint-disable */ + Cu.evalInSandbox("var foo = false;\n", debuggee); + /* eslint-enable */ + ok(!debuggee.foo, "foo is false at startup"); + + /* eslint-disable */ + Cu.evalInSandbox("debugger;\n" + "foo = true;\n", debuggee); + /* eslint-enable */ + } + }); + }) +); + +add_task( + threadFrontTest(({ threadFront, client, debuggee }) => { + return new Promise(resolve => { + (async () => { + await executeOnNextTickAndWaitForPause(evalCode, threadFront); + + ok(true, "The page is paused"); + ok(!debuggee.foo, "foo is still false after we hit the breakpoint"); + + await client.close(); + + // `close` will force the destruction of the thread actor, which, + // will resume the page execution. But all of that seems to be + // synchronous and we have to spin the event loop in order to ensure + // having the content javascript to execute the resumed code. + await new Promise(executeSoon); + + // Closing the connection will force the thread actor to resume page + // execution + ok(debuggee.foo, "foo is true after client close"); + executeSoon(resolve); + dump("resolved\n"); + })(); + + function evalCode() { + /* eslint-disable */ + Cu.evalInSandbox("var foo = false;\n", debuggee); + /* eslint-enable */ + ok(!debuggee.foo, "foo is false at startup"); + + /* eslint-disable */ + Cu.evalInSandbox("debugger;\n" + "foo = true;\n", debuggee); + /* eslint-enable */ + } + }); + }) +); diff --git a/devtools/server/tests/xpcshell/test_breakpoint-26.js b/devtools/server/tests/xpcshell/test_breakpoint-26.js new file mode 100644 index 0000000000..8624171252 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_breakpoint-26.js @@ -0,0 +1,63 @@ +/* eslint-disable max-nested-callbacks */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Bug 925269 - Verify that debugger statements are skipped + * if there is a falsey conditional breakpoint at the same location. + */ +add_task( + threadFrontTest(async props => { + await testBreakpointsAndDebuggerStatements(props); + }) +); + +async function testBreakpointsAndDebuggerStatements({ commands, threadFront }) { + commands.scriptCommand.execute( + `function foo(stop) { + debugger; + debugger; + debugger; + } + foo(); + //# sourceURL=http://example.com/testBreakpointsAndDebuggerStatements.js` + ); + + threadFront.setBreakpoint( + { + sourceUrl: "http://example.com/testBreakpointsAndDebuggerStatements.js", + line: 3, + column: 6, + }, + { condition: "false" } + ); + + await performActions(threadFront, [ + [ + "paused at first debugger statement", + { line: 2, type: "debuggerStatement" }, + "resume", + ], + [ + "pause at the third debugger statement", + { line: 4, type: "debuggerStatement" }, + "resume", + ], + ]); +} + +async function performActions(threadFront, actions) { + for (const action of actions) { + await performAction(threadFront, action); + } +} + +async function performAction(threadFront, [description, result, action]) { + info(description); + const packet = await waitForEvent(threadFront, "paused"); + Assert.equal(packet.frame.where.line, result.line); + Assert.equal(packet.why.type, result.type); + await threadFront[action](); +} diff --git a/devtools/server/tests/xpcshell/test_breakpoint-actor-map.js b/devtools/server/tests/xpcshell/test_breakpoint-actor-map.js new file mode 100644 index 0000000000..e45096095e --- /dev/null +++ b/devtools/server/tests/xpcshell/test_breakpoint-actor-map.js @@ -0,0 +1,257 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test the functionality of the BreakpointActorMap object. + +const { + BreakpointActorMap, +} = require("resource://devtools/server/actors/utils/breakpoint-actor-map.js"); + +function run_test() { + test_get_actor(); + test_set_actor(); + test_delete_actor(); + test_find_actors(); + test_duplicate_actors(); +} + +function test_get_actor() { + const bpStore = new BreakpointActorMap(); + const location = { + generatedSourceActor: { actor: "actor1" }, + generatedLine: 3, + }; + const columnLocation = { + generatedSourceActor: { actor: "actor2" }, + generatedLine: 5, + generatedColumn: 15, + }; + + // Shouldn't have breakpoint + Assert.equal( + null, + bpStore.getActor(location), + "Breakpoint not added and shouldn't exist." + ); + + bpStore.setActor(location, {}); + Assert.ok( + !!bpStore.getActor(location), + "Breakpoint added but not found in Breakpoint Store." + ); + + bpStore.deleteActor(location); + Assert.equal( + null, + bpStore.getActor(location), + "Breakpoint removed but still exists." + ); + + // Same checks for breakpoint with a column + Assert.equal( + null, + bpStore.getActor(columnLocation), + "Breakpoint with column not added and shouldn't exist." + ); + + bpStore.setActor(columnLocation, {}); + Assert.ok( + !!bpStore.getActor(columnLocation), + "Breakpoint with column added but not found in Breakpoint Store." + ); + + bpStore.deleteActor(columnLocation); + Assert.equal( + null, + bpStore.getActor(columnLocation), + "Breakpoint with column removed but still exists in Breakpoint Store." + ); +} + +function test_set_actor() { + // Breakpoint with column + const bpStore = new BreakpointActorMap(); + let location = { + generatedSourceActor: { actor: "actor1" }, + generatedLine: 10, + generatedColumn: 9, + }; + bpStore.setActor(location, {}); + Assert.ok( + !!bpStore.getActor(location), + "We should have the column breakpoint we just added" + ); + + // Breakpoint without column (whole line breakpoint) + location = { + generatedSourceActor: { actor: "actor2" }, + generatedLine: 103, + }; + bpStore.setActor(location, {}); + Assert.ok( + !!bpStore.getActor(location), + "We should have the whole line breakpoint we just added" + ); +} + +function test_delete_actor() { + // Breakpoint with column + const bpStore = new BreakpointActorMap(); + let location = { + generatedSourceActor: { actor: "actor1" }, + generatedLine: 10, + generatedColumn: 9, + }; + bpStore.setActor(location, {}); + bpStore.deleteActor(location); + Assert.equal( + bpStore.getActor(location), + null, + "We should not have the column breakpoint anymore" + ); + + // Breakpoint without column (whole line breakpoint) + location = { + generatedSourceActor: { actor: "actor2" }, + generatedLine: 103, + }; + bpStore.setActor(location, {}); + bpStore.deleteActor(location); + Assert.equal( + bpStore.getActor(location), + null, + "We should not have the whole line breakpoint anymore" + ); +} + +function test_find_actors() { + const bps = [ + { generatedSourceActor: { actor: "actor1" }, generatedLine: 10 }, + { + generatedSourceActor: { actor: "actor1" }, + generatedLine: 10, + generatedColumn: 3, + }, + { + generatedSourceActor: { actor: "actor1" }, + generatedLine: 10, + generatedColumn: 10, + }, + { + generatedSourceActor: { actor: "actor1" }, + generatedLine: 23, + generatedColumn: 89, + }, + { + generatedSourceActor: { actor: "actor2" }, + generatedLine: 10, + generatedColumn: 1, + }, + { + generatedSourceActor: { actor: "actor2" }, + generatedLine: 20, + generatedColumn: 5, + }, + { + generatedSourceActor: { actor: "actor2" }, + generatedLine: 30, + generatedColumn: 34, + }, + { + generatedSourceActor: { actor: "actor2" }, + generatedLine: 40, + generatedColumn: 56, + }, + ]; + + const bpStore = new BreakpointActorMap(); + + for (const bp of bps) { + bpStore.setActor(bp, bp); + } + + // All breakpoints + + let bpSet = new Set(bps); + for (const bp of bpStore.findActors()) { + bpSet.delete(bp); + } + Assert.equal(bpSet.size, 0, "Should be able to iterate over all breakpoints"); + + // Breakpoints by URL + + bpSet = new Set( + bps.filter(bp => { + return bp.generatedSourceActor.actorID === "actor1"; + }) + ); + for (const bp of bpStore.findActors({ + generatedSourceActor: { actorID: "actor1" }, + })) { + bpSet.delete(bp); + } + Assert.equal(bpSet.size, 0, "Should be able to filter the iteration by url"); + + // Breakpoints by URL and line + + bpSet = new Set( + bps.filter(bp => { + return ( + bp.generatedSourceActor.actorID === "actor1" && bp.generatedLine === 10 + ); + }) + ); + let first = true; + for (const bp of bpStore.findActors({ + generatedSourceActor: { actorID: "actor1" }, + generatedLine: 10, + })) { + if (first) { + Assert.equal( + bp.generatedColumn, + undefined, + "Should always get the whole line breakpoint first" + ); + first = false; + } else { + Assert.notEqual( + bp.generatedColumn, + undefined, + "Should not get the whole line breakpoint any time other than first." + ); + } + bpSet.delete(bp); + } + Assert.equal( + bpSet.size, + 0, + "Should be able to filter the iteration by url and line" + ); +} + +function test_duplicate_actors() { + const bpStore = new BreakpointActorMap(); + + // Breakpoint with column + let location = { + generatedSourceActor: { actorID: "foo-actor" }, + generatedLine: 10, + generatedColumn: 9, + }; + bpStore.setActor(location, {}); + bpStore.setActor(location, {}); + Assert.equal(bpStore.size, 1, "We should have only 1 column breakpoint"); + bpStore.deleteActor(location); + + // Breakpoint without column (whole line breakpoint) + location = { + generatedSourceActor: { actorID: "foo-actor" }, + generatedLine: 15, + }; + bpStore.setActor(location, {}); + bpStore.setActor(location, {}); + Assert.equal(bpStore.size, 1, "We should have only 1 whole line breakpoint"); + bpStore.deleteActor(location); +} diff --git a/devtools/server/tests/xpcshell/test_client_request.js b/devtools/server/tests/xpcshell/test_client_request.js new file mode 100644 index 0000000000..837bee5047 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_client_request.js @@ -0,0 +1,220 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test the DevToolsClient.request API. + +var gClient, gActorId; + +const { Actor } = require("resource://devtools/shared/protocol/Actor.js"); + +class TestActor extends Actor { + constructor(conn) { + super(conn, { typeName: "test", methods: [] }); + + this.requestTypes = { + hello: this.hello, + error: this.error, + }; + } + + hello() { + return { hello: "world" }; + } + + error() { + return { error: "code", message: "human message" }; + } +} + +function run_test() { + ActorRegistry.addGlobalActor( + { + constructorName: "TestActor", + constructorFun: TestActor, + }, + "test" + ); + + DevToolsServer.init(); + DevToolsServer.registerAllActors(); + + add_test(init); + add_test(test_client_request_promise); + add_test(test_client_request_promise_error); + add_test(test_client_request_event_emitter); + add_test(test_close_client_while_sending_requests); + add_test(test_client_request_after_close); + run_next_test(); +} + +function init() { + gClient = new DevToolsClient(DevToolsServer.connectPipe()); + gClient + .connect() + .then(() => gClient.mainRoot.rootForm) + .then(response => { + gActorId = response.test; + run_next_test(); + }); +} + +function checkStack(expectedName) { + let stack = Components.stack; + while (stack) { + info(stack.name); + if (stack.name == expectedName) { + // Reached back to outer function before request + ok(true, "Complete stack"); + return; + } + stack = stack.asyncCaller || stack.caller; + } + ok(false, "Incomplete stack"); +} + +function test_client_request_promise() { + // Test that DevToolsClient.request returns a promise that resolves on response + const request = gClient.request({ + to: gActorId, + type: "hello", + }); + + request.then(response => { + Assert.equal(response.from, gActorId); + Assert.equal(response.hello, "world"); + checkStack("test_client_request_promise/<"); + run_next_test(); + }); +} + +function test_client_request_promise_error() { + // Test that DevToolsClient.request returns a promise that reject when server + // returns an explicit error message + const request = gClient.request({ + to: gActorId, + type: "error", + }); + + request.then( + () => { + do_throw("Promise shouldn't be resolved on error"); + }, + response => { + Assert.equal(response.from, gActorId); + Assert.equal(response.error, "code"); + Assert.equal(response.message, "human message"); + checkStack("test_client_request_promise_error/<"); + run_next_test(); + } + ); +} + +function test_client_request_event_emitter() { + // Test that DevToolsClient.request returns also an EventEmitter object + const request = gClient.request({ + to: gActorId, + type: "hello", + }); + request.on("json-reply", reply => { + Assert.equal(reply.from, gActorId); + Assert.equal(reply.hello, "world"); + checkStack("test_client_request_event_emitter"); + run_next_test(); + }); +} + +function test_close_client_while_sending_requests() { + // First send a first request that will be "active" + // while the connection is closed. + // i.e. will be sent but no response received yet. + const activeRequest = gClient.request({ + to: gActorId, + type: "hello", + }); + + // Pile up a second one that will be "pending". + // i.e. won't event be sent. + const pendingRequest = gClient.request({ + to: gActorId, + type: "hello", + }); + + const expectReply = new Promise(resolve => { + gClient.expectReply("root", function (response) { + Assert.equal(response.error, "connectionClosed"); + Assert.equal( + response.message, + "server side packet can't be received as the connection just closed." + ); + resolve(); + }); + }); + + gClient.close().then(() => { + activeRequest + .then( + () => { + ok( + false, + "First request unexpectedly succeed while closing the connection" + ); + }, + response => { + Assert.equal(response.error, "connectionClosed"); + Assert.equal( + response.message, + "'hello' active request packet to '" + + gActorId + + "' can't be sent as the connection just closed." + ); + } + ) + .then(() => pendingRequest) + .then( + () => { + ok( + false, + "Second request unexpectedly succeed while closing the connection" + ); + }, + response => { + Assert.equal(response.error, "connectionClosed"); + Assert.equal( + response.message, + "'hello' pending request packet to '" + + gActorId + + "' can't be sent as the connection just closed." + ); + } + ) + .then(() => expectReply) + .then(run_next_test); + }); +} + +function test_client_request_after_close() { + // Test that DevToolsClient.request fails after we called client.close() + // (with promise API) + const request = gClient.request({ + to: gActorId, + type: "hello", + }); + + request.then( + response => { + ok(false, "Request succeed even after client.close"); + }, + response => { + ok(true, "Request failed after client.close"); + Assert.equal(response.error, "connectionClosed"); + ok( + response.message.match( + /'hello' request packet to '.*' can't be sent as the connection is closed./ + ) + ); + run_next_test(); + } + ); +} diff --git a/devtools/server/tests/xpcshell/test_conditional_breakpoint-01.js b/devtools/server/tests/xpcshell/test_conditional_breakpoint-01.js new file mode 100644 index 0000000000..8f2e58f651 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_conditional_breakpoint-01.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check conditional breakpoint when condition evaluates to true. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + let hitBreakpoint = false; + + const packet1 = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const source = await getSourceById(threadFront, packet1.frame.where.actor); + const location = { sourceUrl: source.url, line: 3 }; + threadFront.setBreakpoint(location, { condition: "a === 1" }); + + // Continue until the breakpoint is hit. + const packet2 = await resumeAndWaitForPause(threadFront); + + Assert.equal(hitBreakpoint, false); + hitBreakpoint = true; + + // Check the return value. + Assert.equal(packet2.why.type, "breakpoint"); + Assert.equal(packet2.frame.where.line, 3); + + // Remove the breakpoint. + await threadFront.removeBreakpoint(location); + + await threadFront.resume(); + + Assert.equal(hitBreakpoint, true); + }) +); + +function evalCode(debuggee) { + /* eslint-disable */ + Cu.evalInSandbox( + "debugger;\n" + // line 1 + "var a = 1;\n" + // line 2 + "var b = 2;\n", // line 3 + debuggee, + "1.8", + "test.js", + 1 + ); + /* eslint-enable */ +} diff --git a/devtools/server/tests/xpcshell/test_conditional_breakpoint-02.js b/devtools/server/tests/xpcshell/test_conditional_breakpoint-02.js new file mode 100644 index 0000000000..18742c4048 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_conditional_breakpoint-02.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check conditional breakpoint when condition evaluates to false. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet1 = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const source = await getSourceById(threadFront, packet1.frame.where.actor); + const location1 = { sourceUrl: source.url, line: 3 }; + threadFront.setBreakpoint(location1, { condition: "a === 2" }); + + const location2 = { sourceUrl: source.url, line: 4 }; + threadFront.setBreakpoint(location2, { condition: "a === 1" }); + + // Continue until the breakpoint is hit. + const packet2 = await resumeAndWaitForPause(threadFront); + + // Check the return value. + Assert.equal(packet2.why.type, "breakpoint"); + Assert.equal(packet2.frame.where.line, 4); + + // Remove the breakpoint. + await threadFront.removeBreakpoint(location2); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + /* eslint-disable */ + Cu.evalInSandbox( + "debugger;\n" + // line 1 + "var a = 1;\n" + // line 2 + "var b = 2;\n" + // line 3 + "b++;" + // line 4 + "debugger;", // line 5 + debuggee, + "1.8", + "test.js", + 1 + ); + /* eslint-enable */ +} diff --git a/devtools/server/tests/xpcshell/test_conditional_breakpoint-03.js b/devtools/server/tests/xpcshell/test_conditional_breakpoint-03.js new file mode 100644 index 0000000000..94ac46c307 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_conditional_breakpoint-03.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * If pauseOnExceptions is checked, when condition throws, + * make sure conditional breakpoint pauses but doesn't trigger an exception breakpoint. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee, commands }) => { + const packet1 = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const source = await getSourceById(threadFront, packet1.frame.where.actor); + + await commands.threadConfigurationCommand.updateConfiguration({ + pauseOnExceptions: true, + ignoreCaughtExceptions: false, + }); + const location = { sourceUrl: source.url, line: 3 }; + threadFront.setBreakpoint(location, { condition: "throw new Error()" }); + + // Continue until the breakpoint is hit. + const packet2 = await resumeAndWaitForPause(threadFront); + + // Check the return value. + Assert.equal(packet2.why.type, "breakpointConditionThrown"); + Assert.equal(packet2.frame.where.line, 3); + + // Remove the breakpoint. + await threadFront.removeBreakpoint(location); + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + /* eslint-disable */ + Cu.evalInSandbox( + "debugger;\n" + // line 1 + "var a = 1;\n" + // line 2 + "var b = 2;\n", // line 3 + debuggee, + "1.8", + "test.js", + 1 + ); + /* eslint-enable */ +} diff --git a/devtools/server/tests/xpcshell/test_conditional_breakpoint-04.js b/devtools/server/tests/xpcshell/test_conditional_breakpoint-04.js new file mode 100644 index 0000000000..b270b92974 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_conditional_breakpoint-04.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Confirm that conditional breakpoint are triggered in case of exceptions, + * even when pause-on-exceptions is disabled. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee, commands }) => { + await threadFront.setBreakpoint( + { sourceUrl: "conditional_breakpoint-04.js", line: 3 }, + { condition: "throw new Error()" } + ); + + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + Assert.equal(packet.frame.where.line, 1); + Assert.equal(packet.why.type, "debuggerStatement"); + + const pausedPacket = await resumeAndWaitForPause(threadFront); + Assert.equal(pausedPacket.frame.where.line, 3); + Assert.equal(pausedPacket.why.type, "breakpointConditionThrown"); + + const secondPausedPacket = await resumeAndWaitForPause(threadFront); + Assert.equal(secondPausedPacket.frame.where.line, 4); + Assert.equal(secondPausedPacket.why.type, "debuggerStatement"); + + // Remove the breakpoint. + await threadFront.removeBreakpoint({ + sourceUrl: "conditional_breakpoint-04.js", + line: 3, + }); + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + /* eslint-disable */ + Cu.evalInSandbox( + `debugger; + var a = 1; + var b = 2; + debugger;`, + debuggee, + "1.8", + "conditional_breakpoint-04.js", + 1 + ); + /* eslint-enable */ +} diff --git a/devtools/server/tests/xpcshell/test_connection_closes_all_pools.js b/devtools/server/tests/xpcshell/test_connection_closes_all_pools.js new file mode 100644 index 0000000000..d69291485d --- /dev/null +++ b/devtools/server/tests/xpcshell/test_connection_closes_all_pools.js @@ -0,0 +1,100 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { Pool } = require("resource://devtools/shared/protocol/Pool.js"); +const { + DevToolsServerConnection, +} = require("resource://devtools/server/devtools-server-connection.js"); +const { + LocalDebuggerTransport, +} = require("resource://devtools/shared/transport/local-transport.js"); + +// Helper class to assert how many times a Pool was destroyed +class FakeActor extends Pool { + constructor(...args) { + super(...args); + this.destroyedCount = 0; + } + + destroy() { + this.destroyedCount++; + super.destroy(); + } +} + +add_task(async function () { + const transport = new LocalDebuggerTransport(); + const conn = new DevToolsServerConnection("prefix", transport); + + // Setup a flat pool hierarchy with multiple pools: + // + // - pool1 + // | + // \- actor1 + // + // - pool2 + // | + // |- actor2a + // | + // \- actor2b + // + // From the point of view of the DevToolsServerConnection, the only pools + // registered in _extraPools should be pool1 and pool2. Even though actor1, + // actor2a and actor2b extend Pool, they don't manage other pools. + const actor1 = new FakeActor(conn); + const pool1 = new Pool(conn, "pool-1"); + pool1.manage(actor1); + + const actor2a = new FakeActor(conn); + const actor2b = new FakeActor(conn); + const pool2 = new Pool(conn, "pool-2"); + pool2.manage(actor2a); + pool2.manage(actor2b); + + ok(!!actor1.actorID, "actor1 has a valid actorID"); + ok(!!actor2a.actorID, "actor2a has a valid actorID"); + ok(!!actor2b.actorID, "actor2b has a valid actorID"); + + conn.close(); + + equal(actor1.destroyedCount, 1, "actor1 was successfully destroyed"); + equal(actor2a.destroyedCount, 1, "actor2 was successfully destroyed"); + equal(actor2b.destroyedCount, 1, "actor2 was successfully destroyed"); +}); + +add_task(async function () { + const transport = new LocalDebuggerTransport(); + const conn = new DevToolsServerConnection("prefix", transport); + + // Setup a nested pool hierarchy: + // + // - pool + // | + // \- parentActor + // | + // \- childActor + // + // Since parentActor is also a Pool from the point of view of the + // DevToolsServerConnection, it will attempt to destroy it when looping on + // this._extraPools. But since `parentActor` is also a direct child of `pool`, + // it has already been destroyed by the Pool destroy() mechanism. + // + // Here we check that we don't call destroy() too many times on a single Pool. + // Even though Pool::destroy() is stable when called multiple times, we can't + // guarantee the same for classes inheriting Pool. + const childActor = new FakeActor(conn); + const parentActor = new FakeActor(conn); + const pool = new Pool(conn, "pool"); + pool.manage(parentActor); + parentActor.manage(childActor); + + ok(!!parentActor.actorID, "customActor has a valid actorID"); + ok(!!childActor.actorID, "childActor has a valid actorID"); + + conn.close(); + + equal(parentActor.destroyedCount, 1, "parentActor was destroyed once"); + equal(parentActor.destroyedCount, 1, "customActor was destroyed once"); +}); diff --git a/devtools/server/tests/xpcshell/test_console_eval-01.js b/devtools/server/tests/xpcshell/test_console_eval-01.js new file mode 100644 index 0000000000..abb6ddc605 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_console_eval-01.js @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* + * Check that is possible to evaluate JS with evaluation timeouts in place. + */ + +add_task( + threadFrontTest(async ({ commands }) => { + await commands.scriptCommand.execute(` + function fib(n) { + if (n == 1 || n == 0) { + return 1; + } + + return fib(n-1) + fib(n-2) + } + `); + + const normalResult = await commands.scriptCommand.execute("fib(1)", { + eager: true, + }); + Assert.equal(normalResult.result, 1, "normal eval"); + + const timeoutResult = await commands.scriptCommand.execute("fib(100)", { + eager: true, + }); + Assert.equal(typeof timeoutResult.result, "object", "timeout eval"); + Assert.equal(timeoutResult.result.type, "undefined", "timeout eval type"); + }) +); diff --git a/devtools/server/tests/xpcshell/test_console_eval-02.js b/devtools/server/tests/xpcshell/test_console_eval-02.js new file mode 100644 index 0000000000..11b3d130b4 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_console_eval-02.js @@ -0,0 +1,22 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* + * Check that bound functions can be eagerly evaluated. + */ + +add_task( + threadFrontTest(async ({ commands }) => { + await commands.scriptCommand.execute(` + var obj = [1, 2, 3]; + var fn = obj.includes.bind(obj, 2); + `); + + const normalResult = await commands.scriptCommand.execute("fn()", { + eager: true, + }); + Assert.equal(normalResult.result, true, "normal eval"); + }) +); diff --git a/devtools/server/tests/xpcshell/test_dbgactor.js b/devtools/server/tests/xpcshell/test_dbgactor.js new file mode 100644 index 0000000000..cb0cf8f7d7 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_dbgactor.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const xpcInspector = Cc["@mozilla.org/jsinspector;1"].getService( + Ci.nsIJSInspector +); + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + Assert.equal(xpcInspector.eventLoopNestLevel, 0); + + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + Assert.equal(false, "error" in packet); + Assert.ok("actor" in packet); + Assert.ok("why" in packet); + Assert.equal(packet.why.type, "debuggerStatement"); + + // Reach around the protocol to check that the debuggee is in the state + // we expect. + Assert.ok(debuggee.a); + Assert.ok(!debuggee.b); + + Assert.equal(xpcInspector.eventLoopNestLevel, 1); + + // Let the debuggee continue execution. + await threadFront.resume(); + + // Now make sure that we've run the code after the debugger statement... + Assert.ok(debuggee.b); + + Assert.equal(xpcInspector.eventLoopNestLevel, 0); + }) +); + +function evalCode(debuggee) { + Cu.evalInSandbox( + "var a = true; var b = false; debugger; var b = true;", + debuggee + ); +} diff --git a/devtools/server/tests/xpcshell/test_dbgclient_debuggerstatement.js b/devtools/server/tests/xpcshell/test_dbgclient_debuggerstatement.js new file mode 100644 index 0000000000..254f582460 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_dbgclient_debuggerstatement.js @@ -0,0 +1,39 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const xpcInspector = Cc["@mozilla.org/jsinspector;1"].getService( + Ci.nsIJSInspector +); + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + Assert.equal(threadFront.state, "paused"); + // Reach around the protocol to check that the debuggee is in the state + // we expect. + Assert.ok(debuggee.a); + Assert.ok(!debuggee.b); + + Assert.equal(xpcInspector.eventLoopNestLevel, 1); + + await threadFront.resume(); + + // Now make sure that we've run the code after the debugger statement... + Assert.ok(debuggee.b); + + Assert.equal(xpcInspector.eventLoopNestLevel, 0); + }) +); + +function evalCode(debuggee) { + Cu.evalInSandbox( + "var a = true; var b = false; debugger; var b = true;", + debuggee + ); +} diff --git a/devtools/server/tests/xpcshell/test_dbgglobal.js b/devtools/server/tests/xpcshell/test_dbgglobal.js new file mode 100644 index 0000000000..407e270da4 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_dbgglobal.js @@ -0,0 +1,86 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { + SocketListener, +} = require("resource://devtools/shared/security/socket.js"); + +function run_test() { + // Should get an exception if we try to interact with DevToolsServer + // before we initialize it... + const socketListener = new SocketListener(DevToolsServer, {}); + Assert.throws( + () => DevToolsServer.addSocketListener(socketListener), + /DevToolsServer has not been initialized/, + "addSocketListener should throw before it has been initialized" + ); + Assert.throws( + DevToolsServer.closeAllSocketListeners, + /this is undefined/, + "closeAllSocketListeners should throw before it has been initialized" + ); + Assert.throws( + DevToolsServer.connectPipe, + /this is undefined/, + "connectPipe should throw before it has been initialized" + ); + + // Allow incoming connections. + DevToolsServer.init(); + + // These should still fail because we haven't added a createRootActor + // implementation yet. + Assert.throws( + DevToolsServer.closeAllSocketListeners, + /this is undefined/, + "closeAllSocketListeners should throw if createRootActor hasn't been added" + ); + Assert.throws( + DevToolsServer.connectPipe, + /this is undefined/, + "closeAllSocketListeners should throw if createRootActor hasn't been added" + ); + + const { createRootActor } = require("xpcshell-test/testactors"); + DevToolsServer.setRootActor(createRootActor); + + // Now they should work. + DevToolsServer.addSocketListener(socketListener); + DevToolsServer.closeAllSocketListeners(); + + // Make sure we got the test's root actor all set up. + const client1 = DevToolsServer.connectPipe(); + client1.hooks = { + onPacket(packet1) { + Assert.equal(packet1.from, "root"); + Assert.equal(packet1.applicationType, "xpcshell-tests"); + + // Spin up a second connection, make sure it has its own root + // actor. + const client2 = DevToolsServer.connectPipe(); + client2.hooks = { + onPacket(packet2) { + Assert.equal(packet2.from, "root"); + Assert.notEqual( + packet1.testConnectionPrefix, + packet2.testConnectionPrefix + ); + client2.close(); + }, + onTransportClosed(result) { + client1.close(); + }, + }; + client2.ready(); + }, + + onTransportClosed(result) { + do_test_finished(); + }, + }; + + client1.ready(); + do_test_pending(); +} diff --git a/devtools/server/tests/xpcshell/test_extension_storage_actor.js b/devtools/server/tests/xpcshell/test_extension_storage_actor.js new file mode 100644 index 0000000000..9816854cf8 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_extension_storage_actor.js @@ -0,0 +1,1155 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* globals browser */ + +"use strict"; + +const { ExtensionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/ExtensionXPCShellUtils.sys.mjs" +); + +const { TestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" +); + +const { + createMissingIndexedDBDirs, + extensionScriptWithMessageListener, + ext_no_bg, + getExtensionConfig, + openAddonStoragePanel, + shutdown, + startupExtension, +} = require("resource://test/webextension-helpers.js"); + +const l10n = new Localization(["devtools/client/storage.ftl"], true); +const sessionString = l10n.formatValueSync("storage-expires-session"); + +// Ignore rejection related to the storage.onChanged listener being removed while the extension context is being closed. +const { PromiseTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PromiseTestUtils.sys.mjs" +); +PromiseTestUtils.allowMatchingRejectionsGlobally( + /Message manager disconnected/ +); +PromiseTestUtils.allowMatchingRejectionsGlobally( + /sendRemoveListener on closed conduit/ +); + +const { createAppInfo, promiseStartupManager } = AddonTestUtils; + +const LEAVE_UUID_PREF = "extensions.webextensions.keepUuidOnUninstall"; +const LEAVE_STORAGE_PREF = "extensions.webextensions.keepStorageOnUninstall"; + +AddonTestUtils.init(this); +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42"); + +ExtensionTestUtils.init(this); + +add_setup(async function setup() { + await promiseStartupManager(); + const dir = createMissingIndexedDBDirs(); + + Assert.ok( + dir.exists(), + "Should have a 'storage/permanent' dir in the profile dir" + ); +}); + +add_task(async function test_extension_store_exists() { + const extension = await startupExtension(getExtensionConfig()); + + const { commands, extensionStorage } = await openAddonStoragePanel( + extension.id + ); + + ok(extensionStorage, "Should have an extensionStorage store"); + + await shutdown(extension, commands); +}); + +add_task( + { + // This test currently fails if the extension runs in the main process + // like in Thunderbird (see bug 1575183 comment #15 for details). + skip_if: () => !WebExtensionPolicy.useRemoteWebExtensions, + }, + async function test_extension_origin_matches_debugger_target() { + async function background() { + // window is available in background scripts + // eslint-disable-next-line no-undef + browser.test.sendMessage("extension-origin", window.location.origin); + } + + const extension = await startupExtension( + getExtensionConfig({ background }) + ); + + const { commands, extensionStorage } = await openAddonStoragePanel( + extension.id + ); + + const { hosts } = extensionStorage; + const expectedHost = await extension.awaitMessage("extension-origin"); + ok( + expectedHost in hosts, + "Should have the expected extension host in the extensionStorage store" + ); + + await shutdown(extension, commands); + } +); + +/** + * Test case: Background page modifies items while storage panel is open. + * - Load extension with background page. + * - Open the add-on debugger storage panel. + * - With the panel still open, from the extension background page: + * - Bulk add storage items + * - Edit the values of some of the storage items + * - Remove some storage items + * - Clear all storage items + * - For each modification, the storage data in the panel should match the + * changes made by the extension. + */ +add_task(async function test_panel_live_updates() { + const extension = await startupExtension( + getExtensionConfig({ background: extensionScriptWithMessageListener }) + ); + + const { commands, extensionStorage } = await openAddonStoragePanel( + extension.id + ); + + const host = await extension.awaitMessage("extension-origin"); + + let { data } = await extensionStorage.getStoreObjects(host); + Assert.deepEqual(data, [], "Got the expected results on empty storage.local"); + + info("Waiting for extension to bulk add 50 items to storage local"); + const bulkStorageItems = {}; + // limited by MAX_STORE_OBJECT_COUNT in devtools/server/actors/resources/storage/index.js + const numItems = 2; + for (let i = 1; i <= numItems; i++) { + bulkStorageItems[i] = i; + } + + // fireOnChanged avoids the race condition where the extension + // modifies storage then immediately tries to access storage before + // the storage actor has finished updating. + extension.sendMessage("storage-local-fireOnChanged"); + extension.sendMessage("storage-local-set", { + ...bulkStorageItems, + a: 123, + b: [4, 5], + c: { d: 678 }, + d: true, + e: "hi", + f: null, + }); + await extension.awaitMessage("storage-local-set:done"); + await extension.awaitMessage("storage-local-onChanged"); + + info( + "Confirming items added by extension match items in extensionStorage store" + ); + const bulkStorageObjects = []; + for (const [name, value] of Object.entries(bulkStorageItems)) { + bulkStorageObjects.push({ + area: "local", + name, + value: { str: String(value) }, + isValueEditable: true, + }); + } + data = (await extensionStorage.getStoreObjects(host, null, { sessionString })) + .data; + Assert.deepEqual( + data, + [ + ...bulkStorageObjects, + { + area: "local", + name: "a", + value: { str: "123" }, + isValueEditable: true, + }, + { + area: "local", + name: "b", + value: { str: "[4,5]" }, + isValueEditable: true, + }, + { + area: "local", + name: "c", + value: { str: '{"d":678}' }, + isValueEditable: true, + }, + { + area: "local", + name: "d", + value: { str: "true" }, + isValueEditable: true, + }, + { + area: "local", + name: "e", + value: { str: "hi" }, + isValueEditable: true, + }, + { + area: "local", + name: "f", + value: { str: "null" }, + isValueEditable: true, + }, + ], + "Got the expected results on populated storage.local" + ); + + info("Waiting for extension to edit a few storage item values"); + extension.sendMessage("storage-local-fireOnChanged"); + extension.sendMessage("storage-local-set", { + a: ["c", "d"], + b: 456, + c: false, + }); + await extension.awaitMessage("storage-local-set:done"); + await extension.awaitMessage("storage-local-onChanged"); + + info( + "Confirming items edited by extension match items in extensionStorage store" + ); + data = (await extensionStorage.getStoreObjects(host, null, { sessionString })) + .data; + Assert.deepEqual( + data, + [ + ...bulkStorageObjects, + { + area: "local", + name: "a", + value: { str: '["c","d"]' }, + isValueEditable: true, + }, + { + area: "local", + name: "b", + value: { str: "456" }, + isValueEditable: true, + }, + { + area: "local", + name: "c", + value: { str: "false" }, + isValueEditable: true, + }, + { + area: "local", + name: "d", + value: { str: "true" }, + isValueEditable: true, + }, + { + area: "local", + name: "e", + value: { str: "hi" }, + isValueEditable: true, + }, + { + area: "local", + name: "f", + value: { str: "null" }, + isValueEditable: true, + }, + ], + "Got the expected results on populated storage.local" + ); + + info("Waiting for extension to remove a few storage item values"); + extension.sendMessage("storage-local-fireOnChanged"); + extension.sendMessage("storage-local-remove", ["d", "e", "f"]); + await extension.awaitMessage("storage-local-remove:done"); + await extension.awaitMessage("storage-local-onChanged"); + + info( + "Confirming items removed by extension were removed in extensionStorage store" + ); + data = (await extensionStorage.getStoreObjects(host, null, { sessionString })) + .data; + Assert.deepEqual( + data, + [ + ...bulkStorageObjects, + { + area: "local", + name: "a", + value: { str: '["c","d"]' }, + isValueEditable: true, + }, + { + area: "local", + name: "b", + value: { str: "456" }, + isValueEditable: true, + }, + { + area: "local", + name: "c", + value: { str: "false" }, + isValueEditable: true, + }, + ], + "Got the expected results on populated storage.local" + ); + + info("Waiting for extension to remove all remaining storage items"); + extension.sendMessage("storage-local-fireOnChanged"); + extension.sendMessage("storage-local-clear"); + await extension.awaitMessage("storage-local-clear:done"); + await extension.awaitMessage("storage-local-onChanged"); + + info("Confirming extensionStorage store was cleared"); + data = (await extensionStorage.getStoreObjects(host)).data; + Assert.deepEqual( + data, + [], + "Got the expected results on populated storage.local" + ); + + await shutdown(extension, commands); +}); + +/** + * Test case: No bg page. Transient page adds item before storage panel opened. + * - Load extension with no background page. + * - Open an extension page in a tab that adds a local storage item. + * - With the extension page still open, open the add-on storage panel. + * - The data in the storage panel should match the items added by the extension. + */ +add_task( + async function test_panel_data_matches_extension_with_transient_page_open() { + const extension = await startupExtension( + getExtensionConfig({ files: ext_no_bg.files }) + ); + + const url = extension.extension.baseURI.resolve( + "extension_page_in_tab.html" + ); + const contentPage = await ExtensionTestUtils.loadContentPage(url, { + extension, + }); + + const host = await extension.awaitMessage("extension-origin"); + + extension.sendMessage("storage-local-set", { a: 123 }); + await extension.awaitMessage("storage-local-set:done"); + + const { commands, extensionStorage } = await openAddonStoragePanel( + extension.id + ); + + const { data } = await extensionStorage.getStoreObjects(host); + Assert.deepEqual( + data, + [ + { + area: "local", + name: "a", + value: { str: "123" }, + isValueEditable: true, + }, + ], + "Got the expected results on populated storage.local" + ); + + await contentPage.close(); + await shutdown(extension, commands); + } +); + +/** + * Test case: No bg page. Transient page adds item then closes before storage panel opened. + * - Load extension with no background page. + * - Open an extension page in a tab that adds a local storage item. + * - Close all extension pages. + * - Open the add-on storage panel. + * - The data in the storage panel should match the item added by the extension. + */ +add_task(async function test_panel_data_matches_extension_with_no_pages_open() { + const extension = await startupExtension( + getExtensionConfig({ files: ext_no_bg.files }) + ); + + const url = extension.extension.baseURI.resolve("extension_page_in_tab.html"); + const contentPage = await ExtensionTestUtils.loadContentPage(url, { + extension, + }); + + const host = await extension.awaitMessage("extension-origin"); + + extension.sendMessage("storage-local-fireOnChanged"); + extension.sendMessage("storage-local-set", { a: 123 }); + await extension.awaitMessage("storage-local-set:done"); + await extension.awaitMessage("storage-local-onChanged"); + await contentPage.close(); + + const { commands, extensionStorage } = await openAddonStoragePanel( + extension.id + ); + + const { data } = await extensionStorage.getStoreObjects(host); + Assert.deepEqual( + data, + [ + { + area: "local", + name: "a", + value: { str: "123" }, + isValueEditable: true, + }, + ], + "Got the expected results on populated storage.local" + ); + + await shutdown(extension, commands); +}); + +/** + * Test case: No bg page. Storage panel live updates when a transient page adds an item. + * - Load extension with no background page. + * - Open the add-on storage panel. + * - With the storage panel still open, open an extension page in a new tab that adds an + * item. + * - The data in the storage panel should live update to match the item added by the + * extension. + * - If an extension page adds the same data again, the data in the storage panel should + * not change. + */ +add_task( + async function test_panel_data_live_updates_for_extension_without_bg_page() { + const extension = await startupExtension( + getExtensionConfig({ files: ext_no_bg.files }) + ); + + const { commands, extensionStorage } = await openAddonStoragePanel( + extension.id + ); + + const url = extension.extension.baseURI.resolve( + "extension_page_in_tab.html" + ); + const contentPage = await ExtensionTestUtils.loadContentPage(url, { + extension, + }); + + const host = await extension.awaitMessage("extension-origin"); + + let { data } = await extensionStorage.getStoreObjects(host); + Assert.deepEqual( + data, + [], + "Got the expected results on empty storage.local" + ); + + extension.sendMessage("storage-local-fireOnChanged"); + extension.sendMessage("storage-local-set", { a: 123 }); + await extension.awaitMessage("storage-local-set:done"); + await extension.awaitMessage("storage-local-onChanged"); + + data = (await extensionStorage.getStoreObjects(host)).data; + Assert.deepEqual( + data, + [ + { + area: "local", + name: "a", + value: { str: "123" }, + isValueEditable: true, + }, + ], + "Got the expected results on populated storage.local" + ); + + extension.sendMessage("storage-local-fireOnChanged"); + extension.sendMessage("storage-local-set", { a: 123 }); + await extension.awaitMessage("storage-local-set:done"); + await extension.awaitMessage("storage-local-onChanged"); + + data = (await extensionStorage.getStoreObjects(host)).data; + Assert.deepEqual( + data, + [ + { + area: "local", + name: "a", + value: { str: "123" }, + isValueEditable: true, + }, + ], + "The results are unchanged when an extension page adds duplicate items" + ); + + await contentPage.close(); + await shutdown(extension, commands); + } +); + +/** + * Test case: Bg page adds item while storage panel is open. Panel edits item's value. + * - Load extension with background page. + * - Open the add-on storage panel. + * - With the storage panel still open, add item from the background page. + * - Edit the value of the item in the storage panel + * - The data in the storage panel should match the item added by the extension. + * - The storage actor is correctly parsing and setting the string representation of + * the value in the storage local database when the item's value is edited in the + * storage panel + */ +add_task( + async function test_editing_items_in_panel_parses_supported_values_correctly() { + const extension = await startupExtension( + getExtensionConfig({ background: extensionScriptWithMessageListener }) + ); + + const host = await extension.awaitMessage("extension-origin"); + + const { commands, extensionStorage } = await openAddonStoragePanel( + extension.id + ); + + const oldItem = { a: 123 }; + const key = Object.keys(oldItem)[0]; + const oldValue = oldItem[key]; + // A tuple representing information for a new value entered into the panel for oldItem: + // [ + // value, + // editItem string representation of value, + // toStoreObject string representation of value, + // ] + const valueInfo = [ + [true, "true", "true"], + ["hi", "hi", "hi"], + [456, "456", "456"], + [{ b: 789 }, "{b: 789}", '{"b":789}'], + [[1, 2, 3], "[1, 2, 3]", "[1,2,3]"], + [null, "null", "null"], + ]; + for (const [value, editItemValueStr, toStoreObjectValueStr] of valueInfo) { + info("Setting a storage item through the extension"); + extension.sendMessage("storage-local-fireOnChanged"); + extension.sendMessage("storage-local-set", oldItem); + await extension.awaitMessage("storage-local-set:done"); + await extension.awaitMessage("storage-local-onChanged"); + + info( + "Editing the storage item in the panel with a new value of a different type" + ); + // When the user edits an item in the panel, they are entering a string into a + // textbox. This string is parsed by the storage actor's editItem method. + await extensionStorage.editItem({ + host, + field: "value", + items: { name: key, value: editItemValueStr }, + oldValue, + }); + + info( + "Verifying item in the storage actor matches the item edited in the panel" + ); + const { data } = await extensionStorage.getStoreObjects(host); + Assert.deepEqual( + data, + [ + { + area: "local", + name: key, + value: { str: toStoreObjectValueStr }, + isValueEditable: true, + }, + ], + "Got the expected results on populated storage.local" + ); + + // The view layer is separate from the database layer; therefore while values are + // stringified (via toStoreObject) for display in the client, the value (and its type) + // in the database is unchanged. + info( + "Verifying the expected new value matches the value fetched in the extension" + ); + extension.sendMessage("storage-local-get", key); + const extItem = await extension.awaitMessage("storage-local-get:done"); + Assert.deepEqual( + value, + extItem[key], + `The string value ${editItemValueStr} was correctly parsed to ${value}` + ); + } + + await shutdown(extension, commands); + } +); + +/** + * Test case: Modifying storage items from the panel update extension storage local data. + * - Load extension with background page. + * - Open the add-on storage panel. From the panel: + * - Edit the value of a storage item, + * - Remove a storage item, + * - Remove all of the storage items, + * - For each modification, the storage data retrieved by the extension should match the + * data in the panel. + */ +add_task( + async function test_modifying_items_in_panel_updates_extension_storage_data() { + const extension = await startupExtension( + getExtensionConfig({ background: extensionScriptWithMessageListener }) + ); + + const host = await extension.awaitMessage("extension-origin"); + + const { commands, extensionStorage } = await openAddonStoragePanel( + extension.id + ); + + const DEFAULT_VALUE = "value"; // global in devtools/server/actors/resources/storage/index.js + let items = { + guid_1: DEFAULT_VALUE, + guid_2: DEFAULT_VALUE, + guid_3: DEFAULT_VALUE, + }; + + info("Adding storage items from the extension"); + let storesUpdate = extensionStorage.once("single-store-update"); + extension.sendMessage("storage-local-set", items); + await extension.awaitMessage("storage-local-set:done"); + + info("Waiting for the storage actor to emit a 'stores-update' event"); + let data = await storesUpdate; + Assert.deepEqual( + { + added: { + extensionStorage: { + [host]: ["guid_1", "guid_2", "guid_3"], + }, + }, + changed: undefined, + deleted: undefined, + }, + data, + "The change data from the storage actor's 'stores-update' event matches the changes made in the client." + ); + + info("Waiting for panel to edit some items"); + storesUpdate = extensionStorage.once("single-store-update"); + await extensionStorage.editItem({ + host, + field: "value", + items: { name: "guid_1", value: "anotherValue" }, + DEFAULT_VALUE, + }); + + info("Waiting for the storage actor to emit a 'stores-update' event"); + data = await storesUpdate; + Assert.deepEqual( + { + added: undefined, + changed: { + extensionStorage: { + [host]: ["guid_1"], + }, + }, + deleted: undefined, + }, + data, + "The change data from the storage actor's 'stores-update' event matches the changes made in the client." + ); + + items = { + guid_1: "anotherValue", + guid_2: DEFAULT_VALUE, + guid_3: DEFAULT_VALUE, + }; + extension.sendMessage("storage-local-get", Object.keys(items)); + let extItems = await extension.awaitMessage("storage-local-get:done"); + Assert.deepEqual( + items, + extItems, + `The storage items in the extension match the items in the panel` + ); + + info("Waiting for panel to remove an item"); + storesUpdate = extensionStorage.once("single-store-update"); + await extensionStorage.removeItem(host, "guid_3"); + + info("Waiting for the storage actor to emit a 'stores-update' event"); + data = await storesUpdate; + Assert.deepEqual( + { + added: undefined, + changed: undefined, + deleted: { + extensionStorage: { + [host]: ["guid_3"], + }, + }, + }, + data, + "The change data from the storage actor's 'stores-update' event matches the changes made in the client." + ); + + items = { + guid_1: "anotherValue", + guid_2: DEFAULT_VALUE, + }; + extension.sendMessage("storage-local-get", Object.keys(items)); + extItems = await extension.awaitMessage("storage-local-get:done"); + Assert.deepEqual( + items, + extItems, + `The storage items in the extension match the items in the panel` + ); + + info("Waiting for panel to remove all items"); + const storesCleared = extensionStorage.once("single-store-cleared"); + await extensionStorage.removeAll(host); + + info("Waiting for the storage actor to emit a 'stores-cleared' event"); + data = await storesCleared; + Assert.deepEqual( + { + clearedHostsOrPaths: { + [host]: [], + }, + }, + data, + "The change data from the storage actor's 'stores-cleared' event matches the changes made in the client." + ); + + items = {}; + extension.sendMessage("storage-local-get", Object.keys(items)); + extItems = await extension.awaitMessage("storage-local-get:done"); + Assert.deepEqual( + items, + extItems, + `The storage items in the extension match the items in the panel` + ); + + await shutdown(extension, commands); + } +); + +/** + * Test case: Storage panel shows extension storage data added prior to extension startup + * - Load extension that adds a storage item + * - Uninstall the extension + * - Reinstall the extension + * - Open the add-on storage panel. + * - The data in the storage panel should match the data added the first time the extension + * was installed + * Related test case: Storage panel shows extension storage data when an extension that has + * already migrated to the IndexedDB storage backend prior to extension startup adds + * another storage item. + * - (Building from previous steps) + * - The reinstalled extension adds a storage item + * - The data in the storage panel should live update with both items: the item added from + * the first and the item added from the reinstall. + */ +add_task( + async function test_panel_data_matches_data_added_prior_to_ext_startup() { + // The pref to leave the addonid->uuid mapping around after uninstall so that we can + // re-attach to the same storage + Services.prefs.setBoolPref(LEAVE_UUID_PREF, true); + + // The pref to prevent cleaning up storage on uninstall + Services.prefs.setBoolPref(LEAVE_STORAGE_PREF, true); + + let extension = await startupExtension( + getExtensionConfig({ background: extensionScriptWithMessageListener }) + ); + + const host = await extension.awaitMessage("extension-origin"); + + extension.sendMessage("storage-local-set", { a: 123 }); + await extension.awaitMessage("storage-local-set:done"); + + await shutdown(extension); + + // Reinstall the same extension + extension = await startupExtension( + getExtensionConfig({ background: extensionScriptWithMessageListener }) + ); + + await extension.awaitMessage("extension-origin"); + + const { commands, extensionStorage } = await openAddonStoragePanel( + extension.id + ); + + let { data } = await extensionStorage.getStoreObjects(host); + Assert.deepEqual( + data, + [ + { + area: "local", + name: "a", + value: { str: "123" }, + isValueEditable: true, + }, + ], + "Got the expected results on populated storage.local" + ); + + // Related test case + extension.sendMessage("storage-local-fireOnChanged"); + extension.sendMessage("storage-local-set", { b: 456 }); + await extension.awaitMessage("storage-local-set:done"); + await extension.awaitMessage("storage-local-onChanged"); + + data = ( + await extensionStorage.getStoreObjects(host, null, { sessionString }) + ).data; + Assert.deepEqual( + data, + [ + { + area: "local", + name: "a", + value: { str: "123" }, + isValueEditable: true, + }, + { + area: "local", + name: "b", + value: { str: "456" }, + isValueEditable: true, + }, + ], + "Got the expected results on populated storage.local" + ); + + Services.prefs.setBoolPref(LEAVE_STORAGE_PREF, false); + Services.prefs.setBoolPref(LEAVE_UUID_PREF, false); + + await shutdown(extension, commands); + } +); + +add_task( + function cleanup_for_test_panel_data_matches_data_added_prior_to_ext_startup() { + Services.prefs.clearUserPref(LEAVE_UUID_PREF); + Services.prefs.clearUserPref(LEAVE_STORAGE_PREF); + } +); + +/** + * Test case: Transient page adds an item to storage. With storage panel open, + * reload extension. + * - Load extension with no background page. + * - Open transient page that adds a storage item on message. + * - Open the add-on storage panel. + * - With the storage panel still open, reload the extension. + * - The data in the storage panel should match the item added prior to reloading. + */ +add_task(async function test_panel_live_reload_for_extension_without_bg_page() { + const EXTENSION_ID = "test_local_storage_live_reload@xpcshell.mozilla.org"; + let manifest = { + version: "1.0", + browser_specific_settings: { + gecko: { + id: EXTENSION_ID, + }, + }, + }; + + info("Loading and starting extension version 1.0"); + const extension = await startupExtension( + getExtensionConfig({ + manifest, + files: ext_no_bg.files, + }) + ); + + info("Opening extension page in a tab"); + const url = extension.extension.baseURI.resolve("extension_page_in_tab.html"); + const contentPage = await ExtensionTestUtils.loadContentPage(url, { + extension, + }); + + const host = await extension.awaitMessage("extension-origin"); + + info("Waiting for extension page in a tab to add storage item"); + extension.sendMessage("storage-local-fireOnChanged"); + extension.sendMessage("storage-local-set", { a: 123 }); + await extension.awaitMessage("storage-local-set:done"); + await extension.awaitMessage("storage-local-onChanged"); + await contentPage.close(); + + info("Opening storage panel"); + const { commands, extensionStorage } = await openAddonStoragePanel( + extension.id + ); + + manifest = { + ...manifest, + version: "2.0", + }; + // "Reload" is most similar to an upgrade, as e.g. storage data is preserved + info("Updating extension to version 2.0"); + await extension.upgrade( + getExtensionConfig({ + manifest, + files: ext_no_bg.files, + }) + ); + + const { data } = await extensionStorage.getStoreObjects(host); + Assert.deepEqual( + data, + [ + { + area: "local", + name: "a", + value: { str: "123" }, + isValueEditable: true, + }, + ], + "Got the expected results on populated storage.local" + ); + + await shutdown(extension, commands); +}); + +/** + * Test case: Bg page auto adds item(s). With storage panel open, reload extension. + * - Load extension with background page that automatically adds a storage item on startup. + * - Open the add-on storage panel. + * - With the storage panel still open, reload the extension. + * - The data in the storage panel should match the item(s) added by the reloaded + * extension. + */ +add_task( + async function test_panel_live_reload_when_extension_auto_adds_items() { + async function background() { + await browser.storage.local.set({ a: { b: 123 }, c: { d: 456 } }); + // window is available in background scripts + // eslint-disable-next-line no-undef + browser.test.sendMessage("extension-origin", window.location.origin); + } + const EXTENSION_ID = "test_local_storage_live_reload@xpcshell.mozilla.org"; + let manifest = { + version: "1.0", + browser_specific_settings: { + gecko: { + id: EXTENSION_ID, + }, + }, + }; + + info("Loading and starting extension version 1.0"); + const extension = await startupExtension( + getExtensionConfig({ manifest, background }) + ); + + info("Waiting for message from test extension"); + const host = await extension.awaitMessage("extension-origin"); + + info("Opening storage panel"); + const { commands, extensionStorage } = await openAddonStoragePanel( + extension.id + ); + + manifest = { + ...manifest, + version: "2.0", + }; + // "Reload" is most similar to an upgrade, as e.g. storage data is preserved + info("Update to version 2.0"); + await extension.upgrade( + getExtensionConfig({ + manifest, + background, + }) + ); + + await extension.awaitMessage("extension-origin"); + + const { data } = await extensionStorage.getStoreObjects(host, null, { + sessionString, + }); + Assert.deepEqual( + data, + [ + { + area: "local", + name: "a", + value: { str: '{"b":123}' }, + isValueEditable: true, + }, + { + area: "local", + name: "c", + value: { str: '{"d":456}' }, + isValueEditable: true, + }, + ], + "Got the expected results on populated storage.local" + ); + + await shutdown(extension, commands); + } +); + +/** + * Test case: Bg page adds one storage.local item and one storage.sync item. + * - Load extension with background page that automatically adds two storage items on startup. + * - Open the add-on storage panel. + * - Assert that only the storage.local item is shown in the panel. + */ +add_task( + async function test_panel_data_only_updates_for_storage_local_changes() { + async function background() { + await browser.storage.local.set({ a: { b: 123 } }); + await browser.storage.sync.set({ c: { d: 456 } }); + // window is available in background scripts + // eslint-disable-next-line no-undef + browser.test.sendMessage("extension-origin", window.location.origin); + } + + // Using the storage.sync API requires a non-temporary extension ID, see Bug 1323228. + const EXTENSION_ID = + "test_panel_data_only_updates_for_storage_local_changes@xpcshell.mozilla.org"; + const manifest = { + browser_specific_settings: { + gecko: { + id: EXTENSION_ID, + }, + }, + }; + + info("Loading and starting extension"); + const extension = await startupExtension( + getExtensionConfig({ manifest, background }) + ); + + info("Waiting for message from test extension"); + const host = await extension.awaitMessage("extension-origin"); + + info("Opening storage panel"); + const { commands, extensionStorage } = await openAddonStoragePanel( + extension.id + ); + + const { data } = await extensionStorage.getStoreObjects(host); + Assert.deepEqual( + data, + [ + { + area: "local", + name: "a", + value: { str: '{"b":123}' }, + isValueEditable: true, + }, + ], + "Got the expected results on populated storage.local" + ); + + await shutdown(extension, commands); + } +); + +// This test verifies that Bug 1802929 fix doesn't regress. +add_task(async function test_live_update_with_no_extension_listener() { + const EXTENSION_ID = "test_with_no_listeners@xpcshell.mozilla.org"; + let manifest = { + version: "1.0", + browser_specific_settings: { + gecko: { + id: EXTENSION_ID, + }, + }, + }; + + function background() { + browser.test.onMessage.addListener(async (msg, ...args) => { + if (msg !== "storage-local-api-call") { + browser.test.fail(`Got unexpected test message: ${msg}`); + return; + } + + const [{ method, methodArgs }] = args; + const res = await browser.storage.local[method](...methodArgs); + browser.test.sendMessage(`${msg}:done`, res); + }); + } + + const extension = await startupExtension( + getExtensionConfig({ manifest, background }) + ); + + const { target, extensionStorage } = await openAddonStoragePanel( + extension.id + ); + + const { baseURI } = extension.extension; + const host = `${baseURI.scheme}://${baseURI.host}`; + + let { data } = await extensionStorage.getStoreObjects(host); + Assert.deepEqual(data, [], "Got the expected results on empty storage.local"); + + async function testStorageLocalUpdate(storageValue) { + info("Store extension data"); + await extension.sendMessage("storage-local-api-call", { + method: "set", + methodArgs: [{ storageKeyName: storageValue }], + }); + await extension.awaitMessage("storage-local-api-call:done"); + + info("Verify stored extension data"); + await extension.sendMessage("storage-local-api-call", { + method: "get", + methodArgs: [], + }); + + Assert.deepEqual( + await extension.awaitMessage("storage-local-api-call:done"), + { storageKeyName: storageValue }, + "Got the expected value from browser.storage.local.get" + ); + + await TestUtils.waitForCondition(async () => { + const res = await extensionStorage.getStoreObjects(host); + return res.data?.length > 0; + }, "Wait for the extension storage panel updates"); + + data = (await extensionStorage.getStoreObjects(host)).data; + Assert.deepEqual( + data, + [ + { + area: "local", + name: "storageKeyName", + value: { str: `${storageValue}` }, + isValueEditable: true, + }, + ], + "Expected DevTools Storage panel data to have been updated" + ); + } + + await testStorageLocalUpdate("aStorageValue 01"); + + manifest = { + ...manifest, + version: "2.0", + }; + // "Reload" is most similar to an upgrade, as e.g. storage data is preserved + info("Update to version 2.0"); + await extension.upgrade(getExtensionConfig({ manifest, background })); + + await testStorageLocalUpdate("aStorageValue 02"); + + await shutdown(extension, target); +}); diff --git a/devtools/server/tests/xpcshell/test_extension_storage_actor_upgrade.js b/devtools/server/tests/xpcshell/test_extension_storage_actor_upgrade.js new file mode 100644 index 0000000000..5d2285b9e8 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_extension_storage_actor_upgrade.js @@ -0,0 +1,142 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Note: this test used to be in test_extension_storage_actor.js, but seems to + * fail frequently as soon as we start auto-attaching targets. + * See Bug 1618059. + */ + +const { ExtensionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/ExtensionXPCShellUtils.sys.mjs" +); + +const { + createMissingIndexedDBDirs, + extensionScriptWithMessageListener, + getExtensionConfig, + openAddonStoragePanel, + shutdown, + startupExtension, +} = require("resource://test/webextension-helpers.js"); + +const l10n = new Localization(["devtools/client/storage.ftl"], true); +const sessionString = l10n.formatValueSync("storage-expires-session"); + +// Ignore rejection related to the storage.onChanged listener being removed while the extension context is being closed. +const { PromiseTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PromiseTestUtils.sys.mjs" +); +PromiseTestUtils.allowMatchingRejectionsGlobally( + /Message manager disconnected/ +); + +const { createAppInfo, promiseStartupManager } = AddonTestUtils; + +AddonTestUtils.init(this); +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42"); + +ExtensionTestUtils.init(this); + +add_task(async function setup() { + await promiseStartupManager(); + const dir = createMissingIndexedDBDirs(); + + Assert.ok( + dir.exists(), + "Should have a 'storage/permanent' dir in the profile dir" + ); +}); + +/** + * Test case: Bg page adds an item to storage. With storage panel open, reload extension. + * - Load extension with background page that adds a storage item on message. + * - Open the add-on storage panel. + * - With the storage panel still open, reload the extension. + * - The data in the storage panel should match the item added prior to reloading. + */ +add_task(async function test_panel_live_reload() { + const EXTENSION_ID = "test_panel_live_reload@xpcshell.mozilla.org"; + let manifest = { + version: "1.0", + browser_specific_settings: { + gecko: { + id: EXTENSION_ID, + }, + }, + }; + + info("Loading extension version 1.0"); + const extension = await startupExtension( + getExtensionConfig({ + manifest, + background: extensionScriptWithMessageListener, + }) + ); + + info("Waiting for message from test extension"); + const host = await extension.awaitMessage("extension-origin"); + + info("Adding storage item"); + extension.sendMessage("storage-local-set", { a: 123 }); + await extension.awaitMessage("storage-local-set:done"); + + const { commands, extensionStorage } = await openAddonStoragePanel( + extension.id + ); + + manifest = { + ...manifest, + version: "2.0", + }; + // "Reload" is most similar to an upgrade, as e.g. storage data is preserved + info("Update to version 2.0"); + + // Wait for the storage front to receive an event for the storage panel refresh + // when the extension has been reloaded. + const promiseStoragePanelUpdated = new Promise(resolve => { + extensionStorage.on( + "single-store-update", + function updateListener(updates) { + info(`Got stores-update event: ${JSON.stringify(updates)}`); + const extStorageAdded = updates.added?.extensionStorage; + if (host in extStorageAdded && extStorageAdded[host].length) { + extensionStorage.off("single-store-update", updateListener); + resolve(); + } + } + ); + }); + + await extension.upgrade( + getExtensionConfig({ + manifest, + background: extensionScriptWithMessageListener, + }) + ); + + await Promise.all([ + extension.awaitMessage("extension-origin"), + promiseStoragePanelUpdated, + ]); + + const { data } = await extensionStorage.getStoreObjects(host, null, { + sessionString, + }); + Assert.deepEqual( + data, + [ + { + area: "local", + name: "a", + value: { str: "123" }, + isValueEditable: true, + }, + ], + "Got the expected results on populated storage.local" + ); + + await shutdown(extension, commands); +}); diff --git a/devtools/server/tests/xpcshell/test_forwardingprefix.js b/devtools/server/tests/xpcshell/test_forwardingprefix.js new file mode 100644 index 0000000000..e917350da5 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_forwardingprefix.js @@ -0,0 +1,226 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* Exercise prefix-based forwarding of packets to other transports. */ + +const { RootActor } = require("resource://devtools/server/actors/root.js"); + +var gMainConnection, gMainTransport; +var gSubconnection1, gSubconnection2; +var gClient; + +function run_test() { + DevToolsServer.init(); + + add_test(createMainConnection); + add_test(TestNoForwardingYet); + add_test(createSubconnection1); + add_test(TestForwardPrefix1OnlyRoot); + add_test(createSubconnection2); + add_test(TestForwardPrefix12OnlyRoot); + add_test(TestForwardPrefix12WithActor1); + add_test(TestForwardPrefix12WithActor12); + run_next_test(); +} + +/* + * Create a pipe connection, and return an object |{ conn, transport }|, + * where |conn| is the new DevToolsServerConnection instance, and + * |transport| is the client side of the transport on which it communicates + * (that is, packets sent on |transport| go to the new connection, and + * |transport|'s hooks receive replies). + * + * |prefix| is optional; if present, it's the prefix (minus the '/') for + * actors in the new connection. + */ +function newConnection(prefix) { + let conn; + DevToolsServer.createRootActor = function (connection) { + conn = connection; + return new RootActor(connection, {}); + }; + + const transport = DevToolsServer.connectPipe(prefix); + + return { conn, transport }; +} + +/* Create the main connection for these tests. */ +function createMainConnection() { + ({ conn: gMainConnection, transport: gMainTransport } = newConnection()); + gClient = new DevToolsClient(gMainTransport); + gClient.connect().then(([type, traits]) => run_next_test()); +} + +/* + * Exchange 'echo' messages with five actors: + * - root + * - prefix1/root + * - prefix1/actor + * - prefix2/root + * - prefix2/actor + * + * Expect proper echos from those named in |reachables|, and 'noSuchActor' + * errors from the others. When we've gotten all our replies (errors or + * otherwise), call |completed|. + * + * To avoid deep stacks, we call completed from the next tick. + */ +async function tryActors(reachables, completed) { + for (const actor of [ + "root", + "prefix1/root", + "prefix1/actor", + "prefix2/root", + "prefix2/actor", + ]) { + let response; + try { + if (actor.endsWith("root")) { + // Root actor doesn't expose any echo method, + // so fallback on getRoot which returns `{ from: "root" }`. + // For the top level root actor, we have to use its front. + if (actor == "root") { + response = await gClient.mainRoot.getRoot(); + } else { + response = await gClient.request({ to: actor, type: "getRoot" }); + } + } else { + response = await gClient.request({ + to: actor, + type: "echo", + value: "tango", + }); + } + } catch (e) { + response = e; + } + if (reachables.has(actor)) { + if (actor.endsWith("root")) { + // RootActor's getRoot response is almost empty on xpcshell + Assert.deepEqual({ from: actor }, response); + } else { + Assert.deepEqual( + { from: actor, to: actor, type: "echo", value: "tango" }, + response + ); + } + } else { + Assert.deepEqual( + { + from: actor, + error: "noSuchActor", + message: "No such actor for ID: " + actor, + }, + response + ); + } + } + executeSoon(completed, "tryActors callback " + completed.name); +} + +/* + * With no forwarding established, sending messages to root should work, + * but sending messages to prefixed actor names, or anyone else, should get + * an error. + */ +function TestNoForwardingYet() { + tryActors(new Set(["root"]), run_next_test); +} + +/* + * Create a new pipe connection which forwards its reply packets to + * gMainConnection's client, and to which gMainConnection forwards packets + * directed to actors whose names begin with |prefix + '/'|, and. + * + * Return an object { conn, transport }, as for newConnection. + */ +function newSubconnection(prefix) { + const { conn, transport } = newConnection(prefix); + transport.hooks = { + onPacket: packet => gMainConnection.send(packet), + }; + gMainConnection.setForwarding(prefix, transport); + + return { conn, transport }; +} + +/* Create a second root actor, to which we can forward things. */ +function createSubconnection1() { + const { conn, transport } = newSubconnection("prefix1"); + gSubconnection1 = conn; + transport.ready(); + gClient.expectReply("prefix1/root", reply => run_next_test()); +} + +// Establish forwarding, but don't put any actors in that server. +function TestForwardPrefix1OnlyRoot() { + tryActors(new Set(["root", "prefix1/root"]), run_next_test); +} + +/* Create a third root actor, to which we can forward things. */ +function createSubconnection2() { + const { conn, transport } = newSubconnection("prefix2"); + gSubconnection2 = conn; + transport.ready(); + gClient.expectReply("prefix2/root", reply => run_next_test()); +} + +function TestForwardPrefix12OnlyRoot() { + tryActors(new Set(["root", "prefix1/root", "prefix2/root"]), run_next_test); +} + +// A dumb actor that implements 'echo'. +// +// It's okay that both subconnections' actors behave identically, because +// the reply-sending code attaches the replying actor's name to the packet, +// so simply matching the 'from' field in the reply ensures that we heard +// from the right actor. +const { Actor } = require("resource://devtools/shared/protocol/Actor.js"); +class EchoActor extends Actor { + constructor(conn) { + super(conn, { typeName: "EchoActor", methods: [] }); + + this.requestTypes = { + echo: EchoActor.prototype.onEcho, + }; + } + + onEcho(request) { + /* + * Request packets are frozen. Copy request, so that + * DevToolsServerConnection.onPacket can attach a 'from' property. + */ + return JSON.parse(JSON.stringify(request)); + } +} + +function TestForwardPrefix12WithActor1() { + const actor = new EchoActor(gSubconnection1); + actor.actorID = "prefix1/actor"; + gSubconnection1.addActor(actor); + + tryActors( + new Set(["root", "prefix1/root", "prefix1/actor", "prefix2/root"]), + run_next_test + ); +} + +function TestForwardPrefix12WithActor12() { + const actor = new EchoActor(gSubconnection2); + actor.actorID = "prefix2/actor"; + gSubconnection2.addActor(actor); + + tryActors( + new Set([ + "root", + "prefix1/root", + "prefix1/actor", + "prefix2/root", + "prefix2/actor", + ]), + run_next_test + ); +} diff --git a/devtools/server/tests/xpcshell/test_frameactor-01.js b/devtools/server/tests/xpcshell/test_frameactor-01.js new file mode 100644 index 0000000000..18c75d0abe --- /dev/null +++ b/devtools/server/tests/xpcshell/test_frameactor-01.js @@ -0,0 +1,35 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Verify that we get a frame actor along with a debugger statement. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + Assert.ok(!!packet.frame); + Assert.ok(!!packet.frame.getActorByID); + Assert.equal(packet.frame.displayName, "stopMe"); + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + debuggee.eval( + "(" + + function () { + function stopMe() { + debugger; + } + stopMe(); + } + + ")()" + ); +} diff --git a/devtools/server/tests/xpcshell/test_frameactor-02.js b/devtools/server/tests/xpcshell/test_frameactor-02.js new file mode 100644 index 0000000000..9529d2f324 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_frameactor-02.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Verify that two pauses in a row will keep the same frame actor. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet1 = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const packet2 = await resumeAndWaitForPause(threadFront); + + Assert.equal(packet1.frame.actor, packet2.frame.actor); + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + debuggee.eval( + "(" + + function () { + function stopMe() { + debugger; + debugger; + } + stopMe(); + } + + ")()" + ); +} diff --git a/devtools/server/tests/xpcshell/test_frameactor-03.js b/devtools/server/tests/xpcshell/test_frameactor-03.js new file mode 100644 index 0000000000..7feecd14e0 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_frameactor-03.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Verify that a frame actor is properly expired when the frame goes away. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + const frameActorID = packet.frame.actorID; + { + const { frames } = await threadFront.getFrames(0, null); + ok( + frames.some(f => f.actorID === frameActorID), + "The paused frame is returned by getFrames" + ); + + Assert.equal(frames.length, 3, "Thread front has 3 frames"); + } + + await resumeAndWaitForPause(threadFront); + await checkFramesLength(threadFront, 2); + { + const { frames } = await threadFront.getFrames(0, null); + ok( + !frames.some(f => f.actorID === frameActorID), + "The paused frame is no longer returned by getFrames" + ); + + Assert.equal(frames.length, 2, "Thread front has 2 frames"); + } + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + debuggee.eval( + "(" + + function () { + function stopMe() { + debugger; + } + stopMe(); + debugger; + } + + ")()" + ); +} diff --git a/devtools/server/tests/xpcshell/test_frameactor-04.js b/devtools/server/tests/xpcshell/test_frameactor-04.js new file mode 100644 index 0000000000..200ee9968d --- /dev/null +++ b/devtools/server/tests/xpcshell/test_frameactor-04.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Verify the "frames" request on the thread. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const response = await threadFront.getFrames(0, 1000); + for (let i = 0; i < response.frames.length; i++) { + const expected = frameFixtures[i]; + const actual = response.frames[i]; + + Assert.equal( + expected.displayname, + actual.displayname, + "Frame displayname" + ); + Assert.equal(expected.type, actual.type, "Frame displayname"); + } + + await threadFront.resume(); + }) +); + +var frameFixtures = [ + // Function calls... + { type: "call", displayName: "depth3" }, + { type: "call", displayName: "depth2" }, + { type: "call", displayName: "depth1" }, + + // Anonymous function call in our eval... + { type: "call", displayName: undefined }, + + // The eval itself. + { type: "eval", displayName: "(eval)" }, +]; + +function evalCode(debuggee) { + debuggee.eval( + "(" + + function () { + function depth3() { + debugger; + } + function depth2() { + depth3(); + } + function depth1() { + depth2(); + } + depth1(); + } + + ")()" + ); +} diff --git a/devtools/server/tests/xpcshell/test_frameactor-05.js b/devtools/server/tests/xpcshell/test_frameactor-05.js new file mode 100644 index 0000000000..90456191e7 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_frameactor-05.js @@ -0,0 +1,39 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + await checkFramesLength(threadFront, 5); + + await resumeAndWaitForPause(threadFront); + await checkFramesLength(threadFront, 2); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + debuggee.eval( + "(" + + function () { + function depth3() { + debugger; + } + function depth2() { + depth3(); + } + function depth1() { + depth2(); + } + depth1(); + debugger; + } + + ")()" + ); +} diff --git a/devtools/server/tests/xpcshell/test_frameactor_wasm-01.js b/devtools/server/tests/xpcshell/test_frameactor_wasm-01.js new file mode 100644 index 0000000000..5967e8a086 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_frameactor_wasm-01.js @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Verify that wasm frame(s) can be requested from the client. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + await threadFront.reconfigure({ + observeAsmJS: true, + observeWasm: true, + }); + + await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const frameResponse = await threadFront.getFrames(0, null); + + Assert.equal(frameResponse.frames.length, 4); + + const wasmFrame = frameResponse.frames[1]; + Assert.equal(wasmFrame.type, "wasmcall"); + Assert.equal(wasmFrame.this, undefined); + + const location = wasmFrame.where; + const source = await getSourceById(threadFront, location.actor); + Assert.equal(location.line > 0, true); + Assert.equal(location.column > 0, true); + Assert.equal(/^wasm:(?:[^:]*:)*?[0-9a-f]{16}$/.test(source.url), true); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + /* eslint-disable comma-spacing, max-len */ + debuggee.eval( + "(" + + function () { + // WebAssembly bytecode was generated by running: + // js -e 'print(wasmTextToBinary("(module(import \"a\" \"b\")(func(export \"c\")call 0))"))' + const m = new WebAssembly.Module( + new Uint8Array([ + 0, 97, 115, 109, 1, 0, 0, 0, 1, 132, 128, 128, 128, 0, 1, 96, 0, 0, + 2, 135, 128, 128, 128, 0, 1, 1, 97, 1, 98, 0, 0, 3, 130, 128, 128, + 128, 0, 1, 0, 6, 129, 128, 128, 128, 0, 0, 7, 133, 128, 128, 128, 0, + 1, 1, 99, 0, 1, 10, 138, 128, 128, 128, 0, 1, 132, 128, 128, 128, 0, + 0, 16, 0, 11, + ]) + ); + const i = new WebAssembly.Instance(m, { + a: { + b: () => { + debugger; + }, + }, + }); + i.exports.c(); + } + + ")()" + ); +} diff --git a/devtools/server/tests/xpcshell/test_framearguments-01.js b/devtools/server/tests/xpcshell/test_framearguments-01.js new file mode 100644 index 0000000000..524d43f58c --- /dev/null +++ b/devtools/server/tests/xpcshell/test_framearguments-01.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check a frame actor's arguments property. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const args = packet.frame.arguments; + Assert.equal(args.length, 6); + Assert.equal(args[0], 42); + Assert.equal(args[1], true); + Assert.equal(args[2], "nasu"); + Assert.equal(args[3].type, "null"); + Assert.equal(args[4].type, "undefined"); + Assert.equal(args[5].type, "object"); + Assert.equal(args[5].class, "Object"); + Assert.ok(!!args[5].actor); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + debuggee.eval( + "(" + + function () { + function stopMe(number, bool, string, null_, undef, object) { + debugger; + } + stopMe(42, true, "nasu", null, undefined, { foo: "bar" }); + } + + ")()" + ); +} diff --git a/devtools/server/tests/xpcshell/test_framebindings-01.js b/devtools/server/tests/xpcshell/test_framebindings-01.js new file mode 100644 index 0000000000..ecf6f02e97 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_framebindings-01.js @@ -0,0 +1,71 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check a frame actor's bindings property. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const environment = await packet.frame.getEnvironment(); + const bindings = environment.bindings; + const args = bindings.arguments; + const vars = bindings.variables; + + Assert.equal(args.length, 6); + Assert.equal(args[0].number.value, 42); + Assert.equal(args[1].bool.value, true); + Assert.equal(args[2].string.value, "nasu"); + Assert.equal(args[3].null_.value.type, "null"); + Assert.equal(args[4].undef.value.type, "undefined"); + Assert.equal(args[5].object.value.type, "object"); + Assert.equal(args[5].object.value.class, "Object"); + Assert.ok(!!args[5].object.value.actor); + + Assert.equal(vars.a.value, 1); + Assert.equal(vars.b.value, true); + Assert.equal(vars.c.value.type, "object"); + Assert.equal(vars.c.value.class, "Object"); + Assert.ok(!!vars.c.value.actor); + + const objClient = threadFront.pauseGrip(vars.c.value); + const response = await objClient.getPrototypeAndProperties(); + Assert.equal(response.ownProperties.a.configurable, true); + Assert.equal(response.ownProperties.a.enumerable, true); + Assert.equal(response.ownProperties.a.writable, true); + Assert.equal(response.ownProperties.a.value, "a"); + + Assert.equal(response.ownProperties.b.configurable, true); + Assert.equal(response.ownProperties.b.enumerable, true); + Assert.equal(response.ownProperties.b.writable, true); + Assert.equal(response.ownProperties.b.value.type, "undefined"); + Assert.equal(false, "class" in response.ownProperties.b.value); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + /* eslint-disable */ + debuggee.eval( + "(" + + function () { + function stopMe(number, bool, string, null_, undef, object) { + var a = 1; + var b = true; + var c = { a: "a", b: undefined }; + debugger; + } + stopMe(42, true, "nasu", null, undefined, { foo: "bar" }); + } + + ")()" + ); + /* eslint-enable */ +} diff --git a/devtools/server/tests/xpcshell/test_framebindings-02.js b/devtools/server/tests/xpcshell/test_framebindings-02.js new file mode 100644 index 0000000000..48c243193b --- /dev/null +++ b/devtools/server/tests/xpcshell/test_framebindings-02.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check a frame actor's parent bindings. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const environment = await packet.frame.getEnvironment(); + let parentEnv = environment.parent; + const bindings = parentEnv.bindings; + const args = bindings.arguments; + const vars = bindings.variables; + Assert.notEqual(parentEnv, undefined); + Assert.equal(args.length, 0); + Assert.equal(vars.stopMe.value.type, "object"); + Assert.equal(vars.stopMe.value.class, "Function"); + Assert.ok(!!vars.stopMe.value.actor); + + // Skip the global lexical scope. + parentEnv = parentEnv.parent.parent; + Assert.notEqual(parentEnv, undefined); + const objClient = threadFront.pauseGrip(parentEnv.object); + const response = await objClient.getPrototypeAndProperties(); + Assert.equal(response.ownProperties.Object.value.getGrip().type, "object"); + Assert.equal( + response.ownProperties.Object.value.getGrip().class, + "Function" + ); + Assert.ok(!!response.ownProperties.Object.value.actorID); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + /* eslint-disable */ + debuggee.eval( + "(" + + function () { + function stopMe(number, bool, string, null_, undef, object) { + var a = 1; + var b = true; + var c = { a: "a" }; + eval(""); + debugger; + } + stopMe(42, true, "nasu", null, undefined, { foo: "bar" }); + } + + ")()" + ); +} diff --git a/devtools/server/tests/xpcshell/test_framebindings-03.js b/devtools/server/tests/xpcshell/test_framebindings-03.js new file mode 100644 index 0000000000..46dc777ef1 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_framebindings-03.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* strict mode code may not contain 'with' statements */ +/* eslint-disable strict */ + +/** + * Check a |with| frame actor's bindings. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const env = await packet.frame.getEnvironment(); + Assert.notEqual(env, undefined); + + const parentEnv = env.parent; + Assert.notEqual(parentEnv, undefined); + + const bindings = parentEnv.bindings; + const args = bindings.arguments; + const vars = bindings.variables; + Assert.equal(args.length, 1); + Assert.equal(args[0].number.value, 10); + Assert.equal(vars.r.value, 10); + Assert.equal(vars.a.value, Math.PI * 100); + Assert.equal(vars.arguments.value.class, "Arguments"); + Assert.ok(!!vars.arguments.value.actor); + + const objClient = threadFront.pauseGrip(env.object); + const response = await objClient.getPrototypeAndProperties(); + Assert.equal(response.ownProperties.PI.value, Math.PI); + Assert.equal(response.ownProperties.cos.value.getGrip().type, "object"); + Assert.equal(response.ownProperties.cos.value.getGrip().class, "Function"); + Assert.ok(!!response.ownProperties.cos.value.actorID); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + /* eslint-disable */ + debuggee.eval( + "(" + + function () { + function stopMe(number) { + var a; + var r = number; + with (Math) { + a = PI * r * r; + debugger; + } + } + stopMe(10); + } + + ")()" + ); + /* eslint-enable */ +} diff --git a/devtools/server/tests/xpcshell/test_framebindings-04.js b/devtools/server/tests/xpcshell/test_framebindings-04.js new file mode 100644 index 0000000000..1e3cc1485c --- /dev/null +++ b/devtools/server/tests/xpcshell/test_framebindings-04.js @@ -0,0 +1,77 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* strict mode code may not contain 'with' statements */ +/* eslint-disable strict */ + +/** + * Check the environment bindings of a |with| within a |with|. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const env = await packet.frame.getEnvironment(); + Assert.notEqual(env, undefined); + + const objClient = threadFront.pauseGrip(env.object); + let response = await objClient.getPrototypeAndProperties(); + Assert.equal(response.ownProperties.one.value, 1); + Assert.equal(response.ownProperties.two.value, 2); + Assert.equal(response.ownProperties.foo, undefined); + + let parentEnv = env.parent; + Assert.notEqual(parentEnv, undefined); + + const parentClient = threadFront.pauseGrip(parentEnv.object); + response = await parentClient.getPrototypeAndProperties(); + Assert.equal(response.ownProperties.PI.value, Math.PI); + Assert.equal(response.ownProperties.cos.value.getGrip().type, "object"); + Assert.equal(response.ownProperties.cos.value.getGrip().class, "Function"); + Assert.ok(!!response.ownProperties.cos.value.actorID); + + parentEnv = parentEnv.parent; + Assert.notEqual(parentEnv, undefined); + + const bindings = parentEnv.bindings; + const args = bindings.arguments; + const vars = bindings.variables; + Assert.equal(args.length, 1); + Assert.equal(args[0].number.value, 10); + Assert.equal(vars.r.value, 10); + Assert.equal(vars.a.value, Math.PI * 100); + Assert.equal(vars.arguments.value.class, "Arguments"); + Assert.ok(!!vars.arguments.value.actor); + Assert.equal(vars.foo.value, 2 * Math.PI); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + /* eslint-disable */ + debuggee.eval( + "(" + + function () { + function stopMe(number) { + var a, + obj = { one: 1, two: 2 }; + var r = number; + with (Math) { + a = PI * r * r; + with (obj) { + var foo = two * PI; + debugger; + } + } + } + stopMe(10); + } + + ")()" + ); + /* eslint-enable */ +} diff --git a/devtools/server/tests/xpcshell/test_framebindings-05.js b/devtools/server/tests/xpcshell/test_framebindings-05.js new file mode 100644 index 0000000000..6206fe8668 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_framebindings-05.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check the environment bindings of a |with| in global scope. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const env = await packet.frame.getEnvironment(); + Assert.notEqual(env, undefined); + + const objClient = threadFront.pauseGrip(env.object); + let response = await objClient.getPrototypeAndProperties(); + Assert.equal(response.ownProperties.PI.value, Math.PI); + Assert.equal(response.ownProperties.cos.value.getGrip().type, "object"); + Assert.equal(response.ownProperties.cos.value.getGrip().class, "Function"); + Assert.ok(!!response.ownProperties.cos.value.actorID); + + // Skip the global lexical scope. + const parentEnv = env.parent.parent; + Assert.notEqual(parentEnv, undefined); + + const parentClient = threadFront.pauseGrip(parentEnv.object); + response = await parentClient.getPrototypeAndProperties(); + Assert.equal(response.ownProperties.a.value, Math.PI * 100); + Assert.equal(response.ownProperties.r.value, 10); + Assert.equal(response.ownProperties.Object.value.getGrip().type, "object"); + Assert.equal( + response.ownProperties.Object.value.getGrip().class, + "Function" + ); + Assert.ok(!!response.ownProperties.Object.value.actorID); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + debuggee.eval( + "var a, r = 10;\n" + + "with (Math) {\n" + + " a = PI * r * r;\n" + + " debugger;\n" + + "}" + ); +} diff --git a/devtools/server/tests/xpcshell/test_framebindings-06.js b/devtools/server/tests/xpcshell/test_framebindings-06.js new file mode 100644 index 0000000000..52ab0cfe7c --- /dev/null +++ b/devtools/server/tests/xpcshell/test_framebindings-06.js @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const env = await packet.frame.getEnvironment(); + equal(env.type, "function"); + equal(env.function.displayName, "banana3"); + let parent = env.parent; + equal(parent.type, "block"); + ok("banana3" in parent.bindings.variables); + parent = parent.parent; + equal(parent.type, "function"); + equal(parent.function.displayName, "banana2"); + parent = parent.parent; + equal(parent.type, "block"); + ok("banana2" in parent.bindings.variables); + parent = parent.parent; + equal(parent.type, "function"); + equal(parent.function.displayName, "banana"); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + debuggee.eval( + "function banana(x) {\n" + + " return function banana2(y) {\n" + + " return function banana3(z) {\n" + + ' eval("");\n' + + " debugger;\n" + + " };\n" + + " };\n" + + "}\n" + + "banana('x')('y')('z');\n" + ); +} diff --git a/devtools/server/tests/xpcshell/test_framebindings-07.js b/devtools/server/tests/xpcshell/test_framebindings-07.js new file mode 100644 index 0000000000..77d43dfba8 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_framebindings-07.js @@ -0,0 +1,41 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task( + threadFrontTest(async ({ threadFront, debuggee, client }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const environment = await packet.frame.getEnvironment(); + Assert.equal(environment.type, "function"); + Assert.equal(environment.bindings.arguments[0].z.value, "z"); + + const parent = environment.parent; + Assert.equal(parent.type, "block"); + Assert.equal(parent.bindings.variables.banana3.value.class, "Function"); + + const grandpa = parent.parent; + Assert.equal(grandpa.type, "function"); + Assert.equal(grandpa.bindings.arguments[0].y.value, "y"); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + debuggee.eval( + "function banana(x) {\n" + + " return function banana2(y) {\n" + + " return function banana3(z) {\n" + + ' eval("");\n' + + " debugger;\n" + + " };\n" + + " };\n" + + "}\n" + + "banana('x')('y')('z');\n" + ); +} diff --git a/devtools/server/tests/xpcshell/test_front_destroy.js b/devtools/server/tests/xpcshell/test_front_destroy.js new file mode 100644 index 0000000000..33e2ac827a --- /dev/null +++ b/devtools/server/tests/xpcshell/test_front_destroy.js @@ -0,0 +1,42 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that fronts throw errors if they are called after being destroyed. + */ + +"use strict"; + +// HACK: ServiceWorkerManager requires the "profile-change-teardown" to cleanly +// shutdown, and setting _profileInitialized to `true` will trigger those +// notifications (see /testing/xpcshell/head.js). +// eslint-disable-next-line no-undef +_profileInitialized = true; + +add_task(async function test() { + DevToolsServer.init(); + DevToolsServer.registerAllActors(); + + info("Create and connect the DevToolsClient"); + const transport = DevToolsServer.connectPipe(); + const client = new DevToolsClient(transport); + await client.connect(); + + info("Get the device front and check calling getDescription() on it"); + const front = await client.mainRoot.getFront("device"); + const description = await front.getDescription(); + ok( + !!description, + "Check that the getDescription() method returns a valid response." + ); + + info("Destroy the device front and try calling getDescription again"); + front.destroy(); + Assert.throws( + () => front.getDescription(), + /Can not send request 'getDescription' because front 'device' is already destroyed\./, + "Check device front throws when getDescription() is called after destroy()" + ); + + await client.close(); +}); diff --git a/devtools/server/tests/xpcshell/test_functiongrips-01.js b/devtools/server/tests/xpcshell/test_functiongrips-01.js new file mode 100644 index 0000000000..5abce26875 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_functiongrips-01.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + // Test named function + function evalCode() { + debuggee.eval( + function stopMe(arg1) { + debugger; + }.toString() + ); + debuggee.eval("stopMe(stopMe)"); + } + + const packet1 = await executeOnNextTickAndWaitForPause( + () => evalCode(), + threadFront + ); + + const args1 = packet1.frame.arguments; + + Assert.equal(args1[0].class, "Function"); + Assert.equal(args1[0].name, "stopMe"); + Assert.equal(args1[0].displayName, "stopMe"); + + await threadFront.resume(); + + // Test inferred name function + const packet2 = await executeOnNextTickAndWaitForPause( + () => + debuggee.eval( + "var o = { m: function(foo, bar, baz) { } }; stopMe(o.m)" + ), + threadFront + ); + + const args2 = packet2.frame.arguments; + + Assert.equal(args2[0].class, "Function"); + // No name for an anonymous function, but it should have an inferred name. + Assert.equal(args2[0].name, undefined); + Assert.equal(args2[0].displayName, "m"); + + await threadFront.resume(); + + // Test anonymous function + const packet3 = await executeOnNextTickAndWaitForPause( + () => debuggee.eval("stopMe(function(foo, bar, baz) { })"), + threadFront + ); + + const args3 = packet3.frame.arguments; + + Assert.equal(args3[0].class, "Function"); + // No name for an anonymous function, and no inferred name, either. + Assert.equal(args3[0].name, undefined); + Assert.equal(args3[0].displayName, undefined); + + await threadFront.resume(); + }) +); diff --git a/devtools/server/tests/xpcshell/test_getRuleText.js b/devtools/server/tests/xpcshell/test_getRuleText.js new file mode 100644 index 0000000000..fe53dca158 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_getRuleText.js @@ -0,0 +1,143 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { + getRuleText, +} = require("resource://devtools/server/actors/utils/style-utils.js"); + +const TEST_DATA = [ + { + desc: "Empty input", + input: "", + line: 1, + column: 1, + throws: true, + }, + { + desc: "Simplest test case", + input: "#id{color:red;background:yellow;}", + line: 1, + column: 1, + expected: { offset: 4, text: "color:red;background:yellow;" }, + }, + { + desc: "Multiple rules test case", + input: + "#id{color:red;background:yellow;}.class-one .class-two " + + "{ position:absolute; line-height: 45px}", + line: 1, + column: 34, + expected: { offset: 56, text: " position:absolute; line-height: 45px" }, + }, + { + desc: "Unclosed rule", + input: "#id{color:red;background:yellow;", + line: 1, + column: 1, + expected: { offset: 4, text: "color:red;background:yellow;" }, + }, + { + desc: "Null input", + input: null, + line: 1, + column: 1, + throws: true, + }, + { + desc: "Missing loc", + input: "#id{color:red;background:yellow;}", + throws: true, + }, + { + desc: "Multi-lines CSS", + input: [ + "/* this is a multi line css */", + "body {", + " color: green;", + " background-repeat: no-repeat", + "}", + " /*something else here */", + "* {", + " color: purple;", + "}", + ].join("\n"), + line: 7, + column: 1, + expected: { offset: 116, text: "\n color: purple;\n" }, + }, + { + desc: "Multi-lines CSS and multi-line rule", + input: [ + "/* ", + "* some comments", + "*/", + "", + "body {", + " margin: 0;", + " padding: 15px 15px 2px 15px;", + " color: red;", + "}", + "", + "#header .btn, #header .txt {", + " font-size: 100%;", + "}", + "", + "#header #information {", + " color: #dddddd;", + " font-size: small;", + "}", + ].join("\n"), + line: 5, + column: 1, + expected: { + offset: 30, + text: "\n margin: 0;\n padding: 15px 15px 2px 15px;\n color: red;\n", + }, + }, + { + desc: "Content string containing a } character", + input: " #id{border:1px solid red;content: '}';color:red;}", + line: 1, + column: 4, + expected: { + offset: 7, + text: "border:1px solid red;content: '}';color:red;", + }, + }, + { + desc: "Rule contains no tokens", + input: "div{}", + line: 1, + column: 1, + expected: { offset: 4, text: "" }, + }, +]; + +function run_test() { + for (const test of TEST_DATA) { + info("Starting test: " + test.desc); + info("Input string " + test.input); + let output; + try { + output = getRuleText(test.input, test.line, test.column); + if (test.throws) { + info("Test should have thrown"); + Assert.ok(false); + } + } catch (e) { + info("getRuleText threw an exception with the given input string"); + if (test.throws) { + info("Exception expected"); + Assert.ok(true); + } else { + info("Exception unexpected\n" + e); + Assert.ok(false); + } + } + if (output) { + deepEqual(output, test.expected); + } + } +} diff --git a/devtools/server/tests/xpcshell/test_getTextAtLineColumn.js b/devtools/server/tests/xpcshell/test_getTextAtLineColumn.js new file mode 100644 index 0000000000..3aa9915192 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_getTextAtLineColumn.js @@ -0,0 +1,35 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { + getTextAtLineColumn, +} = require("resource://devtools/server/actors/utils/style-utils.js"); + +const TEST_DATA = [ + { + desc: "simplest", + input: "#id{color:red;background:yellow;}", + line: 1, + column: 5, + expected: { offset: 4, text: "color:red;background:yellow;}" }, + }, + { + desc: "multiple lines", + input: "one\n two\n three", + line: 3, + column: 3, + expected: { offset: 11, text: "three" }, + }, +]; + +function run_test() { + for (const test of TEST_DATA) { + info("Starting test: " + test.desc); + info("Input string " + test.input); + + const output = getTextAtLineColumn(test.input, test.line, test.column); + deepEqual(output, test.expected); + } +} diff --git a/devtools/server/tests/xpcshell/test_get_command_and_arg.js b/devtools/server/tests/xpcshell/test_get_command_and_arg.js new file mode 100644 index 0000000000..b3f0ab8ec4 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_get_command_and_arg.js @@ -0,0 +1,121 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { + getCommandAndArgs, +} = require("resource://devtools/server/actors/webconsole/commands/parser.js"); + +const testcases = [ + { input: ":help", expectedOutput: "help()" }, + { + input: ":screenshot --fullscreen", + expectedOutput: 'screenshot({"fullscreen":true})', + }, + { + input: ":screenshot --fullscreen true", + expectedOutput: 'screenshot({"fullscreen":true})', + }, + { input: ":screenshot ", expectedOutput: "screenshot()" }, + { + input: ":screenshot --dpr 0.5 --fullpage --chrome", + expectedOutput: 'screenshot({"dpr":0.5,"fullpage":true,"chrome":true})', + }, + { + input: ":screenshot 'filename'", + expectedOutput: 'screenshot({"filename":"filename"})', + }, + { + input: ":screenshot filename", + expectedOutput: 'screenshot({"filename":"filename"})', + }, + { + input: + ":screenshot --name 'filename' --name `filename` --name \"filename\"", + expectedOutput: 'screenshot({"name":["filename","filename","filename"]})', + }, + { + input: ":screenshot 'filename1' 'filename2' 'filename3'", + expectedOutput: 'screenshot({"filename":"filename1"})', + }, + { + input: ":screenshot --chrome --chrome", + expectedOutput: 'screenshot({"chrome":true})', + }, + { + input: ':screenshot "file name with spaces"', + expectedOutput: 'screenshot({"filename":"file name with spaces"})', + }, + { + input: ":screenshot 'filename1' --name 'filename2'", + expectedOutput: 'screenshot({"filename":"filename1","name":"filename2"})', + }, + { + input: ":screenshot --name 'filename1' 'filename2'", + expectedOutput: 'screenshot({"name":"filename1","filename":"filename2"})', + }, + { + input: ':screenshot "fo\\"o bar"', + expectedOutput: 'screenshot({"filename":"fo\\\\\\"o bar"})', + }, + { + input: ':screenshot "foo b\\"ar"', + expectedOutput: 'screenshot({"filename":"foo b\\\\\\"ar"})', + }, +]; + +const edgecases = [ + { input: ":", expectedError: /Missing a command name after ':'/ }, + { input: ":invalid", expectedError: /'invalid' is not a valid command/ }, + { + input: ":screenshot :help", + expectedError: + /Executing multiple commands in one evaluation is not supported/, + }, + { input: ":screenshot --", expectedError: /invalid flag/ }, + { + input: ':screenshot "fo"o bar', + expectedError: + /String has unescaped `"` in \["fo"o\.\.\.\], may miss a space between arguments/, + }, + { + input: ':screenshot "foo b"ar', + expectedError: + // eslint-disable-next-line max-len + /String has unescaped `"` in \["foo b"ar\.\.\.\], may miss a space between arguments/, + }, + { input: ": screenshot", expectedError: /Missing a command name after ':'/ }, + { + input: ':screenshot "file name', + expectedError: /String does not terminate/, + }, + { + input: ':screenshot "file name --clipboard', + expectedError: /String does not terminate before flag "clipboard"/, + }, + { + input: "::screenshot", + expectedError: /':screenshot' is not a valid command/, + }, +]; + +function formatArgs(args) { + return Object.keys(args).length ? JSON.stringify(args) : ""; +} + +function run_test() { + testcases.forEach(testcase => { + const { command, args } = getCommandAndArgs(testcase.input); + const argsString = formatArgs(args); + Assert.equal(`${command}(${argsString})`, testcase.expectedOutput); + }); + + edgecases.forEach(testcase => { + Assert.throws( + () => getCommandAndArgs(testcase.input), + testcase.expectedError, + `"${testcase.input}" should throw expected error` + ); + }); +} diff --git a/devtools/server/tests/xpcshell/test_getyoungestframe.js b/devtools/server/tests/xpcshell/test_getyoungestframe.js new file mode 100644 index 0000000000..f08628b7ed --- /dev/null +++ b/devtools/server/tests/xpcshell/test_getyoungestframe.js @@ -0,0 +1,38 @@ +/* eslint-disable strict */ +function run_test() { + Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); + }); + const { addDebuggerToGlobal } = ChromeUtils.importESModule( + "resource://gre/modules/jsdebugger.sys.mjs" + ); + addDebuggerToGlobal(globalThis); + const xpcInspector = Cc["@mozilla.org/jsinspector;1"].getService( + Ci.nsIJSInspector + ); + const g = createTestGlobal("test1"); + + const dbg = makeDebugger(); + dbg.uncaughtExceptionHook = testExceptionHook; + + dbg.addDebuggee(g); + dbg.onDebuggerStatement = function (frame) { + Assert.ok(frame === dbg.getNewestFrame()); + // Execute from the nested event loop, dbg.getNewestFrame() won't + // be working anymore. + + executeSoon(function () { + try { + Assert.ok(frame === dbg.getNewestFrame()); + } finally { + xpcInspector.exitNestedEventLoop("test"); + } + }); + xpcInspector.enterNestedEventLoop("test"); + }; + + g.eval("function debuggerStatement() { debugger; }; debuggerStatement();"); + + dbg.disable(); +} diff --git a/devtools/server/tests/xpcshell/test_ignore_caught_exceptions.js b/devtools/server/tests/xpcshell/test_ignore_caught_exceptions.js new file mode 100644 index 0000000000..fe04161aab --- /dev/null +++ b/devtools/server/tests/xpcshell/test_ignore_caught_exceptions.js @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that setting ignoreCaughtExceptions will cause the debugger to ignore + * caught exceptions, but not uncaught ones. + */ + +add_task( + threadFrontTest( + async ({ threadFront, debuggee, commands }) => { + await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + + await commands.threadConfigurationCommand.updateConfiguration({ + pauseOnExceptions: true, + ignoreCaughtExceptions: true, + }); + await resume(threadFront); + const paused = await waitForPause(threadFront); + Assert.equal(paused.why.type, "exception"); + equal(paused.frame.where.line, 6, "paused at throw"); + + await resume(threadFront); + }, + { + // Bug 1508289, exception tests fails in worker scope + doNotRunWorker: true, + } + ) +); + +function evaluateTestCode(debuggee) { + // prettier-ignore + try { + Cu.evalInSandbox(` // 1 + debugger; // 2 + try { // 3 + throw "foo"; // 4 + } catch (e) {} // 5 + throw "bar"; // 6 + `, // 7 + debuggee, + "1.8", + "test_pause_exceptions-03.js", + 1 + ); + } catch (e) {} +} diff --git a/devtools/server/tests/xpcshell/test_ignore_no_interface_exceptions.js b/devtools/server/tests/xpcshell/test_ignore_no_interface_exceptions.js new file mode 100644 index 0000000000..50d28ffdc0 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_ignore_no_interface_exceptions.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that the debugger automatically ignores NS_ERROR_NO_INTERFACE + * exceptions, but not normal ones. + */ + +add_task( + threadFrontTest( + async ({ threadFront, debuggee }) => { + await threadFront.pauseOnExceptions(true, false); + const paused = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + equal(paused.frame.where.line, 6, "paused at throw"); + + await resume(threadFront); + }, + { + // Bug 1508289, exception tests fails in worker scope + doNotRunWorker: true, + } + ) +); + +function evaluateTestCode(debuggee) { + // prettier-ignore + Cu.evalInSandbox(` // 1 + function QueryInterface() { // 2 + throw Cr.NS_ERROR_NO_INTERFACE; // 3 + } // 4 + function stopMe() { // 5 + throw 42; // 6 + } // 7 + try { // 8 + QueryInterface(); // 9 + } catch (e) {} // 10 + try { // 11 + stopMe(); // 12 + } catch (e) {}`, // 13 + debuggee, + "1.8", + "test_ignore_no_interface_exceptions.js", + 1 + ); +} diff --git a/devtools/server/tests/xpcshell/test_interrupt.js b/devtools/server/tests/xpcshell/test_interrupt.js new file mode 100644 index 0000000000..07593a7360 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_interrupt.js @@ -0,0 +1,15 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task( + threadFrontTest(async ({ threadFront, debuggee, client, targetFront }) => { + const onPaused = waitForEvent(threadFront, "paused"); + await threadFront.interrupt(); + await onPaused; + Assert.equal(threadFront.paused, true); + await threadFront.resume(); + Assert.equal(threadFront.paused, false); + }) +); diff --git a/devtools/server/tests/xpcshell/test_layout-reflows-observer.js b/devtools/server/tests/xpcshell/test_layout-reflows-observer.js new file mode 100644 index 0000000000..74f31b97fe --- /dev/null +++ b/devtools/server/tests/xpcshell/test_layout-reflows-observer.js @@ -0,0 +1,311 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test the LayoutChangesObserver + +/* eslint-disable mozilla/use-chromeutils-generateqi */ + +var { + getLayoutChangesObserver, + releaseLayoutChangesObserver, + LayoutChangesObserver, +} = require("resource://devtools/server/actors/reflow.js"); +const EventEmitter = require("resource://devtools/shared/event-emitter.js"); + +// Override set/clearTimeout on LayoutChangesObserver to avoid depending on +// time in this unit test. This means that LayoutChangesObserver.eventLoopTimer +// will be the timeout callback instead of the timeout itself, so test cases +// will need to execute it to fake a timeout +LayoutChangesObserver.prototype._setTimeout = cb => cb; +LayoutChangesObserver.prototype._clearTimeout = function () {}; + +// Mock the targetActor since we only really want to test the LayoutChangesObserver +// and don't want to depend on a window object, nor want to test protocol.js +class MockTargetActor extends EventEmitter { + constructor() { + super(); + this.docShell = new MockDocShell(); + this.window = new MockWindow(this.docShell); + this.windows = [this.window]; + this.attached = true; + } + + get chromeEventHandler() { + return this.docShell.chromeEventHandler; + } + + isDestroyed() { + return false; + } +} + +function MockWindow(docShell) { + this.docShell = docShell; +} +MockWindow.prototype = { + QueryInterface() { + const self = this; + return { + getInterface() { + return { + QueryInterface() { + return self.docShell; + }, + }; + }, + }; + }, + setTimeout(cb) { + // Simply return the cb itself so that we can execute it in the test instead + // of depending on a real timeout + return cb; + }, + clearTimeout() {}, +}; + +function MockDocShell() { + this.observer = null; +} +MockDocShell.prototype = { + addWeakReflowObserver(observer) { + this.observer = observer; + }, + removeWeakReflowObserver() {}, + get chromeEventHandler() { + return { + addEventListener: (type, cb) => { + if (type === "resize") { + this.resizeCb = cb; + } + }, + removeEventListener: (type, cb) => { + if (type === "resize" && cb === this.resizeCb) { + this.resizeCb = null; + } + }, + }; + }, + mockResize() { + if (this.resizeCb) { + this.resizeCb(); + } + }, +}; + +function run_test() { + instancesOfObserversAreSharedBetweenWindows(); + eventsAreBatched(); + noEventsAreSentWhenThereAreNoReflowsAndLoopTimeouts(); + observerIsAlreadyStarted(); + destroyStopsObserving(); + stoppingAndStartingSeveralTimesWorksCorrectly(); + reflowsArentStackedWhenStopped(); + stackedReflowsAreResetOnStop(); +} + +function instancesOfObserversAreSharedBetweenWindows() { + info( + "Checking that when requesting twice an instances of the observer " + + "for the same WindowGlobalTargetActor, the instance is shared" + ); + + info("Checking 2 instances of the observer for the targetActor 1"); + const targetActor1 = new MockTargetActor(); + const obs11 = getLayoutChangesObserver(targetActor1); + const obs12 = getLayoutChangesObserver(targetActor1); + Assert.equal(obs11, obs12); + + info("Checking 2 instances of the observer for the targetActor 2"); + const targetActor2 = new MockTargetActor(); + const obs21 = getLayoutChangesObserver(targetActor2); + const obs22 = getLayoutChangesObserver(targetActor2); + Assert.equal(obs21, obs22); + + info( + "Checking that observers instances for 2 different targetActors are " + + "different" + ); + Assert.notEqual(obs11, obs21); + + releaseLayoutChangesObserver(targetActor1); + releaseLayoutChangesObserver(targetActor1); + releaseLayoutChangesObserver(targetActor2); + releaseLayoutChangesObserver(targetActor2); +} + +function eventsAreBatched() { + info( + "Checking that reflow events are batched and only sent when the " + + "timeout expires" + ); + + // Note that in this test, we mock the target actor and its window property, so we also + // mock the setTimeout/clearTimeout mechanism and just call the callback manually + const targetActor = new MockTargetActor(); + const observer = getLayoutChangesObserver(targetActor); + + const reflowsEvents = []; + const onReflows = reflows => reflowsEvents.push(reflows); + observer.on("reflows", onReflows); + + const resizeEvents = []; + const onResize = () => resizeEvents.push("resize"); + observer.on("resize", onResize); + + info("Fake one reflow event"); + targetActor.window.docShell.observer.reflow(); + info("Checking that no batched reflow event has been emitted"); + Assert.equal(reflowsEvents.length, 0); + + info("Fake another reflow event"); + targetActor.window.docShell.observer.reflow(); + info("Checking that still no batched reflow event has been emitted"); + Assert.equal(reflowsEvents.length, 0); + + info("Fake a few of resize events too"); + targetActor.window.docShell.mockResize(); + targetActor.window.docShell.mockResize(); + targetActor.window.docShell.mockResize(); + info("Checking that still no batched resize event has been emitted"); + Assert.equal(resizeEvents.length, 0); + + info("Faking timeout expiration and checking that events are sent"); + observer.eventLoopTimer(); + Assert.equal(reflowsEvents.length, 1); + Assert.equal(reflowsEvents[0].length, 2); + Assert.equal(resizeEvents.length, 1); + + observer.off("reflows", onReflows); + observer.off("resize", onResize); + releaseLayoutChangesObserver(targetActor); +} + +function noEventsAreSentWhenThereAreNoReflowsAndLoopTimeouts() { + info( + "Checking that if no reflows were detected and the event batching " + + "loop expires, then no reflows event is sent" + ); + + const targetActor = new MockTargetActor(); + const observer = getLayoutChangesObserver(targetActor); + + const reflowsEvents = []; + const onReflows = reflows => reflowsEvents.push(reflows); + observer.on("reflows", onReflows); + + info("Faking timeout expiration and checking for reflows"); + observer.eventLoopTimer(); + Assert.equal(reflowsEvents.length, 0); + + observer.off("reflows", onReflows); + releaseLayoutChangesObserver(targetActor); +} + +function observerIsAlreadyStarted() { + info("Checking that the observer is already started when getting it"); + + const targetActor = new MockTargetActor(); + const observer = getLayoutChangesObserver(targetActor); + Assert.ok(observer.isObserving); + + observer.stop(); + Assert.ok(!observer.isObserving); + + observer.start(); + Assert.ok(observer.isObserving); + + releaseLayoutChangesObserver(targetActor); +} + +function destroyStopsObserving() { + info("Checking that the destroying the observer stops it"); + + const targetActor = new MockTargetActor(); + const observer = getLayoutChangesObserver(targetActor); + Assert.ok(observer.isObserving); + + observer.destroy(); + Assert.ok(!observer.isObserving); + + releaseLayoutChangesObserver(targetActor); +} + +function stoppingAndStartingSeveralTimesWorksCorrectly() { + info( + "Checking that the stopping and starting several times the observer" + + " works correctly" + ); + + const targetActor = new MockTargetActor(); + const observer = getLayoutChangesObserver(targetActor); + + Assert.ok(observer.isObserving); + observer.start(); + observer.start(); + observer.start(); + Assert.ok(observer.isObserving); + + observer.stop(); + Assert.ok(!observer.isObserving); + + observer.stop(); + observer.stop(); + Assert.ok(!observer.isObserving); + + releaseLayoutChangesObserver(targetActor); +} + +function reflowsArentStackedWhenStopped() { + info("Checking that when stopped, reflows aren't stacked in the observer"); + + const targetActor = new MockTargetActor(); + const observer = getLayoutChangesObserver(targetActor); + + info("Stoping the observer"); + observer.stop(); + + info("Faking reflows"); + targetActor.window.docShell.observer.reflow(); + targetActor.window.docShell.observer.reflow(); + targetActor.window.docShell.observer.reflow(); + + info("Checking that reflows aren't recorded"); + Assert.equal(observer.reflows.length, 0); + + info("Starting the observer and faking more reflows"); + observer.start(); + targetActor.window.docShell.observer.reflow(); + targetActor.window.docShell.observer.reflow(); + targetActor.window.docShell.observer.reflow(); + + info("Checking that reflows are recorded"); + Assert.equal(observer.reflows.length, 3); + + releaseLayoutChangesObserver(targetActor); +} + +function stackedReflowsAreResetOnStop() { + info("Checking that stacked reflows are reset on stop"); + + const targetActor = new MockTargetActor(); + const observer = getLayoutChangesObserver(targetActor); + + targetActor.window.docShell.observer.reflow(); + Assert.equal(observer.reflows.length, 1); + + observer.stop(); + Assert.equal(observer.reflows.length, 0); + + targetActor.window.docShell.observer.reflow(); + Assert.equal(observer.reflows.length, 0); + + observer.start(); + Assert.equal(observer.reflows.length, 0); + + targetActor.window.docShell.observer.reflow(); + Assert.equal(observer.reflows.length, 1); + + releaseLayoutChangesObserver(targetActor); +} diff --git a/devtools/server/tests/xpcshell/test_listsources-01.js b/devtools/server/tests/xpcshell/test_listsources-01.js new file mode 100644 index 0000000000..306825278c --- /dev/null +++ b/devtools/server/tests/xpcshell/test_listsources-01.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check basic getSources functionality. + */ + +var gNumTimesSourcesSent = 0; + +add_task( + threadFrontTest(async ({ threadFront, debuggee, client }) => { + client.request = (function (origRequest) { + return function (request, onResponse) { + if (request.type === "sources") { + ++gNumTimesSourcesSent; + } + return origRequest.call(this, request, onResponse); + }; + })(client.request); + + await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const response = await threadFront.getSources(); + + Assert.ok( + response.sources.some(function (s) { + return s.url && s.url.match(/test_listsources-01.js/); + }) + ); + + Assert.ok( + gNumTimesSourcesSent <= 1, + "Should only send one sources request at most, even though we" + + " might have had to send one to determine feature support." + ); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + /* eslint-disable */ + Cu.evalInSandbox( + "var line0 = Error().lineNumber;\n" + + "debugger;\n" + // line0 + 1 + "var a = 1;\n" + // line0 + 2 + "var b = 2;\n", // line0 + 3 + debuggee + ); + /* eslint-enable */ +} diff --git a/devtools/server/tests/xpcshell/test_listsources-02.js b/devtools/server/tests/xpcshell/test_listsources-02.js new file mode 100644 index 0000000000..a2f9cc3bda --- /dev/null +++ b/devtools/server/tests/xpcshell/test_listsources-02.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check getting sources before there are any. + */ + +var gNumTimesSourcesSent = 0; + +add_task( + threadFrontTest(async ({ threadFront, client }) => { + client.request = (function (origRequest) { + return function (request, onResponse) { + if (request.type === "sources") { + ++gNumTimesSourcesSent; + } + return origRequest.call(this, request, onResponse); + }; + })(client.request); + + // Test listing zero sources + const packet = await threadFront.getSources(); + + Assert.ok(!packet.error); + Assert.ok(!!packet.sources); + Assert.equal(packet.sources.length, 0); + + Assert.ok( + gNumTimesSourcesSent <= 1, + "Should only send one sources request at most, even though we" + + " might have had to send one to determine feature support." + ); + }) +); diff --git a/devtools/server/tests/xpcshell/test_listsources-03.js b/devtools/server/tests/xpcshell/test_listsources-03.js new file mode 100644 index 0000000000..f8af5aca6e --- /dev/null +++ b/devtools/server/tests/xpcshell/test_listsources-03.js @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check getSources functionality when there are lots of sources. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const response = await threadFront.getSources(); + + Assert.ok( + !response.error, + "There shouldn't be an error fetching large amounts of sources." + ); + + Assert.ok( + response.sources.some(function (s) { + return s.url.match(/foo-999.js$/); + }) + ); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + for (let i = 0; i < 1000; i++) { + Cu.evalInSandbox( + "function foo###() {return ###;}".replace(/###/g, i), + debuggee, + "1.8", + "http://example.com/foo-" + i + ".js", + 1 + ); + } + debuggee.eval("debugger;"); +} diff --git a/devtools/server/tests/xpcshell/test_logpoint-01.js b/devtools/server/tests/xpcshell/test_logpoint-01.js new file mode 100644 index 0000000000..a5cb4f2197 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_logpoint-01.js @@ -0,0 +1,83 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that logpoints generate console messages. + */ + +const Resources = require("resource://devtools/server/actors/resources/index.js"); + +add_task( + threadFrontTest(async ({ threadActor, threadFront, debuggee, client }) => { + let lastMessage, lastExpression; + const targetActor = threadActor._parent; + // Only Workers are evaluating through the WebConsoleActor. + // Tabs will be evaluating directly via the frame object. + targetActor._consoleActor = { + evaluateJS(expression) { + lastExpression = expression; + }, + }; + + // And then listen for resource RDP event. + // Bug 1646677: But we should probably migrate this test to ResourceCommand so that + // we don't have to hack the server side via Resource.watchResources call. + targetActor.on("resource-available-form", resources => { + if (resources[0].resourceType == Resources.TYPES.CONSOLE_MESSAGE) { + lastMessage = resources[0].message; + } + }); + + // But both tabs and processes will be going through the ConsoleMessages module + // We force watching for console message first, + await Resources.watchResources(targetActor, [ + Resources.TYPES.CONSOLE_MESSAGE, + ]); + + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const source = await getSourceById(threadFront, packet.frame.where.actor); + + // Set a logpoint which should invoke console.log. + threadFront.setBreakpoint( + { + sourceUrl: source.url, + line: 3, + }, + { logValue: "a" } + ); + await client.waitForRequestsToSettle(); + + // Execute the rest of the code. + await threadFront.resume(); + + // NOTE: logpoints evaluated in a worker have a lastExpression + if (lastMessage) { + Assert.equal(lastMessage.level, "logPoint"); + Assert.equal(lastMessage.arguments[0], "three"); + Assert.ok(/\d+\.\d+/.test(lastMessage.timeStamp)); + } else { + Assert.equal(lastExpression.text, "console.log(...[a])"); + Assert.equal(lastExpression.lineNumber, 3); + } + }) +); + +function evalCode(debuggee) { + /* eslint-disable */ + Cu.evalInSandbox( + "debugger;\n" + // 1 + "var a = 'three';\n" + // 2 + "var b = 2;\n", // 3 + debuggee, + "1.8", + "test.js", + 1 + ); + /* eslint-enable */ +} diff --git a/devtools/server/tests/xpcshell/test_logpoint-02.js b/devtools/server/tests/xpcshell/test_logpoint-02.js new file mode 100644 index 0000000000..d84d3fc324 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_logpoint-02.js @@ -0,0 +1,85 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that conditions are respected when specified in a logpoint. + */ + +const Resources = require("resource://devtools/server/actors/resources/index.js"); + +add_task( + threadFrontTest(async ({ threadActor, threadFront, debuggee, client }) => { + let lastMessage, lastExpression; + const targetActor = threadActor._parent; + // Only Workers are evaluating through the WebConsoleActor. + // Tabs will be evaluating directly via the frame object. + targetActor._consoleActor = { + evaluateJS(expression) { + lastExpression = expression; + }, + }; + + // And then listen for resource RDP event. + // Bug 1646677: But we should probably migrate this test to ResourceCommand so that + // we don't have to hack the server side via Resource.watchResources call. + targetActor.on("resource-available-form", resources => { + if (resources[0].resourceType == Resources.TYPES.CONSOLE_MESSAGE) { + lastMessage = resources[0].message; + } + }); + + // But both tabs and processes will be going through the ConsoleMessages module + // We force watching for console message first, + await Resources.watchResources(targetActor, [ + Resources.TYPES.CONSOLE_MESSAGE, + ]); + + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const source = await getSourceById(threadFront, packet.frame.where.actor); + + // Set a logpoint which should invoke console.log. + threadFront.setBreakpoint( + { + sourceUrl: source.url, + line: 4, + }, + { logValue: "a", condition: "a === 5" } + ); + await client.waitForRequestsToSettle(); + + // Execute the rest of the code. + await threadFront.resume(); + + // NOTE: logpoints evaluated in a worker have a lastExpression + if (lastMessage) { + Assert.equal(lastMessage.level, "logPoint"); + Assert.equal(lastMessage.arguments[0], 5); + Assert.ok(/\d+\.\d+/.test(lastMessage.timeStamp)); + } else { + Assert.equal(lastExpression.text, "console.log(...[a])"); + Assert.equal(lastExpression.lineNumber, 4); + } + }) +); + +function evalCode(debuggee) { + /* eslint-disable */ + Cu.evalInSandbox( + "debugger;\n" + // 1 + "var a = 1;\n" + // 2 + "while (a < 10) {\n" + // 3 + " a++;\n" + // 4 + "}", + debuggee, + "1.8", + "test.js", + 1 + ); + /* eslint-enable */ +} diff --git a/devtools/server/tests/xpcshell/test_logpoint-03.js b/devtools/server/tests/xpcshell/test_logpoint-03.js new file mode 100644 index 0000000000..b5d4440889 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_logpoint-03.js @@ -0,0 +1,82 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that logpoints generate console errors if the logpoint statement is invalid. + */ + +const Resources = require("resource://devtools/server/actors/resources/index.js"); + +add_task( + threadFrontTest(async ({ threadActor, threadFront, debuggee, client }) => { + let lastMessage, lastExpression; + const targetActor = threadActor._parent; + // Only Workers are evaluating through the WebConsoleActor. + // Tabs will be evaluating directly via the frame object. + targetActor._consoleActor = { + evaluateJS(expression) { + lastExpression = expression; + }, + }; + + // And then listen for resource RDP event. + // Bug 1646677: But we should probably migrate this test to ResourceCommand so that + // we don't have to hack the server side via Resource.watchResources call. + targetActor.on("resource-available-form", resources => { + if (resources[0].resourceType == Resources.TYPES.CONSOLE_MESSAGE) { + lastMessage = resources[0].message; + } + }); + + // But both tabs and processes will be going through the ConsoleMessages module + // We force watching for console message first, + await Resources.watchResources(targetActor, [ + Resources.TYPES.CONSOLE_MESSAGE, + ]); + + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const source = await getSourceById(threadFront, packet.frame.where.actor); + + // Set a logpoint which should throw an error message. + await threadFront.setBreakpoint( + { + sourceUrl: source.url, + line: 3, + }, + { logValue: "c" } + ); + + // Execute the rest of the code. + await threadFront.resume(); + + // NOTE: logpoints evaluated in a worker have a lastExpression + if (lastMessage) { + Assert.equal(lastMessage.level, "logPointError"); + Assert.equal(lastMessage.arguments[0], "c is not defined"); + Assert.ok(/\d+\.\d+/.test(lastMessage.timeStamp)); + } else { + Assert.equal(lastExpression.text, "console.log(...[c])"); + Assert.equal(lastExpression.lineNumber, 3); + } + }) +); + +function evalCode(debuggee) { + /* eslint-disable */ + Cu.evalInSandbox( + "debugger;\n" + // 1 + "var a = 'three';\n" + // 2 + "var b = 2;\n", // 3 + debuggee, + "1.8", + "test.js", + 1 + ); + /* eslint-enable */ +} diff --git a/devtools/server/tests/xpcshell/test_longstringgrips-01.js b/devtools/server/tests/xpcshell/test_longstringgrips-01.js new file mode 100644 index 0000000000..ac0b228c17 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_longstringgrips-01.js @@ -0,0 +1,75 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var gDebuggee; +var gClient; +var gThreadFront; + +add_task( + threadFrontTest( + async ({ threadFront, debuggee, client }) => { + gThreadFront = threadFront; + gDebuggee = debuggee; + gClient = client; + test_longstring_grip(); + }, + { waitForFinish: true } + ) +); + +function test_longstring_grip() { + const longString = + "All I want is to be a monkey of moderate intelligence who" + + " wears a suit... that's why I'm transferring to business school! Maybe I" + + " love you so much, I love you no matter who you are pretending to be." + + " Enough about your promiscuous mother, Hermes! We have bigger problems." + + " For example, if you killed your grandfather, you'd cease to exist! What" + + " kind of a father would I be if I said no? Yep, I remember. They came in" + + " last at the Olympics, then retired to promote alcoholic beverages! And" + + " remember, don't do anything that affects anything, unless it turns out" + + " you were supposed to, in which case, for the love of God, don't not do" + + " it!"; + + DevToolsServer.LONG_STRING_LENGTH = 200; + + gThreadFront.once("paused", function (packet) { + const args = packet.frame.arguments; + Assert.equal(args.length, 1); + const grip = args[0]; + + try { + Assert.equal(grip.type, "longString"); + Assert.equal(grip.length, longString.length); + Assert.equal( + grip.initial, + longString.substr(0, DevToolsServer.LONG_STRING_INITIAL_LENGTH) + ); + + const longStringFront = createLongStringFront(gClient, grip); + longStringFront.substring(22, 28).then(function (response) { + try { + Assert.equal(response, "monkey"); + } finally { + gThreadFront.resume().then(function () { + finishClient(gClient); + }); + } + }); + } catch (error) { + gThreadFront.resume().then(function () { + finishClient(gClient); + do_throw(error); + }); + } + }); + + gDebuggee.eval( + function stopMe(arg1) { + debugger; + }.toString() + ); + + gDebuggee.eval('stopMe("' + longString + '")'); +} diff --git a/devtools/server/tests/xpcshell/test_nativewrappers.js b/devtools/server/tests/xpcshell/test_nativewrappers.js new file mode 100644 index 0000000000..170a2a1e6e --- /dev/null +++ b/devtools/server/tests/xpcshell/test_nativewrappers.js @@ -0,0 +1,39 @@ +/* eslint-disable strict */ +function run_test() { + Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); + }); + const { addDebuggerToGlobal } = ChromeUtils.importESModule( + "resource://gre/modules/jsdebugger.sys.mjs" + ); + addDebuggerToGlobal(globalThis); + const g = createTestGlobal("test1"); + + const dbg = makeDebugger(); + dbg.addDebuggee(g); + dbg.onDebuggerStatement = function (frame) { + const args = frame.arguments; + try { + args[0]; + Assert.ok(true); + } catch (ex) { + Assert.ok(false); + } + }; + + g.eval("function stopMe(arg) {debugger;}"); + + const g2 = createTestGlobal("test2"); + g2.g = g; + // Not using the "stringify a function" trick because that runs afoul of the + // Cu.importGlobalProperties lint and we don't need it here anyway. + g2.eval(`(function createBadEvent() { + Cu.importGlobalProperties(["DOMParser"]); + let parser = new DOMParser(); + let doc = parser.parseFromString("<foo></foo>", "text/xml"); + g.stopMe(doc.createEvent("MouseEvent")); + } )()`); + + dbg.disable(); +} diff --git a/devtools/server/tests/xpcshell/test_nesting-03.js b/devtools/server/tests/xpcshell/test_nesting-03.js new file mode 100644 index 0000000000..0a64e751cd --- /dev/null +++ b/devtools/server/tests/xpcshell/test_nesting-03.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that we can detect nested event loops in tabs with the same URL. + +add_task(async function () { + const GLOBAL_NAME = "test-nesting1"; + + initTestDevToolsServer(); + addTestGlobal(GLOBAL_NAME); + addTestGlobal(GLOBAL_NAME); + + // Connect two thread actors, debugging the same debuggee, and both being paused. + const firstClient = new DevToolsClient(DevToolsServer.connectPipe()); + await firstClient.connect(); + const { threadFront: firstThreadFront } = await attachTestThread( + firstClient, + GLOBAL_NAME + ); + await firstThreadFront.interrupt(); + + const secondClient = new DevToolsClient(DevToolsServer.connectPipe()); + await secondClient.connect(); + const { threadFront: secondThreadFront } = await attachTestThread( + secondClient, + GLOBAL_NAME + ); + await secondThreadFront.interrupt(); + + // Then check how concurrent resume work + let result; + try { + result = await firstThreadFront.resume(); + } catch (e) { + Assert.ok(e.includes("wrongOrder"), "rejects with the wrong order"); + } + Assert.ok(!result, "no response"); + + result = await secondThreadFront.resume(); + Assert.ok(true, "resumed as expected"); + + await firstThreadFront.resume(); + + Assert.ok(true, "resumed as expected"); + await firstClient.close(); + + await finishClient(secondClient); +}); diff --git a/devtools/server/tests/xpcshell/test_nesting-04.js b/devtools/server/tests/xpcshell/test_nesting-04.js new file mode 100644 index 0000000000..dcee257c40 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_nesting-04.js @@ -0,0 +1,86 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Verify that we never pause while being already paused. + * i.e. we don't support more than one nested event loops. + */ + +add_task( + threadFrontTest(async ({ commands, threadFront, debuggee }) => { + await threadFront.setBreakpoint({ sourceUrl: "nesting-04.js", line: 2 }); + + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + Assert.equal(packet.frame.where.line, 5); + Assert.equal(packet.why.type, "debuggerStatement"); + + info("Test calling interrupt"); + const onPaused = waitForPause(threadFront); + await threadFront.interrupt(); + // interrupt() doesn't return anything, but bailout while emitting a paused packet + // But we don't pause again, the reason prove it so + const paused = await onPaused; + equal(paused.why.type, "alreadyPaused"); + + info("Test by evaluating code via the console"); + const { result } = await commands.scriptCommand.execute( + "debugger; functionWithDebuggerStatement()", + { + frameActor: packet.frame.actorID, + } + ); + // The fact that it returned immediately means that we did not pause + equal(result, 42); + + info("Test by calling code from chrome context"); + // This should be equivalent to any actor somehow triggering some page's JS + const rv = debuggee.functionWithDebuggerStatement(); + // The fact that it returned immediately means that we did not pause + equal(rv, 42); + + info("Test by stepping over a function that breaks"); + // This will only step over the debugger; statement we just break on + const step1 = await stepOver(threadFront); + equal(step1.why.type, "resumeLimit"); + equal(step1.frame.where.line, 6); + + // stepOver will actually resume and re-pause on the breakpoint + const step2 = await stepOver(threadFront); + equal(step2.why.type, "breakpoint"); + equal(step2.frame.where.line, 2); + + // Sanity check to ensure that the functionWithDebuggerStatement really pauses + info("Resume and pause on the breakpoint"); + const pausedPacket = await resumeAndWaitForPause(threadFront); + Assert.equal(pausedPacket.frame.where.line, 2); + // The breakpoint takes over the debugger statement + Assert.equal(pausedPacket.why.type, "breakpoint"); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + /* eslint-disable */ + Cu.evalInSandbox( + `function functionWithDebuggerStatement() { + debugger; + return 42; + } + debugger; + functionWithDebuggerStatement(); + var a = 1; + functionWithDebuggerStatement();`, + debuggee, + "1.8", + "nesting-04.js", + 1 + ); + /* eslint-enable */ +} diff --git a/devtools/server/tests/xpcshell/test_new_source-01.js b/devtools/server/tests/xpcshell/test_new_source-01.js new file mode 100644 index 0000000000..929865baa8 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_new_source-01.js @@ -0,0 +1,24 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check basic newSource packet sent from server. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + Cu.evalInSandbox( + function inc(n) { + return n + 1; + }.toString(), + debuggee + ); + + const sourcePacket = await waitForEvent(threadFront, "newSource"); + + Assert.ok(!!sourcePacket.source); + Assert.ok(!!sourcePacket.source.url.match(/test_new_source-01.js$/)); + }) +); diff --git a/devtools/server/tests/xpcshell/test_new_source-02.js b/devtools/server/tests/xpcshell/test_new_source-02.js new file mode 100644 index 0000000000..15259b884a --- /dev/null +++ b/devtools/server/tests/xpcshell/test_new_source-02.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that sourceURL has the correct effect when using threadFront.eval. + */ + +add_task( + threadFrontTest(async ({ commands, threadFront, debuggee }) => { + await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const packet1 = await waitForEvent(threadFront, "newSource"); + + Assert.ok(!!packet1.source); + Assert.ok(packet1.source.introductionType, "eval"); + + commands.scriptCommand.execute( + "function f() { }\n//# sourceURL=http://example.com/code.js" + ); + + const packet2 = await waitForEvent(threadFront, "newSource"); + dump(JSON.stringify(packet2, null, 2)); + Assert.ok(!!packet2.source); + Assert.ok(!!packet2.source.url.match(/example\.com/)); + }) +); + +function evalCode(debuggee) { + /* eslint-disable */ + debuggee.eval( + "(" + + function () { + function stopMe(arg1) { + debugger; + } + stopMe({ obj: true }); + } + + ")()" + ); + /* eslint-enable */ +} diff --git a/devtools/server/tests/xpcshell/test_nodelistactor.js b/devtools/server/tests/xpcshell/test_nodelistactor.js new file mode 100644 index 0000000000..eab6bb07e8 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_nodelistactor.js @@ -0,0 +1,30 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that a NodeListActor initialized with null nodelist doesn't cause +// exceptions when calling NodeListActor.form. + +const { + NodeListActor, +} = require("resource://devtools/server/actors/inspector/node.js"); + +function run_test() { + check_actor_for_list(null); + check_actor_for_list([]); + check_actor_for_list(["fakenode"]); +} + +function check_actor_for_list(nodelist) { + info("Checking NodeListActor with nodelist '" + nodelist + "' works."); + const actor = new NodeListActor({}, nodelist); + const form = actor.form(); + + // No exception occured as a exceptions abort the test. + ok(true, "No exceptions occured."); + equal( + form.length, + nodelist ? nodelist.length : 0, + "NodeListActor reported correct length." + ); +} diff --git a/devtools/server/tests/xpcshell/test_objectgrips-02.js b/devtools/server/tests/xpcshell/test_objectgrips-02.js new file mode 100644 index 0000000000..810a5009c0 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-02.js @@ -0,0 +1,44 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const args = packet.frame.arguments; + + Assert.equal(args[0].class, "Object"); + + const objectFront = threadFront.pauseGrip(args[0]); + const response = await objectFront.getPrototype(); + Assert.ok(response.prototype != undefined); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + debuggee.eval( + function stopMe(arg1) { + debugger; + }.toString() + ); + debuggee.eval( + function Constr() { + this.a = 1; + }.toString() + ); + debuggee.eval( + "Constr.prototype = { b: true, c: 'foo' }; var o = new Constr(); stopMe(o)" + ); +} diff --git a/devtools/server/tests/xpcshell/test_objectgrips-03.js b/devtools/server/tests/xpcshell/test_objectgrips-03.js new file mode 100644 index 0000000000..c8a51d41d3 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-03.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + const args = packet.frame.arguments; + + Assert.equal(args[0].class, "Object"); + + const objClient = threadFront.pauseGrip(args[0]); + let response = await objClient.getProperty("x"); + Assert.equal(response.descriptor.configurable, true); + Assert.equal(response.descriptor.enumerable, true); + Assert.equal(response.descriptor.writable, true); + Assert.equal(response.descriptor.value, 10); + + response = await objClient.getProperty("y"); + Assert.equal(response.descriptor.configurable, true); + Assert.equal(response.descriptor.enumerable, true); + Assert.equal(response.descriptor.writable, true); + Assert.equal(response.descriptor.value, "kaiju"); + + response = await objClient.getProperty("a"); + Assert.equal(response.descriptor.configurable, true); + Assert.equal(response.descriptor.enumerable, true); + Assert.equal(response.descriptor.get.type, "object"); + Assert.equal(response.descriptor.get.class, "Function"); + Assert.equal(response.descriptor.set.type, "undefined"); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + debuggee.eval( + function stopMe(arg1) { + debugger; + }.toString() + ); + debuggee.eval("stopMe({ x: 10, y: 'kaiju', get a() { return 42; } })"); +} diff --git a/devtools/server/tests/xpcshell/test_objectgrips-04.js b/devtools/server/tests/xpcshell/test_objectgrips-04.js new file mode 100644 index 0000000000..d08705db3c --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-04.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const args = packet.frame.arguments; + + Assert.equal(args[0].class, "Object"); + + const objectFront = threadFront.pauseGrip(args[0]); + const { ownProperties, prototype } = + await objectFront.getPrototypeAndProperties(); + Assert.equal(ownProperties.x.configurable, true); + Assert.equal(ownProperties.x.enumerable, true); + Assert.equal(ownProperties.x.writable, true); + Assert.equal(ownProperties.x.value, 10); + + Assert.equal(ownProperties.y.configurable, true); + Assert.equal(ownProperties.y.enumerable, true); + Assert.equal(ownProperties.y.writable, true); + Assert.equal(ownProperties.y.value, "kaiju"); + + Assert.equal(ownProperties.a.configurable, true); + Assert.equal(ownProperties.a.enumerable, true); + Assert.equal(ownProperties.a.get.getGrip().type, "object"); + Assert.equal(ownProperties.a.get.getGrip().class, "Function"); + Assert.equal(ownProperties.a.set.type, "undefined"); + + Assert.ok(prototype != undefined); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + debuggee.eval( + function stopMe(arg1) { + debugger; + }.toString() + ); + debuggee.eval("stopMe({ x: 10, y: 'kaiju', get a() { return 42; } })"); +} diff --git a/devtools/server/tests/xpcshell/test_objectgrips-05.js b/devtools/server/tests/xpcshell/test_objectgrips-05.js new file mode 100644 index 0000000000..4c6f0f107a --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-05.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test checks that frozen objects report themselves as frozen in their + * grip. + */ + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const obj1 = packet.frame.arguments[0]; + Assert.ok(obj1.frozen); + + const obj1Client = threadFront.pauseGrip(obj1); + Assert.ok(obj1Client.isFrozen); + + const obj2 = packet.frame.arguments[1]; + Assert.ok(!obj2.frozen); + + const obj2Client = threadFront.pauseGrip(obj2); + Assert.ok(!obj2Client.isFrozen); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + debuggee.eval( + function stopMe(arg1) { + debugger; + }.toString() + ); + /* eslint-disable no-undef */ + debuggee.eval( + "(" + + function () { + const obj1 = {}; + Object.freeze(obj1); + stopMe(obj1, {}); + } + + "())" + ); + /* eslint-enable no-undef */ +} diff --git a/devtools/server/tests/xpcshell/test_objectgrips-06.js b/devtools/server/tests/xpcshell/test_objectgrips-06.js new file mode 100644 index 0000000000..ef3d2b5b66 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-06.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test checks that sealed objects report themselves as sealed in their + * grip. + */ + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const obj1 = packet.frame.arguments[0]; + Assert.ok(obj1.sealed); + + const obj1Client = threadFront.pauseGrip(obj1); + Assert.ok(obj1Client.isSealed); + + const obj2 = packet.frame.arguments[1]; + Assert.ok(!obj2.sealed); + + const obj2Client = threadFront.pauseGrip(obj2); + Assert.ok(!obj2Client.isSealed); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + debuggee.eval( + function stopMe(arg1) { + debugger; + }.toString() + ); + /* eslint-disable no-undef */ + debuggee.eval( + "(" + + function () { + const obj1 = {}; + Object.seal(obj1); + stopMe(obj1, {}); + } + + "())" + ); + /* eslint-enable no-undef */ +} diff --git a/devtools/server/tests/xpcshell/test_objectgrips-07.js b/devtools/server/tests/xpcshell/test_objectgrips-07.js new file mode 100644 index 0000000000..2a3a0bf00e --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-07.js @@ -0,0 +1,65 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test checks that objects which are not extensible report themselves as + * such. + */ + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const [f, s, ne, e] = packet.frame.arguments; + const [fClient, sClient, neClient, eClient] = packet.frame.arguments.map( + a => threadFront.pauseGrip(a) + ); + + Assert.ok(!f.extensible); + Assert.ok(!fClient.isExtensible); + + Assert.ok(!s.extensible); + Assert.ok(!sClient.isExtensible); + + Assert.ok(!ne.extensible); + Assert.ok(!neClient.isExtensible); + + Assert.ok(e.extensible); + Assert.ok(eClient.isExtensible); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + debuggee.eval( + function stopMe(arg1) { + debugger; + }.toString() + ); + /* eslint-disable no-undef */ + debuggee.eval( + "(" + + function () { + const f = {}; + Object.freeze(f); + const s = {}; + Object.seal(s); + const ne = {}; + Object.preventExtensions(ne); + stopMe(f, s, ne, {}); + } + + "())" + ); + /* eslint-enable no-undef */ +} diff --git a/devtools/server/tests/xpcshell/test_objectgrips-08.js b/devtools/server/tests/xpcshell/test_objectgrips-08.js new file mode 100644 index 0000000000..1a37f19fb8 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-08.js @@ -0,0 +1,61 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const args = packet.frame.arguments; + + Assert.equal(args[0].class, "Object"); + + const objClient = threadFront.pauseGrip(args[0]); + const response = await objClient.getPrototypeAndProperties(); + const { a, b, c, d, e, f, g } = response.ownProperties; + testPropertyType(a, "Infinity"); + testPropertyType(b, "-Infinity"); + testPropertyType(c, "NaN"); + testPropertyType(d, "-0"); + testPropertyType(e, "BigInt"); + testPropertyType(f, "BigInt"); + testPropertyType(g, "BigInt"); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + debuggee.eval( + function stopMe(arg1) { + debugger; + }.toString() + ); + debuggee.eval( + `stopMe({ + a: Infinity, + b: -Infinity, + c: NaN, + d: -0, + e: 1n, + f: -2n, + g: 0n, + })` + ); +} + +function testPropertyType(prop, expectedType) { + Assert.equal(prop.configurable, true); + Assert.equal(prop.enumerable, true); + Assert.equal(prop.writable, true); + Assert.equal(prop.value.type, expectedType); +} diff --git a/devtools/server/tests/xpcshell/test_objectgrips-14.js b/devtools/server/tests/xpcshell/test_objectgrips-14.js new file mode 100644 index 0000000000..cff8611e7d --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-14.js @@ -0,0 +1,55 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test out of scope objects with synchronous functions. + */ + +var gDebuggee; +var gThreadFront; + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + gThreadFront = threadFront; + gDebuggee = debuggee; + await testObjectGroup(); + }) +); + +function evalCode() { + evalCallback(gDebuggee, function runTest() { + const ugh = []; + let i = 0; + + (function () { + (function () { + ugh.push(i++); + debugger; + })(); + })(); + + debugger; + }); +} + +const testObjectGroup = async function () { + let packet = await executeOnNextTickAndWaitForPause(evalCode, gThreadFront); + + const environment = await packet.frame.getEnvironment(); + const ugh = environment.parent.parent.bindings.variables.ugh; + const ughClient = await gThreadFront.pauseGrip(ugh.value); + + packet = await getPrototypeAndProperties(ughClient); + packet = await resumeAndWaitForPause(gThreadFront); + + const environment2 = await packet.frame.getEnvironment(); + const ugh2 = environment2.bindings.variables.ugh; + const ugh2Client = gThreadFront.pauseGrip(ugh2.value); + + packet = await getPrototypeAndProperties(ugh2Client); + Assert.equal(packet.ownProperties.length.value, 1); + + await resume(gThreadFront); +}; diff --git a/devtools/server/tests/xpcshell/test_objectgrips-15.js b/devtools/server/tests/xpcshell/test_objectgrips-15.js new file mode 100644 index 0000000000..3a7aba89c8 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-15.js @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test out of scope objects with async functions. + */ + +var gDebuggee; +var gThreadFront; + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + gThreadFront = threadFront; + gDebuggee = debuggee; + await testObjectGroup(); + }) +); + +function evalCode() { + evalCallback(gDebuggee, function runTest() { + const ugh = []; + let i = 0; + + function foo() { + ugh.push(i++); + debugger; + } + + Promise.resolve().then(foo).then(foo); + }); +} + +const testObjectGroup = async function () { + let packet = await executeOnNextTickAndWaitForPause(evalCode, gThreadFront); + + const environment = await packet.frame.getEnvironment(); + const ugh = environment.parent.bindings.variables.ugh; + const ughClient = await gThreadFront.pauseGrip(ugh.value); + + packet = await getPrototypeAndProperties(ughClient); + + packet = await resumeAndWaitForPause(gThreadFront); + const environment2 = await packet.frame.getEnvironment(); + const ugh2 = environment2.parent.bindings.variables.ugh; + const ugh2Client = gThreadFront.pauseGrip(ugh2.value); + + packet = await getPrototypeAndProperties(ugh2Client); + Assert.equal(packet.ownProperties.length.value, 2); + + await resume(gThreadFront); +}; diff --git a/devtools/server/tests/xpcshell/test_objectgrips-16.js b/devtools/server/tests/xpcshell/test_objectgrips-16.js new file mode 100644 index 0000000000..785c3bc36d --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-16.js @@ -0,0 +1,139 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint-disable no-shadow, max-nested-callbacks */ + +"use strict"; + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + eval_code, + threadFront + ); + const [grip] = packet.frame.arguments; + + // Checks grip.preview properties. + check_preview(grip); + + const objClient = threadFront.pauseGrip(grip); + const response = await objClient.getPrototypeAndProperties(); + // Checks the result of getPrototypeAndProperties. + check_prototype_and_properties(response); + + await threadFront.resume(); + + function eval_code() { + debuggee.eval( + function stopMe(arg1) { + debugger; + }.toString() + ); + debuggee.eval(` + stopMe({ + [Symbol()]: "first unnamed symbol", + [Symbol()]: "second unnamed symbol", + [Symbol("named")] : "named symbol", + [Symbol.iterator] : function* () { + yield 1; + yield 2; + }, + x: 10, + }); + `); + } + + function check_preview(grip) { + Assert.equal(grip.class, "Object"); + + const { preview } = grip; + Assert.equal(preview.ownProperties.x.configurable, true); + Assert.equal(preview.ownProperties.x.enumerable, true); + Assert.equal(preview.ownProperties.x.writable, true); + Assert.equal(preview.ownProperties.x.value, 10); + + const [ + firstUnnamedSymbol, + secondUnnamedSymbol, + namedSymbol, + iteratorSymbol, + ] = preview.ownSymbols; + + Assert.equal(firstUnnamedSymbol.name, undefined); + Assert.equal(firstUnnamedSymbol.type, "symbol"); + Assert.equal(firstUnnamedSymbol.descriptor.configurable, true); + Assert.equal(firstUnnamedSymbol.descriptor.enumerable, true); + Assert.equal(firstUnnamedSymbol.descriptor.writable, true); + Assert.equal(firstUnnamedSymbol.descriptor.value, "first unnamed symbol"); + + Assert.equal(secondUnnamedSymbol.name, undefined); + Assert.equal(secondUnnamedSymbol.type, "symbol"); + Assert.equal(secondUnnamedSymbol.descriptor.configurable, true); + Assert.equal(secondUnnamedSymbol.descriptor.enumerable, true); + Assert.equal(secondUnnamedSymbol.descriptor.writable, true); + Assert.equal( + secondUnnamedSymbol.descriptor.value, + "second unnamed symbol" + ); + + Assert.equal(namedSymbol.name, "named"); + Assert.equal(namedSymbol.type, "symbol"); + Assert.equal(namedSymbol.descriptor.configurable, true); + Assert.equal(namedSymbol.descriptor.enumerable, true); + Assert.equal(namedSymbol.descriptor.writable, true); + Assert.equal(namedSymbol.descriptor.value, "named symbol"); + + Assert.equal(iteratorSymbol.name, "Symbol.iterator"); + Assert.equal(iteratorSymbol.type, "symbol"); + Assert.equal(iteratorSymbol.descriptor.configurable, true); + Assert.equal(iteratorSymbol.descriptor.enumerable, true); + Assert.equal(iteratorSymbol.descriptor.writable, true); + Assert.equal(iteratorSymbol.descriptor.value.class, "Function"); + } + + function check_prototype_and_properties(response) { + Assert.equal(response.ownProperties.x.configurable, true); + Assert.equal(response.ownProperties.x.enumerable, true); + Assert.equal(response.ownProperties.x.writable, true); + Assert.equal(response.ownProperties.x.value, 10); + + const [ + firstUnnamedSymbol, + secondUnnamedSymbol, + namedSymbol, + iteratorSymbol, + ] = response.ownSymbols; + + Assert.equal(firstUnnamedSymbol.name, "Symbol()"); + Assert.equal(firstUnnamedSymbol.descriptor.configurable, true); + Assert.equal(firstUnnamedSymbol.descriptor.enumerable, true); + Assert.equal(firstUnnamedSymbol.descriptor.writable, true); + Assert.equal(firstUnnamedSymbol.descriptor.value, "first unnamed symbol"); + + Assert.equal(secondUnnamedSymbol.name, "Symbol()"); + Assert.equal(secondUnnamedSymbol.descriptor.configurable, true); + Assert.equal(secondUnnamedSymbol.descriptor.enumerable, true); + Assert.equal(secondUnnamedSymbol.descriptor.writable, true); + Assert.equal( + secondUnnamedSymbol.descriptor.value, + "second unnamed symbol" + ); + + Assert.equal(namedSymbol.name, "Symbol(named)"); + Assert.equal(namedSymbol.descriptor.configurable, true); + Assert.equal(namedSymbol.descriptor.enumerable, true); + Assert.equal(namedSymbol.descriptor.writable, true); + Assert.equal(namedSymbol.descriptor.value, "named symbol"); + + Assert.equal(iteratorSymbol.name, "Symbol(Symbol.iterator)"); + Assert.equal(iteratorSymbol.descriptor.configurable, true); + Assert.equal(iteratorSymbol.descriptor.enumerable, true); + Assert.equal(iteratorSymbol.descriptor.writable, true); + Assert.equal(iteratorSymbol.descriptor.value.class, "Function"); + } + }) +); diff --git a/devtools/server/tests/xpcshell/test_objectgrips-17.js b/devtools/server/tests/xpcshell/test_objectgrips-17.js new file mode 100644 index 0000000000..edaea88eaa --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-17.js @@ -0,0 +1,320 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); + +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +async function testPrincipal(options, globalPrincipal, debuggeeHasXrays) { + const { debuggee } = options; + // Create a global object with the specified security principal. + // If none is specified, use the debuggee. + if (globalPrincipal === undefined) { + await test(options, { + global: debuggee, + subsumes: true, + isOpaque: false, + globalIsInvisible: false, + }); + return; + } + + const debuggeePrincipal = Cu.getObjectPrincipal(debuggee); + const sameOrigin = debuggeePrincipal.origin === globalPrincipal.origin; + const subsumes = debuggeePrincipal.subsumes(globalPrincipal); + for (const globalHasXrays of [true, false]) { + const isOpaque = + subsumes && + globalPrincipal !== systemPrincipal && + ((sameOrigin && debuggeeHasXrays) || globalHasXrays); + for (const globalIsInvisible of [true, false]) { + let global = Cu.Sandbox(globalPrincipal, { + wantXrays: globalHasXrays, + invisibleToDebugger: globalIsInvisible, + }); + // Previously, the Sandbox constructor would (bizarrely) waive xrays on + // the return Sandbox if wantXrays was false. This has now been fixed, + // but we need to mimic that behavior here to make the test continue + // to pass. + if (!globalHasXrays) { + global = Cu.waiveXrays(global); + } + await test(options, { global, subsumes, isOpaque, globalIsInvisible }); + } + } +} + +async function test({ threadFront, debuggee }, testOptions) { + const { global } = testOptions; + const packet = await executeOnNextTickAndWaitForPause(eval_code, threadFront); + // Get the grips. + const [proxyGrip, inheritsProxyGrip, inheritsProxy2Grip] = + packet.frame.arguments; + + // Check the grip of the proxy object. + check_proxy_grip(debuggee, testOptions, proxyGrip); + + // Check the target and handler slots of the proxy object. + const proxyClient = threadFront.pauseGrip(proxyGrip); + const proxySlots = await proxyClient.getProxySlots(); + check_proxy_slots(debuggee, testOptions, proxyGrip, proxySlots); + + // Check the prototype and properties of the proxy object. + const proxyResponse = await proxyClient.getPrototypeAndProperties(); + check_properties(testOptions, proxyResponse.ownProperties, true, false); + check_prototype(debuggee, testOptions, proxyResponse.prototype, true, false); + + // Check the prototype and properties of the object which inherits from the proxy. + const inheritsProxyClient = threadFront.pauseGrip(inheritsProxyGrip); + const inheritsProxyResponse = + await inheritsProxyClient.getPrototypeAndProperties(); + check_properties( + testOptions, + inheritsProxyResponse.ownProperties, + false, + false + ); + check_prototype( + debuggee, + testOptions, + inheritsProxyResponse.prototype, + false, + false + ); + + // The prototype chain was not iterated if the object was inaccessible, so now check + // another object which inherits from the proxy, but was created in the debuggee. + const inheritsProxy2Client = threadFront.pauseGrip(inheritsProxy2Grip); + const inheritsProxy2Response = + await inheritsProxy2Client.getPrototypeAndProperties(); + check_properties( + testOptions, + inheritsProxy2Response.ownProperties, + false, + true + ); + check_prototype( + debuggee, + testOptions, + inheritsProxy2Response.prototype, + false, + true + ); + + // Check that none of the above ran proxy traps. + strictEqual(global.trapDidRun, false, "No proxy trap did run."); + + // Resume the debugger and finish the current test. + await threadFront.resume(); + + function eval_code() { + // Create objects in `global`, and debug them in `debuggee`. They may get various + // kinds of security wrappers, or no wrapper at all. + // To detect that no proxy trap runs, the proxy handler should define all possible + // traps, but the list is long and may change. Therefore a second proxy is used as + // the handler, so that a single `get` trap suffices. + global.eval(` + var trapDidRun = false; + var proxy = new Proxy({}, new Proxy({}, {get: (_, trap) => { + trapDidRun = true; + throw new Error("proxy trap '" + trap + "' was called."); + }})); + var inheritsProxy = Object.create(proxy, {x:{value:1}}); + `); + const data = Cu.createObjectIn(debuggee, { defineAs: "data" }); + data.proxy = global.proxy; + data.inheritsProxy = global.inheritsProxy; + debuggee.eval(` + var inheritsProxy2 = Object.create(data.proxy, {x:{value:1}}); + stopMe(data.proxy, data.inheritsProxy, inheritsProxy2); + `); + } +} + +function check_proxy_grip(debuggee, testOptions, grip) { + const { global, isOpaque, subsumes, globalIsInvisible } = testOptions; + const { preview } = grip; + + if (global === debuggee) { + // The proxy has no security wrappers. + strictEqual(grip.class, "Proxy", "The grip has a Proxy class."); + strictEqual( + preview.ownPropertiesLength, + 2, + "The preview has 2 properties." + ); + const props = preview.ownProperties; + ok(props["<target>"].value, "<target> contains the [[ProxyTarget]]."); + ok(props["<handler>"].value, "<handler> contains the [[ProxyHandler]]."); + } else if (isOpaque) { + // The proxy has opaque security wrappers. + strictEqual(grip.class, "Opaque", "The grip has an Opaque class."); + strictEqual(grip.ownPropertyLength, 0, "The grip has no properties."); + } else if (!subsumes) { + // The proxy belongs to compartment not subsumed by the debuggee. + strictEqual(grip.class, "Restricted", "The grip has a Restricted class."); + strictEqual( + grip.ownPropertyLength, + undefined, + "The grip doesn't know the number of properties." + ); + } else if (globalIsInvisible) { + // The proxy belongs to an invisible-to-debugger compartment. + strictEqual( + grip.class, + "InvisibleToDebugger: Object", + "The grip has an InvisibleToDebugger class." + ); + ok( + !("ownPropertyLength" in grip), + "The grip doesn't know the number of properties." + ); + } else { + // The proxy has non-opaque security wrappers. + strictEqual(grip.class, "Proxy", "The grip has a Proxy class."); + strictEqual( + preview.ownPropertiesLength, + 0, + "The preview has no properties." + ); + ok(!("<target>" in preview), "The preview has no <target> property."); + ok(!("<handler>" in preview), "The preview has no <handler> property."); + } +} + +function check_proxy_slots(debuggee, testOptions, grip, proxySlots) { + const { global } = testOptions; + + if (grip.class !== "Proxy") { + strictEqual( + proxySlots, + null, + "Slots can only be retrived for Proxy grips." + ); + } else if (global === debuggee) { + const { proxyTarget, proxyHandler } = proxySlots; + strictEqual( + proxyTarget.getGrip().type, + "object", + "There is a [[ProxyTarget]] grip." + ); + strictEqual( + proxyHandler.getGrip().type, + "object", + "There is a [[ProxyHandler]] grip." + ); + } else { + const { proxyTarget, proxyHandler } = proxySlots; + strictEqual( + proxyTarget.type, + "undefined", + "There is no [[ProxyTarget]] grip." + ); + strictEqual( + proxyHandler.type, + "undefined", + "There is no [[ProxyHandler]] grip." + ); + } +} + +function check_properties(testOptions, props, isProxy, createdInDebuggee) { + const { subsumes, globalIsInvisible } = testOptions; + const ownPropertiesLength = Reflect.ownKeys(props).length; + + if (createdInDebuggee || (!isProxy && subsumes && !globalIsInvisible)) { + // The debuggee can access the properties. + strictEqual(ownPropertiesLength, 1, "1 own property was retrieved."); + strictEqual(props.x.value, 1, "The property has the right value."); + } else { + // The debuggee is not allowed to access the object. + strictEqual(ownPropertiesLength, 0, "No own property could be retrieved."); + } +} + +function check_prototype( + debuggee, + testOptions, + proto, + isProxy, + createdInDebuggee +) { + const { global, isOpaque, subsumes, globalIsInvisible } = testOptions; + if (isOpaque && !globalIsInvisible && !createdInDebuggee) { + // The object is or inherits from a proxy with opaque security wrappers. + // The debuggee sees `Object.prototype` when retrieving the prototype. + strictEqual( + proto.getGrip().class, + "Object", + "The prototype has a Object class." + ); + } else if (isProxy && isOpaque && globalIsInvisible) { + // The object is a proxy with opaque security wrappers in an invisible global. + // The debuggee sees an inaccessible `Object.prototype` when retrieving the prototype. + strictEqual( + proto.getGrip().class, + "InvisibleToDebugger: Object", + "The prototype has an InvisibleToDebugger class." + ); + } else if ( + createdInDebuggee || + (!isProxy && subsumes && !globalIsInvisible) + ) { + // The object inherits from a proxy and has no security wrappers or non-opaque ones. + // The debuggee sees the proxy when retrieving the prototype. + check_proxy_grip( + debuggee, + { global, isOpaque, subsumes, globalIsInvisible }, + proto.getGrip() + ); + } else { + // The debuggee is not allowed to access the object. It sees a null prototype. + strictEqual(proto.type, "null", "The prototype is null."); + } +} + +function createNullPrincipal() { + return Services.scriptSecurityManager.createNullPrincipal({}); +} + +async function run_tests_in_principal( + options, + debuggeePrincipal, + debuggeeHasXrays +) { + const { debuggee } = options; + debuggee.eval( + function stopMe(arg1, arg2) { + debugger; + }.toString() + ); + + // Test objects created in the debuggee. + await testPrincipal(options, undefined, debuggeeHasXrays); + + // Test objects created in a system principal new global. + await testPrincipal(options, systemPrincipal, debuggeeHasXrays); + + // Test objects created in a cross-origin null principal new global. + await testPrincipal(options, createNullPrincipal(), debuggeeHasXrays); + + if (debuggeePrincipal != systemPrincipal) { + // Test objects created in a same-origin principal new global. + await testPrincipal(options, debuggeePrincipal, debuggeeHasXrays); + } +} + +for (const principal of [systemPrincipal, createNullPrincipal()]) { + for (const wantXrays of [true, false]) { + add_task( + threadFrontTest( + options => run_tests_in_principal(options, principal, wantXrays), + { principal, wantXrays } + ) + ); + } +} diff --git a/devtools/server/tests/xpcshell/test_objectgrips-18.js b/devtools/server/tests/xpcshell/test_objectgrips-18.js new file mode 100644 index 0000000000..90c38d99a9 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-18.js @@ -0,0 +1,173 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + eval_code, + threadFront + ); + + const [grip] = packet.frame.arguments; + + const objectFront = threadFront.pauseGrip(grip); + + // Checks the result of enumProperties. + let response = await objectFront.enumProperties({}); + await check_enum_properties(response); + + // Checks the result of enumSymbols. + response = await objectFront.enumSymbols(); + await check_enum_symbols(response); + + await threadFront.resume(); + + function eval_code() { + debuggee.eval( + function stopMe(arg1) { + debugger; + }.toString() + ); + + debuggee.eval(` + var obj = Array.from({length: 10}) + .reduce((res, _, i) => { + res["property_" + i + "_key"] = "property_" + i + "_value"; + res[Symbol("symbol_" + i)] = "symbol_" + i + "_value"; + return res; + }, {}); + + obj[Symbol()] = "first unnamed symbol"; + obj[Symbol()] = "second unnamed symbol"; + obj[Symbol.iterator] = function* () { + yield 1; + yield 2; + }; + + stopMe(obj); + `); + } + + async function check_enum_properties(iterator) { + equal(iterator.count, 10, "iterator.count has the expected value"); + + info("Check iterator.slice response for all properties"); + let sliceResponse = await iterator.slice(0, iterator.count); + ok( + sliceResponse && + Object.getOwnPropertyNames(sliceResponse).includes("ownProperties"), + "The response object has an ownProperties property" + ); + + let { ownProperties } = sliceResponse; + let names = Object.keys(ownProperties); + equal( + names.length, + iterator.count, + "The response has the expected number of properties" + ); + for (let i = 0; i < names.length; i++) { + const name = names[i]; + equal(name, `property_${i}_key`); + equal(ownProperties[name].value, `property_${i}_value`); + } + + info("Check iterator.all response"); + const allResponse = await iterator.all(); + deepEqual( + allResponse, + sliceResponse, + "iterator.all response has the expected data" + ); + + info("Check iterator response for 2 properties only"); + sliceResponse = await iterator.slice(2, 2); + ok( + sliceResponse && + Object.getOwnPropertyNames(sliceResponse).includes("ownProperties"), + "The response object has an ownProperties property" + ); + + ownProperties = sliceResponse.ownProperties; + names = Object.keys(ownProperties); + equal( + names.length, + 2, + "The response has the expected number of properties" + ); + equal(names[0], `property_2_key`); + equal(names[1], `property_3_key`); + equal(ownProperties[names[0]].value, `property_2_value`); + equal(ownProperties[names[1]].value, `property_3_value`); + } + + async function check_enum_symbols(iterator) { + equal(iterator.count, 13, "iterator.count has the expected value"); + + info("Check iterator.slice response for all symbols"); + let sliceResponse = await iterator.slice(0, iterator.count); + ok( + sliceResponse && + Object.getOwnPropertyNames(sliceResponse).includes("ownSymbols"), + "The response object has an ownSymbols property" + ); + + let { ownSymbols } = sliceResponse; + equal( + ownSymbols.length, + iterator.count, + "The response has the expected number of symbols" + ); + for (let i = 0; i < 10; i++) { + const symbol = ownSymbols[i]; + equal(symbol.name, `Symbol(symbol_${i})`); + equal(symbol.descriptor.value, `symbol_${i}_value`); + } + const firstUnnamedSymbol = ownSymbols[10]; + equal(firstUnnamedSymbol.name, "Symbol()"); + equal(firstUnnamedSymbol.descriptor.value, "first unnamed symbol"); + + const secondUnnamedSymbol = ownSymbols[11]; + equal(secondUnnamedSymbol.name, "Symbol()"); + equal(secondUnnamedSymbol.descriptor.value, "second unnamed symbol"); + + const iteratorSymbol = ownSymbols[12]; + equal(iteratorSymbol.name, "Symbol(Symbol.iterator)"); + equal(iteratorSymbol.descriptor.value.getGrip().class, "Function"); + + info("Check iterator.all response"); + const allResponse = await iterator.all(); + deepEqual( + allResponse, + sliceResponse, + "iterator.all response has the expected data" + ); + + info("Check iterator response for 2 symbols only"); + sliceResponse = await iterator.slice(9, 2); + ok( + sliceResponse && + Object.getOwnPropertyNames(sliceResponse).includes("ownSymbols"), + "The response object has an ownSymbols property" + ); + + ownSymbols = sliceResponse.ownSymbols; + equal( + ownSymbols.length, + 2, + "The response has the expected number of symbols" + ); + equal(ownSymbols[0].name, "Symbol(symbol_9)"); + equal(ownSymbols[0].descriptor.value, "symbol_9_value"); + equal(ownSymbols[1].name, "Symbol()"); + equal(ownSymbols[1].descriptor.value, "first unnamed symbol"); + } + }) +); diff --git a/devtools/server/tests/xpcshell/test_objectgrips-19.js b/devtools/server/tests/xpcshell/test_objectgrips-19.js new file mode 100644 index 0000000000..655c7d0f43 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-19.js @@ -0,0 +1,75 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +add_task( + threadFrontTest(async ({ threadFront, debuggee, client }) => { + debuggee.eval( + function stopMe(arg1) { + debugger; + }.toString() + ); + const tests = [ + { + value: true, + class: "Boolean", + }, + { + value: 123, + class: "Number", + }, + { + value: "foo", + class: "String", + }, + { + value: Symbol("bar"), + class: "Symbol", + name: "bar", + }, + ]; + for (const data of tests) { + debuggee.primitive = data.value; + const packet = await executeOnNextTickAndWaitForPause(() => { + debuggee.eval("stopMe(Object(primitive));"); + }, threadFront); + + const [grip] = packet.frame.arguments; + check_wrapped_primitive_grip(grip, data); + + await threadFront.resume(); + } + }) +); + +function check_wrapped_primitive_grip(grip, data) { + strictEqual(grip.class, data.class, "The grip has the proper class."); + + if (!grip.preview) { + // In a worker thread Cu does not exist, the objects are considered unsafe and + // can't be unwrapped, so there is no preview. + return; + } + + const value = grip.preview.wrappedValue; + if (data.class === "Symbol") { + strictEqual( + value.type, + "symbol", + "The wrapped value grip has symbol type." + ); + strictEqual( + value.name, + data.name, + "The wrapped value grip has the proper name." + ); + } else { + strictEqual(value, data.value, "The wrapped value is the primitive one."); + } +} diff --git a/devtools/server/tests/xpcshell/test_objectgrips-20.js b/devtools/server/tests/xpcshell/test_objectgrips-20.js new file mode 100644 index 0000000000..5027ca31a7 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-20.js @@ -0,0 +1,387 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that onEnumProperties returns the expected data +// when passing `ignoreNonIndexedProperties` and `ignoreIndexedProperties` options +// with various objects. (See Bug 1403065) + +const DO_NOT_CHECK_VALUE = Symbol(); + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +add_task( + threadFrontTest(async ({ threadFront, debuggee, client }) => { + debuggee.eval( + function stopMe(arg1) { + debugger; + }.toString() + ); + + const testCases = [ + { + evaledObject: { a: 10 }, + expectedIndexedProperties: [], + expectedNonIndexedProperties: [["a", 10]], + }, + { + evaledObject: { length: 10 }, + expectedIndexedProperties: [], + expectedNonIndexedProperties: [["length", 10]], + }, + { + evaledObject: { a: 10, 0: "indexed" }, + expectedIndexedProperties: [["0", "indexed"]], + expectedNonIndexedProperties: [["a", 10]], + }, + { + evaledObject: { 1: 1, length: 42, a: 10 }, + expectedIndexedProperties: [["1", 1]], + expectedNonIndexedProperties: [ + ["length", 42], + ["a", 10], + ], + }, + { + evaledObject: { 1: 1, length: 2.34, a: 10 }, + expectedIndexedProperties: [["1", 1]], + expectedNonIndexedProperties: [ + ["length", 2.34], + ["a", 10], + ], + }, + { + evaledObject: { 1: 1, length: -0, a: 10 }, + expectedIndexedProperties: [["1", 1]], + expectedNonIndexedProperties: [ + ["length", -0], + ["a", 10], + ], + }, + { + evaledObject: { 1: 1, length: -10, a: 10 }, + expectedIndexedProperties: [["1", 1]], + expectedNonIndexedProperties: [ + ["length", -10], + ["a", 10], + ], + }, + { + evaledObject: { 1: 1, length: true, a: 10 }, + expectedIndexedProperties: [["1", 1]], + expectedNonIndexedProperties: [ + ["length", true], + ["a", 10], + ], + }, + { + evaledObject: { 1: 1, length: null, a: 10 }, + expectedIndexedProperties: [["1", 1]], + expectedNonIndexedProperties: [ + ["length", DO_NOT_CHECK_VALUE], + ["a", 10], + ], + }, + { + evaledObject: { 1: 1, length: Math.pow(2, 53), a: 10 }, + expectedIndexedProperties: [["1", 1]], + expectedNonIndexedProperties: [ + ["length", 9007199254740992], + ["a", 10], + ], + }, + { + evaledObject: { 1: 1, length: "fake", a: 10 }, + expectedIndexedProperties: [["1", 1]], + expectedNonIndexedProperties: [ + ["length", "fake"], + ["a", 10], + ], + }, + { + evaledObject: { 1: 1, length: Infinity, a: 10 }, + expectedIndexedProperties: [["1", 1]], + expectedNonIndexedProperties: [ + ["length", DO_NOT_CHECK_VALUE], + ["a", 10], + ], + }, + { + evaledObject: { 0: 0, length: 0 }, + expectedIndexedProperties: [["0", 0]], + expectedNonIndexedProperties: [["length", 0]], + }, + { + evaledObject: { 0: 0, 1: 1, length: 1 }, + expectedIndexedProperties: [ + ["0", 0], + ["1", 1], + ], + expectedNonIndexedProperties: [["length", 1]], + }, + { + evaledObject: { length: 0 }, + expectedIndexedProperties: [], + expectedNonIndexedProperties: [["length", 0]], + }, + { + evaledObject: { 1: 1 }, + expectedIndexedProperties: [["1", 1]], + expectedNonIndexedProperties: [], + }, + { + evaledObject: { a: 1, [2 ** 32 - 2]: 2, [2 ** 32 - 1]: 3 }, + expectedIndexedProperties: [["4294967294", 2]], + expectedNonIndexedProperties: [ + ["a", 1], + ["4294967295", 3], + ], + }, + { + evaledObject: `(() => { + x = [12, 42]; + x.foo = 90; + return x; + })()`, + expectedIndexedProperties: [ + ["0", 12], + ["1", 42], + ], + expectedNonIndexedProperties: [ + ["length", 2], + ["foo", 90], + ], + }, + { + evaledObject: `(() => { + x = [12, 42]; + x.length = 3; + return x; + })()`, + expectedIndexedProperties: [ + ["0", 12], + ["1", 42], + ["2", undefined], + ], + expectedNonIndexedProperties: [["length", 3]], + }, + { + evaledObject: `(() => { + x = [12, 42]; + x.length = 1; + return x; + })()`, + expectedIndexedProperties: [["0", 12]], + expectedNonIndexedProperties: [["length", 1]], + }, + { + evaledObject: `(() => { + x = [, 42,,]; + x.foo = 90; + return x; + })()`, + expectedIndexedProperties: [ + ["0", undefined], + ["1", 42], + ["2", undefined], + ], + expectedNonIndexedProperties: [ + ["length", 3], + ["foo", 90], + ], + }, + { + evaledObject: `(() => { + x = Array(2); + x.foo = "bar"; + x.bar = "foo"; + return x; + })()`, + expectedIndexedProperties: [ + ["0", undefined], + ["1", undefined], + ], + expectedNonIndexedProperties: [ + ["length", 2], + ["foo", "bar"], + ["bar", "foo"], + ], + }, + { + evaledObject: `(() => { + x = new Int8Array(new ArrayBuffer(2)); + x.foo = "bar"; + x.bar = "foo"; + return x; + })()`, + expectedIndexedProperties: [ + ["0", 0], + ["1", 0], + ], + expectedNonIndexedProperties: [ + ["foo", "bar"], + ["bar", "foo"], + ["length", 2], + ["buffer", DO_NOT_CHECK_VALUE], + ["byteLength", 2], + ["byteOffset", 0], + ], + }, + { + evaledObject: `(() => { + x = new Int8Array([1, 2]); + Object.defineProperty(x, 'length', {value: 0}); + return x; + })()`, + expectedIndexedProperties: [ + ["0", 1], + ["1", 2], + ], + expectedNonIndexedProperties: [ + ["length", 0], + ["buffer", DO_NOT_CHECK_VALUE], + ["byteLength", 2], + ["byteOffset", 0], + ], + }, + { + evaledObject: `(() => { + x = new Int32Array([1, 2]); + Object.setPrototypeOf(x, null); + return x; + })()`, + expectedIndexedProperties: [ + ["0", 1], + ["1", 2], + ], + expectedNonIndexedProperties: [], + }, + { + evaledObject: `(() => { + return new (class extends Int8Array {})([1, 2]); + })()`, + expectedIndexedProperties: [ + ["0", 1], + ["1", 2], + ], + expectedNonIndexedProperties: [ + ["length", 2], + ["buffer", DO_NOT_CHECK_VALUE], + ["byteLength", 2], + ["byteOffset", 0], + ], + }, + ]; + + for (const test of testCases) { + await test_object_grip(debuggee, client, threadFront, test); + } + }) +); + +async function test_object_grip( + debuggee, + dbgClient, + threadFront, + testData = {} +) { + const { + evaledObject, + expectedIndexedProperties, + expectedNonIndexedProperties, + } = testData; + + const packet = await executeOnNextTickAndWaitForPause(eval_code, threadFront); + + const [grip] = packet.frame.arguments; + + const objClient = threadFront.pauseGrip(grip); + + info(` + Check enumProperties response for + ${ + typeof evaledObject === "string" + ? evaledObject + : JSON.stringify(evaledObject) + } + `); + + // Checks the result of enumProperties. + let response = await objClient.enumProperties({ + ignoreNonIndexedProperties: true, + }); + await check_enum_properties(response, expectedIndexedProperties); + + response = await objClient.enumProperties({ + ignoreIndexedProperties: true, + }); + await check_enum_properties(response, expectedNonIndexedProperties); + + await threadFront.resume(); + + function eval_code() { + // Be sure to run debuggee code in its own HTML 'task', so that when we call + // the onDebuggerStatement hook, the test's own microtasks don't get suspended + // along with the debuggee's. + do_timeout(0, () => { + debuggee.eval(` + stopMe(${ + typeof evaledObject === "string" + ? evaledObject + : JSON.stringify(evaledObject) + }); + `); + }); + } +} + +async function check_enum_properties(iterator, expected = []) { + equal( + iterator.count, + expected.length, + "iterator.count has the expected value" + ); + + info("Check iterator.slice response for all properties"); + const sliceResponse = await iterator.slice(0, iterator.count); + ok( + sliceResponse && + Object.getOwnPropertyNames(sliceResponse).includes("ownProperties"), + "The response object has an ownProperties property" + ); + + const { ownProperties } = sliceResponse; + const names = Object.getOwnPropertyNames(ownProperties); + equal( + names.length, + expected.length, + "The response has the expected number of properties" + ); + for (let i = 0; i < names.length; i++) { + const name = names[i]; + const [key, value] = expected[i]; + equal(name, key, "Property has the expected name"); + const property = ownProperties[name]; + + if (value === DO_NOT_CHECK_VALUE) { + return; + } + + if (value === undefined) { + equal( + property, + undefined, + `Response has no value for the "${key}" property` + ); + } else { + const propValue = property.hasOwnProperty("value") + ? property.value + : property.getterValue; + equal(propValue, value, `Property "${key}" has the expected value`); + } + } +} diff --git a/devtools/server/tests/xpcshell/test_objectgrips-21.js b/devtools/server/tests/xpcshell/test_objectgrips-21.js new file mode 100644 index 0000000000..88296f7786 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-21.js @@ -0,0 +1,396 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); + +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +// Run test_unsafe_grips twice, one against a system principal debuggee +// and another time with a null principal debuggee + +// The following tests work like this: +// - The specified code is evaluated in a system principal. +// `Cu`, `systemPrincipal` and `Services` are provided as global variables. +// - The resulting object is debugged in a system or null principal debuggee, +// depending on in which list the test is placed. +// It is tested according to the specified test parameters. +// - An ordinary object that inherits from the resulting one is also debugged. +// This is just to check that it can be normally debugged even with an unsafe +// object in the prototype. The specified test parameters do not apply. + +// The following tests are defined via properties with the following defaults. +const defaults = { + // The class of the grip. + class: "Restricted", + + // The stringification of the object + string: "", + + // Whether the object (not its grip) has class "Function". + isFunction: false, + + // Whether the grip has a preview property. + hasPreview: true, + + // Code that assigns the object to be tested into the obj variable. + code: "var obj = {}", + + // The type of the grip of the prototype. + protoType: "null", + + // Whether the object has some own string properties. + hasOwnPropertyNames: false, + + // Whether the object has some own symbol properties. + hasOwnPropertySymbols: false, + + // The descriptor obtained when retrieving property "x" or Symbol("x"). + property: undefined, + + // Code evaluated after the test, whose result is expected to be true. + afterTest: "true == true", +}; + +// The following tests use a system principal debuggee. +const systemPrincipalTests = [ + { + // Dead objects throw a TypeError when accessing properties. + class: "DeadObject", + string: "<dead object>", + code: ` + var obj = Cu.Sandbox(null); + Cu.nukeSandbox(obj); + `, + property: descriptor({ value: "TypeError" }), + }, + { + // This proxy checks that no trap runs (using a second proxy as the handler + // there is no need to maintain a list of all possible traps). + class: "Proxy", + string: "<proxy>", + code: ` + var trapDidRun = false; + var obj = new Proxy({}, new Proxy({}, {get: (_, trap) => { + trapDidRun = true; + throw new Error("proxy trap '" + trap + "' was called."); + }})); + `, + afterTest: "trapDidRun === false", + }, + { + // Like the previous test, but now the proxy has a Function class. + class: "Proxy", + string: "<proxy>", + isFunction: true, + code: ` + var trapDidRun = false; + var obj = new Proxy(function(){}, new Proxy({}, {get: (_, trap) => { + trapDidRun = true; + throw new Error("proxy trap '" + trap + "' was called.(function)"); + }})); + `, + afterTest: "trapDidRun === false", + }, + { + // Invisisible-to-debugger objects can't be unwrapped, so we don't know if + // they are proxies. Thus they shouldn't be accessed. + class: "InvisibleToDebugger: Array", + string: "<invisibleToDebugger>", + hasPreview: false, + code: ` + var s = Cu.Sandbox(systemPrincipal, {invisibleToDebugger: true}); + var obj = s.eval("[1, 2, 3]"); + `, + }, + { + // Like the previous test, but now the object has a Function class. + class: "InvisibleToDebugger: Function", + string: "<invisibleToDebugger>", + isFunction: true, + hasPreview: false, + code: ` + var s = Cu.Sandbox(systemPrincipal, {invisibleToDebugger: true}); + var obj = s.eval("(function func(arg){})"); + `, + }, + { + // Cu.Sandbox is a WrappedNative that throws when accessing properties. + class: "nsXPCComponents_utils_Sandbox", + string: "[object nsXPCComponents_utils_Sandbox]", + code: `var obj = Cu.Sandbox;`, + protoType: "object", + }, +]; + +// The following tests run code in a system principal, but the resulting object +// is debugged in a null principal. +const nullPrincipalTests = [ + { + // The null principal gets undefined when attempting to access properties. + string: "[object Object]", + code: `var obj = {x: -1};`, + }, + { + // For arrays it's an error instead of undefined. + string: "[object Object]", + code: `var obj = [1, 2, 3];`, + property: descriptor({ value: "Error" }), + }, + { + // For functions it's also an error. + string: "function func(arg){}", + isFunction: true, + hasPreview: false, + code: `var obj = function func(arg){};`, + property: descriptor({ value: "Error" }), + }, + { + // Check that no proxy trap runs. + string: "[object Object]", + code: ` + var trapDidRun = false; + var obj = new Proxy([], new Proxy({}, {get: (_, trap) => { + trapDidRun = true; + throw new Error("proxy trap '" + trap + "' was called."); + }})); + `, + property: descriptor({ value: "Error" }), + afterTest: `trapDidRun === false`, + }, + { + // Like the previous test, but now the object is a callable Proxy. + string: "function () {\n [native code]\n}", + isFunction: true, + hasPreview: false, + code: ` + var trapDidRun = false; + var obj = new Proxy(function(){}, new Proxy({}, {get: (_, trap) => { + trapDidRun = true; + throw new Error("proxy trap '" + trap + "' was called."); + }})); + `, + property: descriptor({ value: "Error" }), + afterTest: `trapDidRun === false`, + }, + { + // Cross-origin Window objects do expose some properties and have a preview. + string: "[object Object]", + code: `var obj = Services.appShell.createWindowlessBrowser().document.defaultView;`, + hasOwnPropertyNames: true, + hasOwnPropertySymbols: true, + property: descriptor({ value: "SecurityError" }), + previewUrl: "about:blank", + }, + { + // Cross-origin Location objects do expose some properties and have a preview. + string: "[object Object]", + code: `var obj = Services.appShell.createWindowlessBrowser().document.defaultView + .location;`, + hasOwnPropertyNames: true, + hasOwnPropertySymbols: true, + property: descriptor({ value: "SecurityError" }), + }, +]; + +function descriptor(descr) { + return Object.assign( + { + configurable: false, + writable: false, + enumerable: false, + value: undefined, + }, + descr + ); +} + +async function test_unsafe_grips( + { threadFront, debuggee, isWorkerServer }, + tests +) { + debuggee.eval( + function stopMe(arg1, arg2) { + debugger; + }.toString() + ); + + for (let data of tests) { + data = { ...defaults, ...data }; + + // Run the code and test the results. + const sandbox = Cu.Sandbox(systemPrincipal); + Object.assign(sandbox, { Services, systemPrincipal, Cu }); + sandbox.eval(data.code); + debuggee.obj = sandbox.obj; + const inherits = `Object.create(obj, { + x: {value: 1}, + [Symbol.for("x")]: {value: 2} + })`; + + const packet = await executeOnNextTickAndWaitForPause( + () => debuggee.eval(`stopMe(obj, ${inherits});`), + threadFront + ); + + const [objGrip, inheritsGrip] = packet.frame.arguments; + for (const grip of [objGrip, inheritsGrip]) { + const isUnsafe = grip === objGrip; + // If `isUnsafe` is true, the parameters in `data` will be used to assert + // against `objGrip`, the grip of the object `obj` created by the test. + // Otherwise, the grip will refer to `inherits`, an ordinary object which + // inherits from `obj`. Then all checks are hardcoded because in every test + // all methods are expected to work the same on `inheritsGrip`. + check_grip(grip, data, isUnsafe, isWorkerServer); + + const objClient = threadFront.pauseGrip(grip); + let response, slice; + + response = await objClient.getPrototypeAndProperties(); + check_properties(response.ownProperties, data, isUnsafe); + check_symbols(response.ownSymbols, data, isUnsafe); + check_prototype(response.prototype, data, isUnsafe, isWorkerServer); + + response = await objClient.enumProperties({ + ignoreIndexedProperties: true, + }); + slice = await response.slice(0, response.count); + check_properties(slice.ownProperties, data, isUnsafe); + + response = await objClient.enumProperties({}); + slice = await response.slice(0, response.count); + check_properties(slice.ownProperties, data, isUnsafe); + + response = await objClient.getProperty("x"); + check_property(response.descriptor, data, isUnsafe); + + response = await objClient.enumSymbols(); + slice = await response.slice(0, response.count); + check_symbol_names(slice.ownSymbols, data, isUnsafe); + + response = await objClient.getProperty(Symbol.for("x")); + check_symbol(response.descriptor, data, isUnsafe); + + response = await objClient.getPrototype(); + check_prototype(response.prototype, data, isUnsafe, isWorkerServer); + } + + await threadFront.resume(); + + ok(sandbox.eval(data.afterTest), "Check after test passes"); + } +} + +function check_grip(grip, data, isUnsafe, isWorkerServer) { + if (isUnsafe) { + strictEqual(grip.class, data.class, "The grip has the proper class."); + strictEqual("preview" in grip, data.hasPreview, "Check preview presence."); + // preview.url isn't populated on worker server. + if (data.previewUrl && !isWorkerServer) { + console.trace(); + strictEqual( + grip.preview.url, + data.previewUrl, + `Check preview.url for "${data.code}".` + ); + } + } else { + strictEqual(grip.class, "Object", "The grip has 'Object' class."); + ok("preview" in grip, "The grip has a preview."); + } +} + +function check_properties(props, data, isUnsafe) { + const propNames = Reflect.ownKeys(props); + check_property_names(propNames, data, isUnsafe); + if (isUnsafe) { + deepEqual(props.x, undefined, "The property does not exist."); + } else { + strictEqual(props.x.value, 1, "The property has the right value."); + } +} + +function check_property_names(props, data, isUnsafe) { + if (isUnsafe) { + strictEqual( + !!props.length, + data.hasOwnPropertyNames, + "Check presence of own string properties." + ); + } else { + strictEqual(props.length, 1, "1 own property was retrieved."); + strictEqual(props[0], "x", "The property has the right name."); + } +} + +function check_property(descr, data, isUnsafe) { + if (isUnsafe) { + deepEqual(descr, data.property, "Got the right property descriptor."); + } else { + strictEqual(descr.value, 1, "The property has the right value."); + } +} + +function check_symbols(symbols, data, isUnsafe) { + check_symbol_names(symbols, data, isUnsafe); + if (!isUnsafe) { + check_symbol(symbols[0].descriptor, data, isUnsafe); + } +} + +function check_symbol_names(props, data, isUnsafe) { + if (isUnsafe) { + strictEqual( + !!props.length, + data.hasOwnPropertySymbols, + "Check presence of own symbol properties." + ); + } else { + strictEqual(props.length, 1, "1 own symbol property was retrieved."); + strictEqual(props[0].name, "Symbol(x)", "The symbol has the right name."); + } +} + +function check_symbol(descr, data, isUnsafe) { + if (isUnsafe) { + deepEqual( + descr, + data.property, + "Got the right symbol property descriptor." + ); + } else { + strictEqual(descr.value, 2, "The symbol property has the right value."); + } +} + +function check_prototype(proto, data, isUnsafe, isWorkerServer) { + const protoGrip = proto && proto.getGrip ? proto.getGrip() : proto; + if (isUnsafe) { + deepEqual(protoGrip.type, data.protoType, "Got the right prototype type."); + } else { + check_grip(protoGrip, data, true, isWorkerServer); + } +} + +// threadFrontTest uses systemPrincipal by default, but let's be explicit here. +add_task( + threadFrontTest( + options => { + return test_unsafe_grips(options, systemPrincipalTests, "system"); + }, + { principal: systemPrincipal } + ) +); + +const nullPrincipal = Services.scriptSecurityManager.createNullPrincipal({}); +add_task( + threadFrontTest( + options => { + return test_unsafe_grips(options, nullPrincipalTests, "null"); + }, + { principal: nullPrincipal } + ) +); diff --git a/devtools/server/tests/xpcshell/test_objectgrips-22.js b/devtools/server/tests/xpcshell/test_objectgrips-22.js new file mode 100644 index 0000000000..34264f5534 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-22.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const [grip] = packet.frame.arguments; + const objClient = threadFront.pauseGrip(grip); + const iterator = await objClient.enumSymbols(); + const { ownSymbols } = await iterator.slice(0, iterator.count); + + strictEqual(ownSymbols.length, 1, "There is 1 symbol property."); + const { name, descriptor } = ownSymbols[0]; + strictEqual(name, "Symbol(sym)", "Got right symbol name."); + deepEqual( + descriptor, + { + configurable: false, + enumerable: false, + writable: false, + value: 1, + }, + "Got right property descriptor." + ); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + debuggee.eval( + function stopMe(arg1) { + debugger; + }.toString() + ); + debuggee.eval( + `stopMe(Object.defineProperty({}, Symbol("sym"), {value: 1}));` + ); +} diff --git a/devtools/server/tests/xpcshell/test_objectgrips-23.js b/devtools/server/tests/xpcshell/test_objectgrips-23.js new file mode 100644 index 0000000000..b44beb2c2d --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-23.js @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that ES6 classes grip have the expected properties. + +"use strict"; + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const [grip] = packet.frame.arguments; + strictEqual( + grip.class, + "Function", + `Grip has expected value for "class" property` + ); + strictEqual( + grip.isClassConstructor, + true, + `Grip has expected value for "isClassConstructor" property` + ); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + debuggee.eval(` + class MyClass {}; + stopMe(MyClass); + + function stopMe(arg1) { + debugger; + } + `); +} diff --git a/devtools/server/tests/xpcshell/test_objectgrips-24.js b/devtools/server/tests/xpcshell/test_objectgrips-24.js new file mode 100644 index 0000000000..9d541c108d --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-24.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that ES6 classes grip have the expected properties. + +"use strict"; + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + debuggee.eval( + function stopMe() { + debugger; + }.toString() + ); + + const tests = [ + { + fn: `function(){}`, + isAsync: false, + isGenerator: false, + }, + { + fn: `async function(){}`, + isAsync: true, + isGenerator: false, + }, + { + fn: `function *(){}`, + isAsync: false, + isGenerator: true, + }, + { + fn: `async function *(){}`, + isAsync: true, + isGenerator: true, + }, + ]; + + for (const { fn, isAsync, isGenerator } of tests) { + const packet = await executeOnNextTickAndWaitForPause( + () => debuggee.eval(`stopMe(${fn})`), + threadFront + ); + const [grip] = packet.frame.arguments; + strictEqual(grip.class, "Function"); + strictEqual(grip.isAsync, isAsync); + strictEqual(grip.isGenerator, isGenerator); + + await threadFront.resume(); + } + }) +); diff --git a/devtools/server/tests/xpcshell/test_objectgrips-25.js b/devtools/server/tests/xpcshell/test_objectgrips-25.js new file mode 100644 index 0000000000..f80572bb19 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-25.js @@ -0,0 +1,131 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test object with private properties (preview + enumPrivateProperties) + +"use strict"; + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +function evalCode(debuggee) { + debuggee.eval( + function stopMe(obj) { + debugger; + }.toString() + ); + debuggee.eval(` + class MyClass { + constructor(name, password) { + this.name = name; + this.#password = password; + } + + #password; + #salt = "sEcr3t"; + #getSaltedPassword() { + return this.#password + this.#salt; + } + } + + stopMe(new MyClass("Susie", "p4$$w0rD")); + `); +} + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const [grip] = packet.frame.arguments; + + let { privateProperties } = grip.preview; + strictEqual( + privateProperties.length, + 2, + "There is 2 private properties in the grip preview" + ); + let [password, salt] = privateProperties; + + strictEqual( + password.name, + "#password", + "Got expected name for #password private property in preview" + ); + deepEqual( + password.descriptor, + { + configurable: true, + enumerable: false, + writable: true, + value: "p4$$w0rD", + }, + "Got expected property descriptor for #password in preview" + ); + + strictEqual( + salt.name, + "#salt", + "Got expected name for #salt private property in preview" + ); + deepEqual( + salt.descriptor, + { + configurable: true, + enumerable: false, + writable: true, + value: "sEcr3t", + }, + "Got expected property descriptor for #salt in preview" + ); + + const objClient = threadFront.pauseGrip(grip); + const iterator = await objClient.enumPrivateProperties(); + ({ privateProperties } = await iterator.slice(0, iterator.count)); + + strictEqual( + privateProperties.length, + 2, + "enumPrivateProperties returned 2 private properties." + ); + [password, salt] = privateProperties; + + strictEqual( + password.name, + "#password", + "Got expected name for #password private property via enumPrivateProperties" + ); + deepEqual( + password.descriptor, + { + configurable: true, + enumerable: false, + writable: true, + value: "p4$$w0rD", + }, + "Got expected property descriptor for #password via enumPrivateProperties" + ); + + strictEqual( + salt.name, + "#salt", + "Got expected name for #salt private property via enumPrivateProperties" + ); + deepEqual( + salt.descriptor, + { + configurable: true, + enumerable: false, + writable: true, + value: "sEcr3t", + }, + "Got expected property descriptor for #salt via enumPrivateProperties" + ); + + await threadFront.resume(); + }) +); diff --git a/devtools/server/tests/xpcshell/test_objectgrips-fn-apply-01.js b/devtools/server/tests/xpcshell/test_objectgrips-fn-apply-01.js new file mode 100644 index 0000000000..f576f16a5e --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-fn-apply-01.js @@ -0,0 +1,117 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const arg1 = packet.frame.arguments[0]; + Assert.equal(arg1.class, "Object"); + + const objectFront = threadFront.pauseGrip(arg1); + + const obj1 = ( + await objectFront.getPropertyValue("obj1", null) + ).value.return.getGrip(); + const obj2 = ( + await objectFront.getPropertyValue("obj2", null) + ).value.return.getGrip(); + + info(`Retrieve "context" function reference`); + const context = (await objectFront.getPropertyValue("context", null)).value + .return; + info(`Retrieve "sum" function reference`); + const sum = (await objectFront.getPropertyValue("sum", null)).value.return; + info(`Retrieve "error" function reference`); + const error = (await objectFront.getPropertyValue("error", null)).value + .return; + + assert_response(await context.apply(obj1, [obj1]), { + return: "correct context", + }); + assert_response(await context.apply(obj2, [obj2]), { + return: "correct context", + }); + assert_response(await context.apply(obj1, [obj2]), { + return: "wrong context", + }); + assert_response(await context.apply(obj2, [obj1]), { + return: "wrong context", + }); + // eslint-disable-next-line no-useless-call + assert_response(await sum.apply(null, [1, 2, 3, 4, 5, 6, 7]), { + return: 1 + 2 + 3 + 4 + 5 + 6 + 7, + }); + // eslint-disable-next-line no-useless-call + assert_response(await error.apply(null, []), { + throw: "an error", + }); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + debuggee.eval( + function stopMe(arg1) { + debugger; + }.toString() + ); + + debuggee.eval(` + stopMe({ + obj1: {}, + obj2: {}, + context(arg) { + return this === arg ? "correct context" : "wrong context"; + }, + sum(...parts) { + return parts.reduce((acc, v) => acc + v, 0); + }, + error() { + throw "an error"; + }, + }); + `); +} + +function assert_response({ value }, expected) { + assert_completion(value, expected); +} + +function assert_completion(value, expected) { + if (expected && "return" in expected) { + assert_value(value.return, expected.return); + } + if (expected && "throw" in expected) { + assert_value(value.throw, expected.throw); + } + if (!expected) { + assert_value(value, expected); + } +} + +function assert_value(actual, expected) { + Assert.equal(typeof actual, typeof expected); + + if (typeof expected === "object") { + // Note: We aren't using deepEqual here because we're only doing a cursory + // check of a few properties, not a full comparison of the result, since + // the full outputs includes stuff like preview info that we don't need. + for (const key of Object.keys(expected)) { + assert_value(actual[key], expected[key]); + } + } else { + Assert.equal(actual, expected); + } +} diff --git a/devtools/server/tests/xpcshell/test_objectgrips-fn-apply-02.js b/devtools/server/tests/xpcshell/test_objectgrips-fn-apply-02.js new file mode 100644 index 0000000000..743286281c --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-fn-apply-02.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const arg1 = packet.frame.arguments[0]; + Assert.equal(arg1.class, "Object"); + + await threadFront.pauseGrip(arg1).threadGrip(); + const obj = arg1; + await threadFront.resume(); + + const objectFront = threadFront.pauseGrip(obj); + + const method = (await objectFront.getPropertyValue("method", null)).value + .return; + + const methodCalled = method.apply(obj, []); + + // Ensure that we actually paused at the `debugger;` line. + const packet2 = await waitForPause(threadFront); + Assert.equal(packet2.frame.where.line, 4); + Assert.equal(packet2.frame.where.column, 8); + + await threadFront.resume(); + await methodCalled; + }) +); + +function evalCode(debuggee) { + debuggee.eval( + function stopMe(arg1) { + debugger; + }.toString() + ); + + debuggee.eval(` + stopMe({ + method(){ + debugger; + }, + }); + `); +} diff --git a/devtools/server/tests/xpcshell/test_objectgrips-fn-apply-03.js b/devtools/server/tests/xpcshell/test_objectgrips-fn-apply-03.js new file mode 100644 index 0000000000..6a3e919661 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-fn-apply-03.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const arg1 = packet.frame.arguments[0]; + Assert.equal(arg1.class, "Object"); + + await threadFront.pauseGrip(arg1).threadGrip(); + const obj = arg1; + + const objectFront = threadFront.pauseGrip(obj); + + const method = (await objectFront.getPropertyValue("method", null)).value + .return; + + try { + await method.apply(obj, []); + Assert.ok(false, "expected exception"); + } catch (err) { + Assert.ok(!!err.message.match(/debugee object is not callable/)); + } + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + debuggee.eval( + function stopMe(arg1) { + debugger; + }.toString() + ); + + debuggee.eval(` + stopMe({ + method: {}, + }); + `); +} diff --git a/devtools/server/tests/xpcshell/test_objectgrips-nested-promise.js b/devtools/server/tests/xpcshell/test_objectgrips-nested-promise.js new file mode 100644 index 0000000000..b60b7328c2 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-nested-promise.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const [grip1, grip2] = packet.frame.arguments; + strictEqual(grip1.class, "Promise", "promise1 has a promise grip."); + strictEqual(grip2.class, "Promise", "promise2 has a promise grip."); + + const objClient1 = threadFront.pauseGrip(grip1); + const objClient2 = threadFront.pauseGrip(grip2); + const { promiseState: state1 } = await objClient1.getPromiseState(); + const { promiseState: state2 } = await objClient2.getPromiseState(); + + strictEqual(state1.state, "fulfilled", "promise1 was fulfilled."); + strictEqual(state1.value, objClient2, "promise1 fulfilled with promise2."); + ok(!state1.hasOwnProperty("reason"), "promise1 has no rejection reason."); + + strictEqual(state2.state, "rejected", "promise2 was rejected."); + strictEqual(state2.reason, objClient1, "promise2 rejected with promise1."); + ok(!state2.hasOwnProperty("value"), "promise2 has no resolution value."); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + debuggee.eval( + function stopMe(arg) { + debugger; + }.toString() + ); + + debuggee.eval(` + var resolve; + var promise1 = new Promise(r => {resolve = r}); + Object.setPrototypeOf(promise1, null); + var promise2 = Promise.reject(promise1); + promise2.catch(() => {}); + Object.setPrototypeOf(promise2, null); + resolve(promise2); + stopMe(promise1, promise2); + `); +} diff --git a/devtools/server/tests/xpcshell/test_objectgrips-nested-proxy.js b/devtools/server/tests/xpcshell/test_objectgrips-nested-proxy.js new file mode 100644 index 0000000000..5b0667c055 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-nested-proxy.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const [grip] = packet.frame.arguments; + const objClient = threadFront.pauseGrip(grip); + const { proxyTarget, proxyHandler } = await objClient.getProxySlots(); + + strictEqual(grip.class, "Proxy", "Its a proxy grip."); + strictEqual( + proxyTarget.getGrip().class, + "Proxy", + "The target is also a proxy." + ); + strictEqual( + proxyHandler.getGrip().class, + "Proxy", + "The handler is also a proxy." + ); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + debuggee.eval( + function stopMe(arg) { + debugger; + }.toString() + ); + + debuggee.eval(` + var proxy = new Proxy({}, {}); + for (let i = 0; i < 1e5; ++i) + proxy = new Proxy(proxy, proxy); + stopMe(proxy); + `); +} diff --git a/devtools/server/tests/xpcshell/test_objectgrips-property-value-01.js b/devtools/server/tests/xpcshell/test_objectgrips-property-value-01.js new file mode 100644 index 0000000000..69da96a741 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-property-value-01.js @@ -0,0 +1,148 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const arg1 = packet.frame.arguments[0]; + Assert.equal(arg1.class, "Object"); + + const objFront = threadFront.pauseGrip(arg1); + + const expectedValues = { + stringProp: { + return: "a value", + }, + stringNormal: { + return: "a value", + }, + stringAbrupt: { + throw: "a value", + }, + objectNormal: { + return: { + _grip: { + type: "object", + class: "Object", + ownPropertyLength: 1, + preview: { + kind: "Object", + ownProperties: { + prop: { + value: 4, + }, + }, + }, + }, + }, + }, + objectAbrupt: { + throw: { + _grip: { + type: "object", + class: "Object", + ownPropertyLength: 1, + preview: { + kind: "Object", + ownProperties: { + prop: { + value: 4, + }, + }, + }, + }, + }, + }, + context: { + return: "correct context", + }, + method: { + return: { + _grip: { + type: "object", + class: "Function", + name: "method", + }, + }, + }, + }; + + for (const [key, expected] of Object.entries(expectedValues)) { + const { value } = await objFront.getPropertyValue(key, null); + assert_completion(value, expected); + } + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + debuggee.eval( + function stopMe(arg1) { + debugger; + }.toString() + ); + + debuggee.eval(` + var obj = { + stringProp: "a value", + get stringNormal(){ + return "a value"; + }, + get stringAbrupt() { + throw "a value"; + }, + get objectNormal() { + return { prop: 4 }; + }, + get objectAbrupt() { + throw { prop: 4 }; + }, + get context(){ + return this === obj ? "correct context" : "wrong context"; + }, + method() { + return "a value"; + }, + }; + stopMe(obj); + `); +} + +function assert_completion(value, expected) { + if (expected && "return" in expected) { + assert_value(value.return, expected.return); + } + if (expected && "throw" in expected) { + assert_value(value.throw, expected.throw); + } + if (!expected) { + assert_value(value, expected); + } +} + +function assert_value(actual, expected) { + Assert.equal(typeof actual, typeof expected); + + if (typeof expected === "object") { + // Note: We aren't using deepEqual here because we're only doing a cursory + // check of a few properties, not a full comparison of the result, since + // the full outputs includes stuff like preview info that we don't need. + for (const key of Object.keys(expected)) { + assert_value(actual[key], expected[key]); + } + } else { + Assert.equal(actual, expected); + } +} diff --git a/devtools/server/tests/xpcshell/test_objectgrips-property-value-02.js b/devtools/server/tests/xpcshell/test_objectgrips-property-value-02.js new file mode 100644 index 0000000000..bc7337128c --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-property-value-02.js @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const arg1 = packet.frame.arguments[0]; + Assert.equal(arg1.class, "Object"); + + const obj = threadFront.pauseGrip(arg1); + await obj.threadGrip(); + + const objClient = obj; + await threadFront.resume(); + + const objClientCalled = objClient.getPropertyValue("prop", null); + + // Ensure that we actually paused at the `debugger;` line. + const packet2 = await waitForPause(threadFront); + Assert.equal(packet2.frame.where.line, 4); + Assert.equal(packet2.frame.where.column, 8); + + await threadFront.resume(); + await objClientCalled; + }) +); + +function evalCode(debuggee) { + debuggee.eval( + function stopMe(arg1) { + debugger; + }.toString() + ); + + debuggee.eval(` + stopMe({ + get prop(){ + debugger; + }, + }); + `); +} diff --git a/devtools/server/tests/xpcshell/test_objectgrips-property-value-03.js b/devtools/server/tests/xpcshell/test_objectgrips-property-value-03.js new file mode 100644 index 0000000000..e9b130db79 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-property-value-03.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const { frame } = packet; + try { + const grips = frame.arguments; + const objClient = threadFront.pauseGrip(grips[0]); + const classes = [ + "Object", + "Object", + "Array", + "Boolean", + "Number", + "String", + ]; + for (const [i, grip] of grips.entries()) { + Assert.equal(grip.class, classes[i]); + await check_getter(objClient, grip.actor, i); + } + await check_getter(objClient, null, 0); + await check_getter(objClient, "invalid receiver actorId", 0); + } finally { + await threadFront.resume(); + } + }) +); + +function evalCode(debuggee) { + debuggee.eval( + function stopMe() { + debugger; + }.toString() + ); + + debuggee.eval(` + var obj = { + get getter() { + return objects.indexOf(this); + }, + }; + var objects = [obj, {}, [], new Boolean(), new Number(), new String()]; + stopMe(...objects); + `); +} + +async function check_getter(objClient, receiverId, expected) { + const { value } = await objClient.getPropertyValue("getter", receiverId); + Assert.equal(value.return, expected); +} diff --git a/devtools/server/tests/xpcshell/test_objectgrips-sparse-array.js b/devtools/server/tests/xpcshell/test_objectgrips-sparse-array.js new file mode 100644 index 0000000000..76a6b32f4b --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-sparse-array.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const [grip] = packet.frame.arguments; + await threadFront.resume(); + + strictEqual(grip.class, "Array", "The grip has an Array class"); + + const { items } = grip.preview; + strictEqual(items[0], null, "The empty slot has null as grip preview"); + deepEqual( + items[1], + { type: "undefined" }, + "The undefined value has grip value of type undefined" + ); + }) +); + +function evalCode(debuggee) { + debuggee.eval( + function stopMe(arr) { + debugger; + }.toString() + ); + debuggee.eval("stopMe([, undefined])"); +} diff --git a/devtools/server/tests/xpcshell/test_pause_exceptions-01.js b/devtools/server/tests/xpcshell/test_pause_exceptions-01.js new file mode 100644 index 0000000000..74bbae55c3 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_pause_exceptions-01.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that setting pauseOnExceptions to true will cause the debuggee to pause + * when an exception is thrown. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee, commands }) => { + await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + + await commands.threadConfigurationCommand.updateConfiguration({ + pauseOnExceptions: true, + ignoreCaughtExceptions: false, + }); + const packet = await resumeAndWaitForPause(threadFront); + Assert.equal(packet.why.type, "exception"); + Assert.equal(packet.why.exception, 42); + + await threadFront.resume(); + }) +); + +function evaluateTestCode(debuggee) { + /* eslint-disable no-throw-literal */ + // prettier-ignore + debuggee.eval("(" + function () { + function stopMe() { + debugger; + throw 42; + } + try { + stopMe(); + } catch (e) {} + } + ")()"); + /* eslint-enable no-throw-literal */ +} diff --git a/devtools/server/tests/xpcshell/test_pause_exceptions-02.js b/devtools/server/tests/xpcshell/test_pause_exceptions-02.js new file mode 100644 index 0000000000..00631b071f --- /dev/null +++ b/devtools/server/tests/xpcshell/test_pause_exceptions-02.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that setting pauseOnExceptions to true when the debugger isn't in a + * paused state will not cause the debuggee to pause when an exception is thrown. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee, commands }) => { + await commands.threadConfigurationCommand.updateConfiguration({ + pauseOnExceptions: true, + ignoreCaughtExceptions: false, + }); + + const packet = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + + Assert.equal(packet.why.type, "exception"); + Assert.equal(packet.why.exception, 42); + await threadFront.resume(); + }) +); + +function evaluateTestCode(debuggee) { + /* eslint-disable no-throw-literal */ + // prettier-ignore + debuggee.eval("(" + function () { // 1 + function stopMe() { // 2 + throw 42; // 3 + } // 4 + try { // 5 + stopMe(); // 6 + } catch (e) {} // 7 + } + ")()"); +} diff --git a/devtools/server/tests/xpcshell/test_pause_exceptions-03.js b/devtools/server/tests/xpcshell/test_pause_exceptions-03.js new file mode 100644 index 0000000000..4fb13f4cf9 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_pause_exceptions-03.js @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that setting pauseOnExceptions to true will cause the debuggee to pause + * when an exception is thrown. + */ + +add_task( + threadFrontTest( + async ({ threadFront, debuggee, commands }) => { + await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + + await commands.threadConfigurationCommand.updateConfiguration({ + pauseOnExceptions: true, + ignoreCaughtExceptions: false, + }); + await resume(threadFront); + const paused = await waitForPause(threadFront); + Assert.equal(paused.why.type, "exception"); + equal(paused.frame.where.line, 4, "paused at throw"); + + await resume(threadFront); + }, + { + // Bug 1508289, exception tests fails in worker scope + doNotRunWorker: true, + } + ) +); + +function evaluateTestCode(debuggee) { + // prettier-ignore + Cu.evalInSandbox( + ` // 1 + function stopMe() { // 2 + debugger; // 3 + throw 42; // 4 + } // 5 + try { // 6 + stopMe(); // 7 + } catch (e) {}`, // 8 + debuggee, + "1.8", + "test_pause_exceptions-03.js", + 1 + ); +} diff --git a/devtools/server/tests/xpcshell/test_pause_exceptions-04.js b/devtools/server/tests/xpcshell/test_pause_exceptions-04.js new file mode 100644 index 0000000000..6246b112e0 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_pause_exceptions-04.js @@ -0,0 +1,93 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { waitForTick } = require("resource://devtools/shared/DevToolsUtils.js"); + +/** + * Test that setting pauseOnExceptions to true and then to false will not cause + * the debuggee to pause when an exception is thrown. + */ + +add_task( + threadFrontTest( + async ({ threadFront, client, debuggee, commands }) => { + let onResume = null; + let packet = null; + + threadFront.once("paused", function (pkt) { + packet = pkt; + onResume = threadFront.resume(); + }); + + await commands.threadConfigurationCommand.updateConfiguration({ + pauseOnExceptions: true, + ignoreCaughtExceptions: true, + }); + + await evaluateTestCode(debuggee, "42"); + + await onResume; + + Assert.equal(!!packet, true); + Assert.equal(packet.why.type, "exception"); + Assert.equal(packet.why.exception, "42"); + packet = null; + + threadFront.once("paused", function (pkt) { + packet = pkt; + onResume = threadFront.resume(); + }); + + await commands.threadConfigurationCommand.updateConfiguration({ + pauseOnExceptions: false, + ignoreCaughtExceptions: true, + }); + + await evaluateTestCode(debuggee, "43"); + + // Test that the paused listener callback hasn't been called + // on the thrown error from dontStopMe() + Assert.equal(!!packet, false); + + await commands.threadConfigurationCommand.updateConfiguration({ + pauseOnExceptions: true, + ignoreCaughtExceptions: true, + }); + + await evaluateTestCode(debuggee, "44"); + + await onResume; + + // Test that the paused listener callback has been called + // on the thrown error from stopMeAgain() + Assert.equal(!!packet, true); + Assert.equal(packet.why.type, "exception"); + Assert.equal(packet.why.exception, "44"); + }, + { + // Bug 1508289, exception tests fails in worker scope + doNotRunWorker: true, + } + ) +); + +async function evaluateTestCode(debuggee, throwValue) { + await waitForTick(); + try { + // prettier-ignore + Cu.evalInSandbox( + ` // 1 + function stopMeAgain() { // 2 + throw ${throwValue}; // 3 + } // 4 + stopMeAgain(); // 5 + `, // 6 + debuggee, + "1.8", + "test_pause_exceptions-04.js", + 1 + ); + } catch (e) {} +} diff --git a/devtools/server/tests/xpcshell/test_pauselifetime-01.js b/devtools/server/tests/xpcshell/test_pauselifetime-01.js new file mode 100644 index 0000000000..db20a02521 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_pauselifetime-01.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that pause-lifetime grips go away correctly after a resume. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee, client }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + + const pauseActor = packet.actor; + + // Make a bogus request to the pause-lifetime actor. Should get + // unrecognized-packet-type (and not no-such-actor). + try { + await client.request({ to: pauseActor, type: "bogusRequest" }); + ok(false, "bogusRequest should throw"); + } catch (e) { + ok(true, "bogusRequest thrown"); + Assert.equal(e.error, "unrecognizedPacketType"); + } + + await threadFront.resume(); + + // Now that we've resumed, should get no-such-actor for the + // same request. + try { + await client.request({ to: pauseActor, type: "bogusRequest" }); + ok(false, "bogusRequest should throw"); + } catch (e) { + ok(true, "bogusRequest thrown"); + Assert.equal(e.error, "noSuchActor"); + } + }) +); + +function evaluateTestCode(debuggee) { + debuggee.eval( + "(" + + function () { + function stopMe() { + debugger; + } + stopMe(); + } + + ")()" + ); +} diff --git a/devtools/server/tests/xpcshell/test_pauselifetime-02.js b/devtools/server/tests/xpcshell/test_pauselifetime-02.js new file mode 100644 index 0000000000..e936df6177 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_pauselifetime-02.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that pause-lifetime grips go away correctly after a resume. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee, client }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + + const args = packet.frame.arguments; + const objActor = args[0].actor; + Assert.equal(args[0].class, "Object"); + Assert.ok(!!objActor); + + // Make a bogus request to the grip actor. Should get + // unrecognized-packet-type (and not no-such-actor). + try { + await client.request({ to: objActor, type: "bogusRequest" }); + ok(false, "bogusRequest should throw"); + } catch (e) { + ok(true, "bogusRequest thrown"); + Assert.equal(e.error, "unrecognizedPacketType"); + } + + await threadFront.resume(); + + // Now that we've resumed, should get no-such-actor for the + // same request. + try { + await client.request({ to: objActor, type: "bogusRequest" }); + ok(false, "bogusRequest should throw"); + } catch (e) { + ok(true, "bogusRequest thrown"); + Assert.equal(e.error, "noSuchActor"); + } + }) +); + +function evaluateTestCode(debuggee) { + debuggee.eval( + "(" + + function () { + function stopMe(obj) { + debugger; + } + stopMe({ foo: "bar" }); + } + + ")()" + ); +} diff --git a/devtools/server/tests/xpcshell/test_pauselifetime-03.js b/devtools/server/tests/xpcshell/test_pauselifetime-03.js new file mode 100644 index 0000000000..558ac8b910 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_pauselifetime-03.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that pause-lifetime grip clients are marked invalid after a resume. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee, client }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + + const args = packet.frame.arguments; + const objActor = args[0].actor; + Assert.equal(args[0].class, "Object"); + Assert.ok(!!objActor); + + const objectFront = threadFront.pauseGrip(args[0]); + Assert.ok(objectFront.valid); + + // Make a bogus request to the grip actor. Should get + // unrecognized-packet-type (and not no-such-actor). + try { + const objFront = client.getFrontByID(objActor); + await objFront.request({ to: objActor, type: "bogusRequest" }); + ok(false, "bogusRequest should throw"); + } catch (e) { + ok(true, "bogusRequest thrown"); + Assert.ok(!!e.message.match(/unrecognizedPacketType/)); + } + Assert.ok(objectFront.valid); + + await threadFront.resume(); + + // Now that we've resumed, should get no-such-actor for the + // same request. + try { + const objFront = client.getFrontByID(objActor); + await objFront.request({ to: objActor, type: "bogusRequest" }); + ok(false, "bogusRequest should throw"); + } catch (e) { + ok(true, "bogusRequest thrown"); + Assert.ok(!!e.message.match(/noSuchActor/)); + } + Assert.ok(!objectFront.valid); + }) +); + +function evaluateTestCode(debuggee) { + debuggee.eval( + "(" + + function () { + function stopMe(obj) { + debugger; + } + stopMe({ foo: "bar" }); + } + + ")()" + ); +} diff --git a/devtools/server/tests/xpcshell/test_pauselifetime-04.js b/devtools/server/tests/xpcshell/test_pauselifetime-04.js new file mode 100644 index 0000000000..7d226260f0 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_pauselifetime-04.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that requesting a pause actor for the same value multiple + * times returns the same actor. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + + const args = packet.frame.arguments; + const objActor1 = args[0].actor; + + const response = await threadFront.getFrames(0, 1); + const frame = response.frames[0]; + Assert.equal(objActor1, frame.arguments[0].actor); + + await threadFront.resume(); + }) +); + +function evaluateTestCode(debuggee) { + debuggee.eval( + "(" + + function () { + function stopMe(obj) { + debugger; + } + stopMe({ foo: "bar" }); + } + + ")()" + ); +} diff --git a/devtools/server/tests/xpcshell/test_promise_state-01.js b/devtools/server/tests/xpcshell/test_promise_state-01.js new file mode 100644 index 0000000000..d02b64a67e --- /dev/null +++ b/devtools/server/tests/xpcshell/test_promise_state-01.js @@ -0,0 +1,44 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint-disable max-nested-callbacks */ + +"use strict"; + +/** + * Test that the preview in a Promise's grip is correct when the Promise is + * pending. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const environment = await packet.frame.getEnvironment(); + const grip = environment.bindings.variables.p.value; + + ok(grip.preview); + equal(grip.class, "Promise"); + equal(grip.preview.ownProperties["<state>"].value, "pending"); + + const objClient = threadFront.pauseGrip(grip); + const { promiseState } = await objClient.getPromiseState(); + equal(promiseState.state, "pending"); + }) +); + +function evalCode(debuggee) { + /* eslint-disable mozilla/var-only-at-top-level, no-unused-vars */ + // prettier-ignore + Cu.evalInSandbox( + "doTest();\n" + + function doTest() { + var p = new Promise(function () {}); + debugger; + }, + debuggee + ); + /* eslint-enable mozilla/var-only-at-top-level, no-unused-vars */ +} diff --git a/devtools/server/tests/xpcshell/test_promise_state-02.js b/devtools/server/tests/xpcshell/test_promise_state-02.js new file mode 100644 index 0000000000..e1219f545c --- /dev/null +++ b/devtools/server/tests/xpcshell/test_promise_state-02.js @@ -0,0 +1,59 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint-disable max-nested-callbacks */ + +"use strict"; + +/** + * Test that the preview in a Promise's grip is correct when the Promise is + * fulfilled. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const environment = await packet.frame.getEnvironment(); + const grip = environment.bindings.variables.p.value; + + ok(grip.preview); + equal(grip.class, "Promise"); + equal(grip.preview.ownProperties["<state>"].value, "fulfilled"); + equal( + grip.preview.ownProperties["<value>"].value.actorID, + packet.frame.arguments[0].actorID, + "The promise's fulfilled state value in the preview should be the same " + + "value passed to the then function" + ); + + const objClient = threadFront.pauseGrip(grip); + const { promiseState } = await objClient.getPromiseState(); + equal(promiseState.state, "fulfilled"); + equal( + promiseState.value.getGrip().actorID, + packet.frame.arguments[0].actorID, + "The promise's fulfilled state value in getPromiseState() should be " + + "the same value passed to the then function" + ); + }) +); + +function evalCode(debuggee) { + /* eslint-disable mozilla/var-only-at-top-level, no-unused-vars */ + // prettier-ignore + Cu.evalInSandbox( + "doTest();\n" + + function doTest() { + var resolved = Promise.resolve({}); + resolved.then(() => { + var p = resolved; + debugger; + }); + }, + debuggee + ); + /* eslint-enable mozilla/var-only-at-top-level, no-unused-vars */ +} diff --git a/devtools/server/tests/xpcshell/test_promise_state-03.js b/devtools/server/tests/xpcshell/test_promise_state-03.js new file mode 100644 index 0000000000..8ec1fa3717 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_promise_state-03.js @@ -0,0 +1,58 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint-disable max-nested-callbacks */ + +"use strict"; + +/** + * Test that the preview in a Promise's grip is correct when the Promise is + * rejected. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const environment = await packet.frame.getEnvironment(); + const grip = environment.bindings.variables.p.value; + ok(grip.preview); + equal(grip.class, "Promise"); + equal(grip.preview.ownProperties["<state>"].value, "rejected"); + equal( + grip.preview.ownProperties["<reason>"].value.actorID, + packet.frame.arguments[0].actorID, + "The promise's rejected state reason in the preview should be the same " + + "value passed to the then function" + ); + + const objClient = threadFront.pauseGrip(grip); + const { promiseState } = await objClient.getPromiseState(); + equal(promiseState.state, "rejected"); + equal( + promiseState.reason.getGrip().actorID, + packet.frame.arguments[0].actorID, + "The promise's rejected state value in getPromiseState() should be " + + "the same value passed to the then function" + ); + }) +); + +function evalCode(debuggee) { + /* eslint-disable mozilla/var-only-at-top-level, no-unused-vars */ + // prettier-ignore + Cu.evalInSandbox( + "doTest();\n" + + function doTest() { + var resolved = Promise.reject(new Error("uh oh")); + resolved.catch(() => { + var p = resolved; + debugger; + }); + }, + debuggee + ); + /* eslint-enable mozilla/var-only-at-top-level, no-unused-vars */ +} diff --git a/devtools/server/tests/xpcshell/test_promises_run_to_completion.js b/devtools/server/tests/xpcshell/test_promises_run_to_completion.js new file mode 100644 index 0000000000..4d1e8745fe --- /dev/null +++ b/devtools/server/tests/xpcshell/test_promises_run_to_completion.js @@ -0,0 +1,132 @@ +// Bug 1145201: Promise then-handlers can still be executed while the debugger is paused. +// +// When a promise is resolved, for each of its callbacks, a microtask is queued +// to run the callback. At various points, the HTML spec says the browser must +// "perform a microtask checkpoint", which means to draw microtasks from the +// queue and run them, until the queue is empty. +// +// The HTML spec is careful to perform a microtask checkpoint directly after +// each invocation of an event handler or DOM callback, so that code using +// promises can trust that its promise callbacks run promptly, in a +// deterministic order, without DOM events or other outside influences +// intervening. +// +// When the JavaScript debugger interrupts the execution of debuggee content +// code, it naturally must process events for its own user interface and promise +// callbacks. However, it must not run any debuggee microtasks. The debuggee has +// been interrupted in the midst of executing some other code, and the +// JavaScript spec promises developers: "Once execution of a Job is initiated, +// the Job always executes to completion. No other Job may be initiated until +// the currently running Job completes." [1] This promise would be broken if the +// debugger's own event processing ran debuggee microtasks during the +// interruption. +// +// Looking at things from the other side, a microtask checkpoint must be +// performed before returning from a debugger callback, rather than being put +// off until the debuggee performs its next microtask checkpoint, so that +// debugger microtasks are not interleaved with debuggee microtasks. A debuggee +// microtask could hit a breakpoint or otherwise re-enter the debugger, which +// might be quite surprised to see a new debugger callback begin before its +// previous promise callbacks could finish. +// +// [1]: https://www.ecma-international.org/ecma-262/9.0/index.html#sec-jobs-and-job-queues + +"use strict"; + +const Debugger = require("Debugger"); + +function test_promises_run_to_completion() { + const g = createTestGlobal( + "test global for test_promises_run_to_completion.js" + ); + const dbg = new Debugger(g); + g.Assert = Assert; + const log = [""]; + g.log = log; + + dbg.onDebuggerStatement = function handleDebuggerStatement(frame) { + dbg.onDebuggerStatement = undefined; + + // Exercise the promise machinery: resolve a promise and perform a microtask + // queue. When called from a debugger hook, the debuggee's microtasks should not + // run. + log[0] += "debug-handler("; + Promise.resolve(42).then(v => { + Assert.equal( + v, + 42, + "debugger callback promise handler got the right value" + ); + log[0] += "debug-react"; + }); + log[0] += "("; + force_microtask_checkpoint(); + log[0] += ")"; + + Promise.resolve(42).then(v => { + // The microtask running this callback should be handled as we leave the + // onDebuggerStatement Debugger callback, and should not be interleaved + // with debuggee microtasks. + log[0] += "(trailing)"; + }); + + log[0] += ")"; + }; + + // Evaluate some debuggee code that resolves a promise, and then enters the debugger. + Cu.evalInSandbox( + ` + log[0] += "eval("; + Promise.resolve(42).then(function debuggeePromiseCallback(v) { + Assert.equal(v, 42, "debuggee promise handler got the right value"); + // Debugger microtask checkpoints must not run debuggee microtasks, so + // this callback should run at the next microtask checkpoint *not* + // performed by the debugger. + log[0] += "eval-react"; + }); + + log[0] += "debugger("; + debugger; + log[0] += "))"; + `, + g + ); + + // Let other microtasks run. This should run the debuggee's promise callback. + log[0] += "final("; + force_microtask_checkpoint(); + log[0] += ")"; + + Assert.equal( + log[0], + `\ +eval(\ +debugger(\ +debug-handler(\ +(debug-react)\ +)\ +(trailing)\ +))\ +final(\ +eval-react\ +)`, + "microtasks ran as expected" + ); + + run_next_test(); +} + +function force_microtask_checkpoint() { + // Services.tm.spinEventLoopUntilEmpty only performs a microtask checkpoint if + // there is actually an event to run. So make one up. + let ran = false; + Services.tm.dispatchToMainThread(() => { + ran = true; + }); + Services.tm.spinEventLoopUntil( + "Test(test_promises_run_to_completion.js:force_microtask_checkpoint)", + () => ran + ); +} + +add_test(test_promises_run_to_completion); diff --git a/devtools/server/tests/xpcshell/test_register_actor.js b/devtools/server/tests/xpcshell/test_register_actor.js new file mode 100644 index 0000000000..f38ab73572 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_register_actor.js @@ -0,0 +1,94 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +function run_test() { + // Allow incoming connections. + DevToolsServer.keepAlive = true; + DevToolsServer.init(); + DevToolsServer.registerAllActors(); + + add_test(test_lazy_api); + add_test(manual_remove); + add_test(cleanup); + run_next_test(); +} + +// Bug 988237: Test the new lazy actor actor-register +function test_lazy_api() { + let isActorLoaded = false; + let isActorInstantiated = false; + function onActorEvent(subject, topic, data) { + if (data == "loaded") { + isActorLoaded = true; + } else if (data == "instantiated") { + isActorInstantiated = true; + } + } + Services.obs.addObserver(onActorEvent, "actor"); + ActorRegistry.registerModule("xpcshell-test/registertestactors-lazy", { + prefix: "lazy", + constructor: "LazyActor", + type: { global: true, target: true }, + }); + // The actor is immediatly registered, but not loaded + Assert.ok( + ActorRegistry.targetScopedActorFactories.hasOwnProperty("lazyActor") + ); + Assert.ok(ActorRegistry.globalActorFactories.hasOwnProperty("lazyActor")); + Assert.ok(!isActorLoaded); + Assert.ok(!isActorInstantiated); + + const client = new DevToolsClient(DevToolsServer.connectPipe()); + client.connect().then(function onConnect() { + client.mainRoot.rootForm.then(onRootForm); + }); + function onRootForm(response) { + // On rootForm, the actor is still not loaded, + // but we can see its name in the list of available actors + Assert.ok(!isActorLoaded); + Assert.ok(!isActorInstantiated); + Assert.ok("lazyActor" in response); + + const { LazyFront } = require("xpcshell-test/registertestactors-lazy"); + const front = new LazyFront(client); + // As this Front isn't instantiated by protocol.js, we have to manually + // set its actor ID and manage it: + front.actorID = response.lazyActor; + client.addActorPool(front); + front.manage(front); + + front.hello().then(onRequest); + } + function onRequest(response) { + Assert.equal(response, "world"); + + // Finally, the actor is loaded on the first request being made to it + Assert.ok(isActorLoaded); + Assert.ok(isActorInstantiated); + + Services.obs.removeObserver(onActorEvent, "actor"); + client.close().then(() => run_next_test()); + } +} + +function manual_remove() { + Assert.ok(ActorRegistry.globalActorFactories.hasOwnProperty("lazyActor")); + ActorRegistry.removeGlobalActor("lazyActor"); + Assert.ok(!ActorRegistry.globalActorFactories.hasOwnProperty("lazyActor")); + + run_next_test(); +} + +function cleanup() { + DevToolsServer.destroy(); + + // Check that all actors are unregistered on server destruction + Assert.ok( + !ActorRegistry.targetScopedActorFactories.hasOwnProperty("lazyActor") + ); + Assert.ok(!ActorRegistry.globalActorFactories.hasOwnProperty("lazyActor")); + + run_next_test(); +} diff --git a/devtools/server/tests/xpcshell/test_requestTypes.js b/devtools/server/tests/xpcshell/test_requestTypes.js new file mode 100644 index 0000000000..8787ae5f85 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_requestTypes.js @@ -0,0 +1,28 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { rootSpec } = require("resource://devtools/shared/specs/root.js"); +const { + generateRequestTypes, +} = require("resource://devtools/shared/protocol/Actor.js"); + +add_task(async function () { + DevToolsServer.init(); + DevToolsServer.registerAllActors(); + + const client = new DevToolsClient(DevToolsServer.connectPipe()); + await client.connect(); + + const response = await client.mainRoot.requestTypes(); + const expectedRequestTypes = Object.keys(generateRequestTypes(rootSpec)); + + Assert.ok(Array.isArray(response.requestTypes)); + Assert.equal( + JSON.stringify(response.requestTypes), + JSON.stringify(expectedRequestTypes) + ); + + await client.close(); +}); diff --git a/devtools/server/tests/xpcshell/test_restartFrame-01.js b/devtools/server/tests/xpcshell/test_restartFrame-01.js new file mode 100644 index 0000000000..cb13ae2d7e --- /dev/null +++ b/devtools/server/tests/xpcshell/test_restartFrame-01.js @@ -0,0 +1,118 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check restarting a frame and stepping out of the + * restarted frame. + */ + +async function testFinish({ threadFront, devToolsClient }) { + await close(devToolsClient); + + do_test_finished(); +} + +async function invokeAndPause({ global, threadFront }, expression) { + return executeOnNextTickAndWaitForPause( + () => Cu.evalInSandbox(expression, global), + threadFront + ); +} + +async function steps(threadFront, sequence) { + const locations = []; + for (const cmd of sequence) { + const packet = await step(threadFront, cmd); + locations.push(getPauseLocation(packet)); + } + return locations; +} + +async function step(threadFront, cmd) { + return cmd(threadFront); +} + +function getPauseLocation(packet) { + const { line, column } = packet.frame.where; + return { line, column }; +} + +async function restartFrame0(dbg, func, expectedLocation) { + const { threadFront } = dbg; + + info("pause and step into a()"); + await invokeAndPause(dbg, `${func}()`); + await steps(threadFront, [stepOver, stepIn]); + + info("restart the youngest frame a()"); + const { frames } = await threadFront.frames(0, 5); + const frameActorID = frames[0].actorID; + const packet = await restartFrame(threadFront, frameActorID); + + deepEqual( + getPauseLocation(packet), + expectedLocation, + "pause location in the restarted frame a()" + ); +} + +async function restartFrame1(dbg, func, expectedLocation) { + const { threadFront } = dbg; + + info("pause and step into b()"); + await invokeAndPause(dbg, `${func}()`); + await steps(threadFront, [stepOver, stepIn, stepIn]); + + info("restart the frame with index 1"); + const { frames } = await threadFront.frames(0, 5); + const frameActorID = frames[1].actorID; + const packet = await restartFrame(threadFront, frameActorID); + + deepEqual( + getPauseLocation(packet), + expectedLocation, + "pause location in the restarted frame c()" + ); +} + +async function stepOutRestartedFrame( + dbg, + restartedFrameName, + expectedLocation, + expectedCallstackLength +) { + const { threadFront } = dbg; + const { frames } = await threadFront.frames(0, 5); + + Assert.equal( + frames.length, + expectedCallstackLength, + `the callstack length after restarting frame ${restartedFrameName}()` + ); + + info(`step out of the restarted frame ${restartedFrameName}()`); + const frameActorID = frames[0].actorID; + const packet = await stepOut(threadFront, frameActorID); + + deepEqual(getPauseLocation(packet), expectedLocation, `step out location`); +} + +function run_test() { + return (async function () { + const dbg = await setupTestFromUrl("stepping.js"); + + info(`Test restarting the youngest frame`); + await restartFrame0(dbg, "arithmetic", { line: 7, column: 2 }); + await stepOutRestartedFrame(dbg, "a", { line: 16, column: 8 }, 3); + await dbg.threadFront.resume(); + + info(`Test restarting the frame with the index 1`); + await restartFrame1(dbg, "nested", { line: 30, column: 2 }); + await stepOutRestartedFrame(dbg, "c", { line: 36, column: 0 }, 3); + await dbg.threadFront.resume(); + + await testFinish(dbg); + })(); +} diff --git a/devtools/server/tests/xpcshell/test_safe-getter.js b/devtools/server/tests/xpcshell/test_safe-getter.js new file mode 100644 index 0000000000..65bf3414ea --- /dev/null +++ b/devtools/server/tests/xpcshell/test_safe-getter.js @@ -0,0 +1,54 @@ +/* eslint-disable strict */ +function run_test() { + Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); + }); + const { addDebuggerToGlobal } = ChromeUtils.importESModule( + "resource://gre/modules/jsdebugger.sys.mjs" + ); + addDebuggerToGlobal(globalThis); + const g = createTestGlobal("test", { + wantGlobalProperties: ["ChromeUtils"], + }); + const dbg = new Debugger(); + const gw = dbg.addDebuggee(g); + + g.eval(` + // This is not a CCW. + Object.defineProperty(this, "bar", { + get: function() { return "bar"; }, + configurable: true, + enumerable: true + }); + + const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" + ); + + // This is a CCW. + XPCOMUtils.defineLazyScriptGetter( + this, "foo", "chrome://global/content/viewZoomOverlay.js"); + `); + + // Neither scripted getter should be considered safe. + assert(!DevToolsUtils.hasSafeGetter(gw.getOwnPropertyDescriptor("bar"))); + assert(!DevToolsUtils.hasSafeGetter(gw.getOwnPropertyDescriptor("foo"))); + + // Create an object in a less privileged sandbox. + const obj = gw.makeDebuggeeValue( + Cu.waiveXrays( + Cu.Sandbox(null).eval(` + Object.defineProperty({}, "bar", { + get: function() { return "bar"; }, + configurable: true, + enumerable: true + }); + `) + ) + ); + + // After waiving Xrays, the object has 2 wrappers. Both must be removed + // in order to detect that the getter is not safe. + assert(!DevToolsUtils.hasSafeGetter(obj.getOwnPropertyDescriptor("bar"))); +} diff --git a/devtools/server/tests/xpcshell/test_sessionDataHelpers.js b/devtools/server/tests/xpcshell/test_sessionDataHelpers.js new file mode 100644 index 0000000000..e0dcc3b21b --- /dev/null +++ b/devtools/server/tests/xpcshell/test_sessionDataHelpers.js @@ -0,0 +1,124 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test SessionDataHelpers. + */ + +"use strict"; + +const { SessionDataHelpers } = ChromeUtils.import( + "resource://devtools/server/actors/watcher/SessionDataHelpers.jsm" +); +const { SUPPORTED_DATA } = SessionDataHelpers; +const { TARGETS } = SUPPORTED_DATA; + +function run_test() { + const sessionData = { + [TARGETS]: [], + }; + + info("Test adding a new entry"); + SessionDataHelpers.addOrSetSessionDataEntry( + sessionData, + TARGETS, + ["frame", "worker"], + "add" + ); + deepEqual( + sessionData[TARGETS], + ["frame", "worker"], + "the two elements were added" + ); + + info("Test adding a duplicated entry"); + SessionDataHelpers.addOrSetSessionDataEntry( + sessionData, + TARGETS, + ["frame"], + "add" + ); + deepEqual( + sessionData[TARGETS], + ["frame", "worker"], + "addOrSetSessionDataEntry ignore duplicates" + ); + + SessionDataHelpers.addOrSetSessionDataEntry( + sessionData, + TARGETS, + ["process"], + "add" + ); + deepEqual( + sessionData[TARGETS], + ["frame", "worker", "process"], + "the third element is added" + ); + + info("Test removing an existing entry"); + let removed = SessionDataHelpers.removeSessionDataEntry( + sessionData, + TARGETS, + ["process"] + ); + ok(removed, "removedSessionDataEntry returned true as it removed an element"); + deepEqual( + sessionData[TARGETS], + ["frame", "worker"], + "the element has been remove" + ); + + info("Test removing non-existing entry"); + removed = SessionDataHelpers.removeSessionDataEntry(sessionData, TARGETS, [ + "not-existing", + ]); + ok( + !removed, + "removedSessionDataEntry returned false as no element has been removed" + ); + deepEqual( + sessionData[TARGETS], + ["frame", "worker"], + "no change made to the array" + ); + + removed = SessionDataHelpers.removeSessionDataEntry(sessionData, TARGETS, [ + "frame", + "worker", + ]); + ok( + removed, + "removedSessionDataEntry returned true as elements have been removed" + ); + deepEqual(sessionData[TARGETS], [], "all elements were removed"); + + info("Test settting instead of adding data entries"); + SessionDataHelpers.addOrSetSessionDataEntry( + sessionData, + TARGETS, + ["frame"], + "add" + ); + deepEqual(sessionData[TARGETS], ["frame"], "frame was re-added"); + + SessionDataHelpers.addOrSetSessionDataEntry( + sessionData, + TARGETS, + ["process", "worker"], + "set" + ); + deepEqual( + sessionData[TARGETS], + ["process", "worker"], + "frame was replaced by process and worker" + ); + + info("Test setting an empty array"); + SessionDataHelpers.addOrSetSessionDataEntry(sessionData, TARGETS, [], "set"); + deepEqual( + sessionData[TARGETS], + [], + "Setting an empty array of entries clears the data entry" + ); +} diff --git a/devtools/server/tests/xpcshell/test_setBreakpoint-at-the-beginning-of-a-minified-fn.js b/devtools/server/tests/xpcshell/test_setBreakpoint-at-the-beginning-of-a-minified-fn.js new file mode 100644 index 0000000000..9140e92d7c --- /dev/null +++ b/devtools/server/tests/xpcshell/test_setBreakpoint-at-the-beginning-of-a-minified-fn.js @@ -0,0 +1,41 @@ +"use strict"; + +const SOURCE_URL = getFileUrl("setBreakpoint-on-column-minified.js"); + +add_task( + threadFrontTest( + async ({ threadFront, debuggee }) => { + const promise = waitForNewSource(threadFront, SOURCE_URL); + loadSubScript(SOURCE_URL, debuggee); + const { source } = await promise; + + // Pause inside of the nested function so we can make sure that we don't + // add any other breakpoints at other places on this line. + const location = { sourceUrl: source.url, line: 3, column: 56 }; + setBreakpoint(threadFront, location); + + const packet = await executeOnNextTickAndWaitForPause(function () { + Cu.evalInSandbox("f()", debuggee); + }, threadFront); + + const why = packet.why; + Assert.equal(why.type, "breakpoint"); + Assert.equal(why.actors.length, 1); + + const frame = packet.frame; + const where = frame.where; + Assert.equal(where.actor, source.actor); + Assert.equal(where.line, location.line); + Assert.equal(where.column, 56); + + const environment = await packet.frame.getEnvironment(); + const variables = environment.bindings.variables; + Assert.equal(variables.a.value.type, "undefined"); + Assert.equal(variables.b.value.type, "undefined"); + Assert.equal(variables.c.value.type, "undefined"); + + await resume(threadFront); + }, + { doNotRunWorker: true } + ) +); diff --git a/devtools/server/tests/xpcshell/test_setBreakpoint-at-the-end-of-a-minified-fn.js b/devtools/server/tests/xpcshell/test_setBreakpoint-at-the-end-of-a-minified-fn.js new file mode 100644 index 0000000000..f9df5adad4 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_setBreakpoint-at-the-end-of-a-minified-fn.js @@ -0,0 +1,41 @@ +"use strict"; + +const SOURCE_URL = getFileUrl("setBreakpoint-on-column-minified.js"); + +add_task( + threadFrontTest( + async ({ threadFront, debuggee }) => { + const promise = waitForNewSource(threadFront, SOURCE_URL); + loadSubScript(SOURCE_URL, debuggee); + const { source } = await promise; + + // Pause inside of the nested function so we can make sure that we don't + // add any other breakpoints at other places on this line. + const location = { sourceUrl: source.url, line: 3, column: 81 }; + setBreakpoint(threadFront, location); + + const packet = await executeOnNextTickAndWaitForPause(function () { + Cu.evalInSandbox("f()", debuggee); + }, threadFront); + + const why = packet.why; + Assert.equal(why.type, "breakpoint"); + Assert.equal(why.actors.length, 1); + + const frame = packet.frame; + const where = frame.where; + Assert.equal(where.actor, source.actor); + Assert.equal(where.line, location.line); + Assert.equal(where.column, 81); + + const environment = await packet.frame.getEnvironment(); + const variables = environment.bindings.variables; + Assert.equal(variables.a.value, 1); + Assert.equal(variables.b.value, 2); + Assert.equal(variables.c.value, 3); + + await resume(threadFront); + }, + { doNotRunWorker: true } + ) +); diff --git a/devtools/server/tests/xpcshell/test_setBreakpoint-on-column-in-gcd-script.js b/devtools/server/tests/xpcshell/test_setBreakpoint-on-column-in-gcd-script.js new file mode 100644 index 0000000000..797cb6cd65 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_setBreakpoint-on-column-in-gcd-script.js @@ -0,0 +1,46 @@ +"use strict"; + +const SOURCE_URL = getFileUrl("setBreakpoint-on-column-in-gcd-script.js"); + +add_task( + threadFrontTest( + async ({ threadFront, debuggee, targetFront }) => { + const promise = waitForNewSource(threadFront, SOURCE_URL); + loadSubScriptWithOptions(SOURCE_URL, { + target: debuggee, + ignoreCache: true, + }); + Cu.forceGC(); + Cu.forceGC(); + Cu.forceGC(); + + const { source } = await promise; + + const location = { sourceUrl: source.url, line: 6, column: 21 }; + setBreakpoint(threadFront, location); + + const packet = await executeOnNextTickAndWaitForPause(function () { + reload(targetFront).then(function () { + loadSubScriptWithOptions(SOURCE_URL, { + target: debuggee, + ignoreCache: true, + }); + }); + }, threadFront); + const environment = await packet.frame.getEnvironment(); + const why = packet.why; + Assert.equal(why.type, "breakpoint"); + Assert.equal(why.actors.length, 1); + const frame = packet.frame; + const where = frame.where; + Assert.equal(where.line, location.line); + Assert.equal(where.column, location.column); + const variables = environment.bindings.variables; + Assert.equal(variables.a.value, 1); + Assert.equal(variables.b.value.type, "undefined"); + Assert.equal(variables.c.value.type, "undefined"); + await resume(threadFront); + }, + { doNotRunWorker: true } + ) +); diff --git a/devtools/server/tests/xpcshell/test_setBreakpoint-on-column.js b/devtools/server/tests/xpcshell/test_setBreakpoint-on-column.js new file mode 100644 index 0000000000..200d8b44e6 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_setBreakpoint-on-column.js @@ -0,0 +1,36 @@ +"use strict"; + +const SOURCE_URL = getFileUrl("setBreakpoint-on-column.js"); + +add_task( + threadFrontTest( + async ({ threadFront, debuggee }) => { + const promise = waitForNewSource(threadFront, SOURCE_URL); + loadSubScript(SOURCE_URL, debuggee); + const { source } = await promise; + + const location = { sourceUrl: source.url, line: 4, column: 21 }; + setBreakpoint(threadFront, location); + + const packet = await executeOnNextTickAndWaitForPause(function () { + Cu.evalInSandbox("f()", debuggee); + }, threadFront); + const environment = await packet.frame.getEnvironment(); + const why = packet.why; + Assert.equal(why.type, "breakpoint"); + Assert.equal(why.actors.length, 1); + const frame = packet.frame; + const where = frame.where; + Assert.equal(where.actor, source.actor); + Assert.equal(where.line, location.line); + Assert.equal(where.column, location.column); + const variables = environment.bindings.variables; + Assert.equal(variables.a.value, 1); + Assert.equal(variables.b.value.type, "undefined"); + Assert.equal(variables.c.value.type, "undefined"); + + await resume(threadFront); + }, + { doNotRunWorker: true } + ) +); diff --git a/devtools/server/tests/xpcshell/test_setBreakpoint-on-line-in-gcd-script.js b/devtools/server/tests/xpcshell/test_setBreakpoint-on-line-in-gcd-script.js new file mode 100644 index 0000000000..565402551e --- /dev/null +++ b/devtools/server/tests/xpcshell/test_setBreakpoint-on-line-in-gcd-script.js @@ -0,0 +1,45 @@ +"use strict"; + +const SOURCE_URL = getFileUrl("setBreakpoint-on-line-in-gcd-script.js"); + +add_task( + threadFrontTest( + async ({ threadFront, debuggee, targetFront }) => { + const promise = waitForNewSource(threadFront, SOURCE_URL); + loadSubScriptWithOptions(SOURCE_URL, { + target: debuggee, + ignoreCache: true, + }); + Cu.forceGC(); + Cu.forceGC(); + Cu.forceGC(); + + const { source } = await promise; + + const location = { sourceUrl: source.url, line: 7 }; + setBreakpoint(threadFront, location); + + const packet = await executeOnNextTickAndWaitForPause(function () { + reload(targetFront).then(function () { + loadSubScriptWithOptions(SOURCE_URL, { + target: debuggee, + ignoreCache: true, + }); + }); + }, threadFront); + const why = packet.why; + const environment = await packet.frame.getEnvironment(); + Assert.equal(why.type, "breakpoint"); + Assert.equal(why.actors.length, 1); + const frame = packet.frame; + const where = frame.where; + Assert.equal(where.line, location.line); + const variables = environment.bindings.variables; + Assert.equal(variables.a.value, 1); + Assert.equal(variables.b.value.type, "undefined"); + Assert.equal(variables.c.value.type, "undefined"); + await resume(threadFront); + }, + { doNotRunWorker: true } + ) +); diff --git a/devtools/server/tests/xpcshell/test_setBreakpoint-on-line-with-multiple-offsets.js b/devtools/server/tests/xpcshell/test_setBreakpoint-on-line-with-multiple-offsets.js new file mode 100644 index 0000000000..2debc26b93 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_setBreakpoint-on-line-with-multiple-offsets.js @@ -0,0 +1,52 @@ +"use strict"; + +const SOURCE_URL = getFileUrl("setBreakpoint-on-line-with-multiple-offsets.js"); + +add_task( + threadFrontTest( + async ({ threadFront, debuggee }) => { + const promise = waitForNewSource(threadFront, SOURCE_URL); + loadSubScript(SOURCE_URL, debuggee); + const { source } = await promise; + const sourceFront = threadFront.source(source); + + const location = { sourceUrl: sourceFront.url, line: 4 }; + setBreakpoint(threadFront, location); + + let packet = await executeOnNextTickAndWaitForPause(function () { + Cu.evalInSandbox("f()", debuggee); + }, threadFront); + let why = packet.why; + let environment = await packet.frame.getEnvironment(); + Assert.equal(why.type, "breakpoint"); + Assert.equal(why.actors.length, 1); + let frame = packet.frame; + let where = frame.where; + Assert.equal(where.actor, source.actor); + Assert.equal(where.line, location.line); + let variables = environment.bindings.variables; + Assert.equal(variables.i.value.type, "undefined"); + + const location2 = { sourceUrl: sourceFront.url, line: 7 }; + setBreakpoint(threadFront, location2); + + packet = await executeOnNextTickAndWaitForPause( + () => resume(threadFront), + threadFront + ); + environment = await packet.frame.getEnvironment(); + why = packet.why; + Assert.equal(why.type, "breakpoint"); + Assert.equal(why.actors.length, 1); + frame = packet.frame; + where = frame.where; + Assert.equal(where.actor, source.actor); + Assert.equal(where.line, location2.line); + variables = environment.bindings.variables; + Assert.equal(variables.i.value, 1); + + await resume(threadFront); + }, + { doNotRunWorker: true } + ) +); diff --git a/devtools/server/tests/xpcshell/test_setBreakpoint-on-line-with-multiple-statements.js b/devtools/server/tests/xpcshell/test_setBreakpoint-on-line-with-multiple-statements.js new file mode 100644 index 0000000000..f5ec75a353 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_setBreakpoint-on-line-with-multiple-statements.js @@ -0,0 +1,38 @@ +"use strict"; + +const SOURCE_URL = getFileUrl( + "setBreakpoint-on-line-with-multiple-statements.js" +); + +add_task( + threadFrontTest( + async ({ threadFront, debuggee }) => { + const promise = waitForNewSource(threadFront, SOURCE_URL); + loadSubScript(SOURCE_URL, debuggee); + const { source } = await promise; + const sourceFront = threadFront.source(source); + + const location = { sourceUrl: sourceFront.url, line: 4 }; + setBreakpoint(threadFront, location); + + const packet = await executeOnNextTickAndWaitForPause(function () { + Cu.evalInSandbox("f()", debuggee); + }, threadFront); + const why = packet.why; + const environment = await packet.frame.getEnvironment(); + Assert.equal(why.type, "breakpoint"); + Assert.equal(why.actors.length, 1); + const frame = packet.frame; + const where = frame.where; + Assert.equal(where.actor, source.actor); + Assert.equal(where.line, location.line); + const variables = environment.bindings.variables; + Assert.equal(variables.a.value.type, "undefined"); + Assert.equal(variables.b.value.type, "undefined"); + Assert.equal(variables.c.value.type, "undefined"); + + await resume(threadFront); + }, + { doNotRunWorker: true } + ) +); diff --git a/devtools/server/tests/xpcshell/test_setBreakpoint-on-line-with-no-offsets-in-gcd-script.js b/devtools/server/tests/xpcshell/test_setBreakpoint-on-line-with-no-offsets-in-gcd-script.js new file mode 100644 index 0000000000..1bcdadbe4a --- /dev/null +++ b/devtools/server/tests/xpcshell/test_setBreakpoint-on-line-with-no-offsets-in-gcd-script.js @@ -0,0 +1,56 @@ +"use strict"; + +const SOURCE_URL = getFileUrl( + "setBreakpoint-on-line-with-no-offsets-in-gcd-script.js" +); + +add_task( + threadFrontTest( + async ({ threadFront, debuggee, targetFront }) => { + const promise = waitForNewSource(threadFront, SOURCE_URL); + loadSubScriptWithOptions(SOURCE_URL, { + target: debuggee, + ignoreCache: true, + }); + Cu.forceGC(); + Cu.forceGC(); + Cu.forceGC(); + + const { source } = await promise; + const sourceFront = threadFront.source(source); + + const location = { line: 7 }; + let [packet, breakpointClient] = await setBreakpoint( + sourceFront, + location + ); + Assert.ok(packet.isPending); + Assert.equal(false, "actualLocation" in packet); + + packet = await executeOnNextTickAndWaitForPause(function () { + reload(targetFront).then(function () { + loadSubScriptWithOptions(SOURCE_URL, { + target: debuggee, + ignoreCache: true, + }); + }); + }, threadFront); + const environment = await packet.frame.getEnvironment(); + Assert.equal(packet.type, "paused"); + const why = packet.why; + Assert.equal(why.type, "breakpoint"); + Assert.equal(why.actors.length, 1); + Assert.equal(why.actors[0], breakpointClient.actor); + const frame = packet.frame; + const where = frame.where; + Assert.equal(where.actor, source.actor); + Assert.equal(where.line, 8); + const variables = environment.bindings.variables; + Assert.equal(variables.a.value, 1); + Assert.equal(variables.c.value.type, "undefined"); + + await resume(threadFront); + }, + { doNotRunWorker: true } + ) +); diff --git a/devtools/server/tests/xpcshell/test_setBreakpoint-on-line-with-no-offsets.js b/devtools/server/tests/xpcshell/test_setBreakpoint-on-line-with-no-offsets.js new file mode 100644 index 0000000000..5700097ea6 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_setBreakpoint-on-line-with-no-offsets.js @@ -0,0 +1,44 @@ +"use strict"; + +const SOURCE_URL = getFileUrl("setBreakpoint-on-line-with-no-offsets.js"); + +add_task( + threadFrontTest( + async ({ threadFront, debuggee }) => { + const promise = waitForNewSource(threadFront, SOURCE_URL); + loadSubScript(SOURCE_URL, debuggee); + const { source } = await promise; + const sourceFront = threadFront.source(source); + + const location = { line: 5 }; + let [packet, breakpointClient] = await setBreakpoint( + sourceFront, + location + ); + Assert.ok(!packet.isPending); + Assert.ok("actualLocation" in packet); + const actualLocation = packet.actualLocation; + Assert.equal(actualLocation.line, 6); + + packet = await executeOnNextTickAndWaitForPause(function () { + Cu.evalInSandbox("f()", debuggee); + }, threadFront); + const environment = await packet.frame.getEnvironment(); + Assert.equal(packet.type, "paused"); + const why = packet.why; + Assert.equal(why.type, "breakpoint"); + Assert.equal(why.actors.length, 1); + Assert.equal(why.actors[0], breakpointClient.actor); + const frame = packet.frame; + const where = frame.where; + Assert.equal(where.actor, source.actor); + Assert.equal(where.line, actualLocation.line); + const variables = environment.bindings.variables; + Assert.equal(variables.a.value, 1); + Assert.equal(variables.c.value.type, "undefined"); + + await resume(threadFront); + }, + { doNotRunWorker: true } + ) +); diff --git a/devtools/server/tests/xpcshell/test_setBreakpoint-on-line.js b/devtools/server/tests/xpcshell/test_setBreakpoint-on-line.js new file mode 100644 index 0000000000..93e01b757c --- /dev/null +++ b/devtools/server/tests/xpcshell/test_setBreakpoint-on-line.js @@ -0,0 +1,36 @@ +"use strict"; + +const SOURCE_URL = getFileUrl("setBreakpoint-on-line.js"); + +add_task( + threadFrontTest( + async ({ threadFront, debuggee }) => { + const promise = waitForNewSource(threadFront, SOURCE_URL); + loadSubScript(SOURCE_URL, debuggee); + const { source } = await promise; + const sourceFront = threadFront.source(source); + + const location = { sourceUrl: sourceFront.url, line: 5 }; + setBreakpoint(threadFront, location); + + const packet = await executeOnNextTickAndWaitForPause(function () { + Cu.evalInSandbox("f()", debuggee); + }, threadFront); + const environment = await packet.frame.getEnvironment(); + const why = packet.why; + Assert.equal(why.type, "breakpoint"); + Assert.equal(why.actors.length, 1); + const frame = packet.frame; + const where = frame.where; + Assert.equal(where.actor, source.actor); + Assert.equal(where.line, location.line); + const variables = environment.bindings.variables; + Assert.equal(variables.a.value, 1); + Assert.equal(variables.b.value.type, "undefined"); + Assert.equal(variables.c.value.type, "undefined"); + + await resume(threadFront); + }, + { doNotRunWorker: true } + ) +); diff --git a/devtools/server/tests/xpcshell/test_shapes_highlighter_helpers.js b/devtools/server/tests/xpcshell/test_shapes_highlighter_helpers.js new file mode 100644 index 0000000000..6876f0a532 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_shapes_highlighter_helpers.js @@ -0,0 +1,274 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test the helper functions of the shapes highlighter. + */ + +"use strict"; + +const { + splitCoords, + coordToPercent, + evalCalcExpression, + shapeModeToCssPropertyName, + getCirclePath, + getDecimalPrecision, + getUnit, +} = require("resource://devtools/server/actors/highlighters/shapes.js"); + +function run_test() { + test_split_coords(); + test_coord_to_percent(); + test_eval_calc_expression(); + test_shape_mode_to_css_property_name(); + test_get_circle_path(); + test_get_decimal_precision(); + test_get_unit(); + run_next_test(); +} + +function test_split_coords() { + const tests = [ + { + desc: "splitCoords for basic coordinate pair", + expr: "30% 20%", + expected: ["30%", "20%"], + }, + { + desc: "splitCoords for coord pair with calc()", + expr: "calc(50px + 20%) 30%", + expected: ["calc(50px\u00a0+\u00a020%)", "30%"], + }, + ]; + + for (const { desc, expr, expected } of tests) { + deepEqual(splitCoords(expr), expected, desc); + } +} + +function test_coord_to_percent() { + const size = 1000; + const tests = [ + { + desc: "coordToPercent for percent value", + expr: "50%", + expected: 50, + }, + { + desc: "coordToPercent for px value", + expr: "500px", + expected: 50, + }, + { + desc: "coordToPercent for zero value", + expr: "0", + expected: 0, + }, + ]; + + for (const { desc, expr, expected } of tests) { + equal(coordToPercent(expr, size), expected, desc); + } +} + +function test_eval_calc_expression() { + const size = 1000; + const tests = [ + { + desc: "evalCalcExpression with one value", + expr: "50%", + expected: 50, + }, + { + desc: "evalCalcExpression with percent and px values", + expr: "50% + 100px", + expected: 60, + }, + { + desc: "evalCalcExpression with a zero value", + expr: "0 + 100px", + expected: 10, + }, + { + desc: "evalCalcExpression with a negative value", + expr: "-200px+50%", + expected: 30, + }, + ]; + + for (const { desc, expr, expected } of tests) { + equal(evalCalcExpression(expr, size), expected, desc); + } +} + +function test_shape_mode_to_css_property_name() { + const tests = [ + { + desc: "shapeModeToCssPropertyName for clip-path", + expr: "cssClipPath", + expected: "clipPath", + }, + { + desc: "shapeModeToCssPropertyName for shape-outside", + expr: "cssShapeOutside", + expected: "shapeOutside", + }, + ]; + + for (const { desc, expr, expected } of tests) { + equal(shapeModeToCssPropertyName(expr), expected, desc); + } +} + +function test_get_circle_path() { + const tests = [ + { + desc: "getCirclePath with size 5, no resizing, no zoom, 1:1 ratio", + size: 5, + cx: 0, + cy: 0, + width: 100, + height: 100, + zoom: 1, + expected: "M-5,0a5,5 0 1,0 10,0a5,5 0 1,0 -10,0", + }, + { + desc: "getCirclePath with size 7, resizing, no zoom, 1:1 ratio", + size: 7, + cx: 0, + cy: 0, + width: 200, + height: 200, + zoom: 1, + expected: "M-3.5,0a3.5,3.5 0 1,0 7,0a3.5,3.5 0 1,0 -7,0", + }, + { + desc: "getCirclePath with size 5, resizing, zoom, 1:1 ratio", + size: 5, + cx: 0, + cy: 0, + width: 200, + height: 200, + zoom: 2, + expected: "M-1.25,0a1.25,1.25 0 1,0 2.5,0a1.25,1.25 0 1,0 -2.5,0", + }, + { + desc: "getCirclePath with size 5, resizing, zoom, non-square ratio", + size: 5, + cx: 0, + cy: 0, + width: 100, + height: 200, + zoom: 2, + expected: "M-2.5,0a2.5,1.25 0 1,0 5,0a2.5,1.25 0 1,0 -5,0", + }, + ]; + + for (const { desc, size, cx, cy, width, height, zoom, expected } of tests) { + equal(getCirclePath(size, cx, cy, width, height, zoom), expected, desc); + } +} + +function test_get_decimal_precision() { + const tests = [ + { + desc: "getDecimalPrecision with px", + expr: "px", + expected: 0, + }, + { + desc: "getDecimalPrecision with %", + expr: "%", + expected: 2, + }, + { + desc: "getDecimalPrecision with em", + expr: "em", + expected: 2, + }, + { + desc: "getDecimalPrecision with undefined", + expr: undefined, + expected: 0, + }, + { + desc: "getDecimalPrecision with empty string", + expr: "", + expected: 0, + }, + ]; + + for (const { desc, expr, expected } of tests) { + equal(getDecimalPrecision(expr), expected, desc); + } +} + +function test_get_unit() { + const tests = [ + { + desc: "getUnit with %", + expr: "30%", + expected: "%", + }, + { + desc: "getUnit with px", + expr: "400px", + expected: "px", + }, + { + desc: "getUnit with em", + expr: "4em", + expected: "em", + }, + { + desc: "getUnit with 0", + expr: "0", + expected: "px", + }, + { + desc: "getUnit with 0%", + expr: "0%", + expected: "%", + }, + { + desc: "getUnit with 0.00%", + expr: "0.00%", + expected: "%", + }, + { + desc: "getUnit with 0px", + expr: "0px", + expected: "px", + }, + { + desc: "getUnit with 0em", + expr: "0em", + expected: "em", + }, + { + desc: "getUnit with calc", + expr: "calc(30px + 5%)", + expected: "px", + }, + { + desc: "getUnit with var", + expr: "var(--variable)", + expected: "px", + }, + { + desc: "getUnit with closest-side", + expr: "closest-side", + expected: "px", + }, + { + desc: "getUnit with farthest-side", + expr: "farthest-side", + expected: "px", + }, + ]; + + for (const { desc, expr, expected } of tests) { + equal(getUnit(expr), expected, desc); + } +} diff --git a/devtools/server/tests/xpcshell/test_source-01.js b/devtools/server/tests/xpcshell/test_source-01.js new file mode 100644 index 0000000000..5cb7a6da52 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_source-01.js @@ -0,0 +1,58 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// This test ensures that we can create SourceActors and SourceFronts properly, +// and that they can communicate over the protocol to fetch the source text for +// a given script. + +const SOURCE_URL = "http://example.com/foobar.js"; +const SOURCE_CONTENT = "stopMe()"; + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + DevToolsServer.LONG_STRING_LENGTH = 200; + + await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + const response = await threadFront.getSources(); + + Assert.ok(!!response); + Assert.ok(!!response.sources); + + const source = response.sources.filter(function (s) { + return s.url === SOURCE_URL; + })[0]; + + Assert.ok(!!source); + + const sourceFront = threadFront.source(source); + const response2 = await sourceFront.source(); + + Assert.ok(!!response2); + Assert.ok(!!response2.contentType); + Assert.ok(response2.contentType.includes("javascript")); + + Assert.ok(!!response2.source); + Assert.equal(SOURCE_CONTENT, response2.source); + + await threadFront.resume(); + }) +); + +function evaluateTestCode(debuggee) { + Cu.evalInSandbox( + "" + + function stopMe(arg1) { + debugger; + }, + debuggee, + "1.8", + getFileUrl("test_source-01.js") + ); + + Cu.evalInSandbox(SOURCE_CONTENT, debuggee, "1.8", SOURCE_URL); +} diff --git a/devtools/server/tests/xpcshell/test_source-02.js b/devtools/server/tests/xpcshell/test_source-02.js new file mode 100644 index 0000000000..9cb88cb0e4 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_source-02.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// This test ensures that we can create SourceActors and SourceFronts properly, +// and that they can communicate over the protocol to fetch the source text for +// a given script. + +const SOURCE_URL = "http://example.com/foobar.js"; +const SOURCE_CONTENT = ` + stopMe(); + for(var i = 0; i < 2; i++) { + debugger; + } +`; + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + DevToolsServer.LONG_STRING_LENGTH = 200; + + await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + + let response = await threadFront.getSources(); + Assert.ok(!!response); + Assert.ok(!!response.sources); + + const source = response.sources.filter(function (s) { + return s.url === SOURCE_URL; + })[0]; + + Assert.ok(!!source); + + const sourceFront = threadFront.source(source); + response = await sourceFront.getBreakpointPositionsCompressed(); + Assert.ok(!!response); + + Assert.deepEqual(response, { + 2: [2], + 3: [14, 17, 24], + 4: [4], + 6: [0], + }); + + await threadFront.resume(); + }) +); + +function evaluateTestCode(debuggee) { + Cu.evalInSandbox( + "" + + function stopMe(arg1) { + debugger; + }, + debuggee, + "1.8", + getFileUrl("test_source-02.js") + ); + + Cu.evalInSandbox(SOURCE_CONTENT, debuggee, "1.8", SOURCE_URL); +} diff --git a/devtools/server/tests/xpcshell/test_source-03.js b/devtools/server/tests/xpcshell/test_source-03.js new file mode 100644 index 0000000000..d0cd4839a0 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_source-03.js @@ -0,0 +1,75 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const SOURCE_URL = getFileUrl("source-03.js"); + +add_task( + threadFrontTest( + async ({ threadFront, server }) => { + const promise = waitForNewSource(threadFront, SOURCE_URL); + + // Create a two globals in the default junk sandbox compartment so that + // both globals are part of the same compartment. + server.allowNewThreadGlobals(); + const debuggee1 = Cu.Sandbox(systemPrincipal); + debuggee1.__name = "debuggee2.js"; + const debuggee2 = Cu.Sandbox(systemPrincipal); + debuggee2.__name = "debuggee2.js"; + server.disallowNewThreadGlobals(); + + // Load two copies of the source file. The first call to "loadSubScript" will + // create a ScriptSourceObject and a JSScript which references it. + // The second call will attempt to re-use JSScript objects because that is + // what loadSubScript does for instances of the same file that are loaded + // in the system principal in the same compartment. + // + // We explicitly want this because it is an edge case of the server. Most + // of the time a Debugger.Source will only have a single Debugger.Script + // associated with a given function, but in the context of explicitly + // cloned JSScripts, this is not the case, and we need to handle that. + loadSubScript(SOURCE_URL, debuggee1); + loadSubScript(SOURCE_URL, debuggee2); + + await promise; + + // We want to set a breakpoint and make sure that the breakpoint is properly + // set on _both_ files backed + await setBreakpoint(threadFront, { + sourceUrl: SOURCE_URL, + line: 4, + }); + + const { sources } = await getSources(threadFront); + + // Note: Since we load the file twice, we end up with two copies of the + // source object, and so two sources here. + Assert.equal(sources.length, 2); + + // Ensure that the breakpoint was properly applied to the JSScipt loaded + // in the first global. + let pausedOne = false; + let onResumed = null; + threadFront.once("paused", function (packet) { + pausedOne = true; + onResumed = resume(threadFront); + }); + Cu.evalInSandbox("init()", debuggee1, "1.8", "test.js", 1); + await onResumed; + Assert.equal(pausedOne, true); + + // Ensure that the breakpoint was properly applied to the JSScipt loaded + // in the second global. + let pausedTwo = false; + threadFront.once("paused", function (packet) { + pausedTwo = true; + onResumed = resume(threadFront); + }); + Cu.evalInSandbox("init()", debuggee2, "1.8", "test.js", 1); + await onResumed; + Assert.equal(pausedTwo, true); + }, + { doNotRunWorker: true } + ) +); diff --git a/devtools/server/tests/xpcshell/test_source-04.js b/devtools/server/tests/xpcshell/test_source-04.js new file mode 100644 index 0000000000..a3e3bef25f --- /dev/null +++ b/devtools/server/tests/xpcshell/test_source-04.js @@ -0,0 +1,74 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const SOURCE_URL = getFileUrl("source-03.js"); + +add_task( + threadFrontTest( + async ({ threadFront, server }) => { + const promise = waitForNewSource(threadFront, SOURCE_URL); + + // Create two globals in the default junk sandbox compartment so that + // both globals are part of the same compartment. + server.allowNewThreadGlobals(); + const debuggee1 = Cu.Sandbox(systemPrincipal); + debuggee1.__name = "debuggee2.js"; + const debuggee2 = Cu.Sandbox(systemPrincipal); + debuggee2.__name = "debuggee2.js"; + server.disallowNewThreadGlobals(); + + // Load first copy of the source file. The first call to "loadSubScript" will + // create a ScriptSourceObject and a JSScript which references it. + loadSubScript(SOURCE_URL, debuggee1); + + await promise; + + // We want to set a breakpoint and make sure that the breakpoint is properly + // set on _both_ files backed + await setBreakpoint(threadFront, { + sourceUrl: SOURCE_URL, + line: 4, + }); + + const { sources } = await getSources(threadFront); + Assert.equal(sources.length, 1); + + // Ensure that the breakpoint was properly applied to the JSScipt loaded + // in the first global. + let pausedOne = false; + let onResumed = null; + threadFront.once("paused", function (packet) { + pausedOne = true; + onResumed = resume(threadFront); + }); + Cu.evalInSandbox("init()", debuggee1, "1.8", "test.js", 1); + await onResumed; + Assert.equal(pausedOne, true); + + // Load second copy of the source file. The second call will attempt to + // re-use JSScript objects because that is what loadSubScript does for + // instances of the same file that are loaded in the system principal in + // the same compartment. + // + // We explicitly want this because it is an edge case of the server. Most + // of the time a Debugger.Source will only have a single Debugger.Script + // associated with a given function, but in the context of explicitly + // cloned JSScripts, this is not the case, and we need to handle that. + loadSubScript(SOURCE_URL, debuggee2); + + // Ensure that the breakpoint was properly applied to the JSScipt loaded + // in the second global. + let pausedTwo = false; + threadFront.once("paused", function (packet) { + pausedTwo = true; + onResumed = resume(threadFront); + }); + Cu.evalInSandbox("init()", debuggee2, "1.8", "test.js", 1); + await onResumed; + Assert.equal(pausedTwo, true); + }, + { doNotRunWorker: true } + ) +); diff --git a/devtools/server/tests/xpcshell/test_stepping-01.js b/devtools/server/tests/xpcshell/test_stepping-01.js new file mode 100644 index 0000000000..0c66404510 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_stepping-01.js @@ -0,0 +1,94 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check scenarios where we're leaving function a and + * going to the function b's call-site. + */ + +async function testFinish({ threadFront, devToolsClient }) { + await close(devToolsClient); + + do_test_finished(); +} + +async function invokeAndPause({ global, threadFront }, expression) { + return executeOnNextTickAndWaitForPause( + () => Cu.evalInSandbox(expression, global), + threadFront + ); +} + +async function step(threadFront, cmd) { + return cmd(threadFront); +} + +function getPauseLocation(packet) { + const { line, column } = packet.frame.where; + return { line, column }; +} + +function getPauseReturn(packet) { + return packet.why.frameFinished.return; +} + +async function steps(threadFront, sequence) { + const locations = []; + for (const cmd of sequence) { + const packet = await step(threadFront, cmd); + locations.push(getPauseLocation(packet)); + } + return locations; +} + +async function stepOutOfA(dbg, func, expectedLocation) { + await invokeAndPause(dbg, `${func}()`); + const { threadFront } = dbg; + await steps(threadFront, [stepOver, stepIn]); + + const packet = await stepOut(threadFront); + + deepEqual( + getPauseLocation(packet), + expectedLocation, + `step out location in ${func}` + ); + + await threadFront.resume(); +} + +async function stepOverInA(dbg, func, expectedLocation) { + await invokeAndPause(dbg, `${func}()`); + const { threadFront } = dbg; + await steps(threadFront, [stepOver, stepIn]); + + let packet = await stepOver(threadFront); + equal(getPauseReturn(packet).ownPropertyLength, 1, "a() is returning obj"); + + packet = await stepOver(threadFront); + deepEqual( + getPauseLocation(packet), + expectedLocation, + `step out location in ${func}` + ); + await dbg.threadFront.resume(); +} + +async function testStep(dbg, func, expectedValue) { + await stepOverInA(dbg, func, expectedValue); + await stepOutOfA(dbg, func, expectedValue); +} + +function run_test() { + return (async function () { + const dbg = await setupTestFromUrl("stepping.js"); + + await testStep(dbg, "arithmetic", { line: 16, column: 8 }); + await testStep(dbg, "composition", { line: 21, column: 3 }); + await testStep(dbg, "chaining", { line: 26, column: 6 }); + + await testFinish(dbg); + })(); +} diff --git a/devtools/server/tests/xpcshell/test_stepping-02.js b/devtools/server/tests/xpcshell/test_stepping-02.js new file mode 100644 index 0000000000..c9df671839 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_stepping-02.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check basic step-in functionality. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + dumpn("Evaluating test code and waiting for first debugger statement"); + const dbgStmt = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + equal( + dbgStmt.frame.where.line, + 2, + "Should be at debugger statement on line 2" + ); + equal(debuggee.a, undefined); + equal(debuggee.b, undefined); + + const step1 = await stepIn(threadFront); + equal(step1.why.type, "resumeLimit"); + equal(step1.frame.where.line, 3); + equal(debuggee.a, undefined); + equal(debuggee.b, undefined); + + const step3 = await stepIn(threadFront); + equal(step3.why.type, "resumeLimit"); + equal(step3.frame.where.line, 4); + equal(debuggee.a, 1); + equal(debuggee.b, undefined); + + const step4 = await stepIn(threadFront); + equal(step4.why.type, "resumeLimit"); + equal(step4.frame.where.line, 4); + equal(debuggee.a, 1); + equal(debuggee.b, 2); + }) +); + +function evaluateTestCode(debuggee) { + // prettier-ignore + Cu.evalInSandbox( + ` // 1 + debugger; // 2 + var a = 1; // 3 + var b = 2;`, // 4 + debuggee, + "1.8", + "test_stepping-01-test-code.js", + 1 + ); +} diff --git a/devtools/server/tests/xpcshell/test_stepping-03.js b/devtools/server/tests/xpcshell/test_stepping-03.js new file mode 100644 index 0000000000..88422ac0cc --- /dev/null +++ b/devtools/server/tests/xpcshell/test_stepping-03.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check basic step-out functionality. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + dumpn("Evaluating test code and waiting for first debugger statement"); + await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + + const step1 = await stepOut(threadFront); + equal(step1.frame.where.line, 8); + equal(step1.why.type, "resumeLimit"); + + equal(debuggee.a, 1); + equal(debuggee.b, 2); + }) +); + +function evaluateTestCode(debuggee) { + // prettier-ignore + Cu.evalInSandbox( + ` // 1 + function f() { // 2 + debugger; // 3 + this.a = 1; // 4 + this.b = 2; // 5 + } // 6 + f(); // 7 + `, // 8 + debuggee, + "1.8", + "test_stepping-01-test-code.js", + 1 + ); +} diff --git a/devtools/server/tests/xpcshell/test_stepping-04.js b/devtools/server/tests/xpcshell/test_stepping-04.js new file mode 100644 index 0000000000..37a9f843d0 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_stepping-04.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that stepping over a function call does not pause inside the function. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + dumpn("Evaluating test code and waiting for first debugger statement"); + await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + + dumpn("Step Over to f()"); + const step1 = await stepOver(threadFront); + equal(step1.why.type, "resumeLimit"); + equal(step1.frame.where.line, 6); + equal(debuggee.a, undefined); + equal(debuggee.b, undefined); + + dumpn("Step Over f()"); + const step2 = await stepOver(threadFront); + equal(step2.frame.where.line, 7); + equal(step2.why.type, "resumeLimit"); + equal(debuggee.a, 1); + equal(debuggee.b, undefined); + }) +); + +function evaluateTestCode(debuggee) { + // prettier-ignore + Cu.evalInSandbox( + ` // 1 + function f() { // 2 + this.a = 1; // 3 + } // 4 + debugger; // 5 + f(); // 6 + let b = 2; // 7 + `, // 8 + debuggee, + "1.8", + "test_stepping-01-test-code.js", + 1 + ); +} diff --git a/devtools/server/tests/xpcshell/test_stepping-05.js b/devtools/server/tests/xpcshell/test_stepping-05.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_stepping-05.js diff --git a/devtools/server/tests/xpcshell/test_stepping-06.js b/devtools/server/tests/xpcshell/test_stepping-06.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_stepping-06.js diff --git a/devtools/server/tests/xpcshell/test_stepping-07.js b/devtools/server/tests/xpcshell/test_stepping-07.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_stepping-07.js diff --git a/devtools/server/tests/xpcshell/test_stepping-08.js b/devtools/server/tests/xpcshell/test_stepping-08.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_stepping-08.js diff --git a/devtools/server/tests/xpcshell/test_stepping-09.js b/devtools/server/tests/xpcshell/test_stepping-09.js new file mode 100644 index 0000000000..da59ed963c --- /dev/null +++ b/devtools/server/tests/xpcshell/test_stepping-09.js @@ -0,0 +1,47 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that step out stops at the end of the parent if it fails to stop + * anywhere else. Bug 1504358. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + dumpn("Evaluating test code and waiting for first debugger statement"); + const dbgStmt = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + equal( + dbgStmt.frame.where.line, + 2, + "Should be at debugger statement on line 2" + ); + + dumpn("Step out of inner and into outer"); + const step2 = await stepOut(threadFront); + // The bug was that we'd step right past the end of the function and never pause. + equal(step2.frame.where.line, 2); + equal(step2.frame.where.column, 31); + deepEqual(step2.why.frameFinished.return, { type: "undefined" }); + }) +); + +function evaluateTestCode(debuggee) { + // By placing the inner and outer on the same line, this triggers the server's + // logic to skip steps for these functions, meaning that onPop is the only + // thing that will cause it to pop. + Cu.evalInSandbox( + ` + function outer(){ inner(); return 42; } function inner(){ debugger; } + outer(); + `, + debuggee, + "1.8", + "test_stepping-09-test-code.js", + 1 + ); +} diff --git a/devtools/server/tests/xpcshell/test_stepping-10.js b/devtools/server/tests/xpcshell/test_stepping-10.js new file mode 100644 index 0000000000..6ea95c3fd3 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_stepping-10.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that step out stops at the parent and the parent's parent. + * This checks for the failure found in bug 1530549. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + dumpn("Evaluating test code and waiting for first debugger statement"); + const dbgStmt = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + equal( + dbgStmt.frame.where.line, + 3, + "Should be at debugger statement on line 3" + ); + + dumpn("Step out of inner and into var statement IIFE"); + const step2 = await stepOut(threadFront); + equal(step2.frame.where.line, 4); + deepEqual(step2.why.frameFinished.return, { type: "undefined" }); + + dumpn("Step out of vars and into script body"); + const step3 = await stepOut(threadFront); + equal(step3.frame.where.line, 9); + deepEqual(step3.why.frameFinished.return, { type: "undefined" }); + }) +); + +function evaluateTestCode(debuggee) { + Cu.evalInSandbox( + ` + (function() { + (function(){debugger;})(); + var a = 1; + a = 2; + a = 3; + a = 4; + })(); + `, + debuggee, + "1.8", + "test_stepping-10-test-code.js", + 1 + ); +} diff --git a/devtools/server/tests/xpcshell/test_stepping-11.js b/devtools/server/tests/xpcshell/test_stepping-11.js new file mode 100644 index 0000000000..8cbd285d89 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_stepping-11.js @@ -0,0 +1,25 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check basic stepping for console evaluations. + */ + +add_task( + threadFrontTest(async ({ commands, threadFront }) => { + dumpn("Evaluating test code and waiting for first debugger statement"); + + commands.scriptCommand.execute(`(function(){ + debugger; + var a = 1; + var b = 2; + })();`); + + await waitForEvent(threadFront, "paused"); + const packet = await stepOver(threadFront); + Assert.equal(packet.frame.where.line, 3, "step to line 3"); + await threadFront.resume(); + }) +); diff --git a/devtools/server/tests/xpcshell/test_stepping-12.js b/devtools/server/tests/xpcshell/test_stepping-12.js new file mode 100644 index 0000000000..de96faf59f --- /dev/null +++ b/devtools/server/tests/xpcshell/test_stepping-12.js @@ -0,0 +1,162 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that step out stops at the parent and the parent's parent. + * This checks for the failure found in bug 1530549. + */ + +const sourceUrl = "test_stepping-10-test-code.js"; + +add_task( + threadFrontTest(async args => { + dumpn("Evaluating test code and waiting for first debugger statement"); + + await testGenerator(args); + await testAwait(args); + await testInterleaving(args); + await testMultipleSteps(args); + }) +); + +async function testAwait({ threadFront, debuggee }) { + function evaluateTestCode() { + Cu.evalInSandbox( + ` + (async function() { + debugger; + r = await Promise.resolve('yay'); + a = 4; + })(); + `, + debuggee, + "1.8", + sourceUrl, + 1 + ); + } + + await executeOnNextTickAndWaitForPause(evaluateTestCode, threadFront); + + dumpn("Step Over and land on line 5"); + const step1 = await stepOver(threadFront); + equal(step1.frame.where.line, 4); + equal(step1.frame.where.column, 10); + + const step2 = await stepOver(threadFront); + equal(step2.frame.where.line, 5); + equal(step2.frame.where.column, 10); + equal(debuggee.r, "yay"); + await resume(threadFront); +} + +async function testInterleaving({ threadFront, debuggee }) { + function evaluateTestCode() { + Cu.evalInSandbox( + ` + (async function simpleRace() { + debugger; + this.result = await new Promise((r) => { + Promise.resolve().then(() => { debugger }); + Promise.resolve().then(r('yay')) + }) + var a = 2; + debugger; + })() + `, + debuggee, + "1.8", + sourceUrl, + 1 + ); + } + + await executeOnNextTickAndWaitForPause(evaluateTestCode, threadFront); + + dumpn("Step Over and land on line 5"); + const step1 = await stepOver(threadFront); + equal(step1.frame.where.line, 4); + + const step2 = await stepOver(threadFront); + equal(step2.frame.where.line, 5); + equal(step2.frame.where.column, 43); + + const step3 = await resumeAndWaitForPause(threadFront); + equal(step3.frame.where.line, 9); + equal(debuggee.result, "yay"); + + await resume(threadFront); +} + +async function testMultipleSteps({ threadFront, debuggee }) { + function evaluateTestCode() { + Cu.evalInSandbox( + ` + (async function simpleRace() { + debugger; + await Promise.resolve(); + var a = 2; + await Promise.resolve(); + var b = 2; + await Promise.resolve(); + debugger; + })() + `, + debuggee, + "1.8", + sourceUrl, + 1 + ); + } + + await executeOnNextTickAndWaitForPause(evaluateTestCode, threadFront); + + const step1 = await stepOver(threadFront); + equal(step1.frame.where.line, 4); + + const step2 = await stepOver(threadFront); + equal(step2.frame.where.line, 5); + + const step3 = await stepOver(threadFront); + equal(step3.frame.where.line, 6); + resume(threadFront); +} + +async function testGenerator({ threadFront, debuggee }) { + function evaluateTestCode() { + Cu.evalInSandbox( + ` + (async function() { + function* makeSteps() { + debugger; + yield 1; + yield 2; + return 3; + } + const s = makeSteps(); + s.next(); + s.next(); + s.next(); + })() + `, + debuggee, + "1.8", + sourceUrl, + 1 + ); + } + + await executeOnNextTickAndWaitForPause(evaluateTestCode, threadFront); + + const step1 = await stepOver(threadFront); + equal(step1.frame.where.line, 5); + + const step2 = await stepOver(threadFront); + equal(step2.frame.where.line, 6); + + const step3 = await stepOver(threadFront); + equal(step3.frame.where.line, 7); + await resume(threadFront); +} diff --git a/devtools/server/tests/xpcshell/test_stepping-13.js b/devtools/server/tests/xpcshell/test_stepping-13.js new file mode 100644 index 0000000000..cbdb78ce2d --- /dev/null +++ b/devtools/server/tests/xpcshell/test_stepping-13.js @@ -0,0 +1,39 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* + * Check that is possible to step into both the inner and outer function + * calls. + */ + +add_task( + threadFrontTest(async ({ commands, threadFront }) => { + dumpn("Evaluating test code and waiting for first debugger statement"); + + commands.scriptCommand.execute( + `(function () { + const a = () => { return 2 }; + debugger; + a(a()) + })()` + ); + + await waitForEvent(threadFront, "paused"); + const step1 = await stepOver(threadFront); + Assert.equal(step1.frame.where.line, 4, "step to line 4"); + + const step2 = await stepIn(threadFront); + Assert.equal(step2.frame.where.line, 2, "step in to line 2"); + + const step3 = await stepOut(threadFront); + Assert.equal(step3.frame.where.line, 4, "step back to line 4"); + Assert.equal(step3.frame.where.column, 9, "step out to column 9"); + + const step4 = await stepIn(threadFront); + Assert.equal(step4.frame.where.line, 2, "step in to line 2"); + + await threadFront.resume(); + }) +); diff --git a/devtools/server/tests/xpcshell/test_stepping-14.js b/devtools/server/tests/xpcshell/test_stepping-14.js new file mode 100644 index 0000000000..6d64a53a66 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_stepping-14.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* + * Check that is possible to step into both the inner and outer function + * calls. + */ + +add_task( + threadFrontTest(async ({ commands, threadFront }) => { + dumpn("Evaluating test code and waiting for first debugger statement"); + + commands.scriptCommand.execute(`(function () { + async function f() { + const p = Promise.resolve(43); + await p; + return p; + } + + function call_f() { + Promise.resolve(42).then(forty_two => { + return forty_two; + }); + + f().then(v => { + return v; + }); + } + debugger; + call_f(); + })()`); + + const packet = await waitForEvent(threadFront, "paused"); + const location = { + sourceId: packet.frame.where.actor, + line: 4, + column: 10, + }; + + await threadFront.setBreakpoint(location, {}); + + const packet2 = await resumeAndWaitForPause(threadFront); + Assert.equal(packet2.frame.where.line, 4, "landed at await"); + + const packet3 = await stepIn(threadFront); + Assert.equal(packet3.frame.where.line, 5, "step to the next line"); + + await threadFront.resume(); + }) +); diff --git a/devtools/server/tests/xpcshell/test_stepping-15.js b/devtools/server/tests/xpcshell/test_stepping-15.js new file mode 100644 index 0000000000..9e79b93687 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_stepping-15.js @@ -0,0 +1,78 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test stepping from inside a blackboxed function + * test-page: https://dbg-blackbox-stepping.glitch.me/ + */ + +async function invokeAndPause({ global, threadFront }, expression, url) { + return executeOnNextTickAndWaitForPause( + () => Cu.evalInSandbox(expression, global, "1.8", url, 1), + threadFront + ); +} +add_task( + threadFrontTest(async ({ commands, threadFront, debuggee }) => { + const dbg = { global: debuggee, threadFront }; + + // Test stepping from a blackboxed location + async function testStepping(action, expectedLine) { + commands.scriptCommand.execute(`outermost()`); + await waitForPause(threadFront); + await blackBox(blackboxedSourceFront); + const packet = await action(threadFront); + const { line, actor } = packet.frame.where; + equal(actor, unblackboxedActor, "paused in unblackboxed source"); + equal(line, expectedLine, "paused at correct line"); + await threadFront.resume(); + await unBlackBox(blackboxedSourceFront); + } + + invokeAndPause( + dbg, + `function outermost() { + const value = blackboxed1(); + return value + 1; + } + function innermost() { + return 1; + }`, + "http://example.com/unblackboxed.js" + ); + invokeAndPause( + dbg, + `function blackboxed1() { + return blackboxed2(); + } + function blackboxed2() { + return innermost(); + }`, + "http://example.com/blackboxed.js" + ); + + const { sources } = await getSources(threadFront); + const blackboxedSourceFront = threadFront.source( + sources.find(source => source.url == "http://example.com/blackboxed.js") + ); + const unblackboxedActor = sources.find( + source => source.url == "http://example.com/unblackboxed.js" + ).actor; + + await setBreakpoint(threadFront, { + sourceUrl: blackboxedSourceFront.url, + line: 5, + }); + + info("Step Out to outermost"); + await testStepping(stepOut, 3); + + info("Step Over to outermost"); + await testStepping(stepOver, 3); + + info("Step In to innermost"); + await testStepping(stepIn, 6); + }) +); diff --git a/devtools/server/tests/xpcshell/test_stepping-16.js b/devtools/server/tests/xpcshell/test_stepping-16.js new file mode 100644 index 0000000000..e3bd94b747 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_stepping-16.js @@ -0,0 +1,81 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test stepping from inside a blackboxed function + * test-page: https://dbg-blackbox-stepping2.glitch.me/ + */ + +async function invokeAndPause({ global, threadFront }, expression, url) { + return executeOnNextTickAndWaitForPause( + () => Cu.evalInSandbox(expression, global, "1.8", url, 1), + threadFront + ); +} + +add_task( + threadFrontTest(async ({ commands, threadFront, debuggee }) => { + const dbg = { global: debuggee, threadFront }; + invokeAndPause( + dbg, + `function outermost() { + blackboxed( + function inner1() { + return 1; + }, + function inner2() { + return 2; + } + ); + }`, + "http://example.com/unblackboxed.js" + ); + invokeAndPause( + dbg, + `function blackboxed(...args) { + for (const arg of args) { + arg(); + } + }`, + "http://example.com/blackboxed.js" + ); + + const { sources } = await getSources(threadFront); + const blackboxedSourceFront = threadFront.source( + sources.find(source => source.url == "http://example.com/blackboxed.js") + ); + const unblackboxedSource = sources.find( + source => source.url == "http://example.com/unblackboxed.js" + ); + const unblackboxedActor = unblackboxedSource.actor; + const unblackboxedSourceFront = threadFront.source(unblackboxedSource); + + await setBreakpoint(threadFront, { + sourceUrl: unblackboxedSourceFront.url, + line: 4, + }); + blackBox(blackboxedSourceFront); + + async function testStepping(action, expectedLine) { + commands.scriptCommand.execute("outermost()"); + await waitForPause(threadFront); + await stepOver(threadFront); + const packet = await action(threadFront); + const { actor, line } = packet.frame.where; + equal(actor, unblackboxedActor, "Paused in unblackboxed source"); + equal(line, expectedLine, "Paused at correct line"); + await threadFront.resume(); + } + + info("Step Out to outermost"); + await testStepping(stepOut, 10); + + info("Step Over to outermost"); + await testStepping(stepOver, 10); + + info("Step In to inner2"); + await testStepping(stepIn, 7); + }) +); diff --git a/devtools/server/tests/xpcshell/test_stepping-17.js b/devtools/server/tests/xpcshell/test_stepping-17.js new file mode 100644 index 0000000000..816946fa4c --- /dev/null +++ b/devtools/server/tests/xpcshell/test_stepping-17.js @@ -0,0 +1,69 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* + * Check that you can step from one script or event to another + */ + +add_task( + threadFrontTest(async ({ commands, threadFront, debuggee }) => { + Cu.evalInSandbox( + `function blackboxed(callback) { return () => callback(); }`, + debuggee, + "1.8", + "http://example.com/blackboxed.js", + 1 + ); + + const { sources } = await getSources(threadFront); + const blackboxedSourceFront = threadFront.source( + sources.find(source => source.url == "http://example.com/blackboxed.js") + ); + blackBox(blackboxedSourceFront); + + const testStepping = async function (wrapperName, stepHandler, message) { + commands.scriptCommand.execute(`(function () { + const p = Promise.resolve(); + p.then(${wrapperName}(() => { debugger; })) + .then(${wrapperName}(() => { })); + })();`); + + await waitForEvent(threadFront, "paused"); + const step = await stepHandler(threadFront); + Assert.equal(step.frame.where.line, 4, message); + await resume(threadFront); + }; + + const stepTwice = async function () { + await stepOver(threadFront); + return stepOver(threadFront); + }; + + await testStepping("", stepTwice, "Step over on the outermost frame"); + await testStepping("blackboxed", stepTwice, "Step over with blackboxing"); + await testStepping("", stepOut, "Step out on the outermost frame"); + await testStepping("blackboxed", stepOut, "Step out with blackboxing"); + + commands.scriptCommand.execute(`(async function () { + const p = Promise.resolve(); + const p2 = p.then(() => { + debugger; + return "async stepping!"; + }); + debugger; + await p; + const result = await p2; + return result; + })(); + `); + + await waitForEvent(threadFront, "paused"); + await stepOver(threadFront); + await stepOver(threadFront); + const step = await stepOut(threadFront); + await resume(threadFront); + Assert.equal(step.frame.where.line, 9, "Step out of promise into async fn"); + }) +); diff --git a/devtools/server/tests/xpcshell/test_stepping-18.js b/devtools/server/tests/xpcshell/test_stepping-18.js new file mode 100644 index 0000000000..e8581835d3 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_stepping-18.js @@ -0,0 +1,100 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check scenarios where we're leaving function a and + * going to the function b's call-site. + */ + +async function testFinish({ threadFront, devToolsClient }) { + await close(devToolsClient); + + do_test_finished(); +} + +async function invokeAndPause({ global, threadFront }, expression) { + return executeOnNextTickAndWaitForPause( + () => Cu.evalInSandbox(expression, global), + threadFront + ); +} + +async function steps(threadFront, sequence) { + const locations = []; + for (const cmd of sequence) { + const packet = await step(threadFront, cmd); + locations.push(getPauseLocation(packet)); + } + return locations; +} + +async function step(threadFront, cmd) { + return cmd(threadFront); +} + +function getPauseLocation(packet) { + const { line, column } = packet.frame.where; + return { line, column }; +} + +async function stepOutOfA(dbg, func, frameIndex, expectedLocation) { + const { threadFront } = dbg; + + info("pause and step into a()"); + await invokeAndPause(dbg, `${func}()`); + await steps(threadFront, [stepOver, stepIn, stepIn]); + + const { frames } = await threadFront.frames(0, 5); + const frameActorID = frames[frameIndex].actorID; + const packet = await stepOut(threadFront, frameActorID); + + deepEqual( + getPauseLocation(packet), + expectedLocation, + `step over location in ${func}` + ); + + await dbg.threadFront.resume(); +} + +async function stepOverInA(dbg, func, frameIndex, expectedLocation) { + const { threadFront } = dbg; + + info("pause and step into a()"); + await invokeAndPause(dbg, `${func}()`); + await steps(threadFront, [stepOver, stepIn]); + + const { frames } = await threadFront.frames(0, 5); + const frameActorID = frames[frameIndex].actorID; + const packet = await stepOver(threadFront, frameActorID); + + deepEqual( + getPauseLocation(packet), + expectedLocation, + `step over location in ${func}` + ); + + await dbg.threadFront.resume(); +} + +function run_test() { + return (async function () { + const dbg = await setupTestFromUrl("stepping.js"); + + info(`Test step over with the 1st frame`); + await stepOverInA(dbg, "arithmetic", 0, { line: 8, column: 0 }); + + info(`Test step over with the 2nd frame`); + await stepOverInA(dbg, "arithmetic", 1, { line: 17, column: 0 }); + + info(`Test step out with the 1st frame`); + await stepOutOfA(dbg, "nested", 0, { line: 31, column: 0 }); + + info(`Test step out with the 2nd frame`); + await stepOutOfA(dbg, "nested", 1, { line: 36, column: 0 }); + + await testFinish(dbg); + })(); +} diff --git a/devtools/server/tests/xpcshell/test_stepping-19.js b/devtools/server/tests/xpcshell/test_stepping-19.js new file mode 100644 index 0000000000..7ab21c7b66 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_stepping-19.js @@ -0,0 +1,93 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that step out stops at the async parent's frame. + */ + +async function testFinish({ threadFront, devToolsClient }) { + await close(devToolsClient); + + do_test_finished(); +} + +async function invokeAndPause({ global, threadFront }, expression) { + return executeOnNextTickAndWaitForPause( + () => Cu.evalInSandbox(expression, global), + threadFront + ); +} + +async function steps(threadFront, sequence) { + const locations = []; + for (const cmd of sequence) { + const packet = await step(threadFront, cmd); + locations.push(getPauseLocation(packet)); + } + return locations; +} + +async function step(threadFront, cmd) { + return cmd(threadFront); +} + +function getPauseLocation(packet) { + const { line, column } = packet.frame.where; + return { line, column }; +} + +async function stepOutBeforeTimer(dbg, func, frameIndex, expectedLocation) { + const { threadFront } = dbg; + + await invokeAndPause(dbg, `${func}()`); + await steps(threadFront, [stepOver, stepIn]); + + const { frames } = await threadFront.frames(0, 5); + const frameActorID = frames[frameIndex].actorID; + const packet = await stepOut(threadFront, frameActorID); + + deepEqual( + getPauseLocation(packet), + expectedLocation, + `step out location in ${func}` + ); + + await resumeAndWaitForPause(threadFront); + await resume(threadFront); +} + +async function stepOutAfterTimer(dbg, func, frameIndex, expectedLocation) { + const { threadFront } = dbg; + + await invokeAndPause(dbg, `${func}()`); + await steps(threadFront, [stepOver, stepIn, stepOver, stepOver]); + + const { frames } = await threadFront.frames(0, 5); + const frameActorID = frames[frameIndex].actorID; + const packet = await stepOut(threadFront, frameActorID); + + deepEqual( + getPauseLocation(packet), + expectedLocation, + `step out location in ${func}` + ); + + await resumeAndWaitForPause(threadFront); + await dbg.threadFront.resume(); +} + +function run_test() { + return (async function () { + const dbg = await setupTestFromUrl("stepping-async.js"); + + info(`Test stepping out before timer;`); + await stepOutBeforeTimer(dbg, "stuff", 0, { line: 27, column: 2 }); + + info(`Test stepping out after timer;`); + await stepOutAfterTimer(dbg, "stuff", 0, { line: 29, column: 2 }); + + await testFinish(dbg); + })(); +} diff --git a/devtools/server/tests/xpcshell/test_stepping-with-skip-breakpoints.js b/devtools/server/tests/xpcshell/test_stepping-with-skip-breakpoints.js new file mode 100644 index 0000000000..3ec4fd994d --- /dev/null +++ b/devtools/server/tests/xpcshell/test_stepping-with-skip-breakpoints.js @@ -0,0 +1,84 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check basic step-over functionality with pause points + * for the first statement and end of the last statement. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + dumpn("Evaluating test code and waiting for first debugger statement"); + const dbgStmt = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + equal( + dbgStmt.frame.where.line, + 2, + "Should be at debugger statement on line 2" + ); + equal(debuggee.a, undefined); + equal(debuggee.b, undefined); + + const source = await getSource( + threadFront, + "test_stepping-01-test-code.js" + ); + + // Add pause points for the first and end of the last statement. + // Note: we intentionally ignore the second statement. + source.setPausePoints([ + { + location: { line: 3, column: 8 }, + types: { breakpoint: true, stepOver: true }, + }, + { + location: { line: 4, column: 14 }, + types: { breakpoint: true, stepOver: true }, + }, + ]); + + dumpn("Step Over to line 3"); + const step1 = await stepOver(threadFront); + equal(step1.why.type, "resumeLimit"); + equal(step1.frame.where.line, 3); + equal(step1.frame.where.column, 12); + + equal(debuggee.a, undefined); + equal(debuggee.b, undefined); + + dumpn("Step Over to line 4"); + const step2 = await stepOver(threadFront); + equal(step2.why.type, "resumeLimit"); + equal(step2.frame.where.line, 4); + equal(step2.frame.where.column, 12); + + equal(debuggee.a, 1); + equal(debuggee.b, undefined); + + dumpn("Step Over to the end of line 4"); + const step3 = await stepOver(threadFront); + equal(step3.why.type, "resumeLimit"); + equal(step3.frame.where.line, 4); + equal(step3.frame.where.column, 14); + equal(debuggee.a, 1); + equal(debuggee.b, 2); + }) +); + +function evaluateTestCode(debuggee) { + // prettier-ignore + Cu.evalInSandbox( + ` // 1 + debugger; // 2 + var a = 1; // 3 + var b = 2;`, // 4 + debuggee, + "1.8", + "test_stepping-01-test-code.js", + 1 + ); +} diff --git a/devtools/server/tests/xpcshell/test_symbolactor.js b/devtools/server/tests/xpcshell/test_symbolactor.js new file mode 100644 index 0000000000..0d04a2bd1d --- /dev/null +++ b/devtools/server/tests/xpcshell/test_symbolactor.js @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { + SymbolActor, +} = require("resource://devtools/server/actors/object/symbol.js"); + +function run_test() { + test_SA_destroy(); + test_SA_form(); + test_SA_raw(); +} + +const SYMBOL_NAME = "abc"; +const TEST_SYMBOL = Symbol(SYMBOL_NAME); + +function makeMockSymbolActor() { + const symbol = TEST_SYMBOL; + const mockConn = null; + const actor = new SymbolActor(mockConn, symbol); + actor.actorID = "symbol1"; + const parentPool = { + symbolActors: { + [symbol]: actor, + }, + unmanage: () => {}, + }; + actor.getParent = () => parentPool; + return actor; +} + +function test_SA_destroy() { + const actor = makeMockSymbolActor(); + strictEqual(actor.getParent().symbolActors[TEST_SYMBOL], actor); + + actor.destroy(); + strictEqual(TEST_SYMBOL in actor.getParent().symbolActors, false); +} + +function test_SA_form() { + const actor = makeMockSymbolActor(); + const form = actor.form(); + strictEqual(form.type, "symbol"); + strictEqual(form.actor, actor.actorID); + strictEqual(form.name, SYMBOL_NAME); +} + +function test_SA_raw() { + const actor = makeMockSymbolActor(); + strictEqual(actor.rawValue(), TEST_SYMBOL); +} diff --git a/devtools/server/tests/xpcshell/test_symbols-01.js b/devtools/server/tests/xpcshell/test_symbols-01.js new file mode 100644 index 0000000000..5352542e83 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_symbols-01.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that we can represent ES6 Symbols over the RDP. + */ + +const URL = "foo.js"; + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + await testSymbols(threadFront, debuggee); + }) +); + +async function testSymbols(threadFront, debuggee) { + const evalCode = () => { + /* eslint-disable mozilla/var-only-at-top-level, no-unused-vars */ + // prettier-ignore + Cu.evalInSandbox( + "(" + function () { + var symbolWithName = Symbol("Chris"); + var symbolWithoutName = Symbol(); + var iteratorSymbol = Symbol.iterator; + debugger; + } + "())", + debuggee, + "1.8", + URL, + 1 + ); + /* eslint-enable mozilla/var-only-at-top-level, no-unused-vars */ + }; + + const packet = await executeOnNextTickAndWaitForPause(evalCode, threadFront); + const environment = await packet.frame.getEnvironment(); + const { symbolWithName, symbolWithoutName, iteratorSymbol } = + environment.bindings.variables; + + equal(symbolWithName.value.type, "symbol"); + equal(symbolWithName.value.name, "Chris"); + + equal(symbolWithoutName.value.type, "symbol"); + ok(!("name" in symbolWithoutName.value)); + + equal(iteratorSymbol.value.type, "symbol"); + equal(iteratorSymbol.value.name, "Symbol.iterator"); +} diff --git a/devtools/server/tests/xpcshell/test_symbols-02.js b/devtools/server/tests/xpcshell/test_symbols-02.js new file mode 100644 index 0000000000..12d4ef80c8 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_symbols-02.js @@ -0,0 +1,44 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that we don't run debuggee code when getting symbol names. + */ + +const URL = "foo.js"; + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + await testSymbols(threadFront, debuggee); + }) +); + +async function testSymbols(threadFront, debuggee) { + const evalCode = () => { + /* eslint-disable mozilla/var-only-at-top-level, no-extend-native, no-unused-vars */ + // prettier-ignore + Cu.evalInSandbox( + "(" + function () { + Symbol.prototype.toString = () => { + throw new Error("lololol"); + }; + var sym = Symbol("le troll"); + debugger; + } + "())", + debuggee, + "1.8", + URL, + 1 + ); + /* eslint-enable mozilla/var-only-at-top-level, no-extend-native, no-unused-vars */ + }; + + const packet = await executeOnNextTickAndWaitForPause(evalCode, threadFront); + const environment = await packet.frame.getEnvironment(); + const { sym } = environment.bindings.variables; + + equal(sym.value.type, "symbol"); + equal(sym.value.name, "le troll"); +} diff --git a/devtools/server/tests/xpcshell/test_threadlifetime-01.js b/devtools/server/tests/xpcshell/test_threadlifetime-01.js new file mode 100644 index 0000000000..d2e8234fb9 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_threadlifetime-01.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that thread-lifetime grips last past a resume. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee, client }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + + const pauseGrip = packet.frame.arguments[0]; + + // Create a thread-lifetime actor for this object. + const response = await client.request({ + to: pauseGrip.actor, + type: "threadGrip", + }); + // Successful promotion won't return an error. + Assert.equal(response.error, undefined); + + const packet2 = await resumeAndWaitForPause(threadFront); + + // Verify that the promoted actor is returned again. + Assert.equal(pauseGrip.actor, packet2.frame.arguments[0].actor); + // Now that we've resumed, should get unrecognizePacketType for the + // promoted grip. + try { + await client.request({ to: pauseGrip.actor, type: "bogusRequest" }); + ok(false, "bogusRequest should throw"); + } catch (e) { + Assert.equal(e.error, "unrecognizedPacketType"); + ok(true, "bogusRequest thrown"); + } + await threadFront.resume(); + }) +); + +function evaluateTestCode(debuggee) { + debuggee.eval( + "(" + + function () { + function stopMe(arg1) { + debugger; + debugger; + } + stopMe({ obj: true }); + } + + ")()" + ); +} diff --git a/devtools/server/tests/xpcshell/test_threadlifetime-02.js b/devtools/server/tests/xpcshell/test_threadlifetime-02.js new file mode 100644 index 0000000000..c35350a48c --- /dev/null +++ b/devtools/server/tests/xpcshell/test_threadlifetime-02.js @@ -0,0 +1,73 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that thread-lifetime grips last past a resume. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee, client }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + + const pauseGrip = packet.frame.arguments[0]; + + // Create a thread-lifetime actor for this object. + const response = await client.request({ + to: pauseGrip.actor, + type: "threadGrip", + }); + // Successful promotion won't return an error. + Assert.equal(response.error, undefined); + + const packet2 = await resumeAndWaitForPause(threadFront); + + // Verify that the promoted actor is returned again. + Assert.equal(pauseGrip.actor, packet2.frame.arguments[0].actor); + // Now that we've resumed, release the thread-lifetime grip. + const objFront = new ObjectFront( + threadFront.conn, + threadFront.targetFront, + threadFront, + pauseGrip + ); + await objFront.release(); + const objFront2 = new ObjectFront( + threadFront.conn, + threadFront.targetFront, + threadFront, + pauseGrip + ); + + try { + await objFront2 + .request({ to: pauseGrip.actor, type: "bogusRequest" }) + .catch(function (error) { + Assert.ok(!!error.message.match(/noSuchActor/)); + threadFront.resume(); + throw new Error(); + }); + ok(false, "bogusRequest should throw"); + } catch (e) { + ok(true, "bogusRequest thrown"); + } + }) +); + +function evaluateTestCode(debuggee) { + debuggee.eval( + "(" + + function () { + function stopMe(arg1) { + debugger; + debugger; + } + stopMe({ obj: true }); + } + + ")()" + ); +} diff --git a/devtools/server/tests/xpcshell/test_threadlifetime-04.js b/devtools/server/tests/xpcshell/test_threadlifetime-04.js new file mode 100644 index 0000000000..6b815c7933 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_threadlifetime-04.js @@ -0,0 +1,58 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint-disable no-shadow, max-nested-callbacks */ + +"use strict"; + +/** + * Check that requesting a thread-lifetime actor twice for the same + * value returns the same actor. + */ + +var gDebuggee; +var gClient; +var gThreadFront; + +add_task( + threadFrontTest( + async ({ threadFront, debuggee, client }) => { + gThreadFront = threadFront; + gClient = client; + gDebuggee = debuggee; + test_thread_lifetime(); + }, + { waitForFinish: true } + ) +); + +function test_thread_lifetime() { + gThreadFront.once("paused", async function (packet) { + const pauseGrip = packet.frame.arguments[0]; + + const response = await gClient.request({ + to: pauseGrip.actor, + type: "threadGrip", + }); + const threadGrip1 = response.from; + + const response2 = await gClient.request({ + to: pauseGrip.actor, + type: "threadGrip", + }); + Assert.equal(threadGrip1, response2.from); + await gThreadFront.resume(); + + threadFrontTestFinished(); + }); + + gDebuggee.eval( + "(" + + function () { + function stopMe(arg1) { + debugger; + } + stopMe({ obj: true }); + } + + ")()" + ); +} diff --git a/devtools/server/tests/xpcshell/test_unsafeDereference.js b/devtools/server/tests/xpcshell/test_unsafeDereference.js new file mode 100644 index 0000000000..53b70420c6 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_unsafeDereference.js @@ -0,0 +1,130 @@ +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ + +/* eslint-disable strict */ + +// Test Debugger.Object.prototype.unsafeDereference in the presence of +// interesting cross-compartment wrappers. +// +// This is not really a devtools server test; it's more of a Debugger test. +// But we need xpcshell and Components.utils.Sandbox to get +// cross-compartment wrappers with interesting properties, and this is the +// xpcshell test directory most closely related to the JS Debugger API. + +addDebuggerToGlobal(globalThis); + +// Add a method to Debugger.Object for fetching value properties +// conveniently. +Debugger.Object.prototype.getProperty = function (name) { + const desc = this.getOwnPropertyDescriptor(name); + if (!desc) { + return undefined; + } + if (!desc.value) { + throw Error( + "Debugger.Object.prototype.getProperty: " + + "not a value property: " + + name + ); + } + return desc.value; +}; + +function run_test() { + // Create a low-privilege sandbox, and a chrome-privilege sandbox. + const contentBox = Cu.Sandbox("http://www.example.com"); + const chromeBox = Cu.Sandbox(this); + + // Create an objects in this compartment, and one in each sandbox. We'll + // refer to the objects as "mainObj", "contentObj", and "chromeObj", in + // variable and property names. + const mainObj = { name: "mainObj" }; + Cu.evalInSandbox('var contentObj = { name: "contentObj" };', contentBox); + Cu.evalInSandbox('var chromeObj = { name: "chromeObj" };', chromeBox); + + // Give each global a pointer to all the other globals' objects. + contentBox.mainObj = chromeBox.mainObj = mainObj; + const contentObj = (chromeBox.contentObj = contentBox.contentObj); + const chromeObj = (contentBox.chromeObj = chromeBox.chromeObj); + + // First, a whole bunch of basic sanity checks, to ensure that JavaScript + // evaluated in various scopes really does see the world the way this + // test expects it to. + + // The objects appear as global variables in the sandbox, and as + // the sandbox object's properties in chrome. + Assert.ok(Cu.evalInSandbox("mainObj", contentBox) === contentBox.mainObj); + Assert.ok( + Cu.evalInSandbox("contentObj", contentBox) === contentBox.contentObj + ); + Assert.ok(Cu.evalInSandbox("chromeObj", contentBox) === contentBox.chromeObj); + Assert.ok(Cu.evalInSandbox("mainObj", chromeBox) === chromeBox.mainObj); + Assert.ok(Cu.evalInSandbox("contentObj", chromeBox) === chromeBox.contentObj); + Assert.ok(Cu.evalInSandbox("chromeObj", chromeBox) === chromeBox.chromeObj); + + // We (the main global) can see properties of all objects in all globals. + Assert.ok(contentBox.mainObj.name === "mainObj"); + Assert.ok(contentBox.contentObj.name === "contentObj"); + Assert.ok(contentBox.chromeObj.name === "chromeObj"); + + // chromeBox can see properties of all objects in all globals. + Assert.equal(Cu.evalInSandbox("mainObj.name", chromeBox), "mainObj"); + Assert.equal(Cu.evalInSandbox("contentObj.name", chromeBox), "contentObj"); + Assert.equal(Cu.evalInSandbox("chromeObj.name", chromeBox), "chromeObj"); + + // contentBox can see properties of the content object, but not of either + // chrome object, because by default, content -> chrome wrappers hide all + // object properties. + Assert.equal(Cu.evalInSandbox("mainObj.name", contentBox), undefined); + Assert.equal(Cu.evalInSandbox("contentObj.name", contentBox), "contentObj"); + Assert.equal(Cu.evalInSandbox("chromeObj.name", contentBox), undefined); + + // When viewing an object in compartment A from the vantage point of + // compartment B, Debugger should give the same results as debuggee code + // would. + + // Create a debugger, debugging our two sandboxes. + const dbg = new Debugger(); + + // Create Debugger.Object instances referring to the two sandboxes, as + // seen from their own compartments. + const contentBoxDO = dbg.addDebuggee(contentBox); + const chromeBoxDO = dbg.addDebuggee(chromeBox); + + // Use Debugger to view the objects from contentBox. We should get the + // same D.O instance from both getProperty and makeDebuggeeValue, and the + // same property visibility we checked for above. + const mainFromContentDO = contentBoxDO.getProperty("mainObj"); + Assert.equal(mainFromContentDO, contentBoxDO.makeDebuggeeValue(mainObj)); + Assert.equal(mainFromContentDO.getProperty("name"), undefined); + Assert.equal(mainFromContentDO.unsafeDereference(), mainObj); + + const contentFromContentDO = contentBoxDO.getProperty("contentObj"); + Assert.equal( + contentFromContentDO, + contentBoxDO.makeDebuggeeValue(contentObj) + ); + Assert.equal(contentFromContentDO.getProperty("name"), "contentObj"); + Assert.equal(contentFromContentDO.unsafeDereference(), contentObj); + + const chromeFromContentDO = contentBoxDO.getProperty("chromeObj"); + Assert.equal(chromeFromContentDO, contentBoxDO.makeDebuggeeValue(chromeObj)); + Assert.equal(chromeFromContentDO.getProperty("name"), undefined); + Assert.equal(chromeFromContentDO.unsafeDereference(), chromeObj); + + // Similarly, viewing from chromeBox. + const mainFromChromeDO = chromeBoxDO.getProperty("mainObj"); + Assert.equal(mainFromChromeDO, chromeBoxDO.makeDebuggeeValue(mainObj)); + Assert.equal(mainFromChromeDO.getProperty("name"), "mainObj"); + Assert.equal(mainFromChromeDO.unsafeDereference(), mainObj); + + const contentFromChromeDO = chromeBoxDO.getProperty("contentObj"); + Assert.equal(contentFromChromeDO, chromeBoxDO.makeDebuggeeValue(contentObj)); + Assert.equal(contentFromChromeDO.getProperty("name"), "contentObj"); + Assert.equal(contentFromChromeDO.unsafeDereference(), contentObj); + + const chromeFromChromeDO = chromeBoxDO.getProperty("chromeObj"); + Assert.equal(chromeFromChromeDO, chromeBoxDO.makeDebuggeeValue(chromeObj)); + Assert.equal(chromeFromChromeDO.getProperty("name"), "chromeObj"); + Assert.equal(chromeFromChromeDO.unsafeDereference(), chromeObj); +} diff --git a/devtools/server/tests/xpcshell/test_wasm_source-01.js b/devtools/server/tests/xpcshell/test_wasm_source-01.js new file mode 100644 index 0000000000..fe8e43e236 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_wasm_source-01.js @@ -0,0 +1,143 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint-disable no-shadow, max-nested-callbacks */ + +"use strict"; + +/** + * Verify if client can receive binary wasm + */ + +var gDebuggee; +var gThreadFront; + +add_task( + threadFrontTest( + async ({ threadFront, debuggee, client }) => { + gThreadFront = threadFront; + gDebuggee = debuggee; + + await gThreadFront.reconfigure({ + observeAsmJS: true, + observeWasm: true, + }); + + test_source(); + }, + { waitForFinish: true, doNotRunWorker: true } + ) +); + +const EXPECTED_CONTENT = String.fromCharCode( + 0, + 97, + 115, + 109, + 1, + 0, + 0, + 0, + 1, + 132, + 128, + 128, + 128, + 0, + 1, + 96, + 0, + 0, + 3, + 130, + 128, + 128, + 128, + 0, + 1, + 0, + 6, + 129, + 128, + 128, + 128, + 0, + 0, + 7, + 133, + 128, + 128, + 128, + 0, + 1, + 1, + 102, + 0, + 0, + 10, + 136, + 128, + 128, + 128, + 0, + 1, + 130, + 128, + 128, + 128, + 0, + 0, + 11 +); + +function test_source() { + gThreadFront.once("paused", function (packet) { + gThreadFront.getSources().then(function (response) { + Assert.ok(!!response); + Assert.ok(!!response.sources); + + const source = response.sources.filter(function (s) { + return s.introductionType === "wasm"; + })[0]; + + Assert.ok(!!source); + + const sourceFront = gThreadFront.source(source); + sourceFront.source().then(function (response) { + Assert.ok(!!response); + Assert.ok(!!response.contentType); + Assert.ok(response.contentType.includes("wasm")); + + const sourceContent = response.source; + Assert.ok(!!sourceContent); + Assert.equal(typeof sourceContent, "object"); + Assert.ok("binary" in sourceContent); + Assert.equal(EXPECTED_CONTENT, sourceContent.binary); + + gThreadFront.resume().then(function () { + threadFrontTestFinished(); + }); + }); + }); + }); + + /* eslint-disable comma-spacing, max-len */ + gDebuggee.eval( + "(" + + function () { + // WebAssembly bytecode was generated by running: + // js -e 'print(wasmTextToBinary("(module(func(export \"f\")))"))' + const m = new WebAssembly.Module( + new Uint8Array([ + 0, 97, 115, 109, 1, 0, 0, 0, 1, 132, 128, 128, 128, 0, 1, 96, 0, 0, + 3, 130, 128, 128, 128, 0, 1, 0, 6, 129, 128, 128, 128, 0, 0, 7, 133, + 128, 128, 128, 0, 1, 1, 102, 0, 0, 10, 136, 128, 128, 128, 0, 1, + 130, 128, 128, 128, 0, 0, 11, + ]) + ); + const i = new WebAssembly.Instance(m); + debugger; + i.exports.f(); + } + + ")()" + ); +} diff --git a/devtools/server/tests/xpcshell/test_watchpoint-01.js b/devtools/server/tests/xpcshell/test_watchpoint-01.js new file mode 100644 index 0000000000..2d1d0e78f4 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_watchpoint-01.js @@ -0,0 +1,197 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint-disable no-shadow */ + +"use strict"; + +/* +- Tests adding set and get watchpoints. +- Tests removing a watchpoint. +- Tests removing all watchpoints. +*/ + +add_task( + threadFrontTest(async args => { + await testSetWatchpoint(args); + await testGetWatchpoint(args); + await testRemoveWatchpoint(args); + await testRemoveWatchpoints(args); + }) +); + +async function testSetWatchpoint({ commands, threadFront, debuggee }) { + async function evaluateJS(input) { + const { result } = await commands.scriptCommand.execute(input, { + thread: threadFront.actor, + frameActor: packet.frame.actorID, + }); + return result; + } + + function evaluateTestCode(debuggee) { + /* eslint-disable */ + Cu.evalInSandbox( + ` // 1 + function stopMe(obj) { // 2 + debugger; // 3 + obj.a = 2; // 4 + } // + stopMe({a: { b: 1 }})`, + debuggee, + "1.8", + "test_watchpoint-01.js" + ); + /* eslint-disable */ + } + + const packet = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + + info("Test that we paused on the debugger statement"); + Assert.equal(packet.frame.where.line, 3); + + info("Add set watchpoint"); + const args = packet.frame.arguments; + const obj = args[0]; + const objClient = threadFront.pauseGrip(obj); + await objClient.addWatchpoint("a", "obj.a", "set"); + + let result = await evaluateJS("obj.a"); + Assert.equal(result.getGrip().preview.ownProperties.b.value, 1); + + result = await evaluateJS("obj.a.b"); + Assert.equal(result, 1); + + info("Test that watchpoint triggers pause on set"); + const packet2 = await resumeAndWaitForPause(threadFront); + Assert.equal(packet2.frame.where.line, 4); + Assert.equal(packet2.why.type, "setWatchpoint"); + Assert.equal(obj.preview.ownProperties.a.value.ownPropertyLength, 1); + + await resume(threadFront); +} + +async function testGetWatchpoint({ threadFront, debuggee }) { + function evaluateTestCode(debuggee) { + /* eslint-disable */ + Cu.evalInSandbox( + ` // 1 + function stopMe(obj) { // 2 + debugger; // 3 + obj.a + 4; // 4 + } // + stopMe({a: 1})`, + debuggee, + "1.8", + "test_watchpoint-01.js" + ); + /* eslint-disable */ + } + + const packet = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + + info("Test that we paused on the debugger statement"); + Assert.equal(packet.frame.where.line, 3); + + info("Add get watchpoint."); + const args = packet.frame.arguments; + const obj = args[0]; + const objClient = threadFront.pauseGrip(obj); + await objClient.addWatchpoint("a", "obj.a", "get"); + + info("Test that watchpoint triggers pause on get."); + const packet2 = await resumeAndWaitForPause(threadFront); + Assert.equal(packet2.frame.where.line, 4); + Assert.equal(packet2.why.type, "getWatchpoint"); + Assert.equal(obj.preview.ownProperties.a.value, 1); + + await resume(threadFront); +} + +async function testRemoveWatchpoint({ threadFront, debuggee }) { + function evaluateTestCode(debuggee) { + /* eslint-disable */ + Cu.evalInSandbox( + ` // 1 + function stopMe(obj) { // 2 + debugger; // 3 + obj.a = 2; // 4 + debugger; // 5 + } // + + stopMe({a: 1})`, + debuggee, + "1.8", + "test_watchpoint-01.js" + ); + /* eslint-disable */ + } + + const packet = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + + info(`Test that we paused on the debugger statement`); + Assert.equal(packet.frame.where.line, 3); + + info(`Add set watchpoint`); + const args = packet.frame.arguments; + const obj = args[0]; + const objClient = threadFront.pauseGrip(obj); + await objClient.addWatchpoint("a", "obj.a", "set"); + + info(`Remove set watchpoint`); + await objClient.removeWatchpoint("a"); + + info(`Test that we do not pause on set`); + const packet2 = await resumeAndWaitForPause(threadFront); + Assert.equal(packet2.frame.where.line, 5); + + await resume(threadFront); +} + +async function testRemoveWatchpoints({ threadFront, debuggee }) { + function evaluateTestCode(debuggee) { + /* eslint-disable */ + Cu.evalInSandbox( + ` // 1 + function stopMe(obj) { // 2 + debugger; // 3 + obj.a = 2; // 4 + debugger; // 5 + } // + stopMe({a: 1})`, + debuggee, + "1.8", + "test_watchpoint-01.js" + ); + /* eslint-disable */ + } + + const packet = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + + info("Test that we paused on the debugger statement"); + Assert.equal(packet.frame.where.line, 3); + + info("Add and then remove set watchpoint"); + const args = packet.frame.arguments; + const obj = args[0]; + const objClient = threadFront.pauseGrip(obj); + await objClient.addWatchpoint("a", "obj.a", "set"); + await objClient.removeWatchpoints(); + + info("Test that we do not pause on set"); + const packet2 = await resumeAndWaitForPause(threadFront); + Assert.equal(packet2.frame.where.line, 5); + + await resume(threadFront); +} diff --git a/devtools/server/tests/xpcshell/test_watchpoint-02.js b/devtools/server/tests/xpcshell/test_watchpoint-02.js new file mode 100644 index 0000000000..d0739c8a00 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_watchpoint-02.js @@ -0,0 +1,223 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint-disable no-shadow */ + +"use strict"; + +/* +Test that debugger advances instead of pausing twice on the +same line when encountering both a watchpoint and a breakpoint. +*/ + +add_task( + threadFrontTest(async args => { + await testBreakpointAndSetWatchpoint(args); + await testBreakpointAndGetWatchpoint(args); + await testLoops(args); + }) +); + +// Test that we advance to the next line when a location +// has both a breakpoint and set watchpoint. +async function testBreakpointAndSetWatchpoint({ + commands, + threadFront, + debuggee, +}) { + async function evaluateJS(input) { + const { result } = await commands.scriptCommand.execute(input, { + frameActor: packet.frame.actorID, + }); + return result; + } + + function evaluateTestCode(debuggee) { + /* eslint-disable */ + Cu.evalInSandbox( + ` // 1 + function stopMe(obj) { // 2 + debugger; // 3 + obj.a = 2; // 4 + debugger; // 5 + } // + stopMe({a: 1})`, + debuggee, + "1.8", + "test_watchpoint-02.js" + ); + /* eslint-disable */ + } + + const packet = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + + info("Test that we pause on the debugger statement."); + Assert.equal(packet.frame.where.line, 3); + Assert.equal(packet.why.type, "debuggerStatement"); + + info("Add set watchpoint."); + const args = packet.frame.arguments; + const obj = args[0]; + const objClient = threadFront.pauseGrip(obj); + await objClient.addWatchpoint("a", "obj.a", "set"); + + info("Add breakpoint."); + const source = await getSourceById(threadFront, packet.frame.where.actor); + + const location = { + sourceUrl: source.url, + line: 4, + }; + + threadFront.setBreakpoint(location, {}); + + info("Test that pause occurs on breakpoint."); + const packet2 = await resumeAndWaitForPause(threadFront); + Assert.equal(packet2.frame.where.line, 4); + Assert.equal(packet2.why.type, "breakpoint"); + + const packet3 = await resumeAndWaitForPause(threadFront); + + info("Test that we pause on the second debugger statement."); + Assert.equal(packet3.frame.where.line, 5); + Assert.equal(packet3.why.type, "debuggerStatement"); + + info("Test that the value has updated."); + const result = await evaluateJS("obj.a"); + Assert.equal(result, 2); + + info("Remove breakpoint and finish."); + threadFront.removeBreakpoint(location, {}); + + await resume(threadFront); +} + +// Test that we advance to the next line when a location +// has both a breakpoint and get watchpoint. +async function testBreakpointAndGetWatchpoint({ threadFront, debuggee }) { + function evaluateTestCode(debuggee) { + /* eslint-disable */ + Cu.evalInSandbox( + ` // 1 + function stopMe(obj) { // 2 + debugger; // 3 + obj.a + 4; // 4 + debugger; // 5 + } // + stopMe({a: 1})`, + debuggee, + "1.8", + "test_watchpoint-02.js" + ); + /* eslint-disable */ + } + + const packet = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + + info("Test that we pause on the debugger statement."); + Assert.equal(packet.frame.where.line, 3); + + info("Add get watchpoint."); + const args = packet.frame.arguments; + const obj = args[0]; + const objClient = threadFront.pauseGrip(obj); + await objClient.addWatchpoint("a", "obj.a", "get"); + + info("Add breakpoint."); + const source = await getSourceById(threadFront, packet.frame.where.actor); + + const location = { + sourceUrl: source.url, + line: 4, + }; + + threadFront.setBreakpoint(location, {}); + + info("Test that pause occurs on breakpoint."); + const packet2 = await resumeAndWaitForPause(threadFront); + Assert.equal(packet2.frame.where.line, 4); + Assert.equal(packet2.why.type, "breakpoint"); + + const packet3 = await resumeAndWaitForPause(threadFront); + + info("Test that we pause on the second debugger statement."); + Assert.equal(packet3.frame.where.line, 5); + Assert.equal(packet3.why.type, "debuggerStatement"); + + info("Remove breakpoint and finish."); + threadFront.removeBreakpoint(location, {}); + + await resume(threadFront); +} + +// Test that we can pause multiple times +// on the same line for a watchpoint. +async function testLoops({ commands, threadFront, debuggee }) { + async function evaluateJS(input) { + const { result } = await commands.scriptCommand.execute(input, { + frameActor: packet.frame.actorID, + }); + return result; + } + + function evaluateTestCode(debuggee) { + /* eslint-disable */ + Cu.evalInSandbox( + ` // 1 + function stopMe(obj) { // 2 + let i = 0; // 3 + debugger; // 4 + while (i++ < 2) { // 5 + obj.a = 2; // 6 + } // 7 + debugger; // 8 + } // + stopMe({a: 1})`, + debuggee, + "1.8", + "test_watchpoint-02.js" + ); + /* eslint-disable */ + } + + const packet = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + + info("Test that we pause on the debugger statement."); + Assert.equal(packet.frame.where.line, 4); + Assert.equal(packet.why.type, "debuggerStatement"); + + info("Add set watchpoint."); + const args = packet.frame.arguments; + const obj = args[0]; + const objClient = threadFront.pauseGrip(obj); + await objClient.addWatchpoint("a", "obj.a", "set"); + + info("Test that watchpoint triggers pause on set."); + const packet2 = await resumeAndWaitForPause(threadFront); + Assert.equal(packet2.frame.where.line, 6); + Assert.equal(packet2.why.type, "setWatchpoint"); + let result = await evaluateJS("obj.a"); + Assert.equal(result, 1); + + info("Test that watchpoint triggers pause on set (2nd time)."); + const packet3 = await resumeAndWaitForPause(threadFront); + Assert.equal(packet3.frame.where.line, 6); + Assert.equal(packet3.why.type, "setWatchpoint"); + let result2 = await evaluateJS("obj.a"); + Assert.equal(result2, 2); + + info("Test that we pause on second debugger statement."); + const packet4 = await resumeAndWaitForPause(threadFront); + Assert.equal(packet4.frame.where.line, 8); + Assert.equal(packet4.why.type, "debuggerStatement"); + + await resume(threadFront); +} diff --git a/devtools/server/tests/xpcshell/test_watchpoint-03.js b/devtools/server/tests/xpcshell/test_watchpoint-03.js new file mode 100644 index 0000000000..33f4fbd2a2 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_watchpoint-03.js @@ -0,0 +1,72 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint-disable no-shadow */ + +"use strict"; +/* +See Bug 1601311. +Tests that removing a watchpoint does not change the value of the property that had the watchpoint. +*/ + +add_task( + threadFrontTest(async ({ commands, threadFront, debuggee }) => { + async function evaluateJS(input) { + const { result } = await commands.scriptCommand.execute(input, { + frameActor: packet.frame.actorID, + }); + return result; + } + + function evaluateTestCode(debuggee) { + /* eslint-disable */ + Cu.evalInSandbox( + ` // 1 + function stopMe(obj) { // 2 + debugger; // 3 + obj.a = 2; // 4 + debugger; // 5 + } // + + stopMe({a: 1})`, + debuggee, + "1.8", + "test_watchpoint-03.js" + ); + /* eslint-disable */ + } + + const packet = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + + info("Test that we paused on the debugger statement."); + Assert.equal(packet.frame.where.line, 3); + + info("Add set watchpoint."); + const args = packet.frame.arguments; + const obj = args[0]; + const objClient = threadFront.pauseGrip(obj); + await objClient.addWatchpoint("a", "obj.a", "set"); + + info("Test that we pause on set."); + const packet2 = await resumeAndWaitForPause(threadFront); + Assert.equal(packet2.frame.where.line, 4); + + const packet3 = await resumeAndWaitForPause(threadFront); + + info("Test that we pause on the second debugger statement."); + Assert.equal(packet3.frame.where.line, 5); + Assert.equal(packet3.why.type, "debuggerStatement"); + + info("Remove watchpoint."); + await objClient.removeWatchpoint("a"); + + info("Test that the value has updated."); + const result = await evaluateJS("obj.a"); + Assert.equal(result, 2); + + info("Finish test."); + await resume(threadFront); + }) +); diff --git a/devtools/server/tests/xpcshell/test_watchpoint-04.js b/devtools/server/tests/xpcshell/test_watchpoint-04.js new file mode 100644 index 0000000000..4ee6eadd5a --- /dev/null +++ b/devtools/server/tests/xpcshell/test_watchpoint-04.js @@ -0,0 +1,78 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that watchpoints ignore blackboxed sources + */ + +const BLACK_BOXED_URL = "http://example.com/blackboxme.js"; +const SOURCE_URL = "http://example.com/source.js"; + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + info(`blackbox the source`); + const { error, sources } = await threadFront.getSources(); + Assert.ok(!error, "Should not get an error: " + error); + const sourceFront = threadFront.source( + sources.filter(s => s.url == BLACK_BOXED_URL)[0] + ); + + await blackBox(sourceFront); + + await threadFront.resume(); + const packet = await executeOnNextTickAndWaitForPause( + debuggee.runTest, + threadFront + ); + + Assert.equal( + packet.frame.where.line, + 3, + "Paused at first debugger statement" + ); + + await addWatchpoint(threadFront, packet.frame, "obj", "a", "set"); + + info(`Resume and skip the watchpoint`); + const pausePacket = await resumeAndWaitForPause(threadFront); + + Assert.equal( + pausePacket.frame.where.line, + 5, + "Paused at second debugger statement" + ); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + Cu.evalInSandbox( + `function doStuff(obj) { + obj.a = 2; + }`, + debuggee, + "1.8", + BLACK_BOXED_URL, + 1 + ); + Cu.evalInSandbox( + `function runTest() { + const obj = {a: 1} + debugger + doStuff(obj); + debugger + }; debugger;`, + debuggee, + "1.8", + SOURCE_URL, + 1 + ); +} diff --git a/devtools/server/tests/xpcshell/test_watchpoint-05.js b/devtools/server/tests/xpcshell/test_watchpoint-05.js new file mode 100644 index 0000000000..4d25a59399 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_watchpoint-05.js @@ -0,0 +1,113 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint-disable no-shadow */ + +"use strict"; + +/* +- Adds a 'get or set' watchpoint. Tests that the debugger will pause on both get and set. +*/ + +add_task( + threadFrontTest(async args => { + await testGetPauseWithGetOrSetWatchpoint(args); + await testSetPauseWithGetOrSetWatchpoint(args); + }) +); + +async function testGetPauseWithGetOrSetWatchpoint({ threadFront, debuggee }) { + function evaluateTestCode(debuggee) { + /* eslint-disable */ + Cu.evalInSandbox( + ` // 1 + function stopMe(obj) { // 2 + debugger; // 3 + obj.a + 4; // 4 + } // + stopMe({a: 1})`, + debuggee, + "1.8", + "test_watchpoint-05.js" + ); + /* eslint-disable */ + } + + const packet = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + + info("Test that we paused on the debugger statement"); + Assert.equal(packet.frame.where.line, 3); + + info("Add get or set watchpoint."); + const args = packet.frame.arguments; + const obj = args[0]; + const objClient = threadFront.pauseGrip(obj); + await objClient.addWatchpoint("a", "obj.a", "getorset"); + + info("Test that watchpoint triggers pause on get."); + const packet2 = await resumeAndWaitForPause(threadFront); + Assert.equal(packet2.frame.where.line, 4); + Assert.equal(packet2.why.type, "getWatchpoint"); + Assert.equal(obj.preview.ownProperties.a.value, 1); + + await resume(threadFront); +} + +async function testSetPauseWithGetOrSetWatchpoint({ + commands, + threadFront, + debuggee, +}) { + async function evaluateJS(input) { + const { result } = await commands.scriptCommand.execute(input, { + frameActor: packet.frame.actorID, + }); + return result; + } + + function evaluateTestCode(debuggee) { + /* eslint-disable */ + Cu.evalInSandbox( + ` // 1 + function stopMe(obj) { // 2 + debugger; // 3 + obj.a = 2; // 4 + } // + stopMe({a: { b: 1 }})`, + debuggee, + "1.8", + "test_watchpoint-05.js" + ); + /* eslint-disable */ + } + + const packet = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + + info("Test that we paused on the debugger statement"); + Assert.equal(packet.frame.where.line, 3); + + info("Add get or set watchpoint"); + const args = packet.frame.arguments; + const obj = args[0]; + const objClient = threadFront.pauseGrip(obj); + await objClient.addWatchpoint("a", "obj.a", "getorset"); + + let result = await evaluateJS("obj.a"); + Assert.equal(result.getGrip().preview.ownProperties.b.value, 1); + + result = await evaluateJS("obj.a.b"); + Assert.equal(result, 1); + + info("Test that watchpoint triggers pause on set"); + const packet2 = await resumeAndWaitForPause(threadFront); + Assert.equal(packet2.frame.where.line, 4); + Assert.equal(packet2.why.type, "setWatchpoint"); + Assert.equal(obj.preview.ownProperties.a.value.ownPropertyLength, 1); + + await resume(threadFront); +} diff --git a/devtools/server/tests/xpcshell/test_webext_apis.js b/devtools/server/tests/xpcshell/test_webext_apis.js new file mode 100644 index 0000000000..5a2f2b990a --- /dev/null +++ b/devtools/server/tests/xpcshell/test_webext_apis.js @@ -0,0 +1,162 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { AddonManager } = ChromeUtils.importESModule( + "resource://gre/modules/AddonManager.sys.mjs" +); +const { ExtensionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/ExtensionXPCShellUtils.sys.mjs" +); + +const DistinctDevToolsServer = getDistinctDevToolsServer(); +ExtensionTestUtils.init(this); + +add_setup(async () => { + Services.prefs.setBoolPref("extensions.blocklist.enabled", false); + await startupAddonsManager(); +}); + +// Basic request wrapper that sends a request and resolves on the next packet. +// Will only work for very basic scenarios, without events emitted on the server +// etc... +async function sendRequest(transport, request) { + return new Promise(resolve => { + transport.hooks = { + onPacket: packet => { + dump(`received packet: ${JSON.stringify(packet)}\n`); + // Let's resolve only when we get a packet that is related to our + // request. It is needed because some methods do not return the correct + // response right away. This is the case of the `reload` method, which + // receives a `addonListChanged` message first and then a `reload` + // message. + if (packet.from === request.to) { + resolve(packet); + } + }, + }; + transport.send(request); + }); +} + +// If this test case fails, please reach out to webext peers because +// https://github.com/mozilla/web-ext relies on the APIs tested here. +add_task(async function test_webext_run_apis() { + DistinctDevToolsServer.init(); + DistinctDevToolsServer.registerAllActors(); + + const transport = DistinctDevToolsServer.connectPipe(); + + // After calling connectPipe, the root actor will be created on the server + // and a packet will be emitted after a tick. Wait for the initial packet. + await new Promise(resolve => { + transport.hooks = { onPacket: resolve }; + }); + + const getRootResponse = await sendRequest(transport, { + to: "root", + type: "getRoot", + }); + + ok(getRootResponse, "received a response after calling RootActor::getRoot"); + ok(getRootResponse.addonsActor, "getRoot returned an addonsActor id"); + + // installTemporaryAddon + const addonId = "test-addons-actor@mozilla.org"; + const addonPath = getFilePath("addons/web-extension", false, true); + const promiseStarted = AddonTestUtils.promiseWebExtensionStartup(addonId); + const { addon } = await sendRequest(transport, { + to: getRootResponse.addonsActor, + type: "installTemporaryAddon", + addonPath, + // The openDevTools parameter is not always passed by web-ext. This test + // omits it, to make sure that the request without the flag is accepted. + // openDevTools: false, + }); + await promiseStarted; + + ok(addon, "addonsActor allows to install a temporary add-on"); + equal(addon.id, addonId, "temporary add-on is the expected one"); + equal(addon.actor, false, "temporary add-on does not have an actor"); + + // listAddons + let { addons } = await sendRequest(transport, { + to: "root", + type: "listAddons", + }); + ok(Array.isArray(addons), "listAddons() returns a list of add-ons"); + equal(addons.length, 1, "expected an add-on installed"); + + const installedAddon = addons[0]; + equal(installedAddon.id, addonId, "installed add-on is the expected one"); + ok(installedAddon.actor, "returned add-on has an actor"); + + // reload + const promiseReloaded = AddonTestUtils.promiseAddonEvent("onInstalled"); + const promiseRestarted = AddonTestUtils.promiseWebExtensionStartup(addonId); + await sendRequest(transport, { + to: installedAddon.actor, + type: "reload", + }); + await Promise.all([promiseReloaded, promiseRestarted]); + + // uninstallAddon + const promiseUninstalled = new Promise(resolve => { + const listener = {}; + listener.onUninstalled = uninstalledAddon => { + if (uninstalledAddon.id == addonId) { + AddonManager.removeAddonListener(listener); + resolve(); + } + }; + AddonManager.addAddonListener(listener); + }); + await sendRequest(transport, { + to: getRootResponse.addonsActor, + type: "uninstallAddon", + addonId, + }); + await promiseUninstalled; + + ({ addons } = await sendRequest(transport, { + to: "root", + type: "listAddons", + })); + equal(addons.length, 0, "expected no add-on installed"); + + // Attempt to uninstall an add-on that is (no longer) installed. + let error = await sendRequest(transport, { + to: getRootResponse.addonsActor, + type: "uninstallAddon", + addonId, + }); + equal( + error?.message, + `Could not uninstall add-on "${addonId}"`, + "expected error" + ); + + // Attempt to uninstall a non-temporarily loaded extension, which we do not + // allow at the moment. We start by loading an extension, then we call the + // `uninstallAddon`. + const id = "not-a-temporary@extension"; + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + browser_specific_settings: { gecko: { id } }, + }, + useAddonManager: "permanent", + }); + await extension.startup(); + + error = await sendRequest(transport, { + to: getRootResponse.addonsActor, + type: "uninstallAddon", + addonId: id, + }); + equal(error?.message, `Could not uninstall add-on "${id}"`, "expected error"); + + await extension.unload(); + + transport.close(); +}); diff --git a/devtools/server/tests/xpcshell/test_webextension_descriptor.js b/devtools/server/tests/xpcshell/test_webextension_descriptor.js new file mode 100644 index 0000000000..00cdeea605 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_webextension_descriptor.js @@ -0,0 +1,141 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { ExtensionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/ExtensionXPCShellUtils.sys.mjs" +); + +const DistinctDevToolsServer = getDistinctDevToolsServer(); +ExtensionTestUtils.init(this); + +add_setup(async () => { + Services.prefs.setBoolPref("extensions.blocklist.enabled", false); + await startupAddonsManager(); + + // We intentionally generate install-time manifest warnings, so don't trigger + // the special test-only mode of converting them to errors. + Services.prefs.setBoolPref( + "extensions.webextensions.warnings-as-errors", + false + ); + + DistinctDevToolsServer.init(); + DistinctDevToolsServer.registerAllActors(); +}); + +// Verifies: +// - listAddons +// - WebExtensionDescriptorActor output +// Also a regression test for bug 1837185, that AddonManager.sys.mjs and +// ExtensionParent.sys.mjs are imported from the correct loader. +add_task(async function test_listAddons_and_WebExtensionDescriptor() { + const transport = DistinctDevToolsServer.connectPipe(); + const client = new DevToolsClient(transport); + await client.connect(); + + const getRootResponse = await client.mainRoot.getRoot(); + + ok(getRootResponse, "received a response after calling RootActor::getRoot"); + ok(getRootResponse.addonsActor, "getRoot returned an addonsActor id"); + + const ADDON_ID = "with@warning"; + const extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + name: "DummyExtensionWithUnknownManifestKey", + unknown_manifest_key: "this is an unknown manifest key", + browser_specific_settings: { gecko: { id: ADDON_ID } }, + }, + background: `browser.test.sendMessage("background_started");`, + }); + await extension.startup(); + await extension.awaitMessage("background_started"); + + // listAddons: addon after new install. + { + const listAddonsResponse = await client.mainRoot.listAddons(); + const addon = listAddonsResponse.find(a => a.id === ADDON_ID); + ok(addon, "listAddons() returns a list of add-ons including with@warning"); + + // Inspect all raw properties of the message, to make sure that we always + // have full coverage for all current and future properties. + const { actor, url, warnings, ...addonMinusSomeKeys } = addon._form; + const actorPattern = /^server\d+\.conn\d+\.webExtensionDescriptor\d+$/; + ok(actorPattern.test(actor), `actor is webExtensionDescriptor: ${actor}`); + // We don't care about the exact path, just a dummy check: + ok(url.endsWith(".xpi"), `url is path to the xpi file`); + + deepEqual( + warnings, + [ + "Reading manifest: Warning processing unknown_manifest_key: An unexpected property was found in the WebExtension manifest.", + ], + "Can retrieve warnings." + ); + + // Verify that the other remaining keys have a meaningful value. + // This is mainly to have some form of verification on the value of the + // properties. If this check ever fails, double-check whether the proposed + // change makes sense and if it does just update the test expectation here. + deepEqual( + addonMinusSomeKeys, + { + backgroundScriptStatus: undefined, + debuggable: true, + hidden: false, + iconDataURL: undefined, + iconURL: null, + id: ADDON_ID, + isSystem: false, + isWebExtension: true, + manifestURL: `moz-extension://${extension.uuid}/manifest.json`, + name: "DummyExtensionWithUnknownManifestKey", + persistentBackgroundScript: true, + temporarilyInstalled: false, + traits: { + supportsReloadDescriptor: true, + watcher: true, + }, + }, + "WebExtensionDescriptorActor content matches the add-on" + ); + } + + await extension.upgrade({ + manifest: { + name: "Updated_extension", + new_unknown_manifest_key: "different warning than before", + browser_specific_settings: { gecko: { id: ADDON_ID } }, + }, + background: `browser.test.sendMessage("updated_done");`, + }); + await extension.awaitMessage("updated_done"); + + // listAddons: addon after update. + { + const listAddonsResponse = await client.mainRoot.listAddons(); + const addon = listAddonsResponse.find(a => a.id === ADDON_ID); + ok(addon, "listAddons() should still list the add-on after update"); + equal(addon.name, "Updated_extension", "Got updated name"); + deepEqual( + addon.warnings, + [ + "Reading manifest: Warning processing new_unknown_manifest_key: An unexpected property was found in the WebExtension manifest.", + ], + "Can retrieve new warnings for updated add-on." + ); + } + + await extension.unload(); + + // listAddons: addon after removal - gone. + { + const listAddonsResponse = await client.mainRoot.listAddons(); + const addon = listAddonsResponse.find(a => a.id === ADDON_ID); + deepEqual(addon, null, "Add-on should be gone after removal"); + } + + await client.close(); +}); diff --git a/devtools/server/tests/xpcshell/test_xpcshell_debugging.js b/devtools/server/tests/xpcshell/test_xpcshell_debugging.js new file mode 100644 index 0000000000..ff54d7390d --- /dev/null +++ b/devtools/server/tests/xpcshell/test_xpcshell_debugging.js @@ -0,0 +1,90 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test the xpcshell-test debug support. Ideally we should have this test +// next to the xpcshell support code, but that's tricky... + +// HACK: ServiceWorkerManager requires the "profile-change-teardown" to cleanly +// shutdown, and setting _profileInitialized to `true` will trigger those +// notifications (see /testing/xpcshell/head.js). +// eslint-disable-next-line no-undef +_profileInitialized = true; + +add_task(async function () { + const testFile = do_get_file("xpcshell_debugging_script.js"); + + // _setupDevToolsServer is from xpcshell-test's head.js + /* global _setupDevToolsServer */ + let testInitialized = false; + const { DevToolsServer } = _setupDevToolsServer([testFile.path], () => { + testInitialized = true; + }); + const transport = DevToolsServer.connectPipe(); + const client = new DevToolsClient(transport); + await client.connect(); + + // Ensure that global actors are available. Just test the device actor. + const deviceFront = await client.mainRoot.getFront("device"); + const desc = await deviceFront.getDescription(); + equal( + desc.geckobuildid, + Services.appinfo.platformBuildID, + "device actor works" + ); + + // Even though we have no tabs, getMainProcess gives us the chrome debugger. + const targetDescriptor = await client.mainRoot.getMainProcess(); + const front = await targetDescriptor.getTarget(); + const watcher = await targetDescriptor.getWatcher(); + + const threadFront = await front.attachThread(); + + // Checks that the thread actor initializes immediately and that _setupDevToolsServer + // callback gets called. + ok(testInitialized); + + const onPause = waitForPause(threadFront); + + // Now load our test script, + // in another event loop so that the test can keep running! + Services.tm.dispatchToMainThread(() => { + load(testFile.path); + }); + + // and our "paused" listener should get hit. + info("Wait for first paused event"); + const packet1 = await onPause; + equal( + packet1.why.type, + "breakpoint", + "yay - hit the breakpoint at the first line in our script" + ); + + // Resume again - next stop should be our "debugger" statement. + info("Wait for second pause event"); + const packet2 = await resumeAndWaitForPause(threadFront); + equal( + packet2.why.type, + "debuggerStatement", + "yay - hit the 'debugger' statement in our script" + ); + + info("Dynamically add a breakpoint after the debugger statement"); + const breakpointsFront = await watcher.getBreakpointListActor(); + await breakpointsFront.setBreakpoint( + { sourceUrl: testFile.path, line: 11, column: 0 }, + {} + ); + + // Resume again - next stop should be the new breakpoint. + info("Wait for third pause event"); + const packet3 = await resumeAndWaitForPause(threadFront); + equal( + packet3.why.type, + "breakpoint", + "yay - hit the breakpoint added after starting the test" + ); + finishClient(client); +}); diff --git a/devtools/server/tests/xpcshell/testactors.js b/devtools/server/tests/xpcshell/testactors.js new file mode 100644 index 0000000000..af208fe93e --- /dev/null +++ b/devtools/server/tests/xpcshell/testactors.js @@ -0,0 +1,242 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { + LazyPool, + createExtraActors, +} = require("resource://devtools/shared/protocol/lazy-pool.js"); +const { RootActor } = require("resource://devtools/server/actors/root.js"); +const { ThreadActor } = require("resource://devtools/server/actors/thread.js"); +const { + DevToolsServer, +} = require("resource://devtools/server/devtools-server.js"); +const { + ActorRegistry, +} = require("resource://devtools/server/actors/utils/actor-registry.js"); +const { + SourcesManager, +} = require("resource://devtools/server/actors/utils/sources-manager.js"); +const makeDebugger = require("resource://devtools/server/actors/utils/make-debugger.js"); +const protocol = require("resource://devtools/shared/protocol.js"); +const { + windowGlobalTargetSpec, +} = require("resource://devtools/shared/specs/targets/window-global.js"); +const { + tabDescriptorSpec, +} = require("resource://devtools/shared/specs/descriptors/tab.js"); +const Targets = require("resource://devtools/server/actors/targets/index.js"); +const { + createContentProcessSessionContext, +} = require("resource://devtools/server/actors/watcher/session-context.js"); + +var gTestGlobals = new Set(); +DevToolsServer.addTestGlobal = function (global) { + gTestGlobals.add(global); +}; +DevToolsServer.removeTestGlobal = function (global) { + gTestGlobals.delete(global); +}; + +DevToolsServer.getTestGlobal = function (name) { + for (const g of gTestGlobals) { + if (g.__name == name) { + return g; + } + } + + return null; +}; + +var gAllowNewThreadGlobals = false; +DevToolsServer.allowNewThreadGlobals = function () { + gAllowNewThreadGlobals = true; +}; +DevToolsServer.disallowNewThreadGlobals = function () { + gAllowNewThreadGlobals = false; +}; + +// A mock tab list, for use by tests. This simply presents each global in +// gTestGlobals as a tab, and the list is fixed: it never calls its +// onListChanged handler. +// +// As implemented now, we consult gTestGlobals when we're constructed, not +// when we're iterated over, so tests have to add their globals before the +// root actor is created. +function TestTabList(connection) { + this.conn = connection; + + // An array of actors for each global added with + // DevToolsServer.addTestGlobal. + this._descriptorActors = []; + + // A pool mapping those actors' names to the actors. + this._descriptorActorPool = new LazyPool(connection); + + for (const global of gTestGlobals) { + const actor = new TestTargetActor(connection, global); + this._descriptorActorPool.manage(actor); + + const descriptorActor = new TestDescriptorActor(connection, actor); + this._descriptorActorPool.manage(descriptorActor); + + this._descriptorActors.push(descriptorActor); + } +} + +TestTabList.prototype = { + constructor: TestTabList, + destroy() {}, + getList() { + return Promise.resolve([...this._descriptorActors]); + }, + // Helper method only available for the xpcshell implementation of tablist. + getTargetActorForTab(title) { + const descriptorActor = this._descriptorActors.find(d => d.title === title); + if (!descriptorActor) { + return null; + } + return descriptorActor._targetActor; + }, +}; + +exports.createRootActor = function createRootActor(connection) { + ActorRegistry.registerModule("devtools/server/actors/webconsole", { + prefix: "console", + constructor: "WebConsoleActor", + type: { target: true }, + }); + const root = new RootActor(connection, { + tabList: new TestTabList(connection), + globalActorFactories: ActorRegistry.globalActorFactories, + }); + + root.applicationType = "xpcshell-tests"; + return root; +}; + +class TestDescriptorActor extends protocol.Actor { + constructor(conn, targetActor) { + super(conn, tabDescriptorSpec); + this._targetActor = targetActor; + } + + // We don't exercise the selected tab in xpcshell tests. + get selected() { + return false; + } + + get title() { + return this._targetActor.title; + } + + form() { + const form = { + actor: this.actorID, + traits: {}, + selected: this.selected, + title: this._targetActor.title, + url: this._targetActor.url, + }; + + return form; + } + + getFavicon() { + return ""; + } + + getTarget() { + return this._targetActor.form(); + } +} + +class TestTargetActor extends protocol.Actor { + constructor(conn, global) { + super(conn, windowGlobalTargetSpec); + + this.sessionContext = createContentProcessSessionContext(); + this._global = global; + this._global.wrappedJSObject = global; + this.threadActor = new ThreadActor(this, this._global); + this.conn.addActor(this.threadActor); + this._extraActors = {}; + // This is a hack in order to enable threadActor to be accessed from getFront + this._extraActors.threadActor = this.threadActor; + this.makeDebugger = makeDebugger.bind(null, { + findDebuggees: () => [this._global], + shouldAddNewGlobalAsDebuggee: g => gAllowNewThreadGlobals, + }); + this.dbg = this.makeDebugger(); + this.notifyResources = this.notifyResources.bind(this); + } + + targetType = Targets.TYPES.FRAME; + + get window() { + return this._global; + } + + // Both title and url point to this._global.__name + get title() { + return this._global.__name; + } + + get url() { + return this._global.__name; + } + + get sourcesManager() { + if (!this._sourcesManager) { + this._sourcesManager = new SourcesManager(this.threadActor); + } + return this._sourcesManager; + } + + form() { + const response = { + actor: this.actorID, + title: this.title, + threadActor: this.threadActor.actorID, + }; + + // Walk over target-scoped actors and add them to a new LazyPool. + const actorPool = new LazyPool(this.conn); + const actors = createExtraActors( + ActorRegistry.targetScopedActorFactories, + actorPool, + this + ); + if (actorPool?._poolMap.size > 0) { + this._descriptorActorPool = actorPool; + this.conn.addActorPool(this._descriptorActorPool); + } + + return { ...response, ...actors }; + } + + detach(request) { + this.threadActor.destroy(); + return { type: "detached" }; + } + + reload(request) { + this.sourcesManager.reset(); + this.threadActor.clearDebuggees(); + this.threadActor.dbg.addDebuggees(); + return {}; + } + + removeActorByName(name) { + const actor = this._extraActors[name]; + if (this._descriptorActorPool) { + this._descriptorActorPool.removeActor(actor); + } + delete this._extraActors[name]; + } + + notifyResources(updateType, resources) { + this.emit(`resource-${updateType}-form`, resources); + } +} diff --git a/devtools/server/tests/xpcshell/webextension-helpers.js b/devtools/server/tests/xpcshell/webextension-helpers.js new file mode 100644 index 0000000000..46968f09e7 --- /dev/null +++ b/devtools/server/tests/xpcshell/webextension-helpers.js @@ -0,0 +1,197 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* globals browser */ + +"use strict"; + +/** + * Test helpers shared by the devtools server xpcshell tests related to webextensions. + */ + +const { FileUtils } = ChromeUtils.importESModule( + "resource://gre/modules/FileUtils.sys.mjs" +); +const { ExtensionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/ExtensionXPCShellUtils.sys.mjs" +); + +const { + CommandsFactory, +} = require("resource://devtools/shared/commands/commands-factory.js"); + +/** + * Loads and starts up a test extension given the provided extension configuration. + * + * @param {Object} extConfig - The extension configuration object + * @return {ExtensionWrapper} extension - Resolves with an extension object once the + * extension has started up. + */ +async function startupExtension(extConfig) { + const extension = ExtensionTestUtils.loadExtension(extConfig); + + await extension.startup(); + + return extension; +} +exports.startupExtension = startupExtension; + +/** + * Initializes the extensionStorage actor for a given extension. This is effectively + * what happens when the addon storage panel is opened in the browser. + * + * @param {String} - id, The addon id + * @return {Object} - Resolves with the DevTools "commands" objact and the extensionStorage + * resource/front. + */ +async function openAddonStoragePanel(id) { + const commands = await CommandsFactory.forAddon(id); + await commands.targetCommand.startListening(); + + // Fetch the EXTENSION_STORAGE resource. + // Unfortunately, we can't use resourceCommand.waitForNextResource as it would destroy + // the actor by immediately unwatching for the resource type. + const extensionStorage = await new Promise(resolve => { + commands.resourceCommand.watchResources( + [commands.resourceCommand.TYPES.EXTENSION_STORAGE], + { + onAvailable(resources) { + resolve(resources[0]); + }, + } + ); + }); + + return { commands, extensionStorage }; +} +exports.openAddonStoragePanel = openAddonStoragePanel; + +/** + * Builds the extension configuration object passed into ExtensionTestUtils.loadExtension + * + * @param {Object} options - Options, if any, to add to the configuration + * @param {Function} options.background - A function comprising the test extension's + * background script if provided + * @param {Object} options.files - An object whose keys correspond to file names and + * values map to the file contents + * @param {Object} options.manifest - An object representing the extension's manifest + * @return {Object} - The extension configuration object + */ +function getExtensionConfig(options = {}) { + const { manifest, ...otherOptions } = options; + const baseConfig = { + manifest: { + ...manifest, + permissions: ["storage"], + }, + useAddonManager: "temporary", + }; + return { + ...baseConfig, + ...otherOptions, + }; +} +exports.getExtensionConfig = getExtensionConfig; + +/** + * Shared files for a test extension that has no background page but adds storage + * items via a transient extension page in a tab + */ +const ext_no_bg = { + files: { + "extension_page_in_tab.html": `<!DOCTYPE html> + <html> + <head> + <meta charset="utf-8"> + </head> + <body> + <h1>Extension Page in a Tab</h1> + <script src="extension_page_in_tab.js"></script> + </body> + </html>`, + "extension_page_in_tab.js": extensionScriptWithMessageListener, + }, +}; +exports.ext_no_bg = ext_no_bg; + +/** + * An extension script that can be used in any extension context (e.g. as a background + * script or as an extension page script loaded in a tab). + */ +async function extensionScriptWithMessageListener() { + let fireOnChanged = false; + browser.storage.onChanged.addListener(() => { + if (fireOnChanged) { + // Do not fire it again until explicitly requested again using the "storage-local-fireOnChanged" test message. + fireOnChanged = false; + browser.test.sendMessage("storage-local-onChanged"); + } + }); + + browser.test.onMessage.addListener(async (msg, ...args) => { + let item = null; + switch (msg) { + case "storage-local-set": + await browser.storage.local.set(args[0]); + break; + case "storage-local-get": + item = await browser.storage.local.get(args[0]); + break; + case "storage-local-remove": + await browser.storage.local.remove(args[0]); + break; + case "storage-local-clear": + await browser.storage.local.clear(); + break; + case "storage-local-fireOnChanged": { + // Allow the storage.onChanged listener to send a test event + // message when onChanged is being fired. + fireOnChanged = true; + // Do not fire fireOnChanged:done. + return; + } + default: + browser.test.fail(`Unexpected test message: ${msg}`); + } + + browser.test.sendMessage(`${msg}:done`, item); + }); + // window is available in background scripts + // eslint-disable-next-line no-undef + browser.test.sendMessage("extension-origin", window.location.origin); +} +exports.extensionScriptWithMessageListener = extensionScriptWithMessageListener; + +/** + * Shutdown procedure common to all tasks. + * + * @param {Object} extension - The test extension + * @param {Object} commands - The web extension commands used by the DevTools to interact with the backend + */ +async function shutdown(extension, commands) { + if (commands) { + await commands.destroy(); + } + await extension.unload(); +} +exports.shutdown = shutdown; + +/** + * Mocks the missing 'storage/permanent' directory needed by the "indexedDB" + * storage actor's 'populateStoresForHosts' method. This + * directory exists in a full browser i.e. mochitest. + */ +function createMissingIndexedDBDirs() { + const dir = Services.dirsvc.get("ProfD", Ci.nsIFile).clone(); + dir.append("storage"); + if (!dir.exists()) { + dir.create(dir.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + } + dir.append("permanent"); + if (!dir.exists()) { + dir.create(dir.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + } + + return dir; +} +exports.createMissingIndexedDBDirs = createMissingIndexedDBDirs; diff --git a/devtools/server/tests/xpcshell/xpcshell.toml b/devtools/server/tests/xpcshell/xpcshell.toml new file mode 100644 index 0000000000..29ca414062 --- /dev/null +++ b/devtools/server/tests/xpcshell/xpcshell.toml @@ -0,0 +1,436 @@ +[DEFAULT] +tags = "devtools" +head = "head_dbg.js" +firefox-appdir = "browser" +skip-if = ["os == 'android'"] +# While not every devtools test uses evalInSandbox over 80 do, so it's easier to +# set allow_parent_unrestricted_js_loads for all the tests here. +# Similar story for the eval restrictions +prefs = [ + "security.allow_parent_unrestricted_js_loads=true", + "security.allow_eval_with_system_principal=true", + "security.allow_eval_in_parent_process=true", +] + +support-files = [ + "completions.js", + "webextension-helpers.js", + "source-map-data/sourcemapped.coffee", + "source-map-data/sourcemapped.map", + "post_init_global_actors.js", + "post_init_target_scoped_actors.js", + "pre_init_global_actors.js", + "pre_init_target_scoped_actors.js", + "registertestactors-lazy.js", + "sourcemapped.js", + "testactors.js", + "hello-actor.js", + "stepping.js", + "stepping-async.js", + "source-03.js", + "setBreakpoint-on-column.js", + "setBreakpoint-on-column-minified.js", + "setBreakpoint-on-column-in-gcd-script.js", + "setBreakpoint-on-column-with-no-offsets.js", + "setBreakpoint-on-column-with-no-offsets-in-gcd-script.js", + "setBreakpoint-on-line.js", + "setBreakpoint-on-line-in-gcd-script.js", + "setBreakpoint-on-line-with-multiple-offsets.js", + "setBreakpoint-on-line-with-multiple-statements.js", + "setBreakpoint-on-line-with-no-offsets.js", + "setBreakpoint-on-line-with-no-offsets-in-gcd-script.js", + "addons/web-extension/manifest.json", + "addons/web-extension-upgrade/manifest.json", + "addons/web-extension2/manifest.json", +] + +["test_MemoryActor_saveHeapSnapshot_01.js"] + +["test_MemoryActor_saveHeapSnapshot_02.js"] + +["test_MemoryActor_saveHeapSnapshot_03.js"] + +["test_add_actors.js"] + +["test_addon_debugging_connect.js"] + +["test_addon_events.js"] + +["test_addon_reload.js"] + +["test_addons_actor.js"] + +["test_animation_name.js"] + +["test_animation_type.js"] + +["test_attach.js"] + +["test_blackboxing-01.js"] + +["test_blackboxing-02.js"] + +["test_blackboxing-03.js"] + +["test_blackboxing-04.js"] + +["test_blackboxing-05.js"] + +["test_blackboxing-08.js"] + +["test_breakpoint-01.js"] + +["test_breakpoint-03.js"] +skip-if = ["true"] # breakpoint sliding is not supported bug 1525685 + +["test_breakpoint-04.js"] + +["test_breakpoint-05.js"] +skip-if = ["true"] # breakpoint sliding is not supported bug 1525685 + +["test_breakpoint-06.js"] +skip-if = ["true"] # breakpoint sliding is not supported bug 1525685 + +["test_breakpoint-07.js"] +skip-if = ["true"] # breakpoint sliding is not supported bug 1525685 + +["test_breakpoint-08.js"] +skip-if = ["true"] # breakpoint sliding is not supported bug 1525685 + +["test_breakpoint-09.js"] + +["test_breakpoint-10.js"] + +["test_breakpoint-11.js"] + +["test_breakpoint-12.js"] +skip-if = ["true"] # breakpoint sliding is not supported bug 1525685 + +["test_breakpoint-13.js"] + +["test_breakpoint-14.js"] + +["test_breakpoint-16.js"] + +["test_breakpoint-17.js"] +skip-if = ["true"] # tests for breakpoint actors are obsolete bug 1524374 + +["test_breakpoint-18.js"] + +["test_breakpoint-19.js"] +skip-if = ["true"] # bug 1104838 + +["test_breakpoint-20.js"] + +["test_breakpoint-21.js"] + +["test_breakpoint-22.js"] +skip-if = ["true"] # breakpoint sliding is not supported bug 1525685 + +["test_breakpoint-23.js"] + +["test_breakpoint-24.js"] + +["test_breakpoint-25.js"] + +["test_breakpoint-26.js"] + +["test_breakpoint-actor-map.js"] +skip-if = ["true"] # tests for breakpoint actors are obsolete bug 1524374 + +["test_client_request.js"] + +["test_conditional_breakpoint-01.js"] + +["test_conditional_breakpoint-02.js"] + +["test_conditional_breakpoint-03.js"] + +["test_conditional_breakpoint-04.js"] + +["test_connection_closes_all_pools.js"] + +["test_console_eval-01.js"] + +["test_console_eval-02.js"] + +["test_dbgactor.js"] + +["test_dbgclient_debuggerstatement.js"] + +["test_dbgglobal.js"] + +["test_extension_storage_actor.js"] +skip-if = ["tsan"] # Unreasonably slow, bug 1612707 + +["test_extension_storage_actor_upgrade.js"] + +["test_forwardingprefix.js"] + +["test_frameactor-01.js"] + +["test_frameactor-02.js"] + +["test_frameactor-03.js"] + +["test_frameactor-04.js"] + +["test_frameactor-05.js"] + +["test_frameactor_wasm-01.js"] + +["test_framearguments-01.js"] + +["test_framebindings-01.js"] + +["test_framebindings-02.js"] + +["test_framebindings-03.js"] + +["test_framebindings-04.js"] + +["test_framebindings-05.js"] + +["test_framebindings-06.js"] + +["test_framebindings-07.js"] + +["test_front_destroy.js"] + +["test_functiongrips-01.js"] + +["test_getRuleText.js"] + +["test_getTextAtLineColumn.js"] + +["test_get_command_and_arg.js"] + +["test_getyoungestframe.js"] + +["test_ignore_caught_exceptions.js"] + +["test_ignore_no_interface_exceptions.js"] + +["test_interrupt.js"] + +["test_layout-reflows-observer.js"] + +["test_listsources-01.js"] + +["test_listsources-02.js"] + +["test_listsources-03.js"] + +["test_logpoint-01.js"] + +["test_logpoint-02.js"] + +["test_logpoint-03.js"] + +["test_longstringgrips-01.js"] + +["test_nativewrappers.js"] + +["test_nesting-03.js"] + +["test_nesting-04.js"] + +["test_new_source-01.js"] + +["test_new_source-02.js"] + +["test_nodelistactor.js"] + +["test_objectgrips-02.js"] + +["test_objectgrips-03.js"] + +["test_objectgrips-04.js"] + +["test_objectgrips-05.js"] + +["test_objectgrips-06.js"] + +["test_objectgrips-07.js"] + +["test_objectgrips-08.js"] + +["test_objectgrips-14.js"] + +["test_objectgrips-15.js"] + +["test_objectgrips-16.js"] + +["test_objectgrips-17.js"] + +["test_objectgrips-18.js"] + +["test_objectgrips-19.js"] + +["test_objectgrips-20.js"] + +["test_objectgrips-21.js"] + +["test_objectgrips-22.js"] + +["test_objectgrips-23.js"] + +["test_objectgrips-24.js"] + +["test_objectgrips-25.js"] + +["test_objectgrips-fn-apply-01.js"] + +["test_objectgrips-fn-apply-02.js"] + +["test_objectgrips-fn-apply-03.js"] + +["test_objectgrips-nested-promise.js"] + +["test_objectgrips-nested-proxy.js"] + +["test_objectgrips-property-value-01.js"] + +["test_objectgrips-property-value-02.js"] + +["test_objectgrips-property-value-03.js"] + +["test_objectgrips-sparse-array.js"] + +["test_pause_exceptions-01.js"] + +["test_pause_exceptions-02.js"] + +["test_pause_exceptions-03.js"] + +["test_pause_exceptions-04.js"] + +["test_pauselifetime-01.js"] + +["test_pauselifetime-02.js"] + +["test_pauselifetime-03.js"] + +["test_pauselifetime-04.js"] + +["test_promise_state-01.js"] + +["test_promise_state-02.js"] + +["test_promise_state-03.js"] + +["test_register_actor.js"] + +["test_requestTypes.js"] + +["test_restartFrame-01.js"] + +["test_safe-getter.js"] + +["test_sessionDataHelpers.js"] + +["test_setBreakpoint-at-the-beginning-of-a-minified-fn.js"] + +["test_setBreakpoint-at-the-end-of-a-minified-fn.js"] + +["test_setBreakpoint-on-column-in-gcd-script.js"] + +["test_setBreakpoint-on-column.js"] + +["test_setBreakpoint-on-line-in-gcd-script.js"] + +["test_setBreakpoint-on-line-with-multiple-offsets.js"] + +["test_setBreakpoint-on-line-with-multiple-statements.js"] + +["test_setBreakpoint-on-line-with-no-offsets-in-gcd-script.js"] +skip-if = ["true"] # breakpoint sliding is not supported bug 1525685 + +["test_setBreakpoint-on-line-with-no-offsets.js"] +skip-if = ["true"] # breakpoint sliding is not supported bug 1525685 + +["test_setBreakpoint-on-line.js"] + +["test_shapes_highlighter_helpers.js"] + +["test_source-01.js"] + +["test_source-02.js"] + +["test_source-03.js"] + +["test_source-04.js"] + +["test_stepping-01.js"] + +["test_stepping-02.js"] + +["test_stepping-03.js"] + +["test_stepping-04.js"] + +["test_stepping-05.js"] + +["test_stepping-06.js"] + +["test_stepping-07.js"] + +["test_stepping-08.js"] + +["test_stepping-09.js"] + +["test_stepping-10.js"] + +["test_stepping-11.js"] + +["test_stepping-12.js"] + +["test_stepping-13.js"] + +["test_stepping-14.js"] + +["test_stepping-15.js"] + +["test_stepping-16.js"] + +["test_stepping-17.js"] + +["test_stepping-18.js"] + +["test_stepping-19.js"] + +["test_stepping-with-skip-breakpoints.js"] + +["test_symbolactor.js"] + +["test_symbols-01.js"] + +["test_symbols-02.js"] + +["test_threadlifetime-01.js"] + +["test_threadlifetime-02.js"] + +["test_threadlifetime-04.js"] + +["test_unsafeDereference.js"] + +["test_wasm_source-01.js"] + +["test_watchpoint-01.js"] + +["test_watchpoint-02.js"] + +["test_watchpoint-03.js"] + +["test_watchpoint-04.js"] +skip-if = ["apple_silicon"] # Disabled due to bleedover with other tests when run in regular suites; passes in "failures" jobs + +["test_watchpoint-05.js"] + +["test_webext_apis.js"] + +["test_webextension_descriptor.js"] + +["test_xpcshell_debugging.js"] +support-files = ["xpcshell_debugging_script.js"] diff --git a/devtools/server/tests/xpcshell/xpcshell_debugging_script.js b/devtools/server/tests/xpcshell/xpcshell_debugging_script.js new file mode 100644 index 0000000000..f762b1c3e8 --- /dev/null +++ b/devtools/server/tests/xpcshell/xpcshell_debugging_script.js @@ -0,0 +1,11 @@ +dump("hello from the debugee!\n"); +// We should hit the above dump as we set a breakpoint on the first line + +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This is a file that test_xpcshell_debugging.js debugs. + +debugger; // and why not check we hit this!? + +dump("try to set a breakpoint here"); |