зеркало из https://github.com/mozilla/gecko-dev.git
Merge mozilla-central to mozilla-inbound
This commit is contained in:
Коммит
172de49cec
|
@ -1132,6 +1132,10 @@ pref("dom.requestSync.enabled", true);
|
|||
// Resample touch events on b2g
|
||||
pref("gfx.touch.resample", true);
|
||||
|
||||
// Comma separated list of activity names that can only be provided by
|
||||
// the system app in dev mode.
|
||||
pref("dom.activities.developer_mode_only", "import-app");
|
||||
|
||||
// mulet apparently loads firefox.js as well as b2g.js, so we have to explicitly
|
||||
// disable serviceworkers here to get them disabled in mulet.
|
||||
pref("dom.serviceWorkers.enabled", false);
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<project name="platform_build" path="build" remote="b2g" revision="2eda36a4795012a5d1275b77ebb20ac377c8cd26">
|
||||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="70077825aab2c7a79611befb40a5fe7e610d5443"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="e1773d6d1014c997be4b5c4233bba3ee073b8d7b"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="d3868ff4bb3a4b81382795e2784258c210fe6cb8"/>
|
||||
<project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="70077825aab2c7a79611befb40a5fe7e610d5443"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="e1773d6d1014c997be4b5c4233bba3ee073b8d7b"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="d3868ff4bb3a4b81382795e2784258c210fe6cb8"/>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
|
||||
<project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="9a9797062c6001d6346504161c51187a2968466b"/>
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
</project>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="70077825aab2c7a79611befb40a5fe7e610d5443"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="e1773d6d1014c997be4b5c4233bba3ee073b8d7b"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="d3868ff4bb3a4b81382795e2784258c210fe6cb8"/>
|
||||
<project name="moztt" path="external/moztt" remote="b2g" revision="adb24954bf8068f21705b570450475d183336b2d"/>
|
||||
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="0627790166dccd8dd370fa7d9f434ed9fc027fb4"/>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<project name="platform_build" path="build" remote="b2g" revision="2eda36a4795012a5d1275b77ebb20ac377c8cd26">
|
||||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="70077825aab2c7a79611befb40a5fe7e610d5443"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="e1773d6d1014c997be4b5c4233bba3ee073b8d7b"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="d3868ff4bb3a4b81382795e2784258c210fe6cb8"/>
|
||||
<project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<project name="platform_build" path="build" remote="b2g" revision="61e82f99bb8bc78d52b5717e9a2481ec7267fa33">
|
||||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="70077825aab2c7a79611befb40a5fe7e610d5443"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="e1773d6d1014c997be4b5c4233bba3ee073b8d7b"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="d3868ff4bb3a4b81382795e2784258c210fe6cb8"/>
|
||||
<project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="70077825aab2c7a79611befb40a5fe7e610d5443"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="e1773d6d1014c997be4b5c4233bba3ee073b8d7b"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="d3868ff4bb3a4b81382795e2784258c210fe6cb8"/>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
|
||||
<project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="9a9797062c6001d6346504161c51187a2968466b"/>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<project name="platform_build" path="build" remote="b2g" revision="2eda36a4795012a5d1275b77ebb20ac377c8cd26">
|
||||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="70077825aab2c7a79611befb40a5fe7e610d5443"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="e1773d6d1014c997be4b5c4233bba3ee073b8d7b"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="d3868ff4bb3a4b81382795e2784258c210fe6cb8"/>
|
||||
<project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
</project>
|
||||
<project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="70077825aab2c7a79611befb40a5fe7e610d5443"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="e1773d6d1014c997be4b5c4233bba3ee073b8d7b"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="d3868ff4bb3a4b81382795e2784258c210fe6cb8"/>
|
||||
<project name="moztt" path="external/moztt" remote="b2g" revision="adb24954bf8068f21705b570450475d183336b2d"/>
|
||||
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="0627790166dccd8dd370fa7d9f434ed9fc027fb4"/>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"git": {
|
||||
"git_revision": "70077825aab2c7a79611befb40a5fe7e610d5443",
|
||||
"git_revision": "e1773d6d1014c997be4b5c4233bba3ee073b8d7b",
|
||||
"remote": "https://git.mozilla.org/releases/gaia.git",
|
||||
"branch": ""
|
||||
},
|
||||
"revision": "536698903e2c4c12a3ff6c6d83f70278fab7311d",
|
||||
"revision": "172680469094791e563268fa7d074fee9e0105ab",
|
||||
"repo_path": "integration/gaia-central"
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
</project>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="70077825aab2c7a79611befb40a5fe7e610d5443"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="e1773d6d1014c997be4b5c4233bba3ee073b8d7b"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="d3868ff4bb3a4b81382795e2784258c210fe6cb8"/>
|
||||
<project name="moztt" path="external/moztt" remote="b2g" revision="adb24954bf8068f21705b570450475d183336b2d"/>
|
||||
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="0627790166dccd8dd370fa7d9f434ed9fc027fb4"/>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<project name="platform_build" path="build" remote="b2g" revision="61e82f99bb8bc78d52b5717e9a2481ec7267fa33">
|
||||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="70077825aab2c7a79611befb40a5fe7e610d5443"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="e1773d6d1014c997be4b5c4233bba3ee073b8d7b"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="d3868ff4bb3a4b81382795e2784258c210fe6cb8"/>
|
||||
<project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
|
||||
|
|
|
@ -426,10 +426,8 @@ skip-if = buildapp == 'mulet' || e10s # Bug 921935 - focusmanager issues with e1
|
|||
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]
|
||||
|
|
|
@ -25,10 +25,15 @@ add_task(function* () {
|
|||
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");
|
||||
|
|
|
@ -112,6 +112,8 @@ let AnimationsController = {
|
|||
"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");
|
||||
|
|
|
@ -6,6 +6,13 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
const {
|
||||
PlayerMetaDataHeader,
|
||||
PlaybackRateSelector,
|
||||
AnimationTargetNode,
|
||||
createNode
|
||||
} = require("devtools/animationinspector/components");
|
||||
|
||||
/**
|
||||
* The main animations panel UI.
|
||||
*/
|
||||
|
@ -199,6 +206,9 @@ function PlayerWidget(player, containerEl) {
|
|||
if (AnimationsController.hasSetPlaybackRate) {
|
||||
this.rateComponent = new PlaybackRateSelector();
|
||||
}
|
||||
if (AnimationsController.hasTargetNode) {
|
||||
this.targetNodeComponent = new AnimationTargetNode(gInspector);
|
||||
}
|
||||
}
|
||||
|
||||
PlayerWidget.prototype = {
|
||||
|
@ -224,6 +234,9 @@ PlayerWidget.prototype = {
|
|||
if (this.rateComponent) {
|
||||
this.rateComponent.destroy();
|
||||
}
|
||||
if (this.targetNodeComponent) {
|
||||
this.targetNodeComponent.destroy();
|
||||
}
|
||||
|
||||
this.el.remove();
|
||||
this.playPauseBtnEl = this.rewindBtnEl = this.fastForwardBtnEl = null;
|
||||
|
@ -261,11 +274,17 @@ PlayerWidget.prototype = {
|
|||
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);
|
||||
|
||||
|
@ -359,8 +378,6 @@ PlayerWidget.prototype = {
|
|||
}
|
||||
});
|
||||
|
||||
this.containerEl.appendChild(this.el);
|
||||
|
||||
// Show the initial time.
|
||||
this.displayTime(state.currentTime);
|
||||
},
|
||||
|
@ -571,261 +588,3 @@ PlayerWidget.prototype = {
|
|||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* UI component responsible for displaying and updating the player meta-data:
|
||||
* name, duration, iterations, delay.
|
||||
* The parent UI component for this should drive its updates by calling
|
||||
* render(state) whenever it wants the component to update.
|
||||
*/
|
||||
function PlayerMetaDataHeader() {
|
||||
// Store the various state pieces we need to only refresh the UI when things
|
||||
// change.
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
PlayerMetaDataHeader.prototype = {
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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;
|
|
@ -5,3 +5,7 @@
|
|||
# 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',
|
||||
]
|
||||
|
|
|
@ -20,6 +20,7 @@ support-files =
|
|||
[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]
|
||||
|
@ -28,6 +29,7 @@ support-files =
|
|||
[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]
|
||||
|
|
|
@ -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");
|
||||
});
|
|
@ -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;
|
||||
});
|
|
@ -55,6 +55,7 @@ EXTRA_JS_MODULES.devtools.shared += [
|
|||
'node-attribute-parser.js',
|
||||
'observable-object.js',
|
||||
'options-view.js',
|
||||
'poller.js',
|
||||
'source-utils.js',
|
||||
'telemetry.js',
|
||||
'theme-switching.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();
|
||||
}
|
||||
};
|
|
@ -84,6 +84,7 @@ skip-if = e10s # Layouthelpers test should not run in a content page.
|
|||
[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]
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
});
|
|
@ -242,3 +242,24 @@ function* openAndCloseToolbox(nbOfTimes, usageTime, toolId) {
|
|||
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!
|
||||
|
|
|
@ -152,6 +152,7 @@ function CssHtmlTree(aStyleInspector, aPageStyle)
|
|||
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);
|
||||
|
@ -168,6 +169,7 @@ function CssHtmlTree(aStyleInspector, aPageStyle)
|
|||
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",
|
||||
|
@ -544,6 +546,46 @@ CssHtmlTree.prototype = {
|
|||
}, 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.
|
||||
*
|
||||
|
@ -831,29 +873,6 @@ CssHtmlTree.prototype = {
|
|||
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.
|
||||
*/
|
||||
|
@ -906,6 +925,7 @@ CssHtmlTree.prototype = {
|
|||
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",
|
||||
|
|
|
@ -1129,6 +1129,7 @@ function CssRuleView(aInspector, aDoc, aStore, aPageStyle) {
|
|||
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);
|
||||
|
||||
|
@ -1141,6 +1142,7 @@ function CssRuleView(aInspector, aDoc, aStore, aPageStyle) {
|
|||
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);
|
||||
|
||||
|
@ -1647,6 +1649,18 @@ CssRuleView.prototype = {
|
|||
}, 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.
|
||||
*/
|
||||
|
@ -1662,12 +1676,17 @@ CssRuleView.prototype = {
|
|||
|
||||
/**
|
||||
* 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() {
|
||||
|
@ -1721,6 +1740,7 @@ CssRuleView.prototype = {
|
|||
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);
|
||||
|
|
|
@ -42,6 +42,7 @@ support-files =
|
|||
[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]
|
||||
|
@ -123,6 +124,7 @@ skip-if = e10s # Bug 1090340
|
|||
[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]
|
||||
|
|
|
@ -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");
|
||||
});
|
||||
}
|
|
@ -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");
|
||||
}
|
|
@ -99,6 +99,36 @@ body {
|
|||
}
|
||||
}
|
||||
|
||||
/* 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 {
|
||||
|
|
|
@ -355,6 +355,23 @@ let Activities = {
|
|||
calleeApp.appStatus !== Ci.nsIPrincipal.APP_STATUS_CERTIFIED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the activity is in the developer mode activity list, only let the
|
||||
// system app be a provider.
|
||||
let isSystemApp = false;
|
||||
let isDevModeActivity = false;
|
||||
try {
|
||||
isSystemApp =
|
||||
aResult.manifest == Services.prefs.getCharPref("b2g.system_manifest_url");
|
||||
isDevModeActivity =
|
||||
Services.prefs.getCharPref("dom.activities.developer_mode_only")
|
||||
.split(",").indexOf(aMsg.options.name) !== -1;
|
||||
} catch(e) {}
|
||||
|
||||
if (isDevModeActivity && !isSystemApp) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ActivitiesServiceFilter.match(aMsg.options.data,
|
||||
aResult.description.filters);
|
||||
};
|
||||
|
|
|
@ -69,6 +69,22 @@ ActivityProxy.prototype = {
|
|||
return;
|
||||
}
|
||||
|
||||
// Check the activities that are restricted to be used in dev mode.
|
||||
let devMode = false;
|
||||
let isDevModeActivity = false;
|
||||
try {
|
||||
devMode = Services.prefs.getBoolPref("dom.apps.developer_mode");
|
||||
isDevModeActivity =
|
||||
Services.prefs.getCharPref("dom.activities.developer_mode_only")
|
||||
.split(",").indexOf(aOptions.name) !== -1;
|
||||
|
||||
} catch(e) {}
|
||||
if (isDevModeActivity && !devMode) {
|
||||
Services.DOMRequest.fireErrorAsync(this.activity, "SecurityError");
|
||||
Services.obs.notifyObservers(null, "Activity:Error", null);
|
||||
return;
|
||||
}
|
||||
|
||||
cpmm.addMessageListener("Activity:FireSuccess", this);
|
||||
cpmm.addMessageListener("Activity:FireError", this);
|
||||
|
||||
|
|
|
@ -8,6 +8,8 @@ DIRS += ['interfaces']
|
|||
|
||||
XPCSHELL_TESTS_MANIFESTS += ['tests/unit/xpcshell.ini']
|
||||
|
||||
MOCHITEST_CHROME_MANIFESTS += ['tests/mochi/mochitest.ini']
|
||||
|
||||
EXPORTS.mozilla.dom += [
|
||||
'Activity.h',
|
||||
]
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "Random app",
|
||||
"activities": {
|
||||
"import-app": { "blob": { "required": true } }
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
Content-Type: application/manifest+json
|
|
@ -0,0 +1,9 @@
|
|||
[DEFAULT]
|
||||
skip-if = e10s
|
||||
support-files =
|
||||
system.webapp
|
||||
system.webapp^headers^
|
||||
manifest.webapp
|
||||
manifest.webapp^headers^
|
||||
|
||||
[test_dev_mode_activity.html]
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "System app",
|
||||
"activities": {
|
||||
"import-app": { "blob": { "required": true } }
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
Content-Type: application/manifest+json
|
|
@ -0,0 +1,290 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<!--
|
||||
https://bugzilla.mozilla.org/show_bug.cgi?id={1123846}
|
||||
-->
|
||||
<head>
|
||||
<title>Test for Bug {1123846}</title>
|
||||
<script type="application/javascript"
|
||||
src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script type="application/javascript"
|
||||
src="chrome://mochikit/content/chrome-harness.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id={1123846}">Mozilla Bug {1123846}</a>
|
||||
<p id="display"></p>
|
||||
<div id="content" style="display: none">
|
||||
|
||||
</div>
|
||||
<pre id="test">
|
||||
<script class="testbody" type="application/javascript;version=1.7">
|
||||
|
||||
|
||||
/**
|
||||
* Tests the developer mode activities that can only be provided by the
|
||||
* system app.
|
||||
*
|
||||
* We test the following:
|
||||
* 1) No dev mode, no system app installed (failure).
|
||||
* 2) No dev mode, system app installed (failure).
|
||||
* 3) No dev mode, system app and other app installed (failure).
|
||||
* 4) Dev mode, system app and other app installed (success, only system app returned).
|
||||
*/
|
||||
|
||||
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
|
||||
|
||||
var gRootUrl = "http://test/chrome/dom/activities/tests/mochi/";
|
||||
var gGenerator = runTest();
|
||||
|
||||
function go() {
|
||||
SpecialPowers.pushPermissions(
|
||||
[{ "type": "webapps-manage", "allow": 1, "context": document },
|
||||
{ "type": "browser", "allow": 1, "context": document },
|
||||
{ "type": "embed-apps", "allow": 1, "context": document }],
|
||||
function() {
|
||||
SpecialPowers.pushPrefEnv(
|
||||
{'set': [["dom.mozBrowserFramesEnabled", true],
|
||||
["dom.sysmsg.enabled", true],
|
||||
["dom.apps.developer_mode", false],
|
||||
["dom.activities.developer_mode_only", "import-app"]]},
|
||||
continueTest) });
|
||||
}
|
||||
|
||||
function cbError(aEvent) {
|
||||
ok(false, "Error callback invoked " +
|
||||
aEvent.target.error.name + " " + aEvent.target.error.message);
|
||||
finish();
|
||||
}
|
||||
|
||||
function unexpectedSuccess(aMsg) {
|
||||
return function() {
|
||||
ok(false, "Should not have succeeded: " + aMsg);
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
|
||||
var systemAppUrl = gRootUrl + "system.webapp";
|
||||
var otherAppUrl = gRootUrl + "manifest.webapp";
|
||||
|
||||
function installApp(aUrl) {
|
||||
var request = navigator.mozApps.install(aUrl, { });
|
||||
request.onerror = cbError;
|
||||
request.onsuccess = continueTest;
|
||||
return request;
|
||||
}
|
||||
|
||||
function installSystemApp() {
|
||||
return installApp(systemAppUrl);
|
||||
}
|
||||
|
||||
function installOtherApp() {
|
||||
return installApp(otherAppUrl);
|
||||
}
|
||||
|
||||
function uninstall(aApp) {
|
||||
info("Uninstalling " + (aApp ? aApp.manifestURL : "NO APP!!"));
|
||||
var request = navigator.mozApps.mgmt.uninstall(aApp);
|
||||
request.onerror = cbError;
|
||||
request.onsuccess = continueTest;
|
||||
}
|
||||
|
||||
function registerComponent(aObject, aDescription, aContract) {
|
||||
var uuidGenerator = Cc["@mozilla.org/uuid-generator;1"]
|
||||
.getService(Ci.nsIUUIDGenerator);
|
||||
var cid = uuidGenerator.generateUUID();
|
||||
|
||||
info("Registering " + cid);
|
||||
|
||||
var componentManager =
|
||||
Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
|
||||
componentManager.registerFactory(cid, aDescription, aContract, aObject);
|
||||
|
||||
// Keep the id on the object so we can unregister later.
|
||||
aObject.cid = cid;
|
||||
}
|
||||
|
||||
function unregisterComponent(aObject) {
|
||||
info("Unregistering " + aObject.cid);
|
||||
var componentManager =
|
||||
Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
|
||||
componentManager.unregisterFactory(aObject.cid, aObject);
|
||||
}
|
||||
|
||||
var ActivityGlue = {
|
||||
// nsISupports implementation.
|
||||
QueryInterface: function(iid) {
|
||||
if (iid.equals(Ci.nsISupports) ||
|
||||
iid.equals(Ci.nsIFactory) ||
|
||||
iid.equals(Ci.nsIActivityUIGlue)) {
|
||||
return this;
|
||||
}
|
||||
|
||||
throw Cr.NS_ERROR_NO_INTERFACE;
|
||||
},
|
||||
|
||||
// nsIFactory implementation.
|
||||
createInstance: function(outer, iid) {
|
||||
return this.QueryInterface(iid);
|
||||
},
|
||||
|
||||
// nsIActivityUIGlue implementation.
|
||||
chooseActivity: function(aOptions, aActivities, aCallback) {
|
||||
aCallback.handleEvent(Ci.nsIActivityUIGlueCallback.WEBAPPS_ACTIVITY,
|
||||
aActivities.length == 1 ? 0 : -1);
|
||||
}
|
||||
};
|
||||
|
||||
var SystemMessageGlue = {
|
||||
// nsISupports implementation.
|
||||
QueryInterface: function(iid) {
|
||||
if (iid.equals(Ci.nsISupports) ||
|
||||
iid.equals(Ci.nsIFactory) ||
|
||||
iid.equals(Ci.nsISystemMessageGlue)) {
|
||||
return this;
|
||||
}
|
||||
|
||||
throw Cr.NS_ERROR_NO_INTERFACE;
|
||||
},
|
||||
|
||||
// nsIFactory implementation.
|
||||
createInstance: function(outer, iid) {
|
||||
return this.QueryInterface(iid);
|
||||
},
|
||||
|
||||
// nsISystemMessageGlue implementation.
|
||||
openApp(pageURL, manifestURL, type, target, showApp, onlyShowApp, extra) {
|
||||
// We should only try to open a page in the sytem app.
|
||||
is(manifestURL, systemAppUrl, "Opening a page in the system app.");
|
||||
}
|
||||
};
|
||||
|
||||
registerComponent(ActivityGlue,
|
||||
"Activity Glue",
|
||||
"@mozilla.org/dom/activities/ui-glue;1");
|
||||
|
||||
registerComponent(SystemMessageGlue,
|
||||
"System Message Glue",
|
||||
"@mozilla.org/dom/messages/system-message-glue;1");
|
||||
|
||||
function finish() {
|
||||
unregisterComponent(ActivityGlue);
|
||||
unregisterComponent(SystemMessageGlue);
|
||||
SimpleTest.finish();
|
||||
}
|
||||
|
||||
function continueTest() {
|
||||
try {
|
||||
gGenerator.next();
|
||||
} catch (e if e instanceof StopIteration) {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test exporting and importing hosted and packaged apps.
|
||||
*/
|
||||
function runTest() {
|
||||
SpecialPowers.setAllAppsLaunchable(true);
|
||||
|
||||
SpecialPowers.autoConfirmAppInstall(continueTest);
|
||||
yield undefined;
|
||||
|
||||
SpecialPowers.autoConfirmAppUninstall(continueTest);
|
||||
yield undefined;
|
||||
|
||||
// Check how many apps we are starting with.
|
||||
var request = navigator.mozApps.mgmt.getAll();
|
||||
request.onerror = cbError;
|
||||
request.onsuccess = continueTest;
|
||||
yield undefined;
|
||||
var initialAppsCount = request.result.length;
|
||||
info("Starting with " + initialAppsCount + " apps installed.");
|
||||
|
||||
// 1) No dev mode, no system app installed (failure).
|
||||
var activity = new MozActivity({ name: "import-app" });
|
||||
activity.onerror = function() {
|
||||
ok(true, "1) No dev mode, no system app installed");
|
||||
continueTest();
|
||||
}
|
||||
activity.onsuccess = unexpectedSuccess("1) No dev mode, no system app installed");
|
||||
yield undefined;
|
||||
|
||||
|
||||
// 2) No dev mode, system app installed (failure).
|
||||
// Configure the system app manifest url.
|
||||
SpecialPowers.pushPrefEnv(
|
||||
{'set': [["b2g.system_manifest_url", systemAppUrl]]},
|
||||
continueTest);
|
||||
yield undefined;
|
||||
|
||||
// Install the system app.
|
||||
request = installSystemApp();
|
||||
yield undefined;
|
||||
var systemApp = request.result;
|
||||
ok(systemApp, "systemApp is non-null");
|
||||
|
||||
activity = new MozActivity({ name: "import-app" });
|
||||
activity.onerror = function() {
|
||||
ok(true, "2) No dev mode, system app installed");
|
||||
continueTest();
|
||||
}
|
||||
activity.onsuccess = unexpectedSuccess("2) No dev mode, system app installed");
|
||||
yield undefined;
|
||||
|
||||
// 3) No dev mode, system app and other app installed (failure).
|
||||
request = installOtherApp();
|
||||
yield undefined;
|
||||
var otherApp = request.result;
|
||||
ok(otherApp, "otherApp is non-null");
|
||||
|
||||
activity = new MozActivity({ name: "import-app" });
|
||||
activity.onerror = function() {
|
||||
ok(true, "3) No dev mode, system app and other app installed");
|
||||
continueTest();
|
||||
}
|
||||
activity.onsuccess = unexpectedSuccess("3) No dev mode, system app and other app installed");
|
||||
yield undefined;
|
||||
|
||||
// 4) Dev mode, no system app installed.
|
||||
SpecialPowers.pushPrefEnv(
|
||||
{'set': [["dom.apps.developer_mode", true]]},
|
||||
continueTest);
|
||||
yield undefined;
|
||||
|
||||
activity = new MozActivity({ name: "import-app" });
|
||||
activity.onsuccess = function() {
|
||||
ok(true, "4) Dev mode, system app and other app installed");
|
||||
continueTest();
|
||||
}
|
||||
activity.onerror = function(aError) {
|
||||
ok(false, "Got error: " + aError.name);
|
||||
finish();
|
||||
}
|
||||
yield undefined;
|
||||
|
||||
// Cleanup
|
||||
uninstall(systemApp);
|
||||
yield undefined;
|
||||
|
||||
uninstall(otherApp);
|
||||
yield undefined;
|
||||
|
||||
// Check that we restored the app registry.
|
||||
request = navigator.mozApps.mgmt.getAll();
|
||||
request.onerror = cbError;
|
||||
request.onsuccess = continueTest;
|
||||
yield undefined;
|
||||
|
||||
is(request.result.length, initialAppsCount, "All apps are uninstalled.");
|
||||
}
|
||||
|
||||
addLoadEvent(go);
|
||||
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
|
@ -41,7 +41,7 @@ public class GuestSession {
|
|||
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
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;
|
||||
|
@ -69,7 +68,7 @@ public class TabQueueDispatcher extends Locales.LocaleAwareActivity {
|
|||
* 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();
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
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;
|
||||
|
@ -154,7 +154,8 @@ public class TabQueueHelper {
|
|||
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);
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
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;
|
||||
|
@ -190,7 +190,7 @@ public class TabQueueService extends Service {
|
|||
|
||||
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);
|
||||
|
||||
|
|
|
@ -195,19 +195,6 @@ this.TelemetryController = Object.freeze({
|
|||
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.
|
||||
|
@ -284,6 +271,16 @@ this.TelemetryController = Object.freeze({
|
|||
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.
|
||||
|
@ -500,23 +497,6 @@ let Impl = {
|
|||
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
|
||||
|
@ -720,6 +700,26 @@ let Impl = {
|
|||
.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);
|
||||
|
||||
|
|
|
@ -1532,12 +1532,11 @@ let Impl = {
|
|||
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();
|
||||
|
||||
|
@ -1965,8 +1964,8 @@ let Impl = {
|
|||
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.
|
||||
|
@ -1985,7 +1984,7 @@ let Impl = {
|
|||
*/
|
||||
_getSessionDataObject: function() {
|
||||
return {
|
||||
previousSubsessionId: this._previousSubsessionId,
|
||||
subsessionId: this._subsessionId,
|
||||
profileSubsessionCounter: this._profileSubsessionCounter,
|
||||
};
|
||||
},
|
||||
|
@ -2009,8 +2008,7 @@ let Impl = {
|
|||
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,
|
||||
|
@ -2078,7 +2076,7 @@ let Impl = {
|
|||
if (abortedExists) {
|
||||
this._log.trace("_checkAbortedSessionPing - aborted session found: " + FILE_PATH);
|
||||
yield this._abortedSessionSerializer.enqueueTask(
|
||||
() => TelemetryController.addPendingPingFromFile(FILE_PATH, true));
|
||||
() => TelemetryController.addAbortedSessionPing(FILE_PATH));
|
||||
}
|
||||
}),
|
||||
|
||||
|
@ -2095,7 +2093,7 @@ let Impl = {
|
|||
|
||||
let payload = null;
|
||||
if (aProvidedPayload) {
|
||||
payload = aProvidedPayload;
|
||||
payload = Cu.cloneInto(aProvidedPayload, myScope);
|
||||
// Overwrite the original reason.
|
||||
payload.info.reason = REASON_ABORTED_SESSION;
|
||||
} else {
|
||||
|
|
|
@ -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();
|
||||
});
|
|
@ -1167,7 +1167,7 @@ add_task(function* test_savedSessionData() {
|
|||
// 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);
|
||||
|
@ -1209,7 +1209,40 @@ add_task(function* test_savedSessionData() {
|
|||
// 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() {
|
||||
|
@ -1238,7 +1271,7 @@ add_task(function* test_invalidSessionData() {
|
|||
// 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() {
|
||||
|
|
|
@ -20,6 +20,7 @@ generated-files =
|
|||
theme.xpi
|
||||
|
||||
[test_nsITelemetry.js]
|
||||
[test_SubsessionChaining.js]
|
||||
[test_TelemetryEnvironment.js]
|
||||
# Bug 1144395: crash on Android 4.3
|
||||
skip-if = android_version == "18"
|
||||
|
|
|
@ -2885,19 +2885,13 @@ var WalkerActor = protocol.ActorClass({
|
|||
}),
|
||||
|
||||
/**
|
||||
* Given an StyleSheetActor (identified by its ID), commonly used in the
|
||||
* Given a StyleSheetActor (identified by its ID), commonly used in the
|
||||
* style-editor, get its ownerNode and return the corresponding walker's
|
||||
* NodeActor
|
||||
* NodeActor.
|
||||
* Note that getNodeFromActor was added later and can now be used instead.
|
||||
*/
|
||||
getStyleSheetOwnerNode: method(function(styleSheetActorID) {
|
||||
let styleSheetActor = this.conn.getActor(styleSheetActorID);
|
||||
let ownerNode = styleSheetActor.ownerNode;
|
||||
|
||||
if (!styleSheetActor || !ownerNode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.attachElement(ownerNode);
|
||||
return this.getNodeFromActor(styleSheetActorID, ["ownerNode"]);
|
||||
}, {
|
||||
request: {
|
||||
styleSheetActorID: Arg(0, "string")
|
||||
|
@ -2906,6 +2900,60 @@ var WalkerActor = protocol.ActorClass({
|
|||
ownerNode: RetVal("nullable:disconnectedNode")
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* This method can be used to retrieve NodeActor for DOM nodes from other
|
||||
* actors in a way that they can later be highlighted in the page, or
|
||||
* selected in the inspector.
|
||||
* If an actor has a reference to a DOM node, and the UI needs to know about
|
||||
* this DOM node (and possibly select it in the inspector), the UI should
|
||||
* first retrieve a reference to the walkerFront:
|
||||
*
|
||||
* // Make sure the inspector/walker have been initialized first.
|
||||
* toolbox.initInspector().then(() => {
|
||||
* // Retrieve the walker.
|
||||
* let walker = toolbox.walker;
|
||||
* });
|
||||
*
|
||||
* And then call this method:
|
||||
*
|
||||
* // Get the nodeFront from my actor, passing the ID and properties path.
|
||||
* walker.getNodeFromActor(myActorID, ["element"]).then(nodeFront => {
|
||||
* // Use the nodeFront, e.g. select the node in the inspector.
|
||||
* toolbox.getPanel("inspector").selection.setNodeFront(nodeFront);
|
||||
* });
|
||||
*
|
||||
* @param {String} actorID The ID for the actor that has a reference to the
|
||||
* DOM node.
|
||||
* @param {Array} path Where, on the actor, is the DOM node stored. If in the
|
||||
* scope of the actor, the node is available as `this.data.node`, then this
|
||||
* should be ["data", "node"].
|
||||
* @return {NodeActor} The attached NodeActor, or null if it couldn't be found.
|
||||
*/
|
||||
getNodeFromActor: method(function(actorID, path) {
|
||||
let actor = this.conn.getActor(actorID);
|
||||
if (!actor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let obj = actor;
|
||||
for (let name of path) {
|
||||
if (!(name in obj)) {
|
||||
return null;
|
||||
}
|
||||
obj = obj[name];
|
||||
}
|
||||
|
||||
return this.attachElement(obj);
|
||||
}, {
|
||||
request: {
|
||||
actorID: Arg(0, "string"),
|
||||
path: Arg(1, "array:string")
|
||||
},
|
||||
response: {
|
||||
node: RetVal("nullable:disconnectedNode")
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
/**
|
||||
|
@ -3063,6 +3111,14 @@ var WalkerFront = exports.WalkerFront = protocol.FrontClass(WalkerActor, {
|
|||
impl: "_getStyleSheetOwnerNode"
|
||||
}),
|
||||
|
||||
getNodeFromActor: protocol.custom(function(actorID, path) {
|
||||
return this._getNodeFromActor(actorID, path).then(response => {
|
||||
return response ? response.node : null;
|
||||
});
|
||||
}, {
|
||||
impl: "_getNodeFromActor"
|
||||
}),
|
||||
|
||||
_releaseFront: function(node, force) {
|
||||
if (node.retained && !force) {
|
||||
node.reparent(null);
|
||||
|
|
|
@ -59,6 +59,16 @@ ProfilerActor.prototype = {
|
|||
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.
|
||||
|
@ -321,6 +331,7 @@ function checkProfilerConsumers() {
|
|||
* 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,
|
||||
|
|
|
@ -172,6 +172,9 @@ RootActor.prototype = {
|
|||
// 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,
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
@ -53,6 +53,7 @@ skip-if = buildapp == 'mulet'
|
|||
[test_inspector-dead-nodes.html]
|
||||
[test_inspector_getImageData.html]
|
||||
skip-if = buildapp == 'mulet'
|
||||
[test_inspector_getNodeFromActor.html]
|
||||
[test_inspector-hide.html]
|
||||
[test_inspector-insert.html]
|
||||
[test_inspector-mutations-attr.html]
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
https://bugzilla.mozilla.org/show_bug.cgi?id=1155653
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Test for Bug 1155653</title>
|
||||
|
||||
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
|
||||
<script type="application/javascript;version=1.8" src="inspector-helpers.js"></script>
|
||||
<script type="application/javascript;version=1.8">
|
||||
Components.utils.import("resource://gre/modules/devtools/Loader.jsm");
|
||||
const inspector = devtools.require("devtools/server/actors/inspector");
|
||||
|
||||
window.onload = function() {
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
runNextTest();
|
||||
}
|
||||
|
||||
var gWalker;
|
||||
|
||||
addTest(function() {
|
||||
let url = document.getElementById("inspectorContent").href;
|
||||
attachURL(url, function(err, client, tab, doc) {
|
||||
let {InspectorFront} = devtools.require("devtools/server/actors/inspector");
|
||||
let inspector = InspectorFront(client, tab);
|
||||
|
||||
promiseDone(inspector.getWalker().then(walker => {
|
||||
gWalker = walker;
|
||||
}).then(runNextTest));
|
||||
});
|
||||
});
|
||||
|
||||
addTest(function() {
|
||||
info("Try to get a NodeFront from an invalid actorID");
|
||||
gWalker.getNodeFromActor("invalid", ["node"]).then(node => {
|
||||
ok(!node, "The node returned is null");
|
||||
runNextTest();
|
||||
});
|
||||
});
|
||||
|
||||
addTest(function() {
|
||||
info("Try to get a NodeFront from a valid actorID but invalid path");
|
||||
gWalker.getNodeFromActor(gWalker.actorID, ["invalid", "path"]).then(node => {
|
||||
ok(!node, "The node returned is null");
|
||||
runNextTest();
|
||||
});
|
||||
});
|
||||
|
||||
addTest(function() {
|
||||
info("Try to get a NodeFront from a valid actorID and valid path");
|
||||
gWalker.getNodeFromActor(gWalker.actorID, ["rootDoc"]).then(rootDocNode => {
|
||||
ok(rootDocNode, "A node was returned");
|
||||
is(rootDocNode, gWalker.rootNode, "The right node was returned");
|
||||
runNextTest();
|
||||
});
|
||||
});
|
||||
|
||||
addTest(function() {
|
||||
info("Try to get a NodeFront from a valid actorID and valid complex path");
|
||||
gWalker.getNodeFromActor(gWalker.actorID,
|
||||
["tabActor", "window", "document", "body"]).then(bodyNode => {
|
||||
ok(bodyNode, "A node was returned");
|
||||
gWalker.querySelector(gWalker.rootNode, "body").then(node => {
|
||||
is(bodyNode, node, "The body node was returned");
|
||||
runNextTest();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
addTest(function() {
|
||||
gWalker = null;
|
||||
runNextTest();
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1155653">Mozilla Bug 1155653</a>
|
||||
<a id="inspectorContent" target="_blank" href="inspector_getImageData.html">Test Document</a>
|
||||
<p id="display"></p>
|
||||
<div id="content" style="display: none">
|
||||
|
||||
</div>
|
||||
<pre id="test">
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
|
@ -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);
|
||||
}
|
|
@ -216,6 +216,7 @@ reason = bug 820380
|
|||
[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]
|
||||
|
|
|
@ -965,7 +965,6 @@ button.button-link {
|
|||
background: transparent;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
text-decoration: underline;
|
||||
color: #0095dd;
|
||||
cursor: pointer;
|
||||
min-width: 0;
|
||||
|
@ -978,20 +977,17 @@ button.button-link:not(:-moz-focusring) > .button-box {
|
|||
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;
|
||||
}
|
||||
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче