diff options
Diffstat (limited to 'spec/frontend/releases')
6 files changed, 746 insertions, 15 deletions
diff --git a/spec/frontend/releases/__snapshots__/util_spec.js.snap b/spec/frontend/releases/__snapshots__/util_spec.js.snap index cad593b76ea..e0a1343c39c 100644 --- a/spec/frontend/releases/__snapshots__/util_spec.js.snap +++ b/spec/frontend/releases/__snapshots__/util_spec.js.snap @@ -5,6 +5,58 @@ Object { "data": Array [ Object { "_links": Object { + "closedIssuesUrl": "http://localhost/releases-namespace/releases-project/-/issues?release_tag=v1.2&scope=all&state=closed", + "closedMergeRequestsUrl": "http://localhost/releases-namespace/releases-project/-/merge_requests?release_tag=v1.2&scope=all&state=closed", + "editUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.2/edit", + "mergedMergeRequestsUrl": "http://localhost/releases-namespace/releases-project/-/merge_requests?release_tag=v1.2&scope=all&state=merged", + "openedIssuesUrl": "http://localhost/releases-namespace/releases-project/-/issues?release_tag=v1.2&scope=all&state=opened", + "openedMergeRequestsUrl": "http://localhost/releases-namespace/releases-project/-/merge_requests?release_tag=v1.2&scope=all&state=opened", + "self": "http://localhost/releases-namespace/releases-project/-/releases/v1.2", + "selfUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.2", + }, + "assets": Object { + "count": 4, + "links": Array [], + "sources": Array [ + Object { + "format": "zip", + "url": "http://localhost/releases-namespace/releases-project/-/archive/v1.2/releases-project-v1.2.zip", + }, + Object { + "format": "tar.gz", + "url": "http://localhost/releases-namespace/releases-project/-/archive/v1.2/releases-project-v1.2.tar.gz", + }, + Object { + "format": "tar.bz2", + "url": "http://localhost/releases-namespace/releases-project/-/archive/v1.2/releases-project-v1.2.tar.bz2", + }, + Object { + "format": "tar", + "url": "http://localhost/releases-namespace/releases-project/-/archive/v1.2/releases-project-v1.2.tar", + }, + ], + }, + "author": Object { + "avatarUrl": "https://www.gravatar.com/avatar/16f8e2050ce10180ca571c2eb19cfce2?s=80&d=identicon", + "username": "administrator", + "webUrl": "http://localhost/administrator", + }, + "commit": Object { + "shortId": "b83d6e39", + "title": "Merge branch 'branch-merged' into 'master'", + }, + "commitPath": "http://localhost/releases-namespace/releases-project/-/commit/b83d6e391c22777fca1ed3012fce84f633d7fed0", + "descriptionHtml": "<p data-sourcepos=\\"1:1-1:23\\" dir=\\"auto\\">An okay release <gl-emoji title=\\"shrug\\" data-name=\\"shrug\\" data-unicode-version=\\"9.0\\">🤷</gl-emoji></p>", + "evidences": Array [], + "milestones": Array [], + "name": "The second release", + "releasedAt": "2019-01-10T00:00:00Z", + "tagName": "v1.2", + "tagPath": "/releases-namespace/releases-project/-/tags/v1.2", + "upcomingRelease": true, + }, + Object { + "_links": Object { "closedIssuesUrl": "http://localhost/releases-namespace/releases-project/-/issues?release_tag=v1.1&scope=all&state=closed", "closedMergeRequestsUrl": "http://localhost/releases-namespace/releases-project/-/merge_requests?release_tag=v1.1&scope=all&state=closed", "editUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.1/edit", @@ -121,10 +173,10 @@ Object { }, ], "paginationInfo": Object { - "endCursor": "eyJpZCI6IjEifQ", + "endCursor": "eyJyZWxlYXNlZF9hdCI6IjIwMTgtMTItMTAgMDA6MDA6MDAuMDAwMDAwMDAwIFVUQyIsImlkIjoiMSJ9", "hasNextPage": false, "hasPreviousPage": false, - "startCursor": "eyJpZCI6IjEifQ", + "startCursor": "eyJyZWxlYXNlZF9hdCI6IjIwMTktMDEtMTAgMDA6MDA6MDAuMDAwMDAwMDAwIFVUQyIsImlkIjoiMiJ9", }, } `; 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(); + }); + }, + ); + }); +}); diff --git a/spec/frontend/releases/components/releases_empty_state_spec.js b/spec/frontend/releases/components/releases_empty_state_spec.js new file mode 100644 index 00000000000..495e6d863f7 --- /dev/null +++ b/spec/frontend/releases/components/releases_empty_state_spec.js @@ -0,0 +1,56 @@ +import { GlEmptyState } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import ReleasesEmptyState from '~/releases/components/releases_empty_state.vue'; + +describe('releases_empty_state.vue', () => { + const documentationPath = 'path/to/releases/documentation'; + const illustrationPath = 'path/to/releases/empty/state/illustration'; + + let wrapper; + + const createComponent = () => { + wrapper = shallowMountExtended(ReleasesEmptyState, { + provide: { + documentationPath, + illustrationPath, + }, + }); + }; + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders a GlEmptyState and provides it with the correct props', () => { + const emptyStateProps = wrapper.findComponent(GlEmptyState).props(); + + expect(emptyStateProps).toEqual( + expect.objectContaining({ + title: ReleasesEmptyState.i18n.emptyStateTitle, + svgPath: illustrationPath, + }), + ); + }); + + it('renders the empty state text', () => { + expect(wrapper.findByText(ReleasesEmptyState.i18n.emptyStateText).exists()).toBe(true); + }); + + it('renders a link to the documentation', () => { + const documentationLink = wrapper.findByText(ReleasesEmptyState.i18n.moreInformation); + + expect(documentationLink.exists()).toBe(true); + + expect(documentationLink.attributes()).toEqual( + expect.objectContaining({ + 'aria-label': ReleasesEmptyState.i18n.releasesDocumentation, + href: documentationPath, + target: '_blank', + }), + ); + }); +}); diff --git a/spec/frontend/releases/components/releases_pagination_apollo_client_spec.js b/spec/frontend/releases/components/releases_pagination_apollo_client_spec.js new file mode 100644 index 00000000000..a538afd5d38 --- /dev/null +++ b/spec/frontend/releases/components/releases_pagination_apollo_client_spec.js @@ -0,0 +1,126 @@ +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_sort_apollo_client_spec.js b/spec/frontend/releases/components/releases_sort_apollo_client_spec.js new file mode 100644 index 00000000000..d93a932af01 --- /dev/null +++ b/spec/frontend/releases/components/releases_sort_apollo_client_spec.js @@ -0,0 +1,103 @@ +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/stores/modules/detail/actions_spec.js b/spec/frontend/releases/stores/modules/detail/actions_spec.js index 688ec4c0a50..6504a09df2f 100644 --- a/spec/frontend/releases/stores/modules/detail/actions_spec.js +++ b/spec/frontend/releases/stores/modules/detail/actions_spec.js @@ -1,7 +1,7 @@ import { cloneDeep } from 'lodash'; import { getJSONFixture } from 'helpers/fixtures'; import testAction from 'helpers/vuex_action_helper'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import { redirectTo } from '~/lib/utils/url_utility'; import { ASSET_LINK_TYPE } from '~/releases/constants'; import createReleaseAssetLinkMutation from '~/releases/graphql/mutations/create_release_link.mutation.graphql'; @@ -151,9 +151,9 @@ describe('Release edit/new actions', () => { it(`shows a flash message`, () => { return actions.fetchRelease({ commit: jest.fn(), state, rootState: state }).then(() => { expect(createFlash).toHaveBeenCalledTimes(1); - expect(createFlash).toHaveBeenCalledWith( - 'Something went wrong while getting the release details.', - ); + expect(createFlash).toHaveBeenCalledWith({ + message: 'Something went wrong while getting the release details.', + }); }); }); }); @@ -352,9 +352,9 @@ describe('Release edit/new actions', () => { .createRelease({ commit: jest.fn(), dispatch: jest.fn(), state, getters: {} }) .then(() => { expect(createFlash).toHaveBeenCalledTimes(1); - expect(createFlash).toHaveBeenCalledWith( - 'Something went wrong while creating a new release.', - ); + expect(createFlash).toHaveBeenCalledWith({ + message: 'Something went wrong while creating a new release.', + }); }); }); }); @@ -483,9 +483,9 @@ describe('Release edit/new actions', () => { await actions.updateRelease({ commit, dispatch, state, getters }); expect(createFlash).toHaveBeenCalledTimes(1); - expect(createFlash).toHaveBeenCalledWith( - 'Something went wrong while saving the release details.', - ); + expect(createFlash).toHaveBeenCalledWith({ + message: 'Something went wrong while saving the release details.', + }); }); }); @@ -503,9 +503,9 @@ describe('Release edit/new actions', () => { await actions.updateRelease({ commit, dispatch, state, getters }); expect(createFlash).toHaveBeenCalledTimes(1); - expect(createFlash).toHaveBeenCalledWith( - 'Something went wrong while saving the release details.', - ); + expect(createFlash).toHaveBeenCalledWith({ + message: 'Something went wrong while saving the release details.', + }); }); }; |