Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ const ignores = [
export const baseRules = {
"@typescript-eslint/no-unused-vars": [
2,
{ args: "none", caughtErrors: "none" },
{ args: "none", caughtErrors: "none", ignoreUsingDeclarations: true },
],

/**
Expand Down
12 changes: 8 additions & 4 deletions packages/isomorphic/manualPromise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
* limitations under the License.
*/

const kDispose: typeof Symbol.dispose = (Symbol.dispose || Symbol.for('Symbol.dispose')) as typeof Symbol.dispose;

export class ManualPromise<T = void> extends Promise<T> {
private _resolve!: (t: T) => void;
private _reject!: (e: Error) => void;
Expand Down Expand Up @@ -111,16 +113,18 @@ export class LongStandingScope {
}
}

export function signalToPromise(signal: AbortSignal): { promise: Promise<void>, dispose: () => void } {
if (signal.aborted)
return { promise: Promise.resolve(), dispose: () => {} };
export function signalToPromise(signal: AbortSignal): Disposable & { promise: Promise<void>, dispose: () => void } {
if (signal.aborted) {
function dispose() {}
return { promise: Promise.resolve(), dispose, [kDispose]: dispose };
}
let dispose: (() => void) | undefined;
const promise = new Promise<void>(resolve => {
const onAbort = () => resolve();
signal.addEventListener('abort', onAbort, { once: true });
dispose = () => signal.removeEventListener('abort', onAbort);
});
return { promise, dispose: dispose! };
return { promise, dispose: dispose!, [kDispose]: dispose! };
}

function cloneError(error: Error, frames: string[]) {
Expand Down
30 changes: 17 additions & 13 deletions packages/isomorphic/timeoutRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,26 @@
/* eslint-disable no-restricted-globals */

import { monotonicTime } from './time';
import { ManualPromise } from './manualPromise';

const kDispose: typeof Symbol.dispose = (Symbol.dispose || Symbol.for('Symbol.dispose')) as typeof Symbol.dispose;

// Timers do not implement Symbol.dispose on every supported Node.js version.
export function createTimeout(callback: () => void, timeout: number): Disposable {
const timer = setTimeout(callback, timeout);
return {
[kDispose]() {
clearTimeout(timer);
},
};
}

export async function raceAgainstDeadline<T>(cb: () => Promise<T>, deadline: number): Promise<{ result: T, timedOut: false } | { timedOut: true }> {
let timer: NodeJS.Timeout | undefined;
const resultPromise: Promise<{ result: T, timedOut: false }> = cb().then(result => ({ result, timedOut: false }));
const timeoutPromise = new ManualPromise<{ timedOut: true }>();
using timer = deadline ? createTimeout(() => timeoutPromise.resolve({ timedOut: true }), Math.max(0, deadline - monotonicTime())) : undefined;
// Note: not including "await" here truncates the async stack inside cb(), so always include it.
return await Promise.race([
cb().then(result => {
return { result, timedOut: false };
}),
new Promise<{ timedOut: true }>(resolve => {
if (!deadline)
return;
timer = setTimeout(() => resolve({ timedOut: true }), Math.max(0, deadline - monotonicTime()));
}),
]).finally(() => {
clearTimeout(timer);
});
return await Promise.race([resultPromise, timeoutPromise]);
}

export async function pollAgainstDeadline<T>(callback: () => Promise<{ continuePolling: boolean, result: T }>, deadline: number, [...pollIntervals]: number[] = [100, 250, 500, 1000]): Promise<{ result?: T, timedOut: boolean }> {
Expand Down
6 changes: 2 additions & 4 deletions packages/playwright-core/src/client/android.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,13 +274,11 @@ export class AndroidDevice extends ChannelOwner<channels.AndroidDeviceChannel> i
return await this._wrapApiCall(async () => {
const timeoutOptions = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate);
const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate;
const waiter = Waiter.createForEvent(this, event);
using waiter = Waiter.createForEvent(this, event);
waiter.rejectOnTimeout(timeoutOptions, `Timeout ${timeoutOptions.timeout}ms exceeded while waiting for event "${event}"`);
if (event !== Events.AndroidDevice.Close)
waiter.rejectOnEvent(this, Events.AndroidDevice.Close, () => new TargetClosedError());
const result = await waiter.waitForEvent(this, event, predicate as any);
waiter.dispose();
return result;
return await waiter.waitForEvent(this, event, predicate as any);
});
}
}
Expand Down
6 changes: 2 additions & 4 deletions packages/playwright-core/src/client/browserContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -469,13 +469,11 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
return await this._wrapApiCall(async () => {
const timeoutOptions = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate);
const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate;
const waiter = Waiter.createForEvent(this, event);
using waiter = Waiter.createForEvent(this, event);
waiter.rejectOnTimeout(timeoutOptions, `Timeout ${timeoutOptions.timeout}ms exceeded while waiting for event "${event}"`);
if (event !== Events.BrowserContext.Close)
waiter.rejectOnEvent(this, Events.BrowserContext.Close, () => new TargetClosedError(this._effectiveCloseReason()));
const result = await waiter.waitForEvent(this, event, predicate as any);
waiter.dispose();
return result;
return await waiter.waitForEvent(this, event, predicate as any);
});
}

