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