chore: add Dialogs for BiDi (#10659)

This commit is contained in:
Nikolay Vitkov 2023-08-01 15:53:02 +02:00 committed by GitHub
parent 04369db145
commit 76f67ebd76
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 252 additions and 92 deletions

View File

@ -0,0 +1,119 @@
/**
* Copyright 2017 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 {assert} from '../util/assert.js';
/**
* Dialog instances are dispatched by the {@link Page} via the `dialog` event.
*
* @remarks
*
* @example
*
* ```ts
* import puppeteer from 'puppeteer';
*
* (async () => {
* const browser = await puppeteer.launch();
* const page = await browser.newPage();
* page.on('dialog', async dialog => {
* console.log(dialog.message());
* await dialog.dismiss();
* await browser.close();
* });
* page.evaluate(() => alert('1'));
* })();
* ```
*
* @public
*/
export class Dialog {
#type: Protocol.Page.DialogType;
#message: string;
#defaultValue: string;
#handled = false;
/**
* @internal
*/
constructor(
type: Protocol.Page.DialogType,
message: string,
defaultValue = ''
) {
this.#type = type;
this.#message = message;
this.#defaultValue = defaultValue;
}
/**
* The type of the dialog.
*/
type(): Protocol.Page.DialogType {
return this.#type;
}
/**
* The message displayed in the dialog.
*/
message(): string {
return this.#message;
}
/**
* The default value of the prompt, or an empty string if the dialog
* is not a `prompt`.
*/
defaultValue(): string {
return this.#defaultValue;
}
/**
* @internal
*/
sendCommand(_options: {accept: boolean; text?: string}): Promise<void> {
throw new Error('Not implemented');
}
/**
* A promise that resolves when the dialog has been accepted.
*
* @param promptText - optional text that will be entered in the dialog
* prompt. Has no effect if the dialog's type is not `prompt`.
*
*/
async accept(promptText?: string): Promise<void> {
assert(!this.#handled, 'Cannot accept dialog which is already handled!');
this.#handled = true;
await this.sendCommand({
accept: true,
text: promptText,
});
}
/**
* A promise which will resolve once the dialog has been dismissed
*/
async dismiss(): Promise<void> {
assert(!this.#handled, 'Cannot dismiss dialog which is already handled!');
this.#handled = true;
await this.sendCommand({
accept: false,
});
}
}

View File

@ -26,7 +26,6 @@ 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 {DeviceRequestPrompt} from '../common/DeviceRequestPrompt.js';
import type {Dialog} from '../common/Dialog.js';
import {TargetCloseError} from '../common/Errors.js'; import {TargetCloseError} from '../common/Errors.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';
@ -65,6 +64,7 @@ import {Deferred} from '../util/Deferred.js';
import type {Browser} from './Browser.js'; import type {Browser} from './Browser.js';
import type {BrowserContext} from './BrowserContext.js'; import type {BrowserContext} from './BrowserContext.js';
import type {Dialog} from './Dialog.js';
import type {ClickOptions, ElementHandle} from './ElementHandle.js'; import type {ClickOptions, ElementHandle} from './ElementHandle.js';
import type { import type {
Frame, Frame,

View File

@ -16,6 +16,7 @@
export * from './Browser.js'; export * from './Browser.js';
export * from './BrowserContext.js'; export * from './BrowserContext.js';
export * from './Dialog.js';
export * from './Page.js'; export * from './Page.js';
export * from './JSHandle.js'; export * from './JSHandle.js';
export * from './ElementHandle.js'; export * from './ElementHandle.js';

View File

@ -16,40 +16,15 @@
import {Protocol} from 'devtools-protocol'; import {Protocol} from 'devtools-protocol';
import {assert} from '../util/assert.js'; import {Dialog as BaseDialog} from '../api/Dialog.js';
import {CDPSession} from './Connection.js'; import {CDPSession} from './Connection.js';
/** /**
* Dialog instances are dispatched by the {@link Page} via the `dialog` event. * @internal
*
* @remarks
*
* @example
*
* ```ts
* import puppeteer from 'puppeteer';
*
* (async () => {
* const browser = await puppeteer.launch();
* const page = await browser.newPage();
* page.on('dialog', async dialog => {
* console.log(dialog.message());
* await dialog.dismiss();
* await browser.close();
* });
* page.evaluate(() => alert('1'));
* })();
* ```
*
* @public
*/ */
export class Dialog { export class CDPDialog extends BaseDialog {
#client: CDPSession; #client: CDPSession;
#type: Protocol.Page.DialogType;
#message: string;
#defaultValue: string;
#handled = false;
/** /**
* @internal * @internal
@ -60,58 +35,20 @@ export class Dialog {
message: string, message: string,
defaultValue = '' defaultValue = ''
) { ) {
super(type, message, defaultValue);
this.#client = client; this.#client = client;
this.#type = type;
this.#message = message;
this.#defaultValue = defaultValue;
} }
/** /**
* The type of the dialog. * @internal
*/ */
type(): Protocol.Page.DialogType { override async sendCommand(options: {
return this.#type; accept: boolean;
} text?: string;
}): Promise<void> {
/**
* The message displayed in the dialog.
*/
message(): string {
return this.#message;
}
/**
* The default value of the prompt, or an empty string if the dialog
* is not a `prompt`.
*/
defaultValue(): string {
return this.#defaultValue;
}
/**
* A promise that resolves when the dialog has been accepted.
*
* @param promptText - optional text that will be entered in the dialog
* prompt. Has no effect if the dialog's type is not `prompt`.
*
*/
async accept(promptText?: string): Promise<void> {
assert(!this.#handled, 'Cannot accept dialog which is already handled!');
this.#handled = true;
await this.#client.send('Page.handleJavaScriptDialog', { await this.#client.send('Page.handleJavaScriptDialog', {
accept: true, accept: options.accept,
promptText: promptText, promptText: options.text,
});
}
/**
* A promise which will resolve once the dialog has been dismissed
*/
async dismiss(): Promise<void> {
assert(!this.#handled, 'Cannot dismiss dialog which is already handled!');
this.#handled = true;
await this.#client.send('Page.handleJavaScriptDialog', {
accept: false,
}); });
} }
} }

View File

@ -51,7 +51,7 @@ import {
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 {DeviceRequestPrompt} from './DeviceRequestPrompt.js';
import {Dialog} from './Dialog.js'; import {CDPDialog} from './Dialog.js';
import {EmulationManager} from './EmulationManager.js'; import {EmulationManager} from './EmulationManager.js';
import {TargetCloseError} from './Errors.js'; import {TargetCloseError} from './Errors.js';
import {FileChooser} from './FileChooser.js'; import {FileChooser} from './FileChooser.js';
@ -81,6 +81,7 @@ import {
isString, isString,
pageBindingInitString, pageBindingInitString,
releaseObject, releaseObject,
validateDialogType,
valueFromRemoteObject, valueFromRemoteObject,
waitForEvent, waitForEvent,
waitWithTimeout, waitWithTimeout,
@ -807,22 +808,10 @@ export class CDPPage extends Page {
} }
#onDialog(event: Protocol.Page.JavascriptDialogOpeningEvent): void { #onDialog(event: Protocol.Page.JavascriptDialogOpeningEvent): void {
let dialogType = null; const type = validateDialogType(event.type);
const validDialogTypes = new Set<Protocol.Page.DialogType>([ const dialog = new CDPDialog(
'alert',
'confirm',
'prompt',
'beforeunload',
]);
if (validDialogTypes.has(event.type)) {
dialogType = event.type as Protocol.Page.DialogType;
}
assert(dialogType, 'Unknown javascript dialog type: ' + event.type);
const dialog = new Dialog(
this.#client, this.#client,
dialogType, type,
event.message, event.message,
event.defaultPrompt event.defaultPrompt
); );

View File

@ -76,6 +76,10 @@ interface Commands {
params: Bidi.BrowsingContext.CaptureScreenshotParameters; params: Bidi.BrowsingContext.CaptureScreenshotParameters;
returnType: Bidi.BrowsingContext.CaptureScreenshotResult; returnType: Bidi.BrowsingContext.CaptureScreenshotResult;
}; };
'browsingContext.handleUserPrompt': {
params: Bidi.BrowsingContext.HandleUserPromptParameters;
returnType: Bidi.EmptyResult;
};
'input.performActions': { 'input.performActions': {
params: Bidi.Input.PerformActionsParameters; params: Bidi.Input.PerformActionsParameters;

View File

@ -0,0 +1,55 @@
/**
* Copyright 2017 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 * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import {Dialog as BaseDialog} from '../../api/Dialog.js';
import {BrowsingContext} from './BrowsingContext.js';
/**
* @internal
*/
export class Dialog extends BaseDialog {
#context: BrowsingContext;
/**
* @internal
*/
constructor(
context: BrowsingContext,
type: Bidi.BrowsingContext.UserPromptOpenedParameters['type'],
message: string,
defaultValue = ''
) {
super(type, message, defaultValue);
this.#context = context;
}
/**
* @internal
*/
override async sendCommand(options: {
accept: boolean;
text?: string;
}): Promise<void> {
await this.#context.connection.send('browsingContext.handleUserPrompt', {
context: this.#context.id,
accept: options.accept,
userText: options.text,
});
}
}

View File

@ -46,6 +46,7 @@ import {EvaluateFunc, HandleFor} from '../types.js';
import { import {
debugError, debugError,
isString, isString,
validateDialogType,
waitForEvent, waitForEvent,
waitWithTimeout, waitWithTimeout,
withSourcePuppeteerURLIfNone, withSourcePuppeteerURLIfNone,
@ -59,6 +60,7 @@ import {
CDPSessionWrapper, CDPSessionWrapper,
} from './BrowsingContext.js'; } from './BrowsingContext.js';
import {Connection} from './Connection.js'; import {Connection} from './Connection.js';
import {Dialog} from './Dialog.js';
import {Frame} from './Frame.js'; import {Frame} from './Frame.js';
import {HTTPRequest} from './HTTPRequest.js'; import {HTTPRequest} from './HTTPRequest.js';
import {HTTPResponse} from './HTTPResponse.js'; import {HTTPResponse} from './HTTPResponse.js';
@ -89,6 +91,7 @@ export class Page extends PageBase {
'browsingContext.navigationStarted', 'browsingContext.navigationStarted',
this.#onFrameNavigationStarted.bind(this), this.#onFrameNavigationStarted.bind(this),
], ],
['browsingContext.userPromptOpened', this.#onDialog.bind(this)],
]); ]);
#networkManagerEvents = new Map<symbol, Handler<any>>([ #networkManagerEvents = new Map<symbol, Handler<any>>([
[ [
@ -376,6 +379,17 @@ export class Page extends PageBase {
} }
} }
#onDialog(event: Bidi.BrowsingContext.UserPromptOpenedParameters): void {
const frame = this.frame(event.context);
if (!frame) {
return;
}
const type = validateDialogType(event.type);
const dialog = new Dialog(frame.context(), type, event.message);
this.emit(PageEmittedEvents.Dialog, dialog);
}
getNavigationResponse(id: string | null): HTTPResponse | null { getNavigationResponse(id: string | null): HTTPResponse | null {
return this.#networkManager.getNavigationResponse(id); return this.#networkManager.getNavigationResponse(id);
} }
@ -384,12 +398,14 @@ export class Page extends PageBase {
if (this.#closedDeferred.finished()) { if (this.#closedDeferred.finished()) {
return; return;
} }
this.#closedDeferred.resolve(new TargetCloseError('Page closed!')); this.#closedDeferred.resolve(new TargetCloseError('Page closed!'));
this.#networkManager.dispose(); this.#networkManager.dispose();
await this.#connection.send('browsingContext.close', { await this.#connection.send('browsingContext.close', {
context: this.mainFrame()._id, context: this.mainFrame()._id,
}); });
this.emit(PageEmittedEvents.Close); this.emit(PageEmittedEvents.Close);
this.removeAllListeners(); this.removeAllListeners();
} }

