Bug 1139189: Uplift the Add-on SDK to Firefox a=me

3fbf5a6bdd..3394ad5d1d
This commit is contained in:
Dave Townsend 2015-03-11 22:19:48 -07:00
parent 0fc67a63f6
commit a89baf6417
108 changed files with 4844 additions and 855 deletions

View File

@ -146,6 +146,7 @@ EXTRA_JS_MODULES.commonjs.dev += [
'source/lib/dev/frame-script.js',
'source/lib/dev/panel.js',
'source/lib/dev/ports.js',
'source/lib/dev/theme.js',
'source/lib/dev/toolbox.js',
'source/lib/dev/utils.js',
'source/lib/dev/volcan.js',
@ -155,6 +156,10 @@ EXTRA_JS_MODULES.commonjs.dev.panel += [
'source/lib/dev/panel/view.js',
]
EXTRA_JS_MODULES.commonjs.dev.theme += [
'source/lib/dev/theme/hooks.js',
]
EXTRA_JS_MODULES.commonjs.diffpatcher += [
'source/lib/diffpatcher/diff.js',
'source/lib/diffpatcher/index.js',
@ -171,12 +176,10 @@ EXTRA_JS_MODULES.commonjs.diffpatcher.test += [
]
EXTRA_JS_MODULES.commonjs.framescript += [
'source/lib/framescript/content.jsm',
'source/lib/framescript/context-menu.js',
'source/lib/framescript/contextmenu-events.js',
'source/lib/framescript/FrameScriptManager.jsm',
'source/lib/framescript/LoaderHelper.jsm',
'source/lib/framescript/manager.js',
'source/lib/framescript/tab-events.js',
'source/lib/framescript/util.js',
]
@ -243,9 +246,12 @@ EXTRA_JS_MODULES.commonjs.sdk.content += [
'source/lib/sdk/content/content.js',
'source/lib/sdk/content/context-menu.js',
'source/lib/sdk/content/events.js',
'source/lib/sdk/content/l10n-html.js',
'source/lib/sdk/content/loader.js',
'source/lib/sdk/content/mod.js',
'source/lib/sdk/content/page-mod.js',
'source/lib/sdk/content/sandbox.js',
'source/lib/sdk/content/tab-events.js',
'source/lib/sdk/content/thumbnail.js',
'source/lib/sdk/content/utils.js',
'source/lib/sdk/content/worker-child.js',
@ -389,6 +395,12 @@ EXTRA_JS_MODULES.commonjs.sdk['private-browsing'] += [
'source/lib/sdk/private-browsing/utils.js',
]
EXTRA_JS_MODULES.commonjs.sdk.remote += [
'source/lib/sdk/remote/child.js',
'source/lib/sdk/remote/parent.js',
'source/lib/sdk/remote/utils.js',
]
EXTRA_JS_MODULES.commonjs.sdk.stylesheet += [
'source/lib/sdk/stylesheet/style.js',
'source/lib/sdk/stylesheet/utils.js',

View File

@ -1,8 +1,8 @@
## Overview
- Changes should follow the [design guidelines], as well as [coding style guide] for Jetpack
- Changes should follow the [design guidelines], as well as the [coding style guide]
- All changes must be accompanied by tests
- In order to land, changes must have review from a core Jetpack developer
- In order to land, changes must have been reviewed by one of the Jetpack reviewers
- Changes should have additional API review when needed
- Changes should have additional review from a Mozilla platform domain-expert when needed
@ -10,25 +10,25 @@ If you have questions, ask in [#jetpack on IRC][jetpack irc channel] or on the [
## How to Make Code Contributions
If you have code that you'd like to contribute the Jetpack project, follow these steps:
If you'd like to contribute the Jetpack project, follow these steps:
1. Look for your issue in the [bugs already filed][open bugs]
2. If no bug exists, [submit one][submit bug]
3. Make your changes, per the Overview
4. Write a test ([intro][test intro], [API][test API])
5. Submit pull request with changes and a title in a form of `Bug XXX - description`
6. Make sure that [Travis CI](https://travis-ci.org/mozilla/addon-sdk/branches) tests are passing for your branch.
7. Copy the pull request link from GitHub and paste it in as an attachment to the bug
8. Each pull request should idealy contain only one commit, so squash the commits if necessary.
9. Flag the attachment for code review from one of the Jetpack reviewers listed below.
This step is optional, but could speed things up.
10. Address any nits (ie style changes), or other issues mentioned in the review.
1. Look for your issue in the list of [bugs already filed][open bugs]. If you want to contribute, but don't already know what you want to do, we keep a list of [good first bugs].
2. If no bug exists, [submit one][submit bug].
3. Get the code: get a [GitHub][GitHub] account, fork the [Add-on SDK repo][Add-on SDK repo], and clone it to your machine.
4. Make your changes. Changes should follow the [design guidelines] as well as the [coding style guide].
5. Write tests: [unit testing introduction][test intro], [unit testing API][test API].
6. Submit a pull request with the changes and a title in the form of `Bug XXX - description`.
7. Make sure that [Travis CI](https://travis-ci.org/mozilla/addon-sdk/branches) tests are passing for your branch.
8. Copy the pull request link from GitHub and paste it in as an attachment to the bug.
9. Each pull request should ideally contain only one commit, so squash the commits if necessary.
10. Flag the attachment for code review from one of the Jetpack reviewers listed below. This step is optional, but could speed things up.
11. Address any issues mentioned in the review.
Finally, once review is approved, a team member will do the merging
## Good First Bugs
There is a list of [good first bugs here](https://bugzilla.mozilla.org/buglist.cgi?list_id=7345714&columnlist=bug_severity%2Cpriority%2Cassigned_to%2Cbug_status%2Ctarget_milestone%2Cresolution%2Cshort_desc%2Cchangeddate&query_based_on=jetpack-good-1st-bugs&status_whiteboard_type=allwordssubstr&query_format=advanced&status_whiteboard=[good%20first%20bug]&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&bug_status=VERIFIED&product=Add-on%20SDK&known_name=jetpack-good-1st-bugs).
There is a list of [good first bugs here][good first bugs].
## Reviewers
@ -36,7 +36,6 @@ All changes must be reviewed by someone on the Jetpack review crew:
- [@mossop]
- [@gozala]
- [@wbamberg]
- [@ZER0]
- [@erikvold]
- [@jsantell]
@ -52,13 +51,15 @@ For API and developer ergonomics review, ask [@gozala].
[Jetpack mailing list]:http://groups.google.com/group/mozilla-labs-jetpack
[open bugs]:https://bugzilla.mozilla.org/buglist.cgi?quicksearch=product%3ASDK
[submit bug]:https://bugzilla.mozilla.org/enter_bug.cgi?product=Add-on%20SDK&component=general
[test intro]:https://jetpack.mozillalabs.com/sdk/latest/docs/#guide/implementing-reusable-module
[test API]:https://jetpack.mozillalabs.com/sdk/latest/docs/#module/api-utils/unit-test
[test intro]:https://developer.mozilla.org/en-US/Add-ons/SDK/Tutorials/Unit_testing
[test API]:https://developer.mozilla.org/en-US/Add-ons/SDK/Low-Level_APIs/test_assert
[coding style guide]:https://github.com/mozilla/addon-sdk/wiki/Coding-style-guide
[Add-on SDK repo]:https://github.com/mozilla/addon-sdk
[GitHub]:https://github.com/
[good first bugs]:https://bugzilla.mozilla.org/buglist.cgi?list_id=7345714&columnlist=bug_severity%2Cpriority%2Cassigned_to%2Cbug_status%2Ctarget_milestone%2Cresolution%2Cshort_desc%2Cchangeddate&query_based_on=jetpack-good-1st-bugs&status_whiteboard_type=allwordssubstr&query_format=advanced&status_whiteboard=[good%20first%20bug]&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&bug_status=VERIFIED&product=Add-on%20SDK&known_name=jetpack-good-1st-bugs
[@mossop]:https://github.com/mossop/
[@gozala]:https://github.com/Gozala/
[@wbamberg]:https://github.com/wbamberg/
[@ZER0]:https://github.com/ZER0/
[@erikvold]:https://github.com/erikvold/
[@jsantell]:https://github.com/jsantell

View File

@ -19,7 +19,7 @@ These resources should provide some help:
Please read these two guides if you wish to contribute some patches to the addon-sdk:
* [Contribute Guide](https://github.com/mozilla/addon-sdk/wiki/Contribute)
* [Contribute Guide](https://github.com/mozilla/addon-sdk/blob/master/CONTRIBUTING.md)
* [Style Guide](https://github.com/mozilla/addon-sdk/wiki/Coding-style-guide)
## Issues

View File

View File

@ -3,7 +3,6 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
var BLACKLIST = [];
var readParam = require("./node-scripts/utils").readParam;
var path = require("path");
var Mocha = require("mocha");

View File

@ -37,7 +37,7 @@ describe("jpm test sdk addons", function () {
function fileFilter(root, file) {
var matcher = filterPattern && new RegExp(filterPattern);
if (/^(l10n|simple-prefs|page-mod-debugger)/.test(file)) {
if (/^(l10n-properties|simple-prefs|page-mod-debugger)/.test(file)) {
return false;
}
if (matcher && !matcher.test(file)) {

View File

@ -1,13 +0,0 @@
/* 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";
// Note that this file is temporary workaroud until JPM is smart enough
// to cover it on it's own.
const { utils: Cu } = Components;
const rootURI = __SCRIPT_URI_SPEC__.replace("bootstrap.js", "");
const { require } = Cu.import(`${rootURI}/lib/toolkit/require.js`, {});
const { Bootstrap } = require(`${rootURI}/lib/sdk/addon/bootstrap.js`);
const { startup, shutdown, install, uninstall } = new Bootstrap(rootURI);

0
addon-sdk/source/examples/library-detector/README.md Executable file → Normal file
View File

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 386 B

After

Width:  |  Height:  |  Size: 386 B

View File

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

View File

0
addon-sdk/source/examples/library-detector/lib/main.js Executable file → Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,9 @@
<!-- 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/. -->
<html>
<head>
</head>
<body>
</body>
</html>

View File

@ -0,0 +1,7 @@
/* 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/. */
#devtools-theme-box {
background-color: red !important;
}

View File

@ -0,0 +1,37 @@
/* 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 { Tool } = require("dev/toolbox");
const { Class } = require("sdk/core/heritage");
const { onEnable, onDisable } = require("dev/theme/hooks");
const { Theme, LightTheme } = require("dev/theme");
/**
* This object represents a new theme registered within the Toolbox.
* You can activate it by clicking on "My Light Theme" theme option
* in the Options panel.
* Note that the new theme derives styles from built-in Light theme.
*/
const MyTheme = Theme({
name: "mytheme",
label: "My Light Theme",
styles: [LightTheme, "./theme.css"],
onEnable: function(window, oldTheme) {
console.log("myTheme.onEnable; method override " +
window.location.href);
},
onDisable: function(window, newTheme) {
console.log("myTheme.onDisable; method override " +
window.location.href);
},
});
// Registration
const mytheme = new Tool({
name: "My Tool",
themes: { mytheme: MyTheme }
});

View File

@ -0,0 +1,10 @@
{
"name": "theme",
"title": "theme",
"id": "theme@jetpack",
"description": "How to create new theme for devtools",
"author": "Jan Odvarko",
"license": "MPL 2.0",
"version": "0.1.0",
"main": "lib/main"
}

View File

@ -0,0 +1,10 @@
/* 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";
exports.testMain = function(assert) {
assert.pass("TODO: Write some tests.");
};
require("sdk/test").run(exports);

View File

@ -0,0 +1,135 @@
/* 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";
module.metadata = {
"stability": "experimental"
};
const { Class } = require("../sdk/core/heritage");
const { EventTarget } = require("../sdk/event/target");
const { Disposable, setup, dispose } = require("../sdk/core/disposable");
const { contract, validate } = require("../sdk/util/contract");
const { id: addonID } = require("../sdk/self");
const { onEnable, onDisable } = require("dev/theme/hooks");
const { isString, instanceOf, isFunction } = require("sdk/lang/type");
const { add } = require("sdk/util/array");
const { data } = require("../sdk/self");
const { isLocalURL } = require("../sdk/url");
const makeID = name =>
("dev-theme-" + addonID + (name ? "-" + name : "")).
split(/[ . /]/).join("-").
replace(/[^A-Za-z0-9_\-]/g, "");
const Theme = Class({
extends: Disposable,
implements: [EventTarget],
initialize: function(options) {
this.name = options.name;
this.label = options.label;
this.styles = options.styles;
// Event handlers
this.onEnable = options.onEnable;
this.onDisable = options.onDisable;
},
get id() {
return makeID(this.name || this.label);
},
setup: function() {
// Any initialization steps done at the registration time.
},
getStyles: function() {
if (!this.styles) {
return [];
}
if (isString(this.styles)) {
if (isLocalURL(this.styles)) {
return [data.url(this.styles)];
}
}
let result = [];
for (let style of this.styles) {
if (isString(style)) {
if (isLocalURL(style)) {
style = data.url(style);
}
add(result, style);
} else if (instanceOf(style, Theme)) {
result = result.concat(style.getStyles());
}
}
return result;
},
getClassList: function() {
let result = [];
for (let style of this.styles) {
if (instanceOf(style, Theme)) {
result = result.concat(style.getClassList());
}
}
if (this.name) {
add(result, this.name);
}
return result;
}
});
exports.Theme = Theme;
// Initialization & dispose
setup.define(Theme, (theme) => {
theme.classList = [];
theme.setup();
});
dispose.define(Theme, function(theme) {
theme.dispose();
});
// Validation
validate.define(Theme, contract({
label: {
is: ["string"],
msg: "The `option.label` must be a provided"
},
}));
// Support theme events: apply and unapply the theme.
onEnable.define(Theme, (theme, {window, oldTheme}) => {
if (isFunction(theme.onEnable)) {
theme.onEnable(window, oldTheme);
}
});
onDisable.define(Theme, (theme, {window, newTheme}) => {
if (isFunction(theme.onDisable)) {
theme.onDisable(window, newTheme);
}
});
// Support for built-in themes
const LightTheme = Theme({
name: "theme-light",
styles: "chrome://browser/skin/devtools/light-theme.css",
});
const DarkTheme = Theme({
name: "theme-dark",
styles: "chrome://browser/skin/devtools/dark-theme.css",
});
exports.LightTheme = LightTheme;
exports.DarkTheme = DarkTheme;

View File

@ -0,0 +1,17 @@
/* 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";
module.metadata = {
"stability": "experimental"
};
const { method } = require("method/core");
const onEnable = method("dev/theme/hooks#onEnable");
const onDisable = method("dev/theme/hooks#onDisable");
exports.onEnable = onEnable;
exports.onDisable = onDisable;

View File

@ -13,6 +13,7 @@ const { Class } = require("../sdk/core/heritage");
const { Disposable, setup } = require("../sdk/core/disposable");
const { contract, validate } = require("../sdk/util/contract");
const { each, pairs, values } = require("../sdk/util/sequence");
const { onEnable, onDisable } = require("../dev/theme/hooks");
const { gDevTools } = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
@ -31,13 +32,14 @@ const registerSDKURI = () => {
registerSDKURI();
const Tool = Class({
extends: Disposable,
setup: function(params={}) {
const { panels } = validate(this, params);
const { themes } = validate(this, params);
this.panels = panels;
this.themes = themes;
each(([key, Panel]) => {
const { url, label, tooltip, icon } = validate(Panel.prototype);
@ -60,16 +62,43 @@ const Tool = Class({
}
});
}, pairs(panels));
each(([key, theme]) => {
validate(theme);
setup(theme);
gDevTools.registerTheme({
id: theme.id,
label: theme.label,
stylesheets: theme.getStyles(),
classList: theme.getClassList(),
onApply: (window, oldTheme) => {
onEnable(theme, { window: window,
oldTheme: oldTheme });
},
onUnapply: (window, newTheme) => {
onDisable(theme, { window: window,
newTheme: newTheme });
}
});
}, pairs(themes));
},
dispose: function() {
each(Panel => gDevTools.unregisterTool(Panel.prototype.id),
values(this.panels));
each(Theme => gDevTools.unregisterTheme(Theme.prototype.id),
values(this.themes));
}
});
validate.define(Tool, contract({
panels: {
is: ["object", "undefined"]
},
themes: {
is: ["object", "undefined"]
}
}));
exports.Tool = Tool;

View File

@ -9,27 +9,19 @@ const globalMM = Components.classes["@mozilla.org/globalmessagemanager;1"].
// Load frame scripts from the same dir as this module.
// Since this JSM will be loaded using require(), PATH will be
// overridden while running tests, just like any other module.
const PATH = __URI__.replace('FrameScriptManager.jsm', '');
const PATH = __URI__.replace('framescript/FrameScriptManager.jsm', '');
// ensure frame scripts are loaded only once
let loadedTabEvents = false;
function enableTabEvents() {
if (loadedTabEvents)
return;
loadedTabEvents = true;
globalMM.loadFrameScript(PATH + 'tab-events.js', true);
// Builds a unique loader ID for this runtime. We prefix with the SDK path so
// overriden versions of the SDK don't conflict
let LOADER_ID = 0;
this.getNewLoaderID = () => {
return PATH + ":" + LOADER_ID++;
}
let loadedCMEvents = false;
function enableCMEvents() {
if (loadedCMEvents)
return;
loadedCMEvents = true;
globalMM.loadFrameScript(PATH + 'contextmenu-events.js', true);
const frame_script = function(contentFrame, PATH) {
let { registerContentFrame } = Components.utils.import(PATH + 'framescript/content.jsm', {});
registerContentFrame(contentFrame);
}
globalMM.loadFrameScript("data:,(" + frame_script.toString() + ")(this, " + JSON.stringify(PATH) + ");", true);
const EXPORTED_SYMBOLS = ['enableTabEvents', 'enableCMEvents'];
this.EXPORTED_SYMBOLS = ['getNewLoaderID'];

View File

@ -1,33 +0,0 @@
/* 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 { utils: Cu, classes: Cc, interfaces: Ci } = Components;
const { Loader } = Cu.import('resource://gre/modules/commonjs/toolkit/loader.js', {});
const cpmm = Cc['@mozilla.org/childprocessmessagemanager;1'].getService(Ci.nsISyncMessageSender);
// one Loader instance per addon (per @loader/options to be precise)
let addons = new Map();
cpmm.addMessageListener('sdk/loader/unload', ({ data: options }) => {
let key = JSON.stringify(options);
let addon = addons.get(key);
if (addon)
addon.loader.unload();
addons.delete(key);
})
// create a Loader instance from @loader/options
function loader(options) {
let key = JSON.stringify(options);
let addon = addons.get(key) || {};
if (!addon.loader) {
addon.loader = Loader.Loader(options);
addon.require = Loader.Require(addon.loader, { id: 'LoaderHelper' });
addons.set(key, addon);
}
return addon;
}
const EXPORTED_SYMBOLS = ['loader'];

View File

@ -0,0 +1,92 @@
/* 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 { utils: Cu, classes: Cc, interfaces: Ci } = Components;
const { Services } = Cu.import('resource://gre/modules/Services.jsm');
const cpmm = Cc['@mozilla.org/childprocessmessagemanager;1'].
getService(Ci.nsISyncMessageSender);
this.EXPORTED_SYMBOLS = ["registerContentFrame"];
// This may be an overriden version of the SDK so use the PATH as a key for the
// initial messages before we have a loaderID.
const PATH = __URI__.replace('framescript/content.jsm', '');
const { Loader } = Cu.import(PATH + 'toolkit/loader.js', {});
// one Loader instance per addon (per @loader/options to be precise)
let addons = new Map();
// Tell the parent that a new process is ready
cpmm.sendAsyncMessage('sdk/remote/process/start', {
modulePath: PATH
});
// Load a child process module loader with the given loader options
cpmm.addMessageListener('sdk/remote/process/load', ({ data: { modulePath, loaderID, options, reason } }) => {
if (modulePath != PATH)
return;
// During startup races can mean we get a second load message
if (addons.has(loaderID))
return;
let loader = Loader.Loader(options);
let addon = {
loader,
require: Loader.Require(loader, { id: 'LoaderHelper' }),
}
addons.set(loaderID, addon);
cpmm.sendAsyncMessage('sdk/remote/process/attach', {
loaderID,
processID: Services.appinfo.processID,
isRemote: Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT
});
addon.child = addon.require('sdk/remote/child');
for (let contentFrame of frames.values())
addon.child.registerContentFrame(contentFrame);
});
// Unload a child process loader
cpmm.addMessageListener('sdk/remote/process/unload', ({ data: { loaderID, reason } }) => {
if (!addons.has(loaderID))
return;
let addon = addons.get(loaderID);
Loader.unload(addon.loader, reason);
// We want to drop the reference to the loader but never allow creating a new
// loader with the same ID
addons.set(loaderID, {});
})
let frames = new Set();
this.registerContentFrame = contentFrame => {
contentFrame.addEventListener("unload", () => {
unregisterContentFrame(contentFrame);
}, false);
frames.add(contentFrame);
for (let addon of addons.values()) {
if ("child" in addon)
addon.child.registerContentFrame(contentFrame);
}
};
function unregisterContentFrame(contentFrame) {
frames.delete(contentFrame);
for (let addon of addons.values()) {
if ("child" in addon)
addon.child.unregisterContentFrame(contentFrame);
}
}

View File

@ -1,63 +0,0 @@
/* 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 { classes: Cc, interfaces: Ci, utils: Cu } = Components;
const { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
// Holds remote items for this frame.
let keepAlive = new Map();
// Called to create remote proxies for items. If they already exist we destroy
// and recreate. This cna happen if the item changes in some way or in odd
// timing cases where the frame script is create around the same time as the
// item is created in the main process
addMessageListener('sdk/contextmenu/createitems', ({ data: { items, addon }}) => {
let { loader } = Cu.import(addon.paths[''] + 'framescript/LoaderHelper.jsm', {});
for (let itemoptions of items) {
let { RemoteItem } = loader(addon).require('sdk/content/context-menu');
let item = new RemoteItem(itemoptions, this);
let oldItem = keepAlive.get(item.id);
if (oldItem) {
oldItem.destroy();
}
keepAlive.set(item.id, item);
}
});
addMessageListener('sdk/contextmenu/destroyitems', ({ data: { items }}) => {
for (let id of items) {
let item = keepAlive.get(id);
item.destroy();
keepAlive.delete(id);
}
});
sendAsyncMessage('sdk/contextmenu/requestitems');
Services.obs.addObserver(function(subject, topic, data) {
// Many frame scripts run in the same process, check that the context menu
// node is in this frame
let { event: { target: popupNode }, addonInfo } = subject.wrappedJSObject;
if (popupNode.ownerDocument.defaultView.top != content)
return;
for (let item of keepAlive.values()) {
item.getContextState(popupNode, addonInfo);
}
}, "content-contextmenu", false);
addMessageListener('sdk/contextmenu/activateitems', ({ data: { items, data }, objects: { popupNode }}) => {
for (let id of items) {
let item = keepAlive.get(id);
if (!item)
continue;
item.activate(popupNode, data);
}
});

View File

@ -1,51 +0,0 @@
/* 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 { classes: Cc, interfaces: Ci, utils: Cu } = Components;
const observerSvc = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
// map observer topics to tab event names
const EVENTS = {
'content-document-interactive': 'ready',
'chrome-document-interactive': 'ready',
'content-document-loaded': 'load',
'chrome-document-loaded': 'load',
// 'content-page-shown': 'pageshow', // bug 1024105
}
function listener(subject, topic) {
// observer service keeps a strong reference to the listener, and this
// method can get called after the tab is closed, so we should remove it.
if (!docShell)
observerSvc.removeObserver(listener, topic);
else if (subject === content.document)
sendAsyncMessage('sdk/tab/event', { type: EVENTS[topic] });
}
for (let topic in EVENTS)
observerSvc.addObserver(listener, topic, false);
// bug 1024105 - content-page-shown notification doesn't pass persisted param
addEventListener('pageshow', ({ target, type, persisted }) => {
if (target === content.document)
sendAsyncMessage('sdk/tab/event', { type, persisted });
}, true);
// workers for windows in this tab
let keepAlive = new Map();
addMessageListener('sdk/worker/create', ({ data: { options, addon }}) => {
options.manager = this;
let { loader } = Cu.import(addon.paths[''] + 'framescript/LoaderHelper.jsm', {});
let { WorkerChild } = loader(addon).require('sdk/content/worker-child');
sendAsyncMessage('sdk/worker/attach', { id: options.id });
keepAlive.set(options.id, new WorkerChild(options));
})
addMessageListener('sdk/worker/event', ({ data: { id, args: [event]}}) => {
if (event === 'detach')
keepAlive.delete(id);
})

View File

@ -72,23 +72,28 @@ Bootstrap.prototype = {
}
},
install(addon, reason) {
return new Promise(resolve => resolve());
},
uninstall(addon, reason) {
const {id} = addon;
return new Promise(resolve => {
const {id} = addon;
prefs.reset(`extensions.${id}.sdk.domain`);
prefs.reset(`extensions.${id}.sdk.version`);
prefs.reset(`extensions.${id}.sdk.rootURI`);
prefs.reset(`extensions.${id}.sdk.baseURI`);
prefs.reset(`extensions.${id}.sdk.load.reason`);
prefs.reset(`extensions.${id}.sdk.domain`);
prefs.reset(`extensions.${id}.sdk.version`);
prefs.reset(`extensions.${id}.sdk.rootURI`);
prefs.reset(`extensions.${id}.sdk.baseURI`);
prefs.reset(`extensions.${id}.sdk.load.reason`);
resolve();
});
},
startup(addon, reasonCode) {
const { id, version, resourceURI: {spec: addonURI} } = addon;
const { id, version, resourceURI: { spec: addonURI } } = addon;
const rootURI = this.mountURI || addonURI;
const reason = REASON[reasonCode];
const self = this;
spawn(function*() {
return spawn(function*() {
const metadata = JSON.parse(yield readURI(`${rootURI}package.json`));
const domain = readDomain(id);
const baseURI = `resource://${domain}/`;
@ -122,7 +127,7 @@ Bootstrap.prototype = {
},
noQuit: prefs.get(`extensions.${id}.sdk.test.no-quit`, false)
});
this.loader = loader;
self.loader = loader;
const module = Module("package.json", `${baseURI}package.json`);
const require = Require(loader, module);
@ -137,24 +142,30 @@ Bootstrap.prototype = {
});
},
shutdown(addon, code) {
const { loader, domain } = this;
this.unmount();
this.unload(REASON[code]);
return this.unload(REASON[code]);
},
unload(reason) {
const {loader} = this;
if (loader) {
this.loader = null;
unload(loader, reason);
setTimeout(() => {
for (let uri of Object.keys(loader.sandboxes)) {
Cu.nukeSandbox(loader.sandboxes[uri]);
delete loader.sandboxes[uri];
delete loader.modules[uri];
}
}, 1000);
}
return new Promise(resolve => {
const { loader } = this;
if (loader) {
this.loader = null;
unload(loader, reason);
setTimeout(() => {
for (let uri of Object.keys(loader.sandboxes)) {
Cu.nukeSandbox(loader.sandboxes[uri]);
delete loader.sandboxes[uri];
delete loader.modules[uri];
}
resolve();
}, 1000);
}
else {
resolve();
}
});
}
};
exports.Bootstrap = Bootstrap;

View File

@ -81,12 +81,14 @@ function startup(reason, options) Startup.onceInitialized.then(() => {
// Exports data to a pseudo module so that api-utils/l10n/core
// can get access to it
definePseudo(options.loader, '@l10n/data', data ? data : null);
return ready.then(() => run(options, !!data));
return ready;
}).then(function() {
run(options);
}).then(null, console.exception);
return void 0; // otherwise we raise a warning, see bug 910304
});
function run(options, hasL10n) {
function run(options) {
try {
// Try initializing HTML localization before running main module. Just print
// an exception in case of error, instead of preventing addon to be run.
@ -94,7 +96,7 @@ function run(options, hasL10n) {
// Do not enable HTML localization while running test as it is hard to
// disable. Because unit tests are evaluated in a another Loader who
// doesn't have access to this current loader.
if (hasL10n && options.main !== 'sdk/test/runner') {
if (options.main !== 'sdk/test/runner') {
require('../l10n/html').enable();
}
}

View File

@ -9,6 +9,8 @@ const { WorkerChild } = require("./worker-child");
const { getInnerId } = require("../window/utils");
const { Ci } = require("chrome");
const { Services } = require("resource://gre/modules/Services.jsm");
const system = require('../system/events');
const { process } = require('../remote/child');
// These functions are roughly copied from sdk/selection which doesn't work
// in the content process
@ -133,7 +135,7 @@ CONTEXTS.PageContext = Class({
if (!popupNode.ownerDocument.defaultView.getSelection().isCollapsed)
return false;
// If the clicked node or any of its ancestors is one of the blacklisted
// If the clicked node or any of its ancestors is one of the blocked
// NON_PAGE_CONTEXT_ELTS then this context does not match
while (!(popupNode instanceof Ci.nsIDOMDocument)) {
if (NON_PAGE_CONTEXT_ELTS.some(function(type) popupNode instanceof type))
@ -285,7 +287,7 @@ function getItemWorkerForWindow(item, window) {
worker = ContextWorker({
id: item.id,
window: id,
window,
manager: item.manager,
contentScript: item.contentScript,
contentScriptFile: item.contentScriptFile,
@ -312,12 +314,14 @@ let RemoteItem = Class({
this.manager = manager;
this.workerMap = new Map();
keepAlive.set(this.id, this);
},
destroy: function() {
for (let worker of this.workerMap.values()) {
worker.destroy();
}
keepAlive.delete(this.id);
},
activate: function(popupNode, data) {
@ -333,15 +337,19 @@ let RemoteItem = Class({
// Fills addonInfo with state data to send through to the main process
getContextState: function(popupNode, addonInfo) {
if (!(self.id in addonInfo))
addonInfo[self.id] = {};
if (!(self.id in addonInfo)) {
addonInfo[self.id] = {
processID: process.id,
items: {}
};
}
let worker = getItemWorkerForWindow(this, popupNode.ownerDocument.defaultView);
let contextStates = {};
for (let context of this.contexts)
contextStates[context.id] = context.getState(popupNode);
addonInfo[self.id][this.id] = {
addonInfo[self.id].items[this.id] = {
// It isn't ideal to create a PageContext for every item but there isn't
// a good shared place to do it.
pageContext: (new CONTEXTS.PageContext()).getState(popupNode),
@ -352,3 +360,49 @@ let RemoteItem = Class({
}
});
exports.RemoteItem = RemoteItem;
// Holds remote items for this frame.
let keepAlive = new Map();
// Called to create remote proxies for items. If they already exist we destroy
// and recreate. This can happen if the item changes in some way or in odd
// timing cases where the frame script is create around the same time as the
// item is created in the main process
process.port.on('sdk/contextmenu/createitems', (process, items) => {
for (let itemoptions of items) {
let oldItem = keepAlive.get(itemoptions.id);
if (oldItem) {
oldItem.destroy();
}
let item = new RemoteItem(itemoptions, this);
}
});
process.port.on('sdk/contextmenu/destroyitems', (process, items) => {
for (let id of items) {
let item = keepAlive.get(id);
item.destroy();
}
});
let lastPopupNode = null;
system.on('content-contextmenu', ({ subject }) => {
let { event: { target: popupNode }, addonInfo } = subject.wrappedJSObject;
lastPopupNode = popupNode;
for (let item of keepAlive.values()) {
item.getContextState(popupNode, addonInfo);
}
}, true);
process.port.on('sdk/contextmenu/activateitems', (process, items, data) => {
for (let id of items) {
let item = keepAlive.get(id);
if (!item)
continue;
item.activate(lastPopupNode, data);
}
});

View File

@ -0,0 +1,94 @@
/* 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";
module.metadata = {
"stability": "unstable"
};
const { Ci } = require("chrome");
const core = require("../l10n/core");
const { loadSheet, removeSheet } = require("../stylesheet/utils");
const { process, frames } = require("../remote/child");
const assetsURI = require('../self').data.url();
const hideSheetUri = "data:text/css,:root {visibility: hidden !important;}";
// Taken from Gaia:
// https://github.com/andreasgal/gaia/blob/04fde2640a7f40314643016a5a6c98bf3755f5fd/webapi.js#L1470
function translateElement(element) {
element = element || document;
// check all translatable children (= w/ a `data-l10n-id' attribute)
var children = element.querySelectorAll('*[data-l10n-id]');
var elementCount = children.length;
for (var i = 0; i < elementCount; i++) {
var child = children[i];
// translate the child
var key = child.dataset.l10nId;
var data = core.get(key);
if (data)
child.textContent = data;
}
}
exports.translateElement = translateElement;
function onDocumentReady2Translate(event) {
let document = event.target;
document.removeEventListener("DOMContentLoaded", onDocumentReady2Translate,
false);
translateElement(document);
try {
// Finally display document when we finished replacing all text content
if (document.defaultView)
removeSheet(document.defaultView, hideSheetUri, 'user');
}
catch(e) {
console.exception(e);
}
}
function onContentWindow({ target: document }) {
// Accept only HTML documents
if (!(document instanceof Ci.nsIDOMHTMLDocument))
return;
// Bug 769483: data:URI documents instanciated with nsIDOMParser
// have a null `location` attribute at this time
if (!document.location)
return;
// Accept only document from this addon
if (document.location.href.indexOf(assetsURI) !== 0)
return;
try {
// First hide content of the document in order to have content blinking
// between untranslated and translated states
loadSheet(document.defaultView, hideSheetUri, 'user');
}
catch(e) {
console.exception(e);
}
// Wait for DOM tree to be built before applying localization
document.addEventListener("DOMContentLoaded", onDocumentReady2Translate,
false);
}
// Listen to creation of content documents in order to translate them as soon
// as possible in their loading process
const ON_CONTENT = "DOMDocElementInserted";
function enable() {
frames.addEventListener(ON_CONTENT, onContentWindow, true);
}
process.port.on("sdk/l10n/html/enable", enable);
function disable() {
frames.removeEventListener(ON_CONTENT, onContentWindow, true);
}
process.port.on("sdk/l10n/html/disable", disable);

View File

@ -56,6 +56,10 @@ function detach(modification, target) {
else {
let documents = iterator(modification);
for (let document of documents) {
let window = document.defaultView;
// The window might have already gone away
if (!window)
continue;
detachFrom(modification, document.defaultView);
remove(modification, document);
}

View File

@ -0,0 +1,237 @@
/* 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";
module.metadata = {
"stability": "stable"
};
const { getAttachEventType } = require('../content/utils');
const { Class } = require('../core/heritage');
const { Disposable } = require('../core/disposable');
const { WeakReference } = require('../core/reference');
const { WorkerChild } = require('./worker-child');
const { EventTarget } = require('../event/target');
const { on, emit, once, setListeners } = require('../event/core');
const { on: domOn, removeListener: domOff } = require('../dom/events');
const { isRegExp, isUndefined } = require('../lang/type');
const { merge } = require('../util/object');
const { isBrowser, getFrames } = require('../window/utils');
const { getTabs, getTabContentWindow, getTabForContentWindow,
getURI: getTabURI } = require('../tabs/utils');
const { ignoreWindow } = require('../private-browsing/utils');
const { Style } = require("../stylesheet/style");
const { attach, detach } = require("../content/mod");
const { has, hasAny } = require("../util/array");
const { Rules } = require("../util/rules");
const { List, addListItem, removeListItem } = require('../util/list');
const { when } = require("../system/unload");
const { uuid } = require('../util/uuid');
const { frames, process } = require('../remote/child');
const pagemods = new Map();
const styles = new WeakMap();
let styleFor = (mod) => styles.get(mod);
// Helper functions
let modMatchesURI = (mod, uri) => mod.include.matchesAny(uri) && !mod.exclude.matchesAny(uri);
/**
* PageMod constructor (exported below).
* @constructor
*/
const ChildPageMod = Class({
implements: [
EventTarget,
Disposable,
],
setup: function PageMod(model) {
merge(this, model);
// Set listeners on {PageMod} itself, not the underlying worker,
// like `onMessage`, as it'll get piped.
setListeners(this, model);
function deserializeRules(rules) {
for (let rule of rules) {
yield rule.type == "string" ? rule.value
: new RegExp(rule.pattern, rule.flags);
}
}
let include = [...deserializeRules(this.include)];
this.include = Rules();
this.include.add.apply(this.include, include);
let exclude = [...deserializeRules(this.exclude)];
this.exclude = Rules();
this.exclude.add.apply(this.exclude, exclude);
if (this.contentStyle || this.contentStyleFile) {
styles.set(this, Style({
uri: this.contentStyleFile,
source: this.contentStyle
}));
}
pagemods.set(this.id, this);
this.seenDocuments = new WeakMap();
// `applyOnExistingDocuments` has to be called after `pagemods.add()`
// otherwise its calls to `onContent` method won't do anything.
if (has(this.attachTo, 'existing'))
applyOnExistingDocuments(this);
},
dispose: function() {
let style = styleFor(this);
if (style)
detach(style);
for (let i in this.include)
this.include.remove(this.include[i]);
pagemods.delete(this.id);
}
});
function onContentWindow({ target: document }) {
// Return if we have no pagemods
if (pagemods.size === 0)
return;
let window = document.defaultView;
// XML documents don't have windows, and we don't yet support them.
if (!window)
return;
// Frame event listeners are bound to the frame the event came from by default
let frame = this;
// We apply only on documents in tabs of Firefox
if (!frame.isTab)
return;
// When the tab is private, only addons with 'private-browsing' flag in
// their package.json can apply content script to private documents
if (ignoreWindow(window))
return;
for (let pagemod of pagemods.values()) {
if (modMatchesURI(pagemod, window.location.href))
onContent(pagemod, window);
}
}
frames.addEventListener("DOMDocElementInserted", onContentWindow, true);
function applyOnExistingDocuments (mod) {
for (let frame of frames) {
// Fake a newly created document
let window = frame.content;
// on startup with e10s, contentWindow might not exist yet,
// in which case we will get notified by "document-element-inserted".
if (!window || !window.frames)
return;
let uri = window.location.href;
if (has(mod.attachTo, "top") && modMatchesURI(mod, uri))
onContent(mod, window);
if (has(mod.attachTo, "frame"))
getFrames(window).
filter(iframe => modMatchesURI(mod, iframe.location.href)).
forEach(frame => onContent(mod, frame));
}
}
function createWorker(mod, window) {
let workerId = String(uuid());
// Instruct the parent to connect to this worker. Do this first so the parent
// side is connected before the worker attempts to send any messages there
let frame = frames.getFrameForWindow(window.top);
frame.port.emit('sdk/page-mod/worker-create', mod.id, {
id: workerId,
url: window.location.href
});
// Create a child worker and notify the parent
let worker = WorkerChild({
id: workerId,
window: window,
contentScript: mod.contentScript,
contentScriptFile: mod.contentScriptFile,
contentScriptOptions: mod.contentScriptOptions
});
once(worker, 'detach', () => worker.destroy());
}
function onContent (mod, window) {
let isTopDocument = window.top === window;
// Is a top level document and `top` is not set, ignore
if (isTopDocument && !has(mod.attachTo, "top"))
return;
// Is a frame document and `frame` is not set, ignore
if (!isTopDocument && !has(mod.attachTo, "frame"))
return;
// ensure we attach only once per document
let seen = mod.seenDocuments;
if (seen.has(window.document))
return;
seen.set(window.document, true);
let style = styleFor(mod);
if (style)
attach(style, window);
// Immediately evaluate content script if the document state is already
// matching contentScriptWhen expectations
if (isMatchingAttachState(mod, window)) {
createWorker(mod, window);
return;
}
let eventName = getAttachEventType(mod) || 'load';
domOn(window, eventName, function onReady (e) {
if (e.target.defaultView !== window)
return;
domOff(window, eventName, onReady, true);
createWorker(mod, window);
// Attaching is asynchronous so if the document is already loaded we will
// miss the pageshow event so send a synthetic one.
if (window.document.readyState == "complete") {
mod.on('attach', worker => {
try {
worker.send('pageshow');
emit(worker, 'pageshow');
}
catch (e) {
// This can fail if an earlier attach listener destroyed the worker
}
});
}
}, true);
}
function isMatchingAttachState (mod, window) {
let state = window.document.readyState;
return 'start' === mod.contentScriptWhen ||
// Is `load` event already dispatched?
'complete' === state ||
// Is DOMContentLoaded already dispatched and waiting for it?
('ready' === mod.contentScriptWhen && state === 'interactive')
}
process.port.on('sdk/page-mod/create', (process, model) => {
if (pagemods.has(model.id))
return;
new ChildPageMod(model);
});
process.port.on('sdk/page-mod/destroy', (process, id) => {
let mod = pagemods.get(id);
if (mod)
mod.destroy();
});

View File

@ -0,0 +1,36 @@
/* 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 system = require('sdk/system/events');
const { frames } = require('sdk/remote/child');
// map observer topics to tab event names
const EVENTS = {
'content-document-interactive': 'ready',
'chrome-document-interactive': 'ready',
'content-document-loaded': 'load',
'chrome-document-loaded': 'load',
// 'content-page-shown': 'pageshow', // bug 1024105
}
function topicListener({ subject, type }) {
let window = subject.defaultView;
if (!window)
return;
let frame = frames.getFrameForWindow(subject.defaultView);
if (frame)
frame.port.emit('sdk/tab/event', EVENTS[type]);
}
for (let topic in EVENTS)
system.on(topic, topicListener, true);
// bug 1024105 - content-page-shown notification doesn't pass persisted param
function eventListener({target, type, persisted}) {
let frame = this;
if (target === frame.content.document)
frame.port.emit('sdk/tab/event', type, persisted);
}
frames.addEventListener('pageshow', eventListener, true);

View File

@ -45,6 +45,9 @@ exports.getAttachEventType = getAttachEventType;
let attach = method('worker-attach');
exports.attach = attach;
let connect = method('worker-connect');
exports.connect = connect;
let detach = method('worker-detach');
exports.detach = detach;

View File

@ -5,12 +5,15 @@
const { merge } = require('../util/object');
const { Class } = require('../core/heritage');
const { emit } = require('../event/core');
const { EventTarget } = require('../event/target');
const { getInnerId, getByInnerId } = require('../window/utils');
const { instanceOf, isObject } = require('../lang/type');
const { on: observe } = require('../system/events');
const system = require('../system/events');
const { when } = require('../system/unload');
const { WorkerSandbox } = require('./sandbox');
const { Ci } = require('chrome');
const { process, frames } = require('../remote/child');
const EVENTS = {
'chrome-page-shown': 'pageshow',
@ -20,10 +23,17 @@ const EVENTS = {
'inner-window-destroyed': 'detach',
}
// The parent Worker must have been created (or an async message sent to spawn
// its creation) before creating the WorkerChild or messages from the content
// script to the parent will get lost.
const WorkerChild = Class({
implements: [EventTarget],
initialize(options) {
merge(this, options);
keepAlive.set(this.id, this);
this.windowId = getInnerId(this.window);
this.port = EventTarget();
this.port.on('*', this.send.bind(this, 'event'));
@ -32,57 +42,77 @@ const WorkerChild = Class({
this.observe = this.observe.bind(this);
for (let topic in EVENTS)
observe(topic, this.observe);
system.on(topic, this.observe);
this.receive = this.receive.bind(this);
this.manager.addMessageListener('sdk/worker/message', this.receive);
process.port.on('sdk/worker/message', this.receive);
let window = getByInnerId(this.window);
this.sandbox = WorkerSandbox(this, window);
this.sandbox = WorkerSandbox(this, this.window);
if (options.currentReadyState != "complete" &&
window.document.readyState == "complete") {
// If we attempted to attach the worker before the document was loaded but
// it has now completed loading then the parent should reasonably expect
// to see a pageshow event.
this.sandbox.emitSync("pageshow");
this.send("pageshow");
}
this.frozen = false;
this.frozenMessages = [];
this.on('pageshow', () => {
this.frozen = false;
this.frozenMessages.forEach(args => this.receive(null, this.id, args));
this.frozenMessages = [];
});
this.on('pagehide', () => {
this.frozen = true;
});
},
// messages
receive({ data: { id, args }}) {
receive(process, id, args) {
if (id !== this.id)
return;
this.sandbox.emit(...args);
if (this.frozen)
this.frozenMessages.push(args);
else
this.sandbox.emit(...args);
if (args[0] === 'detach')
this.destroy(args[1]);
},
send(...args) {
args = JSON.parse(JSON.stringify(args, exceptions));
if (this.manager.content)
this.manager.sendAsyncMessage('sdk/worker/event', { id: this.id, args });
process.port.emit('sdk/worker/event', this.id, args);
},
// notifications
observe({ type, subject }) {
if (!this.sandbox)
return;
if (subject.defaultView && getInnerId(subject.defaultView) === this.window) {
if (subject.defaultView && getInnerId(subject.defaultView) === this.windowId) {
this.sandbox.emitSync(EVENTS[type]);
this.send(EVENTS[type]);
emit(this, EVENTS[type]);
}
if (type === 'inner-window-destroyed' &&
subject.QueryInterface(Ci.nsISupportsPRUint64).data === this.window) {
subject.QueryInterface(Ci.nsISupportsPRUint64).data === this.windowId) {
this.destroy();
}
},
get frame() {
return frames.getFrameForWindow(this.window.top);
},
// detach/destroy: unload and release the sandbox
destroy(reason) {
if (!this.sandbox)
return;
if (this.manager.content)
this.manager.removeMessageListener('sdk/worker/message', this.receive);
for (let topic in EVENTS)
system.off(topic, this.observe);
process.port.off('sdk/worker/message', this.receive);
this.sandbox.destroy(reason);
this.sandbox = null;
keepAlive.delete(this.id);
this.send('detach');
}
})
@ -96,3 +126,19 @@ function exceptions(key, value) {
let { message, fileName, lineNumber, stack, name } = value;
return { _errorType, message, fileName, lineNumber, stack, name };
}
// workers for windows in this tab
let keepAlive = new Map();
process.port.on('sdk/worker/create', (process, options) => {
options.window = getByInnerId(options.window);
if (!options.window)
return;
let worker = new WorkerChild(options);
});
when(reason => {
for (let worker of keepAlive.values())
worker.destroy(reason);
});

View File

@ -8,26 +8,21 @@ module.metadata = {
};
const { emit } = require('../event/core');
const { omit } = require('../util/object');
const { omit, merge } = require('../util/object');
const { Class } = require('../core/heritage');
const { method } = require('../lang/functional');
const { getInnerId } = require('../window/utils');
const { EventTarget } = require('../event/target');
const { when, ensure } = require('../system/unload');
const { getTabForWindow } = require('../tabs/helpers');
const { getTabForContentWindow, getBrowserForTab } = require('../tabs/utils');
const { isPrivate } = require('../private-browsing/utils');
const { getFrameElement } = require('../window/utils');
const { attach, detach, destroy } = require('./utils');
const { getTabForBrowser, getTabForContentWindow, getBrowserForTab } = require('../tabs/utils');
const { attach, connect, detach, destroy } = require('./utils');
const { ensure } = require('../system/unload');
const { on: observe } = require('../system/events');
const { uuid } = require('../util/uuid');
const { Ci, Cc } = require('chrome');
const ppmm = Cc["@mozilla.org/parentprocessmessagemanager;1"].
getService(Ci.nsIMessageBroadcaster);
// null-out cycles in .modules to make @loader/options JSONable
const ADDON = omit(require('@loader/options'), ['modules', 'globals']);
const { Ci } = require('chrome');
const { modelFor: tabFor } = require('sdk/model/core');
const { remoteRequire, processes, frames } = require('../remote/parent');
remoteRequire('sdk/content/worker-child');
const workers = new WeakMap();
let modelFor = (worker) => workers.get(worker);
@ -35,151 +30,158 @@ let modelFor = (worker) => workers.get(worker);
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 ERR_FROZEN = "The page is currently hidden and can no longer be used " +
"until it is visible again.";
// a handle for communication between content script and addon code
const Worker = Class({
implements: [EventTarget],
initialize(options = {}) {
ensure(this, 'detach');
let model = {
inited: false,
earlyEvents: [], // fired before worker was inited
frozen: true, // document is in BFcache, let it go
attached: false,
destroyed: false,
earlyEvents: [], // fired before worker was attached
frozen: true, // document is not yet active
options,
};
workers.set(this, model);
ensure(this, 'destroy');
this.on('detach', this.detach);
EventTarget.prototype.initialize.call(this, options);
this.receive = this.receive.bind(this);
model.observe = ({ subject }) => {
let id = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
if (model.window && getInnerId(model.window) === id)
this.detach();
}
observe('inner-window-destroyed', model.observe);
this.port = EventTarget();
this.port.emit = this.send.bind(this, 'event');
this.postMessage = this.send.bind(this, 'message');
if ('window' in options)
attach(this, options.window);
if ('window' in options) {
let window = options.window;
delete options.window;
attach(this, window);
}
},
// messages
receive({ data: { id, args }}) {
receive(process, id, args) {
let model = modelFor(this);
if (id !== model.id || !model.childWorker)
if (id !== model.id || !model.attached)
return;
if (model.destroyed && args[0] != 'detach')
return;
if (args[0] === 'event')
emit(this.port, ...args.slice(1))
else
emit(this, ...args);
},
send(...args) {
let model = modelFor(this);
if (!model.inited) {
if (model.destroyed && args[0] !== 'detach')
throw new Error(ERR_DESTROYED);
if (!model.attached) {
model.earlyEvents.push(args);
return;
}
if (!model.childWorker && args[0] !== 'detach')
throw new Error(ERR_DESTROYED);
if (model.frozen && args[0] !== 'detach')
throw new Error(ERR_FROZEN);
try {
model.manager.sendAsyncMessage('sdk/worker/message', { id: model.id, args });
} catch (e) {
//
}
processes.port.emit('sdk/worker/message', model.id, args);
},
// properties
get url() {
let { window } = modelFor(this);
return window && window.document.location.href;
let { url } = modelFor(this);
return url;
},
get contentURL() {
let { window } = modelFor(this);
return window && window.document.URL;
return this.url;
},
get tab() {
let { window } = modelFor(this);
return window && getTabForWindow(window);
require('sdk/tabs');
let { frame } = modelFor(this);
if (!frame)
return null;
let rawTab = getTabForBrowser(frame.frameElement);
return rawTab && tabFor(rawTab);
},
toString: () => '[object Worker]',
// methods
attach: method(attach),
detach: method(detach),
destroy: method(destroy),
})
exports.Worker = Worker;
attach.define(Worker, function(worker, window) {
// This method of attaching should be deprecated
let model = modelFor(worker);
if (model.attached)
detach(worker);
model.window = window;
model.options.window = getInnerId(window);
model.options.currentReadyState = window.document.readyState;
model.id = model.options.id = String(uuid());
let frame = null;
let tab = getTabForContentWindow(window.top);
if (tab)
frame = frames.getFrameForBrowser(getBrowserForTab(tab));
let tab = getTabForContentWindow(window);
if (tab) {
model.manager = getBrowserForTab(tab).messageManager;
} else {
model.manager = getFrameElement(window.top).frameLoader.messageManager;
}
model.manager.addMessageListener('sdk/worker/event', worker.receive);
model.manager.addMessageListener('sdk/worker/attach', attach);
model.manager.sendAsyncMessage('sdk/worker/create', {
options: model.options,
addon: ADDON
merge(model.options, {
id: String(uuid()),
window: getInnerId(window),
url: String(window.location)
});
function attach({ data }) {
if (data.id !== model.id)
return;
model.manager.removeMessageListener('sdk/worker/attach', attach);
model.childWorker = true;
processes.port.emit('sdk/worker/create', model.options);
worker.on('pageshow', () => model.frozen = false);
worker.on('pagehide', () => model.frozen = true);
model.inited = true;
model.frozen = false;
model.earlyEvents.forEach(args => worker.send(...args));
emit(worker, 'attach', window);
}
connect(worker, frame, model.options);
})
connect.define(Worker, function(worker, frame, { id, url }) {
let model = modelFor(worker);
if (model.attached)
detach(worker);
model.id = id;
model.frame = frame;
model.url = url;
// Messages from content -> chrome come through the process message manager
// since that lives longer than the frame message manager
processes.port.on('sdk/worker/event', worker.receive);
model.attached = true;
model.destroyed = false;
model.frozen = false;
model.earlyEvents.forEach(args => worker.send(...args));
model.earlyEvents = [];
emit(worker, 'attach', model.window);
});
// unload and release the child worker, release window reference
detach.define(Worker, function(worker, reason) {
detach.define(Worker, function(worker) {
let model = modelFor(worker);
worker.send('detach', reason);
if (!model.childWorker)
if (!model.attached)
return;
model.childWorker = null;
model.earlyEvents = [];
processes.port.off('sdk/worker/event', worker.receive);
model.attached = false;
model.destroyed = true;
model.window = null;
emit(worker, 'detach');
model.manager.removeMessageListener('sdk/worker/event', this.receive);
})
});
isPrivate.define(Worker, ({ tab }) => isPrivate(tab));
// unlod worker, release references
// Something in the parent side has destroyed the worker, tell the child to
// detach, the child will respond when it has detached
destroy.define(Worker, function(worker, reason) {
detach(worker, reason);
modelFor(worker).inited = true;
})
let model = modelFor(worker);
model.destroyed = true;
if (!model.attached)
return;
// unload Loaders used for creating WorkerChild instances in each process
when(() => ppmm.broadcastAsyncMessage('sdk/loader/unload', { data: ADDON }));
worker.send('detach', reason);
});

View File

@ -19,7 +19,6 @@ const { validateOptions, getTypeOf } = require("./deprecated/api-utils");
const { URL, isValidURI } = require("./url");
const { WindowTracker, browserWindowIterator } = require("./deprecated/window-utils");
const { isBrowser, getInnerId } = require("./window/utils");
const { Ci, Cc, Cu } = require("chrome");
const { MatchPattern } = require("./util/match-pattern");
const { EventTarget } = require("./event/target");
const { emit } = require('./event/core');
@ -27,11 +26,8 @@ const { when } = require('./system/unload');
const { contract: loaderContract } = require('./content/loader');
const { omit } = require('./util/object');
const self = require('./self')
// null-out cycles in .modules to make @loader/options JSONable
const ADDON = omit(require('@loader/options'), ['modules', 'globals']);
require('../framescript/FrameScriptManager.jsm').enableCMEvents();
const { remoteRequire, processes } = require('./remote/parent');
remoteRequire('sdk/content/context-menu');
// All user items we add have this class.
const ITEM_CLASS = "addon-context-menu-item";
@ -67,6 +63,10 @@ const OVERFLOW_POPUP_CLASS = "addon-content-menu-overflow-popup";
// Holds private properties for API objects
let internal = ns();
// A little hacky but this is the last process ID that last opened the context
// menu
let lastContextProcessId = null;
function uuid() {
return require('./util/uuid').uuid().toString();
}
@ -80,9 +80,6 @@ function getScheme(spec) {
}
}
let MessageManager = Cc["@mozilla.org/globalmessagemanager;1"].
getService(Ci.nsIMessageBroadcaster);
let Context = Class({
initialize: function() {
internal(this).id = uuid();
@ -340,21 +337,17 @@ function isItemVisible(item, addonInfo, usePageWorker) {
// Called when an item is clicked to send out click events to the content
// scripts
function itemActivated(item, clickedNode) {
let data = {
items: [internal(item).id],
data: item.data,
}
let items = [internal(item).id];
let data = item.data;
while (item.parentMenu) {
item = item.parentMenu;
data.items.push(internal(item).id);
items.push(internal(item).id);
}
let menuData = clickedNode.ownerDocument.defaultView.gContextMenuContentData;
let messageManager = menuData.browser.messageManager;
messageManager.sendAsyncMessage('sdk/contextmenu/activateitems', data, {
popupNode: menuData.popupNode
});
let process = processes.getById(lastContextProcessId);
if (process)
process.port.emit('sdk/contextmenu/activateitems', items, data);
}
function serializeItem(item) {
@ -416,9 +409,7 @@ let BaseItem = Class({
return;
// Tell all existing frames that this item has been destroyed
MessageManager.broadcastAsyncMessage("sdk/contextmenu/destroyitems", {
items: [internal(this).id]
});
processes.port.emit("sdk/contextmenu/destroyitems", [internal(this).id]);
if (this.parentMenu)
this.parentMenu.removeItem(this);
@ -454,7 +445,7 @@ let BaseItem = Class({
},
});
function workerMessageReceived({ data: { id, args } }) {
function workerMessageReceived(process, id, args) {
if (internal(this).id != id)
return;
@ -471,14 +462,14 @@ let LabelledItem = Class({
EventTarget.prototype.initialize.call(this, options);
internal(this).messageListener = workerMessageReceived.bind(this);
MessageManager.addMessageListener('sdk/worker/event', internal(this).messageListener);
processes.port.on('sdk/worker/event', internal(this).messageListener);
},
destroy: function destroy() {
if (internal(this).destroyed)
return;
MessageManager.removeMessageListener('sdk/worker/event', internal(this).messageListener);
processes.port.off('sdk/worker/event', internal(this).messageListener);
BaseItem.prototype.destroy.call(this);
},
@ -674,27 +665,20 @@ function getContainerItems(container) {
// Notify all frames of these new or changed items
function sendItems(items) {
MessageManager.broadcastAsyncMessage("sdk/contextmenu/createitems", {
items,
addon: ADDON,
});
processes.port.emit("sdk/contextmenu/createitems", items);
}
// Called when a new frame is created and wants to get the current list of items
function remoteItemRequest({ target: { messageManager } }) {
// Called when a new process is created and needs to get the current list of items
function remoteItemRequest(process) {
let items = getContainerItems(contentContextMenu);
if (items.length == 0)
return;
messageManager.sendAsyncMessage("sdk/contextmenu/createitems", {
items,
addon: ADDON,
});
process.port.emit("sdk/contextmenu/createitems", items);
}
MessageManager.addMessageListener('sdk/contextmenu/requestitems', remoteItemRequest);
processes.forEvery(remoteItemRequest);
when(function() {
MessageManager.removeMessageListener('sdk/contextmenu/requestitems', remoteItemRequest);
contentContextMenu.destroy();
});
@ -1011,12 +995,13 @@ let MenuWrapper = Class({
let mainWindow = event.target.ownerDocument.defaultView;
this.contextMenuContentData = mainWindow.gContextMenuContentData
let addonInfo = this.contextMenuContentData.addonInfo[self.id];
if (!addonInfo) {
if (!(self.id in this.contextMenuContentData.addonInfo)) {
console.warn("No context menu state data was provided.");
return;
}
this.setVisibility(this.items, addonInfo, true);
let addonInfo = this.contextMenuContentData.addonInfo[self.id];
lastContextProcessId = addonInfo.processID;
this.setVisibility(this.items, addonInfo.items, true);
}
catch (e) {
console.exception(e);

View File

@ -46,6 +46,10 @@ function on(target, type, listener) {
}
exports.on = on;
let onceWeakMap = new WeakMap();
/**
* Registers an event `listener` that is called only the next time an event
* of the specified `type` is emitted on the given event `target`.
@ -57,10 +61,13 @@ exports.on = on;
* The listener function that processes the event.
*/
function once(target, type, listener) {
on(target, type, function observer(...args) {
let replacement = function observer(...args) {
off(target, type, observer);
onceWeakMap.delete(listener);
listener.apply(target, args);
});
};
onceWeakMap.set(listener, replacement);
on(target, type, replacement);
}
exports.once = once;
@ -124,6 +131,11 @@ exports.emit = emit;
function off(target, type, listener) {
let length = arguments.length;
if (length === 3) {
if (onceWeakMap.has(listener)) {
listener = onceWeakMap.get(listener);
onceWeakMap.delete(listener);
}
let listeners = observers(target, type);
let index = listeners.indexOf(listener);
if (~index)

View File

@ -7,88 +7,13 @@ module.metadata = {
"stability": "unstable"
};
const { Ci } = require("chrome");
const events = require("../system/events");
const core = require("./core");
const { loadSheet, removeSheet } = require("../stylesheet/utils");
const { processes, remoteRequire } = require("../remote/parent");
remoteRequire("sdk/content/l10n-html");
const assetsURI = require('../self').data.url();
const hideSheetUri = "data:text/css,:root {visibility: hidden !important;}";
// Taken from Gaia:
// https://github.com/andreasgal/gaia/blob/04fde2640a7f40314643016a5a6c98bf3755f5fd/webapi.js#L1470
function translateElement(element) {
element = element || document;
// check all translatable children (= w/ a `data-l10n-id' attribute)
var children = element.querySelectorAll('*[data-l10n-id]');
var elementCount = children.length;
for (var i = 0; i < elementCount; i++) {
var child = children[i];
// translate the child
var key = child.dataset.l10nId;
var data = core.get(key);
if (data)
child.textContent = data;
}
}
exports.translateElement = translateElement;
function onDocumentReady2Translate(event) {
let document = event.target;
document.removeEventListener("DOMContentLoaded", onDocumentReady2Translate,
false);
translateElement(document);
try {
// Finally display document when we finished replacing all text content
if (document.defaultView)
removeSheet(document.defaultView, hideSheetUri, 'user');
}
catch(e) {
console.exception(e);
}
}
function onContentWindow(event) {
let document = event.subject;
// Accept only HTML documents
if (!(document instanceof Ci.nsIDOMHTMLDocument))
return;
// Bug 769483: data:URI documents instanciated with nsIDOMParser
// have a null `location` attribute at this time
if (!document.location)
return;
// Accept only document from this addon
if (document.location.href.indexOf(assetsURI) !== 0)
return;
try {
// First hide content of the document in order to have content blinking
// between untranslated and translated states
loadSheet(document.defaultView, hideSheetUri, 'user');
}
catch(e) {
console.exception(e);
}
// Wait for DOM tree to be built before applying localization
document.addEventListener("DOMContentLoaded", onDocumentReady2Translate,
false);
}
// Listen to creation of content documents in order to translate them as soon
// as possible in their loading process
const ON_CONTENT = "document-element-inserted";
let enabled = false;
function enable() {
if (!enabled) {
events.on(ON_CONTENT, onContentWindow);
processes.port.emit("sdk/l10n/html/enable");
enabled = true;
}
}
@ -96,10 +21,12 @@ exports.enable = enable;
function disable() {
if (enabled) {
events.off(ON_CONTENT, onContentWindow);
processes.port.emit("sdk/l10n/html/disable");
enabled = false;
}
}
exports.disable = disable;
require("sdk/system/unload").when(disable);
processes.forEvery(process => {
process.port.emit(enabled ? "sdk/l10n/html/enable" : "sdk/l10n/html/disable");
});

View File

@ -7,49 +7,31 @@ module.metadata = {
"stability": "stable"
};
const observers = require('./system/events');
const { contract: loaderContract } = require('./content/loader');
const { contract } = require('./util/contract');
const { getAttachEventType, WorkerHost } = require('./content/utils');
const { WorkerHost, connect } = require('./content/utils');
const { Class } = require('./core/heritage');
const { Disposable } = require('./core/disposable');
const { WeakReference } = require('./core/reference');
const { Worker } = require('./content/worker');
const { EventTarget } = require('./event/target');
const { on, emit, once, setListeners } = require('./event/core');
const { on: domOn, removeListener: domOff } = require('./dom/events');
const { isRegExp, isUndefined } = require('./lang/type');
const { merge } = require('./util/object');
const { windowIterator } = require('./deprecated/window-utils');
const { isBrowser, getFrames } = require('./window/utils');
const { getTabs, getTabContentWindow, getTabForContentWindow,
getURI: getTabURI } = require('./tabs/utils');
const { ignoreWindow } = require('./private-browsing/utils');
const { Style } = require("./stylesheet/style");
const { attach, detach } = require("./content/mod");
const { has, hasAny } = require("./util/array");
const { merge, omit } = require('./util/object');
const { remove, has, hasAny } = require("./util/array");
const { Rules } = require("./util/rules");
const { List, addListItem, removeListItem } = require('./util/list');
const { when: unload } = require("./system/unload");
const { processes, frames, remoteRequire } = require('./remote/parent');
remoteRequire('sdk/content/page-mod');
// Valid values for `attachTo` option
const VALID_ATTACHTO_OPTIONS = ['existing', 'top', 'frame'];
const pagemods = new Set();
const workers = new WeakMap();
const styles = new WeakMap();
const pagemods = new Map();
const workers = new Map();
const models = new WeakMap();
let modelFor = (mod) => models.get(mod);
let workerFor = (mod) => workers.get(mod);
let styleFor = (mod) => styles.get(mod);
// Bind observer
observers.on('document-element-inserted', onContentWindow);
unload(() => observers.off('document-element-inserted', onContentWindow));
let workerFor = (mod) => workers.get(mod)[0];
// Helper functions
let isRegExpOrString = (v) => isRegExp(v) || typeof v === 'string';
let modMatchesURI = (mod, uri) => mod.include.matchesAny(uri) && !mod.exclude.matchesAny(uri);
let PAGEMOD_ID = 0;
// Validation Contracts
const modOptions = {
@ -119,10 +101,7 @@ const PageMod = Class({
let mod = this;
let model = modContract(options);
models.set(this, model);
// Set listeners on {PageMod} itself, not the underlying worker,
// like `onMessage`, as it'll get piped.
setListeners(this, options);
model.id = PAGEMOD_ID++;
let include = model.include;
model.include = Rules();
@ -132,158 +111,77 @@ const PageMod = Class({
model.exclude = Rules();
model.exclude.add.apply(model.exclude, [].concat(exclude));
if (model.contentStyle || model.contentStyleFile) {
styles.set(mod, Style({
uri: model.contentStyleFile,
source: model.contentStyle
}));
// Set listeners on {PageMod} itself, not the underlying worker,
// like `onMessage`, as it'll get piped.
setListeners(this, options);
pagemods.set(model.id, this);
workers.set(this, []);
function serializeRules(rules) {
for (let rule of rules) {
yield isRegExp(rule) ? { type: "regexp", pattern: rule.source, flags: rule.flags }
: { type: "string", value: rule };
}
}
pagemods.add(this);
model.seenDocuments = new WeakMap();
model.childOptions = omit(model, ["include", "exclude"]);
model.childOptions.include = [...serializeRules(model.include)];
model.childOptions.exclude = [...serializeRules(model.exclude)];
// `applyOnExistingDocuments` has to be called after `pagemods.add()`
// otherwise its calls to `onContent` method won't do anything.
if (has(model.attachTo, 'existing'))
applyOnExistingDocuments(mod);
processes.port.emit('sdk/page-mod/create', model.childOptions);
},
dispose: function() {
let style = styleFor(this);
if (style)
detach(style);
dispose: function(reason) {
processes.port.emit('sdk/page-mod/destroy', modelFor(this).id);
pagemods.delete(modelFor(this).id);
workers.delete(this);
},
for (let i in this.include)
this.include.remove(this.include[i]);
destroy: function(reason) {
// Explicit destroy call, i.e. not via unload so destroy the workers
let list = workers.get(this);
if (!list)
return;
pagemods.delete(this);
// Triggers dispose which will cause the child page-mod to be destroyed
Disposable.prototype.destroy.call(this, reason);
// Destroy any active workers
for (let worker of list)
worker.destroy(reason);
}
});
exports.PageMod = PageMod;
function onContentWindow({ subject: document }) {
// Return if we have no pagemods
if (pagemods.size === 0)
// Whenever a new process starts send over the list of page-mods
processes.forEvery(process => {
for (let mod of pagemods.values())
process.port.emit('sdk/page-mod/create', modelFor(mod).childOptions);
});
frames.port.on('sdk/page-mod/worker-create', (frame, modId, workerOptions) => {
let mod = pagemods.get(modId);
if (!mod)
return;
let window = document.defaultView;
// XML documents don't have windows, and we don't yet support them.
if (!window)
return;
// We apply only on documents in tabs of Firefox
if (!getTabForContentWindow(window))
return;
// Attach the parent side of the worker to the child
let worker = Worker();
// When the tab is private, only addons with 'private-browsing' flag in
// their package.json can apply content script to private documents
if (ignoreWindow(window))
return;
for (let pagemod of pagemods) {
if (modMatchesURI(pagemod, document.URL))
onContent(pagemod, window);
}
}
function applyOnExistingDocuments (mod) {
getTabs().forEach(tab => {
// Fake a newly created document
let window = getTabContentWindow(tab);
// on startup with e10s, contentWindow might not exist yet,
// in which case we will get notified by "document-element-inserted".
if (!window || !window.frames)
return;
let uri = getTabURI(tab);
if (has(mod.attachTo, "top") && modMatchesURI(mod, uri))
onContent(mod, window);
if (has(mod.attachTo, "frame"))
getFrames(window).
filter(iframe => modMatchesURI(mod, iframe.location.href)).
forEach(frame => onContent(mod, frame));
});
}
function createWorker (mod, window) {
let worker = Worker({
window: window,
contentScript: mod.contentScript,
contentScriptFile: mod.contentScriptFile,
contentScriptOptions: mod.contentScriptOptions,
// Bug 980468: Syntax errors from scripts can happen before the worker
// can set up an error handler. They are per-mod rather than per-worker
// so are best handled at the mod level.
onError: (e) => emit(mod, 'error', e)
});
workers.set(mod, worker);
workers.get(mod).unshift(worker);
worker.on('*', (event, ...args) => {
// worker's "attach" event passes a window as the argument
// page-mod's "attach" event needs a worker
// page-mod's "attach" event needs to be passed a worker
if (event === 'attach')
emit(mod, event, worker)
else
emit(mod, event, ...args);
})
once(worker, 'detach', () => worker.destroy());
}
});
function onContent (mod, window) {
// not registered yet
if (!pagemods.has(mod))
return;
worker.on('detach', () => {
let array = workers.get(mod);
if (array)
remove(array, worker);
});
let isTopDocument = window.top === window;
// Is a top level document and `top` is not set, ignore
if (isTopDocument && !has(mod.attachTo, "top"))
return;
// Is a frame document and `frame` is not set, ignore
if (!isTopDocument && !has(mod.attachTo, "frame"))
return;
// ensure we attach only once per document
let seen = modelFor(mod).seenDocuments;
if (seen.has(window.document))
return;
seen.set(window.document, true);
let style = styleFor(mod);
if (style)
attach(style, window);
// Immediatly evaluate content script if the document state is already
// matching contentScriptWhen expectations
if (isMatchingAttachState(mod, window)) {
createWorker(mod, window);
return;
}
let eventName = getAttachEventType(mod) || 'load';
domOn(window, eventName, function onReady (e) {
if (e.target.defaultView !== window)
return;
domOff(window, eventName, onReady, true);
createWorker(mod, window);
// Attaching is asynchronous so if the document is already loaded we will
// miss the pageshow event so send a synthetic one.
if (window.document.readyState == "complete") {
mod.on('attach', worker => {
try {
worker.send('pageshow');
emit(worker, 'pageshow');
}
catch (e) {
// This can fail if an earlier attach listener destroyed the worker
}
});
}
}, true);
}
function isMatchingAttachState (mod, window) {
let state = window.document.readyState;
return 'start' === mod.contentScriptWhen ||
// Is `load` event already dispatched?
'complete' === state ||
// Is DOMContentLoaded already dispatched and waiting for it?
('ready' === mod.contentScriptWhen && state === 'interactive')
}
connect(worker, frame, workerOptions);
});

View File

@ -52,7 +52,7 @@ let pageContract = contract(merge({
is: ['function', 'undefined']
},
include: {
is: ['string', 'array', 'undefined']
is: ['string', 'array', 'regexp', 'undefined']
},
contentScriptWhen: {
is: ['string', 'undefined']

View File

@ -0,0 +1,270 @@
/* 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 { Ci, Cc } = require('chrome');
const runtime = require('../system/runtime');
const { Class } = require('../core/heritage');
const { Namespace } = require('../core/namespace');
const { omit } = require('../util/object');
const { when } = require('../system/unload');
const { EventTarget } = require('../event/target');
const { emit } = require('../event/core');
const { Disposable } = require('../core/disposable');
const { EventParent } = require('./utils');
const { addListItem, removeListItem } = require('../util/list');
const loaderID = require('@loader/options').loaderID;
const MAIN_PROCESS = Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
const mm = Cc['@mozilla.org/childprocessmessagemanager;1'].
getService(Ci.nsISyncMessageSender);
const ns = Namespace();
const process = {
port: new EventTarget(),
get id() {
return runtime.processID;
},
get isRemote() {
return runtime.processType != MAIN_PROCESS;
}
};
exports.process = process;
process.port.emit = (...args) => {
mm.sendAsyncMessage('sdk/remote/process/message', {
loaderID,
args
});
}
function processMessageReceived({ data }) {
// Ignore messages from other loaders
if (data.loaderID != loaderID)
return;
let [event, ...args] = data.args;
emit(process.port, event, process, ...args);
}
mm.addMessageListener('sdk/remote/process/message', processMessageReceived);
when(() => {
mm.removeMessageListener('sdk/remote/process/message', processMessageReceived);
frames = null;
});
process.port.on('sdk/remote/require', (process, uri) => {
require(uri);
});
function listenerEquals(a, b) {
for (let prop of ["type", "callback", "isCapturing"]) {
if (a[prop] != b[prop])
return false;
}
return true;
}
function listenerFor(type, callback, isCapturing = false) {
return {
type,
callback,
isCapturing,
registeredCallback: undefined,
get args() {
return [
this.type,
this.registeredCallback ? this.registeredCallback : this.callback,
this.isCapturing
];
}
};
}
function removeListenerFromArray(array, listener) {
let index = array.findIndex(l => listenerEquals(l, listener));
if (index < 0)
return;
array.splice(index, 1);
}
function getListenerFromArray(array, listener) {
return array.find(l => listenerEquals(l, listener));
}
function arrayContainsListener(array, listener) {
return !!getListenerFromArray(array, listener);
}
function makeFrameEventListener(frame, callback) {
return callback.bind(frame);
}
let FRAME_ID = 0;
let tabMap = new Map();
function frameMessageReceived({ data }) {
if (data.loaderID != loaderID)
return;
let [event, ...args] = data.args;
emit(this.port, event, this, ...args);
}
const Frame = Class({
implements: [ Disposable ],
extends: EventTarget,
setup: function(contentFrame) {
// This ID should be unique for this loader across all processes
ns(this).id = runtime.processID + ":" + FRAME_ID++;
ns(this).contentFrame = contentFrame;
ns(this).messageManager = contentFrame;
ns(this).domListeners = [];
tabMap.set(contentFrame.docShell, this);
ns(this).messageReceived = frameMessageReceived.bind(this);
ns(this).messageManager.addMessageListener('sdk/remote/frame/message', ns(this).messageReceived);
this.port = new EventTarget();
this.port.emit = (...args) => {
ns(this).messageManager.sendAsyncMessage('sdk/remote/frame/message', {
loaderID,
args
});
};
ns(this).messageManager.sendAsyncMessage('sdk/remote/frame/attach', {
loaderID,
frameID: ns(this).id,
processID: runtime.processID
});
frames.attachItem(this);
},
dispose: function() {
emit(this, 'detach', this);
for (let listener of ns(this).domListeners)
ns(this).contentFrame.removeEventListener(...listener.args);
ns(this).messageManager.removeMessageListener('sdk/remote/frame/message', ns(this).messageReceived);
tabMap.delete(ns(this).contentFrame.docShell);
ns(this).contentFrame = null;
},
get content() {
return ns(this).contentFrame.content;
},
get isTab() {
let docShell = ns(this).contentFrame.docShell;
if (process.isRemote) {
// We don't want to roundtrip to the main process to get this property.
// This hack relies on the host app having defined webBrowserChrome only
// in frames that are part of the tabs. Since only Firefox has remote
// processes right now and does this this works.
let tabchild = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsITabChild);
return !!tabchild.webBrowserChrome;
}
else {
// This is running in the main process so we can break out to the browser
// And check we can find a tab for the browser element directly.
let browser = docShell.chromeEventHandler;
let tab = require('../tabs/utils').getTabForBrowser(browser);
return !!tab;
}
},
addEventListener: function(...args) {
let listener = listenerFor(...args);
if (arrayContainsListener(ns(this).domListeners, listener))
return;
listener.registeredCallback = makeFrameEventListener(this, listener.callback);
ns(this).domListeners.push(listener);
ns(this).contentFrame.addEventListener(...listener.args);
},
removeEventListener: function(...args) {
let listener = getListenerFromArray(ns(this).domListeners, listenerFor(...args));
if (!listener)
return;
removeListenerFromArray(ns(this).domListeners, listener);
ns(this).contentFrame.removeEventListener(...listener.args);
}
});
const FrameList = Class({
implements: [ EventParent, Disposable ],
extends: EventTarget,
setup: function() {
EventParent.prototype.initialize.call(this);
this.port = new EventTarget();
ns(this).domListeners = [];
this.on('attach', frame => {
for (let listener of ns(this).domListeners)
frame.addEventListener(...listener.args);
});
},
dispose: function() {
// The only case where we get destroyed is when the loader is unloaded in
// which case each frame will clean up its own event listeners.
ns(this).domListeners = null;
},
getFrameForWindow: function(window) {
for (let frame of this) {
if (frame.content == window)
return frame;
}
return null;
},
addEventListener: function(...args) {
let listener = listenerFor(...args);
if (arrayContainsListener(ns(this).domListeners, listener))
return;
ns(this).domListeners.push(listener);
for (let frame of this)
frame.addEventListener(...listener.args);
},
removeEventListener: function(...args) {
let listener = listenerFor(...args);
if (!arrayContainsListener(ns(this).domListeners, listener))
return;
removeListenerFromArray(ns(this).domListeners, listener);
for (let frame of this)
frame.removeEventListener(...listener.args);
}
});
let frames = exports.frames = new FrameList();
function registerContentFrame(contentFrame) {
let frame = new Frame(contentFrame);
}
exports.registerContentFrame = registerContentFrame;
function unregisterContentFrame(contentFrame) {
let frame = tabMap.get(contentFrame.docShell);
if (!frame)
return;
frame.destroy();
}
exports.unregisterContentFrame = unregisterContentFrame;

View File

@ -0,0 +1,337 @@
/* 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 { Cu, Ci, Cc } = require('chrome');
const runtime = require('../system/runtime');
const MAIN_PROCESS = Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
if (runtime.processType != MAIN_PROCESS) {
throw new Error('Cannot use sdk/remote/parent in a child process.');
}
const { Class } = require('../core/heritage');
const { Namespace } = require('../core/namespace');
const { Disposable } = require('../core/disposable');
const { omit } = require('../util/object');
const { when } = require('../system/unload');
const { EventTarget } = require('../event/target');
const { emit } = require('../event/core');
const system = require('../system/events');
const { EventParent } = require('./utils');
const options = require('@loader/options');
const loaderModule = require('toolkit/loader');
const { getTabForBrowser } = require('../tabs/utils');
// Chose the right function for resolving relative a module id
let moduleResolve;
if (options.isNative) {
moduleResolve = (id, requirer) => loaderModule.nodeResolve(id, requirer, { rootURI: options.rootURI });
}
else {
moduleResolve = loaderModule.resolve;
}
// Build the sorted path mapping structure that resolveURI requires
let pathMapping = Object.keys(options.paths)
.sort((a, b) => b.length - a.length)
.map(p => [p, options.paths[p]]);
// Load the scripts in the child processes
let { getNewLoaderID } = require('../../framescript/FrameScriptManager.jsm');
let PATH = options.paths[''];
const childOptions = omit(options, ['modules', 'globals']);
childOptions.modules = {};
// @l10n/data is just JSON data and can be safely sent across to the child loader
try {
childOptions.modules["@l10n/data"] = require("@l10n/data");
}
catch (e) {
// There may be no l10n data
}
const loaderID = getNewLoaderID();
childOptions.loaderID = loaderID;
const ppmm = Cc['@mozilla.org/parentprocessmessagemanager;1'].
getService(Ci.nsIMessageBroadcaster);
const gmm = Cc['@mozilla.org/globalmessagemanager;1'].
getService(Ci.nsIMessageBroadcaster);
const ns = Namespace();
let processMap = new Map();
function processMessageReceived({ target, data }) {
if (data.loaderID != loaderID)
return;
let [event, ...args] = data.args;
emit(this.port, event, this, ...args);
}
// Process represents a gecko process that can load webpages. Each process
// contains a number of Frames. This class is used to send and receive messages
// from a single process.
const Process = Class({
implements: [ Disposable ],
extends: EventTarget,
setup: function(id, messageManager, isRemote) {
ns(this).id = id;
ns(this).isRemote = isRemote;
ns(this).messageManager = messageManager;
ns(this).messageReceived = processMessageReceived.bind(this);
this.destroy = this.destroy.bind(this);
ns(this).messageManager.addMessageListener('sdk/remote/process/message', ns(this).messageReceived);
ns(this).messageManager.addMessageListener('child-process-shutdown', this.destroy);
this.port = new EventTarget();
this.port.emit = (...args) => {
ns(this).messageManager.sendAsyncMessage('sdk/remote/process/message', {
loaderID,
args
});
};
// Load any remote modules
for (let module of remoteModules.values())
this.port.emit('sdk/remote/require', module);
processMap.set(ns(this).id, this);
processes.attachItem(this);
},
dispose: function() {
emit(this, 'detach', this);
processMap.delete(ns(this).id);
ns(this).messageManager.removeMessageListener('sdk/remote/process/message', ns(this).messageReceived);
ns(this).messageManager.removeMessageListener('child-process-shutdown', this.destroy);
ns(this).messageManager = null;
},
// Returns true if this process is a child process
get isRemote() {
return ns(this).isRemote;
}
});
// Processes gives an API for enumerating an sending and receiving messages from
// all processes as well as detecting when a new process starts.
const Processes = Class({
implements: [ EventParent ],
extends: EventTarget,
initialize: function() {
EventParent.prototype.initialize.call(this);
this.port = new EventTarget();
this.port.emit = (...args) => {
ppmm.broadcastAsyncMessage('sdk/remote/process/message', {
loaderID,
args
});
};
},
getById: function(id) {
return processMap.get(id);
}
});
let processes = exports.processes = new Processes();
let frameMap = new Map();
function frameMessageReceived({ target, data }) {
if (data.loaderID != loaderID)
return;
let [event, ...args] = data.args;
emit(this.port, event, this, ...args);
}
function setFrameProcess(frame, process) {
ns(frame).process = process;
frames.attachItem(frame);
}
// Frames display webpages in a process. In the main process every Frame is
// linked with a <browser> or <iframe> element.
const Frame = Class({
implements: [ Disposable ],
extends: EventTarget,
setup: function(id, node) {
ns(this).id = id;
ns(this).node = node;
let frameLoader = node.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader;
ns(this).messageManager = frameLoader.messageManager;
ns(this).messageReceived = frameMessageReceived.bind(this);
ns(this).messageManager.addMessageListener('sdk/remote/frame/message', ns(this).messageReceived);
this.port = new EventTarget();
this.port.emit = (...args) => {
ns(this).messageManager.sendAsyncMessage('sdk/remote/frame/message', {
loaderID,
args
});
};
frameMap.set(ns(this).messageManager, this);
},
dispose: function() {
emit(this, 'detach', this);
ns(this).messageManager.removeMessageListener('sdk/remote/frame/message', ns(this).messageReceived);
ns(this).messageManager = null;
frameMap.delete(ns(this).messageManager);
},
// Returns the browser or iframe element this frame displays in
get frameElement() {
return ns(this).node;
},
// Returns the process that this frame loads in
get process() {
return ns(this).process;
},
// Returns true if this frame is a tab in a main browser window
get isTab() {
let tab = getTabForBrowser(ns(this).node);
return !!tab;
}
});
function managerDisconnected({ subject: manager }) {
let frame = frameMap.get(manager);
if (frame)
frame.destroy();
}
system.on('message-manager-disconnect', managerDisconnected);
// Provides an API for enumerating and sending and receiving messages from all
// Frames
const FrameList = Class({
implements: [ EventParent ],
extends: EventTarget,
initialize: function() {
EventParent.prototype.initialize.call(this);
this.port = new EventTarget();
this.port.emit = (...args) => {
gmm.broadcastAsyncMessage('sdk/remote/frame/message', {
loaderID,
args
});
};
},
// Returns the frame for a browser element
getFrameForBrowser: function(browser) {
for (let frame of this) {
if (frame.frameElement == browser)
return frame;
}
return null;
}
});
let frames = exports.frames = new FrameList();
// Create the module loader in any existing processes
ppmm.broadcastAsyncMessage('sdk/remote/process/load', {
modulePath: PATH,
loaderID,
options: childOptions,
reason: "broadcast"
});
// A loader has started in a remote process
function processLoaderStarted({ target, data }) {
if (data.loaderID != loaderID)
return;
if (processMap.has(data.processID)) {
console.error("Saw the same process load the same loader twice. This is a bug in the SDK.");
return;
}
let process = new Process(data.processID, target, data.isRemote);
if (pendingFrames.has(data.processID)) {
for (let frame of pendingFrames.get(data.processID))
setFrameProcess(frame, process);
pendingFrames.delete(data.processID);
}
}
// A new process has started
function processStarted({ target, data: { modulePath } }) {
if (modulePath != PATH)
return;
// Have it load a loader if it hasn't already
target.sendAsyncMessage('sdk/remote/process/load', {
modulePath,
loaderID,
options: childOptions,
reason: "response"
});
}
let pendingFrames = new Map();
// A new frame has been created in the remote process
function frameAttached({ target, data }) {
if (data.loaderID != loaderID)
return;
let frame = new Frame(data.frameID, target);
let process = processMap.get(data.processID);
if (process) {
setFrameProcess(frame, process);
return;
}
// In some cases frame messages can arrive earlier than process messages
// causing us to see a new frame appear before its process. In this case
// cache the frame data until we see the process. See bug 1131375.
if (!pendingFrames.has(data.processID))
pendingFrames.set(data.processID, [frame]);
else
pendingFrames.get(data.processID).push(frame);
}
// Wait for new processes and frames
ppmm.addMessageListener('sdk/remote/process/attach', processLoaderStarted);
ppmm.addMessageListener('sdk/remote/process/start', processStarted);
gmm.addMessageListener('sdk/remote/frame/attach', frameAttached);
when(reason => {
ppmm.removeMessageListener('sdk/remote/process/attach', processLoaderStarted);
ppmm.removeMessageListener('sdk/remote/process/start', processStarted);
gmm.removeMessageListener('sdk/remote/frame/attach', frameAttached);
ppmm.broadcastAsyncMessage('sdk/remote/process/unload', { loaderID, reason });
});
let remoteModules = new Set();
// Ensures a module is loaded in every child process. It is safe to send
// messages to this module immediately after calling this.
// Pass a module to resolve the id relatively.
function remoteRequire(id, module = null) {
// Resolve relative to calling module if passed
if (module)
id = moduleResolve(id, module.id);
let uri = loaderModule.resolveURI(id, pathMapping);
// Don't reload the same module
if (remoteModules.has(uri))
return;
remoteModules.add(uri);
processes.port.emit('sdk/remote/require', uri);
}
exports.remoteRequire = remoteRequire;

View File

@ -0,0 +1,39 @@
/* 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 { Class } = require('../core/heritage');
const { List, addListItem, removeListItem } = require('../util/list');
const { emit } = require('../event/core');
const { pipe } = require('../event/utils');
// A helper class that maintains a list of EventTargets. Any events emitted
// to an EventTarget are also emitted by the EventParent. Likewise for an
// EventTarget's port property.
const EventParent = Class({
implements: [ List ],
attachItem: function(item) {
addListItem(this, item);
pipe(item.port, this.port);
pipe(item, this);
item.once('detach', () => {
removeListItem(this, item);
})
emit(this, 'attach', item);
},
// Calls listener for every object already in the list and every object
// subsequently added to the list.
forEvery: function(listener) {
for (let item of this)
listener(item);
this.on('attach', listener);
}
});
exports.EventParent = EventParent;

View File

@ -15,6 +15,7 @@ exports.inSafeMode = runtime.inSafeMode;
exports.OS = runtime.OS;
exports.processType = runtime.processType;
exports.widgetToolkit = runtime.widgetToolkit;
exports.processID = runtime.processID;
// Attempt to access `XPCOMABI` may throw exception, in which case exported
// `XPCOMABI` will be set to `null`.

View File

@ -21,8 +21,8 @@ const { deprecateUsage } = require('../util/deprecate');
const { getURL } = require('../url/utils');
const { viewFor } = require('../view/core');
const { observer } = require('./observer');
require('../../framescript/FrameScriptManager.jsm').enableTabEvents();
const { remoteRequire, frames } = require('../remote/parent');
remoteRequire('sdk/content/tab-events');
// Array of the inner instances of all the wrapped tabs.
const TABS = [];
@ -60,7 +60,7 @@ const TabTrait = Trait.compose(EventEmitter, {
this.on(EVENTS.close.name, this.destroy.bind(this));
this._onContentEvent = this._onContentEvent.bind(this);
this._window.messageManager.addMessageListener('sdk/tab/event', this._onContentEvent);
frames.port.on('sdk/tab/event', this._onContentEvent);
// bug 1024632 - first tab inNewWindow gets events from the synthetic
// about:blank document. ignore them unless that is the actual target url.
@ -84,7 +84,7 @@ const TabTrait = Trait.compose(EventEmitter, {
destroy: function destroy() {
this._removeAllListeners();
if (this._tab) {
this._window.messageManager.removeMessageListener('sdk/tab/event', this._onContentEvent);
frames.port.off('sdk/tab/event', this._onContentEvent);
this._tab = null;
TABS.splice(TABS.indexOf(this), 1);
}
@ -94,8 +94,8 @@ const TabTrait = Trait.compose(EventEmitter, {
* internal message listener emits public events (ready, load and pageshow)
* forwarded from content frame script tab-event.js
*/
_onContentEvent: function({ target, data }) {
if (target !== this._browser)
_onContentEvent: function(frame, event, persisted) {
if (frame.frameElement !== this._browser)
return;
// bug 1024632 - skip initial events from synthetic about:blank document
@ -105,7 +105,7 @@ const TabTrait = Trait.compose(EventEmitter, {
// first time we don't skip blank events, disable further skipping
this._skipBlankEvents = false;
this._emit(data.type, this._public, data.persisted);
this._emit(event, this._public, persisted);
},
/**

View File

@ -294,7 +294,9 @@ function getTabForBrowser(browser) {
return tab;
}
}
return null;
let tabbrowser = browser.getTabBrowser && browser.getTabBrowser()
return !!tabbrowser && tabbrowser.getTabForBrowser(browser);
}
exports.getTabForBrowser = getTabForBrowser;

View File

@ -281,7 +281,7 @@ function getPotentialLeaks() {
memory.gc();
// Things we can assume are part of the platform and so aren't leaks
let WHITELIST_BASE_URLS = [
let GOOD_BASE_URLS = [
"chrome://",
"resource:///",
"resource://app/",
@ -302,7 +302,7 @@ function getPotentialLeaks() {
uri = chromeReg.convertChromeURL(uri);
let spec = uri.spec;
let pos = spec.indexOf("!/");
WHITELIST_BASE_URLS.push(spec.substring(0, pos + 2));
GOOD_BASE_URLS.push(spec.substring(0, pos + 2));
let zoneRegExp = new RegExp("^explicit/js-non-window/zones/zone[^/]+/compartment\\((.+)\\)");
let compartmentRegexp = new RegExp("^explicit/js-non-window/compartments/non-window-global/compartment\\((.+)\\)/");
@ -314,9 +314,10 @@ function getPotentialLeaks() {
if (!item.location)
return false;
for (let whitelist of WHITELIST_BASE_URLS) {
if (item.location.substring(0, whitelist.length) == whitelist)
for (let url of GOOD_BASE_URLS) {
if (item.location.substring(0, url.length) == url) {
return false;
}
}
return true;

View File

@ -78,7 +78,7 @@ function safeMerge(source) {
exports.safeMerge = safeMerge;
/*
* Returns a copy of the object without blacklisted properties
* Returns a copy of the object without omitted properties
*/
function omit(source, ...values) {
let copy = {};

View File

@ -527,8 +527,8 @@ function isNodeModule (name) {
// to allow overlays. Used by `resolveURI`, returns an array
function sortPaths (paths) {
return keys(paths).
sort(function(a, b) { return b.length - a.length }).
map(function(path) { return [ path, paths[path] ] });
sort((a, b) => (b.length - a.length)).
map((path) => [ path, paths[path] ]);
}
const resolveURI = iced(function resolveURI(id, mapping) {
@ -538,7 +538,7 @@ const resolveURI = iced(function resolveURI(id, mapping) {
if (isAbsoluteURI(id)) return normalizeExt(id);
while (index < count) {
let [ path, uri ] = mapping[index ++];
let [ path, uri ] = mapping[index++];
if (id.indexOf(path) === 0)
return normalizeExt(id.replace(path, uri));
}
@ -552,12 +552,13 @@ Loader.resolveURI = resolveURI;
// with it during link time.
const Require = iced(function Require(loader, requirer) {
let {
modules, mapping, resolve: loaderResolve, load, manifest, rootURI, isNative, requireMap
modules, mapping, resolve: loaderResolve, load,
manifest, rootURI, isNative, requireMap
} = loader;
function require(id) {
if (!id) // Throw if `id` is not passed.
throw Error('you must provide a module name when calling require() from '
throw Error('You must provide a module name when calling require() from '
+ requirer.id, requirer.uri);
let { uri, requirement } = getRequirements(id);
@ -592,6 +593,7 @@ const Require = iced(function Require(loader, requirer) {
uri = uri + '.js';
}
}
// If not yet cached, load and cache it.
// We also freeze module to prevent it from further changes
// at runtime.
@ -623,17 +625,36 @@ const Require = iced(function Require(loader, requirer) {
throw Error('you must provide a module name when calling require() from '
+ requirer.id, requirer.uri);
let requirement;
let uri;
let requirement, uri;
// TODO should get native Firefox modules before doing node-style lookups
// to save on loading time
if (isNative) {
// If a requireMap is available from `generateMap`, use that to
// immediately resolve the node-style mapping.
// TODO: write more tests for this use case
if (requireMap && requireMap[requirer.id])
requirement = requireMap[requirer.id][id];
let { overrides } = manifest.jetpack;
for (let key in overrides) {
// ignore any overrides using relative keys
if (/^[\.\/]/.test(key)) {
continue;
}
// If the override is for x -> y,
// then using require("x/lib/z") to get reqire("y/lib/z")
// should also work
if (id == key || (id.substr(0, key.length + 1) == (key + "/"))) {
id = overrides[key] + id.substr(key.length);
id = id.replace(/^[\.\/]+/, "./");
if (id.substr(0, 2) == "./") {
id = "" + id.substr(2);
}
}
}
// For native modules, we want to check if it's a module specified
// in 'modules', like `chrome`, or `@loader` -- if it exists,
// just set the uri to skip resolution
@ -660,7 +681,8 @@ const Require = iced(function Require(loader, requirer) {
if (!requirement) {
requirement = isRelative(id) ? Loader.resolve(id, requirer.id) : id;
}
} else {
}
else {
// Resolve `id` to its requirer if it's relative.
requirement = requirer ? loaderResolve(id, requirer.id) : id;
}
@ -668,9 +690,11 @@ const Require = iced(function Require(loader, requirer) {
// Resolves `uri` of module using loaders resolve function.
uri = uri || resolveURI(requirement, mapping);
if (!uri) // Throw if `uri` can not be resolved.
// Throw if `uri` can not be resolved.
if (!uri) {
throw Error('Module: Can not resolve "' + id + '" module required by ' +
requirer.id + ' located at ' + requirer.uri, requirer.uri);
}
return { uri: uri, requirement: requirement };
}
@ -764,6 +788,19 @@ function Loader(options) {
sharedGlobalBlacklist: ["sdk/indexed-db"]
}, options);
// Create overrides defaults, none at the moment
if (typeof manifest != "object" || !manifest) {
manifest = {};
}
if (typeof manifest.jetpack != "object" || !manifest.jetpack) {
manifest.jetpack = {
overrides: {}
};
}
if (typeof manifest.jetpack.overrides != "object" || !manifest.jetpack.overrides) {
manifest.jetpack.overrides = {};
}
// We create an identity object that will be dispatched on an unload
// event as subject. This way unload listeners will be able to assert
// which loader is unloaded. Please note that we intentionally don't

View File

@ -22,15 +22,15 @@
"main": "./lib/index.js",
"loader": "lib/sdk/loader/cuddlefish.js",
"devDependencies": {
"async": "0.2.10",
"chai": "1.9.2",
"glob": "4.0.6",
"jpm": "0.0.23",
"lodash": "2.4.1",
"mocha": "1.21.5",
"promise": "6.0.1",
"rimraf": "2.2.8",
"unzip": "0.1.9",
"async": "0.9.0",
"chai": "2.1.1",
"glob": "4.4.2",
"jpm": "0.0.29",
"lodash": "3.3.1",
"mocha": "2.1.0",
"promise": "6.1.0",
"rimraf": "2.3.1",
"unzip": "0.1.11",
"xmldom": "0.1.19"
}
}

View File

@ -53,6 +53,7 @@ DEFAULT_NO_CONNECTIONS_PREFS = {
'media.gmp-manager.cert.requireBuiltIn' : False,
'media.gmp-manager.url' : 'http://localhost/media-dummy/gmpmanager',
'media.gmp-manager.url.override': 'http://localhost/dummy-gmp-manager.xml',
'browser.aboutHomeSnippets.updateUrl': 'https://localhost/snippet-dummy',
'browser.newtab.url' : 'about:blank',
'browser.search.update': False,
'browser.safebrowsing.enabled' : False,

View File

@ -87,8 +87,7 @@ def build_xpi(template_root_dir, manifest, xpi_path,
# of all packages sections directories
for packageName in harness_options['packages']:
base_arcpath = ZIPSEP.join(['resources', packageName])
# Eventually strip sdk files. We need to do that in addition to the
# whilelist as the whitelist is only used for `cfx xpi`:
# Eventually strip sdk files.
if not bundle_sdk and packageName == 'addon-sdk':
continue
# Always write the top directory, even if it contains no files, since

View File

@ -70,7 +70,7 @@ function createWorker(assert, xrayWindow, contentScript, done) {
let worker = Worker({
window: xrayWindow,
contentScript: [
'new ' + function () {
'let assert, done; new ' + function () {
assert = function assert(v, msg) {
self.port.emit("assert", {assertion:v, msg:msg});
}

View File

@ -13,7 +13,7 @@ module.metadata = {
const { Cc, Ci } = require("chrome");
const { on } = require("sdk/event/core");
const { setTimeout } = require("sdk/timers");
const { LoaderWithHookedConsole } = require("sdk/test/loader");
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");
@ -742,9 +742,17 @@ exports["test:check worker API with page history"] = WorkerTest(
// that will be disable until the page gets visible again
self.on("pagehide", function () {
setTimeout(function () {
self.postMessage("timeout restored");
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"
});
@ -761,21 +769,10 @@ exports["test:check worker API with page history"] = WorkerTest(
// 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 throw
// Now any event sent to this worker should be cached
setTimeout(_ => {
assert.throws(
function () { worker.postMessage("data"); },
/The page is currently hidden and can no longer be used/,
"postMessage should throw when the page is hidden in history"
);
assert.throws(
function () { worker.port.emit("event"); },
/The page is currently hidden and can no longer be used/,
"port.emit should throw when the page is hidden in history"
);
})
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.
@ -784,10 +781,30 @@ exports["test:check worker API with page history"] = WorkerTest(
// do not receive the message immediatly, so that the timeout is
// actually disabled
setTimeout(function () {
worker.on("message", function (data) {
assert.ok(data, "timeout restored");
done();
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);
@ -978,4 +995,133 @@ exports["test:destroy unbinds listeners from port"] = WorkerTest(
);
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);

View File

@ -0,0 +1,24 @@
<!-- 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/. -->
<html>
<head>
<meta charset="UTF-8">
<title>HTML Localization</title>
</head>
<body>
<div data-l10n-id="Not translated">Kept as-is</div>
<ul data-l10n-id="Translated">
<li>Inner html content is replaced,</li>
<li data-l10n-id="text-content">
Elements with data-l10n-id attribute whose parent element is translated
will be replaced by the content of the translation.
</li>
</ul>
<div data-l10n-id="text-content">No</div>
<div data-l10n-id="Translated">
A data-l10n-id value can be used in multiple elements
</div>
</body>
</html

View File

@ -0,0 +1,5 @@
# 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/.
Translated= jes

View File

@ -0,0 +1,14 @@
# 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/.
Translated= Oui
placeholderString= Placeholder %s
# Plural forms
%d downloads=%d téléchargements
%d downloads[one]=%d téléchargement
downloadsCount=%d téléchargements
downloadsCount[one]=%d téléchargement

View File

@ -0,0 +1,247 @@
/* 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 prefs = require("sdk/preferences/service");
const { Loader } = require('sdk/test/loader');
const { resolveURI } = require('toolkit/loader');
const { rootURI, isNative } = require("@loader/options");
const { usingJSON } = require('sdk/l10n/json/core');
const PREF_MATCH_OS_LOCALE = "intl.locale.matchOS";
const PREF_SELECTED_LOCALE = "general.useragent.locale";
function setLocale(locale) {
prefs.set(PREF_MATCH_OS_LOCALE, false);
prefs.set(PREF_SELECTED_LOCALE, locale);
}
function resetLocale() {
prefs.reset(PREF_MATCH_OS_LOCALE);
prefs.reset(PREF_SELECTED_LOCALE);
}
function definePseudo(loader, id, exports) {
let uri = resolveURI(id, loader.mapping);
loader.modules[uri] = { exports: exports };
}
function createTest(locale, testFunction) {
return function (assert, done) {
let loader = Loader(module);
// Change the locale before loading new l10n modules in order to load
// the right .json file
setLocale(locale);
// Initialize main l10n module in order to load new locale files
loader.require("sdk/l10n/loader").
load(rootURI).
then(null, function failure(error) {
if (!isNative)
assert.fail("Unable to load locales: " + error);
}).
then(function success(data) {
definePseudo(loader, '@l10n/data', data ? data : null);
// Execute the given test function
try {
testFunction(assert, loader, function onDone() {
loader.unload();
resetLocale();
done();
});
}
catch(e) {
console.exception(e);
}
},
function failure(error) {
assert.fail("Unable to load locales: " + error);
});
};
}
exports.testExactMatching = createTest("fr-FR", function(assert, loader, done) {
let _ = loader.require("sdk/l10n").get;
assert.equal(_("Not translated"), "Not translated",
"Key not translated");
assert.equal(_("Translated"), "Oui",
"Simple key translated");
// Placeholders
assert.equal(_("placeholderString", "works"), "Placeholder works",
"Value with placeholder");
assert.equal(_("Placeholder %s", "works"), "Placeholder works",
"Key without value but with placeholder");
assert.equal(_("Placeholders %2s %1s %s.", "working", "are", "correctly"),
"Placeholders are working correctly.",
"Multiple placeholders");
// Plurals
assert.equal(_("downloadsCount", 0),
"0 téléchargement",
"PluralForm form 'one' for 0 in french");
assert.equal(_("downloadsCount", 1),
"1 téléchargement",
"PluralForm form 'one' for 1 in french");
assert.equal(_("downloadsCount", 2),
"2 téléchargements",
"PluralForm form 'other' for n > 1 in french");
done();
});
exports.testHtmlLocalizationPageWorker = createTest("en-GB", function(assert, loader, done) {
// Ensure initing html component that watch document creations
// Note that this module is automatically initialized in
// cuddlefish.js:Loader.main in regular addons. But it isn't for unit tests.
let loaderHtmlL10n = loader.require("sdk/l10n/html");
loaderHtmlL10n.enable();
let uri = require("sdk/self").data.url("test-localization.html");
let worker = loader.require("sdk/page-worker").Page({
contentURL: uri,
contentScript: "new " + function ContentScriptScope() {
let nodes = document.body.querySelectorAll("*[data-l10n-id]");
self.postMessage([nodes[0].innerHTML,
nodes[1].innerHTML,
nodes[2].innerHTML,
nodes[3].innerHTML]);
},
onMessage: function (data) {
assert.equal(
data[0],
"Kept as-is",
"Nodes with unknown id in .properties are kept 'as-is'"
);
assert.equal(data[1], "Yes", "HTML is translated");
assert.equal(
data[2],
"no &lt;b&gt;HTML&lt;/b&gt; injection",
"Content from .properties is text content; HTML can't be injected."
);
assert.equal(data[3], "Yes", "Multiple elements with same data-l10n-id are accepted.");
done();
}
});
});
exports.testHtmlLocalization = createTest("en-GB", function(assert, loader, done) {
// Ensure initing html component that watch document creations
// Note that this module is automatically initialized in
// cuddlefish.js:Loader.main in regular addons. But it isn't for unit tests.
let loaderHtmlL10n = loader.require("sdk/l10n/html");
loaderHtmlL10n.enable();
let uri = require("sdk/self").data.url("test-localization.html");
loader.require("sdk/tabs").open({
url: uri,
onReady: function(tab) {
tab.attach({
contentURL: uri,
contentScript: "new " + function ContentScriptScope() {
let nodes = document.body.querySelectorAll("*[data-l10n-id]");
self.postMessage([nodes[0].innerHTML,
nodes[1].innerHTML,
nodes[2].innerHTML,
nodes[3].innerHTML]);
},
onMessage: function (data) {
assert.equal(
data[0],
"Kept as-is",
"Nodes with unknown id in .properties are kept 'as-is'"
);
assert.equal(data[1], "Yes", "HTML is translated");
assert.equal(
data[2],
"no &lt;b&gt;HTML&lt;/b&gt; injection",
"Content from .properties is text content; HTML can't be injected."
);
assert.equal(data[3], "Yes", "Multiple elements with same data-l10n-id are accepted.");
tab.close(done);
}
});
}
});
});
exports.testEnUsLocaleName = createTest("en-US", function(assert, loader, done) {
let _ = loader.require("sdk/l10n").get;
assert.equal(_("Not translated"), "Not translated",
"String w/o translation is kept as-is");
assert.equal(_("Translated"), "Yes",
"String with translation is correctly translated");
// Check Unicode char escaping sequences
assert.equal(_("unicodeEscape"), " @ ",
"Unicode escaped sequances are correctly converted");
// Check plural forms regular matching
assert.equal(_("downloadsCount", 0),
"0 downloads",
"PluralForm form 'other' for 0 in english");
assert.equal(_("downloadsCount", 1),
"one download",
"PluralForm form 'one' for 1 in english");
assert.equal(_("downloadsCount", 2),
"2 downloads",
"PluralForm form 'other' for n != 1 in english");
// Check optional plural forms
assert.equal(_("pluralTest", 0),
"optional zero form",
"PluralForm form 'zero' can be optionaly specified. (Isn't mandatory in english)");
assert.equal(_("pluralTest", 1),
"fallback to other",
"If the specific plural form is missing, we fallback to 'other'");
// Ensure that we can omit specifying the generic key without [other]
// key[one] = ...
// key[other] = ... # Instead of `key = ...`
assert.equal(_("explicitPlural", 1),
"one",
"PluralForm form can be omitting generic key [i.e. without ...[other] at end of key)");
assert.equal(_("explicitPlural", 10),
"other",
"PluralForm form can be omitting generic key [i.e. without ...[other] at end of key)");
assert.equal(_("first_identifier", "ONE", "TWO"), "the entries are ONE and TWO.", "first_identifier no count");
assert.equal(_("first_identifier", 0, "ONE", "TWO"), "the entries are ONE and TWO.", "first_identifier with count = 0");
assert.equal(_("first_identifier", 1, "ONE", "TWO"), "first entry is ONE and the second one is TWO.", "first_identifier with count = 1");
assert.equal(_("first_identifier", 2, "ONE", "TWO"), "the entries are ONE and TWO.", "first_identifier with count = 2");
assert.equal(_("second_identifier", "ONE", "TWO"), "first entry is ONE and the second one is TWO.", "second_identifier with no count");
assert.equal(_("second_identifier", 0, "ONE", "TWO"), "first entry is ONE and the second one is TWO.", "second_identifier with count = 0");
assert.equal(_("second_identifier", 1, "ONE", "TWO"), "first entry is ONE and the second one is TWO.", "second_identifier with count = 1");
assert.equal(_("second_identifier", 2, "ONE", "TWO"), "first entry is ONE and the second one is TWO.", "second_identifier with count = 2");
assert.equal(_("third_identifier", "ONE", "TWO"), "first entry is ONE and the second one is TWO.", "third_identifier with no count");
assert.equal(_("third_identifier", 0, "ONE", "TWO"), "first entry is ONE and the second one is TWO.", "third_identifier with count = 0");
assert.equal(_("third_identifier", 2, "ONE", "TWO"), "first entry is ONE and the second one is TWO.", "third_identifier with count = 2");
done();
});
exports.testUsingJSON = function(assert) {
assert.equal(usingJSON, !isNative, 'using json');
}
exports.testShortLocaleName = createTest("eo", function(assert, loader, done) {
let _ = loader.require("sdk/l10n").get;
assert.equal(_("Not translated"), "Not translated",
"String w/o translation is kept as-is");
assert.equal(_("Translated"), "jes",
"String with translation is correctly translated");
done();
});
// Before running tests, disable HTML service which is automatially enabled
// in api-utils/addon/runner.js
require('sdk/l10n/html').disable();
require("sdk/test/runner").runTestsFromModule(module);

View File

@ -0,0 +1,5 @@
{
"id": "e10s-l10n@jetpack",
"main": "./main.js",
"version": "0.0.1"
}

View File

@ -0,0 +1,501 @@
/* 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 LOCAL_URI = "about:robots";
const REMOTE_URI = "about:home";
const { Loader } = require('sdk/test/loader');
const { getTabs, openTab, closeTab, setTabURL, getBrowserForTab, getURI } = require('sdk/tabs/utils');
const { getMostRecentBrowserWindow } = require('sdk/window/utils');
const { cleanUI } = require("sdk/test/utils");
const { setTimeout } = require("sdk/timers");
const { promiseEvent, promiseDOMEvent, promiseEventOnItemAndContainer,
waitForProcesses, getChildFrameCount, isE10S } = require("./utils");
const { after } = require('sdk/test/utils');
const { processID } = require('sdk/system/runtime');
const { set } = require('sdk/preferences/service');
// The hidden preload browser messes up our frame counts
set('browser.newtab.preload', false);
// Check that we see a process stop and start
exports["test process restart"] = function*(assert) {
if (!isE10S) {
assert.pass("Skipping test in non-e10s mode");
return;
}
let window = getMostRecentBrowserWindow();
let tabs = getTabs(window);
assert.equal(tabs.length, 1, "Should have just the one tab to start with");
let tab = tabs[0];
let loader = new Loader(module);
let { processes, frames } = yield waitForProcesses(loader);
let remoteProcess = Array.filter(processes, p => p.isRemote)[0];
let localProcess = Array.filter(processes, p => !p.isRemote)[0];
let remoteFrame = Array.filter(frames, f => f.process == remoteProcess)[0];
// Switch the remote tab to a local URI which should kill the remote process
let frameDetach = promiseEventOnItemAndContainer(assert, remoteFrame, frames, 'detach');
let frameAttach = promiseEvent(frames, 'attach');
let processDetach = promiseEventOnItemAndContainer(assert, remoteProcess, processes, 'detach');
setTabURL(tab, LOCAL_URI);
// The load should kill the remote frame
yield frameDetach;
// And create a new frame in the local process
let [newFrame] = yield frameAttach;
assert.equal(newFrame.process, localProcess, "New frame should be in the local process");
// And kill the process
yield processDetach;
frameDetach = promiseEventOnItemAndContainer(assert, newFrame, frames, 'detach');
let processAttach = promiseEvent(processes, 'attach');
frameAttach = promiseEvent(frames, 'attach');
setTabURL(tab, REMOTE_URI);
// The load should kill the remote frame
yield frameDetach;
// And create a new remote process
[remoteProcess] = yield processAttach;
assert.ok(remoteProcess.isRemote, "Process should be remote");
// And create a new frame in the remote process
[newFrame] = yield frameAttach;
assert.equal(newFrame.process, remoteProcess, "New frame should be in the remote process");
setTabURL(tab, "about:blank");
loader.unload();
};
// Test that we find the right number of processes and that messaging between
// them works and none of the streams cross
exports["test process list"] = function*(assert) {
let loader = new Loader(module);
let { processes } = loader.require('sdk/remote/parent');
let processCount = 0;
processes.forEvery(processes => processCount++);
yield waitForProcesses(loader);
let remoteProcesses = Array.filter(processes, process => process.isRemote);
let localProcesses = Array.filter(processes, process => !process.isRemote);
assert.equal(localProcesses.length, 1, "Should always be one process");
if (isE10S) {
assert.equal(remoteProcesses.length, 1, "Should be one remote process");
}
else {
assert.equal(remoteProcesses.length, 0, "Should be no remote processes");
}
assert.equal(processCount, processes.length, "Should have seen all processes");
processCount = 0;
processes.forEvery(process => processCount++);
assert.equal(processCount, processes.length, "forEvery should send existing processes to the listener");
localProcesses[0].port.on('sdk/test/pong', (process, key) => {
assert.equal(key, "local", "Should not have seen a pong from the local process with the wrong key");
});
if (isE10S) {
remoteProcesses[0].port.on('sdk/test/pong', (process, key) => {
assert.equal(key, "remote", "Should not have seen a pong from the remote process with the wrong key");
});
}
let promise = promiseEventOnItemAndContainer(assert, localProcesses[0].port, processes.port, 'sdk/test/pong', localProcesses[0]);
localProcesses[0].port.emit('sdk/test/ping', "local");
let reply = yield promise;
assert.equal(reply[0], "local", "Saw the process reply with the right key");
if (isE10S) {
promise = promiseEventOnItemAndContainer(assert, remoteProcesses[0].port, processes.port, 'sdk/test/pong', remoteProcesses[0]);
remoteProcesses[0].port.emit('sdk/test/ping', "remote");
reply = yield promise;
assert.equal(reply[0], "remote", "Saw the process reply with the right key");
assert.notEqual(localProcesses[0], remoteProcesses[0], "Processes should be different");
}
loader.unload();
};
// Test that the frame lists are kept up to date
exports["test frame list"] = function*(assert) {
function browserFrames(list) {
return Array.filter(list, b => b.isTab).length;
}
let window = getMostRecentBrowserWindow();
let tabs = getTabs(window);
assert.equal(tabs.length, 1, "Should have just the one tab to start with");
let loader = new Loader(module);
let { processes, frames } = yield waitForProcesses(loader);
assert.equal(browserFrames(frames), getTabs(window).length, "Should be the right number of browser frames.");
assert.equal((yield getChildFrameCount(processes)), frames.length, "Child processes should have the right number of frames");
let promise = promiseEvent(frames, 'attach');
let tab1 = openTab(window, LOCAL_URI);
let [frame1] = yield promise;
assert.ok(!!frame1, "Should have seen the new frame");
assert.ok(!frame1.process.isRemote, "Frame should not be remote");
assert.equal(browserFrames(frames), getTabs(window).length, "Should be the right number of browser frames.");
assert.equal((yield getChildFrameCount(processes)), frames.length, "Child processes should have the right number of frames");
promise = promiseEvent(frames, 'attach');
let tab2 = openTab(window, REMOTE_URI);
let [frame2] = yield promise;
assert.ok(!!frame2, "Should have seen the new frame");
if (isE10S)
assert.ok(frame2.process.isRemote, "Frame should be remote");
else
assert.ok(!frame2.process.isRemote, "Frame should not be remote");
assert.equal(browserFrames(frames), getTabs(window).length, "Should be the right number of browser frames.");
assert.equal((yield getChildFrameCount(processes)), frames.length, "Child processes should have the right number of frames");
frames.port.emit('sdk/test/ping')
yield new Promise(resolve => {
let count = 0;
let listener = () => {
console.log("Saw pong");
count++;
if (count == frames.length) {
frames.port.off('sdk/test/pong', listener);
resolve();
}
};
frames.port.on('sdk/test/pong', listener);
});
let badListener = () => {
assert.fail("Should not have seen a response through this frame");
}
frame1.port.on('sdk/test/pong', badListener);
frame2.port.emit('sdk/test/ping', 'b');
let [key] = yield promiseEventOnItemAndContainer(assert, frame2.port, frames.port, 'sdk/test/pong', frame2);
assert.equal(key, 'b', "Should have seen the right response");
frame1.port.off('sdk/test/pong', badListener);
frame2.port.on('sdk/test/pong', badListener);
frame1.port.emit('sdk/test/ping', 'b');
[key] = yield promiseEventOnItemAndContainer(assert, frame1.port, frames.port, 'sdk/test/pong', frame1);
assert.equal(key, 'b', "Should have seen the right response");
frame2.port.off('sdk/test/pong', badListener);
promise = promiseEventOnItemAndContainer(assert, frame1, frames, 'detach');
closeTab(tab1);
yield promise;
assert.equal(browserFrames(frames), getTabs(window).length, "Should be the right number of browser frames.");
assert.equal((yield getChildFrameCount(processes)), frames.length, "Child processes should have the right number of frames");
promise = promiseEventOnItemAndContainer(assert, frame2, frames, 'detach');
closeTab(tab2);
yield promise;
assert.equal(browserFrames(frames), getTabs(window).length, "Should be the right number of browser frames.");
assert.equal((yield getChildFrameCount(processes)), frames.length, "Child processes should have the right number of frames");
loader.unload();
};
// Test that multiple loaders get their own loaders in the child and messages
// don't cross. Unload should work
exports["test new loader"] = function*(assert) {
let loader1 = new Loader(module);
let { processes: processes1 } = yield waitForProcesses(loader1);
let loader2 = new Loader(module);
let { processes: processes2 } = yield waitForProcesses(loader2);
let process1 = [...processes1][0];
let process2 = [...processes2][0];
process1.port.on('sdk/test/pong', (process, key) => {
assert.equal(key, "a", "Should have seen the right pong");
});
process2.port.on('sdk/test/pong', (process, key) => {
assert.equal(key, "b", "Should have seen the right pong");
});
process1.port.emit('sdk/test/ping', 'a');
yield promiseEvent(process1.port, 'sdk/test/pong');
process2.port.emit('sdk/test/ping', 'b');
yield promiseEvent(process2.port, 'sdk/test/pong');
loader1.unload();
process2.port.emit('sdk/test/ping', 'b');
yield promiseEvent(process2.port, 'sdk/test/pong');
loader2.unload();
};
// Test that unloading the loader unloads the child instances
exports["test unload"] = function*(assert) {
let window = getMostRecentBrowserWindow();
let loader = new Loader(module);
let { frames } = yield waitForProcesses(loader);
let promise = promiseEvent(frames, 'attach');
let tab = openTab(window, "data:,<html/>");
let browser = getBrowserForTab(tab);
yield promiseDOMEvent(browser, "load", true);
let [frame] = yield promise;
assert.ok(!!frame, "Should have seen the new frame");
promise = promiseDOMEvent(browser, 'hashchange');
frame.port.emit('sdk/test/testunload');
loader.unload("shutdown");
yield promise;
let hash = getURI(tab).replace(/.*#/, "");
assert.equal(hash, "unloaded:shutdown", "Saw the correct hash change.")
closeTab(tab);
}
// Test that unloading the loader causes the child to see frame detach events
exports["test frame detach on unload"] = function*(assert) {
let window = getMostRecentBrowserWindow();
let loader = new Loader(module);
let { frames } = yield waitForProcesses(loader);
let promise = promiseEvent(frames, 'attach');
let tab = openTab(window, "data:,<html/>");
let browser = getBrowserForTab(tab);
yield promiseDOMEvent(browser, "load", true);
let [frame] = yield promise;
assert.ok(!!frame, "Should have seen the new frame");
promise = promiseDOMEvent(browser, 'hashchange');
frame.port.emit('sdk/test/testdetachonunload');
loader.unload("shutdown");
yield promise;
let hash = getURI(tab).replace(/.*#/, "");
assert.equal(hash, "unloaded", "Saw the correct hash change.")
closeTab(tab);
}
// Test that DOM event listener on the frame object works
exports["test frame event listeners"] = function*(assert) {
let window = getMostRecentBrowserWindow();
let loader = new Loader(module);
let { frames } = yield waitForProcesses(loader);
let promise = promiseEvent(frames, 'attach');
let tab = openTab(window, "data:text/html,<html></html>");
let browser = getBrowserForTab(tab);
yield promiseDOMEvent(browser, "load", true);
let [frame] = yield promise;
assert.ok(!!frame, "Should have seen the new frame");
frame.port.emit('sdk/test/registerframeevent');
promise = Promise.all([
promiseEvent(frame.port, 'sdk/test/sawreply'),
promiseEvent(frame.port, 'sdk/test/eventsent')
]);
frame.port.emit('sdk/test/sendevent');
yield promise;
frame.port.emit('sdk/test/unregisterframeevent');
promise = promiseEvent(frame.port, 'sdk/test/eventsent');
frame.port.on('sdk/test/sawreply', () => {
assert.fail("Should not have seen the event listener reply");
});
frame.port.emit('sdk/test/sendevent');
yield promise;
closeTab(tab);
loader.unload();
}
// Test that DOM event listener on the frames object works
exports["test frames event listeners"] = function*(assert) {
let window = getMostRecentBrowserWindow();
let loader = new Loader(module);
let { frames } = yield waitForProcesses(loader);
let promise = promiseEvent(frames, 'attach');
let tab = openTab(window, "data:text/html,<html></html>");
let browser = getBrowserForTab(tab);
yield promiseDOMEvent(browser, "load", true);
let [frame] = yield promise;
assert.ok(!!frame, "Should have seen the new frame");
frame.port.emit('sdk/test/registerframesevent');
promise = Promise.all([
promiseEvent(frame.port, 'sdk/test/sawreply'),
promiseEvent(frame.port, 'sdk/test/eventsent')
]);
frame.port.emit('sdk/test/sendevent');
yield promise;
frame.port.emit('sdk/test/unregisterframesevent');
promise = promiseEvent(frame.port, 'sdk/test/eventsent');
frame.port.on('sdk/test/sawreply', () => {
assert.fail("Should not have seen the event listener reply");
});
frame.port.emit('sdk/test/sendevent');
yield promise;
closeTab(tab);
loader.unload();
}
// Test that unloading unregisters frame DOM events
exports["test unload removes frame event listeners"] = function*(assert) {
let window = getMostRecentBrowserWindow();
let loader = new Loader(module);
let { frames } = yield waitForProcesses(loader);
let loader2 = new Loader(module);
let { frames: frames2 } = yield waitForProcesses(loader2);
let promise = promiseEvent(frames, 'attach');
let promise2 = promiseEvent(frames2, 'attach');
let tab = openTab(window, "data:text/html,<html></html>");
let browser = getBrowserForTab(tab);
yield promiseDOMEvent(browser, "load", true);
let [frame] = yield promise;
let [frame2] = yield promise2;
assert.ok(!!frame && !!frame2, "Should have seen the new frame");
frame.port.emit('sdk/test/registerframeevent');
promise = Promise.all([
promiseEvent(frame2.port, 'sdk/test/sawreply'),
promiseEvent(frame2.port, 'sdk/test/eventsent')
]);
frame2.port.emit('sdk/test/sendevent');
yield promise;
loader.unload();
promise = promiseEvent(frame2.port, 'sdk/test/eventsent');
frame2.port.on('sdk/test/sawreply', () => {
assert.fail("Should not have seen the event listener reply");
});
frame2.port.emit('sdk/test/sendevent');
yield promise;
closeTab(tab);
loader2.unload();
}
// Test that unloading unregisters frames DOM events
exports["test unload removes frames event listeners"] = function*(assert) {
let window = getMostRecentBrowserWindow();
let loader = new Loader(module);
let { frames } = yield waitForProcesses(loader);
let loader2 = new Loader(module);
let { frames: frames2 } = yield waitForProcesses(loader2);
let promise = promiseEvent(frames, 'attach');
let promise2 = promiseEvent(frames2, 'attach');
let tab = openTab(window, "data:text/html,<html></html>");
let browser = getBrowserForTab(tab);
yield promiseDOMEvent(browser, "load", true);
let [frame] = yield promise;
let [frame2] = yield promise2;
assert.ok(!!frame && !!frame2, "Should have seen the new frame");
frame.port.emit('sdk/test/registerframesevent');
promise = Promise.all([
promiseEvent(frame2.port, 'sdk/test/sawreply'),
promiseEvent(frame2.port, 'sdk/test/eventsent')
]);
frame2.port.emit('sdk/test/sendevent');
yield promise;
loader.unload();
promise = promiseEvent(frame2.port, 'sdk/test/eventsent');
frame2.port.on('sdk/test/sawreply', () => {
assert.fail("Should not have seen the event listener reply");
});
frame2.port.emit('sdk/test/sendevent');
yield promise;
closeTab(tab);
loader2.unload();
}
// Check that the child frame has the right properties
exports["test frame properties"] = function*(assert) {
let loader = new Loader(module);
let { frames } = yield waitForProcesses(loader);
let promise = new Promise(resolve => {
let count = frames.length;
let listener = (frame, properties) => {
assert.equal(properties.isTab, frame.isTab,
"Child frame should have the same isTab property");
if (--count == 0) {
frames.port.off('sdk/test/replyproperties', listener);
resolve();
}
}
frames.port.on('sdk/test/replyproperties', listener);
})
frames.port.emit('sdk/test/checkproperties');
yield promise;
loader.unload();
}
// Check that non-remote processes have the same process ID and remote processes
// have different IDs
exports["test processID"] = function*(assert) {
let loader = new Loader(module);
let { processes } = yield waitForProcesses(loader);
for (let process of processes) {
process.port.emit('sdk/test/getprocessid');
let [p, ID] = yield promiseEvent(process.port, 'sdk/test/processid');
if (process.isRemote) {
assert.notEqual(ID, processID, "Remote processes should have a different process ID");
}
else {
assert.equal(ID, processID, "Remote processes should have the same process ID");
}
}
}
after(exports, function*(name, assert) {
yield cleanUI();
});
require('sdk/test/runner').runTestsFromModule(module);

View File

@ -0,0 +1,9 @@
{
"name": "e10s-remote",
"title": "e10s-remote",
"id": "remote@jetpack",
"description": "Run remote tests",
"version": "1.0.0",
"main": "main.js",
"e10s": true
}

View File

@ -0,0 +1,105 @@
/* 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/. */
const { when } = require('sdk/system/unload');
const { process, frames } = require('sdk/remote/child');
const { loaderID } = require('@loader/options');
const { processID } = require('sdk/system/runtime');
const system = require('sdk/system/events');
const { Cu } = require('chrome');
function log(str) {
console.log("remote[" + loaderID + "][" + processID + "]: " + str);
}
log("module loaded");
process.port.emit('sdk/test/load');
process.port.on('sdk/test/ping', (process, key) => {
log("received process ping");
process.port.emit('sdk/test/pong', key);
});
let frameCount = 0;
frames.forEvery(frame => {
frameCount++;
frame.on('detach', () => {
frameCount--;
});
frame.port.on('sdk/test/ping', (frame, key) => {
log("received frame ping");
frame.port.emit('sdk/test/pong', key);
});
});
frames.port.on('sdk/test/checkproperties', frame => {
frame.port.emit('sdk/test/replyproperties', {
isTab: frame.isTab
});
});
process.port.on('sdk/test/count', () => {
log("received count ping");
process.port.emit('sdk/test/count', frameCount);
});
process.port.on('sdk/test/getprocessid', () => {
process.port.emit('sdk/test/processid', processID);
});
frames.port.on('sdk/test/testunload', (frame) => {
// Cache the content since the frame will have been destroyed by the time
// we see the unload event.
let content = frame.content;
when((reason) => {
content.location = "#unloaded:" + reason;
});
});
frames.port.on('sdk/test/testdetachonunload', (frame) => {
let content = frame.content;
frame.on('detach', () => {
console.log("Detach from " + frame.content.location);
frame.content.location = "#unloaded";
});
});
frames.port.on('sdk/test/sendevent', (frame) => {
let doc = frame.content.document;
let listener = () => {
frame.port.emit('sdk/test/sawreply');
}
system.on("Test:Reply", listener);
let event = new frame.content.CustomEvent("Test:Event");
doc.dispatchEvent(event);
system.off("Test:Reply", listener);
frame.port.emit('sdk/test/eventsent');
});
function listener(event) {
// Use the raw observer service here since it will be usable even if the
// loader has unloaded
let { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
Services.obs.notifyObservers(null, "Test:Reply", "");
}
frames.port.on('sdk/test/registerframesevent', (frame) => {
frames.addEventListener("Test:Event", listener, true);
});
frames.port.on('sdk/test/unregisterframesevent', (frame) => {
frames.removeEventListener("Test:Event", listener, true);
});
frames.port.on('sdk/test/registerframeevent', (frame) => {
frame.addEventListener("Test:Event", listener, true);
});
frames.port.on('sdk/test/unregisterframeevent', (frame) => {
frame.removeEventListener("Test:Event", listener, true);
});

View File

@ -0,0 +1,110 @@
/* 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 { Cu } = require('chrome');
const { Task: { async } } = Cu.import('resource://gre/modules/Task.jsm', {});
const { getMostRecentBrowserWindow } = require('sdk/window/utils');
const REMOTE_MODULE = "./remote-module";
function promiseEvent(emitter, event) {
console.log("Waiting for " + event);
return new Promise(resolve => {
emitter.once(event, (...args) => {
console.log("Saw " + event);
resolve(args);
});
});
}
exports.promiseEvent = promiseEvent;
function promiseDOMEvent(target, event, isCapturing = false) {
console.log("Waiting for " + event);
return new Promise(resolve => {
let listener = (event) => {
target.removeEventListener(event, listener, isCapturing);
resolve(event);
};
target.addEventListener(event, listener, isCapturing);
})
}
exports.promiseDOMEvent = promiseDOMEvent;
const promiseEventOnItemAndContainer = async(function*(assert, itemport, container, event, item = itemport) {
let itemEvent = promiseEvent(itemport, event);
let containerEvent = promiseEvent(container, event);
let itemArgs = yield itemEvent;
let containerArgs = yield containerEvent;
assert.equal(containerArgs[0], item, "Should have seen a container event for the right item");
assert.equal(JSON.stringify(itemArgs), JSON.stringify(containerArgs), "Arguments should have matched");
// Strip off the item from the returned arguments
return itemArgs.slice(1);
});
exports.promiseEventOnItemAndContainer = promiseEventOnItemAndContainer;
const waitForProcesses = async(function*(loader) {
console.log("Starting remote");
let { processes, frames, remoteRequire } = loader.require('sdk/remote/parent');
remoteRequire(REMOTE_MODULE, module);
let events = [];
// In e10s we should expect to see two processes
let expectedCount = isE10S ? 2 : 1;
yield new Promise(resolve => {
let count = 0;
// Wait for a process to be detected
let listener = process => {
console.log("Saw a process attach");
// Wait for the remote module to load in this process
process.port.once('sdk/test/load', () => {
console.log("Saw a remote module load");
count++;
if (count == expectedCount) {
processes.off('attach', listener);
resolve();
}
});
}
processes.on('attach', listener);
});
console.log("Remote ready");
return { processes, frames, remoteRequire };
});
exports.waitForProcesses = waitForProcesses;
// Counts the frames in all the child processes
const getChildFrameCount = async(function*(processes) {
let frameCount = 0;
for (let process of processes) {
process.port.emit('sdk/test/count');
let [p, count] = yield promiseEvent(process.port, 'sdk/test/count');
frameCount += count;
}
return frameCount;
});
exports.getChildFrameCount = getChildFrameCount;
const mainWindow = getMostRecentBrowserWindow();
const isE10S = mainWindow.gMultiProcessBrowser;
exports.isE10S = isE10S;
if (isE10S) {
console.log("Testing in E10S mode");
// We expect a child process to already be present, make sure that is the case
mainWindow.XULBrowserWindow.forceInitialBrowserRemote();
}
else {
console.log("Testing in non-E10S mode");
}

View File

@ -12,6 +12,8 @@ skip-if = true
skip-if = true
[e10s-tabs.xpi]
skip-if = true
[e10s-remote.xpi]
skip-if = true
[l10n.xpi]
[l10n-properties.xpi]
[layout-change.xpi]
@ -27,6 +29,7 @@ skip-if = true
[preferences-branch.xpi]
[private-browsing-supported.xpi]
skip-if = true
[remote.xpi]
[require.xpi]
[self.xpi]
[simple-prefs.xpi]

View File

@ -0,0 +1,28 @@
# 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/.
Translated= Yes
text-content=no <b>HTML</b> injection
downloadsCount=%d downloads
downloadsCount[one]=one download
pluralTest=fallback to other
pluralTest[zero]=optional zero form
explicitPlural[one]=one
explicitPlural[other]=other
# You can use unicode char escaping in order to inject space at the beginning/
# end of your string. (Regular spaces are automatically ignore by .properties
# file parser)
unicodeEscape = \u0020\u0040\u0020
# this string equals to " @ "
# bug 1033309 plurals with multiple placeholders
first_identifier[one]=first entry is %s and the second one is %s.
first_identifier=the entries are %s and %s.
second_identifier[other]=first entry is %s and the second one is %s.
third_identifier=first entry is %s and the second one is %s.

View File

@ -6,7 +6,7 @@
const prefs = require("sdk/preferences/service");
const { Loader } = require('sdk/test/loader');
const { resolveURI } = require('toolkit/loader');
const { rootURI } = require("@loader/options");
const { rootURI, isNative } = require("@loader/options");
const { usingJSON } = require('sdk/l10n/json/core');
const PREF_MATCH_OS_LOCALE = "intl.locale.matchOS";
@ -36,8 +36,12 @@ function createTest(locale, testFunction) {
// Initialize main l10n module in order to load new locale files
loader.require("sdk/l10n/loader").
load(rootURI).
then(null, function failure(error) {
if (!isNative)
assert.fail("Unable to load locales: " + error);
}).
then(function success(data) {
definePseudo(loader, '@l10n/data', data);
definePseudo(loader, '@l10n/data', data ? data : null);
// Execute the given test function
try {
testFunction(assert, loader, function onDone() {
@ -86,7 +90,7 @@ exports.testExactMatching = createTest("fr-FR", function(assert, loader, done) {
done();
});
exports.testHtmlLocalization = createTest("en-GB", function(assert, loader, done) {
exports.testHtmlLocalizationPageWorker = createTest("en-GB", function(assert, loader, done) {
// Ensure initing html component that watch document creations
// Note that this module is automatically initialized in
// cuddlefish.js:Loader.main in regular addons. But it isn't for unit tests.
@ -120,7 +124,47 @@ exports.testHtmlLocalization = createTest("en-GB", function(assert, loader, done
done();
}
});
});
exports.testHtmlLocalization = createTest("en-GB", function(assert, loader, done) {
// Ensure initing html component that watch document creations
// Note that this module is automatically initialized in
// cuddlefish.js:Loader.main in regular addons. But it isn't for unit tests.
let loaderHtmlL10n = loader.require("sdk/l10n/html");
loaderHtmlL10n.enable();
let uri = require("sdk/self").data.url("test-localization.html");
loader.require("sdk/tabs").open({
url: uri,
onReady: function(tab) {
tab.attach({
contentURL: uri,
contentScript: "new " + function ContentScriptScope() {
let nodes = document.body.querySelectorAll("*[data-l10n-id]");
self.postMessage([nodes[0].innerHTML,
nodes[1].innerHTML,
nodes[2].innerHTML,
nodes[3].innerHTML]);
},
onMessage: function (data) {
assert.equal(
data[0],
"Kept as-is",
"Nodes with unknown id in .properties are kept 'as-is'"
);
assert.equal(data[1], "Yes", "HTML is translated");
assert.equal(
data[2],
"no &lt;b&gt;HTML&lt;/b&gt; injection",
"Content from .properties is text content; HTML can't be injected."
);
assert.equal(data[3], "Yes", "Multiple elements with same data-l10n-id are accepted.");
tab.close(done);
}
});
}
});
});
exports.testEnUsLocaleName = createTest("en-US", function(assert, loader, done) {
@ -182,7 +226,7 @@ exports.testEnUsLocaleName = createTest("en-US", function(assert, loader, done)
});
exports.testUsingJSON = function(assert) {
assert.equal(usingJSON, true, 'using json');
assert.equal(usingJSON, !isNative, 'using json');
}
exports.testShortLocaleName = createTest("eo", function(assert, loader, done) {

View File

@ -0,0 +1,501 @@
/* 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 LOCAL_URI = "about:robots";
const REMOTE_URI = "about:home";
const { Loader } = require('sdk/test/loader');
const { getTabs, openTab, closeTab, setTabURL, getBrowserForTab, getURI } = require('sdk/tabs/utils');
const { getMostRecentBrowserWindow } = require('sdk/window/utils');
const { cleanUI } = require("sdk/test/utils");
const { setTimeout } = require("sdk/timers");
const { promiseEvent, promiseDOMEvent, promiseEventOnItemAndContainer,
waitForProcesses, getChildFrameCount, isE10S } = require("./utils");
const { after } = require('sdk/test/utils');
const { processID } = require('sdk/system/runtime');
const { set } = require('sdk/preferences/service');
// The hidden preload browser messes up our frame counts
set('browser.newtab.preload', false);
// Check that we see a process stop and start
exports["test process restart"] = function*(assert) {
if (!isE10S) {
assert.pass("Skipping test in non-e10s mode");
return;
}
let window = getMostRecentBrowserWindow();
let tabs = getTabs(window);
assert.equal(tabs.length, 1, "Should have just the one tab to start with");
let tab = tabs[0];
let loader = new Loader(module);
let { processes, frames } = yield waitForProcesses(loader);
let remoteProcess = Array.filter(processes, p => p.isRemote)[0];
let localProcess = Array.filter(processes, p => !p.isRemote)[0];
let remoteFrame = Array.filter(frames, f => f.process == remoteProcess)[0];
// Switch the remote tab to a local URI which should kill the remote process
let frameDetach = promiseEventOnItemAndContainer(assert, remoteFrame, frames, 'detach');
let frameAttach = promiseEvent(frames, 'attach');
let processDetach = promiseEventOnItemAndContainer(assert, remoteProcess, processes, 'detach');
setTabURL(tab, LOCAL_URI);
// The load should kill the remote frame
yield frameDetach;
// And create a new frame in the local process
let [newFrame] = yield frameAttach;
assert.equal(newFrame.process, localProcess, "New frame should be in the local process");
// And kill the process
yield processDetach;
frameDetach = promiseEventOnItemAndContainer(assert, newFrame, frames, 'detach');
let processAttach = promiseEvent(processes, 'attach');
frameAttach = promiseEvent(frames, 'attach');
setTabURL(tab, REMOTE_URI);
// The load should kill the remote frame
yield frameDetach;
// And create a new remote process
[remoteProcess] = yield processAttach;
assert.ok(remoteProcess.isRemote, "Process should be remote");
// And create a new frame in the remote process
[newFrame] = yield frameAttach;
assert.equal(newFrame.process, remoteProcess, "New frame should be in the remote process");
setTabURL(tab, "about:blank");
loader.unload();
};
// Test that we find the right number of processes and that messaging between
// them works and none of the streams cross
exports["test process list"] = function*(assert) {
let loader = new Loader(module);
let { processes } = loader.require('sdk/remote/parent');
let processCount = 0;
processes.forEvery(processes => processCount++);
yield waitForProcesses(loader);
let remoteProcesses = Array.filter(processes, process => process.isRemote);
let localProcesses = Array.filter(processes, process => !process.isRemote);
assert.equal(localProcesses.length, 1, "Should always be one process");
if (isE10S) {
assert.equal(remoteProcesses.length, 1, "Should be one remote process");
}
else {
assert.equal(remoteProcesses.length, 0, "Should be no remote processes");
}
assert.equal(processCount, processes.length, "Should have seen all processes");
processCount = 0;
processes.forEvery(process => processCount++);
assert.equal(processCount, processes.length, "forEvery should send existing processes to the listener");
localProcesses[0].port.on('sdk/test/pong', (process, key) => {
assert.equal(key, "local", "Should not have seen a pong from the local process with the wrong key");
});
if (isE10S) {
remoteProcesses[0].port.on('sdk/test/pong', (process, key) => {
assert.equal(key, "remote", "Should not have seen a pong from the remote process with the wrong key");
});
}
let promise = promiseEventOnItemAndContainer(assert, localProcesses[0].port, processes.port, 'sdk/test/pong', localProcesses[0]);
localProcesses[0].port.emit('sdk/test/ping', "local");
let reply = yield promise;
assert.equal(reply[0], "local", "Saw the process reply with the right key");
if (isE10S) {
promise = promiseEventOnItemAndContainer(assert, remoteProcesses[0].port, processes.port, 'sdk/test/pong', remoteProcesses[0]);
remoteProcesses[0].port.emit('sdk/test/ping', "remote");
reply = yield promise;
assert.equal(reply[0], "remote", "Saw the process reply with the right key");
assert.notEqual(localProcesses[0], remoteProcesses[0], "Processes should be different");
}
loader.unload();
};
// Test that the frame lists are kept up to date
exports["test frame list"] = function*(assert) {
function browserFrames(list) {
return Array.filter(list, b => b.isTab).length;
}
let window = getMostRecentBrowserWindow();
let tabs = getTabs(window);
assert.equal(tabs.length, 1, "Should have just the one tab to start with");
let loader = new Loader(module);
let { processes, frames } = yield waitForProcesses(loader);
assert.equal(browserFrames(frames), getTabs(window).length, "Should be the right number of browser frames.");
assert.equal((yield getChildFrameCount(processes)), frames.length, "Child processes should have the right number of frames");
let promise = promiseEvent(frames, 'attach');
let tab1 = openTab(window, LOCAL_URI);
let [frame1] = yield promise;
assert.ok(!!frame1, "Should have seen the new frame");
assert.ok(!frame1.process.isRemote, "Frame should not be remote");
assert.equal(browserFrames(frames), getTabs(window).length, "Should be the right number of browser frames.");
assert.equal((yield getChildFrameCount(processes)), frames.length, "Child processes should have the right number of frames");
promise = promiseEvent(frames, 'attach');
let tab2 = openTab(window, REMOTE_URI);
let [frame2] = yield promise;
assert.ok(!!frame2, "Should have seen the new frame");
if (isE10S)
assert.ok(frame2.process.isRemote, "Frame should be remote");
else
assert.ok(!frame2.process.isRemote, "Frame should not be remote");
assert.equal(browserFrames(frames), getTabs(window).length, "Should be the right number of browser frames.");
assert.equal((yield getChildFrameCount(processes)), frames.length, "Child processes should have the right number of frames");
frames.port.emit('sdk/test/ping')
yield new Promise(resolve => {
let count = 0;
let listener = () => {
console.log("Saw pong");
count++;
if (count == frames.length) {
frames.port.off('sdk/test/pong', listener);
resolve();
}
};
frames.port.on('sdk/test/pong', listener);
});
let badListener = () => {
assert.fail("Should not have seen a response through this frame");
}
frame1.port.on('sdk/test/pong', badListener);
frame2.port.emit('sdk/test/ping', 'b');
let [key] = yield promiseEventOnItemAndContainer(assert, frame2.port, frames.port, 'sdk/test/pong', frame2);
assert.equal(key, 'b', "Should have seen the right response");
frame1.port.off('sdk/test/pong', badListener);
frame2.port.on('sdk/test/pong', badListener);
frame1.port.emit('sdk/test/ping', 'b');
[key] = yield promiseEventOnItemAndContainer(assert, frame1.port, frames.port, 'sdk/test/pong', frame1);
assert.equal(key, 'b', "Should have seen the right response");
frame2.port.off('sdk/test/pong', badListener);
promise = promiseEventOnItemAndContainer(assert, frame1, frames, 'detach');
closeTab(tab1);
yield promise;
assert.equal(browserFrames(frames), getTabs(window).length, "Should be the right number of browser frames.");
assert.equal((yield getChildFrameCount(processes)), frames.length, "Child processes should have the right number of frames");
promise = promiseEventOnItemAndContainer(assert, frame2, frames, 'detach');
closeTab(tab2);
yield promise;
assert.equal(browserFrames(frames), getTabs(window).length, "Should be the right number of browser frames.");
assert.equal((yield getChildFrameCount(processes)), frames.length, "Child processes should have the right number of frames");
loader.unload();
};
// Test that multiple loaders get their own loaders in the child and messages
// don't cross. Unload should work
exports["test new loader"] = function*(assert) {
let loader1 = new Loader(module);
let { processes: processes1 } = yield waitForProcesses(loader1);
let loader2 = new Loader(module);
let { processes: processes2 } = yield waitForProcesses(loader2);
let process1 = [...processes1][0];
let process2 = [...processes2][0];
process1.port.on('sdk/test/pong', (process, key) => {
assert.equal(key, "a", "Should have seen the right pong");
});
process2.port.on('sdk/test/pong', (process, key) => {
assert.equal(key, "b", "Should have seen the right pong");
});
process1.port.emit('sdk/test/ping', 'a');
yield promiseEvent(process1.port, 'sdk/test/pong');
process2.port.emit('sdk/test/ping', 'b');
yield promiseEvent(process2.port, 'sdk/test/pong');
loader1.unload();
process2.port.emit('sdk/test/ping', 'b');
yield promiseEvent(process2.port, 'sdk/test/pong');
loader2.unload();
};
// Test that unloading the loader unloads the child instances
exports["test unload"] = function*(assert) {
let window = getMostRecentBrowserWindow();
let loader = new Loader(module);
let { frames } = yield waitForProcesses(loader);
let promise = promiseEvent(frames, 'attach');
let tab = openTab(window, "data:,<html/>");
let browser = getBrowserForTab(tab);
yield promiseDOMEvent(browser, "load", true);
let [frame] = yield promise;
assert.ok(!!frame, "Should have seen the new frame");
promise = promiseDOMEvent(browser, 'hashchange');
frame.port.emit('sdk/test/testunload');
loader.unload("shutdown");
yield promise;
let hash = getURI(tab).replace(/.*#/, "");
assert.equal(hash, "unloaded:shutdown", "Saw the correct hash change.")
closeTab(tab);
}
// Test that unloading the loader causes the child to see frame detach events
exports["test frame detach on unload"] = function*(assert) {
let window = getMostRecentBrowserWindow();
let loader = new Loader(module);
let { frames } = yield waitForProcesses(loader);
let promise = promiseEvent(frames, 'attach');
let tab = openTab(window, "data:,<html/>");
let browser = getBrowserForTab(tab);
yield promiseDOMEvent(browser, "load", true);
let [frame] = yield promise;
assert.ok(!!frame, "Should have seen the new frame");
promise = promiseDOMEvent(browser, 'hashchange');
frame.port.emit('sdk/test/testdetachonunload');
loader.unload("shutdown");
yield promise;
let hash = getURI(tab).replace(/.*#/, "");
assert.equal(hash, "unloaded", "Saw the correct hash change.")
closeTab(tab);
}
// Test that DOM event listener on the frame object works
exports["test frame event listeners"] = function*(assert) {
let window = getMostRecentBrowserWindow();
let loader = new Loader(module);
let { frames } = yield waitForProcesses(loader);
let promise = promiseEvent(frames, 'attach');
let tab = openTab(window, "data:text/html,<html></html>");
let browser = getBrowserForTab(tab);
yield promiseDOMEvent(browser, "load", true);
let [frame] = yield promise;
assert.ok(!!frame, "Should have seen the new frame");
frame.port.emit('sdk/test/registerframeevent');
promise = Promise.all([
promiseEvent(frame.port, 'sdk/test/sawreply'),
promiseEvent(frame.port, 'sdk/test/eventsent')
]);
frame.port.emit('sdk/test/sendevent');
yield promise;
frame.port.emit('sdk/test/unregisterframeevent');
promise = promiseEvent(frame.port, 'sdk/test/eventsent');
frame.port.on('sdk/test/sawreply', () => {
assert.fail("Should not have seen the event listener reply");
});
frame.port.emit('sdk/test/sendevent');
yield promise;
closeTab(tab);
loader.unload();
}
// Test that DOM event listener on the frames object works
exports["test frames event listeners"] = function*(assert) {
let window = getMostRecentBrowserWindow();
let loader = new Loader(module);
let { frames } = yield waitForProcesses(loader);
let promise = promiseEvent(frames, 'attach');
let tab = openTab(window, "data:text/html,<html></html>");
let browser = getBrowserForTab(tab);
yield promiseDOMEvent(browser, "load", true);
let [frame] = yield promise;
assert.ok(!!frame, "Should have seen the new frame");
frame.port.emit('sdk/test/registerframesevent');
promise = Promise.all([
promiseEvent(frame.port, 'sdk/test/sawreply'),
promiseEvent(frame.port, 'sdk/test/eventsent')
]);
frame.port.emit('sdk/test/sendevent');
yield promise;
frame.port.emit('sdk/test/unregisterframesevent');
promise = promiseEvent(frame.port, 'sdk/test/eventsent');
frame.port.on('sdk/test/sawreply', () => {
assert.fail("Should not have seen the event listener reply");
});
frame.port.emit('sdk/test/sendevent');
yield promise;
closeTab(tab);
loader.unload();
}
// Test that unloading unregisters frame DOM events
exports["test unload removes frame event listeners"] = function*(assert) {
let window = getMostRecentBrowserWindow();
let loader = new Loader(module);
let { frames } = yield waitForProcesses(loader);
let loader2 = new Loader(module);
let { frames: frames2 } = yield waitForProcesses(loader2);
let promise = promiseEvent(frames, 'attach');
let promise2 = promiseEvent(frames2, 'attach');
let tab = openTab(window, "data:text/html,<html></html>");
let browser = getBrowserForTab(tab);
yield promiseDOMEvent(browser, "load", true);
let [frame] = yield promise;
let [frame2] = yield promise2;
assert.ok(!!frame && !!frame2, "Should have seen the new frame");
frame.port.emit('sdk/test/registerframeevent');
promise = Promise.all([
promiseEvent(frame2.port, 'sdk/test/sawreply'),
promiseEvent(frame2.port, 'sdk/test/eventsent')
]);
frame2.port.emit('sdk/test/sendevent');
yield promise;
loader.unload();
promise = promiseEvent(frame2.port, 'sdk/test/eventsent');
frame2.port.on('sdk/test/sawreply', () => {
assert.fail("Should not have seen the event listener reply");
});
frame2.port.emit('sdk/test/sendevent');
yield promise;
closeTab(tab);
loader2.unload();
}
// Test that unloading unregisters frames DOM events
exports["test unload removes frames event listeners"] = function*(assert) {
let window = getMostRecentBrowserWindow();
let loader = new Loader(module);
let { frames } = yield waitForProcesses(loader);
let loader2 = new Loader(module);
let { frames: frames2 } = yield waitForProcesses(loader2);
let promise = promiseEvent(frames, 'attach');
let promise2 = promiseEvent(frames2, 'attach');
let tab = openTab(window, "data:text/html,<html></html>");
let browser = getBrowserForTab(tab);
yield promiseDOMEvent(browser, "load", true);
let [frame] = yield promise;
let [frame2] = yield promise2;
assert.ok(!!frame && !!frame2, "Should have seen the new frame");
frame.port.emit('sdk/test/registerframesevent');
promise = Promise.all([
promiseEvent(frame2.port, 'sdk/test/sawreply'),
promiseEvent(frame2.port, 'sdk/test/eventsent')
]);
frame2.port.emit('sdk/test/sendevent');
yield promise;
loader.unload();
promise = promiseEvent(frame2.port, 'sdk/test/eventsent');
frame2.port.on('sdk/test/sawreply', () => {
assert.fail("Should not have seen the event listener reply");
});
frame2.port.emit('sdk/test/sendevent');
yield promise;
closeTab(tab);
loader2.unload();
}
// Check that the child frame has the right properties
exports["test frame properties"] = function*(assert) {
let loader = new Loader(module);
let { frames } = yield waitForProcesses(loader);
let promise = new Promise(resolve => {
let count = frames.length;
let listener = (frame, properties) => {
assert.equal(properties.isTab, frame.isTab,
"Child frame should have the same isTab property");
if (--count == 0) {
frames.port.off('sdk/test/replyproperties', listener);
resolve();
}
}
frames.port.on('sdk/test/replyproperties', listener);
})
frames.port.emit('sdk/test/checkproperties');
yield promise;
loader.unload();
}
// Check that non-remote processes have the same process ID and remote processes
// have different IDs
exports["test processID"] = function*(assert) {
let loader = new Loader(module);
let { processes } = yield waitForProcesses(loader);
for (let process of processes) {
process.port.emit('sdk/test/getprocessid');
let [p, ID] = yield promiseEvent(process.port, 'sdk/test/processid');
if (process.isRemote) {
assert.notEqual(ID, processID, "Remote processes should have a different process ID");
}
else {
assert.equal(ID, processID, "Remote processes should have the same process ID");
}
}
}
after(exports, function*(name, assert) {
yield cleanUI();
});
require('sdk/test/runner').runTestsFromModule(module);

View File

@ -0,0 +1,8 @@
{
"name": "remote",
"title": "remote",
"id": "remote@jetpack",
"description": "Run remote tests",
"version": "1.0.0",
"main": "main.js"
}

View File

@ -0,0 +1,105 @@
/* 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/. */
const { when } = require('sdk/system/unload');
const { process, frames } = require('sdk/remote/child');
const { loaderID } = require('@loader/options');
const { processID } = require('sdk/system/runtime');
const system = require('sdk/system/events');
const { Cu } = require('chrome');
function log(str) {
console.log("remote[" + loaderID + "][" + processID + "]: " + str);
}
log("module loaded");
process.port.emit('sdk/test/load');
process.port.on('sdk/test/ping', (process, key) => {
log("received process ping");
process.port.emit('sdk/test/pong', key);
});
let frameCount = 0;
frames.forEvery(frame => {
frameCount++;
frame.on('detach', () => {
frameCount--;
});
frame.port.on('sdk/test/ping', (frame, key) => {
log("received frame ping");
frame.port.emit('sdk/test/pong', key);
});
});
frames.port.on('sdk/test/checkproperties', frame => {
frame.port.emit('sdk/test/replyproperties', {
isTab: frame.isTab
});
});
process.port.on('sdk/test/count', () => {
log("received count ping");
process.port.emit('sdk/test/count', frameCount);
});
process.port.on('sdk/test/getprocessid', () => {
process.port.emit('sdk/test/processid', processID);
});
frames.port.on('sdk/test/testunload', (frame) => {
// Cache the content since the frame will have been destroyed by the time
// we see the unload event.
let content = frame.content;
when((reason) => {
content.location = "#unloaded:" + reason;
});
});
frames.port.on('sdk/test/testdetachonunload', (frame) => {
let content = frame.content;
frame.on('detach', () => {
console.log("Detach from " + frame.content.location);
frame.content.location = "#unloaded";
});
});
frames.port.on('sdk/test/sendevent', (frame) => {
let doc = frame.content.document;
let listener = () => {
frame.port.emit('sdk/test/sawreply');
}
system.on("Test:Reply", listener);
let event = new frame.content.CustomEvent("Test:Event");
doc.dispatchEvent(event);
system.off("Test:Reply", listener);
frame.port.emit('sdk/test/eventsent');
});
function listener(event) {
// Use the raw observer service here since it will be usable even if the
// loader has unloaded
let { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
Services.obs.notifyObservers(null, "Test:Reply", "");
}
frames.port.on('sdk/test/registerframesevent', (frame) => {
frames.addEventListener("Test:Event", listener, true);
});
frames.port.on('sdk/test/unregisterframesevent', (frame) => {
frames.removeEventListener("Test:Event", listener, true);
});
frames.port.on('sdk/test/registerframeevent', (frame) => {
frame.addEventListener("Test:Event", listener, true);
});
frames.port.on('sdk/test/unregisterframeevent', (frame) => {
frame.removeEventListener("Test:Event", listener, true);
});

View File

@ -0,0 +1,110 @@
/* 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 { Cu } = require('chrome');
const { Task: { async } } = Cu.import('resource://gre/modules/Task.jsm', {});
const { getMostRecentBrowserWindow } = require('sdk/window/utils');
const REMOTE_MODULE = "./remote-module";
function promiseEvent(emitter, event) {
console.log("Waiting for " + event);
return new Promise(resolve => {
emitter.once(event, (...args) => {
console.log("Saw " + event);
resolve(args);
});
});
}
exports.promiseEvent = promiseEvent;
function promiseDOMEvent(target, event, isCapturing = false) {
console.log("Waiting for " + event);
return new Promise(resolve => {
let listener = (event) => {
target.removeEventListener(event, listener, isCapturing);
resolve(event);
};
target.addEventListener(event, listener, isCapturing);
})
}
exports.promiseDOMEvent = promiseDOMEvent;
const promiseEventOnItemAndContainer = async(function*(assert, itemport, container, event, item = itemport) {
let itemEvent = promiseEvent(itemport, event);
let containerEvent = promiseEvent(container, event);
let itemArgs = yield itemEvent;
let containerArgs = yield containerEvent;
assert.equal(containerArgs[0], item, "Should have seen a container event for the right item");
assert.equal(JSON.stringify(itemArgs), JSON.stringify(containerArgs), "Arguments should have matched");
// Strip off the item from the returned arguments
return itemArgs.slice(1);
});
exports.promiseEventOnItemAndContainer = promiseEventOnItemAndContainer;
const waitForProcesses = async(function*(loader) {
console.log("Starting remote");
let { processes, frames, remoteRequire } = loader.require('sdk/remote/parent');
remoteRequire(REMOTE_MODULE, module);
let events = [];
// In e10s we should expect to see two processes
let expectedCount = isE10S ? 2 : 1;
yield new Promise(resolve => {
let count = 0;
// Wait for a process to be detected
let listener = process => {
console.log("Saw a process attach");
// Wait for the remote module to load in this process
process.port.once('sdk/test/load', () => {
console.log("Saw a remote module load");
count++;
if (count == expectedCount) {
processes.off('attach', listener);
resolve();
}
});
}
processes.on('attach', listener);
});
console.log("Remote ready");
return { processes, frames, remoteRequire };
});
exports.waitForProcesses = waitForProcesses;
// Counts the frames in all the child processes
const getChildFrameCount = async(function*(processes) {
let frameCount = 0;
for (let process of processes) {
process.port.emit('sdk/test/count');
let [p, count] = yield promiseEvent(process.port, 'sdk/test/count');
frameCount += count;
}
return frameCount;
});
exports.getChildFrameCount = getChildFrameCount;
const mainWindow = getMostRecentBrowserWindow();
const isE10S = mainWindow.gMultiProcessBrowser;
exports.isE10S = isE10S;
if (isE10S) {
console.log("Testing in E10S mode");
// We expect a child process to already be present, make sure that is the case
mainWindow.XULBrowserWindow.forceInitialBrowserRemote();
}
else {
console.log("Testing in non-E10S mode");
}

View File

@ -80,13 +80,13 @@ const select = (target, tab=getActiveTab()) =>
});
exports.select = select;
const attributeBlacklist = new Set(["data-component-path"]);
const attributeBlocklist = new Set(["data-component-path"]);
const attributeRenameTable = Object.assign(Object.create(null), {
class: "className"
});
const readAttributes = node =>
object(...map(({name, value}) => [attributeRenameTable[name] || name, value],
filter(({name}) => !attributeBlacklist.has(name),
filter(({name}) => !attributeBlocklist.has(name),
node.attributes)));
exports.readAttributes = readAttributes;

View File

@ -0,0 +1,4 @@
/* 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";

View File

@ -0,0 +1,5 @@
{
"id": "addon@jetpack",
"name": "addon",
"version": "0.0.1"
}

View File

@ -0,0 +1,52 @@
/* 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 { Cu, Cc, Ci } = require("chrome");
const { evaluate } = require("sdk/loader/sandbox");
const ROOT = require.resolve("sdk/base64").replace("/sdk/base64.js", "");
// Note: much of this test code is from
// http://dxr.mozilla.org/mozilla-central/source/toolkit/mozapps/extensions/internal/XPIProvider.jsm
const BOOTSTRAP_REASONS = {
APP_STARTUP : 1,
APP_SHUTDOWN : 2,
ADDON_ENABLE : 3,
ADDON_DISABLE : 4,
ADDON_INSTALL : 5,
ADDON_UNINSTALL : 6,
ADDON_UPGRADE : 7,
ADDON_DOWNGRADE : 8
};
function createBootstrapScope(options) {
let { uri, id: aId } = options;
let principal = Cc["@mozilla.org/systemprincipal;1"].
createInstance(Ci.nsIPrincipal);
let bootstrapScope = new Cu.Sandbox(principal, {
sandboxName: uri,
wantGlobalProperties: ["indexedDB"],
addonId: aId,
metadata: { addonID: aId, URI: uri }
});
// Copy the reason values from the global object into the bootstrap scope.
for (let name in BOOTSTRAP_REASONS)
bootstrapScope[name] = BOOTSTRAP_REASONS[name];
return bootstrapScope;
}
exports.create = createBootstrapScope;
function evaluateBootstrap(options) {
let { uri, scope } = options;
evaluate(scope,
`${"Components"}.classes['@mozilla.org/moz/jssubscript-loader;1']
.createInstance(${"Components"}.interfaces.mozIJSSubScriptLoader)
.loadSubScript("${uri}");`, "ECMAv5");
}
exports.evaluate = evaluateBootstrap;

View File

@ -0,0 +1,6 @@
/* 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';
exports.bar = "do not ignore this export";

View File

@ -0,0 +1,19 @@
/* 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';
var foo = require("foo");
var coolTabs = require("cool-tabs");
exports.foo = foo.fs;
exports.bar = foo.bar;
exports.fs = require("sdk/io/fs");
exports.extra = require("fs-extra").extra;
exports.overload = require("overload");
exports.overloadLib = require("overload/lib/foo.js");
exports.internal = require("internal").internal;
exports.Tabs = require("sdk/tabs").Tabs;
exports.CoolTabs = coolTabs.Tabs;
exports.CoolTabsLib = coolTabs.TabsLib;
exports.ignore = require("./lib/ignore").foo;

View File

@ -0,0 +1,6 @@
/* 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';
exports.foo = require("../ignore").bar;

View File

@ -0,0 +1,6 @@
/* 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';
exports.internal = "test";

View File

@ -0,0 +1,6 @@
/* 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';
exports.Tabs = "no tabs exist";

View File

@ -0,0 +1,7 @@
/* 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';
exports.Tabs = require("sdk/tabs").Tabs;
exports.TabsLib = require("./lib/tabs").Tabs

View File

@ -0,0 +1,6 @@
/* 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';
exports.Tabs = "a cool tabs implementation";

View File

@ -0,0 +1,4 @@
{
"name": "cool-tabs",
"main": "index.js"
}

View File

@ -0,0 +1,7 @@
/* 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';
exports.fs = require("fs");
exports.bar = require("bar");

View File

@ -0,0 +1,6 @@
/* 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';
exports.fs = require("fs");

View File

@ -0,0 +1,6 @@
/* 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';
module.exports = require("fs");

View File

@ -0,0 +1,5 @@
{
"name": "bar",
"version": "0.0.1",
"main": "./index.js"
}

View File

@ -0,0 +1,8 @@
{
"name": "foo",
"version": "0.0.1",
"main": "./index.js",
"dependencies": {
"bar": "*"
}
}

View File

@ -0,0 +1,6 @@
/* 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';
exports.extra = true;

View File

@ -0,0 +1,4 @@
{
"name": "fs-extra",
"main": "index.js"
}

View File

@ -0,0 +1,18 @@
{
"name": "native-overrides-test",
"main": "index.js",
"dependencies": {
"cool-tabs": "*",
"foo": "*",
"fs-extra": "*"
},
"jetpack": {
"overrides": {
"fs": "sdk/io/fs",
"overload": "foo",
"internal": "./lib/internal",
"sdk/tabs": "./lib/tabs",
"../ignore": "foo"
}
}
}

View File

@ -17,7 +17,7 @@ const PATH = '/test-contentScriptWhen.html';
function createLoader () {
let options = merge({}, require('@loader/options'),
{ prefixURI: require('./fixtures').url() });
{ id: "testloader", prefixURI: require('./fixtures').url() });
return Loader(module, null, options);
}
exports.createLoader = createLoader;

View File

@ -10,6 +10,7 @@
"media.gmp-manager.cert.requireBuiltIn": false,
"media.gmp-manager.url": "http://localhost/media-dummy/gmpmanager",
"media.gmp-manager.url.override": "http://localhost/dummy-gmp-manager.xml",
"browser.aboutHomeSnippets.updateUrl": "https://localhost/snippet-dummy",
"browser.newtab.url": "about:blank",
"browser.search.update": false,
"browser.safebrowsing.enabled": false,

View File

@ -4,8 +4,7 @@
"use strict";
const { Cu, Cc, Ci } = require("chrome");
const { evaluate } = require("sdk/loader/sandbox");
const { create, evaluate } = require("./fixtures/bootstrap/utils");
const ROOT = require.resolve("sdk/base64").replace("/sdk/base64.js", "");
@ -22,51 +21,77 @@ const BOOTSTRAP_REASONS = {
ADDON_DOWNGRADE : 8
};
exports["test minimal bootstrap.js"] = function(assert) {
let aId = "test-min-boot@jetpack";
exports["test install/startup/shutdown/uninstall all return a promise"] = function(assert) {
let uri = require.resolve("./fixtures/addon/bootstrap.js");
let principal = Cc["@mozilla.org/systemprincipal;1"].
createInstance(Ci.nsIPrincipal);
let bootstrapScope = new Cu.Sandbox(principal, {
sandboxName: uri,
wantGlobalProperties: ["indexedDB"],
addonId: aId,
metadata: { addonID: aId, URI: uri }
let id = "test-min-boot@jetpack";
let bootstrapScope = create({
id: id,
uri: uri
});
try {
// Copy the reason values from the global object into the bootstrap scope.
for (let name in BOOTSTRAP_REASONS)
bootstrapScope[name] = BOOTSTRAP_REASONS[name];
// As we don't want our caller to control the JS version used for the
// bootstrap file, we run loadSubScript within the context of the
// sandbox with the latest JS version set explicitly.
bootstrapScope.ROOT = ROOT;
// As we don't want our caller to control the JS version used for the
// bootstrap file, we run loadSubScript within the context of the
// sandbox with the latest JS version set explicitly.
bootstrapScope.ROOT = ROOT;
evaluate({
uri: uri,
scope: bootstrapScope
});
assert.equal(typeof bootstrapScope.install, "undefined", "install DNE");
assert.equal(typeof bootstrapScope.startup, "undefined", "startup DNE");
assert.equal(typeof bootstrapScope.shutdown, "undefined", "shutdown DNE");
assert.equal(typeof bootstrapScope.uninstall, "undefined", "uninstall DNE");
let addon = {
id: id,
version: "0.0.1",
resourceURI: {
spec: uri.replace("bootstrap.js", "")
}
};
evaluate(bootstrapScope,
`${"Components"}.classes['@mozilla.org/moz/jssubscript-loader;1']
.createInstance(${"Components"}.interfaces.mozIJSSubScriptLoader)
.loadSubScript("${uri}");`, "ECMAv5");
let install = bootstrapScope.install(addon, BOOTSTRAP_REASONS.ADDON_INSTALL);
yield install.then(() => assert.pass("install returns a promise"));
assert.equal(typeof bootstrapScope.install, "function", "install exists");
assert.equal(typeof bootstrapScope.startup, "function", "startup exists");
assert.equal(typeof bootstrapScope.shutdown, "function", "shutdown exists");
assert.equal(typeof bootstrapScope.uninstall, "function", "uninstall exists");
let startup = bootstrapScope.startup(addon, BOOTSTRAP_REASONS.ADDON_INSTALL);
yield startup.then(() => assert.pass("startup returns a promise"));
bootstrapScope.shutdown(null, BOOTSTRAP_REASONS.ADDON_DISABLE);
}
catch(e) {
console.exception(e)
assert.fail(e)
}
let shutdown = bootstrapScope.shutdown(addon, BOOTSTRAP_REASONS.ADDON_DISABLE);
yield shutdown.then(() => assert.pass("shutdown returns a promise"));
// calling shutdown multiple times is fine
shutdown = bootstrapScope.shutdown(addon, BOOTSTRAP_REASONS.ADDON_DISABLE);
yield shutdown.then(() => assert.pass("shutdown returns working promise on multiple calls"));
let uninstall = bootstrapScope.uninstall(addon, BOOTSTRAP_REASONS.ADDON_UNINSTALL);
yield uninstall.then(() => assert.pass("uninstall returns a promise"));
}
exports["test minimal bootstrap.js"] = function*(assert) {
let uri = require.resolve("./fixtures/addon/bootstrap.js");
let bootstrapScope = create({
id: "test-min-boot@jetpack",
uri: uri
});
// As we don't want our caller to control the JS version used for the
// bootstrap file, we run loadSubScript within the context of the
// sandbox with the latest JS version set explicitly.
bootstrapScope.ROOT = ROOT;
assert.equal(typeof bootstrapScope.install, "undefined", "install DNE");
assert.equal(typeof bootstrapScope.startup, "undefined", "startup DNE");
assert.equal(typeof bootstrapScope.shutdown, "undefined", "shutdown DNE");
assert.equal(typeof bootstrapScope.uninstall, "undefined", "uninstall DNE");
evaluate({
uri: uri,
scope: bootstrapScope
});
assert.equal(typeof bootstrapScope.install, "function", "install exists");
assert.equal(typeof bootstrapScope.startup, "function", "startup exists");
assert.equal(typeof bootstrapScope.shutdown, "function", "shutdown exists");
assert.equal(typeof bootstrapScope.uninstall, "function", "uninstall exists");
bootstrapScope.shutdown(null, BOOTSTRAP_REASONS.ADDON_DISABLE);
}
require("sdk/test").run(exports);

Some files were not shown because too many files have changed in this diff Show More