Source code

Revision control

Copy as Markdown

Other Tools

/**
* @license
* Copyright 2017 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import type {Point} from '../api/ElementHandle.js';
import {
Keyboard,
Mouse,
MouseButton,
Touchscreen,
type KeyboardTypeOptions,
type KeyDownOptions,
type KeyPressOptions,
type MouseClickOptions,
type MouseMoveOptions,
type MouseOptions,
type MouseWheelOptions,
} from '../api/Input.js';
import {UnsupportedOperation} from '../common/Errors.js';
import type {KeyInput} from '../common/USKeyboardLayout.js';
import type {BidiPage} from './Page.js';
const enum InputId {
Mouse = '__puppeteer_mouse',
Keyboard = '__puppeteer_keyboard',
Wheel = '__puppeteer_wheel',
Finger = '__puppeteer_finger',
}
enum SourceActionsType {
None = 'none',
Key = 'key',
Pointer = 'pointer',
Wheel = 'wheel',
}
enum ActionType {
Pause = 'pause',
KeyDown = 'keyDown',
KeyUp = 'keyUp',
PointerUp = 'pointerUp',
PointerDown = 'pointerDown',
PointerMove = 'pointerMove',
Scroll = 'scroll',
}
const getBidiKeyValue = (key: KeyInput) => {
switch (key) {
case '\r':
case '\n':
key = 'Enter';
break;
}
// Measures the number of code points rather than UTF-16 code units.
if ([...key].length === 1) {
return key;
}
switch (key) {
case 'Cancel':
return '\uE001';
case 'Help':
return '\uE002';
case 'Backspace':
return '\uE003';
case 'Tab':
return '\uE004';
case 'Clear':
return '\uE005';
case 'Enter':
return '\uE007';
case 'Shift':
case 'ShiftLeft':
return '\uE008';
case 'Control':
case 'ControlLeft':
return '\uE009';
case 'Alt':
case 'AltLeft':
return '\uE00A';
case 'Pause':
return '\uE00B';
case 'Escape':
return '\uE00C';
case 'PageUp':
return '\uE00E';
case 'PageDown':
return '\uE00F';
case 'End':
return '\uE010';
case 'Home':
return '\uE011';
case 'ArrowLeft':
return '\uE012';
case 'ArrowUp':
return '\uE013';
case 'ArrowRight':
return '\uE014';
case 'ArrowDown':
return '\uE015';
case 'Insert':
return '\uE016';
case 'Delete':
return '\uE017';
case 'NumpadEqual':
return '\uE019';
case 'Numpad0':
return '\uE01A';
case 'Numpad1':
return '\uE01B';
case 'Numpad2':
return '\uE01C';
case 'Numpad3':
return '\uE01D';
case 'Numpad4':
return '\uE01E';
case 'Numpad5':
return '\uE01F';
case 'Numpad6':
return '\uE020';
case 'Numpad7':
return '\uE021';
case 'Numpad8':
return '\uE022';
case 'Numpad9':
return '\uE023';
case 'NumpadMultiply':
return '\uE024';
case 'NumpadAdd':
return '\uE025';
case 'NumpadSubtract':
return '\uE027';
case 'NumpadDecimal':
return '\uE028';
case 'NumpadDivide':
return '\uE029';
case 'F1':
return '\uE031';
case 'F2':
return '\uE032';
case 'F3':
return '\uE033';
case 'F4':
return '\uE034';
case 'F5':
return '\uE035';
case 'F6':
return '\uE036';
case 'F7':
return '\uE037';
case 'F8':
return '\uE038';
case 'F9':
return '\uE039';
case 'F10':
return '\uE03A';
case 'F11':
return '\uE03B';
case 'F12':
return '\uE03C';
case 'Meta':
case 'MetaLeft':
return '\uE03D';
case 'ShiftRight':
return '\uE050';
case 'ControlRight':
return '\uE051';
case 'AltRight':
return '\uE052';
case 'MetaRight':
return '\uE053';
case 'Digit0':
return '0';
case 'Digit1':
return '1';
case 'Digit2':
return '2';
case 'Digit3':
return '3';
case 'Digit4':
return '4';
case 'Digit5':
return '5';
case 'Digit6':
return '6';
case 'Digit7':
return '7';
case 'Digit8':
return '8';
case 'Digit9':
return '9';
case 'KeyA':
return 'a';
case 'KeyB':
return 'b';
case 'KeyC':
return 'c';
case 'KeyD':
return 'd';
case 'KeyE':
return 'e';
case 'KeyF':
return 'f';
case 'KeyG':
return 'g';
case 'KeyH':
return 'h';
case 'KeyI':
return 'i';
case 'KeyJ':
return 'j';
case 'KeyK':
return 'k';
case 'KeyL':
return 'l';
case 'KeyM':
return 'm';
case 'KeyN':
return 'n';
case 'KeyO':
return 'o';
case 'KeyP':
return 'p';
case 'KeyQ':
return 'q';
case 'KeyR':
return 'r';
case 'KeyS':
return 's';
case 'KeyT':
return 't';
case 'KeyU':
return 'u';
case 'KeyV':
return 'v';
case 'KeyW':
return 'w';
case 'KeyX':
return 'x';
case 'KeyY':
return 'y';
case 'KeyZ':
return 'z';
case 'Semicolon':
return ';';
case 'Equal':
return '=';
case 'Comma':
return ',';
case 'Minus':
return '-';
case 'Period':
return '.';
case 'Slash':
return '/';
case 'Backquote':
return '`';
case 'BracketLeft':
return '[';
case 'Backslash':
return '\\';
case 'BracketRight':
return ']';
case 'Quote':
return '"';
default:
throw new Error(`Unknown key: "${key}"`);
}
};
/**
* @internal
*/
export class BidiKeyboard extends Keyboard {
#page: BidiPage;
constructor(page: BidiPage) {
super();
this.#page = page;
}
override async down(
key: KeyInput,
_options?: Readonly<KeyDownOptions>
): Promise<void> {
await this.#page.mainFrame().browsingContext.performActions([
{
type: SourceActionsType.Key,
id: InputId.Keyboard,
actions: [
{
type: ActionType.KeyDown,
value: getBidiKeyValue(key),
},
],
},
]);
}
override async up(key: KeyInput): Promise<void> {
await this.#page.mainFrame().browsingContext.performActions([
{
type: SourceActionsType.Key,
id: InputId.Keyboard,
actions: [
{
type: ActionType.KeyUp,
value: getBidiKeyValue(key),
},
],
},
]);
}
override async press(
key: KeyInput,
options: Readonly<KeyPressOptions> = {}
): Promise<void> {
const {delay = 0} = options;
const actions: Bidi.Input.KeySourceAction[] = [
{
type: ActionType.KeyDown,
value: getBidiKeyValue(key),
},
];
if (delay > 0) {
actions.push({
type: ActionType.Pause,
duration: delay,
});
}
actions.push({
type: ActionType.KeyUp,
value: getBidiKeyValue(key),
});
await this.#page.mainFrame().browsingContext.performActions([
{
type: SourceActionsType.Key,
id: InputId.Keyboard,
actions,
},
]);
}
override async type(
text: string,
options: Readonly<KeyboardTypeOptions> = {}
): Promise<void> {
const {delay = 0} = options;
// This spread separates the characters into code points rather than UTF-16
// code units.
const values = ([...text] as KeyInput[]).map(getBidiKeyValue);
const actions: Bidi.Input.KeySourceAction[] = [];
if (delay <= 0) {
for (const value of values) {
actions.push(
{
type: ActionType.KeyDown,
value,
},
{
type: ActionType.KeyUp,
value,
}
);
}
} else {
for (const value of values) {
actions.push(
{
type: ActionType.KeyDown,
value,
},
{
type: ActionType.Pause,
duration: delay,
},
{
type: ActionType.KeyUp,
value,
}
);
}
}
await this.#page.mainFrame().browsingContext.performActions([
{
type: SourceActionsType.Key,
id: InputId.Keyboard,
actions,
},
]);
}
override async sendCharacter(char: string): Promise<void> {
// Measures the number of code points rather than UTF-16 code units.
if ([...char].length > 1) {
throw new Error('Cannot send more than 1 character.');
}
const frame = await this.#page.focusedFrame();
await frame.isolatedRealm().evaluate(async char => {
document.execCommand('insertText', false, char);
}, char);
}
}
/**
* @internal
*/
export interface BidiMouseClickOptions extends MouseClickOptions {
origin?: Bidi.Input.Origin;
}
/**
* @internal
*/
export interface BidiMouseMoveOptions extends MouseMoveOptions {
origin?: Bidi.Input.Origin;
}
/**
* @internal
*/
export interface BidiTouchMoveOptions {
origin?: Bidi.Input.Origin;
}
const getBidiButton = (button: MouseButton) => {
switch (button) {
case MouseButton.Left:
return 0;
case MouseButton.Middle:
return 1;
case MouseButton.Right:
return 2;
case MouseButton.Back:
return 3;
case MouseButton.Forward:
return 4;
}
};
/**
* @internal
*/
export class BidiMouse extends Mouse {
#page: BidiPage;
#lastMovePoint: Point = {x: 0, y: 0};
constructor(page: BidiPage) {
super();
this.#page = page;
}
override async reset(): Promise<void> {
this.#lastMovePoint = {x: 0, y: 0};
await this.#page.mainFrame().browsingContext.releaseActions();
}
override async move(
x: number,
y: number,
options: Readonly<BidiMouseMoveOptions> = {}
): Promise<void> {
const from = this.#lastMovePoint;
const to = {
x: Math.round(x),
y: Math.round(y),
};
const actions: Bidi.Input.PointerSourceAction[] = [];
const steps = options.steps ?? 0;
for (let i = 0; i < steps; ++i) {
actions.push({
type: ActionType.PointerMove,
x: from.x + (to.x - from.x) * (i / steps),
y: from.y + (to.y - from.y) * (i / steps),
origin: options.origin,
});
}
actions.push({
type: ActionType.PointerMove,
...to,
origin: options.origin,
});
this.#lastMovePoint = to;
await this.#page.mainFrame().browsingContext.performActions([
{
type: SourceActionsType.Pointer,
id: InputId.Mouse,
actions,
},
]);
}
override async down(options: Readonly<MouseOptions> = {}): Promise<void> {
await this.#page.mainFrame().browsingContext.performActions([
{
type: SourceActionsType.Pointer,
id: InputId.Mouse,
actions: [
{
type: ActionType.PointerDown,
button: getBidiButton(options.button ?? MouseButton.Left),
},
],
},
]);
}
override async up(options: Readonly<MouseOptions> = {}): Promise<void> {
await this.#page.mainFrame().browsingContext.performActions([
{
type: SourceActionsType.Pointer,
id: InputId.Mouse,
actions: [
{
type: ActionType.PointerUp,
button: getBidiButton(options.button ?? MouseButton.Left),
},
],
},
]);
}
override async click(
x: number,
y: number,
options: Readonly<BidiMouseClickOptions> = {}
): Promise<void> {
const actions: Bidi.Input.PointerSourceAction[] = [
{
type: ActionType.PointerMove,
x: Math.round(x),
y: Math.round(y),
origin: options.origin,
},
];
const pointerDownAction = {
type: ActionType.PointerDown,
button: getBidiButton(options.button ?? MouseButton.Left),
} as const;
const pointerUpAction = {
type: ActionType.PointerUp,
button: pointerDownAction.button,
} as const;
for (let i = 1; i < (options.count ?? 1); ++i) {
actions.push(pointerDownAction, pointerUpAction);
}
actions.push(pointerDownAction);
if (options.delay) {
actions.push({
type: ActionType.Pause,
duration: options.delay,
});
}
actions.push(pointerUpAction);
await this.#page.mainFrame().browsingContext.performActions([
{
type: SourceActionsType.Pointer,
id: InputId.Mouse,
actions,
},
]);
}
override async wheel(
options: Readonly<MouseWheelOptions> = {}
): Promise<void> {
await this.#page.mainFrame().browsingContext.performActions([
{
type: SourceActionsType.Wheel,
id: InputId.Wheel,
actions: [
{
type: ActionType.Scroll,
...(this.#lastMovePoint ?? {
x: 0,
y: 0,
}),
deltaX: options.deltaX ?? 0,
deltaY: options.deltaY ?? 0,
},
],
},
]);
}
override drag(): never {
throw new UnsupportedOperation();
}
override dragOver(): never {
throw new UnsupportedOperation();
}
override dragEnter(): never {
throw new UnsupportedOperation();
}
override drop(): never {
throw new UnsupportedOperation();
}
override dragAndDrop(): never {
throw new UnsupportedOperation();
}
}
/**
* @internal
*/
export class BidiTouchscreen extends Touchscreen {
#page: BidiPage;
constructor(page: BidiPage) {
super();
this.#page = page;
}
override async touchStart(
x: number,
y: number,
options: BidiTouchMoveOptions = {}
): Promise<void> {
await this.#page.mainFrame().browsingContext.performActions([
{
type: SourceActionsType.Pointer,
id: InputId.Finger,
parameters: {
pointerType: Bidi.Input.PointerType.Touch,
},
actions: [
{
type: ActionType.PointerMove,
x: Math.round(x),
y: Math.round(y),
origin: options.origin,
},
{
type: ActionType.PointerDown,
button: 0,
width: 0.5 * 2, // 2 times default touch radius.
height: 0.5 * 2, // 2 times default touch radius.
pressure: 0.5,
altitudeAngle: Math.PI / 2,
},
],
},
]);
}
override async touchMove(
x: number,
y: number,
options: BidiTouchMoveOptions = {}
): Promise<void> {
await this.#page.mainFrame().browsingContext.performActions([
{
type: SourceActionsType.Pointer,
id: InputId.Finger,
parameters: {
pointerType: Bidi.Input.PointerType.Touch,
},
actions: [
{
type: ActionType.PointerMove,
x: Math.round(x),
y: Math.round(y),
origin: options.origin,
width: 0.5 * 2, // 2 times default touch radius.
height: 0.5 * 2, // 2 times default touch radius.
pressure: 0.5,
altitudeAngle: Math.PI / 2,
},
],
},
]);
}
override async touchEnd(): Promise<void> {
await this.#page.mainFrame().browsingContext.performActions([
{
type: SourceActionsType.Pointer,
id: InputId.Finger,
parameters: {
pointerType: Bidi.Input.PointerType.Touch,
},
actions: [
{
type: ActionType.PointerUp,
button: 0,
},
],
},
]);
}
}