feat: support mask option in screenshot methods (#12072)

Fixes https://github.com/microsoft/playwright/issues/10162
This commit is contained in:
Andrey Lushnikov 2022-02-15 08:05:05 -07:00 committed by GitHub
parent 618cc66c8d
commit 363b8a6970
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 255 additions and 17 deletions

View file

@ -913,11 +913,18 @@ saved to the disk.
Specify screenshot type, defaults to `png`. Specify screenshot type, defaults to `png`.
## screenshot-option-mask
- `mask` <[Array]<[Locator]>>
Specify locators that should be masked when the screenshot is taken. Masked elements will be overlayed with
a pink box `#FF00FF` that completely covers its bounding box.
## screenshot-options-common-list ## screenshot-options-common-list
- %%-screenshot-option-disable-animations-%% - %%-screenshot-option-disable-animations-%%
- %%-screenshot-option-omit-background-%% - %%-screenshot-option-omit-background-%%
- %%-screenshot-option-quality-%% - %%-screenshot-option-quality-%%
- %%-screenshot-option-path-%% - %%-screenshot-option-path-%%
- %%-screenshot-option-type-%% - %%-screenshot-option-type-%%
- %%-screenshot-option-mask-%%
- %%-input-timeout-%% - %%-input-timeout-%%

View file

@ -16,6 +16,7 @@
import * as channels from '../protocol/channels'; import * as channels from '../protocol/channels';
import { Frame } from './frame'; import { Frame } from './frame';
import { Locator } from './locator';
import { JSHandle, serializeArgument, parseResult } from './jsHandle'; import { JSHandle, serializeArgument, parseResult } from './jsHandle';
import { ChannelOwner } from './channelOwner'; import { ChannelOwner } from './channelOwner';
import { SelectOption, FilePayload, Rect, SelectOptionOptions } from './types'; import { SelectOption, FilePayload, Rect, SelectOptionOptions } from './types';
@ -173,10 +174,16 @@ export class ElementHandle<T extends Node = Node> extends JSHandle<T> implements
return value === undefined ? null : value; return value === undefined ? null : value;
} }
async screenshot(options: channels.ElementHandleScreenshotOptions & { path?: string } = {}): Promise<Buffer> { async screenshot(options: Omit<channels.ElementHandleScreenshotOptions, 'mask'> & { path?: string, mask?: Locator[] } = {}): Promise<Buffer> {
const copy = { ...options }; const copy: channels.ElementHandleScreenshotOptions = { ...options, mask: undefined };
if (!copy.type) if (!copy.type)
copy.type = determineScreenshotType(options); copy.type = determineScreenshotType(options);
if (options.mask) {
copy.mask = options.mask.map(locator => ({
frame: locator._frame._channel,
selector: locator._selector,
}));
}
const result = await this._elementChannel.screenshot(copy); const result = await this._elementChannel.screenshot(copy);
const buffer = Buffer.from(result.binary, 'base64'); const buffer = Buffer.from(result.binary, 'base64');
if (options.path) { if (options.path) {

View file

@ -26,8 +26,8 @@ import { parseResult, serializeArgument } from './jsHandle';
import { escapeWithQuotes } from '../utils/stringUtils'; import { escapeWithQuotes } from '../utils/stringUtils';
export class Locator implements api.Locator { export class Locator implements api.Locator {
private _frame: Frame; _frame: Frame;
private _selector: string; _selector: string;
constructor(frame: Frame, selector: string, options?: { hasText?: string | RegExp, has?: Locator }) { constructor(frame: Frame, selector: string, options?: { hasText?: string | RegExp, has?: Locator }) {
this._frame = frame; this._frame = frame;
@ -200,7 +200,7 @@ export class Locator implements api.Locator {
return this._frame.press(this._selector, key, { strict: true, ...options }); return this._frame.press(this._selector, key, { strict: true, ...options });
} }
async screenshot(options: channels.ElementHandleScreenshotOptions & { path?: string } = {}): Promise<Buffer> { async screenshot(options: Omit<channels.ElementHandleScreenshotOptions, 'mask'> & { path?: string, mask?: Locator[] } = {}): Promise<Buffer> {
return this._withElement((h, timeout) => h.screenshot({ ...options, timeout }), options.timeout); return this._withElement((h, timeout) => h.screenshot({ ...options, timeout }), options.timeout);
} }

View file

@ -457,10 +457,16 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
await this._channel.setNetworkInterceptionEnabled({ enabled: false }); await this._channel.setNetworkInterceptionEnabled({ enabled: false });
} }
async screenshot(options: channels.PageScreenshotOptions & { path?: string } = {}): Promise<Buffer> { async screenshot(options: Omit<channels.PageScreenshotOptions, 'mask'> & { path?: string, mask?: Locator[] } = {}): Promise<Buffer> {
const copy = { ...options }; const copy: channels.PageScreenshotOptions = { ...options, mask: undefined };
if (!copy.type) if (!copy.type)
copy.type = determineScreenshotType(options); copy.type = determineScreenshotType(options);
if (options.mask) {
copy.mask = options.mask.map(locator => ({
frame: locator._frame._channel,
selector: locator._selector,
}));
}
const result = await this._channel.screenshot(copy); const result = await this._channel.screenshot(copy);
const buffer = Buffer.from(result.binary, 'base64'); const buffer = Buffer.from(result.binary, 'base64');
if (options.path) { if (options.path) {

View file

@ -15,6 +15,7 @@
*/ */
import { ElementHandle } from '../server/dom'; import { ElementHandle } from '../server/dom';
import { Frame } from '../server/frames';
import * as js from '../server/javascript'; import * as js from '../server/javascript';
import * as channels from '../protocol/channels'; import * as channels from '../protocol/channels';
import { DispatcherScope, existingDispatcher, lookupNullableDispatcher } from './dispatcher'; import { DispatcherScope, existingDispatcher, lookupNullableDispatcher } from './dispatcher';
@ -171,7 +172,11 @@ export class ElementHandleDispatcher extends JSHandleDispatcher implements chann
} }
async screenshot(params: channels.ElementHandleScreenshotParams, metadata: CallMetadata): Promise<channels.ElementHandleScreenshotResult> { async screenshot(params: channels.ElementHandleScreenshotParams, metadata: CallMetadata): Promise<channels.ElementHandleScreenshotResult> {
return { binary: (await this._elementHandle.screenshot(metadata, params)).toString('base64') }; const mask: { frame: Frame, selector: string }[] = (params.mask || []).map(({ frame, selector }) => ({
frame: (frame as FrameDispatcher)._object,
selector,
}));
return { binary: (await this._elementHandle.screenshot(metadata, { ...params, mask })).toString('base64') };
} }
async querySelector(params: channels.ElementHandleQuerySelectorParams, metadata: CallMetadata): Promise<channels.ElementHandleQuerySelectorResult> { async querySelector(params: channels.ElementHandleQuerySelectorParams, metadata: CallMetadata): Promise<channels.ElementHandleQuerySelectorResult> {

View file

@ -151,7 +151,11 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageChannel> imple
} }
async screenshot(params: channels.PageScreenshotParams, metadata: CallMetadata): Promise<channels.PageScreenshotResult> { async screenshot(params: channels.PageScreenshotParams, metadata: CallMetadata): Promise<channels.PageScreenshotResult> {
return { binary: (await this._page.screenshot(metadata, params)).toString('base64') }; const mask: { frame: Frame, selector: string }[] = (params.mask || []).map(({ frame, selector }) => ({
frame: (frame as FrameDispatcher)._object,
selector,
}));
return { binary: (await this._page.screenshot(metadata, { ...params, mask })).toString('base64') };
} }
async close(params: channels.PageCloseParams, metadata: CallMetadata): Promise<void> { async close(params: channels.PageCloseParams, metadata: CallMetadata): Promise<void> {

View file

@ -1494,6 +1494,10 @@ export type PageScreenshotParams = {
fullPage?: boolean, fullPage?: boolean,
disableAnimations?: boolean, disableAnimations?: boolean,
clip?: Rect, clip?: Rect,
mask?: {
frame: FrameChannel,
selector: string,
}[],
}; };
export type PageScreenshotOptions = { export type PageScreenshotOptions = {
timeout?: number, timeout?: number,
@ -1503,6 +1507,10 @@ export type PageScreenshotOptions = {
fullPage?: boolean, fullPage?: boolean,
disableAnimations?: boolean, disableAnimations?: boolean,
clip?: Rect, clip?: Rect,
mask?: {
frame: FrameChannel,
selector: string,
}[],
}; };
export type PageScreenshotResult = { export type PageScreenshotResult = {
binary: Binary, binary: Binary,
@ -2798,6 +2806,10 @@ export type ElementHandleScreenshotParams = {
quality?: number, quality?: number,
omitBackground?: boolean, omitBackground?: boolean,
disableAnimations?: boolean, disableAnimations?: boolean,
mask?: {
frame: FrameChannel,
selector: string,
}[],
}; };
export type ElementHandleScreenshotOptions = { export type ElementHandleScreenshotOptions = {
timeout?: number, timeout?: number,
@ -2805,6 +2817,10 @@ export type ElementHandleScreenshotOptions = {
quality?: number, quality?: number,
omitBackground?: boolean, omitBackground?: boolean,
disableAnimations?: boolean, disableAnimations?: boolean,
mask?: {
frame: FrameChannel,
selector: string,
}[],
}; };
export type ElementHandleScreenshotResult = { export type ElementHandleScreenshotResult = {
binary: Binary, binary: Binary,

View file

@ -1003,6 +1003,13 @@ Page:
fullPage: boolean? fullPage: boolean?
disableAnimations: boolean? disableAnimations: boolean?
clip: Rect? clip: Rect?
mask:
type: array?
items:
type: object
properties:
frame: Frame
selector: string
returns: returns:
binary: binary binary: binary
@ -2155,6 +2162,13 @@ ElementHandle:
quality: number? quality: number?
omitBackground: boolean? omitBackground: boolean?
disableAnimations: boolean? disableAnimations: boolean?
mask:
type: array?
items:
type: object
properties:
frame: Frame
selector: string
returns: returns:
binary: binary binary: binary

View file

@ -548,6 +548,10 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
fullPage: tOptional(tBoolean), fullPage: tOptional(tBoolean),
disableAnimations: tOptional(tBoolean), disableAnimations: tOptional(tBoolean),
clip: tOptional(tType('Rect')), clip: tOptional(tType('Rect')),
mask: tOptional(tArray(tObject({
frame: tChannel('Frame'),
selector: tString,
}))),
}); });
scheme.PageSetExtraHTTPHeadersParams = tObject({ scheme.PageSetExtraHTTPHeadersParams = tObject({
headers: tArray(tType('NameValue')), headers: tArray(tType('NameValue')),
@ -1038,6 +1042,10 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
quality: tOptional(tNumber), quality: tOptional(tNumber),
omitBackground: tOptional(tBoolean), omitBackground: tOptional(tBoolean),
disableAnimations: tOptional(tBoolean), disableAnimations: tOptional(tBoolean),
mask: tOptional(tArray(tObject({
frame: tChannel('Frame'),
selector: tString,
}))),
}); });
scheme.ElementHandleScrollIntoViewIfNeededParams = tObject({ scheme.ElementHandleScrollIntoViewIfNeededParams = tObject({
timeout: tOptional(tNumber), timeout: tOptional(tNumber),

View file

@ -18,6 +18,7 @@ import * as mime from 'mime';
import * as injectedScriptSource from '../generated/injectedScriptSource'; import * as injectedScriptSource from '../generated/injectedScriptSource';
import * as channels from '../protocol/channels'; import * as channels from '../protocol/channels';
import { isSessionClosedError } from './protocolError'; import { isSessionClosedError } from './protocolError';
import { ScreenshotMaskOption } from './screenshotter';
import * as frames from './frames'; import * as frames from './frames';
import type { InjectedScript, InjectedScriptPoll, LogEntry, HitTargetInterceptionResult } from './injected/injectedScript'; import type { InjectedScript, InjectedScriptPoll, LogEntry, HitTargetInterceptionResult } from './injected/injectedScript';
import { CallMetadata } from './instrumentation'; import { CallMetadata } from './instrumentation';
@ -772,7 +773,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
return this._page._delegate.getBoundingBox(this); return this._page._delegate.getBoundingBox(this);
} }
async screenshot(metadata: CallMetadata, options: types.ElementScreenshotOptions = {}): Promise<Buffer> { async screenshot(metadata: CallMetadata, options: types.ElementScreenshotOptions & ScreenshotMaskOption = {}): Promise<Buffer> {
const controller = new ProgressController(metadata, this); const controller = new ProgressController(metadata, this);
return controller.run( return controller.run(
progress => this._page._screenshotter.screenshotElement(progress, this, options), progress => this._page._screenshotter.screenshotElement(progress, this, options),

View file

@ -34,7 +34,7 @@ import { CallMetadata, internalCallMetadata, SdkObject } from './instrumentation
import type InjectedScript from './injected/injectedScript'; import type InjectedScript from './injected/injectedScript';
import type { ElementStateWithoutStable, FrameExpectParams, InjectedScriptPoll, InjectedScriptProgress } from './injected/injectedScript'; import type { ElementStateWithoutStable, FrameExpectParams, InjectedScriptPoll, InjectedScriptProgress } from './injected/injectedScript';
import { isSessionClosedError } from './protocolError'; import { isSessionClosedError } from './protocolError';
import { isInvalidSelectorError, splitSelectorByFrame, stringifySelector } from './common/selectorParser'; import { isInvalidSelectorError, splitSelectorByFrame, stringifySelector, ParsedSelector } from './common/selectorParser';
import { SelectorInfo } from './selectors'; import { SelectorInfo } from './selectors';
type ContextData = { type ContextData = {
@ -786,6 +786,14 @@ export class Frame extends SdkObject {
return result; return result;
} }
async maskSelectors(selectors: ParsedSelector[]): Promise<void> {
const context = await this._utilityContext();
const injectedScript = await context.injectedScript();
await injectedScript.evaluate((injected, { parsed }) => {
injected.maskSelectors(parsed);
}, { parsed: selectors });
}
async querySelectorAll(selector: string): Promise<dom.ElementHandle<Element>[]> { async querySelectorAll(selector: string): Promise<dom.ElementHandle<Element>[]> {
const pair = await this.resolveFrameForSelectorNoWait(selector, {}); const pair = await this.resolveFrameForSelectorNoWait(selector, {});
if (!pair) if (!pair)

View file

@ -175,6 +175,28 @@ export class Highlight {
} }
} }
maskElements(elements: Element[]) {
const boxes = elements.map(e => e.getBoundingClientRect());
const pool = this._highlightElements;
this._highlightElements = [];
for (const box of boxes) {
const highlightElement = pool.length ? pool.shift()! : this._createHighlightElement();
highlightElement.style.backgroundColor = '#F0F';
highlightElement.style.left = box.x + 'px';
highlightElement.style.top = box.y + 'px';
highlightElement.style.width = box.width + 'px';
highlightElement.style.height = box.height + 'px';
highlightElement.style.display = 'block';
this._highlightElements.push(highlightElement);
}
for (const highlightElement of pool) {
highlightElement.style.display = 'none';
this._highlightElements.push(highlightElement);
}
}
private _createHighlightElement(): HTMLElement { private _createHighlightElement(): HTMLElement {
const highlightElement = document.createElement('x-pw-highlight'); const highlightElement = document.createElement('x-pw-highlight');
highlightElement.style.position = 'absolute'; highlightElement.style.position = 'absolute';

View file

@ -873,6 +873,17 @@ export class InjectedScript {
return error; return error;
} }
maskSelectors(selectors: ParsedSelector[]) {
if (this._highlight)
this.hideHighlight();
this._highlight = new Highlight(false);
this._highlight.install();
const elements = [];
for (const selector of selectors)
elements.push(this.querySelectorAll(selector, document.documentElement));
this._highlight.maskElements(elements.flat());
}
highlight(selector: ParsedSelector) { highlight(selector: ParsedSelector) {
if (!this._highlight) { if (!this._highlight) {
this._highlight = new Highlight(false); this._highlight = new Highlight(false);

View file

@ -20,7 +20,7 @@ import * as frames from './frames';
import * as input from './input'; import * as input from './input';
import * as js from './javascript'; import * as js from './javascript';
import * as network from './network'; import * as network from './network';
import { Screenshotter } from './screenshotter'; import { Screenshotter, ScreenshotMaskOption } from './screenshotter';
import { TimeoutSettings } from '../utils/timeoutSettings'; import { TimeoutSettings } from '../utils/timeoutSettings';
import * as types from './types'; import * as types from './types';
import { BrowserContext } from './browserContext'; import { BrowserContext } from './browserContext';
@ -424,7 +424,7 @@ export class Page extends SdkObject {
route.continue(); route.continue();
} }
async screenshot(metadata: CallMetadata, options: types.ScreenshotOptions = {}): Promise<Buffer> { async screenshot(metadata: CallMetadata, options: types.ScreenshotOptions & ScreenshotMaskOption = {}): Promise<Buffer> {
const controller = new ProgressController(metadata, this); const controller = new ProgressController(metadata, this);
return controller.run( return controller.run(
progress => this._screenshotter.screenshotPage(progress, options), progress => this._screenshotter.screenshotPage(progress, options),

View file

@ -18,9 +18,12 @@
import * as dom from './dom'; import * as dom from './dom';
import { helper } from './helper'; import { helper } from './helper';
import { Page } from './page'; import { Page } from './page';
import { Frame } from './frames';
import { ParsedSelector } from './common/selectorParser';
import * as types from './types'; import * as types from './types';
import { Progress } from './progress'; import { Progress } from './progress';
import { assert } from '../utils/utils'; import { assert } from '../utils/utils';
import { MultiMap } from '../utils/multimap';
declare global { declare global {
interface Window { interface Window {
@ -28,6 +31,10 @@ declare global {
} }
} }
export type ScreenshotMaskOption = {
mask?: { frame: Frame, selector: string}[],
};
export class Screenshotter { export class Screenshotter {
private _queue = new TaskQueue(); private _queue = new TaskQueue();
private _page: Page; private _page: Page;
@ -65,7 +72,7 @@ export class Screenshotter {
return fullPageSize!; return fullPageSize!;
} }
async screenshotPage(progress: Progress, options: types.ScreenshotOptions): Promise<Buffer> { async screenshotPage(progress: Progress, options: types.ScreenshotOptions & ScreenshotMaskOption): Promise<Buffer> {
const format = validateScreenshotOptions(options); const format = validateScreenshotOptions(options);
return this._queue.postTask(async () => { return this._queue.postTask(async () => {
const { viewportSize } = await this._originalViewportSize(progress); const { viewportSize } = await this._originalViewportSize(progress);
@ -92,7 +99,7 @@ export class Screenshotter {
}); });
} }
async screenshotElement(progress: Progress, handle: dom.ElementHandle, options: types.ElementScreenshotOptions = {}): Promise<Buffer> { async screenshotElement(progress: Progress, handle: dom.ElementHandle, options: types.ElementScreenshotOptions & ScreenshotMaskOption = {}): Promise<Buffer> {
const format = validateScreenshotOptions(options); const format = validateScreenshotOptions(options);
return this._queue.postTask(async () => { return this._queue.postTask(async () => {
const { viewportSize } = await this._originalViewportSize(progress); const { viewportSize } = await this._originalViewportSize(progress);
@ -210,7 +217,22 @@ export class Screenshotter {
})); }));
} }
private async _screenshot(progress: Progress, format: 'png' | 'jpeg', documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, fitsViewport: boolean | undefined, options: types.ElementScreenshotOptions): Promise<Buffer> { async _maskElements(progress: Progress, options: ScreenshotMaskOption) {
const framesToParsedSelectors: MultiMap<Frame, ParsedSelector> = new MultiMap();
await Promise.all((options.mask || []).map(async ({ frame, selector }) => {
const pair = await frame.resolveFrameForSelectorNoWait(selector);
if (pair)
framesToParsedSelectors.set(pair.frame, pair.info.parsed);
}));
progress.throwIfAborted(); // Avoid extra work.
await Promise.all([...framesToParsedSelectors.keys()].map(async frame => {
await frame.maskSelectors(framesToParsedSelectors.get(frame));
}));
progress.cleanupWhenAborted(() => this._page.hideHighlight());
}
private async _screenshot(progress: Progress, format: 'png' | 'jpeg', documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, fitsViewport: boolean | undefined, options: types.ElementScreenshotOptions & ScreenshotMaskOption): Promise<Buffer> {
if ((options as any).__testHookBeforeScreenshot) if ((options as any).__testHookBeforeScreenshot)
await (options as any).__testHookBeforeScreenshot(); await (options as any).__testHookBeforeScreenshot();
progress.throwIfAborted(); // Screenshotting is expensive - avoid extra work. progress.throwIfAborted(); // Screenshotting is expensive - avoid extra work.
@ -221,8 +243,15 @@ export class Screenshotter {
} }
progress.throwIfAborted(); // Avoid extra work. progress.throwIfAborted(); // Avoid extra work.
await this._maskElements(progress, options);
progress.throwIfAborted(); // Avoid extra work.
const buffer = await this._page._delegate.takeScreenshot(progress, format, documentRect, viewportRect, options.quality, fitsViewport); const buffer = await this._page._delegate.takeScreenshot(progress, format, documentRect, viewportRect, options.quality, fitsViewport);
progress.throwIfAborted(); // Avoid restoring after failure - should be done by cleanup. progress.throwIfAborted(); // Avoid restoring after failure - should be done by cleanup.
await this._page.hideHighlight();
progress.throwIfAborted(); // Avoid restoring after failure - should be done by cleanup.
if (shouldSetDefaultBackground) if (shouldSetDefaultBackground)
await this._page._delegate.setBackgroundColor(); await this._page._delegate.setBackgroundColor();
progress.throwIfAborted(); // Avoid side effects. progress.throwIfAborted(); // Avoid side effects.

View file

@ -8070,6 +8070,12 @@ export interface ElementHandle<T=Node> extends JSHandle<T> {
*/ */
disableAnimations?: boolean; disableAnimations?: boolean;
/**
* Specify locators that should be masked when the screenshot is taken. Masked elements will be overlayed with a pink box
* `#FF00FF` that completely covers its bounding box.
*/
mask?: Array<Locator>;
/** /**
* Hides default white background and allows capturing screenshots with transparency. Not applicable to `jpeg` images. * Hides default white background and allows capturing screenshots with transparency. Not applicable to `jpeg` images.
* Defaults to `false`. * Defaults to `false`.
@ -9373,6 +9379,12 @@ export interface Locator {
*/ */
disableAnimations?: boolean; disableAnimations?: boolean;
/**
* Specify locators that should be masked when the screenshot is taken. Masked elements will be overlayed with a pink box
* `#FF00FF` that completely covers its bounding box.
*/
mask?: Array<Locator>;
/** /**
* Hides default white background and allows capturing screenshots with transparency. Not applicable to `jpeg` images. * Hides default white background and allows capturing screenshots with transparency. Not applicable to `jpeg` images.
* Defaults to `false`. * Defaults to `false`.
@ -15722,6 +15734,12 @@ export interface PageScreenshotOptions {
*/ */
fullPage?: boolean; fullPage?: boolean;
/**
* Specify locators that should be masked when the screenshot is taken. Masked elements will be overlayed with a pink box
* `#FF00FF` that completely covers its bounding box.
*/
mask?: Array<Locator>;
/** /**
* Hides default white background and allows capturing screenshots with transparency. Not applicable to `jpeg` images. * Hides default white background and allows capturing screenshots with transparency. Not applicable to `jpeg` images.
* Defaults to `false`. * Defaults to `false`.

View file

@ -16,7 +16,7 @@
*/ */
import { test as it, expect } from './pageTest'; import { test as it, expect } from './pageTest';
import { verifyViewport } from '../config/utils'; import { verifyViewport, attachFrame } from '../config/utils';
import path from 'path'; import path from 'path';
import fs from 'fs'; import fs from 'fs';
@ -338,6 +338,86 @@ it.describe('page screenshot', () => {
screenshotSeveralTimes() screenshotSeveralTimes()
]); ]);
}); });
it.describe('mask option', () => {
it('should work', async ({ page, server }) => {
await page.setViewportSize({ width: 500, height: 500 });
await page.goto(server.PREFIX + '/grid.html');
expect(await page.screenshot({
mask: [ page.locator('div').nth(5) ],
})).toMatchSnapshot('mask-should-work.png');
});
it('should work with locator', async ({ page, server }) => {
await page.setViewportSize({ width: 500, height: 500 });
await page.goto(server.PREFIX + '/grid.html');
const bodyLocator = page.locator('body');
expect(await bodyLocator.screenshot({
mask: [ page.locator('div').nth(5) ],
})).toMatchSnapshot('mask-should-work-with-locator.png');
});
it('should work with elementhandle', async ({ page, server }) => {
await page.setViewportSize({ width: 500, height: 500 });
await page.goto(server.PREFIX + '/grid.html');
const bodyHandle = await page.$('body');
expect(await bodyHandle.screenshot({
mask: [ page.locator('div').nth(5) ],
})).toMatchSnapshot('mask-should-work-with-elementhandle.png');
});
it('should mask multiple elements', async ({ page, server }) => {
await page.setViewportSize({ width: 500, height: 500 });
await page.goto(server.PREFIX + '/grid.html');
expect(await page.screenshot({
mask: [
page.locator('div').nth(5),
page.locator('div').nth(12),
],
})).toMatchSnapshot('should-mask-multiple-elements.png');
});
it('should mask inside iframe', async ({ page, server }) => {
await page.setViewportSize({ width: 500, height: 500 });
await page.goto(server.PREFIX + '/grid.html');
await attachFrame(page, 'frame1', server.PREFIX + '/grid.html');
await page.addStyleTag({ content: 'iframe { border: none; }' });
expect(await page.screenshot({
mask: [
page.locator('div').nth(5),
page.frameLocator('#frame1').locator('div').nth(12),
],
})).toMatchSnapshot('should-mask-inside-iframe.png');
});
it('should mask in parallel', async ({ page, server }) => {
await page.setViewportSize({ width: 500, height: 500 });
await attachFrame(page, 'frame1', server.PREFIX + '/grid.html');
await attachFrame(page, 'frame2', server.PREFIX + '/grid.html');
await page.addStyleTag({ content: 'iframe { border: none; }' });
const screenshots = await Promise.all([
page.screenshot({
mask: [ page.frameLocator('#frame1').locator('div').nth(1) ],
}),
page.screenshot({
mask: [ page.frameLocator('#frame2').locator('div').nth(3) ],
}),
]);
expect(screenshots[0]).toMatchSnapshot('should-mask-in-parallel-1.png');
expect(screenshots[1]).toMatchSnapshot('should-mask-in-parallel-2.png');
});
it('should remove mask after screenshot', async ({ page, server }) => {
await page.setViewportSize({ width: 500, height: 500 });
await page.goto(server.PREFIX + '/grid.html');
const screenshot1 = await page.screenshot();
await page.screenshot({
mask: [ page.locator('div').nth(1) ],
});
const screenshot2 = await page.screenshot();
expect(screenshot1.equals(screenshot2)).toBe(true);
});
});
}); });
async function rafraf(page) { async function rafraf(page) {
@ -518,6 +598,7 @@ it.describe('page screenshot animations', () => {
await page.goto(server.PREFIX + '/rotate-z.html'); await page.goto(server.PREFIX + '/rotate-z.html');
await page.evaluate(async () => { await page.evaluate(async () => {
window.animation = document.getAnimations()[0]; window.animation = document.getAnimations()[0];
await window.animation.ready;
window.animation.updatePlaybackRate(0); window.animation.updatePlaybackRate(0);
await window.animation.ready; await window.animation.ready;
window.animation.currentTime = 500; window.animation.currentTime = 500;
@ -615,3 +696,4 @@ it.describe('page screenshot animations', () => {
]); ]);
}); });
}); });

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB