Commit d1eec7ce authored by Eulyeon Ko's avatar Eulyeon Ko Committed by Paul Slaughter

Allow Hiding/Collapsing of Milestone header on Roadmap

https://gitlab.com/gitlab-org/gitlab/-/merge_requests/34357
parent 69b34501
......@@ -74,9 +74,7 @@ export default {
if (this.isEmptyChildrenWithFilter) {
return this.infoSearchLabel;
}
return this.childrenFlags[this.itemId].itemExpanded
? __('Collapse child epics')
: __('Expand child epics');
return this.childrenFlags[this.itemId].itemExpanded ? __('Collapse') : __('Expand');
},
childrenFetchInProgress() {
return this.epic.hasChildren && this.childrenFlags[this.itemId].itemChildrenFetchInProgress;
......@@ -108,9 +106,12 @@ export default {
</script>
<template>
<div class="epic-details-cell" data-qa-selector="epic_details_cell">
<div
class="d-flex align-items-start p-2"
class="epic-details-cell gl-display-flex gl-flex-direction-column gl-justify-content-center"
data-qa-selector="epic_details_cell"
>
<div
class="gl-display-flex align-items-start gl-px-3 gl-mb-1"
:class="[epic.isChildEpic ? childMarginClassname : '']"
>
<span ref="expandCollapseInfo">
......@@ -130,38 +131,44 @@ export default {
</gl-button>
</span>
<gl-tooltip
v-if="isEmptyChildrenWithFilter"
v-if="!isExpandIconHidden"
ref="expandIconTooltip"
triggers="hover"
:target="() => $refs.expandCollapseInfo"
boundary="viewport"
offset="80"
offset="15"
placement="topright"
>
{{ infoSearchLabel }}
{{ expandIconLabel }}
</gl-tooltip>
<div class="overflow-hidden flex-grow-1 mx-2">
<a :href="epic.webUrl" :title="epic.title" class="epic-title d-block text-body bold">
<a
:href="epic.webUrl"
:title="epic.title"
class="epic-title gl-mt-1 d-block text-body bold"
>
{{ epic.title }}
</a>
<div class="epic-group-timeframe d-flex text-secondary">
<p
<span
v-if="isEpicGroupDifferent && !epic.hasParent"
:title="epic.groupFullName"
class="epic-group"
>
{{ epic.groupName }}
</p>
</span>
<span v-if="isEpicGroupDifferent && !epic.hasParent" class="mx-1" aria-hidden="true"
>&middot;</span
>
<p class="epic-timeframe" :title="timeframeString">{{ timeframeString }}</p>
<span class="epic-timeframe" :title="timeframeString">{{ timeframeString }}</span>
</div>
</div>
<template v-if="allowSubEpics">
<div ref="childEpicsCount" class="d-flex text-secondary text-nowrap">
<div ref="childEpicsCount" class="gl-mt-1 d-flex text-secondary text-nowrap">
<gl-icon name="epic" class="align-text-bottom mr-1" aria-hidden="true" />
<p class="m-0" :aria-label="childEpicsCountText">{{ childEpicsCount }}</p>
</div>
<gl-tooltip :target="() => $refs.childEpicsCount">
<gl-tooltip ref="childEpicsCountTooltip" :target="() => $refs.childEpicsCount">
<span :class="{ bold: hasFiltersApplied }">{{ childEpicsCountText }}</span>
<span v-if="hasFiltersApplied" class="d-block">{{ childEpicsSearchText }}</span>
</gl-tooltip>
......
......@@ -24,6 +24,10 @@ export default {
type: Number,
required: true,
},
milestonesExpanded: {
type: Boolean,
required: true,
},
},
};
</script>
......@@ -33,10 +37,12 @@ export default {
<span
v-for="timeframeItem in timeframe"
:key="timeframeItem.id"
class="milestone-timeline-cell d-table-cell position-relative border-right border-bottom"
class="milestone-timeline-cell gl-display-table-cell gl-relative border-right border-bottom"
:class="{ 'milestone-timeline-cell-empty': !milestonesExpanded }"
data-qa-selector="milestone_timeline_cell"
>
<current-day-indicator :preset-type="presetType" :timeframe-item="timeframeItem" />
<template v-if="milestonesExpanded">
<milestone-item
v-for="milestone in milestones"
:key="milestone.id"
......@@ -46,6 +52,7 @@ export default {
:timeframe-item="timeframeItem"
:current-group-id="currentGroupId"
/>
</template>
</span>
</div>
</template>
<script>
import { mapState, mapActions } from 'vuex';
import eventHub from '../event_hub';
import { __, n__ } from '~/locale';
import { GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { EPIC_DETAILS_CELL_WIDTH, EPIC_ITEM_HEIGHT, TIMELINE_CELL_MIN_WIDTH } from '../constants';
import MilestoneTimeline from './milestone_timeline.vue';
const EXPAND_BUTTON_EXPANDED = {
name: 'chevron-down',
iconLabel: __('Collapse milestones'),
tooltip: __('Collapse'),
};
const EXPAND_BUTTON_COLLAPSED = {
name: 'chevron-right',
iconLabel: __('Expand milestones'),
tooltip: __('Expand'),
};
export default {
components: {
MilestoneTimeline,
GlButton,
GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
presetType: {
......@@ -31,6 +50,7 @@ export default {
offsetLeft: 0,
showBottomShadow: false,
roadmapShellEl: null,
milestonesExpanded: true,
};
},
computed: {
......@@ -48,6 +68,17 @@ export default {
left: `${this.offsetLeft}px`,
};
},
expandButton() {
return this.milestonesExpanded ? EXPAND_BUTTON_EXPANDED : EXPAND_BUTTON_COLLAPSED;
},
milestonesCount() {
return this.milestones.length;
},
milestonesCountText() {
return Number.isInteger(this.milestonesCount)
? n__(`%d milestone`, `%d milestones`, this.milestonesCount)
: '';
},
},
mounted() {
eventHub.$on('epicsListScrolled', this.handleEpicsListScroll);
......@@ -76,23 +107,59 @@ export default {
handleEpicsListScroll({ scrollTop, clientHeight, scrollHeight }) {
this.showBottomShadow = Math.ceil(scrollTop) + clientHeight < scrollHeight;
},
toggleMilestonesExpanded() {
this.milestonesExpanded = !this.milestonesExpanded;
},
},
};
</script>
<template>
<div :style="sectionContainerStyles" class="milestones-list-section d-table">
<div
class="milestones-list-title d-table-cell bold border-bottom align-top position-sticky pt-2 pl-3"
:style="sectionContainerStyles"
class="milestones-list-section gl-display-table"
:class="{ 'milestones-list-section-collapsed': !milestonesExpanded }"
>
<div
class="milestones-list-title gl-display-table-cell border-bottom gl-vertical-align-top position-sticky gl-p-3"
>
<div class="gl-display-flex gl-align-items-center">
<span
v-gl-tooltip.hover.topright="{
title: expandButton.tooltip,
offset: 15,
boundary: 'viewport',
}"
data-testid="expandButton"
>
<gl-button
:aria-label="expandButton.iconLabel"
variant="link"
@click="toggleMilestonesExpanded"
>
<gl-icon :name="expandButton.name" class="text-secondary" aria-hidden="true" />
</gl-button>
</span>
<div class="gl-overflow-hidden gl-flex-grow-1 gl-mx-3 gl-font-weight-bold">
{{ __('Milestones') }}
</div>
<div class="milestones-list-items d-table-cell">
<div
v-gl-tooltip="milestonesCountText"
class="gl-display-flex gl-align-items-center gl-justify-content-center text-secondary gl-white-space-nowrap"
data-testid="count"
>
<gl-icon name="clock" class="gl-mr-2" aria-hidden="true" />
<span :aria-label="milestonesCountText">{{ milestonesCount }}</span>
</div>
</div>
</div>
<div class="milestones-list-items gl-display-table-cell">
<milestone-timeline
:preset-type="presetType"
:timeframe="timeframe"
:milestones="milestones"
:current-group-id="currentGroupId"
:milestones-expanded="milestonesExpanded"
/>
</div>
<div v-show="showBottomShadow" :style="shadowCellStyles" class="scroll-bottom-shadow"></div>
......
$header-item-height: 60px;
$item-height: 50px;
$milestones-collapsed-height: 38px;
$details-cell-width: 320px;
$timeline-cell-width: 180px;
$border-style: 1px solid $border-gray-normal;
......@@ -408,11 +409,19 @@ html.group-epics-roadmap-html {
}
.milestones-list-section {
&.milestones-list-section-collapsed {
height: $milestones-collapsed-height;
}
.milestones-list-items {
.milestone-timeline-cell {
width: $timeline-cell-width;
}
.milestone-timeline-cell-empty {
height: $milestones-collapsed-height;
}
.timeline-bar-wrapper {
height: 32px;
color: $gray-700;
......
---
title: Allow Hiding/Collapsing of Milestone header on Roadmap
merge_request: 34357
author:
type: added
import { GlButton, GlIcon, GlTooltip } from '@gitlab/ui';
import { GlButton, GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import createStore from 'ee/roadmap/store';
import EpicItemDetails from 'ee/roadmap/components/epic_item_details.vue';
......@@ -10,70 +10,87 @@ import {
mockFormattedChildEpic1,
} from 'ee_jest/roadmap/mock_data';
let store;
const createComponent = ({
epic = mockFormattedEpic,
currentGroupId = mockGroupId,
timeframeString = 'Jul 10, 2017 – Jun 2, 2018',
childLevel = 0,
childrenFlags = { '41': { itemExpanded: false } },
hasFiltersApplied = false,
isChildrenEmpty = false,
} = {}) => {
return shallowMount(EpicItemDetails, {
describe('EpicItemDetails', () => {
let wrapper;
let store;
beforeEach(() => {
store = createStore();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const createWrapper = (props = {}) => {
wrapper = shallowMount(EpicItemDetails, {
store,
propsData: {
epic,
currentGroupId,
timeframeString,
childLevel,
childrenFlags,
hasFiltersApplied,
isChildrenEmpty,
epic: mockFormattedEpic,
currentGroupId: mockGroupId,
timeframeString: 'Jul 10, 2017 – Jun 2, 2018',
childLevel: 0,
childrenFlags: { '41': { itemExpanded: false } },
hasFiltersApplied: false,
isChildrenEmpty: false,
...props,
},
});
};
};
const getTitle = wrapper => wrapper.find('.epic-title');
const getTitle = () => wrapper.find('.epic-title');
const getGroupName = wrapper => wrapper.find('.epic-group');
const getGroupName = () => wrapper.find('.epic-group');
const getExpandIconButton = wrapper => wrapper.find(GlButton);
const getChildMarginClassName = () => wrapper.vm.childMarginClassname;
const getChildEpicsCount = wrapper => wrapper.find({ ref: 'childEpicsCount' });
const getExpandIconButton = () => wrapper.find(GlButton);
describe('EpicItemDetails', () => {
let wrapper;
const getExpandIconTooltip = () => wrapper.find({ ref: 'expandIconTooltip' });
beforeEach(() => {
store = createStore();
wrapper = createComponent();
const getChildEpicsCount = () => wrapper.find({ ref: 'childEpicsCount' });
const getChildEpicsCountTooltip = () => wrapper.find({ ref: 'childEpicsCountTooltip' });
const getExpandButtonData = () => ({
icon: wrapper.find(GlIcon).attributes('name'),
iconLabel: getExpandIconButton().attributes('aria-label'),
tooltip: getExpandIconTooltip().text(),
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
const getEpicTitleData = () => ({
title: getTitle().text(),
link: getTitle().attributes('href'),
});
const getEpicGroupNameData = () => ({
groupName: getGroupName().text(),
title: getGroupName().attributes('title'),
});
const createMockEpic = epic => ({
...mockFormattedEpic,
...epic,
});
describe('epic title', () => {
it('is displayed', () => {
expect(getTitle(wrapper).text()).toBe(mockFormattedEpic.title);
beforeEach(() => {
createWrapper();
});
it('contains a link to the epic', () => {
expect(getTitle(wrapper).attributes('href')).toBe(mockFormattedEpic.webUrl);
it('is displayed with a link to the epic', () => {
expect(getEpicTitleData()).toEqual({
title: mockFormattedEpic.title,
link: mockFormattedEpic.webUrl,
});
});
});
describe('epic group name', () => {
describe('when the epic group ID is different from the current group ID', () => {
let epic;
beforeEach(() => {
epic = {
const epic = {
id: '41',
mockFormattedEpic,
...mockFormattedEpic,
groupId: 1,
groupName: 'Bar',
groupFullName: 'Foo / Bar',
......@@ -83,40 +100,27 @@ describe('EpicItemDetails', () => {
},
};
wrapper.setProps({ epic, currentGroupId: 2 });
describe('when the epic group ID is different from the current group ID', () => {
it('is displayed and set to the title attribute', () => {
createWrapper({ epic, currentGroupId: 2 });
expect(getEpicGroupNameData()).toEqual({
groupName: epic.groupName,
title: epic.groupFullName,
});
it('is displayed', () => {
expect(getGroupName(wrapper).text()).toContain(epic.groupName);
});
it('is set to the title attribute', () => {
expect(getGroupName(wrapper).attributes('title')).toBe(epic.groupFullName);
});
});
describe('when the epic group ID is the same as the current group ID', () => {
let epic;
beforeEach(() => {
epic = {
...mockFormattedEpic,
groupId: 1,
groupName: 'Bar',
groupFullName: 'Foo / Bar',
};
wrapper.setProps({ epic, currentGroupId: 1 });
});
it('is hidden', () => {
expect(getGroupName(wrapper).exists()).toBe(false);
createWrapper({ epic, currentGroupId: 1 });
expect(getGroupName().exists()).toBe(false);
});
});
});
describe('timeframe', () => {
it('is displayed', () => {
createWrapper();
const timeframe = wrapper.find('.epic-timeframe');
expect(timeframe.text()).toBe('Jul 10, 2017 – Jun 2, 2018');
......@@ -125,13 +129,13 @@ describe('EpicItemDetails', () => {
describe('childMarginClassname', () => {
it('childMarginClassname returns class for level 1 child is childLevel is 1', () => {
wrapper.setProps({ childLevel: 1 });
expect(wrapper.vm.childMarginClassname).toEqual('ml-4');
createWrapper({ childLevel: 1 });
expect(getChildMarginClassName()).toEqual('ml-4');
});
it('childMarginClassname returns class for level 2 child is childLevel is 2', () => {
wrapper.setProps({ childLevel: 2 });
expect(wrapper.vm.childMarginClassname).toEqual('ml-6');
createWrapper({ childLevel: 2 });
expect(getChildMarginClassName()).toEqual('ml-6');
});
});
......@@ -141,204 +145,152 @@ describe('EpicItemDetails', () => {
});
describe('expand icon', () => {
it('is hidden when epic has no child epics', () => {
const epic = {
...mockFormattedEpic,
hasChildren: false,
};
wrapper = createComponent({ epic });
it('is hidden when it is child epic', () => {
const epic = createMockEpic({
isChildEpic: true,
});
createWrapper({ epic });
expect(getExpandIconButton().classes()).toContain('invisible');
});
expect(getExpandIconButton(wrapper).classes()).toContain('invisible');
describe('when epic has no child epics', () => {
beforeEach(() => {
const epic = createMockEpic({
hasChildren: false,
descendantCounts: {
openedEpics: 0,
closedEpics: 0,
},
});
createWrapper({ epic });
});
it('is hidden', () => {
expect(getExpandIconButton().classes()).toContain('invisible');
});
describe('child epics count', () => {
it('shows the count as 0', () => {
expect(getChildEpicsCount().text()).toBe('0');
});
});
});
it('is shown when epic has child epics', () => {
const epic = {
...mockFormattedEpic,
describe('when epic has child epics', () => {
let epic;
beforeEach(() => {
epic = createMockEpic({
id: 41,
hasChildren: true,
children: {
edges: [mockFormattedChildEpic1],
},
};
wrapper = createComponent({ epic });
expect(getExpandIconButton(wrapper).classes()).not.toContain('invisible');
descendantCounts: {
openedEpics: 0,
closedEpics: 1,
},
});
createWrapper({ epic });
});
it('shows "chevron-right" icon when child epics are not expanded', () => {
wrapper = createComponent();
it('is shown', () => {
expect(getExpandIconButton().classes()).not.toContain('invisible');
});
expect(wrapper.find(GlIcon).attributes('name')).toBe('chevron-right');
it('emits toggleIsEpicExpanded event when clicked', () => {
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
getExpandIconButton().vm.$emit('click');
expect(eventHub.$emit).toHaveBeenCalledWith('toggleIsEpicExpanded', epic);
});
it('shows "chevron-down" icon when child epics are expanded', () => {
const epic = {
...mockFormattedEpic,
hasChildren: true,
};
wrapper = createComponent({
epic,
childrenFlags: {
describe('when child epics are expanded', () => {
const childrenFlags = {
'41': { itemExpanded: true },
},
};
beforeEach(() => {
createWrapper({ epic, childrenFlags });
});
expect(wrapper.find(GlIcon).attributes('name')).toBe('chevron-down');
it('shows collapse button', () => {
expect(getExpandButtonData()).toEqual({
icon: 'chevron-down',
iconLabel: 'Collapse',
tooltip: 'Collapse',
});
});
it('shows "information-o" icon when child epics are expanded but no children are returned due to applied filters', () => {
const epic = {
...mockFormattedEpic,
hasChildren: true,
};
wrapper = createComponent({
describe('when filters are applied', () => {
beforeEach(() => {
createWrapper({
epic,
childrenFlags: {
'41': { itemExpanded: true },
},
childrenFlags,
hasFiltersApplied: true,
isChildrenEmpty: true,
});
expect(wrapper.find(GlIcon).attributes('name')).toBe('information-o');
});
it('has "Expand child epics" label when child epics are not expanded', () => {
wrapper = createComponent();
expect(getExpandIconButton(wrapper).attributes('aria-label')).toBe('Expand child epics');
it('shows child epics match filters button', () => {
expect(getExpandButtonData()).toEqual({
icon: 'information-o',
iconLabel: 'No child epics match applied filters',
tooltip: 'No child epics match applied filters',
});
});
it('has "Collapse child epics" label when child epics are expanded', () => {
const epic = {
...mockFormattedEpic,
hasChildren: true,
};
wrapper = createComponent({
epic,
childrenFlags: {
'41': { itemExpanded: true },
},
});
expect(getExpandIconButton(wrapper).attributes('aria-label')).toBe('Collapse child epics');
});
it('has "No child epics match applied filters" label when child epics are expanded', () => {
const epic = {
...mockFormattedEpic,
hasChildren: true,
describe('when child epics are not expanded', () => {
beforeEach(() => {
const childrenFlags = {
'41': { itemExpanded: false },
};
wrapper = createComponent({
createWrapper({
epic,
childrenFlags: {
'41': { itemExpanded: true },
},
hasFiltersApplied: true,
isChildrenEmpty: true,
childrenFlags,
});
expect(getExpandIconButton(wrapper).attributes('aria-label')).toBe(
'No child epics match applied filters',
);
});
it('emits toggleIsEpicExpanded event when clicked', () => {
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
const id = 41;
const epic = {
...mockFormattedEpic,
id,
children: {
edges: [mockFormattedChildEpic1],
},
};
wrapper = createComponent({ epic });
getExpandIconButton(wrapper).vm.$emit('click');
expect(eventHub.$emit).toHaveBeenCalledWith('toggleIsEpicExpanded', epic);
it('shows expand button', () => {
expect(getExpandButtonData()).toEqual({
icon: 'chevron-right',
iconLabel: 'Expand',
tooltip: 'Expand',
});
it('is hidden when it is child epic', () => {
const epic = {
...mockFormattedEpic,
isChildEpic: true,
};
wrapper = createComponent({ epic });
expect(getExpandIconButton(wrapper).classes()).toContain('invisible');
});
});
describe('child epics count', () => {
it('shows the correct count of child epics', () => {
const epic = {
...mockFormattedEpic,
children: {
edges: [mockFormattedChildEpic1, mockFormattedChildEpic2],
},
descendantCounts: {
openedEpics: 0,
closedEpics: 2,
},
};
wrapper = createComponent({ epic });
expect(getChildEpicsCount(wrapper).text()).toBe('2');
it('has a tooltip with the count', () => {
createWrapper({ epic });
expect(getChildEpicsCountTooltip().text()).toBe('1 child epic');
});
it('shows the count as 0 when there are no child epics', () => {
const epic = {
...mockFormattedEpic,
descendantCounts: {
openedEpics: 0,
closedEpics: 0,
},
};
wrapper = createComponent({ epic });
expect(getChildEpicsCount(wrapper).text()).toBe('0');
it('has a tooltip with the count and explanation if search is being performed', () => {
createWrapper({ epic, hasFiltersApplied: true });
expect(getChildEpicsCountTooltip().text()).toBe(
'1 child epic Some child epics may be hidden due to applied filters',
);
});
it('has a tooltip with the count', () => {
const epic = {
...mockFormattedEpic,
children: {
edges: [mockFormattedChildEpic1],
},
descendantCounts: {
openedEpics: 0,
closedEpics: 1,
},
};
wrapper = createComponent({ epic });
expect(wrapper.find(GlTooltip).text()).toBe('1 child epic');
it('does not render if the user license does not support child epics', () => {
store.state.allowSubEpics = false;
createWrapper({ epic });
expect(getChildEpicsCount().exists()).toBe(false);
});
it('has a tooltip with the count and explanation if search is being performed', () => {
const epic = {
...mockFormattedEpic,
it('shows the correct count of child epics', () => {
epic = createMockEpic({
children: {
edges: [mockFormattedChildEpic1],
edges: [mockFormattedChildEpic1, mockFormattedChildEpic2],
},
descendantCounts: {
openedEpics: 0,
closedEpics: 1,
closedEpics: 2,
},
};
wrapper = createComponent({ epic, hasFiltersApplied: true });
expect(wrapper.find(GlTooltip).text()).toBe(
'1 child epic Some child epics may be hidden due to applied filters',
);
});
it('does not render if the user license does not support child epics', () => {
store.state.allowSubEpics = false;
wrapper = createComponent();
expect(getChildEpicsCount(wrapper).exists()).toBe(false);
createWrapper({ epic });
expect(getChildEpicsCount().text()).toBe('2');
});
});
});
});
});
......
import Vue from 'vue';
import { shallowMount } from '@vue/test-utils';
import milestoneTimelineComponent from 'ee/roadmap/components/milestone_timeline.vue';
import MilestoneTimelineComponent from 'ee/roadmap/components/milestone_timeline.vue';
import MilestoneItem from 'ee/roadmap/components/milestone_item.vue';
import { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils';
......@@ -11,24 +10,6 @@ import { mockTimeframeInitialDate, mockMilestone2, mockGroupId } from 'ee_jest/r
const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate);
const createComponent = ({
presetType = PRESET_TYPES.MONTHS,
timeframe = mockTimeframeMonths,
milestones = [mockMilestone2],
currentGroupId = mockGroupId,
} = {}) => {
const Component = Vue.extend(milestoneTimelineComponent);
return shallowMount(Component, {
propsData: {
presetType,
timeframe,
milestones,
currentGroupId,
},
});
};
describe('MilestoneTimelineComponent', () => {
let wrapper;
......@@ -36,17 +17,39 @@ describe('MilestoneTimelineComponent', () => {
wrapper.destroy();
});
describe('template', () => {
it('renders component container element with class `milestone-timeline-cell`', () => {
wrapper = createComponent();
expect(wrapper.find('.milestone-timeline-cell').exists()).toBe(true);
const createWrapper = (props = {}) => {
wrapper = shallowMount(MilestoneTimelineComponent, {
propsData: {
presetType: PRESET_TYPES.MONTHS,
timeframe: mockTimeframeMonths,
milestones: [mockMilestone2],
currentGroupId: mockGroupId,
milestonesExpanded: true,
...props,
},
});
};
const findMilestoneTimelineCell = () => wrapper.find('.milestone-timeline-cell');
const findMilestoneItem = () => wrapper.find(MilestoneItem);
describe.each`
props | hasCellEmpty | hasMilestoneItem
${{}} | ${false} | ${true}
${{ milestonesExpanded: false }} | ${true} | ${false}
`('with $props', ({ props, hasCellEmpty, hasMilestoneItem }) => {
beforeEach(() => {
createWrapper(props);
});
it('renders MilestoneItem component', () => {
wrapper = createComponent();
it(`renders timeline cell with empty class = ${hasCellEmpty}`, () => {
expect(findMilestoneTimelineCell().classes('milestone-timeline-cell-empty')).toBe(
hasCellEmpty,
);
});
expect(wrapper.find(MilestoneItem).exists()).toBe(true);
it(`renders MilestoneItem component = ${hasMilestoneItem}`, () => {
expect(findMilestoneItem().exists()).toBe(hasMilestoneItem);
});
});
});
import { GlIcon, GlButton } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import milestonesListSectionComponent from 'ee/roadmap/components/milestones_list_section.vue';
import MilestoneTimeline from 'ee/roadmap/components/milestone_timeline.vue';
......@@ -9,57 +10,72 @@ import {
TIMELINE_CELL_MIN_WIDTH,
} from 'ee/roadmap/constants';
import { mockTimeframeInitialDate, mockGroupId, rawMilestones } from 'ee_jest/roadmap/mock_data';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate);
const store = createStore();
store.dispatch('setInitialData', {
const initializeStore = mockTimeframeMonths => {
const store = createStore();
store.dispatch('setInitialData', {
currentGroupId: mockGroupId,
presetType: PRESET_TYPES.MONTHS,
timeframe: mockTimeframeMonths,
});
store.dispatch('receiveMilestonesSuccess', { rawMilestones });
const mockMilestones = store.state.milestones;
});
store.dispatch('receiveMilestonesSuccess', { rawMilestones });
return store;
};
const createComponent = ({
milestones = mockMilestones,
timeframe = mockTimeframeMonths,
currentGroupId = mockGroupId,
presetType = PRESET_TYPES.MONTHS,
} = {}) => {
describe('MilestonesListSectionComponent', () => {
let wrapper;
let store;
const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate);
const findMilestoneCount = () => wrapper.find('[data-testid="count"]');
const findMilestoneCountTooltip = () => getBinding(findMilestoneCount().element, 'gl-tooltip');
const findExpandButtonContainer = () => wrapper.find('[data-testid="expandButton"]');
const findExpandButtonData = () => {
const container = findExpandButtonContainer();
return {
icon: container.find(GlIcon).attributes('name'),
iconLabel: container.find(GlButton).attributes('aria-label'),
tooltip: getBinding(container.element, 'gl-tooltip').value.title,
};
};
const createWrapper = (props = {}) => {
const localVue = createLocalVue();
return shallowMount(milestonesListSectionComponent, {
wrapper = shallowMount(milestonesListSectionComponent, {
localVue,
store,
stubs: {
MilestoneTimeline: false,
},
propsData: {
presetType,
milestones,
timeframe,
currentGroupId,
milestones: store.state.milestones,
timeframe: mockTimeframeMonths,
currentGroupId: mockGroupId,
presetType: PRESET_TYPES.MONTHS,
...props,
},
directives: {
GlTooltip: createMockDirective(),
},
});
};
describe('MilestonesListSectionComponent', () => {
let wrapper;
};
beforeEach(() => {
wrapper = createComponent();
store = initializeStore(mockTimeframeMonths);
createWrapper();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('data', () => {
it('returns default data props', () => {
expect(wrapper.vm.offsetLeft).toBe(0);
expect(wrapper.vm.roadmapShellEl).toBeDefined();
expect(wrapper.vm.milestonesExpanded).toBe(true);
});
});
......@@ -134,5 +150,41 @@ describe('MilestonesListSectionComponent', () => {
expect(wrapper.find('.scroll-bottom-shadow').exists()).toBe(true);
});
it('show the correct count of milestones ', () => {
expect(findMilestoneCount().text()).toBe('2');
});
it('has a tooltip with the correct count of milestones', () => {
expect(findMilestoneCountTooltip().value).toBe('2 milestones');
});
describe('milestone expand/collapse button', () => {
it('is rendered', () => {
expect(findExpandButtonData()).toEqual({
icon: 'chevron-down',
iconLabel: 'Collapse milestones',
tooltip: 'Collapse',
});
});
});
});
describe('when the milestone list is expanded', () => {
beforeEach(() => {
findExpandButtonContainer()
.find(GlButton)
.vm.$emit('click');
return wrapper.vm.$nextTick();
});
it('shows "chevron-right" icon when the milestone toggle button is clicked', () => {
expect(findExpandButtonData()).toEqual({
icon: 'chevron-right',
iconLabel: 'Expand milestones',
tooltip: 'Expand',
});
});
});
});
......@@ -199,6 +199,11 @@ msgid_plural "%d metrics"
msgstr[0] ""
msgstr[1] ""
msgid "%d milestone"
msgid_plural "%d milestones"
msgstr[0] ""
msgstr[1] ""
msgid "%d minute"
msgid_plural "%d minutes"
msgstr[0] ""
......@@ -5689,7 +5694,7 @@ msgstr ""
msgid "Collapse approvers"
msgstr ""
msgid "Collapse child epics"
msgid "Collapse milestones"
msgstr ""
msgid "Collapse replies"
......@@ -9296,15 +9301,15 @@ msgstr ""
msgid "Expand approvers"
msgstr ""
msgid "Expand child epics"
msgstr ""
msgid "Expand down"
msgstr ""
msgid "Expand dropdown"
msgstr ""
msgid "Expand milestones"
msgstr ""
msgid "Expand sidebar"
msgstr ""
......
......@@ -2,13 +2,21 @@ export const getKey = name => `$_gl_jest_${name}`;
export const getBinding = (el, name) => el[getKey(name)];
export const createMockDirective = () => ({
bind(el, { name, value, arg, modifiers }) {
const writeBindingToElement = (el, { name, value, arg, modifiers }) => {
el[getKey(name)] = {
value,
arg,
modifiers,
};
};
export const createMockDirective = () => ({
bind(el, binding) {
writeBindingToElement(el, binding);
},
update(el, binding) {
writeBindingToElement(el, binding);
},
unbind(el, { name }) {
......
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