mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 917226 - Build a canvas inspection tool, r=rcampbell, jryans
This commit is contained in:
parent
96b586969b
commit
41f23a846e
@ -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);
|
||||
|
||||
|
1270
browser/devtools/canvasdebugger/canvasdebugger.js
Normal file
1270
browser/devtools/canvasdebugger/canvasdebugger.js
Normal file
File diff suppressed because it is too large
Load Diff
131
browser/devtools/canvasdebugger/canvasdebugger.xul
Normal file
131
browser/devtools/canvasdebugger/canvasdebugger.xul
Normal 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>
|
12
browser/devtools/canvasdebugger/moz.build
Normal file
12
browser/devtools/canvasdebugger/moz.build
Normal 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'
|
||||
]
|
72
browser/devtools/canvasdebugger/panel.js
Normal file
72
browser/devtools/canvasdebugger/panel.js
Normal 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");
|
||||
});
|
||||
}
|
||||
};
|
34
browser/devtools/canvasdebugger/test/browser.ini
Normal file
34
browser/devtools/canvasdebugger/test/browser.ini
Normal 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]
|
@ -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();
|
||||
}
|
@ -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();
|
||||
}
|
@ -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();
|
||||
}
|
@ -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();
|
||||
}
|
@ -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();
|
||||
}
|
@ -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();
|
||||
}
|
@ -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;
|
||||
}
|
@ -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();
|
||||
}
|
@ -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();
|
||||
}
|
@ -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();
|
||||
}
|
@ -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();
|
||||
}
|
@ -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();
|
||||
}
|
@ -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();
|
||||
}
|
@ -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();
|
||||
}
|
@ -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();
|
||||
}
|
@ -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();
|
||||
}
|
@ -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();
|
||||
}
|
@ -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();
|
||||
}
|
@ -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();
|
||||
}
|
@ -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();
|
||||
}
|
@ -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();
|
||||
}
|
@ -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();
|
||||
}
|
@ -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();
|
||||
}
|
@ -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();
|
||||
}
|
@ -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;
|
||||
}
|
@ -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();
|
||||
}
|
@ -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();
|
||||
}
|
@ -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>
|
@ -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>
|
37
browser/devtools/canvasdebugger/test/doc_simple-canvas.html
Normal file
37
browser/devtools/canvasdebugger/test/doc_simple-canvas.html
Normal 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>
|
234
browser/devtools/canvasdebugger/test/head.js
Normal file
234
browser/devtools/canvasdebugger/test/head.js
Normal 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)
|
||||
]);
|
||||
}
|
6
browser/devtools/canvasdebugger/test/moz.build
Normal file
6
browser/devtools/canvasdebugger/test/moz.build
Normal 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']
|
@ -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
|
||||
|
@ -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">
|
||||
|
@ -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);
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -6,6 +6,7 @@
|
||||
|
||||
DIRS += [
|
||||
'app-manager',
|
||||
'canvasdebugger',
|
||||
'commandline',
|
||||
'debugger',
|
||||
'fontinspector',
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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);
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -10,4 +10,3 @@ JS_MODULES_PATH = 'modules/devtools/shadereditor'
|
||||
EXTRA_JS_MODULES += [
|
||||
'panel.js'
|
||||
]
|
||||
|
||||
|
@ -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);
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -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");
|
||||
|
||||
|
@ -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());
|
||||
|
||||
/**
|
||||
|
@ -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 <canvas> 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">
|
@ -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
|
@ -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)
|
||||
|
5
browser/themes/linux/devtools/canvasdebugger.css
Normal file
5
browser/themes/linux/devtools/canvasdebugger.css
Normal 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
|
@ -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)
|
||||
|
6
browser/themes/osx/devtools/canvasdebugger.css
Normal file
6
browser/themes/osx/devtools/canvasdebugger.css
Normal 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
|
@ -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)
|
||||
|
501
browser/themes/shared/devtools/canvasdebugger.inc.css
Normal file
501
browser/themes/shared/devtools/canvasdebugger.inc.css
Normal 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;
|
||||
}
|
@ -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);
|
||||
|
@ -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 */
|
||||
}
|
||||
|
||||
|
5
browser/themes/windows/devtools/canvasdebugger.css
Normal file
5
browser/themes/windows/devtools/canvasdebugger.css
Normal 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
|
@ -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)
|
||||
|
@ -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);
|
||||
};
|
||||
|
||||
|
@ -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
|
||||
|
72
toolkit/devtools/content-observer.js
Normal file
72
toolkit/devtools/content-observer.js
Normal 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;
|
||||
};
|
559
toolkit/devtools/server/actors/call-watcher.js
Normal file
559
toolkit/devtools/server/actors/call-watcher.js
Normal 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;
|
||||
}
|
759
toolkit/devtools/server/actors/canvas.js
Normal file
759
toolkit/devtools/server/actors/canvas.js
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
@ -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)) {
|
||||
|
@ -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");
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user