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