|
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 let gEnableLogging = Services.prefs.getBoolPref("devtools.debugger.log"); |
|
10 // To enable logging for try runs, just set the pref to true. |
|
11 Services.prefs.setBoolPref("devtools.debugger.log", false); |
|
12 |
|
13 let { Task } = Cu.import("resource://gre/modules/Task.jsm", {}); |
|
14 let { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {}); |
|
15 let { gDevTools } = Cu.import("resource:///modules/devtools/gDevTools.jsm", {}); |
|
16 let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); |
|
17 let { DebuggerServer } = Cu.import("resource://gre/modules/devtools/dbg-server.jsm", {}); |
|
18 let { DebuggerClient } = Cu.import("resource://gre/modules/devtools/dbg-client.jsm", {}); |
|
19 |
|
20 let { WebGLFront } = devtools.require("devtools/server/actors/webgl"); |
|
21 let TiltGL = devtools.require("devtools/tilt/tilt-gl"); |
|
22 let TargetFactory = devtools.TargetFactory; |
|
23 let Toolbox = devtools.Toolbox; |
|
24 |
|
25 const EXAMPLE_URL = "http://example.com/browser/browser/devtools/shadereditor/test/"; |
|
26 const SIMPLE_CANVAS_URL = EXAMPLE_URL + "doc_simple-canvas.html"; |
|
27 const SHADER_ORDER_URL = EXAMPLE_URL + "doc_shader-order.html"; |
|
28 const MULTIPLE_CONTEXTS_URL = EXAMPLE_URL + "doc_multiple-contexts.html"; |
|
29 const OVERLAPPING_GEOMETRY_CANVAS_URL = EXAMPLE_URL + "doc_overlapping-geometry.html"; |
|
30 const BLENDED_GEOMETRY_CANVAS_URL = EXAMPLE_URL + "doc_blended-geometry.html"; |
|
31 |
|
32 // All tests are asynchronous. |
|
33 waitForExplicitFinish(); |
|
34 |
|
35 let gToolEnabled = Services.prefs.getBoolPref("devtools.shadereditor.enabled"); |
|
36 |
|
37 registerCleanupFunction(() => { |
|
38 info("finish() was called, cleaning up..."); |
|
39 Services.prefs.setBoolPref("devtools.debugger.log", gEnableLogging); |
|
40 Services.prefs.setBoolPref("devtools.shadereditor.enabled", gToolEnabled); |
|
41 |
|
42 // These tests use a lot of memory due to GL contexts, so force a GC to help |
|
43 // fragmentation. |
|
44 info("Forcing GC after shadereditor test."); |
|
45 Cu.forceGC(); |
|
46 }); |
|
47 |
|
48 function addTab(aUrl, aWindow) { |
|
49 info("Adding tab: " + aUrl); |
|
50 |
|
51 let deferred = promise.defer(); |
|
52 let targetWindow = aWindow || window; |
|
53 let targetBrowser = targetWindow.gBrowser; |
|
54 |
|
55 targetWindow.focus(); |
|
56 let tab = targetBrowser.selectedTab = targetBrowser.addTab(aUrl); |
|
57 let linkedBrowser = tab.linkedBrowser; |
|
58 |
|
59 linkedBrowser.addEventListener("load", function onLoad() { |
|
60 linkedBrowser.removeEventListener("load", onLoad, true); |
|
61 info("Tab added and finished loading: " + aUrl); |
|
62 deferred.resolve(tab); |
|
63 }, true); |
|
64 |
|
65 return deferred.promise; |
|
66 } |
|
67 |
|
68 function removeTab(aTab, aWindow) { |
|
69 info("Removing tab."); |
|
70 |
|
71 let deferred = promise.defer(); |
|
72 let targetWindow = aWindow || window; |
|
73 let targetBrowser = targetWindow.gBrowser; |
|
74 let tabContainer = targetBrowser.tabContainer; |
|
75 |
|
76 tabContainer.addEventListener("TabClose", function onClose(aEvent) { |
|
77 tabContainer.removeEventListener("TabClose", onClose, false); |
|
78 info("Tab removed and finished closing."); |
|
79 deferred.resolve(); |
|
80 }, false); |
|
81 |
|
82 targetBrowser.removeTab(aTab); |
|
83 return deferred.promise; |
|
84 } |
|
85 |
|
86 function handleError(aError) { |
|
87 ok(false, "Got an error: " + aError.message + "\n" + aError.stack); |
|
88 finish(); |
|
89 } |
|
90 |
|
91 function ifWebGLSupported() { |
|
92 ok(false, "You need to define a 'ifWebGLSupported' function."); |
|
93 finish(); |
|
94 } |
|
95 |
|
96 function ifWebGLUnsupported() { |
|
97 todo(false, "Skipping test because WebGL isn't supported."); |
|
98 finish(); |
|
99 } |
|
100 |
|
101 function test() { |
|
102 let generator = isWebGLSupported() ? ifWebGLSupported : ifWebGLUnsupported; |
|
103 Task.spawn(generator).then(null, handleError); |
|
104 } |
|
105 |
|
106 function createCanvas() { |
|
107 return document.createElementNS("http://www.w3.org/1999/xhtml", "canvas"); |
|
108 } |
|
109 |
|
110 function isWebGLSupported() { |
|
111 let supported = |
|
112 !TiltGL.isWebGLForceEnabled() && |
|
113 TiltGL.isWebGLSupported() && |
|
114 TiltGL.create3DContext(createCanvas()); |
|
115 |
|
116 info("Apparently, WebGL is" + (supported ? "" : " not") + " supported."); |
|
117 return supported; |
|
118 } |
|
119 |
|
120 function once(aTarget, aEventName, aUseCapture = false) { |
|
121 info("Waiting for event: '" + aEventName + "' on " + aTarget + "."); |
|
122 |
|
123 let deferred = promise.defer(); |
|
124 |
|
125 for (let [add, remove] of [ |
|
126 ["on", "off"], // Use event emitter before DOM events for consistency |
|
127 ["addEventListener", "removeEventListener"], |
|
128 ["addListener", "removeListener"] |
|
129 ]) { |
|
130 if ((add in aTarget) && (remove in aTarget)) { |
|
131 aTarget[add](aEventName, function onEvent(...aArgs) { |
|
132 aTarget[remove](aEventName, onEvent, aUseCapture); |
|
133 deferred.resolve(...aArgs); |
|
134 }, aUseCapture); |
|
135 break; |
|
136 } |
|
137 } |
|
138 |
|
139 return deferred.promise; |
|
140 } |
|
141 |
|
142 // Hack around `once`, as that only resolves to a single (first) argument |
|
143 // and discards the rest. `onceSpread` is similar, except resolves to an |
|
144 // array of all of the arguments in the handler. These should be consolidated |
|
145 // into the same function, but many tests will need to be changed. |
|
146 function onceSpread(aTarget, aEvent) { |
|
147 let deferred = promise.defer(); |
|
148 aTarget.once(aEvent, (...args) => deferred.resolve(args)); |
|
149 return deferred.promise; |
|
150 } |
|
151 |
|
152 function observe(aNotificationName, aOwnsWeak = false) { |
|
153 info("Waiting for observer notification: '" + aNotificationName + "."); |
|
154 |
|
155 let deferred = promise.defer(); |
|
156 |
|
157 Services.obs.addObserver(function onNotification(...aArgs) { |
|
158 Services.obs.removeObserver(onNotification, aNotificationName); |
|
159 deferred.resolve.apply(deferred, aArgs); |
|
160 }, aNotificationName, aOwnsWeak); |
|
161 |
|
162 return deferred.promise; |
|
163 } |
|
164 |
|
165 function waitForFrame(aDebuggee) { |
|
166 let deferred = promise.defer(); |
|
167 aDebuggee.requestAnimationFrame(deferred.resolve); |
|
168 return deferred.promise; |
|
169 } |
|
170 |
|
171 function isApprox(aFirst, aSecond, aMargin = 1) { |
|
172 return Math.abs(aFirst - aSecond) <= aMargin; |
|
173 } |
|
174 |
|
175 function isApproxColor(aFirst, aSecond, aMargin) { |
|
176 return isApprox(aFirst.r, aSecond.r, aMargin) && |
|
177 isApprox(aFirst.g, aSecond.g, aMargin) && |
|
178 isApprox(aFirst.b, aSecond.b, aMargin) && |
|
179 isApprox(aFirst.a, aSecond.a, aMargin); |
|
180 } |
|
181 |
|
182 function getPixels(aDebuggee, aSelector = "canvas") { |
|
183 let canvas = aDebuggee.document.querySelector(aSelector); |
|
184 let gl = canvas.getContext("webgl"); |
|
185 |
|
186 let { width, height } = canvas; |
|
187 let buffer = new aDebuggee.Uint8Array(width * height * 4); |
|
188 gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, buffer); |
|
189 |
|
190 info("Retrieved pixels: " + width + "x" + height); |
|
191 return [buffer, width, height]; |
|
192 } |
|
193 |
|
194 function getPixel(aDebuggee, aPosition, aSelector = "canvas") { |
|
195 let canvas = aDebuggee.document.querySelector(aSelector); |
|
196 let gl = canvas.getContext("webgl"); |
|
197 |
|
198 let { width, height } = canvas; |
|
199 let { x, y } = aPosition; |
|
200 let buffer = new aDebuggee.Uint8Array(4); |
|
201 gl.readPixels(x, height - y - 1, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, buffer); |
|
202 |
|
203 let pixel = { r: buffer[0], g: buffer[1], b: buffer[2], a: buffer[3] }; |
|
204 |
|
205 info("Retrieved pixel: " + pixel.toSource() + " at " + aPosition.toSource()); |
|
206 return pixel; |
|
207 } |
|
208 |
|
209 function ensurePixelIs(aDebuggee, aPosition, aColor, aWaitFlag = false, aSelector = "canvas") { |
|
210 let pixel = getPixel(aDebuggee, aPosition, aSelector); |
|
211 if (isApproxColor(pixel, aColor)) { |
|
212 ok(true, "Expected pixel is shown at: " + aPosition.toSource()); |
|
213 return promise.resolve(null); |
|
214 } |
|
215 if (aWaitFlag) { |
|
216 return Task.spawn(function() { |
|
217 yield waitForFrame(aDebuggee); |
|
218 yield ensurePixelIs(aDebuggee, aPosition, aColor, aWaitFlag, aSelector); |
|
219 }); |
|
220 } |
|
221 ok(false, "Expected pixel was not already shown at: " + aPosition.toSource()); |
|
222 return promise.reject(null); |
|
223 } |
|
224 |
|
225 function navigateInHistory(aTarget, aDirection, aWaitForTargetEvent = "navigate") { |
|
226 executeSoon(() => content.history[aDirection]()); |
|
227 return once(aTarget, aWaitForTargetEvent); |
|
228 } |
|
229 |
|
230 function navigate(aTarget, aUrl, aWaitForTargetEvent = "navigate") { |
|
231 executeSoon(() => aTarget.activeTab.navigateTo(aUrl)); |
|
232 return once(aTarget, aWaitForTargetEvent); |
|
233 } |
|
234 |
|
235 function reload(aTarget, aWaitForTargetEvent = "navigate") { |
|
236 executeSoon(() => aTarget.activeTab.reload()); |
|
237 return once(aTarget, aWaitForTargetEvent); |
|
238 } |
|
239 |
|
240 function initBackend(aUrl) { |
|
241 info("Initializing a shader editor front."); |
|
242 |
|
243 if (!DebuggerServer.initialized) { |
|
244 DebuggerServer.init(() => true); |
|
245 DebuggerServer.addBrowserActors(); |
|
246 } |
|
247 |
|
248 return Task.spawn(function*() { |
|
249 let tab = yield addTab(aUrl); |
|
250 let target = TargetFactory.forTab(tab); |
|
251 let debuggee = target.window.wrappedJSObject; |
|
252 |
|
253 yield target.makeRemote(); |
|
254 |
|
255 let front = new WebGLFront(target.client, target.form); |
|
256 return [target, debuggee, front]; |
|
257 }); |
|
258 } |
|
259 |
|
260 function initShaderEditor(aUrl) { |
|
261 info("Initializing a shader editor pane."); |
|
262 |
|
263 return Task.spawn(function*() { |
|
264 let tab = yield addTab(aUrl); |
|
265 let target = TargetFactory.forTab(tab); |
|
266 let debuggee = target.window.wrappedJSObject; |
|
267 |
|
268 yield target.makeRemote(); |
|
269 |
|
270 Services.prefs.setBoolPref("devtools.shadereditor.enabled", true); |
|
271 let toolbox = yield gDevTools.showToolbox(target, "shadereditor"); |
|
272 let panel = toolbox.getCurrentPanel(); |
|
273 return [target, debuggee, panel]; |
|
274 }); |
|
275 } |
|
276 |
|
277 function teardown(aPanel) { |
|
278 info("Destroying the specified shader editor."); |
|
279 |
|
280 return promise.all([ |
|
281 once(aPanel, "destroyed"), |
|
282 removeTab(aPanel.target.tab) |
|
283 ]); |
|
284 } |
|
285 |
|
286 // Due to `program-linked` events firing synchronously, we cannot |
|
287 // just yield/chain them together, as then we miss all actors after the |
|
288 // first event since they're fired consecutively. This allows us to capture |
|
289 // all actors and returns an array containing them. |
|
290 // |
|
291 // Takes a `front` object that is an event emitter, the number of |
|
292 // programs that should be listened to and waited on, and an optional |
|
293 // `onAdd` function that calls with the entire actors array on program link |
|
294 function getPrograms(front, count, onAdd) { |
|
295 let actors = []; |
|
296 let deferred = promise.defer(); |
|
297 front.on("program-linked", function onLink (actor) { |
|
298 if (actors.length !== count) { |
|
299 actors.push(actor); |
|
300 if (typeof onAdd === 'function') onAdd(actors) |
|
301 } |
|
302 if (actors.length === count) { |
|
303 front.off("program-linked", onLink); |
|
304 deferred.resolve(actors); |
|
305 } |
|
306 }); |
|
307 return deferred.promise; |
|
308 } |