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 = {
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"
......@@ -11037,9 +11037,6 @@ msgstr ""
msgid "Error fetching payload data."
msgstr ""
msgid "Error fetching projects"
msgstr ""
msgid "Error fetching refs"
msgstr ""
......@@ -27888,6 +27885,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