Expand Down
6 changes: 2 additions & 4 deletions packages/playwright-core/src/client/electron.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,13 +143,11 @@ export class ElectronApplication extends ChannelOwner<channels.ElectronApplicati
return await this._wrapApiCall(async () => {
const timeoutOptions = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate);
const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate;
const waiter = Waiter.createForEvent(this, event);
using waiter = Waiter.createForEvent(this, event);
waiter.rejectOnTimeout(timeoutOptions, `Timeout ${timeoutOptions.timeout}ms exceeded while waiting for event "${event}"`);
if (event !== Events.ElectronApplication.Close)
waiter.rejectOnEvent(this, Events.ElectronApplication.Close, () => new TargetClosedError());
const result = await waiter.waitForEvent(this, event, predicate as any);
waiter.dispose();
return result;
return await waiter.waitForEvent(this, event, predicate as any);
});
}

Expand Down
28 changes: 11 additions & 17 deletions packages/playwright-core/src/client/frame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ export class Frame extends ChannelOwner<channels.FrameChannel> implements api.Fr
async waitForNavigation(options: WaitForNavigationOptions = {}): Promise<network.Response | null> {
return await this._page!._wrapApiCall(async () => {
const waitUntil = verifyLoadState('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil);
const waiter = this._setupNavigationWaiter(options);
using waiter = this._setupNavigationWaiter(options);

const toUrl = typeof options.url === 'string' ? ` to "${options.url}"` : '';
waiter.log(`waiting for navigation${toUrl} until "${waitUntil}"`);
Expand All @@ -169,28 +169,22 @@ export class Frame extends ChannelOwner<channels.FrameChannel> implements api.Fr
}

const request = navigatedEvent.newDocument ? network.Request.fromNullable(navigatedEvent.newDocument.request) : null;
const response = request ? await waiter.waitForPromise(request._finalRequest()._internalResponse()) : null;
waiter.dispose();
return response;
return request ? await waiter.waitForPromise(request._finalRequest()._internalResponse()) : null;
}, { title: 'Wait for navigation' });
}

