summaryrefslogtreecommitdiff
path: root/spec/frontend/nav
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-06-16 18:25:58 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-06-16 18:25:58 +0000
commita5f4bba440d7f9ea47046a0a561d49adf0a1e6d4 (patch)
treefb69158581673816a8cd895f9d352dcb3c678b1e /spec/frontend/nav
parentd16b2e8639e99961de6ddc93909f3bb5c1445ba1 (diff)
downloadgitlab-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.js173
-rw-r--r--spec/frontend/nav/components/responsive_header_spec.js67
-rw-r--r--spec/frontend/nav/components/responsive_home_spec.js137
-rw-r--r--spec/frontend/nav/components/top_nav_app_spec.js31
-rw-r--r--spec/frontend/nav/components/top_nav_container_view_spec.js60
-rw-r--r--spec/frontend/nav/components/top_nav_dropdown_menu_spec.js121
-rw-r--r--spec/frontend/nav/components/top_nav_menu_item_spec.js76
-rw-r--r--spec/frontend/nav/components/top_nav_menu_sections_spec.js107
-rw-r--r--spec/frontend/nav/components/top_nav_new_dropdown_spec.js122
-rw-r--r--spec/frontend/nav/mock_data.js4
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' }],
},
},
};