diff options
Diffstat (limited to 'spec/frontend/releases')
16 files changed, 820 insertions, 129 deletions
diff --git a/spec/frontend/releases/__snapshots__/util_spec.js.snap b/spec/frontend/releases/__snapshots__/util_spec.js.snap new file mode 100644 index 00000000000..f56e296d106 --- /dev/null +++ b/spec/frontend/releases/__snapshots__/util_spec.js.snap @@ -0,0 +1,113 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`releases/util.js convertGraphQLResponse matches snapshot 1`] = ` +Object { + "data": Array [ + Object { + "_links": Object { + "editUrl": "http://0.0.0.0:3000/root/release-test/-/releases/v5.10/edit", + "issuesUrl": null, + "mergeRequestsUrl": null, + "self": "http://0.0.0.0:3000/root/release-test/-/releases/v5.10", + "selfUrl": "http://0.0.0.0:3000/root/release-test/-/releases/v5.10", + }, + "assets": Object { + "count": 7, + "links": Array [ + Object { + "directAssetUrl": "http://0.0.0.0:3000/root/release-test/-/releases/v5.32/permanent/path/to/runbook", + "external": true, + "id": "gid://gitlab/Releases::Link/69", + "linkType": "other", + "name": "An example link", + "url": "https://example.com/link", + }, + Object { + "directAssetUrl": "https://example.com/package", + "external": true, + "id": "gid://gitlab/Releases::Link/68", + "linkType": "package", + "name": "An example package link", + "url": "https://example.com/package", + }, + Object { + "directAssetUrl": "https://example.com/image", + "external": true, + "id": "gid://gitlab/Releases::Link/67", + "linkType": "image", + "name": "An example image", + "url": "https://example.com/image", + }, + ], + "sources": Array [ + Object { + "format": "zip", + "url": "http://0.0.0.0:3000/root/release-test/-/archive/v5.10/release-test-v5.10.zip", + }, + Object { + "format": "tar.gz", + "url": "http://0.0.0.0:3000/root/release-test/-/archive/v5.10/release-test-v5.10.tar.gz", + }, + Object { + "format": "tar.bz2", + "url": "http://0.0.0.0:3000/root/release-test/-/archive/v5.10/release-test-v5.10.tar.bz2", + }, + Object { + "format": "tar", + "url": "http://0.0.0.0:3000/root/release-test/-/archive/v5.10/release-test-v5.10.tar", + }, + ], + }, + "author": Object { + "avatarUrl": "/uploads/-/system/user/avatar/1/avatar.png", + "username": "root", + "webUrl": "http://0.0.0.0:3000/root", + }, + "commit": Object { + "shortId": "92e7ea2e", + "title": "Testing a change.", + }, + "commitPath": "http://0.0.0.0:3000/root/release-test/-/commit/92e7ea2ee4496fe0d00ff69830ba0564d3d1e5a7", + "descriptionHtml": "<p data-sourcepos=\\"1:1-1:24\\" dir=\\"auto\\">This is version <strong>1.0</strong>!</p>", + "evidences": Array [ + Object { + "collectedAt": "2020-08-21T20:15:19Z", + "filepath": "http://0.0.0.0:3000/root/release-test/-/releases/v5.10/evidences/34.json", + "sha": "22bde8e8b93d870a29ddc339287a1fbb598f45d1396d", + }, + ], + "milestones": Array [ + Object { + "description": "", + "id": "gid://gitlab/Milestone/60", + "issueStats": Object { + "closed": 0, + "total": 0, + }, + "stats": undefined, + "title": "12.4", + "webPath": undefined, + "webUrl": "/root/release-test/-/milestones/2", + }, + Object { + "description": "Milestone 12.3", + "id": "gid://gitlab/Milestone/59", + "issueStats": Object { + "closed": 1, + "total": 2, + }, + "stats": undefined, + "title": "12.3", + "webPath": undefined, + "webUrl": "/root/release-test/-/milestones/1", + }, + ], + "name": "Release 1.0", + "releasedAt": "2020-08-21T20:15:18Z", + "tagName": "v5.10", + "tagPath": "/root/release-test/-/tags/v5.10", + "upcomingRelease": false, + }, + ], +} +`; diff --git a/spec/frontend/releases/components/app_index_spec.js b/spec/frontend/releases/components/app_index_spec.js index 8eafe07cb2f..bcb87509cc3 100644 --- a/spec/frontend/releases/components/app_index_spec.js +++ b/spec/frontend/releases/components/app_index_spec.js @@ -1,12 +1,11 @@ import { range as rge } from 'lodash'; -import Vue from 'vue'; -import { mountComponentWithStore } from 'helpers/vue_mount_component_helper'; +import Vuex from 'vuex'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; import waitForPromises from 'helpers/wait_for_promises'; -import app from '~/releases/components/app_index.vue'; +import ReleasesApp from '~/releases/components/app_index.vue'; import createStore from '~/releases/stores'; -import listModule from '~/releases/stores/modules/list'; +import createListModule from '~/releases/stores/modules/list'; import api from '~/api'; -import { resetStore } from '../stores/modules/list/helpers'; import { pageInfoHeadersWithoutPagination, pageInfoHeadersWithPagination, @@ -14,30 +13,67 @@ import { releases, } from '../mock_data'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; +import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue'; + +const localVue = createLocalVue(); +localVue.use(Vuex); describe('Releases App ', () => { - const Component = Vue.extend(app); - let store; - let vm; - let releasesPagination; + let wrapper; + let fetchReleaseSpy; + + const releasesPagination = rge(21).map(index => ({ + ...convertObjectPropsToCamelCase(release, { deep: true }), + tagName: `${index}.00`, + })); - const props = { + const defaultInitialState = { projectId: 'gitlab-ce', + projectPath: 'gitlab-org/gitlab-ce', documentationPath: 'help/releases', illustrationPath: 'illustration/path', }; - beforeEach(() => { - store = createStore({ modules: { list: listModule } }); - releasesPagination = rge(21).map(index => ({ - ...convertObjectPropsToCamelCase(release, { deep: true }), - tagName: `${index}.00`, - })); - }); + const createComponent = (stateUpdates = {}) => { + const listModule = createListModule({ + ...defaultInitialState, + ...stateUpdates, + }); + + fetchReleaseSpy = jest.spyOn(listModule.actions, 'fetchReleases'); + + const store = createStore({ + modules: { list: listModule }, + featureFlags: { + graphqlReleaseData: true, + graphqlReleasesPage: false, + graphqlMilestoneStats: true, + }, + }); + + wrapper = shallowMount(ReleasesApp, { + store, + localVue, + }); + }; afterEach(() => { - resetStore(store); - vm.$destroy(); + wrapper.destroy(); + }); + + describe('on startup', () => { + beforeEach(() => { + jest + .spyOn(api, 'releases') + .mockResolvedValue({ data: releases, headers: pageInfoHeadersWithoutPagination }); + + createComponent(); + }); + + it('calls fetchRelease with the page parameter', () => { + expect(fetchReleaseSpy).toHaveBeenCalledTimes(1); + expect(fetchReleaseSpy).toHaveBeenCalledWith(expect.anything(), { page: null }); + }); }); describe('while loading', () => { @@ -47,16 +83,15 @@ describe('Releases App ', () => { // Need to defer the return value here to the next stack, // otherwise the loading state disappears before our test even starts. .mockImplementation(() => waitForPromises().then(() => ({ data: [], headers: {} }))); - vm = mountComponentWithStore(Component, { props, store }); + + createComponent(); }); it('renders loading icon', () => { - expect(vm.$el.querySelector('.js-loading')).not.toBeNull(); - expect(vm.$el.querySelector('.js-empty-state')).toBeNull(); - expect(vm.$el.querySelector('.js-success-state')).toBeNull(); - expect(vm.$el.querySelector('.gl-pagination')).toBeNull(); - - return waitForPromises(); + expect(wrapper.contains('.js-loading')).toBe(true); + expect(wrapper.contains('.js-empty-state')).toBe(false); + expect(wrapper.contains('.js-success-state')).toBe(false); + expect(wrapper.contains(TablePagination)).toBe(false); }); }); @@ -65,14 +100,15 @@ describe('Releases App ', () => { jest .spyOn(api, 'releases') .mockResolvedValue({ data: releases, headers: pageInfoHeadersWithoutPagination }); - vm = mountComponentWithStore(Component, { props, store }); + + createComponent(); }); it('renders success state', () => { - expect(vm.$el.querySelector('.js-loading')).toBeNull(); - expect(vm.$el.querySelector('.js-empty-state')).toBeNull(); - expect(vm.$el.querySelector('.js-success-state')).not.toBeNull(); - expect(vm.$el.querySelector('.gl-pagination')).toBeNull(); + expect(wrapper.contains('.js-loading')).toBe(false); + expect(wrapper.contains('.js-empty-state')).toBe(false); + expect(wrapper.contains('.js-success-state')).toBe(true); + expect(wrapper.contains(TablePagination)).toBe(true); }); }); @@ -81,69 +117,60 @@ describe('Releases App ', () => { jest .spyOn(api, 'releases') .mockResolvedValue({ data: releasesPagination, headers: pageInfoHeadersWithPagination }); - vm = mountComponentWithStore(Component, { props, store }); + + createComponent(); }); it('renders success state', () => { - expect(vm.$el.querySelector('.js-loading')).toBeNull(); - expect(vm.$el.querySelector('.js-empty-state')).toBeNull(); - expect(vm.$el.querySelector('.js-success-state')).not.toBeNull(); - expect(vm.$el.querySelector('.gl-pagination')).not.toBeNull(); + expect(wrapper.contains('.js-loading')).toBe(false); + expect(wrapper.contains('.js-empty-state')).toBe(false); + expect(wrapper.contains('.js-success-state')).toBe(true); + expect(wrapper.contains(TablePagination)).toBe(true); }); }); describe('with empty request', () => { beforeEach(() => { jest.spyOn(api, 'releases').mockResolvedValue({ data: [], headers: {} }); - vm = mountComponentWithStore(Component, { props, store }); + + createComponent(); }); it('renders empty state', () => { - expect(vm.$el.querySelector('.js-loading')).toBeNull(); - expect(vm.$el.querySelector('.js-empty-state')).not.toBeNull(); - expect(vm.$el.querySelector('.js-success-state')).toBeNull(); - expect(vm.$el.querySelector('.gl-pagination')).toBeNull(); + expect(wrapper.contains('.js-loading')).toBe(false); + expect(wrapper.contains('.js-empty-state')).toBe(true); + expect(wrapper.contains('.js-success-state')).toBe(false); }); }); describe('"New release" button', () => { - const findNewReleaseButton = () => vm.$el.querySelector('.js-new-release-btn'); + const findNewReleaseButton = () => wrapper.find('.js-new-release-btn'); beforeEach(() => { jest.spyOn(api, 'releases').mockResolvedValue({ data: [], headers: {} }); }); - const factory = additionalProps => { - vm = mountComponentWithStore(Component, { - props: { - ...props, - ...additionalProps, - }, - store, - }); - }; - describe('when the user is allowed to create a new Release', () => { const newReleasePath = 'path/to/new/release'; beforeEach(() => { - factory({ newReleasePath }); + createComponent({ ...defaultInitialState, newReleasePath }); }); it('renders the "New release" button', () => { - expect(findNewReleaseButton()).not.toBeNull(); + expect(findNewReleaseButton().exists()).toBe(true); }); it('renders the "New release" button with the correct href', () => { - expect(findNewReleaseButton().getAttribute('href')).toBe(newReleasePath); + expect(findNewReleaseButton().attributes('href')).toBe(newReleasePath); }); }); describe('when the user is not allowed to create a new Release', () => { - beforeEach(() => factory()); + beforeEach(() => createComponent()); it('does not render the "New release" button', () => { - expect(findNewReleaseButton()).toBeNull(); + expect(findNewReleaseButton().exists()).toBe(false); }); }); }); diff --git a/spec/frontend/releases/components/app_show_spec.js b/spec/frontend/releases/components/app_show_spec.js index e757fe98661..502a1053663 100644 --- a/spec/frontend/releases/components/app_show_spec.js +++ b/spec/frontend/releases/components/app_show_spec.js @@ -1,6 +1,6 @@ import Vuex from 'vuex'; import { shallowMount } from '@vue/test-utils'; -import { GlSkeletonLoading } from '@gitlab/ui'; +import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui'; import ReleaseShowApp from '~/releases/components/app_show.vue'; import { release as originalRelease } from '../mock_data'; import ReleaseBlock from '~/releases/components/release_block.vue'; diff --git a/spec/frontend/releases/components/asset_links_form_spec.js b/spec/frontend/releases/components/asset_links_form_spec.js index 727d593d851..582c0b32716 100644 --- a/spec/frontend/releases/components/asset_links_form_spec.js +++ b/spec/frontend/releases/components/asset_links_form_spec.js @@ -115,14 +115,10 @@ describe('Release edit component', () => { const expectStoreMethodToBeCalled = () => { expect(actions.updateAssetLinkUrl).toHaveBeenCalledTimes(1); - expect(actions.updateAssetLinkUrl).toHaveBeenCalledWith( - expect.anything(), - { - linkIdToUpdate, - newUrl, - }, - undefined, - ); + expect(actions.updateAssetLinkUrl).toHaveBeenCalledWith(expect.anything(), { + linkIdToUpdate, + newUrl, + }); }; it('calls the "updateAssetLinkUrl" store method when text is entered into the "URL" input field', () => { @@ -177,14 +173,10 @@ describe('Release edit component', () => { const expectStoreMethodToBeCalled = () => { expect(actions.updateAssetLinkName).toHaveBeenCalledTimes(1); - expect(actions.updateAssetLinkName).toHaveBeenCalledWith( - expect.anything(), - { - linkIdToUpdate, - newName, - }, - undefined, - ); + expect(actions.updateAssetLinkName).toHaveBeenCalledWith(expect.anything(), { + linkIdToUpdate, + newName, + }); }; it('calls the "updateAssetLinkName" store method when text is entered into the "Link title" input field', () => { @@ -225,14 +217,10 @@ describe('Release edit component', () => { wrapper.find({ ref: 'typeSelect' }).vm.$emit('change', newType); expect(actions.updateAssetLinkType).toHaveBeenCalledTimes(1); - expect(actions.updateAssetLinkType).toHaveBeenCalledWith( - expect.anything(), - { - linkIdToUpdate, - newType, - }, - undefined, - ); + expect(actions.updateAssetLinkType).toHaveBeenCalledWith(expect.anything(), { + linkIdToUpdate, + newType, + }); }); it('selects the default asset type if no type was provided by the backend', () => { diff --git a/spec/frontend/releases/components/release_block_assets_spec.js b/spec/frontend/releases/components/release_block_assets_spec.js index 5e84290716c..3453ecbf8ab 100644 --- a/spec/frontend/releases/components/release_block_assets_spec.js +++ b/spec/frontend/releases/components/release_block_assets_spec.js @@ -128,7 +128,7 @@ describe('Release block assets', () => { describe('external vs internal links', () => { const containsExternalSourceIndicator = () => - wrapper.contains('[data-testid="external-link-indicator"]'); + wrapper.find('[data-testid="external-link-indicator"]').exists(); describe('when a link is external', () => { beforeEach(() => { diff --git a/spec/frontend/releases/components/release_block_footer_spec.js b/spec/frontend/releases/components/release_block_footer_spec.js index c066bfbf020..bde01cc0e00 100644 --- a/spec/frontend/releases/components/release_block_footer_spec.js +++ b/spec/frontend/releases/components/release_block_footer_spec.js @@ -1,9 +1,8 @@ import { mount } from '@vue/test-utils'; -import { GlLink } from '@gitlab/ui'; +import { GlLink, GlIcon } from '@gitlab/ui'; import { trimText } from 'helpers/text_helper'; import { cloneDeep } from 'lodash'; import ReleaseBlockFooter from '~/releases/components/release_block_footer.vue'; -import Icon from '~/vue_shared/components/icon.vue'; import { release as originalRelease } from '../mock_data'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; @@ -56,7 +55,7 @@ describe('Release block footer', () => { beforeEach(() => factory()); it('renders the commit icon', () => { - const commitIcon = commitInfoSection().find(Icon); + const commitIcon = commitInfoSection().find(GlIcon); expect(commitIcon.exists()).toBe(true); expect(commitIcon.props('name')).toBe('commit'); @@ -71,7 +70,7 @@ describe('Release block footer', () => { }); it('renders the tag icon', () => { - const commitIcon = tagInfoSection().find(Icon); + const commitIcon = tagInfoSection().find(GlIcon); expect(commitIcon.exists()).toBe(true); expect(commitIcon.props('name')).toBe('tag'); diff --git a/spec/frontend/releases/components/release_block_spec.js b/spec/frontend/releases/components/release_block_spec.js index 19119d99f3c..a7f1388664b 100644 --- a/spec/frontend/releases/components/release_block_spec.js +++ b/spec/frontend/releases/components/release_block_spec.js @@ -1,11 +1,11 @@ import $ from 'jquery'; import { mount } from '@vue/test-utils'; +import { GlIcon } from '@gitlab/ui'; import EvidenceBlock from '~/releases/components/evidence_block.vue'; import ReleaseBlock from '~/releases/components/release_block.vue'; import ReleaseBlockFooter from '~/releases/components/release_block_footer.vue'; import timeagoMixin from '~/vue_shared/mixins/timeago'; import { release as originalRelease } from '../mock_data'; -import Icon from '~/vue_shared/components/icon.vue'; import * as commonUtils from '~/lib/utils/common_utils'; import { BACK_URL_PARAM } from '~/releases/constants'; import * as urlUtility from '~/lib/utils/url_utility'; @@ -247,7 +247,7 @@ describe('Release block', () => { it('renders the milestone icon', () => { expect( milestoneListLabel() - .find(Icon) + .find(GlIcon) .exists(), ).toBe(true); }); diff --git a/spec/frontend/releases/components/releases_pagination_graphql_spec.js b/spec/frontend/releases/components/releases_pagination_graphql_spec.js new file mode 100644 index 00000000000..b01a28eb6c3 --- /dev/null +++ b/spec/frontend/releases/components/releases_pagination_graphql_spec.js @@ -0,0 +1,175 @@ +import Vuex from 'vuex'; +import { mount, createLocalVue } from '@vue/test-utils'; +import createStore from '~/releases/stores'; +import createListModule from '~/releases/stores/modules/list'; +import ReleasesPaginationGraphql from '~/releases/components/releases_pagination_graphql.vue'; +import { historyPushState } from '~/lib/utils/common_utils'; + +jest.mock('~/lib/utils/common_utils', () => ({ + ...jest.requireActual('~/lib/utils/common_utils'), + historyPushState: jest.fn(), +})); + +const localVue = createLocalVue(); +localVue.use(Vuex); + +describe('~/releases/components/releases_pagination_graphql.vue', () => { + let wrapper; + let listModule; + + const cursors = { + startCursor: 'startCursor', + endCursor: 'endCursor', + }; + + const projectPath = 'my/project'; + + const createComponent = pageInfo => { + listModule = createListModule({ projectPath }); + + listModule.state.graphQlPageInfo = pageInfo; + + listModule.actions.fetchReleasesGraphQl = jest.fn(); + + wrapper = mount(ReleasesPaginationGraphql, { + store: createStore({ + modules: { + list: listModule, + }, + featureFlags: {}, + }), + localVue, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + const findPrevButton = () => wrapper.find('[data-testid="prevButton"]'); + const findNextButton = () => wrapper.find('[data-testid="nextButton"]'); + + const expectDisabledPrev = () => { + expect(findPrevButton().attributes().disabled).toBe('disabled'); + }; + const expectEnabledPrev = () => { + expect(findPrevButton().attributes().disabled).toBe(undefined); + }; + const expectDisabledNext = () => { + expect(findNextButton().attributes().disabled).toBe('disabled'); + }; + const expectEnabledNext = () => { + expect(findNextButton().attributes().disabled).toBe(undefined); + }; + + describe('when there is only one page of results', () => { + beforeEach(() => { + createComponent({ + hasPreviousPage: false, + hasNextPage: false, + }); + }); + + it('does not render anything', () => { + expect(wrapper.isEmpty()).toBe(true); + }); + }); + + describe('when there is a next page, but not a previous page', () => { + beforeEach(() => { + createComponent({ + hasPreviousPage: false, + hasNextPage: true, + }); + }); + + it('renders a disabled "Prev" button', () => { + expectDisabledPrev(); + }); + + it('renders an enabled "Next" button', () => { + expectEnabledNext(); + }); + }); + + describe('when there is a previous page, but not a next page', () => { + beforeEach(() => { + createComponent({ + hasPreviousPage: true, + hasNextPage: false, + }); + }); + + it('renders a enabled "Prev" button', () => { + expectEnabledPrev(); + }); + + it('renders an disabled "Next" button', () => { + expectDisabledNext(); + }); + }); + + describe('when there is both a previous page and a next page', () => { + beforeEach(() => { + createComponent({ + hasPreviousPage: true, + hasNextPage: true, + }); + }); + + it('renders a enabled "Prev" button', () => { + expectEnabledPrev(); + }); + + it('renders an enabled "Next" button', () => { + expectEnabledNext(); + }); + }); + + describe('button behavior', () => { + beforeEach(() => { + createComponent({ + hasPreviousPage: true, + hasNextPage: true, + ...cursors, + }); + }); + + describe('next button behavior', () => { + beforeEach(() => { + findNextButton().trigger('click'); + }); + + it('calls fetchReleasesGraphQl with the correct after cursor', () => { + expect(listModule.actions.fetchReleasesGraphQl.mock.calls).toEqual([ + [expect.anything(), { after: cursors.endCursor }], + ]); + }); + + it('calls historyPushState with the new URL', () => { + expect(historyPushState.mock.calls).toEqual([ + [expect.stringContaining(`?after=${cursors.endCursor}`)], + ]); + }); + }); + + describe('previous button behavior', () => { + beforeEach(() => { + findPrevButton().trigger('click'); + }); + + it('calls fetchReleasesGraphQl with the correct before cursor', () => { + expect(listModule.actions.fetchReleasesGraphQl.mock.calls).toEqual([ + [expect.anything(), { before: cursors.startCursor }], + ]); + }); + + it('calls historyPushState with the new URL', () => { + expect(historyPushState.mock.calls).toEqual([ + [expect.stringContaining(`?before=${cursors.startCursor}`)], + ]); + }); + }); + }); +}); diff --git a/spec/frontend/releases/components/releases_pagination_rest_spec.js b/spec/frontend/releases/components/releases_pagination_rest_spec.js new file mode 100644 index 00000000000..4fd3e085fc9 --- /dev/null +++ b/spec/frontend/releases/components/releases_pagination_rest_spec.js @@ -0,0 +1,72 @@ +import Vuex from 'vuex'; +import { mount, createLocalVue } from '@vue/test-utils'; +import { GlPagination } from '@gitlab/ui'; +import ReleasesPaginationRest from '~/releases/components/releases_pagination_rest.vue'; +import createStore from '~/releases/stores'; +import createListModule from '~/releases/stores/modules/list'; +import * as commonUtils from '~/lib/utils/common_utils'; + +commonUtils.historyPushState = jest.fn(); + +const localVue = createLocalVue(); +localVue.use(Vuex); + +describe('~/releases/components/releases_pagination_rest.vue', () => { + let wrapper; + let listModule; + + const projectId = 19; + + const createComponent = pageInfo => { + listModule = createListModule({ projectId }); + + listModule.state.pageInfo = pageInfo; + + listModule.actions.fetchReleasesRest = jest.fn(); + + wrapper = mount(ReleasesPaginationRest, { + store: createStore({ + modules: { + list: listModule, + }, + featureFlags: {}, + }), + localVue, + }); + }; + + const findGlPagination = () => wrapper.find(GlPagination); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('when a page number is clicked', () => { + const newPage = 2; + + beforeEach(() => { + createComponent({ + perPage: 20, + page: 1, + total: 40, + totalPages: 2, + nextPage: 2, + }); + + findGlPagination().vm.$emit('input', newPage); + }); + + it('calls fetchReleasesRest with the correct page', () => { + expect(listModule.actions.fetchReleasesRest.mock.calls).toEqual([ + [expect.anything(), { page: newPage }], + ]); + }); + + it('calls historyPushState with the new URL', () => { + expect(commonUtils.historyPushState.mock.calls).toEqual([ + [expect.stringContaining(`?page=${newPage}`)], + ]); + }); + }); +}); diff --git a/spec/frontend/releases/components/releases_pagination_spec.js b/spec/frontend/releases/components/releases_pagination_spec.js new file mode 100644 index 00000000000..2466fb53a68 --- /dev/null +++ b/spec/frontend/releases/components/releases_pagination_spec.js @@ -0,0 +1,52 @@ +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import Vuex from 'vuex'; +import ReleasesPagination from '~/releases/components/releases_pagination.vue'; +import ReleasesPaginationGraphql from '~/releases/components/releases_pagination_graphql.vue'; +import ReleasesPaginationRest from '~/releases/components/releases_pagination_rest.vue'; + +const localVue = createLocalVue(); +localVue.use(Vuex); + +describe('~/releases/components/releases_pagination.vue', () => { + let wrapper; + + const createComponent = useGraphQLEndpoint => { + const store = new Vuex.Store({ + getters: { + useGraphQLEndpoint: () => useGraphQLEndpoint, + }, + }); + + wrapper = shallowMount(ReleasesPagination, { store, localVue }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + const findRestPagination = () => wrapper.find(ReleasesPaginationRest); + const findGraphQlPagination = () => wrapper.find(ReleasesPaginationGraphql); + + describe('when one of necessary feature flags is disabled', () => { + beforeEach(() => { + createComponent(false); + }); + + it('renders the REST pagination component', () => { + expect(findRestPagination().exists()).toBe(true); + expect(findGraphQlPagination().exists()).toBe(false); + }); + }); + + describe('when all the necessary feature flags are enabled', () => { + beforeEach(() => { + createComponent(true); + }); + + it('renders the GraphQL pagination component', () => { + expect(findGraphQlPagination().exists()).toBe(true); + expect(findRestPagination().exists()).toBe(false); + }); + }); +}); diff --git a/spec/frontend/releases/components/tag_field_exsting_spec.js b/spec/frontend/releases/components/tag_field_exsting_spec.js index 0a04f68bd67..70a195556df 100644 --- a/spec/frontend/releases/components/tag_field_exsting_spec.js +++ b/spec/frontend/releases/components/tag_field_exsting_spec.js @@ -1,5 +1,6 @@ +import Vuex from 'vuex'; import { GlFormInput } from '@gitlab/ui'; -import { shallowMount, mount } from '@vue/test-utils'; +import { shallowMount, mount, createLocalVue } from '@vue/test-utils'; import TagFieldExisting from '~/releases/components/tag_field_existing.vue'; import createStore from '~/releases/stores'; import createDetailModule from '~/releases/stores/modules/detail'; @@ -7,6 +8,9 @@ import createDetailModule from '~/releases/stores/modules/detail'; const TEST_TAG_NAME = 'test-tag-name'; const TEST_DOCS_PATH = '/help/test/docs/path'; +const localVue = createLocalVue(); +localVue.use(Vuex); + describe('releases/components/tag_field_existing', () => { let store; let wrapper; @@ -14,6 +18,7 @@ describe('releases/components/tag_field_existing', () => { const createComponent = (mountFn = shallowMount) => { wrapper = mountFn(TagFieldExisting, { store, + localVue, }); }; diff --git a/spec/frontend/releases/mock_data.js b/spec/frontend/releases/mock_data.js index b97385154bd..58cd69a2f6a 100644 --- a/spec/frontend/releases/mock_data.js +++ b/spec/frontend/releases/mock_data.js @@ -222,3 +222,131 @@ export const release2 = { }; export const releases = [release, release2]; + +export const graphqlReleasesResponse = { + data: { + project: { + releases: { + count: 39, + nodes: [ + { + name: 'Release 1.0', + tagName: 'v5.10', + tagPath: '/root/release-test/-/tags/v5.10', + descriptionHtml: + '<p data-sourcepos="1:1-1:24" dir="auto">This is version <strong>1.0</strong>!</p>', + releasedAt: '2020-08-21T20:15:18Z', + upcomingRelease: false, + assets: { + count: 7, + sources: { + nodes: [ + { + format: 'zip', + url: + 'http://0.0.0.0:3000/root/release-test/-/archive/v5.10/release-test-v5.10.zip', + }, + { + format: 'tar.gz', + url: + 'http://0.0.0.0:3000/root/release-test/-/archive/v5.10/release-test-v5.10.tar.gz', + }, + { + format: 'tar.bz2', + url: + 'http://0.0.0.0:3000/root/release-test/-/archive/v5.10/release-test-v5.10.tar.bz2', + }, + { + format: 'tar', + url: + 'http://0.0.0.0:3000/root/release-test/-/archive/v5.10/release-test-v5.10.tar', + }, + ], + }, + links: { + nodes: [ + { + id: 'gid://gitlab/Releases::Link/69', + name: 'An example link', + url: 'https://example.com/link', + directAssetUrl: + 'http://0.0.0.0:3000/root/release-test/-/releases/v5.32/permanent/path/to/runbook', + linkType: 'OTHER', + external: true, + }, + { + id: 'gid://gitlab/Releases::Link/68', + name: 'An example package link', + url: 'https://example.com/package', + directAssetUrl: 'https://example.com/package', + linkType: 'PACKAGE', + external: true, + }, + { + id: 'gid://gitlab/Releases::Link/67', + name: 'An example image', + url: 'https://example.com/image', + directAssetUrl: 'https://example.com/image', + linkType: 'IMAGE', + external: true, + }, + ], + }, + }, + evidences: { + nodes: [ + { + filepath: + 'http://0.0.0.0:3000/root/release-test/-/releases/v5.10/evidences/34.json', + collectedAt: '2020-08-21T20:15:19Z', + sha: '22bde8e8b93d870a29ddc339287a1fbb598f45d1396d', + }, + ], + }, + links: { + editUrl: 'http://0.0.0.0:3000/root/release-test/-/releases/v5.10/edit', + issuesUrl: null, + mergeRequestsUrl: null, + selfUrl: 'http://0.0.0.0:3000/root/release-test/-/releases/v5.10', + }, + commit: { + sha: '92e7ea2ee4496fe0d00ff69830ba0564d3d1e5a7', + webUrl: + 'http://0.0.0.0:3000/root/release-test/-/commit/92e7ea2ee4496fe0d00ff69830ba0564d3d1e5a7', + title: 'Testing a change.', + }, + author: { + webUrl: 'http://0.0.0.0:3000/root', + avatarUrl: '/uploads/-/system/user/avatar/1/avatar.png', + username: 'root', + }, + milestones: { + nodes: [ + { + id: 'gid://gitlab/Milestone/60', + title: '12.4', + description: '', + webPath: '/root/release-test/-/milestones/2', + stats: { + totalIssuesCount: 0, + closedIssuesCount: 0, + }, + }, + { + id: 'gid://gitlab/Milestone/59', + title: '12.3', + description: 'Milestone 12.3', + webPath: '/root/release-test/-/milestones/1', + stats: { + totalIssuesCount: 2, + closedIssuesCount: 1, + }, + }, + ], + }, + }, + ], + }, + }, + }, +}; diff --git a/spec/frontend/releases/stores/modules/list/actions_spec.js b/spec/frontend/releases/stores/modules/list/actions_spec.js index 4c3af157684..95e30659d6c 100644 --- a/spec/frontend/releases/stores/modules/list/actions_spec.js +++ b/spec/frontend/releases/stores/modules/list/actions_spec.js @@ -1,3 +1,4 @@ +import { cloneDeep } from 'lodash'; import testAction from 'helpers/vuex_action_helper'; import { requestReleases, @@ -5,21 +6,43 @@ import { receiveReleasesSuccess, receiveReleasesError, } from '~/releases/stores/modules/list/actions'; -import state from '~/releases/stores/modules/list/state'; +import createState from '~/releases/stores/modules/list/state'; import * as types from '~/releases/stores/modules/list/mutation_types'; import api from '~/api'; +import { gqClient, convertGraphQLResponse } from '~/releases/util'; import { parseIntPagination, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; -import { pageInfoHeadersWithoutPagination, releases as originalReleases } from '../../../mock_data'; +import { + pageInfoHeadersWithoutPagination, + releases as originalReleases, + graphqlReleasesResponse as originalGraphqlReleasesResponse, +} from '../../../mock_data'; +import allReleasesQuery from '~/releases/queries/all_releases.query.graphql'; describe('Releases State actions', () => { let mockedState; let pageInfo; let releases; + let graphqlReleasesResponse; + + const projectPath = 'root/test-project'; + const projectId = 19; beforeEach(() => { - mockedState = state(); + mockedState = { + ...createState({ + projectId, + projectPath, + }), + featureFlags: { + graphqlReleaseData: true, + graphqlReleasesPage: true, + graphqlMilestoneStats: true, + }, + }; + pageInfo = parseIntPagination(pageInfoHeadersWithoutPagination); releases = convertObjectPropsToCamelCase(originalReleases, { deep: true }); + graphqlReleasesResponse = cloneDeep(originalGraphqlReleasesResponse); }); describe('requestReleases', () => { @@ -31,15 +54,17 @@ describe('Releases State actions', () => { describe('fetchReleases', () => { describe('success', () => { it('dispatches requestReleases and receiveReleasesSuccess', done => { - jest.spyOn(api, 'releases').mockImplementation((id, options) => { - expect(id).toEqual(1); - expect(options.page).toEqual('1'); - return Promise.resolve({ data: releases, headers: pageInfoHeadersWithoutPagination }); + jest.spyOn(gqClient, 'query').mockImplementation(({ query, variables }) => { + expect(query).toBe(allReleasesQuery); + expect(variables).toEqual({ + fullPath: projectPath, + }); + return Promise.resolve(graphqlReleasesResponse); }); testAction( fetchReleases, - { projectId: 1 }, + {}, mockedState, [], [ @@ -47,31 +72,7 @@ describe('Releases State actions', () => { type: 'requestReleases', }, { - payload: { data: releases, headers: pageInfoHeadersWithoutPagination }, - type: 'receiveReleasesSuccess', - }, - ], - done, - ); - }); - - it('dispatches requestReleases and receiveReleasesSuccess on page two', done => { - jest.spyOn(api, 'releases').mockImplementation((_, options) => { - expect(options.page).toEqual('2'); - return Promise.resolve({ data: releases, headers: pageInfoHeadersWithoutPagination }); - }); - - testAction( - fetchReleases, - { page: '2', projectId: 1 }, - mockedState, - [], - [ - { - type: 'requestReleases', - }, - { - payload: { data: releases, headers: pageInfoHeadersWithoutPagination }, + payload: convertGraphQLResponse(graphqlReleasesResponse), type: 'receiveReleasesSuccess', }, ], @@ -82,11 +83,11 @@ describe('Releases State actions', () => { describe('error', () => { it('dispatches requestReleases and receiveReleasesError', done => { - jest.spyOn(api, 'releases').mockReturnValue(Promise.reject()); + jest.spyOn(gqClient, 'query').mockRejectedValue(); testAction( fetchReleases, - { projectId: null }, + {}, mockedState, [], [ @@ -101,6 +102,85 @@ describe('Releases State actions', () => { ); }); }); + + describe('when the graphqlReleaseData feature flag is disabled', () => { + beforeEach(() => { + mockedState.featureFlags.graphqlReleasesPage = false; + }); + + describe('success', () => { + it('dispatches requestReleases and receiveReleasesSuccess', done => { + jest.spyOn(api, 'releases').mockImplementation((id, options) => { + expect(id).toBe(projectId); + expect(options.page).toBe('1'); + return Promise.resolve({ data: releases, headers: pageInfoHeadersWithoutPagination }); + }); + + testAction( + fetchReleases, + {}, + mockedState, + [], + [ + { + type: 'requestReleases', + }, + { + payload: { data: releases, headers: pageInfoHeadersWithoutPagination }, + type: 'receiveReleasesSuccess', + }, + ], + done, + ); + }); + + it('dispatches requestReleases and receiveReleasesSuccess on page two', done => { + jest.spyOn(api, 'releases').mockImplementation((_, options) => { + expect(options.page).toBe('2'); + return Promise.resolve({ data: releases, headers: pageInfoHeadersWithoutPagination }); + }); + + testAction( + fetchReleases, + { page: '2' }, + mockedState, + [], + [ + { + type: 'requestReleases', + }, + { + payload: { data: releases, headers: pageInfoHeadersWithoutPagination }, + type: 'receiveReleasesSuccess', + }, + ], + done, + ); + }); + }); + + describe('error', () => { + it('dispatches requestReleases and receiveReleasesError', done => { + jest.spyOn(api, 'releases').mockReturnValue(Promise.reject()); + + testAction( + fetchReleases, + {}, + mockedState, + [], + [ + { + type: 'requestReleases', + }, + { + type: 'receiveReleasesError', + }, + ], + done, + ); + }); + }); + }); }); describe('receiveReleasesSuccess', () => { diff --git a/spec/frontend/releases/stores/modules/list/helpers.js b/spec/frontend/releases/stores/modules/list/helpers.js index 435ca36047e..3ca255eaf8c 100644 --- a/spec/frontend/releases/stores/modules/list/helpers.js +++ b/spec/frontend/releases/stores/modules/list/helpers.js @@ -1,6 +1,5 @@ import state from '~/releases/stores/modules/list/state'; -// eslint-disable-next-line import/prefer-default-export export const resetStore = store => { store.replaceState(state()); }; diff --git a/spec/frontend/releases/stores/modules/list/mutations_spec.js b/spec/frontend/releases/stores/modules/list/mutations_spec.js index 3035b916ff6..27ad05846e7 100644 --- a/spec/frontend/releases/stores/modules/list/mutations_spec.js +++ b/spec/frontend/releases/stores/modules/list/mutations_spec.js @@ -1,4 +1,4 @@ -import state from '~/releases/stores/modules/list/state'; +import createState from '~/releases/stores/modules/list/state'; import mutations from '~/releases/stores/modules/list/mutations'; import * as types from '~/releases/stores/modules/list/mutation_types'; import { parseIntPagination } from '~/lib/utils/common_utils'; @@ -9,7 +9,7 @@ describe('Releases Store Mutations', () => { let pageInfo; beforeEach(() => { - stateCopy = state(); + stateCopy = createState({}); pageInfo = parseIntPagination(pageInfoHeadersWithoutPagination); }); diff --git a/spec/frontend/releases/util_spec.js b/spec/frontend/releases/util_spec.js index 90aa9c4c7d8..f40e5729188 100644 --- a/spec/frontend/releases/util_spec.js +++ b/spec/frontend/releases/util_spec.js @@ -1,4 +1,6 @@ -import { releaseToApiJson, apiJsonToRelease } from '~/releases/util'; +import { cloneDeep } from 'lodash'; +import { releaseToApiJson, apiJsonToRelease, convertGraphQLResponse } from '~/releases/util'; +import { graphqlReleasesResponse as originalGraphqlReleasesResponse } from './mock_data'; describe('releases/util.js', () => { describe('releaseToApiJson', () => { @@ -100,4 +102,55 @@ describe('releases/util.js', () => { expect(apiJsonToRelease(json)).toEqual(expectedRelease); }); }); + + describe('convertGraphQLResponse', () => { + let graphqlReleasesResponse; + let converted; + + beforeEach(() => { + graphqlReleasesResponse = cloneDeep(originalGraphqlReleasesResponse); + converted = convertGraphQLResponse(graphqlReleasesResponse); + }); + + it('matches snapshot', () => { + expect(converted).toMatchSnapshot(); + }); + + describe('assets', () => { + it("handles asset links that don't have a linkType", () => { + expect(converted.data[0].assets.links[0].linkType).not.toBeUndefined(); + + delete graphqlReleasesResponse.data.project.releases.nodes[0].assets.links.nodes[0] + .linkType; + + converted = convertGraphQLResponse(graphqlReleasesResponse); + + expect(converted.data[0].assets.links[0].linkType).toBeUndefined(); + }); + }); + + describe('_links', () => { + it("handles releases that don't have any links", () => { + expect(converted.data[0]._links.selfUrl).not.toBeUndefined(); + + delete graphqlReleasesResponse.data.project.releases.nodes[0].links; + + converted = convertGraphQLResponse(graphqlReleasesResponse); + + expect(converted.data[0]._links.selfUrl).toBeUndefined(); + }); + }); + + describe('commit', () => { + it("handles releases that don't have any commit info", () => { + expect(converted.data[0].commit).not.toBeUndefined(); + + delete graphqlReleasesResponse.data.project.releases.nodes[0].commit; + + converted = convertGraphQLResponse(graphqlReleasesResponse); + + expect(converted.data[0].commit).toBeUndefined(); + }); + }); + }); }); |