520 lines
16 KiB
JavaScript
520 lines
16 KiB
JavaScript
import { GlLoadingIcon, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui';
|
|
import { mount } from '@vue/test-utils';
|
|
import axios from 'axios';
|
|
import MockAdapter from 'axios-mock-adapter';
|
|
import Vue, { nextTick } from 'vue';
|
|
import Vuex from 'vuex';
|
|
import { ENTER_KEY } from '~/lib/utils/keys';
|
|
import MilestoneCombobox from '~/milestones/components/milestone_combobox.vue';
|
|
import createStore from '~/milestones/stores/';
|
|
import { projectMilestones, groupMilestones } from '../mock_data';
|
|
|
|
const extraLinks = [
|
|
{ text: 'Create new', url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/new' },
|
|
{ text: 'Manage milestones', url: '/h5bp/html5-boilerplate/-/milestones' },
|
|
];
|
|
|
|
Vue.use(Vuex);
|
|
|
|
describe('Milestone combobox component', () => {
|
|
const projectId = '8';
|
|
const groupId = '24';
|
|
const groupMilestonesAvailable = true;
|
|
const X_TOTAL_HEADER = 'x-total';
|
|
|
|
let wrapper;
|
|
let projectMilestonesApiCallSpy;
|
|
let groupMilestonesApiCallSpy;
|
|
let searchApiCallSpy;
|
|
|
|
const createComponent = (props = {}, attrs = {}) => {
|
|
const propsData = {
|
|
projectId,
|
|
groupId,
|
|
groupMilestonesAvailable,
|
|
extraLinks,
|
|
value: [],
|
|
...props,
|
|
};
|
|
|
|
wrapper = mount(MilestoneCombobox, {
|
|
propsData,
|
|
attrs,
|
|
listeners: {
|
|
// simulate a parent component v-model binding
|
|
input: (selectedMilestone) => {
|
|
// ugly hack because setProps plays bad with immediate watchers
|
|
// see https://github.com/vuejs/vue-test-utils/issues/1140 and
|
|
// https://github.com/vuejs/vue-test-utils/pull/1752
|
|
propsData.value = selectedMilestone;
|
|
wrapper.setProps({ value: selectedMilestone });
|
|
},
|
|
},
|
|
stubs: {
|
|
GlSearchBoxByType: true,
|
|
},
|
|
store: createStore(),
|
|
});
|
|
};
|
|
|
|
beforeEach(() => {
|
|
const mock = new MockAdapter(axios);
|
|
gon.api_version = 'v4';
|
|
|
|
projectMilestonesApiCallSpy = jest
|
|
.fn()
|
|
.mockReturnValue([200, projectMilestones, { [X_TOTAL_HEADER]: '6' }]);
|
|
|
|
groupMilestonesApiCallSpy = jest
|
|
.fn()
|
|
.mockReturnValue([200, groupMilestones, { [X_TOTAL_HEADER]: '6' }]);
|
|
|
|
searchApiCallSpy = jest
|
|
.fn()
|
|
.mockReturnValue([200, projectMilestones, { [X_TOTAL_HEADER]: '6' }]);
|
|
|
|
mock
|
|
.onGet(`/api/v4/projects/${projectId}/milestones`)
|
|
.reply((config) => projectMilestonesApiCallSpy(config));
|
|
|
|
mock
|
|
.onGet(`/api/v4/groups/${groupId}/milestones`)
|
|
.reply((config) => groupMilestonesApiCallSpy(config));
|
|
|
|
mock.onGet(`/api/v4/projects/${projectId}/search`).reply((config) => searchApiCallSpy(config));
|
|
});
|
|
|
|
afterEach(() => {
|
|
wrapper.destroy();
|
|
wrapper = null;
|
|
});
|
|
|
|
//
|
|
// Finders
|
|
//
|
|
const findButtonContent = () => wrapper.find('[data-testid="milestone-combobox-button-content"]');
|
|
|
|
const findNoResults = () => wrapper.find('[data-testid="milestone-combobox-no-results"]');
|
|
|
|
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
|
|
|
|
const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
|
|
|
|
const findProjectMilestonesSection = () =>
|
|
wrapper.find('[data-testid="project-milestones-section"]');
|
|
const findProjectMilestonesDropdownItems = () =>
|
|
findProjectMilestonesSection().findAllComponents(GlDropdownItem);
|
|
const findFirstProjectMilestonesDropdownItem = () => findProjectMilestonesDropdownItems().at(0);
|
|
|
|
const findGroupMilestonesSection = () => wrapper.find('[data-testid="group-milestones-section"]');
|
|
const findGroupMilestonesDropdownItems = () =>
|
|
findGroupMilestonesSection().findAllComponents(GlDropdownItem);
|
|
const findFirstGroupMilestonesDropdownItem = () => findGroupMilestonesDropdownItems().at(0);
|
|
|
|
//
|
|
// Expecters
|
|
//
|
|
const projectMilestoneSectionContainsErrorMessage = () => {
|
|
const projectMilestoneSection = findProjectMilestonesSection();
|
|
|
|
return projectMilestoneSection
|
|
.text()
|
|
.includes('An error occurred while searching for milestones');
|
|
};
|
|
|
|
const groupMilestoneSectionContainsErrorMessage = () => {
|
|
const groupMilestoneSection = findGroupMilestonesSection();
|
|
|
|
return groupMilestoneSection
|
|
.text()
|
|
.includes('An error occurred while searching for milestones');
|
|
};
|
|
|
|
//
|
|
// Convenience methods
|
|
//
|
|
const updateQuery = (newQuery) => {
|
|
findSearchBox().vm.$emit('input', newQuery);
|
|
};
|
|
|
|
const selectFirstProjectMilestone = () => {
|
|
findFirstProjectMilestonesDropdownItem().vm.$emit('click');
|
|
};
|
|
|
|
const selectFirstGroupMilestone = () => {
|
|
findFirstGroupMilestonesDropdownItem().vm.$emit('click');
|
|
};
|
|
|
|
const waitForRequests = async ({ andClearMocks } = { andClearMocks: false }) => {
|
|
await axios.waitForAll();
|
|
if (andClearMocks) {
|
|
projectMilestonesApiCallSpy.mockClear();
|
|
groupMilestonesApiCallSpy.mockClear();
|
|
}
|
|
};
|
|
|
|
describe('initialization behavior', () => {
|
|
it('initializes the dropdown with milestones when mounted', () => {
|
|
createComponent();
|
|
|
|
return waitForRequests().then(() => {
|
|
expect(projectMilestonesApiCallSpy).toHaveBeenCalledTimes(1);
|
|
expect(groupMilestonesApiCallSpy).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
|
|
it('shows a spinner while network requests are in progress', () => {
|
|
createComponent();
|
|
|
|
expect(findLoadingIcon().exists()).toBe(true);
|
|
|
|
return waitForRequests().then(() => {
|
|
expect(findLoadingIcon().exists()).toBe(false);
|
|
});
|
|
});
|
|
|
|
it('shows additional links', () => {
|
|
createComponent();
|
|
|
|
const links = wrapper.findAll('[data-testid="milestone-combobox-extra-links"]');
|
|
links.wrappers.forEach((item, idx) => {
|
|
expect(item.text()).toBe(extraLinks[idx].text);
|
|
expect(item.attributes('href')).toBe(extraLinks[idx].url);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('post-initialization behavior', () => {
|
|
describe('when the parent component provides an `id` binding', () => {
|
|
const id = '8';
|
|
|
|
beforeEach(() => {
|
|
createComponent({}, { id });
|
|
|
|
return waitForRequests();
|
|
});
|
|
|
|
it('adds the provided ID to the GlDropdown instance', () => {
|
|
expect(wrapper.attributes().id).toBe(id);
|
|
});
|
|
});
|
|
|
|
describe('when milestones are pre-selected', () => {
|
|
beforeEach(() => {
|
|
createComponent({ value: projectMilestones });
|
|
|
|
return waitForRequests();
|
|
});
|
|
|
|
it('renders the pre-selected milestones', () => {
|
|
expect(findButtonContent().text()).toBe('v0.1 + 5 more');
|
|
});
|
|
});
|
|
|
|
describe('when the search query is updated', () => {
|
|
beforeEach(() => {
|
|
createComponent();
|
|
|
|
return waitForRequests({ andClearMocks: true });
|
|
});
|
|
|
|
it('requeries the search when the search query is updated', () => {
|
|
updateQuery('v1.2.3');
|
|
|
|
return waitForRequests().then(() => {
|
|
expect(searchApiCallSpy).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('when the Enter is pressed', () => {
|
|
beforeEach(() => {
|
|
createComponent();
|
|
|
|
return waitForRequests({ andClearMocks: true });
|
|
});
|
|
|
|
it('requeries the search when Enter is pressed', () => {
|
|
findSearchBox().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY }));
|
|
|
|
return waitForRequests().then(() => {
|
|
expect(searchApiCallSpy).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('when no results are found', () => {
|
|
beforeEach(() => {
|
|
projectMilestonesApiCallSpy = jest
|
|
.fn()
|
|
.mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]);
|
|
|
|
groupMilestonesApiCallSpy = jest.fn().mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]);
|
|
|
|
createComponent();
|
|
|
|
return waitForRequests();
|
|
});
|
|
|
|
describe('when the search query is empty', () => {
|
|
it('renders a "no results" message', () => {
|
|
expect(findNoResults().text()).toBe('No matching results');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('project milestones', () => {
|
|
describe('when the project milestones search returns results', () => {
|
|
beforeEach(() => {
|
|
createComponent();
|
|
|
|
return waitForRequests();
|
|
});
|
|
|
|
it('renders the project milestones section in the dropdown', () => {
|
|
expect(findProjectMilestonesSection().exists()).toBe(true);
|
|
});
|
|
|
|
it('renders the "Project milestones" heading with a total number indicator', () => {
|
|
expect(
|
|
findProjectMilestonesSection()
|
|
.find('[data-testid="milestone-results-section-header"]')
|
|
.text(),
|
|
).toBe('Project milestones 6');
|
|
});
|
|
|
|
it("does not render an error message in the project milestone section's body", () => {
|
|
expect(projectMilestoneSectionContainsErrorMessage()).toBe(false);
|
|
});
|
|
|
|
it('renders each project milestones as a selectable item', () => {
|
|
const dropdownItems = findProjectMilestonesDropdownItems();
|
|
|
|
projectMilestones.forEach((milestone, i) => {
|
|
expect(dropdownItems.at(i).text()).toBe(milestone.title);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('when the project milestones search returns no results', () => {
|
|
beforeEach(() => {
|
|
projectMilestonesApiCallSpy = jest
|
|
.fn()
|
|
.mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]);
|
|
|
|
createComponent();
|
|
|
|
return waitForRequests();
|
|
});
|
|
|
|
it('does not render the project milestones section in the dropdown', () => {
|
|
expect(findProjectMilestonesSection().exists()).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('when the project milestones search returns an error', () => {
|
|
beforeEach(() => {
|
|
projectMilestonesApiCallSpy = jest.fn().mockReturnValue([500]);
|
|
searchApiCallSpy = jest.fn().mockReturnValue([500]);
|
|
|
|
createComponent({ value: [] });
|
|
|
|
return waitForRequests();
|
|
});
|
|
|
|
it('renders the project milestones section in the dropdown', () => {
|
|
expect(findProjectMilestonesSection().exists()).toBe(true);
|
|
});
|
|
|
|
it("renders an error message in the project milestones section's body", () => {
|
|
expect(projectMilestoneSectionContainsErrorMessage()).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('selection', () => {
|
|
beforeEach(() => {
|
|
createComponent();
|
|
|
|
return waitForRequests();
|
|
});
|
|
|
|
it('renders a checkmark by the selected item', async () => {
|
|
selectFirstProjectMilestone();
|
|
|
|
await nextTick();
|
|
|
|
expect(
|
|
findFirstProjectMilestonesDropdownItem()
|
|
.find('svg')
|
|
.classes('gl-dropdown-item-check-icon'),
|
|
).toBe(true);
|
|
|
|
selectFirstProjectMilestone();
|
|
|
|
await nextTick();
|
|
|
|
expect(
|
|
findFirstProjectMilestonesDropdownItem().find('svg').classes('gl-visibility-hidden'),
|
|
).toBe(true);
|
|
});
|
|
|
|
describe('when a project milestones is selected', () => {
|
|
beforeEach(() => {
|
|
createComponent();
|
|
projectMilestonesApiCallSpy = jest
|
|
.fn()
|
|
.mockReturnValue([200, [{ title: 'v1.0' }], { [X_TOTAL_HEADER]: '1' }]);
|
|
|
|
return waitForRequests();
|
|
});
|
|
|
|
it("displays the project milestones name in the dropdown's button", async () => {
|
|
selectFirstProjectMilestone();
|
|
await nextTick();
|
|
|
|
expect(findButtonContent().text()).toBe('v1.0');
|
|
|
|
selectFirstProjectMilestone();
|
|
await nextTick();
|
|
|
|
expect(findButtonContent().text()).toBe('No milestone');
|
|
});
|
|
|
|
it('updates the v-model binding with the project milestone title', async () => {
|
|
selectFirstProjectMilestone();
|
|
await nextTick();
|
|
|
|
expect(wrapper.emitted().input[0][0]).toStrictEqual(['v1.0']);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('group milestones', () => {
|
|
describe('when the group milestones search returns results', () => {
|
|
beforeEach(() => {
|
|
createComponent();
|
|
|
|
return waitForRequests();
|
|
});
|
|
|
|
it('renders the group milestones section in the dropdown', () => {
|
|
expect(findGroupMilestonesSection().exists()).toBe(true);
|
|
});
|
|
|
|
it('renders the "Group milestones" heading with a total number indicator', () => {
|
|
expect(
|
|
findGroupMilestonesSection()
|
|
.find('[data-testid="milestone-results-section-header"]')
|
|
.text(),
|
|
).toBe('Group milestones 6');
|
|
});
|
|
|
|
it("does not render an error message in the group milestone section's body", () => {
|
|
expect(groupMilestoneSectionContainsErrorMessage()).toBe(false);
|
|
});
|
|
|
|
it('renders each group milestones as a selectable item', () => {
|
|
const dropdownItems = findGroupMilestonesDropdownItems();
|
|
|
|
groupMilestones.forEach((milestone, i) => {
|
|
expect(dropdownItems.at(i).text()).toBe(milestone.title);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('when the group milestones search returns no results', () => {
|
|
beforeEach(() => {
|
|
groupMilestonesApiCallSpy = jest
|
|
.fn()
|
|
.mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]);
|
|
|
|
createComponent();
|
|
|
|
return waitForRequests();
|
|
});
|
|
|
|
it('does not render the group milestones section in the dropdown', () => {
|
|
expect(findGroupMilestonesSection().exists()).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('when the group milestones search returns an error', () => {
|
|
beforeEach(() => {
|
|
groupMilestonesApiCallSpy = jest.fn().mockReturnValue([500]);
|
|
searchApiCallSpy = jest.fn().mockReturnValue([500]);
|
|
|
|
createComponent({ value: [] });
|
|
|
|
return waitForRequests();
|
|
});
|
|
|
|
it('renders the group milestones section in the dropdown', () => {
|
|
expect(findGroupMilestonesSection().exists()).toBe(true);
|
|
});
|
|
|
|
it("renders an error message in the group milestones section's body", () => {
|
|
expect(groupMilestoneSectionContainsErrorMessage()).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('selection', () => {
|
|
beforeEach(() => {
|
|
createComponent();
|
|
|
|
return waitForRequests();
|
|
});
|
|
|
|
it('renders a checkmark by the selected item', async () => {
|
|
selectFirstGroupMilestone();
|
|
|
|
await nextTick();
|
|
|
|
expect(
|
|
findFirstGroupMilestonesDropdownItem()
|
|
.find('svg')
|
|
.classes('gl-dropdown-item-check-icon'),
|
|
).toBe(true);
|
|
|
|
selectFirstGroupMilestone();
|
|
|
|
await nextTick();
|
|
|
|
expect(
|
|
findFirstGroupMilestonesDropdownItem().find('svg').classes('gl-visibility-hidden'),
|
|
).toBe(true);
|
|
});
|
|
|
|
describe('when a group milestones is selected', () => {
|
|
beforeEach(() => {
|
|
createComponent();
|
|
groupMilestonesApiCallSpy = jest
|
|
.fn()
|
|
.mockReturnValue([200, [{ title: 'group-v1.0' }], { [X_TOTAL_HEADER]: '1' }]);
|
|
|
|
return waitForRequests();
|
|
});
|
|
|
|
it("displays the group milestones name in the dropdown's button", async () => {
|
|
selectFirstGroupMilestone();
|
|
await nextTick();
|
|
|
|
expect(findButtonContent().text()).toBe('group-v1.0');
|
|
|
|
selectFirstGroupMilestone();
|
|
await nextTick();
|
|
|
|
expect(findButtonContent().text()).toBe('No milestone');
|
|
});
|
|
|
|
it('updates the v-model binding with the group milestone title', async () => {
|
|
selectFirstGroupMilestone();
|
|
await nextTick();
|
|
|
|
expect(wrapper.emitted().input[0][0]).toStrictEqual(['group-v1.0']);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|