1/**
2 * Copyright 2019 Google Inc. All rights reserved.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *     http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17import { assert } from './assert.js';
18import { helper, debugError } from './helper.js';
19import { ExecutionContext } from './ExecutionContext.js';
20import { Page } from './Page.js';
21import { CDPSession } from './Connection.js';
22import { KeyInput } from './USKeyboardLayout.js';
23import { FrameManager, Frame } from './FrameManager.js';
24import { getQueryHandlerAndSelector } from './QueryHandler.js';
25import { Protocol } from 'devtools-protocol';
26import {
27  EvaluateFn,
28  SerializableOrJSHandle,
29  EvaluateFnReturnType,
30  EvaluateHandleFn,
31  WrapElementHandle,
32  UnwrapPromiseLike,
33} from './EvalTypes.js';
34import { isNode } from '../environment.js';
35/**
36 * @public
37 */
38export interface BoxModel {
39  content: Array<{ x: number; y: number }>;
40  padding: Array<{ x: number; y: number }>;
41  border: Array<{ x: number; y: number }>;
42  margin: Array<{ x: number; y: number }>;
43  width: number;
44  height: number;
45}
46
47/**
48 * @public
49 */
50export interface BoundingBox {
51  /**
52   * the x coordinate of the element in pixels.
53   */
54  x: number;
55  /**
56   * the y coordinate of the element in pixels.
57   */
58  y: number;
59  /**
60   * the width of the element in pixels.
61   */
62  width: number;
63  /**
64   * the height of the element in pixels.
65   */
66  height: number;
67}
68
69/**
70 * @internal
71 */
72export function createJSHandle(
73  context: ExecutionContext,
74  remoteObject: Protocol.Runtime.RemoteObject
75): JSHandle {
76  const frame = context.frame();
77  if (remoteObject.subtype === 'node' && frame) {
78    const frameManager = frame._frameManager;
79    return new ElementHandle(
80      context,
81      context._client,
82      remoteObject,
83      frameManager.page(),
84      frameManager
85    );
86  }
87  return new JSHandle(context, context._client, remoteObject);
88}
89
90/**
91 * Represents an in-page JavaScript object. JSHandles can be created with the
92 * {@link Page.evaluateHandle | page.evaluateHandle} method.
93 *
94 * @example
95 * ```js
96 * const windowHandle = await page.evaluateHandle(() => window);
97 * ```
98 *
99 * JSHandle prevents the referenced JavaScript object from being garbage-collected
100 * unless the handle is {@link JSHandle.dispose | disposed}. JSHandles are auto-
101 * disposed when their origin frame gets navigated or the parent context gets destroyed.
102 *
103 * JSHandle instances can be used as arguments for {@link Page.$eval},
104 * {@link Page.evaluate}, and {@link Page.evaluateHandle}.
105 *
106 * @public
107 */
108export class JSHandle<HandleObjectType = unknown> {
109  /**
110   * @internal
111   */
112  _context: ExecutionContext;
113  /**
114   * @internal
115   */
116  _client: CDPSession;
117  /**
118   * @internal
119   */
120  _remoteObject: Protocol.Runtime.RemoteObject;
121  /**
122   * @internal
123   */
124  _disposed = false;
125
126  /**
127   * @internal
128   */
129  constructor(
130    context: ExecutionContext,
131    client: CDPSession,
132    remoteObject: Protocol.Runtime.RemoteObject
133  ) {
134    this._context = context;
135    this._client = client;
136    this._remoteObject = remoteObject;
137  }
138
139  /** Returns the execution context the handle belongs to.
140   */
141  executionContext(): ExecutionContext {
142    return this._context;
143  }
144
145  /**
146   * This method passes this handle as the first argument to `pageFunction`.
147   * If `pageFunction` returns a Promise, then `handle.evaluate` would wait
148   * for the promise to resolve and return its value.
149   *
150   * @example
151   * ```js
152   * const tweetHandle = await page.$('.tweet .retweets');
153   * expect(await tweetHandle.evaluate(node => node.innerText)).toBe('10');
154   * ```
155   */
156
157  async evaluate<T extends EvaluateFn<HandleObjectType>>(
158    pageFunction: T | string,
159    ...args: SerializableOrJSHandle[]
160  ): Promise<UnwrapPromiseLike<EvaluateFnReturnType<T>>> {
161    return await this.executionContext().evaluate<
162      UnwrapPromiseLike<EvaluateFnReturnType<T>>
163    >(pageFunction, this, ...args);
164  }
165
166  /**
167   * This method passes this handle as the first argument to `pageFunction`.
168   *
169   * @remarks
170   *
171   * The only difference between `jsHandle.evaluate` and
172   * `jsHandle.evaluateHandle` is that `jsHandle.evaluateHandle`
173   * returns an in-page object (JSHandle).
174   *
175   * If the function passed to `jsHandle.evaluateHandle` returns a Promise,
176   * then `evaluateHandle.evaluateHandle` waits for the promise to resolve and
177   * returns its value.
178   *
179   * See {@link Page.evaluateHandle} for more details.
180   */
181  async evaluateHandle<HandleType extends JSHandle = JSHandle>(
182    pageFunction: EvaluateHandleFn,
183    ...args: SerializableOrJSHandle[]
184  ): Promise<HandleType> {
185    return await this.executionContext().evaluateHandle(
186      pageFunction,
187      this,
188      ...args
189    );
190  }
191
192  /** Fetches a single property from the referenced object.
193   */
194  async getProperty(propertyName: string): Promise<JSHandle | undefined> {
195    const objectHandle = await this.evaluateHandle(
196      (object: Element, propertyName: string) => {
197        const result = { __proto__: null };
198        result[propertyName] = object[propertyName];
199        return result;
200      },
201      propertyName
202    );
203    const properties = await objectHandle.getProperties();
204    const result = properties.get(propertyName) || null;
205    await objectHandle.dispose();
206    return result;
207  }
208
209  /**
210   * The method returns a map with property names as keys and JSHandle
211   * instances for the property values.
212   *
213   * @example
214   * ```js
215   * const listHandle = await page.evaluateHandle(() => document.body.children);
216   * const properties = await listHandle.getProperties();
217   * const children = [];
218   * for (const property of properties.values()) {
219   *   const element = property.asElement();
220   *   if (element)
221   *     children.push(element);
222   * }
223   * children; // holds elementHandles to all children of document.body
224   * ```
225   */
226  async getProperties(): Promise<Map<string, JSHandle>> {
227    const response = await this._client.send('Runtime.getProperties', {
228      objectId: this._remoteObject.objectId,
229      ownProperties: true,
230    });
231    const result = new Map<string, JSHandle>();
232    for (const property of response.result) {
233      if (!property.enumerable) continue;
234      result.set(property.name, createJSHandle(this._context, property.value));
235    }
236    return result;
237  }
238
239  /**
240   * @returns Returns a JSON representation of the object.If the object has a
241   * `toJSON` function, it will not be called.
242   * @remarks
243   *
244   * The JSON is generated by running {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify | JSON.stringify}
245   * on the object in page and consequent {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse | JSON.parse} in puppeteer.
246   * **NOTE** The method throws if the referenced object is not stringifiable.
247   */
248  async jsonValue<T = unknown>(): Promise<T> {
249    if (this._remoteObject.objectId) {
250      const response = await this._client.send('Runtime.callFunctionOn', {
251        functionDeclaration: 'function() { return this; }',
252        objectId: this._remoteObject.objectId,
253        returnByValue: true,
254        awaitPromise: true,
255      });
256      return helper.valueFromRemoteObject(response.result) as T;
257    }
258    return helper.valueFromRemoteObject(this._remoteObject) as T;
259  }
260
261  /**
262   * @returns Either `null` or the object handle itself, if the object
263   * handle is an instance of {@link ElementHandle}.
264   */
265  asElement(): ElementHandle | null {
266    /*  This always returns null, but subclasses can override this and return an
267        ElementHandle.
268    */
269    return null;
270  }
271
272  /**
273   * Stops referencing the element handle, and resolves when the object handle is
274   * successfully disposed of.
275   */
276  async dispose(): Promise<void> {
277    if (this._disposed) return;
278    this._disposed = true;
279    await helper.releaseObject(this._client, this._remoteObject);
280  }
281
282  /**
283   * Returns a string representation of the JSHandle.
284   *
285   * @remarks Useful during debugging.
286   */
287  toString(): string {
288    if (this._remoteObject.objectId) {
289      const type = this._remoteObject.subtype || this._remoteObject.type;
290      return 'JSHandle@' + type;
291    }
292    return 'JSHandle:' + helper.valueFromRemoteObject(this._remoteObject);
293  }
294}
295
296/**
297 * ElementHandle represents an in-page DOM element.
298 *
299 * @remarks
300 *
301 * ElementHandles can be created with the {@link Page.$} method.
302 *
303 * ```js
304 * const puppeteer = require('puppeteer');
305 *
306 * (async () => {
307 *  const browser = await puppeteer.launch();
308 *  const page = await browser.newPage();
309 *  await page.goto('https://example.com');
310 *  const hrefElement = await page.$('a');
311 *  await hrefElement.click();
312 *  // ...
313 * })();
314 * ```
315 *
316 * ElementHandle prevents the DOM element from being garbage-collected unless the
317 * handle is {@link JSHandle.dispose | disposed}. ElementHandles are auto-disposed
318 * when their origin frame gets navigated.
319 *
320 * ElementHandle instances can be used as arguments in {@link Page.$eval} and
321 * {@link Page.evaluate} methods.
322 *
323 * If you're using TypeScript, ElementHandle takes a generic argument that
324 * denotes the type of element the handle is holding within. For example, if you
325 * have a handle to a `<select>` element, you can type it as
326 * `ElementHandle<HTMLSelectElement>` and you get some nicer type checks.
327 *
328 * @public
329 */
330export class ElementHandle<
331  ElementType extends Element = Element
332> extends JSHandle<ElementType> {
333  private _page: Page;
334  private _frameManager: FrameManager;
335
336  /**
337   * @internal
338   */
339  constructor(
340    context: ExecutionContext,
341    client: CDPSession,
342    remoteObject: Protocol.Runtime.RemoteObject,
343    page: Page,
344    frameManager: FrameManager
345  ) {
346    super(context, client, remoteObject);
347    this._client = client;
348    this._remoteObject = remoteObject;
349    this._page = page;
350    this._frameManager = frameManager;
351  }
352
353  asElement(): ElementHandle<ElementType> | null {
354    return this;
355  }
356
357  /**
358   * Resolves to the content frame for element handles referencing
359   * iframe nodes, or null otherwise
360   */
361  async contentFrame(): Promise<Frame | null> {
362    const nodeInfo = await this._client.send('DOM.describeNode', {
363      objectId: this._remoteObject.objectId,
364    });
365    if (typeof nodeInfo.node.frameId !== 'string') return null;
366    return this._frameManager.frame(nodeInfo.node.frameId);
367  }
368
369  private async _scrollIntoViewIfNeeded(): Promise<void> {
370    const error = await this.evaluate<
371      (
372        element: Element,
373        pageJavascriptEnabled: boolean
374      ) => Promise<string | false>
375    >(async (element, pageJavascriptEnabled) => {
376      if (!element.isConnected) return 'Node is detached from document';
377      if (element.nodeType !== Node.ELEMENT_NODE)
378        return 'Node is not of type HTMLElement';
379      // force-scroll if page's javascript is disabled.
380      if (!pageJavascriptEnabled) {
381        element.scrollIntoView({
382          block: 'center',
383          inline: 'center',
384          // @ts-expect-error Chrome still supports behavior: instant but
385          // it's not in the spec so TS shouts We don't want to make this
386          // breaking change in Puppeteer yet so we'll ignore the line.
387          behavior: 'instant',
388        });
389        return false;
390      }
391      const visibleRatio = await new Promise((resolve) => {
392        const observer = new IntersectionObserver((entries) => {
393          resolve(entries[0].intersectionRatio);
394          observer.disconnect();
395        });
396        observer.observe(element);
397      });
398      if (visibleRatio !== 1.0) {
399        element.scrollIntoView({
400          block: 'center',
401          inline: 'center',
402          // @ts-expect-error Chrome still supports behavior: instant but
403          // it's not in the spec so TS shouts We don't want to make this
404          // breaking change in Puppeteer yet so we'll ignore the line.
405          behavior: 'instant',
406        });
407      }
408      return false;
409    }, this._page.isJavaScriptEnabled());
410
411    if (error) throw new Error(error);
412  }
413
414  private async _clickablePoint(): Promise<{ x: number; y: number }> {
415    const [result, layoutMetrics] = await Promise.all([
416      this._client
417        .send('DOM.getContentQuads', {
418          objectId: this._remoteObject.objectId,
419        })
420        .catch(debugError),
421      this._client.send('Page.getLayoutMetrics'),
422    ]);
423    if (!result || !result.quads.length)
424      throw new Error('Node is either not visible or not an HTMLElement');
425    // Filter out quads that have too small area to click into.
426    const { clientWidth, clientHeight } = layoutMetrics.layoutViewport;
427    const quads = result.quads
428      .map((quad) => this._fromProtocolQuad(quad))
429      .map((quad) =>
430        this._intersectQuadWithViewport(quad, clientWidth, clientHeight)
431      )
432      .filter((quad) => computeQuadArea(quad) > 1);
433    if (!quads.length)
434      throw new Error('Node is either not visible or not an HTMLElement');
435    // Return the middle point of the first quad.
436    const quad = quads[0];
437    let x = 0;
438    let y = 0;
439    for (const point of quad) {
440      x += point.x;
441      y += point.y;
442    }
443    return {
444      x: x / 4,
445      y: y / 4,
446    };
447  }
448
449  private _getBoxModel(): Promise<void | Protocol.DOM.GetBoxModelResponse> {
450    const params: Protocol.DOM.GetBoxModelRequest = {
451      objectId: this._remoteObject.objectId,
452    };
453    return this._client
454      .send('DOM.getBoxModel', params)
455      .catch((error) => debugError(error));
456  }
457
458  private _fromProtocolQuad(quad: number[]): Array<{ x: number; y: number }> {
459    return [
460      { x: quad[0], y: quad[1] },
461      { x: quad[2], y: quad[3] },
462      { x: quad[4], y: quad[5] },
463      { x: quad[6], y: quad[7] },
464    ];
465  }
466
467  private _intersectQuadWithViewport(
468    quad: Array<{ x: number; y: number }>,
469    width: number,
470    height: number
471  ): Array<{ x: number; y: number }> {
472    return quad.map((point) => ({
473      x: Math.min(Math.max(point.x, 0), width),
474      y: Math.min(Math.max(point.y, 0), height),
475    }));
476  }
477
478  /**
479   * This method scrolls element into view if needed, and then
480   * uses {@link Page.mouse} to hover over the center of the element.
481   * If the element is detached from DOM, the method throws an error.
482   */
483  async hover(): Promise<void> {
484    await this._scrollIntoViewIfNeeded();
485    const { x, y } = await this._clickablePoint();
486    await this._page.mouse.move(x, y);
487  }
488
489  /**
490   * This method scrolls element into view if needed, and then
491   * uses {@link Page.mouse} to click in the center of the element.
492   * If the element is detached from DOM, the method throws an error.
493   */
494  async click(options: ClickOptions = {}): Promise<void> {
495    await this._scrollIntoViewIfNeeded();
496    const { x, y } = await this._clickablePoint();
497    await this._page.mouse.click(x, y, options);
498  }
499
500  /**
501   * Triggers a `change` and `input` event once all the provided options have been
502   * selected. If there's no `<select>` element matching `selector`, the method
503   * throws an error.
504   *
505   * @example
506   * ```js
507   * handle.select('blue'); // single selection
508   * handle.select('red', 'green', 'blue'); // multiple selections
509   * ```
510   * @param values - Values of options to select. If the `<select>` has the
511   *    `multiple` attribute, all values are considered, otherwise only the first
512   *    one is taken into account.
513   */
514  async select(...values: string[]): Promise<string[]> {
515    for (const value of values)
516      assert(
517        helper.isString(value),
518        'Values must be strings. Found value "' +
519          value +
520          '" of type "' +
521          typeof value +
522          '"'
523      );
524
525    return this.evaluate<(element: Element, values: string[]) => string[]>(
526      (element, values) => {
527        if (!(element instanceof HTMLSelectElement))
528          throw new Error('Element is not a <select> element.');
529
530        const options = Array.from(element.options);
531        element.value = undefined;
532        for (const option of options) {
533          option.selected = values.includes(option.value);
534          if (option.selected && !element.multiple) break;
535        }
536        element.dispatchEvent(new Event('input', { bubbles: true }));
537        element.dispatchEvent(new Event('change', { bubbles: true }));
538        return options
539          .filter((option) => option.selected)
540          .map((option) => option.value);
541      },
542      values
543    );
544  }
545
546  /**
547   * This method expects `elementHandle` to point to an
548   * {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input | input element}.
549   * @param filePaths - Sets the value of the file input to these paths.
550   *    If some of the  `filePaths` are relative paths, then they are resolved
551   *    relative to the {@link https://nodejs.org/api/process.html#process_process_cwd | current working directory}
552   */
553  async uploadFile(...filePaths: string[]): Promise<void> {
554    const isMultiple = await this.evaluate<(element: Element) => boolean>(
555      (element) => {
556        if (!(element instanceof HTMLInputElement)) {
557          throw new Error('uploadFile can only be called on an input element.');
558        }
559        return element.multiple;
560      }
561    );
562    assert(
563      filePaths.length <= 1 || isMultiple,
564      'Multiple file uploads only work with <input type=file multiple>'
565    );
566
567    if (!isNode) {
568      throw new Error(
569        `JSHandle#uploadFile can only be used in Node environments.`
570      );
571    }
572    /*
573     This import is only needed for `uploadFile`, so keep it scoped here to
574     avoid paying the cost unnecessarily.
575    */
576    const path = await import('path');
577    const fs = await helper.importFSModule();
578    // Locate all files and confirm that they exist.
579    const files = await Promise.all(
580      filePaths.map(async (filePath) => {
581        const resolvedPath: string = path.resolve(filePath);
582        try {
583          await fs.promises.access(resolvedPath, fs.constants.R_OK);
584        } catch (error) {
585          if (error.code === 'ENOENT')
586            throw new Error(`${filePath} does not exist or is not readable`);
587        }
588
589        return resolvedPath;
590      })
591    );
592    const { objectId } = this._remoteObject;
593    const { node } = await this._client.send('DOM.describeNode', { objectId });
594    const { backendNodeId } = node;
595
596    /*  The zero-length array is a special case, it seems that
597        DOM.setFileInputFiles does not actually update the files in that case,
598        so the solution is to eval the element value to a new FileList directly.
599    */
600    if (files.length === 0) {
601      await (this as ElementHandle<HTMLInputElement>).evaluate((element) => {
602        element.files = new DataTransfer().files;
603
604        // Dispatch events for this case because it should behave akin to a user action.
605        element.dispatchEvent(new Event('input', { bubbles: true }));
606        element.dispatchEvent(new Event('change', { bubbles: true }));
607      });
608    } else {
609      await this._client.send('DOM.setFileInputFiles', {
610        objectId,
611        files,
612        backendNodeId,
613      });
614    }
615  }
616
617  /**
618   * This method scrolls element into view if needed, and then uses
619   * {@link Touchscreen.tap} to tap in the center of the element.
620   * If the element is detached from DOM, the method throws an error.
621   */
622  async tap(): Promise<void> {
623    await this._scrollIntoViewIfNeeded();
624    const { x, y } = await this._clickablePoint();
625    await this._page.touchscreen.tap(x, y);
626  }
627
628  /**
629   * Calls {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus | focus} on the element.
630   */
631  async focus(): Promise<void> {
632    await (this as ElementHandle<HTMLElement>).evaluate((element) =>
633      element.focus()
634    );
635  }
636
637  /**
638   * Focuses the element, and then sends a `keydown`, `keypress`/`input`, and
639   * `keyup` event for each character in the text.
640   *
641   * To press a special key, like `Control` or `ArrowDown`,
642   * use {@link ElementHandle.press}.
643   *
644   * @example
645   * ```js
646   * await elementHandle.type('Hello'); // Types instantly
647   * await elementHandle.type('World', {delay: 100}); // Types slower, like a user
648   * ```
649   *
650   * @example
651   * An example of typing into a text field and then submitting the form:
652   *
653   * ```js
654   * const elementHandle = await page.$('input');
655   * await elementHandle.type('some text');
656   * await elementHandle.press('Enter');
657   * ```
658   */
659  async type(text: string, options?: { delay: number }): Promise<void> {
660    await this.focus();
661    await this._page.keyboard.type(text, options);
662  }
663
664  /**
665   * Focuses the element, and then uses {@link Keyboard.down} and {@link Keyboard.up}.
666   *
667   * @remarks
668   * If `key` is a single character and no modifier keys besides `Shift`
669   * are being held down, a `keypress`/`input` event will also be generated.
670   * The `text` option can be specified to force an input event to be generated.
671   *
672   * **NOTE** Modifier keys DO affect `elementHandle.press`. Holding down `Shift`
673   * will type the text in upper case.
674   *
675   * @param key - Name of key to press, such as `ArrowLeft`.
676   *    See {@link KeyInput} for a list of all key names.
677   */
678  async press(key: KeyInput, options?: PressOptions): Promise<void> {
679    await this.focus();
680    await this._page.keyboard.press(key, options);
681  }
682
683  /**
684   * This method returns the bounding box of the element (relative to the main frame),
685   * or `null` if the element is not visible.
686   */
687  async boundingBox(): Promise<BoundingBox | null> {
688    const result = await this._getBoxModel();
689
690    if (!result) return null;
691
692    const quad = result.model.border;
693    const x = Math.min(quad[0], quad[2], quad[4], quad[6]);
694    const y = Math.min(quad[1], quad[3], quad[5], quad[7]);
695    const width = Math.max(quad[0], quad[2], quad[4], quad[6]) - x;
696    const height = Math.max(quad[1], quad[3], quad[5], quad[7]) - y;
697
698    return { x, y, width, height };
699  }
700
701  /**
702   * This method returns boxes of the element, or `null` if the element is not visible.
703   *
704   * @remarks
705   *
706   * Boxes are represented as an array of points;
707   * Each Point is an object `{x, y}`. Box points are sorted clock-wise.
708   */
709  async boxModel(): Promise<BoxModel | null> {
710    const result = await this._getBoxModel();
711
712    if (!result) return null;
713
714    const { content, padding, border, margin, width, height } = result.model;
715    return {
716      content: this._fromProtocolQuad(content),
717      padding: this._fromProtocolQuad(padding),
718      border: this._fromProtocolQuad(border),
719      margin: this._fromProtocolQuad(margin),
720      width,
721      height,
722    };
723  }
724
725  /**
726   * This method scrolls element into view if needed, and then uses
727   * {@link Page.screenshot} to take a screenshot of the element.
728   * If the element is detached from DOM, the method throws an error.
729   */
730  async screenshot(options = {}): Promise<string | Buffer | void> {
731    let needsViewportReset = false;
732
733    let boundingBox = await this.boundingBox();
734    assert(boundingBox, 'Node is either not visible or not an HTMLElement');
735
736    const viewport = this._page.viewport();
737
738    if (
739      viewport &&
740      (boundingBox.width > viewport.width ||
741        boundingBox.height > viewport.height)
742    ) {
743      const newViewport = {
744        width: Math.max(viewport.width, Math.ceil(boundingBox.width)),
745        height: Math.max(viewport.height, Math.ceil(boundingBox.height)),
746      };
747      await this._page.setViewport(Object.assign({}, viewport, newViewport));
748
749      needsViewportReset = true;
750    }
751
752    await this._scrollIntoViewIfNeeded();
753
754    boundingBox = await this.boundingBox();
755    assert(boundingBox, 'Node is either not visible or not an HTMLElement');
756    assert(boundingBox.width !== 0, 'Node has 0 width.');
757    assert(boundingBox.height !== 0, 'Node has 0 height.');
758
759    const {
760      layoutViewport: { pageX, pageY },
761    } = await this._client.send('Page.getLayoutMetrics');
762
763    const clip = Object.assign({}, boundingBox);
764    clip.x += pageX;
765    clip.y += pageY;
766
767    const imageData = await this._page.screenshot(
768      Object.assign(
769        {},
770        {
771          clip,
772        },
773        options
774      )
775    );
776
777    if (needsViewportReset) await this._page.setViewport(viewport);
778
779    return imageData;
780  }
781
782  /**
783   * Runs `element.querySelector` within the page. If no element matches the selector,
784   * the return value resolves to `null`.
785   */
786  async $<T extends Element = Element>(
787    selector: string
788  ): Promise<ElementHandle<T> | null> {
789    const { updatedSelector, queryHandler } =
790      getQueryHandlerAndSelector(selector);
791    return queryHandler.queryOne(this, updatedSelector);
792  }
793
794  /**
795   * Runs `element.querySelectorAll` within the page. If no elements match the selector,
796   * the return value resolves to `[]`.
797   */
798  async $$<T extends Element = Element>(
799    selector: string
800  ): Promise<Array<ElementHandle<T>>> {
801    const { updatedSelector, queryHandler } =
802      getQueryHandlerAndSelector(selector);
803    return queryHandler.queryAll(this, updatedSelector);
804  }
805
806  /**
807   * This method runs `document.querySelector` within the element and passes it as
808   * the first argument to `pageFunction`. If there's no element matching `selector`,
809   * the method throws an error.
810   *
811   * If `pageFunction` returns a Promise, then `frame.$eval` would wait for the promise
812   * to resolve and return its value.
813   *
814   * @example
815   * ```js
816   * const tweetHandle = await page.$('.tweet');
817   * expect(await tweetHandle.$eval('.like', node => node.innerText)).toBe('100');
818   * expect(await tweetHandle.$eval('.retweets', node => node.innerText)).toBe('10');
819   * ```
820   */
821  async $eval<ReturnType>(
822    selector: string,
823    pageFunction: (
824      element: Element,
825      ...args: unknown[]
826    ) => ReturnType | Promise<ReturnType>,
827    ...args: SerializableOrJSHandle[]
828  ): Promise<WrapElementHandle<ReturnType>> {
829    const elementHandle = await this.$(selector);
830    if (!elementHandle)
831      throw new Error(
832        `Error: failed to find element matching selector "${selector}"`
833      );
834    const result = await elementHandle.evaluate<
835      (
836        element: Element,
837        ...args: SerializableOrJSHandle[]
838      ) => ReturnType | Promise<ReturnType>
839    >(pageFunction, ...args);
840    await elementHandle.dispose();
841
842    /**
843     * This `as` is a little unfortunate but helps TS understand the behavior of
844     * `elementHandle.evaluate`. If evaluate returns an element it will return an
845     * ElementHandle instance, rather than the plain object. All the
846     * WrapElementHandle type does is wrap ReturnType into
847     * ElementHandle<ReturnType> if it is an ElementHandle, or leave it alone as
848     * ReturnType if it isn't.
849     */
850    return result as WrapElementHandle<ReturnType>;
851  }
852
853  /**
854   * This method runs `document.querySelectorAll` within the element and passes it as
855   * the first argument to `pageFunction`. If there's no element matching `selector`,
856   * the method throws an error.
857   *
858   * If `pageFunction` returns a Promise, then `frame.$$eval` would wait for the
859   * promise to resolve and return its value.
860   *
861   * @example
862   * ```html
863   * <div class="feed">
864   *   <div class="tweet">Hello!</div>
865   *   <div class="tweet">Hi!</div>
866   * </div>
867   * ```
868   *
869   * @example
870   * ```js
871   * const feedHandle = await page.$('.feed');
872   * expect(await feedHandle.$$eval('.tweet', nodes => nodes.map(n => n.innerText)))
873   *  .toEqual(['Hello!', 'Hi!']);
874   * ```
875   */
876  async $$eval<ReturnType>(
877    selector: string,
878    pageFunction: (
879      elements: Element[],
880      ...args: unknown[]
881    ) => ReturnType | Promise<ReturnType>,
882    ...args: SerializableOrJSHandle[]
883  ): Promise<WrapElementHandle<ReturnType>> {
884    const { updatedSelector, queryHandler } =
885      getQueryHandlerAndSelector(selector);
886    const arrayHandle = await queryHandler.queryAllArray(this, updatedSelector);
887    const result = await arrayHandle.evaluate<
888      (
889        elements: Element[],
890        ...args: unknown[]
891      ) => ReturnType | Promise<ReturnType>
892    >(pageFunction, ...args);
893    await arrayHandle.dispose();
894    /* This `as` exists for the same reason as the `as` in $eval above.
895     * See the comment there for a full explanation.
896     */
897    return result as WrapElementHandle<ReturnType>;
898  }
899
900  /**
901   * The method evaluates the XPath expression relative to the elementHandle.
902   * If there are no such elements, the method will resolve to an empty array.
903   * @param expression - Expression to {@link https://developer.mozilla.org/en-US/docs/Web/API/Document/evaluate | evaluate}
904   */
905  async $x(expression: string): Promise<ElementHandle[]> {
906    const arrayHandle = await this.evaluateHandle(
907      (element: Document, expression: string) => {
908        const document = element.ownerDocument || element;
909        const iterator = document.evaluate(
910          expression,
911          element,
912          null,
913          XPathResult.ORDERED_NODE_ITERATOR_TYPE
914        );
915        const array = [];
916        let item;
917        while ((item = iterator.iterateNext())) array.push(item);
918        return array;
919      },
920      expression
921    );
922    const properties = await arrayHandle.getProperties();
923    await arrayHandle.dispose();
924    const result = [];
925    for (const property of properties.values()) {
926      const elementHandle = property.asElement();
927      if (elementHandle) result.push(elementHandle);
928    }
929    return result;
930  }
931
932  /**
933   * Resolves to true if the element is visible in the current viewport.
934   */
935  async isIntersectingViewport(): Promise<boolean> {
936    return await this.evaluate<(element: Element) => Promise<boolean>>(
937      async (element) => {
938        const visibleRatio = await new Promise((resolve) => {
939          const observer = new IntersectionObserver((entries) => {
940            resolve(entries[0].intersectionRatio);
941            observer.disconnect();
942          });
943          observer.observe(element);
944        });
945        return visibleRatio > 0;
946      }
947    );
948  }
949}
950
951/**
952 * @public
953 */
954export interface ClickOptions {
955  /**
956   * Time to wait between `mousedown` and `mouseup` in milliseconds.
957   *
958   * @defaultValue 0
959   */
960  delay?: number;
961  /**
962   * @defaultValue 'left'
963   */
964  button?: 'left' | 'right' | 'middle';
965  /**
966   * @defaultValue 1
967   */
968  clickCount?: number;
969}
970
971/**
972 * @public
973 */
974export interface PressOptions {
975  /**
976   * Time to wait between `keydown` and `keyup` in milliseconds. Defaults to 0.
977   */
978  delay?: number;
979  /**
980   * If specified, generates an input event with this text.
981   */
982  text?: string;
983}
984
985function computeQuadArea(quad: Array<{ x: number; y: number }>): number {
986  /* Compute sum of all directed areas of adjacent triangles
987    https://en.wikipedia.org/wiki/Polygon#Simple_polygons
988  */
989  let area = 0;
990  for (let i = 0; i < quad.length; ++i) {
991    const p1 = quad[i];
992    const p2 = quad[(i + 1) % quad.length];
993    area += (p1.x * p2.y - p2.x * p1.y) / 2;
994  }
995  return Math.abs(area);
996}
997