Commit 847e6a65 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '262860-add-rotation-to-schedule' into 'master'

Add grid for rotations display + current day indicator

See merge request gitlab-org/gitlab!49248
parents 3379e08a f908ee29
......@@ -30,8 +30,9 @@
}
//// Copied from roadmaps.scss - adapted for on-call schedules
$header-item-height: 60px;
$details-cell-width: px-to-rem(150px);
$header-item-height: 72px;
$item-height: 40px;
$details-cell-width: 150px;
$timeline-cell-height: 32px;
$timeline-cell-width: 180px;
$border-style: 1px solid var(--gray-100, $gray-100);
......@@ -40,7 +41,6 @@ $gradient-gray: rgba(255, 255, 255, 0.001);
$scroll-top-gradient: linear-gradient(to bottom, $gradient-dark-gray 0%, $gradient-gray 100%);
$scroll-bottom-gradient: linear-gradient(to bottom, $gradient-gray 0%, $gradient-dark-gray 100%);
$column-right-gradient: linear-gradient(to right, $gradient-dark-gray 0%, $gradient-gray 100%);
$epic-details-cell-width: 150px;
.schedule-shell {
@include gl-relative;
......@@ -55,7 +55,6 @@ $epic-details-cell-width: 150px;
.timeline-section {
@include gl-sticky;
position: -webkit-sticky;
@include gl-top-0;
z-index: 20;
......@@ -69,27 +68,15 @@ $epic-details-cell-width: 150px;
.timeline-header-blank {
@include gl-sticky;
position: -webkit-sticky;
@include gl-top-0;
@include gl-left-0;
width: $details-cell-width;
z-index: 2;
&::after {
height: $header-item-height;
@include gl-content-empty;
@include gl-absolute;
@include gl-top-0;
right: -$grid-size;
width: $grid-size;
@include gl-pointer-events-none;
background: $column-right-gradient;
}
}
.timeline-header-item {
// container size minus left panel width divided by 2 week timeframes
width: calc((100% - #{$epic-details-cell-width}) / 2);
width: calc((100% - #{$details-cell-width}) / 2);
&:last-of-type .item-label {
@include gl-border-r-0;
......@@ -110,7 +97,8 @@ $epic-details-cell-width: 150px;
}
.item-label {
padding: $gl-padding-8 $gl-padding;
@include gl-py-4;
@include gl-pl-7;
border-right: $border-style;
border-bottom: $border-style;
}
......@@ -124,19 +112,72 @@ $epic-details-cell-width: 150px;
@include gl-flex-basis-0;
text-align: center;
font-size: $code-font-size;
line-height: 1.5;
@include gl-font-base;
padding: 2px 0;
}
}
.current-day-indicator-header {
@include gl-absolute;
@include gl-bottom-0;
height: $gl-vert-padding;
width: $gl-vert-padding;
height: $grid-size;
width: $grid-size;
background-color: var(--red-500, $red-500);
border-radius: 50%;
transform: translateX(-3px);
@include gl-rounded-full;
transform: translate(-50%, 50%);
}
}
}
.timeline-section .timeline-header-blank,
.list-section .details-cell {
&::after {
@include gl-h-full;
@include gl-content-empty;
@include gl-absolute;
@include gl-top-0;
right: -$grid-size;
width: $grid-size;
@include gl-pointer-events-none;
background: $column-right-gradient;
}
}
.details-cell,
.timeline-cell {
@include float-left;
height: $item-height;
border-bottom: $border-style;
}
.details-cell {
@include gl-sticky;
@include gl-left-0;
width: $details-cell-width;
@include gl-font-base;
background-color: var(--white, $white);
z-index: 10;
}
.timeline-cell {
@include gl-relative;
// width: $timeline-cell-width;
// container size minus left panel width divided by 2 week timeframes
width: calc((100% - #{$details-cell-width}) / 2);
@include gl-bg-transparent;
border-right: $border-style;
&:last-child {
@include gl-border-r-0;
}
.current-day-indicator {
@include gl-absolute;
top: -1px;
width: $gl-spacing-scale-1;
height: calc(100% + 1px);
background-color: var(--red-500, $red-500);
@include gl-pointer-events-none;
transform: translateX(-50%);
}
}
......@@ -25,6 +25,10 @@ export default {
type: Object,
required: true,
},
modalId: {
type: String,
required: true,
},
},
data() {
return {
......@@ -88,7 +92,7 @@ export default {
<template>
<gl-modal
ref="deleteScheduleModal"
modal-id="deleteScheduleModal"
:modal-id="modalId"
size="sm"
:data-testid="`delete-schedule-modal-${schedule.iid}`"
:title="$options.i18n.deleteSchedule"
......
......@@ -26,6 +26,10 @@ export default {
type: Object,
required: true,
},
modalId: {
type: String,
required: true,
},
},
data() {
return {
......@@ -112,7 +116,7 @@ export default {
<template>
<gl-modal
ref="updateScheduleModal"
modal-id="updateScheduleModal"
:modal-id="modalId"
size="sm"
:data-testid="`update-schedule-modal-${schedule.iid}`"
:title="$options.i18n.editSchedule"
......
......@@ -11,18 +11,30 @@ import { s__ } from '~/locale';
import ScheduleTimelineSection from './schedule/components/schedule_timeline_section.vue';
import DeleteScheduleModal from './delete_schedule_modal.vue';
import EditScheduleModal from './edit_schedule_modal.vue';
import AddRotationModal from './rotations/add_rotation_modal.vue';
import { getTimeframeForWeeksView } from './schedule/utils';
import { PRESET_TYPES } from './schedule/constants';
import { getFormattedTimezone } from '../utils/common_utils';
import RotationsListSection from './schedule/components/rotations_list_section.vue';
export const i18n = {
scheduleForTz: s__('OnCallSchedules|On-call schedule for the %{tzShort}'),
editScheduleLabel: s__('OnCallSchedules|Edit schedule'),
deleteScheduleLabel: s__('OnCallSchedules|Delete schedule'),
rotationTitle: s__('OnCallSchedules|Rotations'),
addARotation: s__('OnCallSchedules|Add a rotation'),
};
export const addRotationModalId = 'addRotationModal';
export const editScheduleModalId = 'editScheduleModal';
export const deleteScheduleModalId = 'deleteScheduleModal';
export default {
i18n,
addRotationModalId,
editScheduleModalId,
deleteScheduleModalId,
presetType: PRESET_TYPES.WEEKS,
inject: ['timezones'],
components: {
......@@ -33,6 +45,8 @@ export default {
GlButton,
DeleteScheduleModal,
EditScheduleModal,
AddRotationModal,
RotationsListSection,
},
directives: {
GlModal: GlModalDirective,
......@@ -43,6 +57,11 @@ export default {
type: Object,
required: true,
},
rotations: {
type: Array,
required: false,
default: () => [],
},
},
computed: {
tzLong() {
......@@ -60,18 +79,21 @@ export default {
<div>
<gl-card>
<template #header>
<div class="gl-display-flex gl-justify-content-space-between gl-m-0">
<div
class="gl-display-flex gl-justify-content-space-between gl-m-0"
data-testid="scheduleHeader"
>
<span class="gl-font-weight-bold gl-font-lg">{{ schedule.name }}</span>
<gl-button-group>
<gl-button
v-gl-modal.updateScheduleModal
v-gl-modal="$options.editScheduleModalId"
v-gl-tooltip
:title="$options.i18n.editScheduleLabel"
icon="pencil"
:aria-label="$options.i18n.editScheduleLabel"
/>
<gl-button
v-gl-modal.deleteScheduleModal
v-gl-modal="$options.deleteScheduleModalId"
v-gl-tooltip
:title="$options.i18n.deleteScheduleLabel"
icon="remove"
......@@ -80,19 +102,38 @@ export default {
</gl-button-group>
</div>
</template>
<p class="gl-text-gray-500 gl-mb-5">
<p class="gl-text-gray-500 gl-mb-5" data-testid="scheduleBody">
<gl-sprintf :message="$options.i18n.scheduleForTz">
<template #tzShort>{{ schedule.timezone }}</template>
</gl-sprintf>
| {{ tzLong }}
</p>
<div class="schedule-shell">
<gl-card header-class="gl-bg-transparent">
<template #header>
<div
class="gl-display-flex gl-justify-content-space-between"
data-testid="rotationsHeader"
>
<h6 class="gl-m-0">{{ $options.i18n.rotationTitle }}</h6>
<gl-button v-gl-modal="$options.addRotationModalId" variant="link"
>{{ $options.i18n.addARotation }}
</gl-button>
</div>
</template>
<div class="schedule-shell" data-testid="rotationsBody">
<schedule-timeline-section :preset-type="$options.presetType" :timeframe="timeframe" />
<rotations-list-section
:preset-type="$options.presetType"
:rotations="rotations"
:timeframe="timeframe"
/>
</div>
</gl-card>
<delete-schedule-modal :schedule="schedule" />
<edit-schedule-modal :schedule="schedule" />
</gl-card>
<delete-schedule-modal :schedule="schedule" :modal-id="$options.deleteScheduleModalId" />
<edit-schedule-modal :schedule="schedule" :modal-id="$options.editScheduleModalId" />
<add-rotation-modal :modal-id="$options.addRotationModalId" />
</div>
</template>
......@@ -7,7 +7,7 @@ import { s__ } from '~/locale';
import getOncallSchedulesQuery from '../graphql/queries/get_oncall_schedules.query.graphql';
import { fetchPolicies } from '~/lib/graphql';
const addScheduleModalId = 'addScheduleModal';
export const addScheduleModalId = 'addScheduleModal';
export const i18n = {
title: s__('OnCallSchedules|On-call schedule'),
......
......@@ -60,6 +60,12 @@ export default {
GlAvatarLabeled,
GlAlert,
},
props: {
modalId: {
type: String,
required: true,
},
},
apollo: {
participants: {
query: usersSearchQuery,
......@@ -169,7 +175,7 @@ export default {
<template>
<gl-modal
ref="createScheduleRotationModal"
modal-id="create-schedule-rotation-modal"
:modal-id="modalId"
size="sm"
:title="$options.i18n.addRotation"
:action-primary="actionsProps.primary"
......
<script>
import CommonMixin from '../mixins/common_mixin';
export default {
mixins: [CommonMixin],
props: {
presetType: {
type: String,
required: true,
},
timeframeItem: {
type: [Date, Object],
required: true,
},
},
};
</script>
<template>
<span v-if="hasToday" :style="getIndicatorStyles()" class="current-day-indicator"></span>
</template>
<script>
import { monthInWords } from '~/lib/utils/datetime_utility';
import WeeksHeaderSubItem from './weeks_header_sub_item.vue';
import CommonMixin from '../../mixins/common_mixin';
export default {
components: {
WeeksHeaderSubItem,
},
mixins: [CommonMixin],
props: {
timeframeIndex: {
type: Number,
......@@ -21,14 +22,6 @@ export default {
required: true,
},
},
data() {
const currentDate = new Date();
currentDate.setHours(0, 0, 0, 0);
return {
currentDate,
};
},
computed: {
lastDayOfCurrentWeek() {
const lastDayOfCurrentWeek = new Date(this.timeframeItem.getTime());
......@@ -50,7 +43,7 @@ export default {
return `${monthInWords(this.timeframeItem, true)} ${timeframeItemDate}`;
},
timelineHeaderClass() {
const currentDateTime = this.currentDate.getTime();
const currentDateTime = this.$options.currentDate.getTime();
const lastDayOfCurrentWeekTime = this.lastDayOfCurrentWeek.getTime();
if (
......@@ -68,7 +61,9 @@ export default {
<template>
<span class="timeline-header-item">
<div :class="timelineHeaderClass" class="item-label">{{ timelineHeaderLabel }}</div>
<weeks-header-sub-item :timeframe-item="timeframeItem" :current-date="currentDate" />
<div :class="timelineHeaderClass" class="item-label" data-testid="timeline-header-label">
{{ timelineHeaderLabel }}
</div>
<weeks-header-sub-item :timeframe-item="timeframeItem" />
</span>
</template>
<script>
import { PRESET_TYPES } from '../../constants';
import CommonMixin from '../../mixins/common_mixin';
export default {
mixins: [CommonMixin],
props: {
currentDate: {
type: Date,
required: true,
},
timeframeItem: {
type: Date,
required: true,
},
},
data() {
return {
presetType: PRESET_TYPES.WEEKS,
indicatorStyle: {},
};
},
computed: {
headerSubItems() {
const timeframeItem = new Date(this.timeframeItem.getTime());
......@@ -37,17 +26,12 @@ export default {
return headerSubItems;
},
},
mounted() {
this.$nextTick(() => {
this.indicatorStyle = this.getIndicatorStyles();
});
},
methods: {
getSubItemValueClass(subItem) {
// Show dark color text only for current & upcoming dates
if (subItem.getTime() === this.currentDate.getTime()) {
if (subItem.getTime() === this.$options.currentDate.getTime()) {
return 'label-dark label-bold';
} else if (subItem > this.currentDate) {
} else if (subItem > this.$options.currentDate) {
return 'label-dark';
}
return '';
......@@ -63,12 +47,13 @@ export default {
:key="index"
:class="getSubItemValueClass(subItem)"
class="sublabel-value"
data-testid="sublabel-value"
>{{ subItem.getDate() }}</span
>
<span
v-if="hasToday"
:style="indicatorStyle"
class="current-day-indicator-header preset-weeks gl-absolute"
:style="getIndicatorStyles()"
class="current-day-indicator-header preset-weeks"
></span>
</div>
</template>
<script>
import CurrentDayIndicator from './current_day_indicator.vue';
export default {
components: {
CurrentDayIndicator,
},
props: {
presetType: {
type: String,
required: true,
},
rotations: {
type: Array,
required: true,
},
timeframe: {
type: Array,
required: true,
},
},
};
</script>
<template>
<div class="list-section">
<div class="list-item list-item-empty clearfix">
<span class="details-cell"></span>
<span
v-for="(timeframeItem, index) in timeframe"
:key="index"
class="timeline-cell"
data-testid="timelineCell"
>
<current-day-indicator :preset-type="presetType" :timeframe-item="timeframeItem" />
</span>
</div>
</div>
</template>
<script>
import { GlCard, GlButton, GlModalDirective } from '@gitlab/ui';
import { s__ } from '~/locale';
import WeeksHeaderItem from './preset_weeks/weeks_header_item.vue';
import AddRotationModal from '../../rotations/add_rotation_modal.vue';
export const i18n = {
rotationTitle: s__('OnCallSchedules|Rotations'),
addARotation: s__('OnCallSchedules|Add a rotation'),
};
export default {
i18n,
components: {
GlButton,
GlCard,
WeeksHeaderItem,
AddRotationModal,
},
directives: {
GlModal: GlModalDirective,
},
props: {
presetType: {
......@@ -34,17 +19,6 @@ export default {
</script>
<template>
<div>
<gl-card header-class="gl-bg-transparent">
<template #header>
<div class="gl-display-flex gl-justify-content-space-between">
<h6 class="gl-m-0">{{ $options.i18n.rotationTitle }}</h6>
<gl-button v-gl-modal="'create-schedule-rotation-modal'" variant="link">{{
$options.i18n.addARotation
}}</gl-button>
</div>
</template>
<div class="timeline-section clearfix">
<span class="timeline-header-blank"></span>
<weeks-header-item
......@@ -55,7 +29,4 @@ export default {
:timeframe="timeframe"
/>
</div>
</gl-card>
<add-rotation-modal />
</div>
</template>
......@@ -9,7 +9,3 @@ export const PRESET_DEFAULTS = {
TIMEFRAME_LENGTH: 2,
},
};
export const PAST_DATE = new Date(new Date().getFullYear() - 100, 0, 1);
export const FUTURE_DATE = new Date(new Date().getFullYear() + 100, 0, 1);
import { DAYS_IN_WEEK } from '../constants';
export default {
currentDate: null,
computed: {
hasToday() {
const timeframeItem = new Date(this.timeframeItem.getTime());
......@@ -16,11 +17,17 @@ export default {
);
return (
this.currentDate.getTime() >= headerSubItems[0].getTime() &&
this.currentDate.getTime() <= headerSubItems[headerSubItems.length - 1].getTime()
this.$options.currentDate.getTime() >= headerSubItems[0].getTime() &&
this.$options.currentDate.getTime() <= headerSubItems[headerSubItems.length - 1].getTime()
);
},
},
beforeCreate() {
const currentDate = new Date();
currentDate.setHours(0, 0, 0, 0);
this.$options.currentDate = currentDate;
},
methods: {
getIndicatorStyles() {
// as we start schedule scale from the current date the indicator will always be on the first date. So we find
......
......@@ -5,7 +5,7 @@ exports[`AddScheduleModal renders modal layout 1`] = `
actioncancel="[object Object]"
actionprimary="[object Object]"
modalclass=""
modalid="modalId"
modalid="addScheduleModal"
size="sm"
title="Add schedule"
titletag="h4"
......
......@@ -6,7 +6,7 @@ exports[`UpdateScheduleModal renders update schedule modal layout 1`] = `
actionprimary="[object Object]"
data-testid="update-schedule-modal-37"
modalclass=""
modalid="updateScheduleModal"
modalid="editScheduleModal"
size="sm"
title="Edit schedule"
titletag="h4"
......
......@@ -2,6 +2,7 @@ import { shallowMount } from '@vue/test-utils';
import { GlModal, GlAlert } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
import AddScheduleModal from 'ee/oncall_schedules/components/add_schedule_modal.vue';
import { addScheduleModalId } from 'ee/oncall_schedules/components/oncall_schedules_wrapper';
import { getOncallSchedulesQueryResponse } from './mocks/apollo_mock';
import mockTimezones from './mocks/mockTimezones.json';
......@@ -22,7 +23,7 @@ describe('AddScheduleModal', () => {
};
},
propsData: {
modalId: 'modalId',
modalId: addScheduleModalId,
...props,
},
provide: {
......
......@@ -8,6 +8,7 @@ import destroyOncallScheduleMutation from 'ee/oncall_schedules/graphql/mutations
import DeleteScheduleModal, {
i18n,
} from 'ee/oncall_schedules/components/delete_schedule_modal.vue';
import { deleteScheduleModalId } from 'ee/oncall_schedules/components/oncall_schedule';
import {
getOncallSchedulesQueryResponse,
destroyScheduleResponse,
......@@ -53,6 +54,7 @@ describe('DeleteScheduleModal', () => {
};
},
propsData: {
modalId: deleteScheduleModalId,
schedule,
...props,
},
......@@ -95,6 +97,7 @@ describe('DeleteScheduleModal', () => {
apolloProvider: fakeApollo,
propsData: {
schedule,
modalId: deleteScheduleModalId,
},
provide: {
projectPath,
......
......@@ -6,6 +6,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import getOncallSchedulesQuery from 'ee/oncall_schedules/graphql/queries/get_oncall_schedules.query.graphql';
import updateOncallScheduleMutation from 'ee/oncall_schedules/graphql/mutations/update_oncall_schedule.mutation.graphql';
import UpdateScheduleModal, { i18n } from 'ee/oncall_schedules/components/edit_schedule_modal.vue';
import { editScheduleModalId } from 'ee/oncall_schedules/components/oncall_schedule';
import {
getOncallSchedulesQueryResponse,
updateScheduleResponse,
......@@ -52,6 +53,7 @@ describe('UpdateScheduleModal', () => {
};
},
propsData: {
modalId: editScheduleModalId,
schedule,
...props,
},
......@@ -98,6 +100,7 @@ describe('UpdateScheduleModal', () => {
};
},
propsData: {
modalId: editScheduleModalId,
schedule,
},
provide: {
......@@ -122,7 +125,7 @@ describe('UpdateScheduleModal', () => {
describe('renders update modal with the correct schedule information', () => {
it('renders name of correct modal id', () => {
expect(findModal().attributes('modalid')).toBe('updateScheduleModal');
expect(findModal().attributes('modalid')).toBe(editScheduleModalId);
});
it('renders name of schedule to update', () => {
......
import { shallowMount } from '@vue/test-utils';
import { GlCard, GlSprintf } from '@gitlab/ui';
import { GlCard, GlSprintf, GlButton } from '@gitlab/ui';
import OnCallSchedule, { i18n } from 'ee/oncall_schedules/components/oncall_schedule.vue';
import ScheduleTimelineSection from 'ee/oncall_schedules/components/schedule/components/schedule_timeline_section.vue';
import RotationsListSection from 'ee/oncall_schedules/components/schedule/components/rotations_list_section.vue';
import * as utils from 'ee/oncall_schedules/components/schedule/utils';
import * as commonUtils from 'ee/oncall_schedules/utils/common_utils';
import { PRESET_TYPES } from 'ee/oncall_schedules/components/schedule/constants';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import mockTimezones from './mocks/mockTimezones.json';
describe('On-call schedule', () => {
......@@ -21,7 +23,8 @@ describe('On-call schedule', () => {
const formattedTimezone = '(UTC-09:00) AKST Alaska';
function mountComponent({ schedule } = {}) {
wrapper = shallowMount(OnCallSchedule, {
wrapper = extendedWrapper(
shallowMount(OnCallSchedule, {
propsData: {
schedule,
},
......@@ -32,7 +35,8 @@ describe('On-call schedule', () => {
GlCard,
GlSprintf,
},
});
}),
);
}
beforeEach(() => {
......@@ -46,23 +50,32 @@ describe('On-call schedule', () => {
wrapper = null;
});
const findCardHeader = () => wrapper.find('.gl-card-header');
const findCardDescription = () => wrapper.find('.gl-card-body');
const findScheduleTimeline = () => findCardDescription().find(ScheduleTimelineSection);
const findScheduleHeader = () => wrapper.findByTestId('scheduleHeader');
const findRotationsHeader = () => wrapper.findByTestId('rotationsHeader');
const findSchedule = () => wrapper.findByTestId('scheduleBody');
const findRotations = () => wrapper.findByTestId('rotationsBody');
const findAddRotationsBtn = () => findRotationsHeader().find(GlButton);
const findScheduleTimeline = () => findRotations().find(ScheduleTimelineSection);
const findRotationsList = () => findRotations().find(RotationsListSection);
it('shows schedule title', () => {
expect(findCardHeader().text()).toBe(mockSchedule.name);
expect(findScheduleHeader().text()).toBe(mockSchedule.name);
});
it('shows timezone info', () => {
const shortTz = i18n.scheduleForTz.replace('%{tzShort}', lastTz.identifier);
const longTz = formattedTimezone;
const description = findCardDescription().text();
const description = findSchedule().text();
expect(description).toContain(shortTz);
expect(description).toContain(longTz);
});
it('renders ScheduleShell', () => {
it('renders rotations header', () => {
expect(findRotationsHeader().text()).toContain(i18n.rotationTitle);
expect(findAddRotationsBtn().text()).toContain(i18n.addARotation);
});
it('renders schedule timeline', () => {
const timeline = findScheduleTimeline();
expect(timeline.exists()).toBe(true);
expect(timeline.props()).toEqual({
......@@ -70,4 +83,14 @@ describe('On-call schedule', () => {
timeframe: mockWeeksTimeFrame,
});
});
it('renders rotations list', () => {
const rotationsList = findRotationsList();
expect(rotationsList.exists()).toBe(true);
expect(rotationsList.props()).toEqual({
presetType: PRESET_TYPES.WEEKS,
timeframe: mockWeeksTimeFrame,
rotations: expect.any(Array),
});
});
});
......@@ -5,7 +5,7 @@ exports[`AddRotationModal renders rotation modal layout 1`] = `
actioncancel="[object Object]"
actionprimary="[object Object]"
modalclass=""
modalid="create-schedule-rotation-modal"
modalid="addRotationModal"
size="sm"
title="Add rotation"
titletag="h4"
......
......@@ -3,6 +3,7 @@ import createMockApollo from 'jest/helpers/mock_apollo_helper';
import VueApollo from 'vue-apollo';
import waitForPromises from 'helpers/wait_for_promises';
import { GlDropdownItem, GlModal, GlAlert, GlTokenSelector } from '@gitlab/ui';
import { addRotationModalId } from 'ee/oncall_schedules/components/oncall_schedule';
import AddRotationModal from 'ee/oncall_schedules/components/rotations/add_rotation_modal.vue';
// import createOncallScheduleRotationMutation from 'ee/oncall_schedules/graphql/create_oncall_schedule_rotation.mutation.graphql';
import usersSearchQuery from '~/graphql_shared/queries/users_search.query.graphql';
......@@ -34,6 +35,7 @@ describe('AddRotationModal', () => {
};
},
propsData: {
modalId: addRotationModalId,
...props,
},
provide: {
......@@ -58,6 +60,9 @@ describe('AddRotationModal', () => {
wrapper = shallowMount(AddRotationModal, {
localVue,
propsData: {
modalId: addRotationModalId,
},
apolloProvider: fakeApollo,
data() {
return {
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RotationsListSectionComponent renders component layout 1`] = `
<div
class="list-section"
>
<div
class="list-item list-item-empty clearfix"
>
<span
class="details-cell"
/>
<span
class="timeline-cell"
data-testid="timelineCell"
>
<current-day-indicator-stub
presettype="WEEKS"
timeframeitem="Mon Jan 01 2018 00:00:00 GMT+0000 (Greenwich Mean Time)"
/>
</span>
<span
class="timeline-cell"
data-testid="timelineCell"
>
<current-day-indicator-stub
presettype="WEEKS"
timeframeitem="Mon Jan 08 2018 00:00:00 GMT+0000 (Greenwich Mean Time)"
/>
</span>
</div>
</div>
`;
import { shallowMount } from '@vue/test-utils';
import CurrentDayIndicator from 'ee/oncall_schedules/components/schedule/components/current_day_indicator.vue';
import { PRESET_TYPES, DAYS_IN_WEEK } from 'ee/oncall_schedules/components/schedule/constants';
import { useFakeDate } from 'helpers/fake_date';
describe('CurrentDayIndicator', () => {
let wrapper;
// January 3rd, 2018 - current date (faked)
useFakeDate(2018, 0, 3);
// January 1st, 2018 is the first day of the week-long timeframe
// so as long as current date (faked January 3rd, 2018) is within week timeframe
// current indicator will be rendered
const mockTimeframeInitialDate = new Date(2018, 0, 1);
function mountComponent() {
wrapper = shallowMount(CurrentDayIndicator, {
propsData: {
presetType: PRESET_TYPES.WEEKS,
timeframeItem: mockTimeframeInitialDate,
},
});
}
beforeEach(() => {
mountComponent();
});
afterEach(() => {
if (wrapper) {
wrapper.destroy();
wrapper = null;
}
});
it('renders span element containing class `current-day-indicator`', () => {
expect(wrapper.classes('current-day-indicator')).toBe(true);
});
it('sets correct styles', async () => {
const left = 100 / DAYS_IN_WEEK / 2;
expect(wrapper.attributes('style')).toBe(`left: ${left}%;`);
});
});
import { shallowMount } from '@vue/test-utils';
import WeeksHeaderItemComponent from 'ee/oncall_schedules/components/schedule/components/preset_weeks/weeks_header_item.vue';
import { getTimeframeForWeeksView } from 'ee/oncall_schedules/components/schedule/utils';
import { useFakeDate } from 'helpers/fake_date';
describe('WeeksHeaderItemComponent', () => {
let wrapper;
// January 3rd, 2018 - current date (faked)
useFakeDate(2018, 0, 3);
const mockTimeframeIndex = 0;
const mockTimeframeInitialDate = new Date(2018, 0, 1);
const mockTimeframeWeeks = getTimeframeForWeeksView(mockTimeframeInitialDate);
......@@ -12,7 +15,7 @@ describe('WeeksHeaderItemComponent', () => {
timeframeIndex = mockTimeframeIndex,
timeframeItem = mockTimeframeWeeks[mockTimeframeIndex],
timeframe = mockTimeframeWeeks,
}) {
} = {}) {
wrapper = shallowMount(WeeksHeaderItemComponent, {
propsData: {
timeframeIndex,
......@@ -23,7 +26,7 @@ describe('WeeksHeaderItemComponent', () => {
}
beforeEach(() => {
mountComponent({});
mountComponent();
});
afterEach(() => {
......@@ -33,14 +36,8 @@ describe('WeeksHeaderItemComponent', () => {
}
});
describe('data', () => {
it('returns default data props', () => {
const currentDate = new Date();
expect(wrapper.vm.currentDate.getDate()).toBe(currentDate.getDate());
});
});
const findHeaderLabel = () => wrapper.find('[data-testid="timeline-header-label"]');
describe('computed', () => {
describe('lastDayOfCurrentWeek', () => {
it('returns date object representing last day of the week as set in `timeframeItem`', () => {
expect(wrapper.vm.lastDayOfCurrentWeek.getDate()).toBe(
......@@ -51,48 +48,43 @@ describe('WeeksHeaderItemComponent', () => {
describe('timelineHeaderLabel', () => {
it('returns string containing Year, Month and Date for the first timeframe item in the entire timeframe', () => {
expect(wrapper.vm.timelineHeaderLabel).toBe('2018 Jan 1');
expect(findHeaderLabel().text()).toBe('2018 Jan 1');
});
it('returns string containing Year, Month and Date for timeframe item when it is first week of the year', () => {
it('returns string containing Year, Month and Date for timeframe item that is the first week of the year', () => {
mountComponent({
timeframeIndex: 3,
timeframeItem: new Date(2019, 0, 6),
});
expect(wrapper.vm.timelineHeaderLabel).toBe('2019 Jan 6');
expect(findHeaderLabel().text()).toBe('2019 Jan 6');
});
it('returns string containing only Month and Date timeframe item when it is somewhere in the middle of timeframe', () => {
it('returns string containing only Month and Date when timeframe item is somewhere in the middle of the timeframe', () => {
mountComponent({
timeframeIndex: mockTimeframeIndex + 1,
timeframeItem: mockTimeframeWeeks[mockTimeframeIndex + 1],
});
expect(wrapper.vm.timelineHeaderLabel).toBe('Jan 8');
expect(findHeaderLabel().text()).toBe('Jan 8');
});
});
describe('timelineHeaderClass', () => {
it('returns empty string when timeframeItem week is less than current week', () => {
expect(wrapper.vm.timelineHeaderClass).toBe('');
});
it('returns string containing `label-dark label-bold` when current week is same as timeframeItem week', () => {
wrapper.setData({ currentDate: mockTimeframeWeeks[mockTimeframeIndex] });
expect(wrapper.vm.timelineHeaderClass).toBe('label-dark label-bold');
});
});
it('returns empty string when timeframeItem week is outside of current week', () => {
mountComponent({
timeframeIndex: 3,
timeframeItem: new Date(2017, 0, 6),
});
describe('template', () => {
it('renders component container element with class `timeline-header-item`', () => {
expect(wrapper.classes()).toContain('timeline-header-item');
expect(findHeaderLabel().classes()).not.toEqual(
expect.arrayContaining(['label-dark', 'label-bold']),
);
});
it('renders item label element class `item-label` and value as `timelineHeaderLabel`', () => {
expect(wrapper.find('.item-label').text()).toBe('2018 Jan 1');
it('returns string containing `label-dark label-bold` when current week is same as timeframeItem week', () => {
expect(findHeaderLabel().classes()).toEqual(
expect.arrayContaining(['label-dark', 'label-bold']),
);
});
});
});
import { shallowMount } from '@vue/test-utils';
import WeeksHeaderSubItemComponent from 'ee/oncall_schedules/components/schedule/components/preset_weeks/weeks_header_sub_item.vue';
import { getTimeframeForWeeksView } from 'ee/oncall_schedules/components/schedule/utils';
import { PRESET_TYPES } from 'ee/oncall_schedules/components/schedule/constants';
import { useFakeDate } from 'helpers/fake_date';
describe('MonthsHeaderSubItemComponent', () => {
describe('WeeksHeaderSubItemComponent', () => {
let wrapper;
// January 3rd, 2018 - current date (faked)
useFakeDate(2018, 0, 3);
const mockTimeframeInitialDate = new Date(2018, 0, 1);
const mockTimeframeWeeks = getTimeframeForWeeksView(mockTimeframeInitialDate);
function mountComponent({
currentDate = mockTimeframeWeeks[0],
timeframeItem = mockTimeframeWeeks[0],
}) {
function mountComponent({ timeframeItem = mockTimeframeWeeks[0] }) {
wrapper = shallowMount(WeeksHeaderSubItemComponent, {
propsData: {
currentDate,
timeframeItem,
},
});
......@@ -31,12 +29,7 @@ describe('MonthsHeaderSubItemComponent', () => {
}
});
describe('data', () => {
it('initializes `presetType` and `indicatorStyles` data props', () => {
expect(wrapper.vm.presetType).toBe(PRESET_TYPES.WEEKS);
expect(wrapper.vm.indicatorStyle).toBeDefined();
});
});
const findSublabelValues = () => wrapper.findAll('[data-testid="sublabel-value"]');
describe('computed', () => {
describe('headerSubItems', () => {
......@@ -50,29 +43,6 @@ describe('MonthsHeaderSubItemComponent', () => {
});
});
describe('methods', () => {
describe('getSubItemValueClass', () => {
it('returns string containing `label-dark` when provided subItem is greater than current week day', () => {
mountComponent({
currentDate: new Date(2018, 0, 1), // Jan 1, 2018
});
const subItem = new Date(2018, 0, 25); // Jan 25, 2018
expect(wrapper.vm.getSubItemValueClass(subItem)).toBe('label-dark');
});
it('returns string containing `label-dark label-bold` when provided subItem is same as current week day', () => {
const currentDate = new Date(2018, 0, 25);
mountComponent({
currentDate,
});
const subItem = currentDate;
expect(wrapper.vm.getSubItemValueClass(subItem)).toBe('label-dark label-bold');
});
});
});
describe('template', () => {
it('renders component container element with class `item-sublabel`', () => {
expect(wrapper.classes()).toContain('item-sublabel');
......@@ -83,7 +53,29 @@ describe('MonthsHeaderSubItemComponent', () => {
});
it('renders element with class `current-day-indicator-header` when hasToday is true', () => {
// January 1st, 2018 is the first day of the week-long timeframe
// so as long as current date (faked January 3rd, 2018) is within week timeframe
// current indicator will be rendered
expect(wrapper.find('.current-day-indicator-header.preset-weeks').exists()).toBe(true);
});
it('sublabel has `label-dark` class when it is for the day greater than current week day', () => {
// Timeframe starts at Jan 1, 2018, faked today is Jan 3, 2018 (3rd item in a week timeframe)
// labels for dates after current have 'label-dark' class
expect(
findSublabelValues()
.at(3)
.classes(),
).toContain('label-dark');
});
it("sublabel has `label-dark label-bold` classes when it is for today's date", () => {
// Timeframe starts at Jan 1, 2018, faked today is Jan 3, 2018 (3rd item in a week timeframe)
expect(
findSublabelValues()
.at(2)
.classes(),
).toEqual(expect.arrayContaining(['label-dark', 'label-bold']));
});
});
});
import { shallowMount } from '@vue/test-utils';
import { GlCard } from '@gitlab/ui';
import RotationsListSection from 'ee/oncall_schedules/components/schedule/components/rotations_list_section.vue';
import CurrentDayIndicator from 'ee/oncall_schedules/components/schedule/components/current_day_indicator.vue';
import { getTimeframeForWeeksView } from 'ee/oncall_schedules/components/schedule/utils';
import { PRESET_TYPES } from 'ee/oncall_schedules/components/schedule/constants';
describe('RotationsListSectionComponent', () => {
let wrapper;
const mockTimeframeInitialDate = new Date(2018, 0, 1);
const mockTimeframeWeeks = getTimeframeForWeeksView(mockTimeframeInitialDate);
function mountComponent({
presetType = PRESET_TYPES.WEEKS,
timeframe = mockTimeframeWeeks,
} = {}) {
wrapper = shallowMount(RotationsListSection, {
propsData: {
presetType,
timeframe,
rotations: [],
},
stubs: {
GlCard,
},
});
}
beforeEach(() => {
mountComponent();
});
afterEach(() => {
if (wrapper) {
wrapper.destroy();
wrapper = null;
}
});
const findTimelineCells = () => wrapper.findAll('[data-testid="timelineCell"]');
it('renders component layout', () => {
expect(wrapper.element).toMatchSnapshot();
});
it('renders timeline cell items based on timeframe data', () => {
expect(findTimelineCells().length).toBe(mockTimeframeWeeks.length);
});
it('renders current day indicator in the first timeline cell', () => {
expect(
findTimelineCells()
.at(0)
.find(CurrentDayIndicator)
.exists(),
).toBe(true);
});
});
import { shallowMount } from '@vue/test-utils';
import { GlCard, GlButton } from '@gitlab/ui';
import ScheduleTimelineSection, {
i18n,
} from 'ee/oncall_schedules/components/schedule/components/schedule_timeline_section.vue';
import { GlCard } from '@gitlab/ui';
import ScheduleTimelineSection from 'ee/oncall_schedules/components/schedule/components/schedule_timeline_section.vue';
import WeeksHeaderItem from 'ee/oncall_schedules/components/schedule/components/preset_weeks/weeks_header_item.vue';
import { getTimeframeForWeeksView } from 'ee/oncall_schedules/components/schedule/utils';
import { PRESET_TYPES } from 'ee/oncall_schedules/components/schedule/constants';
describe('RoadmapTimelineSectionComponent', () => {
describe('TimelineSectionComponent', () => {
let wrapper;
const mockTimeframeInitialDate = new Date(2018, 0, 1);
const mockTimeframeWeeks = getTimeframeForWeeksView(mockTimeframeInitialDate);
const findRotations = () => wrapper.find(GlCard);
const findAddRotation = () => wrapper.find(GlButton);
function mountComponent({
presetType = PRESET_TYPES.WEEKS,
timeframe = mockTimeframeWeeks,
......@@ -52,13 +47,4 @@ describe('RoadmapTimelineSectionComponent', () => {
it('renders weeks header items based on timeframe data', () => {
expect(wrapper.findAll(WeeksHeaderItem).length).toBe(mockTimeframeWeeks.length);
});
it('renders the rotation card wrapper', () => {
expect(findRotations().exists()).toBe(true);
});
it('renders the add rotation button in the rotation card wrapper', () => {
expect(findAddRotation().exists()).toBe(true);
expect(findAddRotation().text()).toBe(i18n.addARotation);
});
});
import { shallowMount } from '@vue/test-utils';
import CommonMixin from 'ee/oncall_schedules/components/schedule/mixins/common_mixin';
import { DAYS_IN_WEEK } from 'ee/oncall_schedules/components/schedule/constants';
import { useFakeDate } from 'helpers/fake_date';
describe('Schedule Common Mixins', () => {
// January 3rd, 2018
useFakeDate(2018, 0, 3);
const today = new Date();
let wrapper;
const component = {
template: `<span></span>`,
props: {
timeframeItem: {
type: [Date, Object],
required: true,
},
},
mixins: [CommonMixin],
};
const mountComponent = (props = {}) => {
wrapper = shallowMount(component, {
propsData: {
timeframeItem: today,
...props,
},
});
};
describe('data', () => {
it('initializes currentDate default value', () => {
mountComponent();
expect(wrapper.vm.$options.currentDate).toEqual(today);
});
});
describe('hasToday', () => {
it('returns true when today (January 3rd, 2018) is within the set week (January 1st, 2018)', () => {
// January 1st, 2018
mountComponent({
timeframeItem: new Date(2018, 0, 1),
});
expect(wrapper.vm.hasToday).toBe(true);
});
it('returns false when today (January 3rd, 2018) is NOT within the set week (January 8th, 2018)', () => {
// February 1st, 2018
mountComponent({
timeframeItem: new Date(2018, 0, 8),
});
expect(wrapper.vm.hasToday).toBe(false);
});
});
describe('getIndicatorStyles', () => {
it('returns object containing `left` offset', () => {
const left = 100 / DAYS_IN_WEEK / 2;
expect(wrapper.vm.getIndicatorStyles()).toEqual(
expect.objectContaining({
left: `${left}%`,
}),
);
});
});
});
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