diff --git a/.vscode/cspell.json b/.vscode/cspell.json index 0d635abffbad..5649488074b9 100644 --- a/.vscode/cspell.json +++ b/.vscode/cspell.json @@ -6,27 +6,41 @@ "words": [ "Chainable", "composables", + "dedup", "ERRORED", "execa", + "Fetchable", + "Fetchables", "forcedefault", + "getenv", + "graphcache", + "headlessui", "Iconify", + "intlify", "Lachlan", + "loggedin", "msapplication", "NOTESTS", "OVERLIMIT", + "overscan", "Pinia", "pnpm", "pseudoclass", "revparse", "Screenshotting", + "semibold", "shiki", + "speclist", "testid", "TIMEDOUT", + "titleize", + "topnav", "unconfigured", "unplugin", "unrunnable", "unstaged", "urql", + "viewports", "vite", "vitejs", "vueuse", @@ -34,4 +48,4 @@ ], "ignoreWords": [], "import": [] -} +} \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 18f46a05a56a..845b2fc55678 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -28,7 +28,7 @@ // Name: Volar // Description: Language server for Vue. Required for any syntax highlighting in Vue files. - "vue.volar", + "Vue.volar", // Name: Code Spell Checker // Description: Add spell-checking help to your code. diff --git a/graphql-codegen.yml b/graphql-codegen.yml index 111c3dc6c3c1..c540da7d5694 100644 --- a/graphql-codegen.yml +++ b/graphql-codegen.yml @@ -67,11 +67,14 @@ generates: - 'packages/frontend-shared/script/codegen-type-map.js' './packages/graphql/src/gen/cloud-source-types.gen.ts': + config: + useTypeImports: true schema: 'packages/graphql/schemas/cloud.graphql' plugins: - add: content: '/* eslint-disable */' - 'typescript' + - 'typescript-resolvers' ### # Generates types for us to infer the correct keys for graphcache diff --git a/package.json b/package.json index dabd63dfccff..fc41f1d03512 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "@graphql-codegen/typed-document-node": "2.2.8", "@graphql-codegen/typescript": "2.4.2", "@graphql-codegen/typescript-operations": "2.2.3", + "@graphql-codegen/typescript-resolvers": "^2.6.4", "@graphql-codegen/typescript-urql-graphcache": "2.2.3", "@graphql-tools/delegate": "8.2.1", "@graphql-tools/utils": "8.2.3", @@ -276,4 +277,4 @@ "sharp": "0.29.3", "vue-template-compiler": "2.6.12" } -} +} \ No newline at end of file diff --git a/packages/app/cypress.config.ts b/packages/app/cypress.config.ts index 772965e18456..7c42687ab789 100644 --- a/packages/app/cypress.config.ts +++ b/packages/app/cypress.config.ts @@ -3,9 +3,10 @@ import getenv from 'getenv' import { initGitRepoForTestProject, resetGitRepoForTestProject } from './cypress/tasks/git' const CYPRESS_INTERNAL_CLOUD_ENV = getenv('CYPRESS_INTERNAL_CLOUD_ENV', process.env.CYPRESS_INTERNAL_ENV || 'development') +const CYPRESS_INTERNAL_DEV_PROJECT_ID = getenv('CYPRESS_INTERNAL_DEV_PROJECT_ID', process.env.CYPRESS_INTERNAL_DEV_PROJECT_ID || 'sehy69') export default defineConfig({ - projectId: CYPRESS_INTERNAL_CLOUD_ENV === 'staging' ? 'ypt4pf' : 'sehy69', + projectId: CYPRESS_INTERNAL_CLOUD_ENV === 'staging' ? 'ypt4pf' : CYPRESS_INTERNAL_DEV_PROJECT_ID, retries: { runMode: 2, openMode: 0, diff --git a/packages/app/cypress/e2e/cypress-in-cypress-component.cy.ts b/packages/app/cypress/e2e/cypress-in-cypress-component.cy.ts index d1b480379a04..90d5af4b00d9 100644 --- a/packages/app/cypress/e2e/cypress-in-cypress-component.cy.ts +++ b/packages/app/cypress/e2e/cypress-in-cypress-component.cy.ts @@ -80,13 +80,13 @@ describe('Cypress In Cypress CT', { viewportWidth: 1500, defaultCommandTimeout: it('redirects to the specs list with error if a spec is not found', () => { cy.visitApp() - const { noSpecErrorTitle, noSpecErrorIntro, noSpecErrorExplainer } = defaultMessages.specPage + const { title, intro, explainer } = defaultMessages.specPage.noSpecError const badFilePath = 'src/DoesNotExist.spec.js' cy.visitApp(`/specs/runner?file=${badFilePath}`) - cy.contains(noSpecErrorTitle).should('be.visible') - cy.contains(noSpecErrorIntro).should('be.visible') - cy.contains(noSpecErrorExplainer).should('be.visible') + cy.contains(title).should('be.visible') + cy.contains(intro).should('be.visible') + cy.contains(explainer).should('be.visible') cy.contains(getPathForPlatform(badFilePath)).should('be.visible') cy.location() .its('href') @@ -94,11 +94,11 @@ describe('Cypress In Cypress CT', { viewportWidth: 1500, defaultCommandTimeout: // should clear after reload cy.reload() - cy.contains(noSpecErrorTitle).should('not.exist') + cy.contains(title).should('not.exist') }) it('redirects to the specs list with error if an open spec is not found when specs list updates', () => { - const { noSpecErrorTitle, noSpecErrorIntro, noSpecErrorExplainer } = defaultMessages.specPage + const { title, intro, explainer } = defaultMessages.specPage.noSpecError const goodFilePath = 'src/TestComponent.spec.jsx' @@ -109,9 +109,9 @@ describe('Cypress In Cypress CT', { viewportWidth: 1500, defaultCommandTimeout: cy.withCtx((ctx, o) => { ctx.actions.project.setSpecs(ctx.project.specs.filter((spec) => !spec.absolute.includes(o.path))) }, { path: goodFilePath }).then(() => { - cy.contains(noSpecErrorTitle).should('be.visible') - cy.contains(noSpecErrorIntro).should('be.visible') - cy.contains(noSpecErrorExplainer).should('be.visible') + cy.contains(title).should('be.visible') + cy.contains(intro).should('be.visible') + cy.contains(explainer).should('be.visible') cy.contains(getPathForPlatform(goodFilePath)).should('be.visible') cy.location() .its('href') diff --git a/packages/app/cypress/e2e/cypress-in-cypress-e2e.cy.ts b/packages/app/cypress/e2e/cypress-in-cypress-e2e.cy.ts index 7da4e7badfcd..c657da01ae69 100644 --- a/packages/app/cypress/e2e/cypress-in-cypress-e2e.cy.ts +++ b/packages/app/cypress/e2e/cypress-in-cypress-e2e.cy.ts @@ -98,13 +98,13 @@ describe('Cypress In Cypress E2E', { viewportWidth: 1500, defaultCommandTimeout: }) it('redirects to the specs list with error if a spec is not found when navigating', () => { - const { noSpecErrorTitle, noSpecErrorIntro, noSpecErrorExplainer } = defaultMessages.specPage + const { title, intro, explainer } = defaultMessages.specPage.noSpecError const badFilePath = 'cypress/e2e/does-not-exist.spec.js' cy.visitApp(`/specs/runner?file=${badFilePath}`) - cy.contains(noSpecErrorTitle).should('be.visible') - cy.contains(noSpecErrorIntro).should('be.visible') - cy.contains(noSpecErrorExplainer).should('be.visible') + cy.contains(title).should('be.visible') + cy.contains(intro).should('be.visible') + cy.contains(explainer).should('be.visible') cy.contains(getPathForPlatform(badFilePath)).should('be.visible') cy.location() .its('href') @@ -114,11 +114,11 @@ describe('Cypress In Cypress E2E', { viewportWidth: 1500, defaultCommandTimeout: // should clear after reload cy.reload() - cy.contains(noSpecErrorTitle).should('not.exist') + cy.contains(title).should('not.exist') }) it('redirects to the specs list with error if an open spec is not found when specs list updates', () => { - const { noSpecErrorTitle, noSpecErrorIntro, noSpecErrorExplainer } = defaultMessages.specPage + const { title, intro, explainer } = defaultMessages.specPage.noSpecError const goodFilePath = 'cypress/e2e/dom-content.spec.js' @@ -129,9 +129,9 @@ describe('Cypress In Cypress E2E', { viewportWidth: 1500, defaultCommandTimeout: cy.withCtx((ctx, o) => { ctx.actions.project.setSpecs(ctx.project.specs.filter((spec) => !spec.absolute.includes(o.path))) }, { path: goodFilePath }).then(() => { - cy.contains(noSpecErrorTitle).should('be.visible') - cy.contains(noSpecErrorIntro).should('be.visible') - cy.contains(noSpecErrorExplainer).should('be.visible') + cy.contains(title).should('be.visible') + cy.contains(intro).should('be.visible') + cy.contains(explainer).should('be.visible') cy.contains(getPathForPlatform(goodFilePath)).should('be.visible') cy.location() .its('href') diff --git a/packages/app/cypress/e2e/reporter_header.cy.ts b/packages/app/cypress/e2e/reporter_header.cy.ts index d4e5abc05ad1..3f5186584463 100644 --- a/packages/app/cypress/e2e/reporter_header.cy.ts +++ b/packages/app/cypress/e2e/reporter_header.cy.ts @@ -13,7 +13,7 @@ describe('Reporter Header', () => { cy.get('body').type('f') cy.get('[data-selected-spec="true"]').should('contain', 'dom-content').should('have.length', '1') - cy.get('[data-selected-spec="false"]').should('have.length', '18') + cy.get('[data-selected-spec="false"]').should('have.length', '27') }) it('filters the list of specs when searching for specs', () => { @@ -26,7 +26,7 @@ describe('Reporter Header', () => { cy.get('input').clear() - cy.get('[data-cy="spec-file-item"]').should('have.length', '3') + cy.get('[data-cy="spec-file-item"]').should('have.length', 3) cy.get('input').type('asdf', { force: true }) diff --git a/packages/app/cypress/e2e/runs.cy.ts b/packages/app/cypress/e2e/runs.cy.ts index ff6b60ae315f..09ca8bb91bee 100644 --- a/packages/app/cypress/e2e/runs.cy.ts +++ b/packages/app/cypress/e2e/runs.cy.ts @@ -10,7 +10,7 @@ function scaffoldTestingTypeAndVisitRunsPage (testingType: 'e2e' | 'component') // make sure there are no runs found for the project ID cy.remoteGraphQLIntercept(async (obj) => { - if (obj.result.data?.cloudProjectBySlug) { + if (obj.result.data?.cloudProjectBySlug?.runs?.nodes) { obj.result.data.cloudProjectBySlug.runs.nodes = [] } @@ -136,7 +136,7 @@ describe('App: Runs', { viewportWidth: 1200 }, () => { cy.visitApp() cy.remoteGraphQLIntercept(async (obj) => { - if ((obj.operationName === 'CheckCloudOrganizations_cloudViewerChange_cloudViewer' || obj.operationName === 'Runs_cloudViewer')) { + if ((obj.operationName === 'CheckCloudOrganizations_cloudViewerChange_cloudViewer' || obj.operationName === 'Runs_cloudViewer' || obj.operationName === 'SpecsPageContainer_cloudViewer')) { if (obj.result.data?.cloudViewer?.organizations?.nodes) { obj.result.data.cloudViewer.organizations.nodes = [] } @@ -597,8 +597,6 @@ describe('App: Runs', { viewportWidth: 1200 }, () => { let cloudData: any cy.loginUser() - cy.visitApp() - cy.remoteGraphQLIntercept((obj) => { if (obj.operationName === 'Runs_currentProject_cloudProject_cloudProjectBySlug') { cloudData = obj.result @@ -610,7 +608,10 @@ describe('App: Runs', { viewportWidth: 1200 }, () => { return obj.result }) + cy.visitApp() + cy.findByTestId('sidebar-link-runs-page').click() + cy.contains('h2', 'Cannot connect to the Cypress Dashboard') cy.percySnapshot() diff --git a/packages/app/cypress/e2e/sidebar_navigation.cy.ts b/packages/app/cypress/e2e/sidebar_navigation.cy.ts index 27e07abe718e..5fbd5cdd1b36 100644 --- a/packages/app/cypress/e2e/sidebar_navigation.cy.ts +++ b/packages/app/cypress/e2e/sidebar_navigation.cy.ts @@ -221,7 +221,8 @@ describe('Sidebar Navigation', () => { it('has a menu item labeled "Runs" which takes you to the Runs page', () => { cy.get('[data-cy="app-header-bar"]').findByText('Runs').should('not.exist') - cy.findByText('Runs').should('be.visible').click() + + cy.findByTestId('sidebar-link-runs-page').should('have.text', 'Runs').should('be.visible').click() cy.get('[data-cy="app-header-bar"]').findByText('Runs').should('be.visible') cy.get('.router-link-active').findByText('Runs').should('be.visible') }) diff --git a/packages/app/cypress/e2e/specs_list_e2e.cy.ts b/packages/app/cypress/e2e/specs_list_e2e.cy.ts index 0ef87875ac69..2ec0cc25a7c7 100644 --- a/packages/app/cypress/e2e/specs_list_e2e.cy.ts +++ b/packages/app/cypress/e2e/specs_list_e2e.cy.ts @@ -112,19 +112,19 @@ describe('App: Spec List (E2E)', () => { describe('typing the filter', function () { it('displays only matching spec', function () { - cy.get('button').contains('14 Matches') + cy.get('button').contains('23 Matches') cy.findByLabelText('Search Specs').type('content') cy.get('[data-cy="spec-item"]') .should('have.length', 2) .and('contain', 'dom-content.spec.js') - cy.get('button').contains('2 of 14 Matches') + cy.get('button').contains('2 of 23 Matches') cy.findByLabelText('Search Specs').clear().type('asdf') cy.get('[data-cy="spec-item"]') .should('have.length', 0) - cy.get('button').contains('0 of 14 Matches') + cy.get('button').contains('0 of 23 Matches') }) it('only shows matching folders', () => { @@ -176,7 +176,7 @@ describe('App: Spec List (E2E)', () => { .should('have.value', '') cy.get('[data-cy="spec-item"]') - .should('have.length', 14) + .should('have.length', 23) }) it('clears the filter if the user presses ESC key', function () { @@ -187,8 +187,7 @@ describe('App: Spec List (E2E)', () => { cy.findByLabelText('Search Specs') .should('have.value', '') - cy.get('[data-cy="spec-item"]') - .should('have.length', 14) + cy.get('button').contains('23 Matches') }) it('shows empty message if no results', function () { @@ -203,8 +202,7 @@ describe('App: Spec List (E2E)', () => { cy.findByText('Clear Search').click() cy.focused().should('have.id', 'spec-filter') - cy.get('[data-cy="spec-item"]') - .should('have.length', 14) + cy.get('button').contains('23 Matches') }) //TODO: https://cypress-io.atlassian.net/browse/UNIFY-1588 diff --git a/packages/app/cypress/e2e/specs_list_latest_runs.cy.ts b/packages/app/cypress/e2e/specs_list_latest_runs.cy.ts new file mode 100644 index 000000000000..28a76b60039b --- /dev/null +++ b/packages/app/cypress/e2e/specs_list_latest_runs.cy.ts @@ -0,0 +1,633 @@ +import defaultMessages from '@packages/frontend-shared/src/locales/en-US.json' +import type { CloudRunStatus } from '@packages/frontend-shared/src/generated/graphql' + +function specRowSelector (specFileName: string) { + return `[data-cy-row="${specFileName}"]` +} + +function dotSelector (specFileName: string, dotNumber: 0 | 1 | 2 |'latest') { + return `${specRowSelector(specFileName)} [data-cy="run-status-dot-${dotNumber}"]` +} + +function dotsSkeletonSelector (specFileName: string) { + return `${specRowSelector(specFileName)} [data-cy="run-status-dots-loading"]` +} + +function averageDurationSelector (specFileName: string) { + return `${specRowSelector(specFileName)} [data-cy="average-duration"]` +} + +function specShouldShow (specFileName: string, runDotsClasses: string[], latestRunStatus: CloudRunStatus|'PLACEHOLDER') { + const latestStatusSpinning = latestRunStatus === 'RUNNING' + + type dotIndex = Parameters[1]; + const indexes: dotIndex[] = [0, 1, 2] + + indexes.forEach((i) => { + return cy.get(dotSelector(specFileName, i)).should('have.class', `icon-light-${runDotsClasses.length > i ? runDotsClasses[i] : 'gray-300'}`) + }) + + cy.get(dotSelector(specFileName, 'latest')) + .should(`${latestStatusSpinning ? '' : 'not.'}have.class`, 'animate-spin') + .and('have.attr', 'data-cy-run-status', latestRunStatus) + + // TODO: add link verification + // if (latestRunStatus !== 'PLACEHOLDER') { + // cy.get(`${specRowSelector(specFileName)} [data-cy="run-status-dots"]`).validateExternalLink('https://google.com') + // } +} + +function simulateRunData () { + cy.remoteGraphQLIntercept(async (obj) => { + const fakeRuns = (statuses: string[], idPrefix: string) => { + return statuses.map((s, idx) => { + return { + __typename: 'CloudSpecRun', + id: `SpecRun_${idPrefix}_${idx}`, + status: s, + createdAt: new Date('2022-05-08T03:17:00').toISOString(), + completedAt: new Date('2022-05-08T05:17:00').toISOString(), + runNumber: 432, + groupCount: 2, + specDuration: { + min: 143003, // 2:23 + max: 159120, // 3:40 + __typename: 'SpecDataAggregate', + }, + testsFailed: { + min: 1, + max: 2, + __typename: 'SpecDataAggregate', + }, + testsPassed: { + min: 22, + max: 23, + __typename: 'SpecDataAggregate', + }, + testsSkipped: { + min: null, + max: null, + __typename: 'SpecDataAggregate', + }, + testsPending: { + min: 1, + max: 2, + __typename: 'SpecDataAggregate', + }, + url: 'https://google.com', + } + }) + } + + if (obj.result.data && 'cloudSpecByPath' in obj.result.data) { + // simulate network latency to allow for caching to register + await new Promise((r) => setTimeout(r, 20)) + + const statuses = obj.variables.specPath?.includes('accounts_list.spec.js') ? + ['PASSED', 'FAILED', 'CANCELLED', 'ERRORED'] : + obj.variables.specPath?.includes('app.spec.js') ? + [] : + ['RUNNING', 'PASSED'] + + const runs = fakeRuns(statuses, obj.variables.specPath) + const averageDuration = obj.variables.specPath?.includes('accounts_list.spec.js') ? + 12000 : // 0:12 + 123000 // 2:03 + + obj.result.data.cloudSpecByPath = { + __typename: 'CloudProjectSpec', + retrievedAt: new Date().toISOString(), + id: `id${obj.variables.specPath}`, + specRuns: { + __typename: 'CloudSpecRunConnection', + nodes: runs, + }, + averageDuration, + } + } + + return obj.result + }) +} + +function allVisibleSpecsShouldBePlaceholders () { + cy.findAllByTestId('run-status-dot-0').should('have.class', 'icon-light-gray-300') + cy.findAllByTestId('run-status-dot-1').should('have.class', 'icon-light-gray-300') + cy.findAllByTestId('run-status-dot-2').should('have.class', 'icon-light-gray-300') + cy.findAllByTestId('run-status-dot-latest') + .should('not.have.class', 'animate-spin') + .and('have.attr', 'data-cy-run-status', 'PLACEHOLDER') + + cy.get('.spec-list-container').scrollTo('bottom') + cy.get('.spec-list-container').scrollTo('bottom') +} + +describe('App/Cloud Integration - Latest runs and Average duration', { viewportWidth: 1200, viewportHeight: 900 }, () => { + beforeEach(() => { + cy.scaffoldProject('cypress-in-cypress') + cy.openProject('cypress-in-cypress') + cy.startAppServer() + + cy.withCtx((ctx, o) => { + o.sinon.stub(ctx.lifecycleManager.git!, 'currentBranch').value('fakeBranch') + }) + }) + + context('when no runs are recorded', () => { + beforeEach(() => { + cy.loginUser() + + cy.remoteGraphQLIntercept(async (obj) => { + if (obj.result.data && 'cloudSpecByPath' in obj.result.data) { + obj.result.data.cloudSpecByPath = { + __typename: 'CloudProjectSpecNotFound', + retrievedAt: new Date().toISOString(), + id: `id${obj.variables.specPath}`, + specRuns: { + __typename: 'CloudSpecRunConnection', + nodes: [], + }, + averageDuration: null, + } + } + + return obj.result + }) + + cy.visitApp() + cy.findByTestId('sidebar-link-specs-page').click() + }) + + it('shows placeholders for all visible specs', () => { + allVisibleSpecsShouldBePlaceholders() + }) + }) + + context('when logged out', () => { + beforeEach(() => { + cy.visitApp() + cy.findByTestId('sidebar-link-specs-page').click() + }) + + it('shows placeholders for all visible specs', () => { + allVisibleSpecsShouldBePlaceholders() + }) + + it('shows correct tooltips with log in buttons', () => { + cy.findByTestId('latest-runs-header').trigger('mouseenter') + cy.get('.v-popper__popper--shown') + .should('contain', 'Connect to the Cypress Dashboard to see the status of your latest runs') + .find('button') + .should('have.text', 'Log in to the Dashboard') + .click() + + cy.findByRole('dialog', { name: 'Log in to Cypress' }).within(() => { + cy.get('button').contains('Log In') + cy.get('[aria-label="Close"]').click() + }) + + cy.findByTestId('latest-runs-header').trigger('mouseleave') + + cy.findByTestId('average-duration-header').trigger('mouseenter') + cy.get('.v-popper__popper--shown') + .should('contain', 'Connect to the Cypress Dashboard to see the average spec durations of your latest runs') + .find('button') + .should('have.text', 'Log in to the Dashboard') + .click() + + cy.findByRole('dialog', { name: 'Log in to Cypress' }).within(() => { + cy.get('button').contains('Log In') + cy.get('[aria-label="Close"]').click() + }) + + cy.findByTestId('average-duration-header').trigger('mouseleave') + }) + }) + + context('when project disconnected', () => { + beforeEach(() => { + cy.loginUser() + cy.withCtx((ctx, o) => { + o.sinon.stub(ctx.project, 'projectId').resolves(null) + }) + + cy.visitApp() + cy.findByTestId('sidebar-link-specs-page').click() + }) + + it('shows placeholders for all visible specs', () => { + allVisibleSpecsShouldBePlaceholders() + }) + + it('shows correct tooltips with log in buttons', () => { + cy.findByTestId('latest-runs-header').trigger('mouseenter') + cy.get('.v-popper__popper--shown') + .should('contain', 'Connect to the Cypress Dashboard to see the status of your latest runs') + .find('button') + .should('have.text', 'Connect your project') + .click() + + cy.findByRole('dialog', { name: 'Create project' }).within(() => { + cy.get('[aria-label="Close"]').click({ force: true }) + }) + + cy.findByTestId('latest-runs-header').trigger('mouseleave') + + cy.findByTestId('average-duration-header').trigger('mouseenter') + cy.get('.v-popper__popper--shown') + .should('contain', 'Connect to the Cypress Dashboard to see the average spec durations of your latest runs') + .find('button') + .should('have.text', 'Connect your project') + .click() + + cy.findByRole('dialog', { name: 'Create project' }).within(() => { + cy.get('[aria-label="Close"]').click({ force: true }) + }) + + cy.findByTestId('average-duration-header').trigger('mouseleave') + }) + }) + + context('when not using a branch', () => { + beforeEach(() => { + cy.withCtx((ctx, o) => { + o.sinon.stub(ctx.lifecycleManager.git!, 'currentBranch').value(undefined) + }) + + cy.loginUser() + + cy.visitApp() + cy.findByTestId('sidebar-link-specs-page').click() + }) + + it('shows placeholders for all visible specs', () => { + allVisibleSpecsShouldBePlaceholders() + }) + }) + + context('when runs are recorded', () => { + beforeEach(() => { + cy.loginUser() + + simulateRunData() + + cy.visitApp() + cy.findByTestId('sidebar-link-specs-page').click() + }) + + it('shows correct tooltips', () => { + cy.findByTestId('latest-runs-header').trigger('mouseenter') + cy.get('.v-popper__popper--shown') + .should('contain', 'The status of your latest runs in the Cypress Dashboard') + .find('button') + .should('not.exist') + + cy.findByTestId('latest-runs-header').trigger('mouseleave') + + cy.findByTestId('average-duration-header').trigger('mouseenter') + cy.get('.v-popper__popper--shown') + .should('contain', 'The average spec durations of your latest runs in the Cypress Dashboard') + .find('button') + .should('not.exist') + + cy.findByTestId('average-duration-header').trigger('mouseleave') + }) + + it('shows accurate latest runs and average duration data', () => { + specShouldShow('accounts_list.spec.js', ['orange-400', 'gray-300', 'red-400'], 'PASSED') + cy.get(averageDurationSelector('accounts_list.spec.js')).contains('0:12') + + // all should use placeholder + specShouldShow('app.spec.js', [], 'PLACEHOLDER') + cy.get(averageDurationSelector('app.spec.js')).contains('2:03') + // shouldn't have a tooltip + cy.get(dotSelector('app.spec.js', 'latest')).trigger('mouseenter') + cy.get('.v-popper__popper--shown').should('not.exist') + cy.get(dotSelector('app.spec.js', 'latest')).trigger('mouseleave') + + // oldest 2 status dots will use placeholder + specShouldShow('accounts_new.spec.js', ['gray-300', 'gray-300', 'jade-400'], 'RUNNING') + cy.get(dotSelector('accounts_new.spec.js', 'latest')).trigger('mouseenter') + cy.get('.v-popper__popper--shown').should('exist') + // TODO: verify the contents of the tooltip + cy.get(dotSelector('accounts_new.spec.js', 'latest')).trigger('mouseleave') + cy.get(averageDurationSelector('accounts_new.spec.js')).contains('2:03') + }) + + it('lazily loads data for off-screen specs', () => { + // make sure the virtualized list didn't load z008.spec.js + cy.get(specRowSelector('z008.spec.js')).should('not.exist') + + cy.get('.spec-list-container').scrollTo('bottom') + // scrolling down should load z008.spec.js with loading status + cy.get(dotsSkeletonSelector('z008.spec.js')).should('exist') + + // then z008.spec.js should show proper data + specShouldShow('z008.spec.js', ['gray-300', 'gray-300', 'jade-400'], 'RUNNING') + cy.get(averageDurationSelector('z008.spec.js')).contains('2:03') + }) + + describe('preserving tree expansion state', () => { + it('should preserve state when row data is updated without additions/deletions', () => { + // Collapse a directory + cy.get('button[data-cy="row-directory-depth-1"]').first() + .should('have.attr', 'aria-expanded', 'true') + .click() + .should('have.attr', 'aria-expanded', 'false') + + // Trigger cloud specs list change by scrolling + cy.get('.spec-list-container') + .scrollTo('bottom', { duration: 500 }) + .wait(100) + .scrollTo('top', { duration: 500 }) + + // Directory should still be collapsed + cy.get('button[data-cy="row-directory-depth-1"]').first() + .should('have.attr', 'aria-expanded', 'false') + }) + + it('should expand all directories when search is performed', () => { + // Collapse a directory + cy.get('button[data-cy="row-directory-depth-0"]').first() + .should('have.attr', 'aria-expanded', 'true') + .click() + .should('have.attr', 'aria-expanded', 'false') + .then((dir) => { + // Perform a search/filter operation + cy.findByLabelText('Search Specs').type(dir.text()[0]) + }) + + // Previously-collapsed directory should automatically expand + cy.get('button[data-cy="row-directory-depth-0"]').first() + .should('have.attr', 'aria-expanded', 'true') + }) + }) + + it('should retain data after app navigation', () => { + // App/Cloud Integration data should load and render to start + specShouldShow('accounts_list.spec.js', ['orange-400', 'gray-300', 'red-400'], 'PASSED') + cy.get(averageDurationSelector('accounts_list.spec.js')).contains('0:12') + + // Move to Settings page and wait for render + cy.get('a[href="#/settings"]').click() + cy.location('hash').should('include', '/settings') + cy.findByText('Project Settings').should('be.visible') + + // Move back to Specs page and wait for render + cy.get('a[href="#/specs"]').click() + cy.location('hash').should('include', '/specs') + cy.findByText('E2E specs').should('be.visible') + + // App/Cloud Integration data should still be loaded and rendered + specShouldShow('accounts_list.spec.js', ['orange-400', 'gray-300', 'red-400'], 'PASSED') + cy.get(averageDurationSelector('accounts_list.spec.js')).contains('0:12') + }) + }) + + context('polling indicates new data', () => { + beforeEach(() => { + cy.loginUser() + + cy.remoteGraphQLIntercept(async (obj, testState) => { + const fakeRuns = (statuses: string[], idPrefix: string) => { + return statuses.map((s, idx) => { + return { + __typename: 'CloudSpecRun', + id: `SpecRun_${idPrefix}_${idx}`, + status: s, + createdAt: new Date('2022-05-08T03:17:00').toISOString(), + completedAt: new Date('2022-05-08T05:17:00').toISOString(), + runNumber: 432, + groupCount: 2, + specDuration: { + min: 143003, // 2:23 + max: 159120, // 3:40 + __typename: 'SpecDataAggregate', + }, + testsFailed: { + min: 1, + max: 2, + __typename: 'SpecDataAggregate', + }, + testsPassed: { + min: 22, + max: 23, + __typename: 'SpecDataAggregate', + }, + testsSkipped: { + min: null, + max: null, + __typename: 'SpecDataAggregate', + }, + testsPending: { + min: 1, + max: 2, + __typename: 'SpecDataAggregate', + }, + url: 'https://google.com', + } + }) + } + + const pollingCounter = testState.pollingCounter ?? 0 + + if (obj.result.data && 'cloudSpecByPath' in obj.result.data) { + // simulate network latency to allow for caching to register + await new Promise((r) => setTimeout(r, 20)) + + const statuses = pollingCounter < 2 ? ['PASSED', 'FAILED', 'CANCELLED', 'ERRORED'] : ['FAILED', 'PASSED', 'FAILED', 'CANCELLED', 'ERRORED'] + const runs = fakeRuns(statuses, obj.variables.specPath) + const averageDuration = pollingCounter < 2 ? 12000 : 13000 + + obj.result.data.cloudSpecByPath = { + __typename: 'CloudProjectSpec', + retrievedAt: new Date().toISOString(), + id: `id${obj.variables.specPath}`, + specRuns: { + __typename: 'CloudSpecRunConnection', + nodes: runs, + }, + averageDuration, + } + } else if (obj.result.data && 'cloudLatestRunUpdateSpecData' in obj.result.data) { + const mostRecentUpdate = pollingCounter > 1 ? new Date().toISOString() : new Date('2022-06-10').toISOString() + // initial polling interval is set to every second to avoid long wait times + const pollingInterval = pollingCounter > 1 ? 30 : 1 + + obj.result.data.cloudLatestRunUpdateSpecData = { + __typename: 'CloudLatestRunUpdateSpecData', + mostRecentUpdate, + pollingInterval, + } + + testState.pollingCounter = pollingCounter + 1 + } + + return obj.result + }) + + cy.visitApp() + cy.findByTestId('sidebar-link-specs-page').click() + }) + + it('refreshes view to reflect new data', () => { + specShouldShow('accounts_list.spec.js', ['orange-400', 'gray-300', 'red-400'], 'PASSED') + cy.get(dotSelector('accounts_new.spec.js', 'latest')).trigger('mouseenter') + cy.get('.v-popper__popper--shown').should('exist') + // TODO: verify the contents of the tooltip + cy.get(dotSelector('accounts_new.spec.js', 'latest')).trigger('mouseleave') + cy.get(averageDurationSelector('accounts_list.spec.js')).contains('0:12') + + cy.wait(1200) + + // new results should be shown + specShouldShow('accounts_list.spec.js', ['gray-300', 'red-400', 'jade-400'], 'FAILED') + cy.get(dotSelector('accounts_new.spec.js', 'latest')).trigger('mouseenter') + cy.get('.v-popper__popper--shown').should('exist') + // TODO: verify the contents of the tooltip + cy.get(dotSelector('accounts_new.spec.js', 'latest')).trigger('mouseleave') + cy.get(averageDurationSelector('accounts_list.spec.js')).contains('0:13') + }) + }) + + context('polling indicates no new data', () => { + beforeEach(() => { + cy.loginUser() + + cy.remoteGraphQLIntercept(async (obj, testState) => { + const fakeRuns = (statuses: string[], idPrefix: string) => { + return statuses.map((s, idx) => { + return { + __typename: 'CloudSpecRun', + id: `SpecRun_${idPrefix}_${idx}`, + status: s, + createdAt: new Date('2022-05-08T03:17:00').toISOString(), + completedAt: new Date('2022-05-08T05:17:00').toISOString(), + runNumber: 432, + groupCount: 2, + specDuration: { + min: 143003, // 2:23 + max: 159120, // 3:40 + __typename: 'SpecDataAggregate', + }, + testsFailed: { + min: 1, + max: 2, + __typename: 'SpecDataAggregate', + }, + testsPassed: { + min: 22, + max: 23, + __typename: 'SpecDataAggregate', + }, + testsSkipped: { + min: null, + max: null, + __typename: 'SpecDataAggregate', + }, + testsPending: { + min: 1, + max: 2, + __typename: 'SpecDataAggregate', + }, + url: 'https://google.com', + } + }) + } + + const pollingCounter = testState.pollingCounter ?? 0 + + if (obj.result.data && 'cloudSpecByPath' in obj.result.data) { + // simulate network latency to allow for caching to register + await new Promise((r) => setTimeout(r, 20)) + + const statuses = pollingCounter < 2 ? ['PASSED', 'FAILED', 'CANCELLED', 'ERRORED'] : ['FAILED', 'PASSED', 'FAILED', 'CANCELLED', 'ERRORED'] + const runs = fakeRuns(statuses, obj.variables.specPath) + const averageDuration = pollingCounter < 2 ? 12000 : 13000 + + obj.result.data.cloudSpecByPath = { + __typename: 'CloudProjectSpec', + retrievedAt: new Date().toISOString(), + id: `id${obj.variables.specPath}`, + specRuns: { + __typename: 'CloudSpecRunConnection', + nodes: runs, + }, + averageDuration, + } + } else if (obj.result.data && 'cloudLatestRunUpdateSpecData' in obj.result.data) { + const mostRecentUpdate = new Date('2022-06-10').toISOString() + // initial polling interval is set to every second to avoid long wait times + const pollingInterval = pollingCounter > 1 ? 30 : 1 + + obj.result.data.cloudLatestRunUpdateSpecData = { + __typename: 'CloudLatestRunUpdateSpecData', + mostRecentUpdate, + pollingInterval, + } + + testState.pollingCounter = pollingCounter + 1 + } + + return obj.result + }) + + cy.visitApp() + cy.findByTestId('sidebar-link-specs-page').click() + }) + + it('shows the same data after polling', () => { + specShouldShow('accounts_list.spec.js', ['orange-400', 'gray-300', 'red-400'], 'PASSED') + cy.get(dotSelector('accounts_new.spec.js', 'latest')).trigger('mouseenter') + cy.get('.v-popper__popper--shown').should('exist') + // TODO: verify the contents of the tooltip + cy.get(dotSelector('accounts_new.spec.js', 'latest')).trigger('mouseleave') + cy.get(averageDurationSelector('accounts_list.spec.js')).contains('0:12') + + cy.wait(1200) + + // new results should be shown + specShouldShow('accounts_list.spec.js', ['orange-400', 'gray-300', 'red-400'], 'PASSED') + cy.get(dotSelector('accounts_new.spec.js', 'latest')).trigger('mouseenter') + cy.get('.v-popper__popper--shown').should('exist') + // TODO: verify the contents of the tooltip + cy.get(dotSelector('accounts_new.spec.js', 'latest')).trigger('mouseleave') + cy.get(averageDurationSelector('accounts_list.spec.js')).contains('0:12') + }) + }) +}) + +describe('App/Cloud Integration - Latest runs and Average duration', { viewportWidth: 1200 }, () => { + context('when offline', () => { + beforeEach(() => { + cy.scaffoldProject('cypress-in-cypress') + cy.goOffline() + cy.wait(300) + cy.openProject('cypress-in-cypress') + cy.startAppServer() + + cy.loginUser() + + simulateRunData() + cy.visitApp() + + cy.findByTestId('sidebar-link-specs-page').click() + + // after navigating to a new page, the browser needs to go offline again + cy.goOffline() + }) + + it('shows placeholders for all visible specs', () => { + allVisibleSpecsShouldBePlaceholders() + }) + + it('shows offline alert then hides it after coming online', () => { + cy.findByTestId('offline-alert') + .should('contain.text', defaultMessages.specPage.offlineWarning.title) + .and('contain.text', defaultMessages.specPage.offlineWarning.explainer) + + cy.goOnline() + cy.findByTestId('offline-alert').should('not.exist') + }) + }) +}) diff --git a/packages/app/cypress/e2e/subscriptions/authChange-subscription.cy.ts b/packages/app/cypress/e2e/subscriptions/authChange-subscription.cy.ts index 6ea15de793d6..acbfa8274f87 100644 --- a/packages/app/cypress/e2e/subscriptions/authChange-subscription.cy.ts +++ b/packages/app/cypress/e2e/subscriptions/authChange-subscription.cy.ts @@ -33,7 +33,7 @@ describe('authChange subscription', () => { cy.contains('Log In') cy.wait(500) cy.withCtx(async (ctx) => { - await ctx.actions.auth.login() + await ctx.actions.auth.login('testing', 'testing') }) cy.contains('Test User') @@ -62,7 +62,7 @@ describe('authChange subscription', () => { cy.contains('Log In') cy.wait(500) cy.withCtx(async (ctx) => { - await ctx.actions.auth.login() + await ctx.actions.auth.login('testing', 'testing') }) cy.contains('Test User') @@ -90,7 +90,7 @@ describe('authChange subscription', () => { cy.contains('Log In') cy.wait(500) cy.withCtx(async (ctx) => { - await ctx.actions.auth.login() + await ctx.actions.auth.login('testing', 'testing') }) cy.contains('Test User') diff --git a/packages/app/cypress/e2e/subscriptions/createCloudOrgModal-subscription.cy.ts b/packages/app/cypress/e2e/subscriptions/createCloudOrgModal-subscription.cy.ts index f6a6cee32c24..d667d7e15685 100644 --- a/packages/app/cypress/e2e/subscriptions/createCloudOrgModal-subscription.cy.ts +++ b/packages/app/cypress/e2e/subscriptions/createCloudOrgModal-subscription.cy.ts @@ -15,11 +15,10 @@ describe('App: Runs', { viewportWidth: 1200 }, () => { cy.startAppServer('component') cy.loginUser() - cy.visitApp() // Simulate no orgs cy.remoteGraphQLIntercept(async (obj) => { - if ((obj.operationName === 'CheckCloudOrganizations_cloudViewerChange_cloudViewer' || obj.operationName === 'Runs_cloudViewer')) { + if ((obj.operationName === 'CheckCloudOrganizations_cloudViewerChange_cloudViewer' || obj.operationName === 'Runs_cloudViewer' || obj.operationName === 'SpecsPageContainer_cloudViewer')) { if (obj.result.data?.cloudViewer?.organizations?.nodes) { obj.result.data.cloudViewer.organizations.nodes = [] } @@ -28,6 +27,8 @@ describe('App: Runs', { viewportWidth: 1200 }, () => { return obj.result }) + cy.visitApp() + cy.findByTestId('sidebar-link-runs-page').click() cy.findByText(defaultMessages.runs.connect.buttonProject).click() diff --git a/packages/app/cypress/e2e/subscriptions/specChange-subscription.cy.ts b/packages/app/cypress/e2e/subscriptions/specChange-subscription.cy.ts index 95e5a37f3b61..9cd761419f6d 100644 --- a/packages/app/cypress/e2e/subscriptions/specChange-subscription.cy.ts +++ b/packages/app/cypress/e2e/subscriptions/specChange-subscription.cy.ts @@ -12,7 +12,8 @@ describe('specChange subscription', () => { describe('specs list', () => { it('responds to specChange event for an added file', () => { cy.get('[data-cy="spec-item-link"]') - .should('have.length', 14) + // cannot assert a length since this is a virtualized list + // .should('have.length', 14) .should('contain', 'blank-contents.spec.js') .should('contain', 'dom-container.spec.js') .should('contain', 'dom-content.spec.js') @@ -23,7 +24,8 @@ describe('specChange subscription', () => { }, { path: getPathForPlatform('cypress/e2e/new-file.spec.js') }) cy.get('[data-cy="spec-item-link"]') - .should('have.length', 15) + // cannot assert a length since this is a virtualized list + // .should('have.length', 15) .should('contain', 'blank-contents.spec.js') .should('contain', 'dom-container.spec.js') .should('contain', 'dom-content.spec.js') @@ -33,7 +35,8 @@ describe('specChange subscription', () => { it('responds to specChange event for a removed file', () => { cy.get('[data-cy="spec-item-link"]') - .should('have.length', 14) + // cannot assert a length since this is a virtualized list + // .should('have.length', 14) .should('contain', 'blank-contents.spec.js') .should('contain', 'dom-container.spec.js') .should('contain', 'dom-content.spec.js') @@ -44,10 +47,12 @@ describe('specChange subscription', () => { }, { path: getPathForPlatform('cypress/e2e/dom-list.spec.js') }) cy.get('[data-cy="spec-item-link"]') - .should('have.length', 13) + // cannot assert a length since this is a virtualized list + // .should('have.length', 13) .should('contain', 'blank-contents.spec.js') .should('contain', 'dom-container.spec.js') .should('contain', 'dom-content.spec.js') + .should('not.contain', 'dom-list.spec.js') }) it('handles adding the first file', () => { @@ -69,6 +74,15 @@ describe('specChange subscription', () => { getPathForPlatform('cypress/e2e/accounts/accounts_new.spec.js'), getPathForPlatform('cypress/e2e/admin_users/admin_users_list.spec.js'), getPathForPlatform('cypress/e2e/admin_users/admin.user/foo_list.spec.js'), + getPathForPlatform('cypress/e2e/z001.spec.js'), + getPathForPlatform('cypress/e2e/z002.spec.js'), + getPathForPlatform('cypress/e2e/z003.spec.js'), + getPathForPlatform('cypress/e2e/z004.spec.js'), + getPathForPlatform('cypress/e2e/z005.spec.js'), + getPathForPlatform('cypress/e2e/z006.spec.js'), + getPathForPlatform('cypress/e2e/z007.spec.js'), + getPathForPlatform('cypress/e2e/z008.spec.js'), + getPathForPlatform('cypress/e2e/z009.spec.js'), ], }) @@ -102,6 +116,15 @@ describe('specChange subscription', () => { getPathForPlatform('cypress/e2e/accounts/accounts_new.spec.js'), getPathForPlatform('cypress/e2e/admin_users/admin_users_list.spec.js'), getPathForPlatform('cypress/e2e/admin_users/admin.user/foo_list.spec.js'), + getPathForPlatform('cypress/e2e/z001.spec.js'), + getPathForPlatform('cypress/e2e/z002.spec.js'), + getPathForPlatform('cypress/e2e/z003.spec.js'), + getPathForPlatform('cypress/e2e/z004.spec.js'), + getPathForPlatform('cypress/e2e/z005.spec.js'), + getPathForPlatform('cypress/e2e/z006.spec.js'), + getPathForPlatform('cypress/e2e/z007.spec.js'), + getPathForPlatform('cypress/e2e/z008.spec.js'), + getPathForPlatform('cypress/e2e/z009.spec.js'), ], }) @@ -119,7 +142,8 @@ describe('specChange subscription', () => { it('responds to a cypress.config.js file change', () => { cy.get('[data-cy="spec-item-link"]') - .should('have.length', 14) + // cannot assert a length since this is a virtualized list + // .should('have.length', 14) .should('contain', 'blank-contents.spec.js') .should('contain', 'dom-container.spec.js') .should('contain', 'dom-content.spec.js') @@ -161,7 +185,7 @@ e2e: { cy.get('body').type('f') cy.get('[data-cy="spec-file-item"]') - .should('have.length', 14) + .should('have.length', 23) .should('contain', 'blank-contents.spec.js') .should('contain', 'dom-container.spec.js') .should('contain', 'dom-content.spec.js') @@ -172,7 +196,7 @@ e2e: { }, { path: getPathForPlatform('cypress/e2e/new-file.spec.js') }) cy.get('[data-cy="spec-file-item"]') - .should('have.length', 15) + .should('have.length', 24) .should('contain', 'blank-contents.spec.js') .should('contain', 'dom-container.spec.js') .should('contain', 'dom-content.spec.js') @@ -187,7 +211,7 @@ e2e: { cy.get('body').type('f') cy.get('[data-cy="spec-file-item"]') - .should('have.length', 14) + .should('have.length', 23) .should('contain', 'blank-contents.spec.js') .should('contain', 'dom-container.spec.js') .should('contain', 'dom-content.spec.js') @@ -198,7 +222,7 @@ e2e: { }, { path: getPathForPlatform('cypress/e2e/dom-list.spec.js') }) cy.get('[data-cy="spec-file-item"]') - .should('have.length', 13) + .should('have.length', 22) .should('contain', 'blank-contents.spec.js') .should('contain', 'dom-container.spec.js') .should('contain', 'dom-content.spec.js') @@ -226,6 +250,15 @@ e2e: { getPathForPlatform('cypress/e2e/accounts/accounts_new.spec.js'), getPathForPlatform('cypress/e2e/admin_users/admin_users_list.spec.js'), getPathForPlatform('cypress/e2e/admin_users/admin.user/foo_list.spec.js'), + getPathForPlatform('cypress/e2e/z001.spec.js'), + getPathForPlatform('cypress/e2e/z002.spec.js'), + getPathForPlatform('cypress/e2e/z003.spec.js'), + getPathForPlatform('cypress/e2e/z004.spec.js'), + getPathForPlatform('cypress/e2e/z005.spec.js'), + getPathForPlatform('cypress/e2e/z006.spec.js'), + getPathForPlatform('cypress/e2e/z007.spec.js'), + getPathForPlatform('cypress/e2e/z008.spec.js'), + getPathForPlatform('cypress/e2e/z009.spec.js'), ], }) @@ -248,7 +281,7 @@ e2e: { cy.get('body').type('f') cy.get('[data-cy="spec-file-item"]') - .should('have.length', 14) + .should('have.length', 23) .should('contain', 'blank-contents.spec.js') .should('contain', 'dom-container.spec.js') .should('contain', 'dom-content.spec.js') @@ -290,14 +323,14 @@ e2e: { cy.get('[data-cy="spec-pattern"]').contains('cypress/e2e/**/*.spec.{js,ts}') cy.get('[data-cy="file-match-indicator"]') - .should('contain', '14 Matches') + .should('contain', '23 Matches') cy.withCtx(async (ctx, o) => { await ctx.actions.file.writeFileInProject(o.path, '') }, { path: getPathForPlatform('cypress/e2e/new-file.spec.js') }) cy.get('[data-cy="file-match-indicator"]') - .should('contain', '15 Matches') + .should('contain', '24 Matches') }) it('responds to specChange event for a removed file', () => { @@ -307,14 +340,14 @@ e2e: { cy.get('[data-cy="spec-pattern"]').contains('cypress/e2e/**/*.spec.{js,ts}') cy.get('[data-cy="file-match-indicator"]') - .should('contain', '14 Matches') + .should('contain', '23 Matches') cy.withCtx(async (ctx, o) => { await ctx.actions.file.removeFileInProject(o.path) }, { path: getPathForPlatform('cypress/e2e/dom-list.spec.js') }) cy.get('[data-cy="file-match-indicator"]') - .should('contain', '13 Matches') + .should('contain', '22 Matches') }) it('handles removing the last file', () => { @@ -340,6 +373,15 @@ e2e: { getPathForPlatform('cypress/e2e/accounts/accounts_new.spec.js'), getPathForPlatform('cypress/e2e/admin_users/admin_users_list.spec.js'), getPathForPlatform('cypress/e2e/admin_users/admin.user/foo_list.spec.js'), + getPathForPlatform('cypress/e2e/z001.spec.js'), + getPathForPlatform('cypress/e2e/z002.spec.js'), + getPathForPlatform('cypress/e2e/z003.spec.js'), + getPathForPlatform('cypress/e2e/z004.spec.js'), + getPathForPlatform('cypress/e2e/z005.spec.js'), + getPathForPlatform('cypress/e2e/z006.spec.js'), + getPathForPlatform('cypress/e2e/z007.spec.js'), + getPathForPlatform('cypress/e2e/z008.spec.js'), + getPathForPlatform('cypress/e2e/z009.spec.js'), ], }) @@ -361,7 +403,7 @@ e2e: { cy.get('[data-cy="spec-pattern"]').contains('cypress/e2e/**/*.spec.{js,ts}') cy.get('[data-cy="file-match-indicator"]') - .should('contain', '14 Matches') + .should('contain', '23 Matches') cy.withCtx(async (ctx) => { await ctx.actions.file.writeFileInProject('cypress.config.js', diff --git a/packages/app/cypress/e2e/support/execute-spec.ts b/packages/app/cypress/e2e/support/execute-spec.ts index e27b85a0512b..5478bbf37c6a 100644 --- a/packages/app/cypress/e2e/support/execute-spec.ts +++ b/packages/app/cypress/e2e/support/execute-spec.ts @@ -9,7 +9,7 @@ declare global { * 3. Waits (with a timeout of 30s) for the Rerun all tests button to be present. This ensures all tests have completed * */ - waitForSpecToFinish() + waitForSpecToFinish(): void } } } diff --git a/packages/app/src/pages/Specs/Index.vue b/packages/app/src/pages/Specs/Index.vue index bc39deb8e615..eb6945a9fb6f 100644 --- a/packages/app/src/pages/Specs/Index.vue +++ b/packages/app/src/pages/Specs/Index.vue @@ -11,6 +11,7 @@ import { computed, ref } from 'vue' -import { gql, useQuery, useSubscription } from '@urql/vue' +import { gql, SubscriptionHandlerArg, useQuery, useSubscription } from '@urql/vue' import { useI18n } from '@cy/i18n' import SpecsList from '../../specs/SpecsList.vue' import NoSpecsPage from '../../specs/NoSpecsPage.vue' import CreateSpecModal from '../../specs/CreateSpecModal.vue' -import { SpecsPageContainerDocument, SpecsPageContainer_SpecsChangeDocument } from '../../generated/graphql' +import { SpecsPageContainerDocument, SpecsPageContainer_SpecsChangeDocument, SpecsPageContainer_SpecListPollingDocument, SpecsPageContainer_BranchInfoDocument } from '../../generated/graphql' const { t } = useI18n() gql` -query SpecsPageContainer { +query SpecsPageContainer_BranchInfo { + currentProject { + id + branch + projectId + } +} +` + +gql` +query SpecsPageContainer($fromBranch: String!, $hasBranch: Boolean!) { ...Specs_SpecsList ...NoSpecsPage ...CreateSpecModal @@ -47,7 +58,7 @@ query SpecsPageContainer { ` gql` -subscription SpecsPageContainer_specsChange { +subscription SpecsPageContainer_specsChange($fromBranch: String!, $hasBranch: Boolean!) { specsChange { id specs { @@ -58,9 +69,48 @@ subscription SpecsPageContainer_specsChange { } ` -useSubscription({ query: SpecsPageContainer_SpecsChangeDocument }) +gql` +subscription SpecsPageContainer_specListPolling($fromBranch: String, $projectId: String) { + startPollingForSpecs(branchName: $fromBranch, projectId: $projectId) +} +` -const query = useQuery({ query: SpecsPageContainerDocument }) +const branchInfo = useQuery({ query: SpecsPageContainer_BranchInfoDocument }) + +const variables = computed(() => { + const fromBranch = branchInfo.data.value?.currentProject?.branch ?? '' + const hasBranch = Boolean(fromBranch) + + return { fromBranch, hasBranch } +}) + +const pollingVariables = computed(() => { + const fromBranch = branchInfo.data.value?.currentProject?.branch ?? null + const projectId = branchInfo.data.value?.currentProject?.projectId ?? null + + return { fromBranch, projectId } +}) + +useSubscription({ + query: SpecsPageContainer_SpecsChangeDocument, + variables, +}) + +const mostRecentUpdate = ref(null) + +const updateMostRecentUpdate: SubscriptionHandlerArg = (_, reportedUpdate) => { + mostRecentUpdate.value = reportedUpdate?.startPollingForSpecs ?? null +} + +useSubscription({ + query: SpecsPageContainer_SpecListPollingDocument, + variables: pollingVariables, +}, updateMostRecentUpdate) + +const query = useQuery({ + query: SpecsPageContainerDocument, + variables, +}) const isDefaultSpecPattern = computed(() => !!query.data.value?.currentProject?.isDefaultSpecPattern) @@ -83,7 +133,6 @@ const closeCreateSpecModal = () => { modalIsShown.value = false generator.value = null } - diff --git a/packages/app/src/runs/CloudConnectButton.vue b/packages/app/src/runs/CloudConnectButton.vue index bc2e681d3645..fa0d25d620c6 100644 --- a/packages/app/src/runs/CloudConnectButton.vue +++ b/packages/app/src/runs/CloudConnectButton.vue @@ -10,6 +10,7 @@ diff --git a/packages/app/src/runs/RunResults.cy.tsx b/packages/app/src/runs/RunResults.cy.tsx index f374dc3f9f57..efd23fc9146d 100644 --- a/packages/app/src/runs/RunResults.cy.tsx +++ b/packages/app/src/runs/RunResults.cy.tsx @@ -14,8 +14,8 @@ describe('', { viewportHeight: 150, viewportWidth: 250 }, () => { result[key] = res[key] }) }, - render (gql) { - return + render (props) { + return }, }) diff --git a/packages/app/src/runs/RunResults.vue b/packages/app/src/runs/RunResults.vue index e8b8ec861cb3..fe79447f28f5 100644 --- a/packages/app/src/runs/RunResults.vue +++ b/packages/app/src/runs/RunResults.vue @@ -4,36 +4,19 @@ v-if="props.gql.totalFlakyTests" class="rounded-md font-semibold bg-warning-100 text-sm py-2px px-4px text-warning-600 whitespace-nowrap" >{{ props.gql.totalFlakyTests }} Flaky -
-
- - {{ result.name }} - {{ result.value }} -
-
+ diff --git a/packages/app/src/runs/RunsError.spec.tsx b/packages/app/src/runs/RunsError.spec.tsx index f168bcee2e65..0b3261528c0b 100644 --- a/packages/app/src/runs/RunsError.spec.tsx +++ b/packages/app/src/runs/RunsError.spec.tsx @@ -6,17 +6,19 @@ describe('', () => { cy.mount({ name: 'RunsError', render () { - return (
- - The request timed out when trying to retrieve the recorded runs from the Cypress Dashboard.
- Please refresh the page to try again and visit our Status Page if this behavior continues. -
-
) + return ( +
+ + The request timed out when trying to retrieve the recorded runs from the Cypress Dashboard.
+ Please refresh the page to try again and visit our Status Page if this behavior continues. +
+
+ ) }, }) }) diff --git a/packages/app/src/runs/RunsErrorRenderer.vue b/packages/app/src/runs/RunsErrorRenderer.vue index 8ab117956f16..d9278287d8a8 100644 --- a/packages/app/src/runs/RunsErrorRenderer.vue +++ b/packages/app/src/runs/RunsErrorRenderer.vue @@ -38,7 +38,7 @@