diff options
Diffstat (limited to 'spec/frontend/nav')
-rw-r--r-- | spec/frontend/nav/components/top_nav_app_spec.js | 68 | ||||
-rw-r--r-- | spec/frontend/nav/components/top_nav_container_view_spec.js | 114 | ||||
-rw-r--r-- | spec/frontend/nav/components/top_nav_dropdown_menu_spec.js | 157 | ||||
-rw-r--r-- | spec/frontend/nav/components/top_nav_menu_item_spec.js | 74 | ||||
-rw-r--r-- | spec/frontend/nav/mock_data.js | 35 |
5 files changed, 448 insertions, 0 deletions
diff --git a/spec/frontend/nav/components/top_nav_app_spec.js b/spec/frontend/nav/components/top_nav_app_spec.js new file mode 100644 index 00000000000..06700ce748e --- /dev/null +++ b/spec/frontend/nav/components/top_nav_app_spec.js @@ -0,0 +1,68 @@ +import { GlNavItemDropdown, GlTooltip } from '@gitlab/ui'; +import { shallowMount, mount } from '@vue/test-utils'; +import TopNavApp from '~/nav/components/top_nav_app.vue'; +import TopNavDropdownMenu from '~/nav/components/top_nav_dropdown_menu.vue'; +import { TEST_NAV_DATA } from '../mock_data'; + +describe('~/nav/components/top_nav_app.vue', () => { + let wrapper; + + const createComponent = (mountFn = shallowMount) => { + wrapper = mountFn(TopNavApp, { + propsData: { + navData: TEST_NAV_DATA, + }, + }); + }; + + const findNavItemDropdown = () => wrapper.findComponent(GlNavItemDropdown); + const findMenu = () => wrapper.findComponent(TopNavDropdownMenu); + const findTooltip = () => wrapper.findComponent(GlTooltip); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('default', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders nav item dropdown', () => { + expect(findNavItemDropdown().attributes('href')).toBeUndefined(); + expect(findNavItemDropdown().attributes()).toMatchObject({ + icon: 'dot-grid', + text: TEST_NAV_DATA.activeTitle, + 'no-flip': '', + }); + }); + + it('renders top nav dropdown menu', () => { + expect(findMenu().props()).toStrictEqual({ + primary: TEST_NAV_DATA.primary, + secondary: TEST_NAV_DATA.secondary, + views: TEST_NAV_DATA.views, + }); + }); + + it('renders tooltip', () => { + expect(findTooltip().attributes()).toMatchObject({ + 'boundary-padding': '0', + placement: 'right', + title: TopNavApp.TOOLTIP, + }); + }); + }); + + describe('when full mounted', () => { + beforeEach(() => { + createComponent(mount); + }); + + it('has dropdown toggle as tooltip target', () => { + const targetFn = findTooltip().props('target'); + + expect(targetFn()).toBe(wrapper.find('.js-top-nav-dropdown-toggle').element); + }); + }); +}); diff --git a/spec/frontend/nav/components/top_nav_container_view_spec.js b/spec/frontend/nav/components/top_nav_container_view_spec.js new file mode 100644 index 00000000000..b08d75f36ce --- /dev/null +++ b/spec/frontend/nav/components/top_nav_container_view_spec.js @@ -0,0 +1,114 @@ +import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; +import FrequentItemsApp from '~/frequent_items/components/app.vue'; +import { FREQUENT_ITEMS_PROJECTS } from '~/frequent_items/constants'; +import eventHub from '~/frequent_items/event_hub'; +import TopNavContainerView from '~/nav/components/top_nav_container_view.vue'; +import TopNavMenuItem from '~/nav/components/top_nav_menu_item.vue'; +import VuexModuleProvider from '~/vue_shared/components/vuex_module_provider.vue'; +import { TEST_NAV_DATA } from '../mock_data'; + +const DEFAULT_PROPS = { + frequentItemsDropdownType: FREQUENT_ITEMS_PROJECTS.namespace, + frequentItemsVuexModule: FREQUENT_ITEMS_PROJECTS.vuexModule, + linksPrimary: TEST_NAV_DATA.primary, + linksSecondary: TEST_NAV_DATA.secondary, +}; +const TEST_OTHER_PROPS = { + namespace: 'projects', + currentUserName: '', + currentItem: {}, +}; + +describe('~/nav/components/top_nav_container_view.vue', () => { + let wrapper; + + const createComponent = (props = {}) => { + wrapper = shallowMount(TopNavContainerView, { + propsData: { + ...DEFAULT_PROPS, + ...TEST_OTHER_PROPS, + ...props, + }, + }); + }; + + const findMenuItems = (parent = wrapper) => parent.findAll(TopNavMenuItem); + const findMenuItemsModel = (parent = wrapper) => + findMenuItems(parent).wrappers.map((x) => x.props()); + const findMenuItemGroups = () => wrapper.findAll('[data-testid="menu-item-group"]'); + const findMenuItemGroupsModel = () => findMenuItemGroups().wrappers.map(findMenuItemsModel); + const findFrequentItemsApp = () => { + const parent = wrapper.findComponent(VuexModuleProvider); + + return { + vuexModule: parent.props('vuexModule'), + props: parent.findComponent(FrequentItemsApp).props(), + }; + }; + + afterEach(() => { + wrapper.destroy(); + }); + + it.each(['projects', 'groups'])( + 'emits frequent items event to event hub (%s)', + async (frequentItemsDropdownType) => { + const listener = jest.fn(); + eventHub.$on(`${frequentItemsDropdownType}-dropdownOpen`, listener); + createComponent({ frequentItemsDropdownType }); + + expect(listener).not.toHaveBeenCalled(); + + await nextTick(); + + expect(listener).toHaveBeenCalled(); + }, + ); + + describe('default', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders frequent items app', () => { + expect(findFrequentItemsApp()).toEqual({ + vuexModule: DEFAULT_PROPS.frequentItemsVuexModule, + props: TEST_OTHER_PROPS, + }); + }); + + it('renders menu item groups', () => { + expect(findMenuItemGroupsModel()).toEqual([ + TEST_NAV_DATA.primary.map((menuItem) => ({ menuItem })), + TEST_NAV_DATA.secondary.map((menuItem) => ({ menuItem })), + ]); + }); + + it('only the first group does not have margin top', () => { + expect(findMenuItemGroups().wrappers.map((x) => x.classes('gl-mt-3'))).toEqual([false, true]); + }); + + it('only the first menu item does not have margin top', () => { + const actual = findMenuItems(findMenuItemGroups().at(1)).wrappers.map((x) => + x.classes('gl-mt-1'), + ); + + expect(actual).toEqual([false, ...TEST_NAV_DATA.secondary.slice(1).fill(true)]); + }); + }); + + describe('without secondary links', () => { + beforeEach(() => { + createComponent({ + linksSecondary: [], + }); + }); + + it('renders one menu item group', () => { + expect(findMenuItemGroupsModel()).toEqual([ + TEST_NAV_DATA.primary.map((menuItem) => ({ menuItem })), + ]); + }); + }); +}); diff --git a/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js b/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js new file mode 100644 index 00000000000..d9bba22238a --- /dev/null +++ b/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js @@ -0,0 +1,157 @@ +import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; +import TopNavDropdownMenu from '~/nav/components/top_nav_dropdown_menu.vue'; +import KeepAliveSlots from '~/vue_shared/components/keep_alive_slots.vue'; +import { TEST_NAV_DATA } from '../mock_data'; + +const SECONDARY_GROUP_CLASSES = TopNavDropdownMenu.SECONDARY_GROUP_CLASS.split(' '); + +describe('~/nav/components/top_nav_dropdown_menu.vue', () => { + let wrapper; + + const createComponent = (props = {}) => { + wrapper = shallowMount(TopNavDropdownMenu, { + propsData: { + primary: TEST_NAV_DATA.primary, + secondary: TEST_NAV_DATA.secondary, + views: TEST_NAV_DATA.views, + ...props, + }, + }); + }; + + const findMenuItems = (parent = wrapper) => parent.findAll('[data-testid="menu-item"]'); + const findMenuItemsModel = (parent = wrapper) => + findMenuItems(parent).wrappers.map((x) => ({ + menuItem: x.props('menuItem'), + isActive: x.classes('active'), + })); + const findMenuItemGroups = () => wrapper.findAll('[data-testid="menu-item-group"]'); + const findMenuItemGroupsModel = () => + findMenuItemGroups().wrappers.map((x) => ({ + classes: x.classes(), + items: findMenuItemsModel(x), + })); + const findMenuSidebar = () => wrapper.find('[data-testid="menu-sidebar"]'); + const findMenuSubview = () => wrapper.findComponent(KeepAliveSlots); + const hasFullWidthMenuSidebar = () => findMenuSidebar().classes('gl-w-full'); + + const createItemsGroupModelExpectation = ({ + primary = TEST_NAV_DATA.primary, + secondary = TEST_NAV_DATA.secondary, + activeIndex = -1, + } = {}) => [ + { + classes: [], + items: primary.map((menuItem, index) => ({ isActive: index === activeIndex, menuItem })), + }, + { + classes: SECONDARY_GROUP_CLASSES, + items: secondary.map((menuItem) => ({ isActive: false, menuItem })), + }, + ]; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('default', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders menu item groups', () => { + expect(findMenuItemGroupsModel()).toEqual(createItemsGroupModelExpectation()); + }); + + it('has full width menu sidebar', () => { + expect(hasFullWidthMenuSidebar()).toBe(true); + }); + + it('renders hidden subview with no slot key', () => { + const subview = findMenuSubview(); + + expect(subview.isVisible()).toBe(false); + expect(subview.props()).toEqual({ slotKey: '' }); + }); + + it('the first menu item in a group does not render margin top', () => { + const actual = findMenuItems(findMenuItemGroups().at(0)).wrappers.map((x) => + x.classes('gl-mt-1'), + ); + + expect(actual).toEqual([false, ...TEST_NAV_DATA.primary.slice(1).fill(true)]); + }); + }); + + describe('with pre-initialized active view', () => { + const primaryWithActive = [ + TEST_NAV_DATA.primary[0], + { + ...TEST_NAV_DATA.primary[1], + active: true, + }, + ...TEST_NAV_DATA.primary.slice(2), + ]; + + beforeEach(() => { + createComponent({ + primary: primaryWithActive, + }); + }); + + it('renders menu item groups', () => { + expect(findMenuItemGroupsModel()).toEqual( + createItemsGroupModelExpectation({ primary: primaryWithActive, activeIndex: 1 }), + ); + }); + + it('does not have full width menu sidebar', () => { + expect(hasFullWidthMenuSidebar()).toBe(false); + }); + + it('renders visible subview with slot key', () => { + const subview = findMenuSubview(); + + expect(subview.isVisible()).toBe(true); + expect(subview.props('slotKey')).toBe(primaryWithActive[1].view); + }); + + it('does not change view if non-view menu item is clicked', async () => { + const secondaryLink = findMenuItems().at(primaryWithActive.length); + + // Ensure this doesn't have a view + expect(secondaryLink.props('menuItem').view).toBeUndefined(); + + secondaryLink.vm.$emit('click'); + + await nextTick(); + + expect(findMenuSubview().props('slotKey')).toBe(primaryWithActive[1].view); + }); + + describe('when other view menu item is clicked', () => { + let primaryLink; + + beforeEach(async () => { + primaryLink = findMenuItems().at(0); + primaryLink.vm.$emit('click'); + await nextTick(); + }); + + it('clicked on link with view', () => { + expect(primaryLink.props('menuItem').view).toBeTruthy(); + }); + + it('changes active view', () => { + expect(findMenuSubview().props('slotKey')).toBe(primaryWithActive[0].view); + }); + + it('changes active status on menu item', () => { + expect(findMenuItemGroupsModel()).toStrictEqual( + createItemsGroupModelExpectation({ primary: primaryWithActive, activeIndex: 0 }), + ); + }); + }); + }); +}); diff --git a/spec/frontend/nav/components/top_nav_menu_item_spec.js b/spec/frontend/nav/components/top_nav_menu_item_spec.js new file mode 100644 index 00000000000..579af13d08a --- /dev/null +++ b/spec/frontend/nav/components/top_nav_menu_item_spec.js @@ -0,0 +1,74 @@ +import { GlButton, GlIcon } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import TopNavMenuItem from '~/nav/components/top_nav_menu_item.vue'; + +const TEST_MENU_ITEM = { + title: 'Cheeseburger', + icon: 'search', + href: '/pretty/good/burger', + view: 'burger-view', +}; + +describe('~/nav/components/top_nav_menu_item.vue', () => { + let listener; + let wrapper; + + const createComponent = (props = {}) => { + wrapper = shallowMount(TopNavMenuItem, { + propsData: { + menuItem: TEST_MENU_ITEM, + ...props, + }, + listeners: { + click: listener, + }, + }); + }; + + const findButton = () => wrapper.find(GlButton); + const findButtonIcons = () => + findButton() + .findAllComponents(GlIcon) + .wrappers.map((x) => x.props('name')); + + beforeEach(() => { + listener = jest.fn(); + }); + + describe('default', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders button href and text', () => { + const button = findButton(); + + expect(button.attributes('href')).toBe(TEST_MENU_ITEM.href); + expect(button.text()).toBe(TEST_MENU_ITEM.title); + }); + + it('passes listeners to button', () => { + expect(listener).not.toHaveBeenCalled(); + + findButton().vm.$emit('click', 'TEST'); + + expect(listener).toHaveBeenCalledWith('TEST'); + }); + }); + + describe.each` + desc | menuItem | expectedIcons + ${'default'} | ${TEST_MENU_ITEM} | ${[TEST_MENU_ITEM.icon, 'chevron-right']} + ${'with no icon'} | ${{ ...TEST_MENU_ITEM, icon: null }} | ${['chevron-right']} + ${'with no view'} | ${{ ...TEST_MENU_ITEM, view: null }} | ${[TEST_MENU_ITEM.icon]} + ${'with no icon or view'} | ${{ ...TEST_MENU_ITEM, view: null, icon: null }} | ${[]} + `('$desc', ({ menuItem, expectedIcons }) => { + beforeEach(() => { + createComponent({ menuItem }); + }); + + it(`renders expected icons ${JSON.stringify(expectedIcons)}`, () => { + expect(findButtonIcons()).toEqual(expectedIcons); + }); + }); +}); diff --git a/spec/frontend/nav/mock_data.js b/spec/frontend/nav/mock_data.js new file mode 100644 index 00000000000..2987d8deb16 --- /dev/null +++ b/spec/frontend/nav/mock_data.js @@ -0,0 +1,35 @@ +import { range } from 'lodash'; + +export const TEST_NAV_DATA = { + activeTitle: 'Test Active Title', + primary: [ + ...['projects', 'groups'].map((view) => ({ + id: view, + href: null, + title: view, + view, + })), + ...range(0, 2).map((idx) => ({ + id: `primary-link-${idx}`, + href: `/path/to/primary/${idx}`, + title: `Title ${idx}`, + })), + ], + secondary: range(0, 2).map((idx) => ({ + id: `secondary-link-${idx}`, + href: `/path/to/secondary/${idx}`, + title: `SecTitle ${idx}`, + })), + views: { + projects: { + namespace: 'projects', + currentUserName: '', + currentItem: {}, + }, + groups: { + namespace: 'groups', + currentUserName: '', + currentItem: {}, + }, + }, +}; |