Commit 956fb75f authored by Sean McGivern's avatar Sean McGivern

Merge branch '262060_02-project-filter-vue' into 'master'

Global Search - Project Filter

See merge request gitlab-org/gitlab!45711
parents 7eb4d992 f8a78696
...@@ -390,7 +390,10 @@ const Api = { ...@@ -390,7 +390,10 @@ const Api = {
params: { ...defaults, ...options }, params: { ...defaults, ...options },
}) })
.then(({ data }) => callback(data)) .then(({ data }) => callback(data))
.catch(() => flash(__('Something went wrong while fetching projects'))); .catch(() => {
flash(__('Something went wrong while fetching projects'));
callback();
});
}, },
commit(id, sha, params = {}) { commit(id, sha, params = {}) {
......
...@@ -2,6 +2,6 @@ import Search from './search'; ...@@ -2,6 +2,6 @@ import Search from './search';
import { initSearchApp } from '~/search'; import { initSearchApp } from '~/search';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
initSearchApp(); initSearchApp(); // Vue Bootstrap
return new Search(); // Deprecated Dropdown (Projects) return new Search(); // Legacy Search Methods
}); });
import $ from 'jquery'; import $ from 'jquery';
import setHighlightClass from 'ee_else_ce/search/highlight_blob_search_result'; import setHighlightClass from 'ee_else_ce/search/highlight_blob_search_result';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import { deprecatedCreateFlash as Flash } from '~/flash';
import Api from '~/api';
import { __ } from '~/locale';
import Project from '~/pages/projects/project'; import Project from '~/pages/projects/project';
import { visitUrl, queryToObject } from '~/lib/utils/url_utility'; import { visitUrl } from '~/lib/utils/url_utility';
import refreshCounts from './refresh_counts'; import refreshCounts from './refresh_counts';
export default class Search { export default class Search {
constructor() { constructor() {
setHighlightClass(); // Code Highlighting
const $projectDropdown = $('.js-search-project-dropdown');
this.searchInput = '.js-search-input'; this.searchInput = '.js-search-input';
this.searchClear = '.js-search-clear'; this.searchClear = '.js-search-clear';
const query = queryToObject(window.location.search); setHighlightClass(); // Code Highlighting
this.groupId = query?.group_id; this.eventListeners(); // Search Form Actions
this.eventListeners(); refreshCounts(); // Other Scope Tab Counts
refreshCounts(); Project.initRefSwitcher(); // Code Search Branch Picker
initDeprecatedJQueryDropdown($projectDropdown, {
selectable: true,
filterable: true,
filterRemote: true,
fieldName: 'project_id',
search: {
fields: ['name'],
},
data: (term, callback) => {
this.getProjectsData(term)
.then(data => {
data.unshift({
name_with_namespace: __('Any'),
});
data.splice(1, 0, { type: 'divider' });
return data;
})
.then(data => callback(data))
.catch(() => new Flash(__('Error fetching projects')));
},
id(obj) {
return obj.id;
},
text(obj) {
return obj.name_with_namespace;
},
clicked: () => Search.submitSearch(),
});
Project.initRefSwitcher();
} }
eventListeners() { eventListeners() {
...@@ -97,20 +58,4 @@ export default class Search { ...@@ -97,20 +58,4 @@ export default class Search {
visitUrl($target.href); visitUrl($target.href);
ev.stopPropagation(); ev.stopPropagation();
} }
getProjectsData(term) {
return new Promise(resolve => {
if (this.groupId) {
Api.groupProjects(this.groupId, term, {}, resolve);
} else {
Api.projects(
term,
{
order_by: 'id',
},
resolve,
);
}
});
}
} }
...@@ -16,6 +16,28 @@ export const fetchGroups = ({ commit }, search) => { ...@@ -16,6 +16,28 @@ export const fetchGroups = ({ commit }, search) => {
}); });
}; };
export const fetchProjects = ({ commit, state }, search) => {
commit(types.REQUEST_PROJECTS);
const groupId = state.query?.group_id;
const callback = data => {
if (data) {
commit(types.RECEIVE_PROJECTS_SUCCESS, data);
} else {
createFlash({ message: __('There was an error fetching projects') });
commit(types.RECEIVE_PROJECTS_ERROR);
}
};
if (groupId) {
Api.groupProjects(groupId, search, {}, callback);
} else {
// The .catch() is due to the API method not handling a rejection properly
Api.projects(search, { order_by: 'id' }, callback).catch(() => {
callback();
});
}
};
export const setQuery = ({ commit }, { key, value }) => { export const setQuery = ({ commit }, { key, value }) => {
commit(types.SET_QUERY, { key, value }); commit(types.SET_QUERY, { key, value });
}; };
......
...@@ -2,4 +2,8 @@ export const REQUEST_GROUPS = 'REQUEST_GROUPS'; ...@@ -2,4 +2,8 @@ export const REQUEST_GROUPS = 'REQUEST_GROUPS';
export const RECEIVE_GROUPS_SUCCESS = 'RECEIVE_GROUPS_SUCCESS'; export const RECEIVE_GROUPS_SUCCESS = 'RECEIVE_GROUPS_SUCCESS';
export const RECEIVE_GROUPS_ERROR = 'RECEIVE_GROUPS_ERROR'; export const RECEIVE_GROUPS_ERROR = 'RECEIVE_GROUPS_ERROR';
export const REQUEST_PROJECTS = 'REQUEST_PROJECTS';
export const RECEIVE_PROJECTS_SUCCESS = 'RECEIVE_PROJECTS_SUCCESS';
export const RECEIVE_PROJECTS_ERROR = 'RECEIVE_PROJECTS_ERROR';
export const SET_QUERY = 'SET_QUERY'; export const SET_QUERY = 'SET_QUERY';
...@@ -12,6 +12,17 @@ export default { ...@@ -12,6 +12,17 @@ export default {
state.fetchingGroups = false; state.fetchingGroups = false;
state.groups = []; state.groups = [];
}, },
[types.REQUEST_PROJECTS](state) {
state.fetchingProjects = true;
},
[types.RECEIVE_PROJECTS_SUCCESS](state, data) {
state.fetchingProjects = false;
state.projects = data;
},
[types.RECEIVE_PROJECTS_ERROR](state) {
state.fetchingProjects = false;
state.projects = [];
},
[types.SET_QUERY](state, { key, value }) { [types.SET_QUERY](state, { key, value }) {
state.query[key] = value; state.query[key] = value;
}, },
......
...@@ -2,5 +2,7 @@ const createState = ({ query }) => ({ ...@@ -2,5 +2,7 @@ const createState = ({ query }) => ({
query, query,
groups: [], groups: [],
fetchingGroups: false, fetchingGroups: false,
projects: [],
fetchingProjects: false,
}); });
export default createState; export default createState;
<script>
import { mapState, mapActions } from 'vuex';
import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
import SearchableDropdown from './searchable_dropdown.vue';
import { ANY_OPTION, GROUP_DATA, PROJECT_DATA } from '../constants';
export default {
name: 'ProjectFilter',
components: {
SearchableDropdown,
},
props: {
initialData: {
type: Object,
required: false,
default: () => null,
},
},
computed: {
...mapState(['projects', 'fetchingProjects']),
selectedProject() {
return this.initialData ? this.initialData : ANY_OPTION;
},
},
methods: {
...mapActions(['fetchProjects']),
handleProjectChange(project) {
// This determines if we need to update the group filter or not
const queryParams = {
...(project.namespace_id && { [GROUP_DATA.queryParam]: project.namespace_id }),
[PROJECT_DATA.queryParam]: project.id,
};
visitUrl(setUrlParams(queryParams));
},
},
PROJECT_DATA,
};
</script>
<template>
<searchable-dropdown
:header-text="$options.PROJECT_DATA.headerText"
:selected-display-value="$options.PROJECT_DATA.selectedDisplayValue"
:items-display-value="$options.PROJECT_DATA.itemsDisplayValue"
:loading="fetchingProjects"
:selected-item="selectedProject"
:items="projects"
@search="fetchProjects"
@change="handleProjectChange"
/>
</template>
...@@ -81,7 +81,7 @@ export default { ...@@ -81,7 +81,7 @@ export default {
<gl-dropdown <gl-dropdown
class="gl-w-full" class="gl-w-full"
menu-class="gl-w-full!" menu-class="gl-w-full!"
toggle-class="gl-text-truncate gl-reset-line-height!" toggle-class="gl-text-truncate"
:header-text="headerText" :header-text="headerText"
@show="$emit('search', searchText)" @show="$emit('search', searchText)"
@shown="$refs.searchBox.focusInput()" @shown="$refs.searchBox.focusInput()"
......
import Vue from 'vue'; import Vue from 'vue';
import Translate from '~/vue_shared/translate'; import Translate from '~/vue_shared/translate';
import GroupFilter from './components/group_filter.vue'; import GroupFilter from './components/group_filter.vue';
import ProjectFilter from './components/project_filter.vue';
Vue.use(Translate); Vue.use(Translate);
...@@ -33,6 +34,10 @@ const searchableDropdowns = [ ...@@ -33,6 +34,10 @@ const searchableDropdowns = [
id: 'js-search-group-dropdown', id: 'js-search-group-dropdown',
component: GroupFilter, component: GroupFilter,
}, },
{
id: 'js-search-project-dropdown',
component: ProjectFilter,
},
]; ];
export const initTopbar = store => export const initTopbar = store =>
......
...@@ -2,21 +2,13 @@ ...@@ -2,21 +2,13 @@
= hidden_field_tag :group_id, params[:group_id] = hidden_field_tag :group_id, params[:group_id]
- if params[:project_id].present? - if params[:project_id].present?
= hidden_field_tag :project_id, params[:project_id] = hidden_field_tag :project_id, params[:project_id]
- project_attributes = @project&.attributes&.slice('id', 'namespace_id', 'name')&.merge(name_with_namespace: @project&.name_with_namespace)
.dropdown.form-group.mb-lg-0.mx-lg-1.gl-p-0{ data: { testid: "group-filter" } } .dropdown.form-group.mb-lg-0.mx-lg-1.gl-p-0{ data: { testid: "group-filter" } }
%label.d-block{ for: "dashboard_search_group" } %label.d-block{ for: "dashboard_search_group" }
= _("Group") = _("Group")
%input#js-search-group-dropdown.dropdown-menu-toggle{ value: "Loading...", data: { "initial-data": @group.to_json } } %input#js-search-group-dropdown.dropdown-menu-toggle{ value: "Loading...", data: { "initial-data": @group.to_json } }
.dropdown.form-group.mb-lg-0.mx-lg-1{ data: { testid: "project-filter" } } .dropdown.form-group.mb-lg-0.mx-lg-1.gl-p-0{ data: { testid: "project-filter" } }
%label.d-block{ for: "dashboard_search_project" } %label.d-block{ for: "dashboard_search_project" }
= _("Project") = _("Project")
%button.dropdown-menu-toggle.gl-display-inline-flex.js-search-project-dropdown.gl-mt-0{ type: "button", id: "dashboard_search_project", data: { toggle: "dropdown" } } %input#js-search-project-dropdown.dropdown-menu-toggle{ value: "Loading...", data: { "initial-data": project_attributes.to_json } }
%span.dropdown-toggle-text.gl-flex-grow-1.str-truncated-100
= @project&.full_name || _("Any")
- if @project.present?
= link_to sprite_icon("clear"), url_for(safe_params.except(:project_id)), class: 'search-clear js-search-clear has-tooltip', title: _('Clear')
= sprite_icon("chevron-down", css_class: 'dropdown-menu-toggle-icon gl-top-3')
.dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-right
= dropdown_title(_("Filter results by project"))
= dropdown_filter(_("Search projects"))
= dropdown_content
= dropdown_loading
...@@ -7,9 +7,9 @@ ...@@ -7,9 +7,9 @@
.search-field-holder.form-group.mr-lg-1.mb-lg-0 .search-field-holder.form-group.mr-lg-1.mb-lg-0
%label{ for: "dashboard_search" } %label{ for: "dashboard_search" }
= _("What are you searching for?") = _("What are you searching for?")
.position-relative .gl-search-box-by-type
= search_field_tag :search, params[:search], placeholder: _("Search for projects, issues, etc."), class: "form-control search-text-input js-search-input", id: "dashboard_search", autofocus: true, spellcheck: false = search_field_tag :search, params[:search], placeholder: _("Search for projects, issues, etc."), class: "gl-form-input form-control search-text-input js-search-input", id: "dashboard_search", autofocus: true, spellcheck: false
= sprite_icon('search', css_class: 'search-icon') = sprite_icon('search', css_class: 'gl-search-box-by-type-search-icon gl-icon')
%button.search-clear.js-search-clear{ class: [("hidden" if params[:search].blank?), "has-tooltip"], type: "button", tabindex: "-1", title: _('Clear') } %button.search-clear.js-search-clear{ class: [("hidden" if params[:search].blank?), "has-tooltip"], type: "button", tabindex: "-1", title: _('Clear') }
= sprite_icon('clear') = sprite_icon('clear')
%span.sr-only %span.sr-only
...@@ -17,4 +17,4 @@ ...@@ -17,4 +17,4 @@
- unless params[:snippets].eql? 'true' - unless params[:snippets].eql? 'true'
= render 'filter' = render 'filter'
.d-flex-center.flex-column.flex-lg-row .d-flex-center.flex-column.flex-lg-row
= button_tag _("Search"), class: "gl-button btn btn-success btn-search form-control mt-lg-0 ml-lg-1 align-self-end" = button_tag _("Search"), class: "gl-button btn btn-success btn-search mt-lg-0 ml-lg-1 align-self-end"
...@@ -11037,9 +11037,6 @@ msgstr "" ...@@ -11037,9 +11037,6 @@ msgstr ""
msgid "Error fetching payload data." msgid "Error fetching payload data."
msgstr "" msgstr ""
msgid "Error fetching projects"
msgstr ""
msgid "Error fetching refs" msgid "Error fetching refs"
msgstr "" msgstr ""
...@@ -27888,6 +27885,9 @@ msgstr "" ...@@ -27888,6 +27885,9 @@ msgstr ""
msgid "There was an error fetching median data for stages" msgid "There was an error fetching median data for stages"
msgstr "" msgstr ""
msgid "There was an error fetching projects"
msgstr ""
msgid "There was an error fetching the %{replicableType}" msgid "There was an error fetching the %{replicableType}"
msgstr "" msgstr ""
......
...@@ -27,8 +27,13 @@ RSpec.describe 'User searches for code' do ...@@ -27,8 +27,13 @@ RSpec.describe 'User searches for code' do
context 'when on a project page', :js do context 'when on a project page', :js do
before do before do
visit(search_path) visit(search_path)
find('.js-search-project-dropdown').click find('[data-testid="project-filter"]').click
find('[data-testid="project-filter"]').click_link(project.full_name)
wait_for_requests
page.within('[data-testid="project-filter"]') do
click_on(project.full_name)
end
end end
include_examples 'top right search form' include_examples 'top right search form'
......
...@@ -85,8 +85,13 @@ RSpec.describe 'User searches for issues', :js do ...@@ -85,8 +85,13 @@ RSpec.describe 'User searches for issues', :js do
context 'when on a project page' do context 'when on a project page' do
it 'finds an issue' do it 'finds an issue' do
find('.js-search-project-dropdown').click find('[data-testid="project-filter"]').click
find('[data-testid="project-filter"]').click_link(project.full_name)
wait_for_requests
page.within('[data-testid="project-filter"]') do
click_on(project.full_name)
end
search_for_issue(issue1.title) search_for_issue(issue1.title)
......
...@@ -30,8 +30,13 @@ RSpec.describe 'User searches for merge requests', :js do ...@@ -30,8 +30,13 @@ RSpec.describe 'User searches for merge requests', :js do
context 'when on a project page' do context 'when on a project page' do
it 'finds a merge request' do it 'finds a merge request' do
find('.js-search-project-dropdown').click find('[data-testid="project-filter"]').click
find('[data-testid="project-filter"]').click_link(project.full_name)
wait_for_requests
page.within('[data-testid="project-filter"]') do
click_on(project.full_name)
end
fill_in('dashboard_search', with: merge_request1.title) fill_in('dashboard_search', with: merge_request1.title)
find('.btn-search').click find('.btn-search').click
......
...@@ -30,8 +30,13 @@ RSpec.describe 'User searches for milestones', :js do ...@@ -30,8 +30,13 @@ RSpec.describe 'User searches for milestones', :js do
context 'when on a project page' do context 'when on a project page' do
it 'finds a milestone' do it 'finds a milestone' do
find('.js-search-project-dropdown').click find('[data-testid="project-filter"]').click
find('[data-testid="project-filter"]').click_link(project.full_name)
wait_for_requests
page.within('[data-testid="project-filter"]') do
click_on(project.full_name)
end
fill_in('dashboard_search', with: milestone1.title) fill_in('dashboard_search', with: milestone1.title)
find('.btn-search').click find('.btn-search').click
......
...@@ -18,8 +18,13 @@ RSpec.describe 'User searches for wiki pages', :js do ...@@ -18,8 +18,13 @@ RSpec.describe 'User searches for wiki pages', :js do
shared_examples 'search wiki blobs' do shared_examples 'search wiki blobs' do
it 'finds a page' do it 'finds a page' do
find('.js-search-project-dropdown').click find('[data-testid="project-filter"]').click
find('[data-testid="project-filter"]').click_link(project.full_name)
wait_for_requests
page.within('[data-testid="project-filter"]') do
click_on(project.full_name)
end
fill_in('dashboard_search', with: search_term) fill_in('dashboard_search', with: search_term)
find('.btn-search').click find('.btn-search').click
......
...@@ -28,13 +28,15 @@ RSpec.describe 'User uses search filters', :js do ...@@ -28,13 +28,15 @@ RSpec.describe 'User uses search filters', :js do
expect(find('[data-testid="group-filter"]')).to have_content(group.name) expect(find('[data-testid="group-filter"]')).to have_content(group.name)
page.within('[data-testid="project-filter"]') do find('[data-testid="project-filter"]').click
find('.js-search-project-dropdown').click
wait_for_requests wait_for_requests
expect(page).to have_link(group_project.full_name) page.within('[data-testid="project-filter"]') do
click_on(group_project.full_name)
end end
expect(find('[data-testid="project-filter"]')).to have_content(group_project.full_name)
end end
context 'when the group filter is set' do context 'when the group filter is set' do
...@@ -58,15 +60,15 @@ RSpec.describe 'User uses search filters', :js do ...@@ -58,15 +60,15 @@ RSpec.describe 'User uses search filters', :js do
it 'shows a project' do it 'shows a project' do
visit search_path visit search_path
page.within('[data-testid="project-filter"]') do find('[data-testid="project-filter"]').click
find('.js-search-project-dropdown').click
wait_for_requests wait_for_requests
click_link(project.full_name) page.within('[data-testid="project-filter"]') do
click_on(project.full_name)
end end
expect(find('.js-search-project-dropdown')).to have_content(project.full_name) expect(find('[data-testid="project-filter"]')).to have_content(project.full_name)
end end
context 'when the project filter is set' do context 'when the project filter is set' do
...@@ -78,10 +80,10 @@ RSpec.describe 'User uses search filters', :js do ...@@ -78,10 +80,10 @@ RSpec.describe 'User uses search filters', :js do
describe 'clear filter button' do describe 'clear filter button' do
it 'removes Project filters' do it 'removes Project filters' do
link = find('[data-testid="project-filter"] .js-search-clear') find('[data-testid="project-filter"] [data-testid="clear-icon"]').click
params = CGI.parse(URI.parse(link[:href]).query) wait_for_requests
expect(params).not_to include(:project_id) expect(page).to have_current_path(search_path(search: "test"))
end end
end end
end end
......
...@@ -2,6 +2,7 @@ export const MOCK_QUERY = { ...@@ -2,6 +2,7 @@ export const MOCK_QUERY = {
scope: 'issues', scope: 'issues',
state: 'all', state: 'all',
confidential: null, confidential: null,
group_id: 'test_1',
}; };
export const MOCK_GROUP = { export const MOCK_GROUP = {
...@@ -22,3 +23,25 @@ export const MOCK_GROUPS = [ ...@@ -22,3 +23,25 @@ export const MOCK_GROUPS = [
id: 'test_2', id: 'test_2',
}, },
]; ];
export const MOCK_PROJECT = {
name: 'test project',
namespace_id: MOCK_GROUP.id,
nameWithNamespace: 'test group test project',
id: 'test_1',
};
export const MOCK_PROJECTS = [
{
name: 'test project',
namespace_id: MOCK_GROUP.id,
name_with_namespace: 'test group test project',
id: 'test_1',
},
{
name: 'test project 2',
namespace_id: MOCK_GROUP.id,
name_with_namespace: 'test group test project 2',
id: 'test_2',
},
];
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper'; import testAction from 'helpers/vuex_action_helper';
import Api from '~/api';
import * as actions from '~/search/store/actions'; import * as actions from '~/search/store/actions';
import * as types from '~/search/store/mutation_types'; import * as types from '~/search/store/mutation_types';
import { setUrlParams, visitUrl } from '~/lib/utils/url_utility'; import * as urlUtils from '~/lib/utils/url_utility';
import state from '~/search/store/state'; import createState from '~/search/store/state';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { MOCK_GROUPS } from '../mock_data'; import { MOCK_QUERY, MOCK_GROUPS, MOCK_PROJECT, MOCK_PROJECTS } from '../mock_data';
jest.mock('~/flash'); jest.mock('~/flash');
jest.mock('~/lib/utils/url_utility', () => ({ jest.mock('~/lib/utils/url_utility', () => ({
setUrlParams: jest.fn(), setUrlParams: jest.fn(),
joinPaths: jest.fn().mockReturnValue(''),
visitUrl: jest.fn(), visitUrl: jest.fn(),
joinPaths: jest.fn(), // For the axios specs
})); }));
describe('Global Search Store Actions', () => { describe('Global Search Store Actions', () => {
let mock; let mock;
let state;
const noCallback = () => {}; const noCallback = () => {};
const flashCallback = () => { const flashCallback = () => {
...@@ -25,66 +27,97 @@ describe('Global Search Store Actions', () => { ...@@ -25,66 +27,97 @@ describe('Global Search Store Actions', () => {
}; };
beforeEach(() => { beforeEach(() => {
state = createState({ query: MOCK_QUERY });
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
}); });
afterEach(() => { afterEach(() => {
state = null;
mock.restore(); mock.restore();
}); });
describe.each` describe.each`
action | axiosMock | type | mutationCalls | callback action | axiosMock | type | expectedMutations | callback
${actions.fetchGroups} | ${{ method: 'onGet', code: 200, res: MOCK_GROUPS }} | ${'success'} | ${[{ type: types.REQUEST_GROUPS }, { type: types.RECEIVE_GROUPS_SUCCESS, payload: MOCK_GROUPS }]} | ${noCallback} ${actions.fetchGroups} | ${{ method: 'onGet', code: 200, res: MOCK_GROUPS }} | ${'success'} | ${[{ type: types.REQUEST_GROUPS }, { type: types.RECEIVE_GROUPS_SUCCESS, payload: MOCK_GROUPS }]} | ${noCallback}
${actions.fetchGroups} | ${{ method: 'onGet', code: 500, res: null }} | ${'error'} | ${[{ type: types.REQUEST_GROUPS }, { type: types.RECEIVE_GROUPS_ERROR }]} | ${flashCallback} ${actions.fetchGroups} | ${{ method: 'onGet', code: 500, res: null }} | ${'error'} | ${[{ type: types.REQUEST_GROUPS }, { type: types.RECEIVE_GROUPS_ERROR }]} | ${flashCallback}
`(`axios calls`, ({ action, axiosMock, type, mutationCalls, callback }) => { ${actions.fetchProjects} | ${{ method: 'onGet', code: 200, res: MOCK_PROJECTS }} | ${'success'} | ${[{ type: types.REQUEST_PROJECTS }, { type: types.RECEIVE_PROJECTS_SUCCESS, payload: MOCK_PROJECTS }]} | ${noCallback}
${actions.fetchProjects} | ${{ method: 'onGet', code: 500, res: null }} | ${'error'} | ${[{ type: types.REQUEST_PROJECTS }, { type: types.RECEIVE_PROJECTS_ERROR }]} | ${flashCallback}
`(`axios calls`, ({ action, axiosMock, type, expectedMutations, callback }) => {
describe(action.name, () => { describe(action.name, () => {
describe(`on ${type}`, () => { describe(`on ${type}`, () => {
beforeEach(() => { beforeEach(() => {
mock[axiosMock.method]().replyOnce(axiosMock.code, axiosMock.res); mock[axiosMock.method]().replyOnce(axiosMock.code, axiosMock.res);
}); });
it(`should dispatch the correct mutations`, () => { it(`should dispatch the correct mutations`, () => {
return testAction(action, null, state, mutationCalls, []).then(() => callback()); return testAction({ action, state, expectedMutations }).then(() => callback());
}); });
}); });
}); });
}); });
describe('getProjectsData', () => {
const mockCommit = () => {};
beforeEach(() => {
jest.spyOn(Api, 'groupProjects').mockResolvedValue(MOCK_PROJECTS);
jest.spyOn(Api, 'projects').mockResolvedValue(MOCK_PROJECT);
});
describe('when groupId is set', () => {
it('calls Api.groupProjects', () => {
actions.fetchProjects({ commit: mockCommit, state });
expect(Api.groupProjects).toHaveBeenCalled();
expect(Api.projects).not.toHaveBeenCalled();
});
});
describe('when groupId is not set', () => {
beforeEach(() => {
state = createState({ query: { group_id: null } });
});
it('calls Api.projects', () => {
actions.fetchProjects({ commit: mockCommit, state });
expect(Api.groupProjects).not.toHaveBeenCalled();
expect(Api.projects).toHaveBeenCalled();
});
});
});
describe('setQuery', () => { describe('setQuery', () => {
const payload = { key: 'key1', value: 'value1' }; const payload = { key: 'key1', value: 'value1' };
it('calls the SET_QUERY mutation', done => { it('calls the SET_QUERY mutation', () => {
testAction(actions.setQuery, payload, state, [{ type: types.SET_QUERY, payload }], [], done); return testAction({
action: actions.setQuery,
payload,
state,
expectedMutations: [{ type: types.SET_QUERY, payload }],
});
}); });
}); });
describe('applyQuery', () => { describe('applyQuery', () => {
it('calls visitUrl and setParams with the state.query', () => { it('calls visitUrl and setParams with the state.query', () => {
testAction(actions.applyQuery, null, state, [], [], () => { return testAction(actions.applyQuery, null, state, [], [], () => {
expect(setUrlParams).toHaveBeenCalledWith({ ...state.query, page: null }); expect(urlUtils.setUrlParams).toHaveBeenCalledWith({ ...state.query, page: null });
expect(visitUrl).toHaveBeenCalled(); expect(urlUtils.visitUrl).toHaveBeenCalled();
}); });
}); });
}); });
describe('resetQuery', () => { describe('resetQuery', () => {
it('calls visitUrl and setParams with empty values', () => { it('calls visitUrl and setParams with empty values', () => {
testAction(actions.resetQuery, null, state, [], [], () => { return testAction(actions.resetQuery, null, state, [], [], () => {
expect(setUrlParams).toHaveBeenCalledWith({ expect(urlUtils.setUrlParams).toHaveBeenCalledWith({
...state.query, ...state.query,
page: null, page: null,
state: null, state: null,
confidential: null, confidential: null,
}); });
expect(visitUrl).toHaveBeenCalled(); expect(urlUtils.visitUrl).toHaveBeenCalled();
}); });
}); });
}); });
}); });
describe('setQuery', () => {
const payload = { key: 'key1', value: 'value1' };
it('calls the SET_QUERY mutation', done => {
testAction(actions.setQuery, payload, state, [{ type: types.SET_QUERY, payload }], [], done);
});
});
import mutations from '~/search/store/mutations'; import mutations from '~/search/store/mutations';
import createState from '~/search/store/state'; import createState from '~/search/store/state';
import * as types from '~/search/store/mutation_types'; import * as types from '~/search/store/mutation_types';
import { MOCK_QUERY, MOCK_GROUPS } from '../mock_data'; import { MOCK_QUERY, MOCK_GROUPS, MOCK_PROJECTS } from '../mock_data';
describe('Global Search Store Mutations', () => { describe('Global Search Store Mutations', () => {
let state; let state;
...@@ -36,6 +36,32 @@ describe('Global Search Store Mutations', () => { ...@@ -36,6 +36,32 @@ describe('Global Search Store Mutations', () => {
}); });
}); });
describe('REQUEST_PROJECTS', () => {
it('sets fetchingProjects to true', () => {
mutations[types.REQUEST_PROJECTS](state);
expect(state.fetchingProjects).toBe(true);
});
});
describe('RECEIVE_PROJECTS_SUCCESS', () => {
it('sets fetchingProjects to false and sets projects', () => {
mutations[types.RECEIVE_PROJECTS_SUCCESS](state, MOCK_PROJECTS);
expect(state.fetchingProjects).toBe(false);
expect(state.projects).toBe(MOCK_PROJECTS);
});
});
describe('RECEIVE_PROJECTS_ERROR', () => {
it('sets fetchingProjects to false and clears projects', () => {
mutations[types.RECEIVE_PROJECTS_ERROR](state);
expect(state.fetchingProjects).toBe(false);
expect(state.projects).toEqual([]);
});
});
describe('SET_QUERY', () => { describe('SET_QUERY', () => {
const payload = { key: 'key1', value: 'value1' }; const payload = { key: 'key1', value: 'value1' };
......
import Vuex from 'vuex';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { MOCK_PROJECT, MOCK_QUERY } from 'jest/search/mock_data';
import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
import ProjectFilter from '~/search/topbar/components/project_filter.vue';
import SearchableDropdown from '~/search/topbar/components/searchable_dropdown.vue';
import { ANY_OPTION, GROUP_DATA, PROJECT_DATA } from '~/search/topbar/constants';
const localVue = createLocalVue();
localVue.use(Vuex);
jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn(),
setUrlParams: jest.fn(),
}));
describe('ProjectFilter', () => {
let wrapper;
const actionSpies = {
fetchProjects: jest.fn(),
};
const defaultProps = {
initialData: null,
};
const createComponent = (initialState, props) => {
const store = new Vuex.Store({
state: {
query: MOCK_QUERY,
...initialState,
},
actions: actionSpies,
});
wrapper = shallowMount(ProjectFilter, {
localVue,
store,
propsData: {
...defaultProps,
...props,
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findSearchableDropdown = () => wrapper.find(SearchableDropdown);
describe('template', () => {
beforeEach(() => {
createComponent();
});
it('renders SearchableDropdown always', () => {
expect(findSearchableDropdown().exists()).toBe(true);
});
});
describe('events', () => {
beforeEach(() => {
createComponent();
});
describe('when @search is emitted', () => {
const search = 'test';
beforeEach(() => {
findSearchableDropdown().vm.$emit('search', search);
});
it('calls fetchProjects with the search paramter', () => {
expect(actionSpies.fetchProjects).toHaveBeenCalledWith(expect.any(Object), search);
});
});
describe('when @change is emitted', () => {
describe('with Any', () => {
beforeEach(() => {
findSearchableDropdown().vm.$emit('change', ANY_OPTION);
});
it('calls setUrlParams with project id, not group id, then calls visitUrl', () => {
expect(setUrlParams).toHaveBeenCalledWith({
[PROJECT_DATA.queryParam]: ANY_OPTION.id,
});
expect(visitUrl).toHaveBeenCalled();
});
});
describe('with a Project', () => {
beforeEach(() => {
findSearchableDropdown().vm.$emit('change', MOCK_PROJECT);
});
it('calls setUrlParams with project id, group id, then calls visitUrl', () => {
expect(setUrlParams).toHaveBeenCalledWith({
[GROUP_DATA.queryParam]: MOCK_PROJECT.namespace_id,
[PROJECT_DATA.queryParam]: MOCK_PROJECT.id,
});
expect(visitUrl).toHaveBeenCalled();
});
});
});
});
describe('computed', () => {
describe('selectedProject', () => {
describe('when initialData is null', () => {
beforeEach(() => {
createComponent();
});
it('sets selectedProject to ANY_OPTION', () => {
expect(wrapper.vm.selectedProject).toBe(ANY_OPTION);
});
});
describe('when initialData is set', () => {
beforeEach(() => {
createComponent({}, { initialData: MOCK_PROJECT });
});
it('sets selectedProject to the initialData', () => {
expect(wrapper.vm.selectedProject).toBe(MOCK_PROJECT);
});
});
});
});
});
import $ from 'jquery';
import setHighlightClass from 'ee_else_ce/search/highlight_blob_search_result'; import setHighlightClass from 'ee_else_ce/search/highlight_blob_search_result';
import Api from '~/api';
import Search from '~/pages/search/show/search'; import Search from '~/pages/search/show/search';
jest.mock('~/api'); jest.mock('~/api');
...@@ -8,13 +6,6 @@ jest.mock('ee_else_ce/search/highlight_blob_search_result'); ...@@ -8,13 +6,6 @@ jest.mock('ee_else_ce/search/highlight_blob_search_result');
describe('Search', () => { describe('Search', () => {
const fixturePath = 'search/show.html'; const fixturePath = 'search/show.html';
const searchTerm = 'some search';
const fillDropdownInput = dropdownSelector => {
const dropdownElement = document.querySelector(dropdownSelector).parentNode;
const inputElement = dropdownElement.querySelector('.dropdown-input-field');
inputElement.value = searchTerm;
return inputElement;
};
preloadFixtures(fixturePath); preloadFixtures(fixturePath);
...@@ -29,20 +20,4 @@ describe('Search', () => { ...@@ -29,20 +20,4 @@ describe('Search', () => {
expect(setHighlightClass).toHaveBeenCalled(); expect(setHighlightClass).toHaveBeenCalled();
}); });
}); });
describe('dropdown behavior', () => {
beforeEach(() => {
loadFixtures(fixturePath);
new Search(); // eslint-disable-line no-new
});
it('requests projects from backend when filtering', () => {
jest.spyOn(Api, 'projects').mockImplementation(term => {
expect(term).toBe(searchTerm);
});
const inputElement = fillDropdownInput('.js-search-project-dropdown');
$(inputElement).trigger('input');
});
});
}); });
...@@ -11,7 +11,7 @@ RSpec.describe 'search/_filter' do ...@@ -11,7 +11,7 @@ RSpec.describe 'search/_filter' do
expect(rendered).to have_selector('input#js-search-group-dropdown') expect(rendered).to have_selector('input#js-search-group-dropdown')
expect(rendered).to have_selector('label[for="dashboard_search_project"]') expect(rendered).to have_selector('label[for="dashboard_search_project"]')
expect(rendered).to have_selector('button#dashboard_search_project') expect(rendered).to have_selector('input#js-search-project-dropdown')
end end
end end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment