feat: add Page.waitForDevicePrompt (#9299)

Co-authored-by: Alex Rudenko <OrKoN@users.noreply.github.com>
Co-authored-by: Alex Rudenko <alexrudenko@chromium.org>
This commit is contained in:
Michael "Z" Goddard 2023-03-21 05:21:10 -04:00 committed by GitHub
parent d6e5aeeff5
commit a5149d52f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 968 additions and 1 deletions

View File

@ -90,6 +90,7 @@ console.log(text);
| [title()](./puppeteer.frame.title.md) | | | | [title()](./puppeteer.frame.title.md) | | |
| [type(selector, text, options)](./puppeteer.frame.type.md) | | Sends a <code>keydown</code>, <code>keypress</code>/<code>input</code>, and <code>keyup</code> event for each character in the text. | | [type(selector, text, options)](./puppeteer.frame.type.md) | | Sends a <code>keydown</code>, <code>keypress</code>/<code>input</code>, and <code>keyup</code> event for each character in the text. |
| [url()](./puppeteer.frame.url.md) | | | | [url()](./puppeteer.frame.url.md) | | |
| [waitForDevicePrompt(options)](./puppeteer.frame.waitfordeviceprompt.md) | | <p>This method is typically coupled with an action that triggers a device request from an api such as WebBluetooth.</p><p>:::caution</p><p>This must be called before the device request is made. It will not return a currently active device prompt.</p><p>:::</p> |
| [waitForFunction(pageFunction, options, args)](./puppeteer.frame.waitforfunction.md) | | | | [waitForFunction(pageFunction, options, args)](./puppeteer.frame.waitforfunction.md) | | |
| [waitForNavigation(options)](./puppeteer.frame.waitfornavigation.md) | | <p>Waits for the frame to navigate. It is useful for when you run code which will indirectly cause the frame to navigate.</p><p>Usage of the [History API](https://developer.mozilla.org/en-US/docs/Web/API/History_API) to change the URL is considered a navigation.</p> | | [waitForNavigation(options)](./puppeteer.frame.waitfornavigation.md) | | <p>Waits for the frame to navigate. It is useful for when you run code which will indirectly cause the frame to navigate.</p><p>Usage of the [History API](https://developer.mozilla.org/en-US/docs/Web/API/History_API) to change the URL is considered a navigation.</p> |
| [waitForSelector(selector, options)](./puppeteer.frame.waitforselector.md) | | <p>Waits for an element matching the given selector to appear in the frame.</p><p>This method works across navigations.</p> | | [waitForSelector(selector, options)](./puppeteer.frame.waitforselector.md) | | <p>Waits for an element matching the given selector to appear in the frame.</p><p>This method works across navigations.</p> |

View File

@ -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<DeviceRequestPrompt>;
}
```
## Parameters
| Parameter | Type | Description |
| --------- | ------------------------------------------------------- | ------------ |
| options | [WaitTimeoutOptions](./puppeteer.waittimeoutoptions.md) | _(Optional)_ |
**Returns:**
Promise&lt;DeviceRequestPrompt&gt;
## Example
```ts
const [devicePrompt] = Promise.all([
frame.waitForDevicePrompt(),
frame.click('#connect-bluetooth'),
]);
await devicePrompt.select(
await devicePrompt.waitForDevice(({name}) => name.includes('My Device'))
);
```

View File

@ -149,6 +149,7 @@ page.off('request', logRequest);
| [type(selector, text, options)](./puppeteer.page.type.md) | | <p>Sends a <code>keydown</code>, <code>keypress/input</code>, and <code>keyup</code> event for each character in the text.</p><p>To press a special key, like <code>Control</code> or <code>ArrowDown</code>, use [Keyboard.press()](./puppeteer.keyboard.press.md).</p> | | [type(selector, text, options)](./puppeteer.page.type.md) | | <p>Sends a <code>keydown</code>, <code>keypress/input</code>, and <code>keyup</code> event for each character in the text.</p><p>To press a special key, like <code>Control</code> or <code>ArrowDown</code>, use [Keyboard.press()](./puppeteer.keyboard.press.md).</p> |
| [url()](./puppeteer.page.url.md) | | | | [url()](./puppeteer.page.url.md) | | |
| [viewport()](./puppeteer.page.viewport.md) | | | | [viewport()](./puppeteer.page.viewport.md) | | |
| [waitForDevicePrompt(options)](./puppeteer.page.waitfordeviceprompt.md) | | <p>This method is typically coupled with an action that triggers a device request from an api such as WebBluetooth.</p><p>:::caution</p><p>This must be called before the device request is made. It will not return a currently active device prompt.</p><p>:::</p> |
| [waitForFileChooser(options)](./puppeteer.page.waitforfilechooser.md) | | <p>This method is typically coupled with an action that triggers file choosing.</p><p>:::caution</p><p>This must be called before the file chooser is launched. It will not return a currently active file chooser.</p><p>:::</p> | | [waitForFileChooser(options)](./puppeteer.page.waitforfilechooser.md) | | <p>This method is typically coupled with an action that triggers file choosing.</p><p>:::caution</p><p>This must be called before the file chooser is launched. It will not return a currently active file chooser.</p><p>:::</p> |
| [waitForFrame(urlOrPredicate, options)](./puppeteer.page.waitforframe.md) | | | | [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. | | [waitForFunction(pageFunction, options, args)](./puppeteer.page.waitforfunction.md) | | Waits for a function to finish evaluating in the page's context. |

View File

@ -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<DeviceRequestPrompt>;
}
```
## Parameters
| Parameter | Type | Description |
| --------- | ------------------------------------------------------- | ------------ |
| options | [WaitTimeoutOptions](./puppeteer.waittimeoutoptions.md) | _(Optional)_ |
**Returns:**
Promise&lt;DeviceRequestPrompt&gt;
## Example
```ts
const [devicePrompt] = Promise.all([
page.waitForDevicePrompt(),
page.click('#connect-bluetooth'),
]);
await devicePrompt.select(
await devicePrompt.waitForDevice(({name}) => name.includes('My Device'))
);
```

View File

@ -24,6 +24,7 @@ import type {Accessibility} from '../common/Accessibility.js';
import type {ConsoleMessage} from '../common/ConsoleMessage.js'; import type {ConsoleMessage} from '../common/ConsoleMessage.js';
import type {Coverage} from '../common/Coverage.js'; import type {Coverage} from '../common/Coverage.js';
import {Device} from '../common/Device.js'; import {Device} from '../common/Device.js';
import {DeviceRequestPrompt} from '../common/DeviceRequestPrompt.js';
import type {Dialog} from '../common/Dialog.js'; import type {Dialog} from '../common/Dialog.js';
import {EventEmitter, Handler} from '../common/EventEmitter.js'; import {EventEmitter, Handler} from '../common/EventEmitter.js';
import type {FileChooser} from '../common/FileChooser.js'; import type {FileChooser} from '../common/FileChooser.js';
@ -2558,6 +2559,36 @@ export class Page extends EventEmitter {
>(): Promise<HandleFor<Awaited<ReturnType<Func>>>> { >(): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
throw new Error('Not implemented'); 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<DeviceRequestPrompt>;
waitForDevicePrompt(): Promise<DeviceRequestPrompt> {
throw new Error('Not implemented');
}
} }
/** /**

View File

@ -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();
}
}

View File

@ -18,10 +18,15 @@ import {Protocol} from 'devtools-protocol';
import {ElementHandle} from '../api/ElementHandle.js'; import {ElementHandle} from '../api/ElementHandle.js';
import {HTTPResponse} from '../api/HTTPResponse.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 {isErrorLike} from '../util/ErrorLike.js';
import {CDPSession} from './Connection.js'; import {CDPSession} from './Connection.js';
import {
DeviceRequestPrompt,
DeviceRequestPromptManager,
} from './DeviceRequestPrompt.js';
import {ExecutionContext} from './ExecutionContext.js'; import {ExecutionContext} from './ExecutionContext.js';
import {FrameManager} from './FrameManager.js'; import {FrameManager} from './FrameManager.js';
import {getQueryHandlerAndSelector} from './GetQueryHandler.js'; import {getQueryHandlerAndSelector} from './GetQueryHandler.js';
@ -1083,6 +1088,47 @@ export class Frame {
return this.worlds[PUPPETEER_WORLD].title(); 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<DeviceRequestPrompt> {
return this._deviceRequestPromptManager().waitForDevicePrompt(options);
}
/** /**
* @internal * @internal
*/ */

View File

@ -21,6 +21,7 @@ import {assert} from '../util/assert.js';
import {isErrorLike} from '../util/ErrorLike.js'; import {isErrorLike} from '../util/ErrorLike.js';
import {CDPSession, isTargetClosedError} from './Connection.js'; import {CDPSession, isTargetClosedError} from './Connection.js';
import {DeviceRequestPromptManager} from './DeviceRequestPrompt.js';
import {EventEmitter} from './EventEmitter.js'; import {EventEmitter} from './EventEmitter.js';
import {EVALUATION_SCRIPT_URL, ExecutionContext} from './ExecutionContext.js'; import {EVALUATION_SCRIPT_URL, ExecutionContext} from './ExecutionContext.js';
import {Frame} from './Frame.js'; import {Frame} from './Frame.js';
@ -77,6 +78,11 @@ export class FrameManager extends EventEmitter {
*/ */
#frameNavigatedReceived = new Set<string>(); #frameNavigatedReceived = new Set<string>();
#deviceRequestPromptManagerMap = new WeakMap<
CDPSession,
DeviceRequestPromptManager
>();
get timeoutSettings(): TimeoutSettings { get timeoutSettings(): TimeoutSettings {
return this.#timeoutSettings; return this.#timeoutSettings;
} }
@ -219,6 +225,18 @@ export class FrameManager extends EventEmitter {
this.initialize(target._session()); 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 { #onLifecycleEvent(event: Protocol.Page.LifecycleEventEvent): void {
const frame = this.frame(event.frameId); const frame = this.frame(event.frameId);
if (!frame) { if (!frame) {

View File

@ -50,6 +50,7 @@ import {
} from './Connection.js'; } from './Connection.js';
import {ConsoleMessage, ConsoleMessageType} from './ConsoleMessage.js'; import {ConsoleMessage, ConsoleMessageType} from './ConsoleMessage.js';
import {Coverage} from './Coverage.js'; import {Coverage} from './Coverage.js';
import {DeviceRequestPrompt} from './DeviceRequestPrompt.js';
import {Dialog} from './Dialog.js'; import {Dialog} from './Dialog.js';
import {EmulationManager} from './EmulationManager.js'; import {EmulationManager} from './EmulationManager.js';
import {FileChooser} from './FileChooser.js'; import {FileChooser} from './FileChooser.js';
@ -1647,6 +1648,35 @@ export class CDPPage extends Page {
): Promise<HandleFor<Awaited<ReturnType<Func>>>> { ): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
return this.mainFrame().waitForFunction(pageFunction, options, ...args); 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<DeviceRequestPrompt> {
return this.mainFrame().waitForDevicePrompt(options);
}
} }
const supportedMetrics = new Set<string>([ const supportedMetrics = new Set<string>([

View File

@ -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<any> {}
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!'
);
});
});
});