summaryrefslogtreecommitdiffstats
path: root/browser/base/content/test/webrtc
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
commit43a97878ce14b72f0981164f87f2e35e14151312 (patch)
tree620249daf56c0258faa40cbdcf9cfba06de2a846 /browser/base/content/test/webrtc
parentInitial commit. (diff)
downloadfirefox-upstream.tar.xz
firefox-upstream.zip
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--browser/base/content/test/webrtc/browser.ini113
-rw-r--r--browser/base/content/test/webrtc/browser_WebrtcGlobalInformation.js275
-rw-r--r--browser/base/content/test/webrtc/browser_device_controls_menus.js55
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media.js948
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_anim.js106
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_by_device_id.js82
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_default_permissions.js209
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_grace.js388
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_in_frame.js752
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_in_xorigin_frame.js804
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_in_xorigin_frame_chain.js252
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_multi_process.js518
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_paused.js1005
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_queue_request.js386
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_screen.js915
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_screen_tab_close.js73
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_tear_off_tab.js100
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_unprompted_access.js666
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_unprompted_access_in_frame.js309
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_unprompted_access_queue_request.js47
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_unprompted_access_tear_off_tab.js108
-rw-r--r--browser/base/content/test/webrtc/browser_devices_select_audio_output.js208
-rw-r--r--browser/base/content/test/webrtc/browser_global_mute_toggles.js293
-rw-r--r--browser/base/content/test/webrtc/browser_indicator_popuphiding.js50
-rw-r--r--browser/base/content/test/webrtc/browser_notification_silencing.js231
-rw-r--r--browser/base/content/test/webrtc/browser_stop_sharing_button.js175
-rw-r--r--browser/base/content/test/webrtc/browser_stop_streams_on_indicator_close.js215
-rw-r--r--browser/base/content/test/webrtc/browser_tab_switch_warning.js538
-rw-r--r--browser/base/content/test/webrtc/browser_webrtc_hooks.js373
-rw-r--r--browser/base/content/test/webrtc/get_user_media.html124
-rw-r--r--browser/base/content/test/webrtc/get_user_media2.html107
-rw-r--r--browser/base/content/test/webrtc/get_user_media_in_frame.html98
-rw-r--r--browser/base/content/test/webrtc/get_user_media_in_xorigin_frame.html71
-rw-r--r--browser/base/content/test/webrtc/get_user_media_in_xorigin_frame_ancestor.html12
-rw-r--r--browser/base/content/test/webrtc/gracePeriod/browser.ini15
-rw-r--r--browser/base/content/test/webrtc/head.js1342
-rw-r--r--browser/base/content/test/webrtc/legacyIndicator/browser.ini56
-rw-r--r--browser/base/content/test/webrtc/peerconnection_connect.html39
-rw-r--r--browser/base/content/test/webrtc/single_peerconnection.html23
39 files changed, 12081 insertions, 0 deletions
diff --git a/browser/base/content/test/webrtc/browser.ini b/browser/base/content/test/webrtc/browser.ini
new file mode 100644
index 0000000000..2adeaf5e1d
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser.ini
@@ -0,0 +1,113 @@
+[DEFAULT]
+support-files =
+ get_user_media.html
+ get_user_media2.html
+ get_user_media_in_frame.html
+ get_user_media_in_xorigin_frame.html
+ get_user_media_in_xorigin_frame_ancestor.html
+ head.js
+ peerconnection_connect.html
+ single_peerconnection.html
+
+prefs =
+ privacy.webrtc.allowSilencingNotifications=true
+ privacy.webrtc.legacyGlobalIndicator=false
+ privacy.webrtc.sharedTabWarning=false
+ privacy.webrtc.deviceGracePeriodTimeoutMs=0
+
+[browser_device_controls_menus.js]
+skip-if =
+ debug # bug 1369731
+ apple_silicon # bug 1707735
+ apple_catalina # platform migration
+[browser_devices_get_user_media.js]
+https_first_disabled = true
+skip-if =
+ os == "linux" && bits == 64 # linux: bug 976544, Bug 1616011
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+[browser_devices_get_user_media_anim.js]
+https_first_disabled = true
+skip-if =
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+[browser_devices_get_user_media_by_device_id.js]
+https_first_disabled = true
+[browser_devices_get_user_media_default_permissions.js]
+https_first_disabled = true
+[browser_devices_get_user_media_in_frame.js]
+https_first_disabled = true
+skip-if = debug # bug 1369731
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+[browser_devices_get_user_media_in_xorigin_frame.js]
+https_first_disabled = true
+skip-if =
+ debug # bug 1369731
+ apple_silicon # bug 1707735
+ apple_catalina # platform migration
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+[browser_devices_get_user_media_in_xorigin_frame_chain.js]
+https_first_disabled = true
+[browser_devices_get_user_media_multi_process.js]
+https_first_disabled = true
+skip-if =
+ (debug && os == "win") # bug 1393761
+ apple_silicon # bug 1707735
+ apple_catalina # platform migration
+[browser_devices_get_user_media_paused.js]
+https_first_disabled = true
+skip-if =
+ (os == "win" && !debug) # Bug 1440900
+ (os =="linux" && !debug && bits == 64) # Bug 1440900
+ apple_silicon # bug 1707735
+ apple_catalina # platform migration
+[browser_devices_get_user_media_screen.js]
+https_first_disabled = true
+skip-if =
+ (os == 'linux') # Bug 1503991
+ apple_silicon # bug 1707735
+ apple_catalina # platform migration
+ os == 'win' # high frequency intermittent, bug 1739107
+[browser_devices_get_user_media_screen_tab_close.js]
+skip-if =
+ apple_catalina # platform migration
+ apple_silicon # bug 1707735
+[browser_devices_get_user_media_tear_off_tab.js]
+https_first_disabled = true
+skip-if =
+ apple_catalina # platform migration
+ apple_silicon # bug 1707735
+[browser_devices_get_user_media_unprompted_access.js]
+skip-if =
+ os == "linux" && bits == 64 && !debug # Bug 1712012
+https_first_disabled = true
+[browser_devices_get_user_media_unprompted_access_in_frame.js]
+https_first_disabled = true
+[browser_devices_get_user_media_unprompted_access_tear_off_tab.js]
+https_first_disabled = true
+skip-if = (os == "win" && bits == 64) # win8: bug 1334752
+[browser_devices_get_user_media_unprompted_access_queue_request.js]
+https_first_disabled = true
+[browser_devices_select_audio_output.js]
+[browser_global_mute_toggles.js]
+[browser_indicator_popuphiding.js]
+skip-if =
+ apple_silicon # bug 1707735
+ apple_catalina # platform migration
+[browser_notification_silencing.js]
+skip-if =
+ apple_silicon # bug 1707735
+ apple_catalina # platform migration
+[browser_stop_sharing_button.js]
+skip-if =
+ apple_silicon # bug 1707735
+ apple_catalina # platform migration
+[browser_stop_streams_on_indicator_close.js]
+skip-if =
+ apple_silicon # bug 1707735
+ apple_catalina # platform migration
+[browser_tab_switch_warning.js]
+skip-if =
+ apple_catalina # platform migration
+[browser_webrtc_hooks.js]
+[browser_devices_get_user_media_queue_request.js]
+https_first_disabled = true
+[browser_WebrtcGlobalInformation.js]
diff --git a/browser/base/content/test/webrtc/browser_WebrtcGlobalInformation.js b/browser/base/content/test/webrtc/browser_WebrtcGlobalInformation.js
new file mode 100644
index 0000000000..b7986a906e
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_WebrtcGlobalInformation.js
@@ -0,0 +1,275 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const ProcessTools = Cc["@mozilla.org/processtools-service;1"].getService(
+ Ci.nsIProcessToolsService
+);
+
+let getStatsReports = async (filter = "") => {
+ let { reports } = await new Promise(r =>
+ WebrtcGlobalInformation.getAllStats(r, filter)
+ );
+
+ ok(Array.isArray(reports), "|reports| is an array");
+
+ let sanityCheckReport = report => {
+ isnot(report.pcid, "", "pcid is non-empty");
+ if (filter.length) {
+ is(report.pcid, filter, "pcid matches filter");
+ }
+
+ // Check for duplicates
+ const checkForDuplicateId = statsArray => {
+ ok(Array.isArray(statsArray), "|statsArray| is an array");
+ const ids = new Set();
+ statsArray.forEach(stat => {
+ is(typeof stat.id, "string", "|stat.id| is a string");
+ ok(
+ !ids.has(stat.id),
+ `Id ${stat.id} should appear only once. Stat was ${JSON.stringify(
+ stat
+ )}`
+ );
+ ids.add(stat.id);
+ });
+ };
+
+ checkForDuplicateId(report.inboundRtpStreamStats);
+ checkForDuplicateId(report.outboundRtpStreamStats);
+ checkForDuplicateId(report.remoteInboundRtpStreamStats);
+ checkForDuplicateId(report.remoteOutboundRtpStreamStats);
+ checkForDuplicateId(report.rtpContributingSourceStats);
+ checkForDuplicateId(report.iceCandidatePairStats);
+ checkForDuplicateId(report.iceCandidateStats);
+ checkForDuplicateId(report.trickledIceCandidateStats);
+ checkForDuplicateId(report.dataChannelStats);
+ checkForDuplicateId(report.codecStats);
+ };
+
+ reports.forEach(sanityCheckReport);
+ return reports;
+};
+
+let getLogging = async () => {
+ let logs = await new Promise(r => WebrtcGlobalInformation.getLogging("", r));
+ ok(Array.isArray(logs), "|logs| is an array");
+ return logs;
+};
+
+let checkStatsReportCount = async (count, filter = "") => {
+ let reports = await getStatsReports(filter);
+ is(reports.length, count, `|reports| should have length ${count}`);
+ if (reports.length != count) {
+ info(`reports = ${JSON.stringify(reports)}`);
+ }
+ return reports;
+};
+
+let checkLoggingEmpty = async () => {
+ let logs = await getLogging();
+ is(logs.length, 0, "Logging is empty");
+ if (logs.length) {
+ info(`logs = ${JSON.stringify(logs)}`);
+ }
+ return logs;
+};
+
+let checkLoggingNonEmpty = async () => {
+ let logs = await getLogging();
+ isnot(logs.length, 0, "Logging is not empty");
+ return logs;
+};
+
+let clearAndCheck = async () => {
+ WebrtcGlobalInformation.clearAllStats();
+ WebrtcGlobalInformation.clearLogging();
+ await checkStatsReportCount(0);
+ await checkLoggingEmpty();
+};
+
+let openTabInNewProcess = async file => {
+ let rootDir = getRootDirectory(gTestPath);
+ rootDir = rootDir.replace(
+ "chrome://mochitests/content/",
+ "https://example.com/"
+ );
+ let absoluteURI = rootDir + file;
+
+ return BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: absoluteURI,
+ forceNewProcess: true,
+ });
+};
+
+let killTabProcess = async tab => {
+ await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ ChromeUtils.privateNoteIntentionalCrash();
+ });
+ ProcessTools.kill(tab.linkedBrowser.frameLoader.remoteTab.osPid);
+};
+
+add_task(async () => {
+ info("Test that clearAllStats is callable");
+ WebrtcGlobalInformation.clearAllStats();
+ ok(true, "clearAllStats returns");
+});
+
+add_task(async () => {
+ info("Test that clearLogging is callable");
+ WebrtcGlobalInformation.clearLogging();
+ ok(true, "clearLogging returns");
+});
+
+add_task(async () => {
+ info(
+ "Test that getAllStats is callable, and returns 0 results when no RTCPeerConnections have existed"
+ );
+ await checkStatsReportCount(0);
+});
+
+add_task(async () => {
+ info(
+ "Test that getLogging is callable, and returns 0 results when no RTCPeerConnections have existed"
+ );
+ await checkLoggingEmpty();
+});
+
+add_task(async () => {
+ info("Test that we can get stats/logging for a PC on the parent process");
+ await clearAndCheck();
+ let pc = new RTCPeerConnection();
+ await pc.setLocalDescription(
+ await pc.createOffer({ offerToReceiveAudio: true })
+ );
+ // Let ICE stack go quiescent
+ await new Promise(r => {
+ pc.onicegatheringstatechange = () => {
+ if (pc.iceGatheringState == "complete") {
+ r();
+ }
+ };
+ });
+ await checkStatsReportCount(1);
+ await checkLoggingNonEmpty();
+ pc.close();
+ pc = null;
+ // Closing a PC should not do anything to the ICE logging
+ await checkLoggingNonEmpty();
+ // There's just no way to get a signal that the ICE stack has stopped logging
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => setTimeout(r, 2000));
+ await clearAndCheck();
+});
+
+add_task(async () => {
+ info("Test that we can get stats/logging for a PC on a content process");
+ await clearAndCheck();
+ let tab = await openTabInNewProcess("single_peerconnection.html");
+ await checkStatsReportCount(1);
+ await checkLoggingNonEmpty();
+ await killTabProcess(tab);
+ BrowserTestUtils.removeTab(tab);
+ await clearAndCheck();
+});
+
+add_task(async () => {
+ info(
+ "Test that we can get stats/logging for two connected PCs on a content process"
+ );
+ await clearAndCheck();
+ let tab = await openTabInNewProcess("peerconnection_connect.html");
+ await checkStatsReportCount(2);
+ await checkLoggingNonEmpty();
+ await killTabProcess(tab);
+ BrowserTestUtils.removeTab(tab);
+ await clearAndCheck();
+});
+
+add_task(async () => {
+ info("Test filtering for stats reports (parent process)");
+ await clearAndCheck();
+ let pc1 = new RTCPeerConnection();
+ let pc2 = new RTCPeerConnection();
+ let allReports = await checkStatsReportCount(2);
+ await checkStatsReportCount(1, allReports[0].pcid);
+ pc1.close();
+ pc2.close();
+ pc1 = null;
+ pc2 = null;
+ await checkStatsReportCount(1, allReports[0].pcid);
+ await clearAndCheck();
+});
+
+add_task(async () => {
+ info("Test filtering for stats reports (content process)");
+ await clearAndCheck();
+ let tab1 = await openTabInNewProcess("single_peerconnection.html");
+ let tab2 = await openTabInNewProcess("single_peerconnection.html");
+ let allReports = await checkStatsReportCount(2);
+ await checkStatsReportCount(1, allReports[0].pcid);
+ await killTabProcess(tab1);
+ BrowserTestUtils.removeTab(tab1);
+ await killTabProcess(tab2);
+ BrowserTestUtils.removeTab(tab2);
+ await checkStatsReportCount(1, allReports[0].pcid);
+ await clearAndCheck();
+});
+
+add_task(async () => {
+ info("Test that stats/logging persists when PC is closed (parent process)");
+ await clearAndCheck();
+ let pc = new RTCPeerConnection();
+ // This stuff will generate logging
+ await pc.setLocalDescription(
+ await pc.createOffer({ offerToReceiveAudio: true })
+ );
+ // Once gathering is done, the ICE stack should go quiescent
+ await new Promise(r => {
+ pc.onicegatheringstatechange = () => {
+ if (pc.iceGatheringState == "complete") {
+ r();
+ }
+ };
+ });
+ let reports = await checkStatsReportCount(1);
+ isnot(
+ window.browsingContext.browserId,
+ undefined,
+ "browserId is defined for parent process"
+ );
+ is(
+ reports[0].browserId,
+ window.browsingContext.browserId,
+ "browserId for stats report matches parent process"
+ );
+ await checkLoggingNonEmpty();
+ pc.close();
+ pc = null;
+ await checkStatsReportCount(1);
+ await checkLoggingNonEmpty();
+ await clearAndCheck();
+});
+
+add_task(async () => {
+ info("Test that stats/logging persists when PC is closed (content process)");
+ await clearAndCheck();
+ let tab = await openTabInNewProcess("single_peerconnection.html");
+ let { browserId } = tab.linkedBrowser;
+ let reports = await checkStatsReportCount(1);
+ is(reports[0].browserId, browserId, "browserId for stats report matches tab");
+ isnot(
+ browserId,
+ window.browsingContext.browserId,
+ "tab browser id is not the same as parent process browser id"
+ );
+ await checkLoggingNonEmpty();
+ await killTabProcess(tab);
+ BrowserTestUtils.removeTab(tab);
+ await checkStatsReportCount(1);
+ await checkLoggingNonEmpty();
+ await clearAndCheck();
+});
diff --git a/browser/base/content/test/webrtc/browser_device_controls_menus.js b/browser/base/content/test/webrtc/browser_device_controls_menus.js
new file mode 100644
index 0000000000..3d6602bc5e
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_device_controls_menus.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ "https://example.com/"
+);
+const TEST_PAGE = TEST_ROOT + "get_user_media.html";
+
+/**
+ * Regression test for bug 1669801, where sharing a window would
+ * result in a device control menu that showed the wrong count.
+ */
+add_task(async function test_bug_1669801() {
+ let prefs = [
+ [PREF_PERMISSION_FAKE, true],
+ [PREF_AUDIO_LOOPBACK, ""],
+ [PREF_VIDEO_LOOPBACK, ""],
+ [PREF_FAKE_STREAMS, true],
+ [PREF_FOCUS_SOURCE, false],
+ ];
+ await SpecialPowers.pushPrefEnv({ set: prefs });
+
+ await BrowserTestUtils.withNewTab(TEST_PAGE, async browser => {
+ let indicatorPromise = promiseIndicatorWindow();
+
+ await shareDevices(
+ browser,
+ false /* camera */,
+ false /* microphone */,
+ SHARE_WINDOW
+ );
+
+ let indicator = await indicatorPromise;
+ let doc = indicator.document;
+
+ let menupopup = doc.querySelector("menupopup[type='Screen']");
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ menupopup,
+ "popupshown"
+ );
+ menupopup.openPopup(doc.body, {});
+ await popupShownPromise;
+
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ menupopup,
+ "popuphidden"
+ );
+ menupopup.hidePopup();
+ await popupHiddenPromise;
+ await closeStream();
+ });
+});
diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media.js b/browser/base/content/test/webrtc/browser_devices_get_user_media.js
new file mode 100644
index 0000000000..58fca2ae56
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media.js
@@ -0,0 +1,948 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+requestLongerTimeout(2);
+
+const permissionError =
+ "error: NotAllowedError: The request is not allowed " +
+ "by the user agent or the platform in the current context.";
+
+var gTests = [
+ {
+ desc: "getUserMedia audio+video",
+ run: async function checkAudioVideo() {
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ await promiseRequestDevice(true, true);
+ await promise;
+ await observerPromise;
+
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
+ "webRTC-shareDevices-notification-icon",
+ "anchored to device icon"
+ );
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+
+ promise = promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ await promise;
+
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ audio: true, video: true });
+ await closeStream();
+ },
+ },
+
+ {
+ desc: "getUserMedia audio only",
+ run: async function checkAudioOnly() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true);
+ await promise;
+ await observerPromise;
+
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
+ "webRTC-shareMicrophone-notification-icon",
+ "anchored to mic icon"
+ );
+ checkDeviceSelectors(["microphone"]);
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+
+ promise = promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ await promise;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true },
+ "expected microphone to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ audio: true });
+ await closeStream();
+ },
+ },
+
+ {
+ desc: "getUserMedia video only",
+ run: async function checkVideoOnly() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true);
+ await promise;
+ await observerPromise;
+
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
+ "webRTC-shareDevices-notification-icon",
+ "anchored to device icon"
+ );
+ checkDeviceSelectors(["camera"]);
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { video: true },
+ "expected camera to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ video: true });
+ await closeStream();
+ },
+ },
+
+ {
+ desc: 'getUserMedia audio+video, user clicks "Don\'t Share"',
+ run: async function checkDontShare() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ let observerPromise1 = expectObserverCalled("getUserMedia:response:deny");
+ let observerPromise2 = expectObserverCalled("recording-window-ended");
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+ await observerPromise1;
+ await observerPromise2;
+
+ await checkNotSharing();
+
+ // Verify that we set 'Temporarily blocked' permissions.
+ let browser = gBrowser.selectedBrowser;
+ let blockedPerms = document.getElementById(
+ "blocked-permissions-container"
+ );
+
+ let { state, scope } = SitePermissions.getForPrincipal(
+ null,
+ "camera",
+ browser
+ );
+ Assert.equal(state, SitePermissions.BLOCK);
+ Assert.equal(scope, SitePermissions.SCOPE_TEMPORARY);
+ ok(
+ blockedPerms.querySelector(
+ ".blocked-permission-icon.camera-icon[showing=true]"
+ ),
+ "the blocked camera icon is shown"
+ );
+
+ ({ state, scope } = SitePermissions.getForPrincipal(
+ null,
+ "microphone",
+ browser
+ ));
+ Assert.equal(state, SitePermissions.BLOCK);
+ Assert.equal(scope, SitePermissions.SCOPE_TEMPORARY);
+ ok(
+ blockedPerms.querySelector(
+ ".blocked-permission-icon.microphone-icon[showing=true]"
+ ),
+ "the blocked microphone icon is shown"
+ );
+
+ info("requesting devices again to check temporarily blocked permissions");
+ promise = promiseMessage(permissionError);
+ observerPromise1 = expectObserverCalled("getUserMedia:request");
+ observerPromise2 = expectObserverCalled("getUserMedia:response:deny");
+ let observerPromise3 = expectObserverCalled("recording-window-ended");
+ await promiseRequestDevice(true, true);
+ await promise;
+ await observerPromise1;
+ await observerPromise2;
+ await observerPromise3;
+ await checkNotSharing();
+
+ SitePermissions.removeFromPrincipal(
+ browser.contentPrincipal,
+ "camera",
+ browser
+ );
+ SitePermissions.removeFromPrincipal(
+ browser.contentPrincipal,
+ "microphone",
+ browser
+ );
+ },
+ },
+
+ {
+ desc: "getUserMedia audio+video: stop sharing",
+ run: async function checkStopSharing() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ video: true, audio: true });
+
+ await stopSharing();
+
+ // the stream is already closed, but this will do some cleanup anyway
+ await closeStream(true);
+
+ // After stop sharing, gUM(audio+camera) causes a prompt.
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ observerPromise1 = expectObserverCalled("getUserMedia:response:deny");
+ observerPromise2 = expectObserverCalled("recording-window-ended");
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+
+ await observerPromise1;
+ await observerPromise2;
+ await checkNotSharing();
+ SitePermissions.removeFromPrincipal(
+ null,
+ "screen",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "camera",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "microphone",
+ gBrowser.selectedBrowser
+ );
+ },
+ },
+
+ {
+ desc: "getUserMedia audio+video: reloading the page removes all gUM UI",
+ run: async function checkReloading() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ video: true, audio: true });
+
+ await reloadAndAssertClosedStreams();
+
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ // After the reload, gUM(audio+camera) causes a prompt.
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ observerPromise1 = expectObserverCalled("getUserMedia:response:deny");
+ observerPromise2 = expectObserverCalled("recording-window-ended");
+
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+
+ await observerPromise1;
+ await observerPromise2;
+ await checkNotSharing();
+ SitePermissions.removeFromPrincipal(
+ null,
+ "screen",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "camera",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "microphone",
+ gBrowser.selectedBrowser
+ );
+ },
+ },
+
+ {
+ desc: "getUserMedia prompt: Always/Never Share",
+ run: async function checkRememberCheckbox() {
+ let elt = id => document.getElementById(id);
+
+ async function checkPerm(
+ aRequestAudio,
+ aRequestVideo,
+ aExpectedAudioPerm,
+ aExpectedVideoPerm,
+ aNever
+ ) {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(aRequestAudio, aRequestVideo);
+ await promise;
+ await observerPromise;
+
+ is(
+ elt("webRTC-selectMicrophone").hidden,
+ !aRequestAudio,
+ "microphone selector expected to be " +
+ (aRequestAudio ? "visible" : "hidden")
+ );
+
+ is(
+ elt("webRTC-selectCamera").hidden,
+ !aRequestVideo,
+ "camera selector expected to be " +
+ (aRequestVideo ? "visible" : "hidden")
+ );
+
+ let expected = {};
+ let observerPromises = [];
+ let expectedMessage = aNever ? permissionError : "ok";
+ if (expectedMessage == "ok") {
+ observerPromises.push(
+ expectObserverCalled("getUserMedia:response:allow")
+ );
+ observerPromises.push(
+ expectObserverCalled("recording-device-events")
+ );
+ if (aRequestVideo) {
+ expected.video = true;
+ }
+ if (aRequestAudio) {
+ expected.audio = true;
+ }
+ } else {
+ observerPromises.push(
+ expectObserverCalled("getUserMedia:response:deny")
+ );
+ observerPromises.push(expectObserverCalled("recording-window-ended"));
+ }
+ await promiseMessage(expectedMessage, () => {
+ activateSecondaryAction(aNever ? kActionNever : kActionAlways);
+ });
+ await Promise.all(observerPromises);
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ expected,
+ "expected " + Object.keys(expected).join(" and ") + " to be shared"
+ );
+
+ function checkDevicePermissions(aDevice, aExpected) {
+ let uri = gBrowser.selectedBrowser.documentURI;
+ let devicePerms = PermissionTestUtils.testExactPermission(
+ uri,
+ aDevice
+ );
+ if (aExpected === undefined) {
+ is(
+ devicePerms,
+ Services.perms.UNKNOWN_ACTION,
+ "no " + aDevice + " persistent permissions"
+ );
+ } else {
+ is(
+ devicePerms,
+ aExpected
+ ? Services.perms.ALLOW_ACTION
+ : Services.perms.DENY_ACTION,
+ aDevice + " persistently " + (aExpected ? "allowed" : "denied")
+ );
+ }
+ PermissionTestUtils.remove(uri, aDevice);
+ }
+ checkDevicePermissions("microphone", aExpectedAudioPerm);
+ checkDevicePermissions("camera", aExpectedVideoPerm);
+
+ if (expectedMessage == "ok") {
+ await closeStream();
+ }
+ }
+
+ // 3 cases where the user accepts the device prompt.
+ info("audio+video, user grants, expect both Services.perms set to allow");
+ await checkPerm(true, true, true, true);
+ info(
+ "audio only, user grants, check audio perm set to allow, video perm not set"
+ );
+ await checkPerm(true, false, true, undefined);
+ info(
+ "video only, user grants, check video perm set to allow, audio perm not set"
+ );
+ await checkPerm(false, true, undefined, true);
+
+ // 3 cases where the user rejects the device request by using 'Never Share'.
+ info(
+ "audio only, user denies, expect audio perm set to deny, video not set"
+ );
+ await checkPerm(true, false, false, undefined, true);
+ info(
+ "video only, user denies, expect video perm set to deny, audio perm not set"
+ );
+ await checkPerm(false, true, undefined, false, true);
+ info("audio+video, user denies, expect both Services.perms set to deny");
+ await checkPerm(true, true, false, false, true);
+ },
+ },
+
+ {
+ desc: "getUserMedia without prompt: use persistent permissions",
+ run: async function checkUsePersistentPermissions() {
+ async function usePerm(
+ aAllowAudio,
+ aAllowVideo,
+ aRequestAudio,
+ aRequestVideo,
+ aExpectStream
+ ) {
+ let uri = gBrowser.selectedBrowser.documentURI;
+
+ if (aAllowAudio !== undefined) {
+ PermissionTestUtils.add(
+ uri,
+ "microphone",
+ aAllowAudio
+ ? Services.perms.ALLOW_ACTION
+ : Services.perms.DENY_ACTION
+ );
+ }
+ if (aAllowVideo !== undefined) {
+ PermissionTestUtils.add(
+ uri,
+ "camera",
+ aAllowVideo
+ ? Services.perms.ALLOW_ACTION
+ : Services.perms.DENY_ACTION
+ );
+ }
+
+ if (aExpectStream === undefined) {
+ // Check that we get a prompt.
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(aRequestAudio, aRequestVideo);
+ await promise;
+ await observerPromise;
+
+ // Deny the request to cleanup...
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:deny"
+ );
+ let observerPromise2 = expectObserverCalled("recording-window-ended");
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+ await observerPromise1;
+ await observerPromise2;
+
+ let browser = gBrowser.selectedBrowser;
+ SitePermissions.removeFromPrincipal(null, "camera", browser);
+ SitePermissions.removeFromPrincipal(null, "microphone", browser);
+ } else {
+ let expectedMessage = aExpectStream ? "ok" : permissionError;
+
+ let observerPromises = [expectObserverCalled("getUserMedia:request")];
+ if (expectedMessage == "ok") {
+ observerPromises.push(
+ expectObserverCalled("getUserMedia:response:allow"),
+ expectObserverCalled("recording-device-events")
+ );
+ } else {
+ observerPromises.push(
+ expectObserverCalled("getUserMedia:response:deny"),
+ expectObserverCalled("recording-window-ended")
+ );
+ }
+
+ let promise = promiseMessage(expectedMessage);
+ await promiseRequestDevice(aRequestAudio, aRequestVideo);
+ await promise;
+ await Promise.all(observerPromises);
+
+ if (expectedMessage == "ok") {
+ await promiseNoPopupNotification("webRTC-shareDevices");
+
+ // Check what's actually shared.
+ let expected = {};
+ if (aAllowVideo && aRequestVideo) {
+ expected.video = true;
+ }
+ if (aAllowAudio && aRequestAudio) {
+ expected.audio = true;
+ }
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ expected,
+ "expected " +
+ Object.keys(expected).join(" and ") +
+ " to be shared"
+ );
+
+ await closeStream();
+ }
+ }
+
+ PermissionTestUtils.remove(uri, "camera");
+ PermissionTestUtils.remove(uri, "microphone");
+ }
+
+ // Set both permissions identically
+ info("allow audio+video, request audio+video, expect ok (audio+video)");
+ await usePerm(true, true, true, true, true);
+ info("deny audio+video, request audio+video, expect denied");
+ await usePerm(false, false, true, true, false);
+
+ // Allow audio, deny video.
+ info("allow audio, deny video, request audio+video, expect denied");
+ await usePerm(true, false, true, true, false);
+ info("allow audio, deny video, request audio, expect ok (audio)");
+ await usePerm(true, false, true, false, true);
+ info("allow audio, deny video, request video, expect denied");
+ await usePerm(true, false, false, true, false);
+
+ // Deny audio, allow video.
+ info("deny audio, allow video, request audio+video, expect denied");
+ await usePerm(false, true, true, true, false);
+ info("deny audio, allow video, request audio, expect denied");
+ await usePerm(false, true, true, false, false);
+ info("deny audio, allow video, request video, expect ok (video)");
+ await usePerm(false, true, false, true, true);
+
+ // Allow audio, video not set.
+ info("allow audio, request audio+video, expect prompt");
+ await usePerm(true, undefined, true, true, undefined);
+ info("allow audio, request audio, expect ok (audio)");
+ await usePerm(true, undefined, true, false, true);
+ info("allow audio, request video, expect prompt");
+ await usePerm(true, undefined, false, true, undefined);
+
+ // Deny audio, video not set.
+ info("deny audio, request audio+video, expect denied");
+ await usePerm(false, undefined, true, true, false);
+ info("deny audio, request audio, expect denied");
+ await usePerm(false, undefined, true, false, false);
+ info("deny audio, request video, expect prompt");
+ await usePerm(false, undefined, false, true, undefined);
+
+ // Allow video, audio not set.
+ info("allow video, request audio+video, expect prompt");
+ await usePerm(undefined, true, true, true, undefined);
+ info("allow video, request audio, expect prompt");
+ await usePerm(undefined, true, true, false, undefined);
+ info("allow video, request video, expect ok (video)");
+ await usePerm(undefined, true, false, true, true);
+
+ // Deny video, audio not set.
+ info("deny video, request audio+video, expect denied");
+ await usePerm(undefined, false, true, true, false);
+ info("deny video, request audio, expect prompt");
+ await usePerm(undefined, false, true, false, undefined);
+ info("deny video, request video, expect denied");
+ await usePerm(undefined, false, false, true, false);
+ },
+ },
+
+ {
+ desc: "Stop Sharing removes permissions",
+ run: async function checkStopSharingRemovesPermissions() {
+ async function stopAndCheckPerm(
+ aRequestAudio,
+ aRequestVideo,
+ aStopAudio = aRequestAudio,
+ aStopVideo = aRequestVideo
+ ) {
+ let uri = gBrowser.selectedBrowser.documentURI;
+
+ // Initially set both permissions to 'allow'.
+ PermissionTestUtils.add(uri, "microphone", Services.perms.ALLOW_ACTION);
+ PermissionTestUtils.add(uri, "camera", Services.perms.ALLOW_ACTION);
+ // Also set device-specific temporary allows.
+ SitePermissions.setForPrincipal(
+ gBrowser.contentPrincipal,
+ "microphone^myDevice",
+ SitePermissions.ALLOW,
+ SitePermissions.SCOPE_TEMPORARY,
+ gBrowser.selectedBrowser,
+ 10000000
+ );
+ SitePermissions.setForPrincipal(
+ gBrowser.contentPrincipal,
+ "camera^myDevice2",
+ SitePermissions.ALLOW,
+ SitePermissions.SCOPE_TEMPORARY,
+ gBrowser.selectedBrowser,
+ 10000000
+ );
+
+ if (aRequestAudio || aRequestVideo) {
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled("getUserMedia:request");
+ let observerPromise2 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise3 = expectObserverCalled(
+ "recording-device-events"
+ );
+ // Start sharing what's been requested.
+ let promise = promiseMessage("ok");
+ await promiseRequestDevice(aRequestAudio, aRequestVideo);
+ await promise;
+ await observerPromise1;
+ await observerPromise2;
+ await observerPromise3;
+
+ await indicator;
+ await checkSharingUI(
+ { video: aRequestVideo, audio: aRequestAudio },
+ undefined,
+ undefined,
+ {
+ video: { scope: SitePermissions.SCOPE_PERSISTENT },
+ audio: { scope: SitePermissions.SCOPE_PERSISTENT },
+ }
+ );
+ await stopSharing(aStopVideo ? "camera" : "microphone");
+ } else {
+ await revokePermission(aStopVideo ? "camera" : "microphone");
+ }
+
+ // Check that permissions have been removed as expected.
+ let audioPerm = SitePermissions.getForPrincipal(
+ gBrowser.contentPrincipal,
+ "microphone",
+ gBrowser.selectedBrowser
+ );
+ let audioPermDevice = SitePermissions.getForPrincipal(
+ gBrowser.contentPrincipal,
+ "microphone^myDevice",
+ gBrowser.selectedBrowser
+ );
+
+ if (
+ aRequestAudio ||
+ aRequestVideo ||
+ aStopAudio ||
+ (aStopVideo && aRequestAudio)
+ ) {
+ Assert.deepEqual(
+ audioPerm,
+ {
+ state: SitePermissions.UNKNOWN,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ },
+ "microphone permissions removed"
+ );
+ Assert.deepEqual(
+ audioPermDevice,
+ {
+ state: SitePermissions.UNKNOWN,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ },
+ "microphone device-specific permissions removed"
+ );
+ } else {
+ Assert.deepEqual(
+ audioPerm,
+ {
+ state: SitePermissions.ALLOW,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ },
+ "microphone permissions untouched"
+ );
+ Assert.deepEqual(
+ audioPermDevice,
+ {
+ state: SitePermissions.ALLOW,
+ scope: SitePermissions.SCOPE_TEMPORARY,
+ },
+ "microphone device-specific permissions untouched"
+ );
+ }
+
+ let videoPerm = SitePermissions.getForPrincipal(
+ gBrowser.contentPrincipal,
+ "camera",
+ gBrowser.selectedBrowser
+ );
+ let videoPermDevice = SitePermissions.getForPrincipal(
+ gBrowser.contentPrincipal,
+ "camera^myDevice2",
+ gBrowser.selectedBrowser
+ );
+ if (
+ aRequestAudio ||
+ aRequestVideo ||
+ aStopVideo ||
+ (aStopAudio && aRequestVideo)
+ ) {
+ Assert.deepEqual(
+ videoPerm,
+ {
+ state: SitePermissions.UNKNOWN,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ },
+ "camera permissions removed"
+ );
+ Assert.deepEqual(
+ videoPermDevice,
+ {
+ state: SitePermissions.UNKNOWN,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ },
+ "camera device-specific permissions removed"
+ );
+ } else {
+ Assert.deepEqual(
+ videoPerm,
+ {
+ state: SitePermissions.ALLOW,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ },
+ "camera permissions untouched"
+ );
+ Assert.deepEqual(
+ videoPermDevice,
+ {
+ state: SitePermissions.ALLOW,
+ scope: SitePermissions.SCOPE_TEMPORARY,
+ },
+ "camera device-specific permissions untouched"
+ );
+ }
+ await checkNotSharing();
+
+ // Cleanup.
+ await closeStream(true);
+
+ SitePermissions.removeFromPrincipal(
+ gBrowser.contentPrincipal,
+ "camera",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ gBrowser.contentPrincipal,
+ "microphone",
+ gBrowser.selectedBrowser
+ );
+ }
+
+ info("request audio+video, stop sharing video resets both");
+ await stopAndCheckPerm(true, true);
+ info("request audio only, stop sharing audio resets both");
+ await stopAndCheckPerm(true, false);
+ info("request video only, stop sharing video resets both");
+ await stopAndCheckPerm(false, true);
+ info("request audio only, stop sharing video resets both");
+ await stopAndCheckPerm(true, false, false, true);
+ info("request video only, stop sharing audio resets both");
+ await stopAndCheckPerm(false, true, true, false);
+ info("request neither, stop audio affects audio only");
+ await stopAndCheckPerm(false, false, true, false);
+ info("request neither, stop video affects video only");
+ await stopAndCheckPerm(false, false, false, true);
+ },
+ },
+
+ {
+ desc: "test showPermissionPanel",
+ run: async function checkShowPermissionPanel() {
+ if (!USING_LEGACY_INDICATOR) {
+ // The indicator only links to the permission panel for the
+ // legacy indicator.
+ return;
+ }
+
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["camera"]);
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { video: true },
+ "expected camera to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ video: true });
+
+ ok(permissionPopupHidden(), "permission panel should be hidden");
+ if (IS_MAC) {
+ let activeStreams = webrtcUI.getActiveStreams(true, false, false);
+ webrtcUI.showSharingDoorhanger(activeStreams[0]);
+ } else {
+ let win = Services.wm.getMostRecentWindow(
+ "Browser:WebRTCGlobalIndicator"
+ );
+
+ let elt = win.document.getElementById("audioVideoButton");
+ EventUtils.synthesizeMouseAtCenter(elt, {}, win);
+ }
+
+ await TestUtils.waitForCondition(
+ () => !permissionPopupHidden(),
+ "wait for permission panel to open"
+ );
+ ok(!permissionPopupHidden(), "permission panel should be open");
+
+ gPermissionPanel._permissionPopup.hidePopup();
+
+ await closeStream();
+ },
+ },
+
+ {
+ desc: "'Always Allow' disabled on http pages",
+ run: async function checkNoAlwaysOnHttp() {
+ // Load an http page instead of the https version.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.devices.insecure.enabled", true],
+ ["media.getusermedia.insecure.enabled", true],
+ // explicitly testing an http page, setting
+ // https-first to false.
+ ["dom.security.https_first", false],
+ ],
+ });
+
+ // Disable while loading a new page
+ await disableObserverVerification();
+
+ let browser = gBrowser.selectedBrowser;
+ BrowserTestUtils.loadURI(
+ browser,
+ browser.documentURI.spec.replace("https://", "http://")
+ );
+ await BrowserTestUtils.browserLoaded(browser);
+
+ await enableObserverVerification();
+
+ // Initially set both permissions to 'allow'.
+ let uri = browser.documentURI;
+ PermissionTestUtils.add(uri, "microphone", Services.perms.ALLOW_ACTION);
+ PermissionTestUtils.add(uri, "camera", Services.perms.ALLOW_ACTION);
+
+ // Request devices and expect a prompt despite the saved 'Allow' permission,
+ // because the connection isn't secure.
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true);
+ await promise;
+ await observerPromise;
+
+ // Ensure that checking the 'Remember this decision' checkbox disables
+ // 'Allow'.
+ let notification = PopupNotifications.panel.firstElementChild;
+ let checkbox = notification.checkbox;
+ ok(!!checkbox, "checkbox is present");
+ ok(!checkbox.checked, "checkbox is not checked");
+ checkbox.click();
+ ok(checkbox.checked, "checkbox now checked");
+ ok(notification.button.disabled, "Allow button is disabled");
+ ok(
+ !notification.hasAttribute("warninghidden"),
+ "warning message is shown"
+ );
+
+ // Cleanup.
+ await closeStream(true);
+ PermissionTestUtils.remove(uri, "camera");
+ PermissionTestUtils.remove(uri, "microphone");
+ },
+ },
+];
+
+add_task(async function test() {
+ await runTests(gTests);
+});
diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media_anim.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_anim.js
new file mode 100644
index 0000000000..dd20a672c3
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_anim.js
@@ -0,0 +1,106 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var gTests = [
+ {
+ desc: "device sharing animation on background tabs",
+ run: async function checkAudioVideo() {
+ async function getStreamAndCheckBackgroundAnim(aAudio, aVideo, aSharing) {
+ // Get a stream
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let popupPromise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(aAudio, aVideo);
+ await popupPromise;
+ await observerPromise;
+
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ let expected = {};
+ if (aVideo) {
+ expected.video = true;
+ }
+ if (aAudio) {
+ expected.audio = true;
+ }
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ expected,
+ "expected " + Object.keys(expected).join(" and ") + " to be shared"
+ );
+
+ // Check the attribute on the tab, and check there's no visible
+ // sharing icon on the tab
+ let tab = gBrowser.selectedTab;
+ is(
+ tab.getAttribute("sharing"),
+ aSharing,
+ "the tab has the attribute to show the " + aSharing + " icon"
+ );
+ let icon = tab.sharingIcon;
+ is(
+ window.getComputedStyle(icon).display,
+ "none",
+ "the animated sharing icon of the tab is hidden"
+ );
+
+ // After selecting a new tab, check the attribute is still there,
+ // and the icon is now visible.
+ await BrowserTestUtils.switchTab(
+ gBrowser,
+ BrowserTestUtils.addTab(gBrowser)
+ );
+ is(
+ gBrowser.selectedTab.getAttribute("sharing"),
+ "",
+ "the new tab doesn't have the 'sharing' attribute"
+ );
+ is(
+ tab.getAttribute("sharing"),
+ aSharing,
+ "the tab still has the 'sharing' attribute"
+ );
+ isnot(
+ window.getComputedStyle(icon).display,
+ "none",
+ "the animated sharing icon of the tab is now visible"
+ );
+
+ // Ensure the icon disappears when selecting the tab.
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ ok(tab.selected, "the tab with ongoing sharing is selected again");
+ is(
+ window.getComputedStyle(icon).display,
+ "none",
+ "the animated sharing icon is gone after selecting the tab again"
+ );
+
+ // And finally verify the attribute is removed when closing the stream.
+ await closeStream();
+
+ // TODO(Bug 1304997): Fix the race in closeStream() and remove this
+ // TestUtils.waitForCondition().
+ await TestUtils.waitForCondition(() => !tab.getAttribute("sharing"));
+ is(
+ tab.getAttribute("sharing"),
+ "",
+ "the tab no longer has the 'sharing' attribute after closing the stream"
+ );
+ }
+
+ await getStreamAndCheckBackgroundAnim(true, true, "camera");
+ await getStreamAndCheckBackgroundAnim(false, true, "camera");
+ await getStreamAndCheckBackgroundAnim(true, false, "microphone");
+ },
+ },
+];
+
+add_task(async function test() {
+ await runTests(gTests);
+});
diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media_by_device_id.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_by_device_id.js
new file mode 100644
index 0000000000..3e5ca0668a
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_by_device_id.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ "https://example.com/"
+);
+const TEST_PAGE = TEST_ROOT + "get_user_media.html";
+
+/**
+ * Utility function that should be called after a request for a device
+ * has been made. This function will allow sharing that device, and then
+ * immediately close the stream.
+ */
+async function allowStreamsThenClose() {
+ let observerPromise1 = expectObserverCalled("getUserMedia:response:allow");
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+
+ await closeStream();
+}
+
+/**
+ * Tests that if a site requests a particular device by ID, that
+ * the Permission Panel menulist for that device shows only that
+ * device and is disabled.
+ */
+add_task(async function test_get_user_media_by_device_id() {
+ let prefs = [
+ [PREF_PERMISSION_FAKE, true],
+ [PREF_AUDIO_LOOPBACK, ""],
+ [PREF_VIDEO_LOOPBACK, ""],
+ [PREF_FAKE_STREAMS, true],
+ [PREF_FOCUS_SOURCE, false],
+ ];
+ await SpecialPowers.pushPrefEnv({ set: prefs });
+
+ let devices = await navigator.mediaDevices.enumerateDevices();
+ let audioId = devices
+ .filter(d => d.kind == "audioinput")
+ .map(d => d.deviceId)[0];
+ let videoId = devices
+ .filter(d => d.kind == "videoinput")
+ .map(d => d.deviceId)[0];
+
+ await BrowserTestUtils.withNewTab(TEST_PAGE, async browser => {
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ await promiseRequestDevice({ deviceId: { exact: audioId } });
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone"]);
+
+ await allowStreamsThenClose();
+
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ await promiseRequestDevice(false, { deviceId: { exact: videoId } });
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["camera"]);
+
+ await allowStreamsThenClose();
+
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ await promiseRequestDevice(
+ { deviceId: { exact: audioId } },
+ { deviceId: { exact: videoId } }
+ );
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+ await allowStreamsThenClose();
+ });
+});
diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media_default_permissions.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_default_permissions.js
new file mode 100644
index 0000000000..e6464fd4aa
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_default_permissions.js
@@ -0,0 +1,209 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const permissionError =
+ "error: NotAllowedError: The request is not allowed " +
+ "by the user agent or the platform in the current context.";
+
+const CAMERA_PREF = "permissions.default.camera";
+const MICROPHONE_PREF = "permissions.default.microphone";
+
+var gTests = [
+ {
+ desc: "getUserMedia audio+video: globally blocking camera",
+ run: async function checkAudioVideo() {
+ Services.prefs.setIntPref(CAMERA_PREF, SitePermissions.BLOCK);
+
+ // Requesting audio+video shouldn't work.
+ await Promise.all([
+ expectObserverCalled("getUserMedia:request"),
+ expectObserverCalled("getUserMedia:response:deny"),
+ expectObserverCalled("recording-window-ended"),
+ promiseMessage(permissionError),
+ promiseRequestDevice(true, true),
+ ]);
+ await checkNotSharing();
+
+ // Requesting only video shouldn't work.
+ await Promise.all([
+ expectObserverCalled("getUserMedia:request"),
+ expectObserverCalled("getUserMedia:response:deny"),
+ expectObserverCalled("recording-window-ended"),
+ promiseMessage(permissionError),
+ promiseRequestDevice(false, true),
+ ]);
+ await checkNotSharing();
+
+ // Requesting audio should work.
+ const observerPromise = expectObserverCalled("getUserMedia:request");
+ const promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true);
+ await promise;
+ await observerPromise;
+
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
+ "webRTC-shareMicrophone-notification-icon",
+ "anchored to mic icon"
+ );
+ checkDeviceSelectors(["microphone"]);
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true },
+ "expected microphone to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ audio: true });
+ await closeStream();
+ Services.prefs.clearUserPref(CAMERA_PREF);
+ },
+ },
+
+ {
+ desc: "getUserMedia video: globally blocking camera + local exception",
+ run: async function checkAudioVideo() {
+ let browser = gBrowser.selectedBrowser;
+ Services.prefs.setIntPref(CAMERA_PREF, SitePermissions.BLOCK);
+ // Overwrite the permission for that URI, requesting video should work again.
+ PermissionTestUtils.add(
+ browser.currentURI,
+ "camera",
+ Services.perms.ALLOW_ACTION
+ );
+
+ // Requesting video should work.
+ let indicator = promiseIndicatorWindow();
+ let promises = [
+ expectObserverCalled("getUserMedia:request"),
+ expectObserverCalled("getUserMedia:response:allow"),
+ expectObserverCalled("recording-device-events"),
+ ];
+
+ let promise = promiseMessage("ok");
+ await promiseRequestDevice(false, true);
+ await promise;
+
+ await Promise.all(promises);
+ await indicator;
+ await checkSharingUI({ video: true }, undefined, undefined, {
+ video: { scope: SitePermissions.SCOPE_PERSISTENT },
+ });
+ await closeStream();
+
+ PermissionTestUtils.remove(browser.currentURI, "camera");
+ Services.prefs.clearUserPref(CAMERA_PREF);
+ },
+ },
+
+ {
+ desc: "getUserMedia audio+video: globally blocking microphone",
+ run: async function checkAudioVideo() {
+ Services.prefs.setIntPref(MICROPHONE_PREF, SitePermissions.BLOCK);
+
+ // Requesting audio+video shouldn't work.
+ await Promise.all([
+ expectObserverCalled("getUserMedia:request"),
+ expectObserverCalled("getUserMedia:response:deny"),
+ expectObserverCalled("recording-window-ended"),
+ promiseMessage(permissionError),
+ promiseRequestDevice(true, true),
+ ]);
+ await checkNotSharing();
+
+ // Requesting only audio shouldn't work.
+ await Promise.all([
+ expectObserverCalled("getUserMedia:request"),
+ expectObserverCalled("getUserMedia:response:deny"),
+ expectObserverCalled("recording-window-ended"),
+ promiseMessage(permissionError),
+ promiseRequestDevice(true),
+ ]);
+
+ // Requesting video should work.
+ const observerPromise = expectObserverCalled("getUserMedia:request");
+ const promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true);
+ await promise;
+ await observerPromise;
+
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
+ "webRTC-shareDevices-notification-icon",
+ "anchored to device icon"
+ );
+ checkDeviceSelectors(["camera"]);
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { video: true },
+ "expected camera to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ video: true });
+ await closeStream();
+ Services.prefs.clearUserPref(MICROPHONE_PREF);
+ },
+ },
+
+ {
+ desc: "getUserMedia audio: globally blocking microphone + local exception",
+ run: async function checkAudioVideo() {
+ let browser = gBrowser.selectedBrowser;
+ Services.prefs.setIntPref(MICROPHONE_PREF, SitePermissions.BLOCK);
+ // Overwrite the permission for that URI, requesting video should work again.
+ PermissionTestUtils.add(
+ browser.currentURI,
+ "microphone",
+ Services.perms.ALLOW_ACTION
+ );
+
+ // Requesting audio should work.
+ let indicator = promiseIndicatorWindow();
+ let promises = [
+ expectObserverCalled("getUserMedia:request"),
+ expectObserverCalled("getUserMedia:response:allow"),
+ expectObserverCalled("recording-device-events"),
+ ];
+ let promise = promiseMessage("ok");
+ await promiseRequestDevice(true);
+ await promise;
+
+ await Promise.all(promises);
+ await indicator;
+ await checkSharingUI({ audio: true }, undefined, undefined, {
+ audio: { scope: SitePermissions.SCOPE_PERSISTENT },
+ });
+ await closeStream();
+
+ PermissionTestUtils.remove(browser.currentURI, "microphone");
+ Services.prefs.clearUserPref(MICROPHONE_PREF);
+ },
+ },
+];
+add_task(async function test() {
+ await runTests(gTests);
+});
diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media_grace.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_grace.js
new file mode 100644
index 0000000000..0df69bb9da
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_grace.js
@@ -0,0 +1,388 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+requestLongerTimeout(2);
+
+const permissionError =
+ "error: NotAllowedError: The request is not allowed " +
+ "by the user agent or the platform in the current context.";
+
+const SAME_ORIGIN = "https://example.com";
+const CROSS_ORIGIN = "https://example.org";
+
+const PATH = "/browser/browser/base/content/test/webrtc/get_user_media.html";
+const PATH2 = "/browser/browser/base/content/test/webrtc/get_user_media2.html";
+
+const GRACE_PERIOD_MS = 3000;
+const WAIT_PERIOD_MS = GRACE_PERIOD_MS + 500;
+
+// We're inherently testing timeouts (grace periods)
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
+const perms = SitePermissions;
+
+// These tests focus on camera and microphone, so we define some helpers.
+
+async function prompt(audio, video) {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(audio, video);
+ await promise;
+ await observerPromise;
+ const expectedDeviceSelectorTypes = [
+ audio && "microphone",
+ video && "camera",
+ ].filter(x => x);
+ checkDeviceSelectors(expectedDeviceSelectorTypes);
+}
+
+async function allow(audio, video) {
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled("getUserMedia:response:allow");
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ Object.assign({ audio: false, video: false }, await getMediaCaptureState()),
+ { audio, video },
+ `expected ${video ? "camera " : ""} ${audio ? "microphone " : ""}shared`
+ );
+ await indicator;
+ await checkSharingUI({ audio, video });
+}
+
+async function deny(action) {
+ let observerPromise1 = expectObserverCalled("getUserMedia:response:deny");
+ let observerPromise2 = expectObserverCalled("recording-window-ended");
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(action);
+ });
+ await observerPromise1;
+ await observerPromise2;
+ await checkNotSharing();
+}
+
+async function noPrompt(audio, video) {
+ let observerPromises = [
+ expectObserverCalled("getUserMedia:request"),
+ expectObserverCalled("getUserMedia:response:allow"),
+ expectObserverCalled("recording-device-events"),
+ ];
+ let promise = promiseMessage("ok");
+ await promiseRequestDevice(audio, video);
+ await promise;
+ await Promise.all(observerPromises);
+ await promiseNoPopupNotification("webRTC-shareDevices");
+ Assert.deepEqual(
+ Object.assign({ audio: false, video: false }, await getMediaCaptureState()),
+ { audio, video },
+ `expected ${video ? "camera " : ""} ${audio ? "microphone " : ""}shared`
+ );
+ await checkSharingUI({ audio, video });
+}
+
+async function navigate(browser, url) {
+ await disableObserverVerification();
+ let loaded = BrowserTestUtils.browserLoaded(browser, false, url);
+ await SpecialPowers.spawn(
+ browser,
+ [url],
+ u => (content.document.location = u)
+ );
+ await loaded;
+ await enableObserverVerification();
+}
+
+var gTests = [
+ {
+ desc: "getUserMedia camera+mic survives track.stop but not past grace",
+ run: async function checkAudioVideoGracePastStop() {
+ await prompt(true, true);
+ await allow(true, true);
+
+ info(
+ "After closing all streams, gUM(camera+mic) returns a stream " +
+ "without prompting within grace period."
+ );
+ await closeStream();
+ await checkNotSharingWithinGracePeriod();
+ await noPrompt(true, true);
+
+ info(
+ "After closing all streams, gUM(mic) returns a stream " +
+ "without prompting within grace period."
+ );
+ await closeStream();
+ await checkNotSharingWithinGracePeriod();
+ await noPrompt(true, false);
+
+ info(
+ "After closing all streams, gUM(camera) returns a stream " +
+ "without prompting within grace period."
+ );
+ await closeStream();
+ await checkNotSharingWithinGracePeriod();
+ await noPrompt(false, true);
+
+ info("gUM(screen) still causes a prompt.");
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true, null, "screen");
+ await promise;
+ await observerPromise;
+
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
+ "webRTC-shareScreen-notification-icon",
+ "anchored to device icon"
+ );
+ checkDeviceSelectors(["screen"]);
+
+ observerPromise = expectObserverCalled("getUserMedia:response:deny");
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+ await observerPromise;
+ perms.removeFromPrincipal(null, "screen", gBrowser.selectedBrowser);
+
+ await closeStream();
+ info("Closed stream. Waiting past grace period.");
+ await checkNotSharingWithinGracePeriod();
+ await wait(WAIT_PERIOD_MS);
+ await checkNotSharing();
+
+ info("After grace period expires, gUM(camera) causes a prompt.");
+ await prompt(false, true);
+ await deny(kActionDeny);
+ perms.removeFromPrincipal(null, "camera", gBrowser.selectedBrowser);
+
+ info("After grace period expires, gUM(mic) causes a prompt.");
+ await prompt(true, false);
+ await deny(kActionDeny);
+ perms.removeFromPrincipal(null, "microphone", gBrowser.selectedBrowser);
+ },
+ },
+
+ {
+ desc: "getUserMedia camera+mic survives page reload but not past grace",
+ run: async function checkAudioVideoGracePastReload(browser) {
+ await prompt(true, true);
+ await allow(true, true);
+ await closeStream();
+
+ await reloadFromContent();
+ info(
+ "After page reload, gUM(camera+mic) returns a stream " +
+ "without prompting within grace period."
+ );
+ await checkNotSharingWithinGracePeriod();
+ await noPrompt(true, true);
+ await closeStream();
+
+ await reloadAsUser();
+ info(
+ "After user page reload, gUM(camera+mic) returns a stream " +
+ "without prompting within grace period."
+ );
+ await checkNotSharingWithinGracePeriod();
+ await noPrompt(true, true);
+
+ info("gUM(screen) still causes a prompt.");
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true, null, "screen");
+ await promise;
+ await observerPromise;
+
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
+ "webRTC-shareScreen-notification-icon",
+ "anchored to device icon"
+ );
+ checkDeviceSelectors(["screen"]);
+
+ observerPromise = expectObserverCalled("getUserMedia:response:deny");
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+ await observerPromise;
+ perms.removeFromPrincipal(null, "screen", gBrowser.selectedBrowser);
+
+ await closeStream();
+ info("Closed stream. Waiting past grace period.");
+ await checkNotSharingWithinGracePeriod();
+ await wait(WAIT_PERIOD_MS);
+ await checkNotSharing();
+
+ info("After grace period expires, gUM(camera) causes a prompt.");
+ await prompt(false, true);
+ await deny(kActionDeny);
+ perms.removeFromPrincipal(null, "camera", gBrowser.selectedBrowser);
+
+ info("After grace period expires, gUM(mic) causes a prompt.");
+ await prompt(true, false);
+ await deny(kActionDeny);
+ perms.removeFromPrincipal(null, "microphone", gBrowser.selectedBrowser);
+ },
+ },
+
+ {
+ desc: "getUserMedia camera+mic grace period does not carry over to new tab",
+ run: async function checkAudioVideoGraceEndsNewTab() {
+ await prompt(true, true);
+ await allow(true, true);
+
+ info("Open same page in a new tab");
+ await disableObserverVerification();
+ await BrowserTestUtils.withNewTab(SAME_ORIGIN + PATH, async browser => {
+ info("In new tab, gUM(camera+mic) causes a prompt.");
+ await prompt(true, true);
+ });
+ info("Closed tab");
+ await enableObserverVerification();
+ await closeStream();
+ info("Closed stream. Waiting past grace period.");
+ await checkNotSharingWithinGracePeriod();
+ await wait(WAIT_PERIOD_MS);
+ await checkNotSharing();
+
+ info("After grace period expires, gUM(camera+mic) causes a prompt.");
+ await prompt(true, true);
+ await deny(kActionDeny);
+ perms.removeFromPrincipal(null, "camera", gBrowser.selectedBrowser);
+ perms.removeFromPrincipal(null, "microphone", gBrowser.selectedBrowser);
+ },
+ },
+
+ {
+ desc: "getUserMedia camera+mic survives navigation but not past grace",
+ run: async function checkAudioVideoGracePastNavigation(browser) {
+ // Use longer grace period in this test to accommodate navigation
+ const LONG_GRACE_PERIOD_MS = 9000;
+ const LONG_WAIT_PERIOD_MS = LONG_GRACE_PERIOD_MS + 500;
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.webrtc.deviceGracePeriodTimeoutMs", LONG_GRACE_PERIOD_MS],
+ ],
+ });
+ await prompt(true, true);
+ await allow(true, true);
+ await closeStream();
+
+ info("Navigate to a second same-origin page");
+ await navigate(browser, SAME_ORIGIN + PATH2);
+ info(
+ "After navigating to second same-origin page, gUM(camera+mic) " +
+ "returns a stream without prompting within grace period."
+ );
+ await checkNotSharingWithinGracePeriod();
+ await noPrompt(true, true);
+ await closeStream();
+
+ info("Closed stream. Waiting past grace period.");
+ await checkNotSharingWithinGracePeriod();
+ await wait(LONG_WAIT_PERIOD_MS);
+ await checkNotSharing();
+
+ info("After grace period expires, gUM(camera+mic) causes a prompt.");
+ await prompt(true, true);
+ await allow(true, true);
+
+ info("Navigate to a different-origin page");
+ await navigate(browser, CROSS_ORIGIN + PATH2);
+ info(
+ "After navigating to a different-origin page, gUM(camera+mic) " +
+ "causes a prompt."
+ );
+ await prompt(true, true);
+ await deny(kActionDeny);
+ perms.removeFromPrincipal(null, "camera", gBrowser.selectedBrowser);
+ perms.removeFromPrincipal(null, "microphone", gBrowser.selectedBrowser);
+
+ info("Navigate back to the first page");
+ await navigate(browser, SAME_ORIGIN + PATH);
+ info(
+ "After navigating back to the first page, gUM(camera+mic) " +
+ "returns a stream without prompting within grace period."
+ );
+ await checkNotSharingWithinGracePeriod();
+ await noPrompt(true, true);
+ await closeStream();
+ info("Closed stream. Waiting past grace period.");
+ await checkNotSharingWithinGracePeriod();
+ await wait(LONG_WAIT_PERIOD_MS);
+ await checkNotSharing();
+
+ info("After grace period expires, gUM(camera+mic) causes a prompt.");
+ await prompt(true, true);
+ await deny(kActionDeny);
+ perms.removeFromPrincipal(null, "camera", gBrowser.selectedBrowser);
+ perms.removeFromPrincipal(null, "microphone", gBrowser.selectedBrowser);
+ },
+ },
+
+ {
+ desc: "getUserMedia camera+mic grace period cleared on permission block",
+ run: async function checkAudioVideoGraceEndsNewTab(browser) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.webrtc.deviceGracePeriodTimeoutMs", 10000]],
+ });
+ info("Set up longer camera grace period.");
+ await prompt(false, true);
+ await allow(false, true);
+ await closeStream();
+ let principal = gBrowser.selectedBrowser.contentPrincipal;
+ info("Request both to get prompted so we can block both.");
+ await prompt(true, true);
+ // We need to remember this decision to set a block permission here and not just 'Not now' the request, see Bug:1609578
+ await deny(kActionNever);
+ // Clear the block so we can prompt again.
+ perms.removeFromPrincipal(principal, "camera", gBrowser.selectedBrowser);
+ perms.removeFromPrincipal(
+ principal,
+ "microphone",
+ gBrowser.selectedBrowser
+ );
+
+ info("Revoking permission clears camera grace period.");
+ await prompt(false, true);
+ await deny(kActionDeny);
+ perms.removeFromPrincipal(null, "camera", gBrowser.selectedBrowser);
+
+ info("Set up longer microphone grace period.");
+ await prompt(true, false);
+ await allow(true, false);
+ await closeStream();
+
+ info("Request both to get prompted so we can block both.");
+ await prompt(true, true);
+ // We need to remember this decision to be able to set a block permission here
+ await deny(kActionNever);
+ perms.removeFromPrincipal(principal, "camera", gBrowser.selectedBrowser);
+ perms.removeFromPrincipal(
+ principal,
+ "microphone",
+ gBrowser.selectedBrowser
+ );
+
+ info("Revoking permission clears microphone grace period.");
+ await prompt(true, false);
+ // We need to remember this decision to be able to set a block permission here
+ await deny(kActionNever);
+ perms.removeFromPrincipal(null, "microphone", gBrowser.selectedBrowser);
+ },
+ },
+];
+
+add_task(async function test() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.webrtc.deviceGracePeriodTimeoutMs", GRACE_PERIOD_MS]],
+ });
+ await runTests(gTests);
+});
diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media_in_frame.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_in_frame.js
new file mode 100644
index 0000000000..afb4720cae
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_in_frame.js
@@ -0,0 +1,752 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+SpecialPowers.pushPrefEnv({
+ set: [["permissions.delegation.enabled", true]],
+});
+
+// This test has been seen timing out locally in non-opt debug builds.
+requestLongerTimeout(2);
+
+var gTests = [
+ {
+ desc: "getUserMedia audio+video",
+ run: async function checkAudioVideo(aBrowser, aSubFrames) {
+ let { bc: frame1BC, id: frame1ID, observeBC: frame1ObserveBC } = (
+ await getBrowsingContextsAndFrameIdsForSubFrames(
+ aBrowser.browsingContext,
+ aSubFrames
+ )
+ )[0];
+
+ let observerPromise = expectObserverCalled(
+ "getUserMedia:request",
+ 1,
+ frame1ObserveBC
+ );
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true, frame1ID, undefined, frame1BC);
+ await promise;
+ await observerPromise;
+
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
+ "webRTC-shareDevices-notification-icon",
+ "anchored to device icon"
+ );
+ checkDeviceSelectors(["microphone", "camera"]);
+ is(
+ PopupNotifications.panel.firstElementChild.getAttribute("popupid"),
+ "webRTC-shareDevices",
+ "panel using devices icon"
+ );
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow",
+ 1,
+ frame1ObserveBC
+ );
+ let observerPromise2 = expectObserverCalled(
+ "recording-device-events",
+ 1,
+ frame1ObserveBC
+ );
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ audio: true, video: true });
+ await closeStream(false, frame1ID, undefined, frame1BC, frame1ObserveBC);
+ },
+ },
+
+ {
+ desc: "getUserMedia audio+video: stop sharing",
+ run: async function checkStopSharing(aBrowser, aSubFrames) {
+ let { bc: frame1BC, id: frame1ID, observeBC: frame1ObserveBC } = (
+ await getBrowsingContextsAndFrameIdsForSubFrames(
+ aBrowser.browsingContext,
+ aSubFrames
+ )
+ )[0];
+
+ let observerPromise = expectObserverCalled(
+ "getUserMedia:request",
+ 1,
+ frame1ObserveBC
+ );
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true, frame1ID, undefined, frame1BC);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow",
+ 1,
+ frame1ObserveBC
+ );
+ let observerPromise2 = expectObserverCalled(
+ "recording-device-events",
+ 1,
+ frame1ObserveBC
+ );
+ await promiseMessage("ok", () => {
+ activateSecondaryAction(kActionAlways);
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ video: true, audio: true }, undefined, undefined, {
+ video: { scope: SitePermissions.SCOPE_PERSISTENT },
+ audio: { scope: SitePermissions.SCOPE_PERSISTENT },
+ });
+
+ let uri = Services.io.newURI("https://example.com/");
+ is(
+ PermissionTestUtils.testExactPermission(uri, "microphone"),
+ Services.perms.ALLOW_ACTION,
+ "microphone persistently allowed"
+ );
+ is(
+ PermissionTestUtils.testExactPermission(uri, "camera"),
+ Services.perms.ALLOW_ACTION,
+ "camera persistently allowed"
+ );
+
+ await stopSharing("camera", false, frame1ObserveBC);
+
+ // The persistent permissions for the frame should have been removed.
+ is(
+ PermissionTestUtils.testExactPermission(uri, "microphone"),
+ Services.perms.UNKNOWN_ACTION,
+ "microphone not persistently allowed"
+ );
+ is(
+ PermissionTestUtils.testExactPermission(uri, "camera"),
+ Services.perms.UNKNOWN_ACTION,
+ "camera not persistently allowed"
+ );
+
+ // the stream is already closed, but this will do some cleanup anyway
+ await closeStream(true, frame1ID, undefined, frame1BC, frame1ObserveBC);
+ },
+ },
+
+ {
+ desc:
+ "getUserMedia audio+video: Revoking active devices in frame does not add grace period.",
+ run: async function checkStopSharingGracePeriod(aBrowser, aSubFrames) {
+ let { bc: frame1BC, id: frame1ID, observeBC: frame1ObserveBC } = (
+ await getBrowsingContextsAndFrameIdsForSubFrames(
+ aBrowser.browsingContext,
+ aSubFrames
+ )
+ )[0];
+
+ let observerPromise = expectObserverCalled(
+ "getUserMedia:request",
+ 1,
+ frame1ObserveBC
+ );
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true, frame1ID, undefined, frame1BC);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow",
+ 1,
+ frame1ObserveBC
+ );
+ let observerPromise2 = expectObserverCalled(
+ "recording-device-events",
+ 1,
+ frame1ObserveBC
+ );
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ video: true, audio: true });
+
+ // Stop sharing for camera and test that we stopped sharing.
+ await stopSharing("camera", false, frame1ObserveBC);
+
+ // There shouldn't be any grace period permissions at this point.
+ ok(
+ !SitePermissions.getAllForBrowser(aBrowser).length,
+ "Should not set any permissions."
+ );
+
+ // A new request should result in a prompt.
+ observerPromise = expectObserverCalled(
+ "getUserMedia:request",
+ 1,
+ frame1ObserveBC
+ );
+ let notificationPromise = promisePopupNotificationShown(
+ "webRTC-shareDevices"
+ );
+ await promiseRequestDevice(true, true, frame1ID, undefined, frame1BC);
+ await notificationPromise;
+ await observerPromise;
+
+ let denyPromise = expectObserverCalled(
+ "getUserMedia:response:deny",
+ 1,
+ frame1ObserveBC
+ );
+ let recordingEndedPromise = expectObserverCalled(
+ "recording-window-ended",
+ 1,
+ frame1ObserveBC
+ );
+ const permissionError =
+ "error: NotAllowedError: The request is not allowed " +
+ "by the user agent or the platform in the current context.";
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+ await denyPromise;
+ await recordingEndedPromise;
+
+ // Clean up the temporary blocks from the prompt deny.
+ SitePermissions.clearTemporaryBlockPermissions(aBrowser);
+
+ // the stream is already closed, but this will do some cleanup anyway
+ await closeStream(true, frame1ID, undefined, frame1BC, frame1ObserveBC);
+ },
+ },
+
+ {
+ desc:
+ "getUserMedia audio+video: reloading the frame removes all sharing UI",
+ run: async function checkReloading(aBrowser, aSubFrames) {
+ let { bc: frame1BC, id: frame1ID, observeBC: frame1ObserveBC } = (
+ await getBrowsingContextsAndFrameIdsForSubFrames(
+ aBrowser.browsingContext,
+ aSubFrames
+ )
+ )[0];
+
+ let observerPromise = expectObserverCalled(
+ "getUserMedia:request",
+ 1,
+ frame1ObserveBC
+ );
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true, frame1ID, undefined, frame1BC);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow",
+ 1,
+ frame1ObserveBC
+ );
+ let observerPromise2 = expectObserverCalled(
+ "recording-device-events",
+ 1,
+ frame1ObserveBC
+ );
+ let indicator = promiseIndicatorWindow();
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ video: true, audio: true });
+
+ // Disable while loading a new page
+ await disableObserverVerification();
+
+ info("reloading the frame");
+ let promises = [
+ expectObserverCalledOnClose(
+ "recording-device-stopped",
+ 1,
+ frame1ObserveBC
+ ),
+ expectObserverCalledOnClose(
+ "recording-device-events",
+ 1,
+ frame1ObserveBC
+ ),
+ expectObserverCalledOnClose(
+ "recording-window-ended",
+ 1,
+ frame1ObserveBC
+ ),
+ ];
+ await promiseReloadFrame(frame1ID, frame1BC);
+ await Promise.all(promises);
+
+ await enableObserverVerification();
+
+ await checkNotSharing();
+ },
+ },
+
+ {
+ desc: "getUserMedia audio+video: reloading the frame removes prompts",
+ run: async function checkReloadingRemovesPrompts(aBrowser, aSubFrames) {
+ let { bc: frame1BC, id: frame1ID, observeBC: frame1ObserveBC } = (
+ await getBrowsingContextsAndFrameIdsForSubFrames(
+ aBrowser.browsingContext,
+ aSubFrames
+ )
+ )[0];
+
+ let observerPromise = expectObserverCalled(
+ "getUserMedia:request",
+ 1,
+ frame1ObserveBC
+ );
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true, frame1ID, undefined, frame1BC);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ info("reloading the frame");
+ promise = expectObserverCalledOnClose(
+ "recording-window-ended",
+ 1,
+ frame1ObserveBC
+ );
+ await promiseReloadFrame(frame1ID, frame1BC);
+ await promise;
+ await promiseNoPopupNotification("webRTC-shareDevices");
+
+ await checkNotSharing();
+ },
+ },
+
+ {
+ desc:
+ "getUserMedia audio+video: with two frames sharing at the same time, sharing UI shows all shared devices",
+ run: async function checkFrameOverridingSharingUI(aBrowser, aSubFrames) {
+ // This tests an edge case discovered in bug 1440356 that works like this
+ // - Share audio and video in iframe 1.
+ // - Share only video in iframe 2.
+ // The WebRTC UI should still show both video and audio indicators.
+
+ let bcsAndFrameIds = await getBrowsingContextsAndFrameIdsForSubFrames(
+ aBrowser.browsingContext,
+ aSubFrames
+ );
+ let {
+ bc: frame1BC,
+ id: frame1ID,
+ observeBC: frame1ObserveBC,
+ } = bcsAndFrameIds[0];
+
+ let observerPromise = expectObserverCalled(
+ "getUserMedia:request",
+ 1,
+ frame1ObserveBC
+ );
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true, frame1ID, undefined, frame1BC);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow",
+ 1,
+ frame1ObserveBC
+ );
+ let observerPromise2 = expectObserverCalled(
+ "recording-device-events",
+ 1,
+ frame1ObserveBC
+ );
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ video: true, audio: true });
+
+ // Check that requesting a new device from a different frame
+ // doesn't override sharing UI.
+ let {
+ bc: frame2BC,
+ id: frame2ID,
+ observeBC: frame2ObserveBC,
+ } = bcsAndFrameIds[1];
+
+ observerPromise = expectObserverCalled(
+ "getUserMedia:request",
+ 1,
+ frame2ObserveBC
+ );
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true, frame2ID, undefined, frame2BC);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["camera"]);
+
+ observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow",
+ 1,
+ frame2ObserveBC
+ );
+ observerPromise2 = expectObserverCalled(
+ "recording-device-events",
+ 1,
+ frame2ObserveBC
+ );
+
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+
+ await checkSharingUI({ video: true, audio: true });
+
+ // Check that ending the stream with the other frame
+ // doesn't override sharing UI.
+
+ observerPromise = expectObserverCalledOnClose(
+ "recording-window-ended",
+ 1,
+ frame2ObserveBC
+ );
+ promise = expectObserverCalledOnClose(
+ "recording-device-events",
+ 1,
+ frame2ObserveBC
+ );
+ await promiseReloadFrame(frame2ID, frame2BC);
+ await promise;
+
+ await observerPromise;
+ await checkSharingUI({ video: true, audio: true });
+
+ await closeStream(false, frame1ID, undefined, frame1BC, frame1ObserveBC);
+ await checkNotSharing();
+ },
+ },
+
+ {
+ desc: "getUserMedia audio+video: reloading a frame updates the sharing UI",
+ run: async function checkUpdateWhenReloading(aBrowser, aSubFrames) {
+ // We'll share only the cam in the first frame, then share both in the
+ // second frame, then reload the second frame. After each step, we'll check
+ // the UI is in the correct state.
+ let bcsAndFrameIds = await getBrowsingContextsAndFrameIdsForSubFrames(
+ aBrowser.browsingContext,
+ aSubFrames
+ );
+ let {
+ bc: frame1BC,
+ id: frame1ID,
+ observeBC: frame1ObserveBC,
+ } = bcsAndFrameIds[0];
+
+ let observerPromise = expectObserverCalled(
+ "getUserMedia:request",
+ 1,
+ frame1ObserveBC
+ );
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true, frame1ID, undefined, frame1BC);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["camera"]);
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow",
+ 1,
+ frame1ObserveBC
+ );
+ let observerPromise2 = expectObserverCalled(
+ "recording-device-events",
+ 1,
+ frame1ObserveBC
+ );
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { video: true },
+ "expected camera to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ video: true, audio: false });
+
+ let {
+ bc: frame2BC,
+ id: frame2ID,
+ observeBC: frame2ObserveBC,
+ } = bcsAndFrameIds[1];
+
+ observerPromise = expectObserverCalled(
+ "getUserMedia:request",
+ 1,
+ frame2ObserveBC
+ );
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true, frame2ID, undefined, frame2BC);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow",
+ 1,
+ frame2ObserveBC
+ );
+ observerPromise2 = expectObserverCalled(
+ "recording-device-events",
+ 1,
+ frame2ObserveBC
+ );
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+
+ await checkSharingUI({ video: true, audio: true });
+
+ info("reloading the second frame");
+
+ observerPromise1 = expectObserverCalledOnClose(
+ "recording-device-events",
+ 1,
+ frame2ObserveBC
+ );
+ observerPromise2 = expectObserverCalledOnClose(
+ "recording-window-ended",
+ 1,
+ frame2ObserveBC
+ );
+ await promiseReloadFrame(frame2ID, frame2BC);
+ await observerPromise1;
+ await observerPromise2;
+
+ await checkSharingUI({ video: true, audio: false });
+
+ await closeStream(false, frame1ID, undefined, frame1BC, frame1ObserveBC);
+ await checkNotSharing();
+ },
+ },
+
+ {
+ desc:
+ "getUserMedia audio+video: reloading the top level page removes all sharing UI",
+ run: async function checkReloading(aBrowser, aSubFrames) {
+ let { bc: frame1BC, id: frame1ID, observeBC: frame1ObserveBC } = (
+ await getBrowsingContextsAndFrameIdsForSubFrames(
+ aBrowser.browsingContext,
+ aSubFrames
+ )
+ )[0];
+
+ let observerPromise = expectObserverCalled(
+ "getUserMedia:request",
+ 1,
+ frame1ObserveBC
+ );
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true, frame1ID, undefined, frame1BC);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow",
+ 1,
+ frame1ObserveBC
+ );
+ let observerPromise2 = expectObserverCalled(
+ "recording-device-events",
+ 1,
+ frame1ObserveBC
+ );
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ video: true, audio: true });
+
+ await reloadAndAssertClosedStreams();
+ },
+ },
+
+ {
+ desc:
+ "getUserMedia audio+video: closing a window with two frames sharing at the same time, closes the indicator",
+ skipObserverVerification: true,
+ run: async function checkFrameIndicatorClosedUI(aBrowser, aSubFrames) {
+ // This tests a case where the indicator didn't close when audio/video is
+ // shared in two subframes and then the tabs are closed.
+
+ let tabsToRemove = [gBrowser.selectedTab];
+
+ for (let t = 0; t < 2; t++) {
+ let { bc: frame1BC, id: frame1ID, observeBC: frame1ObserveBC } = (
+ await getBrowsingContextsAndFrameIdsForSubFrames(
+ gBrowser.selectedBrowser.browsingContext,
+ aSubFrames
+ )
+ )[0];
+
+ let observerPromise = expectObserverCalled(
+ "getUserMedia:request",
+ 1,
+ frame1ObserveBC
+ );
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true, frame1ID, undefined, frame1BC);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ // During the second pass, the indicator is already open.
+ let indicator = t == 0 ? promiseIndicatorWindow() : Promise.resolve();
+
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow",
+ 1,
+ frame1ObserveBC
+ );
+ let observerPromise2 = expectObserverCalled(
+ "recording-device-events",
+ 1,
+ frame1ObserveBC
+ );
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ video: true, audio: true });
+
+ // The first time around, open another tab with the same uri.
+ // The second time, just open a normal test tab.
+ let uri = t == 0 ? gBrowser.selectedBrowser.currentURI.spec : undefined;
+ tabsToRemove.push(
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, uri)
+ );
+ }
+
+ BrowserTestUtils.removeTab(tabsToRemove[0]);
+ BrowserTestUtils.removeTab(tabsToRemove[1]);
+
+ await checkNotSharing();
+ },
+ },
+];
+
+add_task(async function test_inprocess() {
+ await runTests(gTests, {
+ relativeURI: "get_user_media_in_frame.html",
+ subFrames: { frame1: {}, frame2: {} },
+ });
+});
+
+add_task(async function test_outofprocess() {
+ const origin1 = encodeURI("https://test1.example.org");
+ const origin2 = encodeURI("https://www.mozilla.org:443");
+ const query = `origin=${origin1}&origin=${origin2}`;
+ const observe = SpecialPowers.useRemoteSubframes;
+ await runTests(gTests, {
+ relativeURI: `get_user_media_in_frame.html?${query}`,
+ subFrames: { frame1: { observe }, frame2: { observe } },
+ });
+});
+
+add_task(async function test_inprocess_in_outofprocess() {
+ const oopOrigin = encodeURI("https://www.mozilla.org");
+ const sameOrigin = encodeURI("https://example.com");
+ const query = `origin=${oopOrigin}&nested=${sameOrigin}&nested=${sameOrigin}`;
+ await runTests(gTests, {
+ relativeURI: `get_user_media_in_frame.html?${query}`,
+ subFrames: {
+ frame1: {
+ noTest: true,
+ children: { frame1: {}, frame2: {} },
+ },
+ },
+ });
+});
diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media_in_xorigin_frame.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_in_xorigin_frame.js
new file mode 100644
index 0000000000..0e9ef229a7
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_in_xorigin_frame.js
@@ -0,0 +1,804 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+const permissionError =
+ "error: NotAllowedError: The request is not allowed " +
+ "by the user agent or the platform in the current context.";
+
+const PromptResult = {
+ ALLOW: "allow",
+ DENY: "deny",
+ PROMPT: "prompt",
+};
+
+const Perms = Services.perms;
+
+async function promptNoDelegate(aThirdPartyOrgin, audio = true, video = true) {
+ // Persistent allowed first party origin
+ const uri = gBrowser.selectedBrowser.documentURI;
+ if (audio) {
+ PermissionTestUtils.add(uri, "microphone", Services.perms.ALLOW_ACTION);
+ }
+ if (video) {
+ PermissionTestUtils.add(uri, "camera", Services.perms.ALLOW_ACTION);
+ }
+
+ // Check that we get a prompt.
+ const observerPromise = expectObserverCalled("getUserMedia:request");
+ const promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(audio, video, "frame4");
+ await promise;
+ await observerPromise;
+
+ // The 'Remember this decision' checkbox is hidden.
+ const notification = PopupNotifications.panel.firstElementChild;
+ const checkbox = notification.checkbox;
+ ok(!!checkbox, "checkbox is present");
+ ok(checkbox.hidden, "checkbox is not visible");
+ ok(!checkbox.checked, "checkbox not checked");
+
+ // Check the label of the notification should be the first party
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").options.name,
+ uri.host,
+ "Use first party's origin"
+ );
+
+ // Check the secondName of the notification should be the third party
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").options
+ .secondName,
+ aThirdPartyOrgin,
+ "Use third party's origin as secondName"
+ );
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled("getUserMedia:response:allow");
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () =>
+ EventUtils.synthesizeMouseAtCenter(notification.button, {})
+ );
+ await observerPromise1;
+ await observerPromise2;
+ let state = await getMediaCaptureState();
+ is(
+ !!state.audio,
+ audio,
+ `expected microphone to be ${audio ? "" : "not"} shared`
+ );
+ is(
+ !!state.video,
+ video,
+ `expected camera to be ${video ? "" : "not"} shared`
+ );
+ await indicator;
+ await checkSharingUI({ audio, video }, undefined, undefined, {
+ video: { scope: SitePermissions.SCOPE_PERSISTENT },
+ audio: { scope: SitePermissions.SCOPE_PERSISTENT },
+ });
+
+ // Cleanup.
+ await closeStream(false, "frame4");
+
+ PermissionTestUtils.remove(uri, "camera");
+ PermissionTestUtils.remove(uri, "microphone");
+}
+
+async function promptNoDelegateScreenSharing(aThirdPartyOrgin) {
+ // Persistent allow screen sharing
+ const uri = gBrowser.selectedBrowser.documentURI;
+ PermissionTestUtils.add(uri, "screen", Services.perms.ALLOW_ACTION);
+
+ const observerPromise = expectObserverCalled("getUserMedia:request");
+ const promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true, "frame4", "screen");
+ await promise;
+ await observerPromise;
+
+ checkDeviceSelectors(["screen"]);
+ const notification = PopupNotifications.panel.firstElementChild;
+
+ // The 'Remember this decision' checkbox is hidden.
+ const checkbox = notification.checkbox;
+ ok(!!checkbox, "checkbox is present");
+
+ if (ALLOW_SILENCING_NOTIFICATIONS) {
+ ok(!checkbox.hidden, "Notification silencing checkbox is visible");
+ } else {
+ ok(checkbox.hidden, "checkbox is not visible");
+ }
+
+ ok(!checkbox.checked, "checkbox not checked");
+
+ // Check the label of the notification should be the first party
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").options.name,
+ uri.host,
+ "Use first party's origin"
+ );
+
+ // Check the secondName of the notification should be the third party
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").options
+ .secondName,
+ aThirdPartyOrgin,
+ "Use third party's origin as secondName"
+ );
+
+ const menulist = document.getElementById("webRTC-selectWindow-menulist");
+ const count = menulist.itemCount;
+ menulist.getItemAtIndex(count - 1).doCommand();
+ ok(!notification.button.disabled, "Allow button is enabled");
+
+ const indicator = promiseIndicatorWindow();
+ const observerPromise1 = expectObserverCalled("getUserMedia:response:allow");
+ const observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () =>
+ EventUtils.synthesizeMouseAtCenter(notification.button, {})
+ );
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { screen: "Screen" },
+ "expected screen to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ screen: "Screen" }, undefined, undefined, {
+ screen: { scope: SitePermissions.SCOPE_PERSISTENT },
+ });
+ await closeStream(false, "frame4");
+
+ PermissionTestUtils.remove(uri, "screen");
+}
+
+var gTests = [
+ {
+ desc:
+ "'Always Allow' enabled on third party pages, when origin is explicitly allowed",
+ run: async function checkNoAlwaysOnThirdParty() {
+ // Initially set both permissions to 'prompt'.
+ const uri = gBrowser.selectedBrowser.documentURI;
+ PermissionTestUtils.add(uri, "microphone", Services.perms.PROMPT_ACTION);
+ PermissionTestUtils.add(uri, "camera", Services.perms.PROMPT_ACTION);
+
+ const observerPromise = expectObserverCalled("getUserMedia:request");
+ const promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true, "frame1");
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ // The 'Remember this decision' checkbox is visible.
+ const notification = PopupNotifications.panel.firstElementChild;
+ const checkbox = notification.checkbox;
+ ok(!!checkbox, "checkbox is present");
+ ok(!checkbox.hidden, "checkbox is visible");
+ ok(!checkbox.checked, "checkbox not checked");
+
+ // Check the label of the notification should be the first party
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").options.name,
+ uri.host,
+ "Use first party's origin"
+ );
+
+ const indicator = promiseIndicatorWindow();
+ const observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ const observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () =>
+ EventUtils.synthesizeMouseAtCenter(notification.button, {})
+ );
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+ await indicator;
+ await checkSharingUI({ audio: true, video: true });
+
+ // Cleanup.
+ await closeStream(false, "frame1");
+ PermissionTestUtils.remove(uri, "camera");
+ PermissionTestUtils.remove(uri, "microphone");
+ },
+ },
+ {
+ desc:
+ "'Always Allow' disabled when sharing screen in third party iframes, when origin is explicitly allowed",
+ run: async function checkScreenSharing() {
+ const observerPromise = expectObserverCalled("getUserMedia:request");
+ const promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true, "frame1", "screen");
+ await promise;
+ await observerPromise;
+
+ checkDeviceSelectors(["screen"]);
+ const notification = PopupNotifications.panel.firstElementChild;
+
+ // The 'Remember this decision' checkbox is visible.
+ const checkbox = notification.checkbox;
+ ok(!!checkbox, "checkbox is present");
+ ok(!checkbox.hidden, "checkbox is visible");
+ ok(!checkbox.checked, "checkbox not checked");
+
+ const menulist = document.getElementById("webRTC-selectWindow-menulist");
+ const count = menulist.itemCount;
+ ok(
+ count >= 4,
+ "There should be the 'Select Window or Screen' item, a separator and at least one window and one screen"
+ );
+
+ const noWindowOrScreenItem = menulist.getItemAtIndex(0);
+ ok(
+ noWindowOrScreenItem.hasAttribute("selected"),
+ "the 'Select Window or Screen' item is selected"
+ );
+ is(
+ menulist.selectedItem,
+ noWindowOrScreenItem,
+ "'Select Window or Screen' is the selected item"
+ );
+ is(menulist.value, "-1", "no window or screen is selected by default");
+ ok(
+ noWindowOrScreenItem.disabled,
+ "'Select Window or Screen' item is disabled"
+ );
+ ok(notification.button.disabled, "Allow button is disabled");
+ ok(
+ notification.hasAttribute("invalidselection"),
+ "Notification is marked as invalid"
+ );
+
+ menulist.getItemAtIndex(count - 1).doCommand();
+ ok(!notification.button.disabled, "Allow button is enabled");
+
+ const indicator = promiseIndicatorWindow();
+ const observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ const observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () =>
+ EventUtils.synthesizeMouseAtCenter(notification.button, {})
+ );
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { screen: "Screen" },
+ "expected screen to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ screen: "Screen" });
+ await closeStream(false, "frame1");
+ },
+ },
+
+ {
+ desc: "getUserMedia use persistent permissions from first party",
+ run: async function checkUsePersistentPermissionsFirstParty() {
+ async function checkPersistentPermission(
+ aPermission,
+ aRequestType,
+ aIframeId,
+ aExpect
+ ) {
+ info(
+ `Test persistent permission ${aPermission} type ${aRequestType} expect ${aExpect}`
+ );
+ const uri = gBrowser.selectedBrowser.documentURI;
+ // Persistent allow/deny for first party uri
+ PermissionTestUtils.add(uri, aRequestType, aPermission);
+
+ let audio = aRequestType == "microphone";
+ let video = aRequestType == "camera";
+ const screen = aRequestType == "screen" ? "screen" : undefined;
+ if (screen) {
+ audio = false;
+ video = true;
+ }
+ if (aExpect == PromptResult.PROMPT) {
+ // Check that we get a prompt.
+ const observerPromise = expectObserverCalled("getUserMedia:request");
+ const observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:deny"
+ );
+ const observerPromise2 = expectObserverCalled(
+ "recording-window-ended"
+ );
+ const promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(audio, video, aIframeId, screen);
+ await promise;
+ await observerPromise;
+
+ // Check the label of the notification should be the first party
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").options
+ .name,
+ uri.host,
+ "Use first party's origin"
+ );
+
+ // Deny the request to cleanup...
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+ await observerPromise1;
+ await observerPromise2;
+ let browser = gBrowser.selectedBrowser;
+ SitePermissions.removeFromPrincipal(null, aRequestType, browser);
+ } else if (aExpect == PromptResult.ALLOW) {
+ const observerPromise = expectObserverCalled("getUserMedia:request");
+ const observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ const observerPromise2 = expectObserverCalled(
+ "recording-device-events"
+ );
+ const promise = promiseMessage("ok");
+ await promiseRequestDevice(audio, video, aIframeId, screen);
+ await promise;
+ await observerPromise;
+
+ await promiseNoPopupNotification("webRTC-shareDevices");
+ await observerPromise1;
+ await observerPromise2;
+
+ let expected = {};
+ if (audio) {
+ expected.audio = audio;
+ }
+ if (video) {
+ expected.video = video;
+ }
+
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ expected,
+ "expected " + Object.keys(expected).join(" and ") + " to be shared"
+ );
+
+ await closeStream(false, aIframeId);
+ } else if (aExpect == PromptResult.DENY) {
+ const promises = [];
+ // frame3 disallows by feature Permissions Policy before request.
+ if (aIframeId != "frame3") {
+ promises.push(
+ expectObserverCalled("getUserMedia:request"),
+ expectObserverCalled("getUserMedia:response:deny")
+ );
+ }
+ promises.push(
+ expectObserverCalled("recording-window-ended"),
+ promiseMessage(permissionError),
+ promiseRequestDevice(audio, video, aIframeId, screen)
+ );
+ await Promise.all(promises);
+ }
+
+ PermissionTestUtils.remove(uri, aRequestType);
+ }
+
+ await checkPersistentPermission(
+ Perms.PROMPT_ACTION,
+ "camera",
+ "frame1",
+ PromptResult.PROMPT
+ );
+ await checkPersistentPermission(
+ Perms.DENY_ACTION,
+ "camera",
+ "frame1",
+ PromptResult.DENY
+ );
+ await checkPersistentPermission(
+ Perms.ALLOW_ACTION,
+ "camera",
+ "frame1",
+ PromptResult.ALLOW
+ );
+
+ await checkPersistentPermission(
+ Perms.PROMPT_ACTION,
+ "microphone",
+ "frame1",
+ PromptResult.PROMPT
+ );
+ await checkPersistentPermission(
+ Perms.DENY_ACTION,
+ "microphone",
+ "frame1",
+ PromptResult.DENY
+ );
+ await checkPersistentPermission(
+ Perms.ALLOW_ACTION,
+ "microphone",
+ "frame1",
+ PromptResult.ALLOW
+ );
+
+ // Wildcard attributes still get delegation when their src is unchanged.
+ await checkPersistentPermission(
+ Perms.PROMPT_ACTION,
+ "camera",
+ "frame4",
+ PromptResult.PROMPT
+ );
+ await checkPersistentPermission(
+ Perms.DENY_ACTION,
+ "camera",
+ "frame4",
+ PromptResult.DENY
+ );
+ await checkPersistentPermission(
+ Perms.ALLOW_ACTION,
+ "camera",
+ "frame4",
+ PromptResult.ALLOW
+ );
+
+ // Wildcard attributes still get delegation when their src is unchanged.
+ await checkPersistentPermission(
+ Perms.PROMPT_ACTION,
+ "microphone",
+ "frame4",
+ PromptResult.PROMPT
+ );
+ await checkPersistentPermission(
+ Perms.DENY_ACTION,
+ "microphone",
+ "frame4",
+ PromptResult.DENY
+ );
+ await checkPersistentPermission(
+ Perms.ALLOW_ACTION,
+ "microphone",
+ "frame4",
+ PromptResult.ALLOW
+ );
+
+ await checkPersistentPermission(
+ Perms.PROMPT_ACTION,
+ "screen",
+ "frame1",
+ PromptResult.PROMPT
+ );
+ await checkPersistentPermission(
+ Perms.DENY_ACTION,
+ "screen",
+ "frame1",
+ PromptResult.DENY
+ );
+ // Always prompt screen sharing
+ await checkPersistentPermission(
+ Perms.ALLOW_ACTION,
+ "screen",
+ "frame1",
+ PromptResult.PROMPT
+ );
+ await checkPersistentPermission(
+ Perms.ALLOW_ACTION,
+ "screen",
+ "frame4",
+ PromptResult.PROMPT
+ );
+
+ // Denied by default if allow is not defined
+ await checkPersistentPermission(
+ Perms.PROMPT_ACTION,
+ "camera",
+ "frame3",
+ PromptResult.DENY
+ );
+ await checkPersistentPermission(
+ Perms.DENY_ACTION,
+ "camera",
+ "frame3",
+ PromptResult.DENY
+ );
+ await checkPersistentPermission(
+ Perms.ALLOW_ACTION,
+ "camera",
+ "frame3",
+ PromptResult.DENY
+ );
+
+ await checkPersistentPermission(
+ Perms.PROMPT_ACTION,
+ "microphone",
+ "frame3",
+ PromptResult.DENY
+ );
+ await checkPersistentPermission(
+ Perms.DENY_ACTION,
+ "microphone",
+ "frame3",
+ PromptResult.DENY
+ );
+ await checkPersistentPermission(
+ Perms.ALLOW_ACTION,
+ "microphone",
+ "frame3",
+ PromptResult.DENY
+ );
+
+ await checkPersistentPermission(
+ Perms.PROMPT_ACTION,
+ "screen",
+ "frame3",
+ PromptResult.DENY
+ );
+ await checkPersistentPermission(
+ Perms.DENY_ACTION,
+ "screen",
+ "frame3",
+ PromptResult.DENY
+ );
+ await checkPersistentPermission(
+ Perms.ALLOW_ACTION,
+ "screen",
+ "frame3",
+ PromptResult.DENY
+ );
+ },
+ },
+
+ {
+ desc: "getUserMedia use temporary blocked permissions from first party",
+ run: async function checkUseTempPermissionsBlockFirstParty() {
+ async function checkTempPermission(aRequestType) {
+ let browser = gBrowser.selectedBrowser;
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:deny"
+ );
+ let observerPromise2 = expectObserverCalled("recording-window-ended");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ let audio = aRequestType == "microphone";
+ let video = aRequestType == "camera";
+ const screen = aRequestType == "screen" ? "screen" : undefined;
+ if (screen) {
+ audio = false;
+ video = true;
+ }
+
+ await promiseRequestDevice(audio, video, null, screen);
+ await promise;
+ await observerPromise;
+
+ // Temporarily grant/deny from top level
+ // Only need to check allow and deny temporary permissions
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+ await observerPromise1;
+ await observerPromise2;
+ await checkNotSharing();
+
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ observerPromise1 = expectObserverCalled("getUserMedia:response:deny");
+ observerPromise2 = expectObserverCalled("recording-window-ended");
+ promise = promiseMessage(permissionError);
+ await promiseRequestDevice(audio, video, "frame1", screen);
+ await promise;
+
+ await observerPromise;
+ await observerPromise1;
+ await observerPromise2;
+
+ SitePermissions.removeFromPrincipal(null, aRequestType, browser);
+ }
+
+ // At the moment we only save temporary deny
+ await checkTempPermission("camera");
+ await checkTempPermission("microphone");
+ await checkTempPermission("screen");
+ },
+ },
+ {
+ desc:
+ "Don't reprompt while actively sharing in maybe unsafe permission delegation",
+ run: async function checkNoRepromptNoDelegate() {
+ // Change location to ensure that we're treated as potentially unsafe.
+ await promiseChangeLocationFrame(
+ "frame4",
+ "https://test2.example.com/browser/browser/base/content/test/webrtc/get_user_media.html"
+ );
+
+ // Check that we get a prompt.
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true, "frame4");
+ await promise;
+ await observerPromise;
+
+ // Check the secondName of the notification should be the third party
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").options
+ .secondName,
+ "test2.example.com",
+ "Use third party's origin as secondName"
+ );
+
+ const notification = PopupNotifications.panel.firstElementChild;
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () =>
+ EventUtils.synthesizeMouseAtCenter(notification.button, {})
+ );
+ await observerPromise1;
+ await observerPromise2;
+
+ let state = await getMediaCaptureState();
+ is(!!state.audio, true, "expected microphone to be shared");
+ is(!!state.video, true, "expected camera to be shared");
+ await indicator;
+ await checkSharingUI({ audio: true, video: true });
+
+ // Check that we now don't get a prompt.
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ observerPromise1 = expectObserverCalled("getUserMedia:response:allow");
+ observerPromise2 = expectObserverCalled("recording-device-events");
+ promise = promiseMessage("ok");
+ await promiseRequestDevice(true, true, "frame4");
+ await promise;
+ await observerPromise;
+
+ await promiseNoPopupNotification("webRTC-shareDevices");
+ await observerPromise1;
+ await observerPromise2;
+
+ state = await getMediaCaptureState();
+ is(!!state.audio, true, "expected microphone to be shared");
+ is(!!state.video, true, "expected camera to be shared");
+ await checkSharingUI({ audio: true, video: true });
+
+ // Cleanup.
+ await closeStream(false, "frame4");
+ },
+ },
+ {
+ desc:
+ "Change location, prompt and display both first party and third party origin in maybe unsafe permission delegation",
+ run: async function checkPromptNoDelegateChangeLoxation() {
+ await promiseChangeLocationFrame(
+ "frame4",
+ "https://test2.example.com/browser/browser/base/content/test/webrtc/get_user_media.html"
+ );
+ await promptNoDelegate("test2.example.com");
+ },
+ },
+ {
+ desc:
+ "Change location, prompt and display both first party and third party origin when sharing screen in unsafe permission delegation",
+ run: async function checkPromptNoDelegateScreenSharingChangeLocation() {
+ await promiseChangeLocationFrame(
+ "frame4",
+ "https://test2.example.com/browser/browser/base/content/test/webrtc/get_user_media.html"
+ );
+ await promptNoDelegateScreenSharing("test2.example.com");
+ },
+ },
+ {
+ desc:
+ "Prompt and display both first party and third party origin and temporary deny in frame does not change permission scope",
+ skipObserverVerification: true,
+ run: async function checkPromptBothOriginsTempDenyFrame() {
+ // Change location to ensure that we're treated as potentially unsafe.
+ await promiseChangeLocationFrame(
+ "frame4",
+ "https://test2.example.com/browser/browser/base/content/test/webrtc/get_user_media.html"
+ );
+
+ // Persistent allowed first party origin
+ let browser = gBrowser.selectedBrowser;
+ let uri = gBrowser.selectedBrowser.documentURI;
+ let principal = Services.scriptSecurityManager.createContentPrincipal(
+ uri,
+ {}
+ );
+
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true);
+ await promise;
+ await observerPromise;
+
+ // Ensure that checking the 'Remember this decision'
+ let notification = PopupNotifications.panel.firstElementChild;
+ let checkbox = notification.checkbox;
+ ok(!!checkbox, "checkbox is present");
+ ok(!checkbox.checked, "checkbox is not checked");
+ checkbox.click();
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () =>
+ EventUtils.synthesizeMouseAtCenter(notification.button, {})
+ );
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+ await indicator;
+ await checkSharingUI({ audio: true, video: true }, undefined, undefined, {
+ audio: { scope: SitePermissions.SCOPE_PERSISTENT },
+ video: { scope: SitePermissions.SCOPE_PERSISTENT },
+ });
+ await closeStream(true);
+
+ // Check that we get a prompt.
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true, "frame4");
+ await promise;
+ await observerPromise;
+
+ // The 'Remember this decision' checkbox is hidden.
+ notification = PopupNotifications.panel.firstElementChild;
+ checkbox = notification.checkbox;
+ ok(!!checkbox, "checkbox is present");
+ ok(checkbox.hidden, "checkbox is not visible");
+
+ observerPromise1 = expectObserverCalled("getUserMedia:response:deny");
+ observerPromise2 = expectObserverCalled("recording-window-ended");
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+
+ await observerPromise1;
+ await observerPromise2;
+ await checkNotSharing();
+
+ // Make sure we are not changing the scope and state of persistent
+ // permission
+ let { state, scope } = SitePermissions.getForPrincipal(
+ principal,
+ "camera",
+ browser
+ );
+ Assert.equal(state, SitePermissions.ALLOW);
+ Assert.equal(scope, SitePermissions.SCOPE_PERSISTENT);
+
+ ({ state, scope } = SitePermissions.getForPrincipal(
+ principal,
+ "microphone",
+ browser
+ ));
+ Assert.equal(state, SitePermissions.ALLOW);
+ Assert.equal(scope, SitePermissions.SCOPE_PERSISTENT);
+
+ PermissionTestUtils.remove(uri, "camera");
+ PermissionTestUtils.remove(uri, "microphone");
+ },
+ },
+];
+
+add_task(async function test() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["permissions.delegation.enabled", true],
+ ["dom.security.featurePolicy.header.enabled", true],
+ ["dom.security.featurePolicy.webidl.enabled", true],
+ ],
+ });
+
+ await runTests(gTests, {
+ relativeURI: "get_user_media_in_xorigin_frame.html",
+ });
+});
diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media_in_xorigin_frame_chain.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_in_xorigin_frame_chain.js
new file mode 100644
index 0000000000..cb1e69bdc4
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_in_xorigin_frame_chain.js
@@ -0,0 +1,252 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+const permissionError =
+ "error: NotAllowedError: The request is not allowed " +
+ "by the user agent or the platform in the current context.";
+
+const PromptResult = {
+ ALLOW: "allow",
+ DENY: "deny",
+ PROMPT: "prompt",
+};
+
+const Perms = Services.perms;
+
+function expectObserverCalledAncestor(aTopic, browsingContext) {
+ if (!gMultiProcessBrowser) {
+ return expectObserverCalledInProcess(aTopic);
+ }
+
+ return BrowserTestUtils.contentTopicObserved(browsingContext, aTopic);
+}
+
+function enableObserverVerificationAncestor(browsingContext) {
+ // Skip these checks in single process mode as it isn't worth implementing it.
+ if (!gMultiProcessBrowser) {
+ return Promise.resolve();
+ }
+
+ return BrowserTestUtils.startObservingTopics(browsingContext, observerTopics);
+}
+
+function disableObserverVerificationAncestor(browsingContextt) {
+ if (!gMultiProcessBrowser) {
+ return Promise.resolve();
+ }
+
+ return BrowserTestUtils.stopObservingTopics(
+ browsingContextt,
+ observerTopics
+ ).catch(reason => {
+ ok(false, "Failed " + reason);
+ });
+}
+
+function promiseRequestDeviceAncestor(
+ aRequestAudio,
+ aRequestVideo,
+ aType,
+ aBrowser,
+ aBadDevice = false
+) {
+ info("requesting devices");
+ return SpecialPowers.spawn(
+ aBrowser,
+ [{ aRequestAudio, aRequestVideo, aType, aBadDevice }],
+ async function(args) {
+ let global = content.wrappedJSObject.document.getElementById("frame4")
+ .contentWindow;
+ global.requestDevice(
+ args.aRequestAudio,
+ args.aRequestVideo,
+ args.aType,
+ args.aBadDevice
+ );
+ }
+ );
+}
+
+async function closeStreamAncestor(browser) {
+ let observerPromises = [];
+ observerPromises.push(
+ expectObserverCalledAncestor("recording-device-events", browser)
+ );
+ observerPromises.push(
+ expectObserverCalledAncestor("recording-window-ended", browser)
+ );
+
+ info("closing the stream");
+ await SpecialPowers.spawn(browser, [], async () => {
+ let global = content.wrappedJSObject.document.getElementById("frame4")
+ .contentWindow;
+ global.closeStream();
+ });
+
+ await Promise.all(observerPromises);
+
+ await assertWebRTCIndicatorStatus(null);
+}
+
+var gTests = [
+ {
+ desc:
+ "getUserMedia use persistent permissions from first party if third party is explicitly trusted",
+ skipObserverVerification: true,
+ run: async function checkPermissionsAncestorChain() {
+ async function checkPermission(aPermission, aRequestType, aExpect) {
+ info(
+ `Test persistent permission ${aPermission} type ${aRequestType} expect ${aExpect}`
+ );
+ const uri = gBrowser.selectedBrowser.documentURI;
+ // Persistent allow/deny for first party uri
+ PermissionTestUtils.add(uri, aRequestType, aPermission);
+
+ let audio = aRequestType == "microphone";
+ let video = aRequestType == "camera";
+ const screen = aRequestType == "screen" ? "screen" : undefined;
+ if (screen) {
+ audio = false;
+ video = true;
+ }
+ const iframeAncestor = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ () => {
+ return content.document.getElementById("frameAncestor")
+ .browsingContext;
+ }
+ );
+
+ if (aExpect == PromptResult.PROMPT) {
+ // Check that we get a prompt.
+ const observerPromise = expectObserverCalledAncestor(
+ "getUserMedia:request",
+ iframeAncestor
+ );
+ const promise = promisePopupNotificationShown("webRTC-shareDevices");
+
+ await promiseRequestDeviceAncestor(
+ audio,
+ video,
+ screen,
+ iframeAncestor
+ );
+ await promise;
+ await observerPromise;
+
+ // Check the label of the notification should be the first party
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").options
+ .name,
+ uri.host,
+ "Use first party's origin"
+ );
+ const observerPromise1 = expectObserverCalledAncestor(
+ "getUserMedia:response:deny",
+ iframeAncestor
+ );
+ const observerPromise2 = expectObserverCalledAncestor(
+ "recording-window-ended",
+ iframeAncestor
+ );
+ // Deny the request to cleanup...
+ activateSecondaryAction(kActionDeny);
+ await observerPromise1;
+ await observerPromise2;
+ let browser = gBrowser.selectedBrowser;
+ SitePermissions.removeFromPrincipal(null, aRequestType, browser);
+ } else if (aExpect == PromptResult.ALLOW) {
+ const observerPromise = expectObserverCalledAncestor(
+ "getUserMedia:request",
+ iframeAncestor
+ );
+ const observerPromise1 = expectObserverCalledAncestor(
+ "getUserMedia:response:allow",
+ iframeAncestor
+ );
+ const observerPromise2 = expectObserverCalledAncestor(
+ "recording-device-events",
+ iframeAncestor
+ );
+ await promiseRequestDeviceAncestor(
+ audio,
+ video,
+ screen,
+ iframeAncestor
+ );
+ await observerPromise;
+
+ await promiseNoPopupNotification("webRTC-shareDevices");
+ await observerPromise1;
+ await observerPromise2;
+
+ let expected = {};
+ if (audio) {
+ expected.audio = audio;
+ }
+ if (video) {
+ expected.video = video;
+ }
+
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ expected,
+ "expected " + Object.keys(expected).join(" and ") + " to be shared"
+ );
+
+ await closeStreamAncestor(iframeAncestor);
+ } else if (aExpect == PromptResult.DENY) {
+ const observerPromise = expectObserverCalledAncestor(
+ "recording-window-ended",
+ iframeAncestor
+ );
+ await promiseRequestDeviceAncestor(
+ audio,
+ video,
+ screen,
+ iframeAncestor
+ );
+ await observerPromise;
+ }
+
+ PermissionTestUtils.remove(uri, aRequestType);
+ }
+
+ await checkPermission(Perms.PROMPT_ACTION, "camera", PromptResult.PROMPT);
+ await checkPermission(Perms.DENY_ACTION, "camera", PromptResult.DENY);
+ await checkPermission(Perms.ALLOW_ACTION, "camera", PromptResult.ALLOW);
+
+ await checkPermission(
+ Perms.PROMPT_ACTION,
+ "microphone",
+ PromptResult.PROMPT
+ );
+ await checkPermission(Perms.DENY_ACTION, "microphone", PromptResult.DENY);
+ await checkPermission(
+ Perms.ALLOW_ACTION,
+ "microphone",
+ PromptResult.ALLOW
+ );
+
+ await checkPermission(Perms.PROMPT_ACTION, "screen", PromptResult.PROMPT);
+ await checkPermission(Perms.DENY_ACTION, "screen", PromptResult.DENY);
+ // Always prompt screen sharing
+ await checkPermission(Perms.ALLOW_ACTION, "screen", PromptResult.PROMPT);
+ },
+ },
+];
+
+add_task(async function test() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["permissions.delegation.enabled", true],
+ ["dom.security.featurePolicy.header.enabled", true],
+ ["dom.security.featurePolicy.webidl.enabled", true],
+ ],
+ });
+
+ await runTests(gTests, {
+ relativeURI: "get_user_media_in_xorigin_frame_ancestor.html",
+ });
+});
diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media_multi_process.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_multi_process.js
new file mode 100644
index 0000000000..98c474c58f
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_multi_process.js
@@ -0,0 +1,518 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var gTests = [
+ {
+ desc: "getUserMedia audio in a first process + video in a second process",
+ // These tests call enableObserverVerification manually on a second tab, so
+ // don't add listeners to the first tab.
+ skipObserverVerification: true,
+ run: async function checkMultiProcess() {
+ // The main purpose of this test is to ensure webrtc sharing indicators
+ // work with multiple content processes, but it makes sense to run this
+ // test without e10s too to ensure using webrtc devices in two different
+ // tabs is handled correctly.
+
+ // Request audio in the first tab.
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true);
+ await promise;
+ await observerPromise;
+
+ checkDeviceSelectors(["microphone"]);
+
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ let indicator = promiseIndicatorWindow();
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true },
+ "expected microphone to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ audio: true });
+
+ ok(
+ webrtcUI.showGlobalIndicator,
+ "webrtcUI wants the global indicator shown"
+ );
+ ok(
+ !webrtcUI.showCameraIndicator,
+ "webrtcUI wants the camera indicator hidden"
+ );
+ ok(
+ webrtcUI.showMicrophoneIndicator,
+ "webrtcUI wants the mic indicator shown"
+ );
+ is(
+ webrtcUI.getActiveStreams(false, true).length,
+ 1,
+ "1 active audio stream"
+ );
+ is(
+ webrtcUI.getActiveStreams(true, true, true).length,
+ 1,
+ "1 active stream"
+ );
+
+ // If we have reached the max process count already, increase it to ensure
+ // our new tab can have its own content process.
+ let childCount = Services.ppmm.childCount;
+ let maxContentProcess = Services.prefs.getIntPref("dom.ipc.processCount");
+ // The first check is because if we are on a branch where e10s-multi is
+ // disabled, we want to keep testing e10s with a single content process.
+ // The + 1 is because ppmm.childCount also counts the chrome process
+ // (which also runs process scripts).
+ if (maxContentProcess > 1 && childCount == maxContentProcess + 1) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.ipc.processCount", childCount]],
+ });
+ }
+
+ // Open a new tab with a different hostname.
+ let url = gBrowser.currentURI.spec.replace(
+ "https://example.com/",
+ "http://127.0.0.1:8888/"
+ );
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+
+ await enableObserverVerification();
+
+ // Request video.
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true);
+ await promise;
+ await observerPromise;
+
+ checkDeviceSelectors(["camera"]);
+
+ observerPromise1 = expectObserverCalled("getUserMedia:response:allow");
+ observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+
+ await checkSharingUI({ video: true }, window, {
+ audio: true,
+ video: true,
+ });
+
+ ok(
+ webrtcUI.showGlobalIndicator,
+ "webrtcUI wants the global indicator shown"
+ );
+ ok(
+ webrtcUI.showCameraIndicator,
+ "webrtcUI wants the camera indicator shown"
+ );
+ ok(
+ webrtcUI.showMicrophoneIndicator,
+ "webrtcUI wants the mic indicator shown"
+ );
+ is(
+ webrtcUI.getActiveStreams(false, true).length,
+ 1,
+ "1 active audio stream"
+ );
+ is(webrtcUI.getActiveStreams(true).length, 1, "1 active video stream");
+ is(
+ webrtcUI.getActiveStreams(true, true, true).length,
+ 2,
+ "2 active streams"
+ );
+
+ info("removing the second tab");
+
+ await disableObserverVerification();
+
+ BrowserTestUtils.removeTab(tab);
+
+ // Check that we still show the sharing indicators for the first tab's stream.
+ await Promise.all([
+ TestUtils.waitForCondition(() => !webrtcUI.showCameraIndicator),
+ TestUtils.waitForCondition(
+ () => webrtcUI.getActiveStreams(true, true, true).length == 1
+ ),
+ ]);
+
+ ok(
+ webrtcUI.showGlobalIndicator,
+ "webrtcUI wants the global indicator shown"
+ );
+ ok(
+ !webrtcUI.showCameraIndicator,
+ "webrtcUI wants the camera indicator hidden"
+ );
+ ok(
+ webrtcUI.showMicrophoneIndicator,
+ "webrtcUI wants the mic indicator shown"
+ );
+ is(
+ webrtcUI.getActiveStreams(false, true).length,
+ 1,
+ "1 active audio stream"
+ );
+
+ await checkSharingUI({ audio: true });
+
+ // Close the first tab's stream and verify that all indicators are removed.
+ await closeStream(false, null, true);
+
+ ok(
+ !webrtcUI.showGlobalIndicator,
+ "webrtcUI wants the global indicator hidden"
+ );
+ is(
+ webrtcUI.getActiveStreams(true, true, true).length,
+ 0,
+ "0 active streams"
+ );
+ },
+ },
+
+ {
+ desc: "getUserMedia camera in a first process + camera in a second process",
+ skipObserverVerification: true,
+ run: async function checkMultiProcessCamera() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ // Request camera in the first tab.
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true);
+ await promise;
+ await observerPromise;
+
+ checkDeviceSelectors(["camera"]);
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { video: true },
+ "expected camera to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ video: true });
+
+ ok(
+ webrtcUI.showGlobalIndicator,
+ "webrtcUI wants the global indicator shown"
+ );
+ ok(
+ webrtcUI.showCameraIndicator,
+ "webrtcUI wants the camera indicator shown"
+ );
+ ok(
+ !webrtcUI.showMicrophoneIndicator,
+ "webrtcUI wants the mic indicator hidden"
+ );
+ is(webrtcUI.getActiveStreams(true).length, 1, "1 active camera stream");
+ is(
+ webrtcUI.getActiveStreams(true, true, true).length,
+ 1,
+ "1 active stream"
+ );
+
+ // If we have reached the max process count already, increase it to ensure
+ // our new tab can have its own content process.
+ let childCount = Services.ppmm.childCount;
+ let maxContentProcess = Services.prefs.getIntPref("dom.ipc.processCount");
+ // The first check is because if we are on a branch where e10s-multi is
+ // disabled, we want to keep testing e10s with a single content process.
+ // The + 1 is because ppmm.childCount also counts the chrome process
+ // (which also runs process scripts).
+ if (maxContentProcess > 1 && childCount == maxContentProcess + 1) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.ipc.processCount", childCount]],
+ });
+ }
+
+ // Open a new tab with a different hostname.
+ let url = gBrowser.currentURI.spec.replace(
+ "https://example.com/",
+ "http://127.0.0.1:8888/"
+ );
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+
+ await enableObserverVerification();
+
+ // Request camera in the second tab.
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true);
+ await promise;
+ await observerPromise;
+
+ checkDeviceSelectors(["camera"]);
+
+ observerPromise1 = expectObserverCalled("getUserMedia:response:allow");
+ observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+
+ await checkSharingUI({ video: true }, window, { video: true });
+
+ ok(
+ webrtcUI.showGlobalIndicator,
+ "webrtcUI wants the global indicator shown"
+ );
+ ok(
+ webrtcUI.showCameraIndicator,
+ "webrtcUI wants the camera indicator shown"
+ );
+ ok(
+ !webrtcUI.showMicrophoneIndicator,
+ "webrtcUI wants the mic indicator hidden"
+ );
+ is(webrtcUI.getActiveStreams(true).length, 2, "2 active camera streams");
+ is(
+ webrtcUI.getActiveStreams(true, true, true).length,
+ 2,
+ "2 active streams"
+ );
+
+ await disableObserverVerification();
+
+ info("removing the second tab");
+ BrowserTestUtils.removeTab(tab);
+
+ // Check that we still show the sharing indicators for the first tab's stream.
+ await Promise.all([
+ TestUtils.waitForCondition(() => webrtcUI.showCameraIndicator),
+ TestUtils.waitForCondition(
+ () => webrtcUI.getActiveStreams(true).length == 1
+ ),
+ ]);
+ ok(
+ webrtcUI.showGlobalIndicator,
+ "webrtcUI wants the global indicator shown"
+ );
+ ok(
+ webrtcUI.showCameraIndicator,
+ "webrtcUI wants the camera indicator shown"
+ );
+ ok(
+ !webrtcUI.showMicrophoneIndicator,
+ "webrtcUI wants the mic indicator hidden"
+ );
+ is(
+ webrtcUI.getActiveStreams(true, true, true).length,
+ 1,
+ "1 active stream"
+ );
+
+ await checkSharingUI({ video: true });
+
+ // Close the first tab's stream and verify that all indicators are removed.
+ await closeStream(false, null, true);
+
+ ok(
+ !webrtcUI.showGlobalIndicator,
+ "webrtcUI wants the global indicator hidden"
+ );
+ is(
+ webrtcUI.getActiveStreams(true, true, true).length,
+ 0,
+ "0 active streams"
+ );
+ },
+ },
+
+ {
+ desc:
+ "getUserMedia screen sharing in a first process + screen sharing in a second process",
+ skipObserverVerification: true,
+ run: async function checkMultiProcessScreen() {
+ // Request screen sharing in the first tab.
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true, null, "screen");
+ await promise;
+ await observerPromise;
+
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
+ "webRTC-shareScreen-notification-icon",
+ "anchored to device icon"
+ );
+ checkDeviceSelectors(["screen"]);
+
+ // Select the last screen so that we can have a stream.
+ let menulist = document.getElementById("webRTC-selectWindow-menulist");
+ menulist.getItemAtIndex(menulist.itemCount - 1).doCommand();
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { screen: "Screen" },
+ "expected screen to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ screen: "Screen" });
+
+ ok(
+ webrtcUI.showGlobalIndicator,
+ "webrtcUI wants the global indicator shown"
+ );
+ ok(
+ webrtcUI.showScreenSharingIndicator,
+ "webrtcUI wants the screen sharing indicator shown"
+ );
+ ok(
+ !webrtcUI.showCameraIndicator,
+ "webrtcUI wants the camera indicator hidden"
+ );
+ ok(
+ !webrtcUI.showMicrophoneIndicator,
+ "webrtcUI wants the mic indicator hidden"
+ );
+ is(
+ webrtcUI.getActiveStreams(false, false, true).length,
+ 1,
+ "1 active screen sharing stream"
+ );
+ is(
+ webrtcUI.getActiveStreams(true, true, true).length,
+ 1,
+ "1 active stream"
+ );
+
+ // If we have reached the max process count already, increase it to ensure
+ // our new tab can have its own content process.
+ let childCount = Services.ppmm.childCount;
+ let maxContentProcess = Services.prefs.getIntPref("dom.ipc.processCount");
+ // The first check is because if we are on a branch where e10s-multi is
+ // disabled, we want to keep testing e10s with a single content process.
+ // The + 1 is because ppmm.childCount also counts the chrome process
+ // (which also runs process scripts).
+ if (maxContentProcess > 1 && childCount == maxContentProcess + 1) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.ipc.processCount", childCount]],
+ });
+ }
+
+ // Open a new tab with a different hostname.
+ let url = gBrowser.currentURI.spec.replace(
+ "https://example.com/",
+ "https://example.com/"
+ );
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+
+ await enableObserverVerification();
+
+ // Request screen sharing in the second tab.
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true, null, "screen");
+ await promise;
+ await observerPromise;
+
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
+ "webRTC-shareScreen-notification-icon",
+ "anchored to device icon"
+ );
+ checkDeviceSelectors(["screen"]);
+
+ // Select the last screen so that we can have a stream.
+ menulist.getItemAtIndex(menulist.itemCount - 1).doCommand();
+
+ observerPromise1 = expectObserverCalled("getUserMedia:response:allow");
+ observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+
+ await checkSharingUI({ screen: "Screen" }, window, { screen: "Screen" });
+
+ ok(
+ webrtcUI.showGlobalIndicator,
+ "webrtcUI wants the global indicator shown"
+ );
+ ok(
+ webrtcUI.showScreenSharingIndicator,
+ "webrtcUI wants the screen sharing indicator shown"
+ );
+ ok(
+ !webrtcUI.showCameraIndicator,
+ "webrtcUI wants the camera indicator hidden"
+ );
+ ok(
+ !webrtcUI.showMicrophoneIndicator,
+ "webrtcUI wants the mic indicator hidden"
+ );
+ is(
+ webrtcUI.getActiveStreams(false, false, true).length,
+ 2,
+ "2 active desktop sharing streams"
+ );
+ is(
+ webrtcUI.getActiveStreams(true, true, true).length,
+ 2,
+ "2 active streams"
+ );
+
+ await disableObserverVerification();
+
+ info("removing the second tab");
+ BrowserTestUtils.removeTab(tab);
+
+ await TestUtils.waitForCondition(
+ () => webrtcUI.getActiveStreams(true, true, true).length == 1
+ );
+
+ // Close the first tab's stream and verify that all indicators are removed.
+ await closeStream(false, null, true);
+
+ ok(
+ !webrtcUI.showGlobalIndicator,
+ "webrtcUI wants the global indicator hidden"
+ );
+ is(
+ webrtcUI.getActiveStreams(true, true, true).length,
+ 0,
+ "0 active streams"
+ );
+ },
+ },
+];
+
+add_task(async function test() {
+ await runTests(gTests);
+});
diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media_paused.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_paused.js
new file mode 100644
index 0000000000..dc30f1cd4b
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_paused.js
@@ -0,0 +1,1005 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function setCameraMuted(mute) {
+ return sendObserverNotification(
+ mute ? "getUserMedia:muteVideo" : "getUserMedia:unmuteVideo"
+ );
+}
+
+function setMicrophoneMuted(mute) {
+ return sendObserverNotification(
+ mute ? "getUserMedia:muteAudio" : "getUserMedia:unmuteAudio"
+ );
+}
+
+function sendObserverNotification(topic) {
+ const windowId = gBrowser.selectedBrowser.innerWindowID;
+ return SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [{ topic, windowId }],
+ function(args) {
+ Services.obs.notifyObservers(
+ content.window,
+ args.topic,
+ JSON.stringify(args.windowId)
+ );
+ }
+ );
+}
+
+function setTrackEnabled(audio, video) {
+ return SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [{ audio, video }],
+ function(args) {
+ let stream = content.wrappedJSObject.gStreams[0];
+ if (args.audio != null) {
+ stream.getAudioTracks()[0].enabled = args.audio;
+ }
+ if (args.video != null) {
+ stream.getVideoTracks()[0].enabled = args.video;
+ }
+ }
+ );
+}
+
+async function getVideoTrackMuted() {
+ return SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ () => content.wrappedJSObject.gStreams[0].getVideoTracks()[0].muted
+ );
+}
+
+async function getVideoTrackEvents() {
+ return SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ () => content.wrappedJSObject.gVideoEvents
+ );
+}
+
+async function getAudioTrackMuted() {
+ return SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ () => content.wrappedJSObject.gStreams[0].getAudioTracks()[0].muted
+ );
+}
+
+async function getAudioTrackEvents() {
+ return SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ () => content.wrappedJSObject.gAudioEvents
+ );
+}
+
+function cloneTracks(audio, video) {
+ return SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [{ audio, video }],
+ function(args) {
+ if (!content.wrappedJSObject.gClones) {
+ content.wrappedJSObject.gClones = [];
+ }
+ let clones = content.wrappedJSObject.gClones;
+ let stream = content.wrappedJSObject.gStreams[0];
+ if (args.audio != null) {
+ clones.push(stream.getAudioTracks()[0].clone());
+ }
+ if (args.video != null) {
+ clones.push(stream.getVideoTracks()[0].clone());
+ }
+ }
+ );
+}
+
+function stopClonedTracks(audio, video) {
+ return SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [{ audio, video }],
+ function(args) {
+ let clones = content.wrappedJSObject.gClones || [];
+ if (args.audio != null) {
+ clones.filter(t => t.kind == "audio").forEach(t => t.stop());
+ }
+ if (args.video != null) {
+ clones.filter(t => t.kind == "video").forEach(t => t.stop());
+ }
+ let liveClones = clones.filter(t => t.readyState == "live");
+ if (!liveClones.length) {
+ delete content.wrappedJSObject.gClones;
+ } else {
+ content.wrappedJSObject.gClones = liveClones;
+ }
+ }
+ );
+}
+
+var gTests = [
+ {
+ desc:
+ "getUserMedia audio+video: disabling the stream shows the paused indicator",
+ run: async function checkDisabled() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+ await indicator;
+ await checkSharingUI({
+ video: STATE_CAPTURE_ENABLED,
+ audio: STATE_CAPTURE_ENABLED,
+ });
+
+ // Disable both audio and video.
+ observerPromise = expectObserverCalled("recording-device-events", 2);
+ await setTrackEnabled(false, false);
+
+ // Wait for capture state to propagate to the UI asynchronously.
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ window.gPermissionPanel._sharingState.webRTC.camera ==
+ STATE_CAPTURE_DISABLED,
+ "video should be disabled"
+ );
+
+ await observerPromise;
+
+ // The identity UI should show both as disabled.
+ await checkSharingUI({
+ video: STATE_CAPTURE_DISABLED,
+ audio: STATE_CAPTURE_DISABLED,
+ });
+
+ // Enable only audio again.
+ observerPromise = expectObserverCalled("recording-device-events");
+ await setTrackEnabled(true);
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ window.gPermissionPanel._sharingState.webRTC.microphone ==
+ STATE_CAPTURE_ENABLED,
+ "audio should be enabled"
+ );
+
+ await observerPromise;
+
+ // The identity UI should show only video as disabled.
+ await checkSharingUI({
+ video: STATE_CAPTURE_DISABLED,
+ audio: STATE_CAPTURE_ENABLED,
+ });
+
+ // Enable video again.
+ observerPromise = expectObserverCalled("recording-device-events");
+ await setTrackEnabled(null, true);
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ window.gPermissionPanel._sharingState.webRTC.camera ==
+ STATE_CAPTURE_ENABLED,
+ "video should be enabled"
+ );
+
+ await observerPromise;
+
+ // Both streams should show as running.
+ await checkSharingUI({
+ video: STATE_CAPTURE_ENABLED,
+ audio: STATE_CAPTURE_ENABLED,
+ });
+ await closeStream();
+ },
+ },
+
+ {
+ desc:
+ "getUserMedia audio+video: disabling the original tracks and stopping enabled clones shows the paused indicator",
+ run: async function checkDisabledAfterCloneStop() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+ await indicator;
+ await checkSharingUI({
+ video: STATE_CAPTURE_ENABLED,
+ audio: STATE_CAPTURE_ENABLED,
+ });
+
+ // Clone audio and video, their state will be enabled
+ await cloneTracks(true, true);
+
+ // Disable both audio and video.
+ await setTrackEnabled(false, false);
+
+ observerPromise = expectObserverCalled("recording-device-events", 2);
+
+ // Stop the clones. This should disable the sharing indicators.
+ await stopClonedTracks(true, true);
+
+ // Wait for capture state to propagate to the UI asynchronously.
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ window.gPermissionPanel._sharingState.webRTC.camera ==
+ STATE_CAPTURE_DISABLED &&
+ window.gPermissionPanel._sharingState.webRTC.microphone ==
+ STATE_CAPTURE_DISABLED,
+ "video and audio should be disabled"
+ );
+
+ await observerPromise;
+
+ // The identity UI should show both as disabled.
+ await checkSharingUI({
+ video: STATE_CAPTURE_DISABLED,
+ audio: STATE_CAPTURE_DISABLED,
+ });
+
+ // Enable only audio again.
+ observerPromise = expectObserverCalled("recording-device-events");
+ await setTrackEnabled(true);
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ window.gPermissionPanel._sharingState.webRTC.microphone ==
+ STATE_CAPTURE_ENABLED,
+ "audio should be enabled"
+ );
+
+ await observerPromise;
+
+ // The identity UI should show only video as disabled.
+ await checkSharingUI({
+ video: STATE_CAPTURE_DISABLED,
+ audio: STATE_CAPTURE_ENABLED,
+ });
+
+ // Enable video again.
+ observerPromise = expectObserverCalled("recording-device-events");
+ await setTrackEnabled(null, true);
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ window.gPermissionPanel._sharingState.webRTC.camera ==
+ STATE_CAPTURE_ENABLED,
+ "video should be enabled"
+ );
+
+ await observerPromise;
+
+ // Both streams should show as running.
+ await checkSharingUI({
+ video: STATE_CAPTURE_ENABLED,
+ audio: STATE_CAPTURE_ENABLED,
+ });
+ await closeStream();
+ },
+ },
+
+ {
+ desc:
+ "getUserMedia screen: disabling the stream shows the paused indicator",
+ run: async function checkScreenDisabled() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true, null, "screen");
+ await promise;
+ await observerPromise;
+
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
+ "webRTC-shareScreen-notification-icon",
+ "anchored to device icon"
+ );
+ checkDeviceSelectors(["screen"]);
+
+ let menulist = document.getElementById("webRTC-selectWindow-menulist");
+ menulist.getItemAtIndex(menulist.itemCount - 1).doCommand();
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { screen: "Screen" },
+ "expected screen to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ screen: "Screen" });
+
+ observerPromise = expectObserverCalled("recording-device-events");
+ await setTrackEnabled(null, false);
+
+ // Wait for capture state to propagate to the UI asynchronously.
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ window.gPermissionPanel._sharingState.webRTC.screen == "ScreenPaused",
+ "screen should be disabled"
+ );
+ await observerPromise;
+ await checkSharingUI({ screen: "ScreenPaused" }, window, {
+ screen: "Screen",
+ });
+
+ observerPromise = expectObserverCalled("recording-device-events");
+ await setTrackEnabled(null, true);
+
+ await BrowserTestUtils.waitForCondition(
+ () => window.gPermissionPanel._sharingState.webRTC.screen == "Screen",
+ "screen should be enabled"
+ );
+ await observerPromise;
+ await checkSharingUI({ screen: "Screen" });
+ await closeStream();
+ },
+ },
+
+ {
+ desc:
+ "getUserMedia audio+video: muting the camera shows the muted indicator",
+ run: async function checkCameraMuted() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+ await indicator;
+ await checkSharingUI({
+ video: STATE_CAPTURE_ENABLED,
+ audio: STATE_CAPTURE_ENABLED,
+ });
+ is(await getVideoTrackMuted(), false, "video track starts unmuted");
+ Assert.deepEqual(
+ await getVideoTrackEvents(),
+ [],
+ "no video track events fired yet"
+ );
+
+ // Mute camera.
+ observerPromise = expectObserverCalled("recording-device-events");
+ await setCameraMuted(true);
+
+ // Wait for capture state to propagate to the UI asynchronously.
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ window.gPermissionPanel._sharingState.webRTC.camera ==
+ STATE_CAPTURE_DISABLED,
+ "video should be muted"
+ );
+
+ await observerPromise;
+
+ // The identity UI should show only camera as disabled.
+ await checkSharingUI({
+ video: STATE_CAPTURE_DISABLED,
+ audio: STATE_CAPTURE_ENABLED,
+ });
+ is(await getVideoTrackMuted(), true, "video track is muted");
+ Assert.deepEqual(await getVideoTrackEvents(), ["mute"], "mute fired");
+
+ // Unmute video again.
+ observerPromise = expectObserverCalled("recording-device-events");
+ await setCameraMuted(false);
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ window.gPermissionPanel._sharingState.webRTC.camera ==
+ STATE_CAPTURE_ENABLED,
+ "video should be enabled"
+ );
+
+ await observerPromise;
+
+ // Both streams should show as running.
+ await checkSharingUI({
+ video: STATE_CAPTURE_ENABLED,
+ audio: STATE_CAPTURE_ENABLED,
+ });
+ is(await getVideoTrackMuted(), false, "video track is unmuted");
+ Assert.deepEqual(
+ await getVideoTrackEvents(),
+ ["mute", "unmute"],
+ "unmute fired"
+ );
+ await closeStream();
+ },
+ },
+
+ {
+ desc:
+ "getUserMedia audio+video: muting the microphone shows the muted indicator",
+ run: async function checkMicrophoneMuted() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+ await indicator;
+ await checkSharingUI({
+ video: STATE_CAPTURE_ENABLED,
+ audio: STATE_CAPTURE_ENABLED,
+ });
+ is(await getAudioTrackMuted(), false, "audio track starts unmuted");
+ Assert.deepEqual(
+ await getAudioTrackEvents(),
+ [],
+ "no audio track events fired yet"
+ );
+
+ // Mute microphone.
+ observerPromise = expectObserverCalled("recording-device-events");
+ await setMicrophoneMuted(true);
+
+ // Wait for capture state to propagate to the UI asynchronously.
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ window.gPermissionPanel._sharingState.webRTC.microphone ==
+ STATE_CAPTURE_DISABLED,
+ "audio should be muted"
+ );
+
+ await observerPromise;
+
+ // The identity UI should show only microphone as disabled.
+ await checkSharingUI({
+ video: STATE_CAPTURE_ENABLED,
+ audio: STATE_CAPTURE_DISABLED,
+ });
+ is(await getAudioTrackMuted(), true, "audio track is muted");
+ Assert.deepEqual(await getAudioTrackEvents(), ["mute"], "mute fired");
+
+ // Unmute audio again.
+ observerPromise = expectObserverCalled("recording-device-events");
+ await setMicrophoneMuted(false);
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ window.gPermissionPanel._sharingState.webRTC.microphone ==
+ STATE_CAPTURE_ENABLED,
+ "audio should be enabled"
+ );
+
+ await observerPromise;
+
+ // Both streams should show as running.
+ await checkSharingUI({
+ video: STATE_CAPTURE_ENABLED,
+ audio: STATE_CAPTURE_ENABLED,
+ });
+ is(await getAudioTrackMuted(), false, "audio track is unmuted");
+ Assert.deepEqual(
+ await getAudioTrackEvents(),
+ ["mute", "unmute"],
+ "unmute fired"
+ );
+ await closeStream();
+ },
+ },
+
+ {
+ desc: "getUserMedia audio+video: disabling & muting camera in combination",
+ // Test the following combinations of disabling and muting camera:
+ // 1. Disable video track only.
+ // 2. Mute camera & disable audio (to have a condition to wait for)
+ // 3. Enable both audio and video tracks (only audio should flow).
+ // 4. Unmute camera again (video should flow).
+ // 5. Mute camera & disable both tracks.
+ // 6. Unmute camera & enable audio (only audio should flow)
+ // 7. Enable video track again (video should flow).
+ run: async function checkDisabledMutedCombination() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+ await indicator;
+ await checkSharingUI({
+ video: STATE_CAPTURE_ENABLED,
+ audio: STATE_CAPTURE_ENABLED,
+ });
+
+ // 1. Disable video track only.
+ observerPromise = expectObserverCalled("recording-device-events");
+ await setTrackEnabled(null, false);
+
+ // Wait for capture state to propagate to the UI asynchronously.
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ window.gPermissionPanel._sharingState.webRTC.camera ==
+ STATE_CAPTURE_DISABLED,
+ "video should be disabled"
+ );
+
+ await observerPromise;
+
+ // The identity UI should show only video as disabled.
+ await checkSharingUI({
+ video: STATE_CAPTURE_DISABLED,
+ audio: STATE_CAPTURE_ENABLED,
+ });
+ is(await getVideoTrackMuted(), false, "video track still unmuted");
+ Assert.deepEqual(
+ await getVideoTrackEvents(),
+ [],
+ "no video track events fired yet"
+ );
+
+ // 2. Mute camera & disable audio (to have a condition to wait for)
+ observerPromise = expectObserverCalled("recording-device-events", 2);
+ await setCameraMuted(true);
+ await setTrackEnabled(false, null);
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ window.gPermissionPanel._sharingState.webRTC.microphone ==
+ STATE_CAPTURE_DISABLED,
+ "audio should be disabled"
+ );
+
+ await observerPromise;
+
+ // The identity UI should show both as disabled.
+ await checkSharingUI({
+ video: STATE_CAPTURE_DISABLED,
+ audio: STATE_CAPTURE_DISABLED,
+ });
+ is(await getVideoTrackMuted(), true, "video track is muted");
+ Assert.deepEqual(
+ await getVideoTrackEvents(),
+ ["mute"],
+ "mute is still fired even though track was disabled"
+ );
+
+ // 3. Enable both audio and video tracks (only audio should flow).
+ observerPromise = expectObserverCalled("recording-device-events", 2);
+ await setTrackEnabled(true, true);
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ window.gPermissionPanel._sharingState.webRTC.microphone ==
+ STATE_CAPTURE_ENABLED,
+ "audio should be enabled"
+ );
+
+ await observerPromise;
+
+ // The identity UI should show only audio as enabled, as video is muted.
+ await checkSharingUI({
+ video: STATE_CAPTURE_DISABLED,
+ audio: STATE_CAPTURE_ENABLED,
+ });
+ is(await getVideoTrackMuted(), true, "video track is still muted");
+ Assert.deepEqual(await getVideoTrackEvents(), ["mute"], "no new events");
+
+ // 4. Unmute camera again (video should flow).
+ observerPromise = expectObserverCalled("recording-device-events");
+ await setCameraMuted(false);
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ window.gPermissionPanel._sharingState.webRTC.camera ==
+ STATE_CAPTURE_ENABLED,
+ "video should be enabled"
+ );
+
+ await observerPromise;
+
+ // Both streams should show as running.
+ await checkSharingUI({
+ video: STATE_CAPTURE_ENABLED,
+ audio: STATE_CAPTURE_ENABLED,
+ });
+ is(await getVideoTrackMuted(), false, "video track is unmuted");
+ Assert.deepEqual(
+ await getVideoTrackEvents(),
+ ["mute", "unmute"],
+ "unmute fired"
+ );
+
+ // 5. Mute camera & disable both tracks.
+ observerPromise = expectObserverCalled("recording-device-events", 3);
+ await setCameraMuted(true);
+ await setTrackEnabled(false, false);
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ window.gPermissionPanel._sharingState.webRTC.camera ==
+ STATE_CAPTURE_DISABLED,
+ "video should be disabled"
+ );
+
+ await observerPromise;
+
+ // The identity UI should show both as disabled.
+ await checkSharingUI({
+ video: STATE_CAPTURE_DISABLED,
+ audio: STATE_CAPTURE_DISABLED,
+ });
+ is(await getVideoTrackMuted(), true, "video track is muted");
+ Assert.deepEqual(
+ await getVideoTrackEvents(),
+ ["mute", "unmute", "mute"],
+ "mute fired afain"
+ );
+
+ // 6. Unmute camera & enable audio (only audio should flow)
+ observerPromise = expectObserverCalled("recording-device-events", 2);
+ await setCameraMuted(false);
+ await setTrackEnabled(true, null);
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ window.gPermissionPanel._sharingState.webRTC.microphone ==
+ STATE_CAPTURE_ENABLED,
+ "audio should be enabled"
+ );
+
+ await observerPromise;
+
+ // Only audio should show as running, as video track is still disabled.
+ await checkSharingUI({
+ video: STATE_CAPTURE_DISABLED,
+ audio: STATE_CAPTURE_ENABLED,
+ });
+ is(await getVideoTrackMuted(), false, "video track is unmuted");
+ Assert.deepEqual(
+ await getVideoTrackEvents(),
+ ["mute", "unmute", "mute", "unmute"],
+ "unmute fired even though track is disabled"
+ );
+
+ // 7. Enable video track again (video should flow).
+ observerPromise = expectObserverCalled("recording-device-events");
+ await setTrackEnabled(null, true);
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ window.gPermissionPanel._sharingState.webRTC.camera ==
+ STATE_CAPTURE_ENABLED,
+ "video should be enabled"
+ );
+
+ await observerPromise;
+
+ // The identity UI should show both as running again.
+ await checkSharingUI({
+ video: STATE_CAPTURE_ENABLED,
+ audio: STATE_CAPTURE_ENABLED,
+ });
+ is(await getVideoTrackMuted(), false, "video track remains unmuted");
+ Assert.deepEqual(
+ await getVideoTrackEvents(),
+ ["mute", "unmute", "mute", "unmute"],
+ "no new events fired"
+ );
+ await closeStream();
+ },
+ },
+
+ {
+ desc:
+ "getUserMedia audio+video: disabling & muting microphone in combination",
+ // Test the following combinations of disabling and muting microphone:
+ // 1. Disable audio track only.
+ // 2. Mute microphone & disable video (to have a condition to wait for)
+ // 3. Enable both audio and video tracks (only video should flow).
+ // 4. Unmute microphone again (audio should flow).
+ // 5. Mute microphone & disable both tracks.
+ // 6. Unmute microphone & enable video (only video should flow)
+ // 7. Enable audio track again (audio should flow).
+ run: async function checkDisabledMutedCombination() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+ await indicator;
+ await checkSharingUI({
+ video: STATE_CAPTURE_ENABLED,
+ audio: STATE_CAPTURE_ENABLED,
+ });
+
+ // 1. Disable audio track only.
+ observerPromise = expectObserverCalled("recording-device-events");
+ await setTrackEnabled(false, null);
+
+ // Wait for capture state to propagate to the UI asynchronously.
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ window.gPermissionPanel._sharingState.webRTC.microphone ==
+ STATE_CAPTURE_DISABLED,
+ "audio should be disabled"
+ );
+
+ await observerPromise;
+
+ // The identity UI should show only audio as disabled.
+ await checkSharingUI({
+ video: STATE_CAPTURE_ENABLED,
+ audio: STATE_CAPTURE_DISABLED,
+ });
+ is(await getAudioTrackMuted(), false, "audio track still unmuted");
+ Assert.deepEqual(
+ await getAudioTrackEvents(),
+ [],
+ "no audio track events fired yet"
+ );
+
+ // 2. Mute microphone & disable video (to have a condition to wait for)
+ observerPromise = expectObserverCalled("recording-device-events", 2);
+ await setMicrophoneMuted(true);
+ await setTrackEnabled(null, false);
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ window.gPermissionPanel._sharingState.webRTC.camera ==
+ STATE_CAPTURE_DISABLED,
+ "camera should be disabled"
+ );
+
+ await observerPromise;
+
+ // The identity UI should show both as disabled.
+ await checkSharingUI({
+ video: STATE_CAPTURE_DISABLED,
+ audio: STATE_CAPTURE_DISABLED,
+ });
+ is(await getAudioTrackMuted(), true, "audio track is muted");
+ Assert.deepEqual(
+ await getAudioTrackEvents(),
+ ["mute"],
+ "mute is still fired even though track was disabled"
+ );
+
+ // 3. Enable both audio and video tracks (only video should flow).
+ observerPromise = expectObserverCalled("recording-device-events", 2);
+ await setTrackEnabled(true, true);
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ window.gPermissionPanel._sharingState.webRTC.camera ==
+ STATE_CAPTURE_ENABLED,
+ "video should be enabled"
+ );
+
+ await observerPromise;
+
+ // The identity UI should show only video as enabled, as audio is muted.
+ await checkSharingUI({
+ video: STATE_CAPTURE_ENABLED,
+ audio: STATE_CAPTURE_DISABLED,
+ });
+ is(await getAudioTrackMuted(), true, "audio track is still muted");
+ Assert.deepEqual(await getAudioTrackEvents(), ["mute"], "no new events");
+
+ // 4. Unmute microphone again (audio should flow).
+ observerPromise = expectObserverCalled("recording-device-events");
+ await setMicrophoneMuted(false);
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ window.gPermissionPanel._sharingState.webRTC.microphone ==
+ STATE_CAPTURE_ENABLED,
+ "audio should be enabled"
+ );
+
+ await observerPromise;
+
+ // Both streams should show as running.
+ await checkSharingUI({
+ video: STATE_CAPTURE_ENABLED,
+ audio: STATE_CAPTURE_ENABLED,
+ });
+ is(await getAudioTrackMuted(), false, "audio track is unmuted");
+ Assert.deepEqual(
+ await getAudioTrackEvents(),
+ ["mute", "unmute"],
+ "unmute fired"
+ );
+
+ // 5. Mute microphone & disable both tracks.
+ observerPromise = expectObserverCalled("recording-device-events", 3);
+ await setMicrophoneMuted(true);
+ await setTrackEnabled(false, false);
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ window.gPermissionPanel._sharingState.webRTC.microphone ==
+ STATE_CAPTURE_DISABLED,
+ "audio should be disabled"
+ );
+
+ await observerPromise;
+
+ // The identity UI should show both as disabled.
+ await checkSharingUI({
+ video: STATE_CAPTURE_DISABLED,
+ audio: STATE_CAPTURE_DISABLED,
+ });
+ is(await getAudioTrackMuted(), true, "audio track is muted");
+ Assert.deepEqual(
+ await getAudioTrackEvents(),
+ ["mute", "unmute", "mute"],
+ "mute fired again"
+ );
+
+ // 6. Unmute microphone & enable video (only video should flow)
+ observerPromise = expectObserverCalled("recording-device-events", 2);
+ await setMicrophoneMuted(false);
+ await setTrackEnabled(null, true);
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ window.gPermissionPanel._sharingState.webRTC.camera ==
+ STATE_CAPTURE_ENABLED,
+ "video should be enabled"
+ );
+
+ await observerPromise;
+
+ // Only video should show as running, as audio track is still disabled.
+ await checkSharingUI({
+ video: STATE_CAPTURE_ENABLED,
+ audio: STATE_CAPTURE_DISABLED,
+ });
+ is(await getAudioTrackMuted(), false, "audio track is unmuted");
+ Assert.deepEqual(
+ await getAudioTrackEvents(),
+ ["mute", "unmute", "mute", "unmute"],
+ "unmute fired even though track is disabled"
+ );
+
+ // 7. Enable audio track again (audio should flow).
+ observerPromise = expectObserverCalled("recording-device-events");
+ await setTrackEnabled(true, null);
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ window.gPermissionPanel._sharingState.webRTC.microphone ==
+ STATE_CAPTURE_ENABLED,
+ "audio should be enabled"
+ );
+
+ await observerPromise;
+
+ // The identity UI should show both as running again.
+ await checkSharingUI({
+ video: STATE_CAPTURE_ENABLED,
+ audio: STATE_CAPTURE_ENABLED,
+ });
+ is(await getAudioTrackMuted(), false, "audio track remains unmuted");
+ Assert.deepEqual(
+ await getAudioTrackEvents(),
+ ["mute", "unmute", "mute", "unmute"],
+ "no new events fired"
+ );
+ await closeStream();
+ },
+ },
+];
+
+add_task(async function test() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.getusermedia.camera.off_while_disabled.delay_ms", 0],
+ ["media.getusermedia.microphone.off_while_disabled.delay_ms", 0],
+ ],
+ });
+
+ SimpleTest.requestCompleteLog();
+ await runTests(gTests);
+});
diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media_queue_request.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_queue_request.js
new file mode 100644
index 0000000000..f3c02fcac9
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_queue_request.js
@@ -0,0 +1,386 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const permissionError =
+ "error: NotAllowedError: The request is not allowed " +
+ "by the user agent or the platform in the current context.";
+
+const badDeviceError =
+ "error: NotReadableError: Failed to allocate videosource";
+
+const { webrtcUI } = ChromeUtils.import("resource:///modules/webrtcUI.jsm");
+
+var gTests = [
+ {
+ desc: "test 'Not now' label queueing audio twice behind allow video",
+ run: async function testQueuingDenyAudioBehindAllowVideo() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true);
+ await promiseRequestDevice(true, false);
+ await promiseRequestDevice(true, false);
+ await promise;
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ checkDeviceSelectors(["camera"]);
+ await observerPromise;
+ let indicator = promiseIndicatorWindow();
+
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ is(
+ PopupNotifications.panel.firstElementChild.secondaryButton.label,
+ "Block",
+ "We offer Block because of no active camera/mic device"
+ );
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { video: true },
+ "expected camera to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ audio: false, video: true });
+
+ await promise;
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await observerPromise;
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ checkDeviceSelectors(["microphone"]);
+
+ observerPromise1 = expectObserverCalled("getUserMedia:response:deny");
+ await promiseMessage(permissionError, () => {
+ is(
+ PopupNotifications.panel.firstElementChild.secondaryButton.label,
+ "Not now",
+ "We offer Not now option because of an allowed camera/mic device"
+ );
+ activateSecondaryAction(kActionDeny);
+ });
+
+ await observerPromise1;
+
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone"]);
+
+ observerPromise = expectObserverCalled("getUserMedia:response:deny");
+ await promiseMessage(permissionError, () => {
+ is(
+ PopupNotifications.panel.firstElementChild.secondaryButton.label,
+ "Not now",
+ "We offer Not now option again because of an allowed camera/mic device"
+ );
+ activateSecondaryAction(kActionDeny);
+ });
+
+ await observerPromise;
+
+ // Clean up the active camera in the activePerms map
+ webrtcUI.activePerms.delete(gBrowser.selectedBrowser.outerWindowID);
+
+ // close all streams
+ await closeStream();
+ },
+ },
+
+ {
+ desc:
+ "test 'Not now'/'Block' label queueing microphone behind screen behind allow camera",
+ run: async function testQueuingAudioAndScreenBehindAllowVideo() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true);
+ await promiseRequestDevice(false, true, null, "screen");
+ await promiseRequestDevice(true, false);
+ await promise;
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ checkDeviceSelectors(["camera"]);
+ await observerPromise;
+
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ is(
+ PopupNotifications.panel.firstElementChild.secondaryButton.label,
+ "Block",
+ "We offer Block because of no active camera/mic device"
+ );
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { video: true },
+ "expected camera to be shared"
+ );
+ await checkSharingUI({ audio: false, video: true });
+
+ await promise;
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await observerPromise;
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ checkDeviceSelectors(["screen"]);
+ observerPromise = expectObserverCalled("getUserMedia:response:deny");
+ await promiseMessage(permissionError, () => {
+ is(
+ PopupNotifications.panel.firstElementChild.secondaryButton.label,
+ "Not now",
+ "We offer Not now option because we are asking for screen"
+ );
+ activateSecondaryAction(kActionDeny);
+ });
+ await observerPromise;
+
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone"]);
+ observerPromise = expectObserverCalled("getUserMedia:response:deny");
+ await promiseMessage(permissionError, () => {
+ is(
+ PopupNotifications.panel.firstElementChild.secondaryButton.label,
+ "Not now",
+ "We offer Not now option because we are asking for mic and cam is already active"
+ );
+ activateSecondaryAction(kActionDeny);
+ });
+ await observerPromise;
+
+ // Clean up
+ webrtcUI.activePerms.delete(gBrowser.selectedBrowser.outerWindowID);
+ // close all streams
+ await closeStream();
+ },
+ },
+
+ {
+ desc: "test queueing allow video behind deny audio",
+ run: async function testQueuingAllowVideoBehindDenyAudio() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, false);
+ await promiseRequestDevice(false, true);
+ await promise;
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await observerPromise;
+ checkDeviceSelectors(["microphone"]);
+
+ let observerPromises = [
+ expectObserverCalled("getUserMedia:request"),
+ expectObserverCalled("getUserMedia:response:deny"),
+ ];
+
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+
+ await Promise.all(observerPromises);
+ checkDeviceSelectors(["camera"]);
+
+ let indicator = promiseIndicatorWindow();
+
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { video: true },
+ "expected camera to be shared"
+ );
+ await indicator;
+ await checkSharingUI({ audio: false, video: true });
+
+ SitePermissions.removeFromPrincipal(
+ null,
+ "microphone",
+ gBrowser.selectedBrowser
+ );
+
+ // close all streams
+ await closeStream();
+ },
+ },
+
+ {
+ desc: "test queueing allow audio behind allow video with error",
+ run: async function testQueuingAllowAudioBehindAllowVideoWithError() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(
+ false,
+ true,
+ null,
+ null,
+ gBrowser.selectedBrowser,
+ true
+ );
+ await promiseRequestDevice(true, false);
+ await observerPromise;
+ await promise;
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+
+ checkDeviceSelectors(["camera"]);
+
+ let observerPromise1 = expectObserverCalled("getUserMedia:request");
+ let observerPromise2 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ await promiseMessage(badDeviceError, () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+
+ await observerPromise1;
+ await observerPromise2;
+ await promise;
+ checkDeviceSelectors(["microphone"]);
+
+ let indicator = promiseIndicatorWindow();
+
+ observerPromise1 = expectObserverCalled("getUserMedia:response:allow");
+ observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true },
+ "expected microphone to be shared"
+ );
+ await indicator;
+ await checkSharingUI({ audio: true, video: false });
+
+ // Clean up the active microphone in the activePerms map
+ webrtcUI.activePerms.delete(gBrowser.selectedBrowser.outerWindowID);
+
+ // close all streams
+ await closeStream();
+ },
+ },
+
+ {
+ desc: "test queueing audio+video behind deny audio",
+ run: async function testQueuingAllowVideoBehindDenyAudio() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, false);
+ await promiseRequestDevice(true, true);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone"]);
+
+ let observerPromises = [
+ expectObserverCalled("getUserMedia:request"),
+ expectObserverCalled("getUserMedia:response:deny", 2),
+ expectObserverCalled("recording-window-ended"),
+ ];
+
+ await promiseMessage(
+ permissionError,
+ () => {
+ activateSecondaryAction(kActionDeny);
+ },
+ 2
+ );
+ await Promise.all(observerPromises);
+
+ SitePermissions.removeFromPrincipal(
+ null,
+ "microphone",
+ gBrowser.selectedBrowser
+ );
+ },
+ },
+
+ {
+ desc: "test queueing audio, video behind reload after pending audio, video",
+ run: async function testQueuingDenyAudioBehindAllowVideo() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, false);
+ await promiseRequestDevice(false, true);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone"]);
+
+ await reloadAndAssertClosedStreams();
+
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ // After the reload, gUM(audio) causes a prompt.
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, false);
+ await promiseRequestDevice(false, true);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone"]);
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+
+ // expect pending camera prompt to appear after ok'ing microphone one.
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true },
+ "expected microphone to be shared"
+ );
+ await indicator;
+ await checkSharingUI({ video: false, audio: true });
+
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["camera"]);
+
+ observerPromise1 = expectObserverCalled("getUserMedia:response:allow");
+ observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected microphone and camera to be shared"
+ );
+
+ // close all streams
+ await closeStream();
+ },
+ },
+];
+
+add_task(async function test() {
+ await runTests(gTests);
+});
diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media_screen.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_screen.js
new file mode 100644
index 0000000000..59a75930e9
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_screen.js
@@ -0,0 +1,915 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// The rejection "The fetching process for the media resource was aborted by the
+// user agent at the user's request." is left unhandled in some cases. This bug
+// should be fixed, but for the moment this file allows a class of rejections.
+//
+// NOTE: Allowing a whole class of rejections should be avoided. Normally you
+// should use "expectUncaughtRejection" to flag individual failures.
+const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(/aborted by the user agent/);
+const { BrowserWindowTracker } = ChromeUtils.import(
+ "resource:///modules/BrowserWindowTracker.jsm"
+);
+
+const permissionError =
+ "error: NotAllowedError: The request is not allowed " +
+ "by the user agent or the platform in the current context.";
+
+const notFoundError = "error: NotFoundError: The object can not be found here.";
+
+const isHeadless = Services.env.get("MOZ_HEADLESS");
+
+var gTests = [
+ {
+ desc: "getUserMedia window/screen picking screen",
+ run: async function checkWindowOrScreen() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true, null, "screen");
+ await promise;
+ await observerPromise;
+
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
+ "webRTC-shareScreen-notification-icon",
+ "anchored to device icon"
+ );
+ checkDeviceSelectors(["screen"]);
+ let notification = PopupNotifications.panel.firstElementChild;
+
+ let menulist = document.getElementById("webRTC-selectWindow-menulist");
+ let count = menulist.itemCount;
+ ok(
+ count >= 4,
+ "There should be the 'Select Window or Screen' item, a separator and at least one window and one screen"
+ );
+
+ let noWindowOrScreenItem = menulist.getItemAtIndex(0);
+ ok(
+ noWindowOrScreenItem.hasAttribute("selected"),
+ "the 'Select Window or Screen' item is selected"
+ );
+ is(
+ menulist.selectedItem,
+ noWindowOrScreenItem,
+ "'Select Window or Screen' is the selected item"
+ );
+ is(menulist.value, "-1", "no window or screen is selected by default");
+ ok(
+ noWindowOrScreenItem.disabled,
+ "'Select Window or Screen' item is disabled"
+ );
+ ok(notification.button.disabled, "Allow button is disabled");
+ ok(
+ notification.hasAttribute("invalidselection"),
+ "Notification is marked as invalid"
+ );
+
+ let separator = menulist.getItemAtIndex(1);
+ is(
+ separator.localName,
+ "menuseparator",
+ "the second item is a separator"
+ );
+
+ ok(
+ document.getElementById("webRTC-all-windows-shared").hidden,
+ "the 'all windows will be shared' warning should be hidden while there's no selection"
+ );
+ ok(
+ document.getElementById("webRTC-preview").hidden,
+ "the preview area is hidden"
+ );
+
+ let scaryScreenIndex;
+ for (let i = 2; i < count; ++i) {
+ let item = menulist.getItemAtIndex(i);
+ is(
+ parseInt(item.getAttribute("value")),
+ i - 2,
+ "the window/screen item has the correct index"
+ );
+ let type = item.getAttribute("devicetype");
+ ok(
+ ["window", "screen"].includes(type),
+ "the devicetype attribute is set correctly"
+ );
+ if (type == "screen") {
+ ok(item.scary, "the screen item is marked as scary");
+ scaryScreenIndex = i;
+ }
+ }
+ ok(
+ typeof scaryScreenIndex == "number",
+ "there's at least one scary screen, as as all screens are"
+ );
+
+ // Select a screen, a preview with a scary warning should appear.
+ menulist.getItemAtIndex(scaryScreenIndex).doCommand();
+ ok(
+ !document.getElementById("webRTC-all-windows-shared").hidden,
+ "the 'all windows will be shared' warning should now be visible"
+ );
+ await TestUtils.waitForCondition(
+ () => !document.getElementById("webRTC-preview").hidden,
+ "preview unhide",
+ 100,
+ 100
+ );
+ ok(
+ !document.getElementById("webRTC-preview").hidden,
+ "the preview area is visible"
+ );
+ ok(
+ !document.getElementById("webRTC-previewWarningBox").hidden,
+ "the scary warning is visible"
+ );
+ ok(!notification.button.disabled, "Allow button is enabled");
+
+ // Select the 'Select Window or Screen' item again, the preview should be hidden.
+ menulist.getItemAtIndex(0).doCommand();
+ ok(
+ document.getElementById("webRTC-all-windows-shared").hidden,
+ "the 'all windows will be shared' warning should now be hidden"
+ );
+ ok(
+ document.getElementById("webRTC-preview").hidden,
+ "the preview area is hidden"
+ );
+
+ // Select the scary screen again so that we can have a stream.
+ menulist.getItemAtIndex(scaryScreenIndex).doCommand();
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { screen: "Screen" },
+ "expected screen to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ screen: "Screen" });
+
+ // we always show prompt for screen sharing.
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ await promiseRequestDevice(false, true, null, "screen");
+ await promise;
+ await observerPromise;
+
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
+ "webRTC-shareScreen-notification-icon",
+ "anchored to device icon"
+ );
+ checkDeviceSelectors(["screen"]);
+
+ observerPromise = expectObserverCalled("getUserMedia:response:deny");
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+
+ await observerPromise;
+ SitePermissions.removeFromPrincipal(
+ null,
+ "screen",
+ gBrowser.selectedBrowser
+ );
+ await closeStream();
+ },
+ },
+
+ {
+ desc: "getUserMedia window/screen picking window",
+ run: async function checkWindowOrScreen() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true, null, "window");
+ await promise;
+ await observerPromise;
+
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
+ "webRTC-shareScreen-notification-icon",
+ "anchored to device icon"
+ );
+ checkDeviceSelectors(["screen"]);
+ let notification = PopupNotifications.panel.firstElementChild;
+
+ let menulist = document.getElementById("webRTC-selectWindow-menulist");
+ let count = menulist.itemCount;
+ ok(
+ count >= 4,
+ "There should be the 'Select Window or Screen' item, a separator and at least one window and one screen"
+ );
+
+ let noWindowOrScreenItem = menulist.getItemAtIndex(0);
+ ok(
+ noWindowOrScreenItem.hasAttribute("selected"),
+ "the 'Select Window or Screen' item is selected"
+ );
+ is(
+ menulist.selectedItem,
+ noWindowOrScreenItem,
+ "'Select Window or Screen' is the selected item"
+ );
+ is(menulist.value, "-1", "no window or screen is selected by default");
+ ok(
+ noWindowOrScreenItem.disabled,
+ "'Select Window or Screen' item is disabled"
+ );
+ ok(notification.button.disabled, "Allow button is disabled");
+ ok(
+ notification.hasAttribute("invalidselection"),
+ "Notification is marked as invalid"
+ );
+
+ let separator = menulist.getItemAtIndex(1);
+ is(
+ separator.localName,
+ "menuseparator",
+ "the second item is a separator"
+ );
+
+ ok(
+ document.getElementById("webRTC-all-windows-shared").hidden,
+ "the 'all windows will be shared' warning should be hidden while there's no selection"
+ );
+ ok(
+ document.getElementById("webRTC-preview").hidden,
+ "the preview area is hidden"
+ );
+
+ let scaryWindowIndexes = [],
+ nonScaryWindowIndex,
+ scaryScreenIndex;
+ for (let i = 2; i < count; ++i) {
+ let item = menulist.getItemAtIndex(i);
+ is(
+ parseInt(item.getAttribute("value")),
+ i - 2,
+ "the window/screen item has the correct index"
+ );
+ let type = item.getAttribute("devicetype");
+ ok(
+ ["window", "screen"].includes(type),
+ "the devicetype attribute is set correctly"
+ );
+ if (type == "screen") {
+ ok(item.scary, "the screen item is marked as scary");
+ scaryScreenIndex = i;
+ } else if (item.scary) {
+ scaryWindowIndexes.push(i);
+ } else {
+ nonScaryWindowIndex = i;
+ }
+ }
+ if (isHeadless) {
+ is(
+ scaryWindowIndexes.length,
+ 0,
+ "there are no scary Firefox windows in headless mode"
+ );
+ } else {
+ ok(
+ scaryWindowIndexes.length,
+ "there's at least one scary window, as Firefox is running"
+ );
+ }
+ ok(
+ typeof scaryScreenIndex == "number",
+ "there's at least one scary screen, as all screens are"
+ );
+
+ if (!isHeadless) {
+ // Select one scary window, a preview with a scary warning should appear.
+ let scaryWindowIndex;
+ for (scaryWindowIndex of scaryWindowIndexes) {
+ menulist.getItemAtIndex(scaryWindowIndex).doCommand();
+ ok(
+ document.getElementById("webRTC-all-windows-shared").hidden,
+ "the 'all windows will be shared' warning should still be hidden"
+ );
+ try {
+ await TestUtils.waitForCondition(
+ () => !document.getElementById("webRTC-preview").hidden,
+ "",
+ 100,
+ 100
+ );
+ break;
+ } catch (e) {
+ // A "scary window" is Firefox. Multiple Firefox windows have been
+ // observed to come and go during try runs, so we won't know which one
+ // is ours. To avoid intermittents, we ignore preview failing due to
+ // these going away on us, provided it succeeds on one of them.
+ }
+ }
+ ok(
+ !document.getElementById("webRTC-preview").hidden,
+ "the preview area is visible"
+ );
+ ok(
+ !document.getElementById("webRTC-previewWarningBox").hidden,
+ "the scary warning is visible"
+ );
+ // Select the 'Select Window' item again, the preview should be hidden.
+ menulist.getItemAtIndex(0).doCommand();
+ ok(
+ document.getElementById("webRTC-preview").hidden,
+ "the preview area is hidden"
+ );
+
+ // Select the first window again so that we can have a stream.
+ menulist.getItemAtIndex(scaryWindowIndex).doCommand();
+ }
+
+ let sharingNonScaryWindow = typeof nonScaryWindowIndex == "number";
+
+ // If we have a non-scary window, select it and verify the warning isn't displayed.
+ // A non-scary window may not always exist on test machines.
+ if (sharingNonScaryWindow) {
+ menulist.getItemAtIndex(nonScaryWindowIndex).doCommand();
+ ok(
+ document.getElementById("webRTC-all-windows-shared").hidden,
+ "the 'all windows will be shared' warning should still be hidden"
+ );
+ await TestUtils.waitForCondition(
+ () => !document.getElementById("webRTC-preview").hidden,
+ "preview unhide",
+ 100,
+ 100
+ );
+ ok(
+ !document.getElementById("webRTC-preview").hidden,
+ "the preview area is visible"
+ );
+ ok(
+ document.getElementById("webRTC-previewWarningBox").hidden,
+ "the scary warning is hidden"
+ );
+ } else {
+ info("no non-scary window available on this test machine");
+ }
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { screen: "Window" },
+ "expected screen to be shared"
+ );
+
+ await indicator;
+ if (sharingNonScaryWindow) {
+ await checkSharingUI({ screen: "Window" });
+ } else {
+ await checkSharingUI({ screen: "Window", browserwindow: true });
+ }
+
+ await closeStream();
+ },
+ },
+
+ {
+ desc: "getUserMedia audio + window/screen",
+ run: async function checkAudioVideo() {
+ if (AppConstants.platform == "macosx") {
+ todo(
+ false,
+ "Bug 1323481 - On Mac on treeherder, but not locally, requesting microphone + screen never makes the permission prompt appear, and so causes the test to timeout"
+ );
+ return;
+ }
+
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true, null, "window");
+ await promise;
+ await observerPromise;
+
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
+ "webRTC-shareScreen-notification-icon",
+ "anchored to device icon"
+ );
+ checkDeviceSelectors(["microphone", "screen"]);
+
+ let menulist = document.getElementById("webRTC-selectWindow-menulist");
+ let count = menulist.itemCount;
+ ok(
+ count >= 4,
+ "There should be the 'Select Window or Screen' item, a separator and at least one window and one screen"
+ );
+
+ // Select a screen, a preview with a scary warning should appear.
+ menulist.getItemAtIndex(count - 1).doCommand();
+ ok(
+ !document.getElementById("webRTC-all-windows-shared").hidden,
+ "the 'all windows will be shared' warning should now be visible"
+ );
+ await TestUtils.waitForCondition(
+ () => !document.getElementById("webRTC-preview").hidden,
+ "preview unhide",
+ 100,
+ 100
+ );
+ ok(
+ !document.getElementById("webRTC-preview").hidden,
+ "the preview area is visible"
+ );
+ ok(
+ !document.getElementById("webRTC-previewWarningBox").hidden,
+ "the scary warning is visible"
+ );
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, screen: "Screen" },
+ "expected screen and microphone to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ audio: true, screen: "Screen" });
+ await closeStream();
+ },
+ },
+
+ {
+ desc: 'getUserMedia screen, user clicks "Don\'t Allow"',
+ run: async function checkDontShare() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true, null, "screen");
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["screen"]);
+
+ let observerPromise1 = expectObserverCalled("getUserMedia:response:deny");
+ let observerPromise2 = expectObserverCalled("recording-window-ended");
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+
+ await observerPromise1;
+ await observerPromise2;
+ await checkNotSharing();
+ SitePermissions.removeFromPrincipal(
+ null,
+ "screen",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "camera",
+ gBrowser.selectedBrowser
+ );
+ },
+ },
+
+ {
+ desc: "getUserMedia audio + window/screen: stop sharing",
+ run: async function checkStopSharing() {
+ if (AppConstants.platform == "macosx") {
+ todo(
+ false,
+ "Bug 1323481 - On Mac on treeherder, but not locally, requesting microphone + screen never makes the permission prompt appear, and so causes the test to timeout"
+ );
+ return;
+ }
+
+ async function share(deviceTypes) {
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ await promiseRequestDevice(
+ /* audio */ deviceTypes.includes("microphone"),
+ /* video */ deviceTypes.some(t => t == "screen" || t == "camera"),
+ null,
+ deviceTypes.includes("screen") && "window"
+ );
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(deviceTypes);
+ if (screen) {
+ let menulist = document.getElementById(
+ "webRTC-selectWindow-menulist"
+ );
+ menulist.getItemAtIndex(menulist.itemCount - 1).doCommand();
+ }
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ }
+
+ async function check(expected = {}) {
+ let shared = Object.keys(expected).join(" and ");
+ if (shared) {
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ expected,
+ "expected " + shared + " to be shared"
+ );
+ await checkSharingUI(expected);
+ } else {
+ await checkNotSharing();
+ }
+ }
+
+ info("Share screen and microphone");
+ let indicator = promiseIndicatorWindow();
+ await share(["microphone", "screen"]);
+ await indicator;
+ await check({ audio: true, screen: "Screen" });
+
+ info("Share camera");
+ await share(["camera"]);
+ await check({ video: true, audio: true, screen: "Screen" });
+
+ info("Stop the screen share, mic+cam should continue");
+ await stopSharing("screen", true);
+ await check({ video: true, audio: true });
+
+ info("Stop the camera, everything should stop.");
+ await stopSharing("camera");
+
+ info("Now, share only the screen...");
+ indicator = promiseIndicatorWindow();
+ await share(["screen"]);
+ await indicator;
+ await check({ screen: "Screen" });
+
+ info("... and add camera and microphone in a second request.");
+ await share(["microphone", "camera"]);
+ await check({ video: true, audio: true, screen: "Screen" });
+
+ info("Stop the camera, this should stop everything.");
+ await stopSharing("camera");
+ },
+ },
+
+ {
+ desc: "getUserMedia window/screen: reloading the page removes all gUM UI",
+ run: async function checkReloading() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true, null, "screen");
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["screen"]);
+ let menulist = document.getElementById("webRTC-selectWindow-menulist");
+ menulist.getItemAtIndex(menulist.itemCount - 1).doCommand();
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { screen: "Screen" },
+ "expected screen to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ screen: "Screen" });
+
+ await reloadAndAssertClosedStreams();
+ },
+ },
+
+ {
+ desc: "test showControlCenter from screen icon",
+ run: async function checkShowControlCenter() {
+ if (!USING_LEGACY_INDICATOR) {
+ info(
+ "Skipping since this test doesn't apply to the new global sharing " +
+ "indicator."
+ );
+ return;
+ }
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true, null, "screen");
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["screen"]);
+ let menulist = document.getElementById("webRTC-selectWindow-menulist");
+ menulist.getItemAtIndex(menulist.itemCount - 1).doCommand();
+
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ let indicator = promiseIndicatorWindow();
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { screen: "Screen" },
+ "expected screen to be shared"
+ );
+ await indicator;
+ await checkSharingUI({ screen: "Screen" });
+
+ ok(permissionPopupHidden(), "control center should be hidden");
+ if (IS_MAC) {
+ let activeStreams = webrtcUI.getActiveStreams(false, false, true);
+ webrtcUI.showSharingDoorhanger(activeStreams[0]);
+ } else {
+ let win = Services.wm.getMostRecentWindow(
+ "Browser:WebRTCGlobalIndicator"
+ );
+ let elt = win.document.getElementById("screenShareButton");
+ EventUtils.synthesizeMouseAtCenter(elt, {}, win);
+ }
+ await TestUtils.waitForCondition(
+ () => !permissionPopupHidden(),
+ "wait for control center to open"
+ );
+ ok(!permissionPopupHidden(), "control center should be open");
+
+ gPermissionPanel._permissionPopup.hidePopup();
+
+ await closeStream();
+ },
+ },
+
+ {
+ desc: "Only persistent block is possible for screen sharing",
+ run: async function checkPersistentPermissions() {
+ // This test doesn't apply when the notification silencing
+ // feature is enabled, since the "Remember this decision"
+ // checkbox doesn't exist.
+ if (ALLOW_SILENCING_NOTIFICATIONS) {
+ return;
+ }
+
+ let browser = gBrowser.selectedBrowser;
+ let devicePerms = SitePermissions.getForPrincipal(
+ browser.contentPrincipal,
+ "screen",
+ browser
+ );
+ is(
+ devicePerms.state,
+ SitePermissions.UNKNOWN,
+ "starting without screen persistent permissions"
+ );
+
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true, null, "screen");
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["screen"]);
+ document
+ .getElementById("webRTC-selectWindow-menulist")
+ .getItemAtIndex(2)
+ .doCommand();
+
+ // Ensure that checking the 'Remember this decision' checkbox disables
+ // 'Allow'.
+ let notification = PopupNotifications.panel.firstElementChild;
+ ok(
+ notification.hasAttribute("warninghidden"),
+ "warning message is hidden"
+ );
+ let checkbox = notification.checkbox;
+ ok(!!checkbox, "checkbox is present");
+ ok(!checkbox.checked, "checkbox is not checked");
+ checkbox.click();
+ ok(checkbox.checked, "checkbox now checked");
+ ok(notification.button.disabled, "Allow button is disabled");
+ ok(
+ !notification.hasAttribute("warninghidden"),
+ "warning message is shown"
+ );
+
+ // Click "Don't Allow" to save a persistent block permission.
+ let observerPromise1 = expectObserverCalled("getUserMedia:response:deny");
+ let observerPromise2 = expectObserverCalled("recording-window-ended");
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+ await observerPromise1;
+ await observerPromise2;
+ await checkNotSharing();
+
+ let permission = SitePermissions.getForPrincipal(
+ browser.contentPrincipal,
+ "screen",
+ browser
+ );
+ is(permission.state, SitePermissions.BLOCK, "screen sharing is blocked");
+ is(
+ permission.scope,
+ SitePermissions.SCOPE_PERSISTENT,
+ "screen sharing is persistently blocked"
+ );
+
+ // Request screensharing again, expect an immediate failure.
+ await Promise.all([
+ expectObserverCalled("getUserMedia:request"),
+ expectObserverCalled("getUserMedia:response:deny"),
+ expectObserverCalled("recording-window-ended"),
+ promiseMessage(permissionError),
+ promiseRequestDevice(false, true, null, "screen"),
+ ]);
+
+ // Now set the permission to allow and expect a prompt.
+ SitePermissions.setForPrincipal(
+ browser.contentPrincipal,
+ "screen",
+ SitePermissions.ALLOW
+ );
+
+ // Request devices and expect a prompt despite the saved 'Allow' permission.
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true, null, "screen");
+ await promise;
+ await observerPromise;
+
+ // The 'remember' checkbox shouldn't be checked anymore.
+ notification = PopupNotifications.panel.firstElementChild;
+ ok(
+ notification.hasAttribute("warninghidden"),
+ "warning message is hidden"
+ );
+ checkbox = notification.checkbox;
+ ok(!!checkbox, "checkbox is present");
+ ok(!checkbox.checked, "checkbox is not checked");
+
+ // Deny the request to cleanup...
+ observerPromise1 = expectObserverCalled("getUserMedia:response:deny");
+ observerPromise2 = expectObserverCalled("recording-window-ended");
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+ await observerPromise1;
+ await observerPromise2;
+ SitePermissions.removeFromPrincipal(
+ browser.contentPrincipal,
+ "screen",
+ browser
+ );
+ },
+ },
+
+ {
+ desc:
+ "Switching between menu options maintains correct main action state while window sharing",
+ skipObserverVerification: true,
+ run: async function checkDoorhangerState() {
+ await enableObserverVerification();
+
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ await BrowserTestUtils.openNewForegroundTab(win.gBrowser, "about:newtab");
+ BrowserWindowTracker.orderedWindows[1].focus();
+
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true, null, "window");
+ await promise;
+ await observerPromise;
+
+ let menulist = document.getElementById("webRTC-selectWindow-menulist");
+ let notification = PopupNotifications.panel.firstElementChild;
+ let checkbox = notification.checkbox;
+
+ menulist.getItemAtIndex(2).doCommand();
+ checkbox.click();
+ ok(checkbox.checked, "checkbox now checked");
+
+ if (ALLOW_SILENCING_NOTIFICATIONS) {
+ // When the notification silencing feature is enabled, the checkbox
+ // controls that feature, and its state should not disable the
+ // "Allow" button.
+ ok(!notification.button.disabled, "Allow button is not disabled");
+ } else {
+ ok(notification.button.disabled, "Allow button is disabled");
+ ok(
+ !notification.hasAttribute("warninghidden"),
+ "warning message is shown"
+ );
+ }
+
+ menulist.getItemAtIndex(3).doCommand();
+ ok(checkbox.checked, "checkbox still checked");
+ if (ALLOW_SILENCING_NOTIFICATIONS) {
+ // When the notification silencing feature is enabled, the checkbox
+ // controls that feature, and its state should not disable the
+ // "Allow" button.
+ ok(!notification.button.disabled, "Allow button remains not disabled");
+ } else {
+ ok(notification.button.disabled, "Allow button remains disabled");
+ ok(
+ !notification.hasAttribute("warninghidden"),
+ "warning message is still shown"
+ );
+ }
+
+ await disableObserverVerification();
+
+ observerPromise = expectObserverCalled("recording-window-ended");
+
+ gBrowser.removeCurrentTab();
+ win.close();
+
+ await observerPromise;
+
+ await openNewTestTab();
+ },
+ },
+ {
+ desc: "Switching between tabs does not bleed state into other prompts",
+ skipObserverVerification: true,
+ run: async function checkSwitchingTabs() {
+ // Open a new window in the background to have a choice in the menulist.
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ await BrowserTestUtils.openNewForegroundTab(win.gBrowser, "about:newtab");
+ await enableObserverVerification();
+ BrowserWindowTracker.orderedWindows[1].focus();
+
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true, null, "window");
+ await promise;
+ await observerPromise;
+
+ let notification = PopupNotifications.panel.firstElementChild;
+ ok(notification.button.disabled, "Allow button is disabled");
+ await disableObserverVerification();
+
+ await openNewTestTab("get_user_media_in_xorigin_frame.html");
+ await enableObserverVerification();
+
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true, "frame1");
+ await promise;
+ await observerPromise;
+
+ notification = PopupNotifications.panel.firstElementChild;
+ ok(!notification.button.disabled, "Allow button is not disabled");
+
+ await disableObserverVerification();
+
+ gBrowser.removeCurrentTab();
+ gBrowser.removeCurrentTab();
+ win.close();
+
+ await openNewTestTab();
+ },
+ },
+];
+
+add_task(async function test() {
+ await runTests(gTests);
+});
diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media_screen_tab_close.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_screen_tab_close.js
new file mode 100644
index 0000000000..f4dbd67ea1
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_screen_tab_close.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ "https://example.com/"
+);
+const TEST_PAGE = TEST_ROOT + "get_user_media.html";
+
+/**
+ * Tests that the given tab is the currently selected tab.
+ * @param {Element} aTab - Tab to test.
+ */
+function testSelected(aTab) {
+ is(aTab, gBrowser.selectedTab, "Tab is gBrowser.selectedTab");
+ is(aTab.getAttribute("selected"), "true", "Tab has property 'selected'");
+ is(
+ aTab.getAttribute("visuallyselected"),
+ "true",
+ "Tab has property 'visuallyselected'"
+ );
+}
+
+/**
+ * Tests that when closing a tab with active screen sharing, the screen sharing
+ * ends and the tab closes properly.
+ */
+add_task(async function testScreenSharingTabClose() {
+ let initialTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com"
+ );
+
+ // Open another foreground tab and ensure its selected.
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE);
+ testSelected(tab);
+
+ // Start screen sharing in active tab
+ await shareDevices(tab.linkedBrowser, false, false, SHARE_WINDOW);
+ ok(tab._sharingState.webRTC.screen, "Tab has webRTC screen sharing state");
+
+ let recordingEndedPromise = expectObserverCalled(
+ "recording-window-ended",
+ 1,
+ tab.linkedBrowser.browsingContext
+ );
+ let tabClosedPromise = BrowserTestUtils.waitForCondition(
+ () => gBrowser.selectedTab == initialTab,
+ "Waiting for tab to close"
+ );
+
+ // Close tab
+ BrowserTestUtils.removeTab(tab, { animate: true, byMouse: true });
+
+ // Wait for screen sharing to end
+ await recordingEndedPromise;
+
+ // Wait for tab to be fully closed
+ await tabClosedPromise;
+
+ // Test that we're back to the initial tab.
+ testSelected(initialTab);
+
+ // There should be no active sharing for the selected tab.
+ ok(
+ !gBrowser.selectedTab._sharingState?.webRTC?.screen,
+ "Selected tab doesn't have webRTC screen sharing state"
+ );
+
+ BrowserTestUtils.removeTab(initialTab);
+});
diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media_tear_off_tab.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_tear_off_tab.js
new file mode 100644
index 0000000000..ef69d15971
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_tear_off_tab.js
@@ -0,0 +1,100 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var gTests = [
+ {
+ desc: "getUserMedia: tearing-off a tab keeps sharing indicators",
+ skipObserverVerification: true,
+ run: async function checkTearingOff() {
+ await enableObserverVerification();
+
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ await promiseRequestDevice(true, true);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ video: true, audio: true });
+
+ // Don't listen to observer notifications in the tab any more, as
+ // they will need to be switched to the new window.
+ await disableObserverVerification();
+
+ info("tearing off the tab");
+ let win = gBrowser.replaceTabWithWindow(gBrowser.selectedTab);
+ await whenDelayedStartupFinished(win);
+ await checkSharingUI({ audio: true, video: true }, win);
+
+ await enableObserverVerification(win.gBrowser.selectedBrowser);
+
+ // Clicking the global sharing indicator should open the control center in
+ // the second window.
+ ok(permissionPopupHidden(win), "control center should be hidden");
+ let activeStreams = webrtcUI.getActiveStreams(true, false, false);
+ webrtcUI.showSharingDoorhanger(activeStreams[0]);
+ // If the popup gets hidden before being shown, by stray focus/activate
+ // events, don't bother failing the test. It's enough to know that we
+ // started showing the popup.
+ let popup = win.gPermissionPanel._permissionPopup;
+ let hiddenEvent = BrowserTestUtils.waitForEvent(popup, "popuphidden");
+ let shownEvent = BrowserTestUtils.waitForEvent(popup, "popupshown");
+ let ev = await Promise.race([hiddenEvent, shownEvent]);
+ ok(ev.type, "Tried to show popup");
+ win.gPermissionPanel._permissionPopup.hidePopup();
+
+ ok(
+ permissionPopupHidden(window),
+ "control center should be hidden in the first window"
+ );
+
+ await disableObserverVerification();
+
+ // Closing the new window should remove all sharing indicators.
+ let promises = [
+ expectObserverCalledOnClose(
+ "recording-device-events",
+ 1,
+ win.gBrowser.selectedBrowser
+ ),
+ expectObserverCalledOnClose(
+ "recording-window-ended",
+ 1,
+ win.gBrowser.selectedBrowser
+ ),
+ ];
+
+ await BrowserTestUtils.closeWindow(win);
+ await Promise.all(promises);
+ await checkNotSharing();
+ },
+ },
+];
+
+add_task(async function test() {
+ await SpecialPowers.pushPrefEnv({ set: [["dom.ipc.processCount", 1]] });
+
+ // An empty tab where we can load the content script without leaving it
+ // behind at the end of the test.
+ BrowserTestUtils.addTab(gBrowser);
+
+ await runTests(gTests);
+});
diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media_unprompted_access.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_unprompted_access.js
new file mode 100644
index 0000000000..e3276cebc4
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_unprompted_access.js
@@ -0,0 +1,666 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const permissionError =
+ "error: NotAllowedError: The request is not allowed " +
+ "by the user agent or the platform in the current context.";
+
+var gTests = [
+ {
+ desc: "getUserMedia audio+camera",
+ run: async function checkAudioVideoWhileLiveTracksExist_audio_camera() {
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ await promiseRequestDevice(true, true);
+ await promise;
+ await observerPromise;
+ let indicator = promiseIndicatorWindow();
+
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+ await indicator;
+ await checkSharingUI({ audio: true, video: true });
+
+ // If there's an active audio+camera stream,
+ // gUM(audio+camera) returns a stream without prompting;
+ let observerPromises = [
+ expectObserverCalled("getUserMedia:request"),
+ expectObserverCalled("getUserMedia:response:allow"),
+ expectObserverCalled("recording-device-events"),
+ ];
+
+ promise = promiseMessage("ok");
+
+ await promiseRequestDevice(true, true);
+ await promise;
+ await Promise.all(observerPromises);
+
+ await promiseNoPopupNotification("webRTC-shareDevices");
+
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+
+ await checkSharingUI({ audio: true, video: true });
+
+ // gUM(screen) causes a prompt.
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true, null, "screen");
+ await promise;
+ await observerPromise;
+
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
+ "webRTC-shareScreen-notification-icon",
+ "anchored to device icon"
+ );
+ checkDeviceSelectors(["screen"]);
+
+ observerPromise = expectObserverCalled("getUserMedia:response:deny");
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+ await observerPromise;
+
+ // Revoke screen block (only). Don't over-revoke ahead of remaining steps.
+ SitePermissions.removeFromPrincipal(
+ null,
+ "screen",
+ gBrowser.selectedBrowser
+ );
+
+ // After closing all streams, gUM(audio+camera) causes a prompt.
+ await closeStream();
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ observerPromise1 = expectObserverCalled("getUserMedia:response:deny");
+ observerPromise2 = expectObserverCalled("recording-window-ended");
+
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+
+ await observerPromise1;
+ await observerPromise2;
+
+ await checkNotSharing();
+ SitePermissions.removeFromPrincipal(
+ null,
+ "screen",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "camera",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "microphone",
+ gBrowser.selectedBrowser
+ );
+ },
+ },
+
+ {
+ desc: "getUserMedia audio+camera -camera",
+ run: async function checkAudioVideoWhileLiveTracksExist_audio_nocamera() {
+ // State: fresh
+
+ {
+ const popupShown = promisePopupNotificationShown("webRTC-shareDevices");
+ const request = expectObserverCalled("getUserMedia:request");
+ await promiseRequestDevice(true, true);
+ await popupShown;
+ await request;
+ const indicator = promiseIndicatorWindow();
+ const response = expectObserverCalled("getUserMedia:response:allow");
+ const deviceEvents = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await response;
+ await deviceEvents;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+ await indicator;
+ await checkSharingUI({ audio: true, video: true });
+
+ // Stop the camera track.
+ await stopTracks("video");
+ await checkSharingUI({ audio: true, video: false });
+ }
+
+ // State: live audio
+
+ {
+ // If there's an active audio track from an audio+camera request,
+ // gUM(camera) causes a prompt.
+ const request = expectObserverCalled("getUserMedia:request");
+ const popupShown = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true);
+ await popupShown;
+ await request;
+ checkDeviceSelectors(["camera"]);
+
+ // Allow and stop the camera again.
+ const response = expectObserverCalled("getUserMedia:response:allow");
+ const deviceEvents = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await response;
+ await deviceEvents;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+ await checkSharingUI({ audio: true, video: true });
+
+ await stopTracks("video");
+ await checkSharingUI({ audio: true, video: false });
+ }
+
+ // State: live audio
+
+ {
+ // If there's an active audio track from an audio+camera request,
+ // gUM(audio+camera) causes a prompt.
+ const request = expectObserverCalled("getUserMedia:request");
+ const popupShown = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true);
+ await popupShown;
+ await request;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ // Allow and stop the camera again.
+ const response = expectObserverCalled("getUserMedia:response:allow");
+ const deviceEvents = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await response;
+ await deviceEvents;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+ await checkSharingUI({ audio: true, video: true });
+
+ await stopTracks("video");
+ await checkSharingUI({ audio: true, video: false });
+ }
+
+ // State: live audio
+
+ {
+ // After closing all streams, gUM(audio) causes a prompt.
+ await closeStream();
+ const request = expectObserverCalled("getUserMedia:request");
+ const popupShown = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, false);
+ await popupShown;
+ await request;
+ checkDeviceSelectors(["microphone"]);
+
+ const response = expectObserverCalled("getUserMedia:response:deny");
+ const windowEnded = expectObserverCalled("recording-window-ended");
+
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+
+ await response;
+ await windowEnded;
+
+ await checkNotSharing();
+ SitePermissions.removeFromPrincipal(
+ null,
+ "camera",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "microphone",
+ gBrowser.selectedBrowser
+ );
+ }
+ },
+ },
+
+ {
+ desc: "getUserMedia audio+camera -audio",
+ run: async function checkAudioVideoWhileLiveTracksExist_camera_noaudio() {
+ // State: fresh
+
+ {
+ const popupShown = promisePopupNotificationShown("webRTC-shareDevices");
+ const request = expectObserverCalled("getUserMedia:request");
+ await promiseRequestDevice(true, true);
+ await popupShown;
+ await request;
+ const indicator = promiseIndicatorWindow();
+ const response = expectObserverCalled("getUserMedia:response:allow");
+ const deviceEvents = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await response;
+ await deviceEvents;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+ await indicator;
+ await checkSharingUI({ audio: true, video: true });
+
+ // Stop the audio track.
+ await stopTracks("audio");
+ await checkSharingUI({ audio: false, video: true });
+ }
+
+ // State: live camera
+
+ {
+ // If there's an active video track from an audio+camera request,
+ // gUM(audio) causes a prompt.
+ const request = expectObserverCalled("getUserMedia:request");
+ const popupShown = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, false);
+ await popupShown;
+ await request;
+ checkDeviceSelectors(["microphone"]);
+
+ // Allow and stop the microphone again.
+ const response = expectObserverCalled("getUserMedia:response:allow");
+ const deviceEvents = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await response;
+ await deviceEvents;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+ await checkSharingUI({ audio: true, video: true });
+
+ await stopTracks("audio");
+ await checkSharingUI({ audio: false, video: true });
+ }
+
+ // State: live camera
+
+ {
+ // If there's an active video track from an audio+camera request,
+ // gUM(audio+camera) causes a prompt.
+ const request = expectObserverCalled("getUserMedia:request");
+ const popupShown = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true);
+ await popupShown;
+ await request;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ // Allow and stop the microphone again.
+ const response = expectObserverCalled("getUserMedia:response:allow");
+ const deviceEvents = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await response;
+ await deviceEvents;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+ await checkSharingUI({ audio: true, video: true });
+
+ await stopTracks("audio");
+ await checkSharingUI({ audio: false, video: true });
+ }
+
+ // State: live camera
+
+ {
+ // After closing all streams, gUM(camera) causes a prompt.
+ await closeStream();
+ const request = expectObserverCalled("getUserMedia:request");
+ const popupShown = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true);
+ await popupShown;
+ await request;
+ checkDeviceSelectors(["camera"]);
+
+ const response = expectObserverCalled("getUserMedia:response:deny");
+ const windowEnded = expectObserverCalled("recording-window-ended");
+
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+
+ await response;
+ await windowEnded;
+
+ await checkNotSharing();
+ SitePermissions.removeFromPrincipal(
+ null,
+ "camera",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "microphone",
+ gBrowser.selectedBrowser
+ );
+ }
+ },
+ },
+
+ {
+ desc: "getUserMedia camera",
+ run: async function checkAudioVideoWhileLiveTracksExist_camera() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true);
+ await promise;
+ await observerPromise;
+ let indicator = promiseIndicatorWindow();
+
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { video: true },
+ "expected camera to be shared"
+ );
+ await indicator;
+ await checkSharingUI({ audio: false, video: true });
+
+ // If there's an active camera stream,
+ // gUM(audio) causes a prompt;
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, false);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone"]);
+
+ observerPromise = expectObserverCalled("getUserMedia:response:deny");
+
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+
+ await observerPromise;
+ SitePermissions.removeFromPrincipal(
+ null,
+ "screen",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "camera",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "microphone",
+ gBrowser.selectedBrowser
+ );
+
+ // gUM(audio+camera) causes a prompt;
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ observerPromise = expectObserverCalled("getUserMedia:response:deny");
+
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+
+ await observerPromise;
+ SitePermissions.removeFromPrincipal(
+ null,
+ "screen",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "camera",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "microphone",
+ gBrowser.selectedBrowser
+ );
+
+ // gUM(screen) causes a prompt;
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true, null, "screen");
+ await promise;
+ await observerPromise;
+
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
+ "webRTC-shareScreen-notification-icon",
+ "anchored to device icon"
+ );
+ checkDeviceSelectors(["screen"]);
+
+ observerPromise = expectObserverCalled("getUserMedia:response:deny");
+
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+
+ await observerPromise;
+ SitePermissions.removeFromPrincipal(
+ null,
+ "screen",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "camera",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "microphone",
+ gBrowser.selectedBrowser
+ );
+
+ // gUM(camera) returns a stream without prompting.
+ let observerPromises = [
+ expectObserverCalled("getUserMedia:request"),
+ expectObserverCalled("getUserMedia:response:allow"),
+ expectObserverCalled("recording-device-events"),
+ ];
+
+ promise = promiseMessage("ok");
+ await promiseRequestDevice(false, true);
+ await promise;
+ await Promise.all(observerPromises);
+
+ await promiseNoPopupNotification("webRTC-shareDevices");
+
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { video: true },
+ "expected camera to be shared"
+ );
+
+ await checkSharingUI({ audio: false, video: true });
+
+ // close all streams
+ await closeStream();
+ },
+ },
+
+ {
+ desc: "getUserMedia audio",
+ run: async function checkAudioVideoWhileLiveTracksExist_audio() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, false);
+ await promise;
+ await observerPromise;
+ let indicator = promiseIndicatorWindow();
+
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true },
+ "expected microphone to be shared"
+ );
+ await indicator;
+ await checkSharingUI({ audio: true, video: false });
+
+ // If there's an active audio stream,
+ // gUM(camera) causes a prompt;
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["camera"]);
+
+ observerPromise = expectObserverCalled("getUserMedia:response:deny");
+
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+
+ await observerPromise;
+ SitePermissions.removeFromPrincipal(
+ null,
+ "screen",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "camera",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "microphone",
+ gBrowser.selectedBrowser
+ );
+
+ // gUM(audio+camera) causes a prompt;
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ observerPromise = expectObserverCalled("getUserMedia:response:deny");
+
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+
+ await observerPromise;
+ SitePermissions.removeFromPrincipal(
+ null,
+ "screen",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "camera",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "microphone",
+ gBrowser.selectedBrowser
+ );
+
+ // gUM(audio) returns a stream without prompting.
+ let observerPromises = [
+ expectObserverCalled("getUserMedia:request"),
+ expectObserverCalled("getUserMedia:response:allow"),
+ expectObserverCalled("recording-device-events"),
+ ];
+ promise = promiseMessage("ok");
+ await promiseRequestDevice(true, false);
+ await promise;
+ await observerPromises;
+ await promiseNoPopupNotification("webRTC-shareDevices");
+
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true },
+ "expected microphone to be shared"
+ );
+
+ await checkSharingUI({ audio: true, video: false });
+
+ // close all streams
+ await closeStream();
+ },
+ },
+];
+
+add_task(async function test() {
+ await runTests(gTests);
+});
diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media_unprompted_access_in_frame.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_unprompted_access_in_frame.js
new file mode 100644
index 0000000000..15329ec666
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_unprompted_access_in_frame.js
@@ -0,0 +1,309 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const permissionError =
+ "error: NotAllowedError: The request is not allowed " +
+ "by the user agent or the platform in the current context.";
+
+var gTests = [
+ {
+ desc: "getUserMedia audio+camera in frame 1",
+ run: async function checkAudioVideoWhileLiveTracksExist_frame() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true, "frame1");
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ video: true, audio: true });
+
+ info("gUM(audio+camera) in frame 2 should prompt");
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true, "frame2");
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ observerPromise1 = expectObserverCalled("getUserMedia:response:deny");
+ observerPromise2 = expectObserverCalled("recording-window-ended");
+
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+
+ await observerPromise1;
+ await observerPromise2;
+
+ SitePermissions.removeFromPrincipal(
+ null,
+ "screen",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "camera",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "microphone",
+ gBrowser.selectedBrowser
+ );
+
+ // If there's an active audio+camera stream in frame 1,
+ // gUM(audio+camera) in frame 1 returns a stream without prompting;
+ let observerPromises = [
+ expectObserverCalled("getUserMedia:request"),
+ expectObserverCalled("getUserMedia:response:allow"),
+ expectObserverCalled("recording-device-events"),
+ ];
+ promise = promiseMessage("ok");
+ await promiseRequestDevice(true, true, "frame1");
+ await promise;
+ await observerPromises;
+ await promiseNoPopupNotification("webRTC-shareDevices");
+
+ // close the stream
+ await closeStream(false, "frame1");
+ },
+ },
+
+ {
+ desc: "getUserMedia audio+camera in frame 1 - part II",
+ run: async function checkAudioVideoWhileLiveTracksExist_frame_partII() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true, "frame1");
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ video: true, audio: true });
+
+ // If there's an active audio+camera stream in frame 1,
+ // gUM(audio+camera) in the top level window causes a prompt;
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ observerPromise1 = expectObserverCalled("getUserMedia:response:deny");
+ observerPromise2 = expectObserverCalled("recording-window-ended");
+
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+
+ await observerPromise1;
+ await observerPromise2;
+
+ // close the stream
+ await closeStream(false, "frame1");
+ SitePermissions.removeFromPrincipal(
+ null,
+ "screen",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "camera",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "microphone",
+ gBrowser.selectedBrowser
+ );
+ },
+ },
+
+ {
+ desc: "getUserMedia audio+camera in frame 1 - reload",
+ run: async function checkAudioVideoWhileLiveTracksExist_frame_reload() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true, "frame1");
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ video: true, audio: true });
+
+ // reload frame 1
+ let observerPromises = [
+ expectObserverCalled("recording-device-stopped"),
+ expectObserverCalled("recording-device-events"),
+ expectObserverCalled("recording-window-ended"),
+ ];
+ await promiseReloadFrame("frame1");
+
+ await Promise.all(observerPromises);
+ await checkNotSharing();
+
+ // After the reload,
+ // gUM(audio+camera) in frame 1 causes a prompt.
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true, "frame1");
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ observerPromise1 = expectObserverCalled("getUserMedia:response:deny");
+ observerPromise2 = expectObserverCalled("recording-window-ended");
+
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+
+ await observerPromise1;
+ await observerPromise2;
+ SitePermissions.removeFromPrincipal(
+ null,
+ "screen",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "camera",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "microphone",
+ gBrowser.selectedBrowser
+ );
+ },
+ },
+
+ {
+ desc: "getUserMedia audio+camera at the top level window",
+ run: async function checkAudioVideoWhileLiveTracksExist_topLevel() {
+ // create an active audio+camera stream at the top level window
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+ let indicator = promiseIndicatorWindow();
+
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ audio: true, video: true });
+
+ // If there's an active audio+camera stream at the top level window,
+ // gUM(audio+camera) in frame 2 causes a prompt.
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true, "frame2");
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ observerPromise1 = expectObserverCalled("getUserMedia:response:deny");
+ observerPromise2 = expectObserverCalled("recording-window-ended");
+
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+
+ await observerPromise1;
+ await observerPromise2;
+
+ // close the stream
+ await closeStream();
+ SitePermissions.removeFromPrincipal(
+ null,
+ "screen",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "camera",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "microphone",
+ gBrowser.selectedBrowser
+ );
+ },
+ },
+];
+
+add_task(async function test() {
+ await runTests(gTests, { relativeURI: "get_user_media_in_frame.html" });
+});
diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media_unprompted_access_queue_request.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_unprompted_access_queue_request.js
new file mode 100644
index 0000000000..ed270967d5
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_unprompted_access_queue_request.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var gTests = [
+ {
+ desc: "test queueing allow video behind allow video",
+ run: async function testQueuingAllowVideoBehindAllowVideo() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true);
+ await promiseRequestDevice(false, true);
+ await promise;
+ checkDeviceSelectors(["camera"]);
+ await observerPromise;
+
+ let promises = [
+ expectObserverCalled("getUserMedia:request"),
+ expectObserverCalled("getUserMedia:response:allow", 2),
+ expectObserverCalled("recording-device-events", 2),
+ ];
+
+ await promiseMessage(
+ "ok",
+ () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ },
+ 2
+ );
+ await Promise.all(promises);
+
+ await promiseNoPopupNotification("webRTC-shareDevices");
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { video: true },
+ "expected camera to be shared"
+ );
+
+ // close all streams
+ await closeStream();
+ },
+ },
+];
+
+add_task(async function test() {
+ SimpleTest.requestCompleteLog();
+ await runTests(gTests);
+});
diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media_unprompted_access_tear_off_tab.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_unprompted_access_tear_off_tab.js
new file mode 100644
index 0000000000..4a48a93853
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_unprompted_access_tear_off_tab.js
@@ -0,0 +1,108 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var gTests = [
+ {
+ desc: "getUserMedia: tearing-off a tab",
+ skipObserverVerification: true,
+ run: async function checkAudioVideoWhileLiveTracksExist_TearingOff() {
+ await enableObserverVerification();
+
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ await promiseRequestDevice(true, true);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ video: true, audio: true });
+
+ // Don't listen to observer notifications in the tab any more, as
+ // they will need to be switched to the new window.
+ await disableObserverVerification();
+
+ info("tearing off the tab");
+ let win = gBrowser.replaceTabWithWindow(gBrowser.selectedTab);
+ await whenDelayedStartupFinished(win);
+ await SimpleTest.promiseFocus(win);
+ await checkSharingUI({ audio: true, video: true }, win);
+
+ await enableObserverVerification(win.gBrowser.selectedBrowser);
+
+ info("request audio+video and check if there is no prompt");
+ let observerPromises = [
+ expectObserverCalled(
+ "getUserMedia:request",
+ 1,
+ win.gBrowser.selectedBrowser
+ ),
+ expectObserverCalled(
+ "getUserMedia:response:allow",
+ 1,
+ win.gBrowser.selectedBrowser
+ ),
+ expectObserverCalled(
+ "recording-device-events",
+ 1,
+ win.gBrowser.selectedBrowser
+ ),
+ ];
+ await promiseRequestDevice(
+ true,
+ true,
+ null,
+ null,
+ win.gBrowser.selectedBrowser
+ );
+ await Promise.all(observerPromises);
+
+ await disableObserverVerification();
+
+ observerPromises = [
+ expectObserverCalledOnClose(
+ "recording-device-events",
+ 1,
+ win.gBrowser.selectedBrowser
+ ),
+ expectObserverCalledOnClose(
+ "recording-window-ended",
+ 1,
+ win.gBrowser.selectedBrowser
+ ),
+ ];
+
+ await BrowserTestUtils.closeWindow(win);
+ await Promise.all(observerPromises);
+
+ await checkNotSharing();
+ },
+ },
+];
+
+add_task(async function test() {
+ await SpecialPowers.pushPrefEnv({ set: [["dom.ipc.processCount", 1]] });
+
+ // An empty tab where we can load the content script without leaving it
+ // behind at the end of the test.
+ BrowserTestUtils.addTab(gBrowser);
+
+ await runTests(gTests);
+});
diff --git a/browser/base/content/test/webrtc/browser_devices_select_audio_output.js b/browser/base/content/test/webrtc/browser_devices_select_audio_output.js
new file mode 100644
index 0000000000..ceafc3269a
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_select_audio_output.js
@@ -0,0 +1,208 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+requestLongerTimeout(2);
+
+const permissionError =
+ "error: NotAllowedError: The request is not allowed " +
+ "by the user agent or the platform in the current context.";
+
+async function requestAudioOutput(options) {
+ await Promise.all([
+ expectObserverCalled("getUserMedia:request"),
+ expectObserverCalled("recording-window-ended"),
+ promiseRequestAudioOutput(options),
+ ]);
+}
+
+async function requestAudioOutputExpectingPrompt(options) {
+ await Promise.all([
+ promisePopupNotificationShown("webRTC-shareDevices"),
+ requestAudioOutput(options),
+ ]);
+
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
+ "webRTC-shareSpeaker-notification-icon",
+ "anchored to device icon"
+ );
+ checkDeviceSelectors(["speaker"]);
+}
+
+async function simulateAudioOutputRequest(options) {
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [options],
+ function simPrompt({ deviceCount, deviceId }) {
+ const devices = [...Array(deviceCount).keys()].map(i => ({
+ type: "audiooutput",
+ rawName: `name ${i}`,
+ deviceIndex: i,
+ rawId: `rawId ${i}`,
+ id: `id ${i}`,
+ QueryInterface: ChromeUtils.generateQI([Ci.nsIMediaDevice]),
+ }));
+ const req = {
+ type: "selectaudiooutput",
+ windowID: content.windowGlobalChild.outerWindowId,
+ devices,
+ getConstraints: () => ({}),
+ getAudioOutputOptions: () => ({ deviceId }),
+ isSecure: true,
+ isHandlingUserInput: true,
+ };
+ const { WebRTCChild } = SpecialPowers.ChromeUtils.import(
+ "resource:///actors/WebRTCChild.jsm"
+ );
+ WebRTCChild.observe(req, "getUserMedia:request");
+ }
+ );
+}
+
+async function allowPrompt() {
+ const observerPromise = expectObserverCalled("getUserMedia:response:allow");
+ PopupNotifications.panel.firstElementChild.button.click();
+ await observerPromise;
+}
+
+async function allow() {
+ await Promise.all([promiseMessage("ok"), allowPrompt()]);
+}
+
+async function denyPrompt() {
+ const observerPromise = expectObserverCalled("getUserMedia:response:deny");
+ activateSecondaryAction(kActionDeny);
+ await observerPromise;
+}
+
+async function deny() {
+ await Promise.all([promiseMessage(permissionError), denyPrompt()]);
+}
+
+async function escapePrompt() {
+ const observerPromise = expectObserverCalled("getUserMedia:response:deny");
+ EventUtils.synthesizeKey("KEY_Escape");
+ await observerPromise;
+}
+
+async function escape() {
+ await Promise.all([promiseMessage(permissionError), escapePrompt()]);
+}
+
+var gTests = [
+ {
+ desc: 'User clicks "Allow" and revokes',
+ run: async function checkAllow() {
+ await requestAudioOutputExpectingPrompt();
+ await allow();
+
+ info("selectAudioOutput() with no deviceId again should prompt again.");
+ await requestAudioOutputExpectingPrompt();
+ await allow();
+
+ info("selectAudioOutput() with same deviceId should not prompt again.");
+ await Promise.all([
+ expectObserverCalled("getUserMedia:response:allow"),
+ promiseMessage("ok"),
+ requestAudioOutput({ requestSameDevice: true }),
+ ]);
+
+ await revokePermission("speaker", true);
+ info("Same deviceId should prompt again after revoked permission.");
+ await requestAudioOutputExpectingPrompt({ requestSameDevice: true });
+ await allow();
+ await revokePermission("speaker", true);
+ },
+ },
+ {
+ desc: 'User clicks "Block"',
+ run: async function checkBlock() {
+ await requestAudioOutputExpectingPrompt();
+ await deny();
+ },
+ },
+ {
+ desc: 'User presses "Esc"',
+ run: async function checkEsc() {
+ await requestAudioOutputExpectingPrompt();
+ await escape();
+ info("selectAudioOutput() after Esc should prompt again.");
+ await requestAudioOutputExpectingPrompt();
+ await allow();
+ await revokePermission("speaker", true);
+ },
+ },
+ {
+ desc: "Single Device",
+ run: async function checkSingle() {
+ await Promise.all([
+ promisePopupNotificationShown("webRTC-shareDevices"),
+ simulateAudioOutputRequest({ deviceCount: 1 }),
+ ]);
+ checkDeviceSelectors(["speaker"]);
+ await escapePrompt();
+ },
+ },
+ {
+ desc: "Multi Device with deviceId",
+ run: async function checkMulti() {
+ const deviceCount = 4;
+ await Promise.all([
+ promisePopupNotificationShown("webRTC-shareDevices"),
+ simulateAudioOutputRequest({ deviceCount, deviceId: "id 2" }),
+ ]);
+ const selectorList = document.getElementById(
+ `webRTC-selectSpeaker-menulist`
+ );
+ is(selectorList.selectedIndex, 2, "pre-selected index");
+ checkDeviceSelectors(["speaker"]);
+ await allowPrompt();
+
+ info("Expect same-device request allowed without prompt");
+ await Promise.all([
+ expectObserverCalled("getUserMedia:response:allow"),
+ simulateAudioOutputRequest({ deviceCount, deviceId: "id 2" }),
+ ]);
+
+ info("Expect prompt for different-device request");
+ await Promise.all([
+ promisePopupNotificationShown("webRTC-shareDevices"),
+ simulateAudioOutputRequest({ deviceCount, deviceId: "id 1" }),
+ ]);
+ await denyPrompt();
+
+ info("Expect prompt again for denied-device request");
+ await Promise.all([
+ promisePopupNotificationShown("webRTC-shareDevices"),
+ simulateAudioOutputRequest({ deviceCount, deviceId: "id 1" }),
+ ]);
+ await escapePrompt();
+
+ await revokePermission("speaker", true);
+ },
+ },
+ {
+ desc: "SitePermissions speaker block",
+ run: async function checkPermissionsBlock() {
+ SitePermissions.setForPrincipal(
+ gBrowser.contentPrincipal,
+ "speaker",
+ SitePermissions.BLOCK
+ );
+ await Promise.all([
+ expectObserverCalled("getUserMedia:request"),
+ expectObserverCalled("recording-window-ended"),
+ expectObserverCalled("getUserMedia:response:deny"),
+ promiseMessage(permissionError),
+ promiseRequestAudioOutput(),
+ ]);
+ SitePermissions.removeFromPrincipal(gBrowser.contentPrincipal, "speaker");
+ },
+ },
+];
+
+add_task(async function test() {
+ await SpecialPowers.pushPrefEnv({ set: [["media.setsinkid.enabled", true]] });
+ await runTests(gTests);
+});
diff --git a/browser/base/content/test/webrtc/browser_global_mute_toggles.js b/browser/base/content/test/webrtc/browser_global_mute_toggles.js
new file mode 100644
index 0000000000..37478f4688
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_global_mute_toggles.js
@@ -0,0 +1,293 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ "https://example.com/"
+);
+const TEST_PAGE = TEST_ROOT + "get_user_media.html";
+const MUTE_TOPICS = [
+ "getUserMedia:muteVideo",
+ "getUserMedia:unmuteVideo",
+ "getUserMedia:muteAudio",
+ "getUserMedia:unmuteAudio",
+];
+
+add_setup(async function() {
+ let prefs = [
+ [PREF_PERMISSION_FAKE, true],
+ [PREF_AUDIO_LOOPBACK, ""],
+ [PREF_VIDEO_LOOPBACK, ""],
+ [PREF_FAKE_STREAMS, true],
+ [PREF_FOCUS_SOURCE, false],
+ ["privacy.webrtc.globalMuteToggles", true],
+ ];
+ await SpecialPowers.pushPrefEnv({ set: prefs });
+});
+
+/**
+ * Returns a Promise that resolves when the content process for
+ * <browser> fires the right observer notification based on the
+ * value of isMuted for the camera.
+ *
+ * Note: Callers must ensure that they first call
+ * BrowserTestUtils.startObservingTopics to monitor the mute and
+ * unmute observer notifications for this to work properly.
+ *
+ * @param {<xul:browser>} browser - The browser running in the content process
+ * to be monitored.
+ * @param {Boolean} isMuted - True if the muted topic should be fired.
+ * @return {Promise}
+ * @resolves {undefined} When the notification fires.
+ */
+function waitForCameraMuteState(browser, isMuted) {
+ let topic = isMuted ? "getUserMedia:muteVideo" : "getUserMedia:unmuteVideo";
+ return BrowserTestUtils.contentTopicObserved(browser.browsingContext, topic);
+}
+
+/**
+ * Returns a Promise that resolves when the content process for
+ * <browser> fires the right observer notification based on the
+ * value of isMuted for the microphone.
+ *
+ * Note: Callers must ensure that they first call
+ * BrowserTestUtils.startObservingTopics to monitor the mute and
+ * unmute observer notifications for this to work properly.
+ *
+ * @param {<xul:browser>} browser - The browser running in the content process
+ * to be monitored.
+ * @param {Boolean} isMuted - True if the muted topic should be fired.
+ * @return {Promise}
+ * @resolves {undefined} When the notification fires.
+ */
+function waitForMicrophoneMuteState(browser, isMuted) {
+ let topic = isMuted ? "getUserMedia:muteAudio" : "getUserMedia:unmuteAudio";
+ return BrowserTestUtils.contentTopicObserved(browser.browsingContext, topic);
+}
+
+/**
+ * Tests that the global mute toggles fire the right observer
+ * notifications in pre-existing content processes.
+ */
+add_task(async function test_notifications() {
+ await BrowserTestUtils.withNewTab(TEST_PAGE, async browser => {
+ let indicatorPromise = promiseIndicatorWindow();
+
+ await shareDevices(browser, true /* camera */, true /* microphone */);
+
+ let indicator = await indicatorPromise;
+ let doc = indicator.document;
+
+ let microphoneMute = doc.getElementById("microphone-mute-toggle");
+ let cameraMute = doc.getElementById("camera-mute-toggle");
+
+ Assert.ok(
+ !microphoneMute.checked,
+ "Microphone toggle should not start checked."
+ );
+ Assert.ok(!cameraMute.checked, "Camera toggle should not start checked.");
+
+ await BrowserTestUtils.startObservingTopics(
+ browser.browsingContext,
+ MUTE_TOPICS
+ );
+
+ info("Muting microphone...");
+ let microphoneMuted = waitForMicrophoneMuteState(browser, true);
+ microphoneMute.click();
+ await microphoneMuted;
+ info("Microphone successfully muted.");
+
+ info("Muting camera...");
+ let cameraMuted = waitForCameraMuteState(browser, true);
+ cameraMute.click();
+ await cameraMuted;
+ info("Camera successfully muted.");
+
+ Assert.ok(
+ microphoneMute.checked,
+ "Microphone toggle should now be checked."
+ );
+ Assert.ok(cameraMute.checked, "Camera toggle should now be checked.");
+
+ info("Unmuting microphone...");
+ let microphoneUnmuted = waitForMicrophoneMuteState(browser, false);
+ microphoneMute.click();
+ await microphoneUnmuted;
+ info("Microphone successfully unmuted.");
+
+ info("Unmuting camera...");
+ let cameraUnmuted = waitForCameraMuteState(browser, false);
+ cameraMute.click();
+ await cameraUnmuted;
+ info("Camera successfully unmuted.");
+
+ await BrowserTestUtils.stopObservingTopics(
+ browser.browsingContext,
+ MUTE_TOPICS
+ );
+ });
+});
+
+/**
+ * Tests that if sharing stops while muted, and the indicator closes,
+ * then the mute state is reset.
+ */
+add_task(async function test_closing_indicator_resets_mute() {
+ await BrowserTestUtils.withNewTab(TEST_PAGE, async browser => {
+ let indicatorPromise = promiseIndicatorWindow();
+
+ await shareDevices(browser, true /* camera */, true /* microphone */);
+
+ let indicator = await indicatorPromise;
+ let doc = indicator.document;
+
+ let microphoneMute = doc.getElementById("microphone-mute-toggle");
+ let cameraMute = doc.getElementById("camera-mute-toggle");
+
+ Assert.ok(
+ !microphoneMute.checked,
+ "Microphone toggle should not start checked."
+ );
+ Assert.ok(!cameraMute.checked, "Camera toggle should not start checked.");
+
+ await BrowserTestUtils.startObservingTopics(
+ browser.browsingContext,
+ MUTE_TOPICS
+ );
+
+ info("Muting microphone...");
+ let microphoneMuted = waitForMicrophoneMuteState(browser, true);
+ microphoneMute.click();
+ await microphoneMuted;
+ info("Microphone successfully muted.");
+
+ info("Muting camera...");
+ let cameraMuted = waitForCameraMuteState(browser, true);
+ cameraMute.click();
+ await cameraMuted;
+ info("Camera successfully muted.");
+
+ Assert.ok(
+ microphoneMute.checked,
+ "Microphone toggle should now be checked."
+ );
+ Assert.ok(cameraMute.checked, "Camera toggle should now be checked.");
+
+ let allUnmuted = Promise.all([
+ waitForMicrophoneMuteState(browser, false),
+ waitForCameraMuteState(browser, false),
+ ]);
+
+ await closeStream();
+ await allUnmuted;
+
+ await BrowserTestUtils.stopObservingTopics(
+ browser.browsingContext,
+ MUTE_TOPICS
+ );
+ });
+});
+
+/**
+ * Test that if the global mute state is set, then newly created
+ * content processes also have their tracks muted after sending
+ * a getUserMedia request.
+ */
+add_task(async function test_new_processes() {
+ let tab1 = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_PAGE,
+ });
+ let browser1 = tab1.linkedBrowser;
+
+ let indicatorPromise = promiseIndicatorWindow();
+
+ await shareDevices(browser1, true /* camera */, true /* microphone */);
+
+ let indicator = await indicatorPromise;
+ let doc = indicator.document;
+
+ let microphoneMute = doc.getElementById("microphone-mute-toggle");
+ let cameraMute = doc.getElementById("camera-mute-toggle");
+
+ Assert.ok(
+ !microphoneMute.checked,
+ "Microphone toggle should not start checked."
+ );
+ Assert.ok(!cameraMute.checked, "Camera toggle should not start checked.");
+
+ await BrowserTestUtils.startObservingTopics(
+ browser1.browsingContext,
+ MUTE_TOPICS
+ );
+
+ info("Muting microphone...");
+ let microphoneMuted = waitForMicrophoneMuteState(browser1, true);
+ microphoneMute.click();
+ await microphoneMuted;
+ info("Microphone successfully muted.");
+
+ info("Muting camera...");
+ let cameraMuted = waitForCameraMuteState(browser1, true);
+ cameraMute.click();
+ await cameraMuted;
+ info("Camera successfully muted.");
+
+ // We'll make sure a new process is being launched by observing
+ // for the ipc:content-created notification.
+ let processLaunched = TestUtils.topicObserved("ipc:content-created");
+
+ let tab2 = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_PAGE,
+ forceNewProcess: true,
+ });
+ let browser2 = tab2.linkedBrowser;
+
+ await processLaunched;
+
+ await BrowserTestUtils.startObservingTopics(
+ browser2.browsingContext,
+ MUTE_TOPICS
+ );
+
+ let microphoneMuted2 = waitForMicrophoneMuteState(browser2, true);
+ let cameraMuted2 = waitForCameraMuteState(browser2, true);
+ info("Sharing the microphone and camera from a new process.");
+ await shareDevices(browser2, true /* camera */, true /* microphone */);
+ await Promise.all([microphoneMuted2, cameraMuted2]);
+
+ info("Unmuting microphone...");
+ let microphoneUnmuted = Promise.all([
+ waitForMicrophoneMuteState(browser1, false),
+ waitForMicrophoneMuteState(browser2, false),
+ ]);
+ microphoneMute.click();
+ await microphoneUnmuted;
+ info("Microphone successfully unmuted.");
+
+ info("Unmuting camera...");
+ let cameraUnmuted = Promise.all([
+ waitForCameraMuteState(browser1, false),
+ waitForCameraMuteState(browser2, false),
+ ]);
+ cameraMute.click();
+ await cameraUnmuted;
+ info("Camera successfully unmuted.");
+
+ await BrowserTestUtils.stopObservingTopics(
+ browser1.browsingContext,
+ MUTE_TOPICS
+ );
+
+ await BrowserTestUtils.stopObservingTopics(
+ browser2.browsingContext,
+ MUTE_TOPICS
+ );
+
+ BrowserTestUtils.removeTab(tab2);
+ BrowserTestUtils.removeTab(tab1);
+});
diff --git a/browser/base/content/test/webrtc/browser_indicator_popuphiding.js b/browser/base/content/test/webrtc/browser_indicator_popuphiding.js
new file mode 100644
index 0000000000..8d02eb5c70
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_indicator_popuphiding.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ "https://example.com/"
+);
+const TEST_PAGE = TEST_ROOT + "get_user_media.html";
+
+/**
+ * Regression test for bug 1668838 - make sure that a popuphiding
+ * event that fires for any popup not related to the device control
+ * menus is ignored and doesn't cause the targets contents to be all
+ * removed.
+ */
+add_task(async function test_popuphiding() {
+ let prefs = [
+ [PREF_PERMISSION_FAKE, true],
+ [PREF_AUDIO_LOOPBACK, ""],
+ [PREF_VIDEO_LOOPBACK, ""],
+ [PREF_FAKE_STREAMS, true],
+ [PREF_FOCUS_SOURCE, false],
+ ];
+ await SpecialPowers.pushPrefEnv({ set: prefs });
+
+ await BrowserTestUtils.withNewTab(TEST_PAGE, async browser => {
+ let indicatorPromise = promiseIndicatorWindow();
+
+ await shareDevices(
+ browser,
+ true /* camera */,
+ true /* microphone */,
+ SHARE_SCREEN
+ );
+
+ let indicator = await indicatorPromise;
+ let doc = indicator.document;
+
+ Assert.ok(doc.body, "Should have a document body in the indicator.");
+
+ let event = new indicator.MouseEvent("popuphiding", { bubbles: true });
+ doc.documentElement.dispatchEvent(event);
+
+ Assert.ok(doc.body, "Should still have a document body in the indicator.");
+ });
+
+ await checkNotSharing();
+});
diff --git a/browser/base/content/test/webrtc/browser_notification_silencing.js b/browser/base/content/test/webrtc/browser_notification_silencing.js
new file mode 100644
index 0000000000..8859195285
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_notification_silencing.js
@@ -0,0 +1,231 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ "https://example.com/"
+);
+const TEST_PAGE = TEST_ROOT + "get_user_media.html";
+
+/**
+ * Tests that the screen / window sharing permission popup offers the ability
+ * for users to silence DOM notifications while sharing.
+ */
+
+/**
+ * Helper function that exercises a specific browser to test whether or not the
+ * user can silence notifications via the display sharing permission panel.
+ *
+ * First, we ensure that notification silencing is disabled by default. Then, we
+ * request screen sharing from the browser, and check the checkbox that
+ * silences notifications. Once screen sharing is established, then we ensure
+ * that notification silencing is enabled. Then we stop sharing, and ensure that
+ * notification silencing is disabled again.
+ *
+ * @param {<xul:browser>} aBrowser - The window to run the test on. This browser
+ * should have TEST_PAGE loaded.
+ * @return Promise
+ * @resolves undefined - When the test on the browser is complete.
+ */
+async function testNotificationSilencing(aBrowser) {
+ let hasIndicator = Services.wm
+ .getEnumerator("Browser:WebRTCGlobalIndicator")
+ .hasMoreElements();
+
+ let window = aBrowser.ownerGlobal;
+
+ let alertsService = Cc["@mozilla.org/alerts-service;1"]
+ .getService(Ci.nsIAlertsService)
+ .QueryInterface(Ci.nsIAlertsDoNotDisturb);
+ Assert.ok(alertsService, "Alerts Service implements nsIAlertsDoNotDisturb");
+ Assert.ok(
+ !alertsService.suppressForScreenSharing,
+ "Should not be silencing notifications to start."
+ );
+
+ let observerPromise = expectObserverCalled(
+ "getUserMedia:request",
+ 1,
+ aBrowser
+ );
+ let promise = promisePopupNotificationShown(
+ "webRTC-shareDevices",
+ null,
+ window
+ );
+ let indicatorPromise = hasIndicator
+ ? Promise.resolve()
+ : promiseIndicatorWindow();
+ await promiseRequestDevice(false, true, null, "screen", aBrowser);
+ await promise;
+ await observerPromise;
+
+ checkDeviceSelectors(["screen"], window);
+
+ let document = window.document;
+
+ // Select one of the windows / screens. It doesn't really matter which.
+ let menulist = document.getElementById("webRTC-selectWindow-menulist");
+ menulist.getItemAtIndex(menulist.itemCount - 1).doCommand();
+
+ let notification = window.PopupNotifications.panel.firstElementChild;
+ Assert.ok(
+ notification.hasAttribute("warninghidden"),
+ "Notification silencing warning message is hidden by default"
+ );
+
+ let checkbox = notification.checkbox;
+ Assert.ok(!!checkbox, "Notification silencing checkbox is present");
+ Assert.ok(!checkbox.checked, "checkbox is not checked by default");
+ checkbox.click();
+ Assert.ok(checkbox.checked, "checkbox now checked");
+ // The orginal behaviour of the checkbox disabled the Allow button. Let's
+ // make sure we're not still doing that.
+ Assert.ok(!notification.button.disabled, "Allow button is not disabled");
+ Assert.ok(
+ notification.hasAttribute("warninghidden"),
+ "No warning message is shown"
+ );
+
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow",
+ 1,
+ aBrowser
+ );
+ let observerPromise2 = expectObserverCalled(
+ "recording-device-events",
+ 1,
+ aBrowser
+ );
+ await promiseMessage(
+ "ok",
+ () => {
+ notification.button.click();
+ },
+ 1,
+ aBrowser
+ );
+ await observerPromise1;
+ await observerPromise2;
+ let indicator = await indicatorPromise;
+
+ Assert.ok(
+ alertsService.suppressForScreenSharing,
+ "Should now be silencing notifications"
+ );
+
+ let indicatorClosedPromise = hasIndicator
+ ? Promise.resolve()
+ : BrowserTestUtils.domWindowClosed(indicator);
+
+ await stopSharing("screen", true, aBrowser, window);
+ await indicatorClosedPromise;
+
+ Assert.ok(
+ !alertsService.suppressForScreenSharing,
+ "Should no longer be silencing notifications"
+ );
+}
+
+add_setup(async function() {
+ // Set prefs so that permissions prompts are shown and loopback devices
+ // are not used. To test the chrome we want prompts to be shown, and
+ // these tests are flakey when using loopback devices (though it would
+ // be desirable to make them work with loopback in future). See bug 1643711.
+ let prefs = [
+ [PREF_PERMISSION_FAKE, true],
+ [PREF_AUDIO_LOOPBACK, ""],
+ [PREF_VIDEO_LOOPBACK, ""],
+ [PREF_FAKE_STREAMS, true],
+ [PREF_FOCUS_SOURCE, false],
+ ];
+
+ await SpecialPowers.pushPrefEnv({ set: prefs });
+});
+
+/**
+ * Tests notification silencing in a normal browser window.
+ */
+add_task(async function testNormalWindow() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ await testNotificationSilencing(browser);
+ }
+ );
+});
+
+/**
+ * Tests notification silencing in a private browser window.
+ */
+add_task(async function testPrivateWindow() {
+ let privateWindow = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser: privateWindow.gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ await testNotificationSilencing(browser);
+ }
+ );
+
+ await BrowserTestUtils.closeWindow(privateWindow);
+});
+
+/**
+ * Tests notification silencing when sharing a screen while already
+ * sharing the microphone. Alone ensures that if we stop sharing the
+ * screen, but continue sharing the microphone, that notification
+ * silencing ends.
+ */
+add_task(async function testWhileSharingMic() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ await promiseRequestDevice(true, true);
+ await promise;
+ await observerPromise;
+
+ let indicatorPromise = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+
+ promise = promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ await promise;
+
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+
+ let indicator = await indicatorPromise;
+ await checkSharingUI({ audio: true, video: true });
+
+ await testNotificationSilencing(browser);
+
+ let indicatorClosedPromise = BrowserTestUtils.domWindowClosed(indicator);
+ await closeStream();
+ await indicatorClosedPromise;
+ }
+ );
+});
diff --git a/browser/base/content/test/webrtc/browser_stop_sharing_button.js b/browser/base/content/test/webrtc/browser_stop_sharing_button.js
new file mode 100644
index 0000000000..db9cc0d1fe
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_stop_sharing_button.js
@@ -0,0 +1,175 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ "https://example.com/"
+);
+const TEST_PAGE = TEST_ROOT + "get_user_media.html";
+
+add_setup(async function() {
+ let prefs = [
+ [PREF_PERMISSION_FAKE, true],
+ [PREF_AUDIO_LOOPBACK, ""],
+ [PREF_VIDEO_LOOPBACK, ""],
+ [PREF_FAKE_STREAMS, true],
+ [PREF_FOCUS_SOURCE, false],
+ ];
+ await SpecialPowers.pushPrefEnv({ set: prefs });
+});
+
+/**
+ * Tests that if the user chooses to "Stop Sharing" a display while
+ * also sharing their microphone or camera, that only the display
+ * stream is stopped.
+ */
+add_task(async function test_stop_sharing() {
+ await BrowserTestUtils.withNewTab(TEST_PAGE, async browser => {
+ let indicatorPromise = promiseIndicatorWindow();
+
+ await shareDevices(
+ browser,
+ true /* camera */,
+ true /* microphone */,
+ SHARE_SCREEN
+ );
+
+ let indicator = await indicatorPromise;
+
+ let stopSharingButton = indicator.document.getElementById("stop-sharing");
+ let stopSharingPromise = expectObserverCalled("recording-device-events");
+ stopSharingButton.click();
+ await stopSharingPromise;
+
+ // Ensure that we're still sharing the other streams.
+ await checkSharingUI({ audio: true, video: true });
+
+ // Ensure that the "display-share" section of the indicator is now hidden
+ Assert.ok(
+ BrowserTestUtils.is_hidden(
+ indicator.document.getElementById("display-share")
+ ),
+ "The display-share section of the indicator should now be hidden."
+ );
+ });
+});
+
+/**
+ * Tests that if the user chooses to "Stop Sharing" a display while
+ * sharing their display on multiple sites, all of those display sharing
+ * streams are closed.
+ */
+add_task(async function test_stop_sharing_multiple() {
+ let indicatorPromise = promiseIndicatorWindow();
+
+ info("Opening first tab");
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE);
+ info("Sharing camera, microphone and screen");
+ await shareDevices(tab1.linkedBrowser, true, true, SHARE_SCREEN);
+
+ info("Opening second tab");
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE);
+ info("Sharing camera and screen");
+ await shareDevices(tab2.linkedBrowser, true, false, SHARE_SCREEN);
+
+ let indicator = await indicatorPromise;
+
+ let stopSharingButton = indicator.document.getElementById("stop-sharing");
+ let stopSharingPromise = TestUtils.waitForCondition(() => {
+ return !webrtcUI.showScreenSharingIndicator;
+ });
+ stopSharingButton.click();
+ await stopSharingPromise;
+
+ Assert.equal(gBrowser.selectedTab, tab2, "Should have tab2 selected.");
+ await checkSharingUI({ audio: false, video: true }, window, {
+ audio: true,
+ video: true,
+ });
+ BrowserTestUtils.removeTab(tab2);
+
+ Assert.equal(gBrowser.selectedTab, tab1, "Should have tab1 selected.");
+ await checkSharingUI({ audio: true, video: true }, window, {
+ audio: true,
+ video: true,
+ });
+
+ // Ensure that the "display-share" section of the indicator is now hidden
+ Assert.ok(
+ BrowserTestUtils.is_hidden(
+ indicator.document.getElementById("display-share")
+ ),
+ "The display-share section of the indicator should now be hidden."
+ );
+
+ BrowserTestUtils.removeTab(tab1);
+});
+
+/**
+ * Tests that if the user chooses to "Stop Sharing" a display, persistent
+ * permissions are not removed for camera or microphone devices.
+ */
+add_task(async function test_keep_permissions() {
+ await BrowserTestUtils.withNewTab(TEST_PAGE, async browser => {
+ let indicatorPromise = promiseIndicatorWindow();
+
+ await shareDevices(
+ browser,
+ true /* camera */,
+ true /* microphone */,
+ SHARE_SCREEN,
+ true /* remember */
+ );
+
+ let indicator = await indicatorPromise;
+
+ let stopSharingButton = indicator.document.getElementById("stop-sharing");
+ let stopSharingPromise = expectObserverCalled("recording-device-events");
+ stopSharingButton.click();
+ await stopSharingPromise;
+
+ // Ensure that we're still sharing the other streams.
+ await checkSharingUI({ audio: true, video: true }, undefined, undefined, {
+ audio: { scope: SitePermissions.SCOPE_PERSISTENT },
+ video: { scope: SitePermissions.SCOPE_PERSISTENT },
+ });
+
+ // Ensure that the "display-share" section of the indicator is now hidden
+ Assert.ok(
+ BrowserTestUtils.is_hidden(
+ indicator.document.getElementById("display-share")
+ ),
+ "The display-share section of the indicator should now be hidden."
+ );
+
+ let { state: micState, scope: micScope } = SitePermissions.getForPrincipal(
+ browser.contentPrincipal,
+ "microphone",
+ browser
+ );
+
+ Assert.equal(micState, SitePermissions.ALLOW);
+ Assert.equal(micScope, SitePermissions.SCOPE_PERSISTENT);
+
+ let { state: camState, scope: camScope } = SitePermissions.getForPrincipal(
+ browser.contentPrincipal,
+ "camera",
+ browser
+ );
+ Assert.equal(camState, SitePermissions.ALLOW);
+ Assert.equal(camScope, SitePermissions.SCOPE_PERSISTENT);
+
+ SitePermissions.removeFromPrincipal(
+ browser.contentPrincipal,
+ "camera",
+ browser
+ );
+ SitePermissions.removeFromPrincipal(
+ browser.contentPrincipal,
+ "microphone",
+ browser
+ );
+ });
+});
diff --git a/browser/base/content/test/webrtc/browser_stop_streams_on_indicator_close.js b/browser/base/content/test/webrtc/browser_stop_streams_on_indicator_close.js
new file mode 100644
index 0000000000..b0863dab61
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_stop_streams_on_indicator_close.js
@@ -0,0 +1,215 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ "https://example.com/"
+);
+const TEST_PAGE = TEST_ROOT + "get_user_media.html";
+
+add_setup(async function() {
+ let prefs = [
+ [PREF_PERMISSION_FAKE, true],
+ [PREF_AUDIO_LOOPBACK, ""],
+ [PREF_VIDEO_LOOPBACK, ""],
+ [PREF_FAKE_STREAMS, true],
+ [PREF_FOCUS_SOURCE, false],
+ ];
+ await SpecialPowers.pushPrefEnv({ set: prefs });
+});
+
+/**
+ * Tests that if the indicator is closed somehow by the user when streams
+ * still ongoing, that all of those streams it represents are stopped, and
+ * the most recent tab that a stream was shared with is selected.
+ *
+ * This test makes sure the global mute toggles for camera and microphone
+ * are disabled, so the indicator only represents display streams, and only
+ * those streams should be stopped on close.
+ */
+add_task(async function test_close_indicator_no_global_toggles() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.webrtc.globalMuteToggles", false]],
+ });
+
+ let indicatorPromise = promiseIndicatorWindow();
+
+ info("Opening first tab");
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE);
+ info("Sharing camera, microphone and screen");
+ await shareDevices(tab1.linkedBrowser, true, true, SHARE_SCREEN, false);
+
+ info("Opening second tab");
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE);
+ info("Sharing camera, microphone and screen");
+ await shareDevices(tab2.linkedBrowser, true, true, SHARE_SCREEN, true);
+
+ info("Opening third tab");
+ let tab3 = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE);
+ info("Sharing screen");
+ await shareDevices(tab3.linkedBrowser, false, false, SHARE_SCREEN, false);
+
+ info("Opening fourth tab");
+ let tab4 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com"
+ );
+
+ Assert.equal(
+ gBrowser.selectedTab,
+ tab4,
+ "Most recently opened tab is selected"
+ );
+
+ let indicator = await indicatorPromise;
+
+ indicator.close();
+
+ // Wait a tick of the event loop to give the unload handler in the indicator
+ // a chance to run.
+ await new Promise(resolve => executeSoon(resolve));
+
+ // Make sure the media capture state has a chance to flush up to the parent.
+ await getMediaCaptureState();
+
+ // The camera and microphone streams should still be active.
+ let camStreams = webrtcUI.getActiveStreams(true, false);
+ Assert.equal(camStreams.length, 2, "Should have found two camera streams");
+ let micStreams = webrtcUI.getActiveStreams(false, true);
+ Assert.equal(
+ micStreams.length,
+ 2,
+ "Should have found two microphone streams"
+ );
+
+ // The camera and microphone permission were remembered for tab2, so check to
+ // make sure that the permissions remain.
+ let { state: camState, scope: camScope } = SitePermissions.getForPrincipal(
+ tab2.linkedBrowser.contentPrincipal,
+ "camera",
+ tab2.linkedBrowser
+ );
+ Assert.equal(camState, SitePermissions.ALLOW);
+ Assert.equal(camScope, SitePermissions.SCOPE_PERSISTENT);
+
+ let { state: micState, scope: micScope } = SitePermissions.getForPrincipal(
+ tab2.linkedBrowser.contentPrincipal,
+ "microphone",
+ tab2.linkedBrowser
+ );
+ Assert.equal(micState, SitePermissions.ALLOW);
+ Assert.equal(micScope, SitePermissions.SCOPE_PERSISTENT);
+
+ Assert.equal(
+ gBrowser.selectedTab,
+ tab3,
+ "Most recently tab that streams were shared with is selected"
+ );
+
+ SitePermissions.removeFromPrincipal(
+ tab2.linkedBrowser.contentPrincipal,
+ "camera",
+ tab2.linkedBrowser
+ );
+
+ SitePermissions.removeFromPrincipal(
+ tab2.linkedBrowser.contentPrincipal,
+ "microphone",
+ tab2.linkedBrowser
+ );
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+ BrowserTestUtils.removeTab(tab3);
+ BrowserTestUtils.removeTab(tab4);
+});
+
+/**
+ * Tests that if the indicator is closed somehow by the user when streams
+ * still ongoing, that all of those streams is represents are stopped, and
+ * the most recent tab that a stream was shared with is selected.
+ *
+ * This test makes sure the global mute toggles are enabled. This means that
+ * when the user manages to close the indicator, we should revoke camera
+ * and microphone permissions too.
+ */
+add_task(async function test_close_indicator_with_global_toggles() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.webrtc.globalMuteToggles", true]],
+ });
+
+ let indicatorPromise = promiseIndicatorWindow();
+
+ info("Opening first tab");
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE);
+ info("Sharing camera, microphone and screen");
+ await shareDevices(tab1.linkedBrowser, true, true, SHARE_SCREEN, false);
+
+ info("Opening second tab");
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE);
+ info("Sharing camera, microphone and screen");
+ await shareDevices(tab2.linkedBrowser, true, true, SHARE_SCREEN, true);
+
+ info("Opening third tab");
+ let tab3 = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE);
+ info("Sharing screen");
+ await shareDevices(tab3.linkedBrowser, false, false, SHARE_SCREEN, false);
+
+ info("Opening fourth tab");
+ let tab4 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com"
+ );
+
+ Assert.equal(
+ gBrowser.selectedTab,
+ tab4,
+ "Most recently opened tab is selected"
+ );
+
+ let indicator = await indicatorPromise;
+
+ indicator.close();
+
+ // Wait a tick of the event loop to give the unload handler in the indicator
+ // a chance to run.
+ await new Promise(resolve => executeSoon(resolve));
+
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ {},
+ "expected nothing to be shared"
+ );
+
+ // Ensuring we no longer have any active streams.
+ let streams = webrtcUI.getActiveStreams(true, true, true, true);
+ Assert.equal(streams.length, 0, "Should have found no active streams");
+
+ // The camera and microphone permissions should have been cleared.
+ let { state: camState } = SitePermissions.getForPrincipal(
+ tab2.linkedBrowser.contentPrincipal,
+ "camera",
+ tab2.linkedBrowser
+ );
+ Assert.equal(camState, SitePermissions.UNKNOWN);
+
+ let { state: micState } = SitePermissions.getForPrincipal(
+ tab2.linkedBrowser.contentPrincipal,
+ "microphone",
+ tab2.linkedBrowser
+ );
+ Assert.equal(micState, SitePermissions.UNKNOWN);
+
+ Assert.equal(
+ gBrowser.selectedTab,
+ tab3,
+ "Most recently tab that streams were shared with is selected"
+ );
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+ BrowserTestUtils.removeTab(tab3);
+ BrowserTestUtils.removeTab(tab4);
+});
diff --git a/browser/base/content/test/webrtc/browser_tab_switch_warning.js b/browser/base/content/test/webrtc/browser_tab_switch_warning.js
new file mode 100644
index 0000000000..2f6519bc13
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_tab_switch_warning.js
@@ -0,0 +1,538 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests the warning that is displayed when switching to background
+ * tabs when sharing the browser window or screen
+ */
+
+// The number of tabs to have in the background for testing.
+const NEW_BACKGROUND_TABS_TO_OPEN = 5;
+const WARNING_PANEL_ID = "sharing-tabs-warning-panel";
+const ALLOW_BUTTON_ID = "sharing-warning-proceed-to-tab";
+const DISABLE_WARNING_FOR_SESSION_CHECKBOX_ID =
+ "sharing-warning-disable-for-session";
+const WINDOW_SHARING_HEADER_ID = "sharing-warning-window-panel-header";
+const SCREEN_SHARING_HEADER_ID = "sharing-warning-screen-panel-header";
+// The number of milliseconds we're willing to wait for the
+// warning panel before we decide that it's not coming.
+const WARNING_PANEL_TIMEOUT_MS = 1000;
+const CTRL_TAB_RUO_PREF = "browser.ctrlTab.sortByRecentlyUsed";
+
+/**
+ * Common helper function that pretendToShareWindow and pretendToShareScreen
+ * call into. Ensures that the first tab is selected, and then (optionally)
+ * does the first "freebie" tab switch to the second tab.
+ *
+ * @param {boolean} doFirstTabSwitch - True if this function should take
+ * care of doing the "freebie" tab switch for you.
+ * @return {Promise}
+ * @resolves {undefined} - Once the simulation is set up.
+ */
+async function pretendToShareDisplay(doFirstTabSwitch) {
+ Assert.equal(
+ gBrowser.selectedTab,
+ gBrowser.tabs[0],
+ "Should start on the first tab."
+ );
+
+ webrtcUI.sharingDisplay = true;
+ if (doFirstTabSwitch) {
+ await BrowserTestUtils.switchTab(gBrowser, gBrowser.tabs[1]);
+ }
+}
+
+/**
+ * Simulates the sharing of a particular browser window. The
+ * simulation doesn't actually share the window over WebRTC, but
+ * does enough to convince webrtcUI that the window is in the shared
+ * window list.
+ *
+ * It is assumed that the first tab is the selected tab when calling
+ * this function.
+ *
+ * This helper function can also automatically perform the first
+ * "freebie" tab switch that never warns. This is its default behaviour.
+ *
+ * @param {DOM Window} aWindow - The window that we're simulating sharing.
+ * @param {boolean} doFirstTabSwitch - True if this function should take
+ * care of doing the "freebie" tab switch for you. Defaults to true.
+ * @return {Promise}
+ * @resolves {undefined} - Once the simulation is set up.
+ */
+async function pretendToShareWindow(aWindow, doFirstTabSwitch = true) {
+ // Poke into webrtcUI so that it thinks that the current browser
+ // window is being shared.
+ webrtcUI.sharedBrowserWindows.add(aWindow);
+ await pretendToShareDisplay(doFirstTabSwitch);
+}
+
+/**
+ * Simulates the sharing of the screen. The simulation doesn't actually share
+ * the screen over WebRTC, but does enough to convince webrtcUI that the screen
+ * is being shared.
+ *
+ * It is assumed that the first tab is the selected tab when calling
+ * this function.
+ *
+ * This helper function can also automatically perform the first
+ * "freebie" tab switch that never warns. This is its default behaviour.
+ *
+ * @param {boolean} doFirstTabSwitch - True if this function should take
+ * care of doing the "freebie" tab switch for you. Defaults to true.
+ * @return {Promise}
+ * @resolves {undefined} - Once the simulation is set up.
+ */
+async function pretendToShareScreen(doFirstTabSwitch = true) {
+ // Poke into webrtcUI so that it thinks that the current screen is being
+ // shared.
+ webrtcUI.sharingScreen = true;
+ await pretendToShareDisplay(doFirstTabSwitch);
+}
+
+/**
+ * Resets webrtcUI's notion of what is being shared. This also clears
+ * out any simulated shared windows, and resets any state that only
+ * persists for a sharing session.
+ *
+ * This helper function will also:
+ * 1. Switch back to the first tab if it's not already selected.
+ * 2. Check if the tab switch warning panel is open, and if so, close it.
+ *
+ * @return {Promise}
+ * @resolves {undefined} - Once the state is reset.
+ */
+async function resetDisplaySharingState() {
+ let firstTabBC = gBrowser.browsers[0].browsingContext;
+ webrtcUI.streamAddedOrRemoved(firstTabBC, { remove: true });
+
+ if (gBrowser.selectedTab !== gBrowser.tabs[0]) {
+ await BrowserTestUtils.switchTab(gBrowser, gBrowser.tabs[0]);
+ }
+
+ let panel = document.getElementById(WARNING_PANEL_ID);
+ if (panel && (panel.state == "open" || panel.state == "showing")) {
+ info("Closing the warning panel.");
+ let panelHidden = BrowserTestUtils.waitForEvent(panel, "popuphidden");
+ panel.hidePopup();
+ await panelHidden;
+ }
+}
+
+/**
+ * Checks to make sure that a tab switch warning doesn't show
+ * within WARNING_PANEL_TIMEOUT_MS milliseconds.
+ *
+ * @return {Promise}
+ * @resolves {undefined} - Once the check is complete.
+ */
+async function ensureNoWarning() {
+ let timerExpired = false;
+ let sawWarning = false;
+
+ let resolver;
+ let timeoutOrPopupShowingPromise = new Promise(resolve => {
+ resolver = resolve;
+ });
+
+ let onPopupShowing = event => {
+ if (event.target.id == WARNING_PANEL_ID) {
+ sawWarning = true;
+ resolver();
+ }
+ };
+ // The panel might not have been lazily-inserted yet, so we
+ // attach the popupshowing handler to the window instead.
+ window.addEventListener("popupshowing", onPopupShowing);
+
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ let timer = setTimeout(() => {
+ timerExpired = true;
+ resolver();
+ }, WARNING_PANEL_TIMEOUT_MS);
+
+ await timeoutOrPopupShowingPromise;
+
+ clearTimeout(timer);
+ window.removeEventListener("popupshowing", onPopupShowing);
+
+ Assert.ok(timerExpired, "Timer should have expired.");
+ Assert.ok(!sawWarning, "Should not have shown the tab switch warning.");
+}
+
+/**
+ * Checks to make sure that a tab switch warning appears for
+ * a particular tab.
+ *
+ * @param {<xul:tab>} tab - The tab that the warning should be anchored to.
+ * @return {Promise}
+ * @resolves {undefined} - Once the check is complete.
+ */
+async function ensureWarning(tab) {
+ let popupShowingEvent = await BrowserTestUtils.waitForEvent(
+ window,
+ "popupshowing",
+ false,
+ event => {
+ return event.target.id == WARNING_PANEL_ID;
+ }
+ );
+ let panel = popupShowingEvent.target;
+
+ Assert.equal(
+ panel.anchorNode,
+ tab,
+ "Expected the warning to be anchored to the right tab."
+ );
+}
+
+add_setup(async function() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.webrtc.sharedTabWarning", true]],
+ });
+
+ // Loads up NEW_BACKGROUND_TABS_TO_OPEN background tabs at about:blank,
+ // and waits until they're fully open.
+ let uris = new Array(NEW_BACKGROUND_TABS_TO_OPEN).fill("about:blank");
+
+ let loadPromises = Promise.all(
+ uris.map(uri => BrowserTestUtils.waitForNewTab(gBrowser, uri, false, true))
+ );
+
+ gBrowser.loadTabs(uris, {
+ inBackground: true,
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+
+ await loadPromises;
+
+ // Switches to the first tab and closes all of the rest.
+ registerCleanupFunction(async () => {
+ await resetDisplaySharingState();
+ gBrowser.removeAllTabsBut(gBrowser.tabs[0]);
+ });
+});
+
+/**
+ * Tests that when sharing the window that the first tab switch does _not_ show
+ * the warning. This is because we presume that the first tab switch since
+ * starting display sharing is for a tab that is intentionally being shared.
+ */
+add_task(async function testFirstTabSwitchAllowed() {
+ pretendToShareWindow(window, false);
+
+ let targetTab = gBrowser.tabs[1];
+
+ let noWarningPromise = ensureNoWarning();
+ await BrowserTestUtils.switchTab(gBrowser, targetTab);
+ await noWarningPromise;
+
+ await resetDisplaySharingState();
+});
+
+/**
+ * Tests that the second tab switch after sharing is not allowed
+ * without a warning. Also tests that the warning can "allow"
+ * the tab switch to proceed, and that no warning is subsequently
+ * shown for the "allowed" tab. Finally, ensures that if the sharing
+ * session ends and a new session begins, that warnings are shown
+ * again for the allowed tabs.
+ */
+add_task(async function testWarningOnSecondTabSwitch() {
+ pretendToShareWindow(window);
+ let originalTab = gBrowser.selectedTab;
+
+ // pretendToShareWindow will have switched us to the second
+ // tab automatically as the first "freebie" tab switch
+
+ let targetTab = gBrowser.tabs[2];
+
+ // Ensure that we show the warning on the second tab switch
+ let warningPromise = ensureWarning(targetTab);
+ await BrowserTestUtils.switchTab(gBrowser, targetTab);
+ await warningPromise;
+
+ // Not only should we have warned, but we should have prevented
+ // the tab switch from occurring.
+ Assert.equal(
+ gBrowser.selectedTab,
+ originalTab,
+ "Should still be on the original tab."
+ );
+
+ // Now test the "Allow" button in the warning to make sure the tab
+ // switch goes through.
+ let tabSwitchPromise = BrowserTestUtils.waitForEvent(
+ gBrowser,
+ "TabSwitchDone"
+ );
+ let allowButton = document.getElementById(ALLOW_BUTTON_ID);
+ allowButton.click();
+ await tabSwitchPromise;
+
+ Assert.equal(
+ gBrowser.selectedTab,
+ targetTab,
+ "Should have switched tabs to the target."
+ );
+
+ // We shouldn't see a warning when switching back to that first
+ // "freebie" tab.
+ let noWarningPromise = ensureNoWarning();
+ await BrowserTestUtils.switchTab(gBrowser, originalTab);
+ await noWarningPromise;
+
+ Assert.equal(
+ gBrowser.selectedTab,
+ originalTab,
+ "Should have switched tabs back to the original tab."
+ );
+
+ // We shouldn't see a warning when switching back to the tab that
+ // we had just allowed.
+ noWarningPromise = ensureNoWarning();
+ await BrowserTestUtils.switchTab(gBrowser, targetTab);
+ await noWarningPromise;
+
+ Assert.equal(
+ gBrowser.selectedTab,
+ targetTab,
+ "Should have switched tabs back to the target tab."
+ );
+
+ // Reset the sharing state, and make sure that warnings can
+ // be displayed again.
+ await resetDisplaySharingState();
+ pretendToShareWindow(window);
+
+ // pretendToShareWindow will have switched us to the second
+ // tab automatically as the first "freebie" tab switch
+ //
+ // Make sure we get the warning again when switching to the
+ // target tab.
+ warningPromise = ensureWarning(targetTab);
+ await BrowserTestUtils.switchTab(gBrowser, targetTab);
+ await warningPromise;
+
+ await resetDisplaySharingState();
+});
+
+/**
+ * Tests that warnings can be skipped for a session via the
+ * checkbox in the warning panel. Also checks that once the
+ * session ends and a new one begins that warnings are displayed
+ * again.
+ */
+add_task(async function testDisableWarningForSession() {
+ pretendToShareWindow(window);
+
+ // pretendToShareWindow will have switched us to the second
+ // tab automatically as the first "freebie" tab switch
+ let targetTab = gBrowser.tabs[2];
+
+ // Ensure that we show the warning on the second tab switch
+ let warningPromise = ensureWarning(targetTab);
+ await BrowserTestUtils.switchTab(gBrowser, targetTab);
+ await warningPromise;
+
+ // Check the checkbox to suppress warnings for the rest of this session.
+ let checkbox = document.getElementById(
+ DISABLE_WARNING_FOR_SESSION_CHECKBOX_ID
+ );
+ checkbox.checked = true;
+
+ // Now test the "Allow" button in the warning to make sure the tab
+ // switch goes through.
+ let tabSwitchPromise = BrowserTestUtils.waitForEvent(
+ gBrowser,
+ "TabSwitchDone"
+ );
+ let allowButton = document.getElementById(ALLOW_BUTTON_ID);
+ allowButton.click();
+ await tabSwitchPromise;
+
+ Assert.equal(
+ gBrowser.selectedTab,
+ targetTab,
+ "Should have switched tabs to the target tab."
+ );
+
+ // Switching to the 4th and 5th tabs should now not show warnings.
+ let noWarningPromise = ensureNoWarning();
+ await BrowserTestUtils.switchTab(gBrowser, gBrowser.tabs[3]);
+ await noWarningPromise;
+
+ noWarningPromise = ensureNoWarning();
+ await BrowserTestUtils.switchTab(gBrowser, gBrowser.tabs[4]);
+ await noWarningPromise;
+
+ // Reset the sharing state, and make sure that warnings can
+ // be displayed again.
+ await resetDisplaySharingState();
+ pretendToShareWindow(window);
+
+ // pretendToShareWindow will have switched us to the second
+ // tab automatically as the first "freebie" tab switch
+ //
+ // Make sure we get the warning again when switching to the
+ // target tab.
+ warningPromise = ensureWarning(targetTab);
+ await BrowserTestUtils.switchTab(gBrowser, targetTab);
+ await warningPromise;
+
+ await resetDisplaySharingState();
+});
+
+/**
+ * Tests that we don't show a warning when sharing a different
+ * window than the one we're switching tabs in.
+ */
+add_task(async function testOtherWindow() {
+ let otherWin = await BrowserTestUtils.openNewBrowserWindow();
+ await SimpleTest.promiseFocus(window);
+ pretendToShareWindow(otherWin);
+
+ // Switching to the 4th and 5th tabs should now not show warnings.
+ let noWarningPromise = ensureNoWarning();
+ await BrowserTestUtils.switchTab(gBrowser, gBrowser.tabs[3]);
+ await noWarningPromise;
+
+ noWarningPromise = ensureNoWarning();
+ await BrowserTestUtils.switchTab(gBrowser, gBrowser.tabs[4]);
+ await noWarningPromise;
+
+ await BrowserTestUtils.closeWindow(otherWin);
+
+ await resetDisplaySharingState();
+});
+
+/**
+ * Tests that we show a different label when sharing the screen
+ * vs when sharing a window.
+ */
+add_task(async function testWindowVsScreenLabel() {
+ pretendToShareWindow(window);
+
+ // pretendToShareWindow will have switched us to the second
+ // tab automatically as the first "freebie" tab switch.
+ // Let's now switch to the third tab.
+ let targetTab = gBrowser.tabs[2];
+
+ // Ensure that we show the warning on this second tab switch
+ let warningPromise = ensureWarning(targetTab);
+ await BrowserTestUtils.switchTab(gBrowser, targetTab);
+ await warningPromise;
+
+ let windowHeader = document.getElementById(WINDOW_SHARING_HEADER_ID);
+ let screenHeader = document.getElementById(SCREEN_SHARING_HEADER_ID);
+ Assert.ok(
+ !BrowserTestUtils.is_hidden(windowHeader),
+ "Should be showing window sharing header"
+ );
+ Assert.ok(
+ BrowserTestUtils.is_hidden(screenHeader),
+ "Should not be showing screen sharing header"
+ );
+
+ // Reset the sharing state, and then pretend to share the screen.
+ await resetDisplaySharingState();
+ pretendToShareScreen();
+
+ // Ensure that we show the warning on this second tab switch
+ warningPromise = ensureWarning(targetTab);
+ await BrowserTestUtils.switchTab(gBrowser, targetTab);
+ await warningPromise;
+
+ Assert.ok(
+ BrowserTestUtils.is_hidden(windowHeader),
+ "Should not be showing window sharing header"
+ );
+ Assert.ok(
+ !BrowserTestUtils.is_hidden(screenHeader),
+ "Should be showing screen sharing header"
+ );
+ await resetDisplaySharingState();
+});
+
+/**
+ * Tests that tab switching via the keyboard can also trigger the
+ * tab switch warnings.
+ */
+add_task(async function testKeyboardTabSwitching() {
+ let pressCtrlTab = async (expectPanel = false) => {
+ let promise;
+ if (expectPanel) {
+ promise = BrowserTestUtils.waitForEvent(ctrlTab.panel, "popupshown");
+ } else {
+ promise = BrowserTestUtils.waitForEvent(document, "keyup");
+ }
+ EventUtils.synthesizeKey("VK_TAB", {
+ ctrlKey: true,
+ shiftKey: false,
+ });
+ await promise;
+ };
+
+ let releaseCtrl = async () => {
+ let promise;
+ if (ctrlTab.isOpen) {
+ promise = BrowserTestUtils.waitForEvent(ctrlTab.panel, "popuphidden");
+ } else {
+ promise = BrowserTestUtils.waitForEvent(document, "keyup");
+ }
+ EventUtils.synthesizeKey("VK_CONTROL", { type: "keyup" });
+ return promise;
+ };
+
+ // Ensure that the (on by default) ctrl-tab switch panel is enabled.
+ await SpecialPowers.pushPrefEnv({
+ set: [[CTRL_TAB_RUO_PREF, true]],
+ });
+
+ pretendToShareWindow(window);
+ let originalTab = gBrowser.selectedTab;
+ await pressCtrlTab(true);
+
+ // The Ctrl-Tab MRU list should be:
+ // 0: Second tab (currently selected)
+ // 1: First tab
+ // 2: Last tab
+ //
+ // Having pressed Ctrl-Tab once, 1 (First tab) is selected in the
+ // panel. We want a tab that will warn, so let's hit Ctrl-Tab again
+ // to choose 2 (Last tab).
+ let targetTab = ctrlTab.tabList[2];
+ await pressCtrlTab();
+
+ let warningPromise = ensureWarning(targetTab);
+ await releaseCtrl();
+ await warningPromise;
+
+ // Hide the warning without allowing the tab switch.
+ let panel = document.getElementById(WARNING_PANEL_ID);
+ panel.hidePopup();
+
+ Assert.equal(
+ gBrowser.selectedTab,
+ originalTab,
+ "Should not have changed from the original tab."
+ );
+
+ // Now switch to the in-order tab switching keyboard shortcut mode.
+ await SpecialPowers.popPrefEnv();
+ await SpecialPowers.pushPrefEnv({
+ set: [[CTRL_TAB_RUO_PREF, false]],
+ });
+
+ // Hitting Ctrl-Tab should choose the _next_ tab over from
+ // the originalTab, which should be the third tab.
+ targetTab = gBrowser.tabs[2];
+
+ warningPromise = ensureWarning(targetTab);
+ await pressCtrlTab();
+ await warningPromise;
+
+ await resetDisplaySharingState();
+});
diff --git a/browser/base/content/test/webrtc/browser_webrtc_hooks.js b/browser/base/content/test/webrtc/browser_webrtc_hooks.js
new file mode 100644
index 0000000000..0fe02d40da
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_webrtc_hooks.js
@@ -0,0 +1,373 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { webrtcUI } = ChromeUtils.import("resource:///modules/webrtcUI.jsm");
+
+const ORIGIN = "https://example.com";
+
+async function tryPeerConnection(browser, expectedError = null) {
+ let errtype = await SpecialPowers.spawn(browser, [], async function() {
+ let pc = new content.RTCPeerConnection();
+ try {
+ await pc.createOffer({ offerToReceiveAudio: true });
+ return null;
+ } catch (err) {
+ return err.name;
+ }
+ });
+
+ let detail = expectedError
+ ? `createOffer() threw a ${expectedError}`
+ : "createOffer() succeeded";
+ is(errtype, expectedError, detail);
+}
+
+// Helper for tests that use the peer-request-allowed and -blocked events.
+// A test that expects some of those events does the following:
+// - call Events.on() before the test to setup event handlers
+// - call Events.expect(name) after a specific event is expected to have
+// occured. This will fail if the event didn't occur, and will return
+// the details passed to the handler for furhter checking.
+// - call Events.off() at the end of the test to clean up. At this point, if
+// any events were triggered that the test did not expect, the test fails.
+const Events = {
+ events: ["peer-request-allowed", "peer-request-blocked"],
+ details: new Map(),
+ handlers: new Map(),
+ on() {
+ for (let event of this.events) {
+ let handler = data => {
+ if (this.details.has(event)) {
+ ok(false, `Got multiple ${event} events`);
+ }
+ this.details.set(event, data);
+ };
+ webrtcUI.on(event, handler);
+ this.handlers.set(event, handler);
+ }
+ },
+ expect(event) {
+ let result = this.details.get(event);
+ isnot(result, undefined, `${event} event was triggered`);
+ this.details.delete(event);
+
+ // All events should have a good origin
+ is(result.origin, ORIGIN, `${event} event has correct origin`);
+
+ return result;
+ },
+ off() {
+ for (let event of this.events) {
+ webrtcUI.off(event, this.handlers.get(event));
+ this.handlers.delete(event);
+ }
+ for (let [event] of this.details) {
+ ok(false, `Got unexpected event ${event}`);
+ }
+ },
+};
+
+var gTests = [
+ {
+ desc: "Basic peer-request-allowed event",
+ run: async function testPeerRequestEvent(browser) {
+ Events.on();
+
+ await tryPeerConnection(browser);
+
+ let details = Events.expect("peer-request-allowed");
+ isnot(
+ details.callID,
+ undefined,
+ "peer-request-allowed event includes callID"
+ );
+ isnot(
+ details.windowID,
+ undefined,
+ "peer-request-allowed event includes windowID"
+ );
+
+ Events.off();
+ },
+ },
+
+ {
+ desc: "Immediate peer connection blocker can allow",
+ run: async function testBlocker(browser) {
+ Events.on();
+
+ let blockerCalled = false;
+ let blocker = params => {
+ is(
+ params.origin,
+ ORIGIN,
+ "Peer connection blocker origin parameter is correct"
+ );
+ blockerCalled = true;
+ return "allow";
+ };
+
+ webrtcUI.addPeerConnectionBlocker(blocker);
+
+ await tryPeerConnection(browser);
+ is(blockerCalled, true, "Blocker was called");
+ Events.expect("peer-request-allowed");
+
+ webrtcUI.removePeerConnectionBlocker(blocker);
+ Events.off();
+ },
+ },
+
+ {
+ desc: "Deferred peer connection blocker can allow",
+ run: async function testDeferredBlocker(browser) {
+ Events.on();
+
+ let blocker = params => Promise.resolve("allow");
+ webrtcUI.addPeerConnectionBlocker(blocker);
+
+ await tryPeerConnection(browser);
+ Events.expect("peer-request-allowed");
+
+ webrtcUI.removePeerConnectionBlocker(blocker);
+ Events.off();
+ },
+ },
+
+ {
+ desc: "Immediate peer connection blocker can deny",
+ run: async function testBlockerDeny(browser) {
+ Events.on();
+
+ let blocker = params => "deny";
+ webrtcUI.addPeerConnectionBlocker(blocker);
+
+ await tryPeerConnection(browser, "NotAllowedError");
+
+ Events.expect("peer-request-blocked");
+
+ webrtcUI.removePeerConnectionBlocker(blocker);
+ Events.off();
+ },
+ },
+
+ {
+ desc: "Multiple blockers work (both allow)",
+ run: async function testMultipleAllowBlockers(browser) {
+ Events.on();
+
+ let blocker1Called = false,
+ blocker1 = params => {
+ blocker1Called = true;
+ return "allow";
+ };
+ webrtcUI.addPeerConnectionBlocker(blocker1);
+
+ let blocker2Called = false,
+ blocker2 = params => {
+ blocker2Called = true;
+ return "allow";
+ };
+ webrtcUI.addPeerConnectionBlocker(blocker2);
+
+ await tryPeerConnection(browser);
+
+ Events.expect("peer-request-allowed");
+ ok(blocker1Called, "First blocker was called");
+ ok(blocker2Called, "Second blocker was called");
+
+ webrtcUI.removePeerConnectionBlocker(blocker1);
+ webrtcUI.removePeerConnectionBlocker(blocker2);
+ Events.off();
+ },
+ },
+
+ {
+ desc: "Multiple blockers work (allow then deny)",
+ run: async function testAllowDenyBlockers(browser) {
+ Events.on();
+
+ let blocker1Called = false,
+ blocker1 = params => {
+ blocker1Called = true;
+ return "allow";
+ };
+ webrtcUI.addPeerConnectionBlocker(blocker1);
+
+ let blocker2Called = false,
+ blocker2 = params => {
+ blocker2Called = true;
+ return "deny";
+ };
+ webrtcUI.addPeerConnectionBlocker(blocker2);
+
+ await tryPeerConnection(browser, "NotAllowedError");
+
+ Events.expect("peer-request-blocked");
+ ok(blocker1Called, "First blocker was called");
+ ok(blocker2Called, "Second blocker was called");
+
+ webrtcUI.removePeerConnectionBlocker(blocker1);
+ webrtcUI.removePeerConnectionBlocker(blocker2);
+ Events.off();
+ },
+ },
+
+ {
+ desc: "Multiple blockers work (deny first)",
+ run: async function testDenyAllowBlockers(browser) {
+ Events.on();
+
+ let blocker1Called = false,
+ blocker1 = params => {
+ blocker1Called = true;
+ return "deny";
+ };
+ webrtcUI.addPeerConnectionBlocker(blocker1);
+
+ let blocker2Called = false,
+ blocker2 = params => {
+ blocker2Called = true;
+ return "allow";
+ };
+ webrtcUI.addPeerConnectionBlocker(blocker2);
+
+ await tryPeerConnection(browser, "NotAllowedError");
+
+ Events.expect("peer-request-blocked");
+ ok(blocker1Called, "First blocker was called");
+ ok(
+ !blocker2Called,
+ "Peer connection blocker after a deny is not invoked"
+ );
+
+ webrtcUI.removePeerConnectionBlocker(blocker1);
+ webrtcUI.removePeerConnectionBlocker(blocker2);
+ Events.off();
+ },
+ },
+
+ {
+ desc: "Blockers may be removed",
+ run: async function testRemoveBlocker(browser) {
+ Events.on();
+
+ let blocker1Called = false,
+ blocker1 = params => {
+ blocker1Called = true;
+ return "allow";
+ };
+ webrtcUI.addPeerConnectionBlocker(blocker1);
+
+ let blocker2Called = false,
+ blocker2 = params => {
+ blocker2Called = true;
+ return "allow";
+ };
+ webrtcUI.addPeerConnectionBlocker(blocker2);
+ webrtcUI.removePeerConnectionBlocker(blocker1);
+
+ await tryPeerConnection(browser);
+
+ Events.expect("peer-request-allowed");
+
+ ok(!blocker1Called, "Removed peer connection blocker is not invoked");
+ ok(blocker2Called, "Second peer connection blocker was invoked");
+
+ webrtcUI.removePeerConnectionBlocker(blocker2);
+ Events.off();
+ },
+ },
+
+ {
+ desc: "Blocker that throws is ignored",
+ run: async function testBlockerThrows(browser) {
+ Events.on();
+ let blocker1Called = false,
+ blocker1 = params => {
+ blocker1Called = true;
+ throw new Error("kaboom");
+ };
+ webrtcUI.addPeerConnectionBlocker(blocker1);
+
+ let blocker2Called = false,
+ blocker2 = params => {
+ blocker2Called = true;
+ return "allow";
+ };
+ webrtcUI.addPeerConnectionBlocker(blocker2);
+
+ await tryPeerConnection(browser);
+
+ Events.expect("peer-request-allowed");
+ ok(blocker1Called, "First blocker was invoked");
+ ok(blocker2Called, "Second blocker was invoked");
+
+ webrtcUI.removePeerConnectionBlocker(blocker1);
+ webrtcUI.removePeerConnectionBlocker(blocker2);
+ Events.off();
+ },
+ },
+
+ {
+ desc: "Cancel peer request",
+ run: async function testBlockerCancel(browser) {
+ let blocker,
+ blockerPromise = new Promise(resolve => {
+ blocker = params => {
+ resolve();
+ // defer indefinitely
+ return new Promise(innerResolve => {});
+ };
+ });
+ webrtcUI.addPeerConnectionBlocker(blocker);
+
+ await SpecialPowers.spawn(browser, [], async function() {
+ new content.RTCPeerConnection().createOffer({
+ offerToReceiveAudio: true,
+ });
+ });
+
+ await blockerPromise;
+
+ let eventPromise = new Promise(resolve => {
+ webrtcUI.on("peer-request-cancel", function listener(details) {
+ resolve(details);
+ webrtcUI.off("peer-request-cancel", listener);
+ });
+ });
+
+ await SpecialPowers.spawn(browser, [], async function() {
+ content.location.reload();
+ });
+
+ let details = await eventPromise;
+ isnot(
+ details.callID,
+ undefined,
+ "peer-request-cancel event includes callID"
+ );
+ is(
+ details.origin,
+ ORIGIN,
+ "peer-request-cancel event has correct origin"
+ );
+
+ webrtcUI.removePeerConnectionBlocker(blocker);
+ },
+ },
+];
+
+add_task(async function test() {
+ await runTests(gTests, {
+ skipObserverVerification: true,
+ cleanup() {
+ is(
+ webrtcUI.peerConnectionBlockers.size,
+ 0,
+ "Peer connection blockers list is empty"
+ );
+ },
+ });
+});
diff --git a/browser/base/content/test/webrtc/get_user_media.html b/browser/base/content/test/webrtc/get_user_media.html
new file mode 100644
index 0000000000..844c6428cc
--- /dev/null
+++ b/browser/base/content/test/webrtc/get_user_media.html
@@ -0,0 +1,124 @@
+<!DOCTYPE html>
+<html>
+<head><meta charset="UTF-8"></head>
+<body>
+<div id="message"></div>
+<script>
+// Specifies whether we are using fake streams to run this automation
+var useFakeStreams = true;
+try {
+ var audioDevice = SpecialPowers.getCharPref("media.audio_loopback_dev");
+ var videoDevice = SpecialPowers.getCharPref("media.video_loopback_dev");
+ dump("TEST DEVICES: Using media devices:\n");
+ dump("audio: " + audioDevice + "\nvideo: " + videoDevice + "\n");
+ useFakeStreams = false;
+} catch (e) {
+ dump("TEST DEVICES: No test devices found (in media.{audio,video}_loopback_dev, using fake streams.\n");
+ useFakeStreams = true;
+}
+
+function message(m) {
+ // eslint-disable-next-line no-unsanitized/property
+ document.getElementById("message").innerHTML = m;
+ top.postMessage(m, "*");
+}
+
+var gStreams = [];
+var gVideoEvents = [];
+var gAudioEvents = [];
+
+async function requestDevice(aAudio, aVideo, aShare, aBadDevice = false) {
+ const opts = {video: aVideo, audio: aAudio};
+ if (aShare) {
+ opts.video = { mediaSource: aShare };
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ }
+ if (useFakeStreams) {
+ opts.fake = true;
+ }
+
+ if (aVideo && aBadDevice) {
+ opts.video = {
+ deviceId: "bad device",
+ };
+ opts.fake = true;
+ }
+
+ if (aAudio && aBadDevice) {
+ opts.audio = {
+ deviceId: "bad device",
+ };
+ opts.fake = true;
+ }
+
+ try {
+ const stream = await navigator.mediaDevices.getUserMedia(opts)
+ gStreams.push(stream);
+
+ const videoTrack = stream.getVideoTracks()[0];
+ if (videoTrack) {
+ for (const name of ["mute", "unmute", "ended"]) {
+ videoTrack.addEventListener(name, () => gVideoEvents.push(name));
+ }
+ }
+
+ const audioTrack = stream.getAudioTracks()[0];
+ if (audioTrack) {
+ for (const name of ["mute", "unmute", "ended"]) {
+ audioTrack.addEventListener(name, () => gAudioEvents.push(name));
+ }
+ }
+ message("ok");
+ } catch (err) {
+ message("error: " + err);
+ }
+}
+
+let selectedAudioOutputId;
+async function requestAudioOutput(options = {}) {
+ const audioOutputOptions = options.requestSameDevice && {
+ deviceId: selectedAudioOutputId,
+ };
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ try {
+ ({ deviceId: selectedAudioOutputId } =
+ await navigator.mediaDevices.selectAudioOutput(audioOutputOptions));
+ message("ok");
+ } catch (err) {
+ message("error: " + err);
+ }
+}
+
+message("pending");
+
+function stopTracks(aKind) {
+ for (let stream of gStreams) {
+ for (let track of stream.getTracks()) {
+ if (track.kind == aKind) {
+ track.stop();
+ stream.removeTrack(track);
+ }
+ }
+ }
+ gStreams = gStreams.filter(s => !!s.getTracks().length);
+ if (aKind == "video") {
+ gVideoEvents = [];
+ } else if (aKind == "audio") {
+ gAudioEvents = [];
+ }
+}
+
+function closeStream() {
+ for (let stream of gStreams) {
+ for (let track of stream.getTracks()) {
+ track.stop();
+ }
+ }
+ gStreams = [];
+ gVideoEvents = [];
+ gAudioEvents = [];
+ message("closed");
+}
+</script>
+</body>
+</html>
diff --git a/browser/base/content/test/webrtc/get_user_media2.html b/browser/base/content/test/webrtc/get_user_media2.html
new file mode 100644
index 0000000000..e239c87cba
--- /dev/null
+++ b/browser/base/content/test/webrtc/get_user_media2.html
@@ -0,0 +1,107 @@
+<!DOCTYPE html>
+<html>
+<head><meta charset="UTF-8"></head>
+<body>
+<div id="message"></div>
+<script>
+// Specifies whether we are using fake streams to run this automation
+var useFakeStreams = true;
+try {
+ var audioDevice = SpecialPowers.getCharPref("media.audio_loopback_dev");
+ var videoDevice = SpecialPowers.getCharPref("media.video_loopback_dev");
+ dump("TEST DEVICES: Using media devices:\n");
+ dump("audio: " + audioDevice + "\nvideo: " + videoDevice + "\n");
+ useFakeStreams = false;
+} catch (e) {
+ dump("TEST DEVICES: No test devices found (in media.{audio,video}_loopback_dev, using fake streams.\n");
+ useFakeStreams = true;
+}
+
+function message(m) {
+ // eslint-disable-next-line no-unsanitized/property
+ document.getElementById("message").innerHTML = m;
+ top.postMessage(m, "*");
+}
+
+var gStreams = [];
+var gVideoEvents = [];
+var gAudioEvents = [];
+
+async function requestDevice(aAudio, aVideo, aShare, aBadDevice = false) {
+ const opts = {video: aVideo, audio: aAudio};
+ if (aShare) {
+ opts.video = { mediaSource: aShare };
+ }
+ if (useFakeStreams) {
+ opts.fake = true;
+ }
+
+ if (aVideo && aBadDevice) {
+ opts.video = {
+ deviceId: "bad device",
+ };
+ opts.fake = true;
+ }
+
+ if (aAudio && aBadDevice) {
+ opts.audio = {
+ deviceId: "bad device",
+ };
+ opts.fake = true;
+ }
+
+ try {
+ const stream = await navigator.mediaDevices.getUserMedia(opts)
+ gStreams.push(stream);
+
+ const videoTrack = stream.getVideoTracks()[0];
+ if (videoTrack) {
+ for (const name of ["mute", "unmute", "ended"]) {
+ videoTrack.addEventListener(name, () => gVideoEvents.push(name));
+ }
+ }
+
+ const audioTrack = stream.getAudioTracks()[0];
+ if (audioTrack) {
+ for (const name of ["mute", "unmute", "ended"]) {
+ audioTrack.addEventListener(name, () => gAudioEvents.push(name));
+ }
+ }
+ message("ok");
+ } catch (err) {
+ message("error: " + err);
+ }
+}
+message("pending");
+
+function stopTracks(aKind) {
+ for (let stream of gStreams) {
+ for (let track of stream.getTracks()) {
+ if (track.kind == aKind) {
+ track.stop();
+ stream.removeTrack(track);
+ }
+ }
+ }
+ gStreams = gStreams.filter(s => !!s.getTracks().length);
+ if (aKind == "video") {
+ gVideoEvents = [];
+ } else if (aKind == "audio") {
+ gAudioEvents = [];
+ }
+}
+
+function closeStream() {
+ for (let stream of gStreams) {
+ for (let track of stream.getTracks()) {
+ track.stop();
+ }
+ }
+ gStreams = [];
+ gVideoEvents = [];
+ gAudioEvents = [];
+ message("closed");
+}
+</script>
+</body>
+</html>
diff --git a/browser/base/content/test/webrtc/get_user_media_in_frame.html b/browser/base/content/test/webrtc/get_user_media_in_frame.html
new file mode 100644
index 0000000000..80ff1020fe
--- /dev/null
+++ b/browser/base/content/test/webrtc/get_user_media_in_frame.html
@@ -0,0 +1,98 @@
+<!DOCTYPE html>
+<html>
+<head><meta charset="UTF-8"></head>
+<body>
+<div id="message"></div>
+<script>
+// Specifies whether we are using fake streams to run this automation
+var useFakeStreams = true;
+try {
+ var audioDevice = SpecialPowers.getCharPref("media.audio_loopback_dev");
+ var videoDevice = SpecialPowers.getCharPref("media.video_loopback_dev");
+ dump("TEST DEVICES: Using media devices:\n");
+ dump("audio: " + audioDevice + "\nvideo: " + videoDevice + "\n");
+ useFakeStreams = false;
+} catch (e) {
+ dump("TEST DEVICES: No test devices found (in media.{audio,video}_loopback_dev, using fake streams.\n");
+ useFakeStreams = true;
+}
+
+function message(m) {
+ // eslint-disable-next-line no-unsanitized/property
+ document.getElementById("message").innerHTML = m;
+ window.top.postMessage(m, "*");
+}
+
+var gStreams = [];
+
+function requestDevice(aAudio, aVideo, aShare) {
+ var opts = {video: aVideo, audio: aAudio};
+ if (aShare) {
+ opts.video = { mediaSource: aShare };
+ } else if (useFakeStreams) {
+ opts.fake = true;
+ }
+
+ window.navigator.mediaDevices.getUserMedia(opts)
+ .then(stream => {
+ gStreams.push(stream);
+ message("ok");
+ }, err => message("error: " + err));
+}
+message("pending");
+
+function stopTracks(aKind) {
+ for (let stream of gStreams) {
+ for (let track of stream.getTracks()) {
+ if (track.kind == aKind) {
+ track.stop();
+ stream.removeTrack(track);
+ }
+ }
+ }
+ gStreams = gStreams.filter(s => !!s.getTracks().length);
+}
+
+function closeStream() {
+ for (let stream of gStreams) {
+ for (let track of stream.getTracks()) {
+ track.stop();
+ }
+ }
+ gStreams = [];
+ message("closed");
+}
+
+const query = document.location.search.substring(1);
+const params = new URLSearchParams(query);
+const origins = params.getAll("origin");
+const nested = params.getAll("nested");
+const gumpage = nested.length
+ ? "get_user_media_in_frame.html"
+ : "get_user_media.html";
+let id = 1;
+if (!origins.length) {
+ for(let i = 0; i < 2; ++i) {
+ const iframe = document.createElement("iframe");
+ iframe.id = `frame${id++}`;
+ iframe.src = gumpage;
+ document.body.appendChild(iframe);
+ }
+} else {
+ for (let origin of origins) {
+ const iframe = document.createElement("iframe");
+ iframe.id = `frame${id++}`;
+ const base = new URL("browser/browser/base/content/test/webrtc/", origin).href;
+ const url = new URL(gumpage, base);
+ for (let nestedOrigin of nested) {
+ url.searchParams.append("origin", nestedOrigin);
+ }
+ iframe.src = url.href;
+ iframe.allow = "camera;microphone";
+ iframe.style = `width:${300 * Math.max(1, nested.length) + (nested.length ? 50 : 0)}px;`;
+ document.body.appendChild(iframe);
+ }
+}
+</script>
+</body>
+</html>
diff --git a/browser/base/content/test/webrtc/get_user_media_in_xorigin_frame.html b/browser/base/content/test/webrtc/get_user_media_in_xorigin_frame.html
new file mode 100644
index 0000000000..2e1df090ae
--- /dev/null
+++ b/browser/base/content/test/webrtc/get_user_media_in_xorigin_frame.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<html>
+<head><meta charset="UTF-8"></head>
+<body>
+<div id="message"></div>
+<script>
+// Specifies whether we are using fake streams to run this automation
+var useFakeStreams = true;
+try {
+ var audioDevice = SpecialPowers.getCharPref("media.audio_loopback_dev");
+ var videoDevice = SpecialPowers.getCharPref("media.video_loopback_dev");
+ dump("TEST DEVICES: Using media devices:\n");
+ dump("audio: " + audioDevice + "\nvideo: " + videoDevice + "\n");
+ useFakeStreams = false;
+} catch (e) {
+ dump("TEST DEVICES: No test devices found (in media.{audio,video}_loopback_dev, using fake streams.\n");
+ useFakeStreams = true;
+}
+
+function message(m) {
+ // eslint-disable-next-line no-unsanitized/property
+ document.getElementById("message").innerHTML = m;
+ window.parent.postMessage(m, "*");
+}
+
+var gStreams = [];
+
+function requestDevice(aAudio, aVideo, aShare) {
+ var opts = {video: aVideo, audio: aAudio};
+ if (aShare) {
+ opts.video = { mediaSource: aShare };
+ } else if (useFakeStreams) {
+ opts.fake = true;
+ }
+
+ window.navigator.mediaDevices.getUserMedia(opts)
+ .then(stream => {
+ gStreams.push(stream);
+ message("ok");
+ }, err => message("error: " + err));
+}
+message("pending");
+
+function stopTracks(aKind) {
+ for (let stream of gStreams) {
+ for (let track of stream.getTracks()) {
+ if (track.kind == aKind) {
+ track.stop();
+ stream.removeTrack(track);
+ }
+ }
+ }
+ gStreams = gStreams.filter(s => !!s.getTracks().length);
+}
+
+function closeStream() {
+ for (let stream of gStreams) {
+ for (let track of stream.getTracks()) {
+ track.stop();
+ }
+ }
+ gStreams = [];
+ message("closed");
+}
+</script>
+<iframe id="frame1" allow="camera;microphone;display-capture" src="https://test1.example.com/browser/browser/base/content/test/webrtc/get_user_media.html"></iframe>
+<iframe id="frame2" allow="camera;microphone" src="https://test1.example.com/browser/browser/base/content/test/webrtc/get_user_media.html"></iframe>
+<iframe id="frame3" src="https://test1.example.com/browser/browser/base/content/test/webrtc/get_user_media.html"></iframe>
+<iframe id="frame4" allow="camera *;microphone *;display-capture *" src="https://test1.example.com/browser/browser/base/content/test/webrtc/get_user_media.html"></iframe>
+</body>
+</html>
diff --git a/browser/base/content/test/webrtc/get_user_media_in_xorigin_frame_ancestor.html b/browser/base/content/test/webrtc/get_user_media_in_xorigin_frame_ancestor.html
new file mode 100644
index 0000000000..bed446a7da
--- /dev/null
+++ b/browser/base/content/test/webrtc/get_user_media_in_xorigin_frame_ancestor.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Permissions Test</title>
+<meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta>
+</head>
+<body>
+ <iframe id="frameAncestor"
+ src="https://test2.example.com/browser/browser/base/content/test/webrtc/get_user_media_in_xorigin_frame.html"
+ allow="camera 'src' https://test1.example.com;microphone 'src' https://test1.example.com;display-capture 'src' https://test1.example.com"></iframe>
+</body>
+</html>
diff --git a/browser/base/content/test/webrtc/gracePeriod/browser.ini b/browser/base/content/test/webrtc/gracePeriod/browser.ini
new file mode 100644
index 0000000000..0f9503fe81
--- /dev/null
+++ b/browser/base/content/test/webrtc/gracePeriod/browser.ini
@@ -0,0 +1,15 @@
+[DEFAULT]
+support-files =
+ ../get_user_media.html
+ ../get_user_media_in_frame.html
+ ../get_user_media_in_xorigin_frame.html
+ ../get_user_media_in_xorigin_frame_ancestor.html
+ ../head.js
+prefs =
+ privacy.webrtc.allowSilencingNotifications=true
+ privacy.webrtc.legacyGlobalIndicator=false
+ privacy.webrtc.sharedTabWarning=false
+
+[../browser_devices_get_user_media_grace.js]
+skip-if =
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
diff --git a/browser/base/content/test/webrtc/head.js b/browser/base/content/test/webrtc/head.js
new file mode 100644
index 0000000000..484af268ec
--- /dev/null
+++ b/browser/base/content/test/webrtc/head.js
@@ -0,0 +1,1342 @@
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+var { SitePermissions } = ChromeUtils.import(
+ "resource:///modules/SitePermissions.jsm"
+);
+var { PermissionTestUtils } = ChromeUtils.import(
+ "resource://testing-common/PermissionTestUtils.jsm"
+);
+
+const PREF_PERMISSION_FAKE = "media.navigator.permission.fake";
+const PREF_AUDIO_LOOPBACK = "media.audio_loopback_dev";
+const PREF_VIDEO_LOOPBACK = "media.video_loopback_dev";
+const PREF_FAKE_STREAMS = "media.navigator.streams.fake";
+const PREF_FOCUS_SOURCE = "media.getusermedia.window.focus_source.enabled";
+
+const STATE_CAPTURE_ENABLED = Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED;
+const STATE_CAPTURE_DISABLED = Ci.nsIMediaManagerService.STATE_CAPTURE_DISABLED;
+
+const USING_LEGACY_INDICATOR = Services.prefs.getBoolPref(
+ "privacy.webrtc.legacyGlobalIndicator",
+ false
+);
+
+const ALLOW_SILENCING_NOTIFICATIONS = Services.prefs.getBoolPref(
+ "privacy.webrtc.allowSilencingNotifications",
+ false
+);
+
+const SHOW_GLOBAL_MUTE_TOGGLES = Services.prefs.getBoolPref(
+ "privacy.webrtc.globalMuteToggles",
+ false
+);
+
+const INDICATOR_PATH = USING_LEGACY_INDICATOR
+ ? "chrome://browser/content/webrtcLegacyIndicator.xhtml"
+ : "chrome://browser/content/webrtcIndicator.xhtml";
+
+const IS_MAC = AppConstants.platform == "macosx";
+
+const SHARE_SCREEN = 1;
+const SHARE_WINDOW = 2;
+
+let observerTopics = [
+ "getUserMedia:response:allow",
+ "getUserMedia:revoke",
+ "getUserMedia:response:deny",
+ "getUserMedia:request",
+ "recording-device-events",
+ "recording-window-ended",
+];
+
+// Structured hierarchy of subframes. Keys are frame id:s, The children member
+// contains nested sub frames if any. The noTest member make a frame be ignored
+// for testing if true.
+let gObserveSubFrames = {};
+// Object of subframes to test. Each element contains the members bc and id, for
+// the frames BrowsingContext and id, respectively.
+let gSubFramesToTest = [];
+let gBrowserContextsToObserve = [];
+
+function whenDelayedStartupFinished(aWindow) {
+ return TestUtils.topicObserved(
+ "browser-delayed-startup-finished",
+ subject => subject == aWindow
+ );
+}
+
+function promiseIndicatorWindow() {
+ let startTime = performance.now();
+
+ // We don't show the legacy indicator window on Mac.
+ if (USING_LEGACY_INDICATOR && IS_MAC) {
+ return Promise.resolve();
+ }
+
+ return new Promise(resolve => {
+ Services.obs.addObserver(function obs(win) {
+ win.addEventListener(
+ "load",
+ function() {
+ if (win.location.href !== INDICATOR_PATH) {
+ info("ignoring a window with this url: " + win.location.href);
+ return;
+ }
+
+ Services.obs.removeObserver(obs, "domwindowopened");
+ executeSoon(() => {
+ ChromeUtils.addProfilerMarker("promiseIndicatorWindow", {
+ startTime,
+ category: "Test",
+ });
+ resolve(win);
+ });
+ },
+ { once: true }
+ );
+ }, "domwindowopened");
+ });
+}
+
+async function assertWebRTCIndicatorStatus(expected) {
+ let ui = ChromeUtils.import("resource:///modules/webrtcUI.jsm").webrtcUI;
+ let expectedState = expected ? "visible" : "hidden";
+ let msg = "WebRTC indicator " + expectedState;
+ if (!expected && ui.showGlobalIndicator) {
+ // It seems the global indicator is not always removed synchronously
+ // in some cases.
+ await TestUtils.waitForCondition(
+ () => !ui.showGlobalIndicator,
+ "waiting for the global indicator to be hidden"
+ );
+ }
+ is(ui.showGlobalIndicator, !!expected, msg);
+
+ let expectVideo = false,
+ expectAudio = false,
+ expectScreen = "";
+ if (expected) {
+ if (expected.video) {
+ expectVideo = true;
+ }
+ if (expected.audio) {
+ expectAudio = true;
+ }
+ if (expected.screen) {
+ expectScreen = expected.screen;
+ }
+ }
+ is(
+ Boolean(ui.showCameraIndicator),
+ expectVideo,
+ "camera global indicator as expected"
+ );
+ is(
+ Boolean(ui.showMicrophoneIndicator),
+ expectAudio,
+ "microphone global indicator as expected"
+ );
+ is(
+ ui.showScreenSharingIndicator,
+ expectScreen,
+ "screen global indicator as expected"
+ );
+
+ for (let win of Services.wm.getEnumerator("navigator:browser")) {
+ let menu = win.document.getElementById("tabSharingMenu");
+ is(
+ !!menu && !menu.hidden,
+ !!expected,
+ "WebRTC menu should be " + expectedState
+ );
+ }
+
+ if (USING_LEGACY_INDICATOR && IS_MAC) {
+ return;
+ }
+
+ if (!expected) {
+ let win = Services.wm.getMostRecentWindow("Browser:WebRTCGlobalIndicator");
+ if (win) {
+ await new Promise((resolve, reject) => {
+ win.addEventListener("unload", function listener(e) {
+ if (e.target == win.document) {
+ win.removeEventListener("unload", listener);
+ executeSoon(resolve);
+ }
+ });
+ });
+ }
+ }
+
+ let indicator = Services.wm.getEnumerator("Browser:WebRTCGlobalIndicator");
+ let hasWindow = indicator.hasMoreElements();
+ is(hasWindow, !!expected, "popup " + msg);
+ if (hasWindow) {
+ let document = indicator.getNext().document;
+ let docElt = document.documentElement;
+
+ if (document.readyState != "complete") {
+ info("Waiting for the sharing indicator's document to load");
+ await new Promise(resolve => {
+ document.addEventListener(
+ "readystatechange",
+ function onReadyStateChange() {
+ if (document.readyState != "complete") {
+ return;
+ }
+ document.removeEventListener(
+ "readystatechange",
+ onReadyStateChange
+ );
+ executeSoon(resolve);
+ }
+ );
+ });
+ }
+
+ if (
+ !USING_LEGACY_INDICATOR &&
+ expected.screen &&
+ expected.screen.startsWith("Window")
+ ) {
+ // These tests were originally written to express window sharing by
+ // having expected.screen start with "Window". This meant that the
+ // legacy indicator is expected to have the "sharingscreen" attribute
+ // set to true when sharing a window.
+ //
+ // The new indicator, however, differentiates between screen, window
+ // and browser window sharing. If we're using the new indicator, we
+ // update the expectations accordingly. This can be removed once we
+ // are able to remove the tests for the legacy indicator.
+ expected.screen = null;
+ expected.window = true;
+ }
+
+ if (!USING_LEGACY_INDICATOR && !SHOW_GLOBAL_MUTE_TOGGLES) {
+ expected.video = false;
+ expected.audio = false;
+
+ let visible = docElt.getAttribute("visible") == "true";
+
+ if (!expected.screen && !expected.window && !expected.browserwindow) {
+ ok(!visible, "Indicator should not be visible in this configuation.");
+ } else {
+ ok(visible, "Indicator should be visible.");
+ }
+ }
+
+ for (let item of ["video", "audio", "screen", "window", "browserwindow"]) {
+ let expectedValue;
+
+ if (USING_LEGACY_INDICATOR) {
+ expectedValue = expected && expected[item] ? "true" : "";
+ } else {
+ expectedValue = expected && expected[item] ? "true" : null;
+ }
+
+ is(
+ docElt.getAttribute("sharing" + item),
+ expectedValue,
+ item + " global indicator attribute as expected"
+ );
+ }
+
+ ok(!indicator.hasMoreElements(), "only one global indicator window");
+ }
+}
+
+function promiseNotificationShown(notification) {
+ let win = notification.browser.ownerGlobal;
+ if (win.PopupNotifications.panel.state == "open") {
+ return Promise.resolve();
+ }
+ let panelPromise = BrowserTestUtils.waitForPopupEvent(
+ win.PopupNotifications.panel,
+ "shown"
+ );
+ notification.reshow();
+ return panelPromise;
+}
+
+function ignoreEvent(aSubject, aTopic, aData) {
+ // With e10s disabled, our content script receives notifications for the
+ // preview displayed in our screen sharing permission prompt; ignore them.
+ const kBrowserURL = AppConstants.BROWSER_CHROME_URL;
+ const nsIPropertyBag = Ci.nsIPropertyBag;
+ if (
+ aTopic == "recording-device-events" &&
+ aSubject.QueryInterface(nsIPropertyBag).getProperty("requestURL") ==
+ kBrowserURL
+ ) {
+ return true;
+ }
+ if (aTopic == "recording-window-ended") {
+ let win = Services.wm.getOuterWindowWithId(aData).top;
+ if (win.document.documentURI == kBrowserURL) {
+ return true;
+ }
+ }
+ return false;
+}
+
+function expectObserverCalledInProcess(aTopic, aCount = 1) {
+ let promises = [];
+ for (let count = aCount; count > 0; count--) {
+ promises.push(TestUtils.topicObserved(aTopic, ignoreEvent));
+ }
+ return promises;
+}
+
+function expectObserverCalled(
+ aTopic,
+ aCount = 1,
+ browser = gBrowser.selectedBrowser
+) {
+ if (!gMultiProcessBrowser) {
+ return expectObserverCalledInProcess(aTopic, aCount);
+ }
+
+ let browsingContext = Element.isInstance(browser)
+ ? browser.browsingContext
+ : browser;
+
+ return BrowserTestUtils.contentTopicObserved(browsingContext, aTopic, aCount);
+}
+
+// This is a special version of expectObserverCalled that should only
+// be used when expecting a notification upon closing a window. It uses
+// the per-process message manager instead of actors to send the
+// notifications.
+function expectObserverCalledOnClose(
+ aTopic,
+ aCount = 1,
+ browser = gBrowser.selectedBrowser
+) {
+ if (!gMultiProcessBrowser) {
+ return expectObserverCalledInProcess(aTopic, aCount);
+ }
+
+ let browsingContext = Element.isInstance(browser)
+ ? browser.browsingContext
+ : browser;
+
+ return new Promise(resolve => {
+ BrowserTestUtils.sendAsyncMessage(
+ browsingContext,
+ "BrowserTestUtils:ObserveTopic",
+ {
+ topic: aTopic,
+ count: 1,
+ filterFunctionSource: ((subject, topic, data) => {
+ Services.cpmm.sendAsyncMessage("WebRTCTest:ObserverCalled", {
+ topic,
+ });
+ return true;
+ }).toSource(),
+ }
+ );
+
+ function observerCalled(message) {
+ if (message.data.topic == aTopic) {
+ Services.ppmm.removeMessageListener(
+ "WebRTCTest:ObserverCalled",
+ observerCalled
+ );
+ resolve();
+ }
+ }
+ Services.ppmm.addMessageListener(
+ "WebRTCTest:ObserverCalled",
+ observerCalled
+ );
+ });
+}
+
+function promiseMessage(
+ aMessage,
+ aAction,
+ aCount = 1,
+ browser = gBrowser.selectedBrowser
+) {
+ let startTime = performance.now();
+ let promise = ContentTask.spawn(browser, [aMessage, aCount], async function([
+ expectedMessage,
+ expectedCount,
+ ]) {
+ return new Promise(resolve => {
+ function listenForMessage({ data }) {
+ if (
+ (!expectedMessage || data == expectedMessage) &&
+ --expectedCount == 0
+ ) {
+ content.removeEventListener("message", listenForMessage);
+ resolve(data);
+ }
+ }
+ content.addEventListener("message", listenForMessage);
+ });
+ });
+ if (aAction) {
+ aAction();
+ }
+ return promise.then(data => {
+ ChromeUtils.addProfilerMarker(
+ "promiseMessage",
+ { startTime, category: "Test" },
+ data
+ );
+ return data;
+ });
+}
+
+function promisePopupNotificationShown(aName, aAction, aWindow = window) {
+ let startTime = performance.now();
+ return new Promise(resolve => {
+ // In case the global webrtc indicator has stolen focus (bug 1421724)
+ aWindow.focus();
+
+ aWindow.PopupNotifications.panel.addEventListener(
+ "popupshown",
+ function() {
+ ok(
+ !!aWindow.PopupNotifications.getNotification(aName),
+ aName + " notification shown"
+ );
+ ok(aWindow.PopupNotifications.isPanelOpen, "notification panel open");
+ ok(
+ !!aWindow.PopupNotifications.panel.firstElementChild,
+ "notification panel populated"
+ );
+
+ executeSoon(() => {
+ ChromeUtils.addProfilerMarker(
+ "promisePopupNotificationShown",
+ { startTime, category: "Test" },
+ aName
+ );
+ resolve();
+ });
+ },
+ { once: true }
+ );
+
+ if (aAction) {
+ aAction();
+ }
+ });
+}
+
+async function promisePopupNotification(aName) {
+ return TestUtils.waitForCondition(
+ () => PopupNotifications.getNotification(aName),
+ aName + " notification appeared"
+ );
+}
+
+async function promiseNoPopupNotification(aName) {
+ return TestUtils.waitForCondition(
+ () => !PopupNotifications.getNotification(aName),
+ aName + " notification removed"
+ );
+}
+
+const kActionAlways = 1;
+const kActionDeny = 2;
+const kActionNever = 3;
+
+function activateSecondaryAction(aAction) {
+ let notification = PopupNotifications.panel.firstElementChild;
+ switch (aAction) {
+ case kActionNever:
+ if (!notification.checkbox.checked) {
+ notification.checkbox.click();
+ }
+ // fallthrough
+ case kActionDeny:
+ notification.secondaryButton.click();
+ break;
+ case kActionAlways:
+ if (!notification.checkbox.checked) {
+ notification.checkbox.click();
+ }
+ notification.button.click();
+ break;
+ }
+}
+
+async function getMediaCaptureState() {
+ let startTime = performance.now();
+
+ function gatherBrowsingContexts(aBrowsingContext) {
+ let list = [aBrowsingContext];
+
+ let children = aBrowsingContext.children;
+ for (let child of children) {
+ list.push(...gatherBrowsingContexts(child));
+ }
+
+ return list;
+ }
+
+ function combine(x, y) {
+ if (
+ x == Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED ||
+ y == Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED
+ ) {
+ return Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED;
+ }
+ if (
+ x == Ci.nsIMediaManagerService.STATE_CAPTURE_DISABLED ||
+ y == Ci.nsIMediaManagerService.STATE_CAPTURE_DISABLED
+ ) {
+ return Ci.nsIMediaManagerService.STATE_CAPTURE_DISABLED;
+ }
+ return Ci.nsIMediaManagerService.STATE_NOCAPTURE;
+ }
+
+ let video = Ci.nsIMediaManagerService.STATE_NOCAPTURE;
+ let audio = Ci.nsIMediaManagerService.STATE_NOCAPTURE;
+ let screen = Ci.nsIMediaManagerService.STATE_NOCAPTURE;
+ let window = Ci.nsIMediaManagerService.STATE_NOCAPTURE;
+ let browser = Ci.nsIMediaManagerService.STATE_NOCAPTURE;
+
+ for (let bc of gatherBrowsingContexts(
+ gBrowser.selectedBrowser.browsingContext
+ )) {
+ let state = await SpecialPowers.spawn(bc, [], async function() {
+ let mediaManagerService = Cc[
+ "@mozilla.org/mediaManagerService;1"
+ ].getService(Ci.nsIMediaManagerService);
+
+ let hasCamera = {};
+ let hasMicrophone = {};
+ let hasScreenShare = {};
+ let hasWindowShare = {};
+ let hasBrowserShare = {};
+ let devices = {};
+ mediaManagerService.mediaCaptureWindowState(
+ content,
+ hasCamera,
+ hasMicrophone,
+ hasScreenShare,
+ hasWindowShare,
+ hasBrowserShare,
+ devices,
+ false
+ );
+
+ return {
+ video: hasCamera.value,
+ audio: hasMicrophone.value,
+ screen: hasScreenShare.value,
+ window: hasWindowShare.value,
+ browser: hasBrowserShare.value,
+ };
+ });
+
+ video = combine(state.video, video);
+ audio = combine(state.audio, audio);
+ screen = combine(state.screen, screen);
+ window = combine(state.window, window);
+ browser = combine(state.browser, browser);
+ }
+
+ let result = {};
+
+ if (video != Ci.nsIMediaManagerService.STATE_NOCAPTURE) {
+ result.video = true;
+ }
+ if (audio != Ci.nsIMediaManagerService.STATE_NOCAPTURE) {
+ result.audio = true;
+ }
+
+ if (screen != Ci.nsIMediaManagerService.STATE_NOCAPTURE) {
+ result.screen = "Screen";
+ } else if (window != Ci.nsIMediaManagerService.STATE_NOCAPTURE) {
+ result.screen = "Window";
+ } else if (browser != Ci.nsIMediaManagerService.STATE_NOCAPTURE) {
+ result.screen = "Browser";
+ }
+
+ ChromeUtils.addProfilerMarker("getMediaCaptureState", {
+ startTime,
+ category: "Test",
+ });
+ return result;
+}
+
+async function stopSharing(
+ aType = "camera",
+ aShouldKeepSharing = false,
+ aFrameBC,
+ aWindow = window
+) {
+ let promiseRecordingEvent = expectObserverCalled(
+ "recording-device-events",
+ 1,
+ aFrameBC
+ );
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:revoke",
+ 1,
+ aFrameBC
+ );
+
+ // If we are stopping screen sharing and expect to still have another stream,
+ // "recording-window-ended" won't be fired.
+ let observerPromise2 = null;
+ if (!aShouldKeepSharing) {
+ observerPromise2 = expectObserverCalled(
+ "recording-window-ended",
+ 1,
+ aFrameBC
+ );
+ }
+
+ await revokePermission(aType, aShouldKeepSharing, aFrameBC, aWindow);
+ await promiseRecordingEvent;
+ await observerPromise1;
+ await observerPromise2;
+
+ if (!aShouldKeepSharing) {
+ await checkNotSharing();
+ }
+}
+
+async function revokePermission(
+ aType = "camera",
+ aShouldKeepSharing = false,
+ aFrameBC,
+ aWindow = window
+) {
+ aWindow.gPermissionPanel._identityPermissionBox.click();
+ let popup = aWindow.gPermissionPanel._permissionPopup;
+ // If the popup gets hidden before being shown, by stray focus/activate
+ // events, don't bother failing the test. It's enough to know that we
+ // started showing the popup.
+ let hiddenEvent = BrowserTestUtils.waitForEvent(popup, "popuphidden");
+ let shownEvent = BrowserTestUtils.waitForEvent(popup, "popupshown");
+ await Promise.race([hiddenEvent, shownEvent]);
+ let doc = aWindow.document;
+ let permissions = doc.getElementById("permission-popup-permission-list");
+ let cancelButton = permissions.querySelector(
+ ".permission-popup-permission-icon." +
+ aType +
+ "-icon ~ " +
+ ".permission-popup-permission-remove-button"
+ );
+
+ cancelButton.click();
+ popup.hidePopup();
+
+ if (!aShouldKeepSharing) {
+ await checkNotSharing();
+ }
+}
+
+function getBrowsingContextForFrame(aBrowsingContext, aFrameId) {
+ if (!aFrameId) {
+ return aBrowsingContext;
+ }
+
+ return SpecialPowers.spawn(aBrowsingContext, [aFrameId], frameId => {
+ return content.document.getElementById(frameId).browsingContext;
+ });
+}
+
+async function getBrowsingContextsAndFrameIdsForSubFrames(
+ aBrowsingContext,
+ aSubFrames
+) {
+ let pendingBrowserSubFrames = [
+ { bc: aBrowsingContext, subFrames: aSubFrames },
+ ];
+ let browsingContextsAndFrames = [];
+ while (pendingBrowserSubFrames.length) {
+ let { bc, subFrames } = pendingBrowserSubFrames.shift();
+ for (let id of Object.keys(subFrames)) {
+ let subBc = await getBrowsingContextForFrame(bc, id);
+ if (subFrames[id].children) {
+ pendingBrowserSubFrames.push({
+ bc: subBc,
+ subFrames: subFrames[id].children,
+ });
+ }
+ if (subFrames[id].noTest) {
+ continue;
+ }
+ let observeBC = subFrames[id].observe ? subBc : undefined;
+ browsingContextsAndFrames.push({ bc: subBc, id, observeBC });
+ }
+ }
+ return browsingContextsAndFrames;
+}
+
+async function promiseRequestDevice(
+ aRequestAudio,
+ aRequestVideo,
+ aFrameId,
+ aType,
+ aBrowsingContext,
+ aBadDevice = false
+) {
+ info("requesting devices");
+ let bc =
+ aBrowsingContext ??
+ (await getBrowsingContextForFrame(gBrowser.selectedBrowser, aFrameId));
+ return SpecialPowers.spawn(
+ bc,
+ [{ aRequestAudio, aRequestVideo, aType, aBadDevice }],
+ async function(args) {
+ let global = content.wrappedJSObject;
+ global.requestDevice(
+ args.aRequestAudio,
+ args.aRequestVideo,
+ args.aType,
+ args.aBadDevice
+ );
+ }
+ );
+}
+
+async function promiseRequestAudioOutput(options) {
+ info("requesting audio output");
+ const bc = gBrowser.selectedBrowser;
+ return SpecialPowers.spawn(bc, [options], async function(opts) {
+ const global = content.wrappedJSObject;
+ global.requestAudioOutput(Cu.cloneInto(opts, content));
+ });
+}
+
+async function stopTracks(
+ aKind,
+ aAlreadyStopped,
+ aLastTracks,
+ aFrameId,
+ aBrowsingContext,
+ aBrowsingContextToObserve
+) {
+ // If the observers are listening to other frames, listen for a notification
+ // on the right subframe.
+ let frameBC =
+ aBrowsingContext ??
+ (await getBrowsingContextForFrame(
+ gBrowser.selectedBrowser.browsingContext,
+ aFrameId
+ ));
+
+ let observerPromises = [];
+ if (!aAlreadyStopped) {
+ observerPromises.push(
+ expectObserverCalled(
+ "recording-device-events",
+ 1,
+ aBrowsingContextToObserve
+ )
+ );
+ }
+ if (aLastTracks) {
+ observerPromises.push(
+ expectObserverCalled(
+ "recording-window-ended",
+ 1,
+ aBrowsingContextToObserve
+ )
+ );
+ }
+
+ info(`Stopping all ${aKind} tracks`);
+ await SpecialPowers.spawn(frameBC, [aKind], async function(kind) {
+ content.wrappedJSObject.stopTracks(kind);
+ });
+
+ await Promise.all(observerPromises);
+}
+
+async function closeStream(
+ aAlreadyClosed,
+ aFrameId,
+ aDontFlushObserverVerification,
+ aBrowsingContext,
+ aBrowsingContextToObserve
+) {
+ // Check that spurious notifications that occur while closing the
+ // stream are handled separately. Tests that use skipObserverVerification
+ // should pass true for aDontFlushObserverVerification.
+ if (!aDontFlushObserverVerification) {
+ await disableObserverVerification();
+ await enableObserverVerification();
+ }
+
+ // If the observers are listening to other frames, listen for a notification
+ // on the right subframe.
+ let frameBC =
+ aBrowsingContext ??
+ (await getBrowsingContextForFrame(
+ gBrowser.selectedBrowser.browsingContext,
+ aFrameId
+ ));
+
+ let observerPromises = [];
+ if (!aAlreadyClosed) {
+ observerPromises.push(
+ expectObserverCalled(
+ "recording-device-events",
+ 1,
+ aBrowsingContextToObserve
+ )
+ );
+ observerPromises.push(
+ expectObserverCalled(
+ "recording-window-ended",
+ 1,
+ aBrowsingContextToObserve
+ )
+ );
+ }
+
+ info("closing the stream");
+ await SpecialPowers.spawn(frameBC, [], async function() {
+ content.wrappedJSObject.closeStream();
+ });
+
+ await Promise.all(observerPromises);
+
+ await assertWebRTCIndicatorStatus(null);
+}
+
+async function reloadAsUser() {
+ info("reloading as a user");
+
+ const reloadButton = document.getElementById("reload-button");
+ await TestUtils.waitForCondition(() => !reloadButton.disabled);
+ // Disable observers as the page is being reloaded which can destroy
+ // the actors listening to the notifications.
+ await disableObserverVerification();
+
+ let loadedPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ reloadButton.click();
+ await loadedPromise;
+
+ await enableObserverVerification();
+}
+
+async function reloadFromContent() {
+ info("reloading from content");
+
+ // Disable observers as the page is being reloaded which can destroy
+ // the actors listening to the notifications.
+ await disableObserverVerification();
+
+ let loadedPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ await ContentTask.spawn(gBrowser.selectedBrowser, null, () =>
+ content.location.reload()
+ );
+
+ await loadedPromise;
+
+ await enableObserverVerification();
+}
+
+async function reloadAndAssertClosedStreams() {
+ await reloadFromContent();
+ await checkNotSharing();
+}
+
+/**
+ * @param {("microphone"|"camera"|"screen")[]} aExpectedTypes
+ * @param {Window} [aWindow]
+ */
+function checkDeviceSelectors(aExpectedTypes, aWindow = window) {
+ for (const type of aExpectedTypes) {
+ if (!["microphone", "camera", "screen", "speaker"].includes(type)) {
+ throw new Error(`Bad device type name ${type}`);
+ }
+ }
+ let document = aWindow.document;
+
+ let expectedDescribedBy = "webRTC-shareDevices-notification-description";
+ for (let type of ["Camera", "Microphone", "Speaker"]) {
+ let selector = document.getElementById(`webRTC-select${type}`);
+ if (!aExpectedTypes.includes(type.toLowerCase())) {
+ ok(selector.hidden, `${type} selector hidden`);
+ continue;
+ }
+ ok(!selector.hidden, `${type} selector visible`);
+ let selectorList = document.getElementById(`webRTC-select${type}-menulist`);
+ let label = document.getElementById(
+ `webRTC-select${type}-single-device-label`
+ );
+ // If there's only 1 device listed, then we should show the label
+ // instead of the menulist.
+ if (selectorList.itemCount == 1) {
+ ok(selectorList.hidden, `${type} selector list should be hidden.`);
+ ok(!label.hidden, `${type} selector label should not be hidden.`);
+ is(
+ label.value,
+ selectorList.selectedItem.getAttribute("label"),
+ `${type} label should be showing the lone device label.`
+ );
+ expectedDescribedBy += ` webRTC-select${type}-icon webRTC-select${type}-single-device-label`;
+ } else {
+ ok(!selectorList.hidden, `${type} selector list should not be hidden.`);
+ ok(label.hidden, `${type} selector label should be hidden.`);
+ }
+ }
+ let ariaDescribedby = aWindow.PopupNotifications.panel.getAttribute(
+ "aria-describedby"
+ );
+ is(ariaDescribedby, expectedDescribedBy, "aria-describedby");
+
+ let screenSelector = document.getElementById("webRTC-selectWindowOrScreen");
+ if (aExpectedTypes.includes("screen")) {
+ ok(!screenSelector.hidden, "screen selector visible");
+ } else {
+ ok(screenSelector.hidden, "screen selector hidden");
+ }
+}
+
+/**
+ * Tests the siteIdentity icons, the permission panel and the global indicator
+ * UI state.
+ * @param {Object} aExpected - Expected state for the current tab.
+ * @param {window} [aWin] - Top level chrome window to test state of.
+ * @param {Object} [aExpectedGlobal] - Expected state for all tabs.
+ * @param {Object} [aExpectedPerm] - Expected permission states keyed by device
+ * type.
+ */
+async function checkSharingUI(
+ aExpected,
+ aWin = window,
+ aExpectedGlobal = null,
+ aExpectedPerm = null
+) {
+ function isPaused(streamState) {
+ if (typeof streamState == "string") {
+ return streamState.includes("Paused");
+ }
+ return streamState == STATE_CAPTURE_DISABLED;
+ }
+
+ let doc = aWin.document;
+ // First check the icon above the control center (i) icon.
+ let permissionBox = doc.getElementById("identity-permission-box");
+ let webrtcSharingIcon = doc.getElementById("webrtc-sharing-icon");
+ ok(webrtcSharingIcon.hasAttribute("sharing"), "sharing attribute is set");
+ let sharing = webrtcSharingIcon.getAttribute("sharing");
+ if (aExpected.screen) {
+ is(sharing, "screen", "showing screen icon in the identity block");
+ } else if (aExpected.video == STATE_CAPTURE_ENABLED) {
+ is(sharing, "camera", "showing camera icon in the identity block");
+ } else if (aExpected.audio == STATE_CAPTURE_ENABLED) {
+ is(sharing, "microphone", "showing mic icon in the identity block");
+ } else if (aExpected.video) {
+ is(sharing, "camera", "showing camera icon in the identity block");
+ } else if (aExpected.audio) {
+ is(sharing, "microphone", "showing mic icon in the identity block");
+ }
+
+ let allStreamsPaused = Object.values(aExpected).every(isPaused);
+ is(
+ webrtcSharingIcon.hasAttribute("paused"),
+ allStreamsPaused,
+ "sharing icon(s) should be in paused state when paused"
+ );
+
+ // Then check the sharing indicators inside the permission popup.
+ permissionBox.click();
+ let popup = aWin.gPermissionPanel._permissionPopup;
+ // If the popup gets hidden before being shown, by stray focus/activate
+ // events, don't bother failing the test. It's enough to know that we
+ // started showing the popup.
+ let hiddenEvent = BrowserTestUtils.waitForEvent(popup, "popuphidden");
+ let shownEvent = BrowserTestUtils.waitForEvent(popup, "popupshown");
+ await Promise.race([hiddenEvent, shownEvent]);
+ let permissions = doc.getElementById("permission-popup-permission-list");
+ for (let id of ["microphone", "camera", "screen"]) {
+ let convertId = idToConvert => {
+ if (idToConvert == "camera") {
+ return "video";
+ }
+ if (idToConvert == "microphone") {
+ return "audio";
+ }
+ return idToConvert;
+ };
+ let expected = aExpected[convertId(id)];
+
+ // Extract the expected permission for the device type.
+ // Defaults to temporary allow.
+ let { state, scope } = aExpectedPerm?.[convertId(id)] || {};
+ if (state == null) {
+ state = SitePermissions.ALLOW;
+ }
+ if (scope == null) {
+ scope = SitePermissions.SCOPE_TEMPORARY;
+ }
+
+ is(
+ !!aWin.gPermissionPanel._sharingState.webRTC[id],
+ !!expected,
+ "sharing state for " + id + " as expected"
+ );
+ let item = permissions.querySelectorAll(
+ ".permission-popup-permission-item-" + id
+ );
+ let stateLabel = item?.[0]?.querySelector(
+ ".permission-popup-permission-state-label"
+ );
+ let icon = permissions.querySelectorAll(
+ ".permission-popup-permission-icon." + id + "-icon"
+ );
+ if (expected) {
+ is(item.length, 1, "should show " + id + " item in permission panel");
+ is(
+ stateLabel?.textContent,
+ SitePermissions.getCurrentStateLabel(state, id, scope),
+ "should show correct item label for " + id
+ );
+ is(icon.length, 1, "should show " + id + " icon in permission panel");
+ is(
+ icon[0].classList.contains("in-use"),
+ expected && !isPaused(expected),
+ "icon should have the in-use class, unless paused"
+ );
+ } else if (!icon.length && !item.length && !stateLabel) {
+ ok(true, "should not show " + id + " item in the permission panel");
+ ok(true, "should not show " + id + " icon in the permission panel");
+ ok(
+ true,
+ "should not show " + id + " state label in the permission panel"
+ );
+ } else {
+ // This will happen if there are persistent permissions set.
+ ok(
+ !icon[0].classList.contains("in-use"),
+ "if shown, the " + id + " icon should not have the in-use class"
+ );
+ is(item.length, 1, "should not show more than 1 " + id + " item");
+ is(icon.length, 1, "should not show more than 1 " + id + " icon");
+ }
+ }
+ aWin.gPermissionPanel._permissionPopup.hidePopup();
+ await TestUtils.waitForCondition(
+ () => permissionPopupHidden(aWin),
+ "identity popup should be hidden"
+ );
+
+ // Check the global indicators.
+ await assertWebRTCIndicatorStatus(aExpectedGlobal || aExpected);
+}
+
+async function checkNotSharing() {
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ {},
+ "expected nothing to be shared"
+ );
+
+ ok(
+ !document.getElementById("webrtc-sharing-icon").hasAttribute("sharing"),
+ "no sharing indicator on the control center icon"
+ );
+
+ await assertWebRTCIndicatorStatus(null);
+}
+
+async function checkNotSharingWithinGracePeriod() {
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ {},
+ "expected nothing to be shared"
+ );
+
+ ok(
+ document.getElementById("webrtc-sharing-icon").hasAttribute("sharing"),
+ "has sharing indicator on the control center icon"
+ );
+ ok(
+ document.getElementById("webrtc-sharing-icon").hasAttribute("paused"),
+ "sharing indicator is paused"
+ );
+
+ await assertWebRTCIndicatorStatus(null);
+}
+
+async function promiseReloadFrame(aFrameId, aBrowsingContext) {
+ let loadedPromise = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ true,
+ arg => {
+ return true;
+ }
+ );
+ let bc =
+ aBrowsingContext ??
+ (await getBrowsingContextForFrame(
+ gBrowser.selectedBrowser.browsingContext,
+ aFrameId
+ ));
+ await SpecialPowers.spawn(bc, [], async function() {
+ content.location.reload();
+ });
+ return loadedPromise;
+}
+
+function promiseChangeLocationFrame(aFrameId, aNewLocation) {
+ return SpecialPowers.spawn(
+ gBrowser.selectedBrowser.browsingContext,
+ [{ aFrameId, aNewLocation }],
+ async function(args) {
+ let frame = content.wrappedJSObject.document.getElementById(
+ args.aFrameId
+ );
+ return new Promise(resolve => {
+ function listener() {
+ frame.removeEventListener("load", listener, true);
+ resolve();
+ }
+ frame.addEventListener("load", listener, true);
+
+ content.wrappedJSObject.document.getElementById(
+ args.aFrameId
+ ).contentWindow.location = args.aNewLocation;
+ });
+ }
+ );
+}
+
+async function openNewTestTab(leaf = "get_user_media.html") {
+ let rootDir = getRootDirectory(gTestPath);
+ rootDir = rootDir.replace(
+ "chrome://mochitests/content/",
+ "https://example.com/"
+ );
+ let absoluteURI = rootDir + leaf;
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, absoluteURI);
+ return tab.linkedBrowser;
+}
+
+// Enabling observer verification adds listeners for all of the webrtc
+// observer topics. If any notifications occur for those topics that
+// were not explicitly requested, a failure will occur.
+async function enableObserverVerification(browser = gBrowser.selectedBrowser) {
+ // Skip these checks in single process mode as it isn't worth implementing it.
+ if (!gMultiProcessBrowser) {
+ return;
+ }
+
+ gBrowserContextsToObserve = [browser.browsingContext];
+
+ // A list of subframe indicies to also add observers to. This only
+ // supports one nested level.
+ if (gObserveSubFrames) {
+ let bcsAndFrameIds = await getBrowsingContextsAndFrameIdsForSubFrames(
+ browser,
+ gObserveSubFrames
+ );
+ for (let { observeBC } of bcsAndFrameIds) {
+ if (observeBC) {
+ gBrowserContextsToObserve.push(observeBC);
+ }
+ }
+ }
+
+ for (let bc of gBrowserContextsToObserve) {
+ await BrowserTestUtils.startObservingTopics(bc, observerTopics);
+ }
+}
+
+async function disableObserverVerification() {
+ if (!gMultiProcessBrowser) {
+ return;
+ }
+
+ for (let bc of gBrowserContextsToObserve) {
+ await BrowserTestUtils.stopObservingTopics(bc, observerTopics).catch(
+ reason => {
+ ok(false, "Failed " + reason);
+ }
+ );
+ }
+}
+
+function permissionPopupHidden(win = window) {
+ let popup = win.gPermissionPanel._permissionPopup;
+ return !popup || popup.state == "closed";
+}
+
+async function runTests(tests, options = {}) {
+ let browser = await openNewTestTab(options.relativeURI);
+
+ is(
+ PopupNotifications._currentNotifications.length,
+ 0,
+ "should start the test without any prior popup notification"
+ );
+ ok(
+ permissionPopupHidden(),
+ "should start the test with the permission panel hidden"
+ );
+
+ // Set prefs so that permissions prompts are shown and loopback devices
+ // are not used. To test the chrome we want prompts to be shown, and
+ // these tests are flakey when using loopback devices (though it would
+ // be desirable to make them work with loopback in future). See bug 1643711.
+ let prefs = [
+ [PREF_PERMISSION_FAKE, true],
+ [PREF_AUDIO_LOOPBACK, ""],
+ [PREF_VIDEO_LOOPBACK, ""],
+ [PREF_FAKE_STREAMS, true],
+ [PREF_FOCUS_SOURCE, false],
+ ];
+ await SpecialPowers.pushPrefEnv({ set: prefs });
+
+ // When the frames are in different processes, add observers to each frame,
+ // to ensure that the notifications don't get sent in the wrong process.
+ gObserveSubFrames = SpecialPowers.useRemoteSubframes ? options.subFrames : {};
+
+ for (let testCase of tests) {
+ let startTime = performance.now();
+ info(testCase.desc);
+ if (
+ !testCase.skipObserverVerification &&
+ !options.skipObserverVerification
+ ) {
+ await enableObserverVerification();
+ }
+ await testCase.run(browser, options.subFrames);
+ if (
+ !testCase.skipObserverVerification &&
+ !options.skipObserverVerification
+ ) {
+ await disableObserverVerification();
+ }
+ if (options.cleanup) {
+ await options.cleanup();
+ }
+ ChromeUtils.addProfilerMarker(
+ "browser-test",
+ { startTime, category: "Test" },
+ testCase.desc
+ );
+ }
+
+ // Some tests destroy the original tab and leave a new one in its place.
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+}
+
+/**
+ * Given a browser from a tab in this window, chooses to share
+ * some combination of camera, mic or screen.
+ *
+ * @param {<xul:browser} browser - The browser to share devices with.
+ * @param {boolean} camera - True to share a camera device.
+ * @param {boolean} mic - True to share a microphone device.
+ * @param {Number} [screenOrWin] - One of either SHARE_WINDOW or SHARE_SCREEN
+ * to share a window or screen. Defaults to neither.
+ * @param {boolean} remember - True to persist the permission to the
+ * SitePermissions database as SitePermissions.SCOPE_PERSISTENT. Note that
+ * callers are responsible for clearing this persistent permission.
+ * @return {Promise}
+ * @resolves {undefined} - Once the sharing is complete.
+ */
+async function shareDevices(
+ browser,
+ camera,
+ mic,
+ screenOrWin = 0,
+ remember = false
+) {
+ if (camera || mic) {
+ let promise = promisePopupNotificationShown(
+ "webRTC-shareDevices",
+ null,
+ window
+ );
+
+ await promiseRequestDevice(mic, camera, null, null, browser);
+ await promise;
+
+ const expectedDeviceSelectorTypes = [
+ camera && "camera",
+ mic && "microphone",
+ ].filter(x => x);
+ checkDeviceSelectors(expectedDeviceSelectorTypes);
+ let observerPromise1 = expectObserverCalled("getUserMedia:response:allow");
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+
+ let rememberCheck = PopupNotifications.panel.querySelector(
+ ".popup-notification-checkbox"
+ );
+ rememberCheck.checked = remember;
+
+ promise = promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+
+ await observerPromise1;
+ await observerPromise2;
+ await promise;
+ }
+
+ if (screenOrWin) {
+ let promise = promisePopupNotificationShown(
+ "webRTC-shareDevices",
+ null,
+ window
+ );
+
+ await promiseRequestDevice(false, true, null, "screen", browser);
+ await promise;
+
+ checkDeviceSelectors(["screen"], window);
+
+ let document = window.document;
+
+ let menulist = document.getElementById("webRTC-selectWindow-menulist");
+ let displayMediaSource;
+
+ if (screenOrWin == SHARE_SCREEN) {
+ displayMediaSource = "screen";
+ } else if (screenOrWin == SHARE_WINDOW) {
+ displayMediaSource = "window";
+ } else {
+ throw new Error("Got an invalid argument to shareDevices.");
+ }
+
+ let menuitem = null;
+ for (let i = 0; i < menulist.itemCount; ++i) {
+ let current = menulist.getItemAtIndex(i);
+ if (current.mediaSource == displayMediaSource) {
+ menuitem = current;
+ break;
+ }
+ }
+
+ Assert.ok(menuitem, "Should have found an appropriate display menuitem");
+ menuitem.doCommand();
+
+ let notification = window.PopupNotifications.panel.firstElementChild;
+
+ let observerPromise1 = expectObserverCalled("getUserMedia:response:allow");
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage(
+ "ok",
+ () => {
+ notification.button.click();
+ },
+ 1,
+ browser
+ );
+ await observerPromise1;
+ await observerPromise2;
+ }
+}
diff --git a/browser/base/content/test/webrtc/legacyIndicator/browser.ini b/browser/base/content/test/webrtc/legacyIndicator/browser.ini
new file mode 100644
index 0000000000..ab6865a366
--- /dev/null
+++ b/browser/base/content/test/webrtc/legacyIndicator/browser.ini
@@ -0,0 +1,56 @@
+[DEFAULT]
+support-files =
+ ../get_user_media.html
+ ../get_user_media_in_frame.html
+ ../get_user_media_in_xorigin_frame.html
+ ../get_user_media_in_xorigin_frame_ancestor.html
+ ../head.js
+prefs =
+ privacy.webrtc.allowSilencingNotifications=false
+ privacy.webrtc.legacyGlobalIndicator=true
+ privacy.webrtc.sharedTabWarning=false
+ privacy.webrtc.deviceGracePeriodTimeoutMs=0
+
+[../browser_devices_get_user_media.js]
+skip-if =
+ (os == "linux") # linux: bug 976544, bug 1616011
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+[../browser_devices_get_user_media_anim.js]
+skip-if =
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+[../browser_devices_get_user_media_default_permissions.js]
+[../browser_devices_get_user_media_in_frame.js]
+skip-if = debug # bug 1369731
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+[../browser_devices_get_user_media_in_xorigin_frame.js]
+skip-if =
+ debug # bug 1369731
+ apple_silicon # bug 1707735
+ apple_catalina # platform migration
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+[../browser_devices_get_user_media_in_xorigin_frame_chain.js]
+[../browser_devices_get_user_media_multi_process.js]
+skip-if =
+ (debug && os == "win") # bug 1393761
+ apple_silicon # bug 1707735
+ apple_catalina # platform migration
+[../browser_devices_get_user_media_paused.js]
+skip-if =
+ (os == "win" && !debug) # Bug 1440900
+ (os =="linux" && !debug && bits == 64) # Bug 1440900
+ apple_silicon # bug 1707735
+ apple_catalina # platform migration
+[../browser_devices_get_user_media_screen.js]
+skip-if =
+ (os == 'linux') # Bug 1503991
+ apple_silicon # bug 1707735
+ apple_catalina # platform migration
+[../browser_devices_get_user_media_tear_off_tab.js]
+[../browser_devices_get_user_media_unprompted_access.js]
+skip-if = (os == "linux") # Bug 1712012
+[../browser_devices_get_user_media_unprompted_access_in_frame.js]
+[../browser_devices_get_user_media_unprompted_access_tear_off_tab.js]
+skip-if = (os == "win" && bits == 64) # win8: bug 1334752
+[../browser_devices_get_user_media_unprompted_access_queue_request.js]
+[../browser_webrtc_hooks.js]
+[../browser_devices_get_user_media_queue_request.js]
diff --git a/browser/base/content/test/webrtc/peerconnection_connect.html b/browser/base/content/test/webrtc/peerconnection_connect.html
new file mode 100644
index 0000000000..5af6a4aafd
--- /dev/null
+++ b/browser/base/content/test/webrtc/peerconnection_connect.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<html>
+<head><meta charset="UTF-8"></head>
+<body>
+<div id="Page that opens a two peerconnections, and starts ICE"></div>
+<script>
+ const test = async () => {
+ const offerer = new RTCPeerConnection();
+ const answerer = new RTCPeerConnection();
+ offerer.addTransceiver('audio');
+
+ async function iceConnected(pc) {
+ return new Promise(r => {
+ if (pc.iceConnectionState == "connected") {
+ r();
+ }
+ pc.oniceconnectionstatechange = () => {
+ if (pc.iceConnectionState == "connected") {
+ r();
+ }
+ }
+ });
+ }
+
+ offerer.onicecandidate = e => answerer.addIceCandidate(e.candidate);
+ answerer.onicecandidate = e => offerer.addIceCandidate(e.candidate);
+ await offerer.setLocalDescription();
+ await answerer.setRemoteDescription(offerer.localDescription);
+ await answerer.setLocalDescription();
+ await offerer.setRemoteDescription(answerer.localDescription);
+ await iceConnected(offerer);
+ await iceConnected(answerer);
+ offerer.close();
+ answerer.close();
+ };
+ test();
+</script>
+</body>
+</html>
diff --git a/browser/base/content/test/webrtc/single_peerconnection.html b/browser/base/content/test/webrtc/single_peerconnection.html
new file mode 100644
index 0000000000..4b4432c51b
--- /dev/null
+++ b/browser/base/content/test/webrtc/single_peerconnection.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html>
+<head><meta charset="UTF-8"></head>
+<body>
+<div id="Page that opens a single peerconnection"></div>
+<script>
+ let test = async () => {
+ let pc = new RTCPeerConnection();
+ pc.addTransceiver('audio');
+ pc.addTransceiver('video');
+ await pc.setLocalDescription();
+ await new Promise(r => {
+ pc.onicegatheringstatechange = () => {
+ if (pc.iceGatheringState == "complete") {
+ r();
+ }
+ };
+ });
+ };
+ test();
+</script>
+</body>
+</html>