Skip to content

Commit b87fa03

Browse files
authored
chore: follow up to disposables (microsoft#39510)
1 parent 528c704 commit b87fa03

File tree

18 files changed

+110
-73
lines changed

18 files changed

+110
-73
lines changed

docs/src/api/class-browsercontext.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1021,6 +1021,7 @@ API testing helper associated with this context. Requests made with this API wil
10211021

10221022
## async method: BrowserContext.route
10231023
* since: v1.8
1024+
- returns: <[Disposable]>
10241025

10251026
Routing provides the capability to modify network requests that are made by any page in the browser context. Once route
10261027
is enabled, every request matching the url pattern will stall unless it's continued, fulfilled or aborted.

docs/src/api/class-page.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3566,6 +3566,7 @@ API testing helper associated with this page. This method returns the same insta
35663566

35673567
## async method: Page.route
35683568
* since: v1.8
3569+
- returns: <[Disposable]>
35693570

35703571
Routing provides the capability to modify network requests that are made by a page.
35713572

packages/playwright-client/types/types.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4147,7 +4147,7 @@ export interface Page {
41474147
* How often a route should be used. By default it will be used every time.
41484148
*/
41494149
times?: number;
4150-
}): Promise<void>;
4150+
}): Promise<Disposable>;
41514151

41524152
/**
41534153
* If specified the network requests that are made in the page will be served from the HAR file. Read more about
@@ -9469,7 +9469,7 @@ export interface BrowserContext {
94699469
* How often a route should be used. By default it will be used every time.
94709470
*/
94719471
times?: number;
9472-
}): Promise<void>;
9472+
}): Promise<Disposable>;
94739473

