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: "use strict"; michael@0: michael@0: const { PageMod } = require("sdk/page-mod"); michael@0: const { testPageMod, handleReadyState } = require("./pagemod-test-helpers"); michael@0: const { Loader } = require('sdk/test/loader'); michael@0: const tabs = require("sdk/tabs"); michael@0: const { setTimeout } = require("sdk/timers"); michael@0: const { Cc, Ci, Cu } = require("chrome"); michael@0: const { michael@0: open, michael@0: getFrames, michael@0: getMostRecentBrowserWindow, michael@0: getInnerId michael@0: } = require('sdk/window/utils'); michael@0: const { getTabContentWindow, getActiveTab, setTabURL, openTab, closeTab } = require('sdk/tabs/utils'); michael@0: const xulApp = require("sdk/system/xul-app"); michael@0: const { isPrivateBrowsingSupported } = require('sdk/self'); michael@0: const { isPrivate } = require('sdk/private-browsing'); michael@0: const { openWebpage } = require('./private-browsing/helper'); michael@0: const { isTabPBSupported, isWindowPBSupported, isGlobalPBSupported } = require('sdk/private-browsing/utils'); michael@0: const promise = require("sdk/core/promise"); michael@0: const { pb } = require('./private-browsing/helper'); michael@0: const { URL } = require("sdk/url"); michael@0: const { LoaderWithHookedConsole } = require('sdk/test/loader'); michael@0: michael@0: const { waitUntil } = require("sdk/test/utils"); michael@0: const data = require("./fixtures"); michael@0: michael@0: const { gDevToolsExtensions } = Cu.import("resource://gre/modules/devtools/DevToolsExtensions.jsm", {}); michael@0: michael@0: const testPageURI = data.url("test.html"); michael@0: michael@0: // The following adds Debugger constructor to the global namespace. michael@0: const { addDebuggerToGlobal } = michael@0: Cu.import('resource://gre/modules/jsdebugger.jsm', {}); michael@0: addDebuggerToGlobal(this); michael@0: michael@0: function Isolate(worker) { michael@0: return "(" + worker + ")()"; michael@0: } michael@0: michael@0: /* Tests for the PageMod APIs */ michael@0: michael@0: exports.testPageMod1 = function(assert, done) { michael@0: let mods = testPageMod(assert, done, "about:", [{ michael@0: include: /about:/, michael@0: contentScriptWhen: 'end', michael@0: contentScript: 'new ' + function WorkerScope() { michael@0: window.document.body.setAttribute("JEP-107", "worked"); michael@0: }, michael@0: onAttach: function() { michael@0: assert.equal(this, mods[0], "The 'this' object is the page mod."); michael@0: } michael@0: }], michael@0: function(win, done) { michael@0: assert.equal( michael@0: win.document.body.getAttribute("JEP-107"), michael@0: "worked", michael@0: "PageMod.onReady test" michael@0: ); michael@0: done(); michael@0: } michael@0: ); michael@0: }; michael@0: michael@0: exports.testPageMod2 = function(assert, done) { michael@0: testPageMod(assert, done, "about:", [{ michael@0: include: "about:*", michael@0: contentScript: [ michael@0: 'new ' + function contentScript() { michael@0: window.AUQLUE = function() { return 42; } michael@0: try { michael@0: window.AUQLUE() michael@0: } michael@0: catch(e) { michael@0: throw new Error("PageMod scripts executed in order"); michael@0: } michael@0: document.documentElement.setAttribute("first", "true"); michael@0: }, michael@0: 'new ' + function contentScript() { michael@0: document.documentElement.setAttribute("second", "true"); michael@0: } michael@0: ] michael@0: }], function(win, done) { michael@0: assert.equal(win.document.documentElement.getAttribute("first"), michael@0: "true", michael@0: "PageMod test #2: first script has run"); michael@0: assert.equal(win.document.documentElement.getAttribute("second"), michael@0: "true", michael@0: "PageMod test #2: second script has run"); michael@0: assert.equal("AUQLUE" in win, false, michael@0: "PageMod test #2: scripts get a wrapped window"); michael@0: done(); michael@0: }); michael@0: }; michael@0: michael@0: exports.testPageModIncludes = function(assert, done) { michael@0: var asserts = []; michael@0: function createPageModTest(include, expectedMatch) { michael@0: // Create an 'onload' test function... michael@0: asserts.push(function(test, win) { michael@0: var matches = include in win.localStorage; michael@0: assert.ok(expectedMatch ? matches : !matches, michael@0: "'" + include + "' match test, expected: " + expectedMatch); michael@0: }); michael@0: // ...and corresponding PageMod options michael@0: return { michael@0: include: include, michael@0: contentScript: 'new ' + function() { michael@0: self.on("message", function(msg) { michael@0: window.localStorage[msg] = true; michael@0: }); michael@0: }, michael@0: // The testPageMod callback with test assertions is called on 'end', michael@0: // and we want this page mod to be attached before it gets called, michael@0: // so we attach it on 'start'. michael@0: contentScriptWhen: 'start', michael@0: onAttach: function(worker) { michael@0: worker.postMessage(this.include[0]); michael@0: } michael@0: }; michael@0: } michael@0: michael@0: testPageMod(assert, done, testPageURI, [ michael@0: createPageModTest("*", false), michael@0: createPageModTest("*.google.com", false), michael@0: createPageModTest("resource:*", true), michael@0: createPageModTest("resource:", false), michael@0: createPageModTest(testPageURI, true) michael@0: ], michael@0: function (win, done) { michael@0: waitUntil(function () win.localStorage[testPageURI], michael@0: testPageURI + " page-mod to be executed") michael@0: .then(function () { michael@0: asserts.forEach(function(fn) { michael@0: fn(assert, win); michael@0: }); michael@0: done(); michael@0: }); michael@0: } michael@0: ); michael@0: }; michael@0: michael@0: exports.testPageModValidationAttachTo = function(assert) { michael@0: [{ val: 'top', type: 'string "top"' }, michael@0: { val: 'frame', type: 'string "frame"' }, michael@0: { val: ['top', 'existing'], type: 'array with "top" and "existing"' }, michael@0: { val: ['frame', 'existing'], type: 'array with "frame" and "existing"' }, michael@0: { val: ['top'], type: 'array with "top"' }, michael@0: { val: ['frame'], type: 'array with "frame"' }, michael@0: { val: undefined, type: 'undefined' }].forEach((attachTo) => { michael@0: new PageMod({ attachTo: attachTo.val, include: '*.validation111' }); michael@0: assert.pass("PageMod() does not throw when attachTo is " + attachTo.type); michael@0: }); michael@0: michael@0: [{ val: 'existing', type: 'string "existing"' }, michael@0: { val: ['existing'], type: 'array with "existing"' }, michael@0: { val: 'not-legit', type: 'string with "not-legit"' }, michael@0: { val: ['not-legit'], type: 'array with "not-legit"' }, michael@0: { val: {}, type: 'object' }].forEach((attachTo) => { michael@0: assert.throws(() => michael@0: new PageMod({ attachTo: attachTo.val, include: '*.validation111' }), michael@0: /The `attachTo` option/, michael@0: "PageMod() throws when 'attachTo' option is " + attachTo.type + "."); michael@0: }); michael@0: }; michael@0: michael@0: exports.testPageModValidationInclude = function(assert) { michael@0: [{ val: undefined, type: 'undefined' }, michael@0: { val: {}, type: 'object' }, michael@0: { val: [], type: 'empty array'}, michael@0: { val: [/regexp/, 1], type: 'array with non string/regexp' }, michael@0: { val: 1, type: 'number' }].forEach((include) => { michael@0: assert.throws(() => new PageMod({ include: include.val }), michael@0: /The `include` option must always contain atleast one rule/, michael@0: "PageMod() throws when 'include' option is " + include.type + "."); michael@0: }); michael@0: michael@0: [{ val: '*.validation111', type: 'string' }, michael@0: { val: /validation111/, type: 'regexp' }, michael@0: { val: ['*.validation111'], type: 'array with length > 0'}].forEach((include) => { michael@0: new PageMod({ include: include.val }); michael@0: assert.pass("PageMod() does not throw when include option is " + include.type); michael@0: }); michael@0: }; michael@0: michael@0: /* Tests for internal functions. */ michael@0: exports.testCommunication1 = function(assert, done) { michael@0: let workerDone = false, michael@0: callbackDone = null; michael@0: michael@0: testPageMod(assert, done, "about:", [{ michael@0: include: "about:*", michael@0: contentScriptWhen: 'end', michael@0: contentScript: 'new ' + function WorkerScope() { michael@0: self.on('message', function(msg) { michael@0: document.body.setAttribute('JEP-107', 'worked'); michael@0: self.postMessage(document.body.getAttribute('JEP-107')); michael@0: }) michael@0: }, michael@0: onAttach: function(worker) { michael@0: worker.on('error', function(e) { michael@0: assert.fail('Errors where reported'); michael@0: }); michael@0: worker.on('message', function(value) { michael@0: assert.equal( michael@0: "worked", michael@0: value, michael@0: "test comunication" michael@0: ); michael@0: workerDone = true; michael@0: if (callbackDone) michael@0: callbackDone(); michael@0: }); michael@0: worker.postMessage('do it!') michael@0: } michael@0: }], michael@0: function(win, done) { michael@0: (callbackDone = function() { michael@0: if (workerDone) { michael@0: assert.equal( michael@0: 'worked', michael@0: win.document.body.getAttribute('JEP-107'), michael@0: 'attribute should be modified' michael@0: ); michael@0: done(); michael@0: } michael@0: })(); michael@0: } michael@0: ); michael@0: }; michael@0: michael@0: exports.testCommunication2 = function(assert, done) { michael@0: let callbackDone = null, michael@0: window; michael@0: michael@0: testPageMod(assert, done, "about:license", [{ michael@0: include: "about:*", michael@0: contentScriptWhen: 'start', michael@0: contentScript: 'new ' + function WorkerScope() { michael@0: document.documentElement.setAttribute('AUQLUE', 42); michael@0: window.addEventListener('load', function listener() { michael@0: self.postMessage('onload'); michael@0: }, false); michael@0: self.on("message", function() { michael@0: self.postMessage(document.documentElement.getAttribute("test")) michael@0: }); michael@0: }, michael@0: onAttach: function(worker) { michael@0: worker.on('error', function(e) { michael@0: assert.fail('Errors where reported'); michael@0: }); michael@0: worker.on('message', function(msg) { michael@0: if ('onload' == msg) { michael@0: assert.equal( michael@0: '42', michael@0: window.document.documentElement.getAttribute('AUQLUE'), michael@0: 'PageMod scripts executed in order' michael@0: ); michael@0: window.document.documentElement.setAttribute('test', 'changes in window'); michael@0: worker.postMessage('get window.test') michael@0: } else { michael@0: assert.equal( michael@0: 'changes in window', michael@0: msg, michael@0: 'PageMod test #2: second script has run' michael@0: ) michael@0: callbackDone(); michael@0: } michael@0: }); michael@0: } michael@0: }], michael@0: function(win, done) { michael@0: window = win; michael@0: callbackDone = done; michael@0: } michael@0: ); michael@0: }; michael@0: michael@0: exports.testEventEmitter = function(assert, done) { michael@0: let workerDone = false, michael@0: callbackDone = null; michael@0: michael@0: testPageMod(assert, done, "about:", [{ michael@0: include: "about:*", michael@0: contentScript: 'new ' + function WorkerScope() { 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: onAttach: function(worker) { michael@0: worker.on('error', function(e) { michael@0: assert.fail('Errors were reported : '+e); michael@0: }); michael@0: worker.port.on('content-to-addon', function(value) { michael@0: assert.equal( michael@0: "worked", michael@0: value, michael@0: "EventEmitter API works!" michael@0: ); michael@0: if (callbackDone) michael@0: callbackDone(); michael@0: else michael@0: workerDone = true; michael@0: }); michael@0: worker.port.emit('addon-to-content', 'worked'); michael@0: } michael@0: }], michael@0: function(win, done) { michael@0: if (workerDone) michael@0: done(); michael@0: else michael@0: callbackDone = done; michael@0: } michael@0: ); michael@0: }; michael@0: michael@0: // Execute two concurrent page mods on same document to ensure that their michael@0: // JS contexts are different michael@0: exports.testMixedContext = function(assert, done) { michael@0: let doneCallback = null; michael@0: let messages = 0; michael@0: let modObject = { michael@0: include: "data:text/html;charset=utf-8,", michael@0: contentScript: 'new ' + function WorkerScope() { michael@0: // Both scripts will execute this, michael@0: // context is shared if one script see the other one modification. michael@0: let isContextShared = "sharedAttribute" in document; michael@0: self.postMessage(isContextShared); michael@0: document.sharedAttribute = true; michael@0: }, michael@0: onAttach: function(w) { michael@0: w.on("message", function (isContextShared) { michael@0: if (isContextShared) { michael@0: assert.fail("Page mod contexts are mixed."); michael@0: doneCallback(); michael@0: } michael@0: else if (++messages == 2) { michael@0: assert.pass("Page mod contexts are different."); michael@0: doneCallback(); michael@0: } michael@0: }); michael@0: } michael@0: }; michael@0: testPageMod(assert, done, "data:text/html;charset=utf-8,", [modObject, modObject], michael@0: function(win, done) { michael@0: doneCallback = done; michael@0: } michael@0: ); michael@0: }; michael@0: michael@0: exports.testHistory = function(assert, done) { michael@0: // We need a valid url in order to have a working History API. michael@0: // (i.e do not work on data: or about: pages) michael@0: // Test bug 679054. michael@0: let url = data.url("test-page-mod.html"); michael@0: let callbackDone = null; michael@0: testPageMod(assert, done, url, [{ michael@0: include: url, michael@0: contentScriptWhen: 'end', michael@0: contentScript: 'new ' + function WorkerScope() { michael@0: history.pushState({}, "", "#"); michael@0: history.replaceState({foo: "bar"}, "", "#"); michael@0: self.postMessage(history.state); michael@0: }, michael@0: onAttach: function(worker) { michael@0: worker.on('message', function (data) { michael@0: assert.equal(JSON.stringify(data), JSON.stringify({foo: "bar"}), michael@0: "History API works!"); michael@0: callbackDone(); michael@0: }); michael@0: } michael@0: }], michael@0: function(win, done) { michael@0: callbackDone = done; michael@0: } michael@0: ); michael@0: }; michael@0: michael@0: exports.testRelatedTab = function(assert, done) { michael@0: let tab; michael@0: let pageMod = new PageMod({ michael@0: include: "about:*", michael@0: onAttach: function(worker) { michael@0: assert.ok(!!worker.tab, "Worker.tab exists"); michael@0: assert.equal(tab, worker.tab, "Worker.tab is valid"); michael@0: pageMod.destroy(); michael@0: tab.close(done); michael@0: } michael@0: }); michael@0: michael@0: tabs.open({ michael@0: url: "about:", michael@0: onOpen: function onOpen(t) { michael@0: tab = t; michael@0: } michael@0: }); michael@0: }; michael@0: michael@0: exports.testRelatedTabNoRequireTab = function(assert, done) { michael@0: let loader = Loader(module); michael@0: let tab; michael@0: let url = "data:text/html;charset=utf-8," + encodeURI("Test related worker tab 2"); michael@0: let { PageMod } = loader.require("sdk/page-mod"); michael@0: let pageMod = new PageMod({ michael@0: include: url, michael@0: onAttach: function(worker) { michael@0: assert.equal(worker.tab.url, url, "Worker.tab.url is valid"); michael@0: worker.tab.close(function() { michael@0: pageMod.destroy(); michael@0: loader.unload(); michael@0: done(); michael@0: }); michael@0: } michael@0: }); michael@0: michael@0: tabs.open(url); michael@0: }; michael@0: michael@0: exports.testRelatedTabNoOtherReqs = function(assert, done) { michael@0: let loader = Loader(module); michael@0: let { PageMod } = loader.require("sdk/page-mod"); michael@0: let pageMod = new PageMod({ michael@0: include: "about:blank?testRelatedTabNoOtherReqs", michael@0: onAttach: function(worker) { michael@0: assert.ok(!!worker.tab, "Worker.tab exists"); michael@0: pageMod.destroy(); michael@0: worker.tab.close(function() { michael@0: worker.destroy(); michael@0: loader.unload(); michael@0: done(); michael@0: }); michael@0: } michael@0: }); michael@0: michael@0: tabs.open({ michael@0: url: "about:blank?testRelatedTabNoOtherReqs" michael@0: }); michael@0: }; michael@0: michael@0: exports.testWorksWithExistingTabs = function(assert, done) { michael@0: let url = "data:text/html;charset=utf-8," + encodeURI("Test unique document"); michael@0: let { PageMod } = require("sdk/page-mod"); michael@0: tabs.open({ michael@0: url: url, michael@0: onReady: function onReady(tab) { michael@0: let pageModOnExisting = new PageMod({ michael@0: include: url, michael@0: attachTo: ["existing", "top", "frame"], michael@0: onAttach: function(worker) { michael@0: assert.ok(!!worker.tab, "Worker.tab exists"); michael@0: assert.equal(tab, worker.tab, "A worker has been created on this existing tab"); michael@0: michael@0: setTimeout(function() { michael@0: pageModOnExisting.destroy(); michael@0: pageModOffExisting.destroy(); michael@0: tab.close(done); michael@0: }, 0); michael@0: } michael@0: }); michael@0: michael@0: let pageModOffExisting = new PageMod({ michael@0: include: url, michael@0: onAttach: function(worker) { michael@0: assert.fail("pageModOffExisting page-mod should not have attached to anything"); michael@0: } michael@0: }); michael@0: } michael@0: }); michael@0: }; michael@0: michael@0: exports.testExistingFrameDoesntMatchInclude = function(assert, done) { michael@0: let iframeURL = 'data:text/html;charset=utf-8,UNIQUE-TEST-STRING-42'; michael@0: let iframe = '