mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-10 10:18:38 +09:00
import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo
This commit is contained in:
commit
dcd9973243
150858 changed files with 23884658 additions and 0 deletions
199
devtools/client/aboutdebugging/aboutdebugging.css
Normal file
199
devtools/client/aboutdebugging/aboutdebugging.css
Normal 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;
|
||||
}
|
||||
22
devtools/client/aboutdebugging/aboutdebugging.xhtml
Normal file
22
devtools/client/aboutdebugging/aboutdebugging.xhtml
Normal 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>
|
||||
111
devtools/client/aboutdebugging/components/aboutdebugging.js
Normal file
111
devtools/client/aboutdebugging/components/aboutdebugging.js
Normal 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)
|
||||
);
|
||||
}
|
||||
});
|
||||
97
devtools/client/aboutdebugging/components/addons/controls.js
vendored
Normal file
97
devtools/client/aboutdebugging/components/addons/controls.js
vendored
Normal 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 }));
|
||||
}
|
||||
});
|
||||
|
|
@ -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));
|
||||
}
|
||||
});
|
||||
10
devtools/client/aboutdebugging/components/addons/moz.build
Normal file
10
devtools/client/aboutdebugging/components/addons/moz.build
Normal 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',
|
||||
)
|
||||
146
devtools/client/aboutdebugging/components/addons/panel.js
Normal file
146
devtools/client/aboutdebugging/components/addons/panel.js
Normal 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
|
||||
})
|
||||
));
|
||||
}
|
||||
});
|
||||
84
devtools/client/aboutdebugging/components/addons/target.js
Normal file
84
devtools/client/aboutdebugging/components/addons/target.js
Normal 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"))
|
||||
);
|
||||
}
|
||||
});
|
||||
17
devtools/client/aboutdebugging/components/moz.build
Normal file
17
devtools/client/aboutdebugging/components/moz.build
Normal 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',
|
||||
)
|
||||
24
devtools/client/aboutdebugging/components/panel-header.js
Normal file
24
devtools/client/aboutdebugging/components/panel-header.js
Normal 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));
|
||||
},
|
||||
});
|
||||
|
|
@ -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));
|
||||
}
|
||||
});
|
||||
41
devtools/client/aboutdebugging/components/panel-menu.js
Normal file
41
devtools/client/aboutdebugging/components/panel-menu.js
Normal 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);
|
||||
},
|
||||
});
|
||||
8
devtools/client/aboutdebugging/components/tabs/moz.build
Normal file
8
devtools/client/aboutdebugging/components/tabs/moz.build
Normal 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',
|
||||
)
|
||||
98
devtools/client/aboutdebugging/components/tabs/panel.js
Normal file
98
devtools/client/aboutdebugging/components/tabs/panel.js
Normal 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
|
||||
})
|
||||
));
|
||||
}
|
||||
});
|
||||
53
devtools/client/aboutdebugging/components/tabs/target.js
Normal file
53
devtools/client/aboutdebugging/components/tabs/target.js
Normal 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"))
|
||||
);
|
||||
}
|
||||
});
|
||||
56
devtools/client/aboutdebugging/components/target-list.js
Normal file
56
devtools/client/aboutdebugging/components/target-list.js
Normal 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);
|
||||
},
|
||||
});
|
||||
|
|
@ -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',
|
||||
)
|
||||
193
devtools/client/aboutdebugging/components/workers/panel.js
Normal file
193
devtools/client/aboutdebugging/components/workers/panel.js
Normal 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
|
||||
})
|
||||
));
|
||||
}
|
||||
});
|
||||
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
});
|
||||
57
devtools/client/aboutdebugging/components/workers/target.js
Normal file
57
devtools/client/aboutdebugging/components/workers/target.js
Normal 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"))
|
||||
);
|
||||
}
|
||||
});
|
||||
67
devtools/client/aboutdebugging/initializer.js
Normal file
67
devtools/client/aboutdebugging/initializer.js
Normal 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();
|
||||
});
|
||||
23
devtools/client/aboutdebugging/modules/addon.js
Normal file
23
devtools/client/aboutdebugging/modules/addon.js
Normal 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;
|
||||
}
|
||||
});
|
||||
};
|
||||
8
devtools/client/aboutdebugging/modules/moz.build
Normal file
8
devtools/client/aboutdebugging/modules/moz.build
Normal 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',
|
||||
)
|
||||
77
devtools/client/aboutdebugging/modules/worker.js
Normal file
77
devtools/client/aboutdebugging/modules/worker.js
Normal 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 };
|
||||
});
|
||||
14
devtools/client/aboutdebugging/moz.build
Normal file
14
devtools/client/aboutdebugging/moz.build
Normal 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'
|
||||
]
|
||||
26
devtools/client/aboutdebugging/test/.eslintrc.js
Normal file
26
devtools/client/aboutdebugging/test/.eslintrc.js
Normal 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
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1 @@
|
|||
this is not valid json
|
||||
BIN
devtools/client/aboutdebugging/test/addons/bug1273184.xpi
Normal file
BIN
devtools/client/aboutdebugging/test/addons/bug1273184.xpi
Normal file
Binary file not shown.
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"manifest_version": 2,
|
||||
"name": "test-devtools-webextension-nobg",
|
||||
"version": "1.0",
|
||||
"applications": {
|
||||
"gecko": {
|
||||
"id": "test-devtools-webextension-nobg@mozilla.org"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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());
|
||||
}
|
||||
22
devtools/client/aboutdebugging/test/addons/unpacked/bootstrap.js
vendored
Normal file
22
devtools/client/aboutdebugging/test/addons/unpacked/bootstrap.js
vendored
Normal 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() {}
|
||||
|
|
@ -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>
|
||||
44
devtools/client/aboutdebugging/test/browser.ini
Normal file
44
devtools/client/aboutdebugging/test/browser.ini
Normal 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
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
207
devtools/client/aboutdebugging/test/browser_addons_reload.js
Normal file
207
devtools/client/aboutdebugging/test/browser_addons_reload.js
Normal 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);
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
59
devtools/client/aboutdebugging/test/browser_tabs.js
Normal file
59
devtools/client/aboutdebugging/test/browser_tabs.js
Normal 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);
|
||||
});
|
||||
367
devtools/client/aboutdebugging/test/head.js
Normal file
367
devtools/client/aboutdebugging/test/head.js
Normal 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 });
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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));
|
||||
});
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1 @@
|
|||
// Empty, just test registering.
|
||||
|
|
@ -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>
|
||||
|
|
@ -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"));
|
||||
});
|
||||
390
devtools/client/animationinspector/animation-controller.js
Normal file
390
devtools/client/animationinspector/animation-controller.js
Normal 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);
|
||||
32
devtools/client/animationinspector/animation-inspector.xhtml
Normal file
32
devtools/client/animationinspector/animation-inspector.xhtml
Normal 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>
|
||||
347
devtools/client/animationinspector/animation-panel.js
Normal file
347
devtools/client/animationinspector/animation-panel.js
Normal 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);
|
||||
|
|
@ -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;
|
||||
|
|
@ -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");
|
||||
})
|
||||
};
|
||||
|
|
@ -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)"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -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}%`
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
81
devtools/client/animationinspector/components/keyframes.js
Normal file
81
devtools/client/animationinspector/components/keyframes.js
Normal 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
|
||||
});
|
||||
}
|
||||
};
|
||||
12
devtools/client/animationinspector/components/moz.build
Normal file
12
devtools/client/animationinspector/components/moz.build
Normal 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'
|
||||
)
|
||||
105
devtools/client/animationinspector/components/rate-selector.js
Normal file
105
devtools/client/animationinspector/components/rate-selector.js
Normal 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);
|
||||
16
devtools/client/animationinspector/moz.build
Normal file
16
devtools/client/animationinspector/moz.build
Normal 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',
|
||||
)
|
||||
6
devtools/client/animationinspector/test/.eslintrc.js
Normal file
6
devtools/client/animationinspector/test/.eslintrc.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
// Extend from the shared list of defined globals for mochitests.
|
||||
"extends": "../../../.eslintrc.mochitests.js"
|
||||
};
|
||||
71
devtools/client/animationinspector/test/browser.ini
Normal file
71
devtools/client/animationinspector/test/browser.ini
Normal 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]
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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");
|
||||
});
|
||||
|
|
@ -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}%`);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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");
|
||||
});
|
||||
|
|
@ -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");
|
||||
});
|
||||
|
|
@ -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");
|
||||
});
|
||||
|
|
@ -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");
|
||||
});
|
||||
|
|
@ -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");
|
||||
});
|
||||
|
|
@ -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");
|
||||
});
|
||||
|
|
@ -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");
|
||||
});
|
||||
|
|
@ -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)]);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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");
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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");
|
||||
});
|
||||
|
|
@ -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");
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue