From 9e3c08db40b8916968b9f30096c7be3f00ce9647 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 21 Apr 2024 13:44:51 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- toolkit/modules/tests/browser/browser.ini | 37 + .../modules/tests/browser/browser_AsyncPrefs.js | 133 ++ .../modules/tests/browser/browser_BrowserUtils.js | 50 + .../modules/tests/browser/browser_CreditCard.js | 46 + .../modules/tests/browser/browser_Deprecated.js | 131 ++ toolkit/modules/tests/browser/browser_Finder.js | 73 ++ .../tests/browser/browser_FinderHighlighter.js | 415 ++++++ .../tests/browser/browser_FinderHighlighter2.js | 70 ++ .../browser/browser_Finder_hidden_textarea.js | 70 ++ .../tests/browser/browser_Finder_offscreen_text.js | 72 ++ .../browser/browser_Finder_overflowed_onscreen.js | 45 + .../browser/browser_Finder_overflowed_textarea.js | 75 ++ .../browser/browser_Finder_pointer_events_none.js | 39 + .../browser_Finder_skip_invisible_and_option.js | 132 ++ .../tests/browser/browser_Finder_vertical_text.js | 59 + toolkit/modules/tests/browser/browser_Geometry.js | 134 ++ .../tests/browser/browser_InlineSpellChecker.js | 47 + .../modules/tests/browser/browser_Troubleshoot.js | 1316 ++++++++++++++++++++ .../modules/tests/browser/browser_web_channel.js | 587 +++++++++ .../tests/browser/file_FinderIframeTest.html | 21 + .../modules/tests/browser/file_FinderSample.html | 824 ++++++++++++ .../browser/file_getSelectionDetails_inputs.html | 9 + .../modules/tests/browser/file_web_channel.html | 235 ++++ .../tests/browser/file_web_channel_iframe.html | 96 ++ toolkit/modules/tests/browser/head.js | 251 ++++ 25 files changed, 4967 insertions(+) create mode 100644 toolkit/modules/tests/browser/browser.ini create mode 100644 toolkit/modules/tests/browser/browser_AsyncPrefs.js create mode 100644 toolkit/modules/tests/browser/browser_BrowserUtils.js create mode 100644 toolkit/modules/tests/browser/browser_CreditCard.js create mode 100644 toolkit/modules/tests/browser/browser_Deprecated.js create mode 100644 toolkit/modules/tests/browser/browser_Finder.js create mode 100644 toolkit/modules/tests/browser/browser_FinderHighlighter.js create mode 100644 toolkit/modules/tests/browser/browser_FinderHighlighter2.js create mode 100644 toolkit/modules/tests/browser/browser_Finder_hidden_textarea.js create mode 100644 toolkit/modules/tests/browser/browser_Finder_offscreen_text.js create mode 100644 toolkit/modules/tests/browser/browser_Finder_overflowed_onscreen.js create mode 100644 toolkit/modules/tests/browser/browser_Finder_overflowed_textarea.js create mode 100644 toolkit/modules/tests/browser/browser_Finder_pointer_events_none.js create mode 100644 toolkit/modules/tests/browser/browser_Finder_skip_invisible_and_option.js create mode 100644 toolkit/modules/tests/browser/browser_Finder_vertical_text.js create mode 100644 toolkit/modules/tests/browser/browser_Geometry.js create mode 100644 toolkit/modules/tests/browser/browser_InlineSpellChecker.js create mode 100644 toolkit/modules/tests/browser/browser_Troubleshoot.js create mode 100644 toolkit/modules/tests/browser/browser_web_channel.js create mode 100644 toolkit/modules/tests/browser/file_FinderIframeTest.html create mode 100644 toolkit/modules/tests/browser/file_FinderSample.html create mode 100644 toolkit/modules/tests/browser/file_getSelectionDetails_inputs.html create mode 100644 toolkit/modules/tests/browser/file_web_channel.html create mode 100644 toolkit/modules/tests/browser/file_web_channel_iframe.html create mode 100644 toolkit/modules/tests/browser/head.js (limited to 'toolkit/modules/tests/browser') 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( + '' + + 'test link' + ); + 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(` + + Example + Error +`); + 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( + '
' + + "Here are a lot of words Please use find to highlight some words that wrap" + + " across a line boundary and see what happens.
" + ); + 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 = + ""; + 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 = ""; + for (let i = 0; i < linesToGenerate; i++) { + URI += i + "
"; + if (t < targetCount && linesToInsertTargetText[t] == i) { + URI += TARGET_TEXT; + t++; + } + } + URI += ""; + + 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 = + '
d



c----------------b



a
'; + 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 = ""; + + 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 = '
find this
'; + 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 = ` + +
+ a +
a
+
a
+ + + + `; + 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 = ` + + +
+ Some fallback text + + +

1234

+ + `; + 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 = + '
vertical-rl
vertical-lr
sideways-rl
sideways-lr
'; + 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 @@ + + + + Test (i)frame offsets, bug 1366646 + + +

top level frame

+ + + + + + 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 @@ + + + + Childe Roland + + +

"Childe Roland to the Dark Tower Came"

Robert Browning
+
+
+
+
+
+
I.
+
+
+
+
+
My first thought was, he lied in every word, +
+
That hoary cripple, with malicious eye
+
Askance to watch the working of his lie
+
+
+
On mine, and mouth scarce able to afford
+
Suppression of the glee that pursed and scored +
+
Its edge, at one more victim gained thereby.
+
+
+
+


+
+
+
+
+
+
II.
+
+
+
+
+
What else should he be set for, with his staff? +
+
What, save to waylay with his lies, ensnare
+
All travellers who might find him posted there,
+
+
+
And ask the road? I guessed what skull-like laugh
+
Would break, what crutch 'gin write my epitaph +
+
For pastime in the dusty thoroughfare,
+
+
+
+


+
+
+
+
+
+
III.
+
+
+
+
+
If at his counsel I should turn aside +
+
Into that ominous tract which, all agree,
+
Hides the Dark Tower. Yet acquiescingly
+
+
+
I did turn as he pointed: neither pride
+
Nor hope rekindling at the end descried, +
+
So much as gladness that some end might be.
+
+
+
+


+
+
+
+
+
+
IV.
+
+
+
+
+
For, what with my whole world-wide wandering, +
+
What with my search drawn out thro' years, my hope
+
Dwindled into a ghost not fit to cope
+
+
+
With that obstreperous joy success would bring,
+
I hardly tried now to rebuke the spring +
+
My heart made, finding failure in its scope.
+
+
+
+


+
+
+
+
+
+
V.
+
+
+
+
+
As when a sick man very near to death +
+
Seems dead indeed, and feels begin and end
+
The tears and takes the farewell of each friend,
+
+
+
And hears one bid the other go, draw breath
+
Freelier outside ("since all is o'er," he saith, +
+
"And the blow fallen no grieving can amend;")
+
+
+
+


+
+
+
+
+
+
VI.
+
+
+
+
+
While some discuss if near the other graves +
+
Be room enough for this, and when a day
+
Suits best for carrying the corpse away,
+
+
+
With care about the banners, scarves and staves:
+
And still the man hears all, and only craves +
+
He may not shame such tender love and stay.
+
+
+
+


+
+
+
+
+
+
VII.
+
+
+
+
+
Thus, I had so long suffered in this quest, +
+
Heard failure prophesied so oft, been writ
+
So many times among "The Band" - to wit,
+
+
+
The knights who to the Dark Tower's search addressed
+
Their steps - that just to fail as they, seemed best, +
+
And all the doubt was now—should I be fit?
+
+
+
+


+
+
+
+
+
+
VIII.
+
+
+
+
+
So, quiet as despair, I turned from him, +
+
That hateful cripple, out of his highway
+
Into the path he pointed. All the day
+
+
+
Had been a dreary one at best, and dim
+
Was settling to its close, yet shot one grim +
+
Red leer to see the plain catch its estray.
+
+
+
+


+
+
+
+
+
+
IX.
+
+
+
+
+
For mark! no sooner was I fairly found +
+
Pledged to the plain, after a pace or two,
+
Than, pausing to throw backward a last view
+
+
+
O'er the safe road, 'twas gone; grey plain all round:
+
Nothing but plain to the horizon's bound. +
+
I might go on; nought else remained to do.
+
+
+
+


+
+
+
+
+
+
X.
+
+
+
+
+
So, on I went. I think I never saw +
+
Such starved ignoble nature; nothing throve:
+
For flowers - as well expect a cedar grove!
+
+
+
But cockle, spurge, according to their law
+
Might propagate their kind, with none to awe, +
+
You'd think; a burr had been a treasure trove.
+
+
+
+


+
+
+
+
+
+
XI.
+
+
+
+
+
No! penury, inertness and grimace, +
+
In some strange sort, were the land's portion. "See
+
Or shut your eyes," said Nature peevishly,
+
+
+
"It nothing skills: I cannot help my case:
+
'Tis the Last Judgment's fire must cure this place, +
+
Calcine its clods and set my prisoners free."
+
+
+
+


+
+
+
+
+
+
XII.
+
+
+
+
+
If there pushed any ragged thistle-stalk +
+
Above its mates, the head was chopped; the bents
+
Were jealous else. What made those holes and rents
+
+
+
In the dock's harsh swarth leaves, bruised as to baulk
+
All hope of greenness? 'tis a brute must walk +
+
Pashing their life out, with a brute's intents.
+
+
+
+


+
+
+
+
+
+
XIII.
+
+
+
+
+
As for the grass, it grew as scant as hair +
+
In leprosy; thin dry blades pricked the mud
+
Which underneath looked kneaded up with blood.
+
+
+
One stiff blind horse, his every bone a-stare,
+
Stood stupefied, however he came there: +
+
Thrust out past service from the devil's stud!
+
+
+
+


+
+
+
+
+
+
XIV.
+
+
+
+
+
Alive? he might be dead for aught I know, +
+
With that red gaunt and colloped neck a-strain,
+
And shut eyes underneath the rusty mane;
+
+
+
Seldom went such grotesqueness with such woe;
+
I never saw a brute I hated so; +
+
He must be wicked to deserve such pain.
+
+
+
+


+
+
+
+
+
+
XV.
+
+
+
+
+
I shut my eyes and turned them on my heart. +
+
As a man calls for wine before he fights,
+
I asked one draught of earlier, happier sights,
+
+
+
Ere fitly I could hope to play my part.
+
Think first, fight afterwards - the soldier's art: +
+
One taste of the old time sets all to rights.
+
+
+
+


+
+
+
+
+
+
XVI.
+
+
+
+
+
Not it! I fancied Cuthbert's reddening face +
+
Beneath its garniture of curly gold,
+
Dear fellow, till I almost felt him fold
+
+
+
An arm in mine to fix me to the place
+
That way he used. Alas, one night's disgrace! +
+
Out went my heart's new fire and left it cold.
+
+
+
+


+
+
+
+
+
+
XVII.
+
+
+
+
+
Giles then, the soul of honour - there he stands +
+
Frank as ten years ago when knighted first.
+
What honest men should dare (he said) he durst.
+
+
+
Good - but the scene shifts - faugh! what hangman hands
+
Pin to his breast a parchment? His own bands +
+
Read it. Poor traitor, spit upon and curst!
+
+
+
+


+
+
+
+
+
+
XVIII.
+
+
+
+
+
Better this present than a past like that; +
+
Back therefore to my darkening path again!
+
No sound, no sight as far as eye could strain.
+
+
+
Will the night send a howlet or a bat?
+
I asked: when something on the dismal flat +
+
Came to arrest my thoughts and change their train.
+
+
+
+


+
+
+
+
+
+
XIX.
+
+
+
+
+
A sudden little river crossed my path +
+
As unexpected as a serpent comes.
+
No sluggish tide congenial to the glooms;
+
+
+
This, as it frothed by, might have been a bath
+
For the fiend's glowing hoof - to see the wrath +
+
Of its black eddy bespate with flakes and spumes.
+
+
+
+


+
+
+
+
+
+
XX.
+
+
+
+
+
So petty yet so spiteful! All along +
+
Low scrubby alders kneeled down over it;
+
Drenched willows flung them headlong in a fit
+
+
+
Of mute despair, a suicidal throng:
+
The river which had done them all the wrong, +
+
Whate'er that was, rolled by, deterred no whit.
+
+
+
+


+
+
+
+
+
+
XXI.
+
+
+
+
+
Which, while I forded, - good saints, how I feared +
+
To set my foot upon a dead man's cheek,
+
Each step, or feel the spear I thrust to seek
+
+
+
For hollows, tangled in his hair or beard!
+
—It may have been a water-rat I speared, +
+
But, ugh! it sounded like a baby's shriek.
+
+
+
+


+
+
+
+
+
+
XXII.
+
+
+
+
+
Glad was I when I reached the other bank. +
+
Now for a better country. Vain presage!
+
Who were the strugglers, what war did they wage,
+
+
+
Whose savage trample thus could pad the dank
+
Soil to a plash? Toads in a poisoned tank, +
+
Or wild cats in a red-hot iron cage—
+
+
+
+


+
+
+
+
+
+
XXIII.
+
+
+
+
+
The fight must so have seemed in that fell cirque. +
+
What penned them there, with all the plain to choose?
+
No foot-print leading to that horrid mews,
+
+
+
None out of it. Mad brewage set to work
+
Their brains, no doubt, like galley-slaves the Turk +
+
Pits for his pastime, Christians against Jews.
+
+
+
+


+
+
+
+
+
+
XXIV.
+
+
+
+
+
And more than that - a furlong on - why, there! +
+
What bad use was that engine for, that wheel,
+
Or brake, not wheel - that harrow fit to reel
+
+
+
Men's bodies out like silk? with all the air
+
Of Tophet's tool, on earth left unaware, +
+
Or brought to sharpen its rusty teeth of steel.
+
+
+
+


+
+
+
+
+
+
XXV.
+
+
+
+
+
Then came a bit of stubbed ground, once a wood, +
+
Next a marsh, it would seem, and now mere earth
+
Desperate and done with; (so a fool finds mirth,
+
+
+
Makes a thing and then mars it, till his mood
+
Changes and off he goes!) within a rood— +
+
Bog, clay and rubble, sand and stark black dearth.
+
+
+
+


+
+
+
+
+
+
XXVI.
+
+
+
+
+
Now blotches rankling, coloured gay and grim, +
+
Now patches where some leanness of the soil's
+
Broke into moss or substances like boils;
+
+
+
Then came some palsied oak, a cleft in him
+
Like a distorted mouth that splits its rim +
+
Gaping at death, and dies while it recoils.
+
+
+
+


+
+
+
+
+
+
XXVII.
+
+
+
+
+
And just as far as ever from the end! +
+
Nought in the distance but the evening, nought
+
To point my footstep further! At the thought,
+
+
+
A great black bird, Apollyon's bosom-friend,
+
Sailed past, nor beat his wide wing dragon-penned +
+
That brushed my cap—perchance the guide I sought.
+
+
+
+


+
+
+
+
+
+
XXVIII.
+
+
+
+
+
For, looking up, aware I somehow grew, +
+
'Spite of the dusk, the plain had given place
+
All round to mountains - with such name to grace
+
+
+
Mere ugly heights and heaps now stolen in view.
+
How thus they had surprised me, - solve it, you! +
+
How to get from them was no clearer case.
+
+
+
+


+
+
+
+
+
+
XXIX.
+
+
+
+
+
Yet half I seemed to recognise some trick +
+
Of mischief happened to me, God knows when—
+
In a bad dream perhaps. Here ended, then,
+
+
+
Progress this way. When, in the very nick
+
Of giving up, one time more, came a click +
+
As when a trap shuts - you're inside the den!
+
+
+
+


+
+
+
+
+
+
XXX.
+
+
+
+
+
Burningly it came on me all at once, +
+
This was the place! those two hills on the right,
+
Crouched like two bulls locked horn in horn in fight;
+
+
+
While to the left, a tall scalped mountain... Dunce,
+
Dotard, a-dozing at the very nonce, +
+
After a life spent training for the sight!
+
+
+
+


+
+
+
+
+
+
XXXI.
+
+
+
+
+
What in the midst lay but the Tower itself? +
+
The round squat turret, blind as the fool's heart
+
Built of brown stone, without a counterpart
+
+
+
In the whole world. The tempest's mocking elf
+
Points to the shipman thus the unseen shelf +
+
He strikes on, only when the timbers start.
+
+
+
+


+
+
+
+
+
+
XXXII.
+
+
+
+
+
Not see? because of night perhaps? - why, day +
+
Came back again for that! before it left,
+
The dying sunset kindled through a cleft:
+
+
+
The hills, like giants at a hunting, lay
+
Chin upon hand, to see the game at bay,— +
+
"Now stab and end the creature - to the heft!"
+
+
+
+


+
+
+
+
+
+
XXXIII.
+
+
+
+
+
Not hear? when noise was everywhere! it tolled +
+
Increasing like a bell. Names in my ears
+
Of all the lost adventurers my peers,—
+
+
+
How such a one was strong, and such was bold,
+
And such was fortunate, yet each of old +
+
Lost, lost! one moment knelled the woe of years.
+
+
+
+


+
+
+
+
+
+
XXXIV.
+
+
+
+
+
There they stood, ranged along the hillsides, met +
+
To view the last of me, a living frame
+
For one more picture! in a sheet of flame
+
+
+
I saw them and I knew them all. And yet
+
Dauntless the slug-horn to my lips I set, +
+
And blew "Childe Roland to the Dark Tower came."
+
+
+
+ + 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 @@ + + + + + + + + + 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 @@ + + + + + web_channel_test + + + + +
+ + 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 @@ + + + + + web_channel_test (iframe) + + + + + 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"); + }); + } + ); +} -- cgit v1.2.3