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:
parent
d6e5aeeff5
commit
a5149d52f5
@ -90,6 +90,7 @@ console.log(text);
|
||||
| [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. |
|
||||
| [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) | | |
|
||||
| [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> |
|
||||
|
45
docs/api/puppeteer.frame.waitfordeviceprompt.md
Normal file
45
docs/api/puppeteer.frame.waitfordeviceprompt.md
Normal 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<DeviceRequestPrompt>
|
||||
|
||||
## Example
|
||||
|
||||
```ts
|
||||
const [devicePrompt] = Promise.all([
|
||||
frame.waitForDevicePrompt(),
|
||||
frame.click('#connect-bluetooth'),
|
||||
]);
|
||||
await devicePrompt.select(
|
||||
await devicePrompt.waitForDevice(({name}) => name.includes('My Device'))
|
||||
);
|
||||
```
|
@ -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> |
|
||||
| [url()](./puppeteer.page.url.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> |
|
||||
| [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. |
|
||||
|
45
docs/api/puppeteer.page.waitfordeviceprompt.md
Normal file
45
docs/api/puppeteer.page.waitfordeviceprompt.md
Normal 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<DeviceRequestPrompt>
|
||||
|
||||
## Example
|
||||
|
||||
```ts
|
||||
const [devicePrompt] = Promise.all([
|
||||
page.waitForDevicePrompt(),
|
||||
page.click('#connect-bluetooth'),
|
||||
]);
|
||||
await devicePrompt.select(
|
||||
await devicePrompt.waitForDevice(({name}) => name.includes('My Device'))
|
||||
);
|
||||
```
|
@ -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<HandleFor<Awaited<ReturnType<Func>>>> {
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
293
packages/puppeteer-core/src/common/DeviceRequestPrompt.ts
Normal file
293
packages/puppeteer-core/src/common/DeviceRequestPrompt.ts
Normal 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();
|
||||
}
|
||||
}
|
@ -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<DeviceRequestPrompt> {
|
||||
return this._deviceRequestPromptManager().waitForDevicePrompt(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
|
@ -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<string>();
|
||||
|
||||
#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) {
|
||||
|
@ -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<HandleFor<Awaited<ReturnType<Func>>>> {
|
||||
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>([
|
||||
|
457
test/src/DeviceRequestPrompt.spec.ts
Normal file
457
test/src/DeviceRequestPrompt.spec.ts
Normal 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!'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user