Skip to content

Commit b270a9e

Browse files
committed
fix(client): report a failed BrowserContext.close() instead of resolving
A close() that rejected left _closingStatus === 'closing' behind, so isClosed() reported true for a context that was still live, and every later close() returned early through the entry guard without doing anything. Callers could neither detect the leftover context nor close it: the context stayed alive until the browser exited. Remember the failure, restore the status so isClosed() stays truthful, and rethrow it on subsequent calls.
1 parent 3689414 commit b270a9e

2 files changed

Lines changed: 32 additions & 6 deletions

File tree

packages/playwright-core/src/client/browserContext.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
8282

8383
readonly _serviceWorkers = new Set<Worker>();
8484
private _closingStatus: 'none' | 'closing' | 'closed' = 'none';
85+
private _closeFailure: Error | undefined;
8586
private _closeReason: string | undefined;
8687
private _harRouters: HarRouter[] = [];
8788
private _onRecorderEventSink: RecorderEventSink | undefined;
@@ -521,15 +522,25 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
521522
}
522523

523524
async close(options: { reason?: string } = {}): Promise<void> {
524-
if (this.isClosed())
525+
if (this._closingStatus === 'closed')
525526
return;
527+
if (this._closeFailure)
528+
throw this._closeFailure;
526529
this._closeReason = options.reason;
527530
this._closingStatus = 'closing';
528-
await this.request.dispose(options);
529-
await this._instrumentation.runBeforeCloseBrowserContext(this);
530-
await this.tracing._exportAllHars();
531-
await this._channel.close(options, kNoTimeout);
532-
await this._closedPromise;
531+
try {
532+
await this.request.dispose(options);
533+
await this._instrumentation.runBeforeCloseBrowserContext(this);
534+
await this.tracing._exportAllHars();
535+
await this._channel.close(options, kNoTimeout);
536+
await this._closedPromise;
537+
} catch (error) {
538+
this._closeFailure = error;
539+
// The context did not close, so isClosed() must not claim otherwise.
540+
if (this._closingStatus === 'closing')
541+
this._closingStatus = 'none';
542+
throw error;
543+
}
533544
}
534545

535546
async _enableRecorder(params: channels.BrowserContextEnableRecorderParams, eventSink?: RecorderEventSink) {

tests/library/browsercontext-basic.spec.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import { kTargetClosedErrorMessage } from '../config/errors';
1919
import { browserTest as it, expect } from '../config/browserTest';
20+
import os from 'os';
2021
import { attachFrame, verifyViewport } from '../config/utils';
2122
import type { Page } from '@playwright/test';
2223

@@ -212,6 +213,20 @@ it('close() should be callable twice', async ({ browser }) => {
212213
await context.close();
213214
});
214215

216+
it('close() should report a failure instead of silently succeeding', async ({ browserType }) => {
217+
const browser = await browserType.launch();
218+
// Writing the HAR into a directory fails, which makes close() reject.
219+
const context = await browser.newContext({ recordHar: { path: os.tmpdir() } });
220+
await context.newPage();
221+
const firstError = await context.close().catch(e => e);
222+
expect(firstError).toBeInstanceOf(Error);
223+
// The context is still alive, so it must not report itself as closed.
224+
expect(context.isClosed()).toBe(false);
225+
const secondError = await context.close().catch(e => e);
226+
expect(secondError).toBeInstanceOf(Error);
227+
await browser.close();
228+
});
229+
215230
it('should pass self to close event', async ({ browser }) => {
216231
const newContext = await browser.newContext();
217232
const [closedContext] = await Promise.all([

0 commit comments

Comments
 (0)