gecko-dev/addon-sdk/source/test/test-page-worker.js
Dave Townsend 610674c3bb Bug 1129662: sdk/page-worker should use a remote page. r=krizsa
This makes page-worker load its pages in the remote process. It does so by
creating a single frame in the hidden window used to ensure we have a remote
process when necessary and then a module in the remote process is used to
create windowless browsers to load the pages.

This does break one API, getActiveView, but I don't think we should be
maintaining that and it has been unstable since its inception anyway.

Once downside, the l10n module now has to use the observer service to detect
documents rather than the DOM event, this might be causing more CPOW traffic
since that observer notification is shimmed so we may need to use the shim
waiver there.

--HG--
extra : commitid : FDiGeJzOj6Y
extra : rebase_source : 4a237ee4e75a5b00e8bc17df67dfc9a6db99156e
2015-10-16 13:22:28 -07:00

559 lines
18 KiB
JavaScript

/* 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 { Loader } = require('sdk/test/loader');
const { Page } = require("sdk/page-worker");
const { URL } = require("sdk/url");
const fixtures = require("./fixtures");
const testURI = fixtures.url("test.html");
const { getActiveView } = require("sdk/view/core");
const { getDocShell } = require('sdk/frame/utils');
const ERR_DESTROYED =
"Couldn't find the worker to receive this message. " +
"The script may not be initialized yet, or may already have been unloaded.";
const Isolate = fn => "(" + fn + ")()";
exports.testSimplePageCreation = function(assert, done) {
let page = new Page({
contentScript: "self.postMessage(window.location.href)",
contentScriptWhen: "end",
onMessage: function (message) {
assert.equal(message, "about:blank",
"Page Worker should start with a blank page by default");
assert.equal(this, page, "The 'this' object is the page itself.");
done();
}
});
}
/*
* Tests that we can't be tricked by document overloads as we have access
* to wrapped nodes
*/
exports.testWrappedDOM = function(assert, done) {
let page = Page({
allow: { script: true },
contentURL: "data:text/html;charset=utf-8,<script>document.getElementById=3;window.scrollTo=3;</script>",
contentScript: 'new ' + function() {
function send() {
self.postMessage([typeof(document.getElementById), typeof(window.scrollTo)]);
}
if (document.readyState !== 'complete')
window.addEventListener('load', send, true)
else
send();
},
onMessage: function (message) {
assert.equal(message[0],
"function",
"getElementById from content script is the native one");
assert.equal(message[1],
"function",
"scrollTo from content script is the native one");
done();
}
});
}
/*
// We do not offer unwrapped access to DOM since bug 601295 landed
// See 660780 to track progress of unwrap feature
exports.testUnwrappedDOM = function(assert, done) {
let page = Page({
allow: { script: true },
contentURL: "data:text/html;charset=utf-8,<script>document.getElementById=3;window.scrollTo=3;</script>",
contentScript: "window.addEventListener('load', function () {" +
"return self.postMessage([typeof(unsafeWindow.document.getElementById), " +
"typeof(unsafeWindow.scrollTo)]); }, true)",
onMessage: function (message) {
assert.equal(message[0],
"number",
"document inside page is free to be changed");
assert.equal(message[1],
"number",
"window inside page is free to be changed");
done();
}
});
}
*/
exports.testPageProperties = function(assert) {
let page = new Page();
for (let prop of ['contentURL', 'allow', 'contentScriptFile',
'contentScript', 'contentScriptWhen', 'on',
'postMessage', 'removeListener']) {
assert.ok(prop in page, prop + " property is defined on page.");
}
assert.ok(() => page.postMessage("foo") || true,
"postMessage doesn't throw exception on page.");
}
exports.testConstructorAndDestructor = function(assert, done) {
let loader = Loader(module);
let { Page } = loader.require("sdk/page-worker");
let global = loader.sandbox("sdk/page-worker");
let pagesReady = 0;
let page1 = Page({
contentScript: "self.postMessage('')",
contentScriptWhen: "end",
onMessage: pageReady
});
let page2 = Page({
contentScript: "self.postMessage('')",
contentScriptWhen: "end",
onMessage: pageReady
});
assert.notEqual(page1, page2,
"Page 1 and page 2 should be different objects.");
function pageReady() {
if (++pagesReady == 2) {
page1.destroy();
page2.destroy();
assert.ok(isDestroyed(page1), "page1 correctly unloaded.");
assert.ok(isDestroyed(page2), "page2 correctly unloaded.");
loader.unload();
done();
}
}
}
exports.testAutoDestructor = function(assert, done) {
let loader = Loader(module);
let { Page } = loader.require("sdk/page-worker");
let page = Page({
contentScript: "self.postMessage('')",
contentScriptWhen: "end",
onMessage: function() {
loader.unload();
assert.ok(isDestroyed(page), "Page correctly unloaded.");
done();
}
});
}
exports.testValidateOptions = function(assert) {
assert.throws(
() => Page({ contentURL: 'home' }),
/The `contentURL` option must be a valid URL\./,
"Validation correctly denied a non-URL contentURL"
);
assert.throws(
() => Page({ onMessage: "This is not a function."}),
/The option "onMessage" must be one of the following types: function/,
"Validation correctly denied a non-function onMessage."
);
assert.pass("Options validation is working.");
}
exports.testContentAndAllowGettersAndSetters = function(assert, done) {
let content = "data:text/html;charset=utf-8,<script>window.localStorage.allowScript=3;</script>";
// Load up the page with testURI initially for the resource:// principal,
// then load the actual data:* content, as data:* URIs no longer
// have localStorage
let page = Page({
contentURL: testURI,
contentScript: "if (window.location.href==='"+testURI+"')" +
" self.postMessage('reload');" +
"else " +
" self.postMessage(window.localStorage.allowScript)",
contentScriptWhen: "end",
onMessage: step0
});
function step0(message) {
if (message === 'reload')
return page.contentURL = content;
assert.equal(message, "3",
"Correct value expected for allowScript - 3");
assert.equal(page.contentURL, content,
"Correct content expected");
page.removeListener('message', step0);
page.on('message', step1);
page.allow = { script: false };
page.contentURL = content =
"data:text/html;charset=utf-8,<script>window.localStorage.allowScript='f'</script>";
}
function step1(message) {
assert.equal(message, "3",
"Correct value expected for allowScript - 3");
assert.equal(page.contentURL, content, "Correct content expected");
page.removeListener('message', step1);
page.on('message', step2);
page.allow = { script: true };
page.contentURL = content =
"data:text/html;charset=utf-8,<script>window.localStorage.allowScript='g'</script>";
}
function step2(message) {
assert.equal(message, "g",
"Correct value expected for allowScript - g");
assert.equal(page.contentURL, content, "Correct content expected");
page.removeListener('message', step2);
page.on('message', step3);
page.allow.script = false;
page.contentURL = content =
"data:text/html;charset=utf-8,<script>window.localStorage.allowScript=3</script>";
}
function step3(message) {
assert.equal(message, "g",
"Correct value expected for allowScript - g");
assert.equal(page.contentURL, content, "Correct content expected");
page.removeListener('message', step3);
page.on('message', step4);
page.allow.script = true;
page.contentURL = content =
"data:text/html;charset=utf-8,<script>window.localStorage.allowScript=4</script>";
}
function step4(message) {
assert.equal(message, "4",
"Correct value expected for allowScript - 4");
assert.equal(page.contentURL, content, "Correct content expected");
done();
}
}
exports.testOnMessageCallback = function(assert, done) {
Page({
contentScript: "self.postMessage('')",
contentScriptWhen: "end",
onMessage: function() {
assert.pass("onMessage callback called");
done();
}
});
}
exports.testMultipleOnMessageCallbacks = function(assert, done) {
let count = 0;
let page = Page({
contentScript: "self.postMessage('')",
contentScriptWhen: "end",
onMessage: () => count += 1
});
page.on('message', () => count += 2);
page.on('message', () => count *= 3);
page.on('message', () =>
assert.equal(count, 9, "All callbacks were called, in order."));
page.on('message', done);
};
exports.testLoadContentPage = function(assert, done) {
let page = Page({
onMessage: function(message) {
// The message is an array whose first item is the test method to call
// and the rest of whose items are arguments to pass it.
let msg = message.shift();
if (msg == "done")
return done();
assert[msg].apply(assert, message);
},
contentURL: fixtures.url("addon-sdk/data/test-page-worker.html"),
contentScriptFile: fixtures.url("addon-sdk/data/test-page-worker.js"),
contentScriptWhen: "ready"
});
}
exports.testLoadContentPageRelativePath = function(assert, done) {
const self = require("sdk/self");
const { merge } = require("sdk/util/object");
const options = merge({}, require('@loader/options'),
{ id: "testloader", prefixURI: require('./fixtures').url() });
let loader = Loader(module, null, options);
let page = loader.require("sdk/page-worker").Page({
onMessage: function(message) {
// The message is an array whose first item is the test method to call
// and the rest of whose items are arguments to pass it.
let msg = message.shift();
if (msg == "done")
return done();
assert[msg].apply(assert, message);
},
contentURL: "./test-page-worker.html",
contentScriptFile: "./test-page-worker.js",
contentScriptWhen: "ready"
});
}
exports.testAllowScriptDefault = function(assert, done) {
let page = Page({
onMessage: function(message) {
assert.ok(message, "Script is allowed to run by default.");
done();
},
contentURL: "data:text/html;charset=utf-8,<script>document.documentElement.setAttribute('foo', 3);</script>",
contentScript: "self.postMessage(document.documentElement.getAttribute('foo'))",
contentScriptWhen: "ready"
});
}
exports.testAllowScript = function(assert, done) {
let page = Page({
onMessage: function(message) {
assert.ok(message, "Script runs when allowed to do so.");
page.destroy();
done();
},
allow: { script: true },
contentURL: "data:text/html;charset=utf-8,<script>document.documentElement.setAttribute('foo', 3);</script>",
contentScript: "self.postMessage(document.documentElement.hasAttribute('foo') && " +
" document.documentElement.getAttribute('foo') == 3)",
contentScriptWhen: "ready"
});
}
exports.testPingPong = function(assert, done) {
let page = Page({
contentURL: 'data:text/html;charset=utf-8,ping-pong',
contentScript: 'self.on("message", message => self.postMessage("pong"));'
+ 'self.postMessage("ready");',
onMessage: function(message) {
if ('ready' == message) {
page.postMessage('ping');
}
else {
assert.ok(message, 'pong', 'Callback from contentScript');
done();
}
}
});
};
exports.testRedirect = function (assert, done) {
let page = Page({
contentURL: 'data:text/html;charset=utf-8,first-page',
contentScriptWhen: "end",
contentScript: '' +
'if (/first-page/.test(document.location.href)) ' +
' document.location.href = "data:text/html;charset=utf-8,redirect";' +
'else ' +
' self.port.emit("redirect", document.location.href);'
});
page.port.on('redirect', function (url) {
assert.equal(url, 'data:text/html;charset=utf-8,redirect', 'Reinjects contentScript on reload');
done();
});
};
exports.testRedirectIncludeArrays = function (assert, done) {
let firstURL = 'data:text/html;charset=utf-8,first-page';
let page = Page({
contentURL: firstURL,
contentScript: '(function () {' +
'self.port.emit("load", document.location.href);' +
' self.port.on("redirect", function (url) {' +
' document.location.href = url;' +
' })' +
'})();',
include: ['about:blank', 'data:*']
});
page.port.on('load', function (url) {
if (url === firstURL) {
page.port.emit('redirect', 'about:blank');
} else if (url === 'about:blank') {
page.port.emit('redirect', 'about:mozilla');
assert.ok('`include` property handles arrays');
assert.equal(url, 'about:blank', 'Redirects work with accepted domains');
done();
} else if (url === 'about:mozilla') {
assert.fail('Should not redirect to restricted domain');
}
});
};
exports.testRedirectFromWorker = function (assert, done) {
let firstURL = 'data:text/html;charset=utf-8,first-page';
let secondURL = 'data:text/html;charset=utf-8,second-page';
let thirdURL = 'data:text/html;charset=utf-8,third-page';
let page = Page({
contentURL: firstURL,
contentScript: '(function () {' +
'self.port.emit("load", document.location.href);' +
' self.port.on("redirect", function (url) {' +
' document.location.href = url;' +
' })' +
'})();',
include: 'data:*'
});
page.port.on('load', function (url) {
if (url === firstURL) {
page.port.emit('redirect', secondURL);
} else if (url === secondURL) {
page.port.emit('redirect', thirdURL);
} else if (url === thirdURL) {
page.port.emit('redirect', 'about:mozilla');
assert.equal(url, thirdURL, 'Redirects work with accepted domains on include strings');
done();
} else {
assert.fail('Should not redirect to unauthorized domains');
}
});
};
exports.testRedirectWithContentURL = function (assert, done) {
let firstURL = 'data:text/html;charset=utf-8,first-page';
let secondURL = 'data:text/html;charset=utf-8,second-page';
let thirdURL = 'data:text/html;charset=utf-8,third-page';
let page = Page({
contentURL: firstURL,
contentScript: '(function () {' +
'self.port.emit("load", document.location.href);' +
'})();',
include: 'data:*'
});
page.port.on('load', function (url) {
if (url === firstURL) {
page.contentURL = secondURL;
} else if (url === secondURL) {
page.contentURL = thirdURL;
} else if (url === thirdURL) {
page.contentURL = 'about:mozilla';
assert.equal(url, thirdURL, 'Redirects work with accepted domains on include strings');
done();
} else {
assert.fail('Should not redirect to unauthorized domains');
}
});
};
exports.testMultipleDestroys = function(assert) {
let page = Page();
page.destroy();
page.destroy();
assert.pass("Multiple destroys should not cause an error");
};
exports.testContentScriptOptionsOption = function(assert, done) {
let page = new Page({
contentScript: "self.postMessage( [typeof self.options.d, self.options] );",
contentScriptWhen: "end",
contentScriptOptions: {a: true, b: [1,2,3], c: "string", d: function(){ return 'test'}},
onMessage: function(msg) {
assert.equal(msg[0], 'undefined', 'functions are stripped from contentScriptOptions');
assert.equal(typeof msg[1], 'object', 'object as contentScriptOptions');
assert.equal(msg[1].a, true, 'boolean in contentScriptOptions');
assert.equal(msg[1].b.join(), '1,2,3', 'array and numbers in contentScriptOptions');
assert.equal(msg[1].c, 'string', 'string in contentScriptOptions');
done();
}
});
};
exports.testMessageQueue = function (assert, done) {
let page = new Page({
contentScript: 'self.on("message", function (m) {' +
'self.postMessage(m);' +
'});',
contentURL: 'data:text/html;charset=utf-8,',
});
page.postMessage('ping');
page.on('message', function (m) {
assert.equal(m, 'ping', 'postMessage should queue messages');
done();
});
};
exports.testWindowStopDontBreak = function (assert, done) {
const { Ci, Cc } = require('chrome');
const consoleService = Cc['@mozilla.org/consoleservice;1'].
getService(Ci.nsIConsoleService);
const listener = {
observe: ({message}) => {
if (message.includes('contentWorker is null'))
assert.fail('contentWorker is null');
}
};
consoleService.registerListener(listener)
let page = new Page({
contentURL: 'data:text/html;charset=utf-8,testWindowStopDontBreak',
contentScriptWhen: 'ready',
contentScript: Isolate(() => {
window.stop();
self.port.on('ping', () => self.port.emit('pong'));
})
});
page.port.on('pong', () => {
assert.pass('page-worker works after window.stop');
page.destroy();
consoleService.unregisterListener(listener);
done();
});
page.port.emit("ping");
};
/**
* bug 1138545 - the docs claim you can pass in a bare regexp.
*/
exports.testRegexArgument = function (assert, done) {
let url = 'data:text/html;charset=utf-8,testWindowStopDontBreak';
let page = new Page({
contentURL: url,
contentScriptWhen: 'ready',
contentScript: Isolate(() => {
self.port.emit("pong", document.location.href);
}),
include: /^data\:text\/html;.*/
});
assert.pass("We can pass in a RegExp into page-worker's include option.");
page.port.on("pong", (href) => {
assert.equal(href, url, "we get back the same url from the content script.");
page.destroy();
done();
});
};
function isDestroyed(page) {
try {
page.postMessage("foo");
}
catch (err) {
if (err.message == ERR_DESTROYED) {
return true;
}
else {
throw err;
}
}
return false;
}
require("sdk/test").run(exports);