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')
-rw-r--r--spec/frontend/super_sidebar/components/context_header_spec.js50
-rw-r--r--spec/frontend/super_sidebar/components/context_switcher_spec.js302
-rw-r--r--spec/frontend/super_sidebar/components/context_switcher_toggle_spec.js39
-rw-r--r--spec/frontend/super_sidebar/components/create_menu_spec.js21
-rw-r--r--spec/frontend/super_sidebar/components/flyout_menu_spec.js16
-rw-r--r--spec/frontend/super_sidebar/components/frequent_items_list_spec.js85
-rw-r--r--spec/frontend/super_sidebar/components/global_search/command_palette/__snapshots__/search_item_spec.js.snap27
-rw-r--r--spec/frontend/super_sidebar/components/global_search/command_palette/command_palette_items_spec.js42
-rw-r--r--spec/frontend/super_sidebar/components/global_search/command_palette/mock_data.js17
-rw-r--r--spec/frontend/super_sidebar/components/global_search/command_palette/utils_spec.js4
-rw-r--r--spec/frontend/super_sidebar/components/global_search/components/global_search_default_places_spec.js16
-rw-r--r--spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js63
-rw-r--r--spec/frontend/super_sidebar/components/global_search/mock_data.js44
-rw-r--r--spec/frontend/super_sidebar/components/global_search/utils_spec.js88
-rw-r--r--spec/frontend/super_sidebar/components/groups_list_spec.js90
-rw-r--r--spec/frontend/super_sidebar/components/items_list_spec.js63
-rw-r--r--spec/frontend/super_sidebar/components/menu_section_spec.js36
-rw-r--r--spec/frontend/super_sidebar/components/nav_item_spec.js97
-rw-r--r--spec/frontend/super_sidebar/components/pinned_section_spec.js29
-rw-r--r--spec/frontend/super_sidebar/components/projects_list_spec.js85
-rw-r--r--spec/frontend/super_sidebar/components/search_results_spec.js69
-rw-r--r--spec/frontend/super_sidebar/components/sidebar_hover_peek_behavior_spec.js213
-rw-r--r--spec/frontend/super_sidebar/components/sidebar_menu_spec.js69
-rw-r--r--spec/frontend/super_sidebar/components/sidebar_peek_behavior_spec.js25
-rw-r--r--spec/frontend/super_sidebar/components/super_sidebar_spec.js111
-rw-r--r--spec/frontend/super_sidebar/components/super_sidebar_toggle_spec.js23
-rw-r--r--spec/frontend/super_sidebar/components/user_bar_spec.js20
-rw-r--r--spec/frontend/super_sidebar/components/user_menu_spec.js21
-rw-r--r--spec/frontend/super_sidebar/mock_data.js46
-rw-r--r--spec/frontend/super_sidebar/mocks.js24
-rw-r--r--spec/frontend/super_sidebar/utils_spec.js78
31 files changed, 774 insertions, 1139 deletions
diff --git a/spec/frontend/super_sidebar/components/context_header_spec.js b/spec/frontend/super_sidebar/components/context_header_spec.js
deleted file mode 100644
index 943b659c997..00000000000
--- a/spec/frontend/super_sidebar/components/context_header_spec.js
+++ /dev/null
@@ -1,50 +0,0 @@
-import { GlAvatar } from '@gitlab/ui';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import ContextHeader from '~/super_sidebar/components/context_header.vue';
-
-describe('ContextHeader component', () => {
- let wrapper;
-
- const context = {
- id: 1,
- title: 'Title',
- avatar: '/path/to/avatar.png',
- };
-
- const findGlAvatar = () => wrapper.getComponent(GlAvatar);
-
- const createWrapper = (props = {}) => {
- wrapper = shallowMountExtended(ContextHeader, {
- propsData: {
- context,
- expanded: false,
- ...props,
- },
- });
- };
-
- describe('with an avatar', () => {
- it('passes the correct props to GlAvatar', () => {
- createWrapper();
- const avatar = findGlAvatar();
-
- expect(avatar.props('shape')).toBe('rect');
- expect(avatar.props('entityName')).toBe(context.title);
- expect(avatar.props('entityId')).toBe(context.id);
- expect(avatar.props('src')).toBe(context.avatar);
- });
-
- it('renders the avatar with a custom shape', () => {
- const customShape = 'circle';
- createWrapper({
- context: {
- ...context,
- avatar_shape: customShape,
- },
- });
- const avatar = findGlAvatar();
-
- expect(avatar.props('shape')).toBe(customShape);
- });
- });
-});
diff --git a/spec/frontend/super_sidebar/components/context_switcher_spec.js b/spec/frontend/super_sidebar/components/context_switcher_spec.js
deleted file mode 100644
index dd8f39e7cb7..00000000000
--- a/spec/frontend/super_sidebar/components/context_switcher_spec.js
+++ /dev/null
@@ -1,302 +0,0 @@
-import Vue, { nextTick } from 'vue';
-import VueApollo from 'vue-apollo';
-import { GlDisclosureDropdown, 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 ContextSwitcherToggle from '~/super_sidebar/components/context_switcher_toggle.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';
-import searchUserProjectsAndGroupsQuery from '~/super_sidebar/graphql/queries/search_user_groups_and_projects.query.graphql';
-import { trackContextAccess, formatContextSwitcherItems } from '~/super_sidebar/utils';
-import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
-import waitForPromises from 'helpers/wait_for_promises';
-import { stubComponent } from 'helpers/stub_component';
-import { contextSwitcherLinks, searchUserProjectsAndGroupsResponseMock } from '../mock_data';
-
-jest.mock('~/super_sidebar/utils', () => ({
- getStorageKeyFor: jest.requireActual('~/super_sidebar/utils').getStorageKeyFor,
- getTopFrequentItems: jest.requireActual('~/super_sidebar/utils').getTopFrequentItems,
- formatContextSwitcherItems: jest.requireActual('~/super_sidebar/utils')
- .formatContextSwitcherItems,
- trackContextAccess: jest.fn(),
-}));
-const focusInputMock = jest.fn();
-
-const username = 'root';
-const projectsPath = 'projectsPath';
-const groupsPath = 'groupsPath';
-const contextHeader = { avatar_shape: 'circle' };
-
-Vue.use(VueApollo);
-
-describe('ContextSwitcher component', () => {
- let wrapper;
- let mockApollo;
-
- const findDisclosureDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
- const findContextSwitcherToggle = () => wrapper.findComponent(ContextSwitcherToggle);
- 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');
- await nextTick();
- jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
- return waitForPromises();
- };
-
- const searchUserProjectsAndGroupsHandlerSuccess = jest
- .fn()
- .mockResolvedValue(searchUserProjectsAndGroupsResponseMock);
-
- const createWrapper = ({ props = {}, requestHandlers = {} } = {}) => {
- mockApollo = createMockApollo([
- [
- searchUserProjectsAndGroupsQuery,
- requestHandlers.searchUserProjectsAndGroupsQueryHandler ??
- searchUserProjectsAndGroupsHandlerSuccess,
- ],
- ]);
-
- wrapper = shallowMountExtended(ContextSwitcher, {
- apolloProvider: mockApollo,
- provide: {
- contextSwitcherLinks,
- },
- propsData: {
- username,
- projectsPath,
- groupsPath,
- contextHeader,
- ...props,
- },
- stubs: {
- GlDisclosureDropdown: stubComponent(GlDisclosureDropdown, {
- template: `
- <div>
- <slot name="toggle" />
- <slot />
- </div>
- `,
- }),
- GlSearchBoxByType: stubComponent(GlSearchBoxByType, {
- props: ['placeholder'],
- methods: { focusInput: focusInputMock },
- }),
- ProjectsList: stubComponent(ProjectsList, {
- props: ['username', 'viewAllLink', 'isSearch', 'searchResults'],
- }),
- GroupsList: stubComponent(GroupsList, {
- props: ['username', 'viewAllLink', 'isSearch', 'searchResults'],
- }),
- },
- });
- };
-
- describe('default', () => {
- beforeEach(() => {
- createWrapper();
- });
-
- it('renders the context switcher links', () => {
- const navItems = findNavItems();
- const firstNavItem = navItems.at(0);
-
- expect(navItems.length).toBe(contextSwitcherLinks.length);
- expect(firstNavItem.props('item')).toBe(contextSwitcherLinks[0]);
- expect(firstNavItem.props('linkClasses')).toEqual({
- [contextSwitcherLinks[0].link_classes]: contextSwitcherLinks[0].link_classes,
- });
- });
-
- it('passes the placeholder to the search box', () => {
- expect(findSearchBox().props('placeholder')).toBe(
- s__('Navigation|Search your projects or groups'),
- );
- });
-
- it('passes the correct props to the frequent projects list', () => {
- expect(findProjectsList().props()).toEqual({
- username,
- viewAllLink: projectsPath,
- isSearch: false,
- searchResults: [],
- });
- });
-
- it('passes the correct props to the frequent groups list', () => {
- expect(findGroupsList().props()).toEqual({
- username,
- viewAllLink: groupsPath,
- isSearch: false,
- searchResults: [],
- });
- });
-
- 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);
- });
-
- it('passes the correct props to the toggle', () => {
- expect(findContextSwitcherToggle().props('context')).toEqual(contextHeader);
- expect(findContextSwitcherToggle().props('expanded')).toEqual(false);
- });
-
- it('does not emit the `toggle` event initially', () => {
- expect(wrapper.emitted('toggle')).toBe(undefined);
- });
- });
-
- describe('visibility changes', () => {
- beforeEach(() => {
- createWrapper();
- findDisclosureDropdown().vm.$emit('shown');
- });
-
- it('emits the `toggle` event, focuses the search input and puts the toggle in the expanded state when opened', () => {
- expect(wrapper.emitted('toggle')).toHaveLength(1);
- expect(wrapper.emitted('toggle')[0]).toEqual([true]);
- expect(focusInputMock).toHaveBeenCalledTimes(1);
- expect(findContextSwitcherToggle().props('expanded')).toBe(true);
- });
-
- it("emits the `toggle` event, does not attempt to focus the input, and resets the toggle's `expanded` props to `false` when closed", async () => {
- findDisclosureDropdown().vm.$emit('hidden');
- await nextTick();
-
- expect(wrapper.emitted('toggle')).toHaveLength(2);
- expect(wrapper.emitted('toggle')[1]).toEqual([false]);
- expect(focusInputMock).toHaveBeenCalledTimes(1);
- expect(findContextSwitcherToggle().props('expanded')).toBe(false);
- });
- });
-
- describe('item access tracking', () => {
- it('does not track anything if not within a trackable context', () => {
- createWrapper();
-
- expect(trackContextAccess).not.toHaveBeenCalled();
- });
-
- it('tracks item access if within a trackable context', () => {
- const currentContext = { namespace: 'groups' };
- createWrapper({
- props: {
- currentContext,
- },
- });
-
- expect(trackContextAccess).toHaveBeenCalledWith(username, currentContext);
- });
- });
-
- describe('on search', () => {
- beforeEach(() => {
- createWrapper();
- 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(
- formatContextSwitcherItems(searchUserProjectsAndGroupsResponseMock.data.projects.nodes),
- );
- });
-
- it('passes the groups to the frequent groups list', () => {
- expect(findGroupsList().props('isSearch')).toBe(true);
- expect(findGroupsList().props('searchResults')).toEqual(
- formatContextSwitcherItems(searchUserProjectsAndGroupsResponseMock.data.user.groups.nodes),
- );
- });
- });
-
- describe('when search query does not match any items', () => {
- beforeEach(() => {
- createWrapper({
- requestHandlers: {
- searchUserProjectsAndGroupsQueryHandler: jest.fn().mockResolvedValue({
- data: {
- projects: {
- nodes: [],
- },
- user: {
- id: '1',
- groups: {
- nodes: [],
- },
- },
- },
- }),
- },
- });
- return triggerSearchQuery();
- });
-
- it('passes empty results to the lists', () => {
- expect(findProjectsList().props('isSearch')).toBe(true);
- expect(findProjectsList().props('searchResults')).toEqual([]);
- expect(findGroupsList().props('isSearch')).toBe(true);
- expect(findGroupsList().props('searchResults')).toEqual([]);
- });
- });
-
- describe('when search query fails', () => {
- beforeEach(() => {
- jest.spyOn(Sentry, 'captureException');
- });
-
- it('captures exception and shows an alert if response is formatted incorrectly', async () => {
- createWrapper({
- requestHandlers: {
- searchUserProjectsAndGroupsQueryHandler: jest.fn().mockResolvedValue({
- data: {},
- }),
- },
- });
- await triggerSearchQuery();
-
- expect(Sentry.captureException).toHaveBeenCalled();
- expect(findAlert().exists()).toBe(true);
- });
-
- it('captures exception and shows an alert if query fails', async () => {
- createWrapper({
- requestHandlers: {
- searchUserProjectsAndGroupsQueryHandler: jest.fn().mockRejectedValue(),
- },
- });
- await triggerSearchQuery();
-
- expect(Sentry.captureException).toHaveBeenCalled();
- expect(findAlert().exists()).toBe(true);
- });
- });
-});
diff --git a/spec/frontend/super_sidebar/components/context_switcher_toggle_spec.js b/spec/frontend/super_sidebar/components/context_switcher_toggle_spec.js
deleted file mode 100644
index c20d3c2745f..00000000000
--- a/spec/frontend/super_sidebar/components/context_switcher_toggle_spec.js
+++ /dev/null
@@ -1,39 +0,0 @@
-import { GlIcon } from '@gitlab/ui';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import ContextSwitcherToggle from '~/super_sidebar/components/context_switcher_toggle.vue';
-
-describe('ContextSwitcherToggle component', () => {
- let wrapper;
-
- const context = {
- id: 1,
- title: 'Title',
- avatar: '/path/to/avatar.png',
- };
-
- const findGlIcon = () => wrapper.getComponent(GlIcon);
-
- const createWrapper = (props = {}) => {
- wrapper = shallowMountExtended(ContextSwitcherToggle, {
- propsData: {
- context,
- expanded: false,
- ...props,
- },
- });
- };
-
- it('renders "chevron-down" icon when not expanded', () => {
- createWrapper();
-
- expect(findGlIcon().props('name')).toBe('chevron-down');
- });
-
- it('renders "chevron-up" icon when expanded', () => {
- createWrapper({
- expanded: true,
- });
-
- expect(findGlIcon().props('name')).toBe('chevron-up');
- });
-});
diff --git a/spec/frontend/super_sidebar/components/create_menu_spec.js b/spec/frontend/super_sidebar/components/create_menu_spec.js
index 510a3f5b913..b967fb18a39 100644
--- a/spec/frontend/super_sidebar/components/create_menu_spec.js
+++ b/spec/frontend/super_sidebar/components/create_menu_spec.js
@@ -1,7 +1,6 @@
import { nextTick } from 'vue';
import {
GlDisclosureDropdown,
- GlTooltip,
GlDisclosureDropdownGroup,
GlDisclosureDropdownItem,
} from '@gitlab/ui';
@@ -9,6 +8,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
import { __ } from '~/locale';
import CreateMenu from '~/super_sidebar/components/create_menu.vue';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { createNewMenuGroups } from '../mock_data';
describe('CreateMenu component', () => {
@@ -18,7 +18,6 @@ describe('CreateMenu component', () => {
const findGlDisclosureDropdownGroups = () => wrapper.findAllComponents(GlDisclosureDropdownGroup);
const findGlDisclosureDropdownItems = () => wrapper.findAllComponents(GlDisclosureDropdownItem);
const findInviteMembersTrigger = () => wrapper.findComponent(InviteMembersTrigger);
- const findGlTooltip = () => wrapper.findComponent(GlTooltip);
const createWrapper = ({ provide = {} } = {}) => {
wrapper = shallowMountExtended(CreateMenu, {
@@ -33,6 +32,9 @@ describe('CreateMenu component', () => {
InviteMembersTrigger,
GlDisclosureDropdown,
},
+ directives: {
+ GlTooltip: createMockDirective('gl-tooltip'),
+ },
});
};
@@ -45,7 +47,7 @@ describe('CreateMenu component', () => {
createWrapper();
expect(findGlDisclosureDropdown().props('dropdownOffset')).toEqual({
- crossAxis: -147,
+ crossAxis: -179,
mainAxis: 4,
});
});
@@ -74,16 +76,12 @@ describe('CreateMenu component', () => {
expect(findInviteMembersTrigger().exists()).toBe(true);
});
- it("sets the toggle ID and tooltip's target", () => {
- 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);
+ const tooltip = getBinding(findGlDisclosureDropdown().element, 'gl-tooltip');
+ expect(tooltip.value).toBe('');
});
it('shows the tooltip when the dropdown is closed', async () => {
@@ -91,7 +89,8 @@ describe('CreateMenu component', () => {
findGlDisclosureDropdown().vm.$emit('hidden');
await nextTick();
- expect(findGlTooltip().exists()).toBe(true);
+ const tooltip = getBinding(findGlDisclosureDropdown().element, 'gl-tooltip');
+ expect(tooltip.value).toBe('Create new...');
});
});
@@ -99,7 +98,7 @@ describe('CreateMenu component', () => {
createWrapper({ provide: { isImpersonating: true } });
expect(findGlDisclosureDropdown().props('dropdownOffset')).toEqual({
- crossAxis: -115,
+ crossAxis: -147,
mainAxis: 4,
});
});
diff --git a/spec/frontend/super_sidebar/components/flyout_menu_spec.js b/spec/frontend/super_sidebar/components/flyout_menu_spec.js
index b894d29c875..bf24de870d9 100644
--- a/spec/frontend/super_sidebar/components/flyout_menu_spec.js
+++ b/spec/frontend/super_sidebar/components/flyout_menu_spec.js
@@ -1,16 +1,26 @@
-import { shallowMount } from '@vue/test-utils';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import FlyoutMenu from '~/super_sidebar/components/flyout_menu.vue';
jest.mock('@floating-ui/dom');
describe('FlyoutMenu', () => {
let wrapper;
+ let dummySection;
const createComponent = () => {
- wrapper = shallowMount(FlyoutMenu, {
+ dummySection = document.createElement('section');
+ dummySection.addEventListener = jest.fn();
+
+ dummySection.getBoundingClientRect = jest.fn();
+ dummySection.getBoundingClientRect.mockReturnValue({ top: 0, bottom: 5, width: 10 });
+
+ document.querySelector = jest.fn();
+ document.querySelector.mockReturnValue(dummySection);
+
+ wrapper = mountExtended(FlyoutMenu, {
propsData: {
targetId: 'section-1',
- items: [],
+ items: [{ id: 1, title: 'item 1', link: 'https://example.com' }],
},
});
};
diff --git a/spec/frontend/super_sidebar/components/frequent_items_list_spec.js b/spec/frontend/super_sidebar/components/frequent_items_list_spec.js
deleted file mode 100644
index 63dd941974a..00000000000
--- a/spec/frontend/super_sidebar/components/frequent_items_list_spec.js
+++ /dev/null
@@ -1,85 +0,0 @@
-import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
-import { s__ } from '~/locale';
-import FrequentItemsList from '~/super_sidebar/components//frequent_items_list.vue';
-import ItemsList from '~/super_sidebar/components/items_list.vue';
-import { useLocalStorageSpy } from 'helpers/local_storage_helper';
-import { cachedFrequentProjects } from '../mock_data';
-
-const title = s__('Navigation|FREQUENT PROJECTS');
-const pristineText = s__('Navigation|Projects you visit often will appear here.');
-const storageKey = 'storageKey';
-const maxItems = 5;
-
-describe('FrequentItemsList component', () => {
- useLocalStorageSpy();
-
- let wrapper;
-
- const findListTitle = () => wrapper.findByTestId('list-title');
- const findItemsList = () => wrapper.findComponent(ItemsList);
- const findEmptyText = () => wrapper.findByTestId('empty-text');
- const findRemoveItemButton = () => wrapper.findByTestId('item-remove');
-
- const createWrapperFactory = (mountFn = shallowMountExtended) => () => {
- wrapper = mountFn(FrequentItemsList, {
- propsData: {
- title,
- pristineText,
- storageKey,
- maxItems,
- },
- });
- };
- const createWrapper = createWrapperFactory();
- const createFullWrapper = createWrapperFactory(mountExtended);
-
- describe('default', () => {
- beforeEach(() => {
- createWrapper();
- });
-
- it("renders the list's title", () => {
- expect(findListTitle().text()).toBe(title);
- });
-
- it('renders the empty text', () => {
- expect(findEmptyText().exists()).toBe(true);
- expect(findEmptyText().text()).toBe(pristineText);
- });
- });
-
- describe('when there are cached frequent items', () => {
- beforeEach(() => {
- window.localStorage.setItem(storageKey, cachedFrequentProjects);
- createWrapper();
- });
-
- it('attempts to retrieve the items from the local storage', () => {
- expect(window.localStorage.getItem).toHaveBeenCalledTimes(1);
- expect(window.localStorage.getItem).toHaveBeenCalledWith(storageKey);
- });
-
- it('renders the maximum amount of items', () => {
- expect(findItemsList().props('items').length).toBe(maxItems);
- });
-
- it('does not render the empty text slot', () => {
- expect(findEmptyText().exists()).toBe(false);
- });
- });
-
- describe('items editing', () => {
- beforeEach(() => {
- window.localStorage.setItem(storageKey, cachedFrequentProjects);
- createFullWrapper();
- });
-
- it('remove-item event emission from items-list causes list item to be removed', async () => {
- const localStorageProjects = findItemsList().props('items');
- await findRemoveItemButton().trigger('click');
-
- 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/command_palette/__snapshots__/search_item_spec.js.snap b/spec/frontend/super_sidebar/components/global_search/command_palette/__snapshots__/search_item_spec.js.snap
index d16d137db2f..e6635672ccf 100644
--- a/spec/frontend/super_sidebar/components/global_search/command_palette/__snapshots__/search_item_spec.js.snap
+++ b/spec/frontend/super_sidebar/components/global_search/command_palette/__snapshots__/search_item_spec.js.snap
@@ -2,7 +2,7 @@
exports[`SearchItem should render the item 1`] = `
<div
- class="gl-display-flex gl-align-items-center"
+ class="gl-align-items-center gl-display-flex"
>
<gl-avatar-stub
alt="avatar"
@@ -14,33 +14,25 @@ exports[`SearchItem should render the item 1`] = `
size="16"
src="https://www.gravatar.com/avatar/a9638f4ec70148d51e56bf05ad41e993?s=80&d=identicon"
/>
-
- <!---->
-
<span
class="gl-display-flex gl-flex-direction-column"
>
<span
class="gl-text-gray-900"
/>
-
- <!---->
</span>
</div>
`;
exports[`SearchItem should render the item 2`] = `
<div
- class="gl-display-flex gl-align-items-center"
+ class="gl-align-items-center gl-display-flex"
>
- <!---->
-
<gl-icon-stub
class="gl-mr-3"
name="users"
size="16"
/>
-
<span
class="gl-display-flex gl-flex-direction-column"
>
@@ -49,15 +41,13 @@ exports[`SearchItem should render the item 2`] = `
>
Manage &gt; Activity
</span>
-
- <!---->
</span>
</div>
`;
exports[`SearchItem should render the item 3`] = `
<div
- class="gl-display-flex gl-align-items-center"
+ class="gl-align-items-center gl-display-flex"
>
<gl-avatar-stub
alt="avatar"
@@ -69,9 +59,6 @@ exports[`SearchItem should render the item 3`] = `
size="32"
src="/project/avatar/1/avatar.png"
/>
-
- <!---->
-
<span
class="gl-display-flex gl-flex-direction-column"
>
@@ -80,7 +67,6 @@ exports[`SearchItem should render the item 3`] = `
>
MockProject1
</span>
-
<span
class="gl-font-sm gl-text-gray-500"
>
@@ -92,7 +78,7 @@ exports[`SearchItem should render the item 3`] = `
exports[`SearchItem should render the item 4`] = `
<div
- class="gl-display-flex gl-align-items-center"
+ class="gl-align-items-center gl-display-flex"
>
<gl-avatar-stub
alt="avatar"
@@ -104,9 +90,6 @@ exports[`SearchItem should render the item 4`] = `
size="16"
src=""
/>
-
- <!---->
-
<span
class="gl-display-flex gl-flex-direction-column"
>
@@ -115,8 +98,6 @@ exports[`SearchItem should render the item 4`] = `
>
Dismiss Cipher with no integrity
</span>
-
- <!---->
</span>
</div>
`;
diff --git a/spec/frontend/super_sidebar/components/global_search/command_palette/command_palette_items_spec.js b/spec/frontend/super_sidebar/components/global_search/command_palette/command_palette_items_spec.js
index 85eb7e2e241..7d85dbcbdd3 100644
--- a/spec/frontend/super_sidebar/components/global_search/command_palette/command_palette_items_spec.js
+++ b/spec/frontend/super_sidebar/components/global_search/command_palette/command_palette_items_spec.js
@@ -9,6 +9,7 @@ import {
PATH_GROUP_TITLE,
USER_HANDLE,
PATH_HANDLE,
+ PROJECT_HANDLE,
SEARCH_SCOPE,
MAX_ROWS,
} from '~/super_sidebar/components/global_search/command_palette/constants';
@@ -20,6 +21,7 @@ import {
import { getFormattedItem } from '~/super_sidebar/components/global_search/utils';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import { mockTracking } from 'helpers/tracking_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { COMMANDS, LINKS, USERS, FILES } from './mock_data';
@@ -32,7 +34,7 @@ describe('CommandPaletteItems', () => {
const projectFilesPath = 'project/files/path';
const projectBlobPath = '/blob/main';
- const createComponent = (props) => {
+ const createComponent = (props, options = {}) => {
wrapper = shallowMount(CommandPaletteItems, {
propsData: {
handle: COMMAND_HANDLE,
@@ -51,6 +53,7 @@ describe('CommandPaletteItems', () => {
projectFilesPath,
projectBlobPath,
},
+ ...options,
});
};
@@ -227,4 +230,41 @@ describe('CommandPaletteItems', () => {
expect(axios.get).toHaveBeenCalledTimes(1);
});
});
+
+ describe('Tracking', () => {
+ let trackingSpy;
+ let mockAxios;
+
+ beforeEach(() => {
+ trackingSpy = mockTracking(undefined, undefined, jest.spyOn);
+ mockAxios = new MockAdapter(axios);
+ createComponent({ attachTo: document.body });
+ });
+
+ afterEach(() => {
+ mockAxios.restore();
+ });
+
+ it('tracks event immediately', () => {
+ expect(trackingSpy).toHaveBeenCalledTimes(1);
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'activate_command_palette', {
+ label: 'command',
+ });
+ });
+
+ it.each`
+ handle | label
+ ${USER_HANDLE} | ${'user'}
+ ${PROJECT_HANDLE} | ${'project'}
+ ${PATH_HANDLE} | ${'path'}
+ `('tracks changing the handle to "$handle"', async ({ handle, label }) => {
+ trackingSpy.mockClear();
+
+ await wrapper.setProps({ handle });
+ expect(trackingSpy).toHaveBeenCalledTimes(1);
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'activate_command_palette', {
+ label,
+ });
+ });
+ });
});
diff --git a/spec/frontend/super_sidebar/components/global_search/command_palette/mock_data.js b/spec/frontend/super_sidebar/components/global_search/command_palette/mock_data.js
index d01e5c85741..25a23433b1e 100644
--- a/spec/frontend/super_sidebar/components/global_search/command_palette/mock_data.js
+++ b/spec/frontend/super_sidebar/components/global_search/command_palette/mock_data.js
@@ -69,24 +69,41 @@ export const TRANSFORMED_LINKS = [
icon: 'users',
keywords: 'Manage',
text: 'Manage',
+ extraAttrs: {
+ 'data-track-action': 'click_command_palette_item',
+ 'data-track-label': 'item_without_id',
+ 'data-track-extra': '{"title":"Manage"}',
+ },
},
{
href: '/flightjs/Flight/activity',
icon: 'users',
keywords: 'Activity',
text: 'Manage > Activity',
+ extraAttrs: {
+ 'data-track-action': 'click_command_palette_item',
+ 'data-track-label': 'activity',
+ },
},
{
href: '/flightjs/Flight/-/project_members',
icon: 'users',
keywords: 'Members',
text: 'Manage > Members',
+ extraAttrs: {
+ 'data-track-action': 'click_command_palette_item',
+ 'data-track-label': 'members',
+ },
},
{
href: '/flightjs/Flight/-/labels',
icon: 'users',
keywords: 'Labels',
text: 'Manage > Labels',
+ extraAttrs: {
+ 'data-track-action': 'click_command_palette_item',
+ 'data-track-label': 'labels',
+ },
},
];
diff --git a/spec/frontend/super_sidebar/components/global_search/command_palette/utils_spec.js b/spec/frontend/super_sidebar/components/global_search/command_palette/utils_spec.js
index ebc52e2d910..76768bd8da9 100644
--- a/spec/frontend/super_sidebar/components/global_search/command_palette/utils_spec.js
+++ b/spec/frontend/super_sidebar/components/global_search/command_palette/utils_spec.js
@@ -26,6 +26,10 @@ describe('fileMapper', () => {
icon: 'doc-code',
text: file,
href: `${projectBlobPath}/${file}`,
+ extraAttrs: {
+ 'data-track-action': 'click_command_palette_item',
+ 'data-track-label': 'file',
+ },
});
});
});
diff --git a/spec/frontend/super_sidebar/components/global_search/components/global_search_default_places_spec.js b/spec/frontend/super_sidebar/components/global_search/components/global_search_default_places_spec.js
index c6126a348f5..f91c8034fe9 100644
--- a/spec/frontend/super_sidebar/components/global_search/components/global_search_default_places_spec.js
+++ b/spec/frontend/super_sidebar/components/global_search/components/global_search_default_places_spec.js
@@ -67,10 +67,26 @@ describe('GlobalSearchDefaultPlaces', () => {
{
text: 'Explore',
href: '/explore',
+ extraAttrs: {
+ 'data-track-action': 'click_command_palette_item',
+ 'data-track-extra': '{"title":"Explore"}',
+ 'data-track-label': 'item_without_id',
+ 'data-track-property': 'nav_panel_unknown',
+ 'data-testid': 'places-item-link',
+ 'data-qa-places-item': 'Explore',
+ },
},
{
text: 'Admin area',
href: '/admin',
+ extraAttrs: {
+ 'data-track-action': 'click_command_palette_item',
+ 'data-track-extra': '{"title":"Admin area"}',
+ 'data-track-label': 'item_without_id',
+ 'data-track-property': 'nav_panel_unknown',
+ 'data-testid': 'places-item-link',
+ 'data-qa-places-item': 'Admin area',
+ },
},
]);
});
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 f9a6690a391..038c7a96adc 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
@@ -23,7 +23,6 @@ import {
ICON_SUBGROUP,
SCOPE_TOKEN_MAX_LENGTH,
} from '~/super_sidebar/components/global_search/constants';
-import { SEARCH_GITLAB } from '~/vue_shared/global_search/constants';
import { truncate } from '~/lib/utils/text_utility';
import { visitUrl } from '~/lib/utils/url_utility';
import { ENTER_KEY } from '~/lib/utils/keys';
@@ -52,7 +51,7 @@ describe('GlobalSearchModal', () => {
clearAutocomplete: jest.fn(),
};
- const deafaultMockState = {
+ const defaultMockState = {
searchContext: {
project: MOCK_PROJECT,
group: MOCK_GROUP,
@@ -66,15 +65,14 @@ describe('GlobalSearchModal', () => {
};
const createComponent = ({
- initialState = deafaultMockState,
+ initialState = defaultMockState,
mockGetters = defaultMockGetters,
stubs,
- glFeatures = { commandPalette: false },
...mountOptions
} = {}) => {
const store = new Vuex.Store({
state: {
- ...deafaultMockState,
+ ...defaultMockState,
...initialState,
},
actions: actionSpies,
@@ -89,7 +87,6 @@ describe('GlobalSearchModal', () => {
wrapper = shallowMountExtended(GlobalSearchModal, {
store,
stubs,
- provide: { glFeatures },
...mountOptions,
});
};
@@ -271,49 +268,28 @@ describe('GlobalSearchModal', () => {
});
describe('Command palette', () => {
- describe('when FF `command_palette` is disabled', () => {
+ describe.each([...COMMON_HANDLES, PATH_HANDLE])('when search handle is %s', (handle) => {
beforeEach(() => {
- createComponent();
+ createComponent({
+ initialState: { search: handle },
+ });
});
- it('should not render command mode components', () => {
- expect(findCommandPaletteItems().exists()).toBe(false);
- expect(findFakeSearchInput().exists()).toBe(false);
+ it('should render command mode components', () => {
+ expect(findCommandPaletteItems().exists()).toBe(true);
+ expect(findFakeSearchInput().exists()).toBe(true);
});
- it('should provide default placeholder to the search input', () => {
- expect(findGlobalSearchInput().attributes('placeholder')).toBe(SEARCH_GITLAB);
+ it('should provide an alternative placeholder to the search input', () => {
+ expect(findGlobalSearchInput().attributes('placeholder')).toBe(
+ SEARCH_OR_COMMAND_MODE_PLACEHOLDER,
+ );
});
- });
-
- describe.each([...COMMON_HANDLES, PATH_HANDLE])(
- 'when FF `command_palette` is enabled and search handle is %s',
- (handle) => {
- beforeEach(() => {
- createComponent({
- initialState: { search: handle },
- glFeatures: {
- commandPalette: true,
- },
- });
- });
- it('should render command mode components', () => {
- expect(findCommandPaletteItems().exists()).toBe(true);
- expect(findFakeSearchInput().exists()).toBe(true);
- });
-
- it('should provide an alternative placeholder to the search input', () => {
- expect(findGlobalSearchInput().attributes('placeholder')).toBe(
- SEARCH_OR_COMMAND_MODE_PLACEHOLDER,
- );
- });
-
- it('should not render the scope token', () => {
- expect(findScopeToken().exists()).toBe(false);
- });
- },
- );
+ it('should not render the scope token', () => {
+ expect(findScopeToken().exists()).toBe(false);
+ });
+ });
});
});
@@ -373,9 +349,6 @@ describe('GlobalSearchModal', () => {
beforeEach(() => {
createComponent({
initialState: { search: '>' },
- glFeatures: {
- commandPalette: true,
- },
});
submitSearch();
});
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 dfa8b458844..61ddfb6cae1 100644
--- a/spec/frontend/super_sidebar/components/global_search/mock_data.js
+++ b/spec/frontend/super_sidebar/components/global_search/mock_data.js
@@ -109,6 +109,10 @@ export const MOCK_SCOPED_SEARCH_OPTIONS_DEF = [
scopeCategory: PROJECTS_CATEGORY,
icon: ICON_PROJECT,
href: MOCK_PROJECT.path,
+ extraAttrs: {
+ 'data-track-action': 'click_command_palette_item',
+ 'data-track-label': 'scoped_in_project',
+ },
},
{
text: 'scoped-in-group',
@@ -116,11 +120,19 @@ export const MOCK_SCOPED_SEARCH_OPTIONS_DEF = [
scopeCategory: GROUPS_CATEGORY,
icon: ICON_GROUP,
href: MOCK_GROUP.path,
+ extraAttrs: {
+ 'data-track-action': 'click_command_palette_item',
+ 'data-track-label': 'scoped_in_group',
+ },
},
{
text: 'scoped-in-all',
description: MSG_IN_ALL_GITLAB,
href: MOCK_ALL_PATH,
+ extraAttrs: {
+ 'data-track-action': 'click_command_palette_item',
+ 'data-track-label': 'scoped_in_all',
+ },
},
];
export const MOCK_SCOPED_SEARCH_OPTIONS = [
@@ -263,6 +275,10 @@ export const MOCK_GROUPED_AUTOCOMPLETE_OPTIONS = [
avatar_size: 32,
entity_id: 1,
entity_name: 'MockGroup1',
+ extraAttrs: {
+ 'data-track-action': 'click_command_palette_item',
+ 'data-track-label': 'groups',
+ },
},
],
},
@@ -281,6 +297,10 @@ export const MOCK_GROUPED_AUTOCOMPLETE_OPTIONS = [
avatar_size: 32,
entity_id: 1,
entity_name: 'MockProject1',
+ extraAttrs: {
+ 'data-track-action': 'click_command_palette_item',
+ 'data-track-label': 'projects',
+ },
},
{
category: 'Projects',
@@ -294,6 +314,10 @@ export const MOCK_GROUPED_AUTOCOMPLETE_OPTIONS = [
avatar_size: 32,
entity_id: 2,
entity_name: 'MockProject2',
+ extraAttrs: {
+ 'data-track-action': 'click_command_palette_item',
+ 'data-track-label': 'projects',
+ },
},
],
},
@@ -307,6 +331,10 @@ export const MOCK_GROUPED_AUTOCOMPLETE_OPTIONS = [
href: 'help/gitlab',
avatar_size: 16,
entity_name: 'GitLab Help',
+ extraAttrs: {
+ 'data-track-action': 'click_command_palette_item',
+ 'data-track-label': 'help',
+ },
},
],
},
@@ -325,6 +353,10 @@ export const MOCK_SORTED_AUTOCOMPLETE_OPTIONS = [
avatar_size: 32,
entity_id: 1,
entity_name: 'MockGroup1',
+ extraAttrs: {
+ 'data-track-action': 'click_command_palette_item',
+ 'data-track-label': 'groups',
+ },
},
{
avatar_size: 32,
@@ -338,6 +370,10 @@ export const MOCK_SORTED_AUTOCOMPLETE_OPTIONS = [
namespace: 'Gitlab Org / MockProject1',
text: 'MockProject1',
value: 'MockProject1',
+ extraAttrs: {
+ 'data-track-action': 'click_command_palette_item',
+ 'data-track-label': 'projects',
+ },
},
{
avatar_size: 32,
@@ -351,6 +387,10 @@ export const MOCK_SORTED_AUTOCOMPLETE_OPTIONS = [
namespace: 'Gitlab Org / MockProject2',
text: 'MockProject2',
value: 'MockProject2',
+ extraAttrs: {
+ 'data-track-action': 'click_command_palette_item',
+ 'data-track-label': 'projects',
+ },
},
{
avatar_size: 16,
@@ -359,6 +399,10 @@ export const MOCK_SORTED_AUTOCOMPLETE_OPTIONS = [
label: 'GitLab Help',
text: 'GitLab Help',
href: 'help/gitlab',
+ extraAttrs: {
+ 'data-track-action': 'click_command_palette_item',
+ 'data-track-label': 'help',
+ },
},
];
diff --git a/spec/frontend/super_sidebar/components/global_search/utils_spec.js b/spec/frontend/super_sidebar/components/global_search/utils_spec.js
index 3b12063e733..3c30445e936 100644
--- a/spec/frontend/super_sidebar/components/global_search/utils_spec.js
+++ b/spec/frontend/super_sidebar/components/global_search/utils_spec.js
@@ -13,48 +13,58 @@ import {
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);
- });
+ item | avatarSize | searchContext | entityId | entityName | trackingLabel
+ ${{ category: PROJECTS_CATEGORY, label: 'project1' }} | ${LARGE_AVATAR_PX} | ${{ project: { id: 29 } }} | ${29} | ${'project1'} | ${'projects'}
+ ${{ category: GROUPS_CATEGORY, label: 'project1' }} | ${LARGE_AVATAR_PX} | ${{ group: { id: 12 } }} | ${12} | ${'project1'} | ${'groups'}
+ ${{ category: 'Help', label: 'project1' }} | ${SMALL_AVATAR_PX} | ${null} | ${undefined} | ${'project1'} | ${'help'}
+ ${{ category: 'Settings', label: 'project1' }} | ${SMALL_AVATAR_PX} | ${null} | ${undefined} | ${'project1'} | ${'settings'}
+ ${{ category: GROUPS_CATEGORY, value: 'group1', label: 'Group 1' }} | ${LARGE_AVATAR_PX} | ${{ group: { id: 1, name: 'test1' } }} | ${1} | ${'group1'} | ${'groups'}
+ ${{ category: PROJECTS_CATEGORY, value: 'group2', label: 'Group2' }} | ${LARGE_AVATAR_PX} | ${{ project: { id: 2, name: 'test2' } }} | ${2} | ${'group2'} | ${'projects'}
+ ${{ category: ISSUES_CATEGORY }} | ${SMALL_AVATAR_PX} | ${{ project: { id: 3, name: 'test3' } }} | ${3} | ${'test3'} | ${'recent_issues'}
+ ${{ category: MERGE_REQUEST_CATEGORY }} | ${SMALL_AVATAR_PX} | ${{ project: { id: 4, name: 'test4' } }} | ${4} | ${'test4'} | ${'recent_merge_requests'}
+ ${{ category: RECENT_EPICS_CATEGORY }} | ${SMALL_AVATAR_PX} | ${{ group: { id: 5, name: 'test5' } }} | ${5} | ${'test5'} | ${'recent_epics'}
+ ${{ category: GROUPS_CATEGORY, group_id: 6, group_name: 'test6' }} | ${LARGE_AVATAR_PX} | ${null} | ${6} | ${'test6'} | ${'groups'}
+ ${{ category: PROJECTS_CATEGORY, project_id: 7, project_name: 'test7' }} | ${LARGE_AVATAR_PX} | ${null} | ${7} | ${'test7'} | ${'projects'}
+ ${{ category: ISSUES_CATEGORY, project_id: 8, project_name: 'test8' }} | ${SMALL_AVATAR_PX} | ${null} | ${8} | ${'test8'} | ${'recent_issues'}
+ ${{ category: MERGE_REQUEST_CATEGORY, project_id: 9, project_name: 'test9' }} | ${SMALL_AVATAR_PX} | ${null} | ${9} | ${'test9'} | ${'recent_merge_requests'}
+ ${{ category: RECENT_EPICS_CATEGORY, group_id: 10, group_name: 'test10' }} | ${SMALL_AVATAR_PX} | ${null} | ${10} | ${'test10'} | ${'recent_epics'}
+ ${{ category: GROUPS_CATEGORY, group_id: 11, group_name: 'test11' }} | ${LARGE_AVATAR_PX} | ${{ group: { id: 1, name: 'test1' } }} | ${11} | ${'test11'} | ${'groups'}
+ ${{ category: PROJECTS_CATEGORY, project_id: 12, project_name: 'test12' }} | ${LARGE_AVATAR_PX} | ${{ project: { id: 2, name: 'test2' } }} | ${12} | ${'test12'} | ${'projects'}
+ ${{ category: ISSUES_CATEGORY, project_id: 13, project_name: 'test13' }} | ${SMALL_AVATAR_PX} | ${{ project: { id: 3, name: 'test3' } }} | ${13} | ${'test13'} | ${'recent_issues'}
+ ${{ category: MERGE_REQUEST_CATEGORY, project_id: 14, project_name: 'test14' }} | ${SMALL_AVATAR_PX} | ${{ project: { id: 4, name: 'test4' } }} | ${14} | ${'test14'} | ${'recent_merge_requests'}
+ ${{ category: RECENT_EPICS_CATEGORY, group_id: 15, group_name: 'test15' }} | ${SMALL_AVATAR_PX} | ${{ group: { id: 5, name: 'test5' } }} | ${15} | ${'test15'} | ${'recent_epics'}
+ `(
+ 'formats the item',
+ ({ item, avatarSize, searchContext, entityId, entityName, trackingLabel }) => {
+ 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 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 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 entityId to ${entityId}`, () => {
+ expect(formattedItem.entity_id).toBe(entityId);
+ });
+
+ it(`should set avatar entityName to ${entityName}`, () => {
+ expect(formattedItem.entity_name).toBe(entityName);
+ });
- it(`should set avatar entityName to ${entityName}`, () => {
- expect(formattedItem.entity_name).toBe(entityName);
+ it('should add tracking label', () => {
+ expect(formattedItem.extraAttrs).toEqual({
+ 'data-track-action': 'click_command_palette_item',
+ 'data-track-label': trackingLabel,
+ });
+ });
});
- });
- });
+ },
+ );
});
diff --git a/spec/frontend/super_sidebar/components/groups_list_spec.js b/spec/frontend/super_sidebar/components/groups_list_spec.js
deleted file mode 100644
index 4fa3303c12f..00000000000
--- a/spec/frontend/super_sidebar/components/groups_list_spec.js
+++ /dev/null
@@ -1,90 +0,0 @@
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { s__ } from '~/locale';
-import GroupsList from '~/super_sidebar/components/groups_list.vue';
-import SearchResults from '~/super_sidebar/components/search_results.vue';
-import FrequentItemsList from '~/super_sidebar/components/frequent_items_list.vue';
-import NavItem from '~/super_sidebar/components/nav_item.vue';
-import { MAX_FREQUENT_GROUPS_COUNT } from '~/super_sidebar/constants';
-
-const username = 'root';
-const viewAllLink = '/path/to/groups';
-const storageKey = `${username}/frequent-groups`;
-
-describe('GroupsList component', () => {
- let wrapper;
-
- const findSearchResults = () => wrapper.findComponent(SearchResults);
- const findFrequentItemsList = () => wrapper.findComponent(FrequentItemsList);
- const findViewAllLink = () => wrapper.findComponent(NavItem);
-
- const itRendersViewAllItem = () => {
- it('renders the "View all..." item', () => {
- const link = findViewAllLink();
-
- expect(link.props('item')).toEqual({
- icon: 'group',
- link: viewAllLink,
- title: s__('Navigation|View all your groups'),
- });
- expect(link.props('linkClasses')).toEqual({ 'dashboard-shortcuts-groups': true });
- });
- };
-
- const createWrapper = (props = {}) => {
- wrapper = shallowMountExtended(GroupsList, {
- propsData: {
- username,
- viewAllLink,
- ...props,
- },
- });
- };
-
- describe('when displaying search results', () => {
- const searchResults = ['A search result'];
-
- beforeEach(() => {
- createWrapper({
- isSearch: true,
- searchResults,
- });
- });
-
- it('renders the search results component', () => {
- expect(findSearchResults().exists()).toBe(true);
- expect(findFrequentItemsList().exists()).toBe(false);
- });
-
- it('passes the correct props to the search results component', () => {
- expect(findSearchResults().props()).toEqual({
- title: s__('Navigation|Groups'),
- noResultsText: s__('Navigation|No group matches found'),
- searchResults,
- });
- });
-
- itRendersViewAllItem();
- });
-
- describe('when displaying frequent groups', () => {
- beforeEach(() => {
- createWrapper();
- });
-
- it('renders the frequent items list', () => {
- expect(findFrequentItemsList().exists()).toBe(true);
- expect(findSearchResults().exists()).toBe(false);
- });
-
- it('passes the correct props to the frequent items list', () => {
- expect(findFrequentItemsList().props()).toEqual({
- title: s__('Navigation|Frequently visited groups'),
- storageKey,
- maxItems: MAX_FREQUENT_GROUPS_COUNT,
- pristineText: s__('Navigation|Groups you visit often will appear here.'),
- });
- });
-
- itRendersViewAllItem();
- });
-});
diff --git a/spec/frontend/super_sidebar/components/items_list_spec.js b/spec/frontend/super_sidebar/components/items_list_spec.js
deleted file mode 100644
index 8e00984f500..00000000000
--- a/spec/frontend/super_sidebar/components/items_list_spec.js
+++ /dev/null
@@ -1,63 +0,0 @@
-import { shallowMountExtended } 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';
-
-const mockItems = JSON.parse(cachedFrequentProjects);
-const [firstMockedProject] = mockItems;
-
-describe('ItemsList component', () => {
- let wrapper;
-
- const findNavItems = () => wrapper.findAllComponents(NavItem);
-
- const createWrapper = ({ props = {}, slots = {} } = {}) => {
- wrapper = shallowMountExtended(ItemsList, {
- propsData: {
- ...props,
- },
- slots,
- });
- };
-
- it('does not render nav items when there are no items', () => {
- createWrapper();
-
- expect(findNavItems().length).toBe(0);
- });
-
- it('renders one nav item per item', () => {
- createWrapper({
- props: {
- items: mockItems,
- },
- });
-
- expect(findNavItems().length).not.toBe(0);
- expect(findNavItems().length).toBe(mockItems.length);
- });
-
- it('passes the correct props to the nav items', () => {
- createWrapper({
- props: {
- items: mockItems,
- },
- });
- const firstNavItem = findNavItems().at(0);
-
- expect(firstNavItem.props('item')).toEqual(firstMockedProject);
- });
-
- it('renders the `view-all-items` slot', () => {
- const testId = 'view-all-items';
- createWrapper({
- slots: {
- 'view-all-items': {
- template: `<div data-testid="${testId}" />`,
- },
- },
- });
-
- expect(wrapper.findByTestId(testId).exists()).toBe(true);
- });
-});
diff --git a/spec/frontend/super_sidebar/components/menu_section_spec.js b/spec/frontend/super_sidebar/components/menu_section_spec.js
index 288e317d4c6..e76bb699301 100644
--- a/spec/frontend/super_sidebar/components/menu_section_spec.js
+++ b/spec/frontend/super_sidebar/components/menu_section_spec.js
@@ -79,39 +79,55 @@ describe('MenuSection component', () => {
});
describe('when hasFlyout is true', () => {
- it('is rendered', () => {
+ it('is not yet rendered', () => {
createWrapper({ title: 'Asdf' }, { 'has-flyout': true });
- expect(findFlyout().exists()).toBe(true);
+ expect(findFlyout().exists()).toBe(false);
});
describe('on mouse hover', () => {
describe('when section is expanded', () => {
- it('is not shown', async () => {
+ it('is not rendered', async () => {
createWrapper({ title: 'Asdf' }, { 'has-flyout': true, expanded: true });
await findButton().trigger('pointerover', { pointerType: 'mouse' });
- expect(findFlyout().isVisible()).toBe(false);
+ expect(findFlyout().exists()).toBe(false);
});
});
describe('when section is not expanded', () => {
- it('is shown', async () => {
- createWrapper({ title: 'Asdf' }, { 'has-flyout': true, expanded: false });
- await findButton().trigger('pointerover', { pointerType: 'mouse' });
- expect(findFlyout().isVisible()).toBe(true);
+ describe('when section has no items', () => {
+ it('is not rendered', async () => {
+ createWrapper({ title: 'Asdf' }, { 'has-flyout': true, expanded: false });
+ await findButton().trigger('pointerover', { pointerType: 'mouse' });
+ expect(findFlyout().exists()).toBe(false);
+ });
+ });
+
+ describe('when section has items', () => {
+ it('is rendered and shown', async () => {
+ createWrapper(
+ { title: 'Asdf', items: [{ title: 'Item1', href: '/item1' }] },
+ { 'has-flyout': true, expanded: false },
+ );
+ await findButton().trigger('pointerover', { pointerType: 'mouse' });
+ expect(findFlyout().isVisible()).toBe(true);
+ });
});
});
});
describe('when section gets closed', () => {
beforeEach(async () => {
- createWrapper({ title: 'Asdf' }, { expanded: true, 'has-flyout': true });
+ createWrapper(
+ { title: 'Asdf', items: [{ title: 'Item1', href: '/item1' }] },
+ { expanded: true, 'has-flyout': true },
+ );
await findButton().trigger('click');
await findButton().trigger('pointerover', { pointerType: 'mouse' });
});
it('shows the flyout only after section title gets hovered out and in again', async () => {
expect(findCollapse().props('visible')).toBe(false);
- expect(findFlyout().isVisible()).toBe(false);
+ expect(findFlyout().exists()).toBe(false);
await findButton().trigger('pointerleave');
await findButton().trigger('pointerover', { pointerType: 'mouse' });
diff --git a/spec/frontend/super_sidebar/components/nav_item_spec.js b/spec/frontend/super_sidebar/components/nav_item_spec.js
index f41f6954ed1..89d774c4b43 100644
--- a/spec/frontend/super_sidebar/components/nav_item_spec.js
+++ b/spec/frontend/super_sidebar/components/nav_item_spec.js
@@ -1,5 +1,6 @@
-import { GlBadge } from '@gitlab/ui';
+import { GlBadge, GlButton, GlAvatar } from '@gitlab/ui';
import { RouterLinkStub } from '@vue/test-utils';
+import { nextTick } from 'vue';
import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper';
import NavItem from '~/super_sidebar/components/nav_item.vue';
import NavItemRouterLink from '~/super_sidebar/components/nav_item_router_link.vue';
@@ -13,8 +14,10 @@ import {
describe('NavItem component', () => {
let wrapper;
+ const findAvatar = () => wrapper.findComponent(GlAvatar);
const findLink = () => wrapper.findByTestId('nav-item-link');
const findPill = () => wrapper.findComponent(GlBadge);
+ const findPinButton = () => wrapper.findComponent(GlButton);
const findNavItemRouterLink = () => extendedWrapper(wrapper.findComponent(NavItemRouterLink));
const findNavItemLink = () => extendedWrapper(wrapper.findComponent(NavItemLink));
@@ -59,6 +62,66 @@ describe('NavItem component', () => {
);
});
+ describe('pins', () => {
+ describe('when pins are not supported', () => {
+ it('does not render pin button', () => {
+ createWrapper({
+ item: { title: 'Foo' },
+ provide: {
+ panelSupportsPins: false,
+ },
+ });
+
+ expect(findPinButton().exists()).toBe(false);
+ });
+ });
+
+ describe('when pins are supported', () => {
+ beforeEach(() => {
+ createWrapper({
+ item: { title: 'Foo' },
+ provide: {
+ panelSupportsPins: true,
+ },
+ });
+ });
+
+ it('renders pin button', () => {
+ expect(findPinButton().exists()).toBe(true);
+ });
+
+ it('contains an aria-label', () => {
+ expect(findPinButton().attributes('aria-label')).toBe('Pin Foo');
+ });
+
+ it('toggles pointer events on after CSS fade-in', async () => {
+ const pinButton = findPinButton();
+
+ expect(pinButton.classes()).toContain('gl-pointer-events-none');
+
+ wrapper.trigger('mouseenter');
+ pinButton.vm.$emit('transitionend');
+ await nextTick();
+
+ expect(pinButton.classes()).not.toContain('gl-pointer-events-none');
+ });
+
+ it('does not toggle pointer events if mouse leaves before CSS fade-in ends', async () => {
+ const pinButton = findPinButton();
+
+ expect(pinButton.classes()).toContain('gl-pointer-events-none');
+
+ wrapper.trigger('mouseenter');
+ wrapper.trigger('mousemove');
+ wrapper.trigger('mouseleave');
+ pinButton.vm.$emit('transitionend');
+ await nextTick();
+
+ expect(pinButton.classes()).toContain('gl-pointer-events-none');
+ });
+ });
+ });
+
it('applies custom link classes', () => {
const customClass = 'customClass';
createWrapper({
@@ -153,4 +216,36 @@ describe('NavItem component', () => {
});
});
});
+
+ describe('when `item` prop has `entity_id` attribute', () => {
+ it('renders an avatar', () => {
+ createWrapper({
+ item: { title: 'Foo', entity_id: 123, avatar: '/avatar.png', avatar_shape: 'circle' },
+ });
+
+ expect(findAvatar().props()).toMatchObject({
+ entityId: 123,
+ shape: 'circle',
+ src: '/avatar.png',
+ });
+ });
+ });
+
+ describe('when `item.is_active` is true', () => {
+ it('scrolls into view', () => {
+ createWrapper({
+ item: { is_active: true },
+ });
+ expect(wrapper.element.scrollIntoView).toHaveBeenNthCalledWith(1, false);
+ });
+ });
+
+ describe('when `item.is_active` is false', () => {
+ it('scrolls not into view', () => {
+ createWrapper({
+ item: { is_active: false },
+ });
+ expect(wrapper.element.scrollIntoView).not.toHaveBeenCalled();
+ });
+ });
});
diff --git a/spec/frontend/super_sidebar/components/pinned_section_spec.js b/spec/frontend/super_sidebar/components/pinned_section_spec.js
index 00cc7cf29c9..fe1653f1177 100644
--- a/spec/frontend/super_sidebar/components/pinned_section_spec.js
+++ b/spec/frontend/super_sidebar/components/pinned_section_spec.js
@@ -87,4 +87,33 @@ describe('PinnedSection component', () => {
});
});
});
+
+ describe('ambiguous settings names', () => {
+ it('get renamed to be unambiguous', () => {
+ createWrapper({
+ items: [
+ { title: 'CI/CD', id: 'ci_cd' },
+ { title: 'Merge requests', id: 'merge_request_settings' },
+ { title: 'Monitor', id: 'monitor' },
+ { title: 'Repository', id: 'repository' },
+ { title: 'Repository', id: 'code' },
+ { title: 'Something else', id: 'not_a_setting' },
+ ],
+ });
+
+ expect(
+ wrapper
+ .findComponent(MenuSection)
+ .props('item')
+ .items.map((i) => i.title),
+ ).toEqual([
+ 'CI/CD settings',
+ 'Merge requests settings',
+ 'Monitor settings',
+ 'Repository settings',
+ 'Repository',
+ 'Something else',
+ ]);
+ });
+ });
});
diff --git a/spec/frontend/super_sidebar/components/projects_list_spec.js b/spec/frontend/super_sidebar/components/projects_list_spec.js
deleted file mode 100644
index 93a414e1e8c..00000000000
--- a/spec/frontend/super_sidebar/components/projects_list_spec.js
+++ /dev/null
@@ -1,85 +0,0 @@
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { s__ } from '~/locale';
-import ProjectsList from '~/super_sidebar/components/projects_list.vue';
-import SearchResults from '~/super_sidebar/components/search_results.vue';
-import FrequentItemsList from '~/super_sidebar/components/frequent_items_list.vue';
-import NavItem from '~/super_sidebar/components/nav_item.vue';
-import { MAX_FREQUENT_PROJECTS_COUNT } from '~/super_sidebar/constants';
-
-const username = 'root';
-const viewAllLink = '/path/to/projects';
-const storageKey = `${username}/frequent-projects`;
-
-describe('ProjectsList component', () => {
- let wrapper;
-
- const findSearchResults = () => wrapper.findComponent(SearchResults);
- const findFrequentItemsList = () => wrapper.findComponent(FrequentItemsList);
- const findViewAllLink = () => wrapper.findComponent(NavItem);
-
- const itRendersViewAllItem = () => {
- it('renders the "View all..." item', () => {
- const link = findViewAllLink();
-
- expect(link.props('item')).toEqual({
- icon: 'project',
- link: viewAllLink,
- title: s__('Navigation|View all your projects'),
- });
- expect(link.props('linkClasses')).toEqual({ 'dashboard-shortcuts-projects': true });
- });
- };
-
- const createWrapper = (props = {}) => {
- wrapper = shallowMountExtended(ProjectsList, {
- propsData: {
- username,
- viewAllLink,
- ...props,
- },
- });
- };
-
- describe('when displaying search results', () => {
- const searchResults = ['A search result'];
-
- beforeEach(() => {
- createWrapper({
- isSearch: true,
- searchResults,
- });
- });
-
- it('renders the search results component', () => {
- expect(findSearchResults().exists()).toBe(true);
- expect(findFrequentItemsList().exists()).toBe(false);
- });
-
- it('passes the correct props to the search results component', () => {
- expect(findSearchResults().props()).toEqual({
- title: s__('Navigation|Projects'),
- noResultsText: s__('Navigation|No project matches found'),
- searchResults,
- });
- });
-
- itRendersViewAllItem();
- });
-
- describe('when displaying frequent projects', () => {
- beforeEach(() => {
- createWrapper();
- });
-
- it('passes the correct props to the frequent items list', () => {
- expect(findFrequentItemsList().props()).toEqual({
- title: s__('Navigation|Frequently visited projects'),
- storageKey,
- maxItems: MAX_FREQUENT_PROJECTS_COUNT,
- pristineText: s__('Navigation|Projects you visit often will appear here.'),
- });
- });
-
- itRendersViewAllItem();
- });
-});
diff --git a/spec/frontend/super_sidebar/components/search_results_spec.js b/spec/frontend/super_sidebar/components/search_results_spec.js
deleted file mode 100644
index daec5c2a9b4..00000000000
--- a/spec/frontend/super_sidebar/components/search_results_spec.js
+++ /dev/null
@@ -1,69 +0,0 @@
-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');
-
-describe('SearchResults component', () => {
- let wrapper;
-
- const findSearchResultsToggle = () => wrapper.findByTestId('search-results-toggle');
- const findCollapsibleSection = () => wrapper.findComponent(GlCollapse);
- const findItemsList = () => wrapper.findComponent(ItemsList);
- const findEmptyText = () => wrapper.findByTestId('empty-text');
-
- const createWrapper = ({ props = {} } = {}) => {
- wrapper = shallowMountExtended(SearchResults, {
- propsData: {
- title,
- noResultsText,
- ...props,
- },
- stubs: {
- GlCollapse: stubComponent(GlCollapse, {
- props: ['visible'],
- }),
- },
- });
- };
-
- describe('default state', () => {
- beforeEach(() => {
- createWrapper();
- });
-
- it("renders the list's title", () => {
- expect(findSearchResultsToggle().text()).toBe(title);
- });
-
- it('is expanded', () => {
- expect(findCollapsibleSection().props('visible')).toBe(true);
- });
-
- it('renders the empty text', () => {
- expect(findEmptyText().exists()).toBe(true);
- expect(findEmptyText().text()).toBe(noResultsText);
- });
- });
-
- describe('when displaying search results', () => {
- it('shows search results', () => {
- const searchResults = [{ id: 1 }];
- createWrapper({ props: { isSearch: true, searchResults } });
-
- expect(findItemsList().props('items')[0]).toEqual(searchResults[0]);
- });
-
- it('shows the no results text if search results are empty', () => {
- const searchResults = [];
- createWrapper({ props: { isSearch: true, searchResults } });
-
- expect(findItemsList().props('items').length).toEqual(0);
- expect(findEmptyText().text()).toBe(noResultsText);
- });
- });
-});
diff --git a/spec/frontend/super_sidebar/components/sidebar_hover_peek_behavior_spec.js b/spec/frontend/super_sidebar/components/sidebar_hover_peek_behavior_spec.js
new file mode 100644
index 00000000000..75b834ee7c9
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/sidebar_hover_peek_behavior_spec.js
@@ -0,0 +1,213 @@
+import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import {
+ SUPER_SIDEBAR_PEEK_OPEN_DELAY,
+ SUPER_SIDEBAR_PEEK_CLOSE_DELAY,
+ JS_TOGGLE_EXPAND_CLASS,
+ SUPER_SIDEBAR_PEEK_STATE_CLOSED as STATE_CLOSED,
+ SUPER_SIDEBAR_PEEK_STATE_WILL_OPEN as STATE_WILL_OPEN,
+ SUPER_SIDEBAR_PEEK_STATE_OPEN as STATE_OPEN,
+ SUPER_SIDEBAR_PEEK_STATE_WILL_CLOSE as STATE_WILL_CLOSE,
+} from '~/super_sidebar/constants';
+import SidebarHoverPeek from '~/super_sidebar/components/sidebar_hover_peek_behavior.vue';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
+import { moveMouse, mouseEnter, mouseLeave, moveMouseOutOfDocument } from '../mocks';
+
+// This is measured at runtime in the browser, but statically defined here
+// since Jest does not do layout/styling.
+const X_SIDEBAR_EDGE = 10;
+
+jest.mock('~/lib/utils/css_utils', () => ({
+ getCssClassDimensions: () => ({ width: X_SIDEBAR_EDGE }),
+}));
+
+describe('SidebarHoverPeek component', () => {
+ let wrapper;
+ let toggle;
+ let trackingSpy = null;
+
+ const createComponent = (props = { isMouseOverSidebar: false }) => {
+ wrapper = mount(SidebarHoverPeek, {
+ propsData: props,
+ });
+
+ return nextTick();
+ };
+
+ const lastNChangeEvents = (n = 1) => wrapper.emitted('change').slice(-n).flat();
+
+ beforeEach(() => {
+ toggle = document.createElement('button');
+ toggle.classList.add(JS_TOGGLE_EXPAND_CLASS);
+ document.body.appendChild(toggle);
+ trackingSpy = mockTracking(undefined, undefined, jest.spyOn);
+ });
+
+ afterEach(() => {
+ unmockTracking();
+ // We destroy the wrapper ourselves as that needs to happen before the toggle is removed.
+ // eslint-disable-next-line @gitlab/vtu-no-explicit-wrapper-destroy
+ wrapper.destroy();
+ toggle?.remove();
+ });
+
+ it('begins in the closed state', async () => {
+ await createComponent();
+
+ expect(lastNChangeEvents(Infinity)).toEqual([STATE_CLOSED]);
+ });
+
+ describe('when mouse enters the toggle', () => {
+ beforeEach(async () => {
+ await createComponent();
+ mouseEnter(toggle);
+ });
+
+ it('does not emit duplicate events in a region', () => {
+ mouseEnter(toggle);
+
+ expect(lastNChangeEvents(Infinity)).toEqual([STATE_CLOSED, STATE_WILL_OPEN]);
+ });
+
+ it('transitions to will-open when hovering the toggle', () => {
+ expect(lastNChangeEvents(1)).toEqual([STATE_WILL_OPEN]);
+ });
+
+ describe('when transitioning away from the will-open state', () => {
+ beforeEach(() => {
+ jest.advanceTimersByTime(SUPER_SIDEBAR_PEEK_OPEN_DELAY - 1);
+ });
+
+ it('transitions to open after delay', () => {
+ expect(lastNChangeEvents(1)).toEqual([STATE_WILL_OPEN]);
+
+ jest.advanceTimersByTime(1);
+
+ expect(lastNChangeEvents(2)).toEqual([STATE_WILL_OPEN, STATE_OPEN]);
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'nav_hover_peek', {
+ label: 'nav_sidebar_toggle',
+ property: 'nav_sidebar',
+ });
+ });
+
+ it('cancels transition to open if mouse out of toggle', () => {
+ mouseLeave(toggle);
+ jest.runOnlyPendingTimers();
+
+ expect(lastNChangeEvents(3)).toEqual([STATE_WILL_OPEN, STATE_WILL_CLOSE, STATE_CLOSED]);
+ });
+
+ it('transitions to closed if cursor leaves document', () => {
+ moveMouseOutOfDocument();
+
+ expect(lastNChangeEvents(2)).toEqual([STATE_WILL_OPEN, STATE_CLOSED]);
+ });
+ });
+
+ describe('when transitioning away from the will-close state', () => {
+ beforeEach(() => {
+ jest.runOnlyPendingTimers();
+ moveMouse(X_SIDEBAR_EDGE);
+ jest.advanceTimersByTime(SUPER_SIDEBAR_PEEK_CLOSE_DELAY - 1);
+ });
+
+ it('transitions to closed after delay', () => {
+ expect(lastNChangeEvents(1)).toEqual([STATE_WILL_CLOSE]);
+
+ jest.advanceTimersByTime(1);
+
+ expect(lastNChangeEvents(2)).toEqual([STATE_WILL_CLOSE, STATE_CLOSED]);
+ });
+
+ it('cancels transition to close if mouse moves back to toggle', () => {
+ expect(lastNChangeEvents(1)).toEqual([STATE_WILL_CLOSE]);
+
+ mouseEnter(toggle);
+ jest.runOnlyPendingTimers();
+
+ expect(lastNChangeEvents(4)).toEqual([
+ STATE_OPEN,
+ STATE_WILL_CLOSE,
+ STATE_WILL_OPEN,
+ STATE_OPEN,
+ ]);
+ });
+ });
+
+ describe('when transitioning away from the open state', () => {
+ beforeEach(() => {
+ jest.runOnlyPendingTimers();
+ });
+
+ it('transitions to will-close if mouse out of sidebar region', () => {
+ expect(lastNChangeEvents(1)).toEqual([STATE_OPEN]);
+
+ moveMouse(X_SIDEBAR_EDGE);
+
+ expect(lastNChangeEvents(2)).toEqual([STATE_OPEN, STATE_WILL_CLOSE]);
+ });
+
+ it('transitions to will-close if cursor leaves document', () => {
+ moveMouseOutOfDocument();
+
+ expect(lastNChangeEvents(2)).toEqual([STATE_OPEN, STATE_WILL_CLOSE]);
+ });
+ });
+
+ it('cleans up its mouseleave listener before destroy', () => {
+ jest.runOnlyPendingTimers();
+
+ expect(lastNChangeEvents(1)).toEqual([STATE_OPEN]);
+
+ wrapper.destroy();
+ mouseLeave(toggle);
+
+ expect(lastNChangeEvents(1)).toEqual([STATE_OPEN]);
+ });
+
+ it('cleans up its timers before destroy', () => {
+ wrapper.destroy();
+ jest.runOnlyPendingTimers();
+
+ expect(lastNChangeEvents(1)).toEqual([STATE_WILL_OPEN]);
+ });
+
+ it('cleans up document mouseleave listener before destroy', () => {
+ mouseEnter(toggle);
+
+ wrapper.destroy();
+
+ moveMouseOutOfDocument();
+
+ expect(lastNChangeEvents(1)).not.toEqual([STATE_CLOSED]);
+ });
+ });
+
+ describe('when mouse is over sidebar child element', () => {
+ beforeEach(async () => {
+ await createComponent({ isMouseOverSidebar: true });
+ });
+
+ it('does not transition to will-close or closed when mouse is over sidebar child element', () => {
+ mouseEnter(toggle);
+ jest.runOnlyPendingTimers();
+ mouseLeave(toggle);
+
+ expect(lastNChangeEvents(1)).toEqual([STATE_OPEN]);
+ });
+ });
+
+ it('cleans up its mouseenter listener before destroy', async () => {
+ await createComponent();
+
+ mouseLeave(toggle);
+ jest.runOnlyPendingTimers();
+
+ expect(lastNChangeEvents(1)).toEqual([STATE_CLOSED]);
+
+ wrapper.destroy();
+ mouseEnter(toggle);
+
+ expect(lastNChangeEvents(1)).toEqual([STATE_CLOSED]);
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/sidebar_menu_spec.js b/spec/frontend/super_sidebar/components/sidebar_menu_spec.js
index 5d9a35fbf70..c85a6609e6f 100644
--- a/spec/frontend/super_sidebar/components/sidebar_menu_spec.js
+++ b/spec/frontend/super_sidebar/components/sidebar_menu_spec.js
@@ -16,13 +16,8 @@ const menuItems = [
describe('Sidebar Menu', () => {
let wrapper;
- let flyoutFlag = false;
-
const createWrapper = (extraProps = {}) => {
wrapper = shallowMountExtended(SidebarMenu, {
- provide: {
- glFeatures: { superSidebarFlyoutMenus: flyoutFlag },
- },
propsData: {
items: sidebarData.current_menu_items,
isLoggedIn: sidebarData.is_logged_in,
@@ -125,8 +120,11 @@ describe('Sidebar Menu', () => {
});
describe('flyout menus', () => {
- describe('when feature is disabled', () => {
+ describe('when screen width is smaller than "md" breakpoint', () => {
beforeEach(() => {
+ jest.spyOn(GlBreakpointInstance, 'windowWidth').mockImplementation(() => {
+ return 767;
+ });
createWrapper({
items: menuItems,
});
@@ -140,59 +138,27 @@ describe('Sidebar Menu', () => {
});
});
- describe('when feature is enabled', () => {
+ describe('when screen width is equal or larger than "md" breakpoint', () => {
beforeEach(() => {
- flyoutFlag = true;
- });
-
- describe('when screen width is smaller than "md" breakpoint', () => {
- beforeEach(() => {
- jest.spyOn(GlBreakpointInstance, 'windowWidth').mockImplementation(() => {
- return 767;
- });
- createWrapper({
- items: menuItems,
- });
+ jest.spyOn(GlBreakpointInstance, 'windowWidth').mockImplementation(() => {
+ return 768;
});
-
- it('does not add flyout menus to sections', () => {
- expect(findNonStaticSectionItems().wrappers.map((w) => w.props('hasFlyout'))).toEqual([
- false,
- false,
- ]);
+ createWrapper({
+ items: menuItems,
});
});
- describe('when screen width is equal or larger than "md" breakpoint', () => {
- beforeEach(() => {
- jest.spyOn(GlBreakpointInstance, 'windowWidth').mockImplementation(() => {
- return 768;
- });
- createWrapper({
- items: menuItems,
- });
- });
-
- it('adds flyout menus to sections', () => {
- expect(findNonStaticSectionItems().wrappers.map((w) => w.props('hasFlyout'))).toEqual([
- true,
- true,
- ]);
- });
+ it('adds flyout menus to sections', () => {
+ expect(findNonStaticSectionItems().wrappers.map((w) => w.props('hasFlyout'))).toEqual([
+ true,
+ true,
+ ]);
});
});
});
});
describe('Separators', () => {
- it('should add the separator above pinned section', () => {
- createWrapper({
- items: menuItems,
- panelType: 'project',
- });
- expect(findPinnedSection().props('separated')).toBe(true);
- });
-
it('should add the separator above main menu items when there is a pinned section', () => {
createWrapper({
items: menuItems,
@@ -209,11 +175,4 @@ describe('Sidebar Menu', () => {
expect(findMainMenuSeparator().exists()).toBe(false);
});
});
-
- describe('ARIA attributes', () => {
- it('adds aria-label attribute to nav element', () => {
- createWrapper();
- expect(wrapper.find('nav').attributes('aria-label')).toBe('Main navigation');
- });
- });
});
diff --git a/spec/frontend/super_sidebar/components/sidebar_peek_behavior_spec.js b/spec/frontend/super_sidebar/components/sidebar_peek_behavior_spec.js
index 94ef072a951..90a950c5f35 100644
--- a/spec/frontend/super_sidebar/components/sidebar_peek_behavior_spec.js
+++ b/spec/frontend/super_sidebar/components/sidebar_peek_behavior_spec.js
@@ -2,14 +2,14 @@ import { mount } from '@vue/test-utils';
import {
SUPER_SIDEBAR_PEEK_OPEN_DELAY,
SUPER_SIDEBAR_PEEK_CLOSE_DELAY,
+ SUPER_SIDEBAR_PEEK_STATE_CLOSED as STATE_CLOSED,
+ SUPER_SIDEBAR_PEEK_STATE_WILL_OPEN as STATE_WILL_OPEN,
+ SUPER_SIDEBAR_PEEK_STATE_OPEN as STATE_OPEN,
+ SUPER_SIDEBAR_PEEK_STATE_WILL_CLOSE as STATE_WILL_CLOSE,
} from '~/super_sidebar/constants';
-import SidebarPeek, {
- STATE_CLOSED,
- STATE_WILL_OPEN,
- STATE_OPEN,
- STATE_WILL_CLOSE,
-} from '~/super_sidebar/components/sidebar_peek_behavior.vue';
+import SidebarPeek from '~/super_sidebar/components/sidebar_peek_behavior.vue';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
+import { moveMouse, moveMouseOutOfDocument } from '../mocks';
// These are measured at runtime in the browser, but statically defined here
// since Jest does not do layout/styling.
@@ -41,19 +41,6 @@ describe('SidebarPeek component', () => {
});
};
- const moveMouse = (clientX) => {
- const event = new MouseEvent('mousemove', {
- clientX,
- });
-
- document.dispatchEvent(event);
- };
-
- const moveMouseOutOfDocument = () => {
- const event = new MouseEvent('mouseleave');
- document.documentElement.dispatchEvent(event);
- };
-
const lastNChangeEvents = (n = 1) => wrapper.emitted('change').slice(-n).flat();
beforeEach(() => {
diff --git a/spec/frontend/super_sidebar/components/super_sidebar_spec.js b/spec/frontend/super_sidebar/components/super_sidebar_spec.js
index 7b7b8a7be13..1371f8f00a7 100644
--- a/spec/frontend/super_sidebar/components/super_sidebar_spec.js
+++ b/spec/frontend/super_sidebar/components/super_sidebar_spec.js
@@ -4,29 +4,32 @@ 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 SidebarPeekBehavior, {
- STATE_CLOSED,
- STATE_WILL_OPEN,
- STATE_OPEN,
- STATE_WILL_CLOSE,
-} from '~/super_sidebar/components/sidebar_peek_behavior.vue';
+import SidebarPeekBehavior from '~/super_sidebar/components/sidebar_peek_behavior.vue';
+import SidebarHoverPeekBehavior from '~/super_sidebar/components/sidebar_hover_peek_behavior.vue';
import SidebarPortalTarget from '~/super_sidebar/components/sidebar_portal_target.vue';
-import ContextHeader from '~/super_sidebar/components/context_header.vue';
-import ContextSwitcher from '~/super_sidebar/components/context_switcher.vue';
import SidebarMenu from '~/super_sidebar/components/sidebar_menu.vue';
-import { sidebarState } from '~/super_sidebar/constants';
+import {
+ sidebarState,
+ SUPER_SIDEBAR_PEEK_STATE_CLOSED as STATE_CLOSED,
+ SUPER_SIDEBAR_PEEK_STATE_WILL_OPEN as STATE_WILL_OPEN,
+ SUPER_SIDEBAR_PEEK_STATE_OPEN as STATE_OPEN,
+ SUPER_SIDEBAR_PEEK_STATE_WILL_CLOSE as STATE_WILL_CLOSE,
+} from '~/super_sidebar/constants';
import {
toggleSuperSidebarCollapsed,
isCollapsed,
} from '~/super_sidebar/super_sidebar_collapsed_state_manager';
-import { stubComponent } from 'helpers/stub_component';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
+import { trackContextAccess } from '~/super_sidebar/utils';
import { sidebarData as mockSidebarData, loggedOutSidebarData } from '../mock_data';
const initialSidebarState = { ...sidebarState };
jest.mock('~/super_sidebar/super_sidebar_collapsed_state_manager');
-const closeContextSwitcherMock = jest.fn();
+jest.mock('~/super_sidebar/utils', () => ({
+ ...jest.requireActual('~/super_sidebar/utils'),
+ trackContextAccess: jest.fn(),
+}));
const trialStatusWidgetStubTestId = 'trial-status-widget';
const TrialStatusWidgetStub = { template: `<div data-testid="${trialStatusWidgetStubTestId}" />` };
@@ -36,6 +39,7 @@ const TrialStatusPopoverStub = {
};
const peekClass = 'super-sidebar-peek';
+const hasPeekedClass = 'super-sidebar-has-peeked';
const peekHintClass = 'super-sidebar-peek-hint';
describe('SuperSidebar component', () => {
@@ -43,12 +47,11 @@ describe('SuperSidebar component', () => {
const findSidebar = () => wrapper.findByTestId('super-sidebar');
const findUserBar = () => wrapper.findComponent(UserBar);
- const findContextHeader = () => wrapper.findComponent(ContextHeader);
- const findContextSwitcher = () => wrapper.findComponent(ContextSwitcher);
const findNavContainer = () => wrapper.findByTestId('nav-container');
const findHelpCenter = () => wrapper.findComponent(HelpCenter);
const findSidebarPortalTarget = () => wrapper.findComponent(SidebarPortalTarget);
const findPeekBehavior = () => wrapper.findComponent(SidebarPeekBehavior);
+ const findHoverPeekBehavior = () => wrapper.findComponent(SidebarHoverPeekBehavior);
const findTrialStatusWidget = () => wrapper.findByTestId(trialStatusWidgetStubTestId);
const findTrialStatusPopover = () => wrapper.findByTestId(trialStatusPopoverStubTestId);
const findSidebarMenu = () => wrapper.findComponent(SidebarMenu);
@@ -70,9 +73,6 @@ describe('SuperSidebar component', () => {
sidebarData,
},
stubs: {
- ContextSwitcher: stubComponent(ContextSwitcher, {
- methods: { close: closeContextSwitcherMock },
- }),
TrialStatusWidget: TrialStatusWidgetStub,
TrialStatusPopover: TrialStatusPopoverStub,
},
@@ -128,12 +128,6 @@ describe('SuperSidebar component', () => {
expect(findSidebarPortalTarget().exists()).toBe(true);
});
- it("does not call the context switcher's close method initially", () => {
- createWrapper();
-
- expect(closeContextSwitcherMock).not.toHaveBeenCalled();
- });
-
it('renders hidden shortcut links', () => {
createWrapper();
const [linkAttrs] = mockSidebarData.shortcut_links;
@@ -181,21 +175,43 @@ describe('SuperSidebar component', () => {
expect(findTrialStatusPopover().exists()).toBe(false);
});
- it('does not have peek behavior', () => {
+ it('does not have peek behaviors', () => {
createWrapper();
expect(findPeekBehavior().exists()).toBe(false);
+ expect(findHoverPeekBehavior().exists()).toBe(false);
});
- });
- describe('on collapse', () => {
- beforeEach(() => {
+ it('renders the context header', () => {
createWrapper();
- sidebarState.isCollapsed = true;
+
+ expect(wrapper.text()).toContain('Your work');
});
- it('closes the context switcher', () => {
- expect(closeContextSwitcherMock).toHaveBeenCalled();
+ describe('item access tracking', () => {
+ it('does not track anything if logged out', () => {
+ createWrapper({ sidebarData: loggedOutSidebarData });
+
+ expect(trackContextAccess).not.toHaveBeenCalled();
+ });
+
+ it('does not track anything if logged in and not within a trackable context', () => {
+ createWrapper();
+
+ expect(trackContextAccess).not.toHaveBeenCalled();
+ });
+
+ it('tracks item access if logged in within a trackable context', () => {
+ const currentContext = { namespace: 'groups' };
+ createWrapper({
+ sidebarData: {
+ ...mockSidebarData,
+ current_context: currentContext,
+ },
+ });
+
+ expect(trackContextAccess).toHaveBeenCalledWith('root', currentContext, '/-/track_visits');
+ });
});
});
@@ -205,6 +221,7 @@ describe('SuperSidebar component', () => {
expect(findSidebar().attributes('inert')).toBe('inert');
expect(findSidebar().classes()).not.toContain(peekHintClass);
+ expect(findSidebar().classes()).not.toContain(hasPeekedClass);
expect(findSidebar().classes()).not.toContain(peekClass);
});
@@ -216,6 +233,7 @@ describe('SuperSidebar component', () => {
expect(findSidebar().attributes('inert')).toBe('inert');
expect(findSidebar().classes()).toContain(peekHintClass);
+ expect(findSidebar().classes()).toContain(hasPeekedClass);
expect(findSidebar().classes()).not.toContain(peekClass);
});
@@ -230,9 +248,23 @@ describe('SuperSidebar component', () => {
expect(findSidebar().attributes('inert')).toBe(undefined);
expect(findSidebar().classes()).toContain(peekClass);
expect(findSidebar().classes()).not.toContain(peekHintClass);
+ expect(findHoverPeekBehavior().exists()).toBe(false);
},
);
+ it(`makes sidebar interactive and visible when hover peek state is ${STATE_OPEN}`, async () => {
+ createWrapper({ sidebarState: { isCollapsed: true, isPeekable: true } });
+
+ findHoverPeekBehavior().vm.$emit('change', STATE_OPEN);
+ await nextTick();
+
+ expect(findSidebar().attributes('inert')).toBe(undefined);
+ expect(findSidebar().classes()).toContain(peekClass);
+ expect(findSidebar().classes()).toContain(hasPeekedClass);
+ expect(findSidebar().classes()).not.toContain(peekHintClass);
+ expect(findPeekBehavior().exists()).toBe(false);
+ });
+
it('keeps track of if sidebar has mouseover or not', async () => {
createWrapper({ sidebarState: { isCollapsed: false, isPeekable: true } });
expect(findPeekBehavior().props('isMouseOverSidebar')).toBe(false);
@@ -248,16 +280,9 @@ describe('SuperSidebar component', () => {
createWrapper();
});
- it('allows overflow while the context switcher is closed', () => {
+ it('allows overflow', () => {
expect(findNavContainer().classes()).toContain('gl-overflow-auto');
});
-
- it('hides overflow when context switcher is opened', async () => {
- findContextSwitcher().vm.$emit('toggle', true);
- await nextTick();
-
- expect(findNavContainer().classes()).not.toContain('gl-overflow-auto');
- });
});
describe('when a trial is active', () => {
@@ -271,14 +296,10 @@ describe('SuperSidebar component', () => {
});
});
- describe('Logged out', () => {
- beforeEach(() => {
- createWrapper({ sidebarData: loggedOutSidebarData });
- });
-
- it('renders context header instead of context switcher', () => {
- expect(findContextHeader().exists()).toBe(true);
- expect(findContextSwitcher().exists()).toBe(false);
+ describe('ARIA attributes', () => {
+ it('adds aria-label attribute to nav element', () => {
+ createWrapper();
+ expect(wrapper.find('nav').attributes('aria-label')).toBe('Primary');
});
});
});
diff --git a/spec/frontend/super_sidebar/components/super_sidebar_toggle_spec.js b/spec/frontend/super_sidebar/components/super_sidebar_toggle_spec.js
index 23b735c2773..1f2e5602d10 100644
--- a/spec/frontend/super_sidebar/components/super_sidebar_toggle_spec.js
+++ b/spec/frontend/super_sidebar/components/super_sidebar_toggle_spec.js
@@ -1,6 +1,5 @@
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';
@@ -46,31 +45,29 @@ describe('SuperSidebarToggle component', () => {
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.each(['isCollapsed', 'isPeek', 'isHoverPeek'])(
+ 'has aria-expanded as false when %s is `true`',
+ (stateProp) => {
+ createWrapper({ sidebarState: { [stateProp]: 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')).toBeDefined();
+ expect(findButton().attributes('aria-label')).toBe('Primary navigation sidebar');
});
});
describe('tooltip', () => {
it('displays collapse when expanded', () => {
createWrapper();
- expect(getTooltip().title).toBe(__('Hide sidebar'));
+ expect(getTooltip().title).toBe('Hide sidebar');
});
it('displays expand when collapsed', () => {
createWrapper({ sidebarState: { isCollapsed: true } });
- expect(getTooltip().title).toBe(__('Show sidebar'));
+ expect(getTooltip().title).toBe('Keep sidebar visible');
});
});
diff --git a/spec/frontend/super_sidebar/components/user_bar_spec.js b/spec/frontend/super_sidebar/components/user_bar_spec.js
index c6dd8441094..b58b65f09f5 100644
--- a/spec/frontend/super_sidebar/components/user_bar_spec.js
+++ b/spec/frontend/super_sidebar/components/user_bar_spec.js
@@ -65,8 +65,20 @@ describe('UserBar component', () => {
createWrapper();
});
- it('passes the "Create new..." menu groups to the create-menu component', () => {
- expect(findCreateMenu().props('groups')).toBe(mockSidebarData.create_new_menu_groups);
+ describe('"Create new..." menu', () => {
+ describe('when there are no menu items for it', () => {
+ // This scenario usually happens for an "External" user.
+ it('does not render it', () => {
+ createWrapper({ sidebarData: { ...mockSidebarData, create_new_menu_groups: [] } });
+ expect(findCreateMenu().exists()).toBe(false);
+ });
+ });
+
+ describe('when there are menu items for it', () => {
+ it('passes the "Create new..." menu groups to the create-menu component', () => {
+ expect(findCreateMenu().props('groups')).toBe(mockSidebarData.create_new_menu_groups);
+ });
+ });
});
it('passes the "Merge request" menu groups to the merge_request_menu component', () => {
@@ -165,7 +177,7 @@ describe('UserBar component', () => {
it('search button should have tooltip', () => {
const tooltip = getBinding(findSearchButton().element, 'gl-tooltip');
- expect(tooltip.value).toBe(`Search GitLab <kbd>/</kbd>`);
+ expect(tooltip.value).toBe(`Type <kbd>/</kbd> to search`);
});
it('should render search modal', () => {
@@ -184,7 +196,7 @@ describe('UserBar component', () => {
findSearchModal().vm.$emit('hidden');
await nextTick();
const tooltip = getBinding(findSearchButton().element, 'gl-tooltip');
- expect(tooltip.value).toBe(`Search GitLab <kbd>/</kbd>`);
+ expect(tooltip.value).toBe(`Type <kbd>/</kbd> to search`);
});
});
});
diff --git a/spec/frontend/super_sidebar/components/user_menu_spec.js b/spec/frontend/super_sidebar/components/user_menu_spec.js
index 662677be40f..bcc3383bcd4 100644
--- a/spec/frontend/super_sidebar/components/user_menu_spec.js
+++ b/spec/frontend/super_sidebar/components/user_menu_spec.js
@@ -468,27 +468,6 @@ describe('UserMenu component', () => {
});
});
- describe('Feedback item', () => {
- let item;
-
- beforeEach(() => {
- createWrapper();
- 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_feedback',
- });
- });
- });
-
describe('Sign out group', () => {
const findSignOutGroup = () => wrapper.findByTestId('sign-out-group');
diff --git a/spec/frontend/super_sidebar/mock_data.js b/spec/frontend/super_sidebar/mock_data.js
index 6fb9715824f..d464ce372ed 100644
--- a/spec/frontend/super_sidebar/mock_data.js
+++ b/spec/frontend/super_sidebar/mock_data.js
@@ -79,10 +79,8 @@ export const contextSwitcherLinks = [
export const sidebarData = {
is_logged_in: true,
current_menu_items: [],
- current_context_header: {
- title: 'Your Work',
- icon: 'work',
- },
+ current_context: {},
+ current_context_header: 'Your work',
name: 'Administrator',
username: 'root',
avatar_url: 'path/to/img_administrator',
@@ -124,15 +122,14 @@ export const sidebarData = {
css_class: 'shortcut-link-class',
},
],
+ track_visits_path: '/-/track_visits',
};
export const loggedOutSidebarData = {
is_logged_in: false,
current_menu_items: [],
- current_context_header: {
- title: 'Your Work',
- icon: 'work',
- },
+ current_context: {},
+ current_context_header: 'Your work',
support_path: '/support',
display_whats_new: true,
whats_new_most_recent_release_items_count: 5,
@@ -285,36 +282,3 @@ export const cachedFrequentGroups = JSON.stringify([
frequency: 3,
},
]);
-
-export const searchUserProjectsAndGroupsResponseMock = {
- data: {
- projects: {
- nodes: [
- {
- id: 'gid://gitlab/Project/2',
- name: 'Gitlab Shell',
- namespace: 'Gitlab Org / Gitlab Shell',
- webUrl: 'http://gdk.test:3000/gitlab-org/gitlab-shell',
- avatarUrl: null,
- __typename: 'Project',
- },
- ],
- },
-
- user: {
- id: 'gid://gitlab/User/1',
- groups: {
- nodes: [
- {
- id: 'gid://gitlab/Group/60',
- name: 'GitLab Instance',
- namespace: 'gitlab-instance-2e4abb29',
- webUrl: 'http://gdk.test:3000/groups/gitlab-instance-2e4abb29',
- avatarUrl: null,
- __typename: 'Group',
- },
- ],
- },
- },
- },
-};
diff --git a/spec/frontend/super_sidebar/mocks.js b/spec/frontend/super_sidebar/mocks.js
new file mode 100644
index 00000000000..d13e5f1f361
--- /dev/null
+++ b/spec/frontend/super_sidebar/mocks.js
@@ -0,0 +1,24 @@
+export const moveMouse = (clientX) => {
+ const event = new MouseEvent('mousemove', {
+ clientX,
+ });
+
+ document.dispatchEvent(event);
+};
+
+export const mouseEnter = (el) => {
+ const event = new MouseEvent('mouseenter');
+
+ el.dispatchEvent(event);
+};
+
+export const mouseLeave = (el) => {
+ const event = new MouseEvent('mouseleave');
+
+ el.dispatchEvent(event);
+};
+
+export const moveMouseOutOfDocument = () => {
+ const event = new MouseEvent('mouseleave');
+ document.documentElement.dispatchEvent(event);
+};
diff --git a/spec/frontend/super_sidebar/utils_spec.js b/spec/frontend/super_sidebar/utils_spec.js
index 536599e6c12..85f45de06ba 100644
--- a/spec/frontend/super_sidebar/utils_spec.js
+++ b/spec/frontend/super_sidebar/utils_spec.js
@@ -1,17 +1,20 @@
import * as Sentry from '@sentry/browser';
+import MockAdapter from 'axios-mock-adapter';
import {
getTopFrequentItems,
trackContextAccess,
- formatContextSwitcherItems,
getItemsFromLocalStorage,
removeItemFromLocalStorage,
ariaCurrent,
} from '~/super_sidebar/utils';
+import axios from '~/lib/utils/axios_utils';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import AccessorUtilities from '~/lib/utils/accessor';
import { FREQUENT_ITEMS, FIFTEEN_MINUTES_IN_MS } from '~/frequent_items/constants';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import waitForPromises from 'helpers/wait_for_promises';
import { unsortedFrequentItems, sortedFrequentItems } from '../frequent_items/mock_data';
-import { cachedFrequentProjects, searchUserProjectsAndGroupsResponseMock } from './mock_data';
+import { cachedFrequentProjects } from './mock_data';
jest.mock('@sentry/browser');
@@ -42,13 +45,29 @@ describe('Super sidebar utils spec', () => {
});
describe('trackContextAccess', () => {
+ useLocalStorageSpy();
+
+ let axiosMock;
+
const username = 'root';
+ const trackVisitsPath = '/-/track_visits';
const context = {
namespace: 'groups',
item: { id: 1 },
};
const storageKey = `${username}/frequent-${context.namespace}`;
+ beforeEach(() => {
+ gon.features = { serverSideFrecentNamespaces: true };
+ axiosMock = new MockAdapter(axios);
+ axiosMock.onPost(trackVisitsPath).reply(HTTP_STATUS_OK);
+ });
+
+ afterEach(() => {
+ gon.features = {};
+ axiosMock.restore();
+ });
+
it('returns `false` if local storage is not available', () => {
jest.spyOn(AccessorUtilities, 'canUseLocalStorage').mockReturnValue(false);
@@ -56,7 +75,7 @@ describe('Super sidebar utils spec', () => {
});
it('creates a new item if it does not exist in the local storage', () => {
- trackContextAccess(username, context);
+ trackContextAccess(username, context, trackVisitsPath);
expect(window.localStorage.setItem).toHaveBeenCalledWith(
storageKey,
@@ -70,6 +89,24 @@ describe('Super sidebar utils spec', () => {
);
});
+ it('sends a POST request to persist the visit in the DB', async () => {
+ expect(axiosMock.history.post).toHaveLength(0);
+
+ trackContextAccess(username, context, trackVisitsPath);
+ await waitForPromises();
+
+ expect(axiosMock.history.post).toHaveLength(1);
+ expect(axiosMock.history.post[0].url).toBe(trackVisitsPath);
+ });
+
+ it('does not send a POST request when the serverSideFrecentNamespaces feature flag is disabled', async () => {
+ gon.features = { serverSideFrecentNamespaces: false };
+ trackContextAccess(username, context, trackVisitsPath);
+ await waitForPromises();
+
+ expect(axiosMock.history.post).toHaveLength(0);
+ });
+
it('updates existing item frequency/access time if it was persisted to the local storage over 15 minutes ago', () => {
window.localStorage.setItem(
storageKey,
@@ -81,7 +118,7 @@ describe('Super sidebar utils spec', () => {
},
]),
);
- trackContextAccess(username, context);
+ trackContextAccess(username, context, trackVisitsPath);
expect(window.localStorage.setItem).toHaveBeenCalledWith(
storageKey,
@@ -95,7 +132,7 @@ describe('Super sidebar utils spec', () => {
);
});
- it('leaves item frequency/access time as is if it was persisted to the local storage under 15 minutes ago', () => {
+ it('leaves item frequency/access time as is if it was persisted to the local storage under 15 minutes ago, and does not send a POST request', () => {
const jsonString = JSON.stringify([
{
id: 1,
@@ -108,10 +145,12 @@ describe('Super sidebar utils spec', () => {
expect(window.localStorage.setItem).toHaveBeenCalledTimes(1);
expect(window.localStorage.setItem).toHaveBeenCalledWith(storageKey, jsonString);
- trackContextAccess(username, context);
+ trackContextAccess(username, context, trackVisitsPath);
expect(window.localStorage.setItem).toHaveBeenCalledTimes(3);
expect(window.localStorage.setItem).toHaveBeenLastCalledWith(storageKey, jsonString);
+
+ expect(axiosMock.history.post).toHaveLength(0);
});
it('always updates stored item metadata', () => {
@@ -163,10 +202,14 @@ describe('Super sidebar utils spec', () => {
const newItem = {
id: FREQUENT_ITEMS.MAX_COUNT + 1,
};
- trackContextAccess(username, {
- namespace: 'groups',
- item: newItem,
- });
+ trackContextAccess(
+ username,
+ {
+ namespace: 'groups',
+ item: newItem,
+ },
+ trackVisitsPath,
+ );
// Finally, retrieve the final data from the local storage
const finallyStoredItems = JSON.parse(window.localStorage.getItem(storageKey));
@@ -182,21 +225,6 @@ describe('Super sidebar utils spec', () => {
});
});
- describe('formatContextSwitcherItems', () => {
- it('returns the formatted items', () => {
- const projects = searchUserProjectsAndGroupsResponseMock.data.projects.nodes;
- expect(formatContextSwitcherItems(projects)).toEqual([
- {
- id: projects[0].id,
- avatar: null,
- title: projects[0].name,
- subtitle: 'Gitlab Org',
- link: projects[0].webUrl,
- },
- ]);
- });
- });
-
describe('getItemsFromLocalStorage', () => {
const storageKey = 'mockStorageKey';
const maxItems = 5;