summaryrefslogtreecommitdiffstats
path: root/toolkit/modules/tests/browser
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/modules/tests/browser')
-rw-r--r--toolkit/modules/tests/browser/browser.toml53
-rw-r--r--toolkit/modules/tests/browser/browser_AsyncPrefs.js133
-rw-r--r--toolkit/modules/tests/browser/browser_BrowserUtils.js50
-rw-r--r--toolkit/modules/tests/browser/browser_CreditCard.js46
-rw-r--r--toolkit/modules/tests/browser/browser_Deprecated.js140
-rw-r--r--toolkit/modules/tests/browser/browser_Finder.js73
-rw-r--r--toolkit/modules/tests/browser/browser_FinderHighlighter.js415
-rw-r--r--toolkit/modules/tests/browser/browser_FinderHighlighter2.js70
-rw-r--r--toolkit/modules/tests/browser/browser_Finder_hidden_textarea.js70
-rw-r--r--toolkit/modules/tests/browser/browser_Finder_offscreen_text.js72
-rw-r--r--toolkit/modules/tests/browser/browser_Finder_overflowed_onscreen.js45
-rw-r--r--toolkit/modules/tests/browser/browser_Finder_overflowed_textarea.js75
-rw-r--r--toolkit/modules/tests/browser/browser_Finder_pointer_events_none.js39
-rw-r--r--toolkit/modules/tests/browser/browser_Finder_skip_invisible_and_option.js132
-rw-r--r--toolkit/modules/tests/browser/browser_Finder_vertical_text.js59
-rw-r--r--toolkit/modules/tests/browser/browser_Geometry.js134
-rw-r--r--toolkit/modules/tests/browser/browser_InlineSpellChecker.js47
-rw-r--r--toolkit/modules/tests/browser/browser_Troubleshoot.js1380
-rw-r--r--toolkit/modules/tests/browser/browser_web_channel.js587
-rw-r--r--toolkit/modules/tests/browser/file_FinderIframeTest.html21
-rw-r--r--toolkit/modules/tests/browser/file_FinderSample.html824
-rw-r--r--toolkit/modules/tests/browser/file_getSelectionDetails_inputs.html9
-rw-r--r--toolkit/modules/tests/browser/file_web_channel.html235
-rw-r--r--toolkit/modules/tests/browser/file_web_channel_iframe.html96
-rw-r--r--toolkit/modules/tests/browser/head.js251
25 files changed, 5056 insertions, 0 deletions
diff --git a/toolkit/modules/tests/browser/browser.toml b/toolkit/modules/tests/browser/browser.toml
new file mode 100644
index 0000000000..932b06d19e
--- /dev/null
+++ b/toolkit/modules/tests/browser/browser.toml
@@ -0,0 +1,53 @@
+[DEFAULT]
+support-files = [
+ "file_FinderIframeTest.html",
+ "file_FinderSample.html",
+ "file_getSelectionDetails_inputs.html",
+ "head.js",
+]
+
+["browser_AsyncPrefs.js"]
+
+["browser_BrowserUtils.js"]
+
+["browser_CreditCard.js"]
+skip-if = ["apple_silicon"] # Disabled due to bleedover with other tests when run in regular suites; passes in "failures" jobs
+
+["browser_Deprecated.js"]
+
+["browser_Finder.js"]
+
+["browser_FinderHighlighter.js"]
+skip-if = ["true"] # bug 1831518 covers re-enabling
+
+["browser_FinderHighlighter2.js"]
+skip-if = ["true"] # bug 1831518 covers re-enabling
+
+["browser_Finder_hidden_textarea.js"]
+skip-if = ["verify && debug"]
+
+["browser_Finder_offscreen_text.js"]
+
+["browser_Finder_overflowed_onscreen.js"]
+
+["browser_Finder_overflowed_textarea.js"]
+skip-if = ["verify && debug && (os == 'mac' || os == 'linux')"]
+
+["browser_Finder_pointer_events_none.js"]
+
+["browser_Finder_skip_invisible_and_option.js"]
+
+["browser_Finder_vertical_text.js"]
+
+["browser_Geometry.js"]
+
+["browser_InlineSpellChecker.js"]
+
+["browser_Troubleshoot.js"]
+
+["browser_web_channel.js"]
+https_first_disabled = true
+support-files = [
+ "file_web_channel.html",
+ "file_web_channel_iframe.html",
+]
diff --git a/toolkit/modules/tests/browser/browser_AsyncPrefs.js b/toolkit/modules/tests/browser/browser_AsyncPrefs.js
new file mode 100644
index 0000000000..96eadc4b2e
--- /dev/null
+++ b/toolkit/modules/tests/browser/browser_AsyncPrefs.js
@@ -0,0 +1,133 @@
+"use strict";
+
+const kWhiteListedBool = "testing.allowed-prefs.some-bool-pref";
+const kWhiteListedChar = "testing.allowed-prefs.some-char-pref";
+const kWhiteListedInt = "testing.allowed-prefs.some-int-pref";
+
+function resetPrefs() {
+ for (let pref of [kWhiteListedBool, kWhiteListedChar, kWhiteListedBool]) {
+ Services.prefs.clearUserPref(pref);
+ }
+}
+
+registerCleanupFunction(resetPrefs);
+
+Services.prefs
+ .getDefaultBranch("testing.allowed-prefs.")
+ .setBoolPref("some-bool-pref", false);
+Services.prefs
+ .getDefaultBranch("testing.allowed-prefs.")
+ .setCharPref("some-char-pref", "");
+Services.prefs
+ .getDefaultBranch("testing.allowed-prefs.")
+ .setIntPref("some-int-pref", 0);
+
+async function runTest() {
+ let { AsyncPrefs } = ChromeUtils.importESModule(
+ "resource://gre/modules/AsyncPrefs.sys.mjs"
+ );
+ const kInChildProcess =
+ Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT;
+
+ // Need to define these again because when run in a content task we have no scope access.
+ const kNotWhiteListed = "some.pref.thats.not.whitelisted";
+ const kWhiteListedBool = "testing.allowed-prefs.some-bool-pref";
+ const kWhiteListedChar = "testing.allowed-prefs.some-char-pref";
+ const kWhiteListedInt = "testing.allowed-prefs.some-int-pref";
+
+ const procDesc = kInChildProcess ? "child process" : "parent process";
+
+ const valueResultMap = [
+ [true, "Bool"],
+ [false, "Bool"],
+ [10, "Int"],
+ [-1, "Int"],
+ ["", "Char"],
+ ["stuff", "Char"],
+ [[], false],
+ [{}, false],
+ [Services.io.newURI("http://mozilla.org/"), false],
+ ];
+
+ const prefMap = [
+ ["Bool", kWhiteListedBool],
+ ["Char", kWhiteListedChar],
+ ["Int", kWhiteListedInt],
+ ];
+
+ function doesFail(pref, value) {
+ let msg = `Should not succeed setting ${pref} to ${value} in ${procDesc}`;
+ return AsyncPrefs.set(pref, value).then(
+ () => ok(false, msg),
+ error => ok(true, msg + "; " + error)
+ );
+ }
+
+ function doesWork(pref, value) {
+ let msg = `Should be able to set ${pref} to ${value} in ${procDesc}`;
+ return AsyncPrefs.set(pref, value).then(
+ () => ok(true, msg),
+ error => ok(false, msg + "; " + error)
+ );
+ }
+
+ function doReset(pref) {
+ let msg = `Should be able to reset ${pref} in ${procDesc}`;
+ return AsyncPrefs.reset(pref).then(
+ () => ok(true, msg),
+ () => ok(false, msg)
+ );
+ }
+
+ for (let [val] of valueResultMap) {
+ await doesFail(kNotWhiteListed, val);
+ is(
+ Services.prefs.prefHasUserValue(kNotWhiteListed),
+ false,
+ "Pref shouldn't get changed"
+ );
+ }
+
+ let resetMsg = `Should not succeed resetting ${kNotWhiteListed} in ${procDesc}`;
+ AsyncPrefs.reset(kNotWhiteListed).then(
+ () => ok(false, resetMsg),
+ error => ok(true, resetMsg + "; " + error)
+ );
+
+ for (let [type, pref] of prefMap) {
+ for (let [val, result] of valueResultMap) {
+ if (result == type) {
+ await doesWork(pref, val);
+ is(
+ Services.prefs["get" + type + "Pref"](pref),
+ val,
+ "Pref should have been updated"
+ );
+ await doReset(pref);
+ } else {
+ await doesFail(pref, val);
+ is(
+ Services.prefs.prefHasUserValue(pref),
+ false,
+ `Pref ${pref} shouldn't get changed`
+ );
+ }
+ }
+ }
+}
+
+add_task(async function runInParent() {
+ await runTest();
+ resetPrefs();
+});
+
+if (gMultiProcessBrowser) {
+ add_task(async function runInChild() {
+ ok(
+ gBrowser.selectedBrowser.isRemoteBrowser,
+ "Should actually run this in child process"
+ );
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], runTest);
+ resetPrefs();
+ });
+}
diff --git a/toolkit/modules/tests/browser/browser_BrowserUtils.js b/toolkit/modules/tests/browser/browser_BrowserUtils.js
new file mode 100644
index 0000000000..da28c07b69
--- /dev/null
+++ b/toolkit/modules/tests/browser/browser_BrowserUtils.js
@@ -0,0 +1,50 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+add_task(async function test_getSelectionDetails_input() {
+ // Mostly a regression test for bug 1420560
+ const url = kFixtureBaseURL + "file_getSelectionDetails_inputs.html";
+ await BrowserTestUtils.withNewTab({ gBrowser, url }, async browser => {
+ await SpecialPowers.spawn(browser, [], () => {
+ function checkSelection({ id, text, linkURL }) {
+ const { SelectionUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/SelectionUtils.sys.mjs"
+ );
+ content.document.getElementById(id).select();
+ // It seems that when running as a test, the previous line will set
+ // the both the window's selection and the input's selection to contain
+ // the input's text. Outside of tests, only the input's selection seems
+ // to be updated, so we explicitly clear the window's selection to
+ // ensure we're doing the right thing in the case that only the input's
+ // selection is present.
+ content.getSelection().removeAllRanges();
+ let info = SelectionUtils.getSelectionDetails(content);
+ Assert.equal(text, info.text);
+ Assert.ok(!info.collapsed);
+ Assert.equal(linkURL, info.linkURL);
+ }
+
+ checkSelection({
+ id: "url-no-scheme",
+ text: "test.example.com",
+ linkURL: "http://test.example.com/",
+ });
+ checkSelection({
+ id: "url-with-scheme",
+ text: "https://test.example.com",
+ linkURL: "https://test.example.com/",
+ });
+ checkSelection({
+ id: "not-url",
+ text: "foo. bar",
+ linkURL: null,
+ });
+ checkSelection({
+ id: "not-url-number",
+ text: "3.5",
+ linkURL: null,
+ });
+ });
+ });
+});
diff --git a/toolkit/modules/tests/browser/browser_CreditCard.js b/toolkit/modules/tests/browser/browser_CreditCard.js
new file mode 100644
index 0000000000..304c988707
--- /dev/null
+++ b/toolkit/modules/tests/browser/browser_CreditCard.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { CreditCard } = ChromeUtils.importESModule(
+ "resource://gre/modules/CreditCard.sys.mjs"
+);
+const { OSKeyStore } = ChromeUtils.importESModule(
+ "resource://gre/modules/OSKeyStore.sys.mjs"
+);
+const { OSKeyStoreTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/OSKeyStoreTestUtils.sys.mjs"
+);
+
+let oldGetters = {};
+let gFakeLoggedIn = true;
+
+add_setup(function () {
+ OSKeyStoreTestUtils.setup();
+ oldGetters.isLoggedIn = Object.getOwnPropertyDescriptor(
+ OSKeyStore,
+ "isLoggedIn"
+ ).get;
+ OSKeyStore.__defineGetter__("isLoggedIn", () => gFakeLoggedIn);
+ registerCleanupFunction(async () => {
+ OSKeyStore.__defineGetter__("isLoggedIn", oldGetters.isLoggedIn);
+ await OSKeyStoreTestUtils.cleanup();
+ });
+});
+
+add_task(async function test_getLabel_withOSKeyStore() {
+ ok(
+ OSKeyStore.isLoggedIn,
+ "Confirm that OSKeyStore is faked and thinks it is logged in"
+ );
+
+ const ccNumber = "4111111111111111";
+ const encryptedNumber = await OSKeyStore.encrypt(ccNumber);
+ const decryptedNumber = await OSKeyStore.decrypt(encryptedNumber);
+ is(decryptedNumber, ccNumber, "Decrypted CC number should match original");
+
+ const name = "Foxkeh";
+ const label = CreditCard.getLabel({ name: "Foxkeh", number: ccNumber });
+ is(label, `**** 1111, ${name}`, "Label matches");
+});
diff --git a/toolkit/modules/tests/browser/browser_Deprecated.js b/toolkit/modules/tests/browser/browser_Deprecated.js
new file mode 100644
index 0000000000..b718ba37e7
--- /dev/null
+++ b/toolkit/modules/tests/browser/browser_Deprecated.js
@@ -0,0 +1,140 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const PREF_DEPRECATION_WARNINGS = "devtools.errorconsole.deprecation_warnings";
+
+// Using this named functions to test deprecation and the properly logged
+// callstacks.
+function basicDeprecatedFunction() {
+ Deprecated.warning("this method is deprecated.", "https://example.com");
+ return true;
+}
+
+function deprecationFunctionBogusCallstack() {
+ Deprecated.warning("this method is deprecated.", "https://example.com", {
+ caller: {},
+ });
+ return true;
+}
+
+function deprecationFunctionCustomCallstack() {
+ // Get the nsIStackFrame that will contain the name of this function.
+ function getStack() {
+ return Components.stack;
+ }
+ Deprecated.warning(
+ "this method is deprecated.",
+ "https://example.com",
+ getStack()
+ );
+ return true;
+}
+
+var tests = [
+ // Test deprecation warning without passing the callstack.
+ {
+ deprecatedFunction: basicDeprecatedFunction,
+ expectedObservation(aMessage) {
+ testAMessage(aMessage);
+ Assert.greater(
+ aMessage.indexOf("basicDeprecatedFunction"),
+ 0,
+ "Callstack is correctly logged."
+ );
+ },
+ },
+ // Test a reported error when URL to documentation is not passed.
+ {
+ deprecatedFunction() {
+ Deprecated.warning("this method is deprecated.");
+ return true;
+ },
+ expectedObservation(aMessage) {
+ Assert.greater(
+ aMessage.indexOf("must provide a URL"),
+ 0,
+ "Deprecation warning logged an empty URL argument."
+ );
+ },
+ },
+ // Test deprecation with a bogus callstack passed as an argument (it will be
+ // replaced with the current call stack).
+ {
+ deprecatedFunction: deprecationFunctionBogusCallstack,
+ expectedObservation(aMessage) {
+ testAMessage(aMessage);
+ Assert.greater(
+ aMessage.indexOf("deprecationFunctionBogusCallstack"),
+ 0,
+ "Callstack is correctly logged."
+ );
+ },
+ },
+ // Test deprecation with a valid custom callstack passed as an argument.
+ {
+ deprecatedFunction: deprecationFunctionCustomCallstack,
+ expectedObservation(aMessage) {
+ testAMessage(aMessage);
+ Assert.greater(
+ aMessage.indexOf("deprecationFunctionCustomCallstack"),
+ 0,
+ "Callstack is correctly logged."
+ );
+ },
+ // Set pref to true.
+ logWarnings: true,
+ },
+];
+
+// Test Console Message attributes.
+function testAMessage(aMessage) {
+ Assert.strictEqual(
+ aMessage.indexOf("DEPRECATION WARNING: this method is deprecated."),
+ 0,
+ "Deprecation is correctly logged."
+ );
+ Assert.greater(
+ aMessage.indexOf("https://example.com"),
+ 0,
+ "URL is correctly logged."
+ );
+}
+
+add_task(async function test_setup() {
+ Services.prefs.setBoolPref(PREF_DEPRECATION_WARNINGS, true);
+
+ // Check if Deprecated is loaded.
+ ok(Deprecated, "Deprecated object exists");
+});
+
+add_task(async function test_pref_enabled() {
+ for (let [idx, test] of tests.entries()) {
+ info("Running test #" + idx);
+
+ let promiseObserved = TestUtils.consoleMessageObserved(subject => {
+ let msg = subject.wrappedJSObject.arguments?.[0];
+ return (
+ msg.includes("DEPRECATION WARNING: ") ||
+ msg.includes("must provide a URL")
+ );
+ });
+
+ test.deprecatedFunction();
+
+ let msg = await promiseObserved;
+
+ test.expectedObservation(msg.wrappedJSObject.arguments?.[0]);
+ }
+});
+
+add_task(async function test_pref_disabled() {
+ // Deprecation warnings will be logged only when the preference is set.
+ Services.prefs.setBoolPref(PREF_DEPRECATION_WARNINGS, false);
+
+ let endFn = TestUtils.listenForConsoleMessages();
+ basicDeprecatedFunction();
+
+ let messages = await endFn();
+ Assert.equal(messages.length, 0, "Should not have received any messages");
+});
diff --git a/toolkit/modules/tests/browser/browser_Finder.js b/toolkit/modules/tests/browser/browser_Finder.js
new file mode 100644
index 0000000000..7bcf7e8a00
--- /dev/null
+++ b/toolkit/modules/tests/browser/browser_Finder.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/. */
+
+add_task(async function () {
+ const url =
+ "data:text/html;base64," +
+ btoa(
+ '<body><iframe srcdoc="content"/></iframe>' +
+ '<a href="http://test.com">test link</a>'
+ );
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+
+ let finder = tab.linkedBrowser.finder;
+ let listener = {
+ onFindResult() {
+ ok(false, "onFindResult callback wasn't replaced");
+ },
+ onHighlightFinished() {
+ ok(false, "onHighlightFinished callback wasn't replaced");
+ },
+ };
+ finder.addResultListener(listener);
+
+ function waitForFind(which = "onFindResult") {
+ return new Promise(resolve => {
+ listener[which] = resolve;
+ });
+ }
+
+ let promiseFind = waitForFind("onHighlightFinished");
+ finder.highlight(true, "content");
+ let findResult = await promiseFind;
+ Assert.ok(findResult.found, "should find string");
+
+ promiseFind = waitForFind("onHighlightFinished");
+ finder.highlight(true, "Bla");
+ findResult = await promiseFind;
+ Assert.ok(!findResult.found, "should not find string");
+
+ // Search only for links and draw outlines.
+ promiseFind = waitForFind();
+ finder.fastFind("test link", true, true);
+ findResult = await promiseFind;
+ is(findResult.result, Ci.nsITypeAheadFind.FIND_FOUND, "should find link");
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function (arg) {
+ Assert.ok(
+ !!content.document.getElementsByTagName("a")[0].style.outline,
+ "outline set"
+ );
+ });
+
+ // Just a simple search for "test link".
+ promiseFind = waitForFind();
+ finder.fastFind("test link", false, false);
+ findResult = await promiseFind;
+ is(
+ findResult.result,
+ Ci.nsITypeAheadFind.FIND_FOUND,
+ "should find link again"
+ );
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function (arg) {
+ Assert.ok(
+ !content.document.getElementsByTagName("a")[0].style.outline,
+ "outline not set"
+ );
+ });
+
+ finder.removeResultListener(listener);
+ gBrowser.removeTab(tab);
+});
diff --git a/toolkit/modules/tests/browser/browser_FinderHighlighter.js b/toolkit/modules/tests/browser/browser_FinderHighlighter.js
new file mode 100644
index 0000000000..7e377e47d1
--- /dev/null
+++ b/toolkit/modules/tests/browser/browser_FinderHighlighter.js
@@ -0,0 +1,415 @@
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+"use strict";
+
+const kIteratorTimeout = Services.prefs.getIntPref("findbar.iteratorTimeout");
+const kPrefHighlightAll = "findbar.highlightAll";
+const kPrefModalHighlight = "findbar.modalHighlight";
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [kPrefHighlightAll, true],
+ [kPrefModalHighlight, true],
+ ],
+ });
+});
+
+// Test the results of modal highlighting, which is on by default.
+add_task(async function testModalResults() {
+ let tests = new Map([
+ [
+ "Roland",
+ {
+ rectCount: 2,
+ insertCalls: [2, 4],
+ removeCalls: [0, 1],
+ animationCalls: [1, 2],
+ },
+ ],
+ [
+ "their law might propagate their kind",
+ {
+ rectCount: 2,
+ insertCalls: [5, 6],
+ removeCalls: [4, 5],
+ // eslint-disable-next-line object-shorthand
+ extraTest: function (maskNode, outlineNode, rects) {
+ Assert.equal(
+ outlineNode.getElementsByTagName("div").length,
+ 2,
+ "There should be multiple rects drawn"
+ );
+ },
+ },
+ ],
+ [
+ "ro",
+ {
+ rectCount: 41,
+ insertCalls: [1, 4],
+ removeCalls: [0, 2],
+ },
+ ],
+ [
+ "new",
+ {
+ rectCount: 2,
+ insertCalls: [1, 4],
+ removeCalls: [0, 2],
+ },
+ ],
+ [
+ "o",
+ {
+ rectCount: 492,
+ insertCalls: [1, 4],
+ removeCalls: [0, 2],
+ },
+ ],
+ ]);
+ let url = kFixtureBaseURL + "file_FinderSample.html";
+ await BrowserTestUtils.withNewTab(url, async function (browser) {
+ let findbar = await gBrowser.getFindBar();
+
+ for (let [word, expectedResult] of tests) {
+ await promiseOpenFindbar(findbar);
+ Assert.ok(!findbar.hidden, "Findbar should be open now.");
+
+ let timeout = kIteratorTimeout;
+ if (word.length == 1) {
+ timeout *= 4;
+ } else if (word.length == 2) {
+ timeout *= 2;
+ }
+ await new Promise(resolve => setTimeout(resolve, timeout));
+ let promise = promiseTestHighlighterOutput(
+ browser,
+ word,
+ expectedResult,
+ expectedResult.extraTest
+ );
+ await promiseEnterStringIntoFindField(findbar, word);
+ await promise;
+
+ findbar.close(true);
+ }
+ });
+});
+
+// Test if runtime switching of highlight modes between modal and non-modal works
+// as expected.
+add_task(async function testModalSwitching() {
+ let url = kFixtureBaseURL + "file_FinderSample.html";
+ await BrowserTestUtils.withNewTab(url, async function (browser) {
+ let findbar = await gBrowser.getFindBar();
+
+ await promiseOpenFindbar(findbar);
+ Assert.ok(!findbar.hidden, "Findbar should be open now.");
+
+ let word = "Roland";
+ let expectedResult = {
+ rectCount: 2,
+ insertCalls: [2, 4],
+ removeCalls: [0, 1],
+ };
+ let promise = promiseTestHighlighterOutput(browser, word, expectedResult);
+ await promiseEnterStringIntoFindField(findbar, word);
+ await promise;
+
+ await SpecialPowers.pushPrefEnv({ set: [[kPrefModalHighlight, false]] });
+
+ expectedResult = {
+ rectCount: 0,
+ insertCalls: [0, 0],
+ removeCalls: [0, 0],
+ };
+ promise = promiseTestHighlighterOutput(browser, word, expectedResult);
+ findbar.clear();
+ await promiseEnterStringIntoFindField(findbar, word);
+ await promise;
+
+ findbar.close(true);
+ });
+
+ await SpecialPowers.pushPrefEnv({ set: [[kPrefModalHighlight, true]] });
+});
+
+// Test if highlighting a dark page is detected properly.
+add_task(async function testDarkPageDetection() {
+ let url = kFixtureBaseURL + "file_FinderSample.html";
+ await BrowserTestUtils.withNewTab(url, async function (browser) {
+ let findbar = await gBrowser.getFindBar();
+
+ await promiseOpenFindbar(findbar);
+
+ let word = "Roland";
+ let expectedResult = {
+ rectCount: 2,
+ insertCalls: [1, 3],
+ removeCalls: [0, 1],
+ };
+ let promise = promiseTestHighlighterOutput(
+ browser,
+ word,
+ expectedResult,
+ function (node) {
+ Assert.ok(
+ node.style.background.startsWith("rgba(0, 0, 0"),
+ "White HTML page should have a black background color set for the mask"
+ );
+ }
+ );
+ await promiseEnterStringIntoFindField(findbar, word);
+ await promise;
+
+ findbar.close(true);
+ });
+
+ await BrowserTestUtils.withNewTab(url, async function (browser) {
+ let findbar = await gBrowser.getFindBar();
+
+ await promiseOpenFindbar(findbar);
+
+ let word = "Roland";
+ let expectedResult = {
+ rectCount: 2,
+ insertCalls: [2, 4],
+ removeCalls: [0, 1],
+ };
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ let dwu = content.windowUtils;
+ let uri =
+ "data:text/css;charset=utf-8," +
+ encodeURIComponent(`
+ body {
+ background: maroon radial-gradient(circle, #a01010 0%, #800000 80%) center center / cover no-repeat;
+ color: white;
+ }`);
+ try {
+ dwu.loadSheetUsingURIString(uri, dwu.USER_SHEET);
+ } catch (e) {}
+ });
+
+ let promise = promiseTestHighlighterOutput(
+ browser,
+ word,
+ expectedResult,
+ node => {
+ Assert.ok(
+ node.style.background.startsWith("rgba(255, 255, 255"),
+ "Dark HTML page should have a white background color set for the mask"
+ );
+ }
+ );
+ await promiseEnterStringIntoFindField(findbar, word);
+ await promise;
+
+ findbar.close(true);
+ });
+});
+
+add_task(async function testHighlightAllToggle() {
+ let url = kFixtureBaseURL + "file_FinderSample.html";
+ await BrowserTestUtils.withNewTab(url, async function (browser) {
+ let findbar = await gBrowser.getFindBar();
+
+ await promiseOpenFindbar(findbar);
+
+ let word = "Roland";
+ let expectedResult = {
+ rectCount: 2,
+ insertCalls: [2, 4],
+ removeCalls: [0, 1],
+ };
+ let promise = promiseTestHighlighterOutput(browser, word, expectedResult);
+ await promiseEnterStringIntoFindField(findbar, word);
+ await promise;
+
+ // We now know we have multiple rectangles highlighted, so it's a good time
+ // to flip the pref.
+ expectedResult = {
+ rectCount: 0,
+ insertCalls: [0, 1],
+ removeCalls: [1, 2],
+ };
+ promise = promiseTestHighlighterOutput(browser, word, expectedResult);
+ await SpecialPowers.pushPrefEnv({ set: [[kPrefHighlightAll, false]] });
+ await promise;
+
+ // For posterity, let's switch back.
+ expectedResult = {
+ rectCount: 2,
+ insertCalls: [1, 3],
+ removeCalls: [0, 1],
+ };
+ promise = promiseTestHighlighterOutput(browser, word, expectedResult);
+ await SpecialPowers.pushPrefEnv({ set: [[kPrefHighlightAll, true]] });
+ await promise;
+ });
+});
+
+add_task(async function testXMLDocument() {
+ let url =
+ "data:text/xml;charset=utf-8," +
+ encodeURIComponent(`<?xml version="1.0"?>
+<result>
+ <Title>Example</Title>
+ <Error>Error</Error>
+</result>`);
+ await BrowserTestUtils.withNewTab(url, async function (browser) {
+ let findbar = await gBrowser.getFindBar();
+
+ await promiseOpenFindbar(findbar);
+
+ let word = "Example";
+ let expectedResult = {
+ rectCount: 0,
+ insertCalls: [1, 4],
+ removeCalls: [0, 1],
+ };
+ let promise = promiseTestHighlighterOutput(browser, word, expectedResult);
+ await promiseEnterStringIntoFindField(findbar, word);
+ await promise;
+
+ findbar.close(true);
+ });
+});
+
+add_task(async function testHideOnLocationChange() {
+ let url = kFixtureBaseURL + "file_FinderSample.html";
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+ let browser = tab.linkedBrowser;
+ let findbar = await gBrowser.getFindBar();
+
+ await promiseOpenFindbar(findbar);
+
+ let word = "Roland";
+ let expectedResult = {
+ rectCount: 2,
+ insertCalls: [2, 4],
+ removeCalls: [0, 1],
+ };
+ let promise = promiseTestHighlighterOutput(browser, word, expectedResult);
+ await promiseEnterStringIntoFindField(findbar, word);
+ await promise;
+
+ // Now we try to navigate away! (Using the same page)
+ promise = promiseTestHighlighterOutput(browser, word, {
+ rectCount: 0,
+ insertCalls: [0, 0],
+ removeCalls: [1, 2],
+ });
+ BrowserTestUtils.startLoadingURIString(browser, url);
+ await promise;
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testHideOnClear() {
+ let url = kFixtureBaseURL + "file_FinderSample.html";
+ await BrowserTestUtils.withNewTab(url, async function (browser) {
+ let findbar = await gBrowser.getFindBar();
+ await promiseOpenFindbar(findbar);
+
+ let word = "Roland";
+ let expectedResult = {
+ rectCount: 2,
+ insertCalls: [2, 4],
+ removeCalls: [0, 2],
+ };
+ let promise = promiseTestHighlighterOutput(browser, word, expectedResult);
+ await promiseEnterStringIntoFindField(findbar, word);
+ await promise;
+
+ await new Promise(resolve => setTimeout(resolve, kIteratorTimeout));
+ promise = promiseTestHighlighterOutput(browser, "", {
+ rectCount: 0,
+ insertCalls: [0, 0],
+ removeCalls: [1, 2],
+ });
+ findbar.clear();
+ await promise;
+
+ findbar.close(true);
+ });
+});
+
+add_task(async function testRectsAndTexts() {
+ let url =
+ "data:text/html;charset=utf-8," +
+ encodeURIComponent(
+ '<div style="width: 150px; border: 1px solid black">' +
+ "Here are a lot of words Please use find to highlight some words that wrap" +
+ " across a line boundary and see what happens.</div>"
+ );
+ await BrowserTestUtils.withNewTab(url, async function (browser) {
+ let findbar = await gBrowser.getFindBar();
+ await promiseOpenFindbar(findbar);
+
+ let word = "words please use find to";
+ let expectedResult = {
+ rectCount: 2,
+ insertCalls: [2, 4],
+ removeCalls: [0, 2],
+ };
+ let promise = promiseTestHighlighterOutput(
+ browser,
+ word,
+ expectedResult,
+ (maskNode, outlineNode) => {
+ let boxes = outlineNode.getElementsByTagName("span");
+ Assert.equal(
+ boxes.length,
+ 2,
+ "There should be two outline boxes containing text"
+ );
+ Assert.equal(
+ boxes[0].textContent.trim(),
+ "words",
+ "First text should match"
+ );
+ Assert.equal(
+ boxes[1].textContent.trim(),
+ "Please use find to",
+ "Second word should match"
+ );
+ }
+ );
+ await promiseEnterStringIntoFindField(findbar, word);
+ await promise;
+ });
+});
+
+add_task(async function testTooLargeToggle() {
+ let url = kFixtureBaseURL + "file_FinderSample.html";
+ await BrowserTestUtils.withNewTab(url, async function (browser) {
+ let findbar = await gBrowser.getFindBar();
+ await promiseOpenFindbar(findbar);
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ let dwu = content.windowUtils;
+ let uri =
+ "data:text/css;charset=utf-8," +
+ encodeURIComponent(`
+ body {
+ min-height: 1234567px;
+ }`);
+ try {
+ dwu.loadSheetUsingURIString(uri, dwu.USER_SHEET);
+ } catch (e) {}
+ });
+
+ let word = "Roland";
+ let expectedResult = {
+ rectCount: 2,
+ insertCalls: [2, 4],
+ removeCalls: [0, 2],
+ // No animations should be triggered when the page is too large.
+ animationCalls: [0, 0],
+ };
+ let promise = promiseTestHighlighterOutput(browser, word, expectedResult);
+ await promiseEnterStringIntoFindField(findbar, word);
+ await promise;
+ });
+});
diff --git a/toolkit/modules/tests/browser/browser_FinderHighlighter2.js b/toolkit/modules/tests/browser/browser_FinderHighlighter2.js
new file mode 100644
index 0000000000..1fa026b333
--- /dev/null
+++ b/toolkit/modules/tests/browser/browser_FinderHighlighter2.js
@@ -0,0 +1,70 @@
+"use strict";
+
+const kPrefHighlightAll = "findbar.highlightAll";
+const kPrefModalHighlight = "findbar.modalHighlight";
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [kPrefHighlightAll, true],
+ [kPrefModalHighlight, true],
+ ],
+ });
+});
+
+add_task(async function testIframeOffset() {
+ let url = kFixtureBaseURL + "file_FinderIframeTest.html";
+
+ await BrowserTestUtils.withNewTab(url, async function (browser) {
+ let findbar = gBrowser.getFindBar();
+ await promiseOpenFindbar(findbar);
+
+ let word = "frame";
+ let expectedResult = {
+ rectCount: 12,
+ insertCalls: [2, 4],
+ removeCalls: [0, 2],
+ };
+ let promise = promiseTestHighlighterOutput(
+ browser,
+ word,
+ expectedResult,
+ (maskNode, outlineNode, rects) => {
+ Assert.equal(
+ rects.length,
+ expectedResult.rectCount,
+ "Rect counts should match"
+ );
+ // Checks to guard against regressing this functionality:
+ let expectedOffsets = [
+ { x: 16, y: 60 },
+ { x: 68, y: 104 },
+ { x: 21, y: 215 },
+ { x: 78, y: 264 },
+ { x: 21, y: 375 },
+ { x: 78, y: 424 },
+ { x: 20, y: 534 },
+ { x: 93, y: 534 },
+ { x: 71, y: 577 },
+ { x: 145, y: 577 },
+ ];
+ for (let i = 1, l = rects.length - 1; i < l; ++i) {
+ let rect = rects[i];
+ let expected = expectedOffsets[i - 1];
+ Assert.equal(
+ Math.floor(rect.x),
+ expected.x,
+ "Horizontal offset should match for rect " + i
+ );
+ Assert.equal(
+ Math.floor(rect.y),
+ expected.y,
+ "Vertical offset should match for rect " + i
+ );
+ }
+ }
+ );
+ await promiseEnterStringIntoFindField(findbar, word);
+ await promise;
+ });
+});
diff --git a/toolkit/modules/tests/browser/browser_Finder_hidden_textarea.js b/toolkit/modules/tests/browser/browser_Finder_hidden_textarea.js
new file mode 100644
index 0000000000..149bae7666
--- /dev/null
+++ b/toolkit/modules/tests/browser/browser_Finder_hidden_textarea.js
@@ -0,0 +1,70 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+add_task(async function test_bug1174036() {
+ const URI =
+ "<body><textarea>e1</textarea><textarea>e2</textarea><textarea>e3</textarea></body>";
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "data:text/html;charset=utf-8," + encodeURIComponent(URI),
+ },
+ async function (browser) {
+ // Hide the first textarea.
+ await SpecialPowers.spawn(browser, [], function () {
+ content.document.getElementsByTagName("textarea")[0].style.display =
+ "none";
+ });
+
+ let finder = browser.finder;
+ let listener = {
+ onFindResult() {
+ ok(false, "callback wasn't replaced");
+ },
+ };
+ finder.addResultListener(listener);
+
+ function waitForFind() {
+ return new Promise(resolve => {
+ listener.onFindResult = resolve;
+ });
+ }
+
+ // Find the first 'e' (which should be in the second textarea).
+ let promiseFind = waitForFind();
+ finder.fastFind("e", false, false);
+ let findResult = await promiseFind;
+ is(
+ findResult.result,
+ Ci.nsITypeAheadFind.FIND_FOUND,
+ "find first string"
+ );
+
+ let firstRect = findResult.rect;
+
+ // Find the second 'e' (in the third textarea).
+ promiseFind = waitForFind();
+ finder.findAgain("e", false, false, false);
+ findResult = await promiseFind;
+ is(
+ findResult.result,
+ Ci.nsITypeAheadFind.FIND_FOUND,
+ "find second string"
+ );
+ ok(!findResult.rect.equals(firstRect), "found new string");
+
+ // Ensure that we properly wrap to the second textarea.
+ promiseFind = waitForFind();
+ finder.findAgain("e", false, false, false);
+ findResult = await promiseFind;
+ is(
+ findResult.result,
+ Ci.nsITypeAheadFind.FIND_WRAPPED,
+ "wrapped to first string"
+ );
+ ok(findResult.rect.equals(firstRect), "wrapped to original string");
+
+ finder.removeResultListener(listener);
+ }
+ );
+});
diff --git a/toolkit/modules/tests/browser/browser_Finder_offscreen_text.js b/toolkit/modules/tests/browser/browser_Finder_offscreen_text.js
new file mode 100644
index 0000000000..ea6bb4508f
--- /dev/null
+++ b/toolkit/modules/tests/browser/browser_Finder_offscreen_text.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function test_offscreen_text() {
+ // Generate URI of a big DOM that contains the target text at several
+ // line positions (to force some targets to be offscreen).
+ const linesToGenerate = 155;
+ const linesToInsertTargetText = [5, 50, 150];
+ let targetCount = linesToInsertTargetText.length;
+ let t = 0;
+ const TARGET_TEXT = "findthis";
+
+ let URI = "<body>";
+ for (let i = 0; i < linesToGenerate; i++) {
+ URI += i + "<br>";
+ if (t < targetCount && linesToInsertTargetText[t] == i) {
+ URI += TARGET_TEXT;
+ t++;
+ }
+ }
+ URI += "</body>";
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "data:text/html;charset=utf-8," + encodeURIComponent(URI),
+ },
+ async function (browser) {
+ let finder = browser.finder;
+ let listener = {
+ onFindResult() {
+ ok(false, "callback wasn't replaced");
+ },
+ };
+ finder.addResultListener(listener);
+
+ function waitForFind() {
+ return new Promise(resolve => {
+ listener.onFindResult = resolve;
+ });
+ }
+
+ // Find each of the targets.
+ for (let t = 0; t < targetCount; ++t) {
+ let promiseFind = waitForFind();
+ if (t == 0) {
+ finder.fastFind(TARGET_TEXT, false, false);
+ } else {
+ finder.findAgain(TARGET_TEXT, false, false, false);
+ }
+ let findResult = await promiseFind;
+ is(
+ findResult.result,
+ Ci.nsITypeAheadFind.FIND_FOUND,
+ "Found target " + t
+ );
+ }
+
+ // Find one more time and make sure we wrap.
+ let promiseFind = waitForFind();
+ finder.findAgain(TARGET_TEXT, false, false, false);
+ let findResult = await promiseFind;
+ is(
+ findResult.result,
+ Ci.nsITypeAheadFind.FIND_WRAPPED,
+ "Wrapped to first target"
+ );
+
+ finder.removeResultListener(listener);
+ }
+ );
+});
diff --git a/toolkit/modules/tests/browser/browser_Finder_overflowed_onscreen.js b/toolkit/modules/tests/browser/browser_Finder_overflowed_onscreen.js
new file mode 100644
index 0000000000..c31014c943
--- /dev/null
+++ b/toolkit/modules/tests/browser/browser_Finder_overflowed_onscreen.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function test_vertical_text() {
+ const URI =
+ '<body><div style="max-height: 100px; max-width: 100px; overflow: scroll;"><div style="padding-left: 100px; max-height: 100px; max-width: 200px; overflow: auto;">d<br/><br/><br/><br/>c----------------b<br/><br/><br/><br/>a</div></div></body>';
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "data:text/html;charset=utf-8," + encodeURIComponent(URI),
+ },
+ async function (browser) {
+ let finder = browser.finder;
+ let listener = {
+ onFindResult() {
+ ok(false, "callback wasn't replaced");
+ },
+ };
+ finder.addResultListener(listener);
+
+ function waitForFind() {
+ return new Promise(resolve => {
+ listener.onFindResult = resolve;
+ });
+ }
+
+ let targets = ["a", "b", "c", "d"];
+
+ for (let i = 0; i < targets.length; ++i) {
+ // Find the target text.
+ let target = targets[i];
+ let promiseFind = waitForFind();
+ finder.fastFind(target, false, false);
+ let findResult = await promiseFind;
+ isnot(
+ findResult.result,
+ Ci.nsITypeAheadFind.FIND_NOTFOUND,
+ "Found target text '" + target + "'."
+ );
+ }
+
+ finder.removeResultListener(listener);
+ }
+ );
+});
diff --git a/toolkit/modules/tests/browser/browser_Finder_overflowed_textarea.js b/toolkit/modules/tests/browser/browser_Finder_overflowed_textarea.js
new file mode 100644
index 0000000000..479ee6c4bb
--- /dev/null
+++ b/toolkit/modules/tests/browser/browser_Finder_overflowed_textarea.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+add_task(async function test_offscreen_text() {
+ // Generate URI of a big DOM that contains the target text
+ // within a textarea at several line positions (to force
+ // some targets to be overflowed).
+ const linesToGenerate = 155;
+ const linesToInsertTargetText = [5, 50, 150];
+ const targetCount = linesToInsertTargetText.length;
+ let t = 0;
+ const TARGET_TEXT = "findthis";
+
+ let URI = "<body><textarea>";
+ for (let i = 0; i < linesToGenerate; i++) {
+ URI += i + " ";
+ if (t < targetCount && linesToInsertTargetText[t] == i) {
+ URI += TARGET_TEXT;
+ t++;
+ }
+ URI += "\n";
+ }
+ URI += "</textarea></body>";
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "data:text/html;charset=utf-8," + encodeURIComponent(URI),
+ },
+ async function (browser) {
+ let finder = browser.finder;
+ let listener = {
+ onFindResult() {
+ ok(false, "callback wasn't replaced");
+ },
+ };
+ finder.addResultListener(listener);
+
+ function waitForFind() {
+ return new Promise(resolve => {
+ listener.onFindResult = resolve;
+ });
+ }
+
+ // Find each of the targets.
+ for (let t = 0; t < targetCount; ++t) {
+ let promiseFind = waitForFind();
+ if (t == 0) {
+ finder.fastFind(TARGET_TEXT, false, false);
+ } else {
+ finder.findAgain(TARGET_TEXT, false, false, false);
+ }
+ let findResult = await promiseFind;
+ is(
+ findResult.result,
+ Ci.nsITypeAheadFind.FIND_FOUND,
+ "Found target " + t
+ );
+ }
+
+ // Find one more time and make sure we wrap.
+ let promiseFind = waitForFind();
+ finder.findAgain(TARGET_TEXT, false, false, false);
+ let findResult = await promiseFind;
+ is(
+ findResult.result,
+ Ci.nsITypeAheadFind.FIND_WRAPPED,
+ "Wrapped to first target"
+ );
+
+ finder.removeResultListener(listener);
+ }
+ );
+});
diff --git a/toolkit/modules/tests/browser/browser_Finder_pointer_events_none.js b/toolkit/modules/tests/browser/browser_Finder_pointer_events_none.js
new file mode 100644
index 0000000000..2e6e31d4cf
--- /dev/null
+++ b/toolkit/modules/tests/browser/browser_Finder_pointer_events_none.js
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function test_offscreen_text() {
+ const URI = '<body><div style="pointer-events:none">find this</div></body>';
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "data:text/html;charset=utf-8," + encodeURIComponent(URI),
+ },
+ async function (browser) {
+ let finder = browser.finder;
+ let listener = {
+ onFindResult() {
+ ok(false, "callback wasn't replaced");
+ },
+ };
+ finder.addResultListener(listener);
+
+ function waitForFind() {
+ return new Promise(resolve => {
+ listener.onFindResult = resolve;
+ });
+ }
+
+ // Find the target text.
+ let promiseFind = waitForFind();
+ finder.fastFind("find this", false, false);
+ let findResult = await promiseFind;
+ is(
+ findResult.result,
+ Ci.nsITypeAheadFind.FIND_FOUND,
+ "Found target text."
+ );
+
+ finder.removeResultListener(listener);
+ }
+ );
+});
diff --git a/toolkit/modules/tests/browser/browser_Finder_skip_invisible_and_option.js b/toolkit/modules/tests/browser/browser_Finder_skip_invisible_and_option.js
new file mode 100644
index 0000000000..c133bbb613
--- /dev/null
+++ b/toolkit/modules/tests/browser/browser_Finder_skip_invisible_and_option.js
@@ -0,0 +1,132 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function test_skip_invisible() {
+ const URI = `
+ <body>
+ <div>
+ a
+ <div style="visibility:hidden;">a</div>
+ <div style="visibility: hidden"><span style="visibility: visible">a</div>
+ <select>
+ <option>a</option>
+ </select>
+ <select size=2>
+ <option>a</option>
+ <option>a</option>
+ </select>
+ <input placeholder="a">
+ </body>`;
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "data:text/html;charset=utf-8," + encodeURIComponent(URI),
+ },
+ async function (browser) {
+ let finder = browser.finder;
+ let listener = {
+ onFindResult() {
+ ok(false, "callback wasn't replaced");
+ },
+ };
+ finder.addResultListener(listener);
+
+ function waitForFind() {
+ return new Promise(resolve => {
+ listener.onFindResult = resolve;
+ });
+ }
+
+ // Find the target text. There should be three results.
+ let target = "a";
+ let promiseFind = waitForFind();
+ finder.fastFind(target, false, false);
+ let findResult = await promiseFind;
+
+ // Check the results and repeat four times. After the final repeat, make
+ // sure we've wrapped to the beginning.
+ let i = 0;
+ for (; i < 4; i++) {
+ isnot(
+ findResult.result,
+ Ci.nsITypeAheadFind.FIND_NOTFOUND,
+ "Should find target text '" + target + "' instance " + (i + 1) + "."
+ );
+
+ promiseFind = waitForFind();
+ finder.findAgain("a", false, false, false);
+ findResult = await promiseFind;
+ }
+ is(
+ findResult.result,
+ Ci.nsITypeAheadFind.FIND_WRAPPED,
+ "After " + (i + 1) + " searches, we should wrap to first target text."
+ );
+
+ finder.removeResultListener(listener);
+ }
+ );
+});
+
+add_task(async function test_find_anon_content() {
+ const URI = `
+ <!doctype html>
+ <style>
+ div::before { content: "before content"; }
+ div::after { content: "after content"; }
+ span::after { content: ","; }
+ </style>
+ <div> </div>
+ <img alt="Some fallback text">
+ <input type="submit" value="Some button text">
+ <input type="password" value="password">
+ <p>1<span></span>234</p>
+
+ `;
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "data:text/html;charset=utf-8," + encodeURIComponent(URI),
+ },
+ async function (browser) {
+ let finder = browser.finder;
+ let listener = {
+ onFindResult() {
+ ok(false, "callback wasn't replaced");
+ },
+ };
+ finder.addResultListener(listener);
+
+ function waitForFind() {
+ return new Promise(resolve => {
+ listener.onFindResult = resolve;
+ });
+ }
+
+ async function assertFindable(text, findable = true) {
+ let promiseFind = waitForFind();
+ finder.fastFind(text, false, false);
+ let findResult = await promiseFind;
+ is(
+ findResult.result,
+ findable
+ ? Ci.nsITypeAheadFind.FIND_FOUND
+ : Ci.nsITypeAheadFind.FIND_NOTFOUND,
+ `${text} should ${findable ? "" : "not "}be findable`
+ );
+ }
+
+ await assertFindable("before content");
+ await assertFindable("after content");
+ await assertFindable("fallback text");
+ await assertFindable("button text");
+ await assertFindable("password", false);
+
+ // TODO(emilio): In an ideal world we could select the comma as well and
+ // then you'd find it with "1,234" instead...
+ await assertFindable("1234");
+
+ finder.removeResultListener(listener);
+ }
+ );
+});
diff --git a/toolkit/modules/tests/browser/browser_Finder_vertical_text.js b/toolkit/modules/tests/browser/browser_Finder_vertical_text.js
new file mode 100644
index 0000000000..c20c4ea8f6
--- /dev/null
+++ b/toolkit/modules/tests/browser/browser_Finder_vertical_text.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function test_vertical_text() {
+ const URI =
+ '<body><div style="writing-mode: vertical-rl">vertical-rl</div><div style="writing-mode: vertical-lr">vertical-lr</div><div style="writing-mode: sideways-rl">sideways-rl</div><div style="writing-mode: sideways-lr">sideways-lr</div></body>';
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "data:text/html;charset=utf-8," + encodeURIComponent(URI),
+ },
+ async function (browser) {
+ let finder = browser.finder;
+ let listener = {
+ onFindResult() {
+ ok(false, "callback wasn't replaced");
+ },
+ };
+ finder.addResultListener(listener);
+
+ function waitForFind() {
+ return new Promise(resolve => {
+ listener.onFindResult = resolve;
+ });
+ }
+
+ let targets = [
+ // Full matches use one path in our find code.
+ "vertical-rl",
+ "vertical-lr",
+ "sideways-rl",
+ "sideways-lr",
+ // Partial matches use a second path in our find code.
+ "l-r",
+ "l-l",
+ "s-r",
+ "s-l",
+ ];
+
+ for (let i = 0; i < targets.length; ++i) {
+ // Find the target text.
+ let target = targets[i];
+ let promiseFind = waitForFind();
+ finder.fastFind(target, false, false);
+ let findResult = await promiseFind;
+
+ // We check the logical inversion of not not found, because found and wrapped are
+ // two different correct results, but not found is the only incorrect result.
+ isnot(
+ findResult.result,
+ Ci.nsITypeAheadFind.FIND_NOTFOUND,
+ "Found target text '" + target + "'."
+ );
+ }
+
+ finder.removeResultListener(listener);
+ }
+ );
+});
diff --git a/toolkit/modules/tests/browser/browser_Geometry.js b/toolkit/modules/tests/browser/browser_Geometry.js
new file mode 100644
index 0000000000..83aeb305d1
--- /dev/null
+++ b/toolkit/modules/tests/browser/browser_Geometry.js
@@ -0,0 +1,134 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { Point, Rect } = ChromeUtils.importESModule(
+ "resource://gre/modules/Geometry.sys.mjs"
+);
+
+function test() {
+ ok(Rect, "Rect class exists");
+ for (var fname in tests) {
+ tests[fname]();
+ }
+}
+
+var tests = {
+ testGetDimensions() {
+ let r = new Rect(5, 10, 100, 50);
+ Assert.equal(r.left, 5, "rect has correct left value");
+ Assert.equal(r.top, 10, "rect has correct top value");
+ Assert.equal(r.right, 105, "rect has correct right value");
+ Assert.equal(r.bottom, 60, "rect has correct bottom value");
+ Assert.equal(r.width, 100, "rect has correct width value");
+ Assert.equal(r.height, 50, "rect has correct height value");
+ Assert.equal(r.x, 5, "rect has correct x value");
+ Assert.equal(r.y, 10, "rect has correct y value");
+ },
+
+ testIsEmpty() {
+ let r = new Rect(0, 0, 0, 10);
+ ok(r.isEmpty(), "rect with nonpositive width is empty");
+ r = new Rect(0, 0, 10, 0);
+ ok(r.isEmpty(), "rect with nonpositive height is empty");
+ r = new Rect(0, 0, 10, 10);
+ ok(!r.isEmpty(), "rect with positive dimensions is not empty");
+ },
+
+ testRestrictTo() {
+ let r1 = new Rect(10, 10, 100, 100);
+ let r2 = new Rect(50, 50, 100, 100);
+ r1.restrictTo(r2);
+ ok(r1.equals(new Rect(50, 50, 60, 60)), "intersection is non-empty");
+
+ r1 = new Rect(10, 10, 100, 100);
+ r2 = new Rect(120, 120, 100, 100);
+ r1.restrictTo(r2);
+ ok(r1.isEmpty(), "intersection is empty");
+
+ r1 = new Rect(10, 10, 100, 100);
+ r2 = new Rect(0, 0, 0, 0);
+ r1.restrictTo(r2);
+ ok(r1.isEmpty(), "intersection of rect and empty is empty");
+
+ r1 = new Rect(0, 0, 0, 0);
+ r2 = new Rect(0, 0, 0, 0);
+ r1.restrictTo(r2);
+ ok(r1.isEmpty(), "intersection of empty and empty is empty");
+ },
+
+ testExpandToContain() {
+ let r1 = new Rect(10, 10, 100, 100);
+ let r2 = new Rect(50, 50, 100, 100);
+ r1.expandToContain(r2);
+ ok(
+ r1.equals(new Rect(10, 10, 140, 140)),
+ "correct expandToContain on intersecting rectangles"
+ );
+
+ r1 = new Rect(10, 10, 100, 100);
+ r2 = new Rect(120, 120, 100, 100);
+ r1.expandToContain(r2);
+ ok(
+ r1.equals(new Rect(10, 10, 210, 210)),
+ "correct expandToContain on non-intersecting rectangles"
+ );
+
+ r1 = new Rect(10, 10, 100, 100);
+ r2 = new Rect(0, 0, 0, 0);
+ r1.expandToContain(r2);
+ ok(
+ r1.equals(new Rect(10, 10, 100, 100)),
+ "expandToContain of rect and empty is rect"
+ );
+
+ r1 = new Rect(10, 10, 0, 0);
+ r2 = new Rect(0, 0, 0, 0);
+ r1.expandToContain(r2);
+ ok(r1.isEmpty(), "expandToContain of empty and empty is empty");
+ },
+
+ testSubtract: function testSubtract() {
+ function equals(rects1, rects2) {
+ return (
+ rects1.length == rects2.length &&
+ rects1.every(function (r, i) {
+ return r.equals(rects2[i]);
+ })
+ );
+ }
+
+ let r1 = new Rect(0, 0, 100, 100);
+ let r2 = new Rect(500, 500, 100, 100);
+ ok(
+ equals(r1.subtract(r2), [r1]),
+ "subtract area outside of region yields same region"
+ );
+
+ r1 = new Rect(0, 0, 100, 100);
+ r2 = new Rect(-10, -10, 50, 120);
+ ok(
+ equals(r1.subtract(r2), [new Rect(40, 0, 60, 100)]),
+ "subtracting vertical bar from edge leaves one rect"
+ );
+
+ r1 = new Rect(0, 0, 100, 100);
+ r2 = new Rect(-10, -10, 120, 50);
+ ok(
+ equals(r1.subtract(r2), [new Rect(0, 40, 100, 60)]),
+ "subtracting horizontal bar from edge leaves one rect"
+ );
+
+ r1 = new Rect(0, 0, 100, 100);
+ r2 = new Rect(40, 40, 20, 20);
+ ok(
+ equals(r1.subtract(r2), [
+ new Rect(0, 0, 40, 100),
+ new Rect(40, 0, 20, 40),
+ new Rect(40, 60, 20, 40),
+ new Rect(60, 0, 40, 100),
+ ]),
+ "subtracting rect in middle leaves union of rects"
+ );
+ },
+};
diff --git a/toolkit/modules/tests/browser/browser_InlineSpellChecker.js b/toolkit/modules/tests/browser/browser_InlineSpellChecker.js
new file mode 100644
index 0000000000..c931615091
--- /dev/null
+++ b/toolkit/modules/tests/browser/browser_InlineSpellChecker.js
@@ -0,0 +1,47 @@
+var InlineSpellChecker;
+var SpellCheckHelper;
+
+function test() {
+ let tempScope = ChromeUtils.importESModule(
+ "resource://gre/modules/InlineSpellChecker.sys.mjs"
+ );
+ InlineSpellChecker = tempScope.InlineSpellChecker;
+ SpellCheckHelper = tempScope.SpellCheckHelper;
+
+ ok(InlineSpellChecker, "InlineSpellChecker class exists");
+ for (var fname in tests) {
+ tests[fname]();
+ }
+}
+
+var tests = {
+ testFlagsForInputs() {
+ const HTML_NS = "http://www.w3.org/1999/xhtml";
+ const { INPUT, EDITABLE, TEXTINPUT, NUMERIC, PASSWORD, SPELLCHECKABLE } =
+ SpellCheckHelper;
+ const kExpectedResults = {
+ text: INPUT | EDITABLE | TEXTINPUT | SPELLCHECKABLE,
+ password: INPUT | EDITABLE | TEXTINPUT | PASSWORD,
+ search: INPUT | EDITABLE | TEXTINPUT | SPELLCHECKABLE,
+ url: INPUT | EDITABLE | TEXTINPUT,
+ tel: INPUT | EDITABLE | TEXTINPUT,
+ email: INPUT | EDITABLE | TEXTINPUT,
+ number: INPUT | EDITABLE | TEXTINPUT | NUMERIC,
+ checkbox: INPUT,
+ radio: INPUT,
+ };
+
+ for (let [type, expectedFlags] of Object.entries(kExpectedResults)) {
+ let input = document.createElementNS(HTML_NS, "input");
+ input.type = type;
+ let actualFlags = SpellCheckHelper.isEditable(input, window);
+ is(
+ actualFlags,
+ expectedFlags,
+ `For input type "${type}" expected flags ${
+ "0x" + expectedFlags.toString(16)
+ }; got ${"0x" + actualFlags.toString(16)}`
+ );
+ }
+ },
+};
diff --git a/toolkit/modules/tests/browser/browser_Troubleshoot.js b/toolkit/modules/tests/browser/browser_Troubleshoot.js
new file mode 100644
index 0000000000..d627f175e4
--- /dev/null
+++ b/toolkit/modules/tests/browser/browser_Troubleshoot.js
@@ -0,0 +1,1380 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Ideally this would be an xpcshell test, but Troubleshoot relies on things
+// that aren't initialized outside of a XUL app environment like AddonManager
+// and the "@mozilla.org/xre/app-info;1" component.
+
+const { Troubleshoot } = ChromeUtils.importESModule(
+ "resource://gre/modules/Troubleshoot.sys.mjs"
+);
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+
+const { FeatureGate } = ChromeUtils.importESModule(
+ "resource://featuregates/FeatureGate.sys.mjs"
+);
+const { PreferenceExperiments } = ChromeUtils.importESModule(
+ "resource://normandy/lib/PreferenceExperiments.sys.mjs"
+);
+const { PreferenceRollouts } = ChromeUtils.importESModule(
+ "resource://normandy/lib/PreferenceRollouts.sys.mjs"
+);
+const { AddonStudies } = ChromeUtils.importESModule(
+ "resource://normandy/lib/AddonStudies.sys.mjs"
+);
+const { NormandyTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/NormandyTestUtils.sys.mjs"
+);
+
+NormandyTestUtils.init({ Assert });
+
+add_task(async function snapshotSchema() {
+ let snapshot = await Troubleshoot.snapshot();
+ try {
+ validateObject(snapshot, SNAPSHOT_SCHEMA);
+ ok(true, "The snapshot should conform to the schema.");
+ } catch (err) {
+ ok(false, "Schema mismatch, " + err);
+ }
+});
+
+add_task(async function experimentalFeatures() {
+ let featureGates = await FeatureGate.all();
+ ok(featureGates.length, "Should be at least one FeatureGate");
+
+ let snapshot = await Troubleshoot.snapshot();
+ for (let i = 0; i < snapshot.experimentalFeatures.length; i++) {
+ let experimentalFeature = snapshot.experimentalFeatures[i];
+ is(
+ experimentalFeature[0],
+ featureGates[i].title,
+ "The first item in the array should be the title's l10n-id of the FeatureGate"
+ );
+ is(
+ experimentalFeature[1],
+ featureGates[i].preference,
+ "The second item in the array should be the preference name for the FeatureGate"
+ );
+ is(
+ experimentalFeature[2],
+ Services.prefs.getBoolPref(featureGates[i].preference),
+ "The third item in the array should be the preference value of the FeatureGate"
+ );
+ }
+});
+
+add_task(async function modifiedPreferences() {
+ let prefs = [
+ "javascript.troubleshoot",
+ "troubleshoot.foo",
+ "network.proxy.troubleshoot",
+ "print.print_to_filename",
+ ];
+ prefs.forEach(function (p) {
+ Services.prefs.setBoolPref(p, true);
+ is(Services.prefs.getBoolPref(p), true, "The pref should be set: " + p);
+ });
+ Services.prefs.setCharPref("dom.push.userAgentID", "testvalue");
+ let snapshot = await Troubleshoot.snapshot();
+ let p = snapshot.modifiedPreferences;
+ is(
+ p["javascript.troubleshoot"],
+ true,
+ "The pref should be present because it's in the allowed prefs " +
+ "and not in the pref regexes that are disallowed."
+ );
+ ok(
+ !("troubleshoot.foo" in p),
+ "The pref should be absent because it's not in the allowed prefs."
+ );
+ ok(
+ !("network.proxy.troubleshoot" in p),
+ "The pref should be absent because it's in the pref regexes " +
+ "that are disallowed."
+ );
+ ok(
+ !("dom.push.userAgentID" in p),
+ "The pref should be absent because it's in the pref regexes " +
+ "that are disallowed."
+ );
+ ok(
+ !("print.print_to_filename" in p),
+ "The pref should be absent because it's not in the allowed prefs."
+ );
+ prefs.forEach(p => Services.prefs.deleteBranch(p));
+ Services.prefs.clearUserPref("dom.push.userAgentID");
+});
+
+add_task(async function unicodePreferences() {
+ let name = "font.name.sans-serif.x-western";
+ let utf8Value = "\xc4\x8capk\xc5\xafv Krasopis";
+ let unicodeValue = "\u010Capk\u016Fv Krasopis";
+
+ // set/getCharPref work with 8bit strings (utf8)
+ Services.prefs.setCharPref(name, utf8Value);
+
+ let snapshot = await Troubleshoot.snapshot();
+ let p = snapshot.modifiedPreferences;
+ is(p[name], unicodeValue, "The pref should have correct Unicode value.");
+ Services.prefs.deleteBranch(name);
+});
+
+add_task(async function printingPreferences() {
+ let prefs = [
+ "javascript.print_to_filename",
+ "print.print_bgimages",
+ "print.print_to_filename",
+ ];
+ prefs.forEach(function (p) {
+ Services.prefs.setBoolPref(p, true);
+ is(Services.prefs.getBoolPref(p), true, "The pref should be set: " + p);
+ });
+ let snapshot = await Troubleshoot.snapshot();
+ let p = snapshot.printingPreferences;
+ is(p["print.print_bgimages"], true, "The pref should be present");
+ ok(
+ !("print.print_to_filename" in p),
+ "The pref should not be present (sensitive)"
+ );
+ ok(
+ !("javascript.print_to_filename" in p),
+ "The pref should be absent because it's not a print pref."
+ );
+ prefs.forEach(p => Services.prefs.deleteBranch(p));
+});
+
+add_task(function normandy() {
+ const {
+ preferenceStudyFactory,
+ branchedAddonStudyFactory,
+ preferenceRolloutFactory,
+ } = NormandyTestUtils.factories;
+
+ return NormandyTestUtils.decorate(
+ PreferenceExperiments.withMockExperiments([
+ preferenceStudyFactory({
+ userFacingName: "Test Pref Study B",
+ branch: "test-branch-pref",
+ }),
+ preferenceStudyFactory({
+ userFacingName: "Test Pref Study A",
+ branch: "test-branch-pref",
+ }),
+ ]),
+ AddonStudies.withStudies([
+ branchedAddonStudyFactory({
+ userFacingName: "Test Addon Study B",
+ branch: "test-branch-addon",
+ }),
+ branchedAddonStudyFactory({
+ userFacingName: "Test Addon Study A",
+ branch: "test-branch-addon",
+ }),
+ ]),
+ PreferenceRollouts.withTestMock({
+ rollouts: [
+ preferenceRolloutFactory({
+ statue: "ACTIVE",
+ slug: "test-pref-rollout-b",
+ }),
+ preferenceRolloutFactory({
+ statue: "ACTIVE",
+ slug: "test-pref-rollout-a",
+ }),
+ ],
+ }),
+ async function testNormandyInfoInTroubleshooting({
+ prefExperiments,
+ addonStudies,
+ prefRollouts,
+ }) {
+ let snapshot = await Troubleshoot.snapshot();
+ let info = snapshot.normandy;
+ // The order should be flipped, since each category is sorted by slug.
+ Assert.deepEqual(
+ info.prefStudies,
+ [prefExperiments[1], prefExperiments[0]],
+ "prefs studies should exist in the right order"
+ );
+ Assert.deepEqual(
+ info.addonStudies,
+ [addonStudies[1], addonStudies[0]],
+ "addon studies should exist in the right order"
+ );
+ Assert.deepEqual(
+ info.prefRollouts,
+ [prefRollouts[1], prefRollouts[0]],
+ "pref rollouts should exist in the right order"
+ );
+ }
+ )();
+});
+
+add_task(function normandyErrorHandling() {
+ return NormandyTestUtils.decorate(
+ NormandyTestUtils.withStub(PreferenceExperiments, "getAllActive", {
+ returnValue: Promise.reject("Expected error - PreferenceExperiments"),
+ }),
+ NormandyTestUtils.withStub(AddonStudies, "getAllActive", {
+ returnValue: Promise.reject("Expected error - AddonStudies"),
+ }),
+ NormandyTestUtils.withStub(PreferenceRollouts, "getAllActive", {
+ returnValue: Promise.reject("Expected error - PreferenceRollouts"),
+ }),
+ async function testNormandyErrorHandling() {
+ let consoleEndFn = TestUtils.listenForConsoleMessages();
+ let snapshot = await Troubleshoot.snapshot();
+ let info = snapshot.normandy;
+ Assert.deepEqual(
+ info.prefStudies,
+ [],
+ "prefs studies should be an empty list if there is an error"
+ );
+ Assert.deepEqual(
+ info.addonStudies,
+ [],
+ "addon studies should be an empty list if there is an error"
+ );
+ Assert.deepEqual(
+ info.prefRollouts,
+ [],
+ "pref rollouts should be an empty list if there is an error"
+ );
+ let msgs = await consoleEndFn();
+ let expectedSet = new Set([
+ /Expected error - PreferenceExperiments/,
+ /Expected error - AddonStudies/,
+ /Expected error - PreferenceRollouts/,
+ ]);
+
+ for (let msg of msgs) {
+ msg = msg.wrappedJSObject;
+ if (msg.level != "error") {
+ continue;
+ }
+
+ let msgContents = msg.arguments[0];
+ for (let expected of expectedSet) {
+ if (expected.test(msgContents)) {
+ expectedSet.delete(expected);
+ break;
+ }
+ }
+ }
+
+ Assert.equal(
+ expectedSet.size,
+ 0,
+ "Should have no messages left in the expected set"
+ );
+ }
+ )();
+});
+
+add_task(async function themes() {
+ let snapshot = await Troubleshoot.snapshot();
+ let foundTheme = false;
+ for (let addon of snapshot.addons) {
+ if (addon.type == "theme") {
+ foundTheme = true;
+ break;
+ }
+ }
+ ok(foundTheme, "found a theme in the addons list");
+});
+
+// This is inspired by JSON Schema, or by the example on its Wikipedia page
+// anyway.
+const SNAPSHOT_SCHEMA = {
+ type: "object",
+ required: true,
+ properties: {
+ application: {
+ required: true,
+ type: "object",
+ properties: {
+ name: {
+ required: true,
+ type: "string",
+ },
+ version: {
+ required: true,
+ type: "string",
+ },
+ buildID: {
+ required: true,
+ type: "string",
+ },
+ distributionID: {
+ required: true,
+ type: "string",
+ },
+ userAgent: {
+ required: true,
+ type: "string",
+ },
+ osVersion: {
+ required: true,
+ type: "string",
+ },
+ osTheme: {
+ type: "string",
+ },
+ rosetta: {
+ required: false,
+ type: "boolean",
+ },
+ vendor: {
+ type: "string",
+ },
+ updateChannel: {
+ type: "string",
+ },
+ supportURL: {
+ type: "string",
+ },
+ launcherProcessState: {
+ type: "number",
+ },
+ remoteAutoStart: {
+ type: "boolean",
+ required: true,
+ },
+ fissionAutoStart: {
+ type: "boolean",
+ },
+ fissionDecisionStatus: {
+ type: "string",
+ },
+ numTotalWindows: {
+ type: "number",
+ },
+ numFissionWindows: {
+ type: "number",
+ },
+ numRemoteWindows: {
+ type: "number",
+ },
+ policiesStatus: {
+ type: "number",
+ },
+ keyLocationServiceGoogleFound: {
+ type: "boolean",
+ },
+ keySafebrowsingGoogleFound: {
+ type: "boolean",
+ },
+ keyMozillaFound: {
+ type: "boolean",
+ },
+ safeMode: {
+ type: "boolean",
+ },
+ memorySizeBytes: {
+ type: "number",
+ },
+ diskAvailableBytes: {
+ type: "number",
+ },
+ pointingDevices: {
+ required: false,
+ type: "array",
+ },
+ },
+ },
+ crashes: {
+ required: false,
+ type: "object",
+ properties: {
+ pending: {
+ required: true,
+ type: "number",
+ },
+ submitted: {
+ required: true,
+ type: "array",
+ items: {
+ type: "object",
+ properties: {
+ id: {
+ required: true,
+ type: "string",
+ },
+ date: {
+ required: true,
+ type: "number",
+ },
+ pending: {
+ required: true,
+ type: "boolean",
+ },
+ },
+ },
+ },
+ },
+ },
+ addons: {
+ required: true,
+ type: "array",
+ items: {
+ type: "object",
+ properties: {
+ name: {
+ required: true,
+ type: "string",
+ },
+ type: {
+ required: true,
+ type: "string",
+ },
+ version: {
+ required: true,
+ type: "string",
+ },
+ id: {
+ required: true,
+ type: "string",
+ },
+ isActive: {
+ required: true,
+ type: "boolean",
+ },
+ },
+ },
+ },
+ securitySoftware: {
+ required: false,
+ type: "object",
+ properties: {
+ registeredAntiVirus: {
+ required: true,
+ type: "string",
+ },
+ registeredAntiSpyware: {
+ required: true,
+ type: "string",
+ },
+ registeredFirewall: {
+ required: true,
+ type: "string",
+ },
+ },
+ },
+ features: {
+ required: true,
+ type: "array",
+ items: {
+ type: "object",
+ properties: {
+ name: {
+ required: true,
+ type: "string",
+ },
+ version: {
+ required: true,
+ type: "string",
+ },
+ id: {
+ required: true,
+ type: "string",
+ },
+ },
+ },
+ },
+ processes: {
+ required: true,
+ type: "object",
+ properties: {
+ maxWebContentProcesses: {
+ required: true,
+ type: "number",
+ },
+ remoteTypes: {
+ required: true,
+ type: "object",
+ },
+ },
+ },
+ experimentalFeatures: {
+ required: true,
+ type: "array",
+ },
+ environmentVariables: {
+ required: true,
+ type: "object",
+ },
+ modifiedPreferences: {
+ required: true,
+ type: "object",
+ },
+ printingPreferences: {
+ required: true,
+ type: "object",
+ },
+ lockedPreferences: {
+ required: true,
+ type: "object",
+ properties: {
+ "fission.autostart": {
+ required: false,
+ type: "boolean",
+ },
+ "fission.autostart.session": {
+ required: false,
+ type: "boolean",
+ },
+ "media.utility-process.enabled": {
+ required: false,
+ type: "boolean",
+ },
+ "media.utility-ffmpeg.enabled": {
+ required: false,
+ type: "boolean",
+ },
+ "media.utility-ffvpx.enabled": {
+ required: false,
+ type: "boolean",
+ },
+ "media.utility-wmf.enabled": {
+ required: false,
+ type: "boolean",
+ },
+ "media.utility-applemedia.enabled": {
+ required: false,
+ type: "boolean",
+ },
+ "media.utility-vorbis.enabled": {
+ required: false,
+ type: "boolean",
+ },
+ "media.utility-wav.enabled": {
+ required: false,
+ type: "boolean",
+ },
+ "media.utility-opus.enabled": {
+ required: false,
+ type: "boolean",
+ },
+ },
+ },
+ places: {
+ required: true,
+ type: "array",
+ items: {
+ type: "object",
+ items: {
+ entity: {
+ required: true,
+ type: "string",
+ },
+ count: {
+ required: true,
+ type: "number",
+ },
+ sizeBytes: {
+ required: true,
+ type: "number",
+ },
+ sizePerc: {
+ required: true,
+ type: "number",
+ },
+ efficiencyPerc: {
+ required: true,
+ type: "number",
+ },
+ sequentialityPerc: {
+ required: true,
+ type: "number",
+ },
+ },
+ },
+ },
+ graphics: {
+ required: true,
+ type: "object",
+ properties: {
+ numTotalWindows: {
+ required: true,
+ type: "number",
+ },
+ numAcceleratedWindows: {
+ required: true,
+ type: "number",
+ },
+ graphicsDevicePixelRatios: {
+ type: "array",
+ items: {
+ type: "number",
+ },
+ },
+ windowLayerManagerType: {
+ type: "string",
+ },
+ windowLayerManagerRemote: {
+ type: "boolean",
+ },
+ numAcceleratedWindowsMessage: {
+ type: "object",
+ properties: {
+ key: {
+ required: true,
+ type: "string",
+ },
+ args: {
+ required: false,
+ type: "object",
+ },
+ },
+ },
+ adapterDescription: {
+ type: "string",
+ },
+ adapterVendorID: {
+ type: "string",
+ },
+ adapterDeviceID: {
+ type: "string",
+ },
+ adapterSubsysID: {
+ type: "string",
+ },
+ adapterRAM: {
+ type: "number",
+ },
+ adapterDrivers: {
+ type: "string",
+ },
+ driverVendor: {
+ type: "string",
+ },
+ driverVersion: {
+ type: "string",
+ },
+ driverDate: {
+ type: "string",
+ },
+ adapterDescription2: {
+ type: "string",
+ },
+ adapterVendorID2: {
+ type: "string",
+ },
+ adapterDeviceID2: {
+ type: "string",
+ },
+ adapterSubsysID2: {
+ type: "string",
+ },
+ adapterRAM2: {
+ type: "number",
+ },
+ adapterDrivers2: {
+ type: "string",
+ },
+ driverVendor2: {
+ type: "string",
+ },
+ driverVersion2: {
+ type: "string",
+ },
+ driverDate2: {
+ type: "string",
+ },
+ isGPU2Active: {
+ type: "boolean",
+ },
+ direct2DEnabled: {
+ type: "boolean",
+ },
+ directWriteEnabled: {
+ type: "boolean",
+ },
+ directWriteVersion: {
+ type: "string",
+ },
+ clearTypeParameters: {
+ type: "string",
+ },
+ webgl1Renderer: {
+ type: "string",
+ },
+ webgl1Version: {
+ type: "string",
+ },
+ webgl1DriverExtensions: {
+ type: "string",
+ },
+ webgl1Extensions: {
+ type: "string",
+ },
+ webgl1WSIInfo: {
+ type: "string",
+ },
+ webgl2Renderer: {
+ type: "string",
+ },
+ webgl2Version: {
+ type: "string",
+ },
+ webgl2DriverExtensions: {
+ type: "string",
+ },
+ webgl2Extensions: {
+ type: "string",
+ },
+ webgl2WSIInfo: {
+ type: "string",
+ },
+ webgpuDefaultAdapter: {
+ type: "object",
+ },
+ webgpuFallbackAdapter: {
+ type: "object",
+ },
+ info: {
+ type: "object",
+ },
+ failures: {
+ type: "object",
+ properties: {
+ key: {
+ required: true,
+ type: "string",
+ },
+ args: {
+ required: false,
+ type: "object",
+ },
+ },
+ },
+ indices: {
+ type: "array",
+ items: {
+ type: "number",
+ },
+ },
+ featureLog: {
+ type: "object",
+ },
+ crashGuards: {
+ type: "array",
+ },
+ direct2DEnabledMessage: {
+ type: "object",
+ properties: {
+ key: {
+ required: true,
+ type: "string",
+ },
+ args: {
+ required: false,
+ type: "object",
+ },
+ },
+ },
+ targetFrameRate: {
+ type: "number",
+ },
+ windowProtocol: {
+ type: "string",
+ },
+ desktopEnvironment: {
+ type: "string",
+ },
+ supportFontDetermination: {
+ type: "string",
+ },
+ },
+ },
+ media: {
+ required: true,
+ type: "object",
+ properties: {
+ currentAudioBackend: {
+ required: true,
+ type: "string",
+ },
+ currentMaxAudioChannels: {
+ required: true,
+ type: "number",
+ },
+ currentPreferredSampleRate: {
+ required: true,
+ type: "number",
+ },
+ audioOutputDevices: {
+ required: true,
+ type: "array",
+ items: {
+ type: "object",
+ properties: {
+ name: {
+ required: true,
+ type: "string",
+ },
+ groupId: {
+ required: true,
+ type: "string",
+ },
+ vendor: {
+ required: true,
+ type: "string",
+ },
+ type: {
+ required: true,
+ type: "number",
+ },
+ state: {
+ required: true,
+ type: "number",
+ },
+ preferred: {
+ required: true,
+ type: "number",
+ },
+ supportedFormat: {
+ required: true,
+ type: "number",
+ },
+ defaultFormat: {
+ required: true,
+ type: "number",
+ },
+ maxChannels: {
+ required: true,
+ type: "number",
+ },
+ defaultRate: {
+ required: true,
+ type: "number",
+ },
+ maxRate: {
+ required: true,
+ type: "number",
+ },
+ minRate: {
+ required: true,
+ type: "number",
+ },
+ maxLatency: {
+ required: true,
+ type: "number",
+ },
+ minLatency: {
+ required: true,
+ type: "number",
+ },
+ },
+ },
+ },
+ audioInputDevices: {
+ required: true,
+ type: "array",
+ items: {
+ type: "object",
+ properties: {
+ name: {
+ required: true,
+ type: "string",
+ },
+ groupId: {
+ required: true,
+ type: "string",
+ },
+ vendor: {
+ required: true,
+ type: "string",
+ },
+ type: {
+ required: true,
+ type: "number",
+ },
+ state: {
+ required: true,
+ type: "number",
+ },
+ preferred: {
+ required: true,
+ type: "number",
+ },
+ supportedFormat: {
+ required: true,
+ type: "number",
+ },
+ defaultFormat: {
+ required: true,
+ type: "number",
+ },
+ maxChannels: {
+ required: true,
+ type: "number",
+ },
+ defaultRate: {
+ required: true,
+ type: "number",
+ },
+ maxRate: {
+ required: true,
+ type: "number",
+ },
+ minRate: {
+ required: true,
+ type: "number",
+ },
+ maxLatency: {
+ required: true,
+ type: "number",
+ },
+ minLatency: {
+ required: true,
+ type: "number",
+ },
+ },
+ },
+ },
+ codecSupportInfo: {
+ required: false,
+ type: "string",
+ },
+ },
+ },
+ accessibility: {
+ required: true,
+ type: "object",
+ properties: {
+ isActive: {
+ required: true,
+ type: "boolean",
+ },
+ forceDisabled: {
+ type: "number",
+ },
+ handlerUsed: {
+ type: "boolean",
+ },
+ instantiator: {
+ type: "string",
+ },
+ },
+ },
+ libraryVersions: {
+ required: true,
+ type: "object",
+ properties: {
+ NSPR: {
+ required: true,
+ type: "object",
+ properties: {
+ minVersion: {
+ required: true,
+ type: "string",
+ },
+ version: {
+ required: true,
+ type: "string",
+ },
+ },
+ },
+ NSS: {
+ required: true,
+ type: "object",
+ properties: {
+ minVersion: {
+ required: true,
+ type: "string",
+ },
+ version: {
+ required: true,
+ type: "string",
+ },
+ },
+ },
+ NSSUTIL: {
+ required: true,
+ type: "object",
+ properties: {
+ minVersion: {
+ required: true,
+ type: "string",
+ },
+ version: {
+ required: true,
+ type: "string",
+ },
+ },
+ },
+ NSSSSL: {
+ required: true,
+ type: "object",
+ properties: {
+ minVersion: {
+ required: true,
+ type: "string",
+ },
+ version: {
+ required: true,
+ type: "string",
+ },
+ },
+ },
+ NSSSMIME: {
+ required: true,
+ type: "object",
+ properties: {
+ minVersion: {
+ required: true,
+ type: "string",
+ },
+ version: {
+ required: true,
+ type: "string",
+ },
+ },
+ },
+ },
+ },
+ userJS: {
+ required: true,
+ type: "object",
+ properties: {
+ exists: {
+ required: true,
+ type: "boolean",
+ },
+ },
+ },
+ sandbox: {
+ required: false,
+ type: "object",
+ properties: {
+ hasSeccompBPF: {
+ required: AppConstants.platform == "linux",
+ type: "boolean",
+ },
+ hasSeccompTSync: {
+ required: AppConstants.platform == "linux",
+ type: "boolean",
+ },
+ hasUserNamespaces: {
+ required: AppConstants.platform == "linux",
+ type: "boolean",
+ },
+ hasPrivilegedUserNamespaces: {
+ required: AppConstants.platform == "linux",
+ type: "boolean",
+ },
+ canSandboxContent: {
+ required: false,
+ type: "boolean",
+ },
+ canSandboxMedia: {
+ required: false,
+ type: "boolean",
+ },
+ contentSandboxLevel: {
+ required: AppConstants.MOZ_SANDBOX,
+ type: "number",
+ },
+ effectiveContentSandboxLevel: {
+ required: AppConstants.MOZ_SANDBOX,
+ type: "number",
+ },
+ contentWin32kLockdownState: {
+ required: AppConstants.MOZ_SANDBOX && AppConstants.platform == "win",
+ type: "string",
+ },
+ supportSandboxGpuLevel: {
+ required: AppConstants.MOZ_SANDBOX && AppConstants.platform == "win",
+ type: "number",
+ },
+ syscallLog: {
+ required: AppConstants.platform == "linux",
+ type: "array",
+ items: {
+ type: "object",
+ properties: {
+ index: {
+ required: true,
+ type: "number",
+ },
+ pid: {
+ required: true,
+ type: "number",
+ },
+ tid: {
+ required: true,
+ type: "number",
+ },
+ procType: {
+ required: true,
+ type: "string",
+ },
+ syscall: {
+ required: true,
+ type: "number",
+ },
+ args: {
+ required: true,
+ type: "array",
+ items: {
+ type: "string",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ startupCache: {
+ required: false,
+ type: "object",
+ properties: {
+ DiskCachePath: {
+ required: true,
+ type: "string",
+ },
+ IgnoreDiskCache: {
+ required: true,
+ type: "boolean",
+ },
+ FoundDiskCacheOnInit: {
+ required: true,
+ type: "boolean",
+ },
+ WroteToDiskCache: {
+ required: true,
+ type: "boolean",
+ },
+ },
+ },
+ intl: {
+ required: true,
+ type: "object",
+ properties: {
+ localeService: {
+ required: true,
+ type: "object",
+ properties: {
+ requested: {
+ required: true,
+ type: "array",
+ },
+ available: {
+ required: true,
+ type: "array",
+ },
+ supported: {
+ required: true,
+ type: "array",
+ },
+ regionalPrefs: {
+ required: true,
+ type: "array",
+ },
+ defaultLocale: {
+ required: true,
+ type: "string",
+ },
+ },
+ },
+ osPrefs: {
+ required: true,
+ type: "object",
+ properties: {
+ systemLocales: {
+ required: true,
+ type: "array",
+ },
+ regionalPrefsLocales: {
+ required: true,
+ type: "array",
+ },
+ },
+ },
+ },
+ },
+ remoteAgent: {
+ type: "object",
+ properties: {
+ running: {
+ required: true,
+ type: "boolean",
+ },
+ url: {
+ required: true,
+ type: "string",
+ },
+ },
+ },
+ normandy: {
+ type: "object",
+ required: AppConstants.MOZ_NORMANDY,
+ properties: {
+ addonStudies: {
+ type: "array",
+ items: {
+ type: "object",
+ properties: {
+ userFacingName: { type: "string", required: true },
+ branch: { type: "string", required: true },
+ },
+ },
+ required: true,
+ },
+ prefRollouts: {
+ type: "array",
+ items: {
+ type: "object",
+ properties: {
+ slug: { type: "string", required: true },
+ state: { type: "string", required: true },
+ },
+ },
+ required: true,
+ },
+ prefStudies: {
+ type: "array",
+ items: {
+ type: "object",
+ properties: {
+ userFacingName: { type: "string", required: true },
+ branch: { type: "string", required: true },
+ },
+ },
+ required: true,
+ },
+ nimbusExperiments: {
+ type: "array",
+ items: {
+ type: "object",
+ properties: {
+ userFacingName: { type: "string", required: true },
+ branch: {
+ type: "object",
+ properties: {
+ slug: { type: "string", required: true },
+ },
+ },
+ },
+ },
+ required: true,
+ },
+ nimbusRollouts: {
+ type: "array",
+ items: {
+ type: "object",
+ properties: {
+ featureId: { type: "string", required: true },
+ slug: { type: "string", required: true },
+ },
+ },
+ },
+ },
+ },
+ legacyUserStylesheets: {
+ type: "object",
+ properties: {
+ active: {
+ required: true,
+ type: "boolean",
+ },
+ types: {
+ required: true,
+ type: "array",
+ },
+ },
+ },
+ },
+};
+
+/**
+ * Throws an Error if obj doesn't conform to schema. That way you get a nice
+ * error message and a stack to help you figure out what went wrong, which you
+ * wouldn't get if this just returned true or false instead. There's still
+ * room for improvement in communicating validation failures, however.
+ *
+ * @param obj The object to validate.
+ * @param schema The schema that obj should conform to.
+ */
+function validateObject(obj, schema) {
+ if (obj === undefined && !schema.required) {
+ return;
+ }
+ if (typeof schema.type != "string") {
+ throw schemaErr("'type' must be a string", schema);
+ }
+ if (objType(obj) != schema.type) {
+ throw validationErr("Object is not of the expected type", obj, schema);
+ }
+ let validatorFnName = "validateObject_" + schema.type;
+ if (!(validatorFnName in this)) {
+ throw schemaErr("Validator function not defined for type", schema);
+ }
+ this[validatorFnName](obj, schema);
+}
+
+function validateObject_object(obj, schema) {
+ if (typeof schema.properties != "object") {
+ // Don't care what obj's properties are.
+ return;
+ }
+ // First check that all the schema's properties match the object.
+ for (let prop in schema.properties) {
+ validateObject(obj[prop], schema.properties[prop]);
+ }
+ // Now check that the object doesn't have any properties not in the schema.
+ for (let prop in obj) {
+ if (!(prop in schema.properties)) {
+ throw validationErr(
+ "Object has property " + prop + " not in schema",
+ obj,
+ schema
+ );
+ }
+ }
+}
+
+function validateObject_array(array, schema) {
+ if (typeof schema.items != "object") {
+ // Don't care what the array's elements are.
+ return;
+ }
+ array.forEach(elt => validateObject(elt, schema.items));
+}
+
+function validateObject_string(str, schema) {}
+function validateObject_boolean(bool, schema) {}
+function validateObject_number(num, schema) {}
+
+function validationErr(msg, obj, schema) {
+ return new Error(
+ "Validation error: " +
+ msg +
+ ": object=" +
+ JSON.stringify(obj) +
+ ", schema=" +
+ JSON.stringify(schema)
+ );
+}
+
+function schemaErr(msg, schema) {
+ return new Error("Schema error: " + msg + ": " + JSON.stringify(schema));
+}
+
+function objType(obj) {
+ let type = typeof obj;
+ if (type != "object") {
+ return type;
+ }
+ if (Array.isArray(obj)) {
+ return "array";
+ }
+ if (obj === null) {
+ return "null";
+ }
+ return type;
+}
diff --git a/toolkit/modules/tests/browser/browser_web_channel.js b/toolkit/modules/tests/browser/browser_web_channel.js
new file mode 100644
index 0000000000..9dfa59485b
--- /dev/null
+++ b/toolkit/modules/tests/browser/browser_web_channel.js
@@ -0,0 +1,587 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+ChromeUtils.defineESModuleGetters(this, {
+ WebChannel: "resource://gre/modules/WebChannel.sys.mjs",
+});
+
+const HTTP_PATH = "http://example.com";
+const HTTP_ENDPOINT =
+ getRootDirectory(gTestPath).replace("chrome://mochitests/content", "") +
+ "file_web_channel.html";
+const HTTP_MISMATCH_PATH = "http://example.org";
+const HTTP_IFRAME_PATH = "http://mochi.test:8888";
+const HTTP_REDIRECTED_IFRAME_PATH = "http://example.org";
+
+requestLongerTimeout(2); // timeouts in debug builds.
+
+// Keep this synced with /mobile/android/tests/browser/robocop/testWebChannel.js
+// as much as possible. (We only have that since we can't run browser chrome
+// tests on Android. Yet?)
+var gTests = [
+ {
+ desc: "WebChannel generic message",
+ run() {
+ return new Promise(function (resolve, reject) {
+ let tab;
+ let channel = new WebChannel("generic", Services.io.newURI(HTTP_PATH));
+ channel.listen(function (id, message, target) {
+ is(id, "generic");
+ is(message.something.nested, "hello");
+ channel.stopListening();
+ gBrowser.removeTab(tab);
+ resolve();
+ });
+
+ tab = BrowserTestUtils.addTab(
+ gBrowser,
+ HTTP_PATH + HTTP_ENDPOINT + "?generic"
+ );
+ });
+ },
+ },
+ {
+ desc: "WebChannel generic message in a private window.",
+ async run() {
+ let promiseTestDone = new Promise(function (resolve, reject) {
+ let channel = new WebChannel("generic", Services.io.newURI(HTTP_PATH));
+ channel.listen(function (id, message, target) {
+ is(id, "generic");
+ is(message.something.nested, "hello");
+ channel.stopListening();
+ resolve();
+ });
+ });
+
+ const url = HTTP_PATH + HTTP_ENDPOINT + "?generic";
+ let privateWindow = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ await BrowserTestUtils.openNewForegroundTab(privateWindow.gBrowser, url);
+ await promiseTestDone;
+ await BrowserTestUtils.closeWindow(privateWindow);
+ },
+ },
+ {
+ desc: "WebChannel two way communication",
+ run() {
+ return new Promise(function (resolve, reject) {
+ let tab;
+ let channel = new WebChannel("twoway", Services.io.newURI(HTTP_PATH));
+
+ channel.listen(function (id, message, sender) {
+ is(id, "twoway", "bad id");
+ ok(message.command, "command not ok");
+
+ if (message.command === "one") {
+ channel.send({ data: { nested: true } }, sender);
+ }
+
+ if (message.command === "two") {
+ is(message.detail.data.nested, true);
+ channel.stopListening();
+ gBrowser.removeTab(tab);
+ resolve();
+ }
+ });
+
+ tab = BrowserTestUtils.addTab(
+ gBrowser,
+ HTTP_PATH + HTTP_ENDPOINT + "?twoway"
+ );
+ });
+ },
+ },
+ {
+ desc: "WebChannel two way communication in an iframe",
+ async run() {
+ let parentChannel = new WebChannel("echo", Services.io.newURI(HTTP_PATH));
+ let iframeChannel = new WebChannel(
+ "twoway",
+ Services.io.newURI(HTTP_IFRAME_PATH)
+ );
+ let promiseTestDone = new Promise(function (resolve, reject) {
+ parentChannel.listen(function (id, message, sender) {
+ reject(new Error("WebChannel message incorrectly sent to parent"));
+ });
+
+ iframeChannel.listen(function (id, message, sender) {
+ is(id, "twoway", "bad id (2)");
+ ok(message.command, "command not ok (2)");
+
+ if (message.command === "one") {
+ iframeChannel.send({ data: { nested: true } }, sender);
+ }
+
+ if (message.command === "two") {
+ is(message.detail.data.nested, true);
+ resolve();
+ }
+ });
+ });
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: HTTP_PATH + HTTP_ENDPOINT + "?iframe",
+ },
+ async function () {
+ await promiseTestDone;
+ parentChannel.stopListening();
+ iframeChannel.stopListening();
+ }
+ );
+ },
+ },
+ {
+ desc: "WebChannel response to a redirected iframe",
+ async run() {
+ /**
+ * This test checks that WebChannel responses are only sent
+ * to an iframe if the iframe has not redirected to another origin.
+ * Test flow:
+ * 1. create a page, embed an iframe on origin A.
+ * 2. the iframe sends a message `redirecting`, then redirects to
+ * origin B.
+ * 3. the iframe at origin B is set up to echo any messages back to the
+ * test parent.
+ * 4. the test parent receives the `redirecting` message from origin A.
+ * the test parent creates a new channel with origin B.
+ * 5. when origin B is ready, it sends a `loaded` message to the test
+ * parent, letting the test parent know origin B is ready to echo
+ * messages.
+ * 5. the test parent tries to send a response to origin A. If the
+ * WebChannel does not perform a valid origin check, the response
+ * will be received by origin B. If the WebChannel does perform
+ * a valid origin check, the response will not be sent.
+ * 6. the test parent sends a `done` message to origin B, which origin
+ * B echoes back. If the response to origin A is not echoed but
+ * the message to origin B is, then hooray, the test passes.
+ */
+
+ let preRedirectChannel = new WebChannel(
+ "pre_redirect",
+ Services.io.newURI(HTTP_IFRAME_PATH)
+ );
+ let postRedirectChannel = new WebChannel(
+ "post_redirect",
+ Services.io.newURI(HTTP_REDIRECTED_IFRAME_PATH)
+ );
+
+ let promiseTestDone = new Promise(function (resolve, reject) {
+ preRedirectChannel.listen(function (id, message, preRedirectSender) {
+ if (message.command === "redirecting") {
+ postRedirectChannel.listen(function (
+ aId,
+ aMessage,
+ aPostRedirectSender
+ ) {
+ is(aId, "post_redirect");
+ isnot(aMessage.command, "no_response_expected");
+
+ if (aMessage.command === "loaded") {
+ // The message should not be received on the preRedirectChannel
+ // because the target window has redirected.
+ preRedirectChannel.send(
+ { command: "no_response_expected" },
+ preRedirectSender
+ );
+ postRedirectChannel.send(
+ { command: "done" },
+ aPostRedirectSender
+ );
+ } else if (aMessage.command === "done") {
+ resolve();
+ } else {
+ reject(new Error(`Unexpected command ${aMessage.command}`));
+ }
+ });
+ } else {
+ reject(new Error(`Unexpected command ${message.command}`));
+ }
+ });
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: HTTP_PATH + HTTP_ENDPOINT + "?iframe_pre_redirect",
+ },
+ async function () {
+ await promiseTestDone;
+ preRedirectChannel.stopListening();
+ postRedirectChannel.stopListening();
+ }
+ );
+ },
+ },
+ {
+ desc: "WebChannel multichannel",
+ run() {
+ return new Promise(function (resolve, reject) {
+ let tab;
+ let channel = new WebChannel(
+ "multichannel",
+ Services.io.newURI(HTTP_PATH)
+ );
+
+ channel.listen(function (id, message, sender) {
+ is(id, "multichannel");
+ gBrowser.removeTab(tab);
+ resolve();
+ });
+
+ tab = BrowserTestUtils.addTab(
+ gBrowser,
+ HTTP_PATH + HTTP_ENDPOINT + "?multichannel"
+ );
+ });
+ },
+ },
+ {
+ desc: "WebChannel unsolicited send, using system principal",
+ async run() {
+ let channel = new WebChannel("echo", Services.io.newURI(HTTP_PATH));
+
+ // an unsolicted message is sent from Chrome->Content which is then
+ // echoed back. If the echo is received here, then the content
+ // received the message.
+ let messagePromise = new Promise(function (resolve, reject) {
+ channel.listen(function (id, message, sender) {
+ is(id, "echo");
+ is(message.command, "unsolicited");
+
+ resolve();
+ });
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: HTTP_PATH + HTTP_ENDPOINT + "?unsolicited",
+ },
+ async function (targetBrowser) {
+ channel.send(
+ { command: "unsolicited" },
+ {
+ browsingContext: targetBrowser.browsingContext,
+ principal: Services.scriptSecurityManager.getSystemPrincipal(),
+ }
+ );
+ await messagePromise;
+ channel.stopListening();
+ }
+ );
+ },
+ },
+ {
+ desc: "WebChannel unsolicited send, using target origin's principal",
+ async run() {
+ let targetURI = Services.io.newURI(HTTP_PATH);
+ let channel = new WebChannel("echo", targetURI);
+
+ // an unsolicted message is sent from Chrome->Content which is then
+ // echoed back. If the echo is received here, then the content
+ // received the message.
+ let messagePromise = new Promise(function (resolve, reject) {
+ channel.listen(function (id, message, sender) {
+ is(id, "echo");
+ is(message.command, "unsolicited");
+
+ resolve();
+ });
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: HTTP_PATH + HTTP_ENDPOINT + "?unsolicited",
+ },
+ async function (targetBrowser) {
+ channel.send(
+ { command: "unsolicited" },
+ {
+ browsingContext: targetBrowser.browsingContext,
+ principal: Services.scriptSecurityManager.createContentPrincipal(
+ targetURI,
+ {}
+ ),
+ }
+ );
+
+ await messagePromise;
+ channel.stopListening();
+ }
+ );
+ },
+ },
+ {
+ desc: "WebChannel unsolicited send with principal mismatch",
+ async run() {
+ let targetURI = Services.io.newURI(HTTP_PATH);
+ let channel = new WebChannel("echo", targetURI);
+
+ // two unsolicited messages are sent from Chrome->Content. The first,
+ // `unsolicited_no_response_expected` is sent to the wrong principal
+ // and should not be echoed back. The second, `done`, is sent to the
+ // correct principal and should be echoed back.
+ let messagePromise = new Promise(function (resolve, reject) {
+ channel.listen(function (id, message, sender) {
+ is(id, "echo");
+
+ if (message.command === "done") {
+ resolve();
+ } else {
+ reject(new Error(`Unexpected command ${message.command}`));
+ }
+ });
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: HTTP_PATH + HTTP_ENDPOINT + "?unsolicited",
+ },
+ async function (targetBrowser) {
+ let mismatchURI = Services.io.newURI(HTTP_MISMATCH_PATH);
+ let mismatchPrincipal =
+ Services.scriptSecurityManager.createContentPrincipal(
+ mismatchURI,
+ {}
+ );
+
+ // send a message to the wrong principal. It should not be delivered
+ // to content, and should not be echoed back.
+ channel.send(
+ { command: "unsolicited_no_response_expected" },
+ {
+ browsingContext: targetBrowser.browsingContext,
+ principal: mismatchPrincipal,
+ }
+ );
+
+ let targetPrincipal =
+ Services.scriptSecurityManager.createContentPrincipal(
+ targetURI,
+ {}
+ );
+
+ // send the `done` message to the correct principal. It
+ // should be echoed back.
+ channel.send(
+ { command: "done" },
+ {
+ browsingContext: targetBrowser.browsingContext,
+ principal: targetPrincipal,
+ }
+ );
+
+ await messagePromise;
+ channel.stopListening();
+ }
+ );
+ },
+ },
+ {
+ desc: "WebChannel non-window target",
+ async run() {
+ /**
+ * This test ensures messages can be received from and responses
+ * sent to non-window elements.
+ *
+ * First wait for the non-window element to send a "start" message.
+ * Then send the non-window element a "done" message.
+ * The non-window element will echo the "done" message back, if it
+ * receives the message.
+ * Listen for the response. If received, good to go!
+ */
+ let channel = new WebChannel(
+ "not_a_window",
+ Services.io.newURI(HTTP_PATH)
+ );
+
+ let testDonePromise = new Promise(function (resolve, reject) {
+ channel.listen(function (id, message, sender) {
+ if (message.command === "start") {
+ channel.send({ command: "done" }, sender);
+ } else if (message.command === "done") {
+ resolve();
+ } else {
+ reject(new Error(`Unexpected command ${message.command}`));
+ }
+ });
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: HTTP_PATH + HTTP_ENDPOINT + "?bubbles",
+ },
+ async function () {
+ await testDonePromise;
+ channel.stopListening();
+ }
+ );
+ },
+ },
+ {
+ desc: "WebChannel disallows non-string message from non-whitelisted origin",
+ async run() {
+ /**
+ * This test ensures that non-string messages can't be sent via WebChannels.
+ * We create a page (on a non-whitelisted origin) which should send us two
+ * messages immediately. The first message has an object for it's detail,
+ * and the second has a string. We check that we only get the second
+ * message.
+ */
+ let channel = new WebChannel("objects", Services.io.newURI(HTTP_PATH));
+ let testDonePromise = new Promise((resolve, reject) => {
+ channel.listen((id, message, sender) => {
+ is(id, "objects");
+ is(message.type, "string");
+ resolve();
+ });
+ });
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: HTTP_PATH + HTTP_ENDPOINT + "?object",
+ },
+ async function () {
+ await testDonePromise;
+ channel.stopListening();
+ }
+ );
+ },
+ },
+ {
+ desc: "WebChannel allows both string and non-string message from whitelisted origin",
+ async run() {
+ /**
+ * Same process as above, but we whitelist the origin before loading the page,
+ * and expect to get *both* messages back (each exactly once).
+ */
+ let channel = new WebChannel("objects", Services.io.newURI(HTTP_PATH));
+
+ let testDonePromise = new Promise((resolve, reject) => {
+ let sawObject = false;
+ let sawString = false;
+ channel.listen((id, message, sender) => {
+ is(id, "objects");
+ if (message.type === "object") {
+ ok(!sawObject);
+ sawObject = true;
+ } else if (message.type === "string") {
+ ok(!sawString);
+ sawString = true;
+ } else {
+ reject(new Error(`Unknown message type: ${message.type}`));
+ }
+ if (sawObject && sawString) {
+ resolve();
+ }
+ });
+ });
+ const webchannelWhitelistPref = "webchannel.allowObject.urlWhitelist";
+ let origWhitelist = Services.prefs.getCharPref(webchannelWhitelistPref);
+ let newWhitelist = origWhitelist + " " + HTTP_PATH;
+ Services.prefs.setCharPref(webchannelWhitelistPref, newWhitelist);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: HTTP_PATH + HTTP_ENDPOINT + "?object",
+ },
+ async function () {
+ await testDonePromise;
+ Services.prefs.setCharPref(webchannelWhitelistPref, origWhitelist);
+ channel.stopListening();
+ }
+ );
+ },
+ },
+ {
+ desc: "WebChannel errors handling the message are delivered back to content",
+ async run() {
+ const ERRNO_UNKNOWN_ERROR = 999; // WebChannel.sys.mjs doesn't export this.
+
+ // The channel where we purposely fail responding to a command.
+ let channel = new WebChannel("error", Services.io.newURI(HTTP_PATH));
+ // The channel where we see the response when the content sees the error
+ let echoChannel = new WebChannel("echo", Services.io.newURI(HTTP_PATH));
+
+ let testDonePromise = new Promise((resolve, reject) => {
+ // listen for the confirmation that content saw the error.
+ echoChannel.listen((id, message, sender) => {
+ is(id, "echo");
+ is(message.error, "oh no");
+ is(message.errno, ERRNO_UNKNOWN_ERROR);
+ resolve();
+ });
+
+ // listen for a message telling us to simulate an error.
+ channel.listen((id, message, sender) => {
+ is(id, "error");
+ is(message.command, "oops");
+ throw new Error("oh no");
+ });
+ });
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: HTTP_PATH + HTTP_ENDPOINT + "?error_thrown",
+ },
+ async function () {
+ await testDonePromise;
+ channel.stopListening();
+ echoChannel.stopListening();
+ }
+ );
+ },
+ },
+ {
+ desc: "WebChannel errors due to an invalid channel are delivered back to content",
+ async run() {
+ const ERRNO_NO_SUCH_CHANNEL = 2; // WebChannel.sys.mjs doesn't export this.
+ // The channel where we see the response when the content sees the error
+ let echoChannel = new WebChannel("echo", Services.io.newURI(HTTP_PATH));
+
+ let testDonePromise = new Promise((resolve, reject) => {
+ // listen for the confirmation that content saw the error.
+ echoChannel.listen((id, message, sender) => {
+ is(id, "echo");
+ is(message.error, "No Such Channel");
+ is(message.errno, ERRNO_NO_SUCH_CHANNEL);
+ resolve();
+ });
+ });
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: HTTP_PATH + HTTP_ENDPOINT + "?error_invalid_channel",
+ },
+ async function () {
+ await testDonePromise;
+ echoChannel.stopListening();
+ }
+ );
+ },
+ },
+]; // gTests
+
+function test() {
+ waitForExplicitFinish();
+
+ (async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.security.https_first_pbm", false]],
+ });
+
+ for (let testCase of gTests) {
+ info("Running: " + testCase.desc);
+ await testCase.run();
+ }
+ })().then(finish, ex => {
+ ok(false, "Unexpected Exception: " + ex);
+ finish();
+ });
+}
diff --git a/toolkit/modules/tests/browser/file_FinderIframeTest.html b/toolkit/modules/tests/browser/file_FinderIframeTest.html
new file mode 100644
index 0000000000..826e5dc4ca
--- /dev/null
+++ b/toolkit/modules/tests/browser/file_FinderIframeTest.html
@@ -0,0 +1,21 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+<head>
+ <title>Test (i)frame offsets, bug 1366646</title>
+</head>
+<body>
+<p>top level frame</p>
+<iframe src="data:text/html,&lt;p&gt;frame without border&lt;/p&gt;
+ &lt;iframe src='data:text/html,&lt;p&gt;nested frame without border&lt;/p&gt;' height='50' width='100%' style='background-color: yellow; border: 0'&gt;&lt;/iframe&gt;
+ " style="background-color: pink; border: 0" width="100%" height="150"></iframe>
+<iframe src="data:text/html,&lt;p&gt;frame with 5px border&lt;/p&gt;
+ &lt;iframe src='data:text/html,&lt;p&gt;nested frame with 5px border&lt;/p&gt;' height='50' width='100%' style='background-color: yellow; border: solid 5px black'&gt;&lt;/iframe&gt;
+ " style="background-color: pink; border: solid 5px black" width="100%" height="150"></iframe>
+<iframe src="data:text/html,&lt;p&gt;frame with 5px padding&lt;/p&gt;
+ &lt;iframe src='data:text/html,&lt;p&gt;nested frame with 5px padding&lt;/p&gt;' height='50' width='100%' style='background-color: yellow; border: 0; padding: 5px'&gt;&lt;/iframe&gt;
+ " style="background-color: pink; border: 0; padding: 5px" width="100%" height="150"></iframe>
+<!-- Testing deprecated HTML4 iframe properties too: -->
+<iframe src="data:text/html,&lt;p&gt;frame with frameborder, marginwidth/ height and 5px padding&lt;/p&gt;
+ &lt;iframe src='data:text/html,&lt;p&gt;nested frame with frameborder, marginwidth/ height&lt;/p&gt;' height='50' width='100%' frameborder='1' marginheight='5' marginwidth='5' style='background-color: yellow;'&gt;&lt;/iframe&gt;
+ " frameborder="1" marginheight="5" marginwidth="5" style="background-color: pink; padding: 5px" width="100%" height="150"></iframe>
+</body></html>
diff --git a/toolkit/modules/tests/browser/file_FinderSample.html b/toolkit/modules/tests/browser/file_FinderSample.html
new file mode 100644
index 0000000000..e952d1fe97
--- /dev/null
+++ b/toolkit/modules/tests/browser/file_FinderSample.html
@@ -0,0 +1,824 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Childe Roland</title>
+</head>
+<body>
+<h1>"Childe Roland to the Dark Tower Came"</h1><h5>Robert Browning</h5>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>I.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>My first thought was, he lied in every word,
+<dl>
+<dd>That hoary cripple, with malicious eye</dd>
+<dd>Askance to watch the working of his lie</dd>
+</dl>
+</dd>
+<dd>On mine, and mouth scarce able to afford</dd>
+<dd>Suppression of the glee that pursed and scored
+<dl>
+<dd>Its edge, at one more victim gained thereby.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>II.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>What else should he be set for, with his staff?
+<dl>
+<dd>What, save to waylay with his lies, ensnare</dd>
+<dd>All travellers who might find him posted there,</dd>
+</dl>
+</dd>
+<dd>And ask the road? I guessed what skull-like laugh</dd>
+<dd>Would break, what crutch 'gin write my epitaph
+<dl>
+<dd>For pastime in the dusty thoroughfare,</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>III.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>If at his counsel I should turn aside
+<dl>
+<dd>Into that ominous tract which, all agree,</dd>
+<dd>Hides the Dark Tower. Yet acquiescingly</dd>
+</dl>
+</dd>
+<dd>I did turn as he pointed: neither pride</dd>
+<dd>Nor hope rekindling at the end descried,
+<dl>
+<dd>So much as gladness that some end might be.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>IV.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>For, what with my whole world-wide wandering,
+<dl>
+<dd>What with my search drawn out thro' years, my hope</dd>
+<dd>Dwindled into a ghost not fit to cope</dd>
+</dl>
+</dd>
+<dd>With that obstreperous joy success would bring,</dd>
+<dd>I hardly tried now to rebuke the spring
+<dl>
+<dd>My heart made, finding failure in its scope.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>V.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>As when a sick man very near to death
+<dl>
+<dd>Seems dead indeed, and feels begin and end</dd>
+<dd>The tears and takes the farewell of each friend,</dd>
+</dl>
+</dd>
+<dd>And hears one bid the other go, draw breath</dd>
+<dd>Freelier outside ("since all is o'er," he saith,
+<dl>
+<dd>"And the blow fallen no grieving can amend;")</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>VI.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>While some discuss if near the other graves
+<dl>
+<dd>Be room enough for this, and when a day</dd>
+<dd>Suits best for carrying the corpse away,</dd>
+</dl>
+</dd>
+<dd>With care about the banners, scarves and staves:</dd>
+<dd>And still the man hears all, and only craves
+<dl>
+<dd>He may not shame such tender love and stay.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>VII.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>Thus, I had so long suffered in this quest,
+<dl>
+<dd>Heard failure prophesied so oft, been writ</dd>
+<dd>So many times among "The Band" - to wit,</dd>
+</dl>
+</dd>
+<dd>The knights who to the Dark Tower's search addressed</dd>
+<dd>Their steps - that just to fail as they, seemed best,
+<dl>
+<dd>And all the doubt was now—should I be fit?</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>VIII.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>So, quiet as despair, I turned from him,
+<dl>
+<dd>That hateful cripple, out of his highway</dd>
+<dd>Into the path he pointed. All the day</dd>
+</dl>
+</dd>
+<dd>Had been a dreary one at best, and dim</dd>
+<dd>Was settling to its close, yet shot one grim
+<dl>
+<dd>Red leer to see the plain catch its estray.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>IX.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>For mark! no sooner was I fairly found
+<dl>
+<dd>Pledged to the plain, after a pace or two,</dd>
+<dd>Than, pausing to throw backward a last view</dd>
+</dl>
+</dd>
+<dd>O'er the safe road, 'twas gone; grey plain all round:</dd>
+<dd>Nothing but plain to the horizon's bound.
+<dl>
+<dd>I might go on; nought else remained to do.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>X.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>So, on I went. I think I never saw
+<dl>
+<dd>Such starved ignoble nature; nothing throve:</dd>
+<dd>For flowers - as well expect a cedar grove!</dd>
+</dl>
+</dd>
+<dd>But cockle, spurge, according to their law</dd>
+<dd>Might propagate their kind, with none to awe,
+<dl>
+<dd>You'd think; a burr had been a treasure trove.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XI.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>No! penury, inertness and grimace,
+<dl>
+<dd>In some strange sort, were the land's portion. "See</dd>
+<dd>Or shut your eyes," said Nature peevishly,</dd>
+</dl>
+</dd>
+<dd>"It nothing skills: I cannot help my case:</dd>
+<dd>'Tis the Last Judgment's fire must cure this place,
+<dl>
+<dd>Calcine its clods and set my prisoners free."</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XII.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>If there pushed any ragged thistle-stalk
+<dl>
+<dd>Above its mates, the head was chopped; the bents</dd>
+<dd>Were jealous else. What made those holes and rents</dd>
+</dl>
+</dd>
+<dd>In the dock's harsh swarth leaves, bruised as to baulk</dd>
+<dd>All hope of greenness? 'tis a brute must walk
+<dl>
+<dd>Pashing their life out, with a brute's intents.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XIII.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>As for the grass, it grew as scant as hair
+<dl>
+<dd>In leprosy; thin dry blades pricked the mud</dd>
+<dd>Which underneath looked kneaded up with blood.</dd>
+</dl>
+</dd>
+<dd>One stiff blind horse, his every bone a-stare,</dd>
+<dd>Stood stupefied, however he came there:
+<dl>
+<dd>Thrust out past service from the devil's stud!</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XIV.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>Alive? he might be dead for aught I know,
+<dl>
+<dd>With that red gaunt and colloped neck a-strain,</dd>
+<dd>And shut eyes underneath the rusty mane;</dd>
+</dl>
+</dd>
+<dd>Seldom went such grotesqueness with such woe;</dd>
+<dd>I never saw a brute I hated so;
+<dl>
+<dd>He must be wicked to deserve such pain.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XV.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>I shut my eyes and turned them on my heart.
+<dl>
+<dd>As a man calls for wine before he fights,</dd>
+<dd>I asked one draught of earlier, happier sights,</dd>
+</dl>
+</dd>
+<dd>Ere fitly I could hope to play my part.</dd>
+<dd>Think first, fight afterwards - the soldier's art:
+<dl>
+<dd>One taste of the old time sets all to rights.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XVI.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>Not it! I fancied Cuthbert's reddening face
+<dl>
+<dd>Beneath its garniture of curly gold,</dd>
+<dd>Dear fellow, till I almost felt him fold</dd>
+</dl>
+</dd>
+<dd>An arm in mine to fix me to the place</dd>
+<dd>That way he used. Alas, one night's disgrace!
+<dl>
+<dd>Out went my heart's new fire and left it cold.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XVII.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>Giles then, the soul of honour - there he stands
+<dl>
+<dd>Frank as ten years ago when knighted first.</dd>
+<dd>What honest men should dare (he said) he durst.</dd>
+</dl>
+</dd>
+<dd>Good - but the scene shifts - faugh! what hangman hands</dd>
+<dd>Pin to his breast a parchment? His own bands
+<dl>
+<dd>Read it. Poor traitor, spit upon and curst!</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XVIII.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>Better this present than a past like that;
+<dl>
+<dd>Back therefore to my darkening path again!</dd>
+<dd>No sound, no sight as far as eye could strain.</dd>
+</dl>
+</dd>
+<dd>Will the night send a howlet or a bat?</dd>
+<dd>I asked: when something on the dismal flat
+<dl>
+<dd>Came to arrest my thoughts and change their train.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XIX.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>A sudden little river crossed my path
+<dl>
+<dd>As unexpected as a serpent comes.</dd>
+<dd>No sluggish tide congenial to the glooms;</dd>
+</dl>
+</dd>
+<dd>This, as it frothed by, might have been a bath</dd>
+<dd>For the fiend's glowing hoof - to see the wrath
+<dl>
+<dd>Of its black eddy bespate with flakes and spumes.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XX.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>So petty yet so spiteful! All along
+<dl>
+<dd>Low scrubby alders kneeled down over it;</dd>
+<dd>Drenched willows flung them headlong in a fit</dd>
+</dl>
+</dd>
+<dd>Of mute despair, a suicidal throng:</dd>
+<dd>The river which had done them all the wrong,
+<dl>
+<dd>Whate'er that was, rolled by, deterred no whit.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XXI.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>Which, while I forded, - good saints, how I feared
+<dl>
+<dd>To set my foot upon a dead man's cheek,</dd>
+<dd>Each step, or feel the spear I thrust to seek</dd>
+</dl>
+</dd>
+<dd>For hollows, tangled in his hair or beard!</dd>
+<dd>—It may have been a water-rat I speared,
+<dl>
+<dd>But, ugh! it sounded like a baby's shriek.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XXII.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>Glad was I when I reached the other bank.
+<dl>
+<dd>Now for a better country. Vain presage!</dd>
+<dd>Who were the strugglers, what war did they wage,</dd>
+</dl>
+</dd>
+<dd>Whose savage trample thus could pad the dank</dd>
+<dd>Soil to a plash? Toads in a poisoned tank,
+<dl>
+<dd>Or wild cats in a red-hot iron cage—</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XXIII.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>The fight must so have seemed in that fell cirque.
+<dl>
+<dd>What penned them there, with all the plain to choose?</dd>
+<dd>No foot-print leading to that horrid mews,</dd>
+</dl>
+</dd>
+<dd>None out of it. Mad brewage set to work</dd>
+<dd>Their brains, no doubt, like galley-slaves the Turk
+<dl>
+<dd>Pits for his pastime, Christians against Jews.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XXIV.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>And more than that - a furlong on - why, there!
+<dl>
+<dd>What bad use was that engine for, that wheel,</dd>
+<dd>Or brake, not wheel - that harrow fit to reel</dd>
+</dl>
+</dd>
+<dd>Men's bodies out like silk? with all the air</dd>
+<dd>Of Tophet's tool, on earth left unaware,
+<dl>
+<dd>Or brought to sharpen its rusty teeth of steel.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XXV.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>Then came a bit of stubbed ground, once a wood,
+<dl>
+<dd>Next a marsh, it would seem, and now mere earth</dd>
+<dd>Desperate and done with; (so a fool finds mirth,</dd>
+</dl>
+</dd>
+<dd>Makes a thing and then mars it, till his mood</dd>
+<dd>Changes and off he goes!) within a rood—
+<dl>
+<dd>Bog, clay and rubble, sand and stark black dearth.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XXVI.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>Now blotches rankling, coloured gay and grim,
+<dl>
+<dd>Now patches where some leanness of the soil's</dd>
+<dd>Broke into moss or substances like boils;</dd>
+</dl>
+</dd>
+<dd>Then came some palsied oak, a cleft in him</dd>
+<dd>Like a distorted mouth that splits its rim
+<dl>
+<dd>Gaping at death, and dies while it recoils.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XXVII.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>And just as far as ever from the end!
+<dl>
+<dd>Nought in the distance but the evening, nought</dd>
+<dd>To point my footstep further! At the thought,</dd>
+</dl>
+</dd>
+<dd>A great black bird, Apollyon's bosom-friend,</dd>
+<dd>Sailed past, nor beat his wide wing dragon-penned
+<dl>
+<dd>That brushed my cap—perchance the guide I sought.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XXVIII.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>For, looking up, aware I somehow grew,
+<dl>
+<dd>'Spite of the dusk, the plain had given place</dd>
+<dd>All round to mountains - with such name to grace</dd>
+</dl>
+</dd>
+<dd>Mere ugly heights and heaps now stolen in view.</dd>
+<dd>How thus they had surprised me, - solve it, you!
+<dl>
+<dd>How to get from them was no clearer case.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XXIX.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>Yet half I seemed to recognise some trick
+<dl>
+<dd>Of mischief happened to me, God knows when—</dd>
+<dd>In a bad dream perhaps. Here ended, then,</dd>
+</dl>
+</dd>
+<dd>Progress this way. When, in the very nick</dd>
+<dd>Of giving up, one time more, came a click
+<dl>
+<dd>As when a trap shuts - you're inside the den!</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XXX.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>Burningly it came on me all at once,
+<dl>
+<dd>This was the place! those two hills on the right,</dd>
+<dd>Crouched like two bulls locked horn in horn in fight;</dd>
+</dl>
+</dd>
+<dd>While to the left, a tall scalped mountain... Dunce,</dd>
+<dd>Dotard, a-dozing at the very nonce,
+<dl>
+<dd>After a life spent training for the sight!</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XXXI.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>What in the midst lay but the Tower itself?
+<dl>
+<dd>The round squat turret, blind as the fool's heart</dd>
+<dd>Built of brown stone, without a counterpart</dd>
+</dl>
+</dd>
+<dd>In the whole world. The tempest's mocking elf</dd>
+<dd>Points to the shipman thus the unseen shelf
+<dl>
+<dd>He strikes on, only when the timbers start.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XXXII.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>Not see? because of night perhaps? - why, day
+<dl>
+<dd>Came back again for that! before it left,</dd>
+<dd>The dying sunset kindled through a cleft:</dd>
+</dl>
+</dd>
+<dd>The hills, like giants at a hunting, lay</dd>
+<dd>Chin upon hand, to see the game at bay,—
+<dl>
+<dd>"Now stab and end the creature - to the heft!"</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XXXIII.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>Not hear? when noise was everywhere! it tolled
+<dl>
+<dd>Increasing like a bell. Names in my ears</dd>
+<dd>Of all the lost adventurers my peers,—</dd>
+</dl>
+</dd>
+<dd>How such a one was strong, and such was bold,</dd>
+<dd>And such was fortunate, yet each of old
+<dl>
+<dd>Lost, lost! one moment knelled the woe of years.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XXXIV.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>There they stood, ranged along the hillsides, met
+<dl>
+<dd>To view the last of me, a living frame</dd>
+<dd>For one more picture! in a sheet of flame</dd>
+</dl>
+</dd>
+<dd>I saw them and I knew them all. And yet</dd>
+<dd>Dauntless the slug-horn to my lips I set,
+<dl>
+<dd>And blew "<i>Childe Roland to the Dark Tower came.</i>"</dd>
+</dl>
+</dd>
+</dl>
+</body>
+</html>
diff --git a/toolkit/modules/tests/browser/file_getSelectionDetails_inputs.html b/toolkit/modules/tests/browser/file_getSelectionDetails_inputs.html
new file mode 100644
index 0000000000..2e49146785
--- /dev/null
+++ b/toolkit/modules/tests/browser/file_getSelectionDetails_inputs.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <input id="url-no-scheme" value="test.example.com" >
+ <input id="url-with-scheme" value="https://test.example.com">
+ <input id="not-url" value="foo. bar">
+ <input id="not-url-number" value="3.5">
+ </body>
+</html>
diff --git a/toolkit/modules/tests/browser/file_web_channel.html b/toolkit/modules/tests/browser/file_web_channel.html
new file mode 100644
index 0000000000..bcc9a13da0
--- /dev/null
+++ b/toolkit/modules/tests/browser/file_web_channel.html
@@ -0,0 +1,235 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>web_channel_test</title>
+</head>
+<body>
+<script>
+ var IFRAME_SRC_ROOT =
+ "http://mochi.test:8888" +
+ location.pathname.replace("web_channel.html", "web_channel_iframe.html");
+
+ window.onload = function() {
+ var testName = window.location.search.replace(/^\?/, "");
+
+ switch (testName) {
+ case "generic":
+ test_generic();
+ break;
+ case "twoway":
+ test_twoWay();
+ break;
+ case "multichannel":
+ test_multichannel();
+ break;
+ case "iframe":
+ test_iframe();
+ break;
+ case "iframe_pre_redirect":
+ test_iframe_pre_redirect();
+ break;
+ case "unsolicited":
+ test_unsolicited();
+ break;
+ case "bubbles":
+ test_bubbles();
+ break;
+ case "object":
+ test_object();
+ break;
+ case "error_thrown":
+ test_error_thrown();
+ break;
+ case "error_invalid_channel":
+ test_error_invalid_channel();
+ break;
+ default:
+ throw new Error(`INVALID TEST NAME ${testName}`);
+ }
+ };
+
+ function test_generic() {
+ var event = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: JSON.stringify({
+ id: "generic",
+ message: {
+ something: {
+ nested: "hello",
+ },
+ },
+ }),
+ });
+
+ window.dispatchEvent(event);
+ }
+
+ function test_twoWay() {
+ var firstMessage = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: JSON.stringify({
+ id: "twoway",
+ message: {
+ command: "one",
+ },
+ }),
+ });
+
+ window.addEventListener("WebChannelMessageToContent", function(e) {
+ var secondMessage = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: JSON.stringify({
+ id: "twoway",
+ message: {
+ command: "two",
+ detail: e.detail.message,
+ },
+ }),
+ });
+
+ if (!e.detail.message.error) {
+ window.dispatchEvent(secondMessage);
+ }
+ }, true);
+
+ window.dispatchEvent(firstMessage);
+ }
+
+ function test_multichannel() {
+ var event1 = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: JSON.stringify({
+ id: "wrongchannel",
+ message: {},
+ }),
+ });
+
+ var event2 = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: JSON.stringify({
+ id: "multichannel",
+ message: {},
+ }),
+ });
+
+ window.dispatchEvent(event1);
+ window.dispatchEvent(event2);
+ }
+
+ function test_iframe() {
+ // Note that this message is the response to the message sent
+ // by the iframe! This is bad, as this page is *not* trusted.
+ window.addEventListener("WebChannelMessageToContent", function(e) {
+ // the test parent will fail if the echo message is received.
+ echoEventToChannel(e, "echo");
+ });
+
+ // only attach the iframe for the iframe test to avoid
+ // interfering with other tests.
+ var iframe = document.createElement("iframe");
+ iframe.setAttribute("src", IFRAME_SRC_ROOT + "?iframe");
+ document.body.appendChild(iframe);
+ }
+
+ function test_iframe_pre_redirect() {
+ var iframe = document.createElement("iframe");
+ iframe.setAttribute("src", IFRAME_SRC_ROOT + "?iframe_pre_redirect");
+ document.body.appendChild(iframe);
+ }
+
+ function test_unsolicited() {
+ // echo any unsolicted events back to chrome.
+ window.addEventListener("WebChannelMessageToContent", function(e) {
+ echoEventToChannel(e, "echo");
+ }, true);
+ }
+
+ function test_bubbles() {
+ var event = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: JSON.stringify({
+ id: "not_a_window",
+ message: {
+ command: "start",
+ },
+ }),
+ });
+
+ var nonWindowTarget = document.getElementById("not_a_window");
+
+ nonWindowTarget.addEventListener("WebChannelMessageToContent", function(e) {
+ echoEventToChannel(e, "not_a_window");
+ }, true);
+
+
+ nonWindowTarget.dispatchEvent(event);
+ }
+
+ function test_object() {
+ let objectMessage = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: {
+ id: "objects",
+ message: { type: "object" },
+ },
+ });
+
+ let stringMessage = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: JSON.stringify({
+ id: "objects",
+ message: { type: "string" },
+ }),
+ });
+ // Test fails if objectMessage is received, we send stringMessage to know
+ // when we should stop listening for objectMessage
+ window.dispatchEvent(objectMessage);
+ window.dispatchEvent(stringMessage);
+ }
+
+ function test_error_thrown() {
+ var event = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: JSON.stringify({
+ id: "error",
+ message: {
+ command: "oops",
+ },
+ }),
+ });
+
+ // echo the response back to chrome - chrome will check it is the
+ // expected error.
+ window.addEventListener("WebChannelMessageToContent", function(e) {
+ echoEventToChannel(e, "echo");
+ }, true);
+
+ window.dispatchEvent(event);
+ }
+
+ function test_error_invalid_channel() {
+ var event = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: JSON.stringify({
+ id: "invalid-channel",
+ message: {
+ command: "oops",
+ },
+ }),
+ });
+
+ // echo the response back to chrome - chrome will check it is the
+ // expected error.
+ window.addEventListener("WebChannelMessageToContent", function(e) {
+ echoEventToChannel(e, "echo");
+ }, true);
+
+ window.dispatchEvent(event);
+ }
+
+ function echoEventToChannel(e, channelId) {
+ var echoedEvent = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: JSON.stringify({
+ id: channelId,
+ message: e.detail.message,
+ }),
+ });
+
+ e.target.dispatchEvent(echoedEvent);
+ }
+</script>
+
+<div id="not_a_window"></div>
+</body>
+</html>
diff --git a/toolkit/modules/tests/browser/file_web_channel_iframe.html b/toolkit/modules/tests/browser/file_web_channel_iframe.html
new file mode 100644
index 0000000000..101512184a
--- /dev/null
+++ b/toolkit/modules/tests/browser/file_web_channel_iframe.html
@@ -0,0 +1,96 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>web_channel_test (iframe)</title>
+</head>
+<body>
+<script>
+ var REDIRECTED_IFRAME_SRC_ROOT = "http://example.org/" + location.pathname;
+
+ window.onload = function() {
+ var testName = window.location.search.replace(/^\?/, "");
+ switch (testName) {
+ case "iframe":
+ test_iframe();
+ break;
+ case "iframe_pre_redirect":
+ test_iframe_pre_redirect();
+ break;
+ case "iframe_post_redirect":
+ test_iframe_post_redirect();
+ break;
+ default:
+ throw new Error(`INVALID TEST NAME ${testName}`);
+ }
+ };
+
+ function test_iframe() {
+ var firstMessage = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: JSON.stringify({
+ id: "twoway",
+ message: {
+ command: "one",
+ },
+ }),
+ });
+
+ window.addEventListener("WebChannelMessageToContent", function(e) {
+ var secondMessage = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: JSON.stringify({
+ id: "twoway",
+ message: {
+ command: "two",
+ detail: e.detail.message,
+ },
+ }),
+ });
+
+ if (!e.detail.message.error) {
+ window.dispatchEvent(secondMessage);
+ }
+ }, true);
+
+ window.dispatchEvent(firstMessage);
+ }
+
+
+ function test_iframe_pre_redirect() {
+ var firstMessage = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: JSON.stringify({
+ id: "pre_redirect",
+ message: {
+ command: "redirecting",
+ },
+ }),
+ });
+ window.dispatchEvent(firstMessage);
+ document.location = REDIRECTED_IFRAME_SRC_ROOT + "?iframe_post_redirect";
+ }
+
+ function test_iframe_post_redirect() {
+ window.addEventListener("WebChannelMessageToContent", function(e) {
+ var echoMessage = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: JSON.stringify({
+ id: "post_redirect",
+ message: e.detail.message,
+ }),
+ });
+
+ window.dispatchEvent(echoMessage);
+ }, true);
+
+ // Let the test parent know the page has loaded and is ready to echo events
+ var loadedMessage = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: JSON.stringify({
+ id: "post_redirect",
+ message: {
+ command: "loaded",
+ },
+ }),
+ });
+ window.dispatchEvent(loadedMessage);
+ }
+</script>
+</body>
+</html>
diff --git a/toolkit/modules/tests/browser/head.js b/toolkit/modules/tests/browser/head.js
new file mode 100644
index 0000000000..7c3f75b106
--- /dev/null
+++ b/toolkit/modules/tests/browser/head.js
@@ -0,0 +1,251 @@
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ setTimeout: "resource://gre/modules/Timer.sys.mjs",
+});
+
+const kFixtureBaseURL =
+ "https://example.com/browser/toolkit/modules/tests/browser/";
+
+function removeDupes(list) {
+ let j = 0;
+ for (let i = 1; i < list.length; i++) {
+ if (list[i] != list[j]) {
+ j++;
+ if (i != j) {
+ list[j] = list[i];
+ }
+ }
+ }
+ list.length = j + 1;
+}
+
+function compareLists(list1, list2, kind) {
+ list1.sort();
+ removeDupes(list1);
+ list2.sort();
+ removeDupes(list2);
+ is(String(list1), String(list2), `${kind} URLs correct`);
+}
+
+async function promiseOpenFindbar(findbar) {
+ await gBrowser.getFindBar();
+ findbar.onFindCommand();
+ return gFindBar._startFindDeferred && gFindBar._startFindDeferred.promise;
+}
+
+function promiseFindResult(findbar, str = null) {
+ let highlightFinished = false;
+ let findFinished = false;
+ return new Promise(resolve => {
+ let listener = {
+ onFindResult({ searchString }) {
+ if (str !== null && str != searchString) {
+ return;
+ }
+ findFinished = true;
+ if (highlightFinished) {
+ findbar.browser.finder.removeResultListener(listener);
+ resolve();
+ }
+ },
+ onHighlightFinished() {
+ highlightFinished = true;
+ if (findFinished) {
+ findbar.browser.finder.removeResultListener(listener);
+ resolve();
+ }
+ },
+ onMatchesCountResult: () => {},
+ };
+ findbar.browser.finder.addResultListener(listener);
+ });
+}
+
+function promiseEnterStringIntoFindField(findbar, str) {
+ let promise = promiseFindResult(findbar, str);
+ for (let i = 0; i < str.length; i++) {
+ let event = new KeyboardEvent("keypress", {
+ bubbles: true,
+ cancelable: true,
+ view: null,
+ keyCode: 0,
+ charCode: str.charCodeAt(i),
+ });
+ findbar._findField.dispatchEvent(event);
+ }
+ return promise;
+}
+
+function promiseTestHighlighterOutput(
+ browser,
+ word,
+ expectedResult,
+ extraTest = () => {}
+) {
+ return SpecialPowers.spawn(
+ browser,
+ [{ word, expectedResult, extraTest: extraTest.toSource() }],
+ async function ({ word, expectedResult, extraTest }) {
+ return new Promise((resolve, reject) => {
+ let stubbed = {};
+ let callCounts = {
+ insertCalls: [],
+ removeCalls: [],
+ animationCalls: [],
+ };
+ let lastMaskNode, lastOutlineNode;
+ let rects = [];
+
+ // Amount of milliseconds to wait after the last time one of our stubs
+ // was called.
+ const kTimeoutMs = 1000;
+ // The initial timeout may wait for a while for results to come in.
+ let timeout = content.setTimeout(
+ () => finish(false, "Timeout"),
+ kTimeoutMs * 5
+ );
+
+ function finish(ok = true, message = "finished with error") {
+ // Restore the functions we stubbed out.
+ try {
+ content.document.insertAnonymousContent = stubbed.insert;
+ content.document.removeAnonymousContent = stubbed.remove;
+ } catch (ex) {}
+ stubbed = {};
+ content.clearTimeout(timeout);
+
+ if (expectedResult.rectCount !== 0) {
+ Assert.ok(ok, message);
+ }
+
+ Assert.greaterOrEqual(
+ callCounts.insertCalls.length,
+ expectedResult.insertCalls[0],
+ `Min. insert calls should match for '${word}'.`
+ );
+ Assert.lessOrEqual(
+ callCounts.insertCalls.length,
+ expectedResult.insertCalls[1],
+ `Max. insert calls should match for '${word}'.`
+ );
+ Assert.greaterOrEqual(
+ callCounts.removeCalls.length,
+ expectedResult.removeCalls[0],
+ `Min. remove calls should match for '${word}'.`
+ );
+ Assert.lessOrEqual(
+ callCounts.removeCalls.length,
+ expectedResult.removeCalls[1],
+ `Max. remove calls should match for '${word}'.`
+ );
+
+ // We reached the amount of calls we expected, so now we can check
+ // the amount of rects.
+ if (!lastMaskNode && expectedResult.rectCount !== 0) {
+ Assert.ok(
+ false,
+ `No mask node found, but expected ${expectedResult.rectCount} rects.`
+ );
+ }
+
+ Assert.equal(
+ rects.length,
+ expectedResult.rectCount,
+ `Amount of inserted rects should match for '${word}'.`
+ );
+
+ if ("animationCalls" in expectedResult) {
+ Assert.greaterOrEqual(
+ callCounts.animationCalls.length,
+ expectedResult.animationCalls[0],
+ `Min. animation calls should match for '${word}'.`
+ );
+ Assert.lessOrEqual(
+ callCounts.animationCalls.length,
+ expectedResult.animationCalls[1],
+ `Max. animation calls should match for '${word}'.`
+ );
+ }
+
+ // Allow more specific assertions to be tested in `extraTest`.
+ // eslint-disable-next-line no-eval
+ extraTest = eval(extraTest);
+ extraTest(lastMaskNode, lastOutlineNode, rects);
+
+ resolve();
+ }
+
+ function stubAnonymousContentNode(domNode, anonNode) {
+ let originals = [
+ anonNode.setTextContentForElement,
+ anonNode.setAttributeForElement,
+ anonNode.removeAttributeForElement,
+ anonNode.setCutoutRectsForElement,
+ anonNode.setAnimationForElement,
+ ];
+ anonNode.setTextContentForElement = (id, text) => {
+ try {
+ (domNode.querySelector("#" + id) || domNode).textContent = text;
+ } catch (ex) {}
+ return originals[0].call(anonNode, id, text);
+ };
+ anonNode.setAttributeForElement = (id, attrName, attrValue) => {
+ try {
+ (domNode.querySelector("#" + id) || domNode).setAttribute(
+ attrName,
+ attrValue
+ );
+ } catch (ex) {}
+ return originals[1].call(anonNode, id, attrName, attrValue);
+ };
+ anonNode.removeAttributeForElement = (id, attrName) => {
+ try {
+ let node = domNode.querySelector("#" + id) || domNode;
+ if (node.hasAttribute(attrName)) {
+ node.removeAttribute(attrName);
+ }
+ } catch (ex) {}
+ return originals[2].call(anonNode, id, attrName);
+ };
+ anonNode.setCutoutRectsForElement = (id, cutoutRects) => {
+ rects = cutoutRects;
+ return originals[3].call(anonNode, id, cutoutRects);
+ };
+ anonNode.setAnimationForElement = (id, keyframes, options) => {
+ callCounts.animationCalls.push([keyframes, options]);
+ return originals[4].call(anonNode, id, keyframes, options);
+ };
+ }
+
+ // Create a function that will stub the original version and collects
+ // the arguments so we can check the results later.
+ function stub(which) {
+ stubbed[which] = content.document[which + "AnonymousContent"];
+ let prop = which + "Calls";
+ return function (node) {
+ callCounts[prop].push(node);
+ if (which == "insert") {
+ if (node.outerHTML.indexOf("outlineMask") > -1) {
+ lastMaskNode = node;
+ } else {
+ lastOutlineNode = node;
+ }
+ }
+ content.clearTimeout(timeout);
+ timeout = content.setTimeout(() => {
+ finish();
+ }, kTimeoutMs);
+ let res = stubbed[which].call(content.document, node);
+ if (which == "insert") {
+ stubAnonymousContentNode(node, res);
+ }
+ return res;
+ };
+ }
+ content.document.insertAnonymousContent = stub("insert");
+ content.document.removeAnonymousContent = stub("remove");
+ });
+ }
+ );
+}