michael@0: /* Any copyright is dedicated to the Public Domain. michael@0: http://creativecommons.org/publicdomain/zero/1.0/ */ michael@0: "use strict"; michael@0: michael@0: const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; michael@0: michael@0: let { Services } = Cu.import("resource://gre/modules/Services.jsm", {}); michael@0: michael@0: let gEnableLogging = Services.prefs.getBoolPref("devtools.debugger.log"); michael@0: // To enable logging for try runs, just set the pref to true. michael@0: Services.prefs.setBoolPref("devtools.debugger.log", false); michael@0: michael@0: let { Task } = Cu.import("resource://gre/modules/Task.jsm", {}); michael@0: let { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {}); michael@0: let { gDevTools } = Cu.import("resource:///modules/devtools/gDevTools.jsm", {}); michael@0: let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); michael@0: let { DebuggerServer } = Cu.import("resource://gre/modules/devtools/dbg-server.jsm", {}); michael@0: let { DebuggerClient } = Cu.import("resource://gre/modules/devtools/dbg-client.jsm", {}); michael@0: michael@0: let { WebGLFront } = devtools.require("devtools/server/actors/webgl"); michael@0: let TiltGL = devtools.require("devtools/tilt/tilt-gl"); michael@0: let TargetFactory = devtools.TargetFactory; michael@0: let Toolbox = devtools.Toolbox; michael@0: michael@0: const EXAMPLE_URL = "http://example.com/browser/browser/devtools/shadereditor/test/"; michael@0: const SIMPLE_CANVAS_URL = EXAMPLE_URL + "doc_simple-canvas.html"; michael@0: const SHADER_ORDER_URL = EXAMPLE_URL + "doc_shader-order.html"; michael@0: const MULTIPLE_CONTEXTS_URL = EXAMPLE_URL + "doc_multiple-contexts.html"; michael@0: const OVERLAPPING_GEOMETRY_CANVAS_URL = EXAMPLE_URL + "doc_overlapping-geometry.html"; michael@0: const BLENDED_GEOMETRY_CANVAS_URL = EXAMPLE_URL + "doc_blended-geometry.html"; michael@0: michael@0: // All tests are asynchronous. michael@0: waitForExplicitFinish(); michael@0: michael@0: let gToolEnabled = Services.prefs.getBoolPref("devtools.shadereditor.enabled"); michael@0: michael@0: registerCleanupFunction(() => { michael@0: info("finish() was called, cleaning up..."); michael@0: Services.prefs.setBoolPref("devtools.debugger.log", gEnableLogging); michael@0: Services.prefs.setBoolPref("devtools.shadereditor.enabled", gToolEnabled); michael@0: michael@0: // These tests use a lot of memory due to GL contexts, so force a GC to help michael@0: // fragmentation. michael@0: info("Forcing GC after shadereditor test."); michael@0: Cu.forceGC(); michael@0: }); michael@0: michael@0: function addTab(aUrl, aWindow) { michael@0: info("Adding tab: " + aUrl); michael@0: michael@0: let deferred = promise.defer(); michael@0: let targetWindow = aWindow || window; michael@0: let targetBrowser = targetWindow.gBrowser; michael@0: michael@0: targetWindow.focus(); michael@0: let tab = targetBrowser.selectedTab = targetBrowser.addTab(aUrl); michael@0: let linkedBrowser = tab.linkedBrowser; michael@0: michael@0: linkedBrowser.addEventListener("load", function onLoad() { michael@0: linkedBrowser.removeEventListener("load", onLoad, true); michael@0: info("Tab added and finished loading: " + aUrl); michael@0: deferred.resolve(tab); michael@0: }, true); michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: function removeTab(aTab, aWindow) { michael@0: info("Removing tab."); michael@0: michael@0: let deferred = promise.defer(); michael@0: let targetWindow = aWindow || window; michael@0: let targetBrowser = targetWindow.gBrowser; michael@0: let tabContainer = targetBrowser.tabContainer; michael@0: michael@0: tabContainer.addEventListener("TabClose", function onClose(aEvent) { michael@0: tabContainer.removeEventListener("TabClose", onClose, false); michael@0: info("Tab removed and finished closing."); michael@0: deferred.resolve(); michael@0: }, false); michael@0: michael@0: targetBrowser.removeTab(aTab); michael@0: return deferred.promise; michael@0: } michael@0: michael@0: function handleError(aError) { michael@0: ok(false, "Got an error: " + aError.message + "\n" + aError.stack); michael@0: finish(); michael@0: } michael@0: michael@0: function ifWebGLSupported() { michael@0: ok(false, "You need to define a 'ifWebGLSupported' function."); michael@0: finish(); michael@0: } michael@0: michael@0: function ifWebGLUnsupported() { michael@0: todo(false, "Skipping test because WebGL isn't supported."); michael@0: finish(); michael@0: } michael@0: michael@0: function test() { michael@0: let generator = isWebGLSupported() ? ifWebGLSupported : ifWebGLUnsupported; michael@0: Task.spawn(generator).then(null, handleError); michael@0: } michael@0: michael@0: function createCanvas() { michael@0: return document.createElementNS("http://www.w3.org/1999/xhtml", "canvas"); michael@0: } michael@0: michael@0: function isWebGLSupported() { michael@0: let supported = michael@0: !TiltGL.isWebGLForceEnabled() && michael@0: TiltGL.isWebGLSupported() && michael@0: TiltGL.create3DContext(createCanvas()); michael@0: michael@0: info("Apparently, WebGL is" + (supported ? "" : " not") + " supported."); michael@0: return supported; michael@0: } michael@0: michael@0: function once(aTarget, aEventName, aUseCapture = false) { michael@0: info("Waiting for event: '" + aEventName + "' on " + aTarget + "."); michael@0: michael@0: let deferred = promise.defer(); michael@0: michael@0: for (let [add, remove] of [ michael@0: ["on", "off"], // Use event emitter before DOM events for consistency michael@0: ["addEventListener", "removeEventListener"], michael@0: ["addListener", "removeListener"] michael@0: ]) { michael@0: if ((add in aTarget) && (remove in aTarget)) { michael@0: aTarget[add](aEventName, function onEvent(...aArgs) { michael@0: aTarget[remove](aEventName, onEvent, aUseCapture); michael@0: deferred.resolve(...aArgs); michael@0: }, aUseCapture); michael@0: break; michael@0: } michael@0: } michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: // Hack around `once`, as that only resolves to a single (first) argument michael@0: // and discards the rest. `onceSpread` is similar, except resolves to an michael@0: // array of all of the arguments in the handler. These should be consolidated michael@0: // into the same function, but many tests will need to be changed. michael@0: function onceSpread(aTarget, aEvent) { michael@0: let deferred = promise.defer(); michael@0: aTarget.once(aEvent, (...args) => deferred.resolve(args)); michael@0: return deferred.promise; michael@0: } michael@0: michael@0: function observe(aNotificationName, aOwnsWeak = false) { michael@0: info("Waiting for observer notification: '" + aNotificationName + "."); michael@0: michael@0: let deferred = promise.defer(); michael@0: michael@0: Services.obs.addObserver(function onNotification(...aArgs) { michael@0: Services.obs.removeObserver(onNotification, aNotificationName); michael@0: deferred.resolve.apply(deferred, aArgs); michael@0: }, aNotificationName, aOwnsWeak); michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: function waitForFrame(aDebuggee) { michael@0: let deferred = promise.defer(); michael@0: aDebuggee.requestAnimationFrame(deferred.resolve); michael@0: return deferred.promise; michael@0: } michael@0: michael@0: function isApprox(aFirst, aSecond, aMargin = 1) { michael@0: return Math.abs(aFirst - aSecond) <= aMargin; michael@0: } michael@0: michael@0: function isApproxColor(aFirst, aSecond, aMargin) { michael@0: return isApprox(aFirst.r, aSecond.r, aMargin) && michael@0: isApprox(aFirst.g, aSecond.g, aMargin) && michael@0: isApprox(aFirst.b, aSecond.b, aMargin) && michael@0: isApprox(aFirst.a, aSecond.a, aMargin); michael@0: } michael@0: michael@0: function getPixels(aDebuggee, aSelector = "canvas") { michael@0: let canvas = aDebuggee.document.querySelector(aSelector); michael@0: let gl = canvas.getContext("webgl"); michael@0: michael@0: let { width, height } = canvas; michael@0: let buffer = new aDebuggee.Uint8Array(width * height * 4); michael@0: gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, buffer); michael@0: michael@0: info("Retrieved pixels: " + width + "x" + height); michael@0: return [buffer, width, height]; michael@0: } michael@0: michael@0: function getPixel(aDebuggee, aPosition, aSelector = "canvas") { michael@0: let canvas = aDebuggee.document.querySelector(aSelector); michael@0: let gl = canvas.getContext("webgl"); michael@0: michael@0: let { width, height } = canvas; michael@0: let { x, y } = aPosition; michael@0: let buffer = new aDebuggee.Uint8Array(4); michael@0: gl.readPixels(x, height - y - 1, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, buffer); michael@0: michael@0: let pixel = { r: buffer[0], g: buffer[1], b: buffer[2], a: buffer[3] }; michael@0: michael@0: info("Retrieved pixel: " + pixel.toSource() + " at " + aPosition.toSource()); michael@0: return pixel; michael@0: } michael@0: michael@0: function ensurePixelIs(aDebuggee, aPosition, aColor, aWaitFlag = false, aSelector = "canvas") { michael@0: let pixel = getPixel(aDebuggee, aPosition, aSelector); michael@0: if (isApproxColor(pixel, aColor)) { michael@0: ok(true, "Expected pixel is shown at: " + aPosition.toSource()); michael@0: return promise.resolve(null); michael@0: } michael@0: if (aWaitFlag) { michael@0: return Task.spawn(function() { michael@0: yield waitForFrame(aDebuggee); michael@0: yield ensurePixelIs(aDebuggee, aPosition, aColor, aWaitFlag, aSelector); michael@0: }); michael@0: } michael@0: ok(false, "Expected pixel was not already shown at: " + aPosition.toSource()); michael@0: return promise.reject(null); michael@0: } michael@0: michael@0: function navigateInHistory(aTarget, aDirection, aWaitForTargetEvent = "navigate") { michael@0: executeSoon(() => content.history[aDirection]()); michael@0: return once(aTarget, aWaitForTargetEvent); michael@0: } michael@0: michael@0: function navigate(aTarget, aUrl, aWaitForTargetEvent = "navigate") { michael@0: executeSoon(() => aTarget.activeTab.navigateTo(aUrl)); michael@0: return once(aTarget, aWaitForTargetEvent); michael@0: } michael@0: michael@0: function reload(aTarget, aWaitForTargetEvent = "navigate") { michael@0: executeSoon(() => aTarget.activeTab.reload()); michael@0: return once(aTarget, aWaitForTargetEvent); michael@0: } michael@0: michael@0: function initBackend(aUrl) { michael@0: info("Initializing a shader editor front."); michael@0: michael@0: if (!DebuggerServer.initialized) { michael@0: DebuggerServer.init(() => true); michael@0: DebuggerServer.addBrowserActors(); michael@0: } michael@0: michael@0: return Task.spawn(function*() { michael@0: let tab = yield addTab(aUrl); michael@0: let target = TargetFactory.forTab(tab); michael@0: let debuggee = target.window.wrappedJSObject; michael@0: michael@0: yield target.makeRemote(); michael@0: michael@0: let front = new WebGLFront(target.client, target.form); michael@0: return [target, debuggee, front]; michael@0: }); michael@0: } michael@0: michael@0: function initShaderEditor(aUrl) { michael@0: info("Initializing a shader editor pane."); michael@0: michael@0: return Task.spawn(function*() { michael@0: let tab = yield addTab(aUrl); michael@0: let target = TargetFactory.forTab(tab); michael@0: let debuggee = target.window.wrappedJSObject; michael@0: michael@0: yield target.makeRemote(); michael@0: michael@0: Services.prefs.setBoolPref("devtools.shadereditor.enabled", true); michael@0: let toolbox = yield gDevTools.showToolbox(target, "shadereditor"); michael@0: let panel = toolbox.getCurrentPanel(); michael@0: return [target, debuggee, panel]; michael@0: }); michael@0: } michael@0: michael@0: function teardown(aPanel) { michael@0: info("Destroying the specified shader editor."); michael@0: michael@0: return promise.all([ michael@0: once(aPanel, "destroyed"), michael@0: removeTab(aPanel.target.tab) michael@0: ]); michael@0: } michael@0: michael@0: // Due to `program-linked` events firing synchronously, we cannot michael@0: // just yield/chain them together, as then we miss all actors after the michael@0: // first event since they're fired consecutively. This allows us to capture michael@0: // all actors and returns an array containing them. michael@0: // michael@0: // Takes a `front` object that is an event emitter, the number of michael@0: // programs that should be listened to and waited on, and an optional michael@0: // `onAdd` function that calls with the entire actors array on program link michael@0: function getPrograms(front, count, onAdd) { michael@0: let actors = []; michael@0: let deferred = promise.defer(); michael@0: front.on("program-linked", function onLink (actor) { michael@0: if (actors.length !== count) { michael@0: actors.push(actor); michael@0: if (typeof onAdd === 'function') onAdd(actors) michael@0: } michael@0: if (actors.length === count) { michael@0: front.off("program-linked", onLink); michael@0: deferred.resolve(actors); michael@0: } michael@0: }); michael@0: return deferred.promise; michael@0: }