import { GlLoadingIcon, GlButton, GlIntersectionObserver, GlFormInput } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { STATUSES } from '~/import_entities/constants';
import ImportProjectsTable from '~/import_entities/import_projects/components/import_projects_table.vue';
import ProviderRepoTableRow from '~/import_entities/import_projects/components/provider_repo_table_row.vue';
import * as getters from '~/import_entities/import_projects/store/getters';
import state from '~/import_entities/import_projects/store/state';
describe('ImportProjectsTable', () => {
let wrapper;
const USER_NAMESPACE = 'root';
const findFilterField = () =>
wrapper
.findAllComponents(GlFormInput)
.wrappers.find((w) => w.attributes('placeholder') === 'Filter by name');
const providerTitle = 'THE PROVIDER';
const providerRepo = {
importSource: {
id: 10,
sanitizedName: 'sanitizedName',
fullName: 'fullName',
},
importedProject: null,
};
const findImportAllButton = () =>
wrapper
.findAll(GlButton)
.filter((w) => w.props().variant === 'confirm')
.at(0);
const findImportAllModal = () => wrapper.find({ ref: 'importAllModal' });
const importAllFn = jest.fn();
const importAllModalShowFn = jest.fn();
const fetchReposFn = jest.fn();
function createComponent({
state: initialState,
getters: customGetters,
slots,
filterable,
paginatable,
} = {}) {
Vue.use(Vuex);
const store = new Vuex.Store({
state: { ...state(), defaultTargetNamespace: USER_NAMESPACE, ...initialState },
getters: {
...getters,
...customGetters,
},
actions: {
fetchRepos: fetchReposFn,
fetchJobs: jest.fn(),
fetchNamespaces: jest.fn(),
importAll: importAllFn,
stopJobsPolling: jest.fn(),
clearJobsEtagPoll: jest.fn(),
setFilter: jest.fn(),
},
});
wrapper = shallowMount(ImportProjectsTable, {
store,
propsData: {
providerTitle,
filterable,
paginatable,
},
slots,
stubs: {
GlModal: { template: '
Modal!
', methods: { show: importAllModalShowFn } },
},
});
}
afterEach(() => {
if (wrapper) {
wrapper.destroy();
wrapper = null;
}
});
it('renders a loading icon while repos are loading', () => {
createComponent({ state: { isLoadingRepos: true } });
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
it('renders a loading icon while namespaces are loading', () => {
createComponent({ state: { isLoadingNamespaces: true } });
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
it('renders a table with provider repos', () => {
const repositories = [
{ importSource: { id: 1 }, importedProject: null },
{ importSource: { id: 2 }, importedProject: { importStatus: STATUSES.FINISHED } },
{ importSource: { id: 3, incompatible: true }, importedProject: {} },
];
createComponent({
state: { namespaces: [{ fullPath: 'path' }], repositories },
});
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
expect(wrapper.find('table').exists()).toBe(true);
expect(
wrapper
.findAll('th')
.filter((w) => w.text() === `From ${providerTitle}`)
.exists(),
).toBe(true);
expect(wrapper.findAll(ProviderRepoTableRow)).toHaveLength(repositories.length);
});
it.each`
hasIncompatibleRepos | count | buttonText
${false} | ${1} | ${'Import 1 repository'}
${true} | ${1} | ${'Import 1 compatible repository'}
${false} | ${5} | ${'Import 5 repositories'}
${true} | ${5} | ${'Import 5 compatible repositories'}
`(
'import all button has "$buttonText" text when hasIncompatibleRepos is $hasIncompatibleRepos and repos count is $count',
({ hasIncompatibleRepos, buttonText, count }) => {
createComponent({
state: {
providerRepos: [providerRepo],
},
getters: {
hasIncompatibleRepos: () => hasIncompatibleRepos,
importAllCount: () => count,
},
});
expect(findImportAllButton().text()).toBe(buttonText);
},
);
it.each`
importingRepoCount | buttonMessage
${1} | ${'Importing 1 repository'}
${5} | ${'Importing 5 repositories'}
`(
'sets the button text to "$buttonMessage" when importing repos',
({ importingRepoCount, buttonMessage }) => {
createComponent({
state: {
providerRepos: [providerRepo],
},
getters: {
hasIncompatibleRepos: () => false,
importAllCount: () => 10,
isImportingAnyRepo: () => true,
importingRepoCount: () => importingRepoCount,
},
});
expect(findImportAllButton().text()).toBe(buttonMessage);
},
);
it('renders an empty state if there are no repositories available', () => {
createComponent({ state: { repositories: [] } });
expect(wrapper.find(ProviderRepoTableRow).exists()).toBe(false);
expect(wrapper.text()).toContain(`No ${providerTitle} repositories found`);
});
it('opens confirmation modal when import all button is clicked', async () => {
createComponent({ state: { repositories: [providerRepo] } });
findImportAllButton().vm.$emit('click');
await nextTick();
expect(importAllModalShowFn).toHaveBeenCalled();
});
it('triggers importAll action when modal is confirmed', async () => {
createComponent({ state: { providerRepos: [providerRepo] } });
findImportAllModal().vm.$emit('ok');
await nextTick();
expect(importAllFn).toHaveBeenCalled();
});
it('shows loading spinner when import is in progress', () => {
createComponent({ getters: { isImportingAnyRepo: () => true, importallCount: () => 1 } });
expect(findImportAllButton().props().loading).toBe(true);
});
it('renders filtering input field by default', () => {
createComponent();
expect(findFilterField().exists()).toBe(true);
});
it('does not render filtering input field when filterable is false', () => {
createComponent({ filterable: false });
expect(findFilterField()).toBeUndefined();
});
describe('when paginatable is set to true', () => {
const pageInfo = { page: 1 };
beforeEach(() => {
createComponent({
state: {
namespaces: [{ fullPath: 'path' }],
pageInfo,
repositories: [
{ importSource: { id: 1 }, importedProject: null, importStatus: STATUSES.NONE },
],
},
paginatable: true,
});
});
it('does not call fetchRepos on mount', () => {
expect(fetchReposFn).not.toHaveBeenCalled();
});
it('renders intersection observer component', () => {
expect(wrapper.find(GlIntersectionObserver).exists()).toBe(true);
});
it('calls fetchRepos when intersection observer appears', async () => {
wrapper.find(GlIntersectionObserver).vm.$emit('appear');
await nextTick();
expect(fetchReposFn).toHaveBeenCalled();
});
});
it('calls fetchRepos on mount', () => {
createComponent();
expect(fetchReposFn).toHaveBeenCalled();
});
it.each`
hasIncompatibleRepos | shouldRenderSlot | action
${false} | ${false} | ${'does not render'}
${true} | ${true} | ${'render'}
`(
'$action incompatible-repos-warning slot if hasIncompatibleRepos is $hasIncompatibleRepos',
({ hasIncompatibleRepos, shouldRenderSlot }) => {
const INCOMPATIBLE_TEXT = 'INCOMPATIBLE!';
createComponent({
getters: {
hasIncompatibleRepos: () => hasIncompatibleRepos,
},
slots: {
'incompatible-repos-warning': INCOMPATIBLE_TEXT,
},
});
expect(wrapper.text().includes(INCOMPATIBLE_TEXT)).toBe(shouldRenderSlot);
},
);
});