Merge f-t to m-c

This commit is contained in:
Phil Ringnalda 2014-03-30 16:31:41 -07:00
commit 38bc15c969
89 changed files with 6238 additions and 250 deletions

View File

@ -1205,6 +1205,9 @@ pref("devtools.styleeditor.autocompletion-enabled", true);
// Enable the Shader Editor.
pref("devtools.shadereditor.enabled", false);
// Enable the Canvas Debugger.
pref("devtools.canvasdebugger.enabled", false);
// Enable tools for Chrome development.
pref("devtools.chrome.enabled", false);

View File

@ -88,6 +88,7 @@ skip-if = true # Bug 921984, hopefully fixed by bug 930202
[browser_tabview_bug626791.js]
[browser_tabview_bug627736.js]
[browser_tabview_bug628061.js]
skip-if = os == 'linux'&&debug # bug 989083
[browser_tabview_bug628165.js]
[browser_tabview_bug628270.js]
[browser_tabview_bug628887.js]
@ -118,6 +119,7 @@ skip-if = true # Bug 752862
[browser_tabview_bug649307.js]
[browser_tabview_bug649319.js]
[browser_tabview_bug650280_perwindowpb.js]
skip-if = os == 'linux'&&debug # bug 989083
[browser_tabview_bug650573.js]
[browser_tabview_bug651311.js]
[browser_tabview_bug654295.js]

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,131 @@
<?xml version="1.0"?>
<!-- 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/. -->
<?xml-stylesheet href="chrome://browser/skin/" type="text/css"?>
<?xml-stylesheet href="chrome://browser/content/devtools/widgets.css" type="text/css"?>
<?xml-stylesheet href="chrome://browser/skin/devtools/common.css" type="text/css"?>
<?xml-stylesheet href="chrome://browser/skin/devtools/widgets.css" type="text/css"?>
<?xml-stylesheet href="chrome://browser/skin/devtools/canvasdebugger.css" type="text/css"?>
<!DOCTYPE window [
<!ENTITY % canvasDebuggerDTD SYSTEM "chrome://browser/locale/devtools/canvasdebugger.dtd">
%canvasDebuggerDTD;
]>
<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<script src="chrome://browser/content/devtools/theme-switching.js"/>
<script type="application/javascript" src="canvasdebugger.js"/>
<hbox class="theme-body" flex="1">
<vbox id="snapshots-pane">
<toolbar id="snapshots-toolbar"
class="devtools-toolbar">
<hbox id="snapshots-controls"
class="devtools-toolbarbutton-group">
<toolbarbutton id="record-snapshot"
class="devtools-toolbarbutton"
oncommand="SnapshotsListView._onRecordButtonClick()"
tooltiptext="&canvasDebuggerUI.recordSnapshot.tooltip;"
hidden="true"/>
<toolbarbutton id="import-snapshot"
class="devtools-toolbarbutton"
oncommand="SnapshotsListView._onImportButtonClick()"
label="&canvasDebuggerUI.importSnapshot;"/>
<toolbarbutton id="clear-snapshots"
class="devtools-toolbarbutton"
oncommand="SnapshotsListView._onClearButtonClick()"
label="&canvasDebuggerUI.clearSnapshots;"/>
</hbox>
</toolbar>
<vbox id="snapshots-list" flex="1"/>
</vbox>
<vbox id="debugging-pane" flex="1">
<hbox id="reload-notice"
class="notice-container"
align="center"
pack="center"
flex="1">
<button id="reload-notice-button"
class="devtools-toolbarbutton"
label="&canvasDebuggerUI.reloadNotice1;"
oncommand="gFront.setup({ reload: true })"/>
<label id="reload-notice-label"
class="plain"
value="&canvasDebuggerUI.reloadNotice2;"/>
</hbox>
<hbox id="empty-notice"
class="notice-container"
align="center"
pack="center"
flex="1"
hidden="true">
<label value="&canvasDebuggerUI.emptyNotice1;"/>
<button id="canvas-debugging-empty-notice-button"
class="devtools-toolbarbutton"
oncommand="SnapshotsListView._onRecordButtonClick()"/>
<label value="&canvasDebuggerUI.emptyNotice2;"/>
</hbox>
<hbox id="import-notice"
class="notice-container"
align="center"
pack="center"
flex="1"
hidden="true">
<label value="&canvasDebuggerUI.importNotice;"/>
</hbox>
<box id="debugging-pane-contents"
class="devtools-responsive-container"
flex="1"
hidden="true">
<vbox id="calls-list-container" flex="1">
<toolbar id="debugging-toolbar"
class="devtools-toolbar">
<hbox id="debugging-controls"
class="devtools-toolbarbutton-group">
<toolbarbutton id="resume"
class="devtools-toolbarbutton"
oncommand="CallsListView._onResume()"/>
<toolbarbutton id="step-over"
class="devtools-toolbarbutton"
oncommand="CallsListView._onStepOver()"/>
<toolbarbutton id="step-in"
class="devtools-toolbarbutton"
oncommand="CallsListView._onStepIn()"/>
<toolbarbutton id="step-out"
class="devtools-toolbarbutton"
oncommand="CallsListView._onStepOut()"/>
</hbox>
<toolbarbutton id="debugging-toolbar-sizer-button"
class="devtools-toolbarbutton"
label=""/>
<scale id="calls-slider"
movetoclick="true"
flex="100"/>
<textbox id="calls-searchbox"
class="devtools-searchinput"
placeholder="&canvasDebuggerUI.searchboxPlaceholder;"
type="search"
flex="1"/>
</toolbar>
<vbox id="calls-list" flex="1"/>
</vbox>
<splitter class="devtools-side-splitter"/>
<vbox id="screenshot-container"
hidden="true">
<vbox id="screenshot-image" flex="1"/>
<label id="screenshot-dimensions" class="plain"/>
</vbox>
</box>
<hbox id="snapshot-filmstrip"
hidden="true"/>
</vbox>
</hbox>
</window>

View File

@ -0,0 +1,12 @@
# vim: set filetype=python:
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
TEST_DIRS += ['test']
JS_MODULES_PATH = 'modules/devtools/canvasdebugger'
EXTRA_JS_MODULES += [
'panel.js'
]

View File

@ -0,0 +1,72 @@
/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { Cc, Ci, Cu, Cr } = require("chrome");
const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
const EventEmitter = require("devtools/toolkit/event-emitter");
const { CanvasFront } = require("devtools/server/actors/canvas");
const { DevToolsUtils } = Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm", {});
function CanvasDebuggerPanel(iframeWindow, toolbox) {
this.panelWin = iframeWindow;
this._toolbox = toolbox;
this._destroyer = null;
EventEmitter.decorate(this);
};
exports.CanvasDebuggerPanel = CanvasDebuggerPanel;
CanvasDebuggerPanel.prototype = {
/**
* Open is effectively an asynchronous constructor.
*
* @return object
* A promise that is resolved when the Canvas Debugger completes opening.
*/
open: function() {
let targetPromise;
// Local debugging needs to make the target remote.
if (!this.target.isRemote) {
targetPromise = this.target.makeRemote();
} else {
targetPromise = promise.resolve(this.target);
}
return targetPromise
.then(() => {
this.panelWin.gToolbox = this._toolbox;
this.panelWin.gTarget = this.target;
this.panelWin.gFront = new CanvasFront(this.target.client, this.target.form);
return this.panelWin.startupCanvasDebugger();
})
.then(() => {
this.isReady = true;
this.emit("ready");
return this;
})
.then(null, function onError(aReason) {
DevToolsUtils.reportException("CanvasDebuggerPanel.prototype.open", aReason);
});
},
// DevToolPanel API
get target() this._toolbox.target,
destroy: function() {
// Make sure this panel is not already destroyed.
if (this._destroyer) {
return this._destroyer;
}
return this._destroyer = this.panelWin.shutdownCanvasDebugger().then(() => {
this.emit("destroyed");
});
}
};

View File

@ -0,0 +1,34 @@
[DEFAULT]
support-files =
doc_simple-canvas.html
doc_simple-canvas-deep-stack.html
doc_simple-canvas-transparent.html
head.js
[browser_canvas-actor-test-01.js]
[browser_canvas-actor-test-02.js]
[browser_canvas-actor-test-03.js]
[browser_canvas-actor-test-04.js]
[browser_canvas-actor-test-05.js]
[browser_canvas-actor-test-06.js]
[browser_canvas-actor-test-07.js]
[browser_canvas-frontend-call-highlight.js]
[browser_canvas-frontend-call-list.js]
[browser_canvas-frontend-call-search.js]
[browser_canvas-frontend-call-stack-01.js]
[browser_canvas-frontend-call-stack-02.js]
[browser_canvas-frontend-call-stack-03.js]
[browser_canvas-frontend-clear.js]
[browser_canvas-frontend-img-screenshots.js]
[browser_canvas-frontend-img-thumbnails-01.js]
[browser_canvas-frontend-img-thumbnails-02.js]
[browser_canvas-frontend-open.js]
[browser_canvas-frontend-record-01.js]
[browser_canvas-frontend-record-02.js]
[browser_canvas-frontend-record-03.js]
[browser_canvas-frontend-reload-01.js]
[browser_canvas-frontend-reload-02.js]
[browser_canvas-frontend-slider-01.js]
[browser_canvas-frontend-slider-02.js]
[browser_canvas-frontend-snapshot-select.js]
[browser_canvas-frontend-stepping.js]

View File

@ -0,0 +1,18 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if the canvas debugger leaks on initialization and sudden destruction.
* You can also use this initialization format as a template for other tests.
*/
function ifTestingSupported() {
let [target, debuggee, front] = yield initCallWatcherBackend(SIMPLE_CANVAS_URL);
ok(target, "Should have a target available.");
ok(debuggee, "Should have a debuggee available.");
ok(front, "Should have a protocol front available.");
yield removeTab(target.tab);
finish();
}

View File

@ -0,0 +1,77 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if functions calls are recorded and stored for a canvas context,
* and that their stack is successfully retrieved.
*/
function ifTestingSupported() {
let [target, debuggee, front] = yield initCallWatcherBackend(SIMPLE_CANVAS_URL);
let navigated = once(target, "navigate");
yield front.setup({
tracedGlobals: ["CanvasRenderingContext2D", "WebGLRenderingContext"],
startRecording: true,
performReload: true
});
ok(true, "The front was setup up successfully.");
yield navigated;
ok(true, "Target automatically navigated when the front was set up.");
// Allow the content to execute some functions.
yield waitForTick();
let functionCalls = yield front.pauseRecording();
ok(functionCalls,
"An array of function call actors was sent after reloading.");
ok(functionCalls.length > 0,
"There's at least one function call actor available.");
is(functionCalls[0].type, CallWatcherFront.METHOD_FUNCTION,
"The called function is correctly identified as a method.");
is(functionCalls[0].name, "clearRect",
"The called function's name is correct.");
is(functionCalls[0].file, SIMPLE_CANVAS_URL,
"The called function's file is correct.");
is(functionCalls[0].line, 25,
"The called function's line is correct.");
is(functionCalls[0].callerPreview, "ctx",
"The called function's caller preview is correct.");
is(functionCalls[0].argsPreview, "0, 0, 128, 128",
"The called function's args preview is correct.");
let details = yield functionCalls[1].getDetails();
ok(details,
"The first called function has some details available.")
is(details.stack.length, 3,
"The called function's stack depth is correct.");
is(details.stack[0].name, "fillStyle",
"The called function's stack is correct (1.1).");
is(details.stack[0].file, SIMPLE_CANVAS_URL,
"The called function's stack is correct (1.2).");
is(details.stack[0].line, 20,
"The called function's stack is correct (1.3).");
is(details.stack[1].name, "drawRect",
"The called function's stack is correct (2.1).");
is(details.stack[1].file, SIMPLE_CANVAS_URL,
"The called function's stack is correct (2.2).");
is(details.stack[1].line, 26,
"The called function's stack is correct (2.3).");
is(details.stack[2].name, "drawScene",
"The called function's stack is correct (3.1).");
is(details.stack[2].file, SIMPLE_CANVAS_URL,
"The called function's stack is correct (3.2).");
is(details.stack[2].line, 33,
"The called function's stack is correct (3.3).");
yield removeTab(target.tab);
finish();
}

View File

@ -0,0 +1,75 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if functions inside a single animation frame are recorded and stored
* for a canvas context.
*/
function ifTestingSupported() {
let [target, debuggee, front] = yield initCanavsDebuggerBackend(SIMPLE_CANVAS_URL);
let navigated = once(target, "navigate");
yield front.setup({ reload: true });
ok(true, "The front was setup up successfully.");
yield navigated;
ok(true, "Target automatically navigated when the front was set up.");
let snapshotActor = yield front.recordAnimationFrame();
ok(snapshotActor,
"A snapshot actor was sent after recording.");
let animationOverview = yield snapshotActor.getOverview();
ok(snapshotActor,
"An animation overview could be retrieved after recording.");
let functionCalls = animationOverview.calls;
ok(functionCalls,
"An array of function call actors was sent after recording.");
is(functionCalls.length, 8,
"The number of function call actors is correct.");
is(functionCalls[0].type, CallWatcherFront.METHOD_FUNCTION,
"The first called function is correctly identified as a method.");
is(functionCalls[0].name, "clearRect",
"The first called function's name is correct.");
is(functionCalls[0].file, SIMPLE_CANVAS_URL,
"The first called function's file is correct.");
is(functionCalls[0].line, 25,
"The first called function's line is correct.");
is(functionCalls[0].argsPreview, "0, 0, 128, 128",
"The first called function's args preview is correct.");
is(functionCalls[0].callerPreview, "ctx",
"The first called function's caller preview is correct.");
is(functionCalls[6].type, CallWatcherFront.METHOD_FUNCTION,
"The penultimate called function is correctly identified as a method.");
is(functionCalls[6].name, "fillRect",
"The penultimate called function's name is correct.");
is(functionCalls[6].file, SIMPLE_CANVAS_URL,
"The penultimate called function's file is correct.");
is(functionCalls[6].line, 21,
"The penultimate called function's line is correct.");
is(functionCalls[6].argsPreview, "10, 10, 55, 50",
"The penultimate called function's args preview is correct.");
is(functionCalls[6].callerPreview, "ctx",
"The penultimate called function's caller preview is correct.");
is(functionCalls[7].type, CallWatcherFront.METHOD_FUNCTION,
"The last called function is correctly identified as a method.");
is(functionCalls[7].name, "requestAnimationFrame",
"The last called function's name is correct.");
is(functionCalls[7].file, SIMPLE_CANVAS_URL,
"The last called function's file is correct.");
is(functionCalls[7].line, 30,
"The last called function's line is correct.");
ok(functionCalls[7].argsPreview.contains("Function"),
"The last called function's args preview is correct.");
is(functionCalls[7].callerPreview, "",
"The last called function's caller preview is correct.");
yield removeTab(target.tab);
finish();
}

View File

@ -0,0 +1,80 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if draw calls inside a single animation frame generate and retrieve
* the correct thumbnails.
*/
function ifTestingSupported() {
let [target, debuggee, front] = yield initCanavsDebuggerBackend(SIMPLE_CANVAS_URL);
let navigated = once(target, "navigate");
yield front.setup({ reload: true });
ok(true, "The front was setup up successfully.");
yield navigated;
ok(true, "Target automatically navigated when the front was set up.");
let snapshotActor = yield front.recordAnimationFrame();
ok(snapshotActor,
"A snapshot actor was sent after recording.");
let animationOverview = yield snapshotActor.getOverview();
ok(snapshotActor,
"An animation overview could be retrieved after recording.");
let thumbnails = animationOverview.thumbnails;
ok(thumbnails,
"An array of thumbnails was sent after recording.");
is(thumbnails.length, 4,
"The number of thumbnails is correct.");
is(thumbnails[0].index, 0,
"The first thumbnail's index is correct.");
is(thumbnails[0].width, 50,
"The first thumbnail's width is correct.");
is(thumbnails[0].height, 50,
"The first thumbnail's height is correct.");
is(thumbnails[0].flipped, false,
"The first thumbnail's flipped flag is correct.");
is([].find.call(thumbnails[0].pixels, e => e > 0), undefined,
"The first thumbnail's pixels seem to be completely transparent.");
is(thumbnails[1].index, 2,
"The second thumbnail's index is correct.");
is(thumbnails[1].width, 50,
"The second thumbnail's width is correct.");
is(thumbnails[1].height, 50,
"The second thumbnail's height is correct.");
is(thumbnails[1].flipped, false,
"The second thumbnail's flipped flag is correct.");
is([].find.call(thumbnails[1].pixels, e => e > 0), 4290822336,
"The second thumbnail's pixels seem to not be completely transparent.");
is(thumbnails[2].index, 4,
"The third thumbnail's index is correct.");
is(thumbnails[2].width, 50,
"The third thumbnail's width is correct.");
is(thumbnails[2].height, 50,
"The third thumbnail's height is correct.");
is(thumbnails[2].flipped, false,
"The third thumbnail's flipped flag is correct.");
is([].find.call(thumbnails[2].pixels, e => e > 0), 4290822336,
"The third thumbnail's pixels seem to not be completely transparent.");
is(thumbnails[3].index, 6,
"The fourth thumbnail's index is correct.");
is(thumbnails[3].width, 50,
"The fourth thumbnail's width is correct.");
is(thumbnails[3].height, 50,
"The fourth thumbnail's height is correct.");
is(thumbnails[3].flipped, false,
"The fourth thumbnail's flipped flag is correct.");
is([].find.call(thumbnails[3].pixels, e => e > 0), 4290822336,
"The fourth thumbnail's pixels seem to not be completely transparent.");
yield removeTab(target.tab);
finish();
}

View File

@ -0,0 +1,45 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if draw calls inside a single animation frame generate and retrieve
* the correct "end result" screenshot.
*/
function ifTestingSupported() {
let [target, debuggee, front] = yield initCanavsDebuggerBackend(SIMPLE_CANVAS_URL);
let navigated = once(target, "navigate");
yield front.setup({ reload: true });
ok(true, "The front was setup up successfully.");
yield navigated;
ok(true, "Target automatically navigated when the front was set up.");
let snapshotActor = yield front.recordAnimationFrame();
ok(snapshotActor,
"A snapshot actor was sent after recording.");
let animationOverview = yield snapshotActor.getOverview();
ok(snapshotActor,
"An animation overview could be retrieved after recording.");
let screenshot = animationOverview.screenshot;
ok(screenshot,
"A screenshot was sent after recording.");
is(screenshot.index, 6,
"The screenshot's index is correct.");
is(screenshot.width, 128,
"The screenshot's width is correct.");
is(screenshot.height, 128,
"The screenshot's height is correct.");
is(screenshot.flipped, false,
"The screenshot's flipped flag is correct.");
is([].find.call(screenshot.pixels, e => e > 0), 4290822336,
"The screenshot's pixels seem to not be completely transparent.");
yield removeTab(target.tab);
finish();
}

View File

@ -0,0 +1,95 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if screenshots for arbitrary draw calls are generated properly.
*/
function ifTestingSupported() {
let [target, debuggee, front] = yield initCanavsDebuggerBackend(SIMPLE_CANVAS_TRANSPARENT_URL);
let navigated = once(target, "navigate");
yield front.setup({ reload: true });
ok(true, "The front was setup up successfully.");
yield navigated;
ok(true, "Target automatically navigated when the front was set up.");
let snapshotActor = yield front.recordAnimationFrame();
let animationOverview = yield snapshotActor.getOverview();
let functionCalls = animationOverview.calls;
ok(functionCalls,
"An array of function call actors was sent after recording.");
is(functionCalls.length, 8,
"The number of function call actors is correct.");
is(functionCalls[0].name, "clearRect",
"The first called function's name is correct.");
is(functionCalls[2].name, "fillRect",
"The second called function's name is correct.");
is(functionCalls[4].name, "fillRect",
"The third called function's name is correct.");
is(functionCalls[6].name, "fillRect",
"The fourth called function's name is correct.");
let firstDrawCallScreenshot = yield snapshotActor.generateScreenshotFor(functionCalls[0]);
let secondDrawCallScreenshot = yield snapshotActor.generateScreenshotFor(functionCalls[2]);
let thirdDrawCallScreenshot = yield snapshotActor.generateScreenshotFor(functionCalls[4]);
let fourthDrawCallScreenshot = yield snapshotActor.generateScreenshotFor(functionCalls[6]);
ok(firstDrawCallScreenshot,
"The first draw call has a screenshot attached.");
is(firstDrawCallScreenshot.index, 0,
"The first draw call has the correct screenshot index.");
is(firstDrawCallScreenshot.width, 128,
"The first draw call has the correct screenshot width.");
is(firstDrawCallScreenshot.height, 128,
"The first draw call has the correct screenshot height.");
is([].find.call(firstDrawCallScreenshot.pixels, e => e > 0), undefined,
"The first draw call's screenshot's pixels seems to be completely transparent.");
ok(secondDrawCallScreenshot,
"The second draw call has a screenshot attached.");
is(secondDrawCallScreenshot.index, 2,
"The second draw call has the correct screenshot index.");
is(secondDrawCallScreenshot.width, 128,
"The second draw call has the correct screenshot width.");
is(secondDrawCallScreenshot.height, 128,
"The second draw call has the correct screenshot height.");
is([].find.call(firstDrawCallScreenshot.pixels, e => e > 0), undefined,
"The second draw call's screenshot's pixels seems to be completely transparent.");
ok(thirdDrawCallScreenshot,
"The third draw call has a screenshot attached.");
is(thirdDrawCallScreenshot.index, 4,
"The third draw call has the correct screenshot index.");
is(thirdDrawCallScreenshot.width, 128,
"The third draw call has the correct screenshot width.");
is(thirdDrawCallScreenshot.height, 128,
"The third draw call has the correct screenshot height.");
is([].find.call(thirdDrawCallScreenshot.pixels, e => e > 0), 2160001024,
"The third draw call's screenshot's pixels seems to not be completely transparent.");
ok(fourthDrawCallScreenshot,
"The fourth draw call has a screenshot attached.");
is(fourthDrawCallScreenshot.index, 6,
"The fourth draw call has the correct screenshot index.");
is(fourthDrawCallScreenshot.width, 128,
"The fourth draw call has the correct screenshot width.");
is(fourthDrawCallScreenshot.height, 128,
"The fourth draw call has the correct screenshot height.");
is([].find.call(fourthDrawCallScreenshot.pixels, e => e > 0), 2147483839,
"The fourth draw call's screenshot's pixels seems to not be completely transparent.");
isnot(firstDrawCallScreenshot.pixels, secondDrawCallScreenshot.pixels,
"The screenshots taken on consecutive draw calls are different (1).");
isnot(secondDrawCallScreenshot.pixels, thirdDrawCallScreenshot.pixels,
"The screenshots taken on consecutive draw calls are different (2).");
isnot(thirdDrawCallScreenshot.pixels, fourthDrawCallScreenshot.pixels,
"The screenshots taken on consecutive draw calls are different (3).");
yield removeTab(target.tab);
finish();
}

View File

@ -0,0 +1,94 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if screenshots for non-draw calls can still be retrieved properly,
* by deferring the the most recent previous draw-call.
*/
function ifTestingSupported() {
let [target, debuggee, front] = yield initCanavsDebuggerBackend(SIMPLE_CANVAS_URL);
let navigated = once(target, "navigate");
yield front.setup({ reload: true });
ok(true, "The front was setup up successfully.");
yield navigated;
ok(true, "Target automatically navigated when the front was set up.");
let snapshotActor = yield front.recordAnimationFrame();
let animationOverview = yield snapshotActor.getOverview();
let functionCalls = animationOverview.calls;
ok(functionCalls,
"An array of function call actors was sent after recording.");
is(functionCalls.length, 8,
"The number of function call actors is correct.");
let firstNonDrawCall = yield functionCalls[1].getDetails();
let secondNonDrawCall = yield functionCalls[3].getDetails();
let lastNonDrawCall = yield functionCalls[7].getDetails();
is(firstNonDrawCall.name, "fillStyle",
"The first non-draw function's name is correct.");
is(secondNonDrawCall.name, "fillStyle",
"The second non-draw function's name is correct.");
is(lastNonDrawCall.name, "requestAnimationFrame",
"The last non-draw function's name is correct.");
let firstScreenshot = yield snapshotActor.generateScreenshotFor(functionCalls[1]);
let secondScreenshot = yield snapshotActor.generateScreenshotFor(functionCalls[3]);
let lastScreenshot = yield snapshotActor.generateScreenshotFor(functionCalls[7]);
ok(firstScreenshot,
"A screenshot was successfully retrieved for the first non-draw function.");
ok(secondScreenshot,
"A screenshot was successfully retrieved for the second non-draw function.");
ok(lastScreenshot,
"A screenshot was successfully retrieved for the last non-draw function.");
let firstActualScreenshot = yield snapshotActor.generateScreenshotFor(functionCalls[0]);
ok(sameArray(firstScreenshot.pixels, firstActualScreenshot.pixels),
"The screenshot for the first non-draw function is correct.");
is(firstScreenshot.width, 128,
"The screenshot for the first non-draw function has the correct width.");
is(firstScreenshot.height, 128,
"The screenshot for the first non-draw function has the correct height.");
let secondActualScreenshot = yield snapshotActor.generateScreenshotFor(functionCalls[2]);
ok(sameArray(secondScreenshot.pixels, secondActualScreenshot.pixels),
"The screenshot for the second non-draw function is correct.");
is(secondScreenshot.width, 128,
"The screenshot for the second non-draw function has the correct width.");
is(secondScreenshot.height, 128,
"The screenshot for the second non-draw function has the correct height.");
let lastActualScreenshot = yield snapshotActor.generateScreenshotFor(functionCalls[6]);
ok(sameArray(lastScreenshot.pixels, lastActualScreenshot.pixels),
"The screenshot for the last non-draw function is correct.");
is(lastScreenshot.width, 128,
"The screenshot for the last non-draw function has the correct width.");
is(lastScreenshot.height, 128,
"The screenshot for the last non-draw function has the correct height.");
ok(!sameArray(firstScreenshot.pixels, secondScreenshot.pixels),
"The screenshots taken on consecutive draw calls are different (1).");
ok(!sameArray(secondScreenshot.pixels, lastScreenshot.pixels),
"The screenshots taken on consecutive draw calls are different (2).");
yield removeTab(target.tab);
finish();
}
function sameArray(a, b) {
if (a.length != b.length) {
return false;
}
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) {
return false;
}
}
return true;
}

