Bug 1074694 - Allow rooms to be renamed from the conversation window. r=nperriault a=lsblakk
authorMark Banner <standard8@mozilla.com>
Mon, 17 Nov 2014 22:12:27 +0000
changeset 235289 a504347b83b1fd61d7d2285c712b97e099f19398
parent 235288 9a750b1e767524b3ebf0359b9117a05131f6458c
child 235290 107c64f93febe8a9ae93c7f8ec49483618b74f3b
push id611
push userraliiev@mozilla.com
push dateMon, 05 Jan 2015 23:23:16 +0000
treeherdermozilla-release@345cd3b9c445 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnperriault, lsblakk
bugs1074694
milestone35.0a2
Bug 1074694 - Allow rooms to be renamed from the conversation window. r=nperriault a=lsblakk
browser/components/loop/LoopRooms.jsm
browser/components/loop/content/js/roomViews.js
browser/components/loop/content/js/roomViews.jsx
browser/components/loop/content/shared/js/actions.js
browser/components/loop/content/shared/js/activeRoomStore.js
browser/components/loop/content/shared/js/roomStore.js
browser/components/loop/standalone/content/js/standaloneMozLoop.js
browser/components/loop/test/desktop-local/roomViews_test.js
browser/components/loop/test/shared/activeRoomStore_test.js
browser/components/loop/test/shared/roomStore_test.js
browser/components/loop/test/xpcshell/test_looprooms.js
--- a/browser/components/loop/LoopRooms.jsm
+++ b/browser/components/loop/LoopRooms.jsm
@@ -372,16 +372,44 @@ let LoopRoomsInternal = {
     }
     this._postToRoom(roomToken, {
       action: "leave",
       sessionToken: sessionToken
     }, callback);
   },
 
   /**
+   * Renames a room.
+   *
+   * @param {String} roomToken   The room token
+   * @param {String} newRoomName The new name for the room
+   * @param {Function} callback   Function that will be invoked once the operation
+   *                              finished. The first argument passed will be an
+   *                              `Error` object or `null`.
+   */
+  rename: function(roomToken, newRoomName, callback) {
+    let room = this.rooms.get(roomToken);
+    let url = "/rooms/" + encodeURIComponent(roomToken);
+
+    let origRoom = this.rooms.get(roomToken);
+    let patchData = {
+      roomName: newRoomName,
+      // XXX We have to supply the max size and room owner due to bug 1099063.
+      maxSize: origRoom.maxSize,
+      roomOwner: origRoom.roomOwner
+    };
+    MozLoopService.hawkRequest(this.sessionType, url, "PATCH", patchData)
+      .then(response => {
+        let data = JSON.parse(response.body);
+        extend(room, data);
+        callback(null, room);
+      }, error => callback(error)).catch(error => callback(error));
+  },
+
+  /**
    * Callback used to indicate changes to rooms data on the LoopServer.
    *
    * @param {String} version   Version number assigned to this change set.
    * @param {String} channelID Notification channel identifier.
    */
   onNotification: function(version, channelID) {
     gDirty = true;
     this.getAll(version, () => {});
@@ -438,16 +466,20 @@ this.LoopRooms = {
     return LoopRoomsInternal.refreshMembership(roomToken, sessionToken,
       callback);
   },
 
   leave: function(roomToken, sessionToken, callback) {
     return LoopRoomsInternal.leave(roomToken, sessionToken, callback);
   },
 
+  rename: function(roomToken, newRoomName, callback) {
+    return LoopRoomsInternal.rename(roomToken, newRoomName, callback);
+  },
+
   promise: function(method, ...params) {
     return new Promise((resolve, reject) => {
       this[method](...params, (error, result) => {
         if (error) {
           reject(error);
         } else {
           resolve(result);
         }
--- a/browser/components/loop/content/js/roomViews.js
+++ b/browser/components/loop/content/js/roomViews.js
@@ -54,31 +54,41 @@ loop.roomViews = (function(mozL10n) {
       }, storeState);
     }
   };
 
   /**
    * Desktop room invitation view (overlay).
    */
   var DesktopRoomInvitationView = React.createClass({displayName: 'DesktopRoomInvitationView',
-    mixins: [ActiveRoomStoreMixin],
+    mixins: [ActiveRoomStoreMixin, React.addons.LinkedStateMixin],
 
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
     },
 
     getInitialState: function() {
       return {
-        copiedUrl: false
+        copiedUrl: false,
+        newRoomName: ""
       }
     },
 
     handleFormSubmit: function(event) {
       event.preventDefault();
-      // XXX
+
+      var newRoomName = this.state.newRoomName;
+
+      if (newRoomName && this.state.roomName != newRoomName) {
+        this.props.dispatcher.dispatch(
+          new sharedActions.RenameRoom({
+            roomToken: this.state.roomToken,
+            newRoomName: newRoomName
+          }));
+      }
     },
 
     handleEmailButtonClick: function(event) {
       event.preventDefault();
 
       this.props.dispatcher.dispatch(
         new sharedActions.EmailRoomUrl({roomUrl: this.state.roomUrl}));
     },
@@ -91,17 +101,19 @@ loop.roomViews = (function(mozL10n) {
 
       this.setState({copiedUrl: true});
     },
 
     render: function() {
       return (
         React.DOM.div({className: "room-invitation-overlay"}, 
           React.DOM.form({onSubmit: this.handleFormSubmit}, 
-            React.DOM.input({type: "text", ref: "roomName", 
+            React.DOM.input({type: "text", className: "input-room-name", 
+              valueLink: this.linkState("newRoomName"), 
+              onBlur: this.handleFormSubmit, 
               placeholder: mozL10n.get("rooms_name_this_room_label")})
           ), 
           React.DOM.p(null, mozL10n.get("invite_header_text")), 
           React.DOM.div({className: "btn-group call-action-group"}, 
             React.DOM.button({className: "btn btn-info btn-email", 
                     onClick: this.handleEmailButtonClick}, 
               mozL10n.get("share_button2")
             ), 
--- a/browser/components/loop/content/js/roomViews.jsx
+++ b/browser/components/loop/content/js/roomViews.jsx
@@ -54,31 +54,41 @@ loop.roomViews = (function(mozL10n) {
       }, storeState);
     }
   };
 
   /**
    * Desktop room invitation view (overlay).
    */
   var DesktopRoomInvitationView = React.createClass({
-    mixins: [ActiveRoomStoreMixin],
+    mixins: [ActiveRoomStoreMixin, React.addons.LinkedStateMixin],
 
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
     },
 
     getInitialState: function() {
       return {
-        copiedUrl: false
+        copiedUrl: false,
+        newRoomName: ""
       }
     },
 
     handleFormSubmit: function(event) {
       event.preventDefault();
-      // XXX
+
+      var newRoomName = this.state.newRoomName;
+
+      if (newRoomName && this.state.roomName != newRoomName) {
+        this.props.dispatcher.dispatch(
+          new sharedActions.RenameRoom({
+            roomToken: this.state.roomToken,
+            newRoomName: newRoomName
+          }));
+      }
     },
 
     handleEmailButtonClick: function(event) {
       event.preventDefault();
 
       this.props.dispatcher.dispatch(
         new sharedActions.EmailRoomUrl({roomUrl: this.state.roomUrl}));
     },
@@ -91,17 +101,19 @@ loop.roomViews = (function(mozL10n) {
 
       this.setState({copiedUrl: true});
     },
 
     render: function() {
       return (
         <div className="room-invitation-overlay">
           <form onSubmit={this.handleFormSubmit}>
-            <input type="text" ref="roomName"
+            <input type="text" className="input-room-name"
+              valueLink={this.linkState("newRoomName")}
+              onBlur={this.handleFormSubmit}
               placeholder={mozL10n.get("rooms_name_this_room_label")} />
           </form>
           <p>{mozL10n.get("invite_header_text")}</p>
           <div className="btn-group call-action-group">
             <button className="btn btn-info btn-email"
                     onClick={this.handleEmailButtonClick}>
               {mozL10n.get("share_button2")}
             </button>
--- a/browser/components/loop/content/shared/js/actions.js
+++ b/browser/components/loop/content/shared/js/actions.js
@@ -238,16 +238,25 @@ loop.shared.actions = (function() {
      * Opens a room.
      * XXX: should move to some roomActions module - refs bug 1079284
      */
     OpenRoom: Action.define("openRoom", {
       roomToken: String
     }),
 
     /**
+     * Renames a room.
+     * XXX: should move to some roomActions module - refs bug 1079284
+     */
+    RenameRoom: Action.define("renameRoom", {
+      roomToken: String,
+      newRoomName: String
+    }),
+
+    /**
      * Copy a room url into the user's clipboard.
      * XXX: should move to some roomActions module - refs bug 1079284
      */
     CopyRoomUrl: Action.define("copyRoomUrl", {
       roomUrl: String
     }),
 
     /**
@@ -261,25 +270,37 @@ loop.shared.actions = (function() {
     /**
      * XXX: should move to some roomActions module - refs bug 1079284
      */
     RoomFailure: Action.define("roomFailure", {
       error: Object
     }),
 
     /**
+     * Sets up the room information when it is received.
+     * XXX: should move to some roomActions module - refs bug 1079284
+     *
+     * @see https://wiki.mozilla.org/Loop/Architecture/Rooms#GET_.2Frooms.2F.7Btoken.7D
+     */
+    SetupRoomInfo: Action.define("setupRoomInfo", {
+      roomName: String,
+      roomOwner: String,
+      roomToken: String,
+      roomUrl: String
+    }),
+
+    /**
      * Updates the room information when it is received.
      * XXX: should move to some roomActions module - refs bug 1079284
      *
      * @see https://wiki.mozilla.org/Loop/Architecture/Rooms#GET_.2Frooms.2F.7Btoken.7D
      */
     UpdateRoomInfo: Action.define("updateRoomInfo", {
       roomName: String,
       roomOwner: String,
-      roomToken: String,
       roomUrl: String
     }),
 
     /**
      * Starts the process for the user to join the room.
      * XXX: should move to some roomActions module - refs bug 1079284
      */
     JoinRoom: Action.define("joinRoom", {
--- a/browser/components/loop/content/shared/js/activeRoomStore.js
+++ b/browser/components/loop/content/shared/js/activeRoomStore.js
@@ -145,16 +145,17 @@ loop.store.ActiveRoomStore = (function()
 
     /**
      * Registers the actions with the dispatcher that this store is interested
      * in.
      */
     _registerActions: function() {
       this._dispatcher.register(this, [
         "roomFailure",
+        "setupRoomInfo",
         "updateRoomInfo",
         "joinRoom",
         "joinedRoom",
         "connectedToSdkServers",
         "connectionFailure",
         "setMute",
         "remotePeerDisconnected",
         "remotePeerConnected",
@@ -189,22 +190,22 @@ loop.store.ActiveRoomStore = (function()
           if (error) {
             this._dispatcher.dispatch(new sharedActions.RoomFailure({
               error: error
             }));
             return;
           }
 
           this._dispatcher.dispatch(
-            new sharedActions.UpdateRoomInfo({
-            roomToken: actionData.roomToken,
-            roomName: roomData.roomName,
-            roomOwner: roomData.roomOwner,
-            roomUrl: roomData.roomUrl
-          }));
+            new sharedActions.SetupRoomInfo({
+              roomToken: actionData.roomToken,
+              roomName: roomData.roomName,
+              roomOwner: roomData.roomOwner,
+              roomUrl: roomData.roomUrl
+            }));
 
           // For the conversation window, we need to automatically
           // join the room.
           this._dispatcher.dispatch(new sharedActions.JoinRoom());
         }.bind(this));
     },
 
     /**
@@ -222,35 +223,68 @@ loop.store.ActiveRoomStore = (function()
       }
 
       this._registerActions();
 
       this.setStoreState({
         roomToken: actionData.token,
         roomState: ROOM_STATES.READY
       });
+
+      this._mozLoop.rooms.on("update:" + actionData.roomToken,
+        this._handleRoomUpdate.bind(this));
     },
 
     /**
-     * Handles the updateRoomInfo action. Updates the room data and
+     * Handles the setupRoomInfo action. Sets up the initial room data and
      * sets the state to `READY`.
      *
+     * @param {sharedActions.SetupRoomInfo} actionData
+     */
+    setupRoomInfo: function(actionData) {
+      this.setStoreState({
+        roomName: actionData.roomName,
+        roomOwner: actionData.roomOwner,
+        roomState: ROOM_STATES.READY,
+        roomToken: actionData.roomToken,
+        roomUrl: actionData.roomUrl
+      });
+
+      this._mozLoop.rooms.on("update:" + actionData.roomToken,
+        this._handleRoomUpdate.bind(this));
+    },
+
+    /**
+     * Handles the updateRoomInfo action. Updates the room data.
+     *
      * @param {sharedActions.UpdateRoomInfo} actionData
      */
     updateRoomInfo: function(actionData) {
       this.setStoreState({
         roomName: actionData.roomName,
         roomOwner: actionData.roomOwner,
-        roomState: ROOM_STATES.READY,
-        roomToken: actionData.roomToken,
         roomUrl: actionData.roomUrl
       });
     },
 
     /**
+     * Handles room updates notified by the mozLoop rooms API.
+     *
+     * @param {String} eventName The name of the event
+     * @param {Object} roomData  The new roomData.
+     */
+    _handleRoomUpdate: function(eventName, roomData) {
+      this._dispatcher.dispatch(new sharedActions.UpdateRoomInfo({
+        roomName: roomData.roomName,
+        roomOwner: roomData.roomOwner,
+        roomUrl: roomData.roomUrl
+      }));
+    },
+
+    /**
      * Handles the action to join to a room.
      */
     joinRoom: function() {
       // Reset the failure reason if necessary.
       if (this.getStoreState().failureReason) {
         this.setStoreState({failureReason: undefined});
       }
 
@@ -346,16 +380,20 @@ loop.store.ActiveRoomStore = (function()
       });
     },
 
     /**
      * Handles the window being unloaded. Ensures the room is left.
      */
     windowUnload: function() {
       this._leaveRoom();
+
+      // If we're closing the window, we can stop listening to updates.
+      this._mozLoop.rooms.off("update:" + this.getStoreState().roomToken,
+        this._handleRoomUpdate.bind(this));
     },
 
     /**
      * Handles a room being left.
      */
     leaveRoom: function() {
       this._leaveRoom();
     },
--- a/browser/components/loop/content/shared/js/roomStore.js
+++ b/browser/components/loop/content/shared/js/roomStore.js
@@ -82,16 +82,17 @@ loop.store = loop.store || {};
       "createRoomError",
       "copyRoomUrl",
       "deleteRoom",
       "deleteRoomError",
       "emailRoomUrl",
       "getAllRooms",
       "getAllRoomsError",
       "openRoom",
+      "renameRoom",
       "updateRoomList"
     ]);
   }
 
   RoomStore.prototype = _.extend({
     /**
      * Maximum size given to createRoom; only 2 is supported (and is
      * always passed) because that's what the user-experience is currently
@@ -406,13 +407,28 @@ loop.store = loop.store || {};
 
     /**
      * Opens a room
      *
      * @param {sharedActions.OpenRoom} actionData The action data.
      */
     openRoom: function(actionData) {
       this._mozLoop.rooms.open(actionData.roomToken);
+    },
+
+    /**
+     * Renames a room.
+     *
+     * @param {sharedActions.RenameRoom} actionData
+     */
+    renameRoom: function(actionData) {
+      this._mozLoop.rooms.rename(actionData.roomToken, actionData.newRoomName,
+        function(err) {
+          if (err) {
+            // XXX Give this a proper UI - bug 1100595.
+            console.error("Failed to rename the room", err);
+          }
+        });
     }
   }, Backbone.Events);
 
   loop.store.RoomStore = RoomStore;
 })();
--- a/browser/components/loop/standalone/content/js/standaloneMozLoop.js
+++ b/browser/components/loop/standalone/content/js/standaloneMozLoop.js
@@ -165,17 +165,23 @@ loop.StandaloneMozLoop = (function(mozL1
           }
         };
       }
 
       this._postToRoom(roomToken, sessionToken, {
         action: "leave",
         sessionToken: sessionToken
       }, null, callback);
-    }
+    },
+
+    // Dummy functions to reflect those in the desktop mozLoop.rooms that we
+    // don't currently use.
+    on: function() {},
+    once: function() {},
+    off: function() {}
   };
 
   var StandaloneMozLoop = function(options) {
     options = options || {};
     if (!options.baseServerUrl) {
       throw new Error("missing required baseServerUrl");
     }
 
--- a/browser/components/loop/test/desktop-local/roomViews_test.js
+++ b/browser/components/loop/test/desktop-local/roomViews_test.js
@@ -114,16 +114,58 @@ describe("loop.roomViews", function () {
 
         React.addons.TestUtils.Simulate.click(emailBtn);
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWith(dispatcher.dispatch,
           new sharedActions.EmailRoomUrl({roomUrl: "http://invalid"}));
       });
 
+    describe("Rename Room", function() {
+      var roomNameBox;
+
+      beforeEach(function() {
+        view = mountTestComponent();
+        view.setState({
+          roomToken: "fakeToken",
+          roomName: "fakeName"
+        });
+
+        roomNameBox = view.getDOMNode().querySelector('.input-room-name');
+
+        React.addons.TestUtils.Simulate.change(roomNameBox, { target: {
+          value: "reallyFake"
+        }});
+      });
+
+      it("should dispatch a RenameRoom action when the focus is lost",
+        function() {
+          React.addons.TestUtils.Simulate.blur(roomNameBox);
+
+          sinon.assert.calledOnce(dispatcher.dispatch);
+          sinon.assert.calledWithExactly(dispatcher.dispatch,
+            new sharedActions.RenameRoom({
+              roomToken: "fakeToken",
+              newRoomName: "reallyFake"
+            }));
+        });
+
+      it("should dispatch a RenameRoom action when enter is pressed",
+        function() {
+          React.addons.TestUtils.Simulate.submit(roomNameBox);
+
+          sinon.assert.calledOnce(dispatcher.dispatch);
+          sinon.assert.calledWithExactly(dispatcher.dispatch,
+            new sharedActions.RenameRoom({
+              roomToken: "fakeToken",
+              newRoomName: "reallyFake"
+            }));
+        });
+    });
+
     describe("Copy Button", function() {
       beforeEach(function() {
         view = mountTestComponent();
 
         view.setState({roomUrl: "http://invalid"});
       });
 
       it("should dispatch a CopyRoomUrl action when the copy button is " +
--- a/browser/components/loop/test/shared/activeRoomStore_test.js
+++ b/browser/components/loop/test/shared/activeRoomStore_test.js
@@ -16,20 +16,22 @@ describe("loop.store.ActiveRoomStore", f
     sandbox = sinon.sandbox.create();
     sandbox.useFakeTimers();
 
     dispatcher = new loop.Dispatcher();
     sandbox.stub(dispatcher, "dispatch");
 
     fakeMozLoop = {
       rooms: {
-        get: sandbox.stub(),
-        join: sandbox.stub(),
-        refreshMembership: sandbox.stub(),
-        leave: sandbox.stub()
+        get: sinon.stub(),
+        join: sinon.stub(),
+        refreshMembership: sinon.stub(),
+        leave: sinon.stub(),
+        on: sinon.stub(),
+        off: sinon.stub()
       }
     };
 
     fakeSdkDriver = {
       connectSession: sandbox.stub(),
       disconnectSession: sandbox.stub()
     };
 
@@ -156,27 +158,27 @@ describe("loop.store.ActiveRoomStore", f
           type: "room",
           roomToken: fakeToken
         }));
 
         expect(store.getStoreState()).
           to.have.property('roomState', ROOM_STATES.GATHER);
       });
 
-    it("should dispatch an UpdateRoomInfo action if the get is successful",
+    it("should dispatch an SetupRoomInfo action if the get is successful",
       function() {
         store.setupWindowData(new sharedActions.SetupWindowData({
           windowId: "42",
           type: "room",
           roomToken: fakeToken
         }));
 
         sinon.assert.calledTwice(dispatcher.dispatch);
         sinon.assert.calledWithExactly(dispatcher.dispatch,
-          new sharedActions.UpdateRoomInfo(_.extend({
+          new sharedActions.SetupRoomInfo(_.extend({
             roomToken: fakeToken
           }, fakeRoomData)));
       });
 
     it("should dispatch a JoinRoom action if the get is successful",
       function() {
         store.setupWindowData(new sharedActions.SetupWindowData({
           windowId: "42",
@@ -228,41 +230,62 @@ describe("loop.store.ActiveRoomStore", f
         windowType: "room",
         token: "fakeToken"
       }));
 
       expect(store.getStoreState().roomState).eql(ROOM_STATES.READY);
     });
   });
 
-  describe("#updateRoomInfo", function() {
+  describe("#setupRoomInfo", function() {
     var fakeRoomInfo;
 
     beforeEach(function() {
       fakeRoomInfo = {
         roomName: "Its a room",
         roomOwner: "Me",
         roomToken: "fakeToken",
         roomUrl: "http://invalid"
       };
     });
 
     it("should set the state to READY", function() {
-      store.updateRoomInfo(new sharedActions.UpdateRoomInfo(fakeRoomInfo));
+      store.setupRoomInfo(new sharedActions.SetupRoomInfo(fakeRoomInfo));
 
       expect(store._storeState.roomState).eql(ROOM_STATES.READY);
     });
 
     it("should save the room information", function() {
+      store.setupRoomInfo(new sharedActions.SetupRoomInfo(fakeRoomInfo));
+
+      var state = store.getStoreState();
+      expect(state.roomName).eql(fakeRoomInfo.roomName);
+      expect(state.roomOwner).eql(fakeRoomInfo.roomOwner);
+      expect(state.roomToken).eql(fakeRoomInfo.roomToken);
+      expect(state.roomUrl).eql(fakeRoomInfo.roomUrl);
+    });
+  });
+
+  describe("#updateRoomInfo", function() {
+    var fakeRoomInfo;
+
+    beforeEach(function() {
+      fakeRoomInfo = {
+        roomName: "Its a room",
+        roomOwner: "Me",
+        roomUrl: "http://invalid"
+      };
+    });
+
+    it("should save the room information", function() {
       store.updateRoomInfo(new sharedActions.UpdateRoomInfo(fakeRoomInfo));
 
       var state = store.getStoreState();
       expect(state.roomName).eql(fakeRoomInfo.roomName);
       expect(state.roomOwner).eql(fakeRoomInfo.roomOwner);
-      expect(state.roomToken).eql(fakeRoomInfo.roomToken);
       expect(state.roomUrl).eql(fakeRoomInfo.roomUrl);
     });
   });
 
   describe("#joinRoom", function() {
     beforeEach(function() {
       store.setStoreState({roomToken: "tokenFake"});
     });
@@ -591,9 +614,38 @@ describe("loop.store.ActiveRoomStore", f
     });
 
     it("should set the state to ready", function() {
       store.leaveRoom();
 
       expect(store._storeState.roomState).eql(ROOM_STATES.READY);
     });
   });
+
+  describe("Events", function() {
+    describe("update:{roomToken}", function() {
+      beforeEach(function() {
+        store.setupRoomInfo(new sharedActions.SetupRoomInfo({
+          roomName: "Its a room",
+          roomOwner: "Me",
+          roomToken: "fakeToken",
+          roomUrl: "http://invalid"
+        }));
+      });
+
+      it("should dispatch an UpdateRoomInfo action", function() {
+        sinon.assert.calledOnce(fakeMozLoop.rooms.on);
+
+        var fakeRoomData = {
+          roomName: "fakeName",
+          roomOwner: "you",
+          roomUrl: "original"
+        };
+
+        fakeMozLoop.rooms.on.callArgWith(1, "update", fakeRoomData);
+
+        sinon.assert.calledOnce(dispatcher.dispatch);
+        sinon.assert.calledWithExactly(dispatcher.dispatch,
+          new sharedActions.UpdateRoomInfo(fakeRoomData));
+      });
+    });
+  });
 });
--- a/browser/components/loop/test/shared/roomStore_test.js
+++ b/browser/components/loop/test/shared/roomStore_test.js
@@ -432,9 +432,36 @@ describe("loop.store.RoomStore", functio
 
     it("should open the room via mozLoop", function() {
       dispatcher.dispatch(new sharedActions.OpenRoom({roomToken: "42abc"}));
 
       sinon.assert.calledOnce(fakeMozLoop.rooms.open);
       sinon.assert.calledWithExactly(fakeMozLoop.rooms.open, "42abc");
     });
   });
+
+  describe("#renameRoom", function() {
+    var store, fakeMozLoop;
+
+    beforeEach(function() {
+      fakeMozLoop = {
+        rooms: {
+          rename: sinon.spy()
+        }
+      };
+      store = new loop.store.RoomStore({
+        dispatcher: dispatcher,
+        mozLoop: fakeMozLoop
+      });
+    });
+
+    it("should rename the room via mozLoop", function() {
+      dispatcher.dispatch(new sharedActions.RenameRoom({
+        roomToken: "42abc",
+        newRoomName: "silly name"
+      }));
+
+      sinon.assert.calledOnce(fakeMozLoop.rooms.rename);
+      sinon.assert.calledWith(fakeMozLoop.rooms.rename, "42abc",
+        "silly name");
+    });
+  });
 });
--- a/browser/components/loop/test/xpcshell/test_looprooms.js
+++ b/browser/components/loop/test/xpcshell/test_looprooms.js
@@ -204,26 +204,32 @@ add_task(function* setup_server() {
   function returnRoomDetails(res, roomName) {
     roomDetail.roomName = roomName;
     res.setStatusLine(null, 200, "OK");
     res.write(JSON.stringify(roomDetail));
     res.processAsync();
     res.finish();
   }
 
+  function getJSONData(body) {
+    return JSON.parse(CommonUtils.readBytesFromInputStream(body));
+  }
+
   // Add a request handler for each room in the list.
   [...kRooms.values()].forEach(function(room) {
     loopServer.registerPathHandler("/rooms/" + encodeURIComponent(room.roomToken), (req, res) => {
       if (req.method == "POST") {
-        let body = CommonUtils.readBytesFromInputStream(req.bodyInputStream);
-        let data = JSON.parse(body);
+        let data = getJSONData(req.bodyInputStream);
         res.setStatusLine(null, 200, "OK");
         res.write(JSON.stringify(data));
         res.processAsync();
         res.finish();
+      } else if (req.method == "PATCH") {
+        let data = getJSONData(req.bodyInputStream);
+        returnRoomDetails(res, data.roomName);
       } else {
         returnRoomDetails(res, room.roomName);
       }
     });
   });
 
   loopServer.registerPathHandler("/rooms/error401", (req, res) => {
     res.setStatusLine(null, 401, "Not Found");
@@ -359,16 +365,23 @@ add_task(function* test_refreshMembershi
 // Test if leaving a room works as expected.
 add_task(function* test_leaveRoom() {
   let roomToken = "_nxD4V4FflQ";
   let leaveData = yield LoopRooms.promise("leave", roomToken, "fakeLeaveSessionToken");
   Assert.equal(leaveData.action, "leave");
   Assert.equal(leaveData.sessionToken, "fakeLeaveSessionToken");
 });
 
+// Test if renaming a room works as expected.
+add_task(function* test_renameRoom() {
+  let roomToken = "_nxD4V4FflQ";
+  let renameData = yield LoopRooms.promise("rename", roomToken, "fakeName");
+  Assert.equal(renameData.roomName, "fakeName");
+});
+
 // Test if the event emitter implementation doesn't leak and is working as expected.
 add_task(function* () {
   Assert.strictEqual(gExpectedAdds.length, 0, "No room additions should be expected anymore");
   Assert.strictEqual(gExpectedUpdates.length, 0, "No room updates should be expected anymore");
  });
 
 function run_test() {
   setupFakeLoopServer();