diff --git a/.circleci/src/pipeline/@pipeline.yml b/.circleci/src/pipeline/@pipeline.yml index 29d1b648191..7cdf2548014 100644 --- a/.circleci/src/pipeline/@pipeline.yml +++ b/.circleci/src/pipeline/@pipeline.yml @@ -1967,6 +1967,11 @@ jobs: source ./system-tests/scripts/bootstrap-docker-container.sh 'yarn cypress run --component --spec src/mount.cy.ts src/App.cy.ts' system-tests-chrome: + parameters: + <<: *defaultsParameters + run-index: + type: integer + default: 1 <<: *defaults resource_class: medium+ parallelism: 8 @@ -2067,6 +2072,9 @@ jobs: <<: *defaults parameters: <<: *defaultsParameters + run-index: + type: integer + default: 1 resource_class: type: string default: medium+ diff --git a/.circleci/src/pipeline/workflows/pull-request.yml b/.circleci/src/pipeline/workflows/pull-request.yml index 8f7c6a652ee..ef62b0e37aa 100644 --- a/.circleci/src/pipeline/workflows/pull-request.yml +++ b/.circleci/src/pipeline/workflows/pull-request.yml @@ -1,6 +1,9 @@ # The workflow runs for: # External PR after approval (approved contributor PR that's not a main branch) # Internal PR for any branch that does is not develop or release/ related +# +# TEMPORARY: Reduced to system-tests-chrome (x10) + run-app-component-tests-chrome (x10) + deps for CI testing. +# Restore full job list from git history when done. when: and: - not: @@ -32,63 +35,11 @@ jobs: filters: branches: only: /^pull\/[0-9]+/ - - check-ts: - requires: - - internal-pr-build - - external-pr-build - - health-check: - requires: - - internal-pr-build - - external-pr-build - - lint: - name: linux-lint - requires: - - internal-pr-build - - external-pr-build - - lint-types: - requires: - - internal-pr-build - - external-pr-build - approve-contributor-pr: type: approval filters: branches: only: /^pull\/[0-9]+/ - - # The following jobs are only run for contributor PRs once they are approved - # unit, integration and e2e tests - - cli-visual-tests: - context: test-runner:percy - requires: - - internal-pr-build - - external-pr-build - - approve-contributor-pr - - unit-tests: - requires: - - internal-pr-build - - external-pr-build - - approve-contributor-pr - - verify-release-readiness: - context: [test-runner:npm-release, org-npm-credentials] - requires: - - internal-pr-build - - external-pr-build - - approve-contributor-pr - - server-unit-tests: - requires: - - internal-pr-build - - external-pr-build - - approve-contributor-pr - - server-integration-tests: - requires: - - internal-pr-build - - external-pr-build - - approve-contributor-pr - - server-performance-tests: - requires: - - internal-pr-build - - external-pr-build - - approve-contributor-pr - system-tests-node-modules-install: context: test-runner:performance-tracking requires: @@ -97,126 +48,9 @@ jobs: - approve-contributor-pr - system-tests-chrome: context: test-runner:performance-tracking - requires: - - system-tests-node-modules-install - - system-tests-electron: - context: test-runner:performance-tracking - requires: - - system-tests-node-modules-install - - system-tests-firefox: - context: test-runner:performance-tracking - requires: - - system-tests-node-modules-install - - system-tests-webkit: - context: test-runner:performance-tracking - requires: - - system-tests-node-modules-install - - system-tests-non-root: - context: test-runner:performance-tracking - executor: non-root-docker-user - requires: - - system-tests-node-modules-install - - driver-integration-tests-chrome: - context: test-runner:cypress-record-key - requires: - - internal-pr-build - - external-pr-build - - approve-contributor-pr - - driver-integration-tests-chrome-inject-document-domain: - context: test-runner:cypress-record-key - requires: - - internal-pr-build - - external-pr-build - - approve-contributor-pr - - driver-integration-tests-chrome-beta: - context: test-runner:cypress-record-key - requires: - - internal-pr-build - - external-pr-build - - approve-contributor-pr - - driver-integration-tests-chrome-beta-inject-document-domain: - context: test-runner:cypress-record-key - requires: - - internal-pr-build - - external-pr-build - - approve-contributor-pr - - driver-integration-tests-firefox: - context: test-runner:cypress-record-key - requires: - - internal-pr-build - - external-pr-build - - approve-contributor-pr - - driver-integration-tests-electron: - context: test-runner:cypress-record-key - requires: - - internal-pr-build - - external-pr-build - - approve-contributor-pr - - driver-integration-tests-webkit: - context: test-runner:cypress-record-key - requires: - - internal-pr-build - - external-pr-build - - approve-contributor-pr - - driver-integration-memory-tests: - requires: - - internal-pr-build - - external-pr-build - - approve-contributor-pr - - run-frontend-shared-component-tests-chrome: - context: - [ - test-runner:cypress-record-key, - test-runner:launchpad-tests, - test-runner:percy - ] - percy: true - requires: - - internal-pr-build - - external-pr-build - - approve-contributor-pr - - run-launchpad-integration-tests-chrome: - context: - [ - test-runner:cypress-record-key, - test-runner:launchpad-tests, - test-runner:percy - ] - percy: true - requires: - - internal-pr-build - - external-pr-build - - approve-contributor-pr - - run-launchpad-component-tests-chrome: - context: - [ - test-runner:cypress-record-key, - test-runner:launchpad-tests, - test-runner:percy - ] - percy: true - requires: - - internal-pr-build - - external-pr-build - - approve-contributor-pr - - run-app-integration-tests-chrome: - context: - [ - test-runner:cypress-record-key, - test-runner:launchpad-tests, - test-runner:percy - ] - percy: true - requires: - - internal-pr-build - - external-pr-build - - approve-contributor-pr - - run-webpack-dev-server-integration-tests: - context: [ test-runner:cypress-record-key, test-runner:percy ] - requires: - - system-tests-node-modules-install - - run-vite-dev-server-integration-tests: - context: [ test-runner:cypress-record-key, test-runner:percy ] + matrix: + parameters: + run-index: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] requires: - system-tests-node-modules-install - run-app-component-tests-chrome: @@ -227,201 +61,10 @@ jobs: test-runner:percy ] percy: true + matrix: + parameters: + run-index: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] requires: - internal-pr-build - external-pr-build - - approve-contributor-pr - - run-reporter-component-tests-chrome: - context: [ test-runner:cypress-record-key, test-runner:percy ] - percy: true - requires: - - internal-pr-build - - external-pr-build - - approve-contributor-pr - - reporter-integration-tests: - context: [ test-runner:cypress-record-key, test-runner:percy ] - requires: - - internal-pr-build - - external-pr-build - - approve-contributor-pr - - npm-webpack-dev-server: - requires: - - system-tests-node-modules-install - - npm-vite-dev-server: - requires: - - internal-pr-build - - external-pr-build - - approve-contributor-pr - - npm-vite-plugin-cypress-esm: - requires: - - internal-pr-build - - external-pr-build - - approve-contributor-pr - - npm-webpack-preprocessor: - requires: - - internal-pr-build - - external-pr-build - - approve-contributor-pr - - npm-webpack-batteries-included-preprocessor: - requires: - - internal-pr-build - - external-pr-build - - approve-contributor-pr - - npm-vue: - requires: - - internal-pr-build - - external-pr-build - - approve-contributor-pr - - npm-puppeteer-unit-tests: - requires: - - internal-pr-build - - external-pr-build - - approve-contributor-pr - - npm-puppeteer-cypress-tests: - requires: - - internal-pr-build - - external-pr-build - - approve-contributor-pr - - npm-react: - requires: - - internal-pr-build - - external-pr-build - - approve-contributor-pr - - npm-angular: - requires: - - internal-pr-build - - external-pr-build - - approve-contributor-pr - - npm-angular-zoneless: - requires: - - internal-pr-build - - external-pr-build - - approve-contributor-pr - - npm-mount-utils: - requires: - - internal-pr-build - - external-pr-build - - approve-contributor-pr - - npm-grep: - requires: - - internal-pr-build - - external-pr-build - - approve-contributor-pr - - npm-eslint-plugin-dev: - requires: - - internal-pr-build - - external-pr-build - - approve-contributor-pr - - npm-cypress-schematic: - requires: - - internal-pr-build - - external-pr-build - - approve-contributor-pr - - v8-integration-tests: - requires: - - system-tests-node-modules-install - - - create-and-trigger-packaging-artifacts: - context: - - test-runner:upload - - test-runner:build-binary - - publish-binary - requires: - - node_modules_install - - internal-pr-build - filters: - branches: - ignore: /^pull\/[0-9]+/ - - wait-for-binary-publish: - type: approval - requires: - - create-and-trigger-packaging-artifacts - - get-published-artifacts: - context: - - publish-binary - - test-runner:commit-status-checks - requires: - - wait-for-binary-publish - - test-kitchensink: - requires: - - internal-pr-build - - external-pr-build - - approve-contributor-pr - - test-npm-module-on-minimum-node-version: - context: publish-binary - requires: - - get-published-artifacts - - test-types-cypress-and-jest: - context: publish-binary - requires: - - get-published-artifacts - - test-full-typescript-project: - context: publish-binary - requires: - - get-published-artifacts - - test-binary-against-kitchensink: - context: publish-binary - requires: - - get-published-artifacts - - test-binary-as-specific-user: - name: "test binary as a non-root user" - executor: non-root-docker-user - context: publish-binary - requires: - - get-published-artifacts - - test-binary-as-specific-user: - name: "test binary as a root user" - context: publish-binary - requires: - - get-published-artifacts - - binary-system-tests: - context: publish-binary - requires: - - get-published-artifacts - - system-tests-node-modules-install - - yarn-pnp-preprocessor-system-test: - context: publish-binary - requires: - - get-published-artifacts - - system-tests-node-modules-install - - svelte-webpack-system-test: - context: publish-binary - requires: - - get-published-artifacts - - system-tests-node-modules-install - - # Finalization jobs - - percy-finalize: - context: test-runner:percy - required_env_var: PERCY_TOKEN - requires: - - cli-visual-tests - - reporter-integration-tests - - run-app-component-tests-chrome - - run-app-integration-tests-chrome - - run-frontend-shared-component-tests-chrome - - run-launchpad-component-tests-chrome - - run-launchpad-integration-tests-chrome - - run-reporter-component-tests-chrome - - run-webpack-dev-server-integration-tests - - run-vite-dev-server-integration-tests - # Cypress run must be completed to fetch Accessibility report - - verify-accessibility-results: - context: test-runner:cypress-record-key - requires: - - reporter-integration-tests - - run-app-component-tests-chrome - - run-app-integration-tests-chrome - - run-frontend-shared-component-tests-chrome - - run-launchpad-component-tests-chrome - - run-launchpad-integration-tests-chrome - - run-reporter-component-tests-chrome - - run-webpack-dev-server-integration-tests - - run-vite-dev-server-integration-tests - - driver-integration-tests-firefox - - driver-integration-tests-chrome - - driver-integration-tests-chrome-inject-document-domain - - driver-integration-tests-chrome-beta-inject-document-domain - - driver-integration-tests-electron - - driver-integration-tests-webkit - - driver-integration-memory-tests \ No newline at end of file + - approve-contributor-pr \ No newline at end of file diff --git a/npm/vite-dev-server/client/initCypressTests.js b/npm/vite-dev-server/client/initCypressTests.js index fe44e14e6b5..bd7efda2397 100644 --- a/npm/vite-dev-server/client/initCypressTests.js +++ b/npm/vite-dev-server/client/initCypressTests.js @@ -5,6 +5,33 @@ const CypressInstance = window.Cypress = parent.Cypress const importsToLoad = [] +// Retry dynamic import to tolerate Vite dev server not ready yet (e.g. in CI). +// "Failed to fetch dynamically imported module" can happen when the iframe loads +// before the server has finished serving the support or spec module. +function isRetryableImportError (err) { + const msg = err && err.message + + return typeof msg === 'string' && ( + msg.includes('Failed to fetch') || + msg.includes('dynamically imported module') || + msg.includes('Importing a module script failed') + ) +} + +function retryDynamicImport (url, maxAttempts = 4, delayMs = 400) { + const attempt = (n) => { + return import(url).catch((err) => { + if (n < maxAttempts && isRetryableImportError(err)) { + return new Promise((resolve) => setTimeout(resolve, delayMs)).then(() => attempt(n + 1)) + } + + throw err + }) + } + + return attempt(1) +} + /* Support file import logic, this should be removed once we * are able to return relative paths from the supportFile * Jira #UNIFY-1260 @@ -40,7 +67,7 @@ if (supportFile) { const relativeUrl = `${devServerPublicPathBase}${supportRelativeToProjectRoot}` importsToLoad.push({ - load: () => import(relativeUrl), + load: () => retryDynamicImport(relativeUrl), absolute: supportFile, relative: supportRelativeToProjectRoot, relativeUrl, @@ -62,7 +89,7 @@ if (specPath === '__all' || CypressInstance.spec.relative === '__all') { const specRoute = `${devServerPublicPathBase}/@fs/${normalizedPath}` importsToLoad.push({ - load: () => import(specRoute), + load: () => retryDynamicImport(specRoute), absolute: specObj.absolute, relative: specObj.relative, relativeUrl: specRoute, @@ -78,7 +105,7 @@ if (specPath === '__all' || CypressInstance.spec.relative === '__all') { // We need a slash before /src/my-spec.js, this does not happen by default. importsToLoad.push({ - load: () => import(testFileAbsolutePathRoute), + load: () => retryDynamicImport(testFileAbsolutePathRoute), absolute: CypressInstance.spec.absolute, relative: CypressInstance.spec.relative, relativeUrl: testFileAbsolutePathRoute, diff --git a/npm/vite-dev-server/src/devServer.ts b/npm/vite-dev-server/src/devServer.ts index f988b0d5986..642717d365b 100644 --- a/npm/vite-dev-server/src/devServer.ts +++ b/npm/vite-dev-server/src/devServer.ts @@ -3,6 +3,7 @@ import semverMajor from 'semver/functions/major.js' import type { UserConfig } from 'vite-7' import { getVite, Vite } from './getVite.js' import { createViteDevServerConfig } from './resolveConfig.js' +import { getSupportFileRelativePath, waitUntilUrlReady } from './waitForSupportFile.js' const debug = debugFn('cypress:vite-dev-server:devServer') @@ -38,7 +39,7 @@ export async function devServer (config: ViteDevServerConfig): Promise { + const { maxAttempts = DEFAULT_MAX_ATTEMPTS, delayMs = DEFAULT_DELAY_MS } = options + + let lastError: Error | undefined + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + const res = await fetch(url) + + if (res.ok) { + return + } + + lastError = new Error(`Support file URL returned ${res.status}`) + } catch (err) { + lastError = err instanceof Error ? err : new Error(String(err)) + } + + if (attempt < maxAttempts) { + await new Promise((resolve) => setTimeout(resolve, delayMs)) + } + } + + throw new Error( + `Vite dev server did not become ready in time (${maxAttempts} attempts). ${lastError?.message ?? ''}`, + ) +} diff --git a/npm/vite-dev-server/test/waitForSupportFile.spec.ts b/npm/vite-dev-server/test/waitForSupportFile.spec.ts new file mode 100644 index 00000000000..90bdfc59afc --- /dev/null +++ b/npm/vite-dev-server/test/waitForSupportFile.spec.ts @@ -0,0 +1,95 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { getSupportFileRelativePath, waitUntilUrlReady } from '../src/waitForSupportFile.js' + +describe('waitForSupportFile', () => { + describe('getSupportFileRelativePath', () => { + it('builds path matching client logic when devServerPublicPathRoute is set', () => { + const cypressConfig = { + projectRoot: '/users/proj', + supportFile: '/users/proj/cypress/support/component.ts', + devServerPublicPathRoute: '/__cypress/src', + platform: 'darwin', + } as Cypress.PluginConfigOptions + + expect(getSupportFileRelativePath(cypressConfig)).toBe('/__cypress/src/cypress/support/component.ts') + }) + + it('returns empty string when supportFile is not set', () => { + const cypressConfig = { + projectRoot: '/users/proj', + supportFile: undefined, + devServerPublicPathRoute: '/__cypress/src', + platform: 'darwin', + } as Cypress.PluginConfigOptions + + expect(getSupportFileRelativePath(cypressConfig)).toBe('') + }) + + it('handles win32 paths with backslashes', () => { + const cypressConfig = { + projectRoot: 'C:\\users\\proj', + supportFile: 'C:\\users\\proj\\cypress\\support\\component.ts', + devServerPublicPathRoute: '/__cypress/src', + platform: 'win32', + } as Cypress.PluginConfigOptions + + expect(getSupportFileRelativePath(cypressConfig)).toBe('/__cypress/src/cypress/support/component.ts') + }) + + it('uses relative path when devServerPublicPathRoute is empty', () => { + const cypressConfig = { + projectRoot: '/users/proj', + supportFile: '/users/proj/cypress/support/component.ts', + devServerPublicPathRoute: '', + platform: 'darwin', + } as Cypress.PluginConfigOptions + + expect(getSupportFileRelativePath(cypressConfig)).toBe('./cypress/support/component.ts') + }) + }) + + describe('waitUntilUrlReady', () => { + beforeEach(() => { + vi.stubGlobal('fetch', vi.fn()) + }) + + afterEach(() => { + vi.unstubAllGlobals() + }) + + it('resolves when URL returns 200', async () => { + const fetchMock = vi.mocked(fetch) + + fetchMock.mockResolvedValue({ ok: true } as Response) + + await expect(waitUntilUrlReady('http://127.0.0.1:5173/__cypress/src/cypress/support/component.ts')).resolves.toBeUndefined() + expect(fetchMock).toHaveBeenCalledTimes(1) + }) + + it('retries until 200 then resolves', async () => { + const fetchMock = vi.mocked(fetch) + + fetchMock + .mockResolvedValueOnce({ ok: false, status: 503 } as Response) + .mockResolvedValueOnce({ ok: true } as Response) + + await expect( + waitUntilUrlReady('http://127.0.0.1:5173/ready', { maxAttempts: 3, delayMs: 1 }), + ).resolves.toBeUndefined() + + expect(fetchMock).toHaveBeenCalledTimes(2) + }) + + it('throws after maxAttempts if URL never returns 2xx', async () => { + const fetchMock = vi.mocked(fetch) + + fetchMock.mockResolvedValue({ ok: false, status: 404 } as Response) + + await expect( + waitUntilUrlReady('http://127.0.0.1:5173/missing', { maxAttempts: 2, delayMs: 1 }), + ).rejects.toThrow(/did not become ready in time/) + + expect(fetchMock).toHaveBeenCalledTimes(2) + }) + }) +})