Bug 1210865 - Change how Loop's data channels are setup to cope with the newer SDK that doesn't allow setting them up until subscription is complete. r=dmose, a=ritu
authorMark Banner <standard8@mozilla.com>
Tue, 10 Nov 2015 08:58:10 +0000
changeset 305653 d574b73912fcebd71b66b1117bc44465a212561d
parent 305652 11329c457f99e0168cb37c3b7ec93530e1736cb1
child 305654 3493d5fba50e5cc68c34e868200f5e1d312457f4
push id1001
push userraliiev@mozilla.com
push dateMon, 18 Jan 2016 19:06:03 +0000
treeherdermozilla-release@8b89261f3ac4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdmose, ritu
bugs1210865
milestone44.0a2
Bug 1210865 - Change how Loop's data channels are setup to cope with the newer SDK that doesn't allow setting them up until subscription is complete. r=dmose, a=ritu
browser/components/loop/content/shared/js/otSdkDriver.js
browser/components/loop/test/functional/test_1_browser_call.py
browser/components/loop/test/shared/otSdkDriver_test.js
--- a/browser/components/loop/content/shared/js/otSdkDriver.js
+++ b/browser/components/loop/content/shared/js/otSdkDriver.js
@@ -615,17 +615,17 @@ loop.OTSdkDriver = (function() {
       }));
 
       this._subscribedRemoteStream = true;
       if (this._checkAllStreamsConnected()) {
         this._setTwoWayMediaStartTime(performance.now());
         this.dispatcher.dispatch(new sharedActions.MediaConnected());
       }
 
-      this._setupDataChannelIfNeeded(sdkSubscriberObject.stream.connection);
+      this._setupDataChannelIfNeeded(sdkSubscriberObject);
     },
 
     /**
      * This method is passed as the "completionHandler" parameter to the SDK's
      * Session.subscribe.
      *
      * @param err {(null|Error)} - null on success, an Error object otherwise
      * @param sdkSubscriberObject {OT.Subscriber} - undocumented; returned on success
@@ -650,71 +650,34 @@ loop.OTSdkDriver = (function() {
 
     },
 
     /**
      * Once a remote stream has been subscribed to, this triggers the data
      * channel set-up routines. A data channel cannot be requested before this
      * time as the peer connection is not set up.
      *
-     * @param {OT.Connection} connection The OT connection class object.paul
-     * sched
+     * @param {OT.Subscriber} sdkSubscriberObject The subscriber object for the stream.
      *
      */