async waitForLoadState(state: LifecycleEvent = 'load', options?: TimeoutOptions): Promise<void> {
state = verifyLoadState('state', state);
return await this._page!._wrapApiCall(async () => {
const waiter = this._setupNavigationWaiter(options ?? {});
try {
if (this._loadStates.has(state)) {
waiter.log(` not waiting, "${state}" event already fired`);
waiter.throwIfImmediatelyRejected();
} else {
await waiter.waitForEvent<LifecycleEvent>(this._eventEmitter, 'loadstate', s => {
waiter.log(` "${s}" event fired`);
return s === state;
});
}
} finally {
waiter.dispose();
using waiter = this._setupNavigationWaiter(options ?? {});
if (this._loadStates.has(state)) {
waiter.log(` not waiting, "${state}" event already fired`);
waiter.throwIfImmediatelyRejected();
} else {
await waiter.waitForEvent<LifecycleEvent>(this._eventEmitter, 'loadstate', s => {
waiter.log(` "${s}" event fired`);
return s === state;
});
}
}, { title: `Wait for load state "${state}"` });
}
Expand Down
4 changes: 3 additions & 1 deletion packages/playwright-core/src/client/jsHandle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import type * as api from '../../types/types';
import type * as channels from './channels';
import type { Page } from './page';

// Match esbuild's `await using` fallback in browsers without Symbol.asyncDispose.
const kAsyncDispose: typeof Symbol.asyncDispose = (Symbol.asyncDispose || Symbol.for('Symbol.asyncDispose')) as typeof Symbol.asyncDispose;

export class JSHandle<T = any> extends ChannelOwner<channels.JSHandleChannel> implements api.JSHandle {
private _preview: string;
Expand Down Expand Up @@ -79,7 +81,7 @@ export class JSHandle<T = any> extends ChannelOwner<channels.JSHandleChannel> im
return null as any;
}

async [Symbol.asyncDispose]() {
async [kAsyncDispose]() {
await this.dispose();
}

Expand Down
8 changes: 2 additions & 6 deletions packages/playwright-core/src/client/locator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,10 @@ export class Locator implements api.Locator {

return await this._frame._wrapApiCall<R>(async () => {
const result = await this._frame._channel.waitForSelector({ selector: this._selector, strict: true, state: 'attached' }, { signal: options.signal, timeout });
const handle = ElementHandle.fromNullable(result.element) as ElementHandle<SVGElement | HTMLElement> | null;
await using handle = ElementHandle.fromNullable(result.element) as ElementHandle<SVGElement | HTMLElement> | null;
if (!handle)
throw new Error(`Could not resolve ${this._selector} to DOM Element`);
try {
return await task(handle, deadline ? deadline - monotonicTime() : 0);
} finally {
await handle.dispose();
}
return await task(handle, deadline ? deadline - monotonicTime() : 0);
}, { title: options.title, internal: options.internal });
}

Expand Down
6 changes: 2 additions & 4 deletions packages/playwright-core/src/client/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -806,16 +806,14 @@ export class WebSocket extends ChannelOwner<channels.WebSocketChannel> implement
return await this._wrapApiCall(async () => {
const timeoutOptions = this._page._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate);
const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate;
const waiter = Waiter.createForEvent(this, event);
using waiter = Waiter.createForEvent(this, event);
waiter.rejectOnTimeout(timeoutOptions, `Timeout ${timeoutOptions.timeout}ms exceeded while waiting for event "${event}"`);
if (event !== Events.WebSocket.Error)
waiter.rejectOnEvent(this, Events.WebSocket.Error, new Error('Socket error'));
if (event !== Events.WebSocket.Close)
waiter.rejectOnEvent(this, Events.WebSocket.Close, new Error('Socket closed'));
waiter.rejectOnEvent(this._page, Events.Page.Close, () => this._page._closeErrorWithReason());
const result = await waiter.waitForEvent(this, event, predicate as any);
waiter.dispose();
return result;
return await waiter.waitForEvent(this, event, predicate as any);
});
}
}
Expand Down
6 changes: 2 additions & 4 deletions packages/playwright-core/src/client/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -505,17 +505,15 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
return await this._wrapApiCall(async () => {
const timeoutOptions = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate);
const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate;
const waiter = Waiter.createForEvent(this, event);
using waiter = Waiter.createForEvent(this, event);
if (logLine)
waiter.log(logLine);
waiter.rejectOnTimeout(timeoutOptions, `Timeout ${timeoutOptions.timeout}ms exceeded while waiting for event "${event}"`);
if (event !== Events.Page.Crash)
waiter.rejectOnEvent(this, Events.Page.Crash, new Error('Page crashed'));
if (event !== Events.Page.Close)
waiter.rejectOnEvent(this, Events.Page.Close, () => this._closeErrorWithReason());
const result = await waiter.waitForEvent(this, event, predicate as any);
waiter.dispose();
return result;
return await waiter.waitForEvent(this, event, predicate as any);
});
}

Expand Down
8 changes: 7 additions & 1 deletion packages/playwright-core/src/client/waiter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import type * as channels from './channels';
import type { EventEmitter } from 'events';
import type { Zone } from '@utils/zones';

const kDispose: typeof Symbol.dispose = (Symbol.dispose || Symbol.for('Symbol.dispose')) as typeof Symbol.dispose;

export class Waiter {
private _dispose: (() => void)[];
private _failures: Promise<any>[] = [];
Expand Down Expand Up @@ -107,10 +109,14 @@ export class Waiter {
}

dispose() {
for (const dispose of this._dispose)
for (const dispose of this._dispose.splice(0))
dispose();
}

[kDispose]() {
this.dispose();
}

async waitForPromise<T>(promise: Promise<T>, dispose?: () => void): Promise<T> {
try {
if (this._immediateError)
Expand Down
6 changes: 2 additions & 4 deletions packages/playwright-core/src/client/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,11 @@ export class Worker extends ChannelOwner<channels.WorkerChannel> implements api.
const timeoutSettings = this._page?._timeoutSettings ?? this._context?._timeoutSettings ?? new TimeoutSettings();
const timeoutOptions = timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate);
const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate;
const waiter = Waiter.createForEvent(this, event);
using waiter = Waiter.createForEvent(this, event);
waiter.rejectOnTimeout(timeoutOptions, `Timeout ${timeoutOptions.timeout}ms exceeded while waiting for event "${event}"`);
if (event !== Events.Worker.Close)
waiter.rejectOnEvent(this, Events.Worker.Close, () => this._closeErrorWithReason());
const result = await waiter.waitForEvent(this, event, predicate as any);
waiter.dispose();
return result;
return await waiter.waitForEvent(this, event, predicate as any);
});
}

