diff options
Diffstat (limited to 'spec/frontend/header_search')
7 files changed, 643 insertions, 0 deletions
diff --git a/spec/frontend/header_search/components/app_spec.js b/spec/frontend/header_search/components/app_spec.js new file mode 100644 index 00000000000..2cbcb73ce5b --- /dev/null +++ b/spec/frontend/header_search/components/app_spec.js @@ -0,0 +1,159 @@ +import { GlSearchBoxByType } from '@gitlab/ui'; +import Vue from 'vue'; +import Vuex from 'vuex'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import HeaderSearchApp from '~/header_search/components/app.vue'; +import HeaderSearchDefaultItems from '~/header_search/components/header_search_default_items.vue'; +import HeaderSearchScopedItems from '~/header_search/components/header_search_scoped_items.vue'; +import { ENTER_KEY, ESC_KEY } from '~/lib/utils/keys'; +import { visitUrl } from '~/lib/utils/url_utility'; +import { MOCK_SEARCH, MOCK_SEARCH_QUERY, MOCK_USERNAME } from '../mock_data'; + +Vue.use(Vuex); + +jest.mock('~/lib/utils/url_utility', () => ({ + visitUrl: jest.fn(), +})); + +describe('HeaderSearchApp', () => { + let wrapper; + + const actionSpies = { + setSearch: jest.fn(), + }; + + const createComponent = (initialState) => { + const store = new Vuex.Store({ + state: { + ...initialState, + }, + actions: actionSpies, + getters: { + searchQuery: () => MOCK_SEARCH_QUERY, + }, + }); + + wrapper = shallowMountExtended(HeaderSearchApp, { + store, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + const findHeaderSearchInput = () => wrapper.findComponent(GlSearchBoxByType); + const findHeaderSearchDropdown = () => wrapper.findByTestId('header-search-dropdown-menu'); + const findHeaderSearchDefaultItems = () => wrapper.findComponent(HeaderSearchDefaultItems); + const findHeaderSearchScopedItems = () => wrapper.findComponent(HeaderSearchScopedItems); + + describe('template', () => { + it('always renders Header Search Input', () => { + createComponent(); + expect(findHeaderSearchInput().exists()).toBe(true); + }); + + describe.each` + showDropdown | username | showSearchDropdown + ${false} | ${null} | ${false} + ${false} | ${MOCK_USERNAME} | ${false} + ${true} | ${null} | ${false} + ${true} | ${MOCK_USERNAME} | ${true} + `('Header Search Dropdown', ({ showDropdown, username, showSearchDropdown }) => { + describe(`when showDropdown is ${showDropdown} and current_username is ${username}`, () => { + beforeEach(() => { + createComponent(); + window.gon.current_username = username; + wrapper.setData({ showDropdown }); + }); + + it(`should${showSearchDropdown ? '' : ' not'} render`, () => { + expect(findHeaderSearchDropdown().exists()).toBe(showSearchDropdown); + }); + }); + }); + + describe.each` + search | showDefault | showScoped + ${null} | ${true} | ${false} + ${''} | ${true} | ${false} + ${MOCK_SEARCH} | ${false} | ${true} + `('Header Search Dropdown Items', ({ search, showDefault, showScoped }) => { + describe(`when search is ${search}`, () => { + beforeEach(() => { + createComponent({ search }); + window.gon.current_username = MOCK_USERNAME; + wrapper.setData({ showDropdown: true }); + }); + + it(`should${showDefault ? '' : ' not'} render the Default Dropdown Items`, () => { + expect(findHeaderSearchDefaultItems().exists()).toBe(showDefault); + }); + + it(`should${showScoped ? '' : ' not'} render the Scoped Dropdown Items`, () => { + expect(findHeaderSearchScopedItems().exists()).toBe(showScoped); + }); + }); + }); + }); + + describe('events', () => { + beforeEach(() => { + createComponent(); + window.gon.current_username = MOCK_USERNAME; + }); + + describe('Header Search Input', () => { + describe('when dropdown is closed', () => { + it('onFocus opens dropdown', async () => { + expect(findHeaderSearchDropdown().exists()).toBe(false); + findHeaderSearchInput().vm.$emit('focus'); + + await wrapper.vm.$nextTick(); + + expect(findHeaderSearchDropdown().exists()).toBe(true); + }); + + it('onClick opens dropdown', async () => { + expect(findHeaderSearchDropdown().exists()).toBe(false); + findHeaderSearchInput().vm.$emit('click'); + + await wrapper.vm.$nextTick(); + + expect(findHeaderSearchDropdown().exists()).toBe(true); + }); + }); + + describe('when dropdown is opened', () => { + beforeEach(() => { + wrapper.setData({ showDropdown: true }); + }); + + it('onKey-Escape closes dropdown', async () => { + expect(findHeaderSearchDropdown().exists()).toBe(true); + findHeaderSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: ESC_KEY })); + + await wrapper.vm.$nextTick(); + + expect(findHeaderSearchDropdown().exists()).toBe(false); + }); + }); + + it('calls setSearch when search input event is fired', async () => { + findHeaderSearchInput().vm.$emit('input', MOCK_SEARCH); + + await wrapper.vm.$nextTick(); + + expect(actionSpies.setSearch).toHaveBeenCalledWith(expect.any(Object), MOCK_SEARCH); + }); + + it('submits a search onKey-Enter', async () => { + findHeaderSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY })); + + await wrapper.vm.$nextTick(); + + expect(visitUrl).toHaveBeenCalledWith(MOCK_SEARCH_QUERY); + }); + }); + }); +}); diff --git a/spec/frontend/header_search/components/header_search_default_items_spec.js b/spec/frontend/header_search/components/header_search_default_items_spec.js new file mode 100644 index 00000000000..ce083d0df72 --- /dev/null +++ b/spec/frontend/header_search/components/header_search_default_items_spec.js @@ -0,0 +1,81 @@ +import { GlDropdownItem, GlDropdownSectionHeader } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import Vue from 'vue'; +import Vuex from 'vuex'; +import HeaderSearchDefaultItems from '~/header_search/components/header_search_default_items.vue'; +import { MOCK_SEARCH_CONTEXT, MOCK_DEFAULT_SEARCH_OPTIONS } from '../mock_data'; + +Vue.use(Vuex); + +describe('HeaderSearchDefaultItems', () => { + let wrapper; + + const createComponent = (initialState) => { + const store = new Vuex.Store({ + state: { + searchContext: MOCK_SEARCH_CONTEXT, + ...initialState, + }, + getters: { + defaultSearchOptions: () => MOCK_DEFAULT_SEARCH_OPTIONS, + }, + }); + + wrapper = shallowMount(HeaderSearchDefaultItems, { + store, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + const findDropdownHeader = () => wrapper.findComponent(GlDropdownSectionHeader); + const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); + const findDropdownItemTitles = () => findDropdownItems().wrappers.map((w) => w.text()); + const findDropdownItemLinks = () => findDropdownItems().wrappers.map((w) => w.attributes('href')); + + describe('template', () => { + describe('Dropdown items', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders item for each option in defaultSearchOptions', () => { + expect(findDropdownItems()).toHaveLength(MOCK_DEFAULT_SEARCH_OPTIONS.length); + }); + + it('renders titles correctly', () => { + const expectedTitles = MOCK_DEFAULT_SEARCH_OPTIONS.map((o) => o.title); + expect(findDropdownItemTitles()).toStrictEqual(expectedTitles); + }); + + it('renders links correctly', () => { + const expectedLinks = MOCK_DEFAULT_SEARCH_OPTIONS.map((o) => o.url); + expect(findDropdownItemLinks()).toStrictEqual(expectedLinks); + }); + }); + + describe.each` + group | project | dropdownTitle + ${null} | ${null} | ${'All GitLab'} + ${{ name: 'Test Group' }} | ${null} | ${'Test Group'} + ${{ name: 'Test Group' }} | ${{ name: 'Test Project' }} | ${'Test Project'} + `('Dropdown Header', ({ group, project, dropdownTitle }) => { + describe(`when group is ${group?.name} and project is ${project?.name}`, () => { + beforeEach(() => { + createComponent({ + searchContext: { + group, + project, + }, + }); + }); + + it(`should render as ${dropdownTitle}`, () => { + expect(findDropdownHeader().text()).toBe(dropdownTitle); + }); + }); + }); + }); +}); diff --git a/spec/frontend/header_search/components/header_search_scoped_items_spec.js b/spec/frontend/header_search/components/header_search_scoped_items_spec.js new file mode 100644 index 00000000000..f0e5e182ec4 --- /dev/null +++ b/spec/frontend/header_search/components/header_search_scoped_items_spec.js @@ -0,0 +1,61 @@ +import { GlDropdownItem } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import Vue from 'vue'; +import Vuex from 'vuex'; +import { trimText } from 'helpers/text_helper'; +import HeaderSearchScopedItems from '~/header_search/components/header_search_scoped_items.vue'; +import { MOCK_SEARCH, MOCK_SCOPED_SEARCH_OPTIONS } from '../mock_data'; + +Vue.use(Vuex); + +describe('HeaderSearchScopedItems', () => { + let wrapper; + + const createComponent = (initialState) => { + const store = new Vuex.Store({ + state: { + search: MOCK_SEARCH, + ...initialState, + }, + getters: { + scopedSearchOptions: () => MOCK_SCOPED_SEARCH_OPTIONS, + }, + }); + + wrapper = shallowMount(HeaderSearchScopedItems, { + store, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); + const findDropdownItemTitles = () => findDropdownItems().wrappers.map((w) => trimText(w.text())); + const findDropdownItemLinks = () => findDropdownItems().wrappers.map((w) => w.attributes('href')); + + describe('template', () => { + describe('Dropdown items', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders item for each option in scopedSearchOptions', () => { + expect(findDropdownItems()).toHaveLength(MOCK_SCOPED_SEARCH_OPTIONS.length); + }); + + it('renders titles correctly', () => { + const expectedTitles = MOCK_SCOPED_SEARCH_OPTIONS.map((o) => + trimText(`"${MOCK_SEARCH}" ${o.description} ${o.scope || ''}`), + ); + expect(findDropdownItemTitles()).toStrictEqual(expectedTitles); + }); + + it('renders links correctly', () => { + const expectedLinks = MOCK_SCOPED_SEARCH_OPTIONS.map((o) => o.url); + expect(findDropdownItemLinks()).toStrictEqual(expectedLinks); + }); + }); + }); +}); diff --git a/spec/frontend/header_search/mock_data.js b/spec/frontend/header_search/mock_data.js new file mode 100644 index 00000000000..5963ad9c279 --- /dev/null +++ b/spec/frontend/header_search/mock_data.js @@ -0,0 +1,83 @@ +import { + MSG_ISSUES_ASSIGNED_TO_ME, + MSG_ISSUES_IVE_CREATED, + MSG_MR_ASSIGNED_TO_ME, + MSG_MR_IM_REVIEWER, + MSG_MR_IVE_CREATED, + MSG_IN_PROJECT, + MSG_IN_GROUP, + MSG_IN_ALL_GITLAB, +} from '~/header_search/constants'; + +export const MOCK_USERNAME = 'anyone'; + +export const MOCK_SEARCH_PATH = '/search'; + +export const MOCK_ISSUE_PATH = '/dashboard/issues'; + +export const MOCK_MR_PATH = '/dashboard/merge_requests'; + +export const MOCK_ALL_PATH = '/'; + +export const MOCK_PROJECT = { + id: 123, + name: 'MockProject', + path: '/mock-project', +}; + +export const MOCK_GROUP = { + id: 321, + name: 'MockGroup', + path: '/mock-group', +}; + +export const MOCK_SEARCH_QUERY = 'http://gitlab.com/search?search=test'; + +export const MOCK_SEARCH = 'test'; + +export const MOCK_SEARCH_CONTEXT = { + project: null, + project_metadata: {}, + group: null, + group_metadata: {}, +}; + +export const MOCK_DEFAULT_SEARCH_OPTIONS = [ + { + title: MSG_ISSUES_ASSIGNED_TO_ME, + url: `${MOCK_ISSUE_PATH}/?assignee_username=${MOCK_USERNAME}`, + }, + { + title: MSG_ISSUES_IVE_CREATED, + url: `${MOCK_ISSUE_PATH}/?author_username=${MOCK_USERNAME}`, + }, + { + title: MSG_MR_ASSIGNED_TO_ME, + url: `${MOCK_MR_PATH}/?assignee_username=${MOCK_USERNAME}`, + }, + { + title: MSG_MR_IM_REVIEWER, + url: `${MOCK_MR_PATH}/?reviewer_username=${MOCK_USERNAME}`, + }, + { + title: MSG_MR_IVE_CREATED, + url: `${MOCK_MR_PATH}/?author_username=${MOCK_USERNAME}`, + }, +]; + +export const MOCK_SCOPED_SEARCH_OPTIONS = [ + { + scope: MOCK_PROJECT.name, + description: MSG_IN_PROJECT, + url: MOCK_PROJECT.path, + }, + { + scope: MOCK_GROUP.name, + description: MSG_IN_GROUP, + url: MOCK_GROUP.path, + }, + { + description: MSG_IN_ALL_GITLAB, + url: MOCK_ALL_PATH, + }, +]; diff --git a/spec/frontend/header_search/store/actions_spec.js b/spec/frontend/header_search/store/actions_spec.js new file mode 100644 index 00000000000..4530df0d91c --- /dev/null +++ b/spec/frontend/header_search/store/actions_spec.js @@ -0,0 +1,28 @@ +import testAction from 'helpers/vuex_action_helper'; +import * as actions from '~/header_search/store/actions'; +import * as types from '~/header_search/store/mutation_types'; +import createState from '~/header_search/store/state'; +import { MOCK_SEARCH } from '../mock_data'; + +describe('Header Search Store Actions', () => { + let state; + + beforeEach(() => { + state = createState({}); + }); + + afterEach(() => { + state = null; + }); + + describe('setSearch', () => { + it('calls the SET_SEARCH mutation', () => { + return testAction({ + action: actions.setSearch, + payload: MOCK_SEARCH, + state, + expectedMutations: [{ type: types.SET_SEARCH, payload: MOCK_SEARCH }], + }); + }); + }); +}); diff --git a/spec/frontend/header_search/store/getters_spec.js b/spec/frontend/header_search/store/getters_spec.js new file mode 100644 index 00000000000..2ad0a082f6a --- /dev/null +++ b/spec/frontend/header_search/store/getters_spec.js @@ -0,0 +1,211 @@ +import * as getters from '~/header_search/store/getters'; +import initState from '~/header_search/store/state'; +import { + MOCK_USERNAME, + MOCK_SEARCH_PATH, + MOCK_ISSUE_PATH, + MOCK_MR_PATH, + MOCK_SEARCH_CONTEXT, + MOCK_DEFAULT_SEARCH_OPTIONS, + MOCK_SCOPED_SEARCH_OPTIONS, + MOCK_PROJECT, + MOCK_GROUP, + MOCK_ALL_PATH, + MOCK_SEARCH, +} from '../mock_data'; + +describe('Header Search Store Getters', () => { + let state; + + const createState = (initialState) => { + state = initState({ + searchPath: MOCK_SEARCH_PATH, + issuesPath: MOCK_ISSUE_PATH, + mrPath: MOCK_MR_PATH, + searchContext: MOCK_SEARCH_CONTEXT, + ...initialState, + }); + }; + + afterEach(() => { + state = null; + }); + + describe.each` + group | project | expectedPath + ${null} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=undefined&group_id=undefined&scope=issues`} + ${MOCK_GROUP} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=undefined&group_id=${MOCK_GROUP.id}&scope=issues`} + ${MOCK_GROUP} | ${MOCK_PROJECT} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=${MOCK_PROJECT.id}&group_id=${MOCK_GROUP.id}&scope=issues`} + `('searchQuery', ({ group, project, expectedPath }) => { + describe(`when group is ${group?.name} and project is ${project?.name}`, () => { + beforeEach(() => { + createState({ + searchContext: { + group, + project, + scope: 'issues', + }, + }); + state.search = MOCK_SEARCH; + }); + + it(`should return ${expectedPath}`, () => { + expect(getters.searchQuery(state)).toBe(expectedPath); + }); + }); + }); + + describe.each` + group | group_metadata | project | project_metadata | expectedPath + ${null} | ${null} | ${null} | ${null} | ${MOCK_ISSUE_PATH} + ${{ name: 'Test Group' }} | ${{ issues_path: 'group/path' }} | ${null} | ${null} | ${'group/path'} + ${{ name: 'Test Group' }} | ${{ issues_path: 'group/path' }} | ${{ name: 'Test Project' }} | ${{ issues_path: 'project/path' }} | ${'project/path'} + `('scopedIssuesPath', ({ group, group_metadata, project, project_metadata, expectedPath }) => { + describe(`when group is ${group?.name} and project is ${project?.name}`, () => { + beforeEach(() => { + createState({ + searchContext: { + group, + group_metadata, + project, + project_metadata, + }, + }); + }); + + it(`should return ${expectedPath}`, () => { + expect(getters.scopedIssuesPath(state)).toBe(expectedPath); + }); + }); + }); + + describe.each` + group | group_metadata | project | project_metadata | expectedPath + ${null} | ${null} | ${null} | ${null} | ${MOCK_MR_PATH} + ${{ name: 'Test Group' }} | ${{ mr_path: 'group/path' }} | ${null} | ${null} | ${'group/path'} + ${{ name: 'Test Group' }} | ${{ mr_path: 'group/path' }} | ${{ name: 'Test Project' }} | ${{ mr_path: 'project/path' }} | ${'project/path'} + `('scopedMRPath', ({ group, group_metadata, project, project_metadata, expectedPath }) => { + describe(`when group is ${group?.name} and project is ${project?.name}`, () => { + beforeEach(() => { + createState({ + searchContext: { + group, + group_metadata, + project, + project_metadata, + }, + }); + }); + + it(`should return ${expectedPath}`, () => { + expect(getters.scopedMRPath(state)).toBe(expectedPath); + }); + }); + }); + + describe.each` + group | project | expectedPath + ${null} | ${null} | ${null} + ${MOCK_GROUP} | ${null} | ${null} + ${MOCK_GROUP} | ${MOCK_PROJECT} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=${MOCK_PROJECT.id}&group_id=${MOCK_GROUP.id}&scope=issues`} + `('projectUrl', ({ group, project, expectedPath }) => { + describe(`when group is ${group?.name} and project is ${project?.name}`, () => { + beforeEach(() => { + createState({ + searchContext: { + group, + project, + scope: 'issues', + }, + }); + state.search = MOCK_SEARCH; + }); + + it(`should return ${expectedPath}`, () => { + expect(getters.projectUrl(state)).toBe(expectedPath); + }); + }); + }); + + describe.each` + group | project | expectedPath + ${null} | ${null} | ${null} + ${MOCK_GROUP} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&group_id=${MOCK_GROUP.id}&scope=issues`} + ${MOCK_GROUP} | ${MOCK_PROJECT} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&group_id=${MOCK_GROUP.id}&scope=issues`} + `('groupUrl', ({ group, project, expectedPath }) => { + describe(`when group is ${group?.name} and project is ${project?.name}`, () => { + beforeEach(() => { + createState({ + searchContext: { + group, + project, + scope: 'issues', + }, + }); + state.search = MOCK_SEARCH; + }); + + it(`should return ${expectedPath}`, () => { + expect(getters.groupUrl(state)).toBe(expectedPath); + }); + }); + }); + + describe('allUrl', () => { + const expectedPath = `${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&scope=issues`; + + beforeEach(() => { + createState({ + searchContext: { + scope: 'issues', + }, + }); + state.search = MOCK_SEARCH; + }); + + it(`should return ${expectedPath}`, () => { + expect(getters.allUrl(state)).toBe(expectedPath); + }); + }); + + describe('defaultSearchOptions', () => { + const mockGetters = { + scopedIssuesPath: MOCK_ISSUE_PATH, + scopedMRPath: MOCK_MR_PATH, + }; + + beforeEach(() => { + createState(); + window.gon.current_username = MOCK_USERNAME; + }); + + it('returns the correct array', () => { + expect(getters.defaultSearchOptions(state, mockGetters)).toStrictEqual( + MOCK_DEFAULT_SEARCH_OPTIONS, + ); + }); + }); + + describe('scopedSearchOptions', () => { + const mockGetters = { + projectUrl: MOCK_PROJECT.path, + groupUrl: MOCK_GROUP.path, + allUrl: MOCK_ALL_PATH, + }; + + beforeEach(() => { + createState({ + searchContext: { + project: MOCK_PROJECT, + group: MOCK_GROUP, + }, + }); + }); + + it('returns the correct array', () => { + expect(getters.scopedSearchOptions(state, mockGetters)).toStrictEqual( + MOCK_SCOPED_SEARCH_OPTIONS, + ); + }); + }); +}); diff --git a/spec/frontend/header_search/store/mutations_spec.js b/spec/frontend/header_search/store/mutations_spec.js new file mode 100644 index 00000000000..8196c06099d --- /dev/null +++ b/spec/frontend/header_search/store/mutations_spec.js @@ -0,0 +1,20 @@ +import * as types from '~/header_search/store/mutation_types'; +import mutations from '~/header_search/store/mutations'; +import createState from '~/header_search/store/state'; +import { MOCK_SEARCH } from '../mock_data'; + +describe('Header Search Store Mutations', () => { + let state; + + beforeEach(() => { + state = createState({}); + }); + + describe('SET_SEARCH', () => { + it('sets search to value', () => { + mutations[types.SET_SEARCH](state, MOCK_SEARCH); + + expect(state.search).toBe(MOCK_SEARCH); + }); + }); +}); |