summaryrefslogtreecommitdiffstats
path: root/toolkit/modules/tests/browser
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/modules/tests/browser')
-rw-r--r--toolkit/modules/tests/browser/.eslintrc.js7
-rw-r--r--toolkit/modules/tests/browser/WebRequest_dynamic.sjs13
-rw-r--r--toolkit/modules/tests/browser/WebRequest_redirection.sjs4
-rw-r--r--toolkit/modules/tests/browser/browser.ini41
-rw-r--r--toolkit/modules/tests/browser/browser_AsyncPrefs.js97
-rw-r--r--toolkit/modules/tests/browser/browser_Battery.js51
-rw-r--r--toolkit/modules/tests/browser/browser_Deprecated.js157
-rw-r--r--toolkit/modules/tests/browser/browser_Finder.js62
-rw-r--r--toolkit/modules/tests/browser/browser_FinderHighlighter.js460
-rw-r--r--toolkit/modules/tests/browser/browser_Finder_hidden_textarea.js52
-rw-r--r--toolkit/modules/tests/browser/browser_Geometry.js111
-rw-r--r--toolkit/modules/tests/browser/browser_InlineSpellChecker.js121
-rw-r--r--toolkit/modules/tests/browser/browser_PageMetadata.js73
-rw-r--r--toolkit/modules/tests/browser/browser_PromiseMessage.js38
-rw-r--r--toolkit/modules/tests/browser/browser_RemotePageManager.js400
-rw-r--r--toolkit/modules/tests/browser/browser_Troubleshoot.js546
-rw-r--r--toolkit/modules/tests/browser/browser_WebNavigation.js140
-rw-r--r--toolkit/modules/tests/browser/browser_WebRequest.js214
-rw-r--r--toolkit/modules/tests/browser/browser_WebRequest_cookies.js89
-rw-r--r--toolkit/modules/tests/browser/browser_WebRequest_filtering.js118
-rw-r--r--toolkit/modules/tests/browser/dummy_page.html7
-rw-r--r--toolkit/modules/tests/browser/file_FinderSample.html824
-rw-r--r--toolkit/modules/tests/browser/file_WebNavigation_page1.html9
-rw-r--r--toolkit/modules/tests/browser/file_WebNavigation_page2.html7
-rw-r--r--toolkit/modules/tests/browser/file_WebNavigation_page3.html9
-rw-r--r--toolkit/modules/tests/browser/file_WebRequest_page1.html29
-rw-r--r--toolkit/modules/tests/browser/file_WebRequest_page2.html25
-rw-r--r--toolkit/modules/tests/browser/file_image_bad.pngbin0 -> 5401 bytes
-rw-r--r--toolkit/modules/tests/browser/file_image_good.pngbin0 -> 580 bytes
-rw-r--r--toolkit/modules/tests/browser/file_image_redirect.pngbin0 -> 5401 bytes
-rw-r--r--toolkit/modules/tests/browser/file_script_bad.js1
-rw-r--r--toolkit/modules/tests/browser/file_script_good.js1
-rw-r--r--toolkit/modules/tests/browser/file_script_redirect.js2
-rw-r--r--toolkit/modules/tests/browser/file_script_xhr.js3
-rw-r--r--toolkit/modules/tests/browser/file_style_bad.css3
-rw-r--r--toolkit/modules/tests/browser/file_style_good.css3
-rw-r--r--toolkit/modules/tests/browser/file_style_redirect.css3
-rw-r--r--toolkit/modules/tests/browser/head.js23
-rw-r--r--toolkit/modules/tests/browser/metadata_simple.html10
-rw-r--r--toolkit/modules/tests/browser/metadata_titles.html11
-rw-r--r--toolkit/modules/tests/browser/metadata_titles_fallback.html10
-rw-r--r--toolkit/modules/tests/browser/testremotepagemanager.html66
42 files changed, 3840 insertions, 0 deletions
diff --git a/toolkit/modules/tests/browser/.eslintrc.js b/toolkit/modules/tests/browser/.eslintrc.js
new file mode 100644
index 000000000..c764b133d
--- /dev/null
+++ b/toolkit/modules/tests/browser/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../testing/mochitest/browser.eslintrc.js"
+ ]
+};
diff --git a/toolkit/modules/tests/browser/WebRequest_dynamic.sjs b/toolkit/modules/tests/browser/WebRequest_dynamic.sjs
new file mode 100644
index 000000000..7b34a377d
--- /dev/null
+++ b/toolkit/modules/tests/browser/WebRequest_dynamic.sjs
@@ -0,0 +1,13 @@
+function handleRequest(aRequest, aResponse) {
+ aResponse.setStatusLine(aRequest.httpVersion, 200);
+ if (aRequest.hasHeader('Cookie')) {
+ let value = aRequest.getHeader("Cookie");
+ if (value == "blinky=1") {
+ aResponse.setHeader("Set-Cookie", "dinky=1");
+ }
+ aResponse.write("cookie-present");
+ } else {
+ aResponse.setHeader("Set-Cookie", "foopy=1");
+ aResponse.write("cookie-not-present");
+ }
+}
diff --git a/toolkit/modules/tests/browser/WebRequest_redirection.sjs b/toolkit/modules/tests/browser/WebRequest_redirection.sjs
new file mode 100644
index 000000000..370ecd213
--- /dev/null
+++ b/toolkit/modules/tests/browser/WebRequest_redirection.sjs
@@ -0,0 +1,4 @@
+function handleRequest(aRequest, aResponse) {
+ aResponse.setStatusLine(aRequest.httpVersion, 302);
+ aResponse.setHeader("Location", "./dummy_page.html");
+}
diff --git a/toolkit/modules/tests/browser/browser.ini b/toolkit/modules/tests/browser/browser.ini
new file mode 100644
index 000000000..e82feaa42
--- /dev/null
+++ b/toolkit/modules/tests/browser/browser.ini
@@ -0,0 +1,41 @@
+[DEFAULT]
+support-files =
+ dummy_page.html
+ metadata_*.html
+ testremotepagemanager.html
+ file_WebNavigation_page1.html
+ file_WebNavigation_page2.html
+ file_WebNavigation_page3.html
+ file_WebRequest_page1.html
+ file_WebRequest_page2.html
+ file_image_good.png
+ file_image_bad.png
+ file_image_redirect.png
+ file_style_good.css
+ file_style_bad.css
+ file_style_redirect.css
+ file_script_good.js
+ file_script_bad.js
+ file_script_redirect.js
+ file_script_xhr.js
+ WebRequest_dynamic.sjs
+ WebRequest_redirection.sjs
+
+[browser_AsyncPrefs.js]
+[browser_Battery.js]
+[browser_Deprecated.js]
+[browser_Finder.js]
+[browser_Finder_hidden_textarea.js]
+[browser_FinderHighlighter.js]
+skip-if = debug || os = "linux"
+support-files = file_FinderSample.html
+[browser_Geometry.js]
+[browser_InlineSpellChecker.js]
+[browser_WebNavigation.js]
+[browser_WebRequest.js]
+[browser_WebRequest_cookies.js]
+[browser_WebRequest_filtering.js]
+[browser_PageMetadata.js]
+[browser_PromiseMessage.js]
+[browser_RemotePageManager.js]
+[browser_Troubleshoot.js]
diff --git a/toolkit/modules/tests/browser/browser_AsyncPrefs.js b/toolkit/modules/tests/browser/browser_AsyncPrefs.js
new file mode 100644
index 000000000..1d20a3789
--- /dev/null
+++ b/toolkit/modules/tests/browser/browser_AsyncPrefs.js
@@ -0,0 +1,97 @@
+"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);
+
+function* runTest() {
+ let {AsyncPrefs} = Cu.import("resource://gre/modules/AsyncPrefs.jsm", {});
+ 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],
+ [BrowserUtils.makeURI("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) {
+ yield 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) {
+ yield doesWork(pref, val);
+ is(Services.prefs["get" + type + "Pref"](pref), val, "Pref should have been updated");
+ yield doReset(pref);
+ } else {
+ yield doesFail(pref, val);
+ is(Services.prefs.prefHasUserValue(pref), false, `Pref ${pref} shouldn't get changed`);
+ }
+ }
+ }
+}
+
+add_task(function* runInParent() {
+ yield runTest();
+ resetPrefs();
+});
+
+if (gMultiProcessBrowser) {
+ add_task(function* runInChild() {
+ ok(gBrowser.selectedBrowser.isRemoteBrowser, "Should actually run this in child process");
+ yield ContentTask.spawn(gBrowser.selectedBrowser, null, runTest);
+ resetPrefs();
+ });
+}
diff --git a/toolkit/modules/tests/browser/browser_Battery.js b/toolkit/modules/tests/browser/browser_Battery.js
new file mode 100644
index 000000000..2d3ba5da1
--- /dev/null
+++ b/toolkit/modules/tests/browser/browser_Battery.js
@@ -0,0 +1,51 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+var imported = Components.utils.import("resource://gre/modules/Battery.jsm", this);
+Cu.import("resource://gre/modules/Services.jsm", this);
+
+function test() {
+ waitForExplicitFinish();
+
+ is(imported.Debugging.fake, false, "Battery spoofing is initially false")
+
+ GetBattery().then(function (battery) {
+ for (let k of ["charging", "chargingTime", "dischargingTime", "level"]) {
+ let backup = battery[k];
+ try {
+ battery[k] = "__magic__";
+ } catch (e) {
+ // We are testing that we cannot set battery to new values
+ // when "use strict" is enabled, this throws a TypeError
+ if (e.name != "TypeError")
+ throw e;
+ }
+ is(battery[k], backup, "Setting battery " + k + " preference without spoofing enabled should fail");
+ }
+
+ imported.Debugging.fake = true;
+
+ // reload again to get the fake one
+ GetBattery().then(function (battery) {
+ battery.charging = true;
+ battery.chargingTime = 100;
+ battery.level = 0.5;
+ ok(battery.charging, "Test for charging setter");
+ is(battery.chargingTime, 100, "Test for chargingTime setter");
+ is(battery.level, 0.5, "Test for level setter");
+
+ battery.charging = false;
+ battery.dischargingTime = 50;
+ battery.level = 0.7;
+ ok(!battery.charging, "Test for charging setter");
+ is(battery.dischargingTime, 50, "Test for dischargingTime setter");
+ is(battery.level, 0.7, "Test for level setter");
+
+ // Resetting the value to make the test run successful
+ // for multiple runs in same browser session.
+ imported.Debugging.fake = false;
+ finish();
+ });
+ });
+}
diff --git a/toolkit/modules/tests/browser/browser_Deprecated.js b/toolkit/modules/tests/browser/browser_Deprecated.js
new file mode 100644
index 000000000..3217bdd22
--- /dev/null
+++ b/toolkit/modules/tests/browser/browser_Deprecated.js
@@ -0,0 +1,157 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+const PREF_DEPRECATION_WARNINGS = "devtools.errorconsole.deprecation_warnings";
+
+Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://gre/modules/Deprecated.jsm", this);
+
+// Using this named functions to test deprecation and the properly logged
+// callstacks.
+function basicDeprecatedFunction () {
+ Deprecated.warning("this method is deprecated.", "http://example.com");
+ return true;
+}
+
+function deprecationFunctionBogusCallstack () {
+ Deprecated.warning("this method is deprecated.", "http://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.", "http://example.com",
+ getStack());
+ return true;
+}
+
+var tests = [
+// Test deprecation warning without passing the callstack.
+{
+ deprecatedFunction: basicDeprecatedFunction,
+ expectedObservation: function (aMessage) {
+ testAMessage(aMessage);
+ ok(aMessage.errorMessage.indexOf("basicDeprecatedFunction") > 0,
+ "Callstack is correctly logged.");
+ }
+},
+// Test a reported error when URL to documentation is not passed.
+{
+ deprecatedFunction: function () {
+ Deprecated.warning("this method is deprecated.");
+ return true;
+ },
+ expectedObservation: function (aMessage) {
+ ok(aMessage.errorMessage.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: function (aMessage) {
+ testAMessage(aMessage);
+ ok(aMessage.errorMessage.indexOf("deprecationFunctionBogusCallstack") > 0,
+ "Callstack is correctly logged.");
+ }
+},
+// When pref is unset Deprecated.warning should not log anything.
+{
+ deprecatedFunction: basicDeprecatedFunction,
+ expectedObservation: null,
+ // Set pref to false.
+ logWarnings: false
+},
+// Test deprecation with a valid custom callstack passed as an argument.
+{
+ deprecatedFunction: deprecationFunctionCustomCallstack,
+ expectedObservation: function (aMessage) {
+ testAMessage(aMessage);
+ ok(aMessage.errorMessage.indexOf("deprecationFunctionCustomCallstack") > 0,
+ "Callstack is correctly logged.");
+ },
+ // Set pref to true.
+ logWarnings: true
+}];
+
+// Which test are we running now?
+var idx = -1;
+
+function test() {
+ waitForExplicitFinish();
+
+ // Check if Deprecated is loaded.
+ ok(Deprecated, "Deprecated object exists");
+
+ nextTest();
+}
+
+// Test Consle Message attributes.
+function testAMessage (aMessage) {
+ ok(aMessage.errorMessage.indexOf("DEPRECATION WARNING: " +
+ "this method is deprecated.") === 0,
+ "Deprecation is correctly logged.");
+ ok(aMessage.errorMessage.indexOf("http://example.com") > 0,
+ "URL is correctly logged.");
+}
+
+function nextTest() {
+ idx++;
+
+ if (idx == tests.length) {
+ finish();
+ return;
+ }
+
+ info("Running test #" + idx);
+ let test = tests[idx];
+
+ // Deprecation warnings will be logged only when the preference is set.
+ if (typeof test.logWarnings !== "undefined") {
+ Services.prefs.setBoolPref(PREF_DEPRECATION_WARNINGS, test.logWarnings);
+ }
+
+ // Create a console listener.
+ let consoleListener = {
+ observe: function (aMessage) {
+ // Ignore unexpected messages.
+ if (!(aMessage instanceof Ci.nsIScriptError)) {
+ return;
+ }
+ if (aMessage.errorMessage.indexOf("DEPRECATION WARNING: ") < 0 &&
+ aMessage.errorMessage.indexOf("must provide a URL") < 0) {
+ return;
+ }
+ ok(aMessage instanceof Ci.nsIScriptError,
+ "Deprecation log message is an instance of type nsIScriptError.");
+
+
+ if (test.expectedObservation === null) {
+ ok(false, "Deprecated warning not expected");
+ }
+ else {
+ test.expectedObservation(aMessage);
+ }
+
+ Services.console.unregisterListener(consoleListener);
+ executeSoon(nextTest);
+ }
+ };
+ Services.console.registerListener(consoleListener);
+ test.deprecatedFunction();
+ if (test.expectedObservation === null) {
+ executeSoon(function() {
+ Services.console.unregisterListener(consoleListener);
+ executeSoon(nextTest);
+ });
+ }
+}
diff --git a/toolkit/modules/tests/browser/browser_Finder.js b/toolkit/modules/tests/browser/browser_Finder.js
new file mode 100644
index 000000000..4dfd921d0
--- /dev/null
+++ b/toolkit/modules/tests/browser/browser_Finder.js
@@ -0,0 +1,62 @@
+/* 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/. */
+
+var Ci = Components.interfaces;
+
+add_task(function* () {
+ const url = "data:text/html;base64," +
+ btoa("<body><iframe srcdoc=\"content\"/></iframe>" +
+ "<a href=\"http://test.com\">test link</a>");
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+
+ let finder = tab.linkedBrowser.finder;
+ let listener = {
+ onFindResult: function () {
+ ok(false, "onFindResult callback wasn't replaced");
+ },
+ onHighlightFinished: function () {
+ 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 = yield promiseFind;
+ Assert.ok(findResult.found, "should find string");
+
+ promiseFind = waitForFind("onHighlightFinished");
+ finder.highlight(true, "Bla");
+ findResult = yield 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 = yield promiseFind;
+ is(findResult.result, Ci.nsITypeAheadFind.FIND_FOUND, "should find link");
+
+ yield ContentTask.spawn(tab.linkedBrowser, {}, 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 = yield promiseFind;
+ is(findResult.result, Ci.nsITypeAheadFind.FIND_FOUND, "should find link again");
+
+ yield ContentTask.spawn(tab.linkedBrowser, {}, 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 000000000..cd7eefa11
--- /dev/null
+++ b/toolkit/modules/tests/browser/browser_FinderHighlighter.js
@@ -0,0 +1,460 @@
+"use strict";
+
+Cu.import("resource://testing-common/BrowserTestUtils.jsm", this);
+Cu.import("resource://testing-common/ContentTask.jsm", this);
+Cu.import("resource://gre/modules/Promise.jsm", this);
+Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://gre/modules/Task.jsm", this);
+Cu.import("resource://gre/modules/Timer.jsm", this);
+Cu.import("resource://gre/modules/AppConstants.jsm");
+
+const kHighlightAllPref = "findbar.highlightAll";
+const kPrefModalHighlight = "findbar.modalHighlight";
+const kFixtureBaseURL = "https://example.com/browser/toolkit/modules/tests/browser/";
+const kIteratorTimeout = Services.prefs.getIntPref("findbar.iteratorTimeout");
+
+function promiseOpenFindbar(findbar) {
+ 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 = document.createEvent("KeyboardEvent");
+ event.initKeyEvent("keypress", true, true, null, false, false,
+ false, false, 0, str.charCodeAt(i));
+ findbar._findField.inputField.dispatchEvent(event);
+ }
+ return promise;
+}
+
+function promiseTestHighlighterOutput(browser, word, expectedResult, extraTest = () => {}) {
+ return ContentTask.spawn(browser, { word, expectedResult, extraTest: extraTest.toSource() },
+ function* ({ word, expectedResult, extraTest }) {
+ Cu.import("resource://gre/modules/Timer.jsm", this);
+
+ return new Promise((resolve, reject) => {
+ let stubbed = {};
+ let callCounts = {
+ insertCalls: [],
+ removeCalls: []
+ };
+ 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 = 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 = {};
+ 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}'.`);
+
+ // Allow more specific assertions to be tested in `extraTest`.
+ extraTest = eval(extraTest);
+ extraTest(lastMaskNode, lastOutlineNode, rects);
+
+ resolve();
+ }
+
+ function stubAnonymousContentNode(domNode, anonNode) {
+ let originals = [anonNode.setTextContentForElement,
+ anonNode.setAttributeForElement, anonNode.removeAttributeForElement,
+ anonNode.setCutoutRectsForElement];
+ 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);
+ };
+ }
+
+ // 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;
+ }
+ clearTimeout(timeout);
+ timeout = 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");
+ });
+ });
+}
+
+add_task(function* setup() {
+ yield SpecialPowers.pushPrefEnv({ set: [
+ [kHighlightAllPref, true],
+ [kPrefModalHighlight, true]
+ ]});
+});
+
+// Test the results of modal highlighting, which is on by default.
+add_task(function* testModalResults() {
+ let tests = new Map([
+ ["Roland", {
+ rectCount: 2,
+ insertCalls: [2, 4],
+ removeCalls: [0, 1]
+ }],
+ ["their law might propagate their kind", {
+ rectCount: 2,
+ insertCalls: [5, 6],
+ removeCalls: [4, 5],
+ 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: [1, 3]
+ }],
+ ["new", {
+ rectCount: 2,
+ insertCalls: [1, 4],
+ removeCalls: [0, 2]
+ }],
+ ["o", {
+ rectCount: 492,
+ insertCalls: [1, 4],
+ removeCalls: [0, 2]
+ }]
+ ]);
+ let url = kFixtureBaseURL + "file_FinderSample.html";
+ yield BrowserTestUtils.withNewTab(url, function* (browser) {
+ let findbar = gBrowser.getFindBar();
+
+ for (let [word, expectedResult] of tests) {
+ yield 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;
+ yield new Promise(resolve => setTimeout(resolve, timeout));
+ let promise = promiseTestHighlighterOutput(browser, word, expectedResult,
+ expectedResult.extraTest);
+ yield promiseEnterStringIntoFindField(findbar, word);
+ yield promise;
+
+ findbar.close(true);
+ }
+ });
+});
+
+// Test if runtime switching of highlight modes between modal and non-modal works
+// as expected.
+add_task(function* testModalSwitching() {
+ let url = kFixtureBaseURL + "file_FinderSample.html";
+ yield BrowserTestUtils.withNewTab(url, function* (browser) {
+ let findbar = gBrowser.getFindBar();
+
+ yield 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);
+ yield promiseEnterStringIntoFindField(findbar, word);
+ yield promise;
+
+ yield SpecialPowers.pushPrefEnv({ "set": [[ kPrefModalHighlight, false ]] });
+
+ expectedResult = {
+ rectCount: 0,
+ insertCalls: [0, 0],
+ removeCalls: [0, 0]
+ };
+ promise = promiseTestHighlighterOutput(browser, word, expectedResult);
+ findbar.clear();
+ yield promiseEnterStringIntoFindField(findbar, word);
+ yield promise;
+
+ findbar.close(true);
+ });
+
+ yield SpecialPowers.pushPrefEnv({ "set": [[ kPrefModalHighlight, true ]] });
+});
+
+// Test if highlighting a dark page is detected properly.
+add_task(function* testDarkPageDetection() {
+ let url = kFixtureBaseURL + "file_FinderSample.html";
+ yield BrowserTestUtils.withNewTab(url, function* (browser) {
+ let findbar = gBrowser.getFindBar();
+
+ yield 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");
+ });
+ yield promiseEnterStringIntoFindField(findbar, word);
+ yield promise;
+
+ findbar.close(true);
+ });
+
+ yield BrowserTestUtils.withNewTab(url, function* (browser) {
+ let findbar = gBrowser.getFindBar();
+
+ yield promiseOpenFindbar(findbar);
+
+ let word = "Roland";
+ let expectedResult = {
+ rectCount: 2,
+ insertCalls: [2, 4],
+ removeCalls: [0, 1]
+ };
+
+ yield ContentTask.spawn(browser, null, function* () {
+ let dwu = content.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ 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");
+ });
+ yield promiseEnterStringIntoFindField(findbar, word);
+ yield promise;
+
+ findbar.close(true);
+ });
+});
+
+add_task(function* testHighlightAllToggle() {
+ let url = kFixtureBaseURL + "file_FinderSample.html";
+ yield BrowserTestUtils.withNewTab(url, function* (browser) {
+ let findbar = gBrowser.getFindBar();
+
+ yield promiseOpenFindbar(findbar);
+
+ let word = "Roland";
+ let expectedResult = {
+ rectCount: 2,
+ insertCalls: [2, 4],
+ removeCalls: [0, 1]
+ };
+ let promise = promiseTestHighlighterOutput(browser, word, expectedResult);
+ yield promiseEnterStringIntoFindField(findbar, word);
+ yield 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: [0, 1]
+ };
+ promise = promiseTestHighlighterOutput(browser, word, expectedResult);
+ yield SpecialPowers.pushPrefEnv({ "set": [[ kHighlightAllPref, false ]] });
+ yield promise;
+
+ // For posterity, let's switch back.
+ expectedResult = {
+ rectCount: 2,
+ insertCalls: [1, 3],
+ removeCalls: [0, 1]
+ };
+ promise = promiseTestHighlighterOutput(browser, word, expectedResult);
+ yield SpecialPowers.pushPrefEnv({ "set": [[ kHighlightAllPref, true ]] });
+ yield promise;
+ });
+});
+
+add_task(function* testXMLDocument() {
+ let url = "data:text/xml;charset=utf-8," + encodeURIComponent(`<?xml version="1.0"?>
+<result>
+ <Title>Example</Title>
+ <Error>Error</Error>
+</result>`);
+ yield BrowserTestUtils.withNewTab(url, function* (browser) {
+ let findbar = gBrowser.getFindBar();
+
+ yield promiseOpenFindbar(findbar);
+
+ let word = "Example";
+ let expectedResult = {
+ rectCount: 0,
+ insertCalls: [1, 4],
+ removeCalls: [0, 1]
+ };
+ let promise = promiseTestHighlighterOutput(browser, word, expectedResult);
+ yield promiseEnterStringIntoFindField(findbar, word);
+ yield promise;
+
+ findbar.close(true);
+ });
+});
+
+add_task(function* testHideOnLocationChange() {
+ let url = kFixtureBaseURL + "file_FinderSample.html";
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+ let browser = tab.linkedBrowser;
+ let findbar = gBrowser.getFindBar();
+
+ yield promiseOpenFindbar(findbar);
+
+ let word = "Roland";
+ let expectedResult = {
+ rectCount: 2,
+ insertCalls: [2, 4],
+ removeCalls: [0, 1]
+ };
+ let promise = promiseTestHighlighterOutput(browser, word, expectedResult);
+ yield promiseEnterStringIntoFindField(findbar, word);
+ yield promise;
+
+ // Now we try to navigate away! (Using the same page)
+ promise = promiseTestHighlighterOutput(browser, word, {
+ rectCount: 0,
+ insertCalls: [0, 0],
+ removeCalls: [1, 2]
+ });
+ yield BrowserTestUtils.loadURI(browser, url);
+ yield promise;
+
+ yield BrowserTestUtils.removeTab(tab);
+});
+
+add_task(function* testHideOnClear() {
+ let url = kFixtureBaseURL + "file_FinderSample.html";
+ yield BrowserTestUtils.withNewTab(url, function* (browser) {
+ let findbar = gBrowser.getFindBar();
+ yield promiseOpenFindbar(findbar);
+
+ let word = "Roland";
+ let expectedResult = {
+ rectCount: 2,
+ insertCalls: [2, 4],
+ removeCalls: [0, 2]
+ };
+ let promise = promiseTestHighlighterOutput(browser, word, expectedResult);
+ yield promiseEnterStringIntoFindField(findbar, word);
+ yield promise;
+
+ yield new Promise(resolve => setTimeout(resolve, kIteratorTimeout));
+ promise = promiseTestHighlighterOutput(browser, "", {
+ rectCount: 0,
+ insertCalls: [0, 0],
+ removeCalls: [1, 2]
+ });
+ findbar.clear();
+ yield promise;
+
+ findbar.close(true);
+ });
+});
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 000000000..99d838ada
--- /dev/null
+++ b/toolkit/modules/tests/browser/browser_Finder_hidden_textarea.js
@@ -0,0 +1,52 @@
+/* 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(function* test_bug1174036() {
+ const URI =
+ "<body><textarea>e1</textarea><textarea>e2</textarea><textarea>e3</textarea></body>";
+ yield BrowserTestUtils.withNewTab({ gBrowser, url: "data:text/html;charset=utf-8," + encodeURIComponent(URI) },
+ function* (browser) {
+ // Hide the first textarea.
+ yield ContentTask.spawn(browser, null, function() {
+ content.document.getElementsByTagName("textarea")[0].style.display = "none";
+ });
+
+ let finder = browser.finder;
+ let listener = {
+ onFindResult: function () {
+ 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 = yield 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(false, false, false);
+ findResult = yield 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(false, false, false);
+ findResult = yield 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_Geometry.js b/toolkit/modules/tests/browser/browser_Geometry.js
new file mode 100644
index 000000000..aaca79a06
--- /dev/null
+++ b/toolkit/modules/tests/browser/browser_Geometry.js
@@ -0,0 +1,111 @@
+/* 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/. */
+
+var tempScope = {};
+Components.utils.import("resource://gre/modules/Geometry.jsm", tempScope);
+var Point = tempScope.Point;
+var Rect = tempScope.Rect;
+
+function test() {
+ ok(Rect, "Rect class exists");
+ for (var fname in tests) {
+ tests[fname]();
+ }
+}
+
+var tests = {
+ testGetDimensions: function() {
+ 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: function() {
+ 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: function() {
+ 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: function() {
+ 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 000000000..2bffc9722
--- /dev/null
+++ b/toolkit/modules/tests/browser/browser_InlineSpellChecker.js
@@ -0,0 +1,121 @@
+function test() {
+ let tempScope = {};
+ Components.utils.import("resource://gre/modules/InlineSpellChecker.jsm", tempScope);
+ let InlineSpellChecker = tempScope.InlineSpellChecker;
+
+ ok(InlineSpellChecker, "InlineSpellChecker class exists");
+ for (var fname in tests) {
+ tests[fname]();
+ }
+}
+
+var tests = {
+ // Test various possible dictionary name to ensure they display as expected.
+ // XXX: This only works for the 'en-US' locale, as the testing involves localized output.
+ testDictionaryDisplayNames: function() {
+ let isc = new InlineSpellChecker();
+
+ // Check non-well-formed language tag.
+ is(isc.getDictionaryDisplayName("-invalid-"), "-invalid-", "'-invalid-' should display as '-invalid-'");
+
+ // XXX: It isn't clear how we'd ideally want to display variant subtags.
+
+ // Check valid language subtag.
+ is(isc.getDictionaryDisplayName("en"), "English", "'en' should display as 'English'");
+ is(isc.getDictionaryDisplayName("en-fonipa"), "English (fonipa)", "'en-fonipa' should display as 'English (fonipa)'");
+ is(isc.getDictionaryDisplayName("en-qxqaaaaz"), "English (qxqaaaaz)", "'en-qxqaaaaz' should display as 'English (qxqaaaaz)'");
+
+ // Check valid language subtag and valid region subtag.
+ is(isc.getDictionaryDisplayName("en-US"), "English (United States)", "'en-US' should display as 'English (United States)'");
+ is(isc.getDictionaryDisplayName("en-US-fonipa"), "English (United States) (fonipa)", "'en-US-fonipa' should display as 'English (United States) (fonipa)'");
+ is(isc.getDictionaryDisplayName("en-US-qxqaaaaz"), "English (United States) (qxqaaaaz)", "'en-US-qxqaaaaz' should display as 'English (United States) (qxqaaaaz)'");
+
+ // Check valid language subtag and invalid but well-formed region subtag.
+ is(isc.getDictionaryDisplayName("en-WO"), "English (WO)", "'en-WO' should display as 'English (WO)'");
+ is(isc.getDictionaryDisplayName("en-WO-fonipa"), "English (WO) (fonipa)", "'en-WO-fonipa' should display as 'English (WO) (fonipa)'");
+ is(isc.getDictionaryDisplayName("en-WO-qxqaaaaz"), "English (WO) (qxqaaaaz)", "'en-WO-qxqaaaaz' should display as 'English (WO) (qxqaaaaz)'");
+
+ // Check valid language subtag and valid script subtag.
+ todo_is(isc.getDictionaryDisplayName("en-Cyrl"), "English / Cyrillic", "'en-Cyrl' should display as 'English / Cyrillic'");
+ todo_is(isc.getDictionaryDisplayName("en-Cyrl-fonipa"), "English / Cyrillic (fonipa)", "'en-Cyrl-fonipa' should display as 'English / Cyrillic (fonipa)'");
+ todo_is(isc.getDictionaryDisplayName("en-Cyrl-qxqaaaaz"), "English / Cyrillic (qxqaaaaz)", "'en-Cyrl-qxqaaaaz' should display as 'English / Cyrillic (qxqaaaaz)'");
+ todo_is(isc.getDictionaryDisplayName("en-Cyrl-US"), "English (United States) / Cyrillic", "'en-Cyrl-US' should display as 'English (United States) / Cyrillic'");
+ todo_is(isc.getDictionaryDisplayName("en-Cyrl-US-fonipa"), "English (United States) / Cyrillic (fonipa)", "'en-Cyrl-US-fonipa' should display as 'English (United States) / Cyrillic (fonipa)'");
+ todo_is(isc.getDictionaryDisplayName("en-Cyrl-US-qxqaaaaz"), "English (United States) / Cyrillic (qxqaaaaz)", "'en-Cyrl-US-qxqaaaaz' should display as 'English (United States) / Cyrillic (qxqaaaaz)'");
+ todo_is(isc.getDictionaryDisplayName("en-Cyrl-WO"), "English (WO) / Cyrillic", "'en-Cyrl-WO' should display as 'English (WO) / Cyrillic'");
+ todo_is(isc.getDictionaryDisplayName("en-Cyrl-WO-fonipa"), "English (WO) / Cyrillic (fonipa)", "'en-Cyrl-WO-fonipa' should display as 'English (WO) / Cyrillic (fonipa)'");
+ todo_is(isc.getDictionaryDisplayName("en-Cyrl-WO-qxqaaaaz"), "English (WO) / Cyrillic (qxqaaaaz)", "'en-Cyrl-WO-qxqaaaaz' should display as 'English (WO) / Cyrillic (qxqaaaaz)'");
+
+ // Check valid language subtag and invalid but well-formed script subtag.
+ is(isc.getDictionaryDisplayName("en-Qaaz"), "English / Qaaz", "'en-Qaaz' should display as 'English / Qaaz'");
+ is(isc.getDictionaryDisplayName("en-Qaaz-fonipa"), "English / Qaaz (fonipa)", "'en-Qaaz-fonipa' should display as 'English / Qaaz (fonipa)'");
+ is(isc.getDictionaryDisplayName("en-Qaaz-qxqaaaaz"), "English / Qaaz (qxqaaaaz)", "'en-Qaaz-qxqaaaaz' should display as 'English / Qaaz (qxqaaaaz)'");
+ is(isc.getDictionaryDisplayName("en-Qaaz-US"), "English (United States) / Qaaz", "'en-Qaaz-US' should display as 'English (United States) / Qaaz'");
+ is(isc.getDictionaryDisplayName("en-Qaaz-US-fonipa"), "English (United States) / Qaaz (fonipa)", "'en-Qaaz-US-fonipa' should display as 'English (United States) / Qaaz (fonipa)'");
+ is(isc.getDictionaryDisplayName("en-Qaaz-US-qxqaaaaz"), "English (United States) / Qaaz (qxqaaaaz)", "'en-Qaaz-US-qxqaaaaz' should display as 'English (United States) / Qaaz (qxqaaaaz)'");
+ is(isc.getDictionaryDisplayName("en-Qaaz-WO"), "English (WO) / Qaaz", "'en-Qaaz-WO' should display as 'English (WO) / Qaaz'");
+ is(isc.getDictionaryDisplayName("en-Qaaz-WO-fonipa"), "English (WO) / Qaaz (fonipa)", "'en-Qaaz-WO-fonipa' should display as 'English (WO) / Qaaz (fonipa)'");
+ is(isc.getDictionaryDisplayName("en-Qaaz-WO-qxqaaaaz"), "English (WO) / Qaaz (qxqaaaaz)", "'en-Qaaz-WO-qxqaaaaz' should display as 'English (WO) / Qaaz (qxqaaaaz)'");
+
+ // Check invalid but well-formed language subtag.
+ is(isc.getDictionaryDisplayName("qaz"), "qaz", "'qaz' should display as 'qaz'");
+ is(isc.getDictionaryDisplayName("qaz-fonipa"), "qaz (fonipa)", "'qaz-fonipa' should display as 'qaz (fonipa)'");
+ is(isc.getDictionaryDisplayName("qaz-qxqaaaaz"), "qaz (qxqaaaaz)", "'qaz-qxqaaaaz' should display as 'qaz (qxqaaaaz)'");
+
+ // Check invalid but well-formed language subtag and valid region subtag.
+ is(isc.getDictionaryDisplayName("qaz-US"), "qaz (United States)", "'qaz-US' should display as 'qaz (United States)'");
+ is(isc.getDictionaryDisplayName("qaz-US-fonipa"), "qaz (United States) (fonipa)", "'qaz-US-fonipa' should display as 'qaz (United States) (fonipa)'");
+ is(isc.getDictionaryDisplayName("qaz-US-qxqaaaaz"), "qaz (United States) (qxqaaaaz)", "'qaz-US-qxqaaaaz' should display as 'qaz (United States) (qxqaaaaz)'");
+
+ // Check invalid but well-formed language subtag and invalid but well-formed region subtag.
+ is(isc.getDictionaryDisplayName("qaz-WO"), "qaz (WO)", "'qaz-WO' should display as 'qaz (WO)'");
+ is(isc.getDictionaryDisplayName("qaz-WO-fonipa"), "qaz (WO) (fonipa)", "'qaz-WO-fonipa' should display as 'qaz (WO) (fonipa)'");
+ is(isc.getDictionaryDisplayName("qaz-WO-qxqaaaaz"), "qaz (WO) (qxqaaaaz)", "'qaz-WO-qxqaaaaz' should display as 'qaz (WO) (qxqaaaaz)'");
+
+ // Check invalid but well-formed language subtag and valid script subtag.
+ todo_is(isc.getDictionaryDisplayName("qaz-Cyrl"), "qaz / Cyrillic", "'qaz-Cyrl' should display as 'qaz / Cyrillic'");
+ todo_is(isc.getDictionaryDisplayName("qaz-Cyrl-fonipa"), "qaz / Cyrillic (fonipa)", "'qaz-Cyrl-fonipa' should display as 'qaz / Cyrillic (fonipa)'");
+ todo_is(isc.getDictionaryDisplayName("qaz-Cyrl-qxqaaaaz"), "qaz / Cyrillic (qxqaaaaz)", "'qaz-Cyrl-qxqaaaaz' should display as 'qaz / Cyrillic (qxqaaaaz)'");
+ todo_is(isc.getDictionaryDisplayName("qaz-Cyrl-US"), "qaz (United States) / Cyrillic", "'qaz-Cyrl-US' should display as 'qaz (United States) / Cyrillic'");
+ todo_is(isc.getDictionaryDisplayName("qaz-Cyrl-US-fonipa"), "qaz (United States) / Cyrillic (fonipa)", "'qaz-Cyrl-US-fonipa' should display as 'qaz (United States) / Cyrillic (fonipa)'");
+ todo_is(isc.getDictionaryDisplayName("qaz-Cyrl-US-qxqaaaaz"), "qaz (United States) / Cyrillic (qxqaaaaz)", "'qaz-Cyrl-US-qxqaaaaz' should display as 'qaz (United States) / Cyrillic (qxqaaaaz)'");
+ todo_is(isc.getDictionaryDisplayName("qaz-Cyrl-WO"), "qaz (WO) / Cyrillic", "'qaz-Cyrl-WO' should display as 'qaz (WO) / Cyrillic'");
+ todo_is(isc.getDictionaryDisplayName("qaz-Cyrl-WO-fonipa"), "qaz (WO) / Cyrillic (fonipa)", "'qaz-Cyrl-WO-fonipa' should display as 'qaz (WO) / Cyrillic (fonipa)'");
+ todo_is(isc.getDictionaryDisplayName("qaz-Cyrl-WO-qxqaaaaz"), "qaz (WO) / Cyrillic (qxqaaaaz)", "'qaz-Cyrl-WO-qxqaaaaz' should display as 'qaz (WO) / Cyrillic (qxqaaaaz)'");
+
+ // Check invalid but well-formed language subtag and invalid but well-formed script subtag.
+ is(isc.getDictionaryDisplayName("qaz-Qaaz"), "qaz / Qaaz", "'qaz-Qaaz' should display as 'qaz / Qaaz'");
+ is(isc.getDictionaryDisplayName("qaz-Qaaz-fonipa"), "qaz / Qaaz (fonipa)", "'qaz-Qaaz-fonipa' should display as 'qaz / Qaaz (fonipa)'");
+ is(isc.getDictionaryDisplayName("qaz-Qaaz-qxqaaaaz"), "qaz / Qaaz (qxqaaaaz)", "'qaz-Qaaz-qxqaaaaz' should display as 'qaz / Qaaz (qxqaaaaz)'");
+ is(isc.getDictionaryDisplayName("qaz-Qaaz-US"), "qaz (United States) / Qaaz", "'qaz-Qaaz-US' should display as 'qaz (United States) / Qaaz'");
+ is(isc.getDictionaryDisplayName("qaz-Qaaz-US-fonipa"), "qaz (United States) / Qaaz (fonipa)", "'qaz-Qaaz-US-fonipa' should display as 'qaz (United States) / Qaaz (fonipa)'");
+ is(isc.getDictionaryDisplayName("qaz-Qaaz-US-qxqaaaaz"), "qaz (United States) / Qaaz (qxqaaaaz)", "'qaz-Qaaz-US-qxqaaaaz' should display as 'qaz (United States) / Qaaz (qxqaaaaz)'");
+ is(isc.getDictionaryDisplayName("qaz-Qaaz-WO"), "qaz (WO) / Qaaz", "'qaz-Qaaz-WO' should display as 'qaz (WO) / Qaaz'");
+ is(isc.getDictionaryDisplayName("qaz-Qaaz-WO-fonipa"), "qaz (WO) / Qaaz (fonipa)", "'qaz-Qaaz-WO-fonipa' should display as 'qaz (WO) / Qaaz (fonipa)'");
+ is(isc.getDictionaryDisplayName("qaz-Qaaz-WO-qxqaaaaz"), "qaz (WO) / Qaaz (qxqaaaaz)", "'qaz-Qaaz-WO-qxqaaaaz' should display as 'qaz (WO) / Qaaz (qxqaaaaz)'");
+
+ // Check multiple variant subtags.
+ todo_is(isc.getDictionaryDisplayName("en-Cyrl-US-fonipa-fonxsamp"), "English (United States) / Cyrillic (fonipa / fonxsamp)", "'en-Cyrl-US-fonipa-fonxsamp' should display as 'English (United States) / Cyrillic (fonipa / fonxsamp)'");
+ todo_is(isc.getDictionaryDisplayName("en-Cyrl-US-fonipa-qxqaaaaz"), "English (United States) / Cyrillic (fonipa / qxqaaaaz)", "'en-Cyrl-US-fonipa-qxqaaaaz' should display as 'English (United States) / Cyrillic (fonipa / qxqaaaaz)'");
+ todo_is(isc.getDictionaryDisplayName("en-Cyrl-US-fonipa-fonxsamp-qxqaaaaz"), "English (United States) / Cyrillic (fonipa / fonxsamp / qxqaaaaz)", "'en-Cyrl-US-fonipa-fonxsamp-qxqaaaaz' should display as 'English (United States) / Cyrillic (fonipa / fonxsamp / qxqaaaaz)'");
+ is(isc.getDictionaryDisplayName("qaz-Qaaz-WO-fonipa-fonxsamp"), "qaz (WO) / Qaaz (fonipa / fonxsamp)", "'qaz-Qaaz-WO-fonipa-fonxsamp' should display as 'qaz (WO) / Qaaz (fonipa / fonxsamp)'");
+ is(isc.getDictionaryDisplayName("qaz-Qaaz-WO-fonipa-qxqaaaaz"), "qaz (WO) / Qaaz (fonipa / qxqaaaaz)", "'qaz-Qaaz-WO-fonipa-qxqaaaaz' should display as 'qaz (WO) / Qaaz (fonipa / qxqaaaaz)'");
+ is(isc.getDictionaryDisplayName("qaz-Qaaz-WO-fonipa-fonxsamp-qxqaaaaz"), "qaz (WO) / Qaaz (fonipa / fonxsamp / qxqaaaaz)", "'qaz-Qaaz-WO-fonipa-fonxsamp-qxqaaaaz' should display as 'qaz (WO) / Qaaz (fonipa / fonxsamp / qxqaaaaz)'");
+
+ // Check numeric region subtag.
+ todo_is(isc.getDictionaryDisplayName("es-419"), "Spanish (Latin America and the Caribbean)", "'es-419' should display as 'Spanish (Latin America and the Caribbean)'");
+
+ // Check that extension subtags are ignored.
+ todo_is(isc.getDictionaryDisplayName("en-Cyrl-t-en-latn-m0-ungegn-2007"), "English / Cyrillic", "'en-Cyrl-t-en-latn-m0-ungegn-2007' should display as 'English / Cyrillic'");
+
+ // Check that privateuse subtags are ignored.
+ is(isc.getDictionaryDisplayName("en-x-ignore"), "English", "'en-x-ignore' should display as 'English'");
+ is(isc.getDictionaryDisplayName("en-x-ignore-this"), "English", "'en-x-ignore-this' should display as 'English'");
+ is(isc.getDictionaryDisplayName("en-x-ignore-this-subtag"), "English", "'en-x-ignore-this-subtag' should display as 'English'");
+
+ // Check that both extension and privateuse subtags are ignored.
+ todo_is(isc.getDictionaryDisplayName("en-Cyrl-t-en-latn-m0-ungegn-2007-x-ignore-this-subtag"), "English / Cyrillic", "'en-Cyrl-t-en-latn-m0-ungegn-2007-x-ignore-this-subtag' should display as 'English / Cyrillic'");
+
+ // XXX: Check grandfathered tags.
+ },
+};
diff --git a/toolkit/modules/tests/browser/browser_PageMetadata.js b/toolkit/modules/tests/browser/browser_PageMetadata.js
new file mode 100644
index 000000000..ca6e18368
--- /dev/null
+++ b/toolkit/modules/tests/browser/browser_PageMetadata.js
@@ -0,0 +1,73 @@
+/**
+ * Tests PageMetadata.jsm, which extracts metadata and microdata from a
+ * document.
+ */
+
+var {PageMetadata} = Cu.import("resource://gre/modules/PageMetadata.jsm", {});
+
+var rootURL = "http://example.com/browser/toolkit/modules/tests/browser/";
+
+function promiseDocument(fileName) {
+ let url = rootURL + fileName;
+
+ return new Promise((resolve, reject) => {
+ let xhr = new XMLHttpRequest();
+ xhr.onload = () => resolve(xhr.responseXML);
+ xhr.onerror = () => reject(new Error("Error loading document"));
+ xhr.open("GET", url);
+ xhr.responseType = "document";
+ xhr.send();
+ });
+}
+
+/**
+ * Load a simple document.
+ */
+add_task(function* simpleDoc() {
+ let fileName = "metadata_simple.html";
+ info(`Loading a simple page, ${fileName}`);
+
+ let doc = yield promiseDocument(fileName);
+ Assert.notEqual(doc, null,
+ "Should have a document to analyse");
+
+ let data = PageMetadata.getData(doc);
+ Assert.notEqual(data, null,
+ "Should have non-null result");
+ Assert.equal(data.url, rootURL + fileName,
+ "Should have expected url property");
+ Assert.equal(data.title, "Test Title",
+ "Should have expected title property");
+ Assert.equal(data.description, "A very simple test page",
+ "Should have expected title property");
+});
+
+add_task(function* titlesDoc() {
+ let fileName = "metadata_titles.html";
+ info(`Loading titles page, ${fileName}`);
+
+ let doc = yield promiseDocument(fileName);
+ Assert.notEqual(doc, null,
+ "Should have a document to analyse");
+
+ let data = PageMetadata.getData(doc);
+ Assert.notEqual(data, null,
+ "Should have non-null result");
+ Assert.equal(data.title, "Test Titles",
+ "Should use the page title, not the open graph title");
+});
+
+add_task(function* titlesFallbackDoc() {
+ let fileName = "metadata_titles_fallback.html";
+ info(`Loading titles page, ${fileName}`);
+
+ let doc = yield promiseDocument(fileName);
+ Assert.notEqual(doc, null,
+ "Should have a document to analyse");
+
+ let data = PageMetadata.getData(doc);
+ Assert.notEqual(data, null,
+ "Should have non-null result");
+ Assert.equal(data.title, "Title",
+ "Should use the open graph title");
+});
diff --git a/toolkit/modules/tests/browser/browser_PromiseMessage.js b/toolkit/modules/tests/browser/browser_PromiseMessage.js
new file mode 100644
index 000000000..e967ac4c9
--- /dev/null
+++ b/toolkit/modules/tests/browser/browser_PromiseMessage.js
@@ -0,0 +1,38 @@
+/* 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/. */
+/* global Cu, BrowserTestUtils, is, ok, add_task, gBrowser */
+"use strict";
+Cu.import("resource://gre/modules/PromiseMessage.jsm", this);
+
+
+const url = "http://example.org/tests/dom/manifest/test/resource.sjs";
+
+/**
+ * Test basic API error conditions
+ */
+add_task(function* () {
+ yield BrowserTestUtils.withNewTab({gBrowser, url}, testPromiseMessageAPI)
+});
+
+function* testPromiseMessageAPI(aBrowser) {
+ // Reusing an existing message.
+ const msgKey = "DOM:WebManifest:hasManifestLink";
+ const mm = aBrowser.messageManager;
+ const id = "this should not change";
+ const foo = "neitherShouldThis";
+ const data = {id, foo};
+
+ // This just returns false, and it doesn't matter for this test.
+ yield PromiseMessage.send(mm, msgKey, data);
+
+ // Check that no new props were added
+ const props = Object.getOwnPropertyNames(data);
+ ok(props.length === 2, "There should only be 2 props");
+ ok(props.includes("id"), "Has the id property");
+ ok(props.includes("foo"), "Has the foo property");
+
+ // Check that the props didn't change.
+ is(data.id, id, "The id prop must not change.");
+ is(data.foo, foo, "The foo prop must not change.");
+}
diff --git a/toolkit/modules/tests/browser/browser_RemotePageManager.js b/toolkit/modules/tests/browser/browser_RemotePageManager.js
new file mode 100644
index 000000000..774d33034
--- /dev/null
+++ b/toolkit/modules/tests/browser/browser_RemotePageManager.js
@@ -0,0 +1,400 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const TEST_URL = "http://www.example.com/browser/toolkit/modules/tests/browser/testremotepagemanager.html";
+
+var { RemotePages, RemotePageManager } = Cu.import("resource://gre/modules/RemotePageManager.jsm", {});
+
+function failOnMessage(message) {
+ ok(false, "Should not have seen message " + message.name);
+}
+
+function waitForMessage(port, message, expectedPort = port) {
+ return new Promise((resolve) => {
+ function listener(message) {
+ is(message.target, expectedPort, "Message should be from the right port.");
+
+ port.removeMessageListener(listener);
+ resolve(message);
+ }
+
+ port.addMessageListener(message, listener);
+ });
+}
+
+function waitForPort(url, createTab = true) {
+ return new Promise((resolve) => {
+ RemotePageManager.addRemotePageListener(url, (port) => {
+ RemotePageManager.removeRemotePageListener(url);
+
+ waitForMessage(port, "RemotePage:Load").then(() => resolve(port));
+ });
+
+ if (createTab)
+ gBrowser.selectedTab = gBrowser.addTab(url);
+ });
+}
+
+function waitForPage(pages) {
+ return new Promise((resolve) => {
+ function listener({ target }) {
+ pages.removeMessageListener("RemotePage:Init", listener);
+
+ waitForMessage(target, "RemotePage:Load").then(() => resolve(target));
+ }
+
+ pages.addMessageListener("RemotePage:Init", listener);
+ gBrowser.selectedTab = gBrowser.addTab(TEST_URL);
+ });
+}
+
+function swapDocShells(browser1, browser2) {
+ // Swap frameLoaders.
+ browser1.swapDocShells(browser2);
+
+ // Swap permanentKeys.
+ let tmp = browser1.permanentKey;
+ browser1.permanentKey = browser2.permanentKey;
+ browser2.permanentKey = tmp;
+}
+
+// Test that opening a page creates a port, sends the load event and then
+// navigating to a new page sends the unload event. Going back should create a
+// new port
+add_task(function* init_navigate() {
+ let port = yield waitForPort(TEST_URL);
+ is(port.browser, gBrowser.selectedBrowser, "Port is for the correct browser");
+
+ let loaded = new Promise(resolve => {
+ function listener() {
+ gBrowser.selectedBrowser.removeEventListener("load", listener, true);
+ resolve();
+ }
+ gBrowser.selectedBrowser.addEventListener("load", listener, true);
+ gBrowser.loadURI("about:blank");
+ });
+
+ yield waitForMessage(port, "RemotePage:Unload");
+
+ // Port should be destroyed now
+ try {
+ port.addMessageListener("Foo", failOnMessage);
+ ok(false, "Should have seen exception");
+ }
+ catch (e) {
+ ok(true, "Should have seen exception");
+ }
+
+ try {
+ port.sendAsyncMessage("Foo");
+ ok(false, "Should have seen exception");
+ }
+ catch (e) {
+ ok(true, "Should have seen exception");
+ }
+
+ yield loaded;
+
+ gBrowser.goBack();
+ port = yield waitForPort(TEST_URL, false);
+
+ port.sendAsyncMessage("Ping2");
+ let message = yield waitForMessage(port, "Pong2");
+ port.destroy();
+
+ gBrowser.removeCurrentTab();
+});
+
+// Test that opening a page creates a port, sends the load event and then
+// closing the tab sends the unload event
+add_task(function* init_close() {
+ let port = yield waitForPort(TEST_URL);
+ is(port.browser, gBrowser.selectedBrowser, "Port is for the correct browser");
+
+ let unloadPromise = waitForMessage(port, "RemotePage:Unload");
+ gBrowser.removeCurrentTab();
+ yield unloadPromise;
+
+ // Port should be destroyed now
+ try {
+ port.addMessageListener("Foo", failOnMessage);
+ ok(false, "Should have seen exception");
+ }
+ catch (e) {
+ ok(true, "Should have seen exception");
+ }
+
+ try {
+ port.sendAsyncMessage("Foo");
+ ok(false, "Should have seen exception");
+ }
+ catch (e) {
+ ok(true, "Should have seen exception");
+ }
+});
+
+// Tests that we can send messages to individual pages even when more than one
+// is open
+add_task(function* multiple_ports() {
+ let port1 = yield waitForPort(TEST_URL);
+ is(port1.browser, gBrowser.selectedBrowser, "Port is for the correct browser");
+
+ let port2 = yield waitForPort(TEST_URL);
+ is(port2.browser, gBrowser.selectedBrowser, "Port is for the correct browser");
+
+ port2.addMessageListener("Pong", failOnMessage);
+ port1.sendAsyncMessage("Ping", { str: "foobar", counter: 0 });
+ let message = yield waitForMessage(port1, "Pong");
+ port2.removeMessageListener("Pong", failOnMessage);
+ is(message.data.str, "foobar", "String should pass through");
+ is(message.data.counter, 1, "Counter should be incremented");
+
+ port1.addMessageListener("Pong", failOnMessage);
+ port2.sendAsyncMessage("Ping", { str: "foobaz", counter: 5 });
+ message = yield waitForMessage(port2, "Pong");
+ port1.removeMessageListener("Pong", failOnMessage);
+ is(message.data.str, "foobaz", "String should pass through");
+ is(message.data.counter, 6, "Counter should be incremented");
+
+ let unloadPromise = waitForMessage(port2, "RemotePage:Unload");
+ gBrowser.removeTab(gBrowser.getTabForBrowser(port2.browser));
+ yield unloadPromise;
+
+ try {
+ port2.addMessageListener("Pong", failOnMessage);
+ ok(false, "Should not have been able to add a new message listener to a destroyed port.");
+ }
+ catch (e) {
+ ok(true, "Should not have been able to add a new message listener to a destroyed port.");
+ }
+
+ port1.sendAsyncMessage("Ping", { str: "foobar", counter: 0 });
+ message = yield waitForMessage(port1, "Pong");
+ is(message.data.str, "foobar", "String should pass through");
+ is(message.data.counter, 1, "Counter should be incremented");
+
+ unloadPromise = waitForMessage(port1, "RemotePage:Unload");
+ gBrowser.removeTab(gBrowser.getTabForBrowser(port1.browser));
+ yield unloadPromise;
+});
+
+// Tests that swapping browser docshells doesn't break the ports
+add_task(function* browser_switch() {
+ let port1 = yield waitForPort(TEST_URL);
+ is(port1.browser, gBrowser.selectedBrowser, "Port is for the correct browser");
+ let browser1 = gBrowser.selectedBrowser;
+ port1.sendAsyncMessage("SetCookie", { value: "om nom" });
+
+ let port2 = yield waitForPort(TEST_URL);
+ is(port2.browser, gBrowser.selectedBrowser, "Port is for the correct browser");
+ let browser2 = gBrowser.selectedBrowser;
+ port2.sendAsyncMessage("SetCookie", { value: "om nom nom" });
+
+ port2.addMessageListener("Cookie", failOnMessage);
+ port1.sendAsyncMessage("GetCookie");
+ let message = yield waitForMessage(port1, "Cookie");
+ port2.removeMessageListener("Cookie", failOnMessage);
+ is(message.data.value, "om nom", "Should have the right cookie");
+
+ port1.addMessageListener("Cookie", failOnMessage);
+ port2.sendAsyncMessage("GetCookie", { str: "foobaz", counter: 5 });
+ message = yield waitForMessage(port2, "Cookie");
+ port1.removeMessageListener("Cookie", failOnMessage);
+ is(message.data.value, "om nom nom", "Should have the right cookie");
+
+ swapDocShells(browser1, browser2);
+ is(port1.browser, browser2, "Should have noticed the swap");
+ is(port2.browser, browser1, "Should have noticed the swap");
+
+ // Cookies should have stayed the same
+ port2.addMessageListener("Cookie", failOnMessage);
+ port1.sendAsyncMessage("GetCookie");
+ message = yield waitForMessage(port1, "Cookie");
+ port2.removeMessageListener("Cookie", failOnMessage);
+ is(message.data.value, "om nom", "Should have the right cookie");
+
+ port1.addMessageListener("Cookie", failOnMessage);
+ port2.sendAsyncMessage("GetCookie", { str: "foobaz", counter: 5 });
+ message = yield waitForMessage(port2, "Cookie");
+ port1.removeMessageListener("Cookie", failOnMessage);
+ is(message.data.value, "om nom nom", "Should have the right cookie");
+
+ swapDocShells(browser1, browser2);
+ is(port1.browser, browser1, "Should have noticed the swap");
+ is(port2.browser, browser2, "Should have noticed the swap");
+
+ // Cookies should have stayed the same
+ port2.addMessageListener("Cookie", failOnMessage);
+ port1.sendAsyncMessage("GetCookie");
+ message = yield waitForMessage(port1, "Cookie");
+ port2.removeMessageListener("Cookie", failOnMessage);
+ is(message.data.value, "om nom", "Should have the right cookie");
+
+ port1.addMessageListener("Cookie", failOnMessage);
+ port2.sendAsyncMessage("GetCookie", { str: "foobaz", counter: 5 });
+ message = yield waitForMessage(port2, "Cookie");
+ port1.removeMessageListener("Cookie", failOnMessage);
+ is(message.data.value, "om nom nom", "Should have the right cookie");
+
+ let unloadPromise = waitForMessage(port2, "RemotePage:Unload");
+ gBrowser.removeTab(gBrowser.getTabForBrowser(browser2));
+ yield unloadPromise;
+
+ unloadPromise = waitForMessage(port1, "RemotePage:Unload");
+ gBrowser.removeTab(gBrowser.getTabForBrowser(browser1));
+ yield unloadPromise;
+});
+
+// Tests that removeMessageListener in chrome works
+add_task(function* remove_chrome_listener() {
+ let port = yield waitForPort(TEST_URL);
+ is(port.browser, gBrowser.selectedBrowser, "Port is for the correct browser");
+
+ // This relies on messages sent arriving in the same order. Pong will be
+ // sent back before Pong2 so if removeMessageListener fails the test will fail
+ port.addMessageListener("Pong", failOnMessage);
+ port.removeMessageListener("Pong", failOnMessage);
+ port.sendAsyncMessage("Ping", { str: "remove_listener", counter: 27 });
+ port.sendAsyncMessage("Ping2");
+ yield waitForMessage(port, "Pong2");
+
+ let unloadPromise = waitForMessage(port, "RemotePage:Unload");
+ gBrowser.removeCurrentTab();
+ yield unloadPromise;
+});
+
+// Tests that removeMessageListener in content works
+add_task(function* remove_content_listener() {
+ let port = yield waitForPort(TEST_URL);
+ is(port.browser, gBrowser.selectedBrowser, "Port is for the correct browser");
+
+ // This relies on messages sent arriving in the same order. Pong3 would be
+ // sent back before Pong2 so if removeMessageListener fails the test will fail
+ port.addMessageListener("Pong3", failOnMessage);
+ port.sendAsyncMessage("Ping3");
+ port.sendAsyncMessage("Ping2");
+ yield waitForMessage(port, "Pong2");
+
+ let unloadPromise = waitForMessage(port, "RemotePage:Unload");
+ gBrowser.removeCurrentTab();
+ yield unloadPromise;
+});
+
+// Test RemotePages works
+add_task(function* remote_pages_basic() {
+ let pages = new RemotePages(TEST_URL);
+ let port = yield waitForPage(pages);
+ is(port.browser, gBrowser.selectedBrowser, "Port is for the correct browser");
+
+ // Listening to global messages should work
+ let unloadPromise = waitForMessage(pages, "RemotePage:Unload", port);
+ gBrowser.removeCurrentTab();
+ yield unloadPromise;
+
+ pages.destroy();
+
+ // RemotePages should be destroyed now
+ try {
+ pages.addMessageListener("Foo", failOnMessage);
+ ok(false, "Should have seen exception");
+ }
+ catch (e) {
+ ok(true, "Should have seen exception");
+ }
+
+ try {
+ pages.sendAsyncMessage("Foo");
+ ok(false, "Should have seen exception");
+ }
+ catch (e) {
+ ok(true, "Should have seen exception");
+ }
+});
+
+// Test sending messages to all remote pages works
+add_task(function* remote_pages_multiple() {
+ let pages = new RemotePages(TEST_URL);
+ let port1 = yield waitForPage(pages);
+ let port2 = yield waitForPage(pages);
+
+ let pongPorts = [];
+ yield new Promise((resolve) => {
+ function listener({ name, target, data }) {
+ is(name, "Pong", "Should have seen the right response.");
+ is(data.str, "remote_pages", "String should pass through");
+ is(data.counter, 43, "Counter should be incremented");
+ pongPorts.push(target);
+ if (pongPorts.length == 2)
+ resolve();
+ }
+
+ pages.addMessageListener("Pong", listener);
+ pages.sendAsyncMessage("Ping", { str: "remote_pages", counter: 42 });
+ });
+
+ // We don't make any guarantees about which order messages are sent to known
+ // pages so the pongs could have come back in any order.
+ isnot(pongPorts[0], pongPorts[1], "Should have received pongs from different ports");
+ ok(pongPorts.indexOf(port1) >= 0, "Should have seen a pong from port1");
+ ok(pongPorts.indexOf(port2) >= 0, "Should have seen a pong from port2");
+
+ // After destroy we should see no messages
+ pages.addMessageListener("RemotePage:Unload", failOnMessage);
+ pages.destroy();
+
+ gBrowser.removeTab(gBrowser.getTabForBrowser(port1.browser));
+ gBrowser.removeTab(gBrowser.getTabForBrowser(port2.browser));
+});
+
+// Test sending various types of data across the boundary
+add_task(function* send_data() {
+ let port = yield waitForPort(TEST_URL);
+ is(port.browser, gBrowser.selectedBrowser, "Port is for the correct browser");
+
+ let data = {
+ integer: 45,
+ real: 45.78,
+ str: "foobar",
+ array: [1, 2, 3, 5, 27]
+ };
+
+ port.sendAsyncMessage("SendData", data);
+ let message = yield waitForMessage(port, "ReceivedData");
+
+ ok(message.data.result, message.data.status);
+
+ gBrowser.removeCurrentTab();
+});
+
+// Test sending an object of data across the boundary
+add_task(function* send_data2() {
+ let port = yield waitForPort(TEST_URL);
+ is(port.browser, gBrowser.selectedBrowser, "Port is for the correct browser");
+
+ let data = {
+ integer: 45,
+ real: 45.78,
+ str: "foobar",
+ array: [1, 2, 3, 5, 27]
+ };
+
+ port.sendAsyncMessage("SendData2", {data});
+ let message = yield waitForMessage(port, "ReceivedData2");
+
+ ok(message.data.result, message.data.status);
+
+ gBrowser.removeCurrentTab();
+});
+
+add_task(function* get_ports_for_browser() {
+ let pages = new RemotePages(TEST_URL);
+ let port = yield waitForPage(pages);
+ // waitForPage creates a new tab and selects it by default, so
+ // the selected tab should be the one hosting this port.
+ let browser = gBrowser.selectedBrowser;
+ let foundPorts = pages.portsForBrowser(browser);
+ is(foundPorts.length, 1, "There should only be one port for this simple page");
+ is(foundPorts[0], port, "Should find the port");
+ gBrowser.removeCurrentTab();
+});
diff --git a/toolkit/modules/tests/browser/browser_Troubleshoot.js b/toolkit/modules/tests/browser/browser_Troubleshoot.js
new file mode 100644
index 000000000..34c2a2791
--- /dev/null
+++ b/toolkit/modules/tests/browser/browser_Troubleshoot.js
@@ -0,0 +1,546 @@
+/* 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.
+
+Components.utils.import("resource://gre/modules/AppConstants.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/Troubleshoot.jsm");
+
+function test() {
+ waitForExplicitFinish();
+ function doNextTest() {
+ if (!tests.length) {
+ finish();
+ return;
+ }
+ tests.shift()(doNextTest);
+ }
+ doNextTest();
+}
+
+registerCleanupFunction(function () {
+ // Troubleshoot.jsm is imported into the global scope -- the window -- above.
+ // If it's not deleted, it outlives the test and is reported as a leak.
+ delete window.Troubleshoot;
+});
+
+var tests = [
+
+ function snapshotSchema(done) {
+ Troubleshoot.snapshot(function (snapshot) {
+ try {
+ validateObject(snapshot, SNAPSHOT_SCHEMA);
+ ok(true, "The snapshot should conform to the schema.");
+ }
+ catch (err) {
+ ok(false, "Schema mismatch, " + err);
+ }
+ done();
+ });
+ },
+
+ function modifiedPreferences(done) {
+ let prefs = [
+ "javascript.troubleshoot",
+ "troubleshoot.foo",
+ "javascript.print_to_filename",
+ "network.proxy.troubleshoot",
+ ];
+ prefs.forEach(function (p) {
+ Services.prefs.setBoolPref(p, true);
+ is(Services.prefs.getBoolPref(p), true, "The pref should be set: " + p);
+ });
+ Troubleshoot.snapshot(function (snapshot) {
+ let p = snapshot.modifiedPreferences;
+ is(p["javascript.troubleshoot"], true,
+ "The pref should be present because it's whitelisted " +
+ "but not blacklisted.");
+ ok(!("troubleshoot.foo" in p),
+ "The pref should be absent because it's not in the whitelist.");
+ ok(!("javascript.print_to_filename" in p),
+ "The pref should be absent because it's blacklisted.");
+ ok(!("network.proxy.troubleshoot" in p),
+ "The pref should be absent because it's blacklisted.");
+ prefs.forEach(p => Services.prefs.deleteBranch(p));
+ done();
+ });
+ },
+
+ function unicodePreferences(done) {
+ 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);
+
+ Troubleshoot.snapshot(function (snapshot) {
+ let p = snapshot.modifiedPreferences;
+ is(p[name], unicodeValue, "The pref should have correct Unicode value.");
+ Services.prefs.deleteBranch(name);
+ done();
+ });
+ }
+];
+
+// 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",
+ },
+ userAgent: {
+ required: true,
+ type: "string",
+ },
+ osVersion: {
+ required: true,
+ type: "string",
+ },
+ vendor: {
+ type: "string",
+ },
+ updateChannel: {
+ type: "string",
+ },
+ supportURL: {
+ type: "string",
+ },
+ remoteAutoStart: {
+ type: "boolean",
+ required: true,
+ },
+ autoStartStatus: {
+ type: "number",
+ },
+ numTotalWindows: {
+ type: "number",
+ },
+ numRemoteWindows: {
+ type: "number",
+ },
+ safeMode: {
+ type: "boolean",
+ },
+ },
+ },
+ 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",
+ },
+ },
+ },
+ },
+ },
+ },
+ extensions: {
+ required: true,
+ type: "array",
+ items: {
+ type: "object",
+ properties: {
+ name: {
+ required: true,
+ type: "string",
+ },
+ version: {
+ required: true,
+ type: "string",
+ },
+ id: {
+ required: true,
+ type: "string",
+ },
+ isActive: {
+ required: true,
+ type: "boolean",
+ },
+ },
+ },
+ },
+ modifiedPreferences: {
+ required: true,
+ type: "object",
+ },
+ lockedPreferences: {
+ required: true,
+ type: "object",
+ },
+ graphics: {
+ required: true,
+ type: "object",
+ properties: {
+ numTotalWindows: {
+ required: true,
+ type: "number",
+ },
+ numAcceleratedWindows: {
+ required: true,
+ type: "number",
+ },
+ windowLayerManagerType: {
+ type: "string",
+ },
+ windowLayerManagerRemote: {
+ type: "boolean",
+ },
+ supportsHardwareH264: {
+ type: "string",
+ },
+ currentAudioBackend: {
+ type: "string",
+ },
+ numAcceleratedWindowsMessage: {
+ type: "array",
+ },
+ adapterDescription: {
+ type: "string",
+ },
+ adapterVendorID: {
+ type: "string",
+ },
+ adapterDeviceID: {
+ type: "string",
+ },
+ adapterSubsysID: {
+ type: "string",
+ },
+ adapterRAM: {
+ type: "string",
+ },
+ adapterDrivers: {
+ type: "string",
+ },
+ driverVersion: {
+ type: "string",
+ },
+ driverDate: {
+ type: "string",
+ },
+ adapterDescription2: {
+ type: "string",
+ },
+ adapterVendorID2: {
+ type: "string",
+ },
+ adapterDeviceID2: {
+ type: "string",
+ },
+ adapterSubsysID2: {
+ type: "string",
+ },
+ adapterRAM2: {
+ type: "string",
+ },
+ adapterDrivers2: {
+ type: "string",
+ },
+ driverVersion2: {
+ type: "string",
+ },
+ driverDate2: {
+ type: "string",
+ },
+ isGPU2Active: {
+ type: "boolean",
+ },
+ direct2DEnabled: {
+ type: "boolean",
+ },
+ directWriteEnabled: {
+ type: "boolean",
+ },
+ directWriteVersion: {
+ type: "string",
+ },
+ clearTypeParameters: {
+ type: "string",
+ },
+ webglRenderer: {
+ type: "string",
+ },
+ webgl2Renderer: {
+ type: "string",
+ },
+ info: {
+ type: "object",
+ },
+ failures: {
+ type: "array",
+ items: {
+ type: "string",
+ },
+ },
+ indices: {
+ type: "array",
+ items: {
+ type: "number",
+ },
+ },
+ featureLog: {
+ type: "object",
+ },
+ crashGuards: {
+ type: "array",
+ },
+ direct2DEnabledMessage: {
+ type: "array",
+ },
+ },
+ },
+ javaScript: {
+ required: true,
+ type: "object",
+ properties: {
+ incrementalGCEnabled: {
+ type: "boolean",
+ },
+ },
+ },
+ accessibility: {
+ required: true,
+ type: "object",
+ properties: {
+ isActive: {
+ required: true,
+ type: "boolean",
+ },
+ forceDisabled: {
+ type: "number",
+ },
+ },
+ },
+ 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",
+ },
+ },
+ },
+ experiments: {
+ type: "array",
+ },
+ 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_CONTENT_SANDBOX,
+ type: "number"
+ },
+ },
+ },
+ },
+};
+
+/**
+ * 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_WebNavigation.js b/toolkit/modules/tests/browser/browser_WebNavigation.js
new file mode 100644
index 000000000..e09cb1994
--- /dev/null
+++ b/toolkit/modules/tests/browser/browser_WebNavigation.js
@@ -0,0 +1,140 @@
+"use strict";
+
+var { interfaces: Ci, classes: Cc, utils: Cu, results: Cr } = Components;
+
+var {WebNavigation} = Cu.import("resource://gre/modules/WebNavigation.jsm", {});
+
+const BASE = "http://example.com/browser/toolkit/modules/tests/browser";
+const URL = BASE + "/file_WebNavigation_page1.html";
+const FRAME = BASE + "/file_WebNavigation_page2.html";
+const FRAME2 = BASE + "/file_WebNavigation_page3.html";
+
+const EVENTS = [
+ "onBeforeNavigate",
+ "onCommitted",
+ "onDOMContentLoaded",
+ "onCompleted",
+ "onErrorOccurred",
+ "onReferenceFragmentUpdated",
+];
+
+const REQUIRED = [
+ "onBeforeNavigate",
+ "onCommitted",
+ "onDOMContentLoaded",
+ "onCompleted",
+];
+
+var expectedBrowser;
+var received = [];
+var completedResolve;
+var waitingURL, waitingEvent;
+var rootWindowID;
+
+function gotEvent(event, details)
+{
+ if (!details.url.startsWith(BASE)) {
+ return;
+ }
+ info(`Got ${event} ${details.url} ${details.windowId} ${details.parentWindowId}`);
+
+ is(details.browser, expectedBrowser, "correct <browser> element");
+
+ received.push({url: details.url, event});
+
+ if (typeof(rootWindowID) == "undefined") {
+ rootWindowID = details.windowId;
+ }
+
+ if (details.url == URL) {
+ is(details.windowId, rootWindowID, "root window ID correct");
+ } else {
+ is(details.parentWindowId, rootWindowID, "parent window ID correct");
+ isnot(details.windowId, rootWindowID, "window ID probably okay");
+ }
+
+ isnot(details.windowId, undefined);
+ isnot(details.parentWindowId, undefined);
+
+ if (details.url == waitingURL && event == waitingEvent) {
+ completedResolve();
+ }
+}
+
+function loadViaFrameScript(url, event, script)
+{
+ // Loading via a frame script ensures that the chrome process never
+ // "gets ahead" of frame scripts in non-e10s mode.
+ received = [];
+ waitingURL = url;
+ waitingEvent = event;
+ expectedBrowser.messageManager.loadFrameScript("data:," + script, false);
+ return new Promise(resolve => { completedResolve = resolve; });
+}
+
+add_task(function* webnav_ordering() {
+ let listeners = {};
+ for (let event of EVENTS) {
+ listeners[event] = gotEvent.bind(null, event);
+ WebNavigation[event].addListener(listeners[event]);
+ }
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ let browser = gBrowser.selectedBrowser;
+ expectedBrowser = browser;
+
+ yield BrowserTestUtils.browserLoaded(browser);
+
+ yield loadViaFrameScript(URL, "onCompleted", `content.location = "${URL}";`);
+
+ function checkRequired(url) {
+ for (let event of REQUIRED) {
+ let found = false;
+ for (let r of received) {
+ if (r.url == url && r.event == event) {
+ found = true;
+ }
+ }
+ ok(found, `Received event ${event} from ${url}`);
+ }
+ }
+
+ checkRequired(URL);
+ checkRequired(FRAME);
+
+ function checkBefore(action1, action2) {
+ function find(action) {
+ for (let i = 0; i < received.length; i++) {
+ if (received[i].url == action.url && received[i].event == action.event) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ let index1 = find(action1);
+ let index2 = find(action2);
+ ok(index1 != -1, `Action ${JSON.stringify(action1)} happened`);
+ ok(index2 != -1, `Action ${JSON.stringify(action2)} happened`);
+ ok(index1 < index2, `Action ${JSON.stringify(action1)} happened before ${JSON.stringify(action2)}`);
+ }
+
+ checkBefore({url: URL, event: "onCommitted"}, {url: FRAME, event: "onBeforeNavigate"});
+ checkBefore({url: FRAME, event: "onCompleted"}, {url: URL, event: "onCompleted"});
+
+ yield loadViaFrameScript(FRAME2, "onCompleted", `content.frames[0].location = "${FRAME2}";`);
+
+ checkRequired(FRAME2);
+
+ yield loadViaFrameScript(FRAME2 + "#ref", "onReferenceFragmentUpdated",
+ "content.frames[0].document.getElementById('elt').click();");
+
+ info("Received onReferenceFragmentUpdated from FRAME2");
+
+ gBrowser.removeCurrentTab();
+
+ for (let event of EVENTS) {
+ WebNavigation[event].removeListener(listeners[event]);
+ }
+});
+
diff --git a/toolkit/modules/tests/browser/browser_WebRequest.js b/toolkit/modules/tests/browser/browser_WebRequest.js
new file mode 100644
index 000000000..cdb28b16c
--- /dev/null
+++ b/toolkit/modules/tests/browser/browser_WebRequest.js
@@ -0,0 +1,214 @@
+"use strict";
+
+var { interfaces: Ci, classes: Cc, utils: Cu, results: Cr } = Components;
+
+var {WebRequest} = Cu.import("resource://gre/modules/WebRequest.jsm", {});
+
+const BASE = "http://example.com/browser/toolkit/modules/tests/browser";
+const URL = BASE + "/file_WebRequest_page1.html";
+
+var expected_browser;
+
+function checkType(details)
+{
+ let expected_type = "???";
+ if (details.url.indexOf("style") != -1) {
+ expected_type = "stylesheet";
+ } else if (details.url.indexOf("image") != -1) {
+ expected_type = "image";
+ } else if (details.url.indexOf("script") != -1) {
+ expected_type = "script";
+ } else if (details.url.indexOf("page1") != -1) {
+ expected_type = "main_frame";
+ } else if (/page2|_redirection\.|dummy_page/.test(details.url)) {
+ expected_type = "sub_frame";
+ } else if (details.url.indexOf("xhr") != -1) {
+ expected_type = "xmlhttprequest";
+ }
+ is(details.type, expected_type, "resource type is correct");
+}
+
+var windowIDs = new Map();
+
+var requested = [];
+
+function onBeforeRequest(details)
+{
+ info(`onBeforeRequest ${details.url}`);
+ if (details.url.startsWith(BASE)) {
+ requested.push(details.url);
+
+ is(details.browser, expected_browser, "correct <browser> element");
+ checkType(details);
+
+ windowIDs.set(details.url, details.windowId);
+ if (details.url.indexOf("page2") != -1) {
+ let page1id = windowIDs.get(URL);
+ ok(details.windowId != page1id, "sub-frame gets its own window ID");
+ is(details.parentWindowId, page1id, "parent window id is correct");
+ }
+ }
+ if (details.url.indexOf("_bad.") != -1) {
+ return {cancel: true};
+ }
+ return undefined;
+}
+
+var sendHeaders = [];
+
+function onBeforeSendHeaders(details)
+{
+ info(`onBeforeSendHeaders ${details.url}`);
+ if (details.url.startsWith(BASE)) {
+ sendHeaders.push(details.url);
+
+ is(details.browser, expected_browser, "correct <browser> element");
+ checkType(details);
+
+ let id = windowIDs.get(details.url);
+ is(id, details.windowId, "window ID same in onBeforeSendHeaders as onBeforeRequest");
+ }
+ if (details.url.indexOf("_redirect.") != -1) {
+ return {redirectUrl: details.url.replace("_redirect.", "_good.")};
+ }
+ return undefined;
+}
+
+var beforeRedirect = [];
+
+function onBeforeRedirect(details)
+{
+ info(`onBeforeRedirect ${details.url} -> ${details.redirectUrl}`);
+ checkType(details);
+ if (details.url.startsWith(BASE)) {
+ beforeRedirect.push(details.url);
+
+ is(details.browser, expected_browser, "correct <browser> element");
+ checkType(details);
+
+ let expectedUrl = details.url.replace("_redirect.", "_good.").replace(/\w+_redirection\..*/, "dummy_page.html")
+ is(details.redirectUrl, expectedUrl, "Correct redirectUrl value");
+ }
+ let id = windowIDs.get(details.url);
+ is(id, details.windowId, "window ID same in onBeforeRedirect as onBeforeRequest");
+ // associate stored windowId with final url
+ windowIDs.set(details.redirectUrl, details.windowId);
+ return {};
+}
+
+var headersReceived = [];
+
+function onResponseStarted(details)
+{
+ if (details.url.startsWith(BASE)) {
+ headersReceived.push(details.url);
+ }
+}
+
+const expected_requested = [BASE + "/file_WebRequest_page1.html",
+ BASE + "/file_style_good.css",
+ BASE + "/file_style_bad.css",
+ BASE + "/file_style_redirect.css",
+ BASE + "/file_image_good.png",
+ BASE + "/file_image_bad.png",
+ BASE + "/file_image_redirect.png",
+ BASE + "/file_script_good.js",
+ BASE + "/file_script_bad.js",
+ BASE + "/file_script_redirect.js",
+ BASE + "/file_script_xhr.js",
+ BASE + "/file_WebRequest_page2.html",
+ BASE + "/nonexistent_script_url.js",
+ BASE + "/WebRequest_redirection.sjs",
+ BASE + "/dummy_page.html",
+ BASE + "/xhr_resource"];
+
+const expected_sendHeaders = [BASE + "/file_WebRequest_page1.html",
+ BASE + "/file_style_good.css",
+ BASE + "/file_style_redirect.css",
+ BASE + "/file_image_good.png",
+ BASE + "/file_image_redirect.png",
+ BASE + "/file_script_good.js",
+ BASE + "/file_script_redirect.js",
+ BASE + "/file_script_xhr.js",
+ BASE + "/file_WebRequest_page2.html",
+ BASE + "/nonexistent_script_url.js",
+ BASE + "/WebRequest_redirection.sjs",
+ BASE + "/dummy_page.html",
+ BASE + "/xhr_resource"];
+
+const expected_beforeRedirect = expected_sendHeaders.filter(u => /_redirect\./.test(u))
+ .concat(BASE + "/WebRequest_redirection.sjs");
+
+const expected_headersReceived = [BASE + "/file_WebRequest_page1.html",
+ BASE + "/file_style_good.css",
+ BASE + "/file_image_good.png",
+ BASE + "/file_script_good.js",
+ BASE + "/file_script_xhr.js",
+ BASE + "/file_WebRequest_page2.html",
+ BASE + "/nonexistent_script_url.js",
+ BASE + "/dummy_page.html",
+ BASE + "/xhr_resource"];
+
+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`);
+}
+
+function* test_once()
+{
+ WebRequest.onBeforeRequest.addListener(onBeforeRequest, null, ["blocking"]);
+ WebRequest.onBeforeSendHeaders.addListener(onBeforeSendHeaders, null, ["blocking"]);
+ WebRequest.onBeforeRedirect.addListener(onBeforeRedirect);
+ WebRequest.onResponseStarted.addListener(onResponseStarted);
+
+ yield BrowserTestUtils.withNewTab({ gBrowser, url: "about:blank" },
+ function* (browser) {
+ expected_browser = browser;
+ BrowserTestUtils.loadURI(browser, URL);
+ yield BrowserTestUtils.browserLoaded(expected_browser);
+
+ expected_browser = null;
+
+ yield ContentTask.spawn(browser, null, function() {
+ let win = content.wrappedJSObject;
+ is(win.success, 2, "Good script ran");
+ is(win.failure, undefined, "Failure script didn't run");
+
+ let style =
+ content.getComputedStyle(content.document.getElementById("test"), null);
+ is(style.getPropertyValue("color"), "rgb(255, 0, 0)", "Good CSS loaded");
+ });
+ });
+
+ compareLists(requested, expected_requested, "requested");
+ compareLists(sendHeaders, expected_sendHeaders, "sendHeaders");
+ compareLists(beforeRedirect, expected_beforeRedirect, "beforeRedirect");
+ compareLists(headersReceived, expected_headersReceived, "headersReceived");
+
+ WebRequest.onBeforeRequest.removeListener(onBeforeRequest);
+ WebRequest.onBeforeSendHeaders.removeListener(onBeforeSendHeaders);
+ WebRequest.onBeforeRedirect.removeListener(onBeforeRedirect);
+ WebRequest.onResponseStarted.removeListener(onResponseStarted);
+}
+
+// Run the test twice to make sure it works with caching.
+add_task(test_once);
+add_task(test_once);
diff --git a/toolkit/modules/tests/browser/browser_WebRequest_cookies.js b/toolkit/modules/tests/browser/browser_WebRequest_cookies.js
new file mode 100644
index 000000000..b8c4f24cb
--- /dev/null
+++ b/toolkit/modules/tests/browser/browser_WebRequest_cookies.js
@@ -0,0 +1,89 @@
+"use strict";
+
+var { interfaces: Ci, classes: Cc, utils: Cu, results: Cr } = Components;
+
+var {WebRequest} = Cu.import("resource://gre/modules/WebRequest.jsm", {});
+
+const BASE = "http://example.com/browser/toolkit/modules/tests/browser";
+const URL = BASE + "/WebRequest_dynamic.sjs";
+
+var countBefore = 0;
+var countAfter = 0;
+
+function onBeforeSendHeaders(details)
+{
+ if (details.url != URL) {
+ return undefined;
+ }
+
+ countBefore++;
+
+ info(`onBeforeSendHeaders ${details.url}`);
+ let found = false;
+ let headers = [];
+ for (let {name, value} of details.requestHeaders) {
+ info(`Saw header ${name} '${value}'`);
+ if (name == "Cookie") {
+ is(value, "foopy=1", "Cookie is correct");
+ headers.push({name, value: "blinky=1"});
+ found = true;
+ } else {
+ headers.push({name, value});
+ }
+ }
+ ok(found, "Saw cookie header");
+
+ return {requestHeaders: headers};
+}
+
+function onResponseStarted(details)
+{
+ if (details.url != URL) {
+ return;
+ }
+
+ countAfter++;
+
+ info(`onResponseStarted ${details.url}`);
+ let found = false;
+ for (let {name, value} of details.responseHeaders) {
+ info(`Saw header ${name} '${value}'`);
+ if (name == "Set-Cookie") {
+ is(value, "dinky=1", "Cookie is correct");
+ found = true;
+ }
+ }
+ ok(found, "Saw cookie header");
+}
+
+add_task(function* filter_urls() {
+ // First load the URL so that we set cookie foopy=1.
+ gBrowser.selectedTab = gBrowser.addTab(URL);
+ yield waitForLoad();
+ gBrowser.removeCurrentTab();
+
+ // Now load with WebRequest set up.
+ WebRequest.onBeforeSendHeaders.addListener(onBeforeSendHeaders, null, ["blocking"]);
+ WebRequest.onResponseStarted.addListener(onResponseStarted, null);
+
+ gBrowser.selectedTab = gBrowser.addTab(URL);
+
+ yield waitForLoad();
+
+ gBrowser.removeCurrentTab();
+
+ WebRequest.onBeforeSendHeaders.removeListener(onBeforeSendHeaders);
+ WebRequest.onResponseStarted.removeListener(onResponseStarted);
+
+ is(countBefore, 1, "onBeforeSendHeaders hit once");
+ is(countAfter, 1, "onResponseStarted hit once");
+});
+
+function waitForLoad(browser = gBrowser.selectedBrowser) {
+ return new Promise(resolve => {
+ browser.addEventListener("load", function listener() {
+ browser.removeEventListener("load", listener, true);
+ resolve();
+ }, true);
+ });
+}
diff --git a/toolkit/modules/tests/browser/browser_WebRequest_filtering.js b/toolkit/modules/tests/browser/browser_WebRequest_filtering.js
new file mode 100644
index 000000000..a456678c1
--- /dev/null
+++ b/toolkit/modules/tests/browser/browser_WebRequest_filtering.js
@@ -0,0 +1,118 @@
+"use strict";
+
+var { interfaces: Ci, classes: Cc, utils: Cu, results: Cr } = Components;
+
+var {WebRequest} = Cu.import("resource://gre/modules/WebRequest.jsm", {});
+var {MatchPattern} = Cu.import("resource://gre/modules/MatchPattern.jsm", {});
+
+const BASE = "http://example.com/browser/toolkit/modules/tests/browser";
+const URL = BASE + "/file_WebRequest_page2.html";
+
+var requested = [];
+
+function onBeforeRequest(details)
+{
+ info(`onBeforeRequest ${details.url}`);
+ if (details.url.startsWith(BASE)) {
+ requested.push(details.url);
+ }
+}
+
+var sendHeaders = [];
+
+function onBeforeSendHeaders(details)
+{
+ info(`onBeforeSendHeaders ${details.url}`);
+ if (details.url.startsWith(BASE)) {
+ sendHeaders.push(details.url);
+ }
+}
+
+var completed = [];
+
+function onResponseStarted(details)
+{
+ if (details.url.startsWith(BASE)) {
+ completed.push(details.url);
+ }
+}
+
+const expected_urls = [BASE + "/file_style_good.css",
+ BASE + "/file_style_bad.css",
+ BASE + "/file_style_redirect.css"];
+
+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`);
+}
+
+add_task(function* filter_urls() {
+ let filter = {urls: new MatchPattern("*://*/*_style_*")};
+
+ WebRequest.onBeforeRequest.addListener(onBeforeRequest, filter, ["blocking"]);
+ WebRequest.onBeforeSendHeaders.addListener(onBeforeSendHeaders, filter, ["blocking"]);
+ WebRequest.onResponseStarted.addListener(onResponseStarted, filter);
+
+ gBrowser.selectedTab = gBrowser.addTab(URL);
+
+ yield waitForLoad();
+
+ gBrowser.removeCurrentTab();
+
+ compareLists(requested, expected_urls, "requested");
+ compareLists(sendHeaders, expected_urls, "sendHeaders");
+ compareLists(completed, expected_urls, "completed");
+
+ WebRequest.onBeforeRequest.removeListener(onBeforeRequest);
+ WebRequest.onBeforeSendHeaders.removeListener(onBeforeSendHeaders);
+ WebRequest.onResponseStarted.removeListener(onResponseStarted);
+});
+
+add_task(function* filter_types() {
+ let filter = {types: ["stylesheet"]};
+
+ WebRequest.onBeforeRequest.addListener(onBeforeRequest, filter, ["blocking"]);
+ WebRequest.onBeforeSendHeaders.addListener(onBeforeSendHeaders, filter, ["blocking"]);
+ WebRequest.onResponseStarted.addListener(onResponseStarted, filter);
+
+ gBrowser.selectedTab = gBrowser.addTab(URL);
+
+ yield waitForLoad();
+
+ gBrowser.removeCurrentTab();
+
+ compareLists(requested, expected_urls, "requested");
+ compareLists(sendHeaders, expected_urls, "sendHeaders");
+ compareLists(completed, expected_urls, "completed");
+
+ WebRequest.onBeforeRequest.removeListener(onBeforeRequest);
+ WebRequest.onBeforeSendHeaders.removeListener(onBeforeSendHeaders);
+ WebRequest.onResponseStarted.removeListener(onResponseStarted);
+});
+
+function waitForLoad(browser = gBrowser.selectedBrowser) {
+ return new Promise(resolve => {
+ browser.addEventListener("load", function listener() {
+ browser.removeEventListener("load", listener, true);
+ resolve();
+ }, true);
+ });
+}
diff --git a/toolkit/modules/tests/browser/dummy_page.html b/toolkit/modules/tests/browser/dummy_page.html
new file mode 100644
index 000000000..c1c9a4e04
--- /dev/null
+++ b/toolkit/modules/tests/browser/dummy_page.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+
+<html>
+<body>
+<p>Page</p>
+</body>
+</html>
diff --git a/toolkit/modules/tests/browser/file_FinderSample.html b/toolkit/modules/tests/browser/file_FinderSample.html
new file mode 100644
index 000000000..e952d1fe9
--- /dev/null
+++ b/toolkit/modules/tests/browser/file_FinderSample.html
@@ -0,0 +1,824 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Childe Roland</title>
+</head>
+<body>
+<h1>"Childe Roland to the Dark Tower Came"</h1><h5>Robert Browning</h5>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>I.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>My first thought was, he lied in every word,
+<dl>
+<dd>That hoary cripple, with malicious eye</dd>
+<dd>Askance to watch the working of his lie</dd>
+</dl>
+</dd>
+<dd>On mine, and mouth scarce able to afford</dd>
+<dd>Suppression of the glee that pursed and scored
+<dl>
+<dd>Its edge, at one more victim gained thereby.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>II.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>What else should he be set for, with his staff?
+<dl>
+<dd>What, save to waylay with his lies, ensnare</dd>
+<dd>All travellers who might find him posted there,</dd>
+</dl>
+</dd>
+<dd>And ask the road? I guessed what skull-like laugh</dd>
+<dd>Would break, what crutch 'gin write my epitaph
+<dl>
+<dd>For pastime in the dusty thoroughfare,</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>III.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>If at his counsel I should turn aside
+<dl>
+<dd>Into that ominous tract which, all agree,</dd>
+<dd>Hides the Dark Tower. Yet acquiescingly</dd>
+</dl>
+</dd>
+<dd>I did turn as he pointed: neither pride</dd>
+<dd>Nor hope rekindling at the end descried,
+<dl>
+<dd>So much as gladness that some end might be.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>IV.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>For, what with my whole world-wide wandering,
+<dl>
+<dd>What with my search drawn out thro' years, my hope</dd>
+<dd>Dwindled into a ghost not fit to cope</dd>
+</dl>
+</dd>
+<dd>With that obstreperous joy success would bring,</dd>
+<dd>I hardly tried now to rebuke the spring
+<dl>
+<dd>My heart made, finding failure in its scope.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>V.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>As when a sick man very near to death
+<dl>
+<dd>Seems dead indeed, and feels begin and end</dd>
+<dd>The tears and takes the farewell of each friend,</dd>
+</dl>
+</dd>
+<dd>And hears one bid the other go, draw breath</dd>
+<dd>Freelier outside ("since all is o'er," he saith,
+<dl>
+<dd>"And the blow fallen no grieving can amend;")</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>VI.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>While some discuss if near the other graves
+<dl>
+<dd>Be room enough for this, and when a day</dd>
+<dd>Suits best for carrying the corpse away,</dd>
+</dl>
+</dd>
+<dd>With care about the banners, scarves and staves:</dd>
+<dd>And still the man hears all, and only craves
+<dl>
+<dd>He may not shame such tender love and stay.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>VII.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>Thus, I had so long suffered in this quest,
+<dl>
+<dd>Heard failure prophesied so oft, been writ</dd>
+<dd>So many times among "The Band" - to wit,</dd>
+</dl>
+</dd>
+<dd>The knights who to the Dark Tower's search addressed</dd>
+<dd>Their steps - that just to fail as they, seemed best,
+<dl>
+<dd>And all the doubt was now—should I be fit?</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>VIII.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>So, quiet as despair, I turned from him,
+<dl>
+<dd>That hateful cripple, out of his highway</dd>
+<dd>Into the path he pointed. All the day</dd>
+</dl>
+</dd>
+<dd>Had been a dreary one at best, and dim</dd>
+<dd>Was settling to its close, yet shot one grim
+<dl>
+<dd>Red leer to see the plain catch its estray.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>IX.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>For mark! no sooner was I fairly found
+<dl>
+<dd>Pledged to the plain, after a pace or two,</dd>
+<dd>Than, pausing to throw backward a last view</dd>
+</dl>
+</dd>
+<dd>O'er the safe road, 'twas gone; grey plain all round:</dd>
+<dd>Nothing but plain to the horizon's bound.
+<dl>
+<dd>I might go on; nought else remained to do.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>X.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>So, on I went. I think I never saw
+<dl>
+<dd>Such starved ignoble nature; nothing throve:</dd>
+<dd>For flowers - as well expect a cedar grove!</dd>
+</dl>
+</dd>
+<dd>But cockle, spurge, according to their law</dd>
+<dd>Might propagate their kind, with none to awe,
+<dl>
+<dd>You'd think; a burr had been a treasure trove.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XI.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>No! penury, inertness and grimace,
+<dl>
+<dd>In some strange sort, were the land's portion. "See</dd>
+<dd>Or shut your eyes," said Nature peevishly,</dd>
+</dl>
+</dd>
+<dd>"It nothing skills: I cannot help my case:</dd>
+<dd>'Tis the Last Judgment's fire must cure this place,
+<dl>
+<dd>Calcine its clods and set my prisoners free."</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XII.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>If there pushed any ragged thistle-stalk
+<dl>
+<dd>Above its mates, the head was chopped; the bents</dd>
+<dd>Were jealous else. What made those holes and rents</dd>
+</dl>
+</dd>
+<dd>In the dock's harsh swarth leaves, bruised as to baulk</dd>
+<dd>All hope of greenness? 'tis a brute must walk
+<dl>
+<dd>Pashing their life out, with a brute's intents.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XIII.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>As for the grass, it grew as scant as hair
+<dl>
+<dd>In leprosy; thin dry blades pricked the mud</dd>
+<dd>Which underneath looked kneaded up with blood.</dd>
+</dl>
+</dd>
+<dd>One stiff blind horse, his every bone a-stare,</dd>
+<dd>Stood stupefied, however he came there:
+<dl>
+<dd>Thrust out past service from the devil's stud!</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XIV.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>Alive? he might be dead for aught I know,
+<dl>
+<dd>With that red gaunt and colloped neck a-strain,</dd>
+<dd>And shut eyes underneath the rusty mane;</dd>
+</dl>
+</dd>
+<dd>Seldom went such grotesqueness with such woe;</dd>
+<dd>I never saw a brute I hated so;
+<dl>
+<dd>He must be wicked to deserve such pain.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XV.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>I shut my eyes and turned them on my heart.
+<dl>
+<dd>As a man calls for wine before he fights,</dd>
+<dd>I asked one draught of earlier, happier sights,</dd>
+</dl>
+</dd>
+<dd>Ere fitly I could hope to play my part.</dd>
+<dd>Think first, fight afterwards - the soldier's art:
+<dl>
+<dd>One taste of the old time sets all to rights.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XVI.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>Not it! I fancied Cuthbert's reddening face
+<dl>
+<dd>Beneath its garniture of curly gold,</dd>
+<dd>Dear fellow, till I almost felt him fold</dd>
+</dl>
+</dd>
+<dd>An arm in mine to fix me to the place</dd>
+<dd>That way he used. Alas, one night's disgrace!
+<dl>
+<dd>Out went my heart's new fire and left it cold.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XVII.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>Giles then, the soul of honour - there he stands
+<dl>
+<dd>Frank as ten years ago when knighted first.</dd>
+<dd>What honest men should dare (he said) he durst.</dd>
+</dl>
+</dd>
+<dd>Good - but the scene shifts - faugh! what hangman hands</dd>
+<dd>Pin to his breast a parchment? His own bands
+<dl>
+<dd>Read it. Poor traitor, spit upon and curst!</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XVIII.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>Better this present than a past like that;
+<dl>
+<dd>Back therefore to my darkening path again!</dd>
+<dd>No sound, no sight as far as eye could strain.</dd>
+</dl>
+</dd>
+<dd>Will the night send a howlet or a bat?</dd>
+<dd>I asked: when something on the dismal flat
+<dl>
+<dd>Came to arrest my thoughts and change their train.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XIX.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>A sudden little river crossed my path
+<dl>
+<dd>As unexpected as a serpent comes.</dd>
+<dd>No sluggish tide congenial to the glooms;</dd>
+</dl>
+</dd>
+<dd>This, as it frothed by, might have been a bath</dd>
+<dd>For the fiend's glowing hoof - to see the wrath
+<dl>
+<dd>Of its black eddy bespate with flakes and spumes.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XX.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>So petty yet so spiteful! All along
+<dl>
+<dd>Low scrubby alders kneeled down over it;</dd>
+<dd>Drenched willows flung them headlong in a fit</dd>
+</dl>
+</dd>
+<dd>Of mute despair, a suicidal throng:</dd>
+<dd>The river which had done them all the wrong,
+<dl>
+<dd>Whate'er that was, rolled by, deterred no whit.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XXI.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>Which, while I forded, - good saints, how I feared
+<dl>
+<dd>To set my foot upon a dead man's cheek,</dd>
+<dd>Each step, or feel the spear I thrust to seek</dd>
+</dl>
+</dd>
+<dd>For hollows, tangled in his hair or beard!</dd>
+<dd>—It may have been a water-rat I speared,
+<dl>
+<dd>But, ugh! it sounded like a baby's shriek.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XXII.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>Glad was I when I reached the other bank.
+<dl>
+<dd>Now for a better country. Vain presage!</dd>
+<dd>Who were the strugglers, what war did they wage,</dd>
+</dl>
+</dd>
+<dd>Whose savage trample thus could pad the dank</dd>
+<dd>Soil to a plash? Toads in a poisoned tank,
+<dl>
+<dd>Or wild cats in a red-hot iron cage—</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XXIII.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>The fight must so have seemed in that fell cirque.
+<dl>
+<dd>What penned them there, with all the plain to choose?</dd>
+<dd>No foot-print leading to that horrid mews,</dd>
+</dl>
+</dd>
+<dd>None out of it. Mad brewage set to work</dd>
+<dd>Their brains, no doubt, like galley-slaves the Turk
+<dl>
+<dd>Pits for his pastime, Christians against Jews.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XXIV.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>And more than that - a furlong on - why, there!
+<dl>
+<dd>What bad use was that engine for, that wheel,</dd>
+<dd>Or brake, not wheel - that harrow fit to reel</dd>
+</dl>
+</dd>
+<dd>Men's bodies out like silk? with all the air</dd>
+<dd>Of Tophet's tool, on earth left unaware,
+<dl>
+<dd>Or brought to sharpen its rusty teeth of steel.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XXV.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>Then came a bit of stubbed ground, once a wood,
+<dl>
+<dd>Next a marsh, it would seem, and now mere earth</dd>
+<dd>Desperate and done with; (so a fool finds mirth,</dd>
+</dl>
+</dd>
+<dd>Makes a thing and then mars it, till his mood</dd>
+<dd>Changes and off he goes!) within a rood—
+<dl>
+<dd>Bog, clay and rubble, sand and stark black dearth.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XXVI.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>Now blotches rankling, coloured gay and grim,
+<dl>
+<dd>Now patches where some leanness of the soil's</dd>
+<dd>Broke into moss or substances like boils;</dd>
+</dl>
+</dd>
+<dd>Then came some palsied oak, a cleft in him</dd>
+<dd>Like a distorted mouth that splits its rim
+<dl>
+<dd>Gaping at death, and dies while it recoils.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XXVII.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>And just as far as ever from the end!
+<dl>
+<dd>Nought in the distance but the evening, nought</dd>
+<dd>To point my footstep further! At the thought,</dd>
+</dl>
+</dd>
+<dd>A great black bird, Apollyon's bosom-friend,</dd>
+<dd>Sailed past, nor beat his wide wing dragon-penned
+<dl>
+<dd>That brushed my cap—perchance the guide I sought.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XXVIII.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>For, looking up, aware I somehow grew,
+<dl>
+<dd>'Spite of the dusk, the plain had given place</dd>
+<dd>All round to mountains - with such name to grace</dd>
+</dl>
+</dd>
+<dd>Mere ugly heights and heaps now stolen in view.</dd>
+<dd>How thus they had surprised me, - solve it, you!
+<dl>
+<dd>How to get from them was no clearer case.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XXIX.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>Yet half I seemed to recognise some trick
+<dl>
+<dd>Of mischief happened to me, God knows when—</dd>
+<dd>In a bad dream perhaps. Here ended, then,</dd>
+</dl>
+</dd>
+<dd>Progress this way. When, in the very nick</dd>
+<dd>Of giving up, one time more, came a click
+<dl>
+<dd>As when a trap shuts - you're inside the den!</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XXX.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>Burningly it came on me all at once,
+<dl>
+<dd>This was the place! those two hills on the right,</dd>
+<dd>Crouched like two bulls locked horn in horn in fight;</dd>
+</dl>
+</dd>
+<dd>While to the left, a tall scalped mountain... Dunce,</dd>
+<dd>Dotard, a-dozing at the very nonce,
+<dl>
+<dd>After a life spent training for the sight!</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XXXI.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>What in the midst lay but the Tower itself?
+<dl>
+<dd>The round squat turret, blind as the fool's heart</dd>
+<dd>Built of brown stone, without a counterpart</dd>
+</dl>
+</dd>
+<dd>In the whole world. The tempest's mocking elf</dd>
+<dd>Points to the shipman thus the unseen shelf
+<dl>
+<dd>He strikes on, only when the timbers start.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XXXII.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>Not see? because of night perhaps? - why, day
+<dl>
+<dd>Came back again for that! before it left,</dd>
+<dd>The dying sunset kindled through a cleft:</dd>
+</dl>
+</dd>
+<dd>The hills, like giants at a hunting, lay</dd>
+<dd>Chin upon hand, to see the game at bay,—
+<dl>
+<dd>"Now stab and end the creature - to the heft!"</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XXXIII.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>Not hear? when noise was everywhere! it tolled
+<dl>
+<dd>Increasing like a bell. Names in my ears</dd>
+<dd>Of all the lost adventurers my peers,—</dd>
+</dl>
+</dd>
+<dd>How such a one was strong, and such was bold,</dd>
+<dd>And such was fortunate, yet each of old
+<dl>
+<dd>Lost, lost! one moment knelled the woe of years.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XXXIV.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>There they stood, ranged along the hillsides, met
+<dl>
+<dd>To view the last of me, a living frame</dd>
+<dd>For one more picture! in a sheet of flame</dd>
+</dl>
+</dd>
+<dd>I saw them and I knew them all. And yet</dd>
+<dd>Dauntless the slug-horn to my lips I set,
+<dl>
+<dd>And blew "<i>Childe Roland to the Dark Tower came.</i>"</dd>
+</dl>
+</dd>
+</dl>
+</body>
+</html>
diff --git a/toolkit/modules/tests/browser/file_WebNavigation_page1.html b/toolkit/modules/tests/browser/file_WebNavigation_page1.html
new file mode 100644
index 000000000..1b6869756
--- /dev/null
+++ b/toolkit/modules/tests/browser/file_WebNavigation_page1.html
@@ -0,0 +1,9 @@
+<!DOCTYPE HTML>
+
+<html>
+<body>
+
+<iframe src="file_WebNavigation_page2.html" width="200" height="200"></iframe>
+
+</body>
+</html>
diff --git a/toolkit/modules/tests/browser/file_WebNavigation_page2.html b/toolkit/modules/tests/browser/file_WebNavigation_page2.html
new file mode 100644
index 000000000..cc1acc83d
--- /dev/null
+++ b/toolkit/modules/tests/browser/file_WebNavigation_page2.html
@@ -0,0 +1,7 @@
+<!DOCTYPE HTML>
+
+<html>
+<body>
+
+</body>
+</html>
diff --git a/toolkit/modules/tests/browser/file_WebNavigation_page3.html b/toolkit/modules/tests/browser/file_WebNavigation_page3.html
new file mode 100644
index 000000000..a0a26a2e9
--- /dev/null
+++ b/toolkit/modules/tests/browser/file_WebNavigation_page3.html
@@ -0,0 +1,9 @@
+<!DOCTYPE HTML>
+
+<html>
+<body>
+
+<a id="elt" href="file_WebNavigation_page3.html#ref">click me</a>
+
+</body>
+</html>
diff --git a/toolkit/modules/tests/browser/file_WebRequest_page1.html b/toolkit/modules/tests/browser/file_WebRequest_page1.html
new file mode 100644
index 000000000..00a0b9b4b
--- /dev/null
+++ b/toolkit/modules/tests/browser/file_WebRequest_page1.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+<meta charset="utf-8">
+<link rel="stylesheet" href="file_style_good.css">
+<link rel="stylesheet" href="file_style_bad.css">
+<link rel="stylesheet" href="file_style_redirect.css">
+</head>
+<body>
+
+<div id="test">Sample text</div>
+
+<img id="img_good" src="file_image_good.png">
+<img id="img_bad" src="file_image_bad.png">
+<img id="img_redirect" src="file_image_redirect.png">
+
+<script src="file_script_good.js"></script>
+<script src="file_script_bad.js"></script>
+<script src="file_script_redirect.js"></script>
+
+<script src="file_script_xhr.js"></script>
+
+<script src="nonexistent_script_url.js"></script>
+
+<iframe src="file_WebRequest_page2.html" width="200" height="200"></iframe>
+<iframe src="WebRequest_redirection.sjs" width="200" height="50"></iframe>
+</body>
+</html>
diff --git a/toolkit/modules/tests/browser/file_WebRequest_page2.html b/toolkit/modules/tests/browser/file_WebRequest_page2.html
new file mode 100644
index 000000000..b2cf48f9e
--- /dev/null
+++ b/toolkit/modules/tests/browser/file_WebRequest_page2.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+<meta charset="utf-8">
+<link rel="stylesheet" href="file_style_good.css">
+<link rel="stylesheet" href="file_style_bad.css">
+<link rel="stylesheet" href="file_style_redirect.css">
+</head>
+<body>
+
+<div class="test">Sample text</div>
+
+<img id="img_good" src="file_image_good.png">
+<img id="img_bad" src="file_image_bad.png">
+<img id="img_redirect" src="file_image_redirect.png">
+
+<script src="file_script_good.js"></script>
+<script src="file_script_bad.js"></script>
+<script src="file_script_redirect.js"></script>
+
+<script src="nonexistent_script_url.js"></script>
+
+</body>
+</html>
diff --git a/toolkit/modules/tests/browser/file_image_bad.png b/toolkit/modules/tests/browser/file_image_bad.png
new file mode 100644
index 000000000..4c3be5084
--- /dev/null
+++ b/toolkit/modules/tests/browser/file_image_bad.png
Binary files differ
diff --git a/toolkit/modules/tests/browser/file_image_good.png b/toolkit/modules/tests/browser/file_image_good.png
new file mode 100644
index 000000000..769c63634
--- /dev/null
+++ b/toolkit/modules/tests/browser/file_image_good.png
Binary files differ
diff --git a/toolkit/modules/tests/browser/file_image_redirect.png b/toolkit/modules/tests/browser/file_image_redirect.png
new file mode 100644
index 000000000..4c3be5084
--- /dev/null
+++ b/toolkit/modules/tests/browser/file_image_redirect.png
Binary files differ
diff --git a/toolkit/modules/tests/browser/file_script_bad.js b/toolkit/modules/tests/browser/file_script_bad.js
new file mode 100644
index 000000000..90655f136
--- /dev/null
+++ b/toolkit/modules/tests/browser/file_script_bad.js
@@ -0,0 +1 @@
+window.failure = true;
diff --git a/toolkit/modules/tests/browser/file_script_good.js b/toolkit/modules/tests/browser/file_script_good.js
new file mode 100644
index 000000000..b128e54a1
--- /dev/null
+++ b/toolkit/modules/tests/browser/file_script_good.js
@@ -0,0 +1 @@
+window.success = window.success ? window.success + 1 : 1;
diff --git a/toolkit/modules/tests/browser/file_script_redirect.js b/toolkit/modules/tests/browser/file_script_redirect.js
new file mode 100644
index 000000000..917b5d620
--- /dev/null
+++ b/toolkit/modules/tests/browser/file_script_redirect.js
@@ -0,0 +1,2 @@
+window.failure = true;
+
diff --git a/toolkit/modules/tests/browser/file_script_xhr.js b/toolkit/modules/tests/browser/file_script_xhr.js
new file mode 100644
index 000000000..bc1f65eae
--- /dev/null
+++ b/toolkit/modules/tests/browser/file_script_xhr.js
@@ -0,0 +1,3 @@
+var request = new XMLHttpRequest();
+request.open("get", "http://example.com/browser/toolkit/modules/tests/browser/xhr_resource", false);
+request.send();
diff --git a/toolkit/modules/tests/browser/file_style_bad.css b/toolkit/modules/tests/browser/file_style_bad.css
new file mode 100644
index 000000000..8dbc8dc7a
--- /dev/null
+++ b/toolkit/modules/tests/browser/file_style_bad.css
@@ -0,0 +1,3 @@
+#test {
+ color: green !important;
+}
diff --git a/toolkit/modules/tests/browser/file_style_good.css b/toolkit/modules/tests/browser/file_style_good.css
new file mode 100644
index 000000000..46f9774b5
--- /dev/null
+++ b/toolkit/modules/tests/browser/file_style_good.css
@@ -0,0 +1,3 @@
+#test {
+ color: red;
+}
diff --git a/toolkit/modules/tests/browser/file_style_redirect.css b/toolkit/modules/tests/browser/file_style_redirect.css
new file mode 100644
index 000000000..8dbc8dc7a
--- /dev/null
+++ b/toolkit/modules/tests/browser/file_style_redirect.css
@@ -0,0 +1,3 @@
+#test {
+ color: green !important;
+}
diff --git a/toolkit/modules/tests/browser/head.js b/toolkit/modules/tests/browser/head.js
new file mode 100644
index 000000000..777e087e1
--- /dev/null
+++ b/toolkit/modules/tests/browser/head.js
@@ -0,0 +1,23 @@
+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`);
+}
+
diff --git a/toolkit/modules/tests/browser/metadata_simple.html b/toolkit/modules/tests/browser/metadata_simple.html
new file mode 100644
index 000000000..18089e399
--- /dev/null
+++ b/toolkit/modules/tests/browser/metadata_simple.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Test Title</title>
+ <meta property="description" content="A very simple test page">
+ </head>
+ <body>
+ Llama.
+ </body>
+</html>
diff --git a/toolkit/modules/tests/browser/metadata_titles.html b/toolkit/modules/tests/browser/metadata_titles.html
new file mode 100644
index 000000000..bd4201304
--- /dev/null
+++ b/toolkit/modules/tests/browser/metadata_titles.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Test Titles</title>
+ <meta property="description" content="A very simple test page" />
+ <meta property="og:title" content="Title" />
+ </head>
+ <body>
+ Llama.
+ </body>
+</html>
diff --git a/toolkit/modules/tests/browser/metadata_titles_fallback.html b/toolkit/modules/tests/browser/metadata_titles_fallback.html
new file mode 100644
index 000000000..5b71879b2
--- /dev/null
+++ b/toolkit/modules/tests/browser/metadata_titles_fallback.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta property="description" content="A very simple test page" />
+ <meta property="og:title" content="Title" />
+ </head>
+ <body>
+ Llama.
+ </body>
+</html>
diff --git a/toolkit/modules/tests/browser/testremotepagemanager.html b/toolkit/modules/tests/browser/testremotepagemanager.html
new file mode 100644
index 000000000..4303a38f5
--- /dev/null
+++ b/toolkit/modules/tests/browser/testremotepagemanager.html
@@ -0,0 +1,66 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+<script type="text/javascript">
+addMessageListener("Ping", function(message) {
+ sendAsyncMessage("Pong", {
+ str: message.data.str,
+ counter: message.data.counter + 1
+ });
+});
+
+addMessageListener("Ping2", function(message) {
+ sendAsyncMessage("Pong2", message.data);
+});
+
+function neverCalled() {
+ sendAsyncMessage("Pong3");
+}
+addMessageListener("Pong3", neverCalled);
+removeMessageListener("Pong3", neverCalled);
+
+function testData(data) {
+ var response = {
+ result: true,
+ status: "All data correctly received"
+ }
+
+ function compare(prop, expected) {
+ if (uneval(data[prop]) == uneval(expected))
+ return;
+ if (response.result)
+ response.status = "";
+ response.result = false;
+ response.status += "Property " + prop + " should have been " + expected + " but was " + data[prop] + "\n";
+ }
+
+ compare("integer", 45);
+ compare("real", 45.78);
+ compare("str", "foobar");
+ compare("array", [1, 2, 3, 5, 27]);
+
+ return response;
+}
+
+addMessageListener("SendData", function(message) {
+ sendAsyncMessage("ReceivedData", testData(message.data));
+});
+
+addMessageListener("SendData2", function(message) {
+ sendAsyncMessage("ReceivedData2", testData(message.data.data));
+});
+
+var cookie = "nom";
+addMessageListener("SetCookie", function(message) {
+ cookie = message.data.value;
+});
+
+addMessageListener("GetCookie", function(message) {
+ sendAsyncMessage("Cookie", { value: cookie });
+});
+</script>
+</head>
+<body>
+</body>
+</html>