From 538bafe3ab46280c6c08cc3a19c57b9043d08d0f Mon Sep 17 00:00:00 2001 From: Matthew Schile Date: Mon, 16 Mar 2026 12:46:05 -0600 Subject: [PATCH 1/3] wait for the support file to be ready --- .../client/initCypressTests.js | 33 ++++++- npm/vite-dev-server/src/devServer.ts | 14 ++- npm/vite-dev-server/src/waitForSupportFile.ts | 72 ++++++++++++++ .../test/waitForSupportFile.spec.ts | 95 +++++++++++++++++++ 4 files changed, 210 insertions(+), 4 deletions(-) create mode 100644 npm/vite-dev-server/src/waitForSupportFile.ts create mode 100644 npm/vite-dev-server/test/waitForSupportFile.spec.ts 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) + }) + }) +}) From ba9ae13a59986af74c4cfce7af9d7ef61074df60 Mon Sep 17 00:00:00 2001 From: Matthew Schile Date: Tue, 17 Mar 2026 17:59:07 -0600 Subject: [PATCH 2/3] run system-tests 10 times --- .circleci/src/pipeline/@pipeline.yml | 5 + .../src/pipeline/workflows/pull-request.yml | 386 +----------------- npm/vite-dev-server/src/waitForSupportFile.ts | 2 +- 3 files changed, 13 insertions(+), 380 deletions(-) diff --git a/.circleci/src/pipeline/@pipeline.yml b/.circleci/src/pipeline/@pipeline.yml index 29d1b648191..9faf2c9cdfe 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 diff --git a/.circleci/src/pipeline/workflows/pull-request.yml b/.circleci/src/pipeline/workflows/pull-request.yml index 8f7c6a652ee..c1e0b30181e 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 only system-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,331 +48,8 @@ jobs: - approve-contributor-pr - system-tests-chrome: context: test-runner:performance-tracking + matrix: + parameters: + run-index: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 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 ] - requires: - - system-tests-node-modules-install - - run-app-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-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 + - system-tests-node-modules-install \ No newline at end of file diff --git a/npm/vite-dev-server/src/waitForSupportFile.ts b/npm/vite-dev-server/src/waitForSupportFile.ts index 57a2872657c..12291357c1a 100644 --- a/npm/vite-dev-server/src/waitForSupportFile.ts +++ b/npm/vite-dev-server/src/waitForSupportFile.ts @@ -7,7 +7,7 @@ const DEFAULT_MAX_ATTEMPTS = 30 const DEFAULT_DELAY_MS = 200 -export interface WaitUntilUrlReadyOptions { +interface WaitUntilUrlReadyOptions { maxAttempts?: number delayMs?: number } From 4efbba5f0ab023ba8e75ff6c46e1cdc01b2b342e Mon Sep 17 00:00:00 2001 From: Matthew Schile Date: Tue, 17 Mar 2026 19:40:19 -0600 Subject: [PATCH 3/3] updates --- .circleci/src/pipeline/@pipeline.yml | 3 +++ .../src/pipeline/workflows/pull-request.yml | 19 +++++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/.circleci/src/pipeline/@pipeline.yml b/.circleci/src/pipeline/@pipeline.yml index 9faf2c9cdfe..7cdf2548014 100644 --- a/.circleci/src/pipeline/@pipeline.yml +++ b/.circleci/src/pipeline/@pipeline.yml @@ -2072,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 c1e0b30181e..ef62b0e37aa 100644 --- a/.circleci/src/pipeline/workflows/pull-request.yml +++ b/.circleci/src/pipeline/workflows/pull-request.yml @@ -2,7 +2,7 @@ # 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 only system-tests-chrome (x10) + deps for CI testing. +# 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: @@ -52,4 +52,19 @@ jobs: parameters: run-index: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] requires: - - system-tests-node-modules-install \ No newline at end of file + - system-tests-node-modules-install + - run-app-component-tests-chrome: + context: + [ + test-runner:cypress-record-key, + test-runner:launchpad-tests, + 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 \ No newline at end of file