|
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 // Disable logging for faster test runs. Set this pref to true if you want to |
|
10 // debug a test in your try runs. Both the debugger server and frontend will |
|
11 // be affected by this pref. |
|
12 let gEnableLogging = Services.prefs.getBoolPref("devtools.debugger.log"); |
|
13 Services.prefs.setBoolPref("devtools.debugger.log", false); |
|
14 |
|
15 let { Task } = Cu.import("resource://gre/modules/Task.jsm", {}); |
|
16 let { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {}); |
|
17 let { gDevTools } = Cu.import("resource:///modules/devtools/gDevTools.jsm", {}); |
|
18 let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); |
|
19 let { require } = devtools; |
|
20 let { DevToolsUtils } = Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm", {}); |
|
21 let { BrowserToolboxProcess } = Cu.import("resource:///modules/devtools/ToolboxProcess.jsm", {}); |
|
22 let { DebuggerServer } = Cu.import("resource://gre/modules/devtools/dbg-server.jsm", {}); |
|
23 let { DebuggerClient } = Cu.import("resource://gre/modules/devtools/dbg-client.jsm", {}); |
|
24 let { AddonManager } = Cu.import("resource://gre/modules/AddonManager.jsm", {}); |
|
25 const { promiseInvoke } = require("devtools/async-utils"); |
|
26 let TargetFactory = devtools.TargetFactory; |
|
27 let Toolbox = devtools.Toolbox; |
|
28 |
|
29 const EXAMPLE_URL = "http://example.com/browser/browser/devtools/debugger/test/"; |
|
30 |
|
31 gDevTools.testing = true; |
|
32 SimpleTest.registerCleanupFunction(() => { |
|
33 gDevTools.testing = false; |
|
34 }); |
|
35 |
|
36 // All tests are asynchronous. |
|
37 waitForExplicitFinish(); |
|
38 |
|
39 registerCleanupFunction(function() { |
|
40 info("finish() was called, cleaning up..."); |
|
41 Services.prefs.setBoolPref("devtools.debugger.log", gEnableLogging); |
|
42 |
|
43 // Properly shut down the server to avoid memory leaks. |
|
44 DebuggerServer.destroy(); |
|
45 |
|
46 // Debugger tests use a lot of memory, so force a GC to help fragmentation. |
|
47 info("Forcing GC after debugger test."); |
|
48 Cu.forceGC(); |
|
49 }); |
|
50 |
|
51 // Import the GCLI test helper |
|
52 let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/")); |
|
53 Services.scriptloader.loadSubScript(testDir + "../../../commandline/test/helpers.js", this); |
|
54 |
|
55 // Redeclare dbg_assert with a fatal behavior. |
|
56 function dbg_assert(cond, e) { |
|
57 if (!cond) { |
|
58 throw e; |
|
59 } |
|
60 } |
|
61 |
|
62 function addWindow(aUrl) { |
|
63 info("Adding window: " + aUrl); |
|
64 return promise.resolve(getDOMWindow(window.open(aUrl))); |
|
65 } |
|
66 |
|
67 function getDOMWindow(aReference) { |
|
68 return aReference |
|
69 .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation) |
|
70 .QueryInterface(Ci.nsIDocShellTreeItem).rootTreeItem |
|
71 .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow); |
|
72 } |
|
73 |
|
74 function addTab(aUrl, aWindow) { |
|
75 info("Adding tab: " + aUrl); |
|
76 |
|
77 let deferred = promise.defer(); |
|
78 let targetWindow = aWindow || window; |
|
79 let targetBrowser = targetWindow.gBrowser; |
|
80 |
|
81 targetWindow.focus(); |
|
82 let tab = targetBrowser.selectedTab = targetBrowser.addTab(aUrl); |
|
83 let linkedBrowser = tab.linkedBrowser; |
|
84 |
|
85 linkedBrowser.addEventListener("load", function onLoad() { |
|
86 linkedBrowser.removeEventListener("load", onLoad, true); |
|
87 info("Tab added and finished loading: " + aUrl); |
|
88 deferred.resolve(tab); |
|
89 }, true); |
|
90 |
|
91 return deferred.promise; |
|
92 } |
|
93 |
|
94 function removeTab(aTab, aWindow) { |
|
95 info("Removing tab."); |
|
96 |
|
97 let deferred = promise.defer(); |
|
98 let targetWindow = aWindow || window; |
|
99 let targetBrowser = targetWindow.gBrowser; |
|
100 let tabContainer = targetBrowser.tabContainer; |
|
101 |
|
102 tabContainer.addEventListener("TabClose", function onClose(aEvent) { |
|
103 tabContainer.removeEventListener("TabClose", onClose, false); |
|
104 info("Tab removed and finished closing."); |
|
105 deferred.resolve(); |
|
106 }, false); |
|
107 |
|
108 targetBrowser.removeTab(aTab); |
|
109 return deferred.promise; |
|
110 } |
|
111 |
|
112 function addAddon(aUrl) { |
|
113 info("Installing addon: " + aUrl); |
|
114 |
|
115 let deferred = promise.defer(); |
|
116 |
|
117 AddonManager.getInstallForURL(aUrl, aInstaller => { |
|
118 aInstaller.install(); |
|
119 let listener = { |
|
120 onInstallEnded: function(aAddon, aAddonInstall) { |
|
121 aInstaller.removeListener(listener); |
|
122 |
|
123 // Wait for add-on's startup scripts to execute. See bug 997408 |
|
124 executeSoon(function() { |
|
125 deferred.resolve(aAddonInstall); |
|
126 }); |
|
127 } |
|
128 }; |
|
129 aInstaller.addListener(listener); |
|
130 }, "application/x-xpinstall"); |
|
131 |
|
132 return deferred.promise; |
|
133 } |
|
134 |
|
135 function removeAddon(aAddon) { |
|
136 info("Removing addon."); |
|
137 |
|
138 let deferred = promise.defer(); |
|
139 |
|
140 let listener = { |
|
141 onUninstalled: function(aUninstalledAddon) { |
|
142 if (aUninstalledAddon != aAddon) { |
|
143 return; |
|
144 } |
|
145 AddonManager.removeAddonListener(listener); |
|
146 deferred.resolve(); |
|
147 } |
|
148 }; |
|
149 AddonManager.addAddonListener(listener); |
|
150 aAddon.uninstall(); |
|
151 |
|
152 return deferred.promise; |
|
153 } |
|
154 |
|
155 function getTabActorForUrl(aClient, aUrl) { |
|
156 let deferred = promise.defer(); |
|
157 |
|
158 aClient.listTabs(aResponse => { |
|
159 let tabActor = aResponse.tabs.filter(aGrip => aGrip.url == aUrl).pop(); |
|
160 deferred.resolve(tabActor); |
|
161 }); |
|
162 |
|
163 return deferred.promise; |
|
164 } |
|
165 |
|
166 function getAddonActorForUrl(aClient, aUrl) { |
|
167 info("Get addon actor for URL: " + aUrl); |
|
168 let deferred = promise.defer(); |
|
169 |
|
170 aClient.listAddons(aResponse => { |
|
171 let addonActor = aResponse.addons.filter(aGrip => aGrip.url == aUrl).pop(); |
|
172 info("got addon actor for URL: " + addonActor.actor); |
|
173 deferred.resolve(addonActor); |
|
174 }); |
|
175 |
|
176 return deferred.promise; |
|
177 } |
|
178 |
|
179 function attachTabActorForUrl(aClient, aUrl) { |
|
180 let deferred = promise.defer(); |
|
181 |
|
182 getTabActorForUrl(aClient, aUrl).then(aGrip => { |
|
183 aClient.attachTab(aGrip.actor, aResponse => { |
|
184 deferred.resolve([aGrip, aResponse]); |
|
185 }); |
|
186 }); |
|
187 |
|
188 return deferred.promise; |
|
189 } |
|
190 |
|
191 function attachThreadActorForUrl(aClient, aUrl) { |
|
192 let deferred = promise.defer(); |
|
193 |
|
194 attachTabActorForUrl(aClient, aUrl).then(([aGrip, aResponse]) => { |
|
195 aClient.attachThread(aResponse.threadActor, (aResponse, aThreadClient) => { |
|
196 aThreadClient.resume(aResponse => { |
|
197 deferred.resolve(aThreadClient); |
|
198 }); |
|
199 }); |
|
200 }); |
|
201 |
|
202 return deferred.promise; |
|
203 } |
|
204 |
|
205 function once(aTarget, aEventName, aUseCapture = false) { |
|
206 info("Waiting for event: '" + aEventName + "' on " + aTarget + "."); |
|
207 |
|
208 let deferred = promise.defer(); |
|
209 |
|
210 for (let [add, remove] of [ |
|
211 ["addEventListener", "removeEventListener"], |
|
212 ["addListener", "removeListener"], |
|
213 ["on", "off"] |
|
214 ]) { |
|
215 if ((add in aTarget) && (remove in aTarget)) { |
|
216 aTarget[add](aEventName, function onEvent(...aArgs) { |
|
217 aTarget[remove](aEventName, onEvent, aUseCapture); |
|
218 deferred.resolve.apply(deferred, aArgs); |
|
219 }, aUseCapture); |
|
220 break; |
|
221 } |
|
222 } |
|
223 |
|
224 return deferred.promise; |
|
225 } |
|
226 |
|
227 function waitForTick() { |
|
228 let deferred = promise.defer(); |
|
229 executeSoon(deferred.resolve); |
|
230 return deferred.promise; |
|
231 } |
|
232 |
|
233 function waitForTime(aDelay) { |
|
234 let deferred = promise.defer(); |
|
235 setTimeout(deferred.resolve, aDelay); |
|
236 return deferred.promise; |
|
237 } |
|
238 |
|
239 function waitForSourceShown(aPanel, aUrl) { |
|
240 return waitForDebuggerEvents(aPanel, aPanel.panelWin.EVENTS.SOURCE_SHOWN).then(aSource => { |
|
241 let sourceUrl = aSource.url; |
|
242 info("Source shown: " + sourceUrl); |
|
243 |
|
244 if (!sourceUrl.contains(aUrl)) { |
|
245 return waitForSourceShown(aPanel, aUrl); |
|
246 } else { |
|
247 ok(true, "The correct source has been shown."); |
|
248 } |
|
249 }); |
|
250 } |
|
251 |
|
252 function waitForEditorLocationSet(aPanel) { |
|
253 return waitForDebuggerEvents(aPanel, aPanel.panelWin.EVENTS.EDITOR_LOCATION_SET); |
|
254 } |
|
255 |
|
256 function ensureSourceIs(aPanel, aUrl, aWaitFlag = false) { |
|
257 if (aPanel.panelWin.DebuggerView.Sources.selectedValue.contains(aUrl)) { |
|
258 ok(true, "Expected source is shown: " + aUrl); |
|
259 return promise.resolve(null); |
|
260 } |
|
261 if (aWaitFlag) { |
|
262 return waitForSourceShown(aPanel, aUrl); |
|
263 } |
|
264 ok(false, "Expected source was not already shown: " + aUrl); |
|
265 return promise.reject(null); |
|
266 } |
|
267 |
|
268 function waitForCaretUpdated(aPanel, aLine, aCol = 1) { |
|
269 return waitForEditorEvents(aPanel, "cursorActivity").then(() => { |
|
270 let cursor = aPanel.panelWin.DebuggerView.editor.getCursor(); |
|
271 info("Caret updated: " + (cursor.line + 1) + ", " + (cursor.ch + 1)); |
|
272 |
|
273 if (!isCaretPos(aPanel, aLine, aCol)) { |
|
274 return waitForCaretUpdated(aPanel, aLine, aCol); |
|
275 } else { |
|
276 ok(true, "The correct caret position has been set."); |
|
277 } |
|
278 }); |
|
279 } |
|
280 |
|
281 function ensureCaretAt(aPanel, aLine, aCol = 1, aWaitFlag = false) { |
|
282 if (isCaretPos(aPanel, aLine, aCol)) { |
|
283 ok(true, "Expected caret position is set: " + aLine + "," + aCol); |
|
284 return promise.resolve(null); |
|
285 } |
|
286 if (aWaitFlag) { |
|
287 return waitForCaretUpdated(aPanel, aLine, aCol); |
|
288 } |
|
289 ok(false, "Expected caret position was not already set: " + aLine + "," + aCol); |
|
290 return promise.reject(null); |
|
291 } |
|
292 |
|
293 function isCaretPos(aPanel, aLine, aCol = 1) { |
|
294 let editor = aPanel.panelWin.DebuggerView.editor; |
|
295 let cursor = editor.getCursor(); |
|
296 |
|
297 // Source editor starts counting line and column numbers from 0. |
|
298 info("Current editor caret position: " + (cursor.line + 1) + ", " + (cursor.ch + 1)); |
|
299 return cursor.line == (aLine - 1) && cursor.ch == (aCol - 1); |
|
300 } |
|
301 |
|
302 function isEditorSel(aPanel, [start, end]) { |
|
303 let editor = aPanel.panelWin.DebuggerView.editor; |
|
304 let range = { |
|
305 start: editor.getOffset(editor.getCursor("start")), |
|
306 end: editor.getOffset(editor.getCursor()) |
|
307 }; |
|
308 |
|
309 // Source editor starts counting line and column numbers from 0. |
|
310 info("Current editor selection: " + (range.start + 1) + ", " + (range.end + 1)); |
|
311 return range.start == (start - 1) && range.end == (end - 1); |
|
312 } |
|
313 |
|
314 function waitForSourceAndCaret(aPanel, aUrl, aLine, aCol) { |
|
315 return promise.all([ |
|
316 waitForSourceShown(aPanel, aUrl), |
|
317 waitForCaretUpdated(aPanel, aLine, aCol) |
|
318 ]); |
|
319 } |
|
320 |
|
321 function waitForCaretAndScopes(aPanel, aLine, aCol) { |
|
322 return promise.all([ |
|
323 waitForCaretUpdated(aPanel, aLine, aCol), |
|
324 waitForDebuggerEvents(aPanel, aPanel.panelWin.EVENTS.FETCHED_SCOPES) |
|
325 ]); |
|
326 } |
|
327 |
|
328 function waitForSourceAndCaretAndScopes(aPanel, aUrl, aLine, aCol) { |
|
329 return promise.all([ |
|
330 waitForSourceAndCaret(aPanel, aUrl, aLine, aCol), |
|
331 waitForDebuggerEvents(aPanel, aPanel.panelWin.EVENTS.FETCHED_SCOPES) |
|
332 ]); |
|
333 } |
|
334 |
|
335 function waitForDebuggerEvents(aPanel, aEventName, aEventRepeat = 1) { |
|
336 info("Waiting for debugger event: '" + aEventName + "' to fire: " + aEventRepeat + " time(s)."); |
|
337 |
|
338 let deferred = promise.defer(); |
|
339 let panelWin = aPanel.panelWin; |
|
340 let count = 0; |
|
341 |
|
342 panelWin.on(aEventName, function onEvent(aEventName, ...aArgs) { |
|
343 info("Debugger event '" + aEventName + "' fired: " + (++count) + " time(s)."); |
|
344 |
|
345 if (count == aEventRepeat) { |
|
346 ok(true, "Enough '" + aEventName + "' panel events have been fired."); |
|
347 panelWin.off(aEventName, onEvent); |
|
348 deferred.resolve.apply(deferred, aArgs); |
|
349 } |
|
350 }); |
|
351 |
|
352 return deferred.promise; |
|
353 } |
|
354 |
|
355 function waitForEditorEvents(aPanel, aEventName, aEventRepeat = 1) { |
|
356 info("Waiting for editor event: '" + aEventName + "' to fire: " + aEventRepeat + " time(s)."); |
|
357 |
|
358 let deferred = promise.defer(); |
|
359 let editor = aPanel.panelWin.DebuggerView.editor; |
|
360 let count = 0; |
|
361 |
|
362 editor.on(aEventName, function onEvent(...aArgs) { |
|
363 info("Editor event '" + aEventName + "' fired: " + (++count) + " time(s)."); |
|
364 |
|
365 if (count == aEventRepeat) { |
|
366 ok(true, "Enough '" + aEventName + "' editor events have been fired."); |
|
367 editor.off(aEventName, onEvent); |
|
368 deferred.resolve.apply(deferred, aArgs); |
|
369 } |
|
370 }); |
|
371 |
|
372 return deferred.promise; |
|
373 } |
|
374 |
|
375 function waitForThreadEvents(aPanel, aEventName, aEventRepeat = 1) { |
|
376 info("Waiting for thread event: '" + aEventName + "' to fire: " + aEventRepeat + " time(s)."); |
|
377 |
|
378 let deferred = promise.defer(); |
|
379 let thread = aPanel.panelWin.gThreadClient; |
|
380 let count = 0; |
|
381 |
|
382 thread.addListener(aEventName, function onEvent(aEventName, ...aArgs) { |
|
383 info("Thread event '" + aEventName + "' fired: " + (++count) + " time(s)."); |
|
384 |
|
385 if (count == aEventRepeat) { |
|
386 ok(true, "Enough '" + aEventName + "' thread events have been fired."); |
|
387 thread.removeListener(aEventName, onEvent); |
|
388 deferred.resolve.apply(deferred, aArgs); |
|
389 } |
|
390 }); |
|
391 |
|
392 return deferred.promise; |
|
393 } |
|
394 |
|
395 function waitForClientEvents(aPanel, aEventName, aEventRepeat = 1) { |
|
396 info("Waiting for client event: '" + aEventName + "' to fire: " + aEventRepeat + " time(s)."); |
|
397 |
|
398 let deferred = promise.defer(); |
|
399 let client = aPanel.panelWin.gClient; |
|
400 let count = 0; |
|
401 |
|
402 client.addListener(aEventName, function onEvent(aEventName, ...aArgs) { |
|
403 info("Thread event '" + aEventName + "' fired: " + (++count) + " time(s)."); |
|
404 |
|
405 if (count == aEventRepeat) { |
|
406 ok(true, "Enough '" + aEventName + "' thread events have been fired."); |
|
407 client.removeListener(aEventName, onEvent); |
|
408 deferred.resolve.apply(deferred, aArgs); |
|
409 } |
|
410 }); |
|
411 |
|
412 return deferred.promise; |
|
413 } |
|
414 |
|
415 function ensureThreadClientState(aPanel, aState) { |
|
416 let thread = aPanel.panelWin.gThreadClient; |
|
417 let state = thread.state; |
|
418 |
|
419 info("Thread is: '" + state + "'."); |
|
420 |
|
421 if (state == aState) { |
|
422 return promise.resolve(null); |
|
423 } else { |
|
424 return waitForThreadEvents(aPanel, aState); |
|
425 } |
|
426 } |
|
427 |
|
428 function navigateActiveTabTo(aPanel, aUrl, aWaitForEventName, aEventRepeat) { |
|
429 let finished = waitForDebuggerEvents(aPanel, aWaitForEventName, aEventRepeat); |
|
430 let activeTab = aPanel.panelWin.DebuggerController._target.activeTab; |
|
431 aUrl ? activeTab.navigateTo(aUrl) : activeTab.reload(); |
|
432 return finished; |
|
433 } |
|
434 |
|
435 function navigateActiveTabInHistory(aPanel, aDirection, aWaitForEventName, aEventRepeat) { |
|
436 let finished = waitForDebuggerEvents(aPanel, aWaitForEventName, aEventRepeat); |
|
437 content.history[aDirection](); |
|
438 return finished; |
|
439 } |
|
440 |
|
441 function reloadActiveTab(aPanel, aWaitForEventName, aEventRepeat) { |
|
442 return navigateActiveTabTo(aPanel, null, aWaitForEventName, aEventRepeat); |
|
443 } |
|
444 |
|
445 function clearText(aElement) { |
|
446 info("Clearing text..."); |
|
447 aElement.focus(); |
|
448 aElement.value = ""; |
|
449 } |
|
450 |
|
451 function setText(aElement, aText) { |
|
452 clearText(aElement); |
|
453 info("Setting text: " + aText); |
|
454 aElement.value = aText; |
|
455 } |
|
456 |
|
457 function typeText(aElement, aText) { |
|
458 info("Typing text: " + aText); |
|
459 aElement.focus(); |
|
460 EventUtils.sendString(aText, aElement.ownerDocument.defaultView); |
|
461 } |
|
462 |
|
463 function backspaceText(aElement, aTimes) { |
|
464 info("Pressing backspace " + aTimes + " times."); |
|
465 for (let i = 0; i < aTimes; i++) { |
|
466 aElement.focus(); |
|
467 EventUtils.sendKey("BACK_SPACE", aElement.ownerDocument.defaultView); |
|
468 } |
|
469 } |
|
470 |
|
471 function getTab(aTarget, aWindow) { |
|
472 if (aTarget instanceof XULElement) { |
|
473 return promise.resolve(aTarget); |
|
474 } else { |
|
475 return addTab(aTarget, aWindow); |
|
476 } |
|
477 } |
|
478 |
|
479 function getSources(aClient) { |
|
480 let deferred = promise.defer(); |
|
481 |
|
482 aClient.getSources(({sources}) => deferred.resolve(sources)); |
|
483 |
|
484 return deferred.promise; |
|
485 } |
|
486 |
|
487 function initDebugger(aTarget, aWindow) { |
|
488 info("Initializing a debugger panel."); |
|
489 |
|
490 return getTab(aTarget, aWindow).then(aTab => { |
|
491 info("Debugee tab added successfully: " + aTarget); |
|
492 |
|
493 let deferred = promise.defer(); |
|
494 let debuggee = aTab.linkedBrowser.contentWindow.wrappedJSObject; |
|
495 let target = TargetFactory.forTab(aTab); |
|
496 |
|
497 gDevTools.showToolbox(target, "jsdebugger").then(aToolbox => { |
|
498 info("Debugger panel shown successfully."); |
|
499 |
|
500 let debuggerPanel = aToolbox.getCurrentPanel(); |
|
501 let panelWin = debuggerPanel.panelWin; |
|
502 |
|
503 // Wait for the initial resume... |
|
504 panelWin.gClient.addOneTimeListener("resumed", () => { |
|
505 info("Debugger client resumed successfully."); |
|
506 |
|
507 prepareDebugger(debuggerPanel); |
|
508 deferred.resolve([aTab, debuggee, debuggerPanel, aWindow]); |
|
509 }); |
|
510 }); |
|
511 |
|
512 return deferred.promise; |
|
513 }); |
|
514 } |
|
515 |
|
516 // Creates an add-on debugger for a given add-on. The returned AddonDebugger |
|
517 // object must be destroyed before finishing the test |
|
518 function initAddonDebugger(aUrl) { |
|
519 let addonDebugger = new AddonDebugger(); |
|
520 return addonDebugger.init(aUrl).then(() => addonDebugger); |
|
521 } |
|
522 |
|
523 function AddonDebugger() { |
|
524 this._onMessage = this._onMessage.bind(this); |
|
525 } |
|
526 |
|
527 AddonDebugger.prototype = { |
|
528 init: Task.async(function*(aUrl) { |
|
529 info("Initializing an addon debugger panel."); |
|
530 |
|
531 if (!DebuggerServer.initialized) { |
|
532 DebuggerServer.init(() => true); |
|
533 DebuggerServer.addBrowserActors(); |
|
534 } |
|
535 |
|
536 this.frame = document.createElement("iframe"); |
|
537 this.frame.setAttribute("height", 400); |
|
538 document.documentElement.appendChild(this.frame); |
|
539 window.addEventListener("message", this._onMessage); |
|
540 |
|
541 let transport = DebuggerServer.connectPipe(); |
|
542 this.client = new DebuggerClient(transport); |
|
543 |
|
544 let connected = promise.defer(); |
|
545 this.client.connect(connected.resolve); |
|
546 yield connected.promise; |
|
547 |
|
548 let addonActor = yield getAddonActorForUrl(this.client, aUrl); |
|
549 |
|
550 let targetOptions = { |
|
551 form: { addonActor: addonActor.actor, title: addonActor.name }, |
|
552 client: this.client, |
|
553 chrome: true |
|
554 }; |
|
555 |
|
556 let toolboxOptions = { |
|
557 customIframe: this.frame |
|
558 }; |
|
559 |
|
560 let target = devtools.TargetFactory.forTab(targetOptions); |
|
561 let toolbox = yield gDevTools.showToolbox(target, "jsdebugger", devtools.Toolbox.HostType.CUSTOM, toolboxOptions); |
|
562 |
|
563 info("Addon debugger panel shown successfully."); |
|
564 |
|
565 this.debuggerPanel = toolbox.getCurrentPanel(); |
|
566 |
|
567 // Wait for the initial resume... |
|
568 yield waitForClientEvents(this.debuggerPanel, "resumed"); |
|
569 yield prepareDebugger(this.debuggerPanel); |
|
570 }), |
|
571 |
|
572 destroy: Task.async(function*() { |
|
573 let deferred = promise.defer(); |
|
574 this.client.close(deferred.resolve); |
|
575 yield deferred.promise; |
|
576 yield this.debuggerPanel._toolbox.destroy(); |
|
577 this.frame.remove(); |
|
578 window.removeEventListener("message", this._onMessage); |
|
579 }), |
|
580 |
|
581 /** |
|
582 * Returns a list of the groups and sources in the UI. The returned array |
|
583 * contains objects for each group with properties name and sources. The |
|
584 * sources property contains an array with objects for each source for that |
|
585 * group with properties label and url. |
|
586 */ |
|
587 getSourceGroups: Task.async(function*() { |
|
588 let debuggerWin = this.debuggerPanel.panelWin; |
|
589 let sources = yield getSources(debuggerWin.gThreadClient); |
|
590 ok(sources.length, "retrieved sources"); |
|
591 |
|
592 // groups will be the return value, groupmap and the maps we put in it will |
|
593 // be used as quick lookups to add the url information in below |
|
594 let groups = []; |
|
595 let groupmap = new Map(); |
|
596 |
|
597 let uigroups = this.debuggerPanel.panelWin.document.querySelectorAll(".side-menu-widget-group"); |
|
598 for (let g of uigroups) { |
|
599 let name = g.querySelector(".side-menu-widget-group-title .name").value; |
|
600 let group = { |
|
601 name: name, |
|
602 sources: [] |
|
603 }; |
|
604 groups.push(group); |
|
605 let labelmap = new Map(); |
|
606 groupmap.set(name, labelmap); |
|
607 |
|
608 for (let l of g.querySelectorAll(".dbg-source-item")) { |
|
609 let source = { |
|
610 label: l.value, |
|
611 url: null |
|
612 }; |
|
613 |
|
614 labelmap.set(l.value, source); |
|
615 group.sources.push(source); |
|
616 } |
|
617 } |
|
618 |
|
619 for (let source of sources) { |
|
620 let { label, group } = debuggerWin.DebuggerView.Sources.getItemByValue(source.url).attachment; |
|
621 |
|
622 if (!groupmap.has(group)) { |
|
623 ok(false, "Saw a source group not in the UI: " + group); |
|
624 continue; |
|
625 } |
|
626 |
|
627 if (!groupmap.get(group).has(label)) { |
|
628 ok(false, "Saw a source label not in the UI: " + label); |
|
629 continue; |
|
630 } |
|
631 |
|
632 groupmap.get(group).get(label).url = source.url.split(" -> ").pop(); |
|
633 } |
|
634 |
|
635 return groups; |
|
636 }), |
|
637 |
|
638 _onMessage: function(event) { |
|
639 let json = JSON.parse(event.data); |
|
640 switch (json.name) { |
|
641 case "toolbox-title": |
|
642 this.title = json.data.value; |
|
643 break; |
|
644 } |
|
645 } |
|
646 } |
|
647 |
|
648 function initChromeDebugger(aOnClose) { |
|
649 info("Initializing a chrome debugger process."); |
|
650 |
|
651 let deferred = promise.defer(); |
|
652 |
|
653 // Wait for the toolbox process to start... |
|
654 BrowserToolboxProcess.init(aOnClose, (aEvent, aProcess) => { |
|
655 info("Browser toolbox process started successfully."); |
|
656 |
|
657 prepareDebugger(aProcess); |
|
658 deferred.resolve(aProcess); |
|
659 }); |
|
660 |
|
661 return deferred.promise; |
|
662 } |
|
663 |
|
664 function prepareDebugger(aDebugger) { |
|
665 if ("target" in aDebugger) { |
|
666 let view = aDebugger.panelWin.DebuggerView; |
|
667 view.Variables.lazyEmpty = false; |
|
668 view.Variables.lazySearch = false; |
|
669 view.FilteredSources._autoSelectFirstItem = true; |
|
670 view.FilteredFunctions._autoSelectFirstItem = true; |
|
671 } else { |
|
672 // Nothing to do here yet. |
|
673 } |
|
674 } |
|
675 |
|
676 function teardown(aPanel, aFlags = {}) { |
|
677 info("Destroying the specified debugger."); |
|
678 |
|
679 let toolbox = aPanel._toolbox; |
|
680 let tab = aPanel.target.tab; |
|
681 let debuggerRootActorDisconnected = once(window, "Debugger:Shutdown"); |
|
682 let debuggerPanelDestroyed = once(aPanel, "destroyed"); |
|
683 let devtoolsToolboxDestroyed = toolbox.destroy(); |
|
684 |
|
685 return promise.all([ |
|
686 debuggerRootActorDisconnected, |
|
687 debuggerPanelDestroyed, |
|
688 devtoolsToolboxDestroyed |
|
689 ]).then(() => aFlags.noTabRemoval ? null : removeTab(tab)); |
|
690 } |
|
691 |
|
692 function closeDebuggerAndFinish(aPanel, aFlags = {}) { |
|
693 let thread = aPanel.panelWin.gThreadClient; |
|
694 if (thread.state == "paused" && !aFlags.whilePaused) { |
|
695 ok(false, "You should use 'resumeDebuggerThenCloseAndFinish' instead, " + |
|
696 "unless you're absolutely sure about what you're doing."); |
|
697 } |
|
698 return teardown(aPanel, aFlags).then(finish); |
|
699 } |
|
700 |
|
701 function resumeDebuggerThenCloseAndFinish(aPanel, aFlags = {}) { |
|
702 let deferred = promise.defer(); |
|
703 let thread = aPanel.panelWin.gThreadClient; |
|
704 thread.resume(() => closeDebuggerAndFinish(aPanel, aFlags).then(deferred.resolve)); |
|
705 return deferred.promise; |
|
706 } |
|
707 |
|
708 // Blackboxing helpers |
|
709 |
|
710 function getBlackBoxButton(aPanel) { |
|
711 return aPanel.panelWin.document.getElementById("black-box"); |
|
712 } |
|
713 |
|
714 function toggleBlackBoxing(aPanel, aSource = null) { |
|
715 function clickBlackBoxButton() { |
|
716 getBlackBoxButton(aPanel).click(); |
|
717 } |
|
718 |
|
719 const blackBoxChanged = waitForThreadEvents(aPanel, "blackboxchange"); |
|
720 |
|
721 if (aSource) { |
|
722 aPanel.panelWin.DebuggerView.Sources.selectedValue = aSource; |
|
723 ensureSourceIs(aPanel, aSource, true).then(clickBlackBoxButton); |
|
724 } else { |
|
725 clickBlackBoxButton(); |
|
726 } |
|
727 |
|
728 return blackBoxChanged; |
|
729 } |
|
730 |
|
731 function selectSourceAndGetBlackBoxButton(aPanel, aSource) { |
|
732 function returnBlackboxButton() { |
|
733 return getBlackBoxButton(aPanel); |
|
734 } |
|
735 |
|
736 aPanel.panelWin.DebuggerView.Sources.selectedValue = aSource; |
|
737 return ensureSourceIs(aPanel, aSource, true).then(returnBlackboxButton); |
|
738 } |
|
739 |
|
740 // Variables view inspection popup helpers |
|
741 |
|
742 function openVarPopup(aPanel, aCoords, aWaitForFetchedProperties) { |
|
743 let events = aPanel.panelWin.EVENTS; |
|
744 let editor = aPanel.panelWin.DebuggerView.editor; |
|
745 let bubble = aPanel.panelWin.DebuggerView.VariableBubble; |
|
746 let tooltip = bubble._tooltip.panel; |
|
747 |
|
748 let popupShown = once(tooltip, "popupshown"); |
|
749 let fetchedProperties = aWaitForFetchedProperties |
|
750 ? waitForDebuggerEvents(aPanel, events.FETCHED_BUBBLE_PROPERTIES) |
|
751 : promise.resolve(null); |
|
752 |
|
753 let { left, top } = editor.getCoordsFromPosition(aCoords); |
|
754 bubble._findIdentifier(left, top); |
|
755 return promise.all([popupShown, fetchedProperties]).then(waitForTick); |
|
756 } |
|
757 |
|
758 // Simulates the mouse hovering a variable in the debugger |
|
759 // Takes in account the position of the cursor in the text, if the text is |
|
760 // selected and if a button is currently pushed (aButtonPushed > 0). |
|
761 // The function returns a promise which returns true if the popup opened or |
|
762 // false if it didn't |
|
763 function intendOpenVarPopup(aPanel, aPosition, aButtonPushed) { |
|
764 let bubble = aPanel.panelWin.DebuggerView.VariableBubble; |
|
765 let editor = aPanel.panelWin.DebuggerView.editor; |
|
766 let tooltip = bubble._tooltip; |
|
767 |
|
768 let { left, top } = editor.getCoordsFromPosition(aPosition); |
|
769 |
|
770 const eventDescriptor = { |
|
771 clientX: left, |
|
772 clientY: top, |
|
773 buttons: aButtonPushed |
|
774 }; |
|
775 |
|
776 bubble._onMouseMove(eventDescriptor); |
|
777 |
|
778 const deferred = promise.defer(); |
|
779 window.setTimeout( |
|
780 function() { |
|
781 if(tooltip.isEmpty()) { |
|
782 deferred.resolve(false); |
|
783 } else { |
|
784 deferred.resolve(true); |
|
785 } |
|
786 }, |
|
787 tooltip.defaultShowDelay + 1000 |
|
788 ); |
|
789 |
|
790 return deferred.promise; |
|
791 } |
|
792 |
|
793 function hideVarPopup(aPanel) { |
|
794 let bubble = aPanel.panelWin.DebuggerView.VariableBubble; |
|
795 let tooltip = bubble._tooltip.panel; |
|
796 |
|
797 let popupHiding = once(tooltip, "popuphiding"); |
|
798 bubble.hideContents(); |
|
799 return popupHiding.then(waitForTick); |
|
800 } |
|
801 |
|
802 function hideVarPopupByScrollingEditor(aPanel) { |
|
803 let editor = aPanel.panelWin.DebuggerView.editor; |
|
804 let bubble = aPanel.panelWin.DebuggerView.VariableBubble; |
|
805 let tooltip = bubble._tooltip.panel; |
|
806 |
|
807 let popupHiding = once(tooltip, "popuphiding"); |
|
808 editor.setFirstVisibleLine(0); |
|
809 return popupHiding.then(waitForTick); |
|
810 } |
|
811 |
|
812 function reopenVarPopup(...aArgs) { |
|
813 return hideVarPopup.apply(this, aArgs).then(() => openVarPopup.apply(this, aArgs)); |
|
814 } |
|
815 |
|
816 // Tracing helpers |
|
817 |
|
818 function startTracing(aPanel) { |
|
819 const deferred = promise.defer(); |
|
820 aPanel.panelWin.DebuggerController.Tracer.startTracing(aResponse => { |
|
821 if (aResponse.error) { |
|
822 deferred.reject(aResponse); |
|
823 } else { |
|
824 deferred.resolve(aResponse); |
|
825 } |
|
826 }); |
|
827 return deferred.promise; |
|
828 } |
|
829 |
|
830 function stopTracing(aPanel) { |
|
831 const deferred = promise.defer(); |
|
832 aPanel.panelWin.DebuggerController.Tracer.stopTracing(aResponse => { |
|
833 if (aResponse.error) { |
|
834 deferred.reject(aResponse); |
|
835 } else { |
|
836 deferred.resolve(aResponse); |
|
837 } |
|
838 }); |
|
839 return deferred.promise; |
|
840 } |
|
841 |
|
842 function filterTraces(aPanel, f) { |
|
843 const traces = aPanel.panelWin.document |
|
844 .getElementById("tracer-traces") |
|
845 .querySelector("scrollbox") |
|
846 .children; |
|
847 return Array.filter(traces, f); |
|
848 } |
|
849 function attachAddonActorForUrl(aClient, aUrl) { |
|
850 let deferred = promise.defer(); |
|
851 |
|
852 getAddonActorForUrl(aClient, aUrl).then(aGrip => { |
|
853 aClient.attachAddon(aGrip.actor, aResponse => { |
|
854 deferred.resolve([aGrip, aResponse]); |
|
855 }); |
|
856 }); |
|
857 |
|
858 return deferred.promise; |
|
859 } |
|
860 |
|
861 function rdpInvoke(aClient, aMethod, ...args) { |
|
862 return promiseInvoke(aClient, aMethod, ...args) |
|
863 .then(({error, message }) => { |
|
864 if (error) { |
|
865 throw new Error(error + ": " + message); |
|
866 } |
|
867 }); |
|
868 } |
|
869 |
|
870 function doResume(aPanel) { |
|
871 const threadClient = aPanel.panelWin.gThreadClient; |
|
872 return rdpInvoke(threadClient, threadClient.resume); |
|
873 } |
|
874 |
|
875 function doInterrupt(aPanel) { |
|
876 const threadClient = aPanel.panelWin.gThreadClient; |
|
877 return rdpInvoke(threadClient, threadClient.interrupt); |
|
878 } |
|
879 |