Commit d57f62c4 authored by Kushal Pandya's avatar Kushal Pandya

Use buffered rendering for Roadmap epics

Makes first load of Roadmap page faster by batch
rendering only those epics which are visible on
viewport. This commit also cleans up Roadmap by
using CSS instead of JS to set component sizes.
parent 08943e5e
......@@ -392,15 +392,21 @@ export const getTimeframeWindowFrom = (initialStartDate, length) => {
* @param {Date} date
* @param {Array} quarter
*/
export const dayInQuarter = (date, quarter) =>
quarter.reduce((acc, month) => {
if (date.getMonth() > month.getMonth()) {
export const dayInQuarter = (date, quarter) => {
const dateValues = {
date: date.getDate(),
month: date.getMonth(),
};
return quarter.reduce((acc, month) => {
if (dateValues.month > month.getMonth()) {
return acc + totalDaysInMonth(month);
} else if (date.getMonth() === month.getMonth()) {
return acc + date.getDate();
} else if (dateValues.month === month.getMonth()) {
return acc + dateValues.date;
}
return acc + 0;
}, 0);
};
window.gl = window.gl || {};
window.gl.utils = {
......
- page_classes = page_class << @html_class
- page_classes = page_classes.flatten.compact
!!! 5
%html{ lang: I18n.locale, class: page_class }
%html{ lang: I18n.locale, class: page_classes }
= render "layouts/head"
%body{ class: "#{user_application_theme} #{@body_class} #{client_class_list}", data: body_data }
= render "layouts/init_auto_complete" if @gfm_form
......
......@@ -28,14 +28,6 @@ export default {
type: Number,
required: true,
},
shellWidth: {
type: Number,
required: true,
},
itemWidth: {
type: Number,
required: true,
},
},
updated() {
this.removeHighlight();
......@@ -75,8 +67,6 @@ export default {
:timeframe="timeframe"
:timeframe-item="timeframeItem"
:epic="epic"
:shell-width="shellWidth"
:item-width="itemWidth"
/>
</div>
</template>
<script>
import { s__, sprintf } from '~/locale';
import { dateInWords } from '~/lib/utils/datetime_utility';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
directives: {
tooltip,
},
props: {
epic: {
type: Object,
......@@ -78,22 +74,13 @@ export default {
<template>
<span class="epic-details-cell" data-qa-selector="epic_details_cell">
<div class="epic-title">
<a v-tooltip :href="epic.webUrl" :title="epic.title" data-container="body" class="epic-url">
{{ epic.title }}
</a>
<a :href="epic.webUrl" :title="epic.title" class="epic-url">{{ epic.title }}</a>
</div>
<div class="epic-group-timeframe">
<span
v-if="isEpicGroupDifferent"
v-tooltip
:title="epic.groupFullName"
class="epic-group"
data-placement="right"
data-container="body"
<span v-if="isEpicGroupDifferent" :title="epic.groupFullName" class="epic-group"
>{{ epic.groupName }} &middot;</span
>
{{ epic.groupName }} &middot;
</span>
<span class="epic-timeframe" v-html="timeframeString"> </span>
<span class="epic-timeframe" v-html="timeframeString"></span>
</div>
</span>
</template>
......@@ -5,11 +5,10 @@ import QuartersPresetMixin from '../mixins/quarters_preset_mixin';
import MonthsPresetMixin from '../mixins/months_preset_mixin';
import WeeksPresetMixin from '../mixins/weeks_preset_mixin';
import eventHub from '../event_hub';
import { EPIC_DETAILS_CELL_WIDTH, TIMELINE_CELL_MIN_WIDTH, PRESET_TYPES } from '../constants';
import { TIMELINE_CELL_MIN_WIDTH, PRESET_TYPES } from '../constants';
export default {
cellWidth: TIMELINE_CELL_MIN_WIDTH,
directives: {
tooltip,
},
......@@ -31,56 +30,28 @@ export default {
type: Object,
required: true,
},
shellWidth: {
type: Number,
required: true,
},
itemWidth: {
type: Number,
required: true,
},
},
data() {
const { startDate, endDate } = this.epic;
return {
timelineBarReady: false,
timelineBarStyles: '',
};
epicStartDateValues: {
day: startDate.getDay(),
date: startDate.getDate(),
month: startDate.getMonth(),
year: startDate.getFullYear(),
time: startDate.getTime(),
},
epicEndDateValues: {
day: endDate.getDay(),
date: endDate.getDate(),
month: endDate.getMonth(),
year: endDate.getFullYear(),
time: endDate.getTime(),
},
computed: {
itemStyles() {
return {
width: `${this.itemWidth}px`,
};
},
showTimelineBar() {
return this.hasStartDate();
},
},
watch: {
shellWidth() {
// Render timeline bar only when shellWidth is updated.
this.renderTimelineBar();
},
},
mounted() {
eventHub.$on('refreshTimeline', this.renderTimelineBar);
},
beforeDestroy() {
eventHub.$off('refreshTimeline', this.renderTimelineBar);
},
methods: {
/**
* Gets cell width based on total number months for
* current timeframe and shellWidth excluding details cell width.
*
* In case cell width is too narrow, we have fixed minimum
* cell width (TIMELINE_CELL_MIN_WIDTH) to obey.
*/
getCellWidth() {
const minWidth = (this.shellWidth - EPIC_DETAILS_CELL_WIDTH) / this.timeframe.length;
return Math.max(minWidth, TIMELINE_CELL_MIN_WIDTH);
},
computed: {
hasStartDate() {
if (this.presetType === PRESET_TYPES.QUARTERS) {
return this.hasStartDateForQuarter();
......@@ -91,35 +62,33 @@ export default {
}
return false;
},
/**
* Renders timeline bar only if current
* timeframe item has startDate for the epic.
*/
renderTimelineBar() {
if (this.hasStartDate()) {
timelineBarStyles() {
let barStyles = {};
if (this.hasStartDate) {
if (this.presetType === PRESET_TYPES.QUARTERS) {
// CSS properties are a false positive: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/24
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
this.timelineBarStyles = `width: ${this.getTimelineBarWidthForQuarters()}px; ${this.getTimelineBarStartOffsetForQuarters()}`;
barStyles = `width: ${this.getTimelineBarWidthForQuarters()}px; ${this.getTimelineBarStartOffsetForQuarters()}`;
} else if (this.presetType === PRESET_TYPES.MONTHS) {
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
this.timelineBarStyles = `width: ${this.getTimelineBarWidthForMonths()}px; ${this.getTimelineBarStartOffsetForMonths()}`;
barStyles = `width: ${this.getTimelineBarWidthForMonths()}px; ${this.getTimelineBarStartOffsetForMonths()}`;
} else if (this.presetType === PRESET_TYPES.WEEKS) {
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
this.timelineBarStyles = `width: ${this.getTimelineBarWidthForWeeks()}px; ${this.getTimelineBarStartOffsetForWeeks()}`;
barStyles = `width: ${this.getTimelineBarWidthForWeeks()}px; ${this.getTimelineBarStartOffsetForWeeks()}`;
}
this.timelineBarReady = true;
}
return barStyles;
},
},
};
</script>
<template>
<span :style="itemStyles" class="epic-timeline-cell" data-qa-selector="epic_timeline_cell">
<span class="epic-timeline-cell" data-qa-selector="epic_timeline_cell">
<div class="timeline-bar-wrapper">
<a
v-if="showTimelineBar"
v-if="hasStartDate"
:href="epic.webUrl"
:class="{
'start-date-undefined': epic.startDateUndefined,
......@@ -127,8 +96,7 @@ export default {
}"
:style="timelineBarStyles"
class="timeline-bar"
>
</a>
></a>
</div>
</span>
</template>
<script>
import eventHub from '../event_hub';
import { mapState, mapActions } from 'vuex';
import VirtualList from 'vue-virtual-scroll-list';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import SectionMixin from '../mixins/section_mixin';
import eventHub from '../event_hub';
import { TIMELINE_CELL_MIN_WIDTH, EPIC_ITEM_HEIGHT } from '../constants';
import { EPIC_DETAILS_CELL_WIDTH, TIMELINE_CELL_MIN_WIDTH, EPIC_ITEM_HEIGHT } from '../constants';
import epicItem from './epic_item.vue';
import EpicItem from './epic_item.vue';
export default {
EpicItem,
epicItemHeight: EPIC_ITEM_HEIGHT,
components: {
epicItem,
VirtualList,
EpicItem,
},
mixins: [SectionMixin],
mixins: [glFeatureFlagsMixin()],
props: {
presetType: {
type: String,
......@@ -29,33 +35,23 @@ export default {
type: Number,
required: true,
},
shellWidth: {
type: Number,
required: true,
},
listScrollable: {
type: Boolean,
required: true,
},
},
data() {
return {
shellHeight: 0,
emptyRowHeight: 0,
showEmptyRow: false,
offsetLeft: 0,
emptyRowContainerStyles: {},
showBottomShadow: false,
roadmapShellEl: null,
};
},
computed: {
emptyRowContainerStyles() {
return {
height: `${this.emptyRowHeight}px`,
};
...mapState(['bufferSize']),
emptyRowContainerVisible() {
return this.epics.length < this.bufferSize;
},
emptyRowCellStyles() {
sectionContainerStyles() {
return {
width: `${this.sectionItemWidth}px`,
width: `${EPIC_DETAILS_CELL_WIDTH + TIMELINE_CELL_MIN_WIDTH * this.timeframe.length}px`,
};
},
shadowCellStyles() {
......@@ -64,123 +60,97 @@ export default {
};
},
},
watch: {
shellWidth: function shellWidth() {
// Scroll view to today indicator only when shellWidth is updated.
this.scrollToTodayIndicator();
// Initialize offsetLeft when shellWidth is updated
this.offsetLeft = this.$el.parentElement.offsetLeft;
},
},
mounted() {
eventHub.$on('epicsListScrolled', this.handleEpicsListScroll);
eventHub.$on('refreshTimeline', this.handleTimelineRefresh);
this.$nextTick(() => {
this.initMounted();
});
},
beforeDestroy() {
eventHub.$off('epicsListScrolled', this.handleEpicsListScroll);
eventHub.$off('refreshTimeline', this.handleTimelineRefresh);
},
methods: {
...mapActions(['setBufferSize']),
initMounted() {
// Get available shell height based on viewport height
this.shellHeight = window.innerHeight - this.$el.offsetTop;
this.roadmapShellEl = this.$root.$el && this.$root.$el.firstChild;
this.setBufferSize(Math.ceil((window.innerHeight - this.$el.offsetTop) / EPIC_ITEM_HEIGHT));
// In case there are epics present, initialize empty row
if (this.epics.length) {
this.initEmptyRow();
}
// Wait for component render to complete
this.$nextTick(() => {
this.offsetLeft = (this.$el.parentElement && this.$el.parentElement.offsetLeft) || 0;
eventHub.$emit('epicsListRendered', {
width: this.$el.clientWidth,
height: this.shellHeight,
// We cannot scroll to the indicator immediately
// on render as it will trigger scroll event leading
// to timeline expand, so we wait for another render
// cycle to complete.
this.$nextTick(() => {
this.scrollToTodayIndicator();
});
},
/**
* In case number of epics in the list are not sufficient
* to fill in full page height, we need to show an empty row
* at the bottom with fixed absolute height such that the
* column rulers expand to full page height
*
* This method calculates absolute height for empty column in pixels
* based on height of available list items and sets it to component
* props.
*/
initEmptyRow() {
const children = this.$children;
let approxChildrenHeight = children[0].$el.clientHeight * this.epics.length;
// Check if approximate height is greater than shell height
if (approxChildrenHeight < this.shellHeight) {
// reset approximate height and recalculate actual height
approxChildrenHeight = 0;
children.forEach(child => {
// accumulate children height
// compensate for bottom border
approxChildrenHeight += child.$el.clientHeight;
if (!Object.keys(this.emptyRowContainerStyles).length) {
this.emptyRowContainerStyles = this.getEmptyRowContainerStyles();
}
});
// set height and show empty row reducing horizontal scrollbar size
this.emptyRowHeight = this.shellHeight - approxChildrenHeight;
this.showEmptyRow = true;
} else {
this.showBottomShadow = true;
},
getEmptyRowContainerStyles() {
if (this.$refs.epicItems && this.$refs.epicItems.length) {
return {
height: `${this.$el.clientHeight -
this.epics.length * this.$refs.epicItems[0].$el.clientHeight}px`,
};
}
return {};
},
/**
* Scroll timeframe to the right of the timeline
* by half the column size
*/
scrollToTodayIndicator() {
this.$el.parentElement.scrollBy(TIMELINE_CELL_MIN_WIDTH / 2, 0);
},
/**
* Method to update list section when refreshTimeline event
* is emitted on eventHub
*
* This method ensures that empty row is toggled
* based on whether list has enough epics to make
* list vertically scrollable.
*/
handleTimelineRefresh({ initialRender = false }) {
// Initialize emptyRow only once.
if (initialRender) {
this.initEmptyRow();
}
// Check if container height is less than total height of all Epic
// items combined (AKA list is scrollable).
const offsetHeight = this.$el.parentElement
? this.$el.parentElement.offsetHeight
: this.$el.offsetHeight;
const isListVertScrollable = offsetHeight < EPIC_ITEM_HEIGHT * (this.epics.length + 1);
// Toggle empty row.
this.showEmptyRow = !isListVertScrollable;
if (this.$el.parentElement) this.$el.parentElement.scrollBy(TIMELINE_CELL_MIN_WIDTH / 2, 0);
},
handleEpicsListScroll({ scrollTop, clientHeight, scrollHeight }) {
this.showBottomShadow = Math.ceil(scrollTop) + clientHeight < scrollHeight;
},
getEpicItemProps(index) {
return {
key: index,
props: {
epic: this.epics[index],
presetType: this.presetType,
timeframe: this.timeframe,
currentGroupId: this.currentGroupId,
},
};
},
},
};
</script>
<template>
<div :style="sectionContainerStyles" class="epics-list-section">
<template v-if="glFeatures.roadmapBufferedRendering && !emptyRowContainerVisible">
<virtual-list
v-if="epics.length"
:size="$options.epicItemHeight"
:remain="bufferSize"
:bench="bufferSize"
:scrollelement="roadmapShellEl"
:item="$options.EpicItem"
:itemcount="epics.length"
:itemprops="getEpicItemProps"
/>
</template>
<template v-else>
<epic-item
v-for="(epic, index) in epics"
ref="epicItems"
:key="index"
:preset-type="presetType"
:epic="epic"
:timeframe="timeframe"
:current-group-id="currentGroupId"
:shell-width="sectionShellWidth"
:item-width="sectionItemWidth"
/>
</template>
<div
v-if="showEmptyRow"
v-if="emptyRowContainerVisible"
:style="emptyRowContainerStyles"
class="epics-list-item epics-list-item-empty clearfix"
>
......@@ -188,11 +158,9 @@ export default {
<span
v-for="(timeframeItem, index) in timeframe"
:key="index"
:style="emptyRowCellStyles"
class="epic-timeline-cell"
>
</span>
></span>
</div>
<div v-if="showBottomShadow" :style="shadowCellStyles" class="scroll-bottom-shadow"></div>
<div v-show="showBottomShadow" :style="shadowCellStyles" class="scroll-bottom-shadow"></div>
</div>
</template>
......@@ -20,10 +20,6 @@ export default {
type: Array,
required: true,
},
itemWidth: {
type: Number,
required: true,
},
},
data() {
const currentDate = new Date();
......@@ -36,11 +32,6 @@ export default {
};
},
computed: {
itemStyles() {
return {
width: `${this.itemWidth}px`,
};
},
timelineHeaderLabel() {
const year = this.timeframeItem.getFullYear();
const month = monthInWords(this.timeframeItem, true);
......@@ -85,7 +76,7 @@ export default {
</script>
<template>
<span :style="itemStyles" class="timeline-header-item">
<span class="timeline-header-item">
<div :class="timelineHeaderClass" class="item-label">{{ timelineHeaderLabel }}</div>
<months-header-sub-item :timeframe-item="timeframeItem" :current-date="currentDate" />
</span>
......
......@@ -18,10 +18,6 @@ export default {
type: Array,
required: true,
},
itemWidth: {
type: Number,
required: true,
},
},
data() {
const currentDate = new Date();
......@@ -32,11 +28,6 @@ export default {
};
},
computed: {
itemStyles() {
return {
width: `${this.itemWidth}px`,
};
},
quarterBeginDate() {
return this.timeframeItem.range[0];
},
......@@ -66,7 +57,7 @@ export default {
</script>
<template>
<span :style="itemStyles" class="timeline-header-item">
<span class="timeline-header-item">
<div :class="timelineHeaderClass" class="item-label">{{ timelineHeaderLabel }}</div>
<quarters-header-sub-item :timeframe-item="timeframeItem" :current-date="currentDate" />
</span>
......
......@@ -20,10 +20,6 @@ export default {
type: Array,
required: true,
},
itemWidth: {
type: Number,
required: true,
},
},
data() {
const currentDate = new Date();
......@@ -34,11 +30,6 @@ export default {
};
},
computed: {
itemStyles() {
return {
width: `${this.itemWidth}px`,
};
},
lastDayOfCurrentWeek() {
const lastDayOfCurrentWeek = new Date(this.timeframeItem.getTime());
lastDayOfCurrentWeek.setDate(lastDayOfCurrentWeek.getDate() + 7);
......@@ -77,7 +68,7 @@ export default {
</script>
<template>
<span :style="itemStyles" class="timeline-header-item">
<span class="timeline-header-item">
<div :class="timelineHeaderClass" class="item-label">{{ timelineHeaderLabel }}</div>
<weeks-header-sub-item :timeframe-item="timeframeItem" :current-date="currentDate" />
</span>
......
<script>
import { mapState, mapActions } from 'vuex';
import _ from 'underscore';
import epicsListEmpty from './epics_list_empty.vue';
import roadmapShell from './roadmap_shell.vue';
......@@ -34,7 +33,6 @@ export default {
data() {
const roadmapGraphQL = gon.features && gon.features.roadmapGraphql;
return {
handleResizeThrottled: {},
// TODO
// Remove these method alias and call actual
// method once feature flag is removed.
......@@ -73,25 +71,8 @@ export default {
);
},
},
watch: {
epicsFetchInProgress(value) {
if (!value && this.epics.length) {
this.$nextTick(() => {
eventHub.$emit('refreshTimeline', {
todayBarReady: true,
initialRender: true,
});
});
}
},
},
mounted() {
this.fetchEpicsFn();
this.handleResizeThrottled = _.throttle(this.handleResize, 600);
window.addEventListener('resize', this.handleResizeThrottled, false);
},
beforeDestroy() {
window.removeEventListener('resize', this.handleResizeThrottled, false);
},
methods: {
...mapActions([
......@@ -103,28 +84,6 @@ export default {
'extendTimeframe',
'refreshEpicDates',
]),
/**
* Roadmap view works with absolute sizing and positioning
* of following child components of RoadmapShell;
*
* - RoadmapTimelineSection
* - TimelineTodayIndicator
* - EpicItemTimeline
*
* And hence when window is resized, any size attributes passed
* down to child components are no longer valid, so best approach
* to refresh entire app is to re-render it on resize, hence
* we toggle `windowResizeInProgress` variable which is bound
* to `RoadmapShell`.
*/
handleResize() {
this.setWindowResizeInProgress(true);
// We need to debounce the toggle to make sure loading animation
// shows up while app is being rerendered.
_.debounce(() => {
this.setWindowResizeInProgress(false);
}, 200)();
},
/**
* Once timeline is expanded (either with prepend or append)
* We need performing following actions;
......
......@@ -3,7 +3,7 @@ import { mapState } from 'vuex';
import { GlSkeletonLoading } from '@gitlab/ui';
import { isInViewport } from '~/lib/utils/common_utils';
import { SCROLL_BAR_SIZE, EPIC_ITEM_HEIGHT, EXTEND_AS } from '../constants';
import { EXTEND_AS } from '../constants';
import eventHub from '../event_hub';
import epicsListSection from './epics_list_section.vue';
......@@ -35,50 +35,14 @@ export default {
},
data() {
return {
shellWidth: 0,
shellHeight: 0,
noScroll: false,
timeframeStartOffset: 0,
};
},
computed: {
...mapState(['defaultInnerHeight']),
containerStyles() {
return {
width: `${this.shellWidth}px`,
height: `${this.shellHeight}px`,
};
},
},
watch: {
/**
* Watcher to monitor whether epics list is long enough
* to allow vertical list scrolling.
*
* In case of scrollable list, we don't want vertical scrollbar
* to be visible, so we mask the scrollbar by increasing shell
* width past the scrollbar size.
*/
noScroll(value) {
if (this.$el.parentElement) {
this.shellWidth = this.getShellWidth(value);
}
},
},
mounted() {
eventHub.$on('refreshTimeline', this.handleEpicsListRendered);
this.$nextTick(() => {
// Client width at the time of component mount will not
// provide accurate size of viewport until child contents are
// actually loaded and rendered into the DOM, hence
// we wait for nextTick which ensures DOM update has completed
// before setting shellWidth
// see https://vuejs.org/v2/api/#Vue-nextTick
if (this.$el.parentElement) {
this.shellHeight = (this.defaultInnerHeight || window.innerHeight) - this.$el.offsetTop;
this.noScroll = this.shellHeight > EPIC_ITEM_HEIGHT * (this.epics.length + 1);
this.shellWidth = this.getShellWidth(this.noScroll);
// We're guarding this as in tests, `roadmapTimeline`
// is not ready when this line is executed.
if (this.$refs.roadmapTimeline) {
......@@ -87,19 +51,9 @@ export default {
.querySelector('.item-sublabel .sublabel-value:first-child')
.getBoundingClientRect().left;
}
}
});
},
beforeDestroy() {
eventHub.$off('refreshTimeline', this.handleEpicsListRendered);
},
methods: {
getShellWidth(noScroll) {
return this.$el.parentElement.clientWidth + (noScroll ? 0 : SCROLL_BAR_SIZE);
},
handleEpicsListRendered() {
this.noScroll = this.shellHeight > EPIC_ITEM_HEIGHT * (this.epics.length + 1);
},
handleScroll() {
const { scrollTop, scrollLeft, clientHeight, scrollHeight } = this.$el;
const timelineEdgeStartEl = this.$refs.roadmapTimeline.$el
......@@ -124,20 +78,12 @@ export default {
</script>
<template>
<div
:class="{ 'prevent-vertical-scroll': noScroll }"
:style="containerStyles"
class="roadmap-shell"
data-qa-selector="roadmap_shell"
@scroll="handleScroll"
>
<div class="roadmap-shell" data-qa-selector="roadmap_shell" @scroll="handleScroll">
<roadmap-timeline-section
ref="roadmapTimeline"
:preset-type="presetType"
:epics="epics"
:timeframe="timeframe"
:shell-width="shellWidth"
:list-scrollable="!noScroll"
/>
<div v-if="!epics.length" class="skeleton-loader js-skeleton-loader">
<div v-for="n in 10" :key="n" class="mt-2">
......@@ -145,12 +91,11 @@ export default {
</div>
</div>
<epics-list-section
v-else
:preset-type="presetType"
:epics="epics"
:timeframe="timeframe"
:shell-width="shellWidth"
:current-group-id="currentGroupId"
:list-scrollable="!noScroll"
/>
</div>
</template>
<script>
import eventHub from '../event_hub';
import { PRESET_TYPES } from '../constants';
import SectionMixin from '../mixins/section_mixin';
import { EPIC_DETAILS_CELL_WIDTH, TIMELINE_CELL_MIN_WIDTH, PRESET_TYPES } from '../constants';
import QuartersHeaderItem from './preset_quarters/quarters_header_item.vue';
import MonthsHeaderItem from './preset_months/months_header_item.vue';
......@@ -15,7 +13,6 @@ export default {
MonthsHeaderItem,
WeeksHeaderItem,
},
mixins: [SectionMixin],
props: {
presetType: {
type: String,
......@@ -29,14 +26,6 @@ export default {
type: Array,
required: true,
},
shellWidth: {
type: Number,
required: true,
},
listScrollable: {
type: Boolean,
required: true,
},
},
data() {
return {
......@@ -54,6 +43,11 @@ export default {
}
return '';
},
sectionContainerStyles() {
return {
width: `${EPIC_DETAILS_CELL_WIDTH + TIMELINE_CELL_MIN_WIDTH * this.timeframe.length}px`,
};
},
},
mounted() {
eventHub.$on('epicsListScrolled', this.handleEpicsListScroll);
......@@ -84,7 +78,6 @@ export default {
:timeframe-index="index"
:timeframe-item="timeframeItem"
:timeframe="timeframe"
:item-width="sectionItemWidth"
/>
</div>
</template>
<script>
import { totalDaysInMonth, dayInQuarter, totalDaysInQuarter } from '~/lib/utils/datetime_utility';
import { EPIC_DETAILS_CELL_WIDTH, PRESET_TYPES, DAYS_IN_WEEK } from '../constants';
import { EPIC_DETAILS_CELL_WIDTH, PRESET_TYPES, DAYS_IN_WEEK, SCROLL_BAR_SIZE } from '../constants';
import eventHub from '../event_hub';
......@@ -22,29 +22,22 @@ export default {
},
data() {
return {
todayBarStyles: '',
todayBarReady: false,
todayBarStyles: {},
todayBarReady: true,
};
},
mounted() {
eventHub.$on('epicsListRendered', this.handleEpicsListRender);
eventHub.$on('epicsListScrolled', this.handleEpicsListScroll);
eventHub.$on('refreshTimeline', this.handleEpicsListRender);
this.$nextTick(() => {
this.todayBarStyles = this.getTodayBarStyles();
});
},
beforeDestroy() {
eventHub.$off('epicsListRendered', this.handleEpicsListRender);
eventHub.$off('epicsListScrolled', this.handleEpicsListScroll);
eventHub.$off('refreshTimeline', this.handleEpicsListRender);
},
methods: {
/**
* This method takes height of current shell
* and renders vertical line over the area where
* today falls in current timeline
*/
handleEpicsListRender({ todayBarReady }) {
let left = 0;
let height = 0;
getTodayBarStyles() {
let left;
// Get total days of current timeframe Item and then
// get size in % from current date and days in range
......@@ -63,21 +56,10 @@ export default {
left = Math.floor(((this.currentDate.getDay() + 1) / DAYS_IN_WEEK) * 100 - DAYS_IN_WEEK);
}
// On initial load, container element height is 0
if (this.$root.$el.clientHeight === 0) {
height = window.innerHeight - this.$root.$el.offsetTop;
} else {
// When list is scrollable both vertically and horizontally
// We set height using top-level parent container height & position of
// today indicator element container.
height = this.$root.$el.clientHeight - this.$el.parentElement.offsetTop;
}
this.todayBarStyles = {
height: `${height}px`,
return {
left: `${left}%`,
height: `calc(100vh - ${this.$el.getBoundingClientRect().y + SCROLL_BAR_SIZE}px)`,
};
this.todayBarReady = todayBarReady === undefined ? true : todayBarReady;
},
handleEpicsListScroll() {
const indicatorX = this.$el.getBoundingClientRect().x;
......@@ -91,5 +73,5 @@ export default {
</script>
<template>
<span :class="{ invisible: !todayBarReady }" :style="todayBarStyles" class="today-bar"> </span>
<span :class="{ invisible: !todayBarReady }" :style="todayBarStyles" class="today-bar"></span>
</template>
......@@ -6,12 +6,14 @@ export const EPIC_ITEM_HEIGHT = 50;
export const TIMELINE_CELL_MIN_WIDTH = 180;
export const SCROLL_BAR_SIZE = 15;
export const SCROLL_BAR_SIZE = 16;
export const EPIC_HIGHLIGHT_REMOVE_AFTER = 3000;
export const DAYS_IN_WEEK = 7;
export const BUFFER_OVERLAP_SIZE = 20;
export const PRESET_TYPES = {
QUARTERS: 'QUARTERS',
MONTHS: 'MONTHS',
......
......@@ -7,18 +7,18 @@ export default {
*/
hasStartDateForMonth() {
return (
this.epic.startDate.getMonth() === this.timeframeItem.getMonth() &&
this.epic.startDate.getFullYear() === this.timeframeItem.getFullYear()
this.epicStartDateValues.month === this.timeframeItem.getMonth() &&
this.epicStartDateValues.year === this.timeframeItem.getFullYear()
);
},
/**
* Check if current epic ends within current month (timeline cell)
*/
isTimeframeUnderEndDateForMonth(timeframeItem, epicEndDate) {
if (epicEndDate.getFullYear() <= timeframeItem.getFullYear()) {
return epicEndDate.getMonth() === timeframeItem.getMonth();
isTimeframeUnderEndDateForMonth(timeframeItem) {
if (this.epicEndDateValues.year <= timeframeItem.getFullYear()) {
return this.epicEndDateValues.month === timeframeItem.getMonth();
}
return epicEndDate.getTime() < timeframeItem.getTime();
return this.epicEndDateValues.time < timeframeItem.getTime();
},
/**
* Return timeline bar width for current month (timeline cell) based on
......@@ -42,7 +42,7 @@ export default {
*/
getTimelineBarStartOffsetForMonths() {
const daysInMonth = totalDaysInMonth(this.timeframeItem);
const startDate = this.epic.startDate.getDate();
const startDate = this.epicStartDateValues.date;
if (
this.epic.startDateOutOfRange ||
......@@ -88,9 +88,9 @@ export default {
let timelineBarWidth = 0;
const indexOfCurrentMonth = this.timeframe.indexOf(this.timeframeItem);
const cellWidth = this.getCellWidth();
const epicStartDate = this.epic.startDate;
const epicEndDate = this.epic.endDate;
const { cellWidth } = this.$options;
const epicStartDate = this.epicStartDateValues;
const epicEndDate = this.epicEndDateValues;
// Start iteration from current month
for (let i = indexOfCurrentMonth; i < this.timeframe.length; i += 1) {
......@@ -99,13 +99,13 @@ export default {
if (i === indexOfCurrentMonth) {
// If this is current month
if (this.isTimeframeUnderEndDateForMonth(this.timeframe[i], epicEndDate)) {
if (this.isTimeframeUnderEndDateForMonth(this.timeframe[i])) {
// If Epic endDate falls under the range of current timeframe month
// then get width for number of days between start and end dates (inclusive)
timelineBarWidth += this.getBarWidthForSingleMonth(
cellWidth,
daysInMonth,
epicEndDate.getDate() - epicStartDate.getDate() + 1,
epicEndDate.date - epicStartDate.date + 1,
);
// Break as Epic start and end date fall within current timeframe month itself!
break;
......@@ -115,18 +115,17 @@ export default {
// If start date is first day of the month,
// we need width of full cell (i.e. total days of month)
// otherwise, we need width only for date from total days of month.
const date =
epicStartDate.getDate() === 1 ? daysInMonth : daysInMonth - epicStartDate.getDate();
const date = epicStartDate.date === 1 ? daysInMonth : daysInMonth - epicStartDate.date;
timelineBarWidth += this.getBarWidthForSingleMonth(cellWidth, daysInMonth, date);
}
} else if (this.isTimeframeUnderEndDateForMonth(this.timeframe[i], epicEndDate)) {
} else if (this.isTimeframeUnderEndDateForMonth(this.timeframe[i])) {
// If this is NOT current month but epicEndDate falls under
// current timeframe month then calculate width
// based on date of the month
timelineBarWidth += this.getBarWidthForSingleMonth(
cellWidth,
daysInMonth,
epicEndDate.getDate(),
epicEndDate.date,
);
// Break as Epic end date falls within current timeframe month!
break;
......
......@@ -10,17 +10,17 @@ export default {
const quarterEnd = this.timeframeItem.range[2];
return (
this.epic.startDate.getTime() >= quarterStart.getTime() &&
this.epic.startDate.getTime() <= quarterEnd.getTime()
this.epicStartDateValues.time >= quarterStart.getTime() &&
this.epicStartDateValues.time <= quarterEnd.getTime()
);
},
/**
* Check if current epic ends within current quarter (timeline cell)
*/
isTimeframeUnderEndDateForQuarter(timeframeItem, epicEndDate) {
isTimeframeUnderEndDateForQuarter(timeframeItem) {
const quarterEnd = timeframeItem.range[2];
return epicEndDate.getTime() <= quarterEnd.getTime();
return this.epicEndDateValues.time <= quarterEnd.getTime();
},
/**
* Return timeline bar width for current quarter (timeline cell) based on
......@@ -89,7 +89,7 @@ export default {
let timelineBarWidth = 0;
const indexOfCurrentQuarter = this.timeframe.indexOf(this.timeframeItem);
const cellWidth = this.getCellWidth();
const { cellWidth } = this.$options;
const epicStartDate = this.epic.startDate;
const epicEndDate = this.epic.endDate;
......@@ -97,7 +97,7 @@ export default {
const currentQuarter = this.timeframe[i].range;
if (i === indexOfCurrentQuarter) {
if (this.isTimeframeUnderEndDateForQuarter(this.timeframe[i], epicEndDate)) {
if (this.isTimeframeUnderEndDateForQuarter(this.timeframe[i])) {
timelineBarWidth += this.getBarWidthForSingleQuarter(
cellWidth,
totalDaysInQuarter(currentQuarter),
......@@ -117,7 +117,7 @@ export default {
date,
);
}
} else if (this.isTimeframeUnderEndDateForQuarter(this.timeframe[i], epicEndDate)) {
} else if (this.isTimeframeUnderEndDateForQuarter(this.timeframe[i])) {
timelineBarWidth += this.getBarWidthForSingleQuarter(
cellWidth,
totalDaysInQuarter(currentQuarter),
......
import { EPIC_DETAILS_CELL_WIDTH, TIMELINE_CELL_MIN_WIDTH, SCROLL_BAR_SIZE } from '../constants';
export default {
computed: {
/**
* Return section width after reducing scrollbar size
* based on listScrollable such that Epic item cells
* do not consider scrollbar presence in shellWidth
*/
sectionShellWidth() {
return this.shellWidth - (this.listScrollable ? SCROLL_BAR_SIZE : 0);
},
sectionItemWidth() {
const timeframeLength = this.timeframe.length;
// Calculate minimum width for single cell
// based on total number of months in current timeframe
// and available shellWidth
const width = (this.sectionShellWidth - EPIC_DETAILS_CELL_WIDTH) / timeframeLength;
// When shellWidth is too low, we need to obey global
// minimum cell width.
return Math.max(width, TIMELINE_CELL_MIN_WIDTH);
},
sectionContainerStyles() {
const width = EPIC_DETAILS_CELL_WIDTH + this.sectionItemWidth * this.timeframe.length;
return {
width: `${width}px`,
};
},
},
};
......@@ -11,8 +11,8 @@ export default {
lastDayOfWeek.setDate(lastDayOfWeek.getDate() + 6);
return (
this.epic.startDate.getTime() >= firstDayOfWeek.getTime() &&
this.epic.startDate.getTime() <= lastDayOfWeek.getTime()
this.epicStartDateValues.time >= firstDayOfWeek.getTime() &&
this.epicStartDateValues.time <= lastDayOfWeek.getTime()
);
},
/**
......@@ -26,9 +26,9 @@ export default {
/**
* Check if current epic ends within current week (timeline cell)
*/
isTimeframeUnderEndDateForWeek(timeframeItem, epicEndDate) {
isTimeframeUnderEndDateForWeek(timeframeItem) {
const lastDayOfWeek = this.getLastDayOfWeek(timeframeItem);
return epicEndDate.getTime() <= lastDayOfWeek.getTime();
return this.epicEndDateValues.time <= lastDayOfWeek.getTime();
},
/**
* Return timeline bar width for current week (timeline cell) based on
......@@ -55,8 +55,8 @@ export default {
*/
getTimelineBarStartOffsetForWeeks() {
const daysInWeek = 7;
const dayWidth = this.getCellWidth() / daysInWeek;
const startDate = this.epic.startDate.getDay() + 1;
const dayWidth = this.$options.cellWidth / daysInWeek;
const startDate = this.epicStartDateValues.day + 1;
const firstDayOfWeek = this.timeframeItem.getDay() + 1;
if (
......@@ -98,24 +98,24 @@ export default {
let timelineBarWidth = 0;
const indexOfCurrentWeek = this.timeframe.indexOf(this.timeframeItem);
const cellWidth = this.getCellWidth();
const epicStartDate = this.epic.startDate;
const epicEndDate = this.epic.endDate;
const { cellWidth } = this.$options;
const epicStartDate = this.epicStartDateValues;
const epicEndDate = this.epicEndDateValues;
for (let i = indexOfCurrentWeek; i < this.timeframe.length; i += 1) {
if (i === indexOfCurrentWeek) {
if (this.isTimeframeUnderEndDateForWeek(this.timeframe[i], epicEndDate)) {
if (this.isTimeframeUnderEndDateForWeek(this.timeframe[i])) {
timelineBarWidth += this.getBarWidthForSingleWeek(
cellWidth,
epicEndDate.getDay() - epicStartDate.getDay() + 1,
epicEndDate.day - epicStartDate.day + 1,
);
break;
} else {
const date = epicStartDate.getDay() === 0 ? 7 : 7 - epicStartDate.getDay();
const date = epicStartDate.day === 0 ? 7 : 7 - epicStartDate.day;
timelineBarWidth += this.getBarWidthForSingleWeek(cellWidth, date);
}
} else if (this.isTimeframeUnderEndDateForWeek(this.timeframe[i], epicEndDate)) {
timelineBarWidth += this.getBarWidthForSingleWeek(cellWidth, epicEndDate.getDay() + 1);
} else if (this.isTimeframeUnderEndDateForWeek(this.timeframe[i])) {
timelineBarWidth += this.getBarWidthForSingleWeek(cellWidth, epicEndDate.day + 1);
break;
} else {
timelineBarWidth += this.getBarWidthForSingleWeek(cellWidth, 7);
......
......@@ -84,7 +84,7 @@ export const receiveEpicsSuccess = (
// Exclude any Epic that has invalid dates
// or is already present in Roadmap timeline
if (
formattedEpic.startDate <= formattedEpic.endDate &&
formattedEpic.startDate.getTime() <= formattedEpic.endDate.getTime() &&
state.epicIds.indexOf(formattedEpic.id) < 0
) {
Object.assign(formattedEpic, {
......@@ -192,5 +192,7 @@ export const refreshEpicDates = ({ commit, state, getters }) => {
commit(types.SET_EPICS, epics);
};
export const setBufferSize = ({ commit }, bufferSize) => commit(types.SET_BUFFER_SIZE, bufferSize);
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
......@@ -15,3 +15,5 @@ export const RECEIVE_EPICS_FAILURE = 'RECEIVE_EPICS_FAILURE';
export const PREPEND_TIMEFRAME = 'PREPEND_TIMEFRAME';
export const APPEND_TIMEFRAME = 'APPEND_TIMEFRAME';
export const SET_BUFFER_SIZE = 'SET_BUFFER_SIZE';
......@@ -50,4 +50,8 @@ export default {
state.extendedTimeframe = extendedTimeframe;
state.timeframe.push(...extendedTimeframe);
},
[types.SET_BUFFER_SIZE](state, bufferSize) {
state.bufferSize = bufferSize;
},
};
......@@ -9,6 +9,7 @@ export default () => ({
// Data
epicIid: '',
epics: [],
visibleEpics: [],
epicIds: [],
currentGroupId: -1,
fullPath: '',
......@@ -16,6 +17,7 @@ export default () => ({
extendedTimeframe: [],
presetType: '',
sortedBy: '',
bufferSize: 0,
// UI Flags
defaultInnerHeight: 0,
......
......@@ -19,30 +19,43 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0%
}
}
@keyframes fadeInDetails {
from {
opacity: 0;
}
@mixin roadmap-scroll-mixin {
height: $grid-size;
width: $details-cell-width;
pointer-events: none;
}
to {
opacity: 1;
html.group-epics-roadmap-html {
height: 100%;
// We need to reset this just for Roadmap page
overflow-y: initial;
}
.with-performance-bar {
.group-epics-roadmap-body {
$header-size: $performance-bar-height + $header-height;
height: calc(100% - #{$header-size});
}
}
@keyframes fadeinTimelineBar {
from {
opacity: 0;
.group-epics-roadmap-body {
height: calc(100% - #{$header-height});
.page-with-contextual-sidebar {
height: 100%;
}
to {
opacity: 0.75;
.group-epics-roadmap {
// This size is total of breadcrumb height and computed height of
// filters container (70px)
$header-size: $breadcrumb-min-height + 70px;
height: calc(100% - #{$header-size});
}
}
@mixin roadmap-scroll-mixin {
height: $grid-size;
width: $details-cell-width;
pointer-events: none;
.group-epics-roadmap-wrapper,
.group-epics-roadmap .content {
height: 100%;
}
}
.epics-details-filters {
......@@ -100,6 +113,7 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0%
.roadmap-container {
overflow: hidden;
height: 100%;
&.overflow-reset {
overflow: initial;
......@@ -108,12 +122,13 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0%
.roadmap-shell {
position: relative;
height: 100%;
width: 100%;
overflow-x: auto;
.skeleton-loader {
position: absolute;
top: $header-item-height;
left: $timeline-cell-width / 2;
width: $details-cell-width;
height: 100%;
padding-top: $gl-padding-top;
......@@ -171,6 +186,8 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0%
}
.timeline-header-item {
width: $timeline-cell-width;
&:last-of-type .item-label {
border-right: 0;
}
......@@ -241,6 +258,8 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0%
}
.epics-list-section {
height: calc(100% - 60px);
.epics-list-item {
&:hover {
.epic-details-cell,
......@@ -250,6 +269,8 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0%
}
&.epics-list-item-empty {
height: 100%;
&:hover {
.epic-details-cell,
.epic-timeline-cell {
......@@ -295,7 +316,7 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0%
.epic-title,
.epic-group-timeframe {
animation: fadeInDetails 1s;
will-change: contents;
}
.epic-title {
......@@ -327,6 +348,7 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0%
}
.epic-timeline-cell {
width: $timeline-cell-width;
background-color: transparent;
border-right: $border-style;
......@@ -341,7 +363,7 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0%
background-color: $blue-500;
border-radius: $border-radius-default;
opacity: 0.75;
animation: fadeinTimelineBar 1s;
will-change: width, left;
&:hover {
opacity: 1;
......
......@@ -12,6 +12,7 @@ module Groups
before_action :persist_roadmap_layout, only: [:show]
before_action do
push_frontend_feature_flag(:roadmap_graphql, @group)
push_frontend_feature_flag(:roadmap_buffered_rendering, @group)
end
# show roadmap for a group
......
- @no_breadcrumb_container = true
- @no_container = true
- @html_class = "group-epics-roadmap-html"
- @body_class = "group-epics-roadmap-body"
- @content_wrapper_class = "group-epics-roadmap-wrapper"
- @content_class = "group-epics-roadmap"
- breadcrumb_title _("Epics Roadmap")
......
......@@ -76,18 +76,6 @@ describe 'group epic roadmap', :js do
expect(page).to have_selector('.epics-list-item .epic-title', count: 3)
end
end
it 'resizing browser window causes Roadmap to re-render' do
page.within('.group-epics-roadmap .roadmap-container') do
initial_style = find('.roadmap-shell')[:style]
page.current_window.resize_to(2500, 1000)
wait_for_requests
expect(find('.roadmap-shell')[:style]).not_to eq(initial_style)
restore_window_size
end
end
end
describe 'roadmap page with epics state filter' do
......
......@@ -140,7 +140,7 @@ describe('EpicItemDetailsComponent', () => {
expect(epicGroupNameEl).not.toBeNull();
expect(epicGroupNameEl.innerText.trim()).toContain(mockEpicItem.groupName);
expect(epicGroupNameEl.dataset.originalTitle).toBe(mockEpicItem.groupFullName);
expect(epicGroupNameEl.getAttribute('title')).toBe(mockEpicItem.groupFullName);
});
it('renders Epic timeframe', () => {
......
......@@ -9,13 +9,7 @@ import { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils';
import { PRESET_TYPES } from 'ee/roadmap/constants';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import {
mockTimeframeInitialDate,
mockEpic,
mockGroupId,
mockShellWidth,
mockItemWidth,
} from '../mock_data';
import { mockTimeframeInitialDate, mockEpic, mockGroupId } from '../mock_data';
const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate);
......@@ -24,8 +18,6 @@ const createComponent = ({
epic = mockEpic,
timeframe = mockTimeframeMonths,
currentGroupId = mockGroupId,
shellWidth = mockShellWidth,
itemWidth = mockItemWidth,
}) => {
const Component = Vue.extend(epicItemComponent);
......@@ -34,8 +26,6 @@ const createComponent = ({
epic,
timeframe,
currentGroupId,
shellWidth,
itemWidth,
});
};
......
import Vue from 'vue';
import epicItemTimelineComponent from 'ee/roadmap/components/epic_item_timeline.vue';
import eventHub from 'ee/roadmap/event_hub';
import { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils';
import { TIMELINE_CELL_MIN_WIDTH, PRESET_TYPES } from 'ee/roadmap/constants';
import { PRESET_TYPES } from 'ee/roadmap/constants';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mockTimeframeInitialDate, mockEpic, mockShellWidth, mockItemWidth } from '../mock_data';
import { mockTimeframeInitialDate, mockEpic } from '../mock_data';
const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate);
......@@ -16,8 +15,6 @@ const createComponent = ({
timeframe = mockTimeframeMonths,
timeframeItem = mockTimeframeMonths[0],
epic = mockEpic,
shellWidth = mockShellWidth,
itemWidth = mockItemWidth,
}) => {
const Component = Vue.extend(epicItemTimelineComponent);
......@@ -26,8 +23,6 @@ const createComponent = ({
timeframe,
timeframeItem,
epic,
shellWidth,
itemWidth,
});
};
......@@ -42,80 +37,25 @@ describe('EpicItemTimelineComponent', () => {
it('returns default data props', () => {
vm = createComponent({});
expect(vm.timelineBarReady).toBe(false);
expect(vm.timelineBarStyles).toBe('');
});
});
describe('computed', () => {
describe('itemStyles', () => {
it('returns CSS min-width based on getCellWidth() method', () => {
vm = createComponent({});
expect(vm.itemStyles.width).toBe(`${mockItemWidth}px`);
});
});
});
describe('methods', () => {
describe('getCellWidth', () => {
it('returns proportionate width based on timeframe length and shellWidth', () => {
vm = createComponent({});
expect(vm.getCellWidth()).toBe(210);
});
it('returns minimum fixed width when proportionate width available lower than minimum fixed width defined', () => {
vm = createComponent({
shellWidth: 1000,
});
expect(vm.getCellWidth()).toBe(TIMELINE_CELL_MIN_WIDTH);
});
});
describe('renderTimelineBar', () => {
it('sets `timelineBarStyles` & `timelineBarReady` when timeframeItem has Epic.startDate', () => {
vm = createComponent({
epic: Object.assign({}, mockEpic, { startDate: mockTimeframeMonths[1] }),
timeframeItem: mockTimeframeMonths[1],
});
vm.renderTimelineBar();
expect(vm.timelineBarStyles).toBe('width: 1274px; left: 0;');
expect(vm.timelineBarReady).toBe(true);
});
it('does not set `timelineBarStyles` & `timelineBarReady` when timeframeItem does NOT have Epic.startDate', () => {
vm = createComponent({
epic: Object.assign({}, mockEpic, { startDate: mockTimeframeMonths[0] }),
timeframeItem: mockTimeframeMonths[1],
});
vm.renderTimelineBar();
expect(vm.timelineBarStyles).toBe('');
expect(vm.timelineBarReady).toBe(false);
});
});
});
describe('mounted', () => {
it('binds `refreshTimeline` event listener on eventHub', () => {
spyOn(eventHub, '$on');
const vmX = createComponent({});
expect(eventHub.$on).toHaveBeenCalledWith('refreshTimeline', jasmine.any(Function));
vmX.$destroy();
});
});
describe('beforeDestroy', () => {
it('unbinds `refreshTimeline` event listener on eventHub', () => {
spyOn(eventHub, '$off');
const vmX = createComponent({});
vmX.$destroy();
expect(eventHub.$off).toHaveBeenCalledWith('refreshTimeline', jasmine.any(Function));
expect(vm.epicStartDateValues).toEqual(
jasmine.objectContaining({
day: mockEpic.startDate.getDay(),
date: mockEpic.startDate.getDate(),
month: mockEpic.startDate.getMonth(),
year: mockEpic.startDate.getFullYear(),
time: mockEpic.startDate.getTime(),
}),
);
expect(vm.epicEndDateValues).toEqual(
jasmine.objectContaining({
day: mockEpic.endDate.getDay(),
date: mockEpic.endDate.getDate(),
month: mockEpic.endDate.getMonth(),
year: mockEpic.endDate.getFullYear(),
time: mockEpic.endDate.getTime(),
}),
);
});
});
......@@ -126,12 +66,6 @@ describe('EpicItemTimelineComponent', () => {
expect(vm.$el.classList.contains('epic-timeline-cell')).toBe(true);
});
it('renders component container element with `min-width` property applied via style attribute', () => {
vm = createComponent({});
expect(vm.$el.getAttribute('style')).toBe(`width: ${mockItemWidth}px;`);
});
it('renders timeline bar element with class `timeline-bar` and class `timeline-bar-wrapper` as container element', () => {
vm = createComponent({
epic: Object.assign({}, mockEpic, { startDate: mockTimeframeMonths[1] }),
......@@ -141,7 +75,7 @@ describe('EpicItemTimelineComponent', () => {
expect(vm.$el.querySelector('.timeline-bar-wrapper .timeline-bar')).not.toBeNull();
});
it('renders timeline bar with calculated `width` and `left` properties applied via style attribute', done => {
it('renders timeline bar with calculated `width` and `left` properties applied via style attribute', () => {
vm = createComponent({
epic: Object.assign({}, mockEpic, {
startDate: mockTimeframeMonths[0],
......@@ -150,11 +84,8 @@ describe('EpicItemTimelineComponent', () => {
});
const timelineBarEl = vm.$el.querySelector('.timeline-bar-wrapper .timeline-bar');
vm.renderTimelineBar();
vm.$nextTick(() => {
expect(timelineBarEl.getAttribute('style')).toBe('width: 742.5px; left: 0px;');
done();
});
expect(timelineBarEl.getAttribute('style')).toContain('width');
expect(timelineBarEl.getAttribute('style')).toContain('left: 0px;');
});
it('renders timeline bar with `start-date-undefined` class when Epic startDate is undefined', done => {
......@@ -166,7 +97,6 @@ describe('EpicItemTimelineComponent', () => {
});
const timelineBarEl = vm.$el.querySelector('.timeline-bar-wrapper .timeline-bar');
vm.renderTimelineBar();
vm.$nextTick(() => {
expect(timelineBarEl.classList.contains('start-date-undefined')).toBe(true);
done();
......@@ -183,7 +113,6 @@ describe('EpicItemTimelineComponent', () => {
});
const timelineBarEl = vm.$el.querySelector('.timeline-bar-wrapper .timeline-bar');
vm.renderTimelineBar();
vm.$nextTick(() => {
expect(timelineBarEl.classList.contains('end-date-undefined')).toBe(true);
done();
......
......@@ -4,7 +4,7 @@ import MonthsHeaderItemComponent from 'ee/roadmap/components/preset_months/month
import { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mockTimeframeInitialDate, mockShellWidth, mockItemWidth } from 'ee_spec/roadmap/mock_data';
import { mockTimeframeInitialDate } from 'ee_spec/roadmap/mock_data';
const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate);
const mockTimeframeIndex = 0;
......@@ -13,8 +13,6 @@ const createComponent = ({
timeframeIndex = mockTimeframeIndex,
timeframeItem = mockTimeframeMonths[mockTimeframeIndex],
timeframe = mockTimeframeMonths,
shellWidth = mockShellWidth,
itemWidth = mockItemWidth,
}) => {
const Component = Vue.extend(MonthsHeaderItemComponent);
......@@ -22,8 +20,6 @@ const createComponent = ({
timeframeIndex,
timeframeItem,
timeframe,
shellWidth,
itemWidth,
});
};
......@@ -46,14 +42,6 @@ describe('MonthsHeaderItemComponent', () => {
});
describe('computed', () => {
describe('itemStyles', () => {
it('returns style object for container element based on value of `itemWidth` prop', () => {
vm = createComponent({});
expect(vm.itemStyles.width).toBe('180px');
});
});
describe('timelineHeaderLabel', () => {
it('returns string containing Year and Month for current timeline header item', () => {
vm = createComponent({});
......
......@@ -4,7 +4,7 @@ import QuartersHeaderItemComponent from 'ee/roadmap/components/preset_quarters/q
import { getTimeframeForQuartersView } from 'ee/roadmap/utils/roadmap_utils';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mockTimeframeInitialDate, mockShellWidth, mockItemWidth } from 'ee_spec/roadmap/mock_data';
import { mockTimeframeInitialDate } from 'ee_spec/roadmap/mock_data';
const mockTimeframeIndex = 0;
const mockTimeframeQuarters = getTimeframeForQuartersView(mockTimeframeInitialDate);
......@@ -13,8 +13,6 @@ const createComponent = ({
timeframeIndex = mockTimeframeIndex,
timeframeItem = mockTimeframeQuarters[mockTimeframeIndex],
timeframe = mockTimeframeQuarters,
shellWidth = mockShellWidth,
itemWidth = mockItemWidth,
}) => {
const Component = Vue.extend(QuartersHeaderItemComponent);
......@@ -22,8 +20,6 @@ const createComponent = ({
timeframeIndex,
timeframeItem,
timeframe,
shellWidth,
itemWidth,
});
};
......@@ -44,14 +40,6 @@ describe('QuartersHeaderItemComponent', () => {
});
describe('computed', () => {
describe('itemStyles', () => {
it('returns style object for container element based on value of `itemWidth` prop', () => {
vm = createComponent({});
expect(vm.itemStyles.width).toBe('180px');
});
});
describe('quarterBeginDate', () => {
it('returns date object representing quarter begin date for current `timeframeItem`', () => {
expect(vm.quarterBeginDate).toBe(mockTimeframeQuarters[mockTimeframeIndex].range[0]);
......
......@@ -4,7 +4,7 @@ import WeeksHeaderItemComponent from 'ee/roadmap/components/preset_weeks/weeks_h
import { getTimeframeForWeeksView } from 'ee/roadmap/utils/roadmap_utils';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mockTimeframeInitialDate, mockShellWidth, mockItemWidth } from 'ee_spec/roadmap/mock_data';
import { mockTimeframeInitialDate } from 'ee_spec/roadmap/mock_data';
const mockTimeframeIndex = 0;
const mockTimeframeWeeks = getTimeframeForWeeksView(mockTimeframeInitialDate);
......@@ -13,8 +13,6 @@ const createComponent = ({
timeframeIndex = mockTimeframeIndex,
timeframeItem = mockTimeframeWeeks[mockTimeframeIndex],
timeframe = mockTimeframeWeeks,
shellWidth = mockShellWidth,
itemWidth = mockItemWidth,
}) => {
const Component = Vue.extend(WeeksHeaderItemComponent);
......@@ -22,8 +20,6 @@ const createComponent = ({
timeframeIndex,
timeframeItem,
timeframe,
shellWidth,
itemWidth,
});
};
......@@ -44,14 +40,6 @@ describe('WeeksHeaderItemComponent', () => {
});
describe('computed', () => {
describe('itemStyles', () => {
it('returns style object for container element based on value of `itemWidth` prop', () => {
vm = createComponent({});
expect(vm.itemStyles.width).toBe('180px');
});
});
describe('lastDayOfCurrentWeek', () => {
it('returns date object representing last day of the week as set in `timeframeItem`', () => {
expect(vm.lastDayOfCurrentWeek.getDate()).toBe(
......
......@@ -59,10 +59,6 @@ describe('Roadmap AppComponent', () => {
});
describe('data', () => {
it('returns default data props', () => {
expect(vm.handleResizeThrottled).toBeDefined();
});
describe('when `gon.feature.roadmapGraphql` is true', () => {
const originalGonFeatures = Object.assign({}, gon.features);
......@@ -252,35 +248,6 @@ describe('Roadmap AppComponent', () => {
});
});
describe('mounted', () => {
it('binds window resize event listener', () => {
spyOn(window, 'addEventListener');
const vmX = createComponent();
expect(vmX.handleResizeThrottled).toBeDefined();
expect(window.addEventListener).toHaveBeenCalledWith(
'resize',
vmX.handleResizeThrottled,
false,
);
vmX.$destroy();
});
});
describe('beforeDestroy', () => {
it('unbinds window resize event listener', () => {
spyOn(window, 'removeEventListener');
const vmX = createComponent();
vmX.$destroy();
expect(window.removeEventListener).toHaveBeenCalledWith(
'resize',
vmX.handleResizeThrottled,
false,
);
});
});
describe('template', () => {
it('renders roadmap container with class `roadmap-container`', () => {
expect(vm.$el.classList.contains('roadmap-container')).toBe(true);
......
......@@ -43,8 +43,9 @@ const createComponent = (
describe('RoadmapShellComponent', () => {
let vm;
beforeEach(() => {
beforeEach(done => {
vm = createComponent({});
vm.$nextTick(done);
});
afterEach(() => {
......@@ -53,39 +54,10 @@ describe('RoadmapShellComponent', () => {
describe('data', () => {
it('returns default data props', () => {
expect(vm.shellWidth).toBe(0);
expect(vm.shellHeight).toBe(0);
expect(vm.noScroll).toBe(false);
expect(vm.timeframeStartOffset).toBe(0);
});
});
describe('computed', () => {
describe('containerStyles', () => {
beforeEach(() => {
document.body.innerHTML +=
'<div class="roadmap-container"><div id="roadmap-shell"></div></div>';
});
afterEach(() => {
document.querySelector('.roadmap-container').remove();
});
it('returns style object based on shellWidth and shellHeight', done => {
const vmWithParentEl = createComponent({}, document.getElementById('roadmap-shell'));
Vue.nextTick(() => {
const stylesObj = vmWithParentEl.containerStyles;
// Ensure that value for `width` & `height`
// is a non-zero number.
expect(parseInt(stylesObj.width, 10)).not.toBe(0);
expect(parseInt(stylesObj.height, 10)).not.toBe(0);
vmWithParentEl.$destroy();
done();
});
});
});
});
describe('methods', () => {
beforeEach(() => {
document.body.innerHTML +=
......@@ -103,7 +75,6 @@ describe('RoadmapShellComponent', () => {
Vue.nextTick()
.then(() => {
vmWithParentEl.noScroll = false;
vmWithParentEl.handleScroll();
expect(eventHub.$emit).toHaveBeenCalledWith('epicsListScrolled', jasmine.any(Object));
......@@ -131,13 +102,5 @@ describe('RoadmapShellComponent', () => {
.then(done)
.catch(done.fail);
});
it('adds `prevent-vertical-scroll` class on component container element', done => {
vm.noScroll = true;
Vue.nextTick(() => {
expect(vm.$el.classList.contains('prevent-vertical-scroll')).toBe(true);
done();
});
});
});
});
......@@ -7,7 +7,7 @@ import { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils';
import { PRESET_TYPES } from 'ee/roadmap/constants';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mockEpic, mockTimeframeInitialDate, mockShellWidth } from '../mock_data';
import { mockEpic, mockTimeframeInitialDate } from '../mock_data';
const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate);
......@@ -15,17 +15,13 @@ const createComponent = ({
presetType = PRESET_TYPES.MONTHS,
epics = [mockEpic],
timeframe = mockTimeframeMonths,
shellWidth = mockShellWidth,
listScrollable = false,
}) => {
} = {}) => {
const Component = Vue.extend(roadmapTimelineSectionComponent);
return mountComponent(Component, {
presetType,
epics,
timeframe,
shellWidth,
listScrollable,
});
};
......@@ -33,7 +29,7 @@ describe('RoadmapTimelineSectionComponent', () => {
let vm;
beforeEach(() => {
vm = createComponent({});
vm = createComponent();
});
afterEach(() => {
......@@ -46,6 +42,18 @@ describe('RoadmapTimelineSectionComponent', () => {
});
});
describe('computed', () => {
describe('sectionContainerStyles', () => {
it('returns object containing `width` with value based on epic details cell width, timeline cell width and timeframe length', () => {
expect(vm.sectionContainerStyles).toEqual(
jasmine.objectContaining({
width: '1760px',
}),
);
});
});
});
describe('methods', () => {
describe('handleEpicsListScroll', () => {
it('sets `scrolled-ahead` class on thead element based on provided scrollTop value', () => {
......
......@@ -42,62 +42,47 @@ describe('TimelineTodayIndicatorComponent', () => {
it('returns default data props', () => {
vm = createComponent({});
expect(vm.todayBarStyles).toBe('');
expect(vm.todayBarReady).toBe(false);
expect(vm.todayBarStyles).toEqual({});
expect(vm.todayBarReady).toBe(true);
});
});
describe('methods', () => {
describe('handleEpicsListRender', () => {
describe('getTodayBarStyles', () => {
it('sets `todayBarStyles` and `todayBarReady` props', () => {
vm = createComponent({});
vm.handleEpicsListRender({});
const stylesObj = vm.todayBarStyles;
expect(stylesObj.height).toBe('600px');
expect(stylesObj.left).toBe('50%');
expect(vm.todayBarReady).toBe(true);
});
it('sets `todayBarReady` prop based on value of provided `todayBarReady` param', () => {
vm = createComponent({});
vm.handleEpicsListRender({
todayBarReady: false,
});
const stylesObj = vm.getTodayBarStyles();
expect(vm.todayBarReady).toBe(false);
expect(stylesObj.height).toBe('calc(100vh - 16px)');
expect(stylesObj.left).toBe('50%');
});
});
});
describe('mounted', () => {
it('binds `epicsListRendered`, `epicsListScrolled` and `refreshTimeline` event listeners via eventHub', () => {
it('binds `epicsListScrolled` event listener via eventHub', () => {
spyOn(eventHub, '$on');
const vmX = createComponent({});
expect(eventHub.$on).toHaveBeenCalledWith('epicsListRendered', jasmine.any(Function));
expect(eventHub.$on).toHaveBeenCalledWith('epicsListScrolled', jasmine.any(Function));
expect(eventHub.$on).toHaveBeenCalledWith('refreshTimeline', jasmine.any(Function));
vmX.$destroy();
});
});
describe('beforeDestroy', () => {
it('unbinds `epicsListRendered`, `epicsListScrolled` and `refreshTimeline` event listeners via eventHub', () => {
it('unbinds `epicsListScrolled` event listener via eventHub', () => {
spyOn(eventHub, '$off');
const vmX = createComponent({});
vmX.$destroy();
expect(eventHub.$off).toHaveBeenCalledWith('epicsListRendered', jasmine.any(Function));
expect(eventHub.$off).toHaveBeenCalledWith('epicsListScrolled', jasmine.any(Function));
expect(eventHub.$off).toHaveBeenCalledWith('refreshTimeline', jasmine.any(Function));
});
});
describe('template', () => {
it('renders component container element with class `today-bar`', done => {
vm = createComponent({});
vm.handleEpicsListRender({});
vm.$nextTick(() => {
expect(vm.$el.classList.contains('today-bar')).toBe(true);
done();
......
......@@ -6,7 +6,7 @@ import { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils';
import { PRESET_TYPES } from 'ee/roadmap/constants';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mockTimeframeInitialDate, mockEpic, mockShellWidth, mockItemWidth } from '../mock_data';
import { mockTimeframeInitialDate, mockEpic } from '../mock_data';
const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate);
......@@ -15,8 +15,6 @@ const createComponent = ({
timeframe = mockTimeframeMonths,
timeframeItem = mockTimeframeMonths[0],
epic = mockEpic,
shellWidth = mockShellWidth,
itemWidth = mockItemWidth,
}) => {
const Component = Vue.extend(EpicItemTimelineComponent);
......@@ -25,8 +23,6 @@ const createComponent = ({
timeframe,
timeframeItem,
epic,
shellWidth,
itemWidth,
});
};
......@@ -59,22 +55,34 @@ describe('MonthsPresetMixin', () => {
});
describe('isTimeframeUnderEndDateForMonth', () => {
const timeframeItem = new Date(2018, 0, 10); // Jan 10, 2018
beforeEach(() => {
vm = createComponent({});
});
it('returns true if provided timeframeItem is under epicEndDate', () => {
const timeframeItem = new Date(2018, 0, 10); // Jan 10, 2018
const epicEndDate = new Date(2018, 0, 26); // Jan 26, 2018
expect(vm.isTimeframeUnderEndDateForMonth(timeframeItem, epicEndDate)).toBe(true);
vm = createComponent({
epic: Object.assign({}, mockEpic, {
endDate: epicEndDate,
}),
});
expect(vm.isTimeframeUnderEndDateForMonth(timeframeItem)).toBe(true);
});
it('returns false if provided timeframeItem is NOT under epicEndDate', () => {
const timeframeItem = new Date(2018, 0, 10); // Jan 10, 2018
const epicEndDate = new Date(2018, 1, 26); // Feb 26, 2018
expect(vm.isTimeframeUnderEndDateForMonth(timeframeItem, epicEndDate)).toBe(false);
vm = createComponent({
epic: Object.assign({}, mockEpic, {
endDate: epicEndDate,
}),
});
expect(vm.isTimeframeUnderEndDateForMonth(timeframeItem)).toBe(false);
});
});
......@@ -132,7 +140,6 @@ describe('MonthsPresetMixin', () => {
describe('getTimelineBarWidthForMonths', () => {
it('returns calculated width value based on Epic.startDate and Epic.endDate', () => {
vm = createComponent({
shellWidth: 2000,
timeframeItem: mockTimeframeMonths[0],
epic: Object.assign({}, mockEpic, {
startDate: new Date(2017, 11, 15), // Dec 15, 2017
......@@ -140,7 +147,7 @@ describe('MonthsPresetMixin', () => {
}),
});
expect(Math.floor(vm.getTimelineBarWidthForMonths())).toBe(637);
expect(Math.floor(vm.getTimelineBarWidthForMonths())).toBe(546);
});
});
});
......
......@@ -6,7 +6,7 @@ import { getTimeframeForQuartersView } from 'ee/roadmap/utils/roadmap_utils';
import { PRESET_TYPES } from 'ee/roadmap/constants';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mockTimeframeInitialDate, mockEpic, mockShellWidth, mockItemWidth } from '../mock_data';
import { mockTimeframeInitialDate, mockEpic } from '../mock_data';
const mockTimeframeQuarters = getTimeframeForQuartersView(mockTimeframeInitialDate);
......@@ -15,8 +15,6 @@ const createComponent = ({
timeframe = mockTimeframeQuarters,
timeframeItem = mockTimeframeQuarters[0],
epic = mockEpic,
shellWidth = mockShellWidth,
itemWidth = mockItemWidth,
}) => {
const Component = Vue.extend(EpicItemTimelineComponent);
......@@ -25,8 +23,6 @@ const createComponent = ({
timeframe,
timeframeItem,
epic,
shellWidth,
itemWidth,
});
};
......@@ -59,22 +55,34 @@ describe('QuartersPresetMixin', () => {
});
describe('isTimeframeUnderEndDateForQuarter', () => {
const timeframeItem = mockTimeframeQuarters[1];
beforeEach(() => {
vm = createComponent({});
});
it('returns true if provided timeframeItem is under epicEndDate', () => {
const timeframeItem = mockTimeframeQuarters[1];
const epicEndDate = mockTimeframeQuarters[1].range[2];
expect(vm.isTimeframeUnderEndDateForQuarter(timeframeItem, epicEndDate)).toBe(true);
vm = createComponent({
epic: Object.assign({}, mockEpic, {
endDate: epicEndDate,
}),
});
expect(vm.isTimeframeUnderEndDateForQuarter(timeframeItem)).toBe(true);
});
it('returns false if provided timeframeItem is NOT under epicEndDate', () => {
const timeframeItem = mockTimeframeQuarters[1];
const epicEndDate = mockTimeframeQuarters[2].range[1];
expect(vm.isTimeframeUnderEndDateForQuarter(timeframeItem, epicEndDate)).toBe(false);
vm = createComponent({
epic: Object.assign({}, mockEpic, {
endDate: epicEndDate,
}),
});
expect(vm.isTimeframeUnderEndDateForQuarter(timeframeItem)).toBe(false);
});
});
......@@ -132,7 +140,6 @@ describe('QuartersPresetMixin', () => {
describe('getTimelineBarWidthForQuarters', () => {
it('returns calculated width value based on Epic.startDate and Epic.endDate', () => {
vm = createComponent({
shellWidth: 2000,
timeframeItem: mockTimeframeQuarters[0],
epic: Object.assign({}, mockEpic, {
startDate: mockTimeframeQuarters[0].range[1],
......@@ -140,7 +147,7 @@ describe('QuartersPresetMixin', () => {
}),
});
expect(Math.floor(vm.getTimelineBarWidthForQuarters())).toBe(240);
expect(Math.floor(vm.getTimelineBarWidthForQuarters())).toBe(180);
});
});
});
......
import Vue from 'vue';
import roadmapTimelineSectionComponent from 'ee/roadmap/components/roadmap_timeline_section.vue';
import { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils';
import { PRESET_TYPES } from 'ee/roadmap/constants';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import {
mockEpic,
mockTimeframeInitialDate,
mockShellWidth,
mockScrollBarSize,
} from '../mock_data';
const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate);
const createComponent = ({
presetType = PRESET_TYPES.MONTHS,
epics = [mockEpic],
timeframe = mockTimeframeMonths,
shellWidth = mockShellWidth,
listScrollable = false,
}) => {
const Component = Vue.extend(roadmapTimelineSectionComponent);
return mountComponent(Component, {
presetType,
epics,
timeframe,
shellWidth,
listScrollable,
});
};
describe('SectionMixin', () => {
let vm;
beforeEach(() => {
vm = createComponent({});
});
afterEach(() => {
vm.$destroy();
});
describe('computed', () => {
describe('sectionShellWidth', () => {
it('returns shellWidth as it is when `listScrollable` prop is false', () => {
expect(vm.sectionShellWidth).toBe(mockShellWidth);
});
it('returns shellWidth after deducating value of SCROLL_BAR_SIZE when `listScrollable` prop is true', () => {
const vmScrollable = createComponent({ listScrollable: true });
expect(vmScrollable.sectionShellWidth).toBe(mockShellWidth - mockScrollBarSize);
vmScrollable.$destroy();
});
});
describe('sectionItemWidth', () => {
it('returns calculated item width based on sectionShellWidth and timeframe size', () => {
expect(vm.sectionItemWidth).toBe(210);
});
});
describe('sectionContainerStyles', () => {
it('returns style string for container element based on sectionShellWidth', () => {
expect(vm.sectionContainerStyles.width).toBe(`${mockShellWidth}px`);
});
});
});
});
......@@ -6,7 +6,7 @@ import { getTimeframeForWeeksView } from 'ee/roadmap/utils/roadmap_utils';
import { PRESET_TYPES } from 'ee/roadmap/constants';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mockTimeframeInitialDate, mockEpic, mockShellWidth, mockItemWidth } from '../mock_data';
import { mockTimeframeInitialDate, mockEpic } from '../mock_data';
const mockTimeframeWeeks = getTimeframeForWeeksView(mockTimeframeInitialDate);
......@@ -15,8 +15,6 @@ const createComponent = ({
timeframe = mockTimeframeWeeks,
timeframeItem = mockTimeframeWeeks[0],
epic = mockEpic,
shellWidth = mockShellWidth,
itemWidth = mockItemWidth,
}) => {
const Component = Vue.extend(EpicItemTimelineComponent);
......@@ -25,8 +23,6 @@ const createComponent = ({
timeframe,
timeframeItem,
epic,
shellWidth,
itemWidth,
});
};
......@@ -70,22 +66,34 @@ describe('WeeksPresetMixin', () => {
});
describe('isTimeframeUnderEndDateForWeek', () => {
const timeframeItem = new Date(2018, 0, 7); // Jan 7, 2018
beforeEach(() => {
vm = createComponent({});
});
it('returns true if provided timeframeItem is under epicEndDate', () => {
const timeframeItem = new Date(2018, 0, 7); // Jan 7, 2018
const epicEndDate = new Date(2018, 0, 3); // Jan 3, 2018
expect(vm.isTimeframeUnderEndDateForWeek(timeframeItem, epicEndDate)).toBe(true);
vm = createComponent({
epic: Object.assign({}, mockEpic, {
endDate: epicEndDate,
}),
});
expect(vm.isTimeframeUnderEndDateForWeek(timeframeItem)).toBe(true);
});
it('returns false if provided timeframeItem is NOT under epicEndDate', () => {
const timeframeItem = new Date(2018, 0, 7); // Jan 7, 2018
const epicEndDate = new Date(2018, 0, 15); // Jan 15, 2018
expect(vm.isTimeframeUnderEndDateForWeek(timeframeItem, epicEndDate)).toBe(false);
vm = createComponent({
epic: Object.assign({}, mockEpic, {
endDate: epicEndDate,
}),
});
expect(vm.isTimeframeUnderEndDateForWeek(timeframeItem)).toBe(false);
});
});
......@@ -136,14 +144,13 @@ describe('WeeksPresetMixin', () => {
}),
});
expect(vm.getTimelineBarStartOffsetForWeeks()).toContain('left: 51');
expect(vm.getTimelineBarStartOffsetForWeeks()).toContain('left: 38');
});
});
describe('getTimelineBarWidthForWeeks', () => {
it('returns calculated width value based on Epic.startDate and Epic.endDate', () => {
vm = createComponent({
shellWidth: 2000,
timeframeItem: mockTimeframeWeeks[0],
epic: Object.assign({}, mockEpic, {
startDate: new Date(2018, 0, 1), // Jan 1, 2018
......@@ -151,7 +158,7 @@ describe('WeeksPresetMixin', () => {
}),
});
expect(Math.floor(vm.getTimelineBarWidthForWeeks())).toBe(1611);
expect(Math.floor(vm.getTimelineBarWidthForWeeks())).toBe(1208);
});
});
});
......
......@@ -50,7 +50,7 @@ describe('Roadmap Vuex Actions', () => {
});
describe('setInitialData', () => {
it('Should set initial roadmap props', done => {
it('should set initial roadmap props', done => {
const mockRoadmap = {
foo: 'bar',
bar: 'baz',
......@@ -68,7 +68,7 @@ describe('Roadmap Vuex Actions', () => {
});
describe('setWindowResizeInProgress', () => {
it('Should set value of `state.windowResizeInProgress` based on provided value', done => {
it('should set value of `state.windowResizeInProgress` based on provided value', done => {
testAction(
actions.setWindowResizeInProgress,
true,
......@@ -148,13 +148,13 @@ describe('Roadmap Vuex Actions', () => {
});
describe('requestEpics', () => {
it('Should set `epicsFetchInProgress` to true', done => {
it('should set `epicsFetchInProgress` to true', done => {
testAction(actions.requestEpics, {}, state, [{ type: 'REQUEST_EPICS' }], [], done);
});
});
describe('requestEpicsForTimeframe', () => {
it('Should set `epicsFetchForTimeframeInProgress` to true', done => {
it('should set `epicsFetchForTimeframeInProgress` to true', done => {
testAction(
actions.requestEpicsForTimeframe,
{},
......@@ -167,7 +167,7 @@ describe('Roadmap Vuex Actions', () => {
});
describe('receiveEpicsSuccess', () => {
it('Should set formatted epics array and epicId to IDs array in state based on provided epics list', done => {
it('should set formatted epics array and epicId to IDs array in state based on provided epics list', done => {
testAction(
actions.receiveEpicsSuccess,
{
......@@ -200,7 +200,7 @@ describe('Roadmap Vuex Actions', () => {
);
});
it('Should set formatted epics array and epicId to IDs array in state based on provided epics list when timeframe was extended', done => {
it('should set formatted epics array and epicId to IDs array in state based on provided epics list when timeframe was extended', done => {
testAction(
actions.receiveEpicsSuccess,
{ rawEpics: [mockRawEpic], newEpic: true, timeframeExtended: true },
......@@ -223,7 +223,7 @@ describe('Roadmap Vuex Actions', () => {
setFixtures('<div class="flash-container"></div>');
});
it('Should set epicsFetchInProgress, epicsFetchForTimeframeInProgress to false and epicsFetchFailure to true', done => {
it('should set epicsFetchInProgress, epicsFetchForTimeframeInProgress to false and epicsFetchFailure to true', done => {
testAction(
actions.receiveEpicsFailure,
{},
......@@ -234,7 +234,7 @@ describe('Roadmap Vuex Actions', () => {
);
});
it('Should show flash error', () => {
it('should show flash error', () => {
actions.receiveEpicsFailure({ commit: () => {} });
expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
......@@ -255,7 +255,7 @@ describe('Roadmap Vuex Actions', () => {
});
describe('success', () => {
it('Should dispatch requestEpics and receiveEpicsSuccess when request is successful', done => {
it('should dispatch requestEpics and receiveEpicsSuccess when request is successful', done => {
mock.onGet(epicsPath).replyOnce(200, rawEpics);
testAction(
......@@ -278,7 +278,7 @@ describe('Roadmap Vuex Actions', () => {
});
describe('failure', () => {
it('Should dispatch requestEpics and receiveEpicsFailure when request fails', done => {
it('should dispatch requestEpics and receiveEpicsFailure when request fails', done => {
mock.onGet(epicsPath).replyOnce(500, {});
testAction(
......@@ -314,7 +314,7 @@ describe('Roadmap Vuex Actions', () => {
});
describe('success', () => {
it('Should dispatch requestEpicsForTimeframe and receiveEpicsSuccess when request is successful', done => {
it('should dispatch requestEpicsForTimeframe and receiveEpicsSuccess when request is successful', done => {
mock.onGet(mockEpicsPath).replyOnce(200, rawEpics);
testAction(
......@@ -337,7 +337,7 @@ describe('Roadmap Vuex Actions', () => {
});
describe('failure', () => {
it('Should dispatch requestEpicsForTimeframe and requestEpicsFailure when request fails', done => {
it('should dispatch requestEpicsForTimeframe and requestEpicsFailure when request fails', done => {
mock.onGet(mockEpicsPath).replyOnce(500, {});
testAction(
......@@ -360,7 +360,7 @@ describe('Roadmap Vuex Actions', () => {
});
describe('extendTimeframe', () => {
it('Should prepend to timeframe when called with extend type prepend', done => {
it('should prepend to timeframe when called with extend type prepend', done => {
testAction(
actions.extendTimeframe,
{ extendAs: EXTEND_AS.PREPEND },
......@@ -371,7 +371,7 @@ describe('Roadmap Vuex Actions', () => {
);
});
it('Should append to timeframe when called with extend type append', done => {
it('should append to timeframe when called with extend type append', done => {
testAction(
actions.extendTimeframe,
{ extendAs: EXTEND_AS.APPEND },
......@@ -384,7 +384,7 @@ describe('Roadmap Vuex Actions', () => {
});
describe('refreshEpicDates', () => {
it('Should update epics after refreshing epic dates to match with updated timeframe', done => {
it('should update epics after refreshing epic dates to match with updated timeframe', done => {
const epics = rawEpics.map(epic =>
epicUtils.formatEpicDetails(epic, state.timeframeStartDate, state.timeframeEndDate),
);
......@@ -399,4 +399,17 @@ describe('Roadmap Vuex Actions', () => {
);
});
});
describe('setBufferSize', () => {
it('should set bufferSize in store state', done => {
testAction(
actions.setBufferSize,
10,
state,
[{ type: types.SET_BUFFER_SIZE, payload: 10 }],
[],
done,
);
});
});
});
......@@ -137,4 +137,14 @@ describe('Roadmap Store Mutations', () => {
expect(state.timeframe[1]).toBe(extendedTimeframe[0]);
});
});
describe('SET_BUFFER_SIZE', () => {
it('Should set `bufferSize` in state', () => {
const bufferSize = 10;
mutations[types.SET_BUFFER_SIZE](state, bufferSize);
expect(state.bufferSize).toBe(bufferSize);
});
});
});
......@@ -11690,10 +11690,10 @@ vue-template-es2015-compiler@^1.9.0:
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==
vue-virtual-scroll-list@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/vue-virtual-scroll-list/-/vue-virtual-scroll-list-1.3.1.tgz#efcb83d3a3dcc69cd886fa4de1130a65493e8f76"
integrity sha512-PMTxiK9/P1LtgoWWw4n1QnmDDkYqIdWWCNdt1L4JD9g6rwDgnsGsSV10bAnd5n7DQLHGWHjRex+zAbjXWT8t0g==
vue-virtual-scroll-list@^1.4.4:
version "1.4.4"
resolved "https://registry.yarnpkg.com/vue-virtual-scroll-list/-/vue-virtual-scroll-list-1.4.4.tgz#5fca7a13f785899bbfb70471ec4fe222437d8495"
integrity sha512-wU7FDpd9Xy4f62pf8SBg/ak21jMI/pdx4s4JPah+z/zuhmeAafQgp8BjtZvvt+b0BZOsOS1FJuCfUH7azTkivQ==
vue@^2.6.10:
version "2.6.10"
......
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