View File

@ -0,0 +1,41 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if certain function calls are properly highlighted in the UI.
*/
function ifTestingSupported() {
let [target, debuggee, panel] = yield initCanavsDebuggerFrontend(SIMPLE_CANVAS_URL);
let { window, $, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;
yield reload(target);
let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
let callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
SnapshotsListView._onRecordButtonClick();
yield promise.all([recordingFinished, callListPopulated]);
is(CallsListView.itemCount, 8,
"All the function calls should now be displayed in the UI.");
is($(".call-item-view", CallsListView.getItemAtIndex(0).target).hasAttribute("draw-call"), true,
"The first item's node should have a draw-call attribute.");
is($(".call-item-view", CallsListView.getItemAtIndex(1).target).hasAttribute("draw-call"), false,
"The second item's node should not have a draw-call attribute.");
is($(".call-item-view", CallsListView.getItemAtIndex(2).target).hasAttribute("draw-call"), true,
"The third item's node should have a draw-call attribute.");
is($(".call-item-view", CallsListView.getItemAtIndex(3).target).hasAttribute("draw-call"), false,
"The fourth item's node should not have a draw-call attribute.");
is($(".call-item-view", CallsListView.getItemAtIndex(4).target).hasAttribute("draw-call"), true,
"The fifth item's node should have a draw-call attribute.");
is($(".call-item-view", CallsListView.getItemAtIndex(5).target).hasAttribute("draw-call"), false,
"The sixth item's node should not have a draw-call attribute.");
is($(".call-item-view", CallsListView.getItemAtIndex(6).target).hasAttribute("draw-call"), true,
"The seventh item's node should have a draw-call attribute.");
is($(".call-item-view", CallsListView.getItemAtIndex(7).target).hasAttribute("draw-call"), false,
"The eigth item's node should not have a draw-call attribute.");
yield teardown(panel);
finish();
}

View File

@ -0,0 +1,70 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if all the function calls associated with an animation frame snapshot
* are properly displayed in the UI.
*/
function ifTestingSupported() {
let [target, debuggee, panel] = yield initCanavsDebuggerFrontend(SIMPLE_CANVAS_URL);
let { window, $, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;
yield reload(target);
let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
let callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
SnapshotsListView._onRecordButtonClick();
yield promise.all([recordingFinished, callListPopulated]);
is(CallsListView.itemCount, 8,
"All the function calls should now be displayed in the UI.");
testItem(CallsListView.getItemAtIndex(0),
"1", "ctx", "clearRect", "(0, 0, 128, 128)", "doc_simple-canvas.html:25");
testItem(CallsListView.getItemAtIndex(1),
"2", "ctx", "fillStyle", " = rgb(192, 192, 192)", "doc_simple-canvas.html:20");
testItem(CallsListView.getItemAtIndex(2),
"3", "ctx", "fillRect", "(0, 0, 128, 128)", "doc_simple-canvas.html:21");
testItem(CallsListView.getItemAtIndex(3),
"4", "ctx", "fillStyle", " = rgba(0, 0, 192, 0.5)", "doc_simple-canvas.html:20");
testItem(CallsListView.getItemAtIndex(4),
"5", "ctx", "fillRect", "(30, 30, 55, 50)", "doc_simple-canvas.html:21");
testItem(CallsListView.getItemAtIndex(5),
"6", "ctx", "fillStyle", " = rgba(192, 0, 0, 0.5)", "doc_simple-canvas.html:20");
testItem(CallsListView.getItemAtIndex(6),
"7", "ctx", "fillRect", "(10, 10, 55, 50)", "doc_simple-canvas.html:21");
testItem(CallsListView.getItemAtIndex(7),
"8", "", "requestAnimationFrame", "(Function)", "doc_simple-canvas.html:30");
function testItem(item, index, context, name, args, location) {
let i = CallsListView.indexOfItem(item);
is(i, index - 1,
"The item at index " + index + " is correctly displayed in the UI.");
is($(".call-item-index", item.target).getAttribute("value"), index,
"The item's gutter label has the correct text.");
if (context) {
is($(".call-item-context", item.target).getAttribute("value"), context,
"The item's context label has the correct text.");
} else {
is($(".call-item-context", item.target), null,
"The item's context label should not be available.");
}
is($(".call-item-name", item.target).getAttribute("value"), name,
"The item's name label has the correct text.");
is($(".call-item-args", item.target).getAttribute("value"), args,
"The item's args label has the correct text.");
is($(".call-item-location", item.target).getAttribute("value"), location,
"The item's location label has the correct text.");
}
yield teardown(panel);
finish();
}

View File

@ -0,0 +1,72 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if filtering the items in the call list works properly.
*/
function ifTestingSupported() {
let [target, debuggee, panel] = yield initCanavsDebuggerFrontend(SIMPLE_CANVAS_URL);
let { window, $, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;
let searchbox = $("#calls-searchbox");
yield reload(target);
let firstRecordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
let callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
SnapshotsListView._onRecordButtonClick();
yield promise.all([firstRecordingFinished, callListPopulated]);
is(searchbox.value, "",
"The searchbox should be initially empty.");
is(CallsListView.visibleItems.length, 8,
"All the items should be initially visible in the calls list.");
searchbox.focus();
EventUtils.sendString("clear", window);
is(searchbox.value, "clear",
"The searchbox should now contain the 'clear' string.");
is(CallsListView.visibleItems.length, 1,
"Only one item should now be visible in the calls list.");
is(CallsListView.visibleItems[0].attachment.actor.type, CallWatcherFront.METHOD_FUNCTION,
"The visible item's type has the expected value.");
is(CallsListView.visibleItems[0].attachment.actor.name, "clearRect",
"The visible item's name has the expected value.");
is(CallsListView.visibleItems[0].attachment.actor.file, SIMPLE_CANVAS_URL,
"The visible item's file has the expected value.");
is(CallsListView.visibleItems[0].attachment.actor.line, 25,
"The visible item's line has the expected value.");
is(CallsListView.visibleItems[0].attachment.actor.argsPreview, "0, 0, 128, 128",
"The visible item's args have the expected value.");
is(CallsListView.visibleItems[0].attachment.actor.callerPreview, "ctx",
"The visible item's caller has the expected value.");
let secondRecordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
let callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
SnapshotsListView._onRecordButtonClick();
yield secondRecordingFinished;
SnapshotsListView.selectedIndex = 1;
yield callListPopulated;
is(searchbox.value, "clear",
"The searchbox should still contain the 'clear' string.");
is(CallsListView.visibleItems.length, 1,
"Only one item should still be visible in the calls list.");
for (let i = 0; i < 5; i++) {
searchbox.focus();
EventUtils.sendKey("BACK_SPACE", window);
}
is(searchbox.value, "",
"The searchbox should now be emptied.");
is(CallsListView.visibleItems.length, 8,
"All the items should be initially visible again in the calls list.");
yield teardown(panel);
finish();
}

View File

@ -0,0 +1,74 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if the a function call's stack is properly displayed in the UI.
*/
function ifTestingSupported() {
let [target, debuggee, panel] = yield initCanavsDebuggerFrontend(SIMPLE_CANVAS_DEEP_STACK_URL);
let { window, $, $all, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;
yield reload(target);
let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
let callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
SnapshotsListView._onRecordButtonClick();
yield promise.all([recordingFinished, callListPopulated]);
let callItem = CallsListView.getItemAtIndex(2);
let locationLink = $(".call-item-location", callItem.target);
is($(".call-item-stack", callItem.target), null,
"There should be no stack container available yet for the draw call.");
let callStackDisplayed = once(window, EVENTS.CALL_STACK_DISPLAYED);
EventUtils.sendMouseEvent({ type: "mousedown" }, locationLink, window);
yield callStackDisplayed;
isnot($(".call-item-stack", callItem.target), null,
"There should be a stack container available now for the draw call.");
is($all(".call-item-stack-fn", callItem.target).length, 4,
"There should be 4 functions on the stack for the draw call.");
ok($all(".call-item-stack-fn-name", callItem.target)[0].getAttribute("value")
.contains("C()"),
"The first function on the stack has the correct name.");
ok($all(".call-item-stack-fn-name", callItem.target)[1].getAttribute("value")
.contains("B()"),
"The second function on the stack has the correct name.");
ok($all(".call-item-stack-fn-name", callItem.target)[2].getAttribute("value")
.contains("A()"),
"The third function on the stack has the correct name.");
ok($all(".call-item-stack-fn-name", callItem.target)[3].getAttribute("value")
.contains("drawRect()"),
"The fourth function on the stack has the correct name.");
is($all(".call-item-stack-fn-location", callItem.target)[0].getAttribute("value"),
"doc_simple-canvas-deep-stack.html:26",
"The first function on the stack has the correct location.");
is($all(".call-item-stack-fn-location", callItem.target)[1].getAttribute("value"),
"doc_simple-canvas-deep-stack.html:28",
"The second function on the stack has the correct location.");
is($all(".call-item-stack-fn-location", callItem.target)[2].getAttribute("value"),
"doc_simple-canvas-deep-stack.html:30",
"The third function on the stack has the correct location.");
is($all(".call-item-stack-fn-location", callItem.target)[3].getAttribute("value"),
"doc_simple-canvas-deep-stack.html:35",
"The fourth function on the stack has the correct location.");
let jumpedToSource = once(window, EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER);
EventUtils.sendMouseEvent({ type: "mousedown" }, $(".call-item-stack-fn-location", callItem.target));
yield jumpedToSource;
let toolbox = yield gDevTools.getToolbox(target);
let { panelWin: { DebuggerView: view } } = toolbox.getPanel("jsdebugger");
is(view.Sources.selectedValue, SIMPLE_CANVAS_DEEP_STACK_URL,
"The expected source was shown in the debugger.");
is(view.editor.getCursor().line, 25,
"The expected source line is highlighted in the debugger.");
yield teardown(panel);
finish();
}

View File

@ -0,0 +1,49 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if the a function call's stack is properly displayed in the UI
* and jumping to source in the debugger for the topmost call item works.
*/
function ifTestingSupported() {
let [target, debuggee, panel] = yield initCanavsDebuggerFrontend(SIMPLE_CANVAS_DEEP_STACK_URL);
let { window, $, $all, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;
yield reload(target);
let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
let callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
SnapshotsListView._onRecordButtonClick();
yield promise.all([recordingFinished, callListPopulated]);
let callItem = CallsListView.getItemAtIndex(2);
let locationLink = $(".call-item-location", callItem.target);
is($(".call-item-stack", callItem.target), null,
"There should be no stack container available yet for the draw call.");
let callStackDisplayed = once(window, EVENTS.CALL_STACK_DISPLAYED);
EventUtils.sendMouseEvent({ type: "mousedown" }, locationLink, window);
yield callStackDisplayed;
isnot($(".call-item-stack", callItem.target), null,
"There should be a stack container available now for the draw call.");
is($all(".call-item-stack-fn", callItem.target).length, 4,
"There should be 4 functions on the stack for the draw call.");
let jumpedToSource = once(window, EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER);
EventUtils.sendMouseEvent({ type: "mousedown" }, $(".call-item-location", callItem.target));
yield jumpedToSource;
let toolbox = yield gDevTools.getToolbox(target);
let { panelWin: { DebuggerView: view } } = toolbox.getPanel("jsdebugger");
is(view.Sources.selectedValue, SIMPLE_CANVAS_DEEP_STACK_URL,
"The expected source was shown in the debugger.");
is(view.editor.getCursor().line, 23,
"The expected source line is highlighted in the debugger.");
yield teardown(panel);
finish();
}

View File

@ -0,0 +1,61 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if the a function call's stack can be shown/hidden by double-clicking
* on a function call item.
*/
function ifTestingSupported() {
let [target, debuggee, panel] = yield initCanavsDebuggerFrontend(SIMPLE_CANVAS_DEEP_STACK_URL);
let { window, $, $all, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;
yield reload(target);
let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
let callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
SnapshotsListView._onRecordButtonClick();
yield promise.all([recordingFinished, callListPopulated]);
let callItem = CallsListView.getItemAtIndex(2);
let view = $(".call-item-view", callItem.target);
let contents = $(".call-item-contents", callItem.target);
is(view.hasAttribute("call-stack-populated"), false,
"The call item's view should not have the stack populated yet.");
is(view.hasAttribute("call-stack-expanded"), false,
"The call item's view should not have the stack populated yet.");
is($(".call-item-stack", callItem.target), null,
"There should be no stack container available yet for the draw call.");
let callStackDisplayed = once(window, EVENTS.CALL_STACK_DISPLAYED);
EventUtils.sendMouseEvent({ type: "dblclick" }, contents, window);
yield callStackDisplayed;
is(view.hasAttribute("call-stack-populated"), true,
"The call item's view should have the stack populated now.");
is(view.getAttribute("call-stack-expanded"), "true",
"The call item's view should have the stack expanded now.");
isnot($(".call-item-stack", callItem.target), null,
"There should be a stack container available now for the draw call.");
is($(".call-item-stack", callItem.target).hidden, false,
"The stack container should now be visible.");
is($all(".call-item-stack-fn", callItem.target).length, 4,
"There should be 4 functions on the stack for the draw call.");
EventUtils.sendMouseEvent({ type: "dblclick" }, contents, window);
is(view.hasAttribute("call-stack-populated"), true,
"The call item's view should still have the stack populated.");
is(view.getAttribute("call-stack-expanded"), "false",
"The call item's view should not have the stack expanded anymore.");
isnot($(".call-item-stack", callItem.target), null,
"There should still be a stack container available for the draw call.");
is($(".call-item-stack", callItem.target).hidden, true,
"The stack container should now be hidden.");
is($all(".call-item-stack-fn", callItem.target).length, 4,
"There should still be 4 functions on the stack for the draw call.");
yield teardown(panel);
finish();
}

View File

@ -0,0 +1,43 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if clearing the snapshots list works as expected.
*/
function ifTestingSupported() {
let [target, debuggee, panel] = yield initCanavsDebuggerFrontend(SIMPLE_CANVAS_URL);
let { window, EVENTS, SnapshotsListView } = panel.panelWin;
yield reload(target);
let firstRecordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
SnapshotsListView._onRecordButtonClick();
yield firstRecordingFinished;
ok(true, "Finished recording a snapshot of the animation loop.");
is(SnapshotsListView.itemCount, 1,
"There should be one item available in the snapshots list.");
let secondRecordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
SnapshotsListView._onRecordButtonClick();
yield secondRecordingFinished;
ok(true, "Finished recording another snapshot of the animation loop.");
is(SnapshotsListView.itemCount, 2,
"There should be two items available in the snapshots list.");
let clearingFinished = once(window, EVENTS.SNAPSHOTS_LIST_CLEARED);
SnapshotsListView._onClearButtonClick();
yield clearingFinished;
ok(true, "Finished recording all snapshots.");
is(SnapshotsListView.itemCount, 0,
"There should be no items available in the snapshots list.");
yield teardown(panel);
finish();
}

View File

@ -0,0 +1,34 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if screenshots are properly displayed in the UI.
*/
function ifTestingSupported() {
let [target, debuggee, panel] = yield initCanavsDebuggerFrontend(SIMPLE_CANVAS_URL);
let { window, $, EVENTS, SnapshotsListView } = panel.panelWin;
yield reload(target);
let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
let callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
let screenshotDisplayed = once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED);
SnapshotsListView._onRecordButtonClick();
yield promise.all([recordingFinished, callListPopulated, screenshotDisplayed]);
is($("#screenshot-container").hidden, false,
"The screenshot container should now be visible.");
is($("#screenshot-dimensions").getAttribute("value"), "128 x 128",
"The screenshot dimensions label has the expected value.");
is($("#screenshot-image").getAttribute("flipped"), "false",
"The screenshot element should not be flipped vertically.");
ok(window.getComputedStyle($("#screenshot-image")).backgroundImage.contains("#screenshot-rendering"),
"The screenshot element should have an offscreen canvas element as a background.");
yield teardown(panel);
finish();
}

View File

@ -0,0 +1,65 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if thumbnails are properly displayed in the UI.
*/
function ifTestingSupported() {
let [target, debuggee, panel] = yield initCanavsDebuggerFrontend(SIMPLE_CANVAS_URL);
let { window, $, $all, EVENTS, SnapshotsListView } = panel.panelWin;
yield reload(target);
let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
let callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
let thumbnailsDisplayed = once(window, EVENTS.THUMBNAILS_DISPLAYED);
SnapshotsListView._onRecordButtonClick();
yield promise.all([recordingFinished, callListPopulated, thumbnailsDisplayed]);
is($all(".filmstrip-thumbnail").length, 4,
"There should be 4 thumbnails displayed in the UI.");
let firstThumbnail = $(".filmstrip-thumbnail[index='0']");
ok(firstThumbnail,
"The first thumbnail element should be for the function call at index 0.");
is(firstThumbnail.width, 50,
"The first thumbnail's width is correct.");
is(firstThumbnail.height, 50,
"The first thumbnail's height is correct.");
is(firstThumbnail.getAttribute("flipped"), "false",
"The first thumbnail should not be flipped vertically.");
let secondThumbnail = $(".filmstrip-thumbnail[index='2']");
ok(secondThumbnail,
"The second thumbnail element should be for the function call at index 2.");
is(secondThumbnail.width, 50,
"The second thumbnail's width is correct.");
is(secondThumbnail.height, 50,
"The second thumbnail's height is correct.");
is(secondThumbnail.getAttribute("flipped"), "false",
"The second thumbnail should not be flipped vertically.");
let thirdThumbnail = $(".filmstrip-thumbnail[index='4']");
ok(thirdThumbnail,
"The third thumbnail element should be for the function call at index 4.");
is(thirdThumbnail.width, 50,
"The third thumbnail's width is correct.");
is(thirdThumbnail.height, 50,
"The third thumbnail's height is correct.");
is(thirdThumbnail.getAttribute("flipped"), "false",
"The third thumbnail should not be flipped vertically.");
let fourthThumbnail = $(".filmstrip-thumbnail[index='6']");
ok(fourthThumbnail,
"The fourth thumbnail element should be for the function call at index 6.");
is(fourthThumbnail.width, 50,
"The fourth thumbnail's width is correct.");
is(fourthThumbnail.height, 50,
"The fourth thumbnail's height is correct.");
is(fourthThumbnail.getAttribute("flipped"), "false",
"The fourth thumbnail should not be flipped vertically.");
yield teardown(panel);
finish();
}

View File

@ -0,0 +1,67 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if thumbnails are correctly linked with other UI elements like
* function call items and their respective screenshots.
*/
function ifTestingSupported() {
let [target, debuggee, panel] = yield initCanavsDebuggerFrontend(SIMPLE_CANVAS_URL);
let { window, $, $all, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;
yield reload(target);
let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
let callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
let thumbnailsDisplayed = once(window, EVENTS.THUMBNAILS_DISPLAYED);
let screenshotDisplayed = once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED);
SnapshotsListView._onRecordButtonClick();
yield promise.all([
recordingFinished,
callListPopulated,
thumbnailsDisplayed,
screenshotDisplayed
]);
is($all(".filmstrip-thumbnail[highlighted]").length, 0,
"There should be no highlighted thumbnail available yet.");
is(CallsListView.selectedIndex, -1,
"There should be no selected item in the calls list view.");
EventUtils.sendMouseEvent({ type: "mousedown" }, $all(".filmstrip-thumbnail")[0], window);
yield once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED);
info("The first draw call was selected, by clicking the first thumbnail.");
isnot($(".filmstrip-thumbnail[highlighted][index='0']"), null,
"There should be a highlighted thumbnail available now, for the first draw call.");
is($all(".filmstrip-thumbnail[highlighted]").length, 1,
"There should be only one highlighted thumbnail available now.");
is(CallsListView.selectedIndex, 0,
"The first draw call should be selected in the calls list view.");
EventUtils.sendMouseEvent({ type: "mousedown" }, $all(".call-item-view")[1], window);
yield once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED);
info("The second context call was selected, by clicking the second call item.");
isnot($(".filmstrip-thumbnail[highlighted][index='0']"), null,
"There should be a highlighted thumbnail available, for the first draw call.");
is($all(".filmstrip-thumbnail[highlighted]").length, 1,
"There should be only one highlighted thumbnail available.");
is(CallsListView.selectedIndex, 1,
"The second draw call should be selected in the calls list view.");
EventUtils.sendMouseEvent({ type: "mousedown" }, $all(".call-item-view")[2], window);
yield once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED);
info("The second draw call was selected, by clicking the third call item.");
isnot($(".filmstrip-thumbnail[highlighted][index='2']"), null,
"There should be a highlighted thumbnail available, for the second draw call.");
is($all(".filmstrip-thumbnail[highlighted]").length, 1,
"There should be only one highlighted thumbnail available.");
is(CallsListView.selectedIndex, 2,
"The second draw call should be selected in the calls list view.");
yield teardown(panel);
finish();
}

View File

@ -0,0 +1,41 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests that the frontend UI is properly configured when opening the tool.
*/
function ifTestingSupported() {
let [target, debuggee, panel] = yield initCanavsDebuggerFrontend(SIMPLE_CANVAS_URL);
let { $ } = panel.panelWin;
is($("#snapshots-pane").hasAttribute("hidden"), false,
"The snapshots pane should initially be visible.");
is($("#debugging-pane").hasAttribute("hidden"), false,
"The debugging pane should initially be visible.");
is($("#record-snapshot").getAttribute("hidden"), "true",
"The 'record snapshot' button should initially be hidden.");
is($("#import-snapshot").hasAttribute("hidden"), false,
"The 'import snapshot' button should initially be visible.");
is($("#clear-snapshots").hasAttribute("hidden"), false,
"The 'clear snapshots' button should initially be visible.");
is($("#reload-notice").hasAttribute("hidden"), false,
"The reload notice should initially be visible.");
is($("#empty-notice").getAttribute("hidden"), "true",
"The empty notice should initially be hidden.");
is($("#import-notice").getAttribute("hidden"), "true",
"The import notice should initially be hidden.");
is($("#screenshot-container").getAttribute("hidden"), "true",
"The screenshot container should initially be hidden.");
is($("#snapshot-filmstrip").getAttribute("hidden"), "true",
"The snapshot filmstrip should initially be hidden.");
is($("#debugging-pane-contents").getAttribute("hidden"), "true",
"The rest of the UI should initially be hidden.");
yield teardown(panel);
finish();
}

View File

@ -0,0 +1,62 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests whether the frontend behaves correctly while reording a snapshot.
*/
function ifTestingSupported() {
let [target, debuggee, panel] = yield initCanavsDebuggerFrontend(SIMPLE_CANVAS_URL);
let { window, EVENTS, $, SnapshotsListView } = panel.panelWin;
yield reload(target);
is($("#record-snapshot").hasAttribute("checked"), false,
"The 'record snapshot' button should initially be unchecked.");
is($("#record-snapshot").hasAttribute("disabled"), false,
"The 'record snapshot' button should initially be enabled.");
is($("#record-snapshot").hasAttribute("hidden"), false,
"The 'record snapshot' button should now be visible.");
is(SnapshotsListView.itemCount, 0,
"There should be no items available in the snapshots list view.");
is(SnapshotsListView.selectedIndex, -1,
"There should be no selected item in the snapshots list view.");
let recordingStarted = once(window, EVENTS.SNAPSHOT_RECORDING_STARTED);
let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
SnapshotsListView._onRecordButtonClick();
yield recordingStarted;
ok(true, "Started recording a snapshot of the animation loop.");
is($("#record-snapshot").getAttribute("checked"), "true",
"The 'record snapshot' button should now be checked.");
is($("#record-snapshot").getAttribute("disabled"), "true",
"The 'record snapshot' button should now be disabled.");
is($("#record-snapshot").hasAttribute("hidden"), false,
"The 'record snapshot' button should still be visible.");
is(SnapshotsListView.itemCount, 1,
"There should be one item available in the snapshots list view now.");
is(SnapshotsListView.selectedIndex, -1,
"There should be no selected item in the snapshots list view yet.");
yield recordingFinished;
ok(true, "Finished recording a snapshot of the animation loop.");
is($("#record-snapshot").hasAttribute("checked"), false,
"The 'record snapshot' button should now be unchecked.");
is($("#record-snapshot").hasAttribute("disabled"), false,
"The 'record snapshot' button should now be re-enabled.");
is($("#record-snapshot").hasAttribute("hidden"), false,
"The 'record snapshot' button should still be visible.");
is(SnapshotsListView.itemCount, 1,
"There should still be only one item available in the snapshots list view.");
is(SnapshotsListView.selectedIndex, 0,
"There should be one selected item in the snapshots list view now.");
yield teardown(panel);
finish();
}

View File

@ -0,0 +1,73 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests whether the frontend displays a placeholder snapshot while recording.
*/
function ifTestingSupported() {
let [target, debuggee, panel] = yield initCanavsDebuggerFrontend(SIMPLE_CANVAS_URL);
let { window, EVENTS, L10N, $, SnapshotsListView } = panel.panelWin;
yield reload(target);
let recordingStarted = once(window, EVENTS.SNAPSHOT_RECORDING_STARTED);
let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
let recordingSelected = once(window, EVENTS.SNAPSHOT_RECORDING_SELECTED);
SnapshotsListView._onRecordButtonClick();
yield recordingStarted;
ok(true, "Started recording a snapshot of the animation loop.");
let item = SnapshotsListView.getItemAtIndex(0);
is($(".snapshot-item-title", item.target).getAttribute("value"),
L10N.getFormatStr("snapshotsList.itemLabel", 1),
"The placeholder item's title label is correct.");
is($(".snapshot-item-calls", item.target).getAttribute("value"),
L10N.getStr("snapshotsList.loadingLabel"),
"The placeholder item's calls label is correct.");
is($(".snapshot-item-save", item.target).getAttribute("value"), "",
"The placeholder item's save label should not have a value yet.");
is($("#reload-notice").getAttribute("hidden"), "true",
"The reload notice should now be hidden.");
is($("#empty-notice").getAttribute("hidden"), "true",
"The empty notice should now be hidden.");
is($("#import-notice").hasAttribute("hidden"), false,
"The import notice should now be visible.");
is($("#screenshot-container").getAttribute("hidden"), "true",
"The screenshot container should still be hidden.");
is($("#snapshot-filmstrip").getAttribute("hidden"), "true",
"The snapshot filmstrip should still be hidden.");
is($("#debugging-pane-contents").getAttribute("hidden"), "true",
"The rest of the UI should still be hidden.");
yield recordingFinished;
ok(true, "Finished recording a snapshot of the animation loop.");
yield recordingSelected;
ok(true, "Finished selecting a snapshot of the animation loop.");
is($("#reload-notice").getAttribute("hidden"), "true",
"The reload notice should now be hidden.");
is($("#empty-notice").getAttribute("hidden"), "true",
"The empty notice should now be hidden.");
is($("#import-notice").getAttribute("hidden"), "true",
"The import notice should now be hidden.");
is($("#screenshot-container").hasAttribute("hidden"), false,
"The screenshot container should now be visible.");
is($("#snapshot-filmstrip").hasAttribute("hidden"), false,
"The snapshot filmstrip should now be visible.");
is($("#debugging-pane-contents").hasAttribute("hidden"), false,
"The rest of the UI should now be visible.");
yield teardown(panel);
finish();
}

View File

@ -0,0 +1,37 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests whether the frontend displays the correct info for a snapshot
* after finishing recording.
*/
function ifTestingSupported() {
let [target, debuggee, panel] = yield initCanavsDebuggerFrontend(SIMPLE_CANVAS_URL);
let { window, EVENTS, $, SnapshotsListView } = panel.panelWin;
yield reload(target);
let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
SnapshotsListView._onRecordButtonClick();
yield recordingFinished;
ok(true, "Finished recording a snapshot of the animation loop.");
let item = SnapshotsListView.getItemAtIndex(0);
is(SnapshotsListView.selectedItem, item,
"The first item should now be selected in the snapshots list view (1).");
is(SnapshotsListView.selectedIndex, 0,
"The first item should now be selected in the snapshots list view (2).");
is($(".snapshot-item-calls", item.target).getAttribute("value"), "4 draws, 8 calls",
"The placeholder item's calls label is correct.");
is($(".snapshot-item-save", item.target).getAttribute("value"), "Save",
"The placeholder item's save label is correct.");
is($(".snapshot-item-save", item.target).getAttribute("disabled"), "false",
"The placeholder item's save label should be clickable.");
yield teardown(panel);
finish();
}

View File

@ -0,0 +1,55 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests that the frontend UI is properly reconfigured after reloading.
*/
function ifTestingSupported() {
let [target, debuggee, panel] = yield initCanavsDebuggerFrontend(SIMPLE_CANVAS_URL);
let { window, $, EVENTS } = panel.panelWin;
let reset = once(window, EVENTS.UI_RESET);
let navigated = reload(target);
yield reset;
ok(true, "The UI was reset after the refresh button was clicked.");
yield navigated;
ok(true, "The target finished reloading.");
is($("#snapshots-pane").hasAttribute("hidden"), false,
"The snapshots pane should still be visible.");
is($("#debugging-pane").hasAttribute("hidden"), false,
"The debugging pane should still be visible.");
is($("#record-snapshot").hasAttribute("checked"), false,
"The 'record snapshot' button should not be checked.");
is($("#record-snapshot").hasAttribute("disabled"), false,
"The 'record snapshot' button should not be disabled.");
is($("#record-snapshot").hasAttribute("hidden"), false,
"The 'record snapshot' button should now be visible.");
is($("#import-snapshot").hasAttribute("hidden"), false,
"The 'import snapshot' button should still be visible.");
is($("#clear-snapshots").hasAttribute("hidden"), false,
"The 'clear snapshots' button should still be visible.");
is($("#reload-notice").getAttribute("hidden"), "true",
"The reload notice should now be hidden.");
is($("#empty-notice").hasAttribute("hidden"), false,
"The empty notice should now be visible.");
is($("#import-notice").getAttribute("hidden"), "true",
"The import notice should now be hidden.");
is($("#snapshot-filmstrip").getAttribute("hidden"), "true",
"The snapshot filmstrip should still be hidden.");
is($("#screenshot-container").getAttribute("hidden"), "true",
"The screenshot container should still be hidden.");
is($("#debugging-pane-contents").getAttribute("hidden"), "true",
"The rest of the UI should still be hidden.");
yield teardown(panel);
finish();
}

View File

@ -0,0 +1,70 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests that the frontend UI is properly reconfigured after reloading.
*/
function ifTestingSupported() {
let [target, debuggee, panel] = yield initCanavsDebuggerFrontend(SIMPLE_CANVAS_URL);
let { window, $, $all, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;
is(SnapshotsListView.itemCount, 0,
"There should be no snapshots initially displayed in the UI.");
is(CallsListView.itemCount, 0,
"There should be no function calls initially displayed in the UI.");
is($("#screenshot-container").hidden, true,
"The screenshot should not be initially displayed in the UI.");
is($("#snapshot-filmstrip").hidden, true,
"There should be no thumbnails initially displayed in the UI (1).");
is($all(".filmstrip-thumbnail").length, 0,
"There should be no thumbnails initially displayed in the UI (2).");
yield reload(target);
let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
let callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
let thumbnailsDisplayed = once(window, EVENTS.THUMBNAILS_DISPLAYED);
let screenshotDisplayed = once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED);
SnapshotsListView._onRecordButtonClick();
yield promise.all([
recordingFinished,
callListPopulated,
thumbnailsDisplayed,
screenshotDisplayed
]);
is(SnapshotsListView.itemCount, 1,
"There should be one snapshot displayed in the UI.");
is(CallsListView.itemCount, 8,
"All the function calls should now be displayed in the UI.");
is($("#screenshot-container").hidden, false,
"The screenshot should now be displayed in the UI.");
is($("#snapshot-filmstrip").hidden, false,
"All the thumbnails should now be displayed in the UI (1).");
is($all(".filmstrip-thumbnail").length, 4,
"All the thumbnails should now be displayed in the UI (2).");
let reset = once(window, EVENTS.UI_RESET);
let navigated = reload(target);
yield reset;
ok(true, "The UI was reset after the refresh button was clicked.");
is(SnapshotsListView.itemCount, 0,
"There should be no snapshots displayed in the UI after navigating.");
is(CallsListView.itemCount, 0,
"There should be no function calls displayed in the UI after navigating.");
is($("#snapshot-filmstrip").hidden, true,
"There should be no thumbnails displayed in the UI after navigating.");
is($("#screenshot-container").hidden, true,
"The screenshot should not be displayed in the UI after navigating.");
yield navigated;
ok(true, "The target finished reloading.");
yield teardown(panel);
finish();
}

View File

@ -0,0 +1,39 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if the slider in the calls list view works as advertised.
*/
function ifTestingSupported() {
let [target, debuggee, panel] = yield initCanavsDebuggerFrontend(SIMPLE_CANVAS_URL);
let { window, $, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;
yield reload(target);
let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
let callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
SnapshotsListView._onRecordButtonClick();
yield promise.all([recordingFinished, callListPopulated]);
is(CallsListView.selectedIndex, -1,
"No item in the function calls list should be initially selected.");
is($("#calls-slider").value, 0,
"The slider should be moved all the way to the start.");
is($("#calls-slider").min, 0,
"The slider minimum value should be 0.");
is($("#calls-slider").max, 7,
"The slider maximum value should be 7.");
CallsListView.selectedIndex = 1;
is($("#calls-slider").value, 1,
"The slider should be changed according to the current selection.");
$("#calls-slider").value = 2;
is(CallsListView.selectedIndex, 2,
"The calls selection should be changed according to the current slider value.");
yield teardown(panel);
finish();
}

View File

@ -0,0 +1,97 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if the slider in the calls list view works as advertised.
*/
function ifTestingSupported() {
let [target, debuggee, panel] = yield initCanavsDebuggerFrontend(SIMPLE_CANVAS_URL);
let { window, $, EVENTS, gFront, SnapshotsListView, CallsListView } = panel.panelWin;
yield reload(target);
let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
let callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
let thumbnailsDisplayed = once(window, EVENTS.THUMBNAILS_DISPLAYED);
SnapshotsListView._onRecordButtonClick();
yield promise.all([recordingFinished, callListPopulated, thumbnailsDisplayed]);
let firstSnapshot = SnapshotsListView.getItemAtIndex(0);
let firstSnapshotOverview = yield firstSnapshot.attachment.actor.getOverview();
let thumbnails = firstSnapshotOverview.thumbnails;
is(thumbnails.length, 4,
"There should be 4 thumbnails cached for the snapshot item.");
let thumbnailImageElementSet = waitForMozSetImageElement(window);
$("#calls-slider").value = 1;
let thumbnailPixels = yield thumbnailImageElementSet;
ok(sameArray(thumbnailPixels, thumbnails[0].pixels),
"The screenshot element should have a thumbnail as an immediate background.");
yield once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED);
ok(true, "The full-sized screenshot was displayed for the item at index 1.");
let thumbnailImageElementSet = waitForMozSetImageElement(window);
$("#calls-slider").value = 2;
let thumbnailPixels = yield thumbnailImageElementSet;
ok(sameArray(thumbnailPixels, thumbnails[1].pixels),
"The screenshot element should have a thumbnail as an immediate background.");
yield once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED);
ok(true, "The full-sized screenshot was displayed for the item at index 2.");
let thumbnailImageElementSet = waitForMozSetImageElement(window);
$("#calls-slider").value = 7;
let thumbnailPixels = yield thumbnailImageElementSet;
ok(sameArray(thumbnailPixels, thumbnails[3].pixels),
"The screenshot element should have a thumbnail as an immediate background.");
yield once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED);
ok(true, "The full-sized screenshot was displayed for the item at index 7.");
let thumbnailImageElementSet = waitForMozSetImageElement(window);
$("#calls-slider").value = 4;
let thumbnailPixels = yield thumbnailImageElementSet;
ok(sameArray(thumbnailPixels, thumbnails[2].pixels),
"The screenshot element should have a thumbnail as an immediate background.");
yield once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED);
ok(true, "The full-sized screenshot was displayed for the item at index 4.");
let thumbnailImageElementSet = waitForMozSetImageElement(window);
$("#calls-slider").value = 0;
let thumbnailPixels = yield thumbnailImageElementSet;
ok(sameArray(thumbnailPixels, thumbnails[0].pixels),
"The screenshot element should have a thumbnail as an immediate background.");
yield once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED);
ok(true, "The full-sized screenshot was displayed for the item at index 0.");
yield teardown(panel);
finish();
}
function waitForMozSetImageElement(panel) {
let deferred = promise.defer();
panel._onMozSetImageElement = deferred.resolve;
return deferred.promise;
}
function sameArray(a, b) {
if (a.length != b.length) {
return false;
}
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) {
return false;
}
}
return true;
}

