browser_midi_permission_gated.js (24854B)
1 /* Any copyright is dedicated to the Public Domain. 2 * http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 const EXAMPLE_COM_URL = 5 "https://example.com/document-builder.sjs?html=<h1>Test midi permission with synthetic site permission addon</h1>"; 6 const PAGE_WITH_IFRAMES_URL = `https://example.org/document-builder.sjs?html= 7 <h1>Test midi permission with synthetic site permission addon in iframes</h1> 8 <iframe id=sameOrigin src="${encodeURIComponent( 9 'https://example.org/document-builder.sjs?html=SameOrigin"' 10 )}"></iframe> 11 <iframe id=crossOrigin src="${encodeURIComponent( 12 'https://example.net/document-builder.sjs?html=CrossOrigin"' 13 )}"></iframe>`; 14 15 const l10n = new Localization( 16 [ 17 "browser/addonNotifications.ftl", 18 "toolkit/global/extensions.ftl", 19 "toolkit/global/extensionPermissions.ftl", 20 "branding/brand.ftl", 21 ], 22 true 23 ); 24 25 const { HttpServer } = ChromeUtils.importESModule( 26 "resource://testing-common/httpd.sys.mjs" 27 ); 28 ChromeUtils.defineESModuleGetters(this, { 29 AddonTestUtils: "resource://testing-common/AddonTestUtils.sys.mjs", 30 }); 31 32 add_setup(async function () { 33 await SpecialPowers.pushPrefEnv({ 34 set: [["midi.prompt.testing", false]], 35 }); 36 37 AddonTestUtils.initMochitest(this); 38 AddonTestUtils.hookAMTelemetryEvents(); 39 40 // Once the addon is installed, a dialog is displayed as a confirmation. 41 // This could interfere with tests running after this one, so we set up a listener 42 // that will always accept post install dialogs so we don't have to deal with them in 43 // the test. 44 alwaysAcceptAddonPostInstallDialogs(); 45 46 registerCleanupFunction(async () => { 47 // Remove the permission. 48 await SpecialPowers.removePermission("midi-sysex", { 49 url: EXAMPLE_COM_URL, 50 }); 51 await SpecialPowers.removePermission("midi-sysex", { 52 url: PAGE_WITH_IFRAMES_URL, 53 }); 54 await SpecialPowers.removePermission("midi", { 55 url: EXAMPLE_COM_URL, 56 }); 57 await SpecialPowers.removePermission("midi", { 58 url: PAGE_WITH_IFRAMES_URL, 59 }); 60 await SpecialPowers.removePermission("install", { 61 url: EXAMPLE_COM_URL, 62 }); 63 64 while (gBrowser.tabs.length > 1) { 65 BrowserTestUtils.removeTab(gBrowser.selectedTab); 66 } 67 }); 68 }); 69 70 add_task(async function testRequestMIDIAccess() { 71 gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, EXAMPLE_COM_URL); 72 await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); 73 const testPageHost = gBrowser.selectedTab.linkedBrowser.documentURI.host; 74 Services.fog.testResetFOG(); 75 76 info("Check that midi-sysex isn't set"); 77 ok( 78 await SpecialPowers.testPermission( 79 "midi-sysex", 80 SpecialPowers.Services.perms.UNKNOWN_ACTION, 81 { url: EXAMPLE_COM_URL } 82 ), 83 "midi-sysex value should have UNKNOWN permission" 84 ); 85 86 info("Request midi-sysex access"); 87 let onAddonInstallBlockedNotification = waitForNotification( 88 "addon-install-blocked" 89 ); 90 await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { 91 content.midiAccessRequestPromise = content.navigator.requestMIDIAccess({ 92 sysex: true, 93 }); 94 }); 95 96 info("Deny site permission addon install in first popup"); 97 let addonInstallPanel = await onAddonInstallBlockedNotification; 98 const [installPopupHeader, installPopupMessage] = 99 addonInstallPanel.querySelectorAll( 100 "description.popup-notification-description" 101 ); 102 is( 103 installPopupHeader.textContent, 104 l10n.formatValueSync("site-permission-install-first-prompt-midi-header"), 105 "First popup has expected header text" 106 ); 107 is( 108 installPopupMessage.textContent, 109 l10n.formatValueSync("site-permission-install-first-prompt-midi-message"), 110 "First popup has expected message" 111 ); 112 113 let notification = addonInstallPanel.childNodes[0]; 114 // secondaryButton is the "Don't allow" button 115 notification.secondaryButton.click(); 116 117 let rejectionMessage = await SpecialPowers.spawn( 118 gBrowser.selectedBrowser, 119 [], 120 async () => { 121 let errorMessage; 122 try { 123 await content.midiAccessRequestPromise; 124 } catch (e) { 125 errorMessage = `${e.name}: ${e.message}`; 126 } 127 128 delete content.midiAccessRequestPromise; 129 return errorMessage; 130 } 131 ); 132 is( 133 rejectionMessage, 134 "SecurityError: WebMIDI requires a site permission add-on to activate" 135 ); 136 137 assertSitePermissionInstallTelemetryEvents(["site_warning", "cancelled"]); 138 139 info("Deny site permission addon install in second popup"); 140 onAddonInstallBlockedNotification = waitForNotification( 141 "addon-install-blocked" 142 ); 143 await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { 144 content.midiAccessRequestPromise = content.navigator.requestMIDIAccess({ 145 sysex: true, 146 }); 147 }); 148 addonInstallPanel = await onAddonInstallBlockedNotification; 149 notification = addonInstallPanel.childNodes[0]; 150 let dialogPromise = waitForInstallDialog(); 151 notification.button.click(); 152 let installDialog = await dialogPromise; 153 is( 154 installDialog.querySelector(".popup-notification-description").textContent, 155 l10n.formatValueSync( 156 "webext-site-perms-header-with-gated-perms-midi-sysex", 157 { hostname: testPageHost } 158 ), 159 "Install dialog has expected header text" 160 ); 161 is( 162 installDialog.querySelector("popupnotificationcontent description") 163 .textContent, 164 l10n.formatValueSync("webext-site-perms-description-gated-perms-midi"), 165 "Install dialog has expected description" 166 ); 167 168 // secondaryButton is the "Cancel" button 169 installDialog.secondaryButton.click(); 170 171 rejectionMessage = await SpecialPowers.spawn( 172 gBrowser.selectedBrowser, 173 [], 174 async () => { 175 let errorMessage; 176 try { 177 await content.midiAccessRequestPromise; 178 } catch (e) { 179 errorMessage = `${e.name}: ${e.message}`; 180 } 181 182 delete content.midiAccessRequestPromise; 183 return errorMessage; 184 } 185 ); 186 is( 187 rejectionMessage, 188 "SecurityError: WebMIDI requires a site permission add-on to activate" 189 ); 190 191 assertSitePermissionInstallTelemetryEvents([ 192 "site_warning", 193 "permissions_prompt", 194 "cancelled", 195 ]); 196 197 info("Request midi-sysex access again"); 198 onAddonInstallBlockedNotification = waitForNotification( 199 "addon-install-blocked" 200 ); 201 await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { 202 content.midiAccessRequestPromise = content.navigator.requestMIDIAccess({ 203 sysex: true, 204 }); 205 }); 206 207 info("Accept site permission addon install"); 208 addonInstallPanel = await onAddonInstallBlockedNotification; 209 notification = addonInstallPanel.childNodes[0]; 210 dialogPromise = waitForInstallDialog(); 211 notification.button.click(); 212 installDialog = await dialogPromise; 213 installDialog.button.click(); 214 215 info("Wait for the midi-sysex access request promise to resolve"); 216 let accessGranted = await SpecialPowers.spawn( 217 gBrowser.selectedBrowser, 218 [], 219 async () => { 220 try { 221 await content.midiAccessRequestPromise; 222 return true; 223 } catch (e) {} 224 225 delete content.midiAccessRequestPromise; 226 return false; 227 } 228 ); 229 ok(accessGranted, "requestMIDIAccess resolved"); 230 231 info("Check that midi-sysex is now set"); 232 ok( 233 await SpecialPowers.testPermission( 234 "midi-sysex", 235 SpecialPowers.Services.perms.ALLOW_ACTION, 236 { url: EXAMPLE_COM_URL } 237 ), 238 "midi-sysex value should have ALLOW permission" 239 ); 240 ok( 241 await SpecialPowers.testPermission( 242 "midi", 243 SpecialPowers.Services.perms.UNKNOWN_ACTION, 244 { url: EXAMPLE_COM_URL } 245 ), 246 "but midi should have UNKNOWN permission" 247 ); 248 249 info("Check that we don't prompt user again once they installed the addon"); 250 const accessPromiseState = await SpecialPowers.spawn( 251 gBrowser.selectedBrowser, 252 [], 253 async () => { 254 return content.navigator 255 .requestMIDIAccess({ sysex: true }) 256 .then(() => "resolved"); 257 } 258 ); 259 is( 260 accessPromiseState, 261 "resolved", 262 "requestMIDIAccess resolved without user prompt" 263 ); 264 265 assertSitePermissionInstallTelemetryEvents([ 266 "site_warning", 267 "permissions_prompt", 268 "completed", 269 ]); 270 271 info("Request midi access without sysex"); 272 onAddonInstallBlockedNotification = waitForNotification( 273 "addon-install-blocked" 274 ); 275 await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { 276 content.midiNoSysexAccessRequestPromise = 277 content.navigator.requestMIDIAccess(); 278 }); 279 280 info("Accept site permission addon install"); 281 addonInstallPanel = await onAddonInstallBlockedNotification; 282 notification = addonInstallPanel.childNodes[0]; 283 284 is( 285 notification 286 .querySelector("#addon-install-blocked-info") 287 .getAttribute("href"), 288 Services.urlFormatter.formatURLPref("app.support.baseURL") + 289 "site-permission-addons", 290 "Got the expected SUMO page as a learn more link in the addon-install-blocked panel" 291 ); 292 293 dialogPromise = waitForInstallDialog(); 294 notification.button.click(); 295 installDialog = await dialogPromise; 296 297 is( 298 installDialog.querySelector(".popup-notification-description").textContent, 299 l10n.formatValueSync("webext-site-perms-header-with-gated-perms-midi", { 300 hostname: testPageHost, 301 }), 302 "Install dialog has expected header text" 303 ); 304 is( 305 installDialog.querySelector("popupnotificationcontent description") 306 .textContent, 307 l10n.formatValueSync("webext-site-perms-description-gated-perms-midi"), 308 "Install dialog has expected description" 309 ); 310 311 installDialog.button.click(); 312 313 info("Wait for the midi access request promise to resolve"); 314 accessGranted = await SpecialPowers.spawn( 315 gBrowser.selectedBrowser, 316 [], 317 async () => { 318 try { 319 await content.midiNoSysexAccessRequestPromise; 320 return true; 321 } catch (e) {} 322 323 delete content.midiNoSysexAccessRequestPromise; 324 return false; 325 } 326 ); 327 ok(accessGranted, "requestMIDIAccess resolved"); 328 329 info("Check that both midi-sysex and midi are now set"); 330 ok( 331 await SpecialPowers.testPermission( 332 "midi-sysex", 333 SpecialPowers.Services.perms.ALLOW_ACTION, 334 { url: EXAMPLE_COM_URL } 335 ), 336 "midi-sysex value should have ALLOW permission" 337 ); 338 ok( 339 await SpecialPowers.testPermission( 340 "midi", 341 SpecialPowers.Services.perms.ALLOW_ACTION, 342 { url: EXAMPLE_COM_URL } 343 ), 344 "and midi value should also have ALLOW permission" 345 ); 346 347 assertSitePermissionInstallTelemetryEvents([ 348 "site_warning", 349 "permissions_prompt", 350 "completed", 351 ]); 352 353 info("Check that we don't prompt user again when they perm denied"); 354 // remove permission to have a clean state 355 await SpecialPowers.removePermission("midi-sysex", { 356 url: EXAMPLE_COM_URL, 357 }); 358 359 onAddonInstallBlockedNotification = waitForNotification( 360 "addon-install-blocked" 361 ); 362 await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { 363 content.midiAccessRequestPromise = content.navigator.requestMIDIAccess({ 364 sysex: true, 365 }); 366 }); 367 368 info("Perm-deny site permission addon install"); 369 addonInstallPanel = await onAddonInstallBlockedNotification; 370 // Click the "Report Suspicious Site" menuitem, which has the same effect as 371 // "Never Allow" and also submits a telemetry event (which we check below). 372 notification.menupopup.querySelectorAll("menuitem")[1].click(); 373 374 rejectionMessage = await SpecialPowers.spawn( 375 gBrowser.selectedBrowser, 376 [], 377 async () => { 378 let errorMessage; 379 try { 380 await content.midiAccessRequestPromise; 381 } catch (e) { 382 errorMessage = e.name; 383 } 384 385 delete content.midiAccessRequestPromise; 386 return errorMessage; 387 } 388 ); 389 is(rejectionMessage, "SecurityError", "requestMIDIAccess was rejected"); 390 391 info("Request midi-sysex access again"); 392 let denyIntervalStart = performance.now(); 393 rejectionMessage = await SpecialPowers.spawn( 394 gBrowser.selectedBrowser, 395 [], 396 async () => { 397 let errorMessage; 398 try { 399 await content.navigator.requestMIDIAccess({ 400 sysex: true, 401 }); 402 } catch (e) { 403 errorMessage = e.name; 404 } 405 return errorMessage; 406 } 407 ); 408 is( 409 rejectionMessage, 410 "SecurityError", 411 "requestMIDIAccess was rejected without user prompt" 412 ); 413 let denyIntervalElapsed = performance.now() - denyIntervalStart; 414 Assert.greaterOrEqual( 415 denyIntervalElapsed, 416 3000, 417 `Rejection should be delayed by a randomized interval no less than 3 seconds (got ${ 418 denyIntervalElapsed / 1000 419 } seconds)` 420 ); 421 422 Assert.deepEqual( 423 [{ suspicious_site: "example.com" }], 424 AddonTestUtils.getAMGleanEvents("reportSuspiciousSite"), 425 "Expected Glean event recorded." 426 ); 427 428 assertSitePermissionInstallTelemetryEvents(["site_warning", "cancelled"]); 429 }); 430 431 add_task(async function testIframeRequestMIDIAccess() { 432 gBrowser.selectedTab = BrowserTestUtils.addTab( 433 gBrowser, 434 PAGE_WITH_IFRAMES_URL 435 ); 436 await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); 437 438 info("Check that midi-sysex isn't set"); 439 ok( 440 await SpecialPowers.testPermission( 441 "midi-sysex", 442 SpecialPowers.Services.perms.UNKNOWN_ACTION, 443 { url: PAGE_WITH_IFRAMES_URL } 444 ), 445 "midi-sysex value should have UNKNOWN permission" 446 ); 447 448 info("Request midi-sysex access from the same-origin iframe"); 449 const sameOriginIframeBrowsingContext = await SpecialPowers.spawn( 450 gBrowser.selectedBrowser, 451 [], 452 async () => { 453 return content.document.getElementById("sameOrigin").browsingContext; 454 } 455 ); 456 457 let onAddonInstallBlockedNotification = waitForNotification( 458 "addon-install-blocked" 459 ); 460 await SpecialPowers.spawn(sameOriginIframeBrowsingContext, [], () => { 461 content.midiAccessRequestPromise = content.navigator.requestMIDIAccess({ 462 sysex: true, 463 }); 464 }); 465 466 info("Accept site permission addon install"); 467 const addonInstallPanel = await onAddonInstallBlockedNotification; 468 const notification = addonInstallPanel.childNodes[0]; 469 const dialogPromise = waitForInstallDialog(); 470 notification.button.click(); 471 let installDialog = await dialogPromise; 472 installDialog.button.click(); 473 474 info("Wait for the midi-sysex access request promise to resolve"); 475 const accessGranted = await SpecialPowers.spawn( 476 sameOriginIframeBrowsingContext, 477 [], 478 async () => { 479 try { 480 await content.midiAccessRequestPromise; 481 return true; 482 } catch (e) {} 483 484 delete content.midiAccessRequestPromise; 485 return false; 486 } 487 ); 488 ok(accessGranted, "requestMIDIAccess resolved"); 489 490 info("Check that midi-sysex is now set"); 491 ok( 492 await SpecialPowers.testPermission( 493 "midi-sysex", 494 SpecialPowers.Services.perms.ALLOW_ACTION, 495 { url: PAGE_WITH_IFRAMES_URL } 496 ), 497 "midi-sysex value should have ALLOW permission" 498 ); 499 500 info( 501 "Check that we don't prompt user again once they installed the addon from the same-origin iframe" 502 ); 503 const accessPromiseState = await SpecialPowers.spawn( 504 gBrowser.selectedBrowser, 505 [], 506 async () => { 507 return content.navigator 508 .requestMIDIAccess({ sysex: true }) 509 .then(() => "resolved"); 510 } 511 ); 512 is( 513 accessPromiseState, 514 "resolved", 515 "requestMIDIAccess resolved without user prompt" 516 ); 517 518 assertSitePermissionInstallTelemetryEvents([ 519 "site_warning", 520 "permissions_prompt", 521 "completed", 522 ]); 523 524 info("Check that request is rejected when done from a cross-origin iframe"); 525 const crossOriginIframeBrowsingContext = await SpecialPowers.spawn( 526 gBrowser.selectedBrowser, 527 [], 528 async () => { 529 return content.document.getElementById("crossOrigin").browsingContext; 530 } 531 ); 532 533 const onConsoleErrorMessage = new Promise(resolve => { 534 const errorListener = { 535 observe(error) { 536 if (error.message.includes("WebMIDI access request was denied")) { 537 resolve(error); 538 Services.console.unregisterListener(errorListener); 539 } 540 }, 541 }; 542 Services.console.registerListener(errorListener); 543 }); 544 545 const rejectionMessage = await SpecialPowers.spawn( 546 crossOriginIframeBrowsingContext, 547 [], 548 async () => { 549 let errorName; 550 try { 551 await content.navigator.requestMIDIAccess({ 552 sysex: true, 553 }); 554 } catch (e) { 555 errorName = e.name; 556 } 557 return errorName; 558 } 559 ); 560 561 is( 562 rejectionMessage, 563 "SecurityError", 564 "requestMIDIAccess from the remote iframe was rejected" 565 ); 566 567 const consoleErrorMessage = await onConsoleErrorMessage; 568 ok( 569 consoleErrorMessage.message.includes( 570 `WebMIDI access request was denied: ❝SitePermsAddons can't be installed from cross origin subframes❞`, 571 "an error message is sent to the console" 572 ) 573 ); 574 assertSitePermissionInstallTelemetryEvents([]); 575 }); 576 577 add_task(async function testRequestMIDIAccessLocalhost() { 578 const httpServer = new HttpServer(); 579 httpServer.start(-1); 580 httpServer.registerPathHandler(`/test`, function (request, response) { 581 response.setStatusLine(request.httpVersion, 200, "OK"); 582 response.write(` 583 <!DOCTYPE html> 584 <meta charset=utf8> 585 <h1>Test requestMIDIAccess on lcoalhost</h1>`); 586 }); 587 const localHostTestUrl = `http://localhost:${httpServer.identity.primaryPort}/test`; 588 589 registerCleanupFunction(async function cleanup() { 590 await new Promise(resolve => httpServer.stop(resolve)); 591 }); 592 593 gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, localHostTestUrl); 594 await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); 595 596 info("Check that midi-sysex isn't set"); 597 ok( 598 await SpecialPowers.testPermission( 599 "midi-sysex", 600 SpecialPowers.Services.perms.UNKNOWN_ACTION, 601 { url: localHostTestUrl } 602 ), 603 "midi-sysex value should have UNKNOWN permission" 604 ); 605 606 info( 607 "Request midi-sysex access should not prompt for addon install on locahost, but for permission" 608 ); 609 let popupShown = BrowserTestUtils.waitForEvent( 610 PopupNotifications.panel, 611 "popupshown" 612 ); 613 await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { 614 content.midiAccessRequestPromise = content.navigator.requestMIDIAccess({ 615 sysex: true, 616 }); 617 }); 618 await popupShown; 619 is( 620 PopupNotifications.panel.querySelector("popupnotification").id, 621 "midi-notification", 622 "midi notification was displayed" 623 ); 624 625 info("Accept permission"); 626 PopupNotifications.panel 627 .querySelector(".popup-notification-primary-button") 628 .click(); 629 630 info("Wait for the midi-sysex access request promise to resolve"); 631 let accessGranted = await SpecialPowers.spawn( 632 gBrowser.selectedBrowser, 633 [], 634 async () => { 635 try { 636 await content.midiAccessRequestPromise; 637 return true; 638 } catch (e) {} 639 640 delete content.midiAccessRequestPromise; 641 return false; 642 } 643 ); 644 ok(accessGranted, "requestMIDIAccess resolved"); 645 646 // We're remembering permission grants temporarily on the tab since Bug 1754005. 647 info( 648 "Check that a new request is automatically granted because we granted before in the same tab." 649 ); 650 651 await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => { 652 content.navigator.requestMIDIAccess({ sysex: true }); 653 }); 654 655 accessGranted = await SpecialPowers.spawn( 656 gBrowser.selectedBrowser, 657 [], 658 async () => { 659 try { 660 await content.midiAccessRequestPromise; 661 return true; 662 } catch (e) {} 663 664 delete content.midiAccessRequestPromise; 665 return false; 666 } 667 ); 668 ok(accessGranted, "requestMIDIAccess resolved"); 669 670 assertSitePermissionInstallTelemetryEvents([]); 671 }); 672 673 add_task(async function testDisabledRequestMIDIAccessFile() { 674 let dir = getChromeDir(getResolvedURI(gTestPath)); 675 dir.append("blank.html"); 676 const fileSchemeTestUri = Services.io.newFileURI(dir).spec; 677 678 gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, fileSchemeTestUri); 679 await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); 680 681 info("Check that requestMIDIAccess isn't set on navigator on file scheme"); 682 const isRequestMIDIAccessDefined = await SpecialPowers.spawn( 683 gBrowser.selectedBrowser, 684 [], 685 () => { 686 return "requestMIDIAccess" in content.wrappedJSObject.navigator; 687 } 688 ); 689 is( 690 isRequestMIDIAccessDefined, 691 false, 692 "navigator.requestMIDIAccess is not defined on file scheme" 693 ); 694 }); 695 696 // Ignore any additional telemetry events collected in this file. 697 // Unfortunately it doesn't work to have this in a cleanup function. 698 // Keep this as the last task done. 699 add_task(function teardown_telemetry_events() { 700 AddonTestUtils.getAMTelemetryEvents(); 701 }); 702 703 /** 704 * Check that the expected sitepermission install events are recorded. 705 * 706 * @param {Array<string>} expectedSteps: An array of the expected extra.step values recorded. 707 */ 708 function assertSitePermissionInstallTelemetryEvents(expectedSteps) { 709 let amInstallEvents = AddonTestUtils.getAMTelemetryEvents() 710 .filter(evt => evt.method === "install" && evt.object === "sitepermission") 711 .map(evt => evt.extra.step); 712 713 Assert.deepEqual(amInstallEvents, expectedSteps); 714 } 715 716 async function waitForInstallDialog(id = "addon-webext-permissions") { 717 let panel = await waitForNotification(id); 718 return panel.childNodes[0]; 719 } 720 721 /** 722 * Adds an event listener that will listen for post-install dialog event and automatically 723 * close the dialogs. 724 */ 725 function alwaysAcceptAddonPostInstallDialogs() { 726 // Once the addon is installed, a dialog is displayed as a confirmation. 727 // This could interfere with tests running after this one, so we set up a listener 728 // that will always accept post install dialogs so we don't have to deal with them in 729 // the test. 730 const abortController = new AbortController(); 731 732 const { AppMenuNotifications } = ChromeUtils.importESModule( 733 "resource://gre/modules/AppMenuNotifications.sys.mjs" 734 ); 735 info("Start listening and accept addon post-install notifications"); 736 PanelUI.notificationPanel.addEventListener( 737 "popupshown", 738 async function popupshown() { 739 let notification = AppMenuNotifications.activeNotification; 740 if (!notification || notification.id !== "addon-installed") { 741 return; 742 } 743 744 let popupnotificationID = PanelUI._getPopupId(notification); 745 if (popupnotificationID) { 746 info("Accept post-install dialog"); 747 let popupnotification = document.getElementById(popupnotificationID); 748 popupnotification?.button.click(); 749 } 750 }, 751 { 752 signal: abortController.signal, 753 } 754 ); 755 756 registerCleanupFunction(async () => { 757 // Clear the listener at the end of the test file, to prevent it to stay 758 // around when the same browser instance may be running other unrelated 759 // test files. 760 abortController.abort(); 761 }); 762 } 763 764 const PROGRESS_NOTIFICATION = "addon-progress"; 765 async function waitForNotification(notificationId) { 766 info(`Waiting for ${notificationId} notification`); 767 768 let topic = getObserverTopic(notificationId); 769 770 let observerPromise; 771 if (notificationId !== "addon-webext-permissions") { 772 observerPromise = new Promise(resolve => { 773 Services.obs.addObserver(function observer(aSubject, aTopic) { 774 // Ignore the progress notification unless that is the notification we want 775 if ( 776 notificationId != PROGRESS_NOTIFICATION && 777 aTopic == getObserverTopic(PROGRESS_NOTIFICATION) 778 ) { 779 return; 780 } 781 Services.obs.removeObserver(observer, topic); 782 resolve(); 783 }, topic); 784 }); 785 } 786 787 let panelEventPromise = new Promise(resolve => { 788 window.PopupNotifications.panel.addEventListener( 789 "PanelUpdated", 790 function eventListener(e) { 791 // Skip notifications that are not the one that we are supposed to be looking for 792 if (!e.detail.includes(notificationId)) { 793 return; 794 } 795 window.PopupNotifications.panel.removeEventListener( 796 "PanelUpdated", 797 eventListener 798 ); 799 resolve(); 800 } 801 ); 802 }); 803 804 await observerPromise; 805 await panelEventPromise; 806 await waitForTick(); 807 808 info(`Saw a ${notificationId} notification`); 809 await SimpleTest.promiseFocus(window.PopupNotifications.window); 810 return window.PopupNotifications.panel; 811 } 812 813 // This function is similar to the one in 814 // toolkit/mozapps/extensions/test/xpinstall/browser_doorhanger_installs.js, 815 // please keep both in sync! 816 function getObserverTopic(aNotificationId) { 817 let topic = aNotificationId; 818 if (topic == "xpinstall-disabled") { 819 topic = "addon-install-disabled"; 820 } else if (topic == "addon-progress") { 821 topic = "addon-install-started"; 822 } else if (topic == "addon-installed") { 823 topic = "webextension-install-notify"; 824 } 825 return topic; 826 } 827 828 function waitForTick() { 829 return new Promise(resolve => executeSoon(resolve)); 830 }