/* 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"; const { Cc, Ci, Cu } = require("chrome"); const { PageMod } = require("sdk/page-mod"); const { testPageMod, handleReadyState, openNewTab, contentScriptWhenServer, createLoader } = require("./page-mod/helpers"); const { Loader } = require("sdk/test/loader"); const tabs = require("sdk/tabs"); const { setTimeout } = require("sdk/timers"); const system = require("sdk/system/events"); const { open, getFrames, getMostRecentBrowserWindow, getInnerId } = require("sdk/window/utils"); const { getTabContentWindow, getActiveTab, setTabURL, openTab, closeTab, getBrowserForTab } = require("sdk/tabs/utils"); const xulApp = require("sdk/system/xul-app"); const { isPrivateBrowsingSupported } = require("sdk/self"); const { isPrivate } = require("sdk/private-browsing"); const { openWebpage } = require("./private-browsing/helper"); const { isTabPBSupported, isWindowPBSupported } = require("sdk/private-browsing/utils"); const promise = require("sdk/core/promise"); const { pb } = require("./private-browsing/helper"); const { URL } = require("sdk/url"); const { defer, all, resolve } = require("sdk/core/promise"); const { waitUntil } = require("sdk/test/utils"); const data = require("./fixtures"); const { cleanUI, after } = require("sdk/test/utils"); const testPageURI = data.url("test.html"); function Isolate(worker) { return "(" + worker + ")()"; } /* Tests for the PageMod APIs */ exports.testPageMod1 = function*(assert) { let modAttached = defer(); let mod = PageMod({ include: /about:/, contentScriptWhen: "end", contentScript: "new " + function WorkerScope() { window.document.body.setAttribute("JEP-107", "worked"); self.port.once("done", () => { self.port.emit("results", window.document.body.getAttribute("JEP-107")) }); }, onAttach: function(worker) { assert.equal(this, mod, "The 'this' object is the page mod."); mod.port.once("results", modAttached.resolve) mod.port.emit("done"); } }); let tab = yield new Promise(resolve => { tabs.open({ url: "about:", inBackground: true, onReady: resolve }) }); assert.pass("test tab was opened."); let worked = yield modAttached.promise; assert.pass("test mod was attached."); mod.destroy(); assert.pass("test mod was destroyed."); assert.equal(worked, "worked", "PageMod.onReady test"); }; exports.testPageMod2 = function*(assert) { let modAttached = defer(); let mod = PageMod({ include: testPageURI, contentScriptWhen: "end", contentScript: [ 'new ' + function contentScript() { window.AUQLUE = function() { return 42; } try { window.AUQLUE() } catch(e) { throw new Error("PageMod scripts executed in order"); } document.documentElement.setAttribute("first", "true"); }, 'new ' + function contentScript() { document.documentElement.setAttribute("second", "true"); self.port.once("done", () => { self.port.emit("results", { "first": window.document.documentElement.getAttribute("first"), "second": window.document.documentElement.getAttribute("second"), "AUQLUE": unsafeWindow.getAUQLUE() }); }); } ], onAttach: modAttached.resolve }); let tab = yield new Promise(resolve => { tabs.open({ url: testPageURI, inBackground: true, onReady: resolve }) }); assert.pass("test tab was opened."); let worker = yield modAttached.promise; assert.pass("test mod was attached."); let results = yield new Promise(resolve => { worker.port.once("results", resolve) worker.port.emit("done"); }); mod.destroy(); assert.pass("test mod was destroyed."); assert.equal(results["first"], "true", "PageMod test #2: first script has run"); assert.equal(results["second"], "true", "PageMod test #2: second script has run"); assert.equal(results["AUQLUE"], false, "PageMod test #2: scripts get a wrapped window"); }; exports.testPageModIncludes = function*(assert) { var modsAttached = []; var modNumber = 0; var modAttached = defer(); let includes = [ "*", "*.google.com", "resource:*", "resource:", testPageURI ]; let expected = [ false, false, true, false, true ] let mod = PageMod({ include: testPageURI, contentScript: 'new ' + function() { self.port.on("get-local-storage", () => { let result = {}; self.options.forEach(include => { result[include] = !!window.localStorage[include] }); self.port.emit("got-local-storage", result); window.localStorage.clear(); }); }, contentScriptOptions: includes, onAttach: modAttached.resolve }); function createPageModTest(include, expectedMatch) { var modIndex = modNumber++; let attached = defer(); modsAttached.push(expectedMatch ? attached.promise : resolve()); // ...and corresponding PageMod options return PageMod({ include: include, contentScript: 'new ' + function() { self.on("message", function(msg) { window.localStorage[msg] = true self.port.emit('done'); }); }, // The testPageMod callback with test assertions is called on 'end', // and we want this page mod to be attached before it gets called, // so we attach it on 'start'. contentScriptWhen: 'start', onAttach: function(worker) { assert.pass("mod " + modIndex + " was attached"); worker.port.once("done", () => { assert.pass("mod " + modIndex + " is done"); attached.resolve(worker); }); worker.postMessage(this.include[0]); } }); } let mods = [ createPageModTest("*", false), createPageModTest("*.google.com", false), createPageModTest("resource:*", true), createPageModTest("resource:", false), createPageModTest(testPageURI, true) ]; let tab = yield new Promise(resolve => { tabs.open({ url: testPageURI, inBackground: true, onReady: resolve }); }); assert.pass("tab was opened"); yield all(modsAttached); assert.pass("all mods were attached."); mods.forEach(mod => mod.destroy()); assert.pass("all mods were destroyed."); yield modAttached.promise; assert.pass("final test mod was attached."); yield new Promise(resolve => { mod.port.on("got-local-storage", (storage) => { includes.forEach((include, i) => { assert.equal(storage[include], expected[i], "localStorage is correct for " + include); }); resolve(); }); mod.port.emit("get-local-storage"); }); assert.pass("final test of localStorage is complete."); mod.destroy(); assert.pass("final test mod was destroyed."); }; exports.testPageModExcludes = function(assert, done) { var asserts = []; function createPageModTest(include, exclude, expectedMatch) { // Create an 'onload' test function... asserts.push(function(test, win) { var matches = JSON.stringify([include, exclude]) in win.localStorage; assert.ok(expectedMatch ? matches : !matches, "[include, exclude] = [" + include + ", " + exclude + "] match test, expected: " + expectedMatch); }); // ...and corresponding PageMod options return { include: include, exclude: exclude, contentScript: 'new ' + function() { self.on("message", function(msg) { // The key in localStorage is "[, ]". window.localStorage[JSON.stringify(msg)] = true; }); }, // The testPageMod callback with test assertions is called on 'end', // and we want this page mod to be attached before it gets called, // so we attach it on 'start'. contentScriptWhen: 'start', onAttach: function(worker) { worker.postMessage([this.include[0], this.exclude[0]]); } }; } testPageMod(assert, done, testPageURI, [ createPageModTest("*", testPageURI, false), createPageModTest(testPageURI, testPageURI, false), createPageModTest(testPageURI, "resource://*", false), createPageModTest(testPageURI, "*.google.com", true) ], function (win, done) { waitUntil(() => win.localStorage[JSON.stringify([testPageURI, "*.google.com"])], testPageURI + " page-mod to be executed") .then(() => { asserts.forEach(fn => fn(assert, win)); win.localStorage.clear(); done(); }); }); }; exports.testPageModValidationAttachTo = function(assert) { [{ val: 'top', type: 'string "top"' }, { val: 'frame', type: 'string "frame"' }, { val: ['top', 'existing'], type: 'array with "top" and "existing"' }, { val: ['frame', 'existing'], type: 'array with "frame" and "existing"' }, { val: ['top'], type: 'array with "top"' }, { val: ['frame'], type: 'array with "frame"' }, { val: undefined, type: 'undefined' }].forEach((attachTo) => { new PageMod({ attachTo: attachTo.val, include: '*.validation111' }); assert.pass("PageMod() does not throw when attachTo is " + attachTo.type); }); [{ val: 'existing', type: 'string "existing"' }, { val: ['existing'], type: 'array with "existing"' }, { val: 'not-legit', type: 'string with "not-legit"' }, { val: ['not-legit'], type: 'array with "not-legit"' }, { val: {}, type: 'object' }].forEach((attachTo) => { assert.throws(() => new PageMod({ attachTo: attachTo.val, include: '*.validation111' }), /The `attachTo` option/, "PageMod() throws when 'attachTo' option is " + attachTo.type + "."); }); }; exports.testPageModValidationInclude = function(assert) { [{ val: undefined, type: 'undefined' }, { val: {}, type: 'object' }, { val: [], type: 'empty array'}, { val: [/regexp/, 1], type: 'array with non string/regexp' }, { val: 1, type: 'number' }].forEach((include) => { assert.throws(() => new PageMod({ include: include.val }), /The `include` option must always contain atleast one rule/, "PageMod() throws when 'include' option is " + include.type + "."); }); [{ val: '*.validation111', type: 'string' }, { val: /validation111/, type: 'regexp' }, { val: ['*.validation111'], type: 'array with length > 0'}].forEach((include) => { new PageMod({ include: include.val }); assert.pass("PageMod() does not throw when include option is " + include.type); }); }; exports.testPageModValidationExclude = function(assert) { let includeVal = '*.validation111'; [{ val: {}, type: 'object' }, { val: [], type: 'empty array'}, { val: [/regexp/, 1], type: 'array with non string/regexp' }, { val: 1, type: 'number' }].forEach((exclude) => { assert.throws(() => new PageMod({ include: includeVal, exclude: exclude.val }), /If set, the `exclude` option must always contain at least one rule as a string, regular expression, or an array of strings and regular expressions./, "PageMod() throws when 'exclude' option is " + exclude.type + "."); }); [{ val: undefined, type: 'undefined' }, { val: '*.validation111', type: 'string' }, { val: /validation111/, type: 'regexp' }, { val: ['*.validation111'], type: 'array with length > 0'}].forEach((exclude) => { new PageMod({ include: includeVal, exclude: exclude.val }); assert.pass("PageMod() does not throw when exclude option is " + exclude.type); }); }; /* Tests for internal functions. */ exports.testCommunication1 = function*(assert) { let workerDone = defer(); let mod = PageMod({ include: "about:*", contentScriptWhen: "end", contentScript: 'new ' + function WorkerScope() { self.on('message', function(msg) { document.body.setAttribute('JEP-107', 'worked'); self.postMessage(document.body.getAttribute('JEP-107')); }); self.port.on('get-jep-107', () => { self.port.emit('got-jep-107', document.body.getAttribute('JEP-107')); }); }, onAttach: function(worker) { worker.on('error', function(e) { assert.fail('Errors where reported'); }); worker.on('message', function(value) { assert.equal( "worked", value, "test comunication" ); workerDone.resolve(); }); worker.postMessage("do it!") } }); let tab = yield new Promise(resolve => { tabs.open({ url: "about:", onReady: resolve }); }); assert.pass("opened tab"); yield workerDone.promise; assert.pass("the worker has made a change"); let value = yield new Promise(resolve => { mod.port.once("got-jep-107", resolve); mod.port.emit("get-jep-107"); }); assert.equal("worked", value, "attribute should be modified"); mod.destroy(); assert.pass("the worker was destroyed"); }; exports.testCommunication2 = function*(assert) { let workerDone = defer(); let url = data.url("test.html"); let mod = PageMod({ include: url, contentScriptWhen: 'start', contentScript: 'new ' + function WorkerScope() { document.documentElement.setAttribute('AUQLUE', 42); window.addEventListener('load', function listener() { self.postMessage({ msg: 'onload', AUQLUE: document.documentElement.getAttribute('AUQLUE') }); }, false); self.on("message", function(msg) { if (msg == "get window.test") { unsafeWindow.changesInWindow(); } self.postMessage({ msg: document.documentElement.getAttribute("test") }); }); }, onAttach: function(worker) { worker.on('error', function(e) { assert.fail('Errors where reported'); }); worker.on('message', function({ msg, AUQLUE }) { if ('onload' == msg) { assert.equal('42', AUQLUE, 'PageMod scripts executed in order'); worker.postMessage('get window.test'); } else { assert.equal('changes in window', msg, 'PageMod test #2: second script has run'); workerDone.resolve(); } }); } }); let tab = yield new Promise(resolve => { tabs.open({ url: url, inBackground: true, onReady: resolve }); }); assert.pass("opened tab"); yield workerDone.promise; mod.destroy(); assert.pass("the worker was destroyed"); }; exports.testEventEmitter = function(assert, done) { let workerDone = false, callbackDone = null; testPageMod(assert, done, "about:", [{ include: "about:*", contentScript: 'new ' + function WorkerScope() { self.port.on('addon-to-content', function(data) { self.port.emit('content-to-addon', data); }); }, onAttach: function(worker) { worker.on('error', function(e) { assert.fail('Errors were reported : '+e); }); worker.port.on('content-to-addon', function(value) { assert.equal( "worked", value, "EventEmitter API works!" ); if (callbackDone) callbackDone(); else workerDone = true; }); worker.port.emit('addon-to-content', 'worked'); } }], function(win, done) { if (workerDone) done(); else callbackDone = done; } ); }; // Execute two concurrent page mods on same document to ensure that their // JS contexts are different exports.testMixedContext = function(assert, done) { let doneCallback = null; let messages = 0; let modObject = { include: "data:text/html;charset=utf-8,", contentScript: 'new ' + function WorkerScope() { // Both scripts will execute this, // context is shared if one script see the other one modification. let isContextShared = "sharedAttribute" in document; self.postMessage(isContextShared); document.sharedAttribute = true; }, onAttach: function(w) { w.on("message", function (isContextShared) { if (isContextShared) { assert.fail("Page mod contexts are mixed."); doneCallback(); } else if (++messages == 2) { assert.pass("Page mod contexts are different."); doneCallback(); } }); } }; testPageMod(assert, done, "data:text/html;charset=utf-8,", [modObject, modObject], function(win, done) { doneCallback = done; } ); }; exports.testHistory = function(assert, done) { // We need a valid url in order to have a working History API. // (i.e do not work on data: or about: pages) // Test bug 679054. let url = data.url("test-page-mod.html"); let callbackDone = null; testPageMod(assert, done, url, [{ include: url, contentScriptWhen: 'end', contentScript: 'new ' + function WorkerScope() { history.pushState({}, "", "#"); history.replaceState({foo: "bar"}, "", "#"); self.postMessage(history.state); }, onAttach: function(worker) { worker.on('message', function (data) { assert.equal(JSON.stringify(data), JSON.stringify({foo: "bar"}), "History API works!"); callbackDone(); }); } }], function(win, done) { callbackDone = done; } ); }; exports.testRelatedTab = function(assert, done) { let tab; let pageMod = new PageMod({ include: "about:*", onAttach: function(worker) { assert.ok(!!worker.tab, "Worker.tab exists"); assert.equal(tab, worker.tab, "Worker.tab is valid"); pageMod.destroy(); tab.close(done); } }); tabs.open({ url: "about:", onOpen: function onOpen(t) { tab = t; } }); }; // related to bug #989288 // https://bugzilla.mozilla.org/show_bug.cgi?id=989288 exports.testRelatedTabNewWindow = function(assert, done) { let url = "about:logo" let pageMod = new PageMod({ include: url, onAttach: function(worker) { assert.equal(worker.tab.url, url, "Worker.tab.url is valid"); worker.tab.close(done); } }); tabs.activeTab.attach({ contentScript: "window.open('about:logo', '', " + "'width=800,height=600,resizable=no,status=no,location=no');" }); }; exports.testRelatedTabNoRequireTab = function(assert, done) { let loader = Loader(module); let tab; let url = "data:text/html;charset=utf-8," + encodeURI("Test related worker tab 2"); let { PageMod } = loader.require("sdk/page-mod"); let pageMod = new PageMod({ include: url, onAttach: function(worker) { assert.equal(worker.tab.url, url, "Worker.tab.url is valid"); worker.tab.close(function() { pageMod.destroy(); loader.unload(); done(); }); } }); tabs.open(url); }; exports.testRelatedTabNoOtherReqs = function(assert, done) { let loader = Loader(module); let { PageMod } = loader.require("sdk/page-mod"); let pageMod = new PageMod({ include: "about:blank?testRelatedTabNoOtherReqs", onAttach: function(worker) { assert.ok(!!worker.tab, "Worker.tab exists"); pageMod.destroy(); worker.tab.close(function() { worker.destroy(); loader.unload(); done(); }); } }); tabs.open({ url: "about:blank?testRelatedTabNoOtherReqs" }); }; exports.testWorksWithExistingTabs = function(assert, done) { let url = "data:text/html;charset=utf-8," + encodeURI("Test unique document"); let { PageMod } = require("sdk/page-mod"); tabs.open({ url: url, onReady: function onReady(tab) { let pageModOnExisting = new PageMod({ include: url, attachTo: ["existing", "top", "frame"], onAttach: function(worker) { assert.ok(!!worker.tab, "Worker.tab exists"); assert.equal(tab, worker.tab, "A worker has been created on this existing tab"); worker.on('pageshow', () => { assert.fail("Should not have seen pageshow for an already loaded page"); }); setTimeout(function() { pageModOnExisting.destroy(); pageModOffExisting.destroy(); tab.close(done); }, 0); } }); let pageModOffExisting = new PageMod({ include: url, onAttach: function(worker) { assert.fail("pageModOffExisting page-mod should not have attached to anything"); } }); } }); }; exports.testExistingFrameDoesntMatchInclude = function(assert, done) { let iframeURL = 'data:text/html;charset=utf-8,UNIQUE-TEST-STRING-42'; let iframe = '