author | Carsten "Tomcat" Book <cbook@mozilla.com> |
Tue, 05 May 2015 11:58:18 +0200 | |
changeset 242284 | 4c93d46ab92fe880f1be7afe6114d55b9972cdea |
parent 242272 | bf86d6afdff2273336dfc13fb307e75cd3bc320d (current diff) |
parent 242283 | 16911df6b875f9bc9dc75d79424944e6fe705ea3 (diff) |
child 242338 | 754579ec0e68068d32be534d553d5b191d918d84 |
push id | 28688 |
push user | cbook@mozilla.com |
push date | Tue, 05 May 2015 09:59:25 +0000 |
treeherder | mozilla-central@4c93d46ab92f [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | merge |
milestone | 40.0a1 |
first release with | nightly linux32
4c93d46ab92f
/
40.0a1
/
20150505030206
/
files
nightly linux64
4c93d46ab92f
/
40.0a1
/
20150505030206
/
files
nightly mac
4c93d46ab92f
/
40.0a1
/
20150505030206
/
files
nightly win32
4c93d46ab92f
/
40.0a1
/
20150505030206
/
files
nightly win64
4c93d46ab92f
/
40.0a1
/
20150505030206
/
files
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
releases | nightly linux32
40.0a1
/
20150505030206
/
pushlog to previous
nightly linux64
40.0a1
/
20150505030206
/
pushlog to previous
nightly mac
40.0a1
/
20150505030206
/
pushlog to previous
nightly win32
40.0a1
/
20150505030206
/
pushlog to previous
nightly win64
40.0a1
/
20150505030206
/
pushlog to previous
|
--- a/browser/base/content/test/general/browser.ini +++ b/browser/base/content/test/general/browser.ini @@ -421,20 +421,18 @@ support-files = benignPage.html [browser_typeAheadFind.js] skip-if = buildapp == 'mulet' || e10s # Bug 921935 - focusmanager issues with e10s (test calls waitForFocus) [browser_unknownContentType_title.js] [browser_unloaddialogs.js] skip-if = e10s # Bug 1100700 - test relies on unload event firing on closed tabs, which it doesn't [browser_urlHighlight.js] [browser_urlbarAutoFillTrimURLs.js] -skip-if = e10s # Bug 1093941 - Waits indefinitely for onSearchComplete [browser_urlbarCopying.js] [browser_urlbarEnter.js] -skip-if = e10s # Bug 1093941 - used to cause obscure non-windows child process crashes on try [browser_urlbarEnterAfterMouseOver.js] skip-if = os == "linux" || e10s # Bug 1073339 - Investigate autocomplete test unreliability on Linux/e10s [browser_urlbarRevert.js] [browser_urlbarSearchSingleWordNotification.js] [browser_urlbarStop.js] [browser_urlbarTrimURLs.js] [browser_urlbar_search_healthreport.js] [browser_utilityOverlay.js]
--- a/browser/base/content/test/general/browser_urlbarEnter.js +++ b/browser/base/content/test/general/browser_urlbarEnter.js @@ -20,20 +20,25 @@ add_task(function* () { // Cleanup. gBrowser.removeCurrentTab(); }); add_task(function* () { info("Alt+Return keypress"); let tab = gBrowser.selectedTab = gBrowser.addTab(START_VALUE); + // due to bug 691608, we must wait for the load event, else isTabEmpty() will + // return true on e10s for this tab, so it will be reused even with altKey. + yield BrowserTestUtils.browserLoaded(tab.linkedBrowser); gURLBar.focus(); EventUtils.synthesizeKey("VK_RETURN", {altKey: true}); - yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + + // wait for the new tab to appear. + yield BrowserTestUtils.waitForEvent(gBrowser.tabContainer, "TabOpen"); // Check url bar and selected tab. is(gURLBar.textValue, TEST_VALUE, "Urlbar should preserve the value on return keypress"); isnot(gBrowser.selectedTab, tab, "New URL was loaded in a new tab"); // Cleanup. gBrowser.removeTab(tab); gBrowser.removeCurrentTab();
--- a/browser/devtools/animationinspector/animation-controller.js +++ b/browser/devtools/animationinspector/animation-controller.js @@ -107,16 +107,18 @@ let AnimationsController = { // Expose actor capabilities. this.hasToggleAll = yield target.actorHasMethod("animations", "toggleAll"); this.hasSetCurrentTime = yield target.actorHasMethod("animationplayer", "setCurrentTime"); this.hasMutationEvents = yield target.actorHasMethod("animations", "stopAnimationPlayerUpdates"); this.hasSetPlaybackRate = yield target.actorHasMethod("animationplayer", "setPlaybackRate"); + this.hasTargetNode = yield target.actorHasMethod("domwalker", + "getNodeFromActor"); if (this.destroyed) { console.warn("Could not fully initialize the AnimationsController"); return; } this.startListeners(); yield this.onNewNodeFront();
--- a/browser/devtools/animationinspector/animation-panel.js +++ b/browser/devtools/animationinspector/animation-panel.js @@ -1,16 +1,23 @@ /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; +const { + PlayerMetaDataHeader, + PlaybackRateSelector, + AnimationTargetNode, + createNode +} = require("devtools/animationinspector/components"); + /** * The main animations panel UI. */ let AnimationsPanel = { UI_UPDATED_EVENT: "ui-updated", PANEL_INITIALIZED: "panel-initialized", initialize: Task.async(function*() { @@ -194,16 +201,19 @@ function PlayerWidget(player, containerE this.onFastForwardBtnClick = this.onFastForwardBtnClick.bind(this); this.onCurrentTimeChanged = this.onCurrentTimeChanged.bind(this); this.onPlaybackRateChanged = this.onPlaybackRateChanged.bind(this); this.metaDataComponent = new PlayerMetaDataHeader(); if (AnimationsController.hasSetPlaybackRate) { this.rateComponent = new PlaybackRateSelector(); } + if (AnimationsController.hasTargetNode) { + this.targetNodeComponent = new AnimationTargetNode(gInspector); + } } PlayerWidget.prototype = { initialize: Task.async(function*() { if (this.initialized) { return; } this.initialized = true; @@ -219,16 +229,19 @@ PlayerWidget.prototype = { this.destroyed = true; this.stopTimelineAnimation(); this.stopListeners(); this.metaDataComponent.destroy(); if (this.rateComponent) { this.rateComponent.destroy(); } + if (this.targetNodeComponent) { + this.targetNodeComponent.destroy(); + } this.el.remove(); this.playPauseBtnEl = this.rewindBtnEl = this.fastForwardBtnEl = null; this.currentTimeEl = this.timeDisplayEl = null; this.containerEl = this.el = this.player = null; }), startListeners: function() { @@ -256,21 +269,27 @@ PlayerWidget.prototype = { this.rateComponent.off("rate-changed", this.onPlaybackRateChanged); } }, createMarkup: function() { let state = this.player.state; this.el = createNode({ + parent: this.containerEl, attributes: { "class": "player-widget " + state.playState } }); + if (this.targetNodeComponent) { + this.targetNodeComponent.init(this.el); + this.targetNodeComponent.render(this.player); + } + this.metaDataComponent.init(this.el); this.metaDataComponent.render(state); // Timeline widget. let timelineEl = createNode({ parent: this.el, attributes: { "class": "timeline" @@ -354,18 +373,16 @@ PlayerWidget.prototype = { // Time display this.timeDisplayEl = createNode({ parent: timelineEl, attributes: { "class": "time-display" } }); - this.containerEl.appendChild(this.el); - // Show the initial time. this.displayTime(state.currentTime); }, /** * Executed when the playPause button is clicked. * Note that tests may want to call this callback directly rather than * simulating a click on the button since it returns the promise returned by @@ -566,266 +583,8 @@ PlayerWidget.prototype = { */ stopTimelineAnimation: function() { if (this.rafID) { cancelAnimationFrame(this.rafID); this.rafID = null; } } }; - -/** - * UI component responsible for displaying and updating the player meta-data: - * name, duration, iterations, delay. - * The parent UI component for this should drive its updates by calling - * render(state) whenever it wants the component to update. - */ -function PlayerMetaDataHeader() { - // Store the various state pieces we need to only refresh the UI when things - // change. - this.state = {}; -} - -PlayerMetaDataHeader.prototype = { - init: function(containerEl) { - // The main title element. - this.el = createNode({ - parent: containerEl, - attributes: { - "class": "animation-title" - } - }); - - // Animation name. - this.nameLabel = createNode({ - parent: this.el, - nodeType: "span" - }); - - this.nameValue = createNode({ - parent: this.el, - nodeType: "strong", - attributes: { - "style": "display:none;" - } - }); - - // Animation duration, delay and iteration container. - let metaData = createNode({ - parent: this.el, - nodeType: "span", - attributes: { - "class": "meta-data" - } - }); - - // Animation duration. - this.durationLabel = createNode({ - parent: metaData, - nodeType: "span" - }); - this.durationLabel.textContent = L10N.getStr("player.animationDurationLabel"); - - this.durationValue = createNode({ - parent: metaData, - nodeType: "strong" - }); - - // Animation delay (hidden by default since there may not be a delay). - this.delayLabel = createNode({ - parent: metaData, - nodeType: "span", - attributes: { - "style": "display:none;" - } - }); - this.delayLabel.textContent = L10N.getStr("player.animationDelayLabel"); - - this.delayValue = createNode({ - parent: metaData, - nodeType: "strong" - }); - - // Animation iteration count (also hidden by default since we don't display - // single iterations). - this.iterationLabel = createNode({ - parent: metaData, - nodeType: "span", - attributes: { - "style": "display:none;" - } - }); - this.iterationLabel.textContent = L10N.getStr("player.animationIterationCountLabel"); - - this.iterationValue = createNode({ - parent: metaData, - nodeType: "strong", - attributes: { - "style": "display:none;" - } - }); - }, - - destroy: function() { - this.state = null; - this.el.remove(); - this.el = null; - this.nameLabel = this.nameValue = null; - this.durationLabel = this.durationValue = null; - this.delayLabel = this.delayValue = null; - this.iterationLabel = this.iterationValue = null; - }, - - render: function(state) { - // Update the name if needed. - if (state.name !== this.state.name) { - if (state.name) { - // Animations (and transitions since bug 1122414) have names. - this.nameLabel.textContent = L10N.getStr("player.animationNameLabel"); - this.nameValue.style.display = "inline"; - this.nameValue.textContent = state.name; - } else { - // With older actors, Css transitions don't have names. - this.nameLabel.textContent = L10N.getStr("player.transitionNameLabel"); - this.nameValue.style.display = "none"; - } - } - - // update the duration value if needed. - if (state.duration !== this.state.duration) { - this.durationValue.textContent = L10N.getFormatStr("player.timeLabel", - L10N.numberWithDecimals(state.duration / 1000, 2)); - } - - // Update the delay if needed. - if (state.delay !== this.state.delay) { - if (state.delay) { - this.delayLabel.style.display = "inline"; - this.delayValue.style.display = "inline"; - this.delayValue.textContent = L10N.getFormatStr("player.timeLabel", - L10N.numberWithDecimals(state.delay / 1000, 2)); - } else { - // Hide the delay elements if there is no delay defined. - this.delayLabel.style.display = "none"; - this.delayValue.style.display = "none"; - } - } - - // Update the iterationCount if needed. - if (state.iterationCount !== this.state.iterationCount) { - if (state.iterationCount !== 1) { - this.iterationLabel.style.display = "inline"; - this.iterationValue.style.display = "inline"; - let count = state.iterationCount || - L10N.getStr("player.infiniteIterationCount"); - this.iterationValue.innerHTML = count; - } else { - // Hide the iteration elements if iteration is 1. - this.iterationLabel.style.display = "none"; - this.iterationValue.style.display = "none"; - } - } - - this.state = state; - } -}; - -/** - * UI component responsible for displaying the playback rate drop-down in each - * player widget, updating it when the state changes, and emitting events when - * the user selects a new value. - * The parent UI component for this should drive its updates by calling - * render(state) whenever it wants the component to update. - */ -function PlaybackRateSelector() { - this.currentRate = null; - this.onSelectionChanged = this.onSelectionChanged.bind(this); - EventEmitter.decorate(this); -} - -PlaybackRateSelector.prototype = { - PRESETS: [.1, .5, 1, 2, 5, 10], - - init: function(containerEl) { - // This component is simple enough that we can re-create the markup every - // time it's rendered. So here we only store the parentEl. - this.parentEl = containerEl; - }, - - destroy: function() { - this.removeSelect(); - this.parentEl = this.el = null; - }, - - removeSelect: function() { - if (this.el) { - this.el.removeEventListener("change", this.onSelectionChanged); - this.el.remove(); - } - }, - - /** - * Get the ordered list of presets, including the current playbackRate if - * different from the existing presets. - */ - getCurrentPresets: function({playbackRate}) { - return [...new Set([...this.PRESETS, playbackRate])].sort((a,b) => a > b); - }, - - render: function(state) { - if (state.playbackRate === this.currentRate) { - return; - } - - this.removeSelect(); - - this.el = createNode({ - parent: this.parentEl, - nodeType: "select", - attributes: { - "class": "rate devtools-button" - } - }); - - for (let preset of this.getCurrentPresets(state)) { - let option = createNode({ - parent: this.el, - nodeType: "option", - attributes: { - value: preset, - } - }); - option.textContent = L10N.getFormatStr("player.playbackRateLabel", preset); - if (preset === state.playbackRate) { - option.setAttribute("selected", ""); - } - } - - this.el.addEventListener("change", this.onSelectionChanged); - - this.currentRate = state.playbackRate; - }, - - onSelectionChanged: function(e) { - this.emit("rate-changed", parseFloat(this.el.value)); - } -}; - -/** - * DOM node creation helper function. - * @param {Object} Options to customize the node to be created. - * @return {DOMNode} The newly created node. - */ -function createNode(options) { - let type = options.nodeType || "div"; - let node = document.createElement(type); - - for (let name in options.attributes || {}) { - let value = options.attributes[name]; - node.setAttribute(name, value); - } - - if (options.parent) { - options.parent.appendChild(node); - } - - return node; -}
new file mode 100644 --- /dev/null +++ b/browser/devtools/animationinspector/components.js @@ -0,0 +1,502 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Set of reusable UI components for the animation-inspector UI. +// All components in this module share a common API: +// 1. construct the component: +// let c = new ComponentName(); +// 2. initialize the markup of the component in a given parent node: +// c.init(containerElement); +// 3. render the component, passing in some sort of state: +// This may be called over and over again when the state changes, to update +// the component output. +// c.render(state); +// 4. destroy the component: +// c.destroy(); + +const {Cu} = require('chrome'); +Cu.import("resource:///modules/devtools/ViewHelpers.jsm"); + +const STRINGS_URI = "chrome://browser/locale/devtools/animationinspector.properties"; +const L10N = new ViewHelpers.L10N(STRINGS_URI); + +/** + * UI component responsible for displaying and updating the player meta-data: + * name, duration, iterations, delay. + * The parent UI component for this should drive its updates by calling + * render(state) whenever it wants the component to update. + */ +function PlayerMetaDataHeader() { + // Store the various state pieces we need to only refresh the UI when things + // change. + this.state = {}; +} + +exports.PlayerMetaDataHeader = PlayerMetaDataHeader; + +PlayerMetaDataHeader.prototype = { + init: function(containerEl) { + // The main title element. + this.el = createNode({ + parent: containerEl, + attributes: { + "class": "animation-title" + } + }); + + // Animation name. + this.nameLabel = createNode({ + parent: this.el, + nodeType: "span" + }); + + this.nameValue = createNode({ + parent: this.el, + nodeType: "strong", + attributes: { + "style": "display:none;" + } + }); + + // Animation duration, delay and iteration container. + let metaData = createNode({ + parent: this.el, + nodeType: "span", + attributes: { + "class": "meta-data" + } + }); + + // Animation duration. + this.durationLabel = createNode({ + parent: metaData, + nodeType: "span" + }); + this.durationLabel.textContent = L10N.getStr("player.animationDurationLabel"); + + this.durationValue = createNode({ + parent: metaData, + nodeType: "strong" + }); + + // Animation delay (hidden by default since there may not be a delay). + this.delayLabel = createNode({ + parent: metaData, + nodeType: "span", + attributes: { + "style": "display:none;" + } + }); + this.delayLabel.textContent = L10N.getStr("player.animationDelayLabel"); + + this.delayValue = createNode({ + parent: metaData, + nodeType: "strong" + }); + + // Animation iteration count (also hidden by default since we don't display + // single iterations). + this.iterationLabel = createNode({ + parent: metaData, + nodeType: "span", + attributes: { + "style": "display:none;" + } + }); + this.iterationLabel.textContent = L10N.getStr("player.animationIterationCountLabel"); + + this.iterationValue = createNode({ + parent: metaData, + nodeType: "strong", + attributes: { + "style": "display:none;" + } + }); + }, + + destroy: function() { + this.state = null; + this.el.remove(); + this.el = null; + this.nameLabel = this.nameValue = null; + this.durationLabel = this.durationValue = null; + this.delayLabel = this.delayValue = null; + this.iterationLabel = this.iterationValue = null; + }, + + render: function(state) { + // Update the name if needed. + if (state.name !== this.state.name) { + if (state.name) { + // Animations (and transitions since bug 1122414) have names. + this.nameLabel.textContent = L10N.getStr("player.animationNameLabel"); + this.nameValue.style.display = "inline"; + this.nameValue.textContent = state.name; + } else { + // With older actors, Css transitions don't have names. + this.nameLabel.textContent = L10N.getStr("player.transitionNameLabel"); + this.nameValue.style.display = "none"; + } + } + + // update the duration value if needed. + if (state.duration !== this.state.duration) { + this.durationValue.textContent = L10N.getFormatStr("player.timeLabel", + L10N.numberWithDecimals(state.duration / 1000, 2)); + } + + // Update the delay if needed. + if (state.delay !== this.state.delay) { + if (state.delay) { + this.delayLabel.style.display = "inline"; + this.delayValue.style.display = "inline"; + this.delayValue.textContent = L10N.getFormatStr("player.timeLabel", + L10N.numberWithDecimals(state.delay / 1000, 2)); + } else { + // Hide the delay elements if there is no delay defined. + this.delayLabel.style.display = "none"; + this.delayValue.style.display = "none"; + } + } + + // Update the iterationCount if needed. + if (state.iterationCount !== this.state.iterationCount) { + if (state.iterationCount !== 1) { + this.iterationLabel.style.display = "inline"; + this.iterationValue.style.display = "inline"; + let count = state.iterationCount || + L10N.getStr("player.infiniteIterationCount"); + this.iterationValue.innerHTML = count; + } else { + // Hide the iteration elements if iteration is 1. + this.iterationLabel.style.display = "none"; + this.iterationValue.style.display = "none"; + } + } + + this.state = state; + } +}; + +/** + * UI component responsible for displaying the playback rate drop-down in each + * player widget, updating it when the state changes, and emitting events when + * the user selects a new value. + * The parent UI component for this should drive its updates by calling + * render(state) whenever it wants the component to update. + */ +function PlaybackRateSelector() { + this.currentRate = null; + this.onSelectionChanged = this.onSelectionChanged.bind(this); + EventEmitter.decorate(this); +} + +exports.PlaybackRateSelector = PlaybackRateSelector; + +PlaybackRateSelector.prototype = { + PRESETS: [.1, .5, 1, 2, 5, 10], + + init: function(containerEl) { + // This component is simple enough that we can re-create the markup every + // time it's rendered. So here we only store the parentEl. + this.parentEl = containerEl; + }, + + destroy: function() { + this.removeSelect(); + this.parentEl = this.el = null; + }, + + removeSelect: function() { + if (this.el) { + this.el.removeEventListener("change", this.onSelectionChanged); + this.el.remove(); + } + }, + + /** + * Get the ordered list of presets, including the current playbackRate if + * different from the existing presets. + */ + getCurrentPresets: function({playbackRate}) { + return [...new Set([...this.PRESETS, playbackRate])].sort((a,b) => a > b); + }, + + render: function(state) { + if (state.playbackRate === this.currentRate) { + return; + } + + this.removeSelect(); + + this.el = createNode({ + parent: this.parentEl, + nodeType: "select", + attributes: { + "class": "rate devtools-button" + } + }); + + for (let preset of this.getCurrentPresets(state)) { + let option = createNode({ + parent: this.el, + nodeType: "option", + attributes: { + value: preset, + } + }); + option.textContent = L10N.getFormatStr("player.playbackRateLabel", preset); + if (preset === state.playbackRate) { + option.setAttribute("selected", ""); + } + } + + this.el.addEventListener("change", this.onSelectionChanged); + + this.currentRate = state.playbackRate; + }, + + onSelectionChanged: function(e) { + this.emit("rate-changed", parseFloat(this.el.value)); + } +}; + +/** + * UI component responsible for displaying a preview of the target dom node of + * a given animation. + * @param {InspectorPanel} inspector Requires a reference to the inspector-panel + * to highlight and select the node, as well as refresh it when there are + * mutations. + */ +function AnimationTargetNode(inspector) { + this.inspector = inspector; + + this.onPreviewMouseOver = this.onPreviewMouseOver.bind(this); + this.onPreviewMouseOut = this.onPreviewMouseOut.bind(this); + this.onSelectNodeClick = this.onSelectNodeClick.bind(this); + this.onMarkupMutations = this.onMarkupMutations.bind(this); + + EventEmitter.decorate(this); +} + +exports.AnimationTargetNode = AnimationTargetNode; + +AnimationTargetNode.prototype = { + init: function(containerEl) { + let document = containerEl.ownerDocument; + + // Init the markup for displaying the target node. + this.el = createNode({ + parent: containerEl, + attributes: { + "class": "animation-target" + } + }); + + // Icon to select the node in the inspector. + this.selectNodeEl = createNode({ + parent: this.el, + nodeType: "span", + attributes: { + "class": "node-selector" + } + }); + + // Wrapper used for mouseover/out event handling. + this.previewEl = createNode({ + parent: this.el, + nodeType: "span" + }); + + this.previewEl.appendChild(document.createTextNode("<")); + + // Tag name. + this.tagNameEl = createNode({ + parent: this.previewEl, + nodeType: "span", + attributes: { + "class": "tag-name theme-fg-color3" + } + }); + + // Id attribute container. + this.idEl = createNode({ + parent: this.previewEl, + nodeType: "span" + }); + + createNode({ + parent: this.idEl, + nodeType: "span", + attributes: { + "class": "attribute-name theme-fg-color2" + } + }).textContent = "id"; + + this.idEl.appendChild(document.createTextNode("=\"")); + + createNode({ + parent: this.idEl, + nodeType: "span", + attributes: { + "class": "attribute-value theme-fg-color6" + } + }); + + this.idEl.appendChild(document.createTextNode("\"")); + + // Class attribute container. + this.classEl = createNode({ + parent: this.previewEl, + nodeType: "span" + }); + + createNode({ + parent: this.classEl, + nodeType: "span", + attributes: { + "class": "attribute-name theme-fg-color2" + } + }).textContent = "class"; + + this.classEl.appendChild(document.createTextNode("=\"")); + + createNode({ + parent: this.classEl, + nodeType: "span", + attributes: { + "class": "attribute-value theme-fg-color6" + } + }); + + this.classEl.appendChild(document.createTextNode("\"")); + + this.previewEl.appendChild(document.createTextNode(">")); + + // Init events for highlighting and selecting the node. + this.previewEl.addEventListener("mouseover", this.onPreviewMouseOver); + this.previewEl.addEventListener("mouseout", this.onPreviewMouseOut); + this.selectNodeEl.addEventListener("click", this.onSelectNodeClick); + + // Start to listen for markupmutation events. + this.inspector.on("markupmutation", this.onMarkupMutations); + }, + + destroy: function() { + this.inspector.off("markupmutation", this.onMarkupMutations); + this.previewEl.removeEventListener("mouseover", this.onPreviewMouseOver); + this.previewEl.removeEventListener("mouseout", this.onPreviewMouseOut); + this.selectNodeEl.removeEventListener("click", this.onSelectNodeClick); + this.el.remove(); + this.el = this.tagNameEl = this.idEl = this.classEl = null; + this.selectNodeEl = this.previewEl = null; + this.nodeFront = this.inspector = this.playerFront = null; + }, + + onPreviewMouseOver: function() { + if (!this.nodeFront) { + return; + } + this.inspector.toolbox.highlighterUtils.highlightNodeFront(this.nodeFront); + }, + + onPreviewMouseOut: function() { + this.inspector.toolbox.highlighterUtils.unhighlight(); + }, + + onSelectNodeClick: function() { + if (!this.nodeFront) { + return; + } + this.inspector.selection.setNodeFront(this.nodeFront, "animationinspector"); + }, + + onMarkupMutations: function(e, mutations) { + if (!this.nodeFront || !this.playerFront) { + return; + } + + for (let {target} of mutations) { + if (target === this.nodeFront) { + // Re-render with the same nodeFront to update the output. + this.render(this.playerFront); + break; + } + } + }, + + render: 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 = 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; + if (!this.el) { + console.warn("Cound't retrieve the animation target node, widget destroyed"); + } else { + console.error(e); + } + }); + } +}; + +/** + * 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. + */ +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); + } + + options.parent.appendChild(node); + return node; +} + +exports.createNode = createNode;
--- a/browser/devtools/animationinspector/moz.build +++ b/browser/devtools/animationinspector/moz.build @@ -1,7 +1,11 @@ # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- # vim: set filetype=python: # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. BROWSER_CHROME_MANIFESTS += ['test/browser.ini'] + +EXTRA_JS_MODULES.devtools.animationinspector += [ + 'components.js', +]
--- a/browser/devtools/animationinspector/test/browser.ini +++ b/browser/devtools/animationinspector/test/browser.ini @@ -15,24 +15,26 @@ support-files = [browser_animation_playerFronts_are_refreshed.js] [browser_animation_playerWidgets_appear_on_panel_init.js] [browser_animation_playerWidgets_destroy.js] [browser_animation_playerWidgets_disables_on_finished.js] [browser_animation_playerWidgets_dont_show_time_after_duration.js] [browser_animation_playerWidgets_have_control_buttons.js] [browser_animation_playerWidgets_meta_data.js] [browser_animation_playerWidgets_state_after_pause.js] +[browser_animation_playerWidgets_target_nodes.js] [browser_animation_rate_select_shows_presets.js] [browser_animation_refresh_on_added_animation.js] [browser_animation_refresh_on_removed_animation.js] [browser_animation_refresh_when_active.js] [browser_animation_same_nb_of_playerWidgets_and_playerFronts.js] [browser_animation_setting_currentTime_works_and_pauses.js] [browser_animation_setting_playbackRate_works.js] [browser_animation_shows_player_on_valid_node.js] +[browser_animation_target_highlight_select.js] [browser_animation_timeline_animates.js] [browser_animation_timeline_is_enabled.js] [browser_animation_timeline_waits_for_delay.js] [browser_animation_toggle_button_resets_on_navigate.js] [browser_animation_toggle_button_toggles_animations.js] [browser_animation_toggle_button_updates_playerWidgets.js] [browser_animation_toolbar_exists.js] [browser_animation_ui_updates_when_animation_changes.js]
new file mode 100644 --- /dev/null +++ b/browser/devtools/animationinspector/test/browser_animation_playerWidgets_target_nodes.js @@ -0,0 +1,31 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that player widgets display information about target nodes + +add_task(function*() { + yield addTab(TEST_URL_ROOT + "doc_simple_animation.html"); + let {inspector, panel} = yield openAnimationInspector(); + + info("Select the simple animated node"); + yield selectNode(".animated", inspector); + + let widget = panel.playerWidgets[0]; + + // Make sure to wait for the target-retrieved event if the nodeFront hasn't + // yet been retrieved by the TargetNodeComponent. + if (!widget.targetNodeComponent.nodeFront) { + yield widget.targetNodeComponent.once("target-retrieved"); + } + + let targetEl = widget.el.querySelector(".animation-target"); + ok(targetEl, "The player widget has a target element"); + is(targetEl.textContent, "<divid=\"\"class=\"ball animated\">", + "The target element's content is correct"); + + let selectorEl = targetEl.querySelector(".node-selector"); + ok(selectorEl, "The icon to select the target element in the inspector exists"); +});
new file mode 100644 --- /dev/null +++ b/browser/devtools/animationinspector/test/browser_animation_target_highlight_select.js @@ -0,0 +1,62 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the DOM element targets displayed in animation player widgets can +// be used to highlight elements in the DOM and select them in the inspector. + +add_task(function*() { + yield addTab(TEST_URL_ROOT + "doc_simple_animation.html"); + let {toolbox, inspector, panel} = yield openAnimationInspector(); + + 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; + if (!targetNodeComponent.nodeFront) { + yield targetNodeComponent.once("target-retrieved"); + } + + info("Retrieve the part of the widget that highlights the node on hover"); + let highlightingEl = targetNodeComponent.previewEl; + + info("Listen to node-highlight event and mouse over the widget"); + let onHighlight = toolbox.once("node-highlight"); + EventUtils.synthesizeMouse(highlightingEl, 10, 5, {type: "mouseover"}, + highlightingEl.ownerDocument.defaultView); + let nodeFront = yield onHighlight; + + ok(true, "The node-highlight event was fired"); + is(targetNodeComponent.nodeFront, nodeFront, + "The highlighted node is the one stored on the animation widget"); + is(nodeFront.tagName, "DIV", "The highlighted node has the correct tagName"); + is(nodeFront.attributes[0].name, "class", "The highlighted node has the correct attributes"); + is(nodeFront.attributes[0].value, "ball animated", "The highlighted node has the correct class"); + + info("Select the body node in order to have the list of all animations"); + yield selectNode("body", inspector); + + // Make sure to wait for the target-retrieved event if the nodeFront hasn't + // yet been retrieved by the TargetNodeComponent. + targetNodeComponent = panel.playerWidgets[0].targetNodeComponent; + if (!targetNodeComponent.nodeFront) { + yield targetNodeComponent.once("target-retrieved"); + } + + info("Click on the first animation widget's selector icon and wait for the selection to change"); + let onSelection = inspector.selection.once("new-node-front"); + let onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT); + let selectIconEl = targetNodeComponent.selectNodeEl; + EventUtils.sendMouseEvent({type: "click"}, selectIconEl, + selectIconEl.ownerDocument.defaultView); + yield onSelection; + + is(inspector.selection.nodeFront, targetNodeComponent.nodeFront, + "The selected node is the one stored on the animation widget"); + + yield onPanelUpdated; +});
--- a/browser/devtools/shared/moz.build +++ b/browser/devtools/shared/moz.build @@ -50,16 +50,17 @@ EXTRA_JS_MODULES.devtools.shared += [ 'devices.js', 'doorhanger.js', 'frame-script-utils.js', 'getjson.js', 'inplace-editor.js', 'node-attribute-parser.js', 'observable-object.js', 'options-view.js', + 'poller.js', 'source-utils.js', 'telemetry.js', 'theme-switching.js', 'theme.js', 'undo.js', ] EXTRA_JS_MODULES.devtools.shared.widgets += [
new file mode 100644 --- /dev/null +++ b/browser/devtools/shared/poller.js @@ -0,0 +1,115 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; +loader.lazyRequireGetter(this, "timers", + "resource://gre/modules/Timer.jsm"); +loader.lazyRequireGetter(this, "defer", + "sdk/core/promise", true); + +/** + * @constructor Poller + * Takes a function that is to be called on an interval, + * and can be turned on and off via methods to execute `fn` on the interval + * specified during `on`. If `fn` returns a promise, the polling waits for + * that promise to resolve before waiting the interval to call again. + * + * Specify the `wait` duration between polling here, and optionally + * an `immediate` boolean, indicating whether the function should be called + * immediately when toggling on. + * + * @param {function} fn + * @param {number} wait + * @param {boolean?} immediate + */ +function Poller (fn, wait, immediate) { + this._fn = fn; + this._wait = wait; + this._immediate = immediate; + this._poll = this._poll.bind(this); + this._preparePoll = this._preparePoll.bind(this); +} +exports.Poller = Poller; + +/** + * Returns a boolean indicating whether or not poller + * is polling. + * + * @return {boolean} + */ +Poller.prototype.isPolling = function pollerIsPolling () { + return !!this._timer; +}; + +/** + * Turns polling on. + * + * @return {Poller} + */ +Poller.prototype.on = function pollerOn () { + if (this._destroyed) { + throw Error("Poller cannot be turned on after destruction."); + } + if (this._timer) { + this.off(); + } + this._immediate ? this._poll() : this._preparePoll(); + return this; +}; + +/** + * Turns off polling. Returns a promise that resolves when + * the last outstanding `fn` call finishes if it's an async function. + * + * @return {Promise} + */ +Poller.prototype.off = function pollerOff () { + let { resolve, promise } = defer(); + if (this._timer) { + timers.clearTimeout(this._timer); + this._timer = null; + } + + // Settle an inflight poll call before resolving + // if using a promise-backed poll function + if (this._inflight) { + this._inflight.then(resolve); + } else { + resolve(); + } + return promise; +}; + +/** + * Turns off polling and removes the reference to the poller function. + * Resolves when the last outstanding `fn` call finishes if it's an async function. + */ +Poller.prototype.destroy = function pollerDestroy () { + return this.off().then(() => { + this._destroyed = true; + this._fn = null + }); +}; + +Poller.prototype._preparePoll = function pollerPrepare () { + this._timer = timers.setTimeout(this._poll, this._wait); +}; + +Poller.prototype._poll = function pollerPoll () { + let response = this._fn(); + if (response && typeof response.then === "function") { + // Store the most recent in-flight polling + // call so we can clean it up when disabling + this._inflight = response; + response.then(() => { + // Only queue up the next call if poller was not turned off + // while this async poll call was in flight. + if (this._timer) { + this._preparePoll(); + } + }); + } else { + this._preparePoll(); + } +};
--- a/browser/devtools/shared/test/browser.ini +++ b/browser/devtools/shared/test/browser.ini @@ -79,16 +79,17 @@ skip-if = e10s # Layouthelpers test shou skip-if = e10s # Layouthelpers test should not run in a content page. [browser_mdn-docs-01.js] [browser_mdn-docs-02.js] [browser_num-l10n.js] [browser_observableobject.js] [browser_options-view-01.js] [browser_outputparser.js] skip-if = e10s # Test intermittently fails with e10s. Bug 1124162. +[browser_poller.js] [browser_prefs-01.js] [browser_prefs-02.js] [browser_require_basic.js] [browser_spectrum.js] [browser_theme.js] [browser_tableWidget_basic.js] [browser_tableWidget_keyboard_interaction.js] [browser_tableWidget_mouse_interaction.js]
new file mode 100644 --- /dev/null +++ b/browser/devtools/shared/test/browser_poller.js @@ -0,0 +1,131 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests the Poller class. + +const { Poller } = devtools.require("devtools/shared/poller"); + +add_task(function* () { + let count1 = 0, count2 = 0, count3 = 0; + + let poller1 = new Poller(function () { + count1++; + }, 1000000000, true); + let poller2 = new Poller(function () { + count2++; + }, 10); + let poller3 = new Poller(function () { + count3++; + }, 1000000000); + + poller2.on(); + + ok(!poller1.isPolling(), "isPolling() returns false for an off poller"); + ok(poller2.isPolling(), "isPolling() returns true for an on poller"); + + yield waitUntil(() => count2 > 10); + + ok(count2 > 10, "poller that was turned on polled several times"); + ok(count1 === 0, "poller that was never turned on never polled"); + + yield poller2.off(); + let currentCount2 = count2; + + poller1.on(); // Really high poll time! + poller3.on(); // Really high poll time! + + yield waitUntil(() => count1 === 1); + ok(true, "Poller calls fn immediately when `immediate` is true"); + ok(count3 === 0, "Poller does not call fn immediately when `immediate` is not set"); + + ok(count2 === currentCount2, "a turned off poller does not continue to poll"); + yield poller2.off(); + yield poller2.off(); + yield poller2.off(); + ok(true, "Poller.prototype.off() is idempotent"); + + // This should still have not polled a second time + is(count1, 1, "wait time works"); + + ok(poller1.isPolling(), "isPolling() returns true for an on poller"); + ok(!poller2.isPolling(), "isPolling() returns false for an off poller"); +}); + +add_task(function *() { + let count = -1; + // Create a poller that returns a promise. + // The promise is resolved asynchronously after adding 9 to the count, ensuring + // that on every poll, we have a multiple of 10. + let asyncPoller = new Poller(function () { + count++; + ok(!(count%10), `Async poller called with a multiple of 10: ${count}`); + return new Promise(function (resolve, reject) { + let add9 = 9; + let interval = setInterval(() => { + if (add9--) { + count++; + } else { + clearInterval(interval); + resolve(); + } + }, 10); + }); + }); + + asyncPoller.on(1); + yield waitUntil(() => count > 50); + yield asyncPoller.off(); +}); + +add_task(function *() { + // Create a poller that returns a promise. This poll call + // is called immediately, and then subsequently turned off. + // The call to `off` should not resolve until the inflight call + // finishes. + let inflightFinished = null; + let pollCalls = 0; + let asyncPoller = new Poller(function () { + pollCalls++; + return new Promise(function (resolve, reject) { + setTimeout(() => { + inflightFinished = true; + resolve(); + }, 1000); + }); + }, 1, true); + asyncPoller.on(); + + yield asyncPoller.off(); + ok(inflightFinished, "off() method does not resolve until remaining inflight poll calls finish"); + is(pollCalls, 1, "should only be one poll call to occur before turning off polling"); +}); + +add_task(function *() { + // Create a poller that returns a promise. This poll call + // is called immediately, and then subsequently turned off. + // The call to `off` should not resolve until the inflight call + // finishes. + let inflightFinished = null; + let pollCalls = 0; + let asyncPoller = new Poller(function () { + pollCalls++; + return new Promise(function (resolve, reject) { + setTimeout(() => { + inflightFinished = true; + resolve(); + }, 1000); + }); + }, 1, true); + asyncPoller.on(); + + yield asyncPoller.destroy(); + ok(inflightFinished, "destroy() method does not resolve until remaining inflight poll calls finish"); + is(pollCalls, 1, "should only be one poll call to occur before destroying polling"); + + try { + asyncPoller.on(); + ok(false, "Calling on() after destruction should throw"); + } catch (e) { + ok(true, "Calling on() after destruction should throw"); + } +});
--- a/browser/devtools/shared/test/head.js +++ b/browser/devtools/shared/test/head.js @@ -237,8 +237,29 @@ function* openAndCloseToolbox(nbOfTimes, // We use a timeout to check the toolbox's active time yield new Promise(resolve => setTimeout(resolve, usageTime)); info("Closing toolbox " + (i + 1)); yield gDevTools.closeToolbox(target); } } + +/** + * Waits until a predicate returns true. + * + * @param function predicate + * Invoked once in a while until it returns true. + * @param number interval [optional] + * How often the predicate is invoked, in milliseconds. + */ +function waitUntil(predicate, interval = 10) { + if (predicate()) { + return Promise.resolve(true); + } + return new Promise(resolve => { + setTimeout(function() { + waitUntil(predicate).then(() => resolve(true)); + }, interval); + }); +} + +// EventUtils just doesn't work!
--- a/browser/devtools/styleinspector/computed-view.js +++ b/browser/devtools/styleinspector/computed-view.js @@ -147,32 +147,34 @@ function CssHtmlTree(aStyleInspector, aP this.focusWindow = this.focusWindow.bind(this); this._onContextMenu = this._onContextMenu.bind(this); this._contextMenuUpdate = this._contextMenuUpdate.bind(this); this._onSelectAll = this._onSelectAll.bind(this); this._onClick = this._onClick.bind(this); this._onCopy = this._onCopy.bind(this); this._onCopyColor = this._onCopyColor.bind(this); this._onFilterStyles = this._onFilterStyles.bind(this); + this._onFilterKeyPress = this._onFilterKeyPress.bind(this); this._onClearSearch = this._onClearSearch.bind(this); this._onIncludeBrowserStyles = this._onIncludeBrowserStyles.bind(this); this._onFilterTextboxContextMenu = this._onFilterTextboxContextMenu.bind(this); let doc = this.styleDocument; this.root = doc.getElementById("root"); this.element = doc.getElementById("propertyContainer"); this.searchField = doc.getElementById("computedview-searchbox"); this.searchClearButton = doc.getElementById("computedview-searchinput-clear"); this.includeBrowserStylesCheckbox = doc.getElementById("browser-style-checkbox"); this.styleDocument.addEventListener("mousedown", this.focusWindow); this.element.addEventListener("click", this._onClick); this.element.addEventListener("copy", this._onCopy); this.element.addEventListener("contextmenu", this._onContextMenu); this.searchField.addEventListener("input", this._onFilterStyles); + this.searchField.addEventListener("keypress", this._onFilterKeyPress); this.searchField.addEventListener("contextmenu", this._onFilterTextboxContextMenu); this.searchClearButton.addEventListener("click", this._onClearSearch); this.includeBrowserStylesCheckbox.addEventListener("command", this._onIncludeBrowserStyles); this.searchClearButton.hidden = true; // No results text. @@ -540,16 +542,56 @@ CssHtmlTree.prototype = { } this.refreshPanel(); this._filterChangeTimeout = null; }, filterTimeout); }, /** + * Handle the search box's keypress event. If the escape key is pressed, + * clear the search box field. + */ + _onFilterKeyPress: function(aEvent) { + if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE && + this._onClearSearch()) { + aEvent.preventDefault(); + aEvent.stopPropagation(); + } + }, + + /** + * Context menu handler for filter style search box. + */ + _onFilterTextboxContextMenu: function(event) { + try { + this.styleDocument.defaultView.focus(); + let contextmenu = this.inspector.toolbox.textboxContextMenuPopup; + contextmenu.openPopupAtScreen(event.screenX, event.screenY, true); + } catch(e) { + console.error(e); + } + }, + + /** + * Called when the user clicks on the clear button in the filter style search + * box. Returns true if the search box is cleared and false otherwise. + */ + _onClearSearch: function() { + if (this.searchField.value) { + this.searchField.value = ""; + this.searchField.focus(); + this._onFilterStyles(); + return true; + } + + return false; + }, + + /** * The change event handler for the includeBrowserStyles checkbox. * * @param {Event} aEvent the DOM Event object. */ _onIncludeBrowserStyles: function(aEvent) { this.refreshSourceFilter(); this.refreshPanel(); @@ -826,39 +868,16 @@ CssHtmlTree.prototype = { * Toggle the original sources pref. */ _onToggleOrigSources: function() { let isEnabled = Services.prefs.getBoolPref(PREF_ORIG_SOURCES); Services.prefs.setBoolPref(PREF_ORIG_SOURCES, !isEnabled); }, - /** - * Context menu handler for filter style search box. - */ - _onFilterTextboxContextMenu: function(event) { - try { - this.styleDocument.defaultView.focus(); - let contextmenu = this.inspector.toolbox.textboxContextMenuPopup; - contextmenu.openPopupAtScreen(event.screenX, event.screenY, true); - } catch(e) { - console.error(e); - } - }, - - /** - * Called when the user clicks on the clear button in the filter style search - * box. - */ - _onClearSearch: function() { - this.searchField.value = ""; - this.searchField.focus(); - this._onFilterStyles(); - }, - /** * Destructor for CssHtmlTree. */ destroy: function CssHtmlTree_destroy() { this.viewedElement = null; this._outputParser = null; @@ -901,16 +920,17 @@ CssHtmlTree.prototype = { this.highlighters.destroy(); // Remove bound listeners this.styleDocument.removeEventListener("mousedown", this.focusWindow); this.element.removeEventListener("click", this._onClick); this.element.removeEventListener("copy", this._onCopy); this.element.removeEventListener("contextmenu", this._onContextMenu); this.searchField.removeEventListener("input", this._onFilterStyles); + this.searchField.removeEventListener("keypress", this._onFilterKeyPress); this.searchField.removeEventListener("contextmenu", this._onFilterTextboxContextMenu); this.searchClearButton.removeEventListener("click", this._onClearSearch); this.includeBrowserStylesCheckbox.removeEventListener("command", this.includeBrowserStylesChanged); // Nodes used in templating this.root = null; this.element = null;
--- a/browser/devtools/styleinspector/rule-view.js +++ b/browser/devtools/styleinspector/rule-view.js @@ -1124,28 +1124,30 @@ function CssRuleView(aInspector, aDoc, a this._contextMenuUpdate = this._contextMenuUpdate.bind(this); this._onAddRule = this._onAddRule.bind(this); this._onSelectAll = this._onSelectAll.bind(this); this._onCopy = this._onCopy.bind(this); this._onCopyColor = this._onCopyColor.bind(this); this._onToggleOrigSources = this._onToggleOrigSources.bind(this); this._onShowMdnDocs = this._onShowMdnDocs.bind(this); this._onFilterStyles = this._onFilterStyles.bind(this); + this._onFilterKeyPress = this._onFilterKeyPress.bind(this); this._onClearSearch = this._onClearSearch.bind(this); this._onFilterTextboxContextMenu = this._onFilterTextboxContextMenu.bind(this); this.element = this.doc.getElementById("ruleview-container"); this.searchField = this.doc.getElementById("ruleview-searchbox"); this.searchClearButton = this.doc.getElementById("ruleview-searchinput-clear"); this.searchClearButton.hidden = true; this.element.addEventListener("copy", this._onCopy); this.element.addEventListener("contextmenu", this._onContextMenu); this.searchField.addEventListener("input", this._onFilterStyles); + this.searchField.addEventListener("keypress", this._onFilterKeyPress); this.searchField.addEventListener("contextmenu", this._onFilterTextboxContextMenu); this.searchClearButton.addEventListener("click", this._onClearSearch); this._handlePrefChange = this._handlePrefChange.bind(this); this._onSourcePrefChanged = this._onSourcePrefChanged.bind(this); this._prefObserver = new PrefObserver("devtools."); this._prefObserver.on(PREF_ORIG_SOURCES, this._onSourcePrefChanged); @@ -1643,36 +1645,53 @@ CssRuleView.prototype = { this.inspector.emit("ruleview-filtered"); this._filterChangeTimeout = null; }, filterTimeout); }, /** + * Handle the search box's keypress event. If the escape key is pressed, + * clear the search box field. + */ + _onFilterKeyPress: function(event) { + if (event.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE && + this._onClearSearch()) { + event.preventDefault(); + event.stopPropagation(); + } + }, + + /** * Context menu handler for filter style search box. */ _onFilterTextboxContextMenu: function(event) { try { this.doc.defaultView.focus(); let contextmenu = this.inspector.toolbox.textboxContextMenuPopup; contextmenu.openPopupAtScreen(event.screenX, event.screenY, true); } catch(e) { console.error(e); } }, /** * Called when the user clicks on the clear button in the filter style search - * box. + * box. Returns true if the search box is cleared and false otherwise. */ _onClearSearch: function() { - this.searchField.value = ""; - this.searchField.focus(); - this._onFilterStyles(); + if (this.searchField.value) { + this.searchField.value = ""; + this.searchField.focus(); + this._onFilterStyles(); + return true; + } + + return false; }, destroy: function() { this.isDestroyed = true; this.clear(); gDummyPromise = null; @@ -1716,16 +1735,17 @@ CssRuleView.prototype = { this.tooltips.destroy(); this.highlighters.destroy(); // Remove bound listeners this.element.removeEventListener("copy", this._onCopy); this.element.removeEventListener("contextmenu", this._onContextMenu); this.searchField.removeEventListener("input", this._onFilterStyles); + this.searchField.removeEventListener("keypress", this._onFilterKeyPress); this.searchField.removeEventListener("contextmenu", this._onFilterTextboxContextMenu); this.searchClearButton.removeEventListener("click", this._onClearSearch); this.searchField = null; this.searchClearButton = null; if (this.element.parentNode) { this.element.parentNode.removeChild(this.element);
--- a/browser/devtools/styleinspector/test/browser.ini +++ b/browser/devtools/styleinspector/test/browser.ini @@ -37,16 +37,17 @@ support-files = [browser_computedview_media-queries.js] [browser_computedview_no-results-placeholder.js] [browser_computedview_original-source-link.js] [browser_computedview_pseudo-element_01.js] [browser_computedview_refresh-on-style-change_01.js] [browser_computedview_search-filter.js] [browser_computedview_search-filter_clear.js] [browser_computedview_search-filter_context-menu.js] +[browser_computedview_search-filter_escape-keypress.js] [browser_computedview_select-and-copy-styles.js] [browser_computedview_style-editor-link.js] [browser_ruleview_add-property-and-reselect.js] [browser_ruleview_add-property-cancel_01.js] [browser_ruleview_add-property-cancel_02.js] [browser_ruleview_add-property-cancel_03.js] [browser_ruleview_add-property_01.js] [browser_ruleview_add-property_02.js] @@ -118,16 +119,17 @@ skip-if = e10s # Bug 1090340 [browser_ruleview_search-filter_05.js] [browser_ruleview_search-filter_06.js] [browser_ruleview_search-filter_07.js] [browser_ruleview_search-filter_08.js] [browser_ruleview_search-filter_09.js] [browser_ruleview_search-filter_10.js] [browser_ruleview_search-filter_clear.js] [browser_ruleview_search-filter_context-menu.js] +[browser_ruleview_search-filter_escape-keypress.js] [browser_ruleview_select-and-copy-styles.js] [browser_ruleview_selector-highlighter_01.js] [browser_ruleview_selector-highlighter_02.js] [browser_ruleview_selector-highlighter_03.js] [browser_ruleview_style-editor-link.js] skip-if = e10s # bug 1040670 Cannot open inline styles in viewSourceUtils [browser_ruleview_urls-clickable.js] [browser_ruleview_user-agent-styles.js]
new file mode 100644 --- /dev/null +++ b/browser/devtools/styleinspector/test/browser_computedview_search-filter_escape-keypress.js @@ -0,0 +1,71 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that search filter escape keypress will clear the search field. + +let TEST_URI = [ + '<style type="text/css">', + ' .matches {', + ' color: #F00;', + ' }', + '</style>', + '<span id="matches" class="matches">Some styled text</span>' +].join("\n"); + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {toolbox, inspector, view} = yield openComputedView(); + yield selectNode("#matches", inspector); + yield testAddTextInFilter(inspector, view); + yield testEscapeKeypress(inspector, view); +}); + +function* testAddTextInFilter(inspector, computedView) { + info("Setting filter text to \"background-color\""); + + let win = computedView.styleWindow; + let propertyViews = computedView.propertyViews; + let searchField = computedView.searchField; + let checkbox = computedView.includeBrowserStylesCheckbox; + + info("Include browser styles"); + checkbox.click(); + yield inspector.once("computed-view-refreshed"); + + searchField.focus(); + synthesizeKeys("background-color", win); + yield inspector.once("computed-view-refreshed"); + + info("Check that the correct properties are visible"); + + propertyViews.forEach((propView) => { + let name = propView.name; + is(propView.visible, name.indexOf("background-color") > -1, + "span " + name + " property visibility check"); + }); +} + +function* testEscapeKeypress(inspector, computedView) { + info("Pressing the escape key on search filter"); + + let win = computedView.styleWindow; + let propertyViews = computedView.propertyViews; + let searchField = computedView.searchField; + let onRefreshed = inspector.once("computed-view-refreshed"); + + searchField.focus(); + EventUtils.synthesizeKey("VK_ESCAPE", {}, win); + yield onRefreshed; + + info("Check that the correct properties are visible"); + + ok(!searchField.value, "Search filter is cleared"); + propertyViews.forEach((propView) => { + let name = propView.name; + is(propView.visible, true, + "span " + name + " property is visible"); + }); +}
new file mode 100644 --- /dev/null +++ b/browser/devtools/styleinspector/test/browser_ruleview_search-filter_escape-keypress.js @@ -0,0 +1,66 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that the rule view search filter escape keypress will clear the search +// field. + +let TEST_URI = [ + '<style type="text/css">', + ' #testid {', + ' background-color: #00F;', + ' }', + ' .testclass {', + ' width: 100%;', + ' }', + '</style>', + '<div id="testid" class="testclass">Styled Node</div>' +].join("\n"); + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {toolbox, inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + yield testAddTextInFilter(inspector, view); + yield testEscapeKeypress(inspector, view); +}); + +function* testAddTextInFilter(inspector, ruleView) { + info("Setting filter text to \"00F\""); + + let win = ruleView.doc.defaultView; + let searchField = ruleView.searchField; + let onRuleViewFiltered = inspector.once("ruleview-filtered"); + + searchField.focus(); + synthesizeKeys("00F", win); + yield onRuleViewFiltered; + + info("Check that the correct rules are visible"); + is(ruleView.element.children.length, 2, "Should have 2 rules."); + is(getRuleViewRuleEditor(ruleView, 0).rule.selectorText, "element", "First rule is inline element."); + is(getRuleViewRuleEditor(ruleView, 1).rule.selectorText, "#testid", "Second rule is #testid."); + ok(getRuleViewRuleEditor(ruleView, 1).rule.textProps[0].editor.element.classList.contains("ruleview-highlight"), + "background-color text property is correctly highlighted."); +} + +function* testEscapeKeypress(inspector, ruleView) { + info("Pressing the escape key on search filter"); + + let doc = ruleView.doc; + let win = ruleView.doc.defaultView; + let searchField = ruleView.searchField; + let onRuleViewFiltered = inspector.once("ruleview-filtered"); + + searchField.focus(); + EventUtils.synthesizeKey("VK_ESCAPE", {}, win); + yield onRuleViewFiltered; + + info("Check the search filter is cleared and no rules are highlighted"); + is(ruleView.element.children.length, 3, "Should have 3 rules."); + ok(!searchField.value, "Search filter is cleared"); + ok(!doc.querySelectorAll(".ruleview-highlight").length && + !ruleView._highlightedElements.length, "No rules are higlighted"); +}
--- a/browser/themes/shared/devtools/animationinspector.css +++ b/browser/themes/shared/devtools/animationinspector.css @@ -94,16 +94,46 @@ body { background-image: url("debugger-pause@2x.png"); } #toggle-all.paused::before { background-image: url("debugger-play@2x.png"); } } +/* Animation target node gutter, contains a preview of the dom node */ + +.animation-target { + background-color: var(--theme-toolbar-background); + padding: 1px 4px; + box-sizing: border-box; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.animation-target .attribute-name { + padding-left: 4px; +} + +.animation-target .node-selector { + background: url("chrome://browser/skin/devtools/vview-open-inspector.png") no-repeat 0 0; + padding-left: 16px; + margin-right: 5px; + cursor: pointer; +} + +.animation-target .node-selector:hover { + background-position: -32px 0; +} + +.animation-target .node-selector:active { + background-position: -16px 0; +} + /* Animation title gutter, contains the name, duration, iteration */ .animation-title { background-color: var(--theme-toolbar-background); border-bottom: 1px solid var(--theme-splitter-color); padding: 1px 4px; word-wrap: break-word; overflow: auto;
--- a/mobile/android/base/GuestSession.java +++ b/mobile/android/base/GuestSession.java @@ -36,17 +36,17 @@ public class GuestSession { return false; } return profile.locked(); } private static PendingIntent getNotificationIntent(Context context) { Intent intent = new Intent(NOTIFICATION_INTENT); - intent.setClass(context, BrowserApp.class); + intent.setClassName(context, AppConstants.BROWSER_INTENT_CLASS_NAME); return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); } public static void showNotification(Context context) { final NotificationCompat.Builder builder = new NotificationCompat.Builder(context); final Resources res = context.getResources(); builder.setContentTitle(res.getString(R.string.guest_browsing_notification_title)) .setContentText(res.getString(R.string.guest_browsing_notification_text))
--- a/mobile/android/base/tabqueue/TabQueueDispatcher.java +++ b/mobile/android/base/tabqueue/TabQueueDispatcher.java @@ -1,17 +1,16 @@ /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- * 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/. */ package org.mozilla.gecko.tabqueue; import org.mozilla.gecko.AppConstants; -import org.mozilla.gecko.BrowserApp; import org.mozilla.gecko.GeckoAppShell; import org.mozilla.gecko.GeckoSharedPrefs; import org.mozilla.gecko.Locales; import org.mozilla.gecko.mozglue.ContextUtils; import org.mozilla.gecko.preferences.GeckoPreferences; import org.mozilla.gecko.sync.setup.activities.WebURLFinder; import android.content.Intent; @@ -64,17 +63,17 @@ public class TabQueueDispatcher extends startService(intent); finish(); } /** * Start fennec with the supplied intent. */ private void loadNormally(Intent intent) { - intent.setClass(getApplicationContext(), BrowserApp.class); + intent.setClassName(getApplicationContext(), AppConstants.BROWSER_INTENT_CLASS_NAME); startActivity(intent); finish(); } /** * Abort as we were started with no URL. * @param dataString */
--- a/mobile/android/base/tabqueue/TabQueueHelper.java +++ b/mobile/android/base/tabqueue/TabQueueHelper.java @@ -1,16 +1,16 @@ /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- * 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/. */ package org.mozilla.gecko.tabqueue; -import org.mozilla.gecko.BrowserApp; +import org.mozilla.gecko.AppConstants; import org.mozilla.gecko.GeckoAppShell; import org.mozilla.gecko.GeckoEvent; import org.mozilla.gecko.GeckoProfile; import org.mozilla.gecko.GeckoSharedPrefs; import org.mozilla.gecko.R; import org.mozilla.gecko.preferences.GeckoPreferences; import org.mozilla.gecko.util.ThreadUtils; @@ -149,17 +149,18 @@ public class TabQueueHelper { * will be replaced. * * @param context * @param tabsQueued */ public static void showNotification(final Context context, final int tabsQueued) { ThreadUtils.assertNotOnUiThread(); - Intent resultIntent = new Intent(context, BrowserApp.class); + Intent resultIntent = new Intent(); + resultIntent.setClassName(context, AppConstants.BROWSER_INTENT_CLASS_NAME); resultIntent.setAction(TabQueueHelper.LOAD_URLS_ACTION); PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, resultIntent, PendingIntent.FLAG_CANCEL_CURRENT); final String text; final Resources resources = context.getResources(); if (tabsQueued == 1) { text = resources.getString(R.string.tab_queue_notification_text_singular);
--- a/mobile/android/base/tabqueue/TabQueueService.java +++ b/mobile/android/base/tabqueue/TabQueueService.java @@ -1,16 +1,16 @@ /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- * 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/. */ package org.mozilla.gecko.tabqueue; -import org.mozilla.gecko.BrowserApp; +import org.mozilla.gecko.AppConstants; import org.mozilla.gecko.GeckoProfile; import org.mozilla.gecko.GeckoSharedPrefs; import org.mozilla.gecko.R; import org.mozilla.gecko.mozglue.ContextUtils; import org.mozilla.gecko.preferences.GeckoPreferences; import android.app.Service; import android.content.Context; @@ -185,17 +185,17 @@ public class TabQueueService extends Ser tabQueueHandler.postDelayed(stopServiceRunnable, TOAST_TIMEOUT); return START_REDELIVER_INTENT; } private void openNow(Intent intent) { Intent forwardIntent = new Intent(intent); - forwardIntent.setClass(getApplicationContext(), BrowserApp.class); + forwardIntent.setClassName(getApplicationContext(), AppConstants.BROWSER_INTENT_CLASS_NAME); forwardIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(forwardIntent); GeckoSharedPrefs.forApp(getApplicationContext()).edit().remove(GeckoPreferences.PREFS_TAB_QUEUE_LAST_SITE) .remove(GeckoPreferences.PREFS_TAB_QUEUE_LAST_TIME) .apply(); }
--- a/toolkit/components/telemetry/TelemetryController.jsm +++ b/toolkit/components/telemetry/TelemetryController.jsm @@ -191,29 +191,16 @@ this.TelemetryController = Object.freeze /** * Sets a server to send pings to. */ setServer: function(aServer) { return Impl.setServer(aServer); }, /** - * Adds a ping to the pending ping list by moving it to the saved pings directory - * and adding it to the pending ping list. - * - * @param {String} aPingPath The path of the ping to add to the pending ping list. - * @param {Boolean} [aRemoveOriginal] If true, deletes the ping at aPingPath after adding - * it to the saved pings directory. - * @return {Promise} Resolved when the ping is correctly moved to the saved pings directory. - */ - addPendingPingFromFile: function(aPingPath, aRemoveOriginal) { - return Impl.addPendingPingFromFile(aPingPath, aRemoveOriginal); - }, - - /** * Submit ping payloads to Telemetry. This will assemble a complete ping, adding * environment data, client id and some general info. * Depending on configuration, the ping will be sent to the server (immediately or later) * and archived locally. * * @param {String} aType The type of the ping. * @param {Object} aPayload The actual data payload for the ping. * @param {Object} [aOptions] Options object. @@ -280,16 +267,26 @@ this.TelemetryController = Object.freeze options.addClientId = aOptions.addClientId || false; options.addEnvironment = aOptions.addEnvironment || false; options.overwrite = aOptions.overwrite || false; return Impl.addPendingPing(aType, aPayload, options); }, /** + * Save an aborted-session ping to the pending pings and archive it. + * + * @param {String} aFilePath The path to the aborted-session checkpoint ping. + * @return {Promise} Promise that is resolved when the ping is saved. + */ + addAbortedSessionPing: function addAbortedSessionPing(aFilePath) { + return Impl.addAbortedSessionPing(aFilePath); + }, + + /** * Write a ping to a specified location on the disk. Does not add the ping to the * pending pings. * * @param {String} aType The type of the ping. * @param {Object} aPayload The actual data payload for the ping. * @param {String} aFilePath The path to save the ping to. * @param {Object} [aOptions] Options object. * @param {Number} [aOptions.retentionDays=14] The number of days to keep the ping on disk @@ -496,33 +493,16 @@ let Impl = { * Track any pending ping send and save tasks through the promise passed here. * This is needed to block shutdown on any outstanding ping activity. */ _trackPendingPingTask: function (aPromise) { this._connectionsBarrier.client.addBlocker("Waiting for ping task", aPromise); }, /** - * Adds a ping to the pending ping list by moving it to the saved pings directory - * and adding it to the pending ping list. - * - * @param {String} aPingPath The path of the ping to add to the pending ping list. - * @param {Boolean} [aRemoveOriginal] If true, deletes the ping at aPingPath after adding - * it to the saved pings directory. - * @return {Promise} Resolved when the ping is correctly moved to the saved pings directory. - */ - addPendingPingFromFile: function(aPingPath, aRemoveOriginal) { - return TelemetryStorage.addPendingPingFromFile(aPingPath).then(() => { - if (aRemoveOriginal) { - return OS.File.remove(aPingPath); - } - }, error => this._log.error("addPendingPingFromFile - Unable to add the pending ping", error)); - }, - - /** * This helper calculates the next time that we can send pings at. * Currently this mostly redistributes ping sends around midnight to avoid submission * spikes around local midnight for daily pings. * * @param now Date The current time. * @return Number The next time (ms from UNIX epoch) when we can send pings. */ _getNextPingSendTime: function(now) { @@ -715,16 +695,36 @@ let Impl = { savePing: function savePing(aType, aPayload, aFilePath, aOptions) { this._log.trace("savePing - Type " + aType + ", Server " + this._server + ", File Path " + aFilePath + ", aOptions " + JSON.stringify(aOptions)); let pingData = this.assemblePing(aType, aPayload, aOptions); return TelemetryStorage.savePingToFile(pingData, aFilePath, aOptions.overwrite) .then(() => pingData.id); }, + /** + * Save an aborted-session ping to the pending pings and archive it. + * + * @param {String} aFilePath The path to the aborted-session checkpoint ping. + * @return {Promise} Promise that is resolved when the ping is saved. + */ + addAbortedSessionPing: Task.async(function* addAbortedSessionPing(aFilePath) { + this._log.trace("addAbortedSessionPing"); + + let ping = yield TelemetryStorage.loadPingFile(aFilePath); + try { + yield TelemetryStorage.addPendingPing(ping); + yield TelemetryArchive.promiseArchivePing(ping); + } catch (e) { + this._log.error("addAbortedSessionPing - Unable to add the pending ping", e); + } finally { + yield OS.File.remove(aFilePath); + } + }), + onPingRequestFinished: function(success, startTime, ping, isPersisted) { this._log.trace("onPingRequestFinished - success: " + success + ", persisted: " + isPersisted); let hping = Telemetry.getHistogramById("TELEMETRY_PING"); let hsuccess = Telemetry.getHistogramById("TELEMETRY_SUCCESS"); hsuccess.add(success); hping.add(new Date() - startTime);
--- a/toolkit/components/telemetry/TelemetrySession.jsm +++ b/toolkit/components/telemetry/TelemetrySession.jsm @@ -1527,22 +1527,21 @@ let Impl = { // Delay full telemetry initialization to give the browser time to // run various late initializers. Otherwise our gathered memory // footprint and other numbers would be too optimistic. this._delayedInitTaskDeferred = Promise.defer(); this._delayedInitTask = new DeferredTask(function* () { try { this._initialized = true; - let hasLoaded = yield this._loadSessionData(); - if (!hasLoaded) { - // We could not load a valid session data file. Create one. - yield this._saveSessionData(this._getSessionDataObject()).catch(() => - this._log.error("setupChromeProcess - Could not write session data to disk.")); - } + yield this._loadSessionData(); + // Update the session data to keep track of new subsessions created before + // the initialization. + yield this._saveSessionData(this._getSessionDataObject()); + this.attachObservers(); this.gatherMemory(); Telemetry.asyncFetchTelemetryData(function () {}); if (IS_UNIFIED_TELEMETRY) { // Check for a previously written aborted session ping. yield this._checkAbortedSessionPing(); @@ -1960,18 +1959,18 @@ let Impl = { SESSION_STATE_FILE_NAME); // Try to load the "profileSubsessionCounter" from the state file. try { let data = yield CommonUtils.readJSON(dataFile); if (data && "profileSubsessionCounter" in data && typeof(data.profileSubsessionCounter) == "number" && - "previousSubsessionId" in data) { - this._previousSubsessionId = data.previousSubsessionId; + "subsessionId" in data) { + this._previousSubsessionId = data.subsessionId; // Add |_subsessionCounter| to the |_profileSubsessionCounter| to account for // new subsession while loading still takes place. This will always be exactly // 1 - the current subsessions. this._profileSubsessionCounter = data.profileSubsessionCounter + this._subsessionCounter; return true; } } catch (e) { @@ -1980,17 +1979,17 @@ let Impl = { return false; }), /** * Get the session data object to serialise to disk. */ _getSessionDataObject: function() { return { - previousSubsessionId: this._previousSubsessionId, + subsessionId: this._subsessionId, profileSubsessionCounter: this._profileSubsessionCounter, }; }, /** * Saves session data to disk. */ _saveSessionData: Task.async(function* (sessionData) { @@ -2004,18 +2003,17 @@ let Impl = { this._log.error("_saveSessionData - Failed to write session data to " + filePath, e); } }), _onEnvironmentChange: function(reason, oldEnvironment) { this._log.trace("_onEnvironmentChange", reason); let payload = this.getSessionPayload(REASON_ENVIRONMENT_CHANGE, true); - let clonedPayload = Cu.cloneInto(payload, myScope); - TelemetryScheduler.reschedulePings(REASON_ENVIRONMENT_CHANGE, clonedPayload); + TelemetryScheduler.reschedulePings(REASON_ENVIRONMENT_CHANGE, payload); let options = { retentionDays: RETENTION_DAYS, addClientId: true, addEnvironment: true, overrideEnvironment: oldEnvironment, }; TelemetryController.submitExternalPing(getPingType(payload), payload, options); @@ -2073,34 +2071,34 @@ let Impl = { yield OS.File.makeDir(ABORTED_SESSIONS_DIR, { ignoreExisting: true }); const FILE_PATH = OS.Path.join(OS.Constants.Path.profileDir, DATAREPORTING_DIRECTORY, ABORTED_SESSION_FILE_NAME); let abortedExists = yield OS.File.exists(FILE_PATH); if (abortedExists) { this._log.trace("_checkAbortedSessionPing - aborted session found: " + FILE_PATH); yield this._abortedSessionSerializer.enqueueTask( - () => TelemetryController.addPendingPingFromFile(FILE_PATH, true)); + () => TelemetryController.addAbortedSessionPing(FILE_PATH)); } }), /** * Saves the aborted session ping to disk. * @param {Object} [aProvidedPayload=null] A payload object to be used as an aborted * session ping. The reason of this payload is changed to aborted-session. * If not provided, a new payload is gathered. */ _saveAbortedSessionPing: function(aProvidedPayload = null) { const FILE_PATH = OS.Path.join(OS.Constants.Path.profileDir, DATAREPORTING_DIRECTORY, ABORTED_SESSION_FILE_NAME); this._log.trace("_saveAbortedSessionPing - ping path: " + FILE_PATH); let payload = null; if (aProvidedPayload) { - payload = aProvidedPayload; + payload = Cu.cloneInto(aProvidedPayload, myScope); // Overwrite the original reason. payload.info.reason = REASON_ABORTED_SESSION; } else { payload = this.getSessionPayload(REASON_ABORTED_SESSION, false); } let options = { retentionDays: RETENTION_DAYS,
new file mode 100644 --- /dev/null +++ b/toolkit/components/telemetry/tests/unit/test_SubsessionChaining.js @@ -0,0 +1,227 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +*/ + +Cu.import("resource://gre/modules/Preferences.jsm", this); +Cu.import("resource://gre/modules/Promise.jsm", this); +Cu.import("resource://gre/modules/Task.jsm", this); +Cu.import("resource://gre/modules/TelemetryArchive.jsm", this); +Cu.import("resource://gre/modules/TelemetryController.jsm", this); +Cu.import("resource://gre/modules/TelemetryEnvironment.jsm", this); +Cu.import("resource://gre/modules/osfile.jsm", this); +Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); + +const MS_IN_ONE_HOUR = 60 * 60 * 1000; +const MS_IN_ONE_DAY = 24 * MS_IN_ONE_HOUR; + +const PREF_BRANCH = "toolkit.telemetry."; +const PREF_ENABLED = PREF_BRANCH + "enabled"; +const PREF_ARCHIVE_ENABLED = PREF_BRANCH + "archive.enabled"; + +const REASON_ABORTED_SESSION = "aborted-session"; +const REASON_DAILY = "daily"; +const REASON_ENVIRONMENT_CHANGE = "environment-change"; +const REASON_SHUTDOWN = "shutdown"; + +XPCOMUtils.defineLazyGetter(this, "DATAREPORTING_PATH", function() { + return OS.Path.join(OS.Constants.Path.profileDir, "datareporting"); +}); + +let promiseValidateArchivedPings = Task.async(function*(aExpectedReasons) { + // The list of ping reasons which mark the session end (and must reset the subsession + // count). + const SESSION_END_PING_REASONS = new Set([ REASON_ABORTED_SESSION, REASON_SHUTDOWN ]); + + let list = yield TelemetryArchive.promiseArchivedPingList(); + + // We're just interested in the "main" pings. + list = list.filter(p => p.type == "main"); + + Assert.equal(aExpectedReasons.length, list.length, "All the expected pings must be received."); + + let previousPing = yield TelemetryArchive.promiseArchivedPingById(list[0].id); + Assert.equal(aExpectedReasons.shift(), previousPing.payload.info.reason, + "Telemetry should only get pings with expected reasons."); + Assert.equal(previousPing.payload.info.previousSubsessionId, null, + "The first subsession must report a null previous subsession id."); + Assert.equal(previousPing.payload.info.profileSubsessionCounter, 1, + "profileSubsessionCounter must be 1 the first time."); + Assert.equal(previousPing.payload.info.subsessionCounter, 1, + "subsessionCounter must be 1 the first time."); + + let expectedSubsessionCounter = 1; + + for (let i = 1; i < list.length; i++) { + let currentPing = yield TelemetryArchive.promiseArchivedPingById(list[i].id); + let currentInfo = currentPing.payload.info; + let previousInfo = previousPing.payload.info; + do_print("Archive entry " + i + " - id: " + currentPing.id + ", reason: " + currentInfo.reason); + + Assert.equal(aExpectedReasons.shift(), currentInfo.reason, + "Telemetry should only get pings with expected reasons."); + Assert.equal(currentInfo.previousSubsessionId, previousInfo.subsessionId, + "Telemetry must correctly chain subsession identifiers."); + Assert.equal(currentInfo.profileSubsessionCounter, previousInfo.profileSubsessionCounter + 1, + "Telemetry must correctly track the profile subsessions count."); + Assert.equal(currentInfo.subsessionCounter, expectedSubsessionCounter, + "The subsession counter should be monotonically increasing."); + + // Store the current ping as previous. + previousPing = currentPing; + // Reset the expected subsession counter, if required. Otherwise increment the expected + // subsession counter. + expectedSubsessionCounter = + SESSION_END_PING_REASONS.has(currentInfo.reason) ? 1 : (expectedSubsessionCounter + 1); + } +}); + +function run_test() { + do_test_pending(); + + // Addon manager needs a profile directory + do_get_profile(); + loadAddonManager("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); + + Preferences.set(PREF_ENABLED, true); + + run_next_test(); +} + +add_task(function* test_subsessionsChaining() { + if (gIsAndroid) { + // We don't support subsessions yet on Android, so skip the next checks. + return; + } + + const PREF_TEST = PREF_BRANCH + "test.pref1"; + const PREFS_TO_WATCH = new Map([ + [PREF_TEST, TelemetryEnvironment.RECORD_PREF_VALUE], + ]); + Preferences.reset(PREF_TEST); + + // Fake the clock data to manually trigger an aborted-session ping and a daily ping. + // This is also helpful to make sure we get the archived pings in an expected order. + let now = fakeNow(2009, 9, 18, 0, 0, 0); + + let moveClockForward = (minutes) => { + now = futureDate(now, minutes * MILLISECONDS_PER_MINUTE); + fakeNow(now); + } + + // Keep track of the ping reasons we're expecting in this test. + let expectedReasons = []; + + // Start and shut down Telemetry. We expect a shutdown ping with profileSubsessionCounter: 1, + // subsessionCounter: 1, subsessionId: A, and previousSubsessionId: null to be archived. + yield TelemetrySession.reset(); + yield TelemetrySession.shutdown(); + expectedReasons.push(REASON_SHUTDOWN); + + // Start Telemetry but don't wait for it to initialise before shutting down. We expect a + // shutdown ping with profileSubsessionCounter: 2, subsessionCounter: 1, subsessionId: B + // and previousSubsessionId: A to be archived. + moveClockForward(30); + TelemetrySession.reset(); + yield TelemetrySession.shutdown(); + expectedReasons.push(REASON_SHUTDOWN); + + // Start Telemetry and simulate an aborted-session ping. We expect an aborted-session ping + // with profileSubsessionCounter: 3, subsessionCounter: 1, subsessionId: C and + // previousSubsessionId: B to be archived. + let schedulerTickCallback = null; + fakeSchedulerTimer(callback => schedulerTickCallback = callback, () => {}); + yield TelemetrySession.reset(); + moveClockForward(6); + // Trigger the an aborted session ping save. When testing,we are not saving the aborted-session + // ping as soon as Telemetry starts, otherwise we would end up with unexpected pings being + // sent when calling |TelemetrySession.reset()|, thus breaking some tests. + Assert.ok(!!schedulerTickCallback); + yield schedulerTickCallback(); + expectedReasons.push(REASON_ABORTED_SESSION); + + // Start Telemetry and trigger an environment change through a pref modification. We expect + // an environment-change ping with profileSubsessionCounter: 4, subsessionCounter: 1, + // subsessionId: D and previousSubsessionId: C to be archived. + moveClockForward(30); + yield TelemetryController.reset(); + yield TelemetrySession.reset(); + TelemetryEnvironment._watchPreferences(PREFS_TO_WATCH); + moveClockForward(30); + Preferences.set(PREF_TEST, 1); + expectedReasons.push(REASON_ENVIRONMENT_CHANGE); + + // Shut down Telemetry. We expect a shutdown ping with profileSubsessionCounter: 5, + // subsessionCounter: 2, subsessionId: E and previousSubsessionId: D to be archived. + moveClockForward(30); + yield TelemetrySession.shutdown(); + expectedReasons.push(REASON_SHUTDOWN); + + // Start Telemetry and trigger a daily ping. We expect a daily ping with + // profileSubsessionCounter: 6, subsessionCounter: 1, subsessionId: F and + // previousSubsessionId: E to be archived. + moveClockForward(30); + yield TelemetrySession.reset(); + + // Delay the callback around midnight. + now = fakeNow(futureDate(now, MS_IN_ONE_DAY)); + // Trigger the daily ping. + yield schedulerTickCallback(); + expectedReasons.push(REASON_DAILY); + + // Trigger an environment change ping. We expect an environment-changed ping with + // profileSubsessionCounter: 7, subsessionCounter: 2, subsessionId: G and + // previousSubsessionId: F to be archived. + moveClockForward(30); + Preferences.set(PREF_TEST, 0); + expectedReasons.push(REASON_ENVIRONMENT_CHANGE); + + // Shut down Telemetry and trigger a shutdown ping. + moveClockForward(30); + yield TelemetrySession.shutdown(); + expectedReasons.push(REASON_SHUTDOWN); + + // Start Telemetry and trigger an environment change. + yield TelemetrySession.reset(); + TelemetryEnvironment._watchPreferences(PREFS_TO_WATCH); + moveClockForward(30); + Preferences.set(PREF_TEST, 1); + expectedReasons.push(REASON_ENVIRONMENT_CHANGE); + + // Don't shut down, instead trigger an aborted-session ping. + moveClockForward(6); + // Trigger the an aborted session ping save. + yield schedulerTickCallback(); + expectedReasons.push(REASON_ABORTED_SESSION); + + // Start Telemetry and trigger a daily ping. + moveClockForward(30); + yield TelemetryController.reset(); + yield TelemetrySession.reset(); + // Delay the callback around midnight. + now = futureDate(now, MS_IN_ONE_DAY); + fakeNow(now); + // Trigger the daily ping. + yield schedulerTickCallback(); + expectedReasons.push(REASON_DAILY); + + // Trigger an environment change. + moveClockForward(30); + Preferences.set(PREF_TEST, 0); + expectedReasons.push(REASON_ENVIRONMENT_CHANGE); + + // And an aborted-session ping again. + moveClockForward(6); + // Trigger the an aborted session ping save. + yield schedulerTickCallback(); + expectedReasons.push(REASON_ABORTED_SESSION); + + // Make sure the aborted-session ping gets archived. + yield TelemetryController.reset(); + yield TelemetrySession.reset(); + + yield promiseValidateArchivedPings(expectedReasons); +}); + +add_task(function* () { + do_test_finished(); +});
--- a/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js +++ b/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js @@ -1162,17 +1162,17 @@ add_task(function* test_savedPingsOnShut add_task(function* test_savedSessionData() { // Create the directory which will contain the data file, if it doesn't already // exist. yield OS.File.makeDir(DATAREPORTING_PATH); // Write test data to the session data file. const dataFilePath = OS.Path.join(DATAREPORTING_PATH, "session-state.json"); const sessionState = { - previousSubsessionId: null, + subsessionId: null, profileSubsessionCounter: 3785, }; yield CommonUtils.writeJSON(sessionState, dataFilePath); const PREF_TEST = "toolkit.telemetry.test.pref1"; Preferences.reset(PREF_TEST); const PREFS_TO_WATCH = new Map([ [PREF_TEST, TelemetryEnvironment.RECORD_PREF_VALUE], @@ -1204,17 +1204,50 @@ add_task(function* test_savedSessionData let payload = TelemetrySession.getPayload(); Assert.equal(payload.info.profileSubsessionCounter, expectedSubsessions); yield TelemetrySession.shutdown(); // Load back the serialised session data. let data = yield CommonUtils.readJSON(dataFilePath); Assert.equal(data.profileSubsessionCounter, expectedSubsessions); - Assert.equal(data.previousSubsessionId, expectedUUID); + Assert.equal(data.subsessionId, expectedUUID); +}); + +add_task(function* test_sessionData_ShortSession() { + if (gIsAndroid) { + // We don't support subsessions yet on Android, so skip the next checks. + return; + } + + const SESSION_STATE_PATH = OS.Path.join(DATAREPORTING_PATH, "session-state.json"); + + // Shut down Telemetry and remove the session state file. + yield TelemetrySession.shutdown(); + yield OS.File.remove(SESSION_STATE_PATH, { ignoreAbsent: true }); + + const expectedUUID = "009fd1ad-b85e-4817-b3e5-000000003785"; + fakeGenerateUUID(generateUUID, () => expectedUUID); + + // We intentionally don't wait for the setup to complete and shut down to simulate + // short sessions. We expect the profile subsession counter to be 1. + TelemetrySession.reset(); + yield TelemetrySession.shutdown(); + + // Restore the UUID generation functions. + fakeGenerateUUID(generateUUID, generateUUID); + + // Start TelemetrySession so that it loads the session data file. We expect the profile + // subsession counter to be incremented by 1 again. + yield TelemetrySession.reset(); + + // We expect 2 profile subsession counter updates. + let payload = TelemetrySession.getPayload(); + Assert.equal(payload.info.profileSubsessionCounter, 2); + Assert.equal(payload.info.previousSubsessionId, expectedUUID); }); add_task(function* test_invalidSessionData() { // Create the directory which will contain the data file, if it doesn't already // exist. yield OS.File.makeDir(DATAREPORTING_PATH); // Write test data to the session data file. @@ -1233,17 +1266,17 @@ add_task(function* test_invalidSessionDa yield TelemetrySession.reset(); let payload = TelemetrySession.getPayload(); Assert.equal(payload.info.profileSubsessionCounter, expectedSubsessions); yield TelemetrySession.shutdown(); // Load back the serialised session data. let data = yield CommonUtils.readJSON(dataFilePath); Assert.equal(data.profileSubsessionCounter, expectedSubsessions); - Assert.equal(data.previousSubsessionId, null); + Assert.equal(data.subsessionId, expectedUUID); }); add_task(function* test_abortedSession() { if (gIsAndroid || gIsGonk) { // We don't have the aborted session ping here. return; }
--- a/toolkit/components/telemetry/tests/unit/xpcshell.ini +++ b/toolkit/components/telemetry/tests/unit/xpcshell.ini @@ -15,16 +15,17 @@ generated-files = dictionary.xpi experiment.xpi extension.xpi extension-2.xpi restartless.xpi theme.xpi [test_nsITelemetry.js] +[test_SubsessionChaining.js] [test_TelemetryEnvironment.js] # Bug 1144395: crash on Android 4.3 skip-if = android_version == "18" [test_PingAPI.js] [test_TelemetryFlagClear.js] [test_TelemetryLateWrites.js] [test_TelemetryLockCount.js] [test_TelemetryLog.js]
--- a/toolkit/devtools/server/actors/inspector.js +++ b/toolkit/devtools/server/actors/inspector.js @@ -2880,37 +2880,85 @@ var WalkerActor = protocol.ActorClass({ objectActorID: Arg(0, "string") }, response: { nodeFront: RetVal("nullable:disconnectedNode") } }), /** - * Given an StyleSheetActor (identified by its ID), commonly used in the + * Given a StyleSheetActor (identified by its ID), commonly used in the * style-editor, get its ownerNode and return the corresponding walker's - * NodeActor + * NodeActor. + * Note that getNodeFromActor was added later and can now be used instead. */ getStyleSheetOwnerNode: method(function(styleSheetActorID) { - let styleSheetActor = this.conn.getActor(styleSheetActorID); - let ownerNode = styleSheetActor.ownerNode; - - if (!styleSheetActor || !ownerNode) { - return null; - } - - return this.attachElement(ownerNode); + return this.getNodeFromActor(styleSheetActorID, ["ownerNode"]); }, { request: { styleSheetActorID: Arg(0, "string") }, response: { ownerNode: RetVal("nullable:disconnectedNode") } }), + + /** + * This method can be used to retrieve NodeActor for DOM nodes from other + * actors in a way that they can later be highlighted in the page, or + * selected in the inspector. + * If an actor has a reference to a DOM node, and the UI needs to know about + * this DOM node (and possibly select it in the inspector), the UI should + * first retrieve a reference to the walkerFront: + * + * // Make sure the inspector/walker have been initialized first. + * toolbox.initInspector().then(() => { + * // Retrieve the walker. + * let walker = toolbox.walker; + * }); + * + * And then call this method: + * + * // Get the nodeFront from my actor, passing the ID and properties path. + * walker.getNodeFromActor(myActorID, ["element"]).then(nodeFront => { + * // Use the nodeFront, e.g. select the node in the inspector. + * toolbox.getPanel("inspector").selection.setNodeFront(nodeFront); + * }); + * + * @param {String} actorID The ID for the actor that has a reference to the + * DOM node. + * @param {Array} path Where, on the actor, is the DOM node stored. If in the + * scope of the actor, the node is available as `this.data.node`, then this + * should be ["data", "node"]. + * @return {NodeActor} The attached NodeActor, or null if it couldn't be found. + */ + getNodeFromActor: method(function(actorID, path) { + let actor = this.conn.getActor(actorID); + if (!actor) { + return null; + } + + let obj = actor; + for (let name of path) { + if (!(name in obj)) { + return null; + } + obj = obj[name]; + } + + return this.attachElement(obj); + }, { + request: { + actorID: Arg(0, "string"), + path: Arg(1, "array:string") + }, + response: { + node: RetVal("nullable:disconnectedNode") + } + }) }); /** * Client side of the DOM walker. */ var WalkerFront = exports.WalkerFront = protocol.FrontClass(WalkerActor, { // Set to true if cleanup should be requested after every mutation list. autoCleanup: true, @@ -3058,16 +3106,24 @@ var WalkerFront = exports.WalkerFront = getStyleSheetOwnerNode: protocol.custom(function(styleSheetActorID) { return this._getStyleSheetOwnerNode(styleSheetActorID).then(response => { return response ? response.node : null; }); }, { impl: "_getStyleSheetOwnerNode" }), + getNodeFromActor: protocol.custom(function(actorID, path) { + return this._getNodeFromActor(actorID, path).then(response => { + return response ? response.node : null; + }); + }, { + impl: "_getNodeFromActor" + }), + _releaseFront: function(node, force) { if (node.retained && !force) { node.reparent(null); this._retainedOrphans.add(node); return; } if (node.retained) {
--- a/toolkit/devtools/server/actors/profiler.js +++ b/toolkit/devtools/server/actors/profiler.js @@ -54,16 +54,26 @@ ProfilerActor.prototype = { * Returns an array of feature strings, describing the profiler features * that are available on this platform. Can be called while the profiler * is stopped. */ onGetFeatures: function() { return { features: nsIProfilerModule.GetFeatures([]) }; }, + onGetBufferInfo: function(request) { + let position = {}, totalSize = {}, generation = {}; + nsIProfilerModule.GetBufferInfo(position, totalSize, generation); + return { + position: position.value, + totalSize: totalSize.value, + generation: generation.value + } + }, + /** * Returns the configuration used that was originally passed in to start up the * profiler. Used for tests, and does not account for others using nsIProfiler. */ onGetStartOptions: function() { return this._profilerStartOptions || {}; }, @@ -316,16 +326,17 @@ function checkProfilerConsumers() { /** * The request types this actor can handle. * At the moment there are two known users of the Profiler actor: * the devtools and the Gecko Profiler addon, which uses the debugger * protocol to get profiles from Fennec. */ ProfilerActor.prototype.requestTypes = { + "getBufferInfo": ProfilerActor.prototype.onGetBufferInfo, "getFeatures": ProfilerActor.prototype.onGetFeatures, "startProfiler": ProfilerActor.prototype.onStartProfiler, "stopProfiler": ProfilerActor.prototype.onStopProfiler, "isActive": ProfilerActor.prototype.onIsActive, "getSharedLibraryInformation": ProfilerActor.prototype.onGetSharedLibraryInformation, "getProfile": ProfilerActor.prototype.onGetProfile, "registerEventNotifications": ProfilerActor.prototype.onRegisterEventNotifications, "unregisterEventNotifications": ProfilerActor.prototype.onUnregisterEventNotifications,
--- a/toolkit/devtools/server/actors/root.js +++ b/toolkit/devtools/server/actors/root.js @@ -167,16 +167,19 @@ RootActor.prototype = { // no longer expose tab actors, but also that getProcess forbids // exposing actors for security reasons get allowChromeProcess() { return DebuggerServer.allowChromeProcess; }, // Whether or not `getProfile()` supports specifying a `startTime` // and `endTime` to filter out samples. Fx40+ profilerDataFilterable: true, + // Whether or not the profiler has a `getBufferInfo` method + // necessary as the profiler does not use the ActorFront class. + profilerBufferStatus: true, }, /** * Return a 'hello' packet as specified by the Remote Debugging Protocol. */ sayHello: function() { return { from: this.actorID,
--- a/toolkit/devtools/server/tests/mochitest/chrome.ini +++ b/toolkit/devtools/server/tests/mochitest/chrome.ini @@ -48,16 +48,17 @@ skip-if = buildapp == 'mulet' [test_getProcess.html] skip-if = buildapp == 'mulet' [test_inspector-anonymous.html] [test_inspector-changeattrs.html] [test_inspector-changevalue.html] [test_inspector-dead-nodes.html] [test_inspector_getImageData.html] skip-if = buildapp == 'mulet' +[test_inspector_getNodeFromActor.html] [test_inspector-hide.html] [test_inspector-insert.html] [test_inspector-mutations-attr.html] [test_inspector-mutations-childlist.html] [test_inspector-mutations-frameload.html] [test_inspector-mutations-value.html] [test_inspector-pseudoclass-lock.html] [test_inspector-release.html]
new file mode 100644 --- /dev/null +++ b/toolkit/devtools/server/tests/mochitest/test_inspector_getNodeFromActor.html @@ -0,0 +1,89 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1155653 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1155653</title> + + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> + <script type="application/javascript;version=1.8" src="inspector-helpers.js"></script> + <script type="application/javascript;version=1.8"> +Components.utils.import("resource://gre/modules/devtools/Loader.jsm"); +const inspector = devtools.require("devtools/server/actors/inspector"); + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + runNextTest(); +} + +var gWalker; + +addTest(function() { + let url = document.getElementById("inspectorContent").href; + attachURL(url, function(err, client, tab, doc) { + let {InspectorFront} = devtools.require("devtools/server/actors/inspector"); + let inspector = InspectorFront(client, tab); + + promiseDone(inspector.getWalker().then(walker => { + gWalker = walker; + }).then(runNextTest)); + }); +}); + +addTest(function() { + info("Try to get a NodeFront from an invalid actorID"); + gWalker.getNodeFromActor("invalid", ["node"]).then(node => { + ok(!node, "The node returned is null"); + runNextTest(); + }); +}); + +addTest(function() { + info("Try to get a NodeFront from a valid actorID but invalid path"); + gWalker.getNodeFromActor(gWalker.actorID, ["invalid", "path"]).then(node => { + ok(!node, "The node returned is null"); + runNextTest(); + }); +}); + +addTest(function() { + info("Try to get a NodeFront from a valid actorID and valid path"); + gWalker.getNodeFromActor(gWalker.actorID, ["rootDoc"]).then(rootDocNode => { + ok(rootDocNode, "A node was returned"); + is(rootDocNode, gWalker.rootNode, "The right node was returned"); + runNextTest(); + }); +}); + +addTest(function() { + info("Try to get a NodeFront from a valid actorID and valid complex path"); + gWalker.getNodeFromActor(gWalker.actorID, + ["tabActor", "window", "document", "body"]).then(bodyNode => { + ok(bodyNode, "A node was returned"); + gWalker.querySelector(gWalker.rootNode, "body").then(node => { + is(bodyNode, node, "The body node was returned"); + runNextTest(); + }); + }); +}); + +addTest(function() { + gWalker = null; + runNextTest(); +}); + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1155653">Mozilla Bug 1155653</a> +<a id="inspectorContent" target="_blank" href="inspector_getImageData.html">Test Document</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html>
new file mode 100644 --- /dev/null +++ b/toolkit/devtools/server/tests/unit/test_profiler_getbufferinfo.js @@ -0,0 +1,107 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if the profiler actor returns its buffer status via getBufferInfo. + */ + +const Profiler = Cc["@mozilla.org/tools/profiler;1"].getService(Ci.nsIProfiler); +const INITIAL_WAIT_TIME = 100; // ms +const MAX_WAIT_TIME = 20000; // ms +const MAX_PROFILER_ENTRIES = 10000000; + +function run_test() +{ + get_chrome_actors((client, form) => { + let actor = form.profilerActor; + activate_profiler(client, actor, startTime => { + wait_for_samples(client, actor, () => { + check_buffer(client, actor, () => { + deactivate_profiler(client, actor, () => { + client.close(do_test_finished); + }); + }); + }); + }); + }) + + do_test_pending(); +} + +function check_buffer(client, actor, callback) +{ + client.request({ to: actor, type: "getBufferInfo" }, response => { + do_check_true(typeof response.position === "number"); + do_check_true(typeof response.totalSize === "number"); + do_check_true(typeof response.generation === "number"); + do_check_true(response.position > 0 && response.position < response.totalSize); + do_check_true(response.totalSize === MAX_PROFILER_ENTRIES); + // There's no way we'll fill the buffer in this test. + do_check_true(response.generation === 0); + + callback(); + }); +} + +function activate_profiler(client, actor, callback) +{ + client.request({ to: actor, type: "startProfiler", entries: MAX_PROFILER_ENTRIES }, response => { + do_check_true(response.started); + client.request({ to: actor, type: "isActive" }, response => { + do_check_true(response.isActive); + callback(response.currentTime); + }); + }); +} + +function deactivate_profiler(client, actor, callback) +{ + client.request({ to: actor, type: "stopProfiler" }, response => { + do_check_false(response.started); + client.request({ to: actor, type: "isActive" }, response => { + do_check_false(response.isActive); + callback(); + }); + }); +} + +function wait_for_samples(client, actor, callback) +{ + function attempt(delay) + { + // No idea why, but Components.stack.sourceLine returns null. + let funcLine = Components.stack.lineNumber - 3; + + // Spin for the requested time, then take a sample. + let start = Date.now(); + let stack; + do_print("Attempt: delay = " + delay); + while (Date.now() - start < delay) { stack = Components.stack; } + do_print("Attempt: finished waiting."); + + client.request({ to: actor, type: "getProfile" }, response => { + // At this point, we may or may not have samples, depending on + // whether the spin loop above has given the profiler enough time + // to get started. + if (response.profile.threads[0].samples.length == 0) { + if (delay < MAX_WAIT_TIME) { + // Double the spin-wait time and try again. + do_print("Attempt: no samples, going around again."); + return attempt(delay * 2); + } else { + // We've waited long enough, so just fail. + do_print("Attempt: waited a long time, but no samples were collected."); + do_print("Giving up."); + do_check_true(false); + return; + } + } + callback(); + }); + } + + // Start off with a 100 millisecond delay. + attempt(INITIAL_WAIT_TIME); +}
--- a/toolkit/devtools/server/tests/unit/xpcshell.ini +++ b/toolkit/devtools/server/tests/unit/xpcshell.ini @@ -211,16 +211,17 @@ skip-if = toolkit == "gonk" reason = bug 820380 [test_breakpoint-actor-map.js] [test_profiler_activation-01.js] [test_profiler_activation-02.js] [test_profiler_close.js] [test_profiler_data.js] [test_profiler_events-01.js] [test_profiler_events-02.js] +[test_profiler_getbufferinfo.js] [test_profiler_getfeatures.js] [test_profiler_getsharedlibraryinformation.js] [test_unsafeDereference.js] [test_add_actors.js] [test_trace_actor-01.js] [test_trace_actor-02.js] [test_trace_actor-03.js] [test_trace_actor-04.js]
--- a/toolkit/themes/shared/extensions/extensions.inc.css +++ b/toolkit/themes/shared/extensions/extensions.inc.css @@ -960,43 +960,39 @@ setting[type="radio"] > radiogroup { display: block !important; } button.button-link { -moz-appearance: none; background: transparent; border: none; box-shadow: none; - text-decoration: underline; color: #0095dd; cursor: pointer; min-width: 0; height: 20px; margin: 0 6px; } button.button-link:not(:-moz-focusring) > .button-box { border-width: 0; margin: 1px; } -.text-link { - color: #0095dd; - font-size: inherit; -} - -button.button-link:hover, -.text-link:hover { - color: #4cb1ff; +button.button-link:hover { background-color: transparent; + color: #178ce5; + text-decoration: underline; } /* Needed to override normal button style from inContent.css */ button.button-link:not([disabled="true"]):active:hover { background-color: transparent; + color: #ff9500; + text-decoration: none; } /*** telemetry experiments ***/ #detail-experiment-container { font-size: 80%; margin-bottom: 1em;