From 859a6fb938bb9ee2a317c46dfa4fcc1af49608f0 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Thu, 18 Feb 2021 10:34:06 +0000 Subject: Add latest changes from gitlab-org/gitlab@13-9-stable-ee --- .../components/import_table_row_spec.js | 6 +- .../import_groups/components/import_table_spec.js | 167 ++++++++++++- .../import_groups/graphql/client_factory_spec.js | 137 ++++++---- .../graphql/services/source_groups_manager_spec.js | 4 +- .../graphql/services/status_poller_spec.js | 276 +++++++-------------- .../components/bitbucket_status_table_spec.js | 4 +- .../components/import_projects_table_spec.js | 14 +- .../components/provider_repo_table_row_spec.js | 12 +- .../import_projects/store/actions_spec.js | 12 +- .../import_projects/store/getters_spec.js | 2 +- .../import_projects/store/mutations_spec.js | 2 +- .../import_entities/import_projects/utils_spec.js | 2 +- 12 files changed, 356 insertions(+), 282 deletions(-) (limited to 'spec/frontend/import_entities') diff --git a/spec/frontend/import_entities/import_groups/components/import_table_row_spec.js b/spec/frontend/import_entities/import_groups/components/import_table_row_spec.js index ac8b73aeb49..cdef4b1ee62 100644 --- a/spec/frontend/import_entities/import_groups/components/import_table_row_spec.js +++ b/spec/frontend/import_entities/import_groups/components/import_table_row_spec.js @@ -1,8 +1,8 @@ -import { shallowMount } from '@vue/test-utils'; import { GlButton, GlLink, GlFormInput } from '@gitlab/ui'; -import Select2Select from '~/vue_shared/components/select2_select.vue'; -import ImportTableRow from '~/import_entities/import_groups/components/import_table_row.vue'; +import { shallowMount } from '@vue/test-utils'; import { STATUSES } from '~/import_entities/constants'; +import ImportTableRow from '~/import_entities/import_groups/components/import_table_row.vue'; +import Select2Select from '~/vue_shared/components/select2_select.vue'; import { availableNamespacesFixture } from '../graphql/fixtures'; const getFakeGroup = (status) => ({ diff --git a/spec/frontend/import_entities/import_groups/components/import_table_spec.js b/spec/frontend/import_entities/import_groups/components/import_table_spec.js index cd184bb65cc..dd734782169 100644 --- a/spec/frontend/import_entities/import_groups/components/import_table_spec.js +++ b/spec/frontend/import_entities/import_groups/components/import_table_spec.js @@ -1,15 +1,15 @@ +import { GlEmptyState, GlLoadingIcon, GlSearchBoxByClick, GlSprintf } from '@gitlab/ui'; import { shallowMount, createLocalVue } from '@vue/test-utils'; import VueApollo from 'vue-apollo'; -import { GlLoadingIcon } from '@gitlab/ui'; -import waitForPromises from 'helpers/wait_for_promises'; import createMockApollo from 'helpers/mock_apollo_helper'; -import ImportTableRow from '~/import_entities/import_groups/components/import_table_row.vue'; +import waitForPromises from 'helpers/wait_for_promises'; +import { STATUSES } from '~/import_entities/constants'; import ImportTable from '~/import_entities/import_groups/components/import_table.vue'; -import setTargetNamespaceMutation from '~/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql'; -import setNewNameMutation from '~/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql'; +import ImportTableRow from '~/import_entities/import_groups/components/import_table_row.vue'; import importGroupMutation from '~/import_entities/import_groups/graphql/mutations/import_group.mutation.graphql'; - -import { STATUSES } from '~/import_entities/constants'; +import setNewNameMutation from '~/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql'; +import setTargetNamespaceMutation from '~/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql'; +import PaginationLinks from '~/vue_shared/components/pagination_links.vue'; import { availableNamespacesFixture, generateFakeEntry } from '../graphql/fixtures'; @@ -20,6 +20,9 @@ describe('import table', () => { let wrapper; let apolloProvider; + const FAKE_GROUP = generateFakeEntry({ id: 1, status: STATUSES.NONE }); + const FAKE_PAGE_INFO = { page: 1, perPage: 20, total: 40, totalPages: 2 }; + const createComponent = ({ bulkImportSourceGroups }) => { apolloProvider = createMockApollo([], { Query: { @@ -34,6 +37,12 @@ describe('import table', () => { }); wrapper = shallowMount(ImportTable, { + propsData: { + sourceUrl: 'https://demo.host', + }, + stubs: { + GlSprintf, + }, localVue, apolloProvider, }); @@ -62,25 +71,50 @@ describe('import table', () => { expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); }); + it('renders message about empty state when no groups are available for import', async () => { + createComponent({ + bulkImportSourceGroups: () => ({ + nodes: [], + pageInfo: FAKE_PAGE_INFO, + }), + }); + await waitForPromises(); + + expect(wrapper.find(GlEmptyState).props().title).toBe('No groups available for import'); + }); + it('renders import row for each group in response', async () => { const FAKE_GROUPS = [ generateFakeEntry({ id: 1, status: STATUSES.NONE }), generateFakeEntry({ id: 2, status: STATUSES.FINISHED }), ]; createComponent({ - bulkImportSourceGroups: () => FAKE_GROUPS, + bulkImportSourceGroups: () => ({ + nodes: FAKE_GROUPS, + pageInfo: FAKE_PAGE_INFO, + }), }); await waitForPromises(); expect(wrapper.findAll(ImportTableRow)).toHaveLength(FAKE_GROUPS.length); }); - describe('converts row events to mutation invocations', () => { - const FAKE_GROUP = generateFakeEntry({ id: 1, status: STATUSES.NONE }); + it('does not render status string when result list is empty', async () => { + createComponent({ + bulkImportSourceGroups: jest.fn().mockResolvedValue({ + nodes: [], + pageInfo: FAKE_PAGE_INFO, + }), + }); + await waitForPromises(); + + expect(wrapper.text()).not.toContain('Showing 1-0'); + }); + describe('converts row events to mutation invocations', () => { beforeEach(() => { createComponent({ - bulkImportSourceGroups: () => [FAKE_GROUP], + bulkImportSourceGroups: () => ({ nodes: [FAKE_GROUP], pageInfo: FAKE_PAGE_INFO }), }); return waitForPromises(); }); @@ -100,4 +134,115 @@ describe('import table', () => { }); }); }); + + describe('pagination', () => { + const bulkImportSourceGroupsQueryMock = jest + .fn() + .mockResolvedValue({ nodes: [FAKE_GROUP], pageInfo: FAKE_PAGE_INFO }); + + beforeEach(() => { + createComponent({ + bulkImportSourceGroups: bulkImportSourceGroupsQueryMock, + }); + return waitForPromises(); + }); + + it('correctly passes pagination info from query', () => { + expect(wrapper.find(PaginationLinks).props().pageInfo).toStrictEqual(FAKE_PAGE_INFO); + }); + + 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, + }, + }); + wrapper.find(PaginationLinks).props().change(REQUESTED_PAGE); + await waitForPromises(); + + expect(wrapper.text()).toContain('Showing 21-21 of 38'); + }); + }); + + describe('filters', () => { + const bulkImportSourceGroupsQueryMock = jest + .fn() + .mockResolvedValue({ nodes: [FAKE_GROUP], pageInfo: FAKE_PAGE_INFO }); + + beforeEach(() => { + createComponent({ + bulkImportSourceGroups: bulkImportSourceGroupsQueryMock, + }); + return waitForPromises(); + }); + + const findFilterInput = () => wrapper.find(GlSearchBoxByClick); + + it('properly passes filter to graphql query when search box is submitted', async () => { + createComponent({ + bulkImportSourceGroups: bulkImportSourceGroupsQueryMock, + }); + await waitForPromises(); + + const FILTER_VALUE = 'foo'; + findFilterInput().vm.$emit('submit', 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'; + findFilterInput().vm.$emit('submit', FILTER_VALUE); + await waitForPromises(); + + expect(wrapper.text()).toContain('Showing 1-1 of 40 groups matching filter "foo"'); + }); + + it('properly resets filter in graphql query when search box is cleared', async () => { + const FILTER_VALUE = 'foo'; + findFilterInput().vm.$emit('submit', FILTER_VALUE); + await waitForPromises(); + + bulkImportSourceGroupsQueryMock.mockClear(); + await apolloProvider.defaultClient.resetStore(); + findFilterInput().vm.$emit('clear'); + await waitForPromises(); + + expect(bulkImportSourceGroupsQueryMock).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ filter: '' }), + expect.anything(), + expect.anything(), + ); + }); + }); }); diff --git a/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js b/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js index 514ed411138..4d3d2c41bbe 100644 --- a/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js +++ b/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js @@ -1,20 +1,20 @@ -import MockAdapter from 'axios-mock-adapter'; import { InMemoryCache } from 'apollo-cache-inmemory'; +import MockAdapter from 'axios-mock-adapter'; import { createMockClient } from 'mock-apollo-client'; import waitForPromises from 'helpers/wait_for_promises'; -import axios from '~/lib/utils/axios_utils'; +import { STATUSES } from '~/import_entities/constants'; import { clientTypenames, createResolvers, } from '~/import_entities/import_groups/graphql/client_factory'; +import importGroupMutation from '~/import_entities/import_groups/graphql/mutations/import_group.mutation.graphql'; +import setNewNameMutation from '~/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql'; +import setTargetNamespaceMutation from '~/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql'; +import availableNamespacesQuery from '~/import_entities/import_groups/graphql/queries/available_namespaces.query.graphql'; +import bulkImportSourceGroupsQuery from '~/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql'; import { StatusPoller } from '~/import_entities/import_groups/graphql/services/status_poller'; -import { STATUSES } from '~/import_entities/constants'; -import bulkImportSourceGroupsQuery from '~/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql'; -import availableNamespacesQuery from '~/import_entities/import_groups/graphql/queries/available_namespaces.query.graphql'; -import setTargetNamespaceMutation from '~/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql'; -import setNewNameMutation from '~/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql'; -import importGroupMutation from '~/import_entities/import_groups/graphql/mutations/import_group.mutation.graphql'; +import axios from '~/lib/utils/axios_utils'; import httpStatus from '~/lib/utils/http_status'; import { statusEndpointFixture, availableNamespacesFixture } from './fixtures'; @@ -28,6 +28,7 @@ const FAKE_ENDPOINTS = { status: '/fake_status_url', availableNamespaces: '/fake_available_namespaces', createBulkImport: '/fake_create_bulk_import', + jobs: '/fake_jobs', }; describe('Bulk import resolvers', () => { @@ -79,33 +80,61 @@ describe('Bulk import resolvers', () => { axiosMockAdapter .onGet(FAKE_ENDPOINTS.availableNamespaces) .reply(httpStatus.OK, availableNamespacesFixture); - - const response = await client.query({ query: bulkImportSourceGroupsQuery }); - results = response.data.bulkImportSourceGroups; }); - it('mirrors REST endpoint response fields', () => { - const MIRRORED_FIELDS = ['id', 'full_name', 'full_path', 'web_url']; - expect( - results.every((r, idx) => - MIRRORED_FIELDS.every( - (field) => r[field] === statusEndpointFixture.importable_data[idx][field], + describe('when called', () => { + beforeEach(async () => { + const response = await client.query({ query: bulkImportSourceGroupsQuery }); + results = response.data.bulkImportSourceGroups.nodes; + }); + + it('mirrors REST endpoint response fields', () => { + const MIRRORED_FIELDS = ['id', 'full_name', 'full_path', 'web_url']; + expect( + results.every((r, idx) => + MIRRORED_FIELDS.every( + (field) => r[field] === statusEndpointFixture.importable_data[idx][field], + ), ), - ), - ).toBe(true); - }); + ).toBe(true); + }); - it('populates each result instance with status field default to none', () => { - expect(results.every((r) => r.status === STATUSES.NONE)).toBe(true); - }); + it('populates each result instance with status field default to none', () => { + expect(results.every((r) => r.status === STATUSES.NONE)).toBe(true); + }); + + it('populates each result instance with import_target defaulted to first available namespace', () => { + expect( + results.every( + (r) => r.import_target.target_namespace === availableNamespacesFixture[0].full_path, + ), + ).toBe(true); + }); - it('populates each result instance with import_target defaulted to first available namespace', () => { - expect( - results.every( - (r) => r.import_target.target_namespace === availableNamespacesFixture[0].full_path, - ), - ).toBe(true); + it('starts polling when request completes', async () => { + const [statusPoller] = StatusPoller.mock.instances; + expect(statusPoller.startPolling).toHaveBeenCalled(); + }); }); + + it.each` + variable | queryParam | value + ${'filter'} | ${'filter'} | ${'demo'} + ${'perPage'} | ${'per_page'} | ${30} + ${'page'} | ${'page'} | ${3} + `( + 'properly passes GraphQL variable $variable as REST $queryParam query parameter', + async ({ variable, queryParam, value }) => { + await client.query({ + query: bulkImportSourceGroupsQuery, + variables: { [variable]: value }, + }); + const restCall = axiosMockAdapter.history.get.find( + (q) => q.url === FAKE_ENDPOINTS.status, + ); + expect(restCall.params[queryParam]).toBe(value); + }, + ); }); }); @@ -117,20 +146,28 @@ describe('Bulk import resolvers', () => { client.writeQuery({ query: bulkImportSourceGroupsQuery, data: { - bulkImportSourceGroups: [ - { - __typename: clientTypenames.BulkImportSourceGroup, - id: GROUP_ID, - status: STATUSES.NONE, - web_url: 'https://fake.host/1', - full_path: 'fake_group_1', - full_name: 'fake_name_1', - import_target: { - target_namespace: 'root', - new_name: 'group1', + bulkImportSourceGroups: { + nodes: [ + { + __typename: clientTypenames.BulkImportSourceGroup, + id: GROUP_ID, + status: STATUSES.NONE, + web_url: 'https://fake.host/1', + full_path: 'fake_group_1', + full_name: 'fake_name_1', + import_target: { + target_namespace: 'root', + new_name: 'group1', + }, }, + ], + pageInfo: { + page: 1, + perPage: 20, + total: 37, + totalPages: 2, }, - ], + }, }, }); @@ -140,7 +177,7 @@ describe('Bulk import resolvers', () => { fetchPolicy: 'cache-only', }) .subscribe(({ data }) => { - results = data.bulkImportSourceGroups; + results = data.bulkImportSourceGroups.nodes; }); }); @@ -174,7 +211,9 @@ describe('Bulk import resolvers', () => { }); await waitForPromises(); - const { bulkImportSourceGroups: intermediateResults } = client.readQuery({ + const { + bulkImportSourceGroups: { nodes: intermediateResults }, + } = client.readQuery({ query: bulkImportSourceGroupsQuery, }); @@ -182,7 +221,7 @@ describe('Bulk import resolvers', () => { }); it('sets group status to STARTED when request completes', async () => { - axiosMockAdapter.onPost(FAKE_ENDPOINTS.createBulkImport).reply(httpStatus.OK); + axiosMockAdapter.onPost(FAKE_ENDPOINTS.createBulkImport).reply(httpStatus.OK, { id: 1 }); await client.mutate({ mutation: importGroupMutation, variables: { sourceGroupId: GROUP_ID }, @@ -191,16 +230,6 @@ describe('Bulk import resolvers', () => { expect(results[0].status).toBe(STATUSES.STARTED); }); - it('starts polling when request completes', async () => { - axiosMockAdapter.onPost(FAKE_ENDPOINTS.createBulkImport).reply(httpStatus.OK); - await client.mutate({ - mutation: importGroupMutation, - variables: { sourceGroupId: GROUP_ID }, - }); - const [statusPoller] = StatusPoller.mock.instances; - expect(statusPoller.startPolling).toHaveBeenCalled(); - }); - it('resets status to NONE if request fails', async () => { axiosMockAdapter .onPost(FAKE_ENDPOINTS.createBulkImport) diff --git a/spec/frontend/import_entities/import_groups/graphql/services/source_groups_manager_spec.js b/spec/frontend/import_entities/import_groups/graphql/services/source_groups_manager_spec.js index 5940ea544ea..ca987ab3ab4 100644 --- a/spec/frontend/import_entities/import_groups/graphql/services/source_groups_manager_spec.js +++ b/spec/frontend/import_entities/import_groups/graphql/services/source_groups_manager_spec.js @@ -1,7 +1,7 @@ import { defaultDataIdFromObject } from 'apollo-cache-inmemory'; -import { SourceGroupsManager } from '~/import_entities/import_groups/graphql/services/source_groups_manager'; -import ImportSourceGroupFragment from '~/import_entities/import_groups/graphql/fragments/bulk_import_source_group_item.fragment.graphql'; import { clientTypenames } from '~/import_entities/import_groups/graphql/client_factory'; +import ImportSourceGroupFragment from '~/import_entities/import_groups/graphql/fragments/bulk_import_source_group_item.fragment.graphql'; +import { SourceGroupsManager } from '~/import_entities/import_groups/graphql/services/source_groups_manager'; describe('SourceGroupsManager', () => { let manager; diff --git a/spec/frontend/import_entities/import_groups/graphql/services/status_poller_spec.js b/spec/frontend/import_entities/import_groups/graphql/services/status_poller_spec.js index e7f1626f81d..a5fc4e18a02 100644 --- a/spec/frontend/import_entities/import_groups/graphql/services/status_poller_spec.js +++ b/spec/frontend/import_entities/import_groups/graphql/services/status_poller_spec.js @@ -1,215 +1,113 @@ -import { createMockClient } from 'mock-apollo-client'; -import { InMemoryCache } from 'apollo-cache-inmemory'; -import waitForPromises from 'helpers/wait_for_promises'; - +import MockAdapter from 'axios-mock-adapter'; +import Visibility from 'visibilityjs'; import createFlash from '~/flash'; -import { StatusPoller } from '~/import_entities/import_groups/graphql/services/status_poller'; -import bulkImportSourceGroupsQuery from '~/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql'; import { STATUSES } from '~/import_entities/constants'; import { SourceGroupsManager } from '~/import_entities/import_groups/graphql/services/source_groups_manager'; -import { generateFakeEntry } from '../fixtures'; +import { StatusPoller } from '~/import_entities/import_groups/graphql/services/status_poller'; +import axios from '~/lib/utils/axios_utils'; +import Poll from '~/lib/utils/poll'; +jest.mock('visibilityjs'); jest.mock('~/flash'); +jest.mock('~/lib/utils/poll'); jest.mock('~/import_entities/import_groups/graphql/services/source_groups_manager', () => ({ SourceGroupsManager: jest.fn().mockImplementation(function mock() { this.setImportStatus = jest.fn(); + this.findByImportId = jest.fn(); }), })); -const TEST_POLL_INTERVAL = 1000; +const FAKE_POLL_PATH = '/fake/poll/path'; +const CLIENT_MOCK = {}; describe('Bulk import status poller', () => { let poller; - let clientMock; + let mockAdapter; - const listQueryCacheCalls = () => - clientMock.readQuery.mock.calls.filter((call) => call[0].query === bulkImportSourceGroupsQuery); + const getPollHistory = () => mockAdapter.history.get.filter((x) => x.url === FAKE_POLL_PATH); beforeEach(() => { - clientMock = createMockClient({ - cache: new InMemoryCache({ - fragmentMatcher: { match: () => true }, - }), - }); - - jest.spyOn(clientMock, 'readQuery'); - - poller = new StatusPoller({ - client: clientMock, - interval: TEST_POLL_INTERVAL, - }); + mockAdapter = new MockAdapter(axios); + mockAdapter.onGet(FAKE_POLL_PATH).reply(200, {}); + poller = new StatusPoller({ client: CLIENT_MOCK, pollPath: FAKE_POLL_PATH }); }); - describe('general behavior', () => { - beforeEach(() => { - clientMock.cache.writeQuery({ - query: bulkImportSourceGroupsQuery, - data: { bulkImportSourceGroups: [] }, - }); - }); - - it('does not perform polling when constructed', () => { - jest.runOnlyPendingTimers(); - expect(listQueryCacheCalls()).toHaveLength(0); - }); - - it('immediately start polling when requested', async () => { - await poller.startPolling(); - expect(listQueryCacheCalls()).toHaveLength(1); - }); - - it('constantly polls when started', async () => { - poller.startPolling(); - expect(listQueryCacheCalls()).toHaveLength(1); - - jest.advanceTimersByTime(TEST_POLL_INTERVAL); - expect(listQueryCacheCalls()).toHaveLength(2); - - jest.advanceTimersByTime(TEST_POLL_INTERVAL); - expect(listQueryCacheCalls()).toHaveLength(3); - }); - - it('does not start polling when requested multiple times', async () => { - poller.startPolling(); - expect(listQueryCacheCalls()).toHaveLength(1); - - poller.startPolling(); - expect(listQueryCacheCalls()).toHaveLength(1); - }); - - it('stops polling when requested', async () => { - poller.startPolling(); - expect(listQueryCacheCalls()).toHaveLength(1); - - poller.stopPolling(); - jest.runOnlyPendingTimers(); - expect(listQueryCacheCalls()).toHaveLength(1); - }); - - it('does not query server when list is empty', async () => { - jest.spyOn(clientMock, 'query'); - poller.startPolling(); - expect(clientMock.query).not.toHaveBeenCalled(); - }); + it('creates source group manager with proper client', () => { + expect(SourceGroupsManager.mock.calls).toHaveLength(1); + const [[{ client }]] = SourceGroupsManager.mock.calls; + expect(client).toBe(CLIENT_MOCK); }); - it('does not query server when no groups have STARTED status', async () => { - clientMock.cache.writeQuery({ - query: bulkImportSourceGroupsQuery, - data: { - bulkImportSourceGroups: [STATUSES.NONE, STATUSES.FINISHED].map((status, idx) => - generateFakeEntry({ status, id: idx }), - ), - }, - }); - - jest.spyOn(clientMock, 'query'); + it('creates poller with proper config', () => { + expect(Poll.mock.calls).toHaveLength(1); + const [[pollConfig]] = Poll.mock.calls; + expect(typeof pollConfig.method).toBe('string'); + + const pollOperation = pollConfig.resource[pollConfig.method]; + expect(typeof pollOperation).toBe('function'); + }); + + it('invokes axios when polling is performed', async () => { + const [[pollConfig]] = Poll.mock.calls; + const pollOperation = pollConfig.resource[pollConfig.method]; + expect(getPollHistory()).toHaveLength(0); + + pollOperation(); + await axios.waitForAll(); + + expect(getPollHistory()).toHaveLength(1); + }); + + it('subscribes to visibility changes', () => { + expect(Visibility.change).toHaveBeenCalled(); + }); + + it.each` + isHidden | action + ${true} | ${'stop'} + ${false} | ${'restart'} + `('$action polling when hidden is $isHidden', ({ action, isHidden }) => { + const [pollInstance] = Poll.mock.instances; + const [[changeHandler]] = Visibility.change.mock.calls; + Visibility.hidden.mockReturnValue(isHidden); + expect(pollInstance[action]).not.toHaveBeenCalled(); + + changeHandler(); + + expect(pollInstance[action]).toHaveBeenCalled(); + }); + + it('does not perform polling when constructed', async () => { + await axios.waitForAll(); + + expect(getPollHistory()).toHaveLength(0); + }); + + it('immediately start polling when requested', async () => { + const [pollInstance] = Poll.mock.instances; + poller.startPolling(); - expect(clientMock.query).not.toHaveBeenCalled(); + + expect(pollInstance.makeRequest).toHaveBeenCalled(); + }); + + it('when error occurs shows flash with error', () => { + const [[pollConfig]] = Poll.mock.calls; + pollConfig.errorCallback(); + expect(createFlash).toHaveBeenCalled(); }); - describe('when there are groups which have STARTED status', () => { - const TARGET_NAMESPACE = 'root'; - - const STARTED_GROUP_1 = { - status: STATUSES.STARTED, - id: 'started1', - import_target: { - target_namespace: TARGET_NAMESPACE, - new_name: 'group1', - }, - }; - - const STARTED_GROUP_2 = { - status: STATUSES.STARTED, - id: 'started2', - import_target: { - target_namespace: TARGET_NAMESPACE, - new_name: 'group2', - }, - }; - - const NOT_STARTED_GROUP = { - status: STATUSES.NONE, - id: 'not_started', - import_target: { - target_namespace: TARGET_NAMESPACE, - new_name: 'group3', - }, - }; - - it('query server only for groups with STATUSES.STARTED', async () => { - clientMock.cache.writeQuery({ - query: bulkImportSourceGroupsQuery, - data: { - bulkImportSourceGroups: [ - STARTED_GROUP_1, - NOT_STARTED_GROUP, - STARTED_GROUP_2, - ].map((group) => generateFakeEntry(group)), - }, - }); - - clientMock.query = jest.fn().mockResolvedValue({ data: {} }); - poller.startPolling(); - - expect(clientMock.query).toHaveBeenCalledTimes(1); - await waitForPromises(); - const [[doc]] = clientMock.query.mock.calls; - const { selections } = doc.query.definitions[0].selectionSet; - expect(selections.every((field) => field.name.value === 'group')).toBeTruthy(); - expect(selections).toHaveLength(2); - expect(selections.map((sel) => sel.arguments[0].value.value)).toStrictEqual([ - `${TARGET_NAMESPACE}/${STARTED_GROUP_1.import_target.new_name}`, - `${TARGET_NAMESPACE}/${STARTED_GROUP_2.import_target.new_name}`, - ]); - }); - - it('updates statuses only for groups in response', async () => { - clientMock.cache.writeQuery({ - query: bulkImportSourceGroupsQuery, - data: { - bulkImportSourceGroups: [STARTED_GROUP_1, STARTED_GROUP_2].map((group) => - generateFakeEntry(group), - ), - }, - }); - - clientMock.query = jest.fn().mockResolvedValue({ data: { group0: {} } }); - poller.startPolling(); - await waitForPromises(); - const [managerInstance] = SourceGroupsManager.mock.instances; - expect(managerInstance.setImportStatus).toHaveBeenCalledTimes(1); - expect(managerInstance.setImportStatus).toHaveBeenCalledWith( - expect.objectContaining({ id: STARTED_GROUP_1.id }), - STATUSES.FINISHED, - ); - }); - - describe('when error occurs', () => { - beforeEach(() => { - clientMock.cache.writeQuery({ - query: bulkImportSourceGroupsQuery, - data: { - bulkImportSourceGroups: [STARTED_GROUP_1, STARTED_GROUP_2].map((group) => - generateFakeEntry(group), - ), - }, - }); - - clientMock.query = jest.fn().mockRejectedValue(new Error('dummy error')); - poller.startPolling(); - return waitForPromises(); - }); - - it('reports an error', () => { - expect(createFlash).toHaveBeenCalled(); - }); - - it('continues polling', async () => { - jest.advanceTimersByTime(TEST_POLL_INTERVAL); - expect(listQueryCacheCalls()).toHaveLength(2); - }); - }); + it('when success response arrives updates relevant group status', () => { + const FAKE_ID = 5; + const [[pollConfig]] = Poll.mock.calls; + const [managerInstance] = SourceGroupsManager.mock.instances; + managerInstance.findByImportId.mockReturnValue({ id: FAKE_ID }); + + pollConfig.successCallback({ data: [{ id: FAKE_ID, status_name: STATUSES.FINISHED }] }); + + expect(managerInstance.setImportStatus).toHaveBeenCalledWith( + expect.objectContaining({ id: FAKE_ID }), + STATUSES.FINISHED, + ); }); }); diff --git a/spec/frontend/import_entities/import_projects/components/bitbucket_status_table_spec.js b/spec/frontend/import_entities/import_projects/components/bitbucket_status_table_spec.js index 8f8c01a8b81..ea88c361f7b 100644 --- a/spec/frontend/import_entities/import_projects/components/bitbucket_status_table_spec.js +++ b/spec/frontend/import_entities/import_projects/components/bitbucket_status_table_spec.js @@ -1,7 +1,7 @@ -import { nextTick } from 'vue'; +import { GlAlert } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; -import { GlAlert } from '@gitlab/ui'; import BitbucketStatusTable from '~/import_entities/import_projects/components/bitbucket_status_table.vue'; import ImportProjectsTable from '~/import_entities/import_projects/components/import_projects_table.vue'; 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 index 27f642d15c8..d9f4168f1a5 100644 --- 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 @@ -1,18 +1,20 @@ +import { GlLoadingIcon, GlButton, GlIntersectionObserver, GlFormInput } from '@gitlab/ui'; +import { createLocalVue, shallowMount } from '@vue/test-utils'; 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'; +import * as getters from '~/import_entities/import_projects/store/getters'; +import state from '~/import_entities/import_projects/store/state'; describe('ImportProjectsTable', () => { let wrapper; const findFilterField = () => - wrapper.find('input[data-qa-selector="githubish_import_filter_field"]'); + wrapper + .findAllComponents(GlFormInput) + .wrappers.find((w) => w.attributes('placeholder') === 'Filter your repositories by name'); const providerTitle = 'THE PROVIDER'; const providerRepo = { @@ -205,7 +207,7 @@ describe('ImportProjectsTable', () => { it('does not render filtering input field when filterable is false', () => { createComponent({ filterable: false }); - expect(findFilterField().exists()).toBe(false); + expect(findFilterField()).toBeUndefined(); }); describe('when paginatable is set to true', () => { diff --git a/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js b/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js index 2ed11ae277e..e15389be53a 100644 --- a/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js +++ b/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js @@ -1,10 +1,10 @@ +import { GlBadge, GlButton } from '@gitlab/ui'; +import { createLocalVue, shallowMount } from '@vue/test-utils'; import { nextTick } from 'vue'; import Vuex from 'vuex'; -import { createLocalVue, shallowMount } from '@vue/test-utils'; -import { GlBadge } from '@gitlab/ui'; -import ProviderRepoTableRow from '~/import_entities/import_projects/components/provider_repo_table_row.vue'; -import ImportStatus from '~/import_entities/components/import_status.vue'; import { STATUSES } from '~/import_entities//constants'; +import ImportStatus from '~/import_entities/components/import_status.vue'; +import ProviderRepoTableRow from '~/import_entities/import_projects/components/provider_repo_table_row.vue'; import Select2Select from '~/vue_shared/components/select2_select.vue'; describe('ProviderRepoTableRow', () => { @@ -34,7 +34,7 @@ describe('ProviderRepoTableRow', () => { } const findImportButton = () => { - const buttons = wrapper.findAll('button').filter((node) => node.text() === 'Import'); + const buttons = wrapper.findAllComponents(GlButton).filter((node) => node.text() === 'Import'); return buttons.length ? buttons.at(0) : buttons; }; @@ -91,7 +91,7 @@ describe('ProviderRepoTableRow', () => { }); it('imports repo when clicking import button', async () => { - findImportButton().trigger('click'); + findImportButton().vm.$emit('click'); await nextTick(); diff --git a/spec/frontend/import_entities/import_projects/store/actions_spec.js b/spec/frontend/import_entities/import_projects/store/actions_spec.js index bd731dc3929..9bff77cd34a 100644 --- a/spec/frontend/import_entities/import_projects/store/actions_spec.js +++ b/spec/frontend/import_entities/import_projects/store/actions_spec.js @@ -1,9 +1,10 @@ import MockAdapter from 'axios-mock-adapter'; -import testAction from 'helpers/vuex_action_helper'; import { TEST_HOST } from 'helpers/test_constants'; +import testAction from 'helpers/vuex_action_helper'; import { deprecatedCreateFlash as createFlash } from '~/flash'; -import axios from '~/lib/utils/axios_utils'; -import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; +import { STATUSES } from '~/import_entities/constants'; +import actionsFactory from '~/import_entities/import_projects/store/actions'; +import { getImportTarget } from '~/import_entities/import_projects/store/getters'; import { REQUEST_REPOS, RECEIVE_REPOS_SUCCESS, @@ -18,10 +19,9 @@ import { SET_PAGE, SET_FILTER, } from '~/import_entities/import_projects/store/mutation_types'; -import actionsFactory from '~/import_entities/import_projects/store/actions'; -import { getImportTarget } from '~/import_entities/import_projects/store/getters'; import state from '~/import_entities/import_projects/store/state'; -import { STATUSES } from '~/import_entities/constants'; +import axios from '~/lib/utils/axios_utils'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; jest.mock('~/flash'); diff --git a/spec/frontend/import_entities/import_projects/store/getters_spec.js b/spec/frontend/import_entities/import_projects/store/getters_spec.js index f0ccffc19f2..55826b20ca3 100644 --- a/spec/frontend/import_entities/import_projects/store/getters_spec.js +++ b/spec/frontend/import_entities/import_projects/store/getters_spec.js @@ -1,3 +1,4 @@ +import { STATUSES } from '~/import_entities/constants'; import { isLoading, isImportingAnyRepo, @@ -6,7 +7,6 @@ import { importAllCount, getImportTarget, } from '~/import_entities/import_projects/store/getters'; -import { STATUSES } from '~/import_entities/constants'; import state from '~/import_entities/import_projects/store/state'; const IMPORTED_REPO = { diff --git a/spec/frontend/import_entities/import_projects/store/mutations_spec.js b/spec/frontend/import_entities/import_projects/store/mutations_spec.js index 8b7ddffe6f4..e062d889325 100644 --- a/spec/frontend/import_entities/import_projects/store/mutations_spec.js +++ b/spec/frontend/import_entities/import_projects/store/mutations_spec.js @@ -1,7 +1,7 @@ +import { STATUSES } from '~/import_entities/constants'; import * as types from '~/import_entities/import_projects/store/mutation_types'; import mutations from '~/import_entities/import_projects/store/mutations'; import getInitialState from '~/import_entities/import_projects/store/state'; -import { STATUSES } from '~/import_entities/constants'; describe('import_projects store mutations', () => { let state; diff --git a/spec/frontend/import_entities/import_projects/utils_spec.js b/spec/frontend/import_entities/import_projects/utils_spec.js index 7d9c4b7137e..d705f0acbfe 100644 --- a/spec/frontend/import_entities/import_projects/utils_spec.js +++ b/spec/frontend/import_entities/import_projects/utils_spec.js @@ -1,9 +1,9 @@ +import { STATUSES } from '~/import_entities/constants'; import { isProjectImportable, isIncompatible, getImportStatus, } from '~/import_entities/import_projects/utils'; -import { STATUSES } from '~/import_entities/constants'; describe('import_projects utils', () => { const COMPATIBLE_PROJECT = { -- cgit v1.2.3