diff options
Diffstat (limited to 'spec/frontend/releases/components')
7 files changed, 476 insertions, 942 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 deleted file mode 100644 index 9881ef9bc9f..00000000000 --- a/spec/frontend/releases/components/app_index_apollo_client_spec.js +++ /dev/null @@ -1,398 +0,0 @@ -import { cloneDeep } from 'lodash'; -import Vue, { nextTick } from 'vue'; -import VueApollo from 'vue-apollo'; -import originalAllReleasesQueryResponse from 'test_fixtures/graphql/releases/graphql/queries/all_releases.query.graphql.json'; -import createMockApollo from 'helpers/mock_apollo_helper'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import waitForPromises from 'helpers/wait_for_promises'; -import allReleasesQuery from 'shared_queries/releases/all_releases.query.graphql'; -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'; - -Vue.use(VueApollo); - -jest.mock('~/flash'); - -let mockQueryParams; -jest.mock('~/lib/utils/common_utils', () => ({ - ...jest.requireActual('~/lib/utils/common_utils'), - historyPushState: jest.fn(), -})); - -jest.mock('~/lib/utils/url_utility', () => ({ - ...jest.requireActual('~/lib/utils/url_utility'), - getParameterByName: jest - .fn() - .mockImplementation((parameterName) => mockQueryParams[parameterName]), -})); - -describe('app_index_apollo_client.vue', () => { - 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`, async () => { - await waitForPromises(); - 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 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 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 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 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(); - }); - }, - ); - }); -}); diff --git a/spec/frontend/releases/components/app_index_spec.js b/spec/frontend/releases/components/app_index_spec.js index 43e88650ae3..63ce4c8bb17 100644 --- a/spec/frontend/releases/components/app_index_spec.js +++ b/spec/frontend/releases/components/app_index_spec.js @@ -1,50 +1,87 @@ -import { shallowMount } from '@vue/test-utils'; -import { merge } from 'lodash'; -import Vue from 'vue'; -import Vuex from 'vuex'; -import { getParameterByName } from '~/lib/utils/url_utility'; -import AppIndex from '~/releases/components/app_index.vue'; +import { cloneDeep } from 'lodash'; +import Vue, { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; +import originalAllReleasesQueryResponse from 'test_fixtures/graphql/releases/graphql/queries/all_releases.query.graphql.json'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import allReleasesQuery from '~/releases/graphql/queries/all_releases.query.graphql'; +import createFlash from '~/flash'; +import { historyPushState } from '~/lib/utils/common_utils'; +import ReleasesIndexApp from '~/releases/components/app_index.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 ReleasesPagination from '~/releases/components/releases_pagination.vue'; import ReleasesSort from '~/releases/components/releases_sort.vue'; +import { PAGE_SIZE, CREATED_ASC, DEFAULT_SORT } from '~/releases/constants'; + +Vue.use(VueApollo); + +jest.mock('~/flash'); + +let mockQueryParams; +jest.mock('~/lib/utils/common_utils', () => ({ + ...jest.requireActual('~/lib/utils/common_utils'), + historyPushState: jest.fn(), +})); jest.mock('~/lib/utils/url_utility', () => ({ ...jest.requireActual('~/lib/utils/url_utility'), - getParameterByName: jest.fn(), + getParameterByName: jest + .fn() + .mockImplementation((parameterName) => mockQueryParams[parameterName]), })); -Vue.use(Vuex); - describe('app_index.vue', () => { + const projectPath = 'project/path'; + const newReleasePath = 'path/to/new/release/page'; + const before = 'beforeCursor'; + const after = 'afterCursor'; + let wrapper; - let fetchReleasesSpy; - let urlParams; - - const createComponent = (storeUpdates) => { - wrapper = shallowMount(AppIndex, { - store: new Vuex.Store({ - modules: { - index: merge( - { - namespaced: true, - actions: { - fetchReleases: fetchReleasesSpy, - }, - state: { - isLoading: true, - releases: [], - }, - }, - storeUpdates, - ), - }, - }), + 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(ReleasesIndexApp, { + apolloProvider, + provide: { + newReleasePath, + projectPath, + }, }); }; beforeEach(() => { - fetchReleasesSpy = jest.fn(); - getParameterByName.mockImplementation((paramName) => urlParams[paramName]); + 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(() => { @@ -52,120 +89,221 @@ describe('app_index.vue', () => { }); // Finders - const findLoadingIndicator = () => wrapper.find(ReleaseSkeletonLoader); - const findEmptyState = () => wrapper.find('[data-testid="empty-state"]'); - const findSuccessState = () => wrapper.find('[data-testid="success-state"]'); - const findPagination = () => wrapper.find(ReleasesPagination); - const findSortControls = () => wrapper.find(ReleasesSort); - const findNewReleaseButton = () => wrapper.find('[data-testid="new-release-button"]'); - - // Expectations - const expectLoadingIndicator = (shouldExist) => { - it(`${shouldExist ? 'renders' : 'does not render'} a loading indicator`, () => { - expect(findLoadingIndicator().exists()).toBe(shouldExist); - }); - }; + const findLoadingIndicator = () => wrapper.findComponent(ReleaseSkeletonLoader); + const findEmptyState = () => wrapper.findComponent(ReleasesEmptyState); + const findNewReleaseButton = () => wrapper.findByText(ReleasesIndexApp.i18n.newRelease); + const findAllReleaseBlocks = () => wrapper.findAllComponents(ReleaseBlock); + const findPagination = () => wrapper.findComponent(ReleasesPagination); + const findSort = () => wrapper.findComponent(ReleasesSort); - const expectEmptyState = (shouldExist) => { - it(`${shouldExist ? 'renders' : 'does not render'} an empty state`, () => { - expect(findEmptyState().exists()).toBe(shouldExist); - }); - }; + // 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`, async () => { + await waitForPromises(); + expect(findLoadingIndicator().exists()).toBe(loadingIndicator); + }); + + it(`${toDescription(emptyState)} render an empty state`, () => { + expect(findEmptyState().exists()).toBe(emptyState); + }); + + it(`${toDescription(flashMessage)} show a flash message`, async () => { + await waitForPromises(); + if (flashMessage) { + expect(createFlash).toHaveBeenCalledWith({ + message: ReleasesIndexApp.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); + }); + }, + ); + }); - const expectSuccessState = (shouldExist) => { - it(`${shouldExist ? 'renders' : 'does not render'} the success state`, () => { - expect(findSuccessState().exists()).toBe(shouldExist); - }); - }; + describe('URL parameters', () => { + describe('when the URL contains no query parameters', () => { + beforeEach(() => { + createComponent(); + }); - const expectPagination = (shouldExist) => { - it(`${shouldExist ? 'renders' : 'does not render'} the pagination controls`, () => { - expect(findPagination().exists()).toBe(shouldExist); - }); - }; + it('makes a request with the correct GraphQL query parameters', () => { + expect(queryMock).toHaveBeenCalledTimes(2); - const expectNewReleaseButton = (shouldExist) => { - it(`${shouldExist ? 'renders' : 'does not render'} the "New release" button`, () => { - expect(findNewReleaseButton().exists()).toBe(shouldExist); - }); - }; + expect(queryMock).toHaveBeenCalledWith({ + first: 1, + fullPath: projectPath, + sort: DEFAULT_SORT, + }); - // Tests - describe('on startup', () => { - it.each` - before | after - ${null} | ${null} - ${'before_param_value'} | ${null} - ${null} | ${'after_param_value'} - `( - 'calls fetchRelease with the correct parameters based on the curent query parameters: before: $before, after: $after', - ({ before, after }) => { - urlParams = { before, after }; + expect(queryMock).toHaveBeenCalledWith({ + first: PAGE_SIZE, + fullPath: projectPath, + sort: DEFAULT_SORT, + }); + }); + }); + describe('when the URL contains a "before" query parameter', () => { + beforeEach(() => { + mockQueryParams = { before }; createComponent(); + }); - expect(fetchReleasesSpy).toHaveBeenCalledTimes(1); - expect(fetchReleasesSpy).toHaveBeenCalledWith(expect.anything(), urlParams); - }, - ); - }); + it('makes a request with the correct GraphQL query parameters', () => { + expect(queryMock).toHaveBeenCalledTimes(1); - describe('when the request to fetch releases has not yet completed', () => { - beforeEach(() => { - createComponent(); + expect(queryMock).toHaveBeenCalledWith({ + before, + last: PAGE_SIZE, + fullPath: projectPath, + sort: DEFAULT_SORT, + }); + }); }); - expectLoadingIndicator(true); - expectEmptyState(false); - expectSuccessState(false); - expectPagination(false); - }); + describe('when the URL contains an "after" query parameter', () => { + beforeEach(() => { + mockQueryParams = { after }; + createComponent(); + }); - describe('when the request fails', () => { - beforeEach(() => { - createComponent({ - state: { - isLoading: false, - hasError: true, - }, + 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, + }); }); }); - expectLoadingIndicator(false); - expectEmptyState(false); - expectSuccessState(false); - expectPagination(true); + 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('when the request succeeds but returns no releases', () => { + describe('New release button', () => { beforeEach(() => { - createComponent({ - state: { - isLoading: false, - }, - }); + createComponent(); }); - expectLoadingIndicator(false); - expectEmptyState(true); - expectSuccessState(false); - expectPagination(true); + it('renders the new release button with the correct href', () => { + expect(findNewReleaseButton().attributes().href).toBe(newReleasePath); + }); }); - describe('when the request succeeds and includes at least one release', () => { + describe('pagination', () => { beforeEach(() => { - createComponent({ - state: { - isLoading: false, - releases: [{}], - }, - }); + mockQueryParams = { before }; + createComponent(); }); - expectLoadingIndicator(false); - expectEmptyState(false); - expectSuccessState(true); - expectPagination(true); + 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 nextTick(); + + expect(queryMock.mock.calls).toEqual([ + [expect.objectContaining({ before })], + [expect.objectContaining({ after })], + [expect.objectContaining({ after })], + ]); + }); }); describe('sorting', () => { @@ -173,59 +311,88 @@ describe('app_index.vue', () => { createComponent(); }); - it('renders the sort controls', () => { - expect(findSortControls().exists()).toBe(true); + it(`sorts by ${DEFAULT_SORT} by default`, () => { + expect(queryMock.mock.calls).toEqual([ + [expect.objectContaining({ sort: DEFAULT_SORT })], + [expect.objectContaining({ sort: DEFAULT_SORT })], + ]); }); - it('calls the fetchReleases store method when the sort is updated', () => { - fetchReleasesSpy.mockClear(); + it('requeries the GraphQL endpoint and updates the URL when the sort is changed', async () => { + findSort().vm.$emit('input', CREATED_ASC); + + await nextTick(); - findSortControls().vm.$emit('sort:changed'); + 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 })], + ]); - expect(fetchReleasesSpy).toHaveBeenCalledTimes(1); + // URL manipulation is tested in more detail in the `describe` block below + expect(historyPushState).toHaveBeenCalled(); }); - }); - describe('"New release" button', () => { - describe('when the user is allowed to create releases', () => { - const newReleasePath = 'path/to/new/release/page'; + 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); - beforeEach(() => { - createComponent({ state: { newReleasePath } }); - }); + await nextTick(); - expectNewReleaseButton(true); + expect(queryMock.mock.calls).toEqual([ + [expect.objectContaining({ sort: DEFAULT_SORT })], + [expect.objectContaining({ sort: DEFAULT_SORT })], + ]); - it('renders the button with the correct href', () => { - expect(findNewReleaseButton().attributes('href')).toBe(newReleasePath); - }); + expect(historyPushState).not.toHaveBeenCalled(); }); + }); - describe('when the user is not allowed to create releases', () => { - beforeEach(() => { - createComponent(); - }); + describe('sorting + pagination interaction', () => { + const nonPaginationQueryParam = 'nonPaginationQueryParam'; - expectNewReleaseButton(false); + beforeEach(() => { + historyPushState.mockImplementation((newUrl) => { + mockQueryParams = Object.fromEntries(new URL(newUrl).searchParams); + }); }); - }); - describe("when the browser's back button is pressed", () => { - beforeEach(() => { - urlParams = { - before: 'before_param_value', - }; + 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(); - createComponent(); + findSort().vm.$emit('input', CREATED_ASC); - fetchReleasesSpy.mockClear(); + await nextTick(); + }); - window.dispatchEvent(new PopStateEvent('popstate')); - }); + 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]; - it('calls the fetchRelease store method with the parameters from the URL query', () => { - expect(fetchReleasesSpy).toHaveBeenCalledTimes(1); - expect(fetchReleasesSpy).toHaveBeenCalledWith(expect.anything(), urlParams); - }); + 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(); + }); + }, + ); }); }); diff --git a/spec/frontend/releases/components/app_show_spec.js b/spec/frontend/releases/components/app_show_spec.js index 41c9746a363..c2ea6900d6e 100644 --- a/spec/frontend/releases/components/app_show_spec.js +++ b/spec/frontend/releases/components/app_show_spec.js @@ -143,6 +143,12 @@ describe('Release show component', () => { describe('when the request succeeded, but the returned "project.release" key was null', () => { beforeEach(async () => { + // As we return a release as `null`, Apollo also throws an error to the console + // about the missing field. We need to suppress console.error in order to check + // that flash message was called + + // eslint-disable-next-line no-console + console.error = jest.fn(); const apolloProvider = createMockApollo([ [ oneReleaseQuery, diff --git a/spec/frontend/releases/components/releases_pagination_apollo_client_spec.js b/spec/frontend/releases/components/releases_pagination_apollo_client_spec.js deleted file mode 100644 index a538afd5d38..00000000000 --- a/spec/frontend/releases/components/releases_pagination_apollo_client_spec.js +++ /dev/null @@ -1,126 +0,0 @@ -import { mountExtended } from 'helpers/vue_test_utils_helper'; -import { historyPushState } from '~/lib/utils/common_utils'; -import ReleasesPaginationApolloClient from '~/releases/components/releases_pagination_apollo_client.vue'; - -jest.mock('~/lib/utils/common_utils', () => ({ - ...jest.requireActual('~/lib/utils/common_utils'), - historyPushState: jest.fn(), -})); - -describe('releases_pagination_apollo_client.vue', () => { - const startCursor = 'startCursor'; - const endCursor = 'endCursor'; - let wrapper; - let onPrev; - let onNext; - - const createComponent = (pageInfo) => { - onPrev = jest.fn(); - onNext = jest.fn(); - - wrapper = mountExtended(ReleasesPaginationApolloClient, { - propsData: { - pageInfo, - }, - listeners: { - prev: onPrev, - next: onNext, - }, - }); - }; - - afterEach(() => { - wrapper.destroy(); - }); - - const singlePageInfo = { - hasPreviousPage: false, - hasNextPage: false, - startCursor, - endCursor, - }; - - const onlyNextPageInfo = { - hasPreviousPage: false, - hasNextPage: true, - startCursor, - endCursor, - }; - - const onlyPrevPageInfo = { - hasPreviousPage: true, - hasNextPage: false, - startCursor, - endCursor, - }; - - const prevAndNextPageInfo = { - hasPreviousPage: true, - hasNextPage: true, - startCursor, - endCursor, - }; - - const findPrevButton = () => wrapper.findByTestId('prevButton'); - const findNextButton = () => wrapper.findByTestId('nextButton'); - - describe.each` - description | pageInfo | prevEnabled | nextEnabled - ${'when there is only one page of results'} | ${singlePageInfo} | ${false} | ${false} - ${'when there is a next page, but not a previous page'} | ${onlyNextPageInfo} | ${false} | ${true} - ${'when there is a previous page, but not a next page'} | ${onlyPrevPageInfo} | ${true} | ${false} - ${'when there is both a previous and next page'} | ${prevAndNextPageInfo} | ${true} | ${true} - `('component states', ({ description, pageInfo, prevEnabled, nextEnabled }) => { - describe(description, () => { - beforeEach(() => { - createComponent(pageInfo); - }); - - it(`renders the "Prev" button as ${prevEnabled ? 'enabled' : 'disabled'}`, () => { - expect(findPrevButton().attributes().disabled).toBe(prevEnabled ? undefined : 'disabled'); - }); - - it(`renders the "Next" button as ${nextEnabled ? 'enabled' : 'disabled'}`, () => { - expect(findNextButton().attributes().disabled).toBe(nextEnabled ? undefined : 'disabled'); - }); - }); - }); - - describe('button behavior', () => { - beforeEach(() => { - createComponent(prevAndNextPageInfo); - }); - - describe('next button behavior', () => { - beforeEach(() => { - findNextButton().trigger('click'); - }); - - it('emits an "next" event with the "after" cursor', () => { - expect(onNext.mock.calls).toEqual([[endCursor]]); - }); - - it('calls historyPushState with the new URL', () => { - expect(historyPushState.mock.calls).toEqual([ - [expect.stringContaining(`?after=${endCursor}`)], - ]); - }); - }); - - describe('prev button behavior', () => { - beforeEach(() => { - findPrevButton().trigger('click'); - }); - - it('emits an "prev" event with the "before" cursor', () => { - expect(onPrev.mock.calls).toEqual([[startCursor]]); - }); - - it('calls historyPushState with the new URL', () => { - expect(historyPushState.mock.calls).toEqual([ - [expect.stringContaining(`?before=${startCursor}`)], - ]); - }); - }); - }); -}); diff --git a/spec/frontend/releases/components/releases_pagination_spec.js b/spec/frontend/releases/components/releases_pagination_spec.js index b8c69b0ea70..59be808c802 100644 --- a/spec/frontend/releases/components/releases_pagination_spec.js +++ b/spec/frontend/releases/components/releases_pagination_spec.js @@ -1,140 +1,94 @@ -import { GlKeysetPagination } from '@gitlab/ui'; -import { mount } from '@vue/test-utils'; -import Vue from 'vue'; -import Vuex from 'vuex'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; import { historyPushState } from '~/lib/utils/common_utils'; import ReleasesPagination from '~/releases/components/releases_pagination.vue'; -import createStore from '~/releases/stores'; -import createIndexModule from '~/releases/stores/modules/index'; jest.mock('~/lib/utils/common_utils', () => ({ ...jest.requireActual('~/lib/utils/common_utils'), historyPushState: jest.fn(), })); -Vue.use(Vuex); - -describe('~/releases/components/releases_pagination.vue', () => { +describe('releases_pagination.vue', () => { + const startCursor = 'startCursor'; + const endCursor = 'endCursor'; let wrapper; - let indexModule; - - const cursors = { - startCursor: 'startCursor', - endCursor: 'endCursor', - }; - - const projectPath = 'my/project'; + let onPrev; + let onNext; const createComponent = (pageInfo) => { - indexModule = createIndexModule({ projectPath }); - - indexModule.state.pageInfo = pageInfo; - - indexModule.actions.fetchReleases = jest.fn(); - - wrapper = mount(ReleasesPagination, { - store: createStore({ - modules: { - index: indexModule, - }, - featureFlags: {}, - }), + onPrev = jest.fn(); + onNext = jest.fn(); + + wrapper = mountExtended(ReleasesPagination, { + propsData: { + pageInfo, + }, + listeners: { + prev: onPrev, + next: onNext, + }, }); }; afterEach(() => { wrapper.destroy(); - wrapper = null; }); - const findGlKeysetPagination = () => wrapper.findComponent(GlKeysetPagination); - const findPrevButton = () => findGlKeysetPagination().find('[data-testid="prevButton"]'); - const findNextButton = () => findGlKeysetPagination().find('[data-testid="nextButton"]'); - - const expectDisabledPrev = () => { - expect(findPrevButton().attributes().disabled).toBe('disabled'); + const singlePageInfo = { + hasPreviousPage: false, + hasNextPage: false, + startCursor, + endCursor, }; - const expectEnabledPrev = () => { - expect(findPrevButton().attributes().disabled).toBe(undefined); + + const onlyNextPageInfo = { + hasPreviousPage: false, + hasNextPage: true, + startCursor, + endCursor, }; - const expectDisabledNext = () => { - expect(findNextButton().attributes().disabled).toBe('disabled'); + + const onlyPrevPageInfo = { + hasPreviousPage: true, + hasNextPage: false, + startCursor, + endCursor, }; - const expectEnabledNext = () => { - expect(findNextButton().attributes().disabled).toBe(undefined); + + const prevAndNextPageInfo = { + hasPreviousPage: true, + hasNextPage: true, + startCursor, + endCursor, }; - describe('when there is only one page of results', () => { - beforeEach(() => { - createComponent({ - hasPreviousPage: false, - hasNextPage: false, + const findPrevButton = () => wrapper.findByTestId('prevButton'); + const findNextButton = () => wrapper.findByTestId('nextButton'); + + describe.each` + description | pageInfo | prevEnabled | nextEnabled + ${'when there is only one page of results'} | ${singlePageInfo} | ${false} | ${false} + ${'when there is a next page, but not a previous page'} | ${onlyNextPageInfo} | ${false} | ${true} + ${'when there is a previous page, but not a next page'} | ${onlyPrevPageInfo} | ${true} | ${false} + ${'when there is both a previous and next page'} | ${prevAndNextPageInfo} | ${true} | ${true} + `('component states', ({ description, pageInfo, prevEnabled, nextEnabled }) => { + describe(description, () => { + beforeEach(() => { + createComponent(pageInfo); }); - }); - - it('does not render a GlKeysetPagination', () => { - expect(findGlKeysetPagination().exists()).toBe(false); - }); - }); - describe('when there is a next page, but not a previous page', () => { - beforeEach(() => { - createComponent({ - hasPreviousPage: false, - hasNextPage: true, + it(`renders the "Prev" button as ${prevEnabled ? 'enabled' : 'disabled'}`, () => { + expect(findPrevButton().attributes().disabled).toBe(prevEnabled ? undefined : 'disabled'); }); - }); - - 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 the "Next" button as ${nextEnabled ? 'enabled' : 'disabled'}`, () => { + expect(findNextButton().attributes().disabled).toBe(nextEnabled ? undefined : 'disabled'); }); }); - - it('renders a enabled "Prev" button', () => { - expectEnabledPrev(); - }); - - it('renders an enabled "Next" button', () => { - expectEnabledNext(); - }); }); describe('button behavior', () => { beforeEach(() => { - createComponent({ - hasPreviousPage: true, - hasNextPage: true, - ...cursors, - }); + createComponent(prevAndNextPageInfo); }); describe('next button behavior', () => { @@ -142,33 +96,29 @@ describe('~/releases/components/releases_pagination.vue', () => { findNextButton().trigger('click'); }); - it('calls fetchReleases with the correct after cursor', () => { - expect(indexModule.actions.fetchReleases.mock.calls).toEqual([ - [expect.anything(), { after: cursors.endCursor }], - ]); + it('emits an "next" event with the "after" cursor', () => { + expect(onNext.mock.calls).toEqual([[endCursor]]); }); it('calls historyPushState with the new URL', () => { expect(historyPushState.mock.calls).toEqual([ - [expect.stringContaining(`?after=${cursors.endCursor}`)], + [expect.stringContaining(`?after=${endCursor}`)], ]); }); }); - describe('previous button behavior', () => { + describe('prev button behavior', () => { beforeEach(() => { findPrevButton().trigger('click'); }); - it('calls fetchReleases with the correct before cursor', () => { - expect(indexModule.actions.fetchReleases.mock.calls).toEqual([ - [expect.anything(), { before: cursors.startCursor }], - ]); + it('emits an "prev" event with the "before" cursor', () => { + expect(onPrev.mock.calls).toEqual([[startCursor]]); }); it('calls historyPushState with the new URL', () => { expect(historyPushState.mock.calls).toEqual([ - [expect.stringContaining(`?before=${cursors.startCursor}`)], + [expect.stringContaining(`?before=${startCursor}`)], ]); }); }); diff --git a/spec/frontend/releases/components/releases_sort_apollo_client_spec.js b/spec/frontend/releases/components/releases_sort_apollo_client_spec.js deleted file mode 100644 index d93a932af01..00000000000 --- a/spec/frontend/releases/components/releases_sort_apollo_client_spec.js +++ /dev/null @@ -1,103 +0,0 @@ -import { GlSorting, GlSortingItem } from '@gitlab/ui'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import ReleasesSortApolloClient from '~/releases/components/releases_sort_apollo_client.vue'; -import { RELEASED_AT_ASC, RELEASED_AT_DESC, CREATED_ASC, CREATED_DESC } from '~/releases/constants'; - -describe('releases_sort_apollo_client.vue', () => { - let wrapper; - - const createComponent = (valueProp = RELEASED_AT_ASC) => { - wrapper = shallowMountExtended(ReleasesSortApolloClient, { - propsData: { - value: valueProp, - }, - stubs: { - GlSortingItem, - }, - }); - }; - - afterEach(() => { - wrapper.destroy(); - }); - - const findSorting = () => wrapper.findComponent(GlSorting); - const findSortingItems = () => wrapper.findAllComponents(GlSortingItem); - const findReleasedDateItem = () => - findSortingItems().wrappers.find((item) => item.text() === 'Released date'); - const findCreatedDateItem = () => - findSortingItems().wrappers.find((item) => item.text() === 'Created date'); - const getSortingItemsInfo = () => - findSortingItems().wrappers.map((item) => ({ - label: item.text(), - active: item.attributes().active === 'true', - })); - - describe.each` - valueProp | text | isAscending | items - ${RELEASED_AT_ASC} | ${'Released date'} | ${true} | ${[{ label: 'Released date', active: true }, { label: 'Created date', active: false }]} - ${RELEASED_AT_DESC} | ${'Released date'} | ${false} | ${[{ label: 'Released date', active: true }, { label: 'Created date', active: false }]} - ${CREATED_ASC} | ${'Created date'} | ${true} | ${[{ label: 'Released date', active: false }, { label: 'Created date', active: true }]} - ${CREATED_DESC} | ${'Created date'} | ${false} | ${[{ label: 'Released date', active: false }, { label: 'Created date', active: true }]} - `('component states', ({ valueProp, text, isAscending, items }) => { - beforeEach(() => { - createComponent(valueProp); - }); - - it(`when the sort is ${valueProp}, provides the GlSorting with the props text="${text}" and isAscending=${isAscending}`, () => { - expect(findSorting().props()).toEqual( - expect.objectContaining({ - text, - isAscending, - }), - ); - }); - - it(`when the sort is ${valueProp}, renders the expected dropdown items`, () => { - expect(getSortingItemsInfo()).toEqual(items); - }); - }); - - const clickReleasedDateItem = () => findReleasedDateItem().vm.$emit('click'); - const clickCreatedDateItem = () => findCreatedDateItem().vm.$emit('click'); - const clickSortDirectionButton = () => findSorting().vm.$emit('sortDirectionChange'); - - const releasedAtDropdownItemDescription = 'released at dropdown item'; - const createdAtDropdownItemDescription = 'created at dropdown item'; - const sortDirectionButtonDescription = 'sort direction button'; - - describe.each` - initialValueProp | itemClickFn | itemToClickDescription | emittedEvent - ${RELEASED_AT_ASC} | ${clickReleasedDateItem} | ${releasedAtDropdownItemDescription} | ${undefined} - ${RELEASED_AT_ASC} | ${clickCreatedDateItem} | ${createdAtDropdownItemDescription} | ${CREATED_ASC} - ${RELEASED_AT_ASC} | ${clickSortDirectionButton} | ${sortDirectionButtonDescription} | ${RELEASED_AT_DESC} - ${RELEASED_AT_DESC} | ${clickReleasedDateItem} | ${releasedAtDropdownItemDescription} | ${undefined} - ${RELEASED_AT_DESC} | ${clickCreatedDateItem} | ${createdAtDropdownItemDescription} | ${CREATED_DESC} - ${RELEASED_AT_DESC} | ${clickSortDirectionButton} | ${sortDirectionButtonDescription} | ${RELEASED_AT_ASC} - ${CREATED_ASC} | ${clickReleasedDateItem} | ${releasedAtDropdownItemDescription} | ${RELEASED_AT_ASC} - ${CREATED_ASC} | ${clickCreatedDateItem} | ${createdAtDropdownItemDescription} | ${undefined} - ${CREATED_ASC} | ${clickSortDirectionButton} | ${sortDirectionButtonDescription} | ${CREATED_DESC} - ${CREATED_DESC} | ${clickReleasedDateItem} | ${releasedAtDropdownItemDescription} | ${RELEASED_AT_DESC} - ${CREATED_DESC} | ${clickCreatedDateItem} | ${createdAtDropdownItemDescription} | ${undefined} - ${CREATED_DESC} | ${clickSortDirectionButton} | ${sortDirectionButtonDescription} | ${CREATED_ASC} - `('input event', ({ initialValueProp, itemClickFn, itemToClickDescription, emittedEvent }) => { - beforeEach(() => { - createComponent(initialValueProp); - itemClickFn(); - }); - - it(`emits ${ - emittedEvent || 'nothing' - } when value prop is ${initialValueProp} and the ${itemToClickDescription} is clicked`, () => { - expect(wrapper.emitted().input?.[0]?.[0]).toEqual(emittedEvent); - }); - }); - - describe('prop validation', () => { - it('validates that the `value` prop is one of the expected sort strings', () => { - expect(() => { - createComponent('not a valid value'); - }).toThrow('Invalid prop: custom validator check failed'); - }); - }); -}); diff --git a/spec/frontend/releases/components/releases_sort_spec.js b/spec/frontend/releases/components/releases_sort_spec.js index 7774532bc12..c6e1846d252 100644 --- a/spec/frontend/releases/components/releases_sort_spec.js +++ b/spec/frontend/releases/components/releases_sort_spec.js @@ -1,65 +1,103 @@ import { GlSorting, GlSortingItem } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; -import Vuex from 'vuex'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import ReleasesSort from '~/releases/components/releases_sort.vue'; -import createStore from '~/releases/stores'; -import createIndexModule from '~/releases/stores/modules/index'; +import { RELEASED_AT_ASC, RELEASED_AT_DESC, CREATED_ASC, CREATED_DESC } from '~/releases/constants'; -Vue.use(Vuex); - -describe('~/releases/components/releases_sort.vue', () => { +describe('releases_sort.vue', () => { let wrapper; - let store; - let indexModule; - const projectId = 8; - - const createComponent = () => { - indexModule = createIndexModule({ projectId }); - store = createStore({ - modules: { - index: indexModule, + const createComponent = (valueProp = RELEASED_AT_ASC) => { + wrapper = shallowMountExtended(ReleasesSort, { + propsData: { + value: valueProp, }, - }); - - store.dispatch = jest.fn(); - - wrapper = shallowMount(ReleasesSort, { - store, stubs: { GlSortingItem, }, }); }; - const findReleasesSorting = () => wrapper.find(GlSorting); - const findSortingItems = () => wrapper.findAll(GlSortingItem); - afterEach(() => { wrapper.destroy(); - wrapper = null; }); - beforeEach(() => { - createComponent(); - }); + const findSorting = () => wrapper.findComponent(GlSorting); + const findSortingItems = () => wrapper.findAllComponents(GlSortingItem); + const findReleasedDateItem = () => + findSortingItems().wrappers.find((item) => item.text() === 'Released date'); + const findCreatedDateItem = () => + findSortingItems().wrappers.find((item) => item.text() === 'Created date'); + const getSortingItemsInfo = () => + findSortingItems().wrappers.map((item) => ({ + label: item.text(), + active: item.attributes().active === 'true', + })); + + describe.each` + valueProp | text | isAscending | items + ${RELEASED_AT_ASC} | ${'Released date'} | ${true} | ${[{ label: 'Released date', active: true }, { label: 'Created date', active: false }]} + ${RELEASED_AT_DESC} | ${'Released date'} | ${false} | ${[{ label: 'Released date', active: true }, { label: 'Created date', active: false }]} + ${CREATED_ASC} | ${'Created date'} | ${true} | ${[{ label: 'Released date', active: false }, { label: 'Created date', active: true }]} + ${CREATED_DESC} | ${'Created date'} | ${false} | ${[{ label: 'Released date', active: false }, { label: 'Created date', active: true }]} + `('component states', ({ valueProp, text, isAscending, items }) => { + beforeEach(() => { + createComponent(valueProp); + }); - it('has all the sortable items', () => { - expect(findSortingItems()).toHaveLength(wrapper.vm.sortOptions.length); + it(`when the sort is ${valueProp}, provides the GlSorting with the props text="${text}" and isAscending=${isAscending}`, () => { + expect(findSorting().props()).toEqual( + expect.objectContaining({ + text, + isAscending, + }), + ); + }); + + it(`when the sort is ${valueProp}, renders the expected dropdown items`, () => { + expect(getSortingItemsInfo()).toEqual(items); + }); }); - it('on sort change set sorting in vuex and emit event', () => { - findReleasesSorting().vm.$emit('sortDirectionChange'); - expect(store.dispatch).toHaveBeenCalledWith('index/setSorting', { sort: 'asc' }); - expect(wrapper.emitted('sort:changed')).toBeTruthy(); + const clickReleasedDateItem = () => findReleasedDateItem().vm.$emit('click'); + const clickCreatedDateItem = () => findCreatedDateItem().vm.$emit('click'); + const clickSortDirectionButton = () => findSorting().vm.$emit('sortDirectionChange'); + + const releasedAtDropdownItemDescription = 'released at dropdown item'; + const createdAtDropdownItemDescription = 'created at dropdown item'; + const sortDirectionButtonDescription = 'sort direction button'; + + describe.each` + initialValueProp | itemClickFn | itemToClickDescription | emittedEvent + ${RELEASED_AT_ASC} | ${clickReleasedDateItem} | ${releasedAtDropdownItemDescription} | ${undefined} + ${RELEASED_AT_ASC} | ${clickCreatedDateItem} | ${createdAtDropdownItemDescription} | ${CREATED_ASC} + ${RELEASED_AT_ASC} | ${clickSortDirectionButton} | ${sortDirectionButtonDescription} | ${RELEASED_AT_DESC} + ${RELEASED_AT_DESC} | ${clickReleasedDateItem} | ${releasedAtDropdownItemDescription} | ${undefined} + ${RELEASED_AT_DESC} | ${clickCreatedDateItem} | ${createdAtDropdownItemDescription} | ${CREATED_DESC} + ${RELEASED_AT_DESC} | ${clickSortDirectionButton} | ${sortDirectionButtonDescription} | ${RELEASED_AT_ASC} + ${CREATED_ASC} | ${clickReleasedDateItem} | ${releasedAtDropdownItemDescription} | ${RELEASED_AT_ASC} + ${CREATED_ASC} | ${clickCreatedDateItem} | ${createdAtDropdownItemDescription} | ${undefined} + ${CREATED_ASC} | ${clickSortDirectionButton} | ${sortDirectionButtonDescription} | ${CREATED_DESC} + ${CREATED_DESC} | ${clickReleasedDateItem} | ${releasedAtDropdownItemDescription} | ${RELEASED_AT_DESC} + ${CREATED_DESC} | ${clickCreatedDateItem} | ${createdAtDropdownItemDescription} | ${undefined} + ${CREATED_DESC} | ${clickSortDirectionButton} | ${sortDirectionButtonDescription} | ${CREATED_ASC} + `('input event', ({ initialValueProp, itemClickFn, itemToClickDescription, emittedEvent }) => { + beforeEach(() => { + createComponent(initialValueProp); + itemClickFn(); + }); + + it(`emits ${ + emittedEvent || 'nothing' + } when value prop is ${initialValueProp} and the ${itemToClickDescription} is clicked`, () => { + expect(wrapper.emitted().input?.[0]?.[0]).toEqual(emittedEvent); + }); }); - it('on sort item click set sorting and emit event', () => { - const item = findSortingItems().at(0); - const { orderBy } = wrapper.vm.sortOptions[0]; - item.vm.$emit('click'); - expect(store.dispatch).toHaveBeenCalledWith('index/setSorting', { orderBy }); - expect(wrapper.emitted('sort:changed')).toBeTruthy(); + describe('prop validation', () => { + it('validates that the `value` prop is one of the expected sort strings', () => { + expect(() => { + createComponent('not a valid value'); + }).toThrow('Invalid prop: custom validator check failed'); + }); }); }); |