Expand Down
36 changes: 18 additions & 18 deletions packages/playwright-core/src/server/android/backendAdb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,27 +72,23 @@ class AdbDevice implements DeviceBackend {

async function runCommand(command: string, host: string = '127.0.0.1', port: number = 5037, serial?: string): Promise<Buffer> {
debug('pw:adb:runCommand')(command, serial);
const socket = new BufferedSocketWrapper(command, net.createConnection({ host, port }));
try {
if (serial) {
await socket.write(encodeMessage(`host:transport:${serial}`));
const status = await socket.read(4);
assert(status.toString() === 'OKAY', status.toString());
}
await socket.write(encodeMessage(command));
using socket = new BufferedSocketWrapper(command, net.createConnection({ host, port }));
if (serial) {
await socket.write(encodeMessage(`host:transport:${serial}`));
const status = await socket.read(4);
assert(status.toString() === 'OKAY', status.toString());
let commandOutput: Buffer;
if (!command.startsWith('shell:')) {
const remainingLength = parseInt((await socket.read(4)).toString(), 16);
commandOutput = await socket.read(remainingLength);
} else {
commandOutput = await socket.readAll();
}
return commandOutput;
} finally {
socket.close();
}
await socket.write(encodeMessage(command));
const status = await socket.read(4);
assert(status.toString() === 'OKAY', status.toString());
let commandOutput: Buffer;
if (!command.startsWith('shell:')) {
const remainingLength = parseInt((await socket.read(4)).toString(), 16);
commandOutput = await socket.read(remainingLength);
} else {
commandOutput = await socket.readAll();
}
return commandOutput;
}

async function open(command: string, host: string = '127.0.0.1', port: number = 5037, serial?: string): Promise<BufferedSocketWrapper> {
Expand Down Expand Up @@ -161,6 +157,10 @@ class BufferedSocketWrapper extends EventEmitter implements SocketBackend {
this._socket.destroy();
}

[Symbol.dispose]() {
this.close();
}

async read(length: number): Promise<Buffer> {
await this._connectPromise;
assert(!this._isSocket, 'Can not read by length in socket mode');
Expand Down
12 changes: 3 additions & 9 deletions packages/playwright-core/src/server/bidi/bidiInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,19 +51,13 @@ export class RawKeyboardImpl implements input.RawKeyboard {
let frame: Frame | null = this._page._page.mainFrame();
while (frame) {
const context: FrameExecutionContext = await progress.race(frame.mainContext());
const handle = await progress.race(context.evaluateHandle((text: string) => (window as any).__pw_bidiInsertText(text), text));
using handle = await progress.race(context.evaluateHandle((text: string) => (window as any).__pw_bidiInsertText(text), text));
// insertText returns the focused frame element when the focus lives inside a nested frame,
// otherwise the text was inserted (if possible) and we are done.
const element = handle.asElement();
if (!element) {
handle.dispose();
if (!element)
return;
}
try {
frame = await element.contentFrame(progress);
} finally {
element.dispose();
}
frame = await element.contentFrame(progress);
}
}

Expand Down
6 changes: 3 additions & 3 deletions packages/playwright-core/src/server/bidi/bidiPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -535,14 +535,14 @@ export class BidiPage implements PageDelegate {

async getOwnerFrame(handle: dom.ElementHandle): Promise<string | null> {
// TODO: switch to utility world?
const windowHandle = await handle.evaluateHandle(node => {
using windowHandle = await handle.evaluateHandle(node => {
const doc = node.ownerDocument ?? node as Document;
return doc.defaultView;
});
if (!windowHandle)
return null;
const executionContext = toBidiExecutionContext(handle._context);
return executionContext.frameIdForWindowHandle(windowHandle);
return await executionContext.frameIdForWindowHandle(windowHandle);
}

async getBoundingBox(handle: dom.ElementHandle): Promise<types.Rect | null> {
Expand All @@ -566,7 +566,7 @@ export class BidiPage implements PageDelegate {
private async _framePosition(frame: frames.Frame): Promise<types.Point | null> {
if (frame === this._page.mainFrame())
return { x: 0, y: 0 };
const element = await frame.frameElement(nullProgress);
using element = await frame.frameElement(nullProgress);
const box = await element.boundingBox(nullProgress);
if (!box)
return null;
Expand Down
Loading
Loading