UNPKG

47.3 kBPlain TextView Raw
1/**
2 * @license
3 * Copyright 2023 Google Inc.
4 * SPDX-License-Identifier: Apache-2.0
5 */
6
7import type {Protocol} from 'devtools-protocol';
8
9import type {Frame} from '../api/Frame.js';
10import {getQueryHandlerAndSelector} from '../common/GetQueryHandler.js';
11import {LazyArg} from '../common/LazyArg.js';
12import type {
13 AwaitableIterable,
14 ElementFor,
15 EvaluateFuncWith,
16 HandleFor,
17 HandleOr,
18 NodeFor,
19} from '../common/types.js';
20import type {KeyInput} from '../common/USKeyboardLayout.js';
21import {isString, withSourcePuppeteerURLIfNone} from '../common/util.js';
22import {assert} from '../util/assert.js';
23import {AsyncIterableUtil} from '../util/AsyncIterableUtil.js';
24import {throwIfDisposed} from '../util/decorators.js';
25
26import {_isElementHandle} from './ElementHandleSymbol.js';
27import type {
28 KeyboardTypeOptions,
29 KeyPressOptions,
30 MouseClickOptions,
31 TouchHandle,
32} from './Input.js';
33import {JSHandle} from './JSHandle.js';
34import type {
35 QueryOptions,
36 ScreenshotOptions,
37 WaitForSelectorOptions,
38} from './Page.js';
39
40/**
41 * @public
42 */
43export type Quad = [Point, Point, Point, Point];
44
45/**
46 * @public
47 */
48export interface BoxModel {
49 content: Quad;
50 padding: Quad;
51 border: Quad;
52 margin: Quad;
53 width: number;
54 height: number;
55}
56
57/**
58 * @public
59 */
60export interface BoundingBox extends Point {
61 /**
62 * the width of the element in pixels.
63 */
64 width: number;
65 /**
66 * the height of the element in pixels.
67 */
68 height: number;
69}
70
71/**
72 * @public
73 */
74export interface Offset {
75 /**
76 * x-offset for the clickable point relative to the top-left corner of the border box.
77 */
78 x: number;
79 /**
80 * y-offset for the clickable point relative to the top-left corner of the border box.
81 */
82 y: number;
83}
84
85/**
86 * @public
87 */
88export interface ClickOptions extends MouseClickOptions {
89 /**
90 * Offset for the clickable point relative to the top-left corner of the border box.
91 */
92 offset?: Offset;
93}
94
95/**
96 * @public
97 */
98export interface Point {
99 x: number;
100 y: number;
101}
102
103/**
104 * @public
105 */
106export interface ElementScreenshotOptions extends ScreenshotOptions {
107 /**
108 * @defaultValue `true`
109 */
110 scrollIntoView?: boolean;
111}
112
113/**
114 * A given method will have it's `this` replaced with an isolated version of
115 * `this` when decorated with this decorator.
116 *
117 * All changes of isolated `this` are reflected on the actual `this`.
118 *
119 * @internal
120 */
121export function bindIsolatedHandle<This extends ElementHandle<Node>>(
122 target: (this: This, ...args: any[]) => Promise<any>,
123 _: unknown,
124): typeof target {
125 return async function (...args) {
126 // If the handle is already isolated, then we don't need to adopt it
127 // again.
128 if (this.realm === this.frame.isolatedRealm()) {
129 return await target.call(this, ...args);
130 }
131 let adoptedThis: This;
132 if (this['isolatedHandle']) {
133 adoptedThis = this['isolatedHandle'];
134 } else {
135 this['isolatedHandle'] = adoptedThis = await this.frame
136 .isolatedRealm()
137 .adoptHandle(this);
138 }
139 const result = await target.call(adoptedThis, ...args);
140 // If the function returns `adoptedThis`, then we return `this`.
141 if (result === adoptedThis) {
142 return this;
143 }
144 // If the function returns a handle, transfer it into the current realm.
145 if (result instanceof JSHandle) {
146 return await this.realm.transferHandle(result);
147 }
148 // If the function returns an array of handlers, transfer them into the
149 // current realm.
150 if (Array.isArray(result)) {
151 await Promise.all(
152 result.map(async (item, index, result) => {
153 if (item instanceof JSHandle) {
154 result[index] = await this.realm.transferHandle(item);
155 }
156 }),
157 );
158 }
159 if (result instanceof Map) {
160 await Promise.all(
161 [...result.entries()].map(async ([key, value]) => {
162 if (value instanceof JSHandle) {
163 result.set(key, await this.realm.transferHandle(value));
164 }
165 }),
166 );
167 }
168 return result;
169 };
170}
171
172/**
173 * ElementHandle represents an in-page DOM element.
174 *
175 * @remarks
176 * ElementHandles can be created with the {@link Page.$} method.
177 *
178 * ```ts
179 * import puppeteer from 'puppeteer';
180 *
181 * (async () => {
182 * const browser = await puppeteer.launch();
183 * const page = await browser.newPage();
184 * await page.goto('https://example.com');
185 * const hrefElement = await page.$('a');
186 * await hrefElement.click();
187 * // ...
188 * })();
189 * ```
190 *
191 * ElementHandle prevents the DOM element from being garbage-collected unless the
192 * handle is {@link JSHandle.dispose | disposed}. ElementHandles are auto-disposed
193 * when their origin frame gets navigated.
194 *
195 * ElementHandle instances can be used as arguments in {@link Page.$eval} and
196 * {@link Page.evaluate} methods.
197 *
198 * If you're using TypeScript, ElementHandle takes a generic argument that
199 * denotes the type of element the handle is holding within. For example, if you
200 * have a handle to a `<select>` element, you can type it as
201 * `ElementHandle<HTMLSelectElement>` and you get some nicer type checks.
202 *
203 * @public
204 */
205export abstract class ElementHandle<
206 ElementType extends Node = Element,
207> extends JSHandle<ElementType> {
208 /**
209 * @internal
210 */
211 declare [_isElementHandle]: boolean;
212
213 /**
214 * @internal
215 * Cached isolatedHandle to prevent
216 * trying to adopt it multiple times
217 */
218 isolatedHandle?: typeof this;
219
220 /**
221 * @internal
222 */
223 protected readonly handle;
224
225 /**
226 * @internal
227 */
228 constructor(handle: JSHandle<ElementType>) {
229 super();
230 this.handle = handle;
231 this[_isElementHandle] = true;
232 }
233
234 /**
235 * @internal
236 */
237 override get id(): string | undefined {
238 return this.handle.id;
239 }
240
241 /**
242 * @internal
243 */
244 override get disposed(): boolean {
245 return this.handle.disposed;
246 }
247
248 /**
249 * @internal
250 */
251 @throwIfDisposed()
252 @bindIsolatedHandle
253 override async getProperty<K extends keyof ElementType>(
254 propertyName: HandleOr<K>,
255 ): Promise<HandleFor<ElementType[K]>> {
256 return await this.handle.getProperty(propertyName);
257 }
258
259 /**
260 * @internal
261 */
262 @throwIfDisposed()
263 @bindIsolatedHandle
264 override async getProperties(): Promise<Map<string, JSHandle>> {
265 return await this.handle.getProperties();
266 }
267
268 /**
269 * @internal
270 */
271 override async evaluate<
272 Params extends unknown[],
273 Func extends EvaluateFuncWith<ElementType, Params> = EvaluateFuncWith<
274 ElementType,
275 Params
276 >,
277 >(
278 pageFunction: Func | string,
279 ...args: Params
280 ): Promise<Awaited<ReturnType<Func>>> {
281 pageFunction = withSourcePuppeteerURLIfNone(
282 this.evaluate.name,
283 pageFunction,
284 );
285 return await this.handle.evaluate(pageFunction, ...args);
286 }
287
288 /**
289 * @internal
290 */
291 override async evaluateHandle<
292 Params extends unknown[],
293 Func extends EvaluateFuncWith<ElementType, Params> = EvaluateFuncWith<
294 ElementType,
295 Params
296 >,
297 >(
298 pageFunction: Func | string,
299 ...args: Params
300 ): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
301 pageFunction = withSourcePuppeteerURLIfNone(
302 this.evaluateHandle.name,
303 pageFunction,
304 );
305 return await this.handle.evaluateHandle(pageFunction, ...args);
306 }
307
308 /**
309 * @internal
310 */
311 @throwIfDisposed()
312 @bindIsolatedHandle
313 override async jsonValue(): Promise<ElementType> {
314 return await this.handle.jsonValue();
315 }
316
317 /**
318 * @internal
319 */
320 override toString(): string {
321 return this.handle.toString();
322 }
323
324 /**
325 * @internal
326 */
327 override remoteObject(): Protocol.Runtime.RemoteObject {
328 return this.handle.remoteObject();
329 }
330
331 /**
332 * @internal
333 */
334 override dispose(): Promise<void> {
335 return this.handle.dispose();
336 }
337
338 /**
339 * @internal
340 */
341 override asElement(): ElementHandle<ElementType> {
342 return this;
343 }
344
345 /**
346 * Frame corresponding to the current handle.
347 */
348 abstract get frame(): Frame;
349
350 /**
351 * Queries the current element for an element matching the given selector.
352 *
353 * @param selector -
354 * {@link https://pptr.dev/guides/page-interactions#selectors | selector}
355 * to query the page for.
356 * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | CSS selectors}
357 * can be passed as-is and a
358 * {@link https://pptr.dev/guides/page-interactions#non-css-selectors | Puppeteer-specific selector syntax}
359 * allows quering by
360 * {@link https://pptr.dev/guides/page-interactions#text-selectors--p-text | text},
361 * {@link https://pptr.dev/guides/page-interactions#aria-selectors--p-aria | a11y role and name},
362 * and
363 * {@link https://pptr.dev/guides/page-interactions#xpath-selectors--p-xpath | xpath}
364 * and
365 * {@link https://pptr.dev/guides/page-interactions#querying-elements-in-shadow-dom | combining these queries across shadow roots}.
366 * Alternatively, you can specify the selector type using a
367 * {@link https://pptr.dev/guides/page-interactions#prefixed-selector-syntax | prefix}.
368 * @returns A {@link ElementHandle | element handle} to the first element
369 * matching the given selector. Otherwise, `null`.
370 */
371 @throwIfDisposed()
372 @bindIsolatedHandle
373 async $<Selector extends string>(
374 selector: Selector,
375 ): Promise<ElementHandle<NodeFor<Selector>> | null> {
376 const {updatedSelector, QueryHandler} =
377 getQueryHandlerAndSelector(selector);
378 return (await QueryHandler.queryOne(
379 this,
380 updatedSelector,
381 )) as ElementHandle<NodeFor<Selector>> | null;
382 }
383
384 /**
385 * Queries the current element for all elements matching the given selector.
386 *
387 * @param selector -
388 * {@link https://pptr.dev/guides/page-interactions#selectors | selector}
389 * to query the page for.
390 * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | CSS selectors}
391 * can be passed as-is and a
392 * {@link https://pptr.dev/guides/page-interactions#non-css-selectors | Puppeteer-specific selector syntax}
393 * allows quering by
394 * {@link https://pptr.dev/guides/page-interactions#text-selectors--p-text | text},
395 * {@link https://pptr.dev/guides/page-interactions#aria-selectors--p-aria | a11y role and name},
396 * and
397 * {@link https://pptr.dev/guides/page-interactions#xpath-selectors--p-xpath | xpath}
398 * and
399 * {@link https://pptr.dev/guides/page-interactions#querying-elements-in-shadow-dom | combining these queries across shadow roots}.
400 * Alternatively, you can specify the selector type using a
401 * {@link https://pptr.dev/guides/page-interactions#prefixed-selector-syntax | prefix}.
402 * @returns An array of {@link ElementHandle | element handles} that point to
403 * elements matching the given selector.
404 */
405 @throwIfDisposed()
406 async $$<Selector extends string>(
407 selector: Selector,
408 options?: QueryOptions,
409 ): Promise<Array<ElementHandle<NodeFor<Selector>>>> {
410 if (options?.isolate === false) {
411 return await this.#$$impl(selector);
412 }
413 return await this.#$$(selector);
414 }
415
416 /**
417 * Isolates {@link ElementHandle.$$} if needed.
418 *
419 * @internal
420 */
421 @bindIsolatedHandle
422 async #$$<Selector extends string>(
423 selector: Selector,
424 ): Promise<Array<ElementHandle<NodeFor<Selector>>>> {
425 return await this.#$$impl(selector);
426 }
427
428 /**
429 * Implementation for {@link ElementHandle.$$}.
430 *
431 * @internal
432 */
433 async #$$impl<Selector extends string>(
434 selector: Selector,
435 ): Promise<Array<ElementHandle<NodeFor<Selector>>>> {
436 const {updatedSelector, QueryHandler} =
437 getQueryHandlerAndSelector(selector);
438 return await (AsyncIterableUtil.collect(
439 QueryHandler.queryAll(this, updatedSelector),
440 ) as Promise<Array<ElementHandle<NodeFor<Selector>>>>);
441 }
442
443 /**
444 * Runs the given function on the first element matching the given selector in
445 * the current element.
446 *
447 * If the given function returns a promise, then this method will wait till
448 * the promise resolves.
449 *
450 * @example
451 *
452 * ```ts
453 * const tweetHandle = await page.$('.tweet');
454 * expect(await tweetHandle.$eval('.like', node => node.innerText)).toBe(
455 * '100',
456 * );
457 * expect(await tweetHandle.$eval('.retweets', node => node.innerText)).toBe(
458 * '10',
459 * );
460 * ```
461 *
462 * @param selector -
463 * {@link https://pptr.dev/guides/page-interactions#selectors | selector}
464 * to query the page for.
465 * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | CSS selectors}
466 * can be passed as-is and a
467 * {@link https://pptr.dev/guides/page-interactions#non-css-selectors | Puppeteer-specific selector syntax}
468 * allows quering by
469 * {@link https://pptr.dev/guides/page-interactions#text-selectors--p-text | text},
470 * {@link https://pptr.dev/guides/page-interactions#aria-selectors--p-aria | a11y role and name},
471 * and
472 * {@link https://pptr.dev/guides/page-interactions#xpath-selectors--p-xpath | xpath}
473 * and
474 * {@link https://pptr.dev/guides/page-interactions#querying-elements-in-shadow-dom | combining these queries across shadow roots}.
475 * Alternatively, you can specify the selector type using a
476 * {@link https://pptr.dev/guides/page-interactions#prefixed-selector-syntax | prefix}.
477 * @param pageFunction - The function to be evaluated in this element's page's
478 * context. The first element matching the selector will be passed in as the
479 * first argument.
480 * @param args - Additional arguments to pass to `pageFunction`.
481 * @returns A promise to the result of the function.
482 */
483 async $eval<
484 Selector extends string,
485 Params extends unknown[],
486 Func extends EvaluateFuncWith<NodeFor<Selector>, Params> = EvaluateFuncWith<
487 NodeFor<Selector>,
488 Params
489 >,
490 >(
491 selector: Selector,
492 pageFunction: Func | string,
493 ...args: Params
494 ): Promise<Awaited<ReturnType<Func>>> {
495 pageFunction = withSourcePuppeteerURLIfNone(this.$eval.name, pageFunction);
496 using elementHandle = await this.$(selector);
497 if (!elementHandle) {
498 throw new Error(
499 `Error: failed to find element matching selector "${selector}"`,
500 );
501 }
502 return await elementHandle.evaluate(pageFunction, ...args);
503 }
504
505 /**
506 * Runs the given function on an array of elements matching the given selector
507 * in the current element.
508 *
509 * If the given function returns a promise, then this method will wait till
510 * the promise resolves.
511 *
512 * @example
513 * HTML:
514 *
515 * ```html
516 * <div class="feed">
517 * <div class="tweet">Hello!</div>
518 * <div class="tweet">Hi!</div>
519 * </div>
520 * ```
521 *
522 * JavaScript:
523 *
524 * ```ts
525 * const feedHandle = await page.$('.feed');
526 * expect(
527 * await feedHandle.$$eval('.tweet', nodes => nodes.map(n => n.innerText)),
528 * ).toEqual(['Hello!', 'Hi!']);
529 * ```
530 *
531 * @param selector -
532 * {@link https://pptr.dev/guides/page-interactions#selectors | selector}
533 * to query the page for.
534 * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | CSS selectors}
535 * can be passed as-is and a
536 * {@link https://pptr.dev/guides/page-interactions#non-css-selectors | Puppeteer-specific selector syntax}
537 * allows quering by
538 * {@link https://pptr.dev/guides/page-interactions#text-selectors--p-text | text},
539 * {@link https://pptr.dev/guides/page-interactions#aria-selectors--p-aria | a11y role and name},
540 * and
541 * {@link https://pptr.dev/guides/page-interactions#xpath-selectors--p-xpath | xpath}
542 * and
543 * {@link https://pptr.dev/guides/page-interactions#querying-elements-in-shadow-dom | combining these queries across shadow roots}.
544 * Alternatively, you can specify the selector type using a
545 * {@link https://pptr.dev/guides/page-interactions#prefixed-selector-syntax | prefix}.
546 * @param pageFunction - The function to be evaluated in the element's page's
547 * context. An array of elements matching the given selector will be passed to
548 * the function as its first argument.
549 * @param args - Additional arguments to pass to `pageFunction`.
550 * @returns A promise to the result of the function.
551 */
552 async $$eval<
553 Selector extends string,
554 Params extends unknown[],
555 Func extends EvaluateFuncWith<
556 Array<NodeFor<Selector>>,
557 Params
558 > = EvaluateFuncWith<Array<NodeFor<Selector>>, Params>,
559 >(
560 selector: Selector,
561 pageFunction: Func | string,
562 ...args: Params
563 ): Promise<Awaited<ReturnType<Func>>> {
564 pageFunction = withSourcePuppeteerURLIfNone(this.$$eval.name, pageFunction);
565 const results = await this.$$(selector);
566 using elements = await this.evaluateHandle(
567 (_, ...elements) => {
568 return elements;
569 },
570 ...results,
571 );
572 const [result] = await Promise.all([
573 elements.evaluate(pageFunction, ...args),
574 ...results.map(results => {
575 return results.dispose();
576 }),
577 ]);
578 return result;
579 }
580
581 /**
582 * Wait for an element matching the given selector to appear in the current
583 * element.
584 *
585 * Unlike {@link Frame.waitForSelector}, this method does not work across
586 * navigations or if the element is detached from DOM.
587 *
588 * @example
589 *
590 * ```ts
591 * import puppeteer from 'puppeteer';
592 *
593 * (async () => {
594 * const browser = await puppeteer.launch();
595 * const page = await browser.newPage();
596 * let currentURL;
597 * page
598 * .mainFrame()
599 * .waitForSelector('img')
600 * .then(() => console.log('First URL with image: ' + currentURL));
601 *
602 * for (currentURL of [
603 * 'https://example.com',
604 * 'https://google.com',
605 * 'https://bbc.com',
606 * ]) {
607 * await page.goto(currentURL);
608 * }
609 * await browser.close();
610 * })();
611 * ```
612 *
613 * @param selector - The selector to query and wait for.
614 * @param options - Options for customizing waiting behavior.
615 * @returns An element matching the given selector.
616 * @throws Throws if an element matching the given selector doesn't appear.
617 */
618 @throwIfDisposed()
619 @bindIsolatedHandle
620 async waitForSelector<Selector extends string>(
621 selector: Selector,
622 options: WaitForSelectorOptions = {},
623 ): Promise<ElementHandle<NodeFor<Selector>> | null> {
624 const {updatedSelector, QueryHandler, polling} =
625 getQueryHandlerAndSelector(selector);
626 return (await QueryHandler.waitFor(this, updatedSelector, {
627 polling,
628 ...options,
629 })) as ElementHandle<NodeFor<Selector>> | null;
630 }
631
632 async #checkVisibility(visibility: boolean): Promise<boolean> {
633 return await this.evaluate(
634 async (element, PuppeteerUtil, visibility) => {
635 return Boolean(PuppeteerUtil.checkVisibility(element, visibility));
636 },
637 LazyArg.create(context => {
638 return context.puppeteerUtil;
639 }),
640 visibility,
641 );
642 }
643
644 /**
645 * An element is considered to be visible if all of the following is
646 * true:
647 *
648 * - the element has
649 * {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/getComputedStyle | computed styles}.
650 *
651 * - the element has a non-empty
652 * {@link https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect | bounding client rect}.
653 *
654 * - the element's {@link https://developer.mozilla.org/en-US/docs/Web/CSS/visibility | visibility}
655 * is not `hidden` or `collapse`.
656 */
657 @throwIfDisposed()
658 @bindIsolatedHandle
659 async isVisible(): Promise<boolean> {
660 return await this.#checkVisibility(true);
661 }
662
663 /**
664 * An element is considered to be hidden if at least one of the following is true:
665 *
666 * - the element has no
667 * {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/getComputedStyle | computed styles}.
668 *
669 * - the element has an empty
670 * {@link https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect | bounding client rect}.
671 *
672 * - the element's {@link https://developer.mozilla.org/en-US/docs/Web/CSS/visibility | visibility}
673 * is `hidden` or `collapse`.
674 */
675 @throwIfDisposed()
676 @bindIsolatedHandle
677 async isHidden(): Promise<boolean> {
678 return await this.#checkVisibility(false);
679 }
680
681 /**
682 * Converts the current handle to the given element type.
683 *
684 * @example
685 *
686 * ```ts
687 * const element: ElementHandle<Element> = await page.$(
688 * '.class-name-of-anchor',
689 * );
690 * // DO NOT DISPOSE `element`, this will be always be the same handle.
691 * const anchor: ElementHandle<HTMLAnchorElement> =
692 * await element.toElement('a');
693 * ```
694 *
695 * @param tagName - The tag name of the desired element type.
696 * @throws An error if the handle does not match. **The handle will not be
697 * automatically disposed.**
698 */
699 @throwIfDisposed()
700 @bindIsolatedHandle
701 async toElement<
702 K extends keyof HTMLElementTagNameMap | keyof SVGElementTagNameMap,
703 >(tagName: K): Promise<HandleFor<ElementFor<K>>> {
704 const isMatchingTagName = await this.evaluate((node, tagName) => {
705 return node.nodeName === tagName.toUpperCase();
706 }, tagName);
707 if (!isMatchingTagName) {
708 throw new Error(`Element is not a(n) \`${tagName}\` element`);
709 }
710 return this as unknown as HandleFor<ElementFor<K>>;
711 }
712
713 /**
714 * Resolves the frame associated with the element, if any. Always exists for
715 * HTMLIFrameElements.
716 */
717 abstract contentFrame(this: ElementHandle<HTMLIFrameElement>): Promise<Frame>;
718 abstract contentFrame(): Promise<Frame | null>;
719
720 /**
721 * Returns the middle point within an element unless a specific offset is provided.
722 */
723 @throwIfDisposed()
724 @bindIsolatedHandle
725 async clickablePoint(offset?: Offset): Promise<Point> {
726 const box = await this.#clickableBox();
727 if (!box) {
728 throw new Error('Node is either not clickable or not an Element');
729 }
730 if (offset !== undefined) {
731 return {
732 x: box.x + offset.x,
733 y: box.y + offset.y,
734 };
735 }
736 return {
737 x: box.x + box.width / 2,
738 y: box.y + box.height / 2,
739 };
740 }
741
742 /**
743 * This method scrolls element into view if needed, and then
744 * uses {@link Page.mouse} to hover over the center of the element.
745 * If the element is detached from DOM, the method throws an error.
746 */
747 @throwIfDisposed()
748 @bindIsolatedHandle
749 async hover(this: ElementHandle<Element>): Promise<void> {
750 await this.scrollIntoViewIfNeeded();
751 const {x, y} = await this.clickablePoint();
752 await this.frame.page().mouse.move(x, y);
753 }
754
755 /**
756 * This method scrolls element into view if needed, and then
757 * uses {@link Page.mouse} to click in the center of the element.
758 * If the element is detached from DOM, the method throws an error.
759 */
760 @throwIfDisposed()
761 @bindIsolatedHandle
762 async click(
763 this: ElementHandle<Element>,
764 options: Readonly<ClickOptions> = {},
765 ): Promise<void> {
766 await this.scrollIntoViewIfNeeded();
767 const {x, y} = await this.clickablePoint(options.offset);
768 await this.frame.page().mouse.click(x, y, options);
769 }
770
771 /**
772 * Drags an element over the given element or point.
773 *
774 * @returns DEPRECATED. When drag interception is enabled, the drag payload is
775 * returned.
776 */
777 @throwIfDisposed()
778 @bindIsolatedHandle
779 async drag(
780 this: ElementHandle<Element>,
781 target: Point | ElementHandle<Element>,
782 ): Promise<Protocol.Input.DragData | void> {
783 await this.scrollIntoViewIfNeeded();
784 const page = this.frame.page();
785 if (page.isDragInterceptionEnabled()) {
786 const source = await this.clickablePoint();
787 if (target instanceof ElementHandle) {
788 target = await target.clickablePoint();
789 }
790 return await page.mouse.drag(source, target);
791 }
792 try {
793 if (!page._isDragging) {
794 page._isDragging = true;
795 await this.hover();
796 await page.mouse.down();
797 }
798 if (target instanceof ElementHandle) {
799 await target.hover();
800 } else {
801 await page.mouse.move(target.x, target.y);
802 }
803 } catch (error) {
804 page._isDragging = false;
805 throw error;
806 }
807 }
808
809 /**
810 * @deprecated Do not use. `dragenter` will automatically be performed during dragging.
811 */
812 @throwIfDisposed()
813 @bindIsolatedHandle
814 async dragEnter(
815 this: ElementHandle<Element>,
816 data: Protocol.Input.DragData = {items: [], dragOperationsMask: 1},
817 ): Promise<void> {
818 const page = this.frame.page();
819 await this.scrollIntoViewIfNeeded();
820 const target = await this.clickablePoint();
821 await page.mouse.dragEnter(target, data);
822 }
823
824 /**
825 * @deprecated Do not use. `dragover` will automatically be performed during dragging.
826 */
827 @throwIfDisposed()
828 @bindIsolatedHandle
829 async dragOver(
830 this: ElementHandle<Element>,
831 data: Protocol.Input.DragData = {items: [], dragOperationsMask: 1},
832 ): Promise<void> {
833 const page = this.frame.page();
834 await this.scrollIntoViewIfNeeded();
835 const target = await this.clickablePoint();
836 await page.mouse.dragOver(target, data);
837 }
838
839 /**
840 * Drops the given element onto the current one.
841 */
842 async drop(
843 this: ElementHandle<Element>,
844 element: ElementHandle<Element>,
845 ): Promise<void>;
846
847 /**
848 * @deprecated No longer supported.
849 */
850 async drop(
851 this: ElementHandle<Element>,
852 data?: Protocol.Input.DragData,
853 ): Promise<void>;
854
855 /**
856 * @internal
857 */
858 @throwIfDisposed()
859 @bindIsolatedHandle
860 async drop(
861 this: ElementHandle<Element>,
862 dataOrElement: ElementHandle<Element> | Protocol.Input.DragData = {
863 items: [],
864 dragOperationsMask: 1,
865 },
866 ): Promise<void> {
867 const page = this.frame.page();
868 if ('items' in dataOrElement) {
869 await this.scrollIntoViewIfNeeded();
870 const destination = await this.clickablePoint();
871 await page.mouse.drop(destination, dataOrElement);
872 } else {
873 // Note if the rest errors, we still want dragging off because the errors
874 // is most likely something implying the mouse is no longer dragging.
875 await dataOrElement.drag(this);
876 page._isDragging = false;
877 await page.mouse.up();
878 }
879 }
880
881 /**
882 * @deprecated Use `ElementHandle.drop` instead.
883 */
884 @throwIfDisposed()
885 @bindIsolatedHandle
886 async dragAndDrop(
887 this: ElementHandle<Element>,
888 target: ElementHandle<Node>,
889 options?: {delay: number},
890 ): Promise<void> {
891 const page = this.frame.page();
892 assert(
893 page.isDragInterceptionEnabled(),
894 'Drag Interception is not enabled!',
895 );
896 await this.scrollIntoViewIfNeeded();
897 const startPoint = await this.clickablePoint();
898 const targetPoint = await target.clickablePoint();
899 await page.mouse.dragAndDrop(startPoint, targetPoint, options);
900 }
901
902 /**
903 * Triggers a `change` and `input` event once all the provided options have been
904 * selected. If there's no `<select>` element matching `selector`, the method
905 * throws an error.
906 *
907 * @example
908 *
909 * ```ts
910 * handle.select('blue'); // single selection
911 * handle.select('red', 'green', 'blue'); // multiple selections
912 * ```
913 *
914 * @param values - Values of options to select. If the `<select>` has the
915 * `multiple` attribute, all values are considered, otherwise only the first
916 * one is taken into account.
917 */
918 @throwIfDisposed()
919 @bindIsolatedHandle
920 async select(...values: string[]): Promise<string[]> {
921 for (const value of values) {
922 assert(
923 isString(value),
924 'Values must be strings. Found value "' +
925 value +
926 '" of type "' +
927 typeof value +
928 '"',
929 );
930 }
931
932 return await this.evaluate((element, vals): string[] => {
933 const values = new Set(vals);
934 if (!(element instanceof HTMLSelectElement)) {
935 throw new Error('Element is not a <select> element.');
936 }
937
938 const selectedValues = new Set<string>();
939 if (!element.multiple) {
940 for (const option of element.options) {
941 option.selected = false;
942 }
943 for (const option of element.options) {
944 if (values.has(option.value)) {
945 option.selected = true;
946 selectedValues.add(option.value);
947 break;
948 }
949 }
950 } else {
951 for (const option of element.options) {
952 option.selected = values.has(option.value);
953 if (option.selected) {
954 selectedValues.add(option.value);
955 }
956 }
957 }
958 element.dispatchEvent(new Event('input', {bubbles: true}));
959 element.dispatchEvent(new Event('change', {bubbles: true}));
960 return [...selectedValues.values()];
961 }, values);
962 }
963
964 /**
965 * Sets the value of an
966 * {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input | input element}
967 * to the given file paths.
968 *
969 * @remarks This will not validate whether the file paths exists. Also, if a
970 * path is relative, then it is resolved against the
971 * {@link https://nodejs.org/api/process.html#process_process_cwd | current working directory}.
972 * For locals script connecting to remote chrome environments, paths must be
973 * absolute.
974 */
975 abstract uploadFile(
976 this: ElementHandle<HTMLInputElement>,
977 ...paths: string[]
978 ): Promise<void>;
979
980 /**
981 * @internal
982 */
983 abstract queryAXTree(
984 name?: string,
985 role?: string,
986 ): AwaitableIterable<ElementHandle<Node>>;
987
988 /**
989 * This method scrolls element into view if needed, and then uses
990 * {@link Touchscreen.tap} to tap in the center of the element.
991 * If the element is detached from DOM, the method throws an error.
992 */
993 @throwIfDisposed()
994 @bindIsolatedHandle
995 async tap(this: ElementHandle<Element>): Promise<void> {
996 await this.scrollIntoViewIfNeeded();
997 const {x, y} = await this.clickablePoint();
998 await this.frame.page().touchscreen.tap(x, y);
999 }
1000
1001 /**
1002 * This method scrolls the element into view if needed, and then
1003 * starts a touch in the center of the element.
1004 * @returns A {@link TouchHandle} representing the touch that was started
1005 */
1006 @throwIfDisposed()
1007 @bindIsolatedHandle
1008 async touchStart(this: ElementHandle<Element>): Promise<TouchHandle> {
1009 await this.scrollIntoViewIfNeeded();
1010 const {x, y} = await this.clickablePoint();
1011 return await this.frame.page().touchscreen.touchStart(x, y);
1012 }
1013
1014 /**
1015 * This method scrolls the element into view if needed, and then
1016 * moves the touch to the center of the element.
1017 * @param touch - An optional {@link TouchHandle}. If provided, this touch
1018 * will be moved. If not provided, the first active touch will be moved.
1019 */
1020 @throwIfDisposed()
1021 @bindIsolatedHandle
1022 async touchMove(
1023 this: ElementHandle<Element>,
1024 touch?: TouchHandle,
1025 ): Promise<void> {
1026 await this.scrollIntoViewIfNeeded();
1027 const {x, y} = await this.clickablePoint();
1028 if (touch) {
1029 return await touch.move(x, y);
1030 }
1031 await this.frame.page().touchscreen.touchMove(x, y);
1032 }
1033
1034 @throwIfDisposed()
1035 @bindIsolatedHandle
1036 async touchEnd(this: ElementHandle<Element>): Promise<void> {
1037 await this.scrollIntoViewIfNeeded();
1038 await this.frame.page().touchscreen.touchEnd();
1039 }
1040
1041 /**
1042 * Calls {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus | focus} on the element.
1043 */
1044 @throwIfDisposed()
1045 @bindIsolatedHandle
1046 async focus(): Promise<void> {
1047 await this.evaluate(element => {
1048 if (!(element instanceof HTMLElement)) {
1049 throw new Error('Cannot focus non-HTMLElement');
1050 }
1051 return element.focus();
1052 });
1053 }
1054
1055 /**
1056 * Focuses the element, and then sends a `keydown`, `keypress`/`input`, and
1057 * `keyup` event for each character in the text.
1058 *
1059 * To press a special key, like `Control` or `ArrowDown`,
1060 * use {@link ElementHandle.press}.
1061 *
1062 * @example
1063 *
1064 * ```ts
1065 * await elementHandle.type('Hello'); // Types instantly
1066 * await elementHandle.type('World', {delay: 100}); // Types slower, like a user
1067 * ```
1068 *
1069 * @example
1070 * An example of typing into a text field and then submitting the form:
1071 *
1072 * ```ts
1073 * const elementHandle = await page.$('input');
1074 * await elementHandle.type('some text');
1075 * await elementHandle.press('Enter');
1076 * ```
1077 *
1078 * @param options - Delay in milliseconds. Defaults to 0.
1079 */
1080 @throwIfDisposed()
1081 @bindIsolatedHandle
1082 async type(
1083 text: string,
1084 options?: Readonly<KeyboardTypeOptions>,
1085 ): Promise<void> {
1086 await this.focus();
1087 await this.frame.page().keyboard.type(text, options);
1088 }
1089
1090 /**
1091 * Focuses the element, and then uses {@link Keyboard.down} and {@link Keyboard.up}.
1092 *
1093 * @remarks
1094 * If `key` is a single character and no modifier keys besides `Shift`
1095 * are being held down, a `keypress`/`input` event will also be generated.
1096 * The `text` option can be specified to force an input event to be generated.
1097 *
1098 * **NOTE** Modifier keys DO affect `elementHandle.press`. Holding down `Shift`
1099 * will type the text in upper case.
1100 *
1101 * @param key - Name of key to press, such as `ArrowLeft`.
1102 * See {@link KeyInput} for a list of all key names.
1103 */
1104 @throwIfDisposed()
1105 @bindIsolatedHandle
1106 async press(
1107 key: KeyInput,
1108 options?: Readonly<KeyPressOptions>,
1109 ): Promise<void> {
1110 await this.focus();
1111 await this.frame.page().keyboard.press(key, options);
1112 }
1113
1114 async #clickableBox(): Promise<BoundingBox | null> {
1115 const boxes = await this.evaluate(element => {
1116 if (!(element instanceof Element)) {
1117 return null;
1118 }
1119 return [...element.getClientRects()].map(rect => {
1120 return {x: rect.x, y: rect.y, width: rect.width, height: rect.height};
1121 });
1122 });
1123 if (!boxes?.length) {
1124 return null;
1125 }
1126 await this.#intersectBoundingBoxesWithFrame(boxes);
1127 let frame = this.frame;
1128 let parentFrame: Frame | null | undefined;
1129 while ((parentFrame = frame?.parentFrame())) {
1130 using handle = await frame.frameElement();
1131 if (!handle) {
1132 throw new Error('Unsupported frame type');
1133 }
1134 const parentBox = await handle.evaluate(element => {
1135 // Element is not visible.
1136 if (element.getClientRects().length === 0) {
1137 return null;
1138 }
1139 const rect = element.getBoundingClientRect();
1140 const style = window.getComputedStyle(element);
1141 return {
1142 left:
1143 rect.left +
1144 parseInt(style.paddingLeft, 10) +
1145 parseInt(style.borderLeftWidth, 10),
1146 top:
1147 rect.top +
1148 parseInt(style.paddingTop, 10) +
1149 parseInt(style.borderTopWidth, 10),
1150 };
1151 });
1152 if (!parentBox) {
1153 return null;
1154 }
1155 for (const box of boxes) {
1156 box.x += parentBox.left;
1157 box.y += parentBox.top;
1158 }
1159 await handle.#intersectBoundingBoxesWithFrame(boxes);
1160 frame = parentFrame;
1161 }
1162 const box = boxes.find(box => {
1163 return box.width >= 1 && box.height >= 1;
1164 });
1165 if (!box) {
1166 return null;
1167 }
1168 return {
1169 x: box.x,
1170 y: box.y,
1171 height: box.height,
1172 width: box.width,
1173 };
1174 }
1175
1176 async #intersectBoundingBoxesWithFrame(boxes: BoundingBox[]) {
1177 const {documentWidth, documentHeight} = await this.frame
1178 .isolatedRealm()
1179 .evaluate(() => {
1180 return {
1181 documentWidth: document.documentElement.clientWidth,
1182 documentHeight: document.documentElement.clientHeight,
1183 };
1184 });
1185 for (const box of boxes) {
1186 intersectBoundingBox(box, documentWidth, documentHeight);
1187 }
1188 }
1189
1190 /**
1191 * This method returns the bounding box of the element (relative to the main frame),
1192 * or `null` if the element is {@link https://drafts.csswg.org/css-display-4/#box-generation | not part of the layout}
1193 * (example: `display: none`).
1194 */
1195 @throwIfDisposed()
1196 @bindIsolatedHandle
1197 async boundingBox(): Promise<BoundingBox | null> {
1198 const box = await this.evaluate(element => {
1199 if (!(element instanceof Element)) {
1200 return null;
1201 }
1202 // Element is not visible.
1203 if (element.getClientRects().length === 0) {
1204 return null;
1205 }
1206 const rect = element.getBoundingClientRect();
1207 return {x: rect.x, y: rect.y, width: rect.width, height: rect.height};
1208 });
1209 if (!box) {
1210 return null;
1211 }
1212 const offset = await this.#getTopLeftCornerOfFrame();
1213 if (!offset) {
1214 return null;
1215 }
1216 return {
1217 x: box.x + offset.x,
1218 y: box.y + offset.y,
1219 height: box.height,
1220 width: box.width,
1221 };
1222 }
1223
1224 /**
1225 * This method returns boxes of the element,
1226 * or `null` if the element is {@link https://drafts.csswg.org/css-display-4/#box-generation | not part of the layout}
1227 * (example: `display: none`).
1228 *
1229 * @remarks
1230 *
1231 * Boxes are represented as an array of points;
1232 * Each Point is an object `{x, y}`. Box points are sorted clock-wise.
1233 */
1234 @throwIfDisposed()
1235 @bindIsolatedHandle
1236 async boxModel(): Promise<BoxModel | null> {
1237 const model = await this.evaluate(element => {
1238 if (!(element instanceof Element)) {
1239 return null;
1240 }
1241 // Element is not visible.
1242 if (element.getClientRects().length === 0) {
1243 return null;
1244 }
1245 const rect = element.getBoundingClientRect();
1246 const style = window.getComputedStyle(element);
1247 const offsets = {
1248 padding: {
1249 left: parseInt(style.paddingLeft, 10),
1250 top: parseInt(style.paddingTop, 10),
1251 right: parseInt(style.paddingRight, 10),
1252 bottom: parseInt(style.paddingBottom, 10),
1253 },
1254 margin: {
1255 left: -parseInt(style.marginLeft, 10),
1256 top: -parseInt(style.marginTop, 10),
1257 right: -parseInt(style.marginRight, 10),
1258 bottom: -parseInt(style.marginBottom, 10),
1259 },
1260 border: {
1261 left: parseInt(style.borderLeft, 10),
1262 top: parseInt(style.borderTop, 10),
1263 right: parseInt(style.borderRight, 10),
1264 bottom: parseInt(style.borderBottom, 10),
1265 },
1266 };
1267 const border: Quad = [
1268 {x: rect.left, y: rect.top},
1269 {x: rect.left + rect.width, y: rect.top},
1270 {x: rect.left + rect.width, y: rect.top + rect.height},
1271 {x: rect.left, y: rect.top + rect.height},
1272 ];
1273 const padding = transformQuadWithOffsets(border, offsets.border);
1274 const content = transformQuadWithOffsets(padding, offsets.padding);
1275 const margin = transformQuadWithOffsets(border, offsets.margin);
1276 return {
1277 content,
1278 padding,
1279 border,
1280 margin,
1281 width: rect.width,
1282 height: rect.height,
1283 };
1284
1285 function transformQuadWithOffsets(
1286 quad: Quad,
1287 offsets: {top: number; left: number; right: number; bottom: number},
1288 ): Quad {
1289 return [
1290 {
1291 x: quad[0].x + offsets.left,
1292 y: quad[0].y + offsets.top,
1293 },
1294 {
1295 x: quad[1].x - offsets.right,
1296 y: quad[1].y + offsets.top,
1297 },
1298 {
1299 x: quad[2].x - offsets.right,
1300 y: quad[2].y - offsets.bottom,
1301 },
1302 {
1303 x: quad[3].x + offsets.left,
1304 y: quad[3].y - offsets.bottom,
1305 },
1306 ];
1307 }
1308 });
1309 if (!model) {
1310 return null;
1311 }
1312 const offset = await this.#getTopLeftCornerOfFrame();
1313 if (!offset) {
1314 return null;
1315 }
1316 for (const attribute of [
1317 'content',
1318 'padding',
1319 'border',
1320 'margin',
1321 ] as const) {
1322 for (const point of model[attribute]) {
1323 point.x += offset.x;
1324 point.y += offset.y;
1325 }
1326 }
1327 return model;
1328 }
1329
1330 async #getTopLeftCornerOfFrame() {
1331 const point = {x: 0, y: 0};
1332 let frame = this.frame;
1333 let parentFrame: Frame | null | undefined;
1334 while ((parentFrame = frame?.parentFrame())) {
1335 using handle = await frame.frameElement();
1336 if (!handle) {
1337 throw new Error('Unsupported frame type');
1338 }
1339 const parentBox = await handle.evaluate(element => {
1340 // Element is not visible.
1341 if (element.getClientRects().length === 0) {
1342 return null;
1343 }
1344 const rect = element.getBoundingClientRect();
1345 const style = window.getComputedStyle(element);
1346 return {
1347 left:
1348 rect.left +
1349 parseInt(style.paddingLeft, 10) +
1350 parseInt(style.borderLeftWidth, 10),
1351 top:
1352 rect.top +
1353 parseInt(style.paddingTop, 10) +
1354 parseInt(style.borderTopWidth, 10),
1355 };
1356 });
1357 if (!parentBox) {
1358 return null;
1359 }
1360 point.x += parentBox.left;
1361 point.y += parentBox.top;
1362 frame = parentFrame;
1363 }
1364 return point;
1365 }
1366
1367 /**
1368 * This method scrolls element into view if needed, and then uses
1369 * {@link Page.(screenshot:2) } to take a screenshot of the element.
1370 * If the element is detached from DOM, the method throws an error.
1371 */
1372 async screenshot(
1373 options: Readonly<ScreenshotOptions> & {encoding: 'base64'},
1374 ): Promise<string>;
1375 async screenshot(options?: Readonly<ScreenshotOptions>): Promise<Uint8Array>;
1376 @throwIfDisposed()
1377 @bindIsolatedHandle
1378 async screenshot(
1379 this: ElementHandle<Element>,
1380 options: Readonly<ElementScreenshotOptions> = {},
1381 ): Promise<string | Uint8Array> {
1382 const {scrollIntoView = true, clip} = options;
1383
1384 const page = this.frame.page();
1385
1386 // Only scroll the element into view if the user wants it.
1387 if (scrollIntoView) {
1388 await this.scrollIntoViewIfNeeded();
1389 }
1390 const elementClip = await this.#nonEmptyVisibleBoundingBox();
1391
1392 const [pageLeft, pageTop] = await this.evaluate(() => {
1393 if (!window.visualViewport) {
1394 throw new Error('window.visualViewport is not supported.');
1395 }
1396 return [
1397 window.visualViewport.pageLeft,
1398 window.visualViewport.pageTop,
1399 ] as const;
1400 });
1401 elementClip.x += pageLeft;
1402 elementClip.y += pageTop;
1403 if (clip) {
1404 elementClip.x += clip.x;
1405 elementClip.y += clip.y;
1406 elementClip.height = clip.height;
1407 elementClip.width = clip.width;
1408 }
1409
1410 return await page.screenshot({...options, clip: elementClip});
1411 }
1412
1413 async #nonEmptyVisibleBoundingBox() {
1414 const box = await this.boundingBox();
1415 assert(box, 'Node is either not visible or not an HTMLElement');
1416 assert(box.width !== 0, 'Node has 0 width.');
1417 assert(box.height !== 0, 'Node has 0 height.');
1418 return box;
1419 }
1420
1421 /**
1422 * @internal
1423 */
1424 protected async assertConnectedElement(): Promise<void> {
1425 const error = await this.evaluate(async element => {
1426 if (!element.isConnected) {
1427 return 'Node is detached from document';
1428 }
1429 if (element.nodeType !== Node.ELEMENT_NODE) {
1430 return 'Node is not of type HTMLElement';
1431 }
1432 return;
1433 });
1434
1435 if (error) {
1436 throw new Error(error);
1437 }
1438 }
1439
1440 /**
1441 * @internal
1442 */
1443 protected async scrollIntoViewIfNeeded(
1444 this: ElementHandle<Element>,
1445 ): Promise<void> {
1446 if (
1447 await this.isIntersectingViewport({
1448 threshold: 1,
1449 })
1450 ) {
1451 return;
1452 }
1453 await this.scrollIntoView();
1454 }
1455
1456 /**
1457 * Resolves to true if the element is visible in the current viewport. If an
1458 * element is an SVG, we check if the svg owner element is in the viewport
1459 * instead. See https://crbug.com/963246.
1460 *
1461 * @param options - Threshold for the intersection between 0 (no intersection) and 1
1462 * (full intersection). Defaults to 1.
1463 */
1464 @throwIfDisposed()
1465 @bindIsolatedHandle
1466 async isIntersectingViewport(
1467 this: ElementHandle<Element>,
1468 options: {
1469 threshold?: number;
1470 } = {},
1471 ): Promise<boolean> {
1472 await this.assertConnectedElement();
1473 // eslint-disable-next-line rulesdir/use-using -- Returns `this`.
1474 const handle = await this.#asSVGElementHandle();
1475 using target = handle && (await handle.#getOwnerSVGElement());
1476 return await ((target ?? this) as ElementHandle<Element>).evaluate(
1477 async (element, threshold) => {
1478 const visibleRatio = await new Promise<number>(resolve => {
1479 const observer = new IntersectionObserver(entries => {
1480 resolve(entries[0]!.intersectionRatio);
1481 observer.disconnect();
1482 });
1483 observer.observe(element);
1484 });
1485 return threshold === 1 ? visibleRatio === 1 : visibleRatio > threshold;
1486 },
1487 options.threshold ?? 0,
1488 );
1489 }
1490
1491 /**
1492 * Scrolls the element into view using either the automation protocol client
1493 * or by calling element.scrollIntoView.
1494 */
1495 @throwIfDisposed()
1496 @bindIsolatedHandle
1497 async scrollIntoView(this: ElementHandle<Element>): Promise<void> {
1498 await this.assertConnectedElement();
1499 await this.evaluate(async (element): Promise<void> => {
1500 element.scrollIntoView({
1501 block: 'center',
1502 inline: 'center',
1503 behavior: 'instant',
1504 });
1505 });
1506 }
1507
1508 /**
1509 * Returns true if an element is an SVGElement (included svg, path, rect
1510 * etc.).
1511 */
1512 async #asSVGElementHandle(
1513 this: ElementHandle<Element>,
1514 ): Promise<ElementHandle<SVGElement> | null> {
1515 if (
1516 await this.evaluate(element => {
1517 return element instanceof SVGElement;
1518 })
1519 ) {
1520 return this as ElementHandle<SVGElement>;
1521 } else {
1522 return null;
1523 }
1524 }
1525
1526 async #getOwnerSVGElement(
1527 this: ElementHandle<SVGElement>,
1528 ): Promise<ElementHandle<SVGSVGElement>> {
1529 // SVGSVGElement.ownerSVGElement === null.
1530 return await this.evaluateHandle(element => {
1531 if (element instanceof SVGSVGElement) {
1532 return element;
1533 }
1534 return element.ownerSVGElement!;
1535 });
1536 }
1537
1538 /**
1539 * If the element is a form input, you can use {@link ElementHandle.autofill}
1540 * to test if the form is compatible with the browser's autofill
1541 * implementation. Throws an error if the form cannot be autofilled.
1542 *
1543 * @remarks
1544 *
1545 * Currently, Puppeteer supports auto-filling credit card information only and
1546 * in Chrome in the new headless and headful modes only.
1547 *
1548 * ```ts
1549 * // Select an input on the credit card form.
1550 * const name = await page.waitForSelector('form #name');
1551 * // Trigger autofill with the desired data.
1552 * await name.autofill({
1553 * creditCard: {
1554 * number: '4444444444444444',
1555 * name: 'John Smith',
1556 * expiryMonth: '01',
1557 * expiryYear: '2030',
1558 * cvc: '123',
1559 * },
1560 * });
1561 * ```
1562 */
1563 abstract autofill(data: AutofillData): Promise<void>;
1564
1565 /**
1566 * When connected using Chrome DevTools Protocol, it returns a
1567 * DOM.BackendNodeId for the element.
1568 */
1569 abstract backendNodeId(): Promise<number>;
1570}
1571
1572/**
1573 * @public
1574 */
1575export interface AutofillData {
1576 creditCard: {
1577 // See https://chromedevtools.github.io/devtools-protocol/tot/Autofill/#type-CreditCard.
1578 number: string;
1579 name: string;
1580 expiryMonth: string;
1581 expiryYear: string;
1582 cvc: string;
1583 };
1584}
1585
1586function intersectBoundingBox(
1587 box: BoundingBox,
1588 width: number,
1589 height: number,
1590): void {
1591 box.width = Math.max(
1592 box.x >= 0
1593 ? Math.min(width - box.x, box.width)
1594 : Math.min(width, box.width + box.x),
1595 0,
1596 );
1597 box.height = Math.max(
1598 box.y >= 0
1599 ? Math.min(height - box.y, box.height)
1600 : Math.min(height, box.height + box.y),
1601 0,
1602 );
1603}