From 2d9abf715b2527798d0feedf43a4a6c28044de0c Mon Sep 17 00:00:00 2001 From: Zachary Williams Date: Fri, 27 Jan 2023 10:54:44 -0600 Subject: [PATCH 01/17] chore: bump version and remove misleading changelog entry (#25612) --- cli/CHANGELOG.md | 3 +-- package.json | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index ad46843a6a23..fde5ae8a5cf6 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -1,12 +1,11 @@ ## 12.4.1 -_Released 01/26/2023_ +_Released 01/27/2023_ **Bugfixes:** - Fixed a regression from Cypress [12.4.0](https://docs.cypress.io/guides/references/changelog#12-4-0) where Cypress was not exiting properly when running multiple Component Testing specs in `electron` in `run` mode. Fixes [#25568](https://github.com/cypress-io/cypress/issues/25568). -- Fixed an issue where alternative Microsoft Edge Beta and Canary binary names were not being discovered by Cypress. Fixes [#25455](https://github.com/cypress-io/cypress/issues/25455). **Dependency Updates:** diff --git a/package.json b/package.json index 048f385700a6..a4d138bf7128 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cypress", - "version": "12.4.0", + "version": "12.4.1", "description": "Cypress is a next generation front end testing tool built for the modern web", "private": true, "scripts": { From e4be1899431ec6b848a75500e35946ada91a87e0 Mon Sep 17 00:00:00 2001 From: Matt Schile Date: Fri, 27 Jan 2023 10:59:13 -0700 Subject: [PATCH 02/17] docs: remove cypress-example-todomvc-redux from release-process (#25613) --- guides/release-process.md | 1 - 1 file changed, 1 deletion(-) diff --git a/guides/release-process.md b/guides/release-process.md index 5d28b07cdc5d..4b399977a717 100644 --- a/guides/release-process.md +++ b/guides/release-process.md @@ -175,7 +175,6 @@ In the following instructions, "X.Y.Z" is used to denote the [next version of Cy 23. Check all `cypress-test-*` and `cypress-example-*` repositories, and if there is a branch named `x.y.z` for testing the features or fixes from the newly published version `x.y.z`, update that branch to refer to the newly published NPM version in `package.json`. Then, get the changes approved and merged into that project's main branch. For projects without a `x.y.z` branch, you can go to the Renovate dependency issue and check the box next to `Update dependency cypress to X.Y.Z`. It will automatically create a PR. Once it passes, you can merge it. Try updating at least the following projects: - [cypress-example-todomvc](https://github.com/cypress-io/cypress-example-todomvc/issues/99) - - [cypress-example-todomvc-redux](https://github.com/cypress-io/cypress-example-todomvc-redux/issues/1) - [cypress-realworld-app](https://github.com/cypress-io/cypress-realworld-app/issues/41) - [cypress-example-recipes](https://github.com/cypress-io/cypress-example-recipes/issues/225) - [cypress-fiddle](https://github.com/cypress-io/cypress-fiddle/issues/5) From 5afe19f8d17b5da53d66a0513424403006167adf Mon Sep 17 00:00:00 2001 From: Arnau Giralt <50662025+arnaugiralt@users.noreply.github.com> Date: Fri, 27 Jan 2023 18:59:42 +0100 Subject: [PATCH 03/17] fix: allow version 9 of the babel-loader peer dependency (#25569) Closes https://github.com/cypress-io/cypress/issues/24435 --- npm/webpack-preprocessor/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/npm/webpack-preprocessor/package.json b/npm/webpack-preprocessor/package.json index 5ea478c58a30..2badb36fd7ed 100644 --- a/npm/webpack-preprocessor/package.json +++ b/npm/webpack-preprocessor/package.json @@ -62,7 +62,7 @@ "peerDependencies": { "@babel/core": "^7.0.1", "@babel/preset-env": "^7.0.0", - "babel-loader": "^8.0.2", + "babel-loader": "^8.0.2 || ^9", "webpack": "^4 || ^5" }, "files": [ From b50714108df429571cbbf89166ee74ce270c6ea5 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Fri, 27 Jan 2023 11:59:55 -0600 Subject: [PATCH 04/17] chore: update packages/example deployment script and cleanup package/example (#25091) Co-authored-by: Bill Glesias --- npm/create-cypress-tests/scripts/example.js | 8 +++- .../examples}/cypress/fixtures/example.json | 0 .../examples}/cypress/support/commands.js | 0 .../examples/cypress/support/component.js | 20 +++++++++ .../scripts/examples/cypress/support/e2e.js | 20 +++++++++ .../tsconfig.json} | 0 packages/app/cypress/e2e/specs.cy.ts | 7 ++- .../scaffold/ScaffoldGeneratorStepOne.vue | 10 ++--- .../src/actions/CodegenActions.ts | 4 +- .../src/actions/ProjectActions.ts | 2 +- .../data-context/src/codegen/templates.ts | 2 +- .../test/unit/codegen/code-generator.spec.ts | 12 ++--- ..._USE_DEFAULT_DOCUMENT_DOMAIN_E2E_ONLY.html | 45 +++++++++++++++++++ packages/example/README.md | 25 ++++++----- packages/example/bin/build.js | 13 +++--- packages/example/cypress.config.js | 3 +- packages/example/cypress/plugins/index.js | 22 --------- packages/example/gulpfile.js | 8 ---- packages/example/index.d.ts | 2 +- packages/example/lib/example.d.ts | 8 +--- packages/example/lib/example.js | 33 +------------- packages/example/package.json | 15 ++----- packages/example/test/example_spec.js | 31 ------------- .../support/mock-graphql/stubgql-Mutation.ts | 2 +- packages/graphql/schemas/schema.graphql | 4 +- .../enumTypes/gql-CodeGenTypeEnum.ts | 2 +- .../schemaTypes/objectTypes/gql-Mutation.ts | 4 +- yarn.lock | 30 +++---------- 28 files changed, 149 insertions(+), 183 deletions(-) rename {packages/example => npm/create-cypress-tests/scripts/examples}/cypress/fixtures/example.json (100%) rename {packages/example => npm/create-cypress-tests/scripts/examples}/cypress/support/commands.js (100%) create mode 100644 npm/create-cypress-tests/scripts/examples/cypress/support/component.js create mode 100644 npm/create-cypress-tests/scripts/examples/cypress/support/e2e.js rename npm/create-cypress-tests/scripts/{example-tsconfig.json => examples/tsconfig.json} (100%) create mode 100644 packages/errors/__snapshot-html__/EXPERIMENTAL_USE_DEFAULT_DOCUMENT_DOMAIN_E2E_ONLY.html delete mode 100644 packages/example/cypress/plugins/index.js delete mode 100644 packages/example/test/example_spec.js diff --git a/npm/create-cypress-tests/scripts/example.js b/npm/create-cypress-tests/scripts/example.js index 49b35fd7ee21..0925901447ab 100644 --- a/npm/create-cypress-tests/scripts/example.js +++ b/npm/create-cypress-tests/scripts/example.js @@ -13,9 +13,13 @@ program await fs.remove(destinationPath) await fs.copy(exampleFolder, destinationPath, { recursive: true }) - console.log(`✅ Example was successfully created at ${chalk.cyan(destination)}`) + console.log(`✅ E2E Examples were successfully created at ${chalk.cyan(destination)}`) - await fs.copy(path.join(__dirname, 'example-tsconfig.json'), path.join(destination, 'tsconfig.json')) + await fs.copy(path.join(__dirname, 'examples', 'cypress'), path.join(destination)) + + console.log(`✅ Cypress Setup was successfully created at ${chalk.cyan(destination)}`) + + await fs.copy(path.join(__dirname, 'examples', 'tsconfig.json'), path.join(destination, 'tsconfig.json')) console.log(`✅ tsconfig.json was created for ${chalk.cyan(destination)}`) }) diff --git a/packages/example/cypress/fixtures/example.json b/npm/create-cypress-tests/scripts/examples/cypress/fixtures/example.json similarity index 100% rename from packages/example/cypress/fixtures/example.json rename to npm/create-cypress-tests/scripts/examples/cypress/fixtures/example.json diff --git a/packages/example/cypress/support/commands.js b/npm/create-cypress-tests/scripts/examples/cypress/support/commands.js similarity index 100% rename from packages/example/cypress/support/commands.js rename to npm/create-cypress-tests/scripts/examples/cypress/support/commands.js diff --git a/npm/create-cypress-tests/scripts/examples/cypress/support/component.js b/npm/create-cypress-tests/scripts/examples/cypress/support/component.js new file mode 100644 index 000000000000..5e450a7b1a4a --- /dev/null +++ b/npm/create-cypress-tests/scripts/examples/cypress/support/component.js @@ -0,0 +1,20 @@ +// *********************************************************** +// This example support/component.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands' + +// Alternatively you can use CommonJS syntax: +// require('./commands') diff --git a/npm/create-cypress-tests/scripts/examples/cypress/support/e2e.js b/npm/create-cypress-tests/scripts/examples/cypress/support/e2e.js new file mode 100644 index 000000000000..d1dd1353e812 --- /dev/null +++ b/npm/create-cypress-tests/scripts/examples/cypress/support/e2e.js @@ -0,0 +1,20 @@ +// *********************************************************** +// This example support/e2e.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands' + +// Alternatively you can use CommonJS syntax: +// require('./commands') diff --git a/npm/create-cypress-tests/scripts/example-tsconfig.json b/npm/create-cypress-tests/scripts/examples/tsconfig.json similarity index 100% rename from npm/create-cypress-tests/scripts/example-tsconfig.json rename to npm/create-cypress-tests/scripts/examples/tsconfig.json diff --git a/packages/app/cypress/e2e/specs.cy.ts b/packages/app/cypress/e2e/specs.cy.ts index de73e4a2800d..e7a192bd9c20 100644 --- a/packages/app/cypress/e2e/specs.cy.ts +++ b/packages/app/cypress/e2e/specs.cy.ts @@ -81,11 +81,13 @@ describe('App: Specs', () => { 'cypress_api', 'files', 'location', + 'misc', 'navigation', 'network_requests', 'querying', 'spies_stubs_clocks', 'storage', + 'traversal', 'utilities', 'viewport', 'waiting', @@ -114,10 +116,7 @@ describe('App: Specs', () => { additionalIgnorePattern: [], })).map((spec) => spec.relative) - // Validate that all expected paths have been generated within the data context - expect(generatedSpecPaths.filter((path) => { - return options.expectedScaffoldPathsForPlatform.includes(path) - })).to.have.lengthOf(options.expectedScaffoldPathsForPlatform.length) + expect(generatedSpecPaths).to.include.members(options.expectedScaffoldPathsForPlatform) }, { expectedScaffoldPathsForPlatform }) // cy.percySnapshot() // TODO: restore when Percy CSS is fixed. See https://github.com/cypress-io/cypress/issues/23435 diff --git a/packages/app/src/specs/generators/scaffold/ScaffoldGeneratorStepOne.vue b/packages/app/src/specs/generators/scaffold/ScaffoldGeneratorStepOne.vue index 548519c60d0c..70b776d097c2 100644 --- a/packages/app/src/specs/generators/scaffold/ScaffoldGeneratorStepOne.vue +++ b/packages/app/src/specs/generators/scaffold/ScaffoldGeneratorStepOne.vue @@ -56,7 +56,7 @@ diff --git a/packages/app/src/debug/DebugNewRelevantRunBar.cy.tsx b/packages/app/src/debug/DebugNewRelevantRunBar.cy.tsx new file mode 100644 index 000000000000..65c43e14eed1 --- /dev/null +++ b/packages/app/src/debug/DebugNewRelevantRunBar.cy.tsx @@ -0,0 +1,71 @@ +import { CloudRunStatus, DebugNewRelevantRunBarFragmentDoc, DebugNewRelevantRunBar_MoveToNextDocument, DebugNewRelevantRunBar_SpecsDocument } from '../generated/graphql-test' +import DebugNewRelevantRunBar from './DebugNewRelevantRunBar.vue' + +describe('', () => { + [ + { viewportWidth: 300, description: 'small viewport' }, + { viewportWidth: 1028, description: 'large viewport' }, + ].forEach(({ viewportWidth, description }) => { + context(description, { viewportWidth }, () => { + ['PASSED', 'FAILED', 'RUNNING'].forEach((status) => { + it(`handles ${status} status`, () => { + cy.mountFragment(DebugNewRelevantRunBarFragmentDoc, { + onResult (result) { + result.status = status as CloudRunStatus + }, + render: (gqlVal) => , + }) + + cy.percySnapshot() + }) + }) + }) + }) + + it('should show spec counts for RUNNING', () => { + cy.mountFragment(DebugNewRelevantRunBarFragmentDoc, { + onResult (result) { + result.status = 'RUNNING' + }, + render: (gqlVal) => , + }) + + cy.stubSubscriptionEvent(DebugNewRelevantRunBar_SpecsDocument, () => { + return { + __typename: 'Subscription' as const, + relevantRunSpecChange: { + __typename: 'Query' as const, + currentProject: { + __typename: 'CurrentProject' as const, + id: 'fake', + relevantRunSpecs: { + __typename: 'CurrentProjectRelevantRunSpecs' as const, + next: { + __typename: 'RelevantRunSpecs' as const, + completedSpecs: 3, + totalSpecs: 5, + }, + }, + }, + }, + } + }) + + cy.contains('3 of 5').should('be.visible') + }) + + it('should call mutation when link is clicked', (done) => { + cy.mountFragment(DebugNewRelevantRunBarFragmentDoc, { + onResult (result) { + result.status = 'PASSED' + }, + render: (gqlVal) => , + }) + + cy.stubMutationResolver(DebugNewRelevantRunBar_MoveToNextDocument, (defineResult) => { + done() + }) + + cy.contains('View run').click() + }) +}) diff --git a/packages/app/src/debug/DebugNewRelevantRunBar.vue b/packages/app/src/debug/DebugNewRelevantRunBar.vue new file mode 100644 index 000000000000..f0d8ef11e4fd --- /dev/null +++ b/packages/app/src/debug/DebugNewRelevantRunBar.vue @@ -0,0 +1,107 @@ + + + diff --git a/packages/app/src/debug/DebugNoTests.vue b/packages/app/src/debug/DebugNoTests.vue new file mode 100644 index 000000000000..24da0f2265c2 --- /dev/null +++ b/packages/app/src/debug/DebugNoTests.vue @@ -0,0 +1,19 @@ + + + diff --git a/packages/app/src/debug/DebugOverLimit.vue b/packages/app/src/debug/DebugOverLimit.vue new file mode 100644 index 000000000000..67e4586096fd --- /dev/null +++ b/packages/app/src/debug/DebugOverLimit.vue @@ -0,0 +1,107 @@ + + + diff --git a/packages/app/src/debug/DebugPageDetails.vue b/packages/app/src/debug/DebugPageDetails.vue new file mode 100644 index 000000000000..85e45025fc4f --- /dev/null +++ b/packages/app/src/debug/DebugPageDetails.vue @@ -0,0 +1,78 @@ + + diff --git a/packages/app/src/debug/DebugPageHeader.cy.tsx b/packages/app/src/debug/DebugPageHeader.cy.tsx new file mode 100644 index 000000000000..fea3d08ea611 --- /dev/null +++ b/packages/app/src/debug/DebugPageHeader.cy.tsx @@ -0,0 +1,147 @@ +import { CloudRunStatus, DebugPageHeaderFragmentDoc } from '../generated/graphql-test' +import DebugPageHeader from './DebugPageHeader.vue' +import { defaultMessages } from '@cy/i18n' + +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: 01:00 (an hour ago) ' }, +] + +describe('', { + viewportWidth: 1032, +}, +() => { + it('renders with passed in gql props', () => { + cy.mountFragment(DebugPageHeaderFragmentDoc, { + onResult (result) { + if (result) { + result.status = 'FAILED' + if (result.commitInfo) { + 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 ( + + ) + }, + }) + + cy.findByTestId('debug-header').children().should('have.length', 2) + cy.findByTestId('debug-test-summary').should('have.text', 'Adding a hover state to the button component') + + cy.findByTestId('debug-commitsAhead').should('have.text', 'You are 2 commits ahead') + + cy.findByTestId('debug-results').should('be.visible') + + cy.findByTestId('debug-runNumber-FAILED') + .should('have.text', '#432') + .children().should('have.length', 2) + + cy.findByTestId('debug-flaky-badge') + .should('not.exist') + + defaults.forEach((obj) => { + cy.findByTestId(obj.attr) + .should('have.text', obj.text) + .children().should('have.length', 2) + }) + }) + + it('displays a flaky badge', () => { + const flakyCount = 4 + + cy.mountFragment(DebugPageHeaderFragmentDoc, { + onResult: (result) => { + if (result) { + result.totalFlakyTests = flakyCount + } + }, + render: (gqlVal) => { + return ( + + ) + }, + }) + + cy.findByTestId('debug-flaky-badge') + .contains(defaultMessages.specPage.flaky.badgeLabel) + + cy.findByTestId('total-flaky-tests') + .contains(flakyCount) + }) + + it('displays each run status', () => { + const statuses: CloudRunStatus[] = ['PASSED', 'FAILED', 'CANCELLED', 'RUNNING', 'ERRORED'] + + statuses.forEach((status) => { + cy.mountFragment(DebugPageHeaderFragmentDoc, { + onResult: (result) => { + if (result) { + result.status = status + } + }, + render: (gqlVal) => { + return ( + + ) + }, + }) + + cy.findByTestId(`debug-runNumber-${status}`).should('be.visible') + cy.percySnapshot() + }) + }) + + it('renders singular commit message', () => { + cy.mountFragment(DebugPageHeaderFragmentDoc, { + render: (gqlVal) => { + return ( + + ) + }, + }) + + cy.findByTestId('debug-commitsAhead') + .should('have.text', 'You are 1 commit ahead') + }) + + it('renders long commit message', () => { + const longText = 'This commit contains many changes which are described at great length in this commit summary, arguably the length is long enough to be considered unwieldy, but nevertheless we should render the text nicely' + + cy.mountFragment(DebugPageHeaderFragmentDoc, { + render: (gqlVal) => { + if (!gqlVal.commitInfo?.summary) { + throw new Error('Missing commit info for test') + } + + gqlVal.commitInfo.summary = longText + + return ( + + ) + }, + }) + + cy.contains(longText).should('be.visible') + cy.percySnapshot() + }) + + it('renders no commit message', () => { + cy.mountFragment(DebugPageHeaderFragmentDoc, { + 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 new file mode 100644 index 000000000000..498f4e011044 --- /dev/null +++ b/packages/app/src/debug/DebugPageHeader.vue @@ -0,0 +1,158 @@ + + + diff --git a/packages/app/src/debug/DebugPassed.vue b/packages/app/src/debug/DebugPassed.vue new file mode 100644 index 000000000000..d6e8e1124e97 --- /dev/null +++ b/packages/app/src/debug/DebugPassed.vue @@ -0,0 +1,19 @@ + + + diff --git a/packages/app/src/debug/DebugPendingRunCounts.cy.tsx b/packages/app/src/debug/DebugPendingRunCounts.cy.tsx new file mode 100644 index 000000000000..bbb525db46ed --- /dev/null +++ b/packages/app/src/debug/DebugPendingRunCounts.cy.tsx @@ -0,0 +1,27 @@ +import DebugPendingRunCounts from './DebugPendingRunCounts.vue' + +describe('', () => { + it('renders counts', () => { + cy.mount( + , + ) + + cy.contains('2 of 20').should('be.visible') + + cy.percySnapshot() + }) + + it('renders counts of zeros input is undefined', () => { + cy.mount( + , + ) + + cy.contains('0 of 0').should('be.visible') + + cy.percySnapshot() + }) +}) diff --git a/packages/app/src/debug/DebugPendingRunCounts.vue b/packages/app/src/debug/DebugPendingRunCounts.vue new file mode 100644 index 000000000000..7496381d04fc --- /dev/null +++ b/packages/app/src/debug/DebugPendingRunCounts.vue @@ -0,0 +1,28 @@ + + + diff --git a/packages/app/src/debug/DebugPendingRunSplash.cy.tsx b/packages/app/src/debug/DebugPendingRunSplash.cy.tsx new file mode 100644 index 000000000000..b130a358d8d6 --- /dev/null +++ b/packages/app/src/debug/DebugPendingRunSplash.cy.tsx @@ -0,0 +1,43 @@ +import { DebugPendingRunSplash_SpecsDocument } from '../generated/graphql' +import DebugPendingRunSplash from './DebugPendingRunSplash.vue' + +describe('', () => { + it('renders as expected', () => { + cy.mount() + + const createEvent = (completed: number, total: number) => { + return { + __typename: 'Subscription' as const, + relevantRunSpecChange: { + __typename: 'Query' as const, + currentProject: { + __typename: 'CurrentProject' as const, + id: 'fake', + relevantRunSpecs: { + __typename: 'CurrentProjectRelevantRunSpecs' as const, + current: { + __typename: 'RelevantRunSpecs' as const, + completedSpecs: completed, + totalSpecs: total, + }, + }, + }, + }, + } + } + + cy.stubSubscriptionEvent(DebugPendingRunSplash_SpecsDocument, () => { + return createEvent(3, 5) + }) + + cy.contains('3 of 5').should('be.visible') + + cy.stubSubscriptionEvent(DebugPendingRunSplash_SpecsDocument, () => { + return createEvent(5, 5) + }) + + cy.contains('5 of 5').should('be.visible') + + cy.percySnapshot() + }) +}) diff --git a/packages/app/src/debug/DebugPendingRunSplash.vue b/packages/app/src/debug/DebugPendingRunSplash.vue new file mode 100644 index 000000000000..b9830bc0dde5 --- /dev/null +++ b/packages/app/src/debug/DebugPendingRunSplash.vue @@ -0,0 +1,55 @@ + + + diff --git a/packages/app/src/debug/DebugResults.cy.tsx b/packages/app/src/debug/DebugResults.cy.tsx new file mode 100644 index 000000000000..92e98993a31d --- /dev/null +++ b/packages/app/src/debug/DebugResults.cy.tsx @@ -0,0 +1,53 @@ +import DebugResults from './DebugResults.vue' +import { DebugResultsFragmentDoc } from '../generated/graphql-test' +import { defaultMessages } from '@cy/i18n' +import { CloudRunStubs } from '@packages/graphql/test/stubCloudTypes' + +describe('', () => { + it('shows the failed icon and the number of passed, skipped, pending, failed tests passed through gql props', () => { + const cloudRuns = Object.values(CloudRunStubs) + + cy.mount(() => cloudRuns.map((cloudRun, i) => ())) + + cloudRuns.forEach((cloudRun, i) => { + cy.findByTestId(`run-result-${i}`).within(() => { + cy.get(`[title=${defaultMessages.runs.results.passed}]`).should('contain.text', cloudRun.totalPassed) + cy.get(`[title=${defaultMessages.runs.results.failed}]`).should('contain.text', cloudRun.totalFailed) + cy.get(`[title=${defaultMessages.runs.results.skipped}]`).should('contain.text', cloudRun.totalSkipped) + cy.get(`[title=${defaultMessages.runs.results.pending}]`).should('contain.text', cloudRun.totalPending) + }) + }) + + cy.percySnapshot() + }) +}) + +describe('Flaky badge tests', () => { + const mountingFragment = (flakyTests: number) => { + return ( + cy.mountFragment(DebugResultsFragmentDoc, { + onResult (result) { + if (result) { + result.totalFlakyTests = flakyTests + } + }, + render: (gqlVal) => { + return ( + + ) + }, + }) + ) + } + + it('does not show flaky component when flakyTests are < 1', () => { + mountingFragment(0) + cy.contains('Flaky').should('not.exist') + }) + + it('contains flaky badge', () => { + mountingFragment(4) + cy.findByTestId('debug-flaky-badge').contains(defaultMessages.specPage.flaky.badgeLabel) + cy.findByTestId('total-flaky-tests').contains(4) + }) +}) diff --git a/packages/app/src/debug/DebugResults.vue b/packages/app/src/debug/DebugResults.vue new file mode 100644 index 000000000000..69ad1988218d --- /dev/null +++ b/packages/app/src/debug/DebugResults.vue @@ -0,0 +1,57 @@ + + + diff --git a/packages/app/src/debug/DebugRunNumber.cy.tsx b/packages/app/src/debug/DebugRunNumber.cy.tsx new file mode 100644 index 000000000000..8cdbb0d1a18f --- /dev/null +++ b/packages/app/src/debug/DebugRunNumber.cy.tsx @@ -0,0 +1,14 @@ +import type { CloudRunStatus } from '../generated/graphql-test' +import DebugRunNumber from './DebugRunNumber.vue' + +const STATUSES: CloudRunStatus[] = ['PASSED', 'FAILED', 'CANCELLED', 'RUNNING', 'ERRORED'] + +describe('', () => { + describe('playground', () => { + STATUSES.forEach((status) => { + it(`status = '${status}'`, () => { + cy.mount() + }) + }) + }) +}) diff --git a/packages/app/src/debug/DebugRunNumber.vue b/packages/app/src/debug/DebugRunNumber.vue new file mode 100644 index 000000000000..eda2561aab9c --- /dev/null +++ b/packages/app/src/debug/DebugRunNumber.vue @@ -0,0 +1,45 @@ + + + diff --git a/packages/app/src/debug/DebugRunStates.cy.tsx b/packages/app/src/debug/DebugRunStates.cy.tsx new file mode 100644 index 000000000000..81a740df5475 --- /dev/null +++ b/packages/app/src/debug/DebugRunStates.cy.tsx @@ -0,0 +1,173 @@ +import DebugPassed from './DebugPassed.vue' +import DebugErrored from './DebugErrored.vue' +import DebugNoTests from './DebugNoTests.vue' +import DebugTimedout from './DebugTimedout.vue' +import DebugCancelledAlert from './DebugCancelledAlert.vue' +import DebugOverLimit from './DebugOverLimit.vue' + +const mockError = `We detected that the Chromium Renderer process just crashed. + +This is the equivalent to seeing the 'sad face' when Chrome dies. + +This can happen for a number of different reasons: + +- You wrote an endless loop and you must fix your own code +- You are running Docker (there is an easy fix for this: see link below) +- You are running lots of tests on a memory intense application +- You are running in a memory starved VM environment +- There are problems with your GPU / GPU drivers +- There are browser bugs in Chromium + +You can learn more including how to fix Docker here: + +https://on.cypress.io/renderer-process-crashed` + +describe('Debug page states', () => { + context('passed', () => { + it('renders', () => { + cy.mount() + + cy.percySnapshot() + }) + }) + + context('errored', () => { + it('renders for single spec', () => { + cy.mount() + + cy.contains('1 of 1 spec skipped').should('be.visible') + + cy.percySnapshot() + }) + + it('renders for plural specs', () => { + cy.mount() + + cy.contains('4 of 50 specs skipped').should('be.visible') + + cy.percySnapshot() + }) + }) + + context('no tests', () => { + it('renders', () => { + cy.mount() + + cy.percySnapshot() + }) + }) + + context('timed out', () => { + it('renders without CI information', () => { + cy.mount() + + cy.percySnapshot() + }) + + it('renders with CI information', () => { + cy.mount() + + cy.percySnapshot() + }) + }) + + context('over limit', () => { + context('Usage Exceeded', () => { + it('displays messaging for users', () => { + cy.mount( + , + ) + + cy.percySnapshot() + }) + + it('displays messaging for plan admins', () => { + cy.mount( + , + ) + + cy.percySnapshot() + }) + }) + + context('Retention Exceeded', () => { + it('displays messaging for users', () => { + cy.mount( + , + ) + + cy.percySnapshot() + }) + + it('displays messaging for plan admins', () => { + cy.mount( + , + ) + + cy.percySnapshot() + }) + }) + + context('both usage and retention exceeded', () => { + it('selects usage exceeded and displays appropriate message', () => { + cy.mount( + , + ) + + cy.percySnapshot() + }) + }) + }) + + context('cancelled', () => { + it('renders', () => { + cy.mount() + + cy.percySnapshot() + }) + }) +}) diff --git a/packages/app/src/debug/DebugSpec.cy.tsx b/packages/app/src/debug/DebugSpec.cy.tsx new file mode 100644 index 000000000000..0d1aa4481d7b --- /dev/null +++ b/packages/app/src/debug/DebugSpec.cy.tsx @@ -0,0 +1,532 @@ +import DebugSpec, { Spec, TestResults } from './DebugSpec.vue' +import { defaultMessages } from '@cy/i18n' + +const resultCounts = (min: number, max: number) => { + return { + min, + max, + } +} + +const multipleGroups: {[groupId: string]: any} = { + '123': { + os: { + name: 'Linux', + nameWithVersion: 'Linux Debian', + }, + browser: { + formattedName: 'Chrome', + formattedNameWithVersion: 'Chrome 106', + }, + groupName: 'Staging', + id: '123', + }, + '456': { + os: { + name: 'Apple', + nameWithVersion: 'macOS 12.3', + }, + browser: { + formattedName: 'Firefox', + formattedNameWithVersion: 'Firefox 95.2', + }, + groupName: 'Production', + id: '456', + }, +} + +const singleGroup: {[groupId: string]: any} = { + '123': { + os: { + name: 'Linux', + nameWithVersion: 'Linux Debian', + }, + browser: { + formattedName: 'Chrome', + formattedNameWithVersion: 'Chrome 106', + }, + groupName: 'Staging', + id: '123', + }, +} + +const testResultMultipleGroups: {[thumbprint: string]: TestResults[]} = { + 'abcd': [ + { + id: '676df87878', + titleParts: ['Login', 'Should redirect unauthenticated user to signin page'], + instance: { + id: '123', + status: 'FAILED', + groupId: '123', + hasScreenshots: true, + hasStdout: true, + hasVideo: true, + screenshotsUrl: 'www.cypress.io', + stdoutUrl: 'www.cypress.io', + videoUrl: 'www.cypress.io', + }, + }, + { + id: '78afba8sf89', + titleParts: ['Login', 'Should redirect unauthenticated user to signin page'], + instance: { + id: '456', + status: 'FAILED', + groupId: '456', + hasScreenshots: true, + hasStdout: true, + hasVideo: true, + screenshotsUrl: 'www.cypress.io', + stdoutUrl: 'www.cypress.io', + videoUrl: 'www.cypress.io', + }, + }, + ], + 'efgh': [ + { + id: '78hjkdf987d9f', + titleParts: ['Login', 'redirects to stored path after login'], + instance: { + id: '123', + status: 'FAILED', + groupId: '123', + hasScreenshots: true, + hasStdout: true, + hasVideo: true, + screenshotsUrl: 'www.cypress.io', + stdoutUrl: 'www.cypress.io', + videoUrl: 'www.cypress.io', + }, + }, + { + id: '283sbd0snd8', + titleParts: ['Login', 'redirects to stored path after login'], + instance: { + id: '456', + status: 'FAILED', + groupId: '456', + hasScreenshots: true, + hasStdout: true, + hasVideo: true, + screenshotsUrl: 'www.cypress.io', + stdoutUrl: 'www.cypress.io', + videoUrl: 'www.cypress.io', + }, + }, + ], +} + +const testResultSingleGroup: {[thumbprint: string]: TestResults[]} = { + 'abcd': [ + { + id: '676df87878', + titleParts: ['Login', 'Should redirect unauthenticated user to signin page'], + instance: { + id: '123', + status: 'FAILED', + groupId: '123', + hasScreenshots: true, + hasStdout: true, + hasVideo: true, + screenshotsUrl: 'www.cypress.io', + stdoutUrl: 'www.cypress.io', + videoUrl: 'www.cypress.io', + }, + }, + ], + 'efgh': [ + { + id: '78hjkdf987d9f', + titleParts: ['Login', 'redirects to stored path after login'], + instance: { + id: '123', + status: 'FAILED', + groupId: '123', + hasScreenshots: true, + hasStdout: true, + hasVideo: true, + screenshotsUrl: 'www.cypress.io', + stdoutUrl: 'www.cypress.io', + videoUrl: 'www.cypress.io', + }, + }, + ], +} + +describe(' with multiple test results', () => { + const spec = { + id: '8879798756s88d', + path: 'cypress/tests/', + fileName: 'auth', + fileExtension: '.spec.ts', + fullPath: 'cypress/tests/auth.spec.ts', + testsPassed: resultCounts(22, 22), + testsFailed: resultCounts(2, 2), + testsPending: resultCounts(1, 1), + specDuration: { + min: 143000, + max: 143000, + }, + } + + it('mounts correctly for single groups', () => { + cy.mount(() => ( +
+ +
+ )) + + cy.findByTestId('debug-spec-item').children().should('have.length', 3) + cy.findByTestId('spec-contents').children().should('have.length', 2) + cy.findByTestId('stats-metadata').children().should('have.length', 4) + cy.findByTestId('spec-path').should('have.text', 'cypress/tests/auth.spec.ts') + cy.contains('auth').should('be.visible') + cy.findByTestId('run-failures').should('not.be.disabled') + .contains(defaultMessages.debugPage.runFailures.btn) + + cy.findByTestId('spec-header-metadata').children().should('have.length', 3) + cy.findByTestId('debugHeader-results').should('be.visible') + + // testing debugResultsCalc method + cy.findByTestId('runResults-failed-count').should('have.text', 'failed 2') + cy.findByTestId('runResults-passed-count').should('have.text', 'passed 22') + cy.findByTestId('runResults-pending-count').should('have.text', 'pending 1') + cy.findByTestId('metaData-Results-spec-duration').should('have.text', 'spec-duration 02:23') + + cy.findAllByTestId('test-group').each((ele) => { + cy.wrap(ele).within(() => { + cy.findByTestId('debug-artifacts').should('not.be.visible') + cy.findByTestId('test-row').realHover().then(() => { + cy.findByTestId('debug-artifacts').should('be.visible') + }) + }) + }) + + cy.percySnapshot() + }) +}) + +describe(' responsive UI', () => { + it('renders complete UI on smaller viewports', { viewportHeight: 300, viewportWidth: 580 }, () => { + const spec: Spec = { + id: '8879798756s88d', + path: 'cypress/tests/', + fileName: 'AlertBar', + fileExtension: '.spec.ts', + fullPath: 'cypress/tests/AlertBar.spec.ts', + testsPassed: resultCounts(2, 2), + testsFailed: resultCounts(22, 22), + testsPending: resultCounts(1, 1), + specDuration: { + min: 143000, + max: 143000, + }, + } + + cy.mount(() => ( +
+ +
+ )) + + cy.findByTestId('spec-contents').children().should('have.length', 2) + cy.findByTestId('spec-path').should('have.text', 'cypress/tests/AlertBar.spec.ts') + cy.contains('AlertBar').should('be.visible') + cy.findByTestId('run-failures').should('be.visible') + + cy.percySnapshot() + }) + + it('shows complete spec component header with long relative filePath', { viewportHeight: 400, viewportWidth: 700 }, () => { + const spec: Spec = { + id: '547a0dG90s7f', + path: 'src/shared/frontend/cow/packages/foo/cypress/tests/e2e/components/', + fileName: 'AlertBar', + fileExtension: '.spec.ts', + fullPath: 'src/shared/frontend/cow/packages/foo/cypress/tests/e2e/components/AlertBar.spec.ts', + testsPassed: resultCounts(2, 2), + testsFailed: resultCounts(22, 22), + testsPending: resultCounts(1, 1), + specDuration: { + min: 143000, + max: 143000, + }, + } + + const testResult: {[thumprint: string]: TestResults[]} = { + 'abcd': [ + { + id: '676df87878', + titleParts: ['Login', 'Should redirect unauthenticated user to signin page', 'Enter Password', '.hook() should be called'], + instance: { + id: '123', + status: 'FAILED', + groupId: '123', + hasScreenshots: true, + hasStdout: true, + hasVideo: true, + screenshotsUrl: 'www.cypress.io', + stdoutUrl: 'www.cypress.io', + videoUrl: 'www.cypress.io', + }, + }, + ], + 'efgh': [ + { + id: '78hjkdf987d9f', + titleParts: ['Login', 'redirects to stored path after login'], + instance: { + id: '123', + status: 'FAILED', + groupId: '123', + hasScreenshots: true, + hasStdout: true, + hasVideo: true, + screenshotsUrl: 'www.cypress.io', + stdoutUrl: 'www.cypress.io', + videoUrl: 'www.cypress.io', + }, + }, + ], + } + + cy.mount(() => ( +
+ +
+ )) + + cy.findByTestId('spec-path').should('have.css', 'text-overflow', 'ellipsis') + cy.findByTestId('run-failures').should('be.visible') + + cy.percySnapshot() + }) +}) + +describe('testing groupings', () => { + it('tests debug spec with multiple groups', { viewportWidth: 1032 }, () => { + const spec = { + id: '8879798756s88d', + path: 'cypress/tests/', + fileName: 'auth', + fileExtension: '.spec.ts', + fullPath: 'cypress/tests/auth.spec.ts', + testsPassed: resultCounts(22, 23), + testsFailed: resultCounts(1, 2), + testsPending: resultCounts(1, 2), + specDuration: { + min: 143000, + max: 220000, + }, + } + + cy.mount(() => ( +
+ +
+ )) + + cy.findByTestId('debug-spec-item').children().should('have.length', 3) + cy.findByTestId('spec-contents').children().should('have.length', 2) + cy.findByTestId('spec-header-metadata').within(() => { + cy.findByTestId('stats-metadata').children().should('have.length', 5) + }) + + cy.findByTestId('spec-header-metadata').children().should('have.length', 3) + + cy.findAllByTestId('test-group').each((el) => { + cy.wrap(el).within(() => { + cy.findAllByTestId('grouped-row').should('have.length', 2) + }) + }) + }) + + it('test results with multiple groups and repeated browsers and a single group', { viewportWidth: 1200 }, () => { + const spec = { + id: '8879798756s88d', + path: 'cypress/tests/', + fileName: 'Debug', + fileExtension: '.spec.ts', + fullPath: 'cypress/tests/Debug.spec.ts', + testsPassed: resultCounts(22, 25), + testsFailed: resultCounts(1, 2), + testsPending: resultCounts(1, 3), + specDuration: { + min: 143000, + max: 220000, + }, + } + + const repeatedValueGroups: {[groupId: string]: any} = { + '456': { + os: { + name: 'Apple', + nameWithVersion: 'MacOS 11.4', + }, + browser: { + formattedName: 'Chrome', + formattedNameWithVersion: 'Chrome 106', + }, + groupName: 'Staging', + id: '456', + }, + '123': { + os: { + name: 'Apple', + nameWithVersion: 'MacOS 10.0', + }, + browser: { + formattedName: 'Chrome', + formattedNameWithVersion: 'Chrome 106', + }, + groupName: 'Production', + id: '123', + }, + } + + const tests: {[thumbprint: string]: TestResults[]} = { + 'abcd': [ + { + id: '676df87878', + titleParts: ['Login', 'Should redirect unauthenticated user to signin page'], + instance: { + id: '123', + status: 'FAILED', + groupId: '123', + hasScreenshots: false, + hasStdout: false, + hasVideo: false, + }, + }, + { + id: '78afba8sf89', + titleParts: ['Login', 'Should redirect unauthenticated user to signin page'], + instance: { + id: '456', + status: 'FAILED', + groupId: '456', + hasScreenshots: false, + hasStdout: false, + hasVideo: false, + }, + }, + ], + 'efgh': [ + { + id: '78hjkdf987d9f', + titleParts: ['Login', 'redirects to stored path after login'], + instance: { + id: '456', + status: 'FAILED', + groupId: '456', + hasScreenshots: false, + hasStdout: false, + hasVideo: false, + }, + }, + ], + } + + cy.mount(() => ( +
+ +
+ )) + + // testing debugResultsCalc method + cy.findByTestId('runResults-failed-count').should('have.text', 'failed 1-2') + cy.findByTestId('runResults-passed-count').should('have.text', 'passed 22-25') + cy.findByTestId('runResults-pending-count').should('have.text', 'pending 1-3') + cy.findByTestId('metaData-Results-spec-duration').should('have.text', 'spec-duration 02:23-03:40') + + // testing single OS and browser UI + cy.findByTestId('metaData-Results-operating-system-groups').should('have.text', 'operating-system-groups 1 operating system') + cy.findByTestId('metaData-Results-browser-groups').should('have.text', 'browser-groups 1 browser') + + cy.findAllByTestId('test-group').should('have.length', 2) + cy.findAllByTestId('grouped-row').should('have.length', 3) + }) +}) + +describe('Run Failures button', () => { + const spec: Spec = { + id: '1', + path: 'cypress/tests/', + fileName: 'auth', + fileExtension: '.spec.ts', + fullPath: 'cypress/tests/auth.spec.ts', + testsPassed: resultCounts(22, 25), + testsFailed: resultCounts(1, 3), + testsPending: resultCounts(1, 3), + specDuration: { + min: 143000, + max: 220000, + }, + } + + it('is disabled if spec is not found locally', () => { + cy.mount(() => ( +
+ +
+ )) + + cy.findByTestId('run-failures') + .should('have.attr', 'aria-disabled', 'disabled') + .should('not.have.attr', 'href') + + cy.findByTestId('run-failures').realHover() + + cy.findByTestId('run-all-failures-tooltip').should('be.visible').contains('Spec was not found locally') + + cy.percySnapshot() + }) + + it('is disabled if run testing-type differs from the current testing-type', () => { + cy.mount(() => ( +
+ +
+ )) + + cy.findByTestId('run-failures') + .should('have.attr', 'aria-disabled', 'disabled') + .should('not.have.attr', 'href') + + cy.findByTestId('run-failures').realHover() + + cy.findByTestId('run-all-failures-tooltip').should('be.visible').within(() => { + cy.contains('span', 'There are 2 e2e tests failing in this spec. To run them locally switch to e2e testing.') + cy.contains('button', 'Switch to e2e testing').click() + + cy.get('@switchTestingType').should('have.been.calledWith', 'e2e') + }) + }) + + it('is enabled if found locally and same testing type', () => { + cy.mount(() => ( +
+ +
+ )) + + cy.findByTestId('run-failures') + .should('have.attr', 'href', '#/specs/runner?file=cypress/tests/auth.spec.ts&mode=debug') + .and('not.have.attr', 'aria-disabled') + }) +}) diff --git a/packages/app/src/debug/DebugSpec.vue b/packages/app/src/debug/DebugSpec.vue new file mode 100644 index 000000000000..00ad79ff95bb --- /dev/null +++ b/packages/app/src/debug/DebugSpec.vue @@ -0,0 +1,262 @@ + + diff --git a/packages/app/src/debug/DebugSpecLimitBanner.cy.tsx b/packages/app/src/debug/DebugSpecLimitBanner.cy.tsx new file mode 100644 index 000000000000..739f65e1488d --- /dev/null +++ b/packages/app/src/debug/DebugSpecLimitBanner.cy.tsx @@ -0,0 +1,36 @@ +import DebugSpecLimitBanner from './DebugSpecLimitBanner.vue' +import { defaultMessages } from '@cy/i18n' + +describe('', () => { + it('renders expected copy and link', () => { + cy.mount(() => ( + + )) + + cy.contains(defaultMessages.debugPage.limit.title) + cy.contains(defaultMessages.debugPage.limit.message.split('|')[0].trim().replace('{n}', '120')) + cy.contains(defaultMessages.debugPage.limit.link) + + cy.viewport(1000, 400) + cy.percySnapshot('large viewport') + + cy.viewport(600, 400) + cy.percySnapshot('small viewport') + }) + + it('does not render link if no url provided', () => { + cy.mount(() => ( + + )) + + cy.get('a').should('not.exist') + + cy.percySnapshot() + }) +}) diff --git a/packages/app/src/debug/DebugSpecLimitBanner.vue b/packages/app/src/debug/DebugSpecLimitBanner.vue new file mode 100644 index 000000000000..7bbf44825dee --- /dev/null +++ b/packages/app/src/debug/DebugSpecLimitBanner.vue @@ -0,0 +1,44 @@ + + + + + diff --git a/packages/app/src/debug/DebugSpecList.cy.tsx b/packages/app/src/debug/DebugSpecList.cy.tsx new file mode 100644 index 000000000000..ad1b85aa5dfb --- /dev/null +++ b/packages/app/src/debug/DebugSpecList.cy.tsx @@ -0,0 +1,93 @@ +import { SetTestsForDebugDocument } from '../generated/graphql-test' +import DebugSpecList from './DebugSpecList.vue' +import type { CloudDebugSpec } from './utils/DebugMapping' + +const specs: CloudDebugSpec[] = [{ + spec: { + id: '123', + basename: '', + extension: '.ts', + groupIds: [], + shortPath: '', + specDuration: { + max: 10, + min: 10, + }, + status: 'FAILED', + testsFailed: { + max: 2, + min: 2, + }, + testsPassed: { + max: 0, + min: 0, + }, + testsPending: { + max: 0, + min: 0, + }, + path: 'path/to/test.cy.ts', + }, + tests: { + 'ab123': [{ + id: '123', + specId: '', + titleParts: ['Test', 'make it work'], + duration: 10, + instance: { + id: '123', + groupId: '', + hasScreenshots: false, + hasStdout: false, + hasVideo: false, + status: 'FAILED', + screenshotsUrl: '', + stdoutUrl: '', + videoUrl: '', + totalFailed: 3, + totalPassed: 0, + totalPending: 0, + totalRunning: 0, + totalSkipped: 0, + }, + isFlaky: false, + testUrl: '', + thumbprint: 'ab123', + title: 'Test > make it work', + }], + }, + testingType: 'component', + foundLocally: true, + matchesCurrentTestingType: true, + groups: { '123': { + testingType: 'component', + browser: { + id: '123', + formattedName: '', + formattedNameWithVersion: '', + }, + groupName: 'group1', + id: '123', + os: { + id: '123', + name: 'MacOS', + nameWithVersion: 'MacOS 10', + }, + } }, +}] + +describe('', () => { + it('calls mutation to set tests for debug mode in runner on mount', (done) => { + cy.stubMutationResolver(SetTestsForDebugDocument, (defineResult, variables) => { + expect(variables.testsBySpec[0].specPath).to.eql(specs[0].spec.path) + expect(variables.testsBySpec[0].tests[0]).to.eql(specs[0].tests['ab123'][0].titleParts.join(' ')) + done() + }) + + cy.mount(() => ( + + )) + }) +}) diff --git a/packages/app/src/debug/DebugSpecList.vue b/packages/app/src/debug/DebugSpecList.vue new file mode 100644 index 000000000000..d02b43079b67 --- /dev/null +++ b/packages/app/src/debug/DebugSpecList.vue @@ -0,0 +1,160 @@ + + + diff --git a/packages/app/src/debug/DebugTimedout.vue b/packages/app/src/debug/DebugTimedout.vue new file mode 100644 index 000000000000..d8bf810b13fa --- /dev/null +++ b/packages/app/src/debug/DebugTimedout.vue @@ -0,0 +1,46 @@ + + + diff --git a/packages/app/src/debug/GroupedDebugFailedTest.cy.tsx b/packages/app/src/debug/GroupedDebugFailedTest.cy.tsx new file mode 100644 index 000000000000..8b9055e6a92e --- /dev/null +++ b/packages/app/src/debug/GroupedDebugFailedTest.cy.tsx @@ -0,0 +1,90 @@ +import type { TestResults } from './DebugSpec.vue' +import GroupedDebugFailedTest from './GroupedDebugFailedTest.vue' + +describe('', () => { + const testResult: TestResults[] = [ + { + id: '676df87878', + titleParts: ['Login', 'Should redirect unauthenticated user to signin page'], + instance: { + id: '123', + groupId: '123', + status: 'FAILED', + hasScreenshots: true, + screenshotsUrl: 'https://cloud.cypress.io/projects/123/runs/456/overview/789/screenshots', + hasStdout: true, + stdoutUrl: 'https://cloud.cypress.io/projects/123/runs/456/overview/789/stdout', + hasVideo: true, + videoUrl: 'https://cloud.cypress.io/projects/123/runs/456/overview/789/video', + }, + }, + { + id: 'adfkd33829', + titleParts: ['Groups', 'Testing across multiple groups'], + instance: { + id: '456', + status: 'FAILED', + groupId: '456', + hasScreenshots: true, + screenshotsUrl: 'https://cloud.cypress.io/projects/123/runs/456/overview/789/screenshots', + hasStdout: true, + stdoutUrl: 'https://cloud.cypress.io/projects/123/runs/456/overview/789/stdout', + hasVideo: true, + videoUrl: 'https://cloud.cypress.io/projects/123/runs/456/overview/789/video', + }, + }, + ] + + const groups = [ + { + id: '123', + groupName: 'Staging', + os: { + id: '123', + name: 'Linux', + nameWithVersion: 'Linux Debian', + }, + browser: { + id: '123', + formattedName: 'Chrome', + formattedNameWithVersion: 'Chrome 106', + }, + }, + { + id: '456', + groupName: 'Production', + os: { + id: '456', + name: 'Windows', + nameWithVersion: 'Windows 11.2', + }, + browser: { + id: '456', + formattedName: 'Electron', + formattedNameWithVersion: 'Electron 106', + }, + }, + ] + + it('mounts correctly and shows artifacts on hover', () => { + cy.mount(() => ( +
+ +
+ )) + + cy.get('body').click('topLeft') + // 👆 this click is to address some flake in CI where this component renders already in the hover state + // example: https://cloud.cypress.io/projects/ypt4pf/runs/43417/overview/18107774-3213-47f0-902e-79502a832c34/video?reviewViewBy=FAILED&utm_source=Dashboard&utm_medium=Share+URL&utm_campaign=Video + // this should avoid whatever situation leads to the appearance of being hovered right after mount. + + cy.findAllByTestId(`grouped-row`).should('have.length', 2).each((el) => cy.wrap(el).within(() => { + cy.findByTestId('debug-artifacts').should('not.be.visible') + cy.wrap(el).realHover() + cy.findByTestId('debug-artifacts').should('be.visible').children().should('have.length', 3) + cy.findByTestId('stats-metadata').children().should('have.length', 3) + })) + + cy.percySnapshot() + }) +}) diff --git a/packages/app/src/debug/GroupedDebugFailedTest.vue b/packages/app/src/debug/GroupedDebugFailedTest.vue new file mode 100644 index 000000000000..7a3506b3b822 --- /dev/null +++ b/packages/app/src/debug/GroupedDebugFailedTest.vue @@ -0,0 +1,66 @@ + + + diff --git a/packages/app/src/debug/LayeredBrowserIcons.cy.tsx b/packages/app/src/debug/LayeredBrowserIcons.cy.tsx new file mode 100644 index 000000000000..3482989bc10a --- /dev/null +++ b/packages/app/src/debug/LayeredBrowserIcons.cy.tsx @@ -0,0 +1,33 @@ +import LayeredBrowserIcons from './LayeredBrowserIcons.vue' +import type { BrowserType } from './LayeredBrowserIcons.vue' + +describe('', () => { + const browsers: BrowserType[] = ['CHROME', 'CHROME-CANARY', 'FIREFOX', 'WEBKIT', 'EDGE', 'ELECTRON'] + + it('mounts correctly for single browser', () => { + browsers.forEach((ele) => { + cy.mount(() => ( +
+ +
+ )) + + cy.findByTestId('layered-browser-icons').children().should('have.length', 1) + }) + }) + + it('mounts correctly for multiple browsers', () => { + cy.mount(() => ( +
+ + + + + + +
+ )) + + cy.percySnapshot() + }) +}) diff --git a/packages/app/src/debug/LayeredBrowserIcons.vue b/packages/app/src/debug/LayeredBrowserIcons.vue new file mode 100644 index 000000000000..f35b1e6a016c --- /dev/null +++ b/packages/app/src/debug/LayeredBrowserIcons.vue @@ -0,0 +1,57 @@ + + + diff --git a/packages/app/src/debug/StatsMetadata.cy.tsx b/packages/app/src/debug/StatsMetadata.cy.tsx new file mode 100644 index 000000000000..7f91dbac2211 --- /dev/null +++ b/packages/app/src/debug/StatsMetadata.cy.tsx @@ -0,0 +1,210 @@ +import StatsMetadata from './StatsMetadata.vue' + +describe('', () => { + const group_linux_chrome = { + id: '123', + os: { + id: '123', + name: 'Linux', + nameWithVersion: 'Linux Debian', + }, + browser: { + id: '123', + formattedName: 'Chrome', + formattedNameWithVersion: 'Chrome 106', + }, + groupName: 'Staging', + } + + const group_macos_edge = { + id: '123', + os: { + id: '123', + name: 'Apple', + nameWithVersion: 'macOS 12.3', + }, + browser: { + id: '123', + formattedName: 'Edge', + formattedNameWithVersion: 'Edge 100.2', + }, + groupName: 'Production', + } + + const group_windows_webkit = { + id: '123', + os: { + id: '123', + name: 'Windows', + nameWithVersion: 'Windows 12.3', + }, + browser: { + id: '123', + formattedName: 'Webkit', + formattedNameWithVersion: 'Webkit 108', + }, + groupName: 'Production', + } + + const group_windows_chrome = { + id: '123', + os: { + id: '123', + name: 'Windows', + nameWithVersion: 'Windows 12.3', + }, + browser: { + id: '123', + formattedName: 'Chrome', + formattedNameWithVersion: 'Chrome 106', + }, + groupName: 'Production', + } + + it('single values', () => { + const testingOrder = ['spec-duration 2:23', 'operating-system Linux Debian', 'browser Chrome 106', 'testing-type Component'] + + cy.mount(() => ( +
+ +
+ )) + + cy.findByTestId('stats-metadata').children().should('have.length', 4) + cy.findByTestId('stats-metadata').children().each((ele, index) => { + cy.wrap(ele).should('have.text', testingOrder[index]) + cy.findByTestId(testingOrder[index]).should('be.visible') + }) + + cy.percySnapshot() + }) + + it('group values', () => { + const testingOrder = [ + 'spec-duration 2:23-3:40', + 'group-server 3 groups', + 'operating-system-groups 3 operating systems', + 'browser-groups 3 browsers', + 'testing-type E2E', + ] + + cy.mount(() => ( +
+ +
+ )) + + cy.findByTestId('stats-metadata').children().should('have.length', 5) + cy.findByTestId('stats-metadata').children().each((ele, index) => { + cy.wrap(ele).should('have.text', testingOrder[index]) + cy.findByTestId(testingOrder[index]).should('be.visible') + }) + + cy.percySnapshot() + }) + + it('group values with 1 browser', () => { + const testingOrder = [ + 'spec-duration 2:23-3:40', + 'group-server 2 groups', + 'operating-system-groups 2 operating systems', + 'browser-groups 1 browser', + 'testing-type E2E', + ] + + cy.mount(() => ( +
+ +
+ )) + + cy.findByTestId('stats-metadata').children().should('have.length', 5) + cy.findByTestId('stats-metadata').children().each((ele, index) => { + cy.wrap(ele).should('have.text', testingOrder[index]) + cy.findByTestId(testingOrder[index]).should('be.visible') + }) + }) + + it('group values with 1 os', () => { + const testingOrder = [ + 'spec-duration 2:23-3:40', + 'group-server 2 groups', + 'operating-system-groups 1 operating system', + 'browser-groups 2 browsers', + 'testing-type E2E', + ] + + cy.mount(() => ( +
+ +
+ )) + + cy.findByTestId('stats-metadata').children().should('have.length', 5) + cy.findByTestId('stats-metadata').children().each((ele, index) => { + cy.wrap(ele).should('have.text', testingOrder[index]) + cy.findByTestId(testingOrder[index]).should('be.visible') + }) + }) + + it('shows the correct groupName', () => { + cy.mount(() => ( +
+ +
+ )) + + cy.findByTestId('group_name Staging').should('be.visible') + }) + + // This tests the functionality for arrMapping in StatsMetadata + it('only displays unique browsers and calculates correct number of OS', () => { + cy.mount(() => ( +
+ +
+ )) + + const testingOrder = [ + 'spec-duration 2:23-3:40', + 'group-server 3 groups', + 'operating-system-groups 2 operating systems', + 'browser-groups 2 browsers', + 'testing-type Component', + ] + + cy.findByTestId('stats-metadata').children().each((ele, index) => { + cy.wrap(ele).should('have.text', testingOrder[index]) + cy.findByTestId(testingOrder[index]).should('be.visible') + }) + }) +}) diff --git a/packages/app/src/debug/StatsMetadata.vue b/packages/app/src/debug/StatsMetadata.vue new file mode 100644 index 000000000000..c093072d39d7 --- /dev/null +++ b/packages/app/src/debug/StatsMetadata.vue @@ -0,0 +1,199 @@ + + + + diff --git a/packages/app/src/debug/empty/DebugEmptyStates.cy.tsx b/packages/app/src/debug/empty/DebugEmptyStates.cy.tsx new file mode 100644 index 000000000000..dad90a051224 --- /dev/null +++ b/packages/app/src/debug/empty/DebugEmptyStates.cy.tsx @@ -0,0 +1,66 @@ +import DebugNotLoggedIn from './DebugNotLoggedIn.vue' +import DebugNoProject from './DebugNoProject.vue' +import DebugNoRuns from './DebugNoRuns.vue' +import DebugLoading from './DebugLoading.vue' +import DebugError from './DebugError.vue' +import { useLoginConnectStore } from '@packages/frontend-shared/src/store/login-connect-store' + +describe('Debug page empty states', () => { + context('not logged in', () => { + it('renders', () => { + const loginConnectStore = useLoginConnectStore() + + // We need to set isLoggedIn so that CloudConnectButton shows the correct state + loginConnectStore.setUserFlag('isLoggedIn', false) + + cy.mount() + + cy.findByRole('link', { name: 'Learn about debugging CI failures in Cypress' }).should('have.attr', 'href', 'https://on.cypress.io/debug-page?utm_source=Binary%3A+Launchpad&utm_medium=Debug+Tab&utm_campaign=Learn+More') + + cy.percySnapshot() + }) + }) + + context('no project', () => { + it('renders', () => { + const loginConnectStore = useLoginConnectStore() + + // We need to set isLoggedIn so that CloudConnectButton shows the correct state + loginConnectStore.setUserFlag('isLoggedIn', true) + + cy.mount() + + cy.findByRole('link', { name: 'Learn about project setup in Cypress' }).should('have.attr', 'href', 'https://on.cypress.io/adding-new-project?utm_source=Binary%3A+Launchpad&utm_medium=Debug+Tab&utm_campaign=Learn+More') + + cy.percySnapshot() + }) + }) + + context('no runs', () => { + it('renders', () => { + cy.mount() + + cy.findByRole('link', { name: 'Learn about recording a run to Cypress Cloud' }).should('have.attr', 'href', 'https://on.cypress.io/cypress-run-record-key?utm_source=Binary%3A+Launchpad&utm_medium=Debug+Tab&utm_campaign=Learn+More') + + cy.percySnapshot() + }) + }) + + context('loading', () => { + it('renders', () => { + cy.mount() + + cy.percySnapshot() + }) + }) + + context('error', () => { + it('renders', () => { + cy.mount() + + cy.findByRole('link', { name: 'Learn about debugging CI failures in Cypress' }).should('have.attr', 'href', 'https://on.cypress.io/debug-page?utm_source=Binary%3A+Launchpad&utm_medium=Debug+Tab&utm_campaign=Learn+More') + + cy.percySnapshot() + }) + }) +}) diff --git a/packages/app/src/debug/empty/DebugEmptyView.vue b/packages/app/src/debug/empty/DebugEmptyView.vue new file mode 100644 index 000000000000..73899f3a8d98 --- /dev/null +++ b/packages/app/src/debug/empty/DebugEmptyView.vue @@ -0,0 +1,76 @@ + + + diff --git a/packages/app/src/debug/empty/DebugError.vue b/packages/app/src/debug/empty/DebugError.vue new file mode 100644 index 000000000000..5580ac82dbe8 --- /dev/null +++ b/packages/app/src/debug/empty/DebugError.vue @@ -0,0 +1,29 @@ + + + diff --git a/packages/app/src/debug/empty/DebugLoading.vue b/packages/app/src/debug/empty/DebugLoading.vue new file mode 100644 index 000000000000..dc5c10d2f9f8 --- /dev/null +++ b/packages/app/src/debug/empty/DebugLoading.vue @@ -0,0 +1,90 @@ + + + diff --git a/packages/app/src/debug/empty/DebugLoadingDivider.vue b/packages/app/src/debug/empty/DebugLoadingDivider.vue new file mode 100644 index 000000000000..5dc6a66d0705 --- /dev/null +++ b/packages/app/src/debug/empty/DebugLoadingDivider.vue @@ -0,0 +1,3 @@ + diff --git a/packages/app/src/debug/empty/DebugNoProject.vue b/packages/app/src/debug/empty/DebugNoProject.vue new file mode 100644 index 000000000000..8689b49889a1 --- /dev/null +++ b/packages/app/src/debug/empty/DebugNoProject.vue @@ -0,0 +1,22 @@ + + + diff --git a/packages/app/src/debug/empty/DebugNoRuns.vue b/packages/app/src/debug/empty/DebugNoRuns.vue new file mode 100644 index 000000000000..eaded505810a --- /dev/null +++ b/packages/app/src/debug/empty/DebugNoRuns.vue @@ -0,0 +1,22 @@ + + + diff --git a/packages/app/src/debug/empty/DebugNotLoggedIn.vue b/packages/app/src/debug/empty/DebugNotLoggedIn.vue new file mode 100644 index 000000000000..f1cc82732a7e --- /dev/null +++ b/packages/app/src/debug/empty/DebugNotLoggedIn.vue @@ -0,0 +1,22 @@ + + + diff --git a/packages/app/src/debug/empty/DebugTestLoadingContainer.vue b/packages/app/src/debug/empty/DebugTestLoadingContainer.vue new file mode 100644 index 000000000000..4bc676fe7b1c --- /dev/null +++ b/packages/app/src/debug/empty/DebugTestLoadingContainer.vue @@ -0,0 +1,45 @@ + + + diff --git a/packages/app/src/debug/utils/DebugMapping.ts b/packages/app/src/debug/utils/DebugMapping.ts new file mode 100644 index 000000000000..cbd8627dd6f9 --- /dev/null +++ b/packages/app/src/debug/utils/DebugMapping.ts @@ -0,0 +1,69 @@ +import type { DebugSpecListSpecFragment, DebugSpecListTestsFragment, DebugLocalSpecsFragment, TestingTypeEnum, DebugSpecListGroupsFragment } from '../../generated/graphql' +import { posixify } from '../../paths' + +type DebugSpecsArgs = { + specs: readonly DebugSpecListSpecFragment[] + tests: readonly DebugSpecListTestsFragment[] + groups: readonly DebugSpecListGroupsFragment[] + localSpecs: readonly DebugLocalSpecsFragment[] + currentTestingType: TestingTypeEnum +} + +export type CloudDebugSpec = { + spec: DebugSpecListSpecFragment + tests: { [thumbprint: string]: DebugSpecListTestsFragment[] } + groups: { [groupId: string]: DebugSpecListGroupsFragment } + testingType: TestingTypeEnum + matchesCurrentTestingType: boolean + foundLocally: boolean +} + +export const specsList = ({ specs, tests, localSpecs, currentTestingType, groups }: DebugSpecsArgs): CloudDebugSpec[] => { + const localSpecsSet = new Set(localSpecs.map(((spec) => posixify(spec.relative)))) + const groupsMap = groups.reduce<{[id: string]: DebugSpecListGroupsFragment}>((acc, group) => ({ ...acc, [group.id]: group }), {}) + + const mappedTests = tests.reduce<{[id: string]: CloudDebugSpec}>((acc, curr) => { + let debugResult = acc[curr.specId] + + if (!debugResult) { + const foundSpec = specs.find((spec) => spec.id === curr.specId) + + if (!foundSpec) { + // TODO better handle error case by showing an error message rather than just throwing + // an error. Will be addressed in https://github.com/cypress-io/cypress/issues/25639 + throw new Error(`Could not find spec for id ${ curr.specId}`) + } + + const groupsMapping = (foundSpec.groupIds || []).reduce<{[grpId: string]: DebugSpecListGroupsFragment}>((acc, id) => { + if (id) { + acc[id] = groupsMap[id] + } + + return acc + }, {}) + // The testingType will not differ between groups + const testingType = Object.values(groupsMapping)[0].testingType as TestingTypeEnum + + debugResult = { + spec: foundSpec, + tests: { [curr.thumbprint]: [curr] }, + groups: groupsMapping, + testingType, + matchesCurrentTestingType: testingType === currentTestingType, + foundLocally: localSpecsSet.has(foundSpec.path), + } + + acc[curr.specId] = debugResult + } else { + if (!debugResult.tests[curr.thumbprint]) { + debugResult.tests[curr.thumbprint] = [curr] + } else { + debugResult.tests[curr.thumbprint].push(curr) + } + } + + return acc + }, {}) + + return Object.values(mappedTests) +} diff --git a/packages/app/src/debug/utils/debugArtifacts.ts b/packages/app/src/debug/utils/debugArtifacts.ts new file mode 100644 index 000000000000..7be7b0424603 --- /dev/null +++ b/packages/app/src/debug/utils/debugArtifacts.ts @@ -0,0 +1,24 @@ +import type { CloudRunInstance } from '@packages/data-context/src/gen/graphcache-config.gen' +import type { useI18n } from '@cy/i18n' + +export type ArtifactType = 'TERMINAL_LOG' | 'IMAGE_SCREENSHOT' | 'PLAY' + +export type DebugArtifact = { icon: ArtifactType, text: string, url: string } + +export const getDebugArtifacts = (instance: CloudRunInstance | null, t: ReturnType['t']): DebugArtifact[] => { + const debugArtifacts: DebugArtifact[] = [] + + if (instance?.hasStdout && instance.stdoutUrl) { + debugArtifacts.push({ icon: 'TERMINAL_LOG', text: t('debugPage.artifacts.stdout'), url: instance.stdoutUrl }) + } + + if (instance?.hasScreenshots && instance.screenshotsUrl) { + debugArtifacts.push({ icon: 'IMAGE_SCREENSHOT', text: t('debugPage.artifacts.screenshots'), url: instance.screenshotsUrl }) + } + + if (instance?.hasVideo && instance.videoUrl) { + debugArtifacts.push({ icon: 'PLAY', text: t('debugPage.artifacts.video'), url: instance.videoUrl }) + } + + return debugArtifacts +} diff --git a/packages/app/src/layouts/default.vue b/packages/app/src/layouts/default.vue index 0689d0636085..e53d539541fa 100644 --- a/packages/app/src/layouts/default.vue +++ b/packages/app/src/layouts/default.vue @@ -5,7 +5,7 @@ 'grid-rows-[64px,1fr]': showHeader }" > - @@ -53,7 +53,6 @@ diff --git a/packages/app/src/navigation/SidebarNavigation.cy.tsx b/packages/app/src/navigation/SidebarNavigation.cy.tsx index b7d21dc6dd92..2af4905e297d 100644 --- a/packages/app/src/navigation/SidebarNavigation.cy.tsx +++ b/packages/app/src/navigation/SidebarNavigation.cy.tsx @@ -1,15 +1,54 @@ import SidebarNavigation from './SidebarNavigation.vue' import { defaultMessages } from '@cy/i18n' +import { CloudRunStatus, SidebarNavigationFragment, SidebarNavigationFragmentDoc, SideBarNavigation_SetPreferencesDocument } from '../generated/graphql-test' +import { CloudRunStubs } from '@packages/graphql/test/stubCloudTypes' +import { cloneDeep } from 'lodash' +import { IATR_RELEASE } from '@packages/frontend-shared/src/utils/isAllowedFeature' +import { useLoginConnectStore } from '@packages/frontend-shared/src/store/login-connect-store' +import interval from 'human-interval' -function mountComponent (initialNavExpandedVal = true) { - cy.mount(() => { - return ( -
-
- +function mountComponent (props: { initialNavExpandedVal?: boolean, cloudProject?: { status: CloudRunStatus, numFailedTests: number }, isLoading?: boolean, online?: boolean} = {}) { + const withDefaults = { initialNavExpandedVal: false, isLoading: false, online: true, ...props } + let _gql: SidebarNavigationFragment + + cy.stubMutationResolver(SideBarNavigation_SetPreferencesDocument, (defineResult) => { + _gql.localSettings.preferences.isSideNavigationOpen = !_gql.localSettings.preferences.isSideNavigationOpen + + return defineResult({ setPreferences: _gql }) + }) + + cy.mountFragment(SidebarNavigationFragmentDoc, { + variableTypes: { + runNumber: 'Int', + hasCurrentRun: 'Boolean', + }, + variables: { + runNumber: 1, + hasCurrentRun: true, + }, + 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 + } else { + gql.currentProject.cloudProject = null + } + }, + render (gql) { + _gql = gql + + return ( +
+
+ +
-
- ) + ) + }, }) } @@ -58,7 +97,7 @@ describe('SidebarNavigation', () => { }) it('shows tooltips on hover', () => { - mountComponent(false) + mountComponent() cy.findByTestId('sidebar-header').trigger('mouseenter') cy.contains('.v-popper--some-open--tooltip', 'test-project').should('be.visible') cy.findByTestId('sidebar-header').trigger('mouseout') @@ -86,4 +125,97 @@ describe('SidebarNavigation', () => { mountComponent() cy.findByTestId('sidebar-link-specs-page').should('have.class', 'router-link-exact-active') }) + + context('debug status badge', () => { + it('renders new badge without cloudProject', { viewportWidth: 1280 }, () => { + cy.clock(IATR_RELEASE) + + mountComponent() + cy.tick(1000) //wait for debounce + + cy.findByLabelText('New Debug feature').should('be.visible').contains('New') + cy.percySnapshot('Debug Badge:collapsed') + + cy.findByLabelText(defaultMessages.sidebar.toggleLabel.collapsed, { + selector: 'button', + }).click() + + cy.percySnapshot('Debug Badge:expanded badge') + }) + + it('renders new badge when run status is "NOTESTS" or "RUNNING"', () => { + cy.clock(IATR_RELEASE + interval('1 month')) + + for (const status of ['NOTESTS', 'RUNNING'] as CloudRunStatus[]) { + mountComponent({ cloudProject: { status, numFailedTests: 0 } }) + cy.tick(1000) //wait for debounce + cy.findByLabelText('New Debug feature').should('be.visible').contains('New') + } + }) + + it('renders no badge if no cloudProject and released > 2 months ago', () => { + // Set to February 15, 2023 to see this fail + cy.clock(IATR_RELEASE + interval('3 months')) + mountComponent() + cy.tick(1000) //wait for debounce + cy.findByLabelText('New Debug feature').should('not.exist') + }) + + it('renders success badge when status is "PASSED"', () => { + mountComponent({ cloudProject: { status: 'PASSED', numFailedTests: 0 } }) + cy.findByLabelText('Relevant run passed').should('be.visible').contains('0') + cy.percySnapshot('Debug Badge:passed') + }) + + it('renders failure badge', () => { + mountComponent({ cloudProject: { status: 'FAILED', numFailedTests: 1 } }) + cy.findByLabelText('Relevant run had 1 test failure').should('be.visible').contains('1') + cy.percySnapshot('Debug Badge:failed') + + mountComponent({ cloudProject: { status: 'FAILED', numFailedTests: 10 } }) + cy.findByLabelText('Relevant run had 10 test failures').should('be.visible').contains('9+') + cy.percySnapshot('Debug Badge:failed:truncated') + }) + + it('renders failure badge when failing tests and abnormal status', () => { + for (const status of ['CANCELLED', 'ERRORED', 'OVERLIMIT', 'TIMEDOUT'] as CloudRunStatus[]) { + cy.log(status) + mountComponent({ cloudProject: { status, numFailedTests: 4 } }) + cy.findByLabelText('Relevant run had an error with 4 test failures').should('be.visible').contains('4') + } + }) + + it('renders error badge when no tests and abnormal status', () => { + for (const status of ['CANCELLED', 'ERRORED', 'OVERLIMIT', 'TIMEDOUT'] as CloudRunStatus[]) { + cy.log(status) + mountComponent({ cloudProject: { status, numFailedTests: 0 } }) + cy.findByLabelText('Relevant run had an error').should('be.visible').contains('0') + } + + cy.percySnapshot('Debug Badge:errored') + }) + + it('renders no badge when query is loading', () => { + const loginConnectStore = useLoginConnectStore() + + loginConnectStore.setProjectFlag('isProjectConnected', true) + + cy.clock(IATR_RELEASE) + + mountComponent({ isLoading: true }) + + cy.tick(1000) //wait for debounce + cy.findByLabelText('New Debug feature').should('not.exist') + }) + + it('renders new badge if offline', () => { + cy.clock(IATR_RELEASE) + + mountComponent({ online: false }) + + cy.tick(1000) //wait for debounce + + cy.findByLabelText('New Debug feature').should('be.visible').contains('New') + }) + }) }) diff --git a/packages/app/src/navigation/SidebarNavigation.vue b/packages/app/src/navigation/SidebarNavigation.vue index 396d2691bcec..e0941277fc60 100644 --- a/packages/app/src/navigation/SidebarNavigation.vue +++ b/packages/app/src/navigation/SidebarNavigation.vue @@ -26,10 +26,10 @@ />
-
+
@@ -89,34 +90,31 @@ diff --git a/packages/app/src/navigation/SidebarNavigationRow.cy.tsx b/packages/app/src/navigation/SidebarNavigationRow.cy.tsx index 58a10769d15e..c9bea2ec8f2b 100644 --- a/packages/app/src/navigation/SidebarNavigationRow.cy.tsx +++ b/packages/app/src/navigation/SidebarNavigationRow.cy.tsx @@ -1,5 +1,6 @@ import SidebarNavigationRow from './SidebarNavigationRow.vue' import { + IconStatusFailedOutline, IconTechnologyCodeEditor, IconTechnologyTestResults, IconObjectGear, @@ -14,6 +15,7 @@ describe('SidebarNavigationRow', () => { >

Tab Style

+ diff --git a/packages/app/src/navigation/SidebarNavigationRow.vue b/packages/app/src/navigation/SidebarNavigationRow.vue index 0eb51df91443..7a3eb1a9b0d3 100644 --- a/packages/app/src/navigation/SidebarNavigationRow.vue +++ b/packages/app/src/navigation/SidebarNavigationRow.vue @@ -8,8 +8,7 @@ :class="active ? 'before:(bg-indigo-300 scale-x-100 transition-colors) cursor-default' : 'before:(scale-x-0 transition-transform bg-gray-300)'" - class="rounded-md - flex + class="rounded-md flex h-40px my-16px w-full @@ -48,6 +47,14 @@ > {{ name }} + + {{ badge.value }} +
diff --git a/packages/app/src/pages/Debug.vue b/packages/app/src/pages/Debug.vue new file mode 100644 index 000000000000..9ef21feaac62 --- /dev/null +++ b/packages/app/src/pages/Debug.vue @@ -0,0 +1,70 @@ + + + diff --git a/packages/app/src/pages/Runs.vue b/packages/app/src/pages/Runs.vue index 36099138b41e..5457c2c728f8 100644 --- a/packages/app/src/pages/Runs.vue +++ b/packages/app/src/pages/Runs.vue @@ -1,6 +1,6 @@ diff --git a/packages/app/src/runner/SpecRunnerOpenMode.vue b/packages/app/src/runner/SpecRunnerOpenMode.vue index 51bf8709638d..6642ab0a13cf 100644 --- a/packages/app/src/runner/SpecRunnerOpenMode.vue +++ b/packages/app/src/runner/SpecRunnerOpenMode.vue @@ -35,7 +35,7 @@ @panel-width-updated="handlePanelWidthUpdated" >