gitlab-org--gitlab-foss/spec/frontend/milestones/milestone_combobox_spec.js

518 lines
16 KiB
JavaScript

import Vuex from 'vuex';
import { mount, createLocalVue } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { GlLoadingIcon, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui';
import { s__ } from '~/locale';
import { ENTER_KEY } from '~/lib/utils/keys';
import MilestoneCombobox from '~/milestones/components/milestone_combobox.vue';
import { projectMilestones, groupMilestones } from './mock_data';
import createStore from '~/milestones/stores/';
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' },
];
const localVue = createLocalVue();
localVue.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 = {}) => {
wrapper = mount(MilestoneCombobox, {
propsData: {
projectId,
groupId,
groupMilestonesAvailable,
extraLinks,
value: [],
...props,
},
attrs,
listeners: {
// simulate a parent component v-model binding
input: selectedMilestone => {
wrapper.setProps({ value: selectedMilestone });
},
},
stubs: {
GlSearchBoxByType: true,
},
localVue,
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.find(GlLoadingIcon);
const findSearchBox = () => wrapper.find(GlSearchBoxByType);
const findProjectMilestonesSection = () =>
wrapper.find('[data-testid="project-milestones-section"]');
const findProjectMilestonesDropdownItems = () =>
findProjectMilestonesSection().findAll(GlDropdownItem);
const findFirstProjectMilestonesDropdownItem = () => findProjectMilestonesDropdownItems().at(0);
const findGroupMilestonesSection = () => wrapper.find('[data-testid="group-milestones-section"]');
const findGroupMilestonesDropdownItems = () =>
findGroupMilestonesSection().findAll(GlDropdownItem);
const findFirstGroupMilestonesDropdownItem = () => findGroupMilestonesDropdownItems().at(0);
//
// Expecters
//
const projectMilestoneSectionContainsErrorMessage = () => {
const projectMilestoneSection = findProjectMilestonesSection();
return projectMilestoneSection
.text()
.includes(s__('MilestoneCombobox|An error occurred while searching for milestones'));
};
const groupMilestoneSectionContainsErrorMessage = () => {
const groupMilestoneSection = findGroupMilestonesSection();
return groupMilestoneSection
.text()
.includes(s__('MilestoneCombobox|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 = ({ andClearMocks } = { andClearMocks: false }) =>
axios.waitForAll().then(() => {
if (andClearMocks) {
projectMilestonesApiCallSpy.mockClear();
groupMilestonesApiCallSpy.mockClear();
}
});
describe('initialization behavior', () => {
beforeEach(createComponent);
it('initializes the dropdown with milestones when mounted', () => {
return waitForRequests().then(() => {
expect(projectMilestonesApiCallSpy).toHaveBeenCalledTimes(1);
expect(groupMilestonesApiCallSpy).toHaveBeenCalledTimes(1);
});
});
it('shows a spinner while network requests are in progress', () => {
expect(findLoadingIcon().exists()).toBe(true);
return waitForRequests().then(() => {
expect(findLoadingIcon().exists()).toBe(false);
});
});
it('shows additional links', () => {
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(s__('MilestoneCombobox|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 localVue.nextTick();
expect(
findFirstProjectMilestonesDropdownItem()
.find('span')
.classes('selected-item'),
).toBe(false);
selectFirstProjectMilestone();
await localVue.nextTick();
expect(
findFirstProjectMilestonesDropdownItem()
.find('span')
.classes('selected-item'),
).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 localVue.nextTick();
expect(findButtonContent().text()).toBe(s__('MilestoneCombobox|No milestone'));
selectFirstProjectMilestone();
await localVue.nextTick();
expect(findButtonContent().text()).toBe('v1.0');
});
it('updates the v-model binding with the project milestone title', () => {
expect(wrapper.vm.value).toEqual([]);
selectFirstProjectMilestone();
expect(wrapper.vm.value).toEqual(['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 localVue.nextTick();
expect(
findFirstGroupMilestonesDropdownItem()
.find('span')
.classes('selected-item'),
).toBe(false);
selectFirstGroupMilestone();
await localVue.nextTick();
expect(
findFirstGroupMilestonesDropdownItem()
.find('span')
.classes('selected-item'),
).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 localVue.nextTick();
expect(findButtonContent().text()).toBe(s__('MilestoneCombobox|No milestone'));
selectFirstGroupMilestone();
await localVue.nextTick();
expect(findButtonContent().text()).toBe('group-v1.0');
});
it('updates the v-model binding with the group milestone title', () => {
expect(wrapper.vm.value).toEqual([]);
selectFirstGroupMilestone();
expect(wrapper.vm.value).toEqual(['group-v1.0']);
});
});
});
});
});
});