Merge mozilla-central to autoland. a=merge CLOSED TREE

This commit is contained in:
Bogdan Tara 2018-09-12 01:15:44 +03:00
commit 3fc5bc9ad5
725 changed files with 58262 additions and 40743 deletions

10
Cargo.lock generated
View file

@ -138,7 +138,7 @@ dependencies = [
[[package]]
name = "base64"
version = "0.6.0"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"byteorder 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
@ -829,7 +829,7 @@ dependencies = [
name = "geckodriver"
version = "0.21.0"
dependencies = [
"base64 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
"base64 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)",
"chrono 0.2.25 (registry+https://github.com/rust-lang/crates.io-index)",
"clap 2.31.2 (registry+https://github.com/rust-lang/crates.io-index)",
"hyper 0.12.7 (registry+https://github.com/rust-lang/crates.io-index)",
@ -1876,7 +1876,7 @@ dependencies = [
name = "rsdparsa"
version = "0.1.0"
dependencies = [
"log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.66 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_derive 1.0.66 (git+https://github.com/servo/serde?branch=deserialize_from_enums8)",
]
@ -2615,7 +2615,7 @@ dependencies = [
name = "webdriver"
version = "0.36.0"
dependencies = [
"base64 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
"base64 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)",
"cookie 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
"futures 0.1.23 (registry+https://github.com/rust-lang/crates.io-index)",
"http 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
@ -2840,7 +2840,7 @@ dependencies = [
"checksum atomic_refcell 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fb2dcb6e6d35f20276943cc04bb98e538b348d525a04ac79c10021561d202f21"
"checksum atty 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d0fd4c0631f06448cc45a6bbb3b710ebb7ff8ccb96a0800c994afe23a70d5df2"
"checksum atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "9a7d5b8723950951411ee34d271d99dddcc2035a16ab25310ea2c8cfd4369652"
"checksum base64 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "96434f987501f0ed4eb336a411e0631ecd1afa11574fe148587adc4ff96143c9"
"checksum base64 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)" = "85415d2594767338a74a30c1d370b2f3262ec1b4ed2d7bba5b3faf4de40467d9"
"checksum binary-space-partition 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "88ceb0d16c4fd0e42876e298d7d3ce3780dd9ebdcbe4199816a32c77e08597ff"
"checksum bincode 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bda13183df33055cbb84b847becce220d392df502ebe7a4a78d7021771ed94d0"
"checksum bindgen 0.39.0 (registry+https://github.com/rust-lang/crates.io-index)" = "eac4ed5f2de9efc3c87cb722468fa49d0763e98f999d539bfc5e452c13d85c91"

View file

@ -6,4 +6,4 @@ support-files =
!/accessible/tests/mochitest/*.js
[browser_aria_owns.js]
skip-if = (verify && !debug && (os == 'linux'))
skip-if = true || (verify && !debug && (os == 'linux')) #Bug 1445513

View file

@ -63,10 +63,6 @@ let whitelist = [
intermittent: true,
errorMessage: /Property contained reference to invalid variable.*background/i,
isFromDevTools: true},
{sourceName: /devtools\/skin\/animationinspector\.css$/i,
intermittent: true,
errorMessage: /Property contained reference to invalid variable.*color/i,
isFromDevTools: true},
];
if (!Services.prefs.getBoolPref("layout.css.xul-box-display-values.content.enabled")) {

View file

@ -154,6 +154,7 @@ skip-if = !e10s || !crashreporter # the tab's process is killed during the test.
[browser_ext_sessions_forgetClosedTab.js]
[browser_ext_sessions_forgetClosedWindow.js]
[browser_ext_sessions_getRecentlyClosed.js]
skip-if = (debug && os == 'linux') # Bug 1377641
[browser_ext_sessions_getRecentlyClosed_private.js]
[browser_ext_sessions_getRecentlyClosed_tabs.js]
[browser_ext_sessions_restore.js]

View file

@ -1416,8 +1416,8 @@ add_old_configure_assignment('MOZ_LTO_LDFLAGS', lto.ldflags)
js_option('--enable-address-sanitizer', help='Enable Address Sanitizer')
@depends_if('--enable-address-sanitizer')
def asan(value):
@depends_if('--enable-address-sanitizer', '--help')
def asan(value, _):
return True

View file

@ -1,3 +0,0 @@
This folder contains the code for the current animation inspector. It is currently being rewritten in React in https://bugzilla.mozilla.org/show_bug.cgi?id=1399830.
The new version of the animation inspector can be found in devtools/inspector/animation.

View file

@ -1,399 +0,0 @@
/* -*- 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 { loader, require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm", {});
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 = 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)
await AnimationsController.initialize();
await AnimationsPanel.initialize();
};
/**
* Shutdown the animationinspector controller and view, called by the sidebar
* widget when loading/unloading the iframe into the tab.
*/
var shutdown = async function() {
await AnimationsController.destroy();
// Don't assume that AnimationsPanel is defined here, it's in another file.
if (typeof AnimationsPanel !== "undefined") {
await 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(console.error);
}
function destroy() {
return shutdown().catch(console.error);
}
/**
* 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 = async function(target) {
const 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" },
{ name: "hasGetAnimationTypes", actor: "animationplayer",
method: "getAnimationTypes" },
];
const traits = {};
for (const {name, actor, method} of config) {
traits[name] = await 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",
async initialize() {
if (this.initialized) {
await 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);
const target = gInspector.target;
this.animationsFront = new AnimationsFront(target.client, target.form);
// Expose actor capabilities.
this.traits = await 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) {
await this.animationsFront.setWalkerActor(gInspector.walker);
}
this.startListeners();
await this.onNewNodeFront();
resolver();
},
async destroy() {
if (!this.initialized) {
return;
}
if (this.destroyed) {
await 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";
},
async onPanelVisibilityChange() {
if (this.isPanelVisible()) {
this.onNewNodeFront();
}
},
async onNewNodeFront() {
// Ignore if the panel isn't visible.
// Or the node selection hasn't changed and no animation mutations event occurs during
// hidden.
if (!this.isPanelVisible() || (this.nodeFront === gInspector.selection.nodeFront &&
!this.mutationsDetectedWhileHidden)) {
return;
}
this.mutationsDetectedWhileHidden = false;
this.nodeFront = gInspector.selection.nodeFront;
const done = gInspector.updating("animationscontroller");
if (!gInspector.selection.isConnected() ||
!gInspector.selection.isElementNode()) {
this.destroyAnimationPlayers();
this.emit(this.PLAYERS_UPDATED_EVENT);
done();
return;
}
await 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(console.error);
},
/**
* 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.
*/
async toggleCurrentAnimations(shouldPause) {
if (this.traits.hasToggleSeveral) {
await 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 (const player of this.animationPlayers) {
if (shouldPause) {
await player.pause();
} else {
await 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.
*/
async setCurrentTimeAll(time, shouldPause) {
if (this.traits.hasSetCurrentTimes) {
await 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 (const animation of this.animationPlayers) {
if (shouldPause) {
await animation.pause();
}
await 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.
*/
async setPlaybackRateAll(rate) {
if (this.traits.hasSetPlaybackRates) {
// If the backend can set all playback rates at the same time, use that.
await this.animationsFront.setPlaybackRates(this.animationPlayers, rate);
} else if (this.traits.hasSetPlaybackRate) {
// Otherwise, fall back to setting each rate individually.
for (const animation of this.animationPlayers) {
await 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: [],
async refreshAnimationPlayers(nodeFront) {
this.destroyAnimationPlayers();
this.animationPlayers = await 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 (const {type, player} of changes) {
if (type === "added") {
this.animationPlayers.push(player);
}
if (type === "removed") {
const index = this.animationPlayers.indexOf(player);
this.animationPlayers.splice(index, 1);
}
}
if (this.isPanelVisible()) {
// Let the UI know the list has been updated.
this.emit(this.PLAYERS_UPDATED_EVENT, this.animationPlayers);
} else {
// Avoid updating the UI while the panel is hidden.
// This avoids unnecessary work.
this.mutationsDetectedWhileHidden = true;
}
},
/**
* 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 (const {state} of this.animationPlayers) {
if (!state.documentCurrentTime) {
return false;
}
time = Math.max(time, state.documentCurrentTime);
}
return time;
},
destroyAnimationPlayers: function() {
this.animationPlayers = [];
}
};
EventEmitter.decorate(AnimationsController);

View file

@ -1,42 +0,0 @@
<?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"/>
<link rel="stylesheet" href="resource://devtools/client/shared/components/splitter/SplitBox.css"/>
<script type="application/javascript" src="chrome://devtools/content/shared/theme-switching.js"/>
</head>
<body class="theme-sidebar" role="application" empty="true">
<div id="global-toolbar" class="theme-toolbar">
<span id="all-animations-label" class="label"></span>
<button id="toggle-all" class="devtools-button pause-button"></button>
</div>
<div id="timeline-toolbar" class="theme-toolbar">
<button id="rewind-timeline" class="devtools-button"></button>
<button id="pause-resume-timeline" class="devtools-button pause-button paused"></button>
<span id="timeline-rate" class="devtools-button"></span>
<span id="timeline-current-time" class="label"></span>
</div>
<div id="players"></div>
<div id="error-message" class="devtools-sidepanel-no-result">
<p id="error-type"></p>
<p id="error-hint"></p>
<button id="element-picker" data-standalone="true" class="devtools-button"></button>
</div>
<script type="text/javascript">
/* eslint-disable */
var isInChrome = window.location.href.includes("chrome:");
if (isInChrome) {
var exports = {};
var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
var { BrowserLoader } = Cu.import("resource://devtools/client/shared/browser-loader.js", {});
}
</script>
<script type="application/javascript" src="animation-controller.js"></script>
<script type="application/javascript" src="animation-panel.js"></script>
</body>
</html>

View file

@ -1,348 +0,0 @@
/* -*- 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/inspector/animation-old/components/animation-timeline");
const {RateSelector} = require("devtools/client/inspector/animation-old/components/rate-selector");
const {formatStopwatchTime} = require("devtools/client/inspector/animation-old/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",
async initialize() {
if (AnimationsController.destroyed) {
console.warn("Could not initialize the animation-panel, controller " +
"was destroyed");
return;
}
if (this.initialized) {
await 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 (const functionName of [
"onKeyDown", "onPickerStarted",
"onPickerStopped", "refreshAnimationsUI", "onToggleAllClicked",
"onTabNavigated", "onTimelineDataChanged", "onTimelinePlayClicked",
"onTimelineRewindClicked", "onRateChanged"]) {
this[functionName] = this[functionName].bind(this);
}
const 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();
await this.refreshAnimationsUI();
resolver();
this.emit(this.PANEL_INITIALIZED);
},
async destroy() {
if (!this.initialized) {
return;
}
if (this.destroyed) {
await 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);
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);
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(console.error);
} else {
this.toggleAll().catch(console.error);
}
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.classList.add("checked");
},
onPickerStopped: function() {
this.pickerButtonEl.classList.remove("checked");
},
onToggleAllClicked: function() {
this.toggleAll().catch(console.error);
},
/**
* Toggle (pause/play) all animations in the current target
* and update the UI the toggleAll button.
*/
async toggleAll() {
this.toggleAllButtonEl.classList.toggle("paused");
await AnimationsController.toggleAll();
},
onTimelinePlayClicked: function() {
this.playPauseTimeline().catch(console.error);
},
/**
* 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(console.error);
},
/**
* 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(rate) {
AnimationsController.setPlaybackRateAll(rate)
.then(() => this.refreshAnimationsStateAndUI())
.catch(console.error);
},
onTabNavigated: function() {
this.toggleAllButtonEl.classList.remove("paused");
},
onTimelineDataChanged: function(data) {
this.timelineData = data;
const {isMoving, isUserDrag, time} = data;
this.playTimelineButtonEl.classList.toggle("paused", !isMoving);
const 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(console.error)
.then(() => {
this.setCurrentTimeAllPromise = null;
});
}
this.displayTimelineCurrentTime();
},
displayTimelineCurrentTime: function() {
const {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.
*/
async refreshAnimationsStateAndUI() {
for (const player of AnimationsController.animationPlayers) {
await player.refreshState();
}
await this.refreshAnimationsUI();
},
/**
* Refresh the list of animations UI. This will empty the panel and re-render
* the various components again.
*/
async refreshAnimationsUI() {
// Empty the whole panel first.
this.togglePlayers(true);
// Re-render the timeline component.
this.animationsTimelineComponent.render(
AnimationsController.animationPlayers,
AnimationsController.documentCurrentTime);
// Re-render the rate selector component.
if (this.rateSelectorComponent) {
this.rateSelectorComponent.render(AnimationsController.animationPlayers);
}
// If there are no players to show, show the error message instead and
// return.
if (!AnimationsController.animationPlayers.length) {
this.togglePlayers(false);
this.emit(this.UI_UPDATED_EVENT);
return;
}
this.emit(this.UI_UPDATED_EVENT);
}
};
EventEmitter.decorate(AnimationsPanel);

View file

@ -1,286 +0,0 @@
/* -*- 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 {createNode, getCssPropertyName} =
require("devtools/client/inspector/animation-old/utils");
const {Keyframes} = require("devtools/client/inspector/animation-old/components/keyframes");
const { LocalizationHelper } = require("devtools/shared/l10n");
const L10N =
new LocalizationHelper("devtools/client/locales/animationinspector.properties");
/**
* 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) {
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", "simulateComputeValuesFailure"],
init: function(containerEl) {
this.containerEl = containerEl;
},
destroy: function() {
this.unrender();
this.containerEl = null;
this.serverTraits = null;
this.progressIndicatorEl = null;
},
unrender: function() {
for (const component of this.keyframeComponents) {
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 (const 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 animation types of given CSS property names.
* @param {Array} CSS property names.
* e.g. ["background-color", "opacity", ...]
* @return {Object} Animation type mapped with CSS property name.
* e.g. { "background-color": "color", }
* "opacity": "float", ... }
*/
async getAnimationTypes(propertyNames) {
if (this.serverTraits.hasGetAnimationTypes) {
return this.animation.getAnimationTypes(propertyNames);
}
// Set animation type 'none' since does not support getAnimationTypes.
const animationTypes = {};
propertyNames.forEach(propertyName => {
animationTypes[propertyName] = "none";
});
return Promise.resolve(animationTypes);
},
async render(animation, tracks) {
this.unrender();
if (!animation) {
return;
}
this.animation = animation;
this.tracks = tracks;
// We might have been destroyed in the meantime, or the component might
// have been re-rendered.
if (!this.containerEl || this.animation !== animation) {
return;
}
// Get animation type for each CSS properties.
const animationTypes = await this.getAnimationTypes(Object.keys(this.tracks));
// Render progress indicator.
this.renderProgressIndicator();
// Render animated properties header.
this.renderAnimatedPropertiesHeader();
// Render animated properties body.
this.renderAnimatedPropertiesBody(animationTypes);
// Create dummy animation to indicate the animation progress.
const timing = Object.assign({}, animation.state, {
iterations: animation.state.iterationCount
? animation.state.iterationCount : Infinity
});
this.dummyAnimation =
new this.win.Animation(new this.win.KeyframeEffect(null, null, timing), null);
},
renderAnimatedPropertiesHeader: function() {
// Add animated property header.
const headerEl = createNode({
parent: this.containerEl,
attributes: { "class": "animated-properties-header" }
});
// Add progress tick container.
const progressTickContainerEl = createNode({
parent: this.containerEl,
attributes: { "class": "progress-tick-container track-container" }
});
// Add label container.
const headerLabelContainerEl = createNode({
parent: headerEl,
attributes: { "class": "track-container" }
});
// Add labels
for (const label of [L10N.getFormatStr("detail.propertiesHeader.percentage", 0),
L10N.getFormatStr("detail.propertiesHeader.percentage", 50),
L10N.getFormatStr("detail.propertiesHeader.percentage", 100)]) {
createNode({
parent: progressTickContainerEl,
nodeType: "span",
attributes: { "class": "progress-tick" }
});
createNode({
parent: headerLabelContainerEl,
nodeType: "label",
attributes: { "class": "header-item" },
textContent: label
});
}
},
renderAnimatedPropertiesBody: function(animationTypes) {
// Add animated property body.
const bodyEl = createNode({
parent: this.containerEl,
attributes: { "class": "animated-properties-body" }
});
// Move unchanged value animation to bottom in the list.
const propertyNames = [];
const unchangedPropertyNames = [];
for (const propertyName in this.tracks) {
if (!isUnchangedProperty(this.tracks[propertyName])) {
propertyNames.push(propertyName);
} else {
unchangedPropertyNames.push(propertyName);
}
}
Array.prototype.push.apply(propertyNames, unchangedPropertyNames);
for (const propertyName of propertyNames) {
const line = createNode({
parent: bodyEl,
attributes: {"class": "property"}
});
if (unchangedPropertyNames.includes(propertyName)) {
line.classList.add("unchanged");
}
const {warning, className} =
this.getPerfDataForProperty(this.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.
const framesWrapperEl = createNode({
parent: line,
attributes: {"class": "track-container"}
});
const framesEl = createNode({
parent: framesWrapperEl,
attributes: {"class": "frames"}
});
const keyframesComponent = new Keyframes();
keyframesComponent.init(framesEl);
keyframesComponent.render({
keyframes: this.tracks[propertyName],
propertyName: propertyName,
animation: this.animation,
animationType: animationTypes[propertyName]
});
this.keyframeComponents.push(keyframesComponent);
}
},
renderProgressIndicator: function() {
// The wrapper represents the area which the indicator is displayable.
const progressIndicatorWrapperEl = createNode({
parent: this.containerEl,
attributes: {
"class": "track-container progress-indicator-wrapper"
}
});
this.progressIndicatorEl = createNode({
parent: progressIndicatorWrapperEl,
attributes: {
"class": "progress-indicator"
}
});
createNode({
parent: this.progressIndicatorEl,
attributes: {
"class": "progress-indicator-shape"
}
});
},
indicateProgress: function(time) {
if (!this.progressIndicatorEl) {
// Not displayed yet.
return;
}
const startTime = this.animation.state.previousStartTime || 0;
this.dummyAnimation.currentTime =
(time - startTime) * this.animation.state.playbackRate;
this.progressIndicatorEl.style.left =
`${ this.dummyAnimation.effect.getComputedTiming().progress * 100 }%`;
},
get win() {
return this.containerEl.ownerDocument.defaultView;
}
};
function isUnchangedProperty(values) {
const firstValue = values[0].value;
for (let i = 1; i < values.length; i++) {
if (values[i].value !== firstValue) {
return false;
}
}
return true;
}

View file

@ -1,79 +0,0 @@
/* -*- 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 {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;
},
async render(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 = await this.inspector.walker.getNodeFromActor(
playerFront.actorID, ["node"]);
} catch (e) {
// If an error occured while getting the nodeFront and if it can't be
// attributed to the panel having been destroyed in the meantime, this
// error needs to be logged and render needs to stop.
if (!this.isDestroyed) {
console.error(e);
}
return;
}
// In all cases, if by now the panel doesn't exist anymore, we need to
// stop rendering too.
if (this.isDestroyed) {
return;
}
}
// Add the nodeFront to the cache.
nodeFronts.set(playerFront, nodeFront);
this.previewer.render(nodeFront);
this.emit("target-retrieved");
}
};

View file

@ -1,677 +0,0 @@
/* -*- 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, createSVGNode, TimeScale, getFormattedAnimationTitle} =
require("devtools/client/inspector/animation-old/utils");
const {SummaryGraphHelper, getPreferredKeyframesProgressThreshold,
getPreferredProgressThreshold} =
require("devtools/client/inspector/animation-old/graph-helper");
const { LocalizationHelper } = require("devtools/shared/l10n");
const L10N =
new LocalizationHelper("devtools/client/locales/animationinspector.properties");
// Show max 10 iterations for infinite animations
// to give users a clue that the animation does repeat.
const MAX_INFINITE_ANIMATIONS_ITERATIONS = 10;
// Minimum opacity for semitransparent fill color for keyframes's easing graph.
const MIN_KEYFRAMES_EASING_OPACITY = .5;
/**
* 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, tracks) {
this.unrender();
this.animation = animation;
// Animation summary graph element.
const summaryEl = createSVGNode({
parent: this.containerEl,
nodeType: "svg",
attributes: {
"class": "summary",
"preserveAspectRatio": "none"
}
});
this.updateSummaryGraphViewBox(summaryEl);
const {state} = this.animation;
// Total displayed duration
const totalDisplayedDuration = this.getTotalDisplayedDuration();
// Minimum segment duration is the duration of one pixel.
const minSegmentDuration = totalDisplayedDuration / this.containerEl.clientWidth;
// Minimum progress threshold for effect timing.
const minEffectProgressThreshold = getPreferredProgressThreshold(state.easing);
// Render summary graph.
// The summary graph is constructed from keyframes's easing and effect timing.
const graphHelper = new SummaryGraphHelper(this.win, state, minSegmentDuration);
renderKeyframesEasingGraph(summaryEl, state, totalDisplayedDuration,
minEffectProgressThreshold, tracks, graphHelper);
if (state.easing !== "linear") {
renderEffectEasingGraph(summaryEl, state, totalDisplayedDuration,
minEffectProgressThreshold, graphHelper);
}
graphHelper.destroy();
// The animation name is displayed over the animation.
const nameEl = createNode({
parent: this.containerEl,
attributes: {
"class": "name",
"title": this.getTooltipText(state)
}
});
createSVGNode({
parent: createSVGNode({
parent: nameEl,
nodeType: "svg",
}),
nodeType: "text",
attributes: {
"y": "50%",
"x": "100%",
},
textContent: state.name
});
// Delay.
if (state.delay) {
// Negative delays need to start at 0.
const delayEl = createNode({
parent: this.containerEl,
attributes: {
"class": "delay"
+ (state.delay < 0 ? " negative" : " positive")
+ (state.fill === "both" ||
state.fill === "backwards" ? " fill" : "")
}
});
this.updateDelayBounds(delayEl);
}
// endDelay
if (state.iterationCount && state.endDelay) {
const endDelayEl = createNode({
parent: this.containerEl,
attributes: {
"class": "end-delay"
+ (state.endDelay < 0 ? " negative" : " positive")
+ (state.fill === "both" ||
state.fill === "forwards" ? " fill" : "")
}
});
this.updateEndDelayBounds(endDelayEl);
}
},
/**
* Update animation and updating its DOM accordingly.
* Unlike 'render' method, this method does not generate any elements, but update
* the bounds of DOM.
* @param {Object} animation
*/
update: function(animation) {
this.animation = animation;
this.updateSummaryGraphViewBox(this.containerEl.querySelector(".summary"));
const delayEl = this.containerEl.querySelector(".delay");
if (delayEl) {
this.updateDelayBounds(delayEl);
}
const endDelayEl = this.containerEl.querySelector(".end-delay");
if (endDelayEl) {
this.updateEndDelayBounds(endDelayEl);
}
},
/**
* Update viewBox and style of SVG element for summary graph to fit to latest
* TimeScale.
* @param {Element} summaryEl - SVG element for summary graph.
*/
updateSummaryGraphViewBox: function(summaryEl) {
const {x, delayW} = TimeScale.getAnimationDimensions(this.animation);
const totalDisplayedDuration = this.getTotalDisplayedDuration();
const strokeHeightForViewBox = 0.5 / this.containerEl.clientHeight;
const {state} = this.animation;
summaryEl.setAttribute("viewBox",
`${state.delay < 0 ? state.delay : 0} ` +
`-${1 + strokeHeightForViewBox} ` +
`${totalDisplayedDuration} ` +
`${1 + strokeHeightForViewBox * 2}`);
summaryEl.setAttribute("style", `left: ${ x - (state.delay > 0 ? delayW : 0) }%`);
},
/**
* Update bounds of element which represents delay to fit to latest TimeScale.
* @param {Element} delayEl - which represents delay.
*/
updateDelayBounds: function(delayEl) {
const {delayX, delayW} = TimeScale.getAnimationDimensions(this.animation);
delayEl.style.left = `${ delayX }%`;
delayEl.style.width = `${ delayW }%`;
},
/**
* Update bounds of element which represents endDelay to fit to latest TimeScale.
* @param {Element} endDelayEl - which represents endDelay.
*/
updateEndDelayBounds: function(endDelayEl) {
const {endDelayX, endDelayW} = TimeScale.getAnimationDimensions(this.animation);
endDelayEl.style.left = `${ endDelayX }%`;
endDelayEl.style.width = `${ endDelayW }%`;
},
getTotalDisplayedDuration: function() {
return this.animation.state.playbackRate * TimeScale.getDuration();
},
getTooltipText: function(state) {
const 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) {
const iterationStartTime = state.iterationStart * state.duration / 1000;
text += L10N.getFormatStr("player.animationIterationStartLabel",
state.iterationStart,
L10N.numberWithDecimals(iterationStartTime, 2));
text += "\n";
}
// Adding the easing if it is not "linear".
if (state.easing && state.easing !== "linear") {
text += L10N.getStr("player.animationOverallEasingLabel") + " ";
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 it is not "normal".
if (state.direction && state.direction !== "normal") {
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 the animation-timing-function
// if it is not "ease" which is default value for CSS Animations.
if (state.animationTimingFunction && state.animationTimingFunction !== "ease") {
text += L10N.getStr("player.animationTimingFunctionLabel") + " ";
text += state.animationTimingFunction;
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;
}
};
/**
* Render keyframes's easing graph.
* @param {Element} parentEl - Parent element of this appended path element.
* @param {Object} state - State of animation.
* @param {float} totalDisplayedDuration - Displayable total duration.
* @param {float} minEffectProgressThreshold - Minimum progress threshold for effect.
* @param {Object} tracks - The value of AnimationsTimeline.getTracks().
* @param {Object} graphHelper - SummaryGraphHelper.
*/
function renderKeyframesEasingGraph(parentEl, state, totalDisplayedDuration,
minEffectProgressThreshold, tracks, graphHelper) {
const keyframesList = getOffsetAndEasingOnlyKeyframesList(tracks);
const keyframeEasingOpacity = Math.max(1 / keyframesList.length,
MIN_KEYFRAMES_EASING_OPACITY);
for (const keyframes of keyframesList) {
const minProgressTreshold =
Math.min(minEffectProgressThreshold,
getPreferredKeyframesProgressThreshold(keyframes));
graphHelper.setMinProgressThreshold(minProgressTreshold);
graphHelper.setKeyframes(keyframes);
graphHelper.setClosePathNeeded(true);
const element = renderGraph(parentEl, state, totalDisplayedDuration,
"keyframes-easing", graphHelper);
element.style.opacity = keyframeEasingOpacity;
}
}
/**
* Render effect easing graph.
* @param {Element} parentEl - Parent element of this appended path element.
* @param {Object} state - State of animation.
* @param {float} totalDisplayedDuration - Displayable total duration.
* @param {float} minEffectProgressThreshold - Minimum progress threshold for effect.
* @param {Object} graphHelper - SummaryGraphHelper.
*/
function renderEffectEasingGraph(parentEl, state, totalDisplayedDuration,
minEffectProgressThreshold, graphHelper) {
graphHelper.setMinProgressThreshold(minEffectProgressThreshold);
graphHelper.setKeyframes(null);
graphHelper.setClosePathNeeded(false);
renderGraph(parentEl, state, totalDisplayedDuration, "effect-easing", graphHelper);
}
/**
* Render a graph of given parameters.
* @param {Element} parentEl - Parent element of this appended path element.
* @param {Object} state - State of animation.
* @param {float} totalDisplayedDuration - Displayable total duration.
* @param {String} className - Class name for graph element.
* @param {Object} graphHelper - SummaryGraphHelper.
*/
function renderGraph(parentEl, state, totalDisplayedDuration, className, graphHelper) {
const graphEl = createSVGNode({
parent: parentEl,
nodeType: "g",
attributes: {
"class": className,
}
});
// Starting time of main iteration.
let mainIterationStartTime = 0;
let iterationStart = state.iterationStart;
let iterationCount = state.iterationCount ? state.iterationCount : Infinity;
graphHelper.setFillMode(state.fill);
graphHelper.setOriginalBehavior(true);
// Append delay.
if (state.delay > 0) {
renderDelay(graphEl, state, graphHelper);
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(graphEl, state, mainIterationStartTime,
firstSectionCount, graphHelper);
}
if (iterationCount === Infinity) {
// If the animation repeats infinitely,
// we fill the remaining area with iteration paths.
renderInfinity(graphEl, state, mainIterationStartTime,
firstSectionCount, totalDisplayedDuration, graphHelper);
} else {
// Otherwise, we show remaining iterations, endDelay and fill.
// Append forwards fill-mode.
if (state.fill === "both" || state.fill === "forwards") {
renderForwardsFill(graphEl, state, mainIterationStartTime,
iterationCount, totalDisplayedDuration, graphHelper);
}
// Append middle section of iterations.
// e.g.
// if { iterationStart: 0.25, iterations: 3 }, middleSectionCount is 2.
const middleSectionCount =
Math.floor(iterationCount - firstSectionCount);
renderMiddleIterations(graphEl, state, mainIterationStartTime,
firstSectionCount, middleSectionCount, graphHelper);
// 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(graphEl, state, mainIterationStartTime,
firstSectionCount, middleSectionCount,
lastSectionCount, graphHelper);
}
// Append endDelay.
if (state.endDelay > 0) {
renderEndDelay(graphEl, state,
mainIterationStartTime, iterationCount, graphHelper);
}
}
// Append negative delay (which overlap the animation).
if (state.delay < 0) {
graphHelper.setFillMode("both");
graphHelper.setOriginalBehavior(false);
renderNegativeDelayHiddenProgress(graphEl, state, graphHelper);
}
// Append negative endDelay (which overlap the animation).
if (state.iterationCount && state.endDelay < 0) {
graphHelper.setFillMode("both");
graphHelper.setOriginalBehavior(false);
renderNegativeEndDelayHiddenProgress(graphEl, state, graphHelper);
}
return graphEl;
}
/**
* Render delay section.
* @param {Element} parentEl - Parent element of this appended path element.
* @param {Object} state - State of animation.
* @param {Object} graphHelper - SummaryGraphHelper.
*/
function renderDelay(parentEl, state, graphHelper) {
const startSegment = graphHelper.getSegment(0);
const endSegment = { x: state.delay, y: startSegment.y };
graphHelper.appendShapePath(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 {Object} graphHelper - SummaryGraphHelper.
*/
function renderFirstIteration(parentEl, state, mainIterationStartTime,
firstSectionCount, graphHelper) {
const startTime = mainIterationStartTime;
const endTime = startTime + firstSectionCount * state.duration;
const segments = graphHelper.createPathSegments(startTime, endTime);
graphHelper.appendShapePath(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 {Object} graphHelper - SummaryGraphHelper.
*/
function renderMiddleIterations(parentEl, state, mainIterationStartTime,
firstSectionCount, middleSectionCount,
graphHelper) {
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 = graphHelper.createPathSegments(startTime, endTime);
graphHelper.appendShapePath(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 {Object} graphHelper - SummaryGraphHelper.
*/
function renderLastIteration(parentEl, state, mainIterationStartTime,
firstSectionCount, middleSectionCount,
lastSectionCount, graphHelper) {
const startTime = mainIterationStartTime +
(firstSectionCount + middleSectionCount) * state.duration;
const endTime = startTime + lastSectionCount * state.duration;
const segments = graphHelper.createPathSegments(startTime, endTime);
graphHelper.appendShapePath(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 {Object} graphHelper - SummaryGraphHelper.
*/
function renderInfinity(parentEl, state, mainIterationStartTime,
firstSectionCount, totalDuration, graphHelper) {
// 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 =
graphHelper.createPathSegments(firstStartTime, firstEndTime);
graphHelper.appendShapePath(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 };
});
}
graphHelper.appendShapePath(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} graphHelper - SummaryGraphHelper.
*/
function renderEndDelay(parentEl, state,
mainIterationStartTime, iterationCount, graphHelper) {
const startTime = mainIterationStartTime + iterationCount * state.duration;
const startSegment = graphHelper.getSegment(startTime);
const endSegment = { x: startTime + state.endDelay, y: startSegment.y };
graphHelper.appendShapePath(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} graphHelper - SummaryGraphHelper.
*/
function renderForwardsFill(parentEl, state, mainIterationStartTime,
iterationCount, totalDuration, graphHelper) {
const startTime = mainIterationStartTime + iterationCount * state.duration +
(state.endDelay > 0 ? state.endDelay : 0);
const startSegment = graphHelper.getSegment(startTime);
const endSegment = { x: totalDuration, y: startSegment.y };
graphHelper.appendShapePath(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 {Object} graphHelper - SummaryGraphHelper.
*/
function renderNegativeDelayHiddenProgress(parentEl, state, graphHelper) {
const startTime = state.delay;
const endTime = 0;
const segments =
graphHelper.createPathSegments(startTime, endTime);
graphHelper.appendShapePath(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 {Object} graphHelper - SummaryGraphHelper.
*/
function renderNegativeEndDelayHiddenProgress(parentEl, state, graphHelper) {
const endTime = state.delay + state.iterationCount * state.duration;
const startTime = endTime + state.endDelay;
const segments = graphHelper.createPathSegments(startTime, endTime);
graphHelper.appendShapePath(parentEl, segments, "enddelay-path negative");
}
/**
* Create new keyframes object which has only offset and easing.
* Also, the returned value has no duplication.
* @param {Object} tracks - The value of AnimationsTimeline.getTracks().
* @return {Array} keyframes list.
*/
function getOffsetAndEasingOnlyKeyframesList(tracks) {
return Object.keys(tracks).reduce((result, name) => {
const track = tracks[name];
const exists = result.find(keyframes => {
if (track.length !== keyframes.length) {
return false;
}
for (let i = 0; i < track.length; i++) {
const keyframe1 = track[i];
const keyframe2 = keyframes[i];
if (keyframe1.offset !== keyframe2.offset ||
keyframe1.easing !== keyframe2.easing) {
return false;
}
}
return true;
});
if (!exists) {
const keyframes = track.map(keyframe => {
return { offset: keyframe.offset, easing: keyframe.easing };
});
result.push(keyframes);
}
return result;
}, []);
}

View file

@ -1,806 +0,0 @@
/* -*- 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,
getFormattedAnimationTitle,
TimeScale,
getCssPropertyName
} = require("devtools/client/inspector/animation-old/utils");
const { AnimationDetails } = require("devtools/client/inspector/animation-old/components/animation-details");
const { AnimationTargetNode } = require("devtools/client/inspector/animation-old/components/animation-target-node");
const { AnimationTimeBlock } = require("devtools/client/inspector/animation-old/components/animation-time-block");
const { LocalizationHelper } = require("devtools/shared/l10n");
const L10N =
new LocalizationHelper("devtools/client/locales/animationinspector.properties");
// 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.componentsMap = {};
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.onTimelineDataChanged = this.onTimelineDataChanged.bind(this);
this.onDetailCloseButtonClick = this.onDetailCloseButtonClick.bind(this);
EventEmitter.decorate(this);
}
exports.AnimationsTimeline = AnimationsTimeline;
AnimationsTimeline.prototype = {
init: function(containerEl) {
this.win = containerEl.ownerDocument.defaultView;
this.rootWrapperEl = containerEl;
this.setupSplitBox();
this.setupAnimationTimeline();
this.setupAnimationDetail();
this.win.addEventListener("resize",
this.onWindowResize);
},
setupSplitBox: function() {
const browserRequire = this.win.BrowserLoader({
window: this.win,
useOnlyShared: true
}).require;
const { createFactory } = browserRequire("devtools/client/shared/vendor/react");
const dom = require("devtools/client/shared/vendor/react-dom-factories");
const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
const SplitBox = createFactory(
browserRequire("devtools/client/shared/components/splitter/SplitBox"));
const splitter = SplitBox({
className: "animation-root",
splitterSize: 1,
initialHeight: "50%",
endPanelControl: true,
startPanel: dom.div({
className: "animation-timeline"
}),
endPanel: dom.div({
className: "animation-detail"
}),
vert: false
});
ReactDOM.render(splitter, this.rootWrapperEl);
this.animationRootEl = this.rootWrapperEl.querySelector(".animation-root");
},
setupAnimationTimeline: function() {
const animationTimelineEl = this.rootWrapperEl.querySelector(".animation-timeline");
const scrubberContainer = createNode({
parent: animationTimelineEl,
attributes: {"class": "scrubber-wrapper"}
});
this.scrubberEl = createNode({
parent: scrubberContainer,
attributes: {
"class": "scrubber"
}
});
this.scrubberHandleEl = createNode({
parent: this.scrubberEl,
attributes: {
"class": "scrubber-handle"
}
});
createNode({
parent: this.scrubberHandleEl,
attributes: {
"class": "scrubber-line"
}
});
this.scrubberHandleEl.addEventListener("mousedown",
this.onScrubberMouseDown);
this.headerWrapper = createNode({
parent: animationTimelineEl,
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: animationTimelineEl,
attributes: {
"class": "time-body track-container"
}
});
this.animationsEl = createNode({
parent: animationTimelineEl,
nodeType: "ul",
attributes: {
"class": "animations devtools-monospace"
}
});
},
setupAnimationDetail: function() {
const animationDetailEl = this.rootWrapperEl.querySelector(".animation-detail");
const animationDetailHeaderEl = createNode({
parent: animationDetailEl,
attributes: {
"class": "animation-detail-header"
}
});
const headerTitleEl = createNode({
parent: animationDetailHeaderEl,
attributes: {
"class": "devtools-toolbar"
}
});
createNode({
parent: headerTitleEl,
textContent: L10N.getStr("detail.headerTitle")
});
this.animationAnimationNameEl = createNode({
parent: headerTitleEl
});
this.animationDetailCloseButton = createNode({
parent: headerTitleEl,
nodeType: "button",
attributes: {
"class": "devtools-button",
title: L10N.getStr("detail.header.closeLabel"),
}
});
this.animationDetailCloseButton.addEventListener("click",
this.onDetailCloseButtonClick);
const animationDetailBodyEl = createNode({
parent: animationDetailEl,
attributes: {
"class": "animation-detail-body"
}
});
this.animatedPropertiesEl = createNode({
parent: animationDetailBodyEl,
attributes: {
"class": "animated-properties"
}
});
this.details = new AnimationDetails(this.serverTraits);
this.details.init(this.animatedPropertiesEl);
},
destroy: function() {
this.stopAnimatingScrubber();
this.unrender();
this.details.destroy();
this.win.removeEventListener("resize",
this.onWindowResize);
this.timeHeaderEl.removeEventListener("mousedown",
this.onScrubberMouseDown);
this.scrubberHandleEl.removeEventListener("mousedown",
this.onScrubberMouseDown);
this.animationDetailCloseButton.removeEventListener("click",
this.onDetailCloseButtonClick);
this.rootWrapperEl.remove();
this.animations = [];
this.componentsMap = null;
this.rootWrapperEl = null;
this.timeHeaderEl = null;
this.animationsEl = null;
this.animatedPropertiesEl = null;
this.scrubberEl = null;
this.scrubberHandleEl = null;
this.win = null;
this.inspector = null;
this.serverTraits = null;
this.animationDetailEl = null;
this.animationAnimationNameEl = null;
this.animatedPropertiesEl = null;
this.animationDetailCloseButton = null;
this.animationRootEl = null;
this.selectedAnimation = null;
this.isDestroyed = true;
},
/**
* Destroy all sub-components that have been created and stored on this instance.
*/
destroyAllSubComponents: function() {
for (const actorID in this.componentsMap) {
this.destroySubComponents(actorID);
}
},
/**
* Destroy sub-components which related to given actor id.
* @param {String} actor id
*/
destroySubComponents: function(actorID) {
const components = this.componentsMap[actorID];
components.timeBlock.destroy();
components.targetNode.destroy();
components.animationEl.remove();
delete components.state;
delete components.tracks;
delete this.componentsMap[actorID];
},
unrender: function() {
for (const animation of this.animations) {
animation.off("changed", this.onAnimationStateChanged);
}
this.stopAnimatingScrubber();
TimeScale.reset();
this.destroyAllSubComponents();
this.animationsEl.innerHTML = "";
this.off("timeline-data-changed", this.onTimelineDataChanged);
this.details.unrender();
},
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);
},
async onAnimationSelected(animation) {
const index = this.animations.indexOf(animation);
if (index === -1) {
return;
}
// Unselect an animation which was selected.
const animationEls = this.rootWrapperEl.querySelectorAll(".animation");
for (let i = 0; i < animationEls.length; i++) {
const animationEl = animationEls[i];
if (!animationEl.classList.contains("selected")) {
continue;
}
if (i === index) {
if (this.animationRootEl.classList.contains("animation-detail-visible")) {
// Already the animation is selected.
this.emit("animation-already-selected", this.animations[i]);
return;
}
} else {
animationEl.classList.remove("selected");
this.emit("animation-unselected", this.animations[i]);
}
break;
}
// Add class of animation type to animatedPropertiesEl to display the compositor sign.
if (!this.animatedPropertiesEl.classList.contains(animation.state.type)) {
this.animatedPropertiesEl.className =
`animated-properties ${ animation.state.type }`;
}
// Select and render.
const selectedAnimationEl = animationEls[index];
selectedAnimationEl.classList.add("selected");
this.animationRootEl.classList.add("animation-detail-visible");
// Don't render if the detail displays same animation already.
if (animation !== this.details.animation) {
this.selectedAnimation = animation;
await this.details.render(animation, this.componentsMap[animation.actorID].tracks);
this.animationAnimationNameEl.textContent = getFormattedAnimationTitle(animation);
}
this.onTimelineDataChanged({ time: this.currentTime || 0 });
this.emit("animation-selected", animation);
},
/**
* When move the scrubber to the corresponding position
*/
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 + "%";
const 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;
},
async render(animations, documentCurrentTime) {
this.animations = animations;
// Destroy components which are no longer existed in given animations.
for (const animation of this.animations) {
if (this.componentsMap[animation.actorID]) {
this.componentsMap[animation.actorID].needToLeave = true;
}
}
for (const actorID in this.componentsMap) {
const components = this.componentsMap[actorID];
if (components.needToLeave) {
delete components.needToLeave;
} else {
this.destroySubComponents(actorID);
}
}
if (!this.animations.length) {
this.emit("animation-timeline-rendering-completed");
return;
}
// Loop to set the time scale for all current animations.
TimeScale.reset();
for (const {state} of animations) {
TimeScale.addAnimation(state);
}
this.drawHeaderAndBackground();
for (const animation of this.animations) {
animation.on("changed", this.onAnimationStateChanged);
const tracks = await this.getTracks(animation);
// If we're destroyed by now, just give up.
if (this.isDestroyed) {
return;
}
if (this.componentsMap[animation.actorID]) {
// Update animation UI using existent components.
this.updateAnimation(animation, tracks, this.componentsMap[animation.actorID]);
} else {
// Render animation UI as new element.
const animationEl = createNode({
parent: this.animationsEl,
nodeType: "li",
});
this.renderAnimation(animation, tracks, animationEl);
}
}
// 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);
}
// To indicate the animation progress in AnimationDetails.
this.on("timeline-data-changed", this.onTimelineDataChanged);
if (this.animations.length === 1) {
// Display animation's detail if there is only one animation,
// even if the detail pane is closing.
await this.onAnimationSelected(this.animations[0]);
} else if (this.animationRootEl.classList.contains("animation-detail-visible") &&
this.animations.includes(this.selectedAnimation)) {
// animation's detail displays in case of the previously displayed animation is
// included in timeline list and the detail pane is not closing.
await this.onAnimationSelected(this.selectedAnimation);
} else {
// Otherwise, close detail pane.
this.onDetailCloseButtonClick();
}
this.emit("animation-timeline-rendering-completed");
},
updateAnimation: function(animation, tracks, existentComponents) {
// If keyframes (tracks) and effect timing (state) are not changed, we update the
// view box only.
// As an exception, if iterationCount reprensents Infinity, we need to re-render
// the shape along new time scale.
// FIXME: To avoid re-rendering even Infinity, we need to change the
// representation for Infinity.
if (animation.state.iterationCount &&
areTimingEffectsEqual(existentComponents.state, animation.state) &&
existentComponents.tracks.toString() === tracks.toString()) {
// Update timeBlock.
existentComponents.timeBlock.update(animation);
} else {
// Destroy previous components.
existentComponents.timeBlock.destroy();
existentComponents.targetNode.destroy();
// Remove children to re-use.
existentComponents.animationEl.innerHTML = "";
// Re-render animation using existent animationEl.
this.renderAnimation(animation, tracks, existentComponents.animationEl);
}
},
renderAnimation: function(animation, tracks, animationEl) {
animationEl.setAttribute("class",
"animation " + animation.state.type +
this.getCompositorStatusClassName(animation.state));
// Left sidebar for the animated node.
const animatedNodeEl = createNode({
parent: animationEl,
attributes: {
"class": "target"
}
});
// Draw the animated node target.
const targetNode = new AnimationTargetNode(this.inspector, {compact: true});
targetNode.init(animatedNodeEl);
targetNode.render(animation);
// Right-hand part contains the timeline itself (called time-block here).
const timeBlockEl = createNode({
parent: animationEl,
attributes: {
"class": "time-block track-container"
}
});
// Draw the animation time block.
const timeBlock = new AnimationTimeBlock();
timeBlock.init(timeBlockEl);
timeBlock.render(animation, tracks);
timeBlock.on("selected", this.onAnimationSelected);
this.componentsMap[animation.actorID] = {
animationEl, targetNode, timeBlock, tracks, state: animation.state
};
},
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) {
const isOutOfBounds = time < TimeScale.minStartTime ||
time > TimeScale.maxEndTime;
const isAllPaused = !this.isAtLeastOneAnimationPlaying();
const 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)
});
const 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() {
const width = this.timeHeaderEl.offsetWidth;
const animationDuration = TimeScale.maxEndTime - TimeScale.minStartTime;
const minTimeInterval = TIME_GRADUATION_MIN_SPACING *
animationDuration / width;
const intervalLength = findOptimalTimeInterval(minTimeInterval);
const intervalWidth = intervalLength * width / animationDuration;
// And the time graduation header.
this.timeHeaderEl.innerHTML = "";
this.timeTickEl.innerHTML = "";
for (let i = 0; i <= width / intervalWidth; i++) {
const 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}%`
}
});
}
},
onTimelineDataChanged: function({ time }) {
this.currentTime = time;
const indicateTime =
TimeScale.minStartTime === Infinity ? 0 : this.currentTime + TimeScale.minStartTime;
this.details.indicateProgress(indicateTime);
},
onDetailCloseButtonClick: function(e) {
if (!this.animationRootEl.classList.contains("animation-detail-visible")) {
return;
}
this.animationRootEl.classList.remove("animation-detail-visible");
this.emit("animation-detail-closed");
},
/**
* Get a list of the tracks of the animation actor
* @param {Object} animation
* @return {Object} A list of tracks, one per animated property, each
* with a list of keyframes
*/
async getTracks(animation) {
const 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 = [];
try {
properties = await animation.getProperties();
} catch (e) {
// Expected if we've already been destroyed in the meantime.
if (!this.isDestroyed) {
throw e;
}
}
for (const {name, values} of properties) {
if (!tracks[name]) {
tracks[name] = [];
}
for (let {value, offset, easing, distance} of values) {
distance = distance ? distance : 0;
offset = parseFloat(offset.toFixed(3));
tracks[name].push({value, offset, easing, distance});
}
}
} else {
let frames = [];
try {
frames = await animation.getFrames();
} catch (e) {
// Expected if we've already been destroyed in the meantime.
if (!this.isDestroyed) {
throw e;
}
}
for (const frame of frames) {
for (const name in frame) {
if (this.NON_PROPERTIES.includes(name)) {
continue;
}
// We have to change to CSS property name
// since GetKeyframes returns JS property name.
const propertyCSSName = getCssPropertyName(name);
if (!tracks[propertyCSSName]) {
tracks[propertyCSSName] = [];
}
tracks[propertyCSSName].push({
value: frame[name],
offset: parseFloat(frame.computedOffset.toFixed(3)),
easing: frame.easing,
distance: 0
});
}
}
}
return tracks;
}
};
/**
* Check the equality given states as effect timing.
* @param {Object} state of animation.
* @param {Object} same to avobe.
* @return {boolean} true: same effect timing
*/
function areTimingEffectsEqual(stateA, stateB) {
for (const property of ["playbackRate", "duration", "delay", "endDelay",
"iterationCount", "iterationStart", "easing",
"fill", "direction"]) {
if (stateA[property] !== stateB[property]) {
return false;
}
}
return true;
}

View file

@ -1,236 +0,0 @@
/* -*- 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 {createNode, createSVGNode} =
require("devtools/client/inspector/animation-old/utils");
const { ProgressGraphHelper, } =
require("devtools/client/inspector/animation-old/graph-helper.js");
// Counter for linearGradient ID.
let LINEAR_GRADIENT_ID_COUNTER = 0;
/**
* UI component responsible for displaying a list of keyframes.
* Also, shows a graphical graph for the animation progress of one iteration.
*/
function Keyframes() {}
exports.Keyframes = Keyframes;
Keyframes.prototype = {
init: function(containerEl) {
this.containerEl = containerEl;
this.keyframesEl = createNode({
parent: this.containerEl,
attributes: {"class": "keyframes"}
});
},
destroy: function() {
this.keyframesEl.remove();
this.containerEl = this.keyframesEl = this.animation = null;
},
render: function({keyframes, propertyName, animation, animationType}) {
this.keyframes = keyframes;
this.propertyName = propertyName;
this.animation = animation;
// Create graph element.
const graphEl = createSVGNode({
parent: this.keyframesEl,
nodeType: "svg",
attributes: {
"preserveAspectRatio": "none"
}
});
// This visual is only one iteration,
// so we use animation.state.duration as total duration.
const totalDuration = animation.state.duration;
// Minimum segment duration is the duration of one pixel.
const minSegmentDuration =
totalDuration / this.containerEl.clientWidth;
// Create graph helper to render the animation property graph.
const win = this.containerEl.ownerGlobal;
const graphHelper =
new ProgressGraphHelper(win, propertyName, animationType, keyframes, totalDuration);
renderPropertyGraph(graphEl, totalDuration, minSegmentDuration, graphHelper);
// Destroy ProgressGraphHelper resources.
graphHelper.destroy();
// Set viewBox which includes invisible stroke width.
// At first, calculate invisible stroke width from maximum width.
// The reason why divide by 2 is that half of stroke width will be invisible
// if we use 0 or 1 for y axis.
const maxStrokeWidth =
win.getComputedStyle(graphEl.querySelector(".keyframes svg .hint")).strokeWidth;
const invisibleStrokeWidthInViewBox =
maxStrokeWidth / 2 / this.containerEl.clientHeight;
graphEl.setAttribute("viewBox",
`0 -${ 1 + invisibleStrokeWidthInViewBox }
${ totalDuration }
${ 1 + invisibleStrokeWidthInViewBox * 2 }`);
// Append elements to display keyframe values.
this.keyframesEl.classList.add(animation.state.type);
for (const frame of this.keyframes) {
createNode({
parent: this.keyframesEl,
attributes: {
"class": "frame",
"style": `left:${frame.offset * 100}%;`,
"data-offset": frame.offset,
"data-property": propertyName,
"title": frame.value
}
});
}
}
};
/**
* Render a graph representing the progress of the animation over one iteration.
* @param {Element} parentEl - Parent element of this appended path element.
* @param {Number} duration - Duration of one iteration.
* @param {Number} minSegmentDuration - Minimum segment duration.
* @param {ProgressGraphHelper} graphHelper - The object of ProgressGraphHalper.
*/
function renderPropertyGraph(parentEl, duration, minSegmentDuration, graphHelper) {
const segments = graphHelper.createPathSegments(duration, minSegmentDuration);
const graphType = graphHelper.getGraphType();
if (graphType !== "color") {
graphHelper.appendShapePath(parentEl, segments, graphType);
renderEasingHint(parentEl, segments, graphHelper);
return;
}
// Append the color to the path.
segments.forEach(segment => {
segment.y = 1;
});
const path = graphHelper.appendShapePath(parentEl, segments, graphType);
const defEl = createSVGNode({
parent: parentEl,
nodeType: "def"
});
const id = `color-property-${ LINEAR_GRADIENT_ID_COUNTER++ }`;
const linearGradientEl = createSVGNode({
parent: defEl,
nodeType: "linearGradient",
attributes: {
"id": id
}
});
segments.forEach(segment => {
createSVGNode({
parent: linearGradientEl,
nodeType: "stop",
attributes: {
"stop-color": segment.style,
"offset": segment.x / duration
}
});
});
path.style.fill = `url(#${ id })`;
renderEasingHintForColor(parentEl, graphHelper);
}
/**
* Renders the easing hint.
* This method renders an emphasized path over the easing path for a keyframe.
* It appears when hovering over the easing.
* It also renders a tooltip that appears when hovering.
* @param {Element} parentEl - Parent element of this appended path element.
* @param {Array} path segments - [{x: {Number} time, y: {Number} progress}, ...]
* @param {ProgressGraphHelper} graphHelper - The object of ProgressGraphHelper.
*/
function renderEasingHint(parentEl, segments, helper) {
const keyframes = helper.getKeyframes();
const duration = helper.getDuration();
// Split segments for each keyframe.
for (let i = 0, indexOfSegments = 0; i < keyframes.length - 1; i++) {
const startKeyframe = keyframes[i];
const endKeyframe = keyframes[i + 1];
const endTime = endKeyframe.offset * duration;
const keyframeSegments = [];
for (; indexOfSegments < segments.length; indexOfSegments++) {
const segment = segments[indexOfSegments];
keyframeSegments.push(segment);
if (startKeyframe.offset === endKeyframe.offset) {
keyframeSegments.push(segments[++indexOfSegments]);
break;
} else if (segment.x === endTime) {
break;
}
}
// Append easing hint as text and emphasis path.
const gEl = createSVGNode({
parent: parentEl,
nodeType: "g"
});
createSVGNode({
parent: gEl,
nodeType: "title",
textContent: startKeyframe.easing
});
helper.appendLinePath(gEl, keyframeSegments, `${helper.getGraphType()} hint`);
}
}
/**
* Render easing hint for properties that are represented by color.
* This method render as text only.
* @param {Element} parentEl - Parent element of this appended path element.
* @param {ProgressGraphHelper} graphHelper - The object of ProgressGraphHalper.
*/
function renderEasingHintForColor(parentEl, helper) {
const keyframes = helper.getKeyframes();
const duration = helper.getDuration();
// Split segments for each keyframe.
for (let i = 0; i < keyframes.length - 1; i++) {
const startKeyframe = keyframes[i];
const startTime = startKeyframe.offset * duration;
const endKeyframe = keyframes[i + 1];
const endTime = endKeyframe.offset * duration;
// Append easing hint.
const gEl = createSVGNode({
parent: parentEl,
nodeType: "g"
});
createSVGNode({
parent: gEl,
nodeType: "title",
textContent: startKeyframe.easing
});
createSVGNode({
parent: gEl,
nodeType: "rect",
attributes: {
x: startTime,
y: -1,
width: endTime - startTime,
height: 1,
class: "hint",
}
});
}
}

View file

@ -1,12 +0,0 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
DevToolsModules(
'animation-details.js',
'animation-target-node.js',
'animation-time-block.js',
'animation-timeline.js',
'keyframes.js',
'rate-selector.js'
)

View file

@ -1,105 +0,0 @@
/* -*- 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/inspector/animation-old/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) {
const animationsRates = this.getAnimationsRates(animations);
if (animationsRates.length > 1) {
return PLAYBACK_RATES;
}
return sortedUnique(PLAYBACK_RATES.concat(animationsRates));
},
render: function(animations) {
const allRates = this.getAnimationsRates(animations);
const 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 (const rate of this.getAllRates(animations)) {
const 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() {
const rate = parseFloat(this.selectEl.value);
if (!isNaN(rate)) {
this.emit("rate-changed", rate);
}
}
};
const sortedUnique = arr => [...new Set(arr)].sort((a, b) => a > b);

View file

@ -1,816 +0,0 @@
/* -*- 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 {createSVGNode, getJsPropertyName} =
require("devtools/client/inspector/animation-old/utils");
const {colorUtils} = require("devtools/shared/css/color.js");
const {parseTimingFunction} = require("devtools/client/shared/widgets/CubicBezierWidget");
// 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
// DEFAULT_MIN_PROGRESS_THRESHOLD, then createPathSegments re-divides
// by DURATION_RESOLUTION.
// DURATION_RESOLUTION shoud be integer and more than 2.
const DURATION_RESOLUTION = 4;
// DEFAULT_MIN_PROGRESS_THRESHOLD shoud be between more than 0 to 1.
const DEFAULT_MIN_PROGRESS_THRESHOLD = 0.1;
exports.DEFAULT_MIN_PROGRESS_THRESHOLD = DEFAULT_MIN_PROGRESS_THRESHOLD;
// BOUND_EXCLUDING_TIME should be less than 1ms and is used to exclude start
// and end bounds when dividing duration in createPathSegments.
const BOUND_EXCLUDING_TIME = 0.001;
/**
* This helper return the segment coordinates and style for property graph,
* also return the graph type.
* Parameters of constructor are below.
* @param {Window} win - window object to animate.
* @param {String} propertyCSSName - CSS property name (e.g. background-color).
* @param {String} animationType - Animation type of CSS property.
* @param {Object} keyframes - AnimationInspector's keyframes object.
* @param {float} duration - Duration of animation.
*/
function ProgressGraphHelper(win, propertyCSSName, animationType, keyframes, duration) {
this.win = win;
const doc = this.win.document;
this.targetEl = doc.createElement("div");
doc.documentElement.appendChild(this.targetEl);
this.propertyCSSName = propertyCSSName;
this.propertyJSName = getJsPropertyName(this.propertyCSSName);
this.animationType = animationType;
// Create keyframe object to make dummy animation.
const keyframesObject = keyframes.map(keyframe => {
const keyframeObject = Object.assign({}, keyframe);
keyframeObject[this.propertyJSName] = keyframe.value;
return keyframeObject;
});
// Create effect timing object to make dummy animation.
const effectTiming = {
duration: duration,
fill: "forwards"
};
this.keyframes = keyframesObject;
this.devtoolsKeyframes = keyframes;
this.valueHelperFunction = this.getValueHelperFunction();
this.animation = this.targetEl.animate(this.keyframes, effectTiming);
this.animation.pause();
}
ProgressGraphHelper.prototype = {
/**
* Destory this object.
*/
destroy: function() {
this.targetEl.remove();
this.animation.cancel();
this.targetEl = null;
this.animation = null;
this.valueHelperFunction = null;
this.propertyCSSName = null;
this.propertyJSName = null;
this.animationType = null;
this.keyframes = null;
this.win = null;
},
/**
* Return animation duration.
* @return {Number} duration
*/
getDuration: function() {
return this.animation.effect.getComputedTiming().duration;
},
/**
* Return animation's keyframe.
* @return {Object} keyframe
*/
getKeyframes: function() {
return this.keyframes;
},
/**
* Return graph type.
* @return {String} if property is 'opacity' or 'transform', return that value.
* Otherwise, return given animation type in constructor.
*/
getGraphType: function() {
return (this.propertyJSName === "opacity" || this.propertyJSName === "transform")
? this.propertyJSName : this.animationType;
},
/**
* Return a segment in graph by given the time.
* @return {Object} Computed result which has follwing values.
* - x: x value of graph (float)
* - y: y value of graph (float between 0 - 1)
* - style: the computed style value of the property at the time
*/
getSegment: function(time) {
this.animation.currentTime = time;
const style = this.win.getComputedStyle(this.targetEl)[this.propertyJSName];
const value = this.valueHelperFunction(style);
return { x: time, y: value, style: style };
},
/**
* Get a value helper function which calculates the value of Y axis by animation type.
* @return {function} ValueHelperFunction returns float value of Y axis
* from given progress and style (e.g. rgb(0, 0, 0))
*/
getValueHelperFunction: function() {
switch (this.animationType) {
case "none": {
return () => 1;
}
case "float": {
return this.getFloatValueHelperFunction();
}
case "coord": {
return this.getCoordinateValueHelperFunction();
}
case "color": {
return this.getColorValueHelperFunction();
}
case "discrete": {
return this.getDiscreteValueHelperFunction();
}
}
return null;
},
/**
* Return value helper function of animation type 'float'.
* @param {Object} keyframes - This object shoud be same as
* the parameter of getGraphHelper.
* @return {function} ValueHelperFunction returns float value of Y axis
* from given float (e.g. 1.0, 0.5 and so on)
*/
getFloatValueHelperFunction: function() {
let maxValue = 0;
let minValue = Infinity;
this.keyframes.forEach(keyframe => {
maxValue = Math.max(maxValue, keyframe.value);
minValue = Math.min(minValue, keyframe.value);
});
const distance = maxValue - minValue;
return value => {
return (value - minValue) / distance;
};
},
/**
* Return value helper function of animation type 'coord'.
* @return {function} ValueHelperFunction returns float value of Y axis
* from given style (e.g. 100px)
*/
getCoordinateValueHelperFunction: function() {
let maxValue = 0;
let minValue = Infinity;
for (let i = 0, n = this.keyframes.length; i < n; i++) {
if (this.keyframes[i].value.match(/calc/)) {
return null;
}
const value = parseFloat(this.keyframes[i].value);
minValue = Math.min(minValue, value);
maxValue = Math.max(maxValue, value);
}
const distance = maxValue - minValue;
return value => {
return (parseFloat(value) - minValue) / distance;
};
},
/**
* Return value helper function of animation type 'color'.
* @param {Object} keyframes - This object shoud be same as
* the parameter of getGraphHelper.
* @return {function} ValueHelperFunction returns float value of Y axis
* from given color (e.g. rgb(0, 0, 0))
*/
getColorValueHelperFunction: function() {
const maxObject = { distance: 0 };
for (let i = 0; i < this.keyframes.length - 1; i++) {
const value1 = getRGBA(this.keyframes[i].value);
for (let j = i + 1; j < this.keyframes.length; j++) {
const value2 = getRGBA(this.keyframes[j].value);
const distance = getRGBADistance(value1, value2);
if (maxObject.distance >= distance) {
continue;
}
maxObject.distance = distance;
maxObject.value1 = value1;
maxObject.value2 = value2;
}
}
const baseValue =
maxObject.value1 < maxObject.value2 ? maxObject.value1 : maxObject.value2;
return value => {
const colorValue = getRGBA(value);
return getRGBADistance(baseValue, colorValue) / maxObject.distance;
};
},
/**
* Return value helper function of animation type 'discrete'.
* @return {function} ValueHelperFunction returns float value of Y axis
* from given style (e.g. center)
*/
getDiscreteValueHelperFunction: function() {
const discreteValues = [];
this.keyframes.forEach(keyframe => {
// Set style once since the computed value may differ from specified keyframe value.
this.targetEl.style[this.propertyJSName] = keyframe.value;
const style = this.win.getComputedStyle(this.targetEl)[this.propertyJSName];
if (!discreteValues.includes(style)) {
discreteValues.push(style);
}
});
this.targetEl.style[this.propertyJSName] = "unset";
return value => {
return discreteValues.indexOf(value) / (discreteValues.length - 1);
};
},
/**
* Create the path segments from given parameters.
*
* @param {Number} duration - Duration of animation.
* @param {Number} minSegmentDuration - Minimum segment duration.
* @param {Number} minProgressThreshold - Minimum progress threshold.
* @return {Array} path segments -
* [{x: {Number} time, y: {Number} progress}, ...]
*/
createPathSegments: function(duration, minSegmentDuration, minProgressThreshold) {
if (!this.valueHelperFunction) {
return createKeyframesPathSegments(duration, this.devtoolsKeyframes);
}
const segments = [];
for (let i = 0; i < this.devtoolsKeyframes.length - 1; i++) {
const startKeyframe = this.devtoolsKeyframes[i];
const endKeyframe = this.devtoolsKeyframes[i + 1];
const startTime = startKeyframe.offset * duration;
const endTime = endKeyframe.offset * duration;
if (startKeyframe.offset === endKeyframe.offset) {
const startSegment = this.getSegment(startTime - BOUND_EXCLUDING_TIME);
startSegment.x = startTime;
const endSegment = this.getSegment(endTime);
segments.push(startSegment, endSegment);
} else {
let threshold = getPreferredProgressThreshold(startKeyframe.easing);
if (threshold !== DEFAULT_MIN_PROGRESS_THRESHOLD) {
// We should consider the keyframe's duration.
threshold *= (endKeyframe.offset - startKeyframe.offset);
}
segments.push(...createPathSegments(startTime, endTime - BOUND_EXCLUDING_TIME,
minSegmentDuration, threshold, this));
}
}
const lastKeyframe = this.devtoolsKeyframes[this.devtoolsKeyframes.length - 1];
const lastTime = lastKeyframe.offset * duration;
segments.push(this.getSegment(lastTime));
return segments;
},
/**
* Append path element as shape. Also, this method appends two segment
* that are {start x, 0} and {end x, 0} to make shape.
* @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.
*/
appendShapePath: function(parentEl, pathSegments, cls) {
return appendShapePath(parentEl, pathSegments, cls);
},
/**
* Append path element as line.
* @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.
*/
appendLinePath: function(parentEl, pathSegments, cls) {
const isClosePathNeeded = false;
return appendPathElement(parentEl, pathSegments, cls, isClosePathNeeded);
},
};
exports.ProgressGraphHelper = ProgressGraphHelper;
/**
* This class is used for creating the summary graph in animation-timeline.
* The shape of the graph can be changed by using the following methods:
* setKeyframes:
* If null, the shape is by computed timing progress.
* Otherwise, by computed style of 'opacity' to combine effect easing and
* keyframe's easing.
* setFillMode:
* Animation fill-mode (e.g. "none", "backwards", "forwards" or "both")
* setClosePathNeeded:
* If true, appendShapePath make the last segment of <path> element to
* "close" segment("Z").
* Therefore, if don't need under-line of graph, please set false.
* setOriginalBehavior:
* In Animation::SetCurrentTime spec, even if current time of animation is over
* the endTime, the progress is changed. Likewise, in case the time is less than 0.
* If set true, prevent the time to make the same animation behavior as the original.
* setMinProgressThreshold:
* SummaryGraphHelper searches and creates the summary graph until the progress
* distance is less than this minProgressThreshold.
* So, while setting a low threshold produces a smooth graph,
* it will have an effect on performance.
* @param {Object} win - window object.
* @param {Object} state - animation state.
* @param {Number} minSegmentDuration - Minimum segment duration.
*/
function SummaryGraphHelper(win, state, minSegmentDuration) {
this.win = win;
const doc = this.win.document;
this.targetEl = doc.createElement("div");
doc.documentElement.appendChild(this.targetEl);
const effectTiming = Object.assign({}, state, {
iterations: state.iterationCount ? state.iterationCount : Infinity
});
this.animation = this.targetEl.animate(null, effectTiming);
this.animation.pause();
this.endTime = this.animation.effect.getComputedTiming().endTime;
this.minSegmentDuration = minSegmentDuration;
this.minProgressThreshold = DEFAULT_MIN_PROGRESS_THRESHOLD;
}
SummaryGraphHelper.prototype = {
/**
* Destory this object.
*/
destroy: function() {
this.animation.cancel();
this.targetEl.remove();
this.targetEl = null;
this.animation = null;
this.win = null;
},
/*
* Set keyframes to shape graph by computed style. This method creates new keyframe
* object using only offset and easing of given keyframes.
* Also, allows null value. In case of null, this graph helper shapes graph using
* computed timing progress.
* @param {Object} keyframes - Should have offset and easing, or null.
*/
setKeyframes: function(keyframes) {
let frames = null;
// We need to change the duration resolution in case of interval of keyframes offset
// was narrow.
let durationResolution = DURATION_RESOLUTION;
if (keyframes) {
let previousOffset = 0;
// Create new keyframes for opacity as computed style.
// The reason why we use computed value instead of computed timing progress is to
// include the easing in keyframes as well. Although the computed timing progress
// is not affected by the easing in keyframes at all, computed value reflects that.
frames = keyframes.map(keyframe => {
if (previousOffset && previousOffset != keyframe.offset) {
const interval = keyframe.offset - previousOffset;
durationResolution = Math.max(durationResolution, Math.ceil(1 / interval));
}
previousOffset = keyframe.offset;
return {
opacity: keyframe.offset,
offset: keyframe.offset,
easing: keyframe.easing
};
});
// Set the underlying opacity to zero so that if we sample the animation's output
// during the delay phase and it is not filling backwards, we get zero.
this.targetEl.style.opacity = 0;
}
this.durationResolution = durationResolution;
this.animation.effect.setKeyframes(frames);
this.hasFrames = !!frames;
},
/*
* Set animation behavior.
* In Animation::SetCurrentTime spec, even if current time of animation is over
* endTime, the progress is changed. Likewise, in case the time is less than 0.
* If set true, we prevent the time to make the same animation behavior as the original.
* @param {bool} isOriginalBehavior - true: original behavior
* false: according to spec.
*/
setOriginalBehavior: function(isOriginalBehavior) {
this.isOriginalBehavior = isOriginalBehavior;
},
/**
* Set animation fill mode.
* @param {String} fill - "both", "forwards", "backwards" or "both"
*/
setFillMode: function(fill) {
this.animation.effect.updateTiming({ fill });
},
/**
* Set true if need to close path in appendShapePath.
* @param {bool} isClosePathNeeded - true: close, false: open.
*/
setClosePathNeeded: function(isClosePathNeeded) {
this.isClosePathNeeded = isClosePathNeeded;
},
/**
* SummaryGraphHelper searches and creates the summary graph untill the progress
* distance is less than this minProgressThreshold.
*/
setMinProgressThreshold: function(minProgressThreshold) {
this.minProgressThreshold = minProgressThreshold;
},
/**
* Return a segment in graph by given the time.
* @return {Object} Computed result which has follwing values.
* - x: x value of graph (float)
* - y: y value of graph (float between 0 - 1)
*/
getSegment: function(time) {
if (this.isOriginalBehavior) {
// 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 value = this.hasFrames ? this.getOpacityValue() : this.getProgressValue();
return { x: time, y: value };
},
/**
* Create the path segments from given parameters.
* @param {Number} startTime - Starting time of animation.
* @param {Number} endTime - Ending time of animation.
* @return {Array} path segments -
* [{x: {Number} time, y: {Number} progress}, ...]
*/
createPathSegments: function(startTime, endTime) {
return createPathSegments(startTime, endTime, this.minSegmentDuration,
this.minProgressThreshold, this, this.durationResolution);
},
/**
* Append path element as shape. Also, this method appends two segment
* that are {start x, 0} and {end x, 0} to make shape.
* @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.
*/
appendShapePath: function(parentEl, pathSegments, cls) {
return appendShapePath(parentEl, pathSegments, cls, this.isClosePathNeeded);
},
/**
* Return current computed timing progress of the animation.
* @return {float} computed timing progress as float value of Y axis.
*/
getProgressValue: function() {
return Math.max(this.animation.effect.getComputedTiming().progress, 0);
},
/**
* Return current computed 'opacity' value of the element which is animating.
* @return {float} computed timing progress as float value of Y axis.
*/
getOpacityValue: function() {
return this.win.getComputedStyle(this.targetEl).opacity;
}
};
exports.SummaryGraphHelper = SummaryGraphHelper;
/**
* 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
* @param {Number} resolution - Duration resolution for first time.
* If null, use DURATION_RESOLUTION.
* - getSegment(time): Helper function that, given a time,
* will calculate the animation progress.
* @return {Array} path segments -
* [{x: {Number} time, y: {Number} progress}, ...]
*/
function createPathSegments(startTime, endTime, minSegmentDuration,
minProgressThreshold, segmentHelper,
resolution = DURATION_RESOLUTION) {
// 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) / resolution;
for (let index = 1; index <= 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 BOUND_EXCLUDING_TIME).
pathSegments = pathSegments.concat(
createPathSegments(previousSegment.x + BOUND_EXCLUDING_TIME,
currentSegment.x - BOUND_EXCLUDING_TIME,
minSegmentDuration, minProgressThreshold,
segmentHelper));
}
pathSegments.push(currentSegment);
previousSegment = currentSegment;
}
return pathSegments;
}
/**
* Append path element as shape. Also, this method appends two segment
* that are {start x, 0} and {end x, 0} to make shape.
* But does not affect given pathSegments.
* @param {Element} parentEl - Parent element of this appended path element.
* @param {Array} pathSegments - Path segments. Please see createPathSegments.
* @param {String} cls - Class name.
* @param {bool} isClosePathNeeded - Set true if need to close the path. (default true)
* @return {Element} path element.
*/
function appendShapePath(parentEl, pathSegments, cls, isClosePathNeeded = true) {
const segments = [
{ x: pathSegments[0].x, y: 0 },
...pathSegments,
{ x: pathSegments[pathSegments.length - 1].x, y: 0 }
];
return appendPathElement(parentEl, segments, cls, isClosePathNeeded);
}
/**
* 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.
* @param {bool} isClosePathNeeded - Set true if need to close the path.
* @return {Element} path element.
*/
function appendPathElement(parentEl, pathSegments, cls, isClosePathNeeded) {
// Create path string.
let currentSegment = pathSegments[0];
let path = `M${ currentSegment.x },${ currentSegment.y }`;
for (let i = 1; i < pathSegments.length; i++) {
const currentEasing = currentSegment.easing ? currentSegment.easing : "linear";
const nextSegment = pathSegments[i];
if (currentEasing === "linear") {
path += createLinePathString(nextSegment);
} else if (currentEasing.startsWith("steps")) {
path += createStepsPathString(currentSegment, nextSegment);
} else if (currentEasing.startsWith("frames")) {
path += createFramesPathString(currentSegment, nextSegment);
} else {
path += createCubicBezierPathString(currentSegment, nextSegment);
}
currentSegment = nextSegment;
}
if (isClosePathNeeded) {
path += " Z";
}
// Append and return the path element.
return createSVGNode({
parent: parentEl,
nodeType: "path",
attributes: {
"d": path,
"class": cls,
"vector-effect": "non-scaling-stroke",
"transform": "scale(1, -1)"
}
});
}
/**
* Create the path segments from given keyframes.
* @param {Number} duration - Duration of animation.
* @param {Object} Keyframes of devtool's format.
* @return {Array} path segments -
* [{x: {Number} time, y: {Number} distance,
* easing: {String} keyframe's easing,
* style: {String} keyframe's value}, ...]
*/
function createKeyframesPathSegments(duration, keyframes) {
return keyframes.map(keyframe => {
return {
x: keyframe.offset * duration,
y: keyframe.distance,
easing: keyframe.easing,
style: keyframe.value
};
});
}
/**
* Create a line path string.
* @param {Object} segment - e.g. { x: 100, y: 1 }
* @return {String} path string - e.g. "L100,1"
*/
function createLinePathString(segment) {
return ` L${ segment.x },${ segment.y }`;
}
/**
* Create a path string to represents a step function.
* @param {Object} currentSegment - e.g. { x: 0, y: 0, easing: "steps(2)" }
* @param {Object} nextSegment - e.g. { x: 1, y: 1 }
* @return {String} path string - e.g. "C 0.25 0.1, 0.25 1, 1 1"
*/
function createStepsPathString(currentSegment, nextSegment) {
const matches =
currentSegment.easing.match(/^steps\((\d+)(,\sstart)?\)/);
const stepNumber = parseInt(matches[1], 10);
const oneStepX = (nextSegment.x - currentSegment.x) / stepNumber;
const oneStepY = (nextSegment.y - currentSegment.y) / stepNumber;
const isStepStart = matches[2];
const stepOffsetY = isStepStart ? 1 : 0;
let path = "";
for (let step = 0; step < stepNumber; step++) {
const sx = currentSegment.x + step * oneStepX;
const ex = sx + oneStepX;
const y = currentSegment.y + (step + stepOffsetY) * oneStepY;
path += ` L${ sx },${ y } L${ ex },${ y }`;
}
if (!isStepStart) {
path += ` L${ nextSegment.x },${ nextSegment.y }`;
}
return path;
}
/**
* Create a path string to represents a frames function.
* @param {Object} currentSegment - e.g. { x: 0, y: 0, easing: "frames(2)" }
* @param {Object} nextSegment - e.g. { x: 1, y: 1 }
* @return {String} path string - e.g. "C 0.25 0.1, 0.25 1, 1 1"
*/
function createFramesPathString(currentSegment, nextSegment) {
const matches =
currentSegment.easing.match(/^frames\((\d+)\)/);
const framesNumber = parseInt(matches[1], 10);
const oneFrameX = (nextSegment.x - currentSegment.x) / framesNumber;
const oneFrameY = (nextSegment.y - currentSegment.y) / (framesNumber - 1);
let path = "";
for (let frame = 0; frame < framesNumber; frame++) {
const sx = currentSegment.x + frame * oneFrameX;
const ex = sx + oneFrameX;
const y = currentSegment.y + frame * oneFrameY;
path += ` L${ sx },${ y } L${ ex },${ y }`;
}
return path;
}
/**
* Create a path string to represents a bezier curve.
* @param {Object} currentSegment - e.g. { x: 0, y: 0, easing: "ease" }
* @param {Object} nextSegment - e.g. { x: 1, y: 1 }
* @return {String} path string - e.g. "C 0.25 0.1, 0.25 1, 1 1"
*/
function createCubicBezierPathString(currentSegment, nextSegment) {
const controlPoints = parseTimingFunction(currentSegment.easing);
if (!controlPoints) {
// Just return line path string since we could not parse this easing.
return createLinePathString(currentSegment);
}
const cp1x = controlPoints[0];
const cp1y = controlPoints[1];
const cp2x = controlPoints[2];
const cp2y = controlPoints[3];
const diffX = nextSegment.x - currentSegment.x;
const diffY = nextSegment.y - currentSegment.y;
let path =
` C ${ currentSegment.x + diffX * cp1x } ${ currentSegment.y + diffY * cp1y }`;
path += `, ${ currentSegment.x + diffX * cp2x } ${ currentSegment.y + diffY * cp2y }`;
path += `, ${ nextSegment.x } ${ nextSegment.y }`;
return path;
}
/**
* Parse given RGBA string.
* @param {String} colorString - e.g. rgb(0, 0, 0) or rgba(0, 0, 0, 0.5) and so on.
* @return {Object} RGBA {r: r, g: g, b: b, a: a}.
*/
function getRGBA(colorString) {
const color = new colorUtils.CssColor(colorString);
return color.getRGBATuple();
}
/**
* Return the distance from give two RGBA.
* @param {Object} rgba1 - RGBA (format is same to getRGBA)
* @param {Object} rgba2 - RGBA (format is same to getRGBA)
* @return {float} distance.
*/
function getRGBADistance(rgba1, rgba2) {
const startA = rgba1.a;
const startR = rgba1.r * startA;
const startG = rgba1.g * startA;
const startB = rgba1.b * startA;
const endA = rgba2.a;
const endR = rgba2.r * endA;
const endG = rgba2.g * endA;
const endB = rgba2.b * endA;
const diffA = startA - endA;
const diffR = startR - endR;
const diffG = startG - endG;
const diffB = startB - endB;
return Math.sqrt(diffA * diffA + diffR * diffR + diffG * diffG + diffB * diffB);
}
/**
* Return preferred progress threshold for given keyframes.
* See the documentation of DURATION_RESOLUTION and DEFAULT_MIN_PROGRESS_THRESHOLD
* for more information regarding this.
* @param {Array} keyframes - keyframes
* @return {float} - preferred progress threshold.
*/
function getPreferredKeyframesProgressThreshold(keyframes) {
let minProgressTreshold = DEFAULT_MIN_PROGRESS_THRESHOLD;
for (let i = 0; i < keyframes.length - 1; i++) {
const keyframe = keyframes[i];
if (!keyframe.easing) {
continue;
}
let keyframeProgressThreshold = getPreferredProgressThreshold(keyframe.easing);
if (keyframeProgressThreshold !== DEFAULT_MIN_PROGRESS_THRESHOLD) {
// We should consider the keyframe's duration.
keyframeProgressThreshold *=
(keyframes[i + 1].offset - keyframe.offset);
}
minProgressTreshold = Math.min(keyframeProgressThreshold, minProgressTreshold);
}
return minProgressTreshold;
}
exports.getPreferredKeyframesProgressThreshold = getPreferredKeyframesProgressThreshold;
/**
* Return preferred progress threshold to render summary graph.
* @param {String} - easing e.g. steps(2), linear and so on.
* @return {float} - preferred threshold.
*/
function getPreferredProgressThreshold(easing) {
const stepOrFramesFunction = easing.match(/(steps|frames)\((\d+)/);
return stepOrFramesFunction
? 1 / (parseInt(stepOrFramesFunction[2], 10) + 1)
: DEFAULT_MIN_PROGRESS_THRESHOLD;
}
exports.getPreferredProgressThreshold = getPreferredProgressThreshold;

View file

@ -1,20 +0,0 @@
# -*- 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(
'graph-helper.js',
'utils.js'
)
with Files('**'):
BUG_COMPONENT = ('DevTools', 'Animation Inspector')

View file

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

View file

@ -1,89 +0,0 @@
[DEFAULT]
tags = devtools
subsuite = devtools
support-files =
doc_add_animation.html
doc_body_animation.html
doc_delayed_starttime_animations.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_short_duration_animation.html
doc_simple_animation.html
doc_multiple_animation_types.html
doc_multiple_easings.html
doc_multiple_property_types.html
doc_timing_combination_animation.html
head.js
!/devtools/client/inspector/test/head.js
!/devtools/client/inspector/test/shared-head.js
!/devtools/client/shared/test/frame-script-utils.js
!/devtools/client/shared/test/shared-head.js
!/devtools/client/shared/test/telemetry-test-helpers.js
!/devtools/client/shared/test/test-actor-registry.js
!/devtools/client/shared/test/test-actor.js
[browser_animation_animated_properties_displayed.js]
[browser_animation_animated_properties_for_delayed_starttime_animations.js]
[browser_animation_animated_properties_path.js]
[browser_animation_animated_properties_progress_indicator.js]
[browser_animation_click_selects_animation.js]
[browser_animation_controller_exposes_document_currentTime.js]
[browser_animation_detail_displayed.js]
skip-if = os == "linux" && !debug # Bug 1234567
[browser_animation_detail_easings.js]
[browser_animation_empty_on_invalid_nodes.js]
[browser_animation_keyframe_markers.js]
[browser_animation_mutations_with_same_names.js]
skip-if = true # Bug 1447710
[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_refresh_when_active_after_mutations.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_summarygraph_for_multiple_easings.js]
[browser_animation_target_highlight_select.js]
[browser_animation_target_highlighter_lock.js]
skip-if = verify
[browser_animation_timeline_add_animation.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]
skip-if = (verify && debug && (os == 'linux'))
[browser_animation_timeline_pause_button_03.js]
[browser_animation_timeline_rate_selector.js]
skip-if = (os == "win" && ccov) # Bug 1444211
[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_short_duration.js]
[browser_animation_timeline_shows_delay.js]
[browser_animation_timeline_shows_endDelay.js]
[browser_animation_timeline_shows_iterations.js]
[browser_animation_timeline_shows_name_label.js]
[browser_animation_timeline_shows_time_info.js]
[browser_animation_timeline_takes_rate_into_account.js]
[browser_animation_timeline_ui.js]
[browser_animation_toggle_button_resets_on_navigate.js]
[browser_animation_toggle_button_toggles_animations.js]
[browser_animation_toolbar_exists.js]
[browser_animation_ui_updates_when_animation_data_changes.js]

View file

@ -1,87 +0,0 @@
/* 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 = [
"border-bottom-left-radius",
"border-bottom-right-radius",
"border-top-left-radius",
"border-top-right-radius",
"filter",
"height",
"transform",
"width",
// Unchanged value properties
"background-attachment",
"background-clip",
"background-color",
"background-image",
"background-origin",
"background-position-x",
"background-position-y",
"background-repeat",
"background-size"
].sort();
add_task(async function() {
await addTab(URL_ROOT + "doc_keyframes.html");
const {panel} = await openAnimationInspector();
const timeline = panel.animationsTimelineComponent;
const propertiesList = timeline.rootWrapperEl
.querySelector(".animated-properties");
// doc_keyframes.html has only one animation,
// so the propertiesList shoud be shown.
ok(isNodeVisible(propertiesList),
"The list of properties panel shoud be 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 same animation again");
await clickOnAnimation(panel, 0, true);
ok(isNodeVisible(propertiesList),
"The list of properties panel keeps");
});
function hasExpectedProperties(containerEl) {
const 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) {
const warnings = [...containerEl.querySelectorAll(".warning")];
for (const warning of warnings) {
const warningID =
"CompositorAnimationWarningTransformWithSyncGeometricAnimations";
if (warning.getAttribute("title") == LAYOUT_ERRORS_L10N.getStr(warningID)) {
return true;
}
}
return false;
}

View file

@ -1,44 +0,0 @@
/* 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 for animations that have different starting time.
// We should check progress indicator working well even if the start time is not zero.
// Also, check that there is no duplication display.
add_task(async function() {
await addTab(URL_ROOT + "doc_delayed_starttime_animations.html");
const { panel } = await openAnimationInspector();
await setStyle(null, panel, "animation", "anim 100s", "#target2");
await setStyle(null, panel, "animation", "anim 100s", "#target3");
await setStyle(null, panel, "animation", "anim 100s", "#target4");
await setStyle(null, panel, "animation", "anim 100s", "#target5");
const timelineComponent = panel.animationsTimelineComponent;
const detailsComponent = timelineComponent.details;
const headers =
detailsComponent.containerEl.querySelectorAll(".animated-properties-header");
is(headers.length, 1, "There should be only one header in the details panel");
// Check indicator.
await clickOnAnimation(panel, 1);
const progressIndicatorEl = detailsComponent.progressIndicatorEl;
const startTime = detailsComponent.animation.state.previousStartTime;
detailsComponent.indicateProgress(0);
is(progressIndicatorEl.style.left, "0%",
"The progress indicator position should be 0% at 0ms");
detailsComponent.indicateProgress(startTime);
is(progressIndicatorEl.style.left, "0%",
"The progress indicator position should be 0% at start time");
detailsComponent.indicateProgress(startTime + 50 * 1000);
is(progressIndicatorEl.style.left, "50%",
"The progress indicator position should be 50% at half time of animation");
detailsComponent.indicateProgress(startTime + 99 * 1000);
is(progressIndicatorEl.style.left, "99%",
"The progress indicator position should be 99% at 99s");
detailsComponent.indicateProgress(startTime + 100 * 1000);
is(progressIndicatorEl.style.left, "0%",
"The progress indicator position should be 0% at end of animation");
});

View file

@ -1,357 +0,0 @@
/* 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 animated properties's graph.
// The graph constructs from SVG, also uses path (for shape), linearGradient,
// stop (for color) element and so on.
// We test followings.
// 1. class name - which represents the animation type.
// 2. coordinates of the path - x is time, y is graph y value which should be 0 - 1.
// The path of animation types 'color', 'coord', 'opacity' or 'discrete' are created by
// createPathSegments. Other types are created by createKeyframesPathSegments.
// 3. color - animation type 'color' has linearGradient element.
requestLongerTimeout(5);
const TEST_CASES = [
{
"background-color": {
expectedClass: "color",
expectedValues: [
{ x: 0, y: 0 },
{ x: 0, y: 1, color: "rgb(255, 0, 0)" },
{ x: 1000, y: 1, color: "rgb(0, 255, 0)" }
]
},
"background-repeat": {
expectedClass: "discrete",
expectedValues: [
{ x: 0, y: 0 },
{ x: 499.999, y: 0 },
{ x: 500, y: 1 },
{ x: 1000, y: 1 },
]
},
"font-size": {
expectedClass: "length",
expectedValues: [
{ x: 0, y: 0 },
{ x: 1000, y: 1 },
]
},
"margin-left": {
expectedClass: "coord",
expectedValues: [
{ x: 0, y: 0 },
{ x: 1000, y: 1 },
]
},
"opacity": {
expectedClass: "opacity",
expectedValues: [
{ x: 0, y: 0 },
{ x: 1000, y: 1 },
]
},
"text-align": {
expectedClass: "discrete",
expectedValues: [
{ x: 0, y: 0 },
{ x: 499.999, y: 0 },
{ x: 500, y: 1 },
{ x: 1000, y: 1 },
]
},
"transform": {
expectedClass: "transform",
expectedValues: [
{ x: 0, y: 0 },
{ x: 1000, y: 1 },
]
}
},
{
"background-color": {
expectedClass: "color",
expectedValues: [
{ x: 0, y: 0 },
{ x: 0, y: 1, color: "rgb(0, 255, 0)" },
{ x: 1000, y: 1, color: "rgb(255, 0, 0)" }
]
},
"background-repeat": {
expectedClass: "discrete",
expectedValues: [
{ x: 0, y: 0 },
{ x: 499.999, y: 0 },
{ x: 500, y: 1 },
{ x: 1000, y: 1 },
]
},
"font-size": {
expectedClass: "length",
expectedValues: [
{ x: 0, y: 0 },
{ x: 0, y: 1 },
{ x: 1000, y: 0 },
]
},
"margin-left": {
expectedClass: "coord",
expectedValues: [
{ x: 0, y: 0 },
{ x: 0, y: 1 },
{ x: 1000, y: 0 },
]
},
"opacity": {
expectedClass: "opacity",
expectedValues: [
{ x: 0, y: 0 },
{ x: 0, y: 1 },
{ x: 1000, y: 0 },
]
},
"text-align": {
expectedClass: "discrete",
expectedValues: [
{ x: 0, y: 0 },
{ x: 499.999, y: 0 },
{ x: 500, y: 1 },
{ x: 1000, y: 1 },
]
},
"transform": {
expectedClass: "transform",
expectedValues: [
{ x: 0, y: 0 },
{ x: 0, y: 1 },
{ x: 1000, y: 0 },
]
}
},
{
"background-color": {
expectedClass: "color",
expectedValues: [
{ x: 0, y: 0 },
{ x: 0, y: 1, color: "rgb(255, 0, 0)" },
{ x: 500, y: 1, color: "rgb(0, 0, 255)" },
{ x: 1000, y: 1, color: "rgb(0, 255, 0)" }
]
},
"background-repeat": {
expectedClass: "discrete",
expectedValues: [
{ x: 0, y: 0 },
{ x: 249.999, y: 0 },
{ x: 250, y: 1 },
{ x: 749.999, y: 1 },
{ x: 750, y: 0 },
{ x: 1000, y: 0 },
]
},
"font-size": {
expectedClass: "length",
expectedValues: [
{ x: 0, y: 0 },
{ x: 500, y: 1 },
{ x: 1000, y: 0 },
]
},
"margin-left": {
expectedClass: "coord",
expectedValues: [
{ x: 0, y: 0 },
{ x: 500, y: 1 },
{ x: 1000, y: 0 },
]
},
"opacity": {
expectedClass: "opacity",
expectedValues: [
{ x: 0, y: 0 },
{ x: 500, y: 1 },
{ x: 1000, y: 0 },
]
},
"text-align": {
expectedClass: "discrete",
expectedValues: [
{ x: 0, y: 0 },
{ x: 249.999, y: 0 },
{ x: 250, y: 1 },
{ x: 749.999, y: 1 },
{ x: 750, y: 0 },
{ x: 1000, y: 0 },
]
},
"transform": {
expectedClass: "transform",
expectedValues: [
{ x: 0, y: 0 },
{ x: 500, y: 1 },
{ x: 1000, y: 0 },
]
}
},
{
"background-color": {
expectedClass: "color",
expectedValues: [
{ x: 0, y: 0 },
{ x: 0, y: 1, color: "rgb(255, 0, 0)" },
{ x: 499.999, y: 1, color: "rgb(255, 0, 0)" },
{ x: 500, y: 1, color: "rgb(128, 128, 0)" },
{ x: 999.999, y: 1, color: "rgb(128, 128, 0)" },
{ x: 1000, y: 1, color: "rgb(0, 255, 0)" }
]
},
"background-repeat": {
expectedClass: "discrete",
expectedValues: [
{ x: 0, y: 0 },
{ x: 499.999, y: 0 },
{ x: 500, y: 1 },
{ x: 1000, y: 1 },
]
},
"font-size": {
expectedClass: "length",
expectedValues: [
{ x: 0, y: 0 },
{ x: 500, y: 0 },
{ x: 500, y: 0.5 },
{ x: 1000, y: 0.5 },
{ x: 1000, y: 1 },
]
},
"margin-left": {
expectedClass: "coord",
expectedValues: [
{ x: 0, y: 0 },
{ x: 499.999, y: 0 },
{ x: 500, y: 0.5 },
{ x: 999.999, y: 0.5 },
{ x: 1000, y: 1 },
]
},
"opacity": {
expectedClass: "opacity",
expectedValues: [
{ x: 0, y: 0 },
{ x: 499.999, y: 0 },
{ x: 500, y: 0.5 },
{ x: 999.999, y: 0.5 },
{ x: 1000, y: 1 },
]
},
"text-align": {
expectedClass: "discrete",
expectedValues: [
{ x: 0, y: 0 },
{ x: 499.999, y: 0 },
{ x: 500, y: 1 },
{ x: 1000, y: 1 },
]
},
"transform": {
expectedClass: "transform",
expectedValues: [
{ x: 0, y: 0 },
{ x: 500, y: 0 },
{ x: 500, y: 0.5 },
{ x: 1000, y: 0.5 },
{ x: 1000, y: 1 },
]
}
},
{
"opacity": {
expectedClass: "opacity",
expectedValues: [
{ x: 0, y: 0 },
{ x: 250, y: 0.25 },
{ x: 500, y: 0.5 },
{ x: 750, y: 0.75 },
{ x: 1000, y: 1 },
]
}
},
{
"opacity": {
expectedClass: "opacity",
expectedValues: [
{ x: 0, y: 0 },
{ x: 199, y: 0 },
{ x: 200, y: 0.25 },
{ x: 399, y: 0.25 },
{ x: 400, y: 0.5 },
{ x: 599, y: 0.5 },
{ x: 600, y: 0.75 },
{ x: 799, y: 0.75 },
{ x: 800, y: 1 },
{ x: 1000, y: 1 },
]
}
},
{
"opacity": {
expectedClass: "opacity",
expectedValues: [
{ x: 0, y: 0 },
{ x: 100, y: 1 },
{ x: 110, y: 1 },
{ x: 114.9, y: 1 },
{ x: 115, y: 0.5 },
{ x: 129.9, y: 0.5 },
{ x: 130, y: 0 },
{ x: 1000, y: 1 },
]
}
},
{
"opacity": {
expectedClass: "opacity",
expectedValues: [
{ x: 0, y: 1 },
{ x: 250, y: 1 },
{ x: 499, y: 1 },
{ x: 500, y: 1 },
{ x: 500, y: 0 },
{ x: 750, y: 0.5 },
{ x: 1000, y: 1 },
]
}
}
];
add_task(async function() {
await addTab(URL_ROOT + "doc_multiple_property_types.html");
const {panel} = await openAnimationInspector();
const timelineComponent = panel.animationsTimelineComponent;
const detailEl = timelineComponent.details.containerEl;
const hasClosePath = true;
for (let i = 0; i < TEST_CASES.length; i++) {
info(`Click to select the animation[${ i }]`);
await clickOnAnimation(panel, i);
const timeBlock = getAnimationTimeBlocks(panel)[0];
const state = timeBlock.animation.state;
const properties = TEST_CASES[i];
for (const property in properties) {
const testcase = properties[property];
info(`Test path of ${ property }`);
const className = testcase.expectedClass;
const pathEl = detailEl.querySelector(`path.${ className }`);
ok(pathEl, `Path element with class '${ className }' should exis`);
assertPathSegments(pathEl, state.duration, hasClosePath, testcase.expectedValues);
}
}
});

View file

@ -1,86 +0,0 @@
/* 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 progress indicator in animated properties.
// Since this indicator works with the timeline, after selecting each animation,
// click the timeline header to change the current time and check the change.
add_task(async function() {
await addTab(URL_ROOT + "doc_multiple_property_types.html");
const { panel } = await openAnimationInspector();
const timelineComponent = panel.animationsTimelineComponent;
const detailsComponent = timelineComponent.details;
info("Click to select the animation");
await clickOnAnimation(panel, 0);
let progressIndicatorEl = detailsComponent.progressIndicatorEl;
ok(progressIndicatorEl, "The progress indicator should be exist");
await clickOnTimelineHeader(panel, 0);
is(progressIndicatorEl.style.left, "0%",
"The left style of progress indicator element should be 0% at 0ms");
await clickOnTimelineHeader(panel, 0.5);
approximate(progressIndicatorEl.style.left, "50%",
"The left style of progress indicator element should be "
+ "approximately 50% at 500ms");
await clickOnTimelineHeader(panel, 1);
is(progressIndicatorEl.style.left, "100%",
"The left style of progress indicator element should be 100% at 1000ms");
info("Click to select the steps animation");
await clickOnAnimation(panel, 4);
// Re-get progressIndicatorEl since this element re-create
// in case of select the animation.
progressIndicatorEl = detailsComponent.progressIndicatorEl;
// Use indicateProgess directly from here since
// MouseEvent.clientX may not be able to indicate finely
// in case of the width of header element * xPositionRate has a fraction.
detailsComponent.indicateProgress(499);
is(progressIndicatorEl.style.left, "0%",
"The left style of progress indicator element should be 0% at 0ms");
detailsComponent.indicateProgress(499);
is(progressIndicatorEl.style.left, "0%",
"The left style of progress indicator element should be 0% at 499ms");
detailsComponent.indicateProgress(500);
is(progressIndicatorEl.style.left, "50%",
"The left style of progress indicator element should be 50% at 500ms");
detailsComponent.indicateProgress(999);
is(progressIndicatorEl.style.left, "50%",
"The left style of progress indicator element should be 50% at 999ms");
await clickOnTimelineHeader(panel, 1);
is(progressIndicatorEl.style.left, "100%",
"The left style of progress indicator element should be 100% at 1000ms");
info("Change the playback rate");
await changeTimelinePlaybackRate(panel, 2);
await clickOnAnimation(panel, 0);
progressIndicatorEl = detailsComponent.progressIndicatorEl;
await clickOnTimelineHeader(panel, 0);
is(progressIndicatorEl.style.left, "0%",
"The left style of progress indicator element should be 0% "
+ "at 0ms and playback rate 2");
detailsComponent.indicateProgress(250);
is(progressIndicatorEl.style.left, "50%",
"The left style of progress indicator element should be 50% "
+ "at 250ms and playback rate 2");
detailsComponent.indicateProgress(500);
is(progressIndicatorEl.style.left, "100%",
"The left style of progress indicator element should be 100% "
+ "at 500ms and playback rate 2");
info("Check the progress indicator position after select another animation");
await changeTimelinePlaybackRate(panel, 1);
await clickOnTimelineHeader(panel, 0.5);
const originalIndicatorPosition = progressIndicatorEl.style.left;
await clickOnAnimation(panel, 1);
is(progressIndicatorEl.style.left, originalIndicatorPosition,
"The animation time should be continued even if another animation selects");
});
function approximate(percentageString1, percentageString2, message) {
const val1 = Math.round(parseFloat(percentageString1));
const val2 = Math.round(parseFloat(percentageString2));
is(val1, val2, message);
}

View file

@ -1,45 +0,0 @@
/* 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(async function() {
await addTab(URL_ROOT + "doc_simple_animation.html");
const {panel} = await openAnimationInspector();
const timeline = panel.animationsTimelineComponent;
const 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");
const animation0 = await 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");
const animation1 = await 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");
ok(!isTimeBlockSelected(timeline, 0),
"The first time block has been unselected");
info("Click again on the first animation and check if it unselects");
await clickOnAnimation(panel, 0);
ok(isTimeBlockSelected(timeline, 0),
"The time block has the right selected class again");
ok(!isTimeBlockSelected(timeline, 1),
"The second time block has been unselected");
});
function isTimeBlockSelected(timeline, index) {
const animation = timeline.rootWrapperEl.querySelectorAll(".animation")[index];
return animation.classList.contains("selected");
}

View file

@ -1,47 +0,0 @@
/* 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(async function() {
await addTab(URL_ROOT + "doc_simple_animation.html");
const {panel, controller} = await openAnimationInspector();
ok(controller.documentCurrentTime, "The documentCurrentTime getter exists");
checkDocumentTimeIsCorrect(controller);
const time1 = controller.documentCurrentTime;
await startNewAnimation(controller, panel);
checkDocumentTimeIsCorrect(controller);
const time2 = controller.documentCurrentTime;
ok(time2 > time1, "The new documentCurrentTime is higher than the old one");
});
function checkDocumentTimeIsCorrect(controller) {
let time = 0;
for (const {state} of controller.animationPlayers) {
time = Math.max(time, state.documentCurrentTime);
}
is(controller.documentCurrentTime, time,
"The documentCurrentTime is correct");
}
async function startNewAnimation(controller, panel) {
info("Add a new animation to the page and check the time again");
const onPlayerAdded = controller.once(controller.PLAYERS_UPDATED_EVENT);
const onRendered = waitForAnimationTimelineRendering(panel);
await executeInContent("devtools:test:setAttribute", {
selector: ".still",
attributeName: "class",
attributeValue: "ball still short"
});
await onPlayerAdded;
await onRendered;
await waitForAllAnimationTargets(panel);
}

View file

@ -1,85 +0,0 @@
/* 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";
// Tests the behavior of animation-detail container.
// We test following cases.
// 1. Existance of animation-detail element.
// 2. Hidden at first if multiple animations were displayed.
// 3. Display after click on an animation.
// 4. Display from first time if displayed animation is only one.
// 5. Close the animation-detail element by clicking on close button.
// 6. Stay selected animation even if refresh all UI.
// 7. Close the animation-detail element again and click selected animation again.
requestLongerTimeout(5);
add_task(async function() {
await addTab(URL_ROOT + "doc_multiple_property_types.html");
const { panel, inspector } = await openAnimationInspector();
const timelineComponent = panel.animationsTimelineComponent;
const animationDetailEl =
timelineComponent.rootWrapperEl.querySelector(".animation-detail");
const splitboxControlledEl =
timelineComponent.rootWrapperEl.querySelector(".controlled");
// 1. Existance of animation-detail element.
ok(animationDetailEl, "The animation-detail element should exist");
// 2. Hidden at first if multiple animations were displayed.
const win = timelineComponent.rootWrapperEl.ownerGlobal;
is(win.getComputedStyle(splitboxControlledEl).display, "none",
"The animation-detail element should be hidden at first "
+ "if multiple animations were displayed");
// 3. Display after click on an animation.
await clickOnAnimation(panel, 0);
isnot(win.getComputedStyle(splitboxControlledEl).display, "none",
"The animation-detail element should be displayed after clicked on an animation");
// 4. Display from first time if displayed animation is only one.
await selectNodeAndWaitForAnimations("#target1", inspector);
ok(animationDetailEl.querySelector(".property"),
"The property in animation-detail element should be displayed");
// 5. Close the animation-detail element by clicking on close button.
const previousHeight = animationDetailEl.offsetHeight;
await clickCloseButtonForDetailPanel(timelineComponent, animationDetailEl);
is(win.getComputedStyle(splitboxControlledEl).display, "none",
"animation-detail element should not display");
// Select another animation.
await selectNodeAndWaitForAnimations("#target2", inspector);
isnot(win.getComputedStyle(splitboxControlledEl).display, "none",
"animation-detail element should display");
is(animationDetailEl.offsetHeight, previousHeight,
"The height of animation-detail should keep the height");
// 6. Stay selected animation even if refresh all UI.
await selectNodeAndWaitForAnimations("#target1", inspector);
await clickTimelineRewindButton(panel);
ok(animationDetailEl.querySelector(".property"),
"The property in animation-detail element should stay as is");
// 7. Close the animation-detail element again and click selected animation again.
await clickCloseButtonForDetailPanel(timelineComponent, animationDetailEl);
await clickOnAnimation(panel, 0);
isnot(win.getComputedStyle(splitboxControlledEl).display, "none",
"animation-detail element should display again");
});
/**
* Click close button for animation-detail panel.
*
* @param {AnimationTimeline} AnimationTimeline component
* @param {DOMNode} animation-detail element
* @return {Promise} which wait for close the detail pane
*/
async function clickCloseButtonForDetailPanel(timeline, element) {
const button = element.querySelector(".animation-detail-header button");
const onclosed = timeline.once("animation-detail-closed");
EventUtils.sendMouseEvent({type: "click"}, button, element.ownerDocument.defaultView);
return onclosed;
}

View file

@ -1,118 +0,0 @@
/* 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(5);
// This is a test for displaying the easing of keyframes.
// Checks easing text which is displayed as popup and
// the path which emphasises by mouseover.
const TEST_CASES = {
"no-easing": {
opacity: {
expectedValues: ["linear"],
}
},
"effect-easing": {
opacity: {
expectedValues: ["linear"],
}
},
"keyframe-easing": {
opacity: {
expectedValues: ["steps(2)"],
}
},
"both-easing": {
opacity: {
expectedValues: ["steps(2)"],
}
},
"many-keyframes": {
backgroundColor: {
selector: "rect",
expectedValues: ["steps(2)", "ease-out"],
noEmphasisPath: true,
},
opacity: {
expectedValues: ["steps(2)", "ease-in", "linear", "ease-out"],
}
},
"css-animations": {
opacity: {
expectedValues: ["ease", "ease"],
}
},
};
add_task(async function() {
await addTab(URL_ROOT + "doc_multiple_easings.html");
const { panel } = await openAnimationInspector();
const timelineComponent = panel.animationsTimelineComponent;
const timeBlocks = getAnimationTimeBlocks(panel);
for (let i = 0; i < timeBlocks.length; i++) {
await clickOnAnimation(panel, i);
const detailComponent = timelineComponent.details;
const detailEl = detailComponent.containerEl;
const state = detailComponent.animation.state;
const testcase = TEST_CASES[state.name];
if (!testcase) {
continue;
}
for (const testProperty in testcase) {
const testIdentity = `"${ testProperty }" of "${ state.name }"`;
info(`Test for ${ testIdentity }`);
const testdata = testcase[testProperty];
const selector = testdata.selector ? testdata.selector : `.${testProperty}`;
const hintEls = detailEl.querySelectorAll(`${ selector }.hint`);
const expectedValues = testdata.expectedValues;
is(hintEls.length, expectedValues.length,
`Length of hints for ${ testIdentity } should be ${expectedValues.length}`);
for (let j = 0; j < hintEls.length; j++) {
const hintEl = hintEls[j];
const expectedValue = expectedValues[j];
info("Test easing text");
const gEl = hintEl.closest("g");
ok(gEl, `<g> element for ${ testIdentity } should exists`);
const titleEl = gEl.querySelector("title");
ok(titleEl, `<title> element for ${ testIdentity } should exists`);
is(titleEl.textContent, expectedValue,
`textContent of <title> for ${ testIdentity } should be ${ expectedValue }`);
info("Test emphasis path");
// Scroll to show the hintEl since the element may be out of displayed area.
hintEl.scrollIntoView(false);
const win = hintEl.ownerGlobal;
// Mouse is over the hintEl. Ideally this would use EventUtils, but mouseover
// on the path element is flaky, so let's trust that hovering works and force
// on a hover class to get the same styles.
hintEl.classList.add("hover");
if (testdata.noEmphasisPath) {
is(win.getComputedStyle(hintEl).strokeOpacity, 0,
`stroke-opacity of hintEl for ${ testIdentity } should be 0 ` +
`even while mouse is over the element`);
} else {
is(win.getComputedStyle(hintEl).strokeOpacity, 1,
`stroke-opacity of hintEl for ${ testIdentity } should be 1 ` +
`while mouse is over the element`);
}
// Mouse out once from hintEl.
hintEl.classList.remove("hover");
is(win.getComputedStyle(hintEl).strokeOpacity, 0,
`stroke-opacity of hintEl for ${ testIdentity } should be 0 ` +
`while mouse is out from the element`);
}
}
}
});

View file

@ -1,38 +0,0 @@
/* 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(async function() {
await addTab(URL_ROOT + "doc_simple_animation.html");
const {inspector, panel, window} = await openAnimationInspector();
const {document} = window;
info("Select node .still and check that the panel is empty");
const stillNode = await getNodeFront(".still", inspector);
await selectNodeAndWaitForAnimations(stillNode, inspector);
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");
const commentNode = await inspector.walker.previousSibling(stillNode);
await selectNodeAndWaitForAnimations(commentNode, inspector);
is(panel.animationsTimelineComponent.animations.length, 0,
"No animation players stored in the timeline component for a text node");
is(panel.animationsTimelineComponent.animationsEl.childNodes.length, 0,
"No animation displayed in the timeline component for a text node");
is(document.querySelector("#error-type").textContent,
ANIMATION_L10N.getStr("panel.invalidElementSelected"),
"The correct error message is displayed");
});

View file

@ -1,74 +0,0 @@
/* 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(async function() {
await addTab(URL_ROOT + "doc_keyframes.html");
const {panel} = await openAnimationInspector();
const timeline = panel.animationsTimelineComponent;
// doc_keyframes.html has only one animation.
// So we don't need to click the animation since already the animation detail shown.
ok(timeline.rootWrapperEl.querySelectorAll(".frames .keyframes").length,
"There are container elements for displaying keyframes");
const data = await getExpectedKeyframesData(timeline.animations[0]);
for (const propertyName in data) {
info("Check the keyframe markers for " + propertyName);
const widthMarkerSelector = ".frame[data-property=" + propertyName + "]";
const markers = timeline.rootWrapperEl.querySelectorAll(widthMarkerSelector);
is(markers.length, data[propertyName].length,
"The right number of keyframes was found for " + propertyName);
const offsets = [...markers].map(m => parseFloat(m.dataset.offset));
const 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");
}
}
});
async 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.
const properties = await animation.getProperties();
const data = {};
for (const expectedProperty of EXPECTED_PROPERTIES) {
data[expectedProperty] = [];
for (const {name, values} of properties) {
if (name !== expectedProperty) {
continue;
}
for (const {offset, value} of values) {
data[expectedProperty].push({offset, value});
}
}
}
return data;
}

View file

@ -1,37 +0,0 @@
/* 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(async function() {
await addTab(URL_ROOT + "doc_negative_animation.html");
const {controller, panel} = await openAnimationInspector();
const timeline = panel.animationsTimelineComponent;
const areTracksReady = () => timeline.animations.every(a => {
return timeline.componentsMap[a.actorID];
});
// We need to wait for all tracks to be ready, cause this is an async part of the init
// of the panel.
while (controller.animationPlayers.length < 3 || !areTracksReady()) {
await waitForAnimationTimelineRendering(panel);
}
// Same for animation targets, they're retrieved asynchronously.
await 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.
const nodeFronts =
new Set(getAnimationTargetNodes(panel).map(n => n.previewer.nodeFront));
is(nodeFronts.size, 3, "The animations are applied to 3 different node fronts");
});

View file

@ -1,23 +0,0 @@
/* 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(async function() {
await addTab("data:text/html;charset=utf-8,welcome to the animation panel");
const {panel, controller} = await openAnimationInspector();
ok(controller,
"The animation controller exists");
ok(controller.animationsFront,
"The animation controller has been initialized");
ok(panel,
"The animation panel exists");
ok(panel.playersEl,
"The animation panel has been initialized");
ok(panel.animationsTimelineComponent,
"The animation panel has been initialized");
});

View file

@ -1,46 +0,0 @@
/* 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(async function() {
await addTab(URL_ROOT + "doc_simple_animation.html");
const {inspector, panel, controller} = await openAnimationInspector();
info("Listen for the players-updated, ui-updated and " +
"inspector-updated events");
const 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");
const node = await getNodeFront(".animated", inspector);
await 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.includes(controller.PLAYERS_UPDATED_EVENT),
"The players-updated event was received");
ok(receivedEvents.includes(panel.UI_UPDATED_EVENT),
"The ui-updated event was received");
});

View file

@ -1,36 +0,0 @@
/* 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(async function() {
await addTab(URL_ROOT + "doc_simple_animation.html");
const {controller, inspector} = await 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.
await selectNodeAndWaitForAnimations(".animated", inspector);
is(controller.animationPlayers.length, 1,
"One AnimationPlayerFront has been created");
info("Selecting a node with mutliple animations");
await selectNodeAndWaitForAnimations(".multi", inspector);
is(controller.animationPlayers.length, 2,
"2 AnimationPlayerFronts have been created");
info("Selecting a node with no animations");
await selectNodeAndWaitForAnimations(".still", inspector);
is(controller.animationPlayers.length, 0,
"There are no more AnimationPlayerFront objects");
});

View file

@ -1,41 +0,0 @@
/* 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(async function() {
await addTab(URL_ROOT + "doc_multiple_animation_types.html");
const {panel} = await openAnimationInspector();
is(panel.animationsTimelineComponent.animations.length, 3,
"Three animations are handled by the timeline after init");
assertAnimationsDisplayed(panel, 3,
"Three animations are displayed after init");
is(
panel.animationsTimelineComponent
.animationsEl
.querySelectorAll(`.animation.${ANIMATION_TYPES.SCRIPT_ANIMATION}`)
.length,
1,
"One script-generated animation is displayed");
is(
panel.animationsTimelineComponent
.animationsEl
.querySelectorAll(`.animation.${ANIMATION_TYPES.CSS_ANIMATION}`)
.length,
1,
"One CSS animation is displayed");
is(
panel.animationsTimelineComponent
.animationsEl
.querySelectorAll(`.animation.${ANIMATION_TYPES.CSS_TRANSITION}`)
.length,
1,
"One CSS transition is displayed");
});

View file

@ -1,33 +0,0 @@
/* 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(async function() {
await addTab(URL_ROOT + "doc_simple_animation.html");
const {inspector, panel} = await openAnimationInspector();
info("Select the simple animated node");
await selectNodeAndWaitForAnimations(".animated", inspector);
const targetNodeComponent = getAnimationTargetNodes(panel)[0];
const {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) {
await targetNodeComponent.once("target-retrieved");
}
is(previewer.el.textContent, "div#.ball.animated",
"The target element's content is correct");
const highlighterEl = previewer.el.querySelector(".node-highlighter");
ok(highlighterEl,
"The icon to highlight the target element in the page exists");
});

View file

@ -1,49 +0,0 @@
/* 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(async function() {
await addTab(URL_ROOT + "doc_pseudo_elements.html");
const {inspector, panel} = await openAnimationInspector();
info("With <body> selected by default check the content of the timeline");
is(getAnimationTimeBlocks(panel).length, 3, "There are 3 animations in the timeline");
const targetNodes = getAnimationTargetNodes(panel);
const getTargetNodeText = index => {
const el = 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");
const bodyContainer = await getContainerForSelector("body", inspector);
const getBodyChildNodeFront = index => {
return bodyContainer.elt.children[1].childNodes[index].container.node;
};
const beforeNode = getBodyChildNodeFront(0);
const afterNode = getBodyChildNodeFront(1);
info("Select the ::before pseudo-element in the inspector");
await selectNodeAndWaitForAnimations(beforeNode, inspector);
is(getAnimationTimeBlocks(panel).length, 1, "There is 1 animation in the timeline");
is(getAnimationTargetNodes(panel)[0].previewer.nodeFront,
inspector.selection.nodeFront,
"The right node front is displayed in the timeline");
info("Select the ::after pseudo-element in the inspector");
await selectNodeAndWaitForAnimations(afterNode, inspector);
is(getAnimationTimeBlocks(panel).length, 1, "There is 1 animation in the timeline");
is(getAnimationTargetNodes(panel)[0].previewer.nodeFront,
inspector.selection.nodeFront,
"The right node front is displayed in the timeline");
});

View file

@ -1,50 +0,0 @@
/* 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(async function() {
await addTab(URL_ROOT + "doc_simple_animation.html");
const {inspector, panel} = await openAnimationInspector();
info("Select a non animated node");
await selectNodeAndWaitForAnimations(".still", inspector);
assertAnimationsDisplayed(panel, 0);
info("Start an animation on the node");
const onRendered = waitForAnimationTimelineRendering(panel);
await changeElementAndWait({
selector: ".still",
attributeName: "class",
attributeValue: "ball animated"
}, panel, inspector);
await onRendered;
await waitForAllAnimationTargets(panel);
assertAnimationsDisplayed(panel, 1);
info("Remove the animation class on the node");
await changeElementAndWait({
selector: ".ball.animated",
attributeName: "class",
attributeValue: "ball still"
}, panel, inspector);
assertAnimationsDisplayed(panel, 0);
});
async function changeElementAndWait(options, panel, inspector) {
const onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT);
const onInspectorUpdated = inspector.once("inspector-updated");
await executeInContent("devtools:test:setAttribute", options);
await promise.all([onInspectorUpdated, onPanelUpdated]);
}

View file

@ -1,49 +0,0 @@
/* 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(async function() {
await addTab(URL_ROOT + "doc_simple_animation.html");
const {inspector, panel} = await openAnimationInspector();
await testRefreshOnRemove(inspector, panel);
});
async function testRefreshOnRemove(inspector, panel) {
info("Select a animated node");
await 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");
await executeInContent("devtools:test:setAttribute", {
selector: ".animated",
attributeName: "class",
attributeValue: "ball still test-node"
});
await 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);
await executeInContent("devtools:test:setAttribute", {
selector: ".test-node",
attributeName: "class",
attributeValue: "ball short test-node"
});
await onPanelUpdated;
await waitForAnimationSelecting(panel);
assertAnimationsDisplayed(panel, 1);
}

View file

@ -1,57 +0,0 @@
/* 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(async function() {
info("Switch to 2 pane inspector to see if the panel only refreshes when visible");
await pushPref("devtools.inspector.three-pane-enabled", false);
await addTab(URL_ROOT + "doc_simple_animation.html");
const {inspector, panel} = await openAnimationInspector();
await testRefresh(inspector, panel);
});
async function testRefresh(inspector, panel) {
info("Select a non animated node");
await selectNodeAndWaitForAnimations(".still", inspector);
info("Switch to the rule-view panel");
inspector.sidebar.select("ruleview");
info("Select the animated node now");
await selectNode(".animated", inspector);
assertAnimationsDisplayed(panel, 0,
"The panel doesn't show the animation data while inactive");
info("Switch to the animation panel");
const onRendered = waitForAnimationTimelineRendering(panel);
inspector.sidebar.select("animationinspector");
await panel.once(panel.UI_UPDATED_EVENT);
await onRendered;
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");
await selectNode(".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");
await panel.once(panel.UI_UPDATED_EVENT);
assertAnimationsDisplayed(panel, 0,
"The panel is now empty after refreshing");
}

View file

@ -1,56 +0,0 @@
/* 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 refresh animation UI while the panel is hidden.
const EXPECTED_GRAPH_PATH_SEGMENTS = [{ x: 0, y: 0 },
{ x: 49999, y: 0.0 },
{ x: 50000, y: 0.5 },
{ x: 99999, y: 0.5 },
{ x: 100000, y: 0 }];
add_task(async function() {
info("Switch to 2 pane inspector to see if the panel only refreshes when visible");
await pushPref("devtools.inspector.three-pane-enabled", false);
info("Open animation inspector once so that activate animation mutations listener");
await addTab("data:text/html;charset=utf8,<div id='target'>test</div>");
const { controller, inspector, panel } = await openAnimationInspector();
info("Select other tool to hide animation inspector");
await inspector.sidebar.select("ruleview");
// Count players-updated event in controller.
let updatedEventCount = 0;
controller.on("players-updated", () => {
updatedEventCount += 1;
});
info("Make animation by eval in content");
await evalInDebuggee(`document.querySelector('#target').animate(
{ transform: 'translate(100px)' },
{ duration: 100000, easing: 'steps(2)' });`);
info("Wait for animation mutations event");
await controller.animationsFront.once("mutations");
info("Check players-updated events count");
is(updatedEventCount, 0, "players-updated event shoud not be fired");
info("Re-select animation inspector and check the UI");
await inspector.sidebar.select("animationinspector");
await waitForAnimationTimelineRendering(panel);
const timeBlocks = getAnimationTimeBlocks(panel);
is(timeBlocks.length, 1, "One animation should display");
const timeBlock = timeBlocks[0];
const state = timeBlock.animation.state;
const effectEl = timeBlock.containerEl.querySelector(".effect-easing");
ok(effectEl, "<g> element for effect easing should exist");
const pathEl = effectEl.querySelector("path");
ok(pathEl, "<path> element for effect easing should exist");
assertPathSegments(pathEl, state.duration, false, EXPECTED_GRAPH_PATH_SEGMENTS);
});

View file

@ -1,57 +0,0 @@
/* 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(async function() {
await addTab(URL_ROOT + "doc_simple_animation.html");
const {inspector, panel} = await openAnimationInspector();
const timeline = panel.animationsTimelineComponent;
info("Select a test node we know has an animation running on the compositor");
await selectNodeAndWaitForAnimations(".compositor-all", 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");
await 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");
await 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) {
const el = animationEl.querySelector(".name");
const tooltip = el.getAttribute("title");
return tooltip.includes(expected);
}

View file

@ -1,23 +0,0 @@
/* 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(async function() {
await addTab(URL_ROOT + "doc_simple_animation.html");
const {inspector, panel, controller} = await openAnimationInspector();
const timeline = panel.animationsTimelineComponent;
info("Selecting the test animated node again");
await selectNodeAndWaitForAnimations(".multi", inspector);
is(controller.animationPlayers.length,
timeline.animationsEl.querySelectorAll(".animation").length,
"As many timeline elements were created as there are playerFronts");
});

View file

@ -1,21 +0,0 @@
/* 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(async function() {
await addTab(URL_ROOT + "doc_simple_animation.html");
const {inspector, panel} = await openAnimationInspector();
info("Select node .animated and check that the panel is not empty");
const node = await getNodeFront(".animated", inspector);
await selectNodeAndWaitForAnimations(node, inspector);
assertAnimationsDisplayed(panel, 1);
});

View file

@ -1,43 +0,0 @@
/* 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 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(async function() {
await addTab(URL_ROOT + "doc_simple_animation.html");
const {panel, inspector, window, controller} = await openAnimationInspector();
const {toggleAllButtonEl} = panel;
// select a node without animations
await 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);
await 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);
await onToggled;
ok(!toggleAllButtonEl.classList.contains("paused"),
"The toggle all button is in its playing state again");
});

View file

@ -1,43 +0,0 @@
/* 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(async function() {
requestLongerTimeout(2);
await addTab(URL_ROOT + "doc_simple_animation.html");
const {panel, window} = await openAnimationInspector();
const {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 render animation timeline
EventUtils.sendKey("SPACE", window);
await waitForAnimationTimelineRendering(panel);
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 render animation timeline
EventUtils.sendKey("SPACE", window);
await waitForAnimationTimelineRendering(panel);
ok(!playTimelineButtonEl.classList.contains("paused"),
"The play/resume button is in its play state again");
});

View file

@ -1,180 +0,0 @@
/* 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 summary graph for animations that have multiple easing.
// There are two ways that we can set the easing for animations.
// One is effect easing, another one is keyframe easing for properties.
// The summary graph shows effect easing as dashed line if the easing is not 'linear'.
// If 'linear', does not show.
// Also, shows graph which combine the keyframe easings of all properties.
const TEST_CASES = {
"no-easing": {
expectedKeyframeEasingGraphs: [
[
{ x: 0, y: 0 },
{ x: 25000, y: 0.25 },
{ x: 50000, y: 0.5 },
{ x: 75000, y: 0.75 },
{ x: 99000, y: 0.99 },
{ x: 100000, y: 0 },
]
]
},
"effect-easing": {
expectedEffectEasingGraph: [
{ x: 0, y: 0 },
{ x: 19999, y: 0.0 },
{ x: 20000, y: 0.25 },
{ x: 39999, y: 0.25 },
{ x: 40000, y: 0.5 },
{ x: 59999, y: 0.5 },
{ x: 60000, y: 0.75 },
{ x: 79999, y: 0.75 },
{ x: 80000, y: 1 },
{ x: 99999, y: 1 },
{ x: 100000, y: 0 },
],
expectedKeyframeEasingGraphs: [
[
{ x: 0, y: 0 },
{ x: 19999, y: 0.0 },
{ x: 20000, y: 0.25 },
{ x: 39999, y: 0.25 },
{ x: 40000, y: 0.5 },
{ x: 59999, y: 0.5 },
{ x: 60000, y: 0.75 },
{ x: 79999, y: 0.75 },
{ x: 80000, y: 1 },
{ x: 99999, y: 1 },
{ x: 100000, y: 0 },
]
]
},
"keyframe-easing": {
expectedKeyframeEasingGraphs: [
[
{ x: 0, y: 0 },
{ x: 49999, y: 0.0 },
{ x: 50000, y: 0.5 },
{ x: 99999, y: 0.5 },
{ x: 100000, y: 0 },
]
]
},
"both-easing": {
expectedEffectEasingGraph: [
{ x: 0, y: 0 },
{ x: 9999, y: 0.0 },
{ x: 10000, y: 0.1 },
{ x: 19999, y: 0.1 },
{ x: 20000, y: 0.2 },
{ x: 29999, y: 0.2 },
{ x: 30000, y: 0.3 },
{ x: 39999, y: 0.3 },
{ x: 40000, y: 0.4 },
{ x: 49999, y: 0.4 },
{ x: 50000, y: 0.5 },
{ x: 59999, y: 0.5 },
{ x: 60000, y: 0.6 },
{ x: 69999, y: 0.6 },
{ x: 70000, y: 0.7 },
{ x: 79999, y: 0.7 },
{ x: 80000, y: 0.8 },
{ x: 89999, y: 0.8 },
{ x: 90000, y: 0.9 },
{ x: 99999, y: 0.9 },
{ x: 100000, y: 0 },
],
expectedKeyframeEasingGraphs: [
[
// KeyframeEffect::GetProperties returns sorted properties by the name.
// Therefor, the test html, the 'marginLeft' is upper than 'opacity'.
{ x: 0, y: 0 },
{ x: 19999, y: 0.0 },
{ x: 20000, y: 0.2 },
{ x: 39999, y: 0.2 },
{ x: 40000, y: 0.4 },
{ x: 59999, y: 0.4 },
{ x: 60000, y: 0.6 },
{ x: 79999, y: 0.6 },
{ x: 80000, y: 0.8 },
{ x: 99999, y: 0.8 },
{ x: 100000, y: 0 },
],
[
{ x: 0, y: 0 },
{ x: 49999, y: 0.0 },
{ x: 50000, y: 0.5 },
{ x: 99999, y: 0.5 },
{ x: 100000, y: 0 },
]
]
},
"narrow-keyframes": {
expectedKeyframeEasingGraphs: [
[
{ x: 0, y: 0 },
{ x: 10000, y: 0.1 },
{ x: 11000, y: 0.1 },
{ x: 11500, y: 0.1 },
{ x: 12999, y: 0.1 },
{ x: 13000, y: 0.13 },
{ x: 13500, y: 0.135 },
{ x: 14000, y: 0.14 },
]
]
},
"duplicate-offsets": {
expectedKeyframeEasingGraphs: [
[
{ x: 0, y: 0 },
{ x: 25000, y: 0.25 },
{ x: 50000, y: 0.5 },
{ x: 75000, y: 0.5 },
{ x: 99999, y: 0.5 },
]
]
},
};
add_task(async function() {
await addTab(URL_ROOT + "doc_multiple_easings.html");
const { panel } = await openAnimationInspector();
getAnimationTimeBlocks(panel).forEach(timeBlock => {
const state = timeBlock.animation.state;
const testcase = TEST_CASES[state.name];
if (!testcase) {
return;
}
info(`Test effect easing graph of ${ state.name }`);
const effectEl = timeBlock.containerEl.querySelector(".effect-easing");
if (testcase.expectedEffectEasingGraph) {
ok(effectEl, "<g> element for effect easing should exist");
const pathEl = effectEl.querySelector("path");
ok(pathEl, "<path> element for effect easing should exist");
assertPathSegments(pathEl, state.duration, false,
testcase.expectedEffectEasingGraph);
} else {
ok(!effectEl, "<g> element for effect easing should not exist");
}
info(`Test keyframes easing graph of ${ state.name }`);
const keyframeEls = timeBlock.containerEl.querySelectorAll(".keyframes-easing");
const expectedKeyframeEasingGraphs = testcase.expectedKeyframeEasingGraphs;
is(keyframeEls.length, expectedKeyframeEasingGraphs.length,
`There should be ${ expectedKeyframeEasingGraphs.length } <g> elements `
+ `for keyframe easing`);
expectedKeyframeEasingGraphs.forEach((expectedValues, index) => {
const pathEl = keyframeEls[index].querySelector("path");
ok(pathEl, "<path> element for keyframe easing should exist");
assertPathSegments(pathEl, state.duration, false, expectedValues);
});
});
});

View file

@ -1,68 +0,0 @@
/* 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(async function() {
await addTab(URL_ROOT + "doc_simple_animation.html");
const {toolbox, inspector, panel} = await openAnimationInspector();
info("Select the simple animated node");
await selectNodeAndWaitForAnimations(".animated", inspector);
let targets = getAnimationTargetNodes(panel);
// Arbitrary select the first one
let targetNodeComponent = targets[0];
info("Retrieve the part of the widget that highlights the node on hover");
const highlightingEl = targetNodeComponent.previewer.previewEl;
info("Listen to node-highlight event and mouse over the widget");
const onHighlight = toolbox.once("node-highlight");
EventUtils.synthesizeMouse(highlightingEl, 10, 5, {type: "mouseover"},
highlightingEl.ownerDocument.defaultView);
const nodeFront = await 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");
await selectNodeAndWaitForAnimations("body", inspector);
targets = getAnimationTargetNodes(panel);
targetNodeComponent = targets[0];
info("Click on the first animated node component and wait for the " +
"selection to change");
const onSelection = inspector.selection.once("new-node-front");
const onRendered = waitForAnimationTimelineRendering(panel);
const nodeEl = targetNodeComponent.previewer.previewEl;
EventUtils.sendMouseEvent({type: "click"}, nodeEl,
nodeEl.ownerDocument.defaultView);
await onSelection;
is(inspector.selection.nodeFront, targetNodeComponent.previewer.nodeFront,
"The selected node is the one stored on the animation widget");
await onRendered;
});

View file

@ -1,54 +0,0 @@
/* 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(async function() {
await addTab(URL_ROOT + "doc_simple_animation.html");
const {panel} = await openAnimationInspector();
const targets = getAnimationTargetNodes(panel);
info("Click on the highlighter icon for the first animated node");
const domNodePreview1 = targets[0].previewer;
await lockHighlighterOn(domNodePreview1);
ok(domNodePreview1.highlightNodeEl.classList.contains("selected"),
"The highlighter icon is selected");
info("Click on the highlighter icon for the second animated node");
const domNodePreview2 = targets[1].previewer;
await 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");
await unlockHighlighterOn(domNodePreview2);
ok(!domNodePreview2.highlightNodeEl.classList.contains("selected"),
"The highlighter icon for the second node is unselected");
});
async function lockHighlighterOn(domNodePreview) {
const onLocked = domNodePreview.once("target-highlighter-locked");
clickOnHighlighterIcon(domNodePreview);
await onLocked;
}
async function unlockHighlighterOn(domNodePreview) {
const onUnlocked = domNodePreview.once("target-highlighter-unlocked");
clickOnHighlighterIcon(domNodePreview);
await onUnlocked;
}
function clickOnHighlighterIcon(domNodePreview) {
const lockEl = domNodePreview.highlightNodeEl;
EventUtils.sendMouseEvent({type: "click"}, lockEl,
lockEl.ownerDocument.defaultView);
}

View file

@ -1,68 +0,0 @@
/* 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 re-used animation element since we re-use existent animation element
// for the render performance.
add_task(async function() {
await addTab(URL_ROOT + "doc_add_animation.html");
const {panel, controller} = await openAnimationInspector();
const timelineComponent = panel.animationsTimelineComponent;
// Add new animation which has delay and endDelay.
await startNewAnimation(controller, panel, "#target2");
const previousAnimationEl =
timelineComponent.animationsEl.querySelector(".animation:nth-child(2)");
const previousSummaryGraphEl = previousAnimationEl.querySelector(".summary");
const previousDelayEl = previousAnimationEl.querySelector(".delay");
const previousEndDelayEl = previousAnimationEl.querySelector(".end-delay");
const previousSummaryGraphWidth = previousSummaryGraphEl.viewBox.baseVal.width;
const previousDelayBounds = previousDelayEl.getBoundingClientRect();
const previousEndDelayBounds = previousEndDelayEl.getBoundingClientRect();
// Add another animation.
await startNewAnimation(controller, panel, "#target3");
const currentAnimationEl =
timelineComponent.animationsEl.querySelector(".animation:nth-child(2)");
const currentSummaryGraphEl = currentAnimationEl.querySelector(".summary");
const currentDelayEl = currentAnimationEl.querySelector(".delay");
const currentEndDelayEl = currentAnimationEl.querySelector(".end-delay");
const currentSummaryGraphWidth = currentSummaryGraphEl.viewBox.baseVal.width;
const currentDelayBounds = currentDelayEl.getBoundingClientRect();
const currentEndDelayBounds = currentEndDelayEl.getBoundingClientRect();
is(previousAnimationEl, currentAnimationEl, ".animation element should be reused");
is(previousSummaryGraphEl, currentSummaryGraphEl, ".summary element should be reused");
is(previousDelayEl, currentDelayEl, ".delay element should be reused");
is(previousEndDelayEl, currentEndDelayEl, ".end-delay element should be reused");
ok(currentSummaryGraphWidth > previousSummaryGraphWidth,
"Reused .summary element viewBox width should be longer");
ok(currentDelayBounds.left < previousDelayBounds.left,
"Reused .delay element should move to the left");
ok(currentDelayBounds.width < previousDelayBounds.width,
"Reused .delay element should be shorter");
ok(currentEndDelayBounds.left < previousEndDelayBounds.left,
"Reused .end-delay element should move to the left");
ok(currentEndDelayBounds.width < previousEndDelayBounds.width,
"Reused .end-delay element should be shorter");
});
async function startNewAnimation(controller, panel, selector) {
info("Add a new animation to the page and check the time again");
const onPlayerAdded = controller.once(controller.PLAYERS_UPDATED_EVENT);
const onRendered = waitForAnimationTimelineRendering(panel);
await executeInContent("devtools:test:setAttribute", {
selector: selector,
attributeName: "class",
attributeValue: "animation"
});
await onPlayerAdded;
await onRendered;
await waitForAllAnimationTargets(panel);
}

View file

@ -1,49 +0,0 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/* eslint-disable mozilla/no-arbitrary-setTimeout */
"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(async function() {
await addTab(URL_ROOT + "doc_simple_animation.html");
const {panel} = await openAnimationInspector();
const 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");
await isCurrentTimeLabelChanging(panel, true);
info("Pause the animations and check that the time stops changing");
await clickTimelinePlayPauseButton(panel);
await isCurrentTimeLabelChanging(panel, false);
info("Rewind the animations and check that the time stops changing");
await clickTimelineRewindButton(panel);
await isCurrentTimeLabelChanging(panel, false);
is(label.textContent, "00:00.000");
});
async function isCurrentTimeLabelChanging(panel, isChanging) {
const label = panel.timelineCurrentTimeEl;
const time1 = label.textContent;
await new Promise(r => setTimeout(r, 200));
const time2 = label.textContent;
if (isChanging) {
ok(time1 !== time2, "The text displayed in the label changes with time");
} else {
is(time1, time2, "The text displayed in the label doesn't change");
}
}

View file

@ -1,60 +0,0 @@
/* 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/inspector/animation-old/utils");
// Should be kept in sync with TIME_GRADUATION_MIN_SPACING in
// animation-timeline.js
const TIME_GRADUATION_MIN_SPACING = 40;
add_task(async function() {
await pushPref("devtools.inspector.three-pane-enabled", false);
await 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.
await pushPref("devtools.toolsidebar-width.inspector", 350);
const {panel} = await openAnimationInspector();
const timeline = panel.animationsTimelineComponent;
const headerEl = timeline.timeHeaderEl;
info("Find out how many time graduations should there be");
const width = headerEl.offsetWidth;
const animationDuration = TimeScale.maxEndTime - TimeScale.minStartTime;
const 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.
const interval = findOptimalTimeInterval(minTimeInterval);
const 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) => {
const left = parseFloat(tick.style.left);
const 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.
const formattedTime = TimeScale.formatTime(
TimeScale.distanceToRelativeTime(expectedPos, width));
is(tick.textContent, formattedTime,
`Graduation ${i} has the right text content`);
});
});

View file

@ -1,93 +0,0 @@
/* 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 the iteration start is displayed correctly in time blocks.
add_task(async function() {
await addTab(URL_ROOT + "doc_script_animation.html");
const {panel} = await openAnimationInspector();
const timelineComponent = panel.animationsTimelineComponent;
const timeBlockComponents = getAnimationTimeBlocks(panel);
const detailsComponent = timelineComponent.details;
for (let i = 0; i < timeBlockComponents.length; i++) {
info(`Expand time block ${i} so its keyframes are visible`);
await clickOnAnimation(panel, i);
info(`Check the state of time block ${i}`);
const {containerEl, animation: {state}} = timeBlockComponents[i];
checkAnimationTooltip(containerEl, state);
checkProgressAtStartingTime(containerEl, state);
// Get the first set of keyframes (there's only one animated property
// anyway), and the first frame element from there, we're only interested in
// its offset.
const keyframeComponent = detailsComponent.keyframeComponents[0];
const frameEl = keyframeComponent.keyframesEl.querySelector(".frame");
checkKeyframeOffset(containerEl, frameEl, state);
}
});
function checkAnimationTooltip(el, {iterationStart, duration}) {
info("Check an animation's iterationStart data in its tooltip");
const title = el.querySelector(".name").getAttribute("title");
const iterationStartTime = iterationStart * duration / 1000;
const iterationStartTimeString = iterationStartTime.toLocaleString(undefined, {
maximumFractionDigits: 2,
minimumFractionDigits: 2
}).replace(".", "\\.");
const iterationStartString = iterationStart.toString().replace(".", "\\.");
const regex = new RegExp("Iteration start: " + iterationStartString +
" \\(" + iterationStartTimeString + "s\\)");
ok(title.match(regex), "The tooltip shows the expected iteration start");
}
function checkProgressAtStartingTime(el, { delay, iterationStart }) {
info("Check the progress of starting time");
const groupEls = el.querySelectorAll("svg g");
groupEls.forEach(groupEl => {
const pathEl = groupEl.querySelector(".iteration-path");
const pathSegList = pathEl.pathSegList;
const pathSeg = pathSegList.getItem(1);
const progress = pathSeg.y;
is(progress, iterationStart % 1,
`The progress at starting point should be ${ iterationStart % 1 }`);
if (delay) {
const delayPathEl = groupEl.querySelector(".delay-path");
const delayPathSegList = delayPathEl.pathSegList;
const delayStartingPathSeg = delayPathSegList.getItem(1);
const delayEndingPathSeg =
delayPathSegList.getItem(delayPathSegList.numberOfItems - 2);
const startingX = 0;
const endingX = delay;
is(delayStartingPathSeg.x, startingX,
`The x of starting point should be ${ startingX }`);
is(delayStartingPathSeg.y, progress,
"The y of starting point should be same to starting point of iteration-path "
+ progress);
is(delayEndingPathSeg.x, endingX,
`The x of ending point should be ${ endingX }`);
is(delayStartingPathSeg.y, progress,
"The y of ending point should be same to starting point of iteration-path "
+ progress);
}
});
}
function checkKeyframeOffset(timeBlockEl, frameEl, {iterationStart}) {
info("Check that the first keyframe is offset correctly");
const start = getKeyframeOffset(frameEl);
is(start, 0, "The frame offset for iteration start");
}
function getKeyframeOffset(el) {
return parseFloat(/(\d+)%/.exec(el.style.left)[1]);
}

View file

@ -1,34 +0,0 @@
/* 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 contains a pause button and that this pause button can
// be clicked. Check that when it is, the button changes state and the scrubber stops and
// resumes.
add_task(async function() {
await addTab(URL_ROOT + "doc_simple_animation.html");
const {panel} = await openAnimationInspector();
const btn = panel.playTimelineButtonEl;
ok(btn, "The play/pause button exists");
ok(!btn.classList.contains("paused"), "The play/pause button is in its playing state");
info("Click on the button to pause all timeline animations");
await clickTimelinePlayPauseButton(panel);
ok(btn.classList.contains("paused"), "The play/pause button is in its paused state");
await assertScrubberMoving(panel, false);
info("Click again on the button to play all timeline animations");
await clickTimelinePlayPauseButton(panel);
ok(!btn.classList.contains("paused"),
"The play/pause button is in its playing state again");
await assertScrubberMoving(panel, true);
});

View file

@ -1,57 +0,0 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/* eslint-disable mozilla/no-arbitrary-setTimeout */
"use strict";
requestLongerTimeout(2);
// Checks that the play/pause button goes to the right state when the scrubber has reached
// the end of the timeline but there are infinite animations playing.
add_task(async function() {
// TODO see if this is needed?
// let timerPrecision = Preferences.get("privacy.reduceTimerPrecision");
// Preferences.set("privacy.reduceTimerPrecision", false);
// registerCleanupFunction(function () {
// Preferences.set("privacy.reduceTimerPrecision", timerPrecision);
// });
await addTab(URL_ROOT + "doc_simple_animation.html");
const {panel, inspector} = await openAnimationInspector();
const timeline = panel.animationsTimelineComponent;
const btn = panel.playTimelineButtonEl;
info("Select an infinite animation and wait for the scrubber to reach the end");
await selectNodeAndWaitForAnimations(".multi", inspector);
await waitForOutOfBoundScrubber(timeline);
ok(!btn.classList.contains("paused"),
"The button is in its playing state still, animations are infinite.");
await assertScrubberMoving(panel, true);
info("Click on the button after the scrubber has moved out of bounds");
await clickTimelinePlayPauseButton(panel);
ok(btn.classList.contains("paused"),
"The button can be paused after the scrubber has moved out of bounds");
await assertScrubberMoving(panel, false);
});
function waitForOutOfBoundScrubber({win, scrubberEl}) {
return new Promise(resolve => {
function check() {
const pos = scrubberEl.getBoxQuads()[0].getBounds().right;
const width = win.document.documentElement.offsetWidth;
if (pos >= width) {
setTimeout(resolve, 50);
} else {
setTimeout(check, 50);
}
}
check();
});
}

View file

@ -1,59 +0,0 @@
/* 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);
// Also checks that the button goes to the right state when the scrubber has
// reached the end of the timeline: continues to be in playing mode for infinite
// animations, goes to paused mode otherwise.
// And test that clicking the button once the scrubber has reached the end of
// the timeline does the right thing.
add_task(async function() {
await addTab(URL_ROOT + "doc_simple_animation.html");
const {panel, inspector} = await openAnimationInspector();
const btn = panel.playTimelineButtonEl;
// For a finite animation, once the scrubber reaches the end of the timeline, the pause
// button should go back to paused mode.
info("Select a finite animation and wait for the animation to complete");
await selectNodeAndWaitForAnimations(".negative-delay", inspector);
await reloadTab(inspector);
if (!btn.classList.contains("paused")) {
await waitForButtonPaused(btn);
}
ok(btn.classList.contains("paused"),
"The button is in paused state once finite animations are done");
await assertScrubberMoving(panel, false);
info("Click again on the button to play the animation from the start again");
await clickTimelinePlayPauseButton(panel);
ok(!btn.classList.contains("paused"),
"Clicking the button once finite animations are done should restart them");
await assertScrubberMoving(panel, true);
});
function waitForButtonPaused(btn) {
return new Promise(resolve => {
const observer = new btn.ownerDocument.defaultView.MutationObserver(mutations => {
for (const mutation of mutations) {
if (mutation.type === "attributes" &&
mutation.attributeName === "class" &&
!mutation.oldValue.includes("paused") &&
btn.classList.contains("paused")) {
observer.disconnect();
resolve();
}
}
});
observer.observe(btn, { attributes: true, attributeOldValue: true });
});
}

View file

@ -1,56 +0,0 @@
/* 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 contains a playback rate selector UI and that
// it can be used to change the playback rate of animations in the timeline.
// Also check that it displays the rate of the current animations in case they
// all have the same rate, or that it displays the empty value in case they
// have mixed rates.
add_task(async function() {
await addTab(URL_ROOT + "doc_simple_animation.html");
const {panel, controller, inspector, toolbox} = await openAnimationInspector();
// In this test, we disable the highlighter on purpose because of the way
// events are simulated to select an option in the playbackRate <select>.
// Indeed, this may cause mousemove events to be triggered on the nodes that
// are underneath the <select>, and these are AnimationTargetNode instances.
// Simulating mouse events on them will cause the highlighter to emit requests
// and this might cause the test to fail if they happen after it has ended.
disableHighlighter(toolbox);
const select = panel.rateSelectorEl.firstChild;
ok(select, "The rate selector exists");
info("Change all of the current animations' rates to 0.5");
await changeTimelinePlaybackRate(panel, .5);
checkAllAnimationsRatesChanged(controller, select, .5);
info("Select just one animated node and change its rate only");
await selectNodeAndWaitForAnimations(".animated", inspector);
await changeTimelinePlaybackRate(panel, 2);
checkAllAnimationsRatesChanged(controller, select, 2);
info("Select the <body> again, it should now have mixed-rates animations");
await selectNodeAndWaitForAnimations("body", inspector);
is(select.value, "", "The selected rate is empty");
info("Change the rate for these mixed-rate animations");
await changeTimelinePlaybackRate(panel, 1);
checkAllAnimationsRatesChanged(controller, select, 1);
});
function checkAllAnimationsRatesChanged({animationPlayers}, select, rate) {
ok(animationPlayers.every(({state}) => state.playbackRate === rate),
"All animations' rates have been set to " + rate);
is(select.value, rate, "The right value is displayed in the select");
}

View file

@ -1,52 +0,0 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/* eslint-disable mozilla/no-arbitrary-setTimeout */
"use strict";
requestLongerTimeout(2);
// Check that the timeline toolbar contains a rewind button and that it can be
// clicked. Check that when it is, the current animations displayed in the
// timeline get their playstates changed to paused, and their currentTimes
// reset to 0, and that the scrubber stops moving and is positioned to the
// start.
add_task(async function() {
await addTab(URL_ROOT + "doc_simple_animation.html");
const {panel, controller} = await openAnimationInspector();
const players = controller.animationPlayers;
const btn = panel.rewindTimelineButtonEl;
ok(btn, "The rewind button exists");
info("Click on the button to rewind all timeline animations");
await clickTimelineRewindButton(panel);
info("Check that the scrubber has stopped moving");
await assertScrubberMoving(panel, false);
ok(players.every(({state}) => state.currentTime === 0),
"All animations' currentTimes have been set to 0");
ok(players.every(({state}) => state.playState === "paused"),
"All animations have been paused");
info("Play the animations again");
await clickTimelinePlayPauseButton(panel);
info("And pause them after a short while");
await new Promise(r => setTimeout(r, 200));
info("Check that rewinding when animations are paused works too");
await clickTimelineRewindButton(panel);
info("Check that the scrubber has stopped moving");
await assertScrubberMoving(panel, false);
ok(players.every(({state}) => state.currentTime === 0),
"All animations' currentTimes have been set to 0");
ok(players.every(({state}) => state.playState === "paused"),
"All animations have been paused");
});

View file

@ -1,20 +0,0 @@
/* 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 does have a scrubber element.
add_task(async function() {
await addTab(URL_ROOT + "doc_simple_animation.html");
const {panel} = await openAnimationInspector();
const timeline = panel.animationsTimelineComponent;
const scrubberEl = timeline.scrubberEl;
ok(scrubberEl, "The scrubber element exists");
ok(scrubberEl.classList.contains("scrubber"), "It has the right classname");
});

View file

@ -1,75 +0,0 @@
/* 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 scrubber in the timeline can be moved by clicking & dragging
// in the header area.
// Also check that doing so changes the timeline's play/pause button to paused
// state.
// Finally, also check that the scrubber can be moved using the scrubber handle.
add_task(async function() {
await addTab(URL_ROOT + "doc_simple_animation.html");
const {panel} = await openAnimationInspector();
const timeline = panel.animationsTimelineComponent;
const {win, timeHeaderEl, scrubberEl, scrubberHandleEl} = timeline;
const playTimelineButtonEl = panel.playTimelineButtonEl;
ok(!playTimelineButtonEl.classList.contains("paused"),
"The timeline play button is in its playing state by default");
info("Mousedown in the header to move the scrubber");
await synthesizeInHeaderAndWaitForChange(timeline, 50, 1, "mousedown");
checkScrubberIsAt(scrubberEl, timeHeaderEl, 50);
ok(playTimelineButtonEl.classList.contains("paused"),
"The timeline play button is in its paused state after mousedown");
info("Continue moving the mouse and verify that the scrubber tracks it");
await synthesizeInHeaderAndWaitForChange(timeline, 100, 1, "mousemove");
checkScrubberIsAt(scrubberEl, timeHeaderEl, 100);
ok(playTimelineButtonEl.classList.contains("paused"),
"The timeline play button is in its paused state after mousemove");
info("Release the mouse and move again and verify that the scrubber stays");
EventUtils.synthesizeMouse(timeHeaderEl, 100, 1, {type: "mouseup"}, win);
EventUtils.synthesizeMouse(timeHeaderEl, 200, 1, {type: "mousemove"}, win);
checkScrubberIsAt(scrubberEl, timeHeaderEl, 100);
info("Try to drag the scrubber handle and check that the scrubber moves");
const onDataChanged = timeline.once("timeline-data-changed");
EventUtils.synthesizeMouse(scrubberHandleEl, 1, 20, {type: "mousedown"}, win);
EventUtils.synthesizeMouse(timeHeaderEl, 0, 0, {type: "mousemove"}, win);
EventUtils.synthesizeMouse(timeHeaderEl, 0, 0, {type: "mouseup"}, win);
await onDataChanged;
checkScrubberIsAt(scrubberEl, timeHeaderEl, 0);
// Wait for promise of setCurrentTimes if setCurrentTimes is running.
if (panel.setCurrentTimeAllPromise) {
await panel.setCurrentTimeAllPromise;
}
});
async function synthesizeInHeaderAndWaitForChange(timeline, x, y, type) {
const onDataChanged = timeline.once("timeline-data-changed");
EventUtils.synthesizeMouse(timeline.timeHeaderEl, x, y, {type}, timeline.win);
await onDataChanged;
}
function getPositionPercentage(pos, headerEl) {
return pos * 100 / headerEl.offsetWidth;
}
function checkScrubberIsAt(scrubberEl, timeHeaderEl, pos) {
const newPos = Math.round(parseFloat(scrubberEl.style.left));
const expectedPos = Math.round(getPositionPercentage(pos, timeHeaderEl));
is(newPos, expectedPos,
`The scrubber is at position ${pos} (${expectedPos}%)`);
}

View file

@ -1,29 +0,0 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/* eslint-disable mozilla/no-arbitrary-setTimeout */
"use strict";
requestLongerTimeout(2);
// Check that the scrubber in the timeline moves when animations are playing.
// The animations in the test page last for a very long time, so the test just
// measures the position of the scrubber once, then waits for some time to pass
// and measures its position again.
add_task(async function() {
await addTab(URL_ROOT + "doc_simple_animation.html");
const {panel} = await openAnimationInspector();
const timeline = panel.animationsTimelineComponent;
const scrubberEl = timeline.scrubberEl;
const startPos = scrubberEl.getBoundingClientRect().left;
info("Wait for some time to check that the scrubber moves");
await new Promise(r => setTimeout(r, 2000));
const endPos = scrubberEl.getBoundingClientRect().left;
ok(endPos > startPos, "The scrubber has moved");
});

View file

@ -1,87 +0,0 @@
/* 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);
// Animation.currentTime ignores neagtive delay and positive/negative endDelay
// during fill-mode, even if they are set.
// For example, when the animation timing is
// { duration: 1000, iterations: 1, endDelay: -500, easing: linear },
// the animation progress is 0.5 at 700ms because the progress stops as 0.5 at
// 500ms in original animation. However, if you set as
// animation.currentTime = 700 manually, the progress will be 0.7.
// So we modify setCurrentTime method since
// AnimationInspector should re-produce same as original animation.
// In these tests,
// we confirm the behavior of setCurrentTime by delay and endDelay.
add_task(async function() {
await addTab(URL_ROOT + "doc_timing_combination_animation.html");
const { panel, controller } = await openAnimationInspector();
await clickTimelinePlayPauseButton(panel);
const timeBlockComponents = getAnimationTimeBlocks(panel);
// Test -5000ms.
let time = -5000;
await controller.setCurrentTimeAll(time, true);
for (let i = 0; i < timeBlockComponents.length; i++) {
await timeBlockComponents[i].animation.refreshState();
const state = await timeBlockComponents[i].animation.state;
info(`Check the state at ${ time }ms with `
+ `delay:${ state.delay } and endDelay:${ state.endDelay }`);
is(state.currentTime, 0,
`The currentTime should be 0 at setCurrentTime(${ time })`);
}
// Test 10000ms.
time = 10000;
await controller.setCurrentTimeAll(time, true);
for (let i = 0; i < timeBlockComponents.length; i++) {
await timeBlockComponents[i].animation.refreshState();
const state = await timeBlockComponents[i].animation.state;
info(`Check the state at ${ time }ms with `
+ `delay:${ state.delay } and endDelay:${ state.endDelay }`);
const expected = state.delay < 0 ? 0 : time;
is(state.currentTime, expected,
`The currentTime should be ${ expected } at setCurrentTime(${ time }).`
+ ` delay: ${ state.delay } and endDelay: ${ state.endDelay }`);
}
// Test 60000ms.
time = 60000;
await controller.setCurrentTimeAll(time, true);
for (let i = 0; i < timeBlockComponents.length; i++) {
await timeBlockComponents[i].animation.refreshState();
const state = await timeBlockComponents[i].animation.state;
info(`Check the state at ${ time }ms with `
+ `delay:${ state.delay } and endDelay:${ state.endDelay }`);
const expected = state.delay < 0 ? time + state.delay : time;
is(state.currentTime, expected,
`The currentTime should be ${ expected } at setCurrentTime(${ time }).`
+ ` delay: ${ state.delay } and endDelay: ${ state.endDelay }`);
}
// Test 150000ms.
time = 150000;
await controller.setCurrentTimeAll(time, true);
for (let i = 0; i < timeBlockComponents.length; i++) {
await timeBlockComponents[i].animation.refreshState();
const state = await timeBlockComponents[i].animation.state;
info(`Check the state at ${ time }ms with `
+ `delay:${ state.delay } and endDelay:${ state.endDelay }`);
const currentTime = state.delay < 0 ? time + state.delay : time;
const endTime =
state.delay + state.iterationCount * state.duration + state.endDelay;
const expected =
state.endDelay < 0 && state.fill === "both" && currentTime > endTime
? endTime : currentTime;
is(state.currentTime, expected,
`The currentTime should be ${ expected } at setCurrentTime(${ time }).`
+ ` delay: ${ state.delay } and endDelay: ${ state.endDelay }`);
}
});

View file

@ -1,105 +0,0 @@
/* 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 short duration (e.g. 1ms) animation.
add_task(async function() {
await addTab(URL_ROOT + "doc_short_duration_animation.html");
const { panel, inspector } = await openAnimationInspector();
info("Check the listed time blocks");
const timeBlocks = getAnimationTimeBlocks(panel);
for (let i = 0; i < timeBlocks.length; i++) {
info(`Check the time block ${i}`);
const {containerEl, animation: {state}} = timeBlocks[i];
checkSummaryGraph(containerEl, state);
}
info("Check the time block one by one");
info("Check #onetime");
await selectNodeAndWaitForAnimations("#onetime", inspector);
let timeBlock = getAnimationTimeBlocks(panel)[0];
let containerEl = timeBlock.containerEl;
let state = timeBlock.animation.state;
checkSummaryGraph(containerEl, state, true);
info("Check #infinite");
await selectNodeAndWaitForAnimations("#infinite", inspector);
timeBlock = getAnimationTimeBlocks(panel)[0];
containerEl = timeBlock.containerEl;
state = timeBlock.animation.state;
checkSummaryGraph(containerEl, state, true);
});
function checkSummaryGraph(el, state, isDetail) {
info("Check the coordinates of summary graph");
const groupEls = el.querySelectorAll("svg g");
groupEls.forEach(groupEl => {
const pathEls = groupEl.querySelectorAll(".iteration-path");
let expectedIterationCount = 0;
if (isDetail) {
expectedIterationCount = state.iterationCount ? state.iterationCount : 1;
} else {
expectedIterationCount = state.iterationCount ? state.iterationCount : 2;
}
is(pathEls.length, expectedIterationCount,
`The count of path shoud be ${ expectedIterationCount }`);
pathEls.forEach((pathEl, index) => {
const startX = index * state.duration;
const endX = startX + state.duration;
const pathSegList = pathEl.pathSegList;
const firstPathSeg = pathSegList.getItem(0);
is(firstPathSeg.x, startX,
`The x of first segment should be ${ startX }`);
is(firstPathSeg.y, 0, "The y of first segment should be 0");
// The easing of test animation is 'linear'.
// Therefore, the y of second path segment will be 0.
const secondPathSeg = pathSegList.getItem(1);
is(secondPathSeg.x, startX,
`The x of second segment should be ${ startX }`);
is(secondPathSeg.y, 0, "The y of second segment should be 0");
const thirdLastPathSeg = pathSegList.getItem(pathSegList.numberOfItems - 4);
approximate(thirdLastPathSeg.x, endX - 0.001, 0.005,
"The x of third last segment should be approximately "
+ (endX - 0.001));
approximate(thirdLastPathSeg.y, 0.999, 0.005,
" The y of third last segment should be approximately "
+ thirdLastPathSeg.x);
// The test animation is not 'forwards' fill-mode.
// Therefore, the y of second last path segment will be 0.
const secondLastPathSeg =
pathSegList.getItem(pathSegList.numberOfItems - 3);
is(secondLastPathSeg.x, endX,
`The x of second last segment should be ${ endX }`);
// We use computed style of 'opacity' to create summary graph.
// So, if currentTime is same to the duration, although progress is null
// opacity is 0.
const expectedY = 0;
is(secondLastPathSeg.y, expectedY,
`The y of second last segment should be ${ expectedY }`);
const lastPathSeg = pathSegList.getItem(pathSegList.numberOfItems - 2);
is(lastPathSeg.x, endX, `The x of last segment should be ${ endX }`);
is(lastPathSeg.y, 0, "The y of last segment should be 0");
const closePathSeg = pathSegList.getItem(pathSegList.numberOfItems - 1);
is(closePathSeg.pathSegType, closePathSeg.PATHSEG_CLOSEPATH,
`The actual last segment should be close path`);
});
});
}
function approximate(value, expected, permissibleRange, message) {
const min = expected - permissibleRange;
const max = expected + permissibleRange;
ok(min <= value && value <= max, message);
}

View file

@ -1,109 +0,0 @@
/* 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 animation delay is visualized in the timeline when the animation
// is delayed.
// Also check that negative delays do not overflow the UI, and are shown like
// positive delays.
add_task(async function() {
await addTab(URL_ROOT + "doc_simple_animation.html");
const {inspector, panel} = await openAnimationInspector();
info("Selecting a delayed animated node");
await selectNodeAndWaitForAnimations(".delayed", inspector);
const timelineEl = panel.animationsTimelineComponent.rootWrapperEl;
checkDelayAndName(timelineEl, true);
let animationEl = timelineEl.querySelector(".animation");
let state = getAnimationTimeBlocks(panel)[0].animation.state;
checkPath(animationEl, state);
info("Selecting a no-delay animated node");
await selectNodeAndWaitForAnimations(".animated", inspector);
checkDelayAndName(timelineEl, false);
animationEl = timelineEl.querySelector(".animation");
state = getAnimationTimeBlocks(panel)[0].animation.state;
checkPath(animationEl, state);
info("Selecting a negative-delay animated node");
await selectNodeAndWaitForAnimations(".negative-delay", inspector);
checkDelayAndName(timelineEl, true);
animationEl = timelineEl.querySelector(".animation");
state = getAnimationTimeBlocks(panel)[0].animation.state;
checkPath(animationEl, state);
});
function checkDelayAndName(timelineEl, hasDelay) {
const delay = timelineEl.querySelector(".delay");
is(!!delay, hasDelay, "The timeline " +
(hasDelay ? "contains" : "does not contain") +
" a delay element, as expected");
if (hasDelay) {
const targetNode = timelineEl.querySelector(".target");
// Check that the delay element does not cause the timeline to overflow.
const delayLeft = Math.round(delay.getBoundingClientRect().x);
const sidebarWidth = Math.round(targetNode.getBoundingClientRect().width);
ok(delayLeft >= sidebarWidth,
"The delay element isn't displayed over the sidebar");
}
}
function checkPath(animationEl, state) {
const groupEls = animationEl.querySelectorAll("svg g");
groupEls.forEach(groupEl => {
// Check existance of delay path.
const delayPathEl = groupEl.querySelector(".delay-path");
if (!state.iterationCount && state.delay < 0) {
// Infinity
ok(!delayPathEl, "The delay path for Infinity should not exist");
return;
}
if (state.delay === 0) {
ok(!delayPathEl, "The delay path for zero delay should not exist");
return;
}
ok(delayPathEl, "The delay path should exist");
// Check delay path coordinates.
const pathSegList = delayPathEl.pathSegList;
const startingPathSeg = pathSegList.getItem(0);
const endingPathSeg = pathSegList.getItem(pathSegList.numberOfItems - 2);
if (state.delay < 0) {
ok(delayPathEl.classList.contains("negative"),
"The delay path should have 'negative' class");
const expectedY = 0;
const startingX = state.delay;
const endingX = 0;
is(startingPathSeg.x, startingX,
`The x of starting point should be ${ startingX }`);
is(startingPathSeg.y, expectedY,
`The y of starting point should be ${ expectedY }`);
is(endingPathSeg.x, endingX,
`The x of ending point should be ${ endingX }`);
is(endingPathSeg.y, expectedY,
`The y of ending point should be ${ expectedY }`);
} else {
ok(!delayPathEl.classList.contains("negative"),
"The delay path should not have 'negative' class");
const expectedY = 0;
const startingX = 0;
const endingX = state.delay;
is(startingPathSeg.x, startingX,
`The x of starting point should be ${ startingX }`);
is(startingPathSeg.y, expectedY,
`The y of starting point should be ${ expectedY }`);
is(endingPathSeg.x, endingX,
`The x of ending point should be ${ endingX }`);
is(endingPathSeg.y, expectedY,
`The y of ending point should be ${ expectedY }`);
}
});
}

View file

@ -1,81 +0,0 @@
/* 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 animation endDelay is visualized in the timeline when the
// animation is delayed.
// Also check that negative endDelays do not overflow the UI, and are shown
// like positive endDelays.
add_task(async function() {
await addTab(URL_ROOT + "doc_end_delay.html");
const {inspector, panel} = await openAnimationInspector();
const selectors = ["#target1", "#target2", "#target3", "#target4"];
for (let i = 0; i < selectors.length; i++) {
const selector = selectors[i];
await selectNode(selector, inspector);
await waitForAnimationSelecting(panel);
const timelineEl = panel.animationsTimelineComponent.rootWrapperEl;
const animationEl = timelineEl.querySelector(".animation");
checkEndDelayAndName(animationEl);
const state = getAnimationTimeBlocks(panel)[0].animation.state;
checkPath(animationEl, state);
}
});
function checkEndDelayAndName(animationEl) {
const endDelay = animationEl.querySelector(".end-delay");
const name = animationEl.querySelector(".name");
const targetNode = animationEl.querySelector(".target");
// Check that the endDelay element does not cause the timeline to overflow.
const endDelayLeft = Math.round(endDelay.getBoundingClientRect().x);
const sidebarWidth = Math.round(targetNode.getBoundingClientRect().width);
ok(endDelayLeft >= sidebarWidth,
"The endDelay element isn't displayed over the sidebar");
// Check that the endDelay is not displayed on top of the name.
const endDelayRight = Math.round(endDelay.getBoundingClientRect().right);
const nameLeft = Math.round(name.getBoundingClientRect().left);
ok(endDelayRight >= nameLeft,
"The endDelay element does not span over the name element");
}
function checkPath(animationEl, state) {
const groupEls = animationEl.querySelectorAll("svg g");
groupEls.forEach(groupEl => {
// Check existance of enddelay path.
const endDelayPathEl = groupEl.querySelector(".enddelay-path");
ok(endDelayPathEl, "The endDelay path should exist");
// Check enddelay path coordinates.
const pathSegList = endDelayPathEl.pathSegList;
const startingPathSeg = pathSegList.getItem(0);
const endingPathSeg = pathSegList.getItem(pathSegList.numberOfItems - 2);
if (state.endDelay < 0) {
ok(endDelayPathEl.classList.contains("negative"),
"The endDelay path should have 'negative' class");
const endingX = state.delay + state.iterationCount * state.duration;
const startingX = endingX + state.endDelay;
is(startingPathSeg.x, startingX,
`The x of starting point should be ${ startingX }`);
is(endingPathSeg.x, endingX,
`The x of ending point should be ${ endingX }`);
} else {
ok(!endDelayPathEl.classList.contains("negative"),
"The endDelay path should not have 'negative' class");
const startingX =
state.delay + state.iterationCount * state.duration;
const endingX = startingX + state.endDelay;
is(startingPathSeg.x, startingX,
`The x of starting point should be ${ startingX }`);
is(endingPathSeg.x, endingX,
`The x of ending point should be ${ endingX }`);
}
});
}

View file

@ -1,52 +0,0 @@
/* 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 is displays as many iteration elements as there are
// iterations in an animation.
add_task(async function() {
await addTab(URL_ROOT + "doc_simple_animation.html");
const {inspector, panel} = await openAnimationInspector();
info("Selecting the test node");
await selectNodeAndWaitForAnimations(".delayed", inspector);
info("Getting the animation element from the panel");
const timelineComponent = panel.animationsTimelineComponent;
const timelineEl = timelineComponent.rootWrapperEl;
let animation = timelineEl.querySelector(".time-block");
// Get iteration count from summary graph path.
let iterationCount = getIterationCount(animation);
is(animation.querySelectorAll("svg g").length, 1,
"The animation timeline contains one g element");
is(iterationCount, 10,
"The animation timeline contains the right number of iterations");
ok(!animation.querySelector(".infinity"),
"The summary graph does not have any elements "
+ " that have infinity class");
info("Selecting another test node with an infinite animation");
await selectNodeAndWaitForAnimations(".animated", inspector);
info("Getting the animation element from the panel again");
animation = timelineEl.querySelector(".time-block");
iterationCount = getIterationCount(animation);
is(animation.querySelectorAll("svg g").length, 1,
"The animation timeline contains one g element");
is(iterationCount, 1,
"The animation timeline contains one iteration");
ok(animation.querySelector(".infinity"),
"The summary graph has an element that has infinity class");
});
function getIterationCount(timeblockEl) {
return timeblockEl.querySelectorAll("svg g .iteration-path").length;
}

View file

@ -1,28 +0,0 @@
/* 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 the text content and width of name label.
add_task(async function() {
await addTab(URL_ROOT + "doc_simple_animation.html");
const {inspector, panel} = await openAnimationInspector();
info("Selecting 'simple-animation' animation which is running on compositor");
await selectNodeAndWaitForAnimations(".animated", inspector);
checkNameLabel(panel.animationsTimelineComponent.rootWrapperEl, "simple-animation");
info("Selecting 'no-compositor' animation which is not running on compositor");
await selectNodeAndWaitForAnimations(".no-compositor", inspector);
checkNameLabel(panel.animationsTimelineComponent.rootWrapperEl, "no-compositor");
});
function checkNameLabel(rootWrapperEl, expectedLabelContent) {
const labelEl = rootWrapperEl.querySelector(".name svg text");
is(labelEl.textContent, expectedLabelContent,
`Text content of labelEl sould be ${ expectedLabelContent }`);
}

View file

@ -1,70 +0,0 @@
/* 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 displays animations' duration, delay iteration
// counts and iteration start in tooltips.
add_task(async function() {
await addTab(URL_ROOT + "doc_simple_animation.html");
const {panel, controller} = await openAnimationInspector();
info("Getting the animation element from the panel");
const timelineEl = panel.animationsTimelineComponent.rootWrapperEl;
const timeBlockNameEls = timelineEl.querySelectorAll(".time-block .name");
// Verify that each time-block's name element has a tooltip that looks sort of
// ok. We don't need to test the actual content.
[...timeBlockNameEls].forEach((el, i) => {
ok(el.hasAttribute("title"), "The tooltip is defined for animation " + i);
const title = el.getAttribute("title");
const state = controller.animationPlayers[i].state;
if (state.delay) {
ok(title.match(/Delay: [\d.,-]+s/), "The tooltip shows the delay");
}
ok(title.match(/Duration: [\d.,]+s/), "The tooltip shows the duration");
if (state.endDelay) {
ok(title.match(/End delay: [\d.,-]+s/), "The tooltip shows the endDelay");
}
if (state.iterationCount !== 1) {
ok(title.match(/Repeats: /), "The tooltip shows the iterations");
} else {
ok(!title.match(/Repeats: /), "The tooltip doesn't show the iterations");
}
if (state.easing && state.easing !== "linear") {
ok(title.match(/Overall easing: /), "The tooltip shows the easing");
} else {
ok(!title.match(/Overall easing: /),
"The tooltip doesn't show the easing if it is 'linear'");
}
if (state.animationTimingFunction && state.animationTimingFunction !== "ease") {
is(state.type, "cssanimation",
"The animation type should be CSS Animations if has animation-timing-function");
ok(title.match(/Animation timing function: /),
"The tooltip shows animation-timing-function");
} else {
ok(!title.match(/Animation timing function: /),
"The tooltip doesn't show the animation-timing-function if it is 'ease'"
+ " or not CSS Animations");
}
if (state.fill) {
ok(title.match(/Fill: /), "The tooltip shows the fill");
}
if (state.direction) {
if (state.direction === "normal") {
ok(!title.match(/Direction: /),
"The tooltip doesn't show the direction if it is 'normal'");
} else {
ok(title.match(/Direction: /), "The tooltip shows the direction");
}
}
ok(!title.match(/Iteration start:/),
"The tooltip doesn't show the iteration start");
});
});

View file

@ -1,97 +0,0 @@
/* 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 if an animation has had its playbackRate changed via the DOM, then
// the timeline UI shows the right delay and duration.
// Indeed, the header in the timeline UI always shows the unaltered time,
// because there might be multiple animations displayed at the same time, some
// of which may have a different rate than others. Those that have had their
// rate changed have a delay = delay/rate and a duration = duration/rate.
add_task(async function() {
await addTab(URL_ROOT + "doc_modify_playbackRate.html");
const {panel} = await openAnimationInspector();
const timelineEl = panel.animationsTimelineComponent.rootWrapperEl;
const timeBlocks = timelineEl.querySelectorAll(".time-block");
is(timeBlocks.length, 2, "2 animations are displayed");
info("The first animation has its rate set to 1, let's measure it");
const el = timeBlocks[0];
const delay = parseInt(el.querySelector(".delay").style.width, 10);
let duration = null;
el.querySelectorAll("svg g").forEach(groupEl => {
const dur = getDuration(groupEl.querySelector("path"));
if (!duration) {
duration = dur;
return;
}
is(duration, dur, "The durations shuld be same at all paths in one group");
});
info("The second animation has its rate set to 2, so should be shorter");
const el2 = timeBlocks[1];
const delay2 = parseInt(el2.querySelector(".delay").style.width, 10);
let duration2 = null;
el2.querySelectorAll("svg g").forEach(groupEl => {
const dur = getDuration(groupEl.querySelector("path"));
if (!duration2) {
duration2 = dur;
return;
}
is(duration2, dur, "The durations shuld be same at all paths in one group");
});
// The width are calculated by the animation-inspector dynamically depending
// on the size of the panel, and therefore depends on the test machine/OS.
// Let's not try to be too precise here and compare numbers.
const durationDelta = (2 * duration2) - duration;
ok(durationDelta <= 1, "The duration width is correct");
const delayDelta = (2 * delay2) - delay;
ok(delayDelta <= 1, "The delay width is correct");
});
function getDuration(pathEl) {
const pathSegList = pathEl.pathSegList;
// Find the index of starting iterations.
let startingIterationIndex = 0;
const firstPathSeg = pathSegList.getItem(1);
for (let i = 2, n = pathSegList.numberOfItems - 2; i < n; i++) {
// Changing point of the progress acceleration is the time.
const pathSeg = pathSegList.getItem(i);
if (firstPathSeg.y != pathSeg.y) {
startingIterationIndex = i;
break;
}
}
// Find the index of ending iterations.
let endingIterationIndex = 0;
let previousPathSegment = pathSegList.getItem(startingIterationIndex);
for (let i = startingIterationIndex + 1, n = pathSegList.numberOfItems - 2;
i < n; i++) {
// Find forwards fill-mode.
const pathSeg = pathSegList.getItem(i);
if (previousPathSegment.y == pathSeg.y) {
endingIterationIndex = i;
break;
}
previousPathSegment = pathSeg;
}
if (endingIterationIndex) {
// Not forwards fill-mode
endingIterationIndex = pathSegList.numberOfItems - 2;
}
// Return the distance of starting and ending
const startingIterationPathSegment =
pathSegList.getItem(startingIterationIndex);
const endingIterationPathSegment =
pathSegList.getItem(startingIterationIndex);
return endingIterationPathSegment.x - startingIterationPathSegment.x;
}

View file

@ -1,58 +0,0 @@
/* 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 contains the right elements.
add_task(async function() {
await addTab(URL_ROOT + "doc_simple_animation.html");
const {panel} = await openAnimationInspector();
const timeline = panel.animationsTimelineComponent;
const el = timeline.rootWrapperEl;
ok(el.querySelector(".time-header"),
"The header element is in the DOM of the timeline");
ok(el.querySelectorAll(".time-header .header-item").length,
"The header has some time graduations");
ok(el.querySelector(".animations"),
"The animations container is in the DOM of the timeline");
is(el.querySelectorAll(".animations .animation").length,
timeline.animations.length,
"The number of animations displayed matches the number of animations");
const animationEls = el.querySelectorAll(".animations .animation");
const evenColor =
el.ownerDocument.defaultView.getComputedStyle(animationEls[0]).backgroundColor;
const oddColor =
el.ownerDocument.defaultView.getComputedStyle(animationEls[1]).backgroundColor;
isnot(evenColor, oddColor,
"Background color of an even animation should be different from odd");
for (let i = 0; i < timeline.animations.length; i++) {
const animation = timeline.animations[i];
const animationEl = animationEls[i];
ok(animationEl.querySelector(".target"),
"The animated node target element is in the DOM");
ok(animationEl.querySelector(".time-block"),
"The timeline element is in the DOM");
is(animationEl.querySelector(".name").textContent,
animation.state.name,
"The name on the timeline is correct");
is(animationEl.querySelectorAll("svg g").length, 1,
"The g element should be one since this doc's all animation has only one shape");
ok(animationEl.querySelector("svg g path"),
"The timeline has svg and path element as summary graph");
const expectedBackgroundColor = i % 2 === 0 ? evenColor : oddColor;
const backgroundColor =
el.ownerDocument.defaultView.getComputedStyle(animationEl).backgroundColor;
is(backgroundColor, expectedBackgroundColor,
"The background-color shoud be changed to alternate");
}
});

View file

@ -1,31 +0,0 @@
/* 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 a page navigation resets the state of the global toggle button.
add_task(async function() {
await addTab(URL_ROOT + "doc_simple_animation.html");
const {inspector, panel} = await openAnimationInspector();
info("Select the non-animated test node");
await selectNodeAndWaitForAnimations(".still", inspector);
ok(!panel.toggleAllButtonEl.classList.contains("paused"),
"The toggle button is in its running state by default");
info("Toggle all animations, so that they pause");
await panel.toggleAll();
ok(panel.toggleAllButtonEl.classList.contains("paused"),
"The toggle button now is in its paused state");
info("Reloading the page");
await reloadTab(inspector);
ok(!panel.toggleAllButtonEl.classList.contains("paused"),
"The toggle button is back in its running state");
});

View file

@ -1,32 +0,0 @@
/* 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 main toggle button actually toggles animations.
// This test doesn't need to be extra careful about checking that *all*
// animations have been paused (including inside iframes) because there's an
// actor test in /devtools/server/tests/browser/ that does this.
add_task(async function() {
await addTab(URL_ROOT + "doc_simple_animation.html");
const {panel} = await openAnimationInspector();
info("Click the toggle button");
await panel.toggleAll();
await checkState("paused");
info("Click again the toggle button");
await panel.toggleAll();
await checkState("running");
});
async function checkState(state) {
for (const selector of [".animated", ".multi", ".long"]) {
const playState = await getAnimationPlayerState(selector);
is(playState, state, "The animation on node " + selector + " is " + state);
}
}

View file

@ -1,36 +0,0 @@
/* 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 animation panel has a top toolbar that contains the play/pause
// button and that is displayed at all times.
// Also test that this toolbar gets replaced by the timeline toolbar when there
// are animations to be displayed.
add_task(async function() {
await addTab(URL_ROOT + "doc_simple_animation.html");
const {inspector, window} = await openAnimationInspector();
const doc = window.document;
const toolbar = doc.querySelector("#global-toolbar");
ok(toolbar, "The panel contains the toolbar element with the new UI");
ok(!isNodeVisible(toolbar),
"The toolbar is hidden while there are animations");
const timelineToolbar = doc.querySelector("#timeline-toolbar");
ok(timelineToolbar, "The panel contains a timeline toolbar element");
ok(isNodeVisible(timelineToolbar),
"The timeline toolbar is visible when there are animations");
info("Select a node that has no animations");
await selectNodeAndWaitForAnimations(".still", inspector);
ok(isNodeVisible(toolbar),
"The toolbar is shown when there are no animations");
ok(!isNodeVisible(timelineToolbar),
"The timeline toolbar is hidden when there are no animations");
});

View file

@ -1,36 +0,0 @@
/* 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);
// Verify that if the animation's duration, iterations or delay change in
// content, then the widget reflects the changes.
add_task(async function() {
await addTab(URL_ROOT + "doc_simple_animation.html");
const {panel, controller, inspector} = await openAnimationInspector();
info("Select the test node");
await selectNodeAndWaitForAnimations(".animated", inspector);
const animation = controller.animationPlayers[0];
await setStyle(animation, panel, "animationDuration", "5.5s", ".animated");
await setStyle(animation, panel, "animationIterationCount", "300", ".animated");
await setStyle(animation, panel, "animationDelay", "45s", ".animated");
const animationsEl = panel.animationsTimelineComponent.animationsEl;
const timeBlockEl = animationsEl.querySelector(".time-block");
// 45s delay + (300 * 5.5)s duration
const expectedTotalDuration = 1695 * 1000;
// XXX: the nb and size of each iteration cannot be tested easily (displayed
// using a linear-gradient background and capped at 2px wide). They should
// be tested in bug 1173761.
const delayWidth = parseFloat(timeBlockEl.querySelector(".delay").style.width);
is(Math.round(delayWidth * expectedTotalDuration / 100), 45 * 1000,
"The timeline has the right delay");
});

View file

@ -1,57 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
div {
background-color: lime;
height: 50px;
}
@keyframes anim {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
#target1 {
animation: anim 1s infinite;
}
</style>
</head>
<body>
<div id="target1">1</div>
<div id="target2">2</div>
<div id="target3">3</div>
<script>
"use strict";
const duration = 1000;
const delay = 1000;
const endDelay = 1000;
// ".animation" is appended from test.
const observer = new MutationObserver(mutations => {
for (const mutation of mutations) {
if (mutation.type === "attributes" &&
mutation.attributeName === "class" &&
mutation.target.classList.contains("animation")) {
const effect = new KeyframeEffect(mutation.target,
{ opacity: [1, 0] },
{ duration, delay, endDelay });
const animation = new Animation(effect, document.timeline);
animation.play();
// wait
animation.currentTime = 100;
}
}
});
observer.observe(document.querySelector("#target2"), { attributes: true });
observer.observe(document.querySelector("#target3"), { attributes: true });
</script>
</body>
</html>

View file

@ -1,23 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
body {
background-color: white;
color: black;
animation: change-background-color 3s infinite alternate;
}
@keyframes change-background-color {
to {
background-color: black;
color: white;
}
}
</style>
</head>
<body>
<h1>Animated body element</h1>
</body>
</html>

View file

@ -1,33 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
div {
width: 100px;
height: 100px;
border: 1px solid gray;
}
#target1 {
animation: anim 100s;
}
@keyframes anim {
from {
transform: translate(-50%, 100%);
}
to {
transform: translateX(-50%);
}
}
</style>
</head>
<body>
<div id="target1"></div>
<div id="target2"></div>
<div id="target3"></div>
<div id="target4"></div>
<div id="target5"></div>
</body>
</html>

View file

@ -1,69 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
.target {
width: 50px;
height: 50px;
background: blue;
}
</style>
</head>
<body>
<div id="target1" class="target"></div>
<div id="target2" class="target"></div>
<div id="target3" class="target"></div>
<div id="target4" class="target"></div>
<script>
/* globals KeyframeEffect, Animation */
"use strict";
const animations = [{
id: "target1",
frames: [{ opacity: 0, offset: 0 }, { opacity: 1, offset: 1 }],
timing: {
id: "endDelay_animation1",
duration: 1000000,
endDelay: 500000,
fill: "none"
}
}, {
id: "target2",
frames: [{ opacity: 0, offset: 0 }, { opacity: 1, offset: 1 }],
timing: {
id: "endDelay_animation2",
duration: 1000000,
endDelay: -500000,
fill: "none"
}
}, {
id: "target3",
frames: [{ opacity: 0, offset: 0 }, { opacity: 1, offset: 1 }],
timing: {
id: "endDelay_animation3",
duration: 1000000,
endDelay: -1500000,
fill: "forwards"
}
}, {
id: "target4",
frames: [{ opacity: 0, offset: 0 }, { opacity: 1, offset: 1 }],
timing: {
id: "endDelay_animation4",
duration: 100000,
delay: 100000,
endDelay: -1500000,
fill: "forwards"
}
}];
for (const {id, frames, timing} of animations) {
const effect = new KeyframeEffect(document.getElementById(id),
frames, timing);
const animation = new Animation(effect, document.timeline);
animation.play();
}
</script>
</body>
</html>

View file

@ -1,122 +0,0 @@
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/* globals addMessageListener, sendAsyncMessage */
"use strict";
// A helper frame-script for browser/devtools/animationinspector tests.
/**
* Toggle (play or pause) one of the animation players of a given node.
* @param {Object} data
* - {String} selector The CSS selector to get the node (can be a "super"
* selector).
* - {Number} animationIndex The index of the node's animationPlayers to play
* or pause
* - {Boolean} pause True to pause the animation, false to play.
*/
addMessageListener("Test:ToggleAnimationPlayer", function(msg) {
const {selector, animationIndex, pause} = msg.data;
const node = superQuerySelector(selector);
if (!node) {
return;
}
const animation = node.getAnimations()[animationIndex];
if (pause) {
animation.pause();
} else {
animation.play();
}
sendAsyncMessage("Test:ToggleAnimationPlayer");
});
/**
* Change the currentTime of one of the animation players of a given node.
* @param {Object} data
* - {String} selector The CSS selector to get the node (can be a "super"
* selector).
* - {Number} animationIndex The index of the node's animationPlayers to change.
* - {Number} currentTime The current time to set.
*/
addMessageListener("Test:SetAnimationPlayerCurrentTime", function(msg) {
const {selector, animationIndex, currentTime} = msg.data;
const node = superQuerySelector(selector);
if (!node) {
return;
}
const animation = node.getAnimations()[animationIndex];
animation.currentTime = currentTime;
sendAsyncMessage("Test:SetAnimationPlayerCurrentTime");
});
/**
* Change the playbackRate of one of the animation players of a given node.
* @param {Object} data
* - {String} selector The CSS selector to get the node (can be a "super"
* selector).
* - {Number} animationIndex The index of the node's animationPlayers to change.
* - {Number} playbackRate The rate to set.
*/
addMessageListener("Test:SetAnimationPlayerPlaybackRate", function(msg) {
const {selector, animationIndex, playbackRate} = msg.data;
const node = superQuerySelector(selector);
if (!node) {
return;
}
const player = node.getAnimations()[animationIndex];
player.playbackRate = playbackRate;
sendAsyncMessage("Test:SetAnimationPlayerPlaybackRate");
});
/**
* Get the current playState of an animation player on a given node.
* @param {Object} data
* - {String} selector The CSS selector to get the node (can be a "super"
* selector).
* - {Number} animationIndex The index of the node's animationPlayers to check
*/
addMessageListener("Test:GetAnimationPlayerState", function(msg) {
const {selector, animationIndex} = msg.data;
const node = superQuerySelector(selector);
if (!node) {
return;
}
const animation = node.getAnimations()[animationIndex];
animation.ready.then(() => {
sendAsyncMessage("Test:GetAnimationPlayerState", animation.playState);
});
});
/**
* Like document.querySelector but can go into iframes too.
* ".container iframe || .sub-container div" will first try to find the node
* matched by ".container iframe" in the root document, then try to get the
* content document inside it, and then try to match ".sub-container div" inside
* this document.
* Any selector coming before the || separator *MUST* match a frame node.
* @param {String} superSelector.
* @return {DOMNode} The node, or null if not found.
*/
function superQuerySelector(superSelector, root = content.document) {
const frameIndex = superSelector.indexOf("||");
if (frameIndex === -1) {
return root.querySelector(superSelector);
}
const rootSelector = superSelector.substring(0, frameIndex).trim();
const childSelector = superSelector.substring(frameIndex + 2).trim();
root = root.querySelector(rootSelector);
if (!root || !root.contentWindow) {
return null;
}
return superQuerySelector(childSelector, root.contentWindow.document);
}

View file

@ -1,57 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Yay! Keyframes!</title>
<style>
div {
animation: wow 100s forwards;
/* Add a border here to avoid layout warnings in Linux debug builds: Bug 1329784 */
border: 1px solid transparent;
}
@keyframes wow {
0% {
width: 100px;
height: 100px;
border-radius: 0px;
background: #f06;
}
10% {
border-radius: 2px;
}
20% {
transform: rotate(13deg);
}
30% {
background: gold;
}
40% {
filter: blur(40px);
}
50% {
transform: rotate(720deg) translateX(300px) skew(-13deg);
}
60% {
width: 200px;
height: 200px;
}
70% {
border-radius: 10px;
}
80% {
background: #333;
}
90% {
border-radius: 50%;
}
100% {
width: 500px;
height: 500px;
}
}
</style>
</head>
<body>
<div></div>
</body>
</html>

View file

@ -1,32 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
div {
width: 50px;
height: 50px;
background: blue;
animation: move 20s 20s linear;
animation-fill-mode: forwards;
}
@keyframes move {
to {
margin-left: 200px;
}
}
</style>
</head>
<body>
<div></div>
<div class="rate"></div>
<script>
"use strict";
var el = document.querySelector(".rate");
var ani = el.getAnimations()[0];
ani.playbackRate = 2;
</script>
</body>
</html>

View file

@ -1,63 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
.ball {
width: 80px;
height: 80px;
/* Add a border here to avoid layout warnings in Linux debug builds: Bug 1329784 */
border: 1px solid transparent;
border-radius: 50%;
}
.script-animation {
background: #f06;
}
.css-transition {
background: #006;
transition: background-color 20s;
}
.css-animation {
background: #a06;
animation: flash 10s forwards;
}
@keyframes flash {
0% {
opacity: 1;
}
50% {
opacity: 0;
}
100% {
opacity: 1;
}
}
</style>
</head>
<body>
<div class="ball script-animation"></div>
<div class="ball css-animation"></div>
<div class="ball css-transition"></div>
<script>
/* globals KeyframeEffect, Animation */
"use strict";
setTimeout(function() {
document.querySelector(".css-transition").style.backgroundColor = "yellow";
}, 0);
const effect = new KeyframeEffect(
document.querySelector(".script-animation"), [
{opacity: 1, offset: 0},
{opacity: .1, offset: 1}
], { duration: 10000, fill: "forwards" });
const animation = new Animation(effect, document.timeline);
animation.play();
</script>
</body>
</html>

View file

@ -1,188 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
@keyframes css-animations {
from {
opacity: 1;
}
50% {
opacity: 0.5;
}
to {
opacity: 0;
}
}
#css-animations {
animation: css-animations 100s;
}
div {
background-color: lime;
height: 50px;
}
</style>
</head>
<body>
<div id="css-animations"></div>
<script>
"use strict";
const DURATION = 100 * 1000;
[
{
id: "no-easing",
frames: { opacity: [1, 0] },
timing: {
duration: DURATION
}
},
{
id: "effect-easing",
frames: { opacity: [1, 0] },
timing: {
easing: "frames(5)",
duration: DURATION
}
},
{
id: "keyframe-easing",
frames: [
{
offset: 0,
easing: "steps(2)",
opacity: 1
},
{
offset: 1,
opacity: 0
}
],
timing: {
duration: DURATION,
}
},
{
id: "both-easing",
frames: [
{
offset: 0,
easing: "steps(2)",
opacity: 1
},
{
offset: 0,
easing: "steps(5)",
marginLeft: "0px",
marginTop: "0px"
},
{
offset: 1,
opacity: 0,
marginLeft: "100px",
marginTop: "100px"
},
],
timing: {
easing: "steps(10)",
duration: DURATION,
}
},
{
id: "many-keyframes",
frames: [
{
offset: 0,
easing: "steps(2)",
opacity: 1,
backgroundColor: "red",
},
{
offset: 0.25,
easing: "ease-in",
opacity: 0.25,
},
{
offset: 0.3,
easing: "ease-out",
backgroundColor: "blue",
},
{
offset: 0.5,
easing: "linear",
opacity: 0.5,
},
{
offset: 0.75,
easing: "ease-out",
opacity: 0.75,
},
{
offset: 1,
opacity: 0,
backgroundColor: "lime",
},
],
timing: {
duration: DURATION,
}
},
{
id: "narrow-keyframes",
frames: [
{
offset: 0,
opacity: 0,
},
{
offset: 0.1,
easing: "steps(1)",
opacity: 1,
},
{
offset: 0.13,
opacity: 0,
},
],
timing: {
duration: DURATION,
}
},
{
id: "duplicate-offsets",
frames: [
{
offset: 0,
opacity: 1,
},
{
offset: 0.5,
opacity: 1,
},
{
offset: 0.5,
easing: "steps(1)",
opacity: 0,
},
{
offset: 1,
opacity: 1,
},
],
timing: {
duration: DURATION,
}
},
].forEach(({ id, frames, timing }) => {
const target = document.createElement("div");
document.body.appendChild(target);
const effect = new KeyframeEffect(target, frames, timing);
const animation = new Animation(effect, document.timeline);
animation.id = id;
animation.play();
});
</script>
</body>
</html>

View file

@ -1,146 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
div {
width: 50px;
height: 50px;
}
</style>
</head>
<body>
<div id=target1>1</div>
<div id=target2>2</div>
<div id=target3>3</div>
<div id=target4>4</div>
<div id=target5>5</div>
<div id=target6>6</div>
<div id=target7>7</div>
<div id=target8>8</div>
<script>
"use strict";
const timing = {
duration: 1000,
fill: "forwards"
};
document.querySelector("#target1").animate(
[{ backgroundColor: "red",
backgroundRepeat: "space",
fontSize: "10px",
marginLeft: "0px",
opacity: 0,
textAlign: "right",
transform: "translate(0px)" },
{ backgroundColor: "lime",
backgroundRepeat: "round",
fontSize: "20px",
marginLeft: "100px",
opacity: 1,
textAlign: "center",
transform: "translate(100px)" }], timing).pause();
document.querySelector("#target2").animate(
[{ backgroundColor: "lime",
backgroundRepeat: "space",
fontSize: "20px",
marginLeft: "100px",
opacity: 1,
textAlign: "center",
transform: "translate(100px)" },
{ backgroundColor: "red",
backgroundRepeat: "round",
fontSize: "10px",
marginLeft: "0px",
opacity: 0,
textAlign: "right",
transform: "translate(0px)" }], timing).pause();
document.querySelector("#target3").animate(
[{ backgroundColor: "red",
backgroundRepeat: "space",
fontSize: "10px",
marginLeft: "0px",
opacity: 0,
textAlign: "right",
transform: "translate(0px)" },
{ backgroundColor: "blue",
backgroundRepeat: "round",
fontSize: "20px",
marginLeft: "100px",
opacity: 1,
textAlign: "center",
transform: "translate(100px)" },
{ backgroundColor: "lime",
backgroundRepeat: "space",
fontSize: "10px",
marginLeft: "0px",
opacity: 0,
textAlign: "right",
transform: "translate(0px)" }], timing).pause();
document.querySelector("#target4").animate(
[{ backgroundColor: "red",
backgroundRepeat: "space",
fontSize: "10px",
marginLeft: "0px",
opacity: 0,
textAlign: "right",
transform: "translate(0px)",
easing: "steps(2)" },
{ backgroundColor: "lime",
backgroundRepeat: "round",
fontSize: "20px",
marginLeft: "100px",
opacity: 1,
textAlign: "center",
transform: "translate(100px)" }], timing).pause();
timing.easing = "steps(2)";
document.querySelector("#target5").animate(
[{ opacity: 0 }, { opacity: 1 }], timing).pause();
timing.easing = "linear";
document.querySelector("#target6").animate(
[{ opacity: 0, easing: "frames(5)" }, { opacity: 1 }], timing).pause();
document.querySelector("#target7").animate(
[
{
opacity: 0,
},
{
opacity: 1,
easing: "steps(2)",
offset: 0.1,
},
{
opacity: 0,
offset: 0.13,
},
], timing).pause();
document.querySelector("#target8").animate(
[
{
opacity: 1,
},
{
opacity: 1,
offset: 0.5,
},
{
opacity: 0,
offset: 0.5,
},
{
opacity: 1,
offset: 1,
},
], timing).pause();
</script>
</body>
</html>

View file

@ -1,66 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
html, body {
margin: 0;
height: 100%;
overflow: hidden;
}
div {
position: absolute;
top: 0;
left: -500px;
height: 20px;
width: 500px;
color: red;
background: linear-gradient(to left, currentColor, currentColor 2px, transparent);
}
.zero {
color: blue;
top: 20px;
}
.positive {
color: green;
top: 40px;
}
.negative.move { animation: 5s -1s move linear forwards; }
.zero.move { animation: 5s 0s move linear forwards; }
.positive.move { animation: 5s 1s move linear forwards; }
@keyframes move {
to {
transform: translateX(500px);
}
}
</style>
</head>
<body>
<div class="negative"></div>
<div class="zero"></div>
<div class="positive"></div>
<script>
"use strict";
var negative = document.querySelector(".negative");
var zero = document.querySelector(".zero");
var positive = document.querySelector(".positive");
// The non-delayed animation starts now.
zero.classList.add("move");
// The negative-delayed animation starts in 1 second.
setTimeout(function() {
negative.classList.add("move");
}, 1000);
// The positive-delayed animation starts in 200 ms.
setTimeout(function() {
positive.classList.add("move");
}, 200);
</script>
</body>
</html>

View file

@ -1,61 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Animated pseudo elements</title>
<style>
html, body {
margin: 0;
height: 100%;
width: 100%;
overflow: hidden;
display: flex;
justify-content: center;
align-items: flex-end;
}
body {
animation: color 2s linear infinite;
background: #333;
}
@keyframes color {
to {
filter: hue-rotate(360deg);
}
}
body::before,
body::after {
content: "";
flex-grow: 1;
height: 100%;
animation: grow 1s linear infinite alternate;
}
body::before {
background: hsl(120, 80%, 80%);
}
body::after {
background: hsl(240, 80%, 80%);
animation-delay: -.5s;
}
@keyframes grow {
0% {height: 100%; animation-timing-function: ease-in-out;}
10% {height: 80%; animation-timing-function: ease-in-out;}
20% {height: 60%; animation-timing-function: ease-in-out;}
30% {height: 70%; animation-timing-function: ease-in-out;}
40% {height: 50%; animation-timing-function: ease-in-out;}
50% {height: 30%; animation-timing-function: ease-in-out;}
60% {height: 80%; animation-timing-function: ease-in-out;}
70% {height: 90%; animation-timing-function: ease-in-out;}
80% {height: 70%; animation-timing-function: ease-in-out;}
90% {height: 60%; animation-timing-function: ease-in-out;}
100% {height: 100%; animation-timing-function: ease-in-out;}
}
</style>
</head>
<body>
</body>
</html>

View file

@ -1,90 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
#target1 {
width: 50px;
height: 50px;
background: red;
}
#target2 {
width: 50px;
height: 50px;
background: green;
}
#target3 {
width: 50px;
height: 50px;
background: blue;
}
#target4 {
width: 50px;
height: 50px;
background: pink;
}
</style>
</head>
<body>
<div id="target1"></div>
<div id="target2"></div>
<div id="target3"></div>
<div id="target4"></div>
<script>
/* globals KeyframeEffect, Animation */
"use strict";
const animations = [{
id: "target1",
frames: [{ opacity: 0, offset: 0 }, { opacity: 1, offset: 1 }],
timing: {
duration: 100,
iterations: 2,
iterationStart: 0.25,
fill: "both"
}
}, {
id: "target2",
frames: [{ opacity: 0, offset: 0 }, { opacity: 1, offset: 1 }],
timing: {
duration: 100,
iterations: 1,
iterationStart: 0.25,
fill: "both"
}
}, {
id: "target3",
frames: [{ opacity: 0, offset: 0 }, { opacity: 1, offset: 1 }],
timing: {
duration: 100,
iterations: 1.5,
iterationStart: 2.5,
fill: "both"
}
}, {
id: "target4",
frames: [{ transform: "translate(0px)", offset: 0 },
{ transform: "translate(100px)", offset: 1 }],
timing: {
duration: 100,
iterations: 2,
iterationStart: 0.5,
delay: 10,
fill: "both"
}
}];
for (const {id, frames, timing} of animations) {
const effect = new KeyframeEffect(document.getElementById(id),
frames, timing);
const animation = new Animation(effect, document.timeline);
animation.play();
animation.pause();
}
</script>
</body>
</html>

View file

@ -1,31 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
div {
display: inline-block;
width: 100px;
height: 100px;
background-color: lime;
}
</style>
</head>
<body>
<div id="onetime"></div>
<div id="twotimes"></div>
<div id="infinite"></div>
<script>
"use strict";
let target = document.querySelector("#onetime");
target.animate({ opacity: [0, 1] },
{ duration: 1, iterations: 1 }).pause();
target = document.querySelector("#twotimes");
target.animate({ opacity: [0, 1] },
{ duration: 1, iterations: 2 }).pause();
target = document.querySelector("#infinite");
target.animate({ opacity: [0, 1] },
{ duration: 1, iterations: Infinity }).pause();
</script>
</body>
</html>

View file

@ -1,158 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
.ball {
width: 80px;
height: 80px;
/* Add a border here to avoid layout warnings in Linux debug builds: Bug 1329784 */
border: 1px solid transparent;
border-radius: 50%;
background: #f06;
position: absolute;
}
.still {
top: 0;
left: 10px;
}
.animated {
top: 100px;
left: 10px;
animation: simple-animation 2s infinite alternate;
}
.multi {
top: 200px;
left: 10px;
animation: simple-animation 2s infinite alternate,
other-animation 5s infinite alternate;
}
.delayed {
top: 300px;
left: 10px;
background: rebeccapurple;
animation: simple-animation 3s 60s 10;
}
.multi-finite {
top: 400px;
left: 10px;
background: yellow;
animation: simple-animation 3s,
other-animation 4s;
}
.short {
top: 500px;
left: 10px;
background: red;
animation: simple-animation 2s normal;
}
.long {
top: 600px;
left: 10px;
background: blue;
animation: simple-animation 120s;
}
.negative-delay {
top: 700px;
left: 10px;
background: gray;
animation: simple-animation 15s -10s;
animation-fill-mode: forwards;
}
.no-compositor {
top: 0;
right: 10px;
background: gold;
animation: no-compositor 10s cubic-bezier(.57,-0.02,1,.31) forwards;
}
.compositor-all {
animation: compositor-all 2s infinite;
}
.compositor-notall {
animation: compositor-notall 2s infinite;
}
@keyframes simple-animation {
100% {
transform: translateX(300px);
}
}
@keyframes other-animation {
100% {
background: blue;
}
}
@keyframes no-compositor {
100% {
margin-right: 600px;
}
}
@keyframes compositor-all {
to { opacity: 0.5 }
}
@keyframes compositor-notall {
from {
opacity: 0;
width: 0px;
transform: translate(0px);
}
to {
opacity: 1;
width: 100px;
transform: translate(100px);
}
}
</style>
</head>
<body>
<!-- Comment node -->
<div class="ball still"></div>
<div class="ball animated"></div>
<div class="ball multi"></div>
<div class="ball delayed"></div>
<div class="ball multi-finite"></div>
<div class="ball short"></div>
<div class="ball long"></div>
<div class="ball negative-delay"></div>
<div class="ball no-compositor"></div>
<div class="ball" id="endDelayed"></div>
<div class="ball compositor-all"></div>
<div class="ball compositor-notall"></div>
<script>
/* globals KeyframeEffect, Animation */
"use strict";
var el = document.getElementById("endDelayed");
const effect = new KeyframeEffect(el, [
{ opacity: 0, offset: 0 },
{ opacity: 1, offset: 1 }
], { duration: 1000000, endDelay: 500000, fill: "none" });
const animation = new Animation(effect, document.timeline);
animation.play();
</script>
</body>
</html>

View file

@ -1,35 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
div {
display: inline-block;
width: 100px;
height: 100px;
background-color: lime;
}
</style>
</head>
<body>
<script>
"use strict";
const delayList = [0, 50000, -50000];
const endDelayList = [0, 50000, -50000];
delayList.forEach(delay => {
endDelayList.forEach(endDelay => {
const el = document.createElement("div");
document.body.appendChild(el);
el.animate({ opacity: [0, 1] },
{ duration: 200000,
iterations: 1,
fill: "both",
delay: delay,
endDelay: endDelay });
});
});
</script>
</body>
</html>

View file

@ -1,632 +0,0 @@
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/* eslint no-unused-vars: [2, {"vars": "local", "args": "none"}] */
"use strict";
/* import-globals-from ../../test/head.js */
// Import the inspector's head.js first (which itself imports shared-head.js).
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/devtools/client/inspector/test/head.js",
this);
const FRAME_SCRIPT_URL = CHROME_URL_ROOT + "doc_frame_script.js";
const TAB_NAME = "animationinspector";
const ANIMATION_L10N =
new LocalizationHelper("devtools/client/locales/animationinspector.properties");
// Auto clean-up when a test ends
registerCleanupFunction(async function() {
await closeAnimationInspector();
while (gBrowser.tabs.length > 1) {
gBrowser.removeCurrentTab();
}
});
// Disable new animation inspector.
Services.prefs.setBoolPref("devtools.new-animationinspector.enabled", false);
// Clean-up all prefs that might have been changed during a test run
// (safer here because if the test fails, then the pref is never reverted)
registerCleanupFunction(() => {
Services.prefs.clearUserPref("devtools.new-animationinspector.enabled");
Services.prefs.clearUserPref("devtools.debugger.log");
});
// Some animation features are not enabled by default in release/beta channels
// yet including:
// * parts of the Web Animations API (Bug 1264101), and
// * the frames() timing function (Bug 1379582).
function enableAnimationFeatures() {
return new Promise(resolve => {
SpecialPowers.pushPrefEnv({"set": [
["dom.animations-api.core.enabled", true],
["dom.animations-api.getAnimations.enabled", true],
["dom.animations-api.implicit-keyframes.enabled", true],
["dom.animations-api.timelines.enabled", true],
["layout.css.frames-timing.enabled", true],
]}, resolve);
});
}
/**
* Add a new test tab in the browser and load the given url.
* @param {String} url The url to be loaded in the new tab
* @return a promise that resolves to the tab object when the url is loaded
*/
var _addTab = addTab;
addTab = function(url) {
return enableAnimationFeatures().then(() => _addTab(url)).then(tab => {
const browser = tab.linkedBrowser;
info("Loading the helper frame script " + FRAME_SCRIPT_URL);
browser.messageManager.loadFrameScript(FRAME_SCRIPT_URL, false);
loadFrameScriptUtils(browser);
return tab;
});
};
/**
* Reload the current tab location.
* @param {InspectorPanel} inspector The instance of InspectorPanel currently
* loaded in the toolbox
*/
async function reloadTab(inspector) {
const onNewRoot = inspector.once("new-root");
await executeInContent("devtools:test:reload", {}, {}, false);
await onNewRoot;
await inspector.once("inspector-updated");
}
/*
* Set the inspector's current selection to a node or to the first match of the
* given css selector and wait for the animations to be displayed
* @param {String|NodeFront}
* data The node to select
* @param {InspectorPanel} inspector
* The instance of InspectorPanel currently
* loaded in the toolbox
* @param {String} reason
* Defaults to "test" which instructs the inspector not
* to highlight the node upon selection
* @return {Promise} Resolves when the inspector is updated with the new node
and animations of its subtree are properly displayed.
*/
var selectNodeAndWaitForAnimations = async function(data, inspector, reason = "test") {
// We want to make sure the rest of the test waits for the animations to
// be properly displayed (wait for all target DOM nodes to be previewed).
const {AnimationsController, AnimationsPanel} =
inspector.sidebar.getWindowForTab(TAB_NAME);
const onUiUpdated = AnimationsPanel.once(AnimationsPanel.UI_UPDATED_EVENT);
await selectNode(data, inspector, reason);
await onUiUpdated;
if (AnimationsController.animationPlayers.length !== 0) {
await waitForAnimationTimelineRendering(AnimationsPanel);
await waitForAllAnimationTargets(AnimationsPanel);
}
};
/**
* Check if there are the expected number of animations being displayed in the
* panel right now.
* @param {AnimationsPanel} panel
* @param {Number} nbAnimations The expected number of animations.
* @param {String} msg An optional string to be used as the assertion message.
*/
function assertAnimationsDisplayed(panel, nbAnimations, msg = "") {
msg = msg || `There are ${nbAnimations} animations in the panel`;
is(panel.animationsTimelineComponent
.animationsEl
.querySelectorAll(".animation").length, nbAnimations, msg);
}
/**
* Takes an Inspector panel that was just created, and waits
* for a "inspector-updated" event as well as the animation inspector
* sidebar to be ready. Returns a promise once these are completed.
*
* @param {InspectorPanel} inspector
* @return {Promise}
*/
var waitForAnimationInspectorReady = async function(inspector) {
const win = inspector.sidebar.getWindowForTab(TAB_NAME);
const updated = inspector.once("inspector-updated");
// In e10s, if we wait for underlying toolbox actors to
// load (by setting DevToolsUtils.testing to true), we miss the
// "animationinspector-ready" event on the sidebar, so check to see if the
// iframe is already loaded.
const tabReady = win.document.readyState === "complete" ?
promise.resolve() :
inspector.sidebar.once("animationinspector-ready");
return promise.all([updated, tabReady]);
};
/**
* Open the toolbox, with the inspector tool visible and the animationinspector
* sidebar selected.
* @return a promise that resolves when the inspector is ready.
*/
var openAnimationInspector = async function() {
const {inspector, toolbox} = await openInspectorSidebarTab(TAB_NAME);
info("Waiting for the inspector and sidebar to be ready");
await waitForAnimationInspectorReady(inspector);
const win = inspector.sidebar.getWindowForTab(TAB_NAME);
const {AnimationsController, AnimationsPanel} = win;
info("Waiting for the animation controller and panel to be ready");
if (AnimationsPanel.initialized) {
await AnimationsPanel.initialized;
} else {
await AnimationsPanel.once(AnimationsPanel.PANEL_INITIALIZED);
}
if (AnimationsController.animationPlayers.length !== 0) {
await waitForAnimationTimelineRendering(AnimationsPanel);
await waitForAllAnimationTargets(AnimationsPanel);
}
return {
toolbox: toolbox,
inspector: inspector,
controller: AnimationsController,
panel: AnimationsPanel,
window: win
};
};
/**
* Close the toolbox.
* @return a promise that resolves when the toolbox has closed.
*/
var closeAnimationInspector = async function() {
const target = TargetFactory.forTab(gBrowser.selectedTab);
await gDevTools.closeToolbox(target);
};
/**
* Wait for a content -> chrome message on the message manager (the window
* messagemanager is used).
* @param {String} name The message name
* @return {Promise} A promise that resolves to the response data when the
* message has been received
*/
function waitForContentMessage(name) {
info("Expecting message " + name + " from content");
const mm = gBrowser.selectedBrowser.messageManager;
return new Promise(resolve => {
mm.addMessageListener(name, function onMessage(msg) {
mm.removeMessageListener(name, onMessage);
resolve(msg.data);
});
});
}
/**
* Send an async message to the frame script (chrome -> content) and wait for a
* response message with the same name (content -> chrome).
* @param {String} name The message name. Should be one of the messages defined
* in doc_frame_script.js
* @param {Object} data Optional data to send along
* @param {Object} objects Optional CPOW objects to send along
* @param {Boolean} expectResponse If set to false, don't wait for a response
* with the same name from the content script. Defaults to true.
* @return {Promise} Resolves to the response data if a response is expected,
* immediately resolves otherwise
*/
function executeInContent(name, data = {}, objects = {},
expectResponse = true) {
info("Sending message " + name + " to content");
const mm = gBrowser.selectedBrowser.messageManager;
mm.sendAsyncMessage(name, data, objects);
if (expectResponse) {
return waitForContentMessage(name);
}
return promise.resolve();
}
/**
* Get the current playState of an animation player on a given node.
*/
var getAnimationPlayerState = async function(selector,
animationIndex = 0) {
const playState = await executeInContent("Test:GetAnimationPlayerState",
{selector, animationIndex});
return playState;
};
/**
* Is the given node visible in the page (rendered in the frame tree).
* @param {DOMNode}
* @return {Boolean}
*/
function isNodeVisible(node) {
return !!node.getClientRects().length;
}
/**
* Wait for all AnimationTargetNode instances to be fully loaded
* (fetched their related actor and rendered), and return them.
* This method should be called after "animation-timeline-rendering-completed" is emitted,
* since we get all the AnimationTargetNode instances using getAnimationTargetNodes().
* @param {AnimationsPanel} panel
* @return {Array} all AnimationTargetNode instances
*/
var waitForAllAnimationTargets = async function(panel) {
const targets = getAnimationTargetNodes(panel);
await promise.all(targets.map(t => {
if (!t.previewer.nodeFront) {
return t.once("target-retrieved");
}
return false;
}));
return targets;
};
/**
* Check the scrubber element in the timeline is moving.
* @param {AnimationPanel} panel
* @param {Boolean} isMoving
*/
async function assertScrubberMoving(panel, isMoving) {
const timeline = panel.animationsTimelineComponent;
if (isMoving) {
// If we expect the scrubber to move, just wait for a couple of
// timeline-data-changed events and compare times.
const {time: time1} = await timeline.once("timeline-data-changed");
const {time: time2} = await timeline.once("timeline-data-changed");
ok(time2 > time1, "The scrubber is moving");
} else {
// If instead we expect the scrubber to remain at its position, just wait
// for some time and make sure timeline-data-changed isn't emitted.
let hasMoved = false;
timeline.once("timeline-data-changed", () => {
hasMoved = true;
});
await new Promise(r => setTimeout(r, 500));
ok(!hasMoved, "The scrubber is not moving");
}
}
/**
* Click the play/pause button in the timeline toolbar and wait for animations
* to update.
* @param {AnimationsPanel} panel
*/
async function clickTimelinePlayPauseButton(panel) {
const onUiUpdated = panel.once(panel.UI_UPDATED_EVENT);
const onRendered = waitForAnimationTimelineRendering(panel);
const btn = panel.playTimelineButtonEl;
const win = btn.ownerDocument.defaultView;
EventUtils.sendMouseEvent({type: "click"}, btn, win);
await onUiUpdated;
await onRendered;
await waitForAllAnimationTargets(panel);
}
/**
* Click the rewind button in the timeline toolbar and wait for animations to
* update.
* @param {AnimationsPanel} panel
*/
async function clickTimelineRewindButton(panel) {
const onUiUpdated = panel.once(panel.UI_UPDATED_EVENT);
const onRendered = waitForAnimationTimelineRendering(panel);
const btn = panel.rewindTimelineButtonEl;
const win = btn.ownerDocument.defaultView;
EventUtils.sendMouseEvent({type: "click"}, btn, win);
await onUiUpdated;
await onRendered;
await waitForAllAnimationTargets(panel);
}
/**
* Select a rate inside the playback rate selector in the timeline toolbar and
* wait for animations to update.
* @param {AnimationsPanel} panel
* @param {Number} rate The new rate value to be selected
*/
async function changeTimelinePlaybackRate(panel, rate) {
const onUiUpdated = panel.once(panel.UI_UPDATED_EVENT);
const select = panel.rateSelectorEl.firstChild;
const win = select.ownerDocument.defaultView;
// Get the right option.
const option = [...select.options].filter(o => o.value === rate + "")[0];
if (!option) {
ok(false,
"Could not find an option for rate " + rate + " in the rate selector. " +
"Values are: " + [...select.options].map(o => o.value));
return;
}
// Simulate the right events to select the option in the drop-down.
EventUtils.synthesizeMouseAtCenter(select, {type: "mousedown"}, win);
EventUtils.synthesizeMouseAtCenter(option, {type: "mouseup"}, win);
await onUiUpdated;
await waitForAnimationTimelineRendering(panel);
await waitForAllAnimationTargets(panel);
// Simulate a mousemove outside of the rate selector area to avoid subsequent
// tests from failing because of unwanted mouseover events.
EventUtils.synthesizeMouseAtCenter(
win.document.querySelector("#timeline-toolbar"), {type: "mousemove"}, win);
}
/**
* Wait for animation selecting.
* @param {AnimationsPanel} panel
*/
async function waitForAnimationSelecting(panel) {
await panel.animationsTimelineComponent.once("animation-selected");
}
/**
* Wait for rendering animation timeline.
* @param {AnimationsPanel} panel
*/
function waitForAnimationTimelineRendering(panel) {
return panel.animationsTimelineComponent.once("animation-timeline-rendering-completed");
}
/**
+ * Click the timeline header to update the animation current time.
+ * @param {AnimationsPanel} panel
+ * @param {Number} x position rate on timeline header.
+ * This method calculates
+ * `position * offsetWidth + offsetLeft of timeline header`
+ * as the clientX of MouseEvent.
+ * This parameter should be from 0.0 to 1.0.
+ */
async function clickOnTimelineHeader(panel, position) {
const timeline = panel.animationsTimelineComponent;
const onTimelineDataChanged = timeline.once("timeline-data-changed");
const header = timeline.timeHeaderEl;
const clientX = header.offsetLeft + header.offsetWidth * position;
EventUtils.sendMouseEvent({ type: "mousedown", clientX: clientX },
header, header.ownerDocument.defaultView);
info(`Click at (${ clientX }, 0) on timeline header`);
EventUtils.sendMouseEvent({ type: "mouseup", clientX: clientX }, header,
header.ownerDocument.defaultView);
return onTimelineDataChanged;
}
/**
* Prevent the toolbox common highlighter from making backend requests.
* @param {Toolbox} toolbox
*/
function disableHighlighter(toolbox) {
toolbox._highlighter = {
showBoxModel: () => new Promise(r => r()),
hideBoxModel: () => new Promise(r => r()),
pick: () => new Promise(r => r()),
cancelPick: () => new Promise(r => r()),
destroy: () => {},
traits: {}
};
}
/**
* Click on an animation in the timeline to select/unselect it.
* @param {AnimationsPanel} panel The panel instance.
* @param {Number} index The index of the animation to click on.
* @param {Boolean} shouldAlreadySelected Set to true
* if the clicked animation is already selected.
* @return {Promise} resolves to the animation whose state has changed.
*/
async function clickOnAnimation(panel, index, shouldAlreadySelected) {
const timeline = panel.animationsTimelineComponent;
// Expect a selection event.
const onSelectionChanged = timeline.once(shouldAlreadySelected
? "animation-already-selected"
: "animation-selected");
info("Click on animation " + index + " in the timeline");
const timeBlock = timeline.rootWrapperEl.querySelectorAll(".time-block")[index];
// Scroll to show the timeBlock since the element may be out of displayed area.
timeBlock.scrollIntoView(false);
const timeBlockBounds = timeBlock.getBoundingClientRect();
let x = timeBlockBounds.width / 2;
const y = timeBlockBounds.height / 2;
if (timeBlock != timeBlock.ownerDocument.elementFromPoint(x, y)) {
// Move the mouse pointer a little since scrubber element may be at the point.
x += timeBlockBounds.width / 4;
}
EventUtils.synthesizeMouse(timeBlock, x, y, {}, timeBlock.ownerDocument.defaultView);
return onSelectionChanged;
}
/**
* Get an instance of the Keyframes component from the timeline.
* @param {AnimationsPanel} panel The panel instance.
* @param {String} propertyName The name of the animated property.
* @return {Keyframes} The Keyframes component instance.
*/
function getKeyframeComponent(panel, propertyName) {
const timeline = panel.animationsTimelineComponent;
const detailsComponent = timeline.details;
return detailsComponent.keyframeComponents
.find(c => c.propertyName === propertyName);
}
/**
* Get a keyframe element from the timeline.
* @param {AnimationsPanel} panel The panel instance.
* @param {String} propertyName The name of the animated property.
* @param {Index} keyframeIndex The index of the keyframe.
* @return {DOMNode} The keyframe element.
*/
function getKeyframeEl(panel, propertyName, keyframeIndex) {
const keyframeComponent = getKeyframeComponent(panel, propertyName);
return keyframeComponent.keyframesEl
.querySelectorAll(".frame")[keyframeIndex];
}
/**
* Set style to test document.
* @param {Animation} animation - animation object.
* @param {AnimationsPanel} panel - The panel instance.
* @param {String} name - property name.
* @param {String} value - property value.
* @param {String} selector - selector for test document.
*/
async function setStyle(animation, panel, name, value, selector) {
info("Change the animation style via the content DOM. Setting " +
name + " to " + value + " of " + selector);
const onAnimationChanged = animation ? once(animation, "changed") : Promise.resolve();
const onRendered = waitForAnimationTimelineRendering(panel);
await executeInContent("devtools:test:setStyle", {
selector: selector,
propertyName: name,
propertyValue: value
});
await onAnimationChanged;
await onRendered;
await waitForAllAnimationTargets(panel);
}
/**
* Graph shapes of summary and detail are constructed by <path> element.
* This function checks the vertex of path segments.
* Also, if needed, checks the color for <stop> element.
* @param pathEl - <path> element.
* @param duration - float as duration which pathEl represetns.
* @param hasClosePath - set true if the path shoud be closing.
* @param expectedValues - JSON object format. We can test the vertex and color.
* e.g.
* [
* // Test the vertex (x=0, y=0) should be passing through.
* { x: 0, y: 0 },
* { x: 0, y: 1 },
* // If we have to test the color as well,
* // we can write as following.
* { x: 500, y: 1, color: "rgb(0, 0, 255)" },
* { x: 1000, y: 1 }
* ]
*/
function assertPathSegments(pathEl, duration, hasClosePath, expectedValues) {
const pathSegList = pathEl.pathSegList;
ok(pathSegList, "The tested element should have pathSegList");
expectedValues.forEach(expectedValue => {
ok(isPassingThrough(pathSegList, expectedValue.x, expectedValue.y),
`The path segment of x ${ expectedValue.x }, y ${ expectedValue.y } `
+ `should be passing through`);
if (expectedValue.color) {
assertColor(pathEl.closest("svg"), expectedValue.x / duration, expectedValue.color);
}
});
if (hasClosePath) {
const closePathSeg = pathSegList.getItem(pathSegList.numberOfItems - 1);
is(closePathSeg.pathSegType, closePathSeg.PATHSEG_CLOSEPATH,
"The last segment should be close path");
}
}
/**
* Check the color for <stop> element.
* @param svgEl - <svg> element which has <stop> element.
* @param offset - float which represents the "offset" attribute of <stop>.
* @param expectedColor - e.g. rgb(0, 0, 255)
*/
function assertColor(svgEl, offset, expectedColor) {
const stopEl = findStopElement(svgEl, offset);
ok(stopEl, `stop element at offset ${ offset } should exist`);
is(stopEl.getAttribute("stop-color"), expectedColor,
`stop-color of stop element at offset ${ offset } should be ${ expectedColor }`);
}
/**
* Check whether the given vertex is passing throug on the path.
* @param pathSegList - pathSegList of <path> element.
* @param x - float x of vertex.
* @param y - float y of vertex.
* @return true: passing through, false: no on the path.
*/
function isPassingThrough(pathSegList, x, y) {
let previousPathSeg = pathSegList.getItem(0);
for (let i = 0; i < pathSegList.numberOfItems; i++) {
const pathSeg = pathSegList.getItem(i);
if (pathSeg.x === undefined) {
continue;
}
const currentX = parseFloat(pathSeg.x.toFixed(3));
const currentY = parseFloat(pathSeg.y.toFixed(6));
if (currentX === x && currentY === y) {
return true;
}
const previousX = parseFloat(previousPathSeg.x.toFixed(3));
const previousY = parseFloat(previousPathSeg.y.toFixed(6));
if (previousX <= x && x <= currentX &&
Math.min(previousY, currentY) <= y && y <= Math.max(previousY, currentY)) {
return true;
}
previousPathSeg = pathSeg;
}
return false;
}
/**
* Find <stop> element which has given offset from given <svg> element.
* @param svgEl - <svg> element which has <stop> element.
* @param offset - float which represents the "offset" attribute of <stop>.
* @return <stop> element.
*/
function findStopElement(svgEl, offset) {
for (const stopEl of svgEl.querySelectorAll("stop")) {
if (offset <= parseFloat(stopEl.getAttribute("offset"))) {
return stopEl;
}
}
return null;
}
/*
* Returns all AnimationTargetNode instances.
* This method should be called after emit "animation-timeline-rendering-completed".
* @param {AnimationsPanel} panel The panel instance.
* @return {Array} all AnimationTargetNode instances.
*/
function getAnimationTargetNodes(panel) {
return panel.animationsTimelineComponent.animations.map(animation => {
return panel.animationsTimelineComponent.componentsMap[animation.actorID].targetNode;
});
}
/*
* Returns all AnimationTargetBlock instances.
* This method should be called after emit "animation-timeline-rendering-completed".
* @param {AnimationsPanel} panel The panel instance.
* @return {Array} all AnimationTargetBlock instances.
*/
function getAnimationTimeBlocks(panel) {
return panel.animationsTimelineComponent.animations.map(animation => {
return panel.animationsTimelineComponent.componentsMap[animation.actorID].timeBlock;
});
}

View file

@ -1,6 +0,0 @@
"use strict";
module.exports = {
// Extend from the common devtools xpcshell eslintrc config.
"extends": "../../../../../.eslintrc.xpcshell.js"
};

View file

@ -1,80 +0,0 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/* eslint no-eval:0 */
"use strict";
const {require} = ChromeUtils.import("resource://devtools/shared/Loader.jsm", {});
const {findOptimalTimeInterval} = require("devtools/client/inspector/animation-old/utils");
// This test array contains objects that are used to test the
// findOptimalTimeInterval function. Each object should have the following
// properties:
// - desc: an optional string that will be printed out
// - minTimeInterval: a number that represents the minimum time in ms
// that should be displayed in one interval
// - expectedInterval: a number that you expect the findOptimalTimeInterval
// function to return as a result.
// Optionally you can pass a string where `interval` is the calculated
// interval, this string will be eval'd and tested to be truthy.
const TEST_DATA = [{
desc: "With no minTimeInterval, expect the interval to be 0",
minTimeInterval: null,
expectedInterval: 0
}, {
desc: "With a minTimeInterval of 0 ms, expect the interval to be 0",
minTimeInterval: 0,
expectedInterval: 0
}, {
desc: "With a minInterval of 1ms, expect the interval to be the 1ms too",
minTimeInterval: 1,
expectedInterval: 1
}, {
desc: "With a very small minTimeInterval, expect the interval to be 1ms",
minTimeInterval: 1e-31,
expectedInterval: 1
}, {
desc: "With a minInterval of 2.5ms, expect the interval to be 2.5ms too",
minTimeInterval: 2.5,
expectedInterval: 2.5
}, {
desc: "With a minInterval of 5ms, expect the interval to be 5ms too",
minTimeInterval: 5,
expectedInterval: 5
}, {
desc: "With a minInterval of 7ms, expect the interval to be the next " +
"multiple of 5",
minTimeInterval: 7,
expectedInterval: 10
}, {
minTimeInterval: 20,
expectedInterval: 25
}, {
minTimeInterval: 33,
expectedInterval: 50
}, {
minTimeInterval: 987,
expectedInterval: 1000
}, {
minTimeInterval: 1234,
expectedInterval: 2500
}, {
minTimeInterval: 9800,
expectedInterval: 10000
}];
function run_test() {
for (const {minTimeInterval, desc, expectedInterval} of TEST_DATA) {
info(`Testing minTimeInterval: ${minTimeInterval}.
Expecting ${expectedInterval}.`);
const interval = findOptimalTimeInterval(minTimeInterval);
if (typeof expectedInterval == "string") {
ok(eval(expectedInterval), desc);
} else {
equal(interval, expectedInterval, desc);
}
}
}

View file

@ -1,61 +0,0 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* 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 {require} = ChromeUtils.import("resource://devtools/shared/Loader.jsm", {});
const {formatStopwatchTime} = require("devtools/client/inspector/animation-old/utils");
const TEST_DATA = [{
desc: "Formatting 0",
time: 0,
expected: "00:00.000"
}, {
desc: "Formatting null",
time: null,
expected: "00:00.000"
}, {
desc: "Formatting undefined",
time: undefined,
expected: "00:00.000"
}, {
desc: "Formatting a small number of ms",
time: 13,
expected: "00:00.013"
}, {
desc: "Formatting a slightly larger number of ms",
time: 500,
expected: "00:00.500"
}, {
desc: "Formatting 1 second",
time: 1000,
expected: "00:01.000"
}, {
desc: "Formatting a number of seconds",
time: 1532,
expected: "00:01.532"
}, {
desc: "Formatting a big number of seconds",
time: 58450,
expected: "00:58.450"
}, {
desc: "Formatting 1 minute",
time: 60000,
expected: "01:00.000"
}, {
desc: "Formatting a number of minutes",
time: 263567,
expected: "04:23.567"
}, {
desc: "Formatting a large number of minutes",
time: 1000 * 60 * 60 * 3,
expected: "180:00.000"
}];
function run_test() {
for (const {desc, time, expected} of TEST_DATA) {
equal(formatStopwatchTime(time), expected, desc);
}
}

View file

@ -1,26 +0,0 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* 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 {require} = ChromeUtils.import("resource://devtools/shared/Loader.jsm", {});
const {getCssPropertyName} = require("devtools/client/inspector/animation-old/utils");
const TEST_DATA = [{
jsName: "alllowercase",
cssName: "alllowercase"
}, {
jsName: "borderWidth",
cssName: "border-width"
}, {
jsName: "borderTopRightRadius",
cssName: "border-top-right-radius"
}];
function run_test() {
for (const {jsName, cssName} of TEST_DATA) {
equal(getCssPropertyName(jsName), cssName);
}
}

View file

@ -1,206 +0,0 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* 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 {require} = ChromeUtils.import("resource://devtools/shared/Loader.jsm", {});
const {TimeScale} = require("devtools/client/inspector/animation-old/utils");
const TEST_ANIMATIONS = [{
desc: "Testing a few standard animations",
animations: [{
previousStartTime: 500,
delay: 0,
duration: 1000,
iterationCount: 1,
playbackRate: 1
}, {
previousStartTime: 400,
delay: 100,
duration: 10,
iterationCount: 100,
playbackRate: 1
}, {
previousStartTime: 50,
delay: 1000,
duration: 100,
iterationCount: 20,
playbackRate: 1
}],
expectedMinStart: 50,
expectedMaxEnd: 3050
}, {
desc: "Testing a single negative-delay animation",
animations: [{
previousStartTime: 100,
delay: -100,
duration: 100,
iterationCount: 1,
playbackRate: 1
}],
expectedMinStart: 0,
expectedMaxEnd: 100
}, {
desc: "Testing a single negative-delay animation with a different rate",
animations: [{
previousStartTime: 3500,
delay: -1000,
duration: 10000,
iterationCount: 2,
playbackRate: 2
}],
expectedMinStart: 3000,
expectedMaxEnd: 13000
}];
const TEST_STARTTIME_TO_DISTANCE = [{
time: 50,
expectedDistance: 0
}, {
time: 50,
expectedDistance: 0
}, {
time: 3050,
expectedDistance: 100
}, {
time: 1550,
expectedDistance: 50
}];
const TEST_DURATION_TO_DISTANCE = [{
time: 3000,
expectedDistance: 100
}, {
time: 0,
expectedDistance: 0
}];
const TEST_DISTANCE_TO_TIME = [{
distance: 100,
expectedTime: 3050
}, {
distance: 0,
expectedTime: 50
}, {
distance: 25,
expectedTime: 800
}];
const TEST_DISTANCE_TO_RELATIVE_TIME = [{
distance: 100,
expectedTime: 3000
}, {
distance: 0,
expectedTime: 0
}, {
distance: 25,
expectedTime: 750
}];
const TEST_FORMAT_TIME_MS = [{
time: 0,
expectedFormattedTime: "0ms"
}, {
time: 3540.341,
expectedFormattedTime: "3540ms"
}, {
time: 1.99,
expectedFormattedTime: "2ms"
}, {
time: 4000,
expectedFormattedTime: "4000ms"
}];
const TEST_FORMAT_TIME_S = [{
time: 0,
expectedFormattedTime: "0.0s"
}, {
time: 3540.341,
expectedFormattedTime: "3.5s"
}, {
time: 1.99,
expectedFormattedTime: "0.0s"
}, {
time: 4000,
expectedFormattedTime: "4.0s"
}, {
time: 102540,
expectedFormattedTime: "102.5s"
}, {
time: 102940,
expectedFormattedTime: "102.9s"
}];
function run_test() {
info("Check the default min/max range values");
equal(TimeScale.minStartTime, Infinity);
equal(TimeScale.maxEndTime, 0);
for (const {desc, animations, expectedMinStart, expectedMaxEnd} of
TEST_ANIMATIONS) {
info("Test adding a few animations: " + desc);
for (const state of animations) {
TimeScale.addAnimation(state);
}
info("Checking the time scale range");
equal(TimeScale.minStartTime, expectedMinStart);
equal(TimeScale.maxEndTime, expectedMaxEnd);
info("Test reseting the animations");
TimeScale.reset();
equal(TimeScale.minStartTime, Infinity);
equal(TimeScale.maxEndTime, 0);
}
info("Add a set of animations again");
for (const state of TEST_ANIMATIONS[0].animations) {
TimeScale.addAnimation(state);
}
info("Test converting start times to distances");
for (const {time, expectedDistance} of TEST_STARTTIME_TO_DISTANCE) {
const distance = TimeScale.startTimeToDistance(time);
equal(distance, expectedDistance);
}
info("Test converting durations to distances");
for (const {time, expectedDistance} of TEST_DURATION_TO_DISTANCE) {
const distance = TimeScale.durationToDistance(time);
equal(distance, expectedDistance);
}
info("Test converting distances to times");
for (const {distance, expectedTime} of TEST_DISTANCE_TO_TIME) {
const time = TimeScale.distanceToTime(distance);
equal(time, expectedTime);
}
info("Test converting distances to relative times");
for (const {distance, expectedTime} of TEST_DISTANCE_TO_RELATIVE_TIME) {
const time = TimeScale.distanceToRelativeTime(distance);
equal(time, expectedTime);
}
info("Test formatting times (millis)");
for (const {time, expectedFormattedTime} of TEST_FORMAT_TIME_MS) {
const formattedTime = TimeScale.formatTime(time);
equal(formattedTime, expectedFormattedTime);
}
// Add 1 more animation to increase the range and test more time formatting
// cases.
TimeScale.addAnimation({
startTime: 3000,
duration: 5000,
delay: 0,
iterationCount: 1
});
info("Test formatting times (seconds)");
for (const {time, expectedFormattedTime} of TEST_FORMAT_TIME_S) {
const formattedTime = TimeScale.formatTime(time);
equal(formattedTime, expectedFormattedTime);
}
}

View file

@ -1,53 +0,0 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const {require} = ChromeUtils.import("resource://devtools/shared/Loader.jsm", {});
const {TimeScale} = require("devtools/client/inspector/animation-old/utils");
const TEST_ENDDELAY_X = [{
desc: "Testing positive-endDelay animations",
animations: [{
previousStartTime: 0,
duration: 500,
playbackRate: 1,
iterationCount: 3,
delay: 500,
endDelay: 500
}],
expectedEndDelayX: 80
}, {
desc: "Testing negative-endDelay animations",
animations: [{
previousStartTime: 0,
duration: 500,
playbackRate: 1,
iterationCount: 9,
delay: 500,
endDelay: -500
}],
expectedEndDelayX: 90
}];
function run_test() {
info("Test calculating endDelayX");
// Be independent of possible prior tests
TimeScale.reset();
for (const {desc, animations, expectedEndDelayX} of TEST_ENDDELAY_X) {
info(`Adding animations: ${desc}`);
for (const state of animations) {
TimeScale.addAnimation(state);
const {endDelayX} = TimeScale.getAnimationDimensions({state});
equal(endDelayX, expectedEndDelayX);
TimeScale.reset();
}
}
}

View file

@ -1,11 +0,0 @@
[DEFAULT]
tags = devtools
head =
firefox-appdir = browser
skip-if = toolkit == 'android'
[test_findOptimalTimeInterval.js]
[test_formatStopwatchTime.js]
[test_getCssPropertyName.js]
[test_timeScale.js]
[test_timeScale_dimensions.js]

View file

@ -1,344 +0,0 @@
/* -*- 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 { LocalizationHelper } = require("devtools/shared/l10n");
const L10N =
new LocalizationHelper("devtools/client/locales/animationinspector.properties");
// How many times, maximum, can we loop before we find the optimal time
// interval in the timeline graph.
const OPTIMAL_TIME_INTERVAL_MAX_ITERS = 100;
// Time graduations should be multiple of one of these number.
const OPTIMAL_TIME_INTERVAL_MULTIPLES = [1, 2.5, 5];
const MILLIS_TIME_FORMAT_MAX_DURATION = 4000;
// SVG namespace
const SVG_NS = "http://www.w3.org/2000/svg";
/**
* DOM node creation helper function.
* @param {Object} Options to customize the node to be created.
* - nodeType {String} Optional, defaults to "div",
* - attributes {Object} Optional attributes object like
* {attrName1:value1, attrName2: value2, ...}
* - parent {DOMNode} Mandatory node to append the newly created node to.
* - textContent {String} Optional text for the node.
* - namespace {String} Optional namespace
* @return {DOMNode} The newly created node.
*/
function createNode(options) {
if (!options.parent) {
throw new Error("Missing parent DOMNode to create new node");
}
const type = options.nodeType || "div";
const node =
options.namespace
? options.parent.ownerDocument.createElementNS(options.namespace, type)
: options.parent.ownerDocument.createElement(type);
for (const name in options.attributes || {}) {
const value = options.attributes[name];
node.setAttribute(name, value);
}
if (options.textContent) {
node.textContent = options.textContent;
}
options.parent.appendChild(node);
return node;
}
exports.createNode = createNode;
/**
* SVG DOM node creation helper function.
* @param {Object} Options to customize the node to be created.
* - nodeType {String} Optional, defaults to "div",
* - attributes {Object} Optional attributes object like
* {attrName1:value1, attrName2: value2, ...}
* - parent {DOMNode} Mandatory node to append the newly created node to.
* - textContent {String} Optional text for the node.
* @return {DOMNode} The newly created node.
*/
function createSVGNode(options) {
options.namespace = SVG_NS;
return createNode(options);
}
exports.createSVGNode = createSVGNode;
/**
* Find the optimal interval between time graduations in the animation timeline
* graph based on a minimum time interval
* @param {Number} minTimeInterval Minimum time in ms in one interval
* @return {Number} The optimal interval time in ms
*/
function findOptimalTimeInterval(minTimeInterval) {
let numIters = 0;
let multiplier = 1;
if (!minTimeInterval) {
return 0;
}
let interval;
while (true) {
for (let i = 0; i < OPTIMAL_TIME_INTERVAL_MULTIPLES.length; i++) {
interval = OPTIMAL_TIME_INTERVAL_MULTIPLES[i] * multiplier;
if (minTimeInterval <= interval) {
return interval;
}
}
if (++numIters > OPTIMAL_TIME_INTERVAL_MAX_ITERS) {
return interval;
}
multiplier *= 10;
}
}
exports.findOptimalTimeInterval = findOptimalTimeInterval;
/**
* Format a timestamp (in ms) as a mm:ss.mmm string.
* @param {Number} time
* @return {String}
*/
function formatStopwatchTime(time) {
// Format falsy values as 0
if (!time) {
return "00:00.000";
}
let milliseconds = parseInt(time % 1000, 10);
let seconds = parseInt((time / 1000) % 60, 10);
let minutes = parseInt((time / (1000 * 60)), 10);
const pad = (nb, max) => {
if (nb < max) {
return new Array((max + "").length - (nb + "").length + 1).join("0") + nb;
}
return nb;
};
minutes = pad(minutes, 10);
seconds = pad(seconds, 10);
milliseconds = pad(milliseconds, 100);
return `${minutes}:${seconds}.${milliseconds}`;
}
exports.formatStopwatchTime = formatStopwatchTime;
/**
* The TimeScale helper object is used to know which size should something be
* displayed with in the animation panel, depending on the animations that are
* currently displayed.
* If there are 5 animations displayed, and the first one starts at 10000ms and
* the last one ends at 20000ms, then this helper can be used to convert any
* time in this range to a distance in pixels.
*
* For the helper to know how to convert, it needs to know all the animations.
* Whenever a new animation is added to the panel, addAnimation(state) should be
* called. reset() can be called to start over.
*/
var TimeScale = {
minStartTime: Infinity,
maxEndTime: 0,
/**
* Add a new animation to time scale.
* @param {Object} state A PlayerFront.state object.
*/
addAnimation: function(state) {
let {previousStartTime, delay, duration, endDelay,
iterationCount, playbackRate} = state;
endDelay = typeof endDelay === "undefined" ? 0 : endDelay;
const toRate = v => v / playbackRate;
const minZero = v => Math.max(v, 0);
const rateRelativeDuration =
toRate(duration * (!iterationCount ? 1 : iterationCount));
// Negative-delayed animations have their startTimes set such that we would
// be displaying the delay outside the time window if we didn't take it into
// account here.
const relevantDelay = delay < 0 ? toRate(delay) : 0;
previousStartTime = previousStartTime || 0;
const startTime = toRate(minZero(delay)) +
rateRelativeDuration +
endDelay;
this.minStartTime = Math.min(
this.minStartTime,
previousStartTime +
relevantDelay +
Math.min(startTime, 0)
);
const length = toRate(delay) +
rateRelativeDuration +
toRate(minZero(endDelay));
const endTime = previousStartTime + length;
this.maxEndTime = Math.max(this.maxEndTime, endTime);
},
/**
* Reset the current time scale.
*/
reset: function() {
this.minStartTime = Infinity;
this.maxEndTime = 0;
},
/**
* Convert a startTime to a distance in %, in the current time scale.
* @param {Number} time
* @return {Number}
*/
startTimeToDistance: function(time) {
time -= this.minStartTime;
return this.durationToDistance(time);
},
/**
* Convert a duration to a distance in %, in the current time scale.
* @param {Number} time
* @return {Number}
*/
durationToDistance: function(duration) {
return duration * 100 / this.getDuration();
},
/**
* Convert a distance in % to a time, in the current time scale.
* @param {Number} distance
* @return {Number}
*/
distanceToTime: function(distance) {
return this.minStartTime + (this.getDuration() * distance / 100);
},
/**
* Convert a distance in % to a time, in the current time scale.
* The time will be relative to the current minimum start time.
* @param {Number} distance
* @return {Number}
*/
distanceToRelativeTime: function(distance) {
const time = this.distanceToTime(distance);
return time - this.minStartTime;
},
/**
* Depending on the time scale, format the given time as milliseconds or
* seconds.
* @param {Number} time
* @return {String} The formatted time string.
*/
formatTime: function(time) {
// Format in milliseconds if the total duration is short enough.
if (this.getDuration() <= MILLIS_TIME_FORMAT_MAX_DURATION) {
return L10N.getFormatStr("timeline.timeGraduationLabel", time.toFixed(0));
}
// Otherwise format in seconds.
return L10N.getFormatStr("player.timeLabel", (time / 1000).toFixed(1));
},
getDuration: function() {
return this.maxEndTime - this.minStartTime;
},
/**
* Given an animation, get the various dimensions (in %) useful to draw the
* animation in the timeline.
*/
getAnimationDimensions: function({state}) {
const start = state.previousStartTime || 0;
const duration = state.duration;
const rate = state.playbackRate;
const count = state.iterationCount;
const delay = state.delay || 0;
const endDelay = state.endDelay || 0;
// The start position.
const x = this.startTimeToDistance(start + (delay / rate));
// The width for a single iteration.
const w = this.durationToDistance(duration / rate);
// The width for all iterations.
const iterationW = w * (count || 1);
// The start position of the delay.
const delayX = delay < 0 ? x : this.startTimeToDistance(start);
// The width of the delay.
const delayW = this.durationToDistance(Math.abs(delay) / rate);
// The width of the delay if it is negative, 0 otherwise.
const negativeDelayW = delay < 0 ? delayW : 0;
// The width of the endDelay.
const endDelayW = this.durationToDistance(Math.abs(endDelay) / rate);
// The start position of the endDelay.
const endDelayX = endDelay < 0 ? x + iterationW - endDelayW
: x + iterationW;
return {x, w, iterationW, delayX, delayW, negativeDelayW,
endDelayX, endDelayW};
}
};
exports.TimeScale = TimeScale;
/**
* Convert given CSS property name to JavaScript CSS name.
* @param {String} CSS property name (e.g. background-color).
* @return {String} JavaScript CSS property name (e.g. backgroundColor).
*/
function getJsPropertyName(cssPropertyName) {
if (cssPropertyName == "float") {
return "cssFloat";
}
// https://drafts.csswg.org/cssom/#css-property-to-idl-attribute
return cssPropertyName.replace(/-([a-z])/gi, (str, group) => {
return group.toUpperCase();
});
}
exports.getJsPropertyName = getJsPropertyName;
/**
* 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;
/**
* 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);
}
exports.getFormattedAnimationTitle = getFormattedAnimationTitle;

View file

@ -110,8 +110,8 @@ class AnimationInspector {
const provider = createElement(Provider,
{
id: "newanimationinspector",
key: "newanimationinspector",
id: "animationinspector",
key: "animationinspector",
store: this.inspector.store
},
App(
@ -275,7 +275,7 @@ class AnimationInspector {
isPanelVisible() {
return this.inspector && this.inspector.toolbox && this.inspector.sidebar &&
this.inspector.toolbox.currentToolId === "inspector" &&
this.inspector.sidebar.getCurrentTabID() === "newanimationinspector";
this.inspector.sidebar.getCurrentTabID() === "animationinspector";
}
onAnimationStateChanged() {
@ -625,7 +625,7 @@ class AnimationInspector {
}
async update() {
const done = this.inspector.updating("newanimationinspector");
const done = this.inspector.updating("animationinspector");
const selection = this.inspector.selection;
const animations =

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