import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo

This commit is contained in:
Roy Tam 2018-01-19 03:59:58 +08:00
commit dcd9973243
150858 changed files with 23884658 additions and 0 deletions

View file

@ -0,0 +1,199 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
html, body {
height: 100%;
width: 100%;
}
h2, h3, h4 {
margin-bottom: 10px;
}
button {
padding-left: 20px;
padding-right: 20px;
min-width: 100px;
margin: 0 4px;
}
/* Category panels */
.category {
display: flex;
flex-direction: row;
align-items: center;
}
.category-name {
cursor: default;
}
.app {
height: 100%;
width: 100%;
display: flex;
flex-direction: row;
}
.main-content {
flex: 1;
}
.panel {
max-width: 800px;
}
/* Targets */
.targets {
margin-bottom: 35px;
}
.target-list {
margin: 0;
padding: 0;
}
.target-container {
margin-top: 5px;
min-height: 34px;
display: flex;
flex-direction: row;
align-items: start;
}
.target-icon {
height: 24px;
margin: 0 5px 0 0;
}
.target-icon:not([src]) {
display: none;
}
.inverted-icons .target-icon {
filter: invert(30%);
}
.target {
flex: 1;
margin-top: 2px;
/* This is silly: https://bugzilla.mozilla.org/show_bug.cgi?id=1086218#c4. */
min-width: 0;
}
.target-details {
margin: 0;
padding: 0;
list-style-type: none
}
.target-detail {
display: flex;
font-size: 12px;
margin-top: 7px;
margin-bottom: 7px;
}
.target-detail a {
cursor: pointer;
white-space: nowrap;
}
.target-detail strong {
white-space: nowrap;
}
.target-detail span {
/* Truncate items that are too long (e.g. URLs that would break the UI). */
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.target-detail > :not(:first-child) {
margin-left: 8px;
}
.target-status {
box-sizing: border-box;
display: inline-block;
min-width: 50px;
margin: 4px 5px 0 0;
padding: 2px;
border-width: 1px;
border-style: solid;
font-size: 0.6em;
text-align: center;
}
.target-status-stopped {
border-color: grey;
background-color: lightgrey;
}
.target-status-running {
border-color: limegreen;
background-color: palegreen;
}
.target-name {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.addons-controls {
display: flex;
flex-direction: row;
}
.addons-install-error {
background-color: #f3b0b0;
padding: 5px 10px;
margin: 5px 4px 5px 0px;
}
.service-worker-disabled .warning,
.addons-install-error .warning {
background-image: url(chrome://devtools/skin/images/alerticon-warning.png);
background-size: 13px 12px;
margin-right: 10px;
display: inline-block;
width: 13px;
height: 12px;
}
@media (min-resolution: 1.1dppx) {
.service-worker-disabled .warning,
.addons-install-error .warning {
background-image: url(chrome://devtools/skin/images/alerticon-warning@2x.png);
}
}
.addons-options {
flex: 1;
}
.addons-debugging-label {
display: inline-block;
margin-inline-end: 1ch;
}
.error-page {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
width: 100%;
height: 100%;
}
.error-page .error-page-details {
color: gray;
}

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- 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/. -->
<!DOCTYPE html [
<!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd"> %htmlDTD;
<!ENTITY % toolboxDTD SYSTEM "chrome://devtools/locale/toolbox.dtd"> %toolboxDTD;
<!ENTITY % aboutdebuggingDTD SYSTEM "chrome://devtools/locale/aboutdebugging.dtd"> %aboutdebuggingDTD;
]>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>&aboutDebugging.fullTitle;</title>
<link rel="stylesheet" href="chrome://global/skin/in-content/common.css" type="text/css"/>
<link rel="stylesheet" href="chrome://devtools/content/aboutdebugging/aboutdebugging.css" type="text/css"/>
<script type="application/javascript" src="resource://devtools/client/shared/vendor/react.js"></script>
<script type="application/javascript;version=1.8" src="chrome://devtools/content/aboutdebugging/initializer.js"></script>
</head>
<body id="body">
</body>
</html>

View file

@ -0,0 +1,111 @@
/* 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 */
"use strict";
const { createFactory, createClass, DOM: dom, PropTypes } =
require("devtools/client/shared/vendor/react");
const Services = require("Services");
const PanelMenu = createFactory(require("./panel-menu"));
loader.lazyGetter(this, "AddonsPanel",
() => createFactory(require("./addons/panel")));
loader.lazyGetter(this, "TabsPanel",
() => createFactory(require("./tabs/panel")));
loader.lazyGetter(this, "WorkersPanel",
() => createFactory(require("./workers/panel")));
loader.lazyRequireGetter(this, "DebuggerClient",
"devtools/shared/client/main", true);
loader.lazyRequireGetter(this, "Telemetry",
"devtools/client/shared/telemetry");
const Strings = Services.strings.createBundle(
"chrome://devtools/locale/aboutdebugging.properties");
const panels = [{
id: "addons",
name: Strings.GetStringFromName("addons"),
icon: "chrome://devtools/skin/images/debugging-addons.svg",
component: AddonsPanel
}, {
id: "tabs",
name: Strings.GetStringFromName("tabs"),
icon: "chrome://devtools/skin/images/debugging-tabs.svg",
component: TabsPanel
}, {
id: "workers",
name: Strings.GetStringFromName("workers"),
icon: "chrome://devtools/skin/images/debugging-workers.svg",
component: WorkersPanel
}];
const defaultPanelId = "addons";
module.exports = createClass({
displayName: "AboutDebuggingApp",
propTypes: {
client: PropTypes.instanceOf(DebuggerClient).isRequired,
telemetry: PropTypes.instanceOf(Telemetry).isRequired
},
getInitialState() {
return {
selectedPanelId: defaultPanelId
};
},
componentDidMount() {
window.addEventListener("hashchange", this.onHashChange);
this.onHashChange();
this.props.telemetry.toolOpened("aboutdebugging");
},
componentWillUnmount() {
window.removeEventListener("hashchange", this.onHashChange);
this.props.telemetry.toolClosed("aboutdebugging");
this.props.telemetry.destroy();
},
onHashChange() {
this.setState({
selectedPanelId: window.location.hash.substr(1) || defaultPanelId
});
},
selectPanel(panelId) {
window.location.hash = "#" + panelId;
},
render() {
let { client } = this.props;
let { selectedPanelId } = this.state;
let selectPanel = this.selectPanel;
let selectedPanel = panels.find(p => p.id == selectedPanelId);
let panel;
if (selectedPanel) {
panel = selectedPanel.component({ client, id: selectedPanel.id });
} else {
panel = (
dom.div({ className: "error-page" },
dom.h1({ className: "header-name" },
Strings.GetStringFromName("pageNotFound")
),
dom.h4({ className: "error-page-details" },
Strings.formatStringFromName("doesNotExist", [selectedPanelId], 1))
)
);
}
return dom.div({ className: "app" },
PanelMenu({ panels, selectedPanelId, selectPanel }),
dom.div({ className: "main-content" }, panel)
);
}
});

View file

@ -0,0 +1,97 @@
/* 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 */
/* globals AddonManager */
"use strict";
loader.lazyImporter(this, "AddonManager",
"resource://gre/modules/AddonManager.jsm");
const { Cc, Ci } = require("chrome");
const { createFactory, createClass, DOM: dom, PropTypes } =
require("devtools/client/shared/vendor/react");
const Services = require("Services");
const AddonsInstallError = createFactory(require("./install-error"));
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";
module.exports = createClass({
displayName: "AddonsControls",
propTypes: {
debugDisabled: PropTypes.bool
},
getInitialState() {
return {
installError: null,
};
},
onEnableAddonDebuggingChange(event) {
let enabled = event.target.checked;
Services.prefs.setBoolPref("devtools.chrome.enabled", enabled);
Services.prefs.setBoolPref("devtools.debugger.remote-enabled", enabled);
},
loadAddonFromFile() {
this.setState({ installError: null });
let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
fp.init(window,
Strings.GetStringFromName("selectAddonFromFile2"),
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;
}
AddonManager.installTemporaryAddon(file)
.catch(e => {
console.error(e);
this.setState({ installError: e.message });
});
},
render() {
let { debugDisabled } = this.props;
return dom.div({ className: "addons-top" },
dom.div({ className: "addons-controls" },
dom.div({ className: "addons-options toggle-container-with-text" },
dom.input({
id: "enable-addon-debugging",
type: "checkbox",
checked: !debugDisabled,
onChange: this.onEnableAddonDebuggingChange,
}),
dom.label({
className: "addons-debugging-label",
htmlFor: "enable-addon-debugging",
title: Strings.GetStringFromName("addonDebugging.tooltip")
}, Strings.GetStringFromName("addonDebugging.label")),
"(",
dom.a({ href: MORE_INFO_URL, target: "_blank" },
Strings.GetStringFromName("moreInfo")),
")"
),
dom.button({
id: "load-addon-from-file",
onClick: this.loadAddonFromFile,
}, Strings.GetStringFromName("loadTemporaryAddon"))
),
AddonsInstallError({ error: this.state.installError }));
}
});

View file

@ -0,0 +1,26 @@
/* 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 */
"use strict";
const { createClass, DOM: dom, PropTypes } = require("devtools/client/shared/vendor/react");
module.exports = createClass({
displayName: "AddonsInstallError",
propTypes: {
error: PropTypes.string
},
render() {
if (!this.props.error) {
return null;
}
let text = `There was an error during installation: ${this.props.error}`;
return dom.div({ className: "addons-install-error" },
dom.div({ className: "warning" }),
dom.span({}, text));
}
});

View file

@ -0,0 +1,10 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
DevToolsModules(
'controls.js',
'install-error.js',
'panel.js',
'target.js',
)

View file

@ -0,0 +1,146 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { AddonManager } = require("resource://gre/modules/AddonManager.jsm");
const { createFactory, createClass, DOM: dom, PropTypes } =
require("devtools/client/shared/vendor/react");
const Services = require("Services");
const AddonsControls = createFactory(require("./controls"));
const AddonTarget = createFactory(require("./target"));
const PanelHeader = createFactory(require("../panel-header"));
const TargetList = createFactory(require("../target-list"));
loader.lazyRequireGetter(this, "DebuggerClient",
"devtools/shared/client/main", true);
const Strings = Services.strings.createBundle(
"chrome://devtools/locale/aboutdebugging.properties");
const ExtensionIcon = "chrome://mozapps/skin/extensions/extensionGeneric.svg";
const CHROME_ENABLED_PREF = "devtools.chrome.enabled";
const REMOTE_ENABLED_PREF = "devtools.debugger.remote-enabled";
module.exports = createClass({
displayName: "AddonsPanel",
propTypes: {
client: PropTypes.instanceOf(DebuggerClient).isRequired,
id: PropTypes.string.isRequired
},
getInitialState() {
return {
extensions: [],
debugDisabled: false,
};
},
componentDidMount() {
AddonManager.addAddonListener(this);
Services.prefs.addObserver(CHROME_ENABLED_PREF,
this.updateDebugStatus, false);
Services.prefs.addObserver(REMOTE_ENABLED_PREF,
this.updateDebugStatus, false);
this.updateDebugStatus();
this.updateAddonsList();
},
componentWillUnmount() {
AddonManager.removeAddonListener(this);
Services.prefs.removeObserver(CHROME_ENABLED_PREF,
this.updateDebugStatus);
Services.prefs.removeObserver(REMOTE_ENABLED_PREF,
this.updateDebugStatus);
},
updateDebugStatus() {
let debugDisabled =
!Services.prefs.getBoolPref(CHROME_ENABLED_PREF) ||
!Services.prefs.getBoolPref(REMOTE_ENABLED_PREF);
this.setState({ debugDisabled });
},
updateAddonsList() {
this.props.client.listAddons()
.then(({addons}) => {
let extensions = addons.filter(addon => addon.debuggable).map(addon => {
return {
name: addon.name,
icon: addon.iconURL || ExtensionIcon,
addonID: addon.id,
addonActor: addon.actor,
temporarilyInstalled: addon.temporarilyInstalled
};
});
this.setState({ extensions });
}, error => {
throw new Error("Client error while listing addons: " + error);
});
},
/**
* Mandatory callback as AddonManager listener.
*/
onInstalled() {
this.updateAddonsList();
},
/**
* Mandatory callback as AddonManager listener.
*/
onUninstalled() {
this.updateAddonsList();
},
/**
* Mandatory callback as AddonManager listener.
*/
onEnabled() {
this.updateAddonsList();
},
/**
* Mandatory callback as AddonManager listener.
*/
onDisabled() {
this.updateAddonsList();
},
render() {
let { client, id } = this.props;
let { debugDisabled, extensions: targets } = this.state;
let name = Strings.GetStringFromName("extensions");
let targetClass = AddonTarget;
return dom.div({
id: id + "-panel",
className: "panel",
role: "tabpanel",
"aria-labelledby": id + "-header"
},
PanelHeader({
id: id + "-header",
name: Strings.GetStringFromName("addons")
}),
AddonsControls({ debugDisabled }),
dom.div({ id: "addons" },
TargetList({
id: "extensions",
name,
targets,
client,
debugDisabled,
targetClass,
sort: true
})
));
}
});

View file

@ -0,0 +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/. */
/* eslint-env browser */
"use strict";
const { createClass, DOM: dom, PropTypes } =
require("devtools/client/shared/vendor/react");
const { debugAddon } = require("../../modules/addon");
const Services = require("Services");
loader.lazyImporter(this, "BrowserToolboxProcess",
"resource://devtools/client/framework/ToolboxProcess.jsm");
loader.lazyRequireGetter(this, "DebuggerClient",
"devtools/shared/client/main", true);
const Strings = Services.strings.createBundle(
"chrome://devtools/locale/aboutdebugging.properties");
module.exports = createClass({
displayName: "AddonTarget",
propTypes: {
client: PropTypes.instanceOf(DebuggerClient).isRequired,
debugDisabled: PropTypes.bool,
target: PropTypes.shape({
addonActor: PropTypes.string.isRequired,
addonID: PropTypes.string.isRequired,
icon: PropTypes.string,
name: PropTypes.string.isRequired,
temporarilyInstalled: PropTypes.bool
}).isRequired
},
debug() {
let { target } = this.props;
debugAddon(target.addonID);
},
reload() {
let { client, target } = this.props;
// This function sometimes returns a partial promise that only
// implements then().
client.request({
to: target.addonActor,
type: "reload"
}).then(() => {}, error => {
throw new Error(
"Error reloading addon " + target.addonID + ": " + error);
});
},
render() {
let { target, debugDisabled } = this.props;
// Only temporarily installed add-ons can be reloaded.
const canBeReloaded = target.temporarilyInstalled;
return dom.li({ className: "target-container" },
dom.img({
className: "target-icon",
role: "presentation",
src: target.icon
}),
dom.div({ className: "target" },
dom.div({ className: "target-name", title: target.name }, target.name)
),
dom.button({
className: "debug-button",
onClick: this.debug,
disabled: debugDisabled,
}, Strings.GetStringFromName("debug")),
dom.button({
className: "reload-button",
onClick: this.reload,
disabled: !canBeReloaded,
title: !canBeReloaded ?
Strings.GetStringFromName("reloadDisabledTooltip") : ""
}, Strings.GetStringFromName("reload"))
);
}
});

View file

@ -0,0 +1,17 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
DIRS += [
'addons',
'tabs',
'workers',
]
DevToolsModules(
'aboutdebugging.js',
'panel-header.js',
'panel-menu-entry.js',
'panel-menu.js',
'target-list.js',
)

View file

@ -0,0 +1,24 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { createClass, DOM: dom, PropTypes } =
require("devtools/client/shared/vendor/react");
module.exports = createClass({
displayName: "PanelHeader",
propTypes: {
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired
},
render() {
let { name, id } = this.props;
return dom.div({ className: "header" },
dom.h1({ id, className: "header-name" }, name));
},
});

View file

@ -0,0 +1,48 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { createClass, DOM: dom, PropTypes } =
require("devtools/client/shared/vendor/react");
module.exports = createClass({
displayName: "PanelMenuEntry",
propTypes: {
icon: PropTypes.string.isRequired,
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
selected: PropTypes.bool,
selectPanel: PropTypes.func.isRequired
},
onClick() {
this.props.selectPanel(this.props.id);
},
onKeyDown(event) {
if ([" ", "Enter"].includes(event.key)) {
this.props.selectPanel(this.props.id);
}
},
render() {
let { id, name, icon, selected } = this.props;
// Here .category, .category-icon, .category-name classnames are used to
// apply common styles defined.
let className = "category" + (selected ? " selected" : "");
return dom.div({
"aria-selected": selected,
"aria-controls": id + "-panel",
className,
onClick: this.onClick,
onKeyDown: this.onKeyDown,
tabIndex: "0",
role: "tab" },
dom.img({ className: "category-icon", src: icon, role: "presentation" }),
dom.div({ className: "category-name" }, name));
}
});

View file

@ -0,0 +1,41 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { createClass, createFactory, DOM: dom, PropTypes } =
require("devtools/client/shared/vendor/react");
const PanelMenuEntry = createFactory(require("./panel-menu-entry"));
module.exports = createClass({
displayName: "PanelMenu",
propTypes: {
panels: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
icon: PropTypes.string.isRequired,
component: PropTypes.func.isRequired
})).isRequired,
selectPanel: PropTypes.func.isRequired,
selectedPanelId: PropTypes.string
},
render() {
let { panels, selectedPanelId, selectPanel } = this.props;
let panelLinks = panels.map(({ id, name, icon }) => {
let selected = id == selectedPanelId;
return PanelMenuEntry({
id,
name,
icon,
selected,
selectPanel
});
});
// "categories" id used for styling purposes
return dom.div({ id: "categories", role: "tablist" }, panelLinks);
},
});

View file

@ -0,0 +1,8 @@
# 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/.
DevToolsModules(
'panel.js',
'target.js',
)

View file

@ -0,0 +1,98 @@
/* 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 */
"use strict";
const { createClass, createFactory, DOM: dom, PropTypes } =
require("devtools/client/shared/vendor/react");
const Services = require("Services");
const PanelHeader = createFactory(require("../panel-header"));
const TargetList = createFactory(require("../target-list"));
const TabTarget = createFactory(require("./target"));
loader.lazyRequireGetter(this, "DebuggerClient",
"devtools/shared/client/main", true);
const Strings = Services.strings.createBundle(
"chrome://devtools/locale/aboutdebugging.properties");
module.exports = createClass({
displayName: "TabsPanel",
propTypes: {
client: PropTypes.instanceOf(DebuggerClient).isRequired,
id: PropTypes.string.isRequired
},
getInitialState() {
return {
tabs: []
};
},
componentDidMount() {
let { client } = this.props;
client.addListener("tabListChanged", this.update);
this.update();
},
componentWillUnmount() {
let { client } = this.props;
client.removeListener("tabListChanged", this.update);
},
update() {
this.props.client.mainRoot.listTabs().then(({ tabs }) => {
// Filter out closed tabs (represented as `null`).
tabs = tabs.filter(tab => !!tab);
tabs.forEach(tab => {
// FIXME Also try to fetch low-res favicon. But we should use actor
// support for this to get the high-res one (bug 1061654).
let url = new URL(tab.url);
if (url.protocol.startsWith("http")) {
let prePath = url.origin;
let idx = url.pathname.lastIndexOf("/");
if (idx === -1) {
prePath += url.pathname;
} else {
prePath += url.pathname.substr(0, idx);
}
tab.icon = prePath + "/favicon.ico";
} else {
tab.icon = "chrome://devtools/skin/images/globe.svg";
}
});
this.setState({ tabs });
});
},
render() {
let { client, id } = this.props;
let { tabs } = this.state;
return dom.div({
id: id + "-panel",
className: "panel",
role: "tabpanel",
"aria-labelledby": id + "-header"
},
PanelHeader({
id: id + "-header",
name: Strings.GetStringFromName("tabs")
}),
dom.div({},
TargetList({
client,
id: "tabs",
name: Strings.GetStringFromName("tabs"),
sort: false,
targetClass: TabTarget,
targets: tabs
})
));
}
});

View file

@ -0,0 +1,53 @@
/* 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 */
"use strict";
const { createClass, DOM: dom, PropTypes } =
require("devtools/client/shared/vendor/react");
const Services = require("Services");
const Strings = Services.strings.createBundle(
"chrome://devtools/locale/aboutdebugging.properties");
module.exports = createClass({
displayName: "TabTarget",
propTypes: {
target: PropTypes.shape({
icon: PropTypes.string,
outerWindowID: PropTypes.number.isRequired,
title: PropTypes.string,
url: PropTypes.string.isRequired
}).isRequired
},
debug() {
let { target } = this.props;
window.open("about:devtools-toolbox?type=tab&id=" + target.outerWindowID);
},
render() {
let { target } = this.props;
return dom.div({ className: "target-container" },
dom.img({
className: "target-icon",
role: "presentation",
src: target.icon
}),
dom.div({ className: "target" },
// If the title is empty, display the url instead.
dom.div({ className: "target-name", title: target.url },
target.title || target.url)
),
dom.button({
className: "debug-button",
onClick: this.debug,
}, Strings.GetStringFromName("debug"))
);
}
});

View file

@ -0,0 +1,56 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { createClass, DOM: dom, PropTypes } =
require("devtools/client/shared/vendor/react");
const Services = require("Services");
loader.lazyRequireGetter(this, "DebuggerClient",
"devtools/shared/client/main", true);
const Strings = Services.strings.createBundle(
"chrome://devtools/locale/aboutdebugging.properties");
const LocaleCompare = (a, b) => {
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
};
module.exports = createClass({
displayName: "TargetList",
propTypes: {
client: PropTypes.instanceOf(DebuggerClient).isRequired,
debugDisabled: PropTypes.bool,
error: PropTypes.node,
id: PropTypes.string.isRequired,
name: PropTypes.string,
sort: PropTypes.bool,
targetClass: PropTypes.func.isRequired,
targets: PropTypes.arrayOf(PropTypes.object).isRequired
},
render() {
let { client, debugDisabled, error, targetClass, targets, sort } = this.props;
if (sort) {
targets = targets.sort(LocaleCompare);
}
targets = targets.map(target => {
return targetClass({ client, target, debugDisabled });
});
let content = "";
if (error) {
content = error;
} else if (targets.length > 0) {
content = dom.ul({ className: "target-list" }, targets);
} else {
content = dom.p(null, Strings.GetStringFromName("nothing"));
}
return dom.div({ id: this.props.id, className: "targets" },
dom.h2(null, this.props.name), content);
},
});

View file

@ -0,0 +1,9 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
DevToolsModules(
'panel.js',
'service-worker-target.js',
'target.js',
)

View file

@ -0,0 +1,193 @@
/* 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/. */
/* globals window */
"use strict";
loader.lazyImporter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
const { Ci } = require("chrome");
const { createClass, createFactory, DOM: dom, PropTypes } =
require("devtools/client/shared/vendor/react");
const { getWorkerForms } = require("../../modules/worker");
const Services = require("Services");
const PanelHeader = createFactory(require("../panel-header"));
const TargetList = createFactory(require("../target-list"));
const WorkerTarget = createFactory(require("./target"));
const ServiceWorkerTarget = createFactory(require("./service-worker-target"));
loader.lazyImporter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
loader.lazyRequireGetter(this, "DebuggerClient",
"devtools/shared/client/main", true);
const Strings = Services.strings.createBundle(
"chrome://devtools/locale/aboutdebugging.properties");
const WorkerIcon = "chrome://devtools/skin/images/debugging-workers.svg";
const MORE_INFO_URL = "https://developer.mozilla.org/en-US/docs/Tools/about%3Adebugging";
module.exports = createClass({
displayName: "WorkersPanel",
propTypes: {
client: PropTypes.instanceOf(DebuggerClient).isRequired,
id: PropTypes.string.isRequired
},
getInitialState() {
return {
workers: {
service: [],
shared: [],
other: []
}
};
},
componentDidMount() {
let client = this.props.client;
client.addListener("workerListChanged", this.update);
client.addListener("serviceWorkerRegistrationListChanged", this.update);
client.addListener("processListChanged", this.update);
client.addListener("registration-changed", 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);
client.removeListener("registration-changed", this.update);
},
update() {
let workers = this.getInitialState().workers;
getWorkerForms(this.props.client).then(forms => {
forms.registrations.forEach(form => {
workers.service.push({
icon: WorkerIcon,
name: form.url,
url: form.url,
scope: form.scope,
registrationActor: form.actor,
active: form.active
});
});
forms.workers.forEach(form => {
let worker = {
icon: WorkerIcon,
name: form.url,
url: form.url,
workerActor: form.actor
};
switch (form.type) {
case Ci.nsIWorkerDebugger.TYPE_SERVICE:
let registration = this.getRegistrationForWorker(form, workers.service);
if (registration) {
// 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;
} else {
// If a service worker registration could not be found, this means we are in
// e10s, and registrations are not forwarded to other processes until they
// reach the activated state. Augment the worker as a registration worker to
// display it in aboutdebugging.
worker.scope = form.scope;
worker.active = false;
workers.service.push(worker);
}
break;
case Ci.nsIWorkerDebugger.TYPE_SHARED:
workers.shared.push(worker);
break;
default:
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 });
});
},
getRegistrationForWorker(form, registrations) {
for (let registration of registrations) {
if (registration.scope === form.scope) {
return registration;
}
}
return null;
},
render() {
let { client, id } = this.props;
let { workers } = this.state;
let isWindowPrivate = PrivateBrowsingUtils.isContentWindowPrivate(window);
let isPrivateBrowsingMode = PrivateBrowsingUtils.permanentPrivateBrowsing;
let isServiceWorkerDisabled = !Services.prefs
.getBoolPref("dom.serviceWorkers.enabled");
let errorMsg = isWindowPrivate || isPrivateBrowsingMode ||
isServiceWorkerDisabled ?
dom.p({ className: "service-worker-disabled" },
dom.div({ className: "warning" }),
Strings.GetStringFromName("configurationIsNotCompatible"),
" (",
dom.a({ href: MORE_INFO_URL, target: "_blank" },
Strings.GetStringFromName("moreInfo")),
")"
) : "";
return dom.div({
id: id + "-panel",
className: "panel",
role: "tabpanel",
"aria-labelledby": id + "-header"
},
PanelHeader({
id: id + "-header",
name: Strings.GetStringFromName("workers")
}),
dom.div({ id: "workers", className: "inverted-icons" },
TargetList({
client,
error: errorMsg,
id: "service-workers",
name: Strings.GetStringFromName("serviceWorkers"),
sort: true,
targetClass: ServiceWorkerTarget,
targets: workers.service
}),
TargetList({
client,
id: "shared-workers",
name: Strings.GetStringFromName("sharedWorkers"),
sort: true,
targetClass: WorkerTarget,
targets: workers.shared
}),
TargetList({
client,
id: "other-workers",
name: Strings.GetStringFromName("otherWorkers"),
sort: true,
targetClass: WorkerTarget,
targets: workers.other
})
));
}
});

View file

@ -0,0 +1,231 @@
/* 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 */
"use strict";
const { createClass, DOM: dom, PropTypes } =
require("devtools/client/shared/vendor/react");
const { debugWorker } = require("../../modules/worker");
const Services = require("Services");
loader.lazyRequireGetter(this, "DebuggerClient",
"devtools/shared/client/main", true);
const Strings = Services.strings.createBundle(
"chrome://devtools/locale/aboutdebugging.properties");
module.exports = createClass({
displayName: "ServiceWorkerTarget",
propTypes: {
client: PropTypes.instanceOf(DebuggerClient).isRequired,
debugDisabled: PropTypes.bool,
target: PropTypes.shape({
active: PropTypes.bool,
icon: PropTypes.string,
name: PropTypes.string.isRequired,
url: PropTypes.string,
scope: PropTypes.string.isRequired,
// registrationActor can be missing in e10s.
registrationActor: PropTypes.string,
workerActor: PropTypes.string
}).isRequired
},
getInitialState() {
return {
pushSubscription: null
};
},
componentDidMount() {
let { client } = this.props;
client.addListener("push-subscription-modified", this.onPushSubscriptionModified);
this.updatePushSubscription();
},
componentDidUpdate(oldProps, oldState) {
let wasActive = oldProps.target.active;
if (!wasActive && this.isActive()) {
// While the service worker isn't active, any calls to `updatePushSubscription`
// won't succeed. If we just became active, make sure we didn't miss a push
// subscription change by updating it now.
this.updatePushSubscription();
}
},
componentWillUnmount() {
let { client } = this.props;
client.removeListener("push-subscription-modified", this.onPushSubscriptionModified);
},
debug() {
if (!this.isRunning()) {
// If the worker is not running, we can't debug it.
return;
}
let { client, target } = this.props;
debugWorker(client, target.workerActor);
},
push() {
if (!this.isActive() || !this.isRunning()) {
// If the worker is not running, we can't push to it.
// If the worker is not active, the registration might be unavailable and the
// push will not succeed.
return;
}
let { client, target } = this.props;
client.request({
to: target.workerActor,
type: "push"
});
},
start() {
if (!this.isActive() || this.isRunning()) {
// If the worker is not active or if it is already running, we can't start it.
return;
}
let { client, target } = this.props;
client.request({
to: target.registrationActor,
type: "start"
});
},
unregister() {
let { client, target } = this.props;
client.request({
to: target.registrationActor,
type: "unregister"
});
},
onPushSubscriptionModified(type, data) {
let { target } = this.props;
if (data.from === target.registrationActor) {
this.updatePushSubscription();
}
},
updatePushSubscription() {
if (!this.props.target.registrationActor) {
// A valid registrationActor is needed to retrieve the push subscription.
return;
}
let { client, target } = this.props;
client.request({
to: target.registrationActor,
type: "getPushSubscription"
}, ({ subscription }) => {
this.setState({ pushSubscription: subscription });
});
},
isRunning() {
// We know the target is running if it has a worker actor.
return !!this.props.target.workerActor;
},
isActive() {
return this.props.target.active;
},
getServiceWorkerStatus() {
if (this.isActive() && this.isRunning()) {
return "running";
} else if (this.isActive()) {
return "stopped";
}
// We cannot get service worker registrations unless the registration is in
// ACTIVE state. Unable to know the actual state ("installing", "waiting"), we
// display a custom state "registering" for now. See Bug 1153292.
return "registering";
},
renderButtons() {
let pushButton = dom.button({
className: "push-button",
onClick: this.push
}, Strings.GetStringFromName("push"));
let debugButton = dom.button({
className: "debug-button",
onClick: this.debug,
disabled: this.props.debugDisabled
}, Strings.GetStringFromName("debug"));
let startButton = dom.button({
className: "start-button",
onClick: this.start,
}, Strings.GetStringFromName("start"));
if (this.isRunning()) {
if (this.isActive()) {
return [pushButton, debugButton];
}
// Only debug button is available if the service worker is not active.
return debugButton;
}
return startButton;
},
renderUnregisterLink() {
if (!this.isActive()) {
// If not active, there might be no registrationActor available.
return null;
}
return dom.a({
onClick: this.unregister,
className: "unregister-link"
}, Strings.GetStringFromName("unregister"));
},
render() {
let { target } = this.props;
let { pushSubscription } = this.state;
let status = this.getServiceWorkerStatus();
return dom.div({ className: "target-container" },
dom.img({
className: "target-icon",
role: "presentation",
src: target.icon
}),
dom.span({ className: `target-status target-status-${status}` },
Strings.GetStringFromName(status)),
dom.div({ className: "target" },
dom.div({ className: "target-name", title: target.name }, target.name),
dom.ul({ className: "target-details" },
(pushSubscription ?
dom.li({ className: "target-detail" },
dom.strong(null, Strings.GetStringFromName("pushService")),
dom.span({
className: "service-worker-push-url",
title: pushSubscription.endpoint
}, pushSubscription.endpoint)) :
null
),
dom.li({ className: "target-detail" },
dom.strong(null, Strings.GetStringFromName("scope")),
dom.span({
className: "service-worker-scope",
title: target.scope
}, target.scope),
this.renderUnregisterLink()
)
)
),
this.renderButtons()
);
}
});

View file

@ -0,0 +1,57 @@
/* 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 */
"use strict";
const { createClass, DOM: dom, PropTypes } =
require("devtools/client/shared/vendor/react");
const { debugWorker } = require("../../modules/worker");
const Services = require("Services");
loader.lazyRequireGetter(this, "DebuggerClient",
"devtools/shared/client/main", true);
const Strings = Services.strings.createBundle(
"chrome://devtools/locale/aboutdebugging.properties");
module.exports = createClass({
displayName: "WorkerTarget",
propTypes: {
client: PropTypes.instanceOf(DebuggerClient).isRequired,
debugDisabled: PropTypes.bool,
target: PropTypes.shape({
icon: PropTypes.string,
name: PropTypes.string.isRequired,
workerActor: PropTypes.string
}).isRequired
},
debug() {
let { client, target } = this.props;
debugWorker(client, target.workerActor);
},
render() {
let { target, debugDisabled } = this.props;
return dom.li({ className: "target-container" },
dom.img({
className: "target-icon",
role: "presentation",
src: target.icon
}),
dom.div({ className: "target" },
dom.div({ className: "target-name", title: target.name }, target.name)
),
dom.button({
className: "debug-button",
onClick: this.debug,
disabled: debugDisabled
}, Strings.GetStringFromName("debug"))
);
}
});

View file

@ -0,0 +1,67 @@
/* 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 */
/* globals DebuggerClient, DebuggerServer, Telemetry */
"use strict";
const { loader } = Components.utils.import(
"resource://devtools/shared/Loader.jsm", {});
const { BrowserLoader } = Components.utils.import(
"resource://devtools/client/shared/browser-loader.js", {});
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");
const { require } = BrowserLoader({
baseURI: "resource://devtools/client/aboutdebugging/",
window
});
const { createFactory } = require("devtools/client/shared/vendor/react");
const { render, unmountComponentAtNode } = require("devtools/client/shared/vendor/react-dom");
const AboutDebuggingApp = createFactory(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();
render(AboutDebuggingApp({ client, telemetry }),
document.querySelector("#body"));
});
},
destroy() {
unmountComponentAtNode(document.querySelector("#body"));
this.client.close();
this.client = null;
},
};
window.addEventListener("DOMContentLoaded", function load() {
window.removeEventListener("DOMContentLoaded", load);
AboutDebugging.init();
});
window.addEventListener("unload", function unload() {
window.removeEventListener("unload", unload);
AboutDebugging.destroy();
});

