|
1 /* Any copyright is dedicated to the Public Domain. |
|
2 http://creativecommons.org/publicdomain/zero/1.0/ */ |
|
3 "use strict"; |
|
4 |
|
5 const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; |
|
6 |
|
7 let { Services } = Cu.import("resource://gre/modules/Services.jsm", {}); |
|
8 |
|
9 // Enable logging for all the tests. Both the debugger server and frontend will |
|
10 // be affected by this pref. |
|
11 let gEnableLogging = Services.prefs.getBoolPref("devtools.debugger.log"); |
|
12 Services.prefs.setBoolPref("devtools.debugger.log", true); |
|
13 |
|
14 let { Task } = Cu.import("resource://gre/modules/Task.jsm", {}); |
|
15 let { Promise } = Cu.import("resource://gre/modules/Promise.jsm", {}); |
|
16 let { gDevTools } = Cu.import("resource:///modules/devtools/gDevTools.jsm", {}); |
|
17 let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); |
|
18 let { DebuggerServer } = Cu.import("resource://gre/modules/devtools/dbg-server.jsm", {}); |
|
19 |
|
20 let { WebAudioFront } = devtools.require("devtools/server/actors/webaudio"); |
|
21 let TargetFactory = devtools.TargetFactory; |
|
22 |
|
23 const EXAMPLE_URL = "http://example.com/browser/browser/devtools/webaudioeditor/test/"; |
|
24 const SIMPLE_CONTEXT_URL = EXAMPLE_URL + "doc_simple-context.html"; |
|
25 const COMPLEX_CONTEXT_URL = EXAMPLE_URL + "doc_complex-context.html"; |
|
26 const SIMPLE_NODES_URL = EXAMPLE_URL + "doc_simple-node-creation.html"; |
|
27 |
|
28 // All tests are asynchronous. |
|
29 waitForExplicitFinish(); |
|
30 |
|
31 let gToolEnabled = Services.prefs.getBoolPref("devtools.webaudioeditor.enabled"); |
|
32 |
|
33 registerCleanupFunction(() => { |
|
34 info("finish() was called, cleaning up..."); |
|
35 Services.prefs.setBoolPref("devtools.debugger.log", gEnableLogging); |
|
36 Services.prefs.setBoolPref("devtools.webaudioeditor.enabled", gToolEnabled); |
|
37 Cu.forceGC(); |
|
38 }); |
|
39 |
|
40 function addTab(aUrl, aWindow) { |
|
41 info("Adding tab: " + aUrl); |
|
42 |
|
43 let deferred = Promise.defer(); |
|
44 let targetWindow = aWindow || window; |
|
45 let targetBrowser = targetWindow.gBrowser; |
|
46 |
|
47 targetWindow.focus(); |
|
48 let tab = targetBrowser.selectedTab = targetBrowser.addTab(aUrl); |
|
49 let linkedBrowser = tab.linkedBrowser; |
|
50 |
|
51 linkedBrowser.addEventListener("load", function onLoad() { |
|
52 linkedBrowser.removeEventListener("load", onLoad, true); |
|
53 info("Tab added and finished loading: " + aUrl); |
|
54 deferred.resolve(tab); |
|
55 }, true); |
|
56 |
|
57 return deferred.promise; |
|
58 } |
|
59 |
|
60 function removeTab(aTab, aWindow) { |
|
61 info("Removing tab."); |
|
62 |
|
63 let deferred = Promise.defer(); |
|
64 let targetWindow = aWindow || window; |
|
65 let targetBrowser = targetWindow.gBrowser; |
|
66 let tabContainer = targetBrowser.tabContainer; |
|
67 |
|
68 tabContainer.addEventListener("TabClose", function onClose(aEvent) { |
|
69 tabContainer.removeEventListener("TabClose", onClose, false); |
|
70 info("Tab removed and finished closing."); |
|
71 deferred.resolve(); |
|
72 }, false); |
|
73 |
|
74 targetBrowser.removeTab(aTab); |
|
75 return deferred.promise; |
|
76 } |
|
77 |
|
78 function handleError(aError) { |
|
79 ok(false, "Got an error: " + aError.message + "\n" + aError.stack); |
|
80 finish(); |
|
81 } |
|
82 |
|
83 function once(aTarget, aEventName, aUseCapture = false) { |
|
84 info("Waiting for event: '" + aEventName + "' on " + aTarget + "."); |
|
85 |
|
86 let deferred = Promise.defer(); |
|
87 |
|
88 for (let [add, remove] of [ |
|
89 ["on", "off"], // Use event emitter before DOM events for consistency |
|
90 ["addEventListener", "removeEventListener"], |
|
91 ["addListener", "removeListener"] |
|
92 ]) { |
|
93 if ((add in aTarget) && (remove in aTarget)) { |
|
94 aTarget[add](aEventName, function onEvent(...aArgs) { |
|
95 aTarget[remove](aEventName, onEvent, aUseCapture); |
|
96 deferred.resolve(...aArgs); |
|
97 }, aUseCapture); |
|
98 break; |
|
99 } |
|
100 } |
|
101 |
|
102 return deferred.promise; |
|
103 } |
|
104 |
|
105 function reload(aTarget, aWaitForTargetEvent = "navigate") { |
|
106 aTarget.activeTab.reload(); |
|
107 return once(aTarget, aWaitForTargetEvent); |
|
108 } |
|
109 |
|
110 function test () { |
|
111 Task.spawn(spawnTest).then(finish, handleError); |
|
112 } |
|
113 |
|
114 function initBackend(aUrl) { |
|
115 info("Initializing a web audio editor front."); |
|
116 |
|
117 if (!DebuggerServer.initialized) { |
|
118 DebuggerServer.init(() => true); |
|
119 DebuggerServer.addBrowserActors(); |
|
120 } |
|
121 |
|
122 return Task.spawn(function*() { |
|
123 let tab = yield addTab(aUrl); |
|
124 let target = TargetFactory.forTab(tab); |
|
125 let debuggee = target.window.wrappedJSObject; |
|
126 |
|
127 yield target.makeRemote(); |
|
128 |
|
129 let front = new WebAudioFront(target.client, target.form); |
|
130 return [target, debuggee, front]; |
|
131 }); |
|
132 } |
|
133 |
|
134 function initWebAudioEditor(aUrl) { |
|
135 info("Initializing a web audio editor pane."); |
|
136 |
|
137 return Task.spawn(function*() { |
|
138 let tab = yield addTab(aUrl); |
|
139 let target = TargetFactory.forTab(tab); |
|
140 let debuggee = target.window.wrappedJSObject; |
|
141 |
|
142 yield target.makeRemote(); |
|
143 |
|
144 Services.prefs.setBoolPref("devtools.webaudioeditor.enabled", true); |
|
145 let toolbox = yield gDevTools.showToolbox(target, "webaudioeditor"); |
|
146 let panel = toolbox.getCurrentPanel(); |
|
147 return [target, debuggee, panel]; |
|
148 }); |
|
149 } |
|
150 |
|
151 function teardown(aPanel) { |
|
152 info("Destroying the web audio editor."); |
|
153 |
|
154 return Promise.all([ |
|
155 once(aPanel, "destroyed"), |
|
156 removeTab(aPanel.target.tab) |
|
157 ]).then(() => { |
|
158 let gBrowser = window.gBrowser; |
|
159 while (gBrowser.tabs.length > 1) { |
|
160 gBrowser.removeCurrentTab(); |
|
161 } |
|
162 gBrowser = null; |
|
163 }); |
|
164 } |
|
165 |
|
166 // Due to web audio will fire most events synchronously back-to-back, |
|
167 // and we can't yield them in a chain without missing actors, this allows |
|
168 // us to listen for `n` events and return a promise resolving to them. |
|
169 // |
|
170 // Takes a `front` object that is an event emitter, the number of |
|
171 // programs that should be listened to and waited on, and an optional |
|
172 // `onAdd` function that calls with the entire actors array on program link |
|
173 function getN (front, eventName, count, spread) { |
|
174 let actors = []; |
|
175 let deferred = Promise.defer(); |
|
176 front.on(eventName, function onEvent (...args) { |
|
177 let actor = args[0]; |
|
178 if (actors.length !== count) { |
|
179 actors.push(spread ? args : actor); |
|
180 } |
|
181 if (actors.length === count) { |
|
182 front.off(eventName, onEvent); |
|
183 deferred.resolve(actors); |
|
184 } |
|
185 }); |
|
186 return deferred.promise; |
|
187 } |
|
188 |
|
189 function get (front, eventName) { return getN(front, eventName, 1); } |
|
190 function get2 (front, eventName) { return getN(front, eventName, 2); } |
|
191 function get3 (front, eventName) { return getN(front, eventName, 3); } |
|
192 function getSpread (front, eventName) { return getN(front, eventName, 1, true); } |
|
193 function get2Spread (front, eventName) { return getN(front, eventName, 2, true); } |
|
194 function get3Spread (front, eventName) { return getN(front, eventName, 3, true); } |
|
195 function getNSpread (front, eventName, count) { return getN(front, eventName, count, true); } |
|
196 |
|
197 /** |
|
198 * Waits for the UI_GRAPH_RENDERED event to fire, but only |
|
199 * resolves when the graph was rendered with the correct count of |
|
200 * nodes and edges. |
|
201 */ |
|
202 function waitForGraphRendered (front, nodeCount, edgeCount) { |
|
203 let deferred = Promise.defer(); |
|
204 let eventName = front.EVENTS.UI_GRAPH_RENDERED; |
|
205 front.on(eventName, function onGraphRendered (_, nodes, edges) { |
|
206 if (nodes === nodeCount && edges === edgeCount) { |
|
207 front.off(eventName, onGraphRendered); |
|
208 deferred.resolve(); |
|
209 } |
|
210 }); |
|
211 return deferred.promise; |
|
212 } |
|
213 |
|
214 function checkVariableView (view, index, hash) { |
|
215 let scope = view.getScopeAtIndex(index); |
|
216 let variables = Object.keys(hash); |
|
217 variables.forEach(variable => { |
|
218 let aVar = scope.get(variable); |
|
219 is(aVar.target.querySelector(".name").getAttribute("value"), variable, |
|
220 "Correct property name for " + variable); |
|
221 is(aVar.target.querySelector(".value").getAttribute("value"), hash[variable], |
|
222 "Correct property value of " + hash[variable] + " for " + variable); |
|
223 }); |
|
224 } |
|
225 |
|
226 function modifyVariableView (win, view, index, prop, value) { |
|
227 let deferred = Promise.defer(); |
|
228 let scope = view.getScopeAtIndex(index); |
|
229 let aVar = scope.get(prop); |
|
230 scope.expand(); |
|
231 |
|
232 // Must wait for the scope DOM to be available to receive |
|
233 // events |
|
234 executeSoon(() => { |
|
235 let varValue = aVar.target.querySelector(".title > .value"); |
|
236 EventUtils.sendMouseEvent({ type: "mousedown" }, varValue, win); |
|
237 |
|
238 win.on(win.EVENTS.UI_SET_PARAM, handleSetting); |
|
239 win.on(win.EVENTS.UI_SET_PARAM_ERROR, handleSetting); |
|
240 |
|
241 info("Setting " + value + " for " + prop + "...."); |
|
242 let varInput = aVar.target.querySelector(".title > .element-value-input"); |
|
243 setText(varInput, value); |
|
244 EventUtils.sendKey("RETURN", win); |
|
245 }); |
|
246 |
|
247 function handleSetting (eventName) { |
|
248 win.off(win.EVENTS.UI_SET_PARAM, handleSetting); |
|
249 win.off(win.EVENTS.UI_SET_PARAM_ERROR, handleSetting); |
|
250 if (eventName === win.EVENTS.UI_SET_PARAM) |
|
251 deferred.resolve(); |
|
252 if (eventName === win.EVENTS.UI_SET_PARAM_ERROR) |
|
253 deferred.reject(); |
|
254 } |
|
255 |
|
256 return deferred.promise; |
|
257 } |
|
258 |
|
259 function clearText (aElement) { |
|
260 info("Clearing text..."); |
|
261 aElement.focus(); |
|
262 aElement.value = ""; |
|
263 } |
|
264 |
|
265 function setText (aElement, aText) { |
|
266 clearText(aElement); |
|
267 info("Setting text: " + aText); |
|
268 aElement.value = aText; |
|
269 } |
|
270 |
|
271 function findGraphEdge (win, source, target) { |
|
272 let selector = ".edgePaths .edgePath[data-source='" + source + "'][data-target='" + target + "']"; |
|
273 return win.document.querySelector(selector); |
|
274 } |
|
275 |
|
276 function findGraphNode (win, node) { |
|
277 let selector = ".nodes > g[data-id='" + node + "']"; |
|
278 return win.document.querySelector(selector); |
|
279 } |
|
280 |
|
281 function click (win, element) { |
|
282 EventUtils.sendMouseEvent({ type: "click" }, element, win); |
|
283 } |
|
284 |
|
285 function mouseOver (win, element) { |
|
286 EventUtils.sendMouseEvent({ type: "mouseover" }, element, win); |
|
287 } |
|
288 |
|
289 /** |
|
290 * List of audio node properties to test against expectations of the AudioNode actor |
|
291 */ |
|
292 |
|
293 const NODE_PROPERTIES = { |
|
294 "OscillatorNode": ["type", "frequency", "detune"], |
|
295 "GainNode": ["gain"], |
|
296 "DelayNode": ["delayTime"], |
|
297 "AudioBufferSourceNode": ["buffer", "playbackRate", "loop", "loopStart", "loopEnd"], |
|
298 "ScriptProcessorNode": ["bufferSize"], |
|
299 "PannerNode": ["panningModel", "distanceModel", "refDistance", "maxDistance", "rolloffFactor", "coneInnerAngle", "coneOuterAngle", "coneOuterGain"], |
|
300 "ConvolverNode": ["buffer", "normalize"], |
|
301 "DynamicsCompressorNode": ["threshold", "knee", "ratio", "reduction", "attack", "release"], |
|
302 "BiquadFilterNode": ["type", "frequency", "Q", "detune", "gain"], |
|
303 "WaveShaperNode": ["curve", "oversample"], |
|
304 "AnalyserNode": ["fftSize", "minDecibels", "maxDecibels", "smoothingTimeConstraint", "frequencyBinCount"], |
|
305 "AudioDestinationNode": [], |
|
306 "ChannelSplitterNode": [], |
|
307 "ChannelMergerNode": [] |
|
308 }; |