From 1b863c2300f6ad578190007c20f1779bb80092b9 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Tue, 3 Mar 2020 16:09:32 -0800 Subject: [PATCH] fix(screenshots): simplify implementation, allow fullPage + clip, add tests (#1194) --- docs/api.md | 17 +- src/chromium/crPage.ts | 29 ++-- src/firefox/ffPage.ts | 28 ++-- src/helper.ts | 13 ++ src/page.ts | 5 +- src/platform.ts | 4 +- src/screenshotter.ts | 157 +++++++++--------- src/webkit/wkPage.ts | 20 +-- test/assets/overflow-large.html | 22 +++ .../screenshot-element-mobile.png | Bin 0 -> 474 bytes .../screenshot-mobile-clip.png | Bin 0 -> 1660 bytes .../screenshot-mobile-fullpage.png | Bin 0 -> 20221 bytes test/golden-webkit/screenshot-mobile-clip.png | Bin 0 -> 2090 bytes test/screenshot.spec.js | 45 ++++- test/test.js | 2 +- 15 files changed, 213 insertions(+), 129 deletions(-) create mode 100644 test/assets/overflow-large.html create mode 100644 test/golden-chromium/screenshot-element-mobile.png create mode 100644 test/golden-chromium/screenshot-mobile-clip.png create mode 100644 test/golden-chromium/screenshot-mobile-fullpage.png create mode 100644 test/golden-webkit/screenshot-mobile-clip.png diff --git a/docs/api.md b/docs/api.md index 33dcf9434d..c0f722f62a 100644 --- a/docs/api.md +++ b/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. diff --git a/src/chromium/crPage.ts b/src/chromium/crPage.ts index ed0a863316..fd70da0338 100644 --- a/src/chromium/crPage.ts +++ b/src/chromium/crPage.ts @@ -417,16 +417,6 @@ export class CRPage implements PageDelegate { await this._browser._closePage(this._page); } - async getBoundingBoxForScreenshot(handle: dom.ElementHandle): Promise { - 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 { + async takeScreenshot(format: 'png' | 'jpeg', documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined): Promise { + 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'); } diff --git a/src/firefox/ffPage.ts b/src/firefox/ffPage.ts index dcbfc71a3f..ad5d60ba3a 100644 --- a/src/firefox/ffPage.ts +++ b/src/firefox/ffPage.ts @@ -317,15 +317,6 @@ export class FFPage implements PageDelegate { await this._session.send('Page.close', { runBeforeUnload }); } - async getBoundingBoxForScreenshot(handle: dom.ElementHandle): Promise { - 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 { + async takeScreenshot(format: 'png' | 'jpeg', documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined): Promise { + 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 { - await this._session.send('Page.setViewportSize', { viewportSize: null }); + assert(false, 'Should not be called'); } async getContentFrame(handle: dom.ElementHandle): Promise { diff --git a/src/helper.ts b/src/helper.ts index e02b4be845..c4c7b25990 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -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 { diff --git a/src/page.ts b/src/page.ts index afce416493..e664b0d578 100644 --- a/src/page.ts +++ b/src/page.ts @@ -54,11 +54,10 @@ export interface PageDelegate { authenticate(credentials: types.Credentials | null): Promise; setFileChooserIntercepted(enabled: boolean): Promise; - getBoundingBoxForScreenshot(handle: dom.ElementHandle): Promise; canScreenshotOutsideViewport(): boolean; + resetViewport(): Promise; // Only called if canScreenshotOutsideViewport() returns false. setBackgroundColor(color?: { r: number; g: number; b: number; a: number; }): Promise; - takeScreenshot(format: string, options: types.ScreenshotOptions, viewportSize: types.Size): Promise; - resetViewport(oldSize: types.Size): Promise; + takeScreenshot(format: string, documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined): Promise; isElementHandle(remoteObject: any): boolean; adoptElementHandle(handle: dom.ElementHandle, to: dom.FrameExecutionContext): Promise>; diff --git a/src/platform.ts b/src/platform.ts index ae6c2574ad..97200f0db9 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -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 { diff --git a/src/screenshotter.ts b/src/screenshotter.ts index 772af2f7cc..dcec2d5626 100644 --- a/src/screenshotter.ts +++ b/src/screenshotter.ts @@ -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 { + 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 { 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 { 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 { + 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 { 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')) diff --git a/src/webkit/wkPage.ts b/src/webkit/wkPage.ts index ee0b7f1f3a..e9adfb2eb6 100644 --- a/src/webkit/wkPage.ts +++ b/src/webkit/wkPage.ts @@ -488,12 +488,8 @@ export class WKPage implements PageDelegate { }).catch(debugError); } - getBoundingBoxForScreenshot(handle: dom.ElementHandle): Promise { - return handle.boundingBox(); - } - canScreenshotOutsideViewport(): boolean { - return false; + return true; } async setBackgroundColor(color?: { r: number; g: number; b: number; a: number; }): Promise { @@ -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 { - 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 { + // 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 { - await this._pageProxySession.send('Emulation.setDeviceMetricsOverride', { ...oldSize, fixedLayout: false, deviceScaleFactor: 0 }); + async resetViewport(): Promise { + assert(false, 'Should not be called'); } async getContentFrame(handle: dom.ElementHandle): Promise { diff --git a/test/assets/overflow-large.html b/test/assets/overflow-large.html new file mode 100644 index 0000000000..3d76f830d5 --- /dev/null +++ b/test/assets/overflow-large.html @@ -0,0 +1,22 @@ + + + + + diff --git a/test/golden-chromium/screenshot-element-mobile.png b/test/golden-chromium/screenshot-element-mobile.png new file mode 100644 index 0000000000000000000000000000000000000000..c2c3ddca298aba5c502f56e6656fd9330220b327 GIT binary patch literal 474 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^TK3NDj4wz}vwUf`e5)Ukjg=c4B?ZY6Q{gD+<_wP}}l z6nSQ36>Zn#-f|$iNz7fE$&pKHm-_rCzvuN??>N7ATC?@xO>6j=CvYp+J%0ba_|K}- z?e1;A{tEc13fvXy$m4X`&ax<)>7s7qi)jue-U{}mHHEE$Jc%sMXWq*OW$s-$i%G;W zZF||t6r&}VGkq>ExtSx>>!xYDf7J|T5r=j2<5pbF65(PsvM%I1RJ=tim8p?oS*Dfk z^|OT?x8ELn{&}OJ?ag94p-v0itCJs3c=3Z{t=G@fKZ902`4Zw^|LI!P4gZAOW-CLy zZnhPjNK*3L8l^h>>?RwlH95zZzB$)Wuj{urPJRCQ;w>U!mP`4PSezL|x?Qg=R|`2? z63iqS9eMQe#}9S&%O8Ea|IFgamuF(Pw=sImn|nEL`|j&|;`G&T&-U|Y;!@~!TUip(bP*}7q&3SPEgSM@h9ZZ`&x!)qR}^g{rtKT7+DOSu6{1- HoD!M<45`mj literal 0 HcmV?d00001 diff --git a/test/golden-chromium/screenshot-mobile-clip.png b/test/golden-chromium/screenshot-mobile-clip.png new file mode 100644 index 0000000000000000000000000000000000000000..e0b7ed42f19f742aea4f65d44dbdac706228434a GIT binary patch literal 1660 zcma)-Sy0ng6vkswf-DJ;6j>4$je=}aHVLI+WRDaSihzhI`zm(mpRg1gYS_djVh}J3 z1&e@|0;6G75M_%D0V)szgb)mc5HQVS=a~ox;uet2M{6_52zFguJ|J?<64*!8+ z*wBQwNm1A>wyMv5C8w8nh~7z+&eF^y(rb>+s!3(~NhtX5(`Ea?5J{PjaYJJxmReT; zh|wvkWUewp-1ZJK72ODqFX3(6g}oZUnE)TwX5ld3M}E_En)M|YGO9UoM&7=Jgl3Ax zwwL+O6G-4+^cBt;7qV8DlXV759hpr(@LA|59putTeCDSypCqB zte9)x>WM*4wgAxrRD68=L;vbIuLrT$+3a(5#L-23JvRYx0Ksp#Mp$YASE!ZE>6_+H z&Trp{3J;f~(P-rjxbqPn3R>z;Ia&Dz2A@t(A(${QT9{u6le(q5P%Ayi`?2JEz*Y|w z*pkFdqBlz=NM<}GO}sSTRmkIVom+y%6J349CtfdZu+R`FG;!x69&oVOV%lZwwxTiD z53~CF?+2PvflCPqp^=f1^^gt=x?S(&B*MGIK61(h!Soo0J7(-!3l%z5Gj=6kG38zi z4XIQ6D>=HSE7VPG9DbciHQUhoT;c!zOzq09Bu!8z62&epFH`iWmB+PjYG7AjPc+*j(gELFxj5#km;k&_*Rn7fH~ zZ{l`$=0e-VKGp1ymQ%J7adCO_D$toRa#xH7^+8~BRNj-6o0yI7^uey_42*m>CdWWN0fWoTg z+~=HkY3Fb1&JVraY-8p#6PIi%3`sMO2^X$LcCdDLF&Y{gESk&&?S!37yhn&=!T*t8YtNTQvBd1`10#!gfaF&GhJ9F7GpUC$ zsjnlTX95G&RybQ4vbqA}yr=#VMIupNrhN(IDX*53`SXcBgs!eY<~Cfq5nOSM&p*k` z4ZVh|1AD6Eb|pF_E8j_56?Av~EFK*leYe8>*GG~HyznZxRTYN zn$f^LlNu<`u+~;VAdxamQ3t90$B6nC!m;u3Rr@ph%B(Z(# fOl3p(W(Md3zu@ruk*l+^bppb=db`v*g%JM+0_^iB literal 0 HcmV?d00001 diff --git a/test/golden-chromium/screenshot-mobile-fullpage.png b/test/golden-chromium/screenshot-mobile-fullpage.png new file mode 100644 index 0000000000000000000000000000000000000000..bb975e20efeb92c40af7c066098f946232ff0d54 GIT binary patch literal 20221 zcmeI4e{2(F7{^aQvJeLNBRF7efH6~OfJwliwF5IjLc&n_gQ9HcoJ66)IVSDsT7g7^ z8>F$O~#`- zi!Y$PRMGTAwJ(jN*;{jF9$Zb1$dLuN;=0&K?wqf9WqUx;^!LMNpQ^D(&ufZvPxM2Bkf>~ag$fA zH>%>6{N2kx*&X;X=+xt-E%q(GJ-4anQpE^x;m=%QIo&j4u9eJHwo?~^6^#XT4dmhD zc8xD=pa6c(mreHAAcvaSbd%c$??t-%=P)NA&PEGe*T#2ZzKEgpSWB9(H24s>@Uyp( zG`&3()+!^ncN^)P>VRIw0G{wpQ;WYWIN|eFyQYz1;Jv7?EmE7|fzI-4H(eA*nZDJ3 zuD;e)&Ul^yA3Bt%odjRvpJxKd+Z0CaX;&!{ECqEf5b|Doz~F*6#a}-L1$nPMj3J*$ zR0tps&<-Jw1dCQ>g^;hH-2Se50QvRac__$t@DU98L{d!vc~?PQALLPBL5NKx9nXTgw9ShNTr?+f%m zB_A&ug>0`bRzoFE>yVK5*GDkqB}3k03;q67v^m)-76o~Yw;MxVI^-z^LSBsmi;vT{ z0VUsEkqHVdSYBzoD99gVcno>zkS}*YCBHUKG{(SE?e>*G$X6FBtt5oJlR-khBppLu zI^Zt7GGESWdQm39vTwz8ZU;tbjTZAeM%k$mMWUn z!M;sg4-E-3FjanB4 zyEI6D1O@r8Xl_VA{%G{Vp`weeTV_{%`^Xgq*NXoH-6hx#mJ)UJr$3BHY3Y& zwWjo(;MR^|T6D8A!=*h<&5sk9i044eOF=#mYW*hMzfWL=4sPx6+d~`F z=0asVgM<*Gk&k>J*fqJhf1k*Y9@N@lvN~PBrLEi-l55glzYiwuxq{=sUA{XD@ZUDs z2UP%0>&Yl+rp>Z+$~A2FV#7AU*o6@IDk}uOOv4rfUs?qKfX@W9p{q~HHEc2PrBwhB z_{C~NV7F|;_I?2We+GHbOoNb@Y1m@OOKbB$RqrV1gSJQ#zSr3B!E&GS(?u^IT-CdC#M0SmFZBOZ{-AwwW8o3++&T1qN6)1qU;-!c;e;AG_y|V&__&Xiq~UC!!jpXBYA^FVha-hz z>B%eWs!H1KC9ae%P;Y@8A$%00ZFQQ3p-zS8>DXk$8$5fUKU>(Jd3#4~&a7Zg;oy&G zkQEq*?5)Sal3z2D=E8ZApjO$}M^1Mm!X~k~!2qX+@J0%v8+Qm~J?OAOS#MJy za-E1xL{LMUAby2qeWFcK1hRg>D;F!HVi`oFtfzul)+f_%7%c0{9Z=T)qVWz)CkxA{ z*d_~qH6V^73CeoprbGi}lwM_p zi9?$LIeT1Ri)B3!b>cf>31mG;9Kxouco{XYfeuQ_+ci9v@`;K`fs_Y(hd?Rs7cZTJ z>E`GwuR+EL@)C^7xz@26L?UmC#ogjn+8vb@?C)@=O~ DCS_M( literal 0 HcmV?d00001 diff --git a/test/golden-webkit/screenshot-mobile-clip.png b/test/golden-webkit/screenshot-mobile-clip.png new file mode 100644 index 0000000000000000000000000000000000000000..fd3a8600b53fbe0c79343de99f17c8ba1ffe89f7 GIT binary patch literal 2090 zcmbuA`#;l*AIH~BOlanQP_sUWTq4P3YM5+j%(XBaw-gn*Rbq`?N=Ga?$P~E@nM*Ds zDdv*Y)DXGfa!eRGCkx}d@1JlU=ZE+E_4Difcs!r)dyaS;39;YAKp>C=4vTRTWPo6@ zMfVH#QHIW0LF|igvatlwS;`y`NMs&|v2eMuZ-x5WLH3+Xx5x$I9ZaXfgH)buIiRoV zlIM7-j7qvoZP-gsO6u-T=t$`9#3ah8(;zNzI$W6G0{IsA^oj3>9HuTm0=NGtzFWr>Ey56UYlWIl1w!GA#i;}wXTR@AS?>x4%T5H$N|k< zSu*g*BRdrxV5;^~i|*A7w{K)H7TeE-x;IMMOkgI=I7@}4h#&mhfvz9s&q~n z87+8JWp|>2U$(Y}ZoO2rh>eX^@m3y9-pz{MLK{>LxNT;dI(j)E`S) zXhf83TBrjeQ<$z)fL_v)`&xiT9`HJaA`iN1*O`+sa*V4tz~zC{^TkTv8XqzbQM$eURD;KETv6~K@l?+a zncL2G$Zz_7E=ng6T&oon6vCRWPEbCW1Scvr$8OR&XAvEn+^NL%lEiG1m8GN%Ehs2x zG)ndoeoAsvb*S#VkIyE5j4vc2nNtxr#;rMZZ4xqPx1Ydy=_W2BBGNU~@+lt#*&B8( zaemixII+6^`ui!>wp+Q|5Q^fRrv>QS)&`qU-~{N5Ihl7qU{x9$1d6`}UYG41*U0z) z+W8wYdyLZTfXLm)<3KyzfrzJQ1EIA6>C|fM!fA5A{^>G3@75bL4;!{)e=djH+1Uw> zZXpxs=H_Pq53G~NTYnA^OFq(xuRaJ@(CNC)#D#$}ht>7Dp2rb8+m@dvCipRX{r#_E zxY2w*pR^T4LYvQb|DhzA5UeEEGX93-^L_HC*{#26P*Jz=!tCG66;{WaonNS(sk3K1 ziBZp3IC;_ScvAf3KMgA`@!rXL}9 zes)!9pqW;}VqGst>bLucEK4UvAxD!;Xd%uaSh$+tsixm_ecHUDem$L|^^U)a>5sJSHyR|VZ%;j= zjjl^*#Cb$VFoASs6Snsk^<3<`{l{N;?yO9`nfe`<*Epk+9%J(uPhl8yoGa8wDOj0v z!Bz-pS9UQHcFz`iMHDC;p}0Vh1kFMTG9cqgxsL|4)aq3;?)up|Nu^>+?ixWTj-fa$ za3(G5Q!*NUUk7K6fIo(h!myULYUN_f%JP?wiWkZ~yFyEu&TObZF8y8r^|E){ZN(Pg z5Ytkrj~@96l+>t21*9#=VrhQ@RdXz=S$krsx`xICGv(m_JA(|2jDp}oXk9M`?17%j z^2$n7TwI*f21VsgzF7>*#k}g}i9=`VY(;-a!m!?ayRXn<05`c0+*?V~G$UsLy=G{{ z&42U)H8Lo`O%YL`dbea5;zR+sAoBdsn=C5YmNKt`$Y|aAvrZa|qqGV^DDHbs(O^v+ zCFWVqlP3pQpd{+|;cAg;-2U3r^S?I!k_!mkUI~GVM7JL;>oTB6|puqDhq^7Y}%3mD4=12_BFy7lV~9!_F`IeZ6>K-;i#rt@Ap3w2`DDZL8Io8Bu0$bbL4V#}Q-MFI-A*Do4fqCpg%}5s;(OGDf zqOCg4W>Yxtj>vppo6V~C3k*~XPU*-9d}GzxLKvxa5f7IZi2ne~!PMBm06F{+$;Xp!2!{R{Et49uX5`<9I{DZRA_k%tF&4Sf@*Y` z*F)K}vqj|v37UoIq@JnKrKu IOW%b50k9F@O#lD@ literal 0 HcmV?d00001 diff --git a/test/screenshot.spec.js b/test/screenshot.spec.js index 9249557ddd..66f3b35778 100644 --- a/test/screenshot.spec.js +++ b/test/screenshot.spec.js @@ -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('
'); @@ -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('
'); const elementHandle = await page.$('div'); diff --git a/test/test.js b/test/test.js index 9301130654..1f067902af 100644 --- a/test/test.js +++ b/test/test.js @@ -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;