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