@ -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