gitlab-org--gitlab-foss/spec/frontend/import_entities/import_groups/components/import_table_spec.js

541 lines
16 KiB
JavaScript

import { GlAlert, GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import MockAdapter from 'axios-mock-adapter';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import httpStatus from '~/lib/utils/http_status';
import axios from '~/lib/utils/axios_utils';
import { STATUSES } from '~/import_entities/constants';
import { i18n, ROOT_NAMESPACE } from '~/import_entities/import_groups/constants';
import ImportTable from '~/import_entities/import_groups/components/import_table.vue';
import importGroupsMutation from '~/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql';
import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
import { availableNamespacesFixture, generateFakeEntry } from '../graphql/fixtures';
jest.mock('~/flash');
jest.mock('~/import_entities/import_groups/services/status_poller');
Vue.use(VueApollo);
describe('import table', () => {
let wrapper;
let apolloProvider;
let axiosMock;
const SOURCE_URL = 'https://demo.host';
const FAKE_GROUP = generateFakeEntry({ id: 1, status: STATUSES.NONE });
const FAKE_GROUPS = [
generateFakeEntry({ id: 1, status: STATUSES.NONE }),
generateFakeEntry({ id: 2, status: STATUSES.FINISHED }),
];
const FAKE_PAGE_INFO = { page: 1, perPage: 20, total: 40, totalPages: 2 };
const FAKE_VERSION_VALIDATION = {
features: {
projectMigration: { available: false, minVersion: '14.8.0' },
sourceInstanceVersion: '14.6.0',
},
};
const findImportSelectedButton = () =>
wrapper.findAll('button').wrappers.find((w) => w.text() === 'Import selected');
const findImportButtons = () =>
wrapper.findAll('button').wrappers.filter((w) => w.text() === 'Import');
const findPaginationDropdown = () => wrapper.find('[data-testid="page-size"]');
const findTargetNamespaceDropdown = (rowWrapper) =>
rowWrapper.find('[data-testid="target-namespace-selector"]');
const findPaginationDropdownText = () => findPaginationDropdown().find('button').text();
const findSelectionCount = () => wrapper.find('[data-test-id="selection-count"]');
const triggerSelectAllCheckbox = () =>
wrapper.find('thead input[type=checkbox]').trigger('click');
const selectRow = (idx) =>
wrapper.findAll('tbody td input[type=checkbox]').at(idx).trigger('click');
const createComponent = ({ bulkImportSourceGroups, importGroups }) => {
apolloProvider = createMockApollo([], {
Query: {
availableNamespaces: () => availableNamespacesFixture,
bulkImportSourceGroups,
},
Mutation: {
importGroups,
},
});
wrapper = mount(ImportTable, {
propsData: {
groupPathRegex: /.*/,
jobsPath: '/fake_job_path',
sourceUrl: SOURCE_URL,
historyPath: '/fake_history_path',
},
apolloProvider,
});
};
beforeAll(() => {
gon.api_version = 'v4';
});
beforeEach(() => {
axiosMock = new MockAdapter(axios);
axiosMock.onGet(/.*\/exists$/, () => []).reply(200);
});
afterEach(() => {
wrapper.destroy();
});
describe('loading state', () => {
it('renders loading icon while performing request', async () => {
createComponent({
bulkImportSourceGroups: () => new Promise(() => {}),
});
await waitForPromises();
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
it('does not renders loading icon when request is completed', async () => {
createComponent({
bulkImportSourceGroups: () => [],
});
await waitForPromises();
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
});
});
describe('empty state', () => {
it('renders message about empty state when no groups are available for import', async () => {
createComponent({
bulkImportSourceGroups: () => ({
nodes: [],
pageInfo: FAKE_PAGE_INFO,
versionValidation: FAKE_VERSION_VALIDATION,
}),
});
await waitForPromises();
expect(wrapper.find(GlEmptyState).props().title).toBe('You have no groups to import');
});
});
it('renders import row for each group in response', async () => {
createComponent({
bulkImportSourceGroups: () => ({
nodes: FAKE_GROUPS,
pageInfo: FAKE_PAGE_INFO,
versionValidation: FAKE_VERSION_VALIDATION,
}),
});
await waitForPromises();
expect(wrapper.findAll('tbody tr')).toHaveLength(FAKE_GROUPS.length);
});
it('correctly maintains root namespace as last import target', async () => {
createComponent({
bulkImportSourceGroups: () => ({
nodes: [
{
...generateFakeEntry({ id: 1, status: STATUSES.FINISHED }),
lastImportTarget: {
id: 1,
targetNamespace: ROOT_NAMESPACE.fullPath,
newName: 'does-not-matter',
},
},
],
pageInfo: FAKE_PAGE_INFO,
versionValidation: FAKE_VERSION_VALIDATION,
}),
});
await waitForPromises();
const firstRow = wrapper.find('tbody tr');
const targetNamespaceDropdownButton = findTargetNamespaceDropdown(firstRow).find(
'[aria-haspopup]',
);
expect(targetNamespaceDropdownButton.text()).toBe('No parent');
});
it('does not render status string when result list is empty', async () => {
createComponent({
bulkImportSourceGroups: jest.fn().mockResolvedValue({
nodes: [],
pageInfo: FAKE_PAGE_INFO,
versionValidation: FAKE_VERSION_VALIDATION,
}),
});
await waitForPromises();
expect(wrapper.text()).not.toContain('Showing 1-0');
});
it('invokes importGroups mutation when row button is clicked', async () => {
createComponent({
bulkImportSourceGroups: () => ({
nodes: [FAKE_GROUP],
pageInfo: FAKE_PAGE_INFO,
versionValidation: FAKE_VERSION_VALIDATION,
}),
});
jest.spyOn(apolloProvider.defaultClient, 'mutate');
await waitForPromises();
await findImportButtons()[0].trigger('click');
expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith({
mutation: importGroupsMutation,
variables: {
importRequests: [
{
newName: FAKE_GROUP.lastImportTarget.newName,
sourceGroupId: FAKE_GROUP.id,
targetNamespace: availableNamespacesFixture[0].fullPath,
},
],
},
});
});
it('displays error if importing group fails', async () => {
createComponent({
bulkImportSourceGroups: () => ({
nodes: [FAKE_GROUP],
pageInfo: FAKE_PAGE_INFO,
versionValidation: FAKE_VERSION_VALIDATION,
}),
importGroups: () => {
throw new Error();
},
});
axiosMock.onPost('/import/bulk_imports.json').reply(httpStatus.BAD_REQUEST);
await waitForPromises();
await findImportButtons()[0].trigger('click');
await waitForPromises();
expect(createFlash).toHaveBeenCalledWith(
expect.objectContaining({
message: i18n.ERROR_IMPORT,
}),
);
});
describe('pagination', () => {
const bulkImportSourceGroupsQueryMock = jest.fn().mockResolvedValue({
nodes: [FAKE_GROUP],
pageInfo: FAKE_PAGE_INFO,
versionValidation: FAKE_VERSION_VALIDATION,
});
beforeEach(() => {
createComponent({
bulkImportSourceGroups: bulkImportSourceGroupsQueryMock,
});
return waitForPromises();
});
it('correctly passes pagination info from query', () => {
expect(wrapper.find(PaginationLinks).props().pageInfo).toStrictEqual(FAKE_PAGE_INFO);
});
it('renders pagination dropdown', () => {
expect(findPaginationDropdown().exists()).toBe(true);
});
it('updates page size when selected in Dropdown', async () => {
const otherOption = findPaginationDropdown().findAll('li p').at(1);
expect(otherOption.text()).toMatchInterpolatedText('50 items per page');
bulkImportSourceGroupsQueryMock.mockResolvedValue({
nodes: [FAKE_GROUP],
pageInfo: { ...FAKE_PAGE_INFO, perPage: 50 },
versionValidation: FAKE_VERSION_VALIDATION,
});
await otherOption.trigger('click');
await waitForPromises();
expect(findPaginationDropdownText()).toMatchInterpolatedText('50 items per page');
});
it('updates page when page change is requested', async () => {
const REQUESTED_PAGE = 2;
wrapper.find(PaginationLinks).props().change(REQUESTED_PAGE);
await waitForPromises();
expect(bulkImportSourceGroupsQueryMock).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({ page: REQUESTED_PAGE }),
expect.anything(),
expect.anything(),
);
});
it('updates status text when page is changed', async () => {
const REQUESTED_PAGE = 2;
bulkImportSourceGroupsQueryMock.mockResolvedValue({
nodes: [FAKE_GROUP],
pageInfo: {
page: 2,
total: 38,
perPage: 20,
totalPages: 2,
},
versionValidation: FAKE_VERSION_VALIDATION,
});
wrapper.find(PaginationLinks).props().change(REQUESTED_PAGE);
await waitForPromises();
expect(wrapper.text()).toContain('Showing 21-21 of 38 groups from');
});
});
describe('filters', () => {
const bulkImportSourceGroupsQueryMock = jest.fn().mockResolvedValue({
nodes: [FAKE_GROUP],
pageInfo: FAKE_PAGE_INFO,
versionValidation: FAKE_VERSION_VALIDATION,
});
beforeEach(() => {
createComponent({
bulkImportSourceGroups: bulkImportSourceGroupsQueryMock,
});
return waitForPromises();
});
const setFilter = (value) => {
const input = wrapper.find('input[placeholder="Filter by source group"]');
input.setValue(value);
return input.trigger('keydown.enter');
};
it('properly passes filter to graphql query when search box is submitted', async () => {
createComponent({
bulkImportSourceGroups: bulkImportSourceGroupsQueryMock,
});
await waitForPromises();
const FILTER_VALUE = 'foo';
await setFilter(FILTER_VALUE);
await waitForPromises();
expect(bulkImportSourceGroupsQueryMock).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({ filter: FILTER_VALUE }),
expect.anything(),
expect.anything(),
);
});
it('updates status string when search box is submitted', async () => {
createComponent({
bulkImportSourceGroups: bulkImportSourceGroupsQueryMock,
});
await waitForPromises();
const FILTER_VALUE = 'foo';
await setFilter(FILTER_VALUE);
await waitForPromises();
expect(wrapper.text()).toContain('Showing 1-1 of 40 groups matching filter "foo" from');
});
it('properly resets filter in graphql query when search box is cleared', async () => {
const FILTER_VALUE = 'foo';
await setFilter(FILTER_VALUE);
await waitForPromises();
bulkImportSourceGroupsQueryMock.mockClear();
await apolloProvider.defaultClient.resetStore();
await setFilter('');
await waitForPromises();
expect(bulkImportSourceGroupsQueryMock).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({ filter: '' }),
expect.anything(),
expect.anything(),
);
});
});
describe('bulk operations', () => {
it('import all button correctly selects/deselects all groups', async () => {
createComponent({
bulkImportSourceGroups: () => ({
nodes: FAKE_GROUPS,
pageInfo: FAKE_PAGE_INFO,
versionValidation: FAKE_VERSION_VALIDATION,
}),
});
await waitForPromises();
expect(findSelectionCount().text()).toMatchInterpolatedText('0 selected');
await triggerSelectAllCheckbox();
expect(findSelectionCount().text()).toMatchInterpolatedText('2 selected');
await triggerSelectAllCheckbox();
expect(findSelectionCount().text()).toMatchInterpolatedText('0 selected');
});
it('import selected button is disabled when no groups selected', async () => {
createComponent({
bulkImportSourceGroups: () => ({
nodes: FAKE_GROUPS,
pageInfo: FAKE_PAGE_INFO,
versionValidation: FAKE_VERSION_VALIDATION,
}),
});
await waitForPromises();
expect(findImportSelectedButton().props().disabled).toBe(true);
});
it('import selected button is enabled when groups were selected for import', async () => {
createComponent({
bulkImportSourceGroups: () => ({
nodes: FAKE_GROUPS,
pageInfo: FAKE_PAGE_INFO,
versionValidation: FAKE_VERSION_VALIDATION,
}),
});
await waitForPromises();
await selectRow(0);
expect(findImportSelectedButton().props().disabled).toBe(false);
});
it('does not allow selecting already started groups', async () => {
const NEW_GROUPS = [generateFakeEntry({ id: 1, status: STATUSES.STARTED })];
createComponent({
bulkImportSourceGroups: () => ({
nodes: NEW_GROUPS,
pageInfo: FAKE_PAGE_INFO,
versionValidation: FAKE_VERSION_VALIDATION,
}),
});
await waitForPromises();
await selectRow(0);
await nextTick();
expect(findImportSelectedButton().props().disabled).toBe(true);
});
it('does not allow selecting groups with validation errors', async () => {
const NEW_GROUPS = [
generateFakeEntry({
id: 2,
status: STATUSES.NONE,
}),
];
createComponent({
bulkImportSourceGroups: () => ({
nodes: NEW_GROUPS,
pageInfo: FAKE_PAGE_INFO,
versionValidation: FAKE_VERSION_VALIDATION,
}),
});
await waitForPromises();
await wrapper.find('tbody input[aria-label="New name"]').setValue('');
jest.runOnlyPendingTimers();
await selectRow(0);
await nextTick();
expect(findImportSelectedButton().props().disabled).toBe(true);
});
it('invokes importGroups mutation when import selected button is clicked', async () => {
const NEW_GROUPS = [
generateFakeEntry({ id: 1, status: STATUSES.NONE }),
generateFakeEntry({ id: 2, status: STATUSES.NONE }),
generateFakeEntry({ id: 3, status: STATUSES.FINISHED }),
];
createComponent({
bulkImportSourceGroups: () => ({
nodes: NEW_GROUPS,
pageInfo: FAKE_PAGE_INFO,
versionValidation: FAKE_VERSION_VALIDATION,
}),
});
jest.spyOn(apolloProvider.defaultClient, 'mutate');
await waitForPromises();
await selectRow(0);
await selectRow(1);
await nextTick();
await findImportSelectedButton().trigger('click');
expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith({
mutation: importGroupsMutation,
variables: {
importRequests: [
{
targetNamespace: availableNamespacesFixture[0].fullPath,
newName: NEW_GROUPS[0].lastImportTarget.newName,
sourceGroupId: NEW_GROUPS[0].id,
},
{
targetNamespace: availableNamespacesFixture[0].fullPath,
newName: NEW_GROUPS[1].lastImportTarget.newName,
sourceGroupId: NEW_GROUPS[1].id,
},
],
},
});
});
});
describe('unavailable features warning', () => {
it('renders alert when there are unavailable features', async () => {
createComponent({
bulkImportSourceGroups: () => ({
nodes: FAKE_GROUPS,
pageInfo: FAKE_PAGE_INFO,
versionValidation: FAKE_VERSION_VALIDATION,
}),
});
await waitForPromises();
expect(wrapper.find(GlAlert).exists()).toBe(true);
expect(wrapper.find(GlAlert).text()).toContain('projects (require v14.8.0)');
});
it('does not renders alert when there are no unavailable features', async () => {
createComponent({
bulkImportSourceGroups: () => ({
nodes: FAKE_GROUPS,
pageInfo: FAKE_PAGE_INFO,
versionValidation: {
features: {
projectMigration: { available: true, minVersion: '14.8.0' },
sourceInstanceVersion: '14.6.0',
},
},
}),
});
await waitForPromises();
expect(wrapper.find(GlAlert).exists()).toBe(false);
});
});
});