tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

head.js (41300B)


      1 var { PermissionTestUtils } = ChromeUtils.importESModule(
      2  "resource://testing-common/PermissionTestUtils.sys.mjs"
      3 );
      4 
      5 const PREF_PERMISSION_FAKE = "media.navigator.permission.fake";
      6 const PREF_AUDIO_LOOPBACK = "media.audio_loopback_dev";
      7 const PREF_VIDEO_LOOPBACK = "media.video_loopback_dev";
      8 const PREF_FAKE_STREAMS = "media.navigator.streams.fake";
      9 const PREF_FOCUS_SOURCE = "media.getusermedia.window.focus_source.enabled";
     10 
     11 const STATE_CAPTURE_ENABLED = Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED;
     12 const STATE_CAPTURE_DISABLED = Ci.nsIMediaManagerService.STATE_CAPTURE_DISABLED;
     13 
     14 const ALLOW_SILENCING_NOTIFICATIONS = Services.prefs.getBoolPref(
     15  "privacy.webrtc.allowSilencingNotifications",
     16  false
     17 );
     18 
     19 const SHOW_GLOBAL_MUTE_TOGGLES = Services.prefs.getBoolPref(
     20  "privacy.webrtc.globalMuteToggles",
     21  false
     22 );
     23 
     24 const SHOW_ALWAYS_ASK = Services.prefs.getBoolPref(
     25  "permissions.media.show_always_ask.enabled",
     26  false
     27 );
     28 
     29 let IsIndicatorDisabled =
     30  AppConstants.isPlatformAndVersionAtLeast("macosx", 14.0) &&
     31  !Services.prefs.getBoolPref(
     32    "privacy.webrtc.showIndicatorsOnMacos14AndAbove",
     33    false
     34  );
     35 
     36 const INDICATOR_PATH = "chrome://browser/content/webrtcIndicator.xhtml";
     37 
     38 const IS_MAC = AppConstants.platform == "macosx";
     39 
     40 const SHARE_SCREEN = 1;
     41 const SHARE_WINDOW = 2;
     42 
     43 let observerTopics = [
     44  "getUserMedia:response:allow",
     45  "getUserMedia:revoke",
     46  "getUserMedia:response:deny",
     47  "getUserMedia:request",
     48  "recording-device-events",
     49  "recording-window-ended",
     50 ];
     51 
     52 // Structured hierarchy of subframes. Keys are frame id:s, The children member
     53 // contains nested sub frames if any. The noTest member make a frame be ignored
     54 // for testing if true.
     55 let gObserveSubFrames = {};
     56 // Object of subframes to test. Each element contains the members bc and id, for
     57 // the frames BrowsingContext and id, respectively.
     58 let gSubFramesToTest = [];
     59 let gBrowserContextsToObserve = [];
     60 
     61 function whenDelayedStartupFinished(aWindow) {
     62  return TestUtils.topicObserved(
     63    "browser-delayed-startup-finished",
     64    subject => subject == aWindow
     65  );
     66 }
     67 
     68 function promiseIndicatorWindow() {
     69  let startTime = ChromeUtils.now();
     70 
     71  return new Promise(resolve => {
     72    Services.obs.addObserver(function obs(win) {
     73      win.addEventListener(
     74        "load",
     75        function () {
     76          if (win.location.href !== INDICATOR_PATH) {
     77            info("ignoring a window with this url: " + win.location.href);
     78            return;
     79          }
     80 
     81          Services.obs.removeObserver(obs, "domwindowopened");
     82          executeSoon(() => {
     83            ChromeUtils.addProfilerMarker("promiseIndicatorWindow", {
     84              startTime,
     85              category: "Test",
     86            });
     87            resolve(win);
     88          });
     89        },
     90        { once: true }
     91      );
     92    }, "domwindowopened");
     93  });
     94 }
     95 
     96 async function assertWebRTCIndicatorStatus(expected) {
     97  let ui = ChromeUtils.importESModule(
     98    "resource:///modules/webrtcUI.sys.mjs"
     99  ).webrtcUI;
    100  let expectedState = expected ? "visible" : "hidden";
    101  let msg = "WebRTC indicator " + expectedState;
    102  if (!expected && ui.showGlobalIndicator) {
    103    // It seems the global indicator is not always removed synchronously
    104    // in some cases.
    105    await TestUtils.waitForCondition(
    106      () => !ui.showGlobalIndicator,
    107      "waiting for the global indicator to be hidden"
    108    );
    109  }
    110  is(ui.showGlobalIndicator, !!expected, msg);
    111 
    112  let expectVideo = false,
    113    expectAudio = false,
    114    expectScreen = "";
    115  if (expected && !IsIndicatorDisabled) {
    116    if (expected.video) {
    117      expectVideo = true;
    118    }
    119    if (expected.audio) {
    120      expectAudio = true;
    121    }
    122    if (expected.screen) {
    123      expectScreen = expected.screen;
    124    }
    125  }
    126  is(
    127    Boolean(ui.showCameraIndicator),
    128    expectVideo,
    129    "camera global indicator as expected"
    130  );
    131  is(
    132    Boolean(ui.showMicrophoneIndicator),
    133    expectAudio,
    134    "microphone global indicator as expected"
    135  );
    136  is(
    137    ui.showScreenSharingIndicator,
    138    expectScreen,
    139    "screen global indicator as expected"
    140  );
    141 
    142  for (let win of Services.wm.getEnumerator("navigator:browser")) {
    143    let menu = win.document.getElementById("tabSharingMenu");
    144    is(
    145      !!menu && !menu.hidden,
    146      !!expected,
    147      "WebRTC menu should be " + expectedState
    148    );
    149  }
    150 
    151  if (!expected) {
    152    let win = Services.wm.getMostRecentWindow("Browser:WebRTCGlobalIndicator");
    153    if (win) {
    154      await new Promise(resolve => {
    155        win.addEventListener("unload", function listener(e) {
    156          if (e.target == win.document) {
    157            win.removeEventListener("unload", listener);
    158            executeSoon(resolve);
    159          }
    160        });
    161      });
    162    }
    163  }
    164 
    165  let indicator = Services.wm.getEnumerator("Browser:WebRTCGlobalIndicator");
    166  let hasWindow = indicator.hasMoreElements();
    167  is(hasWindow, !!expected, "popup " + msg);
    168  if (hasWindow) {
    169    let document = indicator.getNext().document;
    170    let docElt = document.documentElement;
    171 
    172    if (document.readyState != "complete") {
    173      info("Waiting for the sharing indicator's document to load");
    174      await new Promise(resolve => {
    175        document.addEventListener(
    176          "readystatechange",
    177          function onReadyStateChange() {
    178            if (document.readyState != "complete") {
    179              return;
    180            }
    181            document.removeEventListener(
    182              "readystatechange",
    183              onReadyStateChange
    184            );
    185            executeSoon(resolve);
    186          }
    187        );
    188      });
    189    }
    190 
    191    if (expected.screen && expected.screen.startsWith("Window")) {
    192      // These tests were originally written to express window sharing by
    193      // having expected.screen start with "Window". This meant that the
    194      // legacy indicator is expected to have the "sharingscreen" attribute
    195      // set to true when sharing a window.
    196      //
    197      // The new indicator, however, differentiates between screen, window
    198      // and browser window sharing. If we're using the new indicator, we
    199      // update the expectations accordingly. This can be removed once we
    200      // are able to remove the tests for the legacy indicator.
    201      expected.screen = null;
    202      expected.window = true;
    203    }
    204 
    205    if (!SHOW_GLOBAL_MUTE_TOGGLES) {
    206      expected.video = false;
    207      expected.audio = false;
    208 
    209      let visible = docElt.getAttribute("visible") == "true";
    210 
    211      if (!expected.screen && !expected.window && !expected.browserwindow) {
    212        ok(!visible, "Indicator should not be visible in this configuation.");
    213      } else {
    214        ok(visible, "Indicator should be visible.");
    215      }
    216    }
    217 
    218    for (let item of ["video", "audio", "screen", "window", "browserwindow"]) {
    219      let expectedValue;
    220 
    221      expectedValue = expected && expected[item] ? "true" : null;
    222 
    223      is(
    224        docElt.getAttribute("sharing" + item),
    225        expectedValue,
    226        item + " global indicator attribute as expected"
    227      );
    228    }
    229 
    230    ok(!indicator.hasMoreElements(), "only one global indicator window");
    231  }
    232 }
    233 
    234 function promiseNotificationShown(notification) {
    235  let win = notification.browser.ownerGlobal;
    236  if (win.PopupNotifications.panel.state == "open") {
    237    return Promise.resolve();
    238  }
    239  let panelPromise = BrowserTestUtils.waitForPopupEvent(
    240    win.PopupNotifications.panel,
    241    "shown"
    242  );
    243  notification.reshow();
    244  return panelPromise;
    245 }
    246 
    247 function ignoreEvent(aSubject, aTopic, aData) {
    248  // With e10s disabled, our content script receives notifications for the
    249  // preview displayed in our screen sharing permission prompt; ignore them.
    250  const kBrowserURL = AppConstants.BROWSER_CHROME_URL;
    251  const nsIPropertyBag = Ci.nsIPropertyBag;
    252  if (
    253    aTopic == "recording-device-events" &&
    254    aSubject.QueryInterface(nsIPropertyBag).getProperty("requestURL") ==
    255      kBrowserURL
    256  ) {
    257    return true;
    258  }
    259  if (aTopic == "recording-window-ended") {
    260    let win = Services.wm.getOuterWindowWithId(aData).top;
    261    if (win.document.documentURI == kBrowserURL) {
    262      return true;
    263    }
    264  }
    265  return false;
    266 }
    267 
    268 function expectObserverCalledInProcess(aTopic, aCount = 1) {
    269  let promises = [];
    270  for (let count = aCount; count > 0; count--) {
    271    promises.push(TestUtils.topicObserved(aTopic, ignoreEvent));
    272  }
    273  return promises;
    274 }
    275 
    276 function expectObserverCalled(
    277  aTopic,
    278  aCount = 1,
    279  browser = gBrowser.selectedBrowser
    280 ) {
    281  if (!gMultiProcessBrowser) {
    282    return expectObserverCalledInProcess(aTopic, aCount);
    283  }
    284 
    285  let browsingContext = Element.isInstance(browser)
    286    ? browser.browsingContext
    287    : browser;
    288 
    289  return BrowserTestUtils.contentTopicObserved(browsingContext, aTopic, aCount);
    290 }
    291 
    292 // This is a special version of expectObserverCalled that should only
    293 // be used when expecting a notification upon closing a window. It uses
    294 // the per-process message manager instead of actors to send the
    295 // notifications.
    296 function expectObserverCalledOnClose(
    297  aTopic,
    298  aCount = 1,
    299  browser = gBrowser.selectedBrowser
    300 ) {
    301  if (!gMultiProcessBrowser) {
    302    return expectObserverCalledInProcess(aTopic, aCount);
    303  }
    304 
    305  let browsingContext = Element.isInstance(browser)
    306    ? browser.browsingContext
    307    : browser;
    308 
    309  return new Promise(resolve => {
    310    BrowserTestUtils.sendAsyncMessage(
    311      browsingContext,
    312      "BrowserTestUtils:ObserveTopic",
    313      {
    314        topic: aTopic,
    315        count: 1,
    316        filterFunctionSource: ((subject, topic) => {
    317          Services.cpmm.sendAsyncMessage("WebRTCTest:ObserverCalled", {
    318            topic,
    319          });
    320          return true;
    321        }).toSource(),
    322      }
    323    );
    324 
    325    function observerCalled(message) {
    326      if (message.data.topic == aTopic) {
    327        Services.ppmm.removeMessageListener(
    328          "WebRTCTest:ObserverCalled",
    329          observerCalled
    330        );
    331        resolve();
    332      }
    333    }
    334    Services.ppmm.addMessageListener(
    335      "WebRTCTest:ObserverCalled",
    336      observerCalled
    337    );
    338  });
    339 }
    340 
    341 function promiseMessage(
    342  aMessage,
    343  aAction,
    344  aCount = 1,
    345  browser = gBrowser.selectedBrowser
    346 ) {
    347  let startTime = ChromeUtils.now();
    348  let promise = ContentTask.spawn(
    349    browser,
    350    [aMessage, aCount],
    351    async function ([expectedMessage, expectedCount]) {
    352      return new Promise(resolve => {
    353        function listenForMessage({ data }) {
    354          if (
    355            (!expectedMessage || data == expectedMessage) &&
    356            --expectedCount == 0
    357          ) {
    358            content.removeEventListener("message", listenForMessage);
    359            resolve(data);
    360          }
    361        }
    362        content.addEventListener("message", listenForMessage);
    363      });
    364    }
    365  );
    366  if (aAction) {
    367    aAction();
    368  }
    369  return promise.then(data => {
    370    ChromeUtils.addProfilerMarker(
    371      "promiseMessage",
    372      { startTime, category: "Test" },
    373      data
    374    );
    375    return data;
    376  });
    377 }
    378 
    379 function promisePopupNotificationShown(aName, aAction, aWindow = window) {
    380  let startTime = ChromeUtils.now();
    381  return new Promise(resolve => {
    382    aWindow.PopupNotifications.panel.addEventListener(
    383      "popupshown",
    384      function () {
    385        ok(
    386          !!aWindow.PopupNotifications.getNotification(aName),
    387          aName + " notification shown"
    388        );
    389        ok(aWindow.PopupNotifications.isPanelOpen, "notification panel open");
    390        ok(
    391          !!aWindow.PopupNotifications.panel.firstElementChild,
    392          "notification panel populated"
    393        );
    394 
    395        executeSoon(() => {
    396          ChromeUtils.addProfilerMarker(
    397            "promisePopupNotificationShown",
    398            { startTime, category: "Test" },
    399            aName
    400          );
    401          resolve();
    402        });
    403      },
    404      { once: true }
    405    );
    406 
    407    if (aAction) {
    408      aAction();
    409    }
    410  });
    411 }
    412 
    413 async function promisePopupNotification(aName) {
    414  return TestUtils.waitForCondition(
    415    () => PopupNotifications.getNotification(aName),
    416    aName + " notification appeared"
    417  );
    418 }
    419 
    420 async function promiseNoPopupNotification(aName) {
    421  return TestUtils.waitForCondition(
    422    () => !PopupNotifications.getNotification(aName),
    423    aName + " notification removed"
    424  );
    425 }
    426 
    427 const kActionAlways = 1;
    428 const kActionDeny = 2;
    429 const kActionNever = 3;
    430 
    431 async function activateSecondaryAction(aAction) {
    432  let notification = PopupNotifications.panel.firstElementChild;
    433  switch (aAction) {
    434    case kActionNever:
    435      if (notification.notification.secondaryActions.length > 1) {
    436        // "Always Block" is the first (and only) item in the menupopup.
    437        await Promise.all([
    438          BrowserTestUtils.waitForEvent(notification.menupopup, "popupshown"),
    439          notification.menubutton.click(),
    440        ]);
    441        notification.menupopup.querySelector("menuitem").click();
    442        return;
    443      }
    444      if (!notification.checkbox.checked) {
    445        notification.checkbox.click();
    446      }
    447    // fallthrough
    448    case kActionDeny:
    449      notification.secondaryButton.click();
    450      break;
    451    case kActionAlways:
    452      if (!notification.checkbox.checked) {
    453        notification.checkbox.click();
    454      }
    455      notification.button.click();
    456      break;
    457  }
    458 }
    459 
    460 async function getMediaCaptureState() {
    461  let startTime = ChromeUtils.now();
    462 
    463  function gatherBrowsingContexts(aBrowsingContext) {
    464    let list = [aBrowsingContext];
    465 
    466    let children = aBrowsingContext.children;
    467    for (let child of children) {
    468      list.push(...gatherBrowsingContexts(child));
    469    }
    470 
    471    return list;
    472  }
    473 
    474  function combine(x, y) {
    475    if (
    476      x == Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED ||
    477      y == Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED
    478    ) {
    479      return Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED;
    480    }
    481    if (
    482      x == Ci.nsIMediaManagerService.STATE_CAPTURE_DISABLED ||
    483      y == Ci.nsIMediaManagerService.STATE_CAPTURE_DISABLED
    484    ) {
    485      return Ci.nsIMediaManagerService.STATE_CAPTURE_DISABLED;
    486    }
    487    return Ci.nsIMediaManagerService.STATE_NOCAPTURE;
    488  }
    489 
    490  let video = Ci.nsIMediaManagerService.STATE_NOCAPTURE;
    491  let audio = Ci.nsIMediaManagerService.STATE_NOCAPTURE;
    492  let screen = Ci.nsIMediaManagerService.STATE_NOCAPTURE;
    493  let window = Ci.nsIMediaManagerService.STATE_NOCAPTURE;
    494  let browser = Ci.nsIMediaManagerService.STATE_NOCAPTURE;
    495 
    496  for (let bc of gatherBrowsingContexts(
    497    gBrowser.selectedBrowser.browsingContext
    498  )) {
    499    let state = await SpecialPowers.spawn(bc, [], async function () {
    500      let mediaManagerService = Cc[
    501        "@mozilla.org/mediaManagerService;1"
    502      ].getService(Ci.nsIMediaManagerService);
    503 
    504      let hasCamera = {};
    505      let hasMicrophone = {};
    506      let hasScreenShare = {};
    507      let hasWindowShare = {};
    508      let hasBrowserShare = {};
    509      let devices = {};
    510      mediaManagerService.mediaCaptureWindowState(
    511        content,
    512        hasCamera,
    513        hasMicrophone,
    514        hasScreenShare,
    515        hasWindowShare,
    516        hasBrowserShare,
    517        devices,
    518        false
    519      );
    520 
    521      return {
    522        video: hasCamera.value,
    523        audio: hasMicrophone.value,
    524        screen: hasScreenShare.value,
    525        window: hasWindowShare.value,
    526        browser: hasBrowserShare.value,
    527      };
    528    });
    529 
    530    video = combine(state.video, video);
    531    audio = combine(state.audio, audio);
    532    screen = combine(state.screen, screen);
    533    window = combine(state.window, window);
    534    browser = combine(state.browser, browser);
    535  }
    536 
    537  let result = {};
    538 
    539  if (video != Ci.nsIMediaManagerService.STATE_NOCAPTURE) {
    540    result.video = true;
    541  }
    542  if (audio != Ci.nsIMediaManagerService.STATE_NOCAPTURE) {
    543    result.audio = true;
    544  }
    545 
    546  if (screen != Ci.nsIMediaManagerService.STATE_NOCAPTURE) {
    547    result.screen = "Screen";
    548  } else if (window != Ci.nsIMediaManagerService.STATE_NOCAPTURE) {
    549    result.window = true;
    550  } else if (browser != Ci.nsIMediaManagerService.STATE_NOCAPTURE) {
    551    result.browserwindow = true;
    552  }
    553 
    554  ChromeUtils.addProfilerMarker("getMediaCaptureState", {
    555    startTime,
    556    category: "Test",
    557  });
    558  return result;
    559 }
    560 
    561 async function stopSharing(
    562  aType = "camera",
    563  aShouldKeepSharing = false,
    564  aFrameBC,
    565  aWindow = window
    566 ) {
    567  let promiseRecordingEvent = expectObserverCalled(
    568    "recording-device-events",
    569    1,
    570    aFrameBC
    571  );
    572  let observerPromise1 = expectObserverCalled(
    573    "getUserMedia:revoke",
    574    1,
    575    aFrameBC
    576  );
    577 
    578  // If we are stopping screen sharing and expect to still have another stream,
    579  // "recording-window-ended" won't be fired.
    580  let observerPromise2 = null;
    581  if (!aShouldKeepSharing) {
    582    observerPromise2 = expectObserverCalled(
    583      "recording-window-ended",
    584      1,
    585      aFrameBC
    586    );
    587  }
    588 
    589  await revokePermission(aType, aShouldKeepSharing, aFrameBC, aWindow);
    590  await promiseRecordingEvent;
    591  await observerPromise1;
    592  await observerPromise2;
    593 
    594  if (!aShouldKeepSharing) {
    595    await checkNotSharing();
    596  }
    597 }
    598 
    599 async function revokePermission(
    600  aType = "camera",
    601  aShouldKeepSharing = false,
    602  aFrameBC,
    603  aWindow = window
    604 ) {
    605  aWindow.gPermissionPanel._identityPermissionBox.click();
    606  let popup = aWindow.gPermissionPanel._permissionPopup;
    607  // If the popup gets hidden before being shown, by stray focus/activate
    608  // events, don't bother failing the test. It's enough to know that we
    609  // started showing the popup.
    610  let hiddenEvent = BrowserTestUtils.waitForEvent(popup, "popuphidden");
    611  let shownEvent = BrowserTestUtils.waitForEvent(popup, "popupshown");
    612  await Promise.race([hiddenEvent, shownEvent]);
    613  let doc = aWindow.document;
    614  let permissions = doc.getElementById("permission-popup-permission-list");
    615  let cancelButton = permissions.querySelector(
    616    ".permission-popup-permission-icon." +
    617      aType +
    618      "-icon ~ " +
    619      ".permission-popup-permission-remove-button"
    620  );
    621 
    622  cancelButton.click();
    623  popup.hidePopup();
    624 
    625  if (!aShouldKeepSharing) {
    626    await checkNotSharing();
    627  }
    628 }
    629 
    630 function getBrowsingContextForFrame(aBrowsingContext, aFrameId) {
    631  if (!aFrameId) {
    632    return aBrowsingContext;
    633  }
    634 
    635  return SpecialPowers.spawn(aBrowsingContext, [aFrameId], frameId => {
    636    return content.document.getElementById(frameId).browsingContext;
    637  });
    638 }
    639 
    640 async function getBrowsingContextsAndFrameIdsForSubFrames(
    641  aBrowsingContext,
    642  aSubFrames
    643 ) {
    644  let pendingBrowserSubFrames = [
    645    { bc: aBrowsingContext, subFrames: aSubFrames },
    646  ];
    647  let browsingContextsAndFrames = [];
    648  while (pendingBrowserSubFrames.length) {
    649    let { bc, subFrames } = pendingBrowserSubFrames.shift();
    650    for (let id of Object.keys(subFrames)) {
    651      let subBc = await getBrowsingContextForFrame(bc, id);
    652      if (subFrames[id].children) {
    653        pendingBrowserSubFrames.push({
    654          bc: subBc,
    655          subFrames: subFrames[id].children,
    656        });
    657      }
    658      if (subFrames[id].noTest) {
    659        continue;
    660      }
    661      let observeBC = subFrames[id].observe ? subBc : undefined;
    662      browsingContextsAndFrames.push({ bc: subBc, id, observeBC });
    663    }
    664  }
    665  return browsingContextsAndFrames;
    666 }
    667 
    668 /**
    669 * Test helper for getUserMedia calls.
    670 *
    671 * @param {boolean} aRequestAudio - Whether to request audio
    672 * @param {boolean} aRequestVideo - Whether to request video
    673 * @param {string} aFrameId - The ID of the frame
    674 * @param {string} aType - The type of screen sharing.
    675 * @param {BrowsingContext} aBrowsingContext - The browsing context
    676 * @param {boolean} [aBadDevice=false] - Whether to use a bad device
    677 * @param {boolean} [viaButtonClick=false] - Whether to call gUM directly or to
    678 *   request via simulated button click.
    679 * @returns {Promise} - Resolves when the gUM request has been made.
    680 */
    681 async function promiseRequestDevice(
    682  aRequestAudio,
    683  aRequestVideo,
    684  aFrameId,
    685  aType,
    686  aBrowsingContext,
    687  aBadDevice = false,
    688  viaButtonClick = false
    689 ) {
    690  info("requesting devices");
    691  let bc =
    692    aBrowsingContext ??
    693    (await getBrowsingContextForFrame(gBrowser.selectedBrowser, aFrameId));
    694 
    695  if (viaButtonClick) {
    696    return SpecialPowers.spawn(
    697      bc,
    698      [{ aRequestAudio, aRequestVideo, aType, aBadDevice }],
    699      async function (args) {
    700        let global = content.wrappedJSObject;
    701        global.queueRequestDeviceViaBtn(
    702          args.aRequestAudio,
    703          args.aRequestVideo,
    704          args.aType,
    705          args.aBadDevice
    706        );
    707        await EventUtils.synthesizeMouseAtCenter(
    708          global.document.getElementById("gum"),
    709          {},
    710          content
    711        );
    712      }
    713    );
    714  }
    715 
    716  return SpecialPowers.spawn(
    717    bc,
    718    [{ aRequestAudio, aRequestVideo, aType, aBadDevice }],
    719    async function (args) {
    720      let global = content.wrappedJSObject;
    721      global.requestDevice(
    722        args.aRequestAudio,
    723        args.aRequestVideo,
    724        args.aType,
    725        args.aBadDevice,
    726        args.withUserActivation
    727      );
    728    }
    729  );
    730 }
    731 
    732 async function promiseRequestAudioOutput(options) {
    733  info("requesting audio output");
    734  const bc = gBrowser.selectedBrowser;
    735  return SpecialPowers.spawn(bc, [options], async function (opts) {
    736    const global = content.wrappedJSObject;
    737    global.requestAudioOutput(Cu.cloneInto(opts, content));
    738  });
    739 }
    740 
    741 async function stopTracks(
    742  aKind,
    743  aAlreadyStopped,
    744  aLastTracks,
    745  aFrameId,
    746  aBrowsingContext,
    747  aBrowsingContextToObserve
    748 ) {
    749  // If the observers are listening to other frames, listen for a notification
    750  // on the right subframe.
    751  let frameBC =
    752    aBrowsingContext ??
    753    (await getBrowsingContextForFrame(
    754      gBrowser.selectedBrowser.browsingContext,
    755      aFrameId
    756    ));
    757 
    758  let observerPromises = [];
    759  if (!aAlreadyStopped) {
    760    observerPromises.push(
    761      expectObserverCalled(
    762        "recording-device-events",
    763        1,
    764        aBrowsingContextToObserve
    765      )
    766    );
    767  }
    768  if (aLastTracks) {
    769    observerPromises.push(
    770      expectObserverCalled(
    771        "recording-window-ended",
    772        1,
    773        aBrowsingContextToObserve
    774      )
    775    );
    776  }
    777 
    778  info(`Stopping all ${aKind} tracks`);
    779  await SpecialPowers.spawn(frameBC, [aKind], async function (kind) {
    780    content.wrappedJSObject.stopTracks(kind);
    781  });
    782 
    783  await Promise.all(observerPromises);
    784 }
    785 
    786 async function closeStream(
    787  aAlreadyClosed,
    788  aFrameId,
    789  aDontFlushObserverVerification,
    790  aBrowsingContext,
    791  aBrowsingContextToObserve
    792 ) {
    793  // Check that spurious notifications that occur while closing the
    794  // stream are handled separately. Tests that use skipObserverVerification
    795  // should pass true for aDontFlushObserverVerification.
    796  if (!aDontFlushObserverVerification) {
    797    await disableObserverVerification();
    798    await enableObserverVerification();
    799  }
    800 
    801  // If the observers are listening to other frames, listen for a notification
    802  // on the right subframe.
    803  let frameBC =
    804    aBrowsingContext ??
    805    (await getBrowsingContextForFrame(
    806      gBrowser.selectedBrowser.browsingContext,
    807      aFrameId
    808    ));
    809 
    810  let observerPromises = [];
    811  if (!aAlreadyClosed) {
    812    observerPromises.push(
    813      expectObserverCalled(
    814        "recording-device-events",
    815        1,
    816        aBrowsingContextToObserve
    817      )
    818    );
    819    observerPromises.push(
    820      expectObserverCalled(
    821        "recording-window-ended",
    822        1,
    823        aBrowsingContextToObserve
    824      )
    825    );
    826  }
    827 
    828  info("closing the stream");
    829  await SpecialPowers.spawn(frameBC, [], async function () {
    830    content.wrappedJSObject.closeStream();
    831  });
    832 
    833  await Promise.all(observerPromises);
    834 
    835  await assertWebRTCIndicatorStatus(null);
    836 }
    837 
    838 async function reloadAsUser() {
    839  info("reloading as a user");
    840 
    841  const reloadButton = document.getElementById("reload-button");
    842  await TestUtils.waitForCondition(() => !reloadButton.disabled);
    843  // Disable observers as the page is being reloaded which can destroy
    844  // the actors listening to the notifications.
    845  await disableObserverVerification();
    846 
    847  let loadedPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
    848  reloadButton.click();
    849  await loadedPromise;
    850 
    851  await enableObserverVerification();
    852 }
    853 
    854 async function reloadFromContent() {
    855  info("reloading from content");
    856 
    857  // Disable observers as the page is being reloaded which can destroy
    858  // the actors listening to the notifications.
    859  await disableObserverVerification();
    860 
    861  let loadedPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
    862  await ContentTask.spawn(gBrowser.selectedBrowser, null, () =>
    863    content.location.reload()
    864  );
    865 
    866  await loadedPromise;
    867 
    868  await enableObserverVerification();
    869 }
    870 
    871 async function reloadAndAssertClosedStreams() {
    872  await reloadFromContent();
    873  await checkNotSharing();
    874 }
    875 
    876 /**
    877 * @param {("microphone"|"camera"|"screen")[]} aExpectedTypes
    878 * @param {Window} [aWindow]
    879 */
    880 function checkDeviceSelectors(aExpectedTypes, aWindow = window) {
    881  for (const type of aExpectedTypes) {
    882    if (!["microphone", "camera", "screen", "speaker"].includes(type)) {
    883      throw new Error(`Bad device type name ${type}`);
    884    }
    885  }
    886  let document = aWindow.document;
    887 
    888  let expectedDescribedBy = "webRTC-shareDevices-notification-description";
    889  for (let type of ["Camera", "Microphone", "Speaker"]) {
    890    let selector = document.getElementById(`webRTC-select${type}`);
    891    if (!aExpectedTypes.includes(type.toLowerCase())) {
    892      ok(selector.hidden, `${type} selector hidden`);
    893      continue;
    894    }
    895    ok(!selector.hidden, `${type} selector visible`);
    896    let tagName = type == "Speaker" ? "richlistbox" : "menulist";
    897    let selectorList = document.getElementById(
    898      `webRTC-select${type}-${tagName}`
    899    );
    900    let label = document.getElementById(
    901      `webRTC-select${type}-single-device-label`
    902    );
    903    // If there's only 1 device listed, then we should show the label
    904    // instead of the menulist.
    905    if (selectorList.itemCount == 1) {
    906      ok(selectorList.hidden, `${type} selector list should be hidden.`);
    907      ok(!label.hidden, `${type} selector label should not be hidden.`);
    908      let itemLabel =
    909        tagName == "richlistbox"
    910          ? selectorList.selectedItem.firstElementChild.getAttribute("value")
    911          : selectorList.selectedItem.getAttribute("label");
    912      is(
    913        label.value,
    914        itemLabel,
    915        `${type} label should be showing the lone device label.`
    916      );
    917      expectedDescribedBy += ` webRTC-select${type}-icon webRTC-select${type}-single-device-label`;
    918    } else {
    919      ok(!selectorList.hidden, `${type} selector list should not be hidden.`);
    920      ok(label.hidden, `${type} selector label should be hidden.`);
    921    }
    922  }
    923  let ariaDescribedby =
    924    aWindow.PopupNotifications.panel.getAttribute("aria-describedby");
    925  is(ariaDescribedby, expectedDescribedBy, "aria-describedby");
    926 
    927  let screenSelector = document.getElementById("webRTC-selectWindowOrScreen");
    928  if (aExpectedTypes.includes("screen")) {
    929    ok(!screenSelector.hidden, "screen selector visible");
    930  } else {
    931    ok(screenSelector.hidden, "screen selector hidden");
    932  }
    933 }
    934 
    935 /**
    936 * Tests the siteIdentity icons, the permission panel and the global indicator
    937 * UI state.
    938 *
    939 * @param {object} aExpected - Expected state for the current tab.
    940 * @param {window} [aWin] - Top level chrome window to test state of.
    941 * @param {object} [aExpectedGlobal] - Expected state for all tabs.
    942 * @param {object} [aExpectedPerm] - Expected permission states keyed by device
    943 * type.
    944 */
    945 async function checkSharingUI(
    946  aExpected,
    947  aWin = window,
    948  aExpectedGlobal = null,
    949  aExpectedPerm = null
    950 ) {
    951  function isPaused(streamState) {
    952    if (typeof streamState == "string") {
    953      return streamState.includes("Paused");
    954    }
    955    return streamState == STATE_CAPTURE_DISABLED;
    956  }
    957 
    958  let doc = aWin.document;
    959  // First check the icon above the control center (i) icon.
    960  let permissionBox = doc.getElementById("identity-permission-box");
    961  let webrtcSharingIcon = doc.getElementById("webrtc-sharing-icon");
    962  let expectOn = aExpected.audio || aExpected.video || aExpected.screen;
    963  if (expectOn) {
    964    ok(webrtcSharingIcon.hasAttribute("sharing"), "sharing attribute is set");
    965  } else {
    966    ok(
    967      !webrtcSharingIcon.hasAttribute("sharing"),
    968      "sharing attribute is not set"
    969    );
    970  }
    971  let sharing = webrtcSharingIcon.getAttribute("sharing");
    972  if (!IsIndicatorDisabled) {
    973    if (aExpected.screen) {
    974      is(sharing, "screen", "showing screen icon in the identity block");
    975    } else if (aExpected.video == STATE_CAPTURE_ENABLED) {
    976      is(sharing, "camera", "showing camera icon in the identity block");
    977    } else if (aExpected.audio == STATE_CAPTURE_ENABLED) {
    978      is(sharing, "microphone", "showing mic icon in the identity block");
    979    } else if (aExpected.video) {
    980      is(sharing, "camera", "showing camera icon in the identity block");
    981    } else if (aExpected.audio) {
    982      is(sharing, "microphone", "showing mic icon in the identity block");
    983    }
    984  }
    985 
    986  let allStreamsPaused = Object.values(aExpected).every(isPaused);
    987  is(
    988    webrtcSharingIcon.hasAttribute("paused"),
    989    allStreamsPaused,
    990    "sharing icon(s) should be in paused state when paused"
    991  );
    992 
    993  // Then check the sharing indicators inside the permission popup.
    994  permissionBox.click();
    995  let popup = aWin.gPermissionPanel._permissionPopup;
    996  // If the popup gets hidden before being shown, by stray focus/activate
    997  // events, don't bother failing the test. It's enough to know that we
    998  // started showing the popup.
    999  let hiddenEvent = BrowserTestUtils.waitForEvent(popup, "popuphidden");
   1000  let shownEvent = BrowserTestUtils.waitForEvent(popup, "popupshown");
   1001  await Promise.race([hiddenEvent, shownEvent]);
   1002  let permissions = doc.getElementById("permission-popup-permission-list");
   1003  for (let id of ["microphone", "camera", "screen"]) {
   1004    let convertId = idToConvert => {
   1005      if (idToConvert == "camera") {
   1006        return "video";
   1007      }
   1008      if (idToConvert == "microphone") {
   1009        return "audio";
   1010      }
   1011      return idToConvert;
   1012    };
   1013    let expected = aExpected[convertId(id)];
   1014 
   1015    // Extract the expected permission for the device type.
   1016    // Defaults to temporary allow.
   1017    let { state, scope } = aExpectedPerm?.[convertId(id)] || {};
   1018    if (state == null) {
   1019      state = SitePermissions.ALLOW;
   1020    }
   1021    if (scope == null) {
   1022      scope = SitePermissions.SCOPE_TEMPORARY;
   1023    }
   1024 
   1025    is(
   1026      !!aWin.gPermissionPanel._sharingState.webRTC[id],
   1027      !!expected,
   1028      "sharing state for " + id + " as expected"
   1029    );
   1030    let item = permissions.querySelectorAll(
   1031      ".permission-popup-permission-item-" + id
   1032    );
   1033    let stateLabel = item?.[0]?.querySelector(
   1034      ".permission-popup-permission-state-label"
   1035    );
   1036    let icon = permissions.querySelectorAll(
   1037      ".permission-popup-permission-icon." + id + "-icon"
   1038    );
   1039    if (expected) {
   1040      is(item.length, 1, "should show " + id + " item in permission panel");
   1041      is(
   1042        stateLabel?.textContent,
   1043        SitePermissions.getCurrentStateLabel(state, id, scope),
   1044        "should show correct item label for " + id
   1045      );
   1046      is(icon.length, 1, "should show " + id + " icon in permission panel");
   1047      is(
   1048        icon[0].classList.contains("in-use"),
   1049        expected && !isPaused(expected),
   1050        "icon should have the in-use class, unless paused"
   1051      );
   1052    } else if (!icon.length && !item.length && !stateLabel) {
   1053      ok(true, "should not show " + id + " item in the permission panel");
   1054      ok(true, "should not show " + id + " icon in the permission panel");
   1055      ok(
   1056        true,
   1057        "should not show " + id + " state label in the permission panel"
   1058      );
   1059      if (state != SitePermissions.PROMPT || SHOW_ALWAYS_ASK) {
   1060        isnot(
   1061          scope,
   1062          SitePermissions.SCOPE_PERSISTENT,
   1063          "persistent permission not shown"
   1064        );
   1065      }
   1066    } else {
   1067      // This will happen if there are persistent permissions set.
   1068      ok(
   1069        !icon[0].classList.contains("in-use"),
   1070        "if shown, the " + id + " icon should not have the in-use class"
   1071      );
   1072      is(item.length, 1, "should not show more than 1 " + id + " item");
   1073      is(icon.length, 1, "should not show more than 1 " + id + " icon");
   1074 
   1075      // Note: To pass, this one needs state and/or scope passed into
   1076      // checkSharingUI() for values other than ALLOW and SCOPE_TEMPORARY
   1077      is(
   1078        stateLabel?.textContent,
   1079        SitePermissions.getCurrentStateLabel(state, id, scope),
   1080        "should show correct item label for " + id
   1081      );
   1082      if (!SHOW_ALWAYS_ASK) {
   1083        isnot(
   1084          state,
   1085          state == SitePermissions.PROMPT,
   1086          "always ask permission should be hidden"
   1087        );
   1088      }
   1089    }
   1090  }
   1091  aWin.gPermissionPanel._permissionPopup.hidePopup();
   1092  await TestUtils.waitForCondition(
   1093    () => permissionPopupHidden(aWin),
   1094    "identity popup should be hidden"
   1095  );
   1096 
   1097  // Check the global indicators.
   1098  if (expectOn) {
   1099    await assertWebRTCIndicatorStatus(aExpectedGlobal || aExpected);
   1100  }
   1101 }
   1102 
   1103 async function checkNotSharing() {
   1104  Assert.deepEqual(
   1105    await getMediaCaptureState(),
   1106    {},
   1107    "expected nothing to be shared"
   1108  );
   1109 
   1110  ok(
   1111    !document.getElementById("webrtc-sharing-icon").hasAttribute("sharing"),
   1112    "no sharing indicator on the control center icon"
   1113  );
   1114 
   1115  await assertWebRTCIndicatorStatus(null);
   1116 }
   1117 
   1118 async function checkNotSharingWithinGracePeriod() {
   1119  Assert.deepEqual(
   1120    await getMediaCaptureState(),
   1121    {},
   1122    "expected nothing to be shared"
   1123  );
   1124 
   1125  ok(
   1126    document.getElementById("webrtc-sharing-icon").hasAttribute("sharing"),
   1127    "has sharing indicator on the control center icon"
   1128  );
   1129  ok(
   1130    document.getElementById("webrtc-sharing-icon").hasAttribute("paused"),
   1131    "sharing indicator is paused"
   1132  );
   1133 
   1134  await assertWebRTCIndicatorStatus(null);
   1135 }
   1136 
   1137 async function promiseReloadFrame(aFrameId, aBrowsingContext) {
   1138  let loadedPromise = BrowserTestUtils.browserLoaded(
   1139    gBrowser.selectedBrowser,
   1140    true,
   1141    () => {
   1142      return true;
   1143    }
   1144  );
   1145  let bc =
   1146    aBrowsingContext ??
   1147    (await getBrowsingContextForFrame(
   1148      gBrowser.selectedBrowser.browsingContext,
   1149      aFrameId
   1150    ));
   1151  await SpecialPowers.spawn(bc, [], async function () {
   1152    content.location.reload();
   1153  });
   1154  return loadedPromise;
   1155 }
   1156 
   1157 function promiseChangeLocationFrame(aFrameId, aNewLocation) {
   1158  return SpecialPowers.spawn(
   1159    gBrowser.selectedBrowser.browsingContext,
   1160    [{ aFrameId, aNewLocation }],
   1161    async function (args) {
   1162      let frame = content.wrappedJSObject.document.getElementById(
   1163        args.aFrameId
   1164      );
   1165      return new Promise(resolve => {
   1166        function listener() {
   1167          frame.removeEventListener("load", listener, true);
   1168          resolve();
   1169        }
   1170        frame.addEventListener("load", listener, true);
   1171 
   1172        content.wrappedJSObject.document.getElementById(
   1173          args.aFrameId
   1174        ).contentWindow.location = args.aNewLocation;
   1175      });
   1176    }
   1177  );
   1178 }
   1179 
   1180 async function openNewTestTab(leaf = "get_user_media.html") {
   1181  let rootDir = getRootDirectory(gTestPath);
   1182  rootDir = rootDir.replace(
   1183    "chrome://mochitests/content/",
   1184    "https://example.com/"
   1185  );
   1186  let absoluteURI = rootDir + leaf;
   1187 
   1188  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, absoluteURI);
   1189  return tab.linkedBrowser;
   1190 }
   1191 
   1192 // Enabling observer verification adds listeners for all of the webrtc
   1193 // observer topics. If any notifications occur for those topics that
   1194 // were not explicitly requested, a failure will occur.
   1195 async function enableObserverVerification(browser = gBrowser.selectedBrowser) {
   1196  // Skip these checks in single process mode as it isn't worth implementing it.
   1197  if (!gMultiProcessBrowser) {
   1198    return;
   1199  }
   1200 
   1201  gBrowserContextsToObserve = [browser.browsingContext];
   1202 
   1203  // A list of subframe indicies to also add observers to. This only
   1204  // supports one nested level.
   1205  if (gObserveSubFrames) {
   1206    let bcsAndFrameIds = await getBrowsingContextsAndFrameIdsForSubFrames(
   1207      browser,
   1208      gObserveSubFrames
   1209    );
   1210    for (let { observeBC } of bcsAndFrameIds) {
   1211      if (observeBC) {
   1212        gBrowserContextsToObserve.push(observeBC);
   1213      }
   1214    }
   1215  }
   1216 
   1217  for (let bc of gBrowserContextsToObserve) {
   1218    await BrowserTestUtils.startObservingTopics(bc, observerTopics);
   1219  }
   1220 }
   1221 
   1222 async function disableObserverVerification() {
   1223  if (!gMultiProcessBrowser) {
   1224    return;
   1225  }
   1226 
   1227  for (let bc of gBrowserContextsToObserve) {
   1228    await BrowserTestUtils.stopObservingTopics(bc, observerTopics).catch(
   1229      reason => {
   1230        ok(false, "Failed " + reason);
   1231      }
   1232    );
   1233  }
   1234 }
   1235 
   1236 function permissionPopupHidden(win = window) {
   1237  let popup = win.gPermissionPanel._permissionPopup;
   1238  return !popup || popup.state == "closed";
   1239 }
   1240 
   1241 async function runTests(tests, options = {}) {
   1242  let browser = await openNewTestTab(options.relativeURI);
   1243 
   1244  is(
   1245    PopupNotifications._currentNotifications.length,
   1246    0,
   1247    "should start the test without any prior popup notification"
   1248  );
   1249  ok(
   1250    permissionPopupHidden(),
   1251    "should start the test with the permission panel hidden"
   1252  );
   1253 
   1254  // Set prefs so that permissions prompts are shown and loopback devices
   1255  // are not used. To test the chrome we want prompts to be shown, and
   1256  // these tests are flakey when using loopback devices (though it would
   1257  // be desirable to make them work with loopback in future). See bug 1643711.
   1258  let prefs = [
   1259    [PREF_PERMISSION_FAKE, true],
   1260    [PREF_AUDIO_LOOPBACK, ""],
   1261    [PREF_VIDEO_LOOPBACK, ""],
   1262    [PREF_FAKE_STREAMS, true],
   1263    [PREF_FOCUS_SOURCE, false],
   1264  ];
   1265  await SpecialPowers.pushPrefEnv({ set: prefs });
   1266 
   1267  // When the frames are in different processes, add observers to each frame,
   1268  // to ensure that the notifications don't get sent in the wrong process.
   1269  gObserveSubFrames = SpecialPowers.useRemoteSubframes ? options.subFrames : {};
   1270 
   1271  for (let testCase of tests) {
   1272    let startTime = ChromeUtils.now();
   1273    info(testCase.desc);
   1274    if (
   1275      !testCase.skipObserverVerification &&
   1276      !options.skipObserverVerification
   1277    ) {
   1278      await enableObserverVerification();
   1279    }
   1280    await testCase.run(browser, options.subFrames);
   1281    if (
   1282      !testCase.skipObserverVerification &&
   1283      !options.skipObserverVerification
   1284    ) {
   1285      await disableObserverVerification();
   1286    }
   1287    if (options.cleanup) {
   1288      await options.cleanup();
   1289    }
   1290    ChromeUtils.addProfilerMarker(
   1291      "browser-test",
   1292      { startTime, category: "Test" },
   1293      testCase.desc
   1294    );
   1295  }
   1296 
   1297  // Some tests destroy the original tab and leave a new one in its place.
   1298  BrowserTestUtils.removeTab(gBrowser.selectedTab);
   1299 }
   1300 
   1301 /**
   1302 * Given a browser from a tab in this window, chooses to share
   1303 * some combination of camera, mic or screen.
   1304 *
   1305 * @param {<xul:browser} browser - The browser to share devices with.
   1306 * @param {boolean} camera - True to share a camera device.
   1307 * @param {boolean} mic - True to share a microphone device.
   1308 * @param {number} [screenOrWin] - One of either SHARE_WINDOW or SHARE_SCREEN
   1309 *   to share a window or screen. Defaults to neither.
   1310 * @param {boolean} remember - True to persist the permission to the
   1311 *   SitePermissions database as SitePermissions.SCOPE_PERSISTENT. Note that
   1312 *   callers are responsible for clearing this persistent permission.
   1313 * @returns {Promise<void>}
   1314 *   Resolves once sharing is complete.
   1315 */
   1316 async function shareDevices(
   1317  browser,
   1318  camera,
   1319  mic,
   1320  screenOrWin = 0,
   1321  remember = false
   1322 ) {
   1323  if (camera || mic) {
   1324    let promise = promisePopupNotificationShown(
   1325      "webRTC-shareDevices",
   1326      null,
   1327      window
   1328    );
   1329 
   1330    await promiseRequestDevice(mic, camera, null, null, browser);
   1331    await promise;
   1332 
   1333    const expectedDeviceSelectorTypes = [
   1334      camera && "camera",
   1335      mic && "microphone",
   1336    ].filter(x => x);
   1337    checkDeviceSelectors(expectedDeviceSelectorTypes);
   1338    let observerPromise1 = expectObserverCalled("getUserMedia:response:allow");
   1339    let observerPromise2 = expectObserverCalled("recording-device-events");
   1340 
   1341    let rememberCheck = PopupNotifications.panel.querySelector(
   1342      ".popup-notification-checkbox"
   1343    );
   1344    rememberCheck.checked = remember;
   1345 
   1346    promise = promiseMessage("ok", () => {
   1347      PopupNotifications.panel.firstElementChild.button.click();
   1348    });
   1349 
   1350    await observerPromise1;
   1351    await observerPromise2;
   1352    await promise;
   1353  }
   1354 
   1355  if (screenOrWin) {
   1356    let promise = promisePopupNotificationShown(
   1357      "webRTC-shareDevices",
   1358      null,
   1359      window
   1360    );
   1361 
   1362    await promiseRequestDevice(false, true, null, "screen", browser);
   1363    await promise;
   1364 
   1365    checkDeviceSelectors(["screen"], window);
   1366 
   1367    let document = window.document;
   1368 
   1369    let menulist = document.getElementById("webRTC-selectWindow-menulist");
   1370    let displayMediaSource;
   1371 
   1372    if (screenOrWin == SHARE_SCREEN) {
   1373      displayMediaSource = "screen";
   1374    } else if (screenOrWin == SHARE_WINDOW) {
   1375      displayMediaSource = "window";
   1376    } else {
   1377      throw new Error("Got an invalid argument to shareDevices.");
   1378    }
   1379 
   1380    let menuitem = null;
   1381    for (let i = 0; i < menulist.itemCount; ++i) {
   1382      let current = menulist.getItemAtIndex(i);
   1383      if (current.mediaSource == displayMediaSource) {
   1384        menuitem = current;
   1385        break;
   1386      }
   1387    }
   1388 
   1389    Assert.ok(menuitem, "Should have found an appropriate display menuitem");
   1390    menuitem.doCommand();
   1391 
   1392    let notification = window.PopupNotifications.panel.firstElementChild;
   1393 
   1394    let observerPromise1 = expectObserverCalled("getUserMedia:response:allow");
   1395    let observerPromise2 = expectObserverCalled("recording-device-events");
   1396    await promiseMessage(
   1397      "ok",
   1398      () => {
   1399        notification.button.click();
   1400      },
   1401      1,
   1402      browser
   1403    );
   1404    await observerPromise1;
   1405    await observerPromise2;
   1406  }
   1407 }