View file

@ -0,0 +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/. */
"use strict";
loader.lazyImporter(this, "BrowserToolboxProcess",
"resource://devtools/client/framework/ToolboxProcess.jsm");
let toolbox = null;
exports.debugAddon = function (addonID) {
if (toolbox) {
toolbox.close();
}
toolbox = BrowserToolboxProcess.init({
addonID,
onClose: () => {
toolbox = null;
}
});
};

View file

@ -0,0 +1,8 @@
# 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/.
DevToolsModules(
'addon.js',
'worker.js',
)

View file

@ -0,0 +1,77 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { Task } = require("devtools/shared/task");
loader.lazyRequireGetter(this, "gDevTools",
"devtools/client/framework/devtools", true);
loader.lazyRequireGetter(this, "TargetFactory",
"devtools/client/framework/target", true);
loader.lazyRequireGetter(this, "Toolbox",
"devtools/client/framework/toolbox", true);
/**
* Open a window-hosted toolbox to debug the worker associated to the provided
* worker actor.
*
* @param {DebuggerClient} client
* @param {Object} workerActor
* worker actor form to debug
*/
exports.debugWorker = function (client, workerActor) {
client.attachWorker(workerActor, (response, workerClient) => {
let workerTarget = TargetFactory.forWorker(workerClient);
gDevTools.showToolbox(workerTarget, "jsdebugger", Toolbox.HostType.WINDOW)
.then(toolbox => {
toolbox.once("destroy", () => workerClient.detach());
});
});
};
/**
* Retrieve all service worker registrations as well as workers from the parent
* and child processes.
*
* @param {DebuggerClient} client
* @return {Object}
* - {Array} registrations
* Array of ServiceWorkerRegistrationActor forms
* - {Array} workers
* Array of WorkerActor forms
*/
exports.getWorkerForms = Task.async(function* (client) {
let registrations = [];
let 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;
}
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);
}
} catch (e) {
// Something went wrong, maybe our client is disconnected?
}
return { registrations, workers };
});

View file

@ -0,0 +1,14 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
DIRS += [
'components',
'modules',
]
BROWSER_CHROME_MANIFESTS += [
'test/browser.ini'
]

View file

@ -0,0 +1,26 @@
"use strict";
module.exports = {
// Extend from the shared list of defined globals for mochitests.
"extends": "../../../.eslintrc.mochitests.js",
// All globals made available in aboutdebugging head.js file.
"globals": {
"AddonManager": true,
"addTab": true,
"assertHasTarget": true,
"CHROME_ROOT": true,
"changeAboutDebuggingHash": true,
"closeAboutDebugging": true,
"getServiceWorkerList": true,
"getSupportsFile": true,
"installAddon": true,
"openAboutDebugging": true,
"openPanel": true,
"removeTab": true,
"uninstallAddon": true,
"unregisterServiceWorker": true,
"waitForInitialAddonList": true,
"waitForMutation": true,
"waitForServiceWorkerRegistered": true
}
};

View file

@ -0,0 +1 @@
this is not valid json

View file

@ -0,0 +1,10 @@
{
"manifest_version": 2,
"name": "test-devtools-webextension-nobg",
"version": "1.0",
"applications": {
"gecko": {
"id": "test-devtools-webextension-nobg@mozilla.org"
}
}
}

View file

@ -0,0 +1,20 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/* eslint-env browser */
/* global browser */
"use strict";
document.body.innerText = "Background Page Body Test Content";
// This function are called from the webconsole test:
// browser_addons_debug_webextension.js
function myWebExtensionAddonFunction() { // eslint-disable-line no-unused-vars
console.log("Background page function called", browser.runtime.getManifest());
}
function myWebExtensionShowPopup() { // eslint-disable-line no-unused-vars
console.log("readyForOpenPopup");
}

View file

@ -0,0 +1,17 @@
{
"manifest_version": 2,
"name": "test-devtools-webextension",
"version": "1.0",
"applications": {
"gecko": {
"id": "test-devtools-webextension@mozilla.org"
}
},
"background": {
"scripts": ["bg.js"]
},
"browser_action": {
"default_title": "WebExtension Popup Debugging",
"default_popup": "popup.html"
}
}

View file

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="popup.js"></script>
</head>
<body>
Background Page Body Test Content
</body>
</html>

View file

@ -0,0 +1,13 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/* eslint-env browser */
/* global browser */
"use strict";
// This function is called from the webconsole test:
// browser_addons_debug_webextension.js
function myWebExtensionPopupAddonFunction() { // eslint-disable-line no-unused-vars
console.log("Popup page function called", browser.runtime.getManifest());
}

View file

@ -0,0 +1,22 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/* eslint-env browser */
/* exported startup, shutdown, install, uninstall */
"use strict";
Components.utils.import("resource://gre/modules/Services.jsm");
// This function is called from the webconsole test:
// browser_addons_debug_bootstrapped.js
function myBootstrapAddonFunction() { // eslint-disable-line no-unused-vars
Services.obs.notifyObservers(null, "addon-console-works", null);
}
function startup() {
Services.obs.notifyObservers(null, "test-devtools", null);
}
function shutdown() {}
function install() {}
function uninstall() {}

View file

@ -0,0 +1,26 @@
<?xml version="1.0"?>
<!--
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
<Description about="urn:mozilla:install-manifest"
em:id="test-devtools@mozilla.org"
em:name="test-devtools"
em:version="1.0"
em:type="2"
em:creator="Mozilla">
<em:bootstrap>true</em:bootstrap>
<em:targetApplication>
<Description>
<em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
<em:minVersion>44.0a1</em:minVersion>
<em:maxVersion>*</em:maxVersion>
</Description>
</em:targetApplication>
</Description>
</RDF>

View file

@ -0,0 +1,44 @@
[DEFAULT]
tags = devtools
subsuite = devtools
support-files =
head.js
addons/unpacked/bootstrap.js
addons/unpacked/install.rdf
addons/bad/manifest.json
addons/bug1273184.xpi
addons/test-devtools-webextension/*
addons/test-devtools-webextension-nobg/*
service-workers/delay-sw.html
service-workers/delay-sw.js
service-workers/empty-sw.html
service-workers/empty-sw.js
service-workers/push-sw.html
service-workers/push-sw.js
!/devtools/client/framework/test/shared-head.js
[browser_addons_debug_bootstrapped.js]
[browser_addons_debug_webextension.js]
tags = webextensions
[browser_addons_debug_webextension_inspector.js]
tags = webextensions
[browser_addons_debug_webextension_nobg.js]
tags = webextensions
[browser_addons_debug_webextension_popup.js]
tags = webextensions
[browser_addons_debugging_initial_state.js]
[browser_addons_install.js]
[browser_addons_reload.js]
[browser_addons_toggle_debug.js]
[browser_page_not_found.js]
[browser_service_workers.js]
[browser_service_workers_not_compatible.js]
[browser_service_workers_push.js]
[browser_service_workers_push_service.js]
[browser_service_workers_start.js]
[browser_service_workers_status.js]
[browser_service_workers_timeout.js]
skip-if = true # Bug 1232931
[browser_service_workers_unregister.js]
[browser_tabs.js]
skip-if = true # Bug 1304941

View file

@ -0,0 +1,83 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Avoid test timeouts that can occur while waiting for the "addon-console-works" message.
requestLongerTimeout(2);
const ADDON_ID = "test-devtools@mozilla.org";
const ADDON_NAME = "test-devtools";
const { BrowserToolboxProcess } = Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm", {});
add_task(function* () {
yield new Promise(resolve => {
let options = {"set": [
// Force enabling of addons debugging
["devtools.chrome.enabled", true],
["devtools.debugger.remote-enabled", true],
// Disable security prompt
["devtools.debugger.prompt-connection", false],
// Enable Browser toolbox test script execution via env variable
["devtools.browser-toolbox.allow-unsafe-script", true],
]};
SpecialPowers.pushPrefEnv(options, resolve);
});
let { tab, document } = yield openAboutDebugging("addons");
yield waitForInitialAddonList(document);
yield installAddon({
document,
path: "addons/unpacked/install.rdf",
name: ADDON_NAME,
});
// Retrieve the DEBUG button for the addon
let names = [...document.querySelectorAll("#addons .target-name")];
let name = names.filter(element => element.textContent === ADDON_NAME)[0];
ok(name, "Found the addon in the list");
let targetElement = name.parentNode.parentNode;
let debugBtn = targetElement.querySelector(".debug-button");
ok(debugBtn, "Found its debug button");
// Wait for a notification sent by a script evaluated the test addon via
// the web console.
let onCustomMessage = new Promise(done => {
Services.obs.addObserver(function listener() {
Services.obs.removeObserver(listener, "addon-console-works");
done();
}, "addon-console-works", false);
});
// Be careful, this JS function is going to be executed in the addon toolbox,
// which lives in another process. So do not try to use any scope variable!
let env = Cc["@mozilla.org/process/environment;1"]
.getService(Ci.nsIEnvironment);
let testScript = function () {
/* eslint-disable no-undef */
toolbox.selectTool("webconsole")
.then(console => {
let { jsterm } = console.hud;
return jsterm.execute("myBootstrapAddonFunction()");
})
.then(() => toolbox.destroy());
/* eslint-enable no-undef */
};
env.set("MOZ_TOOLBOX_TEST_SCRIPT", "new " + testScript);
registerCleanupFunction(() => {
env.set("MOZ_TOOLBOX_TEST_SCRIPT", "");
});
let onToolboxClose = BrowserToolboxProcess.once("close");
debugBtn.click();
yield onCustomMessage;
ok(true, "Received the notification message from the bootstrap.js function");
yield onToolboxClose;
ok(true, "Addon toolbox closed");
yield uninstallAddon({document, id: ADDON_ID, name: ADDON_NAME});
yield closeAboutDebugging(tab);
});

View file

@ -0,0 +1,74 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Avoid test timeouts that can occur while waiting for the "addon-console-works" message.
requestLongerTimeout(2);
const ADDON_ID = "test-devtools-webextension@mozilla.org";
const ADDON_NAME = "test-devtools-webextension";
const ADDON_MANIFEST_PATH = "addons/test-devtools-webextension/manifest.json";
const {
BrowserToolboxProcess
} = Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm", {});
/**
* This test file ensures that the webextension addon developer toolbox:
* - when the debug button is clicked on a webextension, the opened toolbox
* has a working webconsole with the background page as default target;
*/
add_task(function* testWebExtensionsToolboxWebConsole() {
let {
tab, document, debugBtn,
} = yield setupTestAboutDebuggingWebExtension(ADDON_NAME, ADDON_MANIFEST_PATH);
// Wait for a notification sent by a script evaluated the test addon via
// the web console.
let onCustomMessage = new Promise(done => {
Services.obs.addObserver(function listener(message, topic) {
let apiMessage = message.wrappedJSObject;
if (!apiMessage.originAttributes ||
apiMessage.originAttributes.addonId != ADDON_ID) {
return;
}
Services.obs.removeObserver(listener, "console-api-log-event");
done(apiMessage.arguments);
}, "console-api-log-event", false);
});
// Be careful, this JS function is going to be executed in the addon toolbox,
// which lives in another process. So do not try to use any scope variable!
let env = Cc["@mozilla.org/process/environment;1"]
.getService(Ci.nsIEnvironment);
let testScript = function () {
/* eslint-disable no-undef */
toolbox.selectTool("webconsole")
.then(console => {
let { jsterm } = console.hud;
return jsterm.execute("myWebExtensionAddonFunction()");
})
.then(() => toolbox.destroy());
/* eslint-enable no-undef */
};
env.set("MOZ_TOOLBOX_TEST_SCRIPT", "new " + testScript);
registerCleanupFunction(() => {
env.set("MOZ_TOOLBOX_TEST_SCRIPT", "");
});
let onToolboxClose = BrowserToolboxProcess.once("close");
debugBtn.click();
let args = yield onCustomMessage;
ok(true, "Received console message from the background page function as expected");
is(args[0], "Background page function called", "Got the expected console message");
is(args[1] && args[1].name, ADDON_NAME,
"Got the expected manifest from WebExtension API");
yield onToolboxClose;
ok(true, "Addon toolbox closed");
yield uninstallAddon({document, id: ADDON_ID, name: ADDON_NAME});
yield closeAboutDebugging(tab);
});

View file

@ -0,0 +1,82 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Avoid test timeouts that can occur while waiting for the "addon-console-works" message.
requestLongerTimeout(2);
const ADDON_ID = "test-devtools-webextension@mozilla.org";
const ADDON_NAME = "test-devtools-webextension";
const ADDON_PATH = "addons/test-devtools-webextension/manifest.json";
const {
BrowserToolboxProcess
} = Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm", {});
/**
* This test file ensures that the webextension addon developer toolbox:
* - the webextension developer toolbox has a working Inspector panel, with the
* background page as default target;
*/
add_task(function* testWebExtensionsToolboxInspector() {
let {
tab, document, debugBtn,
} = yield setupTestAboutDebuggingWebExtension(ADDON_NAME, ADDON_PATH);
// Be careful, this JS function is going to be executed in the addon toolbox,
// which lives in another process. So do not try to use any scope variable!
let env = Cc["@mozilla.org/process/environment;1"]
.getService(Ci.nsIEnvironment);
let testScript = function () {
/* eslint-disable no-undef */
toolbox.selectTool("inspector")
.then(inspector => {
return inspector.walker.querySelector(inspector.walker.rootNode, "body");
})
.then((nodeActor) => {
if (!nodeActor) {
throw new Error("nodeActor not found");
}
dump("Got a nodeActor\n");
if (!(nodeActor.inlineTextChild)) {
throw new Error("inlineTextChild not found");
}
dump("Got a nodeActor with an inline text child\n");
let expectedValue = "Background Page Body Test Content";
let actualValue = nodeActor.inlineTextChild._form.nodeValue;
if (String(actualValue).trim() !== String(expectedValue).trim()) {
throw new Error(
`mismatched inlineTextchild value: "${actualValue}" !== "${expectedValue}"`
);
}
dump("Got the expected inline text content in the selected node\n");
return Promise.resolve();
})
.then(() => toolbox.destroy())
.catch((error) => {
dump("Error while running code in the browser toolbox process:\n");
dump(error + "\n");
dump("stack:\n" + error.stack + "\n");
});
/* eslint-enable no-undef */
};
env.set("MOZ_TOOLBOX_TEST_SCRIPT", "new " + testScript);
registerCleanupFunction(() => {
env.set("MOZ_TOOLBOX_TEST_SCRIPT", "");
});
let onToolboxClose = BrowserToolboxProcess.once("close");
debugBtn.click();
yield onToolboxClose;
ok(true, "Addon toolbox closed");
yield uninstallAddon({document, id: ADDON_ID, name: ADDON_NAME});
yield closeAboutDebugging(tab);
});

View file

@ -0,0 +1,84 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Avoid test timeouts that can occur while waiting for the "addon-console-works" message.
requestLongerTimeout(2);
const ADDON_NOBG_ID = "test-devtools-webextension-nobg@mozilla.org";
const ADDON_NOBG_NAME = "test-devtools-webextension-nobg";
const ADDON_NOBG_PATH = "addons/test-devtools-webextension-nobg/manifest.json";
const {
BrowserToolboxProcess
} = Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm", {});
/**
* This test file ensures that the webextension addon developer toolbox:
* - the webextension developer toolbox is connected to a fallback page when the
* background page is not available (and in the fallback page document body contains
* the expected message, which warns the user that the current page is not a real
* webextension context);
*/
add_task(function* testWebExtensionsToolboxNoBackgroundPage() {
let {
tab, document, debugBtn,
} = yield setupTestAboutDebuggingWebExtension(ADDON_NOBG_NAME, ADDON_NOBG_PATH);
// Be careful, this JS function is going to be executed in the addon toolbox,
// which lives in another process. So do not try to use any scope variable!
let env = Cc["@mozilla.org/process/environment;1"]
.getService(Ci.nsIEnvironment);
let testScript = function () {
/* eslint-disable no-undef */
toolbox.selectTool("inspector")
.then(inspector => {
return inspector.walker.querySelector(inspector.walker.rootNode, "body");
})
.then((nodeActor) => {
if (!nodeActor) {
throw new Error("nodeActor not found");
}
dump("Got a nodeActor\n");
if (!(nodeActor.inlineTextChild)) {
throw new Error("inlineTextChild not found");
}
dump("Got a nodeActor with an inline text child\n");
let expectedValue = "Your addon does not have any document opened yet.";
let actualValue = nodeActor.inlineTextChild._form.nodeValue;
if (actualValue !== expectedValue) {
throw new Error(
`mismatched inlineTextchild value: "${actualValue}" !== "${expectedValue}"`
);
}
dump("Got the expected inline text content in the selected node\n");
return Promise.resolve();
})
.then(() => toolbox.destroy())
.catch((error) => {
dump("Error while running code in the browser toolbox process:\n");
dump(error + "\n");
dump("stack:\n" + error.stack + "\n");
});
/* eslint-enable no-undef */
};
env.set("MOZ_TOOLBOX_TEST_SCRIPT", "new " + testScript);
registerCleanupFunction(() => {
env.set("MOZ_TOOLBOX_TEST_SCRIPT", "");
});
let onToolboxClose = BrowserToolboxProcess.once("close");
debugBtn.click();
yield onToolboxClose;
ok(true, "Addon toolbox closed");
yield uninstallAddon({document, id: ADDON_NOBG_ID, name: ADDON_NOBG_NAME});
yield closeAboutDebugging(tab);
});

View file

