Commit 90775637 authored by Tim Zallmann's avatar Tim Zallmann

Merge branch 'refactor-sidebar-date-picker-spec' into 'master'

Migrate sidebar_date_picker_spec to Jest

Closes #32462

See merge request gitlab-org/gitlab!17399
parents 84ea3e4a fac3af76
......@@ -99,10 +99,14 @@ export default {
required: false,
default: true,
},
fieldName: {
type: String,
required: false,
default: () => _.uniqueId('dateType_'),
},
},
data() {
return {
fieldName: _.uniqueId('dateType_'),
editing: false,
};
},
......@@ -171,8 +175,8 @@ export default {
this.editing = false;
this.$emit('toggleDateType', true, true);
},
toggleDatePicker(e) {
this.editing = !this.editing;
startEditing(e) {
this.editing = true;
e.stopPropagation();
},
newDateSelected(date = null) {
......@@ -204,9 +208,10 @@ export default {
/>
<gl-button
v-show="canUpdate && !editing"
ref="editButton"
variant="link"
class="btn-sidebar-action"
@click="toggleDatePicker"
@click="startEditing"
>
{{ __('Edit') }}
</gl-button>
......@@ -250,6 +255,7 @@ export default {
<span v-if="selectedAndEditable" class="no-value d-flex">
&nbsp;&ndash;&nbsp;
<gl-button
ref="removeButton"
variant="link"
class="btn-sidebar-date-remove"
@click="newDateSelected(null)"
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SidebarDatePicker renders expected template 1`] = `
<div
class="block date epic-date"
>
<collapsedcalendaricon-stub
class="sidebar-collapsed-icon"
containerclass=""
showicon="true"
text="None"
tooltiptext=""
/>
<div
class="title"
>
Date
<!---->
<div
class="float-right d-flex"
>
<icon-stub
cssclasses="help-icon append-right-5"
name="question-o"
size="16"
tabindex="0"
/>
<glbutton-stub
class="btn-sidebar-action"
variant="link"
>
Edit
</glbutton-stub>
<!---->
</div>
</div>
<div
class="value"
>
<div
class="value-type-fixed text-secondary is-option-selected d-flex"
>
<input
name="datetype_test"
type="radio"
/>
<span
class="prepend-left-5"
>
Fixed:
</span>
<span
class="d-flex value-content prepend-left-2"
>
<span
class="no-value"
>
None
</span>
</span>
</div>
<abbr
class="value-type-dynamic text-secondary d-flex prepend-top-10"
data-html="true"
data-original-title="Select an issue with milestone to set date"
data-placement="bottom"
title=""
>
<input
name="datetype_test"
type="radio"
/>
<span
class="prepend-left-5"
>
From milestones:
</span>
<span
class="value-content prepend-left-2"
>
None
</span>
<!---->
</abbr>
</div>
</div>
`;
import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
import { TEST_HOST } from 'helpers/test_constants';
import SidebarDatepicker from 'ee/epic/components/sidebar_items/sidebar_date_picker.vue';
import { mockDatePickerProps } from '../../mock_data';
import Icon from '~/vue_shared/components/icon.vue';
import DatePicker from '~/vue_shared/components/pikaday.vue';
import ToggleSidebar from '~/vue_shared/components/sidebar/toggle_sidebar.vue';
import CollapsedCalendarIcon from '~/vue_shared/components/sidebar/collapsed_calendar_icon.vue';
const mockPopoverBind = jest.fn();
jest.mock('~/vue_shared/directives/popover', () => ({
bind: (...args) => mockPopoverBind(...args),
}));
describe('SidebarDatePicker', () => {
let originalGon;
beforeAll(() => {
originalGon = global.gon;
global.gon = { gitlab_url: TEST_HOST };
});
afterAll(() => {
global.gon = originalGon;
});
let wrapper;
const findIconByName = name =>
wrapper
.findAll(Icon)
.filter(w => w.props().name === name)
.at(0);
const findEditButton = () => wrapper.find({ ref: 'editButton' });
const findDirectiveCallByTitle = title =>
mockPopoverBind.mock.calls.find(([, binding]) => binding.value.title === title);
const findRemoveButton = () => wrapper.find({ ref: 'removeButton' });
const createFakeEvent = () => ({ stopPropagation: jest.fn() });
const startEditing = () => {
const e = createFakeEvent();
findEditButton().vm.$emit('click', e);
};
const createComponent = props => {
wrapper = shallowMount(SidebarDatepicker, {
propsData: {
...mockDatePickerProps,
...props,
},
sync: false,
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('generates unique names for input if `fieldName` prop is not provided', () => {
createComponent();
const anotherWrapper = shallowMount(SidebarDatepicker, {
propsData: mockDatePickerProps,
sync: false,
});
const firstInputName = wrapper.find('input').attributes('name');
const otherInputName = anotherWrapper.find('input').attributes('name');
expect(firstInputName).toContain('dateType_');
expect(otherInputName).toContain('dateType_');
expect(firstInputName).not.toEqual(otherInputName);
anotherWrapper.destroy();
});
it('renders remove button when both `selectedDate` is defined and `canUpdate` is true', () => {
createComponent({
selectedDate: new Date(),
dateFixed: new Date(),
canUpdate: true,
});
expect(findRemoveButton().exists()).toBe(true);
});
describe('collapsed calendar icon', () => {
it('receives full date string in words based on `selectedDate` prop value', () => {
createComponent({
selectedDate: new Date(2018, 0, 1),
});
expect(wrapper.find(CollapsedCalendarIcon).props('text')).toBe('Jan 1, 2018');
});
it('receives `None` when `selectedDateWords` is not defined', () => {
createComponent();
expect(wrapper.find(CollapsedCalendarIcon).props('text')).toBe('None');
});
});
it('returns full date string in words based on `dateFixed` prop value', () => {
createComponent({
dateFixed: new Date(2018, 0, 1),
});
expect(wrapper.text()).toContain('Jan 1, 2018');
});
it('returns full date string in words when `dateFromMilestones` is defined', () => {
createComponent({ dateFromMilestones: new Date(2018, 0, 1) });
expect(wrapper.text()).toContain('From milestones: Jan 1, 2018');
});
it('returns `None` when `dateFromMilestones` is not defined', () => {
createComponent();
expect(wrapper.text()).toContain('From milestones: None');
});
it('passes correct popover options to directive', () => {
createComponent();
return wrapper.vm.$nextTick().then(() => {
const expectedTitle =
'These dates affect how your epics appear in the roadmap. Dates from milestones come from the milestones assigned to issues in the epic. You can also set fixed dates or remove them entirely.';
const [, binding] = findDirectiveCallByTitle(expectedTitle);
const { content, ...popoverConfig } = binding.value;
delete popoverConfig.template;
const expectedContent = '/help/user/group/epics/index.md#start-date-and-due-date';
const expectedPopoverConfig = {
html: true,
trigger: 'focus',
title: expectedTitle,
container: 'body',
boundary: 'viewport',
};
expect(mockPopoverBind).toHaveBeenCalled();
expect(popoverConfig).toStrictEqual(expectedPopoverConfig);
expect(content).toContain(expectedContent);
});
});
it('returns popover config object containing title with appropriate string', () => {
createComponent({ isDateInvalid: true, selectedDateIsFixed: false });
return wrapper.vm.$nextTick().then(() => {
const expectedTitle = 'Selected date is invalid';
const [, targetBinding] = findDirectiveCallByTitle(expectedTitle);
const { content } = targetBinding.value;
expect(content).toContain('/help/user/group/epics/index.md#start-date-and-due-date');
expect(content).toContain('How can I solve this?');
});
});
it('stops editing and emits `toggleDateType` event on component on `hidePicker` from date picker', () => {
createComponent({ canUpdate: true });
startEditing();
return wrapper.vm
.$nextTick()
.then(() => {
wrapper.find(DatePicker).vm.$emit('hidePicker');
expect(wrapper.emitted().toggleDateType[0]).toStrictEqual([true, true]);
})
.then(() => wrapper.vm.$nextTick())
.then(() => {
expect(wrapper.find(DatePicker).exists()).toBe(false);
});
});
it('starts editing when clicked on edit button', () => {
createComponent();
expect(wrapper.find(DatePicker).exists()).toBe(false);
const e = createFakeEvent();
findEditButton().vm.$emit('click', e);
return wrapper.vm.$nextTick().then(() => {
expect(e.stopPropagation).toHaveBeenCalled();
expect(wrapper.find(DatePicker).exists()).toBe(true);
});
});
it('stops editing and emits `saveDate` when `newDateSelected` emitted by date picker', () => {
const date = new Date();
createComponent();
startEditing();
return wrapper.vm.$nextTick().then(() => {
wrapper.find(DatePicker).vm.$emit('newDateSelected', date);
expect(wrapper.emitted().saveDate).toStrictEqual([[date]]);
});
});
it('emits `toggleDateType` event on component when input is clicked', () => {
createComponent({ canUpdate: true });
wrapper.find('input').trigger('click');
expect(wrapper.emitted().toggleDateType).toStrictEqual([[true]]);
});
it('emits `toggleCollapse` event when toggle-sidebar emits `toggle` event', () => {
createComponent({ showToggleSidebar: true });
wrapper.find(ToggleSidebar).vm.$emit('toggle');
expect(wrapper.emitted().toggleCollapse).toBeDefined();
});
it('renders expected template', () => {
createComponent({
fieldName: 'datetype_test',
});
expect(wrapper.element).toMatchSnapshot();
});
it('renders collapse button when `showToggleSidebar` prop is `true`', () => {
createComponent({ showToggleSidebar: true });
expect(wrapper.find(ToggleSidebar).exists()).toBe(true);
});
it('renders loading icon when `dateSaveInProgress` prop is true', () => {
createComponent({ dateSaveInProgress: true });
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
it('renders date warning icon when `isDateInvalid` prop is `true`', () => {
createComponent({ isDateInvalid: true, selectedDateIsFixed: false });
const warningIcon = findIconByName('warning');
expect(warningIcon.exists()).toBe(true);
expect(warningIcon.attributes('tabindex')).toBe('0');
});
});
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { TEST_HOST } from 'spec/test_constants';
const metaFixture = getJSONFixture('epic/mock_meta.json');
const meta = JSON.parse(metaFixture.meta);
const initial = JSON.parse(metaFixture.initial);
export const mockEpicMeta = convertObjectPropsToCamelCase(meta, {
deep: true,
});
export const mockEpicData = convertObjectPropsToCamelCase(
Object.assign({}, getJSONFixture('epic/mock_data.json'), initial, {
endpoint: TEST_HOST,
sidebarCollapsed: false,
}),
{ deep: true },
);
export const mockDatePickerProps = {
blockClass: 'epic-date',
sidebarCollapsed: false,
showToggleSidebar: false,
dateSaveInProgress: false,
canUpdate: true,
label: 'Date',
datePickerLabel: 'Fixed date',
selectedDate: null,
selectedDateIsFixed: true,
dateFromMilestones: null,
dateFixed: null,
dateFromMilestonesTooltip: 'Select an issue with milestone to set date',
isDateInvalid: false,
dateInvalidTooltip: 'Selected date is invalid',
};
export const mockLabels = [
{
id: 26,
title: 'Foo Label',
description: 'Foobar',
color: '#BADA55',
text_color: '#FFFFFF',
},
];
export const mockAncestors = [
{
id: 1,
title: 'Parent epic',
url: '/groups/gitlab-org/-/epics/6',
},
{
id: 2,
title: 'Parent epic 2',
url: '/groups/gitlab-org/-/epics/7',
},
];
import Vue from 'vue';
import SidebarDatepicker from 'ee/epic/components/sidebar_items/sidebar_date_picker.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mockDatePickerProps } from '../../mock_data';
describe('SidebarDatePicker', () => {
const orginalGitLabUrl = gon.gitlab_url;
gon.gitlab_url = gl.TEST_HOST;
let vm;
beforeEach(() => {
const Component = Vue.extend(SidebarDatepicker);
vm = mountComponent(Component, mockDatePickerProps);
});
afterEach(() => {
gon.gitlab_url = orginalGitLabUrl;
vm.$destroy();
});
describe('data', () => {
it('return data props with uniqueId for `fieldName`', () => {
expect(vm.fieldName).toContain('dateType_');
});
});
describe('computed', () => {
describe('selectedAndEditable', () => {
it('returns `true` when both `selectedDate` is defined and `canUpdate` is true', done => {
vm.selectedDate = new Date();
Vue.nextTick()
.then(() => {
expect(vm.selectedAndEditable).toBe(true);
})
.then(done)
.catch(done.fail);
});
});
describe('selectedDateWords', () => {
it('returns full date string in words based on `selectedDate` prop value', done => {
vm.selectedDate = new Date(2018, 0, 1);
Vue.nextTick()
.then(() => {
expect(vm.selectedDateWords).toBe('Jan 1, 2018');
})
.then(done)
.catch(done.fail);
});
});
describe('dateFixedWords', () => {
it('returns full date string in words based on `dateFixed` prop value', done => {
vm.dateFixed = new Date(2018, 0, 1);
Vue.nextTick()
.then(() => {
expect(vm.dateFixedWords).toBe('Jan 1, 2018');
})
.then(done)
.catch(done.fail);
});
});
describe('dateFromMilestonesWords', () => {
it('returns full date string in words when `dateFromMilestones` is defined', done => {
vm.dateFromMilestones = new Date(2018, 0, 1);
Vue.nextTick()
.then(() => {
expect(vm.dateFromMilestonesWords).toBe('Jan 1, 2018');
})
.then(done)
.catch(done.fail);
});
it('returns `None` when `dateFromMilestones` is not defined', () => {
expect(vm.dateFromMilestonesWords).toBe('None');
});
});
describe('collapsedText', () => {
it('returns value of `selectedDateWords` when it is defined', done => {
vm.selectedDate = new Date(2018, 0, 1);
Vue.nextTick()
.then(() => {
expect(vm.collapsedText).toBe('Jan 1, 2018');
})
.then(done)
.catch(done.fail);
});
it('returns `None` when `selectedDateWords` is not defined', () => {
expect(vm.collapsedText).toBe('None');
});
});
describe('popoverOptions', () => {
it('returns popover config object containing title with appropriate string', () => {
expect(vm.popoverOptions.title).toBe(
'These dates affect how your epics appear in the roadmap. Dates from milestones come from the milestones assigned to issues in the epic. You can also set fixed dates or remove them entirely.',
);
});
it('returns popover config object containing `content` with href pointing to correct documentation', () => {
const hrefContent = vm.popoverOptions.content.trim();
expect(hrefContent).toContain(
`${gon.gitlab_url}/help/user/group/epics/index.md#start-date-and-due-date`,
);
expect(hrefContent).toContain('More information');
});
});
describe('dateInvalidPopoverOptions', () => {
it('returns popover config object containing title with appropriate string', () => {
expect(vm.dateInvalidPopoverOptions.title).toBe('Selected date is invalid');
});
it('returns popover config object containing `content` with href pointing to correct documentation', () => {
const hrefContent = vm.dateInvalidPopoverOptions.content.trim();
expect(hrefContent).toContain(
`${gon.gitlab_url}/help/user/group/epics/index.md#start-date-and-due-date`,
);
expect(hrefContent).toContain('How can I solve this?');
});
});
});
describe('methods', () => {
describe('getPopoverConfig', () => {
it('returns popover config object with provided `title` and `content` values', () => {
const title = 'Popover title';
const content = 'This is a popover content';
const popoverConfig = vm.getPopoverConfig({ title, content });
const expectedPopoverConfig = {
html: true,
trigger: 'focus',
container: 'body',
boundary: 'viewport',
template: '<div class="popover-header"></div>',
title,
content,
};
Object.keys(popoverConfig).forEach(key => {
if (key === 'template') {
expect(popoverConfig[key]).toContain(expectedPopoverConfig[key]);
} else {
expect(popoverConfig[key]).toBe(expectedPopoverConfig[key]);
}
});
});
});
describe('stopEditing', () => {
it('sets `editing` prop to `false` and emits `toggleDateType` event on component', () => {
spyOn(vm, '$emit');
vm.stopEditing();
expect(vm.editing).toBe(false);
expect(vm.$emit).toHaveBeenCalledWith('toggleDateType', true, true);
});
});
describe('toggleDatePicker', () => {
it('flips value of `editing` prop from `true` to `false` and vice-versa', () => {
const e = new Event('click');
spyOn(e, 'stopPropagation');
vm.editing = true;
vm.toggleDatePicker(e);
expect(vm.editing).toBe(false);
expect(e.stopPropagation).toHaveBeenCalled();
});
});
describe('newDateSelected', () => {
it('sets `editing` prop to `false` and emits `saveDate` event on component', () => {
spyOn(vm, '$emit');
const date = new Date();
vm.newDateSelected(date);
expect(vm.editing).toBe(false);
expect(vm.$emit).toHaveBeenCalledWith('saveDate', date);
});
});
describe('toggleDateType', () => {
it('emits `toggleDateType` event on component', () => {
spyOn(vm, '$emit');
vm.toggleDateType(true);
expect(vm.$emit).toHaveBeenCalledWith('toggleDateType', true);
});
});
describe('toggleSidebar', () => {
it('emits `toggleCollapse` event on component', () => {
spyOn(vm, '$emit');
vm.toggleSidebar();
expect(vm.$emit).toHaveBeenCalledWith('toggleCollapse');
});
});
});
describe('template', () => {
it('renders component container element', () => {
expect(vm.$el.classList.contains('block', 'date', 'epic-date')).toBe(true);
});
it('renders collapsed calendar icon component', () => {
expect(vm.$el.querySelector('.sidebar-collapsed-icon')).not.toBe(null);
});
it('renders collapse button when `showToggleSidebar` prop is `true`', done => {
vm.showToggleSidebar = true;
Vue.nextTick()
.then(() => {
expect(vm.$el.querySelector('button.btn-sidebar-action')).not.toBe(null);
})
.then(done)
.catch(done.fail);
});
it('renders title element', () => {
expect(vm.$el.querySelector('.title')).not.toBe(null);
});
it('renders loading icon when `isLoading` prop is true', done => {
vm.dateSaveInProgress = true;
Vue.nextTick()
.then(() => {
expect(vm.$el.querySelector('.loading-container')).not.toBe(null);
})
.then(done)
.catch(done.fail);
});
it('renders help icon', () => {
const helpIconEl = vm.$el.querySelector('.help-icon');
expect(helpIconEl).not.toBe(null);
expect(helpIconEl.getAttribute('tabindex')).toBe('0');
expect(helpIconEl.querySelector('use').getAttribute('xlink:href')).toContain('question-o');
});
it('renderts edit button', () => {
const buttonEl = vm.$el.querySelector('button.btn-sidebar-action');
expect(buttonEl).not.toBe(null);
expect(buttonEl.innerText.trim()).toBe('Edit');
});
it('renders value container element', () => {
expect(vm.$el.querySelector('.value .value-type-fixed')).not.toBe(null);
expect(vm.$el.querySelector('.value .value-type-dynamic')).not.toBe(null);
});
it('renders fixed type date selection element', () => {
const valueFixedEl = vm.$el.querySelector('.value .value-type-fixed');
expect(valueFixedEl.querySelector('input[type="radio"]')).not.toBe(null);
expect(valueFixedEl.innerText.trim()).toContain('Fixed:');
expect(valueFixedEl.querySelector('.value-content').innerText.trim()).toContain('None');
});
it('renders dynamic type date selection element', () => {
const valueDynamicEl = vm.$el.querySelector('.value abbr.value-type-dynamic');
expect(valueDynamicEl.querySelector('input[type="radio"]')).not.toBe(null);
expect(valueDynamicEl.innerText.trim()).toContain('From milestones:');
expect(valueDynamicEl.querySelector('.value-content').innerText.trim()).toContain('None');
});
it('renders date warning icon when `isDateInvalid` prop is `true`', done => {
vm.isDateInvalid = true;
vm.selectedDateIsFixed = false;
Vue.nextTick()
.then(() => {
const warningIconEl = vm.$el.querySelector('.date-warning-icon');
expect(warningIconEl).not.toBe(null);
expect(warningIconEl.getAttribute('tabindex')).toBe('0');
expect(warningIconEl.querySelector('use').getAttribute('xlink:href')).toContain(
'warning',
);
})
.then(done)
.catch(done.fail);
});
});
});
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { TEST_HOST } from 'spec/test_constants';
const metaFixture = getJSONFixture('epic/mock_meta.json');
const meta = JSON.parse(metaFixture.meta);
const initial = JSON.parse(metaFixture.initial);
export const mockEpicMeta = convertObjectPropsToCamelCase(meta, {
deep: true,
});
export const mockEpicData = convertObjectPropsToCamelCase(
Object.assign({}, getJSONFixture('epic/mock_data.json'), initial, {
endpoint: TEST_HOST,
sidebarCollapsed: false,
}),
{ deep: true },
);
export const mockDatePickerProps = {
blockClass: 'epic-date',
sidebarCollapsed: false,
showToggleSidebar: false,
dateSaveInProgress: false,
canUpdate: true,
label: 'Date',
datePickerLabel: 'Fixed date',
selectedDate: null,
selectedDateIsFixed: true,
dateFromMilestones: null,
dateFixed: null,
dateFromMilestonesTooltip: 'Select an issue with milestone to set date',
isDateInvalid: false,
dateInvalidTooltip: 'Selected date is invalid',
};
export const mockLabels = [
{
id: 26,
title: 'Foo Label',
description: 'Foobar',
color: '#BADA55',
text_color: '#FFFFFF',
},
];
export const mockAncestors = [
{
id: 1,
title: 'Parent epic',
url: '/groups/gitlab-org/-/epics/6',
},
{
id: 2,
title: 'Parent epic 2',
url: '/groups/gitlab-org/-/epics/7',
},
];
export * from '../../frontend/epic/mock_data';
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