diff options
Diffstat (limited to 'browser/base/content/test/webrtc')
9 files changed, 1649 insertions, 0 deletions
diff --git a/browser/base/content/test/webrtc/.eslintrc.js b/browser/base/content/test/webrtc/.eslintrc.js new file mode 100644 index 000000000..7c8021192 --- /dev/null +++ b/browser/base/content/test/webrtc/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + "extends": [ + "../../../../../testing/mochitest/browser.eslintrc.js" + ] +}; diff --git a/browser/base/content/test/webrtc/browser.ini b/browser/base/content/test/webrtc/browser.ini new file mode 100644 index 000000000..8830989ad --- /dev/null +++ b/browser/base/content/test/webrtc/browser.ini @@ -0,0 +1,11 @@ +[DEFAULT] +support-files = + get_user_media.html + get_user_media_content_script.js + head.js + +[browser_devices_get_user_media.js] +skip-if = (os == "linux" && debug) # linux: bug 976544 +[browser_devices_get_user_media_anim.js] +[browser_devices_get_user_media_in_frame.js] +[browser_devices_get_user_media_tear_off_tab.js] 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 000000000..3681a810b --- /dev/null +++ b/browser/base/content/test/webrtc/browser_devices_get_user_media.js @@ -0,0 +1,554 @@ +/* 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); + +registerCleanupFunction(function() { + gBrowser.removeCurrentTab(); +}); + +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: function* checkAudioVideo() { + let promise = promisePopupNotificationShown("webRTC-shareDevices"); + yield promiseRequestDevice(true, true); + yield promise; + yield expectObserverCalled("getUserMedia:request"); + + is(PopupNotifications.getNotification("webRTC-shareDevices").anchorID, + "webRTC-shareDevices-notification-icon", "anchored to device icon"); + checkDeviceSelectors(true, true); + let iconclass = + PopupNotifications.panel.firstChild.getAttribute("iconclass"); + ok(iconclass.includes("camera-icon"), "panel using devices icon"); + + let indicator = promiseIndicatorWindow(); + yield promiseMessage("ok", () => { + PopupNotifications.panel.firstChild.button.click(); + }); + yield expectObserverCalled("getUserMedia:response:allow"); + yield expectObserverCalled("recording-device-events"); + is((yield getMediaCaptureState()), "CameraAndMicrophone", + "expected camera and microphone to be shared"); + + yield indicator; + yield checkSharingUI({audio: true, video: true}); + yield closeStream(); + } +}, + +{ + desc: "getUserMedia audio only", + run: function* checkAudioOnly() { + let promise = promisePopupNotificationShown("webRTC-shareDevices"); + yield promiseRequestDevice(true); + yield promise; + yield expectObserverCalled("getUserMedia:request"); + + is(PopupNotifications.getNotification("webRTC-shareDevices").anchorID, + "webRTC-shareMicrophone-notification-icon", "anchored to mic icon"); + checkDeviceSelectors(true); + let iconclass = + PopupNotifications.panel.firstChild.getAttribute("iconclass"); + ok(iconclass.includes("microphone-icon"), "panel using microphone icon"); + + let indicator = promiseIndicatorWindow(); + yield promiseMessage("ok", () => { + PopupNotifications.panel.firstChild.button.click(); + }); + yield expectObserverCalled("getUserMedia:response:allow"); + yield expectObserverCalled("recording-device-events"); + is((yield getMediaCaptureState()), "Microphone", + "expected microphone to be shared"); + + yield indicator; + yield checkSharingUI({audio: true}); + yield closeStream(); + } +}, + +{ + desc: "getUserMedia video only", + run: function* checkVideoOnly() { + let promise = promisePopupNotificationShown("webRTC-shareDevices"); + yield promiseRequestDevice(false, true); + yield promise; + yield expectObserverCalled("getUserMedia:request"); + + is(PopupNotifications.getNotification("webRTC-shareDevices").anchorID, + "webRTC-shareDevices-notification-icon", "anchored to device icon"); + checkDeviceSelectors(false, true); + let iconclass = + PopupNotifications.panel.firstChild.getAttribute("iconclass"); + ok(iconclass.includes("camera-icon"), "panel using devices icon"); + + let indicator = promiseIndicatorWindow(); + yield promiseMessage("ok", () => { + PopupNotifications.panel.firstChild.button.click(); + }); + yield expectObserverCalled("getUserMedia:response:allow"); + yield expectObserverCalled("recording-device-events"); + is((yield getMediaCaptureState()), "Camera", "expected camera to be shared"); + + yield indicator; + yield checkSharingUI({video: true}); + yield closeStream(); + } +}, + +{ + desc: "getUserMedia audio+video, user clicks \"Don't Share\"", + run: function* checkDontShare() { + let promise = promisePopupNotificationShown("webRTC-shareDevices"); + yield promiseRequestDevice(true, true); + yield promise; + yield expectObserverCalled("getUserMedia:request"); + checkDeviceSelectors(true, true); + + yield promiseMessage(permissionError, () => { + activateSecondaryAction(kActionDeny); + }); + + yield expectObserverCalled("getUserMedia:response:deny"); + yield expectObserverCalled("recording-window-ended"); + yield checkNotSharing(); + } +}, + +{ + desc: "getUserMedia audio+video: stop sharing", + run: function* checkStopSharing() { + let promise = promisePopupNotificationShown("webRTC-shareDevices"); + yield promiseRequestDevice(true, true); + yield promise; + yield expectObserverCalled("getUserMedia:request"); + checkDeviceSelectors(true, true); + + let indicator = promiseIndicatorWindow(); + yield promiseMessage("ok", () => { + PopupNotifications.panel.firstChild.button.click(); + }); + yield expectObserverCalled("getUserMedia:response:allow"); + yield expectObserverCalled("recording-device-events"); + is((yield getMediaCaptureState()), "CameraAndMicrophone", + "expected camera and microphone to be shared"); + + yield indicator; + yield checkSharingUI({video: true, audio: true}); + + yield stopSharing(); + + // the stream is already closed, but this will do some cleanup anyway + yield closeStream(true); + } +}, + +{ + desc: "getUserMedia audio+video: reloading the page removes all gUM UI", + run: function* checkReloading() { + let promise = promisePopupNotificationShown("webRTC-shareDevices"); + yield promiseRequestDevice(true, true); + yield promise; + yield expectObserverCalled("getUserMedia:request"); + checkDeviceSelectors(true, true); + + let indicator = promiseIndicatorWindow(); + yield promiseMessage("ok", () => { + PopupNotifications.panel.firstChild.button.click(); + }); + yield expectObserverCalled("getUserMedia:response:allow"); + yield expectObserverCalled("recording-device-events"); + is((yield getMediaCaptureState()), "CameraAndMicrophone", + "expected camera and microphone to be shared"); + + yield indicator; + yield checkSharingUI({video: true, audio: true}); + + info("reloading the web page"); + promise = promiseObserverCalled("recording-device-events"); + content.location.reload(); + yield promise; + + yield expectObserverCalled("recording-window-ended"); + yield expectNoObserverCalled(); + yield checkNotSharing(); + } +}, + +{ + desc: "getUserMedia prompt: Always/Never Share", + run: function* checkRememberCheckbox() { + let elt = id => document.getElementById(id); + + function* checkPerm(aRequestAudio, aRequestVideo, + aExpectedAudioPerm, aExpectedVideoPerm, aNever) { + let promise = promisePopupNotificationShown("webRTC-shareDevices"); + yield promiseRequestDevice(aRequestAudio, aRequestVideo); + yield promise; + yield expectObserverCalled("getUserMedia:request"); + + 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 expectedMessage = aNever ? permissionError : "ok"; + yield promiseMessage(expectedMessage, () => { + activateSecondaryAction(aNever ? kActionNever : kActionAlways); + }); + let expected = []; + if (expectedMessage == "ok") { + yield expectObserverCalled("getUserMedia:response:allow"); + yield expectObserverCalled("recording-device-events"); + if (aRequestVideo) + expected.push("Camera"); + if (aRequestAudio) + expected.push("Microphone"); + expected = expected.join("And"); + } + else { + yield expectObserverCalled("getUserMedia:response:deny"); + yield expectObserverCalled("recording-window-ended"); + expected = "none"; + } + is((yield getMediaCaptureState()), expected, + "expected " + expected + " to be shared"); + + function checkDevicePermissions(aDevice, aExpected) { + let Perms = Services.perms; + let uri = gBrowser.selectedBrowser.documentURI; + let devicePerms = Perms.testExactPermission(uri, aDevice); + if (aExpected === undefined) + is(devicePerms, Perms.UNKNOWN_ACTION, "no " + aDevice + " persistent permissions"); + else { + is(devicePerms, aExpected ? Perms.ALLOW_ACTION : Perms.DENY_ACTION, + aDevice + " persistently " + (aExpected ? "allowed" : "denied")); + } + Perms.remove(uri, aDevice); + } + checkDevicePermissions("microphone", aExpectedAudioPerm); + checkDevicePermissions("camera", aExpectedVideoPerm); + + if (expectedMessage == "ok") + yield closeStream(); + } + + // 3 cases where the user accepts the device prompt. + info("audio+video, user grants, expect both perms set to allow"); + yield checkPerm(true, true, true, true); + info("audio only, user grants, check audio perm set to allow, video perm not set"); + yield checkPerm(true, false, true, undefined); + info("video only, user grants, check video perm set to allow, audio perm not set"); + yield 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"); + yield checkPerm(true, false, false, undefined, true); + info("video only, user denies, expect video perm set to deny, audio perm not set"); + yield checkPerm(false, true, undefined, false, true); + info("audio+video, user denies, expect both perms set to deny"); + yield checkPerm(true, true, false, false, true); + } +}, + +{ + desc: "getUserMedia without prompt: use persistent permissions", + run: function* checkUsePersistentPermissions() { + function* usePerm(aAllowAudio, aAllowVideo, aRequestAudio, aRequestVideo, + aExpectStream) { + let Perms = Services.perms; + let uri = gBrowser.selectedBrowser.documentURI; + + if (aAllowAudio !== undefined) { + Perms.add(uri, "microphone", aAllowAudio ? Perms.ALLOW_ACTION + : Perms.DENY_ACTION); + } + if (aAllowVideo !== undefined) { + Perms.add(uri, "camera", aAllowVideo ? Perms.ALLOW_ACTION + : Perms.DENY_ACTION); + } + + if (aExpectStream === undefined) { + // Check that we get a prompt. + let promise = promisePopupNotificationShown("webRTC-shareDevices"); + yield promiseRequestDevice(aRequestAudio, aRequestVideo); + yield promise; + yield expectObserverCalled("getUserMedia:request"); + + // Deny the request to cleanup... + yield promiseMessage(permissionError, () => { + activateSecondaryAction(kActionDeny); + }); + yield expectObserverCalled("getUserMedia:response:deny"); + yield expectObserverCalled("recording-window-ended"); + } + else { + let expectedMessage = aExpectStream ? "ok" : permissionError; + let promise = promiseMessage(expectedMessage); + yield promiseRequestDevice(aRequestAudio, aRequestVideo); + yield promise; + + if (expectedMessage == "ok") { + yield expectObserverCalled("getUserMedia:request"); + yield promiseNoPopupNotification("webRTC-shareDevices"); + yield expectObserverCalled("getUserMedia:response:allow"); + yield expectObserverCalled("recording-device-events"); + + // Check what's actually shared. + let expected = []; + if (aAllowVideo && aRequestVideo) + expected.push("Camera"); + if (aAllowAudio && aRequestAudio) + expected.push("Microphone"); + expected = expected.join("And"); + is((yield getMediaCaptureState()), expected, + "expected " + expected + " to be shared"); + + yield closeStream(); + } + else { + yield expectObserverCalled("recording-window-ended"); + } + } + + Perms.remove(uri, "camera"); + Perms.remove(uri, "microphone"); + } + + // Set both permissions identically + info("allow audio+video, request audio+video, expect ok (audio+video)"); + yield usePerm(true, true, true, true, true); + info("deny audio+video, request audio+video, expect denied"); + yield usePerm(false, false, true, true, false); + + // Allow audio, deny video. + info("allow audio, deny video, request audio+video, expect denied"); + yield usePerm(true, false, true, true, false); + info("allow audio, deny video, request audio, expect ok (audio)"); + yield usePerm(true, false, true, false, true); + info("allow audio, deny video, request video, expect denied"); + yield usePerm(true, false, false, true, false); + + // Deny audio, allow video. + info("deny audio, allow video, request audio+video, expect denied"); + yield usePerm(false, true, true, true, false); + info("deny audio, allow video, request audio, expect denied"); + yield usePerm(false, true, true, false, false); + info("deny audio, allow video, request video, expect ok (video)"); + yield usePerm(false, true, false, true, true); + + // Allow audio, video not set. + info("allow audio, request audio+video, expect prompt"); + yield usePerm(true, undefined, true, true, undefined); + info("allow audio, request audio, expect ok (audio)"); + yield usePerm(true, undefined, true, false, true); + info("allow audio, request video, expect prompt"); + yield usePerm(true, undefined, false, true, undefined); + + // Deny audio, video not set. + info("deny audio, request audio+video, expect denied"); + yield usePerm(false, undefined, true, true, false); + info("deny audio, request audio, expect denied"); + yield usePerm(false, undefined, true, false, false); + info("deny audio, request video, expect prompt"); + yield usePerm(false, undefined, false, true, undefined); + + // Allow video, audio not set. + info("allow video, request audio+video, expect prompt"); + yield usePerm(undefined, true, true, true, undefined); + info("allow video, request audio, expect prompt"); + yield usePerm(undefined, true, true, false, undefined); + info("allow video, request video, expect ok (video)"); + yield usePerm(undefined, true, false, true, true); + + // Deny video, audio not set. + info("deny video, request audio+video, expect denied"); + yield usePerm(undefined, false, true, true, false); + info("deny video, request audio, expect prompt"); + yield usePerm(undefined, false, true, false, undefined); + info("deny video, request video, expect denied"); + yield usePerm(undefined, false, false, true, false); + } +}, + +{ + desc: "Stop Sharing removes persistent permissions", + run: function* checkStopSharingRemovesPersistentPermissions() { + function* stopAndCheckPerm(aRequestAudio, aRequestVideo) { + let Perms = Services.perms; + let uri = gBrowser.selectedBrowser.documentURI; + + // Initially set both permissions to 'allow'. + Perms.add(uri, "microphone", Perms.ALLOW_ACTION); + Perms.add(uri, "camera", Perms.ALLOW_ACTION); + + let indicator = promiseIndicatorWindow(); + // Start sharing what's been requested. + let promise = promiseMessage("ok"); + yield promiseRequestDevice(aRequestAudio, aRequestVideo); + yield promise; + + yield expectObserverCalled("getUserMedia:request"); + yield expectObserverCalled("getUserMedia:response:allow"); + yield expectObserverCalled("recording-device-events"); + yield indicator; + yield checkSharingUI({video: aRequestVideo, audio: aRequestAudio}); + + yield stopSharing(aRequestVideo ? "camera" : "microphone"); + + // Check that permissions have been removed as expected. + let audioPerm = Perms.testExactPermission(uri, "microphone"); + if (aRequestAudio) + is(audioPerm, Perms.UNKNOWN_ACTION, "microphone permissions removed"); + else + is(audioPerm, Perms.ALLOW_ACTION, "microphone permissions untouched"); + + let videoPerm = Perms.testExactPermission(uri, "camera"); + if (aRequestVideo) + is(videoPerm, Perms.UNKNOWN_ACTION, "camera permissions removed"); + else + is(videoPerm, Perms.ALLOW_ACTION, "camera permissions untouched"); + + // Cleanup. + yield closeStream(true); + + Perms.remove(uri, "camera"); + Perms.remove(uri, "microphone"); + } + + info("request audio+video, stop sharing resets both"); + yield stopAndCheckPerm(true, true); + info("request audio, stop sharing resets audio only"); + yield stopAndCheckPerm(true, false); + info("request video, stop sharing resets video only"); + yield stopAndCheckPerm(false, true); + } +}, + +{ + desc: "test showControlCenter", + run: function* checkShowControlCenter() { + let promise = promisePopupNotificationShown("webRTC-shareDevices"); + yield promiseRequestDevice(false, true); + yield promise; + yield expectObserverCalled("getUserMedia:request"); + checkDeviceSelectors(false, true); + + let indicator = promiseIndicatorWindow(); + yield promiseMessage("ok", () => { + PopupNotifications.panel.firstChild.button.click(); + }); + yield expectObserverCalled("getUserMedia:response:allow"); + yield expectObserverCalled("recording-device-events"); + is((yield getMediaCaptureState()), "Camera", "expected camera to be shared"); + + yield indicator; + yield checkSharingUI({video: true}); + + ok(gIdentityHandler._identityPopup.hidden, "control center should be hidden"); + if ("nsISystemStatusBar" in Ci) { + let activeStreams = webrtcUI.getActiveStreams(true, false, false); + webrtcUI.showSharingDoorhanger(activeStreams[0], "Devices"); + } + else { + let win = + Services.wm.getMostRecentWindow("Browser:WebRTCGlobalIndicator"); + let elt = win.document.getElementById("audioVideoButton"); + EventUtils.synthesizeMouseAtCenter(elt, {}, win); + yield promiseWaitForCondition(() => !gIdentityHandler._identityPopup.hidden); + } + ok(!gIdentityHandler._identityPopup.hidden, "control center should be open"); + + gIdentityHandler._identityPopup.hidden = true; + yield expectNoObserverCalled(); + + yield closeStream(); + } +}, + +{ + desc: "'Always Allow' ignored and not shown on http pages", + run: function* checkNoAlwaysOnHttp() { + // Load an http page instead of the https version. + let browser = gBrowser.selectedBrowser; + browser.loadURI(browser.documentURI.spec.replace("https://", "http://")); + yield BrowserTestUtils.browserLoaded(browser); + + // Initially set both permissions to 'allow'. + let Perms = Services.perms; + let uri = browser.documentURI; + Perms.add(uri, "microphone", Perms.ALLOW_ACTION); + Perms.add(uri, "camera", Perms.ALLOW_ACTION); + + // Request devices and expect a prompt despite the saved 'Allow' permission, + // because the connection isn't secure. + let promise = promisePopupNotificationShown("webRTC-shareDevices"); + yield promiseRequestDevice(true, true); + yield promise; + yield expectObserverCalled("getUserMedia:request"); + + // Ensure that the 'Always Allow' action isn't shown. + let alwaysLabel = gNavigatorBundle.getString("getUserMedia.always.label"); + ok(!!alwaysLabel, "found the 'Always Allow' localized label"); + let labels = []; + let notification = PopupNotifications.panel.firstChild; + for (let node of notification.childNodes) { + if (node.localName == "menuitem") + labels.push(node.getAttribute("label")); + } + is(labels.indexOf(alwaysLabel), -1, "The 'Always Allow' item isn't shown"); + + // Cleanup. + yield closeStream(true); + Perms.remove(uri, "camera"); + Perms.remove(uri, "microphone"); + } +} + +]; + +function test() { + waitForExplicitFinish(); + + let tab = gBrowser.addTab(); + gBrowser.selectedTab = tab; + let browser = tab.linkedBrowser; + + browser.messageManager.loadFrameScript(CONTENT_SCRIPT_HELPER, true); + + browser.addEventListener("load", function onload() { + browser.removeEventListener("load", onload, true); + + is(PopupNotifications._currentNotifications.length, 0, + "should start the test without any prior popup notification"); + ok(gIdentityHandler._identityPopup.hidden, + "should start the test with the control center hidden"); + + Task.spawn(function* () { + yield SpecialPowers.pushPrefEnv({"set": [[PREF_PERMISSION_FAKE, true]]}); + + for (let test of gTests) { + info(test.desc); + yield test.run(); + + // Cleanup before the next test + yield expectNoObserverCalled(); + } + }).then(finish, ex => { + Cu.reportError(ex); + ok(false, "Unexpected Exception: " + ex); + finish(); + }); + }, true); + let rootDir = getRootDirectory(gTestPath); + rootDir = rootDir.replace("chrome://mochitests/content/", + "https://example.com/"); + content.location = rootDir + "get_user_media.html"; +} 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 000000000..f407061a7 --- /dev/null +++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_anim.js @@ -0,0 +1,109 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +registerCleanupFunction(function() { + gBrowser.removeCurrentTab(); +}); + +var gTests = [ + +{ + desc: "device sharing animation on background tabs", + run: function* checkAudioVideo() { + function* getStreamAndCheckBackgroundAnim(aAudio, aVideo, aSharing) { + // Get a stream + let popupPromise = promisePopupNotificationShown("webRTC-shareDevices"); + yield promiseRequestDevice(aAudio, aVideo); + yield popupPromise; + yield expectObserverCalled("getUserMedia:request"); + + yield promiseMessage("ok", () => { + PopupNotifications.panel.firstChild.button.click(); + }); + yield expectObserverCalled("getUserMedia:response:allow"); + yield expectObserverCalled("recording-device-events"); + let expected = []; + if (aVideo) + expected.push("Camera"); + if (aAudio) + expected.push("Microphone"); + is((yield getMediaCaptureState()), expected.join("And"), + "expected stream 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 = + document.getAnonymousElementByAttribute(tab, "anonid", "sharing-icon"); + 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. + yield BrowserTestUtils.switchTab(gBrowser, gBrowser.addTab()); + 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. + yield 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. + yield closeStream(); + + // TODO(Bug 1304997): Fix the race in closeStream() and remove this + // promiseWaitForCondition(). + yield promiseWaitForCondition(() => !tab.getAttribute("sharing")); + is(tab.getAttribute("sharing"), "", + "the tab no longer has the 'sharing' attribute after closing the stream"); + } + + yield getStreamAndCheckBackgroundAnim(true, true, "camera"); + yield getStreamAndCheckBackgroundAnim(false, true, "camera"); + yield getStreamAndCheckBackgroundAnim(true, false, "microphone"); + } +} + +]; + +function test() { + waitForExplicitFinish(); + + let tab = gBrowser.addTab(); + gBrowser.selectedTab = tab; + let browser = tab.linkedBrowser; + + browser.messageManager.loadFrameScript(CONTENT_SCRIPT_HELPER, true); + + browser.addEventListener("load", function onload() { + browser.removeEventListener("load", onload, true); + + is(PopupNotifications._currentNotifications.length, 0, + "should start the test without any prior popup notification"); + + Task.spawn(function* () { + yield SpecialPowers.pushPrefEnv({"set": [[PREF_PERMISSION_FAKE, true]]}); + + for (let test of gTests) { + info(test.desc); + yield test.run(); + } + }).then(finish, ex => { + Cu.reportError(ex); + ok(false, "Unexpected Exception: " + ex); + finish(); + }); + }, true); + let rootDir = getRootDirectory(gTestPath); + rootDir = rootDir.replace("chrome://mochitests/content/", + "https://example.com/"); + content.location = rootDir + "get_user_media.html"; +} 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 000000000..01a544aae --- /dev/null +++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_in_frame.js @@ -0,0 +1,266 @@ +/* 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/. */ + +registerCleanupFunction(function() { + gBrowser.removeCurrentTab(); +}); + +function promiseReloadFrame(aFrameId) { + return ContentTask.spawn(gBrowser.selectedBrowser, aFrameId, function*(aFrameId) { + content.wrappedJSObject.document.getElementById(aFrameId).contentWindow.location.reload(); + }); +} + +var gTests = [ + +{ + desc: "getUserMedia audio+video", + run: function* checkAudioVideo() { + let promise = promisePopupNotificationShown("webRTC-shareDevices"); + yield promiseRequestDevice(true, true, "frame1"); + yield promise; + yield expectObserverCalled("getUserMedia:request"); + + is(PopupNotifications.getNotification("webRTC-shareDevices").anchorID, + "webRTC-shareDevices-notification-icon", "anchored to device icon"); + checkDeviceSelectors(true, true); + is(PopupNotifications.panel.firstChild.getAttribute("popupid"), + "webRTC-shareDevices", "panel using devices icon"); + + let indicator = promiseIndicatorWindow(); + yield promiseMessage("ok", () => { + PopupNotifications.panel.firstChild.button.click(); + }); + yield expectObserverCalled("getUserMedia:response:allow"); + yield expectObserverCalled("recording-device-events"); + is((yield getMediaCaptureState()), "CameraAndMicrophone", + "expected camera and microphone to be shared"); + + yield indicator; + yield checkSharingUI({audio: true, video: true}); + yield closeStream(false, "frame1"); + } +}, + +{ + desc: "getUserMedia audio+video: stop sharing", + run: function* checkStopSharing() { + let promise = promisePopupNotificationShown("webRTC-shareDevices"); + yield promiseRequestDevice(true, true, "frame1"); + yield promise; + yield expectObserverCalled("getUserMedia:request"); + checkDeviceSelectors(true, true); + + let indicator = promiseIndicatorWindow(); + yield promiseMessage("ok", () => { + activateSecondaryAction(kActionAlways); + }); + yield expectObserverCalled("getUserMedia:response:allow"); + yield expectObserverCalled("recording-device-events"); + is((yield getMediaCaptureState()), "CameraAndMicrophone", + "expected camera and microphone to be shared"); + + yield indicator; + yield checkSharingUI({video: true, audio: true}); + + let Perms = Services.perms; + let uri = Services.io.newURI("https://example.com/", null, null); + is(Perms.testExactPermission(uri, "microphone"), Perms.ALLOW_ACTION, + "microphone persistently allowed"); + is(Perms.testExactPermission(uri, "camera"), Perms.ALLOW_ACTION, + "camera persistently allowed"); + + yield stopSharing(); + + // The persistent permissions for the frame should have been removed. + is(Perms.testExactPermission(uri, "microphone"), Perms.UNKNOWN_ACTION, + "microphone not persistently allowed"); + is(Perms.testExactPermission(uri, "camera"), Perms.UNKNOWN_ACTION, + "camera not persistently allowed"); + + // the stream is already closed, but this will do some cleanup anyway + yield closeStream(true, "frame1"); + } +}, + +{ + desc: "getUserMedia audio+video: reloading the frame removes all sharing UI", + run: function* checkReloading() { + let promise = promisePopupNotificationShown("webRTC-shareDevices"); + yield promiseRequestDevice(true, true, "frame1"); + yield promise; + yield expectObserverCalled("getUserMedia:request"); + checkDeviceSelectors(true, true); + + let indicator = promiseIndicatorWindow(); + yield promiseMessage("ok", () => { + PopupNotifications.panel.firstChild.button.click(); + }); + yield expectObserverCalled("getUserMedia:response:allow"); + yield expectObserverCalled("recording-device-events"); + is((yield getMediaCaptureState()), "CameraAndMicrophone", + "expected camera and microphone to be shared"); + + yield indicator; + yield checkSharingUI({video: true, audio: true}); + + info("reloading the frame"); + promise = promiseObserverCalled("recording-device-events"); + yield promiseReloadFrame("frame1"); + yield promise; + + yield expectObserverCalled("recording-window-ended"); + yield expectNoObserverCalled(); + yield checkNotSharing(); + } +}, + +{ + desc: "getUserMedia audio+video: reloading the frame removes prompts", + run: function* checkReloadingRemovesPrompts() { + let promise = promisePopupNotificationShown("webRTC-shareDevices"); + yield promiseRequestDevice(true, true, "frame1"); + yield promise; + yield expectObserverCalled("getUserMedia:request"); + checkDeviceSelectors(true, true); + + info("reloading the frame"); + promise = promiseObserverCalled("recording-window-ended"); + yield promiseReloadFrame("frame1"); + yield promise; + yield promiseNoPopupNotification("webRTC-shareDevices"); + + yield expectNoObserverCalled(); + yield checkNotSharing(); + } +}, + +{ + desc: "getUserMedia audio+video: reloading a frame updates the sharing UI", + run: function* checkUpdateWhenReloading() { + // We'll share only the mic 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 promise = promisePopupNotificationShown("webRTC-shareDevices"); + yield promiseRequestDevice(true, false, "frame1"); + yield promise; + yield expectObserverCalled("getUserMedia:request"); + checkDeviceSelectors(true, false); + + let indicator = promiseIndicatorWindow(); + yield promiseMessage("ok", () => { + PopupNotifications.panel.firstChild.button.click(); + }); + yield expectObserverCalled("getUserMedia:response:allow"); + yield expectObserverCalled("recording-device-events"); + is((yield getMediaCaptureState()), "Microphone", "microphone to be shared"); + + yield indicator; + yield checkSharingUI({video: false, audio: true}); + yield expectNoObserverCalled(); + + promise = promisePopupNotificationShown("webRTC-shareDevices"); + yield promiseRequestDevice(true, true, "frame2"); + yield promise; + yield expectObserverCalled("getUserMedia:request"); + checkDeviceSelectors(true, true); + + yield promiseMessage("ok", () => { + PopupNotifications.panel.firstChild.button.click(); + }); + yield expectObserverCalled("getUserMedia:response:allow"); + yield expectObserverCalled("recording-device-events"); + is((yield getMediaCaptureState()), "CameraAndMicrophone", + "expected camera and microphone to be shared"); + + yield checkSharingUI({video: true, audio: true}); + yield expectNoObserverCalled(); + + info("reloading the second frame"); + promise = promiseObserverCalled("recording-device-events"); + yield promiseReloadFrame("frame2"); + yield promise; + + yield expectObserverCalled("recording-window-ended"); + yield checkSharingUI({video: false, audio: true}); + yield expectNoObserverCalled(); + + yield closeStream(false, "frame1"); + yield expectNoObserverCalled(); + yield checkNotSharing(); + } +}, + +{ + desc: "getUserMedia audio+video: reloading the top level page removes all sharing UI", + run: function* checkReloading() { + let promise = promisePopupNotificationShown("webRTC-shareDevices"); + yield promiseRequestDevice(true, true, "frame1"); + yield promise; + yield expectObserverCalled("getUserMedia:request"); + checkDeviceSelectors(true, true); + + let indicator = promiseIndicatorWindow(); + yield promiseMessage("ok", () => { + PopupNotifications.panel.firstChild.button.click(); + }); + yield expectObserverCalled("getUserMedia:response:allow"); + yield expectObserverCalled("recording-device-events"); + is((yield getMediaCaptureState()), "CameraAndMicrophone", + "expected camera and microphone to be shared"); + + yield indicator; + yield checkSharingUI({video: true, audio: true}); + + info("reloading the web page"); + promise = promiseObserverCalled("recording-device-events"); + content.location.reload(); + yield promise; + + yield expectObserverCalled("recording-window-ended"); + yield expectNoObserverCalled(); + yield checkNotSharing(); + } +} + +]; + +function test() { + waitForExplicitFinish(); + + let tab = gBrowser.addTab(); + gBrowser.selectedTab = tab; + let browser = tab.linkedBrowser; + + browser.messageManager.loadFrameScript(CONTENT_SCRIPT_HELPER, true); + + browser.addEventListener("load", function onload() { + browser.removeEventListener("load", onload, true); + + is(PopupNotifications._currentNotifications.length, 0, + "should start the test without any prior popup notification"); + + Task.spawn(function* () { + yield SpecialPowers.pushPrefEnv({"set": [[PREF_PERMISSION_FAKE, true]]}); + + for (let test of gTests) { + info(test.desc); + yield test.run(); + + // Cleanup before the next test + yield expectNoObserverCalled(); + } + }).then(finish, ex => { + Cu.reportError(ex); + ok(false, "Unexpected Exception: " + ex); + finish(); + }); + }, true); + let rootDir = getRootDirectory(gTestPath); + rootDir = rootDir.replace("chrome://mochitests/content/", + "https://example.com/"); + let url = rootDir + "get_user_media.html"; + content.location = 'data:text/html,<iframe id="frame1" src="' + url + '"></iframe><iframe id="frame2" src="' + url + '"></iframe>' +} 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 000000000..b19065371 --- /dev/null +++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_tear_off_tab.js @@ -0,0 +1,109 @@ +/* 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/. */ + +registerCleanupFunction(function() { + gBrowser.removeCurrentTab(); +}); + +var gTests = [ + +{ + desc: "getUserMedia: tearing-off a tab keeps sharing indicators", + run: function* checkTearingOff() { + let promise = promisePopupNotificationShown("webRTC-shareDevices"); + yield promiseRequestDevice(true, true); + yield promise; + yield expectObserverCalled("getUserMedia:request"); + checkDeviceSelectors(true, true); + + let indicator = promiseIndicatorWindow(); + yield promiseMessage("ok", () => { + PopupNotifications.panel.firstChild.button.click(); + }); + yield expectObserverCalled("getUserMedia:response:allow"); + yield expectObserverCalled("recording-device-events"); + is((yield getMediaCaptureState()), "CameraAndMicrophone", + "expected camera and microphone to be shared"); + + yield indicator; + yield checkSharingUI({video: true, audio: true}); + + info("tearing off the tab"); + let win = gBrowser.replaceTabWithWindow(gBrowser.selectedTab); + yield whenDelayedStartupFinished(win); + yield checkSharingUI({audio: true, video: true}, win); + + // Clicking the global sharing indicator should open the control center in + // the second window. + ok(win.gIdentityHandler._identityPopup.hidden, "control center should be hidden"); + let activeStreams = webrtcUI.getActiveStreams(true, false, false); + webrtcUI.showSharingDoorhanger(activeStreams[0], "Devices"); + ok(!win.gIdentityHandler._identityPopup.hidden, + "control center should be open in the second window"); + ok(gIdentityHandler._identityPopup.hidden, + "control center should be hidden in the first window"); + win.gIdentityHandler._identityPopup.hidden = true; + + // Closing the new window should remove all sharing indicators. + // We need to load the content script in the first window so that we can + // catch the notifications fired globally when closing the second window. + gBrowser.selectedBrowser.messageManager.loadFrameScript(CONTENT_SCRIPT_HELPER, true); + + let promises = [promiseObserverCalled("recording-device-events"), + promiseObserverCalled("recording-window-ended")]; + yield BrowserTestUtils.closeWindow(win); + yield Promise.all(promises); + + yield expectNoObserverCalled(); + yield checkNotSharing(); + } +} + +]; + +function test() { + waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [["dom.ipc.processCount", 1]]}, runTest); +} + +function runTest() { + // An empty tab where we can load the content script without leaving it + // behind at the end of the test. + gBrowser.addTab(); + + let tab = gBrowser.addTab(); + gBrowser.selectedTab = tab; + let browser = tab.linkedBrowser; + + browser.messageManager.loadFrameScript(CONTENT_SCRIPT_HELPER, true); + + browser.addEventListener("load", function onload() { + browser.removeEventListener("load", onload, true); + + is(PopupNotifications._currentNotifications.length, 0, + "should start the test without any prior popup notification"); + ok(gIdentityHandler._identityPopup.hidden, + "should start the test with the control center hidden"); + + Task.spawn(function* () { + yield SpecialPowers.pushPrefEnv({"set": [[PREF_PERMISSION_FAKE, true]]}); + + for (let test of gTests) { + info(test.desc); + yield test.run(); + + // Cleanup before the next test + yield expectNoObserverCalled(); + } + }).then(finish, ex => { + Cu.reportError(ex); + ok(false, "Unexpected Exception: " + ex); + finish(); + }); + }, true); + let rootDir = getRootDirectory(gTestPath); + rootDir = rootDir.replace("chrome://mochitests/content/", + "https://example.com/"); + content.location = rootDir + "get_user_media.html"; +} 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 000000000..16303c62d --- /dev/null +++ b/browser/base/content/test/webrtc/get_user_media.html @@ -0,0 +1,55 @@ +<!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) { + document.getElementById("message").innerHTML = m; + window.parent.postMessage(m, "*"); +} + +var gStream; + +function requestDevice(aAudio, aVideo, aShare) { + var opts = {video: aVideo, audio: aAudio}; + if (aShare) { + opts.video = { + mozMediaSource: aShare, + mediaSource: aShare + } + } else if (useFakeStreams) { + opts.fake = true; + } + + window.navigator.mediaDevices.getUserMedia(opts) + .then(stream => { + gStream = stream; + message("ok"); + }, err => message("error: " + err)); +} +message("pending"); + +function closeStream() { + if (!gStream) + return; + gStream.getTracks().forEach(t => t.stop()); + gStream = null; + message("closed"); +} +</script> +</body> +</html> diff --git a/browser/base/content/test/webrtc/get_user_media_content_script.js b/browser/base/content/test/webrtc/get_user_media_content_script.js new file mode 100644 index 000000000..71b68d826 --- /dev/null +++ b/browser/base/content/test/webrtc/get_user_media_content_script.js @@ -0,0 +1,85 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +XPCOMUtils.defineLazyServiceGetter(this, "MediaManagerService", + "@mozilla.org/mediaManagerService;1", + "nsIMediaManagerService"); + +const kObservedTopics = [ + "getUserMedia:response:allow", + "getUserMedia:revoke", + "getUserMedia:response:deny", + "getUserMedia:request", + "recording-device-events", + "recording-window-ended" +]; + +var gObservedTopics = {}; +function observer(aSubject, aTopic, aData) { + if (!(aTopic in gObservedTopics)) + gObservedTopics[aTopic] = 1; + else + ++gObservedTopics[aTopic]; +} + +kObservedTopics.forEach(topic => { + Services.obs.addObserver(observer, topic, false); +}); + +addMessageListener("Test:ExpectObserverCalled", ({data}) => { + sendAsyncMessage("Test:ExpectObserverCalled:Reply", + {count: gObservedTopics[data]}); + if (data in gObservedTopics) + --gObservedTopics[data]; +}); + +addMessageListener("Test:ExpectNoObserverCalled", data => { + sendAsyncMessage("Test:ExpectNoObserverCalled:Reply", gObservedTopics); + gObservedTopics = {}; +}); + +function _getMediaCaptureState() { + let hasVideo = {}; + let hasAudio = {}; + let hasScreenShare = {}; + let hasWindowShare = {}; + MediaManagerService.mediaCaptureWindowState(content, hasVideo, hasAudio, + hasScreenShare, hasWindowShare); + if (hasVideo.value && hasAudio.value) + return "CameraAndMicrophone"; + if (hasVideo.value) + return "Camera"; + if (hasAudio.value) + return "Microphone"; + if (hasScreenShare.value) + return "Screen"; + if (hasWindowShare.value) + return "Window"; + return "none"; +} + +addMessageListener("Test:GetMediaCaptureState", data => { + sendAsyncMessage("Test:MediaCaptureState", _getMediaCaptureState()); +}); + +addMessageListener("Test:WaitForObserverCall", ({data}) => { + let topic = data; + Services.obs.addObserver(function observer() { + sendAsyncMessage("Test:ObserverCalled", topic); + Services.obs.removeObserver(observer, topic); + + if (kObservedTopics.indexOf(topic) != -1) { + if (!(topic in gObservedTopics)) + gObservedTopics[topic] = -1; + else + --gObservedTopics[topic]; + } + }, topic, false); +}); + +addMessageListener("Test:WaitForMessage", () => { + content.addEventListener("message", ({data}) => { + sendAsyncMessage("Test:MessageReceived", data); + }, {once: true}); +}); diff --git a/browser/base/content/test/webrtc/head.js b/browser/base/content/test/webrtc/head.js new file mode 100644 index 000000000..70b183773 --- /dev/null +++ b/browser/base/content/test/webrtc/head.js @@ -0,0 +1,453 @@ +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "Promise", + "resource://gre/modules/Promise.jsm"); + +const PREF_PERMISSION_FAKE = "media.navigator.permission.fake"; +const CONTENT_SCRIPT_HELPER = getRootDirectory(gTestPath) + "get_user_media_content_script.js"; + +function waitForCondition(condition, nextTest, errorMsg, retryTimes) { + retryTimes = typeof retryTimes !== 'undefined' ? retryTimes : 30; + var tries = 0; + var interval = setInterval(function() { + if (tries >= retryTimes) { + ok(false, errorMsg); + moveOn(); + } + var conditionPassed; + try { + conditionPassed = condition(); + } catch (e) { + ok(false, e + "\n" + e.stack); + conditionPassed = false; + } + if (conditionPassed) { + moveOn(); + } + tries++; + }, 100); + var moveOn = function() { clearInterval(interval); nextTest(); }; +} + +function promiseWaitForCondition(aConditionFn) { + let deferred = Promise.defer(); + waitForCondition(aConditionFn, deferred.resolve, "Condition didn't pass."); + return deferred.promise; +} + +/** + * Waits for a window with the given URL to exist. + * + * @param url + * The url of the window. + * @return {Promise} resolved when the window exists. + * @resolves to the window + */ +function promiseWindow(url) { + info("expecting a " + url + " window"); + return new Promise(resolve => { + Services.obs.addObserver(function obs(win) { + win.QueryInterface(Ci.nsIDOMWindow); + win.addEventListener("load", function loadHandler() { + win.removeEventListener("load", loadHandler); + + if (win.location.href !== url) { + info("ignoring a window with this url: " + win.location.href); + return; + } + + Services.obs.removeObserver(obs, "domwindowopened"); + resolve(win); + }); + }, "domwindowopened", false); + }); +} + +function whenDelayedStartupFinished(aWindow) { + return new Promise(resolve => { + info("Waiting for delayed startup to finish"); + Services.obs.addObserver(function observer(aSubject, aTopic) { + if (aWindow == aSubject) { + Services.obs.removeObserver(observer, aTopic); + resolve(); + } + }, "browser-delayed-startup-finished", false); + }); +} + +function promiseIndicatorWindow() { + // We don't show the indicator window on Mac. + if ("nsISystemStatusBar" in Ci) + return Promise.resolve(); + + return promiseWindow("chrome://browser/content/webrtcIndicator.xul"); +} + +function* assertWebRTCIndicatorStatus(expected) { + let ui = Cu.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. + info("waiting for the global indicator to be hidden"); + yield promiseWaitForCondition(() => !ui.showGlobalIndicator); + } + is(ui.showGlobalIndicator, !!expected, msg); + + let expectVideo = false, expectAudio = false, expectScreen = false; + if (expected) { + if (expected.video) + expectVideo = true; + if (expected.audio) + expectAudio = true; + if (expected.screen) + expectScreen = true; + } + is(ui.showCameraIndicator, expectVideo, "camera global indicator as expected"); + is(ui.showMicrophoneIndicator, expectAudio, "microphone global indicator as expected"); + is(ui.showScreenSharingIndicator, expectScreen, "screen global indicator as expected"); + + let windows = Services.wm.getEnumerator("navigator:browser"); + while (windows.hasMoreElements()) { + let win = windows.getNext(); + let menu = win.document.getElementById("tabSharingMenu"); + is(menu && !menu.hidden, !!expected, "WebRTC menu should be " + expectedState); + } + + if (!("nsISystemStatusBar" in Ci)) { + if (!expected) { + let win = Services.wm.getMostRecentWindow("Browser:WebRTCGlobalIndicator"); + if (win) { + yield new Promise((resolve, reject) => { + win.addEventListener("unload", function listener(e) { + if (e.target == win.document) { + win.removeEventListener("unload", listener); + resolve(); + } + }, false); + }); + } + } + 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"); + let deferred = Promise.defer(); + document.addEventListener("readystatechange", + function onReadyStateChange() { + if (document.readyState != "complete") + return; + document.removeEventListener("readystatechange", onReadyStateChange); + deferred.resolve(); + }); + yield deferred.promise; + } + + for (let item of ["video", "audio", "screen"]) { + let expectedValue = (expected && expected[item]) ? "true" : ""; + is(docElt.getAttribute("sharing" + item), expectedValue, + item + " global indicator attribute as expected"); + } + + ok(!indicator.hasMoreElements(), "only one global indicator window"); + } + } +} + +function promisePopupEvent(popup, eventSuffix) { + let endState = {shown: "open", hidden: "closed"}[eventSuffix]; + + if (popup.state == endState) + return Promise.resolve(); + + let eventType = "popup" + eventSuffix; + let deferred = Promise.defer(); + popup.addEventListener(eventType, function onPopupShown(event) { + popup.removeEventListener(eventType, onPopupShown); + deferred.resolve(); + }); + + return deferred.promise; +} + +function promiseNotificationShown(notification) { + let win = notification.browser.ownerGlobal; + if (win.PopupNotifications.panel.state == "open") { + return Promise.resolve(); + } + let panelPromise = promisePopupEvent(win.PopupNotifications.panel, "shown"); + notification.reshow(); + return panelPromise; +} + +function _mm() { + return gBrowser.selectedBrowser.messageManager; +} + +function promiseObserverCalled(aTopic) { + return new Promise(resolve => { + let mm = _mm(); + mm.addMessageListener("Test:ObserverCalled", function listener({data}) { + if (data == aTopic) { + ok(true, "got " + aTopic + " notification"); + mm.removeMessageListener("Test:ObserverCalled", listener); + resolve(); + } + }); + mm.sendAsyncMessage("Test:WaitForObserverCall", aTopic); + }); +} + +function expectObserverCalled(aTopic) { + return new Promise(resolve => { + let mm = _mm(); + mm.addMessageListener("Test:ExpectObserverCalled:Reply", + function listener({data}) { + is(data.count, 1, "expected notification " + aTopic); + mm.removeMessageListener("Test:ExpectObserverCalled:Reply", listener); + resolve(); + }); + mm.sendAsyncMessage("Test:ExpectObserverCalled", aTopic); + }); +} + +function expectNoObserverCalled() { + return new Promise(resolve => { + let mm = _mm(); + mm.addMessageListener("Test:ExpectNoObserverCalled:Reply", + function listener({data}) { + mm.removeMessageListener("Test:ExpectNoObserverCalled:Reply", listener); + for (let topic in data) { + if (data[topic]) + is(data[topic], 0, topic + " notification unexpected"); + } + resolve(); + }); + mm.sendAsyncMessage("Test:ExpectNoObserverCalled"); + }); +} + +function promiseMessage(aMessage, aAction) { + let promise = new Promise((resolve, reject) => { + let mm = _mm(); + mm.addMessageListener("Test:MessageReceived", function listener({data}) { + is(data, aMessage, "received " + aMessage); + if (data == aMessage) + resolve(); + else + reject(); + mm.removeMessageListener("Test:MessageReceived", listener); + }); + mm.sendAsyncMessage("Test:WaitForMessage"); + }); + + if (aAction) + aAction(); + + return promise; +} + +function promisePopupNotificationShown(aName, aAction) { + let deferred = Promise.defer(); + + PopupNotifications.panel.addEventListener("popupshown", function popupNotifShown() { + PopupNotifications.panel.removeEventListener("popupshown", popupNotifShown); + + ok(!!PopupNotifications.getNotification(aName), aName + " notification shown"); + ok(PopupNotifications.isPanelOpen, "notification panel open"); + ok(!!PopupNotifications.panel.firstChild, "notification panel populated"); + + deferred.resolve(); + }); + + if (aAction) + aAction(); + + return deferred.promise; +} + +function promisePopupNotification(aName) { + let deferred = Promise.defer(); + + waitForCondition(() => PopupNotifications.getNotification(aName), + () => { + ok(!!PopupNotifications.getNotification(aName), + aName + " notification appeared"); + + deferred.resolve(); + }, "timeout waiting for popup notification " + aName); + + return deferred.promise; +} + +function promiseNoPopupNotification(aName) { + let deferred = Promise.defer(); + + waitForCondition(() => !PopupNotifications.getNotification(aName), + () => { + ok(!PopupNotifications.getNotification(aName), + aName + " notification removed"); + deferred.resolve(); + }, "timeout waiting for popup notification " + aName + " to disappear"); + + return deferred.promise; +} + +const kActionAlways = 1; +const kActionDeny = 2; +const kActionNever = 3; + +function activateSecondaryAction(aAction) { + let notification = PopupNotifications.panel.firstChild; + notification.button.focus(); + let popup = notification.menupopup; + popup.addEventListener("popupshown", function () { + popup.removeEventListener("popupshown", arguments.callee, false); + + // Press 'down' as many time as needed to select the requested action. + while (aAction--) + EventUtils.synthesizeKey("VK_DOWN", {}); + + // Activate + EventUtils.synthesizeKey("VK_RETURN", {}); + }, false); + + // One down event to open the popup + EventUtils.synthesizeKey("VK_DOWN", + { altKey: !navigator.platform.includes("Mac") }); +} + +function getMediaCaptureState() { + return new Promise(resolve => { + let mm = _mm(); + mm.addMessageListener("Test:MediaCaptureState", ({data}) => { + resolve(data); + }); + mm.sendAsyncMessage("Test:GetMediaCaptureState"); + }); +} + +function* stopSharing(aType = "camera") { + let promiseRecordingEvent = promiseObserverCalled("recording-device-events"); + gIdentityHandler._identityBox.click(); + let permissions = document.getElementById("identity-popup-permission-list"); + let cancelButton = + permissions.querySelector(".identity-popup-permission-icon." + aType + "-icon ~ " + + ".identity-popup-permission-remove-button"); + cancelButton.click(); + gIdentityHandler._identityPopup.hidden = true; + yield promiseRecordingEvent; + yield expectObserverCalled("getUserMedia:revoke"); + yield expectObserverCalled("recording-window-ended"); + yield expectNoObserverCalled(); + yield* checkNotSharing(); +} + +function promiseRequestDevice(aRequestAudio, aRequestVideo, aFrameId, aType) { + info("requesting devices"); + return ContentTask.spawn(gBrowser.selectedBrowser, + {aRequestAudio, aRequestVideo, aFrameId, aType}, + function*(args) { + let global = content.wrappedJSObject; + if (args.aFrameId) + global = global.document.getElementById(args.aFrameId).contentWindow; + global.requestDevice(args.aRequestAudio, args.aRequestVideo, args.aType); + }); +} + +function* closeStream(aAlreadyClosed, aFrameId) { + yield expectNoObserverCalled(); + + let promises; + if (!aAlreadyClosed) { + promises = [promiseObserverCalled("recording-device-events"), + promiseObserverCalled("recording-window-ended")]; + } + + info("closing the stream"); + yield ContentTask.spawn(gBrowser.selectedBrowser, aFrameId, function*(aFrameId) { + let global = content.wrappedJSObject; + if (aFrameId) + global = global.document.getElementById(aFrameId).contentWindow; + global.closeStream(); + }); + + if (promises) + yield Promise.all(promises); + + yield* assertWebRTCIndicatorStatus(null); +} + +function checkDeviceSelectors(aAudio, aVideo) { + let micSelector = document.getElementById("webRTC-selectMicrophone"); + if (aAudio) + ok(!micSelector.hidden, "microphone selector visible"); + else + ok(micSelector.hidden, "microphone selector hidden"); + + let cameraSelector = document.getElementById("webRTC-selectCamera"); + if (aVideo) + ok(!cameraSelector.hidden, "camera selector visible"); + else + ok(cameraSelector.hidden, "camera selector hidden"); +} + +function* checkSharingUI(aExpected, aWin = window) { + let doc = aWin.document; + // First check the icon above the control center (i) icon. + let identityBox = doc.getElementById("identity-box"); + ok(identityBox.hasAttribute("sharing"), "sharing attribute is set"); + let sharing = identityBox.getAttribute("sharing"); + if (aExpected.video) + is(sharing, "camera", "showing camera icon on the control center icon"); + else if (aExpected.audio) + is(sharing, "microphone", "showing mic icon on the control center icon"); + + // Then check the sharing indicators inside the control center panel. + identityBox.click(); + let permissions = doc.getElementById("identity-popup-permission-list"); + for (let id of ["microphone", "camera", "screen"]) { + let convertId = id => { + if (id == "camera") + return "video"; + if (id == "microphone") + return "audio"; + return id; + }; + let expected = aExpected[convertId(id)]; + is(!!aWin.gIdentityHandler._sharingState[id], !!expected, + "sharing state for " + id + " as expected"); + let icon = permissions.querySelectorAll( + ".identity-popup-permission-icon." + id + "-icon"); + if (expected) { + is(icon.length, 1, "should show " + id + " icon in control center panel"); + ok(icon[0].classList.contains("in-use"), "icon should have the in-use class"); + } else if (!icon.length) { + ok(true, "should not show " + id + " icon in the control center 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(icon.length, 1, "should not show more than 1 " + id + " icon"); + } + } + aWin.gIdentityHandler._identityPopup.hidden = true; + + // Check the global indicators. + yield* assertWebRTCIndicatorStatus(aExpected); +} + +function* checkNotSharing() { + is((yield getMediaCaptureState()), "none", "expected nothing to be shared"); + + ok(!document.getElementById("identity-box").hasAttribute("sharing"), + "no sharing indicator on the control center icon"); + + yield* assertWebRTCIndicatorStatus(null); +} |