@ -0,0 +1,189 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Avoid test timeouts that can occur while waiting for the "addon-console-works" message.
requestLongerTimeout(2);
const ADDON_ID = "test-devtools-webextension@mozilla.org";
const ADDON_NAME = "test-devtools-webextension";
const ADDON_MANIFEST_PATH = "addons/test-devtools-webextension/manifest.json";
const {
BrowserToolboxProcess
} = Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm", {});
/**
* This test file ensures that the webextension addon developer toolbox:
* - when the debug button is clicked on a webextension, the opened toolbox
* has a working webconsole with the background page as default target;
* - the webextension developer toolbox has a working Inspector panel, with the
* background page as default target;
* - the webextension developer toolbox is connected to a fallback page when the
* background page is not available (and in the fallback page document body contains
* the expected message, which warns the user that the current page is not a real
* webextension context);
* - the webextension developer toolbox has a frame list menu and the noautohide toolbar
* toggle button, and they can be used to switch the current target to the extension
* popup page.
*/
/**
* Returns the widget id for an extension with the passed id.
*/
function makeWidgetId(id) {
id = id.toLowerCase();
return id.replace(/[^a-z0-9_-]/g, "_");
}
add_task(function* testWebExtensionsToolboxSwitchToPopup() {
let {
tab, document, debugBtn,
} = yield setupTestAboutDebuggingWebExtension(ADDON_NAME, ADDON_MANIFEST_PATH);
let onReadyForOpenPopup = new Promise(done => {
Services.obs.addObserver(function listener(message, topic) {
let apiMessage = message.wrappedJSObject;
if (!apiMessage.originAttributes ||
apiMessage.originAttributes.addonId != ADDON_ID) {
return;
}
if (apiMessage.arguments[0] == "readyForOpenPopup") {
Services.obs.removeObserver(listener, "console-api-log-event");
done();
}
}, "console-api-log-event", false);
});
// Be careful, this JS function is going to be executed in the addon toolbox,
// which lives in another process. So do not try to use any scope variable!
let env = Cc["@mozilla.org/process/environment;1"]
.getService(Ci.nsIEnvironment);
let testScript = function () {
/* eslint-disable no-undef */
let jsterm;
let popupFramePromise;
toolbox.selectTool("webconsole")
.then(console => {
dump(`Clicking the noautohide button\n`);
toolbox.doc.getElementById("command-button-noautohide").click();
dump(`Clicked the noautohide button\n`);
popupFramePromise = new Promise(resolve => {
let listener = (event, data) => {
if (data.frames.some(({url}) => url && url.endsWith("popup.html"))) {
toolbox.target.off("frame-update", listener);
resolve();
}
};
toolbox.target.on("frame-update", listener);
});
let waitForFrameListUpdate = new Promise((done) => {
toolbox.target.once("frame-update", () => {
done(console);
});
});
jsterm = console.hud.jsterm;
jsterm.execute("myWebExtensionShowPopup()");
// Wait the initial frame update (which list the background page).
return waitForFrameListUpdate;
})
.then((console) => {
// Wait the new frame update (once the extension popup has been opened).
return popupFramePromise;
})
.then(() => {
dump(`Clicking the frame list button\n`);
let btn = toolbox.doc.getElementById("command-button-frames");
let menu = toolbox.showFramesMenu({target: btn});
dump(`Clicked the frame list button\n`);
return menu.once("open").then(() => {
return menu;
});
})
.then(frameMenu => {
let frames = frameMenu.items;
if (frames.length != 2) {
throw Error(`Number of frames found is wrong: ${frames.length} != 2`);
}
let popupFrameBtn = frames.filter((frame) => {
return frame.label.endsWith("popup.html");
}).pop();
if (!popupFrameBtn) {
throw Error("Extension Popup frame not found in the listed frames");
}
let waitForNavigated = toolbox.target.once("navigate");
popupFrameBtn.click();
return waitForNavigated;
})
.then(() => {
return jsterm.execute("myWebExtensionPopupAddonFunction()");
})
.then(() => toolbox.destroy())
.catch((error) => {
dump("Error while running code in the browser toolbox process:\n");
dump(error + "\n");
dump("stack:\n" + error.stack + "\n");
});
/* eslint-enable no-undef */
};
env.set("MOZ_TOOLBOX_TEST_SCRIPT", "new " + testScript);
registerCleanupFunction(() => {
env.set("MOZ_TOOLBOX_TEST_SCRIPT", "");
});
// Wait for a notification sent by a script evaluated the test addon via
// the web console.
let onPopupCustomMessage = new Promise(done => {
Services.obs.addObserver(function listener(message, topic) {
let apiMessage = message.wrappedJSObject;
if (!apiMessage.originAttributes ||
apiMessage.originAttributes.addonId != ADDON_ID) {
return;
}
if (apiMessage.arguments[0] == "Popup page function called") {
Services.obs.removeObserver(listener, "console-api-log-event");
done(apiMessage.arguments);
}
}, "console-api-log-event", false);
});
let onToolboxClose = BrowserToolboxProcess.once("close");
debugBtn.click();
yield onReadyForOpenPopup;
let browserActionId = makeWidgetId(ADDON_ID) + "-browser-action";
let browserActionEl = window.document.getElementById(browserActionId);
ok(browserActionEl, "Got the browserAction button from the browser UI");
browserActionEl.click();
info("Clicked on the browserAction button");
let args = yield onPopupCustomMessage;
ok(true, "Received console message from the popup page function as expected");
is(args[0], "Popup page function called", "Got the expected console message");
is(args[1] && args[1].name, ADDON_NAME,
"Got the expected manifest from WebExtension API");
yield onToolboxClose;
ok(true, "Addon toolbox closed");
yield uninstallAddon({document, id: ADDON_ID, name: ADDON_NAME});
yield closeAboutDebugging(tab);
});

View file

@ -0,0 +1,73 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that addons debugging controls are properly enabled/disabled depending
// on the values of the relevant preferences:
// - devtools.chrome.enabled
// - devtools.debugger.remote-enabled
const ADDON_ID = "test-devtools@mozilla.org";
const ADDON_NAME = "test-devtools";
const TEST_DATA = [
{
chromeEnabled: false,
debuggerRemoteEnable: false,
expected: false,
}, {
chromeEnabled: false,
debuggerRemoteEnable: true,
expected: false,
}, {
chromeEnabled: true,
debuggerRemoteEnable: false,
expected: false,
}, {
chromeEnabled: true,
debuggerRemoteEnable: true,
expected: true,
}
];
add_task(function* () {
for (let testData of TEST_DATA) {
yield testCheckboxState(testData);
}
});
function* testCheckboxState(testData) {
info("Set preferences as defined by the current test data.");
yield new Promise(resolve => {
let options = {"set": [
["devtools.chrome.enabled", testData.chromeEnabled],
["devtools.debugger.remote-enabled", testData.debuggerRemoteEnable],
]};
SpecialPowers.pushPrefEnv(options, resolve);
});
let { tab, document } = yield openAboutDebugging("addons");
yield waitForInitialAddonList(document);
info("Install a test addon.");
yield installAddon({
document,
path: "addons/unpacked/install.rdf",
name: ADDON_NAME,
});
info("Test checkbox checked state.");
let addonDebugCheckbox = document.querySelector("#enable-addon-debugging");
is(addonDebugCheckbox.checked, testData.expected,
"Addons debugging checkbox should be in expected state.");
info("Test debug buttons disabled state.");
let debugButtons = [...document.querySelectorAll("#addons .debug-button")];
ok(debugButtons.every(b => b.disabled != testData.expected),
"Debug buttons should be in the expected state");
info("Uninstall test addon installed earlier.");
yield uninstallAddon({document, id: ADDON_ID, name: ADDON_NAME});
yield closeAboutDebugging(tab);
}

View file

@ -0,0 +1,51 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const ADDON_ID = "test-devtools@mozilla.org";
const ADDON_NAME = "test-devtools";
add_task(function* () {
let { tab, document } = yield openAboutDebugging("addons");
yield waitForInitialAddonList(document);
// Install this add-on, and verify that it appears in the about:debugging UI
yield installAddon({
document,
path: "addons/unpacked/install.rdf",
name: ADDON_NAME,
});
// Install the add-on, and verify that it disappears in the about:debugging UI
yield uninstallAddon({document, id: ADDON_ID, name: ADDON_NAME});
yield closeAboutDebugging(tab);
});
add_task(function* () {
let { tab, document } = yield openAboutDebugging("addons");
yield waitForInitialAddonList(document);
// Start an observer that looks for the install error before
// actually doing the install
let top = document.querySelector(".addons-top");
let promise = waitForMutation(top, { childList: true });
// Mock the file picker to select a test addon
let MockFilePicker = SpecialPowers.MockFilePicker;
MockFilePicker.init(null);
let file = getSupportsFile("addons/bad/manifest.json");
MockFilePicker.returnFiles = [file.file];
// Trigger the file picker by clicking on the button
document.getElementById("load-addon-from-file").click();
// Now wait for the install error to appear.
yield promise;
// And check that it really is there.
let err = document.querySelector(".addons-install-error");
isnot(err, null, "Addon install error message appeared");
yield closeAboutDebugging(tab);
});

View file

@ -0,0 +1,207 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const ADDON_ID = "test-devtools@mozilla.org";
const ADDON_NAME = "test-devtools";
/**
* Returns a promise that resolves when the given add-on event is fired. The
* resolved value is an array of arguments passed for the event.
*/
function promiseAddonEvent(event) {
return new Promise(resolve => {
let listener = {
[event]: function (...args) {
AddonManager.removeAddonListener(listener);
resolve(args);
}
};
AddonManager.addAddonListener(listener);
});
}
function* tearDownAddon(addon) {
const onUninstalled = promiseAddonEvent("onUninstalled");
addon.uninstall();
const [uninstalledAddon] = yield onUninstalled;
is(uninstalledAddon.id, addon.id,
`Add-on was uninstalled: ${uninstalledAddon.id}`);
}
function getReloadButton(document, addonName) {
const names = [...document.querySelectorAll("#addons .target-name")];
const name = names.filter(element => element.textContent === addonName)[0];
ok(name, `Found ${addonName} add-on in the list`);
const targetElement = name.parentNode.parentNode;
const reloadButton = targetElement.querySelector(".reload-button");
info(`Found reload button for ${addonName}`);
return reloadButton;
}
function installAddonWithManager(filePath) {
return new Promise((resolve, reject) => {
AddonManager.getInstallForFile(filePath, install => {
if (!install) {
throw new Error(`An install was not created for ${filePath}`);
}
install.addListener({
onDownloadFailed: reject,
onDownloadCancelled: reject,
onInstallFailed: reject,
onInstallCancelled: reject,
onInstallEnded: resolve
});
install.install();
});
});
}
function getAddonByID(addonId) {
return new Promise(resolve => {
AddonManager.getAddonByID(addonId, addon => resolve(addon));
});
}
/**
* Creates a web extension from scratch in a temporary location.
* The object must be removed when you're finished working with it.
*/
class TempWebExt {
constructor(addonId) {
this.addonId = addonId;
this.tmpDir = FileUtils.getDir("TmpD", ["browser_addons_reload"]);
if (!this.tmpDir.exists()) {
this.tmpDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
}
this.sourceDir = this.tmpDir.clone();
this.sourceDir.append(this.addonId);
if (!this.sourceDir.exists()) {
this.sourceDir.create(Ci.nsIFile.DIRECTORY_TYPE,
FileUtils.PERMS_DIRECTORY);
}
}
writeManifest(manifestData) {
const manifest = this.sourceDir.clone();
manifest.append("manifest.json");
if (manifest.exists()) {
manifest.remove(true);
}
const fos = Cc["@mozilla.org/network/file-output-stream;1"]
.createInstance(Ci.nsIFileOutputStream);
fos.init(manifest,
FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE |
FileUtils.MODE_TRUNCATE,
FileUtils.PERMS_FILE, 0);
const manifestString = JSON.stringify(manifestData);
fos.write(manifestString, manifestString.length);
fos.close();
}
remove() {
return this.tmpDir.remove(true);
}
}
add_task(function* reloadButtonReloadsAddon() {
const { tab, document } = yield openAboutDebugging("addons");
yield waitForInitialAddonList(document);
yield installAddon({
document,
path: "addons/unpacked/install.rdf",
name: ADDON_NAME,
});
const reloadButton = getReloadButton(document, ADDON_NAME);
is(reloadButton.disabled, false, "Reload button should not be disabled");
is(reloadButton.title, "", "Reload button should not have a tooltip");
const onInstalled = promiseAddonEvent("onInstalled");
const onBootstrapInstallCalled = new Promise(done => {
Services.obs.addObserver(function listener() {
Services.obs.removeObserver(listener, ADDON_NAME, false);
info("Add-on was re-installed: " + ADDON_NAME);
done();
}, ADDON_NAME, false);
});
reloadButton.click();
const [reloadedAddon] = yield onInstalled;
is(reloadedAddon.name, ADDON_NAME,
"Add-on was reloaded: " + reloadedAddon.name);
yield onBootstrapInstallCalled;
yield tearDownAddon(reloadedAddon);
yield closeAboutDebugging(tab);
});
add_task(function* reloadButtonRefreshesMetadata() {
const { tab, document } = yield openAboutDebugging("addons");
yield waitForInitialAddonList(document);
const manifestBase = {
"manifest_version": 2,
"name": "Temporary web extension",
"version": "1.0",
"applications": {
"gecko": {
"id": ADDON_ID
}
}
};
const tempExt = new TempWebExt(ADDON_ID);
tempExt.writeManifest(manifestBase);
const onAddonListUpdated = waitForMutation(getAddonList(document),
{ childList: true });
const onInstalled = promiseAddonEvent("onInstalled");
yield AddonManager.installTemporaryAddon(tempExt.sourceDir);
const [addon] = yield onInstalled;
info(`addon installed: ${addon.id}`);
yield onAddonListUpdated;
const newName = "Temporary web extension (updated)";
tempExt.writeManifest(Object.assign({}, manifestBase, {name: newName}));
// Wait for the add-on list to be updated with the reloaded name.
const onReInstall = promiseAddonEvent("onInstalled");
const onAddonReloaded = waitForContentMutation(getAddonList(document));
const reloadButton = getReloadButton(document, manifestBase.name);
reloadButton.click();
yield onAddonReloaded;
const [reloadedAddon] = yield onReInstall;
// Make sure the name was updated correctly.
const allAddons = [...document.querySelectorAll("#addons .target-name")]
.map(element => element.textContent);
const nameWasUpdated = allAddons.some(name => name === newName);
ok(nameWasUpdated, `New name appeared in reloaded add-ons: ${allAddons}`);
yield tearDownAddon(reloadedAddon);
tempExt.remove();
yield closeAboutDebugging(tab);
});
add_task(function* onlyTempInstalledAddonsCanBeReloaded() {
const { tab, document } = yield openAboutDebugging("addons");
yield waitForInitialAddonList(document);
const onAddonListUpdated = waitForMutation(getAddonList(document),
{ childList: true });
yield installAddonWithManager(getSupportsFile("addons/bug1273184.xpi").file);
yield onAddonListUpdated;
const addon = yield getAddonByID("bug1273184@tests");
const reloadButton = getReloadButton(document, addon.name);
ok(reloadButton, "Reload button exists");
is(reloadButton.disabled, true, "Reload button should be disabled");
ok(reloadButton.title, "Disabled reload button should have a tooltip");
yield tearDownAddon(addon);
yield closeAboutDebugging(tab);
});

View file

@ -0,0 +1,65 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that individual Debug buttons are disabled when "Addons debugging"
// is disabled.
// Test that the buttons are updated dynamically if the preference changes.
const ADDON_ID = "test-devtools@mozilla.org";
const ADDON_NAME = "test-devtools";
add_task(function* () {
info("Turn off addon debugging.");
yield new Promise(resolve => {
let options = {"set": [
["devtools.chrome.enabled", false],
["devtools.debugger.remote-enabled", false],
]};
SpecialPowers.pushPrefEnv(options, resolve);
});
let { tab, document } = yield openAboutDebugging("addons");
yield waitForInitialAddonList(document);
info("Install a test addon.");
yield installAddon({
document,
path: "addons/unpacked/install.rdf",
name: ADDON_NAME,
});
let addonDebugCheckbox = document.querySelector("#enable-addon-debugging");
ok(!addonDebugCheckbox.checked, "Addons debugging should be disabled.");
info("Check all debug buttons are disabled.");
let debugButtons = [...document.querySelectorAll("#addons .debug-button")];
ok(debugButtons.every(b => b.disabled), "Debug buttons should be disabled");
info("Click on 'Enable addons debugging' checkbox.");
let addonsContainer = document.getElementById("addons");
let onAddonsMutation = waitForMutation(addonsContainer,
{ subtree: true, attributes: true });
addonDebugCheckbox.click();
yield onAddonsMutation;
info("Check all debug buttons are enabled.");
ok(addonDebugCheckbox.checked, "Addons debugging should be enabled.");
debugButtons = [...document.querySelectorAll("#addons .debug-button")];
ok(debugButtons.every(b => !b.disabled), "Debug buttons should be enabled");
info("Click again on 'Enable addons debugging' checkbox.");
onAddonsMutation = waitForMutation(addonsContainer,
{ subtree: true, attributes: true });
addonDebugCheckbox.click();
yield onAddonsMutation;
info("Check all debug buttons are disabled again.");
debugButtons = [...document.querySelectorAll("#addons .debug-button")];
ok(debugButtons.every(b => b.disabled), "Debug buttons should be disabled");
info("Uninstall addon installed earlier.");
yield uninstallAddon({document, id: ADDON_ID, name: ADDON_NAME});
yield closeAboutDebugging(tab);
});

View file

@ -0,0 +1,37 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that navigating to a about:debugging#invalid-hash should show up an
// error page.
// Every url navigating including #invalid-hash should be kept in history and
// navigate back as expected.
add_task(function* () {
let { tab, document } = yield openAboutDebugging("invalid-hash");
let element = document.querySelector(".header-name");
is(element.textContent, "Page not found", "Show error page");
yield openPanel(document, "addons-panel");
yield waitForInitialAddonList(document);
element = document.querySelector(".header-name");
is(element.textContent, "Add-ons", "Show Addons");
yield changeAboutDebuggingHash(document, "invalid-hash");
element = document.querySelector(".header-name");
is(element.textContent, "Page not found", "Show error page");
gBrowser.goBack();
yield waitForMutation(
document.querySelector(".main-content"), {childList: true});
yield waitForInitialAddonList(document);
element = document.querySelector(".header-name");
is(element.textContent, "Add-ons", "Show Addons");
gBrowser.goBack();
yield waitForMutation(
document.querySelector(".main-content"), {childList: true});
element = document.querySelector(".header-name");
is(element.textContent, "Page not found", "Show error page");
yield closeAboutDebugging(tab);
});

View file

@ -0,0 +1,51 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Service workers can't be loaded from chrome://,
// but http:// is ok with dom.serviceWorkers.testing.enabled turned on.
const SERVICE_WORKER = URL_ROOT + "service-workers/empty-sw.js";
const TAB_URL = URL_ROOT + "service-workers/empty-sw.html";
add_task(function* () {
yield new Promise(done => {
let options = {"set": [
// Accept workers from mochitest's http.
["dom.serviceWorkers.enabled", true],
["dom.serviceWorkers.openWindow.enabled", true],
["dom.serviceWorkers.testing.enabled", true],
]};
SpecialPowers.pushPrefEnv(options, done);
});
let { tab, document } = yield openAboutDebugging("workers");
let swTab = yield addTab(TAB_URL);
let serviceWorkersElement = getServiceWorkerList(document);
yield waitForMutation(serviceWorkersElement, { childList: true });
// Check that the service worker appears in the UI
let names = [...document.querySelectorAll("#service-workers .target-name")];
names = names.map(element => element.textContent);
ok(names.includes(SERVICE_WORKER),
"The service worker url appears in the list: " + names);
try {
yield unregisterServiceWorker(swTab, serviceWorkersElement);
ok(true, "Service worker registration unregistered");
} catch (e) {
ok(false, "SW not unregistered; " + e);
}
// Check that the service worker disappeared from the UI
names = [...document.querySelectorAll("#service-workers .target-name")];
names = names.map(element => element.textContent);
ok(!names.includes(SERVICE_WORKER),
"The service worker url is no longer in the list: " + names);
yield removeTab(swTab);
yield closeAboutDebugging(tab);
});

View file

@ -0,0 +1,60 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that Service Worker section should show warning message in
// about:debugging if any of following conditions is met:
// 1. service worker is disabled
// 2. the about:debugging pannel is openned in private browsing mode
// 3. the about:debugging pannel is openned in private content window
var imgClass = ".service-worker-disabled .warning";
add_task(function* () {
yield new Promise(done => {
info("disable service workers");
let options = {"set": [
["dom.serviceWorkers.enabled", false],
]};
SpecialPowers.pushPrefEnv(options, done);
});
let { tab, document } = yield openAboutDebugging("workers");
// Check that the warning img appears in the UI
let img = document.querySelector(imgClass);
ok(img, "warning message is rendered");
yield closeAboutDebugging(tab);
});
add_task(function* () {
yield new Promise(done => {
info("set private browsing mode as default");
let options = {"set": [
["browser.privatebrowsing.autostart", true],
]};
SpecialPowers.pushPrefEnv(options, done);
});
let { tab, document } = yield openAboutDebugging("workers");
// Check that the warning img appears in the UI
let img = document.querySelector(imgClass);
ok(img, "warning message is rendered");
yield closeAboutDebugging(tab);
});
add_task(function* () {
info("Opening a new private window");
let win = OpenBrowserWindow({private: true});
yield waitForDelayedStartupFinished(win);
let { tab, document } = yield openAboutDebugging("workers", win);
// Check that the warning img appears in the UI
let img = document.querySelector(imgClass);
ok(img, "warning message is rendered");
yield closeAboutDebugging(tab);
win.close();
});

View file

@ -0,0 +1,105 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/* global sendAsyncMessage */
"use strict";
// Test that clicking on the Push button next to a Service Worker works as
// intended in about:debugging.
// It should trigger a "push" notification in the worker.
// Service workers can't be loaded from chrome://, but http:// is ok with
// dom.serviceWorkers.testing.enabled turned on.
const SERVICE_WORKER = URL_ROOT + "service-workers/push-sw.js";
const TAB_URL = URL_ROOT + "service-workers/push-sw.html";
add_task(function* () {
info("Turn on workers via mochitest http.");
yield new Promise(done => {
let options = { "set": [
// Accept workers from mochitest's http.
["dom.serviceWorkers.enabled", true],
["dom.serviceWorkers.openWindow.enabled", true],
["dom.serviceWorkers.testing.enabled", true],
]};
SpecialPowers.pushPrefEnv(options, done);
});
let { tab, document } = yield openAboutDebugging("workers");
// Listen for mutations in the service-workers list.
let serviceWorkersElement = getServiceWorkerList(document);
let onMutation = waitForMutation(serviceWorkersElement, { childList: true });
// Open a tab that registers a push service worker.
let swTab = yield addTab(TAB_URL);
info("Make the test page notify us when the service worker sends a message.");
yield ContentTask.spawn(swTab.linkedBrowser, {}, function () {
let win = content.wrappedJSObject;
win.navigator.serviceWorker.addEventListener("message", function (event) {
sendAsyncMessage(event.data);
}, false);
});
// Expect the service worker to claim the test window when activating.
let mm = swTab.linkedBrowser.messageManager;
let onClaimed = new Promise(done => {
mm.addMessageListener("sw-claimed", function listener() {
mm.removeMessageListener("sw-claimed", listener);
done();
});
});
// Wait for the service-workers list to update.
yield onMutation;
// Check that the service worker appears in the UI.
assertHasTarget(true, document, "service-workers", SERVICE_WORKER);
info("Ensure that the registration resolved before trying to interact with " +
"the service worker.");
yield waitForServiceWorkerRegistered(swTab);
ok(true, "Service worker registration resolved");
yield waitForServiceWorkerActivation(SERVICE_WORKER, document);
// Retrieve the Push 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 targetElement = name.parentNode.parentNode;
let pushBtn = targetElement.querySelector(".push-button");
ok(pushBtn, "Found its push button");
info("Wait for the service worker to claim the test window before " +
"proceeding.");
yield onClaimed;
info("Click on the Push button and wait for the service worker to receive " +
"a push notification");
let onPushNotification = new Promise(done => {
mm.addMessageListener("sw-pushed", function listener() {
mm.removeMessageListener("sw-pushed", listener);
done();
});
});
pushBtn.click();
yield onPushNotification;
ok(true, "Service worker received a push notification");
// Finally, unregister the service worker itself.
try {
yield unregisterServiceWorker(swTab, serviceWorkersElement);
ok(true, "Service worker registration unregistered");
} catch (e) {
ok(false, "SW not unregistered; " + e);
}
yield removeTab(swTab);
yield closeAboutDebugging(tab);
});

View file

@ -0,0 +1,122 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that a Service Worker registration's Push Service subscription appears
// in about:debugging if it exists, and disappears when unregistered.
// Service workers can't be loaded from chrome://, but http:// is ok with
// dom.serviceWorkers.testing.enabled turned on.
const SERVICE_WORKER = URL_ROOT + "service-workers/push-sw.js";
const TAB_URL = URL_ROOT + "service-workers/push-sw.html";
const FAKE_ENDPOINT = "https://fake/endpoint";
const PushService = Cc["@mozilla.org/push/Service;1"]
.getService(Ci.nsIPushService).wrappedJSObject;
add_task(function* () {
info("Turn on workers via mochitest http.");
yield SpecialPowers.pushPrefEnv({
"set": [
// Accept workers from mochitest's http.
["dom.serviceWorkers.enabled", true],
["dom.serviceWorkers.openWindow.enabled", true],
["dom.serviceWorkers.testing.enabled", true],
// Enable the push service.
["dom.push.enabled", true],
["dom.push.connection.enabled", true],
]
});
info("Mock the push service");
PushService.service = {
_registrations: new Map(),
_notify(scope) {
Services.obs.notifyObservers(
null,
PushService.subscriptionModifiedTopic,
scope);
},
init() {},
register(pageRecord) {
let registration = {
endpoint: FAKE_ENDPOINT
};
this._registrations.set(pageRecord.scope, registration);
this._notify(pageRecord.scope);
return Promise.resolve(registration);
},
registration(pageRecord) {
return Promise.resolve(this._registrations.get(pageRecord.scope));
},
unregister(pageRecord) {
let deleted = this._registrations.delete(pageRecord.scope);
if (deleted) {
this._notify(pageRecord.scope);
}
return Promise.resolve(deleted);
},
};
let { tab, document } = yield openAboutDebugging("workers");
// Listen for mutations in the service-workers list.
let serviceWorkersElement = document.getElementById("service-workers");
let onMutation = waitForMutation(serviceWorkersElement, { childList: true });
// Open a tab that registers a push service worker.
let swTab = yield addTab(TAB_URL);
// Wait for the service-workers list to update.
yield onMutation;
// Check that the service worker appears in the UI.
assertHasTarget(true, document, "service-workers", SERVICE_WORKER);
yield waitForServiceWorkerActivation(SERVICE_WORKER, document);
// Wait for the service worker details to update.
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 targetContainer = name.parentNode.parentNode;
let targetDetailsElement = targetContainer.querySelector(".target-details");
// Retrieve the push subscription endpoint URL, and verify it looks good.
let pushURL = targetContainer.querySelector(".service-worker-push-url");
if (!pushURL) {
yield waitForMutation(targetDetailsElement, { childList: true });
pushURL = targetContainer.querySelector(".service-worker-push-url");
}
ok(pushURL, "Found the push service URL in the service worker details");
is(pushURL.textContent, FAKE_ENDPOINT, "The push service URL looks correct");
// Unsubscribe from the push service.
ContentTask.spawn(swTab.linkedBrowser, {}, function () {
let win = content.wrappedJSObject;
return win.sub.unsubscribe();
});
// Wait for the service worker details to update again.
yield waitForMutation(targetDetailsElement, { childList: true });
ok(!targetContainer.querySelector(".service-worker-push-url"),
"The push service URL should be removed");
// Finally, unregister the service worker itself.
try {
yield unregisterServiceWorker(swTab, serviceWorkersElement);
ok(true, "Service worker registration unregistered");
} catch (e) {
ok(false, "SW not unregistered; " + e);
}
info("Unmock the push service");
PushService.service = null;
yield removeTab(swTab);
yield closeAboutDebugging(tab);
});

View file

