diff --git a/docs/api.md b/docs/api.md index 267a307f42..e0afffdce4 100644 --- a/docs/api.md +++ b/docs/api.md @@ -3459,11 +3459,6 @@ If `key` is a single character and no modifier keys besides `Shift` are being he - `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'. - `quality` <[number]> The quality of the image, between 0-100. Not applicable to `png` images. - - `clip` <[Object]> Passed clip value is ignored and instead set to the element's bounding box. - - `x` <[number]> - - `y` <[number]> - - `width` <[number]> - - `height` <[number]> - `omitBackground` <[boolean]> Hides default white background and allows capturing screenshots with transparency. Defaults to `false`. - returns: <[Promise]<|[Buffer]>> Promise which resolves to buffer with the captured screenshot. diff --git a/src/chromium/Screenshotter.ts b/src/chromium/Screenshotter.ts index b61f3e3ca7..c5203472bd 100644 --- a/src/chromium/Screenshotter.ts +++ b/src/chromium/Screenshotter.ts @@ -14,7 +14,7 @@ export class CRScreenshotDelegate implements ScreenshotterDelegate { } - async getBoundingBox(handle: dom.ElementHandle): Promise { + async getBoundingBox(handle: dom.ElementHandle): Promise { const rect = await handle.boundingBox(); if (!rect) return rect; diff --git a/src/dom.ts b/src/dom.ts index 52e24895a0..148f5cd6b0 100644 --- a/src/dom.ts +++ b/src/dom.ts @@ -170,7 +170,7 @@ export class ElementHandle extends js.JSHandle { const element = node as Element; // force-scroll if page's javascript is disabled. if (!pageJavascriptEnabled) { - //@ts-ignore because only Chromium still supports 'instant' + // @ts-ignore because only Chromium still supports 'instant' element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'}); return false; } @@ -185,7 +185,7 @@ export class ElementHandle extends js.JSHandle { requestAnimationFrame(() => {}); }); if (visibleRatio !== 1.0) { - //@ts-ignore because only Chromium still supports 'instant' + // @ts-ignore because only Chromium still supports 'instant' element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'}); } return false; @@ -376,7 +376,7 @@ export class ElementHandle extends js.JSHandle { return this._world.delegate.boundingBox(this); } - async screenshot(options?: types.ScreenshotOptions): Promise { + async screenshot(options?: types.ElementScreenshotOptions): Promise { return this._world.delegate.screenshot(this, options); } diff --git a/src/download.ts b/src/download.ts index a1a44d0d80..e97e7a85ef 100644 --- a/src/download.ts +++ b/src/download.ts @@ -1,22 +1,22 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -export async function download( - browserFetcher: - import('./chromium/BrowserFetcher').BrowserFetcher | - import('./firefox/BrowserFetcher').BrowserFetcher | - import('./webkit/BrowserFetcher').BrowserFetcher, - revision: string, - browserName: string, - {onProgress}: {onProgress?: (downloadedBytes: number, totalBytes: number) => void} = {}) : Promise { - const revisionInfo = browserFetcher.revisionInfo(revision); - await browserFetcher.download(revision, onProgress); - return revisionInfo; -} - -export type RevisionInfo = { - folderPath: string, - executablePath: string, - url: string, - local: boolean, - revision: string, -}; +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +export async function download( + browserFetcher: + import('./chromium/BrowserFetcher').BrowserFetcher | + import('./firefox/BrowserFetcher').BrowserFetcher | + import('./webkit/BrowserFetcher').BrowserFetcher, + revision: string, + browserName: string, + {onProgress}: {onProgress?: (downloadedBytes: number, totalBytes: number) => void} = {}) : Promise { + const revisionInfo = browserFetcher.revisionInfo(revision); + await browserFetcher.download(revision, onProgress); + return revisionInfo; +} + +export type RevisionInfo = { + folderPath: string, + executablePath: string, + url: string, + local: boolean, + revision: string, +}; diff --git a/src/firefox/Screenshotter.ts b/src/firefox/Screenshotter.ts index d1f67deb71..f8feb96823 100644 --- a/src/firefox/Screenshotter.ts +++ b/src/firefox/Screenshotter.ts @@ -16,7 +16,7 @@ export class FFScreenshotDelegate implements ScreenshotterDelegate { this._frameManager = frameManager; } - getBoundingBox(handle: dom.ElementHandle): Promise { + getBoundingBox(handle: dom.ElementHandle): Promise { const frameId = this._frameManager._frameData(handle.executionContext().frame()).frameId; return this._session.send('Page.getBoundingBox', { frameId, diff --git a/src/injected/injected.ts b/src/injected/injected.ts index 046ffa3169..b3d6434de6 100644 --- a/src/injected/injected.ts +++ b/src/injected/injected.ts @@ -19,7 +19,7 @@ class Injected { querySelector(selector: string, root: Node): Element | undefined { const parsed = this._parseSelector(selector); - if (!root["querySelector"]) + if (!root['querySelector']) throw new Error('Node is not queryable.'); let element = root as SelectorRoot; for (const { engine, selector } of parsed) { @@ -33,7 +33,7 @@ class Injected { querySelectorAll(selector: string, root: Node): Element[] { const parsed = this._parseSelector(selector); - if (!root["querySelectorAll"]) + if (!root['querySelectorAll']) throw new Error('Node is not queryable.'); let set = new Set([ root as SelectorRoot ]); for (const { engine, selector } of parsed) { diff --git a/src/screenshotter.ts b/src/screenshotter.ts index 04c083589f..fc2b3a1ee9 100644 --- a/src/screenshotter.ts +++ b/src/screenshotter.ts @@ -24,13 +24,13 @@ import * as types from './types'; const writeFileAsync = helper.promisify(fs.writeFile); export interface Page { - viewport(): types.Viewport; + viewport(): types.Viewport | null; setViewport(v: types.Viewport): Promise; evaluate(f: () => any): Promise; } export interface ScreenshotterDelegate { - getBoundingBox(handle: dom.ElementHandle): Promise; + getBoundingBox(handle: dom.ElementHandle): Promise; canCaptureOutsideViewport(): boolean; setBackgroundColor(color?: { r: number; g: number; b: number; a: number; }): Promise; screenshot(format: string, options: types.ScreenshotOptions, viewport: types.Viewport): Promise; @@ -57,17 +57,17 @@ export class Screenshotter { return this._queue.postTask(async () => { let overridenViewport: types.Viewport | undefined; const viewport = this._page.viewport(); - if (options.fullPage && !this._delegate.canCaptureOutsideViewport()) { + if (viewport && options.fullPage && !this._delegate.canCaptureOutsideViewport()) { const fullPage = await this._page.evaluate(() => ({ width: Math.max( - document.body.scrollWidth, document.documentElement.scrollWidth, - document.body.offsetWidth, document.documentElement.offsetWidth, - document.body.clientWidth, document.documentElement.clientWidth + 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 + document.body.scrollHeight, document.documentElement.scrollHeight, + document.body.offsetHeight, document.documentElement.offsetHeight, + document.body.clientHeight, document.documentElement.clientHeight ) })); overridenViewport = { ...viewport, ...fullPage }; @@ -101,12 +101,12 @@ export class Screenshotter { if (boundingBox.width > viewport.width || boundingBox.height > viewport.height) { overridenViewport = { ...viewport, - width: Math.max(viewport.width, Math.ceil(boundingBox.width)), - height: Math.max(viewport.height, Math.ceil(boundingBox.height)), + width: Math.max(viewport.width, boundingBox.width), + height: Math.max(viewport.height, boundingBox.height), }; await this._page.setViewport(overridenViewport); } - + await handle._scrollIntoViewIfNeeded(); boundingBox = enclosingIntRect(await this._delegate.getBoundingBox(handle)); } @@ -129,7 +129,7 @@ export class Screenshotter { await this._delegate.setBackgroundColor({ r: 0, g: 0, b: 0, a: 0}); const buffer = await this._delegate.screenshot(format, options, viewport); if (shouldSetDefaultBackground) - await this._delegate.setBackgroundColor(); + await this._delegate.setBackgroundColor(); if (options.path) await writeFileAsync(options.path, buffer); return buffer; @@ -152,9 +152,9 @@ class TaskQueue { } } -function trimClipToViewport(viewport: types.Viewport, clip: types.Rect | undefined): types.Rect | undefined { - if (!clip) - return; +function trimClipToViewport(viewport: types.Viewport | null, clip: types.Rect | null): types.Rect | null { + if (!clip || !viewport) + return clip; const p1 = { x: Math.min(clip.x, viewport.width), y: Math.min(clip.y, viewport.height) }; const p2 = { x: Math.min(clip.x + clip.width, viewport.width), y: Math.min(clip.y + clip.height, viewport.height) }; const result = { x: p1.x, y: p1.y, width: p2.x - p1.x, height: p2.y - p1.y }; @@ -200,14 +200,9 @@ function validateScreeshotOptions(options: types.ScreenshotOptions): 'png' | 'jp } function enclosingIntRect(rect: types.Rect): types.Rect { - const x = rect.x | 0; - const y = rect.y | 0; - const x2 = Math.ceil(((rect.x + rect.width) * 100 | 0) / 100); - const y2 = Math.ceil(((rect.y + rect.height) * 100 | 0) / 100); - return { - x, - y, - width: x2 - x, - height: y2 - y - }; + 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 }; } diff --git a/src/types.ts b/src/types.ts index e4266a978c..dbdf2f7341 100644 --- a/src/types.ts +++ b/src/types.ts @@ -10,7 +10,7 @@ type PageFunction = string | ((...args: Args) => R type PageFunctionOn = string | ((on: On, ...args: Args) => R | Promise); type Handle = T extends Node ? dom.ElementHandle : js.JSHandle; -type ElementForSelector = T extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[T] : Element; +type ElementForSelector = T extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[T] : Element; export type Evaluate = (pageFunction: PageFunction, ...args: Boxed) => Promise; export type EvaluateHandle = (pageFunction: PageFunction, ...args: Boxed) => Promise>; diff --git a/src/webkit/Screenshotter.ts b/src/webkit/Screenshotter.ts index 54a02fcb7a..1030fefb7f 100644 --- a/src/webkit/Screenshotter.ts +++ b/src/webkit/Screenshotter.ts @@ -15,7 +15,7 @@ export class WKScreenshotDelegate implements ScreenshotterDelegate { this._session = session; } - getBoundingBox(handle: dom.ElementHandle): Promise { + getBoundingBox(handle: dom.ElementHandle): Promise { return handle.boundingBox(); } @@ -28,7 +28,7 @@ export class WKScreenshotDelegate implements ScreenshotterDelegate { this._session.send('Page.setDefaultBackgroundColorOverride', { color }); } - async screenshot(format: string, options: types.ScreenshotOptions, viewport: types.Viewport ): Promise { + async screenshot(format: string, options: types.ScreenshotOptions, viewport: types.Viewport): Promise { const rect = options.clip || { x: 0, y: 0, width: viewport.width, height: viewport.height }; const result = await this._session.send('Page.snapshotRect', { ...rect, coordinateSystem: options.fullPage ? 'Page' : 'Viewport' }); const prefix = 'data:image/png;base64,'; diff --git a/test/golden-webkit/transparent.png b/test/golden-webkit/transparent.png new file mode 100644 index 0000000000..6c1fe85b52 Binary files /dev/null and b/test/golden-webkit/transparent.png differ diff --git a/test/screenshot.spec.js b/test/screenshot.spec.js index b17e3ff8f7..a8f316854d 100644 --- a/test/screenshot.spec.js +++ b/test/screenshot.spec.js @@ -105,7 +105,7 @@ module.exports.addTests = function({testRunner, expect, product, FFOX, CHROME, W expect(screenshots[i]).toBeGolden(`grid-cell-${i}.png`); await Promise.all(pages.map(page => page.close())); }); - it.skip(FFOX || WEBKIT)('should allow transparency', async({page, server}) => { + it.skip(FFOX)('should allow transparency', async({page, server}) => { await page.setViewport({ width: 50, height: 150 }); await page.setContent(`