This commit is contained in:
Ryan VanderMeulen 2015-06-15 15:49:24 -04:00
Родитель 1ac299f882 8b2b1647a0
Коммит b187c86d0a
77 изменённых файлов: 2422 добавлений и 583 удалений

Просмотреть файл

@ -337,6 +337,7 @@ let AboutReaderListener = {
break;
case "pagehide":
this.cancelPotentialPendingReadabilityCheck();
sendAsyncMessage("Reader:UpdateReaderButton", { isArticle: false });
break;
@ -353,12 +354,42 @@ let AboutReaderListener = {
}
},
/**
* NB: this function will update the state of the reader button asynchronously
* after the next mozAfterPaint call (assuming reader mode is enabled and
* this is a suitable document). Calling it on things which won't be
* painted is not going to work.
*/
updateReaderButton: function(forceNonArticle) {
if (!ReaderMode.isEnabledForParseOnLoad || this.isAboutReader ||
!(content.document instanceof content.HTMLDocument) ||
content.document.mozSyntheticDocument) {
return;
}
this.scheduleReadabilityCheckPostPaint(forceNonArticle);
},
cancelPotentialPendingReadabilityCheck: function() {
if (this._pendingReadabilityCheck) {
removeEventListener("MozAfterPaint", this._pendingReadabilityCheck);
delete this._pendingReadabilityCheck;
}
},
scheduleReadabilityCheckPostPaint: function(forceNonArticle) {
if (this._pendingReadabilityCheck) {
// We need to stop this check before we re-add one because we don't know
// if forceNonArticle was true or false last time.
this.cancelPotentialPendingReadabilityCheck();
}
this._pendingReadabilityCheck = this.onPaintWhenWaitedFor.bind(this, forceNonArticle);
addEventListener("MozAfterPaint", this._pendingReadabilityCheck);
},
onPaintWhenWaitedFor: function(forceNonArticle) {
this.cancelPotentialPendingReadabilityCheck();
// Only send updates when there are articles; there's no point updating with
// |false| all the time.
if (ReaderMode.isProbablyReaderable(content.document)) {

Просмотреть файл

@ -478,6 +478,9 @@ skip-if = e10s # bug 1100687 - test directly manipulates content (content.docume
[browser_readerMode.js]
support-files =
readerModeArticle.html
[browser_readerMode_hidden_nodes.js]
support-files =
readerModeArticleHiddenNodes.html
[browser_bug1124271_readerModePinnedTab.js]
support-files =
readerModeArticle.html

Просмотреть файл

@ -0,0 +1,45 @@
/* 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/. */
/**
* Test that the reader mode button appears and works properly on
* reader-able content, and that ReadingList button can open and close
* its Sidebar UI.
*/
const TEST_PREFS = [
["reader.parse-on-load.enabled", true],
["browser.reader.detectedFirstArticle", false],
];
const TEST_PATH = "http://example.com/browser/browser/base/content/test/general/";
let readerButton = document.getElementById("reader-mode-button");
add_task(function* test_reader_button() {
registerCleanupFunction(function() {
// Reset test prefs.
TEST_PREFS.forEach(([name, value]) => {
Services.prefs.clearUserPref(name);
});
while (gBrowser.tabs.length > 1) {
gBrowser.removeCurrentTab();
}
});
// Set required test prefs.
TEST_PREFS.forEach(([name, value]) => {
Services.prefs.setBoolPref(name, value);
});
let tab = gBrowser.selectedTab = gBrowser.addTab();
is_element_hidden(readerButton, "Reader mode button is not present on a new tab");
// Point tab to a test page that is not reader-able due to hidden nodes.
let url = TEST_PATH + "readerModeArticleHiddenNodes.html";
yield promiseTabLoadEvent(tab, url);
yield ContentTask.spawn(tab.linkedBrowser, "", function() {
return ContentTaskUtils.waitForEvent(content, "MozAfterPaint");
});
is_element_hidden(readerButton, "Reader mode button is still not present on tab with unreadable content.");
});

Просмотреть файл

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<head>
<title>Article title</title>
<meta name="description" content="This is the article description." />
</head>
<body>
<style>
p { display: none }
</style>
<header>Site header</header>
<div>
<h1>Article title</h1>
<h2 class="author">by Jane Doe</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ut gravida lorem. Ut turpis felis, pulvinar a semper sed, adipiscing id dolor. Pellentesque auctor nisi id magna consequat sagittis. Curabitur dapibus enim sit amet elit pharetra tincidunt feugiat nisl imperdiet. Ut convallis libero in urna ultrices accumsan. Donec sed odio eros. Donec viverra mi quis quam pulvinar at malesuada arcu rhoncus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In rutrum accumsan ultricies. Mauris vitae nisi at sem facilisis semper ac in est.</p>
<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
</div>
</body>
</html>

Просмотреть файл

@ -114,6 +114,7 @@ let AnimationsController = {
"setPlaybackRate");
this.hasTargetNode = yield target.actorHasMethod("domwalker",
"getNodeFromActor");
this.isNewUI = Services.prefs.getBoolPref("devtools.inspector.animationInspectorV3");
if (this.destroyed) {
console.warn("Could not fully initialize the AnimationsController");
@ -240,11 +241,15 @@ let AnimationsController = {
for (let {type, player} of changes) {
if (type === "added") {
this.animationPlayers.push(player);
player.startAutoRefresh();
if (!this.isNewUI) {
player.startAutoRefresh();
}
}
if (type === "removed") {
player.stopAutoRefresh();
if (!this.isNewUI) {
player.stopAutoRefresh();
}
yield player.release();
let index = this.animationPlayers.indexOf(player);
this.animationPlayers.splice(index, 1);
@ -256,12 +261,20 @@ let AnimationsController = {
}),
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();
}

Просмотреть файл

@ -3,14 +3,17 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* globals AnimationsController, document, performance, promise,
gToolbox, gInspector, requestAnimationFrame, cancelAnimationFrame, L10N */
"use strict";
const {createNode} = require("devtools/animationinspector/utils");
const {
PlayerMetaDataHeader,
PlaybackRateSelector,
AnimationTargetNode,
createNode
AnimationsTimeline
} = require("devtools/animationinspector/components");
/**
@ -22,7 +25,8 @@ let AnimationsPanel = {
initialize: Task.async(function*() {
if (AnimationsController.destroyed) {
console.warn("Could not initialize the animation-panel, controller was destroyed");
console.warn("Could not initialize the animation-panel, controller " +
"was destroyed");
return;
}
if (this.initialized) {
@ -45,13 +49,18 @@ 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();
yield this.createPlayerWidgets();
if (AnimationsController.isNewUI) {
this.animationsTimelineComponent = new AnimationsTimeline(gInspector);
this.animationsTimelineComponent.init(this.playersEl);
}
yield this.refreshAnimations();
this.initialized.resolve();
@ -69,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;
@ -79,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);
@ -91,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);
@ -122,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");
@ -142,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);
@ -157,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();
}),
@ -392,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() {
@ -406,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);
},
@ -466,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) {
@ -492,7 +519,8 @@ PlayerWidget.prototype = {
*/
setPlaybackRate: function(rate) {
if (!AnimationsController.hasSetPlaybackRate) {
throw new Error("This server version doesn't support setting animations' playbackRate");
throw new Error("This server version doesn't support setting " +
"animations' playbackRate");
}
return this.player.setPlaybackRate(rate);

Просмотреть файл

@ -3,6 +3,7 @@
/* 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";
@ -19,11 +20,20 @@
// 4. destroy the component:
// c.destroy();
const {Cu} = require('chrome');
const {Cu} = require("chrome");
Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
const {Task} = Cu.import("resource://gre/modules/Task.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:
@ -75,9 +85,9 @@ PlayerMetaDataHeader.prototype = {
// Animation duration.
this.durationLabel = createNode({
parent: metaData,
nodeType: "span"
nodeType: "span",
textContent: L10N.getStr("player.animationDurationLabel")
});
this.durationLabel.textContent = L10N.getStr("player.animationDurationLabel");
this.durationValue = createNode({
parent: metaData,
@ -90,9 +100,9 @@ PlayerMetaDataHeader.prototype = {
nodeType: "span",
attributes: {
"style": "display:none;"
}
},
textContent: L10N.getStr("player.animationDelayLabel")
});
this.delayLabel.textContent = L10N.getStr("player.animationDelayLabel");
this.delayValue = createNode({
parent: metaData,
@ -106,9 +116,9 @@ PlayerMetaDataHeader.prototype = {
nodeType: "span",
attributes: {
"style": "display:none;"
}
},
textContent: L10N.getStr("player.animationIterationCountLabel")
});
this.iterationLabel.textContent = L10N.getStr("player.animationIterationCountLabel");
this.iterationValue = createNode({
parent: metaData,
@ -224,7 +234,7 @@ PlaybackRateSelector.prototype = {
* different from the existing presets.
*/
getCurrentPresets: function({playbackRate}) {
return [...new Set([...this.PRESETS, playbackRate])].sort((a,b) => a > b);
return [...new Set([...this.PRESETS, playbackRate])].sort((a, b) => a > b);
},
render: function(state) {
@ -248,9 +258,9 @@ PlaybackRateSelector.prototype = {
nodeType: "option",
attributes: {
value: preset,
}
},
textContent: L10N.getFormatStr("player.playbackRateLabel", preset)
});
option.textContent = L10N.getFormatStr("player.playbackRateLabel", preset);
if (preset === state.playbackRate) {
option.setAttribute("selected", "");
}
@ -261,7 +271,7 @@ PlaybackRateSelector.prototype = {
this.currentRate = state.playbackRate;
},
onSelectionChanged: function(e) {
onSelectionChanged: function() {
this.emit("rate-changed", parseFloat(this.el.value));
}
};
@ -272,9 +282,13 @@ PlaybackRateSelector.prototype = {
* @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) {
function AnimationTargetNode(inspector, options={}) {
this.inspector = inspector;
this.options = options;
this.onPreviewMouseOver = this.onPreviewMouseOver.bind(this);
this.onPreviewMouseOut = this.onPreviewMouseOut.bind(this);
@ -313,7 +327,9 @@ AnimationTargetNode.prototype = {
nodeType: "span"
});
this.previewEl.appendChild(document.createTextNode("<"));
if (!this.options.compact) {
this.previewEl.appendChild(document.createTextNode("<"));
}
// Tag name.
this.tagNameEl = createNode({
@ -330,15 +346,26 @@ AnimationTargetNode.prototype = {
nodeType: "span"
});
createNode({
parent: this.idEl,
nodeType: "span",
attributes: {
"class": "attribute-name theme-fg-color2"
}
}).textContent = "id";
this.idEl.appendChild(document.createTextNode("=\""));
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,
@ -348,7 +375,9 @@ AnimationTargetNode.prototype = {
}
});
this.idEl.appendChild(document.createTextNode("\""));
if (!this.options.compact) {
this.idEl.appendChild(document.createTextNode("\""));
}
// Class attribute container.
this.classEl = createNode({
@ -356,15 +385,26 @@ AnimationTargetNode.prototype = {
nodeType: "span"
});
createNode({
parent: this.classEl,
nodeType: "span",
attributes: {
"class": "attribute-name theme-fg-color2"
}
}).textContent = "class";
this.classEl.appendChild(document.createTextNode("=\""));
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,
@ -374,9 +414,10 @@ AnimationTargetNode.prototype = {
}
});
this.classEl.appendChild(document.createTextNode("\""));
this.previewEl.appendChild(document.createTextNode(">"));
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);
@ -430,73 +471,359 @@ AnimationTargetNode.prototype = {
}
},
render: function(playerFront) {
render: Task.async(function*(playerFront) {
this.playerFront = playerFront;
this.inspector.walker.getNodeFromActor(playerFront.actorID, ["node"]).then(nodeFront => {
// We might have been destroyed in the meantime, or the node might not be found.
if (!this.el || !nodeFront) {
return;
}
this.nodeFront = undefined;
this.nodeFront = nodeFront;
let {tagName, attributes} = 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) {
this.classEl.querySelector(".attribute-value").textContent =
attributes[classIndex].value;
this.classEl.style.display = "inline";
} else {
this.classEl.style.display = "none";
}
this.emit("target-retrieved");
}, e => {
this.nodeFront = null;
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");
} else {
console.error(e);
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");
})
};
/**
* 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.
* @return {DOMNode} The newly created node.
* 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.
*/
function createNode(options) {
if (!options.parent) {
throw new Error("Missing parent DOMNode to create new node");
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));
}
};
let type = options.nodeType || "div";
let node = options.parent.ownerDocument.createElement(type);
exports.TimeScale = TimeScale;
for (let name in options.attributes || {}) {
let value = options.attributes[name];
node.setAttribute(name, value);
}
/**
* 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;
options.parent.appendChild(node);
return node;
this.onAnimationStateChanged = this.onAnimationStateChanged.bind(this);
}
exports.createNode = createNode;
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,7 +5,9 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
EXTRA_JS_MODULES.devtools.animationinspector += [
'components.js',
'utils.js',
]

Просмотреть файл

@ -19,6 +19,9 @@ support-files =
[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_scrubber_delayed.js]
[browser_animation_playerWidgets_scrubber_enabled.js]
[browser_animation_playerWidgets_scrubber_moves.js]
[browser_animation_playerWidgets_state_after_pause.js]
[browser_animation_playerWidgets_target_nodes.js]
[browser_animation_rate_select_shows_presets.js]
@ -30,9 +33,11 @@ support-files =
[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]
[browser_animation_timeline_displays_with_pref.js]
[browser_animation_timeline_header.js]
[browser_animation_timeline_shows_delay.js]
[browser_animation_timeline_shows_iterations.js]
[browser_animation_timeline_ui.js]
[browser_animation_toggle_button_resets_on_navigate.js]
[browser_animation_toggle_button_toggles_animations.js]
[browser_animation_toggle_button_updates_playerWidgets.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");
});
}

Просмотреть файл

@ -9,7 +9,14 @@
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");
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");
});

Просмотреть файл

@ -27,5 +27,26 @@ add_task(function*() {
"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");
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");
});

Просмотреть файл

@ -8,13 +8,19 @@
add_task(function*() {
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
let {toolbox, inspector, panel} = yield openAnimationInspector();
let {inspector, panel} = yield openAnimationInspector();
yield testRefreshOnNewAnimation(inspector, panel);
({inspector, panel}) = yield closeAnimationInspectorAndRestartWithNewUI();
yield testRefreshOnNewAnimation(inspector, panel);
});
function* testRefreshOnNewAnimation(inspector, panel) {
info("Select a non animated node");
yield selectNode(".still", inspector);
is(panel.playersEl.querySelectorAll(".player-widget").length, 0,
"There are no player widgets in the panel");
assertAnimationsDisplayed(panel, 0);
info("Listen to the next UI update event");
let onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT);
@ -29,6 +35,14 @@ add_task(function*() {
yield onPanelUpdated;
ok(true, "The panel update event was fired");
is(panel.playersEl.querySelectorAll(".player-widget").length, 1,
"There is one player widget in the panel");
});
assertAnimationsDisplayed(panel, 1);
info("Remove the animation class on the node");
onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT);
yield executeInContent("devtools:test:setAttribute", {
selector: ".ball.animated",
attributeName: "class",
attributeValue: "ball still"
});
yield onPanelUpdated;
}

Просмотреть файл

@ -8,13 +8,21 @@
add_task(function*() {
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
let {toolbox, inspector, panel} = yield openAnimationInspector();
let {inspector, panel} = yield openAnimationInspector();
yield testRefreshOnRemove(inspector, panel);
yield testAddedAnimationWorks(inspector, panel);
info("Reload and test again with the new UI");
({inspector, panel}) = yield closeAnimationInspectorAndRestartWithNewUI(true);
yield testRefreshOnRemove(inspector, panel, true);
});
function* testRefreshOnRemove(inspector, panel) {
info("Select a animated node");
yield selectNode(".animated", inspector);
is(panel.playersEl.querySelectorAll(".player-widget").length, 1,
"There is one player widget in the panel");
assertAnimationsDisplayed(panel, 1);
info("Listen to the next UI update event");
let onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT);
@ -29,23 +37,24 @@ add_task(function*() {
yield onPanelUpdated;
ok(true, "The panel update event was fired");
is(panel.playersEl.querySelectorAll(".player-widget").length, 0,
"There are no player widgets in the panel anymore");
assertAnimationsDisplayed(panel, 0);
info("Add an finite animation on the node again, and wait for it to appear");
onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT);
yield executeInContent("devtools:test:setAttribute", {
selector: ".test-node",
attributeName: "class",
attributeValue: "ball short"
attributeValue: "ball short test-node"
});
yield onPanelUpdated;
is(panel.playersEl.querySelectorAll(".player-widget").length, 1,
"There is one player widget in the panel again");
assertAnimationsDisplayed(panel, 1);
}
function* testAddedAnimationWorks(inspector, panel) {
info("Now wait until the animation finishes");
let widget = panel.playerWidgets[0];
yield waitForPlayState(widget.player, "finished")
yield waitForPlayState(widget.player, "finished");
is(panel.playersEl.querySelectorAll(".player-widget").length, 1,
"There is still a player widget in the panel after the animation finished");
@ -59,4 +68,4 @@ add_task(function*() {
EventUtils.synthesizeMouseAtCenter(input, {type: "mousedown"}, win);
yield onPaused;
ok(widget.el.classList.contains("paused"), "The widget is in paused mode");
});
}

Просмотреть файл

@ -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");
});

Просмотреть файл

@ -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);
}

Просмотреть файл

@ -9,14 +9,26 @@
add_task(function*() {
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
let {toolbox, inspector, panel} = yield openAnimationInspector();
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 = panel.playerWidgets[0].targetNodeComponent;
let targetNodeComponent;
if (isNewUI) {
targetNodeComponent = panel.animationsTimelineComponent.targetNodes[0];
} else {
targetNodeComponent = panel.playerWidgets[0].targetNodeComponent;
}
if (!targetNodeComponent.nodeFront) {
yield targetNodeComponent.once("target-retrieved");
}
@ -33,21 +45,29 @@ add_task(function*() {
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");
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.
targetNodeComponent = panel.playerWidgets[0].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");
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;
@ -59,4 +79,4 @@ add_task(function*() {
"The selected node is the one stored on the animation widget");
yield onPanelUpdated;
});
}

Просмотреть файл

@ -0,0 +1,24 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Check that the timeline-based UI is displayed instead of the playerwidget-
// based UI when the "devtools.inspector.animationInspectorV3" is set.
add_task(function*() {
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
let {inspector, panel} = yield openAnimationInspectorNewUI();
info("Selecting the test node");
yield selectNode(".animated", inspector);
let timeline = panel.animationsTimelineComponent;
ok(timeline, "The timeline components was created");
is(timeline.rootWrapperEl.parentNode, panel.playersEl,
"The timeline component was appended in the DOM");
is(panel.playersEl.querySelectorAll(".player-widget").length, 0,
"There are no playerWidgets in the DOM");
});

Просмотреть файл

@ -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";
// Check that the timeline-based UI shows correct time graduations in the
// header.
const {findOptimalTimeInterval} = require("devtools/animationinspector/utils");
const {TimeScale} = require("devtools/animationinspector/components");
// Should be kept in sync with TIME_GRADUATION_MIN_SPACING in components.js
const TIME_GRADUATION_MIN_SPACING = 40;
add_task(function*() {
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
let {panel} = yield openAnimationInspectorNewUI();
let timeline = panel.animationsTimelineComponent;
let headerEl = timeline.timeHeaderEl;
info("Find out how many time graduations should there be");
let width = headerEl.offsetWidth;
let scale = width / (TimeScale.maxEndTime - TimeScale.minStartTime);
// Note that findOptimalTimeInterval is tested separately in xpcshell test
// test_findOptimalTimeInterval.js, so we assume that it works here.
let interval = findOptimalTimeInterval(scale, TIME_GRADUATION_MIN_SPACING);
let nb = Math.ceil(width / interval);
is(headerEl.querySelectorAll(".time-tick").length, nb,
"The expected number of time ticks were found");
info("Make sure graduations are evenly distributed and show the right times");
[...headerEl.querySelectorAll(".time-tick")].forEach((tick, i) => {
let left = parseFloat(tick.style.left);
is(Math.round(left), Math.round(i * interval),
"Graduation " + i + " is positioned correctly");
// Note that the distancetoRelativeTime and formatTime functions are tested
// separately in xpcshell test test_timeScale.js, so we assume that they
// work here.
let formattedTime = TimeScale.formatTime(
TimeScale.distanceToRelativeTime(i * interval, width));
is(tick.textContent, formattedTime,
"Graduation " + i + " has the right text content");
});
});

Просмотреть файл

@ -0,0 +1,30 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Check that animation delay is visualized in the timeline-based UI when the
// animation is delayed.
add_task(function*() {
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
let {inspector, panel} = yield openAnimationInspectorNewUI();
info("Selecting a delayed animated node");
yield selectNode(".delayed", inspector);
info("Getting the animation and delay elements from the panel");
let timelineEl = panel.animationsTimelineComponent.rootWrapperEl;
let delay = timelineEl.querySelector(".delay");
ok(delay, "The animation timeline contains the delay element");
info("Selecting a no-delay animated node");
yield selectNode(".animated", inspector);
info("Getting the animation and delay elements from the panel again");
delay = timelineEl.querySelector(".delay");
ok(!delay, "The animation timeline contains no delay element");
});

Просмотреть файл

@ -0,0 +1,51 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Check that the timeline-based UI is displays as many iteration elements as
// there are iterations in an animation.
add_task(function*() {
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
let {inspector, panel} = yield openAnimationInspectorNewUI();
info("Selecting the test node");
yield selectNode(".delayed", inspector);
info("Getting the animation element from the panel");
let timelineEl = panel.animationsTimelineComponent.rootWrapperEl;
let animation = timelineEl.querySelector(".time-block");
let iterations = animation.querySelector(".iterations");
// Iterations are rendered with a repeating linear-gradient, so we need to
// calculate how many iterations are represented by looking at the background
// size.
let iterationCount = getIterationCountFromBackground(iterations);
is(iterationCount, 10,
"The animation timeline contains the right number of iterations");
ok(!iterations.classList.contains("infinite"),
"The iteration element doesn't have the infinite class");
info("Selecting another test node with an infinite animation");
yield selectNode(".animated", inspector);
info("Getting the animation element from the panel again");
animation = timelineEl.querySelector(".time-block");
iterations = animation.querySelector(".iterations");
iterationCount = getIterationCountFromBackground(iterations);
is(iterationCount, 1,
"The animation timeline contains just one iteration");
ok(iterations.classList.contains("infinite"),
"The iteration element has the infinite class");
});
function getIterationCountFromBackground(el) {
let backgroundSize = parseFloat(el.style.backgroundSize.split(" ")[0]);
let width = el.offsetWidth;
return Math.round(width / backgroundSize);
}

Просмотреть файл

@ -0,0 +1,41 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Check that the timeline-based UI contains the right elements.
add_task(function*() {
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
let {panel} = yield openAnimationInspectorNewUI();
let timeline = panel.animationsTimelineComponent;
let el = timeline.rootWrapperEl;
ok(el.querySelector(".time-header"),
"The header element is in the DOM of the timeline");
ok(el.querySelectorAll(".time-header .time-tick").length,
"The header has some time graduations");
ok(el.querySelector(".animations"),
"The animations container is in the DOM of the timeline");
is(el.querySelectorAll(".animations .animation").length,
timeline.animations.length,
"The number of animations displayed matches the number of animations");
for (let i = 0; i < timeline.animations.length; i++) {
let animation = timeline.animations[i];
let animationEl = el.querySelectorAll(".animations .animation")[i];
ok(animationEl.querySelector(".target"),
"The animated node target element is in the DOM");
ok(animationEl.querySelector(".time-block"),
"The timeline element is in the DOM");
is(animationEl.querySelector(".name").textContent,
animation.state.name,
"The name on the timeline is correct");
ok(animationEl.querySelector(".iterations"),
"The timeline has iterations displayed");
}
});

Просмотреть файл

@ -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);
}
}

Просмотреть файл

@ -7,9 +7,10 @@
const Cu = Components.utils;
const {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
const {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
const {require} = devtools;
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:///modules/devtools/ViewHelpers.jsm", {});
// All tests are asynchronous
@ -19,17 +20,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://browser/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 +49,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 +82,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 +131,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 +162,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 +175,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 +215,45 @@ let openAnimationInspector = Task.async(function*() {
};
});
/**
* Turn on the new timeline-based UI pref ON, and then open the toolbox, with
* the inspector tool visible and the animationinspector sidebar selected.
* @return a promise that resolves when the inspector is ready.
*/
function openAnimationInspectorNewUI() {
enableNewUI();
return openAnimationInspector();
}
/**
* 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 +283,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 +347,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 +362,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 +415,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,4 @@
{
// Extend from the common devtools xpcshell eslintrc config.
"extends": "../../../.eslintrc.xpcshell"
}

Просмотреть файл

@ -0,0 +1,85 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/* eslint no-eval:0 */
"use strict";
const Cu = Components.utils;
const {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
const {require} = devtools;
const {findOptimalTimeInterval} = require("devtools/animationinspector/utils");
// This test array contains objects that are used to test the
// findOptimalTimeInterval function. Each object should have the following
// properties:
// - desc: an optional string that will be printed out
// - timeScale: a number that represents how many pixels is 1ms
// - minSpacing: an optional number that represents the minim space between 2
// time graduations
// - expectedInterval: a number that you expect the findOptimalTimeInterval
// function to return as a result.
// Optionally you can pass a string where `interval` is the calculated
// interval, this string will be eval'd and tested to be truthy.
const TEST_DATA = [{
desc: "With 1px being 1ms and no minSpacing, expect the interval to be the " +
"default min spacing",
timeScale: 1,
minSpacing: undefined,
expectedInterval: 10
}, {
desc: "With 1px being 1ms and a custom minSpacing being a multiple of 10 " +
"expect the interval to be the custom min spacing",
timeScale: 1,
minSpacing: 40,
expectedInterval: 40
}, {
desc: "With 1px being 1ms and a custom minSpacing not being multiple of 10 " +
"expect the interval to be the next multiple of 10",
timeScale: 1,
minSpacing: 13,
expectedInterval: 20
}, {
desc: "If 1ms corresponds to a distance that is greater than the min " +
"spacing then, expect the interval to be this distance",
timeScale: 20,
minSpacing: undefined,
expectedInterval: 20
}, {
desc: "If 1ms corresponds to a distance that is greater than the min " +
"spacing then, expect the interval to be this distance, even if it " +
"isn't a multiple of 10",
timeScale: 33,
minSpacing: undefined,
expectedInterval: 33
}, {
desc: "If 1ms is a very small distance, then expect this distance to be " +
"multiplied by 10, 20, 40, 80, etc... until it goes over the min " +
"spacing",
timeScale: 0.001,
minSpacing: undefined,
expectedInterval: 10.24
}, {
desc: "If the time scale is such that we need to iterate more than the " +
"maximum allowed number of iterations, then expect an interval lower " +
"than the minimum one",
timeScale: 1e-31,
minSpacing: undefined,
expectedInterval: "interval < 10"
}];
function run_test() {
for (let {timeScale, desc, minSpacing, expectedInterval} of TEST_DATA) {
do_print("Testing timeScale: " + timeScale + " and minSpacing: " +
minSpacing + ". Expecting " + expectedInterval + ".");
let interval = findOptimalTimeInterval(timeScale, minSpacing);
if (typeof expectedInterval == "string") {
ok(eval(expectedInterval), desc);
} else {
equal(interval, expectedInterval, desc);
}
}
}

Просмотреть файл

@ -0,0 +1,191 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const Cu = Components.utils;
const {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
const {require} = devtools;
const {TimeScale} = require("devtools/animationinspector/components");
const TEST_ANIMATIONS = [{
startTime: 500,
delay: 0,
duration: 1000,
iterationCount: 1
}, {
startTime: 400,
delay: 100,
duration: 10,
iterationCount: 100
}, {
startTime: 50,
delay: 1000,
duration: 100,
iterationCount: 20
}];
const EXPECTED_MIN_START = 50;
const EXPECTED_MAX_END = 3050;
const TEST_STARTTIME_TO_DISTANCE = [{
time: 50,
width: 100,
expectedDistance: 0
}, {
time: 50,
width: 0,
expectedDistance: 0
}, {
time: 3050,
width: 200,
expectedDistance: 200
}, {
time: 1550,
width: 200,
expectedDistance: 100
}];
const TEST_DURATION_TO_DISTANCE = [{
time: 3000,
width: 100,
expectedDistance: 100
}, {
time: 0,
width: 100,
expectedDistance: 0
}];
const TEST_DISTANCE_TO_TIME = [{
distance: 100,
width: 100,
expectedTime: 3050
}, {
distance: 0,
width: 100,
expectedTime: 50
}, {
distance: 25,
width: 200,
expectedTime: 425
}];
const TEST_DISTANCE_TO_RELATIVE_TIME = [{
distance: 100,
width: 100,
expectedTime: 3000
}, {
distance: 0,
width: 100,
expectedTime: 0
}, {
distance: 25,
width: 200,
expectedTime: 375
}];
const TEST_FORMAT_TIME_MS = [{
time: 0,
expectedFormattedTime: "0ms"
}, {
time: 3540.341,
expectedFormattedTime: "3540ms"
}, {
time: 1.99,
expectedFormattedTime: "2ms"
}, {
time: 4000,
expectedFormattedTime: "4000ms"
}];
const TEST_FORMAT_TIME_S = [{
time: 0,
expectedFormattedTime: "0.0s"
}, {
time: 3540.341,
expectedFormattedTime: "3.5s"
}, {
time: 1.99,
expectedFormattedTime: "0.0s"
}, {
time: 4000,
expectedFormattedTime: "4.0s"
}, {
time: 102540,
expectedFormattedTime: "102.5s"
}, {
time: 102940,
expectedFormattedTime: "102.9s"
}];
function run_test() {
do_print("Check the default min/max range values");
equal(TimeScale.minStartTime, Infinity);
equal(TimeScale.maxEndTime, 0);
do_print("Test adding a few animations");
for (let {startTime, delay, duration, iterationCount} of TEST_ANIMATIONS) {
TimeScale.addAnimation({startTime, delay, duration, iterationCount});
}
equal(TimeScale.minStartTime, EXPECTED_MIN_START);
equal(TimeScale.maxEndTime, EXPECTED_MAX_END);
do_print("Test reseting the animations");
TimeScale.reset();
equal(TimeScale.minStartTime, Infinity);
equal(TimeScale.maxEndTime, 0);
do_print("Test adding the animations again");
for (let {startTime, delay, duration, iterationCount} of TEST_ANIMATIONS) {
TimeScale.addAnimation({startTime, delay, duration, iterationCount});
}
equal(TimeScale.minStartTime, EXPECTED_MIN_START);
equal(TimeScale.maxEndTime, EXPECTED_MAX_END);
do_print("Test converting start times to distances");
for (let {time, width, expectedDistance} of TEST_STARTTIME_TO_DISTANCE) {
let distance = TimeScale.startTimeToDistance(time, width);
equal(distance, expectedDistance);
}
do_print("Test converting durations to distances");
for (let {time, width, expectedDistance} of TEST_DURATION_TO_DISTANCE) {
let distance = TimeScale.durationToDistance(time, width);
equal(distance, expectedDistance);
}
do_print("Test converting distances to times");
for (let {distance, width, expectedTime} of TEST_DISTANCE_TO_TIME) {
let time = TimeScale.distanceToTime(distance, width);
equal(time, expectedTime);
}
do_print("Test converting distances to relative times");
for (let {distance, width, expectedTime} of TEST_DISTANCE_TO_RELATIVE_TIME) {
let time = TimeScale.distanceToRelativeTime(distance, width);
equal(time, expectedTime);
}
do_print("Test formatting times (millis)");
for (let {time, expectedFormattedTime} of TEST_FORMAT_TIME_MS) {
let formattedTime = TimeScale.formatTime(time);
equal(formattedTime, expectedFormattedTime);
}
// Add 1 more animation to increase the range and test more time formatting
// cases.
TimeScale.addAnimation({
startTime: 3000,
duration: 5000,
delay: 0,
iterationCount: 1
});
do_print("Test formatting times (seconds)");
for (let {time, expectedFormattedTime} of TEST_FORMAT_TIME_S) {
let formattedTime = TimeScale.formatTime(time);
equal(formattedTime, expectedFormattedTime);
}
}

Просмотреть файл

@ -0,0 +1,9 @@
[DEFAULT]
tags = devtools
head =
tail =
firefox-appdir = browser
skip-if = toolkit == 'android' || toolkit == 'gonk'
[test_findOptimalTimeInterval.js]
[test_timeScale.js]

Просмотреть файл

@ -0,0 +1,134 @@
/* -*- 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 optimal interval, in pixels.
*/
function findOptimalTimeInterval(timeScale,
minSpacing=TIME_GRADUATION_MIN_SPACING) {
let timingStep = TIME_INTERVAL_MULTIPLE;
let numIters = 0;
if (timeScale > minSpacing) {
return timeScale;
}
while (true) {
let scaledStep = timeScale * timingStep;
if (++numIters > OPTIMAL_TIME_INTERVAL_MAX_ITERS) {
return scaledStep;
}
if (scaledStep < minSpacing) {
timingStep *= 2;
continue;
}
return scaledStep;
}
}
exports.findOptimalTimeInterval = findOptimalTimeInterval;

Просмотреть файл

@ -106,14 +106,13 @@ BottomHost.prototype = {
}
this.isMinimized = true;
this.frame.style.marginBottom = -this.frame.height + height + "px";
this._splitter.classList.add("disabled");
let onTransitionEnd = () => {
this.frame.removeEventListener("transitionend", onTransitionEnd);
this.emit("minimized");
};
this.frame.addEventListener("transitionend", onTransitionEnd);
this.frame.style.marginBottom = -this.frame.height + height + "px";
this._splitter.classList.add("disabled");
},
/**
@ -126,14 +125,13 @@ BottomHost.prototype = {
}
this.isMinimized = false;
this.frame.style.marginBottom = "0";
this._splitter.classList.remove("disabled");
let onTransitionEnd = () => {
this.frame.removeEventListener("transitionend", onTransitionEnd);
this.emit("maximized");
};
this.frame.addEventListener("transitionend", onTransitionEnd);
this.frame.style.marginBottom = "0";
this._splitter.classList.remove("disabled");
},
/**

Просмотреть файл

@ -8,7 +8,7 @@
* and parsing out the blueprint to generate correct values for markers.
*/
const { Ci } = require("chrome");
const { Cu, Ci } = require("chrome");
loader.lazyRequireGetter(this, "L10N",
"devtools/performance/global", true);
@ -22,6 +22,18 @@ loader.lazyRequireGetter(this, "WebConsoleUtils",
// String used to fill in platform data when it should be hidden.
const GECKO_SYMBOL = "(Gecko)";
/**
* Takes a marker, blueprint, and filter list and
* determines if this marker should be filtered or not.
*/
function isMarkerValid (marker, filter) {
let isUnknown = !(marker.name in TIMELINE_BLUEPRINT);
if (isUnknown) {
return filter.indexOf("UNKNOWN") === -1;
}
return filter.indexOf(marker.name) === -1;
}
/**
* Returns the correct label to display for passed in marker, based
* off of the blueprints.
@ -30,7 +42,7 @@ const GECKO_SYMBOL = "(Gecko)";
* @return {string}
*/
function getMarkerLabel (marker) {
let blueprint = TIMELINE_BLUEPRINT[marker.name];
let blueprint = getBlueprintFor(marker);
// Either use the label function in the blueprint, or use it directly
// as a string.
return typeof blueprint.label === "function" ? blueprint.label(marker) : blueprint.label;
@ -44,7 +56,7 @@ function getMarkerLabel (marker) {
* @return {string}
*/
function getMarkerClassName (type) {
let blueprint = TIMELINE_BLUEPRINT[type];
let blueprint = getBlueprintFor({ name: type });
// Either use the label function in the blueprint, or use it directly
// as a string.
let className = typeof blueprint.label === "function" ? blueprint.label() : blueprint.label;
@ -72,7 +84,7 @@ function getMarkerClassName (type) {
* @return {Array<object>}
*/
function getMarkerFields (marker) {
let blueprint = TIMELINE_BLUEPRINT[marker.name];
let blueprint = getBlueprintFor(marker);
// If blueprint.fields is a function, use that
if (typeof blueprint.fields === "function") {
@ -111,7 +123,7 @@ const DOM = {
* @return {Array<Element>}
*/
buildFields: function (doc, marker) {
let blueprint = TIMELINE_BLUEPRINT[marker.name];
let blueprint = getBlueprintFor(marker);
let fields = getMarkerFields(marker);
return fields.map(({ label, value }) => DOM.buildNameValueLabel(doc, label, value));
@ -125,7 +137,7 @@ const DOM = {
* @return {Element}
*/
buildTitle: function (doc, marker) {
let blueprint = TIMELINE_BLUEPRINT[marker.name];
let blueprint = getBlueprintFor(marker);
let hbox = doc.createElement("hbox");
hbox.setAttribute("align", "center");
@ -377,6 +389,14 @@ const JS_MARKER_MAP = {
* A series of formatters used by the blueprint.
*/
const Formatters = {
/**
* Uses the marker name as the label for markers that do not have
* a blueprint entry. Uses "Other" in the marker filter menu.
*/
UnknownLabel: function (marker={}) {
return marker.name || L10N.getStr("timeline.label.unknown");
},
GCLabel: function (marker={}) {
let label = L10N.getStr("timeline.label.garbageCollection");
// Only if a `nonincrementalReason` exists, do we want to label
@ -444,9 +464,22 @@ const Formatters = {
},
};
/**
* Takes a marker and returns the definition for that marker type,
* falling back to the UNKNOWN definition if undefined.
*
* @param {Marker} marker
* @return {object}
*/
function getBlueprintFor (marker) {
return TIMELINE_BLUEPRINT[marker.name] || TIMELINE_BLUEPRINT.UNKNOWN;
}
exports.isMarkerValid = isMarkerValid;
exports.getMarkerLabel = getMarkerLabel;
exports.getMarkerClassName = getMarkerClassName;
exports.getMarkerFields = getMarkerFields;
exports.DOM = DOM;
exports.CollapseFunctions = CollapseFunctions;
exports.Formatters = Formatters;
exports.getBlueprintFor = getBlueprintFor;

Просмотреть файл

@ -196,55 +196,6 @@ function getProfileThreadFromAllocations(allocations) {
return thread;
}
/**
* Gets the current timeline blueprint without the hidden markers.
*
* @param blueprint
* The default timeline blueprint.
* @param array hiddenMarkers
* A list of hidden markers' names.
* @return object
* The filtered timeline blueprint.
*/
function getFilteredBlueprint({ blueprint, hiddenMarkers }) {
// Clone functions here just to prevent an error, as the blueprint
// contains functions (even though we do not use them).
let filteredBlueprint = Cu.cloneInto(blueprint, {}, { cloneFunctions: true });
let maybeRemovedGroups = new Set();
let removedGroups = new Set();
// 1. Remove hidden markers from the blueprint.
for (let hiddenMarkerName of hiddenMarkers) {
maybeRemovedGroups.add(filteredBlueprint[hiddenMarkerName].group);
delete filteredBlueprint[hiddenMarkerName];
}
// 2. Get a list of all the groups that will be removed.
for (let maybeRemovedGroup of maybeRemovedGroups) {
let markerNames = Object.keys(filteredBlueprint);
let isGroupRemoved = markerNames.every(e => filteredBlueprint[e].group != maybeRemovedGroup);
if (isGroupRemoved) {
removedGroups.add(maybeRemovedGroup);
}
}
// 3. Offset groups so that their indices are consecutive.
for (let removedGroup of removedGroups) {
let markerNames = Object.keys(filteredBlueprint);
for (let markerName of markerNames) {
let markerDetails = filteredBlueprint[markerName];
if (markerDetails.group > removedGroup) {
markerDetails.group--;
}
}
}
return filteredBlueprint;
};
/**
* Deduplicates a profile by deduplicating stacks, frames, and strings.
*
@ -571,7 +522,6 @@ exports.offsetSampleTimes = offsetSampleTimes;
exports.offsetMarkerTimes = offsetMarkerTimes;
exports.offsetAndScaleTimestamps = offsetAndScaleTimestamps;
exports.getProfileThreadFromAllocations = getProfileThreadFromAllocations;
exports.getFilteredBlueprint = getFilteredBlueprint;
exports.deflateProfile = deflateProfile;
exports.deflateThread = deflateThread;
exports.UniqueStrings = UniqueStrings;

Просмотреть файл

@ -7,27 +7,25 @@
* Utility functions for collapsing markers into a waterfall.
*/
loader.lazyRequireGetter(this, "TIMELINE_BLUEPRINT",
"devtools/performance/markers", true);
loader.lazyRequireGetter(this, "getBlueprintFor",
"devtools/performance/marker-utils", true);
/**
* Collapses markers into a tree-like structure.
* @param object markerNode
* @param array markersList
* @param ?object blueprint
*/
function collapseMarkersIntoNode({ markerNode, markersList, blueprint }) {
function collapseMarkersIntoNode({ markerNode, markersList }) {
let { getCurrentParentNode, collapseMarker, addParentNode, popParentNode } = createParentNodeFactory(markerNode);
blueprint = blueprint || TIMELINE_BLUEPRINT;
for (let i = 0, len = markersList.length; i < len; i++) {
let curr = markersList[i];
let parentNode = getCurrentParentNode();
let def = blueprint[curr.name];
let collapse = def.collapseFunc || (() => null);
let blueprint = getBlueprintFor(curr);
let collapse = blueprint.collapseFunc || (() => null);
let peek = distance => markersList[i + distance];
let foundParent = false;
let collapseInfo = collapse(parentNode, curr, peek);
if (collapseInfo) {

Просмотреть файл

@ -55,6 +55,16 @@ const { Formatters, CollapseFunctions: collapse } = require("devtools/performanc
* updated as well.
*/
const TIMELINE_BLUEPRINT = {
/* Default definition used for markers that occur but
* are not defined here. Should ultimately be defined, but this gives
* us room to work on the front end separately from the platform. */
"UNKNOWN": {
group: 2,
colorName: "graphs-grey",
collapseFunc: collapse.child,
label: Formatters.UnknownLabel
},
/* Group 0 - Reflow and Rendering pipeline */
"Styles": {
group: 0,
@ -131,7 +141,7 @@ const TIMELINE_BLUEPRINT = {
/* Group 2 - User Controlled */
"ConsoleTime": {
group: 2,
colorName: "graphs-grey",
colorName: "graphs-blue",
label: sublabelForProperty(L10N.getStr("timeline.label.consoleTime"), "causeName"),
fields: [{
property: "causeName",

Просмотреть файл

@ -135,8 +135,8 @@ MemoryGraph.prototype = Heritage.extend(PerformanceGraph.prototype, {
}
});
function TimelineGraph(parent, blueprint) {
MarkersOverview.call(this, parent, blueprint);
function TimelineGraph(parent, filter) {
MarkersOverview.call(this, parent, filter);
}
TimelineGraph.prototype = Heritage.extend(MarkersOverview.prototype, {
@ -163,7 +163,6 @@ const GRAPH_DEFINITIONS = {
timeline: {
constructor: TimelineGraph,
selector: "#markers-overview",
needsBlueprints: true,
primaryLink: true
}
};
@ -174,15 +173,15 @@ const GRAPH_DEFINITIONS = {
*
* @param {object} definition
* @param {DOMElement} root
* @param {function} getBlueprint
* @param {function} getFilter
* @param {function} getTheme
*/
function GraphsController ({ definition, root, getBlueprint, getTheme }) {
function GraphsController ({ definition, root, getFilter, getTheme }) {
this._graphs = {};
this._enabled = new Set();
this._definition = definition || GRAPH_DEFINITIONS;
this._root = root;
this._getBlueprint = getBlueprint;
this._getFilter = getFilter;
this._getTheme = getTheme;
this._primaryLink = Object.keys(this._definition).filter(name => this._definition[name].primaryLink)[0];
this.$ = root.ownerDocument.querySelector.bind(root.ownerDocument);
@ -369,8 +368,8 @@ GraphsController.prototype = {
_construct: Task.async(function *(graphName) {
let def = this._definition[graphName];
let el = this.$(def.selector);
let blueprint = def.needsBlueprints ? this._getBlueprint() : void 0;
let graph = this._graphs[graphName] = new def.constructor(el, blueprint);
let filter = this._getFilter();
let graph = this._graphs[graphName] = new def.constructor(el, filter);
graph.graphName = graphName;
yield graph.ready();

Просмотреть файл

@ -13,8 +13,6 @@ loader.lazyRequireGetter(this, "EventEmitter",
"devtools/toolkit/event-emitter");
loader.lazyRequireGetter(this, "L10N",
"devtools/performance/global", true);
loader.lazyRequireGetter(this, "TIMELINE_BLUEPRINT",
"devtools/performance/markers", true);
loader.lazyRequireGetter(this, "MarkerUtils",
"devtools/performance/marker-utils");

Просмотреть файл

@ -11,11 +11,10 @@
const { Cc, Ci, Cu, Cr } = require("chrome");
const { Heritage } = require("resource:///modules/devtools/ViewHelpers.jsm");
const { AbstractTreeItem } = require("resource:///modules/devtools/AbstractTreeItem.jsm");
const { TIMELINE_BLUEPRINT: ORIGINAL_BP } = require("devtools/performance/markers");
loader.lazyRequireGetter(this, "MarkerUtils",
"devtools/performance/marker-utils");
const HTML_NS = "http://www.w3.org/1999/xhtml";
const LEVEL_INDENT = 10; // px
@ -60,15 +59,14 @@ MarkerView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
},
/**
* Sets a list of names and colors used to paint markers.
* @see TIMELINE_BLUEPRINT in timeline/widgets/global.js
* @param object blueprint
* Sets a list of marker types to be filtered out of this view.
* @param Array<String> filter
*/
set blueprint(blueprint) {
this.root._blueprint = blueprint;
set filter(filter) {
this.root._filter = filter;
},
get blueprint() {
return this.root._blueprint;
get filter() {
return this.root._filter;
},
/**
@ -139,7 +137,6 @@ MarkerView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
if (!submarkers || !submarkers.length) {
return;
}
let blueprint = this.root._blueprint;
let startTime = this.root._interval.startTime;
let endTime = this.root._interval.endTime;
let newLevel = this.level + 1;
@ -147,17 +144,15 @@ MarkerView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
for (let i = 0, len = submarkers.length; i < len; i++) {
let marker = submarkers[i];
// If this marker isn't in the global timeline blueprint, don't display
// it, but dump a warning message to the console.
if (!(marker.name in blueprint)) {
if (!(marker.name in ORIGINAL_BP)) {
console.warn(`Marker not found in timeline blueprint: ${marker.name}.`);
}
// Skip filtered markers
if (!MarkerUtils.isMarkerValid(marker, this.filter)) {
continue;
}
if (!isMarkerInRange(marker, startTime|0, endTime|0)) {
continue;
}
children.push(new MarkerView({
owner: this,
marker: marker,
@ -175,15 +170,12 @@ MarkerView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
*/
_buildMarkerCells: function(doc, targetNode, arrowNode) {
let marker = this.marker;
let style = this.root._blueprint[marker.name];
let blueprint = MarkerUtils.getBlueprintFor(marker);
let startTime = this.root._interval.startTime;
let endTime = this.root._interval.endTime;
let sidebarCell = this._buildMarkerSidebar(
doc, style, marker);
let timebarCell = this._buildMarkerTimebar(
doc, style, marker, startTime, endTime, arrowNode);
let sidebarCell = this._buildMarkerSidebar(doc, blueprint, marker);
let timebarCell = this._buildMarkerTimebar(doc, blueprint, marker, startTime, endTime, arrowNode);
targetNode.appendChild(sidebarCell);
targetNode.appendChild(timebarCell);

Просмотреть файл

@ -21,6 +21,10 @@ loader.lazyRequireGetter(this, "L10N",
"devtools/performance/global", true);
loader.lazyRequireGetter(this, "TickUtils",
"devtools/performance/waterfall-ticks", true);
loader.lazyRequireGetter(this, "MarkerUtils",
"devtools/performance/marker-utils");
loader.lazyRequireGetter(this, "TIMELINE_BLUEPRINT",
"devtools/performance/markers", true);
const OVERVIEW_HEADER_HEIGHT = 14; // px
const OVERVIEW_ROW_HEIGHT = 11; // px
@ -28,14 +32,12 @@ const OVERVIEW_ROW_HEIGHT = 11; // px
const OVERVIEW_SELECTION_LINE_COLOR = "#666";
const OVERVIEW_CLIPHEAD_LINE_COLOR = "#555";
const FIND_OPTIMAL_TICK_INTERVAL_MAX_ITERS = 100;
const OVERVIEW_HEADER_TICKS_MULTIPLE = 100; // ms
const OVERVIEW_HEADER_TICKS_SPACING_MIN = 75; // px
const OVERVIEW_HEADER_TEXT_FONT_SIZE = 9; // px
const OVERVIEW_HEADER_TEXT_FONT_FAMILY = "sans-serif";
const OVERVIEW_HEADER_TEXT_PADDING_LEFT = 6; // px
const OVERVIEW_HEADER_TEXT_PADDING_TOP = 1; // px
const OVERVIEW_MARKERS_COLOR_STOPS = [0, 0.1, 0.75, 1];
const OVERVIEW_MARKER_WIDTH_MIN = 4; // px
const OVERVIEW_GROUP_VERTICAL_PADDING = 5; // px
@ -44,13 +46,13 @@ const OVERVIEW_GROUP_VERTICAL_PADDING = 5; // px
*
* @param nsIDOMNode parent
* The parent node holding the overview.
* @param Object blueprint
* List of names and colors defining markers.
* @param Array<String> filter
* List of names of marker types that should not be shown.
*/
function MarkersOverview(parent, blueprint, ...args) {
function MarkersOverview(parent, filter=[], ...args) {
AbstractCanvasGraph.apply(this, [parent, "markers-overview", ...args]);
this.setTheme();
this.setBlueprint(blueprint);
this.setFilter(filter);
}
MarkersOverview.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
@ -64,21 +66,36 @@ MarkersOverview.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
* Compute the height of the overview.
*/
get fixedHeight() {
return this.headerHeight + this.rowHeight * (this._lastGroup + 1);
return this.headerHeight + this.rowHeight * this._numberOfGroups;
},
/**
* List of names and colors used to paint this overview.
* @see TIMELINE_BLUEPRINT in timeline/widgets/global.js
* List of marker types that should not be shown in the graph.
*/
setBlueprint: function(blueprint) {
setFilter: function (filter) {
this._paintBatches = new Map();
this._lastGroup = 0;
this._filter = filter;
this._groupMap = Object.create(null);
for (let type in blueprint) {
this._paintBatches.set(type, { style: blueprint[type], batch: [] });
this._lastGroup = Math.max(this._lastGroup, blueprint[type].group || 0);
let observedGroups = new Set();
for (let type in TIMELINE_BLUEPRINT) {
if (filter.indexOf(type) !== -1) {
continue;
}
this._paintBatches.set(type, { definition: TIMELINE_BLUEPRINT[type], batch: [] });
observedGroups.add(TIMELINE_BLUEPRINT[type].group);
}
// Take our set of observed groups and order them and map
// the group numbers to fill in the holes via `_groupMap`.
// This normalizes our rows by removing rows that aren't used
// if filters are enabled.
let actualPosition = 0;
for (let groupNumber of Array.from(observedGroups).sort()) {
this._groupMap[groupNumber] = actualPosition++;
}
this._numberOfGroups = Object.keys(this._groupMap).length;
},
/**
@ -103,17 +120,19 @@ MarkersOverview.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
// Group markers into separate paint batches. This is necessary to
// draw all markers sharing the same style at once.
for (let marker of markers) {
let markerType = this._paintBatches.get(marker.name);
if (markerType) {
markerType.batch.push(marker);
// Again skip over markers that we're filtering -- we don't want them
// to be labeled as "Unknown"
if (!MarkerUtils.isMarkerValid(marker, this._filter)) {
continue;
}
let markerType = this._paintBatches.get(marker.name) || this._paintBatches.get("UNKNOWN");
markerType.batch.push(marker);
}
// Calculate each row's height, and the time-based scaling.
let totalGroups = this._lastGroup + 1;
let groupHeight = this.rowHeight * this._pixelRatio;
let groupPadding = this.groupPadding * this._pixelRatio;
let headerHeight = this.headerHeight * this._pixelRatio;
@ -132,7 +151,7 @@ MarkersOverview.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
ctx.fillStyle = this.alternatingBackgroundColor;
ctx.beginPath();
for (let i = 0; i < totalGroups; i += 2) {
for (let i = 0; i < this._numberOfGroups; i += 2) {
let top = headerHeight + i * groupHeight;
ctx.rect(0, top, canvasWidth, groupHeight);
}
@ -172,11 +191,12 @@ MarkersOverview.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
// Draw the timeline markers.
for (let [, { style, batch }] of this._paintBatches) {
let top = headerHeight + style.group * groupHeight + groupPadding / 2;
for (let [, { definition, batch }] of this._paintBatches) {
let group = this._groupMap[definition.group];
let top = headerHeight + group * groupHeight + groupPadding / 2;
let height = groupHeight - groupPadding;
let color = getColor(style.colorName, this.theme);
let color = getColor(definition.colorName, this.theme);
ctx.fillStyle = color;
ctx.beginPath();

Просмотреть файл

@ -398,16 +398,6 @@ let PerformanceController = {
return null;
},
/**
* Gets the current timeline blueprint without the hidden markers.
* @return object
*/
getTimelineBlueprint: function() {
let blueprint = TIMELINE_BLUEPRINT;
let hiddenMarkers = this.getPref("hidden-markers");
return RecordingUtils.getFilteredBlueprint({ blueprint, hiddenMarkers });
},
/**
* Fired from RecordingsView, we listen on the PerformanceController so we can
* set it here and re-emit on the controller, where all views can listen.

Просмотреть файл

@ -13,7 +13,6 @@ support-files =
# that need to be moved over to performance tool
[browser_aaa-run-first-leaktest.js]
[browser_marker-utils.js]
[browser_markers-cycle-collection.js]
[browser_markers-gc.js]
[browser_markers-parse-html.js]
@ -137,7 +136,6 @@ support-files =
[browser_profiler_tree-view-08.js]
[browser_profiler_tree-view-09.js]
[browser_profiler_tree-view-10.js]
[browser_timeline-blueprint.js]
[browser_timeline-filters.js]
[browser_timeline-waterfall-background.js]
[browser_timeline-waterfall-generic.js]

Просмотреть файл

@ -1,70 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests the marker utils methods.
*/
function* spawnTest() {
let { TIMELINE_BLUEPRINT } = devtools.require("devtools/performance/markers");
let Utils = devtools.require("devtools/performance/marker-utils");
Services.prefs.setBoolPref(PLATFORM_DATA_PREF, false);
is(Utils.getMarkerLabel({ name: "DOMEvent" }), "DOM Event",
"getMarkerLabel() returns a simple label");
is(Utils.getMarkerLabel({ name: "Javascript", causeName: "setTimeout handler" }), "setTimeout",
"getMarkerLabel() returns a label defined via function");
ok(Utils.getMarkerFields({ name: "Paint" }).length === 0,
"getMarkerFields() returns an empty array when no fields defined");
let fields = Utils.getMarkerFields({ name: "ConsoleTime", causeName: "snowstorm" });
is(fields[0].label, "Timer Name:", "getMarkerFields() returns an array with proper label");
is(fields[0].value, "snowstorm", "getMarkerFields() returns an array with proper value");
fields = Utils.getMarkerFields({ name: "DOMEvent", type: "mouseclick" });
is(fields.length, 1, "getMarkerFields() ignores fields that are not found on marker");
is(fields[0].label, "Event Type:", "getMarkerFields() returns an array with proper label");
is(fields[0].value, "mouseclick", "getMarkerFields() returns an array with proper value");
fields = Utils.getMarkerFields({ name: "DOMEvent", eventPhase: Ci.nsIDOMEvent.AT_TARGET, type: "mouseclick" });
is(fields.length, 2, "getMarkerFields() returns multiple fields when using a fields function");
is(fields[0].label, "Event Type:", "getMarkerFields() correctly returns fields via function (1)");
is(fields[0].value, "mouseclick", "getMarkerFields() correctly returns fields via function (2)");
is(fields[1].label, "Phase:", "getMarkerFields() correctly returns fields via function (3)");
is(fields[1].value, "Target", "getMarkerFields() correctly returns fields via function (4)");
is(Utils.getMarkerFields({ name: "Javascript", causeName: "Some Platform Field" })[0].value, "(Gecko)",
"Correctly obfuscates JS markers when platform data is off.");
Services.prefs.setBoolPref(PLATFORM_DATA_PREF, true);
is(Utils.getMarkerFields({ name: "Javascript", causeName: "Some Platform Field" })[0].value, "Some Platform Field",
"Correctly deobfuscates JS markers when platform data is on.");
is(Utils.getMarkerClassName("Javascript"), "Function Call",
"getMarkerClassName() returns correct string when defined via function");
is(Utils.getMarkerClassName("GarbageCollection"), "GC Event",
"getMarkerClassName() returns correct string when defined via function");
is(Utils.getMarkerClassName("Reflow"), "Layout",
"getMarkerClassName() returns correct string when defined via string");
TIMELINE_BLUEPRINT["fakemarker"] = { group: 0 };
try {
Utils.getMarkerClassName("fakemarker");
ok(false, "getMarkerClassName() should throw when no label on blueprint.");
} catch (e) {
ok(true, "getMarkerClassName() should throw when no label on blueprint.");
}
TIMELINE_BLUEPRINT["fakemarker"] = { group: 0, label: () => void 0};
try {
Utils.getMarkerClassName("fakemarker");
ok(false, "getMarkerClassName() should throw when label function returnd undefined.");
} catch (e) {
ok(true, "getMarkerClassName() should throw when label function returnd undefined.");
}
delete TIMELINE_BLUEPRINT["fakemarker"];
finish();
}

Просмотреть файл

@ -1,34 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if the timeline blueprint has a correct structure.
*/
function* spawnTest() {
let { TIMELINE_BLUEPRINT } = devtools.require("devtools/performance/markers");
ok(TIMELINE_BLUEPRINT,
"A timeline blueprint should be available.");
ok(Object.keys(TIMELINE_BLUEPRINT).length,
"The timeline blueprint has at least one entry.");
for (let [key, value] of Iterator(TIMELINE_BLUEPRINT)) {
if (key.startsWith("meta::")) {
ok(!("group" in value),
"No meta entry in the timeline blueprint can contain a `group` key.");
ok("colorName" in value,
"Each meta entry in the timeline blueprint contains a `colorName` key.");
ok("label" in value,
"Each meta entry in the timeline blueprint contains a `label` key.");
} else {
ok("group" in value,
"Each entry in the timeline blueprint contains a `group` key.");
ok("colorName" in value,
"Each entry in the timeline blueprint contains a `colorName` key.");
ok("label" in value,
"Each entry in the timeline blueprint contains a `label` key.");
}
}
}

Просмотреть файл

@ -5,6 +5,8 @@
* Tests markers filtering mechanism.
*/
const EPSILON = 0.00000001;
function* spawnTest() {
let { panel } = yield initPerformance(SIMPLE_URL);
let { $, $$, EVENTS, PerformanceController, OverviewView, WaterfallView } = panel.panelWin;
@ -24,20 +26,34 @@ function* spawnTest() {
yield stopRecording(panel);
// Push some fake markers of a type we do not have a blueprint for
let markers = PerformanceController.getCurrentRecording().getMarkers();
let endTime = markers[markers.length - 1].end;
markers.push({ name: "CustomMarker", start: endTime + EPSILON, end: endTime + (EPSILON * 2) });
markers.push({ name: "CustomMarker", start: endTime + (EPSILON * 3), end: endTime + (EPSILON * 4) });
// Invalidate marker cache
WaterfallView._cache.delete(markers);
// Select everything
OverviewView.setTimeInterval({ startTime: 0, endTime: Number.MAX_VALUE })
let waterfallRendered = WaterfallView.once(EVENTS.WATERFALL_RENDERED);
OverviewView.setTimeInterval({ startTime: 0, endTime: Number.MAX_VALUE });
$("#filter-button").click();
let menuItem1 = $("menuitem[marker-type=Styles]");
let menuItem2 = $("menuitem[marker-type=Reflow]");
let menuItem3 = $("menuitem[marker-type=Paint]");
let menuItem4 = $("menuitem[marker-type=UNKNOWN]");
let overview = OverviewView.graphs.get("timeline");
let originalHeight = overview.fixedHeight;
yield waterfallRendered;
ok($(".waterfall-marker-bar[type=Styles]"), "Found at least one 'Styles' marker (1)");
ok($(".waterfall-marker-bar[type=Reflow]"), "Found at least one 'Reflow' marker (1)");
ok($(".waterfall-marker-bar[type=Paint]"), "Found at least one 'Paint' marker (1)");
ok($(".waterfall-marker-bar[type=CustomMarker]"), "Found at least one 'Unknown' marker (1)");
let heightBefore = overview.fixedHeight;
EventUtils.synthesizeMouseAtCenter(menuItem1, {type: "mouseup"}, panel.panelWin);
@ -47,6 +63,7 @@ function* spawnTest() {
ok(!$(".waterfall-marker-bar[type=Styles]"), "No 'Styles' marker (2)");
ok($(".waterfall-marker-bar[type=Reflow]"), "Found at least one 'Reflow' marker (2)");
ok($(".waterfall-marker-bar[type=Paint]"), "Found at least one 'Paint' marker (2)");
ok($(".waterfall-marker-bar[type=CustomMarker]"), "Found at least one 'Unknown' marker (2)");
heightBefore = overview.fixedHeight;
EventUtils.synthesizeMouseAtCenter(menuItem2, {type: "mouseup"}, panel.panelWin);
@ -56,6 +73,7 @@ function* spawnTest() {
ok(!$(".waterfall-marker-bar[type=Styles]"), "No 'Styles' marker (3)");
ok(!$(".waterfall-marker-bar[type=Reflow]"), "No 'Reflow' marker (3)");
ok($(".waterfall-marker-bar[type=Paint]"), "Found at least one 'Paint' marker (3)");
ok($(".waterfall-marker-bar[type=CustomMarker]"), "Found at least one 'Unknown' marker (3)");
heightBefore = overview.fixedHeight;
EventUtils.synthesizeMouseAtCenter(menuItem3, {type: "mouseup"}, panel.panelWin);
@ -65,15 +83,25 @@ function* spawnTest() {
ok(!$(".waterfall-marker-bar[type=Styles]"), "No 'Styles' marker (4)");
ok(!$(".waterfall-marker-bar[type=Reflow]"), "No 'Reflow' marker (4)");
ok(!$(".waterfall-marker-bar[type=Paint]"), "No 'Paint' marker (4)");
ok($(".waterfall-marker-bar[type=CustomMarker]"), "Found at least one 'Unknown' marker (4)");
EventUtils.synthesizeMouseAtCenter(menuItem4, {type: "mouseup"}, panel.panelWin);
yield waitForOverviewAndCommand(overview, menuItem4);
ok(!$(".waterfall-marker-bar[type=Styles]"), "No 'Styles' marker (5)");
ok(!$(".waterfall-marker-bar[type=Reflow]"), "No 'Reflow' marker (5)");
ok(!$(".waterfall-marker-bar[type=Paint]"), "No 'Paint' marker (5)");
ok(!$(".waterfall-marker-bar[type=CustomMarker]"), "No 'Unknown' marker (5)");
for (let item of [menuItem1, menuItem2, menuItem3]) {
EventUtils.synthesizeMouseAtCenter(item, {type: "mouseup"}, panel.panelWin);
yield waitForOverviewAndCommand(overview, item);
}
ok($(".waterfall-marker-bar[type=Styles]"), "Found at least one 'Styles' marker (5)");
ok($(".waterfall-marker-bar[type=Reflow]"), "Found at least one 'Reflow' marker (5)");
ok($(".waterfall-marker-bar[type=Paint]"), "Found at least one 'Paint' marker (5)");
ok($(".waterfall-marker-bar[type=Styles]"), "Found at least one 'Styles' marker (6)");
ok($(".waterfall-marker-bar[type=Reflow]"), "Found at least one 'Reflow' marker (6)");
ok($(".waterfall-marker-bar[type=Paint]"), "Found at least one 'Paint' marker (6)");
ok(!$(".waterfall-marker-bar[type=CustomMarker]"), "No 'Unknown' marker (6)");
is(overview.fixedHeight, originalHeight, "Overview restored");

Просмотреть файл

@ -61,6 +61,7 @@ let DEFAULT_PREFS = [
"devtools.performance.profiler.buffer-size",
"devtools.performance.profiler.sample-frequency-khz",
"devtools.performance.ui.experimental",
"devtools.performance.timeline.hidden-markers",
].reduce((prefs, pref) => {
prefs[pref] = Preferences.get(pref);
return prefs;

Просмотреть файл

@ -5,8 +5,11 @@
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
let { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
const RecordingUtils = devtools.require("devtools/performance/recording-utils");
const PLATFORM_DATA_PREF = "devtools.performance.ui.show-platform-data";
/**
* Get a path in a FrameNode call tree.
*/

Просмотреть файл

@ -0,0 +1,29 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if the timeline blueprint has a correct structure.
*/
function run_test() {
run_next_test();
}
add_task(function () {
let { TIMELINE_BLUEPRINT } = devtools.require("devtools/performance/markers");
ok(TIMELINE_BLUEPRINT,
"A timeline blueprint should be available.");
ok(Object.keys(TIMELINE_BLUEPRINT).length,
"The timeline blueprint has at least one entry.");
for (let [key, value] of Iterator(TIMELINE_BLUEPRINT)) {
ok("group" in value,
"Each entry in the timeline blueprint contains a `group` key.");
ok("colorName" in value,
"Each entry in the timeline blueprint contains a `colorName` key.");
ok("label" in value,
"Each entry in the timeline blueprint contains a `label` key.");
}
});

Просмотреть файл

@ -0,0 +1,88 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests the marker utils methods.
*/
function run_test() {
run_next_test();
}
add_task(function () {
let { TIMELINE_BLUEPRINT } = devtools.require("devtools/performance/markers");
let Utils = devtools.require("devtools/performance/marker-utils");
Services.prefs.setBoolPref(PLATFORM_DATA_PREF, false);
equal(Utils.getMarkerLabel({ name: "DOMEvent" }), "DOM Event",
"getMarkerLabel() returns a simple label");
equal(Utils.getMarkerLabel({ name: "Javascript", causeName: "setTimeout handler" }), "setTimeout",
"getMarkerLabel() returns a label defined via function");
ok(Utils.getMarkerFields({ name: "Paint" }).length === 0,
"getMarkerFields() returns an empty array when no fields defined");
let fields = Utils.getMarkerFields({ name: "ConsoleTime", causeName: "snowstorm" });
equal(fields[0].label, "Timer Name:", "getMarkerFields() returns an array with proper label");
equal(fields[0].value, "snowstorm", "getMarkerFields() returns an array with proper value");
fields = Utils.getMarkerFields({ name: "DOMEvent", type: "mouseclick" });
equal(fields.length, 1, "getMarkerFields() ignores fields that are not found on marker");
equal(fields[0].label, "Event Type:", "getMarkerFields() returns an array with proper label");
equal(fields[0].value, "mouseclick", "getMarkerFields() returns an array with proper value");
fields = Utils.getMarkerFields({ name: "DOMEvent", eventPhase: Ci.nsIDOMEvent.AT_TARGET, type: "mouseclick" });
equal(fields.length, 2, "getMarkerFields() returns multiple fields when using a fields function");
equal(fields[0].label, "Event Type:", "getMarkerFields() correctly returns fields via function (1)");
equal(fields[0].value, "mouseclick", "getMarkerFields() correctly returns fields via function (2)");
equal(fields[1].label, "Phase:", "getMarkerFields() correctly returns fields via function (3)");
equal(fields[1].value, "Target", "getMarkerFields() correctly returns fields via function (4)");
equal(Utils.getMarkerFields({ name: "Javascript", causeName: "Some Platform Field" })[0].value, "(Gecko)",
"Correctly obfuscates JS markers when platform data is off.");
Services.prefs.setBoolPref(PLATFORM_DATA_PREF, true);
equal(Utils.getMarkerFields({ name: "Javascript", causeName: "Some Platform Field" })[0].value, "Some Platform Field",
"Correctly deobfuscates JS markers when platform data is on.");
equal(Utils.getMarkerClassName("Javascript"), "Function Call",
"getMarkerClassName() returns correct string when defined via function");
equal(Utils.getMarkerClassName("GarbageCollection"), "GC Event",
"getMarkerClassName() returns correct string when defined via function");
equal(Utils.getMarkerClassName("Reflow"), "Layout",
"getMarkerClassName() returns correct string when defined via string");
TIMELINE_BLUEPRINT["fakemarker"] = { group: 0 };
try {
Utils.getMarkerClassName("fakemarker");
ok(false, "getMarkerClassName() should throw when no label on blueprint.");
} catch (e) {
ok(true, "getMarkerClassName() should throw when no label on blueprint.");
}
TIMELINE_BLUEPRINT["fakemarker"] = { group: 0, label: () => void 0};
try {
Utils.getMarkerClassName("fakemarker");
ok(false, "getMarkerClassName() should throw when label function returnd undefined.");
} catch (e) {
ok(true, "getMarkerClassName() should throw when label function returnd undefined.");
}
delete TIMELINE_BLUEPRINT["fakemarker"];
let customBlueprint = {
UNKNOWN: {
group: 1,
label: "MyDefault"
},
custom: {
group: 0,
label: "MyCustom"
}
};
equal(Utils.getBlueprintFor({ name: "Reflow" }).label, "Layout",
"Utils.getBlueprintFor() should return marker def for passed in marker.");
equal(Utils.getBlueprintFor({ name: "Not sure!" }).label(), "Unknown",
"Utils.getBlueprintFor() should return a default marker def if the marker is undefined.");
});

Просмотреть файл

@ -5,9 +5,11 @@ tail =
firefox-appdir = browser
skip-if = toolkit == 'android' || toolkit == 'gonk'
[test_profiler-categories.js]
[test_frame-utils-01.js]
[test_frame-utils-02.js]
[test_marker-blueprint.js]
[test_marker-utils.js]
[test_profiler-categories.js]
[test_tree-model-01.js]
[test_tree-model-02.js]
[test_tree-model-03.js]

Просмотреть файл

@ -32,6 +32,7 @@ let WaterfallView = Heritage.extend(DetailsSubview, {
this._onMarkerSelected = this._onMarkerSelected.bind(this);
this._onResize = this._onResize.bind(this);
this._onViewSource = this._onViewSource.bind(this);
this._hiddenMarkers = PerformanceController.getPref("hidden-markers");
this.headerContainer = $("#waterfall-header");
this.breakdownContainer = $("#waterfall-breakdown");
@ -111,8 +112,7 @@ let WaterfallView = Heritage.extend(DetailsSubview, {
* Called whenever an observed pref is changed.
*/
_onObservedPrefChange: function(_, prefName) {
let blueprint = PerformanceController.getTimelineBlueprint();
this._markersRoot.blueprint = blueprint;
this._hiddenMarkers = PerformanceController.getPref("hidden-markers");
},
/**
@ -136,7 +136,7 @@ let WaterfallView = Heritage.extend(DetailsSubview, {
WaterfallUtils.collapseMarkersIntoNode({
markerNode: rootMarkerNode,
markersList: markers
markersList: markers,
});
this._cache.set(markers, rootMarkerNode);
@ -160,8 +160,7 @@ let WaterfallView = Heritage.extend(DetailsSubview, {
this._markersRoot = root;
this._waterfallHeader = header;
let blueprint = PerformanceController.getTimelineBlueprint();
root.blueprint = blueprint;
root.filter = this._hiddenMarkers;
root.interval = interval;
root.on("selected", this._onMarkerSelected);
root.on("unselected", this._onMarkerSelected);

Просмотреть файл

@ -42,7 +42,7 @@ let OverviewView = {
initialize: function () {
this.graphs = new GraphsController({
root: $("#overview-pane"),
getBlueprint: () => PerformanceController.getTimelineBlueprint(),
getFilter: () => PerformanceController.getPref("hidden-markers"),
getTheme: () => PerformanceController.getTheme(),
});
@ -331,8 +331,8 @@ let OverviewView = {
case "hidden-markers": {
let graph;
if (graph = yield this.graphs.isAvailable("timeline")) {
let blueprint = PerformanceController.getTimelineBlueprint();
graph.setBlueprint(blueprint);
let filter = PerformanceController.getPref("hidden-markers");
graph.setFilter(filter);
graph.refresh({ force: true });
}
break;

Просмотреть файл

@ -52,3 +52,9 @@ player.timeLabel=%Ss
# drop-down list items that can be used to change the rate at which the
# animation runs (1x being the default, 2x being twice as fast).
player.playbackRateLabel=%Sx
# LOCALIZATION NOTE (timeline.timeGraduationLabel):
# This string is displayed at the top of the animation panel, next to each time
# graduation, to indicate what duration (in milliseconds) this graduation
# corresponds to.
timeline.timeGraduationLabel=%Sms

Просмотреть файл

@ -46,6 +46,7 @@ timeline.label.domevent=DOM Event
timeline.label.consoleTime=Console
timeline.label.garbageCollection=GC Event
timeline.label.timestamp=Timestamp
timeline.label.unknown=Unknown
# LOCALIZATION NOTE (graphs.memory):
# This string is displayed in the memory graph of the Performance tool,

Просмотреть файл

@ -1,3 +1,17 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* Animation-inspector specific theme variables */
.theme-dark {
--even-animation-timeline-background-color: rgba(255,255,255,0.03);
}
.theme-light {
--even-animation-timeline-background-color: rgba(128,128,128,0.03);
}
html {
height: 100%;
}
@ -32,6 +46,13 @@ body {
min-height: 20px;
}
/* The main animations container */
#players {
height: calc(100% - 20px);
overflow: auto;
}
/* The error message, shown when an invalid/unanimated element is selected */
#error-message {
@ -44,12 +65,6 @@ body {
display: none;
}
/* The animation players container */
#players {
flex: 1;
overflow: auto;
}
/* Element picker and toggle-all buttons */
@ -99,6 +114,156 @@ body {
}
}
/* Animation timeline component */
.animation-timeline {
height: 100%;
overflow: hidden;
/* The timeline gets its background-image from a canvas element created in
/browser/devtools/animationinspector/utils.js drawGraphElementBackground
thanks to document.mozSetImageElement("time-graduations", canvas)
This is done so that the background can be built dynamically from script */
background-image: -moz-element(#time-graduations);
background-repeat: repeat-y;
/* The animations are drawn 150px from the left edge so that animated nodes
can be displayed in a sidebar */
background-position: 150px 0;
display: flex;
flex-direction: column;
}
.animation-timeline .time-header {
margin-left: 150px;
height: 20px;
overflow: hidden;
position: relative;
border-bottom: 1px solid var(--theme-splitter-color);
}
.animation-timeline .time-header .time-tick {
position: absolute;
top: 3px;
}
.animation-timeline .animations {
width: 100%;
overflow-y: auto;
overflow-x: hidden;
margin: 0;
padding: 0;
list-style-type: none;
}
/* Animation block widgets */
.animation-timeline .animation {
margin: 4px 0;
height: 20px;
position: relative;
}
.animation-timeline .animation:nth-child(2n) {
background-color: var(--even-animation-timeline-background-color);
}
.animation-timeline .animation .target {
width: 150px;
overflow: hidden;
height: 100%;
}
.animation-timeline .animation-target {
background-color: transparent;
}
.animation-timeline .animation .time-block {
position: absolute;
top: 0;
left: 150px;
right: 0;
height: 100%;
}
/* Animation iterations */
.animation-timeline .animation .iterations {
position: relative;
height: 100%;
border: 1px solid var(--theme-highlight-lightorange);
box-sizing: border-box;
background: var(--theme-contrast-background);
/* Iterations are displayed with a repeating linear-gradient which size is
dynamically changed from JS */
background-image:
linear-gradient(to right,
var(--theme-highlight-lightorange) 0,
var(--theme-highlight-lightorange) 1px,
transparent 1px,
transparent 2px);
background-repeat: repeat-x;
background-position: -1px 0;
}
.animation-timeline .animation .iterations.infinite {
border-right-width: 0;
}
.animation-timeline .animation .iterations.infinite::before,
.animation-timeline .animation .iterations.infinite::after {
content: "";
position: absolute;
top: 0;
right: 0;
width: 0;
height: 0;
border-right: 4px solid var(--theme-body-background);
border-top: 4px solid transparent;
border-bottom: 4px solid transparent;
}
.animation-timeline .animation .iterations.infinite::after {
bottom: 0;
top: unset;
}
.animation-timeline .animation .animation-title {
height: 1.5em;
width: 100%;
box-sizing: border-box;
overflow: hidden;
}
.animation-timeline .animation .delay {
position: absolute;
top: 0;
height: 100%;
background-image: linear-gradient(to bottom,
transparent,
transparent 9px,
var(--theme-highlight-lightorange) 9px,
var(--theme-highlight-lightorange) 11px,
transparent 11px,
transparent);
}
.animation-timeline .animation .delay::before {
position: absolute;
content: "";
left: 0;
width: 2px;
height: 8px;
top: 50%;
margin-top: -4px;
background: var(--theme-highlight-lightorange);
}
.animation-timeline .animation .name {
position: absolute;
z-index: 1;
padding: 2px;
white-space: nowrap;
}
/* Animation target node gutter, contains a preview of the dom node */
.animation-target {

Просмотреть файл

@ -1502,6 +1502,13 @@ richlistitem[type~="action"][actiontype="searchengine"] > .ac-title-box > .ac-si
}
}
@media (-moz-os-version: windows-win10) and (-moz-windows-default-theme) {
.ac-url-text:not([selected="true"]),
.ac-action-text:not([selected="true"]) {
color: Highlight;
}
}
richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action-icon {
list-style-image: url("chrome://browser/skin/actionicon-tab.png");
-moz-image-region: rect(0, 16px, 11px, 0);

Просмотреть файл

@ -97,6 +97,7 @@ function run_test()
function stop_high_accuracy_watch() {
geolocation.clearWatch(watchID2);
check_results();
do_test_finished();
}
function check_results()

Просмотреть файл

@ -58,6 +58,14 @@ var ContextMenus = {
document.getElementById("contextmenu-uninstall").removeAttribute("hidden");
}
// Hide the enable/disable context menu items if the add-on was disabled by
// Firefox (e.g. unsigned or blocklisted add-on).
if (addon.appDisabled) {
document.getElementById("contextmenu-enable").setAttribute("hidden", "true");
document.getElementById("contextmenu-disable").setAttribute("hidden", "true");
return;
}
let enabled = this.target.getAttribute("isDisabled") != "true";
if (enabled) {
document.getElementById("contextmenu-enable").setAttribute("hidden", "true");
@ -311,16 +319,18 @@ var Addons = {
gStringBundle.formatStringFromName("addonStatus.uninstalled", [addon.name], 1);
let enableBtn = document.getElementById("enable-btn");
if (addon.appDisabled)
if (addon.appDisabled) {
enableBtn.setAttribute("disabled", "true");
else
} else {
enableBtn.removeAttribute("disabled");
}
let uninstallBtn = document.getElementById("uninstall-btn");
if (addon.scope == AddonManager.SCOPE_APPLICATION)
if (addon.scope == AddonManager.SCOPE_APPLICATION) {
uninstallBtn.setAttribute("disabled", "true");
else
} else {
uninstallBtn.removeAttribute("disabled");
}
let box = document.querySelector("#addons-details > .addon-item .options-box");
box.innerHTML = "";

Просмотреть файл

@ -6189,19 +6189,21 @@ let HealthReportStatusListener = {
};
var XPInstallObserver = {
init: function xpi_init() {
Services.obs.addObserver(XPInstallObserver, "addon-install-blocked", false);
Services.obs.addObserver(XPInstallObserver, "addon-install-started", false);
init: function() {
Services.obs.addObserver(this, "addon-install-blocked", false);
Services.obs.addObserver(this, "addon-install-started", false);
Services.obs.addObserver(this, "xpi-signature-changed", false);
Services.obs.addObserver(this, "browser-delayed-startup-finished", false);
AddonManager.addInstallListener(XPInstallObserver);
AddonManager.addInstallListener(this);
},
observe: function xpi_observer(aSubject, aTopic, aData) {
observe: function(aSubject, aTopic, aData) {
switch (aTopic) {
case "addon-install-started":
NativeWindow.toast.show(Strings.browser.GetStringFromName("alertAddonsDownloading"), "short");
break;
case "addon-install-blocked":
case "addon-install-blocked": {
let installInfo = aSubject.QueryInterface(Ci.amIWebInstallInfo);
let tab = BrowserApp.getTabForBrowser(installInfo.browser);
if (!tab)
@ -6268,9 +6270,43 @@ var XPInstallObserver = {
}
NativeWindow.doorhanger.show(message, aTopic, buttons, tab.id);
break;
}
case "xpi-signature-changed": {
if (JSON.parse(aData).disabled.length) {
this._notifyUnsignedAddonsDisabled();
}
break;
}
case "browser-delayed-startup-finished": {
let disabledAddons = AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_DISABLED);
for (let id of disabledAddons) {
if (AddonManager.getAddonByID(id).signedState <= AddonManager.SIGNEDSTATE_MISSING) {
this._notifyUnsignedAddonsDisabled();
break;
}
}
break;
}
}
},
_notifyUnsignedAddonsDisabled: function() {
new Prompt({
window: window,
title: Strings.browser.GetStringFromName("unsignedAddonsDisabled.title"),
message: Strings.browser.GetStringFromName("unsignedAddonsDisabled.message"),
buttons: [
Strings.browser.GetStringFromName("unsignedAddonsDisabled.viewAddons"),
Strings.browser.GetStringFromName("unsignedAddonsDisabled.dismiss")
]
}).show((data) => {
if (data.button === 0) {
// TODO: Open about:addons to show only unsigned add-ons?
BrowserApp.addTab("about:addons", { parentId: BrowserApp.selectedTab.id });
}
});
},
onInstallEnded: function(aInstall, aAddon) {
// Don't create a notification for distribution add-ons.
if (Distribution.pendingAddonInstalls.has(aInstall)) {

Просмотреть файл

@ -45,6 +45,13 @@ addonError.titleError=Error
addonError.titleBlocked=Blocked add-on
addonError.learnMore=Learn more
# LOCALIZATION NOTE (unsignedAddonsDisabled.title, unsignedAddonsDisabled.message):
# These strings will appear in a dialog when Firefox detects that installed add-ons cannot be verified.
unsignedAddonsDisabled.title=Unverified add-ons
unsignedAddonsDisabled.message=One or more installed add-ons cannot be verified and have been disabled.
unsignedAddonsDisabled.dismiss=Dismiss
unsignedAddonsDisabled.viewAddons=View add-ons
# LOCALIZATION NOTE (addonError-1, addonError-2, addonError-3, addonError-4, addonError-5):
# #1 is the add-on name, #2 is the add-on host, #3 is the application name
addonError-1=The add-on could not be downloaded because of a connection failure on #2.

Просмотреть файл

@ -66,5 +66,55 @@ this.ContentTaskUtils = {
tries++;
}, interval);
});
}
},
/**
* Waits for an event to be fired on a specified element.
*
* Usage:
* let promiseEvent = ContentTasKUtils.waitForEvent(element, "eventName");
* // Do some processing here that will cause the event to be fired
* // ...
* // Now yield until the Promise is fulfilled
* let receivedEvent = yield promiseEvent;
*
* @param {Element} subject
* The element that should receive the event.
* @param {string} eventName
* Name of the event to listen to.
* @param {bool} capture [optional]
* True to use a capturing listener.
* @param {function} checkFn [optional]
* Called with the Event object as argument, should return true if the
* event is the expected one, or false if it should be ignored and
* listening should continue. If not specified, the first event with
* the specified name resolves the returned promise.
*
* @note Because this function is intended for testing, any error in checkFn
* will cause the returned promise to be rejected instead of waiting for
* the next event, since this is probably a bug in the test.
*
* @returns {Promise}
* @resolves The Event object.
*/
waitForEvent(subject, eventName, capture, checkFn) {
return new Promise((resolve, reject) => {
subject.addEventListener(eventName, function listener(event) {
try {
if (checkFn && !checkFn(event)) {
return;
}
subject.removeEventListener(eventName, listener, capture);
resolve(event);
} catch (ex) {
try {
subject.removeEventListener(eventName, listener, capture);
} catch (ex2) {
// Maybe the provided object does not support removeEventListener.
}
reject(ex);
}
}, capture);
});
},
};

Просмотреть файл

@ -1239,47 +1239,67 @@ function do_load_child_test_harness()
* @param optionalCallback.
* Optional function to be called (in parent) when test on child is
* complete. If provided, the function must call do_test_finished();
* @return Promise Resolved when the test in the child is complete.
*/
function run_test_in_child(testFile, optionalCallback)
{
var callback = (typeof optionalCallback == 'undefined') ?
do_test_finished : optionalCallback;
return new Promise((resolve) => {
var callback = () => {
resolve();
if (typeof optionalCallback == 'undefined') {
do_test_finished();
} else {
optionalCallback();
}
};
do_load_child_test_harness();
do_load_child_test_harness();
var testPath = do_get_file(testFile).path.replace(/\\/g, "/");
do_test_pending("run in child");
sendCommand("_testLogger.info('CHILD-TEST-STARTED'); "
+ "const _TEST_FILE=['" + testPath + "']; "
+ "_execute_test(); "
+ "_testLogger.info('CHILD-TEST-COMPLETED');",
callback);
var testPath = do_get_file(testFile).path.replace(/\\/g, "/");
do_test_pending("run in child");
sendCommand("_testLogger.info('CHILD-TEST-STARTED'); "
+ "const _TEST_FILE=['" + testPath + "']; "
+ "_execute_test(); "
+ "_testLogger.info('CHILD-TEST-COMPLETED');",
callback);
});
}
/**
* Execute a given function as soon as a particular cross-process message is received.
* Must be paired with do_send_remote_message or equivalent ProcessMessageManager calls.
*
* @param optionalCallback
* Optional callback that is invoked when the message is received. If provided,
* the function must call do_test_finished().
* @return Promise Promise that is resolved when the message is received.
*/
function do_await_remote_message(name, callback)
function do_await_remote_message(name, optionalCallback)
{
var listener = {
receiveMessage: function(message) {
if (message.name == name) {
mm.removeMessageListener(name, listener);
callback();
do_test_finished();
return new Promise((resolve) => {
var listener = {
receiveMessage: function(message) {
if (message.name == name) {
mm.removeMessageListener(name, listener);
resolve();
if (optionalCallback) {
optionalCallback();
} else {
do_test_finished();
}
}
}
}
};
};
var mm;
if (runningInParent) {
mm = Cc["@mozilla.org/parentprocessmessagemanager;1"].getService(Ci.nsIMessageBroadcaster);
} else {
mm = Cc["@mozilla.org/childprocessmessagemanager;1"].getService(Ci.nsISyncMessageSender);
}
do_test_pending();
mm.addMessageListener(name, listener);
var mm;
if (runningInParent) {
mm = Cc["@mozilla.org/parentprocessmessagemanager;1"].getService(Ci.nsIMessageBroadcaster);
} else {
mm = Cc["@mozilla.org/childprocessmessagemanager;1"].getService(Ci.nsISyncMessageSender);
}
do_test_pending();
mm.addMessageListener(name, listener);
});
}
/**

Просмотреть файл

@ -132,20 +132,11 @@ this.ReaderMode = {
// We pass in a helper function to determine if a node is visible, because
// it uses gecko APIs that the engine-agnostic readability code can't rely
// upon.
// NB: we need to do a flush the first time we call this, so we keep track of
// this using a property:
this._needFlushForVisibilityCheck = true;
return new Readability(uri, doc).isProbablyReaderable(this.isNodeVisible.bind(this, utils));
},
isNodeVisible: function(utils, node) {
let bounds;
if (this._needFlushForVisibilityCheck) {
bounds = node.getBoundingClientRect();
this._needFlushForVisibilityCheck = false;
} else {
bounds = utils.getBoundsWithoutFlushing(node);
}
let bounds = utils.getBoundsWithoutFlushing(node);
return bounds.height > 0 && bounds.width > 0;
},

Просмотреть файл

@ -155,6 +155,13 @@ this.TelemetryController = Object.freeze({
return Impl.setupTelemetry(true);
},
/**
* Used only for testing purposes.
*/
setupContent: function() {
return Impl.setupContentTelemetry(true);
},
/**
* Send a notification.
*/
@ -587,21 +594,20 @@ let Impl = {
Telemetry.canRecordBase = enabled || IS_UNIFIED_TELEMETRY;
#ifdef MOZILLA_OFFICIAL
if (!Telemetry.isOfficialTelemetry && !this._testMode) {
// We can't send data; no point in initializing observers etc.
// Only do this for official builds so that e.g. developer builds
// still enable Telemetry based on prefs.
Telemetry.canRecordExtended = false;
this._log.config("enableTelemetryRecording - Can't send data, disabling extended Telemetry recording.");
}
// Enable extended telemetry if:
// * the telemetry preference is set and
// * this is an official build or we are in test-mode
// We only do the latter check for official builds so that e.g. developer builds
// still enable Telemetry based on prefs.
Telemetry.canRecordExtended = enabled && (Telemetry.isOfficialTelemetry || this._testMode);
#else
// Turn off extended telemetry recording if disabled by preferences or if base/telemetry
// telemetry recording is off.
Telemetry.canRecordExtended = enabled;
#endif
if (!enabled || !Telemetry.canRecordBase) {
// Turn off extended telemetry recording if disabled by preferences or if base/telemetry
// telemetry recording is off.
Telemetry.canRecordExtended = false;
this._log.config("enableTelemetryRecording - Disabling extended Telemetry recording.");
}
this._log.config("enableTelemetryRecording - canRecordBase:" + Telemetry.canRecordBase +
", canRecordExtended: " + Telemetry.canRecordExtended);
return Telemetry.canRecordBase;
},
@ -689,6 +695,21 @@ let Impl = {
return this._delayedInitTaskDeferred.promise;
},
/**
* This triggers basic telemetry initialization for content processes.
* @param {Boolean} [testing=false] True if we are in test mode, false otherwise.
*/
setupContentTelemetry: function (testing = false) {
this._testMode = testing;
// We call |enableTelemetryRecording| here to make sure that Telemetry.canRecord* flags
// are in sync between chrome and content processes.
if (!this.enableTelemetryRecording()) {
this._log.trace("setupContentTelemetry - Content process recording disabled.");
return;
}
},
// Do proper shutdown waiting and cleanup.
_cleanupOnShutdown: Task.async(function*() {
if (!this._initialized) {
@ -760,13 +781,8 @@ let Impl = {
// profile-after-change is only registered for chrome processes.
return this.setupTelemetry();
case "app-startup":
// app-startup is only registered for content processes. We call
// |enableTelemetryRecording| here to make sure that Telemetry.canRecord* flags
// are in sync between chrome and content processes.
if (!this.enableTelemetryRecording()) {
this._log.trace("observe - Content process recording disabled.");
return;
}
// app-startup is only registered for content processes.
return this.setupContentTelemetry();
break;
}
},

Просмотреть файл

@ -58,7 +58,7 @@ const MIN_SUBSESSION_LENGTH_MS = 10 * 60 * 1000;
#expand const HISTOGRAMS_FILE_VERSION = "__HISTOGRAMS_FILE_VERSION__";
const LOGGER_NAME = "Toolkit.Telemetry";
const LOGGER_PREFIX = "TelemetrySession::";
const LOGGER_PREFIX = "TelemetrySession" + (IS_CONTENT_PROCESS ? "#content::" : "::");
const PREF_BRANCH = "toolkit.telemetry.";
const PREF_PREVIOUS_BUILDID = PREF_BRANCH + "previousBuildID";
@ -775,7 +775,7 @@ this.TelemetrySession = Object.freeze({
let Impl = {
_histograms: {},
_initialized: false,
_log: null,
_logger: null,
_prevValues: {},
// Regex that matches histograms we care about during startup.
// Keep this in sync with gen-histogram-bucket-ranges.py.
@ -817,6 +817,13 @@ let Impl = {
// Used to serialize session state writes to disk.
_stateSaveSerializer: new SaveSerializer(),
get _log() {
if (!this._logger) {
this._logger = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX);
}
return this._logger;
},
/**
* Gets a series of simple measurements (counters). At the moment, this
* only returns startup data from nsIAppStartup.getStartupInfo().
@ -1415,10 +1422,6 @@ let Impl = {
*/
setupChromeProcess: function setupChromeProcess(testing) {
this._initStarted = true;
if (testing && !this._log) {
this._log = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX);
}
this._log.trace("setupChromeProcess");
if (this._delayedInitTask) {
@ -1718,10 +1721,6 @@ let Impl = {
* This observer drives telemetry.
*/
observe: function (aSubject, aTopic, aData) {
if (!this._log) {
this._log = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX);
}
// Prevent the cycle collector begin topic from cluttering the log.
if (aTopic != TOPIC_CYCLE_COLLECTOR_BEGIN) {
this._log.trace("observe - " + aTopic + " notified.");

Просмотреть файл

@ -186,12 +186,14 @@ function promiseRejects(promise) {
return promise.then(() => false, () => true);
}
// Set logging preferences for all the tests.
Services.prefs.setCharPref("toolkit.telemetry.log.level", "Trace");
TelemetryController.initLogging();
if (runningInParent) {
// Set logging preferences for all the tests.
Services.prefs.setCharPref("toolkit.telemetry.log.level", "Trace");
// Telemetry archiving should be on.
Services.prefs.setBoolPref("toolkit.telemetry.archive.enabled", true);
}
// Telemetry archiving should be on.
Services.prefs.setBoolPref("toolkit.telemetry.archive.enabled", true);
TelemetryController.initLogging();
// Avoid timers interrupting test behavior.
fakeSchedulerTimer(() => {}, () => {});

Просмотреть файл

@ -0,0 +1,89 @@
Cu.import("resource://gre/modules/TelemetryController.jsm", this);
Cu.import("resource://gre/modules/TelemetrySession.jsm", this);
Cu.import("resource://gre/modules/PromiseUtils.jsm", this);
const Telemetry = Cc["@mozilla.org/base/telemetry;1"].getService(Ci.nsITelemetry);
const MESSAGE_TELEMETRY_PAYLOAD = "Telemetry:Payload";
const PLATFORM_VERSION = "1.9.2";
const APP_VERSION = "1";
const APP_ID = "xpcshell@tests.mozilla.org";
const APP_NAME = "XPCShell";
function run_child_test() {
// Setup histograms with some fixed values.
let flagHist = Telemetry.getHistogramById("TELEMETRY_TEST_FLAG");
flagHist.add(1);
let countHist = Telemetry.getHistogramById("TELEMETRY_TEST_COUNT");
countHist.add();
countHist.add();
let flagKeyed = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_FLAG");
flagKeyed.add("a", 1);
flagKeyed.add("b", 1);
let countKeyed = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_COUNT");
countKeyed.add("a");
countKeyed.add("b");
countKeyed.add("b");
// Check payload values.
const payload = TelemetrySession.getPayload("test-ping");
check_histogram_values(payload);
}
function check_histogram_values(payload) {
const hs = payload.histograms;
Assert.ok("TELEMETRY_TEST_COUNT" in hs, "Should have count test histogram.");
Assert.ok("TELEMETRY_TEST_FLAG" in hs, "Should have flag test histogram.");
Assert.equal(hs["TELEMETRY_TEST_COUNT"].sum, 2,
"Count test histogram should have the right value.");
Assert.equal(hs["TELEMETRY_TEST_FLAG"].sum, 1,
"Flag test histogram should have the right value.");
const kh = payload.keyedHistograms;
Assert.ok("TELEMETRY_TEST_KEYED_COUNT" in kh, "Should have keyed count test histogram.");
Assert.ok("TELEMETRY_TEST_KEYED_FLAG" in kh, "Should have keyed flag test histogram.");
Assert.equal(kh["TELEMETRY_TEST_KEYED_COUNT"]["a"].sum, 1,
"Keyed count test histogram should have the right value.");
Assert.equal(kh["TELEMETRY_TEST_KEYED_COUNT"]["b"].sum, 2,
"Keyed count test histogram should have the right value.");
Assert.equal(kh["TELEMETRY_TEST_KEYED_FLAG"]["a"].sum, 1,
"Keyed flag test histogram should have the right value.");
Assert.equal(kh["TELEMETRY_TEST_KEYED_FLAG"]["b"].sum, 1,
"Keyed flag test histogram should have the right value.");
}
add_task(function*() {
if (!runningInParent) {
TelemetryController.setupContent();
run_child_test();
return;
}
// Setup.
do_get_profile(true);
loadAddonManager(APP_ID, APP_NAME, APP_VERSION, PLATFORM_VERSION);
Services.prefs.setBoolPref("toolkit.telemetry.enabled", true);
yield TelemetryController.setup();
yield TelemetrySession.setup();
// Run test in child and wait until it is finished.
yield run_test_in_child("test_ChildHistograms.js");
// Gather payload from child.
let promiseMessage = do_await_remote_message(MESSAGE_TELEMETRY_PAYLOAD);
TelemetrySession.requestChildPayloads();
yield promiseMessage;
// Check child payload.
const payload = TelemetrySession.getPayload("test-ping");
Assert.ok("childPayloads" in payload, "Should have child payloads.");
Assert.equal(payload.childPayloads.length, 1, "Should have received one child payload so far.");
Assert.ok("histograms" in payload.childPayloads[0], "Child payload should have histograms.");
Assert.ok("keyedHistograms" in payload.childPayloads[0], "Child payload should have keyed histograms.");
check_histogram_values(payload.childPayloads[0]);
do_test_finished();
});

Просмотреть файл

@ -61,11 +61,13 @@ add_task(function* test_sendTimeout() {
yield TelemetryController.setup();
TelemetrySend.setServer("http://localhost:" + httpServer.identity.primaryPort);
yield TelemetryController.submitExternalPing("test-ping-type", {});
let submissionPromise = TelemetryController.submitExternalPing("test-ping-type", {});
// Trigger the AsyncShutdown phase TelemetryController hangs off.
AsyncShutdown.profileBeforeChange._trigger();
AsyncShutdown.sendTelemetry._trigger();
// Now wait for the ping submission.
yield submissionPromise;
// If we get here, we didn't time out in the shutdown routines.
Assert.ok(true, "Didn't time out on shutdown.");

Просмотреть файл

@ -50,3 +50,5 @@ skip-if = os == "android" # Disabled due to intermittent orange on Android
skip-if = android_version == "18"
[test_ThreadHangStats.js]
run-sequentially = Bug 1046307, test can fail intermittently when CPU load is high
[test_ChildHistograms.js]
skip-if = os == "android"

Просмотреть файл

@ -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);
},
@ -95,14 +122,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;
}
@ -114,7 +141,7 @@ let AnimationPlayerActor = ActorClass({
// If there are several names, retrieve the index of the animation name in
// the list.
names = names.split(",").map(n => n.trim());
for (let i = 0; i < names.length; i ++) {
for (let i = 0; i < names.length; i++) {
if (names[i] === this.player.effect.name) {
return i;
}
@ -244,6 +271,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 +396,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
@ -416,19 +473,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};
}
});
/**
@ -449,7 +515,7 @@ let AnimationsActor = exports.AnimationsActor = ActorClass({
typeName: "animations",
events: {
"mutations" : {
"mutations": {
type: "mutations",
changes: Arg(0, "array:animationMutationChange")
}
@ -500,7 +566,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);
@ -532,7 +598,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
@ -588,9 +654,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)) {
@ -666,7 +732,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 = [];
@ -687,9 +753,8 @@ let AnimationsActor = exports.AnimationsActor = ActorClass({
toggleAll: method(function() {
if (this.allAnimationsPaused) {
return this.playAll();
} else {
return this.pauseAll();
}
return this.pauseAll();
}, {
request: {},
response: {}

Просмотреть файл

@ -152,6 +152,9 @@ RemoteFinder.prototype = {
keyPress: function (aEvent) {
this._browser.messageManager.sendAsyncMessage("Finder:KeyPress",
{ keyCode: aEvent.keyCode,
ctrlKey: aEvent.ctrlKey,
metaKey: aEvent.metaKey,
altKey: aEvent.altKey,
shiftKey: aEvent.shiftKey });
},
@ -235,6 +238,10 @@ RemoteFinderListener.prototype = {
this._finder.highlight(data.highlight, data.word);
break;
case "Finder:EnableSelection":
this._finder.enableSelection();
break;
case "Finder:RemoveSelection":
this._finder.removeSelection();
break;