Bug 1142515: add unit test coverage for the editing of context data inside the Loop conversation view. r=Standard8
authorMike de Boer <mdeboer@mozilla.com>
Thu, 07 May 2015 11:39:00 +0200
changeset 274092 855bbeb7d41f3ece691a0030b059f9f43f8da776
parent 274091 de2e38a20ccaedac1944a2269f53b3fd11aa312d
child 274093 d1431d2370952c51bef31591fcdb4b3a50064f0d
push id863
push userraliiev@mozilla.com
push dateMon, 03 Aug 2015 13:22:43 +0000
treeherdermozilla-release@f6321b14228d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersStandard8
bugs1142515
milestone40.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 1142515: add unit test coverage for the editing of context data inside the Loop conversation view. r=Standard8
browser/components/loop/content/shared/js/mixins.js
browser/components/loop/test/desktop-local/roomStore_test.js
browser/components/loop/test/desktop-local/roomViews_test.js
browser/components/loop/test/xpcshell/test_looprooms.js
--- a/browser/components/loop/content/shared/js/mixins.js
+++ b/browser/components/loop/content/shared/js/mixins.js
@@ -19,17 +19,17 @@ loop.shared.mixins = (function() {
    * Sets a new root object.  This is useful for testing native DOM events so we
    * can fake them. In beforeEach(), loop.shared.mixins.setRootObject is used to
    * substitute a fake window, and in afterEach(), the real window object is
    * replaced.
    *
    * @param {Object}
    */
   function setRootObject(obj) {
-    console.log("loop.shared.mixins: rootObject set to " + obj);
+    // console.log("loop.shared.mixins: rootObject set to " + obj);
     rootObject = obj;
   }
 
   /**
    * window.location mixin. Handles changes in the call url.
    * Forces a reload of the page to ensure proper state of the webapp
    *
    * @type {Object}
--- a/browser/components/loop/test/desktop-local/roomStore_test.js
+++ b/browser/components/loop/test/desktop-local/roomStore_test.js
@@ -592,58 +592,122 @@ 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() {
+  describe("#updateRoomContext", function() {
     var store, fakeMozLoop;
 
     beforeEach(function() {
       fakeMozLoop = {
         rooms: {
-          rename: null
+          get: sinon.stub().callsArgWith(1, null, {
+            roomToken: "42abc",
+            decryptedContext: {
+              roomName: "sillier name"
+            }
+          }),
+          update: null
         }
       };
       store = new loop.store.RoomStore(dispatcher, {mozLoop: fakeMozLoop});
     });
 
     it("should rename the room via mozLoop", function() {
-      fakeMozLoop.rooms.rename = sinon.spy();
-      dispatcher.dispatch(new sharedActions.RenameRoom({
+      fakeMozLoop.rooms.update = sinon.spy();
+      dispatcher.dispatch(new sharedActions.UpdateRoomContext({
         roomToken: "42abc",
         newRoomName: "silly name"
       }));
 
-      sinon.assert.calledOnce(fakeMozLoop.rooms.rename);
-      sinon.assert.calledWith(fakeMozLoop.rooms.rename, "42abc",
-        "silly name");
+      sinon.assert.calledOnce(fakeMozLoop.rooms.get);
+      sinon.assert.calledOnce(fakeMozLoop.rooms.update);
+      sinon.assert.calledWith(fakeMozLoop.rooms.update, "42abc", {
+        roomName: "silly name"
+      });
     });
 
-    it("should store any rename-encountered error", function() {
+    it("should store any update-encountered error", function() {
       var err = new Error("fake");
-      sandbox.stub(fakeMozLoop.rooms, "rename", function(roomToken, roomName, cb) {
+      sandbox.stub(fakeMozLoop.rooms, "update", function(roomToken, roomData, cb) {
         cb(err);
       });
 
-      dispatcher.dispatch(new sharedActions.RenameRoom({
+      dispatcher.dispatch(new sharedActions.UpdateRoomContext({
         roomToken: "42abc",
         newRoomName: "silly name"
       }));
 
       expect(store.getStoreState().error).eql(err);
     });
 
     it("should ensure only submitting a non-empty room name", function() {
-      fakeMozLoop.rooms.rename = sinon.spy();
+      fakeMozLoop.rooms.update = sinon.spy();
 
-      dispatcher.dispatch(new sharedActions.RenameRoom({
+      dispatcher.dispatch(new sharedActions.UpdateRoomContext({
         roomToken: "42abc",
         newRoomName: " \t  \t "
       }));
 
-      sinon.assert.notCalled(fakeMozLoop.rooms.rename);
+      sinon.assert.notCalled(fakeMozLoop.rooms.update);
+    });
+
+    it("should save updated context information", function() {
+      fakeMozLoop.rooms.update = sinon.spy();
+
+      dispatcher.dispatch(new sharedActions.UpdateRoomContext({
+        roomToken: "42abc",
+        // Room name doesn't need to change.
+        newRoomName: "sillier name",
+        newRoomDescription: "Hello, is it me you're looking for?",
+        newRoomThumbnail: "http://example.com/empty.gif",
+        newRoomURL: "http://example.com"
+      }));
+
+      sinon.assert.calledOnce(fakeMozLoop.rooms.update);
+      sinon.assert.calledWith(fakeMozLoop.rooms.update, "42abc", {
+        urls: [{
+          description: "Hello, is it me you're looking for?",
+          location: "http://example.com",
+          thumbnail: "http://example.com/empty.gif"
+        }]
+      });
     });
+
+    it("should not save context information with an invalid URL", function() {
+      fakeMozLoop.rooms.update = sinon.spy();
+
+      dispatcher.dispatch(new sharedActions.UpdateRoomContext({
+        roomToken: "42abc",
+        // Room name doesn't need to change.
+        newRoomName: "sillier name",
+        newRoomDescription: "Hello, is it me you're looking for?",
+        newRoomThumbnail: "http://example.com/empty.gif",
+        // NOTE: there are many variation we could test here, but the URL object
+        // constructor also fails on empty strings and is using the Gecko URL
+        // parser. Therefore we ought to rely on it working properly.
+        newRoomURL: "http/example.com"
+      }));
+
+      sinon.assert.notCalled(fakeMozLoop.rooms.update);
+    });
+
+    it("should not save context information when no context information is provided",
+      function() {
+        fakeMozLoop.rooms.update = sinon.spy();
+
+        dispatcher.dispatch(new sharedActions.UpdateRoomContext({
+          roomToken: "42abc",
+          // Room name doesn't need to change.
+          newRoomName: "sillier name",
+          newRoomDescription: "",
+          newRoomThumbnail: "",
+          newRoomURL: ""
+        }));
+
+        sinon.assert.notCalled(fakeMozLoop.rooms.update);
+      });
   });
 });
--- a/browser/components/loop/test/desktop-local/roomViews_test.js
+++ b/browser/components/loop/test/desktop-local/roomViews_test.js
@@ -14,16 +14,20 @@ describe("loop.roomViews", function () {
   beforeEach(function() {
     sandbox = sinon.sandbox.create();
 
     dispatcher = new loop.Dispatcher();
 
     fakeMozLoop = {
       getAudioBlob: sinon.stub(),
       getLoopPref: sinon.stub(),
+      getSelectedTabMetadata: sinon.stub().callsArgWith(0, {
+        previews: [],
+        title: ""
+      }),
       isSocialShareButtonAvailable: sinon.stub()
     };
 
     fakeWindow = {
       close: sinon.stub(),
       document: {},
       navigator: {
         mozLoop: fakeMozLoop
@@ -107,16 +111,17 @@ describe("loop.roomViews", function () {
 
     afterEach(function() {
       view = null;
     });
 
     function mountTestComponent(props) {
       props = _.extend({
         dispatcher: dispatcher,
+        mozLoop: fakeMozLoop,
         roomData: {},
         show: true,
         showContext: false
       }, props);
       return TestUtils.renderIntoDocument(
         React.createElement(loop.roomViews.DesktopRoomInvitationView, props));
     }
 
@@ -130,63 +135,16 @@ 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({
-          roomData: {
-            roomToken: "fakeToken",
-            roomName: "fakeName"
-          }
-        });
-
-        roomNameBox = view.getDOMNode().querySelector(".input-room-name");
-      });
-
-      it("should dispatch a RenameRoom action when the focus is lost",
-        function() {
-          React.addons.TestUtils.Simulate.change(roomNameBox, { target: {
-            value: "reallyFake"
-          }});
-
-          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 key is pressed",
-        function() {
-          React.addons.TestUtils.Simulate.change(roomNameBox, { target: {
-            value: "reallyFake"
-          }});
-
-          TestUtils.Simulate.keyDown(roomNameBox, {key: "Enter", which: 13});
-
-          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({
           roomData: { roomUrl: "http://invalid" }
         });
       });
 
       it("should dispatch a CopyRoomUrl action when the copy button is " +
@@ -238,16 +196,39 @@ describe("loop.roomViews", function () {
           roomData: {
             roomContextUrls: [fakeContextURL]
           }
         });
 
         expect(view.getDOMNode().querySelector(".room-context")).to.not.eql(null);
       });
 
+      it("should render the context in editMode when the pencil is clicked", function() {
+        view = mountTestComponent({
+          showContext: true,
+          roomData: {
+            roomContextUrls: [fakeContextURL]
+          }
+        });
+
+        var pencil = view.getDOMNode().querySelector(".room-context-btn-edit");
+        expect(pencil).to.not.eql(null);
+
+        React.addons.TestUtils.Simulate.click(pencil);
+
+        expect(view.state.editMode).to.eql(true);
+        var node = view.getDOMNode();
+        expect(node.querySelector("form")).to.not.eql(null);
+        // No text paragraphs should be visible in editMode.
+        var visiblePs = Array.slice(node.querySelector("p")).filter(function(p) {
+          return p.classList.contains("hide") || p.classList.contains("error");
+        });
+        expect(visiblePs.length).to.eql(0);
+      });
+
       it("should format the context url for display", function() {
         sandbox.stub(sharedUtils, "formatURL").returns({
           location: "location",
           hostname: "hostname"
         });
 
         view = mountTestComponent({
           showContext: true,
@@ -624,17 +605,22 @@ describe("loop.roomViews", function () {
     var view;
 
     afterEach(function() {
       view = null;
     });
 
     function mountTestComponent(props) {
       props = _.extend({
-        show: true
+        dispatcher: dispatcher,
+        mozLoop: fakeMozLoop,
+        show: true,
+        roomData: {
+          roomToken: "fakeToken"
+        }
       }, props);
       return TestUtils.renderIntoDocument(
         React.createElement(loop.roomViews.DesktopRoomContextView, props));
     }
 
     describe("#render", function() {
       it("should show the context information properly when available", function() {
         view = mountTestComponent({
@@ -675,11 +661,139 @@ describe("loop.roomViews", function () {
         view = mountTestComponent({
           roomData: { roomContextUrls: [fakeContextURL] }
         });
 
         var closeBtn = view.getDOMNode().querySelector(".room-context-btn-close");
         React.addons.TestUtils.Simulate.click(closeBtn);
         expect(view.getDOMNode()).to.eql(null);
       });
+
+      it("should render the view in editMode when appropriate", function() {
+        var roomName = "Hello, is it me you're looking for?";
+        view = mountTestComponent({
+          editMode: true,
+          roomData: {
+            roomName: roomName,
+            roomContextUrls: [fakeContextURL]
+          }
+        });
+
+        var node = view.getDOMNode();
+        expect(node.querySelector("form")).to.not.eql(null);
+        // Check the contents of the form fields.
+        expect(node.querySelector(".room-context-name").value).to.eql(roomName);
+        expect(node.querySelector(".room-context-url").value).to.eql(fakeContextURL.location);
+        expect(node.querySelector(".room-context-comments").value).to.eql(fakeContextURL.description);
+      });
+
+      it("should show the checkbox as disabled when no context is available", function() {
+        view = mountTestComponent({
+          editMode: true,
+          roomData: {
+            roomToken: "fakeToken",
+            roomName: "fakeName"
+          }
+        });
+
+        var checkbox = view.getDOMNode().querySelector(".checkbox");
+        expect(checkbox.classList.contains("disabled")).to.eql(true);
+      });
+    });
+
+    describe("Update Room", function() {
+      var roomNameBox;
+
+      beforeEach(function() {
+        sandbox.stub(dispatcher, "dispatch");
+
+        view = mountTestComponent({
+          editMode: true,
+          roomData: {
+            roomToken: "fakeToken",
+            roomName: "fakeName",
+            roomContextUrls: [fakeContextURL]
+          }
+        });
+
+        roomNameBox = view.getDOMNode().querySelector(".room-context-name");
+      });
+
+      it("should dispatch a UpdateRoomContext action when the focus is lost",
+        function() {
+          React.addons.TestUtils.Simulate.change(roomNameBox, { target: {
+            value: "reallyFake"
+          }});
+
+          React.addons.TestUtils.Simulate.blur(roomNameBox);
+
+          sinon.assert.calledOnce(dispatcher.dispatch);
+          sinon.assert.calledWithExactly(dispatcher.dispatch,
+            new sharedActions.UpdateRoomContext({
+              roomToken: "fakeToken",
+              newRoomName: "reallyFake",
+              newRoomDescription: fakeContextURL.description,
+              newRoomURL: fakeContextURL.location,
+              newRoomThumbnail: fakeContextURL.thumbnail
+            }));
+        });
+
+      it("should dispatch a UpdateRoomContext action when Enter key is pressed",
+        function() {
+          React.addons.TestUtils.Simulate.change(roomNameBox, { target: {
+            value: "reallyFake"
+          }});
+
+          TestUtils.Simulate.keyDown(roomNameBox, {key: "Enter", which: 13});
+
+          sinon.assert.calledOnce(dispatcher.dispatch);
+          sinon.assert.calledWithExactly(dispatcher.dispatch,
+            new sharedActions.UpdateRoomContext({
+              roomToken: "fakeToken",
+              newRoomName: "reallyFake",
+              newRoomDescription: fakeContextURL.description,
+              newRoomURL: fakeContextURL.location,
+              newRoomThumbnail: fakeContextURL.thumbnail
+            }));
+        });
+    });
+
+    describe("#handleCheckboxChange", function() {
+      var node, checkbox;
+
+      beforeEach(function() {
+        view = mountTestComponent({
+          availableContext: {
+            description: fakeContextURL.description,
+            previewImage: fakeContextURL.thumbnail,
+            url: fakeContextURL.location
+          },
+          editMode: true,
+          roomData: {
+            roomToken: "fakeToken",
+            roomName: "fakeName"
+          }
+        });
+
+        node = view.getDOMNode();
+        checkbox = node.querySelector(".checkbox");
+      });
+
+      it("should prefill the form with available context data when clicked", function() {
+        React.addons.TestUtils.Simulate.click(checkbox);
+
+        expect(node.querySelector(".room-context-name").value).to.eql("fakeName");
+        expect(node.querySelector(".room-context-url").value).to.eql(fakeContextURL.location);
+        expect(node.querySelector(".room-context-comments").value).to.eql(fakeContextURL.description);
+      });
+
+      it("should undo prefill when clicking the checkbox again", function() {
+        React.addons.TestUtils.Simulate.click(checkbox);
+        // Twice.
+        React.addons.TestUtils.Simulate.click(checkbox);
+
+        expect(node.querySelector(".room-context-name").value).to.eql("fakeName");
+        expect(node.querySelector(".room-context-url").value).to.eql("");
+        expect(node.querySelector(".room-context-comments").value).to.eql("");
+      });
     });
   });
 });
--- a/browser/components/loop/test/xpcshell/test_looprooms.js
+++ b/browser/components/loop/test/xpcshell/test_looprooms.js
@@ -93,17 +93,17 @@ const kExpectedRooms = new Map([
     },
     roomUrl: "http://localhost:3000/rooms/3jKS_Els9IU",
     maxSize: 3,
     clientMaxSize: 2,
     ctime: 1405518241
   }]
 ]);
 
-let roomDetail = {
+const kRoomDetail = {
   decryptedContext: {
     roomName: "First Room Name"
   },
   context: {
     wrappedKey: "wrappedKey",
     value: "encryptedValue",
     alg: "AES-GCM"
   },
@@ -181,22 +181,22 @@ const kCreateRoomData = {
 
 const kChannelGuest = MozLoopService.channelIDs.roomsGuest;
 const kChannelFxA = MozLoopService.channelIDs.roomsFxA;
 
 const normalizeRoom = function(room) {
   let newRoom = extend({}, room);
   let name = newRoom.decryptedContext.roomName;
 
-  for (let key of Object.getOwnPropertyNames(roomDetail)) {
+  for (let key of Object.getOwnPropertyNames(kRoomDetail)) {
     // Handle sub-objects if necessary (e.g. context, decryptedContext).
-    if (typeof roomDetail[key] == "object") {
-      newRoom[key] = extend({}, roomDetail[key]);
+    if (typeof kRoomDetail[key] == "object") {
+      newRoom[key] = extend({}, kRoomDetail[key]);
     } else {
-      newRoom[key] = roomDetail[key];
+      newRoom[key] = kRoomDetail[key];
     }
   }
 
   newRoom.decryptedContext.roomName = name;
   return newRoom;
 };
 
 // This compares rooms by normalizing the room fields so that the contents
@@ -312,44 +312,48 @@ add_task(function* setup_server() {
         }
       }
     }
 
     res.processAsync();
     res.finish();
   });
 
-  function returnRoomDetails(res, roomName) {
+  function returnRoomDetails(res, roomDetail, roomName) {
     roomDetail.roomName = roomName;
+    // The decrypted context and roomKey are never part of the server response.
+    delete roomDetail.decryptedContext;
+    delete roomDetail.roomKey;
     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.
   [...kRoomsResponses.values()].forEach(function(room) {
     loopServer.registerPathHandler("/rooms/" + encodeURIComponent(room.roomToken), (req, res) => {
+      let roomDetail = extend({}, kRoomDetail);
       if (req.method == "POST") {
         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);
 
         Assert.ok("context" in data, "should have encrypted context");
         // We return a fake encrypted name here as the context is
         // encrypted.
-        returnRoomDetails(res, "fakeEncrypted");
+        returnRoomDetails(res, roomDetail, "fakeEncrypted");
       } else {
         roomDetail.context = room.context;
         res.setStatusLine(null, 200, "OK");
         res.write(JSON.stringify(roomDetail));
         res.processAsync();
         res.finish();
       }
     });
@@ -569,21 +573,44 @@ add_task(function* test_sendConnectionSt
   );
   Assert.equal(statusData.sessionToken, "fakeStatusSessionToken");
 
   extraData.action = "status";
   extraData.sessionToken = "fakeStatusSessionToken";
   Assert.deepEqual(statusData, extraData);
 });
 
-// Test if renaming a room works as expected.
-add_task(function* test_renameRoom() {
+// Test if updating a room works as expected.
+add_task(function* test_updateRoom() {
   let roomToken = "_nxD4V4FflQ";
-  let renameData = yield LoopRooms.promise("rename", roomToken, "fakeName");
-  Assert.equal(renameData.roomName, "fakeEncrypted", "should have set the new name");
+  let fakeContext = {
+    description: "Hello, is it me you're looking for?",
+    location: "https://example.com",
+    thumbnail: "https://example.com/empty.gif"
+  };
+  let updateData = yield LoopRooms.promise("update", roomToken, {
+    roomName: "fakeEncrypted",
+    urls: [fakeContext]
+  });
+  Assert.equal(updateData.roomName, "fakeEncrypted", "should have set the new name");
+  let contextURL = updateData.decryptedContext.urls[0];
+  Assert.equal(contextURL.description, contextURL.description,
+    "should have set the new context URL description");
+  Assert.equal(contextURL.location, contextURL.location,
+    "should have set the new context URL location");
+  Assert.equal(contextURL.thumbnail, contextURL.thumbnail,
+    "should have set the new context URL thumbnail");
+});
+
+add_task(function* test_updateRoom_nameOnly() {
+  let roomToken = "_nxD4V4FflQ";
+  let updateData = yield LoopRooms.promise("update", roomToken, {
+    roomName: "fakeEncrypted"
+  });
+  Assert.equal(updateData.roomName, "fakeEncrypted", "should have set the new name");
 });
 
 add_task(function* test_roomDeleteNotifications() {
   gExpectedDeletes.push("_nxD4V4FflQ");
   roomsPushNotification("5", kChannelGuest);
   yield waitForCondition(() => gExpectedDeletes.length === 0);
 });