puppeteer-core
Version:
A high-level API to control headless Chrome over the DevTools Protocol
231 lines (205 loc) • 6.31 kB
text/typescript
/**
* @license
* Copyright 2022 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type Protocol from 'devtools-protocol';
import type {CDPSession} from '../api/CDPSession.js';
import {DeviceRequestPrompt} from '../api/DeviceRequestPrompt.js';
import type {DeviceRequestPromptDevice} from '../api/DeviceRequestPrompt.js';
import type {WaitTimeoutOptions} from '../api/Page.js';
import type {TimeoutSettings} from '../common/TimeoutSettings.js';
import {assert} from '../util/assert.js';
import {Deferred} from '../util/Deferred.js';
/**
* @internal
*/
export class CdpDeviceRequestPrompt extends DeviceRequestPrompt {
#client: CDPSession | null;
#timeoutSettings: TimeoutSettings;
#id: string;
#handled = false;
#updateDevicesHandle = this.#updateDevices.bind(this);
#waitForDevicePromises = new Set<{
filter: (device: DeviceRequestPromptDevice) => boolean;
promise: Deferred<DeviceRequestPromptDevice>;
}>();
constructor(
client: CDPSession,
timeoutSettings: TimeoutSettings,
firstEvent: Protocol.DeviceAccess.DeviceRequestPromptedEvent,
) {
super();
this.#client = client;
this.#timeoutSettings = timeoutSettings;
this.#id = firstEvent.id;
this.#client.on(
'DeviceAccess.deviceRequestPrompted',
this.#updateDevicesHandle,
);
this.#client.on('Target.detachedFromTarget', () => {
this.#client = null;
});
this.#updateDevices(firstEvent);
}
#updateDevices(event: Protocol.DeviceAccess.DeviceRequestPromptedEvent) {
if (event.id !== this.#id) {
return;
}
for (const rawDevice of event.devices) {
if (
this.devices.some(device => {
return device.id === rawDevice.id;
})
) {
continue;
}
const newDevice = {id: rawDevice.id, name: rawDevice.name};
this.devices.push(newDevice);
for (const waitForDevicePromise of this.#waitForDevicePromises) {
if (waitForDevicePromise.filter(newDevice)) {
waitForDevicePromise.promise.resolve(newDevice);
}
}
}
}
async waitForDevice(
filter: (device: DeviceRequestPromptDevice) => boolean,
options: WaitTimeoutOptions = {},
): Promise<DeviceRequestPromptDevice> {
for (const device of this.devices) {
if (filter(device)) {
return device;
}
}
const {timeout = this.#timeoutSettings.timeout()} = options;
const deferred = Deferred.create<DeviceRequestPromptDevice>({
message: `Waiting for \`DeviceRequestPromptDevice\` failed: ${timeout}ms exceeded`,
timeout,
});
if (options.signal) {
options.signal.addEventListener(
'abort',
() => {
deferred.reject(options.signal?.reason);
},
{once: true},
);
}
const handle = {filter, promise: deferred};
this.#waitForDevicePromises.add(handle);
try {
return await deferred.valueOrThrow();
} finally {
this.#waitForDevicePromises.delete(handle);
}
}
async select(device: DeviceRequestPromptDevice): Promise<void> {
assert(
this.#client !== null,
'Cannot select device through detached session!',
);
assert(this.devices.includes(device), 'Cannot select unknown device!');
assert(
!this.#handled,
'Cannot select DeviceRequestPrompt which is already handled!',
);
this.#client.off(
'DeviceAccess.deviceRequestPrompted',
this.#updateDevicesHandle,
);
this.#handled = true;
return await this.#client.send('DeviceAccess.selectPrompt', {
id: this.#id,
deviceId: device.id,
});
}
async cancel(): Promise<void> {
assert(
this.#client !== null,
'Cannot cancel prompt through detached session!',
);
assert(
!this.#handled,
'Cannot cancel DeviceRequestPrompt which is already handled!',
);
this.#client.off(
'DeviceAccess.deviceRequestPrompted',
this.#updateDevicesHandle,
);
this.#handled = true;
return await this.#client.send('DeviceAccess.cancelPrompt', {id: this.#id});
}
}
/**
* @internal
*/
export class CdpDeviceRequestPromptManager {
#client: CDPSession | null;
#timeoutSettings: TimeoutSettings;
#deviceRequestPromptDeferreds = new Set<Deferred<DeviceRequestPrompt>>();
constructor(client: CDPSession, timeoutSettings: TimeoutSettings) {
this.#client = client;
this.#timeoutSettings = timeoutSettings;
this.#client.on('DeviceAccess.deviceRequestPrompted', event => {
this.#onDeviceRequestPrompted(event);
});
this.#client.on('Target.detachedFromTarget', () => {
this.#client = null;
});
}
async waitForDevicePrompt(
options: WaitTimeoutOptions = {},
): Promise<DeviceRequestPrompt> {
assert(
this.#client !== null,
'Cannot wait for device prompt through detached session!',
);
const needsEnable = this.#deviceRequestPromptDeferreds.size === 0;
let enablePromise: Promise<void> | undefined;
if (needsEnable) {
enablePromise = this.#client.send('DeviceAccess.enable');
}
const {timeout = this.#timeoutSettings.timeout()} = options;
const deferred = Deferred.create<DeviceRequestPrompt>({
message: `Waiting for \`DeviceRequestPrompt\` failed: ${timeout}ms exceeded`,
timeout,
});
if (options.signal) {
options.signal.addEventListener(
'abort',
() => {
deferred.reject(options.signal?.reason);
},
{once: true},
);
}
this.#deviceRequestPromptDeferreds.add(deferred);
try {
const [result] = await Promise.all([
deferred.valueOrThrow(),
enablePromise,
]);
return result;
} finally {
this.#deviceRequestPromptDeferreds.delete(deferred);
}
}
#onDeviceRequestPrompted(
event: Protocol.DeviceAccess.DeviceRequestPromptedEvent,
) {
if (!this.#deviceRequestPromptDeferreds.size) {
return;
}
assert(this.#client !== null);
const devicePrompt = new CdpDeviceRequestPrompt(
this.#client,
this.#timeoutSettings,
event,
);
for (const promise of this.#deviceRequestPromptDeferreds) {
promise.resolve(devicePrompt);
}
this.#deviceRequestPromptDeferreds.clear();
}
}