--- a/devtools/.eslintrc.js +++ b/devtools/.eslintrc.js @@ -101,16 +101,41 @@ module.exports = { "client/framework/**", "client/scratchpad/**", "client/webide/**", ], "rules": { "strict": "off", } }, { + "files": [ + // Note: Bug 1403938 may be removing canvasdebugger, check before + // doing more work on enabling these rules. + "client/canvasdebugger/**", + // Note: Bug 1342237 may be removing shadereditor, check before + // doing more work on enabling these rules. + "client/shadereditor/**", + // Note: Bug 1403944 may be removing webaudioeditor, check before + // doing more work on enabling these rules. + "client/webaudioeditor/**", + ], + "rules": { + "consistent-return": "off", + "max-len": "off", + "mozilla/no-aArgs": "off", + "mozilla/var-only-at-top-level": "off", + "no-redeclare": "off", + "no-return-assign": "off", + "no-shadow": "off", + "no-undef": "off", + "no-unused-vars": "off", + "no-useless-call": "off", + "strict": "off", + } + }, { // For all head*.js files, turn off no-unused-vars at a global level "files": [ "**/head*.js", ], "rules": { "no-unused-vars": ["error", {"args": "none", "vars": "local"}], } }, {
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/callslist.js @@ -0,0 +1,453 @@ +/* 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/. */ +/* import-globals-from canvasdebugger.js */ +/* globals window, document */ +"use strict"; + +const { METHOD_FUNCTION } = require("devtools/shared/fronts/function-call"); +/** + * Functions handling details about a single recorded animation frame snapshot + * (the calls list, rendering preview, thumbnails filmstrip etc.). + */ +var CallsListView = extend(WidgetMethods, { + /** + * Initialization function, called when the tool is started. + */ + initialize: function() { + this.widget = new SideMenuWidget($("#calls-list")); + this._searchbox = $("#calls-searchbox"); + this._filmstrip = $("#snapshot-filmstrip"); + + this._onSelect = this._onSelect.bind(this); + this._onSearch = this._onSearch.bind(this); + this._onScroll = this._onScroll.bind(this); + this._onExpand = this._onExpand.bind(this); + this._onStackFileClick = this._onStackFileClick.bind(this); + this._onThumbnailClick = this._onThumbnailClick.bind(this); + + this.widget.addEventListener("select", this._onSelect); + this._searchbox.addEventListener("input", this._onSearch); + this._filmstrip.addEventListener("wheel", this._onScroll); + }, + + /** + * Destruction function, called when the tool is closed. + */ + destroy: function() { + this.widget.removeEventListener("select", this._onSelect); + this._searchbox.removeEventListener("input", this._onSearch); + this._filmstrip.removeEventListener("wheel", this._onScroll); + }, + + /** + * Populates this container with a list of function calls. + * + * @param array functionCalls + * A list of function call actors received from the backend. + */ + showCalls: function(functionCalls) { + this.empty(); + + for (let i = 0, len = functionCalls.length; i < len; i++) { + const call = functionCalls[i]; + + const view = document.createElement("vbox"); + view.className = "call-item-view devtools-monospace"; + view.setAttribute("flex", "1"); + + const contents = document.createElement("hbox"); + contents.className = "call-item-contents"; + contents.setAttribute("align", "center"); + contents.addEventListener("dblclick", this._onExpand); + view.appendChild(contents); + + const index = document.createElement("label"); + index.className = "plain call-item-index"; + index.setAttribute("flex", "1"); + index.setAttribute("value", i + 1); + + const gutter = document.createElement("hbox"); + gutter.className = "call-item-gutter"; + gutter.appendChild(index); + contents.appendChild(gutter); + + if (call.callerPreview) { + const context = document.createElement("label"); + context.className = "plain call-item-context"; + context.setAttribute("value", call.callerPreview); + contents.appendChild(context); + + const separator = document.createElement("label"); + separator.className = "plain call-item-separator"; + separator.setAttribute("value", "."); + contents.appendChild(separator); + } + + const name = document.createElement("label"); + name.className = "plain call-item-name"; + name.setAttribute("value", call.name); + contents.appendChild(name); + + const argsPreview = document.createElement("label"); + argsPreview.className = "plain call-item-args"; + argsPreview.setAttribute("crop", "end"); + argsPreview.setAttribute("flex", "100"); + // Getters and setters are displayed differently from regular methods. + if (call.type == METHOD_FUNCTION) { + argsPreview.setAttribute("value", "(" + call.argsPreview + ")"); + } else { + argsPreview.setAttribute("value", " = " + call.argsPreview); + } + contents.appendChild(argsPreview); + + const location = document.createElement("label"); + location.className = "plain call-item-location"; + location.setAttribute("value", getFileName(call.file) + ":" + call.line); + location.setAttribute("crop", "start"); + location.setAttribute("flex", "1"); + location.addEventListener("mousedown", this._onExpand); + contents.appendChild(location); + + // Append a function call item to this container. + this.push([view], { + staged: true, + attachment: { + actor: call, + }, + }); + + // Highlight certain calls that are probably more interesting than + // everything else, making it easier to quickly glance over them. + if (CanvasFront.DRAW_CALLS.has(call.name)) { + view.setAttribute("draw-call", ""); + } + if (CanvasFront.INTERESTING_CALLS.has(call.name)) { + view.setAttribute("interesting-call", ""); + } + } + + // Flushes all the prepared function call items into this container. + this.commit(); + window.emit(EVENTS.CALL_LIST_POPULATED); + }, + + /** + * Displays an image in the rendering preview of this container, generated + * for the specified draw call in the recorded animation frame snapshot. + * + * @param array screenshot + * A single "snapshot-image" instance received from the backend. + */ + showScreenshot: function(screenshot) { + const { index, width, height, scaling, flipped, pixels } = screenshot; + + const screenshotNode = $("#screenshot-image"); + screenshotNode.setAttribute("flipped", flipped); + drawBackground("screenshot-rendering", width, height, pixels); + + const dimensionsNode = $("#screenshot-dimensions"); + const actualWidth = (width / scaling) | 0; + const actualHeight = (height / scaling) | 0; + dimensionsNode.setAttribute("value", + SHARED_L10N.getFormatStr("dimensions", actualWidth, actualHeight)); + + window.emit(EVENTS.CALL_SCREENSHOT_DISPLAYED); + }, + + /** + * Populates this container's footer with a list of thumbnails, one generated + * for each draw call in the recorded animation frame snapshot. + * + * @param array thumbnails + * An array of "snapshot-image" instances received from the backend. + */ + showThumbnails: function(thumbnails) { + while (this._filmstrip.hasChildNodes()) { + this._filmstrip.firstChild.remove(); + } + for (const thumbnail of thumbnails) { + this.appendThumbnail(thumbnail); + } + + window.emit(EVENTS.THUMBNAILS_DISPLAYED); + }, + + /** + * Displays an image in the thumbnails list of this container, generated + * for the specified draw call in the recorded animation frame snapshot. + * + * @param array thumbnail + * A single "snapshot-image" instance received from the backend. + */ + appendThumbnail: function(thumbnail) { + const { index, width, height, flipped, pixels } = thumbnail; + + const thumbnailNode = document.createElementNS(HTML_NS, "canvas"); + thumbnailNode.setAttribute("flipped", flipped); + thumbnailNode.width = Math.max(CanvasFront.THUMBNAIL_SIZE, width); + thumbnailNode.height = Math.max(CanvasFront.THUMBNAIL_SIZE, height); + drawImage(thumbnailNode, width, height, pixels, { centered: true }); + + thumbnailNode.className = "filmstrip-thumbnail"; + thumbnailNode.onmousedown = e => this._onThumbnailClick(e, index); + thumbnailNode.setAttribute("index", index); + this._filmstrip.appendChild(thumbnailNode); + }, + + /** + * Sets the currently highlighted thumbnail in this container. + * A screenshot will always correlate to a thumbnail in the filmstrip, + * both being identified by the same 'index' of the context function call. + * + * @param number index + * The context function call's index. + */ + set highlightedThumbnail(index) { + const currHighlightedThumbnail = $(".filmstrip-thumbnail[index='" + index + "']"); + if (currHighlightedThumbnail == null) { + return; + } + + const prevIndex = this._highlightedThumbnailIndex; + const prevHighlightedThumbnail = $(".filmstrip-thumbnail[index='" + prevIndex + "']"); + if (prevHighlightedThumbnail) { + prevHighlightedThumbnail.removeAttribute("highlighted"); + } + + currHighlightedThumbnail.setAttribute("highlighted", ""); + currHighlightedThumbnail.scrollIntoView(); + this._highlightedThumbnailIndex = index; + }, + + /** + * Gets the currently highlighted thumbnail in this container. + * @return number + */ + get highlightedThumbnail() { + return this._highlightedThumbnailIndex; + }, + + /** + * The select listener for this container. + */ + _onSelect: function({ detail: callItem }) { + if (!callItem) { + return; + } + + // Some of the stepping buttons don't make sense specifically while the + // last function call is selected. + if (this.selectedIndex == this.itemCount - 1) { + $("#resume").setAttribute("disabled", "true"); + $("#step-over").setAttribute("disabled", "true"); + $("#step-out").setAttribute("disabled", "true"); + } else { + $("#resume").removeAttribute("disabled"); + $("#step-over").removeAttribute("disabled"); + $("#step-out").removeAttribute("disabled"); + } + + // Can't generate screenshots for function call actors loaded from disk. + // XXX: Bug 984844. + if (callItem.attachment.actor.isLoadedFromDisk) { + return; + } + + // To keep continuous selection buttery smooth (for example, while pressing + // the DOWN key or moving the slider), only display the screenshot after + // any kind of user input stops. + setConditionalTimeout("screenshot-display", SCREENSHOT_DISPLAY_DELAY, () => { + return !this._isSliding; + }, () => { + const frameSnapshot = SnapshotsListView.selectedItem.attachment.actor; + const functionCall = callItem.attachment.actor; + frameSnapshot.generateScreenshotFor(functionCall).then(screenshot => { + this.showScreenshot(screenshot); + this.highlightedThumbnail = screenshot.index; + }).catch(console.error); + }); + }, + + /** + * The input listener for the calls searchbox. + */ + _onSearch: function(e) { + const lowerCaseSearchToken = this._searchbox.value.toLowerCase(); + + this.filterContents(e => { + const call = e.attachment.actor; + const name = call.name.toLowerCase(); + const file = call.file.toLowerCase(); + const line = call.line.toString().toLowerCase(); + const args = call.argsPreview.toLowerCase(); + + return name.includes(lowerCaseSearchToken) || + file.includes(lowerCaseSearchToken) || + line.includes(lowerCaseSearchToken) || + args.includes(lowerCaseSearchToken); + }); + }, + + /** + * The wheel listener for the filmstrip that contains all the thumbnails. + */ + _onScroll: function(e) { + this._filmstrip.scrollLeft += e.deltaX; + }, + + /** + * The click/dblclick listener for an item or location url in this container. + * When expanding an item, it's corresponding call stack will be displayed. + */ + _onExpand: function(e) { + const callItem = this.getItemForElement(e.target); + const view = $(".call-item-view", callItem.target); + + // If the call stack nodes were already created, simply re-show them + // or jump to the corresponding file and line in the Debugger if a + // location link was clicked. + if (view.hasAttribute("call-stack-populated")) { + const isExpanded = view.getAttribute("call-stack-expanded") == "true"; + + // If clicking on the location, jump to the Debugger. + if (e.target.classList.contains("call-item-location")) { + const { file, line } = callItem.attachment.actor; + this._viewSourceInDebugger(file, line); + return; + } + // Otherwise hide the call stack. + + view.setAttribute("call-stack-expanded", !isExpanded); + $(".call-item-stack", view).hidden = isExpanded; + return; + } + + const list = document.createElement("vbox"); + list.className = "call-item-stack"; + view.setAttribute("call-stack-populated", ""); + view.setAttribute("call-stack-expanded", "true"); + view.appendChild(list); + + /** + * Creates a function call nodes in this container for a stack. + */ + const display = stack => { + for (let i = 1; i < stack.length; i++) { + const call = stack[i]; + + const contents = document.createElement("hbox"); + contents.className = "call-item-stack-fn"; + contents.style.paddingInlineStart = (i * STACK_FUNC_INDENTATION) + "px"; + + const name = document.createElement("label"); + name.className = "plain call-item-stack-fn-name"; + name.setAttribute("value", "↳ " + call.name + "()"); + contents.appendChild(name); + + const spacer = document.createElement("spacer"); + spacer.setAttribute("flex", "100"); + contents.appendChild(spacer); + + const location = document.createElement("label"); + location.className = "plain call-item-stack-fn-location"; + location.setAttribute("value", getFileName(call.file) + ":" + call.line); + location.setAttribute("crop", "start"); + location.setAttribute("flex", "1"); + location.addEventListener("mousedown", e => this._onStackFileClick(e, call)); + contents.appendChild(location); + + list.appendChild(contents); + } + + window.emit(EVENTS.CALL_STACK_DISPLAYED); + }; + + // If this animation snapshot is loaded from disk, there are no corresponding + // backend actors available and the data is immediately available. + const functionCall = callItem.attachment.actor; + if (functionCall.isLoadedFromDisk) { + display(functionCall.stack); + } else { + // ..otherwise we need to request the function call stack from the backend. + callItem.attachment.actor.getDetails().then(fn => display(fn.stack)); + } + }, + + /** + * The click listener for a location link in the call stack. + * + * @param string file + * The url of the source owning the function. + * @param number line + * The line of the respective function. + */ + _onStackFileClick: function(e, { file, line }) { + this._viewSourceInDebugger(file, line); + }, + + /** + * The click listener for a thumbnail in the filmstrip. + * + * @param number index + * The function index in the recorded animation frame snapshot. + */ + _onThumbnailClick: function(e, index) { + this.selectedIndex = index; + }, + + /** + * The click listener for the "resume" button in this container's toolbar. + */ + _onResume: function() { + // Jump to the next draw call in the recorded animation frame snapshot. + const drawCall = getNextDrawCall(this.items, this.selectedItem); + if (drawCall) { + this.selectedItem = drawCall; + return; + } + + // If there are no more draw calls, just jump to the last context call. + this._onStepOut(); + }, + + /** + * The click listener for the "step over" button in this container's toolbar. + */ + _onStepOver: function() { + this.selectedIndex++; + }, + + /** + * The click listener for the "step in" button in this container's toolbar. + */ + _onStepIn: function() { + if (this.selectedIndex == -1) { + this._onResume(); + return; + } + const callItem = this.selectedItem; + const { file, line } = callItem.attachment.actor; + this._viewSourceInDebugger(file, line); + }, + + /** + * The click listener for the "step out" button in this container's toolbar. + */ + _onStepOut: function() { + this.selectedIndex = this.itemCount - 1; + }, + + /** + * Opens the specified file and line in the debugger. Falls back to Firefox's View Source. + */ + _viewSourceInDebugger: function(file, line) { + gToolbox.viewSourceInDebugger(file, line).then(success => { + if (success) { + window.emit(EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER); + } else { + window.emit(EVENTS.SOURCE_NOT_FOUND_IN_JS_DEBUGGER); + } + }); + }, +});
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/canvasdebugger.js @@ -0,0 +1,334 @@ +/* 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 { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm"); +const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm"); +const { SideMenuWidget } = require("resource://devtools/client/shared/widgets/SideMenuWidget.jsm"); +const Services = require("Services"); +const EventEmitter = require("devtools/shared/event-emitter"); +const { CanvasFront } = require("devtools/shared/fronts/canvas"); +const DevToolsUtils = require("devtools/shared/DevToolsUtils"); +const { extend } = require("devtools/shared/extend"); +const flags = require("devtools/shared/flags"); +const { LocalizationHelper } = require("devtools/shared/l10n"); +const { PluralForm } = require("devtools/shared/plural-form"); +const { WidgetMethods, setNamedTimeout, clearNamedTimeout, + setConditionalTimeout } = require("devtools/client/shared/widgets/view-helpers"); + +// Use privileged promise in panel documents to prevent having them to freeze +// during toolbox destruction. See bug 1402779. +const Promise = require("Promise"); + +const CANVAS_ACTOR_RECORDING_ATTEMPT = flags.testing ? 500 : 5000; + +ChromeUtils.defineModuleGetter(this, "FileUtils", + "resource://gre/modules/FileUtils.jsm"); + +ChromeUtils.defineModuleGetter(this, "NetUtil", + "resource://gre/modules/NetUtil.jsm"); + +XPCOMUtils.defineLazyGetter(this, "NetworkHelper", function() { + return require("devtools/shared/webconsole/network-helper"); +}); + +// The panel's window global is an EventEmitter firing the following events: +const EVENTS = { + // When the UI is reset from tab navigation. + UI_RESET: "CanvasDebugger:UIReset", + + // When all the animation frame snapshots are removed by the user. + SNAPSHOTS_LIST_CLEARED: "CanvasDebugger:SnapshotsListCleared", + + // When an animation frame snapshot starts/finishes being recorded, and + // whether it was completed succesfully or cancelled. + SNAPSHOT_RECORDING_STARTED: "CanvasDebugger:SnapshotRecordingStarted", + SNAPSHOT_RECORDING_FINISHED: "CanvasDebugger:SnapshotRecordingFinished", + SNAPSHOT_RECORDING_COMPLETED: "CanvasDebugger:SnapshotRecordingCompleted", + SNAPSHOT_RECORDING_CANCELLED: "CanvasDebugger:SnapshotRecordingCancelled", + + // When an animation frame snapshot was selected and all its data displayed. + SNAPSHOT_RECORDING_SELECTED: "CanvasDebugger:SnapshotRecordingSelected", + + // After all the function calls associated with an animation frame snapshot + // are displayed in the UI. + CALL_LIST_POPULATED: "CanvasDebugger:CallListPopulated", + + // After the stack associated with a call in an animation frame snapshot + // is displayed in the UI. + CALL_STACK_DISPLAYED: "CanvasDebugger:CallStackDisplayed", + + // After a screenshot associated with a call in an animation frame snapshot + // is displayed in the UI. + CALL_SCREENSHOT_DISPLAYED: "CanvasDebugger:ScreenshotDisplayed", + + // After all the thumbnails associated with an animation frame snapshot + // are displayed in the UI. + THUMBNAILS_DISPLAYED: "CanvasDebugger:ThumbnailsDisplayed", + + // When a source is shown in the JavaScript Debugger at a specific location. + SOURCE_SHOWN_IN_JS_DEBUGGER: "CanvasDebugger:SourceShownInJsDebugger", + SOURCE_NOT_FOUND_IN_JS_DEBUGGER: "CanvasDebugger:SourceNotFoundInJsDebugger", +}; +XPCOMUtils.defineConstant(this, "EVENTS", EVENTS); + +const HTML_NS = "http://www.w3.org/1999/xhtml"; +const STRINGS_URI = "devtools/client/locales/canvasdebugger.properties"; +const SHARED_STRINGS_URI = "devtools/client/locales/shared.properties"; + +const SNAPSHOT_START_RECORDING_DELAY = 10; // ms +const SNAPSHOT_DATA_EXPORT_MAX_BLOCK = 1000; // ms +const SNAPSHOT_DATA_DISPLAY_DELAY = 10; // ms +const SCREENSHOT_DISPLAY_DELAY = 100; // ms +const STACK_FUNC_INDENTATION = 14; // px + +// This identifier string is simply used to tentatively ascertain whether or not +// a JSON loaded from disk is actually something generated by this tool or not. +// It isn't, of course, a definitive verification, but a Good Enoughâ„¢ +// approximation before continuing the import. Don't localize this. +const CALLS_LIST_SERIALIZER_IDENTIFIER = "Recorded Animation Frame Snapshot"; +const CALLS_LIST_SERIALIZER_VERSION = 1; +const CALLS_LIST_SLOW_SAVE_DELAY = 100; // ms + +/** + * The current target and the Canvas front, set by this tool's host. + */ +var gToolbox, gTarget, gFront; + +/** + * Initializes the canvas debugger controller and views. + */ +function startupCanvasDebugger() { + return Promise.all([ + EventsHandler.initialize(), + SnapshotsListView.initialize(), + CallsListView.initialize(), + ]); +} + +/** + * Destroys the canvas debugger controller and views. + */ +function shutdownCanvasDebugger() { + return Promise.all([ + EventsHandler.destroy(), + SnapshotsListView.destroy(), + CallsListView.destroy(), + ]); +} + +/** + * Functions handling target-related lifetime events. + */ +var EventsHandler = { + /** + * Listen for events emitted by the current tab target. + */ + initialize: function() { + // Make sure the backend is prepared to handle <canvas> contexts. + // Since actors are created lazily on the first request to them, we need to send an + // early request to ensure the CallWatcherActor is running and watching for new window + // globals. + gFront.setup({ reload: false }); + + this._onTabWillNavigate = this._onTabWillNavigate.bind(this); + gTarget.on("will-navigate", this._onTabWillNavigate); + }, + + /** + * Remove events emitted by the current tab target. + */ + destroy: function() { + gTarget.off("will-navigate", this._onTabWillNavigate); + }, + + /** + * Called for each location change in the debugged tab. + */ + _onTabWillNavigate: function() { + // Reset UI. + SnapshotsListView.empty(); + CallsListView.empty(); + + $("#record-snapshot").removeAttribute("checked"); + $("#record-snapshot").removeAttribute("disabled"); + $("#record-snapshot").hidden = false; + + $("#reload-notice").hidden = true; + $("#empty-notice").hidden = false; + $("#waiting-notice").hidden = true; + + $("#debugging-pane-contents").hidden = true; + $("#screenshot-container").hidden = true; + $("#snapshot-filmstrip").hidden = true; + + window.emit(EVENTS.UI_RESET); + }, +}; + +/** + * Localization convenience methods. + */ +var L10N = new LocalizationHelper(STRINGS_URI); +var SHARED_L10N = new LocalizationHelper(SHARED_STRINGS_URI); + +/** + * Convenient way of emitting events from the panel window. + */ +EventEmitter.decorate(this); + +/** + * DOM query helpers. + */ +var $ = (selector, target = document) => target.querySelector(selector); +var $all = (selector, target = document) => target.querySelectorAll(selector); + +/** + * Gets the fileName part of a string which happens to be an URL. + */ +function getFileName(url) { + try { + const { fileName } = NetworkHelper.nsIURL(url); + return fileName || "/"; + } catch (e) { + // This doesn't look like a url, or nsIURL can't handle it. + return ""; + } +} + +/** + * Gets an image data object containing a buffer large enough to hold + * width * height pixels. + * + * This method avoids allocating memory and tries to reuse a common buffer + * as much as possible. + * + * @param number w + * The desired image data storage width. + * @param number h + * The desired image data storage height. + * @return ImageData + * The requested image data buffer. + */ +function getImageDataStorage(ctx, w, h) { + const storage = getImageDataStorage.cache; + if (storage && storage.width == w && storage.height == h) { + return storage; + } + return getImageDataStorage.cache = ctx.createImageData(w, h); +} + +// The cache used in the `getImageDataStorage` function. +getImageDataStorage.cache = null; + +/** + * Draws image data into a canvas. + * + * This method makes absolutely no assumptions about the canvas element + * dimensions, or pre-existing rendering. It's a dumb proxy that copies pixels. + * + * @param HTMLCanvasElement canvas + * The canvas element to put the image data into. + * @param number width + * The image data width. + * @param number height + * The image data height. + * @param array pixels + * An array buffer view of the image data. + * @param object options + * Additional options supported by this operation: + * - centered: specifies whether the image data should be centered + * when copied in the canvas; this is useful when the + * supplied pixels don't completely cover the canvas. + */ +function drawImage(canvas, width, height, pixels, options = {}) { + const ctx = canvas.getContext("2d"); + + // FrameSnapshot actors return "snapshot-image" type instances with just an + // empty pixel array if the source image is completely transparent. + if (pixels.length <= 1) { + ctx.clearRect(0, 0, canvas.width, canvas.height); + return; + } + + const imageData = getImageDataStorage(ctx, width, height); + imageData.data.set(pixels); + + if (options.centered) { + const left = (canvas.width - width) / 2; + const top = (canvas.height - height) / 2; + ctx.putImageData(imageData, left, top); + } else { + ctx.putImageData(imageData, 0, 0); + } +} + +/** + * Draws image data into a canvas, and sets that as the rendering source for + * an element with the specified id as the -moz-element background image. + * + * @param string id + * The id of the -moz-element background image. + * @param number width + * The image data width. + * @param number height + * The image data height. + * @param array pixels + * An array buffer view of the image data. + */ +function drawBackground(id, width, height, pixels) { + const canvas = document.createElementNS(HTML_NS, "canvas"); + canvas.width = width; + canvas.height = height; + + drawImage(canvas, width, height, pixels); + document.mozSetImageElement(id, canvas); + + // Used in tests. Not emitting an event because this shouldn't be "interesting". + if (window._onMozSetImageElement) { + window._onMozSetImageElement(pixels); + } +} + +/** + * Iterates forward to find the next draw call in a snapshot. + */ +function getNextDrawCall(calls, call) { + for (let i = calls.indexOf(call) + 1, len = calls.length; i < len; i++) { + const nextCall = calls[i]; + const name = nextCall.attachment.actor.name; + if (CanvasFront.DRAW_CALLS.has(name)) { + return nextCall; + } + } + return null; +} + +/** + * Iterates backwards to find the most recent screenshot for a function call + * in a snapshot loaded from disk. + */ +function getScreenshotFromCallLoadedFromDisk(calls, call) { + for (let i = calls.indexOf(call); i >= 0; i--) { + const prevCall = calls[i]; + const screenshot = prevCall.screenshot; + if (screenshot) { + return screenshot; + } + } + return CanvasFront.INVALID_SNAPSHOT_IMAGE; +} + +/** + * Iterates backwards to find the most recent thumbnail for a function call. + */ +function getThumbnailForCall(thumbnails, index) { + for (let i = thumbnails.length - 1; i >= 0; i--) { + const thumbnail = thumbnails[i]; + if (thumbnail.index <= index) { + return thumbnail; + } + } + return CanvasFront.INVALID_SNAPSHOT_IMAGE; +}
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/index.xul @@ -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://global/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://devtools/content/shared/widgets/widgets.css" type="text/css"?> +<?xml-stylesheet href="chrome://devtools/skin/widgets.css" type="text/css"?> +<?xml-stylesheet href="chrome://devtools/skin/canvasdebugger.css" type="text/css"?> +<!DOCTYPE window [ + <!ENTITY % canvasDebuggerDTD SYSTEM "chrome://devtools/locale/canvasdebugger.dtd"> + %canvasDebuggerDTD; +]> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://devtools/content/shared/theme-switching.js"/> + <script type="application/javascript" src="canvasdebugger.js"/> + <script type="application/javascript" src="callslist.js"/> + <script type="application/javascript" src="snapshotslist.js"/> + + <hbox class="theme-body" flex="1"> + <vbox id="snapshots-pane"> + <toolbar id="snapshots-toolbar" + class="devtools-toolbar"> + <hbox id="snapshots-controls"> + <toolbarbutton id="clear-snapshots" + class="devtools-toolbarbutton devtools-clear-icon" + oncommand="SnapshotsListView._onClearButtonClick()" + tooltiptext="&canvasDebuggerUI.clearSnapshots;"/> + <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()" + tooltiptext="&canvasDebuggerUI.importSnapshot;"/> + </hbox> + </toolbar> + <vbox id="snapshots-list" flex="1"/> + </vbox> + + <vbox id="debugging-pane" class="devtools-main-content" flex="1"> + <hbox id="reload-notice" + class="notice-container" + align="center" + pack="center" + flex="1"> + <button id="reload-notice-button" + class="devtools-toolbarbutton" + standalone="true" + 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" + standalone="true" + oncommand="SnapshotsListView._onRecordButtonClick()"/> + <label value="&canvasDebuggerUI.emptyNotice2;"/> + </hbox> + + <hbox id="waiting-notice" + class="notice-container devtools-throbber" + align="center" + pack="center" + flex="1" + hidden="true"> + <label id="requests-menu-waiting-notice-label" + class="plain" + value="&canvasDebuggerUI.waitingNotice;"/> + </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=""/> + <textbox id="calls-searchbox" + class="devtools-filterinput" + placeholder="&canvasDebuggerUI.searchboxPlaceholder;" + 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>
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/moz.build @@ -0,0 +1,13 @@ +# 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/. + +DevToolsModules( + 'panel.js' +) + +BROWSER_CHROME_MANIFESTS += ['test/browser.ini'] + +with Files('**'): + BUG_COMPONENT = ('DevTools', 'Canvas Debugger')
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/panel.js @@ -0,0 +1,60 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const { Cc, Ci, Cu, Cr } = require("chrome"); +const EventEmitter = require("devtools/shared/event-emitter"); +const { CanvasFront } = require("devtools/shared/fronts/canvas"); +const DevToolsUtils = require("devtools/shared/DevToolsUtils"); + +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: async function() { + this.panelWin.gToolbox = this._toolbox; + this.panelWin.gTarget = this.target; + this.panelWin.gFront = await this.target.getFront("canvas"); + + await this.panelWin.startupCanvasDebugger(); + + this.isReady = true; + this.emit("ready"); + return this; + }, + + // DevToolPanel API + + get target() { + return 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(() => { + // Destroy front to ensure packet handler is removed from client + this.panelWin.gFront.destroy(); + this.emit("destroyed"); + }); + }, +};
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/snapshotslist.js @@ -0,0 +1,549 @@ +/* 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/. */ +/* import-globals-from canvasdebugger.js */ +/* globals window, document */ +"use strict"; + +var promise = require("promise"); +var defer = require("devtools/shared/defer"); + +/** + * Functions handling the recorded animation frame snapshots UI. + */ +var SnapshotsListView = extend(WidgetMethods, { + /** + * Initialization function, called when the tool is started. + */ + initialize: function() { + this.widget = new SideMenuWidget($("#snapshots-list"), { + showArrows: true, + }); + + this._onSelect = this._onSelect.bind(this); + this._onClearButtonClick = this._onClearButtonClick.bind(this); + this._onRecordButtonClick = this._onRecordButtonClick.bind(this); + this._onImportButtonClick = this._onImportButtonClick.bind(this); + this._onSaveButtonClick = this._onSaveButtonClick.bind(this); + this._onRecordSuccess = this._onRecordSuccess.bind(this); + this._onRecordFailure = this._onRecordFailure.bind(this); + this._stopRecordingAnimation = this._stopRecordingAnimation.bind(this); + + window.on(EVENTS.SNAPSHOT_RECORDING_FINISHED, this._enableRecordButton); + this.emptyText = L10N.getStr("noSnapshotsText"); + this.widget.addEventListener("select", this._onSelect); + }, + + /** + * Destruction function, called when the tool is closed. + */ + destroy: function() { + clearNamedTimeout("canvas-actor-recording"); + window.off(EVENTS.SNAPSHOT_RECORDING_FINISHED, this._enableRecordButton); + this.widget.removeEventListener("select", this._onSelect); + }, + + /** + * Adds a snapshot entry to this container. + * + * @return object + * The newly inserted item. + */ + addSnapshot: function() { + const contents = document.createElement("hbox"); + contents.className = "snapshot-item"; + + const thumbnail = document.createElementNS(HTML_NS, "canvas"); + thumbnail.className = "snapshot-item-thumbnail"; + thumbnail.width = CanvasFront.THUMBNAIL_SIZE; + thumbnail.height = CanvasFront.THUMBNAIL_SIZE; + + const title = document.createElement("label"); + title.className = "plain snapshot-item-title"; + title.setAttribute("value", + L10N.getFormatStr("snapshotsList.itemLabel", this.itemCount + 1)); + + const calls = document.createElement("label"); + calls.className = "plain snapshot-item-calls"; + calls.setAttribute("value", + L10N.getStr("snapshotsList.loadingLabel")); + + const save = document.createElement("label"); + save.className = "plain snapshot-item-save"; + save.addEventListener("click", this._onSaveButtonClick); + + const spacer = document.createElement("spacer"); + spacer.setAttribute("flex", "1"); + + const footer = document.createElement("hbox"); + footer.className = "snapshot-item-footer"; + footer.appendChild(save); + + const details = document.createElement("vbox"); + details.className = "snapshot-item-details"; + details.appendChild(title); + details.appendChild(calls); + details.appendChild(spacer); + details.appendChild(footer); + + contents.appendChild(thumbnail); + contents.appendChild(details); + + // Append a recorded snapshot item to this container. + return this.push([contents], { + attachment: { + // The snapshot and function call actors, along with the thumbnails + // will be available as soon as recording finishes. + actor: null, + calls: null, + thumbnails: null, + screenshot: null, + }, + }); + }, + + /** + * Removes the last snapshot added, in the event no requestAnimationFrame loop was found. + */ + removeLastSnapshot: function() { + this.removeAt(this.itemCount - 1); + // If this is the only item, revert back to the empty notice + if (this.itemCount === 0) { + $("#empty-notice").hidden = false; + $("#waiting-notice").hidden = true; + } + }, + + /** + * Customizes a shapshot in this container. + * + * @param Item snapshotItem + * An item inserted via `SnapshotsListView.addSnapshot`. + * @param object snapshotActor + * The frame snapshot actor received from the backend. + * @param object snapshotOverview + * Additional data about the snapshot received from the backend. + */ + customizeSnapshot: function(snapshotItem, snapshotActor, snapshotOverview) { + // Make sure the function call actors are stored on the item, + // to be used when populating the CallsListView. + snapshotItem.attachment.actor = snapshotActor; + const functionCalls = snapshotItem.attachment.calls = snapshotOverview.calls; + const thumbnails = snapshotItem.attachment.thumbnails = snapshotOverview.thumbnails; + const screenshot = snapshotItem.attachment.screenshot = snapshotOverview.screenshot; + + const lastThumbnail = thumbnails[thumbnails.length - 1]; + const { width, height, flipped, pixels } = lastThumbnail; + + const thumbnailNode = $(".snapshot-item-thumbnail", snapshotItem.target); + thumbnailNode.setAttribute("flipped", flipped); + drawImage(thumbnailNode, width, height, pixels, { centered: true }); + + const callsNode = $(".snapshot-item-calls", snapshotItem.target); + const drawCalls = functionCalls.filter(e => CanvasFront.DRAW_CALLS.has(e.name)); + + const drawCallsStr = PluralForm.get(drawCalls.length, + L10N.getStr("snapshotsList.drawCallsLabel")); + const funcCallsStr = PluralForm.get(functionCalls.length, + L10N.getStr("snapshotsList.functionCallsLabel")); + + callsNode.setAttribute("value", + drawCallsStr.replace("#1", drawCalls.length) + ", " + + funcCallsStr.replace("#1", functionCalls.length)); + + const saveNode = $(".snapshot-item-save", snapshotItem.target); + saveNode.setAttribute("disabled", !!snapshotItem.isLoadedFromDisk); + saveNode.setAttribute("value", snapshotItem.isLoadedFromDisk + ? L10N.getStr("snapshotsList.loadedLabel") + : L10N.getStr("snapshotsList.saveLabel")); + + // Make sure there's always a selected item available. + if (!this.selectedItem) { + this.selectedIndex = 0; + } + }, + + /** + * The select listener for this container. + */ + _onSelect: function({ detail: snapshotItem }) { + // Check to ensure the attachment has an actor, like + // an in-progress recording. + if (!snapshotItem || !snapshotItem.attachment.actor) { + return; + } + const { calls, thumbnails, screenshot } = snapshotItem.attachment; + + $("#reload-notice").hidden = true; + $("#empty-notice").hidden = true; + $("#waiting-notice").hidden = false; + + $("#debugging-pane-contents").hidden = true; + $("#screenshot-container").hidden = true; + $("#snapshot-filmstrip").hidden = true; + + (async function() { + // Wait for a few milliseconds between presenting the function calls, + // screenshot and thumbnails, to allow each component being + // sequentially drawn. This gives the illusion of snappiness. + + await DevToolsUtils.waitForTime(SNAPSHOT_DATA_DISPLAY_DELAY); + CallsListView.showCalls(calls); + $("#debugging-pane-contents").hidden = false; + $("#waiting-notice").hidden = true; + + await DevToolsUtils.waitForTime(SNAPSHOT_DATA_DISPLAY_DELAY); + CallsListView.showThumbnails(thumbnails); + $("#snapshot-filmstrip").hidden = false; + + await DevToolsUtils.waitForTime(SNAPSHOT_DATA_DISPLAY_DELAY); + CallsListView.showScreenshot(screenshot); + $("#screenshot-container").hidden = false; + + window.emit(EVENTS.SNAPSHOT_RECORDING_SELECTED); + })(); + }, + + /** + * The click listener for the "clear" button in this container. + */ + _onClearButtonClick: function() { + (async function() { + SnapshotsListView.empty(); + CallsListView.empty(); + + $("#reload-notice").hidden = true; + $("#empty-notice").hidden = true; + $("#waiting-notice").hidden = true; + + if (await gFront.isInitialized()) { + $("#empty-notice").hidden = false; + } else { + $("#reload-notice").hidden = false; + } + + $("#debugging-pane-contents").hidden = true; + $("#screenshot-container").hidden = true; + $("#snapshot-filmstrip").hidden = true; + + window.emit(EVENTS.SNAPSHOTS_LIST_CLEARED); + })(); + }, + + /** + * The click listener for the "record" button in this container. + */ + _onRecordButtonClick: function() { + this._disableRecordButton(); + + if (this._recording) { + this._stopRecordingAnimation(); + return; + } + + // Insert a "dummy" snapshot item in the view, to hint that recording + // has now started. However, wait for a few milliseconds before actually + // starting the recording, since that might block rendering and prevent + // the dummy snapshot item from being drawn. + this.addSnapshot(); + + // If this is the first item, immediately show the "Loading…" notice. + if (this.itemCount == 1) { + $("#empty-notice").hidden = true; + $("#waiting-notice").hidden = false; + } + + this._recordAnimation(); + }, + + /** + * Makes the record button able to be clicked again. + */ + _enableRecordButton: function() { + $("#record-snapshot").removeAttribute("disabled"); + }, + + /** + * Makes the record button unable to be clicked. + */ + _disableRecordButton: function() { + $("#record-snapshot").setAttribute("disabled", true); + }, + + /** + * Begins recording an animation. + */ + async _recordAnimation() { + if (this._recording) { + return; + } + this._recording = true; + $("#record-snapshot").setAttribute("checked", "true"); + + setNamedTimeout("canvas-actor-recording", CANVAS_ACTOR_RECORDING_ATTEMPT, this._stopRecordingAnimation); + + await DevToolsUtils.waitForTime(SNAPSHOT_START_RECORDING_DELAY); + window.emit(EVENTS.SNAPSHOT_RECORDING_STARTED); + + gFront.recordAnimationFrame().then(snapshot => { + if (snapshot) { + this._onRecordSuccess(snapshot); + } else { + this._onRecordFailure(); + } + }); + + // Wait another delay before reenabling the button to stop the recording + // if a recording is not found. + await DevToolsUtils.waitForTime(SNAPSHOT_START_RECORDING_DELAY); + this._enableRecordButton(); + }, + + /** + * Stops recording animation. Called when a click on the stopwatch occurs during a recording, + * or if a recording times out. + */ + async _stopRecordingAnimation() { + clearNamedTimeout("canvas-actor-recording"); + const actorCanStop = await gTarget.actorHasMethod("canvas", "stopRecordingAnimationFrame"); + + if (actorCanStop) { + await gFront.stopRecordingAnimationFrame(); + } else { + // If actor does not have the method to stop recording (Fx39+), + // manually call the record failure method. This will call a connection failure + // on disconnect as a result of `gFront.recordAnimationFrame()` never resolving, + // but this is better than it hanging when there is no requestAnimationFrame anyway. + this._onRecordFailure(); + } + + this._recording = false; + $("#record-snapshot").removeAttribute("checked"); + this._enableRecordButton(); + }, + + /** + * Resolves from the front's recordAnimationFrame to setup the interface with the screenshots. + */ + async _onRecordSuccess(snapshotActor) { + // Clear bail-out case if frame found in CANVAS_ACTOR_RECORDING_ATTEMPT milliseconds + clearNamedTimeout("canvas-actor-recording"); + const snapshotItem = this.getItemAtIndex(this.itemCount - 1); + const snapshotOverview = await snapshotActor.getOverview(); + this.customizeSnapshot(snapshotItem, snapshotActor, snapshotOverview); + + this._recording = false; + $("#record-snapshot").removeAttribute("checked"); + + window.emit(EVENTS.SNAPSHOT_RECORDING_COMPLETED); + window.emit(EVENTS.SNAPSHOT_RECORDING_FINISHED); + }, + + /** + * Called as a reject from the front's recordAnimationFrame. + */ + _onRecordFailure: function() { + clearNamedTimeout("canvas-actor-recording"); + showNotification(gToolbox, "canvas-debugger-timeout", L10N.getStr("recordingTimeoutFailure")); + window.emit(EVENTS.SNAPSHOT_RECORDING_CANCELLED); + window.emit(EVENTS.SNAPSHOT_RECORDING_FINISHED); + this.removeLastSnapshot(); + }, + + /** + * The click listener for the "import" button in this container. + */ + _onImportButtonClick: function() { + const fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); + fp.init(window, L10N.getStr("snapshotsList.saveDialogTitle"), Ci.nsIFilePicker.modeOpen); + fp.appendFilter(L10N.getStr("snapshotsList.saveDialogJSONFilter"), "*.json"); + fp.appendFilter(L10N.getStr("snapshotsList.saveDialogAllFilter"), "*.*"); + + fp.open(rv => { + if (rv != Ci.nsIFilePicker.returnOK) { + return; + } + + const channel = NetUtil.newChannel({ + uri: NetUtil.newURI(fp.file), loadUsingSystemPrincipal: true}); + channel.contentType = "text/plain"; + + NetUtil.asyncFetch(channel, (inputStream, status) => { + if (!Components.isSuccessCode(status)) { + console.error("Could not import recorded animation frame snapshot file."); + return; + } + var data; + try { + const string = NetUtil.readInputStreamToString(inputStream, inputStream.available()); + data = JSON.parse(string); + } catch (e) { + console.error("Could not read animation frame snapshot file."); + return; + } + if (data.fileType != CALLS_LIST_SERIALIZER_IDENTIFIER) { + console.error("Unrecognized animation frame snapshot file."); + return; + } + + // Add a `isLoadedFromDisk` flag on everything to avoid sending invalid + // requests to the backend, since we're not dealing with actors anymore. + const snapshotItem = this.addSnapshot(); + snapshotItem.isLoadedFromDisk = true; + data.calls.forEach(e => e.isLoadedFromDisk = true); + + this.customizeSnapshot(snapshotItem, data.calls, data); + }); + }); + }, + + /** + * The click listener for the "save" button of each item in this container. + */ + _onSaveButtonClick: function(e) { + const snapshotItem = this.getItemForElement(e.target); + + const fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); + fp.init(window, L10N.getStr("snapshotsList.saveDialogTitle"), Ci.nsIFilePicker.modeSave); + fp.appendFilter(L10N.getStr("snapshotsList.saveDialogJSONFilter"), "*.json"); + fp.appendFilter(L10N.getStr("snapshotsList.saveDialogAllFilter"), "*.*"); + fp.defaultString = "snapshot.json"; + + // Start serializing all the function call actors for the specified snapshot, + // while the nsIFilePicker dialog is being opened. Snappy. + const serialized = (async function() { + const data = { + fileType: CALLS_LIST_SERIALIZER_IDENTIFIER, + version: CALLS_LIST_SERIALIZER_VERSION, + calls: [], + thumbnails: [], + screenshot: null, + }; + const functionCalls = snapshotItem.attachment.calls; + const thumbnails = snapshotItem.attachment.thumbnails; + const screenshot = snapshotItem.attachment.screenshot; + + // Prepare all the function calls for serialization. + await yieldingEach(functionCalls, (call, i) => { + const { type, name, file, line, timestamp, argsPreview, callerPreview } = call; + return call.getDetails().then(({ stack }) => { + data.calls[i] = { + type: type, + name: name, + file: file, + line: line, + stack: stack, + timestamp: timestamp, + argsPreview: argsPreview, + callerPreview: callerPreview, + }; + }); + }); + + // Prepare all the thumbnails for serialization. + await yieldingEach(thumbnails, (thumbnail, i) => { + const { index, width, height, flipped, pixels } = thumbnail; + data.thumbnails.push({ index, width, height, flipped, pixels }); + }); + + // Prepare the screenshot for serialization. + const { index, width, height, flipped, pixels } = screenshot; + data.screenshot = { index, width, height, flipped, pixels }; + + const string = JSON.stringify(data); + const converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] + .createInstance(Ci.nsIScriptableUnicodeConverter); + + converter.charset = "UTF-8"; + return converter.convertToInputStream(string); + })(); + + // Open the nsIFilePicker and wait for the function call actors to finish + // being serialized, in order to save the generated JSON data to disk. + fp.open({ done: result => { + if (result == Ci.nsIFilePicker.returnCancel) { + return; + } + const footer = $(".snapshot-item-footer", snapshotItem.target); + const save = $(".snapshot-item-save", snapshotItem.target); + + // Show a throbber and a "Saving…" label if serializing isn't immediate. + setNamedTimeout("call-list-save", CALLS_LIST_SLOW_SAVE_DELAY, () => { + footer.classList.add("devtools-throbber"); + save.setAttribute("disabled", "true"); + save.setAttribute("value", L10N.getStr("snapshotsList.savingLabel")); + }); + + serialized.then(inputStream => { + const outputStream = FileUtils.openSafeFileOutputStream(fp.file); + + NetUtil.asyncCopy(inputStream, outputStream, status => { + if (!Components.isSuccessCode(status)) { + console.error("Could not save recorded animation frame snapshot file."); + } + clearNamedTimeout("call-list-save"); + footer.classList.remove("devtools-throbber"); + save.removeAttribute("disabled"); + save.setAttribute("value", L10N.getStr("snapshotsList.saveLabel")); + }); + }); + }}); + }, +}); + +function showNotification(toolbox, name, message) { + const notificationBox = toolbox.getNotificationBox(); + const notification = notificationBox.getNotificationWithValue(name); + if (!notification) { + notificationBox.appendNotification(message, name, "", notificationBox.PRIORITY_WARNING_HIGH); + } +} + +/** + * 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 + * next tick. + * + * @param Array array + * The array being iterated over. + * @param Function fn + * 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, and all promises returned by the fn callback are resolved. + */ +function yieldingEach(array, fn) { + const deferred = defer(); + + let i = 0; + const len = array.length; + const outstanding = [deferred.promise]; + + (function loop() { + const start = Date.now(); + + while (i < len) { + // Don't block the main thread for longer than 16 ms at a time. To + // maintain 60fps, you have to render every frame in at least 16ms; we + // aren't including time spent in non-JS here, but this is Good + // Enough(tm). + if (Date.now() - start > 16) { + DevToolsUtils.executeSoon(loop); + return; + } + + try { + outstanding.push(fn(array[i], i++)); + } catch (e) { + deferred.reject(e); + return; + } + } + + deferred.resolve(); + }()); + + return promise.all(outstanding); +}
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/.eslintrc.js @@ -0,0 +1,6 @@ +"use strict"; + +module.exports = { + // Extend from the shared list of defined globals for mochitests. + "extends": "../../../.eslintrc.mochitests.js" +};
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/browser.ini @@ -0,0 +1,69 @@ +[DEFAULT] +tags = devtools +subsuite = devtools +support-files = + call-watcher-actor.js + call-watcher-front.js + call-watcher-spec.js + doc_raf-begin.html + doc_settimeout.html + doc_no-canvas.html + doc_raf-no-canvas.html + doc_simple-canvas.html + doc_simple-canvas-bitmasks.html + doc_simple-canvas-deep-stack.html + doc_simple-canvas-transparent.html + doc_webgl-bindings.html + doc_webgl-enum.html + doc_webgl-drawArrays.html + doc_webgl-drawElements.html + head.js + !/devtools/client/shared/test/frame-script-utils.js + !/devtools/client/shared/test/shared-head.js + !/devtools/client/debugger/new/test/mochitest/helpers/context.js + !/devtools/client/shared/test/telemetry-test-helpers.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-actor-test-08.js] +[browser_canvas-actor-test-09.js] +subsuite = gpu +skip-if = (os == 'linux' && bits == 32 && debug) # bug 1328915, disable linux32 debug devtools for timeouts +[browser_canvas-actor-test-10.js] +subsuite = gpu +skip-if = (os == 'linux' && bits == 32 && debug) # bug 1328915, disable linux32 debug devtools for timeouts +[browser_canvas-actor-test-11.js] +subsuite = gpu +skip-if = (os == 'linux' && bits == 32 && debug) # bug 1328915, disable linux32 debug devtools for timeouts +[browser_canvas-actor-test-12.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-record-04.js] +[browser_canvas-frontend-reload-01.js] +[browser_canvas-frontend-reload-02.js] +[browser_canvas-frontend-snapshot-select-01.js] +[browser_canvas-frontend-snapshot-select-02.js] +[browser_canvas-frontend-stepping.js] +[browser_canvas-frontend-stop-01.js] +[browser_canvas-frontend-stop-02.js] +[browser_canvas-frontend-stop-03.js] +[browser_profiling-canvas.js] +[browser_profiling-webgl.js] +subsuite = gpu
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/browser_canvas-actor-test-01.js @@ -0,0 +1,17 @@ +/* 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. + */ + +async function ifTestingSupported() { + const { target, front } = await initCallWatcherBackend(SIMPLE_CANVAS_URL); + + ok(target, "Should have a target available."); + ok(front, "Should have a protocol front available."); + + await removeTab(target.tab); + finish(); +}
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/browser_canvas-actor-test-02.js @@ -0,0 +1,78 @@ +/* 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. + */ + +async function ifTestingSupported() { + const { target, front } = await initCallWatcherBackend(SIMPLE_CANVAS_URL); + + const navigated = once(target, "navigate"); + + await front.setup({ + tracedGlobals: ["CanvasRenderingContext2D", "WebGLRenderingContext"], + startRecording: true, + performReload: true, + storeCalls: true, + }); + ok(true, "The front was setup up successfully."); + + await navigated; + ok(true, "Target automatically navigated when the front was set up."); + + // Allow the content to execute some functions. + await waitForTick(); + + const functionCalls = await 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, 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, "Object", + "The called function's caller preview is correct."); + is(functionCalls[0].argsPreview, "0, 0, 128, 128", + "The called function's args preview is correct."); + + const details = await 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)."); + + await removeTab(target.tab); + finish(); +}
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/browser_canvas-actor-test-03.js @@ -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. + */ + +async function ifTestingSupported() { + const { target, front } = await initCanvasDebuggerBackend(SIMPLE_CANVAS_URL); + + const navigated = once(target, "navigate"); + + await front.setup({ reload: true }); + ok(true, "The front was setup up successfully."); + + await navigated; + ok(true, "Target automatically navigated when the front was set up."); + + const snapshotActor = await front.recordAnimationFrame(); + ok(snapshotActor, + "A snapshot actor was sent after recording."); + + const animationOverview = await snapshotActor.getOverview(); + ok(snapshotActor, + "An animation overview could be retrieved after recording."); + + const 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, 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, "Object", + "The first called function's caller preview is correct."); + + is(functionCalls[6].type, 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, "Object", + "The penultimate called function's caller preview is correct."); + + is(functionCalls[7].type, 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.includes("Function"), + "The last called function's args preview is correct."); + is(functionCalls[7].callerPreview, "Object", + "The last called function's caller preview is correct."); + + await removeTab(target.tab); + finish(); +}
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/browser_canvas-actor-test-04.js @@ -0,0 +1,85 @@ +/* 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. + */ + +async function ifTestingSupported() { + const { target, front } = await initCanvasDebuggerBackend(SIMPLE_CANVAS_URL); + + const navigated = once(target, "navigate"); + + await front.setup({ reload: true }); + ok(true, "The front was setup up successfully."); + + await navigated; + ok(true, "Target automatically navigated when the front was set up."); + + const snapshotActor = await front.recordAnimationFrame(); + ok(snapshotActor, + "A snapshot actor was sent after recording."); + + const animationOverview = await snapshotActor.getOverview(); + ok(animationOverview, + "An animation overview could be retrieved after recording."); + + const 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(Uint32(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(Uint32(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(Uint32(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(Uint32(thumbnails[3].pixels), e => e > 0), 4290822336, + "The fourth thumbnail's pixels seem to not be completely transparent."); + + await removeTab(target.tab); + finish(); +} + +function Uint32(src) { + const charView = new Uint8Array(src); + return new Uint32Array(charView.buffer); +}
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/browser_canvas-actor-test-05.js @@ -0,0 +1,50 @@ +/* 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. + */ + +async function ifTestingSupported() { + const { target, front } = await initCanvasDebuggerBackend(SIMPLE_CANVAS_URL); + + const navigated = once(target, "navigate"); + + await front.setup({ reload: true }); + ok(true, "The front was setup up successfully."); + + await navigated; + ok(true, "Target automatically navigated when the front was set up."); + + const snapshotActor = await front.recordAnimationFrame(); + ok(snapshotActor, + "A snapshot actor was sent after recording."); + + const animationOverview = await snapshotActor.getOverview(); + ok(snapshotActor, + "An animation overview could be retrieved after recording."); + + const 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(Uint32(screenshot.pixels), e => e > 0), 4290822336, + "The screenshot's pixels seem to not be completely transparent."); + + await removeTab(target.tab); + finish(); +} + +function Uint32(src) { + const charView = new Uint8Array(src); + return new Uint32Array(charView.buffer); +}
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/browser_canvas-actor-test-06.js @@ -0,0 +1,100 @@ +/* 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. + */ + +async function ifTestingSupported() { + const { target, front } = await initCanvasDebuggerBackend(SIMPLE_CANVAS_TRANSPARENT_URL); + + const navigated = once(target, "navigate"); + + await front.setup({ reload: true }); + ok(true, "The front was setup up successfully."); + + await navigated; + ok(true, "Target automatically navigated when the front was set up."); + + const snapshotActor = await front.recordAnimationFrame(); + const animationOverview = await snapshotActor.getOverview(); + + const 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."); + + const firstDrawCallScreenshot = await snapshotActor.generateScreenshotFor(functionCalls[0]); + const secondDrawCallScreenshot = await snapshotActor.generateScreenshotFor(functionCalls[2]); + const thirdDrawCallScreenshot = await snapshotActor.generateScreenshotFor(functionCalls[4]); + const fourthDrawCallScreenshot = await 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(Uint32(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(Uint32(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(Uint32(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(Uint32(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)."); + + await removeTab(target.tab); + finish(); +} + +function Uint32(src) { + const charView = new Uint8Array(src); + return new Uint32Array(charView.buffer); +}
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/browser_canvas-actor-test-07.js @@ -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. + */ + +async function ifTestingSupported() { + const { target, front } = await initCanvasDebuggerBackend(SIMPLE_CANVAS_URL); + + const navigated = once(target, "navigate"); + + await front.setup({ reload: true }); + ok(true, "The front was setup up successfully."); + + await navigated; + ok(true, "Target automatically navigated when the front was set up."); + + const snapshotActor = await front.recordAnimationFrame(); + const animationOverview = await snapshotActor.getOverview(); + + const 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."); + + const firstNonDrawCall = await functionCalls[1].getDetails(); + const secondNonDrawCall = await functionCalls[3].getDetails(); + const lastNonDrawCall = await 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."); + + const firstScreenshot = await snapshotActor.generateScreenshotFor(functionCalls[1]); + const secondScreenshot = await snapshotActor.generateScreenshotFor(functionCalls[3]); + const lastScreenshot = await 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."); + + const firstActualScreenshot = await 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."); + + const secondActualScreenshot = await 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."); + + const lastActualScreenshot = await 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)."); + + await 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; +}
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/browser_canvas-actor-test-08.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that integers used in arguments are not cast to their constant, enum value + * forms if the method's signature does not expect an enum. Bug 999687. + */ + +async function ifTestingSupported() { + const { target, front } = await initCanvasDebuggerBackend(SIMPLE_BITMASKS_URL); + + const navigated = once(target, "navigate"); + + await front.setup({ reload: true }); + ok(true, "The front was setup up successfully."); + + await navigated; + ok(true, "Target automatically navigated when the front was set up."); + + const snapshotActor = await front.recordAnimationFrame(); + const animationOverview = await snapshotActor.getOverview(); + const functionCalls = animationOverview.calls; + + is(functionCalls[0].name, "clearRect", + "The first called function's name is correct."); + is(functionCalls[0].argsPreview, "0, 0, 4, 4", + "The first called function's args preview is not cast to enums."); + + is(functionCalls[2].name, "fillRect", + "The fillRect called function's name is correct."); + is(functionCalls[2].argsPreview, "0, 0, 1, 1", + "The fillRect called function's args preview is not casted to enums."); + + await removeTab(target.tab); + finish(); +}
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/browser_canvas-actor-test-09.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that integers used in arguments are not cast to their constant, enum value + * forms if the method's signature does not expect an enum. Bug 999687. + */ + +async function ifTestingSupported() { + const { target, front } = await initCanvasDebuggerBackend(WEBGL_ENUM_URL); + + const navigated = once(target, "navigate"); + + await front.setup({ reload: true }); + ok(true, "The front was setup up successfully."); + + await navigated; + ok(true, "Target automatically navigated when the front was set up."); + + const snapshotActor = await front.recordAnimationFrame(); + const animationOverview = await snapshotActor.getOverview(); + const functionCalls = animationOverview.calls; + + is(functionCalls[0].name, "clear", + "The function's name is correct."); + is(functionCalls[0].argsPreview, "DEPTH_BUFFER_BIT | STENCIL_BUFFER_BIT | COLOR_BUFFER_BIT", + "The bits passed into `gl.clear` have been cast to their enum values."); + + is(functionCalls[1].name, "bindTexture", + "The function's name is correct."); + is(functionCalls[1].argsPreview, "TEXTURE_2D, null", + "The bits passed into `gl.bindTexture` have been cast to their enum values."); + + await removeTab(target.tab); + finish(); +}
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/browser_canvas-actor-test-10.js @@ -0,0 +1,109 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that the correct framebuffer, renderbuffer and textures are re-bound + * after generating screenshots using the actor. + */ + +var { CanvasFront } = require("devtools/shared/fronts/canvas"); + +async function ifTestingSupported() { + const { target, front } = await initCanvasDebuggerBackend(WEBGL_BINDINGS_URL); + loadFrameScriptUtils(); + + const navigated = once(target, "navigate"); + + await front.setup({ reload: true }); + ok(true, "The front was setup up successfully."); + + await navigated; + ok(true, "Target automatically navigated when the front was set up."); + + const snapshotActor = await front.recordAnimationFrame(); + const animationOverview = await snapshotActor.getOverview(); + const functionCalls = animationOverview.calls; + + const firstScreenshot = await snapshotActor.generateScreenshotFor(functionCalls[0]); + is(firstScreenshot.index, -1, + "The first screenshot didn't encounter any draw call."); + is(firstScreenshot.scaling, 0.25, + "The first screenshot has the correct scaling."); + is(firstScreenshot.width, CanvasFront.WEBGL_SCREENSHOT_MAX_HEIGHT, + "The first screenshot has the correct width."); + is(firstScreenshot.height, CanvasFront.WEBGL_SCREENSHOT_MAX_HEIGHT, + "The first screenshot has the correct height."); + is(firstScreenshot.flipped, true, + "The first screenshot has the correct 'flipped' flag."); + is(firstScreenshot.pixels.length, 0, + "The first screenshot should be empty."); + + is((await evalInDebuggee("gl.getParameter(gl.FRAMEBUFFER_BINDING) === customFramebuffer")), + true, + "The debuggee's gl context framebuffer wasn't changed."); + is((await evalInDebuggee("gl.getParameter(gl.RENDERBUFFER_BINDING) === customRenderbuffer")), + true, + "The debuggee's gl context renderbuffer wasn't changed."); + is((await evalInDebuggee("gl.getParameter(gl.TEXTURE_BINDING_2D) === customTexture")), + true, + "The debuggee's gl context texture binding wasn't changed."); + is((await evalInDebuggee("gl.getParameter(gl.VIEWPORT)[0]")), + 128, + "The debuggee's gl context viewport's left coord. wasn't changed."); + is((await evalInDebuggee("gl.getParameter(gl.VIEWPORT)[1]")), + 256, + "The debuggee's gl context viewport's left coord. wasn't changed."); + is((await evalInDebuggee("gl.getParameter(gl.VIEWPORT)[2]")), + 384, + "The debuggee's gl context viewport's left coord. wasn't changed."); + is((await evalInDebuggee("gl.getParameter(gl.VIEWPORT)[3]")), + 512, + "The debuggee's gl context viewport's left coord. wasn't changed."); + + const secondScreenshot = await snapshotActor.generateScreenshotFor(functionCalls[1]); + is(secondScreenshot.index, 1, + "The second screenshot has the correct index."); + is(secondScreenshot.width, CanvasFront.WEBGL_SCREENSHOT_MAX_HEIGHT, + "The second screenshot has the correct width."); + is(secondScreenshot.height, CanvasFront.WEBGL_SCREENSHOT_MAX_HEIGHT, + "The second screenshot has the correct height."); + is(secondScreenshot.scaling, 0.25, + "The second screenshot has the correct scaling."); + is(secondScreenshot.flipped, true, + "The second screenshot has the correct 'flipped' flag."); + is(secondScreenshot.pixels.length, Math.pow(CanvasFront.WEBGL_SCREENSHOT_MAX_HEIGHT, 2) * 4, + "The second screenshot should not be empty."); + is(secondScreenshot.pixels[0], 0, + "The second screenshot has the correct red component."); + is(secondScreenshot.pixels[1], 0, + "The second screenshot has the correct green component."); + is(secondScreenshot.pixels[2], 255, + "The second screenshot has the correct blue component."); + is(secondScreenshot.pixels[3], 255, + "The second screenshot has the correct alpha component."); + + is((await evalInDebuggee("gl.getParameter(gl.FRAMEBUFFER_BINDING) === customFramebuffer")), + true, + "The debuggee's gl context framebuffer still wasn't changed."); + is((await evalInDebuggee("gl.getParameter(gl.RENDERBUFFER_BINDING) === customRenderbuffer")), + true, + "The debuggee's gl context renderbuffer still wasn't changed."); + is((await evalInDebuggee("gl.getParameter(gl.TEXTURE_BINDING_2D) === customTexture")), + true, + "The debuggee's gl context texture binding still wasn't changed."); + is((await evalInDebuggee("gl.getParameter(gl.VIEWPORT)[0]")), + 128, + "The debuggee's gl context viewport's left coord. still wasn't changed."); + is((await evalInDebuggee("gl.getParameter(gl.VIEWPORT)[1]")), + 256, + "The debuggee's gl context viewport's left coord. still wasn't changed."); + is((await evalInDebuggee("gl.getParameter(gl.VIEWPORT)[2]")), + 384, + "The debuggee's gl context viewport's left coord. still wasn't changed."); + is((await evalInDebuggee("gl.getParameter(gl.VIEWPORT)[3]")), + 512, + "The debuggee's gl context viewport's left coord. still wasn't changed."); + + await removeTab(target.tab); + finish(); +}
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/browser_canvas-actor-test-11.js @@ -0,0 +1,138 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that loops using setTimeout are recorded and stored + * for a canvas context, and that the generated screenshots are correct. + */ + +async function ifTestingSupported() { + const { target, front } = await initCanvasDebuggerBackend(SET_TIMEOUT_URL); + + const navigated = once(target, "navigate"); + + await front.setup({ reload: true }); + ok(true, "The front was setup up successfully."); + + await navigated; + ok(true, "Target automatically navigated when the front was set up."); + + const snapshotActor = await front.recordAnimationFrame(); + ok(snapshotActor, + "A snapshot actor was sent after recording."); + + const animationOverview = await snapshotActor.getOverview(); + ok(snapshotActor, + "An animation overview could be retrieved after recording."); + + const 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, 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, SET_TIMEOUT_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, "Object", + "The first called function's caller preview is correct."); + + is(functionCalls[6].type, 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, SET_TIMEOUT_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, "Object", + "The penultimate called function's caller preview is correct."); + + is(functionCalls[7].type, METHOD_FUNCTION, + "The last called function is correctly identified as a method."); + is(functionCalls[7].name, "setTimeout", + "The last called function's name is correct."); + is(functionCalls[7].file, SET_TIMEOUT_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.includes("Function"), + "The last called function's args preview is correct."); + is(functionCalls[7].callerPreview, "Object", + "The last called function's caller preview is correct."); + + const firstNonDrawCall = await functionCalls[1].getDetails(); + const secondNonDrawCall = await functionCalls[3].getDetails(); + const lastNonDrawCall = await 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, "setTimeout", + "The last non-draw function's name is correct."); + + const firstScreenshot = await snapshotActor.generateScreenshotFor(functionCalls[1]); + const secondScreenshot = await snapshotActor.generateScreenshotFor(functionCalls[3]); + const lastScreenshot = await 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."); + + const firstActualScreenshot = await 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."); + + const secondActualScreenshot = await 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."); + + const lastActualScreenshot = await 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)."); + + await 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; +}
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/browser_canvas-actor-test-12.js @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that the recording can be disabled via stopRecordingAnimationFrame + * in the event no rAF loop is found. + */ + +async function ifTestingSupported() { + const { target, front } = await initCanvasDebuggerBackend(NO_CANVAS_URL); + loadFrameScriptUtils(); + + const navigated = once(target, "navigate"); + + await front.setup({ reload: true }); + ok(true, "The front was setup up successfully."); + + await navigated; + ok(true, "Target automatically navigated when the front was set up."); + + const startRecording = front.recordAnimationFrame(); + await front.stopRecordingAnimationFrame(); + + ok(!(await startRecording), + "recordAnimationFrame() does not return a SnapshotActor when cancelled."); + + await removeTab(target.tab); + finish(); +}
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-call-highlight.js @@ -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. + */ + +async function ifTestingSupported() { + const { target, panel } = await initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL); + const { window, $, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin; + + await reload(target); + + const recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED); + const callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED); + SnapshotsListView._onRecordButtonClick(); + await 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."); + + await teardown(panel); + finish(); +}
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-call-list.js @@ -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. + */ + +async function ifTestingSupported() { + const { target, panel } = await initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL); + const { window, $, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin; + + await reload(target); + + const recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED); + const callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED); + SnapshotsListView._onRecordButtonClick(); + await Promise.all([recordingFinished, callListPopulated]); + + is(CallsListView.itemCount, 8, + "All the function calls should now be displayed in the UI."); + + testItem(CallsListView.getItemAtIndex(0), + "1", "Object", "clearRect", "(0, 0, 128, 128)", "doc_simple-canvas.html:25"); + + testItem(CallsListView.getItemAtIndex(1), + "2", "Object", "fillStyle", " = rgb(192, 192, 192)", "doc_simple-canvas.html:20"); + testItem(CallsListView.getItemAtIndex(2), + "3", "Object", "fillRect", "(0, 0, 128, 128)", "doc_simple-canvas.html:21"); + + testItem(CallsListView.getItemAtIndex(3), + "4", "Object", "fillStyle", " = rgba(0, 0, 192, 0.5)", "doc_simple-canvas.html:20"); + testItem(CallsListView.getItemAtIndex(4), + "5", "Object", "fillRect", "(30, 30, 55, 50)", "doc_simple-canvas.html:21"); + + testItem(CallsListView.getItemAtIndex(5), + "6", "Object", "fillStyle", " = rgba(192, 0, 0, 0.5)", "doc_simple-canvas.html:20"); + testItem(CallsListView.getItemAtIndex(6), + "7", "Object", "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) { + const 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) + "", "[object XULTextElement]", + "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."); + } + + await teardown(panel); + finish(); +}
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-call-search.js @@ -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. + */ + +async function ifTestingSupported() { + const { target, panel } = await initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL); + const { window, $, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin; + const searchbox = $("#calls-searchbox"); + + await reload(target); + + const firstRecordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED); + let callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED); + SnapshotsListView._onRecordButtonClick(); + await 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, 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, "Object", + "The visible item's caller has the expected value."); + + const secondRecordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED); + callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED); + + SnapshotsListView._onRecordButtonClick(); + await secondRecordingFinished; + + SnapshotsListView.selectedIndex = 1; + await 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."); + + await teardown(panel); + finish(); +}
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-call-stack-01.js @@ -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. + */ + +requestLongerTimeout(2); + +async function ifTestingSupported() { + const { target, panel } = await initCanvasDebuggerFrontend(SIMPLE_CANVAS_DEEP_STACK_URL); + const { window, $, $all, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin; + + await reload(target); + + const recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED); + const callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED); + SnapshotsListView._onRecordButtonClick(); + await Promise.all([recordingFinished, callListPopulated]); + + const callItem = CallsListView.getItemAtIndex(2); + const 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."); + + const callStackDisplayed = once(window, EVENTS.CALL_STACK_DISPLAYED); + EventUtils.sendMouseEvent({ type: "mousedown" }, locationLink, window); + await callStackDisplayed; + + isnot($(".call-item-stack", callItem.target), null, + "There should be a stack container available now for the draw call."); + // We may have more than 4 functions, depending on whether async + // stacks are available. + ok($all(".call-item-stack-fn", callItem.target).length >= 4, + "There should be at least 4 functions on the stack for the draw call."); + + ok($all(".call-item-stack-fn-name", callItem.target)[0].getAttribute("value") + .includes("C()"), + "The first function on the stack has the correct name."); + ok($all(".call-item-stack-fn-name", callItem.target)[1].getAttribute("value") + .includes("B()"), + "The second function on the stack has the correct name."); + ok($all(".call-item-stack-fn-name", callItem.target)[2].getAttribute("value") + .includes("A()"), + "The third function on the stack has the correct name."); + ok($all(".call-item-stack-fn-name", callItem.target)[3].getAttribute("value") + .includes("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."); + + const jumpedToSource = once(window, EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER); + EventUtils.sendMouseEvent({ type: "mousedown" }, $(".call-item-stack-fn-location", callItem.target)); + await jumpedToSource; + + const toolbox = await gDevTools.getToolbox(target); + const dbg = createDebuggerContext(toolbox); + await validateDebuggerLocation(dbg, SIMPLE_CANVAS_DEEP_STACK_URL, 26); + + await teardown(panel); + finish(); +}
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-call-stack-02.js @@ -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. + */ + +requestLongerTimeout(2); + +async function ifTestingSupported() { + const { target, panel } = await initCanvasDebuggerFrontend(SIMPLE_CANVAS_DEEP_STACK_URL); + const { window, $, $all, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin; + + await reload(target); + + const recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED); + const callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED); + SnapshotsListView._onRecordButtonClick(); + await Promise.all([recordingFinished, callListPopulated]); + + const callItem = CallsListView.getItemAtIndex(2); + const 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."); + + const callStackDisplayed = once(window, EVENTS.CALL_STACK_DISPLAYED); + EventUtils.sendMouseEvent({ type: "mousedown" }, locationLink, window); + await callStackDisplayed; + + isnot($(".call-item-stack", callItem.target), null, + "There should be a stack container available now for the draw call."); + // We may have more than 4 functions, depending on whether async + // stacks are available. + ok($all(".call-item-stack-fn", callItem.target).length >= 4, + "There should be at least 4 functions on the stack for the draw call."); + + const jumpedToSource = once(window, EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER); + EventUtils.sendMouseEvent({ type: "mousedown" }, $(".call-item-location", callItem.target)); + await jumpedToSource; + + const toolbox = await gDevTools.getToolbox(target); + const dbg = createDebuggerContext(toolbox); + await validateDebuggerLocation(dbg, SIMPLE_CANVAS_DEEP_STACK_URL, 24); + + await teardown(panel); + finish(); +}
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-call-stack-03.js @@ -0,0 +1,65 @@ +/* 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. + */ + +async function ifTestingSupported() { + const { target, panel } = await initCanvasDebuggerFrontend(SIMPLE_CANVAS_DEEP_STACK_URL); + const { window, $, $all, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin; + + await reload(target); + + const recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED); + const callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED); + SnapshotsListView._onRecordButtonClick(); + await Promise.all([recordingFinished, callListPopulated]); + + const callItem = CallsListView.getItemAtIndex(2); + const view = $(".call-item-view", callItem.target); + const 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."); + + const callStackDisplayed = once(window, EVENTS.CALL_STACK_DISPLAYED); + EventUtils.sendMouseEvent({ type: "dblclick" }, contents, window); + await 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."); + // We may have more than 4 functions, depending on whether async + // stacks are available. + ok($all(".call-item-stack-fn", callItem.target).length >= 4, + "There should be at least 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."); + // We may have more than 4 functions, depending on whether async + // stacks are available. + ok($all(".call-item-stack-fn", callItem.target).length >= 4, + "There should still be at least 4 functions on the stack for the draw call."); + + await teardown(panel); + finish(); +}
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-clear.js @@ -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. + */ + +async function ifTestingSupported() { + const { target, panel } = await initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL); + const { window, EVENTS, SnapshotsListView } = panel.panelWin; + + await reload(target); + + const firstRecordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED); + SnapshotsListView._onRecordButtonClick(); + + await 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."); + + const secondRecordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED); + SnapshotsListView._onRecordButtonClick(); + + await 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."); + + const clearingFinished = once(window, EVENTS.SNAPSHOTS_LIST_CLEARED); + SnapshotsListView._onClearButtonClick(); + + await clearingFinished; + ok(true, "Finished recording all snapshots."); + + is(SnapshotsListView.itemCount, 0, + "There should be no items available in the snapshots list."); + + await teardown(panel); + finish(); +}
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-img-screenshots.js @@ -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. + */ + +async function ifTestingSupported() { + const { target, panel } = await initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL); + const { window, $, EVENTS, SnapshotsListView } = panel.panelWin; + + await reload(target); + + const recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED); + const callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED); + const screenshotDisplayed = once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED); + SnapshotsListView._onRecordButtonClick(); + await Promise.all([recordingFinished, callListPopulated, screenshotDisplayed]); + + is($("#screenshot-container").hidden, false, + "The screenshot container should now be visible."); + + is($("#screenshot-dimensions").getAttribute("value"), "128" + "\u00D7" + "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.includes("#screenshot-rendering"), + "The screenshot element should have an offscreen canvas element as a background."); + + await teardown(panel); + finish(); +}
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-img-thumbnails-01.js @@ -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. + */ + +async function ifTestingSupported() { + const { target, panel } = await initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL); + const { window, $, $all, EVENTS, SnapshotsListView } = panel.panelWin; + + await reload(target); + + const recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED); + const callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED); + const thumbnailsDisplayed = once(window, EVENTS.THUMBNAILS_DISPLAYED); + SnapshotsListView._onRecordButtonClick(); + await Promise.all([recordingFinished, callListPopulated, thumbnailsDisplayed]); + + is($all(".filmstrip-thumbnail").length, 4, + "There should be 4 thumbnails displayed in the UI."); + + const 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."); + + const 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."); + + const 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."); + + const 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."); + + await teardown(panel); + finish(); +}
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-img-thumbnails-02.js @@ -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. + */ + +async function ifTestingSupported() { + const { target, panel } = await initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL); + const { window, $, $all, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin; + + await reload(target); + + const recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED); + const callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED); + const thumbnailsDisplayed = once(window, EVENTS.THUMBNAILS_DISPLAYED); + const screenshotDisplayed = once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED); + SnapshotsListView._onRecordButtonClick(); + await 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); + await 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); + await 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); + await 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."); + + await teardown(panel); + finish(); +}
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-open.js @@ -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. + */ + +async function ifTestingSupported() { + const { target, panel } = await initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL); + const { $ } = 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($("#waiting-notice").getAttribute("hidden"), "true", + "The waiting 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."); + + await teardown(panel); + finish(); +}
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-record-01.js @@ -0,0 +1,60 @@ +/* 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. + */ + +async function ifTestingSupported() { + const { target, panel } = await initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL); + const { window, EVENTS, $, SnapshotsListView } = panel.panelWin; + + await 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."); + + const recordingStarted = once(window, EVENTS.SNAPSHOT_RECORDING_STARTED); + const recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED); + SnapshotsListView._onRecordButtonClick(); + + await 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").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."); + + await 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."); + + await teardown(panel); + finish(); +}
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-record-02.js @@ -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. + */ + +async function ifTestingSupported() { + const { target, panel } = await initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL); + const { window, EVENTS, L10N, $, SnapshotsListView } = panel.panelWin; + + await reload(target); + + const recordingStarted = once(window, EVENTS.SNAPSHOT_RECORDING_STARTED); + const recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED); + const recordingSelected = once(window, EVENTS.SNAPSHOT_RECORDING_SELECTED); + SnapshotsListView._onRecordButtonClick(); + + await recordingStarted; + ok(true, "Started recording a snapshot of the animation loop."); + + const 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($("#waiting-notice").hasAttribute("hidden"), false, + "The waiting 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."); + + await recordingFinished; + ok(true, "Finished recording a snapshot of the animation loop."); + + await 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($("#waiting-notice").getAttribute("hidden"), "true", + "The waiting 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."); + + await teardown(panel); + finish(); +}
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-record-03.js @@ -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. + */ + +async function ifTestingSupported() { + const { target, panel } = await initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL); + const { window, EVENTS, $, SnapshotsListView } = panel.panelWin; + + await reload(target); + + const recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED); + SnapshotsListView._onRecordButtonClick(); + + await recordingFinished; + ok(true, "Finished recording a snapshot of the animation loop."); + + const 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."); + + await teardown(panel); + finish(); +}
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-record-04.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Bug 1122766 + * Tests that the canvas actor correctly returns from recordAnimationFrame + * in the scenario where a loop starts with rAF and has rAF in the beginning + * of its loop, when the recording starts before the rAFs start. + */ + +async function ifTestingSupported() { + const { target, panel } = await initCanvasDebuggerFrontend(RAF_BEGIN_URL); + const { window, EVENTS, gFront, SnapshotsListView } = panel.panelWin; + loadFrameScriptUtils(); + + await reload(target); + + const recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED); + SnapshotsListView._onRecordButtonClick(); + + // Wait until after the recording started to trigger the content. + // Use the gFront method rather than the SNAPSHOT_RECORDING_STARTED event + // which triggers before the underlying actor call + await waitUntil(async function() { + return !(await gFront.isRecording()); + }); + + // Start animation in content + evalInDebuggee("start();"); + + await recordingFinished; + ok(true, "Finished recording a snapshot of the animation loop."); + + await removeTab(target.tab); + finish(); +}
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-reload-01.js @@ -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. + */ + +async function ifTestingSupported() { + const { target, panel } = await initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL); + const { window, $, EVENTS } = panel.panelWin; + + const reset = once(window, EVENTS.UI_RESET); + const navigated = reload(target); + + await reset; + ok(true, "The UI was reset after the refresh button was clicked."); + + await 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($("#waiting-notice").getAttribute("hidden"), "true", + "The waiting 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."); + + await teardown(panel); + finish(); +}
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-reload-02.js @@ -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. + */ + +async function ifTestingSupported() { + const { target, panel } = await initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL); + const { 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)."); + + await reload(target); + + const recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED); + const callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED); + const thumbnailsDisplayed = once(window, EVENTS.THUMBNAILS_DISPLAYED); + const screenshotDisplayed = once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED); + SnapshotsListView._onRecordButtonClick(); + await 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)."); + + const reset = once(window, EVENTS.UI_RESET); + const navigated = reload(target); + + await 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."); + + await navigated; + ok(true, "The target finished reloading."); + + await teardown(panel); + finish(); +}
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-snapshot-select-01.js @@ -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. + */ + +async function ifTestingSupported() { + const { target, panel } = await initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL); + const { window, $, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin; + + await reload(target); + + await 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."); + + await 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."); + + const secondSnapshotTarget = SnapshotsListView.getItemAtIndex(1).target; + let snapshotSelected = waitForSnapshotSelection(); + EventUtils.sendMouseEvent({ type: "mousedown" }, secondSnapshotTarget, window); + + await 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."); + + const firstDrawCallContents = $(".call-item-contents", CallsListView.getItemAtIndex(2).target); + const screenshotDisplayed = once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED); + EventUtils.sendMouseEvent({ type: "mousedown" }, firstDrawCallContents, window); + + await 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."); + + const firstSnapshotTarget = SnapshotsListView.getItemAtIndex(0).target; + snapshotSelected = waitForSnapshotSelection(); + EventUtils.sendMouseEvent({ type: "mousedown" }, firstSnapshotTarget, window); + + await 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() { + const recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED); + const snapshotSelected = waitForSnapshotSelection(); + SnapshotsListView._onRecordButtonClick(); + return Promise.all([recordingFinished, snapshotSelected]); + } + + function recordAndWaitForAnotherSnapshot() { + const recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED); + SnapshotsListView._onRecordButtonClick(); + return recordingFinished; + } + + function waitForSnapshotSelection() { + const callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED); + const thumbnailsDisplayed = once(window, EVENTS.THUMBNAILS_DISPLAYED); + const screenshotDisplayed = once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED); + return Promise.all([ + callListPopulated, + thumbnailsDisplayed, + screenshotDisplayed, + ]); + } + + await teardown(panel); + finish(); +}
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-snapshot-select-02.js @@ -0,0 +1,30 @@ +/* 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. + */ + +async function ifTestingSupported() { + const { target, panel } = await initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL); + const { window, $, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin; + + await reload(target); + + SnapshotsListView._onRecordButtonClick(); + const snapshotTarget = SnapshotsListView.getItemAtIndex(0).target; + + EventUtils.sendMouseEvent({ type: "mousedown" }, snapshotTarget, window); + EventUtils.sendMouseEvent({ type: "mousedown" }, snapshotTarget, window); + EventUtils.sendMouseEvent({ type: "mousedown" }, snapshotTarget, window); + + ok(true, "clicking in-progress snapshot does not fail"); + + const finished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED); + SnapshotsListView._onRecordButtonClick(); + await finished; + + await teardown(panel); + finish(); +}
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-stepping.js @@ -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. + */ + +async function ifTestingSupported() { + const { target, panel } = await initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL); + const { window, $, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin; + + await reload(target); + + const recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED); + const callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED); + SnapshotsListView._onRecordButtonClick(); + await 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."); + } + } + + await teardown(panel); + finish(); +}
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-stop-01.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that you can stop a recording that does not have a rAF cycle. + */ + +async function ifTestingSupported() { + const { target, panel } = await initCanvasDebuggerFrontend(NO_CANVAS_URL); + const { window, EVENTS, $, SnapshotsListView } = panel.panelWin; + + await reload(target); + + const recordingStarted = once(window, EVENTS.SNAPSHOT_RECORDING_STARTED); + SnapshotsListView._onRecordButtonClick(); + + await recordingStarted; + + is($("#empty-notice").hidden, true, "Empty notice not shown"); + is($("#waiting-notice").hidden, false, "Waiting notice shown"); + + const recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED); + const recordingCancelled = once(window, EVENTS.SNAPSHOT_RECORDING_CANCELLED); + SnapshotsListView._onRecordButtonClick(); + + await Promise.all([recordingFinished, recordingCancelled]); + + ok(true, "Recording stopped and was considered failed."); + + is(SnapshotsListView.itemCount, 0, "No snapshots in the list."); + is($("#empty-notice").hidden, false, "Empty notice shown"); + is($("#waiting-notice").hidden, true, "Waiting notice not shown"); + + await teardown(panel); + finish(); +}
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-stop-02.js @@ -0,0 +1,35 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that a recording that does not have a rAF cycle fails after timeout. + */ + +async function ifTestingSupported() { + const { target, panel } = await initCanvasDebuggerFrontend(NO_CANVAS_URL); + const { window, EVENTS, $, SnapshotsListView } = panel.panelWin; + + await reload(target); + + const recordingStarted = once(window, EVENTS.SNAPSHOT_RECORDING_STARTED); + SnapshotsListView._onRecordButtonClick(); + + await recordingStarted; + + is($("#empty-notice").hidden, true, "Empty notice not shown"); + is($("#waiting-notice").hidden, false, "Waiting notice shown"); + + const recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED); + const recordingCancelled = once(window, EVENTS.SNAPSHOT_RECORDING_CANCELLED); + + await Promise.all([recordingFinished, recordingCancelled]); + + ok(true, "Recording stopped and was considered failed."); + + is(SnapshotsListView.itemCount, 0, "No snapshots in the list."); + is($("#empty-notice").hidden, false, "Empty notice shown"); + is($("#waiting-notice").hidden, true, "Waiting notice not shown"); + + await teardown(panel); + finish(); +}
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-stop-03.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that a recording that has a rAF cycle, but no draw calls, fails + * after timeout. + */ + +async function ifTestingSupported() { + const { target, panel } = await initCanvasDebuggerFrontend(RAF_NO_CANVAS_URL); + const { window, EVENTS, $, SnapshotsListView } = panel.panelWin; + + await reload(target); + + const recordingStarted = once(window, EVENTS.SNAPSHOT_RECORDING_STARTED); + SnapshotsListView._onRecordButtonClick(); + + await recordingStarted; + + is($("#empty-notice").hidden, true, "Empty notice not shown"); + is($("#waiting-notice").hidden, false, "Waiting notice shown"); + + const recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED); + const recordingCancelled = once(window, EVENTS.SNAPSHOT_RECORDING_CANCELLED); + + await Promise.all([recordingFinished, recordingCancelled]); + + ok(true, "Recording stopped and was considered failed."); + + is(SnapshotsListView.itemCount, 0, "No snapshots in the list."); + is($("#empty-notice").hidden, false, "Empty notice shown"); + is($("#waiting-notice").hidden, true, "Waiting notice not shown"); + + await teardown(panel); + finish(); +}
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/browser_profiling-canvas.js @@ -0,0 +1,45 @@ +/* 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 profiling. + */ + +async function ifTestingSupported() { + const currentTime = window.performance.now(); + const { target, front } = await initCanvasDebuggerBackend(SIMPLE_CANVAS_URL); + + const navigated = once(target, "navigate"); + + await front.setup({ reload: true }); + ok(true, "The front was setup up successfully."); + + await navigated; + ok(true, "Target automatically navigated when the front was set up."); + + const snapshotActor = await front.recordAnimationFrame(); + ok(snapshotActor, + "A snapshot actor was sent after recording."); + + const animationOverview = await snapshotActor.getOverview(); + ok(animationOverview, + "An animation overview could be retrieved after recording."); + + const 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."); + + info("Check the timestamps of function calls"); + + for (let i = 0; i < functionCalls.length - 1; i += 2) { + ok(functionCalls[i].timestamp > 0, "The timestamp of the called function is larger than 0."); + ok(functionCalls[i].timestamp < currentTime, "The timestamp has been minus the frame start time."); + ok(functionCalls[i + 1].timestamp >= functionCalls[i].timestamp, "The timestamp of the called function is correct."); + } + + await removeTab(target.tab); + finish(); +}
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/browser_profiling-webgl.js @@ -0,0 +1,89 @@ +/* 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 profiling. + */ + +async function ifTestingSupported() { + const currentTime = window.performance.now(); + info("Start to estimate WebGL drawArrays function."); + var { target, front } = await initCanvasDebuggerBackend(WEBGL_DRAW_ARRAYS); + + let navigated = once(target, "navigate"); + + await front.setup({ reload: true }); + ok(true, "The front was setup up successfully."); + + await navigated; + ok(true, "Target automatically navigated when the front was set up."); + + let snapshotActor = await front.recordAnimationFrame(); + ok(snapshotActor, + "A snapshot actor was sent after recording."); + + let animationOverview = await snapshotActor.getOverview(); + ok(animationOverview, + "An animation overview could be retrieved after recording."); + + let functionCalls = animationOverview.calls; + ok(functionCalls, + "An array of function call actors was sent after recording."); + + testFunctionCallTimestamp(functionCalls, currentTime); + + info("Check triangle and vertex counts in drawArrays()"); + is(animationOverview.primitive.tris, 5, "The count of triangles is correct."); + is(animationOverview.primitive.vertices, 26, "The count of vertices is correct."); + is(animationOverview.primitive.points, 4, "The count of points is correct."); + is(animationOverview.primitive.lines, 8, "The count of lines is correct."); + + await removeTab(target.tab); + + info("Start to estimate WebGL drawElements function."); + const result = await initCanvasDebuggerBackend(WEBGL_DRAW_ELEMENTS); + target = result.target; + front = result.front; + + navigated = once(target, "navigate"); + + await front.setup({ reload: true }); + ok(true, "The front was setup up successfully."); + + await navigated; + ok(true, "Target automatically navigated when the front was set up."); + + snapshotActor = await front.recordAnimationFrame(); + ok(snapshotActor, + "A snapshot actor was sent after recording."); + + animationOverview = await snapshotActor.getOverview(); + ok(animationOverview, + "An animation overview could be retrieved after recording."); + + functionCalls = animationOverview.calls; + ok(functionCalls, + "An array of function call actors was sent after recording."); + + testFunctionCallTimestamp(functionCalls, currentTime); + + info("Check triangle and vertex counts in drawElements()"); + is(animationOverview.primitive.tris, 5, "The count of triangles is correct."); + is(animationOverview.primitive.vertices, 26, "The count of vertices is correct."); + is(animationOverview.primitive.points, 4, "The count of points is correct."); + is(animationOverview.primitive.lines, 8, "The count of lines is correct."); + + await removeTab(target.tab); + finish(); +} + +function testFunctionCallTimestamp(functionCalls, currentTime) { + info("Check the timestamps of function calls"); + + for (let i = 0; i < functionCalls.length - 1; i += 2) { + ok(functionCalls[i].timestamp > 0, "The timestamp of the called function is larger than 0."); + ok(functionCalls[i].timestamp < currentTime, "The timestamp has been minus the frame start time."); + ok(functionCalls[i + 1].timestamp >= functionCalls[i].timestamp, "The timestamp of the called function is correct."); + } +}
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/call-watcher-actor.js @@ -0,0 +1,26 @@ +/* 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 protocol = require("devtools/shared/protocol"); +const { + callWatcherSpec, +} = require("chrome://mochitests/content/browser/devtools/client/canvasdebugger/test/call-watcher-spec"); +const {CallWatcher} = require("devtools/server/actors/utils/call-watcher"); + +/** + * This actor observes function calls on certain objects or globals. + * It wraps the CallWatcher Helper so that it can be observed by tests + */ +exports.CallWatcherActor = protocol.ActorClassWithSpec(callWatcherSpec, { + initialize: function(conn, targetActor) { + protocol.Actor.prototype.initialize.call(this, conn); + CallWatcher.call(this, conn, targetActor); + }, + destroy: function(conn) { + protocol.Actor.prototype.destroy.call(this, conn); + this.finalize(); + }, + ...CallWatcher.prototype, +});
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/call-watcher-front.js @@ -0,0 +1,23 @@ +/* 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 { + callWatcherSpec, +} = require("chrome://mochitests/content/browser/devtools/client/canvasdebugger/test/call-watcher-spec"); +const protocol = require("devtools/shared/protocol"); + +/** + * The corresponding Front object for the CallWatcherActor. + */ +class CallWatcherFront extends protocol.FrontClassWithSpec(callWatcherSpec) { + constructor(client) { + super(client); + + // Attribute name from which to retrieve the actorID out of the target actor's form + this.formAttributeName = "callWatcherActor"; + } +} +exports.CallWatcherFront = CallWatcherFront; +protocol.registerFront(CallWatcherFront);
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/call-watcher-spec.js @@ -0,0 +1,49 @@ +/* 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 protocol = require("devtools/shared/protocol"); +const { Arg, RetVal, Option, generateActorSpec } = protocol; + +const callWatcherSpec = generateActorSpec({ + typeName: "call-watcher", + + events: { + /** + * Events emitted when the `onCall` function isn't provided. + */ + "call": { + type: "call", + function: Arg(0, "function-call"), + }, + }, + + methods: { + setup: { + request: { + tracedGlobals: Option(0, "nullable:array:string"), + tracedFunctions: Option(0, "nullable:array:string"), + startRecording: Option(0, "boolean"), + performReload: Option(0, "boolean"), + holdWeak: Option(0, "boolean"), + storeCalls: Option(0, "boolean"), + }, + oneway: true, + }, + finalize: { + oneway: true, + }, + isRecording: { + response: RetVal("boolean"), + }, + initTimestampEpoch: {}, + resumeRecording: {}, + pauseRecording: { + response: { calls: RetVal("array:function-call") }, + }, + eraseRecording: {}, + }, +}); + +exports.callWatcherSpec = callWatcherSpec;
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/doc_no-canvas.html @@ -0,0 +1,14 @@ +<!-- 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> + </body> + +</html>
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/doc_raf-begin.html @@ -0,0 +1,38 @@ +<!-- 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"> + "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() { + window.requestAnimationFrame(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]); + } + + function start() { + window.requestAnimationFrame(drawScene); +} + </script> + </body> + +</html>
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/doc_raf-no-canvas.html @@ -0,0 +1,20 @@ +<!-- 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> + <script> + function render() { + window.requestAnimationFrame(render); +} + window.requestAnimationFrame(render); + </script> + </body> + +</html>
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/doc_settimeout.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"> + "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.setTimeout(drawScene, 50); + } + + drawScene(); + </script> + </body> + +</html>
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/doc_simple-canvas-bitmasks.html @@ -0,0 +1,34 @@ +<!-- 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"> + "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, 4, 4); + drawRect("rgb(192, 192, 192)", [0, 0, 1, 1]); + window.requestAnimationFrame(drawScene); + } + + drawScene(); + </script> + </body> + +</html>
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/doc_simple-canvas-deep-stack.html @@ -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"> + "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>
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/doc_simple-canvas-transparent.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"> + "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>
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/doc_simple-canvas.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"> + "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>
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/doc_webgl-bindings.html @@ -0,0 +1,61 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>WebGL editor test page</title> + </head> + + <body> + <canvas id="canvas" width="1024" height="1024"></canvas> + + <script type="text/javascript"> + "use strict"; + + let canvas, gl; + let customFramebuffer; + let customRenderbuffer; + let customTexture; + + window.onload = function() { + canvas = document.querySelector("canvas"); + gl = canvas.getContext("webgl", { preserveDrawingBuffer: true }); + gl.clearColor(1.0, 0.0, 0.0, 1.0); + gl.clear(gl.COLOR_BUFFER_BIT); + + customFramebuffer = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, customFramebuffer); + + customRenderbuffer = gl.createRenderbuffer(); + gl.bindRenderbuffer(gl.RENDERBUFFER, customRenderbuffer); + gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, 1024, 1024); + + customTexture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, customTexture); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1024, 1024, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); + + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, customTexture, 0); + gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, customRenderbuffer); + + gl.viewport(128, 256, 384, 512); + gl.clearColor(0.0, 1.0, 0.0, 1.0); + gl.clear(gl.COLOR_BUFFER_BIT); + + drawScene(); + }; + + function drawScene() { + gl.clearColor(0.0, 0.0, 1.0, 1.0); + gl.clear(gl.COLOR_BUFFER_BIT); + window.requestAnimationFrame(drawScene); + } + </script> + </body> + +</html>
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/doc_webgl-drawArrays.html @@ -0,0 +1,187 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>WebGL editor test page</title> + </head> + + <body> + <canvas id="canvas" width="128" height="128"></canvas> + <script id="shader-fs" type="x-shader/x-fragment"> + precision mediump float; + uniform vec4 mtrColor; + + void main(void) { + gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0) * mtrColor; + } + </script> + <script id="shader-vs" type="x-shader/x-vertex"> + attribute vec3 aVertexPosition; + + void main(void) { + gl_PointSize = 5.0; + gl_Position = vec4(aVertexPosition, 1.0); + } + </script> + <script type="text/javascript"> + "use strict"; + + let canvas, gl, shaderProgram; + let triangleVertexPositionBuffer, squareVertexPositionBuffer; + + window.onload = function() { + canvas = document.querySelector("canvas"); + gl = canvas.getContext("webgl", { preserveDrawingBuffer: true }); + gl.viewportWidth = canvas.width; + gl.viewportHeight = canvas.height; + + initShaders(); + initBuffers(); + + gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight); + gl.clearColor(0.0, 0.0, 0.0, 1.0); + gl.disable(gl.DEPTH_TEST); + drawScene(); + }; + + function getShader(gl, id) { + var shaderScript = document.getElementById(id); + if (!shaderScript) { + return null; + } + + var str = ""; + var k = shaderScript.firstChild; + while (k) { + if (k.nodeType == 3) { + str += k.textContent; + } + k = k.nextSibling; + } + + var shader; + if (shaderScript.type == "x-shader/x-fragment") { + shader = gl.createShader(gl.FRAGMENT_SHADER); + } else if (shaderScript.type == "x-shader/x-vertex") { + shader = gl.createShader(gl.VERTEX_SHADER); + } else { + return null; + } + + gl.shaderSource(shader, str); + gl.compileShader(shader); + + if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { + alert(gl.getShaderInfoLog(shader)); + return null; + } + + return shader; + } + + function initShaders() { + var fragmentShader = getShader(gl, "shader-fs"); + var vertexShader = getShader(gl, "shader-vs"); + + shaderProgram = gl.createProgram(); + gl.attachShader(shaderProgram, vertexShader); + gl.attachShader(shaderProgram, fragmentShader); + gl.linkProgram(shaderProgram); + + if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) { + alert("Could not initialise shaders"); + } + + gl.useProgram(shaderProgram); + + shaderProgram.vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "aVertexPosition"); + shaderProgram.pMaterialColor = gl.getUniformLocation(shaderProgram, "mtrColor"); + gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute); + } + + function initBuffers() { + // Create triangle vertex/index buffer + triangleVertexPositionBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer); + var vertices = [ + 0.0, 0.5, 0.0, + -0.5, -0.5, 0.0, + 0.5, -0.5, 0.0, + ]; + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); + triangleVertexPositionBuffer.itemSize = 3; + triangleVertexPositionBuffer.numItems = 3; + + // Create square vertex/index buffer + squareVertexPositionBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer); + vertices = [ + 0.8, 0.8, 0.0, + -0.8, 0.8, 0.0, + 0.8, -0.8, 0.0, + -0.8, -0.8, 0.0, + ]; + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); + squareVertexPositionBuffer.itemSize = 3; + squareVertexPositionBuffer.numItems = 4; + } + + function drawScene() { + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT); + + // DrawArrays + // -------------- + // draw square - triangle strip + gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer); + gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0); + gl.uniform4f(shaderProgram.pMaterialColor, 1, 1, 1, 1); + gl.drawArrays(gl.TRIANGLE_STRIP, 0, squareVertexPositionBuffer.numItems); + + // draw square - triangle fan + gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer); + gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0); + gl.uniform4f(shaderProgram.pMaterialColor, 0, 1, 0, 1); + gl.drawArrays(gl.TRIANGLE_FAN, 0, squareVertexPositionBuffer.numItems); + + // draw triangle + gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer); + gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, triangleVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0); + gl.uniform4f(shaderProgram.pMaterialColor, 1, 0, 0, 1); + gl.drawArrays(gl.TRIANGLES, 0, triangleVertexPositionBuffer.numItems); + + // draw points + gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer); + gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0); + gl.uniform4f(shaderProgram.pMaterialColor, 1, 0, 1, 1); + gl.drawArrays(gl.POINTS, 0, squareVertexPositionBuffer.numItems); + + // draw lines + gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer); + gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0); + gl.uniform4f(shaderProgram.pMaterialColor, 0, 0, 1, 1); + gl.lineWidth(8.0); + gl.drawArrays(gl.LINES, 0, squareVertexPositionBuffer.numItems); + + // draw line strip + gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer); + gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0); + gl.uniform4f(shaderProgram.pMaterialColor, 0.9, 0.6, 0, 1); + gl.lineWidth(3.0); + gl.drawArrays(gl.LINE_STRIP, 0, squareVertexPositionBuffer.numItems); + + // draw line loop + gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer); + gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, triangleVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0); + gl.uniform4f(shaderProgram.pMaterialColor, 0, 1, 1, 1); + gl.lineWidth(3.0); + gl.drawArrays(gl.LINE_LOOP, 0, triangleVertexPositionBuffer.numItems); + + window.requestAnimationFrame(drawScene); + } + </script> + </body> + +</html> \ No newline at end of file
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/doc_webgl-drawElements.html @@ -0,0 +1,224 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>WebGL editor test page</title> + </head> + + <body> + <canvas id="canvas" width="128" height="128"></canvas> + <script id="shader-fs" type="x-shader/x-fragment"> + precision mediump float; + uniform vec4 mtrColor; + + void main(void) { + gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0) * mtrColor; + } + </script> + <script id="shader-vs" type="x-shader/x-vertex"> + attribute vec3 aVertexPosition; + + void main(void) { + gl_PointSize = 5.0; + gl_Position = vec4(aVertexPosition, 1.0); + } + </script> + <script type="text/javascript"> + "use strict"; + + let canvas, gl, shaderProgram; + let triangleVertexPositionBuffer, squareVertexPositionBuffer; + let triangleIndexBuffer; + let squareIndexBuffer, squareStripIndexBuffer, squareFanIndexBuffer; + + window.onload = function() { + canvas = document.querySelector("canvas"); + gl = canvas.getContext("webgl", { preserveDrawingBuffer: true }); + gl.viewportWidth = canvas.width; + gl.viewportHeight = canvas.height; + + initShaders(); + initBuffers(); + + gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight); + gl.clearColor(0.0, 0.0, 0.0, 1.0); + gl.disable(gl.DEPTH_TEST); + drawScene(); + }; + + function getShader(gl, id) { + var shaderScript = document.getElementById(id); + if (!shaderScript) { + return null; + } + + var str = ""; + var k = shaderScript.firstChild; + while (k) { + if (k.nodeType == 3) { + str += k.textContent; + } + k = k.nextSibling; + } + + var shader; + if (shaderScript.type == "x-shader/x-fragment") { + shader = gl.createShader(gl.FRAGMENT_SHADER); + } else if (shaderScript.type == "x-shader/x-vertex") { + shader = gl.createShader(gl.VERTEX_SHADER); + } else { + return null; + } + + gl.shaderSource(shader, str); + gl.compileShader(shader); + + if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { + alert(gl.getShaderInfoLog(shader)); + return null; + } + + return shader; + } + + function initShaders() { + var fragmentShader = getShader(gl, "shader-fs"); + var vertexShader = getShader(gl, "shader-vs"); + + shaderProgram = gl.createProgram(); + gl.attachShader(shaderProgram, vertexShader); + gl.attachShader(shaderProgram, fragmentShader); + gl.linkProgram(shaderProgram); + + if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) { + alert("Could not initialise shaders"); + } + + gl.useProgram(shaderProgram); + + shaderProgram.vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "aVertexPosition"); + shaderProgram.pMaterialColor = gl.getUniformLocation(shaderProgram, "mtrColor"); + gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute); + } + + function initBuffers() { + // Create triangle vertex/index buffer + triangleVertexPositionBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer); + var vertices = [ + 0.0, 0.5, 0.0, + -0.5, -0.5, 0.0, + 0.5, -0.5, 0.0, + ]; + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); + triangleVertexPositionBuffer.itemSize = 3; + triangleVertexPositionBuffer.numItems = 3; + + triangleIndexBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, triangleIndexBuffer); + var indices = [ + 0, 1, 2, + ]; + gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW); + triangleIndexBuffer.itemSize = 1; + triangleIndexBuffer.numItems = 3; + + // Create square vertex/index buffer + squareVertexPositionBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer); + vertices = [ + 0.8, 0.8, 0.0, + -0.8, 0.8, 0.0, + 0.8, -0.8, 0.0, + -0.8, -0.8, 0.0, + ]; + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); + squareVertexPositionBuffer.itemSize = 3; + squareVertexPositionBuffer.numItems = 4; + + squareIndexBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, squareIndexBuffer); + indices = [ + 0, 1, 2, + 1, 3, 2, + ]; + gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW); + squareIndexBuffer.itemSize = 1; + squareIndexBuffer.numItems = 6; + + squareStripIndexBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, squareStripIndexBuffer); + indices = [ + 0, 1, 2, 3, + ]; + gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW); + squareStripIndexBuffer.itemSize = 1; + squareStripIndexBuffer.numItems = 4; + } + + function drawScene() { + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT); + + // DrawElements + // -------------- + // draw triangle + gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer); + gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0); + gl.uniform4f(shaderProgram.pMaterialColor, 1, 1, 1, 1); + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, squareIndexBuffer); + gl.drawElements(gl.TRIANGLES, squareIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0); + + // draw square - triangle strip + gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer); + gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0); + gl.uniform4f(shaderProgram.pMaterialColor, 0, 1, 0, 1); + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, squareStripIndexBuffer); + gl.drawElements(gl.TRIANGLE_FAN, squareStripIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0); + + // draw square - triangle fan + gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer); + gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, triangleVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0); + gl.uniform4f(shaderProgram.pMaterialColor, 1, 0, 0, 1); + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, triangleIndexBuffer); + gl.drawElements(gl.TRIANGLE_FAN, triangleIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0); + + // draw points + gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer); + gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0); + gl.uniform4f(shaderProgram.pMaterialColor, 1, 0, 1, 1); + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, squareStripIndexBuffer); + gl.drawElements(gl.POINTS, squareStripIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0); + + // draw lines + gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer); + gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0); + gl.uniform4f(shaderProgram.pMaterialColor, 0, 0, 1, 1); + gl.lineWidth(8.0); + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, squareStripIndexBuffer); + gl.drawElements(gl.LINES, squareStripIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0); + + // draw line strip + gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer); + gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0); + gl.uniform4f(shaderProgram.pMaterialColor, 0.9, 0.6, 0, 1); + gl.lineWidth(3.0); + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, squareStripIndexBuffer); + gl.drawElements(gl.LINE_STRIP, squareStripIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0); + + // draw line loop + gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer); + gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, triangleVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0); + gl.uniform4f(shaderProgram.pMaterialColor, 0, 1, 1, 1); + gl.lineWidth(3.0); + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, triangleIndexBuffer); + gl.drawElements(gl.LINE_LOOP, triangleIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0); + + window.requestAnimationFrame(drawScene); + } + </script> + </body> + +</html> \ No newline at end of file
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/doc_webgl-enum.html @@ -0,0 +1,34 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>WebGL editor test page</title> + </head> + + <body> + <canvas id="canvas" width="128" height="128"></canvas> + + <script type="text/javascript"> + "use strict"; + + let canvas, gl; + + window.onload = function() { + canvas = document.querySelector("canvas"); + gl = canvas.getContext("webgl", { preserveDrawingBuffer: true }); + gl.clearColor(0.0, 0.0, 0.0, 1.0); + drawScene(); + }; + + function drawScene() { + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT); + gl.bindTexture(gl.TEXTURE_2D, null); + window.requestAnimationFrame(drawScene); + } + </script> + </body> + +</html>
new file mode 100644 --- /dev/null +++ b/devtools/client/canvasdebugger/test/head.js @@ -0,0 +1,198 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint no-unused-vars: [2, {"vars": "local"}] */ +/* import-globals-from ../../shared/test/shared-head.js */ +/* import-globals-from ../../debugger/new/test/mochitest/helpers/context.js */ + +"use strict"; + +// Load the shared-head file first. +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js", + this); + +// Import helpers for the new debugger +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/debugger/new/test/mochitest/helpers/context.js", + this); + +var { generateUUID } = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator); + +var { DebuggerClient } = require("devtools/shared/client/debugger-client"); +var { DebuggerServer } = require("devtools/server/main"); +var { METHOD_FUNCTION } = require("devtools/shared/fronts/function-call"); +var { Toolbox } = require("devtools/client/framework/toolbox"); +var { isWebGLSupported } = require("devtools/client/shared/webgl-utils"); + +const EXAMPLE_URL = "http://example.com/browser/devtools/client/canvasdebugger/test/"; +const SET_TIMEOUT_URL = EXAMPLE_URL + "doc_settimeout.html"; +const NO_CANVAS_URL = EXAMPLE_URL + "doc_no-canvas.html"; +const RAF_NO_CANVAS_URL = EXAMPLE_URL + "doc_raf-no-canvas.html"; +const SIMPLE_CANVAS_URL = EXAMPLE_URL + "doc_simple-canvas.html"; +const SIMPLE_BITMASKS_URL = EXAMPLE_URL + "doc_simple-canvas-bitmasks.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"; +const WEBGL_ENUM_URL = EXAMPLE_URL + "doc_webgl-enum.html"; +const WEBGL_BINDINGS_URL = EXAMPLE_URL + "doc_webgl-bindings.html"; +const WEBGL_DRAW_ARRAYS = EXAMPLE_URL + "doc_webgl-drawArrays.html"; +const WEBGL_DRAW_ELEMENTS = EXAMPLE_URL + "doc_webgl-drawElements.html"; +const RAF_BEGIN_URL = EXAMPLE_URL + "doc_raf-begin.html"; + +// Disable logging for all the tests. Both the debugger server and frontend will +// be affected by this pref. +var gEnableLogging = Services.prefs.getBoolPref("devtools.debugger.log"); +Services.prefs.setBoolPref("devtools.debugger.log", false); + +var gToolEnabled = Services.prefs.getBoolPref("devtools.canvasdebugger.enabled"); + +registerCleanupFunction(() => { + 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 handleError(aError) { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + finish(); +} + +var 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(); +} + +async function test() { + const generator = isTestingSupported() ? ifTestingSupported : ifTestingUnsupported; + try { + await generator(); + } catch (e) { + handleError(e); + } +} + +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; + } + + const supported = isWebGLSupported(document); + + info("This test requires WebGL support."); + info("Apparently, WebGL is" + (supported ? "" : " not") + " supported."); + return supported; +} + +function navigateInHistory(aTarget, aDirection, aWaitForTargetEvent = "navigate") { + executeSoon(() => content.history[aDirection]()); + return once(aTarget, aWaitForTargetEvent); +} + +function navigate(aTarget, aUrl, aWaitForTargetEvent = "navigate") { + executeSoon(() => aTarget.navigateTo({ url: aUrl })); + return once(aTarget, aWaitForTargetEvent); +} + +function reload(aTarget, aWaitForTargetEvent = "navigate") { + executeSoon(() => aTarget.reload()); + return once(aTarget, aWaitForTargetEvent); +} + +function initServer() { + DebuggerServer.init(); + DebuggerServer.registerAllActors(); +} + +function initCallWatcherBackend(aUrl) { + info("Initializing a call watcher front."); + initServer(); + + return (async function() { + const tab = await addTab(aUrl); + + await registerActorInContentProcess("chrome://mochitests/content/browser/devtools/client/canvasdebugger/test/call-watcher-actor.js", { + prefix: "callWatcher", + constructor: "CallWatcherActor", + type: { target: true }, + }); + + const target = await TargetFactory.forTab(tab); + await target.attach(); + + // Load the Front module in order to register it and have getFront to find it. + require("chrome://mochitests/content/browser/devtools/client/canvasdebugger/test/call-watcher-front.js"); + + const front = await target.getFront("call-watcher"); + return { target, front }; + })(); +} + +function initCanvasDebuggerBackend(aUrl) { + info("Initializing a canvas debugger front."); + initServer(); + + return (async function() { + const tab = await addTab(aUrl); + const target = await TargetFactory.forTab(tab); + await target.attach(); + + const front = await target.getFront("canvas"); + return { target, front }; + })(); +} + +function initCanvasDebuggerFrontend(aUrl) { + info("Initializing a canvas debugger pane."); + + return (async function() { + const tab = await addTab(aUrl); + const target = await TargetFactory.forTab(tab); + + await target.attach(); + + Services.prefs.setBoolPref("devtools.canvasdebugger.enabled", true); + const toolbox = await gDevTools.showToolbox(target, "canvasdebugger"); + const panel = toolbox.getCurrentPanel(); + return { target, panel }; + })(); +} + +function teardown({target}) { + info("Destroying the specified canvas debugger."); + + const {tab} = target; + return gDevTools.closeToolbox(target).then(() => { + removeTab(tab); + }); +} + +function getSourceActor(aSources, aURL) { + const item = aSources.getItemForAttachment(a => a.source.url === aURL); + return item ? item.value : null; +} + +async function validateDebuggerLocation(dbg, url, line) { + const location = dbg.selectors.getSelectedLocation(dbg.getState()); + const sourceUrl = dbg.selectors.getSelectedSource(dbg.getState()).url; + + is(sourceUrl, url, + "The expected source was shown in the debugger."); + is(location.line, line, + "The expected source line is highlighted in the debugger."); +}
--- a/devtools/client/debugger/new/src/client/firefox/types.js +++ b/devtools/client/debugger/new/src/client/firefox/types.js @@ -152,39 +152,44 @@ export type ResumedPacket = { export type FramesResponse = { frames: FramePacket[], from: ActorId }; export type TabPayload = { actor: ActorId, animationsActor: ActorId, + callWatcherActor: ActorId, + canvasActor: ActorId, consoleActor: ActorId, cssPropertiesActor: ActorId, cssUsageActor: ActorId, directorManagerActor: ActorId, emulationActor: ActorId, eventLoopLagActor: ActorId, framerateActor: ActorId, + gcliActor: ActorId, inspectorActor: ActorId, memoryActor: ActorId, monitorActor: ActorId, outerWindowID: number, performanceActor: ActorId, performanceEntriesActor: ActorId, profilerActor: ActorId, promisesActor: ActorId, reflowActor: ActorId, storageActor: ActorId, styleEditorActor: ActorId, styleSheetsActor: ActorId, timelineActor: ActorId, title: string, url: URL, - webExtensionInspectedWindowActor: ActorId + webExtensionInspectedWindowActor: ActorId, + webaudioActor: ActorId, + webglActor: ActorId }; /** * Actions * @memberof firefox * @static */ export type Actions = {
--- a/devtools/client/definitions.js +++ b/devtools/client/definitions.js @@ -1,23 +1,26 @@ /* 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 Services = require("Services"); const osString = Services.appinfo.OS; +const { Cu } = require("chrome"); // Panels loader.lazyGetter(this, "OptionsPanel", () => require("devtools/client/framework/toolbox-options").OptionsPanel); loader.lazyGetter(this, "InspectorPanel", () => require("devtools/client/inspector/panel").InspectorPanel); loader.lazyGetter(this, "WebConsolePanel", () => require("devtools/client/webconsole/panel").WebConsolePanel); loader.lazyGetter(this, "NewDebuggerPanel", () => require("devtools/client/debugger/new/panel").DebuggerPanel); loader.lazyGetter(this, "StyleEditorPanel", () => require("devtools/client/styleeditor/panel").StyleEditorPanel); +loader.lazyGetter(this, "CanvasDebuggerPanel", () => require("devtools/client/canvasdebugger/panel").CanvasDebuggerPanel); +loader.lazyGetter(this, "WebAudioEditorPanel", () => require("devtools/client/webaudioeditor/panel").WebAudioEditorPanel); loader.lazyGetter(this, "MemoryPanel", () => require("devtools/client/memory/panel").MemoryPanel); loader.lazyGetter(this, "PerformancePanel", () => require("devtools/client/performance/panel").PerformancePanel); loader.lazyGetter(this, "NewPerformancePanel", () => require("devtools/client/performance-new/panel").PerformancePanel); loader.lazyGetter(this, "NetMonitorPanel", () => require("devtools/client/netmonitor/panel").NetMonitorPanel); loader.lazyGetter(this, "StoragePanel", () => require("devtools/client/storage/panel").StoragePanel); loader.lazyGetter(this, "ScratchpadPanel", () => require("devtools/client/scratchpad/panel").ScratchpadPanel); loader.lazyGetter(this, "DomPanel", () => require("devtools/client/dom/panel").DomPanel); loader.lazyGetter(this, "AccessibilityPanel", () => require("devtools/client/accessibility/panel").AccessibilityPanel); @@ -31,16 +34,19 @@ loader.lazyRequireGetter(this, "Responsi loader.lazyImporter(this, "ScratchpadManager", "resource://devtools/client/scratchpad/scratchpad-manager.jsm"); const {MultiLocalizationHelper} = require("devtools/shared/l10n"); const L10N = new MultiLocalizationHelper( "devtools/client/locales/startup.properties", "devtools/startup/locales/key-shortcuts.properties" ); +// URL to direct people to the deprecated tools panel +const DEPRECATION_URL = "https://developer.mozilla.org/en-US/docs/Tools/Deprecated_tools"; + var Tools = {}; exports.Tools = Tools; // Definitions Tools.options = { id: "options", ordinal: 0, url: "chrome://devtools/content/framework/toolbox-options.xhtml", @@ -166,16 +172,66 @@ Tools.styleEditor = { return target.hasActor("styleSheets"); }, build: function(iframeWindow, toolbox) { return new StyleEditorPanel(iframeWindow, toolbox); }, }; +Tools.shaderEditor = { + id: "shadereditor", + deprecated: true, + deprecationURL: DEPRECATION_URL, + ordinal: 5, + visibilityswitch: "devtools.shadereditor.enabled", + icon: "chrome://devtools/skin/images/tool-shadereditor.svg", + url: "chrome://devtools/content/shadereditor/index.xul", + label: l10n("ToolboxShaderEditor.label"), + panelLabel: l10n("ToolboxShaderEditor.panelLabel"), + tooltip: l10n("ToolboxShaderEditor.tooltip"), + + isTargetSupported: function(target) { + return target.hasActor("webgl") && !target.chrome; + }, + + build: function(iframeWindow, toolbox) { + const { BrowserLoader } = Cu.import("resource://devtools/client/shared/browser-loader.js", {}); + const browserRequire = BrowserLoader({ + baseURI: "resource://devtools/client/shadereditor/", + window: iframeWindow, + }).require; + const { ShaderEditorPanel } = browserRequire("devtools/client/shadereditor/panel"); + return new ShaderEditorPanel(toolbox); + }, +}; + +Tools.canvasDebugger = { + id: "canvasdebugger", + deprecated: true, + deprecationURL: DEPRECATION_URL, + ordinal: 6, + visibilityswitch: "devtools.canvasdebugger.enabled", + icon: "chrome://devtools/skin/images/tool-canvas.svg", + url: "chrome://devtools/content/canvasdebugger/index.xul", + label: l10n("ToolboxCanvasDebugger.label"), + panelLabel: l10n("ToolboxCanvasDebugger.panelLabel"), + tooltip: l10n("ToolboxCanvasDebugger.tooltip"), + + // Hide the Canvas Debugger in the Add-on Debugger and Browser Toolbox + // (bug 1047520). + isTargetSupported: function(target) { + return target.hasActor("canvas") && !target.chrome; + }, + + build: function(iframeWindow, toolbox) { + return new CanvasDebuggerPanel(iframeWindow, toolbox); + }, +}; + Tools.performance = { id: "performance", ordinal: 7, icon: "chrome://devtools/skin/images/tool-profiler.svg", visibilityswitch: "devtools.performance.enabled", label: l10n("performance.label"), panelLabel: l10n("performance.panelLabel"), get tooltip() { @@ -284,16 +340,37 @@ Tools.storage = { (target.hasActor("storage") && target.getTrait("storageInspector")); }, build: function(iframeWindow, toolbox) { return new StoragePanel(iframeWindow, toolbox); }, }; +Tools.webAudioEditor = { + id: "webaudioeditor", + deprecated: true, + deprecationURL: DEPRECATION_URL, + ordinal: 11, + visibilityswitch: "devtools.webaudioeditor.enabled", + icon: "chrome://devtools/skin/images/tool-webaudio.svg", + url: "chrome://devtools/content/webaudioeditor/index.xul", + label: l10n("ToolboxWebAudioEditor1.label"), + panelLabel: l10n("ToolboxWebAudioEditor1.panelLabel"), + tooltip: l10n("ToolboxWebAudioEditor1.tooltip"), + + isTargetSupported: function(target) { + return !target.chrome && target.hasActor("webaudio"); + }, + + build: function(iframeWindow, toolbox) { + return new WebAudioEditorPanel(iframeWindow, toolbox); + }, +}; + Tools.scratchpad = { id: "scratchpad", ordinal: 12, visibilityswitch: "devtools.scratchpad.enabled", icon: "chrome://devtools/skin/images/tool-scratchpad.svg", url: "chrome://devtools/content/scratchpad/index.xul", label: l10n("scratchpad.label"), panelLabel: l10n("scratchpad.panelLabel"), @@ -384,16 +461,19 @@ Tools.application = { }; var defaultTools = [ Tools.options, Tools.webConsole, Tools.inspector, Tools.jsdebugger, Tools.styleEditor, + Tools.shaderEditor, + Tools.canvasDebugger, + Tools.webAudioEditor, Tools.performance, Tools.netMonitor, Tools.storage, Tools.scratchpad, Tools.memory, Tools.dom, Tools.accessibility, Tools.application,
--- a/devtools/client/framework/test/browser_target_support.js +++ b/devtools/client/framework/test/browser_target_support.js @@ -5,36 +5,36 @@ // Test support methods on Target, such as `hasActor`, `getActorDescription`, // `actorHasMethod` and `getTrait`. async function testTarget(client, target) { await target.attach(); is(target.hasActor("inspector"), true, "target.hasActor() true when actor exists."); - is(target.hasActor("storage"), true, "target.hasActor() true when actor exists."); + is(target.hasActor("webaudio"), true, "target.hasActor() true when actor exists."); is(target.hasActor("notreal"), false, "target.hasActor() false when actor does not exist."); // Create a front to ensure the actor is loaded - await target.getFront("storage"); + await target.getFront("webaudio"); - let desc = await target.getActorDescription("storage"); - is(desc.typeName, "storage", + let desc = await target.getActorDescription("webaudio"); + is(desc.typeName, "webaudio", "target.getActorDescription() returns definition data for corresponding actor"); - is(desc.events["stores-update"].type, "storesUpdate", + is(desc.events["start-context"].type, "startContext", "target.getActorDescription() returns event data for corresponding actor"); desc = await target.getActorDescription("nope"); is(desc, undefined, "target.getActorDescription() returns undefined for non-existing actor"); desc = await target.getActorDescription(); is(desc, undefined, "target.getActorDescription() returns undefined for undefined actor"); - let hasMethod = await target.actorHasMethod("storage", "listStores"); + let hasMethod = await target.actorHasMethod("audionode", "getType"); is(hasMethod, true, "target.actorHasMethod() returns true for existing actor with method"); - hasMethod = await target.actorHasMethod("localStorage", "nope"); + hasMethod = await target.actorHasMethod("audionode", "nope"); is(hasMethod, false, "target.actorHasMethod() returns false for existing actor with no method"); hasMethod = await target.actorHasMethod("nope", "nope"); is(hasMethod, false, "target.actorHasMethod() returns false for non-existing actor with no method"); hasMethod = await target.actorHasMethod(); is(hasMethod, false, "target.actorHasMethod() returns false for undefined params");
--- a/devtools/client/framework/toolbox-options.js +++ b/devtools/client/framework/toolbox-options.js @@ -244,16 +244,29 @@ OptionsPanel.prototype = { } checkboxInput.addEventListener("change", onCheckboxClick.bind(checkboxInput, this.telemetry, tool)); checkboxLabel.appendChild(checkboxInput); checkboxLabel.appendChild(checkboxSpanLabel); + // TODO: remove in Firefox 68, with bug #1528296 + if (tool.deprecated) { + const deprecationURL = this.panelDoc.createElement("a"); + deprecationURL.title = deprecationURL.href = tool.deprecationURL; + deprecationURL.textContent = L10N.getFormatStr("options.deprecationNotice"); + deprecationURL.target = "_blank"; + + const checkboxSpanDeprecated = this.panelDoc.createElement("span"); + checkboxSpanDeprecated.className = "deprecation-notice"; + checkboxLabel.appendChild(checkboxSpanDeprecated); + checkboxSpanDeprecated.appendChild(deprecationURL); + } + return checkboxLabel; }; // Clean up any existent default tools content. for (const label of defaultToolsBox.querySelectorAll("label")) { label.remove(); }
--- a/devtools/client/framework/toolbox.js +++ b/devtools/client/framework/toolbox.js @@ -1811,16 +1811,29 @@ Toolbox.prototype = { // If no parent yet, append the frame into default location. if (!iframe.parentNode) { const vbox = this.doc.getElementById("toolbox-panel-" + id); vbox.appendChild(iframe); vbox.visibility = "visible"; } + // TODO: remove in Firefox 68, with bug #1528296 + if (definition.deprecated) { + const deprecationURL = this.doc.createXULElement("label"); + deprecationURL.textContent = L10N.getFormatStr("options.deprecationNotice"); + deprecationURL.setAttribute("href", definition.deprecationURL); + deprecationURL.setAttribute("class", "text-link"); + + const deprecationNotice = this.doc.createXULElement("span"); + deprecationNotice.className = "toolbox-panel_deprecation-notice"; + deprecationNotice.appendChild(deprecationURL); + iframe.parentNode.prepend(deprecationNotice); + } + const onLoad = async () => { if (id === "inspector") { await this._initInspector; // Stop loading the inspector if the toolbox is already being destroyed. This // can happen in unit tests where the tests are rapidly opening and closing the // toolbox and we encounter the scenario where the toolbox is closing as // the inspector is still loading.
--- a/devtools/client/jar.mn +++ b/devtools/client/jar.mn @@ -21,16 +21,30 @@ devtools.jar: content/shared/sourceeditor/codemirror/keymap/emacs.js (shared/sourceeditor/codemirror/keymap/emacs.js) content/shared/sourceeditor/codemirror/keymap/vim.js (shared/sourceeditor/codemirror/keymap/vim.js) content/shared/sourceeditor/codemirror/keymap/sublime.js (shared/sourceeditor/codemirror/keymap/sublime.js) content/shared/sourceeditor/codemirror/codemirror.bundle.js (shared/sourceeditor/codemirror/codemirror.bundle.js) content/shared/sourceeditor/codemirror/lib/codemirror.css (shared/sourceeditor/codemirror/lib/codemirror.css) content/shared/sourceeditor/codemirror/mozilla.css (shared/sourceeditor/codemirror/mozilla.css) content/shared/sourceeditor/codemirror/cmiframe.html (shared/sourceeditor/codemirror/cmiframe.html) content/debugger/new/index.html (debugger/new/index.html) + content/shadereditor/index.xul (shadereditor/index.xul) + content/canvasdebugger/index.xul (canvasdebugger/index.xul) + content/canvasdebugger/canvasdebugger.js (canvasdebugger/canvasdebugger.js) + content/canvasdebugger/snapshotslist.js (canvasdebugger/snapshotslist.js) + content/canvasdebugger/callslist.js (canvasdebugger/callslist.js) + content/webaudioeditor/index.xul (webaudioeditor/index.xul) + content/webaudioeditor/includes.js (webaudioeditor/includes.js) + content/webaudioeditor/models.js (webaudioeditor/models.js) + content/webaudioeditor/controller.js (webaudioeditor/controller.js) + content/webaudioeditor/views/utils.js (webaudioeditor/views/utils.js) + content/webaudioeditor/views/context.js (webaudioeditor/views/context.js) + content/webaudioeditor/views/inspector.js (webaudioeditor/views/inspector.js) + content/webaudioeditor/views/properties.js (webaudioeditor/views/properties.js) + content/webaudioeditor/views/automation.js (webaudioeditor/views/automation.js) content/performance/index.xul (performance/index.xul) content/performance-new/index.xhtml (performance-new/index.xhtml) content/performance-new/frame-script.js (performance-new/frame-script.js) content/memory/index.xhtml (memory/index.xhtml) content/framework/toolbox-window.xul (framework/toolbox-window.xul) content/framework/toolbox-options.xhtml (framework/toolbox-options.xhtml) content/framework/toolbox.xul (framework/toolbox.xul) content/framework/toolbox-init.js (framework/toolbox-init.js) @@ -71,16 +85,17 @@ devtools.jar: skin/images/arrow.svg (themes/images/arrow.svg) skin/images/arrow-big.svg (themes/images/arrow-big.svg) skin/images/arrowhead-left.svg (themes/images/arrowhead-left.svg) skin/images/arrowhead-right.svg (themes/images/arrowhead-right.svg) skin/images/arrowhead-down.svg (themes/images/arrowhead-down.svg) skin/images/arrowhead-up.svg (themes/images/arrowhead-up.svg) skin/images/breadcrumbs-divider.svg (themes/images/breadcrumbs-divider.svg) skin/images/checkbox.svg (themes/images/checkbox.svg) + skin/images/filters.svg (themes/images/filters.svg) skin/images/filter-swatch.svg (themes/images/filter-swatch.svg) skin/images/aboutdebugging-connect-icon.svg (themes/images/aboutdebugging-connect-icon.svg) skin/images/aboutdebugging-firefox-aurora.svg (themes/images/aboutdebugging-firefox-aurora.svg) skin/images/aboutdebugging-firefox-beta.svg (themes/images/aboutdebugging-firefox-beta.svg) skin/images/aboutdebugging-firefox-logo.svg (themes/images/aboutdebugging-firefox-logo.svg) skin/images/aboutdebugging-firefox-nightly.svg (themes/images/aboutdebugging-firefox-nightly.svg) skin/images/aboutdebugging-firefox-release.svg (themes/images/aboutdebugging-firefox-release.svg) skin/images/aboutdebugging-globe-icon.svg (themes/images/aboutdebugging-globe-icon.svg) @@ -94,16 +109,17 @@ devtools.jar: skin/images/copy.svg (themes/images/copy.svg) skin/images/animation-fast-track.svg (themes/images/animation-fast-track.svg) skin/images/performance-details-waterfall.svg (themes/images/performance-details-waterfall.svg) skin/images/performance-details-call-tree.svg (themes/images/performance-details-call-tree.svg) skin/images/performance-details-flamegraph.svg (themes/images/performance-details-flamegraph.svg) skin/breadcrumbs.css (themes/breadcrumbs.css) skin/chart.css (themes/chart.css) skin/widgets.css (themes/widgets.css) + skin/images/power.svg (themes/images/power.svg) skin/rules.css (themes/rules.css) skin/images/command-paintflashing.svg (themes/images/command-paintflashing.svg) skin/images/command-screenshot.svg (themes/images/command-screenshot.svg) skin/images/command-responsivemode.svg (themes/images/command-responsivemode.svg) skin/images/command-replay.svg (themes/images/command-replay.svg) skin/images/command-pick.svg (themes/images/command-pick.svg) skin/images/command-pick-accessibility.svg (themes/images/command-pick-accessibility.svg) skin/images/command-frames.svg (themes/images/command-frames.svg) @@ -116,23 +132,26 @@ devtools.jar: skin/markup.css (themes/markup.css) skin/webconsole.css (themes/webconsole.css) skin/images/webconsole/error.svg (themes/images/webconsole/error.svg) skin/images/webconsole/info.svg (themes/images/webconsole/info.svg) skin/images/webconsole/input.svg (themes/images/webconsole/input.svg) skin/images/webconsole/return.svg (themes/images/webconsole/return.svg) skin/images/breadcrumbs-scrollbutton.svg (themes/images/breadcrumbs-scrollbutton.svg) skin/animation.css (themes/animation.css) + skin/canvasdebugger.css (themes/canvasdebugger.css) skin/perf.css (themes/perf.css) skin/performance.css (themes/performance.css) skin/memory.css (themes/memory.css) skin/scratchpad.css (themes/scratchpad.css) + skin/shadereditor.css (themes/shadereditor.css) skin/storage.css (themes/storage.css) skin/splitview.css (themes/splitview.css) skin/styleeditor.css (themes/styleeditor.css) + skin/webaudioeditor.css (themes/webaudioeditor.css) skin/components-frame.css (themes/components-frame.css) skin/components-h-split-box.css (themes/components-h-split-box.css) skin/jit-optimizations.css (themes/jit-optimizations.css) skin/images/filter.svg (themes/images/filter.svg) skin/images/filter-small.svg (themes/images/filter-small.svg) skin/images/search.svg (themes/images/search.svg) skin/images/item-toggle.svg (themes/images/item-toggle.svg) skin/images/item-arrow-dark-rtl.svg (themes/images/item-arrow-dark-rtl.svg) @@ -142,16 +161,19 @@ devtools.jar: skin/images/dropmarker.svg (themes/images/dropmarker.svg) skin/boxmodel.css (themes/boxmodel.css) skin/images/geometry-editor.svg (themes/images/geometry-editor.svg) skin/images/open-inspector.svg (themes/images/open-inspector.svg) skin/images/more.svg (themes/images/more.svg) skin/images/pause.svg (themes/images/pause.svg) skin/images/play.svg (themes/images/play.svg) skin/images/rewind.svg (themes/images/rewind.svg) + skin/images/canvasdebugger-step-in.svg (themes/images/canvasdebugger-step-in.svg) + skin/images/canvasdebugger-step-out.svg (themes/images/canvasdebugger-step-out.svg) + skin/images/canvasdebugger-step-over.svg (themes/images/canvasdebugger-step-over.svg) skin/images/dock-bottom.svg (themes/images/dock-bottom.svg) skin/images/dock-side-left.svg (themes/images/dock-side-left.svg) skin/images/dock-side-right.svg (themes/images/dock-side-right.svg) skin/images/dock-undock.svg (themes/images/dock-undock.svg) skin/floating-scrollbars-responsive-design.css (themes/floating-scrollbars-responsive-design.css) skin/badge.css (themes/badge.css) skin/inspector.css (themes/inspector.css) skin/images/profiler-stopwatch.svg (themes/images/profiler-stopwatch.svg) @@ -162,23 +184,26 @@ devtools.jar: skin/images/globe.svg (themes/images/globe.svg) skin/images/globe-small.svg (themes/images/globe-small.svg) skin/images/next.svg (themes/images/next.svg) skin/images/next-circle.svg (themes/images/next-circle.svg) skin/images/folder.svg (themes/images/folder.svg) skin/images/sad-face.svg (themes/images/sad-face.svg) skin/images/shape-swatch.svg (themes/images/shape-swatch.svg) skin/images/tool-webconsole.svg (themes/images/tool-webconsole.svg) + skin/images/tool-canvas.svg (themes/images/tool-canvas.svg) skin/images/tool-debugger.svg (themes/images/tool-debugger.svg) skin/images/tool-inspector.svg (themes/images/tool-inspector.svg) + skin/images/tool-shadereditor.svg (themes/images/tool-shadereditor.svg) skin/images/tool-styleeditor.svg (themes/images/tool-styleeditor.svg) skin/images/tool-storage.svg (themes/images/tool-storage.svg) skin/images/tool-profiler.svg (themes/images/tool-profiler.svg) skin/images/tool-network.svg (themes/images/tool-network.svg) skin/images/tool-scratchpad.svg (themes/images/tool-scratchpad.svg) + skin/images/tool-webaudio.svg (themes/images/tool-webaudio.svg) skin/images/tool-memory.svg (themes/images/tool-memory.svg) skin/images/tool-dom.svg (themes/images/tool-dom.svg) skin/images/tool-accessibility.svg (themes/images/tool-accessibility.svg) skin/images/tool-application.svg (themes/images/tool-application.svg) skin/images/close.svg (themes/images/close.svg) skin/images/clear.svg (themes/images/clear.svg) skin/images/close-3-pane.svg (themes/images/close-3-pane.svg) skin/images/open-3-pane.svg (themes/images/open-3-pane.svg)
new file mode 100644 --- /dev/null +++ b/devtools/client/locales/en-US/canvasdebugger.dtd @@ -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.waitingNotice): This is the label shown + - in the call list view while recording a snapshot. --> +<!ENTITY canvasDebuggerUI.waitingNotice "Recording an animation cycle…"> + +<!-- 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">
new file mode 100644 --- /dev/null +++ b/devtools/client/locales/en-US/canvasdebugger.properties @@ -0,0 +1,70 @@ +# 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 (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 + +# LOCALIZATION NOTE (recordingTimeoutFailure): +# This notification alert is displayed when attempting to record a requestAnimationFrame +# cycle in the Canvas Debugger and no cycles detected. This alerts the user that no +# loops were found. +recordingTimeoutFailure=Canvas Debugger could not find a requestAnimationFrame or setTimeout cycle.
new file mode 100644 --- /dev/null +++ b/devtools/client/locales/en-US/shadereditor.dtd @@ -0,0 +1,32 @@ +<!-- 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 (shaderEditorUI.vertexShader): This is the label for + - the pane that displays a vertex shader's source. --> +<!ENTITY shaderEditorUI.vertexShader "Vertex Shader"> + +<!-- LOCALIZATION NOTE (shaderEditorUI.fragmentShader): This is the label for + - the pane that displays a fragment shader's source. --> +<!ENTITY shaderEditorUI.fragmentShader "Fragment Shader"> + +<!-- LOCALIZATION NOTE (shaderEditorUI.reloadNotice1): This is the label shown + - on the button that triggers a page refresh. --> +<!ENTITY shaderEditorUI.reloadNotice1 "Reload"> + +<!-- LOCALIZATION NOTE (shaderEditorUI.reloadNotice2): This is the label shown + - along with the button that triggers a page refresh. --> +<!ENTITY shaderEditorUI.reloadNotice2 "the page to be able to edit GLSL code."> + +<!-- LOCALIZATION NOTE (shaderEditorUI.emptyNotice): This is the label shown + - while the page is refreshing and the tool waits for a WebGL context. --> +<!ENTITY shaderEditorUI.emptyNotice "Waiting for a WebGL context to be created…">
new file mode 100644 --- /dev/null +++ b/devtools/client/locales/en-US/shadereditor.properties @@ -0,0 +1,22 @@ +# 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 Debugger +# which is available from the Web Developer sub-menu -> 'Debugger'. +# 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 (shadersList.programLabel): +# This string is displayed in the programs list of the Shader Editor, +# identifying a set of linked GLSL shaders. +shadersList.programLabel=Program %S + +# LOCALIZATION NOTE (shadersList.blackboxLabel): +# This string is displayed in the programs list of the Shader Editor, while +# the user hovers over the checkbox used to toggle blackboxing of a program's +# associated fragment shader. +shadersList.blackboxLabel=Toggle geometry visibility
--- a/devtools/client/locales/en-US/shared.properties +++ b/devtools/client/locales/en-US/shared.properties @@ -1,8 +1,11 @@ # 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 (dimensions): This is used to display the dimensions # of a node or image, like 100×200. dimensions=%S\u00D7%S +# LOCALIZATION NOTE (sideMenu.groupCheckbox.tooltip): This is used in the SideMenuWidget +# as the default tooltip of a group checkbox +sideMenu.groupCheckbox.tooltip=Toggle all checkboxes in this group \ No newline at end of file
--- a/devtools/client/locales/en-US/startup.properties +++ b/devtools/client/locales/en-US/startup.properties @@ -93,16 +93,44 @@ ToolboxStyleEditor.panelLabel=Style Edit # displayed inside the developer tools window. # A keyboard shortcut for Stylesheet Editor will be shown inside the latter pair of brackets. ToolboxStyleEditor.tooltip3=Stylesheet Editor (CSS) (%S) # LOCALIZATION NOTE (open.accesskey): The access key used to open the style # editor. open.accesskey=l +# LOCALIZATION NOTE (ToolboxShaderEditor.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. +ToolboxShaderEditor.label=Shader Editor + +# LOCALIZATION NOTE (ToolboxShaderEditor.panelLabel): +# This is used as the label for the toolbox panel. +ToolboxShaderEditor.panelLabel=Shader Editor Panel + +# LOCALIZATION NOTE (ToolboxShaderEditor.tooltip): +# This string is displayed in the tooltip of the tab when the Shader Editor is +# displayed inside the developer tools window. +ToolboxShaderEditor.tooltip=Live GLSL shader language editor for WebGL + +# 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.panelLabel): +# This is used as the label for the toolbox panel. +ToolboxCanvasDebugger.panelLabel=Canvas Panel + +# 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 (ToolboxWebAudioEditor1.label): # This string is displayed in the title of the tab when the Web Audio Editor # is displayed inside the developer tools window and in the Developer Tools Menu. ToolboxWebAudioEditor1.label=Web Audio # LOCALIZATION NOTE (ToolboxWebAudioEditor1.panelLabel): # This is used as the label for the toolbox panel. ToolboxWebAudioEditor1.panelLabel=Web Audio Panel
--- a/devtools/client/locales/en-US/toolbox.properties +++ b/devtools/client/locales/en-US/toolbox.properties @@ -22,16 +22,21 @@ toolbox.defaultTitle=Developer Tools toolbox.label=Developer Tools # LOCALIZATION NOTE (options.toolNotSupportedMarker): This is the template # used to add a * marker to the label for the Options Panel tool checkbox for the # tool which is not supported for the current toolbox target. # The name of the tool: %1$S. options.toolNotSupportedMarker=%1$S * +# LOCALIZATION NOTE (options.deprecationNotice): This is the template +# is used to generate a deprecation notice for a panel. +# This entire text is treated as a link to an MDN page. +options.deprecationNotice=Deprecated. Learn More… + # LOCALIZATION NOTE (scratchpad.keycode) # Used for opening scratchpad from the detached toolbox window # Needs to match scratchpad.keycode from browser.dtd scratchpad.keycode=VK_F4 # LOCALIZATION NOTE (toolbox.pickButton.tooltip) # This is the tooltip of the element picker button in the toolbox toolbar. # %S is the keyboard shortcut that toggles the element picker.
new file mode 100644 --- /dev/null +++ b/devtools/client/locales/en-US/webaudioeditor.dtd @@ -0,0 +1,53 @@ +<!-- 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 (webAudioEditorUI.reloadNotice1): This is the label shown + - on the button that triggers a page refresh. --> +<!ENTITY webAudioEditorUI.reloadNotice1 "Reload"> + +<!-- LOCALIZATION NOTE (webAudioEditorUI.reloadNotice2): This is the label shown + - along with the button that triggers a page refresh. --> +<!ENTITY webAudioEditorUI.reloadNotice2 "the page to view and edit the audio context."> + +<!-- LOCALIZATION NOTE (webAudioEditorUI.emptyNotice): This is the label shown + - while the page is refreshing and the tool waits for a audio context. --> +<!ENTITY webAudioEditorUI.emptyNotice "Waiting for an audio context to be created…"> + +<!-- LOCALIZATION NOTE (webAudioEditorUI.tab.properties2): This is the label shown + - for the properties tab view. --> +<!ENTITY webAudioEditorUI.tab.properties2 "Properties"> + +<!-- LOCALIZATION NOTE (webAudioEditorUI.tab.automation): This is the label shown + - for the automation tab view. --> +<!ENTITY webAudioEditorUI.tab.automation "Automation"> + +<!-- LOCALIZATION NOTE (webAudioEditorUI.inspectorTitle): This is the title for the + - AudioNode inspector view. --> +<!ENTITY webAudioEditorUI.inspectorTitle "AudioNode Inspector"> + +<!-- LOCALIZATION NOTE (webAudioEditorUI.inspectorEmpty): This is the title for the + - AudioNode inspector view empty message. --> +<!ENTITY webAudioEditorUI.inspectorEmpty "No AudioNode selected."> + +<!-- LOCALIZATION NOTE (webAudioEditorUI.propertiesEmpty): This is the title for the + - AudioNode inspector view properties tab empty message. --> +<!ENTITY webAudioEditorUI.propertiesEmpty "Node does not have any properties."> + +<!-- LOCALIZATION NOTE (webAudioEditorUI.automationEmpty): This is the title for the + - AudioNode inspector view automation tab empty message. --> +<!ENTITY webAudioEditorUI.automationEmpty "Node does not have any AudioParams."> + +<!-- LOCALIZATION NOTE (webAudioEditorUI.automationNoEvents): This is the title for the + - AudioNode inspector view automation tab message when there are no automation + - events. --> +<!ENTITY webAudioEditorUI.automationNoEvents "AudioParam does not have any automation events.">
new file mode 100644 --- /dev/null +++ b/devtools/client/locales/en-US/webaudioeditor.properties @@ -0,0 +1,20 @@ +# 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 Web Audio tool +# which is available in the developer tools' toolbox, once +# enabled in the developer tools' preference "Web Audio". +# 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 (collapseInspector): This is the tooltip for the button +# that collapses the inspector in the web audio tool UI. +collapseInspector=Collapse inspector + +# LOCALIZATION NOTE (expandInspector): This is the tooltip for the button +# that expands the inspector in the web audio tool UI. +expandInspector=Expand inspector
--- a/devtools/client/moz.build +++ b/devtools/client/moz.build @@ -6,33 +6,36 @@ include('../templates.mozbuild') DIRS += [ 'aboutdebugging', 'aboutdebugging-new', 'accessibility', 'application', + 'canvasdebugger', 'debugger', 'dom', 'framework', 'inspector', 'jsonview', 'locales', 'memory', 'netmonitor', 'performance', 'performance-new', 'preferences', 'responsive.html', 'scratchpad', + 'shadereditor', 'shared', 'storage', 'styleeditor', 'themes', + 'webaudioeditor', 'webconsole', 'webide', 'webreplay', ] JAR_MANIFESTS += ['jar.mn'] DevToolsModules(
--- a/devtools/client/netmonitor/test/browser_net_accessibility-01.js +++ b/devtools/client/netmonitor/test/browser_net_accessibility-01.js @@ -1,15 +1,15 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; /** - * Tests if focus modifiers work for the Side Menu. + * Tests if focus modifiers work for the SideMenuWidget. */ add_task(async function() { const { tab, monitor } = await initNetMonitor(CUSTOM_GET_URL); info("Starting test... "); // It seems that this test may be slow on Ubuntu builds running on ec2. requestLongerTimeout(2);
--- a/devtools/client/preferences/devtools-client.js +++ b/devtools/client/preferences/devtools-client.js @@ -224,25 +224,37 @@ pref("devtools.styleeditor.showMediaSide pref("devtools.styleeditor.mediaSidebarWidth", 238); pref("devtools.styleeditor.navSidebarWidth", 245); pref("devtools.styleeditor.transitions", true); // Screenshot Option Settings. pref("devtools.screenshot.clipboard.enabled", false); pref("devtools.screenshot.audio.enabled", true); +// Enable the Shader Editor. +pref("devtools.shadereditor.enabled", false); + +// Enable the Canvas Debugger. +pref("devtools.canvasdebugger.enabled", false); + +// Enable the Web Audio Editor +pref("devtools.webaudioeditor.enabled", false); + // Enable Scratchpad pref("devtools.scratchpad.enabled", false); // Make sure the DOM panel is hidden by default pref("devtools.dom.enabled", false); // Enable the Accessibility panel. pref("devtools.accessibility.enabled", true); +// Web Audio Editor Inspector Width should be a preference +pref("devtools.webaudioeditor.inspectorWidth", 300); + // Web console filters pref("devtools.webconsole.filter.error", true); pref("devtools.webconsole.filter.warn", true); pref("devtools.webconsole.filter.info", true); pref("devtools.webconsole.filter.log", true); pref("devtools.webconsole.filter.debug", true); pref("devtools.webconsole.filter.css", false); pref("devtools.webconsole.filter.net", false);
new file mode 100644 --- /dev/null +++ b/devtools/client/shadereditor/index.xul @@ -0,0 +1,68 @@ +<?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://global/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://devtools/skin/widgets.css" type="text/css"?> +<?xml-stylesheet href="chrome://devtools/skin/shadereditor.css" type="text/css"?> +<?xml-stylesheet href="chrome://devtools/content/shared/widgets/widgets.css" type="text/css"?> +<!DOCTYPE window [ + <!ENTITY % debuggerDTD SYSTEM "chrome://devtools/locale/shadereditor.dtd"> + %debuggerDTD; +]> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://devtools/content/shared/theme-switching.js"/> + + <vbox class="theme-body" flex="1"> + <hbox id="reload-notice" + class="notice-container" + align="center" + pack="center" + flex="1"> + <button id="requests-menu-reload-notice-button" + class="devtools-toolbarbutton" + standalone="true" + label="&shaderEditorUI.reloadNotice1;"/> + <label id="requests-menu-reload-notice-label" + class="plain" + value="&shaderEditorUI.reloadNotice2;"/> + </hbox> + <hbox id="waiting-notice" + class="notice-container devtools-throbber" + align="center" + pack="center" + flex="1" + hidden="true"> + <label id="requests-menu-waiting-notice-label" + class="plain" + value="&shaderEditorUI.emptyNotice;"/> + </hbox> + + <box id="content" + class="devtools-responsive-container" + flex="1" + hidden="true"> + <vbox id="shaders-pane"/> + <splitter class="devtools-side-splitter"/> + <box id="shaders-editors" class="devtools-responsive-container" flex="1"> + <vbox flex="1"> + <vbox id="vs-editor" flex="1"/> + <label id="vs-editor-label" + class="plain editor-label" + value="&shaderEditorUI.vertexShader;"/> + </vbox> + <splitter id="editors-splitter" class="devtools-side-splitter"/> + <vbox flex="1"> + <vbox id="fs-editor" flex="1"/> + <label id="fs-editor-label" + class="plain editor-label" + value="&shaderEditorUI.fragmentShader;"/> + </vbox> + </box> + </box> + </vbox> + +</window>
new file mode 100644 --- /dev/null +++ b/devtools/client/shadereditor/moz.build @@ -0,0 +1,14 @@ +# 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/. + +DevToolsModules( + 'panel.js', + 'shadereditor.js' +) + +BROWSER_CHROME_MANIFESTS += ['test/browser.ini'] + +with Files('**'): + BUG_COMPONENT = ('DevTools', 'WebGL Shader Editor')
new file mode 100644 --- /dev/null +++ b/devtools/client/shadereditor/panel.js @@ -0,0 +1,71 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const EventEmitter = require("devtools/shared/event-emitter"); +const { EventsHandler, ShadersListView, ShadersEditorsView, EVENTS, $, L10N } = + require("./shadereditor"); + +function ShaderEditorPanel(toolbox) { + this._toolbox = toolbox; + this._destroyer = null; + this.panelWin = window; + + EventEmitter.decorate(this); +} + +exports.ShaderEditorPanel = ShaderEditorPanel; + +ShaderEditorPanel.prototype = { + + // Expose symbols for tests: + EVENTS, + $, + L10N, + + /** + * Open is effectively an asynchronous constructor. + * + * @return object + * A promise that is resolved when the Shader Editor completes opening. + */ + async open() { + this.front = await this.target.getFront("webgl"); + this.shadersListView = new ShadersListView(); + this.eventsHandler = new EventsHandler(); + this.shadersEditorsView = new ShadersEditorsView(); + await this.shadersListView.initialize(this._toolbox, this.shadersEditorsView); + await this.eventsHandler.initialize(this, this._toolbox, this.target, this.front, + this.shadersListView); + await this.shadersEditorsView.initialize(this, this.shadersListView); + + this.isReady = true; + this.emit("ready"); + return this; + }, + + // DevToolPanel API + + get target() { + return this._toolbox.target; + }, + + destroy() { + // Make sure this panel is not already destroyed. + if (this._destroyer) { + return this._destroyer; + } + + return (this._destroyer = (async () => { + await this.shadersListView.destroy(); + await this.eventsHandler.destroy(); + await this.shadersEditorsView.destroy(); + // Destroy front to ensure packet handler is removed from client + this.front.destroy(); + this.emit("destroyed"); + })()); + }, +};
new file mode 100644 --- /dev/null +++ b/devtools/client/shadereditor/shadereditor.js @@ -0,0 +1,625 @@ +/* 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 {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm"); +const {SideMenuWidget} = require("devtools/client/shared/widgets/SideMenuWidget.jsm"); +const promise = require("promise"); +const {Task} = require("devtools/shared/task"); +const EventEmitter = require("devtools/shared/event-emitter"); +const { HTMLTooltip } = require("devtools/client/shared/widgets/tooltip/HTMLTooltip"); +const Editor = require("devtools/client/shared/sourceeditor/editor"); +const {LocalizationHelper} = require("devtools/shared/l10n"); +const {extend} = require("devtools/shared/extend"); +const {WidgetMethods, setNamedTimeout} = + require("devtools/client/shared/widgets/view-helpers"); + +// The panel's window global is an EventEmitter firing the following events: +const EVENTS = { + // When new programs are received from the server. + NEW_PROGRAM: "ShaderEditor:NewProgram", + PROGRAMS_ADDED: "ShaderEditor:ProgramsAdded", + + // When the vertex and fragment sources were shown in the editor. + SOURCES_SHOWN: "ShaderEditor:SourcesShown", + + // When a shader's source was edited and compiled via the editor. + SHADER_COMPILED: "ShaderEditor:ShaderCompiled", + + // When the UI is reset from tab navigation + UI_RESET: "ShaderEditor:UIReset", + + // When the editor's error markers are all removed + EDITOR_ERROR_MARKERS_REMOVED: "ShaderEditor:EditorCleaned", +}; +exports.EVENTS = EVENTS; + +const XHTML_NS = "http://www.w3.org/1999/xhtml"; +const STRINGS_URI = "devtools/client/locales/shadereditor.properties"; +const HIGHLIGHT_TINT = [1, 0, 0.25, 1]; // rgba +const TYPING_MAX_DELAY = 500; // ms +const SHADERS_AUTOGROW_ITEMS = 4; +const GUTTER_ERROR_PANEL_OFFSET_X = 7; // px +const GUTTER_ERROR_PANEL_DELAY = 100; // ms +const DEFAULT_EDITOR_CONFIG = { + gutters: ["errors"], + lineNumbers: true, + showAnnotationRuler: true, +}; + +/** + * Functions handling target-related lifetime events. + */ +class EventsHandler { + /** + * Listen for events emitted by the current tab target. + */ + initialize(panel, toolbox, target, front, shadersListView) { + this.panel = panel; + this.toolbox = toolbox; + this.target = target; + this.front = front; + this.shadersListView = shadersListView; + + this._onHostChanged = this._onHostChanged.bind(this); + this._onTabNavigated = this._onTabNavigated.bind(this); + this._onTabWillNavigate = this._onTabWillNavigate.bind(this); + this._onProgramLinked = this._onProgramLinked.bind(this); + this._onProgramsAdded = this._onProgramsAdded.bind(this); + + this.toolbox.on("host-changed", this._onHostChanged); + this.target.on("will-navigate", this._onTabWillNavigate); + this.target.on("navigate", this._onTabNavigated); + this.front.on("program-linked", this._onProgramLinked); + this.reloadButton = $("#requests-menu-reload-notice-button"); + this._onReloadCommand = this._onReloadCommand.bind(this); + this.reloadButton.addEventListener("command", this._onReloadCommand); + } + + /** + * Remove events emitted by the current tab target. + */ + destroy() { + this.toolbox.off("host-changed", this._onHostChanged); + this.target.off("will-navigate", this._onTabWillNavigate); + this.target.off("navigate", this._onTabNavigated); + this.front.off("program-linked", this._onProgramLinked); + this.reloadButton.removeEventListener("command", this._onReloadCommand); + } + + /** + * Handles a command event on reload button + */ + _onReloadCommand() { + this.front.setup({ reload: true }); + } + + /** + * Handles a host change event on the parent toolbox. + */ + _onHostChanged() { + if (this.toolbox.hostType == "right" || this.toolbox.hostType == "left") { + $("#shaders-pane").removeAttribute("height"); + } + } + + _onTabWillNavigate({isFrameSwitching}) { + // Make sure the backend is prepared to handle WebGL contexts. + if (!isFrameSwitching) { + this.front.setup({ reload: false }); + } + + // Reset UI. + this.shadersListView.empty(); + // When switching to an iframe, ensure displaying the reload button. + // As the document has already been loaded without being hooked. + if (isFrameSwitching) { + $("#reload-notice").hidden = false; + $("#waiting-notice").hidden = true; + } else { + $("#reload-notice").hidden = true; + $("#waiting-notice").hidden = false; + } + + $("#content").hidden = true; + this.panel.emit(EVENTS.UI_RESET); + } + + /** + * Called for each location change in the debugged tab. + */ + _onTabNavigated() { + // Manually retrieve the list of program actors known to the server, + // because the backend won't emit "program-linked" notifications + // in the case of a bfcache navigation (since no new programs are + // actually linked). + this.front.getPrograms().then(this._onProgramsAdded); + } + + /** + * Called every time a program was linked in the debugged tab. + */ + _onProgramLinked(programActor) { + this._addProgram(programActor); + this.panel.emit(EVENTS.NEW_PROGRAM); + } + + /** + * Callback for the front's getPrograms() method. + */ + _onProgramsAdded(programActors) { + programActors.forEach(this._addProgram.bind(this)); + this.panel.emit(EVENTS.PROGRAMS_ADDED); + } + + /** + * Adds a program to the shaders list and unhides any modal notices. + */ + _addProgram(programActor) { + $("#waiting-notice").hidden = true; + $("#reload-notice").hidden = true; + $("#content").hidden = false; + this.shadersListView.addProgram(programActor); + } +} +exports.EventsHandler = EventsHandler; + +/** + * Functions handling the sources UI. + */ +function WidgetMethodsClass() { +} +WidgetMethodsClass.prototype = WidgetMethods; +class ShadersListView extends WidgetMethodsClass { + /** + * Initialization function, called when the tool is started. + */ + initialize(toolbox, shadersEditorsView) { + this.toolbox = toolbox; + this.shadersEditorsView = shadersEditorsView; + this.widget = new SideMenuWidget(this._pane = $("#shaders-pane"), { + showArrows: true, + showItemCheckboxes: true, + }); + + this._onProgramSelect = this._onProgramSelect.bind(this); + this._onProgramCheck = this._onProgramCheck.bind(this); + this._onProgramMouseOver = this._onProgramMouseOver.bind(this); + this._onProgramMouseOut = this._onProgramMouseOut.bind(this); + + this.widget.addEventListener("select", this._onProgramSelect); + this.widget.addEventListener("check", this._onProgramCheck); + this.widget.addEventListener("mouseover", this._onProgramMouseOver, true); + this.widget.addEventListener("mouseout", this._onProgramMouseOut, true); + } + + /** + * Destruction function, called when the tool is closed. + */ + destroy() { + this.widget.removeEventListener("select", this._onProgramSelect); + this.widget.removeEventListener("check", this._onProgramCheck); + this.widget.removeEventListener("mouseover", this._onProgramMouseOver, true); + this.widget.removeEventListener("mouseout", this._onProgramMouseOut, true); + } + + /** + * Adds a program to this programs container. + * + * @param object programActor + * The program actor coming from the active thread. + */ + addProgram(programActor) { + if (this.hasProgram(programActor)) { + return; + } + + // Currently, there's no good way of differentiating between programs + // in a way that helps humans. It will be a good idea to implement a + // standard of allowing debuggees to add some identifiable metadata to their + // program sources or instances. + const label = L10N.getFormatStr("shadersList.programLabel", this.itemCount); + const contents = document.createElement("label"); + contents.className = "plain program-item"; + contents.setAttribute("value", label); + contents.setAttribute("crop", "start"); + contents.setAttribute("flex", "1"); + + // Append a program item to this container. + this.push([contents], { + index: -1, /* specifies on which position should the item be appended */ + attachment: { + label: label, + programActor: programActor, + checkboxState: true, + checkboxTooltip: L10N.getStr("shadersList.blackboxLabel"), + }, + }); + + // Make sure there's always a selected item available. + if (!this.selectedItem) { + this.selectedIndex = 0; + } + + // Prevent this container from growing indefinitely in height when the + // toolbox is docked to the side. + if ((this.toolbox.hostType == "left" || this.toolbox.hostType == "right") && + this.itemCount == SHADERS_AUTOGROW_ITEMS) { + this._pane.setAttribute("height", this._pane.getBoundingClientRect().height); + } + } + + /** + * Returns whether a program was already added to this programs container. + * + * @param object programActor + * The program actor coming from the active thread. + * @param boolean + * True if the program was added, false otherwise. + */ + hasProgram(programActor) { + return !!this.attachments.filter(e => e.programActor == programActor).length; + } + + /** + * The select listener for the programs container. + */ + _onProgramSelect({ detail: sourceItem }) { + if (!sourceItem) { + return; + } + // The container is not empty and an actual item was selected. + const attachment = sourceItem.attachment; + + function getShaders() { + return promise.all([ + attachment.vs || (attachment.vs = attachment.programActor.getVertexShader()), + attachment.fs || (attachment.fs = attachment.programActor.getFragmentShader()), + ]); + } + function getSources([vertexShaderActor, fragmentShaderActor]) { + return promise.all([ + vertexShaderActor.getText(), + fragmentShaderActor.getText(), + ]); + } + const showSources = ([vertexShaderText, fragmentShaderText]) => { + return this.shadersEditorsView.setText({ + vs: vertexShaderText, + fs: fragmentShaderText, + }); + }; + + getShaders() + .then(getSources) + .then(showSources) + .catch(console.error); + } + + /** + * The check listener for the programs container. + */ + _onProgramCheck({ detail: { checked }, target }) { + const sourceItem = this.getItemForElement(target); + const attachment = sourceItem.attachment; + attachment.isBlackBoxed = !checked; + attachment.programActor[checked ? "unblackbox" : "blackbox"](); + } + + /** + * The mouseover listener for the programs container. + */ + _onProgramMouseOver(e) { + const sourceItem = this.getItemForElement(e.target, { noSiblings: true }); + if (sourceItem && !sourceItem.attachment.isBlackBoxed) { + sourceItem.attachment.programActor.highlight(HIGHLIGHT_TINT); + + if (e instanceof Event) { + e.preventDefault(); + e.stopPropagation(); + } + } + } + + /** + * The mouseout listener for the programs container. + */ + _onProgramMouseOut(e) { + const sourceItem = this.getItemForElement(e.target, { noSiblings: true }); + if (sourceItem && !sourceItem.attachment.isBlackBoxed) { + sourceItem.attachment.programActor.unhighlight(); + + if (e instanceof Event) { + e.preventDefault(); + e.stopPropagation(); + } + } + } +} +exports.ShadersListView = ShadersListView; + +/** + * Functions handling the editors displaying the vertex and fragment shaders. + */ +class ShadersEditorsView { + /** + * Initialization function, called when the tool is started. + */ + initialize(panel, shadersListView) { + this.panel = panel; + this.shadersListView = shadersListView; + XPCOMUtils.defineLazyGetter(this, "_editorPromises", () => new Map()); + this._vsFocused = this._onFocused.bind(this, "vs", "fs"); + this._fsFocused = this._onFocused.bind(this, "fs", "vs"); + this._vsChanged = this._onChanged.bind(this, "vs"); + this._fsChanged = this._onChanged.bind(this, "fs"); + + this._errors = { + vs: [], + fs: [], + }; + } + + /** + * Destruction function, called when the tool is closed. + */ + async destroy() { + this._destroyed = true; + await this._toggleListeners("off"); + for (const p of this._editorPromises.values()) { + const editor = await p; + editor.destroy(); + } + } + + /** + * Sets the text displayed in the vertex and fragment shader editors. + * + * @param object sources + * An object containing the following properties + * - vs: the vertex shader source code + * - fs: the fragment shader source code + * @return object + * A promise resolving upon completion of text setting. + */ + setText(sources) { + const view = this; + function setTextAndClearHistory(editor, text) { + editor.setText(text); + editor.clearHistory(); + } + + return (async function() { + await view._toggleListeners("off"); + await promise.all([ + view._getEditor("vs").then(e => setTextAndClearHistory(e, sources.vs)), + view._getEditor("fs").then(e => setTextAndClearHistory(e, sources.fs)), + ]); + await view._toggleListeners("on"); + })().then(() => this.panel.emit(EVENTS.SOURCES_SHOWN, sources)); + } + + /** + * Lazily initializes and returns a promise for an Editor instance. + * + * @param string type + * Specifies for which shader type should an editor be retrieved, + * either are "vs" for a vertex, or "fs" for a fragment shader. + * @return object + * Returns a promise that resolves to an editor instance + */ + _getEditor(type) { + if (this._editorPromises.has(type)) { + return this._editorPromises.get(type); + } + + const promise = new Promise(resolve =>{ + // Initialize the source editor and store the newly created instance + // in the ether of a resolved promise's value. + const parent = $("#" + type + "-editor"); + const editor = new Editor(DEFAULT_EDITOR_CONFIG); + editor.config.mode = Editor.modes[type]; + + if (this._destroyed) { + resolve(editor); + } else { + editor.appendTo(parent).then(() => resolve(editor)); + } + }); + this._editorPromises.set(type, promise); + return promise; + } + + /** + * Toggles all the event listeners for the editors either on or off. + * + * @param string flag + * Either "on" to enable the event listeners, "off" to disable them. + * @return object + * A promise resolving upon completion of toggling the listeners. + */ + _toggleListeners(flag) { + return promise.all(["vs", "fs"].map(type => { + return this._getEditor(type).then(editor => { + editor[flag]("focus", this["_" + type + "Focused"]); + editor[flag]("change", this["_" + type + "Changed"]); + }); + })); + } + + /** + * The focus listener for a source editor. + * + * @param string focused + * The corresponding shader type for the focused editor (e.g. "vs"). + * @param string focused + * The corresponding shader type for the other editor (e.g. "fs"). + */ + _onFocused(focused, unfocused) { + $("#" + focused + "-editor-label").setAttribute("selected", ""); + $("#" + unfocused + "-editor-label").removeAttribute("selected"); + } + + /** + * The change listener for a source editor. + * + * @param string type + * The corresponding shader type for the focused editor (e.g. "vs"). + */ + _onChanged(type) { + setNamedTimeout("gl-typed", TYPING_MAX_DELAY, () => this._doCompile(type)); + + // Remove all the gutter markers and line classes from the editor. + this._cleanEditor(type); + } + + /** + * Recompiles the source code for the shader being edited. + * This function is fired at a certain delay after the user stops typing. + * + * @param string type + * The corresponding shader type for the focused editor (e.g. "vs"). + */ + _doCompile(type) { + (async function() { + const editor = await this._getEditor(type); + const shaderActor = await this.shadersListView.selectedAttachment[type]; + + try { + await shaderActor.compile(editor.getText()); + this._onSuccessfulCompilation(); + } catch (e) { + this._onFailedCompilation(type, editor, e); + } + }.bind(this))(); + } + + /** + * Called uppon a successful shader compilation. + */ + _onSuccessfulCompilation() { + // Signal that the shader was compiled successfully. + this.panel.emit(EVENTS.SHADER_COMPILED, null); + } + + /** + * Called uppon an unsuccessful shader compilation. + */ + _onFailedCompilation(type, editor, errors) { + const lineCount = editor.lineCount(); + const currentLine = editor.getCursor().line; + const listeners = { mouseover: this._onMarkerMouseOver }; + + function matchLinesAndMessages(string) { + return { + // First number that is not equal to 0. + lineMatch: string.match(/\d{2,}|[1-9]/), + // The string after all the numbers, semicolons and spaces. + textMatch: string.match(/[^\s\d:][^\r\n|]*/), + }; + } + function discardInvalidMatches(e) { + // Discard empty line and text matches. + return e.lineMatch && e.textMatch; + } + function sanitizeValidMatches(e) { + return { + // Drivers might yield confusing line numbers under some obscure + // circumstances. Don't throw the errors away in those cases, + // just display them on the currently edited line. + line: e.lineMatch[0] > lineCount ? currentLine : e.lineMatch[0] - 1, + // Trim whitespace from the beginning and the end of the message, + // and replace all other occurences of double spaces to a single space. + text: e.textMatch[0].trim().replace(/\s{2,}/g, " "), + }; + } + function sortByLine(first, second) { + // Sort all the errors ascending by their corresponding line number. + return first.line > second.line ? 1 : -1; + } + function groupSameLineMessages(accumulator, current) { + // Group errors corresponding to the same line number to a single object. + const previous = accumulator[accumulator.length - 1]; + if (!previous || previous.line != current.line) { + return [...accumulator, { + line: current.line, + messages: [current.text], + }]; + } + previous.messages.push(current.text); + return accumulator; + } + function displayErrors({ line, messages }) { + // Add gutter markers and line classes for every error in the source. + editor.addMarker(line, "errors", "error"); + editor.setMarkerListeners(line, "errors", "error", listeners, messages); + editor.addLineClass(line, "error-line"); + } + + (this._errors[type] = errors.link + .split("ERROR") + .map(matchLinesAndMessages) + .filter(discardInvalidMatches) + .map(sanitizeValidMatches) + .sort(sortByLine) + .reduce(groupSameLineMessages, [])) + .forEach(displayErrors); + + // Signal that the shader wasn't compiled successfully. + this.panel.emit(EVENTS.SHADER_COMPILED, errors); + } + + /** + * Event listener for the 'mouseover' event on a marker in the editor gutter. + */ + _onMarkerMouseOver(line, node, messages) { + if (node._markerErrorsTooltip) { + return; + } + + const tooltip = node._markerErrorsTooltip = new HTMLTooltip(document, { + type: "arrow", + useXulWrapper: true, + }); + + const div = document.createElementNS(XHTML_NS, "div"); + div.className = "devtools-shader-tooltip-container"; + for (const message of messages) { + const messageDiv = document.createElementNS(XHTML_NS, "div"); + messageDiv.className = "devtools-tooltip-simple-text"; + messageDiv.textContent = message; + div.appendChild(messageDiv); + } + tooltip.panel.appendChild(div); + + tooltip.startTogglingOnHover(node, () => true, { + toggleDelay: GUTTER_ERROR_PANEL_DELAY, + }); + } + + /** + * Removes all the gutter markers and line classes from the editor. + */ + _cleanEditor(type) { + this._getEditor(type).then(editor => { + editor.removeAllMarkers("errors"); + this._errors[type].forEach(e => editor.removeLineClass(e.line)); + this._errors[type].length = 0; + this.panel.emit(EVENTS.EDITOR_ERROR_MARKERS_REMOVED); + }); + } +} +exports.ShadersEditorsView = ShadersEditorsView; + +/** + * Localization convenience methods. + */ +var L10N = new LocalizationHelper(STRINGS_URI); +exports.L10N = L10N; + +/** + * DOM query helper. + */ +var $ = (selector, target = document) => target.querySelector(selector); +exports.$ = $;
new file mode 100644 --- /dev/null +++ b/devtools/client/shadereditor/test/.eslintrc.js @@ -0,0 +1,6 @@ +"use strict"; + +module.exports = { + // Extend from the shared list of defined globals for mochitests. + "extends": "../../../.eslintrc.mochitests.js" +};
new file mode 100644 --- /dev/null +++ b/devtools/client/shadereditor/test/browser.ini @@ -0,0 +1,59 @@ +[DEFAULT] +tags = devtools +subsuite = devtools +support-files = + doc_blended-geometry.html + doc_multiple-contexts.html + doc_overlapping-geometry.html + doc_shader-order.html + doc_simple-canvas.html + head.js + !/devtools/client/shared/test/frame-script-utils.js + !/devtools/client/shared/test/shared-head.js + !/devtools/client/shared/test/telemetry-test-helpers.js + +[browser_se_aaa_run_first_leaktest.js] +[browser_se_bfcache.js] +skip-if = true # Bug 942473, caused by Bug 940541 +[browser_se_editors-contents.js] +skip-if = (verify && (os == 'win')) +[browser_se_editors-error-gutter.js] +skip-if = (verify && !debug && (os == 'win')) +[browser_se_editors-error-tooltip.js] +skip-if = (verify && (os == 'win' || os == 'linux')) +[browser_se_editors-lazy-init.js] +[browser_se_first-run.js] +[browser_se_navigation.js] +[browser_se_programs-blackbox-01.js] +[browser_se_programs-blackbox-02.js] +[browser_se_programs-cache.js] +[browser_se_programs-highlight-01.js] +skip-if = (verify && debug && (os == 'win')) +[browser_se_programs-highlight-02.js] +[browser_se_programs-list.js] +[browser_se_shaders-edit-01.js] +[browser_se_shaders-edit-02.js] +[browser_se_shaders-edit-03.js] +skip-if = (verify && (os == 'win')) +[browser_webgl-actor-test-01.js] +[browser_webgl-actor-test-02.js] +[browser_webgl-actor-test-03.js] +[browser_webgl-actor-test-04.js] +[browser_webgl-actor-test-05.js] +skip-if = (verify && !debug && (os == 'linux')) +[browser_webgl-actor-test-06.js] +skip-if = (verify && (os == 'linux')) +[browser_webgl-actor-test-07.js] +[browser_webgl-actor-test-08.js] +skip-if = (verify && debug && (os == 'win')) +[browser_webgl-actor-test-09.js] +[browser_webgl-actor-test-10.js] +[browser_webgl-actor-test-11.js] +[browser_webgl-actor-test-12.js] +[browser_webgl-actor-test-13.js] +[browser_webgl-actor-test-14.js] +[browser_webgl-actor-test-15.js] +[browser_webgl-actor-test-16.js] +[browser_webgl-actor-test-17.js] +skip-if = (verify && debug && (os == 'win')) +[browser_webgl-actor-test-18.js]
new file mode 100644 --- /dev/null +++ b/devtools/client/shadereditor/test/browser_se_aaa_run_first_leaktest.js @@ -0,0 +1,17 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if the shader editor leaks on initialization and sudden destruction. + * You can also use this initialization format as a template for other tests. + */ + +async function ifWebGLSupported() { + const { target, panel } = await initShaderEditor(SIMPLE_CANVAS_URL); + + ok(target, "Should have a target available."); + ok(panel, "Should have a panel available."); + + await teardown(panel); + finish(); +}
new file mode 100644 --- /dev/null +++ b/devtools/client/shadereditor/test/browser_se_bfcache.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if the shader editor works with bfcache. + */ +async function ifWebGLSupported() { + const { target, panel } = await initShaderEditor(SIMPLE_CANVAS_URL); + const { front, $, EVENTS, shadersListView, shadersEditorsView } = panel; + + // Attach frame scripts if in e10s to perform + // history navigation via the content + loadFrameScripts(); + + const reloaded = reload(target); + const firstProgram = await once(front, "program-linked"); + await reloaded; + + const navigated = navigate(target, MULTIPLE_CONTEXTS_URL); + const [secondProgram, thirdProgram] = await getPrograms(front, 2); + await navigated; + + const vsEditor = await shadersEditorsView._getEditor("vs"); + const fsEditor = await shadersEditorsView._getEditor("fs"); + + await navigateInHistory(target, "back", "will-navigate"); + await once(panel, EVENTS.PROGRAMS_ADDED); + await once(panel, EVENTS.SOURCES_SHOWN); + + is($("#content").hidden, false, + "The tool's content should not be hidden."); + is(shadersListView.itemCount, 1, + "The shaders list contains one entry after navigating back."); + is(shadersListView.selectedIndex, 0, + "The shaders list has a correct selection after navigating back."); + + is(vsEditor.getText().indexOf("gl_Position"), 170, + "The vertex shader editor contains the correct text."); + is(fsEditor.getText().indexOf("gl_FragColor"), 97, + "The fragment shader editor contains the correct text."); + + await navigateInHistory(target, "forward", "will-navigate"); + await once(panel, EVENTS.PROGRAMS_ADDED); + await once(panel, EVENTS.SOURCES_SHOWN); + + is($("#content").hidden, false, + "The tool's content should not be hidden."); + is(shadersListView.itemCount, 2, + "The shaders list contains two entries after navigating forward."); + is(shadersListView.selectedIndex, 0, + "The shaders list has a correct selection after navigating forward."); + + is(vsEditor.getText().indexOf("gl_Position"), 100, + "The vertex shader editor contains the correct text."); + is(fsEditor.getText().indexOf("gl_FragColor"), 89, + "The fragment shader editor contains the correct text."); + + await teardown(panel); + finish(); +}
new file mode 100644 --- /dev/null +++ b/devtools/client/shadereditor/test/browser_se_editors-contents.js @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if the editors contain the correct text when a program + * becomes available. + */ + +async function ifWebGLSupported() { + const { target, panel } = await initShaderEditor(SIMPLE_CANVAS_URL); + const { front, shadersEditorsView, EVENTS } = panel; + + reload(target); + await promise.all([ + once(front, "program-linked"),