import changes from `dev' branch of rmottola/Arctic-Fox:

- Bug 1163719 - Store startup cache entries in loading order; r=nfroyd (410ef75ff)
- Bug 1072313: P1. Make MacIOSurface refcount thread-safe. r=mattwoodrow (3be93ce91)
- Bug 1072313: P2 Prevent instanciating MacIOSurfaceLib directly. r=mattwoodrow (8d5d7e2e5)
- Bug 1178513 - Export libxul symbols needed by ACL. r=mattwoodrow (b01035079)
- Bug 1143575. Remove unused CompositableClient::OnTransaction. r=nical (8901f1fe8)
- Bug 1143575. Convert SetCurrentImage(nullptr) callers to call ClearAllImages instead. r=nical (1a89f04f0)
- Bug 1143575. Fix indent. r=cpearce (6b8f4e725)
- Bug 1179111 part 1 - Implement CSSAnimation.animationName; r=smaug, r=jwatt (0d27b5d2e)
- Bug 1179111 part 2 - Implement CSSTransition.transitionProperty; r=smaug, r=jwatt (f395dfc55)
- Bug 1144615 - 2 - Adds a playbackRate selector widget to the animation panel; r=vporof (d538eb734)
- Bug 1144615 - 1 - Minor css fixes in animation panel; r=bgrins (661a6feab)
- Bug 1144615 - 3 - Tests for the playbackRate selection UI in the animation panel; r=vporof (82a4b4f91)
- Bug 1120833 - 3 - Refresh the list of animation widgets when new animations are added; r=miker (f833abeb2)
- Bug 1153903 - Get rid of logspam during devtools talos test runs;r=pbrosset (77f1968d2)
- Bug 1155653 - Preview animation target nodes in animationinspector panel; r=bgrins (61f197d75)
- Bug 1151018 - Refresh the list of Animation widgets when the animation panel loads; r=bgrins (9351a0b64)
- Bug 1155663 - Show animations as synchronized time blocks in animation inspector; r=bgrins (66f544aea)
- Bug 1149999 - 2 - Send animation removed events to the animation-panel for re-starting transitions; r=past (2913b4d4b)
- Bug 1179111 part 3 - Make DevTools read the appropriate name property; r=pbrosset (6dfecd643)
- Bug 1179111 part 4 - Remove KeyframeEffectReadOnly.name; r=smaug (85894f42f)
- Bug 1004383 follow-up: Mark ElementPropertyTransition::AsTransition() as override (f37e36e8a)
- Bug 1179111 part 5 - Remove Name() methods; r=jwatt (8909c4781)
- Bug 1143575. Remove Theora-only duplicate frame optimization. r=cpearce (fe53385ec)
- Bug 1172825 - MDSM playback should depend on IsPlaying() instead of |mPlayState|. r=cpearce. (9a9cf656d)
- Bug 1163223 - Switch test_buffered to use timeupdate rather than loadedmetadata. r=cpearce (b91463faf)
- Bug 1163223 - Move bailout case in GetBuffered into the readers. r=jww (c875c1d71)
- Bug 1161901 - Dispatch MediaDecoderReader::SetIdle directly. r=jww (a9ad2582d)
- Bug 1161901 - Use ProxyMediaCall instead of MDSM::ShutdownReader. r=jww (c866b524c)
- Bug 1161901 - Hoist MDSM shutdown logic into MDSM::Shutdown and remove brittle requirement that RunStateMachine only happen once in SHUTDOWN state. r=jww (0d81368d6)
- Bug 1153149 - Remove IsWaitingMediaResources() from PlatformDecoderModule. r=jya (70bd67ee3)
- Bug 1161984 - Get rid of IsWaitingMediaResources() in MP4Reader. r=jya (94dd1f58c)
- Bug 1163223 - Introduce StartTimeRendezvous and route samples through it. r=jww (b2a80c47a)
- Bug 1163223 - Use AwaitStartTime to handle metadata end time. r=jww (8e22fc3fd)
- Bug 1163223 - Use AwaitStartTime to invoke MediaDecoderReader::SetStartTime. r=jww (6141f5303)
- Bug 1172387 - Clean up code of MediaDecoderStateMachine::StopAudioThread. r=kinetik. (717640128)
- Bug 1163223 - Adjust incoming samples for start time. r=jww (c4029f934)
This commit is contained in:
2021-03-25 09:18:42 +08:00
parent c34e72358b
commit 3fcb35e62d
97 changed files with 2766 additions and 784 deletions
@@ -55,7 +55,7 @@ let shutdown = Task.async(function*() {
yield AnimationsController.destroy();
// Don't assume that AnimationsPanel is defined here, it's in another file.
if (typeof AnimationsPanel !== "undefined") {
yield AnimationsPanel.destroy()
yield AnimationsPanel.destroy();
}
gToolbox = gInspector = null;
});
@@ -97,8 +97,11 @@ let AnimationsController = {
}
this.initialized = promise.defer();
this.onPanelVisibilityChange = this.onPanelVisibilityChange.bind(this);
this.onNewNodeFront = this.onNewNodeFront.bind(this);
this.onAnimationMutations = this.onAnimationMutations.bind(this);
let target = gToolbox.target;
this.animationsFront = new AnimationsFront(target.client, target.form);
// Expose actor capabilities.
this.hasToggleAll = yield target.actorHasMethod("animations", "toggleAll");
@@ -106,12 +109,19 @@ let AnimationsController = {
"setCurrentTime");
this.hasMutationEvents = yield target.actorHasMethod("animations",
"stopAnimationPlayerUpdates");
this.hasSetPlaybackRate = yield target.actorHasMethod("animationplayer",
"setPlaybackRate");
this.hasTargetNode = yield target.actorHasMethod("domwalker",
"getNodeFromActor");
this.isNewUI = Services.prefs.getBoolPref("devtools.inspector.animationInspectorV3");
this.onPanelVisibilityChange = this.onPanelVisibilityChange.bind(this);
this.onNewNodeFront = this.onNewNodeFront.bind(this);
if (this.destroyed) {
console.warn("Could not fully initialize the AnimationsController");
return;
}
this.animationsFront = new AnimationsFront(target.client, target.form);
this.startListeners();
yield this.onNewNodeFront();
this.initialized.resolve();
@@ -151,6 +161,9 @@ let AnimationsController = {
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() {
@@ -213,15 +226,55 @@ let AnimationsController = {
this.animationPlayers = yield this.animationsFront.getAnimationPlayersForNode(nodeFront);
this.startAllAutoRefresh();
// Start listening for animation mutations only after the first method call
// otherwise events won't be sent.
if (!this.isListeningToMutations && this.hasMutationEvents) {
this.animationsFront.on("mutations", this.onAnimationMutations);
this.isListeningToMutations = true;
}
}),
onAnimationMutations: Task.async(function*(changes) {
// Insert new players into this.animationPlayers when new animations are
// added.
for (let {type, player} of changes) {
if (type === "added") {
this.animationPlayers.push(player);
if (!this.isNewUI) {
player.startAutoRefresh();
}
}
if (type === "removed") {
if (!this.isNewUI) {
player.stopAutoRefresh();
}
yield player.release();
let index = this.animationPlayers.indexOf(player);
this.animationPlayers.splice(index, 1);
}
}
// Let the UI know the list has been updated.
this.emit(this.PLAYERS_UPDATED_EVENT, this.animationPlayers);
}),
startAllAutoRefresh: function() {
if (this.isNewUI) {
return;
}
for (let front of this.animationPlayers) {
front.startAutoRefresh();
}
},
stopAllAutoRefresh: function() {
if (this.isNewUI) {
return;
}
for (let front of this.animationPlayers) {
front.stopAutoRefresh();
}
@@ -19,7 +19,7 @@
<span class="label">&allAnimations;</span>
<button id="toggle-all" standalone="true" class="devtools-button"></button>
</div>
<div id="players" class="theme-toolbar"></div>
<div id="players"></div>
<div id="error-message">
<p>&invalidElement;</p>
<p>&selectElement;</p>
@@ -3,9 +3,19 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* globals AnimationsController, document, performance, promise,
gToolbox, gInspector, requestAnimationFrame, cancelAnimationFrame, L10N */
"use strict";
const {createNode} = require("devtools/animationinspector/utils");
const {
PlayerMetaDataHeader,
PlaybackRateSelector,
AnimationTargetNode,
AnimationsTimeline
} = require("devtools/animationinspector/components");
/**
* The main animations panel UI.
*/
@@ -14,6 +24,11 @@ let AnimationsPanel = {
PANEL_INITIALIZED: "panel-initialized",
initialize: Task.async(function*() {
if (AnimationsController.destroyed) {
console.warn("Could not initialize the animation-panel, controller " +
"was destroyed");
return;
}
if (this.initialized) {
return this.initialized.promise;
}
@@ -34,12 +49,19 @@ let AnimationsPanel = {
this.togglePicker = hUtils.togglePicker.bind(hUtils);
this.onPickerStarted = this.onPickerStarted.bind(this);
this.onPickerStopped = this.onPickerStopped.bind(this);
this.createPlayerWidgets = this.createPlayerWidgets.bind(this);
this.refreshAnimations = this.refreshAnimations.bind(this);
this.toggleAll = this.toggleAll.bind(this);
this.onTabNavigated = this.onTabNavigated.bind(this);
this.startListeners();
if (AnimationsController.isNewUI) {
this.animationsTimelineComponent = new AnimationsTimeline(gInspector);
this.animationsTimelineComponent.init(this.playersEl);
}
yield this.refreshAnimations();
this.initialized.resolve();
this.emit(this.PANEL_INITIALIZED);
@@ -56,6 +78,11 @@ let AnimationsPanel = {
this.destroyed = promise.defer();
this.stopListeners();
if (this.animationsTimelineComponent) {
this.animationsTimelineComponent.destroy();
this.animationsTimelineComponent = null;
}
yield this.destroyPlayerWidgets();
this.playersEl = this.errorMessageEl = null;
@@ -66,7 +93,7 @@ let AnimationsPanel = {
startListeners: function() {
AnimationsController.on(AnimationsController.PLAYERS_UPDATED_EVENT,
this.createPlayerWidgets);
this.refreshAnimations);
this.pickerButtonEl.addEventListener("click", this.togglePicker, false);
gToolbox.on("picker-started", this.onPickerStarted);
@@ -78,7 +105,7 @@ let AnimationsPanel = {
stopListeners: function() {
AnimationsController.off(AnimationsController.PLAYERS_UPDATED_EVENT,
this.createPlayerWidgets);
this.refreshAnimations);
this.pickerButtonEl.removeEventListener("click", this.togglePicker, false);
gToolbox.off("picker-started", this.onPickerStarted);
@@ -109,16 +136,18 @@ let AnimationsPanel = {
toggleAll: Task.async(function*() {
let btnClass = this.toggleAllButtonEl.classList;
// Toggling all animations is async and it may be some time before each of
// the current players get their states updated, so toggle locally too, to
// avoid the timelines from jumping back and forth.
if (this.playerWidgets) {
let currentWidgetStateChange = [];
for (let widget of this.playerWidgets) {
currentWidgetStateChange.push(btnClass.contains("paused")
? widget.play() : widget.pause());
if (!AnimationsController.isNewUI) {
// Toggling all animations is async and it may be some time before each of
// the current players get their states updated, so toggle locally too, to
// avoid the timelines from jumping back and forth.
if (this.playerWidgets) {
let currentWidgetStateChange = [];
for (let widget of this.playerWidgets) {
currentWidgetStateChange.push(btnClass.contains("paused")
? widget.play() : widget.pause());
}
yield promise.all(currentWidgetStateChange).catch(Cu.reportError);
}
yield promise.all(currentWidgetStateChange).catch(Cu.reportError);
}
btnClass.toggle("paused");
@@ -129,14 +158,21 @@ let AnimationsPanel = {
this.toggleAllButtonEl.classList.remove("paused");
},
createPlayerWidgets: Task.async(function*() {
refreshAnimations: Task.async(function*() {
let done = gInspector.updating("animationspanel");
// Empty the whole panel first.
this.hideErrorMessage();
yield this.destroyPlayerWidgets();
// If there are no players to show, show the error message instead and return.
// Re-render the timeline component.
if (this.animationsTimelineComponent) {
this.animationsTimelineComponent.render(
AnimationsController.animationPlayers);
}
// If there are no players to show, show the error message instead and
// return.
if (!AnimationsController.animationPlayers.length) {
this.displayErrorMessage();
this.emit(this.UI_UPDATED_EVENT);
@@ -144,17 +180,21 @@ let AnimationsPanel = {
return;
}
// Otherwise, create player widgets.
this.playerWidgets = [];
let initPromises = [];
// Otherwise, create player widgets (only when isNewUI is false, the
// timeline has already been re-rendered).
if (!AnimationsController.isNewUI) {
this.playerWidgets = [];
let initPromises = [];
for (let player of AnimationsController.animationPlayers) {
let widget = new PlayerWidget(player, this.playersEl);
initPromises.push(widget.initialize());
this.playerWidgets.push(widget);
for (let player of AnimationsController.animationPlayers) {
let widget = new PlayerWidget(player, this.playersEl);
initPromises.push(widget.initialize());
this.playerWidgets.push(widget);
}
yield initPromises;
}
yield initPromises;
this.emit(this.UI_UPDATED_EVENT);
done();
}),
@@ -187,8 +227,15 @@ function PlayerWidget(player, containerEl) {
this.onRewindBtnClick = this.onRewindBtnClick.bind(this);
this.onFastForwardBtnClick = this.onFastForwardBtnClick.bind(this);
this.onCurrentTimeChanged = this.onCurrentTimeChanged.bind(this);
this.onPlaybackRateChanged = this.onPlaybackRateChanged.bind(this);
this.metaDataComponent = new PlayerMetaDataHeader();
if (AnimationsController.hasSetPlaybackRate) {
this.rateComponent = new PlaybackRateSelector();
}
if (AnimationsController.hasTargetNode) {
this.targetNodeComponent = new AnimationTargetNode(gInspector);
}
}
PlayerWidget.prototype = {
@@ -211,6 +258,12 @@ PlayerWidget.prototype = {
this.stopTimelineAnimation();
this.stopListeners();
this.metaDataComponent.destroy();
if (this.rateComponent) {
this.rateComponent.destroy();
}
if (this.targetNodeComponent) {
this.targetNodeComponent.destroy();
}
this.el.remove();
this.playPauseBtnEl = this.rewindBtnEl = this.fastForwardBtnEl = null;
@@ -226,6 +279,9 @@ PlayerWidget.prototype = {
this.fastForwardBtnEl.addEventListener("click", this.onFastForwardBtnClick);
this.currentTimeEl.addEventListener("input", this.onCurrentTimeChanged);
}
if (this.rateComponent) {
this.rateComponent.on("rate-changed", this.onPlaybackRateChanged);
}
},
stopListeners: function() {
@@ -236,18 +292,27 @@ PlayerWidget.prototype = {
this.fastForwardBtnEl.removeEventListener("click", this.onFastForwardBtnClick);
this.currentTimeEl.removeEventListener("input", this.onCurrentTimeChanged);
}
if (this.rateComponent) {
this.rateComponent.off("rate-changed", this.onPlaybackRateChanged);
}
},
createMarkup: function() {
let state = this.player.state;
this.el = createNode({
parent: this.containerEl,
attributes: {
"class": "player-widget " + state.playState
}
});
this.metaDataComponent.createMarkup(this.el);
if (this.targetNodeComponent) {
this.targetNodeComponent.init(this.el);
this.targetNodeComponent.render(this.player);
}
this.metaDataComponent.init(this.el);
this.metaDataComponent.render(state);
// Timeline widget.
@@ -293,6 +358,11 @@ PlayerWidget.prototype = {
});
}
if (this.rateComponent) {
this.rateComponent.init(playbackControlsEl);
this.rateComponent.render(state);
}
// Sliders container.
let slidersContainerEl = createNode({
parent: timelineEl,
@@ -335,8 +405,6 @@ PlayerWidget.prototype = {
}
});
this.containerEl.appendChild(this.el);
// Show the initial time.
this.displayTime(state.currentTime);
},
@@ -351,9 +419,8 @@ PlayerWidget.prototype = {
onPlayPauseBtnClick: function() {
if (this.player.state.playState === "running") {
return this.pause();
} else {
return this.play();
}
return this.play();
},
onRewindBtnClick: function() {
@@ -365,7 +432,7 @@ PlayerWidget.prototype = {
let time = state.duration;
if (state.iterationCount) {
time = state.iterationCount * state.duration;
time = state.iterationCount * state.duration;
}
this.setCurrentTime(time, true);
},
@@ -378,19 +445,30 @@ PlayerWidget.prototype = {
this.setCurrentTime(parseFloat(time), true);
},
/**
* Executed when the playback rate dropdown value changes in the playbackrate
* component.
*/
onPlaybackRateChanged: function(e, rate) {
this.setPlaybackRate(rate);
},
/**
* Whenever a player state update is received.
*/
onStateChanged: function() {
let state = this.player.state;
this.updateWidgetState(state);
this.metaDataComponent.render(state);
if (this.rateComponent) {
this.rateComponent.render(state);
}
switch (state.playState) {
case "finished":
this.stopTimelineAnimation();
this.displayTime(this.player.state.duration);
this.stopListeners();
this.displayTime(this.player.state.currentTime);
break;
case "running":
this.startTimelineAnimation();
@@ -399,6 +477,10 @@ PlayerWidget.prototype = {
this.stopTimelineAnimation();
this.displayTime(this.player.state.currentTime);
break;
case "idle":
this.stopTimelineAnimation();
this.displayTime(0);
break;
}
},
@@ -410,7 +492,8 @@ PlayerWidget.prototype = {
*/
setCurrentTime: Task.async(function*(time, shouldPause) {
if (!AnimationsController.hasSetCurrentTime) {
throw new Error("This server version doesn't support setting animations' currentTime");
throw new Error("This server version doesn't support setting " +
"animations' currentTime");
}
if (shouldPause) {
@@ -429,16 +512,26 @@ PlayerWidget.prototype = {
yield this.player.setCurrentTime(time);
}),
/**
* Set the playback rate of the animation.
* @param {Number} rate.
* @return {Promise} Resolves when the rate has been set.
*/
setPlaybackRate: function(rate) {
if (!AnimationsController.hasSetPlaybackRate) {
throw new Error("This server version doesn't support setting " +
"animations' playbackRate");
}
return this.player.setPlaybackRate(rate);
},
/**
* Pause the animation player via this widget.
* @return {Promise} Resolves when the player is paused, the button is
* switched to the right state, and the timeline animation is stopped.
*/
pause: function() {
if (this.player.state.playState === "finished") {
return;
}
// Switch to the right className on the element right away to avoid waiting
// for the next state update to change the playPause icon.
this.updateWidgetState({playState: "paused"});
@@ -452,10 +545,6 @@ PlayerWidget.prototype = {
* switched to the right state, and the timeline animation is started.
*/
play: function() {
if (this.player.state.playState === "finished") {
return;
}
// Switch to the right className on the element right away to avoid waiting
// for the next state update to change the playPause icon.
this.updateWidgetState({playState: "running"});
@@ -479,7 +568,8 @@ PlayerWidget.prototype = {
let start = performance.now();
let loop = () => {
this.rafID = requestAnimationFrame(loop);
let now = state.currentTime + performance.now() - start;
let delta = (performance.now() - start) * state.playbackRate;
let now = state.currentTime + delta;
this.displayTime(now);
};
@@ -526,180 +616,3 @@ PlayerWidget.prototype = {
}
}
};
/**
* UI component responsible for displaying and updating the player meta-data:
* name, duration, iterations, delay.
* The parent UI component for this should drive its updates by calling
* render(state) whenever it wants the component to update.
*/
function PlayerMetaDataHeader() {
// Store the various state pieces we need to only refresh the UI when things
// change.
this.state = {};
}
PlayerMetaDataHeader.prototype = {
createMarkup: function(containerEl) {
// The main title element.
this.el = createNode({
parent: containerEl,
attributes: {
"class": "animation-title"
}
});
// Animation name.
this.nameLabel = createNode({
parent: this.el,
nodeType: "span"
});
this.nameValue = createNode({
parent: this.el,
nodeType: "strong",
attributes: {
"style": "display:none;"
}
});
// Animation duration, delay and iteration container.
let metaData = createNode({
parent: this.el,
nodeType: "span",
attributes: {
"class": "meta-data"
}
});
// Animation duration.
this.durationLabel = createNode({
parent: metaData,
nodeType: "span"
});
this.durationLabel.textContent = L10N.getStr("player.animationDurationLabel");
this.durationValue = createNode({
parent: metaData,
nodeType: "strong"
});
// Animation delay (hidden by default since there may not be a delay).
this.delayLabel = createNode({
parent: metaData,
nodeType: "span",
attributes: {
"style": "display:none;"
}
});
this.delayLabel.textContent = L10N.getStr("player.animationDelayLabel");
this.delayValue = createNode({
parent: metaData,
nodeType: "strong"
});
// Animation iteration count (also hidden by default since we don't display
// single iterations).
this.iterationLabel = createNode({
parent: metaData,
nodeType: "span",
attributes: {
"style": "display:none;"
}
});
this.iterationLabel.textContent = L10N.getStr("player.animationIterationCountLabel");
this.iterationValue = createNode({
parent: metaData,
nodeType: "strong",
attributes: {
"style": "display:none;"
}
});
},
destroy: function() {
this.state = null;
this.el.remove();
this.el = null;
this.nameLabel = this.nameValue = null;
this.durationLabel = this.durationValue = null;
this.delayLabel = this.delayValue = null;
this.iterationLabel = this.iterationValue = null;
},
render: function(state) {
// Update the name if needed.
if (state.name !== this.state.name) {
if (state.name) {
// Animations (and transitions since bug 1122414) have names.
this.nameLabel.textContent = L10N.getStr("player.animationNameLabel");
this.nameValue.style.display = "inline";
this.nameValue.textContent = state.name;
} else {
// With older actors, Css transitions don't have names.
this.nameLabel.textContent = L10N.getStr("player.transitionNameLabel");
this.nameValue.style.display = "none";
}
}
// update the duration value if needed.
if (state.duration !== this.state.duration) {
this.durationValue.textContent = L10N.getFormatStr("player.timeLabel",
L10N.numberWithDecimals(state.duration / 1000, 2));
}
// Update the delay if needed.
if (state.delay !== this.state.delay) {
if (state.delay) {
this.delayLabel.style.display = "inline";
this.delayValue.style.display = "inline";
this.delayValue.textContent = L10N.getFormatStr("player.timeLabel",
L10N.numberWithDecimals(state.delay / 1000, 2));
} else {
// Hide the delay elements if there is no delay defined.
this.delayLabel.style.display = "none";
this.delayValue.style.display = "none";
}
}
// Update the iterationCount if needed.
if (state.iterationCount !== this.state.iterationCount) {
if (state.iterationCount !== 1) {
this.iterationLabel.style.display = "inline";
this.iterationValue.style.display = "inline";
let count = state.iterationCount ||
L10N.getStr("player.infiniteIterationCount");
this.iterationValue.innerHTML = count;
} else {
// Hide the iteration elements if iteration is 1.
this.iterationLabel.style.display = "none";
this.iterationValue.style.display = "none";
}
}
this.state = state;
}
};
/**
* DOM node creation helper function.
* @param {Object} Options to customize the node to be created.
* @return {DOMNode} The newly created node.
*/
function createNode(options) {
let type = options.nodeType || "div";
let node = document.createElement(type);
for (let name in options.attributes || {}) {
let value = options.attributes[name];
node.setAttribute(name, value);
}
if (options.parent) {
options.parent.appendChild(node);
}
return node;
}
@@ -0,0 +1,826 @@
/* -*- 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/. */
/* globals ViewHelpers */
"use strict";
// Set of reusable UI components for the animation-inspector UI.
// All components in this module share a common API:
// 1. construct the component:
// let c = new ComponentName();
// 2. initialize the markup of the component in a given parent node:
// c.init(containerElement);
// 3. render the component, passing in some sort of state:
// This may be called over and over again when the state changes, to update
// the component output.
// c.render(state);
// 4. destroy the component:
// c.destroy();
const {Cu} = require("chrome");
Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
const {
createNode,
drawGraphElementBackground,
findOptimalTimeInterval
} = require("devtools/animationinspector/utils");
const STRINGS_URI = "chrome://browser/locale/devtools/animationinspector.properties";
const L10N = new ViewHelpers.L10N(STRINGS_URI);
const MILLIS_TIME_FORMAT_MAX_DURATION = 4000;
// The minimum spacing between 2 time graduation headers in the timeline (ms).
const TIME_GRADUATION_MIN_SPACING = 40;
/**
* UI component responsible for displaying and updating the player meta-data:
* name, duration, iterations, delay.
* The parent UI component for this should drive its updates by calling
* render(state) whenever it wants the component to update.
*/
function PlayerMetaDataHeader() {
// Store the various state pieces we need to only refresh the UI when things
// change.
this.state = {};
}
exports.PlayerMetaDataHeader = PlayerMetaDataHeader;
PlayerMetaDataHeader.prototype = {
init: function(containerEl) {
// The main title element.
this.el = createNode({
parent: containerEl,
attributes: {
"class": "animation-title"
}
});
// Animation name.
this.nameLabel = createNode({
parent: this.el,
nodeType: "span"
});
this.nameValue = createNode({
parent: this.el,
nodeType: "strong",
attributes: {
"style": "display:none;"
}
});
// Animation duration, delay and iteration container.
let metaData = createNode({
parent: this.el,
nodeType: "span",
attributes: {
"class": "meta-data"
}
});
// Animation duration.
this.durationLabel = createNode({
parent: metaData,
nodeType: "span",
textContent: L10N.getStr("player.animationDurationLabel")
});
this.durationValue = createNode({
parent: metaData,
nodeType: "strong"
});
// Animation delay (hidden by default since there may not be a delay).
this.delayLabel = createNode({
parent: metaData,
nodeType: "span",
attributes: {
"style": "display:none;"
},
textContent: L10N.getStr("player.animationDelayLabel")
});
this.delayValue = createNode({
parent: metaData,
nodeType: "strong"
});
// Animation iteration count (also hidden by default since we don't display
// single iterations).
this.iterationLabel = createNode({
parent: metaData,
nodeType: "span",
attributes: {
"style": "display:none;"
},
textContent: L10N.getStr("player.animationIterationCountLabel")
});
this.iterationValue = createNode({
parent: metaData,
nodeType: "strong",
attributes: {
"style": "display:none;"
}
});
},
destroy: function() {
this.state = null;
this.el.remove();
this.el = null;
this.nameLabel = this.nameValue = null;
this.durationLabel = this.durationValue = null;
this.delayLabel = this.delayValue = null;
this.iterationLabel = this.iterationValue = null;
},
render: function(state) {
// Update the name if needed.
if (state.name !== this.state.name) {
if (state.name) {
// Animations (and transitions since bug 1122414) have names.
this.nameLabel.textContent = L10N.getStr("player.animationNameLabel");
this.nameValue.style.display = "inline";
this.nameValue.textContent = state.name;
} else {
// With older actors, Css transitions don't have names.
this.nameLabel.textContent = L10N.getStr("player.transitionNameLabel");
this.nameValue.style.display = "none";
}
}
// update the duration value if needed.
if (state.duration !== this.state.duration) {
this.durationValue.textContent = L10N.getFormatStr("player.timeLabel",
L10N.numberWithDecimals(state.duration / 1000, 2));
}
// Update the delay if needed.
if (state.delay !== this.state.delay) {
if (state.delay) {
this.delayLabel.style.display = "inline";
this.delayValue.style.display = "inline";
this.delayValue.textContent = L10N.getFormatStr("player.timeLabel",
L10N.numberWithDecimals(state.delay / 1000, 2));
} else {
// Hide the delay elements if there is no delay defined.
this.delayLabel.style.display = "none";
this.delayValue.style.display = "none";
}
}
// Update the iterationCount if needed.
if (state.iterationCount !== this.state.iterationCount) {
if (state.iterationCount !== 1) {
this.iterationLabel.style.display = "inline";
this.iterationValue.style.display = "inline";
let count = state.iterationCount ||
L10N.getStr("player.infiniteIterationCount");
this.iterationValue.innerHTML = count;
} else {
// Hide the iteration elements if iteration is 1.
this.iterationLabel.style.display = "none";
this.iterationValue.style.display = "none";
}
}
this.state = state;
}
};
/**
* UI component responsible for displaying the playback rate drop-down in each
* player widget, updating it when the state changes, and emitting events when
* the user selects a new value.
* The parent UI component for this should drive its updates by calling
* render(state) whenever it wants the component to update.
*/
function PlaybackRateSelector() {
this.currentRate = null;
this.onSelectionChanged = this.onSelectionChanged.bind(this);
EventEmitter.decorate(this);
}
exports.PlaybackRateSelector = PlaybackRateSelector;
PlaybackRateSelector.prototype = {
PRESETS: [.1, .5, 1, 2, 5, 10],
init: function(containerEl) {
// This component is simple enough that we can re-create the markup every
// time it's rendered. So here we only store the parentEl.
this.parentEl = containerEl;
},
destroy: function() {
this.removeSelect();
this.parentEl = this.el = null;
},
removeSelect: function() {
if (this.el) {
this.el.removeEventListener("change", this.onSelectionChanged);
this.el.remove();
}
},
/**
* Get the ordered list of presets, including the current playbackRate if
* different from the existing presets.
*/
getCurrentPresets: function({playbackRate}) {
return [...new Set([...this.PRESETS, playbackRate])].sort((a, b) => a > b);
},
render: function(state) {
if (state.playbackRate === this.currentRate) {
return;
}
this.removeSelect();
this.el = createNode({
parent: this.parentEl,
nodeType: "select",
attributes: {
"class": "rate devtools-button"
}
});
for (let preset of this.getCurrentPresets(state)) {
let option = createNode({
parent: this.el,
nodeType: "option",
attributes: {
value: preset,
},
textContent: L10N.getFormatStr("player.playbackRateLabel", preset)
});
if (preset === state.playbackRate) {
option.setAttribute("selected", "");
}
}
this.el.addEventListener("change", this.onSelectionChanged);
this.currentRate = state.playbackRate;
},
onSelectionChanged: function() {
this.emit("rate-changed", parseFloat(this.el.value));
}
};
/**
* UI component responsible for displaying a preview of the target dom node of
* a given animation.
* @param {InspectorPanel} inspector Requires a reference to the inspector-panel
* to highlight and select the node, as well as refresh it when there are
* mutations.
* @param {Object} options Supported properties are:
* - compact {Boolean} Defaults to false. If true, nodes will be previewed like
* tag#id.class instead of <tag id="id" class="class">
*/
function AnimationTargetNode(inspector, options={}) {
this.inspector = inspector;
this.options = options;
this.onPreviewMouseOver = this.onPreviewMouseOver.bind(this);
this.onPreviewMouseOut = this.onPreviewMouseOut.bind(this);
this.onSelectNodeClick = this.onSelectNodeClick.bind(this);
this.onMarkupMutations = this.onMarkupMutations.bind(this);
EventEmitter.decorate(this);
}
exports.AnimationTargetNode = AnimationTargetNode;
AnimationTargetNode.prototype = {
init: function(containerEl) {
let document = containerEl.ownerDocument;
// Init the markup for displaying the target node.
this.el = createNode({
parent: containerEl,
attributes: {
"class": "animation-target"
}
});
// Icon to select the node in the inspector.
this.selectNodeEl = createNode({
parent: this.el,
nodeType: "span",
attributes: {
"class": "node-selector"
}
});
// Wrapper used for mouseover/out event handling.
this.previewEl = createNode({
parent: this.el,
nodeType: "span"
});
if (!this.options.compact) {
this.previewEl.appendChild(document.createTextNode("<"));
}
// Tag name.
this.tagNameEl = createNode({
parent: this.previewEl,
nodeType: "span",
attributes: {
"class": "tag-name theme-fg-color3"
}
});
// Id attribute container.
this.idEl = createNode({
parent: this.previewEl,
nodeType: "span"
});
if (!this.options.compact) {
createNode({
parent: this.idEl,
nodeType: "span",
attributes: {
"class": "attribute-name theme-fg-color2"
},
textContent: "id"
});
this.idEl.appendChild(document.createTextNode("=\""));
} else {
createNode({
parent: this.idEl,
nodeType: "span",
attributes: {
"class": "theme-fg-color2"
},
textContent: "#"
});
}
createNode({
parent: this.idEl,
nodeType: "span",
attributes: {
"class": "attribute-value theme-fg-color6"
}
});
if (!this.options.compact) {
this.idEl.appendChild(document.createTextNode("\""));
}
// Class attribute container.
this.classEl = createNode({
parent: this.previewEl,
nodeType: "span"
});
if (!this.options.compact) {
createNode({
parent: this.classEl,
nodeType: "span",
attributes: {
"class": "attribute-name theme-fg-color2"
},
textContent: "class"
});
this.classEl.appendChild(document.createTextNode("=\""));
} else {
createNode({
parent: this.classEl,
nodeType: "span",
attributes: {
"class": "theme-fg-color6"
},
textContent: "."
});
}
createNode({
parent: this.classEl,
nodeType: "span",
attributes: {
"class": "attribute-value theme-fg-color6"
}
});
if (!this.options.compact) {
this.classEl.appendChild(document.createTextNode("\""));
this.previewEl.appendChild(document.createTextNode(">"));
}
// Init events for highlighting and selecting the node.
this.previewEl.addEventListener("mouseover", this.onPreviewMouseOver);
this.previewEl.addEventListener("mouseout", this.onPreviewMouseOut);
this.selectNodeEl.addEventListener("click", this.onSelectNodeClick);
// Start to listen for markupmutation events.
this.inspector.on("markupmutation", this.onMarkupMutations);
},
destroy: function() {
this.inspector.off("markupmutation", this.onMarkupMutations);
this.previewEl.removeEventListener("mouseover", this.onPreviewMouseOver);
this.previewEl.removeEventListener("mouseout", this.onPreviewMouseOut);
this.selectNodeEl.removeEventListener("click", this.onSelectNodeClick);
this.el.remove();
this.el = this.tagNameEl = this.idEl = this.classEl = null;
this.selectNodeEl = this.previewEl = null;
this.nodeFront = this.inspector = this.playerFront = null;
},
onPreviewMouseOver: function() {
if (!this.nodeFront) {
return;
}
this.inspector.toolbox.highlighterUtils.highlightNodeFront(this.nodeFront);
},
onPreviewMouseOut: function() {
this.inspector.toolbox.highlighterUtils.unhighlight();
},
onSelectNodeClick: function() {
if (!this.nodeFront) {
return;
}
this.inspector.selection.setNodeFront(this.nodeFront, "animationinspector");
},
onMarkupMutations: function(e, mutations) {
if (!this.nodeFront || !this.playerFront) {
return;
}
for (let {target} of mutations) {
if (target === this.nodeFront) {
// Re-render with the same nodeFront to update the output.
this.render(this.playerFront);
break;
}
}
},
render: Task.async(function*(playerFront) {
this.playerFront = playerFront;
this.nodeFront = undefined;
try {
this.nodeFront = yield this.inspector.walker.getNodeFromActor(
playerFront.actorID, ["node"]);
} catch (e) {
// We might have been destroyed in the meantime, or the node might not be
// found.
if (!this.el) {
console.warn("Cound't retrieve the animation target node, widget " +
"destroyed");
}
console.error(e);
return;
}
if (!this.nodeFront || !this.el) {
return;
}
let {tagName, attributes} = this.nodeFront;
this.tagNameEl.textContent = tagName.toLowerCase();
let idIndex = attributes.findIndex(({name}) => name === "id");
if (idIndex > -1 && attributes[idIndex].value) {
this.idEl.querySelector(".attribute-value").textContent =
attributes[idIndex].value;
this.idEl.style.display = "inline";
} else {
this.idEl.style.display = "none";
}
let classIndex = attributes.findIndex(({name}) => name === "class");
if (classIndex > -1 && attributes[classIndex].value) {
let value = attributes[classIndex].value;
if (this.options.compact) {
value = value.split(" ").join(".");
}
this.classEl.querySelector(".attribute-value").textContent = value;
this.classEl.style.display = "inline";
} else {
this.classEl.style.display = "none";
}
this.emit("target-retrieved");
})
};
/**
* 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.
*/
let TimeScale = {
minStartTime: Infinity,
maxEndTime: 0,
/**
* Add a new animation to time scale.
* @param {Object} state A PlayerFront.state object.
*/
addAnimation: function({startTime, delay, duration, iterationCount}) {
this.minStartTime = Math.min(this.minStartTime, startTime);
let length = delay + (duration * (!iterationCount ? 1 : iterationCount));
this.maxEndTime = Math.max(this.maxEndTime, startTime + length);
},
/**
* Reset the current time scale.
*/
reset: function() {
this.minStartTime = Infinity;
this.maxEndTime = 0;
},
/**
* Convert a startTime to a distance in pixels, in the current time scale.
* @param {Number} time
* @param {Number} containerWidth The width of the container element.
* @return {Number}
*/
startTimeToDistance: function(time, containerWidth) {
time -= this.minStartTime;
return this.durationToDistance(time, containerWidth);
},
/**
* Convert a duration to a distance in pixels, in the current time scale.
* @param {Number} time
* @param {Number} containerWidth The width of the container element.
* @return {Number}
*/
durationToDistance: function(duration, containerWidth) {
return containerWidth * duration / (this.maxEndTime - this.minStartTime);
},
/**
* Convert a distance in pixels to a time, in the current time scale.
* @param {Number} distance
* @param {Number} containerWidth The width of the container element.
* @return {Number}
*/
distanceToTime: function(distance, containerWidth) {
return this.minStartTime +
((this.maxEndTime - this.minStartTime) * distance / containerWidth);
},
/**
* Convert a distance in pixels to a time, in the current time scale.
* The time will be relative to the current minimum start time.
* @param {Number} distance
* @param {Number} containerWidth The width of the container element.
* @return {Number}
*/
distanceToRelativeTime: function(distance, containerWidth) {
let time = this.distanceToTime(distance, containerWidth);
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) {
let duration = this.maxEndTime - this.minStartTime;
// Format in milliseconds if the total duration is short enough.
if (duration <= 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));
}
};
/**
* 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.
*/
function AnimationsTimeline(inspector) {
this.animations = [];
this.targetNodes = [];
this.inspector = inspector;
this.onAnimationStateChanged = this.onAnimationStateChanged.bind(this);
}
exports.AnimationsTimeline = AnimationsTimeline;
AnimationsTimeline.prototype = {
init: function(containerEl) {
this.win = containerEl.ownerDocument.defaultView;
this.rootWrapperEl = createNode({
parent: containerEl,
attributes: {
"class": "animation-timeline"
}
});
this.timeHeaderEl = createNode({
parent: this.rootWrapperEl,
attributes: {
"class": "time-header"
}
});
this.animationsEl = createNode({
parent: this.rootWrapperEl,
nodeType: "ul",
attributes: {
"class": "animations"
}
});
},
destroy: function() {
this.unrender();
this.rootWrapperEl.remove();
this.animations = [];
this.rootWrapperEl = null;
this.timeHeaderEl = null;
this.animationsEl = null;
this.win = null;
this.inspector = null;
},
destroyTargetNodes: function() {
for (let targetNode of this.targetNodes) {
targetNode.destroy();
}
this.targetNodes = [];
},
unrender: function() {
for (let animation of this.animations) {
animation.off("changed", this.onAnimationStateChanged);
}
TimeScale.reset();
this.destroyTargetNodes();
this.animationsEl.innerHTML = "";
},
render: function(animations) {
this.unrender();
this.animations = animations;
if (!this.animations.length) {
return;
}
// Loop first to set the time scale for all current animations.
for (let {state} of animations) {
TimeScale.addAnimation(state);
}
this.drawHeaderAndBackground();
for (let animation of this.animations) {
animation.on("changed", this.onAnimationStateChanged);
// Each line contains the target animated node and the animation time
// block.
let animationEl = createNode({
parent: this.animationsEl,
nodeType: "li",
attributes: {
"class": "animation"
}
});
// Left sidebar for the animated node.
let animatedNodeEl = createNode({
parent: animationEl,
attributes: {
"class": "target"
}
});
let timeBlockEl = createNode({
parent: animationEl,
attributes: {
"class": "time-block"
}
});
this.drawTimeBlock(animation, timeBlockEl);
// Draw the animated node target.
let targetNode = new AnimationTargetNode(this.inspector, {compact: true});
targetNode.init(animatedNodeEl);
targetNode.render(animation);
// Save the targetNode so it can be destroyed later.
this.targetNodes.push(targetNode);
}
},
onAnimationStateChanged: function() {
// For now, simply re-render the component. The animation front's state has
// already been updated.
this.render(this.animations);
},
drawHeaderAndBackground: function() {
let width = this.timeHeaderEl.offsetWidth;
let scale = width / (TimeScale.maxEndTime - TimeScale.minStartTime);
drawGraphElementBackground(this.win.document, "time-graduations", width, scale);
// And the time graduation header.
this.timeHeaderEl.innerHTML = "";
let interval = findOptimalTimeInterval(scale, TIME_GRADUATION_MIN_SPACING);
for (let i = 0; i < width; i += interval) {
createNode({
parent: this.timeHeaderEl,
nodeType: "span",
attributes: {
"class": "time-tick",
"style": `left:${i}px`
},
textContent: TimeScale.formatTime(
TimeScale.distanceToRelativeTime(i, width))
});
}
},
drawTimeBlock: function({state}, el) {
let width = el.offsetWidth;
// Container for all iterations and delay. Positioned at the right start
// time.
let x = TimeScale.startTimeToDistance(state.startTime + (state.delay || 0),
width);
// With the right width (duration*duration).
let count = state.iterationCount || 1;
let w = TimeScale.durationToDistance(state.duration, width);
let iterations = createNode({
parent: el,
attributes: {
"class": "iterations" + (state.iterationCount ? "" : " infinite"),
// Individual iterations are represented by setting the size of the
// repeating linear-gradient.
"style": `left:${x}px;
width:${w * count}px;
background-size:${Math.max(w, 2)}px 100%;`
}
});
// The animation name is displayed over the iterations.
createNode({
parent: iterations,
attributes: {
"class": "name"
},
textContent: state.name
});
// Delay.
if (state.delay) {
let delay = TimeScale.durationToDistance(state.delay, width);
createNode({
parent: iterations,
attributes: {
"class": "delay",
"style": `left:-${delay}px;
width:${delay}px;`
}
});
}
}
};
@@ -5,3 +5,8 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
EXTRA_JS_MODULES.devtools.animationinspector += [
'components.js',
'utils.js',
]
@@ -1,6 +1,7 @@
[DEFAULT]
subsuite = devtools
support-files =
doc_body_animation.html
doc_frame_script.js
doc_simple_animation.html
head.js
@@ -11,16 +12,21 @@ support-files =
[browser_animation_participate_in_inspector_update.js]
[browser_animation_play_pause_button.js]
[browser_animation_playerFronts_are_refreshed.js]
[browser_animation_playerWidgets_appear_on_panel_init.js]
[browser_animation_playerWidgets_destroy.js]
[browser_animation_playerWidgets_disables_on_finished.js]
[browser_animation_playerWidgets_dont_show_time_after_duration.js]
[browser_animation_playerWidgets_have_control_buttons.js]
[browser_animation_playerWidgets_meta_data.js]
[browser_animation_playerWidgets_state_after_pause.js]
[browser_animation_playerWidgets_target_nodes.js]
[browser_animation_rate_select_shows_presets.js]
[browser_animation_refresh_when_active.js]
[browser_animation_same_nb_of_playerWidgets_and_playerFronts.js]
[browser_animation_setting_currentTime_works_and_pauses.js]
[browser_animation_setting_playbackRate_works.js]
[browser_animation_shows_player_on_valid_node.js]
[browser_animation_target_highlight_select.js]
[browser_animation_timeline_animates.js]
[browser_animation_timeline_is_enabled.js]
[browser_animation_timeline_waits_for_delay.js]
@@ -30,4 +36,5 @@ support-files =
[browser_animation_toolbar_exists.js]
[browser_animation_ui_updates_when_animation_changes.js]
[browser_animation_ui_updates_when_animation_data_changes.js]
[browser_animation_ui_updates_when_animation_rate_changes.js]
[browser_animation_ui_updates_when_animation_time_changes.js]
@@ -8,17 +8,44 @@
add_task(function*() {
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
let {inspector, panel} = yield openAnimationInspector();
let {inspector, panel} = yield openAnimationInspector();
yield testEmptyPanel(inspector, panel);
({inspector, panel}) = yield closeAnimationInspectorAndRestartWithNewUI();
yield testEmptyPanel(inspector, panel, true);
});
function* testEmptyPanel(inspector, panel, isNewUI=false) {
info("Select node .still and check that the panel is empty");
let stillNode = yield getNodeFront(".still", inspector);
let onUpdated = panel.once(panel.UI_UPDATED_EVENT);
yield selectNode(stillNode, inspector);
ok(!panel.playerWidgets || !panel.playerWidgets.length,
"No player widgets displayed for a still node");
yield onUpdated;
if (isNewUI) {
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");
} else {
ok(!panel.playerWidgets || !panel.playerWidgets.length,
"No player widgets displayed for a still node");
}
info("Select the comment text node and check that the panel is empty");
let commentNode = yield inspector.walker.previousSibling(stillNode);
onUpdated = panel.once(panel.UI_UPDATED_EVENT);
yield selectNode(commentNode, inspector);
ok(!panel.playerWidgets || !panel.playerWidgets.length,
"No player widgets displayed for a text node");
});
yield onUpdated;
if (isNewUI) {
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");
} else {
ok(!panel.playerWidgets || !panel.playerWidgets.length,
"No player widgets displayed for a text node");
}
}
@@ -15,4 +15,13 @@ add_task(function*() {
ok(panel, "The animation panel exists");
ok(panel.playersEl, "The animation panel has been initialized");
({panel, controller}) = yield closeAnimationInspectorAndRestartWithNewUI();
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");
});
@@ -10,8 +10,15 @@
add_task(function*() {
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
let {inspector, panel, controller} = yield openAnimationInspector();
let ui = yield openAnimationInspector();
yield testEventsOrder(ui);
ui = yield closeAnimationInspectorAndRestartWithNewUI();
yield testEventsOrder(ui);
});
function* testEventsOrder({inspector, panel, controller}) {
info("Listen for the players-updated, ui-updated and inspector-updated events");
let receivedEvents = [];
controller.once(controller.PLAYERS_UPDATED_EVENT, () => {
@@ -19,7 +26,7 @@ add_task(function*() {
});
panel.once(panel.UI_UPDATED_EVENT, () => {
receivedEvents.push(panel.UI_UPDATED_EVENT);
})
});
inspector.once("inspector-updated", () => {
receivedEvents.push("inspector-updated");
});
@@ -36,4 +43,4 @@ add_task(function*() {
"The second event received was the ui-updated event");
is(receivedEvents[2], "inspector-updated",
"The third event received was the inspector-updated event");
});
}
@@ -0,0 +1,22 @@
/* 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.
add_task(function*() {
yield addTab(TEST_URL_ROOT + "doc_body_animation.html");
let {panel} = yield openAnimationInspector();
is(panel.playerWidgets.length, 1,
"One animation player is displayed after init");
({panel}) = yield closeAnimationInspectorAndRestartWithNewUI();
is(panel.animationsTimelineComponent.animations.length, 1,
"One animation is handled by the timeline after init");
is(panel.animationsTimelineComponent.animationsEl.childNodes.length, 1,
"One animation is displayed after init");
});
@@ -25,6 +25,8 @@ add_task(function*() {
"The second button is the rewind button");
ok(container.children[2].classList.contains("ff"),
"The third button is the fast-forward button");
ok(container.querySelector("select"),
"The container contains the playback rate select");
info("Faking an older server version by setting " +
"AnimationsController.hasSetCurrentTime to false");
@@ -46,4 +48,23 @@ add_task(function*() {
yield selectNode("body", inspector);
controller.hasSetCurrentTime = true;
info("Faking an older server version by setting " +
"AnimationsController.hasSetPlaybackRate to false");
yield selectNode("body", inspector);
controller.hasSetPlaybackRate = false;
info("Selecting the animated node again");
yield selectNode(".animated", inspector);
widget = panel.playerWidgets[0];
container = widget.el.querySelector(".playback-controls");
ok(container, "The control buttons container still exists");
ok(!container.querySelector("select"),
"The playback rate select does not exist");
yield selectNode("body", inspector);
controller.hasSetPlaybackRate = true;
});
@@ -0,0 +1,52 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that player widgets display information about target nodes
add_task(function*() {
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
let {inspector, panel} = yield openAnimationInspector();
info("Select the simple animated node");
yield selectNode(".animated", inspector);
let widget = panel.playerWidgets[0];
// Make sure to wait for the target-retrieved event if the nodeFront hasn't
// yet been retrieved by the TargetNodeComponent.
if (!widget.targetNodeComponent.nodeFront) {
yield widget.targetNodeComponent.once("target-retrieved");
}
let targetEl = widget.el.querySelector(".animation-target");
ok(targetEl, "The player widget has a target element");
is(targetEl.textContent, "<divid=\"\"class=\"ball animated\">",
"The target element's content is correct");
let selectorEl = targetEl.querySelector(".node-selector");
ok(selectorEl,
"The icon to select the target element in the inspector exists");
info("Test again with the new timeline UI");
({inspector, panel}) = yield closeAnimationInspectorAndRestartWithNewUI();
info("Select the simple animated node");
yield selectNode(".animated", inspector);
let targetNodeComponent = panel.animationsTimelineComponent.targetNodes[0];
// Make sure to wait for the target-retrieved event if the nodeFront hasn't
// yet been retrieved by the TargetNodeComponent.
if (!targetNodeComponent.nodeFront) {
yield targetNodeComponent.once("target-retrieved");
}
is(targetNodeComponent.el.textContent, "div#.ball.animated",
"The target element's content is correct");
selectorEl = targetNodeComponent.el.querySelector(".node-selector");
ok(selectorEl,
"The icon to select the target element in the inspector exists");
});
@@ -0,0 +1,49 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that the playbackRate select element contains a list of presets and
// and that if the animation has a current rate that is not part of this list,
// it is added.
add_task(function*() {
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
let {inspector, panel} = yield openAnimationInspector();
info("Selecting the test node");
yield selectNode(".animated", inspector);
info("Get the playback rate UI component");
let widget = panel.playerWidgets[0];
let rateComponent = widget.rateComponent;
let options = rateComponent.el.querySelectorAll("option");
is(options.length, rateComponent.PRESETS.length,
"The playback rate select contains the right number of options");
for (let i = 0; i < rateComponent.PRESETS.length; i ++) {
is(options[i].value, rateComponent.PRESETS[i] + "",
"The playback rate option " + i + " has the right preset value " +
rateComponent.PRESETS[i]);
}
info("Set a custom rate (not part of the presets) via the DOM");
let onRateChanged = waitForStateCondition(widget.player, state => {
return state.playbackRate === 3.6
});
yield executeInContent("Test:SetAnimationPlayerPlaybackRate", {
selector: ".animated",
animationIndex: 0,
playbackRate: 3.6
});
yield onRateChanged;
options = rateComponent.el.querySelectorAll("option");
is(options.length, rateComponent.PRESETS.length + 1,
"The playback rate select contains the right number of options (presets + 1)");
ok([...options].find(option => option.value === "3.6"),
"The custom rate is part of the select");
});
@@ -8,8 +8,15 @@
add_task(function*() {
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
let {toolbox, inspector, panel} = yield openAnimationInspector();
let {inspector, panel} = yield openAnimationInspector();
yield testRefresh(inspector, panel);
({inspector, panel}) = yield closeAnimationInspectorAndRestartWithNewUI();
yield testRefresh(inspector, panel);
});
function* testRefresh(inspector, panel) {
info("Select a non animated node");
yield selectNode(".still", inspector);
@@ -19,14 +26,14 @@ add_task(function*() {
info("Select the animated node now");
yield selectNode(".animated", inspector);
ok(!panel.playerWidgets || !panel.playerWidgets.length,
assertAnimationsDisplayed(panel, 0,
"The panel doesn't show the animation data while inactive");
info("Switch to the animation panel");
inspector.sidebar.select("animationinspector");
yield panel.once(panel.UI_UPDATED_EVENT);
is(panel.playerWidgets.length, 1,
assertAnimationsDisplayed(panel, 1,
"The panel shows the animation data after selecting it");
info("Switch again to the rule-view");
@@ -35,13 +42,13 @@ add_task(function*() {
info("Select the non animated node again");
yield selectNode(".still", inspector);
is(panel.playerWidgets.length, 1,
assertAnimationsDisplayed(panel, 1,
"The panel still shows the previous animation data since it is inactive");
info("Switch to the animation panel again");
inspector.sidebar.select("animationinspector");
yield panel.once(panel.UI_UPDATED_EVENT);
ok(!panel.playerWidgets || !panel.playerWidgets.length,
assertAnimationsDisplayed(panel, 0,
"The panel is now empty after refreshing");
});
}
@@ -22,4 +22,16 @@ add_task(function*() {
is(widget.el.parentNode, panel.playersEl,
"The player widget has been appended to the panel");
}
info("Test again with the new UI, making sure the same number of " +
"animation timelines is created");
({inspector, panel, controller}) = yield closeAnimationInspectorAndRestartWithNewUI();
let timeline = panel.animationsTimelineComponent;
info("Selecting the test animated node again");
yield selectNode(".multi", inspector);
is(controller.animationPlayers.length,
timeline.animationsEl.querySelectorAll(".animation").length,
"As many timeline elements were created as there are playerFronts");
});
@@ -0,0 +1,34 @@
/* 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 setting an animation's playback rate by selecting a rate in the
// presets drop-down sets the rate accordingly.
add_task(function*() {
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
let {toolbox, inspector, panel} = yield openAnimationInspector();
info("Select an animated node");
yield selectNode(".animated", inspector);
info("Get the player widget for this node");
let widget = panel.playerWidgets[0];
let select = widget.rateComponent.el;
let win = select.ownerDocument.defaultView;
info("Click on the rate drop-down");
EventUtils.synthesizeMouseAtCenter(select, {type: "mousedown"}, win);
info("Click on a rate option");
let option = select.options[select.options.length - 1];
EventUtils.synthesizeMouseAtCenter(option, {type: "mouseup"}, win);
let selectedRate = parseFloat(option.value);
info("Check that the rate was changed on the player at the next update");
yield waitForStateCondition(widget.player, ({playbackRate}) => playbackRate === selectedRate);
is(widget.player.state.playbackRate, selectedRate,
"The rate was changed successfully");
});
@@ -9,12 +9,18 @@
add_task(function*() {
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
let {inspector, panel} = yield openAnimationInspector();
let {inspector, panel} = yield openAnimationInspector();
yield testShowsAnimations(inspector, panel);
({inspector, panel}) = yield closeAnimationInspectorAndRestartWithNewUI();
yield testShowsAnimations(inspector, panel);
});
function* testShowsAnimations(inspector, panel) {
info("Select node .animated and check that the panel is not empty");
let node = yield getNodeFront(".animated", inspector);
yield selectNode(node, inspector);
is(panel.playerWidgets.length, 1,
"Exactly 1 player widget is shown for animated node");
});
assertAnimationsDisplayed(panel, 1);
}
@@ -0,0 +1,82 @@
/* 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 DOM element targets displayed in animation player widgets can
// be used to highlight elements in the DOM and select them in the inspector.
add_task(function*() {
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
let ui = yield openAnimationInspector();
yield testTargetNode(ui);
ui = yield closeAnimationInspectorAndRestartWithNewUI();
yield testTargetNode(ui, true);
});
function* testTargetNode({toolbox, inspector, panel}, isNewUI) {
info("Select the simple animated node");
yield selectNode(".animated", inspector);
// Make sure to wait for the target-retrieved event if the nodeFront hasn't
// yet been retrieved by the TargetNodeComponent.
let targetNodeComponent;
if (isNewUI) {
targetNodeComponent = panel.animationsTimelineComponent.targetNodes[0];
} else {
targetNodeComponent = panel.playerWidgets[0].targetNodeComponent;
}
if (!targetNodeComponent.nodeFront) {
yield targetNodeComponent.once("target-retrieved");
}
info("Retrieve the part of the widget that highlights the node on hover");
let highlightingEl = targetNodeComponent.previewEl;
info("Listen to node-highlight event and mouse over the widget");
let onHighlight = toolbox.once("node-highlight");
EventUtils.synthesizeMouse(highlightingEl, 10, 5, {type: "mouseover"},
highlightingEl.ownerDocument.defaultView);
let nodeFront = yield onHighlight;
ok(true, "The node-highlight event was fired");
is(targetNodeComponent.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");
yield selectNode("body", inspector);
// Make sure to wait for the target-retrieved event if the nodeFront hasn't
// yet been retrieved by the TargetNodeComponent.
if (isNewUI) {
targetNodeComponent = panel.animationsTimelineComponent.targetNodes[0];
} else {
targetNodeComponent = panel.playerWidgets[0].targetNodeComponent;
}
if (!targetNodeComponent.nodeFront) {
yield targetNodeComponent.once("target-retrieved");
}
info("Click on the first animation widget's selector icon and wait for the " +
"selection to change");
let onSelection = inspector.selection.once("new-node-front");
let onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT);
let selectIconEl = targetNodeComponent.selectNodeEl;
EventUtils.sendMouseEvent({type: "click"}, selectIconEl,
selectIconEl.ownerDocument.defaultView);
yield onSelection;
is(inspector.selection.nodeFront, targetNodeComponent.nodeFront,
"The selected node is the one stored on the animation widget");
yield onPanelUpdated;
}
@@ -11,7 +11,7 @@
add_task(function*() {
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
let {inspector, panel} = yield openAnimationInspector();
let {panel} = yield openAnimationInspector();
info("Click the toggle button");
yield panel.toggleAll();
@@ -9,7 +9,7 @@
add_task(function*() {
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
let {inspector, panel, window} = yield openAnimationInspector();
let {inspector, window} = yield openAnimationInspector();
let doc = window.document;
let toolbar = doc.querySelector("#toolbar");
@@ -9,36 +9,64 @@
add_task(function*() {
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
let {panel, inspector} = yield openAnimationInspector();
let ui = yield openAnimationInspector();
yield testDataUpdates(ui);
info("Close the toolbox, reload the tab, and try again with the new UI");
ui = yield closeAnimationInspectorAndRestartWithNewUI(true);
yield testDataUpdates(ui, true);
});
function* testDataUpdates({panel, controller, inspector}, isNewUI=false) {
info("Select the test node");
yield selectNode(".animated", inspector);
info("Get the player widget");
let widget = panel.playerWidgets[0];
let animation = controller.animationPlayers[0];
yield setStyle(animation, "animationDuration", "5.5s", isNewUI);
yield setStyle(animation, "animationIterationCount", "300", isNewUI);
yield setStyle(animation, "animationDelay", "45s", isNewUI);
yield setStyle(widget, "animationDuration", "5.5s");
is(widget.metaDataComponent.durationValue.textContent, "5.50s",
"The widget shows the new duration");
if (isNewUI) {
let animationsEl = panel.animationsTimelineComponent.animationsEl;
let timeBlockEl = animationsEl.querySelector(".time-block");
yield setStyle(widget, "animationIterationCount", "300");
is(widget.metaDataComponent.iterationValue.textContent, "300",
"The widget shows the new iteration count");
// 45s delay + (300 * 5.5)s duration
let expectedTotalDuration = 1695 * 1000;
let timeRatio = expectedTotalDuration / timeBlockEl.offsetWidth;
yield setStyle(widget, "animationDelay", "45s");
is(widget.metaDataComponent.delayValue.textContent, "45s",
"The widget shows the new delay");
});
// 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.
let delayWidth = parseFloat(timeBlockEl.querySelector(".delay").style.width);
is(Math.round(delayWidth * timeRatio), 45 * 1000,
"The timeline has the right delay");
} else {
let widget = panel.playerWidgets[0];
is(widget.metaDataComponent.durationValue.textContent, "5.50s",
"The widget shows the new duration");
is(widget.metaDataComponent.iterationValue.textContent, "300",
"The widget shows the new iteration count");
is(widget.metaDataComponent.delayValue.textContent, "45s",
"The widget shows the new delay");
}
}
function* setStyle(widget, name, value) {
function* setStyle(animation, name, value, isNewUI=false) {
info("Change the animation style via the content DOM. Setting " +
name + " to " + value);
let onAnimationChanged = once(animation, "changed");
yield executeInContent("devtools:test:setStyle", {
selector: ".animated",
propertyName: name,
propertyValue: value
});
yield onAnimationChanged;
info("Wait for the next state update");
yield onceNextPlayerRefresh(widget.player);
// If this is the playerWidget-based UI, wait for the auto-refresh event too
// to make sure the UI has updated.
if (!isNewUI) {
yield once(animation, animation.AUTO_REFRESH_EVENT);
}
}
@@ -0,0 +1,47 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that setting an animation's playbackRate via the WebAnimations API (from
// content), actually changes the rate in the corresponding widget too.
add_task(function*() {
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
let {inspector, panel} = yield openAnimationInspector();
info("Selecting the test node");
yield selectNode(".animated", inspector);
info("Get the player widget");
let widget = panel.playerWidgets[0];
info("Change the animation's playbackRate via the content DOM");
let onRateChanged = waitForStateCondition(widget.player, state => {
return state.playbackRate === 2;
}, "playbackRate === 2");
yield executeInContent("Test:SetAnimationPlayerPlaybackRate", {
selector: ".animated",
animationIndex: 0,
playbackRate: 2
});
yield onRateChanged;
is(widget.rateComponent.el.value, "2",
"The playbackRate select value was changed");
info("Change the animation's playbackRate again via the content DOM");
onRateChanged = waitForStateCondition(widget.player, state => {
return state.playbackRate === 0.3;
}, "playbackRate === 0.3");
yield executeInContent("Test:SetAnimationPlayerPlaybackRate", {
selector: ".animated",
animationIndex: 0,
playbackRate: 0.3
});
yield onRateChanged;
is(widget.rateComponent.el.value, "0.3",
"The playbackRate select value was changed again");
});
@@ -0,0 +1,23 @@
<!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>
@@ -52,6 +52,27 @@ addMessageListener("Test:SetAnimationPlayerCurrentTime", function(msg) {
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) {
let {selector, animationIndex, playbackRate} = msg.data;
let node = superQuerySelector(selector);
if (!node) {
return;
}
let 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
@@ -9,7 +9,7 @@ const {gDevTools} = Cu.import("resource://gre/modules/devtools/gDevTools.jsm", {
const {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
const TargetFactory = devtools.TargetFactory;
const {console} = Components.utils.import("resource://gre/modules/devtools/Console.jsm", {});
const {console} = Cu.import("resource://gre/modules/devtools/Console.jsm", {});
const {ViewHelpers} = Cu.import("resource://gre/modules/devtools/ViewHelpers.jsm", {});
// All tests are asynchronous
@@ -19,17 +19,20 @@ const TEST_URL_ROOT = "http://example.com/browser/browser/devtools/animationinsp
const ROOT_TEST_DIR = getRootDirectory(gTestPath);
const FRAME_SCRIPT_URL = ROOT_TEST_DIR + "doc_frame_script.js";
const COMMON_FRAME_SCRIPT_URL = "chrome://global/content/devtools/frame-script-utils.js";
const NEW_UI_PREF = "devtools.inspector.animationInspectorV3";
// Auto clean-up when a test ends
registerCleanupFunction(function*() {
let target = TargetFactory.forTab(gBrowser.selectedTab);
yield gDevTools.closeToolbox(target);
yield closeAnimationInspector();
while (gBrowser.tabs.length > 1) {
gBrowser.removeCurrentTab();
}
});
// Make sure the new UI is off by default.
Services.prefs.setBoolPref(NEW_UI_PREF, false);
// Uncomment this pref to dump all devtools emitted events to the console.
// Services.prefs.setBoolPref("devtools.dump.emit", true);
@@ -45,6 +48,7 @@ registerCleanupFunction(() => gDevTools.testing = false);
registerCleanupFunction(() => {
Services.prefs.clearUserPref("devtools.dump.emit");
Services.prefs.clearUserPref("devtools.debugger.log");
Services.prefs.clearUserPref(NEW_UI_PREF);
});
/**
@@ -77,6 +81,13 @@ function addTab(url) {
return def.promise;
}
/**
* Switch ON the new UI pref.
*/
function enableNewUI() {
Services.prefs.setBoolPref(NEW_UI_PREF, true);
}
/**
* Reload the current tab location.
*/
@@ -119,6 +130,25 @@ let selectNode = Task.async(function*(data, inspector, reason="test") {
yield updated;
});
/**
* 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="") {
let isNewUI = Services.prefs.getBoolPref(NEW_UI_PREF);
msg = msg || `There are ${nbAnimations} animations in the panel`;
if (isNewUI) {
is(panel.animationsTimelineComponent.animationsEl.childNodes.length,
nbAnimations, msg);
} else {
is(panel.playersEl.querySelectorAll(".player-widget").length,
nbAnimations, msg);
}
}
/**
* Takes an Inspector panel that was just created, and waits
* for a "inspector-updated" event as well as the animation inspector
@@ -131,10 +161,9 @@ let waitForAnimationInspectorReady = Task.async(function*(inspector) {
let win = inspector.sidebar.getWindowForTab("animationinspector");
let updated = inspector.once("inspector-updated");
// In e10s, if we wait for underlying toolbox actors to
// load (by setting gDevTools.testing to true), we miss the "animationinspector-ready"
// event on the sidebar, so check to see if the iframe
// is already loaded.
// In e10s, if we wait for underlying toolbox actors to load (by setting
// gDevTools.testing to true), we miss the "animationinspector-ready" event on
// the sidebar, so check to see if the iframe is already loaded.
let tabReady = win.document.readyState === "complete" ?
promise.resolve() :
inspector.sidebar.once("animationinspector-ready");
@@ -145,7 +174,7 @@ let waitForAnimationInspectorReady = Task.async(function*(inspector) {
/**
* Open the toolbox, with the inspector tool visible and the animationinspector
* sidebar selected.
* @return a promise that resolves when the inspector is ready
* @return a promise that resolves when the inspector is ready.
*/
let openAnimationInspector = Task.async(function*() {
let target = TargetFactory.forTab(gBrowser.selectedTab);
@@ -185,6 +214,35 @@ let openAnimationInspector = Task.async(function*() {
};
});
/**
* Close the toolbox.
* @return a promise that resolves when the toolbox has closed.
*/
let closeAnimationInspector = Task.async(function*() {
let target = TargetFactory.forTab(gBrowser.selectedTab);
yield gDevTools.closeToolbox(target);
});
/**
* During the time period we migrate from the playerWidgets-based UI to the new
* AnimationTimeline UI, we'll want to run certain tests against both UI.
* This closes the toolbox, switch the new UI pref ON, and opens the toolbox
* again, with the animation inspector panel selected.
* @param {Boolean} reload Optionally reload the page after the toolbox was
* closed and before it is opened again.
* @return a promise that resolves when the animation inspector is ready.
*/
let closeAnimationInspectorAndRestartWithNewUI = Task.async(function*(reload) {
info("Close the toolbox and test again with the new UI");
yield closeAnimationInspector();
if (reload) {
yield reloadTab();
}
enableNewUI();
return yield openAnimationInspector();
});
/**
* Wait for the toolbox frame to receive focus after it loads
* @param {Toolbox} toolbox
@@ -214,7 +272,7 @@ function hasSideBarTab(inspector, id) {
* @param {Object} target An observable object that either supports on/off or
* addEventListener/removeEventListener
* @param {String} eventName
* @param {Boolean} useCapture Optional, for addEventListener/removeEventListener
* @param {Boolean} useCapture Optional, for add/removeEventListener
* @return A promise that resolves when the event has been handled
*/
function once(target, eventName, useCapture=false) {
@@ -278,9 +336,9 @@ function executeInContent(name, data={}, objects={}, expectResponse=true) {
mm.sendAsyncMessage(name, data, objects);
if (expectResponse) {
return waitForContentMessage(name);
} else {
return promise.resolve();
}
return promise.resolve();
}
function onceNextPlayerRefresh(player) {
@@ -293,7 +351,9 @@ function onceNextPlayerRefresh(player) {
* Simulate a click on the playPause button of a playerWidget.
*/
let togglePlayPauseButton = Task.async(function*(widget) {
let nextState = widget.player.state.playState === "running" ? "paused" : "running";
let nextState = widget.player.state.playState === "running"
? "paused"
: "running";
// Note that instead of simulating a real event here, the callback is just
// called. This is better because the callback returns a promise, so we know
@@ -344,7 +404,8 @@ let waitForStateCondition = Task.async(function*(player, conditionCheck, desc=""
* provided string.
* @param {AnimationPlayerFront} player
* @param {String} playState The playState to expect.
* @return {Promise} Resolves when the playState has changed to the expected value.
* @return {Promise} Resolves when the playState has changed to the expected
* value.
*/
function waitForPlayState(player, playState) {
return waitForStateCondition(player, state => {
@@ -0,0 +1,135 @@
/* -*- 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";
// 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;
// Background time graduations should be multiple of this number of millis.
const TIME_INTERVAL_MULTIPLE = 10;
const TIME_INTERVAL_SCALES = 3;
// The default minimum spacing between time graduations in px.
const TIME_GRADUATION_MIN_SPACING = 10;
// RGB color for the time interval background.
const TIME_INTERVAL_COLOR = [128, 136, 144];
const TIME_INTERVAL_OPACITY_MIN = 32; // byte
const TIME_INTERVAL_OPACITY_ADD = 32; // byte
/**
* 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 createNode(options) {
if (!options.parent) {
throw new Error("Missing parent DOMNode to create new node");
}
let type = options.nodeType || "div";
let node = options.parent.ownerDocument.createElement(type);
for (let name in options.attributes || {}) {
let value = options.attributes[name];
node.setAttribute(name, value);
}
if (options.textContent) {
node.textContent = options.textContent;
}
options.parent.appendChild(node);
return node;
}
exports.createNode = createNode;
/**
* Given a data-scale, draw the background for a graph (vertical lines) into a
* canvas and set that canvas as an image-element with an ID that can be used
* from CSS.
* @param {Document} document The document where the image-element should be set.
* @param {String} id The ID for the image-element.
* @param {Number} graphWidth The width of the graph.
* @param {Number} timeScale How many px is 1ms in the graph.
*/
function drawGraphElementBackground(document, id, graphWidth, timeScale) {
let canvas = document.createElement("canvas");
let ctx = canvas.getContext("2d");
// Set the canvas width (as requested) and height (1px, repeated along the Y
// axis).
canvas.width = graphWidth;
canvas.height = 1;
// Create the image data array which will receive the pixels.
let imageData = ctx.createImageData(canvas.width, canvas.height);
let pixelArray = imageData.data;
let buf = new ArrayBuffer(pixelArray.length);
let view8bit = new Uint8ClampedArray(buf);
let view32bit = new Uint32Array(buf);
// Build new millisecond tick lines...
let [r, g, b] = TIME_INTERVAL_COLOR;
let alphaComponent = TIME_INTERVAL_OPACITY_MIN;
let interval = findOptimalTimeInterval(timeScale);
// Insert one pixel for each division on each scale.
for (let i = 1; i <= TIME_INTERVAL_SCALES; i++) {
let increment = interval * Math.pow(2, i);
for (let x = 0; x < canvas.width; x += increment) {
let position = x | 0;
view32bit[position] = (alphaComponent << 24) | (b << 16) | (g << 8) | r;
}
alphaComponent += TIME_INTERVAL_OPACITY_ADD;
}
// Flush the image data and cache the waterfall background.
pixelArray.set(view8bit);
ctx.putImageData(imageData, 0, 0);
document.mozSetImageElement(id, canvas);
}
exports.drawGraphElementBackground = drawGraphElementBackground;
/**
* Find the optimal interval between time graduations in the animation timeline
* graph based on a time scale and a minimum spacing.
* @param {Number} timeScale How many px is 1ms in the graph.
* @param {Number} minSpacing The minimum spacing between 2 graduations,
* defaults to TIME_GRADUATION_MIN_SPACING.
* @return {Number} The optional interval, in pixels.
*/
function findOptimalTimeInterval(timeScale,
minSpacing=TIME_GRADUATION_MIN_SPACING) {
let timingStep = TIME_INTERVAL_MULTIPLE;
let maxIters = OPTIMAL_TIME_INTERVAL_MAX_ITERS;
let numIters = 0;
if (timeScale > minSpacing) {
return timeScale;
}
while (true) {
let scaledStep = timeScale * timingStep;
if (++numIters > maxIters) {
return scaledStep;
}
if (scaledStep < minSpacing) {
timingStep *= 2;
continue;
}
return scaledStep;
}
}
exports.findOptimalTimeInterval = findOptimalTimeInterval;
@@ -385,6 +385,10 @@ InspectorPanel.prototype = {
* reload
*/
set selectionCssSelector(cssSelector = null) {
if (this._panelDestroyer) {
return;
}
this._selectionCssSelector = {
selector: cssSelector,
url: this._target.url
+142 -39
View File
@@ -5,7 +5,8 @@
"use strict";
/**
* Set of actors that expose the Web Animations API to devtools protocol clients.
* Set of actors that expose the Web Animations API to devtools protocol
* clients.
*
* The |Animations| actor is the main entry point. It is used to discover
* animation players on given nodes.
@@ -29,11 +30,15 @@ const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
const {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
const {setInterval, clearInterval} = require("sdk/timers");
const protocol = require("devtools/server/protocol");
const {ActorClass, Actor, FrontClass, Front, Arg, method, RetVal, types} = protocol;
const {ActorClass, Actor, FrontClass, Front,
Arg, method, RetVal, types} = protocol;
// Make sure the nodeActor type is know here.
const {NodeActor} = require("devtools/server/actors/inspector");
const events = require("sdk/event/core");
const PLAYER_DEFAULT_AUTO_REFRESH_TIMEOUT = 500; // ms
// How long (in ms) should we wait before polling again the state of an
// animationPlayer.
const PLAYER_DEFAULT_AUTO_REFRESH_TIMEOUT = 500;
/**
* The AnimationPlayerActor provides information about a given animation: its
@@ -47,6 +52,13 @@ const PLAYER_DEFAULT_AUTO_REFRESH_TIMEOUT = 500; // ms
let AnimationPlayerActor = ActorClass({
typeName: "animationplayer",
events: {
"changed": {
type: "changed",
state: Arg(0, "json")
}
},
/**
* @param {AnimationsActor} The main AnimationsActor instance
* @param {AnimationPlayer} The player object returned by getAnimationPlayers
@@ -58,14 +70,29 @@ let AnimationPlayerActor = ActorClass({
initialize: function(animationsActor, player, playerIndex) {
Actor.prototype.initialize.call(this, animationsActor.conn);
this.onAnimationMutation = this.onAnimationMutation.bind(this);
this.tabActor = animationsActor.tabActor;
this.player = player;
this.node = player.effect.target;
this.playerIndex = playerIndex;
this.styles = this.node.ownerDocument.defaultView.getComputedStyle(this.node);
let win = this.node.ownerDocument.defaultView;
this.styles = win.getComputedStyle(this.node);
// Listen to animation mutations on the node to alert the front when the
// current animation changes.
this.observer = new win.MutationObserver(this.onAnimationMutation);
this.observer.observe(this.node, {animations: true});
},
destroy: function() {
this.player = this.node = this.styles = null;
// Only try to disconnect the observer if it's not already dead (i.e. if the
// container view hasn't navigated since).
if (this.observer && !Cu.isDeadWrapper(this.observer)) {
this.observer.disconnect();
}
this.tabActor = this.player = this.node = this.styles = this.observer = null;
Actor.prototype.destroy.call(this);
},
@@ -86,6 +113,14 @@ let AnimationPlayerActor = ActorClass({
return data;
},
isAnimation: function(player=this.player) {
return player instanceof this.tabActor.window.CSSAnimation;
},
isTransition: function(player=this.player) {
return player instanceof this.tabActor.window.CSSTransition;
},
/**
* Some of the player's properties are retrieved from the node's
* computed-styles because the Web Animations API does not provide them yet.
@@ -95,14 +130,14 @@ let AnimationPlayerActor = ActorClass({
*/
getPlayerIndex: function() {
let names = this.styles.animationName;
if (names === "none") {
names = this.styles.transitionProperty;
}
// If no names are found, then it's probably a transition, in which case we
// can't find the actual index, so just trust the playerIndex passed by
// the AnimationsActor at initialization time.
// Note that this may be incorrect if by the time the AnimationPlayerActor
// is initialized, one of the transitions has ended, but it's the best we
// can do for now.
if (!names) {
// If we still don't have a name, let's fall back to the provided index
// which may, by now, be wrong, but it's the best we can do until the waapi
// gives us a way to get duration, delay, ... directly.
if (!names || names === "none") {
return this.playerIndex;
}
@@ -113,14 +148,31 @@ let AnimationPlayerActor = ActorClass({
// If there are several names, retrieve the index of the animation name in
// the list.
let playerName = this.getName();
names = names.split(",").map(n => n.trim());
for (let i = 0; i < names.length; i ++) {
if (names[i] === this.player.effect.name) {
for (let i = 0; i < names.length; i++) {
if (names[i] === playerName) {
return i;
}
}
},
/**
* Get the name associated with the player. This is used to match
* up the player with values in the computed animation-name or
* transition-property property.
* @return {String}
*/
getName: function() {
if (this.isAnimation()) {
return this.player.animationName;
} else if (this.isTransition()) {
return this.player.transitionProperty;
} else {
return "";
}
},
/**
* Get the animation duration from this player, in milliseconds.
* Note that the Web Animations API doesn't yet offer a way to retrieve this
@@ -207,7 +259,7 @@ let AnimationPlayerActor = ActorClass({
currentTime: this.player.currentTime,
playState: this.player.playState,
playbackRate: this.player.playbackRate,
name: this.player.effect.name,
name: this.getName(),
duration: this.getDuration(),
delay: this.getDelay(),
iterationCount: this.getIterationCount(),
@@ -244,6 +296,27 @@ let AnimationPlayerActor = ActorClass({
}
}),
/**
* Executed when the current animation changes, used to emit the new state
* the the front.
*/
onAnimationMutation: function(mutations) {
let hasChanged = false;
for (let {changedAnimations} of mutations) {
if (!changedAnimations.length) {
return;
}
if (changedAnimations.some(animation => animation === this.player)) {
hasChanged = true;
break;
}
}
if (hasChanged) {
events.emit(this, "changed", this.getCurrentState());
}
},
/**
* Pause the player.
*/
@@ -348,9 +421,18 @@ let AnimationPlayerFront = FrontClass(AnimationPlayerActor, {
delay: this._form.delay,
iterationCount: this._form.iterationCount,
isRunningOnCompositor: this._form.isRunningOnCompositor
}
};
},
/**
* Executed when the AnimationPlayerActor emits a "changed" event. Used to
* update the local knowledge of the state.
*/
onChanged: protocol.preEvent("changed", function(partialState) {
let {state} = this.reconstructState(partialState);
this.state = state;
}),
// About auto-refresh:
//
// The AnimationPlayerFront is capable of automatically refreshing its state
@@ -404,11 +486,6 @@ let AnimationPlayerFront = FrontClass(AnimationPlayerActor, {
return;
}
// If the animationplayer is now finished, stop auto-refreshing.
if (data.playState === "finished") {
this.stopAutoRefresh();
}
if (this.currentStateHasChanged) {
this.state = data;
events.emit(this, this.AUTO_REFRESH_EVENT, this.state);
@@ -421,19 +498,28 @@ let AnimationPlayerFront = FrontClass(AnimationPlayerActor, {
*/
getCurrentState: protocol.custom(function() {
this.currentStateHasChanged = false;
return this._getCurrentState().then(data => {
for (let key in this.state) {
if (typeof data[key] === "undefined") {
data[key] = this.state[key];
} else if (data[key] !== this.state[key]) {
this.currentStateHasChanged = true;
}
}
return data;
return this._getCurrentState().then(partialData => {
let {state, hasChanged} = this.reconstructState(partialData);
this.currentStateHasChanged = hasChanged;
return state;
});
}, {
impl: "_getCurrentState"
}),
reconstructState: function(data) {
let hasChanged = false;
for (let key in this.state) {
if (typeof data[key] === "undefined") {
data[key] = this.state[key];
} else if (data[key] !== this.state[key]) {
hasChanged = true;
}
}
return {state: data, hasChanged};
}
});
/**
@@ -454,7 +540,7 @@ let AnimationsActor = exports.AnimationsActor = ActorClass({
typeName: "animations",
events: {
"mutations" : {
"mutations": {
type: "mutations",
changes: Arg(0, "array:animationMutationChange")
}
@@ -502,7 +588,7 @@ let AnimationsActor = exports.AnimationsActor = ActorClass({
// No care is taken here to destroy the previously stored actors because it
// is assumed that the client is responsible for lifetimes of actors.
this.actors = [];
for (let i = 0; i < animations.length; i ++) {
for (let i = 0; i < animations.length; i++) {
// XXX: for now the index is passed along as the AnimationPlayerActor uses
// it to retrieve animation information from CSS.
let actor = AnimationPlayerActor(this, animations[i], i);
@@ -531,7 +617,7 @@ let AnimationsActor = exports.AnimationsActor = ActorClass({
onAnimationMutation: function(mutations) {
let eventData = [];
for (let {addedAnimations, changedAnimations, removedAnimations} of mutations) {
for (let {addedAnimations, removedAnimations} of mutations) {
for (let player of removedAnimations) {
// Note that animations are reported as removed either when they are
// actually removed from the node (e.g. css class removed) or when they
@@ -556,6 +642,24 @@ let AnimationsActor = exports.AnimationsActor = ActorClass({
if (this.actors.find(a => a.player === player)) {
continue;
}
// If the added player has the same name and target node as a player we
// already have, it means it's a transition that's re-starting. So send
// a "removed" event for the one we already have.
let index = this.actors.findIndex(a => {
return a.player.constructor === player.constructor &&
((a.isAnimation() &&
a.player.animationName === player.animationName) ||
(a.isTransition() &&
a.player.transitionProperty === player.transitionProperty));
});
if (index !== -1) {
eventData.push({
type: "removed",
player: this.actors[index]
});
this.actors.splice(index, 1);
}
let actor = AnimationPlayerActor(
this, player, player.effect.target.getAnimations().indexOf(player));
this.actors.push(actor);
@@ -572,9 +676,9 @@ let AnimationsActor = exports.AnimationsActor = ActorClass({
},
/**
* After the client has called getAnimationPlayersForNode for a given DOM node,
* the actor starts sending animation mutations for this node. If the client
* doesn't want this to happen anymore, it should call this method.
* After the client has called getAnimationPlayersForNode for a given DOM
* node, the actor starts sending animation mutations for this node. If the
* client doesn't want this to happen anymore, it should call this method.
*/
stopAnimationPlayerUpdates: method(function() {
if (this.observer && !Cu.isDeadWrapper(this.observer)) {
@@ -639,7 +743,7 @@ let AnimationsActor = exports.AnimationsActor = ActorClass({
/**
* Play all animations in the current tabActor's frames.
* This method only returns when the animations have left their pending states.
* This method only returns when animations have left their pending states.
*/
playAll: method(function() {
let readyPromises = [];
@@ -657,9 +761,8 @@ let AnimationsActor = exports.AnimationsActor = ActorClass({
toggleAll: method(function() {
if (this.allAnimationsPaused) {
return this.playAll();
} else {
return this.pauseAll();
}
return this.pauseAll();
}, {
request: {},
response: {}
+67 -11
View File
@@ -2754,19 +2754,13 @@ var WalkerActor = protocol.ActorClass({
}),
/**
* Given an StyleSheetActor (identified by its ID), commonly used in the
* Given a StyleSheetActor (identified by its ID), commonly used in the
* style-editor, get its ownerNode and return the corresponding walker's
* NodeActor
* NodeActor.
* Note that getNodeFromActor was added later and can now be used instead.
*/
getStyleSheetOwnerNode: method(function(styleSheetActorID) {
let styleSheetActor = this.conn.getActor(styleSheetActorID);
let ownerNode = styleSheetActor.ownerNode;
if (!styleSheetActor || !ownerNode) {
return null;
}
return this.attachElement(ownerNode);
return this.getNodeFromActor(styleSheetActorID, ["ownerNode"]);
}, {
request: {
styleSheetActorID: Arg(0, "string")
@@ -2775,6 +2769,60 @@ var WalkerActor = protocol.ActorClass({
ownerNode: RetVal("nullable:disconnectedNode")
}
}),
/**
* This method can be used to retrieve NodeActor for DOM nodes from other
* actors in a way that they can later be highlighted in the page, or
* selected in the inspector.
* If an actor has a reference to a DOM node, and the UI needs to know about
* this DOM node (and possibly select it in the inspector), the UI should
* first retrieve a reference to the walkerFront:
*
* // Make sure the inspector/walker have been initialized first.
* toolbox.initInspector().then(() => {
* // Retrieve the walker.
* let walker = toolbox.walker;
* });
*
* And then call this method:
*
* // Get the nodeFront from my actor, passing the ID and properties path.
* walker.getNodeFromActor(myActorID, ["element"]).then(nodeFront => {
* // Use the nodeFront, e.g. select the node in the inspector.
* toolbox.getPanel("inspector").selection.setNodeFront(nodeFront);
* });
*
* @param {String} actorID The ID for the actor that has a reference to the
* DOM node.
* @param {Array} path Where, on the actor, is the DOM node stored. If in the
* scope of the actor, the node is available as `this.data.node`, then this
* should be ["data", "node"].
* @return {NodeActor} The attached NodeActor, or null if it couldn't be found.
*/
getNodeFromActor: method(function(actorID, path) {
let actor = this.conn.getActor(actorID);
if (!actor) {
return null;
}
let obj = actor;
for (let name of path) {
if (!(name in obj)) {
return null;
}
obj = obj[name];
}
return this.attachElement(obj);
}, {
request: {
actorID: Arg(0, "string"),
path: Arg(1, "array:string")
},
response: {
node: RetVal("nullable:disconnectedNode")
}
})
});
/**
@@ -2932,6 +2980,14 @@ var WalkerFront = exports.WalkerFront = protocol.FrontClass(WalkerActor, {
impl: "_getStyleSheetOwnerNode"
}),
getNodeFromActor: protocol.custom(function(actorID, path) {
return this._getNodeFromActor(actorID, path).then(response => {
return response ? response.node : null;
});
}, {
impl: "_getNodeFromActor"
}),
_releaseFront: function(node, force) {
if (node.retained && !force) {
node.reparent(null);
@@ -3080,7 +3136,7 @@ var WalkerFront = exports.WalkerFront = protocol.FrontClass(WalkerActor, {
*/
onMutations: protocol.preEvent("new-mutations", function() {
// Fetch and process the mutations.
this.getMutations({cleanup: this.autoCleanup}).then(null, console.error);
this.getMutations({cleanup: this.autoCleanup}).catch(() => {});
}),
isLocal: function() {
@@ -103,6 +103,20 @@
animation: move .5s, glow 1s 2s infinite, grow 3s 1s 100;
}
.all-transitions {
position: absolute;
top: 0;
right: 0;
width: 50px;
height: 50px;
background: blue;
transition: all .2s;
}
.all-transitions.expand {
width: 200px;
height: 100px;
}
@keyframes move {
100% {
transform: translateY(100px);
@@ -130,6 +144,7 @@
<div class="delayed-transition"></div>
<div class="delayed-multiple-animations"></div>
<div class="multiple-animations-2"></div>
<div class="all-transitions"></div>
<script type="text/javascript">
// Get the transitions started when the page loads
var players;
@@ -29,6 +29,7 @@ support-files =
[browser_animation_actors_12.js]
[browser_animation_actors_13.js]
[browser_animation_actors_14.js]
[browser_animation_actors_15.js]
[browser_canvasframe_helper_01.js]
[browser_canvasframe_helper_02.js]
[browser_canvasframe_helper_03.js]
@@ -0,0 +1,71 @@
/* 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/ */
"use strict";
// When a transition finishes, no "removed" event is sent because it may still
// be used, but when it restarts again (transitions back), then a new
// AnimationPlayerFront should be sent, and the old one should be removed.
const {AnimationsFront} = require("devtools/server/actors/animation");
const {InspectorFront} = require("devtools/server/actors/inspector");
add_task(function*() {
let doc = yield addTab(MAIN_DOMAIN + "animation.html");
initDebuggerServer();
let client = new DebuggerClient(DebuggerServer.connectPipe());
let form = yield connectDebuggerClient(client);
let inspector = InspectorFront(client, form);
let walker = yield inspector.getWalker();
let animations = AnimationsFront(client, form);
info("Retrieve the test node");
let node = yield walker.querySelector(walker.rootNode, ".all-transitions");
info("Retrieve the animation players for the node");
let players = yield animations.getAnimationPlayersForNode(node);
is(players.length, 0, "The node has no animation players yet");
info("Listen for new animations");
let reportedMutations = [];
function onMutations(mutations) {
reportedMutations = [...reportedMutations, ...mutations];
}
animations.on("mutations", onMutations);
info("Transition the node by adding the expand class");
let cpow = content.document.querySelector(".all-transitions");
cpow.classList.add("expand");
info("Wait for longer than the transition");
yield wait(500);
is(reportedMutations.length, 2, "2 mutation events were received");
is(reportedMutations[0].type, "added", "The first event was 'added'");
is(reportedMutations[1].type, "added", "The second event was 'added'");
reportedMutations = [];
info("Transition back by removing the expand class");
cpow.classList.remove("expand");
info("Wait for longer than the transition");
yield wait(500);
is(reportedMutations.length, 4, "4 new mutation events were received");
is(reportedMutations.filter(m => m.type === "removed").length, 2,
"2 'removed' events were sent (for the old transitions)");
is(reportedMutations.filter(m => m.type === "added").length, 2,
"2 'added' events were sent (for the new transitions)");
animations.off("mutations", onMutations);
yield closeDebuggerClient(client);
gBrowser.removeCurrentTab();
});
function wait(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}
@@ -52,6 +52,7 @@ skip-if = buildapp == 'mulet'
[test_inspector-dead-nodes.html]
[test_inspector_getImageData.html]
skip-if = buildapp == 'mulet'
[test_inspector_getNodeFromActor.html]
[test_inspector-hide.html]
[test_inspector-insert.html]
[test_inspector-mutations-attr.html]
@@ -0,0 +1,89 @@
<!DOCTYPE HTML>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=1155653
-->
<head>
<meta charset="utf-8">
<title>Test for Bug 1155653</title>
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript;version=1.8" src="inspector-helpers.js"></script>
<script type="application/javascript;version=1.8">
Components.utils.import("resource://gre/modules/devtools/Loader.jsm");
const inspector = devtools.require("devtools/server/actors/inspector");
window.onload = function() {
SimpleTest.waitForExplicitFinish();
runNextTest();
}
var gWalker;
addTest(function() {
let url = document.getElementById("inspectorContent").href;
attachURL(url, function(err, client, tab, doc) {
let {InspectorFront} = devtools.require("devtools/server/actors/inspector");
let inspector = InspectorFront(client, tab);
promiseDone(inspector.getWalker().then(walker => {
gWalker = walker;
}).then(runNextTest));
});
});
addTest(function() {
info("Try to get a NodeFront from an invalid actorID");
gWalker.getNodeFromActor("invalid", ["node"]).then(node => {
ok(!node, "The node returned is null");
runNextTest();
});
});
addTest(function() {
info("Try to get a NodeFront from a valid actorID but invalid path");
gWalker.getNodeFromActor(gWalker.actorID, ["invalid", "path"]).then(node => {
ok(!node, "The node returned is null");
runNextTest();
});
});
addTest(function() {
info("Try to get a NodeFront from a valid actorID and valid path");
gWalker.getNodeFromActor(gWalker.actorID, ["rootDoc"]).then(rootDocNode => {
ok(rootDocNode, "A node was returned");
is(rootDocNode, gWalker.rootNode, "The right node was returned");
runNextTest();
});
});
addTest(function() {
info("Try to get a NodeFront from a valid actorID and valid complex path");
gWalker.getNodeFromActor(gWalker.actorID,
["tabActor", "window", "document", "body"]).then(bodyNode => {
ok(bodyNode, "A node was returned");
gWalker.querySelector(gWalker.rootNode, "body").then(node => {
is(bodyNode, node, "The body node was returned");
runNextTest();
});
});
});
addTest(function() {
gWalker = null;
runNextTest();
});
</script>
</head>
<body>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1155653">Mozilla Bug 1155653</a>
<a id="inspectorContent" target="_blank" href="inspector_getImageData.html">Test Document</a>
<p id="display"></p>
<div id="content" style="display: none">
</div>
<pre id="test">
</pre>
</body>
</html>