chore: upgrade to vitest v4 (#18265)

## Summary

Bumps `vitest` and `@vitest/coverage-v8` from `^2.1.9` to `^4.1.7` (two
major versions). Three small test-harness updates compensate for vitest
4 behavior changes; all 7569 tests still pass.

- **`packages/svelte/tests/runtime-browser/test.ts`** — vitest 4 removed
the deprecated `describe(name, fn, opts)` signature. Pass options as the
second argument.
- **`packages/svelte/tests/runtime-legacy/shared.ts`** — vitest 4's
jsdom env binds `virtualConsole` to the original `globalThis.console`
reference *before* vitest wraps the console, so inline-`<script>` logs
and `jsdomError` events no longer reach per-test `console.{log,error}`
overrides. Restore vitest 2's behavior by re-routing the virtual console
through the live `console` in `beforeAll`. Also promote the window-error
listener to a named function and remove it in `finally` — previously
leaked listeners from earlier tests kept writing to module-level
`unhandled_rejection`, polluting later tests.
- **`vitest.config.js`** — bump `testTimeout` to 10s. The 5s default
trips a handful of dev-mode tests that exercise
`effect_update_depth_exceeded`, whose ~1000 Error-stack captures per
flush are slower under vitest 4's deeper async stacks.

## Test plan

- [x] `pnpm test` — 7569 passed, 63 skipped, 0 failed

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Rich Harris <rich.harris@vercel.com>
rolldown
Mathias Picker 3 days ago committed by GitHub
parent a81f96549d
commit 85dcb91f7c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -18,12 +18,12 @@ jobs:
strategy:
matrix:
include:
- node-version: 18
# Vitest 4 requires Node 20+, so tests run on 20/22/24. The published
# Svelte package still supports Node >=18 (see packages/svelte/package.json).
- node-version: 20
os: windows-latest
- node-version: 18
- node-version: 20
os: macOS-latest
- node-version: 18
os: ubuntu-latest
- node-version: 20
os: ubuntu-latest
- node-version: 22

@ -32,7 +32,7 @@
"@svitejs/changesets-changelog-github-compact": "^1.1.0",
"@types/node": "^20.11.5",
"@types/picomatch": "^4.0.2",
"@vitest/coverage-v8": "^2.1.9",
"@vitest/coverage-v8": "^4.1.7",
"eslint": "^10.0.0",
"eslint-plugin-lube": "^0.5.1",
"eslint-plugin-svelte": "^3.15.0",
@ -44,6 +44,6 @@
"typescript": "^5.5.4",
"typescript-eslint": "^8.56.0",
"v8-natives": "^1.2.5",
"vitest": "^2.1.9"
"vitest": "^4.1.7"
}
}

