diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /browser/base/content/test/webrtc | |
parent | Initial commit. (diff) | |
download | firefox-43a97878ce14b72f0981164f87f2e35e14151312.tar.xz firefox-43a97878ce14b72f0981164f87f2e35e14151312.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/base/content/test/webrtc')
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> |