Commit 059a5b43 authored by Illya Klymov's avatar Illya Klymov Committed by Natalia Tepluhina

Refactor storage of import_status

Refactor store to maintain importStatus as part of importedProject
It reflects real state on backend side
parent 66fa2eff
...@@ -4,20 +4,15 @@ import { mapActions, mapState, mapGetters } from 'vuex'; ...@@ -4,20 +4,15 @@ import { mapActions, mapState, mapGetters } from 'vuex';
import { GlButton, GlLoadingIcon } from '@gitlab/ui'; import { GlButton, GlLoadingIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
import PaginationLinks from '~/vue_shared/components/pagination_links.vue'; import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
import ImportedProjectTableRow from './imported_project_table_row.vue';
import ProviderRepoTableRow from './provider_repo_table_row.vue'; import ProviderRepoTableRow from './provider_repo_table_row.vue';
import IncompatibleRepoTableRow from './incompatible_repo_table_row.vue';
import PageQueryParamSync from './page_query_param_sync.vue'; import PageQueryParamSync from './page_query_param_sync.vue';
import { isProjectImportable } from '../utils';
const reposFetchThrottleDelay = 1000; const reposFetchThrottleDelay = 1000;
export default { export default {
name: 'ImportProjectsTable', name: 'ImportProjectsTable',
components: { components: {
ImportedProjectTableRow,
ProviderRepoTableRow, ProviderRepoTableRow,
IncompatibleRepoTableRow,
PageQueryParamSync, PageQueryParamSync,
GlLoadingIcon, GlLoadingIcon,
GlButton, GlButton,
...@@ -109,8 +104,6 @@ export default { ...@@ -109,8 +104,6 @@ export default {
throttledFetchRepos: throttle(function fetch() { throttledFetchRepos: throttle(function fetch() {
this.fetchRepos(); this.fetchRepos();
}, reposFetchThrottleDelay), }, reposFetchThrottleDelay),
isProjectImportable,
}, },
}; };
</script> </script>
...@@ -165,18 +158,11 @@ export default { ...@@ -165,18 +158,11 @@ export default {
</thead> </thead>
<tbody> <tbody>
<template v-for="repo in repositories"> <template v-for="repo in repositories">
<incompatible-repo-table-row
v-if="repo.importSource.incompatible"
:key="repo.importSource.id"
:repo="repo"
/>
<provider-repo-table-row <provider-repo-table-row
v-else-if="isProjectImportable(repo)" :key="repo.importSource.providerLink"
:key="repo.importSource.id"
:repo="repo" :repo="repo"
:available-namespaces="availableNamespaces" :available-namespaces="availableNamespaces"
/> />
<imported-project-table-row v-else :key="repo.importSource.id" :project="repo" />
</template> </template>
</tbody> </tbody>
</table> </table>
......
<script>
import { GlIcon } from '@gitlab/ui';
import ImportStatus from './import_status.vue';
import { STATUSES } from '../constants';
export default {
name: 'ImportedProjectTableRow',
components: {
ImportStatus,
GlIcon,
},
props: {
project: {
type: Object,
required: true,
},
},
computed: {
displayFullPath() {
return this.project.importedProject.fullPath.replace(/^\//, '');
},
isFinished() {
return this.project.importStatus === STATUSES.FINISHED;
},
},
};
</script>
<template>
<tr class="import-row">
<td>
<a
:href="project.importSource.providerLink"
rel="noreferrer noopener"
target="_blank"
data-testid="providerLink"
>{{ project.importSource.fullName }}
<gl-icon v-if="project.importSource.providerLink" name="external-link" />
</a>
</td>
<td data-testid="fullPath">{{ displayFullPath }}</td>
<td>
<import-status :status="project.importStatus" />
</td>
<td>
<a
v-if="isFinished"
class="btn btn-default"
data-testid="goToProject"
:href="project.importedProject.fullPath"
rel="noreferrer noopener"
target="_blank"
>{{ __('Go to project') }}
</a>
</td>
</tr>
</template>
<script>
import { GlIcon, GlBadge } from '@gitlab/ui';
export default {
components: {
GlBadge,
GlIcon,
},
props: {
repo: {
type: Object,
required: true,
},
},
};
</script>
<template>
<tr class="import-row">
<td>
<a :href="repo.importSource.providerLink" rel="noreferrer noopener" target="_blank"
>{{ repo.importSource.fullName }}
<gl-icon v-if="repo.importSource.providerLink" name="external-link" />
</a>
</td>
<td></td>
<td></td>
<td>
<gl-badge variant="danger">{{ __('Incompatible project') }}</gl-badge>
</td>
</tr>
</template>
<script> <script>
import { mapState, mapGetters, mapActions } from 'vuex'; import { mapState, mapGetters, mapActions } from 'vuex';
import { GlIcon } from '@gitlab/ui'; import { GlIcon, GlBadge } from '@gitlab/ui';
import Select2Select from '~/vue_shared/components/select2_select.vue'; import Select2Select from '~/vue_shared/components/select2_select.vue';
import { __ } from '~/locale'; import { __ } from '~/locale';
import ImportStatus from './import_status.vue'; import ImportStatus from './import_status.vue';
import { STATUSES } from '../constants';
import { isProjectImportable, isIncompatible, getImportStatus } from '../utils';
export default { export default {
name: 'ProviderRepoTableRow', name: 'ProviderRepoTableRow',
...@@ -11,6 +13,7 @@ export default { ...@@ -11,6 +13,7 @@ export default {
Select2Select, Select2Select,
ImportStatus, ImportStatus,
GlIcon, GlIcon,
GlBadge,
}, },
props: { props: {
repo: { repo: {
...@@ -27,6 +30,26 @@ export default { ...@@ -27,6 +30,26 @@ export default {
...mapState(['ciCdOnly']), ...mapState(['ciCdOnly']),
...mapGetters(['getImportTarget']), ...mapGetters(['getImportTarget']),
displayFullPath() {
return this.repo.importedProject.fullPath.replace(/^\//, '');
},
isFinished() {
return this.repo.importedProject?.importStatus === STATUSES.FINISHED;
},
isImportNotStarted() {
return isProjectImportable(this.repo);
},
isIncompatible() {
return isIncompatible(this.repo);
},
importStatus() {
return getImportStatus(this.repo);
},
importTarget() { importTarget() {
return this.getImportTarget(this.repo.importSource.id); return this.getImportTarget(this.repo.importSource.id);
}, },
...@@ -85,9 +108,9 @@ export default { ...@@ -85,9 +108,9 @@ export default {
<gl-icon v-if="repo.importSource.providerLink" name="external-link" /> <gl-icon v-if="repo.importSource.providerLink" name="external-link" />
</a> </a>
</td> </td>
<td class="d-flex flex-wrap flex-lg-nowrap"> <td class="d-flex flex-wrap flex-lg-nowrap" data-testid="fullPath">
<template v-if="repo.target">{{ repo.target }}</template> <template v-if="repo.importSource.target">{{ repo.importSource.target }}</template>
<template v-else> <template v-else-if="isImportNotStarted">
<select2-select v-model="targetNamespaceSelect" :options="select2Options" /> <select2-select v-model="targetNamespaceSelect" :options="select2Options" />
<span class="px-2 import-slash-divider d-flex justify-content-center align-items-center" <span class="px-2 import-slash-divider d-flex justify-content-center align-items-center"
>/</span >/</span
...@@ -98,18 +121,31 @@ export default { ...@@ -98,18 +121,31 @@ export default {
class="form-control import-project-name-input qa-project-path-field" class="form-control import-project-name-input qa-project-path-field"
/> />
</template> </template>
<template v-else-if="repo.importedProject">{{ displayFullPath }}</template>
</td> </td>
<td> <td>
<import-status :status="repo.importStatus" /> <import-status :status="importStatus" />
</td> </td>
<td> <td data-testid="actions">
<a
v-if="isFinished"
class="btn btn-default"
:href="repo.importedProject.fullPath"
rel="noreferrer noopener"
target="_blank"
>{{ __('Go to project') }}
</a>
<button <button
v-if="isImportNotStarted"
type="button" type="button"
class="qa-import-button btn btn-default" class="qa-import-button btn btn-default"
@click="fetchImport(repo.importSource.id)" @click="fetchImport(repo.importSource.id)"
> >
{{ importButtonText }} {{ importButtonText }}
</button> </button>
<gl-badge v-else-if="isIncompatible" variant="danger">{{
__('Incompatible project')
}}</gl-badge>
</td> </td>
</tr> </tr>
</template> </template>
import { STATUSES } from '../constants'; import { STATUSES } from '../constants';
import { isProjectImportable, isIncompatible } from '../utils';
export const isLoading = state => state.isLoadingRepos || state.isLoadingNamespaces; export const isLoading = state => state.isLoadingRepos || state.isLoadingNamespaces;
export const isImportingAnyRepo = state => export const isImportingAnyRepo = state =>
state.repositories.some(repo => state.repositories.some(repo =>
[STATUSES.SCHEDULING, STATUSES.SCHEDULED, STATUSES.STARTED].includes(repo.importStatus), [STATUSES.SCHEDULING, STATUSES.SCHEDULED, STATUSES.STARTED].includes(
repo.importedProject?.importStatus,
),
); );
export const hasIncompatibleRepos = state => export const hasIncompatibleRepos = state => state.repositories.some(isIncompatible);
state.repositories.some(repo => repo.importSource.incompatible);
export const hasImportableRepos = state => export const hasImportableRepos = state => state.repositories.some(isProjectImportable);
state.repositories.some(repo => repo.importStatus === STATUSES.NONE);
export const getImportTarget = state => repoId => { export const getImportTarget = state => repoId => {
if (state.customImportTargets[repoId]) { if (state.customImportTargets[repoId]) {
......
...@@ -11,37 +11,36 @@ export default { ...@@ -11,37 +11,36 @@ export default {
state.isLoadingRepos = true; state.isLoadingRepos = true;
}, },
[types.RECEIVE_REPOS_SUCCESS]( [types.RECEIVE_REPOS_SUCCESS](state, repositories) {
state,
{ importedProjects, providerRepos, incompatibleRepos = [] },
) {
// Normalizing structure to support legacy backend format
// See https://gitlab.com/gitlab-org/gitlab/-/issues/27370#note_379034091 for details
state.isLoadingRepos = false; state.isLoadingRepos = false;
state.repositories = [ if (!Array.isArray(repositories)) {
...importedProjects.map(({ importSource, providerLink, importStatus, ...project }) => ({ // Legacy code path, will be removed when all importers will be switched to new pagination format
importSource: { // https://gitlab.com/gitlab-org/gitlab/-/issues/27370#note_379034091
id: `finished-${project.id}`, state.repositories = [
fullName: importSource, ...repositories.importedProjects.map(importedProject => ({
sanitizedName: project.name, importSource: {
providerLink, id: importedProject.id,
}, fullName: importedProject.importSource,
importStatus, sanitizedName: importedProject.name,
importedProject: project, providerLink: importedProject.providerLink,
})), },
...providerRepos.map(project => ({ importedProject,
importSource: project, })),
importStatus: STATUSES.NONE, ...repositories.providerRepos.map(project => ({
importedProject: null, importSource: project,
})), importedProject: null,
...incompatibleRepos.map(project => ({ })),
importSource: { ...project, incompatible: true }, ...(repositories.incompatibleRepos ?? []).map(project => ({
importStatus: STATUSES.NONE, importSource: { ...project, incompatible: true },
importedProject: null, importedProject: null,
})), })),
]; ];
return;
}
state.repositories = repositories;
}, },
[types.RECEIVE_REPOS_ERROR](state) { [types.RECEIVE_REPOS_ERROR](state) {
...@@ -50,31 +49,27 @@ export default { ...@@ -50,31 +49,27 @@ export default {
[types.REQUEST_IMPORT](state, { repoId, importTarget }) { [types.REQUEST_IMPORT](state, { repoId, importTarget }) {
const existingRepo = state.repositories.find(r => r.importSource.id === repoId); const existingRepo = state.repositories.find(r => r.importSource.id === repoId);
existingRepo.importStatus = STATUSES.SCHEDULING;
existingRepo.importedProject = { existingRepo.importedProject = {
importStatus: STATUSES.SCHEDULING,
fullPath: `/${importTarget.targetNamespace}/${importTarget.newName}`, fullPath: `/${importTarget.targetNamespace}/${importTarget.newName}`,
}; };
}, },
[types.RECEIVE_IMPORT_SUCCESS](state, { importedProject, repoId }) { [types.RECEIVE_IMPORT_SUCCESS](state, { importedProject, repoId }) {
const { importStatus, ...project } = importedProject;
const existingRepo = state.repositories.find(r => r.importSource.id === repoId); const existingRepo = state.repositories.find(r => r.importSource.id === repoId);
existingRepo.importStatus = importStatus; existingRepo.importedProject = importedProject;
existingRepo.importedProject = project;
}, },
[types.RECEIVE_IMPORT_ERROR](state, repoId) { [types.RECEIVE_IMPORT_ERROR](state, repoId) {
const existingRepo = state.repositories.find(r => r.importSource.id === repoId); const existingRepo = state.repositories.find(r => r.importSource.id === repoId);
existingRepo.importStatus = STATUSES.NONE;
existingRepo.importedProject = null; existingRepo.importedProject = null;
}, },
[types.RECEIVE_JOBS_SUCCESS](state, updatedProjects) { [types.RECEIVE_JOBS_SUCCESS](state, updatedProjects) {
updatedProjects.forEach(updatedProject => { updatedProjects.forEach(updatedProject => {
const repo = state.repositories.find(p => p.importedProject?.id === updatedProject.id); const repo = state.repositories.find(p => p.importedProject?.id === updatedProject.id);
if (repo) { if (repo?.importedProject) {
repo.importStatus = updatedProject.importStatus; repo.importedProject.importStatus = updatedProject.importStatus;
} }
}); });
}, },
......
import { STATUSES } from './constants'; import { STATUSES } from './constants';
// Will be expanded in future export function isIncompatible(project) {
return project.importSource.incompatible;
}
export function getImportStatus(project) {
return project.importedProject?.importStatus ?? STATUSES.NONE;
}
export function isProjectImportable(project) { export function isProjectImportable(project) {
return project.importStatus === STATUSES.NONE && !project.importSource.incompatible; return !isIncompatible(project) && getImportStatus(project) === STATUSES.NONE;
} }
...@@ -6,9 +6,7 @@ import state from '~/import_projects/store/state'; ...@@ -6,9 +6,7 @@ import state from '~/import_projects/store/state';
import * as getters from '~/import_projects/store/getters'; import * as getters from '~/import_projects/store/getters';
import { STATUSES } from '~/import_projects/constants'; import { STATUSES } from '~/import_projects/constants';
import ImportProjectsTable from '~/import_projects/components/import_projects_table.vue'; import ImportProjectsTable from '~/import_projects/components/import_projects_table.vue';
import ImportedProjectTableRow from '~/import_projects/components/imported_project_table_row.vue';
import ProviderRepoTableRow from '~/import_projects/components/provider_repo_table_row.vue'; import ProviderRepoTableRow from '~/import_projects/components/provider_repo_table_row.vue';
import IncompatibleRepoTableRow from '~/import_projects/components/incompatible_repo_table_row.vue';
import PageQueryParamSync from '~/import_projects/components/page_query_param_sync.vue'; import PageQueryParamSync from '~/import_projects/components/page_query_param_sync.vue';
describe('ImportProjectsTable', () => { describe('ImportProjectsTable', () => {
...@@ -88,20 +86,15 @@ describe('ImportProjectsTable', () => { ...@@ -88,20 +86,15 @@ describe('ImportProjectsTable', () => {
expect(wrapper.contains(GlLoadingIcon)).toBe(true); expect(wrapper.contains(GlLoadingIcon)).toBe(true);
}); });
it('renders a table with imported projects and provider repos', () => { 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({ createComponent({
state: { state: { namespaces: [{ fullPath: 'path' }], repositories },
namespaces: [{ fullPath: 'path' }],
repositories: [
{ importSource: { id: 1 }, importedProject: null, importStatus: STATUSES.NONE },
{ importSource: { id: 2 }, importedProject: {}, importStatus: STATUSES.FINISHED },
{
importSource: { id: 3, incompatible: true },
importedProject: {},
importStatus: STATUSES.NONE,
},
],
},
}); });
expect(wrapper.contains(GlLoadingIcon)).toBe(false); expect(wrapper.contains(GlLoadingIcon)).toBe(false);
...@@ -113,9 +106,7 @@ describe('ImportProjectsTable', () => { ...@@ -113,9 +106,7 @@ describe('ImportProjectsTable', () => {
.exists(), .exists(),
).toBe(true); ).toBe(true);
expect(wrapper.contains(ProviderRepoTableRow)).toBe(true); expect(wrapper.findAll(ProviderRepoTableRow)).toHaveLength(repositories.length);
expect(wrapper.contains(ImportedProjectTableRow)).toBe(true);
expect(wrapper.contains(IncompatibleRepoTableRow)).toBe(true);
}); });
it.each` it.each`
...@@ -142,7 +133,6 @@ describe('ImportProjectsTable', () => { ...@@ -142,7 +133,6 @@ describe('ImportProjectsTable', () => {
createComponent({ state: { repositories: [] } }); createComponent({ state: { repositories: [] } });
expect(wrapper.contains(ProviderRepoTableRow)).toBe(false); expect(wrapper.contains(ProviderRepoTableRow)).toBe(false);
expect(wrapper.contains(ImportedProjectTableRow)).toBe(false);
expect(wrapper.text()).toContain(`No ${providerTitle} repositories found`); expect(wrapper.text()).toContain(`No ${providerTitle} repositories found`);
}); });
......
import { mount } from '@vue/test-utils';
import ImportedProjectTableRow from '~/import_projects/components/imported_project_table_row.vue';
import ImportStatus from '~/import_projects/components/import_status.vue';
import { STATUSES } from '~/import_projects/constants';
describe('ImportedProjectTableRow', () => {
let wrapper;
const project = {
importSource: {
fullName: 'fullName',
providerLink: 'providerLink',
},
importedProject: {
id: 1,
fullPath: 'fullPath',
importSource: 'importSource',
},
importStatus: STATUSES.FINISHED,
};
function mountComponent() {
wrapper = mount(ImportedProjectTableRow, { propsData: { project } });
}
beforeEach(() => {
mountComponent();
});
afterEach(() => {
wrapper.destroy();
});
it('renders an imported project table row', () => {
const providerLink = wrapper.find('[data-testid=providerLink]');
expect(providerLink.attributes().href).toMatch(project.importSource.providerLink);
expect(providerLink.text()).toMatch(project.importSource.fullName);
expect(wrapper.find('[data-testid=fullPath]').text()).toMatch(project.importedProject.fullPath);
expect(wrapper.find(ImportStatus).props().status).toBe(project.importStatus);
expect(wrapper.find('[data-testid=goToProject').attributes().href).toMatch(
project.importedProject.fullPath,
);
});
});
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import { createLocalVue, shallowMount } from '@vue/test-utils'; import { createLocalVue, shallowMount } from '@vue/test-utils';
import { GlBadge } from '@gitlab/ui';
import ProviderRepoTableRow from '~/import_projects/components/provider_repo_table_row.vue'; import ProviderRepoTableRow from '~/import_projects/components/provider_repo_table_row.vue';
import ImportStatus from '~/import_projects/components/import_status.vue'; import ImportStatus from '~/import_projects/components/import_status.vue';
import { STATUSES } from '~/import_projects/constants'; import { STATUSES } from '~/import_projects/constants';
...@@ -14,20 +15,6 @@ describe('ProviderRepoTableRow', () => { ...@@ -14,20 +15,6 @@ describe('ProviderRepoTableRow', () => {
targetNamespace: 'target', targetNamespace: 'target',
newName: 'newName', newName: 'newName',
}; };
const ciCdOnly = false;
const repo = {
importSource: {
id: 'remote-1',
fullName: 'fullName',
providerLink: 'providerLink',
},
importedProject: {
id: 1,
fullPath: 'fullPath',
importSource: 'importSource',
},
importStatus: STATUSES.FINISHED,
};
const availableNamespaces = [ const availableNamespaces = [
{ text: 'Groups', children: [{ id: 'test', text: 'test' }] }, { text: 'Groups', children: [{ id: 'test', text: 'test' }] },
...@@ -46,55 +33,137 @@ describe('ProviderRepoTableRow', () => { ...@@ -46,55 +33,137 @@ describe('ProviderRepoTableRow', () => {
return store; return store;
} }
const findImportButton = () => const findImportButton = () => {
wrapper const buttons = wrapper.findAll('button').filter(node => node.text() === 'Import');
.findAll('button')
.filter(node => node.text() === 'Import') return buttons.length ? buttons.at(0) : buttons;
.at(0); };
function mountComponent(initialState) { function mountComponent(props) {
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(Vuex); localVue.use(Vuex);
const store = initStore({ ciCdOnly, ...initialState }); const store = initStore();
wrapper = shallowMount(ProviderRepoTableRow, { wrapper = shallowMount(ProviderRepoTableRow, {
localVue, localVue,
store, store,
propsData: { repo, availableNamespaces }, propsData: { availableNamespaces, ...props },
}); });
} }
beforeEach(() => {
mountComponent();
});
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null;
}); });
it('renders a provider repo table row', () => { describe('when rendering importable project', () => {
const providerLink = wrapper.find('[data-testid=providerLink]'); const repo = {
importSource: {
id: 'remote-1',
fullName: 'fullName',
providerLink: 'providerLink',
},
};
beforeEach(() => {
mountComponent({ repo });
});
it('renders project information', () => {
const providerLink = wrapper.find('[data-testid=providerLink]');
expect(providerLink.attributes().href).toMatch(repo.importSource.providerLink);
expect(providerLink.text()).toMatch(repo.importSource.fullName);
});
it('renders empty import status', () => {
expect(wrapper.find(ImportStatus).props().status).toBe(STATUSES.NONE);
});
it('renders a select2 namespace select', () => {
expect(wrapper.contains(Select2Select)).toBe(true);
expect(wrapper.find(Select2Select).props().options.data).toBe(availableNamespaces);
});
it('renders import button', () => {
expect(findImportButton().exists()).toBe(true);
});
it('imports repo when clicking import button', async () => {
findImportButton().trigger('click');
await nextTick();
const { calls } = fetchImport.mock;
expect(providerLink.attributes().href).toMatch(repo.importSource.providerLink); expect(calls).toHaveLength(1);
expect(providerLink.text()).toMatch(repo.importSource.fullName); expect(calls[0][1]).toBe(repo.importSource.id);
expect(wrapper.find(ImportStatus).props().status).toBe(repo.importStatus); });
expect(wrapper.contains('button')).toBe(true);
}); });
it('renders a select2 namespace select', () => { describe('when rendering imported project', () => {
expect(wrapper.contains(Select2Select)).toBe(true); const repo = {
expect(wrapper.find(Select2Select).props().options.data).toBe(availableNamespaces); importSource: {
id: 'remote-1',
fullName: 'fullName',
providerLink: 'providerLink',
},
importedProject: {
id: 1,
fullPath: 'fullPath',
importSource: 'importSource',
importStatus: STATUSES.FINISHED,
},
};
beforeEach(() => {
mountComponent({ repo });
});
it('renders project information', () => {
const providerLink = wrapper.find('[data-testid=providerLink]');
expect(providerLink.attributes().href).toMatch(repo.importSource.providerLink);
expect(providerLink.text()).toMatch(repo.importSource.fullName);
});
it('renders proper import status', () => {
expect(wrapper.find(ImportStatus).props().status).toBe(repo.importedProject.importStatus);
});
it('does not renders a namespace select', () => {
expect(wrapper.contains(Select2Select)).toBe(false);
});
it('does not render import button', () => {
expect(findImportButton().exists()).toBe(false);
});
}); });
it('imports repo when clicking import button', async () => { describe('when rendering incompatible project', () => {
findImportButton().trigger('click'); const repo = {
importSource: {
id: 'remote-1',
fullName: 'fullName',
providerLink: 'providerLink',
incompatible: true,
},
};
await nextTick(); beforeEach(() => {
mountComponent({ repo });
});
it('renders project information', () => {
const providerLink = wrapper.find('[data-testid=providerLink]');
const { calls } = fetchImport.mock; expect(providerLink.attributes().href).toMatch(repo.importSource.providerLink);
expect(providerLink.text()).toMatch(repo.importSource.fullName);
});
expect(calls).toHaveLength(1); it('renders badge with error', () => {
expect(calls[0][1]).toBe(repo.importSource.id); expect(wrapper.find(GlBadge).text()).toBe('Incompatible project');
});
}); });
}); });
...@@ -10,13 +10,12 @@ import state from '~/import_projects/store/state'; ...@@ -10,13 +10,12 @@ import state from '~/import_projects/store/state';
const IMPORTED_REPO = { const IMPORTED_REPO = {
importSource: {}, importSource: {},
importedProject: { fullPath: 'some/path' }, importedProject: { fullPath: 'some/path', importStatus: STATUSES.FINISHED },
}; };
const IMPORTABLE_REPO = { const IMPORTABLE_REPO = {
importSource: { id: 'some-id', sanitizedName: 'sanitized' }, importSource: { id: 'some-id', sanitizedName: 'sanitized' },
importedProject: null, importedProject: null,
importStatus: STATUSES.NONE,
}; };
const INCOMPATIBLE_REPO = { const INCOMPATIBLE_REPO = {
...@@ -56,14 +55,20 @@ describe('import_projects store getters', () => { ...@@ -56,14 +55,20 @@ describe('import_projects store getters', () => {
${STATUSES.STARTED} | ${true} ${STATUSES.STARTED} | ${true}
${STATUSES.FINISHED} | ${false} ${STATUSES.FINISHED} | ${false}
`( `(
'isImportingAnyRepo returns $value when repo with $importStatus status is available', 'isImportingAnyRepo returns $value when project with $importStatus status is available',
({ importStatus, value }) => { ({ importStatus, value }) => {
localState.repositories = [{ importStatus }]; localState.repositories = [{ importedProject: { importStatus } }];
expect(isImportingAnyRepo(localState)).toBe(value); expect(isImportingAnyRepo(localState)).toBe(value);
}, },
); );
it('isImportingAnyRepo returns false when project with no defined importStatus status is available', () => {
localState.repositories = [{ importSource: {} }];
expect(isImportingAnyRepo(localState)).toBe(false);
});
describe('hasIncompatibleRepos', () => { describe('hasIncompatibleRepos', () => {
it('returns true if there are any incompatible projects', () => { it('returns true if there are any incompatible projects', () => {
localState.repositories = [IMPORTABLE_REPO, IMPORTED_REPO, INCOMPATIBLE_REPO]; localState.repositories = [IMPORTABLE_REPO, IMPORTED_REPO, INCOMPATIBLE_REPO];
......
...@@ -40,93 +40,100 @@ describe('import_projects store mutations', () => { ...@@ -40,93 +40,100 @@ describe('import_projects store mutations', () => {
}); });
describe(`${types.RECEIVE_REPOS_SUCCESS}`, () => { describe(`${types.RECEIVE_REPOS_SUCCESS}`, () => {
describe('for imported projects', () => { describe('with legacy response format', () => {
const response = { describe('for imported projects', () => {
importedProjects: [IMPORTED_PROJECT], const response = {
providerRepos: [], importedProjects: [IMPORTED_PROJECT],
}; providerRepos: [],
};
it('picks import status from response', () => { it('recreates importSource from response', () => {
state = {}; state = {};
mutations[types.RECEIVE_REPOS_SUCCESS](state, response); mutations[types.RECEIVE_REPOS_SUCCESS](state, response);
expect(state.repositories[0].importStatus).toBe(IMPORTED_PROJECT.importStatus); expect(state.repositories[0].importSource).toStrictEqual(
}); expect.objectContaining({
fullName: IMPORTED_PROJECT.importSource,
sanitizedName: IMPORTED_PROJECT.name,
providerLink: IMPORTED_PROJECT.providerLink,
}),
);
});
it('recreates importSource from response', () => { it('passes project to importProject', () => {
state = {}; state = {};
mutations[types.RECEIVE_REPOS_SUCCESS](state, response); mutations[types.RECEIVE_REPOS_SUCCESS](state, response);
expect(state.repositories[0].importSource).toStrictEqual( expect(IMPORTED_PROJECT).toStrictEqual(
expect.objectContaining({ expect.objectContaining(state.repositories[0].importedProject),
fullName: IMPORTED_PROJECT.importSource, );
sanitizedName: IMPORTED_PROJECT.name, });
providerLink: IMPORTED_PROJECT.providerLink,
}),
);
}); });
it('passes project to importProject', () => { describe('for importable projects', () => {
state = {}; beforeEach(() => {
state = {};
mutations[types.RECEIVE_REPOS_SUCCESS](state, response); const response = {
importedProjects: [],
expect(IMPORTED_PROJECT).toStrictEqual( providerRepos: [SOURCE_PROJECT],
expect.objectContaining(state.repositories[0].importedProject), };
); mutations[types.RECEIVE_REPOS_SUCCESS](state, response);
});
it('sets importSource to project', () => {
expect(state.repositories[0].importSource).toBe(SOURCE_PROJECT);
});
}); });
});
describe('for importable projects', () => { describe('for incompatible projects', () => {
beforeEach(() => {
state = {};
const response = { const response = {
importedProjects: [], importedProjects: [],
providerRepos: [SOURCE_PROJECT], providerRepos: [],
incompatibleRepos: [SOURCE_PROJECT],
}; };
mutations[types.RECEIVE_REPOS_SUCCESS](state, response);
});
it('sets import status to none', () => { beforeEach(() => {
expect(state.repositories[0].importStatus).toBe(STATUSES.NONE); state = {};
}); mutations[types.RECEIVE_REPOS_SUCCESS](state, response);
});
it('sets importSource to project', () => { it('sets incompatible flag', () => {
expect(state.repositories[0].importSource).toBe(SOURCE_PROJECT); expect(state.repositories[0].importSource.incompatible).toBe(true);
}); });
});
describe('for incompatible projects', () => { it('sets importSource to project', () => {
const response = { expect(state.repositories[0].importSource).toStrictEqual(
importedProjects: [], expect.objectContaining(SOURCE_PROJECT),
providerRepos: [], );
incompatibleRepos: [SOURCE_PROJECT], });
}; });
beforeEach(() => { it('sets repos loading flag to false', () => {
const response = {
importedProjects: [],
providerRepos: [],
};
state = {}; state = {};
mutations[types.RECEIVE_REPOS_SUCCESS](state, response); mutations[types.RECEIVE_REPOS_SUCCESS](state, response);
});
it('sets incompatible flag', () => { expect(state.isLoadingRepos).toBe(false);
expect(state.repositories[0].importSource.incompatible).toBe(true);
}); });
});
it('sets importSource to project', () => { it('passes response as it is', () => {
expect(state.repositories[0].importSource).toStrictEqual( const response = [];
expect.objectContaining(SOURCE_PROJECT), state = {};
);
}); mutations[types.RECEIVE_REPOS_SUCCESS](state, response);
expect(state.repositories).toBe(response);
}); });
it('sets repos loading flag to false', () => { it('sets repos loading flag to false', () => {
const response = { const response = [];
importedProjects: [],
providerRepos: [],
};
state = {};
mutations[types.RECEIVE_REPOS_SUCCESS](state, response); mutations[types.RECEIVE_REPOS_SUCCESS](state, response);
...@@ -154,7 +161,7 @@ describe('import_projects store mutations', () => { ...@@ -154,7 +161,7 @@ describe('import_projects store mutations', () => {
}); });
it(`sets status to ${STATUSES.SCHEDULING}`, () => { it(`sets status to ${STATUSES.SCHEDULING}`, () => {
expect(state.repositories[0].importStatus).toBe(STATUSES.SCHEDULING); expect(state.repositories[0].importedProject.importStatus).toBe(STATUSES.SCHEDULING);
}); });
}); });
...@@ -170,7 +177,9 @@ describe('import_projects store mutations', () => { ...@@ -170,7 +177,9 @@ describe('import_projects store mutations', () => {
}); });
it('sets import status', () => { it('sets import status', () => {
expect(state.repositories[0].importStatus).toBe(IMPORTED_PROJECT.importStatus); expect(state.repositories[0].importedProject.importStatus).toBe(
IMPORTED_PROJECT.importStatus,
);
}); });
it('sets imported project', () => { it('sets imported project', () => {
...@@ -188,8 +197,8 @@ describe('import_projects store mutations', () => { ...@@ -188,8 +197,8 @@ describe('import_projects store mutations', () => {
mutations[types.RECEIVE_IMPORT_ERROR](state, REPO_ID); mutations[types.RECEIVE_IMPORT_ERROR](state, REPO_ID);
}); });
it(`resets import status to ${STATUSES.NONE}`, () => { it(`removes importedProject entry`, () => {
expect(state.repositories[0].importStatus).toBe(STATUSES.NONE); expect(state.repositories[0].importedProject).toBeNull();
}); });
}); });
...@@ -203,7 +212,9 @@ describe('import_projects store mutations', () => { ...@@ -203,7 +212,9 @@ describe('import_projects store mutations', () => {
mutations[types.RECEIVE_JOBS_SUCCESS](state, updatedProjects); mutations[types.RECEIVE_JOBS_SUCCESS](state, updatedProjects);
expect(state.repositories[0].importStatus).toBe(updatedProjects[0].importStatus); expect(state.repositories[0].importedProject.importStatus).toBe(
updatedProjects[0].importStatus,
);
}); });
}); });
......
import { isProjectImportable } from '~/import_projects/utils'; import { isProjectImportable, isIncompatible, getImportStatus } from '~/import_projects/utils';
import { STATUSES } from '~/import_projects/constants'; import { STATUSES } from '~/import_projects/constants';
describe('import_projects utils', () => { describe('import_projects utils', () => {
const COMPATIBLE_PROJECT = {
importSource: { incompatible: false },
};
const INCOMPATIBLE_PROJECT = {
importSource: { incompatible: true },
importedProject: null,
};
describe('isProjectImportable', () => { describe('isProjectImportable', () => {
it.each` it.each`
status | result status | result
...@@ -14,19 +23,43 @@ describe('import_projects utils', () => { ...@@ -14,19 +23,43 @@ describe('import_projects utils', () => {
`('returns $result when project is compatible and status is $status', ({ status, result }) => { `('returns $result when project is compatible and status is $status', ({ status, result }) => {
expect( expect(
isProjectImportable({ isProjectImportable({
importStatus: status, ...COMPATIBLE_PROJECT,
importSource: { incompatible: false }, importedProject: { importStatus: status },
}), }),
).toBe(result); ).toBe(result);
}); });
it('returns true if import status is not defined', () => {
expect(isProjectImportable({ importSource: {} })).toBe(true);
});
it('returns false if project is not compatible', () => { it('returns false if project is not compatible', () => {
expect(isProjectImportable(INCOMPATIBLE_PROJECT)).toBe(false);
});
});
describe('isIncompatible', () => {
it('returns true for incompatible project', () => {
expect(isIncompatible(INCOMPATIBLE_PROJECT)).toBe(true);
});
it('returns false for compatible project', () => {
expect(isIncompatible(COMPATIBLE_PROJECT)).toBe(false);
});
});
describe('getImportStatus', () => {
it('returns actual status when project status is provided', () => {
expect( expect(
isProjectImportable({ getImportStatus({
importStatus: STATUSES.NONE, ...COMPATIBLE_PROJECT,
importSource: { incompatible: true }, importedProject: { importStatus: STATUSES.FINISHED },
}), }),
).toBe(false); ).toBe(STATUSES.FINISHED);
});
it('returns NONE as status if import status is not provided', () => {
expect(getImportStatus(COMPATIBLE_PROJECT)).toBe(STATUSES.NONE);
}); });
}); });
}); });
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