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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/super_sidebar/components')
-rw-r--r--spec/frontend/super_sidebar/components/global_search/components/frequent_groups_spec.js34
-rw-r--r--spec/frontend/super_sidebar/components/global_search/components/frequent_item_spec.js43
-rw-r--r--spec/frontend/super_sidebar/components/global_search/components/frequent_items_spec.js157
-rw-r--r--spec/frontend/super_sidebar/components/global_search/components/frequent_projects_spec.js34
-rw-r--r--spec/frontend/super_sidebar/components/help_center_spec.js8
-rw-r--r--spec/frontend/super_sidebar/components/nav_item_link_spec.js2
-rw-r--r--spec/frontend/super_sidebar/components/nav_item_router_link_spec.js4
-rw-r--r--spec/frontend/super_sidebar/components/scroll_scrim_spec.js60
-rw-r--r--spec/frontend/super_sidebar/components/super_sidebar_spec.js66
-rw-r--r--spec/frontend/super_sidebar/components/user_menu_spec.js157
10 files changed, 354 insertions, 211 deletions
diff --git a/spec/frontend/super_sidebar/components/global_search/components/frequent_groups_spec.js b/spec/frontend/super_sidebar/components/global_search/components/frequent_groups_spec.js
index e63768a03c0..38e1baabf41 100644
--- a/spec/frontend/super_sidebar/components/global_search/components/frequent_groups_spec.js
+++ b/spec/frontend/super_sidebar/components/global_search/components/frequent_groups_spec.js
@@ -1,14 +1,32 @@
import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
import FrequentItems from '~/super_sidebar/components/global_search/components/frequent_items.vue';
import FrequentGroups from '~/super_sidebar/components/global_search/components/frequent_groups.vue';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import currentUserFrecentGroupsQuery from '~/super_sidebar/graphql/queries/current_user_frecent_groups.query.graphql';
+import waitForPromises from 'helpers/wait_for_promises';
+import { frecentGroupsMock } from '../../../mock_data';
+
+Vue.use(VueApollo);
describe('FrequentlyVisitedGroups', () => {
let wrapper;
const groupsPath = '/mock/group/path';
+ const currentUserFrecentGroupsQueryHandler = jest.fn().mockResolvedValue({
+ data: {
+ frecentGroups: frecentGroupsMock,
+ },
+ });
const createComponent = (options) => {
+ const mockApollo = createMockApollo([
+ [currentUserFrecentGroupsQuery, currentUserFrecentGroupsQueryHandler],
+ ]);
+
wrapper = shallowMount(FrequentGroups, {
+ apolloProvider: mockApollo,
provide: {
groupsPath,
},
@@ -28,19 +46,25 @@ describe('FrequentlyVisitedGroups', () => {
expect(findFrequentItems().props()).toMatchObject({
emptyStateText: 'Groups you visit often will appear here.',
groupName: 'Frequently visited groups',
- maxItems: 3,
- storageKey: null,
viewAllItemsIcon: 'group',
viewAllItemsText: 'View all my groups',
viewAllItemsPath: groupsPath,
});
});
- it('with a user, passes a storage key string to FrequentItems', () => {
- gon.current_username = 'test_user';
+ it('loads frecent groups', () => {
+ createComponent();
+
+ expect(currentUserFrecentGroupsQueryHandler).toHaveBeenCalled();
+ expect(findFrequentItems().props('loading')).toBe(true);
+ });
+
+ it('passes fetched groups to FrequentItems', async () => {
createComponent();
+ await waitForPromises();
- expect(findFrequentItems().props('storageKey')).toBe('test_user/frequent-groups');
+ expect(findFrequentItems().props('items')).toEqual(frecentGroupsMock);
+ expect(findFrequentItems().props('loading')).toBe(false);
});
it('passes attrs to FrequentItems', () => {
diff --git a/spec/frontend/super_sidebar/components/global_search/components/frequent_item_spec.js b/spec/frontend/super_sidebar/components/global_search/components/frequent_item_spec.js
index aae1fc543f9..b48a9ca6457 100644
--- a/spec/frontend/super_sidebar/components/global_search/components/frequent_item_spec.js
+++ b/spec/frontend/super_sidebar/components/global_search/components/frequent_item_spec.js
@@ -28,7 +28,6 @@ describe('FrequentlyVisitedItem', () => {
};
const findProjectAvatar = () => wrapper.findComponent(ProjectAvatar);
- const findRemoveButton = () => wrapper.findByRole('button');
const findSubtitle = () => wrapper.findByTestId('subtitle');
beforeEach(() => {
@@ -53,46 +52,4 @@ describe('FrequentlyVisitedItem', () => {
await wrapper.setProps({ item: { ...mockItem, subtitle: null } });
expect(findSubtitle().exists()).toBe(false);
});
-
- describe('clicking the remove button', () => {
- const bubbledClickSpy = jest.fn();
- const clickSpy = jest.fn();
-
- beforeEach(() => {
- wrapper.element.addEventListener('click', bubbledClickSpy);
- const button = findRemoveButton();
- button.element.addEventListener('click', clickSpy);
- button.trigger('click');
- });
-
- it('emits a remove event on clicking the remove button', () => {
- expect(wrapper.emitted('remove')).toEqual([[mockItem]]);
- });
-
- it('stops the native event from bubbling and prevents its default behavior', () => {
- expect(bubbledClickSpy).not.toHaveBeenCalled();
- expect(clickSpy.mock.calls[0][0].defaultPrevented).toBe(true);
- });
- });
-
- describe('pressing enter on the remove button', () => {
- const bubbledKeydownSpy = jest.fn();
- const keydownSpy = jest.fn();
-
- beforeEach(() => {
- wrapper.element.addEventListener('keydown', bubbledKeydownSpy);
- const button = findRemoveButton();
- button.element.addEventListener('keydown', keydownSpy);
- button.trigger('keydown.enter');
- });
-
- it('emits a remove event on clicking the remove button', () => {
- expect(wrapper.emitted('remove')).toEqual([[mockItem]]);
- });
-
- it('stops the native event from bubbling and prevents its default behavior', () => {
- expect(bubbledKeydownSpy).not.toHaveBeenCalled();
- expect(keydownSpy.mock.calls[0][0].defaultPrevented).toBe(true);
- });
- });
});
diff --git a/spec/frontend/super_sidebar/components/global_search/components/frequent_items_spec.js b/spec/frontend/super_sidebar/components/global_search/components/frequent_items_spec.js
index 4700e9c7e10..7876dd92701 100644
--- a/spec/frontend/super_sidebar/components/global_search/components/frequent_items_spec.js
+++ b/spec/frontend/super_sidebar/components/global_search/components/frequent_items_spec.js
@@ -2,28 +2,14 @@ import { GlDisclosureDropdownGroup, GlDisclosureDropdownItem, GlIcon } from '@gi
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import GlobalSearchFrequentItems from '~/super_sidebar/components/global_search/components/frequent_items.vue';
import FrequentItem from '~/super_sidebar/components/global_search/components/frequent_item.vue';
-import { getItemsFromLocalStorage, removeItemFromLocalStorage } from '~/super_sidebar/utils';
-import { cachedFrequentProjects } from 'jest/super_sidebar/mock_data';
-
-jest.mock('~/super_sidebar/utils', () => {
- const original = jest.requireActual('~/super_sidebar/utils');
-
- return {
- ...original,
- getItemsFromLocalStorage: jest.fn(),
- removeItemFromLocalStorage: jest.fn(),
- };
-});
+import FrequentItemSkeleton from '~/super_sidebar/components/global_search/components/frequent_item_skeleton.vue';
+import { frecentGroupsMock } from 'jest/super_sidebar/mock_data';
describe('FrequentlyVisitedItems', () => {
let wrapper;
- const storageKey = 'mockStorageKey';
- const mockStoredItems = JSON.parse(cachedFrequentProjects);
const mockProps = {
emptyStateText: 'mock empty state text',
groupName: 'mock group name',
- maxItems: 42,
- storageKey,
viewAllItemsText: 'View all items',
viewAllItemsIcon: 'question-o',
viewAllItemsPath: '/mock/all_items',
@@ -42,118 +28,97 @@ describe('FrequentlyVisitedItems', () => {
};
const findItems = () => wrapper.findAllComponents(GlDisclosureDropdownItem);
+ const findSkeleton = () => wrapper.findComponent(FrequentItemSkeleton);
const findItemRenderer = (root) => root.findComponent(FrequentItem);
- const setStoredItems = (items) => {
- getItemsFromLocalStorage.mockReturnValue(items);
- };
+ describe('common behavior', () => {
+ beforeEach(() => {
+ createComponent({
+ items: frecentGroupsMock,
+ });
+ });
- beforeEach(() => {
- setStoredItems(mockStoredItems);
+ it('renders the group name', () => {
+ expect(wrapper.text()).toContain(mockProps.groupName);
+ });
+
+ it('renders the view all items link', () => {
+ const lastItem = findItems().at(-1);
+ expect(lastItem.props('item')).toMatchObject({
+ text: mockProps.viewAllItemsText,
+ href: mockProps.viewAllItemsPath,
+ });
+
+ const icon = lastItem.findComponent(GlIcon);
+ expect(icon.props('name')).toBe(mockProps.viewAllItemsIcon);
+ });
});
- describe('without a storage key', () => {
+ describe('while items are being fetched', () => {
beforeEach(() => {
- createComponent({ storageKey: null });
+ createComponent({
+ loading: true,
+ });
});
- it('does not render anything', () => {
- expect(wrapper.html()).toBe('');
+ it('shows the loading state', () => {
+ expect(findSkeleton().exists()).toBe(true);
});
- it('emits a nothing-to-render event', () => {
- expect(wrapper.emitted('nothing-to-render')).toEqual([[]]);
+ it('does not show the empty state', () => {
+ expect(wrapper.text()).not.toContain(mockProps.emptyStateText);
});
});
- describe('with a storageKey', () => {
+ describe('when there are no items', () => {
beforeEach(() => {
createComponent();
});
- describe('common behavior', () => {
- it('calls getItemsFromLocalStorage', () => {
- expect(getItemsFromLocalStorage).toHaveBeenCalledWith({
- storageKey,
- maxItems: mockProps.maxItems,
- });
- });
-
- it('renders the group name', () => {
- expect(wrapper.text()).toContain(mockProps.groupName);
- });
-
- it('renders the view all items link', () => {
- const lastItem = findItems().at(-1);
- expect(lastItem.props('item')).toMatchObject({
- text: mockProps.viewAllItemsText,
- href: mockProps.viewAllItemsPath,
- });
-
- const icon = lastItem.findComponent(GlIcon);
- expect(icon.props('name')).toBe(mockProps.viewAllItemsIcon);
- });
+ it('does not show the loading state', () => {
+ expect(findSkeleton().exists()).toBe(false);
});
- describe('with stored items', () => {
- it('renders the items', () => {
- const items = findItems();
-
- mockStoredItems.forEach((storedItem, index) => {
- const dropdownItem = items.at(index);
-
- // Check GlDisclosureDropdownItem's item has the right structure
- expect(dropdownItem.props('item')).toMatchObject({
- text: storedItem.name,
- href: storedItem.webUrl,
- });
-
- // Check FrequentItem's item has the right structure
- expect(findItemRenderer(dropdownItem).props('item')).toMatchObject({
- id: storedItem.id,
- title: storedItem.name,
- subtitle: expect.any(String),
- avatar: storedItem.avatarUrl,
- });
- });
- });
+ it('shows the empty state', () => {
+ expect(wrapper.text()).toContain(mockProps.emptyStateText);
+ });
+ });
- it('does not render the empty state text', () => {
- expect(wrapper.text()).not.toContain('mock empty state text');
+ describe('when there are items', () => {
+ beforeEach(() => {
+ createComponent({
+ items: frecentGroupsMock,
});
+ });
- describe('removing an item', () => {
- let itemToRemove;
+ it('renders the items', () => {
+ const items = findItems();
- beforeEach(() => {
- const itemRenderer = findItemRenderer(findItems().at(0));
- itemToRemove = itemRenderer.props('item');
- itemRenderer.vm.$emit('remove', itemToRemove);
- });
+ frecentGroupsMock.forEach((item, index) => {
+ const dropdownItem = items.at(index);
- it('calls removeItemFromLocalStorage when an item emits a remove event', () => {
- expect(removeItemFromLocalStorage).toHaveBeenCalledWith({
- storageKey,
- item: itemToRemove,
- });
+ // Check GlDisclosureDropdownItem's item has the right structure
+ expect(dropdownItem.props('item')).toMatchObject({
+ text: item.name,
+ href: item.webUrl,
});
- it('no longer renders that item', () => {
- const renderedItemTexts = findItems().wrappers.map((item) => item.props('item').text);
- expect(renderedItemTexts).not.toContain(itemToRemove.text);
+ // Check FrequentItem's item has the right structure
+ expect(findItemRenderer(dropdownItem).props('item')).toMatchObject({
+ id: item.id,
+ title: item.name,
+ subtitle: expect.any(String),
+ avatar: item.avatarUrl,
});
});
});
- });
- describe('with no stored items', () => {
- beforeEach(() => {
- setStoredItems([]);
- createComponent();
+ it('does not show the loading state', () => {
+ expect(findSkeleton().exists()).toBe(false);
});
- it('renders the empty state text', () => {
- expect(wrapper.text()).toContain(mockProps.emptyStateText);
+ it('does not show the empty state', () => {
+ expect(wrapper.text()).not.toContain(mockProps.emptyStateText);
});
});
});
diff --git a/spec/frontend/super_sidebar/components/global_search/components/frequent_projects_spec.js b/spec/frontend/super_sidebar/components/global_search/components/frequent_projects_spec.js
index 7554c123574..b7123f295f7 100644
--- a/spec/frontend/super_sidebar/components/global_search/components/frequent_projects_spec.js
+++ b/spec/frontend/super_sidebar/components/global_search/components/frequent_projects_spec.js
@@ -1,14 +1,32 @@
import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
import FrequentItems from '~/super_sidebar/components/global_search/components/frequent_items.vue';
import FrequentProjects from '~/super_sidebar/components/global_search/components/frequent_projects.vue';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import currentUserFrecentProjectsQuery from '~/super_sidebar/graphql/queries/current_user_frecent_projects.query.graphql';
+import waitForPromises from 'helpers/wait_for_promises';
+import { frecentProjectsMock } from '../../../mock_data';
+
+Vue.use(VueApollo);
describe('FrequentlyVisitedProjects', () => {
let wrapper;
const projectsPath = '/mock/project/path';
+ const currentUserFrecentProjectsQueryHandler = jest.fn().mockResolvedValue({
+ data: {
+ frecentProjects: frecentProjectsMock,
+ },
+ });
const createComponent = (options) => {
+ const mockApollo = createMockApollo([
+ [currentUserFrecentProjectsQuery, currentUserFrecentProjectsQueryHandler],
+ ]);
+
wrapper = shallowMount(FrequentProjects, {
+ apolloProvider: mockApollo,
provide: {
projectsPath,
},
@@ -28,19 +46,25 @@ describe('FrequentlyVisitedProjects', () => {
expect(findFrequentItems().props()).toMatchObject({
emptyStateText: 'Projects you visit often will appear here.',
groupName: 'Frequently visited projects',
- maxItems: 5,
- storageKey: null,
viewAllItemsIcon: 'project',
viewAllItemsText: 'View all my projects',
viewAllItemsPath: projectsPath,
});
});
- it('with a user, passes a storage key string to FrequentItems', () => {
- gon.current_username = 'test_user';
+ it('loads frecent projects', () => {
+ createComponent();
+
+ expect(currentUserFrecentProjectsQueryHandler).toHaveBeenCalled();
+ expect(findFrequentItems().props('loading')).toBe(true);
+ });
+
+ it('passes fetched projects to FrequentItems', async () => {
createComponent();
+ await waitForPromises();
- expect(findFrequentItems().props('storageKey')).toBe('test_user/frequent-projects');
+ expect(findFrequentItems().props('items')).toEqual(frecentProjectsMock);
+ expect(findFrequentItems().props('loading')).toBe(false);
});
it('passes attrs to FrequentItems', () => {
diff --git a/spec/frontend/super_sidebar/components/help_center_spec.js b/spec/frontend/super_sidebar/components/help_center_spec.js
index 39537b65fa5..8e9e3e8ba20 100644
--- a/spec/frontend/super_sidebar/components/help_center_spec.js
+++ b/spec/frontend/super_sidebar/components/help_center_spec.js
@@ -94,7 +94,6 @@ describe('HelpCenter component', () => {
it('passes custom offset to the dropdown', () => {
expect(findDropdown().props('dropdownOffset')).toEqual({
- crossAxis: -4,
mainAxis: 4,
});
});
@@ -169,14 +168,13 @@ describe('HelpCenter component', () => {
describe('showWhatsNew', () => {
beforeEach(() => {
- beforeEach(() => {
- createWrapper({ ...sidebarData, show_version_check: true });
- });
+ createWrapper({ ...sidebarData, show_version_check: true });
+
findButton("What's new 5").click();
});
it('shows the "What\'s new" slideout', () => {
- expect(toggleWhatsNewDrawer).toHaveBeenCalledWith(expect.any(Object));
+ expect(toggleWhatsNewDrawer).toHaveBeenCalledWith(sidebarData.whats_new_version_digest);
});
it('shows the existing "What\'s new" slideout instance on subsequent clicks', () => {
diff --git a/spec/frontend/super_sidebar/components/nav_item_link_spec.js b/spec/frontend/super_sidebar/components/nav_item_link_spec.js
index 5cc1bd01d0f..59fa6d022ae 100644
--- a/spec/frontend/super_sidebar/components/nav_item_link_spec.js
+++ b/spec/frontend/super_sidebar/components/nav_item_link_spec.js
@@ -29,7 +29,7 @@ describe('NavItemLink component', () => {
expect(wrapper.attributes()).toEqual({
href: '/foo',
- class: 'gl-bg-t-gray-a-08',
+ class: 'super-sidebar-nav-item-current',
'aria-current': 'page',
});
});
diff --git a/spec/frontend/super_sidebar/components/nav_item_router_link_spec.js b/spec/frontend/super_sidebar/components/nav_item_router_link_spec.js
index a7ca56325fe..dfae5e96cd8 100644
--- a/spec/frontend/super_sidebar/components/nav_item_router_link_spec.js
+++ b/spec/frontend/super_sidebar/components/nav_item_router_link_spec.js
@@ -45,7 +45,9 @@ describe('NavItemRouterLink component', () => {
routerLinkSlotProps: { isActive: true },
});
- expect(wrapper.findComponent(RouterLinkStub).props('activeClass')).toBe('gl-bg-t-gray-a-08');
+ expect(wrapper.findComponent(RouterLinkStub).props('activeClass')).toBe(
+ 'super-sidebar-nav-item-current',
+ );
expect(wrapper.attributes()).toEqual({
href: '/foo',
'aria-current': 'page',
diff --git a/spec/frontend/super_sidebar/components/scroll_scrim_spec.js b/spec/frontend/super_sidebar/components/scroll_scrim_spec.js
new file mode 100644
index 00000000000..ff1e9968f9b
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/scroll_scrim_spec.js
@@ -0,0 +1,60 @@
+import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ScrollScrim from '~/super_sidebar/components/scroll_scrim.vue';
+import { useMockIntersectionObserver } from 'helpers/mock_dom_observer';
+
+describe('ScrollScrim', () => {
+ let wrapper;
+ const { trigger: triggerIntersection } = useMockIntersectionObserver();
+
+ const createWrapper = () => {
+ wrapper = shallowMountExtended(ScrollScrim, {});
+ };
+
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ const findTopBoundary = () => wrapper.vm.$refs['top-boundary'];
+ const findBottomBoundary = () => wrapper.vm.$refs['bottom-boundary'];
+
+ describe('top scrim', () => {
+ describe('when top boundary is visible', () => {
+ it('does not show', async () => {
+ triggerIntersection(findTopBoundary(), { entry: { isIntersecting: true } });
+ await nextTick();
+
+ expect(wrapper.classes()).not.toContain('top-scrim-visible');
+ });
+ });
+
+ describe('when top boundary is not visible', () => {
+ it('does show', async () => {
+ triggerIntersection(findTopBoundary(), { entry: { isIntersecting: false } });
+ await nextTick();
+
+ expect(wrapper.classes()).toContain('top-scrim-visible');
+ });
+ });
+ });
+
+ describe('bottom scrim', () => {
+ describe('when bottom boundary is visible', () => {
+ it('does not show', async () => {
+ triggerIntersection(findBottomBoundary(), { entry: { isIntersecting: true } });
+ await nextTick();
+
+ expect(wrapper.classes()).not.toContain('bottom-scrim-visible');
+ });
+ });
+
+ describe('when bottom boundary is not visible', () => {
+ it('does show', async () => {
+ triggerIntersection(findBottomBoundary(), { entry: { isIntersecting: false } });
+ await nextTick();
+
+ expect(wrapper.classes()).toContain('bottom-scrim-visible');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/super_sidebar_spec.js b/spec/frontend/super_sidebar/components/super_sidebar_spec.js
index 92736b99e14..9718cb7ad15 100644
--- a/spec/frontend/super_sidebar/components/super_sidebar_spec.js
+++ b/spec/frontend/super_sidebar/components/super_sidebar_spec.js
@@ -1,4 +1,7 @@
import { nextTick } from 'vue';
+import { GlBreakpointInstance as bp, breakpoints } from '@gitlab/ui/dist/utils';
+import sidebarEventHub from '~/super_sidebar/event_hub';
+import ExtraInfo from 'jh_else_ce/super_sidebar/components/extra_info.vue';
import { Mousetrap } from '~/lib/mousetrap';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import SuperSidebar from '~/super_sidebar/components/super_sidebar.vue';
@@ -23,6 +26,7 @@ import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { trackContextAccess } from '~/super_sidebar/utils';
import { sidebarData as mockSidebarData, loggedOutSidebarData } from '../mock_data';
+const { lg, xl } = breakpoints;
const initialSidebarState = { ...sidebarState };
jest.mock('~/super_sidebar/super_sidebar_collapsed_state_manager');
@@ -56,6 +60,8 @@ describe('SuperSidebar component', () => {
const findTrialStatusWidget = () => wrapper.findByTestId(trialStatusWidgetStubTestId);
const findTrialStatusPopover = () => wrapper.findByTestId(trialStatusPopoverStubTestId);
const findSidebarMenu = () => wrapper.findComponent(SidebarMenu);
+ const findAdminLink = () => wrapper.findByTestId('sidebar-admin-link');
+ const findContextHeader = () => wrapper.findComponent('#super-sidebar-context-header');
let trackingSpy = null;
const createWrapper = ({
@@ -128,6 +134,11 @@ describe('SuperSidebar component', () => {
expect(findHelpCenter().props('sidebarData')).toBe(mockSidebarData);
});
+ it('renders extra info section', () => {
+ createWrapper();
+ expect(wrapper.findComponent(ExtraInfo).exists()).toBe(true);
+ });
+
it('does not render SidebarMenu when items are empty', () => {
createWrapper();
expect(findSidebarMenu().exists()).toBe(false);
@@ -207,6 +218,15 @@ describe('SuperSidebar component', () => {
expect(wrapper.text()).toContain('Your work');
});
+ it('handles event toggle-menu-header correctly', async () => {
+ createWrapper();
+
+ sidebarEventHub.$emit('toggle-menu-header', false);
+
+ await nextTick();
+ expect(findContextHeader().exists()).toBe(false);
+ });
+
describe('item access tracking', () => {
it('does not track anything if logged out', () => {
createWrapper({ sidebarData: loggedOutSidebarData });
@@ -299,8 +319,8 @@ describe('SuperSidebar component', () => {
createWrapper();
});
- it('allows overflow', () => {
- expect(findNavContainer().classes()).toContain('gl-overflow-auto');
+ it('allows overflow with scroll scrim', () => {
+ expect(findNavContainer().element.tagName).toContain('SCROLL-SCRIM');
});
});
@@ -314,4 +334,46 @@ describe('SuperSidebar component', () => {
expect(findTrialStatusPopover().exists()).toBe(true);
});
});
+
+ describe('keyboard interactivity', () => {
+ it('does not bind keydown events on screens xl and above', async () => {
+ jest.spyOn(document, 'addEventListener');
+ jest.spyOn(bp, 'windowWidth').mockReturnValue(xl);
+ createWrapper();
+
+ isCollapsed.mockReturnValue(false);
+ await nextTick();
+
+ expect(document.addEventListener).not.toHaveBeenCalled();
+ });
+
+ it('binds keydown events on screens below xl', () => {
+ jest.spyOn(document, 'addEventListener');
+ jest.spyOn(bp, 'windowWidth').mockReturnValue(lg);
+ createWrapper();
+
+ expect(document.addEventListener).toHaveBeenCalledWith('keydown', wrapper.vm.focusTrap);
+ });
+ });
+
+ describe('link to Admin area', () => {
+ describe('when user is admin', () => {
+ it('renders', () => {
+ createWrapper({
+ sidebarData: {
+ ...mockSidebarData,
+ is_admin: true,
+ },
+ });
+ expect(findAdminLink().attributes('href')).toBe(mockSidebarData.admin_url);
+ });
+ });
+
+ describe('when user is not admin', () => {
+ it('renders', () => {
+ createWrapper();
+ expect(findAdminLink().exists()).toBe(false);
+ });
+ });
+ });
});
diff --git a/spec/frontend/super_sidebar/components/user_menu_spec.js b/spec/frontend/super_sidebar/components/user_menu_spec.js
index 45a60fce00a..4af3247693b 100644
--- a/spec/frontend/super_sidebar/components/user_menu_spec.js
+++ b/spec/frontend/super_sidebar/components/user_menu_spec.js
@@ -1,8 +1,10 @@
import { GlAvatar, GlDisclosureDropdown } from '@gitlab/ui';
+import { nextTick } from 'vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { stubComponent } from 'helpers/stub_component';
import UserMenu from '~/super_sidebar/components/user_menu.vue';
import UserMenuProfileItem from '~/super_sidebar/components/user_menu_profile_item.vue';
+import SetStatusModal from '~/set_status_modal/set_status_modal_wrapper.vue';
import { mockTracking } from 'helpers/tracking_helper';
import PersistentUserCallout from '~/persistent_user_callout';
import { userMenuMockData, userMenuMockStatus, userMenuMockPipelineMinutes } from '../mock_data';
@@ -13,6 +15,7 @@ describe('UserMenu component', () => {
const GlEmoji = { template: '<img/>' };
const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
+ const findSetStatusModal = () => wrapper.findComponent(SetStatusModal);
const showDropdown = () => findDropdown().vm.$emit('shown');
const closeDropdownSpy = jest.fn();
@@ -28,6 +31,7 @@ describe('UserMenu component', () => {
stubs: {
GlEmoji,
GlAvatar: true,
+ SetStatusModal: stubComponent(SetStatusModal),
...stubs,
},
provide: {
@@ -74,6 +78,20 @@ describe('UserMenu component', () => {
});
});
+ it('updates avatar url on custom avatar update event', async () => {
+ const url = `${userMenuMockData.avatar_url}-new-avatar`;
+
+ document.dispatchEvent(new CustomEvent('userAvatar:update', { detail: { url } }));
+ await nextTick();
+
+ const avatar = toggle.findComponent(GlAvatar);
+ expect(avatar.exists()).toBe(true);
+ expect(avatar.props()).toMatchObject({
+ entityName: userMenuMockData.name,
+ src: url,
+ });
+ });
+
it('renders screen reader text', () => {
expect(toggle.find('.gl-sr-only').text()).toBe(`${userMenuMockData.name} user’s menu`);
});
@@ -91,31 +109,46 @@ describe('UserMenu component', () => {
describe('User status item', () => {
let item;
- const setItem = ({ can_update, busy, customized, stubs } = {}) => {
- createWrapper({ status: { ...userMenuMockStatus, can_update, busy, customized } }, stubs);
+ const setItem = async ({
+ can_update: canUpdate = false,
+ busy = false,
+ customized = false,
+ stubs,
+ } = {}) => {
+ createWrapper(
+ { status: { ...userMenuMockStatus, can_update: canUpdate, busy, customized } },
+ stubs,
+ );
+ // Mock mounting the modal if we can update
+ if (canUpdate) {
+ expect(wrapper.vm.setStatusModalReady).toEqual(false);
+ findSetStatusModal().vm.$emit('mounted');
+ await nextTick();
+ expect(wrapper.vm.setStatusModalReady).toEqual(true);
+ }
item = wrapper.findByTestId('status-item');
};
describe('When user cannot update the status', () => {
- it('does not render the status menu item', () => {
- setItem();
+ it('does not render the status menu item', async () => {
+ await setItem();
expect(item.exists()).toBe(false);
});
});
describe('When user can update the status', () => {
- it('renders the status menu item', () => {
- setItem({ can_update: true });
+ it('renders the status menu item', async () => {
+ await setItem({ can_update: true });
expect(item.exists()).toBe(true);
+ expect(item.find('button').attributes()).toMatchObject({
+ 'data-track-property': 'nav_user_menu',
+ 'data-track-action': 'click_link',
+ 'data-track-label': 'user_edit_status',
+ });
});
- it('should set the CSS class for triggering status update modal', () => {
- setItem({ can_update: true });
- expect(item.find('.js-set-status-modal-trigger').exists()).toBe(true);
- });
-
- it('should close the dropdown when status modal opened', () => {
- setItem({
+ it('should close the dropdown when status modal opened', async () => {
+ await setItem({
can_update: true,
stubs: {
GlDisclosureDropdown: stubComponent(GlDisclosureDropdown, {
@@ -139,57 +172,75 @@ describe('UserMenu component', () => {
${true} | ${true} | ${'Edit status'}
`(
'when busy is "$busy" and customized is "$customized" the label is "$label"',
- ({ busy, customized, label }) => {
- setItem({ can_update: true, busy, customized });
+ async ({ busy, customized, label }) => {
+ await setItem({ can_update: true, busy, customized });
expect(item.text()).toBe(label);
},
);
});
+ });
+ });
+
+ describe('set status modal', () => {
+ describe('when the user cannot update the status', () => {
+ it('should not render the modal', () => {
+ createWrapper({
+ status: { ...userMenuMockStatus, can_update: false },
+ });
- describe('Status update modal wrapper', () => {
- const findModalWrapper = () => wrapper.find('.js-set-status-modal-wrapper');
+ expect(findSetStatusModal().exists()).toBe(false);
+ });
+ });
- it('renders the modal wrapper', () => {
- setItem({ can_update: true });
- expect(findModalWrapper().exists()).toBe(true);
+ describe('when the user can update the status', () => {
+ describe.each`
+ busy | customized
+ ${true} | ${true}
+ ${true} | ${false}
+ ${false} | ${true}
+ `('and the status is busy or customized', ({ busy, customized }) => {
+ it('should pass the current status to the modal', () => {
+ createWrapper({
+ status: { ...userMenuMockStatus, can_update: true, busy, customized },
+ });
+
+ expect(findSetStatusModal().exists()).toBe(true);
+ expect(findSetStatusModal().props()).toMatchObject({
+ defaultEmoji: 'speech_balloon',
+ currentEmoji: userMenuMockStatus.emoji,
+ currentMessage: userMenuMockStatus.message,
+ currentAvailability: userMenuMockStatus.availability,
+ currentClearStatusAfter: userMenuMockStatus.clear_after,
+ });
});
- describe('when user cannot update status', () => {
- it('sets default data attributes', () => {
- setItem({ can_update: true });
- expect(findModalWrapper().attributes()).toMatchObject({
- 'data-current-emoji': '',
- 'data-current-message': '',
- 'data-default-emoji': 'speech_balloon',
- });
+ it('casts falsey values to empty strings', () => {
+ createWrapper({
+ status: { can_update: true, busy, customized },
+ });
+
+ expect(findSetStatusModal().exists()).toBe(true);
+ expect(findSetStatusModal().props()).toMatchObject({
+ defaultEmoji: 'speech_balloon',
+ currentEmoji: '',
+ currentMessage: '',
+ currentAvailability: '',
+ currentClearStatusAfter: '',
});
});
+ });
+
+ describe('and the status is neither busy nor customized', () => {
+ it('should pass an empty status to the modal', () => {
+ createWrapper({
+ status: { ...userMenuMockStatus, can_update: true, busy: false, customized: false },
+ });
- describe.each`
- busy | customized
- ${true} | ${true}
- ${true} | ${false}
- ${false} | ${true}
- ${false} | ${false}
- `(`when user can update status`, ({ busy, customized }) => {
- it(`and ${busy ? 'is busy' : 'is not busy'} and status ${
- customized ? 'is' : 'is not'
- } customized sets user status data attributes`, () => {
- setItem({ can_update: true, busy, customized });
- if (busy || customized) {
- expect(findModalWrapper().attributes()).toMatchObject({
- 'data-current-emoji': userMenuMockStatus.emoji,
- 'data-current-message': userMenuMockStatus.message,
- 'data-current-availability': userMenuMockStatus.availability,
- 'data-current-clear-status-after': userMenuMockStatus.clear_after,
- });
- } else {
- expect(findModalWrapper().attributes()).toMatchObject({
- 'data-current-emoji': '',
- 'data-current-message': '',
- 'data-default-emoji': 'speech_balloon',
- });
- }
+ expect(findSetStatusModal().exists()).toBe(true);
+ expect(findSetStatusModal().props()).toMatchObject({
+ defaultEmoji: 'speech_balloon',
+ currentEmoji: '',
+ currentMessage: '',
});
});
});