Skip to content

Commit 05afa1e

Browse files
feat: Add status icon to Sidebar Runs Page (#27672)
* basic structure and stubs for tests * think this works, needs validation and icon * partially working, needs i18n cleanup for failure count * working with tests and i18n * update to latest status icon * fix debug integration tests * removing ts-expect-error for isWindows util to match 13 branch * add change log * whatever * speculate on release * fix typo in template
1 parent 0089501 commit 05afa1e

File tree

10 files changed

+281
-61
lines changed

10 files changed

+281
-61
lines changed

cli/CHANGELOG.md

+8
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
<!-- See the ../guides/writing-the-cypress-changelog.md for details on writing the changelog. -->
2+
## 13.1.0
3+
4+
_Released 09/12/2023 (PENDING)_
5+
6+
**Features:**
7+
8+
- Introduce a status icon representing the `latest` test run in the Sidebar for the Runs Page. Addresses [#27206](https://github.com/cypress-io/cypress/issues/27206).
9+
210
## 13.0.0
311

412
_Released 08/29/2023_

packages/app/cypress/e2e/debug.cy.ts

+4
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,11 @@ describe('App - Debug Page', () => {
3232

3333
if (obj.operationName === 'Debug_currentProject_cloudProject_cloudProjectBySlug' || obj.operationName === 'SideBarNavigationContainer_currentProject_cloudProject_cloudProjectBySlug') {
3434
if (obj.result.data) {
35+
// Standard Calls
3536
obj.result.data.cloudProjectBySlug.runByNumber = options.DebugDataPassing.data.currentProject.cloudProject.runByNumber
37+
// Aliased Calls
38+
obj.result.data.cloudProjectBySlug.latestRun = options.DebugDataPassing.data.currentProject.cloudProject.runByNumber
39+
obj.result.data.cloudProjectBySlug.selectedRun = options.DebugDataPassing.data.currentProject.cloudProject.runByNumber
3640
}
3741
}
3842

packages/app/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@
2121
"dependencies": {},
2222
"devDependencies": {
2323
"@cypress-design/vue-button": "^0.10.1",
24-
"@cypress-design/vue-icon": "^0.24.3",
25-
"@cypress-design/vue-statusicon": "^0.4.11",
24+
"@cypress-design/vue-icon": "^0.25.0",
25+
"@cypress-design/vue-statusicon": "^0.5.0",
2626
"@cypress-design/vue-tabs": "^0.5.1",
2727
"@graphql-typed-document-node/core": "^3.1.0",
2828
"@headlessui/vue": "1.4.0",

packages/app/src/navigation/SidebarNavigation.cy.tsx

+106-10
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { CloudRunStubs } from '@packages/graphql/test/stubCloudTypes'
55
import { cloneDeep } from 'lodash'
66
import { useUserProjectStatusStore } from '@packages/frontend-shared/src/store/user-project-status-store'
77

8-
function mountComponent (props: { initialNavExpandedVal?: boolean, cloudProject?: { status: CloudRunStatus, numFailedTests: number }, isLoading?: boolean, online?: boolean} = {}) {
8+
function mountComponent (props: { initialNavExpandedVal?: boolean, cloudProject?: { status: CloudRunStatus, numFailedTests: number }, latestCloudProject?: { status: CloudRunStatus, numFailedTests: number }, isLoading?: boolean, online?: boolean} = {}) {
99
const withDefaults = { initialNavExpandedVal: false, isLoading: false, online: true, ...props }
1010
let _gql: SidebarNavigationFragment
1111

@@ -15,23 +15,48 @@ function mountComponent (props: { initialNavExpandedVal?: boolean, cloudProject?
1515
return defineResult({ setPreferences: _gql })
1616
})
1717

18+
const selectedVariables = withDefaults.cloudProject ? {
19+
selectedRunNumber: 1,
20+
hasSelectedRun: true,
21+
} : {
22+
selectedRunNumber: -1,
23+
hasSelectedRun: false,
24+
}
25+
26+
const latestVariables = withDefaults.latestCloudProject ? {
27+
latestRunNumber: 1,
28+
hasLatestRun: true,
29+
} : {
30+
latestRunNumber: -1,
31+
hasLatestRun: false,
32+
}
33+
1834
cy.mountFragment(SidebarNavigationFragmentDoc, {
1935
variableTypes: {
20-
runNumber: 'Int',
21-
hasCurrentRun: 'Boolean',
36+
selectedRunNumber: 'Int',
37+
hasSelectedRun: 'Boolean',
38+
latestRunNumber: 'Int',
39+
hasLatestRun: 'Boolean',
2240
},
2341
variables: {
24-
runNumber: 1,
25-
hasCurrentRun: true,
42+
...selectedVariables,
43+
...latestVariables,
2644
},
2745
onResult (gql) {
2846
if (!gql.currentProject) return
2947

30-
if (gql.currentProject?.cloudProject?.__typename === 'CloudProject' && withDefaults.cloudProject) {
31-
gql.currentProject.cloudProject.runByNumber = cloneDeep(CloudRunStubs.failingWithTests)
32-
gql.currentProject.cloudProject.runByNumber.status = withDefaults.cloudProject.status as CloudRunStatus
33-
34-
gql.currentProject.cloudProject.runByNumber.totalFailed = withDefaults.cloudProject.numFailedTests
48+
if (gql.currentProject?.cloudProject?.__typename === 'CloudProject') {
49+
if (withDefaults.cloudProject) {
50+
gql.currentProject.cloudProject.selectedRun = cloneDeep(CloudRunStubs.failingWithTests)
51+
gql.currentProject.cloudProject.selectedRun.status = withDefaults.cloudProject.status as CloudRunStatus
52+
gql.currentProject.cloudProject.selectedRun.totalFailed = withDefaults.cloudProject.numFailedTests
53+
}
54+
55+
if (withDefaults.latestCloudProject) {
56+
gql.currentProject.cloudProject.latestRun = cloneDeep(CloudRunStubs.failingWithTests)
57+
gql.currentProject.cloudProject.latestRun.status = withDefaults.latestCloudProject.status as CloudRunStatus
58+
gql.currentProject.cloudProject.latestRun.totalFailed = withDefaults.latestCloudProject.numFailedTests
59+
}
3560
} else {
3661
gql.currentProject.cloudProject = null
3762
}
@@ -215,4 +240,75 @@ describe('SidebarNavigation', () => {
215240
cy.findByTestId('debug-badge').should('not.exist')
216241
})
217242
})
243+
244+
context('runs status icon', () => {
245+
it('renders passing status if run status is "RUNNING" with no failures', () => {
246+
mountComponent({ latestCloudProject: { status: 'RUNNING', numFailedTests: 0 } })
247+
cy.findByTestId('icon-status-message').should('be.visible').contains('Latest run is in progress')
248+
cy.percySnapshot('Runs Icon:running')
249+
})
250+
251+
it('renders failing status if run status is "RUNNING" with failures', () => {
252+
mountComponent({ latestCloudProject: { status: 'RUNNING', numFailedTests: 3 } })
253+
cy.findByTestId('icon-status-message').should('be.visible').contains('failing')
254+
cy.percySnapshot('Runs Icon:failing')
255+
})
256+
257+
it('renders success status when status is "PASSED"', () => {
258+
mountComponent({ latestCloudProject: { status: 'PASSED', numFailedTests: 0 } })
259+
cy.findByTestId('icon-status-message').should('be.visible').contains('Latest run passed')
260+
cy.percySnapshot('Runs Icon:passed')
261+
})
262+
263+
it('renders failed status when status is "FAILED"', () => {
264+
mountComponent({ latestCloudProject: { status: 'FAILED', numFailedTests: 1 } })
265+
cy.findByTestId('icon-status-message').should('be.visible').contains('Latest run had 1 test failure')
266+
cy.percySnapshot('Runs Icon:failed')
267+
})
268+
269+
it('renders cancelled status when status is "CANCELLED"', () => {
270+
mountComponent({ latestCloudProject: { status: 'CANCELLED', numFailedTests: 0 } })
271+
cy.findByTestId('icon-status-message').should('be.visible').contains('Latest run has been cancelled')
272+
cy.percySnapshot('Runs Icon:cancelled')
273+
})
274+
275+
it('renders attention status when abnormal status', () => {
276+
for (const status of ['ERRORED', 'NOTESTS', 'OVERLIMIT', 'TIMEDOUT'] as CloudRunStatus[]) {
277+
cy.log(status)
278+
mountComponent({ latestCloudProject: { status, numFailedTests: 0 } })
279+
cy.findByTestId('icon-status-message').should('be.visible').contains('Latest run had an error')
280+
}
281+
282+
cy.percySnapshot('Runs Icon:errored')
283+
})
284+
285+
it('renders attention status when abnormal status and failing tests', () => {
286+
for (const status of ['ERRORED', 'NOTESTS', 'OVERLIMIT', 'TIMEDOUT'] as CloudRunStatus[]) {
287+
cy.log(status)
288+
mountComponent({ latestCloudProject: { status, numFailedTests: 3 } })
289+
cy.findByTestId('icon-status-message').should('be.visible').contains('Latest run had an error with 3 test failures')
290+
}
291+
})
292+
293+
it('renders no status if no cloudProject', () => {
294+
mountComponent()
295+
cy.findByTestId('icon-status-message').should('not.exist')
296+
})
297+
298+
it('renders no status when query is loading', () => {
299+
const userProjectStatusStore = useUserProjectStatusStore()
300+
301+
userProjectStatusStore.setProjectFlag('isProjectConnected', true)
302+
303+
mountComponent({ isLoading: true })
304+
305+
cy.findByTestId('icon-status-message').should('not.exist')
306+
})
307+
308+
it('renders no status if offline', () => {
309+
mountComponent({ online: false })
310+
311+
cy.findByTestId('icon-status-message').should('not.exist')
312+
})
313+
})
218314
})

packages/app/src/navigation/SidebarNavigation.vue

+82-8
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
:name="item.name"
5050
:is-nav-bar-expanded="isNavBarExpanded"
5151
:badge="item.badge"
52+
:icon-status="item.iconStatus"
5253
/>
5354
</RouterLink>
5455
</nav>
@@ -92,14 +93,15 @@
9293
<script lang="ts" setup>
9394
import { computed, FunctionalComponent, ref, watchEffect } from 'vue'
9495
import { gql, useMutation } from '@urql/vue'
95-
import SidebarNavigationRow, { Badge } from './SidebarNavigationRow.vue'
96+
import SidebarNavigationRow, { Badge, IconStatus } from './SidebarNavigationRow.vue'
9697
import KeyboardBindingsModal from './KeyboardBindingsModal.vue'
9798
import {
9899
IconTechnologyCodeEditor,
99100
IconTechnologyTestResults,
100101
IconObjectGear,
101102
IconObjectBug,
102103
} from '@cypress-design/vue-icon'
104+
import { OutlineStatusIcon } from '@cypress-design/vue-statusicon'
103105
import Tooltip from '@packages/frontend-shared/src/components/Tooltip.vue'
104106
import HideDuringScreenshot from '../runner/screenshot/HideDuringScreenshot.vue'
105107
import { SidebarNavigationFragment, SideBarNavigation_SetPreferencesDocument } from '../generated/graphql'
@@ -135,7 +137,13 @@ fragment SidebarNavigation on Query {
135137
__typename
136138
... on CloudProject {
137139
id
138-
runByNumber(runNumber: $runNumber) @include(if: $hasCurrentRun){
140+
selectedRun: runByNumber(runNumber: $selectedRunNumber) @include(if: $hasSelectedRun){
141+
id
142+
runNumber
143+
status
144+
totalFailed
145+
}
146+
latestRun: runByNumber(runNumber: $latestRunNumber) @include(if: $hasLatestRun){
139147
id
140148
runNumber
141149
status
@@ -171,25 +179,90 @@ const setDebugBadge = useDebounceFn((badge) => {
171179
debugBadge.value = badge
172180
}, 500)
173181
174-
const currentRun = computed(() => {
182+
const runsIconStatus = ref<IconStatus | undefined>()
183+
184+
const setRunsIconStatus = useDebounceFn((iconStatus) => {
185+
runsIconStatus.value = iconStatus
186+
}, 500)
187+
188+
const getStatusIcon = (status) => {
189+
return status ? OutlineStatusIcon : IconTechnologyTestResults
190+
}
191+
192+
const selectedRun = computed(() => {
193+
if (props.gql?.currentProject?.cloudProject?.__typename === 'CloudProject') {
194+
return props.gql.currentProject.cloudProject.selectedRun
195+
}
196+
197+
return undefined
198+
})
199+
200+
const latestRun = computed(() => {
175201
if (props.gql?.currentProject?.cloudProject?.__typename === 'CloudProject') {
176-
return props.gql.currentProject.cloudProject.runByNumber
202+
return props.gql.currentProject.cloudProject.latestRun
177203
}
178204
179205
return undefined
180206
})
181207
208+
watchEffect(() => {
209+
if (props.isLoading && userProjectStatusStore.project.isProjectConnected) {
210+
setRunsIconStatus(undefined)
211+
212+
return
213+
}
214+
215+
if (latestRun.value
216+
&& props.online
217+
) {
218+
const { status, totalFailed } = latestRun.value
219+
220+
switch (status) {
221+
case 'RUNNING':
222+
if ((totalFailed || 0) > 0) {
223+
setRunsIconStatus({ value: 'failing', label: t('sidebar.runs.failing', totalFailed || 0) })
224+
} else {
225+
setRunsIconStatus({ value: 'running', label: t('sidebar.runs.running') })
226+
}
227+
228+
break
229+
case 'PASSED':
230+
setRunsIconStatus({ value: 'passed', label: t('sidebar.runs.passed') })
231+
break
232+
case 'FAILED':
233+
setRunsIconStatus({ value: 'failed', label: t('sidebar.runs.failed', totalFailed || 0) })
234+
break
235+
case 'CANCELLED':
236+
setRunsIconStatus({ value: 'cancelled', label: t('sidebar.runs.cancelled') })
237+
break
238+
case 'ERRORED':
239+
case 'NOTESTS':
240+
case 'OVERLIMIT':
241+
case 'TIMEDOUT':
242+
setRunsIconStatus({ value: 'errored', label: t((totalFailed && totalFailed > 0 ? 'sidebar.runs.erroredWithFailures' : 'sidebar.runs.errored'), totalFailed || 0) })
243+
break
244+
default:
245+
setRunsIconStatus(undefined)
246+
break
247+
}
248+
249+
return
250+
}
251+
252+
setRunsIconStatus(undefined)
253+
})
254+
182255
watchEffect(() => {
183256
if (props.isLoading && userProjectStatusStore.project.isProjectConnected) {
184257
setDebugBadge(undefined)
185258
186259
return
187260
}
188261
189-
if (currentRun.value
262+
if (selectedRun.value
190263
&& props.online
191264
) {
192-
const { status, totalFailed } = currentRun.value
265+
const { status, totalFailed } = selectedRun.value
193266
194267
if (status === 'NOTESTS') {
195268
return
@@ -257,13 +330,14 @@ interface NavigationItem {
257330
params?: Record<string, any>
258331
badge?: Badge
259332
onClick?: () => void
333+
iconStatus?: IconStatus
260334
}
261335
262336
const navigation = computed<NavigationItem[]>(() => {
263337
return [
264338
{ name: 'Specs', icon: IconTechnologyCodeEditor, pageComponent: 'Specs' },
265-
{ name: 'Runs', icon: IconTechnologyTestResults, pageComponent: 'Runs' },
266-
{ name: 'Debug', icon: IconObjectBug, pageComponent: 'Debug', badge: debugBadge.value, params: { from: 'sidebar', runNumber: currentRun.value?.runNumber } },
339+
{ name: 'Runs', icon: getStatusIcon(runsIconStatus.value), pageComponent: 'Runs', iconStatus: runsIconStatus.value },
340+
{ name: 'Debug', icon: IconObjectBug, pageComponent: 'Debug', badge: debugBadge.value, params: { from: 'sidebar', runNumber: selectedRun.value?.runNumber } },
267341
{ name: 'Settings', icon: IconObjectGear, pageComponent: 'Settings' },
268342
]
269343
})

packages/app/src/navigation/SidebarNavigationContainer.vue

+5-3
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { useRelevantRun } from '@packages/app/src/composables/useRelevantRun'
1515
import { computed, ref, watchEffect } from 'vue'
1616
1717
gql`
18-
query SideBarNavigationContainer($runNumber: Int!, $hasCurrentRun: Boolean!) {
18+
query SideBarNavigationContainer($selectedRunNumber: Int!, $hasSelectedRun: Boolean!, $latestRunNumber: Int!, $hasLatestRun: Boolean!) {
1919
...SidebarNavigation
2020
}
2121
`
@@ -26,8 +26,10 @@ const relevantRuns = useRelevantRun('SIDEBAR')
2626
2727
const variables = computed(() => {
2828
return {
29-
runNumber: relevantRuns.value?.selectedRun?.runNumber || -1,
30-
hasCurrentRun: !!relevantRuns.value?.selectedRun?.runNumber,
29+
selectedRunNumber: relevantRuns.value?.selectedRun?.runNumber || -1,
30+
hasSelectedRun: !!relevantRuns.value?.selectedRun?.runNumber,
31+
latestRunNumber: relevantRuns.value?.latest?.[0]?.runNumber || -1,
32+
hasLatestRun: !!relevantRuns.value?.latest?.[0]?.runNumber,
3133
}
3234
})
3335

0 commit comments

Comments
 (0)