Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/super_sidebar/components')
-rw-r--r--spec/frontend/super_sidebar/components/context_switcher_spec.js52
-rw-r--r--spec/frontend/super_sidebar/components/create_menu_spec.js24
-rw-r--r--spec/frontend/super_sidebar/components/frequent_items_list_spec.js35
-rw-r--r--spec/frontend/super_sidebar/components/global_search/components/global_search_autocomplete_items_spec.js196
-rw-r--r--spec/frontend/super_sidebar/components/global_search/components/global_search_default_items_spec.js61
-rw-r--r--spec/frontend/super_sidebar/components/global_search/components/global_search_scoped_items_spec.js107
-rw-r--r--spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js324
-rw-r--r--spec/frontend/super_sidebar/components/global_search/mock_data.js202
-rw-r--r--spec/frontend/super_sidebar/components/global_search/store/actions_spec.js4
-rw-r--r--spec/frontend/super_sidebar/components/global_search/store/getters_spec.js7
-rw-r--r--spec/frontend/super_sidebar/components/global_search/store/mutations_spec.js2
-rw-r--r--spec/frontend/super_sidebar/components/global_search/utils_spec.js60
-rw-r--r--spec/frontend/super_sidebar/components/groups_list_spec.js9
-rw-r--r--spec/frontend/super_sidebar/components/help_center_spec.js103
-rw-r--r--spec/frontend/super_sidebar/components/items_list_spec.js45
-rw-r--r--spec/frontend/super_sidebar/components/merge_request_menu_spec.js19
-rw-r--r--spec/frontend/super_sidebar/components/nav_item_spec.js38
-rw-r--r--spec/frontend/super_sidebar/components/pinned_section_spec.js75
-rw-r--r--spec/frontend/super_sidebar/components/projects_list_spec.js9
-rw-r--r--spec/frontend/super_sidebar/components/search_results_spec.js16
-rw-r--r--spec/frontend/super_sidebar/components/sidebar_menu_spec.js151
-rw-r--r--spec/frontend/super_sidebar/components/super_sidebar_spec.js154
-rw-r--r--spec/frontend/super_sidebar/components/super_sidebar_toggle_spec.js106
-rw-r--r--spec/frontend/super_sidebar/components/user_bar_spec.js142
-rw-r--r--spec/frontend/super_sidebar/components/user_menu_spec.js189
-rw-r--r--spec/frontend/super_sidebar/components/user_name_group_spec.js22
26 files changed, 1468 insertions, 684 deletions
diff --git a/spec/frontend/super_sidebar/components/context_switcher_spec.js b/spec/frontend/super_sidebar/components/context_switcher_spec.js
index 538e87cf843..11da6ef1243 100644
--- a/spec/frontend/super_sidebar/components/context_switcher_spec.js
+++ b/spec/frontend/super_sidebar/components/context_switcher_spec.js
@@ -1,10 +1,11 @@
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
-import { GlSearchBoxByType } from '@gitlab/ui';
+import { GlSearchBoxByType, GlLoadingIcon, GlAlert } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { s__ } from '~/locale';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ContextSwitcher from '~/super_sidebar/components/context_switcher.vue';
+import NavItem from '~/super_sidebar/components/nav_item.vue';
import ProjectsList from '~/super_sidebar/components/projects_list.vue';
import GroupsList from '~/super_sidebar/components/groups_list.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -22,7 +23,11 @@ jest.mock('~/super_sidebar/utils', () => ({
.formatContextSwitcherItems,
trackContextAccess: jest.fn(),
}));
+const focusInputMock = jest.fn();
+const persistentLinks = [
+ { title: 'Explore', link: '/explore', icon: 'compass', link_classes: 'persistent-link-class' },
+];
const username = 'root';
const projectsPath = 'projectsPath';
const groupsPath = 'groupsPath';
@@ -33,9 +38,12 @@ describe('ContextSwitcher component', () => {
let wrapper;
let mockApollo;
+ const findNavItems = () => wrapper.findAllComponents(NavItem);
const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
const findProjectsList = () => wrapper.findComponent(ProjectsList);
const findGroupsList = () => wrapper.findComponent(GroupsList);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findAlert = () => wrapper.findComponent(GlAlert);
const triggerSearchQuery = async () => {
findSearchBox().vm.$emit('input', 'foo');
@@ -60,6 +68,7 @@ describe('ContextSwitcher component', () => {
wrapper = shallowMountExtended(ContextSwitcher, {
apolloProvider: mockApollo,
propsData: {
+ persistentLinks,
username,
projectsPath,
groupsPath,
@@ -68,6 +77,7 @@ describe('ContextSwitcher component', () => {
stubs: {
GlSearchBoxByType: stubComponent(GlSearchBoxByType, {
props: ['placeholder'],
+ methods: { focusInput: focusInputMock },
}),
ProjectsList: stubComponent(ProjectsList, {
props: ['username', 'viewAllLink', 'isSearch', 'searchResults'],
@@ -84,9 +94,20 @@ describe('ContextSwitcher component', () => {
createWrapper();
});
+ it('renders the persistent links', () => {
+ const navItems = findNavItems();
+ const firstNavItem = navItems.at(0);
+
+ expect(navItems.length).toBe(persistentLinks.length);
+ expect(firstNavItem.props('item')).toBe(persistentLinks[0]);
+ expect(firstNavItem.props('linkClasses')).toEqual({
+ [persistentLinks[0].link_classes]: persistentLinks[0].link_classes,
+ });
+ });
+
it('passes the placeholder to the search box', () => {
expect(findSearchBox().props('placeholder')).toBe(
- s__('Navigation|Search for projects or groups'),
+ s__('Navigation|Search your projects or groups'),
);
});
@@ -108,9 +129,22 @@ describe('ContextSwitcher component', () => {
});
});
+ it('focuses the search input when focusInput is called', () => {
+ wrapper.vm.focusInput();
+
+ expect(focusInputMock).toHaveBeenCalledTimes(1);
+ });
+
it('does not trigger the search query on mount', () => {
expect(searchUserProjectsAndGroupsHandlerSuccess).not.toHaveBeenCalled();
});
+
+ it('shows a loading spinner when search query is typed in', async () => {
+ findSearchBox().vm.$emit('input', 'foo');
+ await nextTick();
+
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
});
describe('item access tracking', () => {
@@ -138,10 +172,18 @@ describe('ContextSwitcher component', () => {
return triggerSearchQuery();
});
+ it('hides persistent links', () => {
+ expect(findNavItems().length).toBe(0);
+ });
+
it('triggers the search query on search', () => {
expect(searchUserProjectsAndGroupsHandlerSuccess).toHaveBeenCalled();
});
+ it('hides the loading spinner', () => {
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+
it('passes the projects to the frequent projects list', () => {
expect(findProjectsList().props('isSearch')).toBe(true);
expect(findProjectsList().props('searchResults')).toEqual(
@@ -192,7 +234,7 @@ describe('ContextSwitcher component', () => {
jest.spyOn(Sentry, 'captureException');
});
- it('captures exception if response is formatted incorrectly', async () => {
+ it('captures exception and shows an alert if response is formatted incorrectly', async () => {
createWrapper({
requestHandlers: {
searchUserProjectsAndGroupsQueryHandler: jest.fn().mockResolvedValue({
@@ -203,9 +245,10 @@ describe('ContextSwitcher component', () => {
await triggerSearchQuery();
expect(Sentry.captureException).toHaveBeenCalled();
+ expect(findAlert().exists()).toBe(true);
});
- it('captures exception if query fails', async () => {
+ it('captures exception and shows an alert if query fails', async () => {
createWrapper({
requestHandlers: {
searchUserProjectsAndGroupsQueryHandler: jest.fn().mockRejectedValue(),
@@ -214,6 +257,7 @@ describe('ContextSwitcher component', () => {
await triggerSearchQuery();
expect(Sentry.captureException).toHaveBeenCalled();
+ expect(findAlert().exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/super_sidebar/components/create_menu_spec.js b/spec/frontend/super_sidebar/components/create_menu_spec.js
index b24c6b8de7f..e05b5d30e69 100644
--- a/spec/frontend/super_sidebar/components/create_menu_spec.js
+++ b/spec/frontend/super_sidebar/components/create_menu_spec.js
@@ -1,3 +1,4 @@
+import { nextTick } from 'vue';
import { GlDisclosureDropdown, GlTooltip } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { __ } from '~/locale';
@@ -23,6 +24,14 @@ describe('CreateMenu component', () => {
createWrapper();
});
+ it('passes popper options to the dropdown', () => {
+ createWrapper();
+
+ expect(findGlDisclosureDropdown().props('popperOptions')).toEqual({
+ modifiers: [{ name: 'offset', options: { offset: [-147, 4] } }],
+ });
+ });
+
it("sets the toggle's label", () => {
expect(findGlDisclosureDropdown().props('toggleText')).toBe(__('Create new...'));
});
@@ -35,5 +44,20 @@ describe('CreateMenu component', () => {
expect(findGlDisclosureDropdown().props('toggleId')).toBe(wrapper.vm.$options.toggleId);
expect(findGlTooltip().props('target')).toBe(`#${wrapper.vm.$options.toggleId}`);
});
+
+ it('hides the tooltip when the dropdown is opened', async () => {
+ findGlDisclosureDropdown().vm.$emit('shown');
+ await nextTick();
+
+ expect(findGlTooltip().exists()).toBe(false);
+ });
+
+ it('shows the tooltip when the dropdown is closed', async () => {
+ findGlDisclosureDropdown().vm.$emit('shown');
+ findGlDisclosureDropdown().vm.$emit('hidden');
+ await nextTick();
+
+ expect(findGlTooltip().exists()).toBe(true);
+ });
});
});
diff --git a/spec/frontend/super_sidebar/components/frequent_items_list_spec.js b/spec/frontend/super_sidebar/components/frequent_items_list_spec.js
index 1e98db091f2..86cec3f3d13 100644
--- a/spec/frontend/super_sidebar/components/frequent_items_list_spec.js
+++ b/spec/frontend/super_sidebar/components/frequent_items_list_spec.js
@@ -1,3 +1,4 @@
+import { GlIcon, GlButton } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { s__ } from '~/locale';
import FrequentItemsList from '~/super_sidebar/components//frequent_items_list.vue';
@@ -16,6 +17,7 @@ describe('FrequentItemsList component', () => {
let wrapper;
const findListTitle = () => wrapper.findByTestId('list-title');
+ const findListEditButton = () => findListTitle().findComponent(GlButton);
const findItemsList = () => wrapper.findComponent(ItemsList);
const findEmptyText = () => wrapper.findByTestId('empty-text');
@@ -64,5 +66,38 @@ describe('FrequentItemsList component', () => {
it('does not render the empty text slot', () => {
expect(findEmptyText().exists()).toBe(false);
});
+
+ describe('items editing', () => {
+ it('renders edit button within header', () => {
+ const itemsEditButton = findListEditButton();
+
+ expect(itemsEditButton.exists()).toBe(true);
+ expect(itemsEditButton.attributes('title')).toBe('Toggle edit mode');
+ expect(itemsEditButton.findComponent(GlIcon).props('name')).toBe('pencil');
+ });
+
+ it('clicking edit button makes items list editable', async () => {
+ // Off by default
+ expect(findItemsList().props('editable')).toBe(false);
+
+ // On when clicked
+ await findListEditButton().vm.$emit('click');
+ expect(findItemsList().props('editable')).toBe(true);
+
+ // Off when clicked again
+ await findListEditButton().vm.$emit('click');
+ expect(findItemsList().props('editable')).toBe(false);
+ });
+
+ it('remove-item event emission from items-list causes list item to be removed', async () => {
+ const localStorageProjects = findItemsList().props('items');
+ await findListEditButton().vm.$emit('click');
+
+ await findItemsList().vm.$emit('remove-item', localStorageProjects[0]);
+
+ expect(findItemsList().props('items')).toHaveLength(maxItems - 1);
+ expect(findItemsList().props('items')).not.toContain(localStorageProjects[0]);
+ });
+ });
});
});
diff --git a/spec/frontend/super_sidebar/components/global_search/components/global_search_autocomplete_items_spec.js b/spec/frontend/super_sidebar/components/global_search/components/global_search_autocomplete_items_spec.js
index e5ba1c63996..aac321bd8e0 100644
--- a/spec/frontend/super_sidebar/components/global_search/components/global_search_autocomplete_items_spec.js
+++ b/spec/frontend/super_sidebar/components/global_search/components/global_search_autocomplete_items_spec.js
@@ -1,31 +1,24 @@
-import { GlDropdownItem, GlLoadingIcon, GlAvatar, GlAlert, GlDropdownDivider } from '@gitlab/ui';
+import {
+ GlDisclosureDropdownGroup,
+ GlDisclosureDropdownItem,
+ GlLoadingIcon,
+ GlAvatar,
+ GlAlert,
+} from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
+import Vue from 'vue';
import Vuex from 'vuex';
-import HeaderSearchAutocompleteItems from '~/super_sidebar/components/global_search/components/global_search_autocomplete_items.vue';
-import {
- LARGE_AVATAR_PX,
- SMALL_AVATAR_PX,
-} from '~/super_sidebar/components/global_search/constants';
-import {
- PROJECTS_CATEGORY,
- GROUPS_CATEGORY,
- ISSUES_CATEGORY,
- MERGE_REQUEST_CATEGORY,
- RECENT_EPICS_CATEGORY,
-} from '~/vue_shared/global_search/constants';
+import GlobalSearchAutocompleteItems from '~/super_sidebar/components/global_search/components/global_search_autocomplete_items.vue';
+
import {
MOCK_GROUPED_AUTOCOMPLETE_OPTIONS,
+ MOCK_SCOPED_SEARCH_OPTIONS,
MOCK_SORTED_AUTOCOMPLETE_OPTIONS,
- MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_SETTINGS_HELP,
- MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_HELP,
- MOCK_SEARCH,
- MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_2,
} from '../mock_data';
Vue.use(Vuex);
-describe('HeaderSearchAutocompleteItems', () => {
+describe('GlobalSearchAutocompleteItems', () => {
let wrapper;
const createComponent = (initialState, mockGetters, props) => {
@@ -36,30 +29,34 @@ describe('HeaderSearchAutocompleteItems', () => {
},
getters: {
autocompleteGroupedSearchOptions: () => MOCK_GROUPED_AUTOCOMPLETE_OPTIONS,
+ scopedSearchOptions: () => MOCK_SCOPED_SEARCH_OPTIONS,
...mockGetters,
},
});
- wrapper = shallowMount(HeaderSearchAutocompleteItems, {
+ wrapper = shallowMount(GlobalSearchAutocompleteItems, {
store,
propsData: {
...props,
},
+ stubs: {
+ GlDisclosureDropdownGroup,
+ GlDisclosureDropdownItem,
+ },
});
};
- const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
- const findGlDropdownDividers = () => wrapper.findAllComponents(GlDropdownDivider);
- const findFirstDropdownItem = () => findDropdownItems().at(0);
- const findDropdownItemTitles = () =>
- findDropdownItems().wrappers.map((w) => w.findAll('span').at(1).text());
- const findDropdownItemSubTitles = () =>
- findDropdownItems()
- .wrappers.filter((w) => w.findAll('span').length > 2)
- .map((w) => w.findAll('span').at(2).text());
- const findDropdownItemLinks = () => findDropdownItems().wrappers.map((w) => w.attributes('href'));
+ const findItems = () => wrapper.findAllComponents(GlDisclosureDropdownItem);
+ const findItemTitles = () =>
+ findItems().wrappers.map((w) => w.find('[data-testid="autocomplete-item-name"]').text());
+ const findItemSubTitles = () =>
+ findItems()
+ .wrappers.map((w) => w.find('[data-testid="autocomplete-item-namespace"]'))
+ .filter((w) => w.exists())
+ .map((w) => w.text());
+ const findItemLinks = () => findItems().wrappers.map((w) => w.find('a').attributes('href'));
const findGlLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findGlAvatar = () => wrapper.findComponent(GlAvatar);
+ const findAvatars = () => wrapper.findAllComponents(GlAvatar).wrappers.map((w) => w.props('src'));
const findGlAlert = () => wrapper.findComponent(GlAlert);
describe('template', () => {
@@ -73,7 +70,7 @@ describe('HeaderSearchAutocompleteItems', () => {
});
it('does not render autocomplete options', () => {
- expect(findDropdownItems()).toHaveLength(0);
+ expect(findItems()).toHaveLength(0);
});
});
@@ -86,6 +83,7 @@ describe('HeaderSearchAutocompleteItems', () => {
expect(findGlAlert().exists()).toBe(true);
});
});
+
describe('when loading is false', () => {
beforeEach(() => {
createComponent({ loading: false });
@@ -95,143 +93,35 @@ describe('HeaderSearchAutocompleteItems', () => {
expect(findGlLoadingIcon().exists()).toBe(false);
});
- describe('Dropdown items', () => {
+ describe('Search results items', () => {
it('renders item for each option in autocomplete option', () => {
- expect(findDropdownItems()).toHaveLength(MOCK_SORTED_AUTOCOMPLETE_OPTIONS.length);
+ expect(findItems()).toHaveLength(MOCK_SORTED_AUTOCOMPLETE_OPTIONS.length);
});
it('renders titles correctly', () => {
- const expectedTitles = MOCK_SORTED_AUTOCOMPLETE_OPTIONS.map((o) => o.value || o.label);
- expect(findDropdownItemTitles()).toStrictEqual(expectedTitles);
+ const expectedTitles = MOCK_SORTED_AUTOCOMPLETE_OPTIONS.map((o) => o.value || o.text);
+ expect(findItemTitles()).toStrictEqual(expectedTitles);
});
it('renders sub-titles correctly', () => {
const expectedSubTitles = MOCK_SORTED_AUTOCOMPLETE_OPTIONS.filter((o) => o.value).map(
- (o) => o.label,
+ (o) => o.namespace,
);
- expect(findDropdownItemSubTitles()).toStrictEqual(expectedSubTitles);
- });
-
- it('renders links correctly', () => {
- const expectedLinks = MOCK_SORTED_AUTOCOMPLETE_OPTIONS.map((o) => o.url);
- expect(findDropdownItemLinks()).toStrictEqual(expectedLinks);
- });
- });
-
- describe.each`
- item | showAvatar | avatarSize | searchContext | entityId | entityName
- ${{ data: [{ category: PROJECTS_CATEGORY, avatar_url: null }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${{ project: { id: 29 } }} | ${'29'} | ${''}
- ${{ data: [{ category: GROUPS_CATEGORY, avatar_url: '/123' }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${{ group: { id: 12 } }} | ${'12'} | ${''}
- ${{ data: [{ category: 'Help', avatar_url: '' }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${null} | ${'0'} | ${''}
- ${{ data: [{ category: 'Settings' }] }} | ${false} | ${false} | ${null} | ${false} | ${false}
- ${{ data: [{ category: GROUPS_CATEGORY, avatar_url: null }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${{ group: { id: 1, name: 'test1' } }} | ${'1'} | ${'test1'}
- ${{ data: [{ category: PROJECTS_CATEGORY, avatar_url: null }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${{ project: { id: 2, name: 'test2' } }} | ${'2'} | ${'test2'}
- ${{ data: [{ category: ISSUES_CATEGORY, avatar_url: null }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${{ project: { id: 3, name: 'test3' } }} | ${'3'} | ${'test3'}
- ${{ data: [{ category: MERGE_REQUEST_CATEGORY, avatar_url: null }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${{ project: { id: 4, name: 'test4' } }} | ${'4'} | ${'test4'}
- ${{ data: [{ category: RECENT_EPICS_CATEGORY, avatar_url: null }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${{ group: { id: 5, name: 'test5' } }} | ${'5'} | ${'test5'}
- ${{ data: [{ category: GROUPS_CATEGORY, avatar_url: null, group_id: 6, group_name: 'test6' }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${null} | ${'6'} | ${'test6'}
- ${{ data: [{ category: PROJECTS_CATEGORY, avatar_url: null, project_id: 7, project_name: 'test7' }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${null} | ${'7'} | ${'test7'}
- ${{ data: [{ category: ISSUES_CATEGORY, avatar_url: null, project_id: 8, project_name: 'test8' }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${null} | ${'8'} | ${'test8'}
- ${{ data: [{ category: MERGE_REQUEST_CATEGORY, avatar_url: null, project_id: 9, project_name: 'test9' }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${null} | ${'9'} | ${'test9'}
- ${{ data: [{ category: RECENT_EPICS_CATEGORY, avatar_url: null, group_id: 10, group_name: 'test10' }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${null} | ${'10'} | ${'test10'}
- ${{ data: [{ category: GROUPS_CATEGORY, avatar_url: null, group_id: 11, group_name: 'test11' }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${{ group: { id: 1, name: 'test1' } }} | ${'11'} | ${'test11'}
- ${{ data: [{ category: PROJECTS_CATEGORY, avatar_url: null, project_id: 12, project_name: 'test12' }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${{ project: { id: 2, name: 'test2' } }} | ${'12'} | ${'test12'}
- ${{ data: [{ category: ISSUES_CATEGORY, avatar_url: null, project_id: 13, project_name: 'test13' }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${{ project: { id: 3, name: 'test3' } }} | ${'13'} | ${'test13'}
- ${{ data: [{ category: MERGE_REQUEST_CATEGORY, avatar_url: null, project_id: 14, project_name: 'test14' }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${{ project: { id: 4, name: 'test4' } }} | ${'14'} | ${'test14'}
- ${{ data: [{ category: RECENT_EPICS_CATEGORY, avatar_url: null, group_id: 15, group_name: 'test15' }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${{ group: { id: 5, name: 'test5' } }} | ${'15'} | ${'test15'}
- `('GlAvatar', ({ item, showAvatar, avatarSize, searchContext, entityId, entityName }) => {
- describe(`when category is ${item.data[0].category} and avatar_url is ${item.data[0].avatar_url}`, () => {
- beforeEach(() => {
- createComponent({ searchContext }, { autocompleteGroupedSearchOptions: () => [item] });
- });
-
- it(`should${showAvatar ? '' : ' not'} render`, () => {
- expect(findGlAvatar().exists()).toBe(showAvatar);
- });
-
- it(`should set avatarSize to ${avatarSize}`, () => {
- expect(findGlAvatar().exists() && findGlAvatar().attributes('size')).toBe(avatarSize);
- });
-
- it(`should set avatar entityId to ${entityId}`, () => {
- expect(findGlAvatar().exists() && findGlAvatar().attributes('entityid')).toBe(entityId);
- });
-
- it(`should set avatar entityName to ${entityName}`, () => {
- expect(findGlAvatar().exists() && findGlAvatar().attributes('entityname')).toBe(
- entityName,
- );
- });
- });
- });
- });
-
- describe.each`
- currentFocusedOption | isFocused | ariaSelected
- ${null} | ${false} | ${undefined}
- ${{ html_id: 'not-a-match' }} | ${false} | ${undefined}
- ${MOCK_SORTED_AUTOCOMPLETE_OPTIONS[0]} | ${true} | ${'true'}
- `('isOptionFocused', ({ currentFocusedOption, isFocused, ariaSelected }) => {
- describe(`when currentFocusedOption.html_id is ${currentFocusedOption?.html_id}`, () => {
- beforeEach(() => {
- createComponent({}, {}, { currentFocusedOption });
- });
- it(`should${isFocused ? '' : ' not'} have gl-bg-gray-50 applied`, () => {
- expect(findFirstDropdownItem().classes('gl-bg-gray-50')).toBe(isFocused);
+ expect(findItemSubTitles()).toStrictEqual(expectedSubTitles);
});
- it(`sets "aria-selected to ${ariaSelected}`, () => {
- expect(findFirstDropdownItem().attributes('aria-selected')).toBe(ariaSelected);
+ it('renders links correctly', () => {
+ const expectedLinks = MOCK_SORTED_AUTOCOMPLETE_OPTIONS.map((o) => o.href);
+ expect(findItemLinks()).toStrictEqual(expectedLinks);
});
- });
- });
- describe.each`
- search | items | dividerCount
- ${null} | ${[]} | ${0}
- ${''} | ${[]} | ${0}
- ${'1'} | ${[]} | ${0}
- ${')'} | ${[]} | ${0}
- ${'t'} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_SETTINGS_HELP} | ${1}
- ${'te'} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_HELP} | ${0}
- ${'tes'} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_2} | ${1}
- ${MOCK_SEARCH} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_2} | ${1}
- `('Header Search Dropdown Dividers', ({ search, items, dividerCount }) => {
- describe(`when search is ${search}`, () => {
- beforeEach(() => {
- createComponent(
- { search },
- {
- autocompleteGroupedSearchOptions: () => items,
- },
- {},
+ it('renders avatars', () => {
+ const expectedAvatars = MOCK_SORTED_AUTOCOMPLETE_OPTIONS.map((o) => o.avatar_url).filter(
+ Boolean,
);
+ expect(findAvatars()).toStrictEqual(expectedAvatars);
});
-
- it(`component should have ${dividerCount} dividers`, () => {
- expect(findGlDropdownDividers()).toHaveLength(dividerCount);
- });
- });
- });
- });
-
- describe('watchers', () => {
- describe('currentFocusedOption', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('when focused changes to existing element calls scroll into view on the newly focused element', async () => {
- const focusedElement = findFirstDropdownItem().element;
- const scrollSpy = jest.spyOn(focusedElement, 'scrollIntoView');
-
- wrapper.setProps({ currentFocusedOption: MOCK_SORTED_AUTOCOMPLETE_OPTIONS[0] });
-
- await nextTick();
-
- expect(scrollSpy).toHaveBeenCalledWith(false);
- scrollSpy.mockRestore();
});
});
});
diff --git a/spec/frontend/super_sidebar/components/global_search/components/global_search_default_items_spec.js b/spec/frontend/super_sidebar/components/global_search/components/global_search_default_items_spec.js
index 132f8e60598..52e9aa52c14 100644
--- a/spec/frontend/super_sidebar/components/global_search/components/global_search_default_items_spec.js
+++ b/spec/frontend/super_sidebar/components/global_search/components/global_search_default_items_spec.js
@@ -1,13 +1,13 @@
-import { GlDropdownItem, GlDropdownSectionHeader } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { GlDisclosureDropdownGroup, GlDisclosureDropdownItem } from '@gitlab/ui';
import Vue from 'vue';
import Vuex from 'vuex';
-import HeaderSearchDefaultItems from '~/super_sidebar/components/global_search/components/global_search_default_items.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import GlobalSearchDefaultItems from '~/super_sidebar/components/global_search/components/global_search_default_items.vue';
import { MOCK_SEARCH_CONTEXT, MOCK_DEFAULT_SEARCH_OPTIONS } from '../mock_data';
Vue.use(Vuex);
-describe('HeaderSearchDefaultItems', () => {
+describe('GlobalSearchDefaultItems', () => {
let wrapper;
const createComponent = (initialState, props) => {
@@ -21,19 +21,19 @@ describe('HeaderSearchDefaultItems', () => {
},
});
- wrapper = shallowMount(HeaderSearchDefaultItems, {
+ wrapper = shallowMountExtended(GlobalSearchDefaultItems, {
store,
propsData: {
...props,
},
+ stubs: {
+ GlDisclosureDropdownGroup,
+ },
});
};
- const findDropdownHeader = () => wrapper.findComponent(GlDropdownSectionHeader);
- const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
- const findFirstDropdownItem = () => findDropdownItems().at(0);
- const findDropdownItemTitles = () => findDropdownItems().wrappers.map((w) => w.text());
- const findDropdownItemLinks = () => findDropdownItems().wrappers.map((w) => w.attributes('href'));
+ const findItems = () => wrapper.findAllComponents(GlDisclosureDropdownItem);
+ const findItemsData = () => findItems().wrappers.map((w) => w.props('item'));
describe('template', () => {
describe('Dropdown items', () => {
@@ -42,26 +42,20 @@ describe('HeaderSearchDefaultItems', () => {
});
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);
+ expect(findItems()).toHaveLength(MOCK_DEFAULT_SEARCH_OPTIONS.length);
});
- it('renders links correctly', () => {
- const expectedLinks = MOCK_DEFAULT_SEARCH_OPTIONS.map((o) => o.url);
- expect(findDropdownItemLinks()).toStrictEqual(expectedLinks);
+ it('provides the `item` prop to the `GlDisclosureDropdownItem` component', () => {
+ expect(findItemsData()).toStrictEqual(MOCK_DEFAULT_SEARCH_OPTIONS);
});
});
describe.each`
- group | project | dropdownTitle
+ group | project | groupHeader
${null} | ${null} | ${'All GitLab'}
${{ name: 'Test Group' }} | ${null} | ${'Test Group'}
${{ name: 'Test Group' }} | ${{ name: 'Test Project' }} | ${'Test Project'}
- `('Dropdown Header', ({ group, project, dropdownTitle }) => {
+ `('Group Header', ({ group, project, groupHeader }) => {
describe(`when group is ${group?.name} and project is ${project?.name}`, () => {
beforeEach(() => {
createComponent({
@@ -72,29 +66,8 @@ describe('HeaderSearchDefaultItems', () => {
});
});
- it(`should render as ${dropdownTitle}`, () => {
- expect(findDropdownHeader().text()).toBe(dropdownTitle);
- });
- });
- });
-
- describe.each`
- currentFocusedOption | isFocused | ariaSelected
- ${null} | ${false} | ${undefined}
- ${{ html_id: 'not-a-match' }} | ${false} | ${undefined}
- ${MOCK_DEFAULT_SEARCH_OPTIONS[0]} | ${true} | ${'true'}
- `('isOptionFocused', ({ currentFocusedOption, isFocused, ariaSelected }) => {
- describe(`when currentFocusedOption.html_id is ${currentFocusedOption?.html_id}`, () => {
- beforeEach(() => {
- createComponent({}, { currentFocusedOption });
- });
-
- it(`should${isFocused ? '' : ' not'} have gl-bg-gray-50 applied`, () => {
- expect(findFirstDropdownItem().classes('gl-bg-gray-50')).toBe(isFocused);
- });
-
- it(`sets "aria-selected to ${ariaSelected}`, () => {
- expect(findFirstDropdownItem().attributes('aria-selected')).toBe(ariaSelected);
+ it(`should render as ${groupHeader}`, () => {
+ expect(wrapper.text()).toContain(groupHeader);
});
});
});
diff --git a/spec/frontend/super_sidebar/components/global_search/components/global_search_scoped_items_spec.js b/spec/frontend/super_sidebar/components/global_search/components/global_search_scoped_items_spec.js
index fa91ef43ced..4976f3be4cd 100644
--- a/spec/frontend/super_sidebar/components/global_search/components/global_search_scoped_items_spec.js
+++ b/spec/frontend/super_sidebar/components/global_search/components/global_search_scoped_items_spec.js
@@ -1,21 +1,21 @@
-import { GlDropdownItem, GlToken, GlIcon } from '@gitlab/ui';
+import { GlDisclosureDropdownGroup, GlDisclosureDropdownItem, GlToken, GlIcon } 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 '~/super_sidebar/components/global_search/components/global_search_scoped_items.vue';
+import GlobalSearchScopedItems from '~/super_sidebar/components/global_search/components/global_search_scoped_items.vue';
import { truncate } from '~/lib/utils/text_utility';
import { SCOPE_TOKEN_MAX_LENGTH } from '~/super_sidebar/components/global_search/constants';
import { MSG_IN_ALL_GITLAB } from '~/vue_shared/global_search/constants';
import {
MOCK_SEARCH,
- MOCK_SCOPED_SEARCH_OPTIONS,
+ MOCK_SCOPED_SEARCH_GROUP,
MOCK_GROUPED_AUTOCOMPLETE_OPTIONS,
} from '../mock_data';
Vue.use(Vuex);
-describe('HeaderSearchScopedItems', () => {
+describe('GlobalSearchScopedItems', () => {
let wrapper;
const createComponent = (initialState, mockGetters, props) => {
@@ -25,96 +25,67 @@ describe('HeaderSearchScopedItems', () => {
...initialState,
},
getters: {
- scopedSearchOptions: () => MOCK_SCOPED_SEARCH_OPTIONS,
+ scopedSearchGroup: () => MOCK_SCOPED_SEARCH_GROUP,
autocompleteGroupedSearchOptions: () => MOCK_GROUPED_AUTOCOMPLETE_OPTIONS,
...mockGetters,
},
});
- wrapper = shallowMount(HeaderSearchScopedItems, {
+ wrapper = shallowMount(GlobalSearchScopedItems, {
store,
propsData: {
...props,
},
+ stubs: {
+ GlDisclosureDropdownGroup,
+ GlDisclosureDropdownItem,
+ },
});
};
- const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
- const findFirstDropdownItem = () => findDropdownItems().at(0);
- const findDropdownItemTitles = () => findDropdownItems().wrappers.map((w) => trimText(w.text()));
+ const findItems = () => wrapper.findAllComponents(GlDisclosureDropdownItem);
+ const findItemsText = () => findItems().wrappers.map((w) => trimText(w.text()));
const findScopeTokens = () => wrapper.findAllComponents(GlToken);
const findScopeTokensText = () => findScopeTokens().wrappers.map((w) => trimText(w.text()));
const findScopeTokensIcons = () =>
findScopeTokens().wrappers.map((w) => w.findAllComponents(GlIcon));
- const findDropdownItemAriaLabels = () =>
- findDropdownItems().wrappers.map((w) => trimText(w.attributes('aria-label')));
- 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);
- });
+ const findItemLinks = () => findItems().wrappers.map((w) => w.find('a').attributes('href'));
- it('renders titles correctly', () => {
- findDropdownItemTitles().forEach((title) => expect(title).toContain(MOCK_SEARCH));
- });
-
- it('renders scope names correctly', () => {
- const expectedTitles = MOCK_SCOPED_SEARCH_OPTIONS.map((o) =>
- truncate(trimText(`in ${o.description || o.scope}`), SCOPE_TOKEN_MAX_LENGTH),
- );
+ describe('Search results scoped items', () => {
+ beforeEach(() => {
+ createComponent();
+ });
- expect(findScopeTokensText()).toStrictEqual(expectedTitles);
- });
+ it('renders item for each item in scopedSearchGroup', () => {
+ expect(findItems()).toHaveLength(MOCK_SCOPED_SEARCH_GROUP.items.length);
+ });
- it('renders scope icons correctly', () => {
- findScopeTokensIcons().forEach((icon, i) => {
- const w = icon.wrappers[0];
- expect(w?.attributes('name')).toBe(MOCK_SCOPED_SEARCH_OPTIONS[i].icon);
- });
- });
+ it('renders titles correctly', () => {
+ findItemsText().forEach((title) => expect(title).toContain(MOCK_SEARCH));
+ });
- it(`renders scope ${MSG_IN_ALL_GITLAB} correctly`, () => {
- expect(findScopeTokens().at(-1).findComponent(GlIcon).exists()).toBe(false);
- });
+ it('renders scope names correctly', () => {
+ const expectedTitles = MOCK_SCOPED_SEARCH_GROUP.items.map((o) =>
+ truncate(trimText(`in ${o.scope || o.description}`), SCOPE_TOKEN_MAX_LENGTH),
+ );
- it('renders aria-labels correctly', () => {
- const expectedLabels = MOCK_SCOPED_SEARCH_OPTIONS.map((o) =>
- trimText(`${MOCK_SEARCH} ${o.description || o.icon} ${o.scope || ''}`),
- );
- expect(findDropdownItemAriaLabels()).toStrictEqual(expectedLabels);
- });
+ expect(findScopeTokensText()).toStrictEqual(expectedTitles);
+ });
- it('renders links correctly', () => {
- const expectedLinks = MOCK_SCOPED_SEARCH_OPTIONS.map((o) => o.url);
- expect(findDropdownItemLinks()).toStrictEqual(expectedLinks);
+ it('renders scope icons correctly', () => {
+ findScopeTokensIcons().forEach((icon, i) => {
+ const w = icon.wrappers[0];
+ expect(w?.attributes('name')).toBe(MOCK_SCOPED_SEARCH_GROUP.items[i].icon);
});
});
- describe.each`
- currentFocusedOption | isFocused | ariaSelected
- ${null} | ${false} | ${undefined}
- ${{ html_id: 'not-a-match' }} | ${false} | ${undefined}
- ${MOCK_SCOPED_SEARCH_OPTIONS[0]} | ${true} | ${'true'}
- `('isOptionFocused', ({ currentFocusedOption, isFocused, ariaSelected }) => {
- describe(`when currentFocusedOption.html_id is ${currentFocusedOption?.html_id}`, () => {
- beforeEach(() => {
- createComponent({}, {}, { currentFocusedOption });
- });
-
- it(`should${isFocused ? '' : ' not'} have gl-bg-gray-50 applied`, () => {
- expect(findFirstDropdownItem().classes('gl-bg-gray-50')).toBe(isFocused);
- });
+ it(`renders scope ${MSG_IN_ALL_GITLAB} correctly`, () => {
+ expect(findScopeTokens().at(-1).findComponent(GlIcon).exists()).toBe(false);
+ });
- it(`sets "aria-selected to ${ariaSelected}`, () => {
- expect(findFirstDropdownItem().attributes('aria-selected')).toBe(ariaSelected);
- });
- });
+ it('renders links correctly', () => {
+ const expectedLinks = MOCK_SCOPED_SEARCH_GROUP.items.map((o) => o.href);
+ expect(findItemLinks()).toStrictEqual(expectedLinks);
});
});
});
diff --git a/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js b/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js
index 0dcfc448125..eb8801f68c6 100644
--- a/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js
+++ b/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js
@@ -1,30 +1,25 @@
import { GlSearchBoxByType, GlToken, GlIcon } from '@gitlab/ui';
-import Vue, { nextTick } from 'vue';
+import Vue from 'vue';
import Vuex from 'vuex';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { mockTracking } from 'helpers/tracking_helper';
import { s__, sprintf } from '~/locale';
-import HeaderSearchApp from '~/super_sidebar/components/global_search/components/global_search.vue';
-import HeaderSearchAutocompleteItems from '~/super_sidebar/components/global_search/components/global_search_autocomplete_items.vue';
-import HeaderSearchDefaultItems from '~/super_sidebar/components/global_search/components/global_search_default_items.vue';
-import HeaderSearchScopedItems from '~/super_sidebar/components/global_search/components/global_search_scoped_items.vue';
+import GlobalSearchModal from '~/super_sidebar/components/global_search/components/global_search.vue';
+import GlobalSearchAutocompleteItems from '~/super_sidebar/components/global_search/components/global_search_autocomplete_items.vue';
+import GlobalSearchDefaultItems from '~/super_sidebar/components/global_search/components/global_search_default_items.vue';
+import GlobalSearchScopedItems from '~/super_sidebar/components/global_search/components/global_search_scoped_items.vue';
import {
SEARCH_INPUT_DESCRIPTION,
SEARCH_RESULTS_DESCRIPTION,
- SEARCH_BOX_INDEX,
ICON_PROJECT,
ICON_GROUP,
ICON_SUBGROUP,
SCOPE_TOKEN_MAX_LENGTH,
IS_SEARCHING,
- IS_NOT_FOCUSED,
- IS_FOCUSED,
SEARCH_SHORTCUTS_MIN_CHARACTERS,
} from '~/super_sidebar/components/global_search/constants';
-import DropdownKeyboardNavigation from '~/vue_shared/components/dropdown_keyboard_navigation.vue';
-import { ENTER_KEY } from '~/lib/utils/keys';
-import { visitUrl } from '~/lib/utils/url_utility';
import { truncate } from '~/lib/utils/text_utility';
+import { visitUrl } from '~/lib/utils/url_utility';
+import { ENTER_KEY } from '~/lib/utils/keys';
import {
MOCK_SEARCH,
MOCK_SEARCH_QUERY,
@@ -32,6 +27,8 @@ import {
MOCK_DEFAULT_SEARCH_OPTIONS,
MOCK_SCOPED_SEARCH_OPTIONS,
MOCK_SEARCH_CONTEXT_FULL,
+ MOCK_PROJECT,
+ MOCK_GROUP,
} from '../mock_data';
Vue.use(Vuex);
@@ -40,7 +37,7 @@ jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn(),
}));
-describe('HeaderSearchApp', () => {
+describe('GlobalSearchModal', () => {
let wrapper;
const actionSpies = {
@@ -49,21 +46,31 @@ describe('HeaderSearchApp', () => {
clearAutocomplete: jest.fn(),
};
- const createComponent = (initialState, mockGetters) => {
+ const deafaultMockState = {
+ searchContext: {
+ project: MOCK_PROJECT,
+ group: MOCK_GROUP,
+ },
+ };
+
+ const createComponent = (initialState, mockGetters, stubs) => {
const store = new Vuex.Store({
state: {
+ ...deafaultMockState,
...initialState,
},
actions: actionSpies,
getters: {
searchQuery: () => MOCK_SEARCH_QUERY,
searchOptions: () => MOCK_DEFAULT_SEARCH_OPTIONS,
+ scopedSearchOptions: () => MOCK_SCOPED_SEARCH_OPTIONS,
...mockGetters,
},
});
- wrapper = shallowMountExtended(HeaderSearchApp, {
+ wrapper = shallowMountExtended(GlobalSearchModal, {
store,
+ stubs,
});
};
@@ -80,16 +87,13 @@ describe('HeaderSearchApp', () => {
);
};
- const findHeaderSearchForm = () => wrapper.findByTestId('header-search-form');
- const findHeaderSearchInput = () => wrapper.findComponent(GlSearchBoxByType);
+ const findGlobalSearchForm = () => wrapper.findByTestId('global-search-form');
+ const findGlobalSearchInput = () => wrapper.findComponent(GlSearchBoxByType);
const findScopeToken = () => wrapper.findComponent(GlToken);
- const findHeaderSearchInputKBD = () => wrapper.find('.keyboard-shortcut-helper');
- const findHeaderSearchDropdown = () => wrapper.findByTestId('header-search-dropdown-menu');
- const findHeaderSearchDefaultItems = () => wrapper.findComponent(HeaderSearchDefaultItems);
- const findHeaderSearchScopedItems = () => wrapper.findComponent(HeaderSearchScopedItems);
- const findHeaderSearchAutocompleteItems = () =>
- wrapper.findComponent(HeaderSearchAutocompleteItems);
- const findDropdownKeyboardNavigation = () => wrapper.findComponent(DropdownKeyboardNavigation);
+ const findGlobalSearchDefaultItems = () => wrapper.findComponent(GlobalSearchDefaultItems);
+ const findGlobalSearchScopedItems = () => wrapper.findComponent(GlobalSearchScopedItems);
+ const findGlobalSearchAutocompleteItems = () =>
+ wrapper.findComponent(GlobalSearchAutocompleteItems);
const findSearchInputDescription = () => wrapper.find(`#${SEARCH_INPUT_DESCRIPTION}`);
const findSearchResultsDescription = () => wrapper.findByTestId(SEARCH_RESULTS_DESCRIPTION);
@@ -99,16 +103,8 @@ describe('HeaderSearchApp', () => {
createComponent();
});
- it('Header Search Input', () => {
- expect(findHeaderSearchInput().exists()).toBe(true);
- });
-
- it('Header Search Input KBD hint', () => {
- expect(findHeaderSearchInputKBD().exists()).toBe(true);
- expect(findHeaderSearchInputKBD().text()).toContain('/');
- expect(findHeaderSearchInputKBD().attributes('title')).toContain(
- 'Use the shortcut key <kbd>/</kbd> to start a search',
- );
+ it('Global Search Input', () => {
+ expect(findGlobalSearchInput().exists()).toBe(true);
});
it('Search Input Description', () => {
@@ -121,26 +117,6 @@ describe('HeaderSearchApp', () => {
});
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(() => {
- window.gon.current_username = username;
- createComponent();
- findHeaderSearchInput().vm.$emit(showDropdown ? 'click' : '');
- });
-
- it(`should${showSearchDropdown ? '' : ' not'} render`, () => {
- expect(findHeaderSearchDropdown().exists()).toBe(showSearchDropdown);
- });
- });
- });
-
- describe.each`
search | showDefault | showScoped | showAutocomplete
${null} | ${true} | ${false} | ${false}
${''} | ${true} | ${false} | ${false}
@@ -148,71 +124,40 @@ describe('HeaderSearchApp', () => {
${'te'} | ${false} | ${false} | ${true}
${'tes'} | ${false} | ${true} | ${true}
${MOCK_SEARCH} | ${false} | ${true} | ${true}
- `('Header Search Dropdown Items', ({ search, showDefault, showScoped, showAutocomplete }) => {
+ `('Global Search Result Items', ({ search, showDefault, showScoped, showAutocomplete }) => {
describe(`when search is ${search}`, () => {
beforeEach(() => {
window.gon.current_username = MOCK_USERNAME;
createComponent({ search }, {});
- findHeaderSearchInput().vm.$emit('click');
- });
-
- 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);
- });
-
- it(`should${showAutocomplete ? '' : ' not'} render the Autocomplete Dropdown Items`, () => {
- expect(findHeaderSearchAutocompleteItems().exists()).toBe(showAutocomplete);
- });
-
- it(`should render the Dropdown Navigation Component`, () => {
- expect(findDropdownKeyboardNavigation().exists()).toBe(true);
+ findGlobalSearchInput().vm.$emit('click');
});
- it(`should close the dropdown when press escape key`, async () => {
- findHeaderSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: 27 }));
- await nextTick();
- expect(findHeaderSearchDropdown().exists()).toBe(false);
- expect(wrapper.emitted().expandSearchBar.length).toBe(1);
+ it(`should${showDefault ? '' : ' not'} render the Default Items`, () => {
+ expect(findGlobalSearchDefaultItems().exists()).toBe(showDefault);
});
- });
- });
- describe.each`
- username | showDropdown | expectedDesc
- ${null} | ${false} | ${HeaderSearchApp.i18n.SEARCH_INPUT_DESCRIBE_BY_NO_DROPDOWN}
- ${null} | ${true} | ${HeaderSearchApp.i18n.SEARCH_INPUT_DESCRIBE_BY_NO_DROPDOWN}
- ${MOCK_USERNAME} | ${false} | ${HeaderSearchApp.i18n.SEARCH_INPUT_DESCRIBE_BY_WITH_DROPDOWN}
- ${MOCK_USERNAME} | ${true} | ${HeaderSearchApp.i18n.SEARCH_INPUT_DESCRIBE_BY_WITH_DROPDOWN}
- `('Search Input Description', ({ username, showDropdown, expectedDesc }) => {
- describe(`current_username is ${username} and showDropdown is ${showDropdown}`, () => {
- beforeEach(() => {
- window.gon.current_username = username;
- createComponent();
- findHeaderSearchInput().vm.$emit(showDropdown ? 'click' : '');
+ it(`should${showScoped ? '' : ' not'} render the Scoped Items`, () => {
+ expect(findGlobalSearchScopedItems().exists()).toBe(showScoped);
});
- it(`sets description to ${expectedDesc}`, () => {
- expect(findSearchInputDescription().text()).toBe(expectedDesc);
+ it(`should${showAutocomplete ? '' : ' not'} render the Autocomplete Items`, () => {
+ expect(findGlobalSearchAutocompleteItems().exists()).toBe(showAutocomplete);
});
});
});
describe.each`
- username | showDropdown | search | loading | searchOptions | expectedDesc
- ${null} | ${true} | ${''} | ${false} | ${[]} | ${''}
- ${MOCK_USERNAME} | ${false} | ${''} | ${false} | ${[]} | ${''}
- ${MOCK_USERNAME} | ${true} | ${''} | ${false} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${`${MOCK_DEFAULT_SEARCH_OPTIONS.length} default results provided. Use the up and down arrow keys to navigate search results list.`}
- ${MOCK_USERNAME} | ${true} | ${''} | ${true} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${`${MOCK_DEFAULT_SEARCH_OPTIONS.length} default results provided. Use the up and down arrow keys to navigate search results list.`}
- ${MOCK_USERNAME} | ${true} | ${MOCK_SEARCH} | ${false} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${`Results updated. ${MOCK_SCOPED_SEARCH_OPTIONS.length} results available. Use the up and down arrow keys to navigate search results list, or ENTER to submit.`}
- ${MOCK_USERNAME} | ${true} | ${MOCK_SEARCH} | ${true} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${HeaderSearchApp.i18n.SEARCH_RESULTS_LOADING}
+ username | search | loading | searchOptions | expectedDesc
+ ${null} | ${'gi'} | ${false} | ${[]} | ${GlobalSearchModal.i18n.MIN_SEARCH_TERM}
+ ${MOCK_USERNAME} | ${'gi'} | ${false} | ${[]} | ${GlobalSearchModal.i18n.MIN_SEARCH_TERM}
+ ${MOCK_USERNAME} | ${''} | ${false} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${`${MOCK_DEFAULT_SEARCH_OPTIONS.length} default results provided. Use the up and down arrow keys to navigate search results list.`}
+ ${MOCK_USERNAME} | ${MOCK_SEARCH} | ${true} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${GlobalSearchModal.i18n.SEARCH_RESULTS_LOADING}
+ ${MOCK_USERNAME} | ${MOCK_SEARCH} | ${false} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${`Results updated. ${MOCK_SCOPED_SEARCH_OPTIONS.length} results available. Use the up and down arrow keys to navigate search results list, or ENTER to submit.`}
+ ${MOCK_USERNAME} | ${MOCK_SEARCH} | ${true} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${GlobalSearchModal.i18n.SEARCH_RESULTS_LOADING}
`(
'Search Results Description',
- ({ username, showDropdown, search, loading, searchOptions, expectedDesc }) => {
- describe(`search is "${search}", loading is ${loading}, and showSearchDropdown is ${showDropdown}`, () => {
+ ({ username, search, loading, searchOptions, expectedDesc }) => {
+ describe(`search is "${search}" and loading is ${loading}`, () => {
beforeEach(() => {
window.gon.current_username = username;
createComponent(
@@ -224,7 +169,6 @@ describe('HeaderSearchApp', () => {
searchOptions: () => searchOptions,
},
);
- findHeaderSearchInput().vm.$emit(showDropdown ? 'click' : '');
});
it(`sets description to ${expectedDesc}`, () => {
@@ -253,7 +197,7 @@ describe('HeaderSearchApp', () => {
searchOptions: () => searchOptions,
},
);
- findHeaderSearchInput().vm.$emit('click');
+ findGlobalSearchInput().vm.$emit('click');
});
it(`${hasToken ? 'is' : 'is NOT'} rendered when data set has type "${
@@ -274,42 +218,31 @@ describe('HeaderSearchApp', () => {
describe('form', () => {
describe.each`
- searchContext | search | searchOptions | isFocused
- ${MOCK_SEARCH_CONTEXT_FULL} | ${null} | ${[]} | ${true}
- ${MOCK_SEARCH_CONTEXT_FULL} | ${MOCK_SEARCH} | ${[]} | ${true}
- ${MOCK_SEARCH_CONTEXT_FULL} | ${MOCK_SEARCH} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${true}
- ${MOCK_SEARCH_CONTEXT_FULL} | ${MOCK_SEARCH} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${false}
- ${null} | ${MOCK_SEARCH} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${true}
- ${null} | ${null} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${true}
- ${null} | ${null} | ${[]} | ${true}
- `('wrapper', ({ searchContext, search, searchOptions, isFocused }) => {
+ searchContext | search | searchOptions
+ ${MOCK_SEARCH_CONTEXT_FULL} | ${null} | ${[]}
+ ${MOCK_SEARCH_CONTEXT_FULL} | ${MOCK_SEARCH} | ${[]}
+ ${MOCK_SEARCH_CONTEXT_FULL} | ${MOCK_SEARCH} | ${MOCK_SCOPED_SEARCH_OPTIONS}
+ ${MOCK_SEARCH_CONTEXT_FULL} | ${MOCK_SEARCH} | ${MOCK_SCOPED_SEARCH_OPTIONS}
+ ${null} | ${MOCK_SEARCH} | ${MOCK_SCOPED_SEARCH_OPTIONS}
+ ${null} | ${null} | ${MOCK_SCOPED_SEARCH_OPTIONS}
+ ${null} | ${null} | ${[]}
+ `('wrapper', ({ searchContext, search, searchOptions }) => {
beforeEach(() => {
window.gon.current_username = MOCK_USERNAME;
createComponent({ search, searchContext }, { searchOptions: () => searchOptions });
- if (isFocused) {
- findHeaderSearchInput().vm.$emit('click');
- }
});
const isSearching = search?.length > SEARCH_SHORTCUTS_MIN_CHARACTERS;
it(`classes ${isSearching ? 'contain' : 'do not contain'} "${IS_SEARCHING}"`, () => {
if (isSearching) {
- expect(findHeaderSearchForm().classes()).toContain(IS_SEARCHING);
+ expect(findGlobalSearchForm().classes()).toContain(IS_SEARCHING);
return;
}
if (!isSearching) {
- expect(findHeaderSearchForm().classes()).not.toContain(IS_SEARCHING);
+ expect(findGlobalSearchForm().classes()).not.toContain(IS_SEARCHING);
}
});
-
- it(`classes ${isSearching ? 'contain' : 'do not contain'} "${
- isFocused ? IS_FOCUSED : IS_NOT_FOCUSED
- }"`, () => {
- expect(findHeaderSearchForm().classes()).toContain(
- isFocused ? IS_FOCUSED : IS_NOT_FOCUSED,
- );
- });
});
});
@@ -328,7 +261,7 @@ describe('HeaderSearchApp', () => {
searchOptions: () => searchOptions,
},
);
- findHeaderSearchInput().vm.$emit('click');
+ findGlobalSearchInput().vm.$emit('click');
});
it(`icon for data set type "${searchOptions[0]?.html_id}" ${
@@ -350,56 +283,15 @@ describe('HeaderSearchApp', () => {
describe('events', () => {
beforeEach(() => {
- window.gon.current_username = MOCK_USERNAME;
createComponent();
+ window.gon.current_username = MOCK_USERNAME;
});
- describe('Header Search Input', () => {
- describe('when dropdown is closed', () => {
- let trackingSpy;
-
- beforeEach(() => {
- trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
- });
-
- it('onFocus opens dropdown and triggers snowplow event', async () => {
- expect(findHeaderSearchDropdown().exists()).toBe(false);
- findHeaderSearchInput().vm.$emit('focus');
-
- await nextTick();
-
- expect(findHeaderSearchDropdown().exists()).toBe(true);
- expect(trackingSpy).toHaveBeenCalledWith(undefined, 'focus_input', {
- label: 'global_search',
- property: 'navigation_top',
- });
- });
-
- it('onClick opens dropdown and triggers snowplow event', async () => {
- expect(findHeaderSearchDropdown().exists()).toBe(false);
- findHeaderSearchInput().vm.$emit('click');
-
- await nextTick();
-
- expect(findHeaderSearchDropdown().exists()).toBe(true);
- expect(trackingSpy).toHaveBeenCalledWith(undefined, 'focus_input', {
- label: 'global_search',
- property: 'navigation_top',
- });
- });
-
- it('onClick followed by onFocus only triggers a single snowplow event', async () => {
- findHeaderSearchInput().vm.$emit('click');
- findHeaderSearchInput().vm.$emit('focus');
-
- expect(trackingSpy).toHaveBeenCalledTimes(1);
- });
- });
-
+ describe('Global Search Input', () => {
describe('onInput', () => {
describe('when search has text', () => {
beforeEach(() => {
- findHeaderSearchInput().vm.$emit('input', MOCK_SEARCH);
+ findGlobalSearchInput().vm.$emit('input', MOCK_SEARCH);
});
it('calls setSearch with search term', () => {
@@ -417,7 +309,7 @@ describe('HeaderSearchApp', () => {
describe('when search is emptied', () => {
beforeEach(() => {
- findHeaderSearchInput().vm.$emit('input', '');
+ findGlobalSearchInput().vm.$emit('input', '');
});
it('calls setSearch with empty term', () => {
@@ -433,83 +325,29 @@ describe('HeaderSearchApp', () => {
});
});
});
- });
-
- describe('Dropdown Keyboard Navigation', () => {
- beforeEach(() => {
- findHeaderSearchInput().vm.$emit('click');
- });
-
- it('closes dropdown when @tab is emitted', async () => {
- expect(findHeaderSearchDropdown().exists()).toBe(true);
- findDropdownKeyboardNavigation().vm.$emit('tab');
-
- await nextTick();
-
- expect(findHeaderSearchDropdown().exists()).toBe(false);
- });
- });
- });
-
- describe('computed', () => {
- describe.each`
- MOCK_INDEX | search
- ${1} | ${null}
- ${SEARCH_BOX_INDEX} | ${'test'}
- ${2} | ${'test1'}
- `('currentFocusedOption', ({ MOCK_INDEX, search }) => {
- beforeEach(() => {
- window.gon.current_username = MOCK_USERNAME;
- createComponent({ search });
- findHeaderSearchInput().vm.$emit('click');
- });
-
- it(`when currentFocusIndex changes to ${MOCK_INDEX} updates the data to searchOptions[${MOCK_INDEX}]`, () => {
- findDropdownKeyboardNavigation().vm.$emit('change', MOCK_INDEX);
- expect(wrapper.vm.currentFocusedOption).toBe(MOCK_DEFAULT_SEARCH_OPTIONS[MOCK_INDEX]);
- });
- });
- });
-
- describe('Submitting a search', () => {
- describe('with no currentFocusedOption', () => {
- beforeEach(() => {
- createComponent();
- });
- it('onKey-enter submits a search', () => {
- findHeaderSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY }));
-
- expect(visitUrl).toHaveBeenCalledWith(MOCK_SEARCH_QUERY);
- });
- });
-
- describe('with less than min characters and no dropdown results', () => {
- beforeEach(() => {
- createComponent({ search: 'x' });
- });
-
- it('onKey-enter will NOT submit a search', () => {
- findHeaderSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY }));
+ describe('Submitting a search', () => {
+ beforeEach(() => {
+ createComponent();
+ });
- expect(visitUrl).not.toHaveBeenCalledWith(MOCK_SEARCH_QUERY);
- });
- });
+ it('onKey-enter submits a search', () => {
+ findGlobalSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY }));
- describe('with currentFocusedOption', () => {
- const MOCK_INDEX = 1;
+ expect(visitUrl).toHaveBeenCalledWith(MOCK_SEARCH_QUERY);
+ });
- beforeEach(() => {
- window.gon.current_username = MOCK_USERNAME;
- createComponent();
- findHeaderSearchInput().vm.$emit('click');
- });
+ describe('with less than min characters', () => {
+ beforeEach(() => {
+ createComponent({ search: 'x' });
+ });
- it('onKey-enter clicks the selected dropdown item rather than submitting a search', () => {
- findDropdownKeyboardNavigation().vm.$emit('change', MOCK_INDEX);
+ it('onKey-enter will NOT submit a search', () => {
+ findGlobalSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY }));
- findHeaderSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY }));
- expect(visitUrl).toHaveBeenCalledWith(MOCK_DEFAULT_SEARCH_OPTIONS[MOCK_INDEX].url);
+ expect(visitUrl).not.toHaveBeenCalledWith(MOCK_SEARCH_QUERY);
+ });
+ });
});
});
});
diff --git a/spec/frontend/super_sidebar/components/global_search/mock_data.js b/spec/frontend/super_sidebar/components/global_search/mock_data.js
index 58e578e4c4c..0884fce567c 100644
--- a/spec/frontend/super_sidebar/components/global_search/mock_data.js
+++ b/spec/frontend/super_sidebar/components/global_search/mock_data.js
@@ -3,6 +3,7 @@ import {
ICON_GROUP,
ICON_SUBGROUP,
} from '~/super_sidebar/components/global_search/constants';
+
import {
PROJECTS_CATEGORY,
GROUPS_CATEGORY,
@@ -77,90 +78,107 @@ export const MOCK_SEARCH_CONTEXT_FULL = {
export const MOCK_DEFAULT_SEARCH_OPTIONS = [
{
- html_id: 'default-issues-assigned',
- title: MSG_ISSUES_ASSIGNED_TO_ME,
- url: `${MOCK_ISSUE_PATH}/?assignee_username=${MOCK_USERNAME}`,
+ text: MSG_ISSUES_ASSIGNED_TO_ME,
+ href: `${MOCK_ISSUE_PATH}/?assignee_username=${MOCK_USERNAME}`,
},
{
- html_id: 'default-issues-created',
- title: MSG_ISSUES_IVE_CREATED,
- url: `${MOCK_ISSUE_PATH}/?author_username=${MOCK_USERNAME}`,
+ text: MSG_ISSUES_IVE_CREATED,
+ href: `${MOCK_ISSUE_PATH}/?author_username=${MOCK_USERNAME}`,
},
{
- html_id: 'default-mrs-assigned',
- title: MSG_MR_ASSIGNED_TO_ME,
- url: `${MOCK_MR_PATH}/?assignee_username=${MOCK_USERNAME}`,
+ text: MSG_MR_ASSIGNED_TO_ME,
+ href: `${MOCK_MR_PATH}/?assignee_username=${MOCK_USERNAME}`,
},
{
- html_id: 'default-mrs-reviewer',
- title: MSG_MR_IM_REVIEWER,
- url: `${MOCK_MR_PATH}/?reviewer_username=${MOCK_USERNAME}`,
+ text: MSG_MR_IM_REVIEWER,
+ href: `${MOCK_MR_PATH}/?reviewer_username=${MOCK_USERNAME}`,
},
{
- html_id: 'default-mrs-created',
- title: MSG_MR_IVE_CREATED,
- url: `${MOCK_MR_PATH}/?author_username=${MOCK_USERNAME}`,
+ text: MSG_MR_IVE_CREATED,
+ href: `${MOCK_MR_PATH}/?author_username=${MOCK_USERNAME}`,
},
];
-
-export const MOCK_SCOPED_SEARCH_OPTIONS = [
+export const MOCK_SCOPED_SEARCH_OPTIONS_DEF = [
{
- html_id: 'scoped-in-project',
+ text: 'scoped-in-project',
scope: MOCK_PROJECT.name,
scopeCategory: PROJECTS_CATEGORY,
icon: ICON_PROJECT,
- url: MOCK_PROJECT.path,
- },
- {
- html_id: 'scoped-in-project-long',
- scope: MOCK_PROJECT_LONG.name,
- scopeCategory: PROJECTS_CATEGORY,
- icon: ICON_PROJECT,
- url: MOCK_PROJECT_LONG.path,
+ href: MOCK_PROJECT.path,
},
{
- html_id: 'scoped-in-group',
+ text: 'scoped-in-group',
scope: MOCK_GROUP.name,
scopeCategory: GROUPS_CATEGORY,
icon: ICON_GROUP,
- url: MOCK_GROUP.path,
- },
- {
- html_id: 'scoped-in-subgroup',
- scope: MOCK_SUBGROUP.name,
- scopeCategory: GROUPS_CATEGORY,
- icon: ICON_SUBGROUP,
- url: MOCK_SUBGROUP.path,
+ href: MOCK_GROUP.path,
},
{
- html_id: 'scoped-in-all',
+ text: 'scoped-in-all',
description: MSG_IN_ALL_GITLAB,
- url: MOCK_ALL_PATH,
+ href: MOCK_ALL_PATH,
},
];
-
-export const MOCK_SCOPED_SEARCH_OPTIONS_DEF = [
+export const MOCK_SCOPED_SEARCH_OPTIONS = [
{
- html_id: 'scoped-in-project',
+ text: 'scoped-in-project',
scope: MOCK_PROJECT.name,
scopeCategory: PROJECTS_CATEGORY,
icon: ICON_PROJECT,
url: MOCK_PROJECT.path,
},
{
- html_id: 'scoped-in-group',
+ text: 'scoped-in-project-long',
+ scope: MOCK_PROJECT_LONG.name,
+ scopeCategory: PROJECTS_CATEGORY,
+ icon: ICON_PROJECT,
+ url: MOCK_PROJECT_LONG.path,
+ },
+ {
+ text: 'scoped-in-group',
scope: MOCK_GROUP.name,
scopeCategory: GROUPS_CATEGORY,
icon: ICON_GROUP,
url: MOCK_GROUP.path,
},
{
- html_id: 'scoped-in-all',
+ text: 'scoped-in-subgroup',
+ scope: MOCK_SUBGROUP.name,
+ scopeCategory: GROUPS_CATEGORY,
+ icon: ICON_SUBGROUP,
+ url: MOCK_SUBGROUP.path,
+ },
+ {
+ text: 'scoped-in-all',
description: MSG_IN_ALL_GITLAB,
url: MOCK_ALL_PATH,
},
];
+export const MOCK_SCOPED_SEARCH_GROUP = {
+ items: [
+ {
+ text: 'scoped-in-project',
+ scope: MOCK_PROJECT.name,
+ scopeCategory: PROJECTS_CATEGORY,
+ icon: ICON_PROJECT,
+ href: MOCK_PROJECT.path,
+ },
+ {
+ text: 'scoped-in-group',
+ scope: MOCK_GROUP.name,
+ scopeCategory: GROUPS_CATEGORY,
+ icon: ICON_GROUP,
+ href: MOCK_GROUP.path,
+ },
+ {
+ text: 'scoped-in-all',
+ description: MSG_IN_ALL_GITLAB,
+ href: MOCK_ALL_PATH,
+ },
+ ],
+};
+
export const MOCK_AUTOCOMPLETE_OPTIONS_RES = [
{
category: 'Projects',
@@ -168,8 +186,10 @@ export const MOCK_AUTOCOMPLETE_OPTIONS_RES = [
label: 'Gitlab Org / MockProject1',
value: 'MockProject1',
url: 'project/1',
+ avatar_url: '/project/avatar/1/avatar.png',
},
{
+ avatar_url: '/groups/avatar/1/avatar.png',
category: 'Groups',
id: 1,
label: 'Gitlab Org / MockGroup1',
@@ -177,6 +197,7 @@ export const MOCK_AUTOCOMPLETE_OPTIONS_RES = [
url: 'group/1',
},
{
+ avatar_url: '/project/avatar/2/avatar.png',
category: 'Projects',
id: 2,
label: 'Gitlab Org / MockProject2',
@@ -193,31 +214,30 @@ export const MOCK_AUTOCOMPLETE_OPTIONS_RES = [
export const MOCK_AUTOCOMPLETE_OPTIONS = [
{
category: 'Projects',
- html_id: 'autocomplete-Projects-0',
id: 1,
label: 'Gitlab Org / MockProject1',
value: 'MockProject1',
url: 'project/1',
+ avatar_url: '/project/avatar/1/avatar.png',
},
{
category: 'Groups',
- html_id: 'autocomplete-Groups-1',
id: 1,
label: 'Gitlab Org / MockGroup1',
value: 'MockGroup1',
url: 'group/1',
+ avatar_url: '/groups/avatar/1/avatar.png',
},
{
category: 'Projects',
- html_id: 'autocomplete-Projects-2',
id: 2,
label: 'Gitlab Org / MockProject2',
value: 'MockProject2',
url: 'project/2',
+ avatar_url: '/project/avatar/2/avatar.png',
},
{
category: 'Help',
- html_id: 'autocomplete-Help-3',
label: 'GitLab Help',
url: 'help/gitlab',
},
@@ -225,51 +245,64 @@ export const MOCK_AUTOCOMPLETE_OPTIONS = [
export const MOCK_GROUPED_AUTOCOMPLETE_OPTIONS = [
{
- category: 'Groups',
- data: [
+ name: 'Groups',
+ items: [
{
category: 'Groups',
- html_id: 'autocomplete-Groups-1',
-
id: 1,
label: 'Gitlab Org / MockGroup1',
+ namespace: 'Gitlab Org / MockGroup1',
value: 'MockGroup1',
- url: 'group/1',
+ text: 'MockGroup1',
+ href: 'group/1',
+ avatar_url: '/groups/avatar/1/avatar.png',
+ avatar_size: 32,
+ entity_id: 1,
+ entity_name: 'MockGroup1',
},
],
},
{
- category: 'Projects',
- data: [
+ name: 'Projects',
+ items: [
{
category: 'Projects',
- html_id: 'autocomplete-Projects-0',
-
id: 1,
label: 'Gitlab Org / MockProject1',
+ namespace: 'Gitlab Org / MockProject1',
value: 'MockProject1',
- url: 'project/1',
+ text: 'MockProject1',
+ href: 'project/1',
+ avatar_url: '/project/avatar/1/avatar.png',
+ avatar_size: 32,
+ entity_id: 1,
+ entity_name: 'MockProject1',
},
{
category: 'Projects',
- html_id: 'autocomplete-Projects-2',
-
id: 2,
- label: 'Gitlab Org / MockProject2',
value: 'MockProject2',
- url: 'project/2',
+ label: 'Gitlab Org / MockProject2',
+ namespace: 'Gitlab Org / MockProject2',
+ text: 'MockProject2',
+ href: 'project/2',
+ avatar_url: '/project/avatar/2/avatar.png',
+ avatar_size: 32,
+ entity_id: 2,
+ entity_name: 'MockProject2',
},
],
},
{
- category: 'Help',
- data: [
+ name: 'Help',
+ items: [
{
category: 'Help',
- html_id: 'autocomplete-Help-3',
-
label: 'GitLab Help',
- url: 'help/gitlab',
+ text: 'GitLab Help',
+ href: 'help/gitlab',
+ avatar_size: 16,
+ entity_name: 'GitLab Help',
},
],
},
@@ -278,33 +311,50 @@ export const MOCK_GROUPED_AUTOCOMPLETE_OPTIONS = [
export const MOCK_SORTED_AUTOCOMPLETE_OPTIONS = [
{
category: 'Groups',
- html_id: 'autocomplete-Groups-1',
id: 1,
label: 'Gitlab Org / MockGroup1',
value: 'MockGroup1',
- url: 'group/1',
+ text: 'MockGroup1',
+ href: 'group/1',
+ namespace: 'Gitlab Org / MockGroup1',
+ avatar_url: '/groups/avatar/1/avatar.png',
+ avatar_size: 32,
+ entity_id: 1,
+ entity_name: 'MockGroup1',
},
{
+ avatar_size: 32,
+ avatar_url: '/project/avatar/1/avatar.png',
category: 'Projects',
- html_id: 'autocomplete-Projects-0',
+ entity_id: 1,
+ entity_name: 'MockProject1',
+ href: 'project/1',
id: 1,
label: 'Gitlab Org / MockProject1',
+ namespace: 'Gitlab Org / MockProject1',
+ text: 'MockProject1',
value: 'MockProject1',
- url: 'project/1',
},
{
+ avatar_size: 32,
+ avatar_url: '/project/avatar/2/avatar.png',
category: 'Projects',
- html_id: 'autocomplete-Projects-2',
+ entity_id: 2,
+ entity_name: 'MockProject2',
+ href: 'project/2',
id: 2,
label: 'Gitlab Org / MockProject2',
+ namespace: 'Gitlab Org / MockProject2',
+ text: 'MockProject2',
value: 'MockProject2',
- url: 'project/2',
},
{
+ avatar_size: 16,
+ entity_name: 'GitLab Help',
category: 'Help',
- html_id: 'autocomplete-Help-3',
label: 'GitLab Help',
- url: 'help/gitlab',
+ text: 'GitLab Help',
+ href: 'help/gitlab',
},
];
@@ -315,14 +365,16 @@ export const MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_HELP = [
{
html_id: 'autocomplete-Help-1',
category: 'Help',
+ text: 'Rake Tasks Help',
label: 'Rake Tasks Help',
- url: '/help/raketasks/index',
+ href: '/help/raketasks/index',
},
{
html_id: 'autocomplete-Help-2',
category: 'Help',
+ text: 'System Hooks Help',
label: 'System Hooks Help',
- url: '/help/system_hooks/system_hooks',
+ href: '/help/system_hooks/system_hooks',
},
],
},
diff --git a/spec/frontend/super_sidebar/components/global_search/store/actions_spec.js b/spec/frontend/super_sidebar/components/global_search/store/actions_spec.js
index c87b4513309..f6d8e1f26eb 100644
--- a/spec/frontend/super_sidebar/components/global_search/store/actions_spec.js
+++ b/spec/frontend/super_sidebar/components/global_search/store/actions_spec.js
@@ -16,9 +16,7 @@ import {
MOCK_ISSUE_PATH,
} from '../mock_data';
-jest.mock('~/alert');
-
-describe('Header Search Store Actions', () => {
+describe('Global Search Store Actions', () => {
let state;
let mock;
diff --git a/spec/frontend/super_sidebar/components/global_search/store/getters_spec.js b/spec/frontend/super_sidebar/components/global_search/store/getters_spec.js
index dca96da01a7..68583d04b31 100644
--- a/spec/frontend/super_sidebar/components/global_search/store/getters_spec.js
+++ b/spec/frontend/super_sidebar/components/global_search/store/getters_spec.js
@@ -9,7 +9,7 @@ import {
MOCK_SEARCH_CONTEXT,
MOCK_DEFAULT_SEARCH_OPTIONS,
MOCK_SCOPED_SEARCH_OPTIONS,
- MOCK_SCOPED_SEARCH_OPTIONS_DEF,
+ MOCK_SCOPED_SEARCH_GROUP,
MOCK_PROJECT,
MOCK_GROUP,
MOCK_ALL_PATH,
@@ -17,9 +17,10 @@ import {
MOCK_AUTOCOMPLETE_OPTIONS,
MOCK_GROUPED_AUTOCOMPLETE_OPTIONS,
MOCK_SORTED_AUTOCOMPLETE_OPTIONS,
+ MOCK_SCOPED_SEARCH_OPTIONS_DEF,
} from '../mock_data';
-describe('Header Search Store Getters', () => {
+describe('Global Search Store Getters', () => {
let state;
const createState = (initialState) => {
@@ -288,7 +289,7 @@ describe('Header Search Store Getters', () => {
describe.each`
search | defaultSearchOptions | scopedSearchOptions | autocompleteGroupedSearchOptions | expectedArray
- ${null} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS} | ${MOCK_DEFAULT_SEARCH_OPTIONS}
+ ${null} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${MOCK_SCOPED_SEARCH_GROUP} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS} | ${MOCK_DEFAULT_SEARCH_OPTIONS}
${MOCK_SEARCH} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${[]} | ${MOCK_SCOPED_SEARCH_OPTIONS}
${MOCK_SEARCH} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${[]} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS} | ${MOCK_SORTED_AUTOCOMPLETE_OPTIONS}
${MOCK_SEARCH} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS} | ${MOCK_SCOPED_SEARCH_OPTIONS.concat(MOCK_SORTED_AUTOCOMPLETE_OPTIONS)}
diff --git a/spec/frontend/super_sidebar/components/global_search/store/mutations_spec.js b/spec/frontend/super_sidebar/components/global_search/store/mutations_spec.js
index d2dc484e825..4d275cf86c7 100644
--- a/spec/frontend/super_sidebar/components/global_search/store/mutations_spec.js
+++ b/spec/frontend/super_sidebar/components/global_search/store/mutations_spec.js
@@ -29,7 +29,7 @@ describe('Header Search Store Mutations', () => {
mutations[types.RECEIVE_AUTOCOMPLETE_SUCCESS](state, MOCK_AUTOCOMPLETE_OPTIONS_RES);
expect(state.loading).toBe(false);
- expect(state.autocompleteOptions).toStrictEqual(MOCK_AUTOCOMPLETE_OPTIONS);
+ expect(state.autocompleteOptions).toEqual(MOCK_AUTOCOMPLETE_OPTIONS);
expect(state.autocompleteError).toBe(false);
});
});
diff --git a/spec/frontend/super_sidebar/components/global_search/utils_spec.js b/spec/frontend/super_sidebar/components/global_search/utils_spec.js
new file mode 100644
index 00000000000..3b12063e733
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/global_search/utils_spec.js
@@ -0,0 +1,60 @@
+import { getFormattedItem } from '~/super_sidebar/components/global_search/utils';
+import {
+ LARGE_AVATAR_PX,
+ SMALL_AVATAR_PX,
+} from '~/super_sidebar/components/global_search/constants';
+import {
+ GROUPS_CATEGORY,
+ PROJECTS_CATEGORY,
+ MERGE_REQUEST_CATEGORY,
+ ISSUES_CATEGORY,
+ RECENT_EPICS_CATEGORY,
+} from '~/vue_shared/global_search/constants';
+
+describe('getFormattedItem', () => {
+ describe.each`
+ item | avatarSize | searchContext | entityId | entityName
+ ${{ category: PROJECTS_CATEGORY, label: 'project1' }} | ${LARGE_AVATAR_PX} | ${{ project: { id: 29 } }} | ${29} | ${'project1'}
+ ${{ category: GROUPS_CATEGORY, label: 'project1' }} | ${LARGE_AVATAR_PX} | ${{ group: { id: 12 } }} | ${12} | ${'project1'}
+ ${{ category: 'Help', label: 'project1' }} | ${SMALL_AVATAR_PX} | ${null} | ${undefined} | ${'project1'}
+ ${{ category: 'Settings', label: 'project1' }} | ${SMALL_AVATAR_PX} | ${null} | ${undefined} | ${'project1'}
+ ${{ category: GROUPS_CATEGORY, value: 'group1', label: 'Group 1' }} | ${LARGE_AVATAR_PX} | ${{ group: { id: 1, name: 'test1' } }} | ${1} | ${'group1'}
+ ${{ category: PROJECTS_CATEGORY, value: 'group2', label: 'Group2' }} | ${LARGE_AVATAR_PX} | ${{ project: { id: 2, name: 'test2' } }} | ${2} | ${'group2'}
+ ${{ category: ISSUES_CATEGORY }} | ${SMALL_AVATAR_PX} | ${{ project: { id: 3, name: 'test3' } }} | ${3} | ${'test3'}
+ ${{ category: MERGE_REQUEST_CATEGORY }} | ${SMALL_AVATAR_PX} | ${{ project: { id: 4, name: 'test4' } }} | ${4} | ${'test4'}
+ ${{ category: RECENT_EPICS_CATEGORY }} | ${SMALL_AVATAR_PX} | ${{ group: { id: 5, name: 'test5' } }} | ${5} | ${'test5'}
+ ${{ category: GROUPS_CATEGORY, group_id: 6, group_name: 'test6' }} | ${LARGE_AVATAR_PX} | ${null} | ${6} | ${'test6'}
+ ${{ category: PROJECTS_CATEGORY, project_id: 7, project_name: 'test7' }} | ${LARGE_AVATAR_PX} | ${null} | ${7} | ${'test7'}
+ ${{ category: ISSUES_CATEGORY, project_id: 8, project_name: 'test8' }} | ${SMALL_AVATAR_PX} | ${null} | ${8} | ${'test8'}
+ ${{ category: MERGE_REQUEST_CATEGORY, project_id: 9, project_name: 'test9' }} | ${SMALL_AVATAR_PX} | ${null} | ${9} | ${'test9'}
+ ${{ category: RECENT_EPICS_CATEGORY, group_id: 10, group_name: 'test10' }} | ${SMALL_AVATAR_PX} | ${null} | ${10} | ${'test10'}
+ ${{ category: GROUPS_CATEGORY, group_id: 11, group_name: 'test11' }} | ${LARGE_AVATAR_PX} | ${{ group: { id: 1, name: 'test1' } }} | ${11} | ${'test11'}
+ ${{ category: PROJECTS_CATEGORY, project_id: 12, project_name: 'test12' }} | ${LARGE_AVATAR_PX} | ${{ project: { id: 2, name: 'test2' } }} | ${12} | ${'test12'}
+ ${{ category: ISSUES_CATEGORY, project_id: 13, project_name: 'test13' }} | ${SMALL_AVATAR_PX} | ${{ project: { id: 3, name: 'test3' } }} | ${13} | ${'test13'}
+ ${{ category: MERGE_REQUEST_CATEGORY, project_id: 14, project_name: 'test14' }} | ${SMALL_AVATAR_PX} | ${{ project: { id: 4, name: 'test4' } }} | ${14} | ${'test14'}
+ ${{ category: RECENT_EPICS_CATEGORY, group_id: 15, group_name: 'test15' }} | ${SMALL_AVATAR_PX} | ${{ group: { id: 5, name: 'test5' } }} | ${15} | ${'test15'}
+ `('formats the item', ({ item, avatarSize, searchContext, entityId, entityName }) => {
+ describe(`when item is ${JSON.stringify(item)}`, () => {
+ let formattedItem;
+ beforeEach(() => {
+ formattedItem = getFormattedItem(item, searchContext);
+ });
+
+ it(`should set text to ${item.value || item.label}`, () => {
+ expect(formattedItem.text).toBe(item.value || item.label);
+ });
+
+ it(`should set avatarSize to ${avatarSize}`, () => {
+ expect(formattedItem.avatar_size).toBe(avatarSize);
+ });
+
+ it(`should set avatar entityId to ${entityId}`, () => {
+ expect(formattedItem.entity_id).toBe(entityId);
+ });
+
+ it(`should set avatar entityName to ${entityName}`, () => {
+ expect(formattedItem.entity_name).toBe(entityName);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/groups_list_spec.js b/spec/frontend/super_sidebar/components/groups_list_spec.js
index 6aee895f611..4fa3303c12f 100644
--- a/spec/frontend/super_sidebar/components/groups_list_spec.js
+++ b/spec/frontend/super_sidebar/components/groups_list_spec.js
@@ -19,11 +19,14 @@ describe('GroupsList component', () => {
const itRendersViewAllItem = () => {
it('renders the "View all..." item', () => {
- expect(findViewAllLink().props('item')).toEqual({
+ const link = findViewAllLink();
+
+ expect(link.props('item')).toEqual({
icon: 'group',
link: viewAllLink,
- title: s__('Navigation|View all groups'),
+ title: s__('Navigation|View all your groups'),
});
+ expect(link.props('linkClasses')).toEqual({ 'dashboard-shortcuts-groups': true });
});
};
@@ -75,7 +78,7 @@ describe('GroupsList component', () => {
it('passes the correct props to the frequent items list', () => {
expect(findFrequentItemsList().props()).toEqual({
- title: s__('Navigation|Frequent groups'),
+ title: s__('Navigation|Frequently visited groups'),
storageKey,
maxItems: MAX_FREQUENT_GROUPS_COUNT,
pristineText: s__('Navigation|Groups you visit often will appear here.'),
diff --git a/spec/frontend/super_sidebar/components/help_center_spec.js b/spec/frontend/super_sidebar/components/help_center_spec.js
index 1d072c0ba3c..4c0e7a89a43 100644
--- a/spec/frontend/super_sidebar/components/help_center_spec.js
+++ b/spec/frontend/super_sidebar/components/help_center_spec.js
@@ -1,4 +1,4 @@
-import { GlDisclosureDropdownGroup } from '@gitlab/ui';
+import { GlDisclosureDropdown, GlDisclosureDropdownGroup } from '@gitlab/ui';
import { within } from '@testing-library/dom';
import toggleWhatsNewDrawer from '~/whats_new';
import { mountExtended } from 'helpers/vue_test_utils_helper';
@@ -7,15 +7,18 @@ import { helpPagePath } from '~/helpers/help_page_helper';
import { PROMO_URL } from 'jh_else_ce/lib/utils/url_utility';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import { STORAGE_KEY } from '~/whats_new/utils/notification';
+import { mockTracking } from 'helpers/tracking_helper';
import { sidebarData } from '../mock_data';
jest.mock('~/whats_new');
describe('HelpCenter component', () => {
let wrapper;
+ let trackingSpy;
const GlEmoji = { template: '<img/>' };
+ const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
const findDropdownGroup = (i = 0) => {
return wrapper.findAllComponents(GlDisclosureDropdownGroup).at(i);
};
@@ -28,6 +31,15 @@ describe('HelpCenter component', () => {
propsData: { sidebarData },
stubs: { GlEmoji },
});
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ };
+
+ const trackingAttrs = (label) => {
+ return {
+ 'data-track-action': 'click_link',
+ 'data-track-property': 'nav_help_menu',
+ 'data-track-label': label,
+ };
};
describe('default', () => {
@@ -37,16 +49,37 @@ describe('HelpCenter component', () => {
it('renders menu items', () => {
expect(findDropdownGroup(0).props('group').items).toEqual([
- { text: HelpCenter.i18n.help, href: helpPagePath() },
- { text: HelpCenter.i18n.support, href: sidebarData.support_path },
- { text: HelpCenter.i18n.docs, href: 'https://docs.gitlab.com' },
- { text: HelpCenter.i18n.plans, href: `${PROMO_URL}/pricing` },
- { text: HelpCenter.i18n.forum, href: 'https://forum.gitlab.com/' },
+ { text: HelpCenter.i18n.help, href: helpPagePath(), extraAttrs: trackingAttrs('help') },
+ {
+ text: HelpCenter.i18n.support,
+ href: sidebarData.support_path,
+ extraAttrs: trackingAttrs('support'),
+ },
+ {
+ text: HelpCenter.i18n.docs,
+ href: 'https://docs.gitlab.com',
+ extraAttrs: trackingAttrs('gitlab_documentation'),
+ },
+ {
+ text: HelpCenter.i18n.plans,
+ href: `${PROMO_URL}/pricing`,
+ extraAttrs: trackingAttrs('compare_gitlab_plans'),
+ },
+ {
+ text: HelpCenter.i18n.forum,
+ href: 'https://forum.gitlab.com/',
+ extraAttrs: trackingAttrs('community_forum'),
+ },
{
text: HelpCenter.i18n.contribute,
href: helpPagePath('', { anchor: 'contributing-to-gitlab' }),
+ extraAttrs: trackingAttrs('contribute_to_gitlab'),
+ },
+ {
+ text: HelpCenter.i18n.feedback,
+ href: 'https://about.gitlab.com/submit-feedback',
+ extraAttrs: trackingAttrs('submit_feedback'),
},
- { text: HelpCenter.i18n.feedback, href: 'https://about.gitlab.com/submit-feedback' },
]);
expect(findDropdownGroup(1).props('group').items).toEqual([
@@ -55,6 +88,12 @@ describe('HelpCenter component', () => {
]);
});
+ it('passes popper options to the dropdown', () => {
+ expect(findDropdown().props('popperOptions')).toEqual({
+ modifiers: [{ name: 'offset', options: { offset: [-4, 4] } }],
+ });
+ });
+
describe('with Gitlab version check feature enabled', () => {
beforeEach(() => {
createWrapper({ ...sidebarData, show_version_check: true });
@@ -62,7 +101,12 @@ describe('HelpCenter component', () => {
it('shows version information as first item', () => {
expect(findDropdownGroup(0).props('group').items).toEqual([
- { text: HelpCenter.i18n.version, href: helpPagePath('update/index'), version: '16.0' },
+ {
+ text: HelpCenter.i18n.version,
+ href: helpPagePath('update/index'),
+ version: '16.0',
+ extraAttrs: trackingAttrs('version_help_dropdown'),
+ },
]);
});
});
@@ -86,11 +130,24 @@ describe('HelpCenter component', () => {
// ~/behaviors/shortcuts/shortcuts.js.
expect(button.classList.contains('js-shortcuts-modal-trigger')).toBe(true);
});
+
+ it('should have Snowplow tracking attributes', () => {
+ expect(findButton('Keyboard shortcuts ?').dataset).toEqual(
+ expect.objectContaining({
+ trackAction: 'click_button',
+ trackLabel: 'keyboard_shortcuts_help',
+ trackProperty: 'nav_help_menu',
+ }),
+ );
+ });
});
describe('showWhatsNew', () => {
beforeEach(() => {
jest.spyOn(wrapper.vm.$refs.dropdown, 'close');
+ beforeEach(() => {
+ createWrapper({ ...sidebarData, show_version_check: true });
+ });
findButton("What's new 5").click();
});
@@ -107,6 +164,18 @@ describe('HelpCenter component', () => {
expect(toggleWhatsNewDrawer).toHaveBeenCalledTimes(2);
expect(toggleWhatsNewDrawer).toHaveBeenLastCalledWith();
});
+
+ it('should have Snowplow tracking attributes', () => {
+ createWrapper({ ...sidebarData, display_whats_new: true });
+
+ expect(findButton("What's new 5").dataset).toEqual(
+ expect.objectContaining({
+ trackAction: 'click_button',
+ trackLabel: 'whats_new',
+ trackProperty: 'nav_help_menu',
+ }),
+ );
+ });
});
describe('shouldShowWhatsNewNotification', () => {
@@ -153,5 +222,23 @@ describe('HelpCenter component', () => {
});
});
});
+
+ describe('toggle dropdown', () => {
+ it('should track Snowplow event when dropdown is shown', () => {
+ findDropdown().vm.$emit('shown');
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_toggle', {
+ label: 'show_help_dropdown',
+ property: 'nav_help_menu',
+ });
+ });
+
+ it('should track Snowplow event when dropdown is hidden', () => {
+ findDropdown().vm.$emit('hidden');
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_toggle', {
+ label: 'hide_help_dropdown',
+ property: 'nav_help_menu',
+ });
+ });
+ });
});
});
diff --git a/spec/frontend/super_sidebar/components/items_list_spec.js b/spec/frontend/super_sidebar/components/items_list_spec.js
index 8e00984f500..d49ef35e9d8 100644
--- a/spec/frontend/super_sidebar/components/items_list_spec.js
+++ b/spec/frontend/super_sidebar/components/items_list_spec.js
@@ -1,4 +1,5 @@
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { GlIcon } from '@gitlab/ui';
+import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
import ItemsList from '~/super_sidebar/components/items_list.vue';
import NavItem from '~/super_sidebar/components/nav_item.vue';
import { cachedFrequentProjects } from '../mock_data';
@@ -11,8 +12,8 @@ describe('ItemsList component', () => {
const findNavItems = () => wrapper.findAllComponents(NavItem);
- const createWrapper = ({ props = {}, slots = {} } = {}) => {
- wrapper = shallowMountExtended(ItemsList, {
+ const createWrapper = ({ props = {}, slots = {}, mountFn = shallowMountExtended } = {}) => {
+ wrapper = mountFn(ItemsList, {
propsData: {
...props,
},
@@ -60,4 +61,42 @@ describe('ItemsList component', () => {
expect(wrapper.findByTestId(testId).exists()).toBe(true);
});
+
+ describe('item removal', () => {
+ const findRemoveButton = () => wrapper.findByTestId('item-remove');
+ const mockProject = {
+ ...firstMockedProject,
+ title: firstMockedProject.name,
+ };
+
+ beforeEach(() => {
+ createWrapper({
+ props: {
+ items: [mockProject],
+ editable: true,
+ },
+ mountFn: mountExtended,
+ });
+ });
+
+ it('renders the remove button', () => {
+ const itemRemoveButton = findRemoveButton();
+
+ expect(itemRemoveButton.exists()).toBe(true);
+ expect(itemRemoveButton.attributes('title')).toBe('Remove');
+ expect(itemRemoveButton.findComponent(GlIcon).props('name')).toBe('close');
+ });
+
+ it('emits `remove-item` event with item param when remove button is clicked', () => {
+ const itemRemoveButton = findRemoveButton();
+
+ itemRemoveButton.vm.$emit(
+ 'click',
+ { stopPropagation: jest.fn(), preventDefault: jest.fn() },
+ mockProject,
+ );
+
+ expect(wrapper.emitted('remove-item')).toEqual([[mockProject]]);
+ });
+ });
});
diff --git a/spec/frontend/super_sidebar/components/merge_request_menu_spec.js b/spec/frontend/super_sidebar/components/merge_request_menu_spec.js
index fe87c4be9c3..9c8fd0556f1 100644
--- a/spec/frontend/super_sidebar/components/merge_request_menu_spec.js
+++ b/spec/frontend/super_sidebar/components/merge_request_menu_spec.js
@@ -8,7 +8,7 @@ describe('MergeRequestMenu component', () => {
const findGlBadge = (at) => wrapper.findAllComponents(GlBadge).at(at);
const findGlDisclosureDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
- const findLink = () => wrapper.findByRole('link');
+ const findLink = (name) => wrapper.findByRole('link', { name });
const createWrapper = () => {
wrapper = mountExtended(MergeRequestMenu, {
@@ -27,11 +27,18 @@ describe('MergeRequestMenu component', () => {
expect(findGlDisclosureDropdown().props('items')).toBe(mergeRequestMenuGroup);
});
- it('renders item text and count in link', () => {
- const { text, href, count } = mergeRequestMenuGroup[0].items[0];
- expect(findLink().text()).toContain(text);
- expect(findLink().text()).toContain(String(count));
- expect(findLink().attributes('href')).toBe(href);
+ it.each(mergeRequestMenuGroup[0].items)('renders item text and count in link', (item) => {
+ const index = mergeRequestMenuGroup[0].items.indexOf(item);
+ const { text, href, count, extraAttrs } = mergeRequestMenuGroup[0].items[index];
+ const link = findLink(new RegExp(text));
+
+ expect(link.text()).toContain(text);
+ expect(link.text()).toContain(String(count));
+ expect(link.attributes('href')).toBe(href);
+ expect(link.attributes('data-track-action')).toBe(extraAttrs['data-track-action']);
+ expect(link.attributes('data-track-label')).toBe(extraAttrs['data-track-label']);
+ expect(link.attributes('data-track-property')).toBe(extraAttrs['data-track-property']);
+ expect(link.attributes('class')).toContain(extraAttrs.class);
});
it('renders item count string in badge', () => {
diff --git a/spec/frontend/super_sidebar/components/nav_item_spec.js b/spec/frontend/super_sidebar/components/nav_item_spec.js
index 22989c1a5f9..d96d2b77d21 100644
--- a/spec/frontend/super_sidebar/components/nav_item_spec.js
+++ b/spec/frontend/super_sidebar/components/nav_item_spec.js
@@ -1,18 +1,24 @@
import { GlBadge } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import NavItem from '~/super_sidebar/components/nav_item.vue';
+import {
+ CLICK_MENU_ITEM_ACTION,
+ TRACKING_UNKNOWN_ID,
+ TRACKING_UNKNOWN_PANEL,
+} from '~/super_sidebar/constants';
describe('NavItem component', () => {
let wrapper;
const findLink = () => wrapper.findByTestId('nav-item-link');
const findPill = () => wrapper.findComponent(GlBadge);
- const createWrapper = (item, props = {}) => {
+ const createWrapper = (item, props = {}, provide = {}) => {
wrapper = shallowMountExtended(NavItem, {
propsData: {
item,
...props,
},
+ provide,
});
};
@@ -46,4 +52,34 @@ describe('NavItem component', () => {
expect(findLink().attributes('class')).toContain(customClass);
});
+
+ describe('Data Tracking Attributes', () => {
+ it('adds no labels on sections', () => {
+ const id = 'my-id';
+ createWrapper({ title: 'Foo', id, items: [{ title: 'Baz' }] });
+
+ expect(findLink().attributes('data-track-action')).toBeUndefined();
+ expect(findLink().attributes('data-track-label')).toBeUndefined();
+ expect(findLink().attributes('data-track-property')).toBeUndefined();
+ expect(findLink().attributes('data-track-extra')).toBeUndefined();
+ });
+
+ it.each`
+ id | panelType | eventLabel | eventProperty | eventExtra
+ ${'abc'} | ${'xyz'} | ${'abc'} | ${'nav_panel_xyz'} | ${undefined}
+ ${undefined} | ${'xyz'} | ${TRACKING_UNKNOWN_ID} | ${'nav_panel_xyz'} | ${'{"title":"Foo"}'}
+ ${'abc'} | ${undefined} | ${'abc'} | ${TRACKING_UNKNOWN_PANEL} | ${'{"title":"Foo"}'}
+ ${undefined} | ${undefined} | ${TRACKING_UNKNOWN_ID} | ${TRACKING_UNKNOWN_PANEL} | ${'{"title":"Foo"}'}
+ `(
+ 'adds appropriate data tracking labels for id=$id and panelType=$panelType',
+ ({ id, eventLabel, panelType, eventProperty, eventExtra }) => {
+ createWrapper({ title: 'Foo', id }, {}, { panelType });
+
+ expect(findLink().attributes('data-track-action')).toBe(CLICK_MENU_ITEM_ACTION);
+ expect(findLink().attributes('data-track-label')).toBe(eventLabel);
+ expect(findLink().attributes('data-track-property')).toBe(eventProperty);
+ expect(findLink().attributes('data-track-extra')).toBe(eventExtra);
+ },
+ );
+ });
});
diff --git a/spec/frontend/super_sidebar/components/pinned_section_spec.js b/spec/frontend/super_sidebar/components/pinned_section_spec.js
new file mode 100644
index 00000000000..7ead6a40895
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/pinned_section_spec.js
@@ -0,0 +1,75 @@
+import { nextTick } from 'vue';
+import Cookies from '~/lib/utils/cookies';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import PinnedSection from '~/super_sidebar/components/pinned_section.vue';
+import NavItem from '~/super_sidebar/components/nav_item.vue';
+import { SIDEBAR_PINS_EXPANDED_COOKIE, SIDEBAR_COOKIE_EXPIRATION } from '~/super_sidebar/constants';
+import { setCookie } from '~/lib/utils/common_utils';
+
+jest.mock('~/lib/utils/common_utils', () => ({
+ getCookie: jest.requireActual('~/lib/utils/common_utils').getCookie,
+ setCookie: jest.fn(),
+}));
+
+describe('PinnedSection component', () => {
+ let wrapper;
+
+ const findToggle = () => wrapper.find('a');
+
+ const createWrapper = () => {
+ wrapper = mountExtended(PinnedSection, {
+ propsData: {
+ items: [{ title: 'Pin 1', href: '/page1' }],
+ },
+ });
+ };
+
+ describe('expanded', () => {
+ describe('when cookie is not set', () => {
+ it('is expanded by default', () => {
+ createWrapper();
+ expect(wrapper.findComponent(NavItem).isVisible()).toBe(true);
+ });
+ });
+
+ describe('when cookie is set to false', () => {
+ beforeEach(() => {
+ Cookies.set(SIDEBAR_PINS_EXPANDED_COOKIE, 'false');
+ createWrapper();
+ });
+
+ it('is collapsed', () => {
+ expect(wrapper.findComponent(NavItem).isVisible()).toBe(false);
+ });
+
+ it('updates the cookie when expanding the section', async () => {
+ findToggle().trigger('click');
+ await nextTick();
+
+ expect(setCookie).toHaveBeenCalledWith(SIDEBAR_PINS_EXPANDED_COOKIE, true, {
+ expires: SIDEBAR_COOKIE_EXPIRATION,
+ });
+ });
+ });
+
+ describe('when cookie is set to true', () => {
+ beforeEach(() => {
+ Cookies.set(SIDEBAR_PINS_EXPANDED_COOKIE, 'true');
+ createWrapper();
+ });
+
+ it('is expanded', () => {
+ expect(wrapper.findComponent(NavItem).isVisible()).toBe(true);
+ });
+
+ it('updates the cookie when collapsing the section', async () => {
+ findToggle().trigger('click');
+ await nextTick();
+
+ expect(setCookie).toHaveBeenCalledWith(SIDEBAR_PINS_EXPANDED_COOKIE, false, {
+ expires: SIDEBAR_COOKIE_EXPIRATION,
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/projects_list_spec.js b/spec/frontend/super_sidebar/components/projects_list_spec.js
index cdc003b14e0..93a414e1e8c 100644
--- a/spec/frontend/super_sidebar/components/projects_list_spec.js
+++ b/spec/frontend/super_sidebar/components/projects_list_spec.js
@@ -19,11 +19,14 @@ describe('ProjectsList component', () => {
const itRendersViewAllItem = () => {
it('renders the "View all..." item', () => {
- expect(findViewAllLink().props('item')).toEqual({
+ const link = findViewAllLink();
+
+ expect(link.props('item')).toEqual({
icon: 'project',
link: viewAllLink,
- title: s__('Navigation|View all projects'),
+ title: s__('Navigation|View all your projects'),
});
+ expect(link.props('linkClasses')).toEqual({ 'dashboard-shortcuts-projects': true });
});
};
@@ -70,7 +73,7 @@ describe('ProjectsList component', () => {
it('passes the correct props to the frequent items list', () => {
expect(findFrequentItemsList().props()).toEqual({
- title: s__('Navigation|Frequent projects'),
+ title: s__('Navigation|Frequently visited projects'),
storageKey,
maxItems: MAX_FREQUENT_PROJECTS_COUNT,
pristineText: s__('Navigation|Projects you visit often will appear here.'),
diff --git a/spec/frontend/super_sidebar/components/search_results_spec.js b/spec/frontend/super_sidebar/components/search_results_spec.js
index dd48935c138..daec5c2a9b4 100644
--- a/spec/frontend/super_sidebar/components/search_results_spec.js
+++ b/spec/frontend/super_sidebar/components/search_results_spec.js
@@ -1,7 +1,9 @@
+import { GlCollapse } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { s__ } from '~/locale';
import SearchResults from '~/super_sidebar/components/search_results.vue';
import ItemsList from '~/super_sidebar/components/items_list.vue';
+import { stubComponent } from 'helpers/stub_component';
const title = s__('Navigation|PROJECTS');
const noResultsText = s__('Navigation|No project matches found');
@@ -9,7 +11,8 @@ const noResultsText = s__('Navigation|No project matches found');
describe('SearchResults component', () => {
let wrapper;
- const findListTitle = () => wrapper.findByTestId('list-title');
+ const findSearchResultsToggle = () => wrapper.findByTestId('search-results-toggle');
+ const findCollapsibleSection = () => wrapper.findComponent(GlCollapse);
const findItemsList = () => wrapper.findComponent(ItemsList);
const findEmptyText = () => wrapper.findByTestId('empty-text');
@@ -20,6 +23,11 @@ describe('SearchResults component', () => {
noResultsText,
...props,
},
+ stubs: {
+ GlCollapse: stubComponent(GlCollapse, {
+ props: ['visible'],
+ }),
+ },
});
};
@@ -29,7 +37,11 @@ describe('SearchResults component', () => {
});
it("renders the list's title", () => {
- expect(findListTitle().text()).toBe(title);
+ expect(findSearchResultsToggle().text()).toBe(title);
+ });
+
+ it('is expanded', () => {
+ expect(findCollapsibleSection().props('visible')).toBe(true);
});
it('renders the empty text', () => {
diff --git a/spec/frontend/super_sidebar/components/sidebar_menu_spec.js b/spec/frontend/super_sidebar/components/sidebar_menu_spec.js
new file mode 100644
index 00000000000..26b146f0c8b
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/sidebar_menu_spec.js
@@ -0,0 +1,151 @@
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import SidebarMenu from '~/super_sidebar/components/sidebar_menu.vue';
+import { PANELS_WITH_PINS } from '~/super_sidebar/constants';
+import { sidebarData } from '../mock_data';
+
+describe('SidebarMenu component', () => {
+ let wrapper;
+
+ const createWrapper = (mockData) => {
+ wrapper = mountExtended(SidebarMenu, {
+ propsData: {
+ items: mockData.current_menu_items,
+ pinnedItemIds: mockData.pinned_items,
+ panelType: mockData.panel_type,
+ updatePinsUrl: mockData.update_pins_url,
+ },
+ });
+ };
+
+ describe('computed', () => {
+ const menuItems = [
+ { id: 1, title: 'No subitems' },
+ { id: 2, title: 'With subitems', items: [{ id: 21, title: 'Pinned subitem' }] },
+ { id: 3, title: 'Empty subitems array', items: [] },
+ { id: 4, title: 'Also with subitems', items: [{ id: 41, title: 'Subitem' }] },
+ ];
+
+ describe('supportsPins', () => {
+ it('is true for the project sidebar', () => {
+ createWrapper({ ...sidebarData, panel_type: 'project' });
+ expect(wrapper.vm.supportsPins).toBe(true);
+ });
+
+ it('is true for the group sidebar', () => {
+ createWrapper({ ...sidebarData, panel_type: 'group' });
+ expect(wrapper.vm.supportsPins).toBe(true);
+ });
+
+ it('is false for any other sidebar', () => {
+ createWrapper({ ...sidebarData, panel_type: 'your_work' });
+ expect(wrapper.vm.supportsPins).toEqual(false);
+ });
+ });
+
+ describe('flatPinnableItems', () => {
+ it('returns all subitems in a flat array', () => {
+ createWrapper({ ...sidebarData, current_menu_items: menuItems });
+ expect(wrapper.vm.flatPinnableItems).toEqual([
+ { id: 21, title: 'Pinned subitem' },
+ { id: 41, title: 'Subitem' },
+ ]);
+ });
+ });
+
+ describe('staticItems', () => {
+ describe('when the sidebar supports pins', () => {
+ beforeEach(() => {
+ createWrapper({
+ ...sidebarData,
+ current_menu_items: menuItems,
+ panel_type: PANELS_WITH_PINS[0],
+ });
+ });
+
+ it('makes everything that has no subitems a static item', () => {
+ expect(wrapper.vm.staticItems.map((i) => i.title)).toEqual([
+ 'No subitems',
+ 'Empty subitems array',
+ ]);
+ });
+ });
+
+ describe('when the sidebar does not support pins', () => {
+ beforeEach(() => {
+ createWrapper({
+ ...sidebarData,
+ current_menu_items: menuItems,
+ panel_type: 'explore',
+ });
+ });
+
+ it('returns an empty array', () => {
+ expect(wrapper.vm.staticItems.map((i) => i.title)).toEqual([]);
+ });
+ });
+ });
+
+ describe('nonStaticItems', () => {
+ describe('when the sidebar supports pins', () => {
+ beforeEach(() => {
+ createWrapper({
+ ...sidebarData,
+ current_menu_items: menuItems,
+ panel_type: PANELS_WITH_PINS[0],
+ });
+ });
+
+ it('keeps items that have subitems (aka "sections") as non-static', () => {
+ expect(wrapper.vm.nonStaticItems.map((i) => i.title)).toEqual([
+ 'With subitems',
+ 'Also with subitems',
+ ]);
+ });
+ });
+
+ describe('when the sidebar does not support pins', () => {
+ beforeEach(() => {
+ createWrapper({
+ ...sidebarData,
+ current_menu_items: menuItems,
+ panel_type: 'explore',
+ });
+ });
+
+ it('keeps all items as non-static', () => {
+ expect(wrapper.vm.nonStaticItems).toEqual(menuItems);
+ });
+ });
+ });
+
+ describe('pinnedItems', () => {
+ describe('when user has no pinned item ids stored', () => {
+ beforeEach(() => {
+ createWrapper({
+ ...sidebarData,
+ current_menu_items: menuItems,
+ pinned_items: [],
+ });
+ });
+
+ it('returns an empty array', () => {
+ expect(wrapper.vm.pinnedItems).toEqual([]);
+ });
+ });
+
+ describe('when user has some pinned item ids stored', () => {
+ beforeEach(() => {
+ createWrapper({
+ ...sidebarData,
+ current_menu_items: menuItems,
+ pinned_items: [21],
+ });
+ });
+
+ it('returns the items matching the pinned ids', () => {
+ expect(wrapper.vm.pinnedItems).toEqual([{ id: 21, title: 'Pinned subitem' }]);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/super_sidebar_spec.js b/spec/frontend/super_sidebar/components/super_sidebar_spec.js
index 32921da23aa..85f2a63943d 100644
--- a/spec/frontend/super_sidebar/components/super_sidebar_spec.js
+++ b/spec/frontend/super_sidebar/components/super_sidebar_spec.js
@@ -1,44 +1,57 @@
+import { nextTick } from 'vue';
+import { GlCollapse } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import SuperSidebar from '~/super_sidebar/components/super_sidebar.vue';
import HelpCenter from '~/super_sidebar/components/help_center.vue';
import UserBar from '~/super_sidebar/components/user_bar.vue';
import SidebarPortalTarget from '~/super_sidebar/components/sidebar_portal_target.vue';
-import { isCollapsed } from '~/super_sidebar/super_sidebar_collapsed_state_manager';
+import ContextSwitcher from '~/super_sidebar/components/context_switcher.vue';
+import {
+ SUPER_SIDEBAR_PEEK_OPEN_DELAY,
+ SUPER_SIDEBAR_PEEK_CLOSE_DELAY,
+} from '~/super_sidebar/constants';
+import { stubComponent } from 'helpers/stub_component';
import { sidebarData } from '../mock_data';
-jest.mock('~/super_sidebar/super_sidebar_collapsed_state_manager', () => ({
- isCollapsed: jest.fn(),
-}));
+const focusInputMock = jest.fn();
describe('SuperSidebar component', () => {
let wrapper;
- const findSidebar = () => wrapper.find('.super-sidebar');
+ const findSidebar = () => wrapper.findByTestId('super-sidebar');
+ const findHoverArea = () => wrapper.findByTestId('super-sidebar-hover-area');
const findUserBar = () => wrapper.findComponent(UserBar);
const findHelpCenter = () => wrapper.findComponent(HelpCenter);
const findSidebarPortalTarget = () => wrapper.findComponent(SidebarPortalTarget);
- const createWrapper = (props = {}) => {
+ const createWrapper = ({ props = {}, provide = {}, sidebarState = {} } = {}) => {
wrapper = shallowMountExtended(SuperSidebar, {
+ data() {
+ return {
+ ...sidebarState,
+ };
+ },
propsData: {
sidebarData,
...props,
},
+ stubs: {
+ ContextSwitcher: stubComponent(ContextSwitcher, {
+ methods: { focusInput: focusInputMock },
+ }),
+ },
+ provide,
});
};
describe('default', () => {
- it('add aria-hidden and inert attributes when collapsed', () => {
- isCollapsed.mockReturnValue(true);
- createWrapper();
- expect(findSidebar().attributes('aria-hidden')).toBe('true');
+ it('adds inert attribute when collapsed', () => {
+ createWrapper({ sidebarState: { isCollapsed: true } });
expect(findSidebar().attributes('inert')).toBe('inert');
});
- it('does not add aria-hidden and inert attributes when expanded', () => {
- isCollapsed.mockReturnValue(false);
+ it('does not add inert attribute when expanded', () => {
createWrapper();
- expect(findSidebar().attributes('aria-hidden')).toBe('false');
expect(findSidebar().attributes('inert')).toBe(undefined);
});
@@ -56,5 +69,120 @@ describe('SuperSidebar component', () => {
createWrapper();
expect(findSidebarPortalTarget().exists()).toBe(true);
});
+
+ it("does not call the context switcher's focusInput method initially", () => {
+ createWrapper();
+
+ expect(focusInputMock).not.toHaveBeenCalled();
+ });
+
+ it('renders hidden shortcut links', () => {
+ createWrapper();
+ const [linkAttrs] = sidebarData.shortcut_links;
+ const link = wrapper.find(`.${linkAttrs.css_class}`);
+
+ expect(link.exists()).toBe(true);
+ expect(link.attributes('href')).toBe(linkAttrs.href);
+ expect(link.attributes('class')).toContain('gl-display-none');
+ });
+ });
+
+ describe('when peeking on hover', () => {
+ const peekClass = 'super-sidebar-peek';
+
+ it('updates inert attribute and peek class', async () => {
+ createWrapper({
+ provide: { glFeatures: { superSidebarPeek: true } },
+ sidebarState: { isCollapsed: true },
+ });
+
+ findHoverArea().trigger('mouseenter');
+
+ jest.advanceTimersByTime(SUPER_SIDEBAR_PEEK_OPEN_DELAY - 1);
+ await nextTick();
+
+ // Not quite enough time has elapsed yet for sidebar to open
+ expect(findSidebar().classes()).not.toContain(peekClass);
+ expect(findSidebar().attributes('inert')).toBe('inert');
+
+ jest.advanceTimersByTime(1);
+ await nextTick();
+
+ // Exactly enough time has elapsed to open
+ expect(findSidebar().classes()).toContain(peekClass);
+ expect(findSidebar().attributes('inert')).toBe(undefined);
+
+ // Important: assume the cursor enters the sidebar
+ findSidebar().trigger('mouseenter');
+
+ jest.runAllTimers();
+ await nextTick();
+
+ // Sidebar remains peeked open indefinitely without a mouseleave
+ expect(findSidebar().classes()).toContain(peekClass);
+ expect(findSidebar().attributes('inert')).toBe(undefined);
+
+ findSidebar().trigger('mouseleave');
+
+ jest.advanceTimersByTime(SUPER_SIDEBAR_PEEK_CLOSE_DELAY - 1);
+ await nextTick();
+
+ // Not quite enough time has elapsed yet for sidebar to hide
+ expect(findSidebar().classes()).toContain(peekClass);
+ expect(findSidebar().attributes('inert')).toBe(undefined);
+
+ jest.advanceTimersByTime(1);
+ await nextTick();
+
+ // Exactly enough time has elapsed for sidebar to hide
+ expect(findSidebar().classes()).not.toContain('super-sidebar-peek');
+ expect(findSidebar().attributes('inert')).toBe('inert');
+ });
+
+ it('eventually closes the sidebar if cursor never enters sidebar', async () => {
+ createWrapper({
+ provide: { glFeatures: { superSidebarPeek: true } },
+ sidebarState: { isCollapsed: true },
+ });
+
+ findHoverArea().trigger('mouseenter');
+
+ jest.advanceTimersByTime(SUPER_SIDEBAR_PEEK_OPEN_DELAY);
+ await nextTick();
+
+ // Sidebar is now open
+ expect(findSidebar().classes()).toContain(peekClass);
+ expect(findSidebar().attributes('inert')).toBe(undefined);
+
+ // Important: do *not* fire a mouseenter event on the sidebar here. This
+ // imitates what happens if the cursor moves away from the sidebar before
+ // it actually appears.
+
+ jest.advanceTimersByTime(SUPER_SIDEBAR_PEEK_CLOSE_DELAY - 1);
+ await nextTick();
+
+ // Not quite enough time has elapsed yet for sidebar to hide
+ expect(findSidebar().classes()).toContain(peekClass);
+ expect(findSidebar().attributes('inert')).toBe(undefined);
+
+ jest.advanceTimersByTime(1);
+ await nextTick();
+
+ // Exactly enough time has elapsed for sidebar to hide
+ expect(findSidebar().classes()).not.toContain('super-sidebar-peek');
+ expect(findSidebar().attributes('inert')).toBe('inert');
+ });
+ });
+
+ describe('when opening the context switcher', () => {
+ beforeEach(() => {
+ createWrapper();
+ wrapper.findComponent(GlCollapse).vm.$emit('input', true);
+ wrapper.findComponent(GlCollapse).vm.$emit('shown');
+ });
+
+ it("calls the context switcher's focusInput method", () => {
+ expect(focusInputMock).toHaveBeenCalledTimes(1);
+ });
});
});
diff --git a/spec/frontend/super_sidebar/components/super_sidebar_toggle_spec.js b/spec/frontend/super_sidebar/components/super_sidebar_toggle_spec.js
new file mode 100644
index 00000000000..b9f94e662fe
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/super_sidebar_toggle_spec.js
@@ -0,0 +1,106 @@
+import { nextTick } from 'vue';
+import { GlButton } from '@gitlab/ui';
+import { __ } from '~/locale';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { JS_TOGGLE_COLLAPSE_CLASS, JS_TOGGLE_EXPAND_CLASS } from '~/super_sidebar/constants';
+import SuperSidebarToggle from '~/super_sidebar/components/super_sidebar_toggle.vue';
+import { toggleSuperSidebarCollapsed } from '~/super_sidebar/super_sidebar_collapsed_state_manager';
+
+jest.mock('~/super_sidebar/super_sidebar_collapsed_state_manager.js', () => ({
+ toggleSuperSidebarCollapsed: jest.fn(),
+}));
+
+describe('SuperSidebarToggle component', () => {
+ let wrapper;
+
+ const findButton = () => wrapper.findComponent(GlButton);
+ const getTooltip = () => getBinding(wrapper.element, 'gl-tooltip').value;
+
+ const createWrapper = ({ props = {}, sidebarState = {} } = {}) => {
+ wrapper = shallowMountExtended(SuperSidebarToggle, {
+ data() {
+ return {
+ ...sidebarState,
+ };
+ },
+ directives: {
+ GlTooltip: createMockDirective('gl-tooltip'),
+ },
+ propsData: {
+ ...props,
+ },
+ });
+ };
+
+ describe('attributes', () => {
+ it('has aria-controls attribute', () => {
+ createWrapper();
+ expect(findButton().attributes('aria-controls')).toBe('super-sidebar');
+ });
+
+ it('has aria-expanded as true when expanded', () => {
+ createWrapper();
+ expect(findButton().attributes('aria-expanded')).toBe('true');
+ });
+
+ it('has aria-expanded as false when collapsed', () => {
+ createWrapper({ sidebarState: { isCollapsed: true } });
+ expect(findButton().attributes('aria-expanded')).toBe('false');
+ });
+
+ it('has aria-label attribute', () => {
+ createWrapper();
+ expect(findButton().attributes('aria-label')).toBe(__('Navigation sidebar'));
+ });
+
+ it('is disabled when isPeek is true', () => {
+ createWrapper({ sidebarState: { isPeek: true } });
+ expect(findButton().attributes('disabled')).toBe('true');
+ });
+ });
+
+ describe('toolip', () => {
+ it('displays collapse when expanded', () => {
+ createWrapper();
+ expect(getTooltip().title).toBe(__('Collapse sidebar'));
+ });
+
+ it('displays expand when collapsed', () => {
+ createWrapper({ sidebarState: { isCollapsed: true } });
+ expect(getTooltip().title).toBe(__('Expand sidebar'));
+ });
+ });
+
+ describe('toggle', () => {
+ beforeEach(() => {
+ setHTMLFixture(`
+ <button class="${JS_TOGGLE_COLLAPSE_CLASS}">Collapse</button>
+ <button class="${JS_TOGGLE_EXPAND_CLASS}">Expand</button>
+ `);
+ });
+
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
+ it('collapses the sidebar and focuses the other toggle', async () => {
+ createWrapper();
+ findButton().vm.$emit('click');
+ await nextTick();
+ expect(toggleSuperSidebarCollapsed).toHaveBeenCalledWith(true, true);
+ expect(document.activeElement).toEqual(
+ document.querySelector(`.${JS_TOGGLE_COLLAPSE_CLASS}`),
+ );
+ });
+
+ it('expands the sidebar and focuses the other toggle', async () => {
+ createWrapper({ sidebarState: { isCollapsed: true } });
+ findButton().vm.$emit('click');
+ await nextTick();
+ expect(toggleSuperSidebarCollapsed).toHaveBeenCalledWith(false, true);
+ expect(document.activeElement).toEqual(document.querySelector(`.${JS_TOGGLE_EXPAND_CLASS}`));
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/user_bar_spec.js b/spec/frontend/super_sidebar/components/user_bar_spec.js
index ae15dd55644..2b75fb27972 100644
--- a/spec/frontend/super_sidebar/components/user_bar_spec.js
+++ b/spec/frontend/super_sidebar/components/user_bar_spec.js
@@ -1,29 +1,65 @@
import { GlBadge } from '@gitlab/ui';
+import Vuex from 'vuex';
+import Vue, { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { __ } from '~/locale';
import CreateMenu from '~/super_sidebar/components/create_menu.vue';
+import SearchModal from '~/super_sidebar/components/global_search/components/global_search.vue';
import MergeRequestMenu from '~/super_sidebar/components/merge_request_menu.vue';
import Counter from '~/super_sidebar/components/counter.vue';
import UserBar from '~/super_sidebar/components/user_bar.vue';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import waitForPromises from 'helpers/wait_for_promises';
+import { highCountTrim } from '~/lib/utils/text_utility';
import { sidebarData } from '../mock_data';
+import { MOCK_DEFAULT_SEARCH_OPTIONS } from './global_search/mock_data';
+
+jest.mock('~/lib/utils/text_utility', () => ({
+ highCountTrim: jest.fn().mockReturnValue('99+'),
+}));
describe('UserBar component', () => {
let wrapper;
const findCreateMenu = () => wrapper.findComponent(CreateMenu);
const findCounter = (at) => wrapper.findAllComponents(Counter).at(at);
+ const findIssuesCounter = () => findCounter(0);
+ const findMRsCounter = () => findCounter(1);
+ const findTodosCounter = () => findCounter(2);
const findMergeRequestMenu = () => wrapper.findComponent(MergeRequestMenu);
const findBrandLogo = () => wrapper.findByTestId('brand-header-custom-logo');
+ const findCollapseButton = () => wrapper.findByTestId('super-sidebar-collapse-button');
+ const findSearchButton = () => wrapper.findByTestId('super-sidebar-search-button');
+ const findSearchModal = () => wrapper.findComponent(SearchModal);
+ const findStopImpersonationButton = () => wrapper.findByTestId('stop-impersonation-btn');
+
+ Vue.use(Vuex);
- const createWrapper = (extraSidebarData = {}) => {
+ const store = new Vuex.Store({
+ getters: {
+ searchOptions: () => MOCK_DEFAULT_SEARCH_OPTIONS,
+ },
+ });
+ const createWrapper = ({
+ hasCollapseButton = true,
+ extraSidebarData = {},
+ provideOverrides = {},
+ } = {}) => {
wrapper = shallowMountExtended(UserBar, {
propsData: {
+ hasCollapseButton,
sidebarData: { ...sidebarData, ...extraSidebarData },
},
provide: {
rootPath: '/',
toggleNewNavEndpoint: '/-/profile/preferences',
+ isImpersonating: false,
+ ...provideOverrides,
},
+ directives: {
+ GlTooltip: createMockDirective('gl-tooltip'),
+ },
+ store,
});
};
@@ -41,32 +77,69 @@ describe('UserBar component', () => {
});
it('renders issues counter', () => {
- expect(findCounter(0).props('count')).toBe(sidebarData.assigned_open_issues_count);
- expect(findCounter(0).props('href')).toBe(sidebarData.issues_dashboard_path);
- expect(findCounter(0).props('label')).toBe(__('Issues'));
+ const isuesCounter = findIssuesCounter();
+ expect(isuesCounter.props('count')).toBe(sidebarData.assigned_open_issues_count);
+ expect(isuesCounter.props('href')).toBe(sidebarData.issues_dashboard_path);
+ expect(isuesCounter.props('label')).toBe(__('Issues'));
+ expect(isuesCounter.attributes('data-track-action')).toBe('click_link');
+ expect(isuesCounter.attributes('data-track-label')).toBe('issues_link');
+ expect(isuesCounter.attributes('data-track-property')).toBe('nav_core_menu');
+ expect(isuesCounter.attributes('class')).toContain('dashboard-shortcuts-issues');
});
it('renders merge requests counter', () => {
- expect(findCounter(1).props('count')).toBe(sidebarData.total_merge_requests_count);
- expect(findCounter(1).props('label')).toBe(__('Merge requests'));
+ const mrsCounter = findMRsCounter();
+ expect(mrsCounter.props('count')).toBe(sidebarData.total_merge_requests_count);
+ expect(mrsCounter.props('label')).toBe(__('Merge requests'));
+ expect(mrsCounter.attributes('data-track-action')).toBe('click_dropdown');
+ expect(mrsCounter.attributes('data-track-label')).toBe('merge_requests_menu');
+ expect(mrsCounter.attributes('data-track-property')).toBe('nav_core_menu');
});
- it('renders todos counter', () => {
- expect(findCounter(2).props('count')).toBe(sidebarData.todos_pending_count);
- expect(findCounter(2).props('href')).toBe('/dashboard/todos');
- expect(findCounter(2).props('label')).toBe(__('To-Do list'));
+ describe('Todos counter', () => {
+ it('renders it', () => {
+ const todosCounter = findTodosCounter();
+ expect(todosCounter.props('href')).toBe('/dashboard/todos');
+ expect(todosCounter.props('label')).toBe(__('To-Do list'));
+ expect(todosCounter.attributes('data-track-action')).toBe('click_link');
+ expect(todosCounter.attributes('data-track-label')).toBe('todos_link');
+ expect(todosCounter.attributes('data-track-property')).toBe('nav_core_menu');
+ expect(todosCounter.attributes('class')).toContain('shortcuts-todos');
+ });
+
+ it('should format and update todo counter when event is emitted', async () => {
+ createWrapper();
+ const count = 100;
+ document.dispatchEvent(new CustomEvent('todo:toggle', { detail: { count } }));
+ await nextTick();
+ expect(highCountTrim).toHaveBeenCalledWith(count);
+ expect(findTodosCounter().props('count')).toBe('99+');
+ });
});
it('renders branding logo', () => {
expect(findBrandLogo().exists()).toBe(true);
expect(findBrandLogo().attributes('src')).toBe(sidebarData.logo_url);
});
+
+ it('does not render the "Stop impersonating" button', () => {
+ expect(findStopImpersonationButton().exists()).toBe(false);
+ });
+
+ it('renders collapse button when hasCollapseButton is true', () => {
+ expect(findCollapseButton().exists()).toBe(true);
+ });
+
+ it('does not render collapse button when hasCollapseButton is false', () => {
+ createWrapper({ hasCollapseButton: false });
+ expect(findCollapseButton().exists()).toBe(false);
+ });
});
describe('GitLab Next badge', () => {
describe('when on canary', () => {
it('should render a badge to switch off GitLab Next', () => {
- createWrapper({ gitlab_com_and_canary: true });
+ createWrapper({ extraSidebarData: { gitlab_com_and_canary: true } });
const badge = wrapper.findComponent(GlBadge);
expect(badge.text()).toBe('Next');
expect(badge.attributes('href')).toBe(sidebarData.canary_toggle_com_url);
@@ -75,10 +148,55 @@ describe('UserBar component', () => {
describe('when not on canary', () => {
it('should not render the GitLab Next badge', () => {
- createWrapper({ gitlab_com_and_canary: false });
+ createWrapper({ extraSidebarData: { gitlab_com_and_canary: false } });
const badge = wrapper.findComponent(GlBadge);
expect(badge.exists()).toBe(false);
});
});
});
+
+ describe('Search', () => {
+ beforeEach(async () => {
+ createWrapper();
+ await waitForPromises();
+ });
+
+ it('should render search button', () => {
+ expect(findSearchButton().exists()).toBe(true);
+ });
+
+ it('search button should have tooltip', () => {
+ const tooltip = getBinding(findSearchButton().element, 'gl-tooltip');
+ expect(tooltip.value).toBe(`Search GitLab <kbd>/</kbd>`);
+ });
+
+ it('should render search modal', () => {
+ expect(findSearchModal().exists()).toBe(true);
+ });
+ });
+
+ describe('While impersonating a user', () => {
+ beforeEach(() => {
+ createWrapper({ provideOverrides: { isImpersonating: true } });
+ });
+
+ it('renders the "Stop impersonating" button', () => {
+ expect(findStopImpersonationButton().exists()).toBe(true);
+ });
+
+ it('sets the correct label on the button', () => {
+ const btn = findStopImpersonationButton();
+ const label = __('Stop impersonating');
+
+ expect(btn.attributes('title')).toBe(label);
+ expect(btn.attributes('aria-label')).toBe(label);
+ });
+
+ it('sets the href and data-method attributes', () => {
+ const btn = findStopImpersonationButton();
+
+ expect(btn.attributes('href')).toBe(sidebarData.stop_impersonation_path);
+ expect(btn.attributes('data-method')).toBe('delete');
+ });
+ });
});
diff --git a/spec/frontend/super_sidebar/components/user_menu_spec.js b/spec/frontend/super_sidebar/components/user_menu_spec.js
index b6231e03722..995095d0e35 100644
--- a/spec/frontend/super_sidebar/components/user_menu_spec.js
+++ b/spec/frontend/super_sidebar/components/user_menu_spec.js
@@ -14,7 +14,8 @@ describe('UserMenu component', () => {
const GlEmoji = { template: '<img/>' };
const toggleNewNavEndpoint = invalidUrl;
- const showDropdown = () => wrapper.findComponent(GlDisclosureDropdown).vm.$emit('shown');
+ const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
+ const showDropdown = () => findDropdown().vm.$emit('shown');
const createWrapper = (userDataChanges = {}) => {
wrapper = mountExtended(UserMenu, {
@@ -36,6 +37,14 @@ describe('UserMenu component', () => {
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
};
+ it('passes popper options to the dropdown', () => {
+ createWrapper();
+
+ expect(findDropdown().props('popperOptions')).toEqual({
+ modifiers: [{ name: 'offset', options: { offset: [-211, 4] } }],
+ });
+ });
+
describe('Toggle button', () => {
let toggle;
@@ -93,6 +102,14 @@ describe('UserMenu component', () => {
expect(item.find('.js-set-status-modal-trigger').exists()).toBe(true);
});
+ it('should close the dropdown when status modal opened', () => {
+ setItem({ can_update: true });
+ wrapper.vm.$refs.userDropdown.close = jest.fn();
+ expect(wrapper.vm.$refs.userDropdown.close).not.toHaveBeenCalled();
+ item.vm.$emit('action');
+ expect(wrapper.vm.$refs.userDropdown.close).toHaveBeenCalled();
+ });
+
describe('renders correct label', () => {
it.each`
busy | customized | label
@@ -117,22 +134,42 @@ describe('UserMenu component', () => {
expect(findModalWrapper().exists()).toBe(true);
});
- it('sets default data attributes when status is not customized', () => {
- setItem({ can_update: true });
- expect(findModalWrapper().attributes()).toMatchObject({
- 'data-current-emoji': '',
- 'data-current-message': '',
- 'data-default-emoji': 'speech_balloon',
+ describe('when user cannot update status', () => {
+ it('sets default data attributes', () => {
+ setItem({ can_update: true });
+ expect(findModalWrapper().attributes()).toMatchObject({
+ 'data-current-emoji': '',
+ 'data-current-message': '',
+ 'data-default-emoji': 'speech_balloon',
+ });
});
});
- it('sets user status as data attributes when status is customized', () => {
- setItem({ can_update: true, customized: true });
- expect(findModalWrapper().attributes()).toMatchObject({
- 'data-current-emoji': userMenuMockStatus.emoji,
- 'data-current-message': userMenuMockStatus.message,
- 'data-current-availability': userMenuMockStatus.availability,
- 'data-current-clear-status-after': userMenuMockStatus.clear_after,
+ describe.each`
+ busy | customized
+ ${true} | ${true}
+ ${true} | ${false}
+ ${false} | ${true}
+ ${false} | ${false}
+ `(`when user can update status`, ({ busy, customized }) => {
+ it(`and ${busy ? 'is busy' : 'is not busy'} and status ${
+ customized ? 'is' : 'is not'
+ } customized sets user status data attributes`, () => {
+ setItem({ can_update: true, busy, customized });
+ if (busy || customized) {
+ expect(findModalWrapper().attributes()).toMatchObject({
+ 'data-current-emoji': userMenuMockStatus.emoji,
+ 'data-current-message': userMenuMockStatus.message,
+ 'data-current-availability': userMenuMockStatus.availability,
+ 'data-current-clear-status-after': userMenuMockStatus.clear_after,
+ });
+ } else {
+ expect(findModalWrapper().attributes()).toMatchObject({
+ 'data-current-emoji': '',
+ 'data-current-message': '',
+ 'data-default-emoji': 'speech_balloon',
+ });
+ }
});
});
});
@@ -143,7 +180,7 @@ describe('UserMenu component', () => {
let item;
const setItem = ({ has_start_trial } = {}) => {
- createWrapper({ trial: { has_start_trial } });
+ createWrapper({ trial: { has_start_trial, url: '' } });
item = wrapper.findByTestId('start-trial-item');
};
@@ -160,6 +197,15 @@ describe('UserMenu component', () => {
expect(item.exists()).toBe(true);
});
});
+
+ it('has Snowplow tracking attributes', () => {
+ setItem({ has_start_trial: true });
+ expect(item.find('a').attributes()).toMatchObject({
+ 'data-track-property': 'nav_user_menu',
+ 'data-track-action': 'click_link',
+ 'data-track-label': 'start_trial',
+ });
+ });
});
describe('Buy Pipeline Minutes item', () => {
@@ -202,17 +248,30 @@ describe('UserMenu component', () => {
expect(item.exists()).toBe(true);
});
- it('tracks the Sentry event', () => {
- setItem({ show_buy_pipeline_minutes: true });
- showDropdown();
- expect(trackingSpy).toHaveBeenCalledWith(
- undefined,
- userMenuMockPipelineMinutes.tracking_attrs['track-action'],
- {
- label: userMenuMockPipelineMinutes.tracking_attrs['track-label'],
- property: userMenuMockPipelineMinutes.tracking_attrs['track-property'],
- },
- );
+ describe('Snowplow tracking attributes to track item click', () => {
+ beforeEach(() => {
+ setItem({ show_buy_pipeline_minutes: true });
+ });
+
+ it('has attributes to track item click in scope of new nav', () => {
+ expect(item.find('a').attributes()).toMatchObject({
+ 'data-track-property': 'nav_user_menu',
+ 'data-track-action': 'click_link',
+ 'data-track-label': 'buy_pipeline_minutes',
+ });
+ });
+
+ it('tracks the click on the item', () => {
+ item.vm.$emit('action');
+ expect(trackingSpy).toHaveBeenCalledWith(
+ undefined,
+ userMenuMockPipelineMinutes.tracking_attrs['track-action'],
+ {
+ label: userMenuMockPipelineMinutes.tracking_attrs['track-label'],
+ property: userMenuMockPipelineMinutes.tracking_attrs['track-property'],
+ },
+ );
+ });
});
describe('Callout & notification dot', () => {
@@ -292,33 +351,71 @@ describe('UserMenu component', () => {
});
describe('Edit profile item', () => {
- it('should render a link to the profile page', () => {
+ let item;
+
+ beforeEach(() => {
createWrapper();
- const item = wrapper.findByTestId('edit-profile-item');
+ item = wrapper.findByTestId('edit-profile-item');
+ });
+
+ it('should render a link to the profile page', () => {
expect(item.text()).toBe(UserMenu.i18n.editProfile);
expect(item.find('a').attributes('href')).toBe(userMenuMockData.settings.profile_path);
});
+
+ it('has Snowplow tracking attributes', () => {
+ expect(item.find('a').attributes()).toMatchObject({
+ 'data-track-property': 'nav_user_menu',
+ 'data-track-action': 'click_link',
+ 'data-track-label': 'user_edit_profile',
+ });
+ });
});
describe('Preferences item', () => {
- it('should render a link to the profile page', () => {
+ let item;
+
+ beforeEach(() => {
createWrapper();
- const item = wrapper.findByTestId('preferences-item');
+ item = wrapper.findByTestId('preferences-item');
+ });
+
+ it('should render a link to the profile page', () => {
expect(item.text()).toBe(UserMenu.i18n.preferences);
expect(item.find('a').attributes('href')).toBe(
userMenuMockData.settings.profile_preferences_path,
);
});
+
+ it('has Snowplow tracking attributes', () => {
+ expect(item.find('a').attributes()).toMatchObject({
+ 'data-track-property': 'nav_user_menu',
+ 'data-track-action': 'click_link',
+ 'data-track-label': 'user_preferences',
+ });
+ });
});
describe('GitLab Next item', () => {
describe('on gitlab.com', () => {
- it('should render a link to switch to GitLab Next', () => {
+ let item;
+
+ beforeEach(() => {
createWrapper({ gitlab_com_but_not_canary: true });
- const item = wrapper.findByTestId('gitlab-next-item');
+ item = wrapper.findByTestId('gitlab-next-item');
+ });
+ it('should render a link to switch to GitLab Next', () => {
expect(item.text()).toBe(UserMenu.i18n.gitlabNext);
expect(item.find('a').attributes('href')).toBe(userMenuMockData.canary_toggle_com_url);
});
+
+ it('has Snowplow tracking attributes', () => {
+ expect(item.find('a').attributes()).toMatchObject({
+ 'data-track-property': 'nav_user_menu',
+ 'data-track-action': 'click_link',
+ 'data-track-label': 'switch_to_canary',
+ });
+ });
});
describe('anywhere else', () => {
@@ -340,10 +437,23 @@ describe('UserMenu component', () => {
});
describe('Feedback item', () => {
- it('should render feedback item with a link to a new GitLab issue', () => {
+ let item;
+
+ beforeEach(() => {
createWrapper();
- const feedbackItem = wrapper.findByTestId('feedback-item');
- expect(feedbackItem.find('a').attributes('href')).toBe(UserMenu.feedbackUrl);
+ item = wrapper.findByTestId('feedback-item');
+ });
+
+ it('should render feedback item with a link to a new GitLab issue', () => {
+ expect(item.find('a').attributes('href')).toBe(UserMenu.feedbackUrl);
+ });
+
+ it('has Snowplow tracking attributes', () => {
+ expect(item.find('a').attributes()).toMatchObject({
+ 'data-track-property': 'nav_user_menu',
+ 'data-track-action': 'click_link',
+ 'data-track-label': 'provide_nav_beta_feedback',
+ });
});
});
@@ -370,6 +480,15 @@ describe('UserMenu component', () => {
);
expect(findSignOutGroup().find('a').attributes('data-method')).toBe('post');
});
+
+ it('should track Snowplow event on sign out', () => {
+ findSignOutGroup().vm.$emit('action');
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_link', {
+ label: 'user_sign_out',
+ property: 'nav_user_menu',
+ });
+ });
});
});
});
diff --git a/spec/frontend/super_sidebar/components/user_name_group_spec.js b/spec/frontend/super_sidebar/components/user_name_group_spec.js
index c06c8c218d4..6e3b18d3107 100644
--- a/spec/frontend/super_sidebar/components/user_name_group_spec.js
+++ b/spec/frontend/super_sidebar/components/user_name_group_spec.js
@@ -41,10 +41,12 @@ describe('UserNameGroup component', () => {
});
it('passes the item to the disclosure dropdown item', () => {
- expect(findGlDisclosureDropdownItem().props('item')).toEqual({
- text: userMenuMockData.name,
- href: userMenuMockData.link_to_profile,
- });
+ expect(findGlDisclosureDropdownItem().props('item')).toEqual(
+ expect.objectContaining({
+ text: userMenuMockData.name,
+ href: userMenuMockData.link_to_profile,
+ }),
+ );
});
it("renders user's name", () => {
@@ -97,4 +99,16 @@ describe('UserNameGroup component', () => {
});
});
});
+
+ describe('Tracking', () => {
+ it('sets the tracking attributes', () => {
+ expect(findGlDisclosureDropdownItem().find('a').attributes()).toEqual(
+ expect.objectContaining({
+ 'data-track-property': 'nav_user_menu',
+ 'data-track-action': 'click_link',
+ 'data-track-label': 'user_profile',
+ }),
+ );
+ });
+ });
});