Commit 0ebb1a5d authored by Paul Slaughter's avatar Paul Slaughter Committed by Jose Ivan Vargas

Add top_nav_menu_sections component

parent 9641dac4
...@@ -2,12 +2,12 @@ ...@@ -2,12 +2,12 @@
import FrequentItemsApp from '~/frequent_items/components/app.vue'; import FrequentItemsApp from '~/frequent_items/components/app.vue';
import eventHub from '~/frequent_items/event_hub'; import eventHub from '~/frequent_items/event_hub';
import VuexModuleProvider from '~/vue_shared/components/vuex_module_provider.vue'; import VuexModuleProvider from '~/vue_shared/components/vuex_module_provider.vue';
import TopNavMenuItem from './top_nav_menu_item.vue'; import TopNavMenuSections from './top_nav_menu_sections.vue';
export default { export default {
components: { components: {
FrequentItemsApp, FrequentItemsApp,
TopNavMenuItem, TopNavMenuSections,
VuexModuleProvider, VuexModuleProvider,
}, },
inheritAttrs: false, inheritAttrs: false,
...@@ -32,11 +32,11 @@ export default { ...@@ -32,11 +32,11 @@ export default {
}, },
}, },
computed: { computed: {
linkGroups() { menuSections() {
return [ return [
{ key: 'primary', links: this.linksPrimary }, { id: 'primary', menuItems: this.linksPrimary },
{ key: 'secondary', links: this.linksSecondary }, { id: 'secondary', menuItems: this.linksSecondary },
].filter((x) => x.links?.length); ].filter((x) => x.menuItems?.length);
}, },
}, },
mounted() { mounted() {
...@@ -57,19 +57,6 @@ export default { ...@@ -57,19 +57,6 @@ export default {
</vuex-module-provider> </vuex-module-provider>
</div> </div>
</div> </div>
<div <top-nav-menu-sections class="gl-mt-auto" :sections="menuSections" with-top-border />
v-for="({ key, links }, groupIndex) in linkGroups"
:key="key"
:class="{ 'gl-mt-3': groupIndex !== 0 }"
class="gl-mt-auto gl-pt-3 gl-border-1 gl-border-t-solid gl-border-gray-100"
data-testid="menu-item-group"
>
<top-nav-menu-item
v-for="(link, linkIndex) in links"
:key="link.title"
:menu-item="link"
:class="{ 'gl-mt-1': linkIndex !== 0 }"
/>
</div>
</div> </div>
</template> </template>
<script> <script>
import { cloneDeep } from 'lodash';
import { FREQUENT_ITEMS_PROJECTS, FREQUENT_ITEMS_GROUPS } from '~/frequent_items/constants'; import { FREQUENT_ITEMS_PROJECTS, FREQUENT_ITEMS_GROUPS } from '~/frequent_items/constants';
import KeepAliveSlots from '~/vue_shared/components/keep_alive_slots.vue'; import KeepAliveSlots from '~/vue_shared/components/keep_alive_slots.vue';
import TopNavContainerView from './top_nav_container_view.vue'; import TopNavContainerView from './top_nav_container_view.vue';
import TopNavMenuItem from './top_nav_menu_item.vue'; import TopNavMenuSections from './top_nav_menu_sections.vue';
const ACTIVE_CLASS = 'gl-shadow-none! gl-font-weight-bold! active';
const SECONDARY_GROUP_CLASS = 'gl-pt-3 gl-mt-3 gl-border-1 gl-border-t-solid gl-border-gray-100';
export default { export default {
components: { components: {
KeepAliveSlots, KeepAliveSlots,
TopNavContainerView, TopNavContainerView,
TopNavMenuItem, TopNavMenuSections,
}, },
props: { props: {
primary: { primary: {
...@@ -31,29 +29,25 @@ export default { ...@@ -31,29 +29,25 @@ export default {
}, },
}, },
data() { data() {
// It's expected that primary & secondary never change, so these are treated as "init" props.
// We need to clone so that we can mutate the data without mutating the props
const menuSections = [
{ id: 'primary', menuItems: cloneDeep(this.primary) },
{ id: 'secondary', menuItems: cloneDeep(this.secondary) },
].filter((x) => x.menuItems?.length);
return { return {
activeId: '', menuSections,
}; };
}, },
computed: { computed: {
menuItemGroups() {
return [
{ key: 'primary', items: this.primary, classes: '' },
{
key: 'secondary',
items: this.secondary,
classes: SECONDARY_GROUP_CLASS,
},
].filter((x) => x.items?.length);
},
allMenuItems() { allMenuItems() {
return this.menuItemGroups.flatMap((x) => x.items); return this.menuSections.flatMap((x) => x.menuItems);
},
activeMenuItem() {
return this.allMenuItems.find((x) => x.id === this.activeId);
}, },
activeView() { activeView() {
return this.activeMenuItem?.view; const active = this.allMenuItems.find((x) => x.active);
return active?.view;
}, },
menuClass() { menuClass() {
if (!this.activeView) { if (!this.activeView) {
...@@ -63,61 +57,26 @@ export default { ...@@ -63,61 +57,26 @@ export default {
return ''; return '';
}, },
}, },
created() {
// Initialize activeId based on initialization prop
this.activeId = this.allMenuItems.find((x) => x.active)?.id;
},
methods: { methods: {
onClick({ id, href }) { onMenuItemClick({ id }) {
// If we're a link, let's just do the default behavior so the view won't change this.allMenuItems.forEach((menuItem) => {
if (href) { this.$set(menuItem, 'active', id === menuItem.id);
return; });
}
this.activeId = id;
},
menuItemClasses(menuItem) {
if (menuItem.id === this.activeId) {
return ACTIVE_CLASS;
}
return '';
}, },
}, },
FREQUENT_ITEMS_PROJECTS, FREQUENT_ITEMS_PROJECTS,
FREQUENT_ITEMS_GROUPS, FREQUENT_ITEMS_GROUPS,
// expose for unit tests
ACTIVE_CLASS,
SECONDARY_GROUP_CLASS,
}; };
</script> </script>
<template> <template>
<div class="gl-display-flex gl-align-items-stretch"> <div class="gl-display-flex gl-align-items-stretch">
<div <div
class="gl-w-grid-size-30 gl-flex-shrink-0 gl-bg-gray-10" class="gl-w-grid-size-30 gl-flex-shrink-0 gl-bg-gray-10 gl-py-3 gl-px-5"
:class="menuClass" :class="menuClass"
data-testid="menu-sidebar" data-testid="menu-sidebar"
> >
<div <top-nav-menu-sections :sections="menuSections" @menu-item-click="onMenuItemClick" />
class="gl-py-3 gl-px-5 gl-h-full gl-display-flex gl-align-items-stretch gl-flex-direction-column"
>
<div
v-for="group in menuItemGroups"
:key="group.key"
:class="group.classes"
data-testid="menu-item-group"
>
<top-nav-menu-item
v-for="(menu, index) in group.items"
:key="menu.id"
data-testid="menu-item"
:class="[{ 'gl-mt-1': index !== 0 }, menuItemClasses(menu)]"
:menu-item="menu"
@click="onClick(menu)"
/>
</div>
</div>
</div> </div>
<keep-alive-slots <keep-alive-slots
v-show="activeView" v-show="activeView"
......
...@@ -4,6 +4,8 @@ import { kebabCase, mapKeys } from 'lodash'; ...@@ -4,6 +4,8 @@ import { kebabCase, mapKeys } from 'lodash';
const getDataKey = (key) => `data-${kebabCase(key)}`; const getDataKey = (key) => `data-${kebabCase(key)}`;
const ACTIVE_CLASS = 'gl-shadow-none! gl-font-weight-bold! active';
export default { export default {
components: { components: {
GlButton, GlButton,
...@@ -20,6 +22,7 @@ export default { ...@@ -20,6 +22,7 @@ export default {
return mapKeys(this.menuItem.data || {}, (value, key) => getDataKey(key)); return mapKeys(this.menuItem.data || {}, (value, key) => getDataKey(key));
}, },
}, },
ACTIVE_CLASS,
}; };
</script> </script>
...@@ -28,7 +31,7 @@ export default { ...@@ -28,7 +31,7 @@ export default {
category="tertiary" category="tertiary"
:href="menuItem.href" :href="menuItem.href"
class="top-nav-menu-item gl-display-block" class="top-nav-menu-item gl-display-block"
:class="menuItem.css_class" :class="[menuItem.css_class, { [$options.ACTIVE_CLASS]: menuItem.active }]"
v-bind="dataAttrs" v-bind="dataAttrs"
v-on="$listeners" v-on="$listeners"
> >
......
<script>
import TopNavMenuItem from './top_nav_menu_item.vue';
const BORDER_CLASSES = 'gl-pt-3 gl-border-1 gl-border-t-solid gl-border-gray-100';
export default {
components: {
TopNavMenuItem,
},
props: {
sections: {
type: Array,
required: true,
},
withTopBorder: {
type: Boolean,
required: false,
default: false,
},
},
methods: {
onClick(menuItem) {
// If we're a link, let's just do the default behavior so the view won't change
if (menuItem.href) {
return;
}
this.$emit('menu-item-click', menuItem);
},
getMenuSectionClasses(index) {
// This is a method instead of a computed so we don't have to incur the cost of
// creating a whole new array/object.
return {
[BORDER_CLASSES]: this.withTopBorder || index > 0,
'gl-mt-3': index > 0,
};
},
},
// Expose for unit tests
BORDER_CLASSES,
};
</script>
<template>
<div class="gl-display-flex gl-align-items-stretch gl-flex-direction-column">
<div
v-for="({ id, menuItems }, sectionIndex) in sections"
:key="id"
:class="getMenuSectionClasses(sectionIndex)"
data-testid="menu-section"
>
<top-nav-menu-item
v-for="(menuItem, menuItemIndex) in menuItems"
:key="menuItem.id"
:menu-item="menuItem"
data-testid="menu-item"
:class="{ 'gl-mt-1': menuItemIndex > 0 }"
@click="onClick(menuItem)"
/>
</div>
</div>
</template>
...@@ -4,7 +4,7 @@ import FrequentItemsApp from '~/frequent_items/components/app.vue'; ...@@ -4,7 +4,7 @@ import FrequentItemsApp from '~/frequent_items/components/app.vue';
import { FREQUENT_ITEMS_PROJECTS } from '~/frequent_items/constants'; import { FREQUENT_ITEMS_PROJECTS } from '~/frequent_items/constants';
import eventHub from '~/frequent_items/event_hub'; import eventHub from '~/frequent_items/event_hub';
import TopNavContainerView from '~/nav/components/top_nav_container_view.vue'; 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 VuexModuleProvider from '~/vue_shared/components/vuex_module_provider.vue';
import { TEST_NAV_DATA } from '../mock_data'; import { TEST_NAV_DATA } from '../mock_data';
...@@ -34,11 +34,7 @@ describe('~/nav/components/top_nav_container_view.vue', () => { ...@@ -34,11 +34,7 @@ describe('~/nav/components/top_nav_container_view.vue', () => {
}); });
}; };
const findMenuItems = (parent = wrapper) => parent.findAll(TopNavMenuItem); const findMenuSections = () => wrapper.findComponent(TopNavMenuSections);
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 findFrequentItemsApp = () => {
const parent = wrapper.findComponent(VuexModuleProvider); const parent = wrapper.findComponent(VuexModuleProvider);
...@@ -89,23 +85,16 @@ describe('~/nav/components/top_nav_container_view.vue', () => { ...@@ -89,23 +85,16 @@ describe('~/nav/components/top_nav_container_view.vue', () => {
}); });
}); });
it('renders menu item groups', () => { it('renders menu sections', () => {
expect(findMenuItemGroupsModel()).toEqual([ const sections = [
TEST_NAV_DATA.primary.map((menuItem) => ({ menuItem })), { id: 'primary', menuItems: TEST_NAV_DATA.primary },
TEST_NAV_DATA.secondary.map((menuItem) => ({ menuItem })), { id: 'secondary', menuItems: TEST_NAV_DATA.secondary },
]); ];
});
it('only the first group does not have margin top', () => { expect(findMenuSections().props()).toEqual({
expect(findMenuItemGroups().wrappers.map((x) => x.classes('gl-mt-3'))).toEqual([false, true]); sections,
withTopBorder: 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)]);
}); });
}); });
...@@ -117,8 +106,8 @@ describe('~/nav/components/top_nav_container_view.vue', () => { ...@@ -117,8 +106,8 @@ describe('~/nav/components/top_nav_container_view.vue', () => {
}); });
it('renders one menu item group', () => { it('renders one menu item group', () => {
expect(findMenuItemGroupsModel()).toEqual([ expect(findMenuSections().props('sections')).toEqual([
TEST_NAV_DATA.primary.map((menuItem) => ({ menuItem })), { id: 'primary', menuItems: TEST_NAV_DATA.primary },
]); ]);
}); });
}); });
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount, mount } from '@vue/test-utils';
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import TopNavDropdownMenu from '~/nav/components/top_nav_dropdown_menu.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 KeepAliveSlots from '~/vue_shared/components/keep_alive_slots.vue';
import { TEST_NAV_DATA } from '../mock_data'; import { TEST_NAV_DATA } from '../mock_data';
const SECONDARY_GROUP_CLASSES = TopNavDropdownMenu.SECONDARY_GROUP_CLASS.split(' ');
describe('~/nav/components/top_nav_dropdown_menu.vue', () => { describe('~/nav/components/top_nav_dropdown_menu.vue', () => {
let wrapper; let wrapper;
const createComponent = (props = {}) => { const createComponent = (props = {}, mountFn = shallowMount) => {
wrapper = shallowMount(TopNavDropdownMenu, { wrapper = mountFn(TopNavDropdownMenu, {
propsData: { propsData: {
primary: TEST_NAV_DATA.primary, primary: TEST_NAV_DATA.primary,
secondary: TEST_NAV_DATA.secondary, secondary: TEST_NAV_DATA.secondary,
views: TEST_NAV_DATA.views, views: TEST_NAV_DATA.views,
...props, ...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 findMenuItems = () => wrapper.findAllComponents(TopNavMenuItem);
const findMenuItemsModel = (parent = wrapper) => const findMenuSections = () => wrapper.find(TopNavMenuSections);
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 findMenuSidebar = () => wrapper.find('[data-testid="menu-sidebar"]');
const findMenuSubview = () => wrapper.findComponent(KeepAliveSlots); const findMenuSubview = () => wrapper.findComponent(KeepAliveSlots);
const hasFullWidthMenuSidebar = () => findMenuSidebar().classes('gl-w-full'); const hasFullWidthMenuSidebar = () => findMenuSidebar().classes('gl-w-full');
const createItemsGroupModelExpectation = ({ const withActiveIndex = (menuItems, activeIndex) =>
primary = TEST_NAV_DATA.primary, menuItems.map((x, idx) => ({
secondary = TEST_NAV_DATA.secondary, ...x,
activeIndex = -1, active: idx === activeIndex,
} = {}) => [ }));
{
classes: [],
items: primary.map((menuItem, index) => ({ isActive: index === activeIndex, menuItem })),
},
{
classes: SECONDARY_GROUP_CLASSES,
items: secondary.map((menuItem) => ({ isActive: false, menuItem })),
},
];
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
}); });
beforeEach(() => {
jest.spyOn(console, 'error').mockImplementation();
});
describe('default', () => { describe('default', () => {
beforeEach(() => { beforeEach(() => {
createComponent(); createComponent();
}); });
it('renders menu item groups', () => { it('renders menu sections', () => {
expect(findMenuItemGroupsModel()).toEqual(createItemsGroupModelExpectation()); 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', () => { it('has full width menu sidebar', () => {
...@@ -74,36 +69,25 @@ describe('~/nav/components/top_nav_dropdown_menu.vue', () => { ...@@ -74,36 +69,25 @@ describe('~/nav/components/top_nav_dropdown_menu.vue', () => {
expect(subview.isVisible()).toBe(false); expect(subview.isVisible()).toBe(false);
expect(subview.props()).toEqual({ slotKey: '' }); 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', () => { describe('with pre-initialized active view', () => {
const primaryWithActive = [ beforeEach(() => {
TEST_NAV_DATA.primary[0], // We opt for a small integration test, to make sure the event is handled correctly
// as it would in prod.
createComponent(
{ {
...TEST_NAV_DATA.primary[1], primary: withActiveIndex(TEST_NAV_DATA.primary, 1),
active: true,
}, },
...TEST_NAV_DATA.primary.slice(2), mount,
]; );
beforeEach(() => {
createComponent({
primary: primaryWithActive,
});
}); });
it('renders menu item groups', () => { it('renders menu sections', () => {
expect(findMenuItemGroupsModel()).toEqual( expect(findMenuSections().props('sections')).toStrictEqual([
createItemsGroupModelExpectation({ primary: primaryWithActive, activeIndex: 1 }), { id: 'primary', menuItems: withActiveIndex(TEST_NAV_DATA.primary, 1) },
); { id: 'secondary', menuItems: TEST_NAV_DATA.secondary },
]);
}); });
it('does not have full width menu sidebar', () => { it('does not have full width menu sidebar', () => {
...@@ -114,11 +98,11 @@ describe('~/nav/components/top_nav_dropdown_menu.vue', () => { ...@@ -114,11 +98,11 @@ describe('~/nav/components/top_nav_dropdown_menu.vue', () => {
const subview = findMenuSubview(); const subview = findMenuSubview();
expect(subview.isVisible()).toBe(true); 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 () => { 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 // Ensure this doesn't have a view
expect(secondaryLink.props('menuItem').view).toBeUndefined(); expect(secondaryLink.props('menuItem').view).toBeUndefined();
...@@ -127,10 +111,10 @@ describe('~/nav/components/top_nav_dropdown_menu.vue', () => { ...@@ -127,10 +111,10 @@ describe('~/nav/components/top_nav_dropdown_menu.vue', () => {
await nextTick(); 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; let primaryLink;
beforeEach(async () => { beforeEach(async () => {
...@@ -144,13 +128,20 @@ describe('~/nav/components/top_nav_dropdown_menu.vue', () => { ...@@ -144,13 +128,20 @@ describe('~/nav/components/top_nav_dropdown_menu.vue', () => {
}); });
it('changes active view', () => { 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', () => { it('changes active status on menu item', () => {
expect(findMenuItemGroupsModel()).toStrictEqual( expect(findMenuSections().props('sections')).toStrictEqual([
createItemsGroupModelExpectation({ primary: primaryWithActive, activeIndex: 0 }), {
); id: 'primary',
menuItems: withActiveIndex(TEST_NAV_DATA.primary, 0),
},
{
id: 'secondary',
menuItems: withActiveIndex(TEST_NAV_DATA.secondary, -1),
},
]);
}); });
}); });
}); });
......
...@@ -7,7 +7,6 @@ const TEST_MENU_ITEM = { ...@@ -7,7 +7,6 @@ const TEST_MENU_ITEM = {
icon: 'search', icon: 'search',
href: '/pretty/good/burger', href: '/pretty/good/burger',
view: 'burger-view', view: 'burger-view',
css_class: 'test-super-crazy test-class',
data: { qa_selector: 'not-a-real-selector', method: 'post', testFoo: 'test' }, data: { qa_selector: 'not-a-real-selector', method: 'post', testFoo: 'test' },
}; };
...@@ -49,12 +48,6 @@ describe('~/nav/components/top_nav_menu_item.vue', () => { ...@@ -49,12 +48,6 @@ describe('~/nav/components/top_nav_menu_item.vue', () => {
expect(button.text()).toBe(TEST_MENU_ITEM.title); expect(button.text()).toBe(TEST_MENU_ITEM.title);
}); });
it('renders button classes', () => {
const button = findButton();
expect(button.classes()).toEqual(expect.arrayContaining(TEST_MENU_ITEM.css_class.split(' ')));
});
it('renders button data attributes', () => { it('renders button data attributes', () => {
const button = findButton(); const button = findButton();
...@@ -89,4 +82,29 @@ describe('~/nav/components/top_nav_menu_item.vue', () => { ...@@ -89,4 +82,29 @@ describe('~/nav/components/top_nav_menu_item.vue', () => {
expect(findButtonIcons()).toEqual(expectedIcons); expect(findButtonIcons()).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,
]);
});
});
}); });
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: [],
},
...TEST_SECTIONS[0].menuItems.slice(1).map((menuItem) => ({
menuItem,
classes: ['gl-mt-1'],
})),
],
},
{
classes: [...TopNavMenuSections.BORDER_CLASSES.split(' '), 'gl-mt-3'],
menuItems: [
{
menuItem: TEST_SECTIONS[1].menuItems[0],
classes: [],
},
...TEST_SECTIONS[1].menuItems.slice(1).map((menuItem) => ({
menuItem,
classes: ['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'],
]);
});
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment