1 | /**
|
2 | * @license
|
3 | * Copyright 2017 Google Inc.
|
4 | * SPDX-License-Identifier: Apache-2.0
|
5 | */
|
6 |
|
7 | import type {Protocol} from 'devtools-protocol';
|
8 |
|
9 | import {TouchError} from '../common/Errors.js';
|
10 | import type {KeyInput} from '../common/USKeyboardLayout.js';
|
11 | import {createIncrementalIdGenerator} from '../util/incremental-id-generator.js';
|
12 |
|
13 | import type {Point} from './ElementHandle.js';
|
14 |
|
15 | /**
|
16 | * @public
|
17 | */
|
18 | export interface KeyDownOptions {
|
19 | /**
|
20 | * @deprecated Do not use. This is automatically handled.
|
21 | */
|
22 | text?: string;
|
23 | /**
|
24 | * @deprecated Do not use. This is automatically handled.
|
25 | */
|
26 | commands?: string[];
|
27 | }
|
28 |
|
29 | /**
|
30 | * @public
|
31 | */
|
32 | export interface KeyboardTypeOptions {
|
33 | delay?: number;
|
34 | }
|
35 |
|
36 | /**
|
37 | * @public
|
38 | */
|
39 | export type KeyPressOptions = KeyDownOptions & KeyboardTypeOptions;
|
40 |
|
41 | /**
|
42 | * Keyboard provides an api for managing a virtual keyboard.
|
43 | * The high level api is {@link Keyboard."type"},
|
44 | * which takes raw characters and generates proper keydown, keypress/input,
|
45 | * and keyup events on your page.
|
46 | *
|
47 | * @remarks
|
48 | * For finer control, you can use {@link Keyboard.down},
|
49 | * {@link Keyboard.up}, and {@link Keyboard.sendCharacter}
|
50 | * to manually fire events as if they were generated from a real keyboard.
|
51 | *
|
52 | * On macOS, keyboard shortcuts like `⌘ A` -\> Select All do not work.
|
53 | * See {@link https://github.com/puppeteer/puppeteer/issues/1313 | #1313}.
|
54 | *
|
55 | * @example
|
56 | * An example of holding down `Shift` in order to select and delete some text:
|
57 | *
|
58 | * ```ts
|
59 | * await page.keyboard.type('Hello World!');
|
60 | * await page.keyboard.press('ArrowLeft');
|
61 | *
|
62 | * await page.keyboard.down('Shift');
|
63 | * for (let i = 0; i < ' World'.length; i++)
|
64 | * await page.keyboard.press('ArrowLeft');
|
65 | * await page.keyboard.up('Shift');
|
66 | *
|
67 | * await page.keyboard.press('Backspace');
|
68 | * // Result text will end up saying 'Hello!'
|
69 | * ```
|
70 | *
|
71 | * @example
|
72 | * An example of pressing `A`
|
73 | *
|
74 | * ```ts
|
75 | * await page.keyboard.down('Shift');
|
76 | * await page.keyboard.press('KeyA');
|
77 | * await page.keyboard.up('Shift');
|
78 | * ```
|
79 | *
|
80 | * @public
|
81 | */
|
82 | export abstract class Keyboard {
|
83 | /**
|
84 | * @internal
|
85 | */
|
86 | constructor() {}
|
87 |
|
88 | /**
|
89 | * Dispatches a `keydown` event.
|
90 | *
|
91 | * @remarks
|
92 | * If `key` is a single character and no modifier keys besides `Shift`
|
93 | * are being held down, a `keypress`/`input` event will also generated.
|
94 | * The `text` option can be specified to force an input event to be generated.
|
95 | * If `key` is a modifier key, `Shift`, `Meta`, `Control`, or `Alt`,
|
96 | * subsequent key presses will be sent with that modifier active.
|
97 | * To release the modifier key, use {@link Keyboard.up}.
|
98 | *
|
99 | * After the key is pressed once, subsequent calls to
|
100 | * {@link Keyboard.down} will have
|
101 | * {@link https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/repeat | repeat}
|
102 | * set to true. To release the key, use {@link Keyboard.up}.
|
103 | *
|
104 | * Modifier keys DO influence {@link Keyboard.down}.
|
105 | * Holding down `Shift` will type the text in upper case.
|
106 | *
|
107 | * @param key - Name of key to press, such as `ArrowLeft`.
|
108 | * See {@link KeyInput} for a list of all key names.
|
109 | *
|
110 | * @param options - An object of options. Accepts text which, if specified,
|
111 | * generates an input event with this text. Accepts commands which, if specified,
|
112 | * is the commands of keyboard shortcuts,
|
113 | * see {@link https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/editing/commands/editor_command_names.h | Chromium Source Code} for valid command names.
|
114 | */
|
115 | abstract down(
|
116 | key: KeyInput,
|
117 | options?: Readonly<KeyDownOptions>,
|
118 | ): Promise<void>;
|
119 |
|
120 | /**
|
121 | * Dispatches a `keyup` event.
|
122 | *
|
123 | * @param key - Name of key to release, such as `ArrowLeft`.
|
124 | * See {@link KeyInput | KeyInput}
|
125 | * for a list of all key names.
|
126 | */
|
127 | abstract up(key: KeyInput): Promise<void>;
|
128 |
|
129 | /**
|
130 | * Dispatches a `keypress` and `input` event.
|
131 | * This does not send a `keydown` or `keyup` event.
|
132 | *
|
133 | * @remarks
|
134 | * Modifier keys DO NOT effect {@link Keyboard.sendCharacter | Keyboard.sendCharacter}.
|
135 | * Holding down `Shift` will not type the text in upper case.
|
136 | *
|
137 | * @example
|
138 | *
|
139 | * ```ts
|
140 | * page.keyboard.sendCharacter('嗨');
|
141 | * ```
|
142 | *
|
143 | * @param char - Character to send into the page.
|
144 | */
|
145 | abstract sendCharacter(char: string): Promise<void>;
|
146 |
|
147 | /**
|
148 | * Sends a `keydown`, `keypress`/`input`,
|
149 | * and `keyup` event for each character in the text.
|
150 | *
|
151 | * @remarks
|
152 | * To press a special key, like `Control` or `ArrowDown`,
|
153 | * use {@link Keyboard.press}.
|
154 | *
|
155 | * Modifier keys DO NOT effect `keyboard.type`.
|
156 | * Holding down `Shift` will not type the text in upper case.
|
157 | *
|
158 | * @example
|
159 | *
|
160 | * ```ts
|
161 | * await page.keyboard.type('Hello'); // Types instantly
|
162 | * await page.keyboard.type('World', {delay: 100}); // Types slower, like a user
|
163 | * ```
|
164 | *
|
165 | * @param text - A text to type into a focused element.
|
166 | * @param options - An object of options. Accepts delay which,
|
167 | * if specified, is the time to wait between `keydown` and `keyup` in milliseconds.
|
168 | * Defaults to 0.
|
169 | */
|
170 | abstract type(
|
171 | text: string,
|
172 | options?: Readonly<KeyboardTypeOptions>,
|
173 | ): Promise<void>;
|
174 |
|
175 | /**
|
176 | * Shortcut for {@link Keyboard.down}
|
177 | * and {@link Keyboard.up}.
|
178 | *
|
179 | * @remarks
|
180 | * If `key` is a single character and no modifier keys besides `Shift`
|
181 | * are being held down, a `keypress`/`input` event will also generated.
|
182 | * The `text` option can be specified to force an input event to be generated.
|
183 | *
|
184 | * Modifier keys DO effect {@link Keyboard.press}.
|
185 | * Holding down `Shift` will type the text in upper case.
|
186 | *
|
187 | * @param key - Name of key to press, such as `ArrowLeft`.
|
188 | * See {@link KeyInput} for a list of all key names.
|
189 | *
|
190 | * @param options - An object of options. Accepts text which, if specified,
|
191 | * generates an input event with this text. Accepts delay which,
|
192 | * if specified, is the time to wait between `keydown` and `keyup` in milliseconds.
|
193 | * Defaults to 0. Accepts commands which, if specified,
|
194 | * is the commands of keyboard shortcuts,
|
195 | * see {@link https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/editing/commands/editor_command_names.h | Chromium Source Code} for valid command names.
|
196 | */
|
197 | abstract press(
|
198 | key: KeyInput,
|
199 | options?: Readonly<KeyPressOptions>,
|
200 | ): Promise<void>;
|
201 | }
|
202 |
|
203 | /**
|
204 | * @public
|
205 | */
|
206 | export interface MouseOptions {
|
207 | /**
|
208 | * Determines which button will be pressed.
|
209 | *
|
210 | * @defaultValue `'left'`
|
211 | */
|
212 | button?: MouseButton;
|
213 | /**
|
214 | * Determines the click count for the mouse event. This does not perform
|
215 | * multiple clicks.
|
216 | *
|
217 | * @deprecated Use {@link MouseClickOptions.count}.
|
218 | * @defaultValue `1`
|
219 | */
|
220 | clickCount?: number;
|
221 | }
|
222 |
|
223 | /**
|
224 | * @public
|
225 | */
|
226 | export interface MouseClickOptions extends MouseOptions {
|
227 | /**
|
228 | * Time (in ms) to delay the mouse release after the mouse press.
|
229 | */
|
230 | delay?: number;
|
231 | /**
|
232 | * Number of clicks to perform.
|
233 | *
|
234 | * @defaultValue `1`
|
235 | */
|
236 | count?: number;
|
237 | }
|
238 |
|
239 | /**
|
240 | * @public
|
241 | */
|
242 | export interface MouseWheelOptions {
|
243 | deltaX?: number;
|
244 | deltaY?: number;
|
245 | }
|
246 |
|
247 | /**
|
248 | * @public
|
249 | */
|
250 | export interface MouseMoveOptions {
|
251 | /**
|
252 | * Determines the number of movements to make from the current mouse position
|
253 | * to the new one.
|
254 | *
|
255 | * @defaultValue `1`
|
256 | */
|
257 | steps?: number;
|
258 | }
|
259 |
|
260 | /**
|
261 | * Enum of valid mouse buttons.
|
262 | *
|
263 | * @public
|
264 | */
|
265 | export const MouseButton = Object.freeze({
|
266 | Left: 'left',
|
267 | Right: 'right',
|
268 | Middle: 'middle',
|
269 | Back: 'back',
|
270 | Forward: 'forward',
|
271 | }) satisfies Record<string, Protocol.Input.MouseButton>;
|
272 |
|
273 | /**
|
274 | * @public
|
275 | */
|
276 | export type MouseButton = (typeof MouseButton)[keyof typeof MouseButton];
|
277 |
|
278 | /**
|
279 | * The Mouse class operates in main-frame CSS pixels
|
280 | * relative to the top-left corner of the viewport.
|
281 | *
|
282 | * @remarks
|
283 | * Every `page` object has its own Mouse, accessible with {@link Page.mouse}.
|
284 | *
|
285 | * @example
|
286 | *
|
287 | * ```ts
|
288 | * // Using ‘page.mouse’ to trace a 100x100 square.
|
289 | * await page.mouse.move(0, 0);
|
290 | * await page.mouse.down();
|
291 | * await page.mouse.move(0, 100);
|
292 | * await page.mouse.move(100, 100);
|
293 | * await page.mouse.move(100, 0);
|
294 | * await page.mouse.move(0, 0);
|
295 | * await page.mouse.up();
|
296 | * ```
|
297 | *
|
298 | * **Note**: The mouse events trigger synthetic `MouseEvent`s.
|
299 | * This means that it does not fully replicate the functionality of what a normal user
|
300 | * would be able to do with their mouse.
|
301 | *
|
302 | * For example, dragging and selecting text is not possible using `page.mouse`.
|
303 | * Instead, you can use the {@link https://developer.mozilla.org/en-US/docs/Web/API/DocumentOrShadowRoot/getSelection | `DocumentOrShadowRoot.getSelection()`} functionality implemented in the platform.
|
304 | *
|
305 | * @example
|
306 | * For example, if you want to select all content between nodes:
|
307 | *
|
308 | * ```ts
|
309 | * await page.evaluate(
|
310 | * (from, to) => {
|
311 | * const selection = from.getRootNode().getSelection();
|
312 | * const range = document.createRange();
|
313 | * range.setStartBefore(from);
|
314 | * range.setEndAfter(to);
|
315 | * selection.removeAllRanges();
|
316 | * selection.addRange(range);
|
317 | * },
|
318 | * fromJSHandle,
|
319 | * toJSHandle,
|
320 | * );
|
321 | * ```
|
322 | *
|
323 | * If you then would want to copy-paste your selection, you can use the clipboard api:
|
324 | *
|
325 | * ```ts
|
326 | * // The clipboard api does not allow you to copy, unless the tab is focused.
|
327 | * await page.bringToFront();
|
328 | * await page.evaluate(() => {
|
329 | * // Copy the selected content to the clipboard
|
330 | * document.execCommand('copy');
|
331 | * // Obtain the content of the clipboard as a string
|
332 | * return navigator.clipboard.readText();
|
333 | * });
|
334 | * ```
|
335 | *
|
336 | * **Note**: If you want access to the clipboard API,
|
337 | * you have to give it permission to do so:
|
338 | *
|
339 | * ```ts
|
340 | * await browser
|
341 | * .defaultBrowserContext()
|
342 | * .overridePermissions('<your origin>', [
|
343 | * 'clipboard-read',
|
344 | * 'clipboard-write',
|
345 | * ]);
|
346 | * ```
|
347 | *
|
348 | * @public
|
349 | */
|
350 | export abstract class Mouse {
|
351 | /**
|
352 | * @internal
|
353 | */
|
354 | constructor() {}
|
355 |
|
356 | /**
|
357 | * Resets the mouse to the default state: No buttons pressed; position at
|
358 | * (0,0).
|
359 | */
|
360 | abstract reset(): Promise<void>;
|
361 |
|
362 | /**
|
363 | * Moves the mouse to the given coordinate.
|
364 | *
|
365 | * @param x - Horizontal position of the mouse.
|
366 | * @param y - Vertical position of the mouse.
|
367 | * @param options - Options to configure behavior.
|
368 | */
|
369 | abstract move(
|
370 | x: number,
|
371 | y: number,
|
372 | options?: Readonly<MouseMoveOptions>,
|
373 | ): Promise<void>;
|
374 |
|
375 | /**
|
376 | * Presses the mouse.
|
377 | *
|
378 | * @param options - Options to configure behavior.
|
379 | */
|
380 | abstract down(options?: Readonly<MouseOptions>): Promise<void>;
|
381 |
|
382 | /**
|
383 | * Releases the mouse.
|
384 | *
|
385 | * @param options - Options to configure behavior.
|
386 | */
|
387 | abstract up(options?: Readonly<MouseOptions>): Promise<void>;
|
388 |
|
389 | /**
|
390 | * Shortcut for `mouse.move`, `mouse.down` and `mouse.up`.
|
391 | *
|
392 | * @param x - Horizontal position of the mouse.
|
393 | * @param y - Vertical position of the mouse.
|
394 | * @param options - Options to configure behavior.
|
395 | */
|
396 | abstract click(
|
397 | x: number,
|
398 | y: number,
|
399 | options?: Readonly<MouseClickOptions>,
|
400 | ): Promise<void>;
|
401 |
|
402 | /**
|
403 | * Dispatches a `mousewheel` event.
|
404 | * @param options - Optional: `MouseWheelOptions`.
|
405 | *
|
406 | * @example
|
407 | * An example of zooming into an element:
|
408 | *
|
409 | * ```ts
|
410 | * await page.goto(
|
411 | * 'https://mdn.mozillademos.org/en-US/docs/Web/API/Element/wheel_event$samples/Scaling_an_element_via_the_wheel?revision=1587366',
|
412 | * );
|
413 | *
|
414 | * const elem = await page.$('div');
|
415 | * const boundingBox = await elem.boundingBox();
|
416 | * await page.mouse.move(
|
417 | * boundingBox.x + boundingBox.width / 2,
|
418 | * boundingBox.y + boundingBox.height / 2,
|
419 | * );
|
420 | *
|
421 | * await page.mouse.wheel({deltaY: -100});
|
422 | * ```
|
423 | */
|
424 | abstract wheel(options?: Readonly<MouseWheelOptions>): Promise<void>;
|
425 |
|
426 | /**
|
427 | * Dispatches a `drag` event.
|
428 | * @param start - starting point for drag
|
429 | * @param target - point to drag to
|
430 | */
|
431 | abstract drag(start: Point, target: Point): Promise<Protocol.Input.DragData>;
|
432 |
|
433 | /**
|
434 | * Dispatches a `dragenter` event.
|
435 | * @param target - point for emitting `dragenter` event
|
436 | * @param data - drag data containing items and operations mask
|
437 | */
|
438 | abstract dragEnter(
|
439 | target: Point,
|
440 | data: Protocol.Input.DragData,
|
441 | ): Promise<void>;
|
442 |
|
443 | /**
|
444 | * Dispatches a `dragover` event.
|
445 | * @param target - point for emitting `dragover` event
|
446 | * @param data - drag data containing items and operations mask
|
447 | */
|
448 | abstract dragOver(
|
449 | target: Point,
|
450 | data: Protocol.Input.DragData,
|
451 | ): Promise<void>;
|
452 |
|
453 | /**
|
454 | * Performs a dragenter, dragover, and drop in sequence.
|
455 | * @param target - point to drop on
|
456 | * @param data - drag data containing items and operations mask
|
457 | */
|
458 | abstract drop(target: Point, data: Protocol.Input.DragData): Promise<void>;
|
459 |
|
460 | /**
|
461 | * Performs a drag, dragenter, dragover, and drop in sequence.
|
462 | * @param start - point to drag from
|
463 | * @param target - point to drop on
|
464 | * @param options - An object of options. Accepts delay which,
|
465 | * if specified, is the time to wait between `dragover` and `drop` in milliseconds.
|
466 | * Defaults to 0.
|
467 | */
|
468 | abstract dragAndDrop(
|
469 | start: Point,
|
470 | target: Point,
|
471 | options?: {delay?: number},
|
472 | ): Promise<void>;
|
473 | }
|
474 | /**
|
475 | * The TouchHandle interface exposes methods to manipulate touches that have been started
|
476 | * @public
|
477 | */
|
478 | export interface TouchHandle {
|
479 | /**
|
480 | * Dispatches a `touchMove` event for this touch.
|
481 | * @param x - Horizontal position of the move.
|
482 | * @param y - Vertical position of the move.
|
483 | */
|
484 | move(x: number, y: number): Promise<void>;
|
485 | /**
|
486 | * Dispatches a `touchend` event for this touch.
|
487 | */
|
488 | end(): Promise<void>;
|
489 | }
|
490 | /**
|
491 | * The Touchscreen class exposes touchscreen events.
|
492 | * @public
|
493 | */
|
494 | export abstract class Touchscreen {
|
495 | /**
|
496 | * @internal
|
497 | */
|
498 | idGenerator = createIncrementalIdGenerator();
|
499 | /**
|
500 | * @internal
|
501 | */
|
502 | touches: TouchHandle[] = [];
|
503 | /**
|
504 | * @internal
|
505 | */
|
506 | constructor() {}
|
507 |
|
508 | /**
|
509 | * @internal
|
510 | */
|
511 | removeHandle(handle: TouchHandle): void {
|
512 | const index = this.touches.indexOf(handle);
|
513 | if (index === -1) {
|
514 | return;
|
515 | }
|
516 | this.touches.splice(index, 1);
|
517 | }
|
518 |
|
519 | /**
|
520 | * Dispatches a `touchstart` and `touchend` event.
|
521 | * @param x - Horizontal position of the tap.
|
522 | * @param y - Vertical position of the tap.
|
523 | */
|
524 | async tap(x: number, y: number): Promise<void> {
|
525 | const touch = await this.touchStart(x, y);
|
526 | await touch.end();
|
527 | }
|
528 |
|
529 | /**
|
530 | * Dispatches a `touchstart` event.
|
531 | * @param x - Horizontal position of the tap.
|
532 | * @param y - Vertical position of the tap.
|
533 | * @returns A handle for the touch that was started.
|
534 | */
|
535 | abstract touchStart(x: number, y: number): Promise<TouchHandle>;
|
536 |
|
537 | /**
|
538 | * Dispatches a `touchMove` event on the first touch that is active.
|
539 | * @param x - Horizontal position of the move.
|
540 | * @param y - Vertical position of the move.
|
541 | *
|
542 | * @remarks
|
543 | *
|
544 | * Not every `touchMove` call results in a `touchmove` event being emitted,
|
545 | * depending on the browser's optimizations. For example, Chrome
|
546 | * {@link https://developer.chrome.com/blog/a-more-compatible-smoother-touch/#chromes-new-model-the-throttled-async-touchmove-model | throttles}
|
547 | * touch move events.
|
548 | */
|
549 | async touchMove(x: number, y: number): Promise<void> {
|
550 | const touch = this.touches[0];
|
551 | if (!touch) {
|
552 | throw new TouchError('Must start a new Touch first');
|
553 | }
|
554 | return await touch.move(x, y);
|
555 | }
|
556 |
|
557 | /**
|
558 | * Dispatches a `touchend` event on the first touch that is active.
|
559 | */
|
560 | async touchEnd(): Promise<void> {
|
561 | const touch = this.touches.shift();
|
562 | if (!touch) {
|
563 | throw new TouchError('Must start a new Touch first');
|
564 | }
|
565 | await touch.end();
|
566 | }
|
567 | }
|