Merge fx-team to central, a=merge

This commit is contained in:
Wes Kocher 2015-05-26 15:59:43 -07:00
commit 638c2a9058
79 changed files with 924 additions and 273 deletions

View File

@ -330,6 +330,7 @@ loop.conversationViews = (function(mozL10n) {
var errorString;
switch (this.props.failureReason) {
case FAILURE_DETAILS.NO_MEDIA:
case FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA:
errorString = mozL10n.get("no_media_failure_message");
break;

View File

@ -330,6 +330,7 @@ loop.conversationViews = (function(mozL10n) {
var errorString;
switch (this.props.failureReason) {
case FAILURE_DETAILS.NO_MEDIA:
case FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA:
errorString = mozL10n.get("no_media_failure_message");
break;

View File

@ -978,7 +978,7 @@ loop.OTSdkDriver = (function() {
var bucket = this.mozLoop.SHARING_STATE_CHANGE[type.toUpperCase() + "_" +
(enabled ? "ENABLED" : "DISABLED")];
if (!bucket) {
if (typeof bucket === "undefined") {
console.error("No sharing state bucket found for '" + type + "'");
return;
}

View File

@ -241,7 +241,7 @@ let LoopRoomsInternal = {
* @param {String} roomToken The token for the room that needs encrypting.
*/
queueForEncryption: function(roomToken) {
if (!this.encryptionQueue.queue.includes(roomToken)) {
if (this.encryptionQueue.queue.indexOf(roomToken) == -1) {
this.encryptionQueue.queue.push(roomToken);
}

View File

@ -939,6 +939,15 @@ describe("loop.conversationViews", function () {
expect(view.getDOMNode().querySelector("h2").textContent).eql("no_media_failure_message");
});
it("should show 'no media' for FAILURE_DETAILS.NO_MEDIA reason", function() {
view = mountTestComponent({
cancelCall: function() {},
failureReason: FAILURE_DETAILS.NO_MEDIA
});
expect(view.getDOMNode().querySelector("h2").textContent).eql("no_media_failure_message");
});
it("should show 'generic_failure_title' when no reason is specified", function() {
view = mountTestComponent({cancelCall: function() {}});

View File

@ -78,16 +78,16 @@ describe("loop.OTSdkDriver", function () {
mozLoop = {
telemetryAddValue: sinon.stub(),
TWO_WAY_MEDIA_CONN_LENGTH: {
SHORTER_THAN_10S: "SHORTER_THAN_10S",
BETWEEN_10S_AND_30S: "BETWEEN_10S_AND_30S",
BETWEEN_30S_AND_5M: "BETWEEN_30S_AND_5M",
MORE_THAN_5M: "MORE_THAN_5M"
SHORTER_THAN_10S: 0,
BETWEEN_10S_AND_30S: 1,
BETWEEN_30S_AND_5M: 2,
MORE_THAN_5M: 3
},
SHARING_STATE_CHANGE: {
WINDOW_ENABLED: "WINDOW_ENABLED",
WINDOW_DISABLED: "WINDOW_DISABLED",
BROWSER_ENABLED: "BROWSER_ENABLED",
BROWSER_DISABLED: "BROWSER_DISABLED"
WINDOW_ENABLED: 0,
WINDOW_DISABLED: 1,
BROWSER_ENABLED: 2,
BROWSER_DISABLED: 3
}
};

View File

@ -190,8 +190,8 @@ EventEmitter.decorate(this);
/**
* DOM query helpers.
*/
function $(selector, target = document) target.querySelector(selector);
function $all(selector, target = document) target.querySelectorAll(selector);
let $ = (selector, target = document) => target.querySelector(selector);
let $all = (selector, target = document) => target.querySelectorAll(selector);
/**
* Helper for getting an nsIURL instance out of a string.

View File

@ -57,7 +57,9 @@ CanvasDebuggerPanel.prototype = {
// DevToolPanel API
get target() this._toolbox.target,
get target() {
return this._toolbox.target;
},
destroy: function() {
// Make sure this panel is not already destroyed.

View File

@ -6,7 +6,7 @@
* You can also use this initialization format as a template for other tests.
*/
function ifTestingSupported() {
function* ifTestingSupported() {
let { target, front } = yield initCallWatcherBackend(SIMPLE_CANVAS_URL);
ok(target, "Should have a target available.");

View File

@ -6,7 +6,7 @@
* and that their stack is successfully retrieved.
*/
function ifTestingSupported() {
function* ifTestingSupported() {
let { target, front } = yield initCallWatcherBackend(SIMPLE_CANVAS_URL);
let navigated = once(target, "navigate");

View File

@ -6,7 +6,7 @@
* for a canvas context.
*/
function ifTestingSupported() {
function* ifTestingSupported() {
let { target, front } = yield initCanvasDebuggerBackend(SIMPLE_CANVAS_URL);
let navigated = once(target, "navigate");

View File

@ -6,7 +6,7 @@
* the correct thumbnails.
*/
function ifTestingSupported() {
function* ifTestingSupported() {
let { target, front } = yield initCanvasDebuggerBackend(SIMPLE_CANVAS_URL);
let navigated = once(target, "navigate");

View File

@ -6,7 +6,7 @@
* the correct "end result" screenshot.
*/
function ifTestingSupported() {
function* ifTestingSupported() {
let { target, front } = yield initCanvasDebuggerBackend(SIMPLE_CANVAS_URL);
let navigated = once(target, "navigate");

View File

@ -5,7 +5,7 @@
* Tests if screenshots for arbitrary draw calls are generated properly.
*/
function ifTestingSupported() {
function* ifTestingSupported() {
let { target, front } = yield initCanvasDebuggerBackend(SIMPLE_CANVAS_TRANSPARENT_URL);
let navigated = once(target, "navigate");

View File

@ -6,7 +6,7 @@
* by deferring the the most recent previous draw-call.
*/
function ifTestingSupported() {
function* ifTestingSupported() {
let { target, front } = yield initCanvasDebuggerBackend(SIMPLE_CANVAS_URL);
let navigated = once(target, "navigate");

View File

@ -6,7 +6,7 @@
* forms if the method's signature does not expect an enum. Bug 999687.
*/
function ifTestingSupported() {
function* ifTestingSupported() {
let { target, front } = yield initCanvasDebuggerBackend(SIMPLE_BITMASKS_URL);
let navigated = once(target, "navigate");

View File

@ -6,7 +6,7 @@
* forms if the method's signature does not expect an enum. Bug 999687.
*/
function ifTestingSupported() {
function* ifTestingSupported() {
let { target, front } = yield initCanvasDebuggerBackend(WEBGL_ENUM_URL);
let navigated = once(target, "navigate");

View File

@ -6,7 +6,7 @@
* after generating screenshots using the actor.
*/
function ifTestingSupported() {
function* ifTestingSupported() {
let { target, front } = yield initCanvasDebuggerBackend(WEBGL_BINDINGS_URL);
loadFrameScripts();

View File

@ -6,7 +6,7 @@
* for a canvas context, and that the generated screenshots are correct.
*/
function ifTestingSupported() {
function* ifTestingSupported() {
let { target, front } = yield initCanvasDebuggerBackend(SET_TIMEOUT_URL);
let navigated = once(target, "navigate");

View File

@ -6,7 +6,7 @@
* in the event no rAF loop is found.
*/
function ifTestingSupported() {
function* ifTestingSupported() {
let { target, front } = yield initCanvasDebuggerBackend(NO_CANVAS_URL);
loadFrameScripts();

View File

@ -5,7 +5,7 @@
* Tests if certain function calls are properly highlighted in the UI.
*/
function ifTestingSupported() {
function* ifTestingSupported() {
let { target, panel } = yield initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
let { window, $, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;

View File

@ -6,7 +6,7 @@
* are properly displayed in the UI.
*/
function ifTestingSupported() {
function* ifTestingSupported() {
let { target, panel } = yield initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
let { window, $, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;

View File

@ -5,7 +5,7 @@
* Tests if filtering the items in the call list works properly.
*/
function ifTestingSupported() {
function* ifTestingSupported() {
let { target, panel } = yield initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
let { window, $, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;
let searchbox = $("#calls-searchbox");

View File

@ -5,7 +5,7 @@
* Tests if the a function call's stack is properly displayed in the UI.
*/
function ifTestingSupported() {
function* ifTestingSupported() {
let { target, panel } = yield initCanvasDebuggerFrontend(SIMPLE_CANVAS_DEEP_STACK_URL);
let { window, $, $all, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;

View File

@ -6,7 +6,7 @@
* and jumping to source in the debugger for the topmost call item works.
*/
function ifTestingSupported() {
function* ifTestingSupported() {
let { target, panel } = yield initCanvasDebuggerFrontend(SIMPLE_CANVAS_DEEP_STACK_URL);
let { window, $, $all, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;

View File

@ -6,7 +6,7 @@
* on a function call item.
*/
function ifTestingSupported() {
function* ifTestingSupported() {
let { target, panel } = yield initCanvasDebuggerFrontend(SIMPLE_CANVAS_DEEP_STACK_URL);
let { window, $, $all, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;

View File

@ -5,7 +5,7 @@
* Tests if clearing the snapshots list works as expected.
*/
function ifTestingSupported() {
function* ifTestingSupported() {
let { target, panel } = yield initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
let { window, EVENTS, SnapshotsListView } = panel.panelWin;

View File

@ -5,7 +5,7 @@
* Tests if screenshots are properly displayed in the UI.
*/
function ifTestingSupported() {
function* ifTestingSupported() {
let { target, panel } = yield initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
let { window, $, EVENTS, SnapshotsListView } = panel.panelWin;

View File

@ -5,7 +5,7 @@
* Tests if thumbnails are properly displayed in the UI.
*/
function ifTestingSupported() {
function* ifTestingSupported() {
let { target, panel } = yield initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
let { window, $, $all, EVENTS, SnapshotsListView } = panel.panelWin;

View File

@ -6,7 +6,7 @@
* function call items and their respective screenshots.
*/
function ifTestingSupported() {
function* ifTestingSupported() {
let { target, panel } = yield initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
let { window, $, $all, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;

View File

@ -5,7 +5,7 @@
* Tests that the frontend UI is properly configured when opening the tool.
*/
function ifTestingSupported() {
function* ifTestingSupported() {
let { target, panel } = yield initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
let { $ } = panel.panelWin;

View File

@ -5,7 +5,7 @@
* Tests whether the frontend behaves correctly while reording a snapshot.
*/
function ifTestingSupported() {
function* ifTestingSupported() {
let { target, panel } = yield initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
let { window, EVENTS, $, SnapshotsListView } = panel.panelWin;

View File

@ -5,7 +5,7 @@
* Tests whether the frontend displays a placeholder snapshot while recording.
*/
function ifTestingSupported() {
function* ifTestingSupported() {
let { target, panel } = yield initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
let { window, EVENTS, L10N, $, SnapshotsListView } = panel.panelWin;

View File

@ -6,7 +6,7 @@
* after finishing recording.
*/
function ifTestingSupported() {
function* ifTestingSupported() {
let { target, panel } = yield initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
let { window, EVENTS, $, SnapshotsListView } = panel.panelWin;

View File

@ -8,7 +8,7 @@
* of its loop, when the recording starts before the rAFs start.
*/
function ifTestingSupported() {
function* ifTestingSupported() {
let { target, panel } = yield initCanvasDebuggerFrontend(RAF_BEGIN_URL);
let { window, EVENTS, gFront, SnapshotsListView } = panel.panelWin;
loadFrameScripts();

View File

@ -5,7 +5,7 @@
* Tests that the frontend UI is properly reconfigured after reloading.
*/
function ifTestingSupported() {
function* ifTestingSupported() {
let { target, panel } = yield initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
let { window, $, EVENTS } = panel.panelWin;

View File

@ -5,7 +5,7 @@
* Tests that the frontend UI is properly reconfigured after reloading.
*/
function ifTestingSupported() {
function* ifTestingSupported() {
let { target, panel } = yield initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
let { window, $, $all, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;

View File

@ -5,7 +5,7 @@
* Tests if the slider in the calls list view works as advertised.
*/
function ifTestingSupported() {
function* ifTestingSupported() {
let { target, panel } = yield initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
let { window, $, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;

View File

@ -5,7 +5,7 @@
* Tests if the slider in the calls list view works as advertised.
*/
function ifTestingSupported() {
function* ifTestingSupported() {
let { target, panel } = yield initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
let { window, $, EVENTS, gFront, SnapshotsListView, CallsListView } = panel.panelWin;

View File

@ -6,7 +6,7 @@
* respective to their recorded animation frame.
*/
function ifTestingSupported() {
function* ifTestingSupported() {
let { target, panel } = yield initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
let { window, $, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;

View File

@ -6,7 +6,7 @@
* respective to their recorded animation frame.
*/
function ifTestingSupported() {
function* ifTestingSupported() {
let { target, panel } = yield initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
let { window, $, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;

View File

@ -5,7 +5,7 @@
* Tests if the stepping buttons in the call list toolbar work as advertised.
*/
function ifTestingSupported() {
function* ifTestingSupported() {
let { target, panel } = yield initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
let { window, $, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;

View File

@ -5,7 +5,7 @@
* Tests that you can stop a recording that does not have a rAF cycle.
*/
function ifTestingSupported() {
function* ifTestingSupported() {
let { target, panel } = yield initCanvasDebuggerFrontend(NO_CANVAS_URL);
let { window, EVENTS, $, SnapshotsListView } = panel.panelWin;

View File

@ -5,7 +5,7 @@
* Tests that a recording that does not have a rAF cycle fails after timeout.
*/
function ifTestingSupported() {
function* ifTestingSupported() {
let { target, panel } = yield initCanvasDebuggerFrontend(NO_CANVAS_URL);
let { window, EVENTS, $, SnapshotsListView } = panel.panelWin;

View File

@ -6,7 +6,7 @@
* after timeout.
*/
function ifTestingSupported() {
function* ifTestingSupported() {
let { target, panel } = yield initCanvasDebuggerFrontend(RAF_NO_CANVAS_URL);
let { window, EVENTS, $, SnapshotsListView } = panel.panelWin;

View File

@ -66,6 +66,7 @@ support-files =
[browser_graphs-09f.js]
[browser_graphs-10a.js]
[browser_graphs-10b.js]
[browser_graphs-10c.js]
[browser_graphs-11a.js]
[browser_graphs-11b.js]
[browser_graphs-12.js]

View File

@ -0,0 +1,86 @@
// Tests that graphs properly handle resizing.
const TEST_DATA = [{ delta: 112, value: 48 }, { delta: 213, value: 59 }, { delta: 313, value: 60 }, { delta: 413, value: 59 }, { delta: 530, value: 59 }, { delta: 646, value: 58 }, { delta: 747, value: 60 }, { delta: 863, value: 48 }, { delta: 980, value: 37 }, { delta: 1097, value: 30 }, { delta: 1213, value: 29 }, { delta: 1330, value: 23 }, { delta: 1430, value: 10 }, { delta: 1534, value: 17 }, { delta: 1645, value: 20 }, { delta: 1746, value: 22 }, { delta: 1846, value: 39 }, { delta: 1963, value: 26 }, { delta: 2080, value: 27 }, { delta: 2197, value: 35 }, { delta: 2312, value: 47 }, { delta: 2412, value: 53 }, { delta: 2514, value: 60 }, { delta: 2630, value: 37 }, { delta: 2730, value: 36 }, { delta: 2830, value: 37 }, { delta: 2946, value: 36 }, { delta: 3046, value: 40 }, { delta: 3163, value: 47 }, { delta: 3280, value: 41 }, { delta: 3380, value: 35 }, { delta: 3480, value: 27 }, { delta: 3580, value: 39 }, { delta: 3680, value: 42 }, { delta: 3780, value: 49 }, { delta: 3880, value: 55 }, { delta: 3980, value: 60 }, { delta: 4080, value: 60 }, { delta: 4180, value: 60 }];
let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
add_task(function*() {
yield promiseTab("about:blank");
yield performTest();
gBrowser.removeCurrentTab();
});
function* performTest() {
let [host, win, doc] = yield createHost("window");
doc.body.setAttribute("style", "position: fixed; width: 100%; height: 100%; margin: 0;");
let graph = new LineGraphWidget(doc.body, "fps");
yield graph.once("ready");
let refreshCount = 0;
graph.on("refresh", () => refreshCount++);
yield testGraph(host, graph);
is(refreshCount, 2, "The graph should've been refreshed 2 times.");
yield graph.destroy();
host.destroy();
}
function* testGraph(host, graph) {
graph.setData(TEST_DATA);
host._window.resizeTo(500, 500);
yield graph.once("refresh");
let oldBounds = host.frame.getBoundingClientRect();
is (graph._width, oldBounds.width * window.devicePixelRatio,
"The window was properly resized (1).");
is (graph._height, oldBounds.height * window.devicePixelRatio,
"The window was properly resized (1).");
dragStart(graph, 100);
dragStop(graph, 400);
is(graph.getSelection().start, 100,
"The current selection start value is correct (1).");
is(graph.getSelection().end, 400,
"The current selection end value is correct (1).");
info("Making sure the selection updates when the window is resized");
host._window.resizeTo(250, 250);
yield graph.once("refresh");
let newBounds = host.frame.getBoundingClientRect();
is (graph._width, newBounds.width * window.devicePixelRatio,
"The window was properly resized (2).");
is (graph._height, newBounds.height * window.devicePixelRatio,
"The window was properly resized (2).");
let ratio = oldBounds.width / newBounds.width;
info("The window resize ratio is: " + ratio);
is(graph.getSelection().start, Math.round(100 / ratio),
"The current selection start value is correct (2).");
is(graph.getSelection().end, Math.round(400 / ratio),
"The current selection end value is correct (2).");
}
// EventUtils just doesn't work!
function dragStart(graph, x, y = 1) {
x /= window.devicePixelRatio;
y /= window.devicePixelRatio;
graph._onMouseMove({ testX: x, testY: y });
graph._onMouseDown({ testX: x, testY: y });
}
function dragStop(graph, x, y = 1) {
x /= window.devicePixelRatio;
y /= window.devicePixelRatio;
graph._onMouseMove({ testX: x, testY: y });
graph._onMouseUp({ testX: x, testY: y });
}

View File

@ -684,6 +684,13 @@ AbstractCanvasGraph.prototype = {
return;
}
// Handle a changed size by mapping the old selection to the new width
if (this._width && newWidth && this.hasSelection()) {
let ratio = this._width / (newWidth * this._pixelRatio);
this._selection.start = Math.round(this._selection.start / ratio);
this._selection.end = Math.round(this._selection.end / ratio);
}
bounds.width = newWidth;
bounds.height = newHeight;
this._iframe.setAttribute("width", bounds.width);

View File

@ -151,6 +151,7 @@ function CssHtmlTree(aStyleInspector, aPageStyle)
this._onClick = this._onClick.bind(this);
this._onCopy = this._onCopy.bind(this);
this._onCopyColor = this._onCopyColor.bind(this);
this._onCopyImageDataUrl = this._onCopyImageDataUrl.bind(this);
this._onFilterStyles = this._onFilterStyles.bind(this);
this._onFilterKeyPress = this._onFilterKeyPress.bind(this);
this._onClearSearch = this._onClearSearch.bind(this);
@ -328,7 +329,7 @@ CssHtmlTree.prototype = {
* - type {String} One of the VIEW_NODE_XXX_TYPE const in
* style-inspector-overlays
* - value {Object} Depends on the type of the node
* returns null of the node isn't anything we care about
* returns null if the node isn't anything we care about
*/
getNodeInfo: function(node) {
if (!node) {
@ -351,7 +352,7 @@ CssHtmlTree.prototype = {
return {
type: overlays.VIEW_NODE_SELECTOR_TYPE,
value: selectorText.trim()
}
};
}
// Walk up the nodes to find out where node is
@ -715,6 +716,13 @@ CssHtmlTree.prototype = {
command: this._onCopyColor
});
// Copy data URI
this.menuitemCopyImageDataUrl = createMenuItem(this._contextmenu, {
label: "styleinspector.contextmenu.copyImageDataUrl",
accesskey: "styleinspector.contextmenu.copyImageDataUrl.accessKey",
command: this._onCopyImageDataUrl
});
// Show Original Sources
this.menuitemSources= createMenuItem(this._contextmenu, {
label: "ruleView.contextmenu.showOrigSources",
@ -745,6 +753,7 @@ CssHtmlTree.prototype = {
this.menuitemSources.setAttribute("checked", showOrig);
this.menuitemCopyColor.hidden = !this._isColorPopup();
this.menuitemCopyImageDataUrl.hidden = !this._isImageUrlPopup();
},
/**
@ -757,14 +766,12 @@ CssHtmlTree.prototype = {
_isColorPopup: function () {
this._colorToCopy = "";
let trigger = this.popupNode;
if (!trigger) {
let container = this._getPopupNodeContainer();
if (!container) {
return false;
}
let container = (trigger.nodeType == trigger.TEXT_NODE) ?
trigger.parentElement : trigger;
let isColorNode = el => el.dataset && "color" in el.dataset;
while (!isColorNode(container)) {
@ -778,6 +785,52 @@ CssHtmlTree.prototype = {
return true;
},
/**
* Check if the context menu popup was opened with a click on an image link
* If true, save the image url to this._imageUrlToCopy
*/
_isImageUrlPopup: function () {
this._imageUrlToCopy = "";
let container = this._getPopupNodeContainer();
let isImageUrlNode = this._isImageUrlNode(container);
if (isImageUrlNode) {
this._imageUrlToCopy = container.href;
}
return isImageUrlNode;
},
/**
* Check if a node is an image url
* @param {DOMNode} node The node which we want information about
* @return {Boolean} true if the node is an image url
*/
_isImageUrlNode: function (node) {
let nodeInfo = this.getNodeInfo(node);
if (!nodeInfo) {
return false
}
return nodeInfo.type == overlays.VIEW_NODE_IMAGE_URL_TYPE;
},
/**
* Get the DOM Node container for the current popupNode.
* If popupNode is a textNode, return the parent node, otherwise return popupNode itself.
* @return {DOMNode}
*/
_getPopupNodeContainer: function () {
let container = null;
let node = this.popupNode;
if (node) {
let isTextNode = node.nodeType == node.TEXT_NODE;
container = isTextNode ? node.parentElement : node;
}
return container;
},
/**
* Context menu handler.
*/
@ -821,6 +874,22 @@ CssHtmlTree.prototype = {
clipboardHelper.copyString(this._colorToCopy);
},
/**
* Retrieve the image data for the selected image url and copy it to the clipboard
*/
_onCopyImageDataUrl: Task.async(function*() {
let message;
try {
let inspectorFront = this.inspector.inspector;
let data = yield inspectorFront.getImageDataFromURL(this._imageUrlToCopy);
message = yield data.data.string();
} catch (e) {
message = CssHtmlTree.l10n("styleinspector.copyImageDataUrlError");
}
clipboardHelper.copyString(message);
}),
/**
* Copy selected text.
*
@ -910,6 +979,10 @@ CssHtmlTree.prototype = {
this.menuitemCopyColor.removeEventListener("command", this._onCopyColor);
this.menuitemCopyColor = null;
// Destroy Copy Data URI menuitem.
this.menuitemCopyImageDataUrl.removeEventListener("command", this._onCopyImageDataUrl);
this.menuitemCopyImageDataUrl = null;
// Destroy the context menu.
this._contextmenu.removeEventListener("popupshowing", this._contextMenuUpdate);
this._contextmenu.parentNode.removeChild(this._contextmenu);

View File

@ -1123,6 +1123,7 @@ function CssRuleView(aInspector, aDoc, aStore, aPageStyle) {
this._onSelectAll = this._onSelectAll.bind(this);
this._onCopy = this._onCopy.bind(this);
this._onCopyColor = this._onCopyColor.bind(this);
this._onCopyImageDataUrl = this._onCopyImageDataUrl.bind(this);
this._onToggleOrigSources = this._onToggleOrigSources.bind(this);
this._onShowMdnDocs = this._onShowMdnDocs.bind(this);
this._onFilterStyles = this._onFilterStyles.bind(this);
@ -1214,6 +1215,11 @@ CssRuleView.prototype = {
accesskey: "ruleView.contextmenu.copyColor.accessKey",
command: this._onCopyColor
});
this.menuitemCopyImageDataUrl = createMenuItem(this._contextmenu, {
label: "styleinspector.contextmenu.copyImageDataUrl",
accesskey: "styleinspector.contextmenu.copyImageDataUrl.accessKey",
command: this._onCopyImageDataUrl
});
this.menuitemSources = createMenuItem(this._contextmenu, {
label: "ruleView.contextmenu.showOrigSources",
accesskey: "ruleView.contextmenu.showOrigSources.accessKey",
@ -1348,6 +1354,7 @@ CssRuleView.prototype = {
}
this.menuitemCopyColor.hidden = !this._isColorPopup();
this.menuitemCopyImageDataUrl.hidden = !this._isImageUrlPopup();
this.menuitemCopy.disabled = !copy;
var showOrig = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
@ -1398,7 +1405,7 @@ CssRuleView.prototype = {
pseudoElement: prop.rule.pseudoElement,
sheetHref: prop.rule.domRule.href
};
} else if (classes.contains("theme-link") && prop) {
} else if (classes.contains("theme-link") && !classes.contains("ruleview-rule-source") && prop) {
type = overlays.VIEW_NODE_IMAGE_URL_TYPE;
value = {
property: getPropertyNameAndValue(node).name,
@ -1430,14 +1437,11 @@ CssRuleView.prototype = {
_isColorPopup: function () {
this._colorToCopy = "";
let trigger = this.doc.popupNode;
if (!trigger) {
let container = this._getPopupNodeContainer();
if (!container) {
return false;
}
let container = (trigger.nodeType == trigger.TEXT_NODE) ?
trigger.parentElement : trigger;
let isColorNode = el => el.dataset && "color" in el.dataset;
while (!isColorNode(container)) {
@ -1451,6 +1455,52 @@ CssRuleView.prototype = {
return true;
},
/**
* Check if the context menu popup was opened with a click on an image link
* If true, save the image url to this._imageUrlToCopy
*/
_isImageUrlPopup: function () {
this._imageUrlToCopy = "";
let container = this._getPopupNodeContainer();
let isImageUrlNode = this._isImageUrlNode(container);
if (isImageUrlNode) {
this._imageUrlToCopy = container.href;
}
return isImageUrlNode;
},
/**
* Check if a node is an image url
* @param {DOMNode} node The node which we want information about
* @return {Boolean} true if the node is an image url
*/
_isImageUrlNode: function (node) {
let nodeInfo = this.getNodeInfo(node);
if (!nodeInfo) {
return false
}
return nodeInfo.type == overlays.VIEW_NODE_IMAGE_URL_TYPE;
},
/**
* Get the DOM Node container for the current popupNode.
* If popupNode is a textNode, return the parent node, otherwise return popupNode itself.
* @return {DOMNode}
*/
_getPopupNodeContainer: function () {
let container = null;
let node = this.doc.popupNode;
if (node) {
let isTextNode = node.nodeType == node.TEXT_NODE;
container = isTextNode ? node.parentElement : node;
}
return container;
},
/**
* Context menu handler.
*/
@ -1525,6 +1575,22 @@ CssRuleView.prototype = {
clipboardHelper.copyString(this._colorToCopy);
},
/**
* Retrieve the image data for the selected image url and copy it to the clipboard
*/
_onCopyImageDataUrl: Task.async(function*() {
let message;
try {
let inspectorFront = this.inspector.inspector;
let data = yield inspectorFront.getImageDataFromURL(this._imageUrlToCopy);
message = yield data.data.string();
} catch (e) {
message = _strings.GetStringFromName("styleinspector.copyImageDataUrlError");
}
clipboardHelper.copyString(message);
}),
/**
* Toggle the original sources pref.
*/
@ -1729,6 +1795,10 @@ CssRuleView.prototype = {
this.menuitemCopyColor.removeEventListener("command", this._onCopyColor);
this.menuitemCopyColor = null;
// Destroy Copy Data URI menuitem.
this.menuitemCopyImageDataUrl.removeEventListener("command", this._onCopyImageDataUrl);
this.menuitemCopyImageDataUrl = null;
this.menuitemSources.removeEventListener("command", this._onToggleOrigSources);
this.menuitemSources = null;

View File

@ -21,6 +21,7 @@ const {
SwatchFilterTooltip
} = require("devtools/shared/widgets/Tooltip");
const {CssLogic} = require("devtools/styleinspector/css-logic");
const EventEmitter = require("devtools/toolkit/event-emitter");
const {Promise:promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/Services.jsm");
@ -59,6 +60,8 @@ function HighlightersOverlay(view) {
// Only initialize the overlay if at least one of the highlighter types is
// supported
this.supportsHighlighters = this.highlighterUtils.supportsCustomHighlighters();
EventEmitter.decorate(this);
}
exports.HighlightersOverlay = HighlightersOverlay;
@ -124,8 +127,12 @@ HighlightersOverlay.prototype = {
if (type) {
this.highlighterShown = type;
let node = this.view.inspector.selection.nodeFront;
this._getHighlighter(type).then(highlighter => {
highlighter.show(node);
this._getHighlighter(type)
.then(highlighter => highlighter.show(node))
.then(shown => {
if (shown) {
this.emit("highlighter-shown");
}
});
}
},
@ -176,6 +183,7 @@ HighlightersOverlay.prototype = {
promise.then(null, Cu.reportError);
}
this.highlighterShown = null;
this.emit("highlighter-hidden");
});
}
},

View File

@ -151,6 +151,7 @@ skip-if = e10s # bug 1040670 Cannot open inline styles in viewSourceUtils
[browser_ruleview_user-property-reset.js]
[browser_styleinspector_context-menu-copy-color_01.js]
[browser_styleinspector_context-menu-copy-color_02.js]
[browser_styleinspector_context-menu-copy-data-uri.js]
[browser_styleinspector_csslogic-content-stylesheets.js]
[browser_styleinspector_output-parser.js]
[browser_styleinspector_refresh_when_active.js]

View File

@ -0,0 +1,99 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
const PROPERTIES_URL = "chrome://global/locale/devtools/styleinspector.properties";
const TEST_DATA_URI = "data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=";
// invalid URL still needs to be reachable otherwise getImageDataUrl will timeout.
// Reusing the properties bundle URL as a workaround
const INVALID_IMAGE_URI = PROPERTIES_URL;
const ERROR_MESSAGE = Services.strings
.createBundle(PROPERTIES_URL)
.GetStringFromName("styleinspector.copyImageDataUrlError");
add_task(function*() {
const PAGE_CONTENT = [
"<style type=\"text/css\">",
" .valid-background {",
" background-image: url(" + TEST_DATA_URI + ");",
" }",
" .invalid-background {",
" background-image: url(" + INVALID_IMAGE_URI + ");",
" }",
"</style>",
"<div class=\"valid-background\">Valid background image</div>",
"<div class=\"invalid-background\">Invalid background image</div>"
].join("\n");
yield addTab("data:text/html;charset=utf8," + encodeURIComponent(PAGE_CONTENT));
yield startTest();
});
function* startTest() {
info("Opening rule view");
let ruleViewData = yield openRuleView();
info("Test valid background image URL in rule view");
yield testCopyImageDataUrlToClipboard(ruleViewData, ".valid-background", TEST_DATA_URI);
info("Test invalid background image URL in rue view");
yield testCopyImageDataUrlToClipboard(ruleViewData, ".invalid-background", ERROR_MESSAGE);
info("Opening computed view");
let computedViewData = yield openComputedView();
info("Test valid background image URL in computed view");
yield testCopyImageDataUrlToClipboard(computedViewData, ".valid-background", TEST_DATA_URI);
info("Test invalid background image URL in computed view");
yield testCopyImageDataUrlToClipboard(computedViewData, ".invalid-background", ERROR_MESSAGE);
}
function* testCopyImageDataUrlToClipboard({view, inspector}, selector, expected) {
info("Select node in inspector panel");
yield selectNode(selector, inspector);
info("Retrieve background-image link for selected node in current styleinspector view");
let property = getBackgroundImageProperty(view, selector);
let imageLink = property.valueSpan.querySelector(".theme-link");
ok(imageLink, "Background-image link element found");
info("Simulate right click on the background-image URL");
let popup = once(view._contextmenu, "popupshown");
// Cannot rely on synthesizeMouseAtCenter here. The image URL can be displayed on several lines.
// A click simulated at the exact center may click between the lines and miss the target
// Instead, using the top-left corner of first client rect, with an offset of 2 pixels.
let rect = imageLink.getClientRects()[0];
let x = rect.left + 2;
let y = rect.top + 2;
EventUtils.synthesizeMouseAtPoint(x, y, {button: 2, type: "contextmenu"}, getViewWindow(view));
yield popup;
info("Context menu is displayed");
ok(!view.menuitemCopyImageDataUrl.hidden, "\"Copy Image Data-URL\" menu entry is displayed");
info("Click Copy Data URI and wait for clipboard");
yield waitForClipboard(() => view.menuitemCopyImageDataUrl.click(), expected);
info("Hide context menu");
view._contextmenu.hidePopup();
}
function getBackgroundImageProperty(view, selector) {
let isRuleView = view instanceof CssRuleView;
if (isRuleView) {
return getRuleViewProperty(view, selector, "background-image");
} else {
return getComputedViewProperty(view, "background-image");
}
}
/**
* Function that returns the window for a given view.
*/
function getViewWindow(view) {
let viewDocument = view.styleDocument ? view.styleDocument : view.doc;
return viewDocument.defaultView;
}

View File

@ -36,7 +36,9 @@ add_task(function*() {
info("Faking a mousemove on a transform property");
({valueSpan} = getRuleViewProperty(rView, "body", "transform"));
let onHighlighterShown = hs.once("highlighter-shown");
hs._onMouseMove({target: valueSpan});
yield onHighlighterShown;
ok(hs.promises[TYPE], "The highlighter is being initialized");
let h = yield hs.promises[TYPE];
is(h, hs.highlighters[TYPE], "The initialized highlighter is the right one");
@ -57,7 +59,9 @@ add_task(function*() {
info("Faking a mousemove on a transform property");
({valueSpan} = getComputedViewProperty(cView, "transform"));
onHighlighterShown = hs.once("highlighter-shown");
hs._onMouseMove({target: valueSpan});
yield onHighlighterShown;
ok(hs.promises[TYPE], "The highlighter is being initialized");
h = yield hs.promises[TYPE];
is(h, hs.highlighters[TYPE], "The initialized highlighter is the right one");

View File

@ -39,53 +39,64 @@ add_task(function*() {
this.nodeFront = nodeFront;
this.isShown = true;
this.nbOfTimesShown ++;
return promise.resolve(true);
},
hide: function() {
this.nodeFront = null;
this.isShown = false;
return promise.resolve();
}
};
// Inject the mock highlighter in the rule-view
rView.highlighters.promises[TYPE] = {
then: function(cb) {
cb(HighlighterFront);
}
};
let hs = rView.highlighters;
hs.promises[TYPE] = promise.resolve(HighlighterFront);
let {valueSpan} = getRuleViewProperty(rView, "body", "transform");
info("Checking that the HighlighterFront's show/hide methods are called");
rView.highlighters._onMouseMove({target: valueSpan});
let onHighlighterShown = hs.once("highlighter-shown");
hs._onMouseMove({target: valueSpan});
yield onHighlighterShown;
ok(HighlighterFront.isShown, "The highlighter is shown");
rView.highlighters._onMouseLeave();
let onHighlighterHidden = hs.once("highlighter-hidden");
hs._onMouseLeave();
yield onHighlighterHidden;
ok(!HighlighterFront.isShown, "The highlighter is hidden");
info("Checking that hovering several times over the same property doesn't" +
" show the highlighter several times");
let nb = HighlighterFront.nbOfTimesShown;
rView.highlighters._onMouseMove({target: valueSpan});
onHighlighterShown = hs.once("highlighter-shown");
hs._onMouseMove({target: valueSpan});
yield onHighlighterShown;
is(HighlighterFront.nbOfTimesShown, nb + 1, "The highlighter was shown once");
rView.highlighters._onMouseMove({target: valueSpan});
rView.highlighters._onMouseMove({target: valueSpan});
hs._onMouseMove({target: valueSpan});
hs._onMouseMove({target: valueSpan});
is(HighlighterFront.nbOfTimesShown, nb + 1,
"The highlighter was shown once, after several mousemove");
info("Checking that the right NodeFront reference is passed");
yield selectNode("html", inspector);
({valueSpan} = getRuleViewProperty(rView, "html", "transform"));
rView.highlighters._onMouseMove({target: valueSpan});
onHighlighterShown = hs.once("highlighter-shown");
hs._onMouseMove({target: valueSpan});
yield onHighlighterShown;
is(HighlighterFront.nodeFront.tagName, "HTML",
"The right NodeFront is passed to the highlighter (1)");
yield selectNode("body", inspector);
({valueSpan} = getRuleViewProperty(rView, "body", "transform"));
rView.highlighters._onMouseMove({target: valueSpan});
onHighlighterShown = hs.once("highlighter-shown");
hs._onMouseMove({target: valueSpan});
yield onHighlighterShown;
is(HighlighterFront.nodeFront.tagName, "BODY",
"The right NodeFront is passed to the highlighter (2)");
info("Checking that the highlighter gets hidden when hovering a non-transform property");
({valueSpan} = getRuleViewProperty(rView, "body", "color"));
rView.highlighters._onMouseMove({target: valueSpan});
onHighlighterHidden = hs.once("highlighter-hidden");
hs._onMouseMove({target: valueSpan});
yield onHighlighterHidden;
ok(!HighlighterFront.isShown, "The highlighter is hidden");
});

View File

@ -55,7 +55,9 @@ add_task(function*() {
info("Faking a mousemove on the now unoverriden property");
({valueSpan} = getRuleViewProperty(rView, "div", "transform"));
let onHighlighterShown = hs.once("highlighter-shown");
hs._onMouseMove({target: valueSpan});
yield onHighlighterShown;
ok(hs.promises[TYPE], "The highlighter is being initialized now");
let h = yield hs.promises[TYPE];
is(h, hs.highlighters[TYPE], "The initialized highlighter is the right one");

View File

@ -431,7 +431,7 @@ if test -z "$RUSTC" -a -n "$MOZ_RUST"; then
To compile rust language sources, you must have 'rustc' in your path.
See http://www.rust-lang.org/ for more information.])
fi
if test -n "$MOZ_RUST" -a -z "$_RUSTC_MAJOR_VERSION" -o \
if test -n "$MOZ_RUST" && test -z "$_RUSTC_MAJOR_VERSION" -o \
"$_RUSTC_MAJOR_VERSION" -lt 1; then
AC_MSG_ERROR([Rust compiler ${RUSTC_VERSION} is too old.
To compile rust language sources please install at least

View File

@ -548,6 +548,8 @@ public class BrowserApp extends GeckoApp
}
private void showBookmarkAddedToast() {
// This flow is from the option menu which has check to see if a bookmark was already added.
// So, it is safe here to show the toast that bookmark_added without any checks.
getButtonToast().show(false,
getResources().getString(R.string.bookmark_added),
ButtonToast.LENGTH_SHORT,

View File

@ -559,11 +559,12 @@ public abstract class GeckoApp
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
db.addBookmark(getContentResolver(), title, url);
final boolean bookmarkAdded = db.addBookmark(getContentResolver(), title, url);
final int resId = bookmarkAdded ? R.string.bookmark_added : R.string.bookmark_already_added;
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(context, R.string.bookmark_added, Toast.LENGTH_SHORT).show();
Toast.makeText(context, resId, Toast.LENGTH_SHORT).show();
}
});
}

View File

@ -107,7 +107,7 @@ public interface BrowserDB {
public abstract String getUrlForKeyword(ContentResolver cr, String keyword);
public abstract boolean isBookmark(ContentResolver cr, String uri);
public abstract void addBookmark(ContentResolver cr, String title, String uri);
public abstract boolean addBookmark(ContentResolver cr, String title, String uri);
public abstract Cursor getBookmarkForUrl(ContentResolver cr, String url);
public abstract void removeBookmarksWithURL(ContentResolver cr, String uri);
public abstract void registerBookmarkObserver(ContentResolver cr, ContentObserver observer);

View File

@ -939,9 +939,34 @@ public class LocalBrowserDB implements BrowserDB {
@Override
@RobocopTarget
public void addBookmark(ContentResolver cr, String title, String uri) {
public boolean addBookmark(ContentResolver cr, String title, String uri) {
long folderId = getFolderIdFromGuid(cr, Bookmarks.MOBILE_FOLDER_GUID);
if (isBookmarkForUrlInFolder(cr, uri, folderId)) {
// Bookmark added already.
return false;
}
// Add a new bookmark.
addBookmarkItem(cr, title, uri, folderId);
return true;
}
private boolean isBookmarkForUrlInFolder(ContentResolver cr, String uri, long folderId) {
final Cursor c = cr.query(bookmarksUriWithLimit(1),
new String[] { Bookmarks._ID },
Bookmarks.URL + " = ? AND " + Bookmarks.PARENT + " = ? AND " + Bookmarks.IS_DELETED + " == 0",
new String[] { uri, String.valueOf(folderId) },
Bookmarks.URL);
if (c == null) {
return false;
}
try {
return c.getCount() > 0;
} finally {
c.close();
}
}
@Override

View File

@ -250,7 +250,8 @@ public class StubBrowserDB implements BrowserDB {
}
@RobocopTarget
public void addBookmark(ContentResolver cr, String title, String uri) {
public boolean addBookmark(ContentResolver cr, String title, String uri) {
return false;
}
@RobocopTarget

View File

@ -44,6 +44,10 @@
<!ENTITY bookmark "Bookmark">
<!ENTITY bookmark_remove "Remove bookmark">
<!ENTITY bookmark_added "Bookmark added">
<!-- Localization note (bookmark_already_added) : This string is
used as a label in a toast. It is the verb "to bookmark", not
the noun "a bookmark". -->
<!ENTITY bookmark_already_added "Already bookmarked">
<!ENTITY bookmark_removed "Bookmark removed">
<!ENTITY bookmark_updated "Bookmark updated">
<!ENTITY bookmark_options "Options">

View File

@ -80,6 +80,7 @@
<string name="bookmark">&bookmark;</string>
<string name="bookmark_remove">&bookmark_remove;</string>
<string name="bookmark_added">&bookmark_added;</string>
<string name="bookmark_already_added">&bookmark_already_added;</string>
<string name="bookmark_removed">&bookmark_removed;</string>
<string name="bookmark_updated">&bookmark_updated;</string>
<string name="bookmark_options">&bookmark_options;</string>

View File

@ -50,14 +50,12 @@ class DatabaseHelper {
}
/**
* Adds a bookmark, or updates the bookmark title if the url already exists.
*
* The LocalBrowserDB.addBookmark implementation handles updating existing bookmarks.
* Adds a bookmark.
*/
protected void addOrUpdateMobileBookmark(String title, String url) {
protected void addMobileBookmark(String title, String url) {
final ContentResolver resolver = mActivity.getContentResolver();
getProfileDB().addBookmark(resolver, title, url);
mAsserter.ok(true, "Inserting/updating a new bookmark", "Inserting/updating the bookmark with the title = " + title + " and the url = " + url);
mAsserter.ok(true, "Inserting a new bookmark", "Inserting the bookmark with the title = " + title + " and the url = " + url);
}
/**

View File

@ -31,7 +31,7 @@ public class testBookmark extends AboutHomeTest {
mAsserter.ok(mDatabaseHelper.isBookmark(url), "Checking that " + url + " is bookmarked by default", url + " is bookmarked");
}
mDatabaseHelper.addOrUpdateMobileBookmark(mStringHelper.ROBOCOP_BLANK_PAGE_01_TITLE, BOOKMARK_URL);
mDatabaseHelper.addMobileBookmark(mStringHelper.ROBOCOP_BLANK_PAGE_01_TITLE, BOOKMARK_URL);
waitForBookmarked(true);
isBookmarkDisplayed(BOOKMARK_URL);

View File

@ -13,7 +13,7 @@ public class testBookmarkKeyword extends AboutHomeTest {
final String keyword = "testkeyword";
// Add a bookmark, and update it to have a keyword.
mDatabaseHelper.addOrUpdateMobileBookmark(mStringHelper.ROBOCOP_BLANK_PAGE_01_TITLE, url);
mDatabaseHelper.addMobileBookmark(mStringHelper.ROBOCOP_BLANK_PAGE_01_TITLE, url);
mDatabaseHelper.updateBookmark(url, mStringHelper.ROBOCOP_BLANK_PAGE_01_TITLE, keyword);
// Enter the keyword in the urlbar.

View File

@ -40,7 +40,7 @@ public class testBookmarklets extends AboutHomeTest {
// add the bookmarklet to the database. there's currently no way to
// add this using the UI, so we go through the content provider.
mDatabaseHelper.addOrUpdateMobileBookmark(title, js);
mDatabaseHelper.addMobileBookmark(title, js);
// Open about:home in the Bookmarks page
openAboutHomeTab(AboutHomeTabs.BOOKMARKS);

View File

@ -21,7 +21,7 @@ public class testBookmarksPanel extends AboutHomeTest {
initializeProfile();
// Add a mobile bookmark.
mDatabaseHelper.addOrUpdateMobileBookmark(mStringHelper.ROBOCOP_BLANK_PAGE_01_TITLE, BOOKMARK_URL);
mDatabaseHelper.addMobileBookmark(mStringHelper.ROBOCOP_BLANK_PAGE_01_TITLE, BOOKMARK_URL);
openAboutHomeTab(AboutHomeTabs.BOOKMARKS);

View File

@ -38,7 +38,7 @@ public class testClearPrivateData extends PixelTest {
loadUrlAndWait(blank1);
verifyUrlBarTitle(blank1);
mDatabaseHelper.addOrUpdateMobileBookmark(mStringHelper.ROBOCOP_BLANK_PAGE_02_TITLE, blank2);
mDatabaseHelper.addMobileBookmark(mStringHelper.ROBOCOP_BLANK_PAGE_02_TITLE, blank2);
// Checking that the history list is not empty
verifyHistoryCount(1);

View File

@ -113,7 +113,7 @@ public class testImportFromAndroid extends AboutHomeTest {
for (String url:androidBookmarks) {
// Add every 3rd bookmark to Firefox Mobile
if ((androidBookmarks.indexOf(url) % 3) == 0) {
mDatabaseHelper.addOrUpdateMobileBookmark("Bookmark Number" + String.valueOf(androidBookmarks.indexOf(url)), url);
mDatabaseHelper.addMobileBookmark("Bookmark Number" + String.valueOf(androidBookmarks.indexOf(url)), url);
}
}

View File

@ -23,6 +23,57 @@ const MEASURES = [
{key: "ticks", percentOfDeltaT: false, label: "Activations"},
];
/**
* Used to control the live updates in the performance page.
*/
let AutoUpdate = {
/**
* The timer that is created when setInterval is called.
*/
_timerId: null,
/**
* The dropdown DOM element.
*/
_intervalDropdown: null,
/**
* Starts updating the performance data if the updates are paused.
*/
start: function () {
if (AutoUpdate._intervalDropdown == null){
AutoUpdate._intervalDropdown = document.getElementById("intervalDropdown");
}
if (AutoUpdate._timerId == null) {
let dropdownIndex = AutoUpdate._intervalDropdown.selectedIndex;
let dropdownValue = AutoUpdate._intervalDropdown.options[dropdownIndex].value;
AutoUpdate._timerId = window.setInterval(update, dropdownValue);
}
},
/**
* Stops the updates if the data is updating.
*/
stop: function () {
if (AutoUpdate._timerId == null) {
return;
}
clearInterval(AutoUpdate._timerId);
AutoUpdate._timerId = null;
},
/**
* Updates the refresh interval when the dropdown selection is changed.
*/
updateRefreshRate: function () {
AutoUpdate.stop();
AutoUpdate.start();
}
};
let State = {
/**
* @type{PerformanceData}
@ -50,7 +101,7 @@ let State = {
* - `deltaT`: the number of milliseconds elapsed since the data
* was last displayed.
*/
update: function() {
update: function () {
let snapshot = PerformanceStats.getSnapshot();
let newData = new Map();
let deltas = [];
@ -237,9 +288,11 @@ function updateLiveData() {
function go() {
// Compute initial state immediately, then wait a little
// before we start computing diffs and refreshing.
State.update();
document.getElementById("playButton").addEventListener("click", () => AutoUpdate.start());
document.getElementById("pauseButton").addEventListener("click", () => AutoUpdate.stop());
window.setTimeout(() => {
window.setInterval(update, 10000);
}, 1000);
document.getElementById("intervalDropdown").addEventListener("change", () => AutoUpdate.updateRefreshRate());
State.update();
setTimeout(update, 1000);
}

View File

@ -87,6 +87,16 @@
<body onload="go()">
<h1>Performance monitor</h1>
<input type="button" id="playButton" value="Play" />
<input type="button" id="pauseButton" value="Pause" />
<select id="intervalDropdown">
<option value="500">0.5 seconds</option>
<option value="1000">1 second</option>
<option value="2000">2 seconds</option>
<option value="5000" selected="selected">5 seconds</option>
<option value="10000">10 seconds</option>
</select>
<table id="liveData">
</table>

View File

@ -120,7 +120,7 @@ exports.zip = function zip(a, b) {
*/
exports.executeSoon = function executeSoon(aFn) {
if (isWorker) {
require("Timer").setTimeout(aFn, 0);
setImmediate(aFn);
} else {
Services.tm.mainThread.dispatch({
run: exports.makeInfallible(aFn)

View File

@ -7,7 +7,7 @@
const {Cu, Cc, Ci} = require("chrome");
const Services = require("Services");
const protocol = require("devtools/server/protocol");
const {Arg, Option, method} = protocol;
const {Arg, Option, method, RetVal} = protocol;
const events = require("sdk/event/core");
const Heritage = require("sdk/core/heritage");
const {CssLogic} = require("devtools/styleinspector/css-logic");
@ -455,17 +455,21 @@ let CustomHighlighterActor = exports.CustomHighlighterActor = protocol.ActorClas
*
* @param NodeActor The node to be highlighted
* @param Object Options for the custom highlighter
* @return Boolean True, if the highlighter has been successfully shown (FF41+)
*/
show: method(function(node, options) {
if (!node || !isNodeValid(node.rawNode) || !this._highlighter) {
return;
return false;
}
this._highlighter.show(node.rawNode, options);
return this._highlighter.show(node.rawNode, options);
}, {
request: {
node: Arg(0, "domnode"),
options: Arg(1, "nullable:json")
},
response: {
value: RetVal("nullable:boolean")
}
}),
@ -849,7 +853,7 @@ AutoRefreshHighlighter.prototype = {
let isSameOptions = this._isSameOptions(options);
if (!isNodeValid(node) || (isSameNode && isSameOptions)) {
return;
return false;
}
this.options = options;
@ -858,9 +862,12 @@ AutoRefreshHighlighter.prototype = {
this.currentNode = node;
this._updateAdjustedQuads();
this._startRefreshLoop();
this._show();
let shown = this._show();
if (shown) {
this.emit("shown");
}
return shown;
},
/**
@ -942,6 +949,7 @@ AutoRefreshHighlighter.prototype = {
// To be implemented by sub classes
// When called, sub classes should actually show the highlighter for
// this.currentNode, potentially using options in this.options
throw new Error("Custom highlighter class had to implement _show method");
},
_update: function() {
@ -949,11 +957,13 @@ AutoRefreshHighlighter.prototype = {
// When called, sub classes should update the highlighter shown for
// this.currentNode
// This is called as a result of a page scroll, zoom or repaint
throw new Error("Custom highlighter class had to implement _update method");
},
_hide: function() {
// To be implemented by sub classes
// When called, sub classes should actually hide the highlighter
throw new Error("Custom highlighter class had to implement _hide method");
},
_startRefreshLoop: function() {
@ -1229,9 +1239,10 @@ BoxModelHighlighter.prototype = Heritage.extend(AutoRefreshHighlighter.prototype
this.options.region = "content";
}
this._update();
let shown = this._update();
this._trackMutations();
this.emit("ready");
return shown;
},
/**
@ -1259,6 +1270,7 @@ BoxModelHighlighter.prototype = Heritage.extend(AutoRefreshHighlighter.prototype
* Should be called whenever node size or attributes change
*/
_update: function() {
let shown = false;
setIgnoreLayoutChanges(true);
if (this._updateBoxModel()) {
@ -1268,12 +1280,15 @@ BoxModelHighlighter.prototype = Heritage.extend(AutoRefreshHighlighter.prototype
this._hideInfobar();
}
this._showBoxModel();
shown = true;
} else {
// Nothing to highlight (0px rectangle like a <script> tag for instance)
this._hide();
}
setIgnoreLayoutChanges(false, this.currentNode.ownerDocument.documentElement);
return shown;
},
/**
@ -1775,15 +1790,14 @@ CssTransformHighlighter.prototype = Heritage.extend(AutoRefreshHighlighter.proto
/**
* Show the highlighter on a given node
* @param {DOMNode} node
*/
_show: function() {
if (!this._isTransformed(this.currentNode)) {
this.hide();
return;
return false;
}
this._update();
return this._update();
},
/**
@ -1830,7 +1844,7 @@ CssTransformHighlighter.prototype = Heritage.extend(AutoRefreshHighlighter.proto
if (!quads.length ||
quads[0].bounds.width <= 0 || quads[0].bounds.height <= 0) {
this._hideShapes();
return null;
return false;
}
let [quad] = quads;
@ -1850,6 +1864,7 @@ CssTransformHighlighter.prototype = Heritage.extend(AutoRefreshHighlighter.proto
this._showShapes();
setIgnoreLayoutChanges(false, this.currentNode.ownerDocument.documentElement);
return true;
},
/**
@ -1898,7 +1913,7 @@ SelectorHighlighter.prototype = {
this.hide();
if (!isNodeValid(node) || !options.selector) {
return;
return false;
}
let nodes = [];
@ -1922,6 +1937,8 @@ SelectorHighlighter.prototype = {
this._highlighters.push(highlighter);
i ++;
}
return true;
},
hide: function() {
@ -1998,7 +2015,7 @@ RectHighlighter.prototype = {
show: function(node, options) {
if (!this._hasValidOptions(options) || !node || !node.ownerDocument) {
this.hide();
return;
return false;
}
let contextNode = node.ownerDocument.documentElement;
@ -2007,7 +2024,7 @@ RectHighlighter.prototype = {
let quads = this.layoutHelpers.getAdjustedQuads(contextNode);
if (!quads.length) {
this.hide();
return;
return false;
}
let {bounds} = quads[0];
@ -2025,6 +2042,8 @@ RectHighlighter.prototype = {
let rect = this.getElement("highlighted-rect");
rect.setAttribute("style", style);
rect.removeAttribute("hidden");
return true;
},
hide: function() {
@ -2366,13 +2385,15 @@ GeometryEditorHighlighter.prototype = Heritage.extend(AutoRefreshHighlighter.pro
// XXX: sticky positioning is ignored for now. To be implemented next.
if (pos === "sticky") {
this.hide();
return;
return false;
}
let hasUpdated = this._update();
if (!hasUpdated) {
this.hide();
return false;
}
return true;
},
_update: function() {
@ -2832,6 +2853,7 @@ RulersHighlighter.prototype = {
show: function() {
this.markup.removeAttributeForElement(this.ID_CLASS_PREFIX + "elements",
"hidden");
return true;
},
hide: function() {
@ -2874,6 +2896,7 @@ SimpleOutlineHighlighter.prototype = {
installHelperSheet(getWindow(node), SIMPLE_OUTLINE_SHEET);
DOMUtils.addPseudoClassLock(node, HIGHLIGHTED_PSEUDO_CLASS);
}
return true;
},
/**

View File

@ -13,7 +13,7 @@
// interchangable on the main thread, making them easier to test.
//
// On the worker thread, some of these modules, in particular those that rely on
// the use of Components and for which the worker debugger doesn't provide an
// the use of Components, and for which the worker debugger doesn't provide an
// alternative API, will be replaced by vacuous objects. Consequently, they can
// still be required, but any attempts to use them will lead to an exception.
@ -22,18 +22,37 @@ this.EXPORTED_SYMBOLS = ["WorkerDebuggerLoader", "worker"];
// Some notes on module ids and URLs:
//
// An id is either a relative id or an absolute id. An id is relative if and
// only if it starts with a dot. An absolute id is a normalized id if and only if
// it contains no redundant components. Every normalized id is a URL.
// only if it starts with a dot. An absolute id is a normalized id if and only
// if it contains no redundant components.
//
// A URL is either an absolute URL or a relative URL. A URL is absolute if and
// only if it starts with a scheme name followed by a colon and 2 slashes.
// Every normalized id is a URL. A URL is either an absolute URL or a relative
// URL. A URL is absolute if and only if it starts with a scheme name followed
// by a colon and 2 or 3 slashes.
// Resolve the given relative id to an absolute id.
/**
* Convert the given relative id to an absolute id.
*
* @param String id
* The relative id to be resolved.
* @param String baseId
* The absolute base id to resolve the relative id against.
*
* @return String
* An absolute id
*/
function resolveId(id, baseId) {
return baseId + "/../" + id;
};
// Convert the given absolute id to a normalized id.
/**
* Convert the given absolute id to a normalized id.
*
* @param String id
* The absolute id to be normalized.
*
* @return String
* A normalized id.
*/
function normalizeId(id) {
// An id consists of an optional root and a path. A root consists of either
// a scheme name followed by 2 or 3 slashes, or a single slash. Slashes in the
@ -49,8 +68,7 @@ function normalizeId(id) {
case "..":
if (stack.length === 0) {
if (root !== undefined) {
throw new Error("can't convert absolute id " + id + " to " +
"normalized id because it's going past root!");
throw new Error("Can't normalize absolute id '" + id + "'!");
} else {
stack.push("..");
}
@ -71,7 +89,15 @@ function normalizeId(id) {
return (root ? root : "") + stack.join("/");
}
// Create a module object with the given id.
/**
* Create a module object with the given normalized id.
*
* @param String
* The normalized id of the module to be created.
*
* @return Object
* A module with the given id.
*/
function createModule(id) {
return Object.create(null, {
// CommonJS specifies the id property to be non-configurable and
@ -94,31 +120,42 @@ function createModule(id) {
});
};
// Create a CommonJS loader with the following options:
// - createSandbox:
// A function that will be used to create sandboxes. It takes the name and
// prototype of the sandbox to be created, and should return the newly
// created sandbox as result. This option is mandatory.
// - globals:
// A map of built-in globals that will be exposed to every module. Defaults
// to the empty map.
// - loadInSandbox:
// A function that will be used to load scripts in sandboxes. It takes the
// URL from which and the sandbox in which the script is to be loaded, and
// should not return a result. This option is mandatory.
// - modules:
// A map of built-in modules that will be added to the module cache.
// Defaults to the empty map.
// - paths:
// A map of paths to base URLs that will be used to resolve relative URLs to
// absolute URLS. Defaults to the empty map.
// - resolve:
// A function that will be used to resolve relative ids to absolute ids. It
// takes the relative id of the module to be required and the normalized id
// of the requiring module as arguments, and should return the absolute id
// of the module to be required as result. Defaults to resolveId above.
/**
* Create a CommonJS loader with the following options:
* - createSandbox:
* A function that will be used to create sandboxes. It should take the name
* and prototype of the sandbox to be created, and return the newly created
* sandbox as result. This option is required.
* - globals:
* A map of names to built-in globals that will be exposed to every module.
* Defaults to the empty map.
* - loadSubScript:
* A function that will be used to load scripts in sandboxes. It should take
* the URL from and the sandbox in which the script is to be loaded, and not
* return a result. This option is required.
* - modules:
* A map from normalized ids to built-in modules that will be added to the
* module cache. Defaults to the empty map.
* - paths:
* A map of paths to base URLs that will be used to resolve relative URLs to
* absolute URLS. Defaults to the empty map.
* - resolve:
* A function that will be used to resolve relative ids to absolute ids. It
* should take the relative id of a module to be required and the absolute
* id of the requiring module as arguments, and return the absolute id of
* the module to be required as result. Defaults to resolveId above.
*/
function WorkerDebuggerLoader(options) {
// Resolve the given relative URL to an absolute URL.
/**
* Convert the given relative URL to an absolute URL, using the map of paths
* given below.
*
* @param String url
* The relative URL to be resolved.
*
* @return String
* An absolute URL.
*/
function resolveURL(url) {
let found = false;
for (let [path, baseURL] of paths) {
@ -129,19 +166,25 @@ function WorkerDebuggerLoader(options) {
}
}
if (!found) {
throw new Error("can't resolve relative URL " + url + " to absolute " +
"URL!");
throw new Error("Can't resolve relative URL '" + url + "'!");
}
// If the url has no extension, use ".js" by default.
return url.endsWith(".js") ? url : url + ".js";
}
// Load the given module with the given url.
/**
* Load the given module with the given url.
*
* @param Object module
* The module object to be loaded.
* @param String url
* The URL to load the module from.
*/
function loadModule(module, url) {
// CommonJS specifies 3 free variables named require, exports, and module,
// that must be exposed to every module, so define these as properties on
// the sandbox prototype. Additional built-in globals are exposed by making
// CommonJS specifies 3 free variables: require, exports, and module. These
// must be exposed to every module, so define these as properties on the
// sandbox prototype. Additional built-in globals are exposed by making
// the map of built-in globals the prototype of the sandbox prototype.
let prototype = Object.create(globals);
prototype.Components = {};
@ -151,29 +194,37 @@ function WorkerDebuggerLoader(options) {
let sandbox = createSandbox(url, prototype);
try {
loadInSandbox(url, sandbox);
loadSubScript(url, sandbox);
} catch (error) {
if (/^Error opening input stream/.test(String(error))) {
throw new Error("can't load module " + module.id + " with url " + url +
"!");
throw new Error("Can't load module '" + module.id + "' with url '" +
url + "'!");
}
throw error;
}
// The value of exports may have been changed by the module script, so only
// freeze it if it is still an object.
// The value of exports may have been changed by the module script, so
// freeze it if and only if it is still an object.
if (typeof module.exports === "object" && module.exports !== null) {
Object.freeze(module.exports);
}
};
// Create a require function for the given requirer. If no requirer is given,
// create a require function for top-level modules instead.
/**
* Create a require function for the given module. If no module is given,
* create a require function for the top-level module instead.
*
* @param Object requirer
* The module for which the require function is to be created.
*
* @return Function
* A require function for the given module.
*/
function createRequire(requirer) {
return function require(id) {
// Make sure an id was passed.
if (id === undefined) {
throw new Error("can't require module without id!");
throw new Error("Can't require module without id!");
}
// Built-in modules are cached by id rather than URL, so try to find the
@ -183,11 +234,11 @@ function WorkerDebuggerLoader(options) {
// Failed to find the module to be required by id, so convert the id to
// a URL and try again.
// If the id is relative, resolve it to an absolute id.
// If the id is relative, convert it to an absolute id.
if (id.startsWith(".")) {
if (requirer === undefined) {
throw new Error("can't require top-level module with relative id " +
id + "!");
throw new Error("Can't require top-level module with relative id " +
"'" + id + "'!");
}
id = resolve(id, requirer.id);
}
@ -207,7 +258,7 @@ function WorkerDebuggerLoader(options) {
module = modules[url];
if (module === undefined) {
// Failed to find the module to be required in the cache, so create
// a new module, load it with the given URL, and add it to the cache.
// a new module, load it from the given URL, and add it to the cache.
// Add modules to the cache early so that any recursive calls to
// require for the same module will return the partially-loaded module
@ -219,7 +270,7 @@ function WorkerDebuggerLoader(options) {
} catch (error) {
// If the module failed to load, remove it from the cache so that
// subsequent calls to require for the same module will trigger a
// new load instead of returning a partially-loaded module from
// new load, instead of returning a partially-loaded module from
// the cache.
delete modules[url];
throw error;
@ -235,11 +286,11 @@ function WorkerDebuggerLoader(options) {
let createSandbox = options.createSandbox;
let globals = options.globals || Object.create(null);
let loadInSandbox = options.loadInSandbox;
let loadSubScript = options.loadSubScript;
// Create the module cache by converting each entry in the map of built-in
// modules to a module object with its exports property set to a frozen
// version of the original entry.
// Create the module cache, by converting each entry in the map from
// normalized ids to built-in modules to a module object, with the exports
// property of each module set to a frozen version of the original entry.
let modules = options.modules || {};
for (let id in modules) {
let module = createModule(id);
@ -247,10 +298,10 @@ function WorkerDebuggerLoader(options) {
modules[id] = module;
}
// Convert the map of paths to baseURLs into an array for use by resolveURL.
// The array is sorted from longest to shortest path so the longest path will
// always be the first to be found.
let paths = options.paths || {};
// Convert the map of paths to base URLs into an array for use by resolveURL.
// The array is sorted from longest to shortest path to ensure that the
// longest path is always the first to be found.
let paths = options.paths || Object.create(null);
paths = Object.keys(paths)
.sort((a, b) => b.length - a.length)
.map(path => [path, paths[path]]);
@ -262,20 +313,89 @@ function WorkerDebuggerLoader(options) {
this.WorkerDebuggerLoader = WorkerDebuggerLoader;
const chrome = { CC: undefined, Cc: undefined, ChromeWorker: undefined,
Cm: undefined, Ci: undefined, Cu: undefined,
Cr: undefined, components: undefined };
// The following APIs rely on the use of Components, and the worker debugger
// does not provide alternative definitions for them. Consequently, they are
// stubbed out both on the main thread and worker threads.
// The default instance of the worker debugger loader is defined differently
// depending on whether it is loaded from the main thread or a worker thread.
if (typeof Components === "object") {
const { Constructor: CC, classes: Cc, manager: Cm, interfaces: Ci,
results: Cr, utils: Cu } = Components;
let PromiseDebugging = {
getState: function () {
throw new Error("PromiseDebugging is not available in workers!");
}
};
const principal = CC('@mozilla.org/systemprincipal;1', 'nsIPrincipal')();
let chrome = {
CC: undefined,
Cc: undefined,
ChromeWorker: undefined,
Cm: undefined,
Ci: undefined,
Cu: undefined,
Cr: undefined,
components: undefined
};
// Create a sandbox with the given name and prototype.
const createSandbox = function (name, prototype) {
let loader = {
lazyGetter: function (object, name, lambda) {
Object.defineProperty(object, name, {
get: function () {
delete object[name];
return object[name] = lambda.apply(object);
},
configurable: true,
enumerable: true
});
},
lazyImporter: function () {
throw new Error("Can't import JSM from worker thread!");
},
lazyServiceGetter: function () {
throw new Error("Can't import XPCOM service from worker thread!");
},
lazyRequireGetter: function (obj, property, module, destructure) {
Object.defineProperty(obj, property, {
get: () => destructure ? worker.require(module)[property]
: worker.require(module || property)
});
}
};
// The following APIs are defined differently depending on whether we are on the
// main thread or a worker thread. On the main thread, we use the Components
// object to implement them. On worker threads, we use the APIs provided by
// the worker debugger.
let {
Debugger,
createSandbox,
dump,
loadSubScript,
reportError,
setImmediate,
xpcInspector
} = (function () {
if (typeof Components === "object") { // Main thread
let {
Constructor: CC,
classes: Cc,
manager: Cm,
interfaces: Ci,
results: Cr,
utils: Cu
} = Components;
let principal = CC('@mozilla.org/systemprincipal;1', 'nsIPrincipal')();
// To ensure that the this passed to addDebuggerToGlobal is a global, the
// Debugger object needs to be defined in a sandbox.
let sandbox = Cu.Sandbox(principal, {});
Cu.evalInSandbox(
"Components.utils.import('resource://gre/modules/jsdebugger.jsm');" +
"addDebuggerToGlobal(this);",
sandbox
);
let Debugger = sandbox.Debugger;
let createSandbox = function(name, prototype) {
return Cu.Sandbox(principal, {
invisibleToDebugger: true,
sandboxName: name,
@ -285,72 +405,65 @@ if (typeof Components === "object") {
});
};
const loadSubScript = Cc['@mozilla.org/moz/jssubscript-loader;1'].
getService(Ci.mozIJSSubScriptLoader).loadSubScript;
let subScriptLoader = Cc['@mozilla.org/moz/jssubscript-loader;1'].
getService(Ci.mozIJSSubScriptLoader);
// Load a script from the given URL in the given sandbox.
const loadInSandbox = function (url, sandbox) {
loadSubScript(url, sandbox, "UTF-8");
let loadSubScript = function (url, sandbox) {
subScriptLoader.loadSubScript(url, sandbox, "UTF-8");
};
// Define the Debugger object in a sandbox to ensure that the this passed to
// addDebuggerToGlobal is a global.
let sandbox = Cu.Sandbox(principal, {});
Cu.evalInSandbox(
"Components.utils.import('resource://gre/modules/jsdebugger.jsm');" +
"addDebuggerToGlobal(this);",
sandbox
);
const Debugger = sandbox.Debugger;
let reportError = Cu.reportError;
const Timer = Cu.import("resource://gre/modules/Timer.jsm", {});
const xpcInspector = Cc["@mozilla.org/jsinspector;1"].
let Timer = Cu.import("resource://gre/modules/Timer.jsm", {});
let setImmediate = function (callback) {
Timer.setTimeout(callback, 0);
}
let xpcInspector = Cc["@mozilla.org/jsinspector;1"].
getService(Ci.nsIJSInspector);
let worker = this.worker = new WorkerDebuggerLoader({
return {
Debugger,
createSandbox,
dump,
loadSubScript,
reportError,
setImmediate,
xpcInspector
};
} else { // Worker thread
throw new Error("Not yet implemented!");
}
}).call(this);
// Create the default instance of the worker loader, using the APIs we defined
// above.
this.worker = new WorkerDebuggerLoader({
createSandbox: createSandbox,
globals: {
"isWorker": true,
"reportError": Cu.reportError,
"loader": {
lazyGetter: function (aObject, aName, aLambda) {
Object.defineProperty(aObject, aName, {
get: function () {
delete aObject[aName];
return aObject[aName] = aLambda.apply(aObject);
"dump": dump,
"loader": loader,
"reportError": reportError,
"setImmediate": setImmediate
},
configurable: true,
enumerable: true
});
},
lazyImporter: function () { throw new Error("Can't import JSM from worker debugger server") },
lazyServiceGetter: function () { throw new Error("Can't import XPCOM from worker debugger server") },
lazyRequireGetter: function (obj, property, module, destructure) {
Object.defineProperty(obj, property, {
get: () => destructure
? worker.require(module)[property]
: worker.require(module || property)
});
}
}
},
loadInSandbox: loadInSandbox,
loadSubScript: loadSubScript,
modules: {
"Services": {},
"chrome": chrome,
"promise": Promise,
"Debugger": Debugger,
"xpcInspector": xpcInspector,
"Timer": Object.create(Timer),
"PromiseDebugging": PromiseDebugging
"PromiseDebugging": PromiseDebugging,
"Services": Object.create(null),
"chrome": chrome,
"xpcInspector": xpcInspector
},
paths: {
"": "resource://gre/modules/commonjs/",
"devtools": "resource:///modules/devtools",
"devtools/server": "resource://gre/modules/devtools/server",
"devtools/toolkit": "resource://gre/modules/devtools",
"promise": "resource://gre/modules/Promise-backend.js",
"source-map": "resource://gre/modules/devtools/source-map",
"xpcshell-test": "resource://test",
"xpcshell-test": "resource://test"
}
});
}
});

View File

@ -85,7 +85,7 @@ ruleView.contextmenu.selectAll.accessKey=A
ruleView.contextmenu.copy=Copy
# LOCALIZATION NOTE (ruleView.contextmenu.copy.accessKey): Access key for
# the rule view context menu "Select all" entry.
# the rule view context menu "Copy" entry.
ruleView.contextmenu.copy.accessKey=C
# LOCALIZATION NOTE (ruleView.contextmenu.copyColor): Text displayed in the rule
@ -96,6 +96,20 @@ ruleView.contextmenu.copyColor=Copy Color
# the rule and computed view context menu "Copy Color" entry.
ruleView.contextmenu.copyColor.accessKey=L
# LOCALIZATION NOTE (styleinspector.contextmenu.copyImageDataUrl): In rule and computed view :
# text displayed in the context menu for an image URL.
# Clicking it copies the image as Data-URL to the clipboard of the user.
styleinspector.contextmenu.copyImageDataUrl=Copy Image Data-URL
# LOCALIZATION NOTE (styleinspector.contextmenu.copyDataUri.accessKey): Access key for
# the rule and computed view context menu "Copy Image Data-URL" entry.
styleinspector.contextmenu.copyImageDataUrl.accessKey=U
# LOCALIZATION NOTE (styleinspector.copyDataUriError): Text set in the clipboard
# if an error occurs when using the copyImageDataUrl context menu action
# (invalid image link, timeout, etc...)
styleinspector.copyImageDataUrlError=Failed to copy image Data-URL
# LOCALIZATION NOTE (ruleView.contextmenu.showOrigSources): Text displayed in the rule view
# context menu.
ruleView.contextmenu.showOrigSources=Show original sources
@ -134,5 +148,5 @@ computedView.contextmenu.selectAll.accessKey=A
computedView.contextmenu.copy=Copy
# LOCALIZATION NOTE (computedView.contextmenu.copy.accessKey): Access key for
# the computed view context menu "Select all" entry.
# the computed view context menu "Copy" entry.
computedView.contextmenu.copy.accessKey=C

View File

@ -22,8 +22,16 @@
////////////////////////////////////////////////////////////////////////////////
//// Globals
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
// Do not load the FinalizationWitnessService is we are being required as a
// CommonJS module, because the Components object is not available in workers.
if (!isWorker) {
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "FinalizationWitnessService",
"@mozilla.org/toolkit/finalizationwitness;1",
"nsIFinalizationWitnessService");
}
const STATUS_PENDING = 0;
const STATUS_RESOLVED = 1;
@ -38,7 +46,7 @@ const salt = Math.floor(Math.random() * 100);
const N_INTERNALS = "{private:internals:" + salt + "}";
// We use DOM Promise for scheduling the walker loop.
const DOMPromise = Promise;
const DOMPromise = isWorker ? null : Promise;
/////// Warn-upon-finalization mechanism
//
@ -71,10 +79,6 @@ const DOMPromise = Promise;
// In this snippet, the error is reported both by p1 and by p2.
//
XPCOMUtils.defineLazyServiceGetter(this, "FinalizationWitnessService",
"@mozilla.org/toolkit/finalizationwitness;1",
"nsIFinalizationWitnessService");
let PendingErrors = {
// An internal counter, used to generate unique id.
_counter: 0,
@ -247,7 +251,13 @@ let PendingErrors = {
this._observers.clear();
}
};
PendingErrors.init();
// Do not initialize the warn-on-finalization mechanism if we are being required
// as a CommonJS module by the worker loader, because the Components object (and
// therefore the FinalizationWitnessService) is not available.
if (!isWorker) {
PendingErrors.init();
}
// Default mechanism for displaying errors
PendingErrors.addObserver(function(details) {
@ -618,6 +628,12 @@ Object.freeze(Promise.Debugging);
Object.freeze(Promise);
// Make sure to export the Promise object if we are being required as a CommonJS
// module by the worker loader.
if (isWorker) {
module.exports = Promise;
}
////////////////////////////////////////////////////////////////////////////////
//// PromiseWalker
@ -669,7 +685,7 @@ this.PromiseWalker = {
aPromise[N_INTERNALS].value = aValue;
if (aPromise[N_INTERNALS].handlers.length > 0) {
this.schedulePromise(aPromise);
} else if (aStatus == STATUS_REJECTED) {
} else if (!isWorker && aStatus == STATUS_REJECTED) {
// This is a rejection and the promise is the last in the chain.
// For the time being we therefore have an uncaught error.
let id = PendingErrors.register(aValue);
@ -685,7 +701,11 @@ this.PromiseWalker = {
scheduleWalkerLoop: function()
{
this.walkerLoopScheduled = true;
if (isWorker) {
setImmediate(this.walkerLoop);
} else {
DOMPromise.resolve().then(() => this.walkerLoop());
}
},
/**

View File

@ -96,6 +96,17 @@ this.Ci = Components.interfaces;
this.Cu = Components.utils;
this.Cr = Components.results;
// Promise-backend.js can either be loaded as a subscript by this file, or
// required as a CommonJS module by the worker loader. Because certain APIS (in
// particular, Components) are not available in workers, Promise-backend.js
// behaves slightly different in the latter case.
//
// To distinguish between these two cases, the worker loader defines a global
// variable isWorker, and sets it to true. When loading Promise-backend.js as
// a subscript, we need to make sure this variable is defined as well, and set
// it to false.
this.isWorker = false;
this.Cc["@mozilla.org/moz/jssubscript-loader;1"]
.getService(this.Ci.mozIJSSubScriptLoader)
.loadSubScript("resource://gre/modules/Promise-backend.js", this);