Bug 1079225 - Feedback form displayed for Loop standalone rooms. r=Standard8
authorNicolas Perriault <nperriault@mozilla.com>
Wed, 26 Nov 2014 18:55:28 +0000
changeset 217689 c6dc9a2f152c
parent 217688 21496e80b1fb
child 217690 bf5ee5924070
push id27887
push userryanvm@gmail.com
push dateThu, 27 Nov 2014 02:08:38 +0000
treeherdermozilla-central@c63e741bca2e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersStandard8
bugs1079225
milestone36.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1079225 - Feedback form displayed for Loop standalone rooms. r=Standard8
browser/components/loop/content/shared/css/conversation.css
browser/components/loop/content/shared/js/actions.js
browser/components/loop/content/shared/js/activeRoomStore.js
browser/components/loop/content/shared/js/feedbackViews.js
browser/components/loop/content/shared/js/feedbackViews.jsx
browser/components/loop/standalone/content/js/standaloneRoomViews.js
browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
browser/components/loop/standalone/content/js/webapp.js
browser/components/loop/standalone/content/js/webapp.jsx
browser/components/loop/test/shared/activeRoomStore_test.js
browser/components/loop/test/standalone/standaloneRoomViews_test.js
browser/components/loop/test/standalone/webapp_test.js
--- a/browser/components/loop/content/shared/css/conversation.css
+++ b/browser/components/loop/content/shared/css/conversation.css
@@ -842,8 +842,13 @@ html, .fx-embedded, #main,
 .standalone .room-conversation .conversation-toolbar {
   background: #000;
   border: none;
 }
 
 .standalone .room-conversation .conversation-toolbar .btn-hangup-entry {
   display: block;
 }
+
+.standalone .room-conversation-wrapper .ended-conversation {
+  position: relative;
+  height: auto;
+}
--- a/browser/components/loop/content/shared/js/actions.js
+++ b/browser/components/loop/content/shared/js/actions.js
@@ -326,16 +326,22 @@ loop.shared.actions = (function() {
     JoinedRoom: Action.define("joinedRoom", {
       apiKey: String,
       sessionToken: String,
       sessionId: String,
       expires: Number
     }),
 
     /**
+     * Resets current room.
+     */
+    ResetRoom: Action.define("resetRoom", {
+    }),
+
+    /**
      * Used to indicate the user wishes to leave the room.
      */
     LeaveRoom: Action.define("leaveRoom", {
     }),
 
     /**
      * Requires detailed information on sad feedback.
      */
--- a/browser/components/loop/content/shared/js/activeRoomStore.js
+++ b/browser/components/loop/content/shared/js/activeRoomStore.js
@@ -134,17 +134,18 @@ loop.store.ActiveRoomStore = (function()
         "joinRoom",
         "joinedRoom",
         "connectedToSdkServers",
         "connectionFailure",
         "setMute",
         "remotePeerDisconnected",
         "remotePeerConnected",
         "windowUnload",
-        "leaveRoom"
+        "leaveRoom",
+        "resetRoom"
       ]);
     },
 
     /**
      * Execute setupWindowData event action from the dispatcher. This gets
      * the room data from the mozLoop api, and dispatches an UpdateRoomInfo event.
      * It also dispatches JoinRoom as this action is only applicable to the desktop
      * client, and needs to auto-join.
@@ -357,33 +358,29 @@ loop.store.ActiveRoomStore = (function()
       muteState[actionData.type + "Muted"] = !actionData.enabled;
       this.setStoreState(muteState);
     },
 
     /**
      * Handles recording when a remote peer has connected to the servers.
      */
     remotePeerConnected: function() {
-      this.setStoreState({
-        roomState: ROOM_STATES.HAS_PARTICIPANTS
-      });
+      this.setStoreState({roomState: ROOM_STATES.HAS_PARTICIPANTS});
 
       // We've connected with a third-party, therefore stop displaying the ToS etc.
       this._mozLoop.setLoopPref("seenToS", "seen");
     },
 
     /**
-     * Handles a remote peer disconnecting from the session.
+     * Handles a remote peer disconnecting from the session. As we currently only
+     * support 2 participants, we declare the room as SESSION_CONNECTED as soon as
+     * one participantleaves.
      */
     remotePeerDisconnected: function() {
-      // As we only support two users at the moment, we just set this
-      // back to joined.
-      this.setStoreState({
-        roomState: ROOM_STATES.SESSION_CONNECTED
-      });
+      this.setStoreState({roomState: ROOM_STATES.SESSION_CONNECTED});
     },
 
     /**
      * Handles the window being unloaded. Ensures the room is left.
      */
     windowUnload: function() {
       this._leaveRoom();
 
@@ -447,16 +444,21 @@ loop.store.ActiveRoomStore = (function()
 
       if (this._storeState.roomState === ROOM_STATES.JOINED ||
           this._storeState.roomState === ROOM_STATES.SESSION_CONNECTED ||
           this._storeState.roomState === ROOM_STATES.HAS_PARTICIPANTS) {
         this._mozLoop.rooms.leave(this._storeState.roomToken,
           this._storeState.sessionToken);
       }
 
-      this.setStoreState({
-        roomState: nextState ? nextState : ROOM_STATES.ENDED
-      });
+      this.setStoreState({roomState: nextState || ROOM_STATES.ENDED});
+    },
+
+    /**
+     * Resets current room.
+     */
+    resetRoom: function() {
+      this.setStoreState(this.getInitialStoreState());
     }
   });
 
   return ActiveRoomStore;
 })();
