diff --git a/README.md b/README.md index 5ed74849fdc5..d10a70893421 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,11 @@

- + + + + + Cypress Logo + +

Documentation | diff --git a/assets/cypress-logo-dark.png b/assets/cypress-logo-dark.png new file mode 100644 index 000000000000..97ced299171f Binary files /dev/null and b/assets/cypress-logo-dark.png differ diff --git a/assets/cypress-logo-light.png b/assets/cypress-logo-light.png new file mode 100644 index 000000000000..d8ae53068507 Binary files /dev/null and b/assets/cypress-logo-light.png differ diff --git a/browser-versions.json b/browser-versions.json index 423d9c28e419..1ba5d0e62917 100644 --- a/browser-versions.json +++ b/browser-versions.json @@ -1,4 +1,4 @@ { - "chrome:beta": "103.0.5060.42", - "chrome:stable": "102.0.5005.115" + "chrome:beta": "103.0.5060.53", + "chrome:stable": "103.0.5060.53" } diff --git a/npm/webpack-dev-server/test/.mocharc.js b/npm/webpack-dev-server/test/.mocharc.js index b41c643ee9ec..8b0298fc8597 100644 --- a/npm/webpack-dev-server/test/.mocharc.js +++ b/npm/webpack-dev-server/test/.mocharc.js @@ -1,4 +1,4 @@ module.exports = { spec: 'test/**/*.spec.ts', - timeout: 10000, + timeout: 15000, } diff --git a/package.json b/package.json index 3842468cd349..67c89ad31ade 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cypress", - "version": "10.1.0", + "version": "10.2.0", "description": "Cypress.io end to end testing tool", "private": true, "scripts": { diff --git a/packages/app/cypress/e2e/reporter_header.cy.ts b/packages/app/cypress/e2e/reporter_header.cy.ts index fc7d127b7f45..d5fd71f15577 100644 --- a/packages/app/cypress/e2e/reporter_header.cy.ts +++ b/packages/app/cypress/e2e/reporter_header.cy.ts @@ -24,13 +24,7 @@ describe('Reporter Header', () => { cy.get('[data-cy="spec-file-item"]').should('have.length', 3) .should('contain', 'dom-content.spec') - cy.get('body').type('f') - - cy.get('input').clear() - - cy.get('[data-cy="spec-file-item"]').should('have.length', '3') - - cy.get('input').type('asdf', { force: true }) + cy.get('input').clear().type('asdf', { force: true }) cy.get('[data-cy="spec-file-item"]').should('have.length', 0) }) diff --git a/packages/app/cypress/e2e/runs.cy.ts b/packages/app/cypress/e2e/runs.cy.ts index b207d3a5eb52..09ca8bb91bee 100644 --- a/packages/app/cypress/e2e/runs.cy.ts +++ b/packages/app/cypress/e2e/runs.cy.ts @@ -107,12 +107,6 @@ describe('App: Runs', { viewportWidth: 1200 }, () => { cy.findByTestId('sidebar-link-runs-page').click() - // TODO: investigate the scenario for this test - cy.withCtx((ctx) => { - // clear cloud cache - ctx.cloud.reset() - }) - cy.findByText(defaultMessages.runs.connect.buttonProject).click() cy.get('[aria-modal="true"]').should('exist') @@ -142,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 = [] } @@ -153,12 +147,6 @@ describe('App: Runs', { viewportWidth: 1200 }, () => { cy.findByTestId('sidebar-link-runs-page').click() - // TODO: investigate the scenario for this test - cy.withCtx((ctx) => { - // clear cloud cache - ctx.cloud.reset() - }) - cy.findByText(defaultMessages.runs.connect.buttonProject).click() cy.get('[aria-modal="true"]').should('exist') @@ -609,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 @@ -622,12 +608,9 @@ describe('App: Runs', { viewportWidth: 1200 }, () => { return obj.result }) + cy.visitApp() + cy.findByTestId('sidebar-link-runs-page').click() - // TODO: investigate the scenario for this test - cy.withCtx((ctx) => { - // clear cloud cache - ctx.cloud.reset() - }) cy.contains('h2', 'Cannot connect to the Cypress Dashboard') cy.percySnapshot() diff --git a/packages/app/cypress/e2e/specs_list_latest_runs.cy.ts b/packages/app/cypress/e2e/specs_list_latest_runs.cy.ts index 0a2829122d33..52f934b565ce 100644 --- a/packages/app/cypress/e2e/specs_list_latest_runs.cy.ts +++ b/packages/app/cypress/e2e/specs_list_latest_runs.cy.ts @@ -128,23 +128,11 @@ describe('ACI - Latest runs and Average duration', { viewportWidth: 1200, viewpo cy.openProject('cypress-in-cypress') cy.startAppServer() - cy.withCtx((ctx) => { - // clear cloud cache - ctx.cloud.reset() - }) - cy.withCtx((ctx, o) => { o.sinon.stub(ctx.lifecycleManager.git!, 'currentBranch').value('fakeBranch') }) }) - afterEach(() => { - cy.withCtx((ctx) => { - // clear cloud cache - ctx.cloud.reset() - }) - }) - context('when no runs are recorded', () => { beforeEach(() => { cy.loginUser() @@ -596,11 +584,6 @@ describe('ACI - Latest runs and Average duration', { viewportWidth: 1200 }, () = cy.openProject('cypress-in-cypress') cy.startAppServer() - cy.withCtx((ctx) => { - // clear cloud cache - ctx.cloud.reset() - }) - cy.loginUser() simulateRunData() @@ -612,13 +595,6 @@ describe('ACI - Latest runs and Average duration', { viewportWidth: 1200 }, () = cy.goOffline() }) - afterEach(() => { - cy.withCtx((ctx) => { - // clear cloud cache - ctx.cloud.reset() - }) - }) - it('shows placeholders for all visible specs', () => { allVisibleSpecsShouldBePlaceholders() }) diff --git a/packages/app/cypress/e2e/subscriptions/createCloudOrgModal-subscription.cy.ts b/packages/app/cypress/e2e/subscriptions/createCloudOrgModal-subscription.cy.ts index 8bac4edd174e..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,13 +27,9 @@ describe('App: Runs', { viewportWidth: 1200 }, () => { return obj.result }) - cy.findByTestId('sidebar-link-runs-page').click() + cy.visitApp() - // TODO: investigate the scenario for this test - cy.withCtx((ctx) => { - // clear cloud cache - ctx.cloud.reset() - }) + cy.findByTestId('sidebar-link-runs-page').click() cy.findByText(defaultMessages.runs.connect.buttonProject).click() cy.get('[aria-modal="true"]').should('exist') 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/package.json b/packages/app/package.json index 2f064685b791..99d9eebea668 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -34,7 +34,6 @@ "@vitejs/plugin-vue": "2.2.4", "@vitejs/plugin-vue-jsx": "1.3.8", "@vueuse/core": "7.2.2", - "@windicss/plugin-interaction-variants": "1.0.0", "ansi-to-html": "0.6.14", "bluebird": "3.5.3", "classnames": "2.3.1", @@ -69,7 +68,6 @@ "vue-i18n": "9.2.0-beta.7", "vue-router": "4", "vue-tsc": "^0.3.0", - "windicss": "3.1.4", "wonka": "^4.0.15" }, "files": [ diff --git a/packages/app/src/runner/event-manager.ts b/packages/app/src/runner/event-manager.ts index b43db785e39a..5b8be0ed6ae1 100644 --- a/packages/app/src/runner/event-manager.ts +++ b/packages/app/src/runner/event-manager.ts @@ -39,7 +39,7 @@ const driverToSocketEvents = 'backend:request automation:request mocha recorder: const driverTestEvents = 'test:before:run:async test:after:run'.split(' ') const driverToLocalEvents = 'viewport:changed config stop url:changed page:loading visit:failed visit:blank cypress:in:cypress:runner:event'.split(' ') const socketRerunEvents = 'runner:restart watched:file:changed'.split(' ') -const socketToDriverEvents = 'net:stubbing:event request:event script:error'.split(' ') +const socketToDriverEvents = 'net:stubbing:event request:event script:error cross:origin:automation:cookies'.split(' ') const localToReporterEvents = 'reporter:log:add reporter:log:state:changed reporter:log:remove'.split(' ') /** 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/settings/SettingsCard.vue b/packages/app/src/settings/SettingsCard.vue index 9af59e459c5d..d875b52d769d 100644 --- a/packages/app/src/settings/SettingsCard.vue +++ b/packages/app/src/settings/SettingsCard.vue @@ -32,7 +32,10 @@ -
+
diff --git a/packages/app/src/settings/SettingsContainer.cy.tsx b/packages/app/src/settings/SettingsContainer.cy.tsx index 6638c6e27922..76a766cef461 100644 --- a/packages/app/src/settings/SettingsContainer.cy.tsx +++ b/packages/app/src/settings/SettingsContainer.cy.tsx @@ -2,17 +2,19 @@ import { SettingsContainerFragmentDoc } from '../generated/graphql-test' import { defaultMessages } from '@cy/i18n' import SettingsContainer from './SettingsContainer.vue' +const mountSettingsContainer = () => cy.mountFragment(SettingsContainerFragmentDoc, { render: (gql) => }) + +beforeEach(() => mountSettingsContainer()) + describe('', { viewportHeight: 800, viewportWidth: 900 }, () => { - const mountSettingsContainer = () => cy.mountFragment(SettingsContainerFragmentDoc, { render: (gql) => }) + it('renders sections collapsed by default', () => { + cy.findByTestId('settings').should('be.visible') + cy.findByTestId('setting-expanded-container').should('not.exist') - it('renders', () => { - mountSettingsContainer() cy.percySnapshot() }) it('expands and collapses project settings', () => { - mountSettingsContainer() - cy.contains('Project Settings').click() cy.findByText(defaultMessages.settingsPage.experiments.title).scrollIntoView().should('be.visible') @@ -25,8 +27,6 @@ describe('', { viewportHeight: 800, viewportWidth: 900 }, ( }) it('expands and collapses device settings', () => { - mountSettingsContainer() - cy.contains('Device Settings').click() cy.findByText(defaultMessages.settingsPage.editor.title).should('be.visible') @@ -40,8 +40,6 @@ describe('', { viewportHeight: 800, viewportWidth: 900 }, ( }) it('expands and collapses cloud settings', () => { - mountSettingsContainer() - cy.contains('Dashboard Settings').click() cy.findByText(defaultMessages.settingsPage.projectId.title).scrollIntoView().should('be.visible') @@ -52,7 +50,6 @@ describe('', { viewportHeight: 800, viewportWidth: 900 }, ( }) it('renders footer with CTA button', () => { - mountSettingsContainer() cy.contains('p', defaultMessages.settingsPage.footer.text.replace('{testingType}', 'E2E')) cy.contains('a', defaultMessages.settingsPage.footer.button) .should('have.attr', 'href', defaultMessages.settingsPage.footer.buttonLink) diff --git a/packages/app/src/specs/InlineSpecListHeader.cy.tsx b/packages/app/src/specs/InlineSpecListHeader.cy.tsx index 8ee30fd332de..3d31fa26f187 100644 --- a/packages/app/src/specs/InlineSpecListHeader.cy.tsx +++ b/packages/app/src/specs/InlineSpecListHeader.cy.tsx @@ -10,7 +10,6 @@ describe('InlineSpecListHeader', () => { cy.wrap(search).as('search') const methods = { - search: search.value, 'onUpdate:search': (val: string) => { search.value = val }, @@ -19,7 +18,7 @@ describe('InlineSpecListHeader', () => { cy.mount(() => (
- +
)) } @@ -28,6 +27,7 @@ describe('InlineSpecListHeader', () => { const searchString = 'my/component.cy.tsx' cy.findByLabelText(defaultMessages.specPage.searchPlaceholder) + // `force` necessary due to the field label being overlaid on top of the input .type(searchString, { delay: 0, force: true }) .get('@search').its('value').should('eq', searchString) }) @@ -40,6 +40,21 @@ describe('InlineSpecListHeader', () => { .should('have.been.called') }) + it('clears search field when clear button is clicked', () => { + mountWithResultCount(0) + + cy.findByTestId('clear-search-button') + .should('not.exist') + + cy.findByLabelText(defaultMessages.specPage.searchPlaceholder) + // `force` necessary due to the field label being overlaid on top of the input + .type('abcd', { delay: 0, force: true }) + .get('@search').its('value').should('eq', 'abcd') + + cy.findByTestId('clear-search-button').click() + cy.get('@search').its('value').should('eq', '') + }) + it('exposes the result count correctly to assistive tech', () => { mountWithResultCount(0) cy.contains('No Matches') diff --git a/packages/app/src/specs/InlineSpecListHeader.vue b/packages/app/src/specs/InlineSpecListHeader.vue index ac9a9617a760..b388a71e9567 100644 --- a/packages/app/src/specs/InlineSpecListHeader.vue +++ b/packages/app/src/specs/InlineSpecListHeader.vue @@ -3,7 +3,7 @@ class="border-b-1 border-gray-900 h-64px mx-16px grid gap-8px grid-cols-[minmax(0,1fr),24px] pointer-cursor items-center" >
{{ t('specPage.searchPlaceholder') }} +
+ ) + }) +} + +describe('', () => { + context('runs scenario 1', () => { + beforeEach(() => { + const runs = fakeRuns(['PASSED', 'FAILED', 'CANCELLED', 'ERRORED']) + + mountWithRuns(runs) }) - cy.findByTestId('run-status-dots').trigger('mouseenter') - cy.get('.v-popper__popper--shown').contains('spec.cy.ts') - cy.findAllByTestId('run-status-dot-0').should('have.class', 'icon-light-orange-400') - cy.findAllByTestId('run-status-dot-1').should('have.class', 'icon-light-gray-300') - cy.findAllByTestId('run-status-dot-2').should('have.class', 'icon-light-red-400') - cy.findAllByTestId('run-status-dot-latest').should('not.have.class', 'animate-spin') + it('renders as expected', () => { + cy.findByTestId('run-status-dots').trigger('mouseenter') + cy.get('.v-popper__popper--shown').contains('spec.cy.ts') + cy.findAllByTestId('run-status-dot-0').should('have.class', 'icon-light-orange-400') + cy.findAllByTestId('run-status-dot-1').should('have.class', 'icon-light-gray-300') + cy.findAllByTestId('run-status-dot-2').should('have.class', 'icon-light-red-400') + cy.findAllByTestId('run-status-dot-latest').should('not.have.class', 'animate-spin') + }) }) - it('mounts correctly for example scenario 2', () => { - const runs = fakeRuns(['NOTESTS', 'OVERLIMIT', 'RUNNING', 'TIMEDOUT']) - - cy.mount(() => { - const gql: RunStatusDotsFragment = { - id: 'id', - data: { - __typename: 'CloudProjectSpec', - id: 'id', - retrievedAt: new Date().toISOString(), - specRuns: { - nodes: [ - ...runs as any, // suppress TS compiler - ], - }, - }, - } - - return ( - - ) + context('runs scenario 2', () => { + beforeEach(() => { + const runs = fakeRuns(['NOTESTS', 'UNCLAIMED', 'RUNNING', 'TIMEDOUT']) + + mountWithRuns(runs) }) - cy.findByTestId('run-status-dots').trigger('mouseenter') - cy.get('.v-popper__popper--shown').contains('spec.cy.ts') - cy.findAllByTestId('run-status-dot-0').should('have.class', 'icon-light-orange-400') - cy.findAllByTestId('run-status-dot-1').should('have.class', 'icon-light-indigo-400') - cy.findAllByTestId('run-status-dot-2').should('have.class', 'icon-light-orange-400') - cy.findAllByTestId('run-status-dot-latest').should('not.have.class', 'animate-spin') + it('renders as expected', () => { + cy.findByTestId('run-status-dots').trigger('mouseenter') + cy.get('.v-popper__popper--shown').contains('spec.cy.ts') + cy.findAllByTestId('run-status-dot-0').should('have.class', 'icon-light-orange-400') + cy.findAllByTestId('run-status-dot-1').should('have.class', 'icon-light-indigo-400') + cy.findAllByTestId('run-status-dot-2').should('have.class', 'icon-light-gray-400') + cy.findAllByTestId('run-status-dot-latest').should('not.have.class', 'animate-spin') + }) }) - it('mounts correctly for example scenario 3', () => { - const runs = fakeRuns(['RUNNING']) - - cy.mount(() => { - const gql: RunStatusDotsFragment = { - id: 'id', - data: { - __typename: 'CloudProjectSpec', - id: 'id', - retrievedAt: new Date().toISOString(), - specRuns: { - nodes: [ - ...runs as any, // suppress TS compiler - ], - }, - }, - } - - return ( - - ) + context('single RUNNING status', () => { + beforeEach(() => { + const runs = fakeRuns(['RUNNING']) + + mountWithRuns(runs) }) - cy.findByTestId('run-status-dots').trigger('mouseenter') - cy.get('.v-popper__popper--shown').contains('spec.cy.ts') - 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('have.class', 'animate-spin') + it('renders as expected', () => { + cy.findByTestId('run-status-dots').trigger('mouseenter') + cy.get('.v-popper__popper--shown').contains('spec.cy.ts') + 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('have.class', 'animate-spin') + }) }) - it('renders placeholder without tooltip or link', () => { - cy.mount(() => { - const gql: RunStatusDotsFragment = { - id: 'id', - data: { - __typename: 'CloudProjectSpec', - id: 'id', - retrievedAt: new Date().toISOString(), - specRuns: { - nodes: [], - }, - }, - } - - return ( - - ) + context('single UNCLAIMED status', () => { + beforeEach(() => { + const runs = fakeRuns(['UNCLAIMED']) + + mountWithRuns(runs) + }) + + it('renders as expected', () => { + cy.findByTestId('run-status-dots').trigger('mouseenter') + cy.get('.v-popper__popper--shown').contains('spec.cy.ts') + 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') }) + }) - cy.findByTestId('external').should('not.exist') - cy.findByTestId('run-status-dots').trigger('mouseenter') - cy.get('.v-popper__popper--shown').should('not.exist') + context('no runs', () => { + beforeEach(() => { + mountWithRuns([]) + }) + + it('renders placeholder without tooltip or link', () => { + cy.findByTestId('external').should('not.exist') + cy.findByTestId('run-status-dots').trigger('mouseenter') + cy.get('.v-popper__popper--shown').should('not.exist') + }) + }) + + context('unknown/unhandled statuses', () => { + beforeEach(() => { + const runs = fakeRuns(fill(['', '', '', ''], 'FAKE_UNKNOWN_STATUS' as any)) + + mountWithRuns(runs) + }) + + it('renders as expected', () => { + cy.findByTestId('run-status-dots').trigger('mouseenter') + cy.get('.v-popper__popper--shown').contains('spec.cy.ts') + 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') + }) }) }) diff --git a/packages/app/src/specs/RunStatusDots.vue b/packages/app/src/specs/RunStatusDots.vue index 72b3021a0122..ffce6628cb13 100644 --- a/packages/app/src/specs/RunStatusDots.vue +++ b/packages/app/src/specs/RunStatusDots.vue @@ -73,6 +73,7 @@ import ErroredIcon from '~icons/cy/errored-solid_x16.svg' import FailedIcon from '~icons/cy/failed-solid_x16.svg' import PassedIcon from '~icons/cy/passed-solid_x16.svg' import PlaceholderIcon from '~icons/cy/placeholder-solid_x16.svg' +import QueuedIcon from '~icons/cy/queued-solid_x16.svg' import RunningIcon from '~icons/cy/running-outline_x16.svg' import SpecRunSummary from './SpecRunSummary.vue' import { gql } from '@urql/vue' @@ -155,9 +156,9 @@ const dotClasses = computed(() => { case 'FAILED': return 'icon-light-red-400' case 'ERRORED': - case 'OVERLIMIT': case 'TIMEDOUT': return 'icon-light-orange-400' + case 'UNCLAIMED': case 'NOTESTS': return 'icon-light-gray-400' case 'CANCELLED': @@ -183,10 +184,11 @@ const latestDot = computed(() => { return { icon: PassedIcon, spin: false, status } case 'RUNNING': return { icon: RunningIcon, spin: true, status } + case 'UNCLAIMED': + return { icon: QueuedIcon, spin: false, status } case 'FAILED': return { icon: FailedIcon, spin: false, status } case 'ERRORED': - case 'OVERLIMIT': case 'TIMEDOUT': return { icon: ErroredIcon, spin: false, status } case 'NOTESTS': diff --git a/packages/app/src/specs/SpecHeaderCloudDataTooltip.cy.tsx b/packages/app/src/specs/SpecHeaderCloudDataTooltip.cy.tsx index 608541d186c7..e1f3c002ff64 100644 --- a/packages/app/src/specs/SpecHeaderCloudDataTooltip.cy.tsx +++ b/packages/app/src/specs/SpecHeaderCloudDataTooltip.cy.tsx @@ -1,4 +1,4 @@ -import { SpecHeaderCloudDataTooltipFragmentDoc } from '../generated/graphql-test' +import { SpecHeaderCloudDataTooltipFragmentDoc, SpecHeaderCloudDataTooltip_RequestAccessDocument } from '../generated/graphql-test' import SpecHeaderCloudDataTooltip from './SpecHeaderCloudDataTooltip.vue' import { get, set } from 'lodash' import { defaultMessages } from '@cy/i18n' @@ -136,11 +136,29 @@ describe('SpecHeaderCloudDataTooltip', () => { .should('be.visible') .and('contain', get(defaultMessages, msgKeys.noAccess).replace('{0}', get(defaultMessages, msgKeys.docs))) + cy.percySnapshot() + }) + + it('should update to "Request Sent" when button is triggered', () => { + cy.stubMutationResolver(SpecHeaderCloudDataTooltip_RequestAccessDocument, (defineResult) => { + return defineResult({ + cloudProjectRequestAccess: { + __typename: 'CloudProjectUnauthorized', + message: 'msg', + hasRequestedAccess: true, + }, + }) + }) + + cy.get('.v-popper').trigger('mouseenter') + cy.findByTestId('request-access-button') .should('be.visible') .click() - cy.percySnapshot() + cy.findByTestId('access-requested-button') + .should('be.visible') + .should('be.disabled') }) }) diff --git a/packages/app/src/specs/SpecHeaderCloudDataTooltip.vue b/packages/app/src/specs/SpecHeaderCloudDataTooltip.vue index c22a0bed5638..e74bd36cf3b5 100644 --- a/packages/app/src/specs/SpecHeaderCloudDataTooltip.vue +++ b/packages/app/src/specs/SpecHeaderCloudDataTooltip.vue @@ -100,9 +100,10 @@ import ConnectIcon from '~icons/cy/chain-link_x16.svg' import UserOutlineIcon from '~icons/cy/user-outline_x16.svg' import SendIcon from '~icons/cy/paper-airplane_x16.svg' import ExternalLink from '@cy/gql-components/ExternalLink.vue' -import { RunsErrorRenderer_RequestAccessDocument, SpecHeaderCloudDataTooltipFragment } from '../generated/graphql' +import type { SpecHeaderCloudDataTooltipFragment } from '../generated/graphql' +import { SpecHeaderCloudDataTooltip_RequestAccessDocument } from '../generated/graphql' import { useI18n } from '@cy/i18n' -import { computed } from 'vue' +import { computed, onMounted, ref } from 'vue' import { gql, useMutation } from '@urql/vue' const { t } = useI18n() @@ -138,6 +139,26 @@ fragment SpecHeaderCloudDataTooltip on Query { } ` +gql` +mutation SpecHeaderCloudDataTooltip_RequestAccess( $projectId: String! ) { + cloudProjectRequestAccess(projectSlug: $projectId) { + __typename + ... on CloudProjectUnauthorized { + message + hasRequestedAccess + } + } +} +` + +const hasRequestedAccess = ref(false) + +onMounted(() => { + if (props.gql.currentProject?.cloudProject?.__typename === 'CloudProjectUnauthorized') { + hasRequestedAccess.value = props.gql.currentProject.cloudProject.hasRequestedAccess ?? false + } +}) + const projectConnectionStatus = computed(() => { if (!props.gql.cloudViewer) return 'LOGGED_OUT' @@ -146,7 +167,7 @@ const projectConnectionStatus = computed(() => { if (props.gql.currentProject?.cloudProject?.__typename === 'CloudProjectNotFound') return 'NOT_FOUND' if (props.gql.currentProject?.cloudProject?.__typename === 'CloudProjectUnauthorized') { - if (props.gql.currentProject.cloudProject.hasRequestedAccess) { + if (hasRequestedAccess.value) { return 'ACCESS_REQUESTED' } @@ -156,13 +177,19 @@ const projectConnectionStatus = computed(() => { return 'CONNECTED' }) -const requestAccessMutation = useMutation(RunsErrorRenderer_RequestAccessDocument) +const requestAccessMutation = useMutation(SpecHeaderCloudDataTooltip_RequestAccessDocument) -function requestAccess () { +async function requestAccess () { const projectId = props.gql.currentProject?.projectId if (projectId) { - requestAccessMutation.executeMutation({ projectId }) + const result = await requestAccessMutation.executeMutation({ projectId }) + + if (result.data?.cloudProjectRequestAccess?.__typename === 'CloudProjectUnauthorized') { + hasRequestedAccess.value = result.data.cloudProjectRequestAccess.hasRequestedAccess ?? false + } else { + hasRequestedAccess.value = false + } } } diff --git a/packages/app/src/specs/SpecRunSummary.cy.tsx b/packages/app/src/specs/SpecRunSummary.cy.tsx index 5f4f99b4e7e2..e711be4dd4a6 100644 --- a/packages/app/src/specs/SpecRunSummary.cy.tsx +++ b/packages/app/src/specs/SpecRunSummary.cy.tsx @@ -2,6 +2,40 @@ import SpecRunSummary from './SpecRunSummary.vue' import { exampleRuns } from '@packages/frontend-shared/cypress/support/mock-graphql/fakeCloudSpecRun' import type { CloudSpecRun } from '@packages/graphql/src/gen/cloud-source-types.gen' +function validateTopBorder (color: string): void { + cy.findByTestId('spec-run-summary') + .should('have.css', 'border-top', `4px solid ${color}`) +} + +function validateFilename (expected: string): void { + cy.findByTestId('spec-run-filename').should('have.text', expected) +} + +function validateTimeAgo (expected: string): void { + cy.findByTestId('spec-run-time-ago') + .should('have.text', expected) +} + +function validateStatus (status: string, color: string): void { + cy.findByTestId('spec-run-status') + .should('have.css', 'color', color) + .and('have.text', status) +} + +function validateDuration1 (expected: string): void { + cy.findByTestId('spec-run-duration-1') + .should('have.text', expected) +} + +function validateDuration2 (expected: string): void { + cy.findByTestId('spec-run-duration-2') + .should('have.text', expected) +} + +function validateResultCountsVisible (): void { + cy.findByTestId('spec-run-result-counts').should('be.visible') +} + describe('', { keystrokeDelay: 0 }, () => { const runs = exampleRuns() @@ -19,27 +53,15 @@ describe('', { keystrokeDelay: 0 }, () => { }) it('should render expected content', () => { - cy.findByTestId('spec-run-summary') - // Green border at top - .should('have.css', 'border-top', '4px solid rgb(31, 169, 113)') - - cy.findByTestId('spec-run-filename').should('have.text', 'mySpecFile.spec.ts') - - cy.findByTestId('spec-run-status') - // Green text with expected status text - .should('have.css', 'color', 'rgb(0, 129, 77)') - .and('have.text', 'Passed') - - cy.findByTestId('spec-run-time-ago') - .should('have.text', '1 year ago') - - cy.findByTestId('spec-run-duration-1') - .should('have.text', '2:23') - - cy.findByTestId('spec-run-duration-2') - .should('have.text', '2:39') - - cy.findByTestId('spec-run-result-counts').should('be.visible') + // Green border + validateTopBorder('rgb(31, 169, 113)') + validateFilename('mySpecFile.spec.ts') + // Green text + validateStatus('Passed', 'rgb(0, 129, 77)') + validateTimeAgo('1 year ago') + validateDuration1('2:23') + validateDuration2('2:39') + validateResultCountsVisible() }) }) @@ -49,26 +71,15 @@ describe('', { keystrokeDelay: 0 }, () => { }) it('should render expected content', () => { - cy.findByTestId('spec-run-summary') - // Red border at top - .should('have.css', 'border-top', '4px solid rgb(228, 87, 112)') - - cy.findByTestId('spec-run-filename').should('have.text', 'mySpecFile.spec.ts') - - cy.findByTestId('spec-run-status') - // Red text with expected status text - .should('have.css', 'color', 'rgb(198, 43, 73)') - .and('have.text', 'Failed') - - cy.findByTestId('spec-run-time-ago') - .should('have.text', '1 year ago') - - cy.findByTestId('spec-run-duration-1') - .should('have.text', '1:02:40') - + // Red border + validateTopBorder('rgb(228, 87, 112)') + validateFilename('mySpecFile.spec.ts') + // Red text + validateStatus('Failed', 'rgb(198, 43, 73)') + validateTimeAgo('1 year ago') + validateDuration1('1:02:40') cy.findByTestId('spec-run-duration-2').should('not.exist') - - cy.findByTestId('spec-run-result-counts').should('be.visible') + validateResultCountsVisible() }) }) @@ -78,27 +89,15 @@ describe('', { keystrokeDelay: 0 }, () => { }) it('should render expected content', () => { - cy.findByTestId('spec-run-summary') - // Gray border at top - .should('have.css', 'border-top', '4px solid rgb(144, 149, 173)') - - cy.findByTestId('spec-run-filename').should('have.text', 'mySpecFile.spec.ts') - - cy.findByTestId('spec-run-status') - // Gray text with expected status text - .should('have.css', 'color', 'rgb(90, 95, 122)') - .and('have.text', 'Canceled') - - cy.findByTestId('spec-run-time-ago') - .should('have.text', '1 year ago') - - cy.findByTestId('spec-run-duration-1') - .should('have.text', '2:23') - - cy.findByTestId('spec-run-duration-2') - .should('have.text', '2:39') - - cy.findByTestId('spec-run-result-counts').should('be.visible') + // Gray border + validateTopBorder('rgb(144, 149, 173)') + validateFilename('mySpecFile.spec.ts') + // Gray text + validateStatus('Canceled', 'rgb(90, 95, 122)') + validateTimeAgo('1 year ago') + validateDuration1('2:23') + validateDuration2('2:39') + validateResultCountsVisible() }) }) @@ -108,27 +107,15 @@ describe('', { keystrokeDelay: 0 }, () => { }) it('should render expected content', () => { - cy.findByTestId('spec-run-summary') - // Orange border at top - .should('have.css', 'border-top', '4px solid rgb(219, 121, 3)') - - cy.findByTestId('spec-run-filename').should('have.text', 'mySpecFile.spec.ts') - - cy.findByTestId('spec-run-status') - // Orange text with expected status text - .should('have.css', 'color', 'rgb(189, 88, 0)') - .and('have.text', 'Errored') - - cy.findByTestId('spec-run-time-ago') - .should('have.text', '1 year ago') - - cy.findByTestId('spec-run-duration-1') - .should('have.text', '1:02:40') - - cy.findByTestId('spec-run-duration-2') - .should('have.text', '10:26:40') - - cy.findByTestId('spec-run-result-counts').should('be.visible') + // Orange border + validateTopBorder('rgb(219, 121, 3)') + validateFilename('mySpecFile.spec.ts') + // Orange text + validateStatus('Errored', 'rgb(189, 88, 0)') + validateTimeAgo('1 year ago') + validateDuration1('1:02:40') + validateDuration2('10:26:40') + validateResultCountsVisible() }) }) @@ -138,117 +125,89 @@ describe('', { keystrokeDelay: 0 }, () => { }) it('should render expected content', () => { - cy.findByTestId('spec-run-summary') - // Gray border at top - .should('have.css', 'border-top', '4px solid rgb(144, 149, 173)') - - cy.findByTestId('spec-run-filename').should('have.text', 'mySpecFile.spec.ts') - - cy.findByTestId('spec-run-status') - // Gray text with expected status text - .should('have.css', 'color', 'rgb(90, 95, 122)') - .and('have.text', 'No tests') - - cy.findByTestId('spec-run-time-ago') - .should('have.text', '1 year ago') - - cy.findByTestId('spec-run-duration-1') - .should('have.text', '2:23') - - cy.findByTestId('spec-run-duration-2') - .should('have.text', '2:39') - - cy.findByTestId('spec-run-result-counts').should('be.visible') + // Gray border + validateTopBorder('rgb(144, 149, 173)') + validateFilename('mySpecFile.spec.ts') + validateStatus('No tests', 'rgb(90, 95, 122)') + validateTimeAgo('1 year ago') + validateDuration1('2:23') + validateDuration2('2:39') + validateResultCountsVisible() }) }) - context('over limit', () => { + context('running', () => { beforeEach(() => { - mountWithRun(runs[5]) + mountWithRun(runs[6]) }) it('should render expected content', () => { - cy.findByTestId('spec-run-summary') - // Orange border at top - .should('have.css', 'border-top', '4px solid rgb(219, 121, 3)') - - cy.findByTestId('spec-run-filename').should('have.text', 'mySpecFile.spec.ts') - - cy.findByTestId('spec-run-status') - // Orange text with expected status text - .should('have.css', 'color', 'rgb(189, 88, 0)') - .and('have.text', 'Over limit') - - cy.findByTestId('spec-run-time-ago') - .should('have.text', '1 year ago') - - cy.findByTestId('spec-run-duration-1') - .should('have.text', '2:23') - - cy.findByTestId('spec-run-duration-2') - .should('have.text', '2:39') - - cy.findByTestId('spec-run-result-counts').should('be.visible') + // Blue border + validateTopBorder('rgb(100, 112, 243)') + validateFilename('mySpecFile.spec.ts') + // Blue text + validateStatus('Running', 'rgb(73, 86, 227)') + validateTimeAgo('1 year ago') + validateDuration1('2:23') + validateDuration2('2:39') + validateResultCountsVisible() }) }) - context('running', () => { + context('timed out', () => { beforeEach(() => { - mountWithRun(runs[6]) + mountWithRun(runs[7]) }) it('should render expected content', () => { - cy.findByTestId('spec-run-summary') - // Blue border at top - .should('have.css', 'border-top', '4px solid rgb(100, 112, 243)') - - cy.findByTestId('spec-run-filename').should('have.text', 'mySpecFile.spec.ts') - - cy.findByTestId('spec-run-status') - // Blue text with expected status text - .should('have.css', 'color', 'rgb(73, 86, 227)') - .and('have.text', 'Running') - - cy.findByTestId('spec-run-time-ago') - .should('have.text', '1 year ago') - - cy.findByTestId('spec-run-duration-1') - .should('have.text', '2:23') + // Orange border + validateTopBorder('rgb(219, 121, 3)') + validateFilename('mySpecFile.spec.ts') + // Orange text + validateStatus('Timed out', 'rgb(189, 88, 0)') + validateTimeAgo('1 year ago') + validateDuration1('2:23') + validateDuration2('2:39') + validateResultCountsVisible() + }) + }) - cy.findByTestId('spec-run-duration-2') - .should('have.text', '2:39') + context('queued', () => { + beforeEach(() => { + mountWithRun(runs[8]) + }) - cy.findByTestId('spec-run-result-counts').should('be.visible') + it('should render expected content', () => { + // Gray border + validateTopBorder('rgb(144, 149, 173)') + validateFilename('mySpecFile.spec.ts') + // Gray text + validateStatus('Queued', 'rgb(90, 95, 122)') + validateTimeAgo('1 year ago') + validateDuration1('2:23') + validateDuration2('2:39') + validateResultCountsVisible() }) }) - context('timed out', () => { + context('unhandled status', () => { beforeEach(() => { - mountWithRun(runs[7]) + mountWithRun(runs[9]) }) it('should render expected content', () => { - cy.findByTestId('spec-run-summary') - // Orange border at top - .should('have.css', 'border-top', '4px solid rgb(219, 121, 3)') - - cy.findByTestId('spec-run-filename').should('have.text', 'mySpecFile.spec.ts') + // Gray border + validateTopBorder('rgb(144, 149, 173)') + validateFilename('mySpecFile.spec.ts') + // Should not render any status text cy.findByTestId('spec-run-status') - // Orange text with expected status text - .should('have.css', 'color', 'rgb(189, 88, 0)') - .and('have.text', 'Timed out') - - cy.findByTestId('spec-run-time-ago') - .should('have.text', '1 year ago') - - cy.findByTestId('spec-run-duration-1') - .should('have.text', '2:23') - - cy.findByTestId('spec-run-duration-2') - .should('have.text', '2:39') + .should('not.exist') - cy.findByTestId('spec-run-result-counts').should('be.visible') + validateTimeAgo('1 year ago') + validateDuration1('2:23') + validateDuration2('2:39') + validateResultCountsVisible() }) }) }) diff --git a/packages/app/src/specs/SpecRunSummary.vue b/packages/app/src/specs/SpecRunSummary.vue index 3b215bee0a2b..4ccee8ea1a53 100644 --- a/packages/app/src/specs/SpecRunSummary.vue +++ b/packages/app/src/specs/SpecRunSummary.vue @@ -119,8 +119,8 @@ const statusText = computed(() => { case 'ERRORED': return 'Errored' case 'FAILED': return 'Failed' case 'NOTESTS': return 'No tests' - case 'OVERLIMIT': return 'Over limit' case 'PASSED': return 'Passed' + case 'UNCLAIMED': return 'Queued' case 'RUNNING': return 'Running' case 'TIMEDOUT': return 'Timed out' default: return null @@ -131,7 +131,6 @@ const statusColor = computed(() => { if (!props.run?.status) return 'gray' switch (props.run.status) { - case 'OVERLIMIT': case 'ERRORED': case 'TIMEDOUT': return 'orange' @@ -143,6 +142,7 @@ const statusColor = computed(() => { return 'indigo' case 'CANCELLED': case 'NOTESTS': + case 'UNCLAIMED': default: return 'gray' } }) diff --git a/packages/app/src/specs/SpecsList.vue b/packages/app/src/specs/SpecsList.vue index aa06ea61e6b0..515cb43ccbc0 100644 --- a/packages/app/src/specs/SpecsList.vue +++ b/packages/app/src/specs/SpecsList.vue @@ -184,7 +184,7 @@ class="h-full grid justify-items-end items-center" > { + const search = ref('') + + cy.wrap(search).as('search') + + cy.mount(defineComponent({ + setup () { + return () => h(SpecsListHeader, { + modelValue: search.value, + 'onUpdate:modelValue': (val: string) => { + search.value = val + }, + }) + }, + })) +} + describe('', { keystrokeDelay: 0 }, () => { it('can be searched', () => { - const search = ref('') const searchString = 'my/component.cy.tsx' - cy.mount(defineComponent({ - setup () { - return () => h(SpecsListHeader, { - modelValue: search.value, - 'onUpdate:modelValue': (val: string) => { - search.value = val - }, - }) - }, - })) + mountWithSearchRef() cy.findByLabelText(defaultMessages.specPage.searchPlaceholder) .type(searchString, { delay: 0 }) - .then(() => { - expect(search.value).to.equal(searchString) - }) + .get('@search').its('value').should('eq', searchString) + }) + + it('clears search field when clear button is clicked', () => { + mountWithSearchRef() + + cy.findByTestId('clear-search-button') + .should('not.exist') + + cy.findByLabelText(defaultMessages.specPage.searchPlaceholder) + .type('abcd', { delay: 0 }) + .get('@search').its('value').should('eq', 'abcd') + + cy.findByTestId('clear-search-button') + .click() + .get('@search').its('value').should('eq', '') }) it('emits a new spec event', () => { diff --git a/packages/app/src/specs/SpecsListHeader.vue b/packages/app/src/specs/SpecsListHeader.vue index 9af391d02ee0..bd791c939eb4 100644 --- a/packages/app/src/specs/SpecsListHeader.vue +++ b/packages/app/src/specs/SpecsListHeader.vue @@ -14,7 +14,17 @@ >