Commit f8a78696 authored by Zack Cuddy's avatar Zack Cuddy Committed by Sean McGivern

Global Search - Project Filter

This change replaces the deprecated
jQuery Dropdown plugin.

Instead we use gl-dropdown from
GitLab UI and a Vue component.

This uses a Vuex store to manage the
API calls and API data.  From there
the GitLab UI components take
care of the existing functionality.

The previous change focused on
the Group filter.
This change focueses on
the Project filter.
parent d89f1742
......@@ -390,7 +390,10 @@ const Api = {
params: { ...defaults, ...options },
})
.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 = {}) {
......
......@@ -2,6 +2,6 @@ import Search from './search';
import { initSearchApp } from '~/search';
document.addEventListener('DOMContentLoaded', () => {
initSearchApp();
return new Search(); // Deprecated Dropdown (Projects)
initSearchApp(); // Vue Bootstrap
return new Search(); // Legacy Search Methods
});
import $ from 'jquery';
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 { visitUrl, queryToObject } from '~/lib/utils/url_utility';
import { visitUrl } from '~/lib/utils/url_utility';
import refreshCounts from './refresh_counts';
export default class Search {
constructor() {
setHighlightClass(); // Code Highlighting
const $projectDropdown = $('.js-search-project-dropdown');
this.searchInput = '.js-search-input';
this.searchClear = '.js-search-clear';
const query = queryToObject(window.location.search);
this.groupId = query?.group_id;
this.eventListeners();
refreshCounts();
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();
setHighlightClass(); // Code Highlighting
this.eventListeners(); // Search Form Actions
refreshCounts(); // Other Scope Tab Counts
Project.initRefSwitcher(); // Code Search Branch Picker
}
eventListeners() {
......@@ -97,20 +58,4 @@ export default class Search {
visitUrl($target.href);
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) => {
});
};
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 }) => {
commit(types.SET_QUERY, { key, value });
};
......
......@@ -2,4 +2,8 @@ export const REQUEST_GROUPS = 'REQUEST_GROUPS';
export const RECEIVE_GROUPS_SUCCESS = 'RECEIVE_GROUPS_SUCCESS';
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';
......@@ -12,6 +12,17 @@ export default {
state.fetchingGroups = false;
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 }) {
state.query[key] = value;
},
......
......@@ -2,5 +2,7 @@ const createState = ({ query }) => ({
query,
groups: [],
fetchingGroups: false,
projects: [],
fetchingProjects: false,
});
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 {
<gl-dropdown
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"
@show="$emit('search', searchText)"
@shown="$refs.searchBox.focusInput()"
......
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import GroupFilter from './components/group_filter.vue';
import ProjectFilter from './components/project_filter.vue';
Vue.use(Translate);
......@@ -33,6 +34,10 @@ const searchableDropdowns = [
id: 'js-search-group-dropdown',
component: GroupFilter,
},
{
id: 'js-search-project-dropdown',
component: ProjectFilter,
},
];
export const initTopbar = store =>
......
......@@ -2,21 +2,13 @@
= hidden_field_tag :group_id, params[:group_id]
- if params[:project_id].present?
= 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" } }
%label.d-block{ for: "dashboard_search_group" }
= _("Group")
%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" }
= _("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" } }
%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
%input#js-search-project-dropdown.dropdown-menu-toggle{ value: "Loading...", data: { "initial-data": project_attributes.to_json } }
......@@ -7,9 +7,9 @@
.search-field-holder.form-group.mr-lg-1.mb-lg-0
%label{ for: "dashboard_search" }
= _("What are you searching for?")
.position-relative
= 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
= sprite_icon('search', css_class: 'search-icon')
.gl-search-box-by-type
= 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: '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') }
= sprite_icon('clear')
%span.sr-only
......@@ -17,4 +17,4 @@
- unless params[:snippets].eql? 'true'
= render 'filter'
.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"
......@@ -11034,9 +11034,6 @@ msgstr ""
msgid "Error fetching payload data."
msgstr ""
msgid "Error fetching projects"
msgstr ""
msgid "Error fetching refs"
msgstr ""
......@@ -27879,6 +27876,9 @@ msgstr ""
msgid "There was an error fetching median data for stages"
msgstr ""
msgid "There was an error fetching projects"
msgstr ""
msgid "There was an error fetching the %{replicableType}"
msgstr ""
......
......@@ -27,8 +27,13 @@ RSpec.describe 'User searches for code' do
context 'when on a project page', :js do
before do
visit(search_path)
find('.js-search-project-dropdown').click
find('[data-testid="project-filter"]').click_link(project.full_name)
find('[data-testid="project-filter"]').click
wait_for_requests
page.within('[data-testid="project-filter"]') do
click_on(project.full_name)
end
end
include_examples 'top right search form'
......
......@@ -85,8 +85,13 @@ RSpec.describe 'User searches for issues', :js do
context 'when on a project page' do
it 'finds an issue' do
find('.js-search-project-dropdown').click
find('[data-testid="project-filter"]').click_link(project.full_name)
find('[data-testid="project-filter"]').click
wait_for_requests
page.within('[data-testid="project-filter"]') do
click_on(project.full_name)
end
search_for_issue(issue1.title)
......
......@@ -30,8 +30,13 @@ RSpec.describe 'User searches for merge requests', :js do
context 'when on a project page' do
it 'finds a merge request' do
find('.js-search-project-dropdown').click
find('[data-testid="project-filter"]').click_link(project.full_name)
find('[data-testid="project-filter"]').click
wait_for_requests
page.within('[data-testid="project-filter"]') do
click_on(project.full_name)
end
fill_in('dashboard_search', with: merge_request1.title)
find('.btn-search').click
......
......@@ -30,8 +30,13 @@ RSpec.describe 'User searches for milestones', :js do
context 'when on a project page' do
it 'finds a milestone' do
find('.js-search-project-dropdown').click
find('[data-testid="project-filter"]').click_link(project.full_name)
find('[data-testid="project-filter"]').click
wait_for_requests
page.within('[data-testid="project-filter"]') do
click_on(project.full_name)
end
fill_in('dashboard_search', with: milestone1.title)
find('.btn-search').click
......
......@@ -18,8 +18,13 @@ RSpec.describe 'User searches for wiki pages', :js do
shared_examples 'search wiki blobs' do
it 'finds a page' do
find('.js-search-project-dropdown').click
find('[data-testid="project-filter"]').click_link(project.full_name)
find('[data-testid="project-filter"]').click
wait_for_requests
page.within('[data-testid="project-filter"]') do
click_on(project.full_name)
end
fill_in('dashboard_search', with: search_term)
find('.btn-search').click
......
......@@ -28,13 +28,15 @@ RSpec.describe 'User uses search filters', :js do
expect(find('[data-testid="group-filter"]')).to have_content(group.name)
page.within('[data-testid="project-filter"]') do
find('.js-search-project-dropdown').click
find('[data-testid="project-filter"]').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
expect(find('[data-testid="project-filter"]')).to have_content(group_project.full_name)
end
context 'when the group filter is set' do
......@@ -58,15 +60,15 @@ RSpec.describe 'User uses search filters', :js do
it 'shows a project' do
visit search_path
page.within('[data-testid="project-filter"]') do
find('.js-search-project-dropdown').click
find('[data-testid="project-filter"]').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
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
context 'when the project filter is set' do
......@@ -78,10 +80,10 @@ RSpec.describe 'User uses search filters', :js do
describe 'clear filter button' do
it 'removes Project filters' do
link = find('[data-testid="project-filter"] .js-search-clear')
params = CGI.parse(URI.parse(link[:href]).query)
find('[data-testid="project-filter"] [data-testid="clear-icon"]').click
wait_for_requests
expect(params).not_to include(:project_id)
expect(page).to have_current_path(search_path(search: "test"))
end
end
end
......
......@@ -2,6 +2,7 @@ export const MOCK_QUERY = {
scope: 'issues',
state: 'all',
confidential: null,
group_id: 'test_1',
};
export const MOCK_GROUP = {
......@@ -22,3 +23,25 @@ export const MOCK_GROUPS = [
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 testAction from 'helpers/vuex_action_helper';
import Api from '~/api';
import * as actions from '~/search/store/actions';
import * as types from '~/search/store/mutation_types';
import { setUrlParams, visitUrl } from '~/lib/utils/url_utility';
import state from '~/search/store/state';
import * as urlUtils from '~/lib/utils/url_utility';
import createState from '~/search/store/state';
import axios from '~/lib/utils/axios_utils';
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('~/lib/utils/url_utility', () => ({
setUrlParams: jest.fn(),
joinPaths: jest.fn().mockReturnValue(''),
visitUrl: jest.fn(),
joinPaths: jest.fn(), // For the axios specs
}));
describe('Global Search Store Actions', () => {
let mock;
let state;
const noCallback = () => {};
const flashCallback = () => {
......@@ -25,66 +27,97 @@ describe('Global Search Store Actions', () => {
};
beforeEach(() => {
state = createState({ query: MOCK_QUERY });
mock = new MockAdapter(axios);
});
afterEach(() => {
state = null;
mock.restore();
});
describe.each`
action | axiosMock | type | mutationCalls | 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: 500, res: null }} | ${'error'} | ${[{ type: types.REQUEST_GROUPS }, { type: types.RECEIVE_GROUPS_ERROR }]} | ${flashCallback}
`(`axios calls`, ({ 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: 500, res: null }} | ${'error'} | ${[{ type: types.REQUEST_GROUPS }, { type: types.RECEIVE_GROUPS_ERROR }]} | ${flashCallback}
${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(`on ${type}`, () => {
beforeEach(() => {
mock[axiosMock.method]().replyOnce(axiosMock.code, axiosMock.res);
});
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', () => {
const payload = { key: 'key1', value: 'value1' };
it('calls the SET_QUERY mutation', done => {
testAction(actions.setQuery, payload, state, [{ type: types.SET_QUERY, payload }], [], done);
it('calls the SET_QUERY mutation', () => {
return testAction({
action: actions.setQuery,
payload,
state,
expectedMutations: [{ type: types.SET_QUERY, payload }],
});
});
});
describe('applyQuery', () => {
it('calls visitUrl and setParams with the state.query', () => {
testAction(actions.applyQuery, null, state, [], [], () => {
expect(setUrlParams).toHaveBeenCalledWith({ ...state.query, page: null });
expect(visitUrl).toHaveBeenCalled();
return testAction(actions.applyQuery, null, state, [], [], () => {
expect(urlUtils.setUrlParams).toHaveBeenCalledWith({ ...state.query, page: null });
expect(urlUtils.visitUrl).toHaveBeenCalled();
});
});
});
describe('resetQuery', () => {
it('calls visitUrl and setParams with empty values', () => {
testAction(actions.resetQuery, null, state, [], [], () => {
expect(setUrlParams).toHaveBeenCalledWith({
return testAction(actions.resetQuery, null, state, [], [], () => {
expect(urlUtils.setUrlParams).toHaveBeenCalledWith({
...state.query,
page: null,
state: 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 createState from '~/search/store/state';
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', () => {
let state;
......@@ -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', () => {
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 Api from '~/api';
import Search from '~/pages/search/show/search';
jest.mock('~/api');
......@@ -8,13 +6,6 @@ jest.mock('ee_else_ce/search/highlight_blob_search_result');
describe('Search', () => {
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);
......@@ -29,20 +20,4 @@ describe('Search', () => {
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
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('button#dashboard_search_project')
expect(rendered).to have_selector('input#js-search-project-dropdown')
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