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 217526 c6dc9a2f152c
parent 217525 21496e80b1fb
child 217527 bf5ee5924070
push id10173
push usermbanner@mozilla.com
push dateWed, 26 Nov 2014 18:56:21 +0000
treeherderfx-team@c6dc9a2f152c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersStandard8
bugs1079225
milestone36.0a1
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({}, {