@ -0,0 +1,97 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that clicking on the Start button next to a Service Worker works as
// intended in about:debugging.
// It should cause a worker to start running in a child process.
// Service workers can't be loaded from chrome://, but http:// is ok with
// dom.serviceWorkers.testing.enabled turned on.
const SERVICE_WORKER = URL_ROOT + "service-workers/empty-sw.js";
const TAB_URL = URL_ROOT + "service-workers/empty-sw.html";
const SW_TIMEOUT = 1000;
add_task(function* () {
info("Turn on workers via mochitest http.");
yield new Promise(done => {
let options = { "set": [
// Accept workers from mochitest's http.
["dom.serviceWorkers.enabled", true],
["dom.serviceWorkers.openWindow.enabled", true],
["dom.serviceWorkers.testing.enabled", true],
// Reduce the timeout to accelerate service worker freezing
["dom.serviceWorkers.idle_timeout", SW_TIMEOUT],
["dom.serviceWorkers.idle_extended_timeout", SW_TIMEOUT],
]};
SpecialPowers.pushPrefEnv(options, done);
});
let { tab, document } = yield openAboutDebugging("workers");
// Listen for mutations in the service-workers list.
let serviceWorkersElement = getServiceWorkerList(document);
let onMutation = waitForMutation(serviceWorkersElement, { childList: true });
// Open a tab that registers an empty service worker.
let swTab = yield addTab(TAB_URL);
// Wait for the service-workers list to update.
yield onMutation;
// Check that the service worker appears in the UI.
assertHasTarget(true, document, "service-workers", SERVICE_WORKER);
info("Ensure that the registration resolved before trying to interact with " +
"the service worker.");
yield waitForServiceWorkerRegistered(swTab);
ok(true, "Service worker registration resolved");
yield waitForServiceWorkerActivation(SERVICE_WORKER, document);
// Retrieve the Target element corresponding to the service 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 targetElement = name.parentNode.parentNode;
// The service worker may already be killed with the low 1s timeout
if (!targetElement.querySelector(".start-button")) {
// Check that there is a Debug button but not a Start button.
ok(targetElement.querySelector(".debug-button"), "Found its debug button");
// Wait for the service worker to be killed due to inactivity.
yield waitForMutation(targetElement, { childList: true });
} else {
// Check that there is no Debug button when the SW is already shut down.
ok(!targetElement.querySelector(".debug-button"), "No debug button when " +
"the worker is already killed");
}
// We should now have a Start button but no Debug button.
let startBtn = targetElement.querySelector(".start-button");
ok(startBtn, "Found its start button");
ok(!targetElement.querySelector(".debug-button"), "No debug button");
// Click on the Start button and wait for the service worker to be back.
let onStarted = waitForMutation(targetElement, { childList: true });
startBtn.click();
yield onStarted;
// Check that we have a Debug button but not a Start button again.
ok(targetElement.querySelector(".debug-button"), "Found its debug button");
ok(!targetElement.querySelector(".start-button"), "No start button");
// Finally, unregister the service worker itself.
try {
yield unregisterServiceWorker(swTab, serviceWorkersElement);
ok(true, "Service worker registration unregistered");
} catch (e) {
ok(false, "SW not unregistered; " + e);
}
yield removeTab(swTab);
yield closeAboutDebugging(tab);
});

View file

@ -0,0 +1,72 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Service workers can't be loaded from chrome://,
// but http:// is ok with dom.serviceWorkers.testing.enabled turned on.
const SERVICE_WORKER = URL_ROOT + "service-workers/delay-sw.js";
const TAB_URL = URL_ROOT + "service-workers/delay-sw.html";
const SW_TIMEOUT = 2000;
requestLongerTimeout(2);
add_task(function* () {
yield SpecialPowers.pushPrefEnv({
"set": [
// Accept workers from mochitest's http.
["dom.serviceWorkers.enabled", true],
["dom.serviceWorkers.openWindow.enabled", true],
["dom.serviceWorkers.testing.enabled", true],
// Reduce the timeout to expose issues when service worker
// freezing is broken
["dom.serviceWorkers.idle_timeout", SW_TIMEOUT],
["dom.serviceWorkers.idle_extended_timeout", SW_TIMEOUT],
["dom.ipc.processCount", 1],
]
});
let { tab, document } = yield openAboutDebugging("workers");
// Listen for mutations in the service-workers list.
let serviceWorkersElement = getServiceWorkerList(document);
let onMutation = waitForMutation(serviceWorkersElement, { childList: true });
let swTab = yield addTab(TAB_URL);
info("Make the test page notify us when the service worker sends a message.");
// Wait for the service-workers list to update.
yield onMutation;
// Check that the service worker appears in the UI
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 targetElement = name.parentNode.parentNode;
let status = targetElement.querySelector(".target-status");
is(status.textContent, "Registering", "Service worker is currently registering");
yield waitForMutation(serviceWorkersElement, { childList: true, subtree: true });
is(status.textContent, "Running", "Service worker is currently running");
yield waitForMutation(serviceWorkersElement, { attributes: true, subtree: true });
is(status.textContent, "Stopped", "Service worker is currently stopped");
try {
yield unregisterServiceWorker(swTab, serviceWorkersElement);
ok(true, "Service worker unregistered");
} catch (e) {
ok(false, "Service worker not unregistered; " + e);
}
// Check that the service worker disappeared from the UI
names = [...document.querySelectorAll("#service-workers .target-name")];
names = names.map(element => element.textContent);
ok(!names.includes(SERVICE_WORKER),
"The service worker url is no longer in the list: " + names);
yield removeTab(swTab);
yield closeAboutDebugging(tab);
});

View file

@ -0,0 +1,92 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Service workers can't be loaded from chrome://,
// but http:// is ok with dom.serviceWorkers.testing.enabled turned on.
const SERVICE_WORKER = URL_ROOT + "service-workers/empty-sw.js";
const TAB_URL = URL_ROOT + "service-workers/empty-sw.html";
const SW_TIMEOUT = 1000;
add_task(function* () {
yield new Promise(done => {
let options = {"set": [
// Accept workers from mochitest's http.
["dom.serviceWorkers.enabled", true],
["dom.serviceWorkers.openWindow.enabled", true],
["dom.serviceWorkers.testing.enabled", true],
// Reduce the timeout to expose issues when service worker
// freezing is broken
["dom.serviceWorkers.idle_timeout", SW_TIMEOUT],
["dom.serviceWorkers.idle_extended_timeout", SW_TIMEOUT],
]};
SpecialPowers.pushPrefEnv(options, done);
});
let { tab, document } = yield openAboutDebugging("workers");
let swTab = yield addTab(TAB_URL);
let serviceWorkersElement = getServiceWorkerList(document);
yield waitForMutation(serviceWorkersElement, { childList: true });
assertHasTarget(true, document, "service-workers", SERVICE_WORKER);
// Ensure that the registration resolved before trying to connect to the sw
yield waitForServiceWorkerRegistered(swTab);
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 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);
});
});
debugBtn.click();
let toolbox = yield onToolboxReady;
// 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);
});
assertHasTarget(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 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.
try {
yield unregisterServiceWorker(swTab, serviceWorkersElement);
ok(true, "Service worker registration unregistered");
} catch (e) {
ok(false, "SW not unregistered; " + e);
}
assertHasTarget(false, document, "service-workers", SERVICE_WORKER);
yield removeTab(swTab);
yield closeAboutDebugging(tab);
});

View file

@ -0,0 +1,77 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that clicking on the unregister link in the Service Worker details works
// as intended in about:debugging.
// It should unregister the service worker, which should trigger an update of
// the displayed list of service workers.
// Service workers can't be loaded from chrome://, but http:// is ok with
// dom.serviceWorkers.testing.enabled turned on.
const SCOPE = URL_ROOT + "service-workers/";
const SERVICE_WORKER = SCOPE + "empty-sw.js";
const TAB_URL = SCOPE + "empty-sw.html";
add_task(function* () {
info("Turn on workers via mochitest http.");
yield new Promise(done => {
let options = { "set": [
// Accept workers from mochitest's http.
["dom.serviceWorkers.enabled", true],
["dom.serviceWorkers.openWindow.enabled", true],
["dom.serviceWorkers.testing.enabled", true],
["dom.ipc.processCount", 1],
]};
SpecialPowers.pushPrefEnv(options, done);
});
let { tab, document } = yield openAboutDebugging("workers");
// Listen for mutations in the service-workers list.
let serviceWorkersElement = getServiceWorkerList(document);
let onMutation = waitForMutation(serviceWorkersElement, { childList: true });
// Open a tab that registers an empty service worker.
let swTab = yield addTab(TAB_URL);
// Wait for the service workers-list to update.
yield onMutation;
// Check that the service worker appears in the UI.
assertHasTarget(true, document, "service-workers", SERVICE_WORKER);
yield waitForServiceWorkerActivation(SERVICE_WORKER, document);
info("Ensure that the registration resolved before trying to interact with " +
"the service worker.");
yield waitForServiceWorkerRegistered(swTab);
ok(true, "Service worker registration resolved");
let targets = document.querySelectorAll("#service-workers .target");
is(targets.length, 1, "One service worker is now displayed.");
let target = targets[0];
let name = target.querySelector(".target-name");
is(name.textContent, SERVICE_WORKER, "Found the service worker in the list");
info("Check the scope displayed scope is correct");
let scope = target.querySelector(".service-worker-scope");
is(scope.textContent, SCOPE,
"The expected scope is displayed in the service worker info.");
info("Unregister the service worker via the unregister link.");
let unregisterLink = target.querySelector(".unregister-link");
ok(unregisterLink, "Found the unregister link");
onMutation = waitForMutation(serviceWorkersElement, { childList: true });
unregisterLink.click();
yield onMutation;
is(document.querySelector("#service-workers .target"), null,
"No service worker displayed anymore.");
yield removeTab(swTab);
yield closeAboutDebugging(tab);
});

View file

@ -0,0 +1,59 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const TAB_URL = "data:text/html,<title>foo</title>";
add_task(function* setup() {
yield SpecialPowers.pushPrefEnv({
set: [["dom.ipc.processCount", 1]]
});
});
add_task(function* () {
let { tab, document } = yield openAboutDebugging("tabs");
// Wait for initial tabs list which may be empty
let tabsElement = getTabList(document);
if (tabsElement.querySelectorAll(".target-name").length == 0) {
yield waitForMutation(tabsElement, { childList: true });
}
// Refresh tabsElement to get the .target-list element
tabsElement = getTabList(document);
let names = [...tabsElement.querySelectorAll(".target-name")];
let initialTabCount = names.length;
// Open a new tab in background and wait for its addition in the UI
let onNewTab = waitForMutation(tabsElement, { childList: true });
let newTab = yield addTab(TAB_URL, { background: true });
yield onNewTab;
// Check that the new tab appears in the UI, but with an empty name
let newNames = [...tabsElement.querySelectorAll(".target-name")];
newNames = newNames.filter(node => !names.includes(node));
is(newNames.length, 1, "A new tab appeared in the list");
let newTabTarget = newNames[0];
// Then wait for title update, but on slow test runner, the title may already
// be set to the expected value
if (newTabTarget.textContent != "foo") {
yield waitForContentMutation(newTabTarget);
}
// Check that the new tab appears in the UI
is(newTabTarget.textContent, "foo", "The tab title got updated");
is(newTabTarget.title, TAB_URL, "The tab tooltip is the url");
// Finally, close the tab
let onTabsUpdate = waitForMutation(tabsElement, { childList: true });
yield removeTab(newTab);
yield onTabsUpdate;
// Check that the tab disappeared from the UI
names = [...tabsElement.querySelectorAll("#tabs .target-name")];
is(names.length, initialTabCount, "The tab disappeared from the UI");
yield closeAboutDebugging(tab);
});

View file

@ -0,0 +1,367 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/* eslint-env browser */
/* exported openAboutDebugging, changeAboutDebuggingHash, closeAboutDebugging,
installAddon, uninstallAddon, waitForMutation, waitForContentMutation, assertHasTarget,
getServiceWorkerList, getTabList, openPanel, waitForInitialAddonList,
waitForServiceWorkerRegistered, unregisterServiceWorker,
waitForDelayedStartupFinished, setupTestAboutDebuggingWebExtension,
waitForServiceWorkerActivation */
/* import-globals-from ../../framework/test/shared-head.js */
"use strict";
// Load the shared-head file first.
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js",
this);
const { AddonManager } = Cu.import("resource://gre/modules/AddonManager.jsm", {});
const { Management } = Cu.import("resource://gre/modules/Extension.jsm", {});
flags.testing = true;
registerCleanupFunction(() => {
flags.testing = false;
});
function* openAboutDebugging(page, win) {
info("opening about:debugging");
let url = "about:debugging";
if (page) {
url += "#" + page;
}
let tab = yield addTab(url, { window: win });
let browser = tab.linkedBrowser;
let document = browser.contentDocument;
if (!document.querySelector(".app")) {
yield waitForMutation(document.body, { childList: true });
}
return { tab, document };
}
/**
* Change url hash for current about:debugging tab, return a promise after
* new content is loaded.
* @param {DOMDocument} document container document from current tab
* @param {String} hash hash for about:debugging
* @return {Promise}
*/
function changeAboutDebuggingHash(document, hash) {
info(`Opening about:debugging#${hash}`);
window.openUILinkIn(`about:debugging#${hash}`, "current");
return waitForMutation(
document.querySelector(".main-content"), {childList: true});
}
function openPanel(document, panelId) {
info(`Opening ${panelId} panel`);
document.querySelector(`[aria-controls="${panelId}"]`).click();
return waitForMutation(
document.querySelector(".main-content"), {childList: true});
}
function closeAboutDebugging(tab) {
info("Closing about:debugging");
return removeTab(tab);
}
function getSupportsFile(path) {
let cr = Cc["@mozilla.org/chrome/chrome-registry;1"]
.getService(Ci.nsIChromeRegistry);
let uri = Services.io.newURI(CHROME_URL_ROOT + path, null, null);
let fileurl = cr.convertChromeURL(uri);
return fileurl.QueryInterface(Ci.nsIFileURL);
}
/**
* Depending on whether there are addons installed, return either a target list
* element or its container.
* @param {DOMDocument} document #addons section container document
* @return {DOMNode} target list or container element
*/
function getAddonList(document) {
return document.querySelector("#addons .target-list") ||
document.querySelector("#addons .targets");
}
/**
* Depending on whether there are service workers installed, return either a
* target list element or its container.
* @param {DOMDocument} document #service-workers section container document
* @return {DOMNode} target list or container element
*/
function getServiceWorkerList(document) {
return document.querySelector("#service-workers .target-list") ||
document.querySelector("#service-workers.targets");
}
/**
* Depending on whether there are tabs opened, return either a
* target list element or its container.
* @param {DOMDocument} document #tabs section container document
* @return {DOMNode} target list or container element
*/
function getTabList(document) {
return document.querySelector("#tabs .target-list") ||
document.querySelector("#tabs.targets");
}
function* installAddon({document, path, name, isWebExtension}) {
// Mock the file picker to select a test addon
let MockFilePicker = SpecialPowers.MockFilePicker;
MockFilePicker.init(null);
let file = getSupportsFile(path);
MockFilePicker.returnFiles = [file.file];
let addonList = getAddonList(document);
let addonListMutation = waitForMutation(addonList, { childList: true });
let onAddonInstalled;
if (isWebExtension) {
onAddonInstalled = new Promise(done => {
Management.on("startup", function listener(event, extension) {
if (extension.name != name) {
return;
}
Management.off("startup", listener);
done();
});
});
} else {
// Wait for a "test-devtools" message sent by the addon's bootstrap.js file
onAddonInstalled = new Promise(done => {
Services.obs.addObserver(function listener() {
Services.obs.removeObserver(listener, "test-devtools");
done();
}, "test-devtools", false);
});
}
// Trigger the file picker by clicking on the button
document.getElementById("load-addon-from-file").click();
yield onAddonInstalled;
ok(true, "Addon installed and running its bootstrap.js file");
// Check that the addon appears in the UI
yield addonListMutation;
let names = [...addonList.querySelectorAll(".target-name")];
names = names.map(element => element.textContent);
ok(names.includes(name),
"The addon name appears in the list of addons: " + names);
}
function* uninstallAddon({document, id, name}) {
let addonList = getAddonList(document);
let addonListMutation = waitForMutation(addonList, { childList: true });
// Now uninstall this addon
yield new Promise(done => {
AddonManager.getAddonByID(id, addon => {
let listener = {
onUninstalled: function (uninstalledAddon) {
if (uninstalledAddon != addon) {
return;
}
AddonManager.removeAddonListener(listener);
done();
}
};
AddonManager.addAddonListener(listener);
addon.uninstall();
});
});
// Ensure that the UI removes the addon from the list
yield addonListMutation;
let names = [...addonList.querySelectorAll(".target-name")];
names = names.map(element => element.textContent);
ok(!names.includes(name),
"After uninstall, the addon name disappears from the list of addons: "
+ names);
}
/**
* Returns a promise that will resolve when the add-on list has been updated.
*
* @param {Node} document
* @return {Promise}
*/
function waitForInitialAddonList(document) {
const addonListContainer = getAddonList(document);
let addonCount = addonListContainer.querySelectorAll(".target");
addonCount = addonCount ? [...addonCount].length : -1;
info("Waiting for add-ons to load. Current add-on count: " + addonCount);
// This relies on the network speed of the actor responding to the
// listAddons() request and also the speed of openAboutDebugging().
let result;
if (addonCount > 0) {
info("Actually, the add-ons have already loaded");
result = Promise.resolve();
} else {
result = waitForMutation(addonListContainer, { childList: true });
}
return result;
}
/**
* Returns a promise that will resolve after receiving a mutation matching the
* provided mutation options on the provided target.
* @param {Node} target
* @param {Object} mutationOptions
* @return {Promise}
*/
function waitForMutation(target, mutationOptions) {
return new Promise(resolve => {
let observer = new MutationObserver(() => {
observer.disconnect();
resolve();
});
observer.observe(target, mutationOptions);
});
}
/**
* Returns a promise that will resolve after receiving a mutation in the subtree of the
* provided target. Depending on the current React implementation, a text change might be
* observable as a childList mutation or a characterData mutation.
*
* @param {Node} target
* @return {Promise}
*/
function waitForContentMutation(target) {
return waitForMutation(target, {
characterData: true,
childList: true,
subtree: true
});
}
/**
* Checks if an about:debugging TargetList element contains a Target element
* corresponding to the specified name.
* @param {Boolean} expected
* @param {Document} document
* @param {String} type
* @param {String} name
*/
function assertHasTarget(expected, document, type, name) {
let names = [...document.querySelectorAll("#" + type + " .target-name")];
names = names.map(element => element.textContent);
is(names.includes(name), expected,
"The " + type + " url appears in the list: " + names);
}
/**
* Returns a promise that will resolve after the service worker in the page
* has successfully registered itself.
* @param {Tab} tab
* @return {Promise} Resolves when the service worker is registered.
*/
function waitForServiceWorkerRegistered(tab) {
return ContentTask.spawn(tab.linkedBrowser, {}, function* () {
// Retrieve the `sw` promise created in the html page.
let { sw } = content.wrappedJSObject;
yield sw;
});
}
/**
* Asks the service worker within the test page to unregister, and returns a
* promise that will resolve when it has successfully unregistered itself and the
* about:debugging UI has fully processed this update.
*
* @param {Tab} tab
* @param {Node} serviceWorkersElement
* @return {Promise} Resolves when the service worker is unregistered.
*/
function* unregisterServiceWorker(tab, serviceWorkersElement) {
let onMutation = waitForMutation(serviceWorkersElement, { childList: true });
yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
// Retrieve the `sw` promise created in the html page
let { sw } = content.wrappedJSObject;
let registration = yield sw;
yield registration.unregister();
});
return onMutation;
}
/**
* Waits for the creation of a new window, usually used with create private
* browsing window.
* Returns a promise that will resolve when the window is successfully created.
* @param {window} win
*/
function waitForDelayedStartupFinished(win) {
return new Promise(function (resolve) {
Services.obs.addObserver(function observer(subject, topic) {
if (win == subject) {
Services.obs.removeObserver(observer, topic);
resolve();
}
}, "browser-delayed-startup-finished", false);
});
}
/**
* open the about:debugging page and install an addon
*/
function* setupTestAboutDebuggingWebExtension(name, path) {
yield new Promise(resolve => {
let options = {"set": [
// Force enabling of addons debugging
["devtools.chrome.enabled", true],
["devtools.debugger.remote-enabled", true],
// Disable security prompt
["devtools.debugger.prompt-connection", false],
// Enable Browser toolbox test script execution via env variable
["devtools.browser-toolbox.allow-unsafe-script", true],
]};
SpecialPowers.pushPrefEnv(options, resolve);
});
let { tab, document } = yield openAboutDebugging("addons");
yield waitForInitialAddonList(document);
yield installAddon({
document,
path,
name,
isWebExtension: true,
});
// Retrieve the DEBUG button for the addon
let names = [...document.querySelectorAll("#addons .target-name")];
let nameEl = names.filter(element => element.textContent === name)[0];
ok(name, "Found the addon in the list");
let targetElement = nameEl.parentNode.parentNode;
let debugBtn = targetElement.querySelector(".debug-button");
ok(debugBtn, "Found its debug button");
return { tab, document, debugBtn };
}
/**
* Wait for aboutdebugging to be notified about the activation of the service worker
* corresponding to the provided service worker url.
*/
function* waitForServiceWorkerActivation(swUrl, document) {
let serviceWorkersElement = getServiceWorkerList(document);
let names = serviceWorkersElement.querySelectorAll(".target-name");
let name = [...names].filter(element => element.textContent === swUrl)[0];
let targetElement = name.parentNode.parentNode;
let targetStatus = targetElement.querySelector(".target-status");
while (targetStatus.textContent === "Registering") {
// Wait for the status to leave the "registering" stage.
yield waitForMutation(serviceWorkersElement, { childList: true, subtree: true });
}
}

View file

@ -0,0 +1,22 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>Service worker test</title>
</head>
<body>
<script type="text/javascript">
"use strict";
var sw = navigator.serviceWorker.register("delay-sw.js");
sw.then(
function () {
dump("SW registered\n");
},
function (e) {
dump("SW not registered: " + e + "\n");
}
);
</script>
</body>
</html>

View file

@ -0,0 +1,17 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/* eslint-env worker */
"use strict";
function wait(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}
// Wait for one second to switch from installing to installed.
self.addEventListener("install", function (event) {
event.waitUntil(wait(1000));
});

View file

@ -0,0 +1,22 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>Service worker test</title>
</head>
<body>
<script type="text/javascript">
"use strict";
var sw = navigator.serviceWorker.register("empty-sw.js");
sw.then(
function () {
dump("SW registered\n");
},
function (e) {
dump("SW not registered: " + e + "\n");
}
);
</script>
</body>
</html>

View file

@ -0,0 +1 @@
// Empty, just test registering.

View file

@ -0,0 +1,32 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>Service worker push test</title>
</head>
<body>
<script type="text/javascript">
"use strict";
SpecialPowers.addPermission("desktop-notification", true, document);
var sw = navigator.serviceWorker.register("push-sw.js");
var sub = null;
sw.then(
function (registration) {
dump("SW registered\n");
registration.pushManager.subscribe().then(
function (subscription) {
sub = subscription;
dump("SW subscribed to push: " + sub.endpoint + "\n");
},
function (error) {
dump("SW not subscribed to push: " + error + "\n");
}
);
},
function (error) {
dump("SW not registered: " + error + "\n");
}
);
</script>
</body>
</html>

View file

@ -0,0 +1,33 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/* eslint-env worker */
/* global clients */
"use strict";
// Send a message to all controlled windows.
function postMessage(message) {
return clients.matchAll().then(function (clientlist) {
clientlist.forEach(function (client) {
client.postMessage(message);
});
});
}
// Don't wait for the next page load to become the active service worker.
self.addEventListener("install", function (event) {
event.waitUntil(self.skipWaiting());
});
// Claim control over the currently open test page when activating.
self.addEventListener("activate", function (event) {
event.waitUntil(self.clients.claim().then(function () {
return postMessage("sw-claimed");
}));
});
// Forward all "push" events to the controlled window.
self.addEventListener("push", function (event) {
event.waitUntil(postMessage("sw-pushed"));
});

View file

