From b1d60d7c9a890c664fa80f2abeb7cc5aa828e3d1 Mon Sep 17 00:00:00 2001
From: augier <christophe@c-henry.fr>
Date: Mon, 23 Nov 2015 19:58:51 +0100
Subject: [PATCH] Jasmine tests

---
 .../app/views/publisher/mention_view.js       | 104 +++---
 .../javascripts/app/views/search_base_view.js |  17 +-
 app/assets/stylesheets/publisher.scss         |   7 +-
 .../app/views/publisher_mention_view_spec.js  | 322 ++++++++++++++++++
 .../app/views/search_base_view_spec.js        | 183 ++++++++++
 .../javascripts/app/views/search_view_spec.js |  95 +++---
 6 files changed, 628 insertions(+), 100 deletions(-)
 create mode 100644 spec/javascripts/app/views/publisher_mention_view_spec.js
 create mode 100644 spec/javascripts/app/views/search_base_view_spec.js

diff --git a/app/assets/javascripts/app/views/publisher/mention_view.js b/app/assets/javascripts/app/views/publisher/mention_view.js
index 270e14161b..4383da759c 100644
--- a/app/assets/javascripts/app/views/publisher/mention_view.js
+++ b/app/assets/javascripts/app/views/publisher/mention_view.js
@@ -8,8 +8,8 @@
 
 app.views.PublisherMention = app.views.SearchBase.extend({
   KEYS: {
-    BACKSPACE: 8, TAB: 9, RETURN: 13, ESC: 27, LEFT: 37, UP: 38,
-    RIGHT: 39, DOWN: 40, COMMA: 188, SPACE: 32, HOME: 36, END: 35
+    PASTE: 118, BACKSPACE: 8, TAB: 9, RETURN: 13, ESC: 27, LEFT: 37,
+    UP: 38, RIGHT: 39, DOWN: 40, COMMA: 188, SPACE: 32, HOME: 36, END: 35
   },
 
   settings: {
@@ -23,33 +23,13 @@ app.views.PublisherMention = app.views.SearchBase.extend({
     }
   },
 
-  utils: {
-    setCaretPosition: function(domNode, caretPos){
-      if(domNode.createTextRange){
-        var range = domNode.createTextRange();
-        range.move("character", caretPos);
-        range.select();
-      } else{
-        if(domNode.selectionStart){
-          domNode.focus();
-          domNode.setSelectionRange(caretPos, caretPos);
-        } else{
-          domNode.focus();
-        }
-      }
-    },
-
-    rtrim: function(string){
-      return string.replace(/\s+$/, "");
-    }
-  },
-
   events: {
     "keydown #status_message_fake_text": "onInputBoxKeyDown",
     "keypress #status_message_fake_text": "onInputBoxKeyPress",
     "input #status_message_fake_text": "onInputBoxInput",
     "click #status_message_fake_text": "onInputBoxClick",
-    "blur #status_message_fake_text": "onInputBoxBlur"
+    "blur #status_message_fake_text": "onInputBoxBlur",
+    "paste #status_message_fake_text": "onInputBoxPaste"
   },
 
   initialize: function(){
@@ -66,21 +46,24 @@ app.views.PublisherMention = app.views.SearchBase.extend({
     this.elmMentionsOverlay = $(this.settings.templates.mentionsOverlay());
     this.elmMentionsOverlay.prependTo(this.elmWrapperBox);
 
+    this.bindMentionningEvents();
+    this.completeSetup(this.getTypeaheadInput());
+
+    this.$el.find(".twitter-typeahead").css({position: "absolute", left: "-1px"});
+    this.$el.find(".twitter-typeahead .tt-menu").css("margin-top", 0);
+  },
+
+  bindMentionningEvents: function(){
     var self = this;
-    this.getSearchInput().on("typeahead:select", function(evt, datum){
+    this.getTypeaheadInput().on("typeahead:select", function(evt, datum){
       self.processMention(datum);
       self.resetMentionBox();
       self.addToFilteredResults(datum);
     });
 
-    this.getSearchInput().on("typeahead:render", function(){
+    this.getTypeaheadInput().on("typeahead:render", function(){
       self.select(self.$(".tt-menu .tt-suggestion").first());
     });
-
-    this.completeSetup(this.getSearchInput());
-
-    this.$el.find(".twitter-typeahead").css({position: "absolute", left: "-1px"});
-    this.$el.find(".twitter-typeahead .tt-menu").css("margin-top", 0);
   },
 
   clearBuffer: function(){
@@ -110,12 +93,8 @@ app.views.PublisherMention = app.views.SearchBase.extend({
   processMention: function(mention){
     var currentMessage = this.getInputBoxValue();
 
-    // Using a regex to figure out positions
-    var regex = new RegExp("\\" + this.settings.triggerChar + this.currentDataQuery, "gi");
-    regex.exec(currentMessage);
-
-    var startCaretPosition = regex.lastIndex - this.currentDataQuery.length - 1;
-    var currentCaretPosition = regex.lastIndex;
+    var currentCaretPosition = this.getCaretPosition();
+    var startCaretPosition = currentCaretPosition - (this.currentDataQuery.length + 1);
 
     var start = currentMessage.substr(0, startCaretPosition);
     var end = currentMessage.substr(currentCaretPosition, currentMessage.length);
@@ -135,7 +114,7 @@ app.views.PublisherMention = app.views.SearchBase.extend({
 
     // Set correct focus and selection
     this.elmInputBox.focus();
-    this.utils.setCaretPosition(this.elmInputBox[0], startEndIndex);
+    this.setCaretPosition(startEndIndex);
   },
 
   updateValues: function(){
@@ -153,7 +132,7 @@ app.views.PublisherMention = app.views.SearchBase.extend({
       var textSyntax = self.settings.templates.mentionItemSyntax(mention);
       syntaxMessage = syntaxMessage.replace(mentionVal, textSyntax);
 
-      var textHighlight = self.settings.templates.mentionItemHighlight({ name: _.escape(mention.name) });
+      var textHighlight = self.settings.templates.mentionItemHighlight({name: _.escape(mention.name)});
       mentionText = mentionText.replace(mentionVal, textHighlight);
     });
 
@@ -174,7 +153,11 @@ app.views.PublisherMention = app.views.SearchBase.extend({
     _.each(persons, function(person){
       self.addMention(person);
       self.addToFilteredResults(person);
-      self.elmInputBox.val(self.mentionChar + person.name);
+      var text = self.mentionChar + person.name;
+      if(self.elmInputBox.val().length !== 0){
+        text = self.elmInputBox.val() + " " + text;
+      }
+      self.elmInputBox.val(text);
       self.updateValues();
     });
   },
@@ -210,7 +193,8 @@ app.views.PublisherMention = app.views.SearchBase.extend({
   },
 
   onInputBoxKeyPress: function(e){
-    if(e.keyCode !== this.KEYS.BACKSPACE){
+    // Excluding ctrl+v from key press event in firefox
+    if(!((e.which === this.KEYS.PASTE && e.ctrlKey) || (e.keyCode === this.KEYS.BACKSPACE))){
       var typedValue = String.fromCharCode(e.which || e.keyCode);
       this.inputBuffer.push(typedValue);
     }
@@ -223,7 +207,7 @@ app.views.PublisherMention = app.views.SearchBase.extend({
     var triggerCharIndex = _.lastIndexOf(this.inputBuffer, this.settings.triggerChar);
     if(triggerCharIndex > -1){
       this.currentDataQuery = this.inputBuffer.slice(triggerCharIndex + 1).join("");
-      this.currentDataQuery = this.utils.rtrim(this.currentDataQuery);
+      this.currentDataQuery = this.rtrim(this.currentDataQuery);
 
       this.showMentionBox();
     }
@@ -232,7 +216,7 @@ app.views.PublisherMention = app.views.SearchBase.extend({
   onInputBoxKeyDown: function(e){
     // This also matches HOME/END on OSX which is CMD+LEFT, CMD+RIGHT
     if(e.keyCode === this.KEYS.LEFT || e.keyCode === this.KEYS.RIGHT ||
-        e.keyCode === this.KEYS.HOME || e.keyCode === this.KEYS.END){
+       e.keyCode === this.KEYS.HOME || e.keyCode === this.KEYS.END){
       _.defer(this.clearBuffer);
 
       // IE9 doesn't fire the oninput event when backspace or delete is pressed. This causes the highlighting
@@ -268,7 +252,7 @@ app.views.PublisherMention = app.views.SearchBase.extend({
       case this.KEYS.RETURN:
       case this.KEYS.TAB:
         if(this.getSelected().size() === 1){
-          this.getSelected().click();
+           this.getSelected().click();
           return false;
         }
         break;
@@ -284,6 +268,15 @@ app.views.PublisherMention = app.views.SearchBase.extend({
     this.resetMentionBox();
   },
 
+  onInputBoxPaste: function(evt){
+    var pastedData = evt.originalEvent.clipboardData.getData("text/plain");
+    var dataArray = pastedData.split("");
+    var self = this;
+    _.each(dataArray, function(value){
+      self.inputBuffer.push(value);
+    });
+  },
+
   reset: function(){
     this.elmInputBox.val("");
     this.mentionsCollection.length = 0;
@@ -292,13 +285,13 @@ app.views.PublisherMention = app.views.SearchBase.extend({
   },
 
   showMentionBox: function(){
-    this.getSearchInput().typeahead("val", this.currentDataQuery);
-    this.getSearchInput().typeahead("open");
+    this.getTypeaheadInput().typeahead("val", this.currentDataQuery);
+    this.getTypeaheadInput().typeahead("open");
   },
 
   resetMentionBox: function(){
-    this.getSearchInput().typeahead("val", "");
-    this.getSearchInput().typeahead("close");
+    this.getTypeaheadInput().typeahead("val", "");
+    this.getTypeaheadInput().typeahead("close");
   },
 
   getInputBoxValue: function(){
@@ -309,7 +302,7 @@ app.views.PublisherMention = app.views.SearchBase.extend({
     return this.$el.find(".tt-menu").is(":visible");
   },
 
-  getSearchInput: function(){
+  getTypeaheadInput: function(){
     if(this.$el.find(".typeahead-mention-box").length === 0){
       this.elmInputBox.after("<input class='typeahead-mention-box hidden' type='text'/>");
     }
@@ -318,5 +311,18 @@ app.views.PublisherMention = app.views.SearchBase.extend({
 
   getTextForSubmit: function(){
     return this.mentionsCollection.length ? this.elmInputBox.data("messageText") : this.getInputBoxValue();
+  },
+
+  setCaretPosition: function(caretPos){
+    this.elmInputBox[0].focus();
+    this.elmInputBox[0].setSelectionRange(caretPos, caretPos);
+  },
+
+  getCaretPosition: function(){
+    return this.elmInputBox[0].selectionStart;
+  },
+
+  rtrim: function(string){
+    return string.replace(/\s+$/, "");
   }
 });
diff --git a/app/assets/javascripts/app/views/search_base_view.js b/app/assets/javascripts/app/views/search_base_view.js
index af79141229..65bf83fcd7 100644
--- a/app/assets/javascripts/app/views/search_base_view.js
+++ b/app/assets/javascripts/app/views/search_base_view.js
@@ -98,18 +98,19 @@ app.views.SearchBase = app.views.Base.extend({
    */
   bindSelectionEvents: function(){
     var self = this;
-    var onover = function(evt){
-      var isSuggestion = $(evt.target).is(".tt-suggestion");
-      var suggestion = isSuggestion ? $(evt.target) : $(evt.target).parent(".tt-suggestion");
-      if(suggestion){
+    var onover = function(suggestion){
+      return function(){
         self.select(suggestion);
-      }
+      };
     };
 
     this.typeaheadElement.on("typeahead:render", function(){
-      self.$(".tt-menu *").off("mouseover", onover);
-      self.$(".tt-menu .tt-suggestion").on("mouseover", onover);
-      self.$(".tt-menu .tt-suggestion *").on("mouseover", onover);
+      self.$(".tt-menu *").off("mouseover");
+      self.$(".tt-menu .tt-suggestion").each(function(){
+        var $suggestion = $(this);
+        $suggestion.on("mouseover", onover($suggestion));
+        $suggestion.find("*").on("mouseover", onover($suggestion));
+      });
     });
   },
 
diff --git a/app/assets/stylesheets/publisher.scss b/app/assets/stylesheets/publisher.scss
index 1a2ddaec13..f5b874445e 100644
--- a/app/assets/stylesheets/publisher.scss
+++ b/app/assets/stylesheets/publisher.scss
@@ -17,7 +17,12 @@
   }
 
   .container-fluid{ padding: 0; }
-  .mentions-autocomplete-list ul { width: 100% !important; }
+
+  .twitter-typeahead {
+    width: calc(100% + 2px);
+
+    .tt-menu { width: 100%; }
+  }
 
   form {
     margin: 0;
diff --git a/spec/javascripts/app/views/publisher_mention_view_spec.js b/spec/javascripts/app/views/publisher_mention_view_spec.js
new file mode 100644
index 0000000000..f104704942
--- /dev/null
+++ b/spec/javascripts/app/views/publisher_mention_view_spec.js
@@ -0,0 +1,322 @@
+describe("app.views.PublisherMention", function(){
+  beforeEach(function(){
+    spec.content().html(
+      "<div id='publisher'>" +
+        "<textarea id='status_message_fake_text'></textarea>" +
+      "</div>");
+  });
+
+  describe("initialize", function(){
+    beforeEach(function(){
+      spyOn(app.views.PublisherMention.prototype, "completeSetup").and.callThrough();
+      spyOn(app.views.PublisherMention.prototype, "bindMentionningEvents").and.callThrough();
+      this.view = new app.views.PublisherMention({ el: "#publisher" });
+    });
+
+    it("initializes object properties", function(){
+      expect(this.view.mentionsCollection).toEqual([]);
+      expect(this.view.inputBuffer).toEqual([]);
+      expect(this.view.currentDataQuery).toBe("");
+      expect(this.view.mentionChar).toBe("\u200B");
+    });
+
+    it("calls completeSetup", function(){
+      expect(app.views.PublisherMention.prototype.completeSetup).toHaveBeenCalledWith(this.view.getTypeaheadInput());
+      expect(app.views.PublisherMention.prototype.bindMentionningEvents).toHaveBeenCalled();
+    });
+
+    it("initializes html elements", function(){
+      expect(this.view.$(".typeahead-mention-box").length).toBe(1);
+      expect(this.view.$(".mentions-input-box").length).toBe(1);
+      expect(this.view.$(".mentions-box").length).toBe(1);
+      expect(this.view.$(".mentions").length).toBe(1);
+    });
+  });
+
+  describe("bindMentionningEvents", function(){
+    beforeEach(function(){
+      spyOn(app.views.PublisherMention.prototype, "processMention");
+      spyOn(app.views.PublisherMention.prototype, "resetMentionBox");
+      spyOn(app.views.PublisherMention.prototype, "addToFilteredResults");
+      this.view = new app.views.PublisherMention({ el: "#publisher" });
+      this.view.bloodhound.add([
+        {"person": true, "name":"user1", "handle":"user1@pod.tld"},
+        {"person": true, "name":"user2", "handle":"user2@pod.tld"}
+      ]);
+    });
+
+    it("highlights the first item when rendering results", function(){
+      this.view.getTypeaheadInput().typeahead("val", "user");
+      this.view.getTypeaheadInput().typeahead("open");
+      expect(this.view.$(".tt-suggestion").first()).toHaveClass("tt-cursor");
+    });
+
+    it("process mention when clicking a result", function(){
+      this.view.getTypeaheadInput().typeahead("val", "user");
+      this.view.getTypeaheadInput().typeahead("open");
+      this.view.$(".tt-suggestion").first().click();
+      expect(app.views.PublisherMention.prototype.processMention).toHaveBeenCalled();
+      expect(app.views.PublisherMention.prototype.resetMentionBox).toHaveBeenCalled();
+      expect(app.views.PublisherMention.prototype.addToFilteredResults).toHaveBeenCalled();
+    });
+  });
+
+  describe("updateMentionsCollection", function(){
+    beforeEach(function(){
+      this.view = new app.views.PublisherMention({ el: "#publisher" });
+    });
+
+    it("removes person from mention collection if not mentionned anymore", function(){
+      this.view.mentionsCollection.push({name: "user1"});
+      expect(this.view.mentionsCollection.length).toBe(1);
+      this.view.updateMentionsCollection();
+      expect(this.view.mentionsCollection.length).toBe(0);
+    });
+
+    it("removes item from mention collection if not a person", function(){
+      this.view.mentionsCollection.push({});
+      expect(this.view.mentionsCollection.length).toBe(1);
+      this.view.updateMentionsCollection();
+      expect(this.view.mentionsCollection.length).toBe(0);
+    });
+  });
+
+  describe("addMention", function(){
+    beforeEach(function(){
+      this.view = new app.views.PublisherMention({ el: "#publisher" });
+    });
+
+    it("add person to mentionned people", function(){
+      expect(this.view.mentionsCollection.length).toBe(0);
+      this.view.addMention({"name":"user1", "handle":"user1@pod.tld"});
+      expect(this.view.mentionsCollection.length).toBe(1);
+      expect(this.view.mentionsCollection[0]).toEqual({
+        /* jshint camelcase: false */
+        "name":"user1", "handle":"user1@pod.tld", diaspora_id: "user1@pod.tld"});
+        /* jshint camelcase: true */
+    });
+
+    it("does not add mention if not a person", function(){
+      expect(this.view.mentionsCollection.length).toBe(0);
+      this.view.addMention();
+      expect(this.view.mentionsCollection.length).toBe(0);
+      this.view.addMention({});
+      expect(this.view.mentionsCollection.length).toBe(0);
+      this.view.addMention({"name": "user1"});
+      expect(this.view.mentionsCollection.length).toBe(0);
+      this.view.addMention({"handle":"user1@pod.tld"});
+      expect(this.view.mentionsCollection.length).toBe(0);
+    });
+  });
+
+  describe("getTypeaheadInput", function(){
+    beforeEach(function(){
+      this.view = new app.views.PublisherMention({ el: "#publisher" });
+    });
+
+    it("inserts typeahead input if it does not already exist", function(){
+      this.view.getTypeaheadInput().remove();
+      expect(this.view.$(".typeahead-mention-box").length).toBe(0);
+      this.view.getTypeaheadInput();
+      expect(this.view.$(".typeahead-mention-box").length).toBe(1);
+    });
+  });
+
+  describe("processMention", function(){
+    beforeEach(function(){
+      this.view = new app.views.PublisherMention({ el: "#publisher" });
+      this.view.elmInputBox.val("@user1 Text before @user1 text after");
+      this.view.currentDataQuery = "user1";
+      this.view.elmInputBox[0].setSelectionRange(25, 25);
+    });
+
+    it("add person to mentionned people", function(){
+      spyOn(this.view, "addMention");
+      this.view.processMention({"name":"user1", "handle":"user1@pod.tld"});
+      expect(this.view.addMention).toHaveBeenCalledWith({"name":"user1", "handle":"user1@pod.tld"});
+    });
+
+    it("cleans buffers", function(){
+      spyOn(this.view, "clearBuffer");
+      spyOn(this.view, "resetMentionBox");
+      this.view.processMention({"name":"user1", "handle":"user1@pod.tld"});
+      expect(this.view.clearBuffer).toHaveBeenCalled();
+      expect(this.view.resetMentionBox).toHaveBeenCalled();
+      expect(this.view.currentDataQuery).toBe("");
+    });
+
+    it("correctly formats the text", function(){
+      spyOn(this.view, "updateValues");
+      this.view.processMention({"name":"user1", "handle":"user1@pod.tld"});
+      expect(this.view.updateValues).toHaveBeenCalled();
+      expect(this.view.getInputBoxValue()).toBe("@user1 Text before " + this.view.mentionChar + "user1 text after");
+    });
+
+    it("places the caret at the right position", function(){
+      this.view.processMention({"name":"user1WithLongName", "handle":"user1@pod.tld"});
+      var expectedCaretPosition = ("@user1 Text before " + this.view.mentionChar + "user1WithLongName").length;
+      expect(this.view.elmInputBox[0].selectionStart).toBe(expectedCaretPosition);
+    });
+  });
+
+  describe("updateValues", function(){
+    beforeEach(function(){
+      this.view = new app.views.PublisherMention({ el: "#publisher" });
+      this.view.elmInputBox.val("@user1 Text before " + this.view.mentionChar + "user1\ntext after");
+      this.view.mentionsCollection.push({"name":"user1", "handle":"user1@pod.tld"});
+    });
+
+    it("filters mention from future results", function(){
+      spyOn(this.view, "clearFilteredResults");
+      spyOn(this.view, "addToFilteredResults");
+      this.view.updateValues();
+      expect(this.view.clearFilteredResults).toHaveBeenCalled();
+      expect(this.view.addToFilteredResults).toHaveBeenCalledWith({"name":"user1", "handle":"user1@pod.tld"});
+    });
+
+    it("formats message text data with correct mentionning syntax", function(){
+      this.view.updateValues();
+      expect(this.view.elmInputBox.data("messageText")).toBe("@user1 Text before @{user1 ; user1@pod.tld}\ntext after");
+    });
+
+    it("formats overlay text to HTML", function(){
+      this.view.updateValues();
+      expect(this.view.elmMentionsOverlay.find("div > div").html())
+        .toBe("@user1 Text before <strong><span>user1</span></strong><br>text after");
+    });
+  });
+
+  describe("prefillMention", function(){
+    beforeEach(function(){
+      this.view = new app.views.PublisherMention({ el: "#publisher" });
+      spyOn(this.view, "addMention");
+      spyOn(this.view, "addToFilteredResults");
+      spyOn(this.view, "updateValues");
+    });
+
+    it("prefills one mention", function(){
+      this.view.prefillMention([{"name":"user1", "handle":"user1@pod.tld"}]);
+
+      expect(this.view.addMention).toHaveBeenCalledWith({"name":"user1", "handle":"user1@pod.tld"});
+      expect(this.view.addToFilteredResults)
+        .toHaveBeenCalledWith({"name":"user1", "handle":"user1@pod.tld"});
+      expect(this.view.updateValues).toHaveBeenCalled();
+      expect(this.view.getInputBoxValue()).toBe(this.view.mentionChar + "user1");
+    });
+
+    it("prefills multiple mentions", function(){
+      this.view.prefillMention([
+        {"name":"user1", "handle":"user1@pod.tld"},
+        {"name":"user2", "handle":"user2@pod.tld"}
+      ]);
+
+      expect(this.view.addMention).toHaveBeenCalledWith({"name":"user1", "handle":"user1@pod.tld"});
+      expect(this.view.addMention).toHaveBeenCalledWith({"name":"user2", "handle":"user2@pod.tld"});
+      expect(this.view.addToFilteredResults).toHaveBeenCalledWith({"name":"user1", "handle":"user1@pod.tld"});
+      expect(this.view.addToFilteredResults).toHaveBeenCalledWith({"name":"user2", "handle":"user2@pod.tld"});
+      expect(this.view.updateValues).toHaveBeenCalled();
+      expect(this.view.getInputBoxValue()).toBe(this.view.mentionChar + "user1 " + this.view.mentionChar + "user2");
+    });
+  });
+
+  describe("onInputBoxPaste", function(){
+    beforeEach(function(){
+      this.view = new app.views.PublisherMention({ el: "#publisher" });
+    });
+
+    it("add person to mentionned people", function(){
+      var pasteEvent = {originalEvent: {clipboardData: {getData: function(){
+        return "Pasted text";
+      }}}};
+
+      this.view.onInputBoxPaste(pasteEvent);
+      expect(this.view.inputBuffer).toEqual(["P", "a", "s", "t", "e", "d", " ", "t", "e", "x", "t"]);
+    });
+  });
+
+  describe("reset", function(){
+    beforeEach(function(){
+      this.view = new app.views.PublisherMention({ el: "#publisher" });
+      spyOn(this.view, "clearFilteredResults");
+      spyOn(this.view, "updateValues");
+    });
+
+    it("resets the mention box", function(){
+      this.view.reset();
+      expect(this.view.elmInputBox.val()).toBe("");
+      expect(this.view.mentionsCollection.length).toBe(0);
+      expect(this.view.clearFilteredResults).toHaveBeenCalled();
+      expect(this.view.updateValues).toHaveBeenCalled();
+    });
+  });
+
+  describe("showMentionBox", function(){
+    beforeEach(function(){
+      this.view = new app.views.PublisherMention({ el: "#publisher" });
+      this.view.bloodhound.add([
+        {"person": true, "name":"user1", "handle":"user1@pod.tld"}
+      ]);
+      this.view.currentDataQuery = "user1";
+    });
+
+    it("shows the mention box", function(){
+      expect(this.view.$(".tt-menu").is(":visible")).toBe(false);
+      expect(this.view.$(".tt-menu .tt-suggestion").length).toBe(0);
+      this.view.showMentionBox();
+      expect(this.view.$(".tt-menu").is(":visible")).toBe(true);
+      expect(this.view.$(".tt-menu .tt-suggestion").length).toBe(1);
+    });
+  });
+
+  describe("resetMentionBox", function(){
+    beforeEach(function(){
+      this.view = new app.views.PublisherMention({ el: "#publisher" });
+      this.view.bloodhound.add([
+        {"person": true, "name":"user1", "handle":"user1@pod.tld"}
+      ]);
+    });
+
+    it("resets results and closes mention box", function(){
+      this.view.getTypeaheadInput().typeahead("val", "user");
+      this.view.getTypeaheadInput().typeahead("open");
+      expect(this.view.$(".tt-menu").is(":visible")).toBe(true);
+      expect(this.view.$(".tt-menu .tt-suggestion").length >= 1).toBe(true);
+      this.view.resetMentionBox();
+      expect(this.view.$(".tt-menu").is(":visible")).toBe(false);
+      expect(this.view.$(".tt-menu .tt-suggestion").length).toBe(0);
+    });
+  });
+
+  describe("getInputBoxValue", function(){
+    beforeEach(function(){
+      this.view = new app.views.PublisherMention({ el: "#publisher" });
+    });
+
+    it("returns trimmed text", function(){
+      this.view.elmInputBox.val("Text with trailing spaces        ");
+      expect(this.view.getInputBoxValue()).toBe("Text with trailing spaces");
+    });
+  });
+
+  describe("getTextForSubmit", function(){
+    beforeEach(function(){
+      this.view = new app.views.PublisherMention({ el: "#publisher" });
+      this.view.bloodhound.add([
+        {"person": true, "name":"user1", "handle":"user1@pod.tld"}
+      ]);
+    });
+
+    it("returns text with mention syntax if someone is mentionned", function(){
+      this.view.getTypeaheadInput().typeahead("val", "user");
+      this.view.getTypeaheadInput().typeahead("open");
+      this.view.$(".tt-suggestion").first().click();
+      expect(this.view.getTextForSubmit()).toBe("@{user1 ; user1@pod.tld}");
+    });
+
+    it("returns normal text if nobody is mentionned", function(){
+      this.view.elmInputBox.data("messageText", "Bad text");
+      this.view.elmInputBox.val("Good text");
+      expect(this.view.getTextForSubmit()).toBe("Good text");
+    });
+  });
+});
diff --git a/spec/javascripts/app/views/search_base_view_spec.js b/spec/javascripts/app/views/search_base_view_spec.js
new file mode 100644
index 0000000000..36bd65912b
--- /dev/null
+++ b/spec/javascripts/app/views/search_base_view_spec.js
@@ -0,0 +1,183 @@
+describe("app.views.SearchBase", function() {
+  beforeEach(function(){
+    spec.content().html(
+      "<form action='/search' id='search_people_form'><input id='q' name='q' type='search'></input></form>"
+    );
+  });
+
+  describe("completeSetup", function(){
+    it("calls setupBloodhound", function(){
+      spyOn(app.views.SearchBase.prototype, "setupBloodhound").and.callThrough();
+      var view = new app.views.SearchBase({el: "#search_people_form"});
+      view.completeSetup();
+      expect(app.views.SearchBase.prototype.setupBloodhound).toHaveBeenCalled();
+    });
+
+    it("calls setupTypeahead", function(){
+      spyOn(app.views.SearchBase.prototype, "setupTypeahead");
+      var view = new app.views.SearchBase({el: "#search_people_form"});
+      view.completeSetup();
+      expect(app.views.SearchBase.prototype.setupTypeahead).toHaveBeenCalled();
+    });
+
+    it("calls bindSelectionEvents", function(){
+      spyOn(app.views.SearchBase.prototype, "bindSelectionEvents");
+      var view = new app.views.SearchBase({el: "#search_people_form"});
+      view.completeSetup();
+      expect(app.views.SearchBase.prototype.bindSelectionEvents).toHaveBeenCalled();
+    });
+
+    it("initializes the results to filter", function(){
+      spyOn(app.views.SearchBase.prototype, "bindSelectionEvents");
+      var view = new app.views.SearchBase({el: "#search_people_form"});
+      view.completeSetup();
+      expect(view.resultsTofilter.length).toBe(0);
+    });
+  });
+
+  describe("setupBloodhound", function(){
+    beforeEach(function(){
+      this.view = new app.views.SearchBase({el: "#search_people_form"});
+      this.syncCallback = function(){};
+      this.asyncCallback = function(){};
+    });
+
+    context("when performing a local search with 1 filtered result", function(){
+      beforeEach(function(){
+        this.view.completeSetup(this.view.$("#q"));
+        this.view.bloodhound.add([
+          {"id":1,"guid":"1","name":"user1","handle":"user1@pod.tld","url":"/people/1"},
+          {"id":2,"guid":"2","name":"user2","handle":"user2@pod.tld","url":"/people/2"}
+        ]);
+      });
+
+      it("should not return the filtered result", function(){
+        spyOn(this, "syncCallback");
+        spyOn(this, "asyncCallback");
+
+        this.view.bloodhound.customSearch("user", this.syncCallback, this.asyncCallback);
+        expect(this.syncCallback).toHaveBeenCalledWith([
+          {"id":1,"guid":"1","name":"user1","handle":"user1@pod.tld","url":"/people/1"},
+          {"id":2,"guid":"2","name":"user2","handle":"user2@pod.tld","url":"/people/2"}
+        ]);
+        expect(this.asyncCallback).not.toHaveBeenCalled();
+
+        this.view.addToFilteredResults({"id":1,"guid":"1","name":"user1","handle":"user1@pod.tld","url":"/people/1"});
+        this.view.bloodhound.customSearch("user", this.syncCallback, this.asyncCallback);
+        expect(this.syncCallback).toHaveBeenCalledWith(
+          [{"id":2,"guid":"2","name":"user2","handle":"user2@pod.tld","url":"/people/2"}]);
+        expect(this.asyncCallback).not.toHaveBeenCalled();
+      });
+    });
+  });
+
+  describe("transformBloodhoundResponse", function() {
+    beforeEach(function() {
+      this.view = new app.views.SearchBase({ el: "#search_people_form" });
+    });
+
+    context("with persons", function() {
+      beforeEach(function() {
+        this.response = [{name: "Person", handle: "person@pod.tld"},{name: "User", handle: "user@pod.tld"}];
+      });
+
+      it("sets data.person to true", function() {
+        expect(this.view.transformBloodhoundResponse(this.response)).toEqual([
+         {name: "Person", handle: "person@pod.tld", person: true},
+         {name: "User", handle: "user@pod.tld", person: true}
+        ]);
+      });
+    });
+
+    context("with hashtags", function() {
+      beforeEach(function() {
+        this.response = [{name: "#tag"}, {name: "#hashTag"}];
+      });
+
+      it("sets data.hashtag to true and adds the correct URL", function() {
+        expect(this.view.transformBloodhoundResponse(this.response)).toEqual([
+         {name: "#tag", hashtag: true, url: Routes.tag("tag")},
+         {name: "#hashTag", hashtag: true, url: Routes.tag("hashTag")}
+        ]);
+      });
+    });
+  });
+
+  describe("bindSelectionEvents", function(){
+    beforeEach(function() {
+      this.view = new app.views.SearchBase({ el: "#search_people_form" });
+      this.view.completeSetup(this.view.$("#q"));
+      this.view.bloodhound.add([
+        {"person": true, "name":"user1", "handle":"user1@pod.tld"},
+        {"person": true, "name":"user2", "handle":"user2@pod.tld"}
+      ]);
+    });
+
+    context("bind over events", function(){
+      it("binds over event only once", function(){
+        this.view.$("#q").trigger("focusin");
+        this.view.$("#q").val("user");
+        this.view.$("#q").trigger("keypress");
+        this.view.$("#q").trigger("input");
+        this.view.$("#q").trigger("focus");
+        var numBindedEvents = $._data(this.view.$(".tt-menu .tt-suggestion")[0], "events").mouseover.length;
+        expect(numBindedEvents).toBe(1);
+        this.view.$("#q").trigger("focusout");
+        this.view.$("#q").trigger("focusin");
+        this.view.$("#q").val("user");
+        this.view.$("#q").trigger("keypress");
+        this.view.$("#q").trigger("input");
+        this.view.$("#q").trigger("focus");
+        numBindedEvents = $._data(this.view.$(".tt-menu .tt-suggestion")[0], "events").mouseover.length;
+        expect(numBindedEvents).toBe(1);
+      });
+
+      it("highlights the result when overing it", function(){
+        this.view.$("#q").trigger("focusin");
+        this.view.$("#q").val("user");
+        this.view.$("#q").trigger("keypress");
+        this.view.$("#q").trigger("input");
+        this.view.$("#q").trigger("focus");
+        this.view.$(".tt-menu .tt-suggestion").first().trigger("mouseover");
+        expect(this.view.$(".tt-menu .tt-suggestion").first()).toHaveClass("tt-cursor");
+      });
+    });
+  });
+
+  describe("addToFilteredResults", function(){
+    beforeEach(function() {
+      this.view = new app.views.SearchBase({ el: "#search_people_form" });
+      this.view.completeSetup(this.view.$("#q"));
+    });
+
+    context("when item is a person", function(){
+      it("add the item to filtered results", function(){
+        this.view.addToFilteredResults({handle: "user@pod.tld"});
+        expect(this.view.resultsTofilter.length).toBe(1);
+      });
+    });
+
+    context("when item is not a person", function(){
+      it("does not add the item to filtered results", function(){
+        this.view.addToFilteredResults({});
+        expect(this.view.resultsTofilter.length).toBe(0);
+      });
+    });
+  });
+
+  describe("clearFilteredResults", function(){
+    beforeEach(function() {
+      this.view = new app.views.SearchBase({ el: "#search_people_form" });
+      this.view.completeSetup(this.view.$("#q"));
+    });
+
+    context("clear filtered results", function(){
+      it("clears the filtered results list", function(){
+        this.view.addToFilteredResults({handle: "user@pod.tld"});
+        expect(this.view.resultsTofilter.length).toBe(1);
+        this.view.clearFilteredResults();
+        expect(this.view.resultsTofilter.length).toBe(0);
+      });
+    });
+  });
+});
diff --git a/spec/javascripts/app/views/search_view_spec.js b/spec/javascripts/app/views/search_view_spec.js
index 19fc0ccfb2..70525ae3bb 100644
--- a/spec/javascripts/app/views/search_view_spec.js
+++ b/spec/javascripts/app/views/search_view_spec.js
@@ -1,21 +1,63 @@
-describe("app.views.Search", function() {
+describe("app.views.Search", function(){
   beforeEach(function(){
     spec.content().html(
-      "<form action='/search' id='search_people_form'><input id='q' name='q' type='search'></input></form>"
+      "<form action='/search' id='search_people_form'><input id='q' name='q' type='search'/></form>"
     );
   });
 
-  describe("initialize", function() {
-    it("calls setupBloodhound", function() {
-      spyOn(app.views.Search.prototype, "setupBloodhound").and.callThrough();
-      new app.views.Search({ el: "#search_people_form" });
-      expect(app.views.Search.prototype.setupBloodhound).toHaveBeenCalled();
+  describe("initialize", function(){
+    it("calls completeSetup", function(){
+      spyOn(app.views.Search.prototype, "completeSetup").and.callThrough();
+      var view = new app.views.Search({el: "#search_people_form"});
+      expect(app.views.Search.prototype.completeSetup).toHaveBeenCalledWith(view.getTypeaheadElement());
     });
 
-    it("calls setupTypeahead", function() {
-      spyOn(app.views.Search.prototype, "setupTypeahead");
-      new app.views.Search({ el: "#search_people_form" });
-      expect(app.views.Search.prototype.setupTypeahead).toHaveBeenCalled();
+    it("calls bindMoreSelectionEvents", function(){
+      spyOn(app.views.Search.prototype, "bindMoreSelectionEvents").and.callThrough();
+      new app.views.Search({el: "#search_people_form"});
+      expect(app.views.Search.prototype.bindMoreSelectionEvents).toHaveBeenCalled();
+    });
+  });
+
+  describe("bindMoreSelectionEvents", function(){
+    beforeEach(function() {
+      this.view = new app.views.Search({ el: "#search_people_form" });
+      this.view.bloodhound.add([
+        {"person": true, "name":"user1", "handle":"user1@pod.tld"},
+        {"person": true, "name":"user2", "handle":"user2@pod.tld"}
+      ]);
+    });
+
+    context("bind mouseleave event", function(){
+      it("binds mouseleave event only once", function(){
+        this.view.$("#q").trigger("focusin");
+        this.view.$("#q").val("user");
+        this.view.$("#q").trigger("keypress");
+        this.view.$("#q").trigger("input");
+        this.view.$("#q").trigger("focus");
+        var numBindedEvents = $._data(this.view.$(".tt-menu")[0], "events").mouseout.length;
+        expect(numBindedEvents).toBe(1);
+        this.view.$("#q").trigger("focusout");
+        this.view.$("#q").trigger("focusin");
+        this.view.$("#q").val("user");
+        this.view.$("#q").trigger("keypress");
+        this.view.$("#q").trigger("input");
+        this.view.$("#q").trigger("focus");
+        numBindedEvents = $._data(this.view.$(".tt-menu")[0], "events").mouseout.length;
+        expect(numBindedEvents).toBe(1);
+      });
+
+      it("remove result highlight when leaving results list", function(){
+        this.view.$("#q").trigger("focusin");
+        this.view.$("#q").val("user");
+        this.view.$("#q").trigger("keypress");
+        this.view.$("#q").trigger("input");
+        this.view.$("#q").trigger("focus");
+        this.view.$(".tt-menu .tt-suggestion").first().trigger("mouseover");
+        expect(this.view.$(".tt-menu .tt-suggestion").first()).toHaveClass("tt-cursor");
+        this.view.$(".tt-menu").first().trigger("mouseleave");
+        expect(this.view.$(".tt-menu .tt-cursor").length).toBe(0);
+      });
     });
   });
 
@@ -44,35 +86,4 @@ describe("app.views.Search", function() {
       });
     });
   });
-
-  describe("transformBloodhoundResponse" , function() {
-    beforeEach(function() {
-      this.view = new app.views.Search({ el: "#search_people_form" });
-    });
-    context("with persons", function() {
-      beforeEach(function() {
-        this.response = [{name: "Person", handle: "person@pod.tld"},{name: "User", handle: "user@pod.tld"}];
-      });
-
-      it("sets data.person to true", function() {
-        expect(this.view.transformBloodhoundResponse(this.response)).toEqual([
-         {name: "Person", handle: "person@pod.tld", person: true},
-         {name: "User", handle: "user@pod.tld", person: true}
-        ]);
-      });
-    });
-
-    context("with hashtags", function() {
-      beforeEach(function() {
-        this.response = [{name: "#tag"}, {name: "#hashTag"}];
-      });
-
-      it("sets data.hashtag to true and adds the correct URL", function() {
-        expect(this.view.transformBloodhoundResponse(this.response)).toEqual([
-         {name: "#tag", hashtag: true, url: Routes.tag("tag")},
-         {name: "#hashTag", hashtag: true, url: Routes.tag("hashTag")}
-        ]);
-      });
-    });
-  });
 });
-- 
GitLab