1/** 2 * Copyright 2017 Google Inc. All rights reserved. 3 * 4 * Licensed under the Apache License, Version 2.0 (the 'License'); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an 'AS IS' BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17import { assert } from './assert.js'; 18import { CDPSession } from './Connection.js'; 19import { keyDefinitions, KeyDefinition, KeyInput } from './USKeyboardLayout.js'; 20 21type KeyDescription = Required< 22 Pick<KeyDefinition, 'keyCode' | 'key' | 'text' | 'code' | 'location'> 23>; 24 25/** 26 * Keyboard provides an api for managing a virtual keyboard. 27 * The high level api is {@link Keyboard."type"}, 28 * which takes raw characters and generates proper keydown, keypress/input, 29 * and keyup events on your page. 30 * 31 * @remarks 32 * For finer control, you can use {@link Keyboard.down}, 33 * {@link Keyboard.up}, and {@link Keyboard.sendCharacter} 34 * to manually fire events as if they were generated from a real keyboard. 35 * 36 * On MacOS, keyboard shortcuts like `⌘ A` -\> Select All do not work. 37 * See {@link https://github.com/puppeteer/puppeteer/issues/1313 | #1313}. 38 * 39 * @example 40 * An example of holding down `Shift` in order to select and delete some text: 41 * ```js 42 * await page.keyboard.type('Hello World!'); 43 * await page.keyboard.press('ArrowLeft'); 44 * 45 * await page.keyboard.down('Shift'); 46 * for (let i = 0; i < ' World'.length; i++) 47 * await page.keyboard.press('ArrowLeft'); 48 * await page.keyboard.up('Shift'); 49 * 50 * await page.keyboard.press('Backspace'); 51 * // Result text will end up saying 'Hello!' 52 * ``` 53 * 54 * @example 55 * An example of pressing `A` 56 * ```js 57 * await page.keyboard.down('Shift'); 58 * await page.keyboard.press('KeyA'); 59 * await page.keyboard.up('Shift'); 60 * ``` 61 * 62 * @public 63 */ 64export class Keyboard { 65 private _client: CDPSession; 66 /** @internal */ 67 _modifiers = 0; 68 private _pressedKeys = new Set<string>(); 69 70 /** @internal */ 71 constructor(client: CDPSession) { 72 this._client = client; 73 } 74 75 /** 76 * Dispatches a `keydown` event. 77 * 78 * @remarks 79 * If `key` is a single character and no modifier keys besides `Shift` 80 * are being held down, a `keypress`/`input` event will also generated. 81 * The `text` option can be specified to force an input event to be generated. 82 * If `key` is a modifier key, `Shift`, `Meta`, `Control`, or `Alt`, 83 * subsequent key presses will be sent with that modifier active. 84 * To release the modifier key, use {@link Keyboard.up}. 85 * 86 * After the key is pressed once, subsequent calls to 87 * {@link Keyboard.down} will have 88 * {@link https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/repeat | repeat} 89 * set to true. To release the key, use {@link Keyboard.up}. 90 * 91 * Modifier keys DO influence {@link Keyboard.down}. 92 * Holding down `Shift` will type the text in upper case. 93 * 94 * @param key - Name of key to press, such as `ArrowLeft`. 95 * See {@link KeyInput} for a list of all key names. 96 * 97 * @param options - An object of options. Accepts text which, if specified, 98 * generates an input event with this text. 99 */ 100 async down( 101 key: KeyInput, 102 options: { text?: string } = { text: undefined } 103 ): Promise<void> { 104 const description = this._keyDescriptionForString(key); 105 106 const autoRepeat = this._pressedKeys.has(description.code); 107 this._pressedKeys.add(description.code); 108 this._modifiers |= this._modifierBit(description.key); 109 110 const text = options.text === undefined ? description.text : options.text; 111 await this._client.send('Input.dispatchKeyEvent', { 112 type: text ? 'keyDown' : 'rawKeyDown', 113 modifiers: this._modifiers, 114 windowsVirtualKeyCode: description.keyCode, 115 code: description.code, 116 key: description.key, 117 text: text, 118 unmodifiedText: text, 119 autoRepeat, 120 location: description.location, 121 isKeypad: description.location === 3, 122 }); 123 } 124 125 private _modifierBit(key: string): number { 126 if (key === 'Alt') return 1; 127 if (key === 'Control') return 2; 128 if (key === 'Meta') return 4; 129 if (key === 'Shift') return 8; 130 return 0; 131 } 132 133 private _keyDescriptionForString(keyString: KeyInput): KeyDescription { 134 const shift = this._modifiers & 8; 135 const description = { 136 key: '', 137 keyCode: 0, 138 code: '', 139 text: '', 140 location: 0, 141 }; 142 143 const definition = keyDefinitions[keyString]; 144 assert(definition, `Unknown key: "${keyString}"`); 145 146 if (definition.key) description.key = definition.key; 147 if (shift && definition.shiftKey) description.key = definition.shiftKey; 148 149 if (definition.keyCode) description.keyCode = definition.keyCode; 150 if (shift && definition.shiftKeyCode) 151 description.keyCode = definition.shiftKeyCode; 152 153 if (definition.code) description.code = definition.code; 154 155 if (definition.location) description.location = definition.location; 156 157 if (description.key.length === 1) description.text = description.key; 158 159 if (definition.text) description.text = definition.text; 160 if (shift && definition.shiftText) description.text = definition.shiftText; 161 162 // if any modifiers besides shift are pressed, no text should be sent 163 if (this._modifiers & ~8) description.text = ''; 164 165 return description; 166 } 167 168 /** 169 * Dispatches a `keyup` event. 170 * 171 * @param key - Name of key to release, such as `ArrowLeft`. 172 * See {@link KeyInput | KeyInput} 173 * for a list of all key names. 174 */ 175 async up(key: KeyInput): Promise<void> { 176 const description = this._keyDescriptionForString(key); 177 178 this._modifiers &= ~this._modifierBit(description.key); 179 this._pressedKeys.delete(description.code); 180 await this._client.send('Input.dispatchKeyEvent', { 181 type: 'keyUp', 182 modifiers: this._modifiers, 183 key: description.key, 184 windowsVirtualKeyCode: description.keyCode, 185 code: description.code, 186 location: description.location, 187 }); 188 } 189 190 /** 191 * Dispatches a `keypress` and `input` event. 192 * This does not send a `keydown` or `keyup` event. 193 * 194 * @remarks 195 * Modifier keys DO NOT effect {@link Keyboard.sendCharacter | Keyboard.sendCharacter}. 196 * Holding down `Shift` will not type the text in upper case. 197 * 198 * @example 199 * ```js 200 * page.keyboard.sendCharacter('嗨'); 201 * ``` 202 * 203 * @param char - Character to send into the page. 204 */ 205 async sendCharacter(char: string): Promise<void> { 206 await this._client.send('Input.insertText', { text: char }); 207 } 208 209 private charIsKey(char: string): char is KeyInput { 210 return !!keyDefinitions[char]; 211 } 212 213 /** 214 * Sends a `keydown`, `keypress`/`input`, 215 * and `keyup` event for each character in the text. 216 * 217 * @remarks 218 * To press a special key, like `Control` or `ArrowDown`, 219 * use {@link Keyboard.press}. 220 * 221 * Modifier keys DO NOT effect `keyboard.type`. 222 * Holding down `Shift` will not type the text in upper case. 223 * 224 * @example 225 * ```js 226 * await page.keyboard.type('Hello'); // Types instantly 227 * await page.keyboard.type('World', {delay: 100}); // Types slower, like a user 228 * ``` 229 * 230 * @param text - A text to type into a focused element. 231 * @param options - An object of options. Accepts delay which, 232 * if specified, is the time to wait between `keydown` and `keyup` in milliseconds. 233 * Defaults to 0. 234 */ 235 async type(text: string, options: { delay?: number } = {}): Promise<void> { 236 const delay = options.delay || null; 237 for (const char of text) { 238 if (this.charIsKey(char)) { 239 await this.press(char, { delay }); 240 } else { 241 if (delay) await new Promise((f) => setTimeout(f, delay)); 242 await this.sendCharacter(char); 243 } 244 } 245 } 246 247 /** 248 * Shortcut for {@link Keyboard.down} 249 * and {@link Keyboard.up}. 250 * 251 * @remarks 252 * If `key` is a single character and no modifier keys besides `Shift` 253 * are being held down, a `keypress`/`input` event will also generated. 254 * The `text` option can be specified to force an input event to be generated. 255 * 256 * Modifier keys DO effect {@link Keyboard.press}. 257 * Holding down `Shift` will type the text in upper case. 258 * 259 * @param key - Name of key to press, such as `ArrowLeft`. 260 * See {@link KeyInput} for a list of all key names. 261 * 262 * @param options - An object of options. Accepts text which, if specified, 263 * generates an input event with this text. Accepts delay which, 264 * if specified, is the time to wait between `keydown` and `keyup` in milliseconds. 265 * Defaults to 0. 266 */ 267 async press( 268 key: KeyInput, 269 options: { delay?: number; text?: string } = {} 270 ): Promise<void> { 271 const { delay = null } = options; 272 await this.down(key, options); 273 if (delay) await new Promise((f) => setTimeout(f, options.delay)); 274 await this.up(key); 275 } 276} 277 278/** 279 * @public 280 */ 281export type MouseButton = 'left' | 'right' | 'middle'; 282 283/** 284 * @public 285 */ 286export interface MouseOptions { 287 button?: MouseButton; 288 clickCount?: number; 289} 290 291/** 292 * @public 293 */ 294export interface MouseWheelOptions { 295 deltaX?: number; 296 deltaY?: number; 297} 298 299/** 300 * The Mouse class operates in main-frame CSS pixels 301 * relative to the top-left corner of the viewport. 302 * @remarks 303 * Every `page` object has its own Mouse, accessible with [`page.mouse`](#pagemouse). 304 * 305 * @example 306 * ```js 307 * // Using ‘page.mouse’ to trace a 100x100 square. 308 * await page.mouse.move(0, 0); 309 * await page.mouse.down(); 310 * await page.mouse.move(0, 100); 311 * await page.mouse.move(100, 100); 312 * await page.mouse.move(100, 0); 313 * await page.mouse.move(0, 0); 314 * await page.mouse.up(); 315 * ``` 316 * 317 * **Note**: The mouse events trigger synthetic `MouseEvent`s. 318 * This means that it does not fully replicate the functionality of what a normal user 319 * would be able to do with their mouse. 320 * 321 * For example, dragging and selecting text is not possible using `page.mouse`. 322 * Instead, you can use the {@link https://developer.mozilla.org/en-US/docs/Web/API/DocumentOrShadowRoot/getSelection | `DocumentOrShadowRoot.getSelection()`} functionality implemented in the platform. 323 * 324 * @example 325 * For example, if you want to select all content between nodes: 326 * ```js 327 * await page.evaluate((from, to) => { 328 * const selection = from.getRootNode().getSelection(); 329 * const range = document.createRange(); 330 * range.setStartBefore(from); 331 * range.setEndAfter(to); 332 * selection.removeAllRanges(); 333 * selection.addRange(range); 334 * }, fromJSHandle, toJSHandle); 335 * ``` 336 * If you then would want to copy-paste your selection, you can use the clipboard api: 337 * ```js 338 * // The clipboard api does not allow you to copy, unless the tab is focused. 339 * await page.bringToFront(); 340 * await page.evaluate(() => { 341 * // Copy the selected content to the clipboard 342 * document.execCommand('copy'); 343 * // Obtain the content of the clipboard as a string 344 * return navigator.clipboard.readText(); 345 * }); 346 * ``` 347 * **Note**: If you want access to the clipboard API, 348 * you have to give it permission to do so: 349 * ```js 350 * await browser.defaultBrowserContext().overridePermissions( 351 * '<your origin>', ['clipboard-read', 'clipboard-write'] 352 * ); 353 * ``` 354 * @public 355 */ 356export class Mouse { 357 private _client: CDPSession; 358 private _keyboard: Keyboard; 359 private _x = 0; 360 private _y = 0; 361 private _button: MouseButton | 'none' = 'none'; 362 363 /** 364 * @internal 365 */ 366 constructor(client: CDPSession, keyboard: Keyboard) { 367 this._client = client; 368 this._keyboard = keyboard; 369 } 370 371 /** 372 * Dispatches a `mousemove` event. 373 * @param x - Horizontal position of the mouse. 374 * @param y - Vertical position of the mouse. 375 * @param options - Optional object. If specified, the `steps` property 376 * sends intermediate `mousemove` events when set to `1` (default). 377 */ 378 async move( 379 x: number, 380 y: number, 381 options: { steps?: number } = {} 382 ): Promise<void> { 383 const { steps = 1 } = options; 384 const fromX = this._x, 385 fromY = this._y; 386 this._x = x; 387 this._y = y; 388 for (let i = 1; i <= steps; i++) { 389 await this._client.send('Input.dispatchMouseEvent', { 390 type: 'mouseMoved', 391 button: this._button, 392 x: fromX + (this._x - fromX) * (i / steps), 393 y: fromY + (this._y - fromY) * (i / steps), 394 modifiers: this._keyboard._modifiers, 395 }); 396 } 397 } 398 399 /** 400 * Shortcut for `mouse.move`, `mouse.down` and `mouse.up`. 401 * @param x - Horizontal position of the mouse. 402 * @param y - Vertical position of the mouse. 403 * @param options - Optional `MouseOptions`. 404 */ 405 async click( 406 x: number, 407 y: number, 408 options: MouseOptions & { delay?: number } = {} 409 ): Promise<void> { 410 const { delay = null } = options; 411 if (delay !== null) { 412 await this.move(x, y); 413 await this.down(options); 414 await new Promise((f) => setTimeout(f, delay)); 415 await this.up(options); 416 } else { 417 await this.move(x, y); 418 await this.down(options); 419 await this.up(options); 420 } 421 } 422 423 /** 424 * Dispatches a `mousedown` event. 425 * @param options - Optional `MouseOptions`. 426 */ 427 async down(options: MouseOptions = {}): Promise<void> { 428 const { button = 'left', clickCount = 1 } = options; 429 this._button = button; 430 await this._client.send('Input.dispatchMouseEvent', { 431 type: 'mousePressed', 432 button, 433 x: this._x, 434 y: this._y, 435 modifiers: this._keyboard._modifiers, 436 clickCount, 437 }); 438 } 439 440 /** 441 * Dispatches a `mouseup` event. 442 * @param options - Optional `MouseOptions`. 443 */ 444 async up(options: MouseOptions = {}): Promise<void> { 445 const { button = 'left', clickCount = 1 } = options; 446 this._button = 'none'; 447 await this._client.send('Input.dispatchMouseEvent', { 448 type: 'mouseReleased', 449 button, 450 x: this._x, 451 y: this._y, 452 modifiers: this._keyboard._modifiers, 453 clickCount, 454 }); 455 } 456 457 /** 458 * Dispatches a `mousewheel` event. 459 * @param options - Optional: `MouseWheelOptions`. 460 * 461 * @example 462 * An example of zooming into an element: 463 * ```js 464 * await page.goto('https://mdn.mozillademos.org/en-US/docs/Web/API/Element/wheel_event$samples/Scaling_an_element_via_the_wheel?revision=1587366'); 465 * 466 * const elem = await page.$('div'); 467 * const boundingBox = await elem.boundingBox(); 468 * await page.mouse.move( 469 * boundingBox.x + boundingBox.width / 2, 470 * boundingBox.y + boundingBox.height / 2 471 * ); 472 * 473 * await page.mouse.wheel({ deltaY: -100 }) 474 * ``` 475 */ 476 async wheel(options: MouseWheelOptions = {}): Promise<void> { 477 const { deltaX = 0, deltaY = 0 } = options; 478 await this._client.send('Input.dispatchMouseEvent', { 479 type: 'mouseWheel', 480 x: this._x, 481 y: this._y, 482 deltaX, 483 deltaY, 484 modifiers: this._keyboard._modifiers, 485 pointerType: 'mouse', 486 }); 487 } 488} 489 490/** 491 * The Touchscreen class exposes touchscreen events. 492 * @public 493 */ 494export class Touchscreen { 495 private _client: CDPSession; 496 private _keyboard: Keyboard; 497 498 /** 499 * @internal 500 */ 501 constructor(client: CDPSession, keyboard: Keyboard) { 502 this._client = client; 503 this._keyboard = keyboard; 504 } 505 506 /** 507 * Dispatches a `touchstart` and `touchend` event. 508 * @param x - Horizontal position of the tap. 509 * @param y - Vertical position of the tap. 510 */ 511 async tap(x: number, y: number): Promise<void> { 512 const touchPoints = [{ x: Math.round(x), y: Math.round(y) }]; 513 await this._client.send('Input.dispatchTouchEvent', { 514 type: 'touchStart', 515 touchPoints, 516 modifiers: this._keyboard._modifiers, 517 }); 518 await this._client.send('Input.dispatchTouchEvent', { 519 type: 'touchEnd', 520 touchPoints: [], 521 modifiers: this._keyboard._modifiers, 522 }); 523 } 524} 525