Bug 1120546 - getUserMedia used in frames needs tests. r=felipe, a=test-only
authorFlorian Quèze <florian@queze.net>
Fri, 16 Jan 2015 22:21:13 +0100
changeset 251067 62321380a4c3c14fa507814cb397e18520764efa
parent 251066 510873b2ee88d630a11dd142369bf5a26aaaea4b
child 251068 c0f368aff73869659019b19a48cdf2fdd28b92d8
push id698
push userjlund@mozilla.com
push dateMon, 23 Mar 2015 22:08:11 +0000
treeherdermozilla-release@b0c0ae7b02a3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersfelipe, test-only
bugs1120546
milestone37.0a2
Bug 1120546 - getUserMedia used in frames needs tests. r=felipe, a=test-only
browser/base/content/test/general/browser.ini
browser/base/content/test/general/browser_devices_get_user_media.js
browser/base/content/test/general/browser_devices_get_user_media_in_frame.js
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -296,16 +296,18 @@ skip-if = os == "mac" || e10s # bug 9670
 [browser_customize_popupNotification.js]
 [browser_datareporting_notification.js]
 run-if = datareporting
 [browser_devedition.js]
 [browser_devices_get_user_media.js]
 skip-if = buildapp == 'mulet' || (os == "linux" && debug) || e10s # linux: bug 976544; e10s: bug 1071623
 [browser_devices_get_user_media_about_urls.js]
 skip-if = e10s # Bug 1071623
+[browser_devices_get_user_media_in_frame.js]
+skip-if = e10s # Bug 1071623
 [browser_discovery.js]
 [browser_double_close_tab.js]
 skip-if = e10s
 [browser_duplicateIDs.js]
 [browser_drag.js]
 skip-if = true # browser_drag.js is disabled, as it needs to be updated for the new behavior from bug 320638.
 [browser_favicon_change.js]
 skip-if = e10s
