Merge fx-team to central, a=merge

This commit is contained in:
Wes Kocher 2016-02-25 14:32:51 -08:00
commit fe6551955d
54 changed files with 1333 additions and 452 deletions

View File

@ -2,13 +2,10 @@
support-files = head.js
[browser_basic_functionality.js]
skip-if = buildapp == "mulet" || e10s
skip-if = buildapp == "mulet"
[browser_first_download_panel.js]
skip-if = os == "linux" # Bug 949434
[browser_overflow_anchor.js]
skip-if = os == "linux" # Bug 952422
[browser_confirm_unblock_download.js]
[browser_iframe_gone_mid_download.js]
skip-if = e10s

View File

@ -0,0 +1,53 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
var {
PlatformInfo,
} = ExtensionUtils;
// WeakMap[Extension -> Map[name => Command]]
var commandsMap = new WeakMap();
function Command(description, shortcut) {
this.description = description;
this.shortcut = shortcut;
}
/* eslint-disable mozilla/balanced-listeners */
extensions.on("manifest_commands", (type, directive, extension, manifest) => {
let commands = new Map();
for (let name of Object.keys(manifest.commands)) {
let os = PlatformInfo.os == "win" ? "windows" : PlatformInfo.os;
let manifestCommand = manifest.commands[name];
let description = manifestCommand.description;
let shortcut = manifestCommand.suggested_key[os] || manifestCommand.suggested_key.default;
let command = new Command(description, shortcut);
commands.set(name, command);
}
commandsMap.set(extension, commands);
});
extensions.on("shutdown", (type, extension) => {
commandsMap.delete(extension);
});
/* eslint-enable mozilla/balanced-listeners */
extensions.registerSchemaAPI("commands", null, (extension, context) => {
return {
commands: {
getAll() {
let commands = Array.from(commandsMap.get(extension), ([name, command]) => {
return ({
name,
description: command.description,
shortcut: command.shortcut,
});
});
return Promise.resolve(commands);
},
},
};
});

View File

@ -5,6 +5,7 @@
browser.jar:
content/browser/extension.svg
content/browser/ext-utils.js
content/browser/ext-commands.js
content/browser/ext-contextMenus.js
content/browser/ext-browserAction.js
content/browser/ext-pageAction.js

View File

@ -9,6 +9,7 @@ support-files =
file_popup_api_injection_b.html
[browser_ext_simple.js]
[browser_ext_commands.js]
[browser_ext_currentWindow.js]
[browser_ext_browserAction_simple.js]
[browser_ext_browserAction_pageAction_icon.js]

View File

@ -0,0 +1,81 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
var {AppConstants} = Cu.import("resource://gre/modules/AppConstants.jsm");
add_task(function* () {
let extension = ExtensionTestUtils.loadExtension({
manifest: {
"name": "Commands Extension",
"commands": {
"with-desciption": {
"suggested_key": {
"default": "Ctrl+Shift+Y",
},
"description": "should have a description",
},
"without-description": {
"suggested_key": {
"default": "Ctrl+Shift+D",
},
},
"with-platform-info": {
"suggested_key": {
"mac": "Ctrl+Shift+M",
"linux": "Ctrl+Shift+L",
"windows": "Ctrl+Shift+W",
"android": "Ctrl+Shift+A",
},
},
},
},
background: function() {
browser.test.onMessage.addListener((message, additionalScope) => {
browser.commands.getAll((commands) => {
let errorMessage = "getAll should return an array of commands";
browser.test.assertEq(commands.length, 3, errorMessage);
let command = commands.find(c => c.name == "with-desciption");
errorMessage = "The description should match what is provided in the manifest";
browser.test.assertEq("should have a description", command.description, errorMessage);
errorMessage = "The shortcut should match the default shortcut provided in the manifest";
browser.test.assertEq("Ctrl+Shift+Y", command.shortcut, errorMessage);
command = commands.find(c => c.name == "without-description");
errorMessage = "The description should be empty when it is not provided";
browser.test.assertEq(null, command.description, errorMessage);
errorMessage = "The shortcut should match the default shortcut provided in the manifest";
browser.test.assertEq("Ctrl+Shift+D", command.shortcut, errorMessage);
let platformKeys = {
macosx: "M",
linux: "L",
win: "W",
android: "A",
};
command = commands.find(c => c.name == "with-platform-info");
let platformKey = platformKeys[additionalScope.platform];
let shortcut = `Ctrl+Shift+${platformKey}`;
errorMessage = `The shortcut should match the one provided in the manifest for OS='${additionalScope.platform}'`;
browser.test.assertEq(shortcut, command.shortcut, errorMessage);
browser.test.notifyPass("commands");
});
});
browser.test.sendMessage("ready");
},
});
yield extension.startup();
yield extension.awaitMessage("ready");
extension.sendMessage("additional-scope", {platform: AppConstants.platform});
yield extension.awaitFinish("commands");
yield extension.unload();
});

View File

