summaryrefslogtreecommitdiff
path: root/spec/frontend/releases
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/releases')
-rw-r--r--spec/frontend/releases/__snapshots__/util_spec.js.snap113
-rw-r--r--spec/frontend/releases/components/app_index_spec.js141
-rw-r--r--spec/frontend/releases/components/app_show_spec.js2
-rw-r--r--spec/frontend/releases/components/asset_links_form_spec.js36
-rw-r--r--spec/frontend/releases/components/release_block_assets_spec.js2
-rw-r--r--spec/frontend/releases/components/release_block_footer_spec.js7
-rw-r--r--spec/frontend/releases/components/release_block_spec.js4
-rw-r--r--spec/frontend/releases/components/releases_pagination_graphql_spec.js175
-rw-r--r--spec/frontend/releases/components/releases_pagination_rest_spec.js72
-rw-r--r--spec/frontend/releases/components/releases_pagination_spec.js52
-rw-r--r--spec/frontend/releases/components/tag_field_exsting_spec.js7
-rw-r--r--spec/frontend/releases/mock_data.js128
-rw-r--r--spec/frontend/releases/stores/modules/list/actions_spec.js150
-rw-r--r--spec/frontend/releases/stores/modules/list/helpers.js1
-rw-r--r--spec/frontend/releases/stores/modules/list/mutations_spec.js4
-rw-r--r--spec/frontend/releases/util_spec.js55
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();
+ });
+ });
+ });
});