@ -0,0 +1,390 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript 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/. */
/* animation-panel.js is loaded in the same scope but we don't use
import-globals-from to avoid infinite loops since animation-panel.js already
imports globals from animation-controller.js */
/* globals AnimationsPanel */
/* eslint no-unused-vars: [2, {"vars": "local", "args": "none"}] */
"use strict";
var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
var { loader, require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
var { Task } = require("devtools/shared/task");
loader.lazyRequireGetter(this, "promise");
loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter");
loader.lazyRequireGetter(this, "AnimationsFront", "devtools/shared/fronts/animation", true);
const { LocalizationHelper } = require("devtools/shared/l10n");
const L10N =
new LocalizationHelper("devtools/client/locales/animationinspector.properties");
// Global toolbox/inspector, set when startup is called.
var gToolbox, gInspector;
/**
* Startup the animationinspector controller and view, called by the sidebar
* widget when loading/unloading the iframe into the tab.
*/
var startup = Task.async(function* (inspector) {
gInspector = inspector;
gToolbox = inspector.toolbox;
// Don't assume that AnimationsPanel is defined here, it's in another file.
if (!typeof AnimationsPanel === "undefined") {
throw new Error("AnimationsPanel was not loaded in the " +
"animationinspector window");
}
// Startup first initalizes the controller and then the panel, in sequence.
// If you want to know when everything's ready, do:
// AnimationsPanel.once(AnimationsPanel.PANEL_INITIALIZED)
yield AnimationsController.initialize();
yield AnimationsPanel.initialize();
});
/**
* Shutdown the animationinspector controller and view, called by the sidebar
* widget when loading/unloading the iframe into the tab.
*/
var shutdown = Task.async(function* () {
yield AnimationsController.destroy();
// Don't assume that AnimationsPanel is defined here, it's in another file.
if (typeof AnimationsPanel !== "undefined") {
yield AnimationsPanel.destroy();
}
gToolbox = gInspector = null;
});
// This is what makes the sidebar widget able to load/unload the panel.
function setPanel(panel) {
return startup(panel).catch(e => console.error(e));
}
function destroy() {
return shutdown().catch(e => console.error(e));
}
/**
* Get all the server-side capabilities (traits) so the UI knows whether or not
* features should be enabled/disabled.
* @param {Target} target The current toolbox target.
* @return {Object} An object with boolean properties.
*/
var getServerTraits = Task.async(function* (target) {
let config = [
{ name: "hasToggleAll", actor: "animations",
method: "toggleAll" },
{ name: "hasToggleSeveral", actor: "animations",
method: "toggleSeveral" },
{ name: "hasSetCurrentTime", actor: "animationplayer",
method: "setCurrentTime" },
{ name: "hasMutationEvents", actor: "animations",
method: "stopAnimationPlayerUpdates" },
{ name: "hasSetPlaybackRate", actor: "animationplayer",
method: "setPlaybackRate" },
{ name: "hasSetPlaybackRates", actor: "animations",
method: "setPlaybackRates" },
{ name: "hasTargetNode", actor: "domwalker",
method: "getNodeFromActor" },
{ name: "hasSetCurrentTimes", actor: "animations",
method: "setCurrentTimes" },
{ name: "hasGetFrames", actor: "animationplayer",
method: "getFrames" },
{ name: "hasGetProperties", actor: "animationplayer",
method: "getProperties" },
{ name: "hasSetWalkerActor", actor: "animations",
method: "setWalkerActor" },
];
let traits = {};
for (let {name, actor, method} of config) {
traits[name] = yield target.actorHasMethod(actor, method);
}
return traits;
});
/**
* The animationinspector controller's job is to retrieve AnimationPlayerFronts
* from the server. It is also responsible for keeping the list of players up to
* date when the node selection changes in the inspector, as well as making sure
* no updates are done when the animationinspector sidebar panel is not visible.
*
* AnimationPlayerFronts are available in AnimationsController.animationPlayers.
*
* Usage example:
*
* AnimationsController.on(AnimationsController.PLAYERS_UPDATED_EVENT,
* onPlayers);
* function onPlayers() {
* for (let player of AnimationsController.animationPlayers) {
* // do something with player
* }
* }
*/
var AnimationsController = {
PLAYERS_UPDATED_EVENT: "players-updated",
ALL_ANIMATIONS_TOGGLED_EVENT: "all-animations-toggled",
initialize: Task.async(function* () {
if (this.initialized) {
yield this.initialized;
return;
}
let resolver;
this.initialized = new Promise(resolve => {
resolver = resolve;
});
this.onPanelVisibilityChange = this.onPanelVisibilityChange.bind(this);
this.onNewNodeFront = this.onNewNodeFront.bind(this);
this.onAnimationMutations = this.onAnimationMutations.bind(this);
let target = gInspector.target;
this.animationsFront = new AnimationsFront(target.client, target.form);
// Expose actor capabilities.
this.traits = yield getServerTraits(target);
if (this.destroyed) {
console.warn("Could not fully initialize the AnimationsController");
return;
}
// Let the AnimationsActor know what WalkerActor we're using. This will
// come in handy later to return references to DOM Nodes.
if (this.traits.hasSetWalkerActor) {
yield this.animationsFront.setWalkerActor(gInspector.walker);
}
this.startListeners();
yield this.onNewNodeFront();
resolver();
}),
destroy: Task.async(function* () {
if (!this.initialized) {
return;
}
if (this.destroyed) {
yield this.destroyed;
return;
}
let resolver;
this.destroyed = new Promise(resolve => {
resolver = resolve;
});
this.stopListeners();
this.destroyAnimationPlayers();
this.nodeFront = null;
if (this.animationsFront) {
this.animationsFront.destroy();
this.animationsFront = null;
}
resolver();
}),
startListeners: function () {
// Re-create the list of players when a new node is selected, except if the
// sidebar isn't visible.
gInspector.selection.on("new-node-front", this.onNewNodeFront);
gInspector.sidebar.on("select", this.onPanelVisibilityChange);
gToolbox.on("select", this.onPanelVisibilityChange);
},
stopListeners: function () {
gInspector.selection.off("new-node-front", this.onNewNodeFront);
gInspector.sidebar.off("select", this.onPanelVisibilityChange);
gToolbox.off("select", this.onPanelVisibilityChange);
if (this.isListeningToMutations) {
this.animationsFront.off("mutations", this.onAnimationMutations);
}
},
isPanelVisible: function () {
return gToolbox.currentToolId === "inspector" &&
gInspector.sidebar &&
gInspector.sidebar.getCurrentTabID() == "animationinspector";
},
onPanelVisibilityChange: Task.async(function* () {
if (this.isPanelVisible()) {
this.onNewNodeFront();
}
}),
onNewNodeFront: Task.async(function* () {
// Ignore if the panel isn't visible or the node selection hasn't changed.
if (!this.isPanelVisible() ||
this.nodeFront === gInspector.selection.nodeFront) {
return;
}
this.nodeFront = gInspector.selection.nodeFront;
let done = gInspector.updating("animationscontroller");
if (!gInspector.selection.isConnected() ||
!gInspector.selection.isElementNode()) {
this.destroyAnimationPlayers();
this.emit(this.PLAYERS_UPDATED_EVENT);
done();
return;
}
yield this.refreshAnimationPlayers(this.nodeFront);
this.emit(this.PLAYERS_UPDATED_EVENT, this.animationPlayers);
done();
}),
/**
* Toggle (pause/play) all animations in the current target.
*/
toggleAll: function () {
if (!this.traits.hasToggleAll) {
return promise.resolve();
}
return this.animationsFront.toggleAll()
.then(() => this.emit(this.ALL_ANIMATIONS_TOGGLED_EVENT, this))
.catch(e => console.error(e));
},
/**
* Similar to toggleAll except that it only plays/pauses the currently known
* animations (those listed in this.animationPlayers).
* @param {Boolean} shouldPause True if the animations should be paused, false
* if they should be played.
* @return {Promise} Resolves when the playState has been changed.
*/
toggleCurrentAnimations: Task.async(function* (shouldPause) {
if (this.traits.hasToggleSeveral) {
yield this.animationsFront.toggleSeveral(this.animationPlayers,
shouldPause);
} else {
// Fall back to pausing/playing the players one by one, which is bound to
// introduce some de-synchronization.
for (let player of this.animationPlayers) {
if (shouldPause) {
yield player.pause();
} else {
yield player.play();
}
}
}
}),
/**
* Set all known animations' currentTimes to the provided time.
* @param {Number} time.
* @param {Boolean} shouldPause Should the animations be paused too.
* @return {Promise} Resolves when the current time has been set.
*/
setCurrentTimeAll: Task.async(function* (time, shouldPause) {
if (this.traits.hasSetCurrentTimes) {
yield this.animationsFront.setCurrentTimes(this.animationPlayers, time,
shouldPause);
} else {
// Fall back to pausing and setting the current time on each player, one
// by one, which is bound to introduce some de-synchronization.
for (let animation of this.animationPlayers) {
if (shouldPause) {
yield animation.pause();
}
yield animation.setCurrentTime(time);
}
}
}),
/**
* Set all known animations' playback rates to the provided rate.
* @param {Number} rate.
* @return {Promise} Resolves when the rate has been set.
*/
setPlaybackRateAll: Task.async(function* (rate) {
if (this.traits.hasSetPlaybackRates) {
// If the backend can set all playback rates at the same time, use that.
yield this.animationsFront.setPlaybackRates(this.animationPlayers, rate);
} else if (this.traits.hasSetPlaybackRate) {
// Otherwise, fall back to setting each rate individually.
for (let animation of this.animationPlayers) {
yield animation.setPlaybackRate(rate);
}
}
}),
// AnimationPlayerFront objects are managed by this controller. They are
// retrieved when refreshAnimationPlayers is called, stored in the
// animationPlayers array, and destroyed when refreshAnimationPlayers is
// called again.
animationPlayers: [],
refreshAnimationPlayers: Task.async(function* (nodeFront) {
this.destroyAnimationPlayers();
this.animationPlayers = yield this.animationsFront
.getAnimationPlayersForNode(nodeFront);
// Start listening for animation mutations only after the first method call
// otherwise events won't be sent.
if (!this.isListeningToMutations && this.traits.hasMutationEvents) {
this.animationsFront.on("mutations", this.onAnimationMutations);
this.isListeningToMutations = true;
}
}),
onAnimationMutations: function (changes) {
// Insert new players into this.animationPlayers when new animations are
// added.
for (let {type, player} of changes) {
if (type === "added") {
this.animationPlayers.push(player);
}
if (type === "removed") {
let index = this.animationPlayers.indexOf(player);
this.animationPlayers.splice(index, 1);
}
}
// Let the UI know the list has been updated.
this.emit(this.PLAYERS_UPDATED_EVENT, this.animationPlayers);
},
/**
* Get the latest known current time of document.timeline.
* This value is sent along with all AnimationPlayerActors' states, but it
* isn't updated after that, so this function loops over all know animations
* to find the highest value.
* @return {Number|Boolean} False is returned if this server version doesn't
* provide document's current time.
*/
get documentCurrentTime() {
let time = 0;
for (let {state} of this.animationPlayers) {
if (!state.documentCurrentTime) {
return false;
}
time = Math.max(time, state.documentCurrentTime);
}
return time;
},
destroyAnimationPlayers: function () {
this.animationPlayers = [];
}
};
EventEmitter.decorate(AnimationsController);

View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- 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/. -->
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<link rel="stylesheet" href="chrome://devtools/skin/animationinspector.css" type="text/css"/>
<script type="application/javascript;version=1.8" src="chrome://devtools/content/shared/theme-switching.js"/>
</head>
<body class="theme-sidebar devtools-monospace" role="application" empty="true">
<div id="global-toolbar" class="theme-toolbar">
<span id="all-animations-label" class="label"></span>
<button id="toggle-all" standalone="true" class="devtools-button pause-button"></button>
</div>
<div id="timeline-toolbar" class="theme-toolbar">
<button id="rewind-timeline" standalone="true" class="devtools-button"></button>
<button id="pause-resume-timeline" standalone="true" class="devtools-button pause-button paused"></button>
<span id="timeline-rate" standalone="true" class="devtools-button"></span>
<span id="timeline-current-time" class="label"></span>
</div>
<div id="players"></div>
<div id="error-message">
<p id="error-type"></p>
<p id="error-hint"></p>
<button id="element-picker" standalone="true" class="devtools-button"></button>
</div>
<script type="application/javascript;version=1.8" src="animation-controller.js"></script>
<script type="application/javascript;version=1.8" src="animation-panel.js"></script>
</body>
</html>

View file

@ -0,0 +1,347 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript 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/. */
/* import-globals-from animation-controller.js */
/* globals document */
"use strict";
const {AnimationsTimeline} = require("devtools/client/animationinspector/components/animation-timeline");
const {RateSelector} = require("devtools/client/animationinspector/components/rate-selector");
const {formatStopwatchTime} = require("devtools/client/animationinspector/utils");
const {KeyCodes} = require("devtools/client/shared/keycodes");
var $ = (selector, target = document) => target.querySelector(selector);
/**
* The main animations panel UI.
*/
var AnimationsPanel = {
UI_UPDATED_EVENT: "ui-updated",
PANEL_INITIALIZED: "panel-initialized",
initialize: Task.async(function* () {
if (AnimationsController.destroyed) {
console.warn("Could not initialize the animation-panel, controller " +
"was destroyed");
return;
}
if (this.initialized) {
yield this.initialized;
return;
}
let resolver;
this.initialized = new Promise(resolve => {
resolver = resolve;
});
this.playersEl = $("#players");
this.errorMessageEl = $("#error-message");
this.pickerButtonEl = $("#element-picker");
this.toggleAllButtonEl = $("#toggle-all");
this.playTimelineButtonEl = $("#pause-resume-timeline");
this.rewindTimelineButtonEl = $("#rewind-timeline");
this.timelineCurrentTimeEl = $("#timeline-current-time");
this.rateSelectorEl = $("#timeline-rate");
this.rewindTimelineButtonEl.setAttribute("title",
L10N.getStr("timeline.rewindButtonTooltip"));
$("#all-animations-label").textContent = L10N.getStr("panel.allAnimations");
// If the server doesn't support toggling all animations at once, hide the
// whole global toolbar.
if (!AnimationsController.traits.hasToggleAll) {
$("#global-toolbar").style.display = "none";
}
// Binding functions that need to be called in scope.
for (let functionName of ["onKeyDown", "onPickerStarted",
"onPickerStopped", "refreshAnimationsUI", "onToggleAllClicked",
"onTabNavigated", "onTimelineDataChanged", "onTimelinePlayClicked",
"onTimelineRewindClicked", "onRateChanged"]) {
this[functionName] = this[functionName].bind(this);
}
let hUtils = gToolbox.highlighterUtils;
this.togglePicker = hUtils.togglePicker.bind(hUtils);
this.animationsTimelineComponent = new AnimationsTimeline(gInspector,
AnimationsController.traits);
this.animationsTimelineComponent.init(this.playersEl);
if (AnimationsController.traits.hasSetPlaybackRate) {
this.rateSelectorComponent = new RateSelector();
this.rateSelectorComponent.init(this.rateSelectorEl);
}
this.startListeners();
yield this.refreshAnimationsUI();
resolver();
this.emit(this.PANEL_INITIALIZED);
}),
destroy: Task.async(function* () {
if (!this.initialized) {
return;
}
if (this.destroyed) {
yield this.destroyed;
return;
}
let resolver;
this.destroyed = new Promise(resolve => {
resolver = resolve;
});
this.stopListeners();
this.animationsTimelineComponent.destroy();
this.animationsTimelineComponent = null;
if (this.rateSelectorComponent) {
this.rateSelectorComponent.destroy();
this.rateSelectorComponent = null;
}
this.playersEl = this.errorMessageEl = null;
this.toggleAllButtonEl = this.pickerButtonEl = null;
this.playTimelineButtonEl = this.rewindTimelineButtonEl = null;
this.timelineCurrentTimeEl = this.rateSelectorEl = null;
resolver();
}),
startListeners: function () {
AnimationsController.on(AnimationsController.PLAYERS_UPDATED_EVENT,
this.refreshAnimationsUI);
this.pickerButtonEl.addEventListener("click", this.togglePicker);
gToolbox.on("picker-started", this.onPickerStarted);
gToolbox.on("picker-stopped", this.onPickerStopped);
this.toggleAllButtonEl.addEventListener("click", this.onToggleAllClicked);
this.playTimelineButtonEl.addEventListener(
"click", this.onTimelinePlayClicked);
this.rewindTimelineButtonEl.addEventListener(
"click", this.onTimelineRewindClicked);
document.addEventListener("keydown", this.onKeyDown, false);
gToolbox.target.on("navigate", this.onTabNavigated);
this.animationsTimelineComponent.on("timeline-data-changed",
this.onTimelineDataChanged);
if (this.rateSelectorComponent) {
this.rateSelectorComponent.on("rate-changed", this.onRateChanged);
}
},
stopListeners: function () {
AnimationsController.off(AnimationsController.PLAYERS_UPDATED_EVENT,
this.refreshAnimationsUI);
this.pickerButtonEl.removeEventListener("click", this.togglePicker);
gToolbox.off("picker-started", this.onPickerStarted);
gToolbox.off("picker-stopped", this.onPickerStopped);
this.toggleAllButtonEl.removeEventListener("click",
this.onToggleAllClicked);
this.playTimelineButtonEl.removeEventListener("click",
this.onTimelinePlayClicked);
this.rewindTimelineButtonEl.removeEventListener("click",
this.onTimelineRewindClicked);
document.removeEventListener("keydown", this.onKeyDown, false);
gToolbox.target.off("navigate", this.onTabNavigated);
this.animationsTimelineComponent.off("timeline-data-changed",
this.onTimelineDataChanged);
if (this.rateSelectorComponent) {
this.rateSelectorComponent.off("rate-changed", this.onRateChanged);
}
},
onKeyDown: function (event) {
// If the space key is pressed, it should toggle the play state of
// the animations displayed in the panel, or of all the animations on
// the page if the selected node does not have any animation on it.
if (event.keyCode === KeyCodes.DOM_VK_SPACE) {
if (AnimationsController.animationPlayers.length > 0) {
this.playPauseTimeline().catch(ex => console.error(ex));
} else {
this.toggleAll().catch(ex => console.error(ex));
}
event.preventDefault();
}
},
togglePlayers: function (isVisible) {
if (isVisible) {
document.body.removeAttribute("empty");
document.body.setAttribute("timeline", "true");
} else {
document.body.setAttribute("empty", "true");
document.body.removeAttribute("timeline");
$("#error-type").textContent = L10N.getStr("panel.invalidElementSelected");
$("#error-hint").textContent = L10N.getStr("panel.selectElement");
}
},
onPickerStarted: function () {
this.pickerButtonEl.setAttribute("checked", "true");
},
onPickerStopped: function () {
this.pickerButtonEl.removeAttribute("checked");
},
onToggleAllClicked: function () {
this.toggleAll().catch(ex => console.error(ex));
},
/**
* Toggle (pause/play) all animations in the current target
* and update the UI the toggleAll button.
*/
toggleAll: Task.async(function* () {
this.toggleAllButtonEl.classList.toggle("paused");
yield AnimationsController.toggleAll();
}),
onTimelinePlayClicked: function () {
this.playPauseTimeline().catch(ex => console.error(ex));
},
/**
* Depending on the state of the timeline either pause or play the animations
* displayed in it.
* If the animations are finished, this will play them from the start again.
* If the animations are playing, this will pause them.
* If the animations are paused, this will resume them.
*
* @return {Promise} Resolves when the playState is changed and the UI
* is refreshed
*/
playPauseTimeline: function () {
return AnimationsController
.toggleCurrentAnimations(this.timelineData.isMoving)
.then(() => this.refreshAnimationsStateAndUI());
},
onTimelineRewindClicked: function () {
this.rewindTimeline().catch(ex => console.error(ex));
},
/**
* Reset the startTime of all current animations shown in the timeline and
* pause them.
*
* @return {Promise} Resolves when currentTime is set and the UI is refreshed
*/
rewindTimeline: function () {
return AnimationsController
.setCurrentTimeAll(0, true)
.then(() => this.refreshAnimationsStateAndUI());
},
/**
* Set the playback rate of all current animations shown in the timeline to
* the value of this.rateSelectorEl.
*/
onRateChanged: function (e, rate) {
AnimationsController.setPlaybackRateAll(rate)
.then(() => this.refreshAnimationsStateAndUI())
.catch(ex => console.error(ex));
},
onTabNavigated: function () {
this.toggleAllButtonEl.classList.remove("paused");
},
onTimelineDataChanged: function (e, data) {
this.timelineData = data;
let {isMoving, isUserDrag, time} = data;
this.playTimelineButtonEl.classList.toggle("paused", !isMoving);
let l10nPlayProperty = isMoving ? "timeline.resumedButtonTooltip" :
"timeline.pausedButtonTooltip";
this.playTimelineButtonEl.setAttribute("title",
L10N.getStr(l10nPlayProperty));
// If the timeline data changed as a result of the user dragging the
// scrubber, then pause all animations and set their currentTimes.
// (Note that we want server-side requests to be sequenced, so we only do
// this after the previous currentTime setting was done).
if (isUserDrag && !this.setCurrentTimeAllPromise) {
this.setCurrentTimeAllPromise =
AnimationsController.setCurrentTimeAll(time, true)
.catch(error => console.error(error))
.then(() => {
this.setCurrentTimeAllPromise = null;
});
}
this.displayTimelineCurrentTime();
},
displayTimelineCurrentTime: function () {
let {time} = this.timelineData;
this.timelineCurrentTimeEl.textContent = formatStopwatchTime(time);
},
/**
* Make sure all known animations have their states up to date (which is
* useful after the playState or currentTime has been changed and in case the
* animations aren't auto-refreshing), and then refresh the UI.
*/
refreshAnimationsStateAndUI: Task.async(function* () {
for (let player of AnimationsController.animationPlayers) {
yield player.refreshState();
}
yield this.refreshAnimationsUI();
}),
/**
* Refresh the list of animations UI. This will empty the panel and re-render
* the various components again.
*/
refreshAnimationsUI: Task.async(function* () {
// Empty the whole panel first.
this.togglePlayers(true);
// Re-render the timeline component.
this.animationsTimelineComponent.render(
AnimationsController.animationPlayers,
AnimationsController.documentCurrentTime);
// Re-render the rate selector component.
if (this.rateSelectorComponent) {
this.rateSelectorComponent.render(AnimationsController.animationPlayers);
}
// If there are no players to show, show the error message instead and
// return.
if (!AnimationsController.animationPlayers.length) {
this.togglePlayers(false);
this.emit(this.UI_UPDATED_EVENT);
return;
}
this.emit(this.UI_UPDATED_EVENT);
})
};
EventEmitter.decorate(AnimationsPanel);

View file

@ -0,0 +1,222 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript 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";
const {Task} = require("devtools/shared/task");
const EventEmitter = require("devtools/shared/event-emitter");
const {createNode, TimeScale} = require("devtools/client/animationinspector/utils");
const {Keyframes} = require("devtools/client/animationinspector/components/keyframes");
/**
* UI component responsible for displaying detailed information for a given
* animation.
* This includes information about timing, easing, keyframes, animated
* properties.
*
* @param {Object} serverTraits The list of server-side capabilities.
*/
function AnimationDetails(serverTraits) {
EventEmitter.decorate(this);
this.onFrameSelected = this.onFrameSelected.bind(this);
this.keyframeComponents = [];
this.serverTraits = serverTraits;
}
exports.AnimationDetails = AnimationDetails;
AnimationDetails.prototype = {
// These are part of frame objects but are not animated properties. This
// array is used to skip them.
NON_PROPERTIES: ["easing", "composite", "computedOffset", "offset"],
init: function (containerEl) {
this.containerEl = containerEl;
},
destroy: function () {
this.unrender();
this.containerEl = null;
this.serverTraits = null;
},
unrender: function () {
for (let component of this.keyframeComponents) {
component.off("frame-selected", this.onFrameSelected);
component.destroy();
}
this.keyframeComponents = [];
while (this.containerEl.firstChild) {
this.containerEl.firstChild.remove();
}
},
getPerfDataForProperty: function (animation, propertyName) {
let warning = "";
let className = "";
if (animation.state.propertyState) {
let isRunningOnCompositor;
for (let propState of animation.state.propertyState) {
if (propState.property == propertyName) {
isRunningOnCompositor = propState.runningOnCompositor;
if (typeof propState.warning != "undefined") {
warning = propState.warning;
}
break;
}
}
if (isRunningOnCompositor && warning == "") {
className = "oncompositor";
} else if (!isRunningOnCompositor && warning != "") {
className = "warning";
}
}
return {className, warning};
},
/**
* Get a list of the tracks of the animation actor
* @return {Object} A list of tracks, one per animated property, each
* with a list of keyframes
*/
getTracks: Task.async(function* () {
let tracks = {};
/*
* getFrames is a AnimationPlayorActor method that returns data about the
* keyframes of the animation.
* In FF48, the data it returns change, and will hold only longhand
* properties ( e.g. borderLeftWidth ), which does not match what we
* want to display in the animation detail.
* A new AnimationPlayerActor function, getProperties, is introduced,
* that returns the animated css properties of the animation and their
* keyframes values.
* If the animation actor has the getProperties function, we use it, and if
* not, we fall back to getFrames, which then returns values we used to
* handle.
*/
if (this.serverTraits.hasGetProperties) {
let properties = yield this.animation.getProperties();
for (let {name, values} of properties) {
if (!tracks[name]) {
tracks[name] = [];
}
for (let {value, offset} of values) {
tracks[name].push({value, offset});
}
}
} else {
let frames = yield this.animation.getFrames();
for (let frame of frames) {
for (let name in frame) {
if (this.NON_PROPERTIES.indexOf(name) != -1) {
continue;
}
if (!tracks[name]) {
tracks[name] = [];
}
tracks[name].push({
value: frame[name],
offset: frame.computedOffset
});
}
}
}
return tracks;
}),
render: Task.async(function* (animation) {
this.unrender();
if (!animation) {
return;
}
this.animation = animation;
// We might have been destroyed in the meantime, or the component might
// have been re-rendered.
if (!this.containerEl || this.animation !== animation) {
return;
}
// Build an element for each animated property track.
this.tracks = yield this.getTracks(animation, this.serverTraits);
// Useful for tests to know when the keyframes have been retrieved.
this.emit("keyframes-retrieved");
for (let propertyName in this.tracks) {
let line = createNode({
parent: this.containerEl,
attributes: {"class": "property"}
});
let {warning, className} =
this.getPerfDataForProperty(animation, propertyName);
createNode({
// text-overflow doesn't work in flex items, so we need a second level
// of container to actually have an ellipsis on the name.
// See bug 972664.
parent: createNode({
parent: line,
attributes: {"class": "name"}
}),
textContent: getCssPropertyName(propertyName),
attributes: {"title": warning,
"class": className}
});
// Add the keyframes diagram for this property.
let framesWrapperEl = createNode({
parent: line,
attributes: {"class": "track-container"}
});
let framesEl = createNode({
parent: framesWrapperEl,
attributes: {"class": "frames"}
});
// Scale the list of keyframes according to the current time scale.
let {x, w} = TimeScale.getAnimationDimensions(animation);
framesEl.style.left = `${x}%`;
framesEl.style.width = `${w}%`;
let keyframesComponent = new Keyframes();
keyframesComponent.init(framesEl);
keyframesComponent.render({
keyframes: this.tracks[propertyName],
propertyName: propertyName,
animation: animation
});
keyframesComponent.on("frame-selected", this.onFrameSelected);
this.keyframeComponents.push(keyframesComponent);
}
}),
onFrameSelected: function (e, args) {
// Relay the event up, it's needed in parents too.
this.emit(e, args);
}
};
/**
* Turn propertyName into property-name.
* @param {String} jsPropertyName A camelcased CSS property name. Typically
* something that comes out of computed styles. E.g. borderBottomColor
* @return {String} The corresponding CSS property name: border-bottom-color
*/
function getCssPropertyName(jsPropertyName) {
return jsPropertyName.replace(/[A-Z]/g, "-$&").toLowerCase();
}
exports.getCssPropertyName = getCssPropertyName;

View file

@ -0,0 +1,80 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript 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";
const {Task} = require("devtools/shared/task");
const EventEmitter = require("devtools/shared/event-emitter");
const {DomNodePreview} = require("devtools/client/inspector/shared/dom-node-preview");
// Map dom node fronts by animation fronts so we don't have to get them from the
// walker every time the timeline is refreshed.
var nodeFronts = new WeakMap();
/**
* UI component responsible for displaying a preview of the target dom node of
* a given animation.
* Accepts the same parameters as the DomNodePreview component. See
* devtools/client/inspector/shared/dom-node-preview.js for documentation.
*/
function AnimationTargetNode(inspector, options) {
this.inspector = inspector;
this.previewer = new DomNodePreview(inspector, options);
EventEmitter.decorate(this);
}
exports.AnimationTargetNode = AnimationTargetNode;
AnimationTargetNode.prototype = {
init: function (containerEl) {
this.previewer.init(containerEl);
this.isDestroyed = false;
},
destroy: function () {
this.previewer.destroy();
this.inspector = null;
this.isDestroyed = true;
},
render: Task.async(function* (playerFront) {
// Get the nodeFront from the cache if it was stored previously.
let nodeFront = nodeFronts.get(playerFront);
// Try and get it from the playerFront directly next.
if (!nodeFront) {
nodeFront = playerFront.animationTargetNodeFront;
}
// Finally, get it from the walkerActor if it wasn't found.
if (!nodeFront) {
try {
nodeFront = yield this.inspector.walker.getNodeFromActor(
playerFront.actorID, ["node"]);
} catch (e) {
// If an error occured while getting the nodeFront and if it can't be
// attributed to the panel having been destroyed in the meantime, this
// error needs to be logged and render needs to stop.
if (!this.isDestroyed) {
console.error(e);
}
return;
}
// In all cases, if by now the panel doesn't exist anymore, we need to
// stop rendering too.
if (this.isDestroyed) {
return;
}
}
// Add the nodeFront to the cache.
nodeFronts.set(playerFront, nodeFront);
this.previewer.render(nodeFront);
this.emit("target-retrieved");
})
};

View file

@ -0,0 +1,719 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript 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";
const EventEmitter = require("devtools/shared/event-emitter");
const {createNode, TimeScale} = require("devtools/client/animationinspector/utils");
const { LocalizationHelper } = require("devtools/shared/l10n");
const L10N =
new LocalizationHelper("devtools/client/locales/animationinspector.properties");
// In the createPathSegments function, an animation duration is divided by
// DURATION_RESOLUTION in order to draw the way the animation progresses.
// But depending on the timing-function, we may be not able to make the graph
// smoothly progress if this resolution is not high enough.
// So, if the difference of animation progress between 2 divisions is more than
// MIN_PROGRESS_THRESHOLD, then createPathSegments re-divides
// by DURATION_RESOLUTION.
// DURATION_RESOLUTION shoud be integer and more than 2.
const DURATION_RESOLUTION = 4;
// MIN_PROGRESS_THRESHOLD shoud be between more than 0 to 1.
const MIN_PROGRESS_THRESHOLD = 0.1;
// Show max 10 iterations for infinite animations
// to give users a clue that the animation does repeat.
const MAX_INFINITE_ANIMATIONS_ITERATIONS = 10;
// SVG namespace
const SVG_NS = "http://www.w3.org/2000/svg";
/**
* UI component responsible for displaying a single animation timeline, which
* basically looks like a rectangle that shows the delay and iterations.
*/
function AnimationTimeBlock() {
EventEmitter.decorate(this);
this.onClick = this.onClick.bind(this);
}
exports.AnimationTimeBlock = AnimationTimeBlock;
AnimationTimeBlock.prototype = {
init: function (containerEl) {
this.containerEl = containerEl;
this.containerEl.addEventListener("click", this.onClick);
},
destroy: function () {
this.containerEl.removeEventListener("click", this.onClick);
this.unrender();
this.containerEl = null;
this.animation = null;
},
unrender: function () {
while (this.containerEl.firstChild) {
this.containerEl.firstChild.remove();
}
},
render: function (animation) {
this.unrender();
this.animation = animation;
let {state} = this.animation;
// Create a container element to hold the delay and iterations.
// It is positioned according to its delay (divided by the playbackrate),
// and its width is according to its duration (divided by the playbackrate).
const {x, delayX, delayW, endDelayX, endDelayW} =
TimeScale.getAnimationDimensions(animation);
// Animation summary graph element.
const summaryEl = createNode({
parent: this.containerEl,
namespace: "http://www.w3.org/2000/svg",
nodeType: "svg",
attributes: {
"class": "summary",
"preserveAspectRatio": "none",
"style": `left: ${ x - (state.delay > 0 ? delayW : 0) }%`
}
});
// Total displayed duration
const totalDisplayedDuration = state.playbackRate * TimeScale.getDuration();
// Calculate stroke height in viewBox to display stroke of path.
const strokeHeightForViewBox = 0.5 / this.containerEl.clientHeight;
// Set viewBox
summaryEl.setAttribute("viewBox",
`${ state.delay < 0 ? state.delay : 0 }
-${ 1 + strokeHeightForViewBox }
${ totalDisplayedDuration }
${ 1 + strokeHeightForViewBox * 2 }`);
// Get a helper function that returns the path segment of timing-function.
const segmentHelper = getSegmentHelper(state, this.win);
// Minimum segment duration is the duration of one pixel.
const minSegmentDuration =
totalDisplayedDuration / this.containerEl.clientWidth;
// Minimum progress threshold.
let minProgressThreshold = MIN_PROGRESS_THRESHOLD;
// If the easing is step function,
// minProgressThreshold should be changed by the steps.
const stepFunction = state.easing.match(/steps\((\d+)/);
if (stepFunction) {
minProgressThreshold = 1 / (parseInt(stepFunction[1], 10) + 1);
}
// Starting time of main iteration.
let mainIterationStartTime = 0;
let iterationStart = state.iterationStart;
let iterationCount = state.iterationCount ? state.iterationCount : Infinity;
// Append delay.
if (state.delay > 0) {
renderDelay(summaryEl, state, segmentHelper);
mainIterationStartTime = state.delay;
} else {
const negativeDelayCount = -state.delay / state.duration;
// Move to forward the starting point for negative delay.
iterationStart += negativeDelayCount;
// Consume iteration count by negative delay.
if (iterationCount !== Infinity) {
iterationCount -= negativeDelayCount;
}
}
// Append 1st section of iterations,
// This section is only useful in cases where iterationStart has decimals.
// e.g.
// if { iterationStart: 0.25, iterations: 3 }, firstSectionCount is 0.75.
const firstSectionCount =
iterationStart % 1 === 0
? 0 : Math.min(iterationCount, 1) - iterationStart % 1;
if (firstSectionCount) {
renderFirstIteration(summaryEl, state, mainIterationStartTime,
firstSectionCount, minSegmentDuration,
minProgressThreshold, segmentHelper);
}
if (iterationCount === Infinity) {
// If the animation repeats infinitely,
// we fill the remaining area with iteration paths.
renderInfinity(summaryEl, state, mainIterationStartTime,
firstSectionCount, totalDisplayedDuration,
minSegmentDuration, minProgressThreshold, segmentHelper);
} else {
// Otherwise, we show remaining iterations, endDelay and fill.
// Append forwards fill-mode.
if (state.fill === "both" || state.fill === "forwards") {
renderForwardsFill(summaryEl, state, mainIterationStartTime,
iterationCount, totalDisplayedDuration,
segmentHelper);
}
// Append middle section of iterations.
// e.g.
// if { iterationStart: 0.25, iterations: 3 }, middleSectionCount is 2.
const middleSectionCount =
Math.floor(iterationCount - firstSectionCount);
renderMiddleIterations(summaryEl, state, mainIterationStartTime,
firstSectionCount, middleSectionCount,
minSegmentDuration, minProgressThreshold,
segmentHelper);
// Append last section of iterations, if there is remaining iteration.
// e.g.
// if { iterationStart: 0.25, iterations: 3 }, lastSectionCount is 0.25.
const lastSectionCount =
iterationCount - middleSectionCount - firstSectionCount;
if (lastSectionCount) {
renderLastIteration(summaryEl, state, mainIterationStartTime,
firstSectionCount, middleSectionCount,
lastSectionCount, minSegmentDuration,
minProgressThreshold, segmentHelper);
}
// Append endDelay.
if (state.endDelay > 0) {
renderEndDelay(summaryEl, state,
mainIterationStartTime, iterationCount, segmentHelper);
}
}
// Append negative delay (which overlap the animation).
if (state.delay < 0) {
segmentHelper.animation.effect.timing.fill = "both";
segmentHelper.asOriginalBehavior = false;
renderNegativeDelayHiddenProgress(summaryEl, state, minSegmentDuration,
minProgressThreshold, segmentHelper);
}
// Append negative endDelay (which overlap the animation).
if (state.iterationCount && state.endDelay < 0) {
if (segmentHelper.asOriginalBehavior) {
segmentHelper.animation.effect.timing.fill = "both";
segmentHelper.asOriginalBehavior = false;
}
renderNegativeEndDelayHiddenProgress(summaryEl, state,
minSegmentDuration,
minProgressThreshold,
segmentHelper);
}
// The animation name is displayed over the animation.
createNode({
parent: createNode({
parent: this.containerEl,
attributes: {
"class": "name",
"title": this.getTooltipText(state)
},
}),
textContent: state.name
});
// Delay.
if (state.delay) {
// Negative delays need to start at 0.
createNode({
parent: this.containerEl,
attributes: {
"class": "delay"
+ (state.delay < 0 ? " negative" : " positive")
+ (state.fill === "both" ||
state.fill === "backwards" ? " fill" : ""),
"style": `left:${ delayX }%; width:${ delayW }%;`
}
});
}
// endDelay
if (state.iterationCount && state.endDelay) {
createNode({
parent: this.containerEl,
attributes: {
"class": "end-delay"
+ (state.endDelay < 0 ? " negative" : " positive")
+ (state.fill === "both" ||
state.fill === "forwards" ? " fill" : ""),
"style": `left:${ endDelayX }%; width:${ endDelayW }%;`
}
});
}
},
getTooltipText: function (state) {
let getTime = time => L10N.getFormatStr("player.timeLabel",
L10N.numberWithDecimals(time / 1000, 2));
let text = "";
// Adding the name.
text += getFormattedAnimationTitle({state});
text += "\n";
// Adding the delay.
if (state.delay) {
text += L10N.getStr("player.animationDelayLabel") + " ";
text += getTime(state.delay);
text += "\n";
}
// Adding the duration.
text += L10N.getStr("player.animationDurationLabel") + " ";
text += getTime(state.duration);
text += "\n";
// Adding the endDelay.
if (state.endDelay) {
text += L10N.getStr("player.animationEndDelayLabel") + " ";
text += getTime(state.endDelay);
text += "\n";
}
// Adding the iteration count (the infinite symbol, or an integer).
if (state.iterationCount !== 1) {
text += L10N.getStr("player.animationIterationCountLabel") + " ";
text += state.iterationCount ||
L10N.getStr("player.infiniteIterationCountText");
text += "\n";
}
// Adding the iteration start.
if (state.iterationStart !== 0) {
let iterationStartTime = state.iterationStart * state.duration / 1000;
text += L10N.getFormatStr("player.animationIterationStartLabel",
state.iterationStart,
L10N.numberWithDecimals(iterationStartTime, 2));
text += "\n";
}
// Adding the easing.
if (state.easing) {
text += L10N.getStr("player.animationEasingLabel") + " ";
text += state.easing;
text += "\n";
}
// Adding the fill mode.
if (state.fill) {
text += L10N.getStr("player.animationFillLabel") + " ";
text += state.fill;
text += "\n";
}
// Adding the direction mode.
if (state.direction) {
text += L10N.getStr("player.animationDirectionLabel") + " ";
text += state.direction;
text += "\n";
}
// Adding the playback rate if it's different than 1.
if (state.playbackRate !== 1) {
text += L10N.getStr("player.animationRateLabel") + " ";
text += state.playbackRate;
text += "\n";
}
// Adding a note that the animation is running on the compositor thread if
// needed.
if (state.propertyState) {
if (state.propertyState
.every(propState => propState.runningOnCompositor)) {
text += L10N.getStr("player.allPropertiesOnCompositorTooltip");
} else if (state.propertyState
.some(propState => propState.runningOnCompositor)) {
text += L10N.getStr("player.somePropertiesOnCompositorTooltip");
}
} else if (state.isRunningOnCompositor) {
text += L10N.getStr("player.runningOnCompositorTooltip");
}
return text;
},
onClick: function (e) {
e.stopPropagation();
this.emit("selected", this.animation);
},
get win() {
return this.containerEl.ownerDocument.defaultView;
}
};
/**
* Get a formatted title for this animation. This will be either:
* "some-name", "some-name : CSS Transition", "some-name : CSS Animation",
* "some-name : Script Animation", or "Script Animation", depending
* if the server provides the type, what type it is and if the animation
* has a name
* @param {AnimationPlayerFront} animation
*/
function getFormattedAnimationTitle({state}) {
// Older servers don't send a type, and only know about
// CSSAnimations and CSSTransitions, so it's safe to use
// just the name.
if (!state.type) {
return state.name;
}
// Script-generated animations may not have a name.
if (state.type === "scriptanimation" && !state.name) {
return L10N.getStr("timeline.scriptanimation.unnamedLabel");
}
return L10N.getFormatStr(`timeline.${state.type}.nameLabel`, state.name);
}
/**
* Render delay section.
* @param {Element} parentEl - Parent element of this appended path element.
* @param {Object} state - State of animation.
* @param {Object} segmentHelper - The object returned by getSegmentHelper.
*/
function renderDelay(parentEl, state, segmentHelper) {
const startSegment = segmentHelper.getSegment(0);
const endSegment = { x: state.delay, y: startSegment.y };
appendPathElement(parentEl, [startSegment, endSegment], "delay-path");
}
/**
* Render first iteration section.
* @param {Element} parentEl - Parent element of this appended path element.
* @param {Object} state - State of animation.
* @param {Number} mainIterationStartTime - Starting time of main iteration.
* @param {Number} firstSectionCount - Iteration count of first section.
* @param {Number} minSegmentDuration - Minimum segment duration.
* @param {Number} minProgressThreshold - Minimum progress threshold.
* @param {Object} segmentHelper - The object returned by getSegmentHelper.
*/
function renderFirstIteration(parentEl, state, mainIterationStartTime,
firstSectionCount, minSegmentDuration,
minProgressThreshold, segmentHelper) {
const startTime = mainIterationStartTime;
const endTime = startTime + firstSectionCount * state.duration;
const segments =
createPathSegments(startTime, endTime, minSegmentDuration,
minProgressThreshold, segmentHelper);
appendPathElement(parentEl, segments, "iteration-path");
}
/**
* Render middle iterations section.
* @param {Element} parentEl - Parent element of this appended path element.
* @param {Object} state - State of animation.
* @param {Number} mainIterationStartTime - Starting time of main iteration.
* @param {Number} firstSectionCount - Iteration count of first section.
* @param {Number} middleSectionCount - Iteration count of middle section.
* @param {Number} minSegmentDuration - Minimum segment duration.
* @param {Number} minProgressThreshold - Minimum progress threshold.
* @param {Object} segmentHelper - The object returned by getSegmentHelper.
*/
function renderMiddleIterations(parentEl, state, mainIterationStartTime,
firstSectionCount, middleSectionCount,
minSegmentDuration, minProgressThreshold,
segmentHelper) {
const offset = mainIterationStartTime + firstSectionCount * state.duration;
for (let i = 0; i < middleSectionCount; i++) {
// Get the path segments of each iteration.
const startTime = offset + i * state.duration;
const endTime = startTime + state.duration;
const segments =
createPathSegments(startTime, endTime, minSegmentDuration,
minProgressThreshold, segmentHelper);
appendPathElement(parentEl, segments, "iteration-path");
}
}
/**
* Render last iteration section.
* @param {Element} parentEl - Parent element of this appended path element.
* @param {Object} state - State of animation.
* @param {Number} mainIterationStartTime - Starting time of main iteration.
* @param {Number} firstSectionCount - Iteration count of first section.
* @param {Number} middleSectionCount - Iteration count of middle section.
* @param {Number} lastSectionCount - Iteration count of last section.
* @param {Number} minSegmentDuration - Minimum segment duration.
* @param {Number} minProgressThreshold - Minimum progress threshold.
* @param {Object} segmentHelper - The object returned by getSegmentHelper.
*/
function renderLastIteration(parentEl, state, mainIterationStartTime,
firstSectionCount, middleSectionCount,
lastSectionCount, minSegmentDuration,
minProgressThreshold, segmentHelper) {
const startTime = mainIterationStartTime +
(firstSectionCount + middleSectionCount) * state.duration;
const endTime = startTime + lastSectionCount * state.duration;
const segments =
createPathSegments(startTime, endTime, minSegmentDuration,
minProgressThreshold, segmentHelper);
appendPathElement(parentEl, segments, "iteration-path");
}
/**
* Render Infinity iterations.
* @param {Element} parentEl - Parent element of this appended path element.
* @param {Object} state - State of animation.
* @param {Number} mainIterationStartTime - Starting time of main iteration.
* @param {Number} firstSectionCount - Iteration count of first section.
* @param {Number} totalDuration - Displayed max duration.
* @param {Number} minSegmentDuration - Minimum segment duration.
* @param {Number} minProgressThreshold - Minimum progress threshold.
* @param {Object} segmentHelper - The object returned by getSegmentHelper.
*/
function renderInfinity(parentEl, state, mainIterationStartTime,
firstSectionCount, totalDuration, minSegmentDuration,
minProgressThreshold, segmentHelper) {
// Calculate the number of iterations to display,
// with a maximum of MAX_INFINITE_ANIMATIONS_ITERATIONS
let uncappedInfinityIterationCount =
(totalDuration - firstSectionCount * state.duration) / state.duration;
// If there is a small floating point error resulting in, e.g. 1.0000001
// ceil will give us 2 so round first.
uncappedInfinityIterationCount =
parseFloat(uncappedInfinityIterationCount.toPrecision(6));
const infinityIterationCount =
Math.min(MAX_INFINITE_ANIMATIONS_ITERATIONS,
Math.ceil(uncappedInfinityIterationCount));
// Append first full iteration path.
const firstStartTime =
mainIterationStartTime + firstSectionCount * state.duration;
const firstEndTime = firstStartTime + state.duration;
const firstSegments =
createPathSegments(firstStartTime, firstEndTime, minSegmentDuration,
minProgressThreshold, segmentHelper);
appendPathElement(parentEl, firstSegments, "iteration-path infinity");
// Append other iterations. We can copy first segments.
const isAlternate = state.direction.match(/alternate/);
for (let i = 1; i < infinityIterationCount; i++) {
const startTime = firstStartTime + i * state.duration;
let segments;
if (isAlternate && i % 2) {
// Copy as reverse.
segments = firstSegments.map(segment => {
return { x: firstEndTime - segment.x + startTime, y: segment.y };
});
} else {
// Copy as is.
segments = firstSegments.map(segment => {
return { x: segment.x - firstStartTime + startTime, y: segment.y };
});
}
appendPathElement(parentEl, segments, "iteration-path infinity copied");
}
}
/**
* Render endDelay section.
* @param {Element} parentEl - Parent element of this appended path element.
* @param {Object} state - State of animation.
* @param {Number} mainIterationStartTime - Starting time of main iteration.
* @param {Number} iterationCount - Whole iteration count.
* @param {Object} segmentHelper - The object returned by getSegmentHelper.
*/
function renderEndDelay(parentEl, state,
mainIterationStartTime, iterationCount, segmentHelper) {
const startTime = mainIterationStartTime + iterationCount * state.duration;
const startSegment = segmentHelper.getSegment(startTime);
const endSegment = { x: startTime + state.endDelay, y: startSegment.y };
appendPathElement(parentEl, [startSegment, endSegment], "enddelay-path");
}
/**
* Render forwards fill section.
* @param {Element} parentEl - Parent element of this appended path element.
* @param {Object} state - State of animation.
* @param {Number} mainIterationStartTime - Starting time of main iteration.
* @param {Number} iterationCount - Whole iteration count.
* @param {Number} totalDuration - Displayed max duration.
* @param {Object} segmentHelper - The object returned by getSegmentHelper.
*/
function renderForwardsFill(parentEl, state, mainIterationStartTime,
iterationCount, totalDuration, segmentHelper) {
const startTime = mainIterationStartTime + iterationCount * state.duration +
(state.endDelay > 0 ? state.endDelay : 0);
const startSegment = segmentHelper.getSegment(startTime);
const endSegment = { x: totalDuration, y: startSegment.y };
appendPathElement(parentEl, [startSegment, endSegment], "fill-forwards-path");
}
/**
* Render hidden progress of negative delay.
* @param {Element} parentEl - Parent element of this appended path element.
* @param {Object} state - State of animation.
* @param {Number} minSegmentDuration - Minimum segment duration.
* @param {Number} minProgressThreshold - Minimum progress threshold.
* @param {Object} segmentHelper - The object returned by getSegmentHelper.
*/
function renderNegativeDelayHiddenProgress(parentEl, state, minSegmentDuration,
minProgressThreshold,
segmentHelper) {
const startTime = state.delay;
const endTime = 0;
const segments =
createPathSegments(startTime, endTime, minSegmentDuration,
minProgressThreshold, segmentHelper);
appendPathElement(parentEl, segments, "delay-path negative");
}
/**
* Render hidden progress of negative endDelay.
* @param {Element} parentEl - Parent element of this appended path element.
* @param {Object} state - State of animation.
* @param {Number} minSegmentDuration - Minimum segment duration.
* @param {Number} minProgressThreshold - Minimum progress threshold.
* @param {Object} segmentHelper - The object returned by getSegmentHelper.
*/
function renderNegativeEndDelayHiddenProgress(parentEl, state,
minSegmentDuration,
minProgressThreshold,
segmentHelper) {
const endTime = state.delay + state.iterationCount * state.duration;
const startTime = endTime + state.endDelay;
const segments =
createPathSegments(startTime, endTime, minSegmentDuration,
minProgressThreshold, segmentHelper);
appendPathElement(parentEl, segments, "enddelay-path negative");
}
/**
* Get a helper function which returns the segment coord from given time.
* @param {Object} state - animation state
* @param {Object} win - window object
* @return {Object} A segmentHelper object that has the following properties:
* - animation: The script animation used to get the progress
* - endTime: The end time of the animation
* - asOriginalBehavior: The spec is that the progress of animation is changed
* if the time of setCurrentTime is during the endDelay.
* Likewise, in case the time is less than 0.
* If this flag is true, we prevent the time
* to make the same animation behavior as the original.
* - getSegment: Helper function that, given a time,
* will calculate the progress through the dummy animation.
*/
function getSegmentHelper(state, win) {
// Create a dummy Animation timing data as the
// state object we're being passed in.
const timing = Object.assign({}, state, {
iterations: state.iterationCount ? state.iterationCount : Infinity
});
// Create a dummy Animation with the given timing.
const dummyAnimation =
new win.Animation(new win.KeyframeEffect(null, null, timing), null);
// Returns segment helper object.
return {
animation: dummyAnimation,
endTime: dummyAnimation.effect.getComputedTiming().endTime,
asOriginalBehavior: true,
getSegment: function (time) {
if (this.asOriginalBehavior) {
// If the given time is less than 0, returned progress is 0.
if (time < 0) {
return { x: time, y: 0 };
}
// Avoid to apply over endTime.
this.animation.currentTime = time < this.endTime ? time : this.endTime;
} else {
this.animation.currentTime = time;
}
const progress = this.animation.effect.getComputedTiming().progress;
return { x: time, y: Math.max(progress, 0) };
}
};
}
/**
* Create the path segments from given parameters.
* @param {Number} startTime - Starting time of animation.
* @param {Number} endTime - Ending time of animation.
* @param {Number} minSegmentDuration - Minimum segment duration.
* @param {Number} minProgressThreshold - Minimum progress threshold.
* @param {Object} segmentHelper - The object of getSegmentHelper.
* @return {Array} path segments -
* [{x: {Number} time, y: {Number} progress}, ...]
*/
function createPathSegments(startTime, endTime, minSegmentDuration,
minProgressThreshold, segmentHelper) {
// If the duration is too short, early return.
if (endTime - startTime < minSegmentDuration) {
return [segmentHelper.getSegment(startTime),
segmentHelper.getSegment(endTime)];
}
// Otherwise, start creating segments.
let pathSegments = [];
// Append the segment for the startTime position.
const startTimeSegment = segmentHelper.getSegment(startTime);
pathSegments.push(startTimeSegment);
let previousSegment = startTimeSegment;
// Split the duration in equal intervals, and iterate over them.
// See the definition of DURATION_RESOLUTION for more information about this.
const interval = (endTime - startTime) / DURATION_RESOLUTION;
for (let index = 1; index <= DURATION_RESOLUTION; index++) {
// Create a segment for this interval.
const currentSegment =
segmentHelper.getSegment(startTime + index * interval);
// If the distance between the Y coordinate (the animation's progress) of
// the previous segment and the Y coordinate of the current segment is too
// large, then recurse with a smaller duration to get more details
// in the graph.
if (Math.abs(currentSegment.y - previousSegment.y) > minProgressThreshold) {
// Divide the current interval (excluding start and end bounds
// by adding/subtracting 1ms).
pathSegments = pathSegments.concat(
createPathSegments(previousSegment.x + 1, currentSegment.x - 1,
minSegmentDuration, minProgressThreshold,
segmentHelper));
}
pathSegments.push(currentSegment);
previousSegment = currentSegment;
}
return pathSegments;
}
/**
* Append path element.
* @param {Element} parentEl - Parent element of this appended path element.
* @param {Array} pathSegments - Path segments. Please see createPathSegments.
* @param {String} cls - Class name.
* @return {Element} path element.
*/
function appendPathElement(parentEl, pathSegments, cls) {
// Create path string.
let path = `M${ pathSegments[0].x },0`;
pathSegments.forEach(pathSegment => {
path += ` L${ pathSegment.x },${ pathSegment.y }`;
});
path += ` L${ pathSegments[pathSegments.length - 1].x },0 Z`;
// Append and return the path element.
return createNode({
parent: parentEl,
namespace: SVG_NS,
nodeType: "path",
attributes: {
"d": path,
"class": cls,
"vector-effect": "non-scaling-stroke",
"transform": "scale(1, -1)"
}
});
}

View file

@ -0,0 +1,502 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript 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";
const EventEmitter = require("devtools/shared/event-emitter");
const {
createNode,
findOptimalTimeInterval,
TimeScale
} = require("devtools/client/animationinspector/utils");
const {AnimationDetails} = require("devtools/client/animationinspector/components/animation-details");
const {AnimationTargetNode} = require("devtools/client/animationinspector/components/animation-target-node");
const {AnimationTimeBlock} = require("devtools/client/animationinspector/components/animation-time-block");
// The minimum spacing between 2 time graduation headers in the timeline (px).
const TIME_GRADUATION_MIN_SPACING = 40;
// When the container window is resized, the timeline background gets refreshed,
// but only after a timer, and the timer is reset if the window is continuously
// resized.
const TIMELINE_BACKGROUND_RESIZE_DEBOUNCE_TIMER = 50;
/**
* UI component responsible for displaying a timeline for animations.
* The timeline is essentially a graph with time along the x axis and animations
* along the y axis.
* The time is represented with a graduation header at the top and a current
* time play head.
* Animations are organized by lines, with a left margin containing the preview
* of the target DOM element the animation applies to.
* The current time play head can be moved by clicking/dragging in the header.
* when this happens, the component emits "current-data-changed" events with the
* new time and state of the timeline.
*
* @param {InspectorPanel} inspector.
* @param {Object} serverTraits The list of server-side capabilities.
*/
function AnimationsTimeline(inspector, serverTraits) {
this.animations = [];
this.targetNodes = [];
this.timeBlocks = [];
this.details = [];
this.inspector = inspector;
this.serverTraits = serverTraits;
this.onAnimationStateChanged = this.onAnimationStateChanged.bind(this);
this.onScrubberMouseDown = this.onScrubberMouseDown.bind(this);
this.onScrubberMouseUp = this.onScrubberMouseUp.bind(this);
this.onScrubberMouseOut = this.onScrubberMouseOut.bind(this);
this.onScrubberMouseMove = this.onScrubberMouseMove.bind(this);
this.onAnimationSelected = this.onAnimationSelected.bind(this);
this.onWindowResize = this.onWindowResize.bind(this);
this.onFrameSelected = this.onFrameSelected.bind(this);
EventEmitter.decorate(this);
}
exports.AnimationsTimeline = AnimationsTimeline;
AnimationsTimeline.prototype = {
init: function (containerEl) {
this.win = containerEl.ownerDocument.defaultView;
this.rootWrapperEl = createNode({
parent: containerEl,
attributes: {
"class": "animation-timeline"
}
});
let scrubberContainer = createNode({
parent: this.rootWrapperEl,
attributes: {"class": "scrubber-wrapper"}
});
this.scrubberEl = createNode({
parent: scrubberContainer,
attributes: {
"class": "scrubber"
}
});
this.scrubberHandleEl = createNode({
parent: this.scrubberEl,
attributes: {
"class": "scrubber-handle"
}
});
this.scrubberHandleEl.addEventListener("mousedown",
this.onScrubberMouseDown);
this.headerWrapper = createNode({
parent: this.rootWrapperEl,
attributes: {
"class": "header-wrapper"
}
});
this.timeHeaderEl = createNode({
parent: this.headerWrapper,
attributes: {
"class": "time-header track-container"
}
});
this.timeHeaderEl.addEventListener("mousedown",
this.onScrubberMouseDown);
this.timeTickEl = createNode({
parent: this.rootWrapperEl,
attributes: {
"class": "time-body track-container"
}
});
this.animationsEl = createNode({
parent: this.rootWrapperEl,
nodeType: "ul",
attributes: {
"class": "animations"
}
});
this.win.addEventListener("resize",
this.onWindowResize);
},
destroy: function () {
this.stopAnimatingScrubber();
this.unrender();
this.win.removeEventListener("resize",
this.onWindowResize);
this.timeHeaderEl.removeEventListener("mousedown",
this.onScrubberMouseDown);
this.scrubberHandleEl.removeEventListener("mousedown",
this.onScrubberMouseDown);
this.rootWrapperEl.remove();
this.animations = [];
this.rootWrapperEl = null;
this.timeHeaderEl = null;
this.animationsEl = null;
this.scrubberEl = null;
this.scrubberHandleEl = null;
this.win = null;
this.inspector = null;
this.serverTraits = null;
},
/**
* Destroy sub-components that have been created and stored on this instance.
* @param {String} name An array of components will be expected in this[name]
* @param {Array} handlers An option list of event handlers information that
* should be used to remove these handlers.
*/
destroySubComponents: function (name, handlers = []) {
for (let component of this[name]) {
for (let {event, fn} of handlers) {
component.off(event, fn);
}
component.destroy();
}
this[name] = [];
},
unrender: function () {
for (let animation of this.animations) {
animation.off("changed", this.onAnimationStateChanged);
}
this.stopAnimatingScrubber();
TimeScale.reset();
this.destroySubComponents("targetNodes");
this.destroySubComponents("timeBlocks");
this.destroySubComponents("details", [{
event: "frame-selected",
fn: this.onFrameSelected
}]);
this.animationsEl.innerHTML = "";
},
onWindowResize: function () {
// Don't do anything if the root element has a width of 0
if (this.rootWrapperEl.offsetWidth === 0) {
return;
}
if (this.windowResizeTimer) {
this.win.clearTimeout(this.windowResizeTimer);
}
this.windowResizeTimer = this.win.setTimeout(() => {
this.drawHeaderAndBackground();
}, TIMELINE_BACKGROUND_RESIZE_DEBOUNCE_TIMER);
},
onAnimationSelected: function (e, animation) {
let index = this.animations.indexOf(animation);
if (index === -1) {
return;
}
let el = this.rootWrapperEl;
let animationEl = el.querySelectorAll(".animation")[index];
let propsEl = el.querySelectorAll(".animated-properties")[index];
// Toggle the selected state on this animation.
animationEl.classList.toggle("selected");
propsEl.classList.toggle("selected");
// Render the details component for this animation if it was shown.
if (animationEl.classList.contains("selected")) {
this.details[index].render(animation);
this.emit("animation-selected", animation);
} else {
this.emit("animation-unselected", animation);
}
},
/**
* When a frame gets selected, move the scrubber to the corresponding position
*/
onFrameSelected: function (e, {x}) {
this.moveScrubberTo(x, true);
},
onScrubberMouseDown: function (e) {
this.moveScrubberTo(e.pageX);
this.win.addEventListener("mouseup", this.onScrubberMouseUp);
this.win.addEventListener("mouseout", this.onScrubberMouseOut);
this.win.addEventListener("mousemove", this.onScrubberMouseMove);
// Prevent text selection while dragging.
e.preventDefault();
},
onScrubberMouseUp: function () {
this.cancelTimeHeaderDragging();
},
onScrubberMouseOut: function (e) {
// Check that mouseout happened on the window itself, and if yes, cancel
// the dragging.
if (!this.win.document.contains(e.relatedTarget)) {
this.cancelTimeHeaderDragging();
}
},
cancelTimeHeaderDragging: function () {
this.win.removeEventListener("mouseup", this.onScrubberMouseUp);
this.win.removeEventListener("mouseout", this.onScrubberMouseOut);
this.win.removeEventListener("mousemove", this.onScrubberMouseMove);
},
onScrubberMouseMove: function (e) {
this.moveScrubberTo(e.pageX);
},
moveScrubberTo: function (pageX, noOffset) {
this.stopAnimatingScrubber();
// The offset needs to be in % and relative to the timeline's area (so we
// subtract the scrubber's left offset, which is equal to the sidebar's
// width).
let offset = pageX;
if (!noOffset) {
offset -= this.timeHeaderEl.offsetLeft;
}
offset = offset * 100 / this.timeHeaderEl.offsetWidth;
if (offset < 0) {
offset = 0;
}
this.scrubberEl.style.left = offset + "%";
let time = TimeScale.distanceToRelativeTime(offset);
this.emit("timeline-data-changed", {
isPaused: true,
isMoving: false,
isUserDrag: true,
time: time
});
},
getCompositorStatusClassName: function (state) {
let className = state.isRunningOnCompositor
? " fast-track"
: "";
if (state.isRunningOnCompositor && state.propertyState) {
className +=
state.propertyState.some(propState => !propState.runningOnCompositor)
? " some-properties"
: " all-properties";
}
return className;
},
render: function (animations, documentCurrentTime) {
this.unrender();
this.animations = animations;
if (!this.animations.length) {
return;
}
// Loop first to set the time scale for all current animations.
for (let {state} of animations) {
TimeScale.addAnimation(state);
}
this.drawHeaderAndBackground();
for (let animation of this.animations) {
animation.on("changed", this.onAnimationStateChanged);
// Each line contains the target animated node and the animation time
// block.
let animationEl = createNode({
parent: this.animationsEl,
nodeType: "li",
attributes: {
"class": "animation " +
animation.state.type +
this.getCompositorStatusClassName(animation.state)
}
});
// Right below the line is a hidden-by-default line for displaying the
// inline keyframes.
let detailsEl = createNode({
parent: this.animationsEl,
nodeType: "li",
attributes: {
"class": "animated-properties " + animation.state.type
}
});
let details = new AnimationDetails(this.serverTraits);
details.init(detailsEl);
details.on("frame-selected", this.onFrameSelected);
this.details.push(details);
// Left sidebar for the animated node.
let animatedNodeEl = createNode({
parent: animationEl,
attributes: {
"class": "target"
}
});
// Draw the animated node target.
let targetNode = new AnimationTargetNode(this.inspector, {compact: true});
targetNode.init(animatedNodeEl);
targetNode.render(animation);
this.targetNodes.push(targetNode);
// Right-hand part contains the timeline itself (called time-block here).
let timeBlockEl = createNode({
parent: animationEl,
attributes: {
"class": "time-block track-container"
}
});
// Draw the animation time block.
let timeBlock = new AnimationTimeBlock();
timeBlock.init(timeBlockEl);
timeBlock.render(animation);
this.timeBlocks.push(timeBlock);
timeBlock.on("selected", this.onAnimationSelected);
}
// Use the document's current time to position the scrubber (if the server
// doesn't provide it, hide the scrubber entirely).
// Note that because the currentTime was sent via the protocol, some time
// may have gone by since then, and so the scrubber might be a bit late.
if (!documentCurrentTime) {
this.scrubberEl.style.display = "none";
} else {
this.scrubberEl.style.display = "block";
this.startAnimatingScrubber(this.wasRewound()
? TimeScale.minStartTime
: documentCurrentTime);
}
},
isAtLeastOneAnimationPlaying: function () {
return this.animations.some(({state}) => state.playState === "running");
},
wasRewound: function () {
return !this.isAtLeastOneAnimationPlaying() &&
this.animations.every(({state}) => state.currentTime === 0);
},
hasInfiniteAnimations: function () {
return this.animations.some(({state}) => !state.iterationCount);
},
startAnimatingScrubber: function (time) {
let isOutOfBounds = time < TimeScale.minStartTime ||
time > TimeScale.maxEndTime;
let isAllPaused = !this.isAtLeastOneAnimationPlaying();
let hasInfinite = this.hasInfiniteAnimations();
let x = TimeScale.startTimeToDistance(time);
if (x > 100 && !hasInfinite) {
x = 100;
}
this.scrubberEl.style.left = x + "%";
// Only stop the scrubber if it's out of bounds or all animations have been
// paused, but not if at least an animation is infinite.
if (isAllPaused || (isOutOfBounds && !hasInfinite)) {
this.stopAnimatingScrubber();
this.emit("timeline-data-changed", {
isPaused: !this.isAtLeastOneAnimationPlaying(),
isMoving: false,
isUserDrag: false,
time: TimeScale.distanceToRelativeTime(x)
});
return;
}
this.emit("timeline-data-changed", {
isPaused: false,
isMoving: true,
isUserDrag: false,
time: TimeScale.distanceToRelativeTime(x)
});
let now = this.win.performance.now();
this.rafID = this.win.requestAnimationFrame(() => {
if (!this.rafID) {
// In case the scrubber was stopped in the meantime.
return;
}
this.startAnimatingScrubber(time + this.win.performance.now() - now);
});
},
stopAnimatingScrubber: function () {
if (this.rafID) {
this.win.cancelAnimationFrame(this.rafID);
this.rafID = null;
}
},
onAnimationStateChanged: function () {
// For now, simply re-render the component. The animation front's state has
// already been updated.
this.render(this.animations);
},
drawHeaderAndBackground: function () {
let width = this.timeHeaderEl.offsetWidth;
let animationDuration = TimeScale.maxEndTime - TimeScale.minStartTime;
let minTimeInterval = TIME_GRADUATION_MIN_SPACING *
animationDuration / width;
let intervalLength = findOptimalTimeInterval(minTimeInterval);
let intervalWidth = intervalLength * width / animationDuration;
// And the time graduation header.
this.timeHeaderEl.innerHTML = "";
this.timeTickEl.innerHTML = "";
for (let i = 0; i <= width / intervalWidth; i++) {
let pos = 100 * i * intervalWidth / width;
// This element is the header of time tick for displaying animation
// duration time.
createNode({
parent: this.timeHeaderEl,
nodeType: "span",
attributes: {
"class": "header-item",
"style": `left:${pos}%`
},
textContent: TimeScale.formatTime(TimeScale.distanceToRelativeTime(pos))
});
// This element is displayed as a vertical line separator corresponding
// the header of time tick for indicating time slice for animation
// iterations.
createNode({
parent: this.timeTickEl,
nodeType: "span",
attributes: {
"class": "time-tick",
"style": `left:${pos}%`
}
});
}
}
};

View file

@ -0,0 +1,81 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript 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";
const EventEmitter = require("devtools/shared/event-emitter");
const {createNode} = require("devtools/client/animationinspector/utils");
/**
* UI component responsible for displaying a list of keyframes.
*/
function Keyframes() {
EventEmitter.decorate(this);
this.onClick = this.onClick.bind(this);
}
exports.Keyframes = Keyframes;
Keyframes.prototype = {
init: function (containerEl) {
this.containerEl = containerEl;
this.keyframesEl = createNode({
parent: this.containerEl,
attributes: {"class": "keyframes"}
});
this.containerEl.addEventListener("click", this.onClick);
},
destroy: function () {
this.containerEl.removeEventListener("click", this.onClick);
this.keyframesEl.remove();
this.containerEl = this.keyframesEl = this.animation = null;
},
render: function ({keyframes, propertyName, animation}) {
this.keyframes = keyframes;
this.propertyName = propertyName;
this.animation = animation;
let iterationStartOffset =
animation.state.iterationStart % 1 == 0
? 0
: 1 - animation.state.iterationStart % 1;
this.keyframesEl.classList.add(animation.state.type);
for (let frame of this.keyframes) {
let offset = frame.offset + iterationStartOffset;
createNode({
parent: this.keyframesEl,
attributes: {
"class": "frame",
"style": `left:${offset * 100}%;`,
"data-offset": frame.offset,
"data-property": propertyName,
"title": frame.value
}
});
}
},
onClick: function (e) {
// If the click happened on a frame, tell our parent about it.
if (!e.target.classList.contains("frame")) {
return;
}
e.stopPropagation();
this.emit("frame-selected", {
animation: this.animation,
propertyName: this.propertyName,
offset: parseFloat(e.target.dataset.offset),
value: e.target.getAttribute("title"),
x: e.target.offsetLeft + e.target.closest(".frames").offsetLeft
});
}
};

View file

@ -0,0 +1,12 @@
# 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/.
DevToolsModules(
'animation-details.js',
'animation-target-node.js',
'animation-time-block.js',
'animation-timeline.js',
'keyframes.js',
'rate-selector.js'
)

View file

@ -0,0 +1,105 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript 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";
const EventEmitter = require("devtools/shared/event-emitter");
const {createNode} = require("devtools/client/animationinspector/utils");
const { LocalizationHelper } = require("devtools/shared/l10n");
const L10N =
new LocalizationHelper("devtools/client/locales/animationinspector.properties");
// List of playback rate presets displayed in the timeline toolbar.
const PLAYBACK_RATES = [.1, .25, .5, 1, 2, 5, 10];
/**
* UI component responsible for displaying a playback rate selector UI.
* The rendering logic is such that a predefined list of rates is generated.
* If *all* animations passed to render share the same rate, then that rate is
* selected in the <select> element, otherwise, the empty value is selected.
* If the rate that all animations share isn't part of the list of predefined
* rates, than that rate is added to the list.
*/
function RateSelector() {
this.onRateChanged = this.onRateChanged.bind(this);
EventEmitter.decorate(this);
}
exports.RateSelector = RateSelector;
RateSelector.prototype = {
init: function (containerEl) {
this.selectEl = createNode({
parent: containerEl,
nodeType: "select",
attributes: {
"class": "devtools-button",
"title": L10N.getStr("timeline.rateSelectorTooltip")
}
});
this.selectEl.addEventListener("change", this.onRateChanged);
},
destroy: function () {
this.selectEl.removeEventListener("change", this.onRateChanged);
this.selectEl.remove();
this.selectEl = null;
},
getAnimationsRates: function (animations) {
return sortedUnique(animations.map(a => a.state.playbackRate));
},
getAllRates: function (animations) {
let animationsRates = this.getAnimationsRates(animations);
if (animationsRates.length > 1) {
return PLAYBACK_RATES;
}
return sortedUnique(PLAYBACK_RATES.concat(animationsRates));
},
render: function (animations) {
let allRates = this.getAnimationsRates(animations);
let hasOneRate = allRates.length === 1;
this.selectEl.innerHTML = "";
if (!hasOneRate) {
// When the animations displayed have mixed playback rates, we can't
// select any of the predefined ones, instead, insert an empty rate.
createNode({
parent: this.selectEl,
nodeType: "option",
attributes: {value: "", selector: "true"},
textContent: "-"
});
}
for (let rate of this.getAllRates(animations)) {
let option = createNode({
parent: this.selectEl,
nodeType: "option",
attributes: {value: rate},
textContent: L10N.getFormatStr("player.playbackRateLabel", rate)
});
// If there's only one rate and this is the option for it, select it.
if (hasOneRate && rate === allRates[0]) {
option.setAttribute("selected", "true");
}
}
},
onRateChanged: function () {
let rate = parseFloat(this.selectEl.value);
if (!isNaN(rate)) {
this.emit("rate-changed", rate);
}
}
};
let sortedUnique = arr => [...new Set(arr)].sort((a, b) => a > b);

View file

@ -0,0 +1,16 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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_CHROME_MANIFESTS += ['test/browser.ini']
XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
DIRS += [
'components'
]
DevToolsModules(
'utils.js',
)

View file

@ -0,0 +1,6 @@
"use strict";
module.exports = {
// Extend from the shared list of defined globals for mochitests.
"extends": "../../../.eslintrc.mochitests.js"
};

View file

@ -0,0 +1,71 @@
[DEFAULT]
tags = devtools
subsuite = devtools
support-files =
doc_body_animation.html
doc_end_delay.html
doc_frame_script.js
doc_keyframes.html
doc_modify_playbackRate.html
doc_negative_animation.html
doc_pseudo_elements.html
doc_script_animation.html
doc_simple_animation.html
doc_multiple_animation_types.html
doc_timing_combination_animation.html
head.js
!/devtools/client/commandline/test/helpers.js
!/devtools/client/framework/test/shared-head.js
!/devtools/client/inspector/test/head.js
!/devtools/client/inspector/test/shared-head.js
!/devtools/client/shared/test/test-actor-registry.js
!/devtools/client/shared/test/test-actor.js
[browser_animation_animated_properties_displayed.js]
[browser_animation_click_selects_animation.js]
[browser_animation_controller_exposes_document_currentTime.js]
skip-if = os == "linux" && !debug # Bug 1234567
[browser_animation_empty_on_invalid_nodes.js]
[browser_animation_keyframe_click_to_set_time.js]
[browser_animation_keyframe_markers.js]
[browser_animation_mutations_with_same_names.js]
[browser_animation_panel_exists.js]
[browser_animation_participate_in_inspector_update.js]
[browser_animation_playerFronts_are_refreshed.js]
[browser_animation_playerWidgets_appear_on_panel_init.js]
[browser_animation_playerWidgets_target_nodes.js]
[browser_animation_pseudo_elements.js]
[browser_animation_refresh_on_added_animation.js]
[browser_animation_refresh_on_removed_animation.js]
skip-if = os == "linux" && !debug # Bug 1227792
[browser_animation_refresh_when_active.js]
[browser_animation_running_on_compositor.js]
[browser_animation_same_nb_of_playerWidgets_and_playerFronts.js]
[browser_animation_shows_player_on_valid_node.js]
[browser_animation_spacebar_toggles_animations.js]
[browser_animation_spacebar_toggles_node_animations.js]
[browser_animation_target_highlight_select.js]
[browser_animation_target_highlighter_lock.js]
[browser_animation_timeline_currentTime.js]
[browser_animation_timeline_header.js]
[browser_animation_timeline_iterationStart.js]
[browser_animation_timeline_pause_button_01.js]
[browser_animation_timeline_pause_button_02.js]
[browser_animation_timeline_pause_button_03.js]
[browser_animation_timeline_rate_selector.js]
[browser_animation_timeline_rewind_button.js]
[browser_animation_timeline_scrubber_exists.js]
[browser_animation_timeline_scrubber_movable.js]
[browser_animation_timeline_scrubber_moves.js]
[browser_animation_timeline_setCurrentTime.js]
[browser_animation_timeline_shows_delay.js]
[browser_animation_timeline_shows_endDelay.js]
[browser_animation_timeline_shows_iterations.js]
[browser_animation_timeline_shows_name_label.js]
[browser_animation_timeline_shows_time_info.js]
[browser_animation_timeline_takes_rate_into_account.js]
[browser_animation_timeline_ui.js]
[browser_animation_toggle_button_resets_on_navigate.js]
[browser_animation_toggle_button_toggles_animations.js]
[browser_animation_toolbar_exists.js]
[browser_animation_ui_updates_when_animation_data_changes.js]

View file

@ -0,0 +1,91 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const LAYOUT_ERRORS_L10N =
new LocalizationHelper("toolkit/locales/layout_errors.properties");
// Test that when an animation is selected, its list of animated properties is
// displayed below it.
const EXPECTED_PROPERTIES = [
"background-attachment",
"background-clip",
"background-color",
"background-image",
"background-origin",
"background-position-x",
"background-position-y",
"background-repeat",
"background-size",
"border-bottom-left-radius",
"border-bottom-right-radius",
"border-top-left-radius",
"border-top-right-radius",
"filter",
"height",
"transform",
"width"
].sort();
add_task(function* () {
yield addTab(URL_ROOT + "doc_keyframes.html");
let {panel} = yield openAnimationInspector();
let timeline = panel.animationsTimelineComponent;
let propertiesList = timeline.rootWrapperEl
.querySelector(".animated-properties");
ok(!isNodeVisible(propertiesList),
"The list of properties panel is hidden by default");
info("Click to select the animation");
yield clickOnAnimation(panel, 0);
ok(isNodeVisible(propertiesList),
"The list of properties panel is shown");
ok(propertiesList.querySelectorAll(".property").length,
"The list of properties panel actually contains properties");
ok(hasExpectedProperties(propertiesList),
"The list of properties panel contains the right properties");
ok(hasExpectedWarnings(propertiesList),
"The list of properties panel contains the right warnings");
info("Click to unselect the animation");
yield clickOnAnimation(panel, 0, true);
ok(!isNodeVisible(propertiesList),
"The list of properties panel is hidden again");
});
function hasExpectedProperties(containerEl) {
let names = [...containerEl.querySelectorAll(".property .name")]
.map(n => n.textContent)
.sort();
if (names.length !== EXPECTED_PROPERTIES.length) {
return false;
}
for (let i = 0; i < names.length; i++) {
if (names[i] !== EXPECTED_PROPERTIES[i]) {
return false;
}
}
return true;
}
function hasExpectedWarnings(containerEl) {
let warnings = [...containerEl.querySelectorAll(".warning")];
for (let warning of warnings) {
let warningID =
"CompositorAnimationWarningTransformWithGeometricProperties";
if (warning.getAttribute("title") == LAYOUT_ERRORS_L10N.getStr(warningID)) {
return true;
}
}
return false;
}

View file

@ -0,0 +1,44 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Check that animations displayed in the timeline can be selected by clicking
// them, and that this emits the right events and adds the right classes.
add_task(function* () {
yield addTab(URL_ROOT + "doc_simple_animation.html");
let {panel} = yield openAnimationInspector();
let timeline = panel.animationsTimelineComponent;
let selected = timeline.rootWrapperEl.querySelectorAll(".animation.selected");
ok(!selected.length, "There are no animations selected by default");
info("Click on the first animation, expect the right event and right class");
let animation0 = yield clickOnAnimation(panel, 0);
is(animation0, timeline.animations[0],
"The selected event was emitted with the right animation");
ok(isTimeBlockSelected(timeline, 0),
"The time block has the right selected class");
info("Click on the second animation, expect it to be selected too");
let animation1 = yield clickOnAnimation(panel, 1);
is(animation1, timeline.animations[1],
"The selected event was emitted with the right animation");
ok(isTimeBlockSelected(timeline, 1),
"The second time block has the right selected class");
info("Click again on the first animation and check if it unselects");
yield clickOnAnimation(panel, 0, true);
ok(!isTimeBlockSelected(timeline, 0),
"The first time block has been unselected");
});
function isTimeBlockSelected(timeline, index) {
let animation = timeline.rootWrapperEl.querySelectorAll(".animation")[index];
let animatedProperties = timeline.rootWrapperEl.querySelectorAll(
".animated-properties")[index];
return animation.classList.contains("selected") &&
animatedProperties.classList.contains("selected");
}

View file

@ -0,0 +1,43 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that the controller provides the document.timeline currentTime (at least
// the last known version since new animations were added).
add_task(function* () {
yield addTab(URL_ROOT + "doc_simple_animation.html");
let {panel, controller} = yield openAnimationInspector();
ok(controller.documentCurrentTime, "The documentCurrentTime getter exists");
checkDocumentTimeIsCorrect(controller);
let time1 = controller.documentCurrentTime;
yield startNewAnimation(controller, panel);
checkDocumentTimeIsCorrect(controller);
let time2 = controller.documentCurrentTime;
ok(time2 > time1, "The new documentCurrentTime is higher than the old one");
});
function checkDocumentTimeIsCorrect(controller) {
let time = 0;
for (let {state} of controller.animationPlayers) {
time = Math.max(time, state.documentCurrentTime);
}
is(controller.documentCurrentTime, time,
"The documentCurrentTime is correct");
}
function* startNewAnimation(controller, panel) {
info("Add a new animation to the page and check the time again");
let onPlayerAdded = controller.once(controller.PLAYERS_UPDATED_EVENT);
yield executeInContent("devtools:test:setAttribute", {
selector: ".still",
attributeName: "class",
attributeValue: "ball still short"
});
yield onPlayerAdded;
yield waitForAllAnimationTargets(panel);
}

View file

@ -0,0 +1,42 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
requestLongerTimeout(2);
// Test that the panel shows no animation data for invalid or not animated nodes
add_task(function* () {
yield addTab(URL_ROOT + "doc_simple_animation.html");
let {inspector, panel, window} = yield openAnimationInspector();
let {document} = window;
info("Select node .still and check that the panel is empty");
let stillNode = yield getNodeFront(".still", inspector);
let onUpdated = panel.once(panel.UI_UPDATED_EVENT);
yield selectNodeAndWaitForAnimations(stillNode, inspector);
yield onUpdated;
is(panel.animationsTimelineComponent.animations.length, 0,
"No animation players stored in the timeline component for a still node");
is(panel.animationsTimelineComponent.animationsEl.childNodes.length, 0,
"No animation displayed in the timeline component for a still node");
is(document.querySelector("#error-type").textContent,
ANIMATION_L10N.getStr("panel.invalidElementSelected"),
"The correct error message is displayed");
info("Select the comment text node and check that the panel is empty");
let commentNode = yield inspector.walker.previousSibling(stillNode);
onUpdated = panel.once(panel.UI_UPDATED_EVENT);
yield selectNodeAndWaitForAnimations(commentNode, inspector);
yield onUpdated;
is(panel.animationsTimelineComponent.animations.length, 0,
"No animation players stored in the timeline component for a text node");
is(panel.animationsTimelineComponent.animationsEl.childNodes.length, 0,
"No animation displayed in the timeline component for a text node");
is(document.querySelector("#error-type").textContent,
ANIMATION_L10N.getStr("panel.invalidElementSelected"),
"The correct error message is displayed");
});

View file

@ -0,0 +1,52 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that animated properties' keyframes can be clicked, and that doing so
// sets the current time in the timeline.
add_task(function* () {
yield addTab(URL_ROOT + "doc_keyframes.html");
let {panel} = yield openAnimationInspector();
let timeline = panel.animationsTimelineComponent;
let {scrubberEl} = timeline;
// XXX: The scrollbar is placed in the timeline in such a way that it causes
// the animations to be slightly offset with the header when it appears.
// So for now, let's hide the scrollbar. Bug 1229340 should fix this.
timeline.animationsEl.style.overflow = "hidden";
info("Expand the animation");
yield clickOnAnimation(panel, 0);
info("Click on the first keyframe of the first animated property");
yield clickKeyframe(panel, 0, "background-color", 0);
info("Make sure the scrubber stopped moving and is at the right position");
yield assertScrubberMoving(panel, false);
checkScrubberPos(scrubberEl, 0);
info("Click on a keyframe in the middle");
yield clickKeyframe(panel, 0, "transform", 2);
info("Make sure the scrubber is at the right position");
checkScrubberPos(scrubberEl, 50);
});
function* clickKeyframe(panel, animIndex, property, index) {
let keyframeComponent = getKeyframeComponent(panel, animIndex, property);
let keyframeEl = getKeyframeEl(panel, animIndex, property, index);
let onSelect = keyframeComponent.once("frame-selected");
EventUtils.sendMouseEvent({type: "click"}, keyframeEl,
keyframeEl.ownerDocument.defaultView);
yield onSelect;
}
function checkScrubberPos(scrubberEl, pos) {
let newPos = Math.round(parseFloat(scrubberEl.style.left));
let expectedPos = Math.round(pos);
is(newPos, expectedPos, `The scrubber is at ${pos}%`);
}

View file

@ -0,0 +1,74 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that when an animation is selected and its list of properties is shown,
// there are keyframes markers next to each property being animated.
const EXPECTED_PROPERTIES = [
"backgroundColor",
"backgroundPosition",
"backgroundSize",
"borderBottomLeftRadius",
"borderBottomRightRadius",
"borderTopLeftRadius",
"borderTopRightRadius",
"filter",
"height",
"transform",
"width"
];
add_task(function* () {
yield addTab(URL_ROOT + "doc_keyframes.html");
let {panel} = yield openAnimationInspector();
let timeline = panel.animationsTimelineComponent;
info("Expand the animation");
yield clickOnAnimation(panel, 0);
ok(timeline.rootWrapperEl.querySelectorAll(".frames .keyframes").length,
"There are container elements for displaying keyframes");
let data = yield getExpectedKeyframesData(timeline.animations[0]);
for (let propertyName in data) {
info("Check the keyframe markers for " + propertyName);
let widthMarkerSelector = ".frame[data-property=" + propertyName + "]";
let markers = timeline.rootWrapperEl.querySelectorAll(widthMarkerSelector);
is(markers.length, data[propertyName].length,
"The right number of keyframes was found for " + propertyName);
let offsets = [...markers].map(m => parseFloat(m.dataset.offset));
let values = [...markers].map(m => m.dataset.value);
for (let i = 0; i < markers.length; i++) {
is(markers[i].dataset.offset, offsets[i],
"Marker " + i + " for " + propertyName + " has the right offset");
is(markers[i].dataset.value, values[i],
"Marker " + i + " for " + propertyName + " has the right value");
}
}
});
function* getExpectedKeyframesData(animation) {
// We're testing the UI state here, so it's fine to get the list of expected
// properties from the animation actor.
let properties = yield animation.getProperties();
let data = {};
for (let expectedProperty of EXPECTED_PROPERTIES) {
data[expectedProperty] = [];
for (let {name, values} of properties) {
if (name !== expectedProperty) {
continue;
}
for (let {offset, value} of values) {
data[expectedProperty].push({offset, value});
}
}
}
return data;
}

View file

@ -0,0 +1,31 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Check that when animations are added later (through animation mutations) and
// if these animations have the same names, then all of them are still being
// displayed (which should be true as long as these animations apply to
// different nodes).
add_task(function* () {
yield addTab(URL_ROOT + "doc_negative_animation.html");
let {controller, panel} = yield openAnimationInspector();
info("Wait until all animations have been added " +
"(they're added with setTimeout)");
while (controller.animationPlayers.length < 3) {
yield controller.once(controller.PLAYERS_UPDATED_EVENT);
}
yield waitForAllAnimationTargets(panel);
is(panel.animationsTimelineComponent.animations.length, 3,
"The timeline shows 3 animations too");
// Reduce the known nodeFronts to a set to make them unique.
let nodeFronts = new Set(panel.animationsTimelineComponent
.targetNodes.map(n => n.previewer.nodeFront));
is(nodeFronts.size, 3,
"The animations are applied to 3 different node fronts");
});

View file

@ -0,0 +1,23 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that the animation panel sidebar exists
add_task(function* () {
yield addTab("data:text/html;charset=utf-8,welcome to the animation panel");
let {panel, controller} = yield openAnimationInspector();
ok(controller,
"The animation controller exists");
ok(controller.animationsFront,
"The animation controller has been initialized");
ok(panel,
"The animation panel exists");
ok(panel.playersEl,
"The animation panel has been initialized");
ok(panel.animationsTimelineComponent,
"The animation panel has been initialized");
});

View file

@ -0,0 +1,46 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
requestLongerTimeout(2);
// Test that the update of the animation panel participate in the
// inspector-updated event. This means that the test verifies that the
// inspector-updated event is emitted *after* the animation panel is ready.
add_task(function* () {
yield addTab(URL_ROOT + "doc_simple_animation.html");
let {inspector, panel, controller} = yield openAnimationInspector();
info("Listen for the players-updated, ui-updated and " +
"inspector-updated events");
let receivedEvents = [];
controller.once(controller.PLAYERS_UPDATED_EVENT, () => {
receivedEvents.push(controller.PLAYERS_UPDATED_EVENT);
});
panel.once(panel.UI_UPDATED_EVENT, () => {
receivedEvents.push(panel.UI_UPDATED_EVENT);
});
inspector.once("inspector-updated", () => {
receivedEvents.push("inspector-updated");
});
info("Selecting an animated node");
let node = yield getNodeFront(".animated", inspector);
yield selectNodeAndWaitForAnimations(node, inspector);
info("Check that all events were received");
// Only assert that the inspector-updated event is last, the order of the
// first 2 events is irrelevant.
is(receivedEvents.length, 3, "3 events were received");
is(receivedEvents[2], "inspector-updated",
"The third event received was the inspector-updated event");
ok(receivedEvents.indexOf(controller.PLAYERS_UPDATED_EVENT) !== -1,
"The players-updated event was received");
ok(receivedEvents.indexOf(panel.UI_UPDATED_EVENT) !== -1,
"The ui-updated event was received");
});

View file

@ -0,0 +1,36 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
requestLongerTimeout(2);
// Check that the AnimationPlayerFront objects lifecycle is managed by the
// AnimationController.
add_task(function* () {
yield addTab(URL_ROOT + "doc_simple_animation.html");
let {controller, inspector} = yield openAnimationInspector();
info("Selecting an animated node");
// selectNode waits for the inspector-updated event before resolving, which
// means the controller.PLAYERS_UPDATED_EVENT event has been emitted before
// and players are ready.
yield selectNodeAndWaitForAnimations(".animated", inspector);
is(controller.animationPlayers.length, 1,
"One AnimationPlayerFront has been created");
info("Selecting a node with mutliple animations");
yield selectNodeAndWaitForAnimations(".multi", inspector);
is(controller.animationPlayers.length, 2,
"2 AnimationPlayerFronts have been created");
info("Selecting a node with no animations");
yield selectNodeAndWaitForAnimations(".still", inspector);
is(controller.animationPlayers.length, 0,
"There are no more AnimationPlayerFront objects");
});

View file

@ -0,0 +1,41 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that player widgets are displayed right when the animation panel is
// initialized, if the selected node (<body> by default) is animated.
const { ANIMATION_TYPES } = require("devtools/server/actors/animation");
add_task(function* () {
yield addTab(URL_ROOT + "doc_multiple_animation_types.html");
let {panel} = yield openAnimationInspector();
is(panel.animationsTimelineComponent.animations.length, 3,
"Three animations are handled by the timeline after init");
assertAnimationsDisplayed(panel, 3,
"Three animations are displayed after init");
is(
panel.animationsTimelineComponent
.animationsEl
.querySelectorAll(`.animation.${ANIMATION_TYPES.SCRIPT_ANIMATION}`)
.length,
1,
"One script-generated animation is displayed");
is(
panel.animationsTimelineComponent
.animationsEl
.querySelectorAll(`.animation.${ANIMATION_TYPES.CSS_ANIMATION}`)
.length,
1,
"One CSS animation is displayed");
is(
panel.animationsTimelineComponent
.animationsEl
.querySelectorAll(`.animation.${ANIMATION_TYPES.CSS_TRANSITION}`)
.length,
1,
"One CSS transition is displayed");
});

View file

@ -0,0 +1,33 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
requestLongerTimeout(2);
// Test that player widgets display information about target nodes
add_task(function* () {
yield addTab(URL_ROOT + "doc_simple_animation.html");
let {inspector, panel} = yield openAnimationInspector();
info("Select the simple animated node");
yield selectNodeAndWaitForAnimations(".animated", inspector);
let targetNodeComponent = panel.animationsTimelineComponent.targetNodes[0];
let {previewer} = targetNodeComponent;
// Make sure to wait for the target-retrieved event if the nodeFront hasn't
// yet been retrieved by the TargetNodeComponent.
if (!previewer.nodeFront) {
yield targetNodeComponent.once("target-retrieved");
}
is(previewer.el.textContent, "div#.ball.animated",
"The target element's content is correct");
let highlighterEl = previewer.el.querySelector(".node-highlighter");
ok(highlighterEl,
"The icon to highlight the target element in the page exists");
});

View file

@ -0,0 +1,49 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that animated pseudo-elements do show in the timeline.
add_task(function* () {
yield addTab(URL_ROOT + "doc_pseudo_elements.html");
let {inspector, panel} = yield openAnimationInspector();
let timeline = panel.animationsTimelineComponent;
info("With <body> selected by default check the content of the timeline");
is(timeline.timeBlocks.length, 3, "There are 3 animations in the timeline");
let getTargetNodeText = index => {
let el = timeline.targetNodes[index].previewer.previewEl;
return [...el.childNodes]
.map(n => n.style.display === "none" ? "" : n.textContent)
.join("");
};
is(getTargetNodeText(0), "body", "The first animated node is <body>");
is(getTargetNodeText(1), "::before", "The second animated node is ::before");
is(getTargetNodeText(2), "::after", "The third animated node is ::after");
info("Getting the before and after nodeFronts");
let bodyContainer = yield getContainerForSelector("body", inspector);
let getBodyChildNodeFront = index => {
return bodyContainer.elt.children[1].childNodes[index].container.node;
};
let beforeNode = getBodyChildNodeFront(0);
let afterNode = getBodyChildNodeFront(1);
info("Select the ::before pseudo-element in the inspector");
yield selectNode(beforeNode, inspector);
is(timeline.timeBlocks.length, 1, "There is 1 animation in the timeline");
is(timeline.targetNodes[0].previewer.nodeFront,
inspector.selection.nodeFront,
"The right node front is displayed in the timeline");
info("Select the ::after pseudo-element in the inspector");
yield selectNode(afterNode, inspector);
is(timeline.timeBlocks.length, 1, "There is 1 animation in the timeline");
is(timeline.targetNodes[0].previewer.nodeFront,
inspector.selection.nodeFront,
"The right node front is displayed in the timeline");
});

View file

@ -0,0 +1,47 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
requestLongerTimeout(2);
// Test that the panel content refreshes when new animations are added.
add_task(function* () {
yield addTab(URL_ROOT + "doc_simple_animation.html");
let {inspector, panel} = yield openAnimationInspector();
info("Select a non animated node");
yield selectNodeAndWaitForAnimations(".still", inspector);
assertAnimationsDisplayed(panel, 0);
info("Start an animation on the node");
yield changeElementAndWait({
selector: ".still",
attributeName: "class",
attributeValue: "ball animated"
}, panel, inspector);
assertAnimationsDisplayed(panel, 1);
info("Remove the animation class on the node");
yield changeElementAndWait({
selector: ".ball.animated",
attributeName: "class",
attributeValue: "ball still"
}, panel, inspector);
assertAnimationsDisplayed(panel, 0);
});
function* changeElementAndWait(options, panel, inspector) {
let onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT);
let onInspectorUpdated = inspector.once("inspector-updated");
yield executeInContent("devtools:test:setAttribute", options);
yield promise.all([
onInspectorUpdated, onPanelUpdated, waitForAllAnimationTargets(panel)]);
}

View file

@ -0,0 +1,50 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
requestLongerTimeout(2);
// Test that the panel content refreshes when animations are removed.
add_task(function* () {
yield addTab(URL_ROOT + "doc_simple_animation.html");
let {inspector, panel} = yield openAnimationInspector();
yield testRefreshOnRemove(inspector, panel);
});
function* testRefreshOnRemove(inspector, panel) {
info("Select a animated node");
yield selectNodeAndWaitForAnimations(".animated", inspector);
assertAnimationsDisplayed(panel, 1);
info("Listen to the next UI update event");
let onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT);
info("Remove the animation on the node by removing the class");
yield executeInContent("devtools:test:setAttribute", {
selector: ".animated",
attributeName: "class",
attributeValue: "ball still test-node"
});
yield onPanelUpdated;
ok(true, "The panel update event was fired");
assertAnimationsDisplayed(panel, 0);
info("Add an finite animation on the node again, and wait for it to appear");
onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT);
yield executeInContent("devtools:test:setAttribute", {
selector: ".test-node",
attributeName: "class",
attributeValue: "ball short test-node"
});
yield onPanelUpdated;
yield waitForAllAnimationTargets(panel);
assertAnimationsDisplayed(panel, 1);
}

