summaryrefslogtreecommitdiffstats
path: root/devtools/client/webaudioeditor/test/head.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/webaudioeditor/test/head.js')
-rw-r--r--devtools/client/webaudioeditor/test/head.js556
1 files changed, 556 insertions, 0 deletions
diff --git a/devtools/client/webaudioeditor/test/head.js b/devtools/client/webaudioeditor/test/head.js
new file mode 100644
index 000000000..7b0b0f01a
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/head.js
@@ -0,0 +1,556 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+var { Task } = require("devtools/shared/task");
+var Services = require("Services");
+var { gDevTools } = require("devtools/client/framework/devtools");
+var { TargetFactory } = require("devtools/client/framework/target");
+var { DebuggerServer } = require("devtools/server/main");
+var { generateUUID } = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
+
+var Promise = require("promise");
+var Services = require("Services");
+var { WebAudioFront } = require("devtools/shared/fronts/webaudio");
+var DevToolsUtils = require("devtools/shared/DevToolsUtils");
+var flags = require("devtools/shared/flags");
+var audioNodes = require("devtools/server/actors/utils/audionodes.json");
+var mm = null;
+
+const FRAME_SCRIPT_UTILS_URL = "chrome://devtools/content/shared/frame-script-utils.js";
+const EXAMPLE_URL = "http://example.com/browser/devtools/client/webaudioeditor/test/";
+const SIMPLE_CONTEXT_URL = EXAMPLE_URL + "doc_simple-context.html";
+const COMPLEX_CONTEXT_URL = EXAMPLE_URL + "doc_complex-context.html";
+const SIMPLE_NODES_URL = EXAMPLE_URL + "doc_simple-node-creation.html";
+const MEDIA_NODES_URL = EXAMPLE_URL + "doc_media-node-creation.html";
+const BUFFER_AND_ARRAY_URL = EXAMPLE_URL + "doc_buffer-and-array.html";
+const DESTROY_NODES_URL = EXAMPLE_URL + "doc_destroy-nodes.html";
+const CONNECT_PARAM_URL = EXAMPLE_URL + "doc_connect-param.html";
+const CONNECT_MULTI_PARAM_URL = EXAMPLE_URL + "doc_connect-multi-param.html";
+const IFRAME_CONTEXT_URL = EXAMPLE_URL + "doc_iframe-context.html";
+const AUTOMATION_URL = EXAMPLE_URL + "doc_automation.html";
+
+// Enable 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);
+
+// All tests are asynchronous.
+waitForExplicitFinish();
+
+var gToolEnabled = Services.prefs.getBoolPref("devtools.webaudioeditor.enabled");
+
+flags.testing = true;
+
+registerCleanupFunction(() => {
+ flags.testing = false;
+ info("finish() was called, cleaning up...");
+ Services.prefs.setBoolPref("devtools.debugger.log", gEnableLogging);
+ Services.prefs.setBoolPref("devtools.webaudioeditor.enabled", gToolEnabled);
+ Cu.forceGC();
+});
+
+/**
+ * Call manually in tests that use frame script utils after initializing
+ * the web audio editor. Call after init but before navigating to a different page.
+ */
+function loadFrameScripts() {
+ mm = gBrowser.selectedBrowser.messageManager;
+ mm.loadFrameScript(FRAME_SCRIPT_UTILS_URL, false);
+}
+
+function addTab(aUrl, aWindow) {
+ info("Adding tab: " + aUrl);
+
+ let deferred = Promise.defer();
+ let targetWindow = aWindow || window;
+ let targetBrowser = targetWindow.gBrowser;
+
+ targetWindow.focus();
+ let tab = targetBrowser.selectedTab = targetBrowser.addTab(aUrl);
+ let linkedBrowser = tab.linkedBrowser;
+
+ BrowserTestUtils.browserLoaded(linkedBrowser).then(function () {
+ info("Tab added and finished loading: " + aUrl);
+ deferred.resolve(tab);
+ });
+
+ return deferred.promise;
+}
+
+function removeTab(aTab, aWindow) {
+ info("Removing tab.");
+
+ let deferred = Promise.defer();
+ let targetWindow = aWindow || window;
+ let targetBrowser = targetWindow.gBrowser;
+ let tabContainer = targetBrowser.tabContainer;
+
+ tabContainer.addEventListener("TabClose", function onClose(aEvent) {
+ tabContainer.removeEventListener("TabClose", onClose, false);
+ info("Tab removed and finished closing.");
+ deferred.resolve();
+ }, false);
+
+ targetBrowser.removeTab(aTab);
+ return deferred.promise;
+}
+
+function once(aTarget, aEventName, aUseCapture = false) {
+ info("Waiting for event: '" + aEventName + "' on " + aTarget + ".");
+
+ let deferred = Promise.defer();
+
+ for (let [add, remove] of [
+ ["on", "off"], // Use event emitter before DOM events for consistency
+ ["addEventListener", "removeEventListener"],
+ ["addListener", "removeListener"]
+ ]) {
+ if ((add in aTarget) && (remove in aTarget)) {
+ aTarget[add](aEventName, function onEvent(...aArgs) {
+ aTarget[remove](aEventName, onEvent, aUseCapture);
+ info("Got event: '" + aEventName + "' on " + aTarget + ".");
+ deferred.resolve(...aArgs);
+ }, aUseCapture);
+ break;
+ }
+ }
+
+ return deferred.promise;
+}
+
+function reload(aTarget, aWaitForTargetEvent = "navigate") {
+ aTarget.activeTab.reload();
+ return once(aTarget, aWaitForTargetEvent);
+}
+
+function navigate(aTarget, aUrl, aWaitForTargetEvent = "navigate") {
+ executeSoon(() => aTarget.activeTab.navigateTo(aUrl));
+ return once(aTarget, aWaitForTargetEvent);
+}
+
+/**
+ * Call manually in tests that use frame script utils after initializing
+ * the shader editor. Call after init but before navigating to different pages.
+ */
+function loadFrameScripts() {
+ mm = gBrowser.selectedBrowser.messageManager;
+ mm.loadFrameScript(FRAME_SCRIPT_UTILS_URL, false);
+}
+
+/**
+ * Adds a new tab, and instantiate a WebAudiFront object.
+ * This requires calling removeTab before the test ends.
+ */
+function initBackend(aUrl) {
+ info("Initializing a web audio editor front.");
+
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+
+ return Task.spawn(function* () {
+ let tab = yield addTab(aUrl);
+ let target = TargetFactory.forTab(tab);
+
+ yield target.makeRemote();
+
+ let front = new WebAudioFront(target.client, target.form);
+ return { target, front };
+ });
+}
+
+/**
+ * Adds a new tab, and open the toolbox for that tab, selecting the audio editor
+ * panel.
+ * This requires calling teardown before the test ends.
+ */
+function initWebAudioEditor(aUrl) {
+ info("Initializing a web audio editor pane.");
+
+ return Task.spawn(function* () {
+ let tab = yield addTab(aUrl);
+ let target = TargetFactory.forTab(tab);
+
+ yield target.makeRemote();
+
+ Services.prefs.setBoolPref("devtools.webaudioeditor.enabled", true);
+ let toolbox = yield gDevTools.showToolbox(target, "webaudioeditor");
+ let panel = toolbox.getCurrentPanel();
+ return { target, panel, toolbox };
+ });
+}
+
+/**
+ * Close the toolbox, destroying all panels, and remove the added test tabs.
+ */
+function teardown(aTarget) {
+ info("Destroying the web audio editor.");
+
+ return gDevTools.closeToolbox(aTarget).then(() => {
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeCurrentTab();
+ }
+ });
+}
+
+// Due to web audio will fire most events synchronously back-to-back,
+// and we can't yield them in a chain without missing actors, this allows
+// us to listen for `n` events and return a promise resolving to them.
+//
+// Takes a `front` object that is an event emitter, the number of
+// programs that should be listened to and waited on, and an optional
+// `onAdd` function that calls with the entire actors array on program link
+function getN(front, eventName, count, spread) {
+ let actors = [];
+ let deferred = Promise.defer();
+ info(`Waiting for ${count} ${eventName} events`);
+ front.on(eventName, function onEvent(...args) {
+ let actor = args[0];
+ if (actors.length !== count) {
+ actors.push(spread ? args : actor);
+ }
+ info(`Got ${actors.length} / ${count} ${eventName} events`);
+ if (actors.length === count) {
+ front.off(eventName, onEvent);
+ deferred.resolve(actors);
+ }
+ });
+ return deferred.promise;
+}
+
+function get(front, eventName) { return getN(front, eventName, 1); }
+function get2(front, eventName) { return getN(front, eventName, 2); }
+function get3(front, eventName) { return getN(front, eventName, 3); }
+function getSpread(front, eventName) { return getN(front, eventName, 1, true); }
+function get2Spread(front, eventName) { return getN(front, eventName, 2, true); }
+function get3Spread(front, eventName) { return getN(front, eventName, 3, true); }
+function getNSpread(front, eventName, count) { return getN(front, eventName, count, true); }
+
+/**
+ * Waits for the UI_GRAPH_RENDERED event to fire, but only
+ * resolves when the graph was rendered with the correct count of
+ * nodes and edges.
+ */
+function waitForGraphRendered(front, nodeCount, edgeCount, paramEdgeCount) {
+ let deferred = Promise.defer();
+ let eventName = front.EVENTS.UI_GRAPH_RENDERED;
+ info(`Wait for graph rendered with ${nodeCount} nodes, ${edgeCount} edges`);
+ front.on(eventName, function onGraphRendered(_, nodes, edges, pEdges) {
+ let paramEdgesDone = paramEdgeCount != null ? paramEdgeCount === pEdges : true;
+ info(`Got graph rendered with ${nodes} / ${nodeCount} nodes, ` +
+ `${edges} / ${edgeCount} edges`);
+ if (nodes === nodeCount && edges === edgeCount && paramEdgesDone) {
+ front.off(eventName, onGraphRendered);
+ deferred.resolve();
+ }
+ });
+ return deferred.promise;
+}
+
+function checkVariableView(view, index, hash, description = "") {
+ info("Checking Variable View");
+ let scope = view.getScopeAtIndex(index);
+ let variables = Object.keys(hash);
+
+ // If node shouldn't display any properties, ensure that the 'empty' message is
+ // visible
+ if (!variables.length) {
+ ok(isVisible(scope.window.$("#properties-empty")),
+ description + " should show the empty properties tab.");
+ return;
+ }
+
+ // Otherwise, iterate over expected properties
+ variables.forEach(variable => {
+ let aVar = scope.get(variable);
+ is(aVar.target.querySelector(".name").getAttribute("value"), variable,
+ "Correct property name for " + variable);
+ let value = aVar.target.querySelector(".value").getAttribute("value");
+
+ // Cast value with JSON.parse if possible;
+ // will fail when displaying Object types like "ArrayBuffer"
+ // and "Float32Array", but will match the original value.
+ try {
+ value = JSON.parse(value);
+ }
+ catch (e) {}
+ if (typeof hash[variable] === "function") {
+ ok(hash[variable](value),
+ "Passing property value of " + value + " for " + variable + " " + description);
+ }
+ else {
+ is(value, hash[variable],
+ "Correct property value of " + hash[variable] + " for " + variable + " " + description);
+ }
+ });
+}
+
+function modifyVariableView(win, view, index, prop, value) {
+ let deferred = Promise.defer();
+ let scope = view.getScopeAtIndex(index);
+ let aVar = scope.get(prop);
+ scope.expand();
+
+ win.on(win.EVENTS.UI_SET_PARAM, handleSetting);
+ win.on(win.EVENTS.UI_SET_PARAM_ERROR, handleSetting);
+
+ // Focus and select the variable to begin editing
+ win.focus();
+ aVar.focus();
+ EventUtils.sendKey("RETURN", win);
+
+ // Must wait for the scope DOM to be available to receive
+ // events
+ executeSoon(() => {
+ info("Setting " + value + " for " + prop + "....");
+ for (let c of (value + "")) {
+ EventUtils.synthesizeKey(c, {}, win);
+ }
+ EventUtils.sendKey("RETURN", win);
+ });
+
+ function handleSetting(eventName) {
+ win.off(win.EVENTS.UI_SET_PARAM, handleSetting);
+ win.off(win.EVENTS.UI_SET_PARAM_ERROR, handleSetting);
+ if (eventName === win.EVENTS.UI_SET_PARAM)
+ deferred.resolve();
+ if (eventName === win.EVENTS.UI_SET_PARAM_ERROR)
+ deferred.reject();
+ }
+
+ return deferred.promise;
+}
+
+function findGraphEdge(win, source, target, param) {
+ let selector = ".edgePaths .edgePath[data-source='" + source + "'][data-target='" + target + "']";
+ if (param) {
+ selector += "[data-param='" + param + "']";
+ }
+ return win.document.querySelector(selector);
+}
+
+function findGraphNode(win, node) {
+ let selector = ".nodes > g[data-id='" + node + "']";
+ return win.document.querySelector(selector);
+}
+
+function click(win, element) {
+ EventUtils.sendMouseEvent({ type: "click" }, element, win);
+}
+
+function mouseOver(win, element) {
+ EventUtils.sendMouseEvent({ type: "mouseover" }, element, win);
+}
+
+function command(button) {
+ let ev = button.ownerDocument.createEvent("XULCommandEvent");
+ ev.initCommandEvent("command", true, true, button.ownerDocument.defaultView, 0, false, false, false, false, null);
+ button.dispatchEvent(ev);
+}
+
+function isVisible(element) {
+ return !element.getAttribute("hidden");
+}
+
+/**
+ * Used in debugging, returns a promise that resolves in `n` milliseconds.
+ */
+function wait(n) {
+ let { promise, resolve } = Promise.defer();
+ setTimeout(resolve, n);
+ info("Waiting " + n / 1000 + " seconds.");
+ return promise;
+}
+
+/**
+ * Clicks a graph node based on actorID or passing in an element.
+ * Returns a promise that resolves once UI_INSPECTOR_NODE_SET is fired and
+ * the tabs have rendered, completing all RDP requests for the node.
+ */
+function clickGraphNode(panelWin, el, waitForToggle = false) {
+ let { promise, resolve } = Promise.defer();
+ let promises = [
+ once(panelWin, panelWin.EVENTS.UI_INSPECTOR_NODE_SET),
+ once(panelWin, panelWin.EVENTS.UI_PROPERTIES_TAB_RENDERED),
+ once(panelWin, panelWin.EVENTS.UI_AUTOMATION_TAB_RENDERED)
+ ];
+
+ if (waitForToggle) {
+ promises.push(once(panelWin, panelWin.EVENTS.UI_INSPECTOR_TOGGLED));
+ }
+
+ // Use `el` as the element if it is one, otherwise
+ // assume it's an ID and find the related graph node
+ let element = el.tagName ? el : findGraphNode(panelWin, el);
+ click(panelWin, element);
+
+ return Promise.all(promises);
+}
+
+/**
+ * Returns the primitive value of a grip's value, or the
+ * original form that the string grip.type comes from.
+ */
+function getGripValue(value) {
+ if (~["boolean", "string", "number"].indexOf(typeof value)) {
+ return value;
+ }
+
+ switch (value.type) {
+ case "undefined": return undefined;
+ case "Infinity": return Infinity;
+ case "-Infinity": return -Infinity;
+ case "NaN": return NaN;
+ case "-0": return -0;
+ case "null": return null;
+ default: return value;
+ }
+}
+
+/**
+ * Counts how many nodes and edges are currently in the graph.
+ */
+function countGraphObjects(win) {
+ return {
+ nodes: win.document.querySelectorAll(".nodes > .audionode").length,
+ edges: win.document.querySelectorAll(".edgePaths > .edgePath").length
+ };
+}
+
+/**
+* Forces cycle collection and GC, used in AudioNode destruction tests.
+*/
+function forceNodeCollection() {
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, function*() {
+ // Kill the reference keeping stuff alive.
+ content.wrappedJSObject.keepAlive = null;
+
+ // Collect the now-deceased nodes.
+ Cu.forceGC();
+ Cu.forceCC();
+ Cu.forceGC();
+ Cu.forceCC();
+ });
+}
+
+/**
+ * Takes a `values` array of automation value entries,
+ * looking for the value at `time` seconds, checking
+ * to see if the value is close to `expected`.
+ */
+function checkAutomationValue(values, time, expected) {
+ // Remain flexible on values as we can approximate points
+ let EPSILON = 0.01;
+
+ let value = getValueAt(values, time);
+ ok(Math.abs(value - expected) < EPSILON, "Timeline value at " + time + " with value " + value + " should have value very close to " + expected);
+
+ /**
+ * Entries are ordered in `values` according to time, so if we can't find an exact point
+ * on a time of interest, return the point in between the threshold. This should
+ * get us a very close value.
+ */
+ function getValueAt(values, time) {
+ for (let i = 0; i < values.length; i++) {
+ if (values[i].delta === time) {
+ return values[i].value;
+ }
+ if (values[i].delta > time) {
+ return (values[i - 1].value + values[i].value) / 2;
+ }
+ }
+ return values[values.length - 1].value;
+ }
+}
+
+/**
+ * Wait for all inspector tabs to complete rendering.
+ */
+function waitForInspectorRender(panelWin, EVENTS) {
+ return Promise.all([
+ once(panelWin, EVENTS.UI_PROPERTIES_TAB_RENDERED),
+ once(panelWin, EVENTS.UI_AUTOMATION_TAB_RENDERED)
+ ]);
+}
+
+/**
+ * Takes a string `script` and evaluates it directly in the content
+ * in potentially a different process.
+ */
+function evalInDebuggee(script) {
+ let deferred = Promise.defer();
+
+ if (!mm) {
+ throw new Error("`loadFrameScripts()` must be called when using MessageManager.");
+ }
+
+ let id = generateUUID().toString();
+ mm.sendAsyncMessage("devtools:test:eval", { script: script, id: id });
+ mm.addMessageListener("devtools:test:eval:response", handler);
+
+ function handler({ data }) {
+ if (id !== data.id) {
+ return;
+ }
+
+ mm.removeMessageListener("devtools:test:eval:response", handler);
+ deferred.resolve(data.value);
+ }
+
+ return deferred.promise;
+}
+
+/**
+ * Takes an AudioNode type and returns it's properties (from audionode.json)
+ * as keys and their default values as keys
+ */
+function nodeDefaultValues(nodeName) {
+ let fn = NODE_CONSTRUCTORS[nodeName];
+
+ if (typeof fn === "undefined") return {};
+
+ let init = nodeName === "AudioDestinationNode" ? "destination" : `create${fn}()`;
+
+ let definition = JSON.stringify(audioNodes[nodeName].properties);
+
+ let evalNode = evalInDebuggee(`
+ let ins = (new AudioContext()).${init};
+ let props = ${definition};
+ let answer = {};
+
+ for(let k in props) {
+ if (props[k].param) {
+ answer[k] = ins[k].defaultValue;
+ } else if (typeof ins[k] === "object" && ins[k] !== null) {
+ answer[k] = ins[k].toString().slice(8, -1);
+ } else {
+ answer[k] = ins[k];
+ }
+ }
+ answer;`);
+
+ return evalNode;
+}
+
+const NODE_CONSTRUCTORS = {
+ "MediaStreamAudioDestinationNode": "MediaStreamDestination",
+ "AudioBufferSourceNode": "BufferSource",
+ "ScriptProcessorNode": "ScriptProcessor",
+ "AnalyserNode": "Analyser",
+ "GainNode": "Gain",
+ "DelayNode": "Delay",
+ "BiquadFilterNode": "BiquadFilter",
+ "WaveShaperNode": "WaveShaper",
+ "PannerNode": "Panner",
+ "ConvolverNode": "Convolver",
+ "ChannelSplitterNode": "ChannelSplitter",
+ "ChannelMergerNode": "ChannelMerger",
+ "DynamicsCompressorNode": "DynamicsCompressor",
+ "OscillatorNode": "Oscillator",
+ "StereoPannerNode": "StereoPanner"
+};