diff options
Diffstat (limited to 'remote/test/puppeteer/packages/puppeteer-core/src/common/DeviceRequestPrompt.ts')
-rw-r--r-- | remote/test/puppeteer/packages/puppeteer-core/src/common/DeviceRequestPrompt.ts | 293 |
1 files changed, 293 insertions, 0 deletions
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/DeviceRequestPrompt.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/DeviceRequestPrompt.ts new file mode 100644 index 0000000000..aa3b1264c1 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/DeviceRequestPrompt.ts @@ -0,0 +1,293 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Protocol from 'devtools-protocol'; + +import {WaitTimeoutOptions} from '../api/Page.js'; +import {assert} from '../util/assert.js'; +import { + createDeferredPromise, + DeferredPromise, +} from '../util/DeferredPromise.js'; + +import {CDPSession} from './Connection.js'; +import {TimeoutSettings} from './TimeoutSettings.js'; + +/** + * Device in a request prompt. + * + * @public + */ +export class DeviceRequestPromptDevice { + /** + * Device id during a prompt. + */ + id: string; + + /** + * Device name as it appears in a prompt. + */ + name: string; + + /** + * @internal + */ + constructor(id: string, name: string) { + this.id = id; + this.name = name; + } +} + +/** + * Device request prompts let you respond to the page requesting for a device + * through an API like WebBluetooth. + * + * @remarks + * `DeviceRequestPrompt` instances are returned via the + * {@link Page.waitForDevicePrompt} method. + * + * @example + * + * ```ts + * const [deviceRequest] = Promise.all([ + * page.waitForDevicePrompt(), + * page.click('#connect-bluetooth'), + * ]); + * await devicePrompt.select( + * await devicePrompt.waitForDevice(({name}) => name.includes('My Device')) + * ); + * ``` + * + * @public + */ +export class DeviceRequestPrompt { + #client: CDPSession | null; + #timeoutSettings: TimeoutSettings; + #id: string; + #handled = false; + #updateDevicesHandle = this.#updateDevices.bind(this); + #waitForDevicePromises = new Set<{ + filter: (device: DeviceRequestPromptDevice) => boolean; + promise: DeferredPromise<DeviceRequestPromptDevice>; + }>(); + + /** + * Current list of selectable devices. + */ + devices: DeviceRequestPromptDevice[] = []; + + /** + * @internal + */ + constructor( + client: CDPSession, + timeoutSettings: TimeoutSettings, + firstEvent: Protocol.DeviceAccess.DeviceRequestPromptedEvent + ) { + 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 = new DeviceRequestPromptDevice( + rawDevice.id, + rawDevice.name + ); + this.devices.push(newDevice); + + for (const waitForDevicePromise of this.#waitForDevicePromises) { + if (waitForDevicePromise.filter(newDevice)) { + waitForDevicePromise.promise.resolve(newDevice); + } + } + } + } + + /** + * Resolve to the first device in the prompt matching a filter. + */ + 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 promise = createDeferredPromise<DeviceRequestPromptDevice>({ + message: `Waiting for \`DeviceRequestPromptDevice\` failed: ${timeout}ms exceeded`, + timeout, + }); + const handle = {filter, promise}; + this.#waitForDevicePromises.add(handle); + try { + return await promise; + } finally { + this.#waitForDevicePromises.delete(handle); + } + } + + /** + * Select a device in the prompt's list. + */ + 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 this.#client.send('DeviceAccess.selectPrompt', { + id: this.#id, + deviceId: device.id, + }); + } + + /** + * Cancel the prompt. + */ + 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 this.#client.send('DeviceAccess.cancelPrompt', {id: this.#id}); + } +} + +/** + * @internal + */ +export class DeviceRequestPromptManager { + #client: CDPSession | null; + #timeoutSettings: TimeoutSettings; + #deviceRequestPromptPromises = new Set< + DeferredPromise<DeviceRequestPrompt> + >(); + + /** + * @internal + */ + 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; + }); + } + + /** + * Wait for device prompt created by an action like calling WebBluetooth's + * requestDevice. + */ + async waitForDevicePrompt( + options: WaitTimeoutOptions = {} + ): Promise<DeviceRequestPrompt> { + assert( + this.#client !== null, + 'Cannot wait for device prompt through detached session!' + ); + const needsEnable = this.#deviceRequestPromptPromises.size === 0; + let enablePromise: Promise<void> | undefined; + if (needsEnable) { + enablePromise = this.#client.send('DeviceAccess.enable'); + } + + const {timeout = this.#timeoutSettings.timeout()} = options; + const promise = createDeferredPromise<DeviceRequestPrompt>({ + message: `Waiting for \`DeviceRequestPrompt\` failed: ${timeout}ms exceeded`, + timeout, + }); + this.#deviceRequestPromptPromises.add(promise); + + try { + const [result] = await Promise.all([promise, enablePromise]); + return result; + } finally { + this.#deviceRequestPromptPromises.delete(promise); + } + } + + /** + * @internal + */ + #onDeviceRequestPrompted( + event: Protocol.DeviceAccess.DeviceRequestPromptedEvent + ) { + if (!this.#deviceRequestPromptPromises.size) { + return; + } + + assert(this.#client !== null); + const devicePrompt = new DeviceRequestPrompt( + this.#client, + this.#timeoutSettings, + event + ); + for (const promise of this.#deviceRequestPromptPromises) { + promise.resolve(devicePrompt); + } + this.#deviceRequestPromptPromises.clear(); + } +} |