View file

@ -0,0 +1,53 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
requestLongerTimeout(2);
// Test that the panel only refreshes when it is visible in the sidebar.
add_task(function* () {
yield addTab(URL_ROOT + "doc_simple_animation.html");
let {inspector, panel} = yield openAnimationInspector();
yield testRefresh(inspector, panel);
});
function* testRefresh(inspector, panel) {
info("Select a non animated node");
yield selectNodeAndWaitForAnimations(".still", inspector);
info("Switch to the rule-view panel");
inspector.sidebar.select("ruleview");
info("Select the animated node now");
yield selectNodeAndWaitForAnimations(".animated", inspector);
assertAnimationsDisplayed(panel, 0,
"The panel doesn't show the animation data while inactive");
info("Switch to the animation panel");
inspector.sidebar.select("animationinspector");
yield panel.once(panel.UI_UPDATED_EVENT);
assertAnimationsDisplayed(panel, 1,
"The panel shows the animation data after selecting it");
info("Switch again to the rule-view");
inspector.sidebar.select("ruleview");
info("Select the non animated node again");
yield selectNodeAndWaitForAnimations(".still", inspector);
assertAnimationsDisplayed(panel, 1,
"The panel still shows the previous animation data since it is inactive");
info("Switch to the animation panel again");
inspector.sidebar.select("animationinspector");
yield panel.once(panel.UI_UPDATED_EVENT);
assertAnimationsDisplayed(panel, 0,
"The panel is now empty after refreshing");
}