@ -166,7 +166,7 @@
"source-map": "^0.7.4",
"tinyglobby": "^0.2.12",
"typescript": "^5.5.4",
"vitest": "^2.1.9",
"vitest": "^4.1.7",
"web-features": "^3.29.0"
},
"dependencies": {

@ -25,7 +25,21 @@ interface HydrationTest extends BaseTest {
expect_hydration_error?: true;
snapshot?: (target: HTMLElement) => any;
test?: (
assert: typeof import('vitest').assert & {
// `_config.js` test callbacks rely on inferred parameter types, which
// TS treats as non-explicit and rejects for chai 5's assertion-function
// signatures (TS2775). Override the assertion methods we actually use
// with non-assertion equivalents.
assert: Omit<
typeof import('vitest').assert,
'ok' | 'isOk' | 'isTrue' | 'isFalse' | 'exists' | 'notExists' | 'instanceOf'
> & {
ok(value: unknown, message?: string): void;
isOk(value: unknown, message?: string): void;
isTrue(value: unknown, message?: string): void;
isFalse(value: unknown, message?: string): void;
exists(value: unknown, message?: string): void;
notExists(value: unknown, message?: string): void;
instanceOf(value: unknown, type: Function, message?: string): void;
htmlEqual(a: string, b: string, description?: string): void;
},
target: HTMLElement,
@ -152,7 +166,6 @@ const { test, run } = suite<HydrationTest>(async (config, cwd) => {
if (config.test) {
await config.test(
// @ts-expect-error TS doesn't get it
{
...assert,
htmlEqual: assert_html_equal

@ -42,9 +42,9 @@ const { run: run_browser_tests } = suite_with_variants<
describe.concurrent(
'runtime-browser',
() => run_browser_tests(__dirname),
// Browser tests are brittle and slow on CI
{ timeout: 20000, retry: process.env.CI ? 1 : 0 }
{ timeout: 20000, retry: process.env.CI ? 1 : 0 },
() => run_browser_tests(__dirname)
);
const { run: run_ce_tests } = suite<ReturnType<typeof import('./assert').test>>(
@ -55,9 +55,9 @@ const { run: run_ce_tests } = suite<ReturnType<typeof import('./assert').test>>(
describe.concurrent(
'custom-elements',
() => run_ce_tests(__dirname, 'custom-elements-samples'),
// Browser tests are brittle and slow on CI
{ timeout: 20000, retry: process.env.CI ? 1 : 0 }
{ timeout: 20000, retry: process.env.CI ? 1 : 0 },
() => run_ce_tests(__dirname, 'custom-elements-samples')
);
async function run_test(

@ -15,19 +15,35 @@ import { clear } from '../../src/internal/client/reactivity/batch.js';
import { hydrating } from '../../src/internal/client/dom/hydration.js';
import { ssr_context } from '../../src/internal/server/context.js';
type Assert = typeof import('vitest').assert & {
htmlEqual(a: string, b: string, description?: string): void;
htmlEqualWithOptions(
a: string,
b: string,
opts: {
preserveComments: boolean;
withoutNormalizeHtml: boolean;
},
description?: string
): void;
// `_config.js` files call `assert.ok` etc. with `assert` typed via parameter
// inference, which TypeScript treats as non-explicit. chai 5 (pulled in by
// vitest 4) declares these as assertion functions (`asserts value`), so TS2775
// fires on every call. Override the affected methods with non-assertion
// signatures — the runtime behavior is unchanged.
type NonAssertingMethods = {
ok(value: unknown, message?: string): void;
isOk(value: unknown, message?: string): void;
isTrue(value: unknown, message?: string): void;
isFalse(value: unknown, message?: string): void;
exists(value: unknown, message?: string): void;
notExists(value: unknown, message?: string): void;
instanceOf(value: unknown, type: Function, message?: string): void;
};
type Assert = Omit<typeof import('vitest').assert, keyof NonAssertingMethods> &
NonAssertingMethods & {
htmlEqual(a: string, b: string, description?: string): void;
htmlEqualWithOptions(
a: string,
b: string,
opts: {
preserveComments: boolean;
withoutNormalizeHtml: boolean;
},
description?: string
): void;
};
// TODO remove this shim when we can
// @ts-expect-error
Promise.withResolvers = () => {
@ -75,6 +91,7 @@ export interface RuntimeTest<Props extends Record<string, any> = Record<string,
raf: {
tick: (ms: number) => void;
};
snapshot: any;
target: HTMLElement;
window: Window & {
Event: typeof Event;
@ -126,6 +143,16 @@ const listeners = process.rawListeners('unhandledRejection');
beforeAll(() => {
// @ts-expect-error TODO huh?
process.prependListener('unhandledRejection', unhandled_rejection_handler);
// Route inline-`<script>` console calls through `globalThis.console` at
// call time, so per-test `console.{log,warn,error}` overrides see them.
// (jsdom scripts run in a VM with their own `console` object, separate
// from Node's `globalThis.console` — vitest 4's jsdom env no longer
// bridges them per-test.)
const vc = (window as any)._virtualConsole;
for (const m of ['log', 'warn', 'error'] as const) {
vc?.on(m, (...args: any[]) => console[m](...args));
}
});
beforeEach(() => {
@ -255,6 +282,26 @@ async function run_test_variant(
let warnings: string[] = [];
let errors: string[] = [];
let manual_hydrate = false;
let intercept_errors = false;
// Capture phase so we still see the error if a test installs its own
// `stopImmediatePropagation` listener (e.g. event-handler-*). Named so we
// can remove it in `finally` — otherwise listeners from earlier tests leak
// and keep writing to module-level `unhandled_rejection`. When the test
// intercepts `errors`, mirror jsdom's `Uncaught [...]` format into the
// array — vitest 4's jsdom env no longer routes `jsdomError` to vitest's
// wrapped console reliably (works locally but not in CI's forks pool).
const window_error_listener = (e: ErrorEvent) => {
if (intercept_errors) {
const detail = e.error;
const is_error = detail && detail.name && detail.message !== undefined && detail.stack;
const error_string = is_error ? `[${detail.name}: ${detail.message}]` : String(detail);
errors.push(`Error: Uncaught ${error_string}\n${detail?.stack ?? ''}`, detail);
} else {
unhandled_rejection = e.error;
}
e.preventDefault();
};
{
// use some crude static analysis to determine if logs/warnings are intercepted.
@ -317,6 +364,7 @@ async function run_test_variant(
}
if (str.slice(0, i).includes('errors') || config.errors) {
intercept_errors = true;
// eslint-disable-next-line no-console
console.error = (...args) => {
errors.push(...args);
@ -348,10 +396,7 @@ async function run_test_variant(
window.document.head.innerHTML = styles ? `<style>${styles}</style>` : '';
window.document.body.innerHTML = '<main></main>';
window.addEventListener('error', (e) => {
unhandled_rejection = e.error;
e.preventDefault();
});
window.addEventListener('error', window_error_listener, true);
globalThis.requestAnimationFrame = globalThis.setTimeout;
@ -423,7 +468,6 @@ async function run_test_variant(
await config.test_ssr({
logs,
warnings,
// @ts-expect-error
assert: {
...assert,
htmlEqual: assert_html_equal,
@ -515,7 +559,6 @@ async function run_test_variant(
}
await config.test({
// @ts-expect-error TS doesn't get it
assert: {
...assert,
htmlEqual: assert_html_equal,
@ -525,6 +568,7 @@ async function run_test_variant(
component: runes ? props : instance,
instance,
mod,
ok,
target,
snapshot,
window,
@ -593,6 +637,8 @@ async function run_test_variant(
throw new Error('Hydration state was not cleared');
}
window.removeEventListener('error', window_error_listener, true);
config.after_test?.();
// Free up the microtask queue

@ -31,7 +31,21 @@ interface SourcemapTest extends BaseTest {
/** The expected `sources` array in the source map */
css_map_sources?: string[];
test?: (obj: {
assert: typeof assert;
// chai 5's `asserts value` signatures trip TS2775 in `_config.js` files
// where `assert` is a destructured parameter (non-explicit). Override
// the assertion methods we use with non-assertion equivalents.
assert: Omit<
typeof assert,
'ok' | 'isOk' | 'isTrue' | 'isFalse' | 'exists' | 'notExists' | 'instanceOf'
> & {
ok(value: unknown, message?: string): void;
isOk(value: unknown, message?: string): void;
isTrue(value: unknown, message?: string): void;
isFalse(value: unknown, message?: string): void;
exists(value: unknown, message?: string): void;
notExists(value: unknown, message?: string): void;
instanceOf(value: unknown, type: Function, message?: string): void;
};
input: string;
map_preprocessed: any;
code_preprocessed: string;

File diff suppressed because it is too large Load Diff

@ -1,4 +1,4 @@
import { type Environment, builtinEnvironments } from 'vitest/environments';
import { type Environment, builtinEnvironments } from 'vitest/runtime';
const xhtml_page = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.0//EN" "http://www.w3.org/TR/xhtml-basic/xhtml-basic10.dtd">
@ -6,7 +6,7 @@ const xhtml_page = `<?xml version="1.0" encoding="UTF-8"?>
export default <Environment>{
name: 'jsdom-xhtml',
transformMode: 'web',
viteEnvironment: 'client',
setup(global, { jsdom = {} }) {
return builtinEnvironments.jsdom.setup(global, {
jsdom: {

@ -29,6 +29,11 @@ export default defineConfig({
test: {
dir: '.',
reporters: ['dot'],
// A handful of dev-mode tests trigger Svelte's `effect_update_depth_exceeded`
// guard, which involves ~1000 Error objects per flush for stack tracking —
// slow enough under vitest 4's deeper async stacks (and CI's slower workers)
// to overrun the 5s default.
testTimeout: 30_000,
include: [
'packages/svelte/**/*.test.ts',
'packages/svelte/tests/*/test.ts',

Loading…
Cancel
Save