View File

@ -0,0 +1,93 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if selecting snapshots in the frontend displays the appropriate data
* respective to their recorded animation frame.
*/
function ifTestingSupported() {
let [target, debuggee, panel] = yield initCanavsDebuggerFrontend(SIMPLE_CANVAS_URL);
let { window, $, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;
yield reload(target);
yield recordAndWaitForFirstSnapshot();
info("First snapshot recorded.")
is(SnapshotsListView.selectedIndex, 0,
"A snapshot should be automatically selected after first recording.");
is(CallsListView.selectedIndex, -1,
"There should be no call item automatically selected in the snapshot.");
yield recordAndWaitForAnotherSnapshot();
info("Second snapshot recorded.")
is(SnapshotsListView.selectedIndex, 0,
"A snapshot should not be automatically selected after another recording.");
is(CallsListView.selectedIndex, -1,
"There should still be no call item automatically selected in the snapshot.");
let secondSnapshotTarget = SnapshotsListView.getItemAtIndex(1).target;
let snapshotSelected = waitForSnapshotSelection();
EventUtils.sendMouseEvent({ type: "mousedown" }, secondSnapshotTarget, window);
yield snapshotSelected;
info("Second snapshot selected.");
is(SnapshotsListView.selectedIndex, 1,
"The second snapshot should now be selected.");
is(CallsListView.selectedIndex, -1,
"There should still be no call item automatically selected in the snapshot.");
let firstDrawCallContents = $(".call-item-contents", CallsListView.getItemAtIndex(2).target);
let screenshotDisplayed = once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED);
EventUtils.sendMouseEvent({ type: "mousedown" }, firstDrawCallContents, window);
yield screenshotDisplayed;
info("First draw call in the second snapshot selected.");
is(SnapshotsListView.selectedIndex, 1,
"The second snapshot should still be selected.");
is(CallsListView.selectedIndex, 2,
"The first draw call should now be selected in the snapshot.");
let firstSnapshotTarget = SnapshotsListView.getItemAtIndex(0).target;
let snapshotSelected = waitForSnapshotSelection();
EventUtils.sendMouseEvent({ type: "mousedown" }, firstSnapshotTarget, window);
yield snapshotSelected;
info("First snapshot re-selected.");
is(SnapshotsListView.selectedIndex, 0,
"The first snapshot should now be re-selected.");
is(CallsListView.selectedIndex, -1,
"There should still be no call item automatically selected in the snapshot.");
function recordAndWaitForFirstSnapshot() {
let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
let snapshotSelected = waitForSnapshotSelection();
SnapshotsListView._onRecordButtonClick();
return promise.all([recordingFinished, snapshotSelected]);
}
function recordAndWaitForAnotherSnapshot() {
let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
SnapshotsListView._onRecordButtonClick();
return recordingFinished;
}
function waitForSnapshotSelection() {
let callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
let thumbnailsDisplayed = once(window, EVENTS.THUMBNAILS_DISPLAYED);
let screenshotDisplayed = once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED);
return promise.all([
callListPopulated,
thumbnailsDisplayed,
screenshotDisplayed
]);
}
yield teardown(panel);
finish();
}

View File

