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: // Enable logging for all the tests. Both the debugger server and frontend will michael@0: // be affected by this pref. michael@0: let gEnableLogging = Services.prefs.getBoolPref("devtools.debugger.log"); michael@0: Services.prefs.setBoolPref("devtools.debugger.log", true); michael@0: michael@0: let { Task } = Cu.import("resource://gre/modules/Task.jsm", {}); michael@0: let { 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: michael@0: let { WebAudioFront } = devtools.require("devtools/server/actors/webaudio"); michael@0: let TargetFactory = devtools.TargetFactory; michael@0: michael@0: const EXAMPLE_URL = "http://example.com/browser/browser/devtools/webaudioeditor/test/"; michael@0: const SIMPLE_CONTEXT_URL = EXAMPLE_URL + "doc_simple-context.html"; michael@0: const COMPLEX_CONTEXT_URL = EXAMPLE_URL + "doc_complex-context.html"; michael@0: const SIMPLE_NODES_URL = EXAMPLE_URL + "doc_simple-node-creation.html"; michael@0: michael@0: // All tests are asynchronous. michael@0: waitForExplicitFinish(); michael@0: michael@0: let gToolEnabled = Services.prefs.getBoolPref("devtools.webaudioeditor.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.webaudioeditor.enabled", gToolEnabled); 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 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: function reload(aTarget, aWaitForTargetEvent = "navigate") { michael@0: aTarget.activeTab.reload(); michael@0: return once(aTarget, aWaitForTargetEvent); michael@0: } michael@0: michael@0: function test () { michael@0: Task.spawn(spawnTest).then(finish, handleError); michael@0: } michael@0: michael@0: function initBackend(aUrl) { michael@0: info("Initializing a web audio 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 WebAudioFront(target.client, target.form); michael@0: return [target, debuggee, front]; michael@0: }); michael@0: } michael@0: michael@0: function initWebAudioEditor(aUrl) { michael@0: info("Initializing a web audio 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.webaudioeditor.enabled", true); michael@0: let toolbox = yield gDevTools.showToolbox(target, "webaudioeditor"); 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 web audio editor."); michael@0: michael@0: return Promise.all([ michael@0: once(aPanel, "destroyed"), michael@0: removeTab(aPanel.target.tab) michael@0: ]).then(() => { michael@0: let gBrowser = window.gBrowser; michael@0: while (gBrowser.tabs.length > 1) { michael@0: gBrowser.removeCurrentTab(); michael@0: } michael@0: gBrowser = null; michael@0: }); michael@0: } michael@0: michael@0: // Due to web audio will fire most events synchronously back-to-back, michael@0: // and we can't yield them in a chain without missing actors, this allows michael@0: // us to listen for `n` events and return a promise resolving to 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 getN (front, eventName, count, spread) { michael@0: let actors = []; michael@0: let deferred = Promise.defer(); michael@0: front.on(eventName, function onEvent (...args) { michael@0: let actor = args[0]; michael@0: if (actors.length !== count) { michael@0: actors.push(spread ? args : actor); michael@0: } michael@0: if (actors.length === count) { michael@0: front.off(eventName, onEvent); michael@0: deferred.resolve(actors); michael@0: } michael@0: }); michael@0: return deferred.promise; michael@0: } michael@0: michael@0: function get (front, eventName) { return getN(front, eventName, 1); } michael@0: function get2 (front, eventName) { return getN(front, eventName, 2); } michael@0: function get3 (front, eventName) { return getN(front, eventName, 3); } michael@0: function getSpread (front, eventName) { return getN(front, eventName, 1, true); } michael@0: function get2Spread (front, eventName) { return getN(front, eventName, 2, true); } michael@0: function get3Spread (front, eventName) { return getN(front, eventName, 3, true); } michael@0: function getNSpread (front, eventName, count) { return getN(front, eventName, count, true); } michael@0: michael@0: /** michael@0: * Waits for the UI_GRAPH_RENDERED event to fire, but only michael@0: * resolves when the graph was rendered with the correct count of michael@0: * nodes and edges. michael@0: */ michael@0: function waitForGraphRendered (front, nodeCount, edgeCount) { michael@0: let deferred = Promise.defer(); michael@0: let eventName = front.EVENTS.UI_GRAPH_RENDERED; michael@0: front.on(eventName, function onGraphRendered (_, nodes, edges) { michael@0: if (nodes === nodeCount && edges === edgeCount) { michael@0: front.off(eventName, onGraphRendered); michael@0: deferred.resolve(); michael@0: } michael@0: }); michael@0: return deferred.promise; michael@0: } michael@0: michael@0: function checkVariableView (view, index, hash) { michael@0: let scope = view.getScopeAtIndex(index); michael@0: let variables = Object.keys(hash); michael@0: variables.forEach(variable => { michael@0: let aVar = scope.get(variable); michael@0: is(aVar.target.querySelector(".name").getAttribute("value"), variable, michael@0: "Correct property name for " + variable); michael@0: is(aVar.target.querySelector(".value").getAttribute("value"), hash[variable], michael@0: "Correct property value of " + hash[variable] + " for " + variable); michael@0: }); michael@0: } michael@0: michael@0: function modifyVariableView (win, view, index, prop, value) { michael@0: let deferred = Promise.defer(); michael@0: let scope = view.getScopeAtIndex(index); michael@0: let aVar = scope.get(prop); michael@0: scope.expand(); michael@0: michael@0: // Must wait for the scope DOM to be available to receive michael@0: // events michael@0: executeSoon(() => { michael@0: let varValue = aVar.target.querySelector(".title > .value"); michael@0: EventUtils.sendMouseEvent({ type: "mousedown" }, varValue, win); michael@0: michael@0: win.on(win.EVENTS.UI_SET_PARAM, handleSetting); michael@0: win.on(win.EVENTS.UI_SET_PARAM_ERROR, handleSetting); michael@0: michael@0: info("Setting " + value + " for " + prop + "...."); michael@0: let varInput = aVar.target.querySelector(".title > .element-value-input"); michael@0: setText(varInput, value); michael@0: EventUtils.sendKey("RETURN", win); michael@0: }); michael@0: michael@0: function handleSetting (eventName) { michael@0: win.off(win.EVENTS.UI_SET_PARAM, handleSetting); michael@0: win.off(win.EVENTS.UI_SET_PARAM_ERROR, handleSetting); michael@0: if (eventName === win.EVENTS.UI_SET_PARAM) michael@0: deferred.resolve(); michael@0: if (eventName === win.EVENTS.UI_SET_PARAM_ERROR) michael@0: deferred.reject(); michael@0: } michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: function clearText (aElement) { michael@0: info("Clearing text..."); michael@0: aElement.focus(); michael@0: aElement.value = ""; michael@0: } michael@0: michael@0: function setText (aElement, aText) { michael@0: clearText(aElement); michael@0: info("Setting text: " + aText); michael@0: aElement.value = aText; michael@0: } michael@0: michael@0: function findGraphEdge (win, source, target) { michael@0: let selector = ".edgePaths .edgePath[data-source='" + source + "'][data-target='" + target + "']"; michael@0: return win.document.querySelector(selector); michael@0: } michael@0: michael@0: function findGraphNode (win, node) { michael@0: let selector = ".nodes > g[data-id='" + node + "']"; michael@0: return win.document.querySelector(selector); michael@0: } michael@0: michael@0: function click (win, element) { michael@0: EventUtils.sendMouseEvent({ type: "click" }, element, win); michael@0: } michael@0: michael@0: function mouseOver (win, element) { michael@0: EventUtils.sendMouseEvent({ type: "mouseover" }, element, win); michael@0: } michael@0: michael@0: /** michael@0: * List of audio node properties to test against expectations of the AudioNode actor michael@0: */ michael@0: michael@0: const NODE_PROPERTIES = { michael@0: "OscillatorNode": ["type", "frequency", "detune"], michael@0: "GainNode": ["gain"], michael@0: "DelayNode": ["delayTime"], michael@0: "AudioBufferSourceNode": ["buffer", "playbackRate", "loop", "loopStart", "loopEnd"], michael@0: "ScriptProcessorNode": ["bufferSize"], michael@0: "PannerNode": ["panningModel", "distanceModel", "refDistance", "maxDistance", "rolloffFactor", "coneInnerAngle", "coneOuterAngle", "coneOuterGain"], michael@0: "ConvolverNode": ["buffer", "normalize"], michael@0: "DynamicsCompressorNode": ["threshold", "knee", "ratio", "reduction", "attack", "release"], michael@0: "BiquadFilterNode": ["type", "frequency", "Q", "detune", "gain"], michael@0: "WaveShaperNode": ["curve", "oversample"], michael@0: "AnalyserNode": ["fftSize", "minDecibels", "maxDecibels", "smoothingTimeConstraint", "frequencyBinCount"], michael@0: "AudioDestinationNode": [], michael@0: "ChannelSplitterNode": [], michael@0: "ChannelMergerNode": [] michael@0: };