diff --git a/docs/api/puppeteer.frame.md b/docs/api/puppeteer.frame.md
index ad1806f2..ea076618 100644
--- a/docs/api/puppeteer.frame.md
+++ b/docs/api/puppeteer.frame.md
@@ -90,6 +90,7 @@ console.log(text);
| [title()](./puppeteer.frame.title.md) | | |
| [type(selector, text, options)](./puppeteer.frame.type.md) | | Sends a keydown
, keypress
/input
, and keyup
event for each character in the text. |
| [url()](./puppeteer.frame.url.md) | | |
+| [waitForDevicePrompt(options)](./puppeteer.frame.waitfordeviceprompt.md) | |
This method is typically coupled with an action that triggers a device request from an api such as WebBluetooth.
:::caution
This must be called before the device request is made. It will not return a currently active device prompt.
:::
|
| [waitForFunction(pageFunction, options, args)](./puppeteer.frame.waitforfunction.md) | | |
| [waitForNavigation(options)](./puppeteer.frame.waitfornavigation.md) | | Waits for the frame to navigate. It is useful for when you run code which will indirectly cause the frame to navigate.
Usage of the [History API](https://developer.mozilla.org/en-US/docs/Web/API/History_API) to change the URL is considered a navigation.
|
| [waitForSelector(selector, options)](./puppeteer.frame.waitforselector.md) | | Waits for an element matching the given selector to appear in the frame.
This method works across navigations.
|
diff --git a/docs/api/puppeteer.frame.waitfordeviceprompt.md b/docs/api/puppeteer.frame.waitfordeviceprompt.md
new file mode 100644
index 00000000..e37a0eca
--- /dev/null
+++ b/docs/api/puppeteer.frame.waitfordeviceprompt.md
@@ -0,0 +1,45 @@
+---
+sidebar_label: Frame.waitForDevicePrompt
+---
+
+# Frame.waitForDevicePrompt() method
+
+This method is typically coupled with an action that triggers a device request from an api such as WebBluetooth.
+
+:::caution
+
+This must be called before the device request is made. It will not return a currently active device prompt.
+
+:::
+
+#### Signature:
+
+```typescript
+class Frame {
+ waitForDevicePrompt(
+ options?: WaitTimeoutOptions
+ ): Promise;
+}
+```
+
+## Parameters
+
+| Parameter | Type | Description |
+| --------- | ------------------------------------------------------- | ------------ |
+| options | [WaitTimeoutOptions](./puppeteer.waittimeoutoptions.md) | _(Optional)_ |
+
+**Returns:**
+
+Promise<DeviceRequestPrompt>
+
+## Example
+
+```ts
+const [devicePrompt] = Promise.all([
+ frame.waitForDevicePrompt(),
+ frame.click('#connect-bluetooth'),
+]);
+await devicePrompt.select(
+ await devicePrompt.waitForDevice(({name}) => name.includes('My Device'))
+);
+```
diff --git a/docs/api/puppeteer.page.md b/docs/api/puppeteer.page.md
index 20fb453b..a8bcd627 100644
--- a/docs/api/puppeteer.page.md
+++ b/docs/api/puppeteer.page.md
@@ -149,6 +149,7 @@ page.off('request', logRequest);
| [type(selector, text, options)](./puppeteer.page.type.md) | | Sends a keydown
, keypress/input
, and keyup
event for each character in the text.
To press a special key, like Control
or ArrowDown
, use [Keyboard.press()](./puppeteer.keyboard.press.md).
|
| [url()](./puppeteer.page.url.md) | | |
| [viewport()](./puppeteer.page.viewport.md) | | |
+| [waitForDevicePrompt(options)](./puppeteer.page.waitfordeviceprompt.md) | | This method is typically coupled with an action that triggers a device request from an api such as WebBluetooth.
:::caution
This must be called before the device request is made. It will not return a currently active device prompt.
:::
|
| [waitForFileChooser(options)](./puppeteer.page.waitforfilechooser.md) | | This method is typically coupled with an action that triggers file choosing.
:::caution
This must be called before the file chooser is launched. It will not return a currently active file chooser.
:::
|
| [waitForFrame(urlOrPredicate, options)](./puppeteer.page.waitforframe.md) | | |
| [waitForFunction(pageFunction, options, args)](./puppeteer.page.waitforfunction.md) | | Waits for a function to finish evaluating in the page's context. |
diff --git a/docs/api/puppeteer.page.waitfordeviceprompt.md b/docs/api/puppeteer.page.waitfordeviceprompt.md
new file mode 100644
index 00000000..9b41e3de
--- /dev/null
+++ b/docs/api/puppeteer.page.waitfordeviceprompt.md
@@ -0,0 +1,45 @@
+---
+sidebar_label: Page.waitForDevicePrompt
+---
+
+# Page.waitForDevicePrompt() method
+
+This method is typically coupled with an action that triggers a device request from an api such as WebBluetooth.
+
+:::caution
+
+This must be called before the device request is made. It will not return a currently active device prompt.
+
+:::
+
+#### Signature:
+
+```typescript
+class Page {
+ waitForDevicePrompt(
+ options?: WaitTimeoutOptions
+ ): Promise;
+}
+```
+
+## Parameters
+
+| Parameter | Type | Description |
+| --------- | ------------------------------------------------------- | ------------ |
+| options | [WaitTimeoutOptions](./puppeteer.waittimeoutoptions.md) | _(Optional)_ |
+
+**Returns:**
+
+Promise<DeviceRequestPrompt>
+
+## Example
+
+```ts
+const [devicePrompt] = Promise.all([
+ page.waitForDevicePrompt(),
+ page.click('#connect-bluetooth'),
+]);
+await devicePrompt.select(
+ await devicePrompt.waitForDevice(({name}) => name.includes('My Device'))
+);
+```
diff --git a/packages/puppeteer-core/src/api/Page.ts b/packages/puppeteer-core/src/api/Page.ts
index e826c066..d092a6c1 100644
--- a/packages/puppeteer-core/src/api/Page.ts
+++ b/packages/puppeteer-core/src/api/Page.ts
@@ -24,6 +24,7 @@ import type {Accessibility} from '../common/Accessibility.js';
import type {ConsoleMessage} from '../common/ConsoleMessage.js';
import type {Coverage} from '../common/Coverage.js';
import {Device} from '../common/Device.js';
+import {DeviceRequestPrompt} from '../common/DeviceRequestPrompt.js';
import type {Dialog} from '../common/Dialog.js';
import {EventEmitter, Handler} from '../common/EventEmitter.js';
import type {FileChooser} from '../common/FileChooser.js';
@@ -2558,6 +2559,36 @@ export class Page extends EventEmitter {
>(): Promise>>> {
throw new Error('Not implemented');
}
+
+ /**
+ * This method is typically coupled with an action that triggers a device
+ * request from an api such as WebBluetooth.
+ *
+ * :::caution
+ *
+ * This must be called before the device request is made. It will not return a
+ * currently active device prompt.
+ *
+ * :::
+ *
+ * @example
+ *
+ * ```ts
+ * const [devicePrompt] = Promise.all([
+ * page.waitForDevicePrompt(),
+ * page.click('#connect-bluetooth'),
+ * ]);
+ * await devicePrompt.select(
+ * await devicePrompt.waitForDevice(({name}) => name.includes('My Device'))
+ * );
+ * ```
+ */
+ waitForDevicePrompt(
+ options?: WaitTimeoutOptions
+ ): Promise;
+ waitForDevicePrompt(): Promise {
+ throw new Error('Not implemented');
+ }
}
/**
diff --git a/packages/puppeteer-core/src/common/DeviceRequestPrompt.ts b/packages/puppeteer-core/src/common/DeviceRequestPrompt.ts
new file mode 100644
index 00000000..aa3b1264
--- /dev/null
+++ b/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;
+ }>();
+
+ /**
+ * 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 {
+ for (const device of this.devices) {
+ if (filter(device)) {
+ return device;
+ }
+ }
+
+ const {timeout = this.#timeoutSettings.timeout()} = options;
+ const promise = createDeferredPromise({
+ 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 {
+ 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 {
+ 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
+ >();
+
+ /**
+ * @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 {
+ assert(
+ this.#client !== null,
+ 'Cannot wait for device prompt through detached session!'
+ );
+ const needsEnable = this.#deviceRequestPromptPromises.size === 0;
+ let enablePromise: Promise | undefined;
+ if (needsEnable) {
+ enablePromise = this.#client.send('DeviceAccess.enable');
+ }
+
+ const {timeout = this.#timeoutSettings.timeout()} = options;
+ const promise = createDeferredPromise({
+ 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();
+ }
+}
diff --git a/packages/puppeteer-core/src/common/Frame.ts b/packages/puppeteer-core/src/common/Frame.ts
index 517c70bc..07a86c6b 100644
--- a/packages/puppeteer-core/src/common/Frame.ts
+++ b/packages/puppeteer-core/src/common/Frame.ts
@@ -18,10 +18,15 @@ import {Protocol} from 'devtools-protocol';
import {ElementHandle} from '../api/ElementHandle.js';
import {HTTPResponse} from '../api/HTTPResponse.js';
-import {Page} from '../api/Page.js';
+import {Page, WaitTimeoutOptions} from '../api/Page.js';
+import {assert} from '../util/assert.js';
import {isErrorLike} from '../util/ErrorLike.js';
import {CDPSession} from './Connection.js';
+import {
+ DeviceRequestPrompt,
+ DeviceRequestPromptManager,
+} from './DeviceRequestPrompt.js';
import {ExecutionContext} from './ExecutionContext.js';
import {FrameManager} from './FrameManager.js';
import {getQueryHandlerAndSelector} from './GetQueryHandler.js';
@@ -1083,6 +1088,47 @@ export class Frame {
return this.worlds[PUPPETEER_WORLD].title();
}
+ /**
+ * @internal
+ */
+ _deviceRequestPromptManager(): DeviceRequestPromptManager {
+ if (this.isOOPFrame()) {
+ return this._frameManager._deviceRequestPromptManager(this.#client);
+ }
+ const parentFrame = this.parentFrame();
+ assert(parentFrame !== null);
+ return parentFrame._deviceRequestPromptManager();
+ }
+
+ /**
+ * This method is typically coupled with an action that triggers a device
+ * request from an api such as WebBluetooth.
+ *
+ * :::caution
+ *
+ * This must be called before the device request is made. It will not return a
+ * currently active device prompt.
+ *
+ * :::
+ *
+ * @example
+ *
+ * ```ts
+ * const [devicePrompt] = Promise.all([
+ * frame.waitForDevicePrompt(),
+ * frame.click('#connect-bluetooth'),
+ * ]);
+ * await devicePrompt.select(
+ * await devicePrompt.waitForDevice(({name}) => name.includes('My Device'))
+ * );
+ * ```
+ */
+ waitForDevicePrompt(
+ options: WaitTimeoutOptions = {}
+ ): Promise {
+ return this._deviceRequestPromptManager().waitForDevicePrompt(options);
+ }
+
/**
* @internal
*/
diff --git a/packages/puppeteer-core/src/common/FrameManager.ts b/packages/puppeteer-core/src/common/FrameManager.ts
index 3f07d018..248e2b01 100644
--- a/packages/puppeteer-core/src/common/FrameManager.ts
+++ b/packages/puppeteer-core/src/common/FrameManager.ts
@@ -21,6 +21,7 @@ import {assert} from '../util/assert.js';
import {isErrorLike} from '../util/ErrorLike.js';
import {CDPSession, isTargetClosedError} from './Connection.js';
+import {DeviceRequestPromptManager} from './DeviceRequestPrompt.js';
import {EventEmitter} from './EventEmitter.js';
import {EVALUATION_SCRIPT_URL, ExecutionContext} from './ExecutionContext.js';
import {Frame} from './Frame.js';
@@ -77,6 +78,11 @@ export class FrameManager extends EventEmitter {
*/
#frameNavigatedReceived = new Set();
+ #deviceRequestPromptManagerMap = new WeakMap<
+ CDPSession,
+ DeviceRequestPromptManager
+ >();
+
get timeoutSettings(): TimeoutSettings {
return this.#timeoutSettings;
}
@@ -219,6 +225,18 @@ export class FrameManager extends EventEmitter {
this.initialize(target._session());
}
+ /**
+ * @internal
+ */
+ _deviceRequestPromptManager(client: CDPSession): DeviceRequestPromptManager {
+ let manager = this.#deviceRequestPromptManagerMap.get(client);
+ if (manager === undefined) {
+ manager = new DeviceRequestPromptManager(client, this.#timeoutSettings);
+ this.#deviceRequestPromptManagerMap.set(client, manager);
+ }
+ return manager;
+ }
+
#onLifecycleEvent(event: Protocol.Page.LifecycleEventEvent): void {
const frame = this.frame(event.frameId);
if (!frame) {
diff --git a/packages/puppeteer-core/src/common/Page.ts b/packages/puppeteer-core/src/common/Page.ts
index 60814d8b..fb5c0081 100644
--- a/packages/puppeteer-core/src/common/Page.ts
+++ b/packages/puppeteer-core/src/common/Page.ts
@@ -50,6 +50,7 @@ import {
} from './Connection.js';
import {ConsoleMessage, ConsoleMessageType} from './ConsoleMessage.js';
import {Coverage} from './Coverage.js';
+import {DeviceRequestPrompt} from './DeviceRequestPrompt.js';
import {Dialog} from './Dialog.js';
import {EmulationManager} from './EmulationManager.js';
import {FileChooser} from './FileChooser.js';
@@ -1647,6 +1648,35 @@ export class CDPPage extends Page {
): Promise>>> {
return this.mainFrame().waitForFunction(pageFunction, options, ...args);
}
+
+ /**
+ * This method is typically coupled with an action that triggers a device
+ * request from an api such as WebBluetooth.
+ *
+ * :::caution
+ *
+ * This must be called before the device request is made. It will not return a
+ * currently active device prompt.
+ *
+ * :::
+ *
+ * @example
+ *
+ * ```ts
+ * const [devicePrompt] = Promise.all([
+ * page.waitForDevicePrompt(),
+ * page.click('#connect-bluetooth'),
+ * ]);
+ * await devicePrompt.select(
+ * await devicePrompt.waitForDevice(({name}) => name.includes('My Device'))
+ * );
+ * ```
+ */
+ override waitForDevicePrompt(
+ options: WaitTimeoutOptions = {}
+ ): Promise {
+ return this.mainFrame().waitForDevicePrompt(options);
+ }
}
const supportedMetrics = new Set([
diff --git a/test/src/DeviceRequestPrompt.spec.ts b/test/src/DeviceRequestPrompt.spec.ts
new file mode 100644
index 00000000..b898b209
--- /dev/null
+++ b/test/src/DeviceRequestPrompt.spec.ts
@@ -0,0 +1,457 @@
+import expect from 'expect';
+import {TimeoutError} from 'puppeteer';
+import {
+ DeviceRequestPrompt,
+ DeviceRequestPromptDevice,
+ DeviceRequestPromptManager,
+} from 'puppeteer-core/internal/common/DeviceRequestPrompt.js';
+import {EventEmitter} from 'puppeteer-core/internal/common/EventEmitter.js';
+import {TimeoutSettings} from 'puppeteer-core/internal/common/TimeoutSettings.js';
+
+class MockCDPSession extends EventEmitter {
+ async send(): Promise {}
+ connection() {
+ return undefined;
+ }
+ async detach() {}
+ id() {
+ return '1';
+ }
+}
+
+describe('DeviceRequestPrompt', function () {
+ describe('waitForDevicePrompt', function () {
+ it('should return prompt', async () => {
+ const client = new MockCDPSession();
+ const timeoutSettings = new TimeoutSettings();
+ const manager = new DeviceRequestPromptManager(client, timeoutSettings);
+
+ const [prompt] = await Promise.all([
+ manager.waitForDevicePrompt(),
+ (() => {
+ client.emit('DeviceAccess.deviceRequestPrompted', {
+ id: '00000000000000000000000000000000',
+ devices: [],
+ });
+ })(),
+ ]);
+ expect(prompt).toBeTruthy();
+ });
+
+ it('should respect timeout', async () => {
+ const client = new MockCDPSession();
+ const timeoutSettings = new TimeoutSettings();
+ const manager = new DeviceRequestPromptManager(client, timeoutSettings);
+
+ await expect(
+ manager.waitForDevicePrompt({timeout: 1})
+ ).rejects.toBeInstanceOf(TimeoutError);
+ });
+
+ it('should respect default timeout when there is no custom timeout', async () => {
+ const client = new MockCDPSession();
+ const timeoutSettings = new TimeoutSettings();
+ const manager = new DeviceRequestPromptManager(client, timeoutSettings);
+
+ timeoutSettings.setDefaultTimeout(1);
+ await expect(manager.waitForDevicePrompt()).rejects.toBeInstanceOf(
+ TimeoutError
+ );
+ });
+
+ it('should prioritize exact timeout over default timeout', async () => {
+ const client = new MockCDPSession();
+ const timeoutSettings = new TimeoutSettings();
+ const manager = new DeviceRequestPromptManager(client, timeoutSettings);
+
+ timeoutSettings.setDefaultTimeout(0);
+ await expect(
+ manager.waitForDevicePrompt({timeout: 1})
+ ).rejects.toBeInstanceOf(TimeoutError);
+ });
+
+ it('should work with no timeout', async () => {
+ const client = new MockCDPSession();
+ const timeoutSettings = new TimeoutSettings();
+ const manager = new DeviceRequestPromptManager(client, timeoutSettings);
+
+ const [prompt] = await Promise.all([
+ manager.waitForDevicePrompt({timeout: 0}),
+ (async () => {
+ await new Promise(resolve => {
+ setTimeout(resolve, 50);
+ });
+ client.emit('DeviceAccess.deviceRequestPrompted', {
+ id: '00000000000000000000000000000000',
+ devices: [],
+ });
+ })(),
+ ]);
+ expect(prompt).toBeTruthy();
+ });
+
+ it('should return the same prompt when there are many watchdogs simultaneously', async () => {
+ const client = new MockCDPSession();
+ const timeoutSettings = new TimeoutSettings();
+ const manager = new DeviceRequestPromptManager(client, timeoutSettings);
+
+ const [prompt1, prompt2] = await Promise.all([
+ manager.waitForDevicePrompt(),
+ manager.waitForDevicePrompt(),
+ (() => {
+ client.emit('DeviceAccess.deviceRequestPrompted', {
+ id: '00000000000000000000000000000000',
+ devices: [],
+ });
+ })(),
+ ]);
+ expect(prompt1 === prompt2).toBeTruthy();
+ });
+
+ it('should listen and shortcut when there are no watchdogs', async () => {
+ const client = new MockCDPSession();
+ const timeoutSettings = new TimeoutSettings();
+ const manager = new DeviceRequestPromptManager(client, timeoutSettings);
+
+ client.emit('DeviceAccess.deviceRequestPrompted', {
+ id: '00000000000000000000000000000000',
+ devices: [],
+ });
+
+ expect(manager).toBeTruthy();
+ });
+ });
+
+ describe('DeviceRequestPrompt.devices', function () {
+ it('lists devices as they arrive', function () {
+ const client = new MockCDPSession();
+ const timeoutSettings = new TimeoutSettings();
+ const prompt = new DeviceRequestPrompt(client, timeoutSettings, {
+ id: '00000000000000000000000000000000',
+ devices: [],
+ });
+
+ expect(prompt.devices).toHaveLength(0);
+ client.emit('DeviceAccess.deviceRequestPrompted', {
+ id: '00000000000000000000000000000000',
+ devices: [{id: '00000000', name: 'Device 0'}],
+ });
+ expect(prompt.devices).toHaveLength(1);
+ client.emit('DeviceAccess.deviceRequestPrompted', {
+ id: '00000000000000000000000000000000',
+ devices: [
+ {id: '00000000', name: 'Device 0'},
+ {id: '11111111', name: 'Device 1'},
+ ],
+ });
+ expect(prompt.devices).toHaveLength(2);
+ expect(prompt.devices[0]).toBeInstanceOf(DeviceRequestPromptDevice);
+ expect(prompt.devices[1]).toBeInstanceOf(DeviceRequestPromptDevice);
+ });
+
+ it('does not list devices from events of another prompt', function () {
+ const client = new MockCDPSession();
+ const timeoutSettings = new TimeoutSettings();
+ const prompt = new DeviceRequestPrompt(client, timeoutSettings, {
+ id: '00000000000000000000000000000000',
+ devices: [],
+ });
+
+ expect(prompt.devices).toHaveLength(0);
+ client.emit('DeviceAccess.deviceRequestPrompted', {
+ id: '88888888888888888888888888888888',
+ devices: [
+ {id: '00000000', name: 'Device 0'},
+ {id: '11111111', name: 'Device 1'},
+ ],
+ });
+ expect(prompt.devices).toHaveLength(0);
+ });
+ });
+
+ describe('DeviceRequestPrompt.waitForDevice', function () {
+ it('should return first matching device', async () => {
+ const client = new MockCDPSession();
+ const timeoutSettings = new TimeoutSettings();
+ const prompt = new DeviceRequestPrompt(client, timeoutSettings, {
+ id: '00000000000000000000000000000000',
+ devices: [],
+ });
+
+ const [device] = await Promise.all([
+ prompt.waitForDevice(({name}) => {
+ return name.includes('1');
+ }),
+ (() => {
+ client.emit('DeviceAccess.deviceRequestPrompted', {
+ id: '00000000000000000000000000000000',
+ devices: [{id: '00000000', name: 'Device 0'}],
+ });
+ client.emit('DeviceAccess.deviceRequestPrompted', {
+ id: '00000000000000000000000000000000',
+ devices: [
+ {id: '00000000', name: 'Device 0'},
+ {id: '11111111', name: 'Device 1'},
+ ],
+ });
+ })(),
+ ]);
+ expect(device).toBeInstanceOf(DeviceRequestPromptDevice);
+ });
+
+ it('should return first matching device from already known devices', async () => {
+ const client = new MockCDPSession();
+ const timeoutSettings = new TimeoutSettings();
+ const prompt = new DeviceRequestPrompt(client, timeoutSettings, {
+ id: '00000000000000000000000000000000',
+ devices: [
+ {id: '00000000', name: 'Device 0'},
+ {id: '11111111', name: 'Device 1'},
+ ],
+ });
+
+ const device = await prompt.waitForDevice(({name}) => {
+ return name.includes('1');
+ });
+ expect(device).toBeInstanceOf(DeviceRequestPromptDevice);
+ });
+
+ it('should return device in the devices list', async () => {
+ const client = new MockCDPSession();
+ const timeoutSettings = new TimeoutSettings();
+ const prompt = new DeviceRequestPrompt(client, timeoutSettings, {
+ id: '00000000000000000000000000000000',
+ devices: [],
+ });
+
+ const [device] = await Promise.all([
+ prompt.waitForDevice(({name}) => {
+ return name.includes('1');
+ }),
+ (() => {
+ client.emit('DeviceAccess.deviceRequestPrompted', {
+ id: '00000000000000000000000000000000',
+ devices: [
+ {id: '00000000', name: 'Device 0'},
+ {id: '11111111', name: 'Device 1'},
+ ],
+ });
+ })(),
+ ]);
+ expect(prompt.devices).toContain(device);
+ });
+
+ it('should respect timeout', async () => {
+ const client = new MockCDPSession();
+ const timeoutSettings = new TimeoutSettings();
+ const prompt = new DeviceRequestPrompt(client, timeoutSettings, {
+ id: '00000000000000000000000000000000',
+ devices: [],
+ });
+
+ await expect(
+ prompt.waitForDevice(
+ ({name}) => {
+ return name.includes('Device');
+ },
+ {timeout: 1}
+ )
+ ).rejects.toBeInstanceOf(TimeoutError);
+ });
+
+ it('should respect default timeout when there is no custom timeout', async () => {
+ const client = new MockCDPSession();
+ const timeoutSettings = new TimeoutSettings();
+ const prompt = new DeviceRequestPrompt(client, timeoutSettings, {
+ id: '00000000000000000000000000000000',
+ devices: [],
+ });
+
+ timeoutSettings.setDefaultTimeout(1);
+ await expect(
+ prompt.waitForDevice(
+ ({name}) => {
+ return name.includes('Device');
+ },
+ {timeout: 1}
+ )
+ ).rejects.toBeInstanceOf(TimeoutError);
+ });
+
+ it('should prioritize exact timeout over default timeout', async () => {
+ const client = new MockCDPSession();
+ const timeoutSettings = new TimeoutSettings();
+ const prompt = new DeviceRequestPrompt(client, timeoutSettings, {
+ id: '00000000000000000000000000000000',
+ devices: [],
+ });
+
+ timeoutSettings.setDefaultTimeout(0);
+ await expect(
+ prompt.waitForDevice(
+ ({name}) => {
+ return name.includes('Device');
+ },
+ {timeout: 1}
+ )
+ ).rejects.toBeInstanceOf(TimeoutError);
+ });
+
+ it('should work with no timeout', async () => {
+ const client = new MockCDPSession();
+ const timeoutSettings = new TimeoutSettings();
+ const prompt = new DeviceRequestPrompt(client, timeoutSettings, {
+ id: '00000000000000000000000000000000',
+ devices: [],
+ });
+
+ const [device] = await Promise.all([
+ prompt.waitForDevice(
+ ({name}) => {
+ return name.includes('1');
+ },
+ {timeout: 0}
+ ),
+ (() => {
+ client.emit('DeviceAccess.deviceRequestPrompted', {
+ id: '00000000000000000000000000000000',
+ devices: [{id: '00000000', name: 'Device 0'}],
+ });
+ client.emit('DeviceAccess.deviceRequestPrompted', {
+ id: '00000000000000000000000000000000',
+ devices: [
+ {id: '00000000', name: 'Device 0'},
+ {id: '11111111', name: 'Device 1'},
+ ],
+ });
+ })(),
+ ]);
+ expect(device).toBeInstanceOf(DeviceRequestPromptDevice);
+ });
+
+ it('should return same device from multiple watchdogs', async () => {
+ const client = new MockCDPSession();
+ const timeoutSettings = new TimeoutSettings();
+ const prompt = new DeviceRequestPrompt(client, timeoutSettings, {
+ id: '00000000000000000000000000000000',
+ devices: [],
+ });
+
+ const [device1, device2] = await Promise.all([
+ prompt.waitForDevice(({name}) => {
+ return name.includes('1');
+ }),
+ prompt.waitForDevice(({name}) => {
+ return name.includes('1');
+ }),
+ (() => {
+ client.emit('DeviceAccess.deviceRequestPrompted', {
+ id: '00000000000000000000000000000000',
+ devices: [{id: '00000000', name: 'Device 0'}],
+ });
+ client.emit('DeviceAccess.deviceRequestPrompted', {
+ id: '00000000000000000000000000000000',
+ devices: [
+ {id: '00000000', name: 'Device 0'},
+ {id: '11111111', name: 'Device 1'},
+ ],
+ });
+ })(),
+ ]);
+ expect(device1 === device2).toBeTruthy();
+ });
+ });
+
+ describe('DeviceRequestPrompt.select', function () {
+ it('should succeed with listed device', async () => {
+ const client = new MockCDPSession();
+ const timeoutSettings = new TimeoutSettings();
+ const prompt = new DeviceRequestPrompt(client, timeoutSettings, {
+ id: '00000000000000000000000000000000',
+ devices: [],
+ });
+
+ const [device] = await Promise.all([
+ prompt.waitForDevice(({name}) => {
+ return name.includes('1');
+ }),
+ (() => {
+ client.emit('DeviceAccess.deviceRequestPrompted', {
+ id: '00000000000000000000000000000000',
+ devices: [
+ {id: '00000000', name: 'Device 0'},
+ {id: '11111111', name: 'Device 1'},
+ ],
+ });
+ })(),
+ ]);
+ await prompt.select(device);
+ });
+
+ it('should error for device not listed in devices', async () => {
+ const client = new MockCDPSession();
+ const timeoutSettings = new TimeoutSettings();
+ const prompt = new DeviceRequestPrompt(client, timeoutSettings, {
+ id: '00000000000000000000000000000000',
+ devices: [],
+ });
+
+ await expect(
+ prompt.select(new DeviceRequestPromptDevice('11111111', 'Device 1'))
+ ).rejects.toThrowError('Cannot select unknown device!');
+ });
+
+ it('should fail when selecting prompt twice', async () => {
+ const client = new MockCDPSession();
+ const timeoutSettings = new TimeoutSettings();
+ const prompt = new DeviceRequestPrompt(client, timeoutSettings, {
+ id: '00000000000000000000000000000000',
+ devices: [],
+ });
+
+ const [device] = await Promise.all([
+ prompt.waitForDevice(({name}) => {
+ return name.includes('1');
+ }),
+ (() => {
+ client.emit('DeviceAccess.deviceRequestPrompted', {
+ id: '00000000000000000000000000000000',
+ devices: [
+ {id: '00000000', name: 'Device 0'},
+ {id: '11111111', name: 'Device 1'},
+ ],
+ });
+ })(),
+ ]);
+ await prompt.select(device);
+ await expect(prompt.select(device)).rejects.toThrowError(
+ 'Cannot select DeviceRequestPrompt which is already handled!'
+ );
+ });
+ });
+
+ describe('DeviceRequestPrompt.cancel', function () {
+ it('should succeed on first call', async () => {
+ const client = new MockCDPSession();
+ const timeoutSettings = new TimeoutSettings();
+ const prompt = new DeviceRequestPrompt(client, timeoutSettings, {
+ id: '00000000000000000000000000000000',
+ devices: [],
+ });
+ await prompt.cancel();
+ });
+
+ it('should fail when canceling prompt twice', async () => {
+ const client = new MockCDPSession();
+ const timeoutSettings = new TimeoutSettings();
+ const prompt = new DeviceRequestPrompt(client, timeoutSettings, {
+ id: '00000000000000000000000000000000',
+ devices: [],
+ });
+ await prompt.cancel();
+ await expect(prompt.cancel()).rejects.toThrowError(
+ 'Cannot cancel DeviceRequestPrompt which is already handled!'
+ );
+ });
+ });
+});