Merge fx-team to m-c. a=merge
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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);
|
До Ширина: | Высота: | Размер: 1.1 KiB После Ширина: | Высота: | Размер: 1.1 KiB |
До Ширина: | Высота: | Размер: 3.6 KiB После Ширина: | Высота: | Размер: 3.6 KiB |
До Ширина: | Высота: | Размер: 1.1 KiB После Ширина: | Высота: | Размер: 1.1 KiB |
До Ширина: | Высота: | Размер: 1.1 KiB После Ширина: | Высота: | Размер: 1.1 KiB |
До Ширина: | Высота: | Размер: 386 B После Ширина: | Высота: | Размер: 386 B |
До Ширина: | Высота: | Размер: 6.4 KiB После Ширина: | Высота: | Размер: 6.4 KiB |
После Ширина: | Высота: | Размер: 1.6 KiB |
|
@ -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>
|
|
@ -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;
|
||||
}
|
|
@ -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 }
|
||||
});
|
|
@ -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"
|
||||
}
|
|
@ -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);
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
|
@ -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'];
|
||||
|
|
|
@ -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'];
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
});
|
|
@ -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);
|
||||
})
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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);
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
|
@ -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);
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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");
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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`.
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 = {};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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 <b>HTML</b> 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 <b>HTML</b> 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);
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"id": "e10s-l10n@jetpack",
|
||||
"main": "./main.js",
|
||||
"version": "0.0.1"
|
||||
}
|
|
@ -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);
|
|
@ -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
|
||||
}
|
|
@ -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);
|
||||
});
|
|
@ -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");
|
||||
}
|
|
@ -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]
|
||||
|
|
|
@ -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.
|
|
@ -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 <b>HTML</b> 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) {
|
||||
|
|
|
@ -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);
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "remote",
|
||||
"title": "remote",
|
||||
"id": "remote@jetpack",
|
||||
"description": "Run remote tests",
|
||||
"version": "1.0.0",
|
||||
"main": "main.js"
|
||||
}
|
|
@ -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);
|
||||
});
|
|
@ -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");
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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";
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"id": "addon@jetpack",
|
||||
"name": "addon",
|
||||
"version": "0.0.1"
|
||||
}
|
|
@ -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;
|
|
@ -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";
|
|
@ -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;
|
|
@ -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;
|
|
@ -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";
|
|
@ -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";
|
7
addon-sdk/source/test/fixtures/native-overrides-test/node_modules/cool-tabs/index.js
сгенерированный
поставляемый
Normal 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
|
6
addon-sdk/source/test/fixtures/native-overrides-test/node_modules/cool-tabs/lib/tabs.js
сгенерированный
поставляемый
Normal 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";
|
4
addon-sdk/source/test/fixtures/native-overrides-test/node_modules/cool-tabs/package.json
сгенерированный
поставляемый
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"name": "cool-tabs",
|
||||
"main": "index.js"
|
||||
}
|
7
addon-sdk/source/test/fixtures/native-overrides-test/node_modules/foo/index.js
сгенерированный
поставляемый
Normal 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");
|
6
addon-sdk/source/test/fixtures/native-overrides-test/node_modules/foo/lib/foo.js
сгенерированный
поставляемый
Normal 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");
|
6
addon-sdk/source/test/fixtures/native-overrides-test/node_modules/foo/node_modules/bar/index.js
сгенерированный
поставляемый
Normal 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");
|
5
addon-sdk/source/test/fixtures/native-overrides-test/node_modules/foo/node_modules/bar/package.json
сгенерированный
поставляемый
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"name": "bar",
|
||||
"version": "0.0.1",
|
||||
"main": "./index.js"
|
||||
}
|
8
addon-sdk/source/test/fixtures/native-overrides-test/node_modules/foo/package.json
сгенерированный
поставляемый
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "foo",
|
||||
"version": "0.0.1",
|
||||
"main": "./index.js",
|
||||
"dependencies": {
|
||||
"bar": "*"
|
||||
}
|
||||
}
|
6
addon-sdk/source/test/fixtures/native-overrides-test/node_modules/fs-extra/index.js
сгенерированный
поставляемый
Normal 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;
|
4
addon-sdk/source/test/fixtures/native-overrides-test/node_modules/fs-extra/package.json
сгенерированный
поставляемый
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"name": "fs-extra",
|
||||
"main": "index.js"
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|