import { GlBreakpointInstance } from '@gitlab/ui/dist/utils'; import { SIDEBAR_COLLAPSED_CLASS } from '~/contextual_sidebar'; import { calculateTop, showSubLevelItems, canShowSubItems, canShowActiveSubItems, mouseEnterTopItems, mouseLeaveTopItem, getOpenMenu, setOpenMenu, mousePos, getHideSubItemsInterval, documentMouseMove, getHeaderHeight, setSidebar, subItemsMouseLeave, } from '~/fly_out_nav'; describe('Fly out sidebar navigation', () => { let el; let breakpointSize = 'lg'; const OLD_SIDEBAR_WIDTH = 200; const CONTAINER_INITIAL_BOUNDING_RECT = { x: 8, y: 8, width: 769, height: 0, top: 8, right: 777, bottom: 8, left: 8, }; const SUB_ITEMS_INITIAL_BOUNDING_RECT = { x: 148, y: 8, width: 0, height: 150, top: 8, right: 148, bottom: 158, left: 148, }; const mockBoundingClientRect = (elem, rect) => { jest.spyOn(elem, 'getBoundingClientRect').mockReturnValue(rect); }; const findSubItems = () => document.querySelector('.sidebar-sub-level-items'); const mockBoundingRects = () => { const subItems = findSubItems(); mockBoundingClientRect(el, CONTAINER_INITIAL_BOUNDING_RECT); mockBoundingClientRect(subItems, SUB_ITEMS_INITIAL_BOUNDING_RECT); }; const mockSidebarFragment = (styleProps = '') => ``; beforeEach(() => { el = document.createElement('div'); el.style.position = 'relative'; document.body.appendChild(el); jest.spyOn(GlBreakpointInstance, 'getBreakpointSize').mockImplementation(() => breakpointSize); }); afterEach(() => { document.body.innerHTML = ''; breakpointSize = 'lg'; mousePos.length = 0; setSidebar(null); }); describe('calculateTop', () => { it('returns boundingRect top', () => { const boundingRect = { top: 100, height: 100, }; expect(calculateTop(boundingRect, 100)).toBe(100); }); }); describe('getHideSubItemsInterval', () => { beforeEach(() => { el.innerHTML = mockSidebarFragment('position: fixed; top: 0; left: 100px; height: 150px;'); mockBoundingRects(); }); it('returns 0 if currentOpenMenu is nil', () => { setOpenMenu(null); expect(getHideSubItemsInterval()).toBe(0); }); it('returns 0 if mousePos is empty', () => { expect(getHideSubItemsInterval()).toBe(0); }); it('returns 0 when mouse above sub-items', () => { showSubLevelItems(el); documentMouseMove({ clientX: el.getBoundingClientRect().left, clientY: el.getBoundingClientRect().top, }); documentMouseMove({ clientX: el.getBoundingClientRect().left, clientY: el.getBoundingClientRect().top - 50, }); expect(getHideSubItemsInterval()).toBe(0); }); it('returns 0 when mouse is below sub-items', () => { const subItems = findSubItems(); showSubLevelItems(el); documentMouseMove({ clientX: el.getBoundingClientRect().left, clientY: el.getBoundingClientRect().top, }); documentMouseMove({ clientX: el.getBoundingClientRect().left, clientY: el.getBoundingClientRect().top - subItems.getBoundingClientRect().height + 50, }); expect(getHideSubItemsInterval()).toBe(0); }); it('returns 300 when mouse is moved towards sub-items', () => { documentMouseMove({ clientX: el.getBoundingClientRect().left, clientY: el.getBoundingClientRect().top, }); showSubLevelItems(el); documentMouseMove({ clientX: el.getBoundingClientRect().left + 20, clientY: el.getBoundingClientRect().top + 10, }); expect(getHideSubItemsInterval()).toBe(300); }); }); describe('mouseLeaveTopItem', () => { beforeEach(() => { jest.spyOn(el.classList, 'remove'); }); it('removes is-over class if currentOpenMenu is null', () => { setOpenMenu(null); mouseLeaveTopItem(el); expect(el.classList.remove).toHaveBeenCalledWith('is-over'); }); it('removes is-over class if currentOpenMenu is null & there are sub-items', () => { setOpenMenu(null); el.innerHTML = mockSidebarFragment('position: absolute'); mouseLeaveTopItem(el); expect(el.classList.remove).toHaveBeenCalledWith('is-over'); }); it('does not remove is-over class if currentOpenMenu is the passed in sub-items', () => { setOpenMenu(null); el.innerHTML = mockSidebarFragment('position: absolute'); setOpenMenu(findSubItems()); mouseLeaveTopItem(el); expect(el.classList.remove).not.toHaveBeenCalled(); }); }); describe('mouseEnterTopItems', () => { beforeEach(() => { el.innerHTML = mockSidebarFragment( `position: absolute; top: 0; left: 100px; height: ${OLD_SIDEBAR_WIDTH}px;`, ); mockBoundingRects(); }); it('shows sub-items after 0ms if no menu is open', () => { const subItems = findSubItems(); mouseEnterTopItems(el); expect(getHideSubItemsInterval()).toBe(0); return new Promise((resolve) => { setTimeout(() => { expect(subItems.style.display).toBe('block'); resolve(); }); }); }); it('shows sub-items after 300ms if a menu is currently open', () => { const subItems = findSubItems(); documentMouseMove({ clientX: el.getBoundingClientRect().left, clientY: el.getBoundingClientRect().top, }); setOpenMenu(subItems); documentMouseMove({ clientX: el.getBoundingClientRect().left + 20, clientY: el.getBoundingClientRect().top + 10, }); mouseEnterTopItems(el, 0); return new Promise((resolve) => { setTimeout(() => { expect(subItems.style.display).toBe('block'); resolve(); }); }); }); }); describe('showSubLevelItems', () => { beforeEach(() => { el.innerHTML = mockSidebarFragment('position: absolute'); }); it('adds is-over class to el', () => { jest.spyOn(el.classList, 'add'); showSubLevelItems(el); expect(el.classList.add).toHaveBeenCalledWith('is-over'); }); it('does not show sub-items on mobile', () => { breakpointSize = 'xs'; showSubLevelItems(el); expect(findSubItems().style.display).not.toBe('block'); }); it('shows sub-items', () => { showSubLevelItems(el); expect(findSubItems().style.display).toBe('block'); }); it('shows collapsed only sub-items if icon only sidebar', () => { const subItems = findSubItems(); const sidebar = document.createElement('div'); sidebar.classList.add(SIDEBAR_COLLAPSED_CLASS); subItems.classList.add('is-fly-out-only'); setSidebar(sidebar); showSubLevelItems(el); expect(findSubItems().style.display).toBe('block'); }); it('does not show collapsed only sub-items if icon only sidebar', () => { const subItems = findSubItems(); subItems.classList.add('is-fly-out-only'); showSubLevelItems(el); expect(subItems.style.display).not.toBe('block'); }); it('sets transform of sub-items', () => { const sidebar = document.createElement('div'); const subItems = findSubItems(); sidebar.style.width = `${OLD_SIDEBAR_WIDTH}px`; document.body.appendChild(sidebar); setSidebar(sidebar); showSubLevelItems(el); expect(subItems.style.transform).toBe( `translate3d(${OLD_SIDEBAR_WIDTH}px, ${ Math.floor(el.getBoundingClientRect().top) - getHeaderHeight() }px, 0)`, ); }); it('sets is-above when element is above', () => { const subItems = findSubItems(); mockBoundingRects(); subItems.style.height = `${window.innerHeight + el.offsetHeight}px`; el.style.top = `${window.innerHeight - el.offsetHeight}px`; jest.spyOn(subItems.classList, 'add'); showSubLevelItems(el); expect(subItems.classList.add).toHaveBeenCalledWith('is-above'); }); }); describe('canShowSubItems', () => { it('returns true if on desktop size', () => { expect(canShowSubItems()).toBe(true); }); it('returns false if on mobile size', () => { breakpointSize = 'xs'; expect(canShowSubItems()).toBe(false); }); }); describe('canShowActiveSubItems', () => { it('returns true by default', () => { expect(canShowActiveSubItems(el)).toBe(true); }); it('returns false when active & expanded sidebar', () => { const sidebar = document.createElement('div'); el.classList.add('active'); setSidebar(sidebar); expect(canShowActiveSubItems(el)).toBe(false); }); it('returns true when active & collapsed sidebar', () => { const sidebar = document.createElement('div'); sidebar.classList.add(SIDEBAR_COLLAPSED_CLASS); el.classList.add('active'); setSidebar(sidebar); expect(canShowActiveSubItems(el)).toBe(true); }); }); describe('subItemsMouseLeave', () => { beforeEach(() => { el.innerHTML = mockSidebarFragment('position: absolute'); setOpenMenu(findSubItems()); }); it('hides subMenu if element is not hovered', () => { subItemsMouseLeave(el); expect(getOpenMenu()).toBeNull(); }); it('does not hide subMenu if element is hovered', () => { el.classList.add('is-over'); subItemsMouseLeave(el); expect(getOpenMenu()).not.toBeNull(); }); }); });