summaryrefslogtreecommitdiffstats
path: root/browser/base/content/test/webrtc
diff options
context:
space:
mode:
Diffstat (limited to 'browser/base/content/test/webrtc')
-rw-r--r--browser/base/content/test/webrtc/.eslintrc.js7
-rw-r--r--browser/base/content/test/webrtc/browser.ini11
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media.js554
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_anim.js109
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_in_frame.js266
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_tear_off_tab.js109
-rw-r--r--browser/base/content/test/webrtc/get_user_media.html55
-rw-r--r--browser/base/content/test/webrtc/get_user_media_content_script.js85
-rw-r--r--browser/base/content/test/webrtc/head.js453
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);
+}