Skip to content

Commit 7d1d6e5

Browse files
authored
feat: run filtered debug tests in runner (#25265)
Co-authored-by: Emily Rohrbough <emilyrohrbough@users.noreply.github.com> Co-authored-by: Mark Noonan <mark@cypress.io> Co-authored-by: Mike Plummer <mikep@cypress.io> Closes #24855
1 parent f9d81a8 commit 7d1d6e5

File tree

30 files changed

+487
-35
lines changed

30 files changed

+487
-35
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
describe('cloud debug test filtering', () => {
2+
beforeEach(() => {
3+
cy.scaffoldProject('cloud-debug-filter')
4+
cy.openProject('cloud-debug-filter')
5+
cy.startAppServer('e2e')
6+
})
7+
8+
it('works with nested suites', () => {
9+
cy.visitApp(`specs/runner?file=cypress/e2e/test.cy.js`)
10+
11+
cy.waitForSpecToFinish()
12+
13+
cy.withCtx((ctx) => {
14+
ctx.coreData.cloud.testsForRunResults = ['t2']
15+
})
16+
17+
cy.visitApp(`specs/runner?file=cypress/e2e/test.cy.js&runId=123`)
18+
cy.waitForSpecToFinish({ passCount: 0, failCount: 1 })
19+
20+
cy.get('.runnable-title').contains('t2')
21+
22+
cy.get('.debug-dismiss').contains('1 / 4 tests').click()
23+
cy.waitForSpecToFinish({ passCount: 2, failCount: 2 })
24+
25+
cy.withCtx((ctx) => {
26+
ctx.coreData.cloud.testsForRunResults = ['s1 t4']
27+
})
28+
29+
cy.visitApp(`specs/runner?file=cypress/e2e/test.cy.js&runId=123`)
30+
cy.waitForSpecToFinish({ passCount: 0, failCount: 1 })
31+
32+
cy.get('.runnable-title').contains('t4')
33+
})
34+
35+
it('wraps filter UI with large number of tests', () => {
36+
cy.visitApp(`specs/runner?file=cypress/e2e/lots-of-tests.cy.js`)
37+
38+
cy.get('[data-cy="reporter-panel"]').as('reporterPanel')
39+
40+
cy.waitForSpecToFinish()
41+
42+
cy.withCtx((ctx) => {
43+
ctx.coreData.cloud.testsForRunResults = ['test1']
44+
})
45+
46+
cy.visitApp(`specs/runner?file=cypress/e2e/lots-of-tests.cy.js&runId=123`)
47+
cy.waitForSpecToFinish({ passCount: 50 })
48+
49+
cy.get('@reporterPanel').then((el) => el.width(500))
50+
cy.get('@reporterPanel').percySnapshot('wide')
51+
52+
cy.get('@reporterPanel').then((el) => el.width(350))
53+
cy.get('@reporterPanel').percySnapshot('medium')
54+
55+
cy.get('@reporterPanel').then((el) => el.width(250))
56+
cy.get('@reporterPanel').percySnapshot('narrow')
57+
58+
cy.get('@reporterPanel').then((el) => el.width(150))
59+
cy.get('@reporterPanel').percySnapshot('skinny')
60+
})
61+
62+
it('works with skips and onlys', () => {
63+
cy.visitApp(`specs/runner?file=cypress/e2e/skip-and-only.cy.js`)
64+
65+
cy.waitForSpecToFinish({ passCount: 0, failCount: 1 })
66+
67+
// .only is respected
68+
cy.withCtx((ctx) => {
69+
ctx.coreData.cloud.testsForRunResults = ['t1', 't3']
70+
})
71+
72+
cy.visitApp(`specs/runner?file=cypress/e2e/skip-and-only.cy.js&runId=123`)
73+
cy.waitForSpecToFinish({ passCount: 0, failCount: 1 })
74+
75+
cy.get('.runnable-title').contains('t1')
76+
77+
cy.get('.debug-dismiss').click().waitForSpecToFinish()
78+
79+
// .only is ignored as it is not in set of filtered tests
80+
cy.withCtx((ctx) => {
81+
ctx.coreData.cloud.testsForRunResults = ['t3']
82+
})
83+
84+
cy.visitApp(`specs/runner?file=cypress/e2e/skip-and-only.cy.js&runId=123`)
85+
cy.waitForSpecToFinish({ passCount: 0, failCount: 1 })
86+
87+
cy.get('.runnable-title').contains('t3')
88+
89+
cy.get('.debug-dismiss').click().waitForSpecToFinish()
90+
91+
// .skip is respected
92+
cy.withCtx((ctx) => {
93+
ctx.coreData.cloud.testsForRunResults = ['t2', 't3']
94+
})
95+
96+
cy.visitApp(`specs/runner?file=cypress/e2e/skip-and-only.cy.js&runId=123`)
97+
cy.waitForSpecToFinish({ passCount: 0, failCount: 1, pendingCount: 1 })
98+
cy.get('.runnable-title').first().contains('t2')
99+
cy.get('.runnable-title').last().contains('t3')
100+
101+
cy.get('.debug-dismiss').contains('2 / 4 tests').click().waitForSpecToFinish()
102+
103+
// suite.only is respected
104+
cy.withCtx((ctx) => {
105+
ctx.coreData.cloud.testsForRunResults = ['t3', 's1 t4']
106+
})
107+
108+
cy.visitApp(`specs/runner?file=cypress/e2e/skip-and-only.cy.js&runId=123`)
109+
cy.waitForSpecToFinish({ passCount: 0, failCount: 1 })
110+
cy.get('.runnable-title').contains('t4')
111+
})
112+
113+
it('works with browser filter', () => {
114+
cy.withCtx((ctx) => {
115+
ctx.coreData.cloud.testsForRunResults = ['t1', 's1 t2']
116+
})
117+
118+
cy.visitApp(`specs/runner?file=cypress/e2e/browsers.cy.js&runId=123`)
119+
120+
cy.get('.runnable-title').eq(0).contains('t1 (skipped due to browser)')
121+
cy.get('.runnable-title').eq(1).contains('s1 (skipped due to browser)')
122+
cy.get('.runnable-title').eq(2).contains('t2')
123+
})
124+
125+
it('filter is maintained across cross-domain reinitialization', () => {
126+
cy.visitApp(`specs/runner?file=cypress/e2e/domain-change.cy.js`)
127+
128+
cy.get('[data-cy="reporter-panel"]').as('reporterPanel')
129+
130+
cy.waitForSpecToFinish()
131+
132+
cy.withCtx((ctx) => {
133+
ctx.coreData.cloud.testsForRunResults = ['t2', 't3']
134+
})
135+
136+
cy.visitApp(`specs/runner?file=cypress/e2e/domain-change.cy.js&runId=123`)
137+
cy.waitForSpecToFinish({ failCount: 2 })
138+
})
139+
})

packages/app/src/runner/event-manager-types.ts

+4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export type LocalBusEventMap = {
1919
'before:screenshot': BeforeScreenshot
2020
'after:screenshot': undefined
2121
'open:file': FileDetails
22+
'testFilter:cloudDebug:dismiss': undefined
2223
}
2324

2425
export interface StudioSavePayload {
@@ -46,6 +47,9 @@ export type LocalBusEmitsMap = {
4647
'reporter:log:add': CommandLog
4748
'reporter:log:remove': CommandLog
4849
'reporter:log:state:changed': CommandLog
50+
51+
// Test filter events
52+
'testFilter:cloudDebug:dismiss': undefined
4953
}
5054

5155
export type SocketToDriverMap = {

packages/app/src/runner/event-manager.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { logger } from './logger'
1010
import type { Socket } from '@packages/socket/lib/browser'
1111
import { automation, useRunnerUiStore } from '../store'
1212
import { useScreenshotStore } from '../store/screenshot-store'
13+
import { useSpecStore } from '../store/specs-store'
1314
import { useStudioStore } from '../store/studio-store'
1415
import { getAutIframeModel } from '.'
1516
import { handlePausing } from './events/pausing'
@@ -56,6 +57,7 @@ export class EventManager {
5657
cypressInCypressMochaEvents: CypressInCypressMochaEvent[] = []
5758
// Used for testing the experimentalSingleTabRunMode experiment. Ensures AUT is correctly destroyed between specs.
5859
ws: Socket
60+
specStore: ReturnType<typeof useSpecStore>
5961
studioStore: ReturnType<typeof useStudioStore>
6062

6163
constructor (
@@ -69,6 +71,7 @@ export class EventManager {
6971
) {
7072
this.selectorPlaygroundModel = selectorPlaygroundModel
7173
this.ws = ws
74+
this.specStore = useSpecStore()
7275
this.studioStore = useStudioStore()
7376
}
7477

@@ -236,6 +239,10 @@ export class EventManager {
236239
this.saveState(state)
237240
})
238241

242+
this.reporterBus.on('testFilter:cloudDebug:dismiss', () => {
243+
this.emit('testFilter:cloudDebug:dismiss', undefined)
244+
})
245+
239246
this.reporterBus.on('clear:all:sessions', () => {
240247
if (!Cypress) return
241248

@@ -379,6 +386,8 @@ export class EventManager {
379386
initialize ($autIframe: JQuery<HTMLIFrameElement>, config: Record<string, any>) {
380387
performance.mark('initialize-start')
381388

389+
const testFilter = this.specStore.testFilter
390+
382391
return Cypress.initialize({
383392
$autIframe,
384393
onSpecReady: () => {
@@ -394,7 +403,7 @@ export class EventManager {
394403

395404
this.studioStore.initialize(config, runState)
396405

397-
const runnables = Cypress.runner.normalizeAll(runState.tests, hideCommandLog)
406+
const runnables = Cypress.runner.normalizeAll(runState.tests, hideCommandLog, testFilter)
398407

399408
const run = () => {
400409
performance.mark('initialize-end')

packages/app/src/runner/index.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { AutIframe } from './aut-iframe'
2424
import { EventManager } from './event-manager'
2525
import { createWebsocket as createWebsocketIo } from '@packages/socket/lib/browser'
2626
import { decodeBase64Unicode } from '@packages/frontend-shared/src/utils/base64'
27-
import type { AutomationElementId } from '@packages/types/src'
27+
import type { AutomationElementId } from '@packages/types'
2828
import { useSnapshotStore } from './snapshot-store'
2929
import { useStudioStore } from '../store/studio-store'
3030

@@ -374,7 +374,7 @@ async function initialize () {
374374
* 2. Reset the Reporter. We use the same instance of the Reporter,
375375
* but reset the internal state each time we run a spec.
376376
*
377-
* 3. Teardown spec. This does a few things, primaily stopping the current
377+
* 3. Teardown spec. This does a few things, primarily stopping the current
378378
* spec run, which involves stopping the driver and runner.
379379
*
380380
* 4. Force the Reporter to re-render with the new spec we are executed.

packages/app/src/runner/reporter.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { getMobxRunnerStore, MobxRunnerStore } from '../store'
1+
import { getMobxRunnerStore, MobxRunnerStore, useSpecStore } from '../store'
22
import { getReporterElement } from './utils'
33
import { getEventManager, getRunnerConfigFromWindow } from '.'
44
import type { EventManager } from './event-manager'
@@ -38,6 +38,7 @@ function renderReporter (
3838
eventManager: EventManager,
3939
) {
4040
const runnerUiStore = useRunnerUiStore()
41+
const specsStore = useSpecStore()
4142

4243
const config = getRunnerConfigFromWindow()
4344

@@ -51,6 +52,7 @@ function renderReporter (
5152
// Studio can only be enabled for e2e testing
5253
studioEnabled: window.__CYPRESS_TESTING_TYPE__ === 'e2e' && config.experimentalStudio,
5354
runnerStore: store,
55+
testFilter: specsStore.testFilter,
5456
})
5557

5658
window.UnifiedRunner.ReactDOM.render(reporter, root)

packages/app/src/runner/unifiedRunner.ts

+36-9
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,23 @@ import { getAutIframeModel, UnifiedRunnerAPI } from '../runner'
33
import { useSpecStore } from '../store'
44
import { useSelectorPlaygroundStore } from '../store/selector-playground-store'
55
import { RUN_ALL_SPECS, RUN_ALL_SPECS_KEY, SpecFile } from '@packages/types/src'
6-
import { useRoute } from 'vue-router'
6+
import { LocationQuery, useRoute } from 'vue-router'
77
import { getPathForPlatform } from '../paths'
8+
import { isEqual } from 'lodash'
9+
import { gql, useMutation } from '@urql/vue'
10+
import { TestsForRunDocument } from '../generated/graphql'
11+
12+
gql`
13+
mutation TestsForRun ($runId: String!) {
14+
testsForRun (runId: $runId)
15+
}
16+
`
817

918
const initialized = ref(false)
1019

1120
export function useUnifiedRunner () {
21+
let prevQuery: LocationQuery
22+
1223
onMounted(async () => {
1324
await UnifiedRunnerAPI.initialize()
1425
initialized.value = true
@@ -25,8 +36,9 @@ export function useUnifiedRunner () {
2536
const specStore = useSpecStore()
2637
const route = useRoute()
2738
const selectorPlaygroundStore = useSelectorPlaygroundStore()
39+
const testsForRunMutation = useMutation(TestsForRunDocument)
2840

29-
watchEffect(() => {
41+
watchEffect(async () => {
3042
const queryFile = getPathForPlatform(route.query.file as string)
3143

3244
if (!queryFile) {
@@ -35,17 +47,32 @@ export function useUnifiedRunner () {
3547
return
3648
}
3749

38-
if (queryFile === RUN_ALL_SPECS_KEY) {
39-
return specStore.setActiveSpec(RUN_ALL_SPECS)
50+
const activeSpecInSpecsList = queryFile === RUN_ALL_SPECS_KEY
51+
? RUN_ALL_SPECS
52+
: specs.value.find((x) => x.relative === queryFile)
53+
54+
if (isEqual(route.query, prevQuery) && isEqual(activeSpecInSpecsList, specStore.activeSpec)) {
55+
return
4056
}
4157

42-
const activeSpecInSpecsList = specs.value.find((x) => x.relative === queryFile)
58+
prevQuery = route.query
4359

44-
if (activeSpecInSpecsList && specStore.activeSpec?.relative !== activeSpecInSpecsList.relative) {
45-
specStore.setActiveSpec(activeSpecInSpecsList)
46-
} else if (!activeSpecInSpecsList) {
47-
specStore.setActiveSpec(null)
60+
if (!activeSpecInSpecsList) {
61+
return specStore.setActiveSpec(null)
4862
}
63+
64+
if (route.query.runId) {
65+
const res = await testsForRunMutation.executeMutation({ runId: route.query.runId as string })
66+
67+
specStore.setTestFilter(res.data?.testsForRun?.length ? res.data.testsForRun : undefined)
68+
} else {
69+
specStore.setTestFilter(undefined)
70+
}
71+
72+
// Either there is a new spec or the runId has changed. Returning a new object will kick off a new run.
73+
// A user could be looking at a given run when a new run is kicked off by CI or another user.
74+
// They can view the newer run using an in-UI transition which updates the runId and kicks off new queries.
75+
specStore.setActiveSpec({ ...activeSpecInSpecsList })
4976
})
5077

5178
watch(() => getPathForPlatform(route.query.file as string), () => {

packages/app/src/runner/useEventManager.ts

+11
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { watch } from 'vue'
2+
import { useRouter } from 'vue-router'
23
import { addCrossOriginIframe, getAutIframeModel, getEventManager, UnifiedRunnerAPI } from '.'
34
import { useAutStore, useSpecStore } from '../store'
45
import { useStudioStore } from '../store/studio-store'
@@ -10,6 +11,7 @@ export function useEventManager () {
1011
const autStore = useAutStore()
1112
const specStore = useSpecStore()
1213
const studioStore = useStudioStore()
14+
const router = useRouter()
1315

1416
function runSpec (isRerun: boolean = false) {
1517
if (!specStore.activeSpec) {
@@ -58,6 +60,15 @@ export function useEventManager () {
5860
})
5961

6062
eventManager.on('expect:origin', addCrossOriginIframe)
63+
64+
eventManager.on('testFilter:cloudDebug:dismiss', () => {
65+
const currentRoute = router.currentRoute.value
66+
67+
const { runId, ...query } = currentRoute.query
68+
69+
// Delete runId from query which will remove the test filter and trigger a rerun
70+
router.replace({ ...currentRoute, query })
71+
})
6172
}
6273

6374
const startSpecWatcher = () => {

packages/app/src/store/specs-store.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
1-
import type { SpecFile } from '@packages/types/src'
1+
import type { SpecFile, TestFilter } from '@packages/types/src'
22
import { defineStore } from 'pinia'
33

44
export interface SpecState {
55
activeSpec: SpecFile | null | undefined
66
specFilter?: string
7+
testFilter: TestFilter
78
}
89

910
export const useSpecStore = defineStore({
1011
id: 'spec',
11-
1212
state (): SpecState {
1313
return {
1414
activeSpec: undefined,
1515
specFilter: undefined,
16+
testFilter: undefined,
1617
}
1718
},
1819

@@ -23,5 +24,8 @@ export const useSpecStore = defineStore({
2324
setSpecFilter (filter: string) {
2425
this.specFilter = filter
2526
},
27+
setTestFilter (filter: SpecState['testFilter']) {
28+
this.testFilter = filter
29+
},
2630
},
2731
})

0 commit comments

Comments
 (0)