diff options
Diffstat (limited to '')
25 files changed, 4967 insertions, 0 deletions
diff --git a/toolkit/modules/tests/browser/browser.ini b/toolkit/modules/tests/browser/browser.ini new file mode 100644 index 0000000000..e9a77185da --- /dev/null +++ b/toolkit/modules/tests/browser/browser.ini @@ -0,0 +1,37 @@ +[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_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_FinderHighlighter.js] +skip-if = true # bug 1831518 covers re-enabling +# skip-if = debug || os == "linux" +[browser_FinderHighlighter2.js] +skip-if = true # bug 1831518 covers re-enabling +# skip-if = debug || os == "linux" +[browser_Geometry.js] +[browser_InlineSpellChecker.js] +[browser_Troubleshoot.js] +skip-if = os == "win" && os_version == "6.1" # bug 1715857 +[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..94396b1384 --- /dev/null +++ b/toolkit/modules/tests/browser/browser_Deprecated.js @@ -0,0 +1,131 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +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); + ok( + 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) { + ok( + 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); + ok( + 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); + ok( + aMessage.indexOf("deprecationFunctionCustomCallstack") > 0, + "Callstack is correctly logged." + ); + }, + // Set pref to true. + logWarnings: true, + }, +]; + +// Test Console Message attributes. +function testAMessage(aMessage) { + ok( + aMessage.indexOf("DEPRECATION WARNING: this method is deprecated.") === 0, + "Deprecation is correctly logged." + ); + ok(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..cc09888206 --- /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.loadURIString(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..eae19e3d59 --- /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); + ok(r.left == 5, "rect has correct left value"); + ok(r.top == 10, "rect has correct top value"); + ok(r.right == 105, "rect has correct right value"); + ok(r.bottom == 60, "rect has correct bottom value"); + ok(r.width == 100, "rect has correct width value"); + ok(r.height == 50, "rect has correct height value"); + ok(r.x == 5, "rect has correct x value"); + ok(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..751b1954e1 --- /dev/null +++ b/toolkit/modules/tests/browser/browser_Troubleshoot.js @@ -0,0 +1,1316 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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" + ); + } + )(); +}); + +// 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", + }, + }, + }, + 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", + }, + }, + }, + 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", + }, + }, + }, + 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, + type: "string", + }, + supportSandboxGpuLevel: { + required: AppConstants.MOZ_SANDBOX, + 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 }, + }, + }, + }, + }, + }, + }, +}; + +/** + * 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,<p>frame without border</p> + <iframe src='data:text/html,<p>nested frame without border</p>' height='50' width='100%' style='background-color: yellow; border: 0'></iframe> + " style="background-color: pink; border: 0" width="100%" height="150"></iframe> +<iframe src="data:text/html,<p>frame with 5px border</p> + <iframe src='data:text/html,<p>nested frame with 5px border</p>' height='50' width='100%' style='background-color: yellow; border: solid 5px black'></iframe> + " style="background-color: pink; border: solid 5px black" width="100%" height="150"></iframe> +<iframe src="data:text/html,<p>frame with 5px padding</p> + <iframe src='data:text/html,<p>nested frame with 5px padding</p>' height='50' width='100%' style='background-color: yellow; border: 0; padding: 5px'></iframe> + " style="background-color: pink; border: 0; padding: 5px" width="100%" height="150"></iframe> +<!-- Testing deprecated HTML4 iframe properties too: --> +<iframe src="data:text/html,<p>frame with frameborder, marginwidth/ height and 5px padding</p> + <iframe src='data:text/html,<p>nested frame with frameborder, marginwidth/ height</p>' height='50' width='100%' frameborder='1' marginheight='5' marginwidth='5' style='background-color: yellow;'></iframe> + " 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"); + }); + } + ); +} |