<commit_before>/**
 * Copyright 2019 Google Inc. All rights reserved.
 * Modifications copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import type * as dom from './dom';
import type { Rect } from '../common/types';
import { helper } from './helper';
import type { Page } from './page';
import type { Frame } from './frames';
import type { ParsedSelector } from './isomorphic/selectorParser';
import type * as types from './types';
import type { Progress } from './progress';
import { assert } from '../utils';
import { MultiMap } from '../utils/multimap';

declare global {
  interface Window {
    __cleanupScreenshot?: () => void;
  }
}

export type ScreenshotOptions = {
  type?: 'png' | 'jpeg',
  quality?: number,
  omitBackground?: boolean,
  animations?: 'disabled' | 'allow',
  mask?: { frame: Frame, selector: string}[],
  fullPage?: boolean,
  clip?: Rect,
  scale?: 'css' | 'device',
  caret?: 'hide' | 'initial',
};

export class Screenshotter {
  private _queue = new TaskQueue();
  private _page: Page;

  constructor(page: Page) {
    this._page = page;
    this._queue = new TaskQueue();
  }

  private async _originalViewportSize(progress: Progress): Promise<{ viewportSize: types.Size, originalViewportSize: types.Size | null }> {
    const originalViewportSize = this._page.viewportSize();
    let viewportSize = originalViewportSize;
    if (!viewportSize)
      viewportSize = await this._page.mainFrame().waitForFunctionValueInUtility(progress, () => ({ width: window.innerWidth, height: window.innerHeight }));
    return { viewportSize, originalViewportSize };
  }

  private async _fullPageSize(progress: Progress): Promise<types.Size> {
    const fullPageSize = await this._page.mainFrame().waitForFunctionValueInUtility(progress, () => {
      if (!document.body || !document.documentElement)
        return null;
      return {
        width: Math.max(
            document.body.scrollWidth, document.documentElement.scrollWidth,
            document.body.offsetWidth, document.documentElement.offsetWidth,
            document.body.clientWidth, document.documentElement.clientWidth
        ),
        height: Math.max(
            document.body.scrollHeight, document.documentElement.scrollHeight,
            document.body.offsetHeight, document.documentElement.offsetHeight,
            document.body.clientHeight, document.documentElement.clientHeight
        ),
      };
    });
    return fullPageSize!;
  }

  async screenshotPage(progress: Progress, options: ScreenshotOptions): Promise<Buffer> {
    const format = validateScreenshotOptions(options);
    return this._queue.postTask(async () => {
      progress.log('taking page screenshot');
      const { viewportSize } = await this._originalViewportSize(progress);
      await this._preparePageForScreenshot(progress, options.caret !== 'initial', options.animations === 'disabled');
      progress.throwIfAborted(); // Avoid restoring after failure - should be done by cleanup.

      if (options.fullPage) {
        const fullPageSize = await this._fullPageSize(progress);
        let documentRect = { x: 0, y: 0, width: fullPageSize.width, height: fullPageSize.height };
        const fitsViewport = fullPageSize.width <= viewportSize.width && fullPageSize.height <= viewportSize.height;
        if (options.clip)
          documentRect = trimClipToSize(options.clip, documentRect);
        const buffer = await this._screenshot(progress, format, documentRect, undefined, fitsViewport, options);
        progress.throwIfAborted(); // Avoid restoring after failure - should be done by cleanup.
        await this._restorePageAfterScreenshot();
        return buffer;
      }

      const viewportRect = options.clip ? trimClipToSize(options.clip, viewportSize) : { x: 0, y: 0, ...viewportSize };
      const buffer = await this._screenshot(progress, format, undefined, viewportRect, true, options);
      progress.throwIfAborted(); // Avoid restoring after failure - should be done by cleanup.
      await this._restorePageAfterScreenshot();
      return buffer;
    });
  }

  async screenshotElement(progress: Progress, handle: dom.ElementHandle, options: ScreenshotOptions): Promise<Buffer> {
    const format = validateScreenshotOptions(options);
    return this._queue.postTask(async () => {
      progress.log('taking element screenshot');
      const { viewportSize } = await this._originalViewportSize(progress);

      await this._preparePageForScreenshot(progress, options.caret !== 'initial', options.animations === 'disabled');
      progress.throwIfAborted(); // Do not do extra work.

      await handle._waitAndScrollIntoViewIfNeeded(progress, true /* waitForVisible */);

      progress.throwIfAborted(); // Do not do extra work.
      const boundingBox = await handle.boundingBox();
      assert(boundingBox, 'Node is either not visible or not an HTMLElement');
      assert(boundingBox.width !== 0, 'Node has 0 width.');
      assert(boundingBox.height !== 0, 'Node has 0 height.');

      const fitsViewport = boundingBox.width <= viewportSize.width && boundingBox.height <= viewportSize.height;
      progress.throwIfAborted(); // Avoid extra work.
      const scrollOffset = await this._page.mainFrame().waitForFunctionValueInUtility(progress, () => ({ x: window.scrollX, y: window.scrollY }));
      const documentRect = { ...boundingBox };
      documentRect.x += scrollOffset.x;
      documentRect.y += scrollOffset.y;
      const buffer = await this._screenshot(progress, format, helper.enclosingIntRect(documentRect), undefined, fitsViewport, options);
      progress.throwIfAborted(); // Avoid restoring after failure - should be done by cleanup.
      await this._restorePageAfterScreenshot();
      return buffer;
    });
  }

  async _preparePageForScreenshot(progress: Progress, hideCaret: boolean, disableAnimations: boolean) {
    if (disableAnimations)
      progress.log('  disabled all CSS animations');
    await Promise.all(this._page.frames().map(async frame => {
      await frame.nonStallingEvaluateInExistingContext('(' + (async function(hideCaret: boolean, disableAnimations: boolean) {
        const styleTag = document.createElement('style');
        if (hideCaret) {
          styleTag.textContent = `
            *:not(#playwright-aaaaaaaaaa.playwright-bbbbbbbbbbb.playwright-cccccccccc.playwright-dddddddddd.playwright-eeeeeeeee) {
              caret-color: transparent !important;
            }
          `;
          document.documentElement.append(styleTag);
        }
        const infiniteAnimationsToResume: Set<Animation> = new Set();
        const cleanupCallbacks: (() => void)[] = [];

        if (disableAnimations) {
          const collectRoots = (root: Document | ShadowRoot, roots: (Document|ShadowRoot)[] = []): (Document|ShadowRoot)[] => {
            roots.push(root);
            const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
            do {
              const node = walker.currentNode;<commit_msg>fix(screenshot): avoid unnecessary work (#16035)<commit_after>/**
 * Copyright 2019 Google Inc. All rights reserved.
 * Modifications copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import type * as dom from './dom';
import type { Rect } from '../common/types';
import { helper } from './helper';
import type { Page } from './page';
import type { Frame } from './frames';
import type { ParsedSelector } from './isomorphic/selectorParser';
import type * as types from './types';
import type { Progress } from './progress';
import { assert } from '../utils';
import { MultiMap } from '../utils/multimap';

declare global {
  interface Window {
    __cleanupScreenshot?: () => void;
  }
}

export type ScreenshotOptions = {
  type?: 'png' | 'jpeg',
  quality?: number,
  omitBackground?: boolean,
  animations?: 'disabled' | 'allow',
  mask?: { frame: Frame, selector: string}[],
  fullPage?: boolean,
  clip?: Rect,
  scale?: 'css' | 'device',
  caret?: 'hide' | 'initial',
};

export class Screenshotter {
  private _queue = new TaskQueue();
  private _page: Page;

  constructor(page: Page) {
    this._page = page;
    this._queue = new TaskQueue();
  }

  private async _originalViewportSize(progress: Progress): Promise<{ viewportSize: types.Size, originalViewportSize: types.Size | null }> {
    const originalViewportSize = this._page.viewportSize();
    let viewportSize = originalViewportSize;
    if (!viewportSize)
      viewportSize = await this._page.mainFrame().waitForFunctionValueInUtility(progress, () => ({ width: window.innerWidth, height: window.innerHeight }));
    return { viewportSize, originalViewportSize };
  }

  private async _fullPageSize(progress: Progress): Promise<types.Size> {
    const fullPageSize = await this._page.mainFrame().waitForFunctionValueInUtility(progress, () => {
      if (!document.body || !document.documentElement)
        return null;
      return {
        width: Math.max(
            document.body.scrollWidth, document.documentElement.scrollWidth,
            document.body.offsetWidth, document.documentElement.offsetWidth,
            document.body.clientWidth, document.documentElement.clientWidth
        ),
        height: Math.max(
            document.body.scrollHeight, document.documentElement.scrollHeight,
            document.body.offsetHeight, document.documentElement.offsetHeight,
            document.body.clientHeight, document.documentElement.clientHeight
        ),
      };
    });
    return fullPageSize!;
  }

  async screenshotPage(progress: Progress, options: ScreenshotOptions): Promise<Buffer> {
    const format = validateScreenshotOptions(options);
    return this._queue.postTask(async () => {
      progress.log('taking page screenshot');
      const { viewportSize } = await this._originalViewportSize(progress);
      await this._preparePageForScreenshot(progress, options.caret !== 'initial', options.animations === 'disabled');
      progress.throwIfAborted(); // Avoid restoring after failure - should be done by cleanup.

      if (options.fullPage) {
        const fullPageSize = await this._fullPageSize(progress);
        let documentRect = { x: 0, y: 0, width: fullPageSize.width, height: fullPageSize.height };
        const fitsViewport = fullPageSize.width <= viewportSize.width && fullPageSize.height <= viewportSize.height;
        if (options.clip)
          documentRect = trimClipToSize(options.clip, documentRect);
        const buffer = await this._screenshot(progress, format, documentRect, undefined, fitsViewport, options);
        progress.throwIfAborted(); // Avoid restoring after failure - should be done by cleanup.
        await this._restorePageAfterScreenshot();
        return buffer;
      }

      const viewportRect = options.clip ? trimClipToSize(options.clip, viewportSize) : { x: 0, y: 0, ...viewportSize };
      const buffer = await this._screenshot(progress, format, undefined, viewportRect, true, options);
      progress.throwIfAborted(); // Avoid restoring after failure - should be done by cleanup.
      await this._restorePageAfterScreenshot();
      return buffer;
    });
  }

  async screenshotElement(progress: Progress, handle: dom.ElementHandle, options: ScreenshotOptions): Promise<Buffer> {
    const format = validateScreenshotOptions(options);
    return this._queue.postTask(async () => {
      progress.log('taking element screenshot');
      const { viewportSize } = await this._originalViewportSize(progress);

      await this._preparePageForScreenshot(progress, options.caret !== 'initial', options.animations === 'disabled');
      progress.throwIfAborted(); // Do not do extra work.

      await handle._waitAndScrollIntoViewIfNeeded(progress, true /* waitForVisible */);

      progress.throwIfAborted(); // Do not do extra work.
      const boundingBox = await handle.boundingBox();
      assert(boundingBox, 'Node is either not visible or not an HTMLElement');
      assert(boundingBox.width !== 0, 'Node has 0 width.');
      assert(boundingBox.height !== 0, 'Node has 0 height.');

      const fitsViewport = boundingBox.width <= viewportSize.width && boundingBox.height <= viewportSize.height;
      progress.throwIfAborted(); // Avoid extra work.
      const scrollOffset = await this._page.mainFrame().waitForFunctionValueInUtility(progress, () => ({ x: window.scrollX, y: window.scrollY }));
      const documentRect = { ...boundingBox };
      documentRect.x += scrollOffset.x;
      documentRect.y += scrollOffset.y;
      const buffer = await this._screenshot(progress, format, helper.enclosingIntRect(documentRect), undefined, fitsViewport, options);
      progress.throwIfAborted(); // Avoid restoring after failure - should be done by cleanup.
      await this._restorePageAfterScreenshot();
      return buffer;
    });
  }

  async _preparePageForScreenshot(progress: Progress, hideCaret: boolean, disableAnimations: boolean) {
    if (!hideCaret && !disableAnimations)
      return;

    if (disableAnimations)
      progress.log('  disabled all CSS animations');
    await Promise.all(this._page.frames().map(async frame => {
      await frame.nonStallingEvaluateInExistingContext('(' + (async function(hideCaret: boolean, disableAnimations: boolean) {
        const styleTag = document.createElement('style');
        if (hideCaret) {
          styleTag.textContent = `
            *:not(#playwright-aaaaaaaaaa.playwright-bbbbbbbbbbb.playwright-cccccccccc.playwright-dddddddddd.playwright-eeeeeeeee) {
              caret-color: transparent !important;
            }
          `;
          document.documentElement.append(styleTag);
        }
        const infiniteAnimationsToResume: Set<Animation> = new Set();
        const cleanupCallbacks: (() => void)[] = [];

        if (disableAnimations) {
          const collectRoots = (root: Document | ShadowRoot, roots: (Document|ShadowRoot)[] = []): (Document|ShadowRoot)[] => {
            roots.push(root);
            const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
            do {
              const node = walker.currentNode;
{'fraction_non_alphanumeric': 0.11502966381015162, 'fraction_numerical': 0.0024390243902439024, 'mean_word_length': 3.516522774635308, 'pattern_counts': {'":': 0, '<': 21, '<?xml version=': 0, '>': 31, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 10, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '8381', 'n_tokens_mistral': 4121, 'n_tokens_neox': 4027, 'n_words': 1101}