From 1356e40f22c555f676777ed9385a12b09c19fdce Mon Sep 17 00:00:00 2001
From: Luke Bennett <lukeeeebennettplus@gmail.com>
Date: Thu, 13 Oct 2016 04:33:09 +0100
Subject: [PATCH] Changed autocomplete_sources into an action that returns a
 single 'at' type of sources at a time

Finished up autocomplete_sources action and added frontend to fetch data only when its needed

Added wait_for_ajax to specs

Fixed builds and improved the setup/destroy lifecycle

Changed global namespace and DRYed up loading logic

Added safety for accidentally loading data twice

Removed destroy as its not necessary and is messing with click events from a blur race condition

Created AutocompleteSourcesController and updated routes

Fixed @undefined from tabbing before load ends

Disable tabSelectsMatch until we have loaded data

Review changes
---
 .../javascripts/gfm_auto_complete.js.es6      | 233 +++++++++---------
 app/assets/javascripts/gl_form.js             |   2 +-
 app/assets/javascripts/issuable_form.js       |   2 +-
 .../autocomplete_sources_controller.rb        |  48 ++++
 app/controllers/projects_controller.rb        |  33 ---
 .../layouts/_init_auto_complete.html.haml     |  14 +-
 .../18435-autocomplete-is-not-performant.yml  |   4 +
 config/routes/project.rb                      |  13 +-
 .../participants_autocomplete_spec.rb         |   7 +-
 .../projects/gfm_autocomplete_load_spec.rb    |   4 +-
 spec/routing/project_routing_spec.rb          |  19 +-
 11 files changed, 210 insertions(+), 169 deletions(-)
 create mode 100644 app/controllers/projects/autocomplete_sources_controller.rb
 create mode 100644 changelogs/unreleased/18435-autocomplete-is-not-performant.yml

diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6
index 245383438d1..dda061a556b 100644
--- a/app/assets/javascripts/gfm_auto_complete.js.es6
+++ b/app/assets/javascripts/gfm_auto_complete.js.es6
@@ -2,19 +2,28 @@
 
 // Creates the variables for setting up GFM auto-completion
 (function() {
-  if (window.GitLab == null) {
-    window.GitLab = {};
+  if (window.gl == null) {
+    window.gl = {};
   }
 
   function sanitize(str) {
     return str.replace(/<(?:.|\n)*?>/gm, '');
   }
 
-  window.GitLab.GfmAutoComplete = {
-    dataLoading: false,
-    dataLoaded: false,
+  window.gl.GfmAutoComplete = {
+    dataSources: {},
+    defaultLoadingData: ['loading'],
     cachedData: {},
-    dataSource: '',
+    isLoadingData: {},
+    atTypeMap: {
+      ':': 'emojis',
+      '@': 'members',
+      '#': 'issues',
+      '!': 'mergeRequests',
+      '~': 'labels',
+      '%': 'milestones',
+      '/': 'commands'
+    },
     // Emoji
     Emoji: {
       template: '<li>${name} <img alt="${name}" height="20" src="${path}" width="20" /></li>'
@@ -35,33 +44,31 @@
       template: '<li>${title}</li>'
     },
     Loading: {
-      template: '<li><i class="fa fa-refresh fa-spin"></i> Loading...</li>'
+      template: '<li style="pointer-events: none;"><i class="fa fa-refresh fa-spin"></i> Loading...</li>'
     },
     DefaultOptions: {
       sorter: function(query, items, searchKey) {
-        // Highlight first item only if at least one char was typed
-        this.setting.highlightFirst = this.setting.alwaysHighlightFirst || query.length > 0;
-        if ((items[0].name != null) && items[0].name === 'loading') {
+        if (gl.GfmAutoComplete.isLoading(items)) {
           return items;
         }
         return $.fn.atwho["default"].callbacks.sorter(query, items, searchKey);
       },
       filter: function(query, data, searchKey) {
-        if (data[0] === 'loading') {
+        if (gl.GfmAutoComplete.isLoading(data)) {
+          gl.GfmAutoComplete.togglePreventSelection.call(this, true);
+          gl.GfmAutoComplete.fetchData(this.$inputor, this.at);
           return data;
+        } else {
+          gl.GfmAutoComplete.togglePreventSelection.call(this, false);
+          return $.fn.atwho["default"].callbacks.filter(query, data, searchKey);
         }
-        return $.fn.atwho["default"].callbacks.filter(query, data, searchKey);
       },
       beforeInsert: function(value) {
         if (value && !this.setting.skipSpecialCharacterTest) {
           var withoutAt = value.substring(1);
           if (withoutAt && /[^\w\d]/.test(withoutAt)) value = value.charAt() + '"' + withoutAt + '"';
         }
-        if (!window.GitLab.GfmAutoComplete.dataLoaded) {
-          return this.at;
-        } else {
-          return value;
-        }
+        return value;
       },
       matcher: function (flag, subtext) {
         // The below is taken from At.js source
@@ -85,69 +92,46 @@
         }
       }
     },
-    setup: _.debounce(function(input) {
+    setup: function(input) {
       // Add GFM auto-completion to all input fields, that accept GFM input.
       this.input = input || $('.js-gfm-input');
-      // destroy previous instances
-      this.destroyAtWho();
-      // set up instances
-      this.setupAtWho();
-
-      if (this.dataSource && !this.dataLoading && !this.cachedData) {
-        this.dataLoading = true;
-        return this.fetchData(this.dataSource)
-          .done((data) => {
-            this.dataLoading = false;
-            this.loadData(data);
-          });
-        };
-
-      if (this.cachedData != null) {
-        return this.loadData(this.cachedData);
-      }
-    }, 1000),
-    setupAtWho: function() {
+      this.setupLifecycle();
+    },
+    setupLifecycle() {
+      this.input.each((i, input) => {
+        const $input = $(input);
+        $input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input));
+      });
+    },
+    setupAtWho: function($input) {
       // Emoji
-      this.input.atwho({
+      $input.atwho({
         at: ':',
-        displayTpl: (function(_this) {
-          return function(value) {
-            if (value.path != null) {
-              return _this.Emoji.template;
-            } else {
-              return _this.Loading.template;
-            }
-          };
-        })(this),
+        displayTpl: function(value) {
+          return value.path != null ? this.Emoji.template : this.Loading.template;
+        }.bind(this),
         insertTpl: ':${name}:',
-        data: ['loading'],
         startWithSpace: false,
         skipSpecialCharacterTest: true,
+        data: this.defaultLoadingData,
         callbacks: {
           sorter: this.DefaultOptions.sorter,
-          filter: this.DefaultOptions.filter,
           beforeInsert: this.DefaultOptions.beforeInsert,
-          matcher: this.DefaultOptions.matcher
+          filter: this.DefaultOptions.filter
         }
       });
       // Team Members
-      this.input.atwho({
+      $input.atwho({
         at: '@',
-        displayTpl: (function(_this) {
-          return function(value) {
-            if (value.username != null) {
-              return _this.Members.template;
-            } else {
-              return _this.Loading.template;
-            }
-          };
-        })(this),
+        displayTpl: function(value) {
+          return value.username != null ? this.Members.template : this.Loading.template;
+        }.bind(this),
         insertTpl: '${atwho-at}${username}',
         searchKey: 'search',
-        data: ['loading'],
         startWithSpace: false,
         alwaysHighlightFirst: true,
         skipSpecialCharacterTest: true,
+        data: this.defaultLoadingData,
         callbacks: {
           sorter: this.DefaultOptions.sorter,
           filter: this.DefaultOptions.filter,
@@ -178,20 +162,14 @@
           }
         }
       });
-      this.input.atwho({
+      $input.atwho({
         at: '#',
         alias: 'issues',
         searchKey: 'search',
-        displayTpl: (function(_this) {
-          return function(value) {
-            if (value.title != null) {
-              return _this.Issues.template;
-            } else {
-              return _this.Loading.template;
-            }
-          };
-        })(this),
-        data: ['loading'],
+        displayTpl: function(value) {
+          return value.title != null ? this.Issues.template : this.Loading.template;
+        }.bind(this),
+        data: this.defaultLoadingData,
         insertTpl: '${atwho-at}${id}',
         startWithSpace: false,
         callbacks: {
@@ -213,26 +191,21 @@
           }
         }
       });
-      this.input.atwho({
+      $input.atwho({
         at: '%',
         alias: 'milestones',
         searchKey: 'search',
-        displayTpl: (function(_this) {
-          return function(value) {
-            if (value.title != null) {
-              return _this.Milestones.template;
-            } else {
-              return _this.Loading.template;
-            }
-          };
-        })(this),
         insertTpl: '${atwho-at}${title}',
-        data: ['loading'],
+        displayTpl: function(value) {
+          return value.title != null ? this.Milestones.template : this.Loading.template;
+        }.bind(this),
         startWithSpace: false,
+        data: this.defaultLoadingData,
         callbacks: {
           matcher: this.DefaultOptions.matcher,
           sorter: this.DefaultOptions.sorter,
           beforeInsert: this.DefaultOptions.beforeInsert,
+          filter: this.DefaultOptions.filter,
           beforeSave: function(milestones) {
             return $.map(milestones, function(m) {
               if (m.title == null) {
@@ -247,21 +220,15 @@
           }
         }
       });
-      this.input.atwho({
+      $input.atwho({
         at: '!',
         alias: 'mergerequests',
         searchKey: 'search',
-        displayTpl: (function(_this) {
-          return function(value) {
-            if (value.title != null) {
-              return _this.Issues.template;
-            } else {
-              return _this.Loading.template;
-            }
-          };
-        })(this),
-        data: ['loading'],
         startWithSpace: false,
+        displayTpl: function(value) {
+          return value.title != null ? this.Issues.template : this.Loading.template;
+        }.bind(this),
+        data: this.defaultLoadingData,
         insertTpl: '${atwho-at}${id}',
         callbacks: {
           sorter: this.DefaultOptions.sorter,
@@ -282,18 +249,31 @@
           }
         }
       });
-      this.input.atwho({
+      $input.atwho({
         at: '~',
         alias: 'labels',
         searchKey: 'search',
-        displayTpl: this.Labels.template,
+        data: this.defaultLoadingData,
+        displayTpl: function(value) {
+          return this.isLoading(value) ? this.Loading.template : this.Labels.template;
+        }.bind(this),
         insertTpl: '${atwho-at}${title}',
         startWithSpace: false,
         callbacks: {
           matcher: this.DefaultOptions.matcher,
           sorter: this.DefaultOptions.sorter,
           beforeInsert: this.DefaultOptions.beforeInsert,
+          filter: this.DefaultOptions.filter,
           beforeSave: function(merges) {
+            if (gl.GfmAutoComplete.isLoading(merges)) return merges;
+            var sanitizeLabelTitle;
+            sanitizeLabelTitle = function(title) {
+              if (/[\w\?&]+\s+[\w\?&]+/g.test(title)) {
+                return "\"" + (sanitize(title)) + "\"";
+              } else {
+                return sanitize(title);
+              }
+            };
             return $.map(merges, function(m) {
               return {
                 title: sanitize(m.title),
@@ -305,12 +285,14 @@
         }
       });
       // We don't instantiate the slash commands autocomplete for note and issue/MR edit forms
-      this.input.filter('[data-supports-slash-commands="true"]').atwho({
+      $input.filter('[data-supports-slash-commands="true"]').atwho({
         at: '/',
         alias: 'commands',
         searchKey: 'search',
         skipSpecialCharacterTest: true,
+        data: this.defaultLoadingData,
         displayTpl: function(value) {
+          if (this.isLoading(value)) return this.Loading.template;
           var tpl = '<li>/${name}';
           if (value.aliases.length > 0) {
             tpl += ' <small>(or /<%- aliases.join(", /") %>)</small>';
@@ -323,7 +305,7 @@
           }
           tpl += '</li>';
           return _.template(tpl)(value);
-        },
+        }.bind(this),
         insertTpl: function(value) {
           var tpl = "/${name} ";
           var reference_prefix = null;
@@ -341,6 +323,7 @@
           filter: this.DefaultOptions.filter,
           beforeInsert: this.DefaultOptions.beforeInsert,
           beforeSave: function(commands) {
+            if (gl.GfmAutoComplete.isLoading(commands)) return commands;
             return $.map(commands, function(c) {
               var search = c.name;
               if (c.aliases.length > 0) {
@@ -368,32 +351,40 @@
       });
       return;
     },
-    destroyAtWho: function() {
-      return this.input.atwho('destroy');
-    },
-    fetchData: function(dataSource) {
-      return $.getJSON(dataSource);
+    fetchData: function($input, at) {
+      if (this.isLoadingData[at]) return;
+      this.isLoadingData[at] = true;
+      if (this.cachedData[at]) {
+        this.loadData($input, at, this.cachedData[at]);
+      } else {
+        $.getJSON(this.dataSources[this.atTypeMap[at]], (data) => {
+          this.loadData($input, at, data);
+        }).fail(() => { this.isLoadingData[at] = false; });
+      }
     },
-    loadData: function(data) {
-      this.cachedData = data;
-      this.dataLoaded = true;
-      // load members
-      this.input.atwho('load', '@', data.members);
-      // load issues
-      this.input.atwho('load', 'issues', data.issues);
-      // load milestones
-      this.input.atwho('load', 'milestones', data.milestones);
-      // load merge requests
-      this.input.atwho('load', 'mergerequests', data.mergerequests);
-      // load emojis
-      this.input.atwho('load', ':', data.emojis);
-      // load labels
-      this.input.atwho('load', '~', data.labels);
-      // load commands
-      this.input.atwho('load', '/', data.commands);
+    loadData: function($input, at, data) {
+      this.isLoadingData[at] = false;
+      this.cachedData[at] = data;
+      $input.atwho('load', at, data);
       // This trigger at.js again
       // otherwise we would be stuck with loading until the user types
-      return $(':focus').trigger('keyup');
+      return $input.trigger('keyup');
+    },
+    isLoading(data) {
+      if (!data) return false;
+      if (Array.isArray(data)) data = data[0];
+      return data === this.defaultLoadingData[0] || data.name === this.defaultLoadingData[0];
+    },
+    togglePreventSelection(isPrevented = !!this.setting.tabSelectsMatch) {
+      this.setting.tabSelectsMatch = !isPrevented;
+      this.setting.spaceSelectsMatch = !isPrevented;
+      const eventListenerAction = `${isPrevented ? 'add' : 'remove'}EventListener`;
+      this.$inputor[0][eventListenerAction]('keydown', gl.GfmAutoComplete.preventSpaceTabEnter);
+    },
+    preventSpaceTabEnter(e) {
+      const key = e.which || e.keyCode;
+      const preventables = [9, 13, 32];
+      if (preventables.indexOf(key) > -1) e.preventDefault();
     }
   };
 
diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js
index 56a33eeaad5..7dc2d13e5d8 100644
--- a/app/assets/javascripts/gl_form.js
+++ b/app/assets/javascripts/gl_form.js
@@ -30,7 +30,7 @@
         this.form.addClass('gfm-form');
         // remove notify commit author checkbox for non-commit notes
         gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button'));
-        GitLab.GfmAutoComplete.setup(this.form.find('.js-gfm-input'));
+        gl.GfmAutoComplete.setup(this.form.find('.js-gfm-input'));
         new DropzoneInput(this.form);
         autosize(this.textarea);
         // form and textarea event listeners
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js
index 2f3cad13cc0..1c4086517fe 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable_form.js
@@ -19,7 +19,7 @@
       this.renderWipExplanation = bind(this.renderWipExplanation, this);
       this.resetAutosave = bind(this.resetAutosave, this);
       this.handleSubmit = bind(this.handleSubmit, this);
-      GitLab.GfmAutoComplete.setup();
+      gl.GfmAutoComplete.setup();
       new UsersSelect();
       new ZenMode();
       this.titleField = this.form.find("input[name*='[title]']");
diff --git a/app/controllers/projects/autocomplete_sources_controller.rb b/app/controllers/projects/autocomplete_sources_controller.rb
new file mode 100644
index 00000000000..d9dfa534669
--- /dev/null
+++ b/app/controllers/projects/autocomplete_sources_controller.rb
@@ -0,0 +1,48 @@
+class Projects::AutocompleteSourcesController < Projects::ApplicationController
+  before_action :load_autocomplete_service, except: [:emojis, :members]
+
+  def emojis
+    render json: Gitlab::AwardEmoji.urls
+  end
+
+  def members
+    render json: ::Projects::ParticipantsService.new(@project, current_user).execute(noteable)
+  end
+
+  def issues
+    render json: @autocomplete_service.issues
+  end
+
+  def merge_requests
+    render json: @autocomplete_service.merge_requests
+  end
+
+  def labels
+    render json: @autocomplete_service.labels
+  end
+
+  def milestones
+    render json: @autocomplete_service.milestones
+  end
+
+  def commands
+    render json: @autocomplete_service.commands(noteable, params[:type])
+  end
+
+  private
+
+  def load_autocomplete_service
+    @autocomplete_service = ::Projects::AutocompleteService.new(@project, current_user)
+  end
+
+  def noteable
+    case params[:type]
+    when 'Issue'
+      IssuesFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:type_id])
+    when 'MergeRequest'
+      MergeRequestsFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:type_id])
+    when 'Commit'
+      @project.commit(params[:type_id])
+    end
+  end
+end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index a8a18b4fa16..d5ee503c44c 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -127,39 +127,6 @@ class ProjectsController < Projects::ApplicationController
     redirect_to edit_project_path(@project), alert: ex.message
   end
 
-  def autocomplete_sources
-    noteable =
-      case params[:type]
-      when 'Issue'
-        IssuesFinder.new(current_user, project_id: @project.id).
-          execute.find_by(iid: params[:type_id])
-      when 'MergeRequest'
-        MergeRequestsFinder.new(current_user, project_id: @project.id).
-          execute.find_by(iid: params[:type_id])
-      when 'Commit'
-        @project.commit(params[:type_id])
-      else
-        nil
-      end
-
-    autocomplete = ::Projects::AutocompleteService.new(@project, current_user)
-    participants = ::Projects::ParticipantsService.new(@project, current_user).execute(noteable)
-
-    @suggestions = {
-      emojis: Gitlab::AwardEmoji.urls,
-      issues: autocomplete.issues,
-      milestones: autocomplete.milestones,
-      mergerequests: autocomplete.merge_requests,
-      labels: autocomplete.labels,
-      members: participants,
-      commands: autocomplete.commands(noteable, params[:type])
-    }
-
-    respond_to do |format|
-      format.json { render json: @suggestions }
-    end
-  end
-
   def new_issue_address
     return render_404 unless Gitlab::IncomingEmail.supports_issue_creation?
 
diff --git a/app/views/layouts/_init_auto_complete.html.haml b/app/views/layouts/_init_auto_complete.html.haml
index e138ebab018..3daa1e90a8c 100644
--- a/app/views/layouts/_init_auto_complete.html.haml
+++ b/app/views/layouts/_init_auto_complete.html.haml
@@ -3,6 +3,14 @@
 
 - if project
   :javascript
-    GitLab.GfmAutoComplete.dataSource = "#{autocomplete_sources_namespace_project_path(project.namespace, project, type: noteable_type, type_id: params[:id])}"
-    GitLab.GfmAutoComplete.cachedData = undefined;
-    GitLab.GfmAutoComplete.setup();
+    gl.GfmAutoComplete.dataSources = {
+      emojis: "#{emojis_namespace_project_autocomplete_sources_path(project.namespace, project)}",
+      members: "#{members_namespace_project_autocomplete_sources_path(project.namespace, project, type: noteable_type, type_id: params[:id])}",
+      issues: "#{issues_namespace_project_autocomplete_sources_path(project.namespace, project)}",
+      mergeRequests: "#{merge_requests_namespace_project_autocomplete_sources_path(project.namespace, project)}",
+      labels: "#{labels_namespace_project_autocomplete_sources_path(project.namespace, project)}",
+      milestones: "#{milestones_namespace_project_autocomplete_sources_path(project.namespace, project)}",
+      commands: "#{commands_namespace_project_autocomplete_sources_path(project.namespace, project, type: noteable_type, type_id: params[:id])}"
+    };
+
+    gl.GfmAutoComplete.setup();
diff --git a/changelogs/unreleased/18435-autocomplete-is-not-performant.yml b/changelogs/unreleased/18435-autocomplete-is-not-performant.yml
new file mode 100644
index 00000000000..019c55e27dc
--- /dev/null
+++ b/changelogs/unreleased/18435-autocomplete-is-not-performant.yml
@@ -0,0 +1,4 @@
+---
+title: Made comment autocomplete more performant and removed some loading bugs
+merge_request: 6856
+author: 
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 0754f0ec3b0..e17d6bae10c 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -11,6 +11,18 @@ constraints(ProjectUrlConstrainer.new) do
           module: :projects,
           as: :project) do
 
+      resources :autocomplete_sources, only: [] do
+        collection do
+          get 'emojis'
+          get 'members'
+          get 'issues'
+          get 'merge_requests'
+          get 'labels'
+          get 'milestones'
+          get 'commands'
+        end
+      end
+
       #
       # Templates
       #
@@ -316,7 +328,6 @@ constraints(ProjectUrlConstrainer.new) do
         post :remove_export
         post :generate_new_export
         get :download_export
-        get :autocomplete_sources
         get :activity
         get :refs
         put :new_issue_address
diff --git a/spec/features/participants_autocomplete_spec.rb b/spec/features/participants_autocomplete_spec.rb
index a78a1c9c890..c2545b0c259 100644
--- a/spec/features/participants_autocomplete_spec.rb
+++ b/spec/features/participants_autocomplete_spec.rb
@@ -1,6 +1,8 @@
 require 'spec_helper'
 
 feature 'Member autocomplete', feature: true do
+  include WaitForAjax
+
   let(:project) { create(:project, :public) }
   let(:user) { create(:user) }
   let(:participant) { create(:user) }
@@ -79,11 +81,10 @@ feature 'Member autocomplete', feature: true do
   end
 
   def open_member_suggestions
-    sleep 1
     page.within('.new-note') do
-      sleep 1
-      find('#note_note').native.send_keys('@')
+      find('#note_note').send_keys('@')
     end
+    wait_for_ajax
   end
 
   def visit_issue(project, issue)
diff --git a/spec/features/projects/gfm_autocomplete_load_spec.rb b/spec/features/projects/gfm_autocomplete_load_spec.rb
index 1921ea6d8ae..dd9622f16a0 100644
--- a/spec/features/projects/gfm_autocomplete_load_spec.rb
+++ b/spec/features/projects/gfm_autocomplete_load_spec.rb
@@ -10,12 +10,12 @@ describe 'GFM autocomplete loading', feature: true, js: true do
   end
 
   it 'does not load on project#show' do
-    expect(evaluate_script('GitLab.GfmAutoComplete.dataSource')).to eq('')
+    expect(evaluate_script('gl.GfmAutoComplete.dataSources')).to eq({})
   end
 
   it 'loads on new issue page' do
     visit new_namespace_project_issue_path(project.namespace, project)
 
-    expect(evaluate_script('GitLab.GfmAutoComplete.dataSource')).not_to eq('')
+    expect(evaluate_script('gl.GfmAutoComplete.dataSources')).not_to eq({})
   end
 end
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index b6e7da841b1..77549db2927 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -80,10 +80,6 @@ describe 'project routing' do
       expect(get('/gitlab/gitlabhq/edit')).to route_to('projects#edit', namespace_id: 'gitlab', id: 'gitlabhq')
     end
 
-    it 'to #autocomplete_sources' do
-      expect(get('/gitlab/gitlabhq/autocomplete_sources')).to route_to('projects#autocomplete_sources', namespace_id: 'gitlab', id: 'gitlabhq')
-    end
-
     describe 'to #show' do
       context 'regular name' do
         it { expect(get('/gitlab/gitlabhq')).to route_to('projects#show', namespace_id: 'gitlab', id: 'gitlabhq') }
@@ -117,6 +113,21 @@ describe 'project routing' do
     end
   end
 
+  # emojis_namespace_project_autocomplete_sources_path         GET /:project_id/autocomplete_sources/emojis(.:format)         projects/autocomplete_sources#emojis
+  # members_namespace_project_autocomplete_sources_path        GET /:project_id/autocomplete_sources/members(.:format)        projects/autocomplete_sources#members
+  # issues_namespace_project_autocomplete_sources_path         GET /:project_id/autocomplete_sources/issues(.:format)         projects/autocomplete_sources#issues
+  # merge_requests_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/merge_requests(.:format) projects/autocomplete_sources#merge_requests
+  # labels_namespace_project_autocomplete_sources_path         GET /:project_id/autocomplete_sources/labels(.:format)         projects/autocomplete_sources#labels
+  # milestones_namespace_project_autocomplete_sources_path     GET /:project_id/autocomplete_sources/milestones(.:format)     projects/autocomplete_sources#milestones
+  # commands_namespace_project_autocomplete_sources_path       GET /:project_id/autocomplete_sources/commands(.:format)       projects/autocomplete_sources#commands
+  describe Projects::AutocompleteSourcesController, 'routing' do
+    [:emojis, :members, :issues, :merge_requests, :labels, :milestones, :commands].each do |action|
+      it "to ##{action}" do
+        expect(get("/gitlab/gitlabhq/autocomplete_sources/#{action}")).to route_to("projects/autocomplete_sources##{action}", namespace_id: 'gitlab', project_id: 'gitlabhq')
+      end
+    end
+  end
+
   #  pages_project_wikis GET    /:project_id/wikis/pages(.:format)       projects/wikis#pages
   # history_project_wiki GET    /:project_id/wikis/:id/history(.:format) projects/wikis#history
   #        project_wikis POST   /:project_id/wikis(.:format)             projects/wikis#create
-- 
GitLab