diff options
Diffstat (limited to 'spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js')
-rw-r--r-- | spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js | 249 |
1 files changed, 249 insertions, 0 deletions
diff --git a/spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js b/spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js new file mode 100644 index 00000000000..b4ac11b4404 --- /dev/null +++ b/spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js @@ -0,0 +1,249 @@ +import { nextTick } from 'vue'; +import Vuex from 'vuex'; +import { createLocalVue, shallowMount } from '@vue/test-utils'; +import { GlLoadingIcon, GlButton, GlIntersectionObserver } from '@gitlab/ui'; +import state from '~/import_entities/import_projects/store/state'; +import * as getters from '~/import_entities/import_projects/store/getters'; +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'; + +describe('ImportProjectsTable', () => { + let wrapper; + + const findFilterField = () => + wrapper.find('input[data-qa-selector="githubish_import_filter_field"]'); + + 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 === 'success') + .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, + } = {}) { + const localVue = createLocalVue(); + localVue.use(Vuex); + + const store = new Vuex.Store({ + state: { ...state(), ...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, { + localVue, + store, + propsData: { + providerTitle, + filterable, + paginatable, + }, + slots, + stubs: { + GlModal: { template: '<div>Modal!</div>', 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('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 } }); + + 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().exists()).toBe(false); + }); + + 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); + }, + ); +}); |