diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-06-16 18:25:58 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-06-16 18:25:58 +0000 |
commit | a5f4bba440d7f9ea47046a0a561d49adf0a1e6d4 (patch) | |
tree | fb69158581673816a8cd895f9d352dcb3c678b1e /spec/frontend/nav | |
parent | d16b2e8639e99961de6ddc93909f3bb5c1445ba1 (diff) | |
download | gitlab-ce-a5f4bba440d7f9ea47046a0a561d49adf0a1e6d4.tar.gz |
Add latest changes from gitlab-org/gitlab@14-0-stable-eev14.0.0-rc42
Diffstat (limited to 'spec/frontend/nav')
-rw-r--r-- | spec/frontend/nav/components/responsive_app_spec.js | 173 | ||||
-rw-r--r-- | spec/frontend/nav/components/responsive_header_spec.js | 67 | ||||
-rw-r--r-- | spec/frontend/nav/components/responsive_home_spec.js | 137 | ||||
-rw-r--r-- | spec/frontend/nav/components/top_nav_app_spec.js | 31 | ||||
-rw-r--r-- | spec/frontend/nav/components/top_nav_container_view_spec.js | 60 | ||||
-rw-r--r-- | spec/frontend/nav/components/top_nav_dropdown_menu_spec.js | 121 | ||||
-rw-r--r-- | spec/frontend/nav/components/top_nav_menu_item_spec.js | 76 | ||||
-rw-r--r-- | spec/frontend/nav/components/top_nav_menu_sections_spec.js | 107 | ||||
-rw-r--r-- | spec/frontend/nav/components/top_nav_new_dropdown_spec.js | 122 | ||||
-rw-r--r-- | spec/frontend/nav/mock_data.js | 4 |
10 files changed, 777 insertions, 121 deletions
diff --git a/spec/frontend/nav/components/responsive_app_spec.js b/spec/frontend/nav/components/responsive_app_spec.js new file mode 100644 index 00000000000..7221ea2c5cd --- /dev/null +++ b/spec/frontend/nav/components/responsive_app_spec.js @@ -0,0 +1,173 @@ +import { shallowMount } from '@vue/test-utils'; +import ResponsiveApp from '~/nav/components/responsive_app.vue'; +import ResponsiveHeader from '~/nav/components/responsive_header.vue'; +import ResponsiveHome from '~/nav/components/responsive_home.vue'; +import TopNavContainerView from '~/nav/components/top_nav_container_view.vue'; +import eventHub, { EVENT_RESPONSIVE_TOGGLE } from '~/nav/event_hub'; +import { resetMenuItemsActive } from '~/nav/utils/reset_menu_items_active'; +import KeepAliveSlots from '~/vue_shared/components/keep_alive_slots.vue'; +import { TEST_NAV_DATA } from '../mock_data'; + +const HTML_HEADER_CONTENT = '<div class="header-content"></div>'; +const HTML_MENU_EXPANDED = '<div class="menu-expanded"></div>'; +const HTML_HEADER_WITH_MENU_EXPANDED = + '<div></div><div class="header-content menu-expanded"></div>'; + +describe('~/nav/components/responsive_app.vue', () => { + let wrapper; + + const createComponent = () => { + wrapper = shallowMount(ResponsiveApp, { + propsData: { + navData: TEST_NAV_DATA, + }, + stubs: { + KeepAliveSlots, + }, + }); + }; + const triggerResponsiveToggle = () => eventHub.$emit(EVENT_RESPONSIVE_TOGGLE); + + const findHome = () => wrapper.findComponent(ResponsiveHome); + const findMobileOverlay = () => wrapper.find('[data-testid="mobile-overlay"]'); + const findSubviewHeader = () => wrapper.findComponent(ResponsiveHeader); + const findSubviewContainer = () => wrapper.findComponent(TopNavContainerView); + const hasBodyResponsiveOpen = () => document.body.classList.contains('top-nav-responsive-open'); + const hasMobileOverlayVisible = () => findMobileOverlay().classes('mobile-nav-open'); + + beforeEach(() => { + document.body.innerHTML = ''; + // Add test class to reset state + assert that we're adding classes correctly + document.body.className = 'test-class'; + }); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('default', () => { + beforeEach(() => { + createComponent(); + }); + + it('shows home by default', () => { + expect(findHome().isVisible()).toBe(true); + expect(findHome().props()).toEqual({ + navData: resetMenuItemsActive(TEST_NAV_DATA), + }); + }); + + it.each` + bodyHtml | expectation + ${''} | ${false} + ${HTML_HEADER_CONTENT} | ${false} + ${HTML_MENU_EXPANDED} | ${false} + ${HTML_HEADER_WITH_MENU_EXPANDED} | ${true} + `( + 'with responsive toggle event and html set to $bodyHtml, responsive open = $expectation', + ({ bodyHtml, expectation }) => { + document.body.innerHTML = bodyHtml; + + triggerResponsiveToggle(); + + expect(hasBodyResponsiveOpen()).toBe(expectation); + }, + ); + + it.each` + events | expectation + ${[]} | ${false} + ${['bv::dropdown::show']} | ${true} + ${['bv::dropdown::show', 'bv::dropdown::hide']} | ${false} + `( + 'with root events $events, movile overlay visible = $expectation', + async ({ events, expectation }) => { + // `await...reduce(async` is like doing an `forEach(async (...))` excpet it works + await events.reduce(async (acc, evt) => { + await acc; + + wrapper.vm.$root.$emit(evt); + + await wrapper.vm.$nextTick(); + }, Promise.resolve()); + + expect(hasMobileOverlayVisible()).toBe(expectation); + }, + ); + }); + + describe('with menu expanded in body', () => { + beforeEach(() => { + document.body.innerHTML = HTML_HEADER_WITH_MENU_EXPANDED; + createComponent(); + }); + + it('sets the body responsive open', () => { + expect(hasBodyResponsiveOpen()).toBe(true); + }); + }); + + const projectsContainerProps = { + containerClass: 'gl-px-3', + frequentItemsDropdownType: ResponsiveApp.FREQUENT_ITEMS_PROJECTS.namespace, + frequentItemsVuexModule: ResponsiveApp.FREQUENT_ITEMS_PROJECTS.vuexModule, + linksPrimary: TEST_NAV_DATA.views.projects.linksPrimary, + linksSecondary: TEST_NAV_DATA.views.projects.linksSecondary, + }; + const groupsContainerProps = { + containerClass: 'gl-px-3', + frequentItemsDropdownType: ResponsiveApp.FREQUENT_ITEMS_GROUPS.namespace, + frequentItemsVuexModule: ResponsiveApp.FREQUENT_ITEMS_GROUPS.vuexModule, + linksPrimary: TEST_NAV_DATA.views.groups.linksPrimary, + linksSecondary: TEST_NAV_DATA.views.groups.linksSecondary, + }; + + describe.each` + view | header | containerProps + ${'projects'} | ${'Projects'} | ${projectsContainerProps} + ${'groups'} | ${'Groups'} | ${groupsContainerProps} + `('when menu item with $view is clicked', ({ view, header, containerProps }) => { + beforeEach(async () => { + createComponent(); + + findHome().vm.$emit('menu-item-click', { view }); + + await wrapper.vm.$nextTick(); + }); + + it('shows header', () => { + expect(findSubviewHeader().text()).toBe(header); + }); + + it('shows container subview', () => { + expect(findSubviewContainer().props()).toEqual(containerProps); + }); + + it('hides home', () => { + expect(findHome().isVisible()).toBe(false); + }); + + describe('when header back button is clicked', () => { + beforeEach(() => { + findSubviewHeader().vm.$emit('menu-item-click', { view: 'home' }); + }); + + it('shows home', () => { + expect(findHome().isVisible()).toBe(true); + }); + }); + }); + + describe('when destroyed', () => { + beforeEach(() => { + createComponent(); + wrapper.destroy(); + }); + + it('responsive toggle event does nothing', () => { + triggerResponsiveToggle(); + + expect(hasBodyResponsiveOpen()).toBe(false); + }); + }); +}); diff --git a/spec/frontend/nav/components/responsive_header_spec.js b/spec/frontend/nav/components/responsive_header_spec.js new file mode 100644 index 00000000000..937c44727c7 --- /dev/null +++ b/spec/frontend/nav/components/responsive_header_spec.js @@ -0,0 +1,67 @@ +import { shallowMount } from '@vue/test-utils'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; +import ResponsiveHeader from '~/nav/components/responsive_header.vue'; +import TopNavMenuItem from '~/nav/components/top_nav_menu_item.vue'; + +const TEST_SLOT_CONTENT = 'Test slot content'; + +describe('~/nav/components/top_nav_menu_sections.vue', () => { + let wrapper; + + const createComponent = () => { + wrapper = shallowMount(ResponsiveHeader, { + slots: { + default: TEST_SLOT_CONTENT, + }, + directives: { + GlTooltip: createMockDirective(), + }, + }); + }; + + const findMenuItem = () => wrapper.findComponent(TopNavMenuItem); + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders slot', () => { + expect(wrapper.text()).toBe(TEST_SLOT_CONTENT); + }); + + it('renders back button', () => { + const button = findMenuItem(); + + const tooltip = getBinding(button.element, 'gl-tooltip').value.title; + + expect(tooltip).toBe('Go back'); + expect(button.props()).toEqual({ + menuItem: { + id: 'home', + view: 'home', + icon: 'angle-left', + }, + iconOnly: true, + }); + }); + + it('emits nothing', () => { + expect(wrapper.emitted()).toEqual({}); + }); + + describe('when back button is clicked', () => { + beforeEach(() => { + findMenuItem().vm.$emit('click'); + }); + + it('emits menu-item-click', () => { + expect(wrapper.emitted()).toEqual({ + 'menu-item-click': [[{ id: 'home', view: 'home', icon: 'angle-left' }]], + }); + }); + }); +}); diff --git a/spec/frontend/nav/components/responsive_home_spec.js b/spec/frontend/nav/components/responsive_home_spec.js new file mode 100644 index 00000000000..8f198d92747 --- /dev/null +++ b/spec/frontend/nav/components/responsive_home_spec.js @@ -0,0 +1,137 @@ +import { shallowMount } from '@vue/test-utils'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; +import ResponsiveHome from '~/nav/components/responsive_home.vue'; +import TopNavMenuItem from '~/nav/components/top_nav_menu_item.vue'; +import TopNavMenuSections from '~/nav/components/top_nav_menu_sections.vue'; +import TopNavNewDropdown from '~/nav/components/top_nav_new_dropdown.vue'; +import { TEST_NAV_DATA } from '../mock_data'; + +const TEST_SEARCH_MENU_ITEM = { + id: 'search', + title: 'search', + icon: 'search', + href: '/search', +}; + +const TEST_NEW_DROPDOWN_VIEW_MODEL = { + title: 'new', + menu_sections: [], +}; + +describe('~/nav/components/responsive_home.vue', () => { + let wrapper; + let menuItemClickListener; + + const createComponent = (props = {}) => { + wrapper = shallowMount(ResponsiveHome, { + propsData: { + navData: TEST_NAV_DATA, + ...props, + }, + directives: { + GlTooltip: createMockDirective(), + }, + listeners: { + 'menu-item-click': menuItemClickListener, + }, + }); + }; + + const findSearchMenuItem = () => wrapper.findComponent(TopNavMenuItem); + const findNewDropdown = () => wrapper.findComponent(TopNavNewDropdown); + const findMenuSections = () => wrapper.findComponent(TopNavMenuSections); + + beforeEach(() => { + menuItemClickListener = jest.fn(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('default', () => { + beforeEach(() => { + createComponent(); + }); + + it.each` + desc | fn + ${'does not show search menu item'} | ${findSearchMenuItem} + ${'does not show new dropdown'} | ${findNewDropdown} + `('$desc', ({ fn }) => { + expect(fn().exists()).toBe(false); + }); + + it('shows menu sections', () => { + expect(findMenuSections().props('sections')).toEqual([ + { id: 'primary', menuItems: TEST_NAV_DATA.primary }, + { id: 'secondary', menuItems: TEST_NAV_DATA.secondary }, + ]); + }); + + it('emits when menu sections emits', () => { + expect(menuItemClickListener).not.toHaveBeenCalled(); + + findMenuSections().vm.$emit('menu-item-click', TEST_NAV_DATA.primary[0]); + + expect(menuItemClickListener).toHaveBeenCalledWith(TEST_NAV_DATA.primary[0]); + }); + }); + + describe('without secondary', () => { + beforeEach(() => { + createComponent({ navData: { ...TEST_NAV_DATA, secondary: null } }); + }); + + it('shows menu sections', () => { + expect(findMenuSections().props('sections')).toEqual([ + { id: 'primary', menuItems: TEST_NAV_DATA.primary }, + ]); + }); + }); + + describe('with search view', () => { + beforeEach(() => { + createComponent({ + navData: { + ...TEST_NAV_DATA, + views: { search: TEST_SEARCH_MENU_ITEM }, + }, + }); + }); + + it('shows search menu item', () => { + expect(findSearchMenuItem().props()).toEqual({ + menuItem: TEST_SEARCH_MENU_ITEM, + iconOnly: true, + }); + }); + + it('shows tooltip for search', () => { + const tooltip = getBinding(findSearchMenuItem().element, 'gl-tooltip'); + expect(tooltip.value).toEqual({ title: TEST_SEARCH_MENU_ITEM.title }); + }); + }); + + describe('with new view', () => { + beforeEach(() => { + createComponent({ + navData: { + ...TEST_NAV_DATA, + views: { new: TEST_NEW_DROPDOWN_VIEW_MODEL }, + }, + }); + }); + + it('shows new dropdown', () => { + expect(findNewDropdown().props()).toEqual({ + viewModel: TEST_NEW_DROPDOWN_VIEW_MODEL, + }); + }); + + it('shows tooltip for new dropdown', () => { + const tooltip = getBinding(findNewDropdown().element, 'gl-tooltip'); + expect(tooltip.value).toEqual({ title: TEST_NEW_DROPDOWN_VIEW_MODEL.title }); + }); + }); +}); diff --git a/spec/frontend/nav/components/top_nav_app_spec.js b/spec/frontend/nav/components/top_nav_app_spec.js index 06700ce748e..1d6ea99155b 100644 --- a/spec/frontend/nav/components/top_nav_app_spec.js +++ b/spec/frontend/nav/components/top_nav_app_spec.js @@ -1,5 +1,5 @@ -import { GlNavItemDropdown, GlTooltip } from '@gitlab/ui'; -import { shallowMount, mount } from '@vue/test-utils'; +import { GlNavItemDropdown } from '@gitlab/ui'; +import { shallowMount } 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'; @@ -7,8 +7,8 @@ import { TEST_NAV_DATA } from '../mock_data'; describe('~/nav/components/top_nav_app.vue', () => { let wrapper; - const createComponent = (mountFn = shallowMount) => { - wrapper = mountFn(TopNavApp, { + const createComponent = () => { + wrapper = shallowMount(TopNavApp, { propsData: { navData: TEST_NAV_DATA, }, @@ -17,7 +17,6 @@ describe('~/nav/components/top_nav_app.vue', () => { const findNavItemDropdown = () => wrapper.findComponent(GlNavItemDropdown); const findMenu = () => wrapper.findComponent(TopNavDropdownMenu); - const findTooltip = () => wrapper.findComponent(GlTooltip); afterEach(() => { wrapper.destroy(); @@ -31,7 +30,7 @@ describe('~/nav/components/top_nav_app.vue', () => { it('renders nav item dropdown', () => { expect(findNavItemDropdown().attributes('href')).toBeUndefined(); expect(findNavItemDropdown().attributes()).toMatchObject({ - icon: 'dot-grid', + icon: 'hamburger', text: TEST_NAV_DATA.activeTitle, 'no-flip': '', }); @@ -44,25 +43,5 @@ describe('~/nav/components/top_nav_app.vue', () => { 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 index b08d75f36ce..06d2179b859 100644 --- a/spec/frontend/nav/components/top_nav_container_view_spec.js +++ b/spec/frontend/nav/components/top_nav_container_view_spec.js @@ -4,7 +4,7 @@ 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 TopNavMenuSections from '~/nav/components/top_nav_menu_sections.vue'; import VuexModuleProvider from '~/vue_shared/components/vuex_module_provider.vue'; import { TEST_NAV_DATA } from '../mock_data'; @@ -13,39 +13,39 @@ const DEFAULT_PROPS = { frequentItemsVuexModule: FREQUENT_ITEMS_PROJECTS.vuexModule, linksPrimary: TEST_NAV_DATA.primary, linksSecondary: TEST_NAV_DATA.secondary, + containerClass: 'test-frequent-items-container-class', }; const TEST_OTHER_PROPS = { namespace: 'projects', - currentUserName: '', - currentItem: {}, + currentUserName: 'test-user', + currentItem: { id: 'test' }, }; describe('~/nav/components/top_nav_container_view.vue', () => { let wrapper; - const createComponent = (props = {}) => { + const createComponent = (props = {}, options = {}) => { wrapper = shallowMount(TopNavContainerView, { propsData: { ...DEFAULT_PROPS, ...TEST_OTHER_PROPS, ...props, }, + ...options, }); }; - 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 findMenuSections = () => wrapper.findComponent(TopNavMenuSections); const findFrequentItemsApp = () => { const parent = wrapper.findComponent(VuexModuleProvider); return { vuexModule: parent.props('vuexModule'), props: parent.findComponent(FrequentItemsApp).props(), + attributes: parent.findComponent(FrequentItemsApp).attributes(), }; }; + const findFrequentItemsContainer = () => wrapper.find('[data-testid="frequent-items-container"]'); afterEach(() => { wrapper.destroy(); @@ -67,34 +67,40 @@ describe('~/nav/components/top_nav_container_view.vue', () => { ); describe('default', () => { + const EXTRA_ATTRS = { 'data-test-attribute': 'foo' }; + beforeEach(() => { - createComponent(); + createComponent({}, { attrs: EXTRA_ATTRS }); + }); + + it('does not inherit extra attrs', () => { + expect(wrapper.attributes()).toEqual({ + class: expect.any(String), + }); }); it('renders frequent items app', () => { expect(findFrequentItemsApp()).toEqual({ vuexModule: DEFAULT_PROPS.frequentItemsVuexModule, - props: TEST_OTHER_PROPS, + props: expect.objectContaining(TEST_OTHER_PROPS), + attributes: expect.objectContaining(EXTRA_ATTRS), }); }); - 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('renders given container class', () => { + expect(findFrequentItemsContainer().classes(DEFAULT_PROPS.containerClass)).toBe(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'), - ); + it('renders menu sections', () => { + const sections = [ + { id: 'primary', menuItems: TEST_NAV_DATA.primary }, + { id: 'secondary', menuItems: TEST_NAV_DATA.secondary }, + ]; - expect(actual).toEqual([false, ...TEST_NAV_DATA.secondary.slice(1).fill(true)]); + expect(findMenuSections().props()).toEqual({ + sections, + withTopBorder: true, + }); }); }); @@ -106,8 +112,8 @@ describe('~/nav/components/top_nav_container_view.vue', () => { }); it('renders one menu item group', () => { - expect(findMenuItemGroupsModel()).toEqual([ - TEST_NAV_DATA.primary.map((menuItem) => ({ menuItem })), + expect(findMenuSections().props('sections')).toEqual([ + { id: 'primary', menuItems: TEST_NAV_DATA.primary }, ]); }); }); diff --git a/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js b/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js index d9bba22238a..70df05a2781 100644 --- a/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js +++ b/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js @@ -1,67 +1,62 @@ -import { shallowMount } from '@vue/test-utils'; +import { shallowMount, mount } from '@vue/test-utils'; import { nextTick } from 'vue'; import TopNavDropdownMenu from '~/nav/components/top_nav_dropdown_menu.vue'; +import TopNavMenuItem from '~/nav/components/top_nav_menu_item.vue'; +import TopNavMenuSections from '~/nav/components/top_nav_menu_sections.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, { + const createComponent = (props = {}, mountFn = shallowMount) => { + wrapper = mountFn(TopNavDropdownMenu, { propsData: { primary: TEST_NAV_DATA.primary, secondary: TEST_NAV_DATA.secondary, views: TEST_NAV_DATA.views, ...props, }, + stubs: { + // Stub the keep-alive-slots so we don't render frequent items which uses a store + KeepAliveSlots: true, + }, }); }; - 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 findMenuItems = () => wrapper.findAllComponents(TopNavMenuItem); + const findMenuSections = () => wrapper.find(TopNavMenuSections); 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 })), - }, - ]; + const withActiveIndex = (menuItems, activeIndex) => + menuItems.map((x, idx) => ({ + ...x, + active: idx === activeIndex, + })); afterEach(() => { wrapper.destroy(); }); + beforeEach(() => { + jest.spyOn(console, 'error').mockImplementation(); + }); + describe('default', () => { beforeEach(() => { createComponent(); }); - it('renders menu item groups', () => { - expect(findMenuItemGroupsModel()).toEqual(createItemsGroupModelExpectation()); + it('renders menu sections', () => { + expect(findMenuSections().props()).toEqual({ + sections: [ + { id: 'primary', menuItems: TEST_NAV_DATA.primary }, + { id: 'secondary', menuItems: TEST_NAV_DATA.secondary }, + ], + withTopBorder: false, + }); }); it('has full width menu sidebar', () => { @@ -74,36 +69,25 @@ describe('~/nav/components/top_nav_dropdown_menu.vue', () => { 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, - }); + // We opt for a small integration test, to make sure the event is handled correctly + // as it would in prod. + createComponent( + { + primary: withActiveIndex(TEST_NAV_DATA.primary, 1), + }, + mount, + ); }); - it('renders menu item groups', () => { - expect(findMenuItemGroupsModel()).toEqual( - createItemsGroupModelExpectation({ primary: primaryWithActive, activeIndex: 1 }), - ); + it('renders menu sections', () => { + expect(findMenuSections().props('sections')).toStrictEqual([ + { id: 'primary', menuItems: withActiveIndex(TEST_NAV_DATA.primary, 1) }, + { id: 'secondary', menuItems: TEST_NAV_DATA.secondary }, + ]); }); it('does not have full width menu sidebar', () => { @@ -114,11 +98,11 @@ describe('~/nav/components/top_nav_dropdown_menu.vue', () => { const subview = findMenuSubview(); expect(subview.isVisible()).toBe(true); - expect(subview.props('slotKey')).toBe(primaryWithActive[1].view); + expect(subview.props('slotKey')).toBe(TEST_NAV_DATA.primary[1].view); }); it('does not change view if non-view menu item is clicked', async () => { - const secondaryLink = findMenuItems().at(primaryWithActive.length); + const secondaryLink = findMenuItems().at(TEST_NAV_DATA.primary.length); // Ensure this doesn't have a view expect(secondaryLink.props('menuItem').view).toBeUndefined(); @@ -127,10 +111,10 @@ describe('~/nav/components/top_nav_dropdown_menu.vue', () => { await nextTick(); - expect(findMenuSubview().props('slotKey')).toBe(primaryWithActive[1].view); + expect(findMenuSubview().props('slotKey')).toBe(TEST_NAV_DATA.primary[1].view); }); - describe('when other view menu item is clicked', () => { + describe('when menu item is clicked', () => { let primaryLink; beforeEach(async () => { @@ -144,13 +128,20 @@ describe('~/nav/components/top_nav_dropdown_menu.vue', () => { }); it('changes active view', () => { - expect(findMenuSubview().props('slotKey')).toBe(primaryWithActive[0].view); + expect(findMenuSubview().props('slotKey')).toBe(TEST_NAV_DATA.primary[0].view); }); it('changes active status on menu item', () => { - expect(findMenuItemGroupsModel()).toStrictEqual( - createItemsGroupModelExpectation({ primary: primaryWithActive, activeIndex: 0 }), - ); + expect(findMenuSections().props('sections')).toStrictEqual([ + { + id: 'primary', + menuItems: withActiveIndex(TEST_NAV_DATA.primary, 0), + }, + { + id: 'secondary', + menuItems: withActiveIndex(TEST_NAV_DATA.secondary, -1), + }, + ]); }); }); }); diff --git a/spec/frontend/nav/components/top_nav_menu_item_spec.js b/spec/frontend/nav/components/top_nav_menu_item_spec.js index 579af13d08a..fd2b4d3b056 100644 --- a/spec/frontend/nav/components/top_nav_menu_item_spec.js +++ b/spec/frontend/nav/components/top_nav_menu_item_spec.js @@ -7,6 +7,7 @@ const TEST_MENU_ITEM = { icon: 'search', href: '/pretty/good/burger', view: 'burger-view', + data: { qa_selector: 'not-a-real-selector', method: 'post', testFoo: 'test' }, }; describe('~/nav/components/top_nav_menu_item.vue', () => { @@ -29,7 +30,10 @@ describe('~/nav/components/top_nav_menu_item.vue', () => { const findButtonIcons = () => findButton() .findAllComponents(GlIcon) - .wrappers.map((x) => x.props('name')); + .wrappers.map((x) => ({ + name: x.props('name'), + classes: x.classes(), + })); beforeEach(() => { listener = jest.fn(); @@ -47,6 +51,16 @@ describe('~/nav/components/top_nav_menu_item.vue', () => { expect(button.text()).toBe(TEST_MENU_ITEM.title); }); + it('renders button data attributes', () => { + const button = findButton(); + + expect(button.attributes()).toMatchObject({ + 'data-qa-selector': TEST_MENU_ITEM.data.qa_selector, + 'data-method': TEST_MENU_ITEM.data.method, + 'data-test-foo': TEST_MENU_ITEM.data.testFoo, + }); + }); + it('passes listeners to button', () => { expect(listener).not.toHaveBeenCalled(); @@ -54,11 +68,42 @@ describe('~/nav/components/top_nav_menu_item.vue', () => { expect(listener).toHaveBeenCalledWith('TEST'); }); + + it('renders expected icons', () => { + expect(findButtonIcons()).toEqual([ + { + name: TEST_MENU_ITEM.icon, + classes: ['gl-mr-2!'], + }, + { + name: 'chevron-right', + classes: ['gl-ml-auto'], + }, + ]); + }); + }); + + describe('with icon-only', () => { + beforeEach(() => { + createComponent({ iconOnly: true }); + }); + + it('does not render title or view icon', () => { + expect(wrapper.text()).toBe(''); + }); + + it('only renders menuItem icon', () => { + expect(findButtonIcons()).toEqual([ + { + name: TEST_MENU_ITEM.icon, + classes: [], + }, + ]); + }); }); 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 }} | ${[]} @@ -68,7 +113,32 @@ describe('~/nav/components/top_nav_menu_item.vue', () => { }); it(`renders expected icons ${JSON.stringify(expectedIcons)}`, () => { - expect(findButtonIcons()).toEqual(expectedIcons); + expect(findButtonIcons().map((x) => x.name)).toEqual(expectedIcons); + }); + }); + + describe.each` + desc | active | cssClass | expectedClasses + ${'default'} | ${false} | ${''} | ${[]} + ${'with css class'} | ${false} | ${'test-css-class testing-123'} | ${['test-css-class', 'testing-123']} + ${'with css class & active'} | ${true} | ${'test-css-class'} | ${['test-css-class', ...TopNavMenuItem.ACTIVE_CLASS.split(' ')]} + `('$desc', ({ active, cssClass, expectedClasses }) => { + beforeEach(() => { + createComponent({ + menuItem: { + ...TEST_MENU_ITEM, + active, + css_class: cssClass, + }, + }); + }); + + it('renders expected classes', () => { + expect(wrapper.classes()).toStrictEqual([ + 'top-nav-menu-item', + 'gl-display-block', + ...expectedClasses, + ]); }); }); }); diff --git a/spec/frontend/nav/components/top_nav_menu_sections_spec.js b/spec/frontend/nav/components/top_nav_menu_sections_spec.js new file mode 100644 index 00000000000..d56542fe572 --- /dev/null +++ b/spec/frontend/nav/components/top_nav_menu_sections_spec.js @@ -0,0 +1,107 @@ +import { shallowMount } from '@vue/test-utils'; +import TopNavMenuSections from '~/nav/components/top_nav_menu_sections.vue'; + +const TEST_SECTIONS = [ + { + id: 'primary', + menuItems: [{ id: 'test', href: '/test/href' }, { id: 'foo' }, { id: 'bar' }], + }, + { + id: 'secondary', + menuItems: [{ id: 'lorem' }, { id: 'ipsum' }], + }, +]; + +describe('~/nav/components/top_nav_menu_sections.vue', () => { + let wrapper; + + const createComponent = (props = {}) => { + wrapper = shallowMount(TopNavMenuSections, { + propsData: { + sections: TEST_SECTIONS, + ...props, + }, + }); + }; + + const findMenuItemModels = (parent) => + parent.findAll('[data-testid="menu-item"]').wrappers.map((x) => ({ + menuItem: x.props('menuItem'), + classes: x.classes(), + })); + const findSectionModels = () => + wrapper.findAll('[data-testid="menu-section"]').wrappers.map((x) => ({ + classes: x.classes(), + menuItems: findMenuItemModels(x), + })); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('default', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders sections with menu items', () => { + expect(findSectionModels()).toEqual([ + { + classes: [], + menuItems: [ + { + menuItem: TEST_SECTIONS[0].menuItems[0], + classes: ['gl-w-full'], + }, + ...TEST_SECTIONS[0].menuItems.slice(1).map((menuItem) => ({ + menuItem, + classes: ['gl-w-full', 'gl-mt-1'], + })), + ], + }, + { + classes: [...TopNavMenuSections.BORDER_CLASSES.split(' '), 'gl-mt-3'], + menuItems: [ + { + menuItem: TEST_SECTIONS[1].menuItems[0], + classes: ['gl-w-full'], + }, + ...TEST_SECTIONS[1].menuItems.slice(1).map((menuItem) => ({ + menuItem, + classes: ['gl-w-full', 'gl-mt-1'], + })), + ], + }, + ]); + }); + + it('when clicked menu item with href, does nothing', () => { + const menuItem = wrapper.findAll('[data-testid="menu-item"]').at(0); + + menuItem.vm.$emit('click'); + + expect(wrapper.emitted()).toEqual({}); + }); + + it('when clicked menu item without href, emits "menu-item-click"', () => { + const menuItem = wrapper.findAll('[data-testid="menu-item"]').at(1); + + menuItem.vm.$emit('click'); + + expect(wrapper.emitted('menu-item-click')).toEqual([[TEST_SECTIONS[0].menuItems[1]]]); + }); + }); + + describe('with withTopBorder=true', () => { + beforeEach(() => { + createComponent({ withTopBorder: true }); + }); + + it('renders border classes for top section', () => { + expect(findSectionModels().map((x) => x.classes)).toEqual([ + [...TopNavMenuSections.BORDER_CLASSES.split(' ')], + [...TopNavMenuSections.BORDER_CLASSES.split(' '), 'gl-mt-3'], + ]); + }); + }); +}); diff --git a/spec/frontend/nav/components/top_nav_new_dropdown_spec.js b/spec/frontend/nav/components/top_nav_new_dropdown_spec.js new file mode 100644 index 00000000000..18210658b89 --- /dev/null +++ b/spec/frontend/nav/components/top_nav_new_dropdown_spec.js @@ -0,0 +1,122 @@ +import { GlDropdown } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import TopNavNewDropdown from '~/nav/components/top_nav_new_dropdown.vue'; + +const TEST_VIEW_MODEL = { + title: 'Dropdown', + menu_sections: [ + { + title: 'Section 1', + menu_items: [ + { id: 'foo-1', title: 'Foo 1', href: '/foo/1' }, + { id: 'foo-2', title: 'Foo 2', href: '/foo/2' }, + { id: 'foo-3', title: 'Foo 3', href: '/foo/3' }, + ], + }, + { + title: 'Section 2', + menu_items: [ + { id: 'bar-1', title: 'Bar 1', href: '/bar/1' }, + { id: 'bar-2', title: 'Bar 2', href: '/bar/2' }, + ], + }, + ], +}; + +describe('~/nav/components/top_nav_menu_sections.vue', () => { + let wrapper; + + const createComponent = (props = {}) => { + wrapper = shallowMount(TopNavNewDropdown, { + propsData: { + viewModel: TEST_VIEW_MODEL, + ...props, + }, + }); + }; + + const findDropdown = () => wrapper.findComponent(GlDropdown); + const findDropdownContents = () => + findDropdown() + .findAll('[data-testid]') + .wrappers.map((child) => { + const type = child.attributes('data-testid'); + + if (type === 'divider') { + return { type }; + } else if (type === 'header') { + return { type, text: child.text() }; + } + + return { + type, + text: child.text(), + href: child.attributes('href'), + }; + }); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('default', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders dropdown parent', () => { + expect(findDropdown().props()).toMatchObject({ + text: TEST_VIEW_MODEL.title, + textSrOnly: true, + icon: 'plus', + }); + }); + + it('renders dropdown content', () => { + expect(findDropdownContents()).toEqual([ + { + type: 'header', + text: TEST_VIEW_MODEL.menu_sections[0].title, + }, + ...TEST_VIEW_MODEL.menu_sections[0].menu_items.map(({ title, href }) => ({ + type: 'item', + href, + text: title, + })), + { + type: 'divider', + }, + { + type: 'header', + text: TEST_VIEW_MODEL.menu_sections[1].title, + }, + ...TEST_VIEW_MODEL.menu_sections[1].menu_items.map(({ title, href }) => ({ + type: 'item', + href, + text: title, + })), + ]); + }); + }); + + describe('with only 1 section', () => { + beforeEach(() => { + createComponent({ + viewModel: { + ...TEST_VIEW_MODEL, + menu_sections: TEST_VIEW_MODEL.menu_sections.slice(0, 1), + }, + }); + }); + + it('renders dropdown content without headers and dividers', () => { + expect(findDropdownContents()).toEqual( + TEST_VIEW_MODEL.menu_sections[0].menu_items.map(({ title, href }) => ({ + type: 'item', + href, + text: title, + })), + ); + }); + }); +}); diff --git a/spec/frontend/nav/mock_data.js b/spec/frontend/nav/mock_data.js index 2987d8deb16..c2ad86a4605 100644 --- a/spec/frontend/nav/mock_data.js +++ b/spec/frontend/nav/mock_data.js @@ -25,11 +25,15 @@ export const TEST_NAV_DATA = { namespace: 'projects', currentUserName: '', currentItem: {}, + linksPrimary: [{ id: 'project-link', href: '/path/to/projects', title: 'Project Link' }], + linksSecondary: [], }, groups: { namespace: 'groups', currentUserName: '', currentItem: {}, + linksPrimary: [], + linksSecondary: [{ id: 'group-link', href: '/path/to/groups', title: 'Group Link' }], }, }, }; |