/** * 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 {Point} from '../../api/ElementHandle.js'; import { Keyboard as BaseKeyboard, Mouse as BaseMouse, Touchscreen as BaseTouchscreen, KeyDownOptions, KeyPressOptions, KeyboardTypeOptions, MouseButton, MouseClickOptions, MouseMoveOptions, MouseOptions, MouseWheelOptions, } from '../../api/Input.js'; import {KeyInput} from '../USKeyboardLayout.js'; import {BrowsingContext} from './BrowsingContext.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 Keyboard extends BaseKeyboard { #context: BrowsingContext; /** * @internal */ constructor(context: BrowsingContext) { super(); this.#context = context; } override async down( key: KeyInput, _options?: Readonly ): Promise { await this.#context.connection.send('input.performActions', { context: this.#context.id, actions: [ { type: SourceActionsType.Key, id: InputId.Keyboard, actions: [ { type: ActionType.KeyDown, value: getBidiKeyValue(key), }, ], }, ], }); } override async up(key: KeyInput): Promise { await this.#context.connection.send('input.performActions', { context: this.#context.id, actions: [ { type: SourceActionsType.Key, id: InputId.Keyboard, actions: [ { type: ActionType.KeyUp, value: getBidiKeyValue(key), }, ], }, ], }); } override async press( key: KeyInput, options: Readonly = {} ): Promise { 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.#context.connection.send('input.performActions', { context: this.#context.id, actions: [ { type: SourceActionsType.Key, id: InputId.Keyboard, actions, }, ], }); } override async type( text: string, options: Readonly = {} ): Promise { 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.#context.connection.send('input.performActions', { context: this.#context.id, actions: [ { type: SourceActionsType.Key, id: InputId.Keyboard, actions, }, ], }); } } /** * @internal */ interface BidiMouseClickOptions extends MouseClickOptions { origin?: Bidi.Input.Origin; } /** * @internal */ interface BidiMouseMoveOptions extends MouseMoveOptions { origin?: Bidi.Input.Origin; } /** * @internal */ 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 Mouse extends BaseMouse { #context: BrowsingContext; #lastMovePoint?: Point; /** * @internal */ constructor(context: BrowsingContext) { super(); this.#context = context; } override async reset(): Promise { this.#lastMovePoint = undefined; await this.#context.connection.send('input.releaseActions', { context: this.#context.id, }); } override async move( x: number, y: number, options: Readonly = {} ): Promise { this.#lastMovePoint = { x, y, }; await this.#context.connection.send('input.performActions', { context: this.#context.id, actions: [ { type: SourceActionsType.Pointer, id: InputId.Mouse, actions: [ { type: ActionType.PointerMove, x, y, duration: (options.steps ?? 0) * 50, origin: options.origin, }, ], }, ], }); } override async down(options: Readonly = {}): Promise { await this.#context.connection.send('input.performActions', { context: this.#context.id, actions: [ { type: SourceActionsType.Pointer, id: InputId.Mouse, actions: [ { type: ActionType.PointerDown, button: getBidiButton(options.button ?? MouseButton.Left), }, ], }, ], }); } override async up(options: Readonly = {}): Promise { await this.#context.connection.send('input.performActions', { context: this.#context.id, actions: [ { 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 = {} ): Promise { const actions: Bidi.Input.PointerSourceAction[] = [ { type: ActionType.PointerMove, x, 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.#context.connection.send('input.performActions', { context: this.#context.id, actions: [ { type: SourceActionsType.Pointer, id: InputId.Mouse, actions, }, ], }); } override async wheel( options: Readonly = {} ): Promise { await this.#context.connection.send('input.performActions', { context: this.#context.id, actions: [ { type: SourceActionsType.Wheel, id: InputId.Wheel, actions: [ { type: ActionType.Scroll, ...(this.#lastMovePoint ?? { x: 0, y: 0, }), deltaX: options.deltaX ?? 0, deltaY: options.deltaY ?? 0, }, ], }, ], }); } } /** * @internal */ export class Touchscreen extends BaseTouchscreen { #context: BrowsingContext; /** * @internal */ constructor(context: BrowsingContext) { super(); this.#context = context; } override async tap( x: number, y: number, options: BidiTouchMoveOptions = {} ): Promise { await this.touchStart(x, y, options); await this.touchEnd(); } override async touchStart( x: number, y: number, options: BidiTouchMoveOptions = {} ): Promise { await this.#context.connection.send('input.performActions', { context: this.#context.id, actions: [ { type: SourceActionsType.Pointer, id: InputId.Finger, parameters: { pointerType: Bidi.Input.PointerType.Touch, }, actions: [ { type: ActionType.PointerMove, x, y, origin: options.origin, }, { type: ActionType.PointerDown, button: 0, }, ], }, ], }); } override async touchMove( x: number, y: number, options: BidiTouchMoveOptions = {} ): Promise { await this.#context.connection.send('input.performActions', { context: this.#context.id, actions: [ { type: SourceActionsType.Pointer, id: InputId.Finger, parameters: { pointerType: Bidi.Input.PointerType.Touch, }, actions: [ { type: ActionType.PointerMove, x, y, origin: options.origin, }, ], }, ], }); } override async touchEnd(): Promise { await this.#context.connection.send('input.performActions', { context: this.#context.id, actions: [ { type: SourceActionsType.Pointer, id: InputId.Finger, parameters: { pointerType: Bidi.Input.PointerType.Touch, }, actions: [ { type: ActionType.PointerUp, button: 0, }, ], }, ], }); } }