-    _setupDataChannelIfNeeded: function(connection) {
-      if (this._useDataChannels) {
-        this.session.signal({
-          type: "readyForDataChannel",
-          to: connection
-        }, function(signalError) {
-          if (signalError) {
-            console.error(signalError);
-          }
-        });
-      }
-    },
-
-    /**
-     * Handles receiving the signal that the other end of the connection
-     * has subscribed to the stream and we're ready to setup the data channel.
-     *
-     * We get data channels for both the publisher and subscriber on reception
-     * of the signal, as it means that a) the remote client is setup for data
-     * channels, and b) that subscribing of streams has definitely completed
-     * for both clients.
-     *
-     * @param {OT.SignalEvent} event Details of the signal received.
-     */
-    _onReadyForDataChannel: function(event) {
-      // If we don't want data channels, just ignore the message. We haven't
-      // send the other side a message, so it won't display anything.
+    _setupDataChannelIfNeeded: function(sdkSubscriberObject) {
       if (!this._useDataChannels) {
         return;
       }
 
-      // This won't work until a subscriber exists for this publisher
-      this.publisher._.getDataChannel("text", {}, function(err, channel) {
-        if (err) {
-          console.error(err);
-          return;
+      this.session.signal({
+        type: "readyForDataChannel",
+        to: sdkSubscriberObject.stream.connection
+      }, function(signalError) {
+        if (signalError) {
+          console.error(signalError);
         }
-
-        this._publisherChannel = channel;
+      });
 
-        channel.on({
-          close: function(e) {
-            // XXX We probably want to dispatch and handle this somehow.
-            console.log("Published data channel closed!");
-          }
-        });
-
-        this._checkDataChannelsAvailable();
-      }.bind(this));
-
-      this.subscriber._.getDataChannel("text", {}, function(err, channel) {
+      sdkSubscriberObject._.getDataChannel("text", {}, function(err, channel) {
         // Sends will queue until the channel is fully open.
         if (err) {
           console.error(err);
           return;
         }
 
         channel.on({
           message: function(ev) {
@@ -737,16 +700,54 @@ loop.OTSdkDriver = (function() {
         });
 
         this._subscriberChannel = channel;
         this._checkDataChannelsAvailable();
       }.bind(this));
     },
 
     /**
+     * Handles receiving the signal that the other end of the connection
+     * has subscribed to the stream and we're ready to setup the data channel.
+     *
+     * We create the publisher data channel when we get the signal as it means
+     * that the remote client is setup for data
+     * channels. Getting the data channel for the subscriber is handled
+     * separately when the subscription completes.
+     *
+     * @param {OT.SignalEvent} event Details of the signal received.
+     */
+    _onReadyForDataChannel: function(event) {
+      // If we don't want data channels, just ignore the message. We haven't
+      // send the other side a message, so it won't display anything.
+      if (!this._useDataChannels) {
+        return;
+      }
+
+      // This won't work until a subscriber exists for this publisher
+      this.publisher._.getDataChannel("text", {}, function(err, channel) {
+        if (err) {
+          console.error(err);
+          return;
+        }
+
+        this._publisherChannel = channel;
+
+        channel.on({
+          close: function(e) {
+            // XXX We probably want to dispatch and handle this somehow.
+            console.log("Published data channel closed!");
+          }
+        });
+
+        this._checkDataChannelsAvailable();
+      }.bind(this));
+    },
+
+    /**
      * Checks to see if all channels have been obtained, and if so it dispatches
      * a notification to the stores to inform them.
      */
     _checkDataChannelsAvailable: function() {
       if (this._publisherChannel && this._subscriberChannel) {
         this.dispatcher.dispatch(new sharedActions.DataChannelsAvailable({
           available: true
         }));
--- a/browser/components/loop/test/functional/test_1_browser_call.py
+++ b/browser/components/loop/test/functional/test_1_browser_call.py
@@ -137,16 +137,60 @@ class Test1BrowserCall(MarionetteTestCas
     def standalone_check_remote_video(self):
         self.switch_to_standalone()
         self.check_video(".remote-video")
 
     def local_check_remote_video(self):
         self.switch_to_chatbox()
         self.check_video(".remote-video")
 
+    def send_chat_message(self, text):
+        """
+        Sends a chat message using the current context.
+
+        :param text: The text to send.
+        """
+        chatbox = self.wait_for_element_displayed(By.CSS_SELECTOR,
+                                                  ".text-chat-box > form > input")
+
+        chatbox.send_keys(text + "\n")
+
+    def check_received_message(self, expectedText):
+        """
+        Checks a chat message has been received in the current context. The
+        test assumes only one chat message will be received during the tests.
+
+        :param expectedText: The expected text of the chat message.
+        """
+        text_entry = self.wait_for_element_displayed(By.CSS_SELECTOR,
+                                                     ".text-chat-entry.received > p > span")
+
+        self.assertEqual(text_entry.text, expectedText,
+                         "should have received the correct message")
+
+    def check_text_messaging(self):
+        """
+        Checks text messaging between the generator and clicker in a bi-directional
+        fashion.
+        """
+        # Send a message using the link generator.
+        self.switch_to_chatbox()
+        self.send_chat_message("test1")
+
+        # Now check the result on the link clicker.
+        self.switch_to_standalone()
+        self.check_received_message("test1")
+
+        # Then send a message using the standalone.
+        self.send_chat_message("test2")
+
+        # Finally check the link generator got it.
+        self.switch_to_chatbox()
+        self.check_received_message("test2")
+
     def local_enable_screenshare(self):
         self.switch_to_chatbox()
         button = self.marionette.find_element(By.CLASS_NAME, "btn-screen-share")
 
         button.click()
 
     def standalone_check_remote_screenshare(self):
         self.switch_to_standalone()
@@ -237,16 +281,19 @@ class Test1BrowserCall(MarionetteTestCas
 
         # load the link clicker interface into the current content browser
         self.standalone_load_and_join_room(room_url)
 
         # Check we get the video streams
         self.standalone_check_remote_video()
         self.local_check_remote_video()
 
+        # Check text messaging
+        self.check_text_messaging()
+
         # since bi-directional media is connected, make sure we've set
         # the start time
         self.local_check_media_start_time_initialized()
 
         # XXX To enable this, we either need to navigate the permissions prompt
         # or have a route where we don't need the permissions prompt.
         # self.local_enable_screenshare()
         # self.standalone_check_remote_screenshare()
--- a/browser/components/loop/test/shared/otSdkDriver_test.js
+++ b/browser/components/loop/test/shared/otSdkDriver_test.js
@@ -747,16 +747,19 @@ describe("loop.OTSdkDriver", function() 
       fakeConnection = "fakeConnection";
       fakeStream = {
         hasVideo: true,
         videoType: "camera",
         videoDimensions: { width: 1, height: 2 }
       };
 
       fakeSubscriberObject = _.extend({
+        "_": {
+          getDataChannel: sinon.stub()
+        },
         session: { connection: fakeConnection },
         stream: fakeStream
       }, Backbone.Events);
 
       fakeSdkContainerWithVideo = {
         querySelector: sinon.stub().returns(videoElement)
       };
 
@@ -991,169 +994,233 @@ describe("loop.OTSdkDriver", function() 
             event: "Session.streamCreated",
             state: "receiving",
             connections: 1,
             recvStreams: 1,
             sendStreams: 0
           }));
       });
 
-      it("should subscribe to a camera stream", function() {
-        session.trigger("streamCreated", { stream: fakeStream });
+      describe("Audio/Video streams", function() {
+        beforeEach(function() {
+          session.subscribe.yieldsOn(driver, null, fakeSubscriberObject,
+            videoElement).returns(this.fakeSubscriberObject);
+        });
+
+        it("should subscribe to a camera stream", function() {
+          session.trigger("streamCreated", { stream: fakeStream });
+
+          sinon.assert.calledOnce(session.subscribe);
+          sinon.assert.calledWithExactly(session.subscribe,
+            fakeStream, sinon.match.instanceOf(HTMLDivElement), publisherConfig,
+            sinon.match.func);
+        });
 
-        sinon.assert.calledOnce(session.subscribe);
-        sinon.assert.calledWithExactly(session.subscribe,
-          fakeStream, sinon.match.instanceOf(HTMLDivElement), publisherConfig,
-          sinon.match.func);
-      });
+        it("should dispatch MediaStreamCreated after streamCreated is triggered on the session", function() {
+          driver.session = session;
+          fakeStream.connection = fakeConnection;
+          fakeStream.hasVideo = true;
+
+          session.trigger("streamCreated", { stream: fakeStream });
+
+          sinon.assert.called(dispatcher.dispatch);
+          sinon.assert.calledWithExactly(dispatcher.dispatch,
+            new sharedActions.MediaStreamCreated({
+              hasVideo: true,
+              isLocal: false,
+              srcMediaElement: videoElement
+            }));
+        });
+
+        it("should dispatch MediaStreamCreated after streamCreated with audio-only indication if hasVideo=false", function() {
+          fakeStream.connection = fakeConnection;
+          fakeStream.hasVideo = false;
+
+          session.trigger("streamCreated", { stream: fakeStream });
 
-      it("should dispatch MediaStreamCreated after subscribe is complete", function() {
-        session.subscribe.yieldsOn(driver, null, fakeSubscriberObject,
-          videoElement).returns(this.fakeSubscriberObject);
-        driver.session = session;
-        fakeStream.connection = fakeConnection;
-        fakeStream.hasVideo = true;
+          sinon.assert.called(dispatcher.dispatch);
+          sinon.assert.calledWithExactly(dispatcher.dispatch,
+            new sharedActions.MediaStreamCreated({
+              hasVideo: false,
+              isLocal: false,
+              srcMediaElement: videoElement
+            }));
+        });
 
-        session.trigger("streamCreated", { stream: fakeStream });
+        it("should dispatch a mediaConnected action if both streams are up", function() {
+          driver._publishedLocalStream = true;
+
+          session.trigger("streamCreated", { stream: fakeStream });
+
+          // Called twice due to the VideoDimensionsChanged above.
+          sinon.assert.called(dispatcher.dispatch);
+          sinon.assert.calledWithMatch(dispatcher.dispatch,
+            new sharedActions.MediaConnected({}));
+        });
 
-        sinon.assert.called(dispatcher.dispatch);
-        sinon.assert.calledWithExactly(dispatcher.dispatch,
-          new sharedActions.MediaStreamCreated({
-            hasVideo: true,
-            isLocal: false,
-            srcMediaElement: videoElement
-          }));
-      });
+        it("should store the start time when both streams are up and" +
+        " driver._sendTwoWayMediaTelemetry is true", function() {
+          driver._sendTwoWayMediaTelemetry = true;
+          driver._publishedLocalStream = true;
+          var startTime = 1;
+          sandbox.stub(performance, "now").returns(startTime);
+
+          session.trigger("streamCreated", { stream: fakeStream });
+
+          expect(driver._getTwoWayMediaStartTime()).to.eql(startTime);
+        });
+
+        it("should not store the start time when both streams are up and" +
+           " driver._isDesktop is false", function() {
+          driver._isDesktop = false;
+          driver._publishedLocalStream = true;
+          var startTime = 73;
+          sandbox.stub(performance, "now").returns(startTime);
+
+          session.trigger("streamCreated", { stream: fakeStream });
+
+          expect(driver._getTwoWayMediaStartTime()).to.not.eql(startTime);
+        });
+
+        describe("Data channel setup", function() {
+          var fakeChannel;
 
-      it("should dispatch MediaStreamCreated after subscribe with audio-only indication if hasVideo=false", function() {
-        session.subscribe.yieldsOn(driver, null, fakeSubscriberObject,
-          videoElement);
-        fakeStream.connection = fakeConnection;
-        fakeStream.hasVideo = false;
+          beforeEach(function() {
+            fakeChannel = _.extend({}, Backbone.Events);
+            fakeStream.connection = fakeConnection;
+            driver._useDataChannels = true;
+          });
+
+          it("should trigger a readyForDataChannel signal after subscribe is complete", function() {
+            session.trigger("streamCreated", { stream: fakeStream });
+
+            sinon.assert.calledOnce(session.signal);
+            sinon.assert.calledWith(session.signal, {
+              type: "readyForDataChannel",
+              to: fakeConnection
+            });
+          });
+
+          it("should not trigger readyForDataChannel signal if data channels are not wanted", function() {
+            driver._useDataChannels = false;
+
+            session.trigger("streamCreated", { stream: fakeStream });
 
-        session.trigger("streamCreated", { stream: fakeStream });
+            sinon.assert.notCalled(session.signal);
+          });
+
+          it("should get the data channel after subscribe is complete", function() {
+            session.trigger("streamCreated", { stream: fakeStream });
+
+            sinon.assert.calledOnce(fakeSubscriberObject._.getDataChannel);
+            sinon.assert.calledWith(fakeSubscriberObject._.getDataChannel, "text", {});
+          });
+
+          it("should not get the data channel if data channels are not wanted", function() {
+            driver._useDataChannels = false;
+
+            session.trigger("streamCreated", { stream: fakeStream });
+
+            sinon.assert.notCalled(fakeSubscriberObject._.getDataChannel);
+          });
+
+          it("should dispatch `DataChannelsAvailable` if the publisher channel is setup", function() {
+            // Fake a publisher channel.
+            driver._publisherChannel = {};
+
+            fakeSubscriberObject._.getDataChannel.callsArgWith(2, null, fakeChannel);
 
-        sinon.assert.called(dispatcher.dispatch);
-        sinon.assert.calledWithExactly(dispatcher.dispatch,
-          new sharedActions.MediaStreamCreated({
-            hasVideo: false,
-            isLocal: false,
-            srcMediaElement: videoElement
-          }));
-      });
+            session.trigger("streamCreated", { stream: fakeStream });
+
+            sinon.assert.called(dispatcher.dispatch);
+            sinon.assert.calledWithExactly(dispatcher.dispatch,
+              new sharedActions.DataChannelsAvailable({
+                available: true
+              }));
+          });
+
+          it("should not dispatch `DataChannelsAvailable` if the publisher channel isn't setup", function() {
+            fakeSubscriberObject._.getDataChannel.callsArgWith(2, null, fakeChannel);
+
+            session.trigger("streamCreated", { stream: fakeStream });
+
+            sinon.assert.neverCalledWith(dispatcher.dispatch,
+              new sharedActions.DataChannelsAvailable({
+                available: true
+              }));
+          });
 
-      it("should trigger a readyForDataChannel signal after subscribe is complete", function() {
-        session.subscribe.yieldsOn(driver, null, fakeSubscriberObject,
-          document.createElement("video"));
-        driver._useDataChannels = true;
-        fakeStream.connection = fakeConnection;
+          it("should dispatch `ReceivedTextChatMessage` when a text message is received", function() {
+            var data = '{"contentType":"' + CHAT_CONTENT_TYPES.TEXT +
+                       '","message":"Are you there?","receivedTimestamp": "2015-06-25T00:29:14.197Z"}';
+            var clock = sinon.useFakeTimers();
+
+            fakeSubscriberObject._.getDataChannel.callsArgWith(2, null, fakeChannel);
+
+            session.trigger("streamCreated", { stream: fakeStream });
 
-        session.trigger("streamCreated", { stream: fakeStream });
+            // Now send the message.
+            fakeChannel.trigger("message", {
+              data: data
+            });
 
-        sinon.assert.calledOnce(session.signal);
-        sinon.assert.calledWith(session.signal, {
-          type: "readyForDataChannel",
-          to: fakeConnection
+            sinon.assert.called(dispatcher.dispatch);
+            sinon.assert.calledWithExactly(dispatcher.dispatch,
+              new sharedActions.ReceivedTextChatMessage({
+                contentType: CHAT_CONTENT_TYPES.TEXT,
+                message: "Are you there?",
+                receivedTimestamp: "1970-01-01T00:00:00.000Z"
+              }));
+
+            /* Restore the time. */
+            clock.restore();
+          });
         });
       });
 
-      it("should not trigger readyForDataChannel signal if data channels are not wanted", function() {
-        session.subscribe.yieldsOn(driver, null, fakeSubscriberObject,
-          document.createElement("video"));
-        driver._useDataChannels = false;
-        fakeStream.connection = fakeConnection;
-
-        session.trigger("streamCreated", { stream: fakeStream });
-
-        sinon.assert.notCalled(session.signal);
-      });
-
-      it("should subscribe to a screen sharing stream", function() {
-        fakeStream.videoType = "screen";
+      describe("screen sharing streams", function() {
+        it("should subscribe to a screen sharing stream", function() {
+          fakeStream.videoType = "screen";
 
-        session.trigger("streamCreated", { stream: fakeStream });
-
-        sinon.assert.calledOnce(session.subscribe);
-        sinon.assert.calledWithExactly(session.subscribe,
-          fakeStream, sinon.match.instanceOf(HTMLDivElement), publisherConfig,
-          sinon.match.func);
-      });
-
-      it("should dispatch a mediaConnected action if both streams are up", function() {
-        session.subscribe.yieldsOn(driver, null, fakeSubscriberObject,
-          videoElement);
-        driver._publishedLocalStream = true;
-
-        session.trigger("streamCreated", { stream: fakeStream });
+          session.trigger("streamCreated", { stream: fakeStream });
 
-        // Called twice due to the VideoDimensionsChanged above.
-        sinon.assert.called(dispatcher.dispatch);
-        sinon.assert.calledWithMatch(dispatcher.dispatch,
-          new sharedActions.MediaConnected({}));
-      });
-
-      it("should store the start time when both streams are up and" +
-      " driver._sendTwoWayMediaTelemetry is true", function() {
-        session.subscribe.yieldsOn(driver, null, fakeSubscriberObject,
-          videoElement);
-        driver._sendTwoWayMediaTelemetry = true;
-        driver._publishedLocalStream = true;
-        var startTime = 1;
-        sandbox.stub(performance, "now").returns(startTime);
-
-        session.trigger("streamCreated", { stream: fakeStream });
+          sinon.assert.calledOnce(session.subscribe);
+          sinon.assert.calledWithExactly(session.subscribe,
+            fakeStream, sinon.match.instanceOf(HTMLDivElement), publisherConfig,
+            sinon.match.func);
+        });
 
-        expect(driver._getTwoWayMediaStartTime()).to.eql(startTime);
-      });
-
-      it("should not store the start time when both streams are up and" +
-         " driver._isDesktop is false", function() {
-        session.subscribe.yieldsOn(driver, null, fakeSubscriberObject,
-          videoElement);
-        driver._isDesktop = false;
-        driver._publishedLocalStream = true;
-        var startTime = 73;
-        sandbox.stub(performance, "now").returns(startTime);
-
-        session.trigger("streamCreated", { stream: fakeStream });
-
-        expect(driver._getTwoWayMediaStartTime()).to.not.eql(startTime);
-      });
-
-
-      it("should not dispatch a mediaConnected action for screen sharing streams",
-        function() {
+        it("should not dispatch a mediaConnected action for screen sharing streams", function() {
           driver._publishedLocalStream = true;
           fakeStream.videoType = "screen";
 
           session.trigger("streamCreated", { stream: fakeStream });
 
           sinon.assert.neverCalledWithMatch(dispatcher.dispatch,
             sinon.match.hasOwn("name", "mediaConnected"));
         });
 
-      it("should not dispatch a ReceivingScreenShare action for camera streams",
-        function() {
+        it("should not dispatch a ReceivingScreenShare action for camera streams", function() {
           session.trigger("streamCreated", { stream: fakeStream });
 
           sinon.assert.neverCalledWithMatch(dispatcher.dispatch,
             new sharedActions.ReceivingScreenShare({ receiving: true }));
         });
 
-      it("should dispatch a ReceivingScreenShare action for screen" +
-        " sharing streams", function() {
+        it("should dispatch a ReceivingScreenShare action for screen sharing streams", function() {
           fakeStream.videoType = "screen";
 
           session.trigger("streamCreated", { stream: fakeStream });
 
           // Called twice due to the VideoDimensionsChanged above.
           sinon.assert.called(dispatcher.dispatch);
           sinon.assert.calledWithExactly(dispatcher.dispatch,
             new sharedActions.ReceivingScreenShare({ receiving: true }));
         });
+      });
     });
 
     describe("streamDestroyed: publisher/local", function() {
       it("should dispatch a ConnectionStatus action", function() {
         driver._metrics.sendStreams = 1;
         driver._metrics.recvStreams = 1;
         driver._metrics.connections = 2;
 
@@ -1494,62 +1561,43 @@ describe("loop.OTSdkDriver", function() 
       });
 
       it("should get the data channel for the publisher", function() {
         session.trigger("signal:readyForDataChannel");
 
         sinon.assert.calledOnce(publisher._.getDataChannel);
       });
 
-      it("should get the data channel for the subscriber", function() {
-        session.trigger("signal:readyForDataChannel");
-
-        sinon.assert.calledOnce(subscriber._.getDataChannel);
-      });
-
-      it("should dispatch `DataChannelsAvailable` once both data channels have been obtained", function() {
+      it("should dispatch `DataChannelsAvailable` if the subscriber channel is setup", function() {
         var fakeChannel = _.extend({}, Backbone.Events);
 
-        subscriber._.getDataChannel.callsArgWith(2, null, fakeChannel);
+        driver._subscriberChannel = fakeChannel;
+
         publisher._.getDataChannel.callsArgWith(2, null, fakeChannel);
 
         session.trigger("signal:readyForDataChannel");
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithExactly(dispatcher.dispatch,
           new sharedActions.DataChannelsAvailable({
             available: true
           }));
       });
 
-      it("should dispatch `ReceivedTextChatMessage` when a text message is received", function() {
+      it("should not dispatch `DataChannelsAvailable` if the subscriber channel isn't setup", function() {
         var fakeChannel = _.extend({}, Backbone.Events);
-        var data = '{"contentType":"' + CHAT_CONTENT_TYPES.TEXT +
-                   '","message":"Are you there?","receivedTimestamp": "2015-06-25T00:29:14.197Z"}';
-        var clock = sinon.useFakeTimers();
 
-        subscriber._.getDataChannel.callsArgWith(2, null, fakeChannel);
+        publisher._.getDataChannel.callsArgWith(2, null, fakeChannel);
 
         session.trigger("signal:readyForDataChannel");
 
-        // Now send the message.
-        fakeChannel.trigger("message", {
-          data: data
-        });
-
-        sinon.assert.calledOnce(dispatcher.dispatch);
-        sinon.assert.calledWithExactly(dispatcher.dispatch,
-          new sharedActions.ReceivedTextChatMessage({
-            contentType: CHAT_CONTENT_TYPES.TEXT,
-            message: "Are you there?",
-            receivedTimestamp: "1970-01-01T00:00:00.000Z"
+        sinon.assert.neverCalledWith(dispatcher.dispatch,
+          new sharedActions.DataChannelsAvailable({
+            available: true
           }));
-
-        /* Restore the time. */
-        clock.restore();
       });
     });
 
     describe("exception", function() {
       it("should notify metrics", function() {
         sdk.trigger("exception", {
           code: OT.ExceptionCodes.CONNECT_FAILED,
           message: "Fake",