/* 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 { PageMod } = require("sdk/page-mod"); const { testPageMod, handleReadyState } = require("./pagemod-test-helpers"); const { Loader } = require('sdk/test/loader'); const tabs = require("sdk/tabs"); const { setTimeout } = require("sdk/timers"); const { Cc, Ci, Cu } = require("chrome"); const { open, getFrames, getMostRecentBrowserWindow, getInnerId } = require('sdk/window/utils'); const { getTabContentWindow, getActiveTab, setTabURL, openTab, closeTab } = 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, isGlobalPBSupported } = require('sdk/private-browsing/utils'); const promise = require("sdk/core/promise"); const { pb } = require('./private-browsing/helper'); const { URL } = require("sdk/url"); const { LoaderWithHookedConsole } = require('sdk/test/loader'); const { waitUntil } = require("sdk/test/utils"); const data = require("./fixtures"); const { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); const { require: devtoolsRequire } = devtools; const contentGlobals = devtoolsRequire("devtools/server/content-globals"); const testPageURI = data.url("test.html"); // The following adds Debugger constructor to the global namespace. const { addDebuggerToGlobal } = Cu.import('resource://gre/modules/jsdebugger.jsm', {}); addDebuggerToGlobal(this); function Isolate(worker) { return "(" + worker + ")()"; } /* Tests for the PageMod APIs */ exports.testPageMod1 = function(assert, done) { let mods = testPageMod(assert, done, "about:", [{ include: /about:/, contentScriptWhen: 'end', contentScript: 'new ' + function WorkerScope() { window.document.body.setAttribute("JEP-107", "worked"); }, onAttach: function() { assert.equal(this, mods[0], "The 'this' object is the page mod."); } }], function(win, done) { assert.equal( win.document.body.getAttribute("JEP-107"), "worked", "PageMod.onReady test" ); done(); } ); }; exports.testPageMod2 = function(assert, done) { testPageMod(assert, done, "about:", [{ include: "about:*", 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"); } ] }], function(win, done) { assert.equal(win.document.documentElement.getAttribute("first"), "true", "PageMod test #2: first script has run"); assert.equal(win.document.documentElement.getAttribute("second"), "true", "PageMod test #2: second script has run"); assert.equal("AUQLUE" in win, false, "PageMod test #2: scripts get a wrapped window"); done(); }); }; exports.testPageModIncludes = function(assert, done) { var asserts = []; function createPageModTest(include, expectedMatch) { // Create an 'onload' test function... asserts.push(function(test, win) { var matches = include in win.localStorage; assert.ok(expectedMatch ? matches : !matches, "'" + include + "' match test, expected: " + expectedMatch); }); // ...and corresponding PageMod options return { include: include, contentScript: 'new ' + function() { self.on("message", function(msg) { window.localStorage[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]); } }; } testPageMod(assert, done, testPageURI, [ createPageModTest("*", false), createPageModTest("*.google.com", false), createPageModTest("resource:*", true), createPageModTest("resource:", false), createPageModTest(testPageURI, true) ], function (win, done) { waitUntil(() => win.localStorage[testPageURI], testPageURI + " page-mod to be executed") .then(() => { asserts.forEach(fn => fn(assert, win)); win.localStorage.clear(); done(); }); }); }; 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, done) { let workerDone = false, callbackDone = null; testPageMod(assert, done, "about:", [{ 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')); }) }, 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 = true; if (callbackDone) callbackDone(); }); worker.postMessage('do it!') } }], function(win, done) { (callbackDone = function() { if (workerDone) { assert.equal( 'worked', win.document.body.getAttribute('JEP-107'), 'attribute should be modified' ); done(); } })(); } ); }; exports.testCommunication2 = function(assert, done) { let callbackDone = null, window; testPageMod(assert, done, "about:license", [{ include: "about:*", contentScriptWhen: 'start', contentScript: 'new ' + function WorkerScope() { document.documentElement.setAttribute('AUQLUE', 42); window.addEventListener('load', function listener() { self.postMessage('onload'); }, false); self.on("message", function() { self.postMessage(document.documentElement.getAttribute("test")) }); }, onAttach: function(worker) { worker.on('error', function(e) { assert.fail('Errors where reported'); }); worker.on('message', function(msg) { if ('onload' == msg) { assert.equal( '42', window.document.documentElement.getAttribute('AUQLUE'), 'PageMod scripts executed in order' ); window.document.documentElement.setAttribute('test', 'changes in window'); worker.postMessage('get window.test') } else { assert.equal( 'changes in window', msg, 'PageMod test #2: second script has run' ) callbackDone(); } }); } }], function(win, done) { window = win; callbackDone = done; } ); }; 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; } }); }; 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"); 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 = '