summaryrefslogtreecommitdiff
path: root/spec/frontend/releases/components/app_index_apollo_client_spec.js
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/releases/components/app_index_apollo_client_spec.js')
-rw-r--r--spec/frontend/releases/components/app_index_apollo_client_spec.js394
1 files changed, 394 insertions, 0 deletions
diff --git a/spec/frontend/releases/components/app_index_apollo_client_spec.js b/spec/frontend/releases/components/app_index_apollo_client_spec.js
new file mode 100644
index 00000000000..002d8939058
--- /dev/null
+++ b/spec/frontend/releases/components/app_index_apollo_client_spec.js
@@ -0,0 +1,394 @@
+import { cloneDeep } from 'lodash';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createFlash from '~/flash';
+import { historyPushState } from '~/lib/utils/common_utils';
+import ReleasesIndexApolloClientApp from '~/releases/components/app_index_apollo_client.vue';
+import ReleaseBlock from '~/releases/components/release_block.vue';
+import ReleaseSkeletonLoader from '~/releases/components/release_skeleton_loader.vue';
+import ReleasesEmptyState from '~/releases/components/releases_empty_state.vue';
+import ReleasesPaginationApolloClient from '~/releases/components/releases_pagination_apollo_client.vue';
+import ReleasesSortApolloClient from '~/releases/components/releases_sort_apollo_client.vue';
+import { PAGE_SIZE, CREATED_ASC, DEFAULT_SORT } from '~/releases/constants';
+import allReleasesQuery from '~/releases/graphql/queries/all_releases.query.graphql';
+
+Vue.use(VueApollo);
+
+jest.mock('~/flash');
+
+let mockQueryParams;
+jest.mock('~/lib/utils/common_utils', () => ({
+ ...jest.requireActual('~/lib/utils/common_utils'),
+ getParameterByName: jest
+ .fn()
+ .mockImplementation((parameterName) => mockQueryParams[parameterName]),
+ historyPushState: jest.fn(),
+}));
+
+describe('app_index_apollo_client.vue', () => {
+ const originalAllReleasesQueryResponse = getJSONFixture(
+ 'graphql/releases/graphql/queries/all_releases.query.graphql.json',
+ );
+ const projectPath = 'project/path';
+ const newReleasePath = 'path/to/new/release/page';
+ const before = 'beforeCursor';
+ const after = 'afterCursor';
+
+ let wrapper;
+ let allReleases;
+ let singleRelease;
+ let noReleases;
+ let queryMock;
+
+ const createComponent = ({
+ singleResponse = Promise.resolve(singleRelease),
+ fullResponse = Promise.resolve(allReleases),
+ } = {}) => {
+ const apolloProvider = createMockApollo([
+ [
+ allReleasesQuery,
+ queryMock.mockImplementation((vars) => {
+ return vars.first === 1 ? singleResponse : fullResponse;
+ }),
+ ],
+ ]);
+
+ wrapper = shallowMountExtended(ReleasesIndexApolloClientApp, {
+ apolloProvider,
+ provide: {
+ newReleasePath,
+ projectPath,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ mockQueryParams = {};
+
+ allReleases = cloneDeep(originalAllReleasesQueryResponse);
+
+ singleRelease = cloneDeep(originalAllReleasesQueryResponse);
+ singleRelease.data.project.releases.nodes.splice(
+ 1,
+ singleRelease.data.project.releases.nodes.length,
+ );
+
+ noReleases = cloneDeep(originalAllReleasesQueryResponse);
+ noReleases.data.project.releases.nodes = [];
+
+ queryMock = jest.fn();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ // Finders
+ const findLoadingIndicator = () => wrapper.findComponent(ReleaseSkeletonLoader);
+ const findEmptyState = () => wrapper.findComponent(ReleasesEmptyState);
+ const findNewReleaseButton = () =>
+ wrapper.findByText(ReleasesIndexApolloClientApp.i18n.newRelease);
+ const findAllReleaseBlocks = () => wrapper.findAllComponents(ReleaseBlock);
+ const findPagination = () => wrapper.findComponent(ReleasesPaginationApolloClient);
+ const findSort = () => wrapper.findComponent(ReleasesSortApolloClient);
+
+ // Tests
+ describe('component states', () => {
+ // These need to be defined as functions, since `singleRelease` and
+ // `allReleases` are generated in a `beforeEach`, and therefore
+ // aren't available at test definition time.
+ const getInProgressResponse = () => new Promise(() => {});
+ const getErrorResponse = () => Promise.reject(new Error('Oops!'));
+ const getSingleRequestLoadedResponse = () => Promise.resolve(singleRelease);
+ const getFullRequestLoadedResponse = () => Promise.resolve(allReleases);
+ const getLoadedEmptyResponse = () => Promise.resolve(noReleases);
+
+ const toDescription = (bool) => (bool ? 'does' : 'does not');
+
+ describe.each`
+ description | singleResponseFn | fullResponseFn | loadingIndicator | emptyState | flashMessage | releaseCount | pagination
+ ${'both requests loading'} | ${getInProgressResponse} | ${getInProgressResponse} | ${true} | ${false} | ${false} | ${0} | ${false}
+ ${'both requests failed'} | ${getErrorResponse} | ${getErrorResponse} | ${false} | ${false} | ${true} | ${0} | ${false}
+ ${'both requests loaded'} | ${getSingleRequestLoadedResponse} | ${getFullRequestLoadedResponse} | ${false} | ${false} | ${false} | ${2} | ${true}
+ ${'both requests loaded with no results'} | ${getLoadedEmptyResponse} | ${getLoadedEmptyResponse} | ${false} | ${true} | ${false} | ${0} | ${false}
+ ${'single request loading, full request loaded'} | ${getInProgressResponse} | ${getFullRequestLoadedResponse} | ${false} | ${false} | ${false} | ${2} | ${true}
+ ${'single request loading, full request failed'} | ${getInProgressResponse} | ${getErrorResponse} | ${true} | ${false} | ${true} | ${0} | ${false}
+ ${'single request loaded, full request loading'} | ${getSingleRequestLoadedResponse} | ${getInProgressResponse} | ${true} | ${false} | ${false} | ${1} | ${false}
+ ${'single request loaded, full request failed'} | ${getSingleRequestLoadedResponse} | ${getErrorResponse} | ${false} | ${false} | ${true} | ${1} | ${false}
+ ${'single request failed, full request loading'} | ${getErrorResponse} | ${getInProgressResponse} | ${true} | ${false} | ${false} | ${0} | ${false}
+ ${'single request failed, full request loaded'} | ${getErrorResponse} | ${getFullRequestLoadedResponse} | ${false} | ${false} | ${false} | ${2} | ${true}
+ ${'single request loaded with no results, full request loading'} | ${getLoadedEmptyResponse} | ${getInProgressResponse} | ${true} | ${false} | ${false} | ${0} | ${false}
+ ${'single request loading, full request loadied with no results'} | ${getInProgressResponse} | ${getLoadedEmptyResponse} | ${false} | ${true} | ${false} | ${0} | ${false}
+ `(
+ '$description',
+ ({
+ singleResponseFn,
+ fullResponseFn,
+ loadingIndicator,
+ emptyState,
+ flashMessage,
+ releaseCount,
+ pagination,
+ }) => {
+ beforeEach(() => {
+ createComponent({
+ singleResponse: singleResponseFn(),
+ fullResponse: fullResponseFn(),
+ });
+ });
+
+ it(`${toDescription(loadingIndicator)} render a loading indicator`, () => {
+ expect(findLoadingIndicator().exists()).toBe(loadingIndicator);
+ });
+
+ it(`${toDescription(emptyState)} render an empty state`, () => {
+ expect(findEmptyState().exists()).toBe(emptyState);
+ });
+
+ it(`${toDescription(flashMessage)} show a flash message`, () => {
+ if (flashMessage) {
+ expect(createFlash).toHaveBeenCalledWith({
+ message: ReleasesIndexApolloClientApp.i18n.errorMessage,
+ captureError: true,
+ error: expect.any(Error),
+ });
+ } else {
+ expect(createFlash).not.toHaveBeenCalled();
+ }
+ });
+
+ it(`renders ${releaseCount} release(s)`, () => {
+ expect(findAllReleaseBlocks()).toHaveLength(releaseCount);
+ });
+
+ it(`${toDescription(pagination)} render the pagination controls`, () => {
+ expect(findPagination().exists()).toBe(pagination);
+ });
+
+ it('does render the "New release" button', () => {
+ expect(findNewReleaseButton().exists()).toBe(true);
+ });
+
+ it('does render the sort controls', () => {
+ expect(findSort().exists()).toBe(true);
+ });
+ },
+ );
+ });
+
+ describe('URL parameters', () => {
+ describe('when the URL contains no query parameters', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('makes a request with the correct GraphQL query parameters', () => {
+ expect(queryMock).toHaveBeenCalledTimes(2);
+
+ expect(queryMock).toHaveBeenCalledWith({
+ first: 1,
+ fullPath: projectPath,
+ sort: DEFAULT_SORT,
+ });
+
+ expect(queryMock).toHaveBeenCalledWith({
+ first: PAGE_SIZE,
+ fullPath: projectPath,
+ sort: DEFAULT_SORT,
+ });
+ });
+ });
+
+ describe('when the URL contains a "before" query parameter', () => {
+ beforeEach(() => {
+ mockQueryParams = { before };
+ createComponent();
+ });
+
+ it('makes a request with the correct GraphQL query parameters', () => {
+ expect(queryMock).toHaveBeenCalledTimes(1);
+
+ expect(queryMock).toHaveBeenCalledWith({
+ before,
+ last: PAGE_SIZE,
+ fullPath: projectPath,
+ sort: DEFAULT_SORT,
+ });
+ });
+ });
+
+ describe('when the URL contains an "after" query parameter', () => {
+ beforeEach(() => {
+ mockQueryParams = { after };
+ createComponent();
+ });
+
+ it('makes a request with the correct GraphQL query parameters', () => {
+ expect(queryMock).toHaveBeenCalledTimes(2);
+
+ expect(queryMock).toHaveBeenCalledWith({
+ after,
+ first: 1,
+ fullPath: projectPath,
+ sort: DEFAULT_SORT,
+ });
+
+ expect(queryMock).toHaveBeenCalledWith({
+ after,
+ first: PAGE_SIZE,
+ fullPath: projectPath,
+ sort: DEFAULT_SORT,
+ });
+ });
+ });
+
+ describe('when the URL contains both "before" and "after" query parameters', () => {
+ beforeEach(() => {
+ mockQueryParams = { before, after };
+ createComponent();
+ });
+
+ it('ignores the "before" parameter and behaves as if only the "after" parameter was provided', () => {
+ expect(queryMock).toHaveBeenCalledTimes(2);
+
+ expect(queryMock).toHaveBeenCalledWith({
+ after,
+ first: 1,
+ fullPath: projectPath,
+ sort: DEFAULT_SORT,
+ });
+
+ expect(queryMock).toHaveBeenCalledWith({
+ after,
+ first: PAGE_SIZE,
+ fullPath: projectPath,
+ sort: DEFAULT_SORT,
+ });
+ });
+ });
+ });
+
+ describe('New release button', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders the new release button with the correct href', () => {
+ expect(findNewReleaseButton().attributes().href).toBe(newReleasePath);
+ });
+ });
+
+ describe('pagination', () => {
+ beforeEach(() => {
+ mockQueryParams = { before };
+ createComponent();
+ });
+
+ it('requeries the GraphQL endpoint when a pagination button is clicked', async () => {
+ expect(queryMock.mock.calls).toEqual([[expect.objectContaining({ before })]]);
+
+ mockQueryParams = { after };
+ findPagination().vm.$emit('next', after);
+
+ await wrapper.vm.$nextTick();
+
+ expect(queryMock.mock.calls).toEqual([
+ [expect.objectContaining({ before })],
+ [expect.objectContaining({ after })],
+ [expect.objectContaining({ after })],
+ ]);
+ });
+ });
+
+ describe('sorting', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it(`sorts by ${DEFAULT_SORT} by default`, () => {
+ expect(queryMock.mock.calls).toEqual([
+ [expect.objectContaining({ sort: DEFAULT_SORT })],
+ [expect.objectContaining({ sort: DEFAULT_SORT })],
+ ]);
+ });
+
+ it('requeries the GraphQL endpoint and updates the URL when the sort is changed', async () => {
+ findSort().vm.$emit('input', CREATED_ASC);
+
+ await wrapper.vm.$nextTick();
+
+ expect(queryMock.mock.calls).toEqual([
+ [expect.objectContaining({ sort: DEFAULT_SORT })],
+ [expect.objectContaining({ sort: DEFAULT_SORT })],
+ [expect.objectContaining({ sort: CREATED_ASC })],
+ [expect.objectContaining({ sort: CREATED_ASC })],
+ ]);
+
+ // URL manipulation is tested in more detail in the `describe` block below
+ expect(historyPushState).toHaveBeenCalled();
+ });
+
+ it('does not requery the GraphQL endpoint or update the URL if the sort is updated to the same value', async () => {
+ findSort().vm.$emit('input', DEFAULT_SORT);
+
+ await wrapper.vm.$nextTick();
+
+ expect(queryMock.mock.calls).toEqual([
+ [expect.objectContaining({ sort: DEFAULT_SORT })],
+ [expect.objectContaining({ sort: DEFAULT_SORT })],
+ ]);
+
+ expect(historyPushState).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('sorting + pagination interaction', () => {
+ const nonPaginationQueryParam = 'nonPaginationQueryParam';
+
+ beforeEach(() => {
+ historyPushState.mockImplementation((newUrl) => {
+ mockQueryParams = Object.fromEntries(new URL(newUrl).searchParams);
+ });
+ });
+
+ describe.each`
+ queryParamsBefore | paramName | paramInitialValue
+ ${{ before, nonPaginationQueryParam }} | ${'before'} | ${before}
+ ${{ after, nonPaginationQueryParam }} | ${'after'} | ${after}
+ `(
+ 'when the URL contains a "$paramName" pagination cursor',
+ ({ queryParamsBefore, paramName, paramInitialValue }) => {
+ beforeEach(async () => {
+ mockQueryParams = queryParamsBefore;
+ createComponent();
+
+ findSort().vm.$emit('input', CREATED_ASC);
+
+ await wrapper.vm.$nextTick();
+ });
+
+ it(`resets the page's "${paramName}" pagination cursor when the sort is changed`, () => {
+ const firstRequestVariables = queryMock.mock.calls[0][0];
+ // Might be request #2 or #3, depending on the pagination direction
+ const mostRecentRequestVariables =
+ queryMock.mock.calls[queryMock.mock.calls.length - 1][0];
+
+ expect(firstRequestVariables[paramName]).toBe(paramInitialValue);
+ expect(mostRecentRequestVariables[paramName]).toBeUndefined();
+ });
+
+ it(`updates the URL to not include the "${paramName}" URL query parameter`, () => {
+ expect(historyPushState).toHaveBeenCalledTimes(1);
+
+ const updatedUrlQueryParams = Object.fromEntries(
+ new URL(historyPushState.mock.calls[0][0]).searchParams,
+ );
+
+ expect(updatedUrlQueryParams[paramName]).toBeUndefined();
+ });
+ },
+ );
+ });
+});