summaryrefslogtreecommitdiffstats
path: root/browser/base/content/test/webrtc/head.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--browser/base/content/test/webrtc/head.js1338
1 files changed, 1338 insertions, 0 deletions
diff --git a/browser/base/content/test/webrtc/head.js b/browser/base/content/test/webrtc/head.js
new file mode 100644
index 0000000000..13526e91b6
--- /dev/null
+++ b/browser/base/content/test/webrtc/head.js
@@ -0,0 +1,1338 @@
+var { PermissionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PermissionTestUtils.sys.mjs"
+);
+
+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 => {
+ 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;
+
+async function activateSecondaryAction(aAction) {
+ let notification = PopupNotifications.panel.firstElementChild;
+ switch (aAction) {
+ case kActionNever:
+ if (notification.notification.secondaryActions.length > 1) {
+ // "Always Block" is the first (and only) item in the menupopup.
+ await Promise.all([
+ BrowserTestUtils.waitForEvent(notification.menupopup, "popupshown"),
+ notification.menubutton.click(),
+ ]);
+ notification.menupopup.querySelector("menuitem").click();
+ return;
+ }
+ 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;
+ }
+}