@ -0,0 +1,76 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if the stepping buttons in the call list toolbar work as advertised.
*/
function ifTestingSupported() {
let [target, debuggee, panel] = yield initCanavsDebuggerFrontend(SIMPLE_CANVAS_URL);
let { window, $, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;
yield reload(target);
let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
let callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
SnapshotsListView._onRecordButtonClick();
yield promise.all([recordingFinished, callListPopulated]);
checkSteppingButtons(1, 1, 1, 1);
is(CallsListView.selectedIndex, -1,
"There should be no selected item in the calls list view initially.");
CallsListView._onResume();
checkSteppingButtons(1, 1, 1, 1);
is(CallsListView.selectedIndex, 0,
"The first draw call should now be selected.");
CallsListView._onResume();
checkSteppingButtons(1, 1, 1, 1);
is(CallsListView.selectedIndex, 2,
"The second draw call should now be selected.");
CallsListView._onStepOver();
checkSteppingButtons(1, 1, 1, 1);
is(CallsListView.selectedIndex, 3,
"The next context call should now be selected.");
CallsListView._onStepOut();
checkSteppingButtons(0, 0, 1, 0);
is(CallsListView.selectedIndex, 7,
"The last context call should now be selected.");
function checkSteppingButtons(resume, stepOver, stepIn, stepOut) {
if (!resume) {
is($("#resume").getAttribute("disabled"), "true",
"The resume button doesn't have the expected disabled state.");
} else {
is($("#resume").hasAttribute("disabled"), false,
"The resume button doesn't have the expected enabled state.");
}
if (!stepOver) {
is($("#step-over").getAttribute("disabled"), "true",
"The stepOver button doesn't have the expected disabled state.");
} else {
is($("#step-over").hasAttribute("disabled"), false,
"The stepOver button doesn't have the expected enabled state.");
}
if (!stepIn) {
is($("#step-in").getAttribute("disabled"), "true",
"The stepIn button doesn't have the expected disabled state.");
} else {
is($("#step-in").hasAttribute("disabled"), false,
"The stepIn button doesn't have the expected enabled state.");
}
if (!stepOut) {
is($("#step-out").getAttribute("disabled"), "true",
"The stepOut button doesn't have the expected disabled state.");
} else {
is($("#step-out").hasAttribute("disabled"), false,
"The stepOut button doesn't have the expected enabled state.");
}
}
yield teardown(panel);
finish();
}

View File

@ -0,0 +1,46 @@
<!-- Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ -->
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<title>Canvas inspector test page</title>
</head>
<body>
<canvas width="128" height="128"></canvas>
<script type="text/javascript;version=1.8">
"use strict";
var ctx = document.querySelector("canvas").getContext("2d");
function drawRect(fill, size) {
function A() {
function B() {
function C() {
ctx.fillStyle = fill;
ctx.fillRect(size[0], size[1], size[2], size[3]);
}
C();
}
B();
}
A();
}
function drawScene() {
ctx.clearRect(0, 0, 128, 128);
drawRect("rgb(192, 192, 192)", [0, 0, 128, 128]);
drawRect("rgba(0, 0, 192, 0.5)", [30, 30, 55, 50]);
drawRect("rgba(192, 0, 0, 0.5)", [10, 10, 55, 50]);
window.requestAnimationFrame(drawScene);
}
drawScene();
</script>
</body>
</html>

View File

@ -0,0 +1,37 @@
<!-- Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ -->
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<title>Canvas inspector test page</title>
</head>
<body>
<canvas width="128" height="128"></canvas>
<script type="text/javascript;version=1.8">
"use strict";
var ctx = document.querySelector("canvas").getContext("2d");
function drawRect(fill, size) {
ctx.fillStyle = fill;
ctx.fillRect(size[0], size[1], size[2], size[3]);
}
function drawScene() {
ctx.clearRect(0, 0, 128, 128);
drawRect("rgba(255, 255, 255, 0)", [0, 0, 128, 128]);
drawRect("rgba(0, 0, 192, 0.5)", [30, 30, 55, 50]);
drawRect("rgba(192, 0, 0, 0.5)", [10, 10, 55, 50]);
window.requestAnimationFrame(drawScene);
}
drawScene();
</script>
</body>
</html>

View File

@ -0,0 +1,37 @@
<!-- Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ -->
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<title>Canvas inspector test page</title>
</head>
<body>
<canvas width="128" height="128"></canvas>
<script type="text/javascript;version=1.8">
"use strict";
var ctx = document.querySelector("canvas").getContext("2d");
function drawRect(fill, size) {
ctx.fillStyle = fill;
ctx.fillRect(size[0], size[1], size[2], size[3]);
}
function drawScene() {
ctx.clearRect(0, 0, 128, 128);
drawRect("rgb(192, 192, 192)", [0, 0, 128, 128]);
drawRect("rgba(0, 0, 192, 0.5)", [30, 30, 55, 50]);
drawRect("rgba(192, 0, 0, 0.5)", [10, 10, 55, 50]);
window.requestAnimationFrame(drawScene);
}
drawScene();
</script>
</body>
</html>

View File

@ -0,0 +1,234 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
let { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
// Disable logging for all the tests. Both the debugger server and frontend will
// be affected by this pref.
let gEnableLogging = Services.prefs.getBoolPref("devtools.debugger.log");
Services.prefs.setBoolPref("devtools.debugger.log", false);
let { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
let { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
let { gDevTools } = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
let { DebuggerServer } = Cu.import("resource://gre/modules/devtools/dbg-server.jsm", {});
let { DebuggerClient } = Cu.import("resource://gre/modules/devtools/dbg-client.jsm", {});
let { CallWatcherFront } = devtools.require("devtools/server/actors/call-watcher");
let { CanvasFront } = devtools.require("devtools/server/actors/canvas");
let TiltGL = devtools.require("devtools/tilt/tilt-gl");
let TargetFactory = devtools.TargetFactory;
let Toolbox = devtools.Toolbox;
const EXAMPLE_URL = "http://example.com/browser/browser/devtools/canvasdebugger/test/";
const SIMPLE_CANVAS_URL = EXAMPLE_URL + "doc_simple-canvas.html";
const SIMPLE_CANVAS_TRANSPARENT_URL = EXAMPLE_URL + "doc_simple-canvas-transparent.html";
const SIMPLE_CANVAS_DEEP_STACK_URL = EXAMPLE_URL + "doc_simple-canvas-deep-stack.html";
// All tests are asynchronous.
waitForExplicitFinish();
let gToolEnabled = Services.prefs.getBoolPref("devtools.canvasdebugger.enabled");
registerCleanupFunction(() => {
info("finish() was called, cleaning up...");
Services.prefs.setBoolPref("devtools.debugger.log", gEnableLogging);
Services.prefs.setBoolPref("devtools.canvasdebugger.enabled", gToolEnabled);
// Some of yhese tests use a lot of memory due to GL contexts, so force a GC
// to help fragmentation.
info("Forcing GC after canvas debugger test.");
Cu.forceGC();
});
function addTab(aUrl, aWindow) {
info("Adding tab: " + aUrl);
let deferred = promise.defer();
let targetWindow = aWindow || window;
let targetBrowser = targetWindow.gBrowser;
targetWindow.focus();
let tab = targetBrowser.selectedTab = targetBrowser.addTab(aUrl);
let linkedBrowser = tab.linkedBrowser;
linkedBrowser.addEventListener("load", function onLoad() {
linkedBrowser.removeEventListener("load", onLoad, true);
info("Tab added and finished loading: " + aUrl);
deferred.resolve(tab);
}, true);
return deferred.promise;
}
function removeTab(aTab, aWindow) {
info("Removing tab.");
let deferred = promise.defer();
let targetWindow = aWindow || window;
let targetBrowser = targetWindow.gBrowser;
let tabContainer = targetBrowser.tabContainer;
tabContainer.addEventListener("TabClose", function onClose(aEvent) {
tabContainer.removeEventListener("TabClose", onClose, false);
info("Tab removed and finished closing.");
deferred.resolve();
}, false);
targetBrowser.removeTab(aTab);
return deferred.promise;
}
function handleError(aError) {
ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
finish();
}
let gRequiresWebGL = false;
function ifTestingSupported() {
ok(false, "You need to define a 'ifTestingSupported' function.");
finish();
}
function ifTestingUnsupported() {
todo(false, "Skipping test because some required functionality isn't supported.");
finish();
}
function test() {
let generator = isTestingSupported() ? ifTestingSupported : ifTestingUnsupported;
Task.spawn(generator).then(null, handleError);
}
function createCanvas() {
return document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
}
function isTestingSupported() {
if (!gRequiresWebGL) {
info("This test does not require WebGL support.");
return true;
}
let supported =
!TiltGL.isWebGLForceEnabled() &&
TiltGL.isWebGLSupported() &&
TiltGL.create3DContext(createCanvas());
info("This test requires WebGL support.");
info("Apparently, WebGL is" + (supported ? "" : " not") + " supported.");
return supported;
}
function once(aTarget, aEventName, aUseCapture = false) {
info("Waiting for event: '" + aEventName + "' on " + aTarget + ".");
let deferred = promise.defer();
for (let [add, remove] of [
["on", "off"], // Use event emitter before DOM events for consistency
["addEventListener", "removeEventListener"],
["addListener", "removeListener"]
]) {
if ((add in aTarget) && (remove in aTarget)) {
aTarget[add](aEventName, function onEvent(...aArgs) {
aTarget[remove](aEventName, onEvent, aUseCapture);
deferred.resolve(...aArgs);
}, aUseCapture);
break;
}
}
return deferred.promise;
}
function waitForTick() {
let deferred = promise.defer();
executeSoon(deferred.resolve);
return deferred.promise;
}
function navigateInHistory(aTarget, aDirection, aWaitForTargetEvent = "navigate") {
executeSoon(() => content.history[aDirection]());
return once(aTarget, aWaitForTargetEvent);
}
function navigate(aTarget, aUrl, aWaitForTargetEvent = "navigate") {
executeSoon(() => aTarget.activeTab.navigateTo(aUrl));
return once(aTarget, aWaitForTargetEvent);
}
function reload(aTarget, aWaitForTargetEvent = "navigate") {
executeSoon(() => aTarget.activeTab.reload());
return once(aTarget, aWaitForTargetEvent);
}
function initServer() {
if (!DebuggerServer.initialized) {
DebuggerServer.init(() => true);
DebuggerServer.addBrowserActors();
}
}
function initCallWatcherBackend(aUrl) {
info("Initializing a call watcher front.");
initServer();
return Task.spawn(function*() {
let tab = yield addTab(aUrl);
let target = TargetFactory.forTab(tab);
let debuggee = target.window.wrappedJSObject;
yield target.makeRemote();
let front = new CallWatcherFront(target.client, target.form);
return [target, debuggee, front];
});
}
function initCanavsDebuggerBackend(aUrl) {
info("Initializing a canvas debugger front.");
initServer();
return Task.spawn(function*() {
let tab = yield addTab(aUrl);
let target = TargetFactory.forTab(tab);
let debuggee = target.window.wrappedJSObject;
yield target.makeRemote();
let front = new CanvasFront(target.client, target.form);
return [target, debuggee, front];
});
}
function initCanavsDebuggerFrontend(aUrl) {
info("Initializing a canvas debugger pane.");
return Task.spawn(function*() {
let tab = yield addTab(aUrl);
let target = TargetFactory.forTab(tab);
let debuggee = target.window.wrappedJSObject;
yield target.makeRemote();
Services.prefs.setBoolPref("devtools.canvasdebugger.enabled", true);
let toolbox = yield gDevTools.showToolbox(target, "canvasdebugger");
let panel = toolbox.getCurrentPanel();
return [target, debuggee, panel];
});
}
function teardown(aPanel) {
info("Destroying the specified canvas debugger.");
return promise.all([
once(aPanel, "destroyed"),
removeTab(aPanel.target.tab)
]);
}

View File

@ -0,0 +1,6 @@
# vim: set filetype=python:
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
BROWSER_CHROME_MANIFESTS += ['browser.ini']

View File

@ -390,8 +390,7 @@ let DebuggerView = {
this._setEditorText(L10N.getStr("loadingText"));
this._editorSource = { url: aSource.url, promise: deferred.promise };
DebuggerController.SourceScripts.getText(aSource)
.then(([, aText, aContentType]) => {
DebuggerController.SourceScripts.getText(aSource).then(([, aText, aContentType]) => {
// Avoid setting an unexpected source. This may happen when switching
// very fast between sources that haven't been fetched yet.
if (this._editorSource.url != aSource.url) {
@ -469,8 +468,7 @@ let DebuggerView = {
// Make sure the requested source client is shown in the editor, then
// update the source editor's caret position and debug location.
return this._setEditorSource(sourceForm, aFlags)
.then(([,, aContentType]) => {
return this._setEditorSource(sourceForm, aFlags).then(([,, aContentType]) => {
// Record the contentType learned from fetching
sourceForm.contentType = aContentType;
// Line numbers in the source editor should start from 1. If invalid

View File

@ -434,7 +434,7 @@
<tabpanels flex="1">
<tabpanel id="variables-tabpanel">
<vbox id="expressions"/>
<splitter class="devtools-horizontal-splitter devtools-invisible splitter"/>
<splitter class="devtools-horizontal-splitter devtools-invisible-splitter"/>
<vbox id="variables" flex="1"/>
</tabpanel>
<tabpanel id="events-tabpanel">

View File

@ -8,7 +8,6 @@
const { Cc, Ci, Cu, Cr } = require("chrome");
const promise = require("sdk/core/promise");
const EventEmitter = require("devtools/toolkit/event-emitter");
const { DevToolsUtils } = Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm", {});
function DebuggerPanel(iframeWindow, toolbox) {
@ -60,7 +59,7 @@ DebuggerPanel.prototype = {
return this;
})
.then(null, function onError(aReason) {
DevToolsUtils.reportException("DebuggerPane.prototype.open", aReason);
DevToolsUtils.reportException("DebuggerPanel.prototype.open", aReason);
});
},

View File

@ -245,8 +245,7 @@ function waitForSourceShown(aPanel, aUrl) {
}
function waitForEditorLocationSet(aPanel) {
return waitForDebuggerEvents(aPanel,
aPanel.panelWin.EVENTS.EDITOR_LOCATION_SET);
return waitForDebuggerEvents(aPanel, aPanel.panelWin.EVENTS.EDITOR_LOCATION_SET);
}
function ensureSourceIs(aPanel, aUrl, aWaitFlag = false) {

View File

@ -60,6 +60,8 @@ browser.jar:
content/browser/devtools/debugger-panes.js (debugger/debugger-panes.js)
content/browser/devtools/shadereditor.xul (shadereditor/shadereditor.xul)
content/browser/devtools/shadereditor.js (shadereditor/shadereditor.js)
content/browser/devtools/canvasdebugger.xul (canvasdebugger/canvasdebugger.xul)
content/browser/devtools/canvasdebugger.js (canvasdebugger/canvasdebugger.js)
content/browser/devtools/profiler.xul (profiler/profiler.xul)
content/browser/devtools/cleopatra.html (profiler/cleopatra/cleopatra.html)
content/browser/devtools/profiler/cleopatra/css/ui.css (profiler/cleopatra/css/ui.css)

View File

@ -28,6 +28,7 @@ loader.lazyGetter(this, "WebConsolePanel", () => require("devtools/webconsole/pa
loader.lazyGetter(this, "DebuggerPanel", () => require("devtools/debugger/panel").DebuggerPanel);
loader.lazyGetter(this, "StyleEditorPanel", () => require("devtools/styleeditor/styleeditor-panel").StyleEditorPanel);
loader.lazyGetter(this, "ShaderEditorPanel", () => require("devtools/shadereditor/panel").ShaderEditorPanel);
loader.lazyGetter(this, "CanvasDebuggerPanel", () => require("devtools/canvasdebugger/panel").CanvasDebuggerPanel);
loader.lazyGetter(this, "ProfilerPanel", () => require("devtools/profiler/panel"));
loader.lazyGetter(this, "NetMonitorPanel", () => require("devtools/netmonitor/panel").NetMonitorPanel);
loader.lazyGetter(this, "ScratchpadPanel", () => require("devtools/scratchpad/scratchpad-panel").ScratchpadPanel);
@ -38,6 +39,7 @@ const inspectorProps = "chrome://browser/locale/devtools/inspector.properties";
const debuggerProps = "chrome://browser/locale/devtools/debugger.properties";
const styleEditorProps = "chrome://browser/locale/devtools/styleeditor.properties";
const shaderEditorProps = "chrome://browser/locale/devtools/shadereditor.properties";
const canvasDebuggerProps = "chrome://browser/locale/devtools/canvasdebugger.properties";
const webConsoleProps = "chrome://browser/locale/devtools/webconsole.properties";
const profilerProps = "chrome://browser/locale/devtools/profiler.properties";
const netMonitorProps = "chrome://browser/locale/devtools/netmonitor.properties";
@ -47,6 +49,7 @@ loader.lazyGetter(this, "webConsoleStrings", () => Services.strings.createBundle
loader.lazyGetter(this, "debuggerStrings", () => Services.strings.createBundle(debuggerProps));
loader.lazyGetter(this, "styleEditorStrings", () => Services.strings.createBundle(styleEditorProps));
loader.lazyGetter(this, "shaderEditorStrings", () => Services.strings.createBundle(shaderEditorProps));
loader.lazyGetter(this, "canvasDebuggerStrings", () => Services.strings.createBundle(canvasDebuggerProps));
loader.lazyGetter(this, "inspectorStrings", () => Services.strings.createBundle(inspectorProps));
loader.lazyGetter(this, "profilerStrings",() => Services.strings.createBundle(profilerProps));
loader.lazyGetter(this, "netMonitorStrings", () => Services.strings.createBundle(netMonitorProps));
@ -200,11 +203,31 @@ Tools.shaderEditor = {
}
};
Tools.canvasDebugger = {
id: "canvasdebugger",
ordinal: 6,
visibilityswitch: "devtools.canvasdebugger.enabled",
icon: "chrome://browser/skin/devtools/tool-styleeditor.svg",
invertIconForLightTheme: true,
url: "chrome://browser/content/devtools/canvasdebugger.xul",
label: l10n("ToolboxCanvasDebugger.label", canvasDebuggerStrings),
tooltip: l10n("ToolboxCanvasDebugger.tooltip", canvasDebuggerStrings),
isTargetSupported: function(target) {
return true;
},
build: function(iframeWindow, toolbox) {
let panel = new CanvasDebuggerPanel(iframeWindow, toolbox);
return panel.open();
}
};
Tools.jsprofiler = {
id: "jsprofiler",
accesskey: l10n("profiler.accesskey", profilerStrings),
key: l10n("profiler2.commandkey", profilerStrings),
ordinal: 6,
ordinal: 7,
modifiers: "shift",
visibilityswitch: "devtools.profiler.enabled",
icon: "chrome://browser/skin/devtools/tool-profiler.svg",
@ -228,7 +251,7 @@ Tools.netMonitor = {
id: "netmonitor",
accesskey: l10n("netmonitor.accesskey", netMonitorStrings),
key: l10n("netmonitor.commandkey", netMonitorStrings),
ordinal: 7,
ordinal: 8,
modifiers: osString == "Darwin" ? "accel,alt" : "accel,shift",
visibilityswitch: "devtools.netmonitor.enabled",
icon: "chrome://browser/skin/devtools/tool-network.svg",
@ -251,7 +274,7 @@ Tools.netMonitor = {
Tools.scratchpad = {
id: "scratchpad",
ordinal: 8,
ordinal: 9,
visibilityswitch: "devtools.scratchpad.enabled",
icon: "chrome://browser/skin/devtools/tool-scratchpad.svg",
invertIconForLightTheme: true,
@ -277,6 +300,7 @@ let defaultTools = [
Tools.jsdebugger,
Tools.styleEditor,
Tools.shaderEditor,
Tools.canvasDebugger,
Tools.jsprofiler,
Tools.netMonitor,
Tools.scratchpad

View File

@ -6,6 +6,7 @@
DIRS += [
'app-manager',
'canvasdebugger',
'commandline',
'debugger',
'fontinspector',

View File

@ -117,6 +117,9 @@ const {Tooltip} = require("devtools/shared/widgets/Tooltip");
XPCOMUtils.defineLazyModuleGetter(this, "Chart",
"resource:///modules/devtools/Chart.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Curl",
"resource:///modules/devtools/Curl.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
@ -126,23 +129,17 @@ XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
XPCOMUtils.defineLazyModuleGetter(this, "DevToolsUtils",
"resource://gre/modules/devtools/DevToolsUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "devtools",
"resource://gre/modules/devtools/Loader.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper",
"@mozilla.org/widget/clipboardhelper;1", "nsIClipboardHelper");
Object.defineProperty(this, "NetworkHelper", {
get: function() {
return devtools.require("devtools/toolkit/webconsole/network-helper");
return require("devtools/toolkit/webconsole/network-helper");
},
configurable: true,
enumerable: true
});
XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper",
"@mozilla.org/widget/clipboardhelper;1", "nsIClipboardHelper");
XPCOMUtils.defineLazyModuleGetter(this, "Curl",
"resource:///modules/devtools/Curl.jsm");
/**
* Object defining the network monitor controller components.
*/

View File

@ -8,6 +8,7 @@
const { Cc, Ci, Cu, Cr } = require("chrome");
const { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
const EventEmitter = require("devtools/toolkit/event-emitter");
const { DevToolsUtils } = Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm", {});
function NetMonitorPanel(iframeWindow, toolbox) {
this.panelWin = iframeWindow;
@ -49,8 +50,7 @@ NetMonitorPanel.prototype = {
return this;
})
.then(null, function onError(aReason) {
Cu.reportError("NetMonitorPanel open failed. " +
aReason.error + ": " + aReason.message);
DevToolsUtils.reportException("NetMonitorPanel.prototype.open", aReason);
});
},

View File

@ -10,4 +10,3 @@ JS_MODULES_PATH = 'modules/devtools/shadereditor'
EXTRA_JS_MODULES += [
'panel.js'
]

View File

@ -9,6 +9,7 @@ const { Cc, Ci, Cu, Cr } = require("chrome");
const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
const EventEmitter = require("devtools/toolkit/event-emitter");
const { WebGLFront } = require("devtools/server/actors/webgl");
const { DevToolsUtils } = Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm", {});
function ShaderEditorPanel(iframeWindow, toolbox) {
this.panelWin = iframeWindow;
@ -21,6 +22,12 @@ function ShaderEditorPanel(iframeWindow, toolbox) {
exports.ShaderEditorPanel = ShaderEditorPanel;
ShaderEditorPanel.prototype = {
/**
* Open is effectively an asynchronous constructor.
*
* @return object
* A promise that is resolved when the Shader Editor completes opening.
*/
open: function() {
let targetPromise;
@ -44,8 +51,7 @@ ShaderEditorPanel.prototype = {
return this;
})
.then(null, function onError(aReason) {
Cu.reportError("ShaderEditorPanel open failed. " +
aReason.error + ": " + aReason.message);
DevToolsUtils.reportException("ShaderEditorPanel.prototype.open", aReason);
});
},

View File

@ -8,7 +8,6 @@ const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/devtools/Loader.jsm");
Cu.import("resource:///modules/devtools/SideMenuWidget.jsm");
Cu.import("resource:///modules/devtools/ViewHelpers.jsm");

View File

@ -20,7 +20,8 @@ Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm");
this.EXPORTED_SYMBOLS = [
"Heritage", "ViewHelpers", "WidgetMethods",
"setNamedTimeout", "clearNamedTimeout"
"setNamedTimeout", "clearNamedTimeout",
"setConditionalTimeout", "clearConditionalTimeout",
];
/**
@ -57,7 +58,7 @@ this.Heritage = {
* @param function aCallback
* Invoked when no more events are fired after the specified time.
*/
this.setNamedTimeout = function(aId, aWait, aCallback) {
this.setNamedTimeout = function setNamedTimeout(aId, aWait, aCallback) {
clearNamedTimeout(aId);
namedTimeoutsStore.set(aId, setTimeout(() =>
@ -71,7 +72,7 @@ this.setNamedTimeout = function(aId, aWait, aCallback) {
* @param string aId
* A string identifier for the named timeout.
*/
this.clearNamedTimeout = function(aId) {
this.clearNamedTimeout = function clearNamedTimeout(aId) {
if (!namedTimeoutsStore) {
return;
}
@ -79,6 +80,41 @@ this.clearNamedTimeout = function(aId) {
namedTimeoutsStore.delete(aId);
};
/**
* Same as `setNamedTimeout`, but invokes the callback only if the provided
* predicate function returns true. Otherwise, the timeout is re-triggered.
*
* @param string aId
* A string identifier for the conditional timeout.
* @param number aWait
* The amount of milliseconds to wait after no more events are fired.
* @param function aPredicate
* The predicate function used to determine whether the timeout restarts.
* @param function aCallback
* Invoked when no more events are fired after the specified time, and
* the provided predicate function returns true.
*/
this.setConditionalTimeout = function setConditionalTimeout(aId, aWait, aPredicate, aCallback) {
setNamedTimeout(aId, aWait, function maybeCallback() {
if (aPredicate()) {
aCallback();
return;
}
setConditionalTimeout(aId, aWait, aPredicate, aCallback);
});
};
/**
* Clears a conditional timeout.
* @see setConditionalTimeout
*
* @param string aId
* A string identifier for the conditional timeout.
*/
this.clearConditionalTimeout = function clearConditionalTimeout(aId) {
clearNamedTimeout(aId);
};
XPCOMUtils.defineLazyGetter(this, "namedTimeoutsStore", () => new Map());
/**

View File

@ -95,6 +95,9 @@ const gPrefsTelemetry = new Preferences(PREF_BRANCH_TELEMETRY);
let gExperimentsEnabled = false;
let gExperiments = null;
let gLogAppenderDump = null;
let gPolicyCounter = 0;
let gExperimentsCounter = 0;
let gExperimentEntryCounter = 0;
let gLogger;
let gLogDumping = false;
@ -236,6 +239,9 @@ let Experiments = {
*/
Experiments.Policy = function () {
this._log = Log.repository.getLoggerWithMessagePrefix(
"Browser.Experiments.Policy",
"Policy #" + gPolicyCounter++ + "::");
};
Experiments.Policy.prototype = {
@ -247,7 +253,7 @@ Experiments.Policy.prototype = {
let pref = gPrefs.get(PREF_FORCE_SAMPLE);
if (pref !== undefined) {
let val = Number.parseFloat(pref);
gLogger.debug("Experiments::Policy::random sample forced: " + val);
this._log.debug("random sample forced: " + val);
if (IsNaN(val) || val < 0) {
return 0;
}
@ -301,6 +307,10 @@ Experiments.Policy.prototype = {
*/
Experiments.Experiments = function (policy=new Experiments.Policy()) {
this._log = Log.repository.getLoggerWithMessagePrefix(
"Browser.Experiments.Experiments",
"Experiments #" + gExperimentsCounter++ + "::");
this._policy = policy;
// This is a Map of (string -> ExperimentEntry), keyed with the experiment id.
@ -340,7 +350,7 @@ Experiments.Experiments.prototype = {
configureLogging();
gExperimentsEnabled = gPrefs.get(PREF_ENABLED, false);
gLogger.trace("enabled="+gExperimentsEnabled+", "+this.enabled);
this._log.trace("enabled=" + gExperimentsEnabled + ", " + this.enabled);
gPrefs.observe(PREF_LOGGING, configureLogging);
gPrefs.observe(PREF_MANIFEST_URI, this.updateManifest, this);
@ -357,12 +367,12 @@ Experiments.Experiments.prototype = {
this._loadTask = Task.spawn(this._loadFromCache.bind(this));
this._loadTask.then(
() => {
gLogger.trace("Experiments::_loadTask finished ok");
this._log.trace("_loadTask finished ok");
this._loadTask = null;
this._run();
},
(e) => {
gLogger.error("Experiments::_loadFromCache caught error: " + e);
this._log.error("_loadFromCache caught error: " + e);
}
);
},
@ -414,12 +424,12 @@ Experiments.Experiments.prototype = {
* Toggle whether the experiments feature is enabled or not.
*/
set enabled(enabled) {
gLogger.trace("Experiments::set enabled(" + enabled + ")");
this._log.trace("set enabled(" + enabled + ")");
gPrefs.set(PREF_ENABLED, enabled);
},
_toggleExperimentsEnabled: function (enabled) {
gLogger.trace("Experiments::_toggleExperimentsEnabled(" + enabled + ")");
this._log.trace("_toggleExperimentsEnabled(" + enabled + ")");
let wasEnabled = gExperimentsEnabled;
gExperimentsEnabled = enabled && telemetryEnabled();
@ -525,18 +535,18 @@ Experiments.Experiments.prototype = {
},
_run: function() {
gLogger.trace("Experiments::_run");
this._log.trace("_run");
this._checkForShutdown();
if (!this._mainTask) {
this._mainTask = Task.spawn(this._main.bind(this));
this._mainTask.then(
() => {
gLogger.trace("Experiments::_main finished, scheduling next run");
this._log.trace("_main finished, scheduling next run");
this._mainTask = null;
this._scheduleNextRun();
},
(e) => {
gLogger.error("Experiments::_main caught error: " + e);
this._log.error("_main caught error: " + e);
this._mainTask = null;
}
);
@ -546,7 +556,7 @@ Experiments.Experiments.prototype = {
_main: function*() {
do {
gLogger.trace("Experiments::_main iteration");
this._log.trace("_main iteration");
yield this._loadTask;
if (this._refresh) {
yield this._loadManifest();
@ -562,7 +572,7 @@ Experiments.Experiments.prototype = {
},
_loadManifest: function*() {
gLogger.trace("Experiments::_loadManifest");
this._log.trace("_loadManifest");
let uri = Services.urlFormatter.formatURLPref(PREF_BRANCH + PREF_MANIFEST_URI);
this._checkForShutdown();
@ -570,7 +580,7 @@ Experiments.Experiments.prototype = {
this._refresh = false;
try {
let responseText = yield this._httpGetRequest(uri);
gLogger.trace("Experiments::_loadManifest() - responseText=\"" + responseText + "\"");
this._log.trace("_loadManifest() - responseText=\"" + responseText + "\"");
if (this._shutdown) {
return;
@ -579,7 +589,7 @@ Experiments.Experiments.prototype = {
let data = JSON.parse(responseText);
this._updateExperiments(data);
} catch (e) {
gLogger.error("Experiments::_loadManifest - failure to fetch/parse manifest (continuing anyway): " + e);
this._log.error("_loadManifest - failure to fetch/parse manifest (continuing anyway): " + e);
}
},
@ -591,7 +601,7 @@ Experiments.Experiments.prototype = {
* The promise is resolved when the manifest and experiment list is updated.
*/
updateManifest: function () {
gLogger.trace("Experiments::updateManifest()");
this._log.trace("updateManifest()");
if (!gExperimentsEnabled) {
return Promise.reject(new Error("experiments are disabled"));
@ -606,13 +616,15 @@ Experiments.Experiments.prototype = {
},
notify: function (timer) {
gLogger.trace("Experiments::notify()");
this._log.trace("notify()");
this._checkForShutdown();
return this._run();
},
// START OF ADD-ON LISTENERS
onDisabled: function (addon) {
gLogger.trace("Experiments::onDisabled() - addon id: " + addon.id);
this._log.trace("onDisabled() - addon id: " + addon.id);
if (addon.id == this._pendingUninstall) {
return;
}
@ -624,9 +636,9 @@ Experiments.Experiments.prototype = {
},
onUninstalled: function (addon) {
gLogger.trace("Experiments::onUninstalled() - addon id: " + addon.id);
this._log.trace("onUninstalled() - addon id: " + addon.id);
if (addon.id == this._pendingUninstall) {
gLogger.trace("onUninstalled: matches pending uninstall");
this._log.trace("matches pending uninstall");
return;
}
let activeExperiment = this._getActiveExperiment();
@ -636,6 +648,8 @@ Experiments.Experiments.prototype = {
this.disableExperiment();
},
// END OF ADD-ON LISTENERS.
_getExperimentByAddonId: function (addonId) {
for (let [, entry] of this._experiments) {
if (entry._addonId === addonId) {
@ -651,25 +665,26 @@ Experiments.Experiments.prototype = {
* the responseText when the request is complete.
*/
_httpGetRequest: function (url) {
gLogger.trace("Experiments::httpGetRequest(" + url + ")");
this._log.trace("httpGetRequest(" + url + ")");
let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
try {
xhr.open("GET", url);
} catch (e) {
gLogger.error("Experiments::httpGetRequest() - Error opening request to " + url + ": " + e);
this._log.error("httpGetRequest() - Error opening request to " + url + ": " + e);
return Promise.reject(new Error("Experiments - Error opening XHR for " + url));
}
let deferred = Promise.defer();
let log = this._log;
xhr.onerror = function (e) {
gLogger.error("Experiments::httpGetRequest::onError() - Error making request to " + url + ": " + e.error);
log.error("httpGetRequest::onError() - Error making request to " + url + ": " + e.error);
deferred.reject(new Error("Experiments - XHR error for " + url + " - " + e.error));
};
xhr.onload = function (event) {
if (xhr.status !== 200 && xhr.state !== 0) {
gLogger.error("Experiments::httpGetRequest::onLoad() - Request to " + url + " returned status " + xhr.status);
log.error("httpGetRequest::onLoad() - Request to " + url + " returned status " + xhr.status);
deferred.reject(new Error("Experiments - XHR status for " + url + " is " + xhr.status));
return;
}
@ -683,7 +698,7 @@ Experiments.Experiments.prototype = {
CertUtils.checkCert(xhr.channel, allowNonBuiltin, certs);
}
catch (e) {
gLogger.error("Experiments: manifest fetch failed certificate checks", [e]);
log.error("manifest fetch failed certificate checks", [e]);
deferred.reject(new Error("Experiments - manifest fetch failed certificate checks: " + e));
return;
}
@ -710,7 +725,7 @@ Experiments.Experiments.prototype = {
* Part of the main task to save the cache to disk, called from _main.
*/
_saveToCache: function* () {
gLogger.trace("Experiments::_saveToCache");
this._log.trace("_saveToCache");
let path = this._cacheFilePath;
let textData = JSON.stringify({
version: CACHE_VERSION,
@ -722,14 +737,14 @@ Experiments.Experiments.prototype = {
let options = { tmpPath: path + ".tmp", compression: "lz4" };
yield OS.File.writeAtomic(path, data, options);
this._dirty = false;
gLogger.debug("Experiments._saveToCache saved to " + path);
this._log.debug("_saveToCache saved to " + path);
},
/*
* Task function, load the cached experiments manifest file from disk.
*/
_loadFromCache: function*() {
gLogger.trace("Experiments::_loadFromCache");
this._log.trace("_loadFromCache");
let path = this._cacheFilePath;
try {
let result = yield loadJSONAsync(path, { compression: "lz4" });
@ -741,7 +756,7 @@ Experiments.Experiments.prototype = {
},
_populateFromCache: function (data) {
gLogger.trace("Experiments::populateFromCache() - data: " + JSON.stringify(data));
this._log.trace("populateFromCache() - data: " + JSON.stringify(data));
// If the user has a newer cache version than we can understand, we fail
// hard; no experiments should be active in this older client.
@ -766,10 +781,10 @@ Experiments.Experiments.prototype = {
* array in the manifest
*/
_updateExperiments: function (manifestObject) {
gLogger.trace("Experiments::_updateExperiments() - experiments: " + JSON.stringify(manifestObject));
this._log.trace("_updateExperiments() - experiments: " + JSON.stringify(manifestObject));
if (manifestObject.version !== MANIFEST_VERSION) {
gLogger.warning("Experiments::updateExperiments() - unsupported version " + manifestObject.version);
this._log.warning("updateExperiments() - unsupported version " + manifestObject.version);
}
let experiments = new Map(); // The new experiments map
@ -780,7 +795,7 @@ Experiments.Experiments.prototype = {
if (entry) {
if (!entry.updateFromManifestData(data)) {
gLogger.error("Experiments::updateExperiments() - Invalid manifest data for " + data.id);
this._log.error("updateExperiments() - Invalid manifest data for " + data.id);
continue;
}
} else {
@ -801,7 +816,7 @@ Experiments.Experiments.prototype = {
// We remove them after KEEP_HISTORY_N_DAYS.
for (let [id, entry] of this._experiments) {
if (experiments.has(id) || !entry.startDate || entry.shouldDiscard()) {
gLogger.trace("Experiments::updateExperiments() - discarding entry for " + id);
this._log.trace("updateExperiments() - discarding entry for " + id);
continue;
}
@ -820,7 +835,7 @@ Experiments.Experiments.prototype = {
}
if (enabled.length > 1) {
gLogger.error("Experiments::getActiveExperimentId() - should not have more than 1 active experiment");
this._log.error("getActiveExperimentId() - should not have more than 1 active experiment");
throw new Error("have more than 1 active experiment");
}
@ -834,7 +849,7 @@ Experiments.Experiments.prototype = {
* @return Promise<> Promise that will get resolved once the task is done or failed.
*/
disableExperiment: function (userDisabled=true) {
gLogger.trace("Experiments::disableExperiment()");
this._log.trace("disableExperiment()");
this._terminateReason = userDisabled ? TELEMETRY_LOG.TERMINATION.USERDISABLED : TELEMETRY_LOG.TERMINATION.FROM_API;
return this._run();
@ -845,10 +860,34 @@ Experiments.Experiments.prototype = {
* experiment if needed and activate the first applicable candidate.
*/
_evaluateExperiments: function*() {
gLogger.trace("Experiments::_evaluateExperiments");
this._log.trace("_evaluateExperiments");
this._checkForShutdown();
// The first thing we do is reconcile our state against what's in the
// Addon Manager. It's possible that the Addon Manager knows of experiment
// add-ons that we don't. This could happen if an experiment gets installed
// when we're not listening or if there is a bug in our synchronization
// code.
//
// We have a few options of what to do with unknown experiment add-ons
// coming from the Addon Manager. Ideally, we'd convert these to
// ExperimentEntry instances and stuff them inside this._experiments.
// However, since ExperimentEntry contain lots of metadata from the
// manifest and trying to make up data could be error prone, it's safer
// to not try. Furthermore, if an experiment really did come from us, we
// should have some record of it. In the end, we decide to discard all
// knowledge for these unknown experiment add-ons.
let installedExperiments = yield installedExperimentAddons();
let expectedAddonIds = new Set([e._addonId for ([,e] of this._experiments)]);
let unknownAddons = [a for (a of installedExperiments) if (!expectedAddonIds.has(a.id))];
if (unknownAddons.length) {
this._log.warn("_evaluateExperiments() - unknown add-ons in AddonManager: " +
[a.id for (a of unknownAddons)].join(", "));
yield uninstallAddons(unknownAddons);
}
let activeExperiment = this._getActiveExperiment();
let activeChanged = false;
let now = this._policy.now();
@ -865,18 +904,18 @@ Experiments.Experiments.prototype = {
}
if (wasStopped) {
this._dirty = true;
gLogger.debug("Experiments::evaluateExperiments() - stopped experiment "
this._log.debug("evaluateExperiments() - stopped experiment "
+ activeExperiment.id);
activeExperiment = null;
activeChanged = true;
} else if (activeExperiment.needsUpdate) {
gLogger.debug("Experiments::evaluateExperiments() - updating experiment "
this._log.debug("evaluateExperiments() - updating experiment "
+ activeExperiment.id);
try {
yield activeExperiment.stop();
yield activeExperiment.start();
} catch (e) {
gLogger.error(e);
this._log.error(e);
// On failure try the next experiment.
activeExperiment = null;
}
@ -910,7 +949,7 @@ Experiments.Experiments.prototype = {
}
if (applicable) {
gLogger.debug("Experiments::evaluateExperiments() - activating experiment " + id);
this._log.debug("evaluateExperiments() - activating experiment " + id);
try {
yield experiment.start();
activeChanged = true;
@ -968,7 +1007,7 @@ Experiments.Experiments.prototype = {
return;
}
gLogger.trace("Experiments::scheduleExperimentEvaluation() - scheduling for "+time+", now: "+now);
this._log.trace("scheduleExperimentEvaluation() - scheduling for "+time+", now: "+now);
this._policy.oneshotTimer(this.notify, time - now, this, "_timer");
},
};
@ -980,6 +1019,9 @@ Experiments.Experiments.prototype = {
Experiments.ExperimentEntry = function (policy) {
this._policy = policy || new Experiments.Policy();
this._log = Log.repository.getLoggerWithMessagePrefix(
"Browser.Experiments.Experiments",
"ExperimentEntry #" + gExperimentEntryCounter++ + "::");
// Is this experiment running?
this._enabled = false;
@ -1112,7 +1154,7 @@ Experiments.ExperimentEntry.prototype = {
initFromCacheData: function (data) {
for (let key of this.SERIALIZE_KEYS) {
if (!(key in data) && !this.DATE_KEYS.has(key)) {
gLogger.error("ExperimentEntry::initFromCacheData() - missing required key " + key);
this._log.error("initFromCacheData() - missing required key " + key);
return false;
}
};
@ -1219,9 +1261,9 @@ Experiments.ExperimentEntry.prototype = {
let maxActive = data.maxActiveSeconds || 0;
let startSec = (this.startDate || 0) / 1000;
gLogger.trace("ExperimentEntry::isApplicable() - now=" + now
+ ", randomValue=" + this._randomValue
+ ", data=" + JSON.stringify(this._manifestData));
this._log.trace("isApplicable() - now=" + now
+ ", randomValue=" + this._randomValue
+ ", data=" + JSON.stringify(this._manifestData));
// Not applicable if it already ran.
@ -1273,8 +1315,8 @@ Experiments.ExperimentEntry.prototype = {
for (let check of simpleChecks) {
let result = check.condition();
if (!result) {
gLogger.debug("ExperimentEntry::isApplicable() - id="
+ data.id + " - test '" + check.name + "' failed");
this._log.debug("isApplicable() - id="
+ data.id + " - test '" + check.name + "' failed");
return Promise.reject([check.name]);
}
}
@ -1291,7 +1333,7 @@ Experiments.ExperimentEntry.prototype = {
* result (forced to boolean).
*/
_runFilterFunction: function (jsfilter) {
gLogger.trace("ExperimentEntry::runFilterFunction() - filter: " + jsfilter);
this._log.trace("runFilterFunction() - filter: " + jsfilter);
return Task.spawn(function ExperimentEntry_runFilterFunction_task() {
const nullprincipal = Cc["@mozilla.org/nullprincipal;1"].createInstance(Ci.nsIPrincipal);
@ -1308,7 +1350,7 @@ Experiments.ExperimentEntry.prototype = {
try {
Cu.evalInSandbox(jsfilter, sandbox);
} catch (e) {
gLogger.error("ExperimentEntry::runFilterFunction() - failed to eval jsfilter: " + e.message);
this._log.error("runFilterFunction() - failed to eval jsfilter: " + e.message);
throw ["jsfilter-evalfailed"];
}
@ -1323,7 +1365,7 @@ Experiments.ExperimentEntry.prototype = {
result = !!Cu.evalInSandbox("filter({healthReportPayload: JSON.parse(_hr), telemetryPayload: JSON.parse(_t)})", sandbox);
}
catch (e) {
gLogger.debug("ExperimentEntry::runFilterFunction() - filter function failed: "
this._log.debug("runFilterFunction() - filter function failed: "
+ e.message + ", " + e.stack);
throw ["jsfilter-threw", e.message];
}
@ -1344,13 +1386,13 @@ Experiments.ExperimentEntry.prototype = {
* @return Promise<> Resolved when the operation is complete.
*/
start: function () {
gLogger.trace("ExperimentEntry::start() for " + this.id);
this._log.trace("start() for " + this.id);
return Task.spawn(function* ExperimentEntry_start_task() {
let addons = yield installedExperimentAddons();
if (addons.length > 0) {
gLogger.error("ExperimentEntry::start() - there are already "
+ addons.length + " experiment addons installed");
this._log.error("start() - there are already "
+ addons.length + " experiment addons installed");
yield uninstallAddons(addons);
}
@ -1367,7 +1409,7 @@ Experiments.ExperimentEntry.prototype = {
let failureHandler = (install, handler) => {
let message = "AddonInstall " + handler + " for " + this.id + ", state=" +
(install.state || "?") + ", error=" + install.error;
gLogger.error("ExperimentEntry::_installAddon() - " + message);
this._log.error("_installAddon() - " + message);
this._failedStart = true;
TelemetryLog.log(TELEMETRY_LOG.ACTIVATION_KEY,
@ -1378,33 +1420,36 @@ Experiments.ExperimentEntry.prototype = {
let listener = {
onDownloadEnded: install => {
gLogger.trace("ExperimentEntry::_installAddon() - onDownloadEnded for " + this.id);
this._log.trace("_installAddon() - onDownloadEnded for " + this.id);
if (install.existingAddon) {
gLogger.warn("ExperimentEntry::_installAddon() - onDownloadEnded, addon already installed");
this._log.warn("_installAddon() - onDownloadEnded, addon already installed");
}
if (install.addon.type !== "experiment") {
gLogger.error("ExperimentEntry::_installAddon() - onDownloadEnded, wrong addon type");
this._log.error("_installAddon() - onDownloadEnded, wrong addon type");
install.cancel();
}
},
onInstallStarted: install => {
gLogger.trace("ExperimentEntry::_installAddon() - onInstallStarted for " + this.id);
this._log.trace("_installAddon() - onInstallStarted for " + this.id);
if (install.existingAddon) {
gLogger.warn("ExperimentEntry::_installAddon() - onInstallStarted, addon already installed");
this._log.warn("_installAddon() - onInstallStarted, addon already installed");
}
if (install.addon.type !== "experiment") {
gLogger.error("ExperimentEntry::_installAddon() - onInstallStarted, wrong addon type");
this._log.error("_installAddon() - onInstallStarted, wrong addon type");
return false;
}
// Experiment add-ons default to userDisabled = true.
install.addon.userDisabled = false;
},
onInstallEnded: install => {
gLogger.trace("ExperimentEntry::_installAddon() - install ended for " + this.id);
this._log.trace("_installAddon() - install ended for " + this.id);
this._lastChangedDate = this._policy.now();
this._startDate = this._policy.now();
this._enabled = true;
@ -1441,9 +1486,9 @@ Experiments.ExperimentEntry.prototype = {
* @return Promise<> Resolved when the operation is complete.
*/
stop: function (terminationKind, terminationReason) {
gLogger.trace("ExperimentEntry::stop() - id=" + this.id + ", terminationKind=" + terminationKind);
this._log.trace("stop() - id=" + this.id + ", terminationKind=" + terminationKind);
if (!this._enabled) {
gLogger.warning("ExperimentEntry::stop() - experiment not enabled: " + id);
this._log.warning("stop() - experiment not enabled: " + id);
return Promise.reject();
}
@ -1458,7 +1503,7 @@ Experiments.ExperimentEntry.prototype = {
AddonManager.getAddonByID(this._addonId, addon => {
if (!addon) {
let message = "could not get Addon for " + this.id;
gLogger.warn("ExperimentEntry::stop() - " + message);
this._log.warn("stop() - " + message);
updateDates();
deferred.resolve();
return;
@ -1478,7 +1523,7 @@ Experiments.ExperimentEntry.prototype = {
}
if (!(terminationKind in TELEMETRY_LOG.TERMINATION)) {
gLogger.warn("ExperimentEntry::stop() - unknown terminationKind " + terminationKind);
this._log.warn("stop() - unknown terminationKind " + terminationKind);
return;
}
@ -1496,7 +1541,7 @@ Experiments.ExperimentEntry.prototype = {
* the value indicates whether it was stopped.
*/
maybeStop: function () {
gLogger.trace("ExperimentEntry::maybeStop()");
this._log.trace("maybeStop()");
return Task.spawn(function ExperimentEntry_maybeStop_task() {
let result = yield this._shouldStop();
@ -1563,11 +1608,11 @@ Experiments.ExperimentEntry.prototype = {
* Perform sanity checks on the experiment data.
*/
_isManifestDataValid: function (data) {
gLogger.trace("ExperimentEntry::isManifestDataValid() - data: " + JSON.stringify(data));
this._log.trace("isManifestDataValid() - data: " + JSON.stringify(data));
for (let key of this.MANIFEST_REQUIRED_FIELDS) {
if (!(key in data)) {
gLogger.error("ExperimentEntry::isManifestDataValid() - missing required key: " + key);
this._log.error("isManifestDataValid() - missing required key: " + key);
return false;
}
}
@ -1575,7 +1620,7 @@ Experiments.ExperimentEntry.prototype = {
for (let key in data) {
if (!this.MANIFEST_OPTIONAL_FIELDS.has(key) &&
!this.MANIFEST_REQUIRED_FIELDS.has(key)) {
gLogger.error("ExperimentEntry::isManifestDataValid() - unknown key: " + key);
this._log.error("isManifestDataValid() - unknown key: " + key);
return false;
}
}

View File

@ -105,6 +105,18 @@ function dateToSeconds(date) {
return date.getTime() / 1000;
}
let gGlobalScope = this;
function loadAddonManager() {
let ns = {};
Cu.import("resource://gre/modules/Services.jsm", ns);
let head = "../../../../toolkit/mozapps/extensions/test/xpcshell/head_addons.js";
let file = do_get_file(head);
let uri = ns.Services.io.newFileURI(file);
ns.Services.scriptloader.loadSubScript(uri.spec, gGlobalScope);
createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
startupManager();
}
// Install addon and return a Promise<boolean> that is
// resolve with true on success, false otherwise.
function installAddon(url, hash) {
@ -160,6 +172,14 @@ function uninstallAddon(id) {
return deferred.promise;
}
function getExperimentAddons() {
let deferred = Promise.defer();
AddonManager.getAddonsByTypes(["experiment"], deferred.resolve);
return deferred.promise;
}
function createAppInfo(optionsIn) {
const XULAPPINFO_CONTRACTID = "@mozilla.org/xre/app-info;1";
const XULAPPINFO_CID = Components.ID("{c763b610-9d49-455a-bbd2-ede71682a1ac}");

View File

@ -21,18 +21,6 @@ let gHttpRoot = null;
let gReporter = null;
let gPolicy = null;
let gGlobalScope = this;
function loadAddonManager() {
let ns = {};
Cu.import("resource://gre/modules/Services.jsm", ns);
let head = "../../../../toolkit/mozapps/extensions/test/xpcshell/head_addons.js";
let file = do_get_file(head);
let uri = ns.Services.io.newFileURI(file);
ns.Services.scriptloader.loadSubScript(uri.spec, gGlobalScope);
createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
startupManager();
}
function ManifestEntry(data) {
this.id = data.id || EXPERIMENT1_ID;
this.xpiURL = data.xpiURL || gHttpRoot + EXPERIMENT1_XPI_NAME;

View File

@ -29,18 +29,6 @@ let gManifestObject = null;
let gManifestHandlerURI = null;
let gTimerScheduleOffset = -1;
let gGlobalScope = this;
function loadAddonManager() {
let ns = {};
Cu.import("resource://gre/modules/Services.jsm", ns);
let head = "../../../../toolkit/mozapps/extensions/test/xpcshell/head_addons.js";
let file = do_get_file(head);
let uri = ns.Services.io.newFileURI(file);
ns.Services.scriptloader.loadSubScript(uri.spec, gGlobalScope);
createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
startupManager();
}
function run_test() {
run_next_test();
}
@ -1352,3 +1340,32 @@ add_task(function* test_unexpectedUninstall() {
yield experiments.uninit();
yield removeCacheFile();
});
// If the Addon Manager knows of an experiment that we don't, it should get
// uninstalled.
add_task(function* testUnknownExperimentsUninstalled() {
let experiments = new Experiments.Experiments(gPolicy);
let addons = yield getExperimentAddons();
Assert.equal(addons.length, 0, "Precondition: No experiment add-ons are present.");
yield installAddon(gDataRoot + EXPERIMENT1_XPI_NAME, EXPERIMENT1_XPI_SHA1);
addons = yield getExperimentAddons();
Assert.equal(addons.length, 1, "Experiment 1 installed via AddonManager");
// Simulate no known experiments.
gManifestObject = {
"version": 1,
experiments: [],
};
yield experiments.updateManifest();
let fromManifest = yield experiments.getExperiments();
Assert.equal(fromManifest.length, 0, "No experiments known in manifest.");
// And the unknown add-on should be gone.
addons = yield getExperimentAddons();
Assert.equal(addons.length, 0, "Experiment 1 was uninstalled.");
yield experiments.uninit();
yield removeCacheFile();
});

View File

@ -29,18 +29,6 @@ let gManifestObject = null;
let gManifestHandlerURI = null;
let gTimerScheduleOffset = -1;
let gGlobalScope = this;
function loadAddonManager() {
let ns = {};
Cu.import("resource://gre/modules/Services.jsm", ns);
let head = "../../../../toolkit/mozapps/extensions/test/xpcshell/head_addons.js";
let file = do_get_file(head);
let uri = ns.Services.io.newFileURI(file);
ns.Services.scriptloader.loadSubScript(uri.spec, gGlobalScope);
createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
startupManager();
}
function run_test() {
run_next_test();
}

View File

@ -20,7 +20,7 @@ let gHttpRoot = null;
let gPolicy = new Experiments.Policy();
function run_test() {
createAppInfo();
loadAddonManager();
gProfileDir = do_get_profile();
gHttpServer = new HttpServer();

View File

@ -20,11 +20,15 @@ function getStorageAndProvider(name) {
}
function run_test() {
run_next_test();
}
add_test(function setup() {
do_get_profile();
initTestLogging();
run_next_test();
}
});
add_task(function test_constructor() {
let provider = new ExperimentsProvider();

View File

@ -0,0 +1,45 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<!-- LOCALIZATION NOTE : FILE This file contains the Debugger strings -->
<!-- LOCALIZATION NOTE : FILE Do not translate commandkey -->
<!-- LOCALIZATION NOTE : FILE The correct localization of this file might be to
- keep it in English, or another language commonly spoken among web developers.
- You want to make that choice consistent across the developer tools.
- A good criteria is the language in which you'd find the best
- documentation on web development on the web. -->
<!-- LOCALIZATION NOTE (canvasDebuggerUI.reloadNotice1): This is the label shown
- on the button that triggers a page refresh. -->
<!ENTITY canvasDebuggerUI.reloadNotice1 "Reload">
<!-- LOCALIZATION NOTE (canvasDebuggerUI.reloadNotice2): This is the label shown
- along with the button that triggers a page refresh. -->
<!ENTITY canvasDebuggerUI.reloadNotice2 "the page to be able to debug &lt;canvas&gt; contexts.">
<!-- LOCALIZATION NOTE (canvasDebuggerUI.emptyNotice1/2): This is the label shown
- in the call list view when empty. -->
<!ENTITY canvasDebuggerUI.emptyNotice1 "Click on the">
<!ENTITY canvasDebuggerUI.emptyNotice2 "button to record an animation frame's call stack.">
<!-- LOCALIZATION NOTE (canvasDebuggerUI.reloadNotice1): This is the label shown
- in the call list view while loading a snapshot. -->
<!ENTITY canvasDebuggerUI.importNotice "Loading…">
<!-- LOCALIZATION NOTE (canvasDebuggerUI.recordSnapshot): This string is displayed
- on a button that starts a new snapshot. -->
<!ENTITY canvasDebuggerUI.recordSnapshot.tooltip "Record the next frame in the animation loop.">
<!-- LOCALIZATION NOTE (canvasDebuggerUI.importSnapshot): This string is displayed
- on a button that opens a dialog to import a saved snapshot data file. -->
<!ENTITY canvasDebuggerUI.importSnapshot "Import…">
<!-- LOCALIZATION NOTE (canvasDebuggerUI.clearSnapshots): This string is displayed
- on a button that remvoes all the snapshots. -->
<!ENTITY canvasDebuggerUI.clearSnapshots "Clear">
<!-- LOCALIZATION NOTE (canvasDebuggerUI.searchboxPlaceholder): This string is displayed
- as a placeholder of the search box that filters the calls list. -->
<!ENTITY canvasDebuggerUI.searchboxPlaceholder "Filter calls">

View File

@ -0,0 +1,74 @@
# 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/.
# LOCALIZATION NOTE These strings are used inside the Canvas Debugger
# which is available from the Web Developer sub-menu -> 'Canvas'.
# The correct localization of this file might be to keep it in
# English, or another language commonly spoken among web developers.
# You want to make that choice consistent across the developer tools.
# A good criteria is the language in which you'd find the best
# documentation on web development on the web.
# LOCALIZATION NOTE (ToolboxCanvasDebugger.label):
# This string is displayed in the title of the tab when the Shader Editor is
# displayed inside the developer tools window and in the Developer Tools Menu.
ToolboxCanvasDebugger.label=Canvas
# LOCALIZATION NOTE (ToolboxCanvasDebugger.tooltip):
# This string is displayed in the tooltip of the tab when the Shader Editor is
# displayed inside the developer tools window.
ToolboxCanvasDebugger.tooltip=Tools to inspect and debug <canvas> contexts
# LOCALIZATION NOTE (noSnapshotsText): The text to display in the snapshots menu
# when there are no recorded snapshots yet.
noSnapshotsText=There are no snapshots yet.
# LOCALIZATION NOTE (snapshotsList.itemLabel):
# This string is displayed in the snapshots list of the Canvas Debugger,
# identifying a set of function calls of a recorded animation frame.
snapshotsList.itemLabel=Snapshot #%S
# LOCALIZATION NOTE (snapshotsList.loadingLabel):
# This string is displayed in the snapshots list of the Canvas Debugger,
# for an item that has not finished loading.
snapshotsList.loadingLabel=Loading…
# LOCALIZATION NOTE (snapshotsList.saveLabel):
# This string is displayed in the snapshots list of the Canvas Debugger,
# for saving an item to disk.
snapshotsList.saveLabel=Save
# LOCALIZATION NOTE (snapshotsList.savingLabel):
# This string is displayed in the snapshots list of the Canvas Debugger,
# while saving an item to disk.
snapshotsList.savingLabel=Saving…
# LOCALIZATION NOTE (snapshotsList.loadedLabel):
# This string is displayed in the snapshots list of the Canvas Debugger,
# for an item which was loaded from disk
snapshotsList.loadedLabel=Loaded from disk
# LOCALIZATION NOTE (snapshotsList.saveDialogTitle):
# This string is displayed as a title for saving a snapshot to disk.
snapshotsList.saveDialogTitle=Save animation frame snapshot…
# LOCALIZATION NOTE (snapshotsList.saveDialogJSONFilter):
# This string is displayed as a filter for saving a snapshot to disk.
snapshotsList.saveDialogJSONFilter=JSON Files
# LOCALIZATION NOTE (snapshotsList.saveDialogAllFilter):
# This string is displayed as a filter for saving a snapshot to disk.
snapshotsList.saveDialogAllFilter=All Files
# LOCALIZATION NOTE (snapshotsList.drawCallsLabel):
# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
# This string is displayed in the snapshots list of the Canvas Debugger,
# as a generic description about how many draw calls were made.
snapshotsList.drawCallsLabel=#1 draw;#1 draws
# LOCALIZATION NOTE (snapshotsList.functionCallsLabel):
# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
# This string is displayed in the snapshots list of the Canvas Debugger,
# as a generic description about how many function calls were made in total.
snapshotsList.functionCallsLabel=#1 call;#1 calls

View File

@ -32,6 +32,8 @@
locale/browser/devtools/netmonitor.properties (%chrome/browser/devtools/netmonitor.properties)
locale/browser/devtools/shadereditor.dtd (%chrome/browser/devtools/shadereditor.dtd)
locale/browser/devtools/shadereditor.properties (%chrome/browser/devtools/shadereditor.properties)
locale/browser/devtools/canvasdebugger.dtd (%chrome/browser/devtools/canvasdebugger.dtd)
locale/browser/devtools/canvasdebugger.properties (%chrome/browser/devtools/canvasdebugger.properties)
locale/browser/devtools/gcli.properties (%chrome/browser/devtools/gcli.properties)
locale/browser/devtools/gclicommands.properties (%chrome/browser/devtools/gclicommands.properties)
locale/browser/devtools/webconsole.properties (%chrome/browser/devtools/webconsole.properties)

View File

@ -0,0 +1,5 @@
/* 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/. */
%include ../../shared/devtools/canvasdebugger.inc.css

View File

@ -209,6 +209,7 @@ browser.jar:
* skin/classic/browser/devtools/splitview.css (../shared/devtools/splitview.css)
skin/classic/browser/devtools/styleeditor.css (../shared/devtools/styleeditor.css)
* skin/classic/browser/devtools/shadereditor.css (devtools/shadereditor.css)
* skin/classic/browser/devtools/canvasdebugger.css (devtools/canvasdebugger.css)
* skin/classic/browser/devtools/debugger.css (devtools/debugger.css)
* skin/classic/browser/devtools/profiler.css (devtools/profiler.css)
* skin/classic/browser/devtools/netmonitor.css (devtools/netmonitor.css)

View File

@ -0,0 +1,6 @@
/* 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/. */
%include ../shared.inc
%include ../../shared/devtools/canvasdebugger.inc.css

View File

@ -330,6 +330,7 @@ browser.jar:
* skin/classic/browser/devtools/splitview.css (../shared/devtools/splitview.css)
skin/classic/browser/devtools/styleeditor.css (../shared/devtools/styleeditor.css)
* skin/classic/browser/devtools/shadereditor.css (devtools/shadereditor.css)
* skin/classic/browser/devtools/canvasdebugger.css (devtools/canvasdebugger.css)
* skin/classic/browser/devtools/debugger.css (devtools/debugger.css)
* skin/classic/browser/devtools/profiler.css (devtools/profiler.css)
* skin/classic/browser/devtools/netmonitor.css (devtools/netmonitor.css)

View File

@ -0,0 +1,501 @@
/* 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/. */
%filter substitution
%define darkCheckerboardBackground #000
%define lightCheckerboardBackground #fff
%define checkerboardCell rgba(128,128,128,0.2)
%define checkerboardPattern linear-gradient(45deg, @checkerboardCell@ 25%, transparent 25%, transparent 75%, @checkerboardCell@ 75%, @checkerboardCell@), linear-gradient(45deg, @checkerboardCell@ 25%, transparent 25%, transparent 75%, @checkerboardCell@ 75%, @checkerboardCell@)
%define gutterWidth 3em
%define gutterPaddingStart 22px
/* Reload and waiting notices */
.notice-container {
margin-top: -50vh;
font-size: 120%;
}
.theme-dark .notice-container {
background: url(background-noise-toolbar.png), #343c45; /* Toolbars */
color: #f5f7fa; /* Light foreground text */
}
.theme-light .notice-container {
background: url(background-noise-toolbar.png), #f0f1f2; /* Toolbars */
color: #585959; /* Grey foreground text */
}
#reload-notice > button {
min-height: 2em;
}
#empty-notice > button {
min-width: 30px;
min-height: 28px;
margin: 0;
list-style-image: url(profiler-stopwatch.png);
-moz-image-region: rect(0px,16px,16px,0px);
}
#empty-notice > button .button-text {
display: none;
}
.theme-dark #import-notice {
font-size: 250%;
color: rgba(255,255,255,0.2);
}
.theme-light #import-notice {
font-size: 250%;
color: rgba(0,0,0,0.2);
}
/* Snapshots pane */
#snapshots-pane > tabs {
-moz-border-end: 1px solid;
}
#snapshots-pane .devtools-toolbar {
-moz-border-end: 1px solid;
}
.theme-dark #snapshots-pane > tabs,
.theme-dark #snapshots-pane .devtools-toolbar {
-moz-border-end-color: black; /* Match the splitter color. */
}
.theme-light #snapshots-pane > tabs,
.theme-light #snapshots-pane .devtools-toolbar {
-moz-border-end-color: #aaa; /* Match the splitter color. */
}
#record-snapshot {
list-style-image: url("chrome://browser/skin/devtools/profiler-stopwatch.png");
-moz-image-region: rect(0px,16px,16px,0px);
}
#record-snapshot[checked] {
-moz-image-region: rect(0px,32px,16px,16px);
}
/* Snapshots items */
.snapshot-item-thumbnail {
image-rendering: -moz-crisp-edges;
background-image: @checkerboardPattern@;
background-size: 12px 12px, 12px 12px;
background-position: 0px 0px, 6px 6px;
background-repeat: repeat, repeat;
}
.snapshot-item-thumbnail[flipped=true] {
transform: scaleY(-1);
}
.theme-dark .snapshot-item-thumbnail {
background-color: @darkCheckerboardBackground@;
}
.theme-light .snapshot-item-thumbnail {
background-color: @lightCheckerboardBackground@;
}
.snapshot-item-details {
-moz-padding-start: 6px;
}
.snapshot-item-calls {
padding-top: 4px;
font-size: 80%;
}
.snapshot-item-save {
padding-bottom: 2px;
font-size: 90%;
}
.theme-dark .snapshot-item-calls,
.theme-dark .snapshot-item-save {
color: #b6babf; /* Foreground (Text) - Grey */
}
.theme-light .snapshot-item-calls,
.theme-light .snapshot-item-save {
color: #585959; /* Foreground (Text) - Grey */
}
.snapshot-item-save {
text-decoration: underline;
cursor: pointer;
}
.snapshot-item-save[disabled=true] {
text-decoration: none;
pointer-events: none;
}
.snapshot-item-footer[saving]::before {
display: inline-block;
content: "";
background: url("chrome://global/skin/icons/loading_16.png") center no-repeat;
width: 16px;
height: 16px;
margin-top: -2px;
-moz-margin-end: 4px;
}
#snapshots-list .selected label {
/* Text inside a selected item should not be custom colored. */
color: inherit !important;
}
/* Debugging pane controls */
#resume {
list-style-image: url(debugger-play.png);
-moz-image-region: rect(0px,32px,16px,16px);
}
#step-over {
list-style-image: url(debugger-step-over.png);
}
#step-in {
list-style-image: url(debugger-step-in.png);
}
#step-out {
list-style-image: url(debugger-step-out.png);
}
#debugging-controls > toolbarbutton {
transition: opacity 0.15s ease-in-out;
}
#debugging-controls > toolbarbutton[disabled=true] {
opacity: 0.5;
}
#calls-slider {
-moz-padding-end: 24px;
}
#calls-slider .scale-slider {
margin: 0;
}
#debugging-toolbar-sizer-button {
/* This button's only purpose in life is to make the
container .devtools-toolbar have the right height. */
visibility: hidden;
min-width: 1px;
}
/* Calls list pane */
#calls-list .side-menu-widget-container {
background: transparent;
}
#calls-list .side-menu-widget-item {
padding: 0;
}
/* Calls list items */
.theme-dark #calls-list .side-menu-widget-item {
border-color: #111;
border-bottom-color: transparent;
}
.theme-light #calls-list .side-menu-widget-item {
border-color: #eee;
border-bottom-color: transparent;
}
.theme-dark .call-item-view:hover {
background-color: rgba(255,255,255,.025);
}
.theme-light .call-item-view:hover {
background-color: rgba(0,0,0,.025);
}
.theme-dark .call-item-view[draw-call] {
background-color: rgba(112,191,83,0.15);
}
.theme-light .call-item-view[draw-call] {
background-color: rgba(44,187,15,0.1);
}
.theme-dark .call-item-view[interesting-call] {
background-color: rgba(223,128,255,0.15);
}
.theme-light .call-item-view[interesting-call] {
background-color: rgba(184,46,229,0.1);
}
.call-item-gutter {
width: calc(@gutterWidth@ + @gutterPaddingStart@);
-moz-padding-start: @gutterPaddingStart@;
-moz-padding-end: 4px;
padding-top: 2px;
padding-bottom: 2px;
-moz-border-end: 1px solid;
-moz-margin-end: 6px;
}
.selected .call-item-gutter {
background-image: url("editor-debug-location.png");
background-repeat: no-repeat;
background-position: 6px center;
background-size: 12px;
}
.theme-dark .call-item-gutter {
background-color: #181d20;
color: #5f7387;
border-color: #000;
}
.theme-light .call-item-gutter {
background-color: #f7f7f7;
color: #667380;
border-color: #aaa;
}
.call-item-index {
text-align: end;
}
.theme-dark .call-item-context {
color: #eb5368; /* Highlight Orange */
}
.theme-light .call-item-context {
color: #f13c00; /* Highlight Orange */
}
.theme-dark .call-item-name {
color: #46afe3; /* Highlight Blue */
}
.theme-light .call-item-name {
color: #0088cc; /* Highlight Blue */
}
.call-item-location {
-moz-padding-start: 2px;
-moz-padding-end: 6px;
text-align: end;
cursor: pointer;
}
.theme-dark .call-item-location:hover {
color: #0088cc; /* Highlight Blue */
}
.theme-light .call-item-location:hover {
color: #46afe3; /* Highlight Blue */
}
.call-item-view:hover .call-item-location,
.call-item-view[expanded] .call-item-location {
text-decoration: underline;
}
.theme-dark .call-item-location {
border-color: #111;
color: #5e88b0; /* Highlight Blue-Grey */
}
.theme-light .call-item-location {
border-color: #eee;
color: #5f88b0; /* Highlight Blue-Grey */
}
.call-item-stack {
-moz-padding-start: calc(@gutterWidth@ + @gutterPaddingStart@);
padding-bottom: 10px;
}
.theme-dark .call-item-stack {
background: rgba(0,0,0,0.9);
}
.theme-light .call-item-stack {
background: rgba(255,255,255,0.9);
}
.call-item-stack-fn {
padding-top: 2px;
padding-bottom: 2px;
}
.call-item-stack-fn-location {
-moz-padding-start: 2px;
-moz-padding-end: 6px;
text-align: end;
cursor: pointer;
text-decoration: underline;
}
.theme-dark .call-item-stack-fn-name {
color: #a9bacb; /* Content (Text) - Light */
}
.theme-light .call-item-stack-fn-name {
color: #667380; /* Content (Text) - Dark Grey */
}
.theme-dark .call-item-stack-fn-location {
color: #5e88b0; /* Highlight Blue-Grey */
}
.theme-light .call-item-stack-fn-location {
color: #5e88b0; /* Highlight Blue-Grey */
}
.theme-dark .call-item-stack-fn-location:hover {
color: #0088cc; /* Highlight Blue */
}
.theme-light .call-item-stack-fn-location:hover {
color: #46afe3; /* Highlight Blue */
}
#calls-list .selected .call-item-contents > label:not(.call-item-gutter) {
/* Text inside a selected item should not be custom colored. */
color: inherit !important;
}
/* Rendering preview */
#screenshot-container {
background-image: @checkerboardPattern@;
background-size: 30px 30px, 30px 30px;
background-position: 0px 0px, 15px 15px;
background-repeat: repeat, repeat;
}
.theme-dark #screenshot-container {
background-color: @darkCheckerboardBackground@;
}
.theme-light #screenshot-container {
background-color: @lightCheckerboardBackground@;
}
@media (min-width: 701px) {
#screenshot-container {
width: 30vw;
max-width: 50vw;
min-width: 100px;
}
}
@media (max-width: 700px) {
#screenshot-container {
height: 40vh;
max-height: 70vh;
min-height: 100px;
}
}
#screenshot-image {
background-image: -moz-element(#screenshot-rendering);
background-size: contain;
background-position: center, center;
background-repeat: no-repeat;
}
#screenshot-image[flipped=true] {
transform: scaleY(-1);
}
#screenshot-dimensions {
padding-top: 4px;
padding-bottom: 4px;
text-align: center;
}
.theme-dark #screenshot-dimensions {
background-color: rgba(0,0,0,0.4);
}
.theme-light #screenshot-dimensions {
background-color: rgba(255,255,255,0.8);
}
/* Snapshot filmstrip */
#snapshot-filmstrip {
overflow: hidden;
}
.theme-dark #snapshot-filmstrip {
border-top: 1px solid #000;
background-image: url(background-noise-toolbar.png);
color: #f5f7fa; /* Light foreground text */
}
.theme-light #snapshot-filmstrip {
border-top: 1px solid #aaa;
background-image: url(background-noise-toolbar.png);
color: #585959; /* Grey foreground text */
}
.filmstrip-thumbnail {
image-rendering: -moz-crisp-edges;
background-image: @checkerboardPattern@;
background-size: 12px 12px, 12px 12px;
background-position: 0px -1px, 6px 5px;
background-repeat: repeat, repeat;
background-origin: content-box;
cursor: pointer;
padding-top: 1px;
padding-bottom: 1px;
transition: opacity 0.1s ease-in-out;
}
.filmstrip-thumbnail[flipped=true] {
transform: scaleY(-1);
}
.theme-dark .filmstrip-thumbnail {
background-color: @darkCheckerboardBackground@;
}
.theme-light .filmstrip-thumbnail {
background-color: @lightCheckerboardBackground@;
}
.theme-dark .filmstrip-thumbnail {
-moz-border-end: 1px solid #000;
}
.theme-light .filmstrip-thumbnail {
-moz-border-end: 1px solid #aaa;
}
.theme-dark #snapshot-filmstrip > .filmstrip-thumbnail:hover,
.theme-dark #snapshot-filmstrip:not(:hover) > .filmstrip-thumbnail[highlighted] {
border: 1px solid #46afe3; /* Highlight Blue */
margin: 0 0 0 -1px;
padding: 0;
opacity: 0.66;
}
.theme-light #snapshot-filmstrip > .filmstrip-thumbnail:hover,
.theme-light #snapshot-filmstrip:not(:hover) > .filmstrip-thumbnail[highlighted] {
border: 1px solid #0088cc; /* Highlight Blue */
margin: 0 0 0 -1px;
padding: 0;
opacity: 0.66;
}

View File

@ -774,6 +774,7 @@
.theme-light .scrollbutton-up > .toolbarbutton-icon,
.theme-light .scrollbutton-down > .toolbarbutton-icon,
.theme-light #black-boxed-message-button .button-icon,
.theme-light #canvas-debugging-empty-notice-button .button-icon,
.theme-light #requests-menu-perf-notice-button .button-icon,
.theme-light #requests-menu-network-summary-button .button-icon {
filter: url(filters.svg#invert);

View File

@ -564,10 +564,12 @@
}
.theme-dark .side-menu-widget-empty-text {
background: url(background-noise-toolbar.png), #343c45; /* Toolbars */
color: #b6babf; /* Foreground (Text) - Grey */
}
.theme-light .side-menu-widget-empty-text {
background: #f7f7f7; /* Toolbars */
color: #585959; /* Grey foreground text */
}

View File

@ -0,0 +1,5 @@
/* 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/. */
%include ../../shared/devtools/canvasdebugger.inc.css

View File

@ -242,6 +242,7 @@ browser.jar:
* skin/classic/browser/devtools/splitview.css (../shared/devtools/splitview.css)
skin/classic/browser/devtools/styleeditor.css (../shared/devtools/styleeditor.css)
* skin/classic/browser/devtools/shadereditor.css (devtools/shadereditor.css)
* skin/classic/browser/devtools/canvasdebugger.css (devtools/canvasdebugger.css)
* skin/classic/browser/devtools/debugger.css (devtools/debugger.css)
* skin/classic/browser/devtools/profiler.css (devtools/profiler.css)
* skin/classic/browser/devtools/netmonitor.css (devtools/netmonitor.css)
@ -589,6 +590,7 @@ browser.jar:
* skin/classic/aero/browser/devtools/splitview.css (../shared/devtools/splitview.css)
skin/classic/aero/browser/devtools/styleeditor.css (../shared/devtools/styleeditor.css)
* skin/classic/aero/browser/devtools/shadereditor.css (devtools/shadereditor.css)
* skin/classic/aero/browser/devtools/canvasdebugger.css (devtools/canvasdebugger.css)
* skin/classic/aero/browser/devtools/debugger.css (devtools/debugger.css)
* skin/classic/aero/browser/devtools/profiler.css (devtools/profiler.css)
* skin/classic/aero/browser/devtools/netmonitor.css (devtools/netmonitor.css)

View File

@ -8,6 +8,7 @@
const { Ci, Cu } = require("chrome");
let { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
let { setTimeout, clearTimeout } = Cu.import("resource://gre/modules/Timer.jsm", {});
/**
* Turn the error |aError| into a string, without fail.
@ -27,6 +28,8 @@ exports.safeErrorString = function safeErrorString(aError) {
}
} catch (ee) { }
// Append additional line and column number information to the output,
// since it might not be part of the stringified error.
if (typeof aError.lineNumber == "number" && typeof aError.columnNumber == "number") {
errorString += "Line: " + aError.lineNumber + ", column: " + aError.columnNumber;
}
@ -113,12 +116,41 @@ exports.zip = function zip(a, b) {
return pairs;
};
const executeSoon = aFn => {
/**
* Waits for the next tick in the event loop to execute a callback.
*/
exports.executeSoon = function executeSoon(aFn) {
Services.tm.mainThread.dispatch({
run: exports.makeInfallible(aFn)
}, Ci.nsIThread.DISPATCH_NORMAL);
};
/**
* Waits for the next tick in the event loop.
*
* @return Promise
* A promise that is resolved after the next tick in the event loop.
*/
exports.waitForTick = function waitForTick() {
let deferred = promise.defer();
exports.executeSoon(deferred.resolve);
return deferred.promise;
};
/**
* Waits for the specified amount of time to pass.
*
* @param number aDelay
* The amount of time to wait, in milliseconds.
* @return Promise
* A promise that is resolved after the specified amount of time passes.
*/
exports.waitForTime = function waitForTime(aDelay) {
let deferred = promise.defer();
setTimeout(deferred.resolve, aDelay);
return deferred.promise;
};
/**
* Like Array.prototype.forEach, but doesn't cause jankiness when iterating over
* very large arrays by yielding to the browser and continuing execution on the
@ -127,16 +159,19 @@ const executeSoon = aFn => {
* @param Array aArray
* The array being iterated over.
* @param Function aFn
* The function called on each item in the array.
* The function called on each item in the array. If a promise is
* returned by this function, iterating over the array will be paused
* until the respective promise is resolved.
* @returns Promise
* A promise that is resolved once the whole array has been iterated
* over.
* over, and all promises returned by the aFn callback are resolved.
*/
exports.yieldingEach = function yieldingEach(aArray, aFn) {
const deferred = promise.defer();
let i = 0;
let len = aArray.length;
let outstanding = [deferred.promise];
(function loop() {
const start = Date.now();
@ -147,12 +182,12 @@ exports.yieldingEach = function yieldingEach(aArray, aFn) {
// aren't including time spent in non-JS here, but this is Good
// Enough(tm).
if (Date.now() - start > 16) {
executeSoon(loop);
exports.executeSoon(loop);
return;
}
try {
aFn(aArray[i++]);
outstanding.push(aFn(aArray[i], i++));
} catch (e) {
deferred.reject(e);
return;
@ -162,10 +197,9 @@ exports.yieldingEach = function yieldingEach(aArray, aFn) {
deferred.resolve();
}());
return deferred.promise;
return promise.all(outstanding);
}
/**
* Like XPCOMUtils.defineLazyGetter, but with a |this| sensitive getter that
* allows the lazy getter to be defined on a prototype and work correctly with
@ -266,4 +300,3 @@ exports.isSafeJSObject = function isSafeJSObject(aObj) {
return Cu.isXrayWrapper(aObj);
};

View File

@ -71,6 +71,7 @@ BuiltinProvider.prototype = {
"devtools/client": "resource://gre/modules/devtools/client",
"devtools/pretty-fast": "resource://gre/modules/devtools/pretty-fast.js",
"devtools/async-utils": "resource://gre/modules/devtools/async-utils",
"devtools/content-observer": "resource://gre/modules/devtools/content-observer",
"gcli": "resource://gre/modules/devtools/gcli",
"acorn": "resource://gre/modules/devtools/acorn",
"acorn/util/walk": "resource://gre/modules/devtools/acorn/walk.js",
@ -120,6 +121,7 @@ SrcdirProvider.prototype = {
let clientURI = this.fileURI(OS.Path.join(toolkitDir, "client"));
let prettyFastURI = this.fileURI(OS.Path.join(toolkitDir), "pretty-fast.js");
let asyncUtilsURI = this.fileURI(OS.Path.join(toolkitDir), "async-utils.js");
let contentObserverURI = this.fileURI(OS.Path.join(toolkitDir), "content-observer.js");
let gcliURI = this.fileURI(OS.Path.join(toolkitDir, "gcli", "source", "lib", "gcli"));
let acornURI = this.fileURI(OS.Path.join(toolkitDir, "acorn"));
let acornWalkURI = OS.Path.join(acornURI, "walk.js");
@ -144,6 +146,7 @@ SrcdirProvider.prototype = {
"devtools/client": clientURI,
"devtools/pretty-fast": prettyFastURI,
"devtools/async-utils": asyncUtilsURI,
"devtools/content-observer": contentObserverURI,
"gcli": gcliURI,
"acorn": acornURI,
"acorn/util/walk": acornWalkURI

View File

@ -0,0 +1,72 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {Cc, Ci, Cu, Cr} = require("chrome");
const {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
const events = require("sdk/event/core");
const promise = require("sdk/core/promise");
/**
* Handles adding an observer for the creation of content document globals,
* event sent immediately after a web content document window has been set up,
* but before any script code has been executed.
*/
function ContentObserver(tabActor) {
this._contentWindow = tabActor.window;
this._onContentGlobalCreated = this._onContentGlobalCreated.bind(this);
this._onInnerWindowDestroyed = this._onInnerWindowDestroyed.bind(this);
this.startListening();
}
module.exports.ContentObserver = ContentObserver;
ContentObserver.prototype = {
/**
* Starts listening for the required observer messages.
*/
startListening: function() {
Services.obs.addObserver(
this._onContentGlobalCreated, "content-document-global-created", false);
Services.obs.addObserver(
this._onInnerWindowDestroyed, "inner-window-destroyed", false);
},
/**
* Stops listening for the required observer messages.
*/
stopListening: function() {
Services.obs.removeObserver(
this._onContentGlobalCreated, "content-document-global-created", false);
Services.obs.removeObserver(
this._onInnerWindowDestroyed, "inner-window-destroyed", false);
},
/**
* Fired immediately after a web content document window has been set up.
*/
_onContentGlobalCreated: function(subject, topic, data) {
if (subject == this._contentWindow) {
events.emit(this, "global-created", subject);
}
},
/**
* Fired when an inner window is removed from the backward/forward cache.
*/
_onInnerWindowDestroyed: function(subject, topic, data) {
let id = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
events.emit(this, "global-destroyed", id);
}
};
// Utility functions.
ContentObserver.GetInnerWindowID = function(window) {
return window
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
.currentInnerWindowID;
};

View File

@ -0,0 +1,559 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {Cc, Ci, Cu, Cr} = require("chrome");
const events = require("sdk/event/core");
const promise = require("sdk/core/promise");
const protocol = require("devtools/server/protocol");
const {ContentObserver} = require("devtools/content-observer");
const {on, once, off, emit} = events;
const {method, Arg, Option, RetVal} = protocol;
exports.register = function(handle) {
handle.addTabActor(CallWatcherActor, "callWatcherActor");
};
exports.unregister = function(handle) {
handle.removeTabActor(CallWatcherActor);
};
/**
* Type describing a single function call in a stack trace.
*/
protocol.types.addDictType("call-stack-item", {
name: "string",
file: "string",
line: "number"
});
/**
* Type describing an overview of a function call.
*/
protocol.types.addDictType("call-details", {
type: "number",
name: "string",
stack: "array:call-stack-item"
});
/**
* This actor contains information about a function call, like the function
* type, name, stack, arguments, returned value etc.
*/
let FunctionCallActor = protocol.ActorClass({
typeName: "function-call",
/**
* Creates the function call actor.
*
* @param DebuggerServerConnection conn
* The server connection.
* @param DOMWindow window
* The content window.
* @param string global
* The name of the global object owning this function, like
* "CanvasRenderingContext2D" or "WebGLRenderingContext".
* @param object caller
* The object owning the function when it was called.
* For example, in `foo.bar()`, the caller is `foo`.
* @param number type
* Either METHOD_FUNCTION, METHOD_GETTER or METHOD_SETTER.
* @param string name
* The called function's name.
* @param array stack
* The called function's stack, as a list of { name, file, line } objects.
* @param array args
* The called function's arguments.
* @param any result
* The value returned by the function call.
*/
initialize: function(conn, [window, global, caller, type, name, stack, args, result]) {
protocol.Actor.prototype.initialize.call(this, conn);
this.details = {
window: window,
caller: caller,
type: type,
name: name,
stack: stack,
args: args,
return: result
};
this.meta = {
global: -1,
previews: { caller: "", args: "" }
};
if (global == "WebGLRenderingContext") {
this.meta.global = CallWatcherFront.CANVAS_WEBGL_CONTEXT;
} else if (global == "CanvasRenderingContext2D") {
this.meta.global = CallWatcherFront.CANVAS_2D_CONTEXT;
} else if (global == "window") {
this.meta.global = CallWatcherFront.UNKNOWN_SCOPE;
} else {
this.meta.global = CallWatcherFront.GLOBAL_SCOPE;
}
this.meta.previews.caller = this._generateCallerPreview();
this.meta.previews.args = this._generateArgsPreview();
},
/**
* Customize the marshalling of this actor to provide some generic information
* directly on the Front instance.
*/
form: function() {
return {
actor: this.actorID,
type: this.details.type,
name: this.details.name,
file: this.details.stack[0].file,
line: this.details.stack[0].line,
callerPreview: this.meta.previews.caller,
argsPreview: this.meta.previews.args
};
},
/**
* Gets more information about this function call, which is not necessarily
* available on the Front instance.
*/
getDetails: method(function() {
let { type, name, stack } = this.details;
// Since not all calls on the stack have corresponding owner files (e.g.
// callbacks of a requestAnimationFrame etc.), there's no benefit in
// returning them, as the user can't jump to the Debugger from them.
for (let i = stack.length - 1;;) {
if (stack[i].file) {
break;
}
stack.pop();
i--;
}
// XXX: Use grips for objects and serialize them properly, in order
// to add the function's caller, arguments and return value. Bug 978957.
return {
type: type,
name: name,
stack: stack
};
}, {
response: { info: RetVal("call-details") }
}),
/**
* Serializes the caller's name so that it can be easily be transferred
* as a string, but still be useful when displayed in a potential UI.
*
* @return string
* The caller's name as a string.
*/
_generateCallerPreview: function() {
let global = this.meta.global;
if (global == CallWatcherFront.CANVAS_WEBGL_CONTEXT) {
return "gl";
}
if (global == CallWatcherFront.CANVAS_2D_CONTEXT) {
return "ctx";
}
return "";
},
/**
* Serializes the arguments so that they can be easily be transferred
* as a string, but still be useful when displayed in a potential UI.
*
* @return string
* The arguments as a string.
*/
_generateArgsPreview: function() {
let { caller, args } = this.details;
let { global } = this.meta;
// XXX: All of this sucks. Make this smarter, so that the frontend
// can inspect each argument, be it object or primitive. Bug 978960.
let serializeArgs = () => args.map(arg => {
if (typeof arg == "undefined") {
return "undefined";
}
if (typeof arg == "function") {
return "Function";
}
if (typeof arg == "object") {
return "Object";
}
if (global == CallWatcherFront.CANVAS_WEBGL_CONTEXT) {
// XXX: This doesn't handle combined bitmasks. Bug 978964.
return getEnumsLookupTable("webgl", caller)[arg] || arg;
}
if (global == CallWatcherFront.CANVAS_2D_CONTEXT) {
return getEnumsLookupTable("2d", caller)[arg] || arg;
}
return arg;
});
return serializeArgs().join(", ");
}
});
/**
* The corresponding Front object for the FunctionCallActor.
*/
let FunctionCallFront = protocol.FrontClass(FunctionCallActor, {
initialize: function(client, form) {
protocol.Front.prototype.initialize.call(this, client, form);
},
/**
* Adds some generic information directly to this instance,
* to avoid extra roundtrips.
*/
form: function(form) {
this.actorID = form.actor;
this.type = form.type;
this.name = form.name;
this.file = form.file;
this.line = form.line;
this.callerPreview = form.callerPreview;
this.argsPreview = form.argsPreview;
}
});
/**
* This actor observes function calls on certain objects or globals.
*/
let CallWatcherActor = exports.CallWatcherActor = protocol.ActorClass({
typeName: "call-watcher",
initialize: function(conn, tabActor) {
protocol.Actor.prototype.initialize.call(this, conn);
this.tabActor = tabActor;
this._onGlobalCreated = this._onGlobalCreated.bind(this);
this._onGlobalDestroyed = this._onGlobalDestroyed.bind(this);
this._onContentFunctionCall = this._onContentFunctionCall.bind(this);
},
destroy: function(conn) {
protocol.Actor.prototype.destroy.call(this, conn);
this.finalize();
},
/**
* Starts waiting for the current tab actor's document global to be
* created, in order to instrument the specified objects and become
* aware of everything the content does with them.
*/
setup: method(function({ tracedGlobals, tracedFunctions, startRecording, performReload }) {
if (this._initialized) {
return;
}
this._initialized = true;
this._functionCalls = [];
this._tracedGlobals = tracedGlobals || [];
this._tracedFunctions = tracedFunctions || [];
this._contentObserver = new ContentObserver(this.tabActor);
on(this._contentObserver, "global-created", this._onGlobalCreated);
on(this._contentObserver, "global-destroyed", this._onGlobalDestroyed);
if (startRecording) {
this.resumeRecording();
}
if (performReload) {
this.tabActor.window.location.reload();
}
}, {
request: {
tracedGlobals: Option(0, "nullable:array:string"),
tracedFunctions: Option(0, "nullable:array:string"),
startRecording: Option(0, "boolean"),
performReload: Option(0, "boolean")
},
oneway: true
}),
/**
* Stops listening for document global changes and puts this actor
* to hibernation. This method is called automatically just before the
* actor is destroyed.
*/
finalize: method(function() {
if (!this._initialized) {
return;
}
this._initialized = false;
this._contentObserver.stopListening();
off(this._contentObserver, "global-created", this._onGlobalCreated);
off(this._contentObserver, "global-destroyed", this._onGlobalDestroyed);
this._tracedGlobals = null;
this._tracedFunctions = null;
this._contentObserver = null;
}, {
oneway: true
}),
/**
* Returns whether the instrumented function calls are currently recorded.
*/
isRecording: method(function() {
return this._recording;
}, {
response: RetVal("boolean")
}),
/**
* Starts recording function calls.
*/
resumeRecording: method(function() {
this._recording = true;
}),
/**
* Stops recording function calls.
*/
pauseRecording: method(function() {
this._recording = false;
return this._functionCalls;
}, {
response: { calls: RetVal("array:function-call") }
}),
/**
* Erases all the recorded function calls.
* Calling `resumeRecording` or `pauseRecording` does not erase history.
*/
eraseRecording: method(function() {
this._functionCalls = [];
}),
/**
* Lightweight listener invoked whenever an instrumented function is called
* while recording. We're doing this to avoid the event emitter overhead,
* since this is expected to be a very hot function.
*/
onCall: function() {},
/**
* Invoked whenever the current tab actor's document global is created.
*/
_onGlobalCreated: function(window) {
let self = this;
this._tracedWindowId = ContentObserver.GetInnerWindowID(window);
let unwrappedWindow = XPCNativeWrapper.unwrap(window);
let callback = this._onContentFunctionCall;
for (let global of this._tracedGlobals) {
let prototype = unwrappedWindow[global].prototype;
let properties = Object.keys(prototype);
properties.forEach(name => overrideSymbol(global, prototype, name, callback));
}
for (let name of this._tracedFunctions) {
overrideSymbol("window", unwrappedWindow, name, callback);
}
/**
* Instruments a method, getter or setter on the specified target object to
* invoke a callback whenever it is called.
*/
function overrideSymbol(global, target, name, callback) {
let propertyDescriptor = Object.getOwnPropertyDescriptor(target, name);
if (propertyDescriptor.get || propertyDescriptor.set) {
overrideAccessor(global, target, name, propertyDescriptor, callback);
return;
}
if (propertyDescriptor.writable && typeof propertyDescriptor.value == "function") {
overrideFunction(global, target, name, propertyDescriptor, callback);
return;
}
}
/**
* Instruments a function on the specified target object.
*/
function overrideFunction(global, target, name, descriptor, callback) {
let originalFunc = target[name];
Object.defineProperty(target, name, {
value: function(...args) {
let result = originalFunc.apply(this, args);
if (self._recording) {
let stack = getStack(name);
let type = CallWatcherFront.METHOD_FUNCTION;
callback(unwrappedWindow, global, this, type, name, stack, args, result);
}
return result;
},
configurable: descriptor.configurable,
enumerable: descriptor.enumerable,
writable: true
});
}
/**
* Instruments a getter or setter on the specified target object.
*/
function overrideAccessor(global, target, name, descriptor, callback) {
let originalGetter = target.__lookupGetter__(name);
let originalSetter = target.__lookupSetter__(name);
Object.defineProperty(target, name, {
get: function(...args) {
if (!originalGetter) return undefined;
let result = originalGetter.apply(this, args);
if (self._recording) {
let stack = getStack(name);
let type = CallWatcherFront.GETTER_FUNCTION;
callback(unwrappedWindow, global, this, type, name, stack, args, result);
}
return result;
},
set: function(...args) {
if (!originalSetter) return;
originalSetter.apply(this, args);
if (self._recording) {
let stack = getStack(name);
let type = CallWatcherFront.SETTER_FUNCTION;
callback(unwrappedWindow, global, this, type, name, stack, args, undefined);
}
},
configurable: descriptor.configurable,
enumerable: descriptor.enumerable
});
}
/**
* Stores the relevant information about calls on the stack when
* a function is called.
*/
function getStack(caller) {
try {
// Using Components.stack wouldn't be a better idea, since it's
// much slower because it attempts to retrieve the C++ stack as well.
throw new Error();
} catch (e) {
var stack = e.stack;
}
// Of course, using a simple regex like /(.*?)@(.*):(\d*):\d*/ would be
// much prettier, but this is a very hot function, so let's sqeeze
// every drop of performance out of it.
let calls = [];
let callIndex = 0;
let currNewLinePivot = stack.indexOf("\n") + 1;
let nextNewLinePivot = stack.indexOf("\n", currNewLinePivot);
while (nextNewLinePivot > 0) {
let nameDelimiterIndex = stack.indexOf("@", currNewLinePivot);
let columnDelimiterIndex = stack.lastIndexOf(":", nextNewLinePivot - 1);
let lineDelimiterIndex = stack.lastIndexOf(":", columnDelimiterIndex - 1);
if (!calls[callIndex]) {
calls[callIndex] = { name: "", file: "", line: 0 };
}
if (!calls[callIndex + 1]) {
calls[callIndex + 1] = { name: "", file: "", line: 0 };
}
if (callIndex > 0) {
let file = stack.substring(nameDelimiterIndex + 1, lineDelimiterIndex);
let line = stack.substring(lineDelimiterIndex + 1, columnDelimiterIndex);
let name = stack.substring(currNewLinePivot, nameDelimiterIndex);
calls[callIndex].name = name;
calls[callIndex - 1].file = file;
calls[callIndex - 1].line = line;
} else {
// Since the topmost stack frame is actually our overwritten function,
// it will not have the expected name.
calls[0].name = caller;
}
currNewLinePivot = nextNewLinePivot + 1;
nextNewLinePivot = stack.indexOf("\n", currNewLinePivot);
callIndex++;
}
return calls;
}
},
/**
* Invoked whenever the current tab actor's inner window is destroyed.
*/
_onGlobalDestroyed: function(id) {
if (this._tracedWindowId == id) {
this.pauseRecording();
this.eraseRecording();
}
},
/**
* Invoked whenever an instrumented function is called.
*/
_onContentFunctionCall: function(...details) {
let functionCall = new FunctionCallActor(this.conn, details);
this._functionCalls.push(functionCall);
this.onCall(functionCall);
}
});
/**
* The corresponding Front object for the CallWatcherActor.
*/
let CallWatcherFront = exports.CallWatcherFront = protocol.FrontClass(CallWatcherActor, {
initialize: function(client, { callWatcherActor }) {
protocol.Front.prototype.initialize.call(this, client, { actor: callWatcherActor });
client.addActorPool(this);
this.manage(this);
}
});
/**
* Constants.
*/
CallWatcherFront.METHOD_FUNCTION = 0;
CallWatcherFront.GETTER_FUNCTION = 1;
CallWatcherFront.SETTER_FUNCTION = 2;
CallWatcherFront.GLOBAL_SCOPE = 0;
CallWatcherFront.UNKNOWN_SCOPE = 1;
CallWatcherFront.CANVAS_WEBGL_CONTEXT = 2;
CallWatcherFront.CANVAS_2D_CONTEXT = 3;
/**
* A lookup table for cross-referencing flags or properties with their name
* assuming they look LIKE_THIS most of the time.
*
* For example, when gl.clear(gl.COLOR_BUFFER_BIT) is called, the actual passed
* argument's value is 16384, which we want identified as "COLOR_BUFFER_BIT".
*/
var gEnumRegex = /^[A-Z_]+$/;
var gEnumsLookupTable = {};
function getEnumsLookupTable(type, object) {
let cachedEnum = gEnumsLookupTable[type];
if (cachedEnum) {
return cachedEnum;
}
let table = gEnumsLookupTable[type] = {};
for (let key in object) {
if (key.match(gEnumRegex)) {
table[object[key]] = key;
}
}
return table;
}

View File

@ -0,0 +1,759 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {Cc, Ci, Cu, Cr} = require("chrome");
const events = require("sdk/event/core");
const promise = require("sdk/core/promise");
const protocol = require("devtools/server/protocol");
const {CallWatcherActor, CallWatcherFront} = require("devtools/server/actors/call-watcher");
const DevToolsUtils = require("devtools/toolkit/DevToolsUtils.js");
const {on, once, off, emit} = events;
const {method, custom, Arg, Option, RetVal} = protocol;
const CANVAS_CONTEXTS = [
"CanvasRenderingContext2D",
"WebGLRenderingContext"
];
const ANIMATION_GENERATORS = [
"requestAnimationFrame",
"mozRequestAnimationFrame"
];
const DRAW_CALLS = [
// 2D canvas
"fill",
"stroke",
"clearRect",
"fillRect",
"strokeRect",
"fillText",
"strokeText",
"drawImage",
// WebGL
"clear",
"drawArrays",
"drawElements",
"finish",
"flush"
];
const INTERESTING_CALLS = [
// 2D canvas
"save",
"restore",
// WebGL
"useProgram"
];
exports.register = function(handle) {
handle.addTabActor(CanvasActor, "canvasActor");
};
exports.unregister = function(handle) {
handle.removeTabActor(CanvasActor);
};
/**
* Type representing an Uint32Array buffer, serialized fast(er).
*
* XXX: It would be nice if on local connections (only), we could just *give*
* the buffer directly to the front, instead of going through all this
* serialization redundancy.
*/
protocol.types.addType("uint32-array", {
write: (v) => "[" + Array.join(v, ",") + "]",
read: (v) => new Uint32Array(JSON.parse(v))
});
/**
* Type describing a thumbnail or screenshot in a recorded animation frame.
*/
protocol.types.addDictType("snapshot-image", {
index: "number",
width: "number",
height: "number",
flipped: "boolean",
pixels: "uint32-array"
});
/**
* Type describing an overview of a recorded animation frame.
*/
protocol.types.addDictType("snapshot-overview", {
calls: "array:function-call",
thumbnails: "array:snapshot-image",
screenshot: "snapshot-image"
});
/**
* This actor represents a recorded animation frame snapshot, along with
* all the corresponding canvas' context methods invoked in that frame,
* thumbnails for each draw call and a screenshot of the end result.
*/
let FrameSnapshotActor = protocol.ActorClass({
typeName: "frame-snapshot",
/**
* Creates the frame snapshot call actor.
*
* @param DebuggerServerConnection conn
* The server connection.
* @param HTMLCanvasElement canvas
* A reference to the content canvas.
* @param array calls
* An array of "function-call" actor instances.
* @param object screenshot
* A single "snapshot-image" type instance.
*/
initialize: function(conn, { canvas, calls, screenshot }) {
protocol.Actor.prototype.initialize.call(this, conn);
this._contentCanvas = canvas;
this._functionCalls = calls;
this._lastDrawCallScreenshot = screenshot;
},
/**
* Gets as much data about this snapshot without computing anything costly.
*/
getOverview: method(function() {
return {
calls: this._functionCalls,
thumbnails: this._functionCalls.map(e => e._thumbnail).filter(e => !!e),
screenshot: this._lastDrawCallScreenshot
};
}, {
response: { overview: RetVal("snapshot-overview") }
}),
/**
* Gets a screenshot of the canvas's contents after the specified
* function was called.
*/
generateScreenshotFor: method(function(functionCall) {
let caller = functionCall.details.caller;
let global = functionCall.meta.global;
let canvas = this._contentCanvas;
let calls = this._functionCalls;
let index = calls.indexOf(functionCall);
// To get a screenshot, replay all the steps necessary to render the frame,
// by invoking the context calls up to and including the specified one.
// This will be done in a custom framebuffer in case of a WebGL context.
let { replayContext, lastDrawCallIndex } = ContextUtils.replayAnimationFrame({
contextType: global,
canvas: canvas,
calls: calls,
first: 0,
last: index
});
// To keep things fast, generate an image that's relatively small.
let dimensions = Math.min(CanvasFront.SCREENSHOT_HEIGHT_MAX, canvas.height);
let screenshot;
// Depending on the canvas' context, generating a screenshot is done
// in different ways. In case of the WebGL context, we also need to reset
// the framebuffer binding to the default value.
if (global == CallWatcherFront.CANVAS_WEBGL_CONTEXT) {
screenshot = ContextUtils.getPixelsForWebGL(replayContext);
replayContext.bindFramebuffer(replayContext.FRAMEBUFFER, null);
screenshot.flipped = true;
}
// In case of 2D contexts, no additional special treatment is necessary.
else if (global == CallWatcherFront.CANVAS_2D_CONTEXT) {
screenshot = ContextUtils.getPixelsFor2D(replayContext);
screenshot.flipped = false;
}
screenshot.index = lastDrawCallIndex;
return screenshot;
}, {
request: { call: Arg(0, "function-call") },
response: { screenshot: RetVal("snapshot-image") }
})
});
/**
* The corresponding Front object for the FrameSnapshotActor.
*/
let FrameSnapshotFront = protocol.FrontClass(FrameSnapshotActor, {
initialize: function(client, form) {
protocol.Front.prototype.initialize.call(this, client, form);
this._lastDrawCallScreenshot = null;
this._cachedScreenshots = new WeakMap();
},
/**
* This implementation caches the last draw call screenshot to optimize
* frontend requests to `generateScreenshotFor`.
*/
getOverview: custom(function() {
return this._getOverview().then(data => {
this._lastDrawCallScreenshot = data.screenshot;
return data;
});
}, {
impl: "_getOverview"
}),
/**
* This implementation saves a roundtrip to the backend if the screenshot
* was already generated and retrieved once.
*/
generateScreenshotFor: custom(function(functionCall) {
if (CanvasFront.ANIMATION_GENERATORS.has(functionCall.name)) {
return promise.resolve(this._lastDrawCallScreenshot);
}
let cachedScreenshot = this._cachedScreenshots.get(functionCall);
if (cachedScreenshot) {
return cachedScreenshot;
}
let screenshot = this._generateScreenshotFor(functionCall);
this._cachedScreenshots.set(functionCall, screenshot);
return screenshot;
}, {
impl: "_generateScreenshotFor"
})
});
/**
* This Canvas Actor handles simple instrumentation of all the methods
* of a 2D or WebGL context, to provide information regarding all the calls
* made when drawing frame inside an animation loop.
*/
let CanvasActor = exports.CanvasActor = protocol.ActorClass({
typeName: "canvas",
initialize: function(conn, tabActor) {
protocol.Actor.prototype.initialize.call(this, conn);
this.tabActor = tabActor;
this._onContentFunctionCall = this._onContentFunctionCall.bind(this);
},
destroy: function(conn) {
protocol.Actor.prototype.destroy.call(this, conn);
this.finalize();
},
/**
* Starts listening for function calls.
*/
setup: method(function({ reload }) {
if (this._initialized) {
return;
}
this._initialized = true;
this._callWatcher = new CallWatcherActor(this.conn, this.tabActor);
this._callWatcher.onCall = this._onContentFunctionCall;
this._callWatcher.setup({
tracedGlobals: CANVAS_CONTEXTS,
tracedFunctions: ANIMATION_GENERATORS,
performReload: reload
});
}, {
request: { reload: Option(0, "boolean") },
oneway: true
}),
/**
* Stops listening for function calls.
*/
finalize: method(function() {
if (!this._initialized) {
return;
}
this._initialized = false;
this._callWatcher.finalize();
this._callWatcher = null;
}, {
oneway: true
}),
/**
* Returns whether this actor has been set up.
*/
isInitialized: method(function() {
return !!this._initialized;
}, {
response: { initialized: RetVal("boolean") }
}),
/**
* Records a snapshot of all the calls made during the next animation frame.
* The animation should be implemented via the de-facto requestAnimationFrame
* utility, not inside a `setInterval` or recursive `setTimeout`.
*
* XXX: Currently only supporting requestAnimationFrame. When this isn't used,
* it'd be a good idea to display a huge red flashing banner telling people to
* STOP USING `setInterval` OR `setTimeout` FOR ANIMATION. Bug 978948.
*/
recordAnimationFrame: method(function() {
if (this._callWatcher.isRecording()) {
return this._currentAnimationFrameSnapshot.promise;
}
this._callWatcher.eraseRecording();
this._callWatcher.resumeRecording();
let deferred = this._currentAnimationFrameSnapshot = promise.defer();
return deferred.promise;
}, {
response: { snapshot: RetVal("frame-snapshot") }
}),
/**
* Invoked whenever an instrumented function is called, be it on a
* 2d or WebGL context, or an animation generator like requestAnimationFrame.
*/
_onContentFunctionCall: function(functionCall) {
let { window, name, args } = functionCall.details;
// The function call arguments are required to replay animation frames,
// in order to generate screenshots. However, simply storing references to
// every kind of object is a bad idea, since their properties may change.
// Consider transformation matrices for example, which are typically
// Float32Arrays whose values can easily change across context calls.
// They need to be cloned.
inplaceShallowCloneArrays(args, window);
if (CanvasFront.ANIMATION_GENERATORS.has(name)) {
this._handleAnimationFrame(functionCall);
return;
}
if (CanvasFront.DRAW_CALLS.has(name) && this._animationStarted) {
this._handleDrawCall(functionCall);
return;
}
},
/**
* Handle animations generated using requestAnimationFrame.
*/
_handleAnimationFrame: function(functionCall) {
if (!this._animationStarted) {
this._handleAnimationFrameBegin();
} else {
this._handleAnimationFrameEnd(functionCall);
}
},
/**
* Called whenever an animation frame rendering begins.
*/
_handleAnimationFrameBegin: function() {
this._callWatcher.eraseRecording();
this._animationStarted = true;
},
/**
* Called whenever an animation frame rendering ends.
*/
_handleAnimationFrameEnd: function() {
// Get a hold of all the function calls made during this animation frame.
// Since only one snapshot can be recorded at a time, erase all the
// previously recorded calls.
let functionCalls = this._callWatcher.pauseRecording();
this._callWatcher.eraseRecording();
// Since the animation frame finished, get a hold of the (already retrieved)
// canvas pixels to conveniently create a screenshot of the final rendering.
let index = this._lastDrawCallIndex;
let width = this._lastContentCanvasWidth;
let height = this._lastContentCanvasHeight;
let flipped = this._lastThumbnailFlipped;
let pixels = ContextUtils.getPixelStorage()["32bit"];
let lastDrawCallScreenshot = {
index: index,
width: width,
height: height,
flipped: flipped,
pixels: pixels.subarray(0, width * height)
};
// Wrap the function calls and screenshot in a FrameSnapshotActor instance,
// which will resolve the promise returned by `recordAnimationFrame`.
let frameSnapshot = new FrameSnapshotActor(this.conn, {
canvas: this._lastDrawCallCanvas,
calls: functionCalls,
screenshot: lastDrawCallScreenshot
});
this._currentAnimationFrameSnapshot.resolve(frameSnapshot);
this._currentAnimationFrameSnapshot = null;
this._animationStarted = false;
},
/**
* Invoked whenever a draw call is detected in the animation frame which is
* currently being recorded.
*/
_handleDrawCall: function(functionCall) {
let functionCalls = this._callWatcher.pauseRecording();
let caller = functionCall.details.caller;
let global = functionCall.meta.global;
let contentCanvas = this._lastDrawCallCanvas = caller.canvas;
let index = this._lastDrawCallIndex = functionCalls.indexOf(functionCall);
let w = this._lastContentCanvasWidth = contentCanvas.width;
let h = this._lastContentCanvasHeight = contentCanvas.height;
// To keep things fast, generate images of small and fixed dimensions.
let dimensions = CanvasFront.THUMBNAIL_HEIGHT;
let thumbnail;
// Create a thumbnail on every draw call on the canvas context, to augment
// the respective function call actor with this additional data.
if (global == CallWatcherFront.CANVAS_WEBGL_CONTEXT) {
// Check if drawing to a custom framebuffer (when rendering to texture).
// Don't create a thumbnail in this particular case.
let framebufferBinding = caller.getParameter(caller.FRAMEBUFFER_BINDING);
if (framebufferBinding == null) {
thumbnail = ContextUtils.getPixelsForWebGL(caller, 0, 0, w, h, dimensions);
thumbnail.flipped = this._lastThumbnailFlipped = true;
thumbnail.index = index;
}
} else if (global == CallWatcherFront.CANVAS_2D_CONTEXT) {
thumbnail = ContextUtils.getPixelsFor2D(caller, 0, 0, w, h, dimensions);
thumbnail.flipped = this._lastThumbnailFlipped = false;
thumbnail.index = index;
}
functionCall._thumbnail = thumbnail;
this._callWatcher.resumeRecording();
}
});
/**
* A collection of methods for manipulating canvas contexts.
*/
let ContextUtils = {
/**
* WebGL contexts are sensitive to how they're queried. Use this function
* to make sure the right context is always retrieved, if available.
*
* @param HTMLCanvasElement canvas
* The canvas element for which to get a WebGL context.
* @param WebGLRenderingContext gl
* The queried WebGL context, or null if unavailable.
*/
getWebGLContext: function(canvas) {
return canvas.getContext("webgl") ||
canvas.getContext("experimental-webgl");
},
/**
* Gets a hold of the rendered pixels in the most efficient way possible for
* a canvas with a WebGL context.
*
* @param WebGLRenderingContext gl
* The WebGL context to get a screenshot from.
* @param number srcX [optional]
* The first left pixel that is read from the framebuffer.
* @param number srcY [optional]
* The first top pixel that is read from the framebuffer.
* @param number srcWidth [optional]
* The number of pixels to read on the X axis.
* @param number srcHeight [optional]
* The number of pixels to read on the Y axis.
* @param number dstHeight [optional]
* The desired generated screenshot height.
* @return object
* An objet containing the screenshot's width, height and pixel data.
*/
getPixelsForWebGL: function(gl,
srcX = 0, srcY = 0,
srcWidth = gl.canvas.width,
srcHeight = gl.canvas.height,
dstHeight = srcHeight)
{
let contentPixels = ContextUtils.getPixelStorage(srcWidth, srcHeight);
let { "8bit": charView, "32bit": intView } = contentPixels;
gl.readPixels(srcX, srcY, srcWidth, srcHeight, gl.RGBA, gl.UNSIGNED_BYTE, charView);
return this.resizePixels(intView, srcWidth, srcHeight, dstHeight);
},
/**
* Gets a hold of the rendered pixels in the most efficient way possible for
* a canvas with a 2D context.
*
* @param CanvasRenderingContext2D ctx
* The 2D context to get a screenshot from.
* @param number srcX [optional]
* The first left pixel that is read from the canvas.
* @param number srcY [optional]
* The first top pixel that is read from the canvas.
* @param number srcWidth [optional]
* The number of pixels to read on the X axis.
* @param number srcHeight [optional]
* The number of pixels to read on the Y axis.
* @param number dstHeight [optional]
* The desired generated screenshot height.
* @return object
* An objet containing the screenshot's width, height and pixel data.
*/
getPixelsFor2D: function(ctx,
srcX = 0, srcY = 0,
srcWidth = ctx.canvas.width,
srcHeight = ctx.canvas.height,
dstHeight = srcHeight)
{
let { data } = ctx.getImageData(srcX, srcY, srcWidth, srcHeight);
let { "32bit": intView } = ContextUtils.usePixelStorage(data.buffer);
return this.resizePixels(intView, srcWidth, srcHeight, dstHeight);
},
/**
* Resizes the provided pixels to fit inside a rectangle with the specified
* height and the same aspect ratio as the source.
*
* @param Uint32Array srcPixels
* The source pixel data, assuming 32bit/pixel and 4 color components.
* @param number srcWidth
* The source pixel data width.
* @param number srcHeight
* The source pixel data height.
* @param number dstHeight [optional]
* The desired resized pixel data height.
* @return object
* An objet containing the resized pixels width, height and data.
*/
resizePixels: function(srcPixels, srcWidth, srcHeight, dstHeight) {
let screenshotRatio = dstHeight / srcHeight;
let dstWidth = Math.floor(srcWidth * screenshotRatio);
// Use a plain array instead of a Uint32Array to make serializing faster.
let dstPixels = new Array(dstWidth * dstHeight);
// If the resized image ends up being completely transparent, returning
// an empty array will skip some redundant serialization cycles.
let isTransparent = true;
for (let dstX = 0; dstX < dstWidth; dstX++) {
for (let dstY = 0; dstY < dstHeight; dstY++) {
let srcX = Math.floor(dstX / screenshotRatio);
let srcY = Math.floor(dstY / screenshotRatio);
let cPos = srcX + srcWidth * srcY;
let dPos = dstX + dstWidth * dstY;
let color = dstPixels[dPos] = srcPixels[cPos];
if (color) {
isTransparent = false;
}
}
}
return {
width: dstWidth,
height: dstHeight,
pixels: isTransparent ? [] : dstPixels
};
},
/**
* Invokes a series of canvas context calls, to "replay" an animation frame
* and generate a screenshot.
*
* In case of a WebGL context, an offscreen framebuffer is created for
* the respective canvas, and the rendering will be performed into it.
* This is necessary because some state (like shaders, textures etc.) can't
* be shared between two different WebGL contexts.
* Hopefully, once SharedResources are a thing this won't be necessary:
* http://www.khronos.org/webgl/wiki/SharedResouces
*
* In case of a 2D context, a new canvas is created, since there's no
* intrinsic state that can't be easily duplicated.
*
* @param number contexType
* The type of context to use. See the CallWatcherFront scope types.
* @param HTMLCanvasElement canvas
* The canvas element which is the source of all context calls.
* @param array calls
* An array of function call actors.
* @param number first
* The first function call to start from.
* @param number last
* The last (inclusive) function call to end at.
* @return object
* The context on which the specified calls were invoked and the
* last registered draw call's index.
*/
replayAnimationFrame: function({ contextType, canvas, calls, first, last }) {
let w = canvas.width;
let h = canvas.height;
let replayCanvas;
let replayContext;
let customFramebuffer;
let lastDrawCallIndex = -1;
// In case of WebGL contexts, rendering will be done offscreen, in a
// custom framebuffer, but on the provided canvas context.
if (contextType == CallWatcherFront.CANVAS_WEBGL_CONTEXT) {
replayCanvas = canvas;
replayContext = this.getWebGLContext(replayCanvas);
customFramebuffer = this.createBoundFramebuffer(replayContext, w, h);
}
// In case of 2D contexts, draw everything on a separate canvas context.
else if (contextType == CallWatcherFront.CANVAS_2D_CONTEXT) {
let contentDocument = canvas.ownerDocument;
replayCanvas = contentDocument.createElement("canvas");
replayCanvas.width = w;
replayCanvas.height = h;
replayContext = replayCanvas.getContext("2d");
replayContext.clearRect(0, 0, w, h);
}
// Replay all the context calls up to and including the specified one.
for (let i = first; i <= last; i++) {
let { type, name, args } = calls[i].details;
// Prevent WebGL context calls that try to reset the framebuffer binding
// to the default value, since we want to perform the rendering offscreen.
if (name == "bindFramebuffer" && args[1] == null) {
replayContext.bindFramebuffer(replayContext.FRAMEBUFFER, customFramebuffer);
} else {
if (type == CallWatcherFront.METHOD_FUNCTION) {
replayContext[name].apply(replayContext, args);
} else if (type == CallWatcherFront.SETTER_FUNCTION) {
replayContext[name] = args;
} else {
// Ignore getter calls.
}
if (CanvasFront.DRAW_CALLS.has(name)) {
lastDrawCallIndex = i;
}
}
}
return {
replayContext: replayContext,
lastDrawCallIndex: lastDrawCallIndex
};
},
/**
* Gets an object containing a buffer large enough to hold width * height
* pixels, assuming 32bit/pixel and 4 color components.
*
* This method avoids allocating memory and tries to reuse a common buffer
* as much as possible.
*
* @param number w
* The desired pixel array storage width.
* @param number h
* The desired pixel array storage height.
* @return object
* The requested pixel array buffer.
*/
getPixelStorage: function(w = 0, h = 0) {
let storage = this._currentPixelStorage;
if (storage && storage["32bit"].length >= w * h) {
return storage;
}
return this.usePixelStorage(new ArrayBuffer(w * h * 4));
},
/**
* Creates and saves the array buffer views used by `getPixelStorage`.
*
* @param ArrayBuffer buffer
* The raw buffer used as storage for various array buffer views.
*/
usePixelStorage: function(buffer) {
let array8bit = new Uint8Array(buffer);
let array32bit = new Uint32Array(buffer);
return this._currentPixelStorage = {
"8bit": array8bit,
"32bit": array32bit
};
},
/**
* Creates a framebuffer of the specified dimensions for a WebGL context,
* assuming a RGBA color buffer, a depth buffer and no stencil buffer.
*
* @param WebGLRenderingContext gl
* The WebGL context to create and bind a framebuffer for.
* @param number width
* The desired width of the renderbuffers.
* @param number height
* The desired height of the renderbuffers.
* @return WebGLFramebuffer
* The generated framebuffer object.
*/
createBoundFramebuffer: function(gl, width, height) {
let framebuffer = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
// Use a texture as the color rendebuffer attachment, since consumenrs of
// this function will most likely want to read the rendered pixels back.
let colorBuffer = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, colorBuffer);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.generateMipmap(gl.TEXTURE_2D);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
let depthBuffer = gl.createRenderbuffer();
gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer);
gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, width, height);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, colorBuffer, 0);
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depthBuffer);
gl.bindTexture(gl.TEXTURE_2D, null);
gl.bindRenderbuffer(gl.RENDERBUFFER, null);
return framebuffer;
}
};
/**
* The corresponding Front object for the CanvasActor.
*/
let CanvasFront = exports.CanvasFront = protocol.FrontClass(CanvasActor, {
initialize: function(client, { canvasActor }) {
protocol.Front.prototype.initialize.call(this, client, { actor: canvasActor });
client.addActorPool(this);
this.manage(this);
}
});
/**
* Constants.
*/
CanvasFront.CANVAS_CONTEXTS = new Set(CANVAS_CONTEXTS);
CanvasFront.ANIMATION_GENERATORS = new Set(ANIMATION_GENERATORS);
CanvasFront.DRAW_CALLS = new Set(DRAW_CALLS);
CanvasFront.INTERESTING_CALLS = new Set(INTERESTING_CALLS);
CanvasFront.THUMBNAIL_HEIGHT = 50; // px
CanvasFront.SCREENSHOT_HEIGHT_MAX = 256; // px
CanvasFront.INVALID_SNAPSHOT_IMAGE = {
index: -1,
width: 0,
height: 0,
pixels: []
};
/**
* Goes through all the arguments and creates a one-level shallow copy
* of all arrays and array buffers.
*/
function inplaceShallowCloneArrays(functionArguments, contentWindow) {
let { Object, Array, ArrayBuffer } = contentWindow;
functionArguments.forEach((arg, index, store) => {
if (arg instanceof Array) {
store[index] = arg.slice();
}
if (arg instanceof Object && arg.buffer instanceof ArrayBuffer) {
store[index] = new arg.constructor(arg);
}
});
}

View File

@ -4,9 +4,9 @@
"use strict";
const {Cc, Ci, Cu, Cr} = require("chrome");
const Services = require("Services");
const events = require("sdk/event/core");
const protocol = require("devtools/server/protocol");
const { ContentObserver } = require("devtools/content-observer");
const { on, once, off, emit } = events;
const { method, Arg, Option, RetVal } = protocol;
@ -293,7 +293,7 @@ let WebGLActor = exports.WebGLActor = protocol.ActorClass({
* This is useful for dealing with bfcache, when no new programs are linked.
*/
getPrograms: method(function() {
let id = getInnerWindowID(this.tabActor.window);
let id = ContentObserver.GetInnerWindowID(this.tabActor.window);
return this._programActorsCache.filter(e => e.ownerWindow == id);
}, {
response: { programs: RetVal("array:gl-program") }
@ -346,58 +346,6 @@ let WebGLFront = exports.WebGLFront = protocol.FrontClass(WebGLActor, {
}
});
/**
* Handles adding an observer for the creation of content document globals,
* event sent immediately after a web content document window has been set up,
* but before any script code has been executed. This will allow us to
* instrument the HTMLCanvasElement with the appropriate inspection methods.
*/
function ContentObserver(tabActor) {
this._contentWindow = tabActor.window;
this._onContentGlobalCreated = this._onContentGlobalCreated.bind(this);
this._onInnerWindowDestroyed = this._onInnerWindowDestroyed.bind(this);
this.startListening();
}
ContentObserver.prototype = {
/**
* Starts listening for the required observer messages.
*/
startListening: function() {
Services.obs.addObserver(
this._onContentGlobalCreated, "content-document-global-created", false);
Services.obs.addObserver(
this._onInnerWindowDestroyed, "inner-window-destroyed", false);
},
/**
* Stops listening for the required observer messages.
*/
stopListening: function() {
Services.obs.removeObserver(
this._onContentGlobalCreated, "content-document-global-created", false);
Services.obs.removeObserver(
this._onInnerWindowDestroyed, "inner-window-destroyed", false);
},
/**
* Fired immediately after a web content document window has been set up.
*/
_onContentGlobalCreated: function(subject, topic, data) {
if (subject == this._contentWindow) {
emit(this, "global-created", subject);
}
},
/**
* Fired when an inner window is removed from the backward/forward cache.
*/
_onInnerWindowDestroyed: function(subject, topic, data) {
let id = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
emit(this, "global-destroyed", id);
}
};
/**
* Instruments a HTMLCanvasElement with the appropriate inspection methods.
*/
@ -413,7 +361,7 @@ let WebGLInstrumenter = {
handle: function(window, observer) {
let self = this;
let id = getInnerWindowID(window);
let id = ContentObserver.GetInnerWindowID(window);
let canvasElem = XPCNativeWrapper.unwrap(window.HTMLCanvasElement);
let canvasPrototype = canvasElem.prototype;
let originalGetContext = canvasPrototype.getContext;
@ -1354,13 +1302,6 @@ WebGLProxy.prototype = {
// Utility functions.
function getInnerWindowID(window) {
return window
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
.currentInnerWindowID;
}
function removeFromMap(map, predicate) {
for (let [key, value] of map) {
if (predicate(value)) {

View File

@ -393,6 +393,8 @@ var DebuggerServer = {
this.addActors("resource://gre/modules/devtools/server/actors/script.js");
this.addActors("resource://gre/modules/devtools/server/actors/webconsole.js");
this.registerModule("devtools/server/actors/inspector");
this.registerModule("devtools/server/actors/call-watcher");
this.registerModule("devtools/server/actors/canvas");
this.registerModule("devtools/server/actors/webgl");
this.registerModule("devtools/server/actors/stylesheets");
this.registerModule("devtools/server/actors/styleeditor");
@ -401,8 +403,9 @@ var DebuggerServer = {
this.registerModule("devtools/server/actors/tracer");
this.registerModule("devtools/server/actors/memory");
this.registerModule("devtools/server/actors/eventlooplag");
if ("nsIProfiler" in Ci)
if ("nsIProfiler" in Ci) {
this.addActors("resource://gre/modules/devtools/server/actors/profiler.js");
}
},
/**

View File

@ -69,6 +69,7 @@ this.Log = {
Formatter: Formatter,
BasicFormatter: BasicFormatter,
MessageOnlyFormatter: MessageOnlyFormatter,
StructuredFormatter: StructuredFormatter,
Appender: Appender,
@ -359,13 +360,58 @@ LoggerRepository.prototype = {
}
},
getLogger: function LogRep_getLogger(name) {
/**
* Obtain a named Logger.
*
* The returned Logger instance for a particular name is shared among
* all callers. In other words, if two consumers call getLogger("foo"),
* they will both have a reference to the same object.
*
* @return Logger
*/
getLogger: function (name) {
if (name in this._loggers)
return this._loggers[name];
this._loggers[name] = new Logger(name, this);
this._updateParents(name);
return this._loggers[name];
}
},
/**
* Obtain a Logger that logs all string messages with a prefix.
*
* A common pattern is to have separate Logger instances for each instance
* of an object. But, you still want to distinguish between each instance.
* Since Log.repository.getLogger() returns shared Logger objects,
* monkeypatching one Logger modifies them all.
*
* This function returns a new object with a prototype chain that chains
* up to the original Logger instance. The new prototype has log functions
* that prefix content to each message.
*
* @param name
* (string) The Logger to retrieve.
* @param prefix
* (string) The string to prefix each logged message with.
*/
getLoggerWithMessagePrefix: function (name, prefix) {
let log = this.getLogger(name);
let proxy = {__proto__: log};
for (let level in Log.Level) {
if (level == "Desc") {
continue;
}
let lc = level.toLowerCase();
proxy[lc] = function (msg, ...args) {
return log[lc].apply(log, [prefix + msg, ...args]);
};
}
return proxy;
},
};
/*
@ -396,6 +442,19 @@ BasicFormatter.prototype = {
}
};
/**
* A formatter that only formats the string message component.
*/
function MessageOnlyFormatter() {
}
MessageOnlyFormatter.prototype = Object.freeze({
__proto__: Formatter.prototype,
format: function (message) {
return message.message + "\n";
},
});
// Structured formatter that outputs JSON based on message data.
// This formatter will format unstructured messages by supplying
// default values.

View File

@ -173,28 +173,8 @@ function openConnection(options) {
* `openConnection`.
*/
function OpenedConnection(connection, basename, number, options) {
let log = Log.repository.getLogger("Sqlite.Connection." + basename);
// getLogger() returns a shared object. We can't modify the functions on this
// object since they would have effect on all instances and last write would
// win. So, we create a "proxy" object with our custom functions. Everything
// else is proxied back to the shared logger instance via prototype
// inheritance.
let logProxy = {__proto__: log};
// Automatically prefix all log messages with the identifier.
for (let level in Log.Level) {
if (level == "Desc") {
continue;
}
let lc = level.toLowerCase();
logProxy[lc] = function (msg) {
return log[lc].call(log, "Conn #" + number + ": " + msg);
};
}
this._log = logProxy;
this._log = Log.repository.getLoggerWithMessagePrefix("Sqlite.Connection." + basename,
"Conn #" + number + ": ");
this._log.info("Opened");

View File

@ -72,6 +72,26 @@ add_test(function test_Logger_parent() {
run_next_test();
});
add_test(function test_LoggerWithMessagePrefix() {
let log = Log.repository.getLogger("test.logger.prefix");
let appender = new MockAppender(new Log.MessageOnlyFormatter());
log.addAppender(appender);
let prefixed = Log.repository.getLoggerWithMessagePrefix(
"test.logger.prefix", "prefix: ");
log.warn("no prefix");
prefixed.warn("with prefix");
Assert.equal(appender.messages.length, 2, "2 messages were logged.");
Assert.deepEqual(appender.messages, [
"no prefix\n",
"prefix: with prefix\n",
], "Prefix logger works.");
run_next_test();
});
// A utility method for checking object equivalence.
// Fields with a reqular expression value in expected will be tested
// against the corresponding value in actual. Otherwise objects

View File

@ -2317,7 +2317,25 @@ this.AddonManagerPrivate = {
return {
done: () => this.recordSimpleMeasure(aName, Date.now() - startTime)
};
}
},
/**
* Helper to call update listeners when no update is available.
*
* This can be used as an implementation for Addon.findUpdates() when
* no update mechanism is available.
*/
callNoUpdateListeners: function (addon, listener, reason, appVersion, platformVersion) {
if ("onNoCompatibilityUpdateAvailable" in listener) {
safeCall(listener.onNoCompatibilityUpdateAvailable.bind(listener), addon);
}
if ("onNoUpdateAvailable" in listener) {
safeCall(listener.onNoUpdateAvailable.bind(listener), addon);
}
if ("onUpdateFinished" in listener) {
safeCall(listener.onUpdateFinished.bind(listener), addon);
}
},
};
/**

View File

@ -508,12 +508,7 @@ function AddonWrapper(aTheme) {
};
this.findUpdates = function AddonWrapper_findUpdates(listener, reason, appVersion, platformVersion) {
if ("onNoCompatibilityUpdateAvailable" in listener)
listener.onNoCompatibilityUpdateAvailable(this);
if ("onNoUpdateAvailable" in listener)
listener.onNoUpdateAvailable(this);
if ("onUpdateFinished" in listener)
listener.onUpdateFinished(this);
AddonManagerPrivate.callNoUpdateListeners(this, listener, reason, appVersion, platformVersion);
};
}

View File

@ -908,6 +908,11 @@ function loadManifestFromRDF(aUri, aStream) {
addon.userDisabled = !!LightweightThemeManager.currentTheme ||
addon.internalName != XPIProvider.selectedSkin;
}
// Experiments are disabled by default. It is up to the Experiments Manager
// to enable them (it drives installation).
else if (addon.type == "experiment") {
addon.userDisabled = true;
}
else {
addon.userDisabled = false;
addon.softDisabled = addon.blocklistState == Ci.nsIBlocklistService.STATE_SOFTBLOCKED;
@ -915,6 +920,17 @@ function loadManifestFromRDF(aUri, aStream) {
addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DEFAULT;
// Experiments are managed and updated through an external "experiments
// manager." So disable some built-in mechanisms.
if (addon.type == "experiment") {
addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DISABLE;
addon.updateURL = null;
addon.updateKey = null;
addon.targetApplications = [];
addon.targetPlatforms = [];
}
// Load the storage service before NSS (nsIRandomGenerator),
// to avoid a SQLite initialization error (bug 717904).
let storage = Services.storage;
@ -2071,8 +2087,19 @@ var XPIProvider = {
* Persists changes to XPIProvider.bootstrappedAddons to its store (a pref).
*/
persistBootstrappedAddons: function XPI_persistBootstrappedAddons() {
// Experiments are disabled upon app load, so don't persist references.
let filtered = {};
for (let id in this.bootstrappedAddons) {
let entry = this.bootstrappedAddons[id];
if (entry.type == "experiment") {
continue;
}
filtered[id] = entry;
}
Services.prefs.setCharPref(PREF_BOOTSTRAP_ADDONS,
JSON.stringify(this.bootstrappedAddons));
JSON.stringify(filtered));
},
/**
@ -4227,12 +4254,16 @@ var XPIProvider = {
// no onDisabling/onEnabling is sent - so send a onPropertyChanged.
let appDisabledChanged = aAddon.appDisabled != appDisabled;
// Update the properties in the database
XPIDatabase.setAddonProperties(aAddon, {
userDisabled: aUserDisabled,
appDisabled: appDisabled,
softDisabled: aSoftDisabled
});
// Update the properties in the database.
// We never persist this for experiments because the disabled flags
// are controlled by the Experiments Manager.
if (aAddon.type != "experiment") {
XPIDatabase.setAddonProperties(aAddon, {
userDisabled: aUserDisabled,
appDisabled: appDisabled,
softDisabled: aSoftDisabled
});
}
if (appDisabledChanged) {
AddonManagerPrivate.callAddonListeners("onPropertyChanged",
@ -6014,6 +6045,17 @@ AddonInternal.prototype = {
},
isCompatibleWith: function AddonInternal_isCompatibleWith(aAppVersion, aPlatformVersion) {
// Experiments are installed through an external mechanism that
// limits target audience to compatible clients. We trust it knows what
// it's doing and skip compatibility checks.
//
// This decision does forfeit defense in depth. If the experiments system
// is ever wrong about targeting an add-on to a specific application
// or platform, the client will likely see errors.
if (this.type == "experiment") {
return true;
}
let app = this.matchingTargetApplication;
if (!app)
return false;
@ -6398,6 +6440,11 @@ function AddonWrapper(aAddon) {
return aAddon.applyBackgroundUpdates;
});
this.__defineSetter__("applyBackgroundUpdates", function AddonWrapper_applyBackgroundUpdatesSetter(val) {
if (this.type == "experiment") {
logger.warn("Setting applyBackgroundUpdates on an experiment is not supported.");
return;
}
if (val != AddonManager.AUTOUPDATE_DEFAULT &&
val != AddonManager.AUTOUPDATE_DISABLE &&
val != AddonManager.AUTOUPDATE_ENABLE) {
@ -6498,22 +6545,33 @@ function AddonWrapper(aAddon) {
if (!(aAddon.inDatabase))
return permissions;
// Experiments can only be uninstalled. An uninstall reflects the user
// intent of "disable this experiment." This is partially managed by the
// experiments manager.
if (aAddon.type == "experiment") {
return AddonManager.PERM_CAN_UNINSTALL;
}
if (!aAddon.appDisabled) {
if (this.userDisabled)
if (this.userDisabled) {
permissions |= AddonManager.PERM_CAN_ENABLE;
else if (aAddon.type != "theme")
}
else if (aAddon.type != "theme") {
permissions |= AddonManager.PERM_CAN_DISABLE;
}
}
// Add-ons that are in locked install locations, or are pending uninstall
// cannot be upgraded or uninstalled
if (!aAddon._installLocation.locked && !aAddon.pendingUninstall) {
// Add-ons that are installed by a file link cannot be upgraded
if (!aAddon._installLocation.isLinkedAddon(aAddon.id))
if (!aAddon._installLocation.isLinkedAddon(aAddon.id)) {
permissions |= AddonManager.PERM_CAN_UPGRADE;
}
permissions |= AddonManager.PERM_CAN_UNINSTALL;
}
return permissions;
});
@ -6595,6 +6653,14 @@ function AddonWrapper(aAddon) {
};
this.findUpdates = function AddonWrapper_findUpdates(aListener, aReason, aAppVersion, aPlatformVersion) {
// Short-circuit updates for experiments because updates are handled
// through the Experiments Manager.
if (this.type == "experiment") {
AddonManagerPrivate.callNoUpdateListeners(this, aListener, aReason,
aAppVersion, aPlatformVersion);
return;
}
new UpdateChecker(aAddon, aListener, aReason, aAppVersion, aPlatformVersion);
};

View File

@ -0,0 +1,17 @@
<?xml version="1.0"?>
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
<Description about="urn:mozilla:install-manifest">
<em:id>experiment1@tests.mozilla.org</em:id>
<em:version>1.0</em:version>
<em:type>128</em:type>
<em:bootstrap>true</em:bootstrap>
<!-- Front End MetaData -->
<em:name>Test Experiment 1</em:name>
<em:description>Test Description</em:description>
</Description>
</RDF>

View File

@ -50,8 +50,7 @@ add_test(function testActiveExperiment() {
install_addon("addons/browser_experiment1.xpi", (addon) => {
gInstalledAddons.push(addon);
// This may change if we remove compatibility checking from experiments.
// Putting this check here so a test fails if preconditions change.
Assert.ok(addon.userDisabled, "Add-on is disabled upon initial install.");
Assert.equal(addon.isActive, false, "Add-on is not active.");
Assert.ok(gCategoryUtilities.isTypeVisible("experiment"), "Experiment tab visible.");
@ -133,3 +132,22 @@ add_test(function testOpenPreferences() {
EventUtils.synthesizeMouseAtCenter(btn, {}, gManagerWindow);
});
});
add_test(function testButtonPresence() {
gCategoryUtilities.openType("experiment", (win) => {
let item = get_addon_element(gManagerWindow, "test-experiment1@experiments.mozilla.org");
Assert.ok(item, "Got add-on element.");
let el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "remove-btn");
// Corresponds to the uninstall permission.
is_element_visible(el, "Remove button is visible.");
// Corresponds to lack of disable permission.
el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "disable-btn");
is_element_hidden(el, "Disable button not visible.");
// Corresponds to lack of enable permission.
el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "enable-btn");
is_element_hidden(el, "Enable button not visible.");
run_next_test();
});
});

View File

@ -0,0 +1,89 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
let scope = Components.utils.import("resource://gre/modules/addons/XPIProvider.jsm");
const XPIProvider = scope.XPIProvider;
function run_test() {
createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
startupManager();
run_next_test();
}
add_test(function test_experiment() {
AddonManager.getInstallForFile(do_get_addon("test_experiment1"), (install) => {
completeAllInstalls([install], () => {
AddonManager.getAddonByID("experiment1@tests.mozilla.org", (addon) => {
Assert.ok(addon, "Addon is found.");
Assert.ok(addon.userDisabled, "Experiments are userDisabled by default.");
Assert.equal(addon.isActive, false, "Add-on is not active.");
Assert.equal(addon.updateURL, null, "No updateURL for experiments.");
Assert.equal(addon.applyBackgroundUpdates, AddonManager.AUTOUPDATE_DISABLE,
"Background updates are disabled.");
Assert.equal(addon.permissions, AddonManager.PERM_CAN_UNINSTALL,
"Permissions are minimal.");
// Setting applyBackgroundUpdates should not work.
addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_ENABLE;
Assert.equal(addon.applyBackgroundUpdates, AddonManager.AUTOUPDATE_DISABLE,
"Setting applyBackgroundUpdates shouldn't do anything.");
let noCompatibleCalled = false;
let noUpdateCalled = false;
let finishedCalled = false;
let listener = {
onNoCompatibilityUpdateAvailable: () => { noCompatibleCalled = true; },
onNoUpdateAvailable: () => { noUpdateCalled = true; },
onUpdateFinished: () => { finishedCalled = true; },
};
addon.findUpdates(listener, "testing", null, null);
Assert.ok(noCompatibleCalled, "Listener called.");
Assert.ok(noUpdateCalled, "Listener called.");
Assert.ok(finishedCalled, "Listener called.");
run_next_test();
});
});
});
});
// Changes to userDisabled should not be persisted to the database.
add_test(function test_userDisabledNotPersisted() {
AddonManager.getAddonByID("experiment1@tests.mozilla.org", (addon) => {
Assert.ok(addon, "Addon is found.");
let listener = {
onEnabled: (addon2) => {
Assert.equal(addon2.id, addon.id, "Changed add-on matches expected.");
Assert.ok(addon2.isActive, "Add-on is no longer disabled.");
Assert.ok("experiment1@tests.mozilla.org" in XPIProvider.bootstrappedAddons,
"Experiment add-on listed in XPIProvider bootstrapped list.");
AddonManager.getAddonByID("experiment1@tests.mozilla.org", (addon) => {
Assert.ok(addon, "Add-on retrieved.");
Assert.ok(addon.userDisabled, "Add-on is disabled according to database.");
restartManager();
let persisted = JSON.parse(Services.prefs.getCharPref("extensions.bootstrappedAddons"));
Assert.ok(!("experiment1@tests.mozilla.org" in persisted),
"Experiment add-on not persisted to bootstrappedAddons.");
AddonManager.getAddonByID("experiment1@tests.mozilla.org", (addon) => {
Assert.ok(addon, "Add-on retrieved.");
Assert.ok(addon.userDisabled, "Add-on is disabled after restart.");
run_next_test();
});
});
},
};
AddonManager.addAddonListener(listener);
addon.userDisabled = false;
});
});

View File

@ -17,7 +17,8 @@ const IGNORE_PRIVATE = ["AddonAuthor", "AddonCompatibilityOverride",
"addStartupChange", "removeStartupChange",
"recordTimestamp", "recordSimpleMeasure",
"recordException", "getSimpleMeasures", "simpleTimer",
"setTelemetryDetails", "getTelemetryDetails"];
"setTelemetryDetails", "getTelemetryDetails",
"callNoUpdateListeners"];
function test_functions() {
for (let prop in AddonManager) {

View File

@ -163,6 +163,7 @@ fail-if = os == "android"
# Bug 676992: test consistently hangs on Android
skip-if = os == "android"
[test_error.js]
[test_experiment.js]
[test_filepointer.js]
# Bug 676992: test consistently hangs on Android
skip-if = os == "android"