View file

@ -0,0 +1,57 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
requestLongerTimeout(2);
// Test that when animations displayed in the timeline are running on the
// compositor, they get a special icon and information in the tooltip.
add_task(function* () {
yield addTab(URL_ROOT + "doc_simple_animation.html");
let {inspector, panel} = yield openAnimationInspector();
let timeline = panel.animationsTimelineComponent;
info("Select a test node we know has an animation running on the compositor");
yield selectNodeAndWaitForAnimations(".animated", inspector);
let animationEl = timeline.animationsEl.querySelector(".animation");
ok(animationEl.classList.contains("fast-track"),
"The animation element has the fast-track css class");
ok(hasTooltip(animationEl,
ANIMATION_L10N.getStr("player.allPropertiesOnCompositorTooltip")),
"The animation element has the right tooltip content");
info("Select a node we know doesn't have an animation on the compositor");
yield selectNodeAndWaitForAnimations(".no-compositor", inspector);
animationEl = timeline.animationsEl.querySelector(".animation");
ok(!animationEl.classList.contains("fast-track"),
"The animation element does not have the fast-track css class");
ok(!hasTooltip(animationEl,
ANIMATION_L10N.getStr("player.allPropertiesOnCompositorTooltip")),
"The animation element does not have oncompositor tooltip content");
ok(!hasTooltip(animationEl,
ANIMATION_L10N.getStr("player.somePropertiesOnCompositorTooltip")),
"The animation element does not have oncompositor tooltip content");
info("Select a node we know has animation on the compositor and not on the" +
" compositor");
yield selectNodeAndWaitForAnimations(".compositor-notall", inspector);
animationEl = timeline.animationsEl.querySelector(".animation");
ok(animationEl.classList.contains("fast-track"),
"The animation element has the fast-track css class");
ok(hasTooltip(animationEl,
ANIMATION_L10N.getStr("player.somePropertiesOnCompositorTooltip")),
"The animation element has the right tooltip content");
});
function hasTooltip(animationEl, expected) {
let el = animationEl.querySelector(".name");
let tooltip = el.getAttribute("title");
return tooltip.indexOf(expected) !== -1;
}

