Skip to content

Commit

Permalink
feat: Add status icon to Sidebar Runs Page (#27672)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
dkasper-was-taken committed Aug 29, 2023
1 parent 0089501 commit 05afa1e
Show file tree
Hide file tree
Showing 10 changed files with 281 additions and 61 deletions.
8 changes: 8 additions & 0 deletions cli/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
<!-- See the ../guides/writing-the-cypress-changelog.md for details on writing the changelog. -->
## 13.1.0

_Released 09/12/2023 (PENDING)_

**Features:**

- 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).

## 13.0.0

_Released 08/29/2023_
Expand Down
4 changes: 4 additions & 0 deletions packages/app/cypress/e2e/debug.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ describe('App - Debug Page', () => {

if (obj.operationName === 'Debug_currentProject_cloudProject_cloudProjectBySlug' || obj.operationName === 'SideBarNavigationContainer_currentProject_cloudProject_cloudProjectBySlug') {
if (obj.result.data) {
// Standard Calls
obj.result.data.cloudProjectBySlug.runByNumber = options.DebugDataPassing.data.currentProject.cloudProject.runByNumber
// Aliased Calls
obj.result.data.cloudProjectBySlug.latestRun = options.DebugDataPassing.data.currentProject.cloudProject.runByNumber
obj.result.data.cloudProjectBySlug.selectedRun = options.DebugDataPassing.data.currentProject.cloudProject.runByNumber
}
}

Expand Down
4 changes: 2 additions & 2 deletions packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
"dependencies": {},
"devDependencies": {
"@cypress-design/vue-button": "^0.10.1",
"@cypress-design/vue-icon": "^0.24.3",
"@cypress-design/vue-statusicon": "^0.4.11",
"@cypress-design/vue-icon": "^0.25.0",
"@cypress-design/vue-statusicon": "^0.5.0",
"@cypress-design/vue-tabs": "^0.5.1",
"@graphql-typed-document-node/core": "^3.1.0",
"@headlessui/vue": "1.4.0",
Expand Down
116 changes: 106 additions & 10 deletions packages/app/src/navigation/SidebarNavigation.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { CloudRunStubs } from '@packages/graphql/test/stubCloudTypes'
import { cloneDeep } from 'lodash'
import { useUserProjectStatusStore } from '@packages/frontend-shared/src/store/user-project-status-store'

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

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

const selectedVariables = withDefaults.cloudProject ? {
selectedRunNumber: 1,
hasSelectedRun: true,
} : {
selectedRunNumber: -1,
hasSelectedRun: false,
}

const latestVariables = withDefaults.latestCloudProject ? {
latestRunNumber: 1,
hasLatestRun: true,
} : {
latestRunNumber: -1,
hasLatestRun: false,
}

cy.mountFragment(SidebarNavigationFragmentDoc, {
variableTypes: {
runNumber: 'Int',
hasCurrentRun: 'Boolean',
selectedRunNumber: 'Int',
hasSelectedRun: 'Boolean',
latestRunNumber: 'Int',
hasLatestRun: 'Boolean',
},
variables: {
runNumber: 1,
hasCurrentRun: true,
...selectedVariables,
...latestVariables,
},
onResult (gql) {
if (!gql.currentProject) return

if (gql.currentProject?.cloudProject?.__typename === 'CloudProject' && withDefaults.cloudProject) {
gql.currentProject.cloudProject.runByNumber = cloneDeep(CloudRunStubs.failingWithTests)
gql.currentProject.cloudProject.runByNumber.status = withDefaults.cloudProject.status as CloudRunStatus

gql.currentProject.cloudProject.runByNumber.totalFailed = withDefaults.cloudProject.numFailedTests
if (gql.currentProject?.cloudProject?.__typename === 'CloudProject') {
if (withDefaults.cloudProject) {
gql.currentProject.cloudProject.selectedRun = cloneDeep(CloudRunStubs.failingWithTests)
gql.currentProject.cloudProject.selectedRun.status = withDefaults.cloudProject.status as CloudRunStatus
gql.currentProject.cloudProject.selectedRun.totalFailed = withDefaults.cloudProject.numFailedTests
}

if (withDefaults.latestCloudProject) {
gql.currentProject.cloudProject.latestRun = cloneDeep(CloudRunStubs.failingWithTests)
gql.currentProject.cloudProject.latestRun.status = withDefaults.latestCloudProject.status as CloudRunStatus
gql.currentProject.cloudProject.latestRun.totalFailed = withDefaults.latestCloudProject.numFailedTests
}
} else {
gql.currentProject.cloudProject = null
}
Expand Down Expand Up @@ -215,4 +240,75 @@ describe('SidebarNavigation', () => {
cy.findByTestId('debug-badge').should('not.exist')
})
})

context('runs status icon', () => {
it('renders passing status if run status is "RUNNING" with no failures', () => {
mountComponent({ latestCloudProject: { status: 'RUNNING', numFailedTests: 0 } })
cy.findByTestId('icon-status-message').should('be.visible').contains('Latest run is in progress')
cy.percySnapshot('Runs Icon:running')
})

it('renders failing status if run status is "RUNNING" with failures', () => {
mountComponent({ latestCloudProject: { status: 'RUNNING', numFailedTests: 3 } })
cy.findByTestId('icon-status-message').should('be.visible').contains('failing')
cy.percySnapshot('Runs Icon:failing')
})

it('renders success status when status is "PASSED"', () => {
mountComponent({ latestCloudProject: { status: 'PASSED', numFailedTests: 0 } })
cy.findByTestId('icon-status-message').should('be.visible').contains('Latest run passed')
cy.percySnapshot('Runs Icon:passed')
})

it('renders failed status when status is "FAILED"', () => {
mountComponent({ latestCloudProject: { status: 'FAILED', numFailedTests: 1 } })
cy.findByTestId('icon-status-message').should('be.visible').contains('Latest run had 1 test failure')
cy.percySnapshot('Runs Icon:failed')
})

it('renders cancelled status when status is "CANCELLED"', () => {
mountComponent({ latestCloudProject: { status: 'CANCELLED', numFailedTests: 0 } })
cy.findByTestId('icon-status-message').should('be.visible').contains('Latest run has been cancelled')
cy.percySnapshot('Runs Icon:cancelled')
})

it('renders attention status when abnormal status', () => {
for (const status of ['ERRORED', 'NOTESTS', 'OVERLIMIT', 'TIMEDOUT'] as CloudRunStatus[]) {
cy.log(status)
mountComponent({ latestCloudProject: { status, numFailedTests: 0 } })
cy.findByTestId('icon-status-message').should('be.visible').contains('Latest run had an error')
}

cy.percySnapshot('Runs Icon:errored')
})

it('renders attention status when abnormal status and failing tests', () => {
for (const status of ['ERRORED', 'NOTESTS', 'OVERLIMIT', 'TIMEDOUT'] as CloudRunStatus[]) {
cy.log(status)
mountComponent({ latestCloudProject: { status, numFailedTests: 3 } })
cy.findByTestId('icon-status-message').should('be.visible').contains('Latest run had an error with 3 test failures')
}
})

it('renders no status if no cloudProject', () => {
mountComponent()
cy.findByTestId('icon-status-message').should('not.exist')
})

it('renders no status when query is loading', () => {
const userProjectStatusStore = useUserProjectStatusStore()

userProjectStatusStore.setProjectFlag('isProjectConnected', true)

mountComponent({ isLoading: true })

cy.findByTestId('icon-status-message').should('not.exist')
})

it('renders no status if offline', () => {
mountComponent({ online: false })

cy.findByTestId('icon-status-message').should('not.exist')
})
})
})
90 changes: 82 additions & 8 deletions packages/app/src/navigation/SidebarNavigation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
:name="item.name"
:is-nav-bar-expanded="isNavBarExpanded"
:badge="item.badge"
:icon-status="item.iconStatus"
/>
</RouterLink>
</nav>
Expand Down Expand Up @@ -92,14 +93,15 @@
<script lang="ts" setup>
import { computed, FunctionalComponent, ref, watchEffect } from 'vue'
import { gql, useMutation } from '@urql/vue'
import SidebarNavigationRow, { Badge } from './SidebarNavigationRow.vue'
import SidebarNavigationRow, { Badge, IconStatus } from './SidebarNavigationRow.vue'
import KeyboardBindingsModal from './KeyboardBindingsModal.vue'
import {
IconTechnologyCodeEditor,
IconTechnologyTestResults,
IconObjectGear,
IconObjectBug,
} from '@cypress-design/vue-icon'
import { OutlineStatusIcon } from '@cypress-design/vue-statusicon'
import Tooltip from '@packages/frontend-shared/src/components/Tooltip.vue'
import HideDuringScreenshot from '../runner/screenshot/HideDuringScreenshot.vue'
import { SidebarNavigationFragment, SideBarNavigation_SetPreferencesDocument } from '../generated/graphql'
Expand Down Expand Up @@ -135,7 +137,13 @@ fragment SidebarNavigation on Query {
__typename
... on CloudProject {
id
runByNumber(runNumber: $runNumber) @include(if: $hasCurrentRun){
selectedRun: runByNumber(runNumber: $selectedRunNumber) @include(if: $hasSelectedRun){
id
runNumber
status
totalFailed
}
latestRun: runByNumber(runNumber: $latestRunNumber) @include(if: $hasLatestRun){
id
runNumber
status
Expand Down Expand Up @@ -171,25 +179,90 @@ const setDebugBadge = useDebounceFn((badge) => {
debugBadge.value = badge
}, 500)
const currentRun = computed(() => {
const runsIconStatus = ref<IconStatus | undefined>()
const setRunsIconStatus = useDebounceFn((iconStatus) => {
runsIconStatus.value = iconStatus
}, 500)
const getStatusIcon = (status) => {
return status ? OutlineStatusIcon : IconTechnologyTestResults
}
const selectedRun = computed(() => {
if (props.gql?.currentProject?.cloudProject?.__typename === 'CloudProject') {
return props.gql.currentProject.cloudProject.selectedRun
}
return undefined
})
const latestRun = computed(() => {
if (props.gql?.currentProject?.cloudProject?.__typename === 'CloudProject') {
return props.gql.currentProject.cloudProject.runByNumber
return props.gql.currentProject.cloudProject.latestRun
}
return undefined
})
watchEffect(() => {
if (props.isLoading && userProjectStatusStore.project.isProjectConnected) {
setRunsIconStatus(undefined)
return
}
if (latestRun.value
&& props.online
) {
const { status, totalFailed } = latestRun.value
switch (status) {
case 'RUNNING':
if ((totalFailed || 0) > 0) {
setRunsIconStatus({ value: 'failing', label: t('sidebar.runs.failing', totalFailed || 0) })
} else {
setRunsIconStatus({ value: 'running', label: t('sidebar.runs.running') })
}
break
case 'PASSED':
setRunsIconStatus({ value: 'passed', label: t('sidebar.runs.passed') })
break
case 'FAILED':
setRunsIconStatus({ value: 'failed', label: t('sidebar.runs.failed', totalFailed || 0) })
break
case 'CANCELLED':
setRunsIconStatus({ value: 'cancelled', label: t('sidebar.runs.cancelled') })
break
case 'ERRORED':
case 'NOTESTS':
case 'OVERLIMIT':
case 'TIMEDOUT':
setRunsIconStatus({ value: 'errored', label: t((totalFailed && totalFailed > 0 ? 'sidebar.runs.erroredWithFailures' : 'sidebar.runs.errored'), totalFailed || 0) })
break
default:
setRunsIconStatus(undefined)
break
}
return
}
setRunsIconStatus(undefined)
})
watchEffect(() => {
if (props.isLoading && userProjectStatusStore.project.isProjectConnected) {
setDebugBadge(undefined)
return
}
if (currentRun.value
if (selectedRun.value
&& props.online
) {
const { status, totalFailed } = currentRun.value
const { status, totalFailed } = selectedRun.value
if (status === 'NOTESTS') {
return
Expand Down Expand Up @@ -257,13 +330,14 @@ interface NavigationItem {
params?: Record<string, any>
badge?: Badge
onClick?: () => void
iconStatus?: IconStatus
}
const navigation = computed<NavigationItem[]>(() => {
return [
{ name: 'Specs', icon: IconTechnologyCodeEditor, pageComponent: 'Specs' },
{ name: 'Runs', icon: IconTechnologyTestResults, pageComponent: 'Runs' },
{ name: 'Debug', icon: IconObjectBug, pageComponent: 'Debug', badge: debugBadge.value, params: { from: 'sidebar', runNumber: currentRun.value?.runNumber } },
{ name: 'Runs', icon: getStatusIcon(runsIconStatus.value), pageComponent: 'Runs', iconStatus: runsIconStatus.value },
{ name: 'Debug', icon: IconObjectBug, pageComponent: 'Debug', badge: debugBadge.value, params: { from: 'sidebar', runNumber: selectedRun.value?.runNumber } },
{ name: 'Settings', icon: IconObjectGear, pageComponent: 'Settings' },
]
})
Expand Down
8 changes: 5 additions & 3 deletions packages/app/src/navigation/SidebarNavigationContainer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { useRelevantRun } from '@packages/app/src/composables/useRelevantRun'
import { computed, ref, watchEffect } from 'vue'
gql`
query SideBarNavigationContainer($runNumber: Int!, $hasCurrentRun: Boolean!) {
query SideBarNavigationContainer($selectedRunNumber: Int!, $hasSelectedRun: Boolean!, $latestRunNumber: Int!, $hasLatestRun: Boolean!) {
...SidebarNavigation
}
`
Expand All @@ -26,8 +26,10 @@ const relevantRuns = useRelevantRun('SIDEBAR')
const variables = computed(() => {
return {
runNumber: relevantRuns.value?.selectedRun?.runNumber || -1,
hasCurrentRun: !!relevantRuns.value?.selectedRun?.runNumber,
selectedRunNumber: relevantRuns.value?.selectedRun?.runNumber || -1,
hasSelectedRun: !!relevantRuns.value?.selectedRun?.runNumber,
latestRunNumber: relevantRuns.value?.latest?.[0]?.runNumber || -1,
hasLatestRun: !!relevantRuns.value?.latest?.[0]?.runNumber,
}
})
Expand Down

5 comments on commit 05afa1e

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 05afa1e Aug 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the linux x64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/13.1.0/linux-x64/develop-05afa1eaa51385101530f7fe5299f4f77ca5fa90/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 05afa1e Aug 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the darwin arm64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/13.1.0/darwin-arm64/develop-05afa1eaa51385101530f7fe5299f4f77ca5fa90/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 05afa1e Aug 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the linux arm64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/13.1.0/linux-arm64/develop-05afa1eaa51385101530f7fe5299f4f77ca5fa90/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 05afa1e Aug 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the darwin x64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/13.1.0/darwin-x64/develop-05afa1eaa51385101530f7fe5299f4f77ca5fa90/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 05afa1e Aug 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the win32 x64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/13.1.0/win32-x64/develop-05afa1eaa51385101530f7fe5299f4f77ca5fa90/cypress.tgz

Please sign in to comment.