View File

@ -647,3 +647,24 @@ export function getPageContent(): string {
return content; return content;
} }
/**
* @internal
*/
export function validateDialogType(
type: string
): 'alert' | 'confirm' | 'prompt' | 'beforeunload' {
let dialogType = null;
const validDialogTypes = new Set([
'alert',
'confirm',
'prompt',
'beforeunload',
]);
if (validDialogTypes.has(type)) {
dialogType = type;
}
assert(dialogType, `Unknown javascript dialog type: ${type}`);
return dialogType as 'alert' | 'confirm' | 'prompt' | 'beforeunload';
}

View File

@ -515,6 +515,12 @@
"parameters": ["webDriverBiDi"], "parameters": ["webDriverBiDi"],
"expectations": ["FAIL", "PASS"] "expectations": ["FAIL", "PASS"]
}, },
{
"testIdPattern": "[dialog.spec] Page.Events.Dialog *",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "webDriverBiDi"],
"expectations": ["PASS"]
},
{ {
"testIdPattern": "[drag-and-drop.spec] *", "testIdPattern": "[drag-and-drop.spec] *",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
@ -1895,6 +1901,12 @@
"parameters": ["firefox", "webDriverBiDi"], "parameters": ["firefox", "webDriverBiDi"],
"expectations": ["FAIL"] "expectations": ["FAIL"]
}, },
{
"testIdPattern": "[dialog.spec] Page.Events.Dialog should allow accepting prompts",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "webDriverBiDi"],
"expectations": ["FAIL"]
},
{ {
"testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.boundingBox should handle nested frames", "testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.boundingBox should handle nested frames",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
@ -3940,5 +3952,11 @@
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
"parameters": ["cdp", "chrome", "headless"], "parameters": ["cdp", "chrome", "headless"],
"expectations": ["FAIL", "PASS"] "expectations": ["FAIL", "PASS"]
},
{
"testIdPattern": "[page.spec] Page Page.close should run beforeunload if asked for",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "webDriverBiDi"],
"expectations": ["PASS"]
} }
] ]

View File

@ -142,7 +142,7 @@ export const waitEvent = async <T = any>(
): Promise<T> => { ): Promise<T> => {
const deferred = Deferred.create<T>({ const deferred = Deferred.create<T>({
timeout: 5000, timeout: 5000,
message: 'Waiting for test event timed out.', message: `Waiting for ${eventName} event timed out.`,
}); });
const handler = (event: T) => { const handler = (event: T) => {
if (!predicate(event)) { if (!predicate(event)) {