summaryrefslogtreecommitdiff
path: root/toolkit/devtools/webaudioeditor
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/devtools/webaudioeditor')
-rw-r--r--toolkit/devtools/webaudioeditor/controller.js226
-rw-r--r--toolkit/devtools/webaudioeditor/includes.js112
-rw-r--r--toolkit/devtools/webaudioeditor/lib/D3_LICENSE26
-rw-r--r--toolkit/devtools/webaudioeditor/lib/DAGRE_D3_LICENSE19
-rw-r--r--toolkit/devtools/webaudioeditor/lib/dagre-d3.js4560
-rw-r--r--toolkit/devtools/webaudioeditor/models.js312
-rw-r--r--toolkit/devtools/webaudioeditor/moz.build10
-rw-r--r--toolkit/devtools/webaudioeditor/panel.js69
-rw-r--r--toolkit/devtools/webaudioeditor/test/440hz_sine.oggbin0 -> 11822 bytes
-rw-r--r--toolkit/devtools/webaudioeditor/test/browser.ini72
-rw-r--r--toolkit/devtools/webaudioeditor/test/browser_audionode-actor-add-automation-event.js52
-rw-r--r--toolkit/devtools/webaudioeditor/test/browser_audionode-actor-bypass.js36
-rw-r--r--toolkit/devtools/webaudioeditor/test/browser_audionode-actor-connectnode-disconnect.js40
-rw-r--r--toolkit/devtools/webaudioeditor/test/browser_audionode-actor-connectparam.js33
-rw-r--r--toolkit/devtools/webaudioeditor/test/browser_audionode-actor-get-automation-data-01.js53
-rw-r--r--toolkit/devtools/webaudioeditor/test/browser_audionode-actor-get-automation-data-02.js42
-rw-r--r--toolkit/devtools/webaudioeditor/test/browser_audionode-actor-get-automation-data-03.js34
-rw-r--r--toolkit/devtools/webaudioeditor/test/browser_audionode-actor-get-param-flags.js47
-rw-r--r--toolkit/devtools/webaudioeditor/test/browser_audionode-actor-get-params-01.js44
-rw-r--r--toolkit/devtools/webaudioeditor/test/browser_audionode-actor-get-params-02.js44
-rw-r--r--toolkit/devtools/webaudioeditor/test/browser_audionode-actor-get-set-param.js47
-rw-r--r--toolkit/devtools/webaudioeditor/test/browser_audionode-actor-get-type.js28
-rw-r--r--toolkit/devtools/webaudioeditor/test/browser_audionode-actor-is-source.js27
-rw-r--r--toolkit/devtools/webaudioeditor/test/browser_callwatcher-01.js26
-rw-r--r--toolkit/devtools/webaudioeditor/test/browser_callwatcher-02.js44
-rw-r--r--toolkit/devtools/webaudioeditor/test/browser_wa_automation-view-01.js56
-rw-r--r--toolkit/devtools/webaudioeditor/test/browser_wa_automation-view-02.js56
-rw-r--r--toolkit/devtools/webaudioeditor/test/browser_wa_controller-01.js27
-rw-r--r--toolkit/devtools/webaudioeditor/test/browser_wa_destroy-node-01.js64
-rw-r--r--toolkit/devtools/webaudioeditor/test/browser_wa_first-run.js50
-rw-r--r--toolkit/devtools/webaudioeditor/test/browser_wa_graph-click.js50
-rw-r--r--toolkit/devtools/webaudioeditor/test/browser_wa_graph-markers.js75
-rw-r--r--toolkit/devtools/webaudioeditor/test/browser_wa_graph-render-01.js45
-rw-r--r--toolkit/devtools/webaudioeditor/test/browser_wa_graph-render-02.js49
-rw-r--r--toolkit/devtools/webaudioeditor/test/browser_wa_graph-render-03.js35
-rw-r--r--toolkit/devtools/webaudioeditor/test/browser_wa_graph-render-04.js38
-rw-r--r--toolkit/devtools/webaudioeditor/test/browser_wa_graph-render-05.js29
-rw-r--r--toolkit/devtools/webaudioeditor/test/browser_wa_graph-selected.js50
-rw-r--r--toolkit/devtools/webaudioeditor/test/browser_wa_graph-zoom.js43
-rw-r--r--toolkit/devtools/webaudioeditor/test/browser_wa_inspector-bypass-01.js67
-rw-r--r--toolkit/devtools/webaudioeditor/test/browser_wa_inspector-toggle.js60
-rw-r--r--toolkit/devtools/webaudioeditor/test/browser_wa_inspector-width.js56
-rw-r--r--toolkit/devtools/webaudioeditor/test/browser_wa_inspector.js46
-rw-r--r--toolkit/devtools/webaudioeditor/test/browser_wa_navigate.js44
-rw-r--r--toolkit/devtools/webaudioeditor/test/browser_wa_properties-view-edit-01.js65
-rw-r--r--toolkit/devtools/webaudioeditor/test/browser_wa_properties-view-edit-02.js44
-rw-r--r--toolkit/devtools/webaudioeditor/test/browser_wa_properties-view-media-nodes.js72
-rw-r--r--toolkit/devtools/webaudioeditor/test/browser_wa_properties-view-params-objects.js46
-rw-r--r--toolkit/devtools/webaudioeditor/test/browser_wa_properties-view-params.js38
-rw-r--r--toolkit/devtools/webaudioeditor/test/browser_wa_properties-view.js42
-rw-r--r--toolkit/devtools/webaudioeditor/test/browser_wa_reset-01.js65
-rw-r--r--toolkit/devtools/webaudioeditor/test/browser_wa_reset-02.js37
-rw-r--r--toolkit/devtools/webaudioeditor/test/browser_wa_reset-03.js48
-rw-r--r--toolkit/devtools/webaudioeditor/test/browser_wa_reset-04.js62
-rw-r--r--toolkit/devtools/webaudioeditor/test/browser_webaudio-actor-automation-event.js52
-rw-r--r--toolkit/devtools/webaudioeditor/test/browser_webaudio-actor-connect-param.js25
-rw-r--r--toolkit/devtools/webaudioeditor/test/browser_webaudio-actor-destroy-node.js40
-rw-r--r--toolkit/devtools/webaudioeditor/test/browser_webaudio-actor-simple.js34
-rw-r--r--toolkit/devtools/webaudioeditor/test/doc_automation.html30
-rw-r--r--toolkit/devtools/webaudioeditor/test/doc_buffer-and-array.html56
-rw-r--r--toolkit/devtools/webaudioeditor/test/doc_bug_1112378.html57
-rw-r--r--toolkit/devtools/webaudioeditor/test/doc_bug_1125817.html23
-rw-r--r--toolkit/devtools/webaudioeditor/test/doc_bug_1130901.html22
-rw-r--r--toolkit/devtools/webaudioeditor/test/doc_complex-context.html44
-rw-r--r--toolkit/devtools/webaudioeditor/test/doc_connect-multi-param.html32
-rw-r--r--toolkit/devtools/webaudioeditor/test/doc_connect-param.html28
-rw-r--r--toolkit/devtools/webaudioeditor/test/doc_destroy-nodes.html32
-rw-r--r--toolkit/devtools/webaudioeditor/test/doc_iframe-context.html14
-rw-r--r--toolkit/devtools/webaudioeditor/test/doc_media-node-creation.html29
-rw-r--r--toolkit/devtools/webaudioeditor/test/doc_simple-context.html33
-rw-r--r--toolkit/devtools/webaudioeditor/test/doc_simple-node-creation.html28
-rw-r--r--toolkit/devtools/webaudioeditor/test/head.js562
-rw-r--r--toolkit/devtools/webaudioeditor/views/automation.js159
-rw-r--r--toolkit/devtools/webaudioeditor/views/context.js306
-rw-r--r--toolkit/devtools/webaudioeditor/views/inspector.js187
-rw-r--r--toolkit/devtools/webaudioeditor/views/properties.js164
-rw-r--r--toolkit/devtools/webaudioeditor/views/utils.js103
-rw-r--r--toolkit/devtools/webaudioeditor/webaudioeditor.xul142
78 files changed, 9664 insertions, 0 deletions
diff --git a/toolkit/devtools/webaudioeditor/controller.js b/toolkit/devtools/webaudioeditor/controller.js
new file mode 100644
index 000000000..2f51b6926
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/controller.js
@@ -0,0 +1,226 @@
+/* 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/. */
+
+/**
+ * A collection of `AudioNodeModel`s used throughout the editor
+ * to keep track of audio nodes within the audio context.
+ */
+let gAudioNodes = new AudioNodesCollection();
+
+/**
+ * Initializes the web audio editor views
+ */
+function startupWebAudioEditor() {
+ return all([
+ WebAudioEditorController.initialize(),
+ ContextView.initialize(),
+ InspectorView.initialize(),
+ PropertiesView.initialize(),
+ AutomationView.initialize()
+ ]);
+}
+
+/**
+ * Destroys the web audio editor controller and views.
+ */
+function shutdownWebAudioEditor() {
+ return all([
+ WebAudioEditorController.destroy(),
+ ContextView.destroy(),
+ InspectorView.destroy(),
+ PropertiesView.destroy(),
+ AutomationView.destroy()
+ ]);
+}
+
+/**
+ * Functions handling target-related lifetime events.
+ */
+let WebAudioEditorController = {
+ /**
+ * Listen for events emitted by the current tab target.
+ */
+ initialize: Task.async(function* () {
+ telemetry.toolOpened("webaudioeditor");
+ this._onTabNavigated = this._onTabNavigated.bind(this);
+ this._onThemeChange = this._onThemeChange.bind(this);
+
+ gTarget.on("will-navigate", this._onTabNavigated);
+ gTarget.on("navigate", this._onTabNavigated);
+ gFront.on("start-context", this._onStartContext);
+ gFront.on("create-node", this._onCreateNode);
+ gFront.on("connect-node", this._onConnectNode);
+ gFront.on("connect-param", this._onConnectParam);
+ gFront.on("disconnect-node", this._onDisconnectNode);
+ gFront.on("change-param", this._onChangeParam);
+ gFront.on("destroy-node", this._onDestroyNode);
+
+ // Hook into theme change so we can change
+ // the graph's marker styling, since we can't do this
+ // with CSS
+ gDevTools.on("pref-changed", this._onThemeChange);
+
+ // Store the AudioNode definitions from the WebAudioFront
+ AUDIO_NODE_DEFINITION = yield gFront.getDefinition();
+ }),
+
+ /**
+ * Remove events emitted by the current tab target.
+ */
+ destroy: function() {
+ telemetry.toolClosed("webaudioeditor");
+ gTarget.off("will-navigate", this._onTabNavigated);
+ gTarget.off("navigate", this._onTabNavigated);
+ gFront.off("start-context", this._onStartContext);
+ gFront.off("create-node", this._onCreateNode);
+ gFront.off("connect-node", this._onConnectNode);
+ gFront.off("connect-param", this._onConnectParam);
+ gFront.off("disconnect-node", this._onDisconnectNode);
+ gFront.off("change-param", this._onChangeParam);
+ gFront.off("destroy-node", this._onDestroyNode);
+ gDevTools.off("pref-changed", this._onThemeChange);
+ },
+
+ /**
+ * Called when page is reloaded to show the reload notice and waiting
+ * for an audio context notice.
+ */
+ reset: function () {
+ $("#content").hidden = true;
+ ContextView.resetUI();
+ InspectorView.resetUI();
+ PropertiesView.resetUI();
+ },
+
+ // Since node events (create, disconnect, connect) are all async,
+ // we have to make sure to wait that the node has finished creating
+ // before performing an operation on it.
+ getNode: function* (nodeActor) {
+ let id = nodeActor.actorID;
+ let node = gAudioNodes.get(id);
+
+ if (!node) {
+ let { resolve, promise } = defer();
+ gAudioNodes.on("add", function createNodeListener (createdNode) {
+ if (createdNode.id === id) {
+ gAudioNodes.off("add", createNodeListener);
+ resolve(createdNode);
+ }
+ });
+ node = yield promise;
+ }
+ return node;
+ },
+
+ /**
+ * Fired when the devtools theme changes (light, dark, etc.)
+ * so that the graph can update marker styling, as that
+ * cannot currently be done with CSS.
+ */
+ _onThemeChange: function (event, data) {
+ window.emit(EVENTS.THEME_CHANGE, data.newValue);
+ },
+
+ /**
+ * Called for each location change in the debugged tab.
+ */
+ _onTabNavigated: Task.async(function* (event, {isFrameSwitching}) {
+ switch (event) {
+ case "will-navigate": {
+ // Make sure the backend is prepared to handle audio contexts.
+ if (!isFrameSwitching) {
+ yield gFront.setup({ reload: false });
+ }
+
+ // Clear out current UI.
+ this.reset();
+
+ // 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 {
+ // Otherwise, we are loading a new top level document,
+ // so we don't need to reload anymore and should receive
+ // new node events.
+ $("#reload-notice").hidden = true;
+ $("#waiting-notice").hidden = false;
+ }
+
+ // Clear out stored audio nodes
+ gAudioNodes.reset();
+
+ window.emit(EVENTS.UI_RESET);
+ break;
+ }
+ case "navigate": {
+ // TODO Case of bfcache, needs investigating
+ // bug 994250
+ break;
+ }
+ }
+ }),
+
+ /**
+ * Called after the first audio node is created in an audio context,
+ * signaling that the audio context is being used.
+ */
+ _onStartContext: function() {
+ $("#reload-notice").hidden = true;
+ $("#waiting-notice").hidden = true;
+ $("#content").hidden = false;
+ window.emit(EVENTS.START_CONTEXT);
+ },
+
+ /**
+ * Called when a new node is created. Creates an `AudioNodeView` instance
+ * for tracking throughout the editor.
+ */
+ _onCreateNode: Task.async(function* (nodeActor) {
+ yield gAudioNodes.add(nodeActor);
+ }),
+
+ /**
+ * Called on `destroy-node` when an AudioNode is GC'd. Removes
+ * from the AudioNode array and fires an event indicating the removal.
+ */
+ _onDestroyNode: function (nodeActor) {
+ gAudioNodes.remove(gAudioNodes.get(nodeActor.actorID));
+ },
+
+ /**
+ * Called when a node is connected to another node.
+ */
+ _onConnectNode: Task.async(function* ({ source: sourceActor, dest: destActor }) {
+ let source = yield WebAudioEditorController.getNode(sourceActor);
+ let dest = yield WebAudioEditorController.getNode(destActor);
+ source.connect(dest);
+ }),
+
+ /**
+ * Called when a node is conneceted to another node's AudioParam.
+ */
+ _onConnectParam: Task.async(function* ({ source: sourceActor, dest: destActor, param }) {
+ let source = yield WebAudioEditorController.getNode(sourceActor);
+ let dest = yield WebAudioEditorController.getNode(destActor);
+ source.connect(dest, param);
+ }),
+
+ /**
+ * Called when a node is disconnected.
+ */
+ _onDisconnectNode: Task.async(function* (nodeActor) {
+ let node = yield WebAudioEditorController.getNode(nodeActor);
+ node.disconnect();
+ }),
+
+ /**
+ * Called when a node param is changed.
+ */
+ _onChangeParam: Task.async(function* ({ actor, param, value }) {
+ let node = yield WebAudioEditorController.getNode(actor);
+ window.emit(EVENTS.CHANGE_PARAM, node, param, value);
+ })
+};
diff --git a/toolkit/devtools/webaudioeditor/includes.js b/toolkit/devtools/webaudioeditor/includes.js
new file mode 100644
index 000000000..2e6b8ece4
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/includes.js
@@ -0,0 +1,112 @@
+/* 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 { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
+Cu.import("resource:///modules/devtools/gDevTools.jsm");
+
+const devtools = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools;
+const { require } = devtools;
+
+let { console } = Cu.import("resource://gre/modules/devtools/Console.jsm", {});
+let { EventTarget } = require("sdk/event/target");
+
+const { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
+const { Class } = require("sdk/core/heritage");
+const EventEmitter = require("devtools/toolkit/event-emitter");
+const STRINGS_URI = "chrome://browser/locale/devtools/webaudioeditor.properties"
+const L10N = new ViewHelpers.L10N(STRINGS_URI);
+const Telemetry = require("devtools/shared/telemetry");
+const telemetry = new Telemetry();
+devtools.lazyImporter(this, "LineGraphWidget",
+ "resource:///modules/devtools/Graphs.jsm");
+
+// `AUDIO_NODE_DEFINITION` defined in the controller's initialization,
+// which describes all the properties of an AudioNode
+let AUDIO_NODE_DEFINITION;
+
+// Override DOM promises with Promise.jsm helpers
+const { defer, all } = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
+
+/* Events fired on `window` to indicate state or actions*/
+const EVENTS = {
+ // Fired when the first AudioNode has been created, signifying
+ // that the AudioContext is being used and should be tracked via the editor.
+ START_CONTEXT: "WebAudioEditor:StartContext",
+
+ // When the devtools theme changes.
+ THEME_CHANGE: "WebAudioEditor:ThemeChange",
+
+ // When the UI is reset from tab navigation.
+ UI_RESET: "WebAudioEditor:UIReset",
+
+ // When a param has been changed via the UI and successfully
+ // pushed via the actor to the raw audio node.
+ UI_SET_PARAM: "WebAudioEditor:UISetParam",
+
+ // When a node is to be set in the InspectorView.
+ UI_SELECT_NODE: "WebAudioEditor:UISelectNode",
+
+ // When the inspector is finished setting a new node.
+ UI_INSPECTOR_NODE_SET: "WebAudioEditor:UIInspectorNodeSet",
+
+ // When the inspector is finished rendering in or out of view.
+ UI_INSPECTOR_TOGGLED: "WebAudioEditor:UIInspectorToggled",
+
+ // When an audio node is finished loading in the Properties tab.
+ UI_PROPERTIES_TAB_RENDERED: "WebAudioEditor:UIPropertiesTabRendered",
+
+ // When an audio node is finished loading in the Automation tab.
+ UI_AUTOMATION_TAB_RENDERED: "WebAudioEditor:UIAutomationTabRendered",
+
+ // When the Audio Context graph finishes rendering.
+ // Is called with two arguments, first representing number of nodes
+ // rendered, second being the number of edge connections rendering (not counting
+ // param edges), followed by the count of the param edges rendered.
+ UI_GRAPH_RENDERED: "WebAudioEditor:UIGraphRendered",
+
+ // Called when the inspector splitter is moved and resized.
+ UI_INSPECTOR_RESIZE: "WebAudioEditor:UIInspectorResize"
+};
+
+/**
+ * The current target and the Web Audio Editor front, set by this tool's host.
+ */
+let gToolbox, gTarget, gFront;
+
+/**
+ * Convenient way of emitting events from the panel window.
+ */
+EventEmitter.decorate(this);
+
+/**
+ * DOM query helper.
+ */
+function $(selector, target = document) { return target.querySelector(selector); }
+function $$(selector, target = document) { return target.querySelectorAll(selector); }
+
+/**
+ * Takes an iterable collection, and a hash. Return the first
+ * object in the collection that matches the values in the hash.
+ * From Backbone.Collection#findWhere
+ * http://backbonejs.org/#Collection-findWhere
+ */
+function findWhere (collection, attrs) {
+ let keys = Object.keys(attrs);
+ for (let model of collection) {
+ if (keys.every(key => model[key] === attrs[key])) {
+ return model;
+ }
+ }
+ return void 0;
+}
+
+function mixin (source, ...args) {
+ args.forEach(obj => Object.keys(obj).forEach(prop => source[prop] = obj[prop]));
+ return source;
+}
diff --git a/toolkit/devtools/webaudioeditor/lib/D3_LICENSE b/toolkit/devtools/webaudioeditor/lib/D3_LICENSE
new file mode 100644
index 000000000..fb7d95d70
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/lib/D3_LICENSE
@@ -0,0 +1,26 @@
+Copyright (c) 2014, Michael Bostock
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+* The name Michael Bostock may not be used to endorse or promote products
+ derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/toolkit/devtools/webaudioeditor/lib/DAGRE_D3_LICENSE b/toolkit/devtools/webaudioeditor/lib/DAGRE_D3_LICENSE
new file mode 100644
index 000000000..1d64ed68c
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/lib/DAGRE_D3_LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2013 Chris Pettitt
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/toolkit/devtools/webaudioeditor/lib/dagre-d3.js b/toolkit/devtools/webaudioeditor/lib/dagre-d3.js
new file mode 100644
index 000000000..482ce827f
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/lib/dagre-d3.js
@@ -0,0 +1,4560 @@
+;(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
+var global=self;/**
+ * @license
+ * Copyright (c) 2012-2013 Chris Pettitt
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+global.dagreD3 = require('./index');
+
+},{"./index":2}],2:[function(require,module,exports){
+/**
+ * @license
+ * Copyright (c) 2012-2013 Chris Pettitt
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+module.exports = {
+ Digraph: require('graphlib').Digraph,
+ Renderer: require('./lib/Renderer'),
+ json: require('graphlib').converter.json,
+ layout: require('dagre').layout,
+ version: require('./lib/version')
+};
+
+},{"./lib/Renderer":3,"./lib/version":4,"dagre":11,"graphlib":28}],3:[function(require,module,exports){
+var layout = require('dagre').layout;
+
+var d3;
+try { d3 = require('d3'); } catch (_) { d3 = window.d3; }
+
+module.exports = Renderer;
+
+function Renderer() {
+ // Set up defaults...
+ this._layout = layout();
+
+ this.drawNodes(defaultDrawNodes);
+ this.drawEdgeLabels(defaultDrawEdgeLabels);
+ this.drawEdgePaths(defaultDrawEdgePaths);
+ this.positionNodes(defaultPositionNodes);
+ this.positionEdgeLabels(defaultPositionEdgeLabels);
+ this.positionEdgePaths(defaultPositionEdgePaths);
+ this.transition(defaultTransition);
+ this.postLayout(defaultPostLayout);
+ this.postRender(defaultPostRender);
+
+ this.edgeInterpolate('bundle');
+ this.edgeTension(0.95);
+}
+
+Renderer.prototype.layout = function(layout) {
+ if (!arguments.length) { return this._layout; }
+ this._layout = layout;
+ return this;
+};
+
+Renderer.prototype.drawNodes = function(drawNodes) {
+ if (!arguments.length) { return this._drawNodes; }
+ this._drawNodes = bind(drawNodes, this);
+ return this;
+};
+
+Renderer.prototype.drawEdgeLabels = function(drawEdgeLabels) {
+ if (!arguments.length) { return this._drawEdgeLabels; }
+ this._drawEdgeLabels = bind(drawEdgeLabels, this);
+ return this;
+};
+
+Renderer.prototype.drawEdgePaths = function(drawEdgePaths) {
+ if (!arguments.length) { return this._drawEdgePaths; }
+ this._drawEdgePaths = bind(drawEdgePaths, this);
+ return this;
+};
+
+Renderer.prototype.positionNodes = function(positionNodes) {
+ if (!arguments.length) { return this._positionNodes; }
+ this._positionNodes = bind(positionNodes, this);
+ return this;
+};
+
+Renderer.prototype.positionEdgeLabels = function(positionEdgeLabels) {
+ if (!arguments.length) { return this._positionEdgeLabels; }
+ this._positionEdgeLabels = bind(positionEdgeLabels, this);
+ return this;
+};
+
+Renderer.prototype.positionEdgePaths = function(positionEdgePaths) {
+ if (!arguments.length) { return this._positionEdgePaths; }
+ this._positionEdgePaths = bind(positionEdgePaths, this);
+ return this;
+};
+
+Renderer.prototype.transition = function(transition) {
+ if (!arguments.length) { return this._transition; }
+ this._transition = bind(transition, this);
+ return this;
+};
+
+Renderer.prototype.postLayout = function(postLayout) {
+ if (!arguments.length) { return this._postLayout; }
+ this._postLayout = bind(postLayout, this);
+ return this;
+};
+
+Renderer.prototype.postRender = function(postRender) {
+ if (!arguments.length) { return this._postRender; }
+ this._postRender = bind(postRender, this);
+ return this;
+};
+
+Renderer.prototype.edgeInterpolate = function(edgeInterpolate) {
+ if (!arguments.length) { return this._edgeInterpolate; }
+ this._edgeInterpolate = edgeInterpolate;
+ return this;
+};
+
+Renderer.prototype.edgeTension = function(edgeTension) {
+ if (!arguments.length) { return this._edgeTension; }
+ this._edgeTension = edgeTension;
+ return this;
+};
+
+Renderer.prototype.run = function(graph, svg) {
+ // First copy the input graph so that it is not changed by the rendering
+ // process.
+ graph = copyAndInitGraph(graph);
+
+ // Create layers
+ svg
+ .selectAll('g.edgePaths, g.edgeLabels, g.nodes')
+ .data(['edgePaths', 'edgeLabels', 'nodes'])
+ .enter()
+ .append('g')
+ .attr('class', function(d) { return d; });
+
+
+ // Create node and edge roots, attach labels, and capture dimension
+ // information for use with layout.
+ var svgNodes = this._drawNodes(graph, svg.select('g.nodes'));
+ var svgEdgeLabels = this._drawEdgeLabels(graph, svg.select('g.edgeLabels'));
+
+ svgNodes.each(function(u) { calculateDimensions(this, graph.node(u)); });
+ svgEdgeLabels.each(function(e) { calculateDimensions(this, graph.edge(e)); });
+
+ // Now apply the layout function
+ var result = runLayout(graph, this._layout);
+
+ // Run any user-specified post layout processing
+ this._postLayout(result, svg);
+
+ var svgEdgePaths = this._drawEdgePaths(graph, svg.select('g.edgePaths'));
+
+ // Apply the layout information to the graph
+ this._positionNodes(result, svgNodes);
+ this._positionEdgeLabels(result, svgEdgeLabels);
+ this._positionEdgePaths(result, svgEdgePaths);
+
+ this._postRender(result, svg);
+
+ return result;
+};
+
+function copyAndInitGraph(graph) {
+ var copy = graph.copy();
+
+ // Init labels if they were not present in the source graph
+ copy.nodes().forEach(function(u) {
+ var value = copy.node(u);
+ if (value === undefined) {
+ value = {};
+ copy.node(u, value);
+ }
+ if (!('label' in value)) { value.label = ''; }
+ });
+
+ copy.edges().forEach(function(e) {
+ var value = copy.edge(e);
+ if (value === undefined) {
+ value = {};
+ copy.edge(e, value);
+ }
+ if (!('label' in value)) { value.label = ''; }
+ });
+
+ return copy;
+}
+
+function calculateDimensions(group, value) {
+ var bbox = group.getBBox();
+ value.width = bbox.width;
+ value.height = bbox.height;
+}
+
+function runLayout(graph, layout) {
+ var result = layout.run(graph);
+
+ // Copy labels to the result graph
+ graph.eachNode(function(u, value) { result.node(u).label = value.label; });
+ graph.eachEdge(function(e, u, v, value) { result.edge(e).label = value.label; });
+
+ return result;
+}
+
+function defaultDrawNodes(g, root) {
+ var nodes = g.nodes().filter(function(u) { return !isComposite(g, u); });
+
+ var svgNodes = root
+ .selectAll('g.node')
+ .classed('enter', false)
+ .data(nodes, function(u) { return u; });
+
+ svgNodes.selectAll('*').remove();
+
+ svgNodes
+ .enter()
+ .append('g')
+ .style('opacity', 0)
+ .attr('class', 'node enter');
+
+ svgNodes.each(function(u) { addLabel(g.node(u), d3.select(this), 10, 10); });
+
+ this._transition(svgNodes.exit())
+ .style('opacity', 0)
+ .remove();
+
+ return svgNodes;
+}
+
+function defaultDrawEdgeLabels(g, root) {
+ var svgEdgeLabels = root
+ .selectAll('g.edgeLabel')
+ .classed('enter', false)
+ .data(g.edges(), function (e) { return e; });
+
+ svgEdgeLabels.selectAll('*').remove();
+
+ svgEdgeLabels
+ .enter()
+ .append('g')
+ .style('opacity', 0)
+ .attr('class', 'edgeLabel enter');
+
+ svgEdgeLabels.each(function(e) { addLabel(g.edge(e), d3.select(this), 0, 0); });
+
+ this._transition(svgEdgeLabels.exit())
+ .style('opacity', 0)
+ .remove();
+
+ return svgEdgeLabels;
+}
+
+var defaultDrawEdgePaths = function(g, root) {
+ var svgEdgePaths = root
+ .selectAll('g.edgePath')
+ .classed('enter', false)
+ .data(g.edges(), function(e) { return e; });
+
+ svgEdgePaths
+ .enter()
+ .append('g')
+ .attr('class', 'edgePath enter')
+ .append('path')
+ .style('opacity', 0)
+ .attr('marker-end', 'url(#arrowhead)');
+
+ this._transition(svgEdgePaths.exit())
+ .style('opacity', 0)
+ .remove();
+
+ return svgEdgePaths;
+};
+
+function defaultPositionNodes(g, svgNodes, svgNodesEnter) {
+ function transform(u) {
+ var value = g.node(u);
+ return 'translate(' + value.x + ',' + value.y + ')';
+ }
+
+ // For entering nodes, position immediately without transition
+ svgNodes.filter('.enter').attr('transform', transform);
+
+ this._transition(svgNodes)
+ .style('opacity', 1)
+ .attr('transform', transform);
+}
+
+function defaultPositionEdgeLabels(g, svgEdgeLabels) {
+ function transform(e) {
+ var value = g.edge(e);
+ var point = findMidPoint(value.points);
+ return 'translate(' + point.x + ',' + point.y + ')';
+ }
+
+ // For entering edge labels, position immediately without transition
+ svgEdgeLabels.filter('.enter').attr('transform', transform);
+
+ this._transition(svgEdgeLabels)
+ .style('opacity', 1)
+ .attr('transform', transform);
+}
+
+function defaultPositionEdgePaths(g, svgEdgePaths) {
+ var interpolate = this._edgeInterpolate,
+ tension = this._edgeTension;
+
+ function calcPoints(e) {
+ var value = g.edge(e);
+ var source = g.node(g.incidentNodes(e)[0]);
+ var target = g.node(g.incidentNodes(e)[1]);
+ var points = value.points.slice();
+
+ var p0 = points.length === 0 ? target : points[0];
+ var p1 = points.length === 0 ? source : points[points.length - 1];
+
+ points.unshift(intersectRect(source, p0));
+ // TODO: use bpodgursky's shortening algorithm here
+ points.push(intersectRect(target, p1));
+
+ return d3.svg.line()
+ .x(function(d) { return d.x; })
+ .y(function(d) { return d.y; })
+ .interpolate(interpolate)
+ .tension(tension)
+ (points);
+ }
+
+ svgEdgePaths.filter('.enter').selectAll('path')
+ .attr('d', calcPoints);
+
+ this._transition(svgEdgePaths.selectAll('path'))
+ .attr('d', calcPoints)
+ .style('opacity', 1);
+}
+
+// By default we do not use transitions
+function defaultTransition(selection) {
+ return selection;
+}
+
+function defaultPostLayout() {
+ // Do nothing
+}
+
+function defaultPostRender(graph, root) {
+ if (graph.isDirected() && root.select('#arrowhead').empty()) {
+ root
+ .append('svg:defs')
+ .append('svg:marker')
+ .attr('id', 'arrowhead')
+ .attr('viewBox', '0 0 10 10')
+ .attr('refX', 8)
+ .attr('refY', 5)
+ .attr('markerUnits', 'strokewidth')
+ .attr('markerWidth', 8)
+ .attr('markerHeight', 5)
+ .attr('orient', 'auto')
+ .attr('style', 'fill: #333')
+ .append('svg:path')
+ .attr('d', 'M 0 0 L 10 5 L 0 10 z');
+ }
+}
+
+function addLabel(node, root, marginX, marginY) {
+ // Add the rect first so that it appears behind the label
+ var label = node.label;
+ var rect = root.append('rect');
+ var labelSvg = root.append('g');
+
+ if (label[0] === '<') {
+ addForeignObjectLabel(label, labelSvg);
+ // No margin for HTML elements
+ marginX = marginY = 0;
+ } else {
+ addTextLabel(label,
+ labelSvg,
+ Math.floor(node.labelCols),
+ node.labelCut);
+ }
+
+ var bbox = root.node().getBBox();
+
+ labelSvg.attr('transform',
+ 'translate(' + (-bbox.width / 2) + ',' + (-bbox.height / 2) + ')');
+
+ rect
+ .attr('rx', 5)
+ .attr('ry', 5)
+ .attr('x', -(bbox.width / 2 + marginX))
+ .attr('y', -(bbox.height / 2 + marginY))
+ .attr('width', bbox.width + 2 * marginX)
+ .attr('height', bbox.height + 2 * marginY);
+}
+
+function addForeignObjectLabel(label, root) {
+ var fo = root
+ .append('foreignObject')
+ .attr('width', '100000');
+
+ var w, h;
+ fo
+ .append('xhtml:div')
+ .style('float', 'left')
+ // TODO find a better way to get dimensions for foreignObjects...
+ .html(function() { return label; })
+ .each(function() {
+ w = this.clientWidth;
+ h = this.clientHeight;
+ });
+
+ fo
+ .attr('width', w)
+ .attr('height', h);
+}
+
+function addTextLabel(label, root, labelCols, labelCut) {
+ if (labelCut === undefined) labelCut = "false";
+ labelCut = (labelCut.toString().toLowerCase() === "true");
+
+ var node = root
+ .append('text')
+ .attr('text-anchor', 'left');
+
+ label = label.replace(/\\n/g, "\n");
+
+ var arr = labelCols ? wordwrap(label, labelCols, labelCut) : label;
+ arr = arr.split("\n");
+ for (var i = 0; i < arr.length; i++) {
+ node
+ .append('tspan')
+ .attr('dy', '1em')
+ .attr('x', '1')
+ .text(arr[i]);
+ }
+}
+
+// Thanks to
+// http://james.padolsey.com/javascript/wordwrap-for-javascript/
+function wordwrap (str, width, cut, brk) {
+ brk = brk || '\n';
+ width = width || 75;
+ cut = cut || false;
+
+ if (!str) { return str; }
+
+ var regex = '.{1,' +width+ '}(\\s|$)' + (cut ? '|.{' +width+ '}|.+$' : '|\\S+?(\\s|$)');
+
+ return str.match( RegExp(regex, 'g') ).join( brk );
+}
+
+function findMidPoint(points) {
+ var midIdx = points.length / 2;
+ if (points.length % 2) {
+ return points[Math.floor(midIdx)];
+ } else {
+ var p0 = points[midIdx - 1];
+ var p1 = points[midIdx];
+ return {x: (p0.x + p1.x) / 2, y: (p0.y + p1.y) / 2};
+ }
+}
+
+function intersectRect(rect, point) {
+ var x = rect.x;
+ var y = rect.y;
+
+ // For now we only support rectangles
+
+ // Rectangle intersection algorithm from:
+ // http://math.stackexchange.com/questions/108113/find-edge-between-two-boxes
+ var dx = point.x - x;
+ var dy = point.y - y;
+ var w = rect.width / 2;
+ var h = rect.height / 2;
+
+ var sx, sy;
+ if (Math.abs(dy) * w > Math.abs(dx) * h) {
+ // Intersection is top or bottom of rect.
+ if (dy < 0) {
+ h = -h;
+ }
+ sx = dy === 0 ? 0 : h * dx / dy;
+ sy = h;
+ } else {
+ // Intersection is left or right of rect.
+ if (dx < 0) {
+ w = -w;
+ }
+ sx = w;
+ sy = dx === 0 ? 0 : w * dy / dx;
+ }
+
+ return {x: x + sx, y: y + sy};
+}
+
+function isComposite(g, u) {
+ return 'children' in g && g.children(u).length;
+}
+
+function bind(func, thisArg) {
+ // For some reason PhantomJS occassionally fails when using the builtin bind,
+ // so we check if it is available and if not, use a degenerate polyfill.
+ if (func.bind) {
+ return func.bind(thisArg);
+ }
+
+ return function() {
+ return func.apply(thisArg, arguments);
+ };
+}
+
+},{"d3":10,"dagre":11}],4:[function(require,module,exports){
+module.exports = '0.1.5';
+
+},{}],5:[function(require,module,exports){
+exports.Set = require('./lib/Set');
+exports.PriorityQueue = require('./lib/PriorityQueue');
+exports.version = require('./lib/version');
+
+},{"./lib/PriorityQueue":6,"./lib/Set":7,"./lib/version":9}],6:[function(require,module,exports){
+module.exports = PriorityQueue;
+
+/**
+ * A min-priority queue data structure. This algorithm is derived from Cormen,
+ * et al., "Introduction to Algorithms". The basic idea of a min-priority
+ * queue is that you can efficiently (in O(1) time) get the smallest key in
+ * the queue. Adding and removing elements takes O(log n) time. A key can
+ * have its priority decreased in O(log n) time.
+ */
+function PriorityQueue() {
+ this._arr = [];
+ this._keyIndices = {};
+}
+
+/**
+ * Returns the number of elements in the queue. Takes `O(1)` time.
+ */
+PriorityQueue.prototype.size = function() {
+ return this._arr.length;
+};
+
+/**
+ * Returns the keys that are in the queue. Takes `O(n)` time.
+ */
+PriorityQueue.prototype.keys = function() {
+ return this._arr.map(function(x) { return x.key; });
+};
+
+/**
+ * Returns `true` if **key** is in the queue and `false` if not.
+ */
+PriorityQueue.prototype.has = function(key) {
+ return key in this._keyIndices;
+};
+
+/**
+ * Returns the priority for **key**. If **key** is not present in the queue
+ * then this function returns `undefined`. Takes `O(1)` time.
+ *
+ * @param {Object} key
+ */
+PriorityQueue.prototype.priority = function(key) {
+ var index = this._keyIndices[key];
+ if (index !== undefined) {
+ return this._arr[index].priority;
+ }
+};
+
+/**
+ * Returns the key for the minimum element in this queue. If the queue is
+ * empty this function throws an Error. Takes `O(1)` time.
+ */
+PriorityQueue.prototype.min = function() {
+ if (this.size() === 0) {
+ throw new Error("Queue underflow");
+ }
+ return this._arr[0].key;
+};
+
+/**
+ * Inserts a new key into the priority queue. If the key already exists in
+ * the queue this function returns `false`; otherwise it will return `true`.
+ * Takes `O(n)` time.
+ *
+ * @param {Object} key the key to add
+ * @param {Number} priority the initial priority for the key
+ */
+PriorityQueue.prototype.add = function(key, priority) {
+ var keyIndices = this._keyIndices;
+ if (!(key in keyIndices)) {
+ var arr = this._arr;
+ var index = arr.length;
+ keyIndices[key] = index;
+ arr.push({key: key, priority: priority});
+ this._decrease(index);
+ return true;
+ }
+ return false;
+};
+
+/**
+ * Removes and returns the smallest key in the queue. Takes `O(log n)` time.
+ */
+PriorityQueue.prototype.removeMin = function() {
+ this._swap(0, this._arr.length - 1);
+ var min = this._arr.pop();
+ delete this._keyIndices[min.key];
+ this._heapify(0);
+ return min.key;
+};
+
+/**
+ * Decreases the priority for **key** to **priority**. If the new priority is
+ * greater than the previous priority, this function will throw an Error.
+ *
+ * @param {Object} key the key for which to raise priority
+ * @param {Number} priority the new priority for the key
+ */
+PriorityQueue.prototype.decrease = function(key, priority) {
+ var index = this._keyIndices[key];
+ if (priority > this._arr[index].priority) {
+ throw new Error("New priority is greater than current priority. " +
+ "Key: " + key + " Old: " + this._arr[index].priority + " New: " + priority);
+ }
+ this._arr[index].priority = priority;
+ this._decrease(index);
+};
+
+PriorityQueue.prototype._heapify = function(i) {
+ var arr = this._arr;
+ var l = 2 * i,
+ r = l + 1,
+ largest = i;
+ if (l < arr.length) {
+ largest = arr[l].priority < arr[largest].priority ? l : largest;
+ if (r < arr.length) {
+ largest = arr[r].priority < arr[largest].priority ? r : largest;
+ }
+ if (largest !== i) {
+ this._swap(i, largest);
+ this._heapify(largest);
+ }
+ }
+};
+
+PriorityQueue.prototype._decrease = function(index) {
+ var arr = this._arr;
+ var priority = arr[index].priority;
+ var parent;
+ while (index !== 0) {
+ parent = index >> 1;
+ if (arr[parent].priority < priority) {
+ break;
+ }
+ this._swap(index, parent);
+ index = parent;
+ }
+};
+
+PriorityQueue.prototype._swap = function(i, j) {
+ var arr = this._arr;
+ var keyIndices = this._keyIndices;
+ var origArrI = arr[i];
+ var origArrJ = arr[j];
+ arr[i] = origArrJ;
+ arr[j] = origArrI;
+ keyIndices[origArrJ.key] = i;
+ keyIndices[origArrI.key] = j;
+};
+
+},{}],7:[function(require,module,exports){
+var util = require('./util');
+
+module.exports = Set;
+
+/**
+ * Constructs a new Set with an optional set of `initialKeys`.
+ *
+ * It is important to note that keys are coerced to String for most purposes
+ * with this object, similar to the behavior of JavaScript's Object. For
+ * example, the following will add only one key:
+ *
+ * var s = new Set();
+ * s.add(1);
+ * s.add("1");
+ *
+ * However, the type of the key is preserved internally so that `keys` returns
+ * the original key set uncoerced. For the above example, `keys` would return
+ * `[1]`.
+ */
+function Set(initialKeys) {
+ this._size = 0;
+ this._keys = {};
+
+ if (initialKeys) {
+ for (var i = 0, il = initialKeys.length; i < il; ++i) {
+ this.add(initialKeys[i]);
+ }
+ }
+}
+
+/**
+ * Returns a new Set that represents the set intersection of the array of given
+ * sets.
+ */
+Set.intersect = function(sets) {
+ if (sets.length === 0) {
+ return new Set();
+ }
+
+ var result = new Set(!util.isArray(sets[0]) ? sets[0].keys() : sets[0]);
+ for (var i = 1, il = sets.length; i < il; ++i) {
+ var resultKeys = result.keys(),
+ other = !util.isArray(sets[i]) ? sets[i] : new Set(sets[i]);
+ for (var j = 0, jl = resultKeys.length; j < jl; ++j) {
+ var key = resultKeys[j];
+ if (!other.has(key)) {
+ result.remove(key);
+ }
+ }
+ }
+
+ return result;
+};
+
+/**
+ * Returns a new Set that represents the set union of the array of given sets.
+ */
+Set.union = function(sets) {
+ var totalElems = util.reduce(sets, function(lhs, rhs) {
+ return lhs + (rhs.size ? rhs.size() : rhs.length);
+ }, 0);
+ var arr = new Array(totalElems);
+
+ var k = 0;
+ for (var i = 0, il = sets.length; i < il; ++i) {
+ var cur = sets[i],
+ keys = !util.isArray(cur) ? cur.keys() : cur;
+ for (var j = 0, jl = keys.length; j < jl; ++j) {
+ arr[k++] = keys[j];
+ }
+ }
+
+ return new Set(arr);
+};
+
+/**
+ * Returns the size of this set in `O(1)` time.
+ */
+Set.prototype.size = function() {
+ return this._size;
+};
+
+/**
+ * Returns the keys in this set. Takes `O(n)` time.
+ */
+Set.prototype.keys = function() {
+ return values(this._keys);
+};
+
+/**
+ * Tests if a key is present in this Set. Returns `true` if it is and `false`
+ * if not. Takes `O(1)` time.
+ */
+Set.prototype.has = function(key) {
+ return key in this._keys;
+};
+
+/**
+ * Adds a new key to this Set if it is not already present. Returns `true` if
+ * the key was added and `false` if it was already present. Takes `O(1)` time.
+ */
+Set.prototype.add = function(key) {
+ if (!(key in this._keys)) {
+ this._keys[key] = key;
+ ++this._size;
+ return true;
+ }
+ return false;
+};
+
+/**
+ * Removes a key from this Set. If the key was removed this function returns
+ * `true`. If not, it returns `false`. Takes `O(1)` time.
+ */
+Set.prototype.remove = function(key) {
+ if (key in this._keys) {
+ delete this._keys[key];
+ --this._size;
+ return true;
+ }
+ return false;
+};
+
+/*
+ * Returns an array of all values for properties of **o**.
+ */
+function values(o) {
+ var ks = Object.keys(o),
+ len = ks.length,
+ result = new Array(len),
+ i;
+ for (i = 0; i < len; ++i) {
+ result[i] = o[ks[i]];
+ }
+ return result;
+}
+
+},{"./util":8}],8:[function(require,module,exports){
+/*
+ * This polyfill comes from
+ * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray
+ */
+if(!Array.isArray) {
+ exports.isArray = function (vArg) {
+ return Object.prototype.toString.call(vArg) === '[object Array]';
+ };
+} else {
+ exports.isArray = Array.isArray;
+}
+
+/*
+ * Slightly adapted polyfill from
+ * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce
+ */
+if ('function' !== typeof Array.prototype.reduce) {
+ exports.reduce = function(array, callback, opt_initialValue) {
+ 'use strict';
+ if (null === array || 'undefined' === typeof array) {
+ // At the moment all modern browsers, that support strict mode, have
+ // native implementation of Array.prototype.reduce. For instance, IE8
+ // does not support strict mode, so this check is actually useless.
+ throw new TypeError(
+ 'Array.prototype.reduce called on null or undefined');
+ }
+ if ('function' !== typeof callback) {
+ throw new TypeError(callback + ' is not a function');
+ }
+ var index, value,
+ length = array.length >>> 0,
+ isValueSet = false;
+ if (1 < arguments.length) {
+ value = opt_initialValue;
+ isValueSet = true;
+ }
+ for (index = 0; length > index; ++index) {
+ if (array.hasOwnProperty(index)) {
+ if (isValueSet) {
+ value = callback(value, array[index], index, array);
+ }
+ else {
+ value = array[index];
+ isValueSet = true;
+ }
+ }
+ }
+ if (!isValueSet) {
+ throw new TypeError('Reduce of empty array with no initial value');
+ }
+ return value;
+ };
+} else {
+ exports.reduce = function(array, callback, opt_initialValue) {
+ return array.reduce(callback, opt_initialValue);
+ };
+}
+
+},{}],9:[function(require,module,exports){
+module.exports = '1.1.3';
+
+},{}],10:[function(require,module,exports){
+require("./d3");
+module.exports = d3;
+(function () { delete this.d3; })(); // unset global
+
+},{}],11:[function(require,module,exports){
+/*
+Copyright (c) 2012-2013 Chris Pettitt
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+exports.Digraph = require("graphlib").Digraph;
+exports.Graph = require("graphlib").Graph;
+exports.layout = require("./lib/layout");
+exports.version = require("./lib/version");
+
+},{"./lib/layout":12,"./lib/version":27,"graphlib":28}],12:[function(require,module,exports){
+var util = require('./util'),
+ rank = require('./rank'),
+ order = require('./order'),
+ CGraph = require('graphlib').CGraph,
+ CDigraph = require('graphlib').CDigraph;
+
+module.exports = function() {
+ // External configuration
+ var config = {
+ // How much debug information to include?
+ debugLevel: 0,
+ // Max number of sweeps to perform in order phase
+ orderMaxSweeps: order.DEFAULT_MAX_SWEEPS,
+ // Use network simplex algorithm in ranking
+ rankSimplex: false,
+ // Rank direction. Valid values are (TB, LR)
+ rankDir: 'TB'
+ };
+
+ // Phase functions
+ var position = require('./position')();
+
+ // This layout object
+ var self = {};
+
+ self.orderIters = util.propertyAccessor(self, config, 'orderMaxSweeps');
+
+ self.rankSimplex = util.propertyAccessor(self, config, 'rankSimplex');
+
+ self.nodeSep = delegateProperty(position.nodeSep);
+ self.edgeSep = delegateProperty(position.edgeSep);
+ self.universalSep = delegateProperty(position.universalSep);
+ self.rankSep = delegateProperty(position.rankSep);
+ self.rankDir = util.propertyAccessor(self, config, 'rankDir');
+ self.debugAlignment = delegateProperty(position.debugAlignment);
+
+ self.debugLevel = util.propertyAccessor(self, config, 'debugLevel', function(x) {
+ util.log.level = x;
+ position.debugLevel(x);
+ });
+
+ self.run = util.time('Total layout', run);
+
+ self._normalize = normalize;
+
+ return self;
+
+ /*
+ * Constructs an adjacency graph using the nodes and edges specified through
+ * config. For each node and edge we add a property `dagre` that contains an
+ * object that will hold intermediate and final layout information. Some of
+ * the contents include:
+ *
+ * 1) A generated ID that uniquely identifies the object.
+ * 2) Dimension information for nodes (copied from the source node).
+ * 3) Optional dimension information for edges.
+ *
+ * After the adjacency graph is constructed the code no longer needs to use
+ * the original nodes and edges passed in via config.
+ */
+ function initLayoutGraph(inputGraph) {
+ var g = new CDigraph();
+
+ inputGraph.eachNode(function(u, value) {
+ if (value === undefined) value = {};
+ g.addNode(u, {
+ width: value.width,
+ height: value.height
+ });
+ if (value.hasOwnProperty('rank')) {
+ g.node(u).prefRank = value.rank;
+ }
+ });
+
+ // Set up subgraphs
+ if (inputGraph.parent) {
+ inputGraph.nodes().forEach(function(u) {
+ g.parent(u, inputGraph.parent(u));
+ });
+ }
+
+ inputGraph.eachEdge(function(e, u, v, value) {
+ if (value === undefined) value = {};
+ var newValue = {
+ e: e,
+ minLen: value.minLen || 1,
+ width: value.width || 0,
+ height: value.height || 0,
+ points: []
+ };
+
+ g.addEdge(null, u, v, newValue);
+ });
+
+ // Initial graph attributes
+ var graphValue = inputGraph.graph() || {};
+ g.graph({
+ rankDir: graphValue.rankDir || config.rankDir,
+ orderRestarts: graphValue.orderRestarts
+ });
+
+ return g;
+ }
+
+ function run(inputGraph) {
+ var rankSep = self.rankSep();
+ var g;
+ try {
+ // Build internal graph
+ g = util.time('initLayoutGraph', initLayoutGraph)(inputGraph);
+
+ if (g.order() === 0) {
+ return g;
+ }
+
+ // Make space for edge labels
+ g.eachEdge(function(e, s, t, a) {
+ a.minLen *= 2;
+ });
+ self.rankSep(rankSep / 2);
+
+ // Determine the rank for each node. Nodes with a lower rank will appear
+ // above nodes of higher rank.
+ util.time('rank.run', rank.run)(g, config.rankSimplex);
+
+ // Normalize the graph by ensuring that every edge is proper (each edge has
+ // a length of 1). We achieve this by adding dummy nodes to long edges,
+ // thus shortening them.
+ util.time('normalize', normalize)(g);
+
+ // Order the nodes so that edge crossings are minimized.
+ util.time('order', order)(g, config.orderMaxSweeps);
+
+ // Find the x and y coordinates for every node in the graph.
+ util.time('position', position.run)(g);
+
+ // De-normalize the graph by removing dummy nodes and augmenting the
+ // original long edges with coordinate information.
+ util.time('undoNormalize', undoNormalize)(g);
+
+ // Reverses points for edges that are in a reversed state.
+ util.time('fixupEdgePoints', fixupEdgePoints)(g);
+
+ // Restore delete edges and reverse edges that were reversed in the rank
+ // phase.
+ util.time('rank.restoreEdges', rank.restoreEdges)(g);
+
+ // Construct final result graph and return it
+ return util.time('createFinalGraph', createFinalGraph)(g, inputGraph.isDirected());
+ } finally {
+ self.rankSep(rankSep);
+ }
+ }
+
+ /*
+ * This function is responsible for 'normalizing' the graph. The process of
+ * normalization ensures that no edge in the graph has spans more than one
+ * rank. To do this it inserts dummy nodes as needed and links them by adding
+ * dummy edges. This function keeps enough information in the dummy nodes and
+ * edges to ensure that the original graph can be reconstructed later.
+ *
+ * This method assumes that the input graph is cycle free.
+ */
+ function normalize(g) {
+ var dummyCount = 0;
+ g.eachEdge(function(e, s, t, a) {
+ var sourceRank = g.node(s).rank;
+ var targetRank = g.node(t).rank;
+ if (sourceRank + 1 < targetRank) {
+ for (var u = s, rank = sourceRank + 1, i = 0; rank < targetRank; ++rank, ++i) {
+ var v = '_D' + (++dummyCount);
+ var node = {
+ width: a.width,
+ height: a.height,
+ edge: { id: e, source: s, target: t, attrs: a },
+ rank: rank,
+ dummy: true
+ };
+
+ // If this node represents a bend then we will use it as a control
+ // point. For edges with 2 segments this will be the center dummy
+ // node. For edges with more than two segments, this will be the
+ // first and last dummy node.
+ if (i === 0) node.index = 0;
+ else if (rank + 1 === targetRank) node.index = 1;
+
+ g.addNode(v, node);
+ g.addEdge(null, u, v, {});
+ u = v;
+ }
+ g.addEdge(null, u, t, {});
+ g.delEdge(e);
+ }
+ });
+ }
+
+ /*
+ * Reconstructs the graph as it was before normalization. The positions of
+ * dummy nodes are used to build an array of points for the original 'long'
+ * edge. Dummy nodes and edges are removed.
+ */
+ function undoNormalize(g) {
+ g.eachNode(function(u, a) {
+ if (a.dummy) {
+ if ('index' in a) {
+ var edge = a.edge;
+ if (!g.hasEdge(edge.id)) {
+ g.addEdge(edge.id, edge.source, edge.target, edge.attrs);
+ }
+ var points = g.edge(edge.id).points;
+ points[a.index] = { x: a.x, y: a.y, ul: a.ul, ur: a.ur, dl: a.dl, dr: a.dr };
+ }
+ g.delNode(u);
+ }
+ });
+ }
+
+ /*
+ * For each edge that was reversed during the `acyclic` step, reverse its
+ * array of points.
+ */
+ function fixupEdgePoints(g) {
+ g.eachEdge(function(e, s, t, a) { if (a.reversed) a.points.reverse(); });
+ }
+
+ function createFinalGraph(g, isDirected) {
+ var out = isDirected ? new CDigraph() : new CGraph();
+ out.graph(g.graph());
+ g.eachNode(function(u, value) { out.addNode(u, value); });
+ g.eachNode(function(u) { out.parent(u, g.parent(u)); });
+ g.eachEdge(function(e, u, v, value) {
+ out.addEdge(value.e, u, v, value);
+ });
+
+ // Attach bounding box information
+ var maxX = 0, maxY = 0;
+ g.eachNode(function(u, value) {
+ if (!g.children(u).length) {
+ maxX = Math.max(maxX, value.x + value.width / 2);
+ maxY = Math.max(maxY, value.y + value.height / 2);
+ }
+ });
+ g.eachEdge(function(e, u, v, value) {
+ var maxXPoints = Math.max.apply(Math, value.points.map(function(p) { return p.x; }));
+ var maxYPoints = Math.max.apply(Math, value.points.map(function(p) { return p.y; }));
+ maxX = Math.max(maxX, maxXPoints + value.width / 2);
+ maxY = Math.max(maxY, maxYPoints + value.height / 2);
+ });
+ out.graph().width = maxX;
+ out.graph().height = maxY;
+
+ return out;
+ }
+
+ /*
+ * Given a function, a new function is returned that invokes the given
+ * function. The return value from the function is always the `self` object.
+ */
+ function delegateProperty(f) {
+ return function() {
+ if (!arguments.length) return f();
+ f.apply(null, arguments);
+ return self;
+ };
+ }
+};
+
+
+},{"./order":13,"./position":18,"./rank":19,"./util":26,"graphlib":28}],13:[function(require,module,exports){
+var util = require('./util'),
+ crossCount = require('./order/crossCount'),
+ initLayerGraphs = require('./order/initLayerGraphs'),
+ initOrder = require('./order/initOrder'),
+ sortLayer = require('./order/sortLayer');
+
+module.exports = order;
+
+// The maximum number of sweeps to perform before finishing the order phase.
+var DEFAULT_MAX_SWEEPS = 24;
+order.DEFAULT_MAX_SWEEPS = DEFAULT_MAX_SWEEPS;
+
+/*
+ * Runs the order phase with the specified `graph, `maxSweeps`, and
+ * `debugLevel`. If `maxSweeps` is not specified we use `DEFAULT_MAX_SWEEPS`.
+ * If `debugLevel` is not set we assume 0.
+ */
+function order(g, maxSweeps) {
+ if (arguments.length < 2) {
+ maxSweeps = DEFAULT_MAX_SWEEPS;
+ }
+
+ var restarts = g.graph().orderRestarts || 0;
+
+ var layerGraphs = initLayerGraphs(g);
+ // TODO: remove this when we add back support for ordering clusters
+ layerGraphs.forEach(function(lg) {
+ lg = lg.filterNodes(function(u) { return !g.children(u).length; });
+ });
+
+ var iters = 0,
+ currentBestCC,
+ allTimeBestCC = Number.MAX_VALUE,
+ allTimeBest = {};
+
+ function saveAllTimeBest() {
+ g.eachNode(function(u, value) { allTimeBest[u] = value.order; });
+ }
+
+ for (var j = 0; j < Number(restarts) + 1 && allTimeBestCC !== 0; ++j) {
+ currentBestCC = Number.MAX_VALUE;
+ initOrder(g, restarts > 0);
+
+ util.log(2, 'Order phase start cross count: ' + g.graph().orderInitCC);
+
+ var i, lastBest, cc;
+ for (i = 0, lastBest = 0; lastBest < 4 && i < maxSweeps && currentBestCC > 0; ++i, ++lastBest, ++iters) {
+ sweep(g, layerGraphs, i);
+ cc = crossCount(g);
+ if (cc < currentBestCC) {
+ lastBest = 0;
+ currentBestCC = cc;
+ if (cc < allTimeBestCC) {
+ saveAllTimeBest();
+ allTimeBestCC = cc;
+ }
+ }
+ util.log(3, 'Order phase start ' + j + ' iter ' + i + ' cross count: ' + cc);
+ }
+ }
+
+ Object.keys(allTimeBest).forEach(function(u) {
+ if (!g.children || !g.children(u).length) {
+ g.node(u).order = allTimeBest[u];
+ }
+ });
+ g.graph().orderCC = allTimeBestCC;
+
+ util.log(2, 'Order iterations: ' + iters);
+ util.log(2, 'Order phase best cross count: ' + g.graph().orderCC);
+}
+
+function predecessorWeights(g, nodes) {
+ var weights = {};
+ nodes.forEach(function(u) {
+ weights[u] = g.inEdges(u).map(function(e) {
+ return g.node(g.source(e)).order;
+ });
+ });
+ return weights;
+}
+
+function successorWeights(g, nodes) {
+ var weights = {};
+ nodes.forEach(function(u) {
+ weights[u] = g.outEdges(u).map(function(e) {
+ return g.node(g.target(e)).order;
+ });
+ });
+ return weights;
+}
+
+function sweep(g, layerGraphs, iter) {
+ if (iter % 2 === 0) {
+ sweepDown(g, layerGraphs, iter);
+ } else {
+ sweepUp(g, layerGraphs, iter);
+ }
+}
+
+function sweepDown(g, layerGraphs) {
+ var cg;
+ for (i = 1; i < layerGraphs.length; ++i) {
+ cg = sortLayer(layerGraphs[i], cg, predecessorWeights(g, layerGraphs[i].nodes()));
+ }
+}
+
+function sweepUp(g, layerGraphs) {
+ var cg;
+ for (i = layerGraphs.length - 2; i >= 0; --i) {
+ sortLayer(layerGraphs[i], cg, successorWeights(g, layerGraphs[i].nodes()));
+ }
+}
+
+},{"./order/crossCount":14,"./order/initLayerGraphs":15,"./order/initOrder":16,"./order/sortLayer":17,"./util":26}],14:[function(require,module,exports){
+var util = require('../util');
+
+module.exports = crossCount;
+
+/*
+ * Returns the cross count for the given graph.
+ */
+function crossCount(g) {
+ var cc = 0;
+ var ordering = util.ordering(g);
+ for (var i = 1; i < ordering.length; ++i) {
+ cc += twoLayerCrossCount(g, ordering[i-1], ordering[i]);
+ }
+ return cc;
+}
+
+/*
+ * This function searches through a ranked and ordered graph and counts the
+ * number of edges that cross. This algorithm is derived from:
+ *
+ * W. Barth et al., Bilayer Cross Counting, JGAA, 8(2) 179–194 (2004)
+ */
+function twoLayerCrossCount(g, layer1, layer2) {
+ var indices = [];
+ layer1.forEach(function(u) {
+ var nodeIndices = [];
+ g.outEdges(u).forEach(function(e) { nodeIndices.push(g.node(g.target(e)).order); });
+ nodeIndices.sort(function(x, y) { return x - y; });
+ indices = indices.concat(nodeIndices);
+ });
+
+ var firstIndex = 1;
+ while (firstIndex < layer2.length) firstIndex <<= 1;
+
+ var treeSize = 2 * firstIndex - 1;
+ firstIndex -= 1;
+
+ var tree = [];
+ for (var i = 0; i < treeSize; ++i) { tree[i] = 0; }
+
+ var cc = 0;
+ indices.forEach(function(i) {
+ var treeIndex = i + firstIndex;
+ ++tree[treeIndex];
+ while (treeIndex > 0) {
+ if (treeIndex % 2) {
+ cc += tree[treeIndex + 1];
+ }
+ treeIndex = (treeIndex - 1) >> 1;
+ ++tree[treeIndex];
+ }
+ });
+
+ return cc;
+}
+
+},{"../util":26}],15:[function(require,module,exports){
+var nodesFromList = require('graphlib').filter.nodesFromList,
+ /* jshint -W079 */
+ Set = require('cp-data').Set;
+
+module.exports = initLayerGraphs;
+
+/*
+ * This function takes a compound layered graph, g, and produces an array of
+ * layer graphs. Each entry in the array represents a subgraph of nodes
+ * relevant for performing crossing reduction on that layer.
+ */
+function initLayerGraphs(g) {
+ var ranks = [];
+
+ function dfs(u) {
+ if (u === null) {
+ g.children(u).forEach(function(v) { dfs(v); });
+ return;
+ }
+
+ var value = g.node(u);
+ value.minRank = ('rank' in value) ? value.rank : Number.MAX_VALUE;
+ value.maxRank = ('rank' in value) ? value.rank : Number.MIN_VALUE;
+ var uRanks = new Set();
+ g.children(u).forEach(function(v) {
+ var rs = dfs(v);
+ uRanks = Set.union([uRanks, rs]);
+ value.minRank = Math.min(value.minRank, g.node(v).minRank);
+ value.maxRank = Math.max(value.maxRank, g.node(v).maxRank);
+ });
+
+ if ('rank' in value) uRanks.add(value.rank);
+
+ uRanks.keys().forEach(function(r) {
+ if (!(r in ranks)) ranks[r] = [];
+ ranks[r].push(u);
+ });
+
+ return uRanks;
+ }
+ dfs(null);
+
+ var layerGraphs = [];
+ ranks.forEach(function(us, rank) {
+ layerGraphs[rank] = g.filterNodes(nodesFromList(us));
+ });
+
+ return layerGraphs;
+}
+
+},{"cp-data":5,"graphlib":28}],16:[function(require,module,exports){
+var crossCount = require('./crossCount'),
+ util = require('../util');
+
+module.exports = initOrder;
+
+/*
+ * Given a graph with a set of layered nodes (i.e. nodes that have a `rank`
+ * attribute) this function attaches an `order` attribute that uniquely
+ * arranges each node of each rank. If no constraint graph is provided the
+ * order of the nodes in each rank is entirely arbitrary.
+ */
+function initOrder(g, random) {
+ var layers = [];
+
+ g.eachNode(function(u, value) {
+ var layer = layers[value.rank];
+ if (g.children && g.children(u).length > 0) return;
+ if (!layer) {
+ layer = layers[value.rank] = [];
+ }
+ layer.push(u);
+ });
+
+ layers.forEach(function(layer) {
+ if (random) {
+ util.shuffle(layer);
+ }
+ layer.forEach(function(u, i) {
+ g.node(u).order = i;
+ });
+ });
+
+ var cc = crossCount(g);
+ g.graph().orderInitCC = cc;
+ g.graph().orderCC = Number.MAX_VALUE;
+}
+
+},{"../util":26,"./crossCount":14}],17:[function(require,module,exports){
+var util = require('../util');
+/*
+ Digraph = require('graphlib').Digraph,
+ topsort = require('graphlib').alg.topsort,
+ nodesFromList = require('graphlib').filter.nodesFromList;
+*/
+
+module.exports = sortLayer;
+
+/*
+function sortLayer(g, cg, weights) {
+ var result = sortLayerSubgraph(g, null, cg, weights);
+ result.list.forEach(function(u, i) {
+ g.node(u).order = i;
+ });
+ return result.constraintGraph;
+}
+*/
+
+function sortLayer(g, cg, weights) {
+ var ordering = [];
+ var bs = {};
+ g.eachNode(function(u, value) {
+ ordering[value.order] = u;
+ var ws = weights[u];
+ if (ws.length) {
+ bs[u] = util.sum(ws) / ws.length;
+ }
+ });
+
+ var toSort = g.nodes().filter(function(u) { return bs[u] !== undefined; });
+ toSort.sort(function(x, y) {
+ return bs[x] - bs[y] || g.node(x).order - g.node(y).order;
+ });
+
+ for (var i = 0, j = 0, jl = toSort.length; j < jl; ++i) {
+ if (bs[ordering[i]] !== undefined) {
+ g.node(toSort[j++]).order = i;
+ }
+ }
+}
+
+// TOOD: re-enable constrained sorting once we have a strategy for handling
+// undefined barycenters.
+/*
+function sortLayerSubgraph(g, sg, cg, weights) {
+ cg = cg ? cg.filterNodes(nodesFromList(g.children(sg))) : new Digraph();
+
+ var nodeData = {};
+ g.children(sg).forEach(function(u) {
+ if (g.children(u).length) {
+ nodeData[u] = sortLayerSubgraph(g, u, cg, weights);
+ nodeData[u].firstSG = u;
+ nodeData[u].lastSG = u;
+ } else {
+ var ws = weights[u];
+ nodeData[u] = {
+ degree: ws.length,
+ barycenter: ws.length > 0 ? util.sum(ws) / ws.length : 0,
+ list: [u]
+ };
+ }
+ });
+
+ resolveViolatedConstraints(g, cg, nodeData);
+
+ var keys = Object.keys(nodeData);
+ keys.sort(function(x, y) {
+ return nodeData[x].barycenter - nodeData[y].barycenter;
+ });
+
+ var result = keys.map(function(u) { return nodeData[u]; })
+ .reduce(function(lhs, rhs) { return mergeNodeData(g, lhs, rhs); });
+ return result;
+}
+
+/*
+function mergeNodeData(g, lhs, rhs) {
+ var cg = mergeDigraphs(lhs.constraintGraph, rhs.constraintGraph);
+
+ if (lhs.lastSG !== undefined && rhs.firstSG !== undefined) {
+ if (cg === undefined) {
+ cg = new Digraph();
+ }
+ if (!cg.hasNode(lhs.lastSG)) { cg.addNode(lhs.lastSG); }
+ cg.addNode(rhs.firstSG);
+ cg.addEdge(null, lhs.lastSG, rhs.firstSG);
+ }
+
+ return {
+ degree: lhs.degree + rhs.degree,
+ barycenter: (lhs.barycenter * lhs.degree + rhs.barycenter * rhs.degree) /
+ (lhs.degree + rhs.degree),
+ list: lhs.list.concat(rhs.list),
+ firstSG: lhs.firstSG !== undefined ? lhs.firstSG : rhs.firstSG,
+ lastSG: rhs.lastSG !== undefined ? rhs.lastSG : lhs.lastSG,
+ constraintGraph: cg
+ };
+}
+
+function mergeDigraphs(lhs, rhs) {
+ if (lhs === undefined) return rhs;
+ if (rhs === undefined) return lhs;
+
+ lhs = lhs.copy();
+ rhs.nodes().forEach(function(u) { lhs.addNode(u); });
+ rhs.edges().forEach(function(e, u, v) { lhs.addEdge(null, u, v); });
+ return lhs;
+}
+
+function resolveViolatedConstraints(g, cg, nodeData) {
+ // Removes nodes `u` and `v` from `cg` and makes any edges incident on them
+ // incident on `w` instead.
+ function collapseNodes(u, v, w) {
+ // TODO original paper removes self loops, but it is not obvious when this would happen
+ cg.inEdges(u).forEach(function(e) {
+ cg.delEdge(e);
+ cg.addEdge(null, cg.source(e), w);
+ });
+
+ cg.outEdges(v).forEach(function(e) {
+ cg.delEdge(e);
+ cg.addEdge(null, w, cg.target(e));
+ });
+
+ cg.delNode(u);
+ cg.delNode(v);
+ }
+
+ var violated;
+ while ((violated = findViolatedConstraint(cg, nodeData)) !== undefined) {
+ var source = cg.source(violated),
+ target = cg.target(violated);
+
+ var v;
+ while ((v = cg.addNode(null)) && g.hasNode(v)) {
+ cg.delNode(v);
+ }
+
+ // Collapse barycenter and list
+ nodeData[v] = mergeNodeData(g, nodeData[source], nodeData[target]);
+ delete nodeData[source];
+ delete nodeData[target];
+
+ collapseNodes(source, target, v);
+ if (cg.incidentEdges(v).length === 0) { cg.delNode(v); }
+ }
+}
+
+function findViolatedConstraint(cg, nodeData) {
+ var us = topsort(cg);
+ for (var i = 0; i < us.length; ++i) {
+ var u = us[i];
+ var inEdges = cg.inEdges(u);
+ for (var j = 0; j < inEdges.length; ++j) {
+ var e = inEdges[j];
+ if (nodeData[cg.source(e)].barycenter >= nodeData[u].barycenter) {
+ return e;
+ }
+ }
+ }
+}
+*/
+
+},{"../util":26}],18:[function(require,module,exports){
+var util = require('./util');
+
+/*
+ * The algorithms here are based on Brandes and Köpf, "Fast and Simple
+ * Horizontal Coordinate Assignment".
+ */
+module.exports = function() {
+ // External configuration
+ var config = {
+ nodeSep: 50,
+ edgeSep: 10,
+ universalSep: null,
+ rankSep: 30
+ };
+
+ var self = {};
+
+ self.nodeSep = util.propertyAccessor(self, config, 'nodeSep');
+ self.edgeSep = util.propertyAccessor(self, config, 'edgeSep');
+ // If not null this separation value is used for all nodes and edges
+ // regardless of their widths. `nodeSep` and `edgeSep` are ignored with this
+ // option.
+ self.universalSep = util.propertyAccessor(self, config, 'universalSep');
+ self.rankSep = util.propertyAccessor(self, config, 'rankSep');
+ self.debugLevel = util.propertyAccessor(self, config, 'debugLevel');
+
+ self.run = run;
+
+ return self;
+
+ function run(g) {
+ g = g.filterNodes(util.filterNonSubgraphs(g));
+
+ var layering = util.ordering(g);
+
+ var conflicts = findConflicts(g, layering);
+
+ var xss = {};
+ ['u', 'd'].forEach(function(vertDir) {
+ if (vertDir === 'd') layering.reverse();
+
+ ['l', 'r'].forEach(function(horizDir) {
+ if (horizDir === 'r') reverseInnerOrder(layering);
+
+ var dir = vertDir + horizDir;
+ var align = verticalAlignment(g, layering, conflicts, vertDir === 'u' ? 'predecessors' : 'successors');
+ xss[dir]= horizontalCompaction(g, layering, align.pos, align.root, align.align);
+
+ if (config.debugLevel >= 3)
+ debugPositioning(vertDir + horizDir, g, layering, xss[dir]);
+
+ if (horizDir === 'r') flipHorizontally(xss[dir]);
+
+ if (horizDir === 'r') reverseInnerOrder(layering);
+ });
+
+ if (vertDir === 'd') layering.reverse();
+ });
+
+ balance(g, layering, xss);
+
+ g.eachNode(function(v) {
+ var xs = [];
+ for (var alignment in xss) {
+ var alignmentX = xss[alignment][v];
+ posXDebug(alignment, g, v, alignmentX);
+ xs.push(alignmentX);
+ }
+ xs.sort(function(x, y) { return x - y; });
+ posX(g, v, (xs[1] + xs[2]) / 2);
+ });
+
+ // Align y coordinates with ranks
+ var y = 0, reverseY = g.graph().rankDir === 'BT' || g.graph().rankDir === 'RL';
+ layering.forEach(function(layer) {
+ var maxHeight = util.max(layer.map(function(u) { return height(g, u); }));
+ y += maxHeight / 2;
+ layer.forEach(function(u) {
+ posY(g, u, reverseY ? -y : y);
+ });
+ y += maxHeight / 2 + config.rankSep;
+ });
+
+ // Translate layout so that top left corner of bounding rectangle has
+ // coordinate (0, 0).
+ var minX = util.min(g.nodes().map(function(u) { return posX(g, u) - width(g, u) / 2; }));
+ var minY = util.min(g.nodes().map(function(u) { return posY(g, u) - height(g, u) / 2; }));
+ g.eachNode(function(u) {
+ posX(g, u, posX(g, u) - minX);
+ posY(g, u, posY(g, u) - minY);
+ });
+ }
+
+ /*
+ * Generate an ID that can be used to represent any undirected edge that is
+ * incident on `u` and `v`.
+ */
+ function undirEdgeId(u, v) {
+ return u < v
+ ? u.toString().length + ':' + u + '-' + v
+ : v.toString().length + ':' + v + '-' + u;
+ }
+
+ function findConflicts(g, layering) {
+ var conflicts = {}, // Set of conflicting edge ids
+ pos = {}, // Position of node in its layer
+ prevLayer,
+ currLayer,
+ k0, // Position of the last inner segment in the previous layer
+ l, // Current position in the current layer (for iteration up to `l1`)
+ k1; // Position of the next inner segment in the previous layer or
+ // the position of the last element in the previous layer
+
+ if (layering.length <= 2) return conflicts;
+
+ function updateConflicts(v) {
+ var k = pos[v];
+ if (k < k0 || k > k1) {
+ conflicts[undirEdgeId(currLayer[l], v)] = true;
+ }
+ }
+
+ layering[1].forEach(function(u, i) { pos[u] = i; });
+ for (var i = 1; i < layering.length - 1; ++i) {
+ prevLayer = layering[i];
+ currLayer = layering[i+1];
+ k0 = 0;
+ l = 0;
+
+ // Scan current layer for next node that is incident to an inner segement
+ // between layering[i+1] and layering[i].
+ for (var l1 = 0; l1 < currLayer.length; ++l1) {
+ var u = currLayer[l1]; // Next inner segment in the current layer or
+ // last node in the current layer
+ pos[u] = l1;
+ k1 = undefined;
+
+ if (g.node(u).dummy) {
+ var uPred = g.predecessors(u)[0];
+ // Note: In the case of self loops and sideways edges it is possible
+ // for a dummy not to have a predecessor.
+ if (uPred !== undefined && g.node(uPred).dummy)
+ k1 = pos[uPred];
+ }
+ if (k1 === undefined && l1 === currLayer.length - 1)
+ k1 = prevLayer.length - 1;
+
+ if (k1 !== undefined) {
+ for (; l <= l1; ++l) {
+ g.predecessors(currLayer[l]).forEach(updateConflicts);
+ }
+ k0 = k1;
+ }
+ }
+ }
+
+ return conflicts;
+ }
+
+ function verticalAlignment(g, layering, conflicts, relationship) {
+ var pos = {}, // Position for a node in its layer
+ root = {}, // Root of the block that the node participates in
+ align = {}; // Points to the next node in the block or, if the last
+ // element in the block, points to the first block's root
+
+ layering.forEach(function(layer) {
+ layer.forEach(function(u, i) {
+ root[u] = u;
+ align[u] = u;
+ pos[u] = i;
+ });
+ });
+
+ layering.forEach(function(layer) {
+ var prevIdx = -1;
+ layer.forEach(function(v) {
+ var related = g[relationship](v), // Adjacent nodes from the previous layer
+ mid; // The mid point in the related array
+
+ if (related.length > 0) {
+ related.sort(function(x, y) { return pos[x] - pos[y]; });
+ mid = (related.length - 1) / 2;
+ related.slice(Math.floor(mid), Math.ceil(mid) + 1).forEach(function(u) {
+ if (align[v] === v) {
+ if (!conflicts[undirEdgeId(u, v)] && prevIdx < pos[u]) {
+ align[u] = v;
+ align[v] = root[v] = root[u];
+ prevIdx = pos[u];
+ }
+ }
+ });
+ }
+ });
+ });
+
+ return { pos: pos, root: root, align: align };
+ }
+
+ // This function deviates from the standard BK algorithm in two ways. First
+ // it takes into account the size of the nodes. Second it includes a fix to
+ // the original algorithm that is described in Carstens, "Node and Label
+ // Placement in a Layered Layout Algorithm".
+ function horizontalCompaction(g, layering, pos, root, align) {
+ var sink = {}, // Mapping of node id -> sink node id for class
+ maybeShift = {}, // Mapping of sink node id -> { class node id, min shift }
+ shift = {}, // Mapping of sink node id -> shift
+ pred = {}, // Mapping of node id -> predecessor node (or null)
+ xs = {}; // Calculated X positions
+
+ layering.forEach(function(layer) {
+ layer.forEach(function(u, i) {
+ sink[u] = u;
+ maybeShift[u] = {};
+ if (i > 0)
+ pred[u] = layer[i - 1];
+ });
+ });
+
+ function updateShift(toShift, neighbor, delta) {
+ if (!(neighbor in maybeShift[toShift])) {
+ maybeShift[toShift][neighbor] = delta;
+ } else {
+ maybeShift[toShift][neighbor] = Math.min(maybeShift[toShift][neighbor], delta);
+ }
+ }
+
+ function placeBlock(v) {
+ if (!(v in xs)) {
+ xs[v] = 0;
+ var w = v;
+ do {
+ if (pos[w] > 0) {
+ var u = root[pred[w]];
+ placeBlock(u);
+ if (sink[v] === v) {
+ sink[v] = sink[u];
+ }
+ var delta = sep(g, pred[w]) + sep(g, w);
+ if (sink[v] !== sink[u]) {
+ updateShift(sink[u], sink[v], xs[v] - xs[u] - delta);
+ } else {
+ xs[v] = Math.max(xs[v], xs[u] + delta);
+ }
+ }
+ w = align[w];
+ } while (w !== v);
+ }
+ }
+
+ // Root coordinates relative to sink
+ util.values(root).forEach(function(v) {
+ placeBlock(v);
+ });
+
+ // Absolute coordinates
+ // There is an assumption here that we've resolved shifts for any classes
+ // that begin at an earlier layer. We guarantee this by visiting layers in
+ // order.
+ layering.forEach(function(layer) {
+ layer.forEach(function(v) {
+ xs[v] = xs[root[v]];
+ if (v === root[v] && v === sink[v]) {
+ var minShift = 0;
+ if (v in maybeShift && Object.keys(maybeShift[v]).length > 0) {
+ minShift = util.min(Object.keys(maybeShift[v])
+ .map(function(u) {
+ return maybeShift[v][u] + (u in shift ? shift[u] : 0);
+ }
+ ));
+ }
+ shift[v] = minShift;
+ }
+ });
+ });
+
+ layering.forEach(function(layer) {
+ layer.forEach(function(v) {
+ xs[v] += shift[sink[root[v]]] || 0;
+ });
+ });
+
+ return xs;
+ }
+
+ function findMinCoord(g, layering, xs) {
+ return util.min(layering.map(function(layer) {
+ var u = layer[0];
+ return xs[u];
+ }));
+ }
+
+ function findMaxCoord(g, layering, xs) {
+ return util.max(layering.map(function(layer) {
+ var u = layer[layer.length - 1];
+ return xs[u];
+ }));
+ }
+
+ function balance(g, layering, xss) {
+ var min = {}, // Min coordinate for the alignment
+ max = {}, // Max coordinate for the alginment
+ smallestAlignment,
+ shift = {}; // Amount to shift a given alignment
+
+ function updateAlignment(v) {
+ xss[alignment][v] += shift[alignment];
+ }
+
+ var smallest = Number.POSITIVE_INFINITY;
+ for (var alignment in xss) {
+ var xs = xss[alignment];
+ min[alignment] = findMinCoord(g, layering, xs);
+ max[alignment] = findMaxCoord(g, layering, xs);
+ var w = max[alignment] - min[alignment];
+ if (w < smallest) {
+ smallest = w;
+ smallestAlignment = alignment;
+ }
+ }
+
+ // Determine how much to adjust positioning for each alignment
+ ['u', 'd'].forEach(function(vertDir) {
+ ['l', 'r'].forEach(function(horizDir) {
+ var alignment = vertDir + horizDir;
+ shift[alignment] = horizDir === 'l'
+ ? min[smallestAlignment] - min[alignment]
+ : max[smallestAlignment] - max[alignment];
+ });
+ });
+
+ // Find average of medians for xss array
+ for (alignment in xss) {
+ g.eachNode(updateAlignment);
+ }
+ }
+
+ function flipHorizontally(xs) {
+ for (var u in xs) {
+ xs[u] = -xs[u];
+ }
+ }
+
+ function reverseInnerOrder(layering) {
+ layering.forEach(function(layer) {
+ layer.reverse();
+ });
+ }
+
+ function width(g, u) {
+ switch (g.graph().rankDir) {
+ case 'LR': return g.node(u).height;
+ case 'RL': return g.node(u).height;
+ default: return g.node(u).width;
+ }
+ }
+
+ function height(g, u) {
+ switch(g.graph().rankDir) {
+ case 'LR': return g.node(u).width;
+ case 'RL': return g.node(u).width;
+ default: return g.node(u).height;
+ }
+ }
+
+ function sep(g, u) {
+ if (config.universalSep !== null) {
+ return config.universalSep;
+ }
+ var w = width(g, u);
+ var s = g.node(u).dummy ? config.edgeSep : config.nodeSep;
+ return (w + s) / 2;
+ }
+
+ function posX(g, u, x) {
+ if (g.graph().rankDir === 'LR' || g.graph().rankDir === 'RL') {
+ if (arguments.length < 3) {
+ return g.node(u).y;
+ } else {
+ g.node(u).y = x;
+ }
+ } else {
+ if (arguments.length < 3) {
+ return g.node(u).x;
+ } else {
+ g.node(u).x = x;
+ }
+ }
+ }
+
+ function posXDebug(name, g, u, x) {
+ if (g.graph().rankDir === 'LR' || g.graph().rankDir === 'RL') {
+ if (arguments.length < 3) {
+ return g.node(u)[name];
+ } else {
+ g.node(u)[name] = x;
+ }
+ } else {
+ if (arguments.length < 3) {
+ return g.node(u)[name];
+ } else {
+ g.node(u)[name] = x;
+ }
+ }
+ }
+
+ function posY(g, u, y) {
+ if (g.graph().rankDir === 'LR' || g.graph().rankDir === 'RL') {
+ if (arguments.length < 3) {
+ return g.node(u).x;
+ } else {
+ g.node(u).x = y;
+ }
+ } else {
+ if (arguments.length < 3) {
+ return g.node(u).y;
+ } else {
+ g.node(u).y = y;
+ }
+ }
+ }
+
+ function debugPositioning(align, g, layering, xs) {
+ layering.forEach(function(l, li) {
+ var u, xU;
+ l.forEach(function(v) {
+ var xV = xs[v];
+ if (u) {
+ var s = sep(g, u) + sep(g, v);
+ if (xV - xU < s)
+ console.log('Position phase: sep violation. Align: ' + align + '. Layer: ' + li + '. ' +
+ 'U: ' + u + ' V: ' + v + '. Actual sep: ' + (xV - xU) + ' Expected sep: ' + s);
+ }
+ u = v;
+ xU = xV;
+ });
+ });
+ }
+};
+
+},{"./util":26}],19:[function(require,module,exports){
+var util = require('./util'),
+ acyclic = require('./rank/acyclic'),
+ initRank = require('./rank/initRank'),
+ feasibleTree = require('./rank/feasibleTree'),
+ constraints = require('./rank/constraints'),
+ simplex = require('./rank/simplex'),
+ components = require('graphlib').alg.components,
+ filter = require('graphlib').filter;
+
+exports.run = run;
+exports.restoreEdges = restoreEdges;
+
+/*
+ * Heuristic function that assigns a rank to each node of the input graph with
+ * the intent of minimizing edge lengths, while respecting the `minLen`
+ * attribute of incident edges.
+ *
+ * Prerequisites:
+ *
+ * * Each edge in the input graph must have an assigned 'minLen' attribute
+ */
+function run(g, useSimplex) {
+ expandSelfLoops(g);
+
+ // If there are rank constraints on nodes, then build a new graph that
+ // encodes the constraints.
+ util.time('constraints.apply', constraints.apply)(g);
+
+ expandSidewaysEdges(g);
+
+ // Reverse edges to get an acyclic graph, we keep the graph in an acyclic
+ // state until the very end.
+ util.time('acyclic', acyclic)(g);
+
+ // Convert the graph into a flat graph for ranking
+ var flatGraph = g.filterNodes(util.filterNonSubgraphs(g));
+
+ // Assign an initial ranking using DFS.
+ initRank(flatGraph);
+
+ // For each component improve the assigned ranks.
+ components(flatGraph).forEach(function(cmpt) {
+ var subgraph = flatGraph.filterNodes(filter.nodesFromList(cmpt));
+ rankComponent(subgraph, useSimplex);
+ });
+
+ // Relax original constraints
+ util.time('constraints.relax', constraints.relax(g));
+
+ // When handling nodes with constrained ranks it is possible to end up with
+ // edges that point to previous ranks. Most of the subsequent algorithms assume
+ // that edges are pointing to successive ranks only. Here we reverse any "back
+ // edges" and mark them as such. The acyclic algorithm will reverse them as a
+ // post processing step.
+ util.time('reorientEdges', reorientEdges)(g);
+}
+
+function restoreEdges(g) {
+ acyclic.undo(g);
+}
+
+/*
+ * Expand self loops into three dummy nodes. One will sit above the incident
+ * node, one will be at the same level, and one below. The result looks like:
+ *
+ * /--<--x--->--\
+ * node y
+ * \--<--z--->--/
+ *
+ * Dummy nodes x, y, z give us the shape of a loop and node y is where we place
+ * the label.
+ *
+ * TODO: consolidate knowledge of dummy node construction.
+ * TODO: support minLen = 2
+ */
+function expandSelfLoops(g) {
+ g.eachEdge(function(e, u, v, a) {
+ if (u === v) {
+ var x = addDummyNode(g, e, u, v, a, 0, false),
+ y = addDummyNode(g, e, u, v, a, 1, true),
+ z = addDummyNode(g, e, u, v, a, 2, false);
+ g.addEdge(null, x, u, {minLen: 1, selfLoop: true});
+ g.addEdge(null, x, y, {minLen: 1, selfLoop: true});
+ g.addEdge(null, u, z, {minLen: 1, selfLoop: true});
+ g.addEdge(null, y, z, {minLen: 1, selfLoop: true});
+ g.delEdge(e);
+ }
+ });
+}
+
+function expandSidewaysEdges(g) {
+ g.eachEdge(function(e, u, v, a) {
+ if (u === v) {
+ var origEdge = a.originalEdge,
+ dummy = addDummyNode(g, origEdge.e, origEdge.u, origEdge.v, origEdge.value, 0, true);
+ g.addEdge(null, u, dummy, {minLen: 1});
+ g.addEdge(null, dummy, v, {minLen: 1});
+ g.delEdge(e);
+ }
+ });
+}
+
+function addDummyNode(g, e, u, v, a, index, isLabel) {
+ return g.addNode(null, {
+ width: isLabel ? a.width : 0,
+ height: isLabel ? a.height : 0,
+ edge: { id: e, source: u, target: v, attrs: a },
+ dummy: true,
+ index: index
+ });
+}
+
+function reorientEdges(g) {
+ g.eachEdge(function(e, u, v, value) {
+ if (g.node(u).rank > g.node(v).rank) {
+ g.delEdge(e);
+ value.reversed = true;
+ g.addEdge(e, v, u, value);
+ }
+ });
+}
+
+function rankComponent(subgraph, useSimplex) {
+ var spanningTree = feasibleTree(subgraph);
+
+ if (useSimplex) {
+ util.log(1, 'Using network simplex for ranking');
+ simplex(subgraph, spanningTree);
+ }
+ normalize(subgraph);
+}
+
+function normalize(g) {
+ var m = util.min(g.nodes().map(function(u) { return g.node(u).rank; }));
+ g.eachNode(function(u, node) { node.rank -= m; });
+}
+
+},{"./rank/acyclic":20,"./rank/constraints":21,"./rank/feasibleTree":22,"./rank/initRank":23,"./rank/simplex":25,"./util":26,"graphlib":28}],20:[function(require,module,exports){
+var util = require('../util');
+
+module.exports = acyclic;
+module.exports.undo = undo;
+
+/*
+ * This function takes a directed graph that may have cycles and reverses edges
+ * as appropriate to break these cycles. Each reversed edge is assigned a
+ * `reversed` attribute with the value `true`.
+ *
+ * There should be no self loops in the graph.
+ */
+function acyclic(g) {
+ var onStack = {},
+ visited = {},
+ reverseCount = 0;
+
+ function dfs(u) {
+ if (u in visited) return;
+ visited[u] = onStack[u] = true;
+ g.outEdges(u).forEach(function(e) {
+ var t = g.target(e),
+ value;
+
+ if (u === t) {
+ console.error('Warning: found self loop "' + e + '" for node "' + u + '"');
+ } else if (t in onStack) {
+ value = g.edge(e);
+ g.delEdge(e);
+ value.reversed = true;
+ ++reverseCount;
+ g.addEdge(e, t, u, value);
+ } else {
+ dfs(t);
+ }
+ });
+
+ delete onStack[u];
+ }
+
+ g.eachNode(function(u) { dfs(u); });
+
+ util.log(2, 'Acyclic Phase: reversed ' + reverseCount + ' edge(s)');
+
+ return reverseCount;
+}
+
+/*
+ * Given a graph that has had the acyclic operation applied, this function
+ * undoes that operation. More specifically, any edge with the `reversed`
+ * attribute is again reversed to restore the original direction of the edge.
+ */
+function undo(g) {
+ g.eachEdge(function(e, s, t, a) {
+ if (a.reversed) {
+ delete a.reversed;
+ g.delEdge(e);
+ g.addEdge(e, t, s, a);
+ }
+ });
+}
+
+},{"../util":26}],21:[function(require,module,exports){
+exports.apply = function(g) {
+ function dfs(sg) {
+ var rankSets = {};
+ g.children(sg).forEach(function(u) {
+ if (g.children(u).length) {
+ dfs(u);
+ return;
+ }
+
+ var value = g.node(u),
+ prefRank = value.prefRank;
+ if (prefRank !== undefined) {
+ if (!checkSupportedPrefRank(prefRank)) { return; }
+
+ if (!(prefRank in rankSets)) {
+ rankSets.prefRank = [u];
+ } else {
+ rankSets.prefRank.push(u);
+ }
+
+ var newU = rankSets[prefRank];
+ if (newU === undefined) {
+ newU = rankSets[prefRank] = g.addNode(null, { originalNodes: [] });
+ g.parent(newU, sg);
+ }
+
+ redirectInEdges(g, u, newU, prefRank === 'min');
+ redirectOutEdges(g, u, newU, prefRank === 'max');
+
+ // Save original node and remove it from reduced graph
+ g.node(newU).originalNodes.push({ u: u, value: value, parent: sg });
+ g.delNode(u);
+ }
+ });
+
+ addLightEdgesFromMinNode(g, sg, rankSets.min);
+ addLightEdgesToMaxNode(g, sg, rankSets.max);
+ }
+
+ dfs(null);
+};
+
+function checkSupportedPrefRank(prefRank) {
+ if (prefRank !== 'min' && prefRank !== 'max' && prefRank.indexOf('same_') !== 0) {
+ console.error('Unsupported rank type: ' + prefRank);
+ return false;
+ }
+ return true;
+}
+
+function redirectInEdges(g, u, newU, reverse) {
+ g.inEdges(u).forEach(function(e) {
+ var origValue = g.edge(e),
+ value;
+ if (origValue.originalEdge) {
+ value = origValue;
+ } else {
+ value = {
+ originalEdge: { e: e, u: g.source(e), v: g.target(e), value: origValue },
+ minLen: g.edge(e).minLen
+ };
+ }
+
+ // Do not reverse edges for self-loops.
+ if (origValue.selfLoop) {
+ reverse = false;
+ }
+
+ if (reverse) {
+ // Ensure that all edges to min are reversed
+ g.addEdge(null, newU, g.source(e), value);
+ value.reversed = true;
+ } else {
+ g.addEdge(null, g.source(e), newU, value);
+ }
+ });
+}
+
+function redirectOutEdges(g, u, newU, reverse) {
+ g.outEdges(u).forEach(function(e) {
+ var origValue = g.edge(e),
+ value;
+ if (origValue.originalEdge) {
+ value = origValue;
+ } else {
+ value = {
+ originalEdge: { e: e, u: g.source(e), v: g.target(e), value: origValue },
+ minLen: g.edge(e).minLen
+ };
+ }
+
+ // Do not reverse edges for self-loops.
+ if (origValue.selfLoop) {
+ reverse = false;
+ }
+
+ if (reverse) {
+ // Ensure that all edges from max are reversed
+ g.addEdge(null, g.target(e), newU, value);
+ value.reversed = true;
+ } else {
+ g.addEdge(null, newU, g.target(e), value);
+ }
+ });
+}
+
+function addLightEdgesFromMinNode(g, sg, minNode) {
+ if (minNode !== undefined) {
+ g.children(sg).forEach(function(u) {
+ // The dummy check ensures we don't add an edge if the node is involved
+ // in a self loop or sideways edge.
+ if (u !== minNode && !g.outEdges(minNode, u).length && !g.node(u).dummy) {
+ g.addEdge(null, minNode, u, { minLen: 0 });
+ }
+ });
+ }
+}
+
+function addLightEdgesToMaxNode(g, sg, maxNode) {
+ if (maxNode !== undefined) {
+ g.children(sg).forEach(function(u) {
+ // The dummy check ensures we don't add an edge if the node is involved
+ // in a self loop or sideways edge.
+ if (u !== maxNode && !g.outEdges(u, maxNode).length && !g.node(u).dummy) {
+ g.addEdge(null, u, maxNode, { minLen: 0 });
+ }
+ });
+ }
+}
+
+/*
+ * This function "relaxes" the constraints applied previously by the "apply"
+ * function. It expands any nodes that were collapsed and assigns the rank of
+ * the collapsed node to each of the expanded nodes. It also restores the
+ * original edges and removes any dummy edges pointing at the collapsed nodes.
+ *
+ * Note that the process of removing collapsed nodes also removes dummy edges
+ * automatically.
+ */
+exports.relax = function(g) {
+ // Save original edges
+ var originalEdges = [];
+ g.eachEdge(function(e, u, v, value) {
+ var originalEdge = value.originalEdge;
+ if (originalEdge) {
+ originalEdges.push(originalEdge);
+ }
+ });
+
+ // Expand collapsed nodes
+ g.eachNode(function(u, value) {
+ var originalNodes = value.originalNodes;
+ if (originalNodes) {
+ originalNodes.forEach(function(originalNode) {
+ originalNode.value.rank = value.rank;
+ g.addNode(originalNode.u, originalNode.value);
+ g.parent(originalNode.u, originalNode.parent);
+ });
+ g.delNode(u);
+ }
+ });
+
+ // Restore original edges
+ originalEdges.forEach(function(edge) {
+ g.addEdge(edge.e, edge.u, edge.v, edge.value);
+ });
+};
+
+},{}],22:[function(require,module,exports){
+/* jshint -W079 */
+var Set = require('cp-data').Set,
+/* jshint +W079 */
+ Digraph = require('graphlib').Digraph,
+ util = require('../util');
+
+module.exports = feasibleTree;
+
+/*
+ * Given an acyclic graph with each node assigned a `rank` attribute, this
+ * function constructs and returns a spanning tree. This function may reduce
+ * the length of some edges from the initial rank assignment while maintaining
+ * the `minLen` specified by each edge.
+ *
+ * Prerequisites:
+ *
+ * * The input graph is acyclic
+ * * Each node in the input graph has an assigned `rank` attribute
+ * * Each edge in the input graph has an assigned `minLen` attribute
+ *
+ * Outputs:
+ *
+ * A feasible spanning tree for the input graph (i.e. a spanning tree that
+ * respects each graph edge's `minLen` attribute) represented as a Digraph with
+ * a `root` attribute on graph.
+ *
+ * Nodes have the same id and value as that in the input graph.
+ *
+ * Edges in the tree have arbitrarily assigned ids. The attributes for edges
+ * include `reversed`. `reversed` indicates that the edge is a
+ * back edge in the input graph.
+ */
+function feasibleTree(g) {
+ var remaining = new Set(g.nodes()),
+ tree = new Digraph();
+
+ if (remaining.size() === 1) {
+ var root = g.nodes()[0];
+ tree.addNode(root, {});
+ tree.graph({ root: root });
+ return tree;
+ }
+
+ function addTightEdges(v) {
+ var continueToScan = true;
+ g.predecessors(v).forEach(function(u) {
+ if (remaining.has(u) && !slack(g, u, v)) {
+ if (remaining.has(v)) {
+ tree.addNode(v, {});
+ remaining.remove(v);
+ tree.graph({ root: v });
+ }
+
+ tree.addNode(u, {});
+ tree.addEdge(null, u, v, { reversed: true });
+ remaining.remove(u);
+ addTightEdges(u);
+ continueToScan = false;
+ }
+ });
+
+ g.successors(v).forEach(function(w) {
+ if (remaining.has(w) && !slack(g, v, w)) {
+ if (remaining.has(v)) {
+ tree.addNode(v, {});
+ remaining.remove(v);
+ tree.graph({ root: v });
+ }
+
+ tree.addNode(w, {});
+ tree.addEdge(null, v, w, {});
+ remaining.remove(w);
+ addTightEdges(w);
+ continueToScan = false;
+ }
+ });
+ return continueToScan;
+ }
+
+ function createTightEdge() {
+ var minSlack = Number.MAX_VALUE;
+ remaining.keys().forEach(function(v) {
+ g.predecessors(v).forEach(function(u) {
+ if (!remaining.has(u)) {
+ var edgeSlack = slack(g, u, v);
+ if (Math.abs(edgeSlack) < Math.abs(minSlack)) {
+ minSlack = -edgeSlack;
+ }
+ }
+ });
+
+ g.successors(v).forEach(function(w) {
+ if (!remaining.has(w)) {
+ var edgeSlack = slack(g, v, w);
+ if (Math.abs(edgeSlack) < Math.abs(minSlack)) {
+ minSlack = edgeSlack;
+ }
+ }
+ });
+ });
+
+ tree.eachNode(function(u) { g.node(u).rank -= minSlack; });
+ }
+
+ while (remaining.size()) {
+ var nodesToSearch = !tree.order() ? remaining.keys() : tree.nodes();
+ for (var i = 0, il = nodesToSearch.length;
+ i < il && addTightEdges(nodesToSearch[i]);
+ ++i);
+ if (remaining.size()) {
+ createTightEdge();
+ }
+ }
+
+ return tree;
+}
+
+function slack(g, u, v) {
+ var rankDiff = g.node(v).rank - g.node(u).rank;
+ var maxMinLen = util.max(g.outEdges(u, v)
+ .map(function(e) { return g.edge(e).minLen; }));
+ return rankDiff - maxMinLen;
+}
+
+},{"../util":26,"cp-data":5,"graphlib":28}],23:[function(require,module,exports){
+var util = require('../util'),
+ topsort = require('graphlib').alg.topsort;
+
+module.exports = initRank;
+
+/*
+ * Assigns a `rank` attribute to each node in the input graph and ensures that
+ * this rank respects the `minLen` attribute of incident edges.
+ *
+ * Prerequisites:
+ *
+ * * The input graph must be acyclic
+ * * Each edge in the input graph must have an assigned 'minLen' attribute
+ */
+function initRank(g) {
+ var sorted = topsort(g);
+
+ sorted.forEach(function(u) {
+ var inEdges = g.inEdges(u);
+ if (inEdges.length === 0) {
+ g.node(u).rank = 0;
+ return;
+ }
+
+ var minLens = inEdges.map(function(e) {
+ return g.node(g.source(e)).rank + g.edge(e).minLen;
+ });
+ g.node(u).rank = util.max(minLens);
+ });
+}
+
+},{"../util":26,"graphlib":28}],24:[function(require,module,exports){
+module.exports = {
+ slack: slack
+};
+
+/*
+ * A helper to calculate the slack between two nodes (`u` and `v`) given a
+ * `minLen` constraint. The slack represents how much the distance between `u`
+ * and `v` could shrink while maintaining the `minLen` constraint. If the value
+ * is negative then the constraint is currently violated.
+ *
+ This function requires that `u` and `v` are in `graph` and they both have a
+ `rank` attribute.
+ */
+function slack(graph, u, v, minLen) {
+ return Math.abs(graph.node(u).rank - graph.node(v).rank) - minLen;
+}
+
+},{}],25:[function(require,module,exports){
+var util = require('../util'),
+ rankUtil = require('./rankUtil');
+
+module.exports = simplex;
+
+function simplex(graph, spanningTree) {
+ // The network simplex algorithm repeatedly replaces edges of
+ // the spanning tree with negative cut values until no such
+ // edge exists.
+ initCutValues(graph, spanningTree);
+ while (true) {
+ var e = leaveEdge(spanningTree);
+ if (e === null) break;
+ var f = enterEdge(graph, spanningTree, e);
+ exchange(graph, spanningTree, e, f);
+ }
+}
+
+/*
+ * Set the cut values of edges in the spanning tree by a depth-first
+ * postorder traversal. The cut value corresponds to the cost, in
+ * terms of a ranking's edge length sum, of lengthening an edge.
+ * Negative cut values typically indicate edges that would yield a
+ * smaller edge length sum if they were lengthened.
+ */
+function initCutValues(graph, spanningTree) {
+ computeLowLim(spanningTree);
+
+ spanningTree.eachEdge(function(id, u, v, treeValue) {
+ treeValue.cutValue = 0;
+ });
+
+ // Propagate cut values up the tree.
+ function dfs(n) {
+ var children = spanningTree.successors(n);
+ for (var c in children) {
+ var child = children[c];
+ dfs(child);
+ }
+ if (n !== spanningTree.graph().root) {
+ setCutValue(graph, spanningTree, n);
+ }
+ }
+ dfs(spanningTree.graph().root);
+}
+
+/*
+ * Perform a DFS postorder traversal, labeling each node v with
+ * its traversal order 'lim(v)' and the minimum traversal number
+ * of any of its descendants 'low(v)'. This provides an efficient
+ * way to test whether u is an ancestor of v since
+ * low(u) <= lim(v) <= lim(u) if and only if u is an ancestor.
+ */
+function computeLowLim(tree) {
+ var postOrderNum = 0;
+
+ function dfs(n) {
+ var children = tree.successors(n);
+ var low = postOrderNum;
+ for (var c in children) {
+ var child = children[c];
+ dfs(child);
+ low = Math.min(low, tree.node(child).low);
+ }
+ tree.node(n).low = low;
+ tree.node(n).lim = postOrderNum++;
+ }
+
+ dfs(tree.graph().root);
+}
+
+/*
+ * To compute the cut value of the edge parent -> child, we consider
+ * it and any other graph edges to or from the child.
+ * parent
+ * |
+ * child
+ * / \
+ * u v
+ */
+function setCutValue(graph, tree, child) {
+ var parentEdge = tree.inEdges(child)[0];
+
+ // List of child's children in the spanning tree.
+ var grandchildren = [];
+ var grandchildEdges = tree.outEdges(child);
+ for (var gce in grandchildEdges) {
+ grandchildren.push(tree.target(grandchildEdges[gce]));
+ }
+
+ var cutValue = 0;
+
+ // TODO: Replace unit increment/decrement with edge weights.
+ var E = 0; // Edges from child to grandchild's subtree.
+ var F = 0; // Edges to child from grandchild's subtree.
+ var G = 0; // Edges from child to nodes outside of child's subtree.
+ var H = 0; // Edges from nodes outside of child's subtree to child.
+
+ // Consider all graph edges from child.
+ var outEdges = graph.outEdges(child);
+ var gc;
+ for (var oe in outEdges) {
+ var succ = graph.target(outEdges[oe]);
+ for (gc in grandchildren) {
+ if (inSubtree(tree, succ, grandchildren[gc])) {
+ E++;
+ }
+ }
+ if (!inSubtree(tree, succ, child)) {
+ G++;
+ }
+ }
+
+ // Consider all graph edges to child.
+ var inEdges = graph.inEdges(child);
+ for (var ie in inEdges) {
+ var pred = graph.source(inEdges[ie]);
+ for (gc in grandchildren) {
+ if (inSubtree(tree, pred, grandchildren[gc])) {
+ F++;
+ }
+ }
+ if (!inSubtree(tree, pred, child)) {
+ H++;
+ }
+ }
+
+ // Contributions depend on the alignment of the parent -> child edge
+ // and the child -> u or v edges.
+ var grandchildCutSum = 0;
+ for (gc in grandchildren) {
+ var cv = tree.edge(grandchildEdges[gc]).cutValue;
+ if (!tree.edge(grandchildEdges[gc]).reversed) {
+ grandchildCutSum += cv;
+ } else {
+ grandchildCutSum -= cv;
+ }
+ }
+
+ if (!tree.edge(parentEdge).reversed) {
+ cutValue += grandchildCutSum - E + F - G + H;
+ } else {
+ cutValue -= grandchildCutSum - E + F - G + H;
+ }
+
+ tree.edge(parentEdge).cutValue = cutValue;
+}
+
+/*
+ * Return whether n is a node in the subtree with the given
+ * root.
+ */
+function inSubtree(tree, n, root) {
+ return (tree.node(root).low <= tree.node(n).lim &&
+ tree.node(n).lim <= tree.node(root).lim);
+}
+
+/*
+ * Return an edge from the tree with a negative cut value, or null if there
+ * is none.
+ */
+function leaveEdge(tree) {
+ var edges = tree.edges();
+ for (var n in edges) {
+ var e = edges[n];
+ var treeValue = tree.edge(e);
+ if (treeValue.cutValue < 0) {
+ return e;
+ }
+ }
+ return null;
+}
+
+/*
+ * The edge e should be an edge in the tree, with an underlying edge
+ * in the graph, with a negative cut value. Of the two nodes incident
+ * on the edge, take the lower one. enterEdge returns an edge with
+ * minimum slack going from outside of that node's subtree to inside
+ * of that node's subtree.
+ */
+function enterEdge(graph, tree, e) {
+ var source = tree.source(e);
+ var target = tree.target(e);
+ var lower = tree.node(target).lim < tree.node(source).lim ? target : source;
+
+ // Is the tree edge aligned with the graph edge?
+ var aligned = !tree.edge(e).reversed;
+
+ var minSlack = Number.POSITIVE_INFINITY;
+ var minSlackEdge;
+ if (aligned) {
+ graph.eachEdge(function(id, u, v, value) {
+ if (id !== e && inSubtree(tree, u, lower) && !inSubtree(tree, v, lower)) {
+ var slack = rankUtil.slack(graph, u, v, value.minLen);
+ if (slack < minSlack) {
+ minSlack = slack;
+ minSlackEdge = id;
+ }
+ }
+ });
+ } else {
+ graph.eachEdge(function(id, u, v, value) {
+ if (id !== e && !inSubtree(tree, u, lower) && inSubtree(tree, v, lower)) {
+ var slack = rankUtil.slack(graph, u, v, value.minLen);
+ if (slack < minSlack) {
+ minSlack = slack;
+ minSlackEdge = id;
+ }
+ }
+ });
+ }
+
+ if (minSlackEdge === undefined) {
+ var outside = [];
+ var inside = [];
+ graph.eachNode(function(id) {
+ if (!inSubtree(tree, id, lower)) {
+ outside.push(id);
+ } else {
+ inside.push(id);
+ }
+ });
+ throw new Error('No edge found from outside of tree to inside');
+ }
+
+ return minSlackEdge;
+}
+
+/*
+ * Replace edge e with edge f in the tree, recalculating the tree root,
+ * the nodes' low and lim properties and the edges' cut values.
+ */
+function exchange(graph, tree, e, f) {
+ tree.delEdge(e);
+ var source = graph.source(f);
+ var target = graph.target(f);
+
+ // Redirect edges so that target is the root of its subtree.
+ function redirect(v) {
+ var edges = tree.inEdges(v);
+ for (var i in edges) {
+ var e = edges[i];
+ var u = tree.source(e);
+ var value = tree.edge(e);
+ redirect(u);
+ tree.delEdge(e);
+ value.reversed = !value.reversed;
+ tree.addEdge(e, v, u, value);
+ }
+ }
+
+ redirect(target);
+
+ var root = source;
+ var edges = tree.inEdges(root);
+ while (edges.length > 0) {
+ root = tree.source(edges[0]);
+ edges = tree.inEdges(root);
+ }
+
+ tree.graph().root = root;
+
+ tree.addEdge(null, source, target, {cutValue: 0});
+
+ initCutValues(graph, tree);
+
+ adjustRanks(graph, tree);
+}
+
+/*
+ * Reset the ranks of all nodes based on the current spanning tree.
+ * The rank of the tree's root remains unchanged, while all other
+ * nodes are set to the sum of minimum length constraints along
+ * the path from the root.
+ */
+function adjustRanks(graph, tree) {
+ function dfs(p) {
+ var children = tree.successors(p);
+ children.forEach(function(c) {
+ var minLen = minimumLength(graph, p, c);
+ graph.node(c).rank = graph.node(p).rank + minLen;
+ dfs(c);
+ });
+ }
+
+ dfs(tree.graph().root);
+}
+
+/*
+ * If u and v are connected by some edges in the graph, return the
+ * minimum length of those edges, as a positive number if v succeeds
+ * u and as a negative number if v precedes u.
+ */
+function minimumLength(graph, u, v) {
+ var outEdges = graph.outEdges(u, v);
+ if (outEdges.length > 0) {
+ return util.max(outEdges.map(function(e) {
+ return graph.edge(e).minLen;
+ }));
+ }
+
+ var inEdges = graph.inEdges(u, v);
+ if (inEdges.length > 0) {
+ return -util.max(inEdges.map(function(e) {
+ return graph.edge(e).minLen;
+ }));
+ }
+}
+
+},{"../util":26,"./rankUtil":24}],26:[function(require,module,exports){
+/*
+ * Returns the smallest value in the array.
+ */
+exports.min = function(values) {
+ return Math.min.apply(Math, values);
+};
+
+/*
+ * Returns the largest value in the array.
+ */
+exports.max = function(values) {
+ return Math.max.apply(Math, values);
+};
+
+/*
+ * Returns `true` only if `f(x)` is `true` for all `x` in `xs`. Otherwise
+ * returns `false`. This function will return immediately if it finds a
+ * case where `f(x)` does not hold.
+ */
+exports.all = function(xs, f) {
+ for (var i = 0; i < xs.length; ++i) {
+ if (!f(xs[i])) {
+ return false;
+ }
+ }
+ return true;
+};
+
+/*
+ * Accumulates the sum of elements in the given array using the `+` operator.
+ */
+exports.sum = function(values) {
+ return values.reduce(function(acc, x) { return acc + x; }, 0);
+};
+
+/*
+ * Returns an array of all values in the given object.
+ */
+exports.values = function(obj) {
+ return Object.keys(obj).map(function(k) { return obj[k]; });
+};
+
+exports.shuffle = function(array) {
+ for (i = array.length - 1; i > 0; --i) {
+ var j = Math.floor(Math.random() * (i + 1));
+ var aj = array[j];
+ array[j] = array[i];
+ array[i] = aj;
+ }
+};
+
+exports.propertyAccessor = function(self, config, field, setHook) {
+ return function(x) {
+ if (!arguments.length) return config[field];
+ config[field] = x;
+ if (setHook) setHook(x);
+ return self;
+ };
+};
+
+/*
+ * Given a layered, directed graph with `rank` and `order` node attributes,
+ * this function returns an array of ordered ranks. Each rank contains an array
+ * of the ids of the nodes in that rank in the order specified by the `order`
+ * attribute.
+ */
+exports.ordering = function(g) {
+ var ordering = [];
+ g.eachNode(function(u, value) {
+ var rank = ordering[value.rank] || (ordering[value.rank] = []);
+ rank[value.order] = u;
+ });
+ return ordering;
+};
+
+/*
+ * A filter that can be used with `filterNodes` to get a graph that only
+ * includes nodes that do not contain others nodes.
+ */
+exports.filterNonSubgraphs = function(g) {
+ return function(u) {
+ return g.children(u).length === 0;
+ };
+};
+
+/*
+ * Returns a new function that wraps `func` with a timer. The wrapper logs the
+ * time it takes to execute the function.
+ *
+ * The timer will be enabled provided `log.level >= 1`.
+ */
+function time(name, func) {
+ return function() {
+ var start = new Date().getTime();
+ try {
+ return func.apply(null, arguments);
+ } finally {
+ log(1, name + ' time: ' + (new Date().getTime() - start) + 'ms');
+ }
+ };
+}
+time.enabled = false;
+
+exports.time = time;
+
+/*
+ * A global logger with the specification `log(level, message, ...)` that
+ * will log a message to the console if `log.level >= level`.
+ */
+function log(level) {
+ if (log.level >= level) {
+ console.log.apply(console, Array.prototype.slice.call(arguments, 1));
+ }
+}
+log.level = 0;
+
+exports.log = log;
+
+},{}],27:[function(require,module,exports){
+module.exports = '0.4.5';
+
+},{}],28:[function(require,module,exports){
+exports.Graph = require("./lib/Graph");
+exports.Digraph = require("./lib/Digraph");
+exports.CGraph = require("./lib/CGraph");
+exports.CDigraph = require("./lib/CDigraph");
+require("./lib/graph-converters");
+
+exports.alg = {
+ isAcyclic: require("./lib/alg/isAcyclic"),
+ components: require("./lib/alg/components"),
+ dijkstra: require("./lib/alg/dijkstra"),
+ dijkstraAll: require("./lib/alg/dijkstraAll"),
+ findCycles: require("./lib/alg/findCycles"),
+ floydWarshall: require("./lib/alg/floydWarshall"),
+ postorder: require("./lib/alg/postorder"),
+ preorder: require("./lib/alg/preorder"),
+ prim: require("./lib/alg/prim"),
+ tarjan: require("./lib/alg/tarjan"),
+ topsort: require("./lib/alg/topsort")
+};
+
+exports.converter = {
+ json: require("./lib/converter/json.js")
+};
+
+var filter = require("./lib/filter");
+exports.filter = {
+ all: filter.all,
+ nodesFromList: filter.nodesFromList
+};
+
+exports.version = require("./lib/version");
+
+},{"./lib/CDigraph":30,"./lib/CGraph":31,"./lib/Digraph":32,"./lib/Graph":33,"./lib/alg/components":34,"./lib/alg/dijkstra":35,"./lib/alg/dijkstraAll":36,"./lib/alg/findCycles":37,"./lib/alg/floydWarshall":38,"./lib/alg/isAcyclic":39,"./lib/alg/postorder":40,"./lib/alg/preorder":41,"./lib/alg/prim":42,"./lib/alg/tarjan":43,"./lib/alg/topsort":44,"./lib/converter/json.js":46,"./lib/filter":47,"./lib/graph-converters":48,"./lib/version":50}],29:[function(require,module,exports){
+/* jshint -W079 */
+var Set = require("cp-data").Set;
+/* jshint +W079 */
+
+module.exports = BaseGraph;
+
+function BaseGraph() {
+ // The value assigned to the graph itself.
+ this._value = undefined;
+
+ // Map of node id -> { id, value }
+ this._nodes = {};
+
+ // Map of edge id -> { id, u, v, value }
+ this._edges = {};
+
+ // Used to generate a unique id in the graph
+ this._nextId = 0;
+}
+
+// Number of nodes
+BaseGraph.prototype.order = function() {
+ return Object.keys(this._nodes).length;
+};
+
+// Number of edges
+BaseGraph.prototype.size = function() {
+ return Object.keys(this._edges).length;
+};
+
+// Accessor for graph level value
+BaseGraph.prototype.graph = function(value) {
+ if (arguments.length === 0) {
+ return this._value;
+ }
+ this._value = value;
+};
+
+BaseGraph.prototype.hasNode = function(u) {
+ return u in this._nodes;
+};
+
+BaseGraph.prototype.node = function(u, value) {
+ var node = this._strictGetNode(u);
+ if (arguments.length === 1) {
+ return node.value;
+ }
+ node.value = value;
+};
+
+BaseGraph.prototype.nodes = function() {
+ var nodes = [];
+ this.eachNode(function(id) { nodes.push(id); });
+ return nodes;
+};
+
+BaseGraph.prototype.eachNode = function(func) {
+ for (var k in this._nodes) {
+ var node = this._nodes[k];
+ func(node.id, node.value);
+ }
+};
+
+BaseGraph.prototype.hasEdge = function(e) {
+ return e in this._edges;
+};
+
+BaseGraph.prototype.edge = function(e, value) {
+ var edge = this._strictGetEdge(e);
+ if (arguments.length === 1) {
+ return edge.value;
+ }
+ edge.value = value;
+};
+
+BaseGraph.prototype.edges = function() {
+ var es = [];
+ this.eachEdge(function(id) { es.push(id); });
+ return es;
+};
+
+BaseGraph.prototype.eachEdge = function(func) {
+ for (var k in this._edges) {
+ var edge = this._edges[k];
+ func(edge.id, edge.u, edge.v, edge.value);
+ }
+};
+
+BaseGraph.prototype.incidentNodes = function(e) {
+ var edge = this._strictGetEdge(e);
+ return [edge.u, edge.v];
+};
+
+BaseGraph.prototype.addNode = function(u, value) {
+ if (u === undefined || u === null) {
+ do {
+ u = "_" + (++this._nextId);
+ } while (this.hasNode(u));
+ } else if (this.hasNode(u)) {
+ throw new Error("Graph already has node '" + u + "'");
+ }
+ this._nodes[u] = { id: u, value: value };
+ return u;
+};
+
+BaseGraph.prototype.delNode = function(u) {
+ this._strictGetNode(u);
+ this.incidentEdges(u).forEach(function(e) { this.delEdge(e); }, this);
+ delete this._nodes[u];
+};
+
+// inMap and outMap are opposite sides of an incidence map. For example, for
+// Graph these would both come from the _incidentEdges map, while for Digraph
+// they would come from _inEdges and _outEdges.
+BaseGraph.prototype._addEdge = function(e, u, v, value, inMap, outMap) {
+ this._strictGetNode(u);
+ this._strictGetNode(v);
+
+ if (e === undefined || e === null) {
+ do {
+ e = "_" + (++this._nextId);
+ } while (this.hasEdge(e));
+ }
+ else if (this.hasEdge(e)) {
+ throw new Error("Graph already has edge '" + e + "'");
+ }
+
+ this._edges[e] = { id: e, u: u, v: v, value: value };
+ addEdgeToMap(inMap[v], u, e);
+ addEdgeToMap(outMap[u], v, e);
+
+ return e;
+};
+
+// See note for _addEdge regarding inMap and outMap.
+BaseGraph.prototype._delEdge = function(e, inMap, outMap) {
+ var edge = this._strictGetEdge(e);
+ delEdgeFromMap(inMap[edge.v], edge.u, e);
+ delEdgeFromMap(outMap[edge.u], edge.v, e);
+ delete this._edges[e];
+};
+
+BaseGraph.prototype.copy = function() {
+ var copy = new this.constructor();
+ copy.graph(this.graph());
+ this.eachNode(function(u, value) { copy.addNode(u, value); });
+ this.eachEdge(function(e, u, v, value) { copy.addEdge(e, u, v, value); });
+ copy._nextId = this._nextId;
+ return copy;
+};
+
+BaseGraph.prototype.filterNodes = function(filter) {
+ var copy = new this.constructor();
+ copy.graph(this.graph());
+ this.eachNode(function(u, value) {
+ if (filter(u)) {
+ copy.addNode(u, value);
+ }
+ });
+ this.eachEdge(function(e, u, v, value) {
+ if (copy.hasNode(u) && copy.hasNode(v)) {
+ copy.addEdge(e, u, v, value);
+ }
+ });
+ return copy;
+};
+
+BaseGraph.prototype._strictGetNode = function(u) {
+ var node = this._nodes[u];
+ if (node === undefined) {
+ throw new Error("Node '" + u + "' is not in graph");
+ }
+ return node;
+};
+
+BaseGraph.prototype._strictGetEdge = function(e) {
+ var edge = this._edges[e];
+ if (edge === undefined) {
+ throw new Error("Edge '" + e + "' is not in graph");
+ }
+ return edge;
+};
+
+function addEdgeToMap(map, v, e) {
+ (map[v] || (map[v] = new Set())).add(e);
+}
+
+function delEdgeFromMap(map, v, e) {
+ var vEntry = map[v];
+ vEntry.remove(e);
+ if (vEntry.size() === 0) {
+ delete map[v];
+ }
+}
+
+
+},{"cp-data":5}],30:[function(require,module,exports){
+var Digraph = require("./Digraph"),
+ compoundify = require("./compoundify");
+
+var CDigraph = compoundify(Digraph);
+
+module.exports = CDigraph;
+
+CDigraph.fromDigraph = function(src) {
+ var g = new CDigraph(),
+ graphValue = src.graph();
+
+ if (graphValue !== undefined) {
+ g.graph(graphValue);
+ }
+
+ src.eachNode(function(u, value) {
+ if (value === undefined) {
+ g.addNode(u);
+ } else {
+ g.addNode(u, value);
+ }
+ });
+ src.eachEdge(function(e, u, v, value) {
+ if (value === undefined) {
+ g.addEdge(null, u, v);
+ } else {
+ g.addEdge(null, u, v, value);
+ }
+ });
+ return g;
+};
+
+CDigraph.prototype.toString = function() {
+ return "CDigraph " + JSON.stringify(this, null, 2);
+};
+
+},{"./Digraph":32,"./compoundify":45}],31:[function(require,module,exports){
+var Graph = require("./Graph"),
+ compoundify = require("./compoundify");
+
+var CGraph = compoundify(Graph);
+
+module.exports = CGraph;
+
+CGraph.fromGraph = function(src) {
+ var g = new CGraph(),
+ graphValue = src.graph();
+
+ if (graphValue !== undefined) {
+ g.graph(graphValue);
+ }
+
+ src.eachNode(function(u, value) {
+ if (value === undefined) {
+ g.addNode(u);
+ } else {
+ g.addNode(u, value);
+ }
+ });
+ src.eachEdge(function(e, u, v, value) {
+ if (value === undefined) {
+ g.addEdge(null, u, v);
+ } else {
+ g.addEdge(null, u, v, value);
+ }
+ });
+ return g;
+};
+
+CGraph.prototype.toString = function() {
+ return "CGraph " + JSON.stringify(this, null, 2);
+};
+
+},{"./Graph":33,"./compoundify":45}],32:[function(require,module,exports){
+/*
+ * This file is organized with in the following order:
+ *
+ * Exports
+ * Graph constructors
+ * Graph queries (e.g. nodes(), edges()
+ * Graph mutators
+ * Helper functions
+ */
+
+var util = require("./util"),
+ BaseGraph = require("./BaseGraph"),
+/* jshint -W079 */
+ Set = require("cp-data").Set;
+/* jshint +W079 */
+
+module.exports = Digraph;
+
+/*
+ * Constructor to create a new directed multi-graph.
+ */
+function Digraph() {
+ BaseGraph.call(this);
+
+ /*! Map of sourceId -> {targetId -> Set of edge ids} */
+ this._inEdges = {};
+
+ /*! Map of targetId -> {sourceId -> Set of edge ids} */
+ this._outEdges = {};
+}
+
+Digraph.prototype = new BaseGraph();
+Digraph.prototype.constructor = Digraph;
+
+/*
+ * Always returns `true`.
+ */
+Digraph.prototype.isDirected = function() {
+ return true;
+};
+
+/*
+ * Returns all successors of the node with the id `u`. That is, all nodes
+ * that have the node `u` as their source are returned.
+ *
+ * If no node `u` exists in the graph this function throws an Error.
+ *
+ * @param {String} u a node id
+ */
+Digraph.prototype.successors = function(u) {
+ this._strictGetNode(u);
+ return Object.keys(this._outEdges[u])
+ .map(function(v) { return this._nodes[v].id; }, this);
+};
+
+/*
+ * Returns all predecessors of the node with the id `u`. That is, all nodes
+ * that have the node `u` as their target are returned.
+ *
+ * If no node `u` exists in the graph this function throws an Error.
+ *
+ * @param {String} u a node id
+ */
+Digraph.prototype.predecessors = function(u) {
+ this._strictGetNode(u);
+ return Object.keys(this._inEdges[u])
+ .map(function(v) { return this._nodes[v].id; }, this);
+};
+
+/*
+ * Returns all nodes that are adjacent to the node with the id `u`. In other
+ * words, this function returns the set of all successors and predecessors of
+ * node `u`.
+ *
+ * @param {String} u a node id
+ */
+Digraph.prototype.neighbors = function(u) {
+ return Set.union([this.successors(u), this.predecessors(u)]).keys();
+};
+
+/*
+ * Returns all nodes in the graph that have no in-edges.
+ */
+Digraph.prototype.sources = function() {
+ var self = this;
+ return this._filterNodes(function(u) {
+ // This could have better space characteristics if we had an inDegree function.
+ return self.inEdges(u).length === 0;
+ });
+};
+
+/*
+ * Returns all nodes in the graph that have no out-edges.
+ */
+Digraph.prototype.sinks = function() {
+ var self = this;
+ return this._filterNodes(function(u) {
+ // This could have better space characteristics if we have an outDegree function.
+ return self.outEdges(u).length === 0;
+ });
+};
+
+/*
+ * Returns the source node incident on the edge identified by the id `e`. If no
+ * such edge exists in the graph this function throws an Error.
+ *
+ * @param {String} e an edge id
+ */
+Digraph.prototype.source = function(e) {
+ return this._strictGetEdge(e).u;
+};
+
+/*
+ * Returns the target node incident on the edge identified by the id `e`. If no
+ * such edge exists in the graph this function throws an Error.
+ *
+ * @param {String} e an edge id
+ */
+Digraph.prototype.target = function(e) {
+ return this._strictGetEdge(e).v;
+};
+
+/*
+ * Returns an array of ids for all edges in the graph that have the node
+ * `target` as their target. If the node `target` is not in the graph this
+ * function raises an Error.
+ *
+ * Optionally a `source` node can also be specified. This causes the results
+ * to be filtered such that only edges from `source` to `target` are included.
+ * If the node `source` is specified but is not in the graph then this function
+ * raises an Error.
+ *
+ * @param {String} target the target node id
+ * @param {String} [source] an optional source node id
+ */
+Digraph.prototype.inEdges = function(target, source) {
+ this._strictGetNode(target);
+ var results = Set.union(util.values(this._inEdges[target])).keys();
+ if (arguments.length > 1) {
+ this._strictGetNode(source);
+ results = results.filter(function(e) { return this.source(e) === source; }, this);
+ }
+ return results;
+};
+
+/*
+ * Returns an array of ids for all edges in the graph that have the node
+ * `source` as their source. If the node `source` is not in the graph this
+ * function raises an Error.
+ *
+ * Optionally a `target` node may also be specified. This causes the results
+ * to be filtered such that only edges from `source` to `target` are included.
+ * If the node `target` is specified but is not in the graph then this function
+ * raises an Error.
+ *
+ * @param {String} source the source node id
+ * @param {String} [target] an optional target node id
+ */
+Digraph.prototype.outEdges = function(source, target) {
+ this._strictGetNode(source);
+ var results = Set.union(util.values(this._outEdges[source])).keys();
+ if (arguments.length > 1) {
+ this._strictGetNode(target);
+ results = results.filter(function(e) { return this.target(e) === target; }, this);
+ }
+ return results;
+};
+
+/*
+ * Returns an array of ids for all edges in the graph that have the `u` as
+ * their source or their target. If the node `u` is not in the graph this
+ * function raises an Error.
+ *
+ * Optionally a `v` node may also be specified. This causes the results to be
+ * filtered such that only edges between `u` and `v` - in either direction -
+ * are included. IF the node `v` is specified but not in the graph then this
+ * function raises an Error.
+ *
+ * @param {String} u the node for which to find incident edges
+ * @param {String} [v] option node that must be adjacent to `u`
+ */
+Digraph.prototype.incidentEdges = function(u, v) {
+ if (arguments.length > 1) {
+ return Set.union([this.outEdges(u, v), this.outEdges(v, u)]).keys();
+ } else {
+ return Set.union([this.inEdges(u), this.outEdges(u)]).keys();
+ }
+};
+
+/*
+ * Returns a string representation of this graph.
+ */
+Digraph.prototype.toString = function() {
+ return "Digraph " + JSON.stringify(this, null, 2);
+};
+
+/*
+ * Adds a new node with the id `u` to the graph and assigns it the value
+ * `value`. If a node with the id is already a part of the graph this function
+ * throws an Error.
+ *
+ * @param {String} u a node id
+ * @param {Object} [value] an optional value to attach to the node
+ */
+Digraph.prototype.addNode = function(u, value) {
+ u = BaseGraph.prototype.addNode.call(this, u, value);
+ this._inEdges[u] = {};
+ this._outEdges[u] = {};
+ return u;
+};
+
+/*
+ * Removes a node from the graph that has the id `u`. Any edges incident on the
+ * node are also removed. If the graph does not contain a node with the id this
+ * function will throw an Error.
+ *
+ * @param {String} u a node id
+ */
+Digraph.prototype.delNode = function(u) {
+ BaseGraph.prototype.delNode.call(this, u);
+ delete this._inEdges[u];
+ delete this._outEdges[u];
+};
+
+/*
+ * Adds a new edge to the graph with the id `e` from a node with the id `source`
+ * to a node with an id `target` and assigns it the value `value`. This graph
+ * allows more than one edge from `source` to `target` as long as the id `e`
+ * is unique in the set of edges. If `e` is `null` the graph will assign a
+ * unique identifier to the edge.
+ *
+ * If `source` or `target` are not present in the graph this function will
+ * throw an Error.
+ *
+ * @param {String} [e] an edge id
+ * @param {String} source the source node id
+ * @param {String} target the target node id
+ * @param {Object} [value] an optional value to attach to the edge
+ */
+Digraph.prototype.addEdge = function(e, source, target, value) {
+ return BaseGraph.prototype._addEdge.call(this, e, source, target, value,
+ this._inEdges, this._outEdges);
+};
+
+/*
+ * Removes an edge in the graph with the id `e`. If no edge in the graph has
+ * the id `e` this function will throw an Error.
+ *
+ * @param {String} e an edge id
+ */
+Digraph.prototype.delEdge = function(e) {
+ BaseGraph.prototype._delEdge.call(this, e, this._inEdges, this._outEdges);
+};
+
+// Unlike BaseGraph.filterNodes, this helper just returns nodes that
+// satisfy a predicate.
+Digraph.prototype._filterNodes = function(pred) {
+ var filtered = [];
+ this.eachNode(function(u) {
+ if (pred(u)) {
+ filtered.push(u);
+ }
+ });
+ return filtered;
+};
+
+
+},{"./BaseGraph":29,"./util":49,"cp-data":5}],33:[function(require,module,exports){
+/*
+ * This file is organized with in the following order:
+ *
+ * Exports
+ * Graph constructors
+ * Graph queries (e.g. nodes(), edges()
+ * Graph mutators
+ * Helper functions
+ */
+
+var util = require("./util"),
+ BaseGraph = require("./BaseGraph"),
+/* jshint -W079 */
+ Set = require("cp-data").Set;
+/* jshint +W079 */
+
+module.exports = Graph;
+
+/*
+ * Constructor to create a new undirected multi-graph.
+ */
+function Graph() {
+ BaseGraph.call(this);
+
+ /*! Map of nodeId -> { otherNodeId -> Set of edge ids } */
+ this._incidentEdges = {};
+}
+
+Graph.prototype = new BaseGraph();
+Graph.prototype.constructor = Graph;
+
+/*
+ * Always returns `false`.
+ */
+Graph.prototype.isDirected = function() {
+ return false;
+};
+
+/*
+ * Returns all nodes that are adjacent to the node with the id `u`.
+ *
+ * @param {String} u a node id
+ */
+Graph.prototype.neighbors = function(u) {
+ this._strictGetNode(u);
+ return Object.keys(this._incidentEdges[u])
+ .map(function(v) { return this._nodes[v].id; }, this);
+};
+
+/*
+ * Returns an array of ids for all edges in the graph that are incident on `u`.
+ * If the node `u` is not in the graph this function raises an Error.
+ *
+ * Optionally a `v` node may also be specified. This causes the results to be
+ * filtered such that only edges between `u` and `v` are included. If the node
+ * `v` is specified but not in the graph then this function raises an Error.
+ *
+ * @param {String} u the node for which to find incident edges
+ * @param {String} [v] option node that must be adjacent to `u`
+ */
+Graph.prototype.incidentEdges = function(u, v) {
+ this._strictGetNode(u);
+ if (arguments.length > 1) {
+ this._strictGetNode(v);
+ return v in this._incidentEdges[u] ? this._incidentEdges[u][v].keys() : [];
+ } else {
+ return Set.union(util.values(this._incidentEdges[u])).keys();
+ }
+};
+
+/*
+ * Returns a string representation of this graph.
+ */
+Graph.prototype.toString = function() {
+ return "Graph " + JSON.stringify(this, null, 2);
+};
+
+/*
+ * Adds a new node with the id `u` to the graph and assigns it the value
+ * `value`. If a node with the id is already a part of the graph this function
+ * throws an Error.
+ *
+ * @param {String} u a node id
+ * @param {Object} [value] an optional value to attach to the node
+ */
+Graph.prototype.addNode = function(u, value) {
+ u = BaseGraph.prototype.addNode.call(this, u, value);
+ this._incidentEdges[u] = {};
+ return u;
+};
+
+/*
+ * Removes a node from the graph that has the id `u`. Any edges incident on the
+ * node are also removed. If the graph does not contain a node with the id this
+ * function will throw an Error.
+ *
+ * @param {String} u a node id
+ */
+Graph.prototype.delNode = function(u) {
+ BaseGraph.prototype.delNode.call(this, u);
+ delete this._incidentEdges[u];
+};
+
+/*
+ * Adds a new edge to the graph with the id `e` between a node with the id `u`
+ * and a node with an id `v` and assigns it the value `value`. This graph
+ * allows more than one edge between `u` and `v` as long as the id `e`
+ * is unique in the set of edges. If `e` is `null` the graph will assign a
+ * unique identifier to the edge.
+ *
+ * If `u` or `v` are not present in the graph this function will throw an
+ * Error.
+ *
+ * @param {String} [e] an edge id
+ * @param {String} u the node id of one of the adjacent nodes
+ * @param {String} v the node id of the other adjacent node
+ * @param {Object} [value] an optional value to attach to the edge
+ */
+Graph.prototype.addEdge = function(e, u, v, value) {
+ return BaseGraph.prototype._addEdge.call(this, e, u, v, value,
+ this._incidentEdges, this._incidentEdges);
+};
+
+/*
+ * Removes an edge in the graph with the id `e`. If no edge in the graph has
+ * the id `e` this function will throw an Error.
+ *
+ * @param {String} e an edge id
+ */
+Graph.prototype.delEdge = function(e) {
+ BaseGraph.prototype._delEdge.call(this, e, this._incidentEdges, this._incidentEdges);
+};
+
+
+},{"./BaseGraph":29,"./util":49,"cp-data":5}],34:[function(require,module,exports){
+/* jshint -W079 */
+var Set = require("cp-data").Set;
+/* jshint +W079 */
+
+module.exports = components;
+
+/**
+ * Finds all [connected components][] in a graph and returns an array of these
+ * components. Each component is itself an array that contains the ids of nodes
+ * in the component.
+ *
+ * This function only works with undirected Graphs.
+ *
+ * [connected components]: http://en.wikipedia.org/wiki/Connected_component_(graph_theory)
+ *
+ * @param {Graph} g the graph to search for components
+ */
+function components(g) {
+ var results = [];
+ var visited = new Set();
+
+ function dfs(v, component) {
+ if (!visited.has(v)) {
+ visited.add(v);
+ component.push(v);
+ g.neighbors(v).forEach(function(w) {
+ dfs(w, component);
+ });
+ }
+ }
+
+ g.nodes().forEach(function(v) {
+ var component = [];
+ dfs(v, component);
+ if (component.length > 0) {
+ results.push(component);
+ }
+ });
+
+ return results;
+}
+
+},{"cp-data":5}],35:[function(require,module,exports){
+var PriorityQueue = require("cp-data").PriorityQueue;
+
+module.exports = dijkstra;
+
+/**
+ * This function is an implementation of [Dijkstra's algorithm][] which finds
+ * the shortest path from **source** to all other nodes in **g**. This
+ * function returns a map of `u -> { distance, predecessor }`. The distance
+ * property holds the sum of the weights from **source** to `u` along the
+ * shortest path or `Number.POSITIVE_INFINITY` if there is no path from
+ * **source**. The predecessor property can be used to walk the individual
+ * elements of the path from **source** to **u** in reverse order.
+ *
+ * This function takes an optional `weightFunc(e)` which returns the
+ * weight of the edge `e`. If no weightFunc is supplied then each edge is
+ * assumed to have a weight of 1. This function throws an Error if any of
+ * the traversed edges have a negative edge weight.
+ *
+ * This function takes an optional `incidentFunc(u)` which returns the ids of
+ * all edges incident to the node `u` for the purposes of shortest path
+ * traversal. By default this function uses the `g.outEdges` for Digraphs and
+ * `g.incidentEdges` for Graphs.
+ *
+ * This function takes `O((|E| + |V|) * log |V|)` time.
+ *
+ * [Dijkstra's algorithm]: http://en.wikipedia.org/wiki/Dijkstra%27s_algorithm
+ *
+ * @param {Graph} g the graph to search for shortest paths from **source**
+ * @param {Object} source the source from which to start the search
+ * @param {Function} [weightFunc] optional weight function
+ * @param {Function} [incidentFunc] optional incident function
+ */
+function dijkstra(g, source, weightFunc, incidentFunc) {
+ var results = {},
+ pq = new PriorityQueue();
+
+ function updateNeighbors(e) {
+ var incidentNodes = g.incidentNodes(e),
+ v = incidentNodes[0] !== u ? incidentNodes[0] : incidentNodes[1],
+ vEntry = results[v],
+ weight = weightFunc(e),
+ distance = uEntry.distance + weight;
+
+ if (weight < 0) {
+ throw new Error("dijkstra does not allow negative edge weights. Bad edge: " + e + " Weight: " + weight);
+ }
+
+ if (distance < vEntry.distance) {
+ vEntry.distance = distance;
+ vEntry.predecessor = u;
+ pq.decrease(v, distance);
+ }
+ }
+
+ weightFunc = weightFunc || function() { return 1; };
+ incidentFunc = incidentFunc || (g.isDirected()
+ ? function(u) { return g.outEdges(u); }
+ : function(u) { return g.incidentEdges(u); });
+
+ g.eachNode(function(u) {
+ var distance = u === source ? 0 : Number.POSITIVE_INFINITY;
+ results[u] = { distance: distance };
+ pq.add(u, distance);
+ });
+
+ var u, uEntry;
+ while (pq.size() > 0) {
+ u = pq.removeMin();
+ uEntry = results[u];
+ if (uEntry.distance === Number.POSITIVE_INFINITY) {
+ break;
+ }
+
+ incidentFunc(u).forEach(updateNeighbors);
+ }
+
+ return results;
+}
+
+},{"cp-data":5}],36:[function(require,module,exports){
+var dijkstra = require("./dijkstra");
+
+module.exports = dijkstraAll;
+
+/**
+ * This function finds the shortest path from each node to every other
+ * reachable node in the graph. It is similar to [alg.dijkstra][], but
+ * instead of returning a single-source array, it returns a mapping of
+ * of `source -> alg.dijksta(g, source, weightFunc, incidentFunc)`.
+ *
+ * This function takes an optional `weightFunc(e)` which returns the
+ * weight of the edge `e`. If no weightFunc is supplied then each edge is
+ * assumed to have a weight of 1. This function throws an Error if any of
+ * the traversed edges have a negative edge weight.
+ *
+ * This function takes an optional `incidentFunc(u)` which returns the ids of
+ * all edges incident to the node `u` for the purposes of shortest path
+ * traversal. By default this function uses the `outEdges` function on the
+ * supplied graph.
+ *
+ * This function takes `O(|V| * (|E| + |V|) * log |V|)` time.
+ *
+ * [alg.dijkstra]: dijkstra.js.html#dijkstra
+ *
+ * @param {Graph} g the graph to search for shortest paths from **source**
+ * @param {Function} [weightFunc] optional weight function
+ * @param {Function} [incidentFunc] optional incident function
+ */
+function dijkstraAll(g, weightFunc, incidentFunc) {
+ var results = {};
+ g.eachNode(function(u) {
+ results[u] = dijkstra(g, u, weightFunc, incidentFunc);
+ });
+ return results;
+}
+
+},{"./dijkstra":35}],37:[function(require,module,exports){
+var tarjan = require("./tarjan");
+
+module.exports = findCycles;
+
+/*
+ * Given a Digraph **g** this function returns all nodes that are part of a
+ * cycle. Since there may be more than one cycle in a graph this function
+ * returns an array of these cycles, where each cycle is itself represented
+ * by an array of ids for each node involved in that cycle.
+ *
+ * [alg.isAcyclic][] is more efficient if you only need to determine whether
+ * a graph has a cycle or not.
+ *
+ * [alg.isAcyclic]: isAcyclic.js.html#isAcyclic
+ *
+ * @param {Digraph} g the graph to search for cycles.
+ */
+function findCycles(g) {
+ return tarjan(g).filter(function(cmpt) { return cmpt.length > 1; });
+}
+
+},{"./tarjan":43}],38:[function(require,module,exports){
+module.exports = floydWarshall;
+
+/**
+ * This function is an implementation of the [Floyd-Warshall algorithm][],
+ * which finds the shortest path from each node to every other reachable node
+ * in the graph. It is similar to [alg.dijkstraAll][], but it handles negative
+ * edge weights and is more efficient for some types of graphs. This function
+ * returns a map of `source -> { target -> { distance, predecessor }`. The
+ * distance property holds the sum of the weights from `source` to `target`
+ * along the shortest path of `Number.POSITIVE_INFINITY` if there is no path
+ * from `source`. The predecessor property can be used to walk the individual
+ * elements of the path from `source` to `target` in reverse order.
+ *
+ * This function takes an optional `weightFunc(e)` which returns the
+ * weight of the edge `e`. If no weightFunc is supplied then each edge is
+ * assumed to have a weight of 1.
+ *
+ * This function takes an optional `incidentFunc(u)` which returns the ids of
+ * all edges incident to the node `u` for the purposes of shortest path
+ * traversal. By default this function uses the `outEdges` function on the
+ * supplied graph.
+ *
+ * This algorithm takes O(|V|^3) time.
+ *
+ * [Floyd-Warshall algorithm]: https://en.wikipedia.org/wiki/Floyd-Warshall_algorithm
+ * [alg.dijkstraAll]: dijkstraAll.js.html#dijkstraAll
+ *
+ * @param {Graph} g the graph to search for shortest paths from **source**
+ * @param {Function} [weightFunc] optional weight function
+ * @param {Function} [incidentFunc] optional incident function
+ */
+function floydWarshall(g, weightFunc, incidentFunc) {
+ var results = {},
+ nodes = g.nodes();
+
+ weightFunc = weightFunc || function() { return 1; };
+ incidentFunc = incidentFunc || (g.isDirected()
+ ? function(u) { return g.outEdges(u); }
+ : function(u) { return g.incidentEdges(u); });
+
+ nodes.forEach(function(u) {
+ results[u] = {};
+ results[u][u] = { distance: 0 };
+ nodes.forEach(function(v) {
+ if (u !== v) {
+ results[u][v] = { distance: Number.POSITIVE_INFINITY };
+ }
+ });
+ incidentFunc(u).forEach(function(e) {
+ var incidentNodes = g.incidentNodes(e),
+ v = incidentNodes[0] !== u ? incidentNodes[0] : incidentNodes[1],
+ d = weightFunc(e);
+ if (d < results[u][v].distance) {
+ results[u][v] = { distance: d, predecessor: u };
+ }
+ });
+ });
+
+ nodes.forEach(function(k) {
+ var rowK = results[k];
+ nodes.forEach(function(i) {
+ var rowI = results[i];
+ nodes.forEach(function(j) {
+ var ik = rowI[k];
+ var kj = rowK[j];
+ var ij = rowI[j];
+ var altDistance = ik.distance + kj.distance;
+ if (altDistance < ij.distance) {
+ ij.distance = altDistance;
+ ij.predecessor = kj.predecessor;
+ }
+ });
+ });
+ });
+
+ return results;
+}
+
+},{}],39:[function(require,module,exports){
+var topsort = require("./topsort");
+
+module.exports = isAcyclic;
+
+/*
+ * Given a Digraph **g** this function returns `true` if the graph has no
+ * cycles and returns `false` if it does. This algorithm returns as soon as it
+ * detects the first cycle.
+ *
+ * Use [alg.findCycles][] if you need the actual list of cycles in a graph.
+ *
+ * [alg.findCycles]: findCycles.js.html#findCycles
+ *
+ * @param {Digraph} g the graph to test for cycles
+ */
+function isAcyclic(g) {
+ try {
+ topsort(g);
+ } catch (e) {
+ if (e instanceof topsort.CycleException) return false;
+ throw e;
+ }
+ return true;
+}
+
+},{"./topsort":44}],40:[function(require,module,exports){
+/* jshint -W079 */
+var Set = require("cp-data").Set;
+/* jshint +W079 */
+
+module.exports = postorder;
+
+// Postorder traversal of g, calling f for each visited node. Assumes the graph
+// is a tree.
+function postorder(g, root, f) {
+ var visited = new Set();
+ if (g.isDirected()) {
+ throw new Error("This function only works for undirected graphs");
+ }
+ function dfs(u, prev) {
+ if (visited.has(u)) {
+ throw new Error("The input graph is not a tree: " + g);
+ }
+ visited.add(u);
+ g.neighbors(u).forEach(function(v) {
+ if (v !== prev) dfs(v, u);
+ });
+ f(u);
+ }
+ dfs(root);
+}
+
+},{"cp-data":5}],41:[function(require,module,exports){
+/* jshint -W079 */
+var Set = require("cp-data").Set;
+/* jshint +W079 */
+
+module.exports = preorder;
+
+// Preorder traversal of g, calling f for each visited node. Assumes the graph
+// is a tree.
+function preorder(g, root, f) {
+ var visited = new Set();
+ if (g.isDirected()) {
+ throw new Error("This function only works for undirected graphs");
+ }
+ function dfs(u, prev) {
+ if (visited.has(u)) {
+ throw new Error("The input graph is not a tree: " + g);
+ }
+ visited.add(u);
+ f(u);
+ g.neighbors(u).forEach(function(v) {
+ if (v !== prev) dfs(v, u);
+ });
+ }
+ dfs(root);
+}
+
+},{"cp-data":5}],42:[function(require,module,exports){
+var Graph = require("../Graph"),
+ PriorityQueue = require("cp-data").PriorityQueue;
+
+module.exports = prim;
+
+/**
+ * [Prim's algorithm][] takes a connected undirected graph and generates a
+ * [minimum spanning tree][]. This function returns the minimum spanning
+ * tree as an undirected graph. This algorithm is derived from the description
+ * in "Introduction to Algorithms", Third Edition, Cormen, et al., Pg 634.
+ *
+ * This function takes a `weightFunc(e)` which returns the weight of the edge
+ * `e`. It throws an Error if the graph is not connected.
+ *
+ * This function takes `O(|E| log |V|)` time.
+ *
+ * [Prim's algorithm]: https://en.wikipedia.org/wiki/Prim's_algorithm
+ * [minimum spanning tree]: https://en.wikipedia.org/wiki/Minimum_spanning_tree
+ *
+ * @param {Graph} g the graph used to generate the minimum spanning tree
+ * @param {Function} weightFunc the weight function to use
+ */
+function prim(g, weightFunc) {
+ var result = new Graph(),
+ parents = {},
+ pq = new PriorityQueue(),
+ u;
+
+ function updateNeighbors(e) {
+ var incidentNodes = g.incidentNodes(e),
+ v = incidentNodes[0] !== u ? incidentNodes[0] : incidentNodes[1],
+ pri = pq.priority(v);
+ if (pri !== undefined) {
+ var edgeWeight = weightFunc(e);
+ if (edgeWeight < pri) {
+ parents[v] = u;
+ pq.decrease(v, edgeWeight);
+ }
+ }
+ }
+
+ if (g.order() === 0) {
+ return result;
+ }
+
+ g.eachNode(function(u) {
+ pq.add(u, Number.POSITIVE_INFINITY);
+ result.addNode(u);
+ });
+
+ // Start from an arbitrary node
+ pq.decrease(g.nodes()[0], 0);
+
+ var init = false;
+ while (pq.size() > 0) {
+ u = pq.removeMin();
+ if (u in parents) {
+ result.addEdge(null, u, parents[u]);
+ } else if (init) {
+ throw new Error("Input graph is not connected: " + g);
+ } else {
+ init = true;
+ }
+
+ g.incidentEdges(u).forEach(updateNeighbors);
+ }
+
+ return result;
+}
+
+},{"../Graph":33,"cp-data":5}],43:[function(require,module,exports){
+module.exports = tarjan;
+
+/**
+ * This function is an implementation of [Tarjan's algorithm][] which finds
+ * all [strongly connected components][] in the directed graph **g**. Each
+ * strongly connected component is composed of nodes that can reach all other
+ * nodes in the component via directed edges. A strongly connected component
+ * can consist of a single node if that node cannot both reach and be reached
+ * by any other specific node in the graph. Components of more than one node
+ * are guaranteed to have at least one cycle.
+ *
+ * This function returns an array of components. Each component is itself an
+ * array that contains the ids of all nodes in the component.
+ *
+ * [Tarjan's algorithm]: http://en.wikipedia.org/wiki/Tarjan's_strongly_connected_components_algorithm
+ * [strongly connected components]: http://en.wikipedia.org/wiki/Strongly_connected_component
+ *
+ * @param {Digraph} g the graph to search for strongly connected components
+ */
+function tarjan(g) {
+ if (!g.isDirected()) {
+ throw new Error("tarjan can only be applied to a directed graph. Bad input: " + g);
+ }
+
+ var index = 0,
+ stack = [],
+ visited = {}, // node id -> { onStack, lowlink, index }
+ results = [];
+
+ function dfs(u) {
+ var entry = visited[u] = {
+ onStack: true,
+ lowlink: index,
+ index: index++
+ };
+ stack.push(u);
+
+ g.successors(u).forEach(function(v) {
+ if (!(v in visited)) {
+ dfs(v);
+ entry.lowlink = Math.min(entry.lowlink, visited[v].lowlink);
+ } else if (visited[v].onStack) {
+ entry.lowlink = Math.min(entry.lowlink, visited[v].index);
+ }
+ });
+
+ if (entry.lowlink === entry.index) {
+ var cmpt = [],
+ v;
+ do {
+ v = stack.pop();
+ visited[v].onStack = false;
+ cmpt.push(v);
+ } while (u !== v);
+ results.push(cmpt);
+ }
+ }
+
+ g.nodes().forEach(function(u) {
+ if (!(u in visited)) {
+ dfs(u);
+ }
+ });
+
+ return results;
+}
+
+},{}],44:[function(require,module,exports){
+module.exports = topsort;
+topsort.CycleException = CycleException;
+
+/*
+ * Given a graph **g**, this function returns an ordered list of nodes such
+ * that for each edge `u -> v`, `u` appears before `v` in the list. If the
+ * graph has a cycle it is impossible to generate such a list and
+ * **CycleException** is thrown.
+ *
+ * See [topological sorting](https://en.wikipedia.org/wiki/Topological_sorting)
+ * for more details about how this algorithm works.
+ *
+ * @param {Digraph} g the graph to sort
+ */
+function topsort(g) {
+ if (!g.isDirected()) {
+ throw new Error("topsort can only be applied to a directed graph. Bad input: " + g);
+ }
+
+ var visited = {};
+ var stack = {};
+ var results = [];
+
+ function visit(node) {
+ if (node in stack) {
+ throw new CycleException();
+ }
+
+ if (!(node in visited)) {
+ stack[node] = true;
+ visited[node] = true;
+ g.predecessors(node).forEach(function(pred) {
+ visit(pred);
+ });
+ delete stack[node];
+ results.push(node);
+ }
+ }
+
+ var sinks = g.sinks();
+ if (g.order() !== 0 && sinks.length === 0) {
+ throw new CycleException();
+ }
+
+ g.sinks().forEach(function(sink) {
+ visit(sink);
+ });
+
+ return results;
+}
+
+function CycleException() {}
+
+CycleException.prototype.toString = function() {
+ return "Graph has at least one cycle";
+};
+
+},{}],45:[function(require,module,exports){
+// This file provides a helper function that mixes-in Dot behavior to an
+// existing graph prototype.
+
+/* jshint -W079 */
+var Set = require("cp-data").Set;
+/* jshint +W079 */
+
+module.exports = compoundify;
+
+// Extends the given SuperConstructor with the ability for nodes to contain
+// other nodes. A special node id `null` is used to indicate the root graph.
+function compoundify(SuperConstructor) {
+ function Constructor() {
+ SuperConstructor.call(this);
+
+ // Map of object id -> parent id (or null for root graph)
+ this._parents = {};
+
+ // Map of id (or null) -> children set
+ this._children = {};
+ this._children[null] = new Set();
+ }
+
+ Constructor.prototype = new SuperConstructor();
+ Constructor.prototype.constructor = Constructor;
+
+ Constructor.prototype.parent = function(u, parent) {
+ this._strictGetNode(u);
+
+ if (arguments.length < 2) {
+ return this._parents[u];
+ }
+
+ if (u === parent) {
+ throw new Error("Cannot make " + u + " a parent of itself");
+ }
+ if (parent !== null) {
+ this._strictGetNode(parent);
+ }
+
+ this._children[this._parents[u]].remove(u);
+ this._parents[u] = parent;
+ this._children[parent].add(u);
+ };
+
+ Constructor.prototype.children = function(u) {
+ if (u !== null) {
+ this._strictGetNode(u);
+ }
+ return this._children[u].keys();
+ };
+
+ Constructor.prototype.addNode = function(u, value) {
+ u = SuperConstructor.prototype.addNode.call(this, u, value);
+ this._parents[u] = null;
+ this._children[u] = new Set();
+ this._children[null].add(u);
+ return u;
+ };
+
+ Constructor.prototype.delNode = function(u) {
+ // Promote all children to the parent of the subgraph
+ var parent = this.parent(u);
+ this._children[u].keys().forEach(function(child) {
+ this.parent(child, parent);
+ }, this);
+
+ this._children[parent].remove(u);
+ delete this._parents[u];
+ delete this._children[u];
+
+ return SuperConstructor.prototype.delNode.call(this, u);
+ };
+
+ Constructor.prototype.copy = function() {
+ var copy = SuperConstructor.prototype.copy.call(this);
+ this.nodes().forEach(function(u) {
+ copy.parent(u, this.parent(u));
+ }, this);
+ return copy;
+ };
+
+ Constructor.prototype.filterNodes = function(filter) {
+ var self = this,
+ copy = SuperConstructor.prototype.filterNodes.call(this, filter);
+
+ var parents = {};
+ function findParent(u) {
+ var parent = self.parent(u);
+ if (parent === null || copy.hasNode(parent)) {
+ parents[u] = parent;
+ return parent;
+ } else if (parent in parents) {
+ return parents[parent];
+ } else {
+ return findParent(parent);
+ }
+ }
+
+ copy.eachNode(function(u) { copy.parent(u, findParent(u)); });
+
+ return copy;
+ };
+
+ return Constructor;
+}
+
+},{"cp-data":5}],46:[function(require,module,exports){
+var Graph = require("../Graph"),
+ Digraph = require("../Digraph"),
+ CGraph = require("../CGraph"),
+ CDigraph = require("../CDigraph");
+
+exports.decode = function(nodes, edges, Ctor) {
+ Ctor = Ctor || Digraph;
+
+ if (typeOf(nodes) !== "Array") {
+ throw new Error("nodes is not an Array");
+ }
+
+ if (typeOf(edges) !== "Array") {
+ throw new Error("edges is not an Array");
+ }
+
+ if (typeof Ctor === "string") {
+ switch(Ctor) {
+ case "graph": Ctor = Graph; break;
+ case "digraph": Ctor = Digraph; break;
+ case "cgraph": Ctor = CGraph; break;
+ case "cdigraph": Ctor = CDigraph; break;
+ default: throw new Error("Unrecognized graph type: " + Ctor);
+ }
+ }
+
+ var graph = new Ctor();
+
+ nodes.forEach(function(u) {
+ graph.addNode(u.id, u.value);
+ });
+
+ // If the graph is compound, set up children...
+ if (graph.parent) {
+ nodes.forEach(function(u) {
+ if (u.children) {
+ u.children.forEach(function(v) {
+ graph.parent(v, u.id);
+ });
+ }
+ });
+ }
+
+ edges.forEach(function(e) {
+ graph.addEdge(e.id, e.u, e.v, e.value);
+ });
+
+ return graph;
+};
+
+exports.encode = function(graph) {
+ var nodes = [];
+ var edges = [];
+
+ graph.eachNode(function(u, value) {
+ var node = {id: u, value: value};
+ if (graph.children) {
+ var children = graph.children(u);
+ if (children.length) {
+ node.children = children;
+ }
+ }
+ nodes.push(node);
+ });
+
+ graph.eachEdge(function(e, u, v, value) {
+ edges.push({id: e, u: u, v: v, value: value});
+ });
+
+ var type;
+ if (graph instanceof CDigraph) {
+ type = "cdigraph";
+ } else if (graph instanceof CGraph) {
+ type = "cgraph";
+ } else if (graph instanceof Digraph) {
+ type = "digraph";
+ } else if (graph instanceof Graph) {
+ type = "graph";
+ } else {
+ throw new Error("Couldn't determine type of graph: " + graph);
+ }
+
+ return { nodes: nodes, edges: edges, type: type };
+};
+
+function typeOf(obj) {
+ return Object.prototype.toString.call(obj).slice(8, -1);
+}
+
+},{"../CDigraph":30,"../CGraph":31,"../Digraph":32,"../Graph":33}],47:[function(require,module,exports){
+/* jshint -W079 */
+var Set = require("cp-data").Set;
+/* jshint +W079 */
+
+exports.all = function() {
+ return function() { return true; };
+};
+
+exports.nodesFromList = function(nodes) {
+ var set = new Set(nodes);
+ return function(u) {
+ return set.has(u);
+ };
+};
+
+},{"cp-data":5}],48:[function(require,module,exports){
+var Graph = require("./Graph"),
+ Digraph = require("./Digraph");
+
+// Side-effect based changes are lousy, but node doesn't seem to resolve the
+// requires cycle.
+
+/**
+ * Returns a new directed graph using the nodes and edges from this graph. The
+ * new graph will have the same nodes, but will have twice the number of edges:
+ * each edge is split into two edges with opposite directions. Edge ids,
+ * consequently, are not preserved by this transformation.
+ */
+Graph.prototype.toDigraph =
+Graph.prototype.asDirected = function() {
+ var g = new Digraph();
+ this.eachNode(function(u, value) { g.addNode(u, value); });
+ this.eachEdge(function(e, u, v, value) {
+ g.addEdge(null, u, v, value);
+ g.addEdge(null, v, u, value);
+ });
+ return g;
+};
+
+/**
+ * Returns a new undirected graph using the nodes and edges from this graph.
+ * The new graph will have the same nodes, but the edges will be made
+ * undirected. Edge ids are preserved in this transformation.
+ */
+Digraph.prototype.toGraph =
+Digraph.prototype.asUndirected = function() {
+ var g = new Graph();
+ this.eachNode(function(u, value) { g.addNode(u, value); });
+ this.eachEdge(function(e, u, v, value) {
+ g.addEdge(e, u, v, value);
+ });
+ return g;
+};
+
+},{"./Digraph":32,"./Graph":33}],49:[function(require,module,exports){
+// Returns an array of all values for properties of **o**.
+exports.values = function(o) {
+ var ks = Object.keys(o),
+ len = ks.length,
+ result = new Array(len),
+ i;
+ for (i = 0; i < len; ++i) {
+ result[i] = o[ks[i]];
+ }
+ return result;
+};
+
+},{}],50:[function(require,module,exports){
+module.exports = '0.7.4';
+
+},{}]},{},[1])
+; \ No newline at end of file
diff --git a/toolkit/devtools/webaudioeditor/models.js b/toolkit/devtools/webaudioeditor/models.js
new file mode 100644
index 000000000..aa1975f58
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/models.js
@@ -0,0 +1,312 @@
+/* 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";
+
+// Import as different name `coreEmit`, so we don't conflict
+// with the global `window` listener itself.
+const { emit: coreEmit } = require("sdk/event/core");
+
+/**
+ * Representational wrapper around AudioNodeActors. Adding and destroying
+ * AudioNodes should be performed through the AudioNodes collection.
+ *
+ * Events:
+ * - `connect`: node, destinationNode, parameter
+ * - `disconnect`: node
+ */
+const AudioNodeModel = Class({
+ extends: EventTarget,
+
+ // Will be added via AudioNodes `add`
+ collection: null,
+
+ initialize: function (actor) {
+ this.actor = actor;
+ this.id = actor.actorID;
+ this.connections = [];
+ },
+
+ /**
+ * After instantiating the AudioNodeModel, calling `setup` caches values
+ * from the actor onto the model. In this case, only the type of audio node.
+ *
+ * @return promise
+ */
+ setup: Task.async(function* () {
+ yield this.getType();
+
+ // Query bypass status on start up
+ this._bypassed = yield this.isBypassed();
+
+ // Store whether or not this node is bypassable in the first place
+ this.bypassable = !AUDIO_NODE_DEFINITION[this.type].unbypassable;
+ }),
+
+ /**
+ * A proxy for the underlying AudioNodeActor to fetch its type
+ * and subsequently assign the type to the instance.
+ *
+ * @return Promise->String
+ */
+ getType: Task.async(function* () {
+ this.type = yield this.actor.getType();
+ return this.type;
+ }),
+
+ /**
+ * Stores connection data inside this instance of this audio node connecting
+ * to another node (destination). If connecting to another node's AudioParam,
+ * the second argument (param) must be populated with a string.
+ *
+ * Connecting nodes is idempotent. Upon new connection, emits "connect" event.
+ *
+ * @param AudioNodeModel destination
+ * @param String param
+ */
+ connect: function (destination, param) {
+ let edge = findWhere(this.connections, { destination: destination.id, param: param });
+
+ if (!edge) {
+ this.connections.push({ source: this.id, destination: destination.id, param: param });
+ coreEmit(this, "connect", this, destination, param);
+ }
+ },
+
+ /**
+ * Clears out all internal connection data. Emits "disconnect" event.
+ */
+ disconnect: function () {
+ this.connections.length = 0;
+ coreEmit(this, "disconnect", this);
+ },
+
+ /**
+ * Gets the bypass status of the audio node.
+ *
+ * @return Promise->Boolean
+ */
+ isBypassed: function () {
+ return this.actor.isBypassed();
+ },
+
+ /**
+ * Sets the bypass value of an AudioNode.
+ *
+ * @param Boolean enable
+ * @return Promise
+ */
+ bypass: function (enable) {
+ this._bypassed = enable;
+ return this.actor.bypass(enable).then(() => coreEmit(this, "bypass", this, enable));
+ },
+
+ /**
+ * Returns a promise that resolves to an array of objects containing
+ * both a `param` name property and a `value` property.
+ *
+ * @return Promise->Object
+ */
+ getParams: function () {
+ return this.actor.getParams();
+ },
+
+ /**
+ * Returns a promise that resolves to an object containing an
+ * array of event information and an array of automation data.
+ *
+ * @param String paramName
+ * @return Promise->Array
+ */
+ getAutomationData: function (paramName) {
+ return this.actor.getAutomationData(paramName);
+ },
+
+ /**
+ * Takes a `dagreD3.Digraph` object and adds this node to
+ * the graph to be rendered.
+ *
+ * @param dagreD3.Digraph
+ */
+ addToGraph: function (graph) {
+ graph.addNode(this.id, {
+ type: this.type,
+ label: this.type.replace(/Node$/, ""),
+ id: this.id,
+ bypassed: this._bypassed
+ });
+ },
+
+ /**
+ * Takes a `dagreD3.Digraph` object and adds edges to
+ * the graph to be rendered. Separate from `addToGraph`,
+ * as while we depend on D3/Dagre's constraints, we cannot
+ * add edges for nodes that have not yet been added to the graph.
+ *
+ * @param dagreD3.Digraph
+ */
+ addEdgesToGraph: function (graph) {
+ for (let edge of this.connections) {
+ let options = {
+ source: this.id,
+ target: edge.destination
+ };
+
+ // Only add `label` if `param` specified, as this is an AudioParam
+ // connection then. `label` adds the magic to render with dagre-d3,
+ // and `param` is just more explicitly the param, ignoring
+ // implementation details.
+ if (edge.param) {
+ options.label = options.param = edge.param;
+ }
+
+ graph.addEdge(null, this.id, edge.destination, options);
+ }
+ }
+});
+
+
+/**
+ * Constructor for a Collection of `AudioNodeModel` models.
+ *
+ * Events:
+ * - `add`: node
+ * - `remove`: node
+ * - `connect`: node, destinationNode, parameter
+ * - `disconnect`: node
+ */
+const AudioNodesCollection = Class({
+ extends: EventTarget,
+
+ model: AudioNodeModel,
+
+ initialize: function () {
+ this.models = new Set();
+ this._onModelEvent = this._onModelEvent.bind(this);
+ },
+
+ /**
+ * Iterates over all models within the collection, calling `fn` with the
+ * model as the first argument.
+ *
+ * @param Function fn
+ */
+ forEach: function (fn) {
+ this.models.forEach(fn);
+ },
+
+ /**
+ * Creates a new AudioNodeModel, passing through arguments into the AudioNodeModel
+ * constructor, and adds the model to the internal collection store of this
+ * instance.
+ *
+ * Also calls `setup` on the model itself, and sets up event piping, so that
+ * events emitted on each model propagate to the collection itself.
+ *
+ * Emits "add" event on instance when completed.
+ *
+ * @param Object obj
+ * @return Promise->AudioNodeModel
+ */
+ add: Task.async(function* (obj) {
+ let node = new this.model(obj);
+ node.collection = this;
+ yield node.setup();
+
+ this.models.add(node);
+
+ node.on("*", this._onModelEvent);
+ coreEmit(this, "add", node);
+ return node;
+ }),
+
+ /**
+ * Removes an AudioNodeModel from the internal collection. Calls `delete` method
+ * on the model, and emits "remove" on this instance.
+ *
+ * @param AudioNodeModel node
+ */
+ remove: function (node) {
+ this.models.delete(node);
+ coreEmit(this, "remove", node);
+ },
+
+ /**
+ * Empties out the internal collection of all AudioNodeModels.
+ */
+ reset: function () {
+ this.models.clear();
+ },
+
+ /**
+ * Takes an `id` from an AudioNodeModel and returns the corresponding
+ * AudioNodeModel within the collection that matches that id. Returns `null`
+ * if not found.
+ *
+ * @param Number id
+ * @return AudioNodeModel|null
+ */
+ get: function (id) {
+ return findWhere(this.models, { id: id });
+ },
+
+ /**
+ * Returns the count for how many models are a part of this collection.
+ *
+ * @return Number
+ */
+ get length() {
+ return this.models.size;
+ },
+
+ /**
+ * Returns detailed information about the collection. used during tests
+ * to query state. Returns an object with information on node count,
+ * how many edges are within the data graph, as well as how many of those edges
+ * are for AudioParams.
+ *
+ * @return Object
+ */
+ getInfo: function () {
+ let info = {
+ nodes: this.length,
+ edges: 0,
+ paramEdges: 0
+ };
+
+ this.models.forEach(node => {
+ let paramEdgeCount = node.connections.filter(edge => edge.param).length;
+ info.edges += node.connections.length - paramEdgeCount;
+ info.paramEdges += paramEdgeCount;
+ });
+ return info;
+ },
+
+ /**
+ * Adds all nodes within the collection to the passed in graph,
+ * as well as their corresponding edges.
+ *
+ * @param dagreD3.Digraph
+ */
+ populateGraph: function (graph) {
+ this.models.forEach(node => node.addToGraph(graph));
+ this.models.forEach(node => node.addEdgesToGraph(graph));
+ },
+
+ /**
+ * Called when a stored model emits any event. Used to manage
+ * event propagation, or listening to model events to react, like
+ * removing a model from the collection when it's destroyed.
+ */
+ _onModelEvent: function (eventName, node, ...args) {
+ if (eventName === "remove") {
+ // If a `remove` event from the model, remove it
+ // from the collection, and let the method handle the emitting on
+ // the collection
+ this.remove(node);
+ } else {
+ // Pipe the event to the collection
+ coreEmit(this, eventName, node, ...args);
+ }
+ }
+});
diff --git a/toolkit/devtools/webaudioeditor/moz.build b/toolkit/devtools/webaudioeditor/moz.build
new file mode 100644
index 000000000..7fbea6cc9
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/moz.build
@@ -0,0 +1,10 @@
+# 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/.
+
+EXTRA_JS_MODULES.devtools.webaudioeditor += [
+ 'panel.js'
+]
+
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
diff --git a/toolkit/devtools/webaudioeditor/panel.js b/toolkit/devtools/webaudioeditor/panel.js
new file mode 100644
index 000000000..c2da91a9c
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/panel.js
@@ -0,0 +1,69 @@
+/* -*- 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/toolkit/event-emitter");
+const { WebAudioFront } = require("devtools/server/actors/webaudio");
+let Promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
+
+function WebAudioEditorPanel (iframeWindow, toolbox) {
+ this.panelWin = iframeWindow;
+ this._toolbox = toolbox;
+ this._destroyer = null;
+
+ EventEmitter.decorate(this);
+}
+
+exports.WebAudioEditorPanel = WebAudioEditorPanel;
+
+WebAudioEditorPanel.prototype = {
+ open: function() {
+ let targetPromise;
+
+ // Local debugging needs to make the target remote.
+ if (!this.target.isRemote) {
+ targetPromise = this.target.makeRemote();
+ } else {
+ targetPromise = Promise.resolve(this.target);
+ }
+
+ return targetPromise
+ .then(() => {
+ this.panelWin.gToolbox = this._toolbox;
+ this.panelWin.gTarget = this.target;
+
+ this.panelWin.gFront = new WebAudioFront(this.target.client, this.target.form);
+ return this.panelWin.startupWebAudioEditor();
+ })
+ .then(() => {
+ this.isReady = true;
+ this.emit("ready");
+ return this;
+ })
+ .then(null, function onError(aReason) {
+ Cu.reportError("WebAudioEditorPanel open failed. " +
+ aReason.error + ": " + aReason.message);
+ });
+ },
+
+ // DevToolPanel API
+
+ get target() this._toolbox.target,
+
+ destroy: function() {
+ // Make sure this panel is not already destroyed.
+ if (this._destroyer) {
+ return this._destroyer;
+ }
+
+ return this._destroyer = this.panelWin.shutdownWebAudioEditor().then(() => {
+ // Destroy front to ensure packet handler is removed from client
+ this.panelWin.gFront.destroy();
+ this.emit("destroyed");
+ });
+ }
+};
diff --git a/toolkit/devtools/webaudioeditor/test/440hz_sine.ogg b/toolkit/devtools/webaudioeditor/test/440hz_sine.ogg
new file mode 100644
index 000000000..bd84564e2
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/440hz_sine.ogg
Binary files differ
diff --git a/toolkit/devtools/webaudioeditor/test/browser.ini b/toolkit/devtools/webaudioeditor/test/browser.ini
new file mode 100644
index 000000000..f58bdf623
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/browser.ini
@@ -0,0 +1,72 @@
+[DEFAULT]
+subsuite = devtools
+support-files =
+ doc_simple-context.html
+ doc_complex-context.html
+ doc_simple-node-creation.html
+ doc_buffer-and-array.html
+ doc_media-node-creation.html
+ doc_destroy-nodes.html
+ doc_connect-param.html
+ doc_connect-multi-param.html
+ doc_iframe-context.html
+ doc_automation.html
+ doc_bug_1125817.html
+ doc_bug_1130901.html
+ doc_bug_1112378.html
+ 440hz_sine.ogg
+ head.js
+
+[browser_audionode-actor-get-param-flags.js]
+[browser_audionode-actor-get-params-01.js]
+[browser_audionode-actor-get-params-02.js]
+[browser_audionode-actor-get-set-param.js]
+[browser_audionode-actor-get-type.js]
+[browser_audionode-actor-is-source.js]
+[browser_audionode-actor-bypass.js]
+[browser_audionode-actor-connectnode-disconnect.js]
+[browser_audionode-actor-connectparam.js]
+skip-if = true # bug 1092571
+# [browser_audionode-actor-add-automation-event.js] bug 1134036
+# [browser_audionode-actor-get-automation-data-01.js] bug 1134036
+# [browser_audionode-actor-get-automation-data-02.js] bug 1134036
+# [browser_audionode-actor-get-automation-data-03.js] bug 1134036
+[browser_callwatcher-01.js]
+[browser_callwatcher-02.js]
+[browser_webaudio-actor-simple.js]
+[browser_webaudio-actor-destroy-node.js]
+[browser_webaudio-actor-connect-param.js]
+# [browser_webaudio-actor-automation-event.js] bug 1134036
+
+# [browser_wa_automation-view-01.js] bug 1134036
+# [browser_wa_automation-view-02.js] bug 1134036
+[browser_wa_controller-01.js]
+[browser_wa_destroy-node-01.js]
+[browser_wa_first-run.js]
+[browser_wa_graph-click.js]
+[browser_wa_graph-markers.js]
+[browser_wa_graph-render-01.js]
+[browser_wa_graph-render-02.js]
+[browser_wa_graph-render-03.js]
+[browser_wa_graph-render-04.js]
+[browser_wa_graph-render-05.js]
+skip-if = true # bug 1092571
+[browser_wa_graph-selected.js]
+[browser_wa_graph-zoom.js]
+[browser_wa_inspector.js]
+[browser_wa_inspector-toggle.js]
+[browser_wa_inspector-width.js]
+[browser_wa_inspector-bypass-01.js]
+[browser_wa_navigate.js]
+[browser_wa_properties-view.js]
+[browser_wa_properties-view-edit-01.js]
+skip-if = true # bug 1010423
+[browser_wa_properties-view-edit-02.js]
+skip-if = true # bug 1010423
+[browser_wa_properties-view-media-nodes.js]
+[browser_wa_properties-view-params.js]
+[browser_wa_properties-view-params-objects.js]
+[browser_wa_reset-01.js]
+[browser_wa_reset-02.js]
+[browser_wa_reset-03.js]
+[browser_wa_reset-04.js]
diff --git a/toolkit/devtools/webaudioeditor/test/browser_audionode-actor-add-automation-event.js b/toolkit/devtools/webaudioeditor/test/browser_audionode-actor-add-automation-event.js
new file mode 100644
index 000000000..0e96b83ab
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/browser_audionode-actor-add-automation-event.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test AudioNode#addAutomationEvent();
+ */
+
+add_task(function*() {
+ let { target, front } = yield initBackend(SIMPLE_CONTEXT_URL);
+ let [_, [destNode, oscNode, gainNode]] = yield Promise.all([
+ front.setup({ reload: true }),
+ get3(front, "create-node")
+ ]);
+ let count = 0;
+ let counter = () => count++;
+ front.on("automation-event", counter);
+
+ let t0 = 0, t1 = 0.1, t2 = 0.2, t3 = 0.3, t4 = 0.4, t5 = 0.6, t6 = 0.7, t7 = 1;
+ let curve = [-1, 0, 1];
+ yield oscNode.addAutomationEvent("frequency", "setValueAtTime", [0.2, t0]);
+ yield oscNode.addAutomationEvent("frequency", "setValueAtTime", [0.3, t1]);
+ yield oscNode.addAutomationEvent("frequency", "setValueAtTime", [0.4, t2]);
+ yield oscNode.addAutomationEvent("frequency", "linearRampToValueAtTime", [1, t3]);
+ yield oscNode.addAutomationEvent("frequency", "linearRampToValueAtTime", [0.15, t4]);
+ yield oscNode.addAutomationEvent("frequency", "exponentialRampToValueAtTime", [0.75, t5]);
+ yield oscNode.addAutomationEvent("frequency", "exponentialRampToValueAtTime", [0.5, t6]);
+ yield oscNode.addAutomationEvent("frequency", "setValueCurveAtTime", [curve, t7, t7 - t6]);
+ yield oscNode.addAutomationEvent("frequency", "setTargetAtTime", [20, 2, 5]);
+
+ ok(true, "successfully set automation events for valid automation events");
+
+ try {
+ yield oscNode.addAutomationEvent("frequency", "notAMethod", 20, 2, 5);
+ ok(false, "non-automation methods should not be successful");
+ } catch (e) {
+ ok(/invalid/.test(e.message), "AudioNode:addAutomationEvent fails for invalid automation methods");
+ }
+
+ try {
+ yield oscNode.addAutomationEvent("invalidparam", "setValueAtTime", 0.2, t0);
+ ok(false, "automating non-AudioParams should not be successful");
+ } catch (e) {
+ ok(/invalid/.test(e.message), "AudioNode:addAutomationEvent fails for a non AudioParam");
+ }
+
+ front.off("automation-event", counter);
+
+ is(count, 9,
+ "when calling `addAutomationEvent`, the WebAudioActor should still fire `automation-event`.");
+
+ yield removeTab(target.tab);
+});
diff --git a/toolkit/devtools/webaudioeditor/test/browser_audionode-actor-bypass.js b/toolkit/devtools/webaudioeditor/test/browser_audionode-actor-bypass.js
new file mode 100644
index 000000000..57ce888ef
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/browser_audionode-actor-bypass.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test AudioNode#bypass(), AudioNode#isBypassed()
+ */
+
+add_task(function*() {
+ let { target, front } = yield initBackend(SIMPLE_CONTEXT_URL);
+ let [_, [destNode, oscNode, gainNode]] = yield Promise.all([
+ front.setup({ reload: true }),
+ get3(front, "create-node")
+ ]);
+
+ is((yield gainNode.isBypassed()), false, "Nodes start off unbypassed.");
+
+ info("Calling node#bypass(true)");
+ let isBypassed = yield gainNode.bypass(true);
+
+ is(isBypassed, true, "node.bypass(true) resolves to true");
+ is((yield gainNode.isBypassed()), true, "Node is now bypassed.");
+
+ info("Calling node#bypass(false)");
+ isBypassed = yield gainNode.bypass(false);
+
+ is(isBypassed, false, "node.bypass(false) resolves to false");
+ is((yield gainNode.isBypassed()), false, "Node back to being unbypassed.");
+
+ info("Calling node#bypass(true) on unbypassable node");
+ isBypassed = yield destNode.bypass(true);
+
+ is(isBypassed, false, "node.bypass(true) resolves to false for unbypassable node");
+ is((yield gainNode.isBypassed()), false, "Unbypassable node is unaffect");
+
+ yield removeTab(target.tab);
+});
diff --git a/toolkit/devtools/webaudioeditor/test/browser_audionode-actor-connectnode-disconnect.js b/toolkit/devtools/webaudioeditor/test/browser_audionode-actor-connectnode-disconnect.js
new file mode 100644
index 000000000..f0e437d86
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/browser_audionode-actor-connectnode-disconnect.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that AudioNodeActor#connectNode() and AudioNodeActor#disconnect() work.
+ * Uses the editor front as the actors do not retain connect state.
+ */
+
+add_task(function*() {
+ let { target, panel } = yield initWebAudioEditor(SIMPLE_CONTEXT_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$, EVENTS, gAudioNodes } = panelWin;
+
+ reload(target);
+
+ let [actors] = yield Promise.all([
+ get3(gFront, "create-node"),
+ waitForGraphRendered(panelWin, 3, 2)
+ ]);
+
+ let [dest, osc, gain] = actors;
+
+ info("Disconnecting oscillator...");
+ osc.disconnect();
+ yield Promise.all([
+ waitForGraphRendered(panelWin, 3, 1),
+ once(gAudioNodes, "disconnect")
+ ]);
+ ok(true, "Oscillator disconnected, event emitted.");
+
+ info("Reconnecting oscillator...");
+ osc.connectNode(gain);
+ yield Promise.all([
+ waitForGraphRendered(panelWin, 3, 2),
+ once(gAudioNodes, "connect")
+ ]);
+ ok(true, "Oscillator reconnected.");
+
+ yield teardown(target);
+});
diff --git a/toolkit/devtools/webaudioeditor/test/browser_audionode-actor-connectparam.js b/toolkit/devtools/webaudioeditor/test/browser_audionode-actor-connectparam.js
new file mode 100644
index 000000000..df1f6e0b3
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/browser_audionode-actor-connectparam.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that AudioNodeActor#connectParam() work.
+ * Uses the editor front as the actors do not retain connect state.
+ */
+
+add_task(function*() {
+ let { target, panel } = yield initWebAudioEditor(SIMPLE_CONTEXT_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$, EVENTS, gAudioNodes } = panelWin;
+
+ reload(target);
+
+ let [actors] = yield Promise.all([
+ get3(gFront, "create-node"),
+ waitForGraphRendered(panelWin, 3, 2)
+ ]);
+
+ let [dest, osc, gain] = actors;
+
+ yield osc.disconnect();
+
+ osc.connectParam(gain, "gain");
+ yield Promise.all([
+ waitForGraphRendered(panelWin, 3, 1, 1),
+ once(gAudioNodes, "connect")
+ ]);
+ ok(true, "Oscillator connect to Gain's Gain AudioParam, event emitted.");
+
+ yield teardown(target);
+});
diff --git a/toolkit/devtools/webaudioeditor/test/browser_audionode-actor-get-automation-data-01.js b/toolkit/devtools/webaudioeditor/test/browser_audionode-actor-get-automation-data-01.js
new file mode 100644
index 000000000..8bb35d270
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/browser_audionode-actor-get-automation-data-01.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test AudioNode#addAutomationEvent() checking automation values, also using
+ * a curve as the last event to check duration spread.
+ */
+
+add_task(function*() {
+ let { target, front } = yield initBackend(SIMPLE_CONTEXT_URL);
+ let [_, [destNode, oscNode, gainNode]] = yield Promise.all([
+ front.setup({ reload: true }),
+ get3(front, "create-node")
+ ]);
+
+ let t0 = 0, t1 = 0.1, t2 = 0.2, t3 = 0.3, t4 = 0.4, t5 = 0.6, t6 = 0.7, t7 = 1;
+ let curve = [-1, 0, 1];
+ yield oscNode.addAutomationEvent("frequency", "setValueAtTime", [0.2, t0]);
+ yield oscNode.addAutomationEvent("frequency", "setValueAtTime", [0.3, t1]);
+ yield oscNode.addAutomationEvent("frequency", "setValueAtTime", [0.4, t2]);
+ yield oscNode.addAutomationEvent("frequency", "linearRampToValueAtTime", [1, t3]);
+ yield oscNode.addAutomationEvent("frequency", "linearRampToValueAtTime", [0.15, t4]);
+ yield oscNode.addAutomationEvent("frequency", "exponentialRampToValueAtTime", [0.75, t5]);
+ yield oscNode.addAutomationEvent("frequency", "exponentialRampToValueAtTime", [0.05, t6]);
+ // End with a curve here so we can get proper results on the last event (which takes into account
+ // duration)
+ yield oscNode.addAutomationEvent("frequency", "setValueCurveAtTime", [curve, t6, t7 - t6]);
+
+ let { events, values } = yield oscNode.getAutomationData("frequency");
+
+ is(events.length, 8, "8 recorded events returned.");
+ is(values.length, 2000, "2000 value points returned.");
+
+ checkAutomationValue(values, 0.05, 0.2);
+ checkAutomationValue(values, 0.1, 0.3);
+ checkAutomationValue(values, 0.15, 0.3);
+ checkAutomationValue(values, 0.2, 0.4);
+ checkAutomationValue(values, 0.25, 0.7);
+ checkAutomationValue(values, 0.3, 1);
+ checkAutomationValue(values, 0.35, 0.575);
+ checkAutomationValue(values, 0.4, 0.15);
+ checkAutomationValue(values, 0.45, 0.15 * Math.pow(0.75/0.15,0.05/0.2));
+ checkAutomationValue(values, 0.5, 0.15 * Math.pow(0.75/0.15,0.5));
+ checkAutomationValue(values, 0.55, 0.15 * Math.pow(0.75/0.15,0.15/0.2));
+ checkAutomationValue(values, 0.6, 0.75);
+ checkAutomationValue(values, 0.65, 0.75 * Math.pow(0.05/0.75, 0.5));
+ checkAutomationValue(values, 0.705, -1); // Increase this time a bit to prevent off by the previous exponential amount
+ checkAutomationValue(values, 0.8, 0);
+ checkAutomationValue(values, 0.9, 1);
+ checkAutomationValue(values, 1, 1);
+
+ yield removeTab(target.tab);
+});
diff --git a/toolkit/devtools/webaudioeditor/test/browser_audionode-actor-get-automation-data-02.js b/toolkit/devtools/webaudioeditor/test/browser_audionode-actor-get-automation-data-02.js
new file mode 100644
index 000000000..0ace882cd
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/browser_audionode-actor-get-automation-data-02.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test AudioNode#addAutomationEvent() when automation series ends with
+ * `setTargetAtTime`, which approaches its target to infinity.
+ */
+
+add_task(function*() {
+ let { target, front } = yield initBackend(SIMPLE_CONTEXT_URL);
+ let [_, [destNode, oscNode, gainNode]] = yield Promise.all([
+ front.setup({ reload: true }),
+ get3(front, "create-node")
+ ]);
+
+ yield oscNode.addAutomationEvent("frequency", "setValueAtTime", [300, 0.1]);
+ yield oscNode.addAutomationEvent("frequency", "linearRampToValueAtTime", [500, 0.4]);
+ yield oscNode.addAutomationEvent("frequency", "exponentialRampToValueAtTime", [200, 0.6]);
+ // End with a setTargetAtTime event, as the target approaches infinity, which will
+ // give us more points to render than the default 2000
+ yield oscNode.addAutomationEvent("frequency", "setTargetAtTime", [1000, 2, 0.5]);
+
+ var { events, values } = yield oscNode.getAutomationData("frequency");
+
+ is(events.length, 4, "4 recorded events returned.");
+ is(values.length, 4000, "4000 value points returned when ending with exponentiall approaching automator.");
+
+ checkAutomationValue(values, 2.01, 215.055)
+ checkAutomationValue(values, 2.1, 345.930);
+ checkAutomationValue(values, 3, 891.601);
+ checkAutomationValue(values, 5, 998.01);
+
+ // Refetch the automation data to ensure it recalculates correctly (bug 1118071)
+ var { events, values } = yield oscNode.getAutomationData("frequency");
+
+ checkAutomationValue(values, 2.01, 215.055)
+ checkAutomationValue(values, 2.1, 345.930);
+ checkAutomationValue(values, 3, 891.601);
+ checkAutomationValue(values, 5, 998.01);
+
+ yield removeTab(target.tab);
+});
diff --git a/toolkit/devtools/webaudioeditor/test/browser_audionode-actor-get-automation-data-03.js b/toolkit/devtools/webaudioeditor/test/browser_audionode-actor-get-automation-data-03.js
new file mode 100644
index 000000000..13fb287b3
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/browser_audionode-actor-get-automation-data-03.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test that `cancelScheduledEvents` clears out events on and after
+ * its argument.
+ */
+
+add_task(function*() {
+ let { target, front } = yield initBackend(SIMPLE_CONTEXT_URL);
+ let [_, [destNode, oscNode, gainNode]] = yield Promise.all([
+ front.setup({ reload: true }),
+ get3(front, "create-node")
+ ]);
+
+ yield oscNode.addAutomationEvent("frequency", "setValueAtTime", [300, 0]);
+ yield oscNode.addAutomationEvent("frequency", "linearRampToValueAtTime", [500, 0.9]);
+ yield oscNode.addAutomationEvent("frequency", "setValueAtTime", [700, 1]);
+ yield oscNode.addAutomationEvent("frequency", "exponentialRampToValueAtTime", [1000, 2]);
+ yield oscNode.addAutomationEvent("frequency", "cancelScheduledValues", [1]);
+
+ var { events, values } = yield oscNode.getAutomationData("frequency");
+
+ is(events.length, 2, "2 recorded events returned.");
+ is(values.length, 2000, "2000 value points returned");
+
+ checkAutomationValue(values, 0, 300);
+ checkAutomationValue(values, 0.5, 411.15);
+ checkAutomationValue(values, 0.9, 499.9);
+ checkAutomationValue(values, 1, 499.9);
+ checkAutomationValue(values, 2, 499.9);
+
+ yield removeTab(target.tab);
+});
diff --git a/toolkit/devtools/webaudioeditor/test/browser_audionode-actor-get-param-flags.js b/toolkit/devtools/webaudioeditor/test/browser_audionode-actor-get-param-flags.js
new file mode 100644
index 000000000..c7ba798af
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/browser_audionode-actor-get-param-flags.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test AudioNode#getParamFlags()
+ */
+
+add_task(function*() {
+ let { target, front } = yield initBackend(SIMPLE_NODES_URL);
+ let [_, nodes] = yield Promise.all([
+ front.setup({ reload: true }),
+ getN(front, "create-node", 15)
+ ]);
+
+ let allNodeParams = yield Promise.all(nodes.map(node => node.getParams()));
+ let nodeTypes = [
+ "AudioDestinationNode",
+ "AudioBufferSourceNode", "ScriptProcessorNode", "AnalyserNode", "GainNode",
+ "DelayNode", "BiquadFilterNode", "WaveShaperNode", "PannerNode", "ConvolverNode",
+ "ChannelSplitterNode", "ChannelMergerNode", "DynamicsCompressorNode", "OscillatorNode",
+ "StereoPannerNode"
+ ];
+
+ // For some reason nodeTypes.forEach and params.forEach fail here so we use
+ // simple for loops.
+ for (let i = 0; i < nodeTypes.length; i++) {
+ let type = nodeTypes[i];
+ let params = allNodeParams[i];
+
+ for (let {param, value, flags} of params) {
+ let testFlags = yield nodes[i].getParamFlags(param);
+ ok(typeof testFlags === "object", type + " has flags from #getParamFlags(" + param + ")");
+
+ if (param === "buffer") {
+ is(flags.Buffer, true, "`buffer` params have Buffer flag");
+ }
+ else if (param === "bufferSize" || param === "frequencyBinCount") {
+ is(flags.readonly, true, param + " is readonly");
+ }
+ else if (param === "curve") {
+ is(flags["Float32Array"], true, "`curve` param has Float32Array flag");
+ }
+ }
+ }
+
+ yield removeTab(target.tab);
+});
diff --git a/toolkit/devtools/webaudioeditor/test/browser_audionode-actor-get-params-01.js b/toolkit/devtools/webaudioeditor/test/browser_audionode-actor-get-params-01.js
new file mode 100644
index 000000000..b04ae2463
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/browser_audionode-actor-get-params-01.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test AudioNode#getParams()
+ */
+
+add_task(function*() {
+ let { target, front } = yield initBackend(SIMPLE_NODES_URL);
+ let [_, nodes] = yield Promise.all([
+ front.setup({ reload: true }),
+ getN(front, "create-node", 15)
+ ]);
+
+ let allNodeParams = yield Promise.all(nodes.map(node => node.getParams()));
+ let nodeTypes = [
+ "AudioDestinationNode",
+ "AudioBufferSourceNode", "ScriptProcessorNode", "AnalyserNode", "GainNode",
+ "DelayNode", "BiquadFilterNode", "WaveShaperNode", "PannerNode", "ConvolverNode",
+ "ChannelSplitterNode", "ChannelMergerNode", "DynamicsCompressorNode", "OscillatorNode",
+ "StereoPannerNode"
+ ];
+
+ nodeTypes.forEach((type, i) => {
+ let params = allNodeParams[i];
+ params.forEach(({param, value, flags}) => {
+ ok(param in NODE_DEFAULT_VALUES[type], "expected parameter for " + type);
+
+ ok(typeof flags === "object", type + " has a flags object");
+
+ if (param === "buffer") {
+ is(flags.Buffer, true, "`buffer` params have Buffer flag");
+ }
+ else if (param === "bufferSize" || param === "frequencyBinCount") {
+ is(flags.readonly, true, param + " is readonly");
+ }
+ else if (param === "curve") {
+ is(flags["Float32Array"], true, "`curve` param has Float32Array flag");
+ }
+ });
+ });
+
+ yield removeTab(target.tab);
+});
diff --git a/toolkit/devtools/webaudioeditor/test/browser_audionode-actor-get-params-02.js b/toolkit/devtools/webaudioeditor/test/browser_audionode-actor-get-params-02.js
new file mode 100644
index 000000000..ce2eca630
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/browser_audionode-actor-get-params-02.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that default properties are returned with the correct type
+ * from the AudioNode actors.
+ */
+
+add_task(function*() {
+ let { target, front } = yield initBackend(SIMPLE_NODES_URL);
+ let [_, nodes] = yield Promise.all([
+ front.setup({ reload: true }),
+ getN(front, "create-node", 15)
+ ]);
+
+ let allParams = yield Promise.all(nodes.map(node => node.getParams()));
+ let types = [
+ "AudioDestinationNode", "AudioBufferSourceNode", "ScriptProcessorNode",
+ "AnalyserNode", "GainNode", "DelayNode", "BiquadFilterNode", "WaveShaperNode",
+ "PannerNode", "ConvolverNode", "ChannelSplitterNode", "ChannelMergerNode",
+ "DynamicsCompressorNode", "OscillatorNode", "StereoPannerNode"
+ ];
+
+ allParams.forEach((params, i) => {
+ compare(params, NODE_DEFAULT_VALUES[types[i]], types[i]);
+ });
+
+ yield removeTab(target.tab);
+});
+
+function compare (actual, expected, type) {
+ actual.forEach(({ value, param }) => {
+ value = getGripValue(value);
+ if (typeof expected[param] === "function") {
+ ok(expected[param](value), type + " has a passing value for " + param);
+ }
+ else {
+ ise(value, expected[param], type + " has correct default value and type for " + param);
+ }
+ });
+
+ is(actual.length, Object.keys(expected).length,
+ type + " has correct amount of properties.");
+}
diff --git a/toolkit/devtools/webaudioeditor/test/browser_audionode-actor-get-set-param.js b/toolkit/devtools/webaudioeditor/test/browser_audionode-actor-get-set-param.js
new file mode 100644
index 000000000..4679159aa
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/browser_audionode-actor-get-set-param.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test AudioNode#getParam() / AudioNode#setParam()
+ */
+
+add_task(function*() {
+ let { target, front } = yield initBackend(SIMPLE_CONTEXT_URL);
+ let [_, [destNode, oscNode, gainNode]] = yield Promise.all([
+ front.setup({ reload: true }),
+ get3(front, "create-node")
+ ]);
+
+ let freq = yield oscNode.getParam("frequency");
+ info(typeof freq);
+ ise(freq, 440, "AudioNode:getParam correctly fetches AudioParam");
+
+ let type = yield oscNode.getParam("type");
+ ise(type, "sine", "AudioNode:getParam correctly fetches non-AudioParam");
+
+ type = yield oscNode.getParam("not-a-valid-param");
+ ok(type.type === "undefined",
+ "AudioNode:getParam correctly returns a grip value for `undefined` for an invalid param.");
+
+ let resSuccess = yield oscNode.setParam("frequency", 220);
+ freq = yield oscNode.getParam("frequency");
+ ise(freq, 220, "AudioNode:setParam correctly sets a `number` AudioParam");
+ is(resSuccess, undefined, "AudioNode:setParam returns undefined for correctly set AudioParam");
+
+ resSuccess = yield oscNode.setParam("type", "square");
+ type = yield oscNode.getParam("type");
+ ise(type, "square", "AudioNode:setParam correctly sets a `string` non-AudioParam");
+ is(resSuccess, undefined, "AudioNode:setParam returns undefined for correctly set AudioParam");
+
+ try {
+ yield oscNode.setParam("frequency", "hello");
+ ok(false, "setParam with invalid types should throw");
+ } catch (e) {
+ ok(/is not a finite floating-point/.test(e.message), "AudioNode:setParam returns error with correct message when attempting an invalid assignment");
+ is(e.type, "TypeError", "AudioNode:setParam returns error with correct type when attempting an invalid assignment");
+ freq = yield oscNode.getParam("frequency");
+ ise(freq, 220, "AudioNode:setParam does not modify value when an error occurs");
+ }
+
+ yield removeTab(target.tab);
+});
diff --git a/toolkit/devtools/webaudioeditor/test/browser_audionode-actor-get-type.js b/toolkit/devtools/webaudioeditor/test/browser_audionode-actor-get-type.js
new file mode 100644
index 000000000..94d31a6c3
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/browser_audionode-actor-get-type.js
@@ -0,0 +1,28 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test AudioNode#getType()
+ */
+
+add_task(function*() {
+ let { target, front } = yield initBackend(SIMPLE_NODES_URL);
+ let [_, nodes] = yield Promise.all([
+ front.setup({ reload: true }),
+ getN(front, "create-node", 14)
+ ]);
+
+ let actualTypes = yield Promise.all(nodes.map(node => node.getType()));
+ let expectedTypes = [
+ "AudioDestinationNode",
+ "AudioBufferSourceNode", "ScriptProcessorNode", "AnalyserNode", "GainNode",
+ "DelayNode", "BiquadFilterNode", "WaveShaperNode", "PannerNode", "ConvolverNode",
+ "ChannelSplitterNode", "ChannelMergerNode", "DynamicsCompressorNode", "OscillatorNode"
+ ];
+
+ expectedTypes.forEach((type, i) => {
+ is(actualTypes[i], type, type + " successfully created with correct type");
+ });
+
+ yield removeTab(target.tab);
+});
diff --git a/toolkit/devtools/webaudioeditor/test/browser_audionode-actor-is-source.js b/toolkit/devtools/webaudioeditor/test/browser_audionode-actor-is-source.js
new file mode 100644
index 000000000..13523fb36
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/browser_audionode-actor-is-source.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test AudioNode#isSource()
+ */
+
+add_task(function*() {
+ let { target, front } = yield initBackend(SIMPLE_NODES_URL);
+ let [_, nodes] = yield Promise.all([
+ front.setup({ reload: true }),
+ getN(front, "create-node", 14)
+ ]);
+
+ let actualTypes = yield Promise.all(nodes.map(node => node.getType()));
+ let isSourceResult = yield Promise.all(nodes.map(node => node.isSource()));
+
+ actualTypes.forEach((type, i) => {
+ let shouldBeSource = type === "AudioBufferSourceNode" || type === "OscillatorNode";
+ if (shouldBeSource)
+ is(isSourceResult[i], true, type + "'s isSource() yields into `true`");
+ else
+ is(isSourceResult[i], false, type + "'s isSource() yields into `false`");
+ });
+
+ yield removeTab(target.tab);
+});
diff --git a/toolkit/devtools/webaudioeditor/test/browser_callwatcher-01.js b/toolkit/devtools/webaudioeditor/test/browser_callwatcher-01.js
new file mode 100644
index 000000000..a3dc801c6
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/browser_callwatcher-01.js
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Bug 1130901
+ * Tests to ensure that calling call/apply on methods wrapped
+ * via CallWatcher do not throw a security permissions error:
+ * "Error: Permission denied to access property 'call'"
+ */
+
+const BUG_1130901_URL = EXAMPLE_URL + "doc_bug_1130901.html";
+
+add_task(function*() {
+ let { target, panel } = yield initWebAudioEditor(BUG_1130901_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$, EVENTS, gAudioNodes } = panelWin;
+
+ reload(target);
+
+ yield waitForGraphRendered(panelWin, 3, 0);
+
+ ok(true, "Successfully created a node from AudioContext via `call`.");
+ ok(true, "Successfully created a node from AudioContext via `apply`.");
+
+ yield teardown(target);
+});
diff --git a/toolkit/devtools/webaudioeditor/test/browser_callwatcher-02.js b/toolkit/devtools/webaudioeditor/test/browser_callwatcher-02.js
new file mode 100644
index 000000000..8dc95e0c5
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/browser_callwatcher-02.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Bug 1112378
+ * Tests to ensure that errors called on wrapped functions via call-watcher
+ * correctly looks like the error comes from the content, not from within the devtools.
+ */
+
+const BUG_1112378_URL = EXAMPLE_URL + "doc_bug_1112378.html";
+
+add_task(function*() {
+ let { target, panel } = yield initWebAudioEditor(BUG_1112378_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$, EVENTS, gAudioNodes } = panelWin;
+
+ loadFrameScripts();
+
+ reload(target);
+
+ yield waitForGraphRendered(panelWin, 2, 0);
+
+ let error = yield evalInDebuggee("throwError()");
+ is(error.lineNumber, 21, "error has correct lineNumber");
+ is(error.columnNumber, 11, "error has correct columnNumber");
+ is(error.name, "TypeError", "error has correct name");
+ is(error.message, "Argument 1 is not valid for any of the 2-argument overloads of AudioNode.connect.", "error has correct message");
+ is(error.stringified, "TypeError: Argument 1 is not valid for any of the 2-argument overloads of AudioNode.connect.", "error is stringified correctly");
+ ise(error.instanceof, true, "error is correctly an instanceof TypeError");
+ is(error.fileName, "http://example.com/browser/browser/devtools/webaudioeditor/test/doc_bug_1112378.html", "error has correct fileName");
+
+ error = yield evalInDebuggee("throwDOMException()");
+ is(error.lineNumber, 37, "exception has correct lineNumber");
+ is(error.columnNumber, 0, "exception has correct columnNumber");
+ is(error.code, 9, "exception has correct code");
+ is(error.result, 2152923145, "exception has correct result");
+ is(error.name, "NotSupportedError", "exception has correct name");
+ is(error.message, "Operation is not supported", "exception has correct message");
+ is(error.stringified, "NotSupportedError: Operation is not supported", "exception is stringified correctly");
+ ise(error.instanceof, true, "exception is correctly an instance of DOMException");
+ is(error.filename, "http://example.com/browser/browser/devtools/webaudioeditor/test/doc_bug_1112378.html", "exception has correct filename");
+
+ yield teardown(target);
+});
diff --git a/toolkit/devtools/webaudioeditor/test/browser_wa_automation-view-01.js b/toolkit/devtools/webaudioeditor/test/browser_wa_automation-view-01.js
new file mode 100644
index 000000000..59aaf2795
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/browser_wa_automation-view-01.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that automation view shows the correct view depending on if events
+ * or params exist.
+ */
+
+add_task(function*() {
+ let { target, panel } = yield initWebAudioEditor(AUTOMATION_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$, EVENTS } = panelWin;
+
+ let started = once(gFront, "start-context");
+
+ reload(target);
+
+ let [actors] = yield Promise.all([
+ get3(gFront, "create-node"),
+ waitForGraphRendered(panelWin, 3, 2)
+ ]);
+ let nodeIds = actors.map(actor => actor.actorID);
+ let $tabbox = $("#web-audio-editor-tabs");
+
+ // Oscillator node
+ click(panelWin, findGraphNode(panelWin, nodeIds[1]));
+ yield waitForInspectorRender(panelWin, EVENTS);
+ $tabbox.selectedIndex = 1;
+
+ ok(isVisible($("#automation-graph-container")), "graph container should be visible");
+ ok(isVisible($("#automation-content")), "automation content should be visible");
+ ok(!isVisible($("#automation-no-events")), "no-events panel should not be visible");
+ ok(!isVisible($("#automation-empty")), "empty panel should not be visible");
+
+ // Gain node
+ click(panelWin, findGraphNode(panelWin, nodeIds[2]));
+ yield waitForInspectorRender(panelWin, EVENTS);
+ $tabbox.selectedIndex = 1;
+
+ ok(!isVisible($("#automation-graph-container")), "graph container should not be visible");
+ ok(isVisible($("#automation-content")), "automation content should be visible");
+ ok(isVisible($("#automation-no-events")), "no-events panel should be visible");
+ ok(!isVisible($("#automation-empty")), "empty panel should not be visible");
+
+ // destination node
+ click(panelWin, findGraphNode(panelWin, nodeIds[0]));
+ yield waitForInspectorRender(panelWin, EVENTS);
+ $tabbox.selectedIndex = 1;
+
+ ok(!isVisible($("#automation-graph-container")), "graph container should not be visible");
+ ok(!isVisible($("#automation-content")), "automation content should not be visible");
+ ok(!isVisible($("#automation-no-events")), "no-events panel should not be visible");
+ ok(isVisible($("#automation-empty")), "empty panel should be visible");
+
+ yield teardown(target);
+});
diff --git a/toolkit/devtools/webaudioeditor/test/browser_wa_automation-view-02.js b/toolkit/devtools/webaudioeditor/test/browser_wa_automation-view-02.js
new file mode 100644
index 000000000..ecb9957ae
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/browser_wa_automation-view-02.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that automation view selects the first parameter by default and
+ * switching between AudioParam rerenders the graph.
+ */
+
+add_task(function*() {
+ let { target, panel } = yield initWebAudioEditor(AUTOMATION_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$, EVENTS, AutomationView } = panelWin;
+
+ let started = once(gFront, "start-context");
+
+ reload(target);
+
+ let [actors] = yield Promise.all([
+ get3(gFront, "create-node"),
+ waitForGraphRendered(panelWin, 3, 2)
+ ]);
+ let nodeIds = actors.map(actor => actor.actorID);
+
+
+ // Oscillator node
+ click(panelWin, findGraphNode(panelWin, nodeIds[1]));
+ yield waitForInspectorRender(panelWin, EVENTS);
+ click(panelWin, $("#automation-tab"));
+
+ ok(AutomationView._selectedParamName, "frequency",
+ "AutomatioView is set on 'frequency'");
+ ok($(".automation-param-button[data-param='frequency']").getAttribute("selected"),
+ "frequency param should be selected on load");
+ ok(!$(".automation-param-button[data-param='detune']").getAttribute("selected"),
+ "detune param should not be selected on load");
+ ok(isVisible($("#automation-content")), "automation content should be visible");
+ ok(isVisible($("#automation-graph-container")), "graph container should be visible");
+ ok(!isVisible($("#automation-no-events")), "no-events panel should not be visible");
+
+ click(panelWin, $(".automation-param-button[data-param='detune']"));
+ yield once(panelWin, EVENTS.UI_AUTOMATION_TAB_RENDERED);
+
+ ok(true, "automation tab rerendered");
+
+ ok(AutomationView._selectedParamName, "detune",
+ "AutomatioView is set on 'detune'");
+ ok(!$(".automation-param-button[data-param='frequency']").getAttribute("selected"),
+ "frequency param should not be selected after clicking detune");
+ ok($(".automation-param-button[data-param='detune']").getAttribute("selected"),
+ "detune param should be selected after clicking detune");
+ ok(isVisible($("#automation-content")), "automation content should be visible");
+ ok(!isVisible($("#automation-graph-container")), "graph container should not be visible");
+ ok(isVisible($("#automation-no-events")), "no-events panel should be visible");
+
+ yield teardown(target);
+});
diff --git a/toolkit/devtools/webaudioeditor/test/browser_wa_controller-01.js b/toolkit/devtools/webaudioeditor/test/browser_wa_controller-01.js
new file mode 100644
index 000000000..3fb70b1ed
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/browser_wa_controller-01.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Bug 1125817
+ * Tests to ensure that disconnecting a node immediately
+ * after creating it does not fail.
+ */
+
+const BUG_1125817_URL = EXAMPLE_URL + "doc_bug_1125817.html";
+
+add_task(function*() {
+ let { target, panel } = yield initWebAudioEditor(BUG_1125817_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$, EVENTS, gAudioNodes } = panelWin;
+
+ reload(target);
+
+ let [actors] = yield Promise.all([
+ once(gAudioNodes, "add", 2),
+ once(gAudioNodes, "disconnect")
+ ]);
+
+ ok(true, "Successfully disconnected a just-created node.");
+
+ yield teardown(target);
+});
diff --git a/toolkit/devtools/webaudioeditor/test/browser_wa_destroy-node-01.js b/toolkit/devtools/webaudioeditor/test/browser_wa_destroy-node-01.js
new file mode 100644
index 000000000..ca62a581d
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/browser_wa_destroy-node-01.js
@@ -0,0 +1,64 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the destruction node event is fired and that the nodes are no
+ * longer stored internally in the tool, that the graph is updated properly, and
+ * that selecting a soon-to-be dead node clears the inspector.
+ *
+ * All done in one test since this test takes a few seconds to clear GC.
+ */
+
+add_task(function*() {
+ // Use a longer timeout as garbage collection event
+ // can be unpredictable.
+ requestLongerTimeout(2);
+
+ let { target, panel } = yield initWebAudioEditor(DESTROY_NODES_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$, gAudioNodes } = panelWin;
+
+ let started = once(gFront, "start-context");
+
+ reload(target);
+
+ let destroyed = getN(gAudioNodes, "remove", 10);
+
+ forceCC();
+
+ let [created] = yield Promise.all([
+ getNSpread(gAudioNodes, "add", 13),
+ waitForGraphRendered(panelWin, 13, 2)
+ ]);
+
+ // Flatten arrays of event arguments and take the first (AudioNodeModel)
+ // and get its ID.
+ let actorIDs = created.map(ev => ev[0].id);
+
+ // Click a soon-to-be dead buffer node
+ yield clickGraphNode(panelWin, actorIDs[5]);
+
+ forceCC();
+
+ // Wait for destruction and graph to re-render
+ yield Promise.all([destroyed, waitForGraphRendered(panelWin, 3, 2)]);
+
+ // Test internal storage
+ is(panelWin.gAudioNodes.length, 3, "All nodes should be GC'd except one gain, osc and dest node.");
+
+ // Test graph rendering
+ ok(findGraphNode(panelWin, actorIDs[0]), "dest should be in graph");
+ ok(findGraphNode(panelWin, actorIDs[1]), "osc should be in graph");
+ ok(findGraphNode(panelWin, actorIDs[2]), "gain should be in graph");
+
+ let { nodes, edges } = countGraphObjects(panelWin);
+
+ is(nodes, 3, "Only 3 nodes rendered in graph.");
+ is(edges, 2, "Only 2 edges rendered in graph.");
+
+ // Test that the inspector reset to no node selected
+ ok(isVisible($("#web-audio-editor-details-pane-empty")),
+ "InspectorView empty message should show if the currently selected node gets collected.");
+
+ yield teardown(target);
+});
diff --git a/toolkit/devtools/webaudioeditor/test/browser_wa_first-run.js b/toolkit/devtools/webaudioeditor/test/browser_wa_first-run.js
new file mode 100644
index 000000000..5229b0923
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/browser_wa_first-run.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+///////////////////
+//
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejection should be fixed.
+//
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Connection closed");
+
+/**
+ * Tests that the reloading/onContentLoaded hooks work.
+ */
+
+add_task(function*() {
+ let { target, panel } = yield initWebAudioEditor(SIMPLE_CONTEXT_URL);
+ let { gFront, $ } = panel.panelWin;
+
+ is($("#reload-notice").hidden, false,
+ "The 'reload this page' notice should initially be visible.");
+ is($("#waiting-notice").hidden, true,
+ "The 'waiting for an audio context' notice should initially be hidden.");
+ is($("#content").hidden, true,
+ "The tool's content should initially be hidden.");
+
+ let navigating = once(target, "will-navigate");
+ let started = once(gFront, "start-context");
+
+ reload(target);
+
+ yield navigating;
+
+ is($("#reload-notice").hidden, true,
+ "The 'reload this page' notice should be hidden when navigating.");
+ is($("#waiting-notice").hidden, false,
+ "The 'waiting for an audio context' notice should be visible when navigating.");
+ is($("#content").hidden, true,
+ "The tool's content should still be hidden.");
+
+ yield started;
+
+ is($("#reload-notice").hidden, true,
+ "The 'reload this page' notice should be hidden after context found.");
+ is($("#waiting-notice").hidden, true,
+ "The 'waiting for an audio context' notice should be hidden after context found.");
+ is($("#content").hidden, false,
+ "The tool's content should not be hidden anymore.");
+
+ yield teardown(target);
+});
diff --git a/toolkit/devtools/webaudioeditor/test/browser_wa_graph-click.js b/toolkit/devtools/webaudioeditor/test/browser_wa_graph-click.js
new file mode 100644
index 000000000..d14d3e027
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/browser_wa_graph-click.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the clicking on a node in the GraphView opens and sets
+ * the correct node in the InspectorView
+ */
+
+add_task(function*() {
+ let { target, panel } = yield initWebAudioEditor(COMPLEX_CONTEXT_URL);
+ let panelWin = panel.panelWin;
+ let { gFront, $, $$, InspectorView } = panelWin;
+
+ let started = once(gFront, "start-context");
+
+ reload(target);
+
+ let [actors, _] = yield Promise.all([
+ getN(gFront, "create-node", 8),
+ waitForGraphRendered(panel.panelWin, 8, 8)
+ ]);
+
+ let nodeIds = actors.map(actor => actor.actorID);
+
+ ok(!InspectorView.isVisible(), "InspectorView hidden on start.");
+
+ yield clickGraphNode(panelWin, nodeIds[1], true);
+
+ ok(InspectorView.isVisible(), "InspectorView visible after selecting a node.");
+ is(InspectorView.getCurrentAudioNode().id, nodeIds[1], "InspectorView has correct node set.");
+
+ yield clickGraphNode(panelWin, nodeIds[2]);
+
+ ok(InspectorView.isVisible(), "InspectorView still visible after selecting another node.");
+ is(InspectorView.getCurrentAudioNode().id, nodeIds[2], "InspectorView has correct node set on second node.");
+
+ yield clickGraphNode(panelWin, nodeIds[2]);
+ is(InspectorView.getCurrentAudioNode().id, nodeIds[2], "Clicking the same node again works (idempotent).");
+
+ yield clickGraphNode(panelWin, $("rect", findGraphNode(panelWin, nodeIds[3])));
+ is(InspectorView.getCurrentAudioNode().id, nodeIds[3], "Clicking on a <rect> works as expected.");
+
+ yield clickGraphNode(panelWin, $("tspan", findGraphNode(panelWin, nodeIds[4])));
+ is(InspectorView.getCurrentAudioNode().id, nodeIds[4], "Clicking on a <tspan> works as expected.");
+
+ ok(InspectorView.isVisible(),
+ "InspectorView still visible after several nodes have been clicked.");
+
+ yield teardown(target);
+});
diff --git a/toolkit/devtools/webaudioeditor/test/browser_wa_graph-markers.js b/toolkit/devtools/webaudioeditor/test/browser_wa_graph-markers.js
new file mode 100644
index 000000000..684561161
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/browser_wa_graph-markers.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the SVG marker styling is updated when devtools theme changes.
+ */
+
+add_task(function*() {
+ let { target, panel } = yield initWebAudioEditor(SIMPLE_CONTEXT_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$, MARKER_STYLING } = panelWin;
+
+ let currentTheme = Services.prefs.getCharPref("devtools.theme");
+
+ ok(MARKER_STYLING.light, "Marker styling exists for light theme.");
+ ok(MARKER_STYLING.dark, "Marker styling exists for dark theme.");
+
+ let started = once(gFront, "start-context");
+
+ reload(target);
+
+ let [actors] = yield Promise.all([
+ get3(gFront, "create-node"),
+ waitForGraphRendered(panelWin, 3, 2)
+ ]);
+
+ is(getFill($("#arrowhead")), MARKER_STYLING[currentTheme],
+ "marker initially matches theme.");
+
+ // Switch to light
+ setTheme("light");
+ is(getFill($("#arrowhead")), MARKER_STYLING.light,
+ "marker styling matches light theme on change.");
+
+ // Switch to dark
+ setTheme("dark");
+ is(getFill($("#arrowhead")), MARKER_STYLING.dark,
+ "marker styling matches dark theme on change.");
+
+ // Switch to dark again
+ setTheme("dark");
+ is(getFill($("#arrowhead")), MARKER_STYLING.dark,
+ "marker styling remains dark.");
+
+ // Switch to back to light again
+ setTheme("light");
+ is(getFill($("#arrowhead")), MARKER_STYLING.light,
+ "marker styling switches back to light once again.");
+
+ yield teardown(target);
+});
+
+/**
+ * Returns a hex value found in styling for an element. So parses
+ * <marker style="fill: #abcdef"> and returns "#abcdef"
+ */
+function getFill (el) {
+ return el.getAttribute("style").match(/(#.*)$/)[1];
+}
+
+/**
+ * Mimics selecting the theme selector in the toolbox;
+ * sets the preference and emits an event on gDevTools to trigger
+ * the themeing.
+ */
+function setTheme (newTheme) {
+ let oldTheme = Services.prefs.getCharPref("devtools.theme");
+ info("Setting `devtools.theme` to \"" + newTheme + "\"");
+ Services.prefs.setCharPref("devtools.theme", newTheme);
+ gDevTools.emit("pref-changed", {
+ pref: "devtools.theme",
+ newValue: newTheme,
+ oldValue: oldTheme
+ });
+}
diff --git a/toolkit/devtools/webaudioeditor/test/browser_wa_graph-render-01.js b/toolkit/devtools/webaudioeditor/test/browser_wa_graph-render-01.js
new file mode 100644
index 000000000..fdbe1ce5d
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/browser_wa_graph-render-01.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that SVG nodes and edges were created for the Graph View.
+ */
+
+let connectCount = 0;
+
+add_task(function*() {
+ let { target, panel } = yield initWebAudioEditor(SIMPLE_CONTEXT_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$, EVENTS, gAudioNodes } = panelWin;
+
+ let started = once(gFront, "start-context");
+
+ reload(target);
+
+ gAudioNodes.on("connect", onConnectNode);
+
+ let [actors] = yield Promise.all([
+ get3(gFront, "create-node"),
+ waitForGraphRendered(panelWin, 3, 2)
+ ]);
+
+ let [destId, oscId, gainId] = actors.map(actor => actor.actorID);
+
+ ok(findGraphNode(panelWin, oscId).classList.contains("type-OscillatorNode"), "found OscillatorNode with class");
+ ok(findGraphNode(panelWin, gainId).classList.contains("type-GainNode"), "found GainNode with class");
+ ok(findGraphNode(panelWin, destId).classList.contains("type-AudioDestinationNode"), "found AudioDestinationNode with class");
+ is(findGraphEdge(panelWin, oscId, gainId).toString(), "[object SVGGElement]", "found edge for osc -> gain");
+ is(findGraphEdge(panelWin, gainId, destId).toString(), "[object SVGGElement]", "found edge for gain -> dest");
+
+ yield wait(1000);
+
+ is(connectCount, 2, "Only two node connect events should be fired.");
+
+ gAudioNodes.off("connect", onConnectNode);
+
+ yield teardown(target);
+});
+
+function onConnectNode () {
+ ++connectCount;
+}
diff --git a/toolkit/devtools/webaudioeditor/test/browser_wa_graph-render-02.js b/toolkit/devtools/webaudioeditor/test/browser_wa_graph-render-02.js
new file mode 100644
index 000000000..825ab76b9
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/browser_wa_graph-render-02.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests more edge rendering for complex graphs.
+ */
+
+add_task(function*() {
+ let { target, panel } = yield initWebAudioEditor(COMPLEX_CONTEXT_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$ } = panelWin;
+
+ let started = once(gFront, "start-context");
+
+ reload(target);
+
+ let [actors] = yield Promise.all([
+ getN(gFront, "create-node", 8),
+ waitForGraphRendered(panelWin, 8, 8)
+ ]);
+
+ let nodeIDs = actors.map(actor => actor.actorID);
+
+ let types = ["AudioDestinationNode", "OscillatorNode", "GainNode", "ScriptProcessorNode",
+ "OscillatorNode", "GainNode", "AudioBufferSourceNode", "BiquadFilterNode"];
+
+
+ types.forEach((type, i) => {
+ ok(findGraphNode(panelWin, nodeIDs[i]).classList.contains("type-" + type), "found " + type + " with class");
+ });
+
+ let edges = [
+ [1, 2, "osc1 -> gain1"],
+ [1, 3, "osc1 -> proc"],
+ [2, 0, "gain1 -> dest"],
+ [4, 5, "osc2 -> gain2"],
+ [5, 0, "gain2 -> dest"],
+ [6, 7, "buf -> filter"],
+ [4, 7, "osc2 -> filter"],
+ [7, 0, "filter -> dest"],
+ ];
+
+ edges.forEach(([source, target, msg], i) => {
+ is(findGraphEdge(panelWin, nodeIDs[source], nodeIDs[target]).toString(), "[object SVGGElement]",
+ "found edge for " + msg);
+ });
+
+ yield teardown(target);
+});
diff --git a/toolkit/devtools/webaudioeditor/test/browser_wa_graph-render-03.js b/toolkit/devtools/webaudioeditor/test/browser_wa_graph-render-03.js
new file mode 100644
index 000000000..4486483fa
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/browser_wa_graph-render-03.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests to ensure that selected nodes stay selected on graph redraw.
+ */
+
+add_task(function*() {
+ let { target, panel } = yield initWebAudioEditor(SIMPLE_CONTEXT_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$, EVENTS } = panelWin;
+
+ reload(target);
+
+ let [actors] = yield Promise.all([
+ getN(gFront, "create-node", 3),
+ waitForGraphRendered(panelWin, 3, 2)
+ ]);
+
+ let [dest, osc, gain] = actors;
+
+ yield clickGraphNode(panelWin, gain.actorID);
+ ok(findGraphNode(panelWin, gain.actorID).classList.contains("selected"),
+ "Node selected once.");
+
+ // Disconnect a node to trigger a rerender
+ osc.disconnect();
+
+ yield once(panelWin, EVENTS.UI_GRAPH_RENDERED);
+
+ ok(findGraphNode(panelWin, gain.actorID).classList.contains("selected"),
+ "Node still selected after rerender.");
+
+ yield teardown(target);
+});
diff --git a/toolkit/devtools/webaudioeditor/test/browser_wa_graph-render-04.js b/toolkit/devtools/webaudioeditor/test/browser_wa_graph-render-04.js
new file mode 100644
index 000000000..84db904a9
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/browser_wa_graph-render-04.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests audio param connection rendering.
+ */
+
+add_task(function*() {
+ let { target, panel } = yield initWebAudioEditor(CONNECT_MULTI_PARAM_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$, EVENTS } = panelWin;
+
+ let started = once(gFront, "start-context");
+
+ reload(target);
+
+ let [actors] = yield Promise.all([
+ getN(gFront, "create-node", 5),
+ waitForGraphRendered(panelWin, 5, 2, 3)
+ ]);
+
+ let nodeIDs = actors.map(actor => actor.actorID);
+
+ let [, carrier, gain, mod1, mod2] = nodeIDs;
+
+ let edges = [
+ [mod1, gain, "gain", "mod1 -> gain[gain]"],
+ [mod2, carrier, "frequency", "mod2 -> carrier[frequency]"],
+ [mod2, carrier, "detune", "mod2 -> carrier[detune]"]
+ ];
+
+ edges.forEach(([source, target, param, msg], i) => {
+ let edge = findGraphEdge(panelWin, source, target, param);
+ ok(edge.classList.contains("param-connection"), "edge is classified as a param-connection");
+ });
+
+ yield teardown(target);
+});
diff --git a/toolkit/devtools/webaudioeditor/test/browser_wa_graph-render-05.js b/toolkit/devtools/webaudioeditor/test/browser_wa_graph-render-05.js
new file mode 100644
index 000000000..b1ddd78b3
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/browser_wa_graph-render-05.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests to ensure that param connections trigger graph redraws
+ */
+
+add_task(function*() {
+ let { target, panel } = yield initWebAudioEditor(SIMPLE_CONTEXT_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$, EVENTS } = panelWin;
+
+ reload(target);
+
+ let [actors] = yield Promise.all([
+ getN(gFront, "create-node", 3),
+ waitForGraphRendered(panelWin, 3, 2, 0)
+ ]);
+
+ let [dest, osc, gain] = actors;
+
+ yield osc.disconnect();
+
+ osc.connectParam(gain, "gain");
+ yield waitForGraphRendered(panelWin, 3, 1, 1);
+ ok(true, "Graph re-rendered upon param connection");
+
+ yield teardown(target);
+});
diff --git a/toolkit/devtools/webaudioeditor/test/browser_wa_graph-selected.js b/toolkit/devtools/webaudioeditor/test/browser_wa_graph-selected.js
new file mode 100644
index 000000000..4ee7e04fe
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/browser_wa_graph-selected.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that SVG nodes and edges were created for the Graph View.
+ */
+
+add_task(function*() {
+ let { target, panel } = yield initWebAudioEditor(SIMPLE_CONTEXT_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$, EVENTS } = panelWin;
+
+ let started = once(gFront, "start-context");
+
+ reload(target);
+
+ let [actors] = yield Promise.all([
+ get3(gFront, "create-node"),
+ waitForGraphRendered(panelWin, 3, 2)
+ ]);
+
+ let [destId, oscId, gainId] = actors.map(actor => actor.actorID);
+
+ ok(!findGraphNode(panelWin, destId).classList.contains("selected"),
+ "No nodes selected on start. (destination)");
+ ok(!findGraphNode(panelWin, oscId).classList.contains("selected"),
+ "No nodes selected on start. (oscillator)");
+ ok(!findGraphNode(panelWin, gainId).classList.contains("selected"),
+ "No nodes selected on start. (gain)");
+
+ yield clickGraphNode(panelWin, oscId);
+
+ ok(findGraphNode(panelWin, oscId).classList.contains("selected"),
+ "Selected node has class 'selected'.");
+ ok(!findGraphNode(panelWin, destId).classList.contains("selected"),
+ "Non-selected nodes do not have class 'selected'.");
+ ok(!findGraphNode(panelWin, gainId).classList.contains("selected"),
+ "Non-selected nodes do not have class 'selected'.");
+
+ yield clickGraphNode(panelWin, gainId);
+
+ ok(!findGraphNode(panelWin, oscId).classList.contains("selected"),
+ "Previously selected node no longer has class 'selected'.");
+ ok(!findGraphNode(panelWin, destId).classList.contains("selected"),
+ "Non-selected nodes do not have class 'selected'.");
+ ok(findGraphNode(panelWin, gainId).classList.contains("selected"),
+ "Newly selected node now has class 'selected'.");
+
+ yield teardown(target);
+});
diff --git a/toolkit/devtools/webaudioeditor/test/browser_wa_graph-zoom.js b/toolkit/devtools/webaudioeditor/test/browser_wa_graph-zoom.js
new file mode 100644
index 000000000..d06cc1549
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/browser_wa_graph-zoom.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the graph's scale and position is reset on a page reload.
+ */
+
+add_task(function*() {
+ let { target, panel } = yield initWebAudioEditor(SIMPLE_CONTEXT_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$, EVENTS, ContextView } = panelWin;
+
+ let started = once(gFront, "start-context");
+
+ yield Promise.all([
+ reload(target),
+ waitForGraphRendered(panelWin, 3, 2)
+ ]);
+
+ is(ContextView.getCurrentScale(), 1, "Default graph scale is 1.");
+ is(ContextView.getCurrentTranslation()[0], 20, "Default x-translation is 20.");
+ is(ContextView.getCurrentTranslation()[1], 20, "Default y-translation is 20.");
+
+ // Change both attribute and D3's internal store
+ panelWin.d3.select("#graph-target").attr("transform", "translate([100, 400]) scale(10)");
+ ContextView._zoomBinding.scale(10);
+ ContextView._zoomBinding.translate([100, 400]);
+
+ is(ContextView.getCurrentScale(), 10, "After zoom, scale is 10.");
+ is(ContextView.getCurrentTranslation()[0], 100, "After zoom, x-translation is 100.");
+ is(ContextView.getCurrentTranslation()[1], 400, "After zoom, y-translation is 400.");
+
+ yield Promise.all([
+ reload(target),
+ waitForGraphRendered(panelWin, 3, 2)
+ ]);
+
+ is(ContextView.getCurrentScale(), 1, "After refresh, graph scale is 1.");
+ is(ContextView.getCurrentTranslation()[0], 20, "After refresh, x-translation is 20.");
+ is(ContextView.getCurrentTranslation()[1], 20, "After refresh, y-translation is 20.");
+
+ yield teardown(target);
+});
diff --git a/toolkit/devtools/webaudioeditor/test/browser_wa_inspector-bypass-01.js b/toolkit/devtools/webaudioeditor/test/browser_wa_inspector-bypass-01.js
new file mode 100644
index 000000000..b6c2a061c
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/browser_wa_inspector-bypass-01.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that nodes are correctly bypassed when bypassing.
+ */
+
+add_task(function*() {
+ let { target, panel } = yield initWebAudioEditor(SIMPLE_CONTEXT_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$, EVENTS, gAudioNodes } = panelWin;
+
+ reload(target);
+
+ let [actors] = yield Promise.all([
+ get3(gFront, "create-node"),
+ waitForGraphRendered(panelWin, 3, 2)
+ ]);
+ let nodeIds = actors.map(actor => actor.actorID);
+
+ click(panelWin, findGraphNode(panelWin, nodeIds[1]));
+ // Wait for the node to be set as well as the inspector to come fully into the view
+ yield Promise.all([
+ waitForInspectorRender(panelWin, EVENTS),
+ once(panelWin, EVENTS.UI_INSPECTOR_TOGGLED)
+ ]);
+
+ let $bypass = $("toolbarbutton.bypass");
+
+ is((yield actors[1].isBypassed()), false, "AudioNodeActor is not bypassed by default.")
+ is($bypass.checked, true, "Button is 'on' for normal nodes");
+ is($bypass.disabled, false, "Bypass button is not disabled for normal nodes");
+
+ command($bypass);
+ yield gAudioNodes.once("bypass");
+
+ is((yield actors[1].isBypassed()), true, "AudioNodeActor is bypassed.")
+ is($bypass.checked, false, "Button is 'off' when clicked");
+ is($bypass.disabled, false, "Bypass button is not disabled after click");
+ ok(findGraphNode(panelWin, nodeIds[1]).classList.contains("bypassed"),
+ "AudioNode has 'bypassed' class.");
+
+ command($bypass);
+ yield gAudioNodes.once("bypass");
+
+ is((yield actors[1].isBypassed()), false, "AudioNodeActor is no longer bypassed.")
+ is($bypass.checked, true, "Button is back on when clicked");
+ is($bypass.disabled, false, "Bypass button is not disabled after click");
+ ok(!findGraphNode(panelWin, nodeIds[1]).classList.contains("bypassed"),
+ "AudioNode no longer has 'bypassed' class.");
+
+ click(panelWin, findGraphNode(panelWin, nodeIds[0]));
+
+ yield once(panelWin, EVENTS.UI_INSPECTOR_NODE_SET);
+
+ is((yield actors[0].isBypassed()), false, "Unbypassable AudioNodeActor is not bypassed.");
+ is($bypass.checked, false, "Button is 'off' for unbypassable nodes");
+ is($bypass.disabled, true, "Bypass button is disabled for unbypassable nodes");
+
+ command($bypass);
+ is((yield actors[0].isBypassed()), false,
+ "Clicking button on unbypassable node does not change bypass state on actor.");
+ is($bypass.checked, false, "Button is still 'off' for unbypassable nodes");
+ is($bypass.disabled, true, "Bypass button is still disabled for unbypassable nodes");
+
+ yield teardown(target);
+});
diff --git a/toolkit/devtools/webaudioeditor/test/browser_wa_inspector-toggle.js b/toolkit/devtools/webaudioeditor/test/browser_wa_inspector-toggle.js
new file mode 100644
index 000000000..33b20559d
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/browser_wa_inspector-toggle.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the inspector toggle button shows and hides
+ * the inspector panel as intended.
+ */
+
+add_task(function*() {
+ let { target, panel } = yield initWebAudioEditor(SIMPLE_CONTEXT_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$, EVENTS, InspectorView } = panelWin;
+ let gVars = InspectorView._propsView;
+
+ let started = once(gFront, "start-context");
+
+ reload(target);
+
+ let [actors] = yield Promise.all([
+ get3(gFront, "create-node"),
+ waitForGraphRendered(panelWin, 3, 2)
+ ]);
+ let nodeIds = actors.map(actor => actor.actorID);
+
+ ok(!InspectorView.isVisible(), "InspectorView hidden on start.");
+
+ // Open inspector pane
+ $("#inspector-pane-toggle").click();
+ yield once(panelWin, EVENTS.UI_INSPECTOR_TOGGLED);
+
+ ok(InspectorView.isVisible(), "InspectorView shown after toggling.");
+
+ ok(isVisible($("#web-audio-editor-details-pane-empty")),
+ "InspectorView empty message should still be visible.");
+ ok(!isVisible($("#web-audio-editor-tabs")),
+ "InspectorView tabs view should still be hidden.");
+
+ // Close inspector pane
+ $("#inspector-pane-toggle").click();
+ yield once(panelWin, EVENTS.UI_INSPECTOR_TOGGLED);
+
+ ok(!InspectorView.isVisible(), "InspectorView back to being hidden.");
+
+ // Open again to test node loading while open
+ $("#inspector-pane-toggle").click();
+ yield once(panelWin, EVENTS.UI_INSPECTOR_TOGGLED);
+
+ ok(InspectorView.isVisible(), "InspectorView being shown.");
+ ok(!isVisible($("#web-audio-editor-tabs")),
+ "InspectorView tabs are still hidden.");
+
+ yield clickGraphNode(panelWin, findGraphNode(panelWin, nodeIds[1]));
+
+ ok(!isVisible($("#web-audio-editor-details-pane-empty")),
+ "Empty message hides even when loading node while open.");
+ ok(isVisible($("#web-audio-editor-tabs")),
+ "Switches to tab view when loading node while open.");
+
+ yield teardown(target);
+});
diff --git a/toolkit/devtools/webaudioeditor/test/browser_wa_inspector-width.js b/toolkit/devtools/webaudioeditor/test/browser_wa_inspector-width.js
new file mode 100644
index 000000000..5d2c44c5b
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/browser_wa_inspector-width.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test that the WebAudioInspector's Width is saved as
+ * a preference
+ */
+
+add_task(function*() {
+ let { target, panel } = yield initWebAudioEditor(SIMPLE_CONTEXT_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$, EVENTS, InspectorView } = panelWin;
+ let gVars = InspectorView._propsView;
+
+ let started = once(gFront, "start-context");
+
+ reload(target);
+
+ let [actors] = yield Promise.all([
+ get3(gFront, "create-node"),
+ waitForGraphRendered(panelWin, 3, 2)
+ ]);
+ let nodeIds = actors.map(actor => actor.actorID);
+
+ ok(!InspectorView.isVisible(), "InspectorView hidden on start.");
+
+ // Open inspector pane
+ $("#inspector-pane-toggle").click();
+ yield once(panelWin, EVENTS.UI_INSPECTOR_TOGGLED);
+
+ let newInspectorWidth = 500;
+
+ // Setting width to new_inspector_width
+ $("#web-audio-inspector").setAttribute("width", newInspectorWidth);
+ reload(target);
+
+ //Width should be 500 after reloading
+ [actors] = yield Promise.all([
+ get3(gFront, "create-node"),
+ waitForGraphRendered(panelWin, 3, 2)
+ ]);
+ nodeIds = actors.map(actor => actor.actorID);
+
+ // Open inspector pane
+ $("#inspector-pane-toggle").click();
+ yield once(panelWin, EVENTS.UI_INSPECTOR_TOGGLED);
+
+ yield clickGraphNode(panelWin, findGraphNode(panelWin, nodeIds[1]));
+
+ // Getting the width of the audio inspector
+ let width = $("#web-audio-inspector").getAttribute("width");
+
+ is(width, newInspectorWidth, "WebAudioEditor's Inspector width should be saved as a preference");
+
+ yield teardown(target);
+});
diff --git a/toolkit/devtools/webaudioeditor/test/browser_wa_inspector.js b/toolkit/devtools/webaudioeditor/test/browser_wa_inspector.js
new file mode 100644
index 000000000..083bf2671
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/browser_wa_inspector.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that inspector view opens on graph node click, and
+ * loads the correct node inside the inspector.
+ */
+
+add_task(function*() {
+ let { target, panel } = yield initWebAudioEditor(SIMPLE_CONTEXT_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$, EVENTS, InspectorView } = panelWin;
+ let gVars = InspectorView._propsView;
+
+ let started = once(gFront, "start-context");
+
+ reload(target);
+
+ let [actors] = yield Promise.all([
+ get3(gFront, "create-node"),
+ waitForGraphRendered(panelWin, 3, 2)
+ ]);
+ let nodeIds = actors.map(actor => actor.actorID);
+
+ ok(!InspectorView.isVisible(), "InspectorView hidden on start.");
+ ok(isVisible($("#web-audio-editor-details-pane-empty")),
+ "InspectorView empty message should show when no node's selected.");
+ ok(!isVisible($("#web-audio-editor-tabs")),
+ "InspectorView tabs view should be hidden when no node's selected.");
+
+ // Wait for the node to be set as well as the inspector to come fully into the view
+ yield clickGraphNode(panelWin, findGraphNode(panelWin, nodeIds[1]), true);
+
+ ok(InspectorView.isVisible(), "InspectorView shown once node selected.");
+ ok(!isVisible($("#web-audio-editor-details-pane-empty")),
+ "InspectorView empty message hidden when node selected.");
+ ok(isVisible($("#web-audio-editor-tabs")),
+ "InspectorView tabs view visible when node selected.");
+
+ is($("#web-audio-editor-tabs").selectedIndex, 0,
+ "default tab selected should be the parameters tab.");
+
+ yield clickGraphNode(panelWin, findGraphNode(panelWin, nodeIds[2]));
+
+ yield teardown(target);
+});
diff --git a/toolkit/devtools/webaudioeditor/test/browser_wa_navigate.js b/toolkit/devtools/webaudioeditor/test/browser_wa_navigate.js
new file mode 100644
index 000000000..598deabd3
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/browser_wa_navigate.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests naviating from a page to another will repopulate
+ * the audio graph if both pages have an AudioContext.
+ */
+
+add_task(function*() {
+ let { target, panel } = yield initWebAudioEditor(SIMPLE_CONTEXT_URL);
+ let { panelWin } = panel;
+ let { gFront, $ } = panelWin;
+
+ reload(target);
+
+ var [actors] = yield Promise.all([
+ get3(gFront, "create-node"),
+ waitForGraphRendered(panelWin, 3, 2)
+ ]);
+
+ var { nodes, edges } = countGraphObjects(panelWin);
+ ise(nodes, 3, "should only be 3 nodes.");
+ ise(edges, 2, "should only be 2 edges.");
+
+ navigate(target, SIMPLE_NODES_URL);
+
+ var [actors] = yield Promise.all([
+ getN(gFront, "create-node", 15),
+ waitForGraphRendered(panelWin, 15, 0)
+ ]);
+
+ is($("#reload-notice").hidden, true,
+ "The 'reload this page' notice should be hidden after context found after navigation.");
+ is($("#waiting-notice").hidden, true,
+ "The 'waiting for an audio context' notice should be hidden after context found after navigation.");
+ is($("#content").hidden, false,
+ "The tool's content should reappear without closing and reopening the toolbox.");
+
+ var { nodes, edges } = countGraphObjects(panelWin);
+ ise(nodes, 15, "after navigation, should have 15 nodes");
+ ise(edges, 0, "after navigation, should have 0 edges.");
+
+ yield teardown(target);
+});
diff --git a/toolkit/devtools/webaudioeditor/test/browser_wa_properties-view-edit-01.js b/toolkit/devtools/webaudioeditor/test/browser_wa_properties-view-edit-01.js
new file mode 100644
index 000000000..bebcbc32d
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/browser_wa_properties-view-edit-01.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that properties are updated when modifying the VariablesView.
+ */
+
+add_task(function*() {
+ let { target, panel } = yield initWebAudioEditor(SIMPLE_CONTEXT_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$, EVENTS, PropertiesView } = panelWin;
+ let gVars = PropertiesView._propsView;
+
+ let started = once(gFront, "start-context");
+
+ reload(target);
+
+ let [actors] = yield Promise.all([
+ get3(gFront, "create-node"),
+ waitForGraphRendered(panelWin, 3, 2)
+ ]);
+ let nodeIds = actors.map(actor => actor.actorID);
+
+ click(panelWin, findGraphNode(panelWin, nodeIds[1]));
+ // Wait for the node to be set as well as the inspector to come fully into the view
+ yield Promise.all([
+ waitForInspectorRender(panelWin, EVENTS),
+ once(panelWin, EVENTS.UI_INSPECTOR_TOGGLED)
+ ]);
+
+ let setAndCheck = setAndCheckVariable(panelWin, gVars);
+
+ checkVariableView(gVars, 0, {
+ "type": "sine",
+ "frequency": 440,
+ "detune": 0
+ }, "default loaded string");
+
+ click(panelWin, findGraphNode(panelWin, nodeIds[2]));
+ yield waitForInspectorRender(panelWin, EVENTS),
+ checkVariableView(gVars, 0, {
+ "gain": 0
+ }, "default loaded number");
+
+ click(panelWin, findGraphNode(panelWin, nodeIds[1]));
+ yield waitForInspectorRender(panelWin, EVENTS),
+ yield setAndCheck(0, "type", "square", "square", "sets string as string");
+
+ click(panelWin, findGraphNode(panelWin, nodeIds[2]));
+ yield waitForInspectorRender(panelWin, EVENTS),
+ yield setAndCheck(0, "gain", "0.005", 0.005, "sets number as number");
+ yield setAndCheck(0, "gain", "0.1", 0.1, "sets float as float");
+ yield setAndCheck(0, "gain", ".2", 0.2, "sets float without leading zero as float");
+
+ yield teardown(target);
+});
+
+function setAndCheckVariable (panelWin, gVars) {
+ return Task.async(function (varNum, prop, value, expected, desc) {
+ yield modifyVariableView(panelWin, gVars, varNum, prop, value);
+ var props = {};
+ props[prop] = expected;
+ checkVariableView(gVars, varNum, props, desc);
+ });
+}
diff --git a/toolkit/devtools/webaudioeditor/test/browser_wa_properties-view-edit-02.js b/toolkit/devtools/webaudioeditor/test/browser_wa_properties-view-edit-02.js
new file mode 100644
index 000000000..f7f439832
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/browser_wa_properties-view-edit-02.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that properties are not updated when modifying the VariablesView.
+ */
+
+add_task(function*() {
+ let { target, panel } = yield initWebAudioEditor(COMPLEX_CONTEXT_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$, EVENTS, PropertiesView } = panelWin;
+ let gVars = PropertiesView._propsView;
+
+ let started = once(gFront, "start-context");
+
+ reload(target);
+
+ let [actors] = yield Promise.all([
+ getN(gFront, "create-node", 8),
+ waitForGraphRendered(panelWin, 8, 8)
+ ]);
+ let nodeIds = actors.map(actor => actor.actorID);
+
+ click(panelWin, findGraphNode(panelWin, nodeIds[3]));
+ // Wait for the node to be set as well as the inspector to come fully into the view
+ yield Promise.all([
+ waitForInspectorRender(panelWin, EVENTS),
+ once(panelWin, EVENTS.UI_INSPECTOR_TOGGLED),
+ ]);
+
+ let errorEvent = once(panelWin, EVENTS.UI_SET_PARAM_ERROR);
+
+ try {
+ yield modifyVariableView(panelWin, gVars, 0, "bufferSize", 2048);
+ } catch(e) {
+ // we except modifyVariableView to fail here, because bufferSize is not writable
+ }
+
+ yield errorEvent;
+
+ checkVariableView(gVars, 0, {bufferSize: 4096}, "check that unwritable variable is not updated");
+
+ yield teardown(target);
+});
diff --git a/toolkit/devtools/webaudioeditor/test/browser_wa_properties-view-media-nodes.js b/toolkit/devtools/webaudioeditor/test/browser_wa_properties-view-media-nodes.js
new file mode 100644
index 000000000..917ecc390
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/browser_wa_properties-view-media-nodes.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that params view correctly displays all properties for nodes
+ * correctly, with default values and correct types.
+ */
+
+let MEDIA_PERMISSION = "media.navigator.permission.disabled";
+
+function waitForDeviceClosed() {
+ info("Checking that getUserMedia streams are no longer in use.");
+
+ let temp = {};
+ Cu.import("resource:///modules/webrtcUI.jsm", temp);
+ let webrtcUI = temp.webrtcUI;
+
+ if (!webrtcUI.showGlobalIndicator)
+ return Promise.resolve();
+
+ let deferred = Promise.defer();
+
+ const message = "webrtc:UpdateGlobalIndicators";
+ let ppmm = Cc["@mozilla.org/parentprocessmessagemanager;1"]
+ .getService(Ci.nsIMessageBroadcaster);
+ ppmm.addMessageListener(message, function listener(aMessage) {
+ info("Received " + message + " message");
+ if (!aMessage.data.showGlobalIndicator) {
+ ppmm.removeMessageListener(message, listener);
+ deferred.resolve();
+ }
+ });
+
+ return deferred.promise;
+}
+
+add_task(function*() {
+ let { target, panel } = yield initWebAudioEditor(MEDIA_NODES_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$, EVENTS, PropertiesView } = panelWin;
+ let gVars = PropertiesView._propsView;
+
+ // Auto enable getUserMedia
+ let mediaPermissionPref = Services.prefs.getBoolPref(MEDIA_PERMISSION);
+ Services.prefs.setBoolPref(MEDIA_PERMISSION, true);
+
+ reload(target);
+
+ let [actors] = yield Promise.all([
+ getN(gFront, "create-node", 4),
+ waitForGraphRendered(panelWin, 4, 0)
+ ]);
+
+ let nodeIds = actors.map(actor => actor.actorID);
+ let types = [
+ "AudioDestinationNode", "MediaElementAudioSourceNode",
+ "MediaStreamAudioSourceNode", "MediaStreamAudioDestinationNode"
+ ];
+
+ for (let i = 0; i < types.length; i++) {
+ click(panelWin, findGraphNode(panelWin, nodeIds[i]));
+ yield waitForInspectorRender(panelWin, EVENTS);
+ checkVariableView(gVars, 0, NODE_DEFAULT_VALUES[types[i]], types[i]);
+ }
+
+ // Reset permissions on getUserMedia
+ Services.prefs.setBoolPref(MEDIA_PERMISSION, mediaPermissionPref);
+
+ yield teardown(target);
+
+ yield waitForDeviceClosed();
+});
diff --git a/toolkit/devtools/webaudioeditor/test/browser_wa_properties-view-params-objects.js b/toolkit/devtools/webaudioeditor/test/browser_wa_properties-view-params-objects.js
new file mode 100644
index 000000000..61fe5398e
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/browser_wa_properties-view-params-objects.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that params view correctly displays non-primitive properties
+ * like AudioBuffer and Float32Array in properties of AudioNodes.
+ */
+
+add_task(function*() {
+ let { target, panel } = yield initWebAudioEditor(BUFFER_AND_ARRAY_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$, EVENTS, PropertiesView } = panelWin;
+ let gVars = PropertiesView._propsView;
+
+ let started = once(gFront, "start-context");
+
+ reload(target);
+
+ let [actors] = yield Promise.all([
+ getN(gFront, "create-node", 3),
+ waitForGraphRendered(panelWin, 3, 2)
+ ]);
+ let nodeIds = actors.map(actor => actor.actorID);
+
+ click(panelWin, findGraphNode(panelWin, nodeIds[2]));
+ yield waitForInspectorRender(panelWin, EVENTS);
+ checkVariableView(gVars, 0, {
+ "curve": "Float32Array"
+ }, "WaveShaper's `curve` is listed as an `Float32Array`.");
+
+ let aVar = gVars.getScopeAtIndex(0).get("curve")
+ let state = aVar.target.querySelector(".theme-twisty").hasAttribute("invisible");
+ ok(state, "Float32Array property should not have a dropdown.");
+
+ click(panelWin, findGraphNode(panelWin, nodeIds[1]));
+ yield waitForInspectorRender(panelWin, EVENTS);
+ checkVariableView(gVars, 0, {
+ "buffer": "AudioBuffer"
+ }, "AudioBufferSourceNode's `buffer` is listed as an `AudioBuffer`.");
+
+ aVar = gVars.getScopeAtIndex(0).get("buffer")
+ state = aVar.target.querySelector(".theme-twisty").hasAttribute("invisible");
+ ok(state, "AudioBuffer property should not have a dropdown.");
+
+ yield teardown(target);
+});
diff --git a/toolkit/devtools/webaudioeditor/test/browser_wa_properties-view-params.js b/toolkit/devtools/webaudioeditor/test/browser_wa_properties-view-params.js
new file mode 100644
index 000000000..5af047ff0
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/browser_wa_properties-view-params.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that params view correctly displays all properties for nodes
+ * correctly, with default values and correct types.
+ */
+
+add_task(function*() {
+ let { target, panel } = yield initWebAudioEditor(SIMPLE_NODES_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$, EVENTS, PropertiesView } = panelWin;
+ let gVars = PropertiesView._propsView;
+
+ let started = once(gFront, "start-context");
+
+ reload(target);
+
+ let [actors] = yield Promise.all([
+ getN(gFront, "create-node", 15),
+ waitForGraphRendered(panelWin, 15, 0)
+ ]);
+ let nodeIds = actors.map(actor => actor.actorID);
+ let types = [
+ "AudioDestinationNode", "AudioBufferSourceNode", "ScriptProcessorNode",
+ "AnalyserNode", "GainNode", "DelayNode", "BiquadFilterNode", "WaveShaperNode",
+ "PannerNode", "ConvolverNode", "ChannelSplitterNode", "ChannelMergerNode",
+ "DynamicsCompressorNode", "OscillatorNode"
+ ];
+
+ for (let i = 0; i < types.length; i++) {
+ click(panelWin, findGraphNode(panelWin, nodeIds[i]));
+ yield waitForInspectorRender(panelWin, EVENTS);
+ checkVariableView(gVars, 0, NODE_DEFAULT_VALUES[types[i]], types[i]);
+ }
+
+ yield teardown(target);
+});
diff --git a/toolkit/devtools/webaudioeditor/test/browser_wa_properties-view.js b/toolkit/devtools/webaudioeditor/test/browser_wa_properties-view.js
new file mode 100644
index 000000000..702f1161f
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/browser_wa_properties-view.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that params view shows params when they exist, and are hidden otherwise.
+ */
+
+add_task(function*() {
+ let { target, panel } = yield initWebAudioEditor(SIMPLE_CONTEXT_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$, EVENTS, PropertiesView } = panelWin;
+ let gVars = PropertiesView._propsView;
+
+ let started = once(gFront, "start-context");
+
+ reload(target);
+
+ let [actors] = yield Promise.all([
+ get3(gFront, "create-node"),
+ waitForGraphRendered(panelWin, 3, 2)
+ ]);
+ let nodeIds = actors.map(actor => actor.actorID);
+
+ // Gain node
+ click(panelWin, findGraphNode(panelWin, nodeIds[2]));
+ yield waitForInspectorRender(panelWin, EVENTS);
+
+ ok(isVisible($("#properties-content")), "Parameters shown when they exist.");
+ ok(!isVisible($("#properties-empty")),
+ "Empty message hidden when AudioParams exist.");
+
+ // Destination node
+ click(panelWin, findGraphNode(panelWin, nodeIds[0]));
+ yield waitForInspectorRender(panelWin, EVENTS);
+
+ ok(!isVisible($("#properties-content")),
+ "Parameters hidden when they don't exist.");
+ ok(isVisible($("#properties-empty")),
+ "Empty message shown when no AudioParams exist.");
+
+ yield teardown(target);
+});
diff --git a/toolkit/devtools/webaudioeditor/test/browser_wa_reset-01.js b/toolkit/devtools/webaudioeditor/test/browser_wa_reset-01.js
new file mode 100644
index 000000000..3208227ac
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/browser_wa_reset-01.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+///////////////////
+//
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejection should be fixed.
+//
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Connection closed");
+
+/**
+ * Tests that reloading a tab will properly listen for the `start-context`
+ * event and reshow the tools after reloading.
+ */
+
+add_task(function*() {
+ let { target, panel } = yield initWebAudioEditor(SIMPLE_CONTEXT_URL);
+ let { gFront, $ } = panel.panelWin;
+
+ is($("#reload-notice").hidden, false,
+ "The 'reload this page' notice should initially be visible.");
+ is($("#waiting-notice").hidden, true,
+ "The 'waiting for an audio context' notice should initially be hidden.");
+ is($("#content").hidden, true,
+ "The tool's content should initially be hidden.");
+
+ let navigating = once(target, "will-navigate");
+ let started = once(gFront, "start-context");
+
+ reload(target);
+
+ yield navigating;
+
+ is($("#reload-notice").hidden, true,
+ "The 'reload this page' notice should be hidden when navigating.");
+ is($("#waiting-notice").hidden, false,
+ "The 'waiting for an audio context' notice should be visible when navigating.");
+ is($("#content").hidden, true,
+ "The tool's content should still be hidden.");
+
+ yield started;
+
+ is($("#reload-notice").hidden, true,
+ "The 'reload this page' notice should be hidden after context found.");
+ is($("#waiting-notice").hidden, true,
+ "The 'waiting for an audio context' notice should be hidden after context found.");
+ is($("#content").hidden, false,
+ "The tool's content should not be hidden anymore.");
+
+ navigating = once(target, "will-navigate");
+ started = once(gFront, "start-context");
+
+ reload(target);
+
+ yield Promise.all([navigating, started]);
+
+ is($("#reload-notice").hidden, true,
+ "The 'reload this page' notice should be hidden after context found after reload.");
+ is($("#waiting-notice").hidden, true,
+ "The 'waiting for an audio context' notice should be hidden after context found after reload.");
+ is($("#content").hidden, false,
+ "The tool's content should reappear without closing and reopening the toolbox.");
+
+ yield teardown(target);
+});
diff --git a/toolkit/devtools/webaudioeditor/test/browser_wa_reset-02.js b/toolkit/devtools/webaudioeditor/test/browser_wa_reset-02.js
new file mode 100644
index 000000000..478b9f5db
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/browser_wa_reset-02.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests reloading a tab with the tools open properly cleans up
+ * the graph.
+ */
+
+add_task(function*() {
+ let { target, panel } = yield initWebAudioEditor(SIMPLE_CONTEXT_URL);
+ let { panelWin } = panel;
+ let { gFront, $ } = panelWin;
+
+ reload(target);
+
+ let [actors] = yield Promise.all([
+ get3(gFront, "create-node"),
+ waitForGraphRendered(panelWin, 3, 2)
+ ]);
+
+ let { nodes, edges } = countGraphObjects(panelWin);
+ ise(nodes, 3, "should only be 3 nodes.");
+ ise(edges, 2, "should only be 2 edges.");
+
+ reload(target);
+
+ [actors] = yield Promise.all([
+ get3(gFront, "create-node"),
+ waitForGraphRendered(panelWin, 3, 2)
+ ]);
+
+ ({ nodes, edges } = countGraphObjects(panelWin));
+ ise(nodes, 3, "after reload, should only be 3 nodes.");
+ ise(edges, 2, "after reload, should only be 2 edges.");
+
+ yield teardown(target);
+});
diff --git a/toolkit/devtools/webaudioeditor/test/browser_wa_reset-03.js b/toolkit/devtools/webaudioeditor/test/browser_wa_reset-03.js
new file mode 100644
index 000000000..60c9572d1
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/browser_wa_reset-03.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests reloading a tab with the tools open properly cleans up
+ * the inspector and selected node.
+ */
+
+add_task(function*() {
+ let { target, panel } = yield initWebAudioEditor(SIMPLE_CONTEXT_URL);
+ let { panelWin } = panel;
+ let { gFront, $, InspectorView } = panelWin;
+
+ reload(target);
+
+ let [actors] = yield Promise.all([
+ get3(gFront, "create-node"),
+ waitForGraphRendered(panelWin, 3, 2)
+ ]);
+ let nodeIds = actors.map(actor => actor.actorID);
+
+ yield clickGraphNode(panelWin, nodeIds[1], true);
+ ok(InspectorView.isVisible(), "InspectorView visible after selecting a node.");
+ is(InspectorView.getCurrentAudioNode().id, nodeIds[1], "InspectorView has correct node set.");
+
+ /**
+ * Reload
+ */
+
+ reload(target);
+
+ [actors] = yield Promise.all([
+ get3(gFront, "create-node"),
+ waitForGraphRendered(panelWin, 3, 2)
+ ]);
+ nodeIds = actors.map(actor => actor.actorID);
+
+ ok(!InspectorView.isVisible(), "InspectorView hidden on start.");
+ ise(InspectorView.getCurrentAudioNode(), null,
+ "InspectorView has no current node set on reset.");
+
+ yield clickGraphNode(panelWin, nodeIds[2], true);
+ ok(InspectorView.isVisible(),
+ "InspectorView visible after selecting a node after a reset.");
+ is(InspectorView.getCurrentAudioNode().id, nodeIds[2], "InspectorView has correct node set upon clicking graph node after a reset.");
+
+ yield teardown(target);
+});
diff --git a/toolkit/devtools/webaudioeditor/test/browser_wa_reset-04.js b/toolkit/devtools/webaudioeditor/test/browser_wa_reset-04.js
new file mode 100644
index 000000000..3bef5775d
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/browser_wa_reset-04.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+///////////////////
+//
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejection should be fixed.
+//
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Connection closed");
+
+/**
+ * Tests that switching to an iframe works fine.
+ */
+
+add_task(function*() {
+ Services.prefs.setBoolPref("devtools.command-button-frames.enabled", true);
+
+ let { target, panel, toolbox } = yield initWebAudioEditor(IFRAME_CONTEXT_URL);
+ let { gFront, $ } = panel.panelWin;
+
+ is($("#reload-notice").hidden, false,
+ "The 'reload this page' notice should initially be visible.");
+ is($("#waiting-notice").hidden, true,
+ "The 'waiting for an audio context' notice should initially be hidden.");
+ is($("#content").hidden, true,
+ "The tool's content should initially be hidden.");
+
+ let btn = toolbox.doc.getElementById("command-button-frames");
+ ok(!btn.firstChild.getAttribute("hidden"), "The frame list button is visible");
+ let frameBtns = btn.firstChild.querySelectorAll("[data-window-id]");
+ is(frameBtns.length, 2, "We have both frames in the list");
+
+ // Select the iframe
+ frameBtns[1].click();
+
+ let navigating = once(target, "will-navigate");
+
+ yield navigating;
+
+ is($("#reload-notice").hidden, false,
+ "The 'reload this page' notice should still be visible when switching to a frame.");
+ is($("#waiting-notice").hidden, true,
+ "The 'waiting for an audio context' notice should be kept hidden when switching to a frame.");
+ is($("#content").hidden, true,
+ "The tool's content should still be hidden.");
+
+ navigating = once(target, "will-navigate");
+ let started = once(gFront, "start-context");
+
+ reload(target);
+
+ yield Promise.all([navigating, started]);
+
+ is($("#reload-notice").hidden, true,
+ "The 'reload this page' notice should be hidden after reloading the frame.");
+ is($("#waiting-notice").hidden, true,
+ "The 'waiting for an audio context' notice should be hidden after reloading the frame.");
+ is($("#content").hidden, false,
+ "The tool's content should appear after reload.");
+
+ yield teardown(target);
+});
diff --git a/toolkit/devtools/webaudioeditor/test/browser_webaudio-actor-automation-event.js b/toolkit/devtools/webaudioeditor/test/browser_webaudio-actor-automation-event.js
new file mode 100644
index 000000000..ef98dfac9
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/browser_webaudio-actor-automation-event.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test that the WebAudioActor receives and emits the `automation-event` events
+ * with correct arguments from the content.
+ */
+
+add_task(function*() {
+ let { target, front } = yield initBackend(AUTOMATION_URL);
+ let events = [];
+
+ let expected = [
+ ["setValueAtTime", 0.2, 0],
+ ["linearRampToValueAtTime", 1, 0.3],
+ ["exponentialRampToValueAtTime", 0.75, 0.6],
+ ["setValueCurveAtTime", [-1, 0 ,1], 0.7, 0.3],
+ ];
+
+ front.on("automation-event", onAutomationEvent);
+
+ let [_, __, [destNode, oscNode, gainNode], [connect1, connect2]] = yield Promise.all([
+ front.setup({ reload: true }),
+ once(front, "start-context"),
+ get3(front, "create-node"),
+ get2(front, "connect-node")
+ ]);
+
+ is(events.length, 4, "correct number of events fired");
+
+ function onAutomationEvent (e) {
+ let { eventName, paramName, args } = e;
+ let exp = expected[events.length];
+
+ is(eventName, exp[0], "correct eventName in event");
+ is(paramName, "frequency", "correct paramName in event");
+ is(args.length, exp.length - 1, "correct length in args");
+
+ args.forEach((a, i) => {
+ // In the case of an array
+ if (typeof a === "object") {
+ a.forEach((f, j) => is(f, exp[i + 1][j], `correct argument in Float32Array: ${f}`));
+ } else {
+ is(a, exp[i + 1], `correct ${i+1}th argument in args: ${a}`);
+ }
+ });
+ events.push([eventName].concat(args));
+ }
+
+ front.off("automation-event", onAutomationEvent);
+ yield removeTab(target.tab);
+});
diff --git a/toolkit/devtools/webaudioeditor/test/browser_webaudio-actor-connect-param.js b/toolkit/devtools/webaudioeditor/test/browser_webaudio-actor-connect-param.js
new file mode 100644
index 000000000..a436401dc
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/browser_webaudio-actor-connect-param.js
@@ -0,0 +1,25 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test the `connect-param` event on the web audio actor.
+ */
+
+add_task(function*() {
+ let { target, front } = yield initBackend(CONNECT_PARAM_URL);
+ let [, , [destNode, carrierNode, modNode, gainNode], , connectParam] = yield Promise.all([
+ front.setup({ reload: true }),
+ once(front, "start-context"),
+ getN(front, "create-node", 4),
+ get2(front, "connect-node"),
+ once(front, "connect-param")
+ ]);
+
+ info(connectParam);
+
+ is(connectParam.source.actorID, modNode.actorID, "`connect-param` has correct actor for `source`");
+ is(connectParam.dest.actorID, gainNode.actorID, "`connect-param` has correct actor for `dest`");
+ is(connectParam.param, "gain", "`connect-param` has correct parameter name for `param`");
+
+ yield removeTab(target.tab);
+});
diff --git a/toolkit/devtools/webaudioeditor/test/browser_webaudio-actor-destroy-node.js b/toolkit/devtools/webaudioeditor/test/browser_webaudio-actor-destroy-node.js
new file mode 100644
index 000000000..60140dbde
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/browser_webaudio-actor-destroy-node.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test `destroy-node` event on WebAudioActor.
+ */
+
+add_task(function*() {
+ let { target, front } = yield initBackend(DESTROY_NODES_URL);
+
+ let waitUntilDestroyed = getN(front, "destroy-node", 10);
+ let [, , created] = yield Promise.all([
+ front.setup({ reload: true }),
+ once(front, "start-context"),
+ // Should create 1 destination node and 10 disposable buffer nodes
+ getN(front, "create-node", 13)
+ ]);
+
+ // Force CC so we can ensure it's run to clear out dead AudioNodes
+ forceCC();
+
+ let destroyed = yield waitUntilDestroyed;
+
+ let destroyedTypes = yield Promise.all(destroyed.map(actor => actor.getType()));
+ destroyedTypes.forEach((type, i) => {
+ ok(type, "AudioBufferSourceNode", "Only buffer nodes are destroyed");
+ ok(actorIsInList(created, destroyed[i]),
+ "`destroy-node` called only on AudioNodes in current document.");
+ });
+
+ yield removeTab(target.tab);
+});
+
+function actorIsInList (list, actor) {
+ for (let i = 0; i < list.length; i++) {
+ if (list[i].actorID === actor.actorID)
+ return list[i];
+ }
+ return null;
+}
diff --git a/toolkit/devtools/webaudioeditor/test/browser_webaudio-actor-simple.js b/toolkit/devtools/webaudioeditor/test/browser_webaudio-actor-simple.js
new file mode 100644
index 000000000..949e19f0f
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/browser_webaudio-actor-simple.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test basic communication of Web Audio actor
+ */
+
+add_task(function*() {
+ let { target, front } = yield initBackend(SIMPLE_CONTEXT_URL);
+ let [_, __, [destNode, oscNode, gainNode], [connect1, connect2]] = yield Promise.all([
+ front.setup({ reload: true }),
+ once(front, "start-context"),
+ get3(front, "create-node"),
+ get2(front, "connect-node")
+ ]);
+
+ let destType = yield destNode.getType();
+ let oscType = yield oscNode.getType();
+ let gainType = yield gainNode.getType();
+
+ is(destType, "AudioDestinationNode", "WebAudioActor:create-node returns AudioNodeActor for AudioDestination");
+ is(oscType, "OscillatorNode", "WebAudioActor:create-node returns AudioNodeActor");
+ is(gainType, "GainNode", "WebAudioActor:create-node returns AudioNodeActor");
+
+ let { source, dest } = connect1;
+ is(source.actorID, oscNode.actorID, "WebAudioActor:connect-node returns correct actor with ID on source (osc->gain)");
+ is(dest.actorID, gainNode.actorID, "WebAudioActor:connect-node returns correct actor with ID on dest (osc->gain)");
+
+ ({ source, dest } = connect2);
+ is(source.actorID, gainNode.actorID, "WebAudioActor:connect-node returns correct actor with ID on source (gain->dest)");
+ is(dest.actorID, destNode.actorID, "WebAudioActor:connect-node returns correct actor with ID on dest (gain->dest)");
+
+ yield removeTab(target.tab);
+});
diff --git a/toolkit/devtools/webaudioeditor/test/doc_automation.html b/toolkit/devtools/webaudioeditor/test/doc_automation.html
new file mode 100644
index 000000000..6f074208c
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/doc_automation.html
@@ -0,0 +1,30 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Web Audio Editor test page</title>
+ </head>
+
+ <body>
+
+ <script type="text/javascript;version=1.8">
+ "use strict";
+
+ let ctx = new AudioContext();
+ let osc = ctx.createOscillator();
+ let gain = ctx.createGain();
+ gain.gain.value = 0;
+ osc.frequency.setValueAtTime(0.2, 0);
+ osc.frequency.linearRampToValueAtTime(1, 0.3);
+ osc.frequency.exponentialRampToValueAtTime(0.75, 0.6);
+ osc.frequency.setValueCurveAtTime(new Float32Array([-1, 0, 1]), 0.7, 0.3);
+ osc.connect(gain);
+ gain.connect(ctx.destination);
+ osc.start(0);
+ </script>
+ </body>
+
+</html>
diff --git a/toolkit/devtools/webaudioeditor/test/doc_buffer-and-array.html b/toolkit/devtools/webaudioeditor/test/doc_buffer-and-array.html
new file mode 100644
index 000000000..49dc91f29
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/doc_buffer-and-array.html
@@ -0,0 +1,56 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Web Audio Editor test page</title>
+ </head>
+
+ <body>
+
+ <script type="text/javascript;version=1.8">
+ "use strict";
+
+ let audioURL = "http://example.com/browser/browser/devtools/webaudioeditor/test/440hz_sine.ogg";
+
+ let ctx = new AudioContext();
+ let bufferNode = ctx.createBufferSource();
+ let shaperNode = ctx.createWaveShaper();
+ shaperNode.curve = generateWaveShapingCurve();
+
+ let xhr = getBuffer(audioURL, () => {
+ ctx.decodeAudioData(xhr.response, (buffer) => {
+ bufferNode.buffer = buffer;
+ bufferNode.connect(shaperNode);
+ shaperNode.connect(ctx.destination);
+ });
+ });
+
+ function generateWaveShapingCurve() {
+ let frames = 65536;
+ let curve = new Float32Array(frames);
+ let n = frames;
+ let n2 = n / 2;
+
+ for (let i = 0; i < n; ++i) {
+ let x = (i - n2) / n2;
+ let y = Math.atan(5 * x) / (0.5 * Math.PI);
+ }
+
+ return curve;
+ }
+
+ function getBuffer (url, callback) {
+ let xhr = new XMLHttpRequest();
+ xhr.open("GET", url, true);
+ xhr.responseType = "arraybuffer";
+ xhr.onload = callback;
+ xhr.send();
+ return xhr;
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/toolkit/devtools/webaudioeditor/test/doc_bug_1112378.html b/toolkit/devtools/webaudioeditor/test/doc_bug_1112378.html
new file mode 100644
index 000000000..ecdfd7d63
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/doc_bug_1112378.html
@@ -0,0 +1,57 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Web Audio Editor test page</title>
+ </head>
+
+ <body>
+
+ <script type="text/javascript;version=1.8">
+ "use strict";
+
+ let ctx = new AudioContext();
+ let osc = ctx.createOscillator();
+
+ function throwError () {
+ try {
+ osc.connect({});
+ } catch (e) {
+ return {
+ lineNumber: e.lineNumber,
+ fileName: e.fileName,
+ columnNumber: e.columnNumber,
+ message: e.message,
+ instanceof: e instanceof TypeError,
+ stringified: e.toString(),
+ name: e.name
+ }
+ }
+ }
+
+ function throwDOMException () {
+ try {
+ osc.frequency.setValueAtTime(0, -1);
+ } catch (e) {
+ return {
+ lineNumber: e.lineNumber,
+ columnNumber: e.columnNumber,
+ filename: e.filename,
+ message: e.message,
+ code: e.code,
+ result: e.result,
+ instanceof: e instanceof DOMException,
+ stringified: e.toString(),
+ name: e.name
+ }
+ }
+ }
+
+
+ </script>
+ </body>
+
+</html>
diff --git a/toolkit/devtools/webaudioeditor/test/doc_bug_1125817.html b/toolkit/devtools/webaudioeditor/test/doc_bug_1125817.html
new file mode 100644
index 000000000..49a2be11a
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/doc_bug_1125817.html
@@ -0,0 +1,23 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Web Audio Editor test page</title>
+ </head>
+
+ <body>
+
+ <script type="text/javascript;version=1.8">
+ "use strict";
+
+ let ctx = new AudioContext();
+ let osc = ctx.createOscillator();
+ osc.frequency.value = 200;
+ osc.disconnect();
+ </script>
+ </body>
+
+</html>
diff --git a/toolkit/devtools/webaudioeditor/test/doc_bug_1130901.html b/toolkit/devtools/webaudioeditor/test/doc_bug_1130901.html
new file mode 100644
index 000000000..1ce1ebf55
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/doc_bug_1130901.html
@@ -0,0 +1,22 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Web Audio Editor test page</title>
+ </head>
+
+ <body>
+
+ <script type="text/javascript;version=1.8">
+ "use strict";
+
+ let ctx = new AudioContext();
+ ctx.createOscillator.call(ctx);
+ ctx.createGain.apply(ctx, []);
+ </script>
+ </body>
+
+</html>
diff --git a/toolkit/devtools/webaudioeditor/test/doc_complex-context.html b/toolkit/devtools/webaudioeditor/test/doc_complex-context.html
new file mode 100644
index 000000000..396bbce3f
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/doc_complex-context.html
@@ -0,0 +1,44 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Web Audio Editor test page</title>
+ </head>
+
+ <body>
+
+ <script type="text/javascript;version=1.8">
+ "use strict";
+
+/*
+ ↱ proc
+ osc → gain →
+ osc → gain → destination
+ buffer →↳ filter →
+*/
+ let ctx = new AudioContext();
+ let osc1 = ctx.createOscillator();
+ let gain1 = ctx.createGain();
+ let proc = ctx.createScriptProcessor();
+ osc1.connect(gain1);
+ osc1.connect(proc);
+ gain1.connect(ctx.destination);
+
+ let osc2 = ctx.createOscillator();
+ let gain2 = ctx.createGain();
+ osc2.connect(gain2);
+ gain2.connect(ctx.destination);
+
+ let buf = ctx.createBufferSource();
+ let filter = ctx.createBiquadFilter();
+ buf.connect(filter);
+ osc2.connect(filter);
+ filter.connect(ctx.destination);
+
+ </script>
+ </body>
+
+</html>
diff --git a/toolkit/devtools/webaudioeditor/test/doc_connect-multi-param.html b/toolkit/devtools/webaudioeditor/test/doc_connect-multi-param.html
new file mode 100644
index 000000000..ed4bd84e8
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/doc_connect-multi-param.html
@@ -0,0 +1,32 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Web Audio Editor test page</title>
+ </head>
+
+ <body>
+
+ <script type="text/javascript;version=1.8">
+ "use strict";
+
+ let ctx = new AudioContext();
+ let carrier = ctx.createOscillator();
+ let gain = ctx.createGain();
+ let modulator = ctx.createOscillator();
+ let modulator2 = ctx.createOscillator();
+ carrier.connect(gain);
+ gain.connect(ctx.destination);
+ modulator.connect(gain.gain);
+ modulator2.connect(carrier.frequency);
+ modulator2.connect(carrier.detune);
+ modulator.start(0);
+ modulator2.start(0);
+ carrier.start(0);
+ </script>
+ </body>
+
+</html>
diff --git a/toolkit/devtools/webaudioeditor/test/doc_connect-param.html b/toolkit/devtools/webaudioeditor/test/doc_connect-param.html
new file mode 100644
index 000000000..9185c0b05
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/doc_connect-param.html
@@ -0,0 +1,28 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Web Audio Editor test page</title>
+ </head>
+
+ <body>
+
+ <script type="text/javascript;version=1.8">
+ "use strict";
+
+ let ctx = new AudioContext();
+ let carrier = ctx.createOscillator();
+ let modulator = ctx.createOscillator();
+ let gain = ctx.createGain();
+ carrier.connect(gain);
+ gain.connect(ctx.destination);
+ modulator.connect(gain.gain);
+ modulator.start(0);
+ carrier.start(0);
+ </script>
+ </body>
+
+</html>
diff --git a/toolkit/devtools/webaudioeditor/test/doc_destroy-nodes.html b/toolkit/devtools/webaudioeditor/test/doc_destroy-nodes.html
new file mode 100644
index 000000000..7738c39ab
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/doc_destroy-nodes.html
@@ -0,0 +1,32 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Web Audio Editor test page</title>
+ </head>
+
+ <body>
+
+ <script type="text/javascript;version=1.8">
+ "use strict";
+ (function () {
+ let ctx = new AudioContext();
+ let osc = ctx.createOscillator();
+ let gain = ctx.createGain();
+
+ for (let i = 0; i < 10; i++) {
+ ctx.createBufferSource();
+ }
+
+ osc.connect(gain);
+ gain.connect(ctx.destination);
+ gain.gain.value = 0;
+ osc.start();
+ })();
+ </script>
+ </body>
+
+</html>
diff --git a/toolkit/devtools/webaudioeditor/test/doc_iframe-context.html b/toolkit/devtools/webaudioeditor/test/doc_iframe-context.html
new file mode 100644
index 000000000..a0a411a47
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/doc_iframe-context.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>Web Audio Editor test page with an iframe</title>
+ </head>
+
+ <body>
+ <iframe id="frame" src="doc_simple-context.html" />
+ </body>
+</html>
diff --git a/toolkit/devtools/webaudioeditor/test/doc_media-node-creation.html b/toolkit/devtools/webaudioeditor/test/doc_media-node-creation.html
new file mode 100644
index 000000000..685062e3f
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/doc_media-node-creation.html
@@ -0,0 +1,29 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Web Audio Editor test page</title>
+ </head>
+
+ <body>
+
+ <script type="text/javascript;version=1.8">
+ "use strict";
+
+ let ctx = new AudioContext();
+ let audio = new Audio();
+ let meNode, msNode, mdNode;
+ navigator.getUserMedia = navigator.getUserMedia || navigator.mozGetUserMedia;
+
+ navigator.getUserMedia({ audio: true }, stream => {
+ meNode = ctx.createMediaElementSource(audio);
+ msNode = ctx.createMediaStreamSource(stream);
+ mdNode = ctx.createMediaStreamDestination();
+ }, () => {});
+ </script>
+ </body>
+
+</html>
diff --git a/toolkit/devtools/webaudioeditor/test/doc_simple-context.html b/toolkit/devtools/webaudioeditor/test/doc_simple-context.html
new file mode 100644
index 000000000..89a84b882
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/doc_simple-context.html
@@ -0,0 +1,33 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Web Audio Editor test page</title>
+ </head>
+
+ <body>
+
+ <script type="text/javascript;version=1.8">
+ "use strict";
+
+ let ctx = new AudioContext();
+ let osc = ctx.createOscillator();
+ let gain = ctx.createGain();
+ gain.gain.value = 0;
+
+ // Connect multiple times to test that it's disregarded.
+ osc.connect(gain);
+ gain.connect(ctx.destination);
+ gain.connect(ctx.destination);
+ gain.connect(ctx.destination);
+ gain.connect(ctx.destination);
+ gain.connect(ctx.destination);
+ gain.connect(ctx.destination);
+ osc.start(0);
+ </script>
+ </body>
+
+</html>
diff --git a/toolkit/devtools/webaudioeditor/test/doc_simple-node-creation.html b/toolkit/devtools/webaudioeditor/test/doc_simple-node-creation.html
new file mode 100644
index 000000000..e6dcf7b32
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/doc_simple-node-creation.html
@@ -0,0 +1,28 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Web Audio Editor test page</title>
+ </head>
+
+ <body>
+
+ <script type="text/javascript;version=1.8">
+ "use strict";
+
+ let ctx = new AudioContext();
+ let NODE_CREATION_METHODS = [
+ "createBufferSource", "createScriptProcessor", "createAnalyser",
+ "createGain", "createDelay", "createBiquadFilter", "createWaveShaper",
+ "createPanner", "createConvolver", "createChannelSplitter", "createChannelMerger",
+ "createDynamicsCompressor", "createOscillator", "createStereoPanner"
+ ];
+ let nodes = NODE_CREATION_METHODS.map(method => ctx[method]());
+
+ </script>
+ </body>
+
+</html>
diff --git a/toolkit/devtools/webaudioeditor/test/head.js b/toolkit/devtools/webaudioeditor/test/head.js
new file mode 100644
index 000000000..13f526a05
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/test/head.js
@@ -0,0 +1,562 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+let { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
+
+// Enable logging for all the tests. Both the debugger server and frontend will
+// be affected by this pref.
+let gEnableLogging = Services.prefs.getBoolPref("devtools.debugger.log");
+Services.prefs.setBoolPref("devtools.debugger.log", false);
+
+let { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
+let { Promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
+let { gDevTools } = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
+let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+let { DebuggerServer } = Cu.import("resource://gre/modules/devtools/dbg-server.jsm", {});
+let { generateUUID } = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
+
+let { WebAudioFront } = devtools.require("devtools/server/actors/webaudio");
+let TargetFactory = devtools.TargetFactory;
+let mm = null;
+
+const FRAME_SCRIPT_UTILS_URL = "chrome://browser/content/devtools/frame-script-utils.js";
+const EXAMPLE_URL = "http://example.com/browser/browser/devtools/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";
+
+// All tests are asynchronous.
+waitForExplicitFinish();
+
+let gToolEnabled = Services.prefs.getBoolPref("devtools.webaudioeditor.enabled");
+
+gDevTools.testing = true;
+
+registerCleanupFunction(() => {
+ gDevTools.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;
+
+ linkedBrowser.addEventListener("load", function onLoad() {
+ linkedBrowser.removeEventListener("load", onLoad, true);
+ info("Tab added and finished loading: " + aUrl);
+ deferred.resolve(tab);
+ }, true);
+
+ return deferred.promise;
+}
+
+function removeTab(aTab, aWindow) {
+ info("Removing tab.");
+
+ let deferred = Promise.defer();
+ let targetWindow = aWindow || window;
+ let targetBrowser = targetWindow.gBrowser;
+ let tabContainer = targetBrowser.tabContainer;
+
+ tabContainer.addEventListener("TabClose", function onClose(aEvent) {
+ tabContainer.removeEventListener("TabClose", onClose, false);
+ info("Tab removed and finished closing.");
+ deferred.resolve();
+ }, false);
+
+ targetBrowser.removeTab(aTab);
+ return deferred.promise;
+}
+
+function 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);
+}
+
+/**
+ * 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();
+ front.on(eventName, function onEvent (...args) {
+ let actor = args[0];
+ if (actors.length !== count) {
+ actors.push(spread ? args : actor);
+ }
+ 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;
+ front.on(eventName, function onGraphRendered (_, nodes, edges, pEdges) {
+ let paramEdgesDone = paramEdgeCount != null ? paramEdgeCount === pEdges : true;
+ 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 {
+ ise(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 forceCC () {
+ SpecialPowers.DOMWindowUtils.cycleCollect();
+ SpecialPowers.DOMWindowUtils.garbageCollect();
+ SpecialPowers.DOMWindowUtils.garbageCollect();
+}
+
+/**
+ * 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;
+}
+
+/**
+ * List of audio node properties to test against expectations of the AudioNode actor
+ */
+
+const NODE_DEFAULT_VALUES = {
+ "AudioDestinationNode": {},
+ "MediaElementAudioSourceNode": {},
+ "MediaStreamAudioSourceNode": {},
+ "MediaStreamAudioDestinationNode": {
+ "stream": "MediaStream"
+ },
+ "AudioBufferSourceNode": {
+ "playbackRate": 1,
+ "loop": false,
+ "loopStart": 0,
+ "loopEnd": 0,
+ "buffer": null
+ },
+ "ScriptProcessorNode": {
+ "bufferSize": 4096
+ },
+ "AnalyserNode": {
+ "fftSize": 2048,
+ "minDecibels": -100,
+ "maxDecibels": -30,
+ "smoothingTimeConstant": 0.8,
+ "frequencyBinCount": 1024
+ },
+ "GainNode": {
+ "gain": 1
+ },
+ "DelayNode": {
+ "delayTime": 0
+ },
+ "BiquadFilterNode": {
+ "type": "lowpass",
+ "frequency": 350,
+ "Q": 1,
+ "detune": 0,
+ "gain": 0
+ },
+ "WaveShaperNode": {
+ "curve": null,
+ "oversample": "none"
+ },
+ "PannerNode": {
+ "panningModel": "equalpower",
+ "distanceModel": "inverse",
+ "refDistance": 1,
+ "maxDistance": 10000,
+ "rolloffFactor": 1,
+ "coneInnerAngle": 360,
+ "coneOuterAngle": 360,
+ "coneOuterGain": 0
+ },
+ "ConvolverNode": {
+ "buffer": null,
+ "normalize": true
+ },
+ "ChannelSplitterNode": {},
+ "ChannelMergerNode": {},
+ "DynamicsCompressorNode": {
+ "threshold": -24,
+ "knee": 30,
+ "ratio": 12,
+ "reduction": 0,
+ "attack": 0.003000000026077032,
+ "release": 0.25
+ },
+ "OscillatorNode": {
+ "type": "sine",
+ "frequency": 440,
+ "detune": 0
+ },
+ "StereoPannerNode": {
+ "pan": 0
+ }
+};
diff --git a/toolkit/devtools/webaudioeditor/views/automation.js b/toolkit/devtools/webaudioeditor/views/automation.js
new file mode 100644
index 000000000..d6107d7f7
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/views/automation.js
@@ -0,0 +1,159 @@
+/* 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";
+
+/**
+ * Functions handling the audio node inspector UI.
+ */
+
+let AutomationView = {
+
+ /**
+ * Initialization function called when the tool starts up.
+ */
+ initialize: function () {
+ this._buttons = $("#automation-param-toolbar-buttons");
+ this.graph = new LineGraphWidget($("#automation-graph"), { avg: false });
+ this.graph.selectionEnabled = false;
+
+ this._onButtonClick = this._onButtonClick.bind(this);
+ this._onNodeSet = this._onNodeSet.bind(this);
+ this._onResize = this._onResize.bind(this);
+
+ this._buttons.addEventListener("click", this._onButtonClick);
+ window.on(EVENTS.UI_INSPECTOR_RESIZE, this._onResize);
+ window.on(EVENTS.UI_INSPECTOR_NODE_SET, this._onNodeSet);
+ },
+
+ /**
+ * Destruction function called when the tool cleans up.
+ */
+ destroy: function () {
+ this._buttons.removeEventListener("click", this._onButtonClick);
+ window.off(EVENTS.UI_INSPECTOR_RESIZE, this._onResize);
+ window.off(EVENTS.UI_INSPECTOR_NODE_SET, this._onNodeSet);
+ },
+
+ /**
+ * Empties out the props view.
+ */
+ resetUI: function () {
+ this._currentNode = null;
+ },
+
+ /**
+ * On a new node selection, create the Automation panel for
+ * that specific node.
+ */
+ build: Task.async(function* () {
+ let node = this._currentNode;
+
+ let props = yield node.getParams();
+ let params = props.filter(({ flags }) => flags && flags.param);
+
+ this._createParamButtons(params);
+
+ this._selectedParamName = params[0] ? params[0].param : null;
+ this.render();
+ }),
+
+ /**
+ * Renders the graph for specified `paramName`. Called when
+ * the parameter view is changed, or when new param data events
+ * are fired for the currently specified param.
+ */
+ render: Task.async(function *() {
+ let node = this._currentNode;
+ let paramName = this._selectedParamName;
+ // Escape if either node or parameter name does not exist.
+ if (!node || !paramName) {
+ this._setState("no-params");
+ window.emit(EVENTS.UI_AUTOMATION_TAB_RENDERED, null);
+ return;
+ }
+
+ let { values, events } = yield node.getAutomationData(paramName);
+ this._setState(events.length ? "show" : "no-events");
+ yield this.graph.setDataWhenReady(values);
+ window.emit(EVENTS.UI_AUTOMATION_TAB_RENDERED, node.id);
+ }),
+
+ /**
+ * Create the buttons for each AudioParam, that when clicked,
+ * render the graph for that AudioParam.
+ */
+ _createParamButtons: function (params) {
+ this._buttons.innerHTML = "";
+ params.forEach((param, i) => {
+ let button = document.createElement("toolbarbutton");
+ button.setAttribute("class", "devtools-toolbarbutton automation-param-button");
+ button.setAttribute("data-param", param.param);
+ // Set label to the parameter name, should not be L10N'd
+ button.setAttribute("label", param.param);
+
+ // If first button, set to 'selected' for styling
+ if (i === 0) {
+ button.setAttribute("selected", true);
+ }
+
+ this._buttons.appendChild(button);
+ });
+ },
+
+ /**
+ * Internally sets the current audio node and rebuilds appropriate
+ * views.
+ */
+ _setAudioNode: function (node) {
+ this._currentNode = node;
+ if (this._currentNode) {
+ this.build();
+ }
+ },
+
+ /**
+ * Toggles the subviews to display messages whether or not
+ * the audio node has no AudioParams, no automation events, or
+ * shows the graph.
+ */
+ _setState: function (state) {
+ let contentView = $("#automation-content");
+ let emptyView = $("#automation-empty");
+
+ let graphView = $("#automation-graph-container");
+ let noEventsView = $("#automation-no-events");
+
+ contentView.hidden = state === "no-params";
+ emptyView.hidden = state !== "no-params";
+
+ graphView.hidden = state !== "show";
+ noEventsView.hidden = state !== "no-events";
+ },
+
+ /**
+ * Event handlers
+ */
+
+ _onButtonClick: function (e) {
+ Array.forEach($$(".automation-param-button"), $btn => $btn.removeAttribute("selected"));
+ let paramName = e.target.getAttribute("data-param");
+ e.target.setAttribute("selected", true);
+ this._selectedParamName = paramName;
+ this.render();
+ },
+
+ /**
+ * Called when the inspector is resized.
+ */
+ _onResize: function () {
+ this.graph.refresh();
+ },
+
+ /**
+ * Called when the inspector view determines a node is selected.
+ */
+ _onNodeSet: function (_, id) {
+ this._setAudioNode(id != null ? gAudioNodes.get(id) : null);
+ }
+};
diff --git a/toolkit/devtools/webaudioeditor/views/context.js b/toolkit/devtools/webaudioeditor/views/context.js
new file mode 100644
index 000000000..f2160acf8
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/views/context.js
@@ -0,0 +1,306 @@
+/* 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 { debounce } = require("sdk/lang/functional");
+
+// Globals for d3 stuff
+// Default properties of the graph on rerender
+const GRAPH_DEFAULTS = {
+ translate: [20, 20],
+ scale: 1
+};
+
+// Sizes of SVG arrows in graph
+const ARROW_HEIGHT = 5;
+const ARROW_WIDTH = 8;
+
+// Styles for markers as they cannot be done with CSS.
+const MARKER_STYLING = {
+ light: "#AAA",
+ dark: "#CED3D9"
+};
+
+const GRAPH_DEBOUNCE_TIMER = 100;
+
+// `gAudioNodes` events that should require the graph
+// to redraw
+const GRAPH_REDRAW_EVENTS = ["add", "connect", "disconnect", "remove"];
+
+/**
+ * Functions handling the graph UI.
+ */
+let ContextView = {
+ /**
+ * Initialization function, called when the tool is started.
+ */
+ initialize: function() {
+ this._onGraphClick = this._onGraphClick.bind(this);
+ this._onThemeChange = this._onThemeChange.bind(this);
+ this._onStartContext = this._onStartContext.bind(this);
+ this._onEvent = this._onEvent.bind(this);
+
+ this.draw = debounce(this.draw.bind(this), GRAPH_DEBOUNCE_TIMER);
+ $("#graph-target").addEventListener("click", this._onGraphClick, false);
+
+ window.on(EVENTS.THEME_CHANGE, this._onThemeChange);
+ window.on(EVENTS.START_CONTEXT, this._onStartContext);
+ gAudioNodes.on("*", this._onEvent);
+ },
+
+ /**
+ * Destruction function, called when the tool is closed.
+ */
+ destroy: function() {
+ // If the graph was rendered at all, then the handler
+ // for zooming in will be set. We must remove it to prevent leaks.
+ if (this._zoomBinding) {
+ this._zoomBinding.on("zoom", null);
+ }
+ $("#graph-target").removeEventListener("click", this._onGraphClick, false);
+
+ window.off(EVENTS.THEME_CHANGE, this._onThemeChange);
+ window.off(EVENTS.START_CONTEXT, this._onStartContext);
+ gAudioNodes.off("*", this._onEvent);
+ },
+
+ /**
+ * Called when a page is reloaded and waiting for a "start-context" event
+ * and clears out old content
+ */
+ resetUI: function () {
+ this.clearGraph();
+ this.resetGraphTransform();
+ },
+
+ /**
+ * Clears out the rendered graph, called when resetting the SVG elements to draw again,
+ * or when resetting the entire UI tool
+ */
+ clearGraph: function () {
+ $("#graph-target").innerHTML = "";
+ },
+
+ /**
+ * Moves the graph back to its original scale and translation.
+ */
+ resetGraphTransform: function () {
+ // Only reset if the graph was ever drawn.
+ if (this._zoomBinding) {
+ let { translate, scale } = GRAPH_DEFAULTS;
+ // Must set the `zoomBinding` so the next `zoom` event is in sync with
+ // where the graph is visually (set by the `transform` attribute).
+ this._zoomBinding.scale(scale);
+ this._zoomBinding.translate(translate);
+ d3.select("#graph-target")
+ .attr("transform", "translate(" + translate + ") scale(" + scale + ")");
+ }
+ },
+
+ getCurrentScale: function () {
+ return this._zoomBinding ? this._zoomBinding.scale() : null;
+ },
+
+ getCurrentTranslation: function () {
+ return this._zoomBinding ? this._zoomBinding.translate() : null;
+ },
+
+ /**
+ * Makes the corresponding graph node appear "focused", removing
+ * focused styles from all other nodes. If no `actorID` specified,
+ * make all nodes appear unselected.
+ */
+ focusNode: function (actorID) {
+ // Remove class "selected" from all nodes
+ Array.forEach($$(".nodes > g"), $node => $node.classList.remove("selected"));
+ // Add to "selected"
+ if (actorID) {
+ this._getNodeByID(actorID).classList.add("selected");
+ }
+ },
+
+ /**
+ * Takes an actorID and returns the corresponding DOM SVG element in the graph
+ */
+ _getNodeByID: function (actorID) {
+ return $(".nodes > g[data-id='" + actorID + "']");
+ },
+
+ /**
+ * Sets the appropriate class on an SVG node when its bypass
+ * status is toggled.
+ */
+ _bypassNode: function (node, enabled) {
+ let el = this._getNodeByID(node.id);
+ el.classList[enabled ? "add" : "remove"]("bypassed");
+ },
+
+ /**
+ * This method renders the nodes currently available in `gAudioNodes` and is
+ * throttled to be called at most every `GRAPH_DEBOUNCE_TIMER` milliseconds.
+ * It's called whenever the audio context routing changes, after being debounced.
+ */
+ draw: function () {
+ // Clear out previous SVG information
+ this.clearGraph();
+
+ let graph = new dagreD3.Digraph();
+ let renderer = new dagreD3.Renderer();
+ gAudioNodes.populateGraph(graph);
+
+ // Post-render manipulation of the nodes
+ let oldDrawNodes = renderer.drawNodes();
+ renderer.drawNodes(function(graph, root) {
+ let svgNodes = oldDrawNodes(graph, root);
+ svgNodes.each(function (n) {
+ let node = graph.node(n);
+ let classString = "audionode type-" + node.type + (node.bypassed ? " bypassed" : "");
+ this.setAttribute("class", classString);
+ this.setAttribute("data-id", node.id);
+ this.setAttribute("data-type", node.type);
+ });
+ return svgNodes;
+ });
+
+ // Post-render manipulation of edges
+ let oldDrawEdgePaths = renderer.drawEdgePaths();
+ let defaultClasses = "edgePath enter";
+
+ renderer.drawEdgePaths(function(graph, root) {
+ let svgEdges = oldDrawEdgePaths(graph, root);
+ svgEdges.each(function (e) {
+ let edge = graph.edge(e);
+
+ // We have to manually specify the default classes on the edges
+ // as to not overwrite them
+ let edgeClass = defaultClasses + (edge.param ? (" param-connection " + edge.param) : "");
+
+ this.setAttribute("data-source", edge.source);
+ this.setAttribute("data-target", edge.target);
+ this.setAttribute("data-param", edge.param ? edge.param : null);
+ this.setAttribute("class", edgeClass);
+ });
+
+ return svgEdges;
+ });
+
+ // Override Dagre-d3's post render function by passing in our own.
+ // This way we can leave styles out of it.
+ renderer.postRender((graph, root) => {
+ // We have to manually set the marker styling since we cannot
+ // do this currently with CSS, although it is in spec for SVG2
+ // https://svgwg.org/svg2-draft/painting.html#VertexMarkerProperties
+ // For now, manually set it on creation, and the `_onThemeChange`
+ // function will fire when the devtools theme changes to update the
+ // styling manually.
+ let theme = Services.prefs.getCharPref("devtools.theme");
+ let markerColor = MARKER_STYLING[theme];
+ if (graph.isDirected() && root.select("#arrowhead").empty()) {
+ root
+ .append("svg:defs")
+ .append("svg:marker")
+ .attr("id", "arrowhead")
+ .attr("viewBox", "0 0 10 10")
+ .attr("refX", ARROW_WIDTH)
+ .attr("refY", ARROW_HEIGHT)
+ .attr("markerUnits", "strokewidth")
+ .attr("markerWidth", ARROW_WIDTH)
+ .attr("markerHeight", ARROW_HEIGHT)
+ .attr("orient", "auto")
+ .attr("style", "fill: " + markerColor)
+ .append("svg:path")
+ .attr("d", "M 0 0 L 10 5 L 0 10 z");
+ }
+
+ // Reselect the previously selected audio node
+ let currentNode = InspectorView.getCurrentAudioNode();
+ if (currentNode) {
+ this.focusNode(currentNode.id);
+ }
+
+ // Fire an event upon completed rendering, with extra information
+ // if in testing mode only.
+ let info = {};
+ if (gDevTools.testing) {
+ info = gAudioNodes.getInfo();
+ }
+ window.emit(EVENTS.UI_GRAPH_RENDERED, info.nodes, info.edges, info.paramEdges);
+ });
+
+ let layout = dagreD3.layout().rankDir("LR");
+ renderer.layout(layout).run(graph, d3.select("#graph-target"));
+
+ // Handle the sliding and zooming of the graph,
+ // store as `this._zoomBinding` so we can unbind during destruction
+ if (!this._zoomBinding) {
+ this._zoomBinding = d3.behavior.zoom().on("zoom", function () {
+ var ev = d3.event;
+ d3.select("#graph-target")
+ .attr("transform", "translate(" + ev.translate + ") scale(" + ev.scale + ")");
+ });
+ d3.select("svg").call(this._zoomBinding);
+
+ // Set initial translation and scale -- this puts D3's awareness of
+ // the graph in sync with what the user sees originally.
+ this.resetGraphTransform();
+ }
+ },
+
+ /**
+ * Event handlers
+ */
+
+ /**
+ * Called once "start-context" is fired, indicating that there is an audio
+ * context being created to view so render the graph.
+ */
+ _onStartContext: function () {
+ this.draw();
+ },
+
+ /**
+ * Called when `gAudioNodes` fires an event -- most events (listed
+ * in GRAPH_REDRAW_EVENTS) qualify as a redraw event.
+ */
+ _onEvent: function (eventName, ...args) {
+ // If bypassing, just toggle the class on the SVG node
+ // rather than rerendering everything
+ if (eventName === "bypass") {
+ this._bypassNode.apply(this, args);
+ }
+ if (~GRAPH_REDRAW_EVENTS.indexOf(eventName)) {
+ this.draw();
+ }
+ },
+
+ /**
+ * Fired when the devtools theme changes.
+ */
+ _onThemeChange: function (eventName, theme) {
+ let markerColor = MARKER_STYLING[theme];
+ let marker = $("#arrowhead");
+ if (marker) {
+ marker.setAttribute("style", "fill: " + markerColor);
+ }
+ },
+
+ /**
+ * Fired when a click occurs in the graph.
+ *
+ * @param Event e
+ * Click event.
+ */
+ _onGraphClick: function (e) {
+ let node = findGraphNodeParent(e.target);
+ // If node not found (clicking outside of an audio node in the graph),
+ // then ignore this event
+ if (!node)
+ return;
+
+ let id = node.getAttribute("data-id");
+
+ this.focusNode(id);
+ window.emit(EVENTS.UI_SELECT_NODE, id);
+ }
+};
diff --git a/toolkit/devtools/webaudioeditor/views/inspector.js b/toolkit/devtools/webaudioeditor/views/inspector.js
new file mode 100644
index 000000000..38e94f4b8
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/views/inspector.js
@@ -0,0 +1,187 @@
+/* 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 MIN_INSPECTOR_WIDTH = 300;
+
+// Strings for rendering
+const EXPAND_INSPECTOR_STRING = L10N.getStr("expandInspector");
+const COLLAPSE_INSPECTOR_STRING = L10N.getStr("collapseInspector");
+
+/**
+ * Functions handling the audio node inspector UI.
+ */
+
+let InspectorView = {
+ _currentNode: null,
+
+ // Set up config for view toggling
+ _collapseString: COLLAPSE_INSPECTOR_STRING,
+ _expandString: EXPAND_INSPECTOR_STRING,
+ _toggleEvent: EVENTS.UI_INSPECTOR_TOGGLED,
+ _animated: true,
+ _delayed: true,
+
+ /**
+ * Initialization function called when the tool starts up.
+ */
+ initialize: function () {
+ // Set up view controller
+ this.el = $("#web-audio-inspector");
+ this.splitter = $("#inspector-splitter");
+ this.el.setAttribute("width", Services.prefs.getIntPref("devtools.webaudioeditor.inspectorWidth"));
+ this.button = $("#inspector-pane-toggle");
+ mixin(this, ToggleMixin);
+ this.bindToggle();
+
+ // Hide inspector view on startup
+ this.hideImmediately();
+
+ this._onNodeSelect = this._onNodeSelect.bind(this);
+ this._onDestroyNode = this._onDestroyNode.bind(this);
+ this._onResize = this._onResize.bind(this);
+ this._onCommandClick = this._onCommandClick.bind(this);
+
+ this.splitter.addEventListener("mouseup", this._onResize);
+ for (let $el of $$("#audio-node-toolbar toolbarbutton")) {
+ $el.addEventListener("command", this._onCommandClick);
+ }
+ window.on(EVENTS.UI_SELECT_NODE, this._onNodeSelect);
+ gAudioNodes.on("remove", this._onDestroyNode);
+ },
+
+ /**
+ * Destruction function called when the tool cleans up.
+ */
+ destroy: function () {
+ this.unbindToggle();
+ this.splitter.removeEventListener("mouseup", this._onResize);
+
+ $("#audio-node-toolbar toolbarbutton").removeEventListener("command", this._onCommandClick);
+ for (let $el of $$("#audio-node-toolbar toolbarbutton")) {
+ $el.removeEventListener("command", this._onCommandClick);
+ }
+ window.off(EVENTS.UI_SELECT_NODE, this._onNodeSelect);
+ gAudioNodes.off("remove", this._onDestroyNode);
+
+ this.el = null;
+ this.button = null;
+ this.splitter = null;
+ },
+
+ /**
+ * Takes a AudioNodeView `node` and sets it as the current
+ * node and scaffolds the inspector view based off of the new node.
+ */
+ setCurrentAudioNode: Task.async(function* (node) {
+ this._currentNode = node || null;
+
+ // If no node selected, set the inspector back to "no AudioNode selected"
+ // view.
+ if (!node) {
+ $("#web-audio-editor-details-pane-empty").removeAttribute("hidden");
+ $("#web-audio-editor-tabs").setAttribute("hidden", "true");
+ window.emit(EVENTS.UI_INSPECTOR_NODE_SET, null);
+ }
+ // Otherwise load up the tabs view and hide the empty placeholder
+ else {
+ $("#web-audio-editor-details-pane-empty").setAttribute("hidden", "true");
+ $("#web-audio-editor-tabs").removeAttribute("hidden");
+ yield this._buildToolbar();
+ window.emit(EVENTS.UI_INSPECTOR_NODE_SET, this._currentNode.id);
+ }
+ }),
+
+ /**
+ * Returns the current AudioNodeView.
+ */
+ getCurrentAudioNode: function () {
+ return this._currentNode;
+ },
+
+ /**
+ * Empties out the props view.
+ */
+ resetUI: function () {
+ // Set current node to empty to load empty view
+ this.setCurrentAudioNode();
+
+ // Reset AudioNode inspector and hide
+ this.hideImmediately();
+ },
+
+ _buildToolbar: Task.async(function* () {
+ let node = this.getCurrentAudioNode();
+
+ let bypassable = node.bypassable;
+ let bypassed = yield node.isBypassed();
+ let button = $("#audio-node-toolbar .bypass");
+
+ if (!bypassable) {
+ button.setAttribute("disabled", true);
+ } else {
+ button.removeAttribute("disabled");
+ }
+
+ if (!bypassable || bypassed) {
+ button.removeAttribute("checked");
+ } else {
+ button.setAttribute("checked", true);
+ }
+ }),
+
+ /**
+ * Event handlers
+ */
+
+ /**
+ * Called on EVENTS.UI_SELECT_NODE, and takes an actorID `id`
+ * and calls `setCurrentAudioNode` to scaffold the inspector view.
+ */
+ _onNodeSelect: function (_, id) {
+ this.setCurrentAudioNode(gAudioNodes.get(id));
+
+ // Ensure inspector is visible when selecting a new node
+ this.show();
+ },
+
+ _onResize: function () {
+ if (this.el.getAttribute("width") < MIN_INSPECTOR_WIDTH) {
+ this.el.setAttribute("width", MIN_INSPECTOR_WIDTH);
+ }
+ Services.prefs.setIntPref("devtools.webaudioeditor.inspectorWidth", this.el.getAttribute("width"));
+ window.emit(EVENTS.UI_INSPECTOR_RESIZE);
+ },
+
+ /**
+ * Called when `DESTROY_NODE` is fired to remove the node from props view if
+ * it's currently selected.
+ */
+ _onDestroyNode: function (node) {
+ if (this._currentNode && this._currentNode.id === node.id) {
+ this.setCurrentAudioNode(null);
+ }
+ },
+
+ _onCommandClick: function (e) {
+ let node = this.getCurrentAudioNode();
+ let button = e.target;
+ let command = button.getAttribute("data-command");
+ let checked = button.getAttribute("checked");
+
+ if (button.getAttribute("disabled")) {
+ return;
+ }
+
+ if (command === "bypass") {
+ if (checked) {
+ button.removeAttribute("checked");
+ node.bypass(true);
+ } else {
+ button.setAttribute("checked", true);
+ node.bypass(false);
+ }
+ }
+ }
+};
diff --git a/toolkit/devtools/webaudioeditor/views/properties.js b/toolkit/devtools/webaudioeditor/views/properties.js
new file mode 100644
index 000000000..29ea0a410
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/views/properties.js
@@ -0,0 +1,164 @@
+/* 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";
+
+Cu.import("resource:///modules/devtools/VariablesView.jsm");
+Cu.import("resource:///modules/devtools/VariablesViewController.jsm");
+
+const GENERIC_VARIABLES_VIEW_SETTINGS = {
+ searchEnabled: false,
+ editableValueTooltip: "",
+ editableNameTooltip: "",
+ preventDisableOnChange: true,
+ preventDescriptorModifiers: false,
+ eval: () => {}
+};
+
+/**
+ * Functions handling the audio node inspector UI.
+ */
+
+let PropertiesView = {
+
+ /**
+ * Initialization function called when the tool starts up.
+ */
+ initialize: function () {
+ this._onEval = this._onEval.bind(this);
+ this._onNodeSet = this._onNodeSet.bind(this);
+
+ window.on(EVENTS.UI_INSPECTOR_NODE_SET, this._onNodeSet);
+ this._propsView = new VariablesView($("#properties-content"), GENERIC_VARIABLES_VIEW_SETTINGS);
+ this._propsView.eval = this._onEval;
+ },
+
+ /**
+ * Destruction function called when the tool cleans up.
+ */
+ destroy: function () {
+ window.off(EVENTS.UI_INSPECTOR_NODE_SET, this._onNodeSet);
+ this._propsView = null;
+ },
+
+ /**
+ * Empties out the props view.
+ */
+ resetUI: function () {
+ this._propsView.empty();
+ this._currentNode = null;
+ },
+
+ /**
+ * Internally sets the current audio node and rebuilds appropriate
+ * views.
+ */
+ _setAudioNode: function (node) {
+ this._currentNode = node;
+ if (this._currentNode) {
+ this._buildPropertiesView();
+ }
+ },
+
+ /**
+ * Reconstructs the `Properties` tab in the inspector
+ * with the `this._currentNode` as it's source.
+ */
+ _buildPropertiesView: Task.async(function* () {
+ let propsView = this._propsView;
+ let node = this._currentNode;
+ propsView.empty();
+
+ let audioParamsScope = propsView.addScope("AudioParams");
+ let props = yield node.getParams();
+
+ // Disable AudioParams VariableView expansion
+ // when there are no props i.e. AudioDestinationNode
+ this._togglePropertiesView(!!props.length);
+
+ props.forEach(({ param, value, flags }) => {
+ let descriptor = {
+ value: value,
+ writable: !flags || !flags.readonly,
+ };
+ let item = audioParamsScope.addItem(param, descriptor);
+
+ // No items should currently display a dropdown
+ item.twisty = false;
+ });
+
+ audioParamsScope.expanded = true;
+
+ window.emit(EVENTS.UI_PROPERTIES_TAB_RENDERED, node.id);
+ }),
+
+ /**
+ * Toggles the display of the "empty" properties view when
+ * node has no properties to display.
+ */
+ _togglePropertiesView: function (show) {
+ let propsView = $("#properties-content");
+ let emptyView = $("#properties-empty");
+ (show ? propsView : emptyView).removeAttribute("hidden");
+ (show ? emptyView : propsView).setAttribute("hidden", "true");
+ },
+
+ /**
+ * Returns the scope for AudioParams in the
+ * VariablesView.
+ *
+ * @return Scope
+ */
+ _getAudioPropertiesScope: function () {
+ return this._propsView.getScopeAtIndex(0);
+ },
+
+ /**
+ * Event handlers
+ */
+
+ /**
+ * Called when the inspector view determines a node is selected.
+ */
+ _onNodeSet: function (_, id) {
+ this._setAudioNode(gAudioNodes.get(id));
+ },
+
+ /**
+ * Executed when an audio prop is changed in the UI.
+ */
+ _onEval: Task.async(function* (variable, value) {
+ let ownerScope = variable.ownerView;
+ let node = this._currentNode;
+ let propName = variable.name;
+ let error;
+
+ if (!variable._initialDescriptor.writable) {
+ error = new Error("Variable " + propName + " is not writable.");
+ } else {
+ // Cast value to proper type
+ try {
+ let number = parseFloat(value);
+ if (!isNaN(number)) {
+ value = number;
+ } else {
+ value = JSON.parse(value);
+ }
+ error = yield node.actor.setParam(propName, value);
+ }
+ catch (e) {
+ error = e;
+ }
+ }
+
+ // TODO figure out how to handle and display set prop errors
+ // and enable `test/brorwser_wa_properties-view-edit.js`
+ // Bug 994258
+ if (!error) {
+ ownerScope.get(propName).setGrip(value);
+ window.emit(EVENTS.UI_SET_PARAM, node.id, propName, value);
+ } else {
+ window.emit(EVENTS.UI_SET_PARAM_ERROR, node.id, propName, value);
+ }
+ })
+};
diff --git a/toolkit/devtools/webaudioeditor/views/utils.js b/toolkit/devtools/webaudioeditor/views/utils.js
new file mode 100644
index 000000000..c397a16cb
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/views/utils.js
@@ -0,0 +1,103 @@
+/* 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";
+
+/**
+ * Takes an element in an SVG graph and iterates over
+ * ancestors until it finds the graph node container. If not found,
+ * returns null.
+ */
+
+function findGraphNodeParent (el) {
+ // Some targets may not contain `classList` property
+ if (!el.classList)
+ return null;
+
+ while (!el.classList.contains("nodes")) {
+ if (el.classList.contains("audionode"))
+ return el;
+ else
+ el = el.parentNode;
+ }
+ return null;
+}
+
+/**
+ * Object for use with `mix` into a view.
+ * Must have the following properties defined on the view:
+ * - `el`
+ * - `button`
+ * - `_collapseString`
+ * - `_expandString`
+ * - `_toggleEvent`
+ *
+ * Optional properties on the view can be defined to specify default
+ * visibility options.
+ * - `_animated`
+ * - `_delayed`
+ */
+let ToggleMixin = {
+
+ bindToggle: function () {
+ this._onToggle = this._onToggle.bind(this);
+ this.button.addEventListener("mousedown", this._onToggle, false);
+ },
+
+ unbindToggle: function () {
+ this.button.removeEventListener("mousedown", this._onToggle);
+ },
+
+ show: function () {
+ this._viewController({ visible: true });
+ },
+
+ hide: function () {
+ this._viewController({ visible: false });
+ },
+
+ hideImmediately: function () {
+ this._viewController({ visible: false, delayed: false, animated: false });
+ },
+
+ /**
+ * Returns a boolean indicating whether or not the view.
+ * is currently being shown.
+ */
+ isVisible: function () {
+ return !this.el.hasAttribute("pane-collapsed");
+ },
+
+ /**
+ * Toggles the visibility of the view.
+ *
+ * @param object visible
+ * - visible: boolean indicating whether the panel should be shown or not
+ * - animated: boolean indiciating whether the pane should be animated
+ * - delayed: boolean indicating whether the pane's opening should wait
+ * a few cycles or not
+ */
+ _viewController: function ({ visible, animated, delayed }) {
+ let flags = {
+ visible: visible,
+ animated: animated != null ? animated : !!this._animated,
+ delayed: delayed != null ? delayed : !!this._delayed,
+ callback: () => window.emit(this._toggleEvent, visible)
+ };
+
+ ViewHelpers.togglePane(flags, this.el);
+
+ if (flags.visible) {
+ this.button.removeAttribute("pane-collapsed");
+ this.button.setAttribute("tooltiptext", this._collapseString);
+ }
+ else {
+ this.button.setAttribute("pane-collapsed", "");
+ this.button.setAttribute("tooltiptext", this._expandString);
+ }
+ },
+
+ _onToggle: function () {
+ this._viewController({ visible: !this.isVisible() });
+ }
+}
diff --git a/toolkit/devtools/webaudioeditor/webaudioeditor.xul b/toolkit/devtools/webaudioeditor/webaudioeditor.xul
new file mode 100644
index 000000000..e0af48b6f
--- /dev/null
+++ b/toolkit/devtools/webaudioeditor/webaudioeditor.xul
@@ -0,0 +1,142 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<?xml-stylesheet href="chrome://browser/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/devtools/widgets.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/common.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/widgets.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/webaudioeditor.css" type="text/css"?>
+<!DOCTYPE window [
+ <!ENTITY % debuggerDTD SYSTEM "chrome://browser/locale/devtools/webaudioeditor.dtd">
+ %debuggerDTD;
+]>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script type="application/javascript;version=1.8"
+ src="chrome://browser/content/devtools/theme-switching.js"/>
+
+ <script type="application/javascript" src="chrome://browser/content/devtools/d3.js"/>
+ <script type="application/javascript" src="dagre-d3.js"/>
+ <script type="application/javascript" src="webaudioeditor/includes.js"/>
+ <script type="application/javascript" src="webaudioeditor/models.js"/>
+ <script type="application/javascript" src="webaudioeditor/controller.js"/>
+ <script type="application/javascript" src="webaudioeditor/views/utils.js"/>
+ <script type="application/javascript" src="webaudioeditor/views/context.js"/>
+ <script type="application/javascript" src="webaudioeditor/views/inspector.js"/>
+ <script type="application/javascript" src="webaudioeditor/views/properties.js"/>
+ <script type="application/javascript" src="webaudioeditor/views/automation.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="&webAudioEditorUI.reloadNotice1;"
+ oncommand="gFront.setup({ reload: true });"/>
+ <label id="requests-menu-reload-notice-label"
+ class="plain"
+ value="&webAudioEditorUI.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="&webAudioEditorUI.emptyNotice;"/>
+ </hbox>
+
+ <vbox id="content"
+ flex="1"
+ hidden="true">
+ <toolbar id="web-audio-toolbar" class="devtools-toolbar">
+ <spacer flex="1"></spacer>
+ <toolbarbutton id="inspector-pane-toggle" class="devtools-toolbarbutton"
+ tabindex="0"/>
+ </toolbar>
+ <splitter class="devtools-horizontal-splitter"/>
+ <box id="web-audio-content-pane"
+ class="devtools-responsive-container"
+ flex="1">
+ <hbox flex="1">
+ <box id="web-audio-graph" flex="1">
+ <vbox flex="1">
+ <svg id="graph-svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink">
+ <g id="graph-target" transform="translate(20,20)"/>
+ </svg>
+ </vbox>
+ </box>
+ </hbox>
+ <splitter id="inspector-splitter" class="devtools-side-splitter"/>
+ <vbox id="web-audio-inspector" hidden="true">
+ <deck id="web-audio-editor-details-pane" flex="1">
+ <vbox id="web-audio-editor-details-pane-empty" flex="1">
+ <label value="&webAudioEditorUI.inspectorEmpty;"></label>
+ </vbox>
+ <tabbox id="web-audio-editor-tabs"
+ class="devtools-sidebar-tabs"
+ handleCtrlTab="false">
+ <toolbar id="audio-node-toolbar" class="devtools-toolbar">
+ <hbox class="devtools-toolbarbutton-group">
+ <toolbarbutton class="bypass devtools-toolbarbutton"
+ data-command="bypass"
+ tabindex="0"/>
+ </hbox>
+ </toolbar>
+ <tabs>
+ <tab id="properties-tab"
+ label="&webAudioEditorUI.tab.properties2;"/>
+ <!-- bug 1134036
+ <tab id="automation-tab"
+ label="&webAudioEditorUI.tab.automation;"/>
+ -->
+ </tabs>
+ <tabpanels flex="1">
+ <!-- Properties Panel -->
+ <tabpanel id="properties-tabpanel"
+ class="tabpanel-content">
+ <vbox id="properties-content" flex="1" hidden="true">
+ </vbox>
+ <vbox id="properties-empty" flex="1" hidden="true">
+ <label value="&webAudioEditorUI.propertiesEmpty;"></label>
+ </vbox>
+ </tabpanel>
+
+ <!-- Automation Panel -->
+ <tabpanel id="automation-tabpanel"
+ class="tabpanel-content">
+ <vbox id="automation-content" flex="1" hidden="true">
+ <toolbar id="automation-param-toolbar" class="devtools-toolbar">
+ <hbox id="automation-param-toolbar-buttons" class="devtools-toolbarbutton-group">
+ </hbox>
+ </toolbar>
+ <box id="automation-graph-container" flex="1">
+ <canvas id="automation-graph"></canvas>
+ </box>
+ <vbox id="automation-no-events" flex="1" hidden="true">
+ <label value="&webAudioEditorUI.automationNoEvents;"></label>
+ </vbox>
+ </vbox>
+ <vbox id="automation-empty" flex="1" hidden="true">
+ <label value="&webAudioEditorUI.automationEmpty;"></label>
+ </vbox>
+ </tabpanel>
+ </tabpanels>
+ </tabbox>
+ </deck>
+ </vbox>
+ </box>
+ </vbox>
+ </vbox>
+
+</window>