summaryrefslogtreecommitdiffstats
path: root/browser/base/content/test/webrtc/browser_devices_get_user_media_grace.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/base/content/test/webrtc/browser_devices_get_user_media_grace.js')
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_grace.js388
1 files changed, 388 insertions, 0 deletions
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);
+});