2021-09-04 01:27:46 +05:30
import { cloneDeep } from 'lodash' ;
import Vue from 'vue' ;
import VueApollo from 'vue-apollo' ;
2021-11-18 22:05:49 +05:30
import originalAllReleasesQueryResponse from 'test_fixtures/graphql/releases/graphql/queries/all_releases.query.graphql.json' ;
2021-09-04 01:27:46 +05:30
import createMockApollo from 'helpers/mock_apollo_helper' ;
import { shallowMountExtended } from 'helpers/vue_test_utils_helper' ;
2021-09-30 23:02:18 +05:30
import allReleasesQuery from 'shared_queries/releases/all_releases.query.graphql' ;
2021-09-04 01:27:46 +05:30
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' ) ,
2021-09-30 23:02:18 +05:30
historyPushState : jest . fn ( ) ,
} ) ) ;
jest . mock ( '~/lib/utils/url_utility' , ( ) => ( {
... jest . requireActual ( '~/lib/utils/url_utility' ) ,
2021-09-04 01:27:46 +05:30
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 ` , ( ) => {
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 ( ) ;
} ) ;
} ,
) ;
} ) ;
} ) ;