--- a/browser/base/content/test/general/browser_devices_get_user_media.js
+++ b/browser/base/content/test/general/browser_devices_get_user_media.js
@@ -455,16 +455,60 @@ let gTests = [
     yield checkNotSharing();
 
     // 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() {
+    yield promisePopupNotificationShown("webRTC-shareDevices", () => {
+      info("requesting devices");
+      content.wrappedJSObject.requestDevice(true, true);
+    });
+    expectObserverCalled("getUserMedia:request");
+    checkDeviceSelectors(true, true);
+
+    yield promiseMessage("ok", () => {
+      PopupNotifications.panel.firstChild.button.click();
+    });
+    expectObserverCalled("getUserMedia:response:allow");
+    expectObserverCalled("recording-device-events");
+    is(getMediaCaptureState(), "CameraAndMicrophone",
+       "expected camera and microphone to be shared");
+
+    yield checkSharingUI({video: true, audio: true});
+
+    yield promiseNotificationShown(PopupNotifications.getNotification("webRTC-sharingDevices"));
+
+    info("reloading the web page");
+    let deferred = Promise.defer();
+    let browser = gBrowser.selectedBrowser;
+    browser.addEventListener("load", function onload() {
+      browser.removeEventListener("load", onload, true);
+      deferred.resolve();
+    }, true);
+    content.location.reload();
+    yield deferred.promise;
+
+    yield promiseNoPopupNotification("webRTC-sharingDevices");
+    if (gObservedTopics["recording-device-events"] == 2) {
+      todo(false, "Got the 'recording-device-events' notification twice, likely because of bug 962719");
+      --gObservedTopics["recording-device-events"];
+    }
+    expectObserverCalled("recording-device-events");
+    expectObserverCalled("recording-window-ended");
+    expectNoObserverCalled();
+    yield checkNotSharing();
+  }
+},
+
+{
   desc: "getUserMedia prompt: Always/Never Share",
   run: function checkRememberCheckbox() {
     let elt = id => document.getElementById(id);
 
     function checkPerm(aRequestAudio, aRequestVideo, aAllowAudio, aAllowVideo,
                        aExpectedAudioPerm, aExpectedVideoPerm, aNever) {
       yield promisePopupNotificationShown("webRTC-shareDevices", () => {
         content.wrappedJSObject.requestDevice(aRequestAudio, aRequestVideo);
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/general/browser_devices_get_user_media_in_frame.js
@@ -0,0 +1,477 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const kObservedTopics = [
+  "getUserMedia:response:allow",
+  "getUserMedia:revoke",
+  "getUserMedia:response:deny",
+  "getUserMedia:request",
+  "recording-device-events",
+  "recording-window-ended"
+];
+
+const PREF_PERMISSION_FAKE = "media.navigator.permission.fake";
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyServiceGetter(this, "MediaManagerService",
+                                   "@mozilla.org/mediaManagerService;1",
+                                   "nsIMediaManagerService");
+
+var gObservedTopics = {};
+function observer(aSubject, aTopic, aData) {
+  if (!(aTopic in gObservedTopics))
+    gObservedTopics[aTopic] = 1;
+  else
+    ++gObservedTopics[aTopic];
+}
+
+function promiseObserverCalled(aTopic, aAction) {
+  let deferred = Promise.defer();
+
+  Services.obs.addObserver(function observer() {
+    ok(true, "got " + aTopic + " notification");
+    Services.obs.removeObserver(observer, aTopic);
+
+    if (kObservedTopics.indexOf(aTopic) != -1) {
+      if (!(aTopic in gObservedTopics))
+        gObservedTopics[aTopic] = -1;
+      else
+        --gObservedTopics[aTopic];
+    }
+
+    deferred.resolve();
+  }, aTopic, false);
+
+  if (aAction)
+    aAction();
+
+  return deferred.promise;
+}
+
+function expectObserverCalled(aTopic) {
+  is(gObservedTopics[aTopic], 1, "expected notification " + aTopic);
+  if (aTopic in gObservedTopics)
+    --gObservedTopics[aTopic];
+}
+
+function expectNoObserverCalled() {
+  for (let topic in gObservedTopics) {
+    if (gObservedTopics[topic])
+      is(gObservedTopics[topic], 0, topic + " notification unexpected");
+  }
+  gObservedTopics = {};
+}
+
+function promiseMessage(aMessage, aAction) {
+  let deferred = Promise.defer();
+
+  content.addEventListener("message", function messageListener(event) {
+    content.removeEventListener("message", messageListener);
+    is(event.data, aMessage, "received " + aMessage);
+    if (event.data == aMessage)
+      deferred.resolve();
+    else
+      deferred.reject();
+  });
+
+  if (aAction)
+    aAction();
+
+  return deferred.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.contains("Mac") });
+}
+
+registerCleanupFunction(function() {
+  gBrowser.removeCurrentTab();
+  kObservedTopics.forEach(topic => {
+    Services.obs.removeObserver(observer, topic);
+  });
+  Services.prefs.clearUserPref(PREF_PERMISSION_FAKE);
+});
+
+function getMediaCaptureState() {
+  let hasVideo = {};
+  let hasAudio = {};
+  MediaManagerService.mediaCaptureWindowState(content, hasVideo, hasAudio);
+  if (hasVideo.value && hasAudio.value)
+    return "CameraAndMicrophone";
+  if (hasVideo.value)
+    return "Camera";
+  if (hasAudio.value)
+    return "Microphone";
+  return "none";
+}
+
+function* closeStream(aGlobal, aAlreadyClosed) {
+  expectNoObserverCalled();
+
+  info("closing the stream");
+  aGlobal.closeStream();
+
+  if (!aAlreadyClosed)
+    yield promiseObserverCalled("recording-device-events");
+
+  yield promiseNoPopupNotification("webRTC-sharingDevices");
+  if (!aAlreadyClosed)
+    expectObserverCalled("recording-window-ended");
+
+  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) {
+  yield promisePopupNotification("webRTC-sharingDevices");
+
+  yield* assertWebRTCIndicatorStatus(aExpected);
+}
+
+function* checkNotSharing() {
+  is(getMediaCaptureState(), "none", "expected nothing to be shared");
+
+  ok(!PopupNotifications.getNotification("webRTC-sharingDevices"),
+     "no webRTC-sharingDevices popup notification");
+
+  yield* assertWebRTCIndicatorStatus(null);
+}
+
+function getFrameGlobal(aFrameId) {
+  return content.wrappedJSObject.document.getElementById(aFrameId).contentWindow;
+}
+
+const permissionError = "error: PermissionDeniedError: The user did not grant permission for the operation.";
+
+let gTests = [
+
+{
+  desc: "getUserMedia audio+video",
+  run: function checkAudioVideo() {
+    let global = getFrameGlobal("frame1");
+    yield promisePopupNotificationShown("webRTC-shareDevices", () => {
+      info("requesting devices");
+      global.requestDevice(true, true);
+    });
+    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");
+
+    yield promiseMessage("ok", () => {
+      PopupNotifications.panel.firstChild.button.click();
+    });
+    expectObserverCalled("getUserMedia:response:allow");
+    expectObserverCalled("recording-device-events");
+    is(getMediaCaptureState(), "CameraAndMicrophone",
+       "expected camera and microphone to be shared");
+
+    yield checkSharingUI({audio: true, video: true});
+    yield closeStream(global);
+  }
+},
+
+{
+  desc: "getUserMedia audio+video: stop sharing",
+  run: function checkStopSharing() {
+    let global = getFrameGlobal("frame1");
+    yield promisePopupNotificationShown("webRTC-shareDevices", () => {
+      info("requesting devices");
+      global.requestDevice(true, true);
+    });
+    expectObserverCalled("getUserMedia:request");
+    checkDeviceSelectors(true, true);
+
+    yield promiseMessage("ok", () => {
+      PopupNotifications.panel.firstChild.button.click();
+    });
+    expectObserverCalled("getUserMedia:response:allow");
+    expectObserverCalled("recording-device-events");
+    is(getMediaCaptureState(), "CameraAndMicrophone",
+       "expected camera and microphone to be shared");
+
+    yield checkSharingUI({video: true, audio: true});
+
+    yield promiseNotificationShown(PopupNotifications.getNotification("webRTC-sharingDevices"));
+    activateSecondaryAction(kActionDeny);
+
+    yield promiseObserverCalled("recording-device-events");
+    expectObserverCalled("getUserMedia:revoke");
+
+    yield promiseNoPopupNotification("webRTC-sharingDevices");
+    expectObserverCalled("recording-window-ended");
+
+    if (gObservedTopics["recording-device-events"] == 1) {
+      todo(false, "Got the 'recording-device-events' notification twice, likely because of bug 962719");
+      gObservedTopics["recording-device-events"] = 0;
+    }
+
+    expectNoObserverCalled();
+    yield checkNotSharing();
+
+    // the stream is already closed, but this will do some cleanup anyway
+    yield closeStream(global, true);
+  }
+},
+
+{
+  desc: "getUserMedia audio+video: reloading the frame removes all sharing UI",
+  run: function checkReloading() {
+    let global = getFrameGlobal("frame1");
+    yield promisePopupNotificationShown("webRTC-shareDevices", () => {
+      info("requesting devices");
+      global.requestDevice(true, true);
+    });
+    expectObserverCalled("getUserMedia:request");
+    checkDeviceSelectors(true, true);
+
+    yield promiseMessage("ok", () => {
+      PopupNotifications.panel.firstChild.button.click();
+    });
+    expectObserverCalled("getUserMedia:response:allow");
+    expectObserverCalled("recording-device-events");
+    is(getMediaCaptureState(), "CameraAndMicrophone",
+       "expected camera and microphone to be shared");
+
+    yield checkSharingUI({video: true, audio: true});
+
+    info("reloading the frame");
+    let deferred = Promise.defer();
+    let browser = gBrowser.selectedBrowser;
+    browser.addEventListener("load", function onload() {
+      browser.removeEventListener("load", onload, true);
+      deferred.resolve();
+    }, true);
+    global.location.reload();
+    yield deferred.promise;
+
+    yield promiseNoPopupNotification("webRTC-sharingDevices");
+    if (gObservedTopics["recording-device-events"] == 2) {
+      todo(false, "Got the 'recording-device-events' notification twice, likely because of bug 962719");
+      --gObservedTopics["recording-device-events"];
+    }
+    expectObserverCalled("recording-device-events");
+    expectObserverCalled("recording-window-ended");
+    expectNoObserverCalled();
+    yield checkNotSharing();
+  }
+},
+
+{
+  desc: "getUserMedia audio+video: reloading the frame removes prompts",
+  run: function checkReloadingRemovesPrompts() {
+    let global = getFrameGlobal("frame1");
+    yield promisePopupNotificationShown("webRTC-shareDevices", () => {
+      info("requesting devices");
+      global.requestDevice(true, true);
+    });
+    expectObserverCalled("getUserMedia:request");
+    checkDeviceSelectors(true, true);
+
+    info("reloading the frame");
+    let deferred = Promise.defer();
+    let browser = gBrowser.selectedBrowser;
+    browser.addEventListener("load", function onload() {
+      browser.removeEventListener("load", onload, true);
+      deferred.resolve();
+    }, true);
+    global.location.reload();
+    yield deferred.promise;
+
+    yield promiseNoPopupNotification("webRTC-shareDevices");
+
+    expectObserverCalled("recording-window-ended");
+    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 g1 = getFrameGlobal("frame1"), g2 = getFrameGlobal("frame2");
+
+    yield promisePopupNotificationShown("webRTC-shareDevices", () => {
+      info("requesting microphone in the first frame");
+      g1.requestDevice(true, false);
+    });
+    expectObserverCalled("getUserMedia:request");
+    checkDeviceSelectors(true, false);
+
+    yield promiseMessage("ok", () => {
+      PopupNotifications.panel.firstChild.button.click();
+    });
+    expectObserverCalled("getUserMedia:response:allow");
+    expectObserverCalled("recording-device-events");
+    is(getMediaCaptureState(), "Microphone", "microphone to be shared");
+
+    yield checkSharingUI({video: false, audio: true});
+    expectNoObserverCalled();
+
+    yield promisePopupNotificationShown("webRTC-shareDevices", () => {
+      info("requesting both devices in the second frame");
+      g2.requestDevice(true, true);
+    });
+    expectObserverCalled("getUserMedia:request");
+    checkDeviceSelectors(true, true);
+
+    yield promiseMessage("ok", () => {
+      PopupNotifications.panel.firstChild.button.click();
+    });
+    expectObserverCalled("getUserMedia:response:allow");
+    expectObserverCalled("recording-device-events");
+    is(getMediaCaptureState(), "CameraAndMicrophone",
+       "expected camera and microphone to be shared");
+
+    yield checkSharingUI({video: true, audio: true});
+    expectNoObserverCalled();
+
+    info("reloading the second frame");
+    let deferred = Promise.defer();
+    let browser = gBrowser.selectedBrowser;
+    browser.addEventListener("load", function onload() {
+      browser.removeEventListener("load", onload, true);
+      deferred.resolve();
+    }, true);
+    g2.location.reload();
+    yield deferred.promise;
+
+    yield checkSharingUI({video: false, audio: true});
+    expectObserverCalled("recording-window-ended");
+    if (gObservedTopics["recording-device-events"] == 2) {
+      todo(false, "Got the 'recording-device-events' notification twice, likely because of bug 962719");
+      --gObservedTopics["recording-device-events"];
+    }
+    expectObserverCalled("recording-device-events");
+    expectNoObserverCalled();
+
+    yield closeStream(g1);
+    yield promiseNoPopupNotification("webRTC-sharingDevices");
+    expectNoObserverCalled();
+    yield checkNotSharing();
+  }
+}
+
+];
+
+function test() {
+  waitForExplicitFinish();
+
+  let tab = gBrowser.addTab();
+  gBrowser.selectedTab = tab;
+  tab.linkedBrowser.addEventListener("load", function onload() {
+    tab.linkedBrowser.removeEventListener("load", onload, true);
+
+    kObservedTopics.forEach(topic => {
+      Services.obs.addObserver(observer, topic, false);
+    });
+    Services.prefs.setBoolPref(PREF_PERMISSION_FAKE, true);
+
+    is(PopupNotifications._currentNotifications.length, 0,
+       "should start the test without any prior popup notification");
+
+    Task.spawn(function () {
+      for (let test of gTests) {
+        info(test.desc);
+        yield test.run();
+
+        // Cleanup before the next test
+        expectNoObserverCalled();
+      }
+    }).then(finish, 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>'
+}