Input.ts (16657B)
1 /** 2 * @license 3 * Copyright 2017 Google Inc. 4 * SPDX-License-Identifier: Apache-2.0 5 */ 6 7 import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; 8 9 import type {Point} from '../api/ElementHandle.js'; 10 import { 11 Keyboard, 12 Mouse, 13 MouseButton, 14 Touchscreen, 15 type TouchHandle, 16 type KeyboardTypeOptions, 17 type KeyDownOptions, 18 type KeyPressOptions, 19 type MouseClickOptions, 20 type MouseMoveOptions, 21 type MouseOptions, 22 type MouseWheelOptions, 23 } from '../api/Input.js'; 24 import {UnsupportedOperation} from '../common/Errors.js'; 25 import {TouchError} from '../common/Errors.js'; 26 import type {KeyInput} from '../common/USKeyboardLayout.js'; 27 28 import type {BidiPage} from './Page.js'; 29 30 const enum InputId { 31 Mouse = '__puppeteer_mouse', 32 Keyboard = '__puppeteer_keyboard', 33 Wheel = '__puppeteer_wheel', 34 Finger = '__puppeteer_finger', 35 } 36 37 enum SourceActionsType { 38 None = 'none', 39 Key = 'key', 40 Pointer = 'pointer', 41 Wheel = 'wheel', 42 } 43 44 enum ActionType { 45 Pause = 'pause', 46 KeyDown = 'keyDown', 47 KeyUp = 'keyUp', 48 PointerUp = 'pointerUp', 49 PointerDown = 'pointerDown', 50 PointerMove = 'pointerMove', 51 Scroll = 'scroll', 52 } 53 54 const getBidiKeyValue = (key: KeyInput) => { 55 switch (key) { 56 case '\r': 57 case '\n': 58 key = 'Enter'; 59 break; 60 } 61 // Measures the number of code points rather than UTF-16 code units. 62 if ([...key].length === 1) { 63 return key; 64 } 65 switch (key) { 66 case 'Cancel': 67 return '\uE001'; 68 case 'Help': 69 return '\uE002'; 70 case 'Backspace': 71 return '\uE003'; 72 case 'Tab': 73 return '\uE004'; 74 case 'Clear': 75 return '\uE005'; 76 case 'Enter': 77 return '\uE007'; 78 case 'Shift': 79 case 'ShiftLeft': 80 return '\uE008'; 81 case 'Control': 82 case 'ControlLeft': 83 return '\uE009'; 84 case 'Alt': 85 case 'AltLeft': 86 return '\uE00A'; 87 case 'Pause': 88 return '\uE00B'; 89 case 'Escape': 90 return '\uE00C'; 91 case 'PageUp': 92 return '\uE00E'; 93 case 'PageDown': 94 return '\uE00F'; 95 case 'End': 96 return '\uE010'; 97 case 'Home': 98 return '\uE011'; 99 case 'ArrowLeft': 100 return '\uE012'; 101 case 'ArrowUp': 102 return '\uE013'; 103 case 'ArrowRight': 104 return '\uE014'; 105 case 'ArrowDown': 106 return '\uE015'; 107 case 'Insert': 108 return '\uE016'; 109 case 'Delete': 110 return '\uE017'; 111 case 'NumpadEqual': 112 return '\uE019'; 113 case 'Numpad0': 114 return '\uE01A'; 115 case 'Numpad1': 116 return '\uE01B'; 117 case 'Numpad2': 118 return '\uE01C'; 119 case 'Numpad3': 120 return '\uE01D'; 121 case 'Numpad4': 122 return '\uE01E'; 123 case 'Numpad5': 124 return '\uE01F'; 125 case 'Numpad6': 126 return '\uE020'; 127 case 'Numpad7': 128 return '\uE021'; 129 case 'Numpad8': 130 return '\uE022'; 131 case 'Numpad9': 132 return '\uE023'; 133 case 'NumpadMultiply': 134 return '\uE024'; 135 case 'NumpadAdd': 136 return '\uE025'; 137 case 'NumpadSubtract': 138 return '\uE027'; 139 case 'NumpadDecimal': 140 return '\uE028'; 141 case 'NumpadDivide': 142 return '\uE029'; 143 case 'F1': 144 return '\uE031'; 145 case 'F2': 146 return '\uE032'; 147 case 'F3': 148 return '\uE033'; 149 case 'F4': 150 return '\uE034'; 151 case 'F5': 152 return '\uE035'; 153 case 'F6': 154 return '\uE036'; 155 case 'F7': 156 return '\uE037'; 157 case 'F8': 158 return '\uE038'; 159 case 'F9': 160 return '\uE039'; 161 case 'F10': 162 return '\uE03A'; 163 case 'F11': 164 return '\uE03B'; 165 case 'F12': 166 return '\uE03C'; 167 case 'Meta': 168 case 'MetaLeft': 169 return '\uE03D'; 170 case 'ShiftRight': 171 return '\uE050'; 172 case 'ControlRight': 173 return '\uE051'; 174 case 'AltRight': 175 return '\uE052'; 176 case 'MetaRight': 177 return '\uE053'; 178 case 'Digit0': 179 return '0'; 180 case 'Digit1': 181 return '1'; 182 case 'Digit2': 183 return '2'; 184 case 'Digit3': 185 return '3'; 186 case 'Digit4': 187 return '4'; 188 case 'Digit5': 189 return '5'; 190 case 'Digit6': 191 return '6'; 192 case 'Digit7': 193 return '7'; 194 case 'Digit8': 195 return '8'; 196 case 'Digit9': 197 return '9'; 198 case 'KeyA': 199 return 'a'; 200 case 'KeyB': 201 return 'b'; 202 case 'KeyC': 203 return 'c'; 204 case 'KeyD': 205 return 'd'; 206 case 'KeyE': 207 return 'e'; 208 case 'KeyF': 209 return 'f'; 210 case 'KeyG': 211 return 'g'; 212 case 'KeyH': 213 return 'h'; 214 case 'KeyI': 215 return 'i'; 216 case 'KeyJ': 217 return 'j'; 218 case 'KeyK': 219 return 'k'; 220 case 'KeyL': 221 return 'l'; 222 case 'KeyM': 223 return 'm'; 224 case 'KeyN': 225 return 'n'; 226 case 'KeyO': 227 return 'o'; 228 case 'KeyP': 229 return 'p'; 230 case 'KeyQ': 231 return 'q'; 232 case 'KeyR': 233 return 'r'; 234 case 'KeyS': 235 return 's'; 236 case 'KeyT': 237 return 't'; 238 case 'KeyU': 239 return 'u'; 240 case 'KeyV': 241 return 'v'; 242 case 'KeyW': 243 return 'w'; 244 case 'KeyX': 245 return 'x'; 246 case 'KeyY': 247 return 'y'; 248 case 'KeyZ': 249 return 'z'; 250 case 'Semicolon': 251 return ';'; 252 case 'Equal': 253 return '='; 254 case 'Comma': 255 return ','; 256 case 'Minus': 257 return '-'; 258 case 'Period': 259 return '.'; 260 case 'Slash': 261 return '/'; 262 case 'Backquote': 263 return '`'; 264 case 'BracketLeft': 265 return '['; 266 case 'Backslash': 267 return '\\'; 268 case 'BracketRight': 269 return ']'; 270 case 'Quote': 271 return '"'; 272 default: 273 throw new Error(`Unknown key: "${key}"`); 274 } 275 }; 276 277 /** 278 * @internal 279 */ 280 export class BidiKeyboard extends Keyboard { 281 #page: BidiPage; 282 283 constructor(page: BidiPage) { 284 super(); 285 this.#page = page; 286 } 287 288 override async down( 289 key: KeyInput, 290 _options?: Readonly<KeyDownOptions>, 291 ): Promise<void> { 292 await this.#page.mainFrame().browsingContext.performActions([ 293 { 294 type: SourceActionsType.Key, 295 id: InputId.Keyboard, 296 actions: [ 297 { 298 type: ActionType.KeyDown, 299 value: getBidiKeyValue(key), 300 }, 301 ], 302 }, 303 ]); 304 } 305 306 override async up(key: KeyInput): Promise<void> { 307 await this.#page.mainFrame().browsingContext.performActions([ 308 { 309 type: SourceActionsType.Key, 310 id: InputId.Keyboard, 311 actions: [ 312 { 313 type: ActionType.KeyUp, 314 value: getBidiKeyValue(key), 315 }, 316 ], 317 }, 318 ]); 319 } 320 321 override async press( 322 key: KeyInput, 323 options: Readonly<KeyPressOptions> = {}, 324 ): Promise<void> { 325 const {delay = 0} = options; 326 const actions: Bidi.Input.KeySourceAction[] = [ 327 { 328 type: ActionType.KeyDown, 329 value: getBidiKeyValue(key), 330 }, 331 ]; 332 if (delay > 0) { 333 actions.push({ 334 type: ActionType.Pause, 335 duration: delay, 336 }); 337 } 338 actions.push({ 339 type: ActionType.KeyUp, 340 value: getBidiKeyValue(key), 341 }); 342 await this.#page.mainFrame().browsingContext.performActions([ 343 { 344 type: SourceActionsType.Key, 345 id: InputId.Keyboard, 346 actions, 347 }, 348 ]); 349 } 350 351 override async type( 352 text: string, 353 options: Readonly<KeyboardTypeOptions> = {}, 354 ): Promise<void> { 355 const {delay = 0} = options; 356 // This spread separates the characters into code points rather than UTF-16 357 // code units. 358 const values = ([...text] as KeyInput[]).map(getBidiKeyValue); 359 const actions: Bidi.Input.KeySourceAction[] = []; 360 if (delay <= 0) { 361 for (const value of values) { 362 actions.push( 363 { 364 type: ActionType.KeyDown, 365 value, 366 }, 367 { 368 type: ActionType.KeyUp, 369 value, 370 }, 371 ); 372 } 373 } else { 374 for (const value of values) { 375 actions.push( 376 { 377 type: ActionType.KeyDown, 378 value, 379 }, 380 { 381 type: ActionType.Pause, 382 duration: delay, 383 }, 384 { 385 type: ActionType.KeyUp, 386 value, 387 }, 388 ); 389 } 390 } 391 await this.#page.mainFrame().browsingContext.performActions([ 392 { 393 type: SourceActionsType.Key, 394 id: InputId.Keyboard, 395 actions, 396 }, 397 ]); 398 } 399 400 override async sendCharacter(char: string): Promise<void> { 401 // Measures the number of code points rather than UTF-16 code units. 402 if ([...char].length > 1) { 403 throw new Error('Cannot send more than 1 character.'); 404 } 405 const frame = await this.#page.focusedFrame(); 406 await frame.isolatedRealm().evaluate(async char => { 407 document.execCommand('insertText', false, char); 408 }, char); 409 } 410 } 411 412 /** 413 * @internal 414 */ 415 export interface BidiMouseClickOptions extends MouseClickOptions { 416 origin?: Bidi.Input.Origin; 417 } 418 419 /** 420 * @internal 421 */ 422 export interface BidiMouseMoveOptions extends MouseMoveOptions { 423 origin?: Bidi.Input.Origin; 424 } 425 426 /** 427 * @internal 428 */ 429 export interface BidiTouchMoveOptions { 430 origin?: Bidi.Input.Origin; 431 } 432 433 const getBidiButton = (button: MouseButton) => { 434 switch (button) { 435 case MouseButton.Left: 436 return 0; 437 case MouseButton.Middle: 438 return 1; 439 case MouseButton.Right: 440 return 2; 441 case MouseButton.Back: 442 return 3; 443 case MouseButton.Forward: 444 return 4; 445 } 446 }; 447 448 /** 449 * @internal 450 */ 451 export class BidiMouse extends Mouse { 452 #page: BidiPage; 453 #lastMovePoint: Point = {x: 0, y: 0}; 454 455 constructor(page: BidiPage) { 456 super(); 457 this.#page = page; 458 } 459 460 override async reset(): Promise<void> { 461 this.#lastMovePoint = {x: 0, y: 0}; 462 await this.#page.mainFrame().browsingContext.releaseActions(); 463 } 464 465 override async move( 466 x: number, 467 y: number, 468 options: Readonly<BidiMouseMoveOptions> = {}, 469 ): Promise<void> { 470 const from = this.#lastMovePoint; 471 const to = { 472 x: Math.round(x), 473 y: Math.round(y), 474 }; 475 const actions: Bidi.Input.PointerSourceAction[] = []; 476 const steps = options.steps ?? 0; 477 for (let i = 0; i < steps; ++i) { 478 actions.push({ 479 type: ActionType.PointerMove, 480 x: from.x + (to.x - from.x) * (i / steps), 481 y: from.y + (to.y - from.y) * (i / steps), 482 origin: options.origin, 483 }); 484 } 485 actions.push({ 486 type: ActionType.PointerMove, 487 ...to, 488 origin: options.origin, 489 }); 490 // https://w3c.github.io/webdriver-bidi/#command-input-performActions:~:text=input.PointerMoveAction%20%3D%20%7B%0A%20%20type%3A%20%22pointerMove%22%2C%0A%20%20x%3A%20js%2Dint%2C 491 this.#lastMovePoint = to; 492 await this.#page.mainFrame().browsingContext.performActions([ 493 { 494 type: SourceActionsType.Pointer, 495 id: InputId.Mouse, 496 actions, 497 }, 498 ]); 499 } 500 501 override async down(options: Readonly<MouseOptions> = {}): Promise<void> { 502 await this.#page.mainFrame().browsingContext.performActions([ 503 { 504 type: SourceActionsType.Pointer, 505 id: InputId.Mouse, 506 actions: [ 507 { 508 type: ActionType.PointerDown, 509 button: getBidiButton(options.button ?? MouseButton.Left), 510 }, 511 ], 512 }, 513 ]); 514 } 515 516 override async up(options: Readonly<MouseOptions> = {}): Promise<void> { 517 await this.#page.mainFrame().browsingContext.performActions([ 518 { 519 type: SourceActionsType.Pointer, 520 id: InputId.Mouse, 521 actions: [ 522 { 523 type: ActionType.PointerUp, 524 button: getBidiButton(options.button ?? MouseButton.Left), 525 }, 526 ], 527 }, 528 ]); 529 } 530 531 override async click( 532 x: number, 533 y: number, 534 options: Readonly<BidiMouseClickOptions> = {}, 535 ): Promise<void> { 536 const actions: Bidi.Input.PointerSourceAction[] = [ 537 { 538 type: ActionType.PointerMove, 539 x: Math.round(x), 540 y: Math.round(y), 541 origin: options.origin, 542 }, 543 ]; 544 const pointerDownAction = { 545 type: ActionType.PointerDown, 546 button: getBidiButton(options.button ?? MouseButton.Left), 547 } as const; 548 const pointerUpAction = { 549 type: ActionType.PointerUp, 550 button: pointerDownAction.button, 551 } as const; 552 for (let i = 1; i < (options.count ?? 1); ++i) { 553 actions.push(pointerDownAction, pointerUpAction); 554 } 555 actions.push(pointerDownAction); 556 if (options.delay) { 557 actions.push({ 558 type: ActionType.Pause, 559 duration: options.delay, 560 }); 561 } 562 actions.push(pointerUpAction); 563 await this.#page.mainFrame().browsingContext.performActions([ 564 { 565 type: SourceActionsType.Pointer, 566 id: InputId.Mouse, 567 actions, 568 }, 569 ]); 570 } 571 572 override async wheel( 573 options: Readonly<MouseWheelOptions> = {}, 574 ): Promise<void> { 575 await this.#page.mainFrame().browsingContext.performActions([ 576 { 577 type: SourceActionsType.Wheel, 578 id: InputId.Wheel, 579 actions: [ 580 { 581 type: ActionType.Scroll, 582 ...(this.#lastMovePoint ?? { 583 x: 0, 584 y: 0, 585 }), 586 deltaX: options.deltaX ?? 0, 587 deltaY: options.deltaY ?? 0, 588 }, 589 ], 590 }, 591 ]); 592 } 593 594 override drag(): never { 595 throw new UnsupportedOperation(); 596 } 597 598 override dragOver(): never { 599 throw new UnsupportedOperation(); 600 } 601 602 override dragEnter(): never { 603 throw new UnsupportedOperation(); 604 } 605 606 override drop(): never { 607 throw new UnsupportedOperation(); 608 } 609 610 override dragAndDrop(): never { 611 throw new UnsupportedOperation(); 612 } 613 } 614 615 /** 616 * @internal 617 */ 618 class BidiTouchHandle implements TouchHandle { 619 #started = false; 620 #x: number; 621 #y: number; 622 #bidiId: string; 623 #page: BidiPage; 624 #touchScreen: BidiTouchscreen; 625 #properties: Bidi.Input.PointerCommonProperties; 626 627 constructor( 628 page: BidiPage, 629 touchScreen: BidiTouchscreen, 630 id: number, 631 x: number, 632 y: number, 633 properties: Bidi.Input.PointerCommonProperties, 634 ) { 635 this.#page = page; 636 this.#touchScreen = touchScreen; 637 this.#x = Math.round(x); 638 this.#y = Math.round(y); 639 this.#properties = properties; 640 this.#bidiId = `${InputId.Finger}_${id}`; 641 } 642 643 async start(options: BidiTouchMoveOptions = {}): Promise<void> { 644 if (this.#started) { 645 throw new TouchError('Touch has already started'); 646 } 647 await this.#page.mainFrame().browsingContext.performActions([ 648 { 649 type: SourceActionsType.Pointer, 650 id: this.#bidiId, 651 parameters: { 652 pointerType: Bidi.Input.PointerType.Touch, 653 }, 654 actions: [ 655 { 656 type: ActionType.PointerMove, 657 x: this.#x, 658 y: this.#y, 659 origin: options.origin, 660 }, 661 { 662 ...this.#properties, 663 type: ActionType.PointerDown, 664 button: 0, 665 }, 666 ], 667 }, 668 ]); 669 this.#started = true; 670 } 671 672 move(x: number, y: number): Promise<void> { 673 const newX = Math.round(x); 674 const newY = Math.round(y); 675 return this.#page.mainFrame().browsingContext.performActions([ 676 { 677 type: SourceActionsType.Pointer, 678 id: this.#bidiId, 679 parameters: { 680 pointerType: Bidi.Input.PointerType.Touch, 681 }, 682 actions: [ 683 { 684 ...this.#properties, 685 type: ActionType.PointerMove, 686 x: newX, 687 y: newY, 688 }, 689 ], 690 }, 691 ]); 692 } 693 694 async end(): Promise<void> { 695 await this.#page.mainFrame().browsingContext.performActions([ 696 { 697 type: SourceActionsType.Pointer, 698 id: this.#bidiId, 699 parameters: { 700 pointerType: Bidi.Input.PointerType.Touch, 701 }, 702 actions: [ 703 { 704 type: ActionType.PointerUp, 705 button: 0, 706 }, 707 ], 708 }, 709 ]); 710 this.#touchScreen.removeHandle(this); 711 } 712 } 713 /** 714 * @internal 715 */ 716 export class BidiTouchscreen extends Touchscreen { 717 #page: BidiPage; 718 declare touches: BidiTouchHandle[]; 719 720 constructor(page: BidiPage) { 721 super(); 722 this.#page = page; 723 } 724 725 override async touchStart( 726 x: number, 727 y: number, 728 options: BidiTouchMoveOptions = {}, 729 ): Promise<TouchHandle> { 730 const id = this.idGenerator(); 731 const properties: Bidi.Input.PointerCommonProperties = { 732 width: 0.5 * 2, // 2 times default touch radius. 733 height: 0.5 * 2, // 2 times default touch radius. 734 pressure: 0.5, 735 altitudeAngle: Math.PI / 2, 736 }; 737 const touch = new BidiTouchHandle(this.#page, this, id, x, y, properties); 738 await touch.start(options); 739 this.touches.push(touch); 740 return touch; 741 } 742 }