gitlab-ce/spec/frontend/super_sidebar/components/super_sidebar_spec.js

363 lines
13 KiB
JavaScript

import { nextTick } from 'vue';
import { GlBreakpointInstance as bp, breakpoints } from '@gitlab/ui/dist/utils';
import { Mousetrap } from '~/lib/mousetrap';
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 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 SidebarMenu from '~/super_sidebar/components/sidebar_menu.vue';
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 { 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');
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}" />` };
const trialStatusPopoverStubTestId = 'trial-status-popover';
const TrialStatusPopoverStub = {
template: `<div data-testid="${trialStatusPopoverStubTestId}" />`,
};
const peekClass = 'super-sidebar-peek';
const hasPeekedClass = 'super-sidebar-has-peeked';
const peekHintClass = 'super-sidebar-peek-hint';
describe('SuperSidebar component', () => {
let wrapper;
const findSkipToLink = () => wrapper.findByTestId('super-sidebar-skip-to');
const findSidebar = () => wrapper.findByTestId('super-sidebar');
const findUserBar = () => wrapper.findComponent(UserBar);
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);
const findAdminLink = () => wrapper.findByTestId('sidebar-admin-link');
let trackingSpy = null;
const createWrapper = ({
provide = {},
sidebarData = mockSidebarData,
sidebarState: state = {},
} = {}) => {
Object.assign(sidebarState, state);
wrapper = shallowMountExtended(SuperSidebar, {
provide: {
showTrialStatusWidget: false,
...provide,
},
propsData: {
sidebarData,
},
stubs: {
TrialStatusWidget: TrialStatusWidgetStub,
TrialStatusPopover: TrialStatusPopoverStub,
},
});
};
beforeEach(() => {
Object.assign(sidebarState, initialSidebarState);
trackingSpy = mockTracking(undefined, undefined, jest.spyOn);
});
afterEach(() => {
unmockTracking();
});
describe('default', () => {
it('renders skip to main content link when logged in', () => {
createWrapper();
expect(findSkipToLink().attributes('href')).toBe('#content-body');
});
it('does not render skip to main content link when logged out', () => {
createWrapper({ sidebarData: { is_logged_in: false } });
expect(findSkipToLink().exists()).toBe(false);
});
it('has accessible role and name', () => {
createWrapper();
const nav = wrapper.findByRole('navigation');
const heading = wrapper.findByText('Primary navigation');
expect(nav.attributes('aria-labelledby')).toBe('super-sidebar-heading');
expect(heading.attributes('id')).toBe('super-sidebar-heading');
});
it('adds inert attribute when collapsed', () => {
createWrapper({ sidebarState: { isCollapsed: true } });
expect(findSidebar().attributes('inert')).toBe('inert');
});
it('does not add inert attribute when expanded', () => {
createWrapper();
expect(findSidebar().attributes('inert')).toBe(undefined);
});
it('renders UserBar with sidebarData', () => {
createWrapper();
expect(findUserBar().props('sidebarData')).toBe(mockSidebarData);
});
it('renders HelpCenter with sidebarData', () => {
createWrapper();
expect(findHelpCenter().props('sidebarData')).toBe(mockSidebarData);
});
it('does not render SidebarMenu when items are empty', () => {
createWrapper();
expect(findSidebarMenu().exists()).toBe(false);
});
it('renders SidebarMenu with menu items', () => {
const menuItems = [
{ id: 1, title: 'Menu item 1' },
{ id: 2, title: 'Menu item 2' },
];
createWrapper({ sidebarData: { ...mockSidebarData, current_menu_items: menuItems } });
expect(findSidebarMenu().props('items')).toBe(menuItems);
});
it('renders SidebarPortalTarget', () => {
createWrapper();
expect(findSidebarPortalTarget().exists()).toBe(true);
});
it('renders hidden shortcut links', () => {
createWrapper();
const [linkAttrs] = mockSidebarData.shortcut_links;
const link = wrapper.find(`.${linkAttrs.css_class}`);
expect(link.exists()).toBe(true);
expect(link.attributes('href')).toBe(linkAttrs.href);
expect(link.attributes('class')).toContain('gl-display-none');
});
it('sets up the sidebar toggle shortcut', () => {
createWrapper();
isCollapsed.mockReturnValue(false);
Mousetrap.trigger('mod+\\');
expect(toggleSuperSidebarCollapsed).toHaveBeenCalledTimes(1);
expect(toggleSuperSidebarCollapsed).toHaveBeenCalledWith(true, true);
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'nav_hide', {
label: 'nav_toggle_keyboard_shortcut',
property: 'nav_sidebar',
});
isCollapsed.mockReturnValue(true);
Mousetrap.trigger('mod+\\');
expect(toggleSuperSidebarCollapsed).toHaveBeenCalledTimes(2);
expect(toggleSuperSidebarCollapsed).toHaveBeenCalledWith(false, true);
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'nav_show', {
label: 'nav_toggle_keyboard_shortcut',
property: 'nav_sidebar',
});
jest.spyOn(Mousetrap, 'unbind');
wrapper.destroy();
expect(Mousetrap.unbind).toHaveBeenCalledWith(['mod+\\']);
});
it('does not render trial status widget', () => {
createWrapper();
expect(findTrialStatusWidget().exists()).toBe(false);
expect(findTrialStatusPopover().exists()).toBe(false);
});
it('does not have peek behaviors', () => {
createWrapper();
expect(findPeekBehavior().exists()).toBe(false);
expect(findHoverPeekBehavior().exists()).toBe(false);
});
it('renders the context header', () => {
createWrapper();
expect(wrapper.text()).toContain('Your work');
});
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');
});
});
});
describe('peek behavior', () => {
it(`initially makes sidebar inert and peekable (${STATE_CLOSED})`, () => {
createWrapper({ sidebarState: { isCollapsed: true, isPeekable: true } });
expect(findSidebar().attributes('inert')).toBe('inert');
expect(findSidebar().classes()).not.toContain(peekHintClass);
expect(findSidebar().classes()).not.toContain(hasPeekedClass);
expect(findSidebar().classes()).not.toContain(peekClass);
});
it(`makes sidebar inert and shows peek hint when peek state is ${STATE_WILL_OPEN}`, async () => {
createWrapper({ sidebarState: { isCollapsed: true, isPeekable: true } });
findPeekBehavior().vm.$emit('change', STATE_WILL_OPEN);
await nextTick();
expect(findSidebar().attributes('inert')).toBe('inert');
expect(findSidebar().classes()).toContain(peekHintClass);
expect(findSidebar().classes()).toContain(hasPeekedClass);
expect(findSidebar().classes()).not.toContain(peekClass);
});
it.each([STATE_OPEN, STATE_WILL_CLOSE])(
'makes sidebar interactive and visible when peek state is %s',
async (state) => {
createWrapper({ sidebarState: { isCollapsed: true, isPeekable: true } });
findPeekBehavior().vm.$emit('change', state);
await nextTick();
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);
await findSidebar().trigger('mouseenter');
expect(findPeekBehavior().props('isMouseOverSidebar')).toBe(true);
await findSidebar().trigger('mouseleave');
expect(findPeekBehavior().props('isMouseOverSidebar')).toBe(false);
});
});
describe('nav container', () => {
beforeEach(() => {
createWrapper();
});
it('allows overflow with scroll scrim', () => {
expect(findNavContainer().element.tagName).toContain('SCROLL-SCRIM');
});
});
describe('when a trial is active', () => {
beforeEach(() => {
createWrapper({ provide: { showTrialStatusWidget: true } });
});
it('renders trial status widget', () => {
expect(findTrialStatusWidget().exists()).toBe(true);
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);
});
});
});
});