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 ALLOW_SILENCING_NOTIFICATIONS = Services.prefs.getBoolPref( "privacy.webrtc.allowSilencingNotifications", false ); const SHOW_GLOBAL_MUTE_TOGGLES = Services.prefs.getBoolPref( "privacy.webrtc.globalMuteToggles", false ); let IsIndicatorDisabled = AppConstants.isPlatformAndVersionAtLeast("macosx", 14.0) && !Services.prefs.getBoolPref( "privacy.webrtc.showIndicatorsOnMacos14AndAbove", false ); const INDICATOR_PATH = "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(); 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.importESModule( "resource:///modules/webrtcUI.sys.mjs" ).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 && !IsIndicatorDisabled) { 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 (!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 (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 (!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; 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.window = true; } else if (browser != Ci.nsIMediaManagerService.STATE_NOCAPTURE) { result.browserwindow = true; } 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 tagName = type == "Speaker" ? "richlistbox" : "menulist"; let selectorList = document.getElementById( `webRTC-select${type}-${tagName}` ); 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.`); let itemLabel = tagName == "richlistbox" ? selectorList.selectedItem.firstElementChild.getAttribute("value") : selectorList.selectedItem.getAttribute("label"); is( label.value, itemLabel, `${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 && !IsIndicatorDisabled) { is(sharing, "screen", "showing screen icon in the identity block"); } else if (aExpected.video == STATE_CAPTURE_ENABLED && !IsIndicatorDisabled) { is(sharing, "camera", "showing camera icon in the identity block"); } else if (aExpected.audio == STATE_CAPTURE_ENABLED && !IsIndicatorDisabled) { is(sharing, "microphone", "showing mic icon in the identity block"); } else if (aExpected.video && !IsIndicatorDisabled) { is(sharing, "camera", "showing camera icon in the identity block"); } else if (aExpected.audio && !IsIndicatorDisabled) { 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 { 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; } }