Bug 1131574 - In Loop's tab sharing, make the shared tab follow the active tab. r=mikedeboer, a=lsblakk
authorMark Banner <standard8@mozilla.com>
Mon, 02 Mar 2015 19:23:35 +0000
changeset 248116 d12f76b7e524fd9ae6f1545cbc1cf9d864ee1777
parent 248115 8f1e24aa5874eaec3afb142923d9398e50abb465
child 248117 e6b04fc354b3feffe262e6cda635e66850357380
push id7762
push usermdeboer@mozilla.com
push dateMon, 16 Mar 2015 15:07:09 +0000
treeherdermozilla-aurora@9b59f3a2743d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmikedeboer, lsblakk
bugs1131574
milestone38.0a2
Bug 1131574 - In Loop's tab sharing, make the shared tab follow the active tab. r=mikedeboer, a=lsblakk
browser/base/content/browser-loop.js
browser/components/loop/MozLoopAPI.jsm
browser/components/loop/content/shared/js/activeRoomStore.js
browser/components/loop/content/shared/js/otSdkDriver.js
browser/components/loop/test/mochitest/browser.ini
browser/components/loop/test/mochitest/browser_mozLoop_sharingListeners.js
browser/components/loop/test/mochitest/browser_mozLoop_tabSharing.js
browser/components/loop/test/shared/activeRoomStore_test.js
browser/components/loop/test/shared/otSdkDriver_test.js
--- a/browser/base/content/browser-loop.js
+++ b/browser/base/content/browser-loop.js
@@ -338,10 +338,68 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 
       this.activeSound = new window.Audio();
       this.activeSound.src = `chrome://browser/content/loop/shared/sounds/${name}.ogg`;
       this.activeSound.load();
       this.activeSound.play();
 
       this.activeSound.addEventListener("ended", () => this.activeSound = undefined, false);
     },
+
+    /**
+     * Adds a listener for browser sharing. It will inform the listener straight
+     * away for the current windowId, and then on every tab change.
+     *
+     * Listener parameters:
+     * - {Object}  err       If there is a error this will be defined, null otherwise.
+     * - {Integer} windowId  The new windowId for the browser.
+     *
+     * @param {Function} listener The listener to receive information on when the
+     *                            windowId changes.
+     */
+    addBrowserSharingListener: function(listener) {
+      if (!this._tabChangeListeners) {
+        this._tabChangeListeners = new Set();
+        gBrowser.addEventListener("select", this);
+      }
+
+      this._tabChangeListeners.add(listener);
+
+      // Get the first window Id for the listener.
+      listener(null, gBrowser.selectedTab.linkedBrowser.outerWindowID);
+    },
+
+    /**
+     * Removes a listener from browser sharing.
+     *
+     * @param {Function} listener The listener to remove from the list.
+     */
+    removeBrowserSharingListener: function(listener) {
+      if (!this._tabChangeListeners) {
+        return;
+      }
+
+      if (this._tabChangeListeners.has(listener)) {
+        this._tabChangeListeners.delete(listener);
+      }
+
+      if (!this._tabChangeListeners.size) {
+        gBrowser.removeEventListener("select", this);
+        delete this._tabChangeListeners;
+      }
+    },
+
+    /**
+     * Handles events from gBrowser.
+     */
+    handleEvent: function(event) {
+      // We only should get "select" events.
+      if (event.type != "select") {
+        return;
+      }
+
+      // We've changed the tab, so get the new window id.
+      for (let listener of this._tabChangeListeners) {
+        listener(null, gBrowser.selectedTab.linkedBrowser.outerWindowID);
+      };
+    },
   };
 })();
