From fbe37c083064decdc26af084cf20df27620b8634 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Tue, 20 Oct 2026 14:54:27 +0200 Subject: [PATCH] fix(browser): clear screenshot stability timeout (#11659) --- .../node/commands/screenshotMatcher/index.ts | 22 ++++++++----- .../node/commands/screenshotMatcher/utils.ts | 14 +++++--- .../browser/specs/to-match-screenshot.test.ts | 33 ++++++++++++++++++- 3 files changed, 56 insertions(+), 13 deletions(-) diff --git a/packages/browser/src/node/commands/screenshotMatcher/index.ts b/packages/browser/src/node/commands/screenshotMatcher/index.ts index 7ec0a3773..5452f28d5 100644 --- a/packages/browser/src/node/commands/screenshotMatcher/index.ts +++ b/packages/browser/src/node/commands/screenshotMatcher/index.ts @@ -428,14 +428,20 @@ async function waitForStableScreenshot( const stableScreenshot = getStableScreenshot(options, abortController.signal) - const result = await (timeout === 0 - ? stableScreenshot - : Promise.race([ - stableScreenshot, - asyncTimeout(timeout).finally(() => abortController.abort()), - ])) - - return result + if (timeout === 0) { + return stableScreenshot + } + + const stabilityTimeout = asyncTimeout(timeout) + + try { + return await Promise.race([ + stableScreenshot, + stabilityTimeout.promise.finally(() => abortController.abort()), + ]) + } finally { + stabilityTimeout.clear() + } } /** diff --git a/packages/browser/src/node/commands/screenshotMatcher/utils.ts b/packages/browser/src/node/commands/screenshotMatcher/utils.ts index 2aafa5ebd..1c5a5466e 100644 --- a/packages/browser/src/node/commands/screenshotMatcher/utils.ts +++ b/packages/browser/src/node/commands/screenshotMatcher/utils.ts @@ -275,15 +275,21 @@ export function takeDecodedScreenshot({ * Creates a promise that resolves to `null` after the specified timeout. * If the timeout is `0`, the promise resolves immediately. * + * Call `clear` once the result is not needed, otherwise the timer keeps the process alive until it fires. + * * @param timeout - The delay in milliseconds before the promise resolves - * @returns `Promise` that resolves to `null` after the timeout + * @returns The `promise` and a `clear` function that stops the timer */ -export function asyncTimeout(timeout: number): Promise { - return new Promise((resolve) => { +export function asyncTimeout(timeout: number): { promise: Promise; clear: () => void } { + let timer: NodeJS.Timeout | undefined + + const promise = new Promise((resolve) => { if (timeout === 0) { resolve(null) } else { - setTimeout(resolve, timeout, null) + timer = setTimeout(resolve, timeout, null) } }) + + return { promise, clear: () => clearTimeout(timer) } } diff --git a/test/browser/specs/to-match-screenshot.test.ts b/test/browser/specs/to-match-screenshot.test.ts index 1db5df17f..c374d6a7f 100644 --- a/test/browser/specs/to-match-screenshot.test.ts +++ b/test/browser/specs/to-match-screenshot.test.ts @@ -3,7 +3,7 @@ import type { ViteUserConfig } from 'vitest/config' import type { TestFsStructure } from '../../test-utils' import { platform } from 'node:os' import { describe, expect, test } from 'vitest' -import { runInlineTests } from '../../test-utils' +import { runInlineTests, runVitestCli, useTmpFS } from '../../test-utils' import { extractToMatchScreenshotPaths } from '../fixtures/expect-dom/utils' import utilsContent from '../fixtures/expect-dom/utils?raw' import { instances, provider } from '../settings' @@ -322,3 +322,34 @@ test.for([{ option: 'retry' }, { option: 'repeats' }] as const)( ) }, ) + +test('does not keep the process alive until the stability timeout elapses', async () => { + const { root } = useTmpFS({ + [testFilename]: testContent.replace( + 'toMatchScreenshot()', + 'toMatchScreenshot({ timeout: 30_000 })', + ), + 'utils.ts': utilsContent, + 'vitest.config.js': /* ts */ ` + import { instances, provider } from '../settings' + + export default { + test: { + teardownTimeout: 5_000, + browser: { + enabled: true, + ui: false, + headless: true, + provider, + instances: [instances[0]], + viewport: { width: 400, height: 200 }, + }, + }, + }`, + }) + + const { stderr, exitCode } = await runVitestCli('--root', root, '--run', '--update') + + expect(stderr).toMatchInlineSnapshot(`""`) + expect(exitCode).toBe(0) +}) -- 2.54.0