mirror of
https://github.com/microsoft/playwright.git
synced 2025-01-05 10:15:12 +03:00
fix(screenshots): simplify implementation, allow fullPage + clip, add tests (#1194)
This commit is contained in:
parent
2ec9e6daa2
commit
1b863c2300
17
docs/api.md
17
docs/api.md
@ -1298,18 +1298,18 @@ await browser.close();
|
||||
#### page.screenshot([options])
|
||||
- `options` <[Object]> Options object which might have the following properties:
|
||||
- `path` <[string]> The file path to save the image to. The screenshot type will be inferred from file extension. If `path` is a relative path, then it is resolved relative to [current working directory](https://nodejs.org/api/process.html#process_process_cwd). If no path is provided, the image won't be saved to the disk.
|
||||
- `type` <"png"|"jpeg"> Specify screenshot type, defaults to 'png'.
|
||||
- `type` <"png"|"jpeg"> Specify screenshot type, defaults to `png`.
|
||||
- `quality` <[number]> The quality of the image, between 0-100. Not applicable to `png` images.
|
||||
- `fullPage` <[boolean]> When true, takes a screenshot of the full scrollable page. Defaults to `false`.
|
||||
- `clip` <[Object]> An object which specifies clipping region of the page. Should have the following fields:
|
||||
- `fullPage` <[boolean]> When true, takes a screenshot of the full scrollable page, instead of the currently visibvle viewport. Defaults to `false`.
|
||||
- `clip` <[Object]> An object which specifies clipping of the resulting image. Should have the following fields:
|
||||
- `x` <[number]> x-coordinate of top-left corner of clip area
|
||||
- `y` <[number]> y-coordinate of top-left corner of clip area
|
||||
- `width` <[number]> width of clipping area
|
||||
- `height` <[number]> height of clipping area
|
||||
- `omitBackground` <[boolean]> Hides default white background and allows capturing screenshots with transparency. Defaults to `false`.
|
||||
- `omitBackground` <[boolean]> Hides default white background and allows capturing screenshots with transparency. Not applicable to `jpeg` images. Defaults to `false`.
|
||||
- returns: <[Promise]<[Buffer]>> Promise which resolves to buffer with the captured screenshot.
|
||||
|
||||
> **NOTE** Screenshots take at least 1/6 second on OS X. See https://crbug.com/741689 for discussion.
|
||||
> **NOTE** Screenshots take at least 1/6 second on Chromium OS X and Chromium Windows. See https://crbug.com/741689 for discussion.
|
||||
|
||||
#### page.select(selector, value, options)
|
||||
- `selector` <[string]> A selector to query frame for.
|
||||
@ -2483,13 +2483,12 @@ If `key` is a single character and no modifier keys besides `Shift` are being he
|
||||
#### elementHandle.screenshot([options])
|
||||
- `options` <[Object]> Screenshot options.
|
||||
- `path` <[string]> The file path to save the image to. The screenshot type will be inferred from file extension. If `path` is a relative path, then it is resolved relative to [current working directory](https://nodejs.org/api/process.html#process_process_cwd). If no path is provided, the image won't be saved to the disk.
|
||||
- `type` <"png"|"jpeg"> Specify screenshot type, defaults to 'png'.
|
||||
- `type` <"png"|"jpeg"> Specify screenshot type, defaults to `png`.
|
||||
- `quality` <[number]> The quality of the image, between 0-100. Not applicable to `png` images.
|
||||
- `omitBackground` <[boolean]> Hides default white background and allows capturing screenshots with transparency. Defaults to `false`.
|
||||
- `omitBackground` <[boolean]> Hides default white background and allows capturing screenshots with transparency. Not applicable to `jpeg` images. Defaults to `false`.
|
||||
- returns: <[Promise]<|[Buffer]>> Promise which resolves to buffer with the captured screenshot.
|
||||
|
||||
This method scrolls element into view if needed, and then uses [page.screenshot](#pagescreenshotoptions) to take a screenshot of the element.
|
||||
If the element is detached from DOM, the method throws an error.
|
||||
This method scrolls element into view if needed before taking a screenshot. If the element is detached from DOM, the method throws an error.
|
||||
|
||||
#### elementHandle.scrollIntoViewIfNeeded()
|
||||
- returns: <[Promise]> Resolves after the element has been scrolled into view.
|
||||
|
@ -417,16 +417,6 @@ export class CRPage implements PageDelegate {
|
||||
await this._browser._closePage(this._page);
|
||||
}
|
||||
|
||||
async getBoundingBoxForScreenshot(handle: dom.ElementHandle<Node>): Promise<types.Rect | null> {
|
||||
const rect = await handle.boundingBox();
|
||||
if (!rect)
|
||||
return rect;
|
||||
const { layoutViewport: { pageX, pageY } } = await this._client.send('Page.getLayoutMetrics');
|
||||
rect.x += pageX;
|
||||
rect.y += pageY;
|
||||
return rect;
|
||||
}
|
||||
|
||||
canScreenshotOutsideViewport(): boolean {
|
||||
return false;
|
||||
}
|
||||
@ -435,10 +425,23 @@ export class CRPage implements PageDelegate {
|
||||
await this._client.send('Emulation.setDefaultBackgroundColorOverride', { color });
|
||||
}
|
||||
|
||||
async takeScreenshot(format: 'png' | 'jpeg', options: types.ScreenshotOptions, viewportSize: types.Size): Promise<platform.BufferType> {
|
||||
async takeScreenshot(format: 'png' | 'jpeg', documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined): Promise<platform.BufferType> {
|
||||
const { visualViewport } = await this._client.send('Page.getLayoutMetrics');
|
||||
if (!documentRect) {
|
||||
documentRect = {
|
||||
x: visualViewport.pageX + viewportRect!.x,
|
||||
y: visualViewport.pageY + viewportRect!.y,
|
||||
...helper.enclosingIntSize({
|
||||
width: viewportRect!.width / visualViewport.scale,
|
||||
height: viewportRect!.height / visualViewport.scale,
|
||||
})
|
||||
};
|
||||
}
|
||||
await this._client.send('Page.bringToFront', {});
|
||||
const clip = options.clip ? { ...options.clip, scale: 1 } : undefined;
|
||||
const result = await this._client.send('Page.captureScreenshot', { format, quality: options.quality, clip });
|
||||
// When taking screenshots with documentRect (based on the page content, not viewport),
|
||||
// ignore current page scale.
|
||||
const clip = { ...documentRect, scale: viewportRect ? visualViewport.scale : 1 };
|
||||
const result = await this._client.send('Page.captureScreenshot', { format, quality, clip });
|
||||
return platform.Buffer.from(result.data, 'base64');
|
||||
}
|
||||
|
||||
|
@ -317,15 +317,6 @@ export class FFPage implements PageDelegate {
|
||||
await this._session.send('Page.close', { runBeforeUnload });
|
||||
}
|
||||
|
||||
async getBoundingBoxForScreenshot(handle: dom.ElementHandle<Node>): Promise<types.Rect | null> {
|
||||
const frameId = handle._context.frame._id;
|
||||
const response = await this._session.send('Page.getBoundingBox', {
|
||||
frameId,
|
||||
objectId: handle._remoteObject.objectId,
|
||||
});
|
||||
return response.boundingBox;
|
||||
}
|
||||
|
||||
canScreenshotOutsideViewport(): boolean {
|
||||
return true;
|
||||
}
|
||||
@ -335,11 +326,22 @@ export class FFPage implements PageDelegate {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
async takeScreenshot(format: 'png' | 'jpeg', options: types.ScreenshotOptions, viewportSize: types.Size): Promise<platform.BufferType> {
|
||||
async takeScreenshot(format: 'png' | 'jpeg', documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined): Promise<platform.BufferType> {
|
||||
if (!documentRect) {
|
||||
const context = await this._page.mainFrame()._utilityContext();
|
||||
const scrollOffset = await context.evaluate(() => ({ x: window.scrollX, y: window.scrollY }));
|
||||
documentRect = {
|
||||
x: viewportRect!.x + scrollOffset.x,
|
||||
y: viewportRect!.y + scrollOffset.y,
|
||||
width: viewportRect!.width,
|
||||
height: viewportRect!.height,
|
||||
};
|
||||
}
|
||||
// TODO: remove fullPage option from Page.screenshot.
|
||||
// TODO: remove Page.getBoundingBox method.
|
||||
const { data } = await this._session.send('Page.screenshot', {
|
||||
mimeType: ('image/' + format) as ('image/png' | 'image/jpeg'),
|
||||
fullPage: options.fullPage,
|
||||
clip: options.clip,
|
||||
clip: documentRect,
|
||||
}).catch(e => {
|
||||
if (e instanceof Error && e.message.includes('document.documentElement is null'))
|
||||
e.message = kScreenshotDuringNavigationError;
|
||||
@ -349,7 +351,7 @@ export class FFPage implements PageDelegate {
|
||||
}
|
||||
|
||||
async resetViewport(): Promise<void> {
|
||||
await this._session.send('Page.setViewportSize', { viewportSize: null });
|
||||
assert(false, 'Should not be called');
|
||||
}
|
||||
|
||||
async getContentFrame(handle: dom.ElementHandle): Promise<frames.Frame | null> {
|
||||
|
@ -17,6 +17,7 @@
|
||||
|
||||
import { TimeoutError } from './errors';
|
||||
import * as platform from './platform';
|
||||
import * as types from './types';
|
||||
|
||||
export const debugError = platform.debug(`pw:error`);
|
||||
|
||||
@ -266,6 +267,18 @@ class Helper {
|
||||
const rightHalf = maxLength - leftHalf - 1;
|
||||
return string.substr(0, leftHalf) + '\u2026' + string.substr(this.length - rightHalf, rightHalf);
|
||||
}
|
||||
|
||||
static enclosingIntRect(rect: types.Rect): types.Rect {
|
||||
const x = Math.floor(rect.x + 1e-3);
|
||||
const y = Math.floor(rect.y + 1e-3);
|
||||
const x2 = Math.ceil(rect.x + rect.width - 1e-3);
|
||||
const y2 = Math.ceil(rect.y + rect.height - 1e-3);
|
||||
return { x, y, width: x2 - x, height: y2 - y };
|
||||
}
|
||||
|
||||
static enclosingIntSize(size: types.Size): types.Size {
|
||||
return { width: Math.floor(size.width + 1e-3), height: Math.floor(size.height + 1e-3) };
|
||||
}
|
||||
}
|
||||
|
||||
export function assert(value: any, message?: string): asserts value {
|
||||
|
@ -54,11 +54,10 @@ export interface PageDelegate {
|
||||
authenticate(credentials: types.Credentials | null): Promise<void>;
|
||||
setFileChooserIntercepted(enabled: boolean): Promise<void>;
|
||||
|
||||
getBoundingBoxForScreenshot(handle: dom.ElementHandle<Node>): Promise<types.Rect | null>;
|
||||
canScreenshotOutsideViewport(): boolean;
|
||||
resetViewport(): Promise<void>; // Only called if canScreenshotOutsideViewport() returns false.
|
||||
setBackgroundColor(color?: { r: number; g: number; b: number; a: number; }): Promise<void>;
|
||||
takeScreenshot(format: string, options: types.ScreenshotOptions, viewportSize: types.Size): Promise<platform.BufferType>;
|
||||
resetViewport(oldSize: types.Size): Promise<void>;
|
||||
takeScreenshot(format: string, documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined): Promise<platform.BufferType>;
|
||||
|
||||
isElementHandle(remoteObject: any): boolean;
|
||||
adoptElementHandle<T extends Node>(handle: dom.ElementHandle<T>, to: dom.FrameExecutionContext): Promise<dom.ElementHandle<T>>;
|
||||
|
@ -234,9 +234,9 @@ export function urlMatches(urlString: string, match: types.URLMatch | undefined)
|
||||
return match(url);
|
||||
}
|
||||
|
||||
export function pngToJpeg(buffer: Buffer): Buffer {
|
||||
export function pngToJpeg(buffer: Buffer, quality?: number): Buffer {
|
||||
assert(isNode, 'Converting from png to jpeg is only supported in Node.js');
|
||||
return jpeg.encode(png.PNG.sync.read(buffer)).data;
|
||||
return jpeg.encode(png.PNG.sync.read(buffer), quality).data;
|
||||
}
|
||||
|
||||
function nodeFetch(url: string): Promise<string> {
|
||||
|
@ -16,7 +16,7 @@
|
||||
*/
|
||||
|
||||
import * as dom from './dom';
|
||||
import { assert } from './helper';
|
||||
import { assert, helper } from './helper';
|
||||
import * as types from './types';
|
||||
import { Page } from './page';
|
||||
import * as platform from './platform';
|
||||
@ -40,108 +40,120 @@ export class Screenshotter {
|
||||
const originalViewportSize = this._page.viewportSize();
|
||||
let viewportSize = originalViewportSize;
|
||||
if (!viewportSize) {
|
||||
const maybeViewportSize = await this._page.evaluate(() => {
|
||||
const context = await this._page.mainFrame()._utilityContext();
|
||||
viewportSize = await context.evaluate(() => {
|
||||
if (!document.body || !document.documentElement)
|
||||
return;
|
||||
return null;
|
||||
return {
|
||||
width: Math.max(document.body.offsetWidth, document.documentElement.offsetWidth),
|
||||
height: Math.max(document.body.offsetHeight, document.documentElement.offsetHeight),
|
||||
};
|
||||
});
|
||||
if (!maybeViewportSize)
|
||||
if (!viewportSize)
|
||||
throw new Error(kScreenshotDuringNavigationError);
|
||||
viewportSize = maybeViewportSize;
|
||||
}
|
||||
return { viewportSize, originalViewportSize };
|
||||
}
|
||||
|
||||
private async _fullPageSize(): Promise<types.Size> {
|
||||
const context = await this._page.mainFrame()._utilityContext();
|
||||
const fullPageSize = await context.evaluate(() => {
|
||||
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
|
||||
),
|
||||
};
|
||||
});
|
||||
if (!fullPageSize)
|
||||
throw new Error(kScreenshotDuringNavigationError);
|
||||
return fullPageSize;
|
||||
}
|
||||
|
||||
async screenshotPage(options: types.ScreenshotOptions = {}): Promise<platform.BufferType> {
|
||||
const format = validateScreeshotOptions(options);
|
||||
return this._queue.postTask(async () => {
|
||||
const { viewportSize, originalViewportSize } = await this._originalViewportSize();
|
||||
let overridenViewportSize: types.Size | null = null;
|
||||
if (options.fullPage && !this._page._delegate.canScreenshotOutsideViewport()) {
|
||||
const fullPageRect = await this._page.evaluate(() => {
|
||||
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
|
||||
),
|
||||
};
|
||||
});
|
||||
if (!fullPageRect)
|
||||
throw new Error(kScreenshotDuringNavigationError);
|
||||
overridenViewportSize = fullPageRect;
|
||||
await this._page.setViewportSize(overridenViewportSize);
|
||||
} else if (options.clip) {
|
||||
options.clip = trimClipToViewport(viewportSize, options.clip);
|
||||
|
||||
if (options.fullPage) {
|
||||
const fullPageSize = await this._fullPageSize();
|
||||
let documentRect = { x: 0, y: 0, width: fullPageSize.width, height: fullPageSize.height };
|
||||
let overridenViewportSize: types.Size | null = null;
|
||||
const fitsViewport = fullPageSize.width <= viewportSize.width && fullPageSize.height <= viewportSize.height;
|
||||
if (!this._page._delegate.canScreenshotOutsideViewport() && !fitsViewport) {
|
||||
overridenViewportSize = fullPageSize;
|
||||
await this._page.setViewportSize(overridenViewportSize);
|
||||
}
|
||||
if (options.clip)
|
||||
documentRect = trimClipToSize(options.clip, documentRect);
|
||||
return await this._screenshot(format, documentRect, undefined, options, overridenViewportSize, originalViewportSize);
|
||||
}
|
||||
|
||||
return await this._screenshot(format, options, viewportSize, overridenViewportSize, originalViewportSize);
|
||||
const viewportRect = options.clip ? trimClipToSize(options.clip, viewportSize) : { x: 0, y: 0, ...viewportSize };
|
||||
return await this._screenshot(format, undefined, viewportRect, options, null, originalViewportSize);
|
||||
}).catch(rewriteError);
|
||||
}
|
||||
|
||||
async screenshotElement(handle: dom.ElementHandle, options: types.ElementScreenshotOptions = {}): Promise<platform.BufferType> {
|
||||
const format = validateScreeshotOptions(options);
|
||||
const rewrittenOptions: types.ScreenshotOptions = { ...options };
|
||||
return this._queue.postTask(async () => {
|
||||
let maybeBoundingBox = await this._page._delegate.getBoundingBoxForScreenshot(handle);
|
||||
assert(maybeBoundingBox, 'Node is either not visible or not an HTMLElement');
|
||||
let boundingBox = maybeBoundingBox;
|
||||
assert(boundingBox.width !== 0, 'Node has 0 width.');
|
||||
assert(boundingBox.height !== 0, 'Node has 0 height.');
|
||||
boundingBox = enclosingIntRect(boundingBox);
|
||||
|
||||
const { viewportSize, originalViewportSize } = await this._originalViewportSize();
|
||||
|
||||
await handle.scrollIntoViewIfNeeded();
|
||||
let 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.');
|
||||
|
||||
let overridenViewportSize: types.Size | null = null;
|
||||
if (!this._page._delegate.canScreenshotOutsideViewport()) {
|
||||
if (boundingBox.width > viewportSize.width || boundingBox.height > viewportSize.height) {
|
||||
overridenViewportSize = {
|
||||
width: Math.max(viewportSize.width, boundingBox.width),
|
||||
height: Math.max(viewportSize.height, boundingBox.height),
|
||||
};
|
||||
await this._page.setViewportSize(overridenViewportSize);
|
||||
}
|
||||
const fitsViewport = boundingBox.width <= viewportSize.width && boundingBox.height <= viewportSize.height;
|
||||
if (!this._page._delegate.canScreenshotOutsideViewport() && !fitsViewport) {
|
||||
overridenViewportSize = helper.enclosingIntSize({
|
||||
width: Math.max(viewportSize.width, boundingBox.width),
|
||||
height: Math.max(viewportSize.height, boundingBox.height),
|
||||
});
|
||||
await this._page.setViewportSize(overridenViewportSize);
|
||||
|
||||
await handle.scrollIntoViewIfNeeded();
|
||||
maybeBoundingBox = await this._page._delegate.getBoundingBoxForScreenshot(handle);
|
||||
assert(maybeBoundingBox, 'Node is either not visible or not an HTMLElement');
|
||||
boundingBox = enclosingIntRect(maybeBoundingBox);
|
||||
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.');
|
||||
}
|
||||
|
||||
if (!overridenViewportSize)
|
||||
rewrittenOptions.clip = boundingBox;
|
||||
|
||||
return await this._screenshot(format, rewrittenOptions, viewportSize, overridenViewportSize, originalViewportSize);
|
||||
const context = await this._page.mainFrame()._utilityContext();
|
||||
const scrollOffset = await context.evaluate(() => ({ x: window.scrollX, y: window.scrollY }));
|
||||
const documentRect = { ...boundingBox };
|
||||
documentRect.x += scrollOffset.x;
|
||||
documentRect.y += scrollOffset.y;
|
||||
return await this._screenshot(format, helper.enclosingIntRect(documentRect), undefined, options, overridenViewportSize, originalViewportSize);
|
||||
}).catch(rewriteError);
|
||||
}
|
||||
|
||||
private async _screenshot(format: 'png' | 'jpeg', options: types.ScreenshotOptions, viewportSize: types.Size, overridenViewportSize: types.Size | null, originalViewportSize: types.Size | null): Promise<platform.BufferType> {
|
||||
private async _screenshot(format: 'png' | 'jpeg', documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, options: types.ElementScreenshotOptions, overridenViewportSize: types.Size | null, originalViewportSize: types.Size | null): Promise<platform.BufferType> {
|
||||
const shouldSetDefaultBackground = options.omitBackground && format === 'png';
|
||||
if (shouldSetDefaultBackground)
|
||||
await this._page._delegate.setBackgroundColor({ r: 0, g: 0, b: 0, a: 0});
|
||||
const buffer = await this._page._delegate.takeScreenshot(format, options, overridenViewportSize || viewportSize);
|
||||
const buffer = await this._page._delegate.takeScreenshot(format, documentRect, viewportRect, options.quality);
|
||||
if (shouldSetDefaultBackground)
|
||||
await this._page._delegate.setBackgroundColor();
|
||||
if (options.path)
|
||||
await platform.writeFileAsync(options.path, buffer);
|
||||
|
||||
if (overridenViewportSize) {
|
||||
assert(!this._page._delegate.canScreenshotOutsideViewport());
|
||||
if (originalViewportSize)
|
||||
await this._page.setViewportSize(originalViewportSize);
|
||||
else
|
||||
await this._page._delegate.resetViewport(viewportSize);
|
||||
await this._page._delegate.resetViewport();
|
||||
}
|
||||
|
||||
if (options.path)
|
||||
await platform.writeFileAsync(options.path, buffer);
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
@ -162,13 +174,17 @@ class TaskQueue {
|
||||
}
|
||||
}
|
||||
|
||||
function trimClipToViewport(viewportSize: types.Size, clip: types.Rect | undefined): types.Rect | undefined {
|
||||
if (!clip)
|
||||
return clip;
|
||||
const p1 = { x: Math.min(clip.x, viewportSize.width), y: Math.min(clip.y, viewportSize.height) };
|
||||
const p2 = { x: Math.min(clip.x + clip.width, viewportSize.width), y: Math.min(clip.y + clip.height, viewportSize.height) };
|
||||
function trimClipToSize(clip: types.Rect, size: types.Size): types.Rect {
|
||||
const p1 = {
|
||||
x: Math.max(0, Math.min(clip.x, size.width)),
|
||||
y: Math.max(0, Math.min(clip.y, size.height))
|
||||
};
|
||||
const p2 = {
|
||||
x: Math.max(0, Math.min(clip.x + clip.width, size.width)),
|
||||
y: Math.max(0, Math.min(clip.y + clip.height, size.height))
|
||||
};
|
||||
const result = { x: p1.x, y: p1.y, width: p2.x - p1.x, height: p2.y - p1.y };
|
||||
assert(result.width && result.height, 'Clipped area is either empty or outside the viewport');
|
||||
assert(result.width && result.height, 'Clipped area is either empty or outside the resulting image');
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -197,7 +213,6 @@ function validateScreeshotOptions(options: types.ScreenshotOptions): 'png' | 'jp
|
||||
assert(Number.isInteger(options.quality), 'Expected options.quality to be an integer');
|
||||
assert(options.quality >= 0 && options.quality <= 100, 'Expected options.quality to be between 0 and 100 (inclusive), got ' + options.quality);
|
||||
}
|
||||
assert(!options.clip || !options.fullPage, 'options.clip and options.fullPage are exclusive');
|
||||
if (options.clip) {
|
||||
assert(typeof options.clip.x === 'number', 'Expected options.clip.x to be a number but found ' + (typeof options.clip.x));
|
||||
assert(typeof options.clip.y === 'number', 'Expected options.clip.y to be a number but found ' + (typeof options.clip.y));
|
||||
@ -209,14 +224,6 @@ function validateScreeshotOptions(options: types.ScreenshotOptions): 'png' | 'jp
|
||||
return format;
|
||||
}
|
||||
|
||||
function enclosingIntRect(rect: types.Rect): types.Rect {
|
||||
const x = Math.floor(rect.x + 1e-3);
|
||||
const y = Math.floor(rect.y + 1e-3);
|
||||
const x2 = Math.ceil(rect.x + rect.width - 1e-3);
|
||||
const y2 = Math.ceil(rect.y + rect.height - 1e-3);
|
||||
return { x, y, width: x2 - x, height: y2 - y };
|
||||
}
|
||||
|
||||
export const kScreenshotDuringNavigationError = 'Cannot take a screenshot while page is navigating';
|
||||
function rewriteError(e: any) {
|
||||
if (typeof e === 'object' && e instanceof Error && e.message.includes('Execution context was destroyed'))
|
||||
|
@ -488,12 +488,8 @@ export class WKPage implements PageDelegate {
|
||||
}).catch(debugError);
|
||||
}
|
||||
|
||||
getBoundingBoxForScreenshot(handle: dom.ElementHandle<Node>): Promise<types.Rect | null> {
|
||||
return handle.boundingBox();
|
||||
}
|
||||
|
||||
canScreenshotOutsideViewport(): boolean {
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
async setBackgroundColor(color?: { r: number; g: number; b: number; a: number; }): Promise<void> {
|
||||
@ -501,18 +497,20 @@ export class WKPage implements PageDelegate {
|
||||
await this._session.send('Page.setDefaultBackgroundColorOverride', { color });
|
||||
}
|
||||
|
||||
async takeScreenshot(format: string, options: types.ScreenshotOptions, viewportSize: types.Size): Promise<platform.BufferType> {
|
||||
const rect = options.clip || { x: 0, y: 0, width: viewportSize.width, height: viewportSize.height };
|
||||
const result = await this._session.send('Page.snapshotRect', { ...rect, coordinateSystem: options.fullPage ? 'Page' : 'Viewport' });
|
||||
async takeScreenshot(format: string, documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined): Promise<platform.BufferType> {
|
||||
// TODO: documentRect does not include pageScale, while backend considers it does.
|
||||
// This brakes mobile screenshots of elements or full page.
|
||||
const rect = (documentRect || viewportRect)!;
|
||||
const result = await this._session.send('Page.snapshotRect', { ...rect, coordinateSystem: documentRect ? 'Page' : 'Viewport' });
|
||||
const prefix = 'data:image/png;base64,';
|
||||
let buffer = platform.Buffer.from(result.dataURL.substr(prefix.length), 'base64');
|
||||
if (format === 'jpeg')
|
||||
buffer = platform.pngToJpeg(buffer);
|
||||
buffer = platform.pngToJpeg(buffer, quality);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
async resetViewport(oldSize: types.Size): Promise<void> {
|
||||
await this._pageProxySession.send('Emulation.setDeviceMetricsOverride', { ...oldSize, fixedLayout: false, deviceScaleFactor: 0 });
|
||||
async resetViewport(): Promise<void> {
|
||||
assert(false, 'Should not be called');
|
||||
}
|
||||
|
||||
async getContentFrame(handle: dom.ElementHandle): Promise<frames.Frame | null> {
|
||||
|
22
test/assets/overflow-large.html
Normal file
22
test/assets/overflow-large.html
Normal file
@ -0,0 +1,22 @@
|
||||
<style>
|
||||
body { margin: 0; padding: 0; }
|
||||
|
||||
div {
|
||||
display: inline-flex;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-right: 1px solid black;
|
||||
border-bottom: 1px solid black;
|
||||
}
|
||||
</style>
|
||||
|
||||
<body></body>
|
||||
|
||||
<script>
|
||||
const colors = ['#222', '#444', '#666', '#888', '#aaa'];
|
||||
for (let i = 0; i < 1000; ++i) {
|
||||
const div = document.createElement('div');
|
||||
div.style.setProperty('background-color', colors[i % 5]);
|
||||
document.body.appendChild(div);
|
||||
}
|
||||
</script>
|
BIN
test/golden-chromium/screenshot-element-mobile.png
Normal file
BIN
test/golden-chromium/screenshot-element-mobile.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 474 B |
BIN
test/golden-chromium/screenshot-mobile-clip.png
Normal file
BIN
test/golden-chromium/screenshot-mobile-clip.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
BIN
test/golden-chromium/screenshot-mobile-fullpage.png
Normal file
BIN
test/golden-chromium/screenshot-mobile-fullpage.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
BIN
test/golden-webkit/screenshot-mobile-clip.png
Normal file
BIN
test/golden-webkit/screenshot-mobile-clip.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.0 KiB |
@ -43,6 +43,21 @@ module.exports.describe = function({testRunner, expect, product, FFOX, CHROMIUM,
|
||||
});
|
||||
expect(screenshot).toBeGolden('screenshot-clip-rect.png');
|
||||
});
|
||||
it('should clip rect with fullPage', async({page, server}) => {
|
||||
await page.setViewportSize({width: 500, height: 500});
|
||||
await page.goto(server.PREFIX + '/grid.html');
|
||||
await page.evaluate(() => window.scrollBy(150, 200));
|
||||
const screenshot = await page.screenshot({
|
||||
fullPage: true,
|
||||
clip: {
|
||||
x: 50,
|
||||
y: 100,
|
||||
width: 150,
|
||||
height: 100,
|
||||
},
|
||||
});
|
||||
expect(screenshot).toBeGolden('screenshot-clip-rect.png');
|
||||
});
|
||||
it('should clip elements to the viewport', async({page, server}) => {
|
||||
await page.setViewportSize({width: 500, height: 500});
|
||||
await page.goto(server.PREFIX + '/grid.html');
|
||||
@ -67,7 +82,7 @@ module.exports.describe = function({testRunner, expect, product, FFOX, CHROMIUM,
|
||||
height: 100
|
||||
}
|
||||
}).catch(error => error);
|
||||
expect(screenshotError.message).toBe('Clipped area is either empty or outside the viewport');
|
||||
expect(screenshotError.message).toBe('Clipped area is either empty or outside the resulting image');
|
||||
});
|
||||
it('should run in parallel', async({page, server}) => {
|
||||
await page.setViewportSize({width: 500, height: 500});
|
||||
@ -164,6 +179,22 @@ module.exports.describe = function({testRunner, expect, product, FFOX, CHROMIUM,
|
||||
expect(screenshot).toBeGolden('screenshot-mobile.png');
|
||||
await context.close();
|
||||
});
|
||||
it.skip(FFOX)('should work with a mobile viewport and clip', async({browser, server}) => {
|
||||
const context = await browser.newContext({viewport: { width: 320, height: 480, isMobile: true }});
|
||||
const page = await context.newPage();
|
||||
await page.goto(server.PREFIX + '/overflow.html');
|
||||
const screenshot = await page.screenshot({ clip: { x: 10, y: 10, width: 100, height: 150 } });
|
||||
expect(screenshot).toBeGolden('screenshot-mobile-clip.png');
|
||||
await context.close();
|
||||
});
|
||||
it.skip(FFOX).fail(WEBKIT)('should work with a mobile viewport and fullPage', async({browser, server}) => {
|
||||
const context = await browser.newContext({viewport: { width: 320, height: 480, isMobile: true }});
|
||||
const page = await context.newPage();
|
||||
await page.goto(server.PREFIX + '/overflow-large.html');
|
||||
const screenshot = await page.screenshot({ fullPage: true });
|
||||
expect(screenshot).toBeGolden('screenshot-mobile-fullpage.png');
|
||||
await context.close();
|
||||
});
|
||||
it('should work for canvas', async({page, server}) => {
|
||||
await page.setViewportSize({width: 500, height: 500});
|
||||
await page.goto(server.PREFIX + '/screenshots/canvas.html');
|
||||
@ -339,7 +370,7 @@ module.exports.describe = function({testRunner, expect, product, FFOX, CHROMIUM,
|
||||
const elementHandle = await page.$('h1');
|
||||
await page.evaluate(element => element.remove(), elementHandle);
|
||||
const screenshotError = await elementHandle.screenshot().catch(error => error);
|
||||
expect(screenshotError.message).toBe('Node is either not visible or not an HTMLElement');
|
||||
expect(screenshotError.message).toContain('Node is detached');
|
||||
});
|
||||
it('should not hang with zero width/height element', async({page, server}) => {
|
||||
await page.setContent('<div style="width: 50px; height: 0"></div>');
|
||||
@ -353,6 +384,16 @@ module.exports.describe = function({testRunner, expect, product, FFOX, CHROMIUM,
|
||||
const screenshot = await elementHandle.screenshot();
|
||||
expect(screenshot).toBeGolden('screenshot-element-fractional.png');
|
||||
});
|
||||
it.skip(FFOX).fail(WEBKIT)('should work with a mobile viewport', async({browser, server}) => {
|
||||
const context = await browser.newContext({viewport: { width: 320, height: 480, isMobile: true }});
|
||||
const page = await context.newPage();
|
||||
await page.goto(server.PREFIX + '/grid.html');
|
||||
await page.evaluate(() => window.scrollBy(50, 100));
|
||||
const elementHandle = await page.$('.box:nth-of-type(3)');
|
||||
const screenshot = await elementHandle.screenshot();
|
||||
expect(screenshot).toBeGolden('screenshot-element-mobile.png');
|
||||
await context.close();
|
||||
});
|
||||
it('should work for an element with an offset', async({page}) => {
|
||||
await page.setContent('<div style="position:absolute; top: 10.3px; left: 20.4px;width:50.3px;height:20.2px;border:1px solid black;"></div>');
|
||||
const elementHandle = await page.$('div');
|
||||
|
@ -87,7 +87,7 @@ if (process.env.BROWSER === 'firefox') {
|
||||
...require('../lib/events').Events,
|
||||
...require('../lib/chromium/events').Events,
|
||||
};
|
||||
missingCoverage = ['browserContext.setGeolocation', 'elementHandle.scrollIntoViewIfNeeded', 'page.setOfflineMode'];
|
||||
missingCoverage = ['browserContext.setGeolocation', 'page.setOfflineMode'];
|
||||
} else if (process.env.BROWSER === 'webkit') {
|
||||
product = 'WebKit';
|
||||
events = require('../lib/events').Events;
|
||||
|
Loading…
Reference in New Issue
Block a user