--- a/browser/components/loop/MozLoopAPI.jsm
+++ b/browser/components/loop/MozLoopAPI.jsm
@@ -191,16 +191,17 @@ const injectObjectAPI = function(api, ta
  */
 function injectLoopAPI(targetWindow) {
   let ringer;
   let ringerStopper;
   let appVersionInfo;
   let contactsAPI;
   let roomsAPI;
   let callsAPI;
+  let savedWindowListeners = new Map();
 
   let api = {
     /**
      * Gets an object with data that represents the currently
      * authenticated user's identity.
      *
      * @return null if user not logged in; profile object otherwise
      */
@@ -261,37 +262,71 @@ function injectLoopAPI(targetWindow) {
      */
     locale: {
       enumerable: true,
       get: function() {
         return MozLoopService.locale;
       }
     },
 
-    getActiveTabWindowId: {
+    /**
+     * Adds a listener to the most recent window for browser/tab sharing. The
+     * listener will be notified straight away of the current tab id, then every
+     * time there is a change of tab.
+     *
+     * Listener parameters:
+     * - {Object}  err      If there is a error this will be defined, null otherwise.
+     * - {Number} windowId The new windowId after a change of tab.
+     *
+     * @param {Function} listener The listener to handle the windowId changes.
+     */
+    addBrowserSharingListener: {
       enumerable: true,
       writable: true,
-      value: function(callback) {
+      value: function(listener) {
         let win = Services.wm.getMostRecentWindow("navigator:browser");
         let browser = win && win.gBrowser.selectedTab.linkedBrowser;
         if (!win || !browser) {
           // This may happen when an undocked conversation window is the only
           // window left.
           let err = new Error("No tabs available to share.");
           MozLoopService.log.error(err);
-          callback(cloneValueInto(err, targetWindow));
+          listener(cloneValueInto(err, targetWindow));
+          return;
+        }
+        win.LoopUI.addBrowserSharingListener(listener);
+
+        savedWindowListeners.set(listener, Cu.getWeakReference(win));
+      }
+    },
+
+    /**
+     * Removes a listener that was previously added.
+     *
+     * @param {Function} listener The listener to handle the windowId changes.
+     */
+    removeBrowserSharingListener: {
+      enumerable: true,
+      writable: true,
+      value: function(listener) {
+        if (!savedWindowListeners.has(listener)) {
           return;
         }
 
-        let mm = browser.messageManager;
-        mm.addMessageListener("webrtc:response:StartBrowserSharing", function listener(message) {
-          mm.removeMessageListener("webrtc:response:StartBrowserSharing", listener);
-          callback(null, message.data.windowID);
-        });
-        mm.sendAsyncMessage("webrtc:StartBrowserSharing");
+        let win = savedWindowListeners.get(listener).get();
+
+        // Remove the element, regardless of if the window exists or not so
+        // that we clean the map.
+        savedWindowListeners.delete(listener);
+
+        if (!win) {
+          return;
+        }
+
+        win.LoopUI.removeBrowserSharingListener(listener);
       }
     },
 
     /**
      * Returns the window data for a specific conversation window id.
      *
      * This data will be relevant to the type of window, e.g. rooms or calls.
      * See LoopRooms or LoopCalls for more information.
--- a/browser/components/loop/content/shared/js/activeRoomStore.js
+++ b/browser/components/loop/content/shared/js/activeRoomStore.js
@@ -408,51 +408,82 @@ loop.store.ActiveRoomStore = (function()
     /**
      * Used to note the current state of receiving screenshare data.
      */
     receivingScreenShare: function(actionData) {
       this.setStoreState({receivingScreenShare: actionData.receiving});
     },
 
     /**
+     * Handles switching browser (aka tab) sharing to a new window. Should
+     * only be used for browser sharing.
+     *
+     * @param {Number} windowId  The new windowId to start sharing.
+     */
+    _handleSwitchBrowserShare: function(err, windowId) {
+      if (err) {
+        console.error("Error getting the windowId: " + err);
+        return;
+      }
+
+      var screenSharingState = this.getStoreState().screenSharingState;
+
+      if (screenSharingState === SCREEN_SHARE_STATES.INACTIVE) {
+        // Screen sharing is still pending, so assume that we need to kick it off.
+        var options = {
+          videoSource: "browser",
+          constraints: {
+            browserWindow: windowId,
+            scrollWithPage: true
+          },
+        };
+        this._sdkDriver.startScreenShare(options);
+      } else if (screenSharingState === SCREEN_SHARE_STATES.ACTIVE) {
+        // Just update the current share.
+        this._sdkDriver.switchAcquiredWindow(windowId);
+      } else {
+        console.error("Unexpectedly received windowId for browser sharing when pending");
+      }
+    },
+
+    /**
      * Initiates a screen sharing publisher.
      *
      * @param {sharedActions.StartScreenShare} actionData
      */
     startScreenShare: function(actionData) {
       this.dispatchAction(new sharedActions.ScreenSharingState({
         state: SCREEN_SHARE_STATES.PENDING
       }));
 
       var options = {
         videoSource: actionData.type
       };
       if (options.videoSource === "browser") {
-        this._mozLoop.getActiveTabWindowId(function(err, windowId) {
-          if (err || !windowId) {
-            this.dispatchAction(new sharedActions.ScreenSharingState({
-              state: SCREEN_SHARE_STATES.INACTIVE
-            }));
-            return;
-          }
-          options.constraints = {
-            browserWindow: windowId,
-            scrollWithPage: true
-          };
-          this._sdkDriver.startScreenShare(options);
-        }.bind(this));
+        this._browserSharingListener = this._handleSwitchBrowserShare.bind(this);
+
+        // Set up a listener for watching screen shares. This will get notified
+        // with the first windowId when it is added, so we start off the sharing
+        // from within the listener.
+        this._mozLoop.addBrowserSharingListener(this._browserSharingListener);
       } else {
         this._sdkDriver.startScreenShare(options);
       }
     },
 
     /**
      * Ends an active screenshare session.
      */
     endScreenShare: function() {
+      if (this._browserSharingListener) {
+        // Remove the browser sharing listener as we don't need it now.
+        this._mozLoop.removeBrowserSharingListener(this._browserSharingListener);
+        this._browserSharingListener = null;
+      }
+
       if (this._sdkDriver.endScreenShare()) {
         this.dispatchAction(new sharedActions.ScreenSharingState({
           state: SCREEN_SHARE_STATES.INACTIVE
         }));
       }
     },
 
     /**
--- a/browser/components/loop/content/shared/js/otSdkDriver.js
+++ b/browser/components/loop/content/shared/js/otSdkDriver.js
@@ -131,39 +131,60 @@ loop.OTSdkDriver = (function() {
      *                             be passed when `videoSource` is 'browser'.
      *  - {Boolean} scrollWithPage Flag to signal that scrolling a page should
      *                             update the stream. May be passed when
      *                             `videoSource` is 'browser'.
      *
      * @param {Object} options Hash containing options for the SDK
      */
     startScreenShare: function(options) {
+      // For browser sharing, we store the window Id so that we can avoid unnecessary
+      // re-triggers.
+      if (options.videoSource === "browser") {
+        this._windowId = options.constraints.browserWindow;
+      }
+
       var config = _.extend(this._getCopyPublisherConfig(), options);
 
       this.screenshare = this.sdk.initPublisher(this.getScreenShareElementFunc(),
         config);
       this.screenshare.on("accessAllowed", this._onScreenShareGranted.bind(this));
       this.screenshare.on("accessDenied", this._onScreenShareDenied.bind(this));
     },
 
     /**
+     * Initiates switching the browser window that is being shared.
+     *
+     * @param {Integer} windowId  The windowId of the browser.
+     */
+    switchAcquiredWindow: function(windowId) {
+      if (windowId === this._windowId) {
+        return;
+      }
+
+      this._windowId = windowId;
+      this.screenshare._.switchAcquiredWindow(windowId);
+    },
+
+    /**
      * Ends an active screenshare session. Return `true` when an active screen-
      * sharing session was ended or `false` when no session is active.
      *
      * @type {Boolean}
      */
     endScreenShare: function() {
       if (!this.screenshare) {
         return false;
       }
 
       this.session.unpublish(this.screenshare);
       this.screenshare.off("accessAllowed accessDenied");
       this.screenshare.destroy();
       delete this.screenshare;
+      delete this._windowId;
       return true;
     },
 
     /**
      * Connects a session for the SDK, listening to the required events.
      *
      * sessionData items:
      * - sessionId: The OT session ID
--- a/browser/components/loop/test/mochitest/browser.ini
+++ b/browser/components/loop/test/mochitest/browser.ini
@@ -14,13 +14,13 @@ support-files =
 [browser_GoogleImporter.js]
 skip-if = e10s
 [browser_loop_fxa_server.js]
 [browser_LoopContacts.js]
 [browser_mozLoop_appVersionInfo.js]
 [browser_mozLoop_prefs.js]
 [browser_mozLoop_doNotDisturb.js]
 skip-if = buildapp == 'mulet'
-[browser_toolbarbutton.js]
 [browser_mozLoop_pluralStrings.js]
-[browser_mozLoop_tabSharing.js]
+[browser_mozLoop_sharingListeners.js]
 [browser_mozLoop_telemetry.js]
 skip-if = e10s
+[browser_toolbarbutton.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/test/mochitest/browser_mozLoop_sharingListeners.js
@@ -0,0 +1,125 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * This file contains tests for the window.LoopUI active tab trackers.
+ */
+"use strict";
+
+const {injectLoopAPI} = Cu.import("resource:///modules/loop/MozLoopAPI.jsm");
+gMozLoopAPI = injectLoopAPI({});
+
+let handlers = [
+  {
+    resolve: null,
+    windowId: null,
+    listener: function(err, windowId) {
+      handlers[0].windowId = windowId;
+      handlers[0].resolve();
+    }
+  },
+  {
+    resolve: null,
+    windowId: null,
+    listener: function(err, windowId) {
+      handlers[1].windowId = windowId;
+      handlers[1].resolve();
+    }
+  }
+];
+
+function promiseWindowIdReceivedOnAdd(handler) {
+  return new Promise(resolve => {
+    handler.resolve = resolve;
+    gMozLoopAPI.addBrowserSharingListener(handler.listener);
+  });
+};
+
+let createdTabs = [];
+
+function promiseWindowIdReceivedNewTab(handlers) {
+  let promiseHandlers = [];
+
+  handlers.forEach(handler => {
+    promiseHandlers.push(new Promise(resolve => {
+      handler.resolve = resolve;
+    }));
+  });
+
+  let createdTab = gBrowser.selectedTab = gBrowser.addTab();
+  createdTabs.push(createdTab);
+
+  promiseHandlers.push(promiseTabLoadEvent(createdTab, "about:mozilla"));
+
+  return Promise.all(promiseHandlers);
+};
+
+function removeTabs() {
+  for (let createdTab of createdTabs) {
+    gBrowser.removeTab(createdTab);
+  }
+
+  createdTabs = [];
+}
+
+add_task(function* test_singleListener() {
+  yield promiseWindowIdReceivedOnAdd(handlers[0]);
+
+  let initialWindowId = handlers[0].windowId;
+
+  Assert.notEqual(initialWindowId, null, "window id should be valid");
+
+  // Check that a new tab updates the window id.
+  yield promiseWindowIdReceivedNewTab([handlers[0]]);
+
+  let newWindowId = handlers[0].windowId;
+
+  Assert.notEqual(initialWindowId, newWindowId, "Tab contentWindow IDs shouldn't be the same");
+
+  // Now remove the listener.
+  gMozLoopAPI.removeBrowserSharingListener(handlers[0].listener);
+
+  removeTabs();
+});
+
+add_task(function* test_multipleListener() {
+  yield promiseWindowIdReceivedOnAdd(handlers[0]);
+
+  let initialWindowId0 = handlers[0].windowId;
+
+  Assert.notEqual(initialWindowId0, null, "window id should be valid");
+
+  yield promiseWindowIdReceivedOnAdd(handlers[1]);
+
+  let initialWindowId1 = handlers[1].windowId;
+
+  Assert.notEqual(initialWindowId1, null, "window id should be valid");
+  Assert.equal(initialWindowId0, initialWindowId1, "window ids should be the same");
+
+  // Check that a new tab updates the window id.
+  yield promiseWindowIdReceivedNewTab(handlers);
+
+  let newWindowId0 = handlers[0].windowId;
+  let newWindowId1 = handlers[1].windowId;
+
+  Assert.equal(newWindowId0, newWindowId1, "Listeners should have the same windowId");
+  Assert.notEqual(initialWindowId0, newWindowId0, "Tab contentWindow IDs shouldn't be the same");
+
+  // Now remove the first listener.
+  gMozLoopAPI.removeBrowserSharingListener(handlers[0].listener);
+
+  // Check that a new tab updates the window id.
+  yield promiseWindowIdReceivedNewTab([handlers[1]]);
+
+  let nextWindowId0 = handlers[0].windowId;
+  let nextWindowId1 = handlers[1].windowId;
+
+  Assert.equal(newWindowId0, nextWindowId0, "First listener shouldn't have updated");
+  Assert.notEqual(newWindowId1, nextWindowId1, "Second listener should have updated");
+
+  // Cleanup.
+  gMozLoopAPI.removeBrowserSharingListener(handlers[1].listener);
+
+  removeTabs();
+});
+
deleted file mode 100644
--- a/browser/components/loop/test/mochitest/browser_mozLoop_tabSharing.js
+++ /dev/null
@@ -1,41 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/**
- * This is an integration test to make sure that passing window IDs is working as
- * expected, with or without e10s enabled - rather than just testing MozLoopAPI
- * alone.
- */
-
-const {injectLoopAPI} = Cu.import("resource:///modules/loop/MozLoopAPI.jsm");
-gMozLoopAPI = injectLoopAPI({});
-
-let promiseTabWindowId = function() {
-  return new Promise(resolve => {
-    gMozLoopAPI.getActiveTabWindowId((err, windowId) => {
-      Assert.equal(null, err, "No error should've occurred.");
-      Assert.equal(typeof windowId, "number", "We should have a window ID");
-      resolve(windowId);
-    });
-  });
-};
-
-add_task(function* test_windowIdFetch_simple() {
-  Assert.ok(gMozLoopAPI, "mozLoop should exist");
-
-  yield promiseTabWindowId();
-});
-
-add_task(function* test_windowIdFetch_multipleTabs() {
-  let previousTab = gBrowser.selectedTab;
-  let previousTabId = yield promiseTabWindowId();
-
-  let tab = gBrowser.selectedTab = gBrowser.addTab();
-  yield promiseTabLoadEvent(tab, "about:mozilla");
-  let tabId = yield promiseTabWindowId();
-  Assert.ok(tabId !== previousTabId, "Tab contentWindow IDs shouldn't be the same");
-  gBrowser.removeTab(tab);
-
-  tabId = yield promiseTabWindowId();
-  Assert.equal(previousTabId, tabId, "Window IDs should be back to what they were");
-});
--- a/browser/components/loop/test/shared/activeRoomStore_test.js
+++ b/browser/components/loop/test/shared/activeRoomStore_test.js
@@ -16,37 +16,40 @@ describe("loop.store.ActiveRoomStore", f
   beforeEach(function() {
     sandbox = sinon.sandbox.create();
     sandbox.useFakeTimers();
 
     dispatcher = new loop.Dispatcher();
     sandbox.stub(dispatcher, "dispatch");
 
     fakeMozLoop = {
-      setLoopPref: sandbox.stub(),
-      addConversationContext: sandbox.stub(),
+      setLoopPref: sinon.stub(),
+      addConversationContext: sinon.stub(),
+      addBrowserSharingListener: sinon.stub(),
+      removeBrowserSharingListener: sinon.stub(),
       rooms: {
         get: sinon.stub(),
         join: sinon.stub(),
         refreshMembership: sinon.stub(),
         leave: sinon.stub(),
         on: sinon.stub(),
         off: sinon.stub()
       },
       setScreenShareState: sinon.stub(),
       getActiveTabWindowId: sandbox.stub().callsArgWith(0, null, 42)
     };
 
     fakeSdkDriver = {
-      connectSession: sandbox.stub(),
-      disconnectSession: sandbox.stub(),
-      forceDisconnectAll: sandbox.stub().callsArg(0),
+      connectSession: sinon.stub(),
+      disconnectSession: sinon.stub(),
+      forceDisconnectAll: sinon.stub().callsArg(0),
       retryPublishWithoutVideo: sinon.stub(),
-      startScreenShare: sandbox.stub(),
-      endScreenShare: sandbox.stub().returns(true)
+      startScreenShare: sinon.stub(),
+      switchAcquiredWindow: sinon.stub(),
+      endScreenShare: sinon.stub().returns(true)
     };
 
     fakeMultiplexGum = {
         reset: sandbox.spy()
     };
 
     loop.standaloneMedia = {
       multiplexGum: fakeMultiplexGum
@@ -733,44 +736,88 @@ describe("loop.store.ActiveRoomStore", f
       }));
 
       sinon.assert.calledOnce(fakeSdkDriver.startScreenShare);
       sinon.assert.calledWith(fakeSdkDriver.startScreenShare, {
         videoSource: "window"
       });
     });
 
-    it("should invoke the SDK driver with the correct options for tab sharing", function() {
+    it("should add a browser sharing listener for tab sharing", function() {
       store.startScreenShare(new sharedActions.StartScreenShare({
         type: "browser"
       }));
 
-      sinon.assert.calledOnce(fakeMozLoop.getActiveTabWindowId);
+      sinon.assert.calledOnce(fakeMozLoop.addBrowserSharingListener);
+    });
+
+    it("should invoke the SDK driver with the correct options for tab sharing", function() {
+      fakeMozLoop.addBrowserSharingListener.callsArgWith(0, null, 42);
+
+      store.startScreenShare(new sharedActions.StartScreenShare({
+        type: "browser"
+      }));
 
       sinon.assert.calledOnce(fakeSdkDriver.startScreenShare);
       sinon.assert.calledWith(fakeSdkDriver.startScreenShare, {
         videoSource: "browser",
         constraints: {
           browserWindow: 42,
           scrollWithPage: true
         }
       });
-    })
+    });
+  });
+
+  describe("Screen share Events", function() {
+    var listener;
+
+    beforeEach(function() {
+      store.startScreenShare(new sharedActions.StartScreenShare({
+        type: "browser"
+      }));
+
+      // Listener is the first argument of the first call.
+      listener = fakeMozLoop.addBrowserSharingListener.args[0][0];
+
+      store.setStoreState({
+        screenSharingState: SCREEN_SHARE_STATES.ACTIVE
+      });
+    });
+
+    it("should update the SDK driver when a new window id is received", function() {
+      listener(null, 72);
+
+      sinon.assert.calledOnce(fakeSdkDriver.switchAcquiredWindow);
+      sinon.assert.calledWithExactly(fakeSdkDriver.switchAcquiredWindow, 72);
+    });
   });
 
   describe("#endScreenShare", function() {
     it("should set the state to 'inactive'", function() {
       store.endScreenShare();
 
       sinon.assert.calledOnce(dispatcher.dispatch);
       sinon.assert.calledWith(dispatcher.dispatch,
         new sharedActions.ScreenSharingState({
           state: SCREEN_SHARE_STATES.INACTIVE
         }));
     });
+
+    it("should remove the sharing listener", function() {
+      // Setup the listener.
+      store.startScreenShare(new sharedActions.StartScreenShare({
+        type: "browser"
+      }));
+
+      // Now stop the screen share.
+      store.endScreenShare();
+
+      sinon.assert.calledOnce(fakeMozLoop.removeBrowserSharingListener);
+    });
   });
 
   describe("#remotePeerConnected", function() {
     it("should set the state to `HAS_PARTICIPANTS`", function() {
       store.remotePeerConnected();
 
       expect(store.getStoreState().roomState).eql(ROOM_STATES.HAS_PARTICIPANTS);
     });
--- a/browser/components/loop/test/shared/otSdkDriver_test.js
+++ b/browser/components/loop/test/shared/otSdkDriver_test.js
@@ -42,17 +42,20 @@ describe("loop.OTSdkDriver", function ()
       unpublish: sinon.stub(),
       subscribe: sinon.stub(),
       forceDisconnect: sinon.stub()
     }, Backbone.Events);
 
     publisher = _.extend({
       destroy: sinon.stub(),
       publishAudio: sinon.stub(),
-      publishVideo: sinon.stub()
+      publishVideo: sinon.stub(),
+      _: {
+        switchAcquiredWindow: sinon.stub()
+      }
     }, Backbone.Events);
 
     sdk = _.extend({
       initPublisher: sinon.stub().returns(publisher),
       initSession: sinon.stub().returns(session)
     }, Backbone.Events);
 
     window.OT = {
@@ -176,26 +179,59 @@ describe("loop.OTSdkDriver", function ()
       };
     });
 
     it("should initialize a publisher", function() {
       // We're testing with `videoSource` set to 'browser', not 'window', as it
       // has multiple options.
       var options = {
         videoSource: "browser",
-        browserWindow: 42,
-        scrollWithPage: true
+        constraints: {
+          browserWindow: 42,
+          scrollWithPage: true
+        }
       };
       driver.startScreenShare(options);
 
       sinon.assert.calledOnce(sdk.initPublisher);
       sinon.assert.calledWithMatch(sdk.initPublisher, fakeElement, options);
     });
   });
 
+  describe("#switchAcquiredWindow", function() {
+    beforeEach(function() {
+      var options = {
+        videoSource: "browser",
+        constraints: {
+          browserWindow: 42,
+          scrollWithPage: true
+        }
+      };
+      driver.getScreenShareElementFunc = function() {
+        return fakeScreenElement;
+      };
+      sandbox.stub(dispatcher, "dispatch");
+
+      driver.startScreenShare(options);
+    });
+
+    it("should switch to the acquired window", function() {
+      driver.switchAcquiredWindow(72);
+
+      sinon.assert.calledOnce(publisher._.switchAcquiredWindow);
+      sinon.assert.calledWithExactly(publisher._.switchAcquiredWindow, 72);
+    });
+
+    it("should not switch if the window is the same as the currently selected one", function() {
+      driver.switchAcquiredWindow(42);
+
+      sinon.assert.notCalled(publisher._.switchAcquiredWindow);
+    });
+  });
+
   describe("#endScreenShare", function() {
     beforeEach(function() {
       driver.getScreenShareElementFunc = function() {};
 
       driver.startScreenShare({
         videoSource: "window"
       });