|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 "use strict"; |
|
6 |
|
7 // Avoid leaks by using tmp for imports... |
|
8 let tmp = {}; |
|
9 Cu.import("resource://gre/modules/Promise.jsm", tmp); |
|
10 Cu.import("resource:///modules/CustomizableUI.jsm", tmp); |
|
11 let {Promise, CustomizableUI} = tmp; |
|
12 |
|
13 let ChromeUtils = {}; |
|
14 Services.scriptloader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/ChromeUtils.js", ChromeUtils); |
|
15 |
|
16 Services.prefs.setBoolPref("browser.uiCustomization.skipSourceNodeCheck", true); |
|
17 registerCleanupFunction(() => Services.prefs.clearUserPref("browser.uiCustomization.skipSourceNodeCheck")); |
|
18 |
|
19 let {synthesizeDragStart, synthesizeDrop} = ChromeUtils; |
|
20 |
|
21 const kNSXUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; |
|
22 const kTabEventFailureTimeoutInMs = 20000; |
|
23 |
|
24 function createDummyXULButton(id, label) { |
|
25 let btn = document.createElementNS(kNSXUL, "toolbarbutton"); |
|
26 btn.id = id; |
|
27 btn.setAttribute("label", label || id); |
|
28 btn.className = "toolbarbutton-1 chromeclass-toolbar-additional"; |
|
29 window.gNavToolbox.palette.appendChild(btn); |
|
30 return btn; |
|
31 } |
|
32 |
|
33 let gAddedToolbars = new Set(); |
|
34 |
|
35 function createToolbarWithPlacements(id, placements = []) { |
|
36 gAddedToolbars.add(id); |
|
37 let tb = document.createElementNS(kNSXUL, "toolbar"); |
|
38 tb.id = id; |
|
39 tb.setAttribute("customizable", "true"); |
|
40 CustomizableUI.registerArea(id, { |
|
41 type: CustomizableUI.TYPE_TOOLBAR, |
|
42 defaultPlacements: placements |
|
43 }); |
|
44 gNavToolbox.appendChild(tb); |
|
45 return tb; |
|
46 } |
|
47 |
|
48 function createOverflowableToolbarWithPlacements(id, placements) { |
|
49 gAddedToolbars.add(id); |
|
50 |
|
51 let tb = document.createElementNS(kNSXUL, "toolbar"); |
|
52 tb.id = id; |
|
53 tb.setAttribute("customizationtarget", id + "-target"); |
|
54 |
|
55 let customizationtarget = document.createElementNS(kNSXUL, "hbox"); |
|
56 customizationtarget.id = id + "-target"; |
|
57 customizationtarget.setAttribute("flex", "1"); |
|
58 tb.appendChild(customizationtarget); |
|
59 |
|
60 let overflowPanel = document.createElementNS(kNSXUL, "panel"); |
|
61 overflowPanel.id = id + "-overflow"; |
|
62 document.getElementById("mainPopupSet").appendChild(overflowPanel); |
|
63 |
|
64 let overflowList = document.createElementNS(kNSXUL, "vbox"); |
|
65 overflowList.id = id + "-overflow-list"; |
|
66 overflowPanel.appendChild(overflowList); |
|
67 |
|
68 let chevron = document.createElementNS(kNSXUL, "toolbarbutton"); |
|
69 chevron.id = id + "-chevron"; |
|
70 tb.appendChild(chevron); |
|
71 |
|
72 CustomizableUI.registerArea(id, { |
|
73 type: CustomizableUI.TYPE_TOOLBAR, |
|
74 defaultPlacements: placements, |
|
75 overflowable: true, |
|
76 }); |
|
77 |
|
78 tb.setAttribute("customizable", "true"); |
|
79 tb.setAttribute("overflowable", "true"); |
|
80 tb.setAttribute("overflowpanel", overflowPanel.id); |
|
81 tb.setAttribute("overflowtarget", overflowList.id); |
|
82 tb.setAttribute("overflowbutton", chevron.id); |
|
83 |
|
84 gNavToolbox.appendChild(tb); |
|
85 return tb; |
|
86 } |
|
87 |
|
88 function removeCustomToolbars() { |
|
89 CustomizableUI.reset(); |
|
90 for (let toolbarId of gAddedToolbars) { |
|
91 CustomizableUI.unregisterArea(toolbarId, true); |
|
92 let tb = document.getElementById(toolbarId); |
|
93 if (tb.hasAttribute("overflowpanel")) { |
|
94 let panel = document.getElementById(tb.getAttribute("overflowpanel")); |
|
95 if (panel) |
|
96 panel.remove(); |
|
97 } |
|
98 tb.remove(); |
|
99 } |
|
100 gAddedToolbars.clear(); |
|
101 } |
|
102 |
|
103 function getToolboxCustomToolbarId(toolbarName) { |
|
104 return "__customToolbar_" + toolbarName.replace(" ", "_"); |
|
105 } |
|
106 |
|
107 function resetCustomization() { |
|
108 return CustomizableUI.reset(); |
|
109 } |
|
110 |
|
111 function isInWin8() { |
|
112 if (!Services.metro) |
|
113 return false; |
|
114 return Services.metro.supported; |
|
115 } |
|
116 |
|
117 function addSwitchToMetroButtonInWindows8(areaPanelPlacements) { |
|
118 if (isInWin8()) { |
|
119 areaPanelPlacements.push("switch-to-metro-button"); |
|
120 } |
|
121 } |
|
122 |
|
123 function assertAreaPlacements(areaId, expectedPlacements) { |
|
124 let actualPlacements = getAreaWidgetIds(areaId); |
|
125 placementArraysEqual(areaId, actualPlacements, expectedPlacements); |
|
126 } |
|
127 |
|
128 function placementArraysEqual(areaId, actualPlacements, expectedPlacements) { |
|
129 is(actualPlacements.length, expectedPlacements.length, |
|
130 "Area " + areaId + " should have " + expectedPlacements.length + " items."); |
|
131 let minItems = Math.min(expectedPlacements.length, actualPlacements.length); |
|
132 for (let i = 0; i < minItems; i++) { |
|
133 if (typeof expectedPlacements[i] == "string") { |
|
134 is(actualPlacements[i], expectedPlacements[i], |
|
135 "Item " + i + " in " + areaId + " should match expectations."); |
|
136 } else if (expectedPlacements[i] instanceof RegExp) { |
|
137 ok(expectedPlacements[i].test(actualPlacements[i]), |
|
138 "Item " + i + " (" + actualPlacements[i] + ") in " + |
|
139 areaId + " should match " + expectedPlacements[i]); |
|
140 } else { |
|
141 ok(false, "Unknown type of expected placement passed to " + |
|
142 " assertAreaPlacements. Is your test broken?"); |
|
143 } |
|
144 } |
|
145 } |
|
146 |
|
147 function todoAssertAreaPlacements(areaId, expectedPlacements) { |
|
148 let actualPlacements = getAreaWidgetIds(areaId); |
|
149 let isPassing = actualPlacements.length == expectedPlacements.length; |
|
150 let minItems = Math.min(expectedPlacements.length, actualPlacements.length); |
|
151 for (let i = 0; i < minItems; i++) { |
|
152 if (typeof expectedPlacements[i] == "string") { |
|
153 isPassing = isPassing && actualPlacements[i] == expectedPlacements[i]; |
|
154 } else if (expectedPlacements[i] instanceof RegExp) { |
|
155 isPassing = isPassing && expectedPlacements[i].test(actualPlacements[i]); |
|
156 } else { |
|
157 ok(false, "Unknown type of expected placement passed to " + |
|
158 " assertAreaPlacements. Is your test broken?"); |
|
159 } |
|
160 } |
|
161 todo(isPassing, "The area placements for " + areaId + |
|
162 " should equal the expected placements."); |
|
163 } |
|
164 |
|
165 function getAreaWidgetIds(areaId) { |
|
166 return CustomizableUI.getWidgetIdsInArea(areaId); |
|
167 } |
|
168 |
|
169 function simulateItemDrag(toDrag, target) { |
|
170 let docId = toDrag.ownerDocument.documentElement.id; |
|
171 let dragData = [[{type: 'text/toolbarwrapper-id/' + docId, |
|
172 data: toDrag.id}]]; |
|
173 synthesizeDragStart(toDrag.parentNode, dragData); |
|
174 synthesizeDrop(target, target, dragData); |
|
175 } |
|
176 |
|
177 function endCustomizing(aWindow=window) { |
|
178 if (aWindow.document.documentElement.getAttribute("customizing") != "true") { |
|
179 return true; |
|
180 } |
|
181 Services.prefs.setBoolPref("browser.uiCustomization.disableAnimation", true); |
|
182 let deferredEndCustomizing = Promise.defer(); |
|
183 function onCustomizationEnds() { |
|
184 Services.prefs.setBoolPref("browser.uiCustomization.disableAnimation", false); |
|
185 aWindow.gNavToolbox.removeEventListener("aftercustomization", onCustomizationEnds); |
|
186 deferredEndCustomizing.resolve(); |
|
187 } |
|
188 aWindow.gNavToolbox.addEventListener("aftercustomization", onCustomizationEnds); |
|
189 aWindow.gCustomizeMode.exit(); |
|
190 |
|
191 return deferredEndCustomizing.promise.then(function() { |
|
192 let deferredLoadNewTab = Promise.defer(); |
|
193 |
|
194 //XXXgijs so some tests depend on this tab being about:blank. Make it so. |
|
195 let newTabBrowser = aWindow.gBrowser.selectedBrowser; |
|
196 newTabBrowser.stop(); |
|
197 |
|
198 // If we stop early enough, this might actually be about:blank. |
|
199 if (newTabBrowser.contentDocument.location.href == "about:blank") { |
|
200 return; |
|
201 } |
|
202 |
|
203 // Otherwise, make it be about:blank, and wait for that to be done. |
|
204 function onNewTabLoaded(e) { |
|
205 newTabBrowser.removeEventListener("load", onNewTabLoaded, true); |
|
206 deferredLoadNewTab.resolve(); |
|
207 } |
|
208 newTabBrowser.addEventListener("load", onNewTabLoaded, true); |
|
209 newTabBrowser.contentDocument.location.replace("about:blank"); |
|
210 return deferredLoadNewTab.promise; |
|
211 }); |
|
212 } |
|
213 |
|
214 function startCustomizing(aWindow=window) { |
|
215 if (aWindow.document.documentElement.getAttribute("customizing") == "true") { |
|
216 return; |
|
217 } |
|
218 Services.prefs.setBoolPref("browser.uiCustomization.disableAnimation", true); |
|
219 let deferred = Promise.defer(); |
|
220 function onCustomizing() { |
|
221 aWindow.gNavToolbox.removeEventListener("customizationready", onCustomizing); |
|
222 Services.prefs.setBoolPref("browser.uiCustomization.disableAnimation", false); |
|
223 deferred.resolve(); |
|
224 } |
|
225 aWindow.gNavToolbox.addEventListener("customizationready", onCustomizing); |
|
226 aWindow.gCustomizeMode.enter(); |
|
227 return deferred.promise; |
|
228 } |
|
229 |
|
230 function openAndLoadWindow(aOptions, aWaitForDelayedStartup=false) { |
|
231 let deferred = Promise.defer(); |
|
232 let win = OpenBrowserWindow(aOptions); |
|
233 if (aWaitForDelayedStartup) { |
|
234 Services.obs.addObserver(function onDS(aSubject, aTopic, aData) { |
|
235 if (aSubject != win) { |
|
236 return; |
|
237 } |
|
238 Services.obs.removeObserver(onDS, "browser-delayed-startup-finished"); |
|
239 deferred.resolve(win); |
|
240 }, "browser-delayed-startup-finished", false); |
|
241 |
|
242 } else { |
|
243 win.addEventListener("load", function onLoad() { |
|
244 win.removeEventListener("load", onLoad); |
|
245 deferred.resolve(win); |
|
246 }); |
|
247 } |
|
248 return deferred.promise; |
|
249 } |
|
250 |
|
251 function promiseWindowClosed(win) { |
|
252 let deferred = Promise.defer(); |
|
253 win.addEventListener("unload", function onunload() { |
|
254 win.removeEventListener("unload", onunload); |
|
255 deferred.resolve(); |
|
256 }); |
|
257 win.close(); |
|
258 return deferred.promise; |
|
259 } |
|
260 |
|
261 function promisePanelShown(win) { |
|
262 let panelEl = win.PanelUI.panel; |
|
263 return promisePanelElementShown(win, panelEl); |
|
264 } |
|
265 |
|
266 function promiseOverflowShown(win) { |
|
267 let panelEl = win.document.getElementById("widget-overflow"); |
|
268 return promisePanelElementShown(win, panelEl); |
|
269 } |
|
270 |
|
271 function promisePanelElementShown(win, aPanel) { |
|
272 let deferred = Promise.defer(); |
|
273 let timeoutId = win.setTimeout(() => { |
|
274 deferred.reject("Panel did not show within 20 seconds."); |
|
275 }, 20000); |
|
276 function onPanelOpen(e) { |
|
277 aPanel.removeEventListener("popupshown", onPanelOpen); |
|
278 win.clearTimeout(timeoutId); |
|
279 deferred.resolve(); |
|
280 }; |
|
281 aPanel.addEventListener("popupshown", onPanelOpen); |
|
282 return deferred.promise; |
|
283 } |
|
284 |
|
285 function promisePanelHidden(win) { |
|
286 let panelEl = win.PanelUI.panel; |
|
287 return promisePanelElementHidden(win, panelEl); |
|
288 } |
|
289 |
|
290 function promiseOverflowHidden(win) { |
|
291 let panelEl = document.getElementById("widget-overflow"); |
|
292 return promisePanelElementHidden(win, panelEl); |
|
293 } |
|
294 |
|
295 function promisePanelElementHidden(win, aPanel) { |
|
296 let deferred = Promise.defer(); |
|
297 let timeoutId = win.setTimeout(() => { |
|
298 deferred.reject("Panel did not hide within 20 seconds."); |
|
299 }, 20000); |
|
300 function onPanelClose(e) { |
|
301 aPanel.removeEventListener("popuphidden", onPanelClose); |
|
302 win.clearTimeout(timeoutId); |
|
303 deferred.resolve(); |
|
304 } |
|
305 aPanel.addEventListener("popuphidden", onPanelClose); |
|
306 return deferred.promise; |
|
307 } |
|
308 |
|
309 function isPanelUIOpen() { |
|
310 return PanelUI.panel.state == "open" || PanelUI.panel.state == "showing"; |
|
311 } |
|
312 |
|
313 function subviewShown(aSubview) { |
|
314 let deferred = Promise.defer(); |
|
315 let win = aSubview.ownerDocument.defaultView; |
|
316 let timeoutId = win.setTimeout(() => { |
|
317 deferred.reject("Subview (" + aSubview.id + ") did not show within 20 seconds."); |
|
318 }, 20000); |
|
319 function onViewShowing(e) { |
|
320 aSubview.removeEventListener("ViewShowing", onViewShowing); |
|
321 win.clearTimeout(timeoutId); |
|
322 deferred.resolve(); |
|
323 }; |
|
324 aSubview.addEventListener("ViewShowing", onViewShowing); |
|
325 return deferred.promise; |
|
326 } |
|
327 |
|
328 function subviewHidden(aSubview) { |
|
329 let deferred = Promise.defer(); |
|
330 let win = aSubview.ownerDocument.defaultView; |
|
331 let timeoutId = win.setTimeout(() => { |
|
332 deferred.reject("Subview (" + aSubview.id + ") did not hide within 20 seconds."); |
|
333 }, 20000); |
|
334 function onViewHiding(e) { |
|
335 aSubview.removeEventListener("ViewHiding", onViewHiding); |
|
336 win.clearTimeout(timeoutId); |
|
337 deferred.resolve(); |
|
338 }; |
|
339 aSubview.addEventListener("ViewHiding", onViewHiding); |
|
340 return deferred.promise; |
|
341 } |
|
342 |
|
343 function waitForCondition(aConditionFn, aMaxTries=50, aCheckInterval=100) { |
|
344 function tryNow() { |
|
345 tries++; |
|
346 if (aConditionFn()) { |
|
347 deferred.resolve(); |
|
348 } else if (tries < aMaxTries) { |
|
349 tryAgain(); |
|
350 } else { |
|
351 deferred.reject("Condition timed out: " + aConditionFn.toSource()); |
|
352 } |
|
353 } |
|
354 function tryAgain() { |
|
355 setTimeout(tryNow, aCheckInterval); |
|
356 } |
|
357 let deferred = Promise.defer(); |
|
358 let tries = 0; |
|
359 tryAgain(); |
|
360 return deferred.promise; |
|
361 } |
|
362 |
|
363 function waitFor(aTimeout=100) { |
|
364 let deferred = Promise.defer(); |
|
365 setTimeout(function() deferred.resolve(), aTimeout); |
|
366 return deferred.promise; |
|
367 } |
|
368 |
|
369 /** |
|
370 * Starts a load in an existing tab and waits for it to finish (via some event). |
|
371 * |
|
372 * @param aTab The tab to load into. |
|
373 * @param aUrl The url to load. |
|
374 * @param aEventType The load event type to wait for. Defaults to "load". |
|
375 * @return {Promise} resolved when the event is handled. |
|
376 */ |
|
377 function promiseTabLoadEvent(aTab, aURL, aEventType="load") { |
|
378 let deferred = Promise.defer(); |
|
379 info("Wait for tab event: " + aEventType); |
|
380 |
|
381 let timeoutId = setTimeout(() => { |
|
382 aTab.linkedBrowser.removeEventListener(aEventType, onTabLoad, true); |
|
383 deferred.reject("TabSelect did not happen within " + kTabEventFailureTimeoutInMs + "ms"); |
|
384 }, kTabEventFailureTimeoutInMs); |
|
385 |
|
386 function onTabLoad(event) { |
|
387 if (event.originalTarget != aTab.linkedBrowser.contentDocument || |
|
388 event.target.location.href == "about:blank") { |
|
389 info("skipping spurious load event"); |
|
390 return; |
|
391 } |
|
392 clearTimeout(timeoutId); |
|
393 aTab.linkedBrowser.removeEventListener(aEventType, onTabLoad, true); |
|
394 info("Tab event received: " + aEventType); |
|
395 deferred.resolve(); |
|
396 } |
|
397 aTab.linkedBrowser.addEventListener(aEventType, onTabLoad, true, true); |
|
398 aTab.linkedBrowser.loadURI(aURL); |
|
399 return deferred.promise; |
|
400 } |
|
401 |
|
402 /** |
|
403 * Navigate back or forward in tab history and wait for it to finish. |
|
404 * |
|
405 * @param aDirection Number to indicate to move backward or forward in history. |
|
406 * @param aConditionFn Function that returns the result of an evaluated condition |
|
407 * that needs to be `true` to resolve the promise. |
|
408 * @return {Promise} resolved when navigation has finished. |
|
409 */ |
|
410 function promiseTabHistoryNavigation(aDirection = -1, aConditionFn) { |
|
411 let deferred = Promise.defer(); |
|
412 |
|
413 let timeoutId = setTimeout(() => { |
|
414 gBrowser.removeEventListener("pageshow", listener, true); |
|
415 deferred.reject("Pageshow did not happen within " + kTabEventFailureTimeoutInMs + "ms"); |
|
416 }, kTabEventFailureTimeoutInMs); |
|
417 |
|
418 function listener(event) { |
|
419 gBrowser.removeEventListener("pageshow", listener, true); |
|
420 clearTimeout(timeoutId); |
|
421 |
|
422 if (aConditionFn) { |
|
423 waitForCondition(aConditionFn).then(() => deferred.resolve(), |
|
424 aReason => deferred.reject(aReason)); |
|
425 } else { |
|
426 deferred.resolve(); |
|
427 } |
|
428 } |
|
429 gBrowser.addEventListener("pageshow", listener, true); |
|
430 |
|
431 content.history.go(aDirection); |
|
432 |
|
433 return deferred.promise; |
|
434 } |
|
435 |
|
436 function popupShown(aPopup) { |
|
437 return promisePopupEvent(aPopup, "shown"); |
|
438 } |
|
439 |
|
440 function popupHidden(aPopup) { |
|
441 return promisePopupEvent(aPopup, "hidden"); |
|
442 } |
|
443 |
|
444 /** |
|
445 * Returns a Promise that resolves when aPopup fires an event of type |
|
446 * aEventType. Times out and rejects after 20 seconds. |
|
447 * |
|
448 * @param aPopup the popup to monitor for events. |
|
449 * @param aEventSuffix the _suffix_ for the popup event type to watch for. |
|
450 * |
|
451 * Example usage: |
|
452 * let popupShownPromise = promisePopupEvent(somePopup, "shown"); |
|
453 * // ... something that opens a popup |
|
454 * yield popupShownPromise; |
|
455 * |
|
456 * let popupHiddenPromise = promisePopupEvent(somePopup, "hidden"); |
|
457 * // ... something that hides a popup |
|
458 * yield popupHiddenPromise; |
|
459 */ |
|
460 function promisePopupEvent(aPopup, aEventSuffix) { |
|
461 let deferred = Promise.defer(); |
|
462 let win = aPopup.ownerDocument.defaultView; |
|
463 let eventType = "popup" + aEventSuffix; |
|
464 |
|
465 let timeoutId = win.setTimeout(() => { |
|
466 deferred.reject("Context menu (" + aPopup.id + ") did not fire " |
|
467 + eventType + " within 20 seconds."); |
|
468 }, 20000); |
|
469 |
|
470 function onPopupEvent(e) { |
|
471 win.clearTimeout(timeoutId); |
|
472 aPopup.removeEventListener(eventType, onPopupEvent); |
|
473 deferred.resolve(); |
|
474 }; |
|
475 |
|
476 aPopup.addEventListener(eventType, onPopupEvent); |
|
477 return deferred.promise; |
|
478 } |
|
479 |
|
480 // This is a simpler version of the context menu check that |
|
481 // exists in contextmenu_common.js. |
|
482 function checkContextMenu(aContextMenu, aExpectedEntries, aWindow=window) { |
|
483 let childNodes = aContextMenu.childNodes; |
|
484 for (let i = 0; i < childNodes.length; i++) { |
|
485 let menuitem = childNodes[i]; |
|
486 try { |
|
487 if (aExpectedEntries[i][0] == "---") { |
|
488 is(menuitem.localName, "menuseparator", "menuseparator expected"); |
|
489 continue; |
|
490 } |
|
491 |
|
492 let selector = aExpectedEntries[i][0]; |
|
493 ok(menuitem.mozMatchesSelector(selector), "menuitem should match " + selector + " selector"); |
|
494 let commandValue = menuitem.getAttribute("command"); |
|
495 let relatedCommand = commandValue ? aWindow.document.getElementById(commandValue) : null; |
|
496 let menuItemDisabled = relatedCommand ? |
|
497 relatedCommand.getAttribute("disabled") == "true" : |
|
498 menuitem.getAttribute("disabled") == "true"; |
|
499 is(menuItemDisabled, !aExpectedEntries[i][1], "disabled state for " + selector); |
|
500 } catch (e) { |
|
501 ok(false, "Exception when checking context menu: " + e); |
|
502 } |
|
503 } |
|
504 } |