--- a/browser/components/loop/content/shared/js/feedbackViews.js
+++ b/browser/components/loop/content/shared/js/feedbackViews.js
@@ -10,17 +10,18 @@ var loop = loop || {};
 loop.shared = loop.shared || {};
 loop.shared.views = loop.shared.views || {};
 loop.shared.views.FeedbackView = (function(l10n) {
   "use strict";
 
   var sharedActions = loop.shared.actions;
   var sharedMixins = loop.shared.mixins;
 
-  var WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS = 5;
+  var WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS =
+      loop.shared.views.WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS = 5;
   var FEEDBACK_STATES = loop.store.FEEDBACK_STATES;
 
   /**
    * Feedback outer layout.
    *
    * Props:
    * -
    */
--- a/browser/components/loop/content/shared/js/feedbackViews.jsx
+++ b/browser/components/loop/content/shared/js/feedbackViews.jsx
@@ -10,17 +10,18 @@ var loop = loop || {};
 loop.shared = loop.shared || {};
 loop.shared.views = loop.shared.views || {};
 loop.shared.views.FeedbackView = (function(l10n) {
   "use strict";
 
   var sharedActions = loop.shared.actions;
   var sharedMixins = loop.shared.mixins;
 
-  var WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS = 5;
+  var WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS =
+      loop.shared.views.WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS = 5;
   var FEEDBACK_STATES = loop.store.FEEDBACK_STATES;
 
   /**
    * Feedback outer layout.
    *
    * Props:
    * -
    */
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.js
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.js
@@ -14,17 +14,28 @@ loop.standaloneRoomViews = (function(moz
   var FAILURE_REASONS = loop.shared.utils.FAILURE_REASONS;
   var ROOM_STATES = loop.store.ROOM_STATES;
   var sharedActions = loop.shared.actions;
   var sharedMixins = loop.shared.mixins;
   var sharedViews = loop.shared.views;
 
   var StandaloneRoomInfoArea = React.createClass({displayName: 'StandaloneRoomInfoArea',
     propTypes: {
-      helper: React.PropTypes.instanceOf(loop.shared.utils.Helper).isRequired
+      helper: React.PropTypes.instanceOf(loop.shared.utils.Helper).isRequired,
+      activeRoomStore:
+        React.PropTypes.instanceOf(loop.store.ActiveRoomStore).isRequired,
+      feedbackStore:
+        React.PropTypes.instanceOf(loop.store.FeedbackStore).isRequired
+    },
+
+    onFeedbackSent: function() {
+      // We pass a tick to prevent React warnings regarding nested updates.
+      setTimeout(function() {
+        this.props.activeRoomStore.dispatchAction(new sharedActions.ResetRoom());
+      }.bind(this));
     },
 
     _renderCallToActionLink: function() {
       if (this.props.helper.isFirefox(navigator.userAgent)) {
         return (
           React.DOM.a({href: loop.config.learnMoreUrl, className: "btn btn-info"}, 
             mozL10n.get("rooms_room_full_call_to_action_label", {
               clientShortname: mozL10n.get("clientShortname2")
@@ -50,73 +61,83 @@ loop.standaloneRoomViews = (function(moz
           return mozL10n.get("rooms_media_denied_message");
         case FAILURE_REASONS.EXPIRED_OR_INVALID:
           return mozL10n.get("rooms_unavailable_notification_message");
         default:
           return mozL10n.get("status_error");
       }
     },
 
-    _renderContent: function() {
+    render: function() {
       switch(this.props.roomState) {
         case ROOM_STATES.INIT:
-        case ROOM_STATES.READY:
-        case ROOM_STATES.ENDED: {
+        case ROOM_STATES.READY: {
           // XXX: In ENDED state, we should rather display the feedback form.
           return (
-            React.DOM.button({className: "btn btn-join btn-info", 
-                    onClick: this.props.joinRoom}, 
-              mozL10n.get("rooms_room_join_label")
+            React.DOM.div({className: "room-inner-info-area"}, 
+              React.DOM.button({className: "btn btn-join btn-info", 
+                      onClick: this.props.joinRoom}, 
+                mozL10n.get("rooms_room_join_label")
+              )
             )
           );
         }
         case ROOM_STATES.MEDIA_WAIT: {
           var msg = mozL10n.get("call_progress_getting_media_description",
                                 {clientShortname: mozL10n.get("clientShortname2")});
           // XXX Bug 1047040 will add images to help prompt the user.
           return (
             React.DOM.p({className: "prompt-media-message"}, 
               msg
             )
           );
         }
         case ROOM_STATES.JOINED:
         case ROOM_STATES.SESSION_CONNECTED: {
           return (
-            React.DOM.p({className: "empty-room-message"}, 
-              mozL10n.get("rooms_only_occupant_label")
+            React.DOM.div({className: "room-inner-info-area"}, 
+              React.DOM.p({className: "empty-room-message"}, 
+                mozL10n.get("rooms_only_occupant_label")
+              )
             )
           );
         }
-        case ROOM_STATES.FULL:
+        case ROOM_STATES.FULL: {
           return (
-            React.DOM.div(null, 
+            React.DOM.div({className: "room-inner-info-area"}, 
               React.DOM.p({className: "full-room-message"}, 
                 mozL10n.get("rooms_room_full_label")
               ), 
               React.DOM.p(null, this._renderCallToActionLink())
             )
           );
-        case ROOM_STATES.FAILED:
+        }
+        case ROOM_STATES.ENDED: {
           return (
-            React.DOM.p({className: "failed-room-message"}, 
-              this._getFailureString()
+            React.DOM.div({className: "ended-conversation"}, 
+              sharedViews.FeedbackView({
+                feedbackStore: this.props.feedbackStore, 
+                onAfterFeedbackReceived: this.onFeedbackSent}
+              )
             )
           );
-        default:
+        }
+        case ROOM_STATES.FAILED: {
+          return (
+            React.DOM.div({className: "room-inner-info-area"}, 
+              React.DOM.p({className: "failed-room-message"}, 
+                this._getFailureString()
+              )
+            )
+          );
+        }
+        default: {
           return null;
+        }
       }
-    },
-
-    render: function() {
-      return (
-        React.DOM.div({className: "room-inner-info-area"}, 
-          this._renderContent()
-        )
-      );
     }
   });
 
   var StandaloneRoomHeader = React.createClass({displayName: 'StandaloneRoomHeader',
     render: function() {
       return (
         React.DOM.header(null, 
           React.DOM.h1(null, mozL10n.get("clientShortname2")), 
@@ -159,16 +180,18 @@ loop.standaloneRoomViews = (function(moz
     mixins: [
       Backbone.Events,
       sharedMixins.RoomsAudioMixin
     ],
 
     propTypes: {
       activeRoomStore:
         React.PropTypes.instanceOf(loop.store.ActiveRoomStore).isRequired,
+      feedbackStore:
+        React.PropTypes.instanceOf(loop.store.FeedbackStore).isRequired,
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       helper: React.PropTypes.instanceOf(loop.shared.utils.Helper).isRequired
     },
 
     getInitialState: function() {
       var storeState = this.props.activeRoomStore.getStoreState();
       return _.extend({}, storeState, {
         // Used by the UI showcase.
@@ -321,17 +344,19 @@ loop.standaloneRoomViews = (function(moz
       });
 
       return (
         React.DOM.div({className: "room-conversation-wrapper"}, 
           StandaloneRoomHeader(null), 
           StandaloneRoomInfoArea({roomState: this.state.roomState, 
                                   failureReason: this.state.failureReason, 
                                   joinRoom: this.joinRoom, 
-                                  helper: this.props.helper}), 
+                                  helper: this.props.helper, 
+                                  activeRoomStore: this.props.activeRoomStore, 
+                                  feedbackStore: this.props.feedbackStore}), 
           React.DOM.div({className: "video-layout-wrapper"}, 
             React.DOM.div({className: "conversation room-conversation"}, 
               React.DOM.h2({className: "room-name"}, this.state.roomName), 
               React.DOM.div({className: "media nested"}, 
                 React.DOM.div({className: "video_wrapper remote_wrapper"}, 
                   React.DOM.div({className: "video_inner remote"})
                 ), 
                 React.DOM.div({className: localStreamClasses})
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
@@ -14,17 +14,28 @@ loop.standaloneRoomViews = (function(moz
   var FAILURE_REASONS = loop.shared.utils.FAILURE_REASONS;
   var ROOM_STATES = loop.store.ROOM_STATES;
   var sharedActions = loop.shared.actions;
   var sharedMixins = loop.shared.mixins;
   var sharedViews = loop.shared.views;
 
   var StandaloneRoomInfoArea = React.createClass({
     propTypes: {
-      helper: React.PropTypes.instanceOf(loop.shared.utils.Helper).isRequired
+      helper: React.PropTypes.instanceOf(loop.shared.utils.Helper).isRequired,
+      activeRoomStore:
+        React.PropTypes.instanceOf(loop.store.ActiveRoomStore).isRequired,
+      feedbackStore:
+        React.PropTypes.instanceOf(loop.store.FeedbackStore).isRequired
+    },
+
+    onFeedbackSent: function() {
+      // We pass a tick to prevent React warnings regarding nested updates.
+      setTimeout(function() {
+        this.props.activeRoomStore.dispatchAction(new sharedActions.ResetRoom());
+      }.bind(this));
     },
 
     _renderCallToActionLink: function() {
       if (this.props.helper.isFirefox(navigator.userAgent)) {
         return (
           <a href={loop.config.learnMoreUrl} className="btn btn-info">
             {mozL10n.get("rooms_room_full_call_to_action_label", {
               clientShortname: mozL10n.get("clientShortname2")
@@ -50,73 +61,83 @@ loop.standaloneRoomViews = (function(moz
           return mozL10n.get("rooms_media_denied_message");
         case FAILURE_REASONS.EXPIRED_OR_INVALID:
           return mozL10n.get("rooms_unavailable_notification_message");
         default:
           return mozL10n.get("status_error");
       }
     },
 
-    _renderContent: function() {
+    render: function() {
       switch(this.props.roomState) {
         case ROOM_STATES.INIT:
-        case ROOM_STATES.READY:
-        case ROOM_STATES.ENDED: {
+        case ROOM_STATES.READY: {
           // XXX: In ENDED state, we should rather display the feedback form.
           return (
-            <button className="btn btn-join btn-info"
-                    onClick={this.props.joinRoom}>
-              {mozL10n.get("rooms_room_join_label")}
-            </button>
+            <div className="room-inner-info-area">
+              <button className="btn btn-join btn-info"
+                      onClick={this.props.joinRoom}>
+                {mozL10n.get("rooms_room_join_label")}
+              </button>
+            </div>
           );
         }
         case ROOM_STATES.MEDIA_WAIT: {
           var msg = mozL10n.get("call_progress_getting_media_description",
                                 {clientShortname: mozL10n.get("clientShortname2")});
           // XXX Bug 1047040 will add images to help prompt the user.
           return (
             <p className="prompt-media-message">
               {msg}
             </p>
           );
         }
         case ROOM_STATES.JOINED:
         case ROOM_STATES.SESSION_CONNECTED: {
           return (
-            <p className="empty-room-message">
-              {mozL10n.get("rooms_only_occupant_label")}
-            </p>
+            <div className="room-inner-info-area">
+              <p className="empty-room-message">
+                {mozL10n.get("rooms_only_occupant_label")}
+              </p>
+            </div>
           );
         }
-        case ROOM_STATES.FULL:
+        case ROOM_STATES.FULL: {
           return (
-            <div>
+            <div className="room-inner-info-area">
               <p className="full-room-message">
                 {mozL10n.get("rooms_room_full_label")}
               </p>
               <p>{this._renderCallToActionLink()}</p>
             </div>
           );
-        case ROOM_STATES.FAILED:
+        }
+        case ROOM_STATES.ENDED: {
           return (
-            <p className="failed-room-message">
-              {this._getFailureString()}
-            </p>
+            <div className="ended-conversation">
+              <sharedViews.FeedbackView
+                feedbackStore={this.props.feedbackStore}
+                onAfterFeedbackReceived={this.onFeedbackSent}
+              />
+            </div>
           );
-        default:
+        }
+        case ROOM_STATES.FAILED: {
+          return (
+            <div className="room-inner-info-area">
+              <p className="failed-room-message">
+                {this._getFailureString()}
+              </p>
+            </div>
+          );
+        }
+        default: {
           return null;
+        }
       }
-    },
-
-    render: function() {
-      return (
-        <div className="room-inner-info-area">
-          {this._renderContent()}
-        </div>
-      );
     }
   });
 
   var StandaloneRoomHeader = React.createClass({
     render: function() {
       return (
         <header>
           <h1>{mozL10n.get("clientShortname2")}</h1>
@@ -159,16 +180,18 @@ loop.standaloneRoomViews = (function(moz
     mixins: [
       Backbone.Events,
       sharedMixins.RoomsAudioMixin
     ],
 
     propTypes: {
       activeRoomStore:
         React.PropTypes.instanceOf(loop.store.ActiveRoomStore).isRequired,
+      feedbackStore:
+        React.PropTypes.instanceOf(loop.store.FeedbackStore).isRequired,
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       helper: React.PropTypes.instanceOf(loop.shared.utils.Helper).isRequired
     },
 
     getInitialState: function() {
       var storeState = this.props.activeRoomStore.getStoreState();
       return _.extend({}, storeState, {
         // Used by the UI showcase.
@@ -321,17 +344,19 @@ loop.standaloneRoomViews = (function(moz
       });
 
       return (
         <div className="room-conversation-wrapper">
           <StandaloneRoomHeader />
           <StandaloneRoomInfoArea roomState={this.state.roomState}
                                   failureReason={this.state.failureReason}
                                   joinRoom={this.joinRoom}
-                                  helper={this.props.helper} />
+                                  helper={this.props.helper}
+                                  activeRoomStore={this.props.activeRoomStore}
+                                  feedbackStore={this.props.feedbackStore} />
           <div className="video-layout-wrapper">
             <div className="conversation room-conversation">
               <h2 className="room-name">{this.state.roomName}</h2>
               <div className="media nested">
                 <div className="video_wrapper remote_wrapper">
                   <div className="video_inner remote"></div>
                 </div>
                 <div className={localStreamClasses}></div>
--- a/browser/components/loop/standalone/content/js/webapp.js
+++ b/browser/components/loop/standalone/content/js/webapp.js
@@ -1005,16 +1005,17 @@ loop.webapp = (function($, _, OT, mozL10
                feedbackStore: this.props.feedbackStore}
             )
           );
         }
         case "room": {
           return (
             loop.standaloneRoomViews.StandaloneRoomView({
               activeRoomStore: this.props.activeRoomStore, 
+              feedbackStore: this.props.feedbackStore, 
               dispatcher: this.props.dispatcher, 
               helper: this.props.helper}
             )
           );
         }
         case "home": {
           return HomeView(null);
         }
--- a/browser/components/loop/standalone/content/js/webapp.jsx
+++ b/browser/components/loop/standalone/content/js/webapp.jsx
@@ -1005,16 +1005,17 @@ loop.webapp = (function($, _, OT, mozL10
                feedbackStore={this.props.feedbackStore}
             />
           );
         }
         case "room": {
           return (
             <loop.standaloneRoomViews.StandaloneRoomView
               activeRoomStore={this.props.activeRoomStore}
+              feedbackStore={this.props.feedbackStore}
               dispatcher={this.props.dispatcher}
               helper={this.props.helper}
             />
           );
         }
         case "home": {
           return <HomeView />;
         }
--- a/browser/components/loop/test/shared/activeRoomStore_test.js
+++ b/browser/components/loop/test/shared/activeRoomStore_test.js
@@ -259,16 +259,32 @@ describe("loop.store.ActiveRoomStore", f
         windowType: "room",
         token: "fakeToken"
       }));
 
       expect(store.getStoreState().roomState).eql(ROOM_STATES.READY);
     });
   });
 
+  describe("#resetRoom", function() {
+    it("should reset the room store state", function() {
+      var initialState = store.getInitialStoreState();
+      store.setStoreState({
+        roomState: ROOM_STATES.ENDED,
+        audioMuted: true,
+        videoMuted: true,
+        failureReason: "foo"
+      });
+
+      store.resetRoom(new sharedActions.ResetRoom());
+
+      expect(store.getStoreState()).eql(initialState);
+    });
+  });
+
   describe("#setupRoomInfo", function() {
     var fakeRoomInfo;
 
     beforeEach(function() {
       fakeRoomInfo = {
         roomName: "Its a room",
         roomOwner: "Me",
         roomToken: "fakeToken",
--- a/browser/components/loop/test/standalone/standaloneRoomViews_test.js
+++ b/browser/components/loop/test/standalone/standaloneRoomViews_test.js
@@ -5,40 +5,50 @@
 /* global loop, sinon */
 
 var expect = chai.expect;
 
 describe("loop.standaloneRoomViews", function() {
   "use strict";
 
   var ROOM_STATES = loop.store.ROOM_STATES;
+  var FEEDBACK_STATES = loop.store.FEEDBACK_STATES;
   var sharedActions = loop.shared.actions;
 
-  var sandbox, dispatcher, activeRoomStore, dispatch;
+  var sandbox, dispatcher, activeRoomStore, feedbackStore, dispatch;
 
   beforeEach(function() {
     sandbox = sinon.sandbox.create();
     dispatcher = new loop.Dispatcher();
     dispatch = sandbox.stub(dispatcher, "dispatch");
     activeRoomStore = new loop.store.ActiveRoomStore(dispatcher, {
       mozLoop: {},
       sdkDriver: {}
     });
+    feedbackStore = new loop.store.FeedbackStore(dispatcher, {
+      feedbackClient: {}
+    });
+
+    sandbox.useFakeTimers();
+
+    // Prevents audio request errors in the test console.
+    sandbox.useFakeXMLHttpRequest();
   });
 
   afterEach(function() {
     sandbox.restore();
   });
 
-  describe("standaloneRoomView", function() {
+  describe("StandaloneRoomView", function() {
     function mountTestComponent() {
       return TestUtils.renderIntoDocument(
         loop.standaloneRoomViews.StandaloneRoomView({
           dispatcher: dispatcher,
           activeRoomStore: activeRoomStore,
+          feedbackStore: feedbackStore,
           helper: new loop.shared.utils.Helper()
         }));
     }
 
     function expectActionDispatched(view) {
       sinon.assert.calledOnce(dispatch);
       sinon.assert.calledWithExactly(dispatch,
         sinon.match.instanceOf(sharedActions.SetupStreamElements));
@@ -274,11 +284,29 @@ describe("loop.standaloneRoomViews", fun
           activeRoomStore.setStoreState({roomState: ROOM_STATES.HAS_PARTICIPANTS});
 
           TestUtils.Simulate.click(getLeaveButton(view));
 
           sinon.assert.calledOnce(dispatch);
           sinon.assert.calledWithExactly(dispatch, new sharedActions.LeaveRoom());
         });
       });
+
+      describe("Feedback", function() {
+        it("should display a feedback form when the user leaves the room",
+          function() {
+            activeRoomStore.setStoreState({roomState: ROOM_STATES.ENDED});
+
+            expect(view.getDOMNode().querySelector(".faces")).not.eql(null);
+          });
+
+        it("should reinit the view after feedback is sent", function() {
+          feedbackStore.setStoreState({feedbackState: FEEDBACK_STATES.SENT});
+
+          sandbox.clock.tick(
+            loop.shared.views.WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS * 1000);
+
+          expect(view.getDOMNode().querySelector(".btn-join")).not.eql(null);
+        });
+      });
     });
   });
 });
--- a/browser/components/loop/test/standalone/webapp_test.js
+++ b/browser/components/loop/test/standalone/webapp_test.js
@@ -12,29 +12,25 @@ describe("loop.webapp", function() {
 
   var sharedActions = loop.shared.actions;
   var sharedModels = loop.shared.models,
       sharedViews = loop.shared.views,
       sharedUtils = loop.shared.utils,
       standaloneMedia = loop.standaloneMedia,
       sandbox,
       notifications,
-      feedbackApiClient,
       stubGetPermsAndCacheMedia,
       fakeAudioXHR,
       dispatcher,
       feedbackStore;
 
   beforeEach(function() {
     sandbox = sinon.sandbox.create();
     dispatcher = new loop.Dispatcher();
     notifications = new sharedModels.NotificationCollection();
-    feedbackApiClient = new loop.FeedbackAPIClient("http://invalid", {
-      product: "Loop"
-    });
     feedbackStore = new loop.store.FeedbackStore(dispatcher, {
       feedbackClient: {}
     });
 
     stubGetPermsAndCacheMedia = sandbox.stub(
       loop.standaloneMedia._MultiplexGum.prototype, "getPermsAndCacheMedia");
 
     fakeAudioXHR = {
@@ -645,25 +641,25 @@ describe("loop.webapp", function() {
 
   describe("WebappRootView", function() {
     var helper, sdk, conversationModel, client, props, standaloneAppStore;
     var activeRoomStore;
 
     function mountTestComponent() {
       return TestUtils.renderIntoDocument(
         loop.webapp.WebappRootView({
-        client: client,
-        helper: helper,
-        notifications: notifications,
-        sdk: sdk,
-        conversation: conversationModel,
-        feedbackApiClient: feedbackApiClient,
-        standaloneAppStore: standaloneAppStore,
-        activeRoomStore: activeRoomStore
-      }));
+          client: client,
+          helper: helper,
+          notifications: notifications,
+          sdk: sdk,
+          conversation: conversationModel,
+          standaloneAppStore: standaloneAppStore,
+          activeRoomStore: activeRoomStore,
+          feedbackStore: feedbackStore
+        }));
     }
 
     beforeEach(function() {
       helper = new sharedUtils.Helper();
       sdk = {
         checkSystemRequirements: function() { return true; }
       };
       conversationModel = new sharedModels.ConversationModel({}, {