summaryrefslogtreecommitdiffstats
path: root/addon-sdk/source/test/test-content-worker.js
diff options
context:
space:
mode:
Diffstat (limited to 'addon-sdk/source/test/test-content-worker.js')
-rw-r--r--addon-sdk/source/test/test-content-worker.js1129
1 files changed, 1129 insertions, 0 deletions
diff --git a/addon-sdk/source/test/test-content-worker.js b/addon-sdk/source/test/test-content-worker.js
new file mode 100644
index 000000000..16c9436b3
--- /dev/null
+++ b/addon-sdk/source/test/test-content-worker.js
@@ -0,0 +1,1129 @@
+/* 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";
+
+// Skipping due to window creation being unsupported in Fennec
+module.metadata = {
+ engines: {
+ 'Firefox': '*'
+ }
+};
+
+const { Cc, Ci } = require("chrome");
+const { on } = require("sdk/event/core");
+const { setTimeout } = require("sdk/timers");
+const { LoaderWithHookedConsole, Loader } = require("sdk/test/loader");
+const { Worker } = require("sdk/content/worker");
+const { close } = require("sdk/window/helpers");
+const { set: setPref } = require("sdk/preferences/service");
+const { isArray } = require("sdk/lang/type");
+const { URL } = require('sdk/url');
+const fixtures = require("./fixtures");
+const system = require("sdk/system/events");
+
+const DEPRECATE_PREF = "devtools.errorconsole.deprecation_warnings";
+
+const DEFAULT_CONTENT_URL = "data:text/html;charset=utf-8,foo";
+
+const WINDOW_SCRIPT_URL = "data:text/html;charset=utf-8," +
+ "<script>window.addEventListener('message', function (e) {" +
+ " if (e.data === 'from -> content-script')" +
+ " window.postMessage('from -> window', '*');" +
+ "});</script>";
+
+function makeWindow() {
+ let content =
+ "<?xml version=\"1.0\"?>" +
+ "<window " +
+ "xmlns=\"http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul\">" +
+ "<script>var documentValue=true;</script>" +
+ "</window>";
+ var url = "data:application/vnd.mozilla.xul+xml;charset=utf-8," +
+ encodeURIComponent(content);
+ var features = ["chrome", "width=10", "height=10"];
+
+ return Cc["@mozilla.org/embedcomp/window-watcher;1"].
+ getService(Ci.nsIWindowWatcher).
+ openWindow(null, url, null, features.join(","), null);
+}
+
+// Listen for only first one occurence of DOM event
+function listenOnce(node, eventName, callback) {
+ node.addEventListener(eventName, function onevent(event) {
+ node.removeEventListener(eventName, onevent, true);
+ callback(node);
+ }, true);
+}
+
+// Load a given url in a given browser and fires the callback when it is loaded
+function loadAndWait(browser, url, callback) {
+ listenOnce(browser, "load", callback);
+ // We have to wait before calling `loadURI` otherwise, if we call
+ // `loadAndWait` during browser load event, the history will be broken
+ setTimeout(function () {
+ browser.loadURI(url);
+ }, 0);
+}
+
+// Returns a test function that will automatically open a new chrome window
+// with a <browser> element loaded on a given content URL
+// The callback receive 3 arguments:
+// - test: reference to the jetpack test object
+// - browser: a reference to the <browser> xul node
+// - done: a callback to call when test is over
+function WorkerTest(url, callback) {
+ return function testFunction(assert, done) {
+ let chromeWindow = makeWindow();
+ chromeWindow.addEventListener("load", function onload() {
+ chromeWindow.removeEventListener("load", onload, true);
+ let browser = chromeWindow.document.createElement("browser");
+ browser.setAttribute("type", "content");
+ chromeWindow.document.documentElement.appendChild(browser);
+ // Wait for about:blank load event ...
+ listenOnce(browser, "load", function onAboutBlankLoad() {
+ // ... before loading the expected doc and waiting for its load event
+ loadAndWait(browser, url, function onDocumentLoaded() {
+ callback(assert, browser, function onTestDone() {
+
+ close(chromeWindow).then(done);
+ });
+ });
+ });
+ }, true);
+ };
+}
+
+exports["test:sample"] = WorkerTest(
+ DEFAULT_CONTENT_URL,
+ function(assert, browser, done) {
+
+ assert.notEqual(browser.contentWindow.location.href, "about:blank",
+ "window is now on the right document");
+
+ let window = browser.contentWindow
+ let worker = Worker({
+ window: window,
+ contentScript: "new " + function WorkerScope() {
+ // window is accessible
+ let myLocation = window.location.toString();
+ self.on("message", function(data) {
+ if (data == "hi!")
+ self.postMessage("bye!");
+ });
+ },
+ contentScriptWhen: "ready",
+ onMessage: function(msg) {
+ assert.equal("bye!", msg);
+ assert.equal(worker.url, window.location.href,
+ "worker.url still works");
+ done();
+ },
+ onAttach: function() {
+ assert.equal(worker.url, window.location.href,
+ "worker.url works");
+ assert.equal(worker.contentURL, window.location.href,
+ "worker.contentURL works");
+ worker.postMessage("hi!");
+ }
+ });
+ }
+);
+
+exports["test:emit"] = WorkerTest(
+ DEFAULT_CONTENT_URL,
+ function(assert, browser, done) {
+
+ let worker = Worker({
+ window: browser.contentWindow,
+ contentScript: "new " + function WorkerScope() {
+ // Validate self.on and self.emit
+ self.port.on("addon-to-content", function (data) {
+ self.port.emit("content-to-addon", data);
+ });
+
+ // Check for global pollution
+ //if (typeof on != "undefined")
+ // self.postMessage("`on` is in globals");
+ if (typeof once != "undefined")
+ self.postMessage("`once` is in globals");
+ if (typeof emit != "undefined")
+ self.postMessage("`emit` is in globals");
+
+ },
+ onMessage: function(msg) {
+ assert.fail("Got an unexpected message : "+msg);
+ }
+ });
+
+ // Validate worker.port
+ worker.port.on("content-to-addon", function (data) {
+ assert.equal(data, "event data");
+ done();
+ });
+ worker.port.emit("addon-to-content", "event data");
+ }
+);
+
+exports["test:emit hack message"] = WorkerTest(
+ DEFAULT_CONTENT_URL,
+ function(assert, browser, done) {
+ let worker = Worker({
+ window: browser.contentWindow,
+ contentScript: "new " + function WorkerScope() {
+ // Validate self.port
+ self.port.on("message", function (data) {
+ self.port.emit("message", data);
+ });
+ // We should not receive message on self, but only on self.port
+ self.on("message", function (data) {
+ self.postMessage("message", data);
+ });
+ },
+ onError: function(e) {
+ assert.fail("Got exception: "+e);
+ }
+ });
+
+ worker.port.on("message", function (data) {
+ assert.equal(data, "event data");
+ done();
+ });
+ worker.on("message", function (data) {
+ assert.fail("Got an unexpected message : "+msg);
+ });
+ worker.port.emit("message", "event data");
+ }
+);
+
+exports["test:n-arguments emit"] = WorkerTest(
+ DEFAULT_CONTENT_URL,
+ function(assert, browser, done) {
+ let repeat = 0;
+ let worker = Worker({
+ window: browser.contentWindow,
+ contentScript: "new " + function WorkerScope() {
+ // Validate self.on and self.emit
+ self.port.on("addon-to-content", function (a1, a2, a3) {
+ self.port.emit("content-to-addon", a1, a2, a3);
+ });
+ }
+ });
+
+ // Validate worker.port
+ worker.port.on("content-to-addon", function (arg1, arg2, arg3) {
+ if (!repeat++) {
+ this.emit("addon-to-content", "first argument", "second", "third");
+ } else {
+ assert.equal(arg1, "first argument");
+ assert.equal(arg2, "second");
+ assert.equal(arg3, "third");
+ done();
+ }
+ });
+ worker.port.emit("addon-to-content", "first argument", "second", "third");
+ }
+);
+
+exports["test:post-json-values-only"] = WorkerTest(
+ DEFAULT_CONTENT_URL,
+ function(assert, browser, done) {
+
+ let worker = Worker({
+ window: browser.contentWindow,
+ contentScript: "new " + function WorkerScope() {
+ self.on("message", function (message) {
+ self.postMessage([ message.fun === undefined,
+ typeof message.w,
+ message.w && "port" in message.w,
+ message.w._url,
+ Array.isArray(message.array),
+ JSON.stringify(message.array)]);
+ });
+ }
+ });
+
+ // Validate worker.onMessage
+ let array = [1, 2, 3];
+ worker.on("message", function (message) {
+ assert.ok(message[0], "function becomes undefined");
+ assert.equal(message[1], "object", "object stays object");
+ assert.ok(message[2], "object's attributes are enumerable");
+ assert.equal(message[3], DEFAULT_CONTENT_URL,
+ "jsonable attributes are accessible");
+ // See bug 714891, Arrays may be broken over compartements:
+ assert.ok(message[4], "Array keeps being an array");
+ assert.equal(message[5], JSON.stringify(array),
+ "Array is correctly serialized");
+ done();
+ });
+ // Add a new url property sa the Class function used by
+ // Worker doesn't set enumerables to true for non-functions
+ worker._url = DEFAULT_CONTENT_URL;
+
+ worker.postMessage({ fun: function () {}, w: worker, array: array });
+ }
+);
+
+exports["test:emit-json-values-only"] = WorkerTest(
+ DEFAULT_CONTENT_URL,
+ function(assert, browser, done) {
+
+ let worker = Worker({
+ window: browser.contentWindow,
+ contentScript: "new " + function WorkerScope() {
+ // Validate self.on and self.emit
+ self.port.on("addon-to-content", function (fun, w, obj, array) {
+ self.port.emit("content-to-addon", [
+ fun === null,
+ typeof w,
+ "port" in w,
+ w._url,
+ "fun" in obj,
+ Object.keys(obj.dom).length,
+ Array.isArray(array),
+ JSON.stringify(array)
+ ]);
+ });
+ }
+ });
+
+ // Validate worker.port
+ let array = [1, 2, 3];
+ worker.port.on("content-to-addon", function (result) {
+ assert.ok(result[0], "functions become null");
+ assert.equal(result[1], "object", "objects stay objects");
+ assert.ok(result[2], "object's attributes are enumerable");
+ assert.equal(result[3], DEFAULT_CONTENT_URL,
+ "json attribute is accessible");
+ assert.ok(!result[4], "function as object attribute is removed");
+ assert.equal(result[5], 0, "DOM nodes are converted into empty object");
+ // See bug 714891, Arrays may be broken over compartments:
+ assert.ok(result[6], "Array keeps being an array");
+ assert.equal(result[7], JSON.stringify(array),
+ "Array is correctly serialized");
+ done();
+ });
+
+ let obj = {
+ fun: function () {},
+ dom: browser.contentWindow.document.createElement("div")
+ };
+ // Add a new url property sa the Class function used by
+ // Worker doesn't set enumerables to true for non-functions
+ worker._url = DEFAULT_CONTENT_URL;
+ worker.port.emit("addon-to-content", function () {}, worker, obj, array);
+ }
+);
+
+exports["test:content is wrapped"] = WorkerTest(
+ "data:text/html;charset=utf-8,<script>var documentValue=true;</script>",
+ function(assert, browser, done) {
+
+ let worker = Worker({
+ window: browser.contentWindow,
+ contentScript: "new " + function WorkerScope() {
+ self.postMessage(!window.documentValue);
+ },
+ contentScriptWhen: "ready",
+ onMessage: function(msg) {
+ assert.ok(msg,
+ "content script has a wrapped access to content document");
+ done();
+ }
+ });
+ }
+);
+
+// ContentWorker is not for chrome
+/*
+exports["test:chrome is unwrapped"] = function(assert, done) {
+ let window = makeWindow();
+
+ listenOnce(window, "load", function onload() {
+
+ let worker = Worker({
+ window: window,
+ contentScript: "new " + function WorkerScope() {
+ self.postMessage(window.documentValue);
+ },
+ contentScriptWhen: "ready",
+ onMessage: function(msg) {
+ assert.ok(msg,
+ "content script has an unwrapped access to chrome document");
+ close(window).then(done);
+ }
+ });
+
+ });
+}
+*/
+
+exports["test:nothing is leaked to content script"] = WorkerTest(
+ DEFAULT_CONTENT_URL,
+ function(assert, browser, done) {
+
+ let worker = Worker({
+ window: browser.contentWindow,
+ contentScript: "new " + function WorkerScope() {
+ self.postMessage([
+ "ContentWorker" in window,
+ "UNWRAP_ACCESS_KEY" in window,
+ "getProxyForObject" in window
+ ]);
+ },
+ contentScriptWhen: "ready",
+ onMessage: function(list) {
+ assert.ok(!list[0], "worker API contrustor isn't leaked");
+ assert.ok(!list[1], "Proxy API stuff isn't leaked 1/2");
+ assert.ok(!list[2], "Proxy API stuff isn't leaked 2/2");
+ done();
+ }
+ });
+ }
+);
+
+exports["test:ensure console.xxx works in cs"] = WorkerTest(
+ DEFAULT_CONTENT_URL,
+ function(assert, browser, done) {
+ const EXPECTED = ["time", "log", "info", "warn", "error", "error", "timeEnd"];
+
+ let calls = [];
+ let levels = [];
+
+ system.on('console-api-log-event', onMessage);
+
+ function onMessage({ subject }) {
+ calls.push(subject.wrappedJSObject.arguments[0]);
+ levels.push(subject.wrappedJSObject.level);
+ }
+
+ let worker = Worker({
+ window: browser.contentWindow,
+ contentScript: "new " + function WorkerScope() {
+ console.time("time");
+ console.log("log");
+ console.info("info");
+ console.warn("warn");
+ console.error("error");
+ console.debug("debug");
+ console.exception("error");
+ console.timeEnd("timeEnd");
+ self.postMessage();
+ },
+ onMessage: function() {
+ system.off('console-api-log-event', onMessage);
+
+ assert.equal(JSON.stringify(calls),
+ JSON.stringify(EXPECTED),
+ "console methods have been called successfully, in expected order");
+
+ assert.equal(JSON.stringify(levels),
+ JSON.stringify(EXPECTED),
+ "console messages have correct log levels, in expected order");
+
+ done();
+ }
+ });
+ }
+);
+
+exports["test:setTimeout works with string argument"] = WorkerTest(
+ "data:text/html;charset=utf-8,<script>var docVal=5;</script>",
+ function(assert, browser, done) {
+ let worker = Worker({
+ window: browser.contentWindow,
+ contentScript: "new " + function ContentScriptScope() {
+ // must use "window.scVal" instead of "var csVal"
+ // since we are inside ContentScriptScope function.
+ // i'm NOT putting code-in-string inside code-in-string </YO DAWG>
+ window.csVal = 13;
+ setTimeout("self.postMessage([" +
+ "csVal, " +
+ "window.docVal, " +
+ "'ContentWorker' in window, " +
+ "'UNWRAP_ACCESS_KEY' in window, " +
+ "'getProxyForObject' in window, " +
+ "])", 1);
+ },
+ contentScriptWhen: "ready",
+ onMessage: function([csVal, docVal, chrome1, chrome2, chrome3]) {
+ // test timer code is executed in the correct context
+ assert.equal(csVal, 13, "accessing content-script values");
+ assert.notEqual(docVal, 5, "can't access document values (directly)");
+ assert.ok(!chrome1 && !chrome2 && !chrome3, "nothing is leaked from chrome");
+ done();
+ }
+ });
+ }
+);
+
+exports["test:setInterval works with string argument"] = WorkerTest(
+ DEFAULT_CONTENT_URL,
+ function(assert, browser, done) {
+ let count = 0;
+ let worker = Worker({
+ window: browser.contentWindow,
+ contentScript: "setInterval('self.postMessage(1)', 50)",
+ contentScriptWhen: "ready",
+ onMessage: function(one) {
+ count++;
+ assert.equal(one, 1, "got " + count + " message(s) from setInterval");
+ if (count >= 3) done();
+ }
+ });
+ }
+);
+
+exports["test:setInterval async Errors passed to .onError"] = WorkerTest(
+ DEFAULT_CONTENT_URL,
+ function(assert, browser, done) {
+ let count = 0;
+ let worker = Worker({
+ window: browser.contentWindow,
+ contentScript: "setInterval(() => { throw Error('ubik') }, 50)",
+ contentScriptWhen: "ready",
+ onError: function(err) {
+ count++;
+ assert.equal(err.message, "ubik",
+ "error (correctly) propagated " + count + " time(s)");
+ if (count >= 3) done();
+ }
+ });
+ }
+);
+
+exports["test:setTimeout throws array, passed to .onError"] = WorkerTest(
+ DEFAULT_CONTENT_URL,
+ function(assert, browser, done) {
+ let worker = Worker({
+ window: browser.contentWindow,
+ contentScript: "setTimeout(function() { throw ['array', 42] }, 1)",
+ contentScriptWhen: "ready",
+ onError: function(arr) {
+ assert.ok(isArray(arr),
+ "the type of thrown/propagated object is array");
+ assert.ok(arr.length==2,
+ "the propagated thrown array is the right length");
+ assert.equal(arr[1], 42,
+ "element inside the thrown array correctly propagated");
+ done();
+ }
+ });
+ }
+);
+
+exports["test:setTimeout string arg with SyntaxError to .onError"] = WorkerTest(
+ DEFAULT_CONTENT_URL,
+ function(assert, browser, done) {
+ let worker = Worker({
+ window: browser.contentWindow,
+ contentScript: "setTimeout('syntax 123 error', 1)",
+ contentScriptWhen: "ready",
+ onError: function(err) {
+ assert.equal(err.name, "SyntaxError",
+ "received SyntaxError thrown from bad code in string argument to setTimeout");
+ assert.ok('fileName' in err,
+ "propagated SyntaxError contains a fileName property");
+ assert.ok('stack' in err,
+ "propagated SyntaxError contains a stack property");
+ assert.equal(err.message, "missing ; before statement",
+ "propagated SyntaxError has the correct (helpful) message");
+ assert.equal(err.lineNumber, 1,
+ "propagated SyntaxError was thrown on the right lineNumber");
+ done();
+ }
+ });
+ }
+);
+
+exports["test:setTimeout can't be cancelled by content"] = WorkerTest(
+ "data:text/html;charset=utf-8,<script>var documentValue=true;</script>",
+ function(assert, browser, done) {
+
+ let worker = Worker({
+ window: browser.contentWindow,
+ contentScript: "new " + function WorkerScope() {
+ let id = setTimeout(function () {
+ self.postMessage("timeout");
+ }, 100);
+ unsafeWindow.eval("clearTimeout("+id+");");
+ },
+ contentScriptWhen: "ready",
+ onMessage: function(msg) {
+ assert.ok(msg,
+ "content didn't managed to cancel our setTimeout");
+ done();
+ }
+ });
+ }
+);
+
+exports["test:clearTimeout"] = WorkerTest(
+ "data:text/html;charset=utf-8,clear timeout",
+ function(assert, browser, done) {
+ let worker = Worker({
+ window: browser.contentWindow,
+ contentScript: "new " + function WorkerScope() {
+ let id1 = setTimeout(function() {
+ self.postMessage("failed");
+ }, 10);
+ let id2 = setTimeout(function() {
+ self.postMessage("done");
+ }, 100);
+ clearTimeout(id1);
+ },
+ contentScriptWhen: "ready",
+ onMessage: function(msg) {
+ if (msg === "failed") {
+ assert.fail("failed to cancel timer");
+ } else {
+ assert.pass("timer cancelled");
+ done();
+ }
+ }
+ });
+ }
+);
+
+exports["test:clearInterval"] = WorkerTest(
+ "data:text/html;charset=utf-8,clear timeout",
+ function(assert, browser, done) {
+ let called = 0;
+ let worker = Worker({
+ window: browser.contentWindow,
+ contentScript: "new " + function WorkerScope() {
+ let id = setInterval(function() {
+ self.postMessage("intreval")
+ clearInterval(id)
+ setTimeout(function() {
+ self.postMessage("done")
+ }, 100)
+ }, 10);
+ },
+ contentScriptWhen: "ready",
+ onMessage: function(msg) {
+ if (msg === "intreval") {
+ called = called + 1;
+ if (called > 1) assert.fail("failed to cancel timer");
+ } else {
+ assert.pass("interval cancelled");
+ done();
+ }
+ }
+ });
+ }
+)
+
+exports["test:setTimeout are unregistered on content unload"] = WorkerTest(
+ DEFAULT_CONTENT_URL,
+ function(assert, browser, done) {
+
+ let originalWindow = browser.contentWindow;
+ let worker = Worker({
+ window: browser.contentWindow,
+ contentScript: "new " + function WorkerScope() {
+ document.title = "ok";
+ let i = 0;
+ setInterval(function () {
+ document.title = i++;
+ }, 10);
+ },
+ contentScriptWhen: "ready"
+ });
+
+ // Change location so that content script is destroyed,
+ // and all setTimeout/setInterval should be unregistered.
+ // Wait some cycles in order to execute some intervals.
+ setTimeout(function () {
+ // Bug 689621: Wait for the new document load so that we are sure that
+ // previous document cancelled its intervals
+ let url2 = "data:text/html;charset=utf-8,<title>final</title>";
+ loadAndWait(browser, url2, function onload() {
+ let titleAfterLoad = originalWindow.document.title;
+ // Wait additional cycles to verify that intervals are really cancelled
+ setTimeout(function () {
+ assert.equal(browser.contentDocument.title, "final",
+ "New document has not been modified");
+ assert.equal(originalWindow.document.title, titleAfterLoad,
+ "Nor previous one");
+
+ done();
+ }, 100);
+ });
+ }, 100);
+ }
+);
+
+exports['test:check window attribute in iframes'] = WorkerTest(
+ DEFAULT_CONTENT_URL,
+ function(assert, browser, done) {
+
+ // Create a first iframe and wait for its loading
+ let contentWin = browser.contentWindow;
+ let contentDoc = contentWin.document;
+ let iframe = contentDoc.createElement("iframe");
+ contentDoc.body.appendChild(iframe);
+
+ listenOnce(iframe, "load", function onload() {
+
+ // Create a second iframe inside the first one and wait for its loading
+ let iframeDoc = iframe.contentWindow.document;
+ let subIframe = iframeDoc.createElement("iframe");
+ iframeDoc.body.appendChild(subIframe);
+
+ listenOnce(subIframe, "load", function onload() {
+ subIframe.removeEventListener("load", onload, true);
+
+ // And finally create a worker against this second iframe
+ let worker = Worker({
+ window: subIframe.contentWindow,
+ contentScript: 'new ' + function WorkerScope() {
+ self.postMessage([
+ window.top !== window,
+ frameElement,
+ window.parent !== window,
+ top.location.href,
+ parent.location.href,
+ ]);
+ },
+ onMessage: function(msg) {
+ assert.ok(msg[0], "window.top != window");
+ assert.ok(msg[1], "window.frameElement is defined");
+ assert.ok(msg[2], "window.parent != window");
+ assert.equal(msg[3], contentWin.location.href,
+ "top.location refers to the toplevel content doc");
+ assert.equal(msg[4], iframe.contentWindow.location.href,
+ "parent.location refers to the first iframe doc");
+ done();
+ }
+ });
+
+ });
+ subIframe.setAttribute("src", "data:text/html;charset=utf-8,bar");
+
+ });
+ iframe.setAttribute("src", "data:text/html;charset=utf-8,foo");
+ }
+);
+
+exports['test:check window attribute in toplevel documents'] = WorkerTest(
+ DEFAULT_CONTENT_URL,
+ function(assert, browser, done) {
+
+ let worker = Worker({
+ window: browser.contentWindow,
+ contentScript: 'new ' + function WorkerScope() {
+ self.postMessage([
+ window.top === window,
+ frameElement,
+ window.parent === window
+ ]);
+ },
+ onMessage: function(msg) {
+ assert.ok(msg[0], "window.top == window");
+ assert.ok(!msg[1], "window.frameElement is null");
+ assert.ok(msg[2], "window.parent == window");
+ done();
+ }
+ });
+ }
+);
+
+exports["test:check worker API with page history"] = WorkerTest(
+ DEFAULT_CONTENT_URL,
+ function(assert, browser, done) {
+ let url2 = "data:text/html;charset=utf-8,bar";
+
+ loadAndWait(browser, url2, function () {
+ let worker = Worker({
+ window: browser.contentWindow,
+ contentScript: "new " + function WorkerScope() {
+ // Just before the content script is disable, we register a timeout
+ // that will be disable until the page gets visible again
+ self.on("pagehide", function () {
+ setTimeout(function () {
+ self.port.emit("timeout");
+ }, 0);
+ });
+
+ self.on("message", function() {
+ self.postMessage("saw message");
+ });
+
+ self.on("event", function() {
+ self.port.emit("event", "saw event");
+ });
+ },
+ contentScriptWhen: "start"
+ });
+
+ // postMessage works correctly when the page is visible
+ worker.postMessage("ok");
+
+ // We have to wait before going back into history,
+ // otherwise `goBack` won't do anything.
+ setTimeout(function () {
+ browser.goBack();
+ }, 0);
+
+ // Wait for the document to be hidden
+ browser.addEventListener("pagehide", function onpagehide() {
+ browser.removeEventListener("pagehide", onpagehide, false);
+ // Now any event sent to this worker should be cached
+
+ worker.postMessage("message");
+ worker.port.emit("event");
+
+ // Display the page with attached content script back in order to resume
+ // its timeout and receive the expected message.
+ // We have to delay this in order to not break the history.
+ // We delay for a non-zero amount of time in order to ensure that we
+ // do not receive the message immediatly, so that the timeout is
+ // actually disabled
+ setTimeout(function () {
+ worker.on("pageshow", function() {
+ let promise = Promise.all([
+ new Promise(resolve => {
+ worker.port.on("event", () => {
+ assert.pass("Saw event");
+ resolve();
+ });
+ }),
+ new Promise(resolve => {
+ worker.on("message", () => {
+ assert.pass("Saw message");
+ resolve();
+ });
+ }),
+ new Promise(resolve => {
+ worker.port.on("timeout", () => {
+ assert.pass("Timer fired");
+ resolve();
+ });
+ })
+ ]);
+ promise.then(done);
+ });
+
+ browser.goForward();
+ }, 500);
+
+ }, false);
+ });
+
+ }
+);
+
+exports['test:conentScriptFile as URL instance'] = WorkerTest(
+ DEFAULT_CONTENT_URL,
+ function(assert, browser, done) {
+
+ let url = new URL(fixtures.url("test-contentScriptFile.js"));
+ let worker = Worker({
+ window: browser.contentWindow,
+ contentScriptFile: url,
+ onMessage: function(msg) {
+ assert.equal(msg, "msg from contentScriptFile",
+ "received a wrong message from contentScriptFile");
+ done();
+ }
+ });
+ }
+);
+
+exports["test:worker events"] = WorkerTest(
+ DEFAULT_CONTENT_URL,
+ function (assert, browser, done) {
+ let window = browser.contentWindow;
+ let events = [];
+ let worker = Worker({
+ window: window,
+ contentScript: 'new ' + function WorkerScope() {
+ self.postMessage('start');
+ },
+ onAttach: () => {
+ events.push('attach');
+ assert.pass('attach event called when attached');
+ },
+ onError: err => {
+ assert.equal(err.message, 'Custom',
+ 'Error passed into error event');
+ worker.detach();
+ },
+ onMessage: msg => {
+ assert.pass('`onMessage` handles postMessage')
+ throw new Error('Custom');
+ },
+ onDetach: _ => {
+ assert.pass('`onDetach` called when worker detached');
+ done();
+ }
+ });
+ // `attach` event is called synchronously during instantiation,
+ // so we can't listen to that, TODO FIX?
+ // worker.on('attach', obj => console.log('attach', obj));
+ }
+);
+
+exports["test:onDetach in contentScript on destroy"] = WorkerTest(
+ "data:text/html;charset=utf-8,foo#detach",
+ function(assert, browser, done) {
+ let worker = Worker({
+ window: browser.contentWindow,
+ contentScript: 'new ' + function WorkerScope() {
+ self.port.on('detach', function(reason) {
+ window.location.hash += '!' + reason;
+ })
+ },
+
+ onAttach: function() {
+ browser.contentWindow.addEventListener('hashchange', _ => {
+ assert.equal(browser.contentWindow.location.hash, '#detach!',
+ "location.href is as expected");
+ done();
+ })
+ worker.destroy();
+ }
+ });
+ }
+);
+
+exports["test:onDetach in contentScript on unload"] = WorkerTest(
+ "data:text/html;charset=utf-8,foo#detach",
+ function(assert, browser, done) {
+ let { loader } = LoaderWithHookedConsole(module);
+ let worker = loader.require("sdk/content/worker").Worker({
+ window: browser.contentWindow,
+ contentScript: 'new ' + function WorkerScope() {
+ self.port.on('detach', function(reason) {
+ window.location.hash += '!' + reason;
+ })
+ },
+ });
+ browser.contentWindow.addEventListener('hashchange', _ => {
+ assert.equal(browser.contentWindow.location.hash, '#detach!shutdown',
+ "location.href is as expected");
+ done();
+ })
+ loader.unload('shutdown');
+ }
+);
+
+exports["test:console method log functions properly"] = WorkerTest(
+ DEFAULT_CONTENT_URL,
+ function(assert, browser, done) {
+ let logs = [];
+
+ system.on('console-api-log-event', onMessage);
+
+ function onMessage({ subject }) {
+ logs.push(clean(subject.wrappedJSObject.arguments[0]));
+ }
+
+ let clean = message =>
+ message.trim().
+ replace(/[\r\n]/g, " ").
+ replace(/ +/g, " ");
+
+ let worker = Worker({
+ window: browser.contentWindow,
+ contentScript: "new " + function WorkerScope() {
+ console.log(Function);
+ console.log((foo) => foo * foo);
+ console.log(function foo(bar) { return bar + bar });
+
+ self.postMessage();
+ },
+ onMessage: () => {
+ system.off('console-api-log-event', onMessage);
+
+ assert.deepEqual(logs, [
+ "function Function() { [native code] }",
+ "(foo) => foo * foo",
+ "function foo(bar) { return bar + bar }"
+ ]);
+
+ done();
+ }
+ });
+ }
+);
+
+exports["test:global postMessage"] = WorkerTest(
+ WINDOW_SCRIPT_URL,
+ function(assert, browser, done) {
+ let contentScript = "window.addEventListener('message', function (e) {" +
+ " if (e.data === 'from -> window')" +
+ " self.port.emit('response', e.data, e.origin);" +
+ "});" +
+ "postMessage('from -> content-script', '*');";
+ let { loader } = LoaderWithHookedConsole(module);
+ let worker = loader.require("sdk/content/worker").Worker({
+ window: browser.contentWindow,
+ contentScriptWhen: "ready",
+ contentScript: contentScript
+ });
+
+ worker.port.on("response", (data, origin) => {
+ assert.equal(data, "from -> window", "Communication from content-script to window completed");
+ done();
+ });
+});
+
+exports["test:destroy unbinds listeners from port"] = WorkerTest(
+ "data:text/html;charset=utf-8,portdestroyer",
+ function(assert, browser, done) {
+ let destroyed = false;
+ let worker = Worker({
+ window: browser.contentWindow,
+ contentScript: "new " + function WorkerScope() {
+ self.port.emit("destroy");
+ setInterval(self.port.emit, 10, "ping");
+ },
+ onDestroy: done
+ });
+ worker.port.on("ping", () => {
+ if (destroyed) {
+ assert.fail("Should not call events on port after destroy.");
+ }
+ });
+ worker.port.on("destroy", () => {
+ destroyed = true;
+ worker.destroy();
+ assert.pass("Worker destroyed, waiting for no future listeners handling events.");
+ setTimeout(done, 500);
+ });
+ }
+);
+
+exports["test:destroy kills child worker"] = WorkerTest(
+ "data:text/html;charset=utf-8,<html><body><p id='detail'></p></body></html>",
+ function(assert, browser, done) {
+ let worker1 = Worker({
+ window: browser.contentWindow,
+ contentScript: "new " + function WorkerScope() {
+ self.port.on("ping", detail => {
+ let event = document.createEvent("CustomEvent");
+ event.initCustomEvent("Test:Ping", true, true, detail);
+ document.dispatchEvent(event);
+ self.port.emit("pingsent");
+ });
+
+ let listener = function(event) {
+ self.port.emit("pong", event.detail);
+ };
+
+ self.port.on("detach", () => {
+ window.removeEventListener("Test:Pong", listener);
+ });
+ window.addEventListener("Test:Pong", listener);
+ },
+ onAttach: function() {
+ let worker2 = Worker({
+ window: browser.contentWindow,
+ contentScript: "new " + function WorkerScope() {
+ let listener = function(event) {
+ let newEvent = document.createEvent("CustomEvent");
+ newEvent.initCustomEvent("Test:Pong", true, true, event.detail);
+ document.dispatchEvent(newEvent);
+ };
+ self.port.on("detach", () => {
+ window.removeEventListener("Test:Ping", listener);
+ })
+ window.addEventListener("Test:Ping", listener);
+ self.postMessage();
+ },
+ onMessage: function() {
+ worker1.port.emit("ping", "test1");
+ worker1.port.once("pong", detail => {
+ assert.equal(detail, "test1", "Saw the right message");
+ worker1.port.once("pingsent", () => {
+ assert.pass("The message was sent");
+
+ worker2.destroy();
+
+ worker1.port.emit("ping", "test2");
+ worker1.port.once("pong", detail => {
+ assert.fail("worker2 shouldn't have responded");
+ })
+ worker1.port.once("pingsent", () => {
+ assert.pass("The message was sent");
+ worker1.destroy();
+ done();
+ });
+ });
+ })
+ }
+ });
+ }
+ });
+ }
+);
+
+exports["test:unload kills child worker"] = WorkerTest(
+ "data:text/html;charset=utf-8,<html><body><p id='detail'></p></body></html>",
+ function(assert, browser, done) {
+ let loader = Loader(module);
+ let worker1 = Worker({
+ window: browser.contentWindow,
+ contentScript: "new " + function WorkerScope() {
+ self.port.on("ping", detail => {
+ let event = document.createEvent("CustomEvent");
+ event.initCustomEvent("Test:Ping", true, true, detail);
+ document.dispatchEvent(event);
+ self.port.emit("pingsent");
+ });
+
+ let listener = function(event) {
+ self.port.emit("pong", event.detail);
+ };
+
+ self.port.on("detach", () => {
+ window.removeEventListener("Test:Pong", listener);
+ });
+ window.addEventListener("Test:Pong", listener);
+ },
+ onAttach: function() {
+ let worker2 = loader.require("sdk/content/worker").Worker({
+ window: browser.contentWindow,
+ contentScript: "new " + function WorkerScope() {
+ let listener = function(event) {
+ let newEvent = document.createEvent("CustomEvent");
+ newEvent.initCustomEvent("Test:Pong", true, true, event.detail);
+ document.dispatchEvent(newEvent);
+ };
+ self.port.on("detach", () => {
+ window.removeEventListener("Test:Ping", listener);
+ })
+ window.addEventListener("Test:Ping", listener);
+ self.postMessage();
+ },
+ onMessage: function() {
+ worker1.port.emit("ping", "test1");
+ worker1.port.once("pong", detail => {
+ assert.equal(detail, "test1", "Saw the right message");
+ worker1.port.once("pingsent", () => {
+ assert.pass("The message was sent");
+
+ loader.unload();
+
+ worker1.port.emit("ping", "test2");
+ worker1.port.once("pong", detail => {
+ assert.fail("worker2 shouldn't have responded");
+ })
+ worker1.port.once("pingsent", () => {
+ assert.pass("The message was sent");
+ worker1.destroy();
+ done();
+ });
+ });
+ })
+ }
+ });
+ }
+ });
+ }
+);
+
+require("sdk/test").run(exports);