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 4 months 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: strategy:
matrix: matrix:
include: 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 os: windows-latest
- node-version: 18 - node-version: 20
os: macOS-latest os: macOS-latest
- node-version: 18
os: ubuntu-latest
- node-version: 20 - node-version: 20
os: ubuntu-latest os: ubuntu-latest
- node-version: 22 - node-version: 22

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

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

@ -25,7 +25,21 @@ interface HydrationTest extends BaseTest {
expect_hydration_error?: true; expect_hydration_error?: true;
snapshot?: (target: HTMLElement) => any; snapshot?: (target: HTMLElement) => any;
test?: ( 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; htmlEqual(a: string, b: string, description?: string): void;
}, },
target: HTMLElement, target: HTMLElement,
@ -152,7 +166,6 @@ const { test, run } = suite<HydrationTest>(async (config, cwd) => {
if (config.test) { if (config.test) {
await config.test( await config.test(
// @ts-expect-error TS doesn't get it
{ {
...assert, ...assert,
htmlEqual: assert_html_equal htmlEqual: assert_html_equal

@ -42,9 +42,9 @@ const { run: run_browser_tests } = suite_with_variants<
describe.concurrent( describe.concurrent(
'runtime-browser', 'runtime-browser',
() => run_browser_tests(__dirname),
// Browser tests are brittle and slow on CI // 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>>( 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( describe.concurrent(
'custom-elements', 'custom-elements',
() => run_ce_tests(__dirname, 'custom-elements-samples'),
// Browser tests are brittle and slow on CI // 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( 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 { hydrating } from '../../src/internal/client/dom/hydration.js';
import { ssr_context } from '../../src/internal/server/context.js'; import { ssr_context } from '../../src/internal/server/context.js';
type Assert = typeof import('vitest').assert & { // `_config.js` files call `assert.ok` etc. with `assert` typed via parameter
htmlEqual(a: string, b: string, description?: string): void; // inference, which TypeScript treats as non-explicit. chai 5 (pulled in by
htmlEqualWithOptions( // vitest 4) declares these as assertion functions (`asserts value`), so TS2775
a: string, // fires on every call. Override the affected methods with non-assertion
b: string, // signatures — the runtime behavior is unchanged.
opts: { type NonAssertingMethods = {
preserveComments: boolean; ok(value: unknown, message?: string): void;
withoutNormalizeHtml: boolean; isOk(value: unknown, message?: string): void;
}, isTrue(value: unknown, message?: string): void;
description?: string isFalse(value: unknown, message?: string): void;
): 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 // TODO remove this shim when we can
// @ts-expect-error // @ts-expect-error
Promise.withResolvers = () => { Promise.withResolvers = () => {
@ -75,6 +91,7 @@ export interface RuntimeTest<Props extends Record<string, any> = Record<string,
raf: { raf: {
tick: (ms: number) => void; tick: (ms: number) => void;
}; };
snapshot: any;
target: HTMLElement; target: HTMLElement;
window: Window & { window: Window & {
Event: typeof Event; Event: typeof Event;
@ -126,6 +143,16 @@ const listeners = process.rawListeners('unhandledRejection');
beforeAll(() => { beforeAll(() => {
// @ts-expect-error TODO huh? // @ts-expect-error TODO huh?
process.prependListener('unhandledRejection', unhandled_rejection_handler); 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(() => { beforeEach(() => {
@ -255,6 +282,26 @@ async function run_test_variant(
let warnings: string[] = []; let warnings: string[] = [];
let errors: string[] = []; let errors: string[] = [];
let manual_hydrate = false; 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. // 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) { if (str.slice(0, i).includes('errors') || config.errors) {
intercept_errors = true;
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error = (...args) => { console.error = (...args) => {
errors.push(...args); errors.push(...args);
@ -348,10 +396,7 @@ async function run_test_variant(
window.document.head.innerHTML = styles ? `<style>${styles}</style>` : ''; window.document.head.innerHTML = styles ? `<style>${styles}</style>` : '';
window.document.body.innerHTML = '<main></main>'; window.document.body.innerHTML = '<main></main>';
window.addEventListener('error', (e) => { window.addEventListener('error', window_error_listener, true);
unhandled_rejection = e.error;
e.preventDefault();
});
globalThis.requestAnimationFrame = globalThis.setTimeout; globalThis.requestAnimationFrame = globalThis.setTimeout;
@ -423,7 +468,6 @@ async function run_test_variant(
await config.test_ssr({ await config.test_ssr({
logs, logs,
warnings, warnings,
// @ts-expect-error
assert: { assert: {
...assert, ...assert,
htmlEqual: assert_html_equal, htmlEqual: assert_html_equal,
@ -515,7 +559,6 @@ async function run_test_variant(
} }
await config.test({ await config.test({
// @ts-expect-error TS doesn't get it
assert: { assert: {
...assert, ...assert,
htmlEqual: assert_html_equal, htmlEqual: assert_html_equal,
@ -525,6 +568,7 @@ async function run_test_variant(
component: runes ? props : instance, component: runes ? props : instance,
instance, instance,
mod, mod,
ok,
target, target,
snapshot, snapshot,
window, window,
@ -593,6 +637,8 @@ async function run_test_variant(
throw new Error('Hydration state was not cleared'); throw new Error('Hydration state was not cleared');
} }
window.removeEventListener('error', window_error_listener, true);
config.after_test?.(); config.after_test?.();
// Free up the microtask queue // Free up the microtask queue

@ -31,7 +31,21 @@ interface SourcemapTest extends BaseTest {
/** The expected `sources` array in the source map */ /** The expected `sources` array in the source map */
css_map_sources?: string[]; css_map_sources?: string[];
test?: (obj: { 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; input: string;
map_preprocessed: any; map_preprocessed: any;
code_preprocessed: string; 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"?> 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"> <!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>{ export default <Environment>{
name: 'jsdom-xhtml', name: 'jsdom-xhtml',
transformMode: 'web', viteEnvironment: 'client',
setup(global, { jsdom = {} }) { setup(global, { jsdom = {} }) {
return builtinEnvironments.jsdom.setup(global, { return builtinEnvironments.jsdom.setup(global, {
jsdom: { jsdom: {

@ -29,6 +29,11 @@ export default defineConfig({
test: { test: {
dir: '.', dir: '.',
reporters: ['dot'], 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: [ include: [
'packages/svelte/**/*.test.ts', 'packages/svelte/**/*.test.ts',
'packages/svelte/tests/*/test.ts', 'packages/svelte/tests/*/test.ts',

Loading…
Cancel
Save