From d18c4b6999ef115f96fb85ccd6c410ca2b1a43b3 Mon Sep 17 00:00:00 2001 From: Raashish Aggarwal <94279692+raashish1601@users.noreply.github.com> Date: Wed, 7 Oct 2026 17:38:14 +0530 Subject: [PATCH] fix(test-runner-core): await browser cleanup in watch mode --- .changeset/quiet-browsers-wait.md | 5 +++ .../test-runner-core/src/runner/TestRunner.ts | 1 - .../test/src/runner/TestRunner.test.ts | 35 +++++++++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 .changeset/quiet-browsers-wait.md diff --git a/.changeset/quiet-browsers-wait.md b/.changeset/quiet-browsers-wait.md new file mode 100644 index 0000000000..8a4e41df72 --- /dev/null +++ b/.changeset/quiet-browsers-wait.md @@ -0,0 +1,5 @@ +--- +'@web/test-runner-core': patch +--- + +Wait for browsers to finish closing before completing test runner shutdown in watch mode. diff --git a/packages/test-runner-core/src/runner/TestRunner.ts b/packages/test-runner-core/src/runner/TestRunner.ts index 31f3f15a39..6b8c0c5f64 100644 --- a/packages/test-runner-core/src/runner/TestRunner.ts +++ b/packages/test-runner-core/src/runner/TestRunner.ts @@ -166,7 +166,6 @@ export class TestRunner extends EventEmitter { if (this.config.watch) { // we only need to stop the browsers in watch mode, in non-watch // mode the scheduler has already stopped them - const stopActions = []; for (const browser of this.browsers) { if (browser.stop) { stopActions.push( diff --git a/packages/test-runner-core/test/src/runner/TestRunner.test.ts b/packages/test-runner-core/test/src/runner/TestRunner.test.ts index 29aa9b5633..230a2f70ea 100644 --- a/packages/test-runner-core/test/src/runner/TestRunner.test.ts +++ b/packages/test-runner-core/test/src/runner/TestRunner.test.ts @@ -1,6 +1,7 @@ import assert from 'node:assert/strict'; import path from 'node:path'; import { describe, it, mock } from 'node:test'; +import { setImmediate } from 'node:timers/promises'; import portfinder from 'portfinder'; @@ -9,6 +10,7 @@ import type { TestRunnerCoreConfig } from '../../../dist/config/TestRunnerCoreCo import type { TestRunnerGroupConfig } from '../../../dist/index.js'; import type { Logger } from '../../../dist/logger/Logger.js'; import { TestRunner } from '../../../dist/runner/TestRunner.js'; +import { TestRunnerServer } from '../../../dist/server/TestRunnerServer.js'; import { SESSION_STATUS } from '../../../dist/test-session/TestSessionStatus.js'; function createBrowserStub(): BrowserLauncher { @@ -134,6 +136,39 @@ describe('TestRunner', function () { assert.equal(passed, false, 'test runner quits with false'); }); + it('waits for browser cleanup in watch mode', async t => { + const { runner, browser } = await createTestRunner({ watch: true }); + const stopServer = t.mock.method(TestRunnerServer.prototype, 'stop'); + const browserStopped = Promise.withResolvers(); + const stopped = mock.fn(); + runner.on('stopped', stopped); + browser.stop.mock.mockImplementation(() => browserStopped.promise); + + await runner.start(); + + let stopResolved = false; + const stopping = runner.stop().then(() => { + stopResolved = true; + }); + + try { + await setImmediate(); + assert.equal(stopServer.mock.callCount(), 1); + await stopServer.mock.calls[0].result; + await setImmediate(); + + assert.equal(browser.stop.mock.callCount(), 1, 'browser cleanup has started'); + assert.equal(stopped.mock.callCount(), 0, 'stopped event waits for browser cleanup'); + assert.equal(stopResolved, false, 'stop waits for browser cleanup'); + } finally { + browserStopped.resolve(); + await stopping; + } + + assert.equal(stopped.mock.callCount(), 1, 'stopped event is emitted after browser cleanup'); + assert.equal(stopResolved, true); + }); + describe('groups', () => { it('can create a group in addition to the default group', async () => { const { runner } = await createTestRunner(undefined, [