tor-browser

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

browser_devices_select_audio_output.js (8611B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 requestLongerTimeout(2);
      6 
      7 const permissionError =
      8  "error: NotAllowedError: The request is not allowed " +
      9  "by the user agent or the platform in the current context.";
     10 
     11 async function requestAudioOutput(options) {
     12  await Promise.all([
     13    expectObserverCalled("getUserMedia:request"),
     14    expectObserverCalled("recording-window-ended"),
     15    promiseRequestAudioOutput(options),
     16  ]);
     17 }
     18 
     19 async function requestAudioOutputExpectingPrompt(options) {
     20  await Promise.all([
     21    promisePopupNotificationShown("webRTC-shareDevices"),
     22    requestAudioOutput(options),
     23  ]);
     24 
     25  is(
     26    PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
     27    "webRTC-shareSpeaker-notification-icon",
     28    "anchored to device icon"
     29  );
     30  checkDeviceSelectors(["speaker"]);
     31 }
     32 
     33 async function requestAudioOutputExpectingDeny(options) {
     34  await Promise.all([
     35    requestAudioOutput(options),
     36    expectObserverCalled("getUserMedia:response:deny"),
     37    promiseMessage(permissionError),
     38  ]);
     39 }
     40 
     41 async function simulateAudioOutputRequest(options) {
     42  await SpecialPowers.spawn(
     43    gBrowser.selectedBrowser,
     44    [options],
     45    function simPrompt({ deviceCount, deviceId }) {
     46      const nsIMediaDeviceQI = ChromeUtils.generateQI([Ci.nsIMediaDevice]);
     47      const devices = [...Array(deviceCount).keys()].map(i => ({
     48        type: "audiooutput",
     49        rawName: `name ${i}`,
     50        rawId: `rawId ${i}`,
     51        id: `id ${i}`,
     52        QueryInterface: nsIMediaDeviceQI,
     53      }));
     54      const req = {
     55        type: "selectaudiooutput",
     56        windowID: content.windowGlobalChild.outerWindowId,
     57        devices,
     58        getConstraints: () => ({}),
     59        getAudioOutputOptions: () => ({ deviceId }),
     60        isSecure: true,
     61        isHandlingUserInput: true,
     62      };
     63      const { WebRTCChild } = SpecialPowers.ChromeUtils.importESModule(
     64        "resource:///actors/WebRTCChild.sys.mjs"
     65      );
     66      WebRTCChild.observe(req, "getUserMedia:request");
     67    }
     68  );
     69 }
     70 
     71 async function allowPrompt() {
     72  const observerPromise = expectObserverCalled("getUserMedia:response:allow");
     73  PopupNotifications.panel.firstElementChild.button.click();
     74  await observerPromise;
     75 }
     76 
     77 async function allow() {
     78  await Promise.all([promiseMessage("ok"), allowPrompt()]);
     79 }
     80 
     81 async function denyPrompt() {
     82  const observerPromise = expectObserverCalled("getUserMedia:response:deny");
     83  activateSecondaryAction(kActionDeny);
     84  await observerPromise;
     85 }
     86 
     87 async function deny() {
     88  await Promise.all([promiseMessage(permissionError), denyPrompt()]);
     89 }
     90 
     91 async function escapePrompt() {
     92  const observerPromise = expectObserverCalled("getUserMedia:response:deny");
     93  EventUtils.synthesizeKey("KEY_Escape");
     94  await observerPromise;
     95 }
     96 
     97 async function escape() {
     98  await Promise.all([promiseMessage(permissionError), escapePrompt()]);
     99 }
    100 
    101 var gTests = [
    102  {
    103    desc: 'User clicks "Allow" and revokes',
    104    run: async function checkAllow() {
    105      await requestAudioOutputExpectingPrompt();
    106      await allow();
    107 
    108      info("selectAudioOutput() with no deviceId again should prompt again.");
    109      await requestAudioOutputExpectingPrompt();
    110      await allow();
    111 
    112      info("selectAudioOutput() with same deviceId should not prompt again.");
    113      await Promise.all([
    114        expectObserverCalled("getUserMedia:response:allow"),
    115        promiseMessage("ok"),
    116        requestAudioOutput({ requestSameDevice: true }),
    117      ]);
    118 
    119      await revokePermission("speaker", true);
    120      info("Same deviceId should prompt again after revoked permission.");
    121      await requestAudioOutputExpectingPrompt({ requestSameDevice: true });
    122      await allow();
    123      await revokePermission("speaker", true);
    124    },
    125  },
    126  {
    127    desc: 'User clicks "Not Now"',
    128    run: async function checkNotNow() {
    129      await requestAudioOutputExpectingPrompt();
    130      is(
    131        PopupNotifications.getNotification("webRTC-shareDevices")
    132          .secondaryActions[0].label,
    133        "Not now",
    134        "first secondary action label"
    135      );
    136      await deny();
    137      info("selectAudioOutput() after Not Now should prompt again.");
    138      await requestAudioOutputExpectingPrompt();
    139      await escape();
    140    },
    141  },
    142  {
    143    desc: 'User presses "Esc"',
    144    run: async function checkEsc() {
    145      await requestAudioOutputExpectingPrompt();
    146      await escape();
    147      info("selectAudioOutput() after Esc should prompt again.");
    148      await requestAudioOutputExpectingPrompt();
    149      await allow();
    150      await revokePermission("speaker", true);
    151    },
    152  },
    153  {
    154    desc: 'User clicks "Always Block"',
    155    run: async function checkAlwaysBlock() {
    156      await requestAudioOutputExpectingPrompt();
    157      await Promise.all([
    158        expectObserverCalled("getUserMedia:response:deny"),
    159        promiseMessage(permissionError),
    160        activateSecondaryAction(kActionNever),
    161      ]);
    162      info("selectAudioOutput() after Always Block should not prompt again.");
    163      await requestAudioOutputExpectingDeny();
    164      await revokePermission("speaker", true);
    165    },
    166  },
    167  {
    168    desc: "Single Device",
    169    run: async function checkSingle() {
    170      await Promise.all([
    171        promisePopupNotificationShown("webRTC-shareDevices"),
    172        simulateAudioOutputRequest({ deviceCount: 1 }),
    173      ]);
    174      is(
    175        document.activeElement.className,
    176        "popup-notification-primary-button primary footer-button",
    177        "popup button focus"
    178      );
    179      checkDeviceSelectors(["speaker"]);
    180      await escapePrompt();
    181    },
    182  },
    183  {
    184    desc: "Multi Device with deviceId",
    185    run: async function checkMulti() {
    186      const deviceCount = 4;
    187      await Promise.all([
    188        promisePopupNotificationShown("webRTC-shareDevices"),
    189        simulateAudioOutputRequest({ deviceCount, deviceId: "id 2" }),
    190      ]);
    191      const selectorList = document.getElementById(
    192        `webRTC-selectSpeaker-richlistbox`
    193      );
    194      is(selectorList.selectedIndex, 2, "pre-selected index");
    195      ok(selectorList.contains(document.activeElement), "richlistbox focus");
    196      checkDeviceSelectors(["speaker"]);
    197      await allowPrompt();
    198 
    199      info("Expect same-device request allowed without prompt");
    200      await Promise.all([
    201        expectObserverCalled("getUserMedia:response:allow"),
    202        simulateAudioOutputRequest({ deviceCount, deviceId: "id 2" }),
    203      ]);
    204 
    205      info("Expect prompt for different-device request");
    206      await Promise.all([
    207        promisePopupNotificationShown("webRTC-shareDevices"),
    208        simulateAudioOutputRequest({ deviceCount, deviceId: "id 1" }),
    209      ]);
    210      await denyPrompt();
    211 
    212      info("Expect prompt again for denied-device request");
    213      await Promise.all([
    214        promisePopupNotificationShown("webRTC-shareDevices"),
    215        simulateAudioOutputRequest({ deviceCount, deviceId: "id 1" }),
    216      ]);
    217      is(selectorList.selectedIndex, 1, "pre-selected index");
    218      info("Expect allow from double click");
    219      const targetIndex = 2;
    220      const target = selectorList.getItemAtIndex(targetIndex);
    221      EventUtils.synthesizeMouseAtCenter(target, { clickCount: 1 });
    222      is(selectorList.selectedIndex, targetIndex, "selected index after click");
    223      const messagePromise = promiseMessage();
    224      const observerPromise = BrowserTestUtils.contentTopicObserved(
    225        gBrowser.selectedBrowser.browsingContext,
    226        "getUserMedia:response:allow",
    227        1,
    228        aSubject => {
    229          const device = aSubject
    230            .QueryInterface(Ci.nsIArrayExtensions)
    231            .GetElementAt(0).wrappedJSObject;
    232          // content defined by BrowserTestUtilsChild.
    233          content.wrappedJSObject.message(device.id);
    234          return true;
    235        }
    236      );
    237      EventUtils.synthesizeMouseAtCenter(target, { clickCount: 2 });
    238      await observerPromise;
    239      const id = await messagePromise;
    240      is(id, `id ${targetIndex}`, "selected device");
    241 
    242      await revokePermission("speaker", true);
    243    },
    244  },
    245  {
    246    desc: "SitePermissions speaker block",
    247    run: async function checkPermissionsBlock() {
    248      SitePermissions.setForPrincipal(
    249        gBrowser.contentPrincipal,
    250        "speaker",
    251        SitePermissions.BLOCK
    252      );
    253      await requestAudioOutputExpectingDeny();
    254      SitePermissions.removeFromPrincipal(gBrowser.contentPrincipal, "speaker");
    255    },
    256  },
    257 ];
    258 
    259 add_task(async function test() {
    260  await SpecialPowers.pushPrefEnv({ set: [["media.setsinkid.enabled", true]] });
    261  await runTests(gTests);
    262 });