ElementHandle.ts (47432B)
1 /** 2 * @license 3 * Copyright 2023 Google Inc. 4 * SPDX-License-Identifier: Apache-2.0 5 */ 6 7 import type {Protocol} from 'devtools-protocol'; 8 9 import type {Frame} from '../api/Frame.js'; 10 import {getQueryHandlerAndSelector} from '../common/GetQueryHandler.js'; 11 import {LazyArg} from '../common/LazyArg.js'; 12 import type { 13 AwaitableIterable, 14 ElementFor, 15 EvaluateFuncWith, 16 HandleFor, 17 HandleOr, 18 NodeFor, 19 } from '../common/types.js'; 20 import type {KeyInput} from '../common/USKeyboardLayout.js'; 21 import {isString, withSourcePuppeteerURLIfNone} from '../common/util.js'; 22 import {assert} from '../util/assert.js'; 23 import {AsyncIterableUtil} from '../util/AsyncIterableUtil.js'; 24 import {throwIfDisposed} from '../util/decorators.js'; 25 26 import {_isElementHandle} from './ElementHandleSymbol.js'; 27 import type { 28 KeyboardTypeOptions, 29 KeyPressOptions, 30 MouseClickOptions, 31 TouchHandle, 32 } from './Input.js'; 33 import {JSHandle} from './JSHandle.js'; 34 import type { 35 QueryOptions, 36 ScreenshotOptions, 37 WaitForSelectorOptions, 38 } from './Page.js'; 39 40 /** 41 * @public 42 */ 43 export type Quad = [Point, Point, Point, Point]; 44 45 /** 46 * @public 47 */ 48 export 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 */ 60 export 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 */ 74 export 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 */ 88 export 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 */ 98 export interface Point { 99 x: number; 100 y: number; 101 } 102 103 /** 104 * @public 105 */ 106 export 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 */ 121 export 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 */ 205 export 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 async dispose(): Promise<void> { 335 await Promise.all([this.handle.dispose(), this.isolatedHandle?.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 querying 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 querying 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 querying 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 querying 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 */ 1575 export interface AutofillData { 1576 /** 1577 * See {@link https://chromedevtools.github.io/devtools-protocol/tot/Autofill/#type-CreditCard | Autofill.CreditCard}. 1578 */ 1579 creditCard: { 1580 number: string; 1581 name: string; 1582 expiryMonth: string; 1583 expiryYear: string; 1584 cvc: string; 1585 }; 1586 } 1587 1588 function intersectBoundingBox( 1589 box: BoundingBox, 1590 width: number, 1591 height: number, 1592 ): void { 1593 box.width = Math.max( 1594 box.x >= 0 1595 ? Math.min(width - box.x, box.width) 1596 : Math.min(width, box.width + box.x), 1597 0, 1598 ); 1599 box.height = Math.max( 1600 box.y >= 0 1601 ? Math.min(height - box.y, box.height) 1602 : Math.min(height, box.height + box.y), 1603 0, 1604 ); 1605 }