head.js (18320B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 // Load the shared-head file first. 7 Services.scriptloader.loadSubScript( 8 "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js", 9 this 10 ); 11 12 /* import-globals-from helper-mocks.js */ 13 Services.scriptloader.loadSubScript(CHROME_URL_ROOT + "helper-mocks.js", this); 14 15 Services.scriptloader.loadSubScript( 16 "chrome://mochitests/content/browser/devtools/client/webconsole/test/browser/shared-head.js", 17 this 18 ); 19 20 // Make sure the ADB addon is removed and ADB is stopped when the test ends. 21 registerCleanupFunction(async function () { 22 try { 23 const { 24 adbAddon, 25 } = require("resource://devtools/client/shared/remote-debugging/adb/adb-addon.js"); 26 await adbAddon.uninstall(); 27 } catch (e) { 28 // Will throw if the addon is already uninstalled, ignore exceptions here. 29 } 30 const { 31 adbProcess, 32 } = require("resource://devtools/client/shared/remote-debugging/adb/adb-process.js"); 33 await adbProcess.kill(); 34 35 const { 36 remoteClientManager, 37 } = require("resource://devtools/client/shared/remote-debugging/remote-client-manager.js"); 38 await remoteClientManager.removeAllClients(); 39 }); 40 41 async function openAboutDebugging({ 42 enableWorkerUpdates, 43 enableLocalTabs = true, 44 } = {}) { 45 if (!enableWorkerUpdates) { 46 silenceWorkerUpdates(); 47 } 48 49 // This preference changes value depending on the build type, tests need to use a 50 // consistent value regarless of the build used. 51 await pushPref( 52 "devtools.aboutdebugging.local-tab-debugging", 53 enableLocalTabs 54 ); 55 56 info("opening about:debugging"); 57 58 const tab = await addTab("about:debugging"); 59 const browser = tab.linkedBrowser; 60 const document = browser.contentDocument; 61 const window = browser.contentWindow; 62 63 info("Wait until Connect page is displayed"); 64 await waitUntil(() => document.querySelector(".qa-connect-page")); 65 66 return { tab, document, window }; 67 } 68 69 async function openAboutDevtoolsToolbox( 70 doc, 71 tab, 72 win, 73 targetText = "about:debugging", 74 shouldWaitToolboxReady = true 75 ) { 76 info("Open about:devtools-toolbox page"); 77 78 info("Wait for the target to appear: " + targetText); 79 await waitUntil(() => findDebugTargetByText(targetText, doc)); 80 81 const target = findDebugTargetByText(targetText, doc); 82 ok(target, `${targetText} target appeared`); 83 84 const { 85 DEBUG_TARGETS, 86 } = require("resource://devtools/client/aboutdebugging/src/constants.js"); 87 const isWebExtension = target.dataset.qaTargetType == DEBUG_TARGETS.EXTENSION; 88 89 const inspectButton = target.querySelector(".qa-debug-target-inspect-button"); 90 ok(inspectButton, `Inspect button for ${targetText} appeared`); 91 inspectButton.click(); 92 const onToolboxReady = gDevTools.once("toolbox-ready"); 93 94 info("Wait for about debugging requests to settle"); 95 await waitForAboutDebuggingRequests(win.AboutDebugging.store); 96 97 if (shouldWaitToolboxReady) { 98 info("Wait for onToolboxReady"); 99 await onToolboxReady; 100 } 101 102 const { runtimes } = win.AboutDebugging.store.getState(); 103 const isOnThisFirefox = runtimes.selectedRuntimeId === "this-firefox"; 104 const isLocalWebExtension = isWebExtension && isOnThisFirefox; 105 106 // Local WebExtension toolboxes open in a dedicated window 107 if (isLocalWebExtension) { 108 const toolbox = await onToolboxReady; 109 // For some reason the test helpers prevents the toolbox from being automatically focused on opening, 110 // whereas it is IRL. 111 const focusedWin = Services.focus.focusedWindow; 112 if (focusedWin?.top != toolbox.win) { 113 info("Wait for the toolbox window to be focused"); 114 await new Promise(r => { 115 // focus event only fired on the chrome event handler and in capture phase 116 toolbox.win.docShell.chromeEventHandler.addEventListener("focus", r, { 117 once: true, 118 capture: true, 119 }); 120 toolbox.win.focus(); 121 }); 122 info("The toolbox is focused"); 123 } 124 return { 125 devtoolsBrowser: null, 126 devtoolsDocument: toolbox.doc, 127 devtoolsTab: null, 128 devtoolsWindow: toolbox.win, 129 }; 130 } 131 132 info("Wait until a new tab is opened"); 133 await waitUntil(() => tab.nextElementSibling); 134 135 info("Wait for about:devtools-toolbox tab will be selected"); 136 const devtoolsTab = tab.nextElementSibling; 137 await waitUntil(() => gBrowser.selectedTab === devtoolsTab); 138 const devtoolsBrowser = gBrowser.selectedBrowser; 139 info("Wait for about:devtools-toolbox tab to have the expected URL"); 140 await waitUntil(() => 141 devtoolsBrowser.contentWindow.location.href.startsWith( 142 "about:devtools-toolbox?" 143 ) 144 ); 145 146 if (!shouldWaitToolboxReady) { 147 // Wait for show error page. 148 await waitUntil(() => 149 devtoolsBrowser.contentDocument.querySelector(".qa-error-page") 150 ); 151 } 152 153 return { 154 devtoolsBrowser, 155 devtoolsDocument: devtoolsBrowser.contentDocument, 156 devtoolsTab, 157 devtoolsWindow: devtoolsBrowser.contentWindow, 158 }; 159 } 160 161 async function closeAboutDevtoolsToolbox( 162 aboutDebuggingDocument, 163 devtoolsTab, 164 win 165 ) { 166 // Wait for all requests to settle on the opened about:devtools toolbox. 167 const devtoolsBrowser = devtoolsTab.linkedBrowser; 168 const devtoolsWindow = devtoolsBrowser.contentWindow; 169 const toolbox = getToolbox(devtoolsWindow); 170 171 info("Wait for requests to settle"); 172 await toolbox.commands.client.waitForRequestsToSettle({ 173 ignoreOrphanedFronts: true, 174 }); 175 176 info("Close about:devtools-toolbox page"); 177 const onToolboxDestroyed = gDevTools.once("toolbox-destroyed"); 178 179 info("Wait for removeTab"); 180 await removeTab(devtoolsTab); 181 182 info("Wait for toolbox destroyed"); 183 await onToolboxDestroyed; 184 185 // Changing the tab will also trigger a request to list tabs, so wait until the selected 186 // tab has changed to wait for requests to settle. 187 info("Wait until aboutdebugging is selected"); 188 await waitUntil(() => gBrowser.selectedTab !== devtoolsTab); 189 190 // Wait for removing about:devtools-toolbox tab info from about:debugging. 191 info("Wait until about:devtools-toolbox is removed from debug targets"); 192 await waitUntil( 193 () => !findDebugTargetByText("Toolbox - ", aboutDebuggingDocument) 194 ); 195 196 await waitForAboutDebuggingRequests(win.AboutDebugging.store); 197 } 198 199 async function closeWebExtAboutDevtoolsToolbox(devtoolsWindow, win) { 200 // Wait for all requests to settle on the opened about:devtools toolbox. 201 const toolbox = getToolbox(devtoolsWindow); 202 await toolbox.commands.client.waitForRequestsToSettle(); 203 204 info("Close the toolbox and wait for its destruction"); 205 await toolbox.destroy(); 206 207 await waitForAboutDebuggingRequests(win.AboutDebugging.store); 208 } 209 210 async function reloadAboutDebugging(tab) { 211 info("reload about:debugging"); 212 213 await reloadBrowser(tab.linkedBrowser); 214 const browser = tab.linkedBrowser; 215 const document = browser.contentDocument; 216 const window = browser.contentWindow; 217 info("wait for the initial about:debugging requests to settle"); 218 await waitForAboutDebuggingRequests(window.AboutDebugging.store); 219 220 return document; 221 } 222 223 // Wait for all about:debugging target request actions to succeed. 224 // They will typically be triggered after watching a new runtime or loading 225 // about:debugging. 226 function waitForRequestsSuccess(store) { 227 return Promise.all([ 228 waitForDispatch(store, "REQUEST_EXTENSIONS_SUCCESS"), 229 waitForDispatch(store, "REQUEST_TABS_SUCCESS"), 230 waitForDispatch(store, "REQUEST_WORKERS_SUCCESS"), 231 ]); 232 } 233 234 /** 235 * Wait for all aboutdebugging REQUEST_*_SUCCESS actions to settle, meaning here 236 * that no new request has been dispatched after the provided delay. 237 */ 238 async function waitForAboutDebuggingRequests(store, delay = 500) { 239 let hasSettled = false; 240 241 // After each iteration of this while loop, we check is the timerPromise had the time 242 // to resolve or if we captured a REQUEST_*_SUCCESS action before. 243 while (!hasSettled) { 244 let timer; 245 246 // This timer will be executed only if no REQUEST_*_SUCCESS action is dispatched 247 // during the delay. We consider that when no request are received for some time, it 248 // means there are no ongoing requests anymore. 249 const timerPromise = new Promise(resolve => { 250 timer = setTimeout(() => { 251 hasSettled = true; 252 resolve(); 253 }, delay); 254 }); 255 256 // Wait either for a REQUEST_*_SUCCESS to be dispatched, or for the timer to resolve. 257 await Promise.race([ 258 waitForDispatch(store, "REQUEST_EXTENSIONS_SUCCESS"), 259 waitForDispatch(store, "REQUEST_TABS_SUCCESS"), 260 waitForDispatch(store, "REQUEST_WORKERS_SUCCESS"), 261 timerPromise, 262 ]); 263 264 // Clear the timer to avoid setting hasSettled to true accidently unless timerPromise 265 // was the first to resolve. 266 clearTimeout(timer); 267 } 268 } 269 270 /** 271 * Navigate to "This Firefox" 272 */ 273 async function selectThisFirefoxPage(doc, store) { 274 info("Select This Firefox page"); 275 276 const onRequestSuccess = waitForRequestsSuccess(store); 277 doc.location.hash = "#/runtime/this-firefox"; 278 info("Wait for requests to be complete"); 279 await onRequestSuccess; 280 281 info("Wait for runtime page to be rendered"); 282 await waitUntil(() => doc.querySelector(".qa-runtime-page")); 283 284 // Navigating to this-firefox will trigger a title change for the 285 // about:debugging tab. This title change _might_ trigger a tablist update. 286 // If it does, we should make sure to wait for pending tab requests. 287 await waitForAboutDebuggingRequests(store); 288 } 289 290 /** 291 * Navigate to the Connect page. Resolves when the Connect page is rendered. 292 */ 293 async function selectConnectPage(doc) { 294 const sidebarItems = doc.querySelectorAll(".qa-sidebar-item"); 295 const connectSidebarItem = [...sidebarItems].find(element => { 296 return element.textContent === "Setup"; 297 }); 298 ok(connectSidebarItem, "Sidebar contains a Connect item"); 299 const connectLink = connectSidebarItem.querySelector(".qa-sidebar-link"); 300 ok(connectLink, "Sidebar contains a Connect link"); 301 302 info("Click on the Connect link in the sidebar"); 303 connectLink.click(); 304 305 info("Wait until Connect page is displayed"); 306 await waitUntil(() => doc.querySelector(".qa-connect-page")); 307 } 308 309 function getDebugTargetPane(title, document) { 310 // removes the suffix "(<NUMBER>)" in debug target pane's title, if needed 311 const sanitizeTitle = x => { 312 return x.replace(/\s+\(\d+\)$/, ""); 313 }; 314 315 const targetTitle = sanitizeTitle(title); 316 for (const titleEl of document.querySelectorAll( 317 ".qa-debug-target-pane-title" 318 )) { 319 if (sanitizeTitle(titleEl.textContent) !== targetTitle) { 320 continue; 321 } 322 323 return titleEl.closest(".qa-debug-target-pane"); 324 } 325 326 return null; 327 } 328 329 function findDebugTargetByText(text, document) { 330 const targets = [...document.querySelectorAll(".qa-debug-target-item")]; 331 return targets.find(target => target.textContent.includes(text)); 332 } 333 334 function findSidebarItemByText(text, document) { 335 const sidebarItems = document.querySelectorAll(".qa-sidebar-item"); 336 return [...sidebarItems].find(element => { 337 return element.textContent.includes(text); 338 }); 339 } 340 341 function findSidebarItemLinkByText(text, document) { 342 const links = document.querySelectorAll(".qa-sidebar-link"); 343 return [...links].find(element => { 344 return element.textContent.includes(text); 345 }); 346 } 347 348 async function connectToRuntime(deviceName, document) { 349 info(`Wait until the sidebar item for ${deviceName} appears`); 350 await waitUntil(() => findSidebarItemByText(deviceName, document)); 351 const sidebarItem = findSidebarItemByText(deviceName, document); 352 const connectButton = sidebarItem.querySelector(".qa-connect-button"); 353 ok( 354 connectButton, 355 `Connect button is displayed for the runtime ${deviceName}` 356 ); 357 358 info("Click on the connect button and wait until it disappears"); 359 connectButton.click(); 360 await waitUntil(() => !sidebarItem.querySelector(".qa-connect-button")); 361 } 362 363 async function waitForRuntimePage(name, document) { 364 await waitUntil(() => { 365 const runtimeInfo = document.querySelector(".qa-runtime-name"); 366 return runtimeInfo && runtimeInfo.textContent.includes(name); 367 }); 368 } 369 370 async function selectRuntime(deviceName, name, document) { 371 const sidebarItem = findSidebarItemByText(deviceName, document); 372 const store = document.defaultView.AboutDebugging.store; 373 const onSelectPageSuccess = waitForDispatch(store, "SELECT_PAGE_SUCCESS"); 374 375 sidebarItem.querySelector(".qa-sidebar-link").click(); 376 377 await waitForRuntimePage(name, document); 378 379 info("Wait for SELECT_PAGE_SUCCESS to be dispatched"); 380 await onSelectPageSuccess; 381 } 382 383 function getToolbox(win) { 384 return gDevTools.getToolboxes().find(toolbox => toolbox.win === win); 385 } 386 387 /** 388 * Open the performance profiler dialog. Assumes the client is a mocked remote runtime 389 * client. 390 */ 391 async function openProfilerDialog(client, doc) { 392 const onProfilerLoaded = new Promise(r => { 393 client.loadPerformanceProfiler = r; 394 }); 395 396 info("Click on the Profile Runtime button"); 397 const profileButton = doc.querySelector(".qa-profile-runtime-button"); 398 profileButton.click(); 399 400 info( 401 "Wait for the loadPerformanceProfiler callback to be executed on client-wrapper" 402 ); 403 return onProfilerLoaded; 404 } 405 406 /** 407 * The "This Firefox" string depends on the brandShortName, which will be different 408 * depending on the channel where tests are running. 409 */ 410 function getThisFirefoxString(aboutDebuggingWindow) { 411 const loader = aboutDebuggingWindow.getBrowserLoaderForWindow(); 412 const { l10n } = loader.require( 413 "resource://devtools/client/aboutdebugging/src/modules/l10n.js" 414 ); 415 return l10n.getString("about-debugging-this-firefox-runtime-name"); 416 } 417 418 function waitUntilUsbDeviceIsUnplugged(deviceName, aboutDebuggingDocument) { 419 info("Wait until the USB sidebar item appears as unplugged"); 420 return waitUntil(() => { 421 const sidebarItem = findSidebarItemByText( 422 deviceName, 423 aboutDebuggingDocument 424 ); 425 return !!sidebarItem.querySelector(".qa-runtime-item-unplugged"); 426 }); 427 } 428 429 /** 430 * Changing the selected tab in the current browser will trigger a tablist 431 * update. 432 * If the currently selected page is "this-firefox", we should wait for the 433 * the corresponding REQUEST_TABS_SUCCESS that will be triggered by the change. 434 * 435 * @param {Browser} browser 436 * The browser instance to update. 437 * @param {XULTab} tab 438 * The tab to select. 439 * @param {object} store 440 * The about:debugging redux store. 441 */ 442 async function updateSelectedTab(browser, tab, store) { 443 info("Update the selected tab"); 444 445 const { runtimes, ui } = store.getState(); 446 const isOnThisFirefox = 447 runtimes.selectedRuntimeId === "this-firefox" && 448 ui.selectedPage === "runtime"; 449 450 // A tabs request will only be issued if we are on this-firefox. 451 const onTabsSuccess = isOnThisFirefox 452 ? waitForDispatch(store, "REQUEST_TABS_SUCCESS") 453 : null; 454 455 // Update the selected tab. 456 browser.selectedTab = tab; 457 458 if (onTabsSuccess) { 459 info("Wait for the tablist update after updating the selected tab"); 460 await onTabsSuccess; 461 } 462 } 463 464 /** 465 * Synthesizes key input inside the DebugTargetInfo's URL component. 466 * 467 * @param {DevToolsToolbox} toolbox 468 * The DevToolsToolbox debugging the target. 469 * @param {HTMLElement} inputEl 470 * The <input> element to submit the URL with. 471 * @param {string} url 472 * The URL to navigate to. 473 */ 474 async function synthesizeUrlKeyInput(toolbox, inputEl, url) { 475 const { devtoolsDocument, devtoolsWindow } = toolbox; 476 info("Wait for URL input to be focused."); 477 const onInputFocused = waitUntil( 478 () => devtoolsDocument.activeElement === inputEl 479 ); 480 inputEl.focus(); 481 await onInputFocused; 482 483 info("Synthesize entering URL into text field"); 484 const onInputChange = waitUntil(() => inputEl.value === url); 485 for (const key of url.split("")) { 486 EventUtils.synthesizeKey(key, {}, devtoolsWindow); 487 } 488 await onInputChange; 489 490 info("Submit URL to navigate to"); 491 EventUtils.synthesizeKey("KEY_Enter"); 492 } 493 494 /** 495 * Click on a given add-on widget button so that its browser actor is fired. 496 * Typically a popup would open, or a listener would be called in the background page. 497 * 498 * @param {string} addonId 499 * The ID of the add-on to click on. 500 */ 501 function clickOnAddonWidget(addonId) { 502 // Devtools are in another window and may have the focus. 503 // Ensure focusing the browser window when clicking on the widget. 504 const focusedWin = Services.focus.focusedWindow; 505 if (focusedWin != window) { 506 window.focus(); 507 } 508 // Find the browserAction button that will show the webextension popup. 509 const widgetId = addonId.toLowerCase().replace(/[^a-z0-9_-]/g, "_"); 510 const browserActionId = widgetId + "-browser-action"; 511 const browserActionEl = window.document.getElementById(browserActionId); 512 ok(browserActionEl, "Got the browserAction button from the browser UI"); 513 514 info("Show the web extension popup"); 515 browserActionEl 516 .querySelector(".unified-extensions-item-action-button") 517 .click(); 518 } 519 520 // Create basic addon data as the DevToolsClient would return it. 521 function createAddonData({ 522 id, 523 name, 524 isSystem = false, 525 hidden = false, 526 temporary = false, 527 }) { 528 return { 529 actor: `actorid-${id}`, 530 hidden, 531 iconURL: `moz-extension://${id}/icon-url.png`, 532 id, 533 manifestURL: `moz-extension://${id}/manifest-url.json`, 534 name, 535 isSystem, 536 temporarilyInstalled: temporary, 537 debuggable: true, 538 }; 539 } 540 541 async function connectToLocalFirefox({ runtimeId, runtimeName, deviceName }) { 542 // This is a client to the current Firefox. 543 const clientWrapper = await createLocalClientWrapper(); 544 545 // enable USB devices mocks 546 const mocks = new Mocks(); 547 const usbClient = mocks.createUSBRuntime(runtimeId, { 548 deviceName, 549 name: runtimeName, 550 clientWrapper, 551 }); 552 553 // Wrap a disconnect helper for convenience for the caller. 554 const disconnect = doc => 555 disconnectFromLocalFirefox({ 556 doc, 557 runtimeId, 558 deviceName, 559 mocks, 560 }); 561 562 return { disconnect, mocks, usbClient }; 563 } 564 /* exported connectToLocalFirefox */ 565 566 async function disconnectFromLocalFirefox({ 567 doc, 568 mocks, 569 runtimeId, 570 deviceName, 571 }) { 572 info("Remove USB runtime"); 573 mocks.removeUSBRuntime(runtimeId); 574 mocks.emitUSBUpdate(); 575 await waitUntilUsbDeviceIsUnplugged(deviceName, doc); 576 } 577 /* exported disconnectFromLocalFirefox */