From f2fdaf1daf1c3f2119990b79d3dc578cc711e0c7 Mon Sep 17 00:00:00 2001 From: Augier Date: Fri, 30 Sep 2016 12:04:04 +0200 Subject: [PATCH] Use typeahead on conversations --- app/assets/javascripts/app/pages/contacts.js | 3 + .../app/views/conversations_form_view.js | 83 ++- .../app/views/conversations_inbox_view.js | 2 +- .../app/views/profile_header_view.js | 5 +- .../app/views/publisher/mention_view.js | 2 +- .../javascripts/app/views/search_base_view.js | 8 +- .../javascripts/app/views/search_view.js | 2 +- app/assets/stylesheets/conversations.scss | 52 +- .../stylesheets/mobile/conversations.scss | 2 + .../conversation_recipient_tag_tpl.jst.hbs | 12 + app/controllers/contacts_controller.rb | 3 +- app/controllers/conversations_controller.rb | 34 +- app/models/person.rb | 4 +- app/views/contacts/index.html.haml | 2 +- app/views/conversations/_new.haml | 14 +- app/views/conversations/new.html.haml | 12 +- app/views/conversations/new.mobile.haml | 8 +- app/views/people/show.html.haml | 2 +- .../step_definitions/conversations_steps.rb | 8 +- spec/controllers/contacts_controller_spec.rb | 11 + .../conversations_controller_spec.rb | 511 +++++++++++------- .../jasmine_fixtures/conversations_spec.rb | 3 + spec/javascripts/app/pages/contacts_spec.js | 27 + .../app/views/conversations_form_view_spec.js | 171 +++++- .../app/views/profile_header_view_spec.js | 26 + .../app/views/publisher_mention_view_spec.js | 2 +- .../javascripts/app/views/search_view_spec.js | 2 +- 27 files changed, 749 insertions(+), 262 deletions(-) create mode 100644 app/assets/templates/conversation_recipient_tag_tpl.jst.hbs diff --git a/app/assets/javascripts/app/pages/contacts.js b/app/assets/javascripts/app/pages/contacts.js index eca20fa33..f9bd2db27 100644 --- a/app/assets/javascripts/app/pages/contacts.js +++ b/app/assets/javascripts/app/pages/contacts.js @@ -79,6 +79,9 @@ app.pages.Contacts = Backbone.View.extend({ }, showMessageModal: function(){ + $("#conversationModal").on("modal:loaded", function() { + new app.views.ConversationsForm({prefill: gon.conversationPrefill}); + }); app.helpers.showModal("#conversationModal"); }, diff --git a/app/assets/javascripts/app/views/conversations_form_view.js b/app/assets/javascripts/app/views/conversations_form_view.js index bfdfe62fb..0b304739b 100644 --- a/app/assets/javascripts/app/views/conversations_form_view.js +++ b/app/assets/javascripts/app/views/conversations_form_view.js @@ -5,40 +5,83 @@ app.views.ConversationsForm = Backbone.View.extend({ events: { "keydown .conversation-message-text": "keyDown", + "click .conversation-recipient-tag .remove": "removeRecipient" }, initialize: function(opts) { - this.contacts = _.has(opts, "contacts") ? opts.contacts : null; - this.prefill = []; - if (_.has(opts, "prefillName") && _.has(opts, "prefillValue")) { - this.prefill = [{name: opts.prefillName, value: opts.prefillValue}]; + opts = opts || {}; + this.conversationRecipients = []; + + this.typeaheadElement = this.$el.find("#contacts-search-input"); + this.contactsIdsListInput = this.$el.find("#contact-ids"); + this.tagListElement = this.$("#recipients-tag-list"); + + this.search = new app.views.SearchBase({ + el: this.$el.find("#new-conversation"), + typeaheadInput: this.typeaheadElement, + customSearch: true, + autoselect: true, + remoteRoute: {url: "/contacts", extraParameters: "mutual=true"} + }); + + this.bindTypeaheadEvents(); + + this.tagListElement.empty(); + if (opts.prefill) { + this.prefill(opts.prefill); } - this.prepareAutocomplete(this.contacts); + this.$("form#new-conversation").on("ajax:success", this.conversationCreateSuccess); this.$("form#new-conversation").on("ajax:error", this.conversationCreateError); }, - prepareAutocomplete: function(data){ - this.$("#contact-autocomplete").autoSuggest(data, { - selectedItemProp: "name", - searchObjProps: "name", - asHtmlID: "contact_ids", - retrieveLimit: 10, - minChars: 1, - keyDelay: 0, - startText: '', - emptyText: Diaspora.I18n.t("no_results"), - preFill: this.prefill - }); - $("#contact_ids").attr("aria-labelledby", "toLabel").focus(); + addRecipient: function(person) { + this.conversationRecipients.push(person); + this.updateContactIdsListInput(); + /* eslint-disable camelcase */ + this.tagListElement.append(HandlebarsTemplates.conversation_recipient_tag_tpl(person)); + /* eslint-enable camelcase */ }, - keyDown : function(evt) { - if(evt.which === Keycodes.ENTER && evt.ctrlKey) { + prefill: function(handles) { + handles.forEach(this.addRecipient.bind(this)); + }, + + updateContactIdsListInput: function() { + this.contactsIdsListInput.val(_(this.conversationRecipients).pluck("id").join(",")); + this.search.ignoreDiasporaIds.length = 0; + this.conversationRecipients.forEach(this.search.ignorePersonForSuggestions.bind(this.search)); + }, + + bindTypeaheadEvents: function() { + this.typeaheadElement.on("typeahead:select", function(evt, person) { + this.onSuggestionSelection(person); + }.bind(this)); + }, + + onSuggestionSelection: function(person) { + this.addRecipient(person); + this.typeaheadElement.typeahead("val", ""); + }, + + keyDown: function(evt) { + if (evt.which === Keycodes.ENTER && evt.ctrlKey) { $(evt.target).parents("form").submit(); } }, + removeRecipient: function(evt) { + var $recipientTagEl = $(evt.target).parents(".conversation-recipient-tag"); + var diasporaHandle = $recipientTagEl.data("diaspora-handle"); + + this.conversationRecipients = this.conversationRecipients.filter(function(person) { + return diasporaHandle.localeCompare(person.handle) !== 0; + }); + + this.updateContactIdsListInput(); + $recipientTagEl.remove(); + }, + conversationCreateSuccess: function(evt, data) { app._changeLocation(Routes.conversation(data.id)); }, diff --git a/app/assets/javascripts/app/views/conversations_inbox_view.js b/app/assets/javascripts/app/views/conversations_inbox_view.js index 97e31f5c3..4e301a8a0 100644 --- a/app/assets/javascripts/app/views/conversations_inbox_view.js +++ b/app/assets/javascripts/app/views/conversations_inbox_view.js @@ -9,7 +9,7 @@ app.views.ConversationsInbox = Backbone.View.extend({ }, initialize: function() { - new app.views.ConversationsForm({contacts: gon.contacts}); + new app.views.ConversationsForm(); this.setupConversation(); }, diff --git a/app/assets/javascripts/app/views/profile_header_view.js b/app/assets/javascripts/app/views/profile_header_view.js index 906736895..0fc671b8e 100644 --- a/app/assets/javascripts/app/views/profile_header_view.js +++ b/app/assets/javascripts/app/views/profile_header_view.js @@ -79,8 +79,11 @@ app.views.ProfileHeader = app.views.Base.extend({ }, showMessageModal: function(){ + $("#conversationModal").on("modal:loaded", function() { + new app.views.ConversationsForm({prefill: gon.conversationPrefill}); + }); app.helpers.showModal("#conversationModal"); - }, + } }); // @license-end diff --git a/app/assets/javascripts/app/views/publisher/mention_view.js b/app/assets/javascripts/app/views/publisher/mention_view.js index 75c0015ea..86b3b07aa 100644 --- a/app/assets/javascripts/app/views/publisher/mention_view.js +++ b/app/assets/javascripts/app/views/publisher/mention_view.js @@ -32,7 +32,7 @@ app.views.PublisherMention = app.views.SearchBase.extend({ typeaheadInput: this.typeaheadInput, customSearch: true, autoselect: true, - remoteRoute: "/contacts" + remoteRoute: {url: "/contacts"} }); }, diff --git a/app/assets/javascripts/app/views/search_base_view.js b/app/assets/javascripts/app/views/search_base_view.js index 44277b851..eb00aaeaa 100644 --- a/app/assets/javascripts/app/views/search_base_view.js +++ b/app/assets/javascripts/app/views/search_base_view.js @@ -28,9 +28,13 @@ app.views.SearchBase = app.views.Base.extend({ }; // Allow bloodhound to look for remote results if there is a route given in the options - if(options.remoteRoute) { + if (options.remoteRoute && options.remoteRoute.url) { + var extraParameters = ""; + if (options.remoteRoute.extraParameters) { + extraParameters += "&" + options.remoteRoute.extraParameters; + } bloodhoundOptions.remote = { - url: options.remoteRoute + ".json?q=%QUERY", + url: options.remoteRoute.url + ".json?q=%QUERY" + extraParameters, wildcard: "%QUERY", transform: this.transformBloodhoundResponse.bind(this) }; diff --git a/app/assets/javascripts/app/views/search_view.js b/app/assets/javascripts/app/views/search_view.js index b2486659b..c0d8ec749 100644 --- a/app/assets/javascripts/app/views/search_view.js +++ b/app/assets/javascripts/app/views/search_view.js @@ -10,7 +10,7 @@ app.views.Search = app.views.SearchBase.extend({ this.searchInput = this.$("#q"); app.views.SearchBase.prototype.initialize.call(this, { typeaheadInput: this.searchInput, - remoteRoute: this.$el.attr("action"), + remoteRoute: {url: this.$el.attr("action")}, suggestionLink: true }); this.searchInput.on("typeahead:select", this.suggestionSelected); diff --git a/app/assets/stylesheets/conversations.scss b/app/assets/stylesheets/conversations.scss index dd443d646..8c1216cd5 100644 --- a/app/assets/stylesheets/conversations.scss +++ b/app/assets/stylesheets/conversations.scss @@ -183,10 +183,60 @@ } // scss-lint:enable SelectorDepth -#new_conversation_pane { +.new-conversation { ul.as-selections { width: 100% !important; } + input#contact_ids { box-shadow: none; } + label { font-weight: bold; } + + .twitter-typeahead, + .tt-menu { + width: 100%; + } +} + +.recipients-tag-list { + .conversation-recipient-tag { + background-color: $brand-primary; + border-radius: $btn-border-radius-base; + display: inline-flex; + margin: 0 2px $form-group-margin-bottom; + padding: 8px; + + &:first-child { margin-left: 0; } + + &:last-child { margin-right: 0; } + + div { + align-self: center; + justify-content: flex-start; + } + } + + .avatar { + height: 40px; + margin-right: 8px; + width: 40px; + } + + .name-and-handle { + color: $white; + margin-right: 8px; + text-align: left; + + .diaspora-id { font-size: $font-size-small; } + } + + .entypo-circled-cross { + color: $white; + cursor: pointer; + font-size: 20px; + height: 22px; + line-height: 22px; + + &:hover { color: $light-grey; } + } } .new-conversation.form-horizontal .form-group:last-of-type { margin-bottom: 0; } diff --git a/app/assets/stylesheets/mobile/conversations.scss b/app/assets/stylesheets/mobile/conversations.scss index e0e02411a..ef3409f79 100644 --- a/app/assets/stylesheets/mobile/conversations.scss +++ b/app/assets/stylesheets/mobile/conversations.scss @@ -61,3 +61,5 @@ .subject { padding: 0 10px; } .message-count, .unread-message-count { margin: 10px 2px; } + +.new-conversation .as-selections { background-color: transparent; } diff --git a/app/assets/templates/conversation_recipient_tag_tpl.jst.hbs b/app/assets/templates/conversation_recipient_tag_tpl.jst.hbs new file mode 100644 index 000000000..296c0b7f9 --- /dev/null +++ b/app/assets/templates/conversation_recipient_tag_tpl.jst.hbs @@ -0,0 +1,12 @@ +
+
+ +
+
+
{{ name }}
+
{{ handle }}
+
+
+ +
+
diff --git a/app/controllers/contacts_controller.rb b/app/controllers/contacts_controller.rb index a0be1dcec..ed5999f35 100644 --- a/app/controllers/contacts_controller.rb +++ b/app/controllers/contacts_controller.rb @@ -17,7 +17,8 @@ class ContactsController < ApplicationController # Used for mentions in the publisher and pagination on the contacts page format.json { @people = if params[:q].present? - Person.search(params[:q], current_user, only_contacts: true).limit(15) + mutual = params[:mutual].present? && params[:mutual] + Person.search(params[:q], current_user, only_contacts: true, mutual: mutual).limit(15) else set_up_contacts_json end diff --git a/app/controllers/conversations_controller.rb b/app/controllers/conversations_controller.rb index ba99b6b6a..4512e8a3c 100644 --- a/app/controllers/conversations_controller.rb +++ b/app/controllers/conversations_controller.rb @@ -30,12 +30,12 @@ class ConversationsController < ApplicationController end def create - contact_ids = params[:contact_ids] - - # Can't split nil - if contact_ids - contact_ids = contact_ids.split(',') if contact_ids.is_a? String - person_ids = current_user.contacts.where(id: contact_ids).pluck(:person_id) + # Contacts autocomplete does not work the same way on mobile and desktop + # Mobile returns contact ids array while desktop returns person id + # This will have to be removed when mobile autocomplete is ported to Typeahead + recipients_param, column = [%i(contact_ids id), %i(person_ids person_id)].find {|param, _| params[param].present? } + if recipients_param + person_ids = current_user.contacts.where(column => params[recipients_param].split(",")).pluck(:person_id) end opts = params.require(:conversation).permit(:subject) @@ -91,17 +91,23 @@ class ConversationsController < ApplicationController return end - @contacts_json = contacts_data.to_json - @contact_ids = "" - - if params[:contact_id] - @contact_ids = current_user.contacts.find(params[:contact_id]).id - elsif params[:aspect_id] - @contact_ids = current_user.aspects.find(params[:aspect_id]).contacts.map{|c| c.id}.join(',') - end if session[:mobile_view] == true && request.format.html? + @contacts_json = contacts_data.to_json + + @contact_ids = if params[:contact_id] + current_user.contacts.find(params[:contact_id]).id + elsif params[:aspect_id] + current_user.aspects.find(params[:aspect_id]).contacts.pluck(:id).join(",") + end + render :layout => true else + if params[:contact_id] + gon.push conversation_prefill: [current_user.contacts.find(params[:contact_id]).person.as_json] + elsif params[:aspect_id] + gon.push conversation_prefill: current_user.aspects + .find(params[:aspect_id]).contacts.map {|c| c.person.as_json } + end render :layout => false end end diff --git a/app/models/person.rb b/app/models/person.rb index 35673144b..8e0d5b6e2 100644 --- a/app/models/person.rb +++ b/app/models/person.rb @@ -145,7 +145,7 @@ class Person < ActiveRecord::Base [where_clause, q_tokens] end - def self.search(search_str, user, only_contacts: false) + def self.search(search_str, user, only_contacts: false, mutual: false) search_str.strip! return none if search_str.blank? || search_str.size < 2 @@ -159,6 +159,8 @@ class Person < ActiveRecord::Base ).searchable(user) end + query = query.where(contacts: {sharing: true, receiving: true}) if mutual + query.where(closed_account: false) .where(sql, *tokens) .includes(:profile) diff --git a/app/views/contacts/index.html.haml b/app/views/contacts/index.html.haml index dffe8db21..df7a58a74 100644 --- a/app/views/contacts/index.html.haml +++ b/app/views/contacts/index.html.haml @@ -34,7 +34,7 @@ .spinner -if @aspect - #new_conversation_pane + .conversations-form-container#new_conversation_pane = render 'shared/modal', :path => new_conversation_path(:aspect_id => @aspect.id, :name => @aspect.name, :modal => true), :title => t('conversations.index.new_conversation'), diff --git a/app/views/conversations/_new.haml b/app/views/conversations/_new.haml index 41fb777aa..c84a6b95a 100644 --- a/app/views/conversations/_new.haml +++ b/app/views/conversations/_new.haml @@ -2,9 +2,13 @@ = form_for Conversation.new, html: {id: "new-conversation", class: "new-conversation form-horizontal"}, remote: true do |conversation| .form-group - %label#toLabel{for: "contact_ids"} - = t(".to") - = text_field_tag "contact_autocomplete", nil, id: "contact-autocomplete", class: "form-control" + %label#to-label{for: "contacts-search-input"}= t(".to") + .recipients-tag-list.clearfix#recipients-tag-list + = text_field_tag "contact_autocomplete", nil, id: "contacts-search-input", class: "form-control" + - unless defined?(mobile) && mobile + = text_field_tag "person_ids", nil, id: "contact-ids", type: "hidden", + aria: {labelledby: "to-label"} + .form-group %label#subject-label{for: "conversation-subject"} = t(".subject") @@ -14,12 +18,14 @@ aria: {labelledby: "subject-label"}, value: "", placeholder: t("conversations.new.subject_default") + .form-group - %label.sr-only#message-label{for: "new-message-text"} = t(".message") + %label.sr-only#message-label{for: "new-message-text"}= t(".message") = text_area_tag "conversation[text]", "", rows: 5, id: "new-message-text", class: "conversation-message-text input-block-level form-control", aria: {labelledby: "message-label"} + .form-group = conversation.submit t(".send"), "data-disable-with" => t(".sending"), :class => "btn btn-primary pull-right" diff --git a/app/views/conversations/new.html.haml b/app/views/conversations/new.html.haml index 1acbba809..e66a929dd 100644 --- a/app/views/conversations/new.html.haml +++ b/app/views/conversations/new.html.haml @@ -1,12 +1,2 @@ -:javascript - $(document).ready(function () { - var data = $.parseJSON( "#{escape_javascript(@contacts_json)}" ); - new app.views.ConversationsForm({ - el: $("form#new-conversation").parent(), - contacts: data, - prefillName: "#{h params[:name]}", - prefillValue: "#{@contact_ids}" - }); - }); - += include_gon camel_case: true = render 'conversations/new' diff --git a/app/views/conversations/new.mobile.haml b/app/views/conversations/new.mobile.haml index ba78c558f..1da58fcfa 100644 --- a/app/views/conversations/new.mobile.haml +++ b/app/views/conversations/new.mobile.haml @@ -6,7 +6,7 @@ :plain $(document).ready(function () { var data = $.parseJSON( "#{escape_javascript(@contacts_json).html_safe}" ), - autocompleteInput = $("#contact-autocomplete"); + autocompleteInput = $("#contacts-search-input"); autocompleteInput.autoSuggest(data, { selectedItemProp: "name", @@ -15,7 +15,7 @@ retrieveLimit: 10, minChars: 1, keyDelay: 0, - startText: '', + startText: "", emptyText: "#{t("no_results")}", preFill: [{name : "#{h params[:name]}", value : "#{@contact_ids}"}] @@ -27,6 +27,6 @@ #flash-messages .container-fluid.row %h3 - = t('conversations.index.new_conversation') + = t("conversations.index.new_conversation") - = render 'conversations/new' + = render "conversations/new", mobile: true diff --git a/app/views/people/show.html.haml b/app/views/people/show.html.haml index 0eae5e336..60e707010 100644 --- a/app/views/people/show.html.haml +++ b/app/views/people/show.html.haml @@ -40,7 +40,7 @@ id: 'mentionModal' -if @contact - #new_conversation_pane + .conversations-form-container#new_conversation_pane = render 'shared/modal', path: new_conversation_path(:contact_id => @contact.id, name: @contact.person.name, modal: true), title: t('conversations.index.new_conversation'), diff --git a/features/step_definitions/conversations_steps.rb b/features/step_definitions/conversations_steps.rb index 1e7bace8b..ea076b30e 100644 --- a/features/step_definitions/conversations_steps.rb +++ b/features/step_definitions/conversations_steps.rb @@ -15,8 +15,8 @@ end Then /^I send a message with subject "([^"]*)" and text "([^"]*)" to "([^"]*)"$/ do |subject, text, person| step %(I am on the conversations page) within("#new-conversation", match: :first) do - step %(I fill in "contact_autocomplete" with "#{person}") - step %(I press the first ".as-result-item" within ".as-results") + find("#contacts-search-input").native.send_key(person.to_s) + step %(I press the first ".tt-suggestion" within ".twitter-typeahead") step %(I fill in "conversation-subject" with "#{subject}") step %(I fill in "new-message-text" with "#{text}") step %(I press "Send") @@ -26,8 +26,8 @@ end Then /^I send a message with subject "([^"]*)" and text "([^"]*)" to "([^"]*)" using keyboard shortcuts$/ do |subject, text, person| step %(I am on the conversations page) within("#new-conversation", match: :first) do - step %(I fill in "contact_autocomplete" with "#{person}") - step %(I press the first ".as-result-item" within ".as-results") + find("#contacts-search-input").native.send_key(person.to_s) + step %(I press the first ".tt-suggestion" within ".twitter-typeahead") step %(I fill in "conversation-subject" with "#{subject}") step %(I fill in "new-message-text" with "#{text}") find("#new-message-text").native.send_key %i(Ctrl Return) diff --git a/spec/controllers/contacts_controller_spec.rb b/spec/controllers/contacts_controller_spec.rb index 7a8ab7a13..8b7723b25 100644 --- a/spec/controllers/contacts_controller_spec.rb +++ b/spec/controllers/contacts_controller_spec.rb @@ -37,6 +37,8 @@ describe ContactsController, :type => :controller do @person1 = FactoryGirl.create(:person) bob.share_with(@person1, bob.aspects.first) @person2 = FactoryGirl.create(:person) + @person3 = FactoryGirl.create(:person) + bob.contacts.create(person: @person3, aspects: [bob.aspects.first], receiving: true, sharing: true) end it "succeeds" do @@ -53,6 +55,15 @@ describe ContactsController, :type => :controller do get :index, q: @person2.first_name, format: "json" expect(response.body).to eq([].to_json) end + + it "only returns mutual contacts when mutual parameter is true" do + get :index, q: @person1.first_name, mutual: true, format: "json" + expect(response.body).to eq([].to_json) + get :index, q: @person2.first_name, mutual: true, format: "json" + expect(response.body).to eq([].to_json) + get :index, q: @person3.first_name, mutual: true, format: "json" + expect(response.body).to eq([@person3].to_json) + end end context "for pagination on the contacts page" do diff --git a/spec/controllers/conversations_controller_spec.rb b/spec/controllers/conversations_controller_spec.rb index 0002468de..86709b263 100644 --- a/spec/controllers/conversations_controller_spec.rb +++ b/spec/controllers/conversations_controller_spec.rb @@ -16,48 +16,57 @@ describe ConversationsController, :type => :controller do end end - describe '#new modal' do - it 'succeeds' do - get :new, :modal => true - expect(response).to be_success - end + describe "#new modal" do + context "desktop and mobile" do + it "succeeds" do + get :new, modal: true + expect(response).to be_success + end - it "assigns a json list of contacts that are sharing with the person" do - sharing_user = FactoryGirl.create(:user_with_aspect) - sharing_user.share_with(alice.person, sharing_user.aspects.first) - get :new, :modal => true - expect(assigns(:contacts_json)).to include(alice.contacts.where(sharing: true, receiving: true).first.person.name) - alice.contacts << Contact.new(:person_id => eve.person.id, :user_id => alice.id, :sharing => false, :receiving => true) - expect(assigns(:contacts_json)).not_to include(alice.contacts.where(sharing: false).first.person.name) - expect(assigns(:contacts_json)).not_to include(alice.contacts.where(receiving: false).first.person.name) - end + it "assigns a contact if passed a contact id" do + get :new, contact_id: alice.contacts.first.id, modal: true + expect(controller.gon.conversation_prefill).to eq([alice.contacts.first.person.as_json]) + end - it "assigns a contact if passed a contact id" do - get :new, :contact_id => alice.contacts.first.id, :modal => true - expect(assigns(:contact_ids)).to eq(alice.contacts.first.id) - end + it "assigns a set of contacts if passed an aspect id" do + get :new, aspect_id: alice.aspects.first.id, modal: true + expect(controller.gon.conversation_prefill).to eq(alice.aspects.first.contacts.map {|c| c.person.as_json }) + end - it "assigns a set of contacts if passed an aspect id" do - get :new, :aspect_id => alice.aspects.first.id, :modal => true - expect(assigns(:contact_ids)).to eq(alice.aspects.first.contacts.map(&:id).join(',')) - end - - it "does not allow XSS via the name parameter" do - ["", - '"}]});alert(1);(function f() {var foo = [{b:"'].each do |xss| - get :new, :modal => true, name: xss - expect(response.body).not_to include xss + it "does not allow XSS via the name parameter" do + ["", + '"}]});alert(1);(function f() {var foo = [{b:"'].each do |xss| + get :new, modal: true, name: xss + expect(response.body).not_to include xss + end end end - it "does not allow XSS via the profile name" do - xss = "" - contact = alice.contacts.first - contact.person.profile.update_attribute(:first_name, xss) - get :new, :modal => true - json = JSON.parse(assigns(:contacts_json)).first - expect(json['value'].to_s).to eq(contact.id.to_s) - expect(json['name']).to_not include(xss) + context "mobile" do + before do + controller.session[:mobile_view] = true + end + + it "assigns a json list of contacts that are sharing with the person" do + sharing_user = FactoryGirl.create(:user_with_aspect) + sharing_user.share_with(alice.person, sharing_user.aspects.first) + get :new, modal: true + expect(assigns(:contacts_json)) + .to include(alice.contacts.where(sharing: true, receiving: true).first.person.name) + alice.contacts << Contact.new(person_id: eve.person.id, user_id: alice.id, sharing: false, receiving: true) + expect(assigns(:contacts_json)).not_to include(alice.contacts.where(sharing: false).first.person.name) + expect(assigns(:contacts_json)).not_to include(alice.contacts.where(receiving: false).first.person.name) + end + + it "does not allow XSS via the profile name" do + xss = "" + contact = alice.contacts.first + contact.person.profile.update_attribute(:first_name, xss) + get :new, modal: true + json = JSON.parse(assigns(:contacts_json)).first + expect(json["value"].to_s).to eq(contact.id.to_s) + expect(json["name"]).to_not include(xss) + end end end @@ -115,203 +124,323 @@ describe ConversationsController, :type => :controller do end end - describe '#create' do - context 'with a valid conversation' do - before do - @hash = { - :format => :js, - :conversation => { - :subject => "secret stuff", - :text => 'text debug' - }, - :contact_ids => [alice.contacts.first.id] - } - end - - it 'creates a conversation' do - expect { - post :create, @hash - }.to change(Conversation, :count).by(1) - end - - it 'creates a message' do - expect { - post :create, @hash - }.to change(Message, :count).by(1) - end - - it "responds with the conversation id as JSON" do - post :create, @hash - expect(response).to be_success - expect(JSON.parse(response.body)["id"]).to eq(Conversation.first.id) - end - - it 'sets the author to the current_user' do - @hash[:author] = FactoryGirl.create(:user) - post :create, @hash - expect(Message.first.author).to eq(alice.person) - expect(Conversation.first.author).to eq(alice.person) - end - - it 'dispatches the conversation' do - cnv = Conversation.create( - { - :author => alice.person, - :participant_ids => [alice.contacts.first.person.id, alice.person.id], - :subject => 'not spam', - :messages_attributes => [ {:author => alice.person, :text => 'cool stuff'} ] + describe "#create" do + context "desktop" do + context "with a valid conversation" do + before do + @hash = { + format: :js, + conversation: {subject: "secret stuff", text: "text debug"}, + person_ids: [alice.contacts.first.person.id] } - ) + end - expect(Diaspora::Federation::Dispatcher).to receive(:defer_dispatch) - post :create, @hash - end - end + it "creates a conversation" do + expect { post :create, @hash }.to change(Conversation, :count).by(1) + end - context 'with empty subject' do - before do - @hash = { - :format => :js, - :conversation => { - :subject => ' ', - :text => 'text debug' - }, - :contact_ids => [alice.contacts.first.id] - } - end + it "creates a message" do + expect { post :create, @hash }.to change(Message, :count).by(1) + end - it 'creates a conversation' do - expect { + it "responds with the conversation id as JSON" do post :create, @hash - }.to change(Conversation, :count).by(1) - end + expect(response).to be_success + expect(JSON.parse(response.body)["id"]).to eq(Conversation.first.id) + end - it 'creates a message' do - expect { + it "sets the author to the current_user" do + @hash[:author] = FactoryGirl.create(:user) post :create, @hash - }.to change(Message, :count).by(1) + expect(Message.first.author).to eq(alice.person) + expect(Conversation.first.author).to eq(alice.person) + end + + it "dispatches the conversation" do + Conversation.create(author: alice.person, participant_ids: [alice.contacts.first.person.id, alice.person.id], + subject: "not spam", messages_attributes: [{author: alice.person, text: "cool stuff"}]) + + expect(Diaspora::Federation::Dispatcher).to receive(:defer_dispatch) + post :create, @hash + end end - it "responds with the conversation id as JSON" do - post :create, @hash - expect(response).to be_success - expect(JSON.parse(response.body)["id"]).to eq(Conversation.first.id) + context "with empty subject" do + before do + @hash = { + format: :js, + conversation: {subject: " ", text: "text debug"}, + person_ids: [alice.contacts.first.person.id] + } + end + + it "creates a conversation" do + expect { post :create, @hash }.to change(Conversation, :count).by(1) + end + + it "creates a message" do + expect { post :create, @hash }.to change(Message, :count).by(1) + end + + it "responds with the conversation id as JSON" do + post :create, @hash + expect(response).to be_success + expect(JSON.parse(response.body)["id"]).to eq(Conversation.first.id) + end + end + + context "with empty text" do + before do + @hash = { + format: :js, + conversation: {subject: "secret stuff", text: " "}, + person_ids: [alice.contacts.first.person.id] + } + end + + it "does not create a conversation" do + count = Conversation.count + post :create, @hash + expect(Conversation.count).to eq(count) + end + + it "does not create a message" do + count = Message.count + post :create, @hash + expect(Message.count).to eq(count) + end + + it "responds with an error message" do + post :create, @hash + expect(response).not_to be_success + expect(response.body).to eq(I18n.t("conversations.create.fail")) + end + end + + context "with empty contact" do + before do + @hash = { + format: :js, + conversation: {subject: "secret stuff", text: "text debug"}, + person_ids: " " + } + end + + it "does not create a conversation" do + count = Conversation.count + post :create, @hash + expect(Conversation.count).to eq(count) + end + + it "does not create a message" do + count = Message.count + post :create, @hash + expect(Message.count).to eq(count) + end + + it "responds with an error message" do + post :create, @hash + expect(response).not_to be_success + expect(response.body).to eq(I18n.t("javascripts.conversation.create.no_recipient")) + end + end + + context "with nil contact" do + before do + @hash = { + format: :js, + conversation: {subject: "secret stuff", text: "text debug"}, + person_ids: nil + } + end + + it "does not create a conversation" do + count = Conversation.count + post :create, @hash + expect(Conversation.count).to eq(count) + end + + it "does not create a message" do + count = Message.count + post :create, @hash + expect(Message.count).to eq(count) + end + + it "responds with an error message" do + post :create, @hash + expect(response).not_to be_success + expect(response.body).to eq(I18n.t("javascripts.conversation.create.no_recipient")) + end end end - context 'with empty text' do + context "mobile" do before do - @hash = { - :format => :js, - :conversation => { - :subject => 'secret stuff', - :text => ' ' - }, - :contact_ids => [alice.contacts.first.id] - } + controller.session[:mobile_view] = true end - it 'does not create a conversation' do - count = Conversation.count - post :create, @hash - expect(Conversation.count).to eq(count) + context "with a valid conversation" do + before do + @hash = { + format: :js, + conversation: {subject: "secret stuff", text: "text debug"}, + contact_ids: [alice.contacts.first.id] + } + end + + it "creates a conversation" do + expect { post :create, @hash }.to change(Conversation, :count).by(1) + end + + it "creates a message" do + expect { post :create, @hash }.to change(Message, :count).by(1) + end + + it "responds with the conversation id as JSON" do + post :create, @hash + expect(response).to be_success + expect(JSON.parse(response.body)["id"]).to eq(Conversation.first.id) + end + + it "sets the author to the current_user" do + @hash[:author] = FactoryGirl.create(:user) + post :create, @hash + expect(Message.first.author).to eq(alice.person) + expect(Conversation.first.author).to eq(alice.person) + end + + it "dispatches the conversation" do + Conversation.create(author: alice.person, participant_ids: [alice.contacts.first.person.id, alice.person.id], + subject: "not spam", messages_attributes: [{author: alice.person, text: "cool stuff"}]) + + expect(Diaspora::Federation::Dispatcher).to receive(:defer_dispatch) + post :create, @hash + end end - it 'does not create a message' do - count = Message.count - post :create, @hash - expect(Message.count).to eq(count) + context "with empty subject" do + before do + @hash = { + format: :js, + conversation: {subject: " ", text: "text debug"}, + contact_ids: [alice.contacts.first.id] + } + end + + it "creates a conversation" do + expect { post :create, @hash }.to change(Conversation, :count).by(1) + end + + it "creates a message" do + expect { post :create, @hash }.to change(Message, :count).by(1) + end + + it "responds with the conversation id as JSON" do + post :create, @hash + expect(response).to be_success + expect(JSON.parse(response.body)["id"]).to eq(Conversation.first.id) + end end - it "responds with an error message" do - post :create, @hash - expect(response).not_to be_success - expect(response.body).to eq(I18n.t("conversations.create.fail")) - end - end + context "with empty text" do + before do + @hash = { + format: :js, + conversation: {subject: "secret stuff", text: " "}, + contact_ids: [alice.contacts.first.id] + } + end - context 'with empty contact' do - before do - @hash = { - :format => :js, - :conversation => { - :subject => 'secret stuff', - :text => 'text debug' - }, - :contact_ids => ' ' - } + it "does not create a conversation" do + count = Conversation.count + post :create, @hash + expect(Conversation.count).to eq(count) + end + + it "does not create a message" do + count = Message.count + post :create, @hash + expect(Message.count).to eq(count) + end + + it "responds with an error message" do + post :create, @hash + expect(response).not_to be_success + expect(response.body).to eq(I18n.t("conversations.create.fail")) + end end - it 'does not create a conversation' do - count = Conversation.count - post :create, @hash - expect(Conversation.count).to eq(count) + context "with empty contact" do + before do + @hash = { + format: :js, + conversation: {subject: "secret stuff", text: "text debug"}, + contact_ids: " " + } + end + + it "does not create a conversation" do + count = Conversation.count + post :create, @hash + expect(Conversation.count).to eq(count) + end + + it "does not create a message" do + count = Message.count + post :create, @hash + expect(Message.count).to eq(count) + end + + it "responds with an error message" do + post :create, @hash + expect(response).not_to be_success + expect(response.body).to eq(I18n.t("javascripts.conversation.create.no_recipient")) + end end - it 'does not create a message' do - count = Message.count - post :create, @hash - expect(Message.count).to eq(count) - end + context "with nil contact" do + before do + @hash = { + format: :js, + conversation: {subject: "secret stuff", text: "text debug"}, + contact_ids: nil + } + end - it "responds with an error message" do - post :create, @hash - expect(response).not_to be_success - expect(response.body).to eq(I18n.t("javascripts.conversation.create.no_recipient")) - end - end + it "does not create a conversation" do + count = Conversation.count + post :create, @hash + expect(Conversation.count).to eq(count) + end - context 'with nil contact' do - before do - @hash = { - :format => :js, - :conversation => { - :subject => 'secret stuff', - :text => 'text debug' - }, - :contact_ids => nil - } - end - - it 'does not create a conversation' do - count = Conversation.count - post :create, @hash - expect(Conversation.count).to eq(count) - end - - it 'does not create a message' do - count = Message.count - post :create, @hash - expect(Message.count).to eq(count) - end - - it "responds with an error message" do - post :create, @hash - expect(response).not_to be_success - expect(response.body).to eq(I18n.t("javascripts.conversation.create.no_recipient")) + it "does not create a message" do + count = Message.count + post :create, @hash + expect(Message.count).to eq(count) + end end end end - describe '#show' do + describe "#show" do before do hash = { - :author => alice.person, - :participant_ids => [alice.contacts.first.person.id, alice.person.id], - :subject => 'not spam', - :messages_attributes => [ {:author => alice.person, :text => 'cool stuff'} ] + author: alice.person, + participant_ids: [alice.contacts.first.person.id, alice.person.id], + subject: "not spam", + messages_attributes: [{author: alice.person, text: "cool stuff"}] } @conversation = Conversation.create(hash) end - it 'succeeds with json' do + it "succeeds with json" do get :show, :id => @conversation.id, :format => :json expect(response).to be_success expect(assigns[:conversation]).to eq(@conversation) expect(response.body).to include @conversation.guid end - it 'redirects to index' do + it "redirects to index" do get :show, :id => @conversation.id expect(response).to redirect_to(conversations_path(:conversation_id => @conversation.id)) end diff --git a/spec/controllers/jasmine_fixtures/conversations_spec.rb b/spec/controllers/jasmine_fixtures/conversations_spec.rb index c07360d69..ac74bb7f8 100644 --- a/spec/controllers/jasmine_fixtures/conversations_spec.rb +++ b/spec/controllers/jasmine_fixtures/conversations_spec.rb @@ -29,6 +29,9 @@ describe ConversationsController, :type => :controller do get :index, :conversation_id => @conv1.id save_fixture(html_for("body"), "conversations_read") + + get :new, modal: true + save_fixture(response.body, "conversations_modal") end end diff --git a/spec/javascripts/app/pages/contacts_spec.js b/spec/javascripts/app/pages/contacts_spec.js index a40b899d2..8574e6a5f 100644 --- a/spec/javascripts/app/pages/contacts_spec.js +++ b/spec/javascripts/app/pages/contacts_spec.js @@ -277,4 +277,31 @@ describe("app.pages.Contacts", function(){ }); }); }); + + describe("showMessageModal", function() { + beforeEach(function() { + $("body").append("
").append(spec.readFixture("conversations_modal")); + }); + + it("calls app.helpers.showModal", function() { + spyOn(app.helpers, "showModal"); + this.view.showMessageModal(); + expect(app.helpers.showModal); + }); + + it("app.views.ConversationsForm with correct parameters when modal is loaded", function() { + gon.conversationPrefill = [ + {id: 1, name: "diaspora user", handle: "diaspora-user@pod.tld"}, + {id: 2, name: "other diaspora user", handle: "other-diaspora-user@pod.tld"}, + {id: 3, name: "user@pod.tld", handle: "user@pod.tld"} + ]; + + spyOn(app.views.ConversationsForm.prototype, "initialize"); + this.view.showMessageModal(); + $("#conversationModal").trigger("modal:loaded"); + expect($("#conversationModal").length).toBe(1); + expect(app.views.ConversationsForm.prototype.initialize) + .toHaveBeenCalledWith({prefill: gon.conversationPrefill}); + }); + }); }); diff --git a/spec/javascripts/app/views/conversations_form_view_spec.js b/spec/javascripts/app/views/conversations_form_view_spec.js index 989469670..97760a067 100644 --- a/spec/javascripts/app/views/conversations_form_view_spec.js +++ b/spec/javascripts/app/views/conversations_form_view_spec.js @@ -1,12 +1,133 @@ describe("app.views.ConversationsForm", function() { beforeEach(function() { spec.loadFixture("conversations_read"); + this.target = new app.views.ConversationsForm(); + }); + + describe("initialize", function() { + it("initializes the conversation participants list", function() { + expect(this.target.conversationRecipients).toEqual([]); + }); + + it("initializes the search view", function() { + spyOn(app.views.SearchBase.prototype, "initialize"); + this.target.initialize(); + expect(app.views.SearchBase.prototype.initialize).toHaveBeenCalled(); + expect(app.views.SearchBase.prototype.initialize.calls.argsFor(0)[0].customSearch).toBe(true); + expect(app.views.SearchBase.prototype.initialize.calls.argsFor(0)[0].autoselect).toBe(true); + expect(app.views.SearchBase.prototype.initialize.calls.argsFor(0)[0].remoteRoute).toEqual({ + url: "/contacts", + extraParameters: "mutual=true" + }); + expect(this.target.search).toBeDefined(); + }); + + it("calls bindTypeaheadEvents", function() { + spyOn(app.views.ConversationsForm.prototype, "bindTypeaheadEvents"); + this.target.initialize(); + expect(app.views.ConversationsForm.prototype.bindTypeaheadEvents).toHaveBeenCalled(); + }); + + it("calls prefill correctly", function() { + spyOn(app.views.ConversationsForm.prototype, "prefill"); + this.target.initialize(); + expect(app.views.ConversationsForm.prototype.prefill).not.toHaveBeenCalled(); + this.target.initialize({prefill: {}}); + expect(app.views.ConversationsForm.prototype.prefill).toHaveBeenCalledWith({}); + }); + }); + + describe("addRecipient", function() { + beforeEach(function() { + $("#conversation-new").removeClass("hidden"); + $("#conversation-show").addClass("hidden"); + }); + + it("add the participant", function() { + expect(this.target.conversationRecipients).toEqual([]); + this.target.addRecipient({name: "diaspora user", handle: "diaspora-user@pod.tld"}); + expect(this.target.conversationRecipients).toEqual([{name: "diaspora user", handle: "diaspora-user@pod.tld"}]); + }); + + it("call updateContactIdsListInput", function() { + spyOn(app.views.ConversationsForm.prototype, "updateContactIdsListInput"); + this.target.addRecipient({name: "diaspora user", handle: "diaspora-user@pod.tld"}); + expect(app.views.ConversationsForm.prototype.updateContactIdsListInput).toHaveBeenCalled(); + }); + + it("adds a recipient tag", function() { + expect($(".conversation-recipient-tag").length).toBe(0); + this.target.addRecipient({name: "diaspora user", handle: "diaspora-user@pod.tld"}); + expect($(".conversation-recipient-tag").length).toBe(1); + }); + }); + + describe("prefill", function() { + beforeEach(function() { + this.prefills = [{name: "diaspora user"}, {name: "other diaspora user"}, {name: "user"}]; + }); + + it("call addRecipient for each prefilled participant", function() { + spyOn(app.views.ConversationsForm.prototype, "addRecipient"); + this.target.prefill(this.prefills); + expect(app.views.ConversationsForm.prototype.addRecipient).toHaveBeenCalledTimes(this.prefills.length); + var allArgsFlattened = app.views.ConversationsForm.prototype.addRecipient.calls.allArgs().map(function(arg) { + return arg[0]; + }); + expect(allArgsFlattened).toEqual(this.prefills); + }); + }); + + describe("updateContactIdsListInput", function() { + beforeEach(function() { + this.target.conversationRecipients.push({id: 1, name: "diaspora user", handle: "diaspora-user@pod.tld"}); + this.target.conversationRecipients + .push({id: 2, name: "other diaspora user", handle: "other-diaspora-user@pod.tld"}); + this.target.conversationRecipients.push({id: 3, name: "user@pod.tld", handle: "user@pod.tld"}); + }); + + it("updates hidden input value", function() { + this.target.updateContactIdsListInput(); + expect(this.target.contactsIdsListInput.val()).toBe("1,2,3"); + }); + + it("calls app.views.SearchBase.ignorePersonForSuggestions() for each participant", function() { + spyOn(app.views.SearchBase.prototype, "ignorePersonForSuggestions"); + this.target.updateContactIdsListInput(); + expect(app.views.SearchBase.prototype.ignorePersonForSuggestions).toHaveBeenCalledTimes(3); + expect(app.views.SearchBase.prototype.ignorePersonForSuggestions.calls.argsFor(0)[0]) + .toEqual({id: 1, name: "diaspora user", handle: "diaspora-user@pod.tld"}); + expect(app.views.SearchBase.prototype.ignorePersonForSuggestions.calls.argsFor(1)[0]) + .toEqual({id: 2, name: "other diaspora user", handle: "other-diaspora-user@pod.tld"}); + expect(app.views.SearchBase.prototype.ignorePersonForSuggestions.calls.argsFor(2)[0]) + .toEqual({id: 3, name: "user@pod.tld", handle: "user@pod.tld"}); + }); + }); + + describe("bindTypeaheadEvents", function() { + it("calls onSuggestionSelection() when clicking on a result", function() { + spyOn(app.views.ConversationsForm.prototype, "onSuggestionSelection"); + var event = $.Event("typeahead:select"); + var person = {name: "diaspora user"}; + this.target.typeaheadElement.trigger(event, [person]); + expect(app.views.ConversationsForm.prototype.onSuggestionSelection).toHaveBeenCalledWith(person); + }); + }); + + describe("onSuggestionSelection", function() { + it("calls addRecipient, updateContactIdsListInput and $.fn.typeahead", function() { + spyOn(app.views.ConversationsForm.prototype, "addRecipient"); + spyOn($.fn, "typeahead"); + var person = {name: "diaspora user"}; + this.target.onSuggestionSelection(person); + expect(app.views.ConversationsForm.prototype.addRecipient).toHaveBeenCalledWith(person); + expect($.fn.typeahead).toHaveBeenCalledWith("val", ""); + }); }); describe("keyDown", function() { beforeEach(function() { this.submitCallback = jasmine.createSpy().and.returnValue(false); - new app.views.ConversationsForm(); }); context("on new message form", function() { @@ -52,6 +173,54 @@ describe("app.views.ConversationsForm", function() { }); }); + describe("removeRecipient", function() { + beforeEach(function() { + this.target.addRecipient({id: 1, name: "diaspora user", handle: "diaspora-user@pod.tld"}); + this.target.addRecipient({id: 2, name: "other diaspora user", handle: "other-diaspora-user@pod.tld"}); + this.target.addRecipient({id: 3, name: "user@pod.tld", handle: "user@pod.tld"}); + }); + + it("removes the user from conversation recipients when clicking the tag's remove button", function() { + expect(this.target.conversationRecipients).toEqual([ + {id: 1, name: "diaspora user", handle: "diaspora-user@pod.tld"}, + {id: 2, name: "other diaspora user", handle: "other-diaspora-user@pod.tld"}, + {id: 3, name: "user@pod.tld", handle: "user@pod.tld"} + ]); + + $("[data-diaspora-handle='diaspora-user@pod.tld'] .remove").click(); + + expect(this.target.conversationRecipients).toEqual([ + {id: 2, name: "other diaspora user", handle: "other-diaspora-user@pod.tld"}, + {id: 3, name: "user@pod.tld", handle: "user@pod.tld"} + ]); + + $("[data-diaspora-handle='other-diaspora-user@pod.tld'] .remove").click(); + + expect(this.target.conversationRecipients).toEqual([ + {id: 3, name: "user@pod.tld", handle: "user@pod.tld"} + ]); + + $("[data-diaspora-handle='user@pod.tld'] .remove").click(); + + expect(this.target.conversationRecipients).toEqual([]); + }); + + it("removes the tag element when clicking the tag's remove button", function() { + expect($("[data-diaspora-handle='diaspora-user@pod.tld']").length).toBe(1); + $("[data-diaspora-handle='diaspora-user@pod.tld'] .remove").click(); + expect($("[data-diaspora-handle='other-diaspora-user@pod.tld']").length).toBe(1); + $("[data-diaspora-handle='other-diaspora-user@pod.tld'] .remove").click(); + expect($("[data-diaspora-handle='user@pod.tld']").length).toBe(1); + $("[data-diaspora-handle='user@pod.tld'] .remove").click(); + }); + + it("calls updateContactIdsListInput", function() { + spyOn(app.views.ConversationsForm.prototype, "updateContactIdsListInput"); + $("[data-diaspora-handle='diaspora-user@pod.tld'] .remove").click(); + expect(app.views.ConversationsForm.prototype.updateContactIdsListInput).toHaveBeenCalled(); + }); + }); + describe("conversationCreateSuccess", function() { it("is called when there was a successful ajax request for the conversation form", function() { spyOn(app.views.ConversationsForm.prototype, "conversationCreateSuccess"); diff --git a/spec/javascripts/app/views/profile_header_view_spec.js b/spec/javascripts/app/views/profile_header_view_spec.js index 724cc7260..f6e78d934 100644 --- a/spec/javascripts/app/views/profile_header_view_spec.js +++ b/spec/javascripts/app/views/profile_header_view_spec.js @@ -30,4 +30,30 @@ describe("app.views.ProfileHeader", function() { })); }); }); + + describe("showMessageModal", function() { + beforeEach(function() { + $("body").append("
").append(spec.readFixture("conversations_modal")); + }); + + it("calls app.helpers.showModal", function() { + spyOn(app.helpers, "showModal"); + this.view.showMessageModal(); + expect(app.helpers.showModal); + }); + + it("app.views.ConversationsForm with correct parameterswhen modal is loaded", function() { + gon.conversationPrefill = [ + {id: 1, name: "diaspora user", handle: "diaspora-user@pod.tld"}, + {id: 2, name: "other diaspora user", handle: "other-diaspora-user@pod.tld"}, + {id: 3, name: "user@pod.tld", handle: "user@pod.tld"} + ]; + + spyOn(app.views.ConversationsForm.prototype, "initialize"); + this.view.showMessageModal(); + $("#conversationModal").trigger("modal:loaded"); + expect(app.views.ConversationsForm.prototype.initialize) + .toHaveBeenCalledWith({prefill: gon.conversationPrefill}); + }); + }); }); diff --git a/spec/javascripts/app/views/publisher_mention_view_spec.js b/spec/javascripts/app/views/publisher_mention_view_spec.js index 436e5e674..d4e9417e3 100644 --- a/spec/javascripts/app/views/publisher_mention_view_spec.js +++ b/spec/javascripts/app/views/publisher_mention_view_spec.js @@ -19,7 +19,7 @@ describe("app.views.PublisherMention", function() { expect(call.args[0].typeaheadInput.selector).toBe("#publisher .typeahead-mention-box"); expect(call.args[0].customSearch).toBeTruthy(); expect(call.args[0].autoselect).toBeTruthy(); - expect(call.args[0].remoteRoute).toBe("/contacts"); + expect(call.args[0].remoteRoute).toEqual({url: "/contacts"}); }); it("calls bindTypeaheadEvents", function() { diff --git a/spec/javascripts/app/views/search_view_spec.js b/spec/javascripts/app/views/search_view_spec.js index 74e4831eb..ffb652304 100644 --- a/spec/javascripts/app/views/search_view_spec.js +++ b/spec/javascripts/app/views/search_view_spec.js @@ -11,7 +11,7 @@ describe("app.views.Search", function() { this.view = new app.views.Search({el: "#search_people_form"}); var call = app.views.SearchBase.prototype.initialize.calls.mostRecent(); expect(call.args[0].typeaheadInput.selector).toBe("#search_people_form #q"); - expect(call.args[0].remoteRoute).toBe("/search"); + expect(call.args[0].remoteRoute).toEqual({url: "/search"}); }); it("binds typeahead:select", function() {