Skip to content

Commit 2bfeb53

Browse files
marktnoonanwarrensplayermike-plummer
authored
fix: implement new graphql fields for spec counts (#25757)
Co-authored-by: Stokes Player <stokes@cypress.io> Co-authored-by: Mike Plummer <mike-plummer@users.noreply.github.com>
1 parent 536c905 commit 2bfeb53

File tree

9 files changed

+290
-37
lines changed

9 files changed

+290
-37
lines changed

cli/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ _Released 02/14/2023 (PENDING)_
77

88
- Fixed an issue with the Cloud project selection modal not showing the correct prompts. Fixes [#25520](https://github.com/cypress-io/cypress/issues/25520).
99
- Fixed an issue in middleware where error-handling code could itself generate an error and fail to report the original issue. Fixes [#22825](https://github.com/cypress-io/cypress/issues/22825).
10+
- Fixed an issue that could cause the Debug page to display a different number of specs for in-progress runs than shown in Cypress Cloud. Fixes [#25647](https://github.com/cypress-io/cypress/issues/25647).
1011

1112
**Features:**
1213

packages/app/src/debug/DebugContainer.cy.tsx

+47
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,8 @@ describe('<DebugContainer />', () => {
241241
result.currentProject.cloudProject.runByNumber = {
242242
...CloudRunStubs.running,
243243
runNumber: 1,
244+
completedInstanceCount: 2,
245+
totalInstanceCount: 3,
244246
} as typeof test
245247
}
246248
},
@@ -255,6 +257,51 @@ describe('<DebugContainer />', () => {
255257
})
256258
})
257259

260+
it('does not render DebugPendingRunSplash and DebugNewRelevantRunBar at the same time', () => {
261+
cy.mountFragment(DebugSpecsFragmentDoc, {
262+
variableTypes: DebugSpecVariableTypes,
263+
variables: {
264+
hasNextRun: false,
265+
runNumber: 1,
266+
nextRunNumber: -1,
267+
},
268+
onResult: (result) => {
269+
if (result.currentProject?.cloudProject?.__typename === 'CloudProject') {
270+
const test = result.currentProject.cloudProject.runByNumber
271+
272+
// Testing this to confirm we are "making impossible states impossible" in the UI,
273+
// and document the expectation in this scenario. For clarity,
274+
// we do not expect a 'RUNNING` current and next run at the same time, so
275+
// the data below represents an invalid state.
276+
277+
result.currentProject.cloudProject.runByNumber = {
278+
...CloudRunStubs.running,
279+
runNumber: 1,
280+
completedInstanceCount: 2,
281+
totalInstanceCount: 3,
282+
} as typeof test
283+
284+
result.currentProject.cloudProject.nextRun = {
285+
...CloudRunStubs.running,
286+
runNumber: 1,
287+
completedInstanceCount: 5,
288+
totalInstanceCount: 6,
289+
} as typeof test
290+
}
291+
},
292+
render: (gqlVal) => <DebugContainer gql={gqlVal} />,
293+
})
294+
295+
cy.findByTestId('debug-header').should('be.visible')
296+
cy.findByTestId('debug-pending-splash')
297+
.should('be.visible')
298+
.within(() => {
299+
cy.findByTestId('debug-pending-counts').should('have.text', '0 of 0 specs completed')
300+
})
301+
302+
cy.findByTestId('newer-relevant-run').should('not.exist')
303+
})
304+
258305
it('renders specs and tests when completed run available', () => {
259306
cy.mountFragment(DebugSpecsFragmentDoc, {
260307
variableTypes: DebugSpecVariableTypes,

packages/app/src/debug/DebugContainer.vue

+5-5
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,15 @@
2828
:gql="run"
2929
:commits-ahead="props.commitsAhead"
3030
/>
31-
<DebugNewRelevantRunBar
32-
v-if="newerRelevantRun"
33-
:gql="newerRelevantRun"
34-
/>
35-
3631
<DebugPendingRunSplash
3732
v-if="isFirstPendingRun"
3833
class="mt-12"
3934
/>
35+
<DebugNewRelevantRunBar
36+
v-else-if="newerRelevantRun"
37+
:gql="newerRelevantRun"
38+
/>
39+
4040
<template v-else>
4141
<DebugPageDetails
4242
v-if="shouldDisplayDetails(run.status, run.isHidden)"

packages/data-context/src/sources/RelevantRunSpecsDataSource.ts

+30-31
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import debugLib from 'debug'
44
import { isEqual } from 'lodash'
55

66
import type { DataContext } from '../DataContext'
7-
import type { CloudSpecStatus, Query, RelevantRun, CurrentProjectRelevantRunSpecs, CloudSpecRun, CloudRun } from '../gen/graphcache-config.gen'
7+
import type { Query, RelevantRun, CurrentProjectRelevantRunSpecs, CloudRun } from '../gen/graphcache-config.gen'
88
import { Poller } from '../polling'
99
import type { CloudRunStatus } from '@packages/graphql/src/gen/cloud-source-types.gen'
1010

@@ -15,6 +15,8 @@ const RELEVANT_RUN_SPEC_OPERATION_DOC = gql`
1515
id
1616
runNumber
1717
status
18+
completedInstanceCount
19+
totalInstanceCount
1820
specs {
1921
id
2022
status
@@ -55,8 +57,6 @@ export const SPECS_EMPTY_RETURN: RunSpecReturn = {
5557
statuses: {},
5658
}
5759

58-
const INCOMPLETE_STATUSES: CloudSpecStatus[] = ['RUNNING', 'UNCLAIMED']
59-
6060
export type RunSpecReturn = {
6161
runSpecs: CurrentProjectRelevantRunSpecs
6262
statuses: {
@@ -86,23 +86,9 @@ export class RelevantRunSpecsDataSource {
8686
return this.#cached.runSpecs
8787
}
8888

89-
#calculateSpecMetadata (specs: CloudSpecRun[]) {
90-
//mimic logic in Cloud to sum up the count of groups per spec to give the total spec counts
91-
const countGroupsForSpec = (specs: CloudSpecRun[]) => {
92-
return specs.map((spec) => spec.groupIds?.length || 0).reduce((acc, curr) => acc += curr, 0)
93-
}
94-
95-
return {
96-
totalSpecs: countGroupsForSpec(specs),
97-
completedSpecs: countGroupsForSpec(specs.filter((spec) => !INCOMPLETE_STATUSES.includes(spec.status || 'UNCLAIMED'))),
98-
}
99-
}
100-
10189
/**
102-
* Pulls runs from the current Cypress Cloud account and determines which runs are considered:
103-
* - "current" the most recent completed run, or if not found, the most recent running run
104-
* - "next" the most recent running run if a completed run is found
105-
* @param shas list of Git commit shas to query the Cloud with for matching runs
90+
* Pulls the specs that match the relevant run.
91+
* @param runs - the current and (optionally) next relevant run
10692
*/
10793
async getRelevantRunSpecs (runs: RelevantRun): Promise<RunSpecReturn> {
10894
const projectSlug = await this.ctx.project.projectId()
@@ -147,28 +133,40 @@ export class RelevantRunSpecsDataSource {
147133
}
148134
}
149135

136+
function isValidNumber (value: unknown): value is number {
137+
return Number.isFinite(value)
138+
}
139+
150140
if (cloudProject?.__typename === 'CloudProject') {
151141
const runSpecsToReturn: RunSpecReturn = {
152142
runSpecs: {},
153143
statuses: {},
154144
}
155145

156-
if (cloudProject.current && cloudProject.current.runNumber && cloudProject.current.status) {
157-
runSpecsToReturn.runSpecs.current = {
158-
...this.#calculateSpecMetadata(cloudProject.current.specs || []),
159-
runNumber: cloudProject.current.runNumber,
146+
const { current, next } = cloudProject
147+
148+
const formatCloudRunInfo = (cloudRunDetails: Partial<CloudRun>) => {
149+
const { runNumber, totalInstanceCount, completedInstanceCount } = cloudRunDetails
150+
151+
if (runNumber && isValidNumber(totalInstanceCount) && isValidNumber(completedInstanceCount)) {
152+
return {
153+
totalSpecs: totalInstanceCount,
154+
completedSpecs: completedInstanceCount,
155+
runNumber,
156+
}
160157
}
161158

162-
runSpecsToReturn.statuses.current = cloudProject.current.status
159+
return undefined
163160
}
164161

165-
if (cloudProject.next && cloudProject.next.runNumber && cloudProject.next.status) {
166-
runSpecsToReturn.runSpecs.next = {
167-
...this.#calculateSpecMetadata(cloudProject.next.specs || []),
168-
runNumber: cloudProject.next.runNumber,
169-
}
162+
if (current && current.status) {
163+
runSpecsToReturn.runSpecs.current = formatCloudRunInfo(current)
164+
runSpecsToReturn.statuses.current = current.status
165+
}
170166

171-
runSpecsToReturn.statuses.next = cloudProject.next.status
167+
if (next && next.status) {
168+
runSpecsToReturn.runSpecs.next = formatCloudRunInfo(next)
169+
runSpecsToReturn.statuses.next = next.status
172170
}
173171

174172
return runSpecsToReturn
@@ -193,6 +191,7 @@ export class RelevantRunSpecsDataSource {
193191

194192
debug(`Spec data is `, specs)
195193

194+
const wasWatchingCurrentProject = this.#cached.statuses.current === 'RUNNING'
196195
const specCountsChanged = !isEqual(specs.runSpecs, this.#cached.runSpecs)
197196
const statusesChanged = !isEqual(specs.statuses, this.#cached.statuses)
198197

@@ -208,7 +207,7 @@ export class RelevantRunSpecsDataSource {
208207
debug('Run statuses changed')
209208
const projectSlug = await this.ctx.project.projectId()
210209

211-
if (projectSlug) {
210+
if (projectSlug && wasWatchingCurrentProject) {
212211
debug(`Invalidate cloudProjectBySlug ${projectSlug}`)
213212
await this.ctx.cloud.invalidate('Query', 'cloudProjectBySlug', { slug: projectSlug })
214213
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { expect } from 'chai'
2+
import sinon from 'sinon'
3+
4+
import { DataContext } from '../../../src'
5+
import { createTestDataContext } from '../helper'
6+
import { RelevantRunSpecsDataSource, SPECS_EMPTY_RETURN } from '../../../src/sources'
7+
import { FAKE_PROJECT_ONE_RUNNING_RUN_ONE_COMPLETED_THREE_SPECS, FAKE_PROJECT_ONE_RUNNING_RUN_ONE_SPEC } from './fixtures/graphqlFixtures'
8+
9+
describe('RelevantRunSpecsDataSource', () => {
10+
let ctx: DataContext
11+
let dataSource: RelevantRunSpecsDataSource
12+
13+
beforeEach(() => {
14+
ctx = createTestDataContext('open')
15+
dataSource = new RelevantRunSpecsDataSource(ctx)
16+
sinon.stub(ctx.project, 'projectId').resolves('test123')
17+
})
18+
19+
describe('getRelevantRunSpecs()', () => {
20+
it('returns no specs or statuses when no specs found for run', async () => {
21+
const result = await dataSource.getRelevantRunSpecs({ current: 11111, next: 22222, commitsAhead: 0 })
22+
23+
expect(result).to.eql(SPECS_EMPTY_RETURN)
24+
})
25+
26+
it('returns expected specs and statuses when one run is found', async () => {
27+
sinon.stub(ctx.cloud, 'executeRemoteGraphQL').resolves(FAKE_PROJECT_ONE_RUNNING_RUN_ONE_SPEC)
28+
29+
const result = await dataSource.getRelevantRunSpecs({ current: 1, next: null, commitsAhead: 0 })
30+
31+
expect(result).to.eql({
32+
runSpecs: {
33+
current: {
34+
runNumber: 1,
35+
completedSpecs: 1,
36+
totalSpecs: 1,
37+
},
38+
},
39+
statuses: { current: 'RUNNING' },
40+
})
41+
})
42+
43+
it('returns expected specs and statuses when one run is completed and one is running', async () => {
44+
sinon.stub(ctx.cloud, 'executeRemoteGraphQL').resolves(FAKE_PROJECT_ONE_RUNNING_RUN_ONE_COMPLETED_THREE_SPECS)
45+
46+
const result = await dataSource.getRelevantRunSpecs({ current: 1, next: null, commitsAhead: 0 })
47+
48+
expect(result).to.eql({
49+
runSpecs: {
50+
current: {
51+
runNumber: 1,
52+
completedSpecs: 3,
53+
totalSpecs: 3,
54+
},
55+
next: {
56+
runNumber: 2,
57+
completedSpecs: 0,
58+
totalSpecs: 3,
59+
},
60+
},
61+
statuses: {
62+
current: 'PASSED',
63+
next: 'RUNNING',
64+
},
65+
})
66+
})
67+
})
68+
})

packages/data-context/test/unit/sources/fixtures/graphqlFixtures.ts

+37-1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,42 @@ export const FAKE_PROJECT_NO_RUNS = { data: { cloudProjectBySlug: { __typename:
4949

5050
export const FAKE_PROJECT_ONE_RUNNING_RUN = { data: { cloudProjectBySlug: { __typename: 'CloudProject', runsByCommitShas: [{ runNumber: 1, status: 'RUNNING', commitInfo: { sha: FAKE_SHAS[0] } }] } } }
5151

52-
export const FAKE_PROJECT_MULTIPLE_COMPLETED = { data: { cloudProjectBySlug: { __typename: 'CloudProject', runsByCommitShas: [{ runNumber: 4, status: 'FAILED', commitInfo: { sha: FAKE_SHAS[1] } }, { runNumber: 1, status: 'PASSED', commitInfo: { sha: FAKE_SHAS[0] } }] } } }
52+
export const FAKE_PROJECT_MULTIPLE_COMPLETED = { data: { cloudProjectBySlug: { __typename: 'CloudProject', runsByCommitShas: [
53+
{ runNumber: 4, status: 'FAILED', commitInfo: { sha: FAKE_SHAS[1] } }, { runNumber: 1, status: 'PASSED', commitInfo: { sha: FAKE_SHAS[0] } },
54+
] } } }
5355

5456
export const FAKE_PROJECT_MULTIPLE_COMPLETED_PLUS_RUNNING = { data: { cloudProjectBySlug: { __typename: 'CloudProject', runsByCommitShas: [{ runNumber: 5, status: 'RUNNING', commitInfo: { sha: FAKE_SHAS[2] } }, { runNumber: 4, status: 'FAILED', commitInfo: { sha: FAKE_SHAS[1] } }, { runNumber: 1, status: 'PASSED', commitInfo: { sha: FAKE_SHAS[0] } }] } } }
57+
58+
export const FAKE_PROJECT_ONE_RUNNING_RUN_ONE_SPEC = {
59+
data: {
60+
cloudProjectBySlug: {
61+
__typename: 'CloudProject',
62+
current: {
63+
runNumber: 1,
64+
completedInstanceCount: 1,
65+
totalInstanceCount: 1,
66+
status: 'RUNNING',
67+
},
68+
},
69+
},
70+
}
71+
72+
export const FAKE_PROJECT_ONE_RUNNING_RUN_ONE_COMPLETED_THREE_SPECS = {
73+
data: {
74+
cloudProjectBySlug: {
75+
__typename: 'CloudProject',
76+
current: {
77+
runNumber: 1,
78+
status: 'PASSED',
79+
completedInstanceCount: 3,
80+
totalInstanceCount: 3,
81+
},
82+
next: {
83+
runNumber: 2,
84+
status: 'RUNNING',
85+
completedInstanceCount: 0,
86+
totalInstanceCount: 3,
87+
},
88+
},
89+
},
90+
}

0 commit comments

Comments
 (0)