diff --git a/.github/actions/setup-playwright/action.yml b/.github/actions/setup-playwright/action.yml index 7f87b1855..b55276555 100644 --- a/.github/actions/setup-playwright/action.yml +++ b/.github/actions/setup-playwright/action.yml @@ -1,6 +1,12 @@ name: Setup Playwright description: Setup and cache Playwright browser binaries +inputs: + browsers: + required: false + default: chromium + description: 'Playwright browsers to install: "chromium" (headless shell only) or "all"' + runs: using: composite steps: @@ -29,19 +35,26 @@ runs: id: playwright-cache with: path: ${{ env.PLAYWRIGHT_BROWSERS_PATH }} - key: ${{ runner.os }}-playwright-${{ steps.resolve-package-versions.outputs.PLAYWRIGHT_VERSION }} + key: ${{ runner.os }}-playwright-${{ inputs.browsers }}-${{ steps.resolve-package-versions.outputs.PLAYWRIGHT_VERSION }} restore-keys: | - ${{ runner.os }}-playwright- + ${{ runner.os }}-playwright-${{ inputs.browsers }}- + + # webkit/firefox link against apt system libraries (e.g. webkit's libwoff2dec) + # that are NOT stored in the browser cache, so install them on Linux whenever + # the full set is requested, even on a cache hit. chromium needs none, and + # Windows/macOS need no system deps (--with-deps on Windows is a slow ~3min + # Media Foundation DISM). + - name: Install Playwright system deps (Linux) + shell: bash + if: runner.os == 'Linux' && inputs.browsers == 'all' + run: pnpm exec playwright install-deps - # Only Linux needs --with-deps: it apt-installs system libraries the browser - # links against. On Windows --with-deps triggers a slow Media Foundation DISM - # install (~3min) that the tests don't need, and on macOS it is a no-op. - - name: Install Playwright browser with system deps (Linux) + - name: Install Playwright chromium (cache miss) shell: bash - if: steps.playwright-cache.outputs.cache-hit != 'true' && runner.os == 'Linux' - run: pnpm exec playwright install --with-deps --only-shell + if: steps.playwright-cache.outputs.cache-hit != 'true' && inputs.browsers != 'all' + run: pnpm exec playwright install chromium --only-shell - - name: Install Playwright browser (macOS/Windows) + - name: Install Playwright browsers (cache miss) shell: bash - if: steps.playwright-cache.outputs.cache-hit != 'true' && runner.os != 'Linux' + if: steps.playwright-cache.outputs.cache-hit != 'true' && inputs.browsers == 'all' run: pnpm exec playwright install --only-shell diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f2861c61e..074717b54 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -90,20 +90,38 @@ jobs: test: needs: changed - name: 'Build&Test: node-${{ matrix.node_version }}, ${{ matrix.os }}' + name: 'Test: ${{ matrix.suite }}, node-${{ matrix.node_version }}, ${{ matrix.os }}' if: needs.changed.outputs.should_skip != 'true' runs-on: ${{ matrix.os }} timeout-minutes: 30 strategy: + # Split the heavy e2e and coverage suites onto their own runners so they run + # in parallel with the unit suite instead of sequentially on a single machine. + # Linux runs the full matrix on node 24/26; node 22 (the minimum supported + # version, currently in maintenance LTS) gets a single e2e leg as a floor + # smoke. macOS runs one e2e leg as an Apple-hardware smoke; unit/coverage are + # redundant on both. matrix: + suite: [unit, e2e, coverage] os: [ubuntu-latest] - node_version: [22, 24, 26] + node_version: [24, 26] include: - - os: macos-latest + - suite: e2e + os: ubuntu-latest + node_version: 22 + - suite: e2e + os: macos-latest node_version: 24 - - os: windows-latest + - suite: unit + os: windows-latest + node_version: 24 + - suite: e2e + os: windows-latest + node_version: 24 + - suite: coverage + os: windows-latest node_version: 24 fail-fast: false @@ -120,35 +138,31 @@ jobs: run: pnpm i --filter '!docs' - uses: ./.github/actions/setup-playwright + with: + browsers: chromium - name: Build run: pnpm run build - name: Test - run: pnpm run test:ci + # matrix.suite is a fixed, workflow-defined value (unit/e2e/coverage) + run: pnpm run test:ci:${{ matrix.suite }} # zizmor: ignore[template-injection] env: VITEST_CI_BLOB_LABEL: ${{ matrix.os }}-node-${{ matrix.node_version }} - name: Test Examples - if: matrix.os == 'ubuntu-latest' + if: matrix.suite == 'unit' && matrix.os == 'ubuntu-latest' run: pnpm run test:examples - name: Unit Test UI + if: matrix.suite == 'unit' run: pnpm -C packages/ui test:ui env: VITEST_CI_BLOB_LABEL: ${{ matrix.os }}-node-${{ matrix.node_version }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - if: ${{ !cancelled() }} - with: - name: playwright-report-${{ matrix.os }}-node-${{ matrix.node_version }} - path: test/ui/test-results/ - retention-days: 30 - - - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - if: ${{ !cancelled() }} with: - name: vitest-results-${{ matrix.os }}-node-${{ matrix.node_version }} + name: vitest-results-${{ matrix.suite }}-${{ matrix.os }}-node-${{ matrix.node_version }} path: | README.md test/unit/.vitest @@ -159,16 +173,32 @@ jobs: test-browser: needs: changed - name: 'Browsers: node-${{ matrix.node_version }}, ${{ matrix.os }}' + name: 'Browsers: ${{ matrix.name }}, node-24, ${{ matrix.os }}' if: needs.changed.outputs.should_skip != 'true' runs-on: ${{ matrix.os }} strategy: + # runner.test.ts is the heaviest spec (~28% of the suite, ~70% of whichever + # shard it lands in). Windows isolates it in its own job and shards the other + # 32 specs; Ubuntu shards the whole suite 2 ways (runner rides in one shard). + # Every spec, runner included, runs on both OSes. matrix: - node_version: [24] - os: - - macos-latest - - windows-latest + include: + - os: windows-latest + name: runner + command: pnpm run test:browser:playwright runner.test.ts + - os: windows-latest + name: rest 1/2 + command: "pnpm run test:browser:playwright --exclude 'specs/runner.test.ts' --shard=1/2" + - os: windows-latest + name: rest 2/2 + command: "pnpm run test:browser:playwright --exclude 'specs/runner.test.ts' --shard=2/2" + - os: ubuntu-latest + name: shard 1/2 + command: pnpm run test:browser:playwright --shard=1/2 + - os: ubuntu-latest + name: shard 2/2 + command: pnpm run test:browser:playwright --shard=2/2 fail-fast: false timeout-minutes: 30 @@ -180,33 +210,37 @@ jobs: - uses: ./.github/actions/setup-and-cache with: - node-version: ${{ matrix.node_version }} + node-version: 24 - name: Install run: pnpm i --filter '!docs' - uses: ./.github/actions/setup-playwright + with: + browsers: all - name: Build run: pnpm run build - name: Test Browser (playwright) - run: pnpm run test:browser:playwright + # matrix.command is a fixed, workflow-defined string + run: ${{ matrix.command }} # zizmor: ignore[template-injection] test-vite7: needs: changed - # macos-latest is the fastest one - name: 'Test: vite@7, ${{ matrix.suite }}, node-24, macos-latest' + # Runs on ubuntu to keep macOS under its ~5 concurrent-runner limit. + name: 'Test: vite@7, ${{ matrix.suite }}, node-24, ubuntu-latest' if: needs.changed.outputs.should_skip != 'true' - runs-on: macos-latest + runs-on: ubuntu-latest timeout-minutes: 30 strategy: - # split the node suite and the browser suite onto separate runners so they - # run in parallel instead of sequentially on a single machine + # vite7 is a compatibility smoke, not a coverage gate. Run the e2e suite + # (which exercises real config resolution, transform and runs) and the + # browser suite on separate runners in parallel. matrix: - suite: [node, browser] + suite: [e2e, browser] fail-fast: false steps: @@ -221,35 +255,25 @@ jobs: - name: Install run: | pnpm override-vite7 - git add . && git commit -m "ci" && pnpm i --prefer-offline --no-frozen-lockfile --filter '!docs' + # ubuntu runners have no default git identity; set one for this commit + git add . && git -c user.email=ci@vitest.dev -c user.name=vitest-ci commit -m "ci" && pnpm i --prefer-offline --no-frozen-lockfile --filter '!docs' - uses: ./.github/actions/setup-playwright + with: + browsers: all - name: Build run: pnpm run build - - name: Test - if: ${{ matrix.suite == 'node' }} - run: pnpm run test:ci:no-bail - - - name: Test Examples - if: ${{ matrix.suite == 'node' && !cancelled() }} - run: pnpm run test:examples - - - name: Unit Test UI - if: ${{ matrix.suite == 'node' && !cancelled() }} - run: pnpm -C packages/ui test:ui - - - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - if: ${{ matrix.suite == 'node' && !cancelled() }} - with: - name: playwright-report-vite7 - path: test/ui/test-results/ - retention-days: 30 + - name: Test E2E + if: ${{ matrix.suite == 'e2e' }} + run: pnpm run test:ci:e2e - name: Test Browser (playwright) if: ${{ matrix.suite == 'browser' && !cancelled() }} - run: pnpm run test:browser:playwright + # smoke: runner.test.ts exercises the core browser-runner behaviour across + # every instance, enough to catch vite7 browser-mode regressions + run: pnpm run test:browser:playwright runner.test.ts merge-reports: needs: diff --git a/package.json b/package.json index cc089ab5c..2f2d60e08 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,9 @@ "test": "pnpm --filter test-unit test:threads", "test:ci": "CI=true pnpm -r --reporter-hide-prefix --stream --sequential --filter '@vitest/test-*' --filter !test-browser run test", "test:ci:no-bail": "CI=true pnpm -r --no-bail --reporter-hide-prefix --stream --sequential --filter '@vitest/test-*' --filter !test-browser run test", + "test:ci:e2e": "CI=true pnpm --filter @vitest/test-e2e run test", + "test:ci:coverage": "CI=true pnpm --filter @vitest/test-coverage run test", + "test:ci:unit": "CI=true pnpm -r --reporter-hide-prefix --stream --filter '@vitest/test-*' --filter !test-browser --filter !test-e2e --filter !test-coverage --filter !test-ui --filter !test-workspaces-browser run test && CI=true pnpm -r --reporter-hide-prefix --stream --sequential --filter test-ui --filter test-workspaces-browser run test", "test:examples": "CI=true pnpm -r --reporter-hide-prefix --stream --filter '@vitest/example-*' run test", "test:ecosystem-ci": "ECOSYSTEM_CI=true pnpm test:ci", "typebuild": "tsx ./scripts/explain-types.ts", diff --git a/test/browser/vitest.config.unit.mts b/test/browser/vitest.config.unit.mts index 0741869f0..21dbabc16 100644 --- a/test/browser/vitest.config.unit.mts +++ b/test/browser/vitest.config.unit.mts @@ -1,5 +1,6 @@ import type { TestSpecification } from 'vitest/node' import { defineConfig } from 'vitest/config' +import { BaseSequencer } from 'vitest/node' export default defineConfig({ test: { @@ -12,17 +13,16 @@ export default defineConfig({ hookTimeout: process.env.CI ? 120_000 * 3 : 20_000, testTimeout: process.env.CI ? 120_000 * 3 : 20_000, sequence: { - sequencer: class Sequencer { - sort(specifications: TestSpecification[]) { + // Extend BaseSequencer so `--shard` works (its deterministic hash split), + // keeping a stable name-based sort. Sharding splits the specs across CI + // runners while each spec still runs across every browser instance. + sequencer: class Sequencer extends BaseSequencer { + async sort(specifications: TestSpecification[]) { return specifications.sort((spec1, spec2) => { // just sort by name, ignore the cache optimization return spec1.moduleId.localeCompare(spec2.moduleId) }) } - - shard(): TestSpecification[] { - throw new Error('not supported') - } }, }, }, diff --git a/test/e2e/fixtures/console-color/vitest.config.ts b/test/e2e/fixtures/console-color/vitest.config.ts index abed6b211..b18292bd2 100644 --- a/test/e2e/fixtures/console-color/vitest.config.ts +++ b/test/e2e/fixtures/console-color/vitest.config.ts @@ -1,3 +1,9 @@ import { defineConfig } from 'vitest/config' -export default defineConfig({}) +// These tests assert the console-color behaviour of the default (forks) pool. +// The test-utils harness defaults spawned runs to threads, so pin forks here. +export default defineConfig({ + test: { + pool: 'forks', + }, +}) diff --git a/test/e2e/test/detect-async-leaks.test.ts b/test/e2e/test/detect-async-leaks.test.ts index 934ce486e..09b25ee27 100644 --- a/test/e2e/test/detect-async-leaks.test.ts +++ b/test/e2e/test/detect-async-leaks.test.ts @@ -335,7 +335,7 @@ test('leak in project setup', async () => { }) async function runInlineTests(...params: Parameters) { - const result = await base(params[0], { globals: true, detectAsyncLeaks: true, ...params[1] }, params[2]) + const result = await base(params[0], { globals: true, detectAsyncLeaks: true, pool: 'forks', ...params[1] }, params[2]) return { ...result, stderr: trimWhitespace(result.stderr) } } diff --git a/test/e2e/test/failures.test.ts b/test/e2e/test/failures.test.ts index 3bdd1af1a..8e93ef695 100644 --- a/test/e2e/test/failures.test.ts +++ b/test/e2e/test/failures.test.ts @@ -353,6 +353,7 @@ test('boolean flag 100 should not crash CLI', async () => { test('nextTick cannot be mocked inside child_process', async () => { const { stderr } = await runVitest({ + pool: 'forks', fakeTimers: { toFake: ['nextTick'] }, include: ['./fake-timers.test.ts'], }) diff --git a/test/e2e/vitest.config.ts b/test/e2e/vitest.config.ts index 2e3a9f89a..6b659ce94 100644 --- a/test/e2e/vitest.config.ts +++ b/test/e2e/vitest.config.ts @@ -41,7 +41,7 @@ export default defineConfig({ testTimeout: 60_000, isolate: false, fileParallelism: true, - maxWorkers: 2, + maxWorkers: Number(process.env.VITEST_E2E_MAX_WORKERS) || 2, // TODO: should enabled when support for older node is dropped? // experimental: { // viteModuleRunner: false, diff --git a/test/test-utils/index.ts b/test/test-utils/index.ts index 431ceb074..7266758fa 100644 --- a/test/test-utils/index.ts +++ b/test/test-utils/index.ts @@ -184,6 +184,23 @@ export async function runVitest( }, }, { ...viteConfig, + plugins: [ + ...(viteConfig.plugins ?? []), + // Spawning the worker is the dominant cost of these meta-tests (each `runVitest` + // boots a fresh Vitest). Default the spawned run to `threads`, which is cheaper + // to start than `forks`, especially on Windows. A `config` hook only fills the + // gap when nothing else set a pool, so an explicit `pool` (from `config` or the + // fixture's own config file) always wins. Browser runs are left alone since they + // don't execute in a node pool. + { + name: 'vitest:test-utils:default-pool', + config(config) { + if (config.test?.pool == null && !config.test?.browser?.enabled) { + return { test: { pool: 'threads' } } + } + }, + }, + ], server: { // we never need a websocket connection for the root config because it doesn't connect to the browser // browser mode uses a separate config that doesn't inherit CLI overrides