94749474
/**
94759475
* If specified the network requests that are made in the context will be served from the HAR file. Read more about

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export { Clock } from './clock';
2323
export { ConsoleMessage } from './consoleMessage';
2424
export { Coverage } from './coverage';
2525
export { Dialog } from './dialog';
26-
export { Disposable } from './disposable';
26+
export type { Disposable } from './disposable';
2727
export { Download } from './download';
2828
export { Electron, ElectronApplication } from './electron';
2929
export { FrameLocator, Locator } from './locator';

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

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { evaluationScript } from './clientHelper';
2323
import { Clock } from './clock';
2424
import { ConsoleMessage } from './consoleMessage';
2525
import { Dialog } from './dialog';
26-
import { Disposable } from './disposable';
26+
import { DisposableObject, DisposableStub } from './disposable';
2727
import { TargetClosedError, parseError } from './errors';
2828
import { Events } from './events';
2929
import { APIRequestContext } from './fetch';
@@ -352,25 +352,26 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
352352

353353
async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any) {
354354
const source = await evaluationScript(this._platform, script, arg);
355-
return Disposable.from((await this._channel.addInitScript({ source })).disposable);
355+
return DisposableObject.from((await this._channel.addInitScript({ source })).disposable);
356356
}
357357

358-
async exposeBinding(name: string, callback: (source: structs.BindingSource, ...args: any[]) => any, options: { handle?: boolean } = {}): Promise<Disposable> {
358+
async exposeBinding(name: string, callback: (source: structs.BindingSource, ...args: any[]) => any, options: { handle?: boolean } = {}): Promise<DisposableObject> {
359359
const result = await this._channel.exposeBinding({ name, needsHandle: options.handle });
360360
this._bindings.set(name, callback);
361-
return Disposable.from(result.disposable);
361+
return DisposableObject.from(result.disposable);
362362
}
363363

364-
async exposeFunction(name: string, callback: Function): Promise<Disposable> {
364+
async exposeFunction(name: string, callback: Function): Promise<DisposableObject> {
365365
const result = await this._channel.exposeBinding({ name });
366366
const binding = (source: structs.BindingSource, ...args: any[]) => callback(...args);
367367
this._bindings.set(name, binding);
368-
return Disposable.from(result.disposable);
368+
return DisposableObject.from(result.disposable);
369369
}
370370

371-
async route(url: URLMatch, handler: network.RouteHandlerCallback, options: { times?: number } = {}): Promise<void> {
371+
async route(url: URLMatch, handler: network.RouteHandlerCallback, options: { times?: number } = {}): Promise<DisposableStub> {
372372
this._routes.unshift(new network.RouteHandler(this._platform, this._options.baseURL, url, handler, options.times));
373373
await this._updateInterceptionPatterns({ title: 'Route requests' });
374+
return new DisposableStub(() => this.unroute(url, handler));
374375
}
375376

376377
async routeWebSocket(url: URLMatch, handler: network.WebSocketRouteHandlerCallback): Promise<void> {

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { CDPSession } from './cdpSession';
2424
import { ChannelOwner } from './channelOwner';
2525
import { createInstrumentation } from './clientInstrumentation';
2626
import { Dialog } from './dialog';
27-
import { Disposable } from './disposable';
27+
import { DisposableObject } from './disposable';
2828
import { Electron, ElectronApplication } from './electron';
2929
import { ElementHandle } from './elementHandle';
3030
import { TargetClosedError, parseError } from './errors';
@@ -272,7 +272,7 @@ export class Connection extends EventEmitter {
272272
result = new Dialog(parent, type, guid, initializer);
273273
break;
274274
case 'Disposable':
275-
result = new Disposable(parent, type, guid, initializer);
275+
result = new DisposableObject(parent, type, guid, initializer);
276276
break;
277277
case 'Electron':
278278
result = new Electron(parent, type, guid, initializer);

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

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,12 @@ import { isTargetClosedError } from './errors';
1919

2020
import type * as channels from '@protocol/channels';
2121

22-
export class Disposable<T extends channels.DisposableChannel = channels.DisposableChannel> extends ChannelOwner<T> {
23-
static from(channel: channels.DisposableChannel): Disposable {
22+
export interface Disposable {
23+
dispose: () => Promise<void>;
24+
}
25+
26+
export class DisposableObject<T extends channels.DisposableChannel = channels.DisposableChannel> extends ChannelOwner<T> implements Disposable {
27+
static from(channel: channels.DisposableChannel): DisposableObject {
2428
return (channel as any)._object;
2529
}
2630

@@ -38,3 +42,33 @@ export class Disposable<T extends channels.DisposableChannel = channels.Disposab
3842
}
3943
}
4044
}
45+
46+
export class DisposableStub implements Disposable {
47+
private _dispose: (() => Promise<void>) | undefined;
48+
49+
constructor(dispose: () => Promise<void>) {
50+
this._dispose = dispose;
51+
}
52+
53+
async [Symbol.asyncDispose]() {
54+
await this.dispose();
55+
}
56+
57+
async dispose() {
58+
if (!this._dispose)
59+
return;
60+
try {
61+
const dispose = this._dispose;
62+
this._dispose = undefined;
63+
await dispose();
64+
} catch (e) {
65+
if (isTargetClosedError(e))
66+
return;
67+
throw e;
68+
}
69+
}
70+
}
71+
72+
export function disposeAll(disposables: Disposable[]) {
73+
return Promise.all(disposables.map(d => d.dispose()));
74+
}

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

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
import type { EventEmitter as EventEmitterType } from 'events';
2626
import type { Platform } from './platform';
27+
import type { Disposable } from './disposable';
2728

2829
type EventType = string | symbol;
2930
type Listener = (...args: any[]) => any;
@@ -397,18 +398,14 @@ function wrappedListener(l: Listener): Listener {
397398
return (l as any).listener;
398399
}
399400

400-
export type Disposable = {
401-
dispose: () => void;
402-
};
403-
404401
class EventsHelper {
405402
static addEventListener(
406403
emitter: EventEmitterType,
407404
eventName: (string | symbol),
408405
handler: (...args: any[]) => void): Disposable {
409406
emitter.on(eventName, handler);
410407
return {
411-
dispose: () => emitter.removeListener(eventName, handler)
408+
dispose: async () => { emitter.removeListener(eventName, handler); }
412409
};
413410
}
414411
}

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { Artifact } from './artifact';
1919
import { ChannelOwner } from './channelOwner';
2020
import { evaluationScript } from './clientHelper';
2121
import { Coverage } from './coverage';
22-
import { Disposable } from './disposable';
22+
import { DisposableObject, DisposableStub } from './disposable';
2323
import { Download } from './download';
2424
import { ElementHandle, determineScreenshotType } from './elementHandle';
2525
import { TargetClosedError, isTargetClosedError, parseError, serializeError } from './errors';
@@ -336,13 +336,13 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
336336
const result = await this._channel.exposeBinding({ name });
337337
const binding = (source: structs.BindingSource, ...args: any[]) => callback(...args);
338338
this._bindings.set(name, binding);
339-
return Disposable.from(result.disposable);
339+
return DisposableObject.from(result.disposable);
340340
}
341341

342342
async exposeBinding(name: string, callback: (source: structs.BindingSource, ...args: any[]) => any, options: { handle?: boolean } = {}) {
343343
const result = await this._channel.exposeBinding({ name, needsHandle: options.handle });
344344
this._bindings.set(name, callback);
345-
return Disposable.from(result.disposable);
345+
return DisposableObject.from(result.disposable);
346346
}
347347

348348
async setExtraHTTPHeaders(headers: Headers) {
@@ -510,12 +510,13 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
510510

511511
async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any) {
512512
const source = await evaluationScript(this._platform, script, arg);
513-
return Disposable.from((await this._channel.addInitScript({ source })).disposable);
513+
return DisposableObject.from((await this._channel.addInitScript({ source })).disposable);
514514
}
515515

516-
async route(url: URLMatch, handler: RouteHandlerCallback, options: { times?: number } = {}): Promise<void> {
516+
async route(url: URLMatch, handler: RouteHandlerCallback, options: { times?: number } = {}): Promise<DisposableStub> {
517517
this._routes.unshift(new RouteHandler(this._platform, this._browserContext._options.baseURL, url, handler, options.times));
518518
await this._updateInterceptionPatterns({ title: 'Route requests' });
519+
return new DisposableStub(() => this.unroute(url, handler));
519520
}
520521

521522
async routeFromHAR(har: string, options: { url?: string | RegExp, notFound?: 'abort' | 'fallback', update?: boolean, updateContent?: 'attach' | 'embed', updateMode?: 'minimal' | 'full'} = {}): Promise<void> {

packages/playwright-core/src/mcp/browser/context.ts

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import path from 'path';
1818

19+
import { disposeAll } from '../../client/disposable';
1920
import { eventsHelper } from '../../client/eventEmitter';
2021
import { debug } from '../../utilsBundle';
2122
import { escapeWithQuotes } from '../../utils/isomorphic/stringUtils';
@@ -29,7 +30,7 @@ import type * as playwright from '../../..';
2930
import type { FullConfig } from './config';
3031
import type { SessionLog } from './sessionLog';
3132
import type { Tracing } from '../../client/tracing';
32-
import type { Disposable } from '../../client/eventEmitter';
33+
import type { Disposable } from '../../client/disposable';
3334
import type { BrowserContext } from '../../client/browserContext';
3435
import type { ClientInfo } from '../sdk/server';
3536

@@ -88,14 +89,12 @@ export class Context {
8889
}
8990

9091
async dispose() {
91-
for (const disposable of this._disposables)
92-
disposable.dispose();
93-
this._disposables = [];
92+
disposeAll(this._disposables);
9493
for (const tab of this._tabs)
95-
tab.dispose();
94+
await tab.dispose();
9695
this._tabs.length = 0;
9796
this._currentTab = undefined;
98-
this.stopVideoRecording();
97+
await this.stopVideoRecording();
9998
}
10099

101100
tabs(): Tab[] {
@@ -235,15 +234,17 @@ export class Context {
235234

236235
private async _setupRequestInterception(context: playwright.BrowserContext) {
237236
if (this.config.network?.allowedOrigins?.length) {
238-
await context.route('**', route => route.abort('blockedbyclient'));
237+
this._disposables.push(await context.route('**', route => route.abort('blockedbyclient')));
239238

240-
for (const origin of this.config.network.allowedOrigins)
241-
await context.route(originOrHostGlob(origin), route => route.continue());
239+
for (const origin of this.config.network.allowedOrigins) {
240+
const glob = originOrHostGlob(origin);
241+
this._disposables.push(await context.route(glob, route => route.continue()));
242+
}
242243
}
243244

244245
if (this.config.network?.blockedOrigins?.length) {
245246
for (const origin of this.config.network.blockedOrigins)
246-
await context.route(originOrHostGlob(origin), route => route.abort('blockedbyclient'));
247+
this._disposables.push(await context.route(originOrHostGlob(origin), route => route.abort('blockedbyclient')));
247248
}
248249
}
249250

@@ -263,9 +264,7 @@ export class Context {
263264
(browserContext as any)._setAllowedDirectories(allRootPaths(this._clientInfo));
264265
}
265266
await this._setupRequestInterception(browserContext);
266-
for (const page of browserContext.pages())
267-
this._onPageCreated(page);
268-
this._disposables.push(eventsHelper.addEventListener(browserContext as BrowserContext, 'page', page => this._onPageCreated(page)));
267+
269268
if (this.config.saveTrace) {
270269
await (browserContext.tracing as Tracing).start({
271270
name: 'trace-' + Date.now(),
@@ -281,7 +280,12 @@ export class Context {
281280
}
282281
const rootPath = firstRootPath(this._clientInfo);
283282
for (const initScript of this.config.browser.initScript || [])
284-
await browserContext.addInitScript({ path: path.resolve(rootPath, initScript) });
283+
this._disposables.push(await browserContext.addInitScript({ path: path.resolve(rootPath, initScript) }));
284+
285+
for (const page of browserContext.pages())
286+
this._onPageCreated(page);
287+
this._disposables.push(eventsHelper.addEventListener(browserContext as BrowserContext, 'page', page => this._onPageCreated(page)));
288+
285289
return browserContext;
286290
}
287291

0 commit comments

Comments
 (0)