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="epic-details-cell gl-display-flex gl-flex-direction-column gl-justify-content-center"
data-qa-selector="epic_details_cell"
>
<div
class="d-flex align-items-start p-2"
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,19 +37,22 @@ 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" />
<milestone-item
v-for="milestone in milestones"
:key="milestone.id"
:preset-type="presetType"
:milestone="milestone"
:timeframe="timeframe"
:timeframe-item="timeframeItem"
:current-group-id="currentGroupId"
/>
<template v-if="milestonesExpanded">
<milestone-item
v-for="milestone in milestones"
:key="milestone.id"
:preset-type="presetType"
:milestone="milestone"
:timeframe="timeframe"
: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
:style="sectionContainerStyles"
class="milestones-list-section gl-display-table"
:class="{ 'milestones-list-section-collapsed': !milestonesExpanded }"
>
<div
class="milestones-list-title d-table-cell bold border-bottom align-top position-sticky pt-2 pl-3"
class="milestones-list-title gl-display-table-cell border-bottom gl-vertical-align-top position-sticky gl-p-3"
>
{{ __('Milestones') }}
<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
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 d-table-cell">
<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,45 +10,12 @@ 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, {
store,
propsData: {
epic,
currentGroupId,
timeframeString,
childLevel,
childrenFlags,
hasFiltersApplied,
isChildrenEmpty,
},
});
};
const getTitle = wrapper => wrapper.find('.epic-title');
const getGroupName = wrapper => wrapper.find('.epic-group');
const getExpandIconButton = wrapper => wrapper.find(GlButton);
const getChildEpicsCount = wrapper => wrapper.find({ ref: 'childEpicsCount' });
describe('EpicItemDetails', () => {
let wrapper;
let store;
beforeEach(() => {
store = createStore();
wrapper = createComponent();
});
afterEach(() => {
......@@ -56,67 +23,104 @@ describe('EpicItemDetails', () => {
wrapper = null;
});
describe('epic title', () => {
it('is displayed', () => {
expect(getTitle(wrapper).text()).toBe(mockFormattedEpic.title);
const createWrapper = (props = {}) => {
wrapper = shallowMount(EpicItemDetails, {
store,
propsData: {
epic: mockFormattedEpic,
currentGroupId: mockGroupId,
timeframeString: 'Jul 10, 2017 – Jun 2, 2018',
childLevel: 0,
childrenFlags: { '41': { itemExpanded: false } },
hasFiltersApplied: false,
isChildrenEmpty: false,
...props,
},
});
};
it('contains a link to the epic', () => {
expect(getTitle(wrapper).attributes('href')).toBe(mockFormattedEpic.webUrl);
});
const getTitle = () => wrapper.find('.epic-title');
const getGroupName = () => wrapper.find('.epic-group');
const getChildMarginClassName = () => wrapper.vm.childMarginClassname;
const getExpandIconButton = () => wrapper.find(GlButton);
const getExpandIconTooltip = () => wrapper.find({ ref: 'expandIconTooltip' });
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(),
});
describe('epic group name', () => {
describe('when the epic group ID is different from the current group ID', () => {
let epic;
beforeEach(() => {
epic = {
id: '41',
mockFormattedEpic,
groupId: 1,
groupName: 'Bar',
groupFullName: 'Foo / Bar',
descendantCounts: {
closedIssues: 3,
openedIssues: 2,
},
};
wrapper.setProps({ epic, currentGroupId: 2 });
});
const getEpicTitleData = () => ({
title: getTitle().text(),
link: getTitle().attributes('href'),
});
it('is displayed', () => {
expect(getGroupName(wrapper).text()).toContain(epic.groupName);
});
const getEpicGroupNameData = () => ({
groupName: getGroupName().text(),
title: getGroupName().attributes('title'),
});
it('is set to the title attribute', () => {
expect(getGroupName(wrapper).attributes('title')).toBe(epic.groupFullName);
});
const createMockEpic = epic => ({
...mockFormattedEpic,
...epic,
});
describe('epic title', () => {
beforeEach(() => {
createWrapper();
});
describe('when the epic group ID is the same as the current group ID', () => {
let epic;
it('is displayed with a link to the epic', () => {
expect(getEpicTitleData()).toEqual({
title: mockFormattedEpic.title,
link: mockFormattedEpic.webUrl,
});
});
});
beforeEach(() => {
epic = {
...mockFormattedEpic,
groupId: 1,
groupName: 'Bar',
groupFullName: 'Foo / Bar',
};
describe('epic group name', () => {
const epic = {
id: '41',
...mockFormattedEpic,
groupId: 1,
groupName: 'Bar',
groupFullName: 'Foo / Bar',
descendantCounts: {
closedIssues: 3,
openedIssues: 2,
},
};
wrapper.setProps({ epic, currentGroupId: 1 });
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,
});
});
});
describe('when the epic group ID is the same as the current group ID', () => {
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 });
expect(getExpandIconButton(wrapper).classes()).toContain('invisible');
});
it('is shown when epic has child epics', () => {
const epic = {
...mockFormattedEpic,
hasChildren: true,
children: {
edges: [mockFormattedChildEpic1],
},
};
wrapper = createComponent({ epic });
expect(getExpandIconButton(wrapper).classes()).not.toContain('invisible');
});
it('shows "chevron-right" icon when child epics are not expanded', () => {
wrapper = createComponent();
expect(wrapper.find(GlIcon).attributes('name')).toBe('chevron-right');
});
it('shows "chevron-down" icon when child epics are expanded', () => {
const epic = {
...mockFormattedEpic,
hasChildren: true,
};
wrapper = createComponent({
epic,
childrenFlags: {
'41': { itemExpanded: true },
},
it('is hidden when it is child epic', () => {
const epic = createMockEpic({
isChildEpic: true,
});
expect(wrapper.find(GlIcon).attributes('name')).toBe('chevron-down');
createWrapper({ epic });
expect(getExpandIconButton().classes()).toContain('invisible');
});
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({
epic,
childrenFlags: {
'41': { itemExpanded: true },
},
hasFiltersApplied: true,
isChildrenEmpty: true,
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');
});
});
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');
});
describe('when epic has child epics', () => {
let epic;
beforeEach(() => {
epic = createMockEpic({
id: 41,
hasChildren: true,
children: {
edges: [mockFormattedChildEpic1],
},
descendantCounts: {
openedEpics: 0,
closedEpics: 1,
},
});
createWrapper({ epic });
});
it('has "Collapse child epics" label when child epics are expanded', () => {
const epic = {
...mockFormattedEpic,
hasChildren: true,
};
wrapper = createComponent({
epic,
childrenFlags: {
'41': { itemExpanded: true },
},
it('is shown', () => {
expect(getExpandIconButton().classes()).not.toContain('invisible');
});
expect(getExpandIconButton(wrapper).attributes('aria-label')).toBe('Collapse child epics');
});
it('emits toggleIsEpicExpanded event when clicked', () => {
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
getExpandIconButton().vm.$emit('click');
expect(eventHub.$emit).toHaveBeenCalledWith('toggleIsEpicExpanded', epic);
});
it('has "No child epics match applied filters" label 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 },
},
hasFiltersApplied: true,
isChildrenEmpty: true,
};
beforeEach(() => {
createWrapper({ epic, childrenFlags });
});
it('shows collapse button', () => {
expect(getExpandButtonData()).toEqual({
icon: 'chevron-down',
iconLabel: 'Collapse',
tooltip: 'Collapse',
});
});
describe('when filters are applied', () => {
beforeEach(() => {
createWrapper({
epic,
childrenFlags,
hasFiltersApplied: true,
isChildrenEmpty: true,
});
});
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',
});
});
});
});
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('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('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', () => {
const epic = {
...mockFormattedEpic,
children: {
edges: [mockFormattedChildEpic1],
},
descendantCounts: {
openedEpics: 0,
closedEpics: 1,
},
};
wrapper = createComponent({ epic });
expect(wrapper.find(GlTooltip).text()).toBe('1 child epic');
});
it('has a tooltip with the count and explanation if search is being performed', () => {
const epic = {
...mockFormattedEpic,
children: {
edges: [mockFormattedChildEpic1],
},
descendantCounts: {
openedEpics: 0,
closedEpics: 1,
},
};
wrapper = createComponent({ epic, hasFiltersApplied: true });
expect(wrapper.find(GlTooltip).text()).toBe(
'1 child epic Some child epics may be hidden due to applied filters',
);
});
describe('when child epics are not expanded', () => {
beforeEach(() => {
const childrenFlags = {
'41': { itemExpanded: false },
};
createWrapper({
epic,
childrenFlags,
});
});
it('shows expand button', () => {
expect(getExpandButtonData()).toEqual({
icon: 'chevron-right',
iconLabel: 'Expand',
tooltip: 'Expand',
});
});
});
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);
describe('child epics count', () => {
it('has a tooltip with the count', () => {
createWrapper({ epic });
expect(getChildEpicsCountTooltip().text()).toBe('1 child epic');
});
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('does not render if the user license does not support child epics', () => {
store.state.allowSubEpics = false;
createWrapper({ epic });
expect(getChildEpicsCount().exists()).toBe(false);
});
it('shows the correct count of child epics', () => {
epic = createMockEpic({
children: {
edges: [mockFormattedChildEpic1, mockFormattedChildEpic2],
},
descendantCounts: {
openedEpics: 0,
closedEpics: 2,
},
});
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';
const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate);
const store = createStore();
store.dispatch('setInitialData', {
currentGroupId: mockGroupId,
presetType: PRESET_TYPES.MONTHS,
timeframe: mockTimeframeMonths,
});
store.dispatch('receiveMilestonesSuccess', { rawMilestones });
const mockMilestones = store.state.milestones;
const createComponent = ({
milestones = mockMilestones,
timeframe = mockTimeframeMonths,
currentGroupId = mockGroupId,
presetType = PRESET_TYPES.MONTHS,
} = {}) => {
const localVue = createLocalVue();
return shallowMount(milestonesListSectionComponent, {
localVue,
store,
stubs: {
MilestoneTimeline: false,
},
propsData: {
presetType,
milestones,
timeframe,
currentGroupId,
},
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
const initializeStore = mockTimeframeMonths => {
const store = createStore();
store.dispatch('setInitialData', {
currentGroupId: mockGroupId,
presetType: PRESET_TYPES.MONTHS,
timeframe: mockTimeframeMonths,
});
store.dispatch('receiveMilestonesSuccess', { rawMilestones });
return store;
};
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();
wrapper = shallowMount(milestonesListSectionComponent, {
localVue,
store,
stubs: {
MilestoneTimeline: false,
},
propsData: {
milestones: store.state.milestones,
timeframe: mockTimeframeMonths,
currentGroupId: mockGroupId,
presetType: PRESET_TYPES.MONTHS,
...props,
},
directives: {
GlTooltip: createMockDirective(),
},
});
};
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)];
const writeBindingToElement = (el, { name, value, arg, modifiers }) => {
el[getKey(name)] = {
value,
arg,
modifiers,
};
};
export const createMockDirective = () => ({
bind(el, { name, value, arg, modifiers }) {
el[getKey(name)] = {
value,
arg,
modifiers,
};
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