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 @@ + + + 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 @@