@ -546,6 +546,7 @@ BrowserGlue.prototype = {
ExtensionManagement.registerScript("chrome://browser/content/ext-utils.js");
ExtensionManagement.registerScript("chrome://browser/content/ext-browserAction.js");
ExtensionManagement.registerScript("chrome://browser/content/ext-pageAction.js");
ExtensionManagement.registerScript("chrome://browser/content/ext-commands.js");
ExtensionManagement.registerScript("chrome://browser/content/ext-contextMenus.js");
ExtensionManagement.registerScript("chrome://browser/content/ext-desktop-runtime.js");
ExtensionManagement.registerScript("chrome://browser/content/ext-tabs.js");

View File

@ -2,18 +2,17 @@
* 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/. */
/* global React */
/* eslint-env browser */
"use strict";
loader.lazyRequireGetter(this, "React",
"devtools/client/shared/vendor/react");
loader.lazyRequireGetter(this, "AddonsTab",
"devtools/client/aboutdebugging/components/addons-tab", true);
loader.lazyRequireGetter(this, "TabMenu",
"devtools/client/aboutdebugging/components/tab-menu", true);
loader.lazyRequireGetter(this, "WorkersTab",
"devtools/client/aboutdebugging/components/workers-tab", true);
const Services = require("Services");
const React = require("devtools/client/shared/vendor/react");
const { TabMenu } = require("./tab-menu");
loader.lazyRequireGetter(this, "AddonsTab", "./components/addons-tab", true);
loader.lazyRequireGetter(this, "WorkersTab", "./components/workers-tab", true);
const Strings = Services.strings.createBundle(
"chrome://devtools/locale/aboutdebugging.properties");
@ -38,13 +37,13 @@ exports.AboutDebuggingApp = React.createClass({
},
componentDidMount() {
this.props.window.addEventListener("hashchange", this.onHashChange);
window.addEventListener("hashchange", this.onHashChange);
this.onHashChange();
this.props.telemetry.toolOpened("aboutdebugging");
},
componentWillUnmount() {
this.props.window.removeEventListener("hashchange", this.onHashChange);
window.removeEventListener("hashchange", this.onHashChange);
this.props.telemetry.toolClosed("aboutdebugging");
this.props.telemetry.destroy();
},
@ -65,7 +64,7 @@ exports.AboutDebuggingApp = React.createClass({
},
onHashChange() {
let tabId = this.props.window.location.hash.substr(1);
let tabId = window.location.hash.substr(1);
let isValid = tabs.some(t => t.id == tabId);
if (isValid) {
@ -78,7 +77,6 @@ exports.AboutDebuggingApp = React.createClass({
},
selectTab(tabId) {
let win = this.props.window;
win.location.hash = "#" + tabId;
window.location.hash = "#" + tabId;
}
});

View File

@ -2,18 +2,18 @@
* 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/. */
/* global React */
/* eslint-env browser */
"use strict";
loader.lazyRequireGetter(this, "Ci", "chrome", true);
loader.lazyRequireGetter(this, "Cc", "chrome", true);
loader.lazyRequireGetter(this, "React", "devtools/client/shared/vendor/react");
loader.lazyRequireGetter(this, "Services");
loader.lazyImporter(this, "AddonManager",
"resource://gre/modules/AddonManager.jsm");
const { Cc, Ci } = require("chrome");
const Services = require("Services");
const React = require("devtools/client/shared/vendor/react");
const Strings = Services.strings.createBundle(
"chrome://devtools/locale/aboutdebugging.properties");
@ -58,11 +58,9 @@ exports.AddonsControls = React.createClass({
Services.prefs.setBoolPref("devtools.debugger.remote-enabled", enabled);
},
loadAddonFromFile(event) {
let win = event.target.ownerDocument.defaultView;
loadAddonFromFile() {
let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
fp.init(win,
fp.init(window,
Strings.GetStringFromName("selectAddonFromFile"),
Ci.nsIFilePicker.modeOpen);
let res = fp.show();
@ -78,7 +76,7 @@ exports.AddonsControls = React.createClass({
try {
AddonManager.installTemporaryAddon(file);
} catch (e) {
win.alert("Error while installing the addon:\n" + e.message + "\n");
window.alert("Error while installing the addon:\n" + e.message + "\n");
throw e;
}
},

View File

@ -2,22 +2,17 @@
* 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/. */
/* global AddonManager, React */
/* global React */
"use strict";
loader.lazyRequireGetter(this, "React",
"devtools/client/shared/vendor/react");
loader.lazyRequireGetter(this, "TargetList",
"devtools/client/aboutdebugging/components/target-list", true);
loader.lazyRequireGetter(this, "TabHeader",
"devtools/client/aboutdebugging/components/tab-header", true);
loader.lazyRequireGetter(this, "AddonsControls",
"devtools/client/aboutdebugging/components/addons-controls", true);
loader.lazyRequireGetter(this, "Services");
const { AddonManager } = require("resource://gre/modules/AddonManager.jsm");
const Services = require("Services");
loader.lazyImporter(this, "AddonManager",
"resource://gre/modules/AddonManager.jsm");
const React = require("devtools/client/shared/vendor/react");
const { AddonsControls } = require("./addons-controls");
const { TabHeader } = require("./tab-header");
const { TargetList } = require("./target-list");
const ExtensionIcon = "chrome://mozapps/skin/extensions/extensionGeneric.svg";
const Strings = Services.strings.createBundle(

View File

@ -2,12 +2,9 @@
* 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/. */
/* global React */
"use strict";
loader.lazyRequireGetter(this, "React",
"devtools/client/shared/vendor/react");
const React = require("devtools/client/shared/vendor/react");
exports.TabHeader = React.createClass({
displayName: "TabHeader",

View File

@ -2,12 +2,9 @@
* 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/. */
/* global React */
"use strict";
loader.lazyRequireGetter(this, "React",
"devtools/client/shared/vendor/react");
const React = require("devtools/client/shared/vendor/react");
exports.TabMenuEntry = React.createClass({
displayName: "TabMenuEntry",

View File

@ -6,10 +6,8 @@
"use strict";
loader.lazyRequireGetter(this, "React",
"devtools/client/shared/vendor/react");
loader.lazyRequireGetter(this, "TabMenuEntry",
"devtools/client/aboutdebugging/components/tab-menu-entry", true);
const React = require("devtools/client/shared/vendor/react");
const { TabMenuEntry } = require("./tab-menu-entry");
exports.TabMenu = React.createClass({
displayName: "TabMenu",

View File

@ -6,14 +6,14 @@
"use strict";
loader.lazyRequireGetter(this, "React",
"devtools/client/shared/vendor/react");
loader.lazyRequireGetter(this, "Target",
"devtools/client/aboutdebugging/components/target", true);
loader.lazyRequireGetter(this, "Services");
const Services = require("Services");
const React = require("devtools/client/shared/vendor/react");
const { Target } = require("./target");
const Strings = Services.strings.createBundle(
"chrome://devtools/locale/aboutdebugging.properties");
const LocaleCompare = (a, b) => {
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
};

View File

@ -2,23 +2,22 @@
* 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/. */
/* global alert, BrowserToolboxProcess, gDevTools, React, TargetFactory,
Toolbox */
/* eslint-env browser */
"use strict";
loader.lazyRequireGetter(this, "React",
"devtools/client/shared/vendor/react");
loader.lazyRequireGetter(this, "TargetFactory",
"devtools/client/framework/target", true);
"devtools/client/framework/target", true);
loader.lazyRequireGetter(this, "gDevTools",
"devtools/client/framework/devtools", true);
loader.lazyRequireGetter(this, "Toolbox",
"devtools/client/framework/toolbox", true);
loader.lazyRequireGetter(this, "Services");
"devtools/client/framework/toolbox", true);
loader.lazyImporter(this, "BrowserToolboxProcess",
"resource://devtools/client/framework/ToolboxProcess.jsm");
loader.lazyRequireGetter(this, "gDevTools",
"devtools/client/framework/devtools", true);
"resource://devtools/client/framework/ToolboxProcess.jsm");
const Services = require("Services");
const React = require("devtools/client/shared/vendor/react");
const Strings = Services.strings.createBundle(
"chrome://devtools/locale/aboutdebugging.properties");
@ -26,47 +25,60 @@ const Strings = Services.strings.createBundle(
exports.Target = React.createClass({
displayName: "Target",
debug() {
let { client, target } = this.props;
switch (target.type) {
case "extension":
BrowserToolboxProcess.init({ addonID: target.addonID });
break;
case "serviceworker":
// Fall through.
case "sharedworker":
// Fall through.
case "worker":
let workerActor = this.props.target.actorID;
client.attachWorker(workerActor, (response, workerClient) => {
gDevTools.showToolbox(TargetFactory.forWorker(workerClient),
"jsdebugger", Toolbox.HostType.WINDOW)
.then(toolbox => {
toolbox.once("destroy", () => workerClient.detach());
});
});
break;
default:
alert("Not implemented yet!");
}
},
render() {
let { target, debugDisabled } = this.props;
let isServiceWorker = (target.type === "serviceworker");
let isRunning = (!isServiceWorker || target.workerActor);
return React.createElement("div", { className: "target" },
React.createElement("img", {
className: "target-icon",
role: "presentation",
src: target.icon }),
React.createElement("div", { className: "target-details" },
React.createElement("div", { className: "target-name" }, target.name),
React.createElement("div", { className: "target-url" }, target.url)
React.createElement("div", { className: "target-name" }, target.name)
),
React.createElement("button", {
className: "debug-button",
onClick: this.debug,
disabled: debugDisabled,
}, Strings.GetStringFromName("debug"))
(isRunning ?
React.createElement("button", {
className: "debug-button",
onClick: this.debug,
disabled: debugDisabled,
}, Strings.GetStringFromName("debug")) :
null
)
);
},
debug() {
let { target } = this.props;
switch (target.type) {
case "extension":
BrowserToolboxProcess.init({ addonID: target.addonID });
break;
case "serviceworker":
if (target.workerActor) {
this.openWorkerToolbox(target.workerActor);
}
break;
case "sharedworker":
this.openWorkerToolbox(target.workerActor);
break;
case "worker":
this.openWorkerToolbox(target.workerActor);
break;
default:
window.alert("Not implemented yet!");
break;
}
},
openWorkerToolbox(workerActor) {
let { client } = this.props;
client.attachWorker(workerActor, (response, workerClient) => {
gDevTools.showToolbox(TargetFactory.forWorker(workerClient),
"jsdebugger", Toolbox.HostType.WINDOW)
.then(toolbox => {
toolbox.once("destroy", () => workerClient.detach());
});
});
},
});

View File

@ -2,21 +2,15 @@
* 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/. */
/* global React */
"use strict";
loader.lazyRequireGetter(this, "Ci",
"chrome", true);
loader.lazyRequireGetter(this, "React",
"devtools/client/shared/vendor/react");
loader.lazyRequireGetter(this, "TargetList",
"devtools/client/aboutdebugging/components/target-list", true);
loader.lazyRequireGetter(this, "TabHeader",
"devtools/client/aboutdebugging/components/tab-header", true);
loader.lazyRequireGetter(this, "Services");
const { Ci } = require("chrome");
const { Task } = require("resource://gre/modules/Task.jsm");
const Services = require("Services");
loader.lazyImporter(this, "Task", "resource://gre/modules/Task.jsm");
const React = require("devtools/client/shared/vendor/react");
const { TargetList } = require("./target-list");
const { TabHeader } = require("./tab-header");
const Strings = Services.strings.createBundle(
"chrome://devtools/locale/aboutdebugging.properties");
@ -38,6 +32,7 @@ exports.WorkersTab = React.createClass({
componentDidMount() {
let client = this.props.client;
client.addListener("workerListChanged", this.update);
client.addListener("serviceWorkerRegistrationListChanged", this.update);
client.addListener("processListChanged", this.update);
this.update();
},
@ -45,6 +40,7 @@ exports.WorkersTab = React.createClass({
componentWillUnmount() {
let client = this.props.client;
client.removeListener("processListChanged", this.update);
client.removeListener("serviceWorkerRegistrationListChanged", this.update);
client.removeListener("workerListChanged", this.update);
},
@ -77,52 +73,90 @@ exports.WorkersTab = React.createClass({
update() {
let workers = this.getInitialState().workers;
this.getWorkerForms().then(forms => {
forms.forEach(form => {
let worker = {
name: form.url,
forms.registrations.forEach(form => {
workers.service.push({
type: "serviceworker",
icon: WorkerIcon,
actorID: form.actor
name: form.url,
url: form.url,
scope: form.scope,
registrationActor: form.actor
});
});
forms.workers.forEach(form => {
let worker = {
type: "worker",
icon: WorkerIcon,
name: form.url,
url: form.url,
workerActor: form.actor
};
switch (form.type) {
case Ci.nsIWorkerDebugger.TYPE_SERVICE:
worker.type = "serviceworker";
workers.service.push(worker);
for (let registration of workers.service) {
if (registration.scope === form.scope) {
// XXX: Race, sometimes a ServiceWorkerRegistrationInfo doesn't
// have a scriptSpec, but its associated WorkerDebugger does.
if (!registration.url) {
registration.name = registration.url = form.url;
}
registration.workerActor = form.actor;
break;
}
}
break;
case Ci.nsIWorkerDebugger.TYPE_SHARED:
worker.type = "sharedworker";
workers.shared.push(worker);
break;
default:
worker.type = "worker";
workers.other.push(worker);
}
});
// XXX: Filter out the service worker registrations for which we couldn't
// find the scriptSpec.
workers.service = workers.service.filter(reg => !!reg.url);
this.setState({ workers });
});
},
getWorkerForms: Task.async(function*() {
let client = this.props.client;
let registrations = [];
let workers = [];
// List workers from the Parent process
let result = yield client.mainRoot.listWorkers();
let forms = result.workers;
try {
// List service worker registrations
({ registrations } =
yield client.mainRoot.listServiceWorkerRegistrations());
// And then from the Child processes
let { processes } = yield client.mainRoot.listProcesses();
for (let process of processes) {
// Ignore parent process
if (process.parent) {
continue;
// List workers from the Parent process
({ workers } = yield client.mainRoot.listWorkers());
// And then from the Child processes
let { processes } = yield client.mainRoot.listProcesses();
for (let process of processes) {
// Ignore parent process
if (process.parent) {
continue;
}
let { form } = yield client.getProcess(process.id);
let processActor = form.actor;
let response = yield client.request({
to: processActor,
type: "listWorkers"
});
workers = workers.concat(response.workers);
}
let { form } = yield client.getProcess(process.id);
let processActor = form.actor;
let { workers } = yield client.request({to: processActor,
type: "listWorkers"});
forms = forms.concat(workers);
} catch (e) {
// Something went wrong, maybe our client is disconnected?
}
return forms;
return { registrations, workers };
}),
});

View File

@ -3,7 +3,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* eslint-env browser */
/* global DebuggerClient, DebuggerServer, React */
/* global DebuggerClient, DebuggerServer */
"use strict";
@ -16,8 +16,14 @@ loader.lazyRequireGetter(this, "DebuggerServer",
"devtools/server/main", true);
loader.lazyRequireGetter(this, "Telemetry",
"devtools/client/shared/telemetry");
loader.lazyRequireGetter(this, "AboutDebuggingApp",
"devtools/client/aboutdebugging/components/aboutdebugging", true);
const { BrowserLoader } = Components.utils.import(
"resource://devtools/client/shared/browser-loader.js", {});
const { require } =
BrowserLoader("resource://devtools/client/aboutdebugging/", window);
const React = require("devtools/client/shared/vendor/react");
const { AboutDebuggingApp } = require("./components/aboutdebugging");
var AboutDebugging = {
init() {
@ -31,8 +37,9 @@ var AboutDebugging = {
this.client.connect().then(() => {
let client = this.client;
let telemetry = new Telemetry();
React.render(React.createElement(AboutDebuggingApp,
{ client, telemetry, window }), document.querySelector("#body"));
let app = React.createElement(AboutDebuggingApp, { client, telemetry });
React.render(app, document.querySelector("#body"));
});
},

View File

@ -67,7 +67,8 @@ add_task(function* () {
let names = [...document.querySelectorAll("#service-workers .target-name")];
let name = names.filter(element => element.textContent === SERVICE_WORKER)[0];
ok(name, "Found the service worker in the list");
let debugBtn = name.parentNode.parentNode.querySelector("button");
let targetElement = name.parentNode.parentNode;
let debugBtn = targetElement.querySelector(".debug-button");
ok(debugBtn, "Found its debug button");
// Click on it and wait for the toolbox to be ready
@ -88,16 +89,18 @@ add_task(function* () {
});
assertHasWorker(true, document, "service-workers", SERVICE_WORKER);
ok(targetElement.querySelector(".debug-button"),
"The debug button is still there");
yield toolbox.destroy();
toolbox = null;
// Now ensure that the worker is correctly destroyed
// after we destroy the toolbox.
// The list should update once it get destroyed.
yield waitForMutation(serviceWorkersElement, { childList: true });
assertHasWorker(false, document, "service-workers", SERVICE_WORKER);
// The DEBUG button should disappear once the worker is destroyed.
yield waitForMutation(targetElement, { childList: true });
ok(!targetElement.querySelector(".debug-button"),
"The debug button was removed when the worker was killed");
// Finally, unregister the service worker itself
// Use message manager to work with e10s
@ -124,6 +127,11 @@ add_task(function* () {
});
ok(true, "Service worker registration unregistered");
// Now ensure that the worker registration is correctly removed.
// The list should update once the registration is destroyed.
yield waitForMutation(serviceWorkersElement, { childList: true });
assertHasWorker(false, document, "service-workers", SERVICE_WORKER);
yield removeTab(swTab);
yield closeAboutDebugging(tab);
});

View File

@ -42,7 +42,6 @@ support-files =
[browser_inspector_destroy-before-ready.js]
[browser_inspector_expand-collapse.js]
[browser_inspector_gcli-inspect-command.js]
skip-if = e10s # GCLI isn't e10s compatible. See bug 1128988.
[browser_inspector_highlighter-01.js]
[browser_inspector_highlighter-02.js]
[browser_inspector_highlighter-03.js]
@ -102,8 +101,6 @@ skip-if = e10s && debug && os == 'win' # Bug 1250058 - Docshell leak on win debu
[browser_inspector_reload-01.js]
[browser_inspector_reload-02.js]
[browser_inspector_remove-iframe-during-load.js]
[browser_inspector_scrolling.js]
skip-if = e10s # Test synthesize scrolling events in content. Also, see bug 1035661.
[browser_inspector_search-01.js]
[browser_inspector_search-02.js]
[browser_inspector_search-03.js]

View File

@ -1,45 +0,0 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* 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";
// Test that highlighted nodes can be scrolled.
// TODO: This doesn't test anything useful. See b.m.o 1035661.
const IFRAME_SRC = "data:text/html;charset=utf-8," +
"<div style='height:500px; width:500px; border:1px solid gray;'>" +
"big div" +
"</div>";
const TEST_URI = "data:text/html;charset=utf-8," +
"<p>browser_inspector_scrolling.js</p>" +
"<iframe src=\"" + IFRAME_SRC + "\" />";
add_task(function* () {
let { inspector, toolbox } = yield openInspectorForURL(TEST_URI);
let iframe = getNode("iframe");
let div = getNode("div", { document: iframe.contentDocument });
let divFront = yield getNodeFrontInFrame("div", "iframe", inspector);
info("Waiting for highlighter box model to appear.");
yield toolbox.highlighter.showBoxModel(divFront);
let scrolled = once(gBrowser.selectedBrowser, "scroll");
info("Scrolling iframe.");
EventUtils.synthesizeWheel(div, 10, 10,
{ deltaY: 50.0, deltaMode: WheelEvent.DOM_DELTA_PIXEL },
iframe.contentWindow);
info("Waiting for scroll event");
yield scrolled;
let isRetina = devicePixelRatio === 2;
is(iframe.contentDocument.body.scrollTop,
isRetina ? 25 : 50, "inspected iframe scrolled");
info("Hiding box model.");
yield toolbox.highlighter.hideBoxModel();
});

View File

@ -63,12 +63,18 @@ var Manager = {
*
* @param aWindow the main window.
* @param aTab the tab targeted.
* @returns {ResponsiveUI} the instance of ResponsiveUI for the current tab.
*/
runIfNeeded: function(aWindow, aTab) {
runIfNeeded: Task.async(function*(aWindow, aTab) {
let ui;
if (!this.isActiveForTab(aTab)) {
new ResponsiveUI(aWindow, aTab);
ui = new ResponsiveUI(aWindow, aTab);
yield ui.inited;
} else {
ui = this.getResponsiveUIForTab(aTab);
}
},
return ui;
}),
/**
* Returns true if responsive view is active for the provided tab.
@ -97,9 +103,7 @@ var Manager = {
handleGcliCommand: Task.async(function*(aWindow, aTab, aCommand, aArgs) {
switch (aCommand) {
case "resize to":
this.runIfNeeded(aWindow, aTab);
let ui = ActiveTabs.get(aTab);
yield ui.inited;
let ui = yield this.runIfNeeded(aWindow, aTab);
ui.setSize(aArgs.width, aArgs.height);
break;
case "resize on":

View File

@ -18,40 +18,6 @@ const BROWSER_BASED_DIRS = [
"resource://devtools/client/shared/redux"
];
function clearCache() {
Services.obs.notifyObservers(null, "startupcache-invalidate", null);
}
function hotReloadFile(window, require, loader, componentProxies, fileURI) {
dump("Hot reloading: " + fileURI + "\n");
if (fileURI.match(/\.js$/)) {
// Test for React proxy components
const proxy = componentProxies.get(fileURI);
if (proxy) {
// Remove the old module and re-require the new one; the require
// hook in the loader will take care of the rest
delete loader.modules[fileURI];
clearCache();
require(fileURI);
}
} else if (fileURI.match(/\.css$/)) {
const links = [...window.document.getElementsByTagNameNS("http://www.w3.org/1999/xhtml", "link")];
links.forEach(link => {
if (link.href.indexOf(fileURI) === 0) {
const parentNode = link.parentNode;
const newLink = window.document.createElementNS("http://www.w3.org/1999/xhtml", "link");
newLink.rel = "stylesheet";
newLink.type = "text/css";
newLink.href = fileURI + "?s=" + Math.random();
parentNode.insertBefore(newLink, link);
parentNode.removeChild(link);
}
});
}
}
/*
* Create a loader to be used in a browser environment. This evaluates
* modules in their own environment, but sets window (the normal
@ -78,6 +44,23 @@ function hotReloadFile(window, require, loader, componentProxies, fileURI) {
* - require: a function to require modules with
*/
function BrowserLoader(baseURI, window) {
const browserLoaderBuilder = new BrowserLoaderBuilder(baseURI, window);
return {
loader: browserLoaderBuilder.loader,
require: browserLoaderBuilder.require
};
}
/**
* Private class used to build the Loader instance and require method returned
* by BrowserLoader(baseURI, window).
*
* @param string baseURI
* Base path to load modules from.
* @param Object window
* The window instance to evaluate modules within
*/
function BrowserLoaderBuilder(baseURI, window) {
const loaderOptions = devtools.require("@loader/options");
const dynamicPaths = {};
const componentProxies = new Map();
@ -126,6 +109,13 @@ function BrowserLoader(baseURI, window) {
define(factory) {
factory(this.require, this.exports, this.module);
},
// Allow modules to use the DevToolsLoader lazy loading helpers.
loader: {
lazyGetter: devtools.lazyGetter,
lazyImporter: devtools.lazyImporter,
lazyServiceGetter: devtools.lazyServiceGetter,
lazyRequireGetter: this.lazyRequireGetter.bind(this),
},
}
};
@ -155,29 +145,81 @@ function BrowserLoader(baseURI, window) {
}
}
const mainModule = loaders.Module(baseURI, joinURI(baseURI, "main.js"));
const mainLoader = loaders.Loader(opts);
const require = loaders.Require(mainLoader, mainModule);
this.loader = loaders.Loader(opts);
this.require = loaders.Require(this.loader, mainModule);
if (hotReloadEnabled) {
const watcher = devtools.require("devtools/client/shared/file-watcher");
function onFileChanged(_, fileURI) {
hotReloadFile(window, require, mainLoader, componentProxies, fileURI);
}
const onFileChanged = (_, fileURI) => {
this.hotReloadFile(window, componentProxies, fileURI);
};
watcher.on("file-changed", onFileChanged);
window.addEventListener("unload", () => {
watcher.off("file-changed", onFileChanged);
});
}
return {
loader: mainLoader,
require: require
};
}
BrowserLoaderBuilder.prototype = {
/**
* Define a getter property on the given object that requires the given
* module. This enables delaying importing modules until the module is
* actually used.
*
* @param Object obj
* The object to define the property on.
* @param String property
* The property name.
* @param String module
* The module path.
* @param Boolean destructure
* Pass true if the property name is a member of the module's exports.
*/
lazyRequireGetter: function(obj, property, module, destructure) {
devtools.lazyGetter(obj, property, () => {
return destructure
? this.require(module)[property]
: this.require(module || property);
});
},
clearCache: function() {
Services.obs.notifyObservers(null, "startupcache-invalidate", null);
},
hotReloadFile: function(window, componentProxies, fileURI) {
dump("Hot reloading: " + fileURI + "\n");
if (fileURI.match(/\.js$/)) {
// Test for React proxy components
const proxy = componentProxies.get(fileURI);
if (proxy) {
// Remove the old module and re-require the new one; the require
// hook in the loader will take care of the rest
delete this.loader.modules[fileURI];
this.clearCache();
this.require(fileURI);
}
} else if (fileURI.match(/\.css$/)) {
const links = [...window.document.getElementsByTagNameNS("http://www.w3.org/1999/xhtml", "link")];
links.forEach(link => {
if (link.href.indexOf(fileURI) === 0) {
const parentNode = link.parentNode;
const newLink = window.document.createElementNS("http://www.w3.org/1999/xhtml", "link");
newLink.rel = "stylesheet";
newLink.type = "text/css";
newLink.href = fileURI + "?s=" + Math.random();
parentNode.insertBefore(newLink, link);
parentNode.removeChild(link);
}
});
}
}
};
this.BrowserLoader = BrowserLoader;
this.EXPORTED_SYMBOLS = ["BrowserLoader"];

View File

@ -945,11 +945,11 @@ StyleEditorUI.prototype = {
* @param {object} options
* Object with width or/and height properties.
*/
_launchResponsiveMode: function(options = {}) {
_launchResponsiveMode: Task.async(function*(options = {}) {
let tab = this._target.tab;
let win = this._target.tab.ownerGlobal;
ResponsiveUIManager.runIfNeeded(win, tab);
yield ResponsiveUIManager.runIfNeeded(win, tab);
if (options.width && options.height) {
ResponsiveUIManager.getResponsiveUIForTab(tab).setSize(options.width,
options.height);
@ -958,7 +958,7 @@ StyleEditorUI.prototype = {
} else if (options.height) {
ResponsiveUIManager.getResponsiveUIForTab(tab).setHeight(options.height);
}
},
}),
/**
* Jump cursor to the editor for a stylesheet and line number for a rule.

View File

@ -7,25 +7,29 @@
/* Tests responsive mode links for
* @media sidebar width and height related conditions */
const {ResponsiveUIManager} =
Cu.import("resource://devtools/client/responsivedesign/responsivedesign.jsm", {});
const mgr = "resource://devtools/client/responsivedesign/responsivedesign.jsm";
const {ResponsiveUIManager} = Cu.import(mgr, {});
const TESTCASE_URI = TEST_BASE_HTTPS + "media-rules.html";
waitForExplicitFinish();
const responsiveModeToggleClass = ".media-responsive-mode-toggle";
add_task(function*() {
let {ui} = yield openStyleEditorForURL(TESTCASE_URI);
let mediaEditor = ui.editors[1];
yield openEditor(mediaEditor);
let editor = ui.editors[1];
yield openEditor(editor);
yield testLinkifiedConditions(mediaEditor, gBrowser.selectedTab, ui);
let tab = gBrowser.selectedTab;
testNumberOfLinks(editor);
yield testMediaLink(editor, tab, ui, 2, "width", 400);
yield testMediaLink(editor, tab, ui, 3, "height", 200);
yield closeRDM(tab, ui);
doFinalChecks(editor);
});
function* testLinkifiedConditions(editor, tab, ui) {
function testNumberOfLinks(editor) {
let sidebar = editor.details.querySelector(".stylesheet-sidebar");
let conditions = sidebar.querySelectorAll(".media-rule-condition");
let responsiveModeToggleClass = ".media-responsive-mode-toggle";
info("Testing if media rules have the appropriate number of links");
ok(!conditions[0].querySelector(responsiveModeToggleClass),
@ -35,36 +39,69 @@ function* testLinkifiedConditions(editor, tab, ui) {
ok(conditions[2].querySelector(responsiveModeToggleClass),
"There should be 1 responsive mode link in the media rule");
is(conditions[3].querySelectorAll(responsiveModeToggleClass).length, 2,
"There should be 2 resposnive mode links in the media rule");
"There should be 2 responsive mode links in the media rule");
}
function* testMediaLink(editor, tab, ui, itemIndex, type, value) {
let sidebar = editor.details.querySelector(".stylesheet-sidebar");
let conditions = sidebar.querySelectorAll(".media-rule-condition");
let onMediaChange = once("media-list-changed", ui);
let ruiEvent = !ResponsiveUIManager.isActiveForTab(tab) ?
once("on", ResponsiveUIManager) :
once("contentResize", ResponsiveUIManager);
info("Launching responsive mode");
conditions[2].querySelector(responsiveModeToggleClass).click();
conditions[itemIndex].querySelector(responsiveModeToggleClass).click();
info("Waiting for the @media list to update");
let onMediaChange = once("media-list-changed", ui);
yield once("on", ResponsiveUIManager);
yield ruiEvent;
yield onMediaChange;
ResponsiveUIManager.getResponsiveUIForTab(tab).transitionsEnabled = false;
ok(ResponsiveUIManager.isActiveForTab(tab),
"Responsive mode should be active.");
conditions = sidebar.querySelectorAll(".media-rule-condition");
ok(!conditions[2].classList.contains("media-condition-unmatched"),
ok(!conditions[itemIndex].classList.contains("media-condition-unmatched"),
"media rule should now be matched after responsive mode is active");
let dimension = (yield getSizing())[type];
is(dimension, value, `${type} should be properly set.`);
}
function* closeRDM(tab, ui) {
info("Closing responsive mode");
ResponsiveUIManager.toggle(window, tab);
onMediaChange = once("media-list-changed", ui);
let onMediaChange = once("media-list-changed", ui);
yield once("off", ResponsiveUIManager);
yield onMediaChange;
ok(!ResponsiveUIManager.isActiveForTab(tab),
"Responsive mode should no longer be active.");
}
function doFinalChecks(editor) {
let sidebar = editor.details.querySelector(".stylesheet-sidebar");
let conditions = sidebar.querySelectorAll(".media-rule-condition");
conditions = sidebar.querySelectorAll(".media-rule-condition");
ok(conditions[2].classList.contains("media-condition-unmatched"),
"media rule should now be unmatched after responsive mode is closed");
"The width condition should now be unmatched");
ok(conditions[3].classList.contains("media-condition-unmatched"),
"The height condition should now be unmatched");
}
/* Helpers */
function* getSizing() {
let browser = gBrowser.selectedBrowser;
let sizing = yield ContentTask.spawn(browser, {}, function*() {
return {
width: content.innerWidth,
height: content.innerHeight
};
});
return sizing;
}
function once(event, target) {
let deferred = promise.defer();
target.once(event, () => {

View File

@ -219,7 +219,6 @@ skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
[browser_webconsole_bug_589162_css_filter.js]
[browser_webconsole_bug_592442_closing_brackets.js]
[browser_webconsole_bug_593003_iframe_wrong_hud.js]
skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
[browser_webconsole_bug_594497_history_arrow_keys.js]
[browser_webconsole_bug_595223_file_uri.js]
[browser_webconsole_bug_595350_multiple_windows_and_tabs.js]
@ -298,7 +297,6 @@ skip-if = e10s && os == 'win'
[browser_webconsole_change_font_size.js]
[browser_webconsole_chrome.js]
[browser_webconsole_clickable_urls.js]
skip-if = e10s # Bug 1042253 - webconsole e10s tests (Linux debug timeout)
[browser_webconsole_closure_inspection.js]
skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
[browser_webconsole_completion.js]

View File

@ -15,57 +15,54 @@ const TEST_IFRAME_URI = "http://example.com/browser/devtools/client/" +
const TEST_DUMMY_URI = "http://example.com/browser/devtools/client/" +
"webconsole/test/test-console.html";
var tab1, tab2;
function test() {
loadTab(TEST_URI).then(({tab}) => {
tab1 = tab;
add_task(function*() {
let tab1 = (yield loadTab(TEST_URI)).tab;
yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function*() {
content.console.log("FOO");
openConsole().then(() => {
tab2 = gBrowser.addTab(TEST_DUMMY_URI);
gBrowser.selectedTab = tab2;
gBrowser.selectedBrowser.addEventListener("load", tab2Loaded, true);
});
});
yield openConsole();
let tab2 = (yield loadTab(TEST_DUMMY_URI)).tab;
yield openConsole(gBrowser.selectedTab);
info("Reloading tab 1");
yield reloadTab(tab1);
info("Checking for messages");
yield checkMessages(tab1, tab2);
info("Cleaning up");
yield closeConsole(tab1);
yield closeConsole(tab2);
});
function* reloadTab(tab) {
let loaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
tab.linkedBrowser.reload();
yield loaded;
}
function tab2Loaded(aEvent) {
tab2.linkedBrowser.removeEventListener(aEvent.type, tab2Loaded, true);
openConsole(gBrowser.selectedTab).then(() => {
tab1.linkedBrowser.addEventListener("load", tab1Reloaded, true);
tab1.linkedBrowser.contentWindow.location.reload();
});
}
function tab1Reloaded(aEvent) {
tab1.linkedBrowser.removeEventListener(aEvent.type, tab1Reloaded, true);
let hud1 = HUDService.getHudByWindow(tab1.linkedBrowser.contentWindow);
function* checkMessages(tab1, tab2) {
let hud1 = yield openConsole(tab1);
let outputNode1 = hud1.outputNode;
waitForMessages({
info("Waiting for messages");
yield waitForMessages({
webconsole: hud1,
messages: [{
text: TEST_IFRAME_URI,
category: CATEGORY_NETWORK,
severity: SEVERITY_LOG,
}],
}).then(() => {
let hud2 = HUDService.getHudByWindow(tab2.linkedBrowser.contentWindow);
let outputNode2 = hud2.outputNode;
isnot(outputNode1, outputNode2,
"the two HUD outputNodes must be different");
let msg = "Didn't find the iframe network request in tab2";
testLogEntry(outputNode2, TEST_IFRAME_URI, msg, true, true);
closeConsole(tab2).then(() => {
gBrowser.removeTab(tab2);
tab1 = tab2 = null;
executeSoon(finishTest);
});
}]
});
let hud2 = yield openConsole(tab2);
let outputNode2 = hud2.outputNode;
isnot(outputNode1, outputNode2,
"the two HUD outputNodes must be different");
let msg = "Didn't find the iframe network request in tab2";
testLogEntry(outputNode2, TEST_IFRAME_URI, msg, true, true);
}

View File

@ -8,9 +8,7 @@
"use strict";
const TEST_URI = "data:text/html;charset=utf8,Bug 1005909 - Clickable URLS";
var inputTests = [
const inputTests = [
// 0: URL opens page when clicked.
{
@ -96,11 +94,10 @@ var inputTests = [
];
function test() {
Task.spawn(function*() {
let {tab} = yield loadTab(TEST_URI);
let hud = yield openConsole(tab);
yield checkOutputForInputs(hud, inputTests);
inputTests = null;
}).then(finishTest);
}
const url = "data:text/html;charset=utf8,Bug 1005909 - Clickable URLS";
add_task(function* () {
yield BrowserTestUtils.openNewForegroundTab(gBrowser, url);
let hud = yield openConsole();
yield checkOutputForInputs(hud, inputTests);
});

View File

@ -95,7 +95,7 @@ ChildProcessActor.prototype = {
onListWorkers: function () {
if (!this._workerList) {
this._workerList = new WorkerActorList({});
this._workerList = new WorkerActorList(this.conn, {});
}
return this._workerList.getList().then(actors => {
let pool = new ActorPool(this.conn);

View File

@ -131,8 +131,9 @@ function createRootActor(aConnection)
{
tabList: new BrowserTabList(aConnection),
addonList: new BrowserAddonList(aConnection),
workerList: new WorkerActorList({}),
serviceWorkerRegistrationList: new ServiceWorkerRegistrationActorList(),
workerList: new WorkerActorList(aConnection, {}),
serviceWorkerRegistrationList:
new ServiceWorkerRegistrationActorList(aConnection),
processList: new ProcessActorList(),
globalActorFactories: DebuggerServer.globalActorFactories,
onShutdown: sendShutdownEvent
@ -1098,7 +1099,7 @@ TabActor.prototype = {
}
if (this._workerActorList === null) {
this._workerActorList = new WorkerActorList({
this._workerActorList = new WorkerActorList(this.conn, {
type: Ci.nsIWorkerDebugger.TYPE_DEDICATED,
window: this.window
});

View File

@ -2,6 +2,8 @@
var { Ci, Cu } = require("chrome");
var { DebuggerServer } = require("devtools/server/main");
const protocol = require("devtools/server/protocol");
const { Arg, method, RetVal } = protocol;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
@ -35,31 +37,41 @@ function matchWorkerDebugger(dbg, options) {
return true;
}
function WorkerActor(dbg) {
this._dbg = dbg;
this._isAttached = false;
this._threadActor = null;
this._transport = null;
}
let WorkerActor = protocol.ActorClass({
typeName: "worker",
WorkerActor.prototype = {
actorPrefix: "worker",
initialize: function (conn, dbg) {
protocol.Actor.prototype.initialize.call(this, conn);
this._dbg = dbg;
this._attached = false;
this._threadActor = null;
this._transport = null;
this.manage(this);
},
form: function () {
return {
form: function (detail) {
if (detail === "actorid") {
return this.actorID;
}
let form = {
actor: this.actorID,
consoleActor: this._consoleActor,
url: this._dbg.url,
type: this._dbg.type
};
if (this._dbg.type === Ci.nsIWorkerDebugger.TYPE_SERVICE) {
let registration = this._getServiceWorkerRegistrationInfo();
form.scope = registration.scope;
}
return form;
},
onAttach: function () {
attach: method(function () {
if (this._dbg.isClosed) {
return { error: "closed" };
}
if (!this._isAttached) {
if (!this._attached) {
// Automatically disable their internal timeout that shut them down
// Should be refactored by having actors specific to service workers
if (this._dbg.type == Ci.nsIWorkerDebugger.TYPE_SERVICE) {
@ -69,27 +81,33 @@ WorkerActor.prototype = {
}
}
this._dbg.addListener(this);
this._isAttached = true;
this._attached = true;
}
return {
type: "attached",
url: this._dbg.url
};
},
}, {
request: {},
response: RetVal("json")
}),
onDetach: function () {
if (!this._isAttached) {
detach: method(function () {
if (!this._attached) {
return { error: "wrongState" };
}
this._detach();
return { type: "detached" };
},
}, {
request: {},
response: RetVal("json")
}),
onConnect: function (request) {
if (!this._isAttached) {
connect: method(function (options) {
if (!this._attached) {
return { error: "wrongState" };
}
@ -101,7 +119,7 @@ WorkerActor.prototype = {
}
return DebuggerServer.connectToWorker(
this.conn, this._dbg, this.actorID, request.options
this.conn, this._dbg, this.actorID, options
).then(({ threadActor, transport, consoleActor }) => {
this._threadActor = threadActor;
this._transport = transport;
@ -115,10 +133,15 @@ WorkerActor.prototype = {
}, (error) => {
return { error: error.toString() };
});
},
}, {
request: {
options: Arg(0, "json"),
},
response: RetVal("json")
}),
onClose: function () {
if (this._isAttached) {
if (this._attached) {
this._detach();
}
@ -129,9 +152,13 @@ WorkerActor.prototype = {
reportError("ERROR:" + filename + ":" + lineno + ":" + message + "\n");
},
_getServiceWorkerRegistrationInfo() {
return swm.getRegistrationByPrincipal(this._dbg.principal, this._dbg.url);
},
_getServiceWorkerInfo: function () {
let info = swm.getRegistrationByPrincipal(this._dbg.principal, this._dbg.url);
return info.getWorkerByID(this._dbg.serviceWorkerID);
let registration = this._getServiceWorkerRegistrationInfo();
return registration.getWorkerByID(this._dbg.serviceWorkerID);
},
_detach: function () {
@ -156,19 +183,14 @@ WorkerActor.prototype = {
}
this._dbg.removeListener(this);
this._isAttached = false;
this._attached = false;
}
};
WorkerActor.prototype.requestTypes = {
"attach": WorkerActor.prototype.onAttach,
"detach": WorkerActor.prototype.onDetach,
"connect": WorkerActor.prototype.onConnect
};
});
exports.WorkerActor = WorkerActor;
function WorkerActorList(options) {
function WorkerActorList(conn, options) {
this._conn = conn;
this._options = options;
this._actors = new Map();
this._onListChanged = null;
@ -199,7 +221,7 @@ WorkerActorList.prototype = {
// Create an actor for each debugger for which we don't have one.
for (let dbg of dbgs) {
if (!this._actors.has(dbg)) {
this._actors.set(dbg, new WorkerActor(dbg));
this._actors.set(dbg, new WorkerActor(this._conn, dbg));
}
}
@ -265,22 +287,30 @@ WorkerActorList.prototype = {
exports.WorkerActorList = WorkerActorList;
function ServiceWorkerRegistrationActor(registration) {
this._registration = registration;
};
let ServiceWorkerRegistrationActor = protocol.ActorClass({
typeName: "serviceWorkerRegistration",
ServiceWorkerRegistrationActor.prototype = {
actorPrefix: "serviceWorkerRegistration",
initialize: function(conn, registration) {
protocol.Actor.prototype.initialize.call(this, conn);
this._registration = registration;
this.manage(this);
},
form: function () {
form: function(detail) {
if (detail === "actorid") {
return this.actorID;
}
return {
actor: this.actorID,
scope: this._registration.scope
scope: this._registration.scope,
url: this._registration.scriptSpec
};
}
};
},
function ServiceWorkerRegistrationActorList() {
});
function ServiceWorkerRegistrationActorList(conn) {
this._conn = conn;
this._actors = new Map();
this._onListChanged = null;
this._mustNotify = false;
@ -309,7 +339,7 @@ ServiceWorkerRegistrationActorList.prototype = {
for (let registration of registrations) {
if (!this._actors.has(registration)) {
this._actors.set(registration,
new ServiceWorkerRegistrationActor(registration));
new ServiceWorkerRegistrationActor(this._conn, registration));
}
}

View File

@ -1416,6 +1416,9 @@ WorkerClient.prototype = {
detach: DebuggerClient.requester({ type: "detach" }, {
after: function (aResponse) {
if (this.thread) {
this.client.unregisterClient(this.thread);
}
this.client.unregisterClient(this);
return aResponse;
},

View File

@ -384,9 +384,17 @@ GlobalManager = {
injectAPI(api, browserObj);
let schemaApi = Management.generateAPIs(extension, context, Management.schemaApis);
// Add in any extra API namespaces which do not have implementations
// outside of their schema file.
schemaApi.extensionTypes = {};
function findPath(path) {
let obj = schemaApi;
for (let elt of path) {
if (!(elt in obj)) {
return null;
}
obj = obj[elt];
}
return obj;
@ -423,6 +431,10 @@ GlobalManager = {
return context.wrapPromise(promise || Promise.resolve(), callback);
},
shouldInject(path, name) {
return findPath(path) != null;
},
getProperty(path, name) {
return findPath(path)[name];
},

View File

@ -120,6 +120,11 @@ var api = context => {
return context.extension.localizeMessage(messageName, substitutions);
},
getAcceptLanguages: function(callback) {
let result = context.extension.localeData.acceptLanguages;
return context.wrapPromise(Promise.resolve(result), callback);
},
getUILanguage: function() {
return context.extension.localeData.uiLocale;
},

View File

@ -20,6 +20,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "LanguageDetector",
"resource:///modules/translation/LanguageDetector.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Locale",
"resource://gre/modules/Locale.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
"resource://gre/modules/Preferences.jsm");
function filterStack(error) {
return String(error.stack).replace(/(^.*(Task\.jsm|Promise-backend\.js).*\n)+/gm, "<Promise Chain>\n");
@ -429,6 +431,16 @@ LocaleData.prototype = {
return result;
},
get acceptLanguages() {
let result = Preferences.get("intl.accept_languages", "", Ci.nsIPrefLocalizedString);
result = result.split(",");
result = result.map(lang => {
return lang.replace(/-/g, "_").trim();
});
return result;
},
get uiLocale() {
// Return the browser locale, but convert it to a Chrome-style
// locale code.

View File

@ -1319,7 +1319,9 @@ this.Schemas = {
for (let [namespace, ns] of this.namespaces) {
let obj = Cu.createObjectIn(dest, {defineAs: namespace});
for (let [name, entry] of ns) {
entry.inject([namespace], name, obj, new Context(wrapperFuncs));
if (wrapperFuncs.shouldInject([namespace], name)) {
entry.inject([namespace], name, obj, new Context(wrapperFuncs));
}
}
if (!Object.keys(obj).length) {

View File

@ -2,14 +2,104 @@
var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
"resource://gre/modules/Downloads.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DownloadPaths",
"resource://gre/modules/DownloadPaths.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS",
"resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
"resource://gre/modules/FileUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
const {
ignoreEvent,
} = ExtensionUtils;
let currentId = 0;
extensions.registerSchemaAPI("downloads", "downloads", (extension, context) => {
return {
downloads: {
download(options) {
if (options.filename != null) {
if (options.filename.length == 0) {
return Promise.reject({message: "filename must not be empty"});
}
let path = OS.Path.split(options.filename);
if (path.absolute) {
return Promise.reject({message: "filename must not be an absolute path"});
}
if (path.components.some(component => component == "..")) {
return Promise.reject({message: "filename must not contain back-references (..)"});
}
}
if (options.conflictAction == "prompt") {
// TODO
return Promise.reject({message: "conflictAction prompt not yet implemented"});
}
function createTarget(downloadsDir) {
// TODO
// if (options.saveAs) { }
let target;
if (options.filename) {
target = OS.Path.join(downloadsDir, options.filename);
} else {
let uri = NetUtil.newURI(options.url).QueryInterface(Ci.nsIURL);
target = OS.Path.join(downloadsDir, uri.fileName);
}
// This has a race, something else could come along and create
// the file between this test and them time the download code
// creates the target file. But we can't easily fix it without
// modifying DownloadCore so we live with it for now.
return OS.File.exists(target).then(exists => {
if (exists) {
switch (options.conflictAction) {
case "uniquify":
default:
target = DownloadPaths.createNiceUniqueFile(new FileUtils.File(target)).path;
break;
case "overwrite":
break;
}
}
return target;
});
}
let download;
return Downloads.getPreferredDownloadsDirectory()
.then(downloadsDir => createTarget(downloadsDir))
.then(target => Downloads.createDownload({
source: options.url,
target: target,
})).then(dl => {
download = dl;
return Downloads.getList(Downloads.ALL);
}).then(list => {
list.add(download);
// This is necessary to make pause/resume work.
download.tryToKeepPartialData = true;
download.start();
// Without other chrome.downloads methods, we can't actually
// do anything with the id so just return a dummy value for now.
return currentId++;
});
},
// When we do open(), check for additional downloads.open permission.
// i.e.:
// open(downloadId) {

View File

@ -12,6 +12,11 @@ extensions.registerSchemaAPI("i18n", null, (extension, context) => {
return extension.localizeMessage(messageName, substitutions);
},
getAcceptLanguages: function() {
let result = extension.localeData.acceptLanguages;
return Promise.resolve(result);
},
getUILanguage: function() {
return extension.localeData.uiLocale;
},

View File

@ -79,6 +79,7 @@ extensions.registerSchemaAPI("webNavigation", "webNavigation", (extension, conte
onCompleted: new WebNavigationEventManager(context, "onCompleted").api(),
onErrorOccurred: new WebNavigationEventManager(context, "onErrorOccurred").api(),
onReferenceFragmentUpdated: new WebNavigationEventManager(context, "onReferenceFragmentUpdated").api(),
onHistoryStateUpdated: new WebNavigationEventManager(context, "onHistoryStateUpdated").api(),
onCreatedNavigationTarget: ignoreEvent(context, "webNavigation.onCreatedNavigationTarget"),
getAllFrames(details) {
let tab = TabManager.getTab(details.tabId);

View File

@ -19,4 +19,5 @@ DIRS += ['schemas']
JAR_MANIFESTS += ['jar.mn']
MOCHITEST_MANIFESTS += ['test/mochitest/mochitest.ini']
MOCHITEST_CHROME_MANIFESTS += ['test/mochitest/chrome.ini']
XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell/xpcshell.ini']

View File

@ -22,7 +22,7 @@
"id": "FilenameConflictAction",
"type": "string",
"enum": [
"uniqify",
"uniquify",
"overwrite",
"prompt"
]
@ -214,7 +214,7 @@
{
"name": "download",
"type": "function",
"unsupported": true,
"async": "callback",
"description": "Download a URL. If the URL uses the HTTP[S] protocol, then the request will include all cookies currently set for its hostname. If both <code>filename</code> and <code>saveAs</code> are specified, then the Save As dialog will be displayed, pre-populated with the specified <code>filename</code>. If the download started successfully, <code>callback</code> will be called with the new <a href='#type-DownloadItem'>DownloadItem</a>'s <code>downloadId</code>. If there was an error starting the download, then <code>callback</code> will be called with <code>downloadId=undefined</code> and <a href='extension.html#property-lastError'>chrome.extension.lastError</a> will contain a descriptive string. The error strings are not guaranteed to remain backwards compatible between releases. You must not parse it.",
"parameters": [
{
@ -224,7 +224,8 @@
"properties": {
"url": {
"description": "The URL to download.",
"type": "string"
"type": "string",
"format": "url"
},
"filename": {
"description": "A file path relative to the Downloads directory to contain the downloaded file.",
@ -236,11 +237,13 @@
"optional": true
},
"saveAs": {
"unsupported": true,
"description": "Use a file-chooser to allow the user to select a filename.",
"optional": true,
"type": "boolean"
},
"method": {
"unsupported": true,
"description": "The HTTP method to use if the URL uses the HTTP[S] protocol.",
"enum": [
"GET",
@ -250,6 +253,7 @@
"type": "string"
},
"headers": {
"unsupported": true,
"optional": true,
"type": "array",
"description": "Extra HTTP headers to send with the request if the URL uses the HTTP[s] protocol. Each header is represented as a dictionary containing the keys <code>name</code> and either <code>value</code> or <code>binaryValue</code>, restricted to those allowed by XMLHttpRequest.",
@ -268,6 +272,7 @@
}
},
"body": {
"unsupported": true,
"description": "Post body.",
"optional": true,
"type": "string"

View File

@ -30,7 +30,6 @@
"functions": [
{
"name": "getAcceptLanguages",
"unsupported": true,
"type": "function",
"description": "Gets the accept-languages of the browser. This is different from the locale used by the browser; to get the locale, use $(ref:i18n.getUILanguage).",
"async": "callback",

View File

@ -345,7 +345,6 @@
},
{
"name": "onHistoryStateUpdated",
"unsupported": true,
"type": "function",
"description": "Fired when the frame's history was updated to a new URL. All future events for that frame will use the updated URL.",
"filters": [

View File

@ -0,0 +1,6 @@
[DEFAULT]
skip-if = os == 'android'
support-files =
file_download.txt
[test_chrome_ext_downloads_download.html]

View File

@ -0,0 +1 @@
This is a sample file used in download tests.

View File

@ -0,0 +1,221 @@
<!DOCTYPE HTML>
<html>
<head>
<title>WebExtension test</title>
<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
<script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
<script type="text/javascript" src="head.js"></script>
<link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
</head>
<body>
<script type="text/javascript">
"use strict";
const {
interfaces: Ci,
utils: Cu,
} = Components;
/* global OS */
Cu.import("resource://gre/modules/osfile.jsm");
Cu.import("resource://gre/modules/AppConstants.jsm");
Cu.import("resource://gre/modules/FileUtils.jsm");
Cu.import("resource://gre/modules/Downloads.jsm");
Cu.import("resource://gre/modules/Services.jsm");
const WINDOWS = (AppConstants.platform == "win");
const BASE = "http://mochi.test:8888/chrome/toolkit/components/extensions/test/mochitest";
const FILE_NAME = "file_download.txt";
const FILE_URL = BASE + "/" + FILE_NAME;
const FILE_NAME_UNIQUE = "file_download(1).txt";
const FILE_LEN = 46;
let downloadDir;
function setup() {
downloadDir = FileUtils.getDir("TmpD", ["downloads"]);
downloadDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
info(`Using download directory ${downloadDir.path}`);
Services.prefs.setIntPref("browser.download.folderList", 2);
Services.prefs.setComplexValue("browser.download.dir", Ci.nsIFile, downloadDir);
SimpleTest.registerCleanupFunction(() => {
Services.prefs.clearUserPref("browser.download.folderList");
Services.prefs.clearUserPref("browser.download.dir");
});
}
function backgroundScript() {
browser.test.onMessage.addListener(function(msg) {
if (msg == "download.request") {
// download() throws on bad arguments, we can remove the extra
// promise when bug 1250223 is fixed.
return Promise.resolve().then(() => browser.downloads.download(arguments[1]))
.then((id) => browser.test.sendMessage("download.done", {status: "success", id}))
.catch(error => browser.test.sendMessage("download.done", {status: "error", errmsg: error.message}));
}
});
browser.test.sendMessage("ready");
}
// This function is a bit of a sledgehammer, it looks at every download
// the browser knows about and waits for all active downloads to complete.
// But we only start one at a time and only do a handful in total, so
// this lets us test download() without depending on anything else.
function waitForDownloads() {
return Downloads.getList(Downloads.ALL)
.then(list => list.getAll())
.then(downloads => {
let inprogress = downloads.filter(dl => !dl.stopped);
return Promise.all(inprogress.map(dl => dl.whenSucceeded()));
});
}
// Create a file in the downloads directory.
function touch(filename) {
let file = downloadDir.clone();
file.append(filename);
file.create(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
}
// Remove a file in the downloads directory.
function remove(filename) {
let file = downloadDir.clone();
file.append(filename);
file.remove(false);
}
add_task(function* test_downloads() {
setup();
let extension = ExtensionTestUtils.loadExtension({
background: `(${backgroundScript})()`,
manifest: {
permissions: ["downloads"],
},
});
function download(options) {
extension.sendMessage("download.request", options);
return extension.awaitMessage("download.done");
}
function testDownload(options, localFile, expectedSize, description) {
return download(options).then(msg => {
is(msg.status, "success", `downloads.download() works with ${description}`);
return waitForDownloads();
}).then(() => {
let localPath = downloadDir.clone();
localPath.append(localFile);
is(localPath.fileSize, expectedSize, "Downloaded file has expected size");
localPath.remove(false);
});
}
yield extension.startup();
yield extension.awaitMessage("ready");
info("extension started");
// Call download() with just the url property.
yield testDownload({url: FILE_URL}, FILE_NAME, FILE_LEN, "just source");
// Call download() with a filename property.
yield testDownload({
url: FILE_URL,
filename: "newpath.txt",
}, "newpath.txt", FILE_LEN, "source and filename");
// Check conflictAction of "uniquify".
touch(FILE_NAME);
yield testDownload({
url: FILE_URL,
conflictAction: "uniquify",
}, FILE_NAME_UNIQUE, FILE_LEN, "conflictAction=uniquify");
// todo check that preexisting file was not modified?
remove(FILE_NAME);
// Check conflictAction of "overwrite".
touch(FILE_NAME);
yield testDownload({
url: FILE_URL,
conflictAction: "overwrite",
}, FILE_NAME, FILE_LEN, "conflictAction=overwrite");
// Try to download in invalid url
yield download({url: "this is not a valid URL"}).then(msg => {
is(msg.status, "error", "downloads.download() fails with invalid url");
ok(/not a valid URL/.test(msg.errmsg), "error message for invalid url is correct");
});
// Try to download to an empty path.
yield download({
url: FILE_URL,
filename: "",
}).then(msg => {
is(msg.status, "error", "downloads.download() fails with empty filename");
is(msg.errmsg, "filename must not be empty", "error message for empty filename is correct");
});
// Try to download to an absolute path.
const absolutePath = OS.Path.join(WINDOWS ? "\\tmp" : "/tmp", "file_download.txt");
yield download({
url: FILE_URL,
filename: absolutePath,
}).then(msg => {
is(msg.status, "error", "downloads.download() fails with absolute filename");
is(msg.errmsg, "filename must not be an absolute path", `error message for absolute path (${absolutePath}) is correct`);
});
if (WINDOWS) {
yield download({
url: FILE_URL,
filename: "C:\\file_download.txt",
}).then(msg => {
is(msg.status, "error", "downloads.download() fails with absolute filename");
is(msg.errmsg, "filename must not be an absolute path", "error message for absolute path with drive letter is correct");
});
}
// Try to download to a relative path containing ..
yield download({
url: FILE_URL,
filename: OS.Path.join("..", "file_download.txt"),
}).then(msg => {
is(msg.status, "error", "downloads.download() fails with back-references");
is(msg.errmsg, "filename must not contain back-references (..)", "error message for back-references is correct");
});
// Try to download to a long relative path containing ..
yield download({
url: FILE_URL,
filename: OS.Path.join("foo", "..", "..", "file_download.txt"),
}).then(msg => {
is(msg.status, "error", "downloads.download() fails with back-references");
is(msg.errmsg, "filename must not contain back-references (..)", "error message for back-references is correct");
});
yield extension.unload();
});
// check for leftover files in the download directory
add_task(function*() {
let entries = downloadDir.directoryEntries;
while (entries.hasMoreElements()) {
let entry = entries.getNext().QueryInterface(Ci.nsIFile);
ok(false, `Leftover file ${entry.path} in download directory`);
entry.remove(false);
}
downloadDir.remove(false);
});
</script>
</body>
</html>

View File

@ -15,10 +15,8 @@
add_task(function* test_alarm_without_permissions() {
function backgroundScript() {
browser.test.log("running alarm script");
browser.test.assertTrue(!browser.alarms,
"alarm API should not be available if the alarm permission is not required");
"alarm API is not available when the alarm permission is not required");
browser.test.notifyPass("alarms_permission");
}
@ -30,25 +28,22 @@ add_task(function* test_alarm_without_permissions() {
});
yield extension.startup();
info("extension loaded");
yield extension.awaitFinish("alarms_permission");
yield extension.unload();
info("extension unloaded");
});
add_task(function* test_alarm_fires() {
function backgroundScript() {
let ALARM_NAME = "test_ext_alarms";
browser.test.log("running alarm script");
chrome.alarms.onAlarm.addListener(function(alarm) {
browser.test.assertEq(alarm.name, ALARM_NAME, "alarm should have the correct name");
browser.alarms.onAlarm.addListener(alarm => {
browser.test.assertEq(alarm.name, ALARM_NAME, "alarm has the correct name");
browser.test.notifyPass("alarms");
});
chrome.alarms.create(ALARM_NAME, {delayInMinutes: 0.02});
browser.alarms.create(ALARM_NAME, {delayInMinutes: 0.02});
setTimeout(() => {
browser.test.notifyFail("alarms test failed, took too long");
browser.test.fail("alarm fired within expected time");
}, 10000);
}
@ -60,33 +55,146 @@ add_task(function* test_alarm_fires() {
});
yield extension.startup();
info("extension loaded");
yield extension.awaitFinish("alarms");
yield extension.unload();
info("extension unloaded");
});
add_task(function* test_cleared_alarm_does_not_fire() {
function backgroundScript() {
let ALARM_NAME = "test_ext_alarms";
browser.alarms.onAlarm.addListener(alarm => {
browser.test.fail("cleared alarm does not fire");
});
browser.alarms.create(ALARM_NAME, {when: Date.now() + 1000});
browser.alarms.clear(ALARM_NAME, wasCleared => {
browser.test.assertTrue(wasCleared, "alarm was cleared");
setTimeout(() => {
browser.test.notifyPass("alarms");
}, 2000);
});
}
let extension = ExtensionTestUtils.loadExtension({
background: `(${backgroundScript})()`,
manifest: {
permissions: ["alarms"],
},
});
yield extension.startup();
yield extension.awaitFinish("alarms");
yield extension.unload();
});
add_task(function* test_alarm_fires_with_when() {
function backgroundScript() {
let ALARM_NAME = "test_ext_alarms";
browser.alarms.onAlarm.addListener(alarm => {
browser.test.assertEq(alarm.name, ALARM_NAME, "alarm has the expected name");
browser.test.notifyPass("alarms");
});
browser.alarms.create(ALARM_NAME, {when: Date.now() + 1000});
setTimeout(() => {
browser.test.fail("alarm fired within expected time");
browser.alarms.clear(ALARM_NAME, (wasCleared) => {
browser.test.assertTrue(wasCleared, "alarm was cleared");
});
}, 10000);
}
let extension = ExtensionTestUtils.loadExtension({
background: `(${backgroundScript})()`,
manifest: {
permissions: ["alarms"],
},
});
yield extension.startup();
yield extension.awaitFinish("alarms");
yield extension.unload();
});
add_task(function* test_alarm_clear_non_matching_name() {
function backgroundScript() {
let ALARM_NAME = "test_ext_alarms";
browser.alarms.create(ALARM_NAME, {when: Date.now() + 2000});
browser.alarms.clear(ALARM_NAME + "1", wasCleared => {
browser.test.assertFalse(wasCleared, "alarm was not cleared");
browser.alarms.getAll(alarms => {
browser.test.assertEq(1, alarms.length, "alarm was not removed");
browser.test.notifyPass("alarms");
});
});
}
let extension = ExtensionTestUtils.loadExtension({
background: `(${backgroundScript})()`,
manifest: {
permissions: ["alarms"],
},
});
yield extension.startup();
yield extension.awaitFinish("alarms");
yield extension.unload();
});
add_task(function* test_alarm_get_and_clear_single_argument() {
function backgroundScript() {
browser.alarms.create({when: Date.now() + 2000});
browser.alarms.get(alarm => {
browser.test.assertEq("", alarm.name, "expected alarm returned");
browser.alarms.clear(wasCleared => {
browser.test.assertTrue(wasCleared, "alarm was cleared");
browser.alarms.getAll(alarms => {
browser.test.assertEq(0, alarms.length, "alarm was removed");
browser.test.notifyPass("alarms");
});
});
});
}
let extension = ExtensionTestUtils.loadExtension({
background: `(${backgroundScript})()`,
manifest: {
permissions: ["alarms"],
},
});
yield extension.startup();
yield extension.awaitFinish("alarms");
yield extension.unload();
});
add_task(function* test_periodic_alarm_fires() {
function backgroundScript() {
const ALARM_NAME = "test_ext_alarms";
browser.test.log("running alarm script");
let count = 0;
chrome.alarms.onAlarm.addListener(function(alarm) {
browser.test.assertEq(alarm.name, ALARM_NAME, "alarm should have the correct name");
browser.alarms.onAlarm.addListener(alarm => {
browser.test.assertEq(alarm.name, ALARM_NAME, "alarm has the expected name");
if (count++ === 3) {
chrome.alarms.clear(ALARM_NAME, (wasCleared) => {
browser.test.assertTrue(wasCleared, "alarm should be cleared");
browser.alarms.clear(ALARM_NAME, (wasCleared) => {
browser.test.assertTrue(wasCleared, "alarm was cleared");
browser.test.notifyPass("alarms");
});
}
});
chrome.alarms.create(ALARM_NAME, {periodInMinutes: 0.02});
browser.alarms.create(ALARM_NAME, {periodInMinutes: 0.02});
setTimeout(() => {
browser.test.notifyFail("alarms test failed, took too long");
chrome.alarms.clear(ALARM_NAME, (wasCleared) => {
browser.test.assertTrue(wasCleared, "alarm should be cleared");
browser.test.notify("alarm fired within expected time");
browser.alarms.clear(ALARM_NAME, (wasCleared) => {
browser.test.assertTrue(wasCleared, "alarm was cleared");
});
}, 30000);
}
@ -99,10 +207,8 @@ add_task(function* test_periodic_alarm_fires() {
});
yield extension.startup();
info("extension loaded");
yield extension.awaitFinish("alarms");
yield extension.unload();
info("extension unloaded");
});
@ -129,13 +235,13 @@ add_task(function* test_get_get_all_clear_all_alarms() {
promiseAlarms.getAll().then(alarms => {
browser.test.assertEq(suffixes.length, alarms.length);
alarms.forEach((alarm, index) => {
browser.test.assertEq(ALARM_NAME + index, alarm.name, "expected alarm returned");
browser.test.assertEq(ALARM_NAME + index, alarm.name, "alarm has the expected name");
});
return Promise.all(
suffixes.map(suffix => {
return promiseAlarms.get(ALARM_NAME + suffix).then(alarm => {
browser.test.assertEq(ALARM_NAME + suffix, alarm.name, "expected alarm returned");
browser.test.assertEq(ALARM_NAME + suffix, alarm.name, "alarm has the expected name");
browser.test.sendMessage(`get-${suffix}`);
});
}));
@ -150,7 +256,7 @@ add_task(function* test_get_get_all_clear_all_alarms() {
return promiseAlarms.get(ALARM_NAME + suffixes[0]);
}).then(alarm => {
browser.test.assertEq(undefined, alarm, "non-existent alarm should be undefined");
browser.test.assertEq(undefined, alarm, "non-existent alarm is undefined");
browser.test.sendMessage(`get-invalid`);
return promiseAlarms.clearAll();

View File

@ -14,6 +14,7 @@
<script type="text/javascript">
"use strict";
SimpleTest.registerCleanupFunction(() => { SpecialPowers.clearUserPref("intl.accept_languages"); });
SimpleTest.registerCleanupFunction(() => { SpecialPowers.clearUserPref("general.useragent.locale"); });
add_task(function* test_i18n() {
@ -163,6 +164,85 @@ add_task(function* test_i18n() {
yield extension.unload();
});
add_task(function* test_get_accept_languages() {
function background() {
function checkResults(source, results, expected) {
browser.test.assertEq(
expected.length,
results.length,
`got expected number of languages in ${source}`);
results.forEach((lang, index) => {
browser.test.assertEq(
expected[index],
lang,
`got expected language in ${source}`);
});
}
let tabId;
browser.tabs.query({currentWindow: true, active: true}, tabs => {
tabId = tabs[0].id;
browser.test.sendMessage("ready");
});
browser.test.onMessage.addListener(([msg, expected]) => {
Promise.all([
new Promise(
resolve => browser.tabs.sendMessage(tabId, "get-results", resolve)),
browser.i18n.getAcceptLanguages(),
]).then(([contentResults, backgroundResults]) => {
checkResults("contentScript", contentResults, expected);
checkResults("background", backgroundResults, expected);
browser.test.sendMessage("done");
});
});
}
function content() {
browser.runtime.onMessage.addListener((msg, sender, respond) => {
browser.i18n.getAcceptLanguages(respond);
return true;
});
}
let extension = ExtensionTestUtils.loadExtension({
manifest: {
"content_scripts": [{
"matches": ["http://mochi.test/*/file_sample.html"],
"run_at": "document_start",
"js": ["content_script.js"],
}],
},
background: `(${background})()`,
files: {
"content_script.js": `(${content})()`,
},
});
let win = window.open("file_sample.html");
yield extension.startup();
yield extension.awaitMessage("ready");
let expectedLangs = ["en_US", "en"];
extension.sendMessage(["expect-results", expectedLangs]);
yield extension.awaitMessage("done");
expectedLangs = ["en_US", "en", "fr_CA", "fr"];
SpecialPowers.setCharPref("intl.accept_languages", expectedLangs.toString());
extension.sendMessage(["expect-results", expectedLangs]);
yield extension.awaitMessage("done");
SpecialPowers.clearUserPref("intl.accept_languages");
win.close();
yield extension.unload();
});
add_task(function* test_get_ui_language() {
function getResults() {
return {

View File

@ -16,18 +16,20 @@
add_task(function* testEmptySchema() {
function background() {
browser.test.assertTrue(!("manifest" in browser), "browser.manifest is not defined");
browser.test.assertTrue("storage" in browser, "browser.storage should be defined");
browser.test.assertTrue(!("contextMenus" in browser), "browser.contextMenus should not be defined");
browser.test.notifyPass("schema");
}
let extension = ExtensionTestUtils.loadExtension({
background: `(${background})()`,
manifest: {
permissions: ["storage"],
},
});
yield extension.startup();
yield extension.awaitFinish("schema");
yield extension.unload();
});

View File

@ -24,6 +24,7 @@ function backgroundScript() {
"onCompleted",
"onErrorOccurred",
"onReferenceFragmentUpdated",
"onHistoryStateUpdated",
];
let expectedTabId = -1;
@ -61,13 +62,14 @@ function backgroundScript() {
browser.webNavigation[event].addListener(listeners[event]);
}
browser.test.sendMessage("ready", browser.webRequest.ResourceType);
browser.test.sendMessage("ready");
}
const BASE = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest";
const URL = BASE + "/file_WebNavigation_page1.html";
const FRAME = BASE + "/file_WebNavigation_page2.html";
const FRAME2 = BASE + "/file_WebNavigation_page3.html";
const FRAME_PUSHSTATE = BASE + "/file_WebNavigation_page3_pushState.html";
const REQUIRED = [
"onBeforeNavigate",
@ -155,10 +157,71 @@ add_task(function* webnav_ordering() {
checkRequired(FRAME2);
yield loadAndWait(win, "onReferenceFragmentUpdated", FRAME2 + "#ref",
() => { win.frames[0].document.getElementById("elt").click(); });
let navigationSequence = [
{
action: () => { win.frames[0].document.getElementById("elt").click(); },
waitURL: `${FRAME2}#ref`,
expectedEvent: "onReferenceFragmentUpdated",
description: "clicked an anchor link",
},
{
action: () => { win.frames[0].history.pushState({}, "History PushState", `${FRAME2}#ref2`); },
waitURL: `${FRAME2}#ref2`,
expectedEvent: "onReferenceFragmentUpdated",
description: "history.pushState, same pathname, different hash",
},
{
action: () => { win.frames[0].history.pushState({}, "History PushState", `${FRAME2}#ref2`); },
waitURL: `${FRAME2}#ref2`,
expectedEvent: "onHistoryStateUpdated",
description: "history.pushState, same pathname, same hash",
},
{
action: () => {
win.frames[0].history.pushState({}, "History PushState", `${FRAME2}?query_param1=value#ref2`);
},
waitURL: `${FRAME2}?query_param1=value#ref2`,
expectedEvent: "onHistoryStateUpdated",
description: "history.pushState, same pathname, same hash, different query params",
},
{
action: () => {
win.frames[0].history.pushState({}, "History PushState", `${FRAME2}?query_param2=value#ref3`);
},
waitURL: `${FRAME2}?query_param2=value#ref3`,
expectedEvent: "onHistoryStateUpdated",
description: "history.pushState, same pathname, different hash, different query params",
},
{
action: () => { win.frames[0].history.pushState(null, "History PushState", FRAME_PUSHSTATE); },
waitURL: FRAME_PUSHSTATE,
expectedEvent: "onHistoryStateUpdated",
description: "history.pushState, different pathname",
},
];
info("Received onReferenceFragmentUpdated from FRAME2");
for (let navigation of navigationSequence) {
let {expectedEvent, waitURL, action, description} = navigation;
info(`Waiting ${expectedEvent} from ${waitURL} - ${description}`);
yield loadAndWait(win, expectedEvent, waitURL, action);
info(`Received ${expectedEvent} from ${waitURL} - ${description}`);
}
for (let i = navigationSequence.length - 1; i > 0; i--) {
let {waitURL: fromURL, expectedEvent} = navigationSequence[i];
let {waitURL} = navigationSequence[i - 1];
info(`Waiting ${expectedEvent} from ${waitURL} - history.back() from ${fromURL} to ${waitURL}`);
yield loadAndWait(win, expectedEvent, waitURL, () => { win.frames[0].history.back(); });
info(`Received ${expectedEvent} from ${waitURL} - history.back() from ${fromURL} to ${waitURL}`);
}
for (let i = 0; i < navigationSequence.length - 1; i++) {
let {waitURL: fromURL} = navigationSequence[i];
let {waitURL, expectedEvent} = navigationSequence[i + 1];
info(`Waiting ${expectedEvent} from ${waitURL} - history.forward() from ${fromURL} to ${waitURL}`);
yield loadAndWait(win, expectedEvent, waitURL, () => { win.frames[0].history.forward(); });
info(`Received ${expectedEvent} from ${waitURL} - history.forward() from ${fromURL} to ${waitURL}`);
}
win.close();

View File

@ -281,6 +281,18 @@ let json = [
},
],
},
{
namespace: "inject",
properties: {
PROP1: {value: "should inject"},
},
},
{
namespace: "do-not-inject",
properties: {
PROP1: {value: "should not inject"},
},
},
];
let tallied = null;
@ -322,6 +334,11 @@ let wrapper = {
tally("call", ns, name, args);
},
shouldInject(path) {
let ns = path.join(".");
return ns != "do-not-inject";
},
getProperty(path, name) {
let ns = path.join(".");
tally("get", ns, name);
@ -358,6 +375,9 @@ add_task(function* () {
do_check_eq(root.testing.type1.VALUE1, "value1", "enum type");
do_check_eq(root.testing.type1.VALUE2, "value2", "enum type");
do_check_eq("inject" in root, true, "namespace 'inject' should be injected");
do_check_eq("do-not-inject" in root, false, "namespace 'do-not-inject' should not be injected");
root.testing.foo(11, true);
verify("call", "testing", "foo", [11, true]);

View File

@ -842,6 +842,10 @@ this.FormHistory = {
},
update : function formHistoryUpdate(aChanges, aCallbacks) {
if (!Prefs.enabled) {
return;
}
// Used to keep track of how many searches have been started. When that number
// are finished, updateFormHistoryWrite can be called.
let numSearches = 0;
@ -856,13 +860,6 @@ this.FormHistory = {
if (!("length" in aChanges))
aChanges = [aChanges];
let isRemoveOperation = aChanges.every(change => change && change.op && change.op == "remove");
if (!Prefs.enabled && !isRemoveOperation) {
throw Components.Exception(
"Form history is disabled, only remove operations are allowed",
Cr.NS_ERROR_ILLEGAL_VALUE);
}
for (let change of aChanges) {
switch (change.op) {
case "remove":

View File

@ -405,36 +405,6 @@ add_task(function* ()
yield promiseCountEntries(null, null, num => do_check_eq(num, 4));
// ===== 21 =====
// Check update throws if form history is disabled and the operation is not a
// pure removal.
testnum++;
Services.prefs.setBoolPref("browser.formfill.enable", false);
Assert.throws(() => promiseUpdate(
{ op : "bump", fieldname: "field5", value: "value5" }),
/NS_ERROR_ILLEGAL_VALUE/);
Assert.throws(() => promiseUpdate(
{ op : "add", fieldname: "field5", value: "value5" }),
/NS_ERROR_ILLEGAL_VALUE/);
Assert.throws(() => promiseUpdate([
{ op : "update", fieldname: "field5", value: "value5" },
{ op : "remove", fieldname: "field5", value: "value5" }
]),
/NS_ERROR_ILLEGAL_VALUE/);
Assert.throws(() => promiseUpdate([
null,
undefined,
"",
1,
{},
{ op : "remove", fieldname: "field5", value: "value5" }
]),
/NS_ERROR_ILLEGAL_VALUE/);
// Remove should work though.
yield promiseUpdate([{ op: "remove", fieldname: "field5", value: null },
{ op: "remove", fieldname: null, value: null }]);
Services.prefs.clearUserPref("browser.formfill.enable");
} catch (e) {
throw "FAILED in test #" + testnum + " -- " + e;
}

View File

@ -99,8 +99,11 @@ var Manager = {
onLocationChange(browser, data) {
let url = data.location;
if (data.flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) {
if (data.isReferenceFragmentUpdated) {
this.fire("onReferenceFragmentUpdated", browser, data, {url});
} else if (data.isHistoryStateUpdated) {
this.fire("onHistoryStateUpdated", browser, data, {url});
} else {
this.fire("onCommitted", browser, data, {url});
}
@ -142,9 +145,8 @@ const EVENTS = [
"onCompleted",
"onErrorOccurred",
"onReferenceFragmentUpdated",
"onHistoryStateUpdated",
// "onCreatedNavigationTarget",
// "onHistoryStateUpdated",
];
var WebNavigation = {};

View File

@ -25,6 +25,19 @@ addMessageListener("Extension:DisableWebNavigation", () => {
var WebProgressListener = {
init: function() {
// This WeakMap (DOMWindow -> nsIURI) keeps track of the pathname and hash
// of the previous location for all the existent docShells.
this.previousURIMap = new WeakMap();
// Populate the above previousURIMap by iterating over the docShells tree.
for (let currentDocShell of WebNavigationFrames.iterateDocShellTree(docShell)) {
let win = currentDocShell.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindow);
let {currentURI} = currentDocShell.QueryInterface(Ci.nsIWebNavigation);
this.previousURIMap.set(win, currentURI);
}
let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebProgress);
webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_WINDOW |
@ -48,6 +61,7 @@ var WebProgressListener = {
status,
stateFlags,
};
sendAsyncMessage("Extension:StateChange", data);
if (webProgress.DOMWindow.top != webProgress.DOMWindow) {
@ -56,24 +70,51 @@ var WebProgressListener = {
// For some reason we don't fire onLocationChange for the
// initial navigation of a sub-frame. So we need to simulate
// it here.
let data = {
location: request.QueryInterface(Ci.nsIChannel).URI.spec,
windowId: webProgress.DOMWindowID,
parentWindowId: WebNavigationFrames.getParentWindowId(webProgress.DOMWindow),
flags: 0,
};
sendAsyncMessage("Extension:LocationChange", data);
this.onLocationChange(webProgress, request, request.QueryInterface(Ci.nsIChannel).URI, 0);
}
}
},
onLocationChange: function onLocationChange(webProgress, request, locationURI, flags) {
let {DOMWindow, loadType} = webProgress;
// Get the previous URI loaded in the DOMWindow.
let previousURI = this.previousURIMap.get(DOMWindow);
// Update the URI in the map with the new locationURI.
this.previousURIMap.set(DOMWindow, locationURI);
let isSameDocument = (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT);
let isHistoryStateUpdated = false;
let isReferenceFragmentUpdated = false;
if (isSameDocument) {
let pathChanged = !(previousURI && locationURI.equalsExceptRef(previousURI));
let hashChanged = !(previousURI && previousURI.ref == locationURI.ref);
// When the location changes but the document is the same:
// - path not changed and hash changed -> |onReferenceFragmentUpdated|
// (even if it changed using |history.pushState|)
// - path not changed and hash not changed -> |onHistoryStateUpdated|
// (only if it changes using |history.pushState|)
// - path changed -> |onHistoryStateUpdated|
if (!pathChanged && hashChanged) {
isReferenceFragmentUpdated = true;
} else if (loadType & Ci.nsIDocShell.LOAD_CMD_PUSHSTATE) {
isHistoryStateUpdated = true;
} else if (loadType & Ci.nsIDocShell.LOAD_CMD_HISTORY) {
isHistoryStateUpdated = true;
}
}
let data = {
isHistoryStateUpdated, isReferenceFragmentUpdated,
location: locationURI ? locationURI.spec : "",
windowId: webProgress.DOMWindowID,
parentWindowId: WebNavigationFrames.getParentWindowId(webProgress.DOMWindow),
flags,
};
sendAsyncMessage("Extension:LocationChange", data);
},

View File

@ -97,6 +97,8 @@ function findFrame(windowId, rootDocShell) {
}
var WebNavigationFrames = {
iterateDocShellTree,
getFrame(docShell, frameId) {
if (frameId == 0) {
return convertDocShellToFrameDetail(docShell);