diff options
Diffstat (limited to 'addon-sdk/source/test/addons/e10s-content/lib/test-content-worker.js')
-rw-r--r-- | addon-sdk/source/test/addons/e10s-content/lib/test-content-worker.js | 1127 |
1 files changed, 1127 insertions, 0 deletions
diff --git a/addon-sdk/source/test/addons/e10s-content/lib/test-content-worker.js b/addon-sdk/source/test/addons/e10s-content/lib/test-content-worker.js new file mode 100644 index 000000000..5eddb826a --- /dev/null +++ b/addon-sdk/source/test/addons/e10s-content/lib/test-content-worker.js @@ -0,0 +1,1127 @@ +/* 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(); + } + }); + + 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: win => { + events.push('attach'); + assert.pass('attach event called when attached'); + assert.equal(window, win, 'attach event passes in attached window'); + }, + 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; + }) + }, + }); + 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) { \"use strict\"; 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); |