browser_aboutdebugging_addons_debug_console.js (17987B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 "use strict"; 4 5 /* import-globals-from helper-addons.js */ 6 Services.scriptloader.loadSubScript(CHROME_URL_ROOT + "helper-addons.js", this); 7 8 // There are shutdown issues for which multiple rejections are left uncaught. 9 // See bug 1018184 for resolving these issues. 10 const { PromiseTestUtils } = ChromeUtils.importESModule( 11 "resource://testing-common/PromiseTestUtils.sys.mjs" 12 ); 13 PromiseTestUtils.allowMatchingRejectionsGlobally(/File closed/); 14 15 const { ExtensionStorageIDB } = ChromeUtils.importESModule( 16 "resource://gre/modules/ExtensionStorageIDB.sys.mjs" 17 ); 18 19 // Avoid test timeouts that can occur while waiting for the "addon-console-works" message. 20 requestLongerTimeout(2); 21 22 const ADDON_ID = "test-devtools-webextension@mozilla.org"; 23 const ADDON_NAME = "base-test-devtools-webextension"; 24 25 const OTHER_ADDON_ID = "other-test-devtools-webextension@mozilla.org"; 26 const OTHER_ADDON_NAME = "other-test-devtools-webextension"; 27 28 const POPUPONLY_ADDON_ID = "popuponly-test-devtools-webextension@mozilla.org"; 29 const POPUPONLY_ADDON_NAME = "popuponly-test-devtools-webextension"; 30 31 const BACKGROUND_ADDON_ID = "background-test-devtools-webextension@mozilla.org"; 32 const BACKGROUND_ADDON_NAME = "background-test-devtools-webextension"; 33 34 const TEST_URI = "https://example.com/document-builder.sjs?html=foo"; 35 36 /** 37 * This test file ensures that the webextension addon developer toolbox: 38 * - when the debug button is clicked on a webextension, the opened toolbox 39 * has a working webconsole with the background page as default target; 40 */ 41 add_task(async function testWebExtensionsToolboxWebConsole() { 42 await pushPref("devtools.webconsole.filter.css", true); 43 await enableExtensionDebugging(); 44 45 await addTab(TEST_URI); 46 47 const { document, tab, window } = await openAboutDebugging(); 48 await selectThisFirefoxPage(document, window.AboutDebugging.store); 49 50 await installTemporaryExtensionFromXPI( 51 { 52 background() { 53 /* global browser */ 54 window.myWebExtensionAddonFunction = async function () { 55 console.log( 56 "Background page function called", 57 this.browser.runtime.getManifest() 58 ); 59 60 const [t] = await browser.tabs.query({ 61 url: "https://example.com/document-builder*", 62 }); 63 browser.tabs.sendMessage(t.id, {}); 64 }; 65 66 const style = document.createElement("style"); 67 style.textContent = "* { color: error; }"; 68 document.documentElement.appendChild(style); 69 70 browser.runtime.onMessage.addListener(() => { 71 browser.runtime.sendMessage("messageForOnMessageInPopup"); 72 throw new Error("onMessage exception from background page"); 73 }); 74 75 browser.storage.local.onChanged.addListener(() => { 76 throw new Error("local.onChanged exception"); 77 }); 78 79 throw new Error("Background page exception"); 80 }, 81 extraProperties: { 82 browser_action: { 83 default_title: "WebExtension Popup Debugging", 84 default_popup: "popup.html", 85 default_area: "navbar", 86 }, 87 permissions: ["storage", "https://example.com/*"], 88 content_scripts: [ 89 { 90 matches: ["<all_urls>"], 91 js: ["content-script.js"], 92 }, 93 ], 94 }, 95 files: { 96 "popup.html": `<!DOCTYPE html> 97 <html> 98 <head> 99 <meta charset="utf-8"> 100 <script src="popup.js"></script> 101 </head> 102 <body> 103 Popup 104 </body> 105 </html> 106 `, 107 "popup.js": function () { 108 console.log("Popup log"); 109 110 const style = document.createElement("style"); 111 style.textContent = "* { color: popup-error; }"; 112 document.documentElement.appendChild(style); 113 114 browser.runtime.onMessage.addListener(() => { 115 throw new Error("onMessage exception from popup"); 116 }); 117 118 browser.runtime.sendMessage("messageForOnMessageInBackgroundPage"); 119 120 throw new Error("Popup exception"); 121 }, 122 "content-script.js": function () { 123 browser.runtime.onMessage.addListener(() => { 124 throw new Error("onMessage exception from content script"); 125 }); 126 127 throw new Error("Content script exception"); 128 }, 129 }, 130 id: ADDON_ID, 131 name: ADDON_NAME, 132 }, 133 document 134 ); 135 136 // Install another addon in order to ensure we don't get its logs 137 await installTemporaryExtensionFromXPI( 138 { 139 background() { 140 console.log("Other addon log"); 141 142 const style = document.createElement("style"); 143 style.textContent = "* { background-color: error; }"; 144 document.documentElement.appendChild(style); 145 146 throw new Error("Other addon exception"); 147 }, 148 extraProperties: { 149 browser_action: { 150 default_title: "Other addon popup", 151 default_popup: "other-popup.html", 152 default_area: "navbar", 153 }, 154 }, 155 files: { 156 "other-popup.html": `<!DOCTYPE html> 157 <html> 158 <head> 159 <meta charset="utf-8"> 160 <script src="other-popup.js"></script> 161 </head> 162 <body> 163 Other popup 164 </body> 165 </html> 166 `, 167 "other-popup.js": function () { 168 console.log("Other popup log"); 169 170 const style = document.createElement("style"); 171 style.textContent = "* { background-color: popup-error; }"; 172 document.documentElement.appendChild(style); 173 174 throw new Error("Other popup exception"); 175 }, 176 }, 177 id: OTHER_ADDON_ID, 178 name: OTHER_ADDON_NAME, 179 }, 180 document 181 ); 182 183 // Trigger a browser.local storage change 184 ExtensionStorageIDB.notifyListeners(ADDON_ID, {}); 185 186 const { devtoolsWindow } = await openAboutDevtoolsToolbox( 187 document, 188 tab, 189 window, 190 ADDON_NAME 191 ); 192 const toolbox = getToolbox(devtoolsWindow); 193 const webconsole = await toolbox.selectTool("webconsole"); 194 const { hud } = webconsole; 195 196 info("Wait for the exception coming from the background page"); 197 await waitUntil(() => { 198 return !!findMessagesByType(hud, "Background page exception", ".error") 199 .length; 200 }); 201 202 info("Trigger some code in the background page logging some stuff"); 203 const onLogMessage = waitUntil(() => { 204 return !!findMessagesByType( 205 hud, 206 "Background page function called", 207 ".message" 208 ).length; 209 }); 210 hud.ui.wrapper.dispatchEvaluateExpression("myWebExtensionAddonFunction()"); 211 await onLogMessage; 212 213 info("Open the two add-ons popups to cover popups messages"); 214 const onPopupMessage = waitUntil(() => { 215 return !!findMessagesByType(hud, "Popup exception", ".error").length; 216 }); 217 clickOnAddonWidget(OTHER_ADDON_ID); 218 clickOnAddonWidget(ADDON_ID); 219 await onPopupMessage; 220 await waitUntil(() => { 221 return !!findMessagesByType( 222 hud, 223 "onMessage exception from background page", 224 ".error" 225 ).length; 226 }); 227 await waitUntil(() => { 228 return !!findMessagesByType(hud, "onMessage exception from popup", ".error") 229 .length; 230 }); 231 await waitUntil(() => { 232 return !!findMessagesByType(hud, "local.onChanged exception", ".error") 233 .length; 234 }); 235 236 info("Assert the context of the evaluation context selector"); 237 const contextLabels = getContextLabels(toolbox); 238 is(contextLabels.length, 3); 239 is(contextLabels[0], "Web Extension Fallback Document"); 240 is(contextLabels[1], "/_generated_background_page.html"); 241 is(contextLabels[2], "/popup.html"); 242 243 info("Wait a bit to catch unexpected duplicates or mixed up messages"); 244 await wait(1000); 245 246 is( 247 findMessagesByType(hud, "Background page exception", ".error").length, 248 1, 249 "We get the background page exception" 250 ); 251 is( 252 findMessagesByType(hud, "Popup exception", ".error").length, 253 1, 254 "We get the popup exception" 255 ); 256 is( 257 findMessagesByType( 258 hud, 259 "Expected color but found ‘error’. Error in parsing value for ‘color’. Declaration dropped.", 260 ".warn" 261 ).length, 262 1, 263 "We get the addon's background page CSS error message" 264 ); 265 is( 266 findMessagesByType( 267 hud, 268 "Expected color but found ‘popup-error’. Error in parsing value for ‘color’. Declaration dropped.", 269 ".warn" 270 ).length, 271 1, 272 "We get the addon's popup CSS error message" 273 ); 274 275 // Verify that we don't get the other addon log and errors 276 ok( 277 !findMessageByType(hud, "Other addon log", ".console-api"), 278 "We don't get the other addon log" 279 ); 280 ok( 281 !findMessageByType(hud, "Other addon exception", ".console-api"), 282 "We don't get the other addon exception" 283 ); 284 ok( 285 !findMessageByType(hud, "Other popup log", ".console-api"), 286 "We don't get the other addon popup log" 287 ); 288 ok( 289 !findMessageByType(hud, "Other popup exception", ".error"), 290 "We don't get the other addon popup exception" 291 ); 292 ok( 293 !findMessageByType( 294 hud, 295 "Expected color but found ‘error’. Error in parsing value for ‘background-color’. Declaration dropped.", 296 ".warn" 297 ), 298 "We don't get the other addon's background page CSS error message" 299 ); 300 ok( 301 !findMessageByType( 302 hud, 303 "Expected color but found ‘popup-error’. Error in parsing value for ‘background-color’. Declaration dropped.", 304 ".warn" 305 ), 306 "We don't get the other addon's popup CSS error message" 307 ); 308 309 // Verify that console evaluations still work after reloading the page 310 info("Reload the webextension document"); 311 const { onResource: onDomCompleteResource } = 312 await toolbox.commands.resourceCommand.waitForNextResource( 313 toolbox.commands.resourceCommand.TYPES.DOCUMENT_EVENT, 314 { 315 ignoreExistingResources: true, 316 predicate: resource => { 317 return ( 318 resource.name === "dom-complete" && 319 resource.targetFront.url.endsWith("background_page.html") 320 ); 321 }, 322 } 323 ); 324 hud.ui.wrapper.dispatchEvaluateExpression("location.reload()"); 325 await onDomCompleteResource; 326 327 info("Try to evaluate something after reload"); 328 329 const onEvaluationResultAfterReload = waitUntil(() => 330 findMessageByType(hud, "result:2", ".result") 331 ); 332 const onMessageAfterReload = waitUntil(() => 333 findMessageByType(hud, "message after reload", ".console-api") 334 ); 335 hud.ui.wrapper.dispatchEvaluateExpression( 336 "console.log('message after reload'); 'result:' + (1 + 1)" 337 ); 338 // Both cover that the console.log worked 339 await onMessageAfterReload; 340 // And we received the evaluation result 341 await onEvaluationResultAfterReload; 342 343 await closeWebExtAboutDevtoolsToolbox(devtoolsWindow, window); 344 345 info( 346 "Open a toolbox against the tab in order to cover the content script exceptions" 347 ); 348 const { devtoolsTab, devtoolsWindow: tabDevtoolsWindow } = 349 await openAboutDevtoolsToolbox(document, tab, window, TEST_URI); 350 const tabToolbox = getToolbox(tabDevtoolsWindow); 351 const tabWebconsole = await tabToolbox.selectTool("webconsole"); 352 const tabHud = tabWebconsole.hud; 353 await waitUntil(() => { 354 return !!findMessagesByType( 355 tabHud, 356 "onMessage exception from content script", 357 ".error" 358 ).length; 359 }); 360 await closeAboutDevtoolsToolbox(document, devtoolsTab, window); 361 362 // Note that it seems to be important to remove the addons in the reverse order 363 // from which they were installed... 364 await removeTemporaryExtension(OTHER_ADDON_NAME, document); 365 await removeTemporaryExtension(ADDON_NAME, document); 366 await removeTab(tab); 367 }); 368 369 add_task(async function testWebExtensionNoBgScript() { 370 await pushPref("devtools.webconsole.filter.css", true); 371 await enableExtensionDebugging(); 372 const { document, tab, window } = await openAboutDebugging(); 373 await selectThisFirefoxPage(document, window.AboutDebugging.store); 374 375 await installTemporaryExtensionFromXPI( 376 { 377 extraProperties: { 378 browser_action: { 379 default_title: "WebExtension Popup Only", 380 default_popup: "popup.html", 381 default_area: "navbar", 382 }, 383 }, 384 files: { 385 "popup.html": `<!DOCTYPE html> 386 <html> 387 <head> 388 <meta charset="utf-8"> 389 <script src="popup.js"></script> 390 </head> 391 <body> 392 Popup 393 </body> 394 </html> 395 `, 396 "popup.js": function () { 397 console.log("Popup-only log"); 398 399 const style = document.createElement("style"); 400 style.textContent = "* { color: popup-only-error; }"; 401 document.documentElement.appendChild(style); 402 403 throw new Error("Popup-only exception"); 404 }, 405 }, 406 id: POPUPONLY_ADDON_ID, 407 name: POPUPONLY_ADDON_NAME, 408 }, 409 document 410 ); 411 412 const { devtoolsWindow } = await openAboutDevtoolsToolbox( 413 document, 414 tab, 415 window, 416 POPUPONLY_ADDON_NAME 417 ); 418 const toolbox = getToolbox(devtoolsWindow); 419 const webconsole = await toolbox.selectTool("webconsole"); 420 const { hud } = webconsole; 421 422 info("Open the add-on popup"); 423 const onPopupMessage = waitUntil(() => { 424 return !!findMessagesByType(hud, "Popup-only exception", ".error").length; 425 }); 426 clickOnAddonWidget(POPUPONLY_ADDON_ID); 427 await onPopupMessage; 428 429 info("Wait a bit to catch unexpected duplicates or mixed up messages"); 430 await wait(1000); 431 is( 432 findMessagesByType(hud, "Popup-only exception", ".error").length, 433 1, 434 "We get the popup exception" 435 ); 436 is( 437 findMessagesByType(hud, "Popup-only log", ".console-api").length, 438 1, 439 "We get the addon's popup log" 440 ); 441 is( 442 findMessagesByType( 443 hud, 444 "Expected color but found ‘popup-only-error’. Error in parsing value for ‘color’. Declaration dropped.", 445 ".warn" 446 ).length, 447 1, 448 "We get the addon's popup CSS error message" 449 ); 450 451 await closeWebExtAboutDevtoolsToolbox(devtoolsWindow, window); 452 await removeTemporaryExtension(POPUPONLY_ADDON_NAME, document); 453 await removeTab(tab); 454 }); 455 456 // Check that reloading the addon several times does not break the console, 457 // see Bug 1778951. 458 add_task(async function testWebExtensionTwoReloads() { 459 await enableExtensionDebugging(); 460 const { document, tab, window } = await openAboutDebugging(); 461 await selectThisFirefoxPage(document, window.AboutDebugging.store); 462 463 await installTemporaryExtensionFromXPI( 464 { 465 background() { 466 console.log("Background page log"); 467 }, 468 extraProperties: { 469 browser_action: { 470 default_title: "WebExtension with background script", 471 default_popup: "popup.html", 472 default_area: "navbar", 473 }, 474 }, 475 files: { 476 "popup.html": `<!DOCTYPE html> 477 <html> 478 <body> 479 Popup 480 </body> 481 </html> 482 `, 483 }, 484 id: BACKGROUND_ADDON_ID, 485 name: BACKGROUND_ADDON_NAME, 486 }, 487 document 488 ); 489 490 // Retrieve the addonTarget element before calling `openAboutDevtoolsToolbox`, 491 // otherwise it will pick the about:devtools-toolbox tab with the same name 492 // instead. 493 const addonTarget = findDebugTargetByText(BACKGROUND_ADDON_NAME, document); 494 495 const { devtoolsWindow } = await openAboutDevtoolsToolbox( 496 document, 497 tab, 498 window, 499 BACKGROUND_ADDON_NAME 500 ); 501 const toolbox = getToolbox(devtoolsWindow); 502 const webconsole = await toolbox.selectTool("webconsole"); 503 const { hud } = webconsole; 504 505 // Verify that console evaluations still work after reloading the addon 506 info("Reload the webextension itself"); 507 let { onResource: onDomCompleteResource } = 508 await toolbox.commands.resourceCommand.waitForNextResource( 509 toolbox.commands.resourceCommand.TYPES.DOCUMENT_EVENT, 510 { 511 ignoreExistingResources: true, 512 predicate: resource => 513 resource.name === "dom-complete" && 514 resource.targetFront.url.endsWith("background_page.html"), 515 } 516 ); 517 const reloadButton = addonTarget.querySelector( 518 ".qa-temporary-extension-reload-button" 519 ); 520 reloadButton.click(); 521 await onDomCompleteResource; 522 523 info("Try to evaluate something after 1st addon reload"); 524 // Wait before evaluating the message, otherwise they might be cleaned up by 525 // the console UI. 526 info("Wait until the background script log is visible"); 527 await waitUntil(() => 528 findMessageByType(hud, "Background page log", ".message") 529 ); 530 531 hud.ui.wrapper.dispatchEvaluateExpression("40+1"); 532 await waitUntil(() => findMessageByType(hud, "41", ".result")); 533 534 info("Reload the extension a second time"); 535 ({ onResource: onDomCompleteResource } = 536 await toolbox.commands.resourceCommand.waitForNextResource( 537 toolbox.commands.resourceCommand.TYPES.DOCUMENT_EVENT, 538 { 539 ignoreExistingResources: true, 540 predicate: resource => 541 resource.name === "dom-complete" && 542 resource.targetFront.url.endsWith("background_page.html"), 543 } 544 )); 545 reloadButton.click(); 546 await onDomCompleteResource; 547 548 info("Wait until the background script log is visible - after reload"); 549 await waitUntil(() => 550 findMessageByType(hud, "Background page log", ".message") 551 ); 552 553 info("Try to evaluate something after 2nd addon reload"); 554 hud.ui.wrapper.dispatchEvaluateExpression("40+2"); 555 await waitUntil(() => findMessageByType(hud, "42", ".result")); 556 557 await closeWebExtAboutDevtoolsToolbox(devtoolsWindow, window); 558 await removeTemporaryExtension(BACKGROUND_ADDON_NAME, document); 559 await removeTab(tab); 560 }); 561 562 function getContextLabels(toolbox) { 563 // Note that the context menu is in the top level chrome document (toolbox.xhtml) 564 // instead of webconsole.xhtml. 565 const labels = toolbox.doc.querySelectorAll( 566 "#webconsole-console-evaluation-context-selector-menu-list li .label" 567 ); 568 return Array.from(labels).map(item => item.textContent); 569 }