View file

@ -0,0 +1,23 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
requestLongerTimeout(2);
// Check that when playerFronts are updated, the same number of playerWidgets
// are created in the panel.
add_task(function* () {
yield addTab(URL_ROOT + "doc_simple_animation.html");
let {inspector, panel, controller} = yield openAnimationInspector();
let timeline = panel.animationsTimelineComponent;
info("Selecting the test animated node again");
yield selectNodeAndWaitForAnimations(".multi", inspector);
is(controller.animationPlayers.length,
timeline.animationsEl.querySelectorAll(".animation").length,
"As many timeline elements were created as there are playerFronts");
});

View file

@ -0,0 +1,21 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
requestLongerTimeout(2);
// Test that the panel shows an animation player when an animated node is
// selected.
add_task(function* () {
yield addTab(URL_ROOT + "doc_simple_animation.html");
let {inspector, panel} = yield openAnimationInspector();
info("Select node .animated and check that the panel is not empty");
let node = yield getNodeFront(".animated", inspector);
yield selectNodeAndWaitForAnimations(node, inspector);
assertAnimationsDisplayed(panel, 1);
});

View file

@ -0,0 +1,49 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
add_task(function* setup() {
yield SpecialPowers.pushPrefEnv({
set: [["dom.ipc.processCount", 1]]
});
});
// Test that the spacebar key press toggles the toggleAll button state
// when a node with no animation is selected.
// This test doesn't need to test if animations actually pause/resume
// because there's an other test that does this :
// browser_animation_toggle_button_toggles_animation.js
add_task(function* () {
yield addTab(URL_ROOT + "doc_simple_animation.html");
let {panel, inspector, window, controller} = yield openAnimationInspector();
let {toggleAllButtonEl} = panel;
// select a node without animations
yield selectNodeAndWaitForAnimations(".still", inspector);
// ensure the focus is on the animation panel
window.focus();
info("Simulate spacebar stroke and check toggleAll button" +
" is in paused state");
// sending the key will lead to a ALL_ANIMATIONS_TOGGLED_EVENT
let onToggled = once(controller, controller.ALL_ANIMATIONS_TOGGLED_EVENT);
EventUtils.sendKey("SPACE", window);
yield onToggled;
ok(toggleAllButtonEl.classList.contains("paused"),
"The toggle all button is in its paused state");
info("Simulate spacebar stroke and check toggleAll button" +
" is in playing state");
// sending the key will lead to a ALL_ANIMATIONS_TOGGLED_EVENT
onToggled = once(controller, controller.ALL_ANIMATIONS_TOGGLED_EVENT);
EventUtils.sendKey("SPACE", window);
yield onToggled;
ok(!toggleAllButtonEl.classList.contains("paused"),
"The toggle all button is in its playing state again");
});

View file

@ -0,0 +1,45 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that the spacebar key press toggles the play/resume button state.
// This test doesn't need to test if animations actually pause/resume
// because there's an other test that does this.
// There are animations in the test page and since, by default, the <body> node
// is selected, animations will be displayed in the timeline, so the timeline
// play/resume button will be displayed
requestLongerTimeout(2);
add_task(function* () {
requestLongerTimeout(2);
yield addTab(URL_ROOT + "doc_simple_animation.html");
let {panel, window} = yield openAnimationInspector();
let {playTimelineButtonEl} = panel;
// ensure the focus is on the animation panel
window.focus();
info("Simulate spacebar stroke and check playResume button" +
" is in paused state");
// sending the key will lead to a UI_UPDATE_EVENT
let onUpdated = panel.once(panel.UI_UPDATED_EVENT);
EventUtils.sendKey("SPACE", window);
yield onUpdated;
ok(playTimelineButtonEl.classList.contains("paused"),
"The play/resume button is in its paused state");
info("Simulate spacebar stroke and check playResume button" +
" is in playing state");
// sending the key will lead to a UI_UPDATE_EVENT
onUpdated = panel.once(panel.UI_UPDATED_EVENT);
EventUtils.sendKey("SPACE", window);
yield onUpdated;
ok(!playTimelineButtonEl.classList.contains("paused"),
"The play/resume button is in its play state again");
});

View file

@ -0,0 +1,73 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
requestLongerTimeout(2);
// Test that the DOM element targets displayed in animation player widgets can
// be used to highlight elements in the DOM and select them in the inspector.
add_task(function* () {
yield addTab(URL_ROOT + "doc_simple_animation.html");
let {toolbox, inspector, panel} = yield openAnimationInspector();
info("Select the simple animated node");
let onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT);
yield selectNodeAndWaitForAnimations(".animated", inspector);
yield onPanelUpdated;
let targets = yield waitForAllAnimationTargets(panel);
// Arbitrary select the first one
let targetNodeComponent = targets[0];
info("Retrieve the part of the widget that highlights the node on hover");
let highlightingEl = targetNodeComponent.previewer.previewEl;
info("Listen to node-highlight event and mouse over the widget");
let onHighlight = toolbox.once("node-highlight");
EventUtils.synthesizeMouse(highlightingEl, 10, 5, {type: "mouseover"},
highlightingEl.ownerDocument.defaultView);
let nodeFront = yield onHighlight;
// Do not forget to mouseout, otherwise we get random mouseover event
// when selecting another node, which triggers some requests in animation
// inspector.
EventUtils.synthesizeMouse(highlightingEl, 10, 5, {type: "mouseout"},
highlightingEl.ownerDocument.defaultView);
ok(true, "The node-highlight event was fired");
is(targetNodeComponent.previewer.nodeFront, nodeFront,
"The highlighted node is the one stored on the animation widget");
is(nodeFront.tagName, "DIV",
"The highlighted node has the correct tagName");
is(nodeFront.attributes[0].name, "class",
"The highlighted node has the correct attributes");
is(nodeFront.attributes[0].value, "ball animated",
"The highlighted node has the correct class");
info("Select the body node in order to have the list of all animations");
onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT);
yield selectNodeAndWaitForAnimations("body", inspector);
yield onPanelUpdated;
targets = yield waitForAllAnimationTargets(panel);
targetNodeComponent = targets[0];
info("Click on the first animated node component and wait for the " +
"selection to change");
let onSelection = inspector.selection.once("new-node-front");
onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT);
let nodeEl = targetNodeComponent.previewer.previewEl;
EventUtils.sendMouseEvent({type: "click"}, nodeEl,
nodeEl.ownerDocument.defaultView);
yield onSelection;
is(inspector.selection.nodeFront, targetNodeComponent.previewer.nodeFront,
"The selected node is the one stored on the animation widget");
yield onPanelUpdated;
yield waitForAllAnimationTargets(panel);
});

View file

@ -0,0 +1,54 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
requestLongerTimeout(2);
// Test that the DOM element targets displayed in animation player widgets can
// be used to highlight elements in the DOM and select them in the inspector.
add_task(function* () {
yield addTab(URL_ROOT + "doc_simple_animation.html");
let {panel} = yield openAnimationInspector();
let targets = panel.animationsTimelineComponent.targetNodes;
info("Click on the highlighter icon for the first animated node");
let domNodePreview1 = targets[0].previewer;
yield lockHighlighterOn(domNodePreview1);
ok(domNodePreview1.highlightNodeEl.classList.contains("selected"),
"The highlighter icon is selected");
info("Click on the highlighter icon for the second animated node");
let domNodePreview2 = targets[1].previewer;
yield lockHighlighterOn(domNodePreview2);
ok(domNodePreview2.highlightNodeEl.classList.contains("selected"),
"The highlighter icon is selected");
ok(!domNodePreview1.highlightNodeEl.classList.contains("selected"),
"The highlighter icon for the first node is unselected");
info("Click again to unhighlight");
yield unlockHighlighterOn(domNodePreview2);
ok(!domNodePreview2.highlightNodeEl.classList.contains("selected"),
"The highlighter icon for the second node is unselected");
});
function* lockHighlighterOn(domNodePreview) {
let onLocked = domNodePreview.once("target-highlighter-locked");
clickOnHighlighterIcon(domNodePreview);
yield onLocked;
}
function* unlockHighlighterOn(domNodePreview) {
let onUnlocked = domNodePreview.once("target-highlighter-unlocked");
clickOnHighlighterIcon(domNodePreview);
yield onUnlocked;
}
function clickOnHighlighterIcon(domNodePreview) {
let lockEl = domNodePreview.highlightNodeEl;
EventUtils.sendMouseEvent({type: "click"}, lockEl,
lockEl.ownerDocument.defaultView);
}

View file

@ -0,0 +1,48 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
requestLongerTimeout(2);
// Check that the timeline toolbar displays the current time, and that it
// changes when animations are playing, gets back to 0 when animations are
// rewound, and stops when animations are paused.
add_task(function* () {
yield addTab(URL_ROOT + "doc_simple_animation.html");
let {panel} = yield openAnimationInspector();
let label = panel.timelineCurrentTimeEl;
ok(label, "The current time label exists");
// On page load animations are playing so the time shoud change, although we
// don't want to test the exact value of the time displayed, just that it
// actually changes.
info("Make sure the time displayed actually changes");
yield isCurrentTimeLabelChanging(panel, true);
info("Pause the animations and check that the time stops changing");
yield clickTimelinePlayPauseButton(panel);
yield isCurrentTimeLabelChanging(panel, false);
info("Rewind the animations and check that the time stops changing");
yield clickTimelineRewindButton(panel);
yield isCurrentTimeLabelChanging(panel, false);
is(label.textContent, "00:00.000");
});
function* isCurrentTimeLabelChanging(panel, isChanging) {
let label = panel.timelineCurrentTimeEl;
let time1 = label.textContent;
yield new Promise(r => setTimeout(r, 200));
let time2 = label.textContent;
if (isChanging) {
ok(time1 !== time2, "The text displayed in the label changes with time");
} else {
is(time1, time2, "The text displayed in the label doesn't change");
}
}

View file

@ -0,0 +1,59 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
requestLongerTimeout(2);
// Check that the timeline shows correct time graduations in the header.
const {findOptimalTimeInterval, TimeScale} = require("devtools/client/animationinspector/utils");
// Should be kept in sync with TIME_GRADUATION_MIN_SPACING in
// animation-timeline.js
const TIME_GRADUATION_MIN_SPACING = 40;
add_task(function* () {
yield addTab(URL_ROOT + "doc_simple_animation.html");
// System scrollbar is enabled by default on our testing envionment and it
// would shrink width of inspector and affect number of time-ticks causing
// unexpected results. So, we set it wider to avoid this kind of edge case.
yield pushPref("devtools.toolsidebar-width.inspector", 350);
let {panel} = yield openAnimationInspector();
let timeline = panel.animationsTimelineComponent;
let headerEl = timeline.timeHeaderEl;
info("Find out how many time graduations should there be");
let width = headerEl.offsetWidth;
let animationDuration = TimeScale.maxEndTime - TimeScale.minStartTime;
let minTimeInterval = TIME_GRADUATION_MIN_SPACING * animationDuration / width;
// Note that findOptimalTimeInterval is tested separately in xpcshell test
// test_findOptimalTimeInterval.js, so we assume that it works here.
let interval = findOptimalTimeInterval(minTimeInterval);
let nb = Math.ceil(animationDuration / interval);
is(headerEl.querySelectorAll(".header-item").length, nb,
"The expected number of time ticks were found");
info("Make sure graduations are evenly distributed and show the right times");
[...headerEl.querySelectorAll(".time-tick")].forEach((tick, i) => {
let left = parseFloat(tick.style.left);
let expectedPos = i * interval * 100 / animationDuration;
is(Math.round(left), Math.round(expectedPos),
`Graduation ${i} is positioned correctly`);
// Note that the distancetoRelativeTime and formatTime functions are tested
// separately in xpcshell test test_timeScale.js, so we assume that they
// work here.
let formattedTime = TimeScale.formatTime(
TimeScale.distanceToRelativeTime(expectedPos, width));
is(tick.textContent, formattedTime,
`Graduation ${i} has the right text content`);
});
});

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