head.js (14020B)
1 /* Any copyright is dedicated to the Public Domain. 2 * http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 // shared-head.js handles imports, constants, and utility functions 5 Services.scriptloader.loadSubScript( 6 "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js", 7 this 8 ); 9 Services.scriptloader.loadSubScript( 10 "chrome://mochitests/content/browser/devtools/client/inspector/test/shared-head.js", 11 this 12 ); 13 14 const EventEmitter = require("resource://devtools/shared/event-emitter.js"); 15 16 /** 17 * Retrieve all tool ids compatible with a target created for the provided tab. 18 * 19 * @param {XULTab} tab 20 * The tab for which we want to get the list of supported toolIds 21 * @return {Array<string>} array of tool ids 22 */ 23 async function getSupportedToolIds(tab) { 24 info("Getting the entire list of tools supported in this tab"); 25 26 let shouldDestroyToolbox = false; 27 28 // Get the toolbox for this tab, or create one if needed. 29 let toolbox = gDevTools.getToolboxForTab(tab); 30 if (!toolbox) { 31 toolbox = await gDevTools.showToolboxForTab(tab); 32 shouldDestroyToolbox = true; 33 } 34 35 const toolIds = gDevTools 36 .getToolDefinitionArray() 37 .filter(def => def.isToolSupported(toolbox)) 38 .map(def => def.id); 39 40 if (shouldDestroyToolbox) { 41 // Only close the toolbox if it was explicitly created here. 42 await toolbox.destroy(); 43 } 44 45 return toolIds; 46 } 47 48 function toggleAllTools(state) { 49 for (const [, tool] of gDevTools._tools) { 50 if (!tool.visibilityswitch) { 51 continue; 52 } 53 if (state) { 54 Services.prefs.setBoolPref(tool.visibilityswitch, true); 55 } else { 56 Services.prefs.clearUserPref(tool.visibilityswitch); 57 } 58 } 59 } 60 61 async function getParentProcessActors(callback) { 62 const commands = await CommandsFactory.forMainProcess(); 63 const mainProcessTargetFront = await commands.descriptorFront.getTarget(); 64 65 callback(commands.client, mainProcessTargetFront); 66 } 67 68 function getSourceActor(sources, url) { 69 const item = sources.getItemForAttachment(a => a.source.url === url); 70 return item && item.value; 71 } 72 73 /** 74 * Synthesize a keypress from a <key> element, taking into account 75 * any modifiers. 76 * 77 * @param {Element} el the <key> element to synthesize 78 */ 79 function synthesizeKeyElement(el) { 80 const key = el.getAttribute("key") || el.getAttribute("keycode"); 81 const mod = {}; 82 el.getAttribute("modifiers") 83 .split(" ") 84 .forEach(m => (mod[m + "Key"] = true)); 85 info(`Synthesizing: key=${key}, mod=${JSON.stringify(mod)}`); 86 EventUtils.synthesizeKey(key, mod, el.ownerDocument.defaultView); 87 } 88 89 /** 90 * Check the toolbox host type and prefs to make sure they match the 91 * expected values 92 * 93 * @param {Toolbox} 94 * @param {HostType} hostType 95 * One of {SIDE, BOTTOM, WINDOW} from Toolbox.HostType 96 * @param {HostType} Optional previousHostType 97 * The host that will be switched to when calling switchToPreviousHost 98 */ 99 function checkHostType(toolbox, hostType, previousHostType) { 100 is(toolbox.hostType, hostType, "host type is " + hostType); 101 102 const pref = Services.prefs.getCharPref("devtools.toolbox.host"); 103 is(pref, hostType, "host pref is " + hostType); 104 105 if (previousHostType) { 106 is( 107 Services.prefs.getCharPref("devtools.toolbox.previousHost"), 108 previousHostType, 109 "The previous host is correct" 110 ); 111 } 112 } 113 114 /** 115 * Create a new <script> referencing URL. Return a promise that 116 * resolves when this has happened 117 * 118 * @param {string} url 119 * the url 120 * @return {Promise} a promise that resolves when the element has been created 121 */ 122 function createScript(url) { 123 info(`Creating script: ${url}`); 124 // This is not ideal if called multiple times, as it loads the frame script 125 // separately each time. See bug 1443680. 126 return SpecialPowers.spawn(gBrowser.selectedBrowser, [url], urlChild => { 127 const script = content.document.createElement("script"); 128 script.setAttribute("src", urlChild); 129 content.document.body.appendChild(script); 130 }); 131 } 132 133 /** 134 * Wait for the toolbox to notice that a given source is loaded 135 * 136 * @param {Toolbox} toolbox 137 * @param {string} url 138 * the url to wait for 139 * @return {Promise} a promise that is resolved when the source is loaded 140 */ 141 function waitForSourceLoad(toolbox, url) { 142 info(`Waiting for source ${url} to be available...`); 143 return new Promise(resolve => { 144 const { resourceCommand } = toolbox.commands; 145 146 function onAvailable(sources) { 147 for (const source of sources) { 148 if (source.url === url) { 149 resourceCommand.unwatchResources([resourceCommand.TYPES.SOURCE], { 150 onAvailable, 151 }); 152 resolve(); 153 } 154 } 155 } 156 resourceCommand.watchResources([resourceCommand.TYPES.SOURCE], { 157 onAvailable, 158 // Ignore the cached resources as we always listen *before* 159 // the action creating a source. 160 ignoreExistingResources: true, 161 }); 162 }); 163 } 164 165 /** 166 * When a Toolbox is started it creates a DevToolPanel for each of the tools 167 * by calling toolDefinition.build(). The returned object should 168 * at least implement these functions. They will be used by the ToolBox. 169 * 170 * There may be no benefit in doing this as an abstract type, but if nothing 171 * else gives us a place to write documentation. 172 */ 173 class DevToolPanel extends EventEmitter { 174 constructor(iframeWindow, toolbox) { 175 super(); 176 177 this._toolbox = toolbox; 178 this._window = iframeWindow; 179 } 180 open() { 181 return new Promise(resolve => { 182 executeSoon(() => { 183 resolve(this); 184 }); 185 }); 186 } 187 188 get document() { 189 return this._window.document; 190 } 191 192 get target() { 193 return this._toolbox.target; 194 } 195 196 get toolbox() { 197 return this._toolbox; 198 } 199 200 destroy() { 201 return Promise.resolve(null); 202 } 203 } 204 205 /** 206 * Create a simple devtools test panel that implements the minimum API needed to be 207 * registered and opened in the toolbox. 208 */ 209 function createTestPanel(iframeWindow, toolbox) { 210 return new DevToolPanel(iframeWindow, toolbox); 211 } 212 213 async function openChevronMenu(toolbox) { 214 const chevronMenuButton = toolbox.doc.querySelector(".tools-chevron-menu"); 215 EventUtils.synthesizeMouseAtCenter(chevronMenuButton, {}, toolbox.win); 216 217 const menuPopup = toolbox.doc.getElementById( 218 "tools-chevron-menu-button-panel" 219 ); 220 ok(menuPopup, "tools-chevron-menupopup is available"); 221 222 info("Waiting for the menu popup to be displayed"); 223 await waitUntil(() => menuPopup.classList.contains("tooltip-visible")); 224 } 225 226 async function closeChevronMenu(toolbox) { 227 // In order to close the popup menu with escape key, set the focus to the chevron 228 // button at first. 229 const chevronMenuButton = toolbox.doc.querySelector(".tools-chevron-menu"); 230 chevronMenuButton.focus(); 231 232 EventUtils.sendKey("ESCAPE", toolbox.doc.defaultView); 233 const menuPopup = toolbox.doc.getElementById( 234 "tools-chevron-menu-button-panel" 235 ); 236 237 info("Closing the chevron popup menu"); 238 await waitUntil(() => !menuPopup.classList.contains("tooltip-visible")); 239 } 240 241 function prepareToolTabReorderTest(toolbox, startingOrder) { 242 Services.prefs.setCharPref( 243 "devtools.toolbox.tabsOrder", 244 startingOrder.join(",") 245 ); 246 ok( 247 !toolbox.doc.getElementById("tools-chevron-menu-button"), 248 "The size of the screen being too small" 249 ); 250 251 for (const id of startingOrder) { 252 ok(getElementByToolId(toolbox, id), `Tab element should exist for ${id}`); 253 } 254 } 255 256 async function dndToolTab(toolbox, dragTarget, dropTarget, passedTargets = []) { 257 info(`Drag ${dragTarget} to ${dropTarget}`); 258 const dragTargetEl = getElementByToolIdOrExtensionIdOrSelector( 259 toolbox, 260 dragTarget 261 ); 262 263 const onReady = dragTargetEl.classList.contains("selected") 264 ? Promise.resolve() 265 : toolbox.once("select"); 266 EventUtils.synthesizeMouseAtCenter( 267 dragTargetEl, 268 { type: "mousedown" }, 269 dragTargetEl.ownerGlobal 270 ); 271 await onReady; 272 273 for (const passedTarget of passedTargets) { 274 info(`Via ${passedTarget}`); 275 const passedTargetEl = getElementByToolIdOrExtensionIdOrSelector( 276 toolbox, 277 passedTarget 278 ); 279 EventUtils.synthesizeMouseAtCenter( 280 passedTargetEl, 281 { type: "mousemove" }, 282 passedTargetEl.ownerGlobal 283 ); 284 } 285 286 if (dropTarget) { 287 const dropTargetEl = getElementByToolIdOrExtensionIdOrSelector( 288 toolbox, 289 dropTarget 290 ); 291 EventUtils.synthesizeMouseAtCenter( 292 dropTargetEl, 293 { type: "mousemove" }, 294 dropTargetEl.ownerGlobal 295 ); 296 EventUtils.synthesizeMouseAtCenter( 297 dropTargetEl, 298 { type: "mouseup" }, 299 dropTargetEl.ownerGlobal 300 ); 301 } else { 302 const containerEl = toolbox.doc.getElementById("toolbox-container"); 303 EventUtils.synthesizeMouse( 304 containerEl, 305 0, 306 0, 307 { type: "mouseout" }, 308 containerEl.ownerGlobal 309 ); 310 } 311 312 // Wait for updating the preference. 313 await new Promise(resolve => { 314 const onUpdated = () => { 315 Services.prefs.removeObserver("devtools.toolbox.tabsOrder", onUpdated); 316 resolve(); 317 }; 318 319 Services.prefs.addObserver("devtools.toolbox.tabsOrder", onUpdated); 320 }); 321 } 322 323 function assertToolTabOrder(toolbox, expectedOrder) { 324 info("Check the order of the tabs on the toolbar"); 325 326 const tabEls = toolbox.doc.querySelectorAll(".devtools-tab"); 327 328 for (let i = 0; i < expectedOrder.length; i++) { 329 const isOrdered = 330 tabEls[i].dataset.id === expectedOrder[i] || 331 tabEls[i].dataset.extensionId === expectedOrder[i]; 332 ok(isOrdered, `The tab[${expectedOrder[i]}] should exist at [${i}]`); 333 } 334 } 335 336 function assertToolTabSelected(toolbox, dragTarget) { 337 info("Check whether the drag target was selected"); 338 const dragTargetEl = getElementByToolIdOrExtensionIdOrSelector( 339 toolbox, 340 dragTarget 341 ); 342 ok( 343 dragTargetEl.classList.contains("selected"), 344 "The dragged tool should be selected" 345 ); 346 } 347 348 function assertToolTabPreferenceOrder(expectedOrder) { 349 info("Check the order in DevTools preference for tabs order"); 350 is( 351 Services.prefs.getCharPref("devtools.toolbox.tabsOrder"), 352 expectedOrder.join(","), 353 "The preference should be correct" 354 ); 355 } 356 357 function getElementByToolId(toolbox, id) { 358 for (const tabEl of toolbox.doc.querySelectorAll(".devtools-tab")) { 359 if (tabEl.dataset.id === id || tabEl.dataset.extensionId === id) { 360 return tabEl; 361 } 362 } 363 364 return null; 365 } 366 367 function getElementByToolIdOrExtensionIdOrSelector(toolbox, idOrSelector) { 368 const tabEl = getElementByToolId(toolbox, idOrSelector); 369 return tabEl ? tabEl : toolbox.doc.querySelector(idOrSelector); 370 } 371 372 /** 373 * Returns a toolbox tab element, even if it's overflowed 374 */ 375 function getToolboxTab(doc, toolId) { 376 return ( 377 doc.getElementById(`toolbox-tab-${toolId}`) || 378 doc.getElementById(`tools-chevron-menupopup-${toolId}`) 379 ); 380 } 381 382 function getWindow(toolbox) { 383 return toolbox.topWindow; 384 } 385 386 async function resizeWindow(toolbox, width, height) { 387 const hostWindow = toolbox.win.parent; 388 const originalWidth = hostWindow.outerWidth; 389 const originalHeight = hostWindow.outerHeight; 390 const toWidth = width || originalWidth; 391 const toHeight = height || originalHeight; 392 393 const onResize = once(hostWindow, "resize"); 394 hostWindow.resizeTo(toWidth, toHeight); 395 await onResize; 396 } 397 398 function assertSelectedLocationInDebugger(debuggerPanel, line, column) { 399 const location = debuggerPanel._selectors.getSelectedLocation( 400 debuggerPanel._getState() 401 ); 402 is(location.line, line); 403 is(location.column, column); 404 } 405 406 /** 407 * Open a new tab on about:devtools-toolbox with the provided params object used as 408 * queryString. 409 */ 410 async function openAboutToolbox(params) { 411 info("Open about:devtools-toolbox"); 412 const querystring = new URLSearchParams(); 413 Object.keys(params).forEach(x => querystring.append(x, params[x])); 414 415 const tab = await addTab(`about:devtools-toolbox?${querystring}`); 416 const browser = tab.linkedBrowser; 417 418 return { 419 tab, 420 document: browser.contentDocument, 421 }; 422 } 423 424 /** 425 * Load FTL. 426 * 427 * @param {Toolbox} toolbox 428 * Toolbox instance. 429 * @param {string} path 430 * Path to the FTL file. 431 */ 432 function loadFTL(toolbox, path) { 433 const win = toolbox.doc.ownerGlobal; 434 435 if (win.MozXULElement) { 436 win.MozXULElement.insertFTLIfNeeded(path); 437 } 438 } 439 440 /** 441 * Emit a reload key shortcut from a given toolbox, and wait for the reload to 442 * be completed. 443 * 444 * @param {string} shortcut 445 * The key shortcut to send, as expected by the devtools shortcuts 446 * helpers (eg. "CmdOrCtrl+F5"). 447 * @param {Toolbox} toolbox 448 * The toolbox through which the event should be emitted. 449 */ 450 async function sendToolboxReloadShortcut(shortcut, toolbox) { 451 const promises = []; 452 453 // If we have a jsdebugger panel, wait for it to complete its reload. 454 const jsdebugger = toolbox.getPanel("jsdebugger"); 455 if (jsdebugger) { 456 promises.push(jsdebugger.once("reloaded")); 457 } 458 459 // If we have an inspector panel, wait for it to complete its reload. 460 const inspector = toolbox.getPanel("inspector"); 461 if (inspector) { 462 promises.push( 463 inspector.once("reloaded"), 464 inspector.once("inspector-updated") 465 ); 466 } 467 468 const loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); 469 promises.push(loadPromise); 470 471 info("Focus the toolbox window and emit the reload shortcut: " + shortcut); 472 toolbox.win.focus(); 473 synthesizeKeyShortcut(shortcut, toolbox.win); 474 475 info("Wait for page and toolbox reload promises"); 476 await Promise.all(promises); 477 } 478 479 function getErrorIcon(toolbox) { 480 return toolbox.doc.querySelector(".toolbox-error"); 481 } 482 483 function getErrorIconCount(toolbox) { 484 const textContent = getErrorIcon(toolbox)?.textContent; 485 try { 486 const int = parseInt(textContent, 10); 487 // 99+ parses to 99, so we check if the parsedInt does not match the textContent. 488 return int.toString() === textContent ? int : textContent; 489 } catch (e) { 490 // In case the parseInt threw, return the actual textContent so the test can display 491 // an easy to debug failure. 492 return textContent; 493 } 494 }