2022-06-21 17:19:12 +05:30
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' ;
2023-07-09 08:55:56 +05:30
import { createAlert , VARIANT _SUCCESS } from '~/alert' ;
2022-06-21 17:19:12 +05:30
import { historyPushState } from '~/lib/utils/common_utils' ;
import ReleasesIndexApp from '~/releases/components/app_index.vue' ;
import ReleaseBlock from '~/releases/components/release_block.vue' ;
2021-06-08 01:23:25 +05:30
import ReleaseSkeletonLoader from '~/releases/components/release_skeleton_loader.vue' ;
2022-06-21 17:19:12 +05:30
import ReleasesEmptyState from '~/releases/components/releases_empty_state.vue' ;
2021-03-11 19:13:27 +05:30
import ReleasesPagination from '~/releases/components/releases_pagination.vue' ;
2021-06-08 01:23:25 +05:30
import ReleasesSort from '~/releases/components/releases_sort.vue' ;
2022-06-21 17:19:12 +05:30
import { PAGE _SIZE , CREATED _ASC , DEFAULT _SORT } from '~/releases/constants' ;
2023-07-09 08:55:56 +05:30
import { deleteReleaseSessionKey } from '~/releases/release_notification_service' ;
2022-06-21 17:19:12 +05:30
Vue . use ( VueApollo ) ;
2023-05-27 22:25:52 +05:30
jest . mock ( '~/alert' ) ;
2022-06-21 17:19:12 +05:30
let mockQueryParams ;
jest . mock ( '~/lib/utils/common_utils' , ( ) => ( {
... jest . requireActual ( '~/lib/utils/common_utils' ) ,
historyPushState : jest . fn ( ) ,
} ) ) ;
2021-01-03 14:25:43 +05:30
2021-09-30 23:02:18 +05:30
jest . mock ( '~/lib/utils/url_utility' , ( ) => ( {
... jest . requireActual ( '~/lib/utils/url_utility' ) ,
2022-06-21 17:19:12 +05:30
getParameterByName : jest
. fn ( )
. mockImplementation ( ( parameterName ) => mockQueryParams [ parameterName ] ) ,
2021-01-03 14:25:43 +05:30
} ) ) ;
2020-11-24 15:15:51 +05:30
2021-06-08 01:23:25 +05:30
describe ( 'app_index.vue' , ( ) => {
2022-06-21 17:19:12 +05:30
const projectPath = 'project/path' ;
const newReleasePath = 'path/to/new/release/page' ;
const before = 'beforeCursor' ;
const after = 'afterCursor' ;
2020-11-24 15:15:51 +05:30
let wrapper ;
2022-06-21 17:19:12 +05:30
let allReleases ;
let singleRelease ;
let noReleases ;
let queryMock ;
2022-08-13 15:12:31 +05:30
let toast ;
2022-06-21 17:19:12 +05:30
const createComponent = ( {
singleResponse = Promise . resolve ( singleRelease ) ,
fullResponse = Promise . resolve ( allReleases ) ,
} = { } ) => {
const apolloProvider = createMockApollo ( [
[
allReleasesQuery ,
queryMock . mockImplementation ( ( vars ) => {
return vars . first === 1 ? singleResponse : fullResponse ;
} ) ,
] ,
] ) ;
2022-08-13 15:12:31 +05:30
toast = jest . fn ( ) ;
2022-06-21 17:19:12 +05:30
wrapper = shallowMountExtended ( ReleasesIndexApp , {
apolloProvider ,
provide : {
newReleasePath ,
projectPath ,
} ,
2022-08-13 15:12:31 +05:30
mocks : {
$toast : { show : toast } ,
} ,
2021-06-08 01:23:25 +05:30
} ) ;
2019-02-15 15:39:39 +05:30
} ;
2021-06-08 01:23:25 +05:30
beforeEach ( ( ) => {
2022-06-21 17:19:12 +05:30
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 ( ) ;
2021-06-08 01:23:25 +05:30
} ) ;
// Finders
2022-06-21 17:19:12 +05:30
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 ) ;
2020-11-24 15:15:51 +05:30
2022-06-21 17:19:12 +05:30
// 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 `
2023-05-27 22:25:52 +05:30
description | singleResponseFn | fullResponseFn | loadingIndicator | emptyState | alertMessage | releaseCount | pagination
2022-06-21 17:19:12 +05:30
$ { '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 ,
2023-05-27 22:25:52 +05:30
alertMessage ,
2022-06-21 17:19:12 +05:30
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 ) ;
} ) ;
2023-05-27 22:25:52 +05:30
it ( ` ${ toDescription ( alertMessage ) } show a flash message ` , async ( ) => {
2022-06-21 17:19:12 +05:30
await waitForPromises ( ) ;
2023-05-27 22:25:52 +05:30
if ( alertMessage ) {
2022-11-25 23:54:43 +05:30
expect ( createAlert ) . toHaveBeenCalledWith ( {
2022-06-21 17:19:12 +05:30
message : ReleasesIndexApp . i18n . errorMessage ,
captureError : true ,
error : expect . any ( Error ) ,
} ) ;
} else {
2022-11-25 23:54:43 +05:30
expect ( createAlert ) . not . toHaveBeenCalled ( ) ;
2022-06-21 17:19:12 +05:30
}
} ) ;
it ( ` renders ${ releaseCount } release(s) ` , ( ) => {
expect ( findAllReleaseBlocks ( ) ) . toHaveLength ( releaseCount ) ;
} ) ;
it ( ` ${ toDescription ( pagination ) } render the pagination controls ` , ( ) => {
expect ( findPagination ( ) . exists ( ) ) . toBe ( pagination ) ;
} ) ;
2023-04-23 21:23:45 +05:30
it ( 'does render the "New release" button only for non-empty state' , ( ) => {
const shouldRenderNewReleaseButton = ! emptyState ;
expect ( findNewReleaseButton ( ) . exists ( ) ) . toBe ( shouldRenderNewReleaseButton ) ;
2022-06-21 17:19:12 +05:30
} ) ;
2023-04-23 21:23:45 +05:30
it ( 'does render the sort controls only for non-empty state' , ( ) => {
const shouldRenderControls = ! emptyState ;
expect ( findSort ( ) . exists ( ) ) . toBe ( shouldRenderControls ) ;
2022-06-21 17:19:12 +05:30
} ) ;
} ,
) ;
} ) ;
2020-11-24 15:15:51 +05:30
2022-06-21 17:19:12 +05:30
describe ( 'URL parameters' , ( ) => {
describe ( 'when the URL contains no query parameters' , ( ) => {
beforeEach ( ( ) => {
createComponent ( ) ;
} ) ;
2020-11-24 15:15:51 +05:30
2022-06-21 17:19:12 +05:30
it ( 'makes a request with the correct GraphQL query parameters' , ( ) => {
expect ( queryMock ) . toHaveBeenCalledTimes ( 2 ) ;
2019-02-15 15:39:39 +05:30
2022-06-21 17:19:12 +05:30
expect ( queryMock ) . toHaveBeenCalledWith ( {
first : 1 ,
fullPath : projectPath ,
sort : DEFAULT _SORT ,
} ) ;
2020-11-24 15:15:51 +05:30
2022-06-21 17:19:12 +05:30
expect ( queryMock ) . toHaveBeenCalledWith ( {
first : PAGE _SIZE ,
fullPath : projectPath ,
sort : DEFAULT _SORT ,
} ) ;
} ) ;
} ) ;
2021-06-08 01:23:25 +05:30
2022-06-21 17:19:12 +05:30
describe ( 'when the URL contains a "before" query parameter' , ( ) => {
beforeEach ( ( ) => {
mockQueryParams = { before } ;
2021-06-08 01:23:25 +05:30
createComponent ( ) ;
2022-06-21 17:19:12 +05:30
} ) ;
2021-06-08 01:23:25 +05:30
2022-06-21 17:19:12 +05:30
it ( 'makes a request with the correct GraphQL query parameters' , ( ) => {
expect ( queryMock ) . toHaveBeenCalledTimes ( 1 ) ;
2020-11-24 15:15:51 +05:30
2022-06-21 17:19:12 +05:30
expect ( queryMock ) . toHaveBeenCalledWith ( {
before ,
last : PAGE _SIZE ,
fullPath : projectPath ,
sort : DEFAULT _SORT ,
} ) ;
} ) ;
2020-11-24 15:15:51 +05:30
} ) ;
2022-06-21 17:19:12 +05:30
describe ( 'when the URL contains an "after" query parameter' , ( ) => {
beforeEach ( ( ) => {
mockQueryParams = { after } ;
createComponent ( ) ;
} ) ;
2019-02-15 15:39:39 +05:30
2022-06-21 17:19:12 +05:30
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 ,
} ) ;
2021-06-08 01:23:25 +05:30
} ) ;
2019-02-15 15:39:39 +05:30
} ) ;
2022-06-21 17:19:12 +05:30
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 ,
} ) ;
} ) ;
} ) ;
2019-02-15 15:39:39 +05:30
} ) ;
2022-06-21 17:19:12 +05:30
describe ( 'New release button' , ( ) => {
2019-02-15 15:39:39 +05:30
beforeEach ( ( ) => {
2022-06-21 17:19:12 +05:30
createComponent ( ) ;
2020-01-01 13:55:28 +05:30
} ) ;
2022-06-21 17:19:12 +05:30
it ( 'renders the new release button with the correct href' , ( ) => {
expect ( findNewReleaseButton ( ) . attributes ( ) . href ) . toBe ( newReleasePath ) ;
} ) ;
2020-01-01 13:55:28 +05:30
} ) ;
2022-06-21 17:19:12 +05:30
describe ( 'pagination' , ( ) => {
2020-01-01 13:55:28 +05:30
beforeEach ( ( ) => {
2022-06-21 17:19:12 +05:30
mockQueryParams = { before } ;
createComponent ( ) ;
2019-02-15 15:39:39 +05:30
} ) ;
2022-06-21 17:19:12 +05:30
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 } ) ] ,
] ) ;
} ) ;
2019-02-15 15:39:39 +05:30
} ) ;
2021-06-08 01:23:25 +05:30
describe ( 'sorting' , ( ) => {
2019-02-15 15:39:39 +05:30
beforeEach ( ( ) => {
2020-11-24 15:15:51 +05:30
createComponent ( ) ;
2019-02-15 15:39:39 +05:30
} ) ;
2022-06-21 17:19:12 +05:30
it ( ` sorts by ${ DEFAULT _SORT } by default ` , ( ) => {
expect ( queryMock . mock . calls ) . toEqual ( [
[ expect . objectContaining ( { sort : DEFAULT _SORT } ) ] ,
[ expect . objectContaining ( { sort : DEFAULT _SORT } ) ] ,
] ) ;
2020-04-08 14:13:33 +05:30
} ) ;
2022-06-21 17:19:12 +05:30
it ( 'requeries the GraphQL endpoint and updates the URL when the sort is changed' , async ( ) => {
findSort ( ) . vm . $emit ( 'input' , CREATED _ASC ) ;
await nextTick ( ) ;
2020-04-08 14:13:33 +05:30
2022-06-21 17:19:12 +05:30
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 } ) ] ,
] ) ;
2021-06-08 01:23:25 +05:30
2022-06-21 17:19:12 +05:30
// URL manipulation is tested in more detail in the `describe` block below
expect ( historyPushState ) . toHaveBeenCalled ( ) ;
2020-04-08 14:13:33 +05:30
} ) ;
2022-06-21 17:19:12 +05:30
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 ) ;
2020-04-08 14:13:33 +05:30
2022-06-21 17:19:12 +05:30
await nextTick ( ) ;
2020-04-08 14:13:33 +05:30
2022-06-21 17:19:12 +05:30
expect ( queryMock . mock . calls ) . toEqual ( [
[ expect . objectContaining ( { sort : DEFAULT _SORT } ) ] ,
[ expect . objectContaining ( { sort : DEFAULT _SORT } ) ] ,
] ) ;
2020-04-08 14:13:33 +05:30
2022-06-21 17:19:12 +05:30
expect ( historyPushState ) . not . toHaveBeenCalled ( ) ;
2020-04-08 14:13:33 +05:30
} ) ;
2022-06-21 17:19:12 +05:30
} ) ;
2020-04-08 14:13:33 +05:30
2022-06-21 17:19:12 +05:30
describe ( 'sorting + pagination interaction' , ( ) => {
const nonPaginationQueryParam = 'nonPaginationQueryParam' ;
2021-06-08 01:23:25 +05:30
2022-06-21 17:19:12 +05:30
beforeEach ( ( ) => {
historyPushState . mockImplementation ( ( newUrl ) => {
mockQueryParams = Object . fromEntries ( new URL ( newUrl ) . searchParams ) ;
} ) ;
2019-02-15 15:39:39 +05:30
} ) ;
2021-01-03 14:25:43 +05:30
2022-06-21 17:19:12 +05:30
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 ( ) ;
2021-01-03 14:25:43 +05:30
2022-06-21 17:19:12 +05:30
findSort ( ) . vm . $emit ( 'input' , CREATED _ASC ) ;
2021-01-03 14:25:43 +05:30
2022-06-21 17:19:12 +05:30
await nextTick ( ) ;
} ) ;
2021-01-03 14:25:43 +05:30
2022-06-21 17:19:12 +05:30
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 ] ;
2021-01-03 14:25:43 +05:30
2022-06-21 17:19:12 +05:30
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 ( ) ;
} ) ;
} ,
) ;
2021-01-03 14:25:43 +05:30
} ) ;
2022-08-13 15:12:31 +05:30
describe ( 'after deleting' , ( ) => {
const release = 'fake release' ;
const key = deleteReleaseSessionKey ( projectPath ) ;
beforeEach ( async ( ) => {
window . sessionStorage . setItem ( key , release ) ;
await createComponent ( ) ;
} ) ;
2023-06-20 00:43:36 +05:30
it ( 'shows a toast' , ( ) => {
2023-07-09 08:55:56 +05:30
expect ( createAlert ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( createAlert ) . toHaveBeenCalledWith ( {
message : ` Release ${ release } has been successfully deleted. ` ,
variant : VARIANT _SUCCESS ,
} ) ;
2022-08-13 15:12:31 +05:30
} ) ;
2023-06-20 00:43:36 +05:30
it ( 'clears session storage' , ( ) => {
2022-08-13 15:12:31 +05:30
expect ( window . sessionStorage . getItem ( key ) ) . toBe ( null ) ;
} ) ;
} ) ;
2019-02-15 15:39:39 +05:30
} ) ;