[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
12

Configure Feed

Select the types of activity you want to include in your feed.

test: prove watcher delivery with probe cycling, suppress stale startup events

Vladimir Sheremet (Jul 27, 2026, 2:50 PM +0200) bfa35002 f15a8073

+44 -23
+44 -23
test/test-utils/index.ts
··· 290 290 // watcher's baseline and ignored, so newly added files wouldn't trigger a 291 291 // rerun. Wait for the watcher to be ready before handing control back. 292 292 if (watch && ctx) { 293 - await waitForWatcherReady(ctx.vite.watcher, ctx.config.root) 293 + await waitForWatcherReady(ctx) 294 294 } 295 295 296 296 return { ··· 333 333 // time after it is discovered, and an edit made before the poller exists is 334 334 // folded into its baseline stat and never emits an event. No amount of chokidar 335 335 // bookkeeping (`_readyEmitted`, `getWatched()`) proves the pollers are live, so 336 - // verify end-to-end: keep touching a probe file in the root until the watcher 337 - // reports it. Once a probe event arrives, the initial scan is complete and 338 - // every pre-existing file has a poller, so subsequent edits are guaranteed to 339 - // fire. The probe produces no output: it matches no test glob and no fixture's 340 - // rerun triggers, so `VitestWatcher` ignores it without scheduling a rerun. 336 + // verify end-to-end: keep cycling a probe file in the root until the watcher 337 + // reports it. The probe is recreated rather than rewritten because a creation 338 + // folded into the initial scan of the file AND its directory leaves nothing to 339 + // rescan; deleting and recreating it bumps the directory mtime every cycle, so 340 + // a live directory poller always sees a difference, and any probe event 341 + // (`add`, `change` or `unlink`) proves delivery. 342 + // 343 + // The scan can also surface writes made by the PREVIOUS test (an `afterEach` 344 + // restoring a fixture right before this instance started) as fresh change 345 + // events, triggering a rerun the test never asked for. While waiting, neuter 346 + // every rerun attempt by clearing the watcher state (the rerun debounce 347 + // returns silently when `changedTests` is empty) and only hand control back 348 + // after the event backlog has been quiet for a couple of poll intervals. 341 349 // 342 350 // Only instances with an explicit small root (a fixture or an inline-test dir) 343 351 // get the probe: tests that assert on watch reruns always use one. Instances ··· 345 353 // `watch: true` without a root) never wait for file events, and polling their 346 354 // huge tree can take longer than any reasonable deadline, so for them only the 347 355 // cheap bookkeeping check runs, as before. 348 - async function waitForWatcherReady(watcher: FSWatcher, root: string): Promise<void> { 356 + async function waitForWatcherReady(ctx: Vitest): Promise<void> { 357 + const watcher: FSWatcher = ctx.vite.watcher 358 + const root = ctx.config.root 349 359 const slash = (p: string) => p.replace(/\\/g, '/') 350 360 const parent = slash(dirname(root)) 351 361 const bookkeepingDeadline = Date.now() + 2000 ··· 365 375 366 376 const deadline = Date.now() + 10_000 367 377 const probe = resolve(root, '.vitest-watcher-ready-probe') 368 - let onProbeSeen!: () => void 369 - const probeSeen = new Promise<void>(resolve => (onProbeSeen = resolve)) 370 - const onProbeEvent = (file: string) => { 378 + const suppressRerun = () => { 379 + ctx.watcher.changedTests.clear() 380 + ctx.watcher.invalidates.clear() 381 + } 382 + let probeSeen = false 383 + let lastEventAt = Date.now() 384 + const onWatcherEvent = (_event: string, file: string) => { 385 + suppressRerun() 371 386 if (slash(file) === probe) { 372 - onProbeSeen() 387 + probeSeen = true 388 + } 389 + else { 390 + lastEventAt = Date.now() 373 391 } 374 392 } 375 - watcher.on('add', onProbeEvent) 376 - watcher.on('change', onProbeEvent) 393 + watcher.on('all', onWatcherEvent) 377 394 try { 378 395 while (true) { 379 - // rewrite instead of writing once: if the initial scan captured a probe 380 - // write without emitting `add`, the next mtime bump still fires `change` 381 - fs.writeFileSync(probe, `${Date.now()}`, 'utf-8') 382 - const seen = await Promise.race([ 383 - probeSeen.then(() => true), 384 - new Promise<boolean>(resolve => setTimeout(resolve, 50, false)), 385 - ]) 386 - if (seen) { 396 + if (probeSeen) { 387 397 break 388 398 } 389 399 if (Date.now() > deadline) { ··· 392 402 JSON.stringify(watcher.getWatched(), null, 2)}`, 393 403 ) 394 404 } 405 + fs.writeFileSync(probe, `${Date.now()}`, 'utf-8') 406 + await new Promise(resolve => setTimeout(resolve, 50)) 407 + if (probeSeen) { 408 + break 409 + } 410 + fs.rmSync(probe, { force: true }) 411 + await new Promise(resolve => setTimeout(resolve, 50)) 412 + } 413 + 414 + while (Date.now() - lastEventAt < 200 && Date.now() < deadline) { 415 + await new Promise(resolve => setTimeout(resolve, 50)) 395 416 } 396 417 } 397 418 finally { 398 - watcher.off('add', onProbeEvent) 399 - watcher.off('change', onProbeEvent) 419 + watcher.off('all', onWatcherEvent) 400 420 fs.rmSync(probe, { force: true }) 421 + suppressRerun() 401 422 } 402 423 } 403 424