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