summaryrefslogtreecommitdiffstats
path: root/remote/test/puppeteer/packages/puppeteer-core/src/common/DeviceRequestPrompt.ts
diff options
context:
space:
mode:
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.ts293
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();
+ }
+}