author | Wes Kocher <wkocher@mozilla.com> |
Thu, 25 Feb 2016 14:32:51 -0800 | |
changeset 285597 | 918df3a0bc1c4d07299e4f66274a7da923534577 |
parent 285577 | 97cf677ee66802809808a3e61a0ccb89542ca54e (current diff) |
parent 285596 | 93aaf03b7399b9e2efd8d91b2ae060787b002281 (diff) |
child 285645 | 7381731bbb411698aa74352841f79a7fb13020c3 |
child 285704 | 8e34b12969bf345b0cd765f612d59f8c0a6de444 |
push id | 30032 |
push user | kwierso@gmail.com |
push date | Thu, 25 Feb 2016 22:32:52 +0000 |
treeherder | mozilla-central@918df3a0bc1c [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | merge |
milestone | 47.0a1 |
first release with | nightly linux32
918df3a0bc1c
/
47.0a1
/
20160226030256
/
files
nightly linux64
918df3a0bc1c
/
47.0a1
/
20160226030256
/
files
nightly mac
918df3a0bc1c
/
47.0a1
/
20160226030256
/
files
nightly win32
918df3a0bc1c
/
47.0a1
/
20160226030256
/
files
nightly win64
918df3a0bc1c
/
47.0a1
/
20160226030256
/
files
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
releases | nightly linux32
47.0a1
/
20160226030256
/
pushlog to previous
nightly linux64
47.0a1
/
20160226030256
/
pushlog to previous
nightly mac
47.0a1
/
20160226030256
/
pushlog to previous
nightly win32
47.0a1
/
20160226030256
/
pushlog to previous
nightly win64
47.0a1
/
20160226030256
/
pushlog to previous
|
devtools/client/inspector/test/browser_inspector_scrolling.js | file | annotate | diff | comparison | revisions |
--- a/browser/components/downloads/test/browser/browser.ini +++ b/browser/components/downloads/test/browser/browser.ini @@ -1,14 +1,11 @@ [DEFAULT] 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 -
new file mode 100644 --- /dev/null +++ b/browser/components/extensions/ext-commands.js @@ -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); + }, + }, + }; +});
--- a/browser/components/extensions/jar.mn +++ b/browser/components/extensions/jar.mn @@ -1,14 +1,15 @@ # 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/. 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 content/browser/ext-desktop-runtime.js content/browser/ext-tabs.js content/browser/ext-windows.js content/browser/ext-bookmarks.js
--- a/browser/components/extensions/test/browser/browser.ini +++ b/browser/components/extensions/test/browser/browser.ini @@ -4,16 +4,17 @@ support-files = context.html ctxmenu-image.png context_tabs_onUpdated_page.html context_tabs_onUpdated_iframe.html file_popup_api_injection_a.html 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] [browser_ext_browserAction_context.js] [browser_ext_browserAction_disabled.js] [browser_ext_pageAction_context.js] [browser_ext_pageAction_popup.js] [browser_ext_browserAction_popup.js]
new file mode 100644 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_commands.js @@ -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(); +});
--- a/browser/components/nsBrowserGlue.js +++ b/browser/components/nsBrowserGlue.js @@ -541,16 +541,17 @@ BrowserGlue.prototype = { if (AppConstants.NIGHTLY_BUILD) { os.addObserver(this, AddonWatcher.TOPIC_SLOW_ADDON_DETECTED, false); } 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"); ExtensionManagement.registerScript("chrome://browser/content/ext-windows.js"); ExtensionManagement.registerScript("chrome://browser/content/ext-bookmarks.js"); ExtensionManagement.registerSchema("chrome://browser/content/schemas/bookmarks.json"); ExtensionManagement.registerSchema("chrome://browser/content/schemas/browser_action.json");
--- a/devtools/client/aboutdebugging/components/aboutdebugging.js +++ b/devtools/client/aboutdebugging/components/aboutdebugging.js @@ -1,24 +1,23 @@ /* 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/. */ -/* 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"); const tabs = [ { id: "addons", name: Strings.GetStringFromName("addons"), icon: "chrome://devtools/skin/images/debugging-addons.svg", component: AddonsTab }, @@ -33,23 +32,23 @@ exports.AboutDebuggingApp = React.create getInitialState() { return { selectedTabId: defaultTabId }; }, 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(); }, render() { let { client } = this.props; let { selectedTabId } = this.state; let selectTab = this.selectTab; @@ -60,25 +59,24 @@ exports.AboutDebuggingApp = React.create "div", { className: "app"}, React.createElement(TabMenu, { tabs, selectedTabId, selectTab }), React.createElement("div", { className: "main-content" }, React.createElement(selectedTab.component, { client })) ); }, 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) { this.setState({ selectedTabId: tabId }); } else { // If the current hash matches no valid category, navigate to the default // tab. this.selectTab(defaultTabId); } }, selectTab(tabId) { - let win = this.props.window; - win.location.hash = "#" + tabId; + window.location.hash = "#" + tabId; } });
--- a/devtools/client/aboutdebugging/components/addons-controls.js +++ b/devtools/client/aboutdebugging/components/addons-controls.js @@ -1,24 +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/. */ -/* 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"); const MORE_INFO_URL = "https://developer.mozilla.org/docs/Tools" + "/about:debugging#Enabling_add-on_debugging"; exports.AddonsControls = React.createClass({ displayName: "AddonsControls", @@ -53,33 +53,31 @@ exports.AddonsControls = React.createCla }, onEnableAddonDebuggingChange(event) { let enabled = event.target.checked; Services.prefs.setBoolPref("devtools.chrome.enabled", enabled); 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(); if (res == Ci.nsIFilePicker.returnCancel || !fp.file) { return; } let file = fp.file; // AddonManager.installTemporaryAddon accepts either // addon directory or final xpi file. if (!file.isDirectory() && !file.leafName.endsWith(".xpi")) { file = file.parent; } 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; } }, });
--- a/devtools/client/aboutdebugging/components/addons-tab.js +++ b/devtools/client/aboutdebugging/components/addons-tab.js @@ -1,28 +1,23 @@ /* 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/. */ -/* 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( "chrome://devtools/locale/aboutdebugging.properties"); const CHROME_ENABLED_PREF = "devtools.chrome.enabled"; const REMOTE_ENABLED_PREF = "devtools.debugger.remote-enabled";
--- a/devtools/client/aboutdebugging/components/tab-header.js +++ b/devtools/client/aboutdebugging/components/tab-header.js @@ -1,18 +1,15 @@ /* 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/. */ -/* 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", render() { let { name, id } = this.props; return React.createElement(
--- a/devtools/client/aboutdebugging/components/tab-menu-entry.js +++ b/devtools/client/aboutdebugging/components/tab-menu-entry.js @@ -1,18 +1,15 @@ /* 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/. */ -/* 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", render() { let { icon, name, selected } = this.props; // Here .category, .category-icon, .category-name classnames are used to
--- a/devtools/client/aboutdebugging/components/tab-menu.js +++ b/devtools/client/aboutdebugging/components/tab-menu.js @@ -1,20 +1,18 @@ /* 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/. */ /* global React */ "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", render() { let { tabs, selectedTabId, selectTab } = this.props; let tabLinks = tabs.map(({ id, name, icon }) => { let selected = id == selectedTabId;
--- a/devtools/client/aboutdebugging/components/target-list.js +++ b/devtools/client/aboutdebugging/components/target-list.js @@ -1,24 +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/. */ /* global React */ "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()); }; exports.TargetList = React.createClass({ displayName: "TargetList", render() {
--- a/devtools/client/aboutdebugging/components/target.js +++ b/devtools/client/aboutdebugging/components/target.js @@ -1,72 +1,84 @@ /* 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/. */ -/* 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"); 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()); + }); + }); + }, });
--- a/devtools/client/aboutdebugging/components/workers-tab.js +++ b/devtools/client/aboutdebugging/components/workers-tab.js @@ -1,27 +1,21 @@ /* 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/. */ -/* 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"); const WorkerIcon = "chrome://devtools/skin/images/debugging-workers.svg"; exports.WorkersTab = React.createClass({ displayName: "WorkersTab", @@ -33,23 +27,25 @@ exports.WorkersTab = React.createClass({ other: [] } }; }, componentDidMount() { let client = this.props.client; client.addListener("workerListChanged", this.update); + client.addListener("serviceWorkerRegistrationListChanged", this.update); client.addListener("processListChanged", this.update); this.update(); }, componentWillUnmount() { let client = this.props.client; client.removeListener("processListChanged", this.update); + client.removeListener("serviceWorkerRegistrationListChanged", this.update); client.removeListener("workerListChanged", this.update); }, render() { let { client } = this.props; let { workers } = this.state; return React.createElement( @@ -72,57 +68,95 @@ exports.WorkersTab = React.createClass({ id: "other-workers", name: Strings.GetStringFromName("otherWorkers"), targets: workers.other, client })) ); }, update() { let workers = this.getInitialState().workers; + this.getWorkerForms().then(forms => { - forms.forEach(form => { + forms.registrations.forEach(form => { + workers.service.push({ + type: "serviceworker", + icon: WorkerIcon, + name: form.url, + url: form.url, + scope: form.scope, + registrationActor: form.actor + }); + }); + + forms.workers.forEach(form => { let worker = { - name: form.url, + type: "worker", icon: WorkerIcon, - actorID: form.actor + 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()); + + // 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; + // 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 }; }), });
--- a/devtools/client/aboutdebugging/initializer.js +++ b/devtools/client/aboutdebugging/initializer.js @@ -1,43 +1,50 @@ /* 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/. */ /* eslint-env browser */ -/* global DebuggerClient, DebuggerServer, React */ +/* global DebuggerClient, DebuggerServer */ "use strict"; const { loader } = Components.utils.import( "resource://devtools/shared/Loader.jsm", {}); loader.lazyRequireGetter(this, "DebuggerClient", "devtools/shared/client/main", true); 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() { if (!DebuggerServer.initialized) { DebuggerServer.init(); DebuggerServer.addBrowserActors(); } DebuggerServer.allowChromeProcess = true; this.client = new DebuggerClient(DebuggerServer.connectPipe()); 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")); }); }, destroy() { React.unmountComponentAtNode(document.querySelector("#body")); this.client.close(); this.client = null;
--- a/devtools/client/aboutdebugging/test/browser_service_workers_timeout.js +++ b/devtools/client/aboutdebugging/test/browser_service_workers_timeout.js @@ -62,17 +62,18 @@ add_task(function* () { }); }); ok(true, "Service worker registration resolved"); // Retrieve the DEBUG button for the worker 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 let onToolboxReady = new Promise(done => { gDevTools.once("toolbox-ready", function(e, toolbox) { done(toolbox); }); }); @@ -83,26 +84,28 @@ add_task(function* () { // Wait for more than the regular timeout, // so that if the worker freezing doesn't work, // it will be destroyed and removed from the list yield new Promise(done => { setTimeout(done, SW_TIMEOUT * 2); }); 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 frameScript = function() { // Retrieve the `sw` promise created in the html page let { sw } = content.wrappedJSObject; sw.then(function(registration) { registration.unregister().then(function() { @@ -119,11 +122,16 @@ add_task(function* () { yield new Promise(done => { mm.addMessageListener("sw-unregistered", function listener() { mm.removeMessageListener("sw-unregistered", listener); done(); }); }); 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); });
--- a/devtools/client/inspector/test/browser.ini +++ b/devtools/client/inspector/test/browser.ini @@ -37,17 +37,16 @@ support-files = [browser_inspector_breadcrumbs_mutations.js] [browser_inspector_delete-selected-node-01.js] [browser_inspector_delete-selected-node-02.js] [browser_inspector_delete-selected-node-03.js] [browser_inspector_destroy-after-navigation.js] [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] [browser_inspector_highlighter-04.js] [browser_inspector_highlighter-by-type.js] [browser_inspector_highlighter-comments.js] [browser_inspector_highlighter-csstransform_01.js] [browser_inspector_highlighter-csstransform_02.js] @@ -97,18 +96,16 @@ skip-if = e10s && debug && os == 'win' # [browser_inspector_pane-toggle-03.js] [browser_inspector_picker-stop-on-destroy.js] [browser_inspector_picker-stop-on-tool-change.js] [browser_inspector_pseudoclass-lock.js] [browser_inspector_pseudoclass-menu.js] [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] [browser_inspector_search-04.js] [browser_inspector_search-05.js] [browser_inspector_search-06.js] [browser_inspector_search-07.js] [browser_inspector_search-reserved.js]
deleted file mode 100644 --- a/devtools/client/inspector/test/browser_inspector_scrolling.js +++ /dev/null @@ -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(); -});
--- a/devtools/client/responsivedesign/responsivedesign.jsm +++ b/devtools/client/responsivedesign/responsivedesign.jsm @@ -58,22 +58,28 @@ var Manager = { } }, /** * Launches the responsive mode. * * @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. * * @param aTab the tab targeted. */ isActiveForTab: function(aTab) { return ActiveTabs.has(aTab); @@ -92,19 +98,17 @@ var Manager = { * @param aWindow the browser window. * @param aTab the tab targeted. * @param aCommand the command name. * @param aArgs command arguments. */ 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": this.runIfNeeded(aWindow, aTab); break; case "resize off": if (this.isActiveForTab(aTab)) { yield ActiveTabs.get(aTab).close();
--- a/devtools/client/shared/browser-loader.js +++ b/devtools/client/shared/browser-loader.js @@ -13,50 +13,16 @@ Cu.import("resource://gre/modules/AppCon const BROWSER_BASED_DIRS = [ "resource://devtools/client/jsonview", "resource://devtools/client/shared/vendor", "resource://devtools/client/shared/components", "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 * global object) as the sandbox prototype, so when a variable is not * defined it checks `window` before throwing an error. This makes all * browser APIs available to modules by default, like a normal browser * environment, but modules are still evaluated in their own scope. * @@ -73,16 +39,33 @@ function hotReloadFile(window, require, * @param Object window * The window instance to evaluate modules within * @return Object * An object with two properties: * - loader: the Loader instance * - 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(); const hotReloadEnabled = Services.prefs.getBoolPref("devtools.loader.hotreload"); if(AppConstants.DEBUG || AppConstants.DEBUG_JS_MODULES) { dynamicPaths["devtools/client/shared/vendor/react"] = "resource://devtools/client/shared/vendor/react-dev"; @@ -121,16 +104,23 @@ function BrowserLoader(baseURI, window) // ... code ... // }); // // Bug 1248830 will work out a better plan here for our content module // loading needs, especially as we head towards devtools.html. 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), + }, } }; if(hotReloadEnabled) { opts.loadModuleHook = (module, require) => { const { uri, exports } = module; if (exports.prototype && @@ -150,34 +140,86 @@ function BrowserLoader(baseURI, window) instances.forEach(getForceUpdate(React)); module.exports = proxy.get(); } } return exports; } } - 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"];
--- a/devtools/client/styleeditor/StyleEditorUI.jsm +++ b/devtools/client/styleeditor/StyleEditorUI.jsm @@ -940,30 +940,30 @@ StyleEditorUI.prototype = { }, /** * Launches the responsive mode with a specific width or height * * @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); } else if (options.width) { ResponsiveUIManager.getResponsiveUIForTab(tab).setWidth(options.width); } else if (options.height) { ResponsiveUIManager.getResponsiveUIForTab(tab).setHeight(options.height); } - }, + }), /** * Jump cursor to the editor for a stylesheet and line number for a rule. * * @param {object} location * Location object with 'line', 'column', and 'source' properties. */ _jumpToLocation: function(location) {
--- a/devtools/client/styleeditor/test/browser_styleeditor_media_sidebar_links.js +++ b/devtools/client/styleeditor/test/browser_styleeditor_media_sidebar_links.js @@ -2,74 +2,111 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; /* 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), "There should be no links in the first media rule."); ok(!conditions[1].querySelector(responsiveModeToggleClass), "There should be no links in the second media rule."); 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, () => { deferred.resolve(); }); return deferred.promise; }
--- a/devtools/client/webconsole/test/browser.ini +++ b/devtools/client/webconsole/test/browser.ini @@ -214,17 +214,16 @@ tags = mcb [browser_webconsole_bug_587617_output_copy.js] [browser_webconsole_bug_588342_document_focus.js] skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s [browser_webconsole_bug_588730_text_node_insertion.js] [browser_webconsole_bug_588967_input_expansion.js] [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] skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s [browser_webconsole_bug_595934_message_categories.js] skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s [browser_webconsole_bug_597103_deactivateHUDForContext_unfocused_window.js] [browser_webconsole_bug_597136_external_script_errors.js] @@ -293,17 +292,16 @@ skip-if = e10s && os == 'win' [browser_webconsole_certificate_messages.js] skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s [browser_webconsole_show_subresource_security_errors.js] skip-if = e10s && os == 'win' [browser_webconsole_cached_autocomplete.js] [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] [browser_webconsole_console_extras.js] [browser_webconsole_console_logging_api.js] [browser_webconsole_console_logging_workers_api.js] [browser_webconsole_console_trace_async.js] [browser_webconsole_count.js]
--- a/devtools/client/webconsole/test/browser_webconsole_bug_593003_iframe_wrong_hud.js +++ b/devtools/client/webconsole/test/browser_webconsole_bug_593003_iframe_wrong_hud.js @@ -10,62 +10,59 @@ const TEST_URI = "http://example.com/bro const TEST_IFRAME_URI = "http://example.com/browser/devtools/client/" + "webconsole/test/test-bug-593003-iframe-wrong-" + "hud-iframe.html"; const TEST_DUMMY_URI = "http://example.com/browser/devtools/client/" + "webconsole/test/test-console.html"; -var tab1, tab2; +add_task(function*() { -function test() { - loadTab(TEST_URI).then(({tab}) => { - tab1 = tab; - + 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 hud2 = yield openConsole(tab2); + let outputNode2 = hud2.outputNode; - let msg = "Didn't find the iframe network request in tab2"; - testLogEntry(outputNode2, TEST_IFRAME_URI, msg, true, true); + isnot(outputNode1, outputNode2, + "the two HUD outputNodes must be different"); - closeConsole(tab2).then(() => { - gBrowser.removeTab(tab2); - tab1 = tab2 = null; - executeSoon(finishTest); - }); - }); + let msg = "Didn't find the iframe network request in tab2"; + testLogEntry(outputNode2, TEST_IFRAME_URI, msg, true, true); }
--- a/devtools/client/webconsole/test/browser_webconsole_clickable_urls.js +++ b/devtools/client/webconsole/test/browser_webconsole_clickable_urls.js @@ -3,19 +3,17 @@ /* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ // When strings containing URLs are entered into the webconsole, check // its output and ensure that the output can be clicked to open those URLs. "use strict"; -const TEST_URI = "data:text/html;charset=utf8,Bug 1005909 - Clickable URLS"; - -var inputTests = [ +const inputTests = [ // 0: URL opens page when clicked. { input: "'http://example.com'", output: "http://example.com", expectedTab: "http://example.com/", }, @@ -91,16 +89,15 @@ var inputTests = [ noClick: true, consoleLogClick: true, expectedTab: "http://example.com/abcdefghijabcdefghij", getClickableNode: (msg) => msg.querySelectorAll("a")[1], }, ]; -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); +});
--- a/devtools/server/actors/child-process.js +++ b/devtools/server/actors/child-process.js @@ -90,17 +90,17 @@ ChildProcessActor.prototype = { highlightable: false, networkMonitor: false, }, }; }, 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); for (let actor of actors) { pool.addActor(actor); } this.conn.removeActorPool(this._workerActorPool);
--- a/devtools/server/actors/webbrowser.js +++ b/devtools/server/actors/webbrowser.js @@ -126,18 +126,19 @@ exports.sendShutdownEvent = sendShutdown * The conection to the client. */ function createRootActor(aConnection) { return new RootActor(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 }); } /** * A live list of BrowserTabActors representing the current browser tabs, @@ -1093,17 +1094,17 @@ TabActor.prototype = { }, onListWorkers: function BTA_onListWorkers(aRequest) { if (!this.attached) { return { error: "wrongState" }; } if (this._workerActorList === null) { - this._workerActorList = new WorkerActorList({ + this._workerActorList = new WorkerActorList(this.conn, { type: Ci.nsIWorkerDebugger.TYPE_DEDICATED, window: this.window }); } return this._workerActorList.getList().then((actors) => { let pool = new ActorPool(this.conn); for (let actor of actors) {
--- a/devtools/server/actors/worker.js +++ b/devtools/server/actors/worker.js @@ -1,12 +1,14 @@ "use strict"; 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"); XPCOMUtils.defineLazyServiceGetter( this, "wdm", "@mozilla.org/dom/workers/workerdebuggermanager;1", "nsIWorkerDebuggerManager" ); @@ -30,113 +32,138 @@ function matchWorkerDebugger(dbg, option if (window !== options.window) { return false; } } 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) { let worker = this._getServiceWorkerInfo(); if (worker) { worker.attachDebugger(); } } 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" }; } if (this._threadActor !== null) { return { type: "connected", threadActor: this._threadActor }; } 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; this._consoleActor = consoleActor; return { type: "connected", threadActor: this._threadActor, consoleActor: this._consoleActor }; }, (error) => { return { error: error.toString() }; }); - }, + }, { + request: { + options: Arg(0, "json"), + }, + response: RetVal("json") + }), onClose: function () { - if (this._isAttached) { + if (this._attached) { this._detach(); } this.conn.sendActorEvent(this.actorID, "close"); }, onError: function (filename, lineno, message) { 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 () { if (this._threadActor !== null) { this._transport.close(); this._transport = null; this._threadActor = null; } @@ -151,29 +178,24 @@ WorkerActor.prototype = { if (type == Ci.nsIWorkerDebugger.TYPE_SERVICE) { let worker = this._getServiceWorkerInfo(); if (worker) { worker.detachDebugger(); } } 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; this._mustNotify = false; this.onRegister = this.onRegister.bind(this); this.onUnregister = this.onUnregister.bind(this); } @@ -194,17 +216,17 @@ WorkerActorList.prototype = { if (!dbgs.has(dbg)) { this._actors.delete(dbg); } } // 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)); } } let actors = []; for (let [, actor] of this._actors) { actors.push(actor); } @@ -260,32 +282,40 @@ WorkerActorList.prototype = { if (matchWorkerDebugger(dbg, this._options)) { this._notifyListChanged(); } } }; 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; this.onRegister = this.onRegister.bind(this); this.onUnregister = this.onUnregister.bind(this); }; ServiceWorkerRegistrationActorList.prototype = { @@ -304,17 +334,17 @@ ServiceWorkerRegistrationActorList.proto this._actors.delete(registration); } } // Create an actor for each registration for which we don't have one. for (let registration of registrations) { if (!this._actors.has(registration)) { this._actors.set(registration, - new ServiceWorkerRegistrationActor(registration)); + new ServiceWorkerRegistrationActor(this._conn, registration)); } } if (!this._mustNotify) { if (this._onListChanged !== null) { swm.addListener(this); } this._mustNotify = true;
--- a/devtools/shared/client/main.js +++ b/devtools/shared/client/main.js @@ -1411,16 +1411,19 @@ WorkerClient.prototype = { }, get isClosed() { return this._isClosed; }, detach: DebuggerClient.requester({ type: "detach" }, { after: function (aResponse) { + if (this.thread) { + this.client.unregisterClient(this.thread); + } this.client.unregisterClient(this); return aResponse; }, telemetry: "WORKERDETACH" }), attachThread: function(aOptions = {}, aOnResponse = noop) {
--- a/toolkit/components/extensions/Extension.jsm +++ b/toolkit/components/extensions/Extension.jsm @@ -379,19 +379,27 @@ GlobalManager = { // does not. let injectObject = (name, defaultCallback) => { let browserObj = Cu.createObjectIn(contentWindow, {defineAs: name}); let api = Management.generateAPIs(extension, context, Management.apis); 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; } let schemaWrapper = { get cloneScope() { return context.cloneScope; }, @@ -418,16 +426,20 @@ GlobalManager = { Cu.reportError(e); promise = Promise.reject({message: "An unexpected error occurred"}); } } return context.wrapPromise(promise || Promise.resolve(), callback); }, + shouldInject(path, name) { + return findPath(path) != null; + }, + getProperty(path, name) { return findPath(path)[name]; }, setProperty(path, name, value) { findPath(path)[name] = value; },
--- a/toolkit/components/extensions/ExtensionContent.jsm +++ b/toolkit/components/extensions/ExtensionContent.jsm @@ -115,16 +115,21 @@ var api = context => { inIncognitoContext: PrivateBrowsingUtils.isContentWindowPrivate(context.contentWindow), }, i18n: { getMessage: function(messageName, substitutions) { 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; }, detectLanguage: function(text, callback) { let result = detectLanguage(text); return context.wrapPromise(result, callback); },
--- a/toolkit/components/extensions/ExtensionUtils.jsm +++ b/toolkit/components/extensions/ExtensionUtils.jsm @@ -15,16 +15,18 @@ Cu.import("resource://gre/modules/XPCOMU Cu.import("resource://gre/modules/Services.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "AppConstants", "resource://gre/modules/AppConstants.jsm"); 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"); } // Run a function and report exceptions. function runSafeSyncWithoutClone(f, ...args) { try { @@ -424,16 +426,26 @@ LocaleData.prototype = { // Message names are also case-insensitive, so normalize them to lower-case. result.set(key.toLowerCase(), value); } this.messages.set(locale, result); 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. return Locale.getLocale().replace(/-/g, "_"); }, }; // This is a generic class for managing event listeners. Example usage:
--- a/toolkit/components/extensions/Schemas.jsm +++ b/toolkit/components/extensions/Schemas.jsm @@ -1314,17 +1314,19 @@ this.Schemas = { } }); }, inject(dest, wrapperFuncs) { 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) { delete dest[namespace]; } } },
--- a/toolkit/components/extensions/ext-downloads.js +++ b/toolkit/components/extensions/ext-downloads.js @@ -1,20 +1,110 @@ "use strict"; 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) { // if (!extension.hasPermission("downloads.open")) { // throw new context.cloneScope.Error("Permission denied because 'downloads.open' permission is missing."); // } // ... // }
--- a/toolkit/components/extensions/ext-i18n.js +++ b/toolkit/components/extensions/ext-i18n.js @@ -7,16 +7,21 @@ var { extensions.registerSchemaAPI("i18n", null, (extension, context) => { return { i18n: { getMessage: function(messageName, substitutions) { return extension.localizeMessage(messageName, substitutions); }, + getAcceptLanguages: function() { + let result = extension.localeData.acceptLanguages; + return Promise.resolve(result); + }, + getUILanguage: function() { return extension.localeData.uiLocale; }, detectLanguage: function(text) { return detectLanguage(text); }, },
--- a/toolkit/components/extensions/ext-webNavigation.js +++ b/toolkit/components/extensions/ext-webNavigation.js @@ -74,16 +74,17 @@ extensions.registerSchemaAPI("webNavigat return { webNavigation: { onBeforeNavigate: new WebNavigationEventManager(context, "onBeforeNavigate").api(), onCommitted: new WebNavigationEventManager(context, "onCommitted").api(), onDOMContentLoaded: new WebNavigationEventManager(context, "onDOMContentLoaded").api(), 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); if (!tab) { return Promise.reject({message: `No tab found with tabId: ${details.tabId}`}); } let {innerWindowID, messageManager} = tab.linkedBrowser;
--- a/toolkit/components/extensions/moz.build +++ b/toolkit/components/extensions/moz.build @@ -14,9 +14,10 @@ EXTRA_JS_MODULES += [ 'Schemas.jsm', ] 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']
--- a/toolkit/components/extensions/schemas/downloads.json +++ b/toolkit/components/extensions/schemas/downloads.json @@ -17,17 +17,17 @@ }, { "namespace": "downloads", "types": [ { "id": "FilenameConflictAction", "type": "string", "enum": [ - "uniqify", + "uniquify", "overwrite", "prompt" ] }, { "id": "InterruptReason", "type": "string", "enum": [ @@ -209,52 +209,56 @@ } } } ], "functions": [ { "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": [ { "description": "What to download and how.", "name": "options", "type": "object", "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.", "optional": true, "type": "string" }, "conflictAction": { "$ref": "FilenameConflictAction", "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", "POST" ], "optional": true, "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.", "items": { "type": "object", "properties": { "name": { "description": "Name of the HTTP header.", @@ -263,16 +267,17 @@ "value": { "description": "Value of the HTTP header.", "type": "string" } } } }, "body": { + "unsupported": true, "description": "Post body.", "optional": true, "type": "string" } } }, { "name": "callback",
--- a/toolkit/components/extensions/schemas/i18n.json +++ b/toolkit/components/extensions/schemas/i18n.json @@ -25,17 +25,16 @@ "id": "LanguageCode", "type": "string", "description": "An ISO language code such as <code>en</code> or <code>fr</code>. For a complete list of languages supported by this method, see <a href='http://src.chromium.org/viewvc/chrome/trunk/src/third_party/cld/languages/internal/languages.cc'>kLanguageInfoTable</a>. For an unknown language, <code>und</code> will be returned, which means that [percentage] of the text is unknown to CLD" } ], "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", "parameters": [ { "type": "function", "name": "callback", "parameters": [
--- a/toolkit/components/extensions/schemas/web_navigation.json +++ b/toolkit/components/extensions/schemas/web_navigation.json @@ -340,17 +340,16 @@ "tabId": {"type": "integer", "description": "The ID of the tab that replaced the old tab."}, "timeStamp": {"type": "number", "description": "The time when the replacement happened, in milliseconds since the epoch."} } } ] }, { "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": [ { "name": "url", "type": "array", "items": { "$ref": "events.UrlFilter" }, "description": "Conditions that the URL being navigated to must satisfy. The 'schemes' and 'ports' fields of UrlFilter are ignored for this event."
new file mode 100644 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/chrome.ini @@ -0,0 +1,6 @@ +[DEFAULT] +skip-if = os == 'android' +support-files = + file_download.txt + +[test_chrome_ext_downloads_download.html]
new file mode 100644 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_download.txt @@ -0,0 +1,1 @@ +This is a sample file used in download tests.
new file mode 100644 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_downloads_download.html @@ -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>
--- a/toolkit/components/extensions/test/mochitest/test_ext_alarms.html +++ b/toolkit/components/extensions/test/mochitest/test_ext_alarms.html @@ -10,104 +10,210 @@ </head> <body> <script type="text/javascript"> "use strict"; 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"); } let extension = ExtensionTestUtils.loadExtension({ background: `(${backgroundScript})()`, manifest: { 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); } let extension = ExtensionTestUtils.loadExtension({ background: `(${backgroundScript})()`, manifest: { permissions: ["alarms"], }, }); yield extension.startup(); - info("extension loaded"); + yield extension.awaitFinish("alarms"); + yield extension.unload(); +}); + + +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(); - info("extension unloaded"); +}); + + +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); } let extension = ExtensionTestUtils.loadExtension({ background: `(${backgroundScript})()`, manifest: { permissions: ["alarms"], }, }); yield extension.startup(); - info("extension loaded"); yield extension.awaitFinish("alarms"); yield extension.unload(); - info("extension unloaded"); }); add_task(function* test_get_get_all_clear_all_alarms() { function backgroundScript() { const ALARM_NAME = "test_alarm"; let suffixes = [0, 1, 2]; @@ -124,38 +230,38 @@ add_task(function* test_get_get_all_clea suffixes.forEach(suffix => { browser.alarms.create(ALARM_NAME + suffix, {when: Date.now() + (suffix + 1) * 10000}); }); 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}`); }); })); }).then(() => { return promiseAlarms.clear(ALARM_NAME + suffixes[0]); }).then(wasCleared => { browser.test.assertTrue(wasCleared, "alarm was cleared"); return promiseAlarms.getAll(); }).then(alarms => { browser.test.assertEq(2, alarms.length, "alarm was removed"); 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(); }).then(wasCleared => { browser.test.assertTrue(wasCleared, "alarms were cleared"); return promiseAlarms.getAll(); }).then(alarms => {
--- a/toolkit/components/extensions/test/mochitest/test_ext_i18n.html +++ b/toolkit/components/extensions/test/mochitest/test_ext_i18n.html @@ -9,16 +9,17 @@ <script type="text/javascript" src="head.js"></script> <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> </head> <body> <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() { function runTests(assertEq) { let _ = browser.i18n.getMessage.bind(browser.i18n); let url = browser.runtime.getURL("/"); assertEq(url, `moz-extension://${_("@@extension_id")}/`, "@@extension_id builtin message"); @@ -158,16 +159,95 @@ add_task(function* test_i18n() { let win = window.open("file_sample.html"); yield extension.awaitMessage("content-script-finished"); win.close(); yield extension.awaitFinish("l10n"); 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 { getUILanguage: browser.i18n.getUILanguage(), getMessage: browser.i18n.getMessage("@@ui_locale"), }; }
--- a/toolkit/components/extensions/test/mochitest/test_ext_schema.html +++ b/toolkit/components/extensions/test/mochitest/test_ext_schema.html @@ -11,28 +11,30 @@ <body> <script type="text/javascript"> "use strict"; 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(); }); add_task(function* testUnknownProperties() { function background() { browser.test.notifyPass("loaded"); }
--- a/toolkit/components/extensions/test/mochitest/test_ext_webnavigation.html +++ b/toolkit/components/extensions/test/mochitest/test_ext_webnavigation.html @@ -19,16 +19,17 @@ function backgroundScript() { const EVENTS = [ "onBeforeNavigate", "onCommitted", "onDOMContentLoaded", "onCompleted", "onErrorOccurred", "onReferenceFragmentUpdated", + "onHistoryStateUpdated", ]; let expectedTabId = -1; function gotEvent(event, details) { if (!details.url.startsWith(BASE)) { return; } @@ -56,23 +57,24 @@ function backgroundScript() { } let listeners = {}; for (let event of EVENTS) { listeners[event] = gotEvent.bind(null, event); 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", "onCommitted", "onDOMContentLoaded", "onCompleted", ]; @@ -150,20 +152,81 @@ add_task(function* webnav_ordering() { checkBefore({url: URL, event: "onCommitted"}, {url: FRAME, event: "onBeforeNavigate"}); checkBefore({url: FRAME, event: "onCompleted"}, {url: URL, event: "onCompleted"}); yield loadAndWait(win, "onCompleted", FRAME2, () => { win.frames[0].location = FRAME2; }); 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(); yield extension.unload(); info("webnavigation extension unloaded"); }); </script>
--- a/toolkit/components/extensions/test/xpcshell/test_ext_schemas.js +++ b/toolkit/components/extensions/test/xpcshell/test_ext_schemas.js @@ -276,16 +276,28 @@ let json = [ type: "function", extraParameters: [{ name: "filter", type: "integer", }], }, ], }, + { + namespace: "inject", + properties: { + PROP1: {value: "should inject"}, + }, + }, + { + namespace: "do-not-inject", + properties: { + PROP1: {value: "should not inject"}, + }, + }, ]; let tallied = null; function tally(kind, ns, name, args) { tallied = [kind, ns, name, args]; } @@ -317,16 +329,21 @@ let wrapper = { talliedErrors.push(message); }, callFunction(path, name, args) { let ns = path.join("."); 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); }, setProperty(path, name, value) { let ns = path.join("."); tally("set", ns, name, value); @@ -353,16 +370,19 @@ add_task(function* () { let root = {}; Schemas.inject(root, wrapper); do_check_eq(root.testing.PROP1, 20, "simple value property"); 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]); root.testing.foo(true); verify("call", "testing", "foo", [null, true]); root.testing.foo(null, true); verify("call", "testing", "foo", [null, true]);
--- a/toolkit/components/satchel/FormHistory.jsm +++ b/toolkit/components/satchel/FormHistory.jsm @@ -837,37 +837,34 @@ this.FormHistory = { } } }; stmt.executeAsync(handlers); }, 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; let completedSearches = 0; let searchFailed = false; function validIdentifier(change) { // The identifier is only valid if one of either the guid or the (fieldname/value) are set return Boolean(change.guid) != Boolean(change.fieldname && change.value); } 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": validateSearchData(change, "Remove"); continue; case "update": if (validIdentifier(change)) { validateOpData(change, "Update");
--- a/toolkit/components/satchel/test/unit/test_history_api.js +++ b/toolkit/components/satchel/test/unit/test_history_api.js @@ -400,46 +400,16 @@ add_task(function* () do_check_eq(13, results[3].timesUsed); do_check_eq(230, results[2].firstUsed); do_check_eq(430, results[3].firstUsed); do_check_true(results[2].lastUsed > 600); do_check_true(results[3].lastUsed > 700); 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; } finally { FormHistory._supportsDeletedTable = oldSupportsDeletedTable; dbConnection.asyncClose(do_test_finished); } });
--- a/toolkit/modules/addons/WebNavigation.jsm +++ b/toolkit/modules/addons/WebNavigation.jsm @@ -94,18 +94,21 @@ var Manager = { this.fire("onErrorOccurred", browser, data, {error, url}); } } } }, 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}); } }, onLoad(browser, data) { this.fire("onDOMContentLoaded", browser, data, {url: data.url}); }, @@ -137,19 +140,18 @@ var Manager = { const EVENTS = [ "onBeforeNavigate", "onCommitted", "onDOMContentLoaded", "onCompleted", "onErrorOccurred", "onReferenceFragmentUpdated", - + "onHistoryStateUpdated", // "onCreatedNavigationTarget", - // "onHistoryStateUpdated", ]; var WebNavigation = {}; for (let event of EVENTS) { WebNavigation[event] = { addListener: Manager.addListener.bind(Manager, event), removeListener: Manager.removeListener.bind(Manager, event),
--- a/toolkit/modules/addons/WebNavigationContent.js +++ b/toolkit/modules/addons/WebNavigationContent.js @@ -20,16 +20,29 @@ function loadListener(event) { addEventListener("DOMContentLoaded", loadListener); addMessageListener("Extension:DisableWebNavigation", () => { removeEventListener("DOMContentLoaded", loadListener); }); 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 | Ci.nsIWebProgress.NOTIFY_LOCATION); }, uninit() { if (!docShell) { @@ -43,42 +56,70 @@ var WebProgressListener = { onStateChange: function onStateChange(webProgress, request, stateFlags, status) { let data = { requestURL: request.QueryInterface(Ci.nsIChannel).URI.spec, windowId: webProgress.DOMWindowID, parentWindowId: WebNavigationFrames.getParentWindowId(webProgress.DOMWindow), status, stateFlags, }; + sendAsyncMessage("Extension:StateChange", data); if (webProgress.DOMWindow.top != webProgress.DOMWindow) { let webNav = webProgress.QueryInterface(Ci.nsIWebNavigation); if (!webNav.canGoBack) { // 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); }, QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference]), }; var disabled = false; WebProgressListener.init();
--- a/toolkit/modules/addons/WebNavigationFrames.jsm +++ b/toolkit/modules/addons/WebNavigationFrames.jsm @@ -92,16 +92,18 @@ function findFrame(windowId, rootDocShel return convertDocShellToFrameDetail(docShell); } } return null; } var WebNavigationFrames = { + iterateDocShellTree, + getFrame(docShell, frameId) { if (frameId == 0) { return convertDocShellToFrameDetail(docShell); } return findFrame(frameId, docShell); },