diff --git a/packages/app/cypress/e2e/sidebar_navigation.cy.ts b/packages/app/cypress/e2e/sidebar_navigation.cy.ts
index 48d791221fb6..f21c566f4a92 100644
--- a/packages/app/cypress/e2e/sidebar_navigation.cy.ts
+++ b/packages/app/cypress/e2e/sidebar_navigation.cy.ts
@@ -16,6 +16,7 @@ describe('Sidebar Navigation', { viewportWidth: 1280 }, () => {
.tab().should('have.attr', 'data-cy', 'sidebar-header').should('have.attr', 'role', 'button')
.tab().should('have.attr', 'href', '#/specs').should('have.prop', 'tagName', 'A')
.tab().should('have.attr', 'href', '#/runs').should('have.prop', 'tagName', 'A')
+ .tab().should('have.attr', 'href', '#/debug').should('have.prop', 'tagName', 'A')
.tab().should('have.attr', 'href', '#/settings').should('have.prop', 'tagName', 'A')
.tab().should('have.attr', 'data-cy', 'keyboard-modal-trigger').should('have.prop', 'tagName', 'BUTTON')
})
@@ -146,6 +147,10 @@ describe('Sidebar Navigation', { viewportWidth: 1280 }, () => {
cy.contains('.v-popper--some-open--tooltip', 'Specs')
cy.findByTestId('sidebar-link-specs-page').trigger('mouseout')
+ cy.findByTestId('sidebar-link-debug-page').trigger('mouseenter')
+ cy.contains('.v-popper--some-open--tooltip', 'Debug')
+ cy.findByTestId('sidebar-link-debug-page').trigger('mouseout')
+
cy.findByTestId('sidebar-link-settings-page').trigger('mouseenter')
cy.contains('.v-popper--some-open--tooltip', 'Settings')
cy.findByTestId('sidebar-link-settings-page').trigger('mouseout')
@@ -237,6 +242,14 @@ describe('Sidebar Navigation', { viewportWidth: 1280 }, () => {
cy.get('.router-link-active').findByText('Specs').should('be.visible')
})
+ it('has a menu item labeled "Debug" which takes you to the Debug page', () => {
+ cy.get('[data-cy="app-header-bar"]').findByText('Debug').should('not.exist')
+
+ cy.findByTestId('sidebar-link-debug-page').should('have.text', 'Debug').should('be.visible').click()
+ cy.get('[data-cy="app-header-bar"]').findByText('Debug').should('be.visible')
+ cy.get('.router-link-active').findByText('Debug').should('be.visible')
+ })
+
it('Specs sidebar nav link is not active when a test is running', () => {
cy.location('hash').should('equal', '#/specs')
cy.contains('.router-link-exact-active', 'Specs')
diff --git a/packages/app/cypress/e2e/specs_list_latest_runs.cy.ts b/packages/app/cypress/e2e/specs_list_latest_runs.cy.ts
index bff378126cfc..10fcb6b86f79 100644
--- a/packages/app/cypress/e2e/specs_list_latest_runs.cy.ts
+++ b/packages/app/cypress/e2e/specs_list_latest_runs.cy.ts
@@ -75,6 +75,9 @@ function simulateRunData () {
status: s,
createdAt: new Date('2022-05-08T03:17:00').toISOString(),
completedAt: new Date('2022-05-08T05:17:00').toISOString(),
+ basename: idPrefix.substring(idPrefix.lastIndexOf('/') + 1, idPrefix.indexOf('.')),
+ path: idPrefix,
+ extension: idPrefix.substring(idPrefix.indexOf('.')),
runNumber: 432,
groupCount: 2,
specDuration: {
@@ -444,6 +447,9 @@ describe('App/Cloud Integration - Latest runs and Average duration', { viewportW
status: s,
createdAt: new Date('2022-05-08T03:17:00').toISOString(),
completedAt: new Date('2022-05-08T05:17:00').toISOString(),
+ basename: idPrefix.substring(idPrefix.lastIndexOf('/') + 1, idPrefix.indexOf('.')),
+ path: idPrefix,
+ extension: idPrefix.substring(idPrefix.indexOf('.')),
runNumber: 432,
groupCount: 2,
specDuration: {
@@ -550,6 +556,9 @@ describe('App/Cloud Integration - Latest runs and Average duration', { viewportW
status: s,
createdAt: new Date('2022-05-08T03:17:00').toISOString(),
completedAt: new Date('2022-05-08T05:17:00').toISOString(),
+ basename: idPrefix.substring(idPrefix.lastIndexOf('/') + 1, idPrefix.indexOf('.')),
+ path: idPrefix,
+ extension: idPrefix.substring(idPrefix.indexOf('.')),
runNumber: 432,
groupCount: 2,
specDuration: {
diff --git a/packages/app/package.json b/packages/app/package.json
index 5c4babc749b1..f9b76f81b4ae 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -20,7 +20,7 @@
},
"dependencies": {},
"devDependencies": {
- "@cypress-design/vue-icon": "^0.4.2",
+ "@cypress-design/vue-icon": "^0.12.1",
"@graphql-typed-document-node/core": "^3.1.0",
"@headlessui/vue": "1.4.0",
"@iconify/iconify": "2.1.2",
diff --git a/packages/app/src/composables/useDurationFormat.cy.tsx b/packages/app/src/composables/useDurationFormat.cy.tsx
new file mode 100644
index 000000000000..54914b6d727f
--- /dev/null
+++ b/packages/app/src/composables/useDurationFormat.cy.tsx
@@ -0,0 +1,39 @@
+import { ref } from 'vue'
+import { useDurationFormat } from './useDurationFormat'
+
+describe('useDurationFormat', () => {
+ it('should format duration', () => {
+ expect(useDurationFormat(1000).value).to.eq('00:01')
+ expect(useDurationFormat(60000).value).to.eq('01:00')
+ expect(useDurationFormat(6000000).value).to.eq('01:40:00')
+
+ // expects 24 hours and greater to "roll over" and not include day information
+ expect(useDurationFormat(86400000).value).to.eq('00:00')
+ })
+
+ it('should render with value', () => {
+ const duration = 1000
+ const formatted = useDurationFormat(duration)
+
+ cy.mount(() => (
+ {formatted.value}
+
))
+
+ cy.contains('00:01')
+ })
+
+ it('should render with ref and update if ref changes', () => {
+ const duration = ref(1000)
+ const formatted = useDurationFormat(duration)
+
+ cy.mount(() => (
+ {formatted.value}
+
))
+
+ cy.contains('00:01').then(() => {
+ duration.value = 2000
+ })
+
+ cy.contains('00:02')
+ })
+})
diff --git a/packages/app/src/composables/useDurationFormat.ts b/packages/app/src/composables/useDurationFormat.ts
new file mode 100644
index 000000000000..08b4e6bb476e
--- /dev/null
+++ b/packages/app/src/composables/useDurationFormat.ts
@@ -0,0 +1,13 @@
+import { computed, Ref, unref } from 'vue'
+import { dayjs } from '../runs/utils/day.js'
+
+/*
+ Format duration to in HH:mm:ss format. The `totalDuration` field is milliseconds. Remove the leading "00:" if the value is less
+ than an hour. Currently, there is no expectation that a run duration will be greater 24 hours or greater, so it is okay that
+ this format would "roll-over" in that scenario.
+ Ex: 1 second which is 1000ms = 00:01
+ Ex: 1 hour and 1 second which is 3601000ms = 01:00:01
+*/
+export function useDurationFormat (value: number | Ref) {
+ return computed(() => dayjs.duration(unref(value)).format('HH:mm:ss').replace(/^0+:/, ''))
+}
diff --git a/packages/app/src/debug/DebugContainer.cy.tsx b/packages/app/src/debug/DebugContainer.cy.tsx
new file mode 100644
index 000000000000..bf0295c40f8a
--- /dev/null
+++ b/packages/app/src/debug/DebugContainer.cy.tsx
@@ -0,0 +1,154 @@
+import { DebugSpecsFragmentDoc } from '../generated/graphql-test'
+import DebugContainer from './DebugContainer.vue'
+import { defaultMessages } from '@cy/i18n'
+import { useLoginConnectStore } from '@packages/frontend-shared/src/store/login-connect-store'
+import { specsList } from './utils/DebugMapping'
+import { CloudRunStubs } from '@packages/graphql/test/stubCloudTypes'
+
+describe('', () => {
+ context('empty states', () => {
+ const validateEmptyState = (expectedMessage: string) => {
+ cy.mountFragment(DebugSpecsFragmentDoc, {
+ render: (gqlVal) => {
+ return (
+
+ )
+ },
+ })
+
+ cy.findByTestId('debug-empty').contains(expectedMessage)
+ }
+
+ it('shows not logged in', () => {
+ validateEmptyState(defaultMessages.debugPage.notLoggedIn)
+ })
+
+ it('is logged in', () => {
+ const loginConnectStore = useLoginConnectStore()
+
+ loginConnectStore.setUserFlag('isLoggedIn', true)
+
+ validateEmptyState(defaultMessages.debugPage.notConnected)
+ })
+
+ it('has no runs', () => {
+ const loginConnectStore = useLoginConnectStore()
+
+ loginConnectStore.setUserFlag('isLoggedIn', true)
+ loginConnectStore.setProjectFlag('isProjectConnected', true)
+ cy.mountFragment(DebugSpecsFragmentDoc, {
+ render: (gqlVal) => {
+ return (
+
+ )
+ },
+ })
+
+ cy.findByTestId('debug-empty').contains(defaultMessages.debugPage.noRuns)
+ })
+ })
+
+ describe('render specs and tests', () => {
+ it('renders data when logged in and connected', () => {
+ const loginConnectStore = useLoginConnectStore()
+
+ loginConnectStore.setUserFlag('isLoggedIn', true)
+ loginConnectStore.setProjectFlag('isProjectConnected', true)
+ cy.mountFragment(DebugSpecsFragmentDoc, {
+ onResult: (result) => {
+ if (result.currentProject?.cloudProject?.__typename === 'CloudProject') {
+ const test = result.currentProject.cloudProject.runByNumber
+ const other = CloudRunStubs.failingWithTests as typeof test
+
+ result.currentProject.cloudProject.runByNumber = other
+ }
+ },
+ render: (gqlVal) => {
+ return (
+
+ )
+ },
+ })
+
+ // Only asserting that it is rendering the components for failed specs
+ cy.findByTestId('debug-header').should('be.visible')
+ cy.findByTestId('debug-spec-item').should('be.visible')
+ })
+ })
+
+ describe('testing util function: debugMapping', () => {
+ const createSpecs = (idArr: string[]) => {
+ const acc: {id: string}[] = []
+
+ idArr.forEach((val) => {
+ acc.push({ id: val })
+ })
+
+ return acc
+ }
+
+ it('maps correctly for a single spec', () => {
+ const spec = createSpecs(['a1c'])
+ const tests = [
+ { specId: 'a1c', id: 'random1' },
+ { specId: 'a1c', id: 'random2' },
+ ]
+
+ const debugMappingArray = specsList(spec, tests)
+
+ expect(debugMappingArray).to.have.length(1)
+ expect(debugMappingArray[0]).to.deep.equal({ spec: { id: 'a1c' }, tests: [{ specId: 'a1c', id: 'random1' }, { specId: 'a1c', id: 'random2' }] })
+ })
+
+ it('maps correctly for multiple specs and test', () => {
+ const specs = createSpecs(['123', '456', '789'])
+ const tests = [
+ { specId: '123', id: 'random1' },
+ { specId: '456', id: 'random2' },
+ { specId: '456', id: 'random3' },
+ { specId: '789', id: 'random4' },
+ { specId: '123', id: 'random6' },
+ ]
+
+ const debugMappingArray = specsList(specs, tests)
+
+ const expected = [
+ { spec: { id: '123' }, tests: [{ specId: '123', id: 'random1' }, { specId: '123', id: 'random6' }] },
+ { spec: { id: '456' }, tests: [{ specId: '456', id: 'random2' }, { specId: '456', id: 'random3' }] },
+ { spec: { id: '789' }, tests: [{ specId: '789', id: 'random4' }] },
+ ]
+
+ expect(debugMappingArray).to.deep.equal(expected)
+ })
+
+ it('maps does not show specs that do not have tests', () => {
+ const specs = createSpecs(['123', '456', '789'])
+ const tests = [{ specId: '123', id: 'random1' }]
+
+ const debugMappingArray = specsList(specs, tests)
+
+ expect(debugMappingArray).to.have.length(1)
+ expect(debugMappingArray).to.deep.equal([{ spec: { id: '123' }, tests: [{ specId: '123', id: 'random1' }] }])
+ })
+
+ it('throws an error when a test does not map to a spec', () => {
+ const specs = createSpecs(['123'])
+ const tests = [
+ { specId: '123', id: 'random1' },
+ { specId: '456', id: 'random2' },
+ ]
+
+ const specsListWrapper = () => {
+ return specsList(specs, tests)
+ }
+
+ expect(specsListWrapper).to.throw('Could not find spec for id 456')
+ })
+ })
+})
diff --git a/packages/app/src/debug/DebugContainer.vue b/packages/app/src/debug/DebugContainer.vue
new file mode 100644
index 000000000000..17702747c885
--- /dev/null
+++ b/packages/app/src/debug/DebugContainer.vue
@@ -0,0 +1,100 @@
+
+
+
+
+
+
+
+
+ {{ t('debugPage.notLoggedIn') }}
+
+
+ {{ t('debugPage.notConnected' ) }}
+
+
+ {{ t('debugPage.noRuns') }}
+
+
+
+
+
+
diff --git a/packages/app/src/debug/DebugPageHeader.cy.tsx b/packages/app/src/debug/DebugPageHeader.cy.tsx
index 14ebb3bb5edd..b61e46be0aea 100644
--- a/packages/app/src/debug/DebugPageHeader.cy.tsx
+++ b/packages/app/src/debug/DebugPageHeader.cy.tsx
@@ -5,7 +5,7 @@ const defaults = [
{ attr: 'debug-header-branch', text: 'Branch Name: feature/DESIGN-183' },
{ attr: 'debug-header-commitHash', text: 'Commit Hash: b5e6fde' },
{ attr: 'debug-header-author', text: 'Commit Author: cypressDTest' },
- { attr: 'debug-header-createdAt', text: 'Run Total Duration: 60000 (an hour ago) ' },
+ { attr: 'debug-header-createdAt', text: 'Run Total Duration: 01:00 (an hour ago) ' },
]
describe('', {
@@ -20,12 +20,13 @@ describe('', {
result.commitInfo.summary = 'Adding a hover state to the button component'
result.commitInfo.branch = 'feature/DESIGN-183'
result.commitInfo.authorName = 'cypressDTest'
+ result.commitInfo.sha = 'b5e6fde'
}
}
},
render: (gqlVal) => {
return (
-
+
)
},
})
@@ -36,7 +37,7 @@ describe('', {
cy.findByTestId('debug-runCommit-info').children().should('have.length', 3)
cy.findByTestId('debug-runNumber')
- .should('have.text', ' Run #468')
+ .should('have.text', ' Run #432')
.should('have.css', 'color', 'rgb(90, 95, 122)')
cy.findByTestId('debug-commitsAhead')
@@ -51,4 +52,29 @@ describe('', {
.children().should('have.length', 2)
})
})
+
+ it('renders singular commit message', () => {
+ cy.mountFragment(DebugPageFragmentDoc, {
+ render: (gqlVal) => {
+ return (
+
+ )
+ },
+ })
+
+ cy.findByTestId('debug-commitsAhead')
+ .should('have.text', 'You are 1 commit ahead')
+ })
+
+ it('renders no commit message', () => {
+ cy.mountFragment(DebugPageFragmentDoc, {
+ render: (gqlVal) => {
+ return (
+
+ )
+ },
+ })
+
+ cy.findByTestId('debug-commitsAhead').should('not.exist')
+ })
})
diff --git a/packages/app/src/debug/DebugPageHeader.vue b/packages/app/src/debug/DebugPageHeader.vue
index f84e9e667612..74c4b773fde9 100644
--- a/packages/app/src/debug/DebugPageHeader.vue
+++ b/packages/app/src/debug/DebugPageHeader.vue
@@ -1,15 +1,15 @@
-
- Run #{{ props.runNumber }}
+ Run #{{ debug.runNumber }}
-
+
- {{ props.commitsAhead }}
+ {{ t('debugPage.header.commitsAhead', props.commitsAhead) }}
-
@@ -46,13 +46,13 @@
:href="debug.url || '#'"
:use-default-hocus="false"
>
- Dashboard Link: View in the dashboard
+ Dashboard Link: {{ t('debugPage.header.runUrl') }}
-
-
+
Branch Name: {{ debug.commitInfo.branch }}
-
- Commit Hash: {{ props.commitHash }}
+ Commit Hash: {{ debug.commitInfo?.sha?.substring(0,7) }}
-
Commit Author: {{ debug.commitInfo.authorName }}
@@ -97,7 +97,7 @@
stroke-color="gray-500"
fill-color="gray-50"
/>
- Run Total Duration: {{ debug.totalDuration }} ({{ relativeCreatedAt }})
+ Run Total Duration: {{ totalDuration }} ({{ relativeCreatedAt }})
@@ -112,40 +112,42 @@ import CommitIcon from '~icons/cy/commit_x14'
import { IconTimeStopwatch } from '@cypress-design/vue-icon'
import { gql } from '@urql/core'
import { dayjs } from '../runs/utils/day.js'
+import { useI18n } from 'vue-i18n'
+import { useDurationFormat } from '../composables/useDurationFormat'
+
+const { t } = useI18n()
-// runNumber and commitHash dont currently exist in the query and therefore are being obtained as props instead
gql`
fragment DebugPage on CloudRun {
- id
- createdAt
- status
+ id
+ runNumber
+ createdAt
+ status
totalDuration
+ commitInfo {
+ sha
+ }
url
- tags {
- id
- name
+ ...RunResults
+ commitInfo {
+ authorName
+ summary
+ branch
}
- ...RunResults
- commitInfo {
- authorName
- authorEmail
- summary
- branch
- }
}
`
const props = defineProps<{
gql: DebugPageFragment
- commitsAhead: string
- commitHash: string
- runNumber: number
+ commitsAhead: number
}>()
const debug = computed(() => props.gql)
const relativeCreatedAt = computed(() => dayjs(new Date(debug.value.createdAt!)).fromNow())
+const totalDuration = useDurationFormat(debug.value.totalDuration ?? 0)
+