diff --git a/packages/playwright-core/src/server/chromium/crPage.ts b/packages/playwright-core/src/server/chromium/crPage.ts index 0644ea0dfb..ec257d1429 100644 --- a/packages/playwright-core/src/server/chromium/crPage.ts +++ b/packages/playwright-core/src/server/chromium/crPage.ts @@ -244,15 +244,11 @@ export class CRPage implements PageDelegate { await this._browserContext._browser._closePage(this); } - canScreenshotOutsideViewport(): boolean { - return false; - } - async setBackgroundColor(color?: { r: number; g: number; b: number; a: number; }): Promise { await this._mainFrameSession._client.send('Emulation.setDefaultBackgroundColorOverride', { color }); } - async takeScreenshot(progress: Progress, format: 'png' | 'jpeg', documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined): Promise { + async takeScreenshot(progress: Progress, format: 'png' | 'jpeg', documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined, fitsViewport: boolean | undefined): Promise { const { visualViewport } = await this._mainFrameSession._client.send('Page.getLayoutMetrics'); if (!documentRect) { documentRect = { @@ -268,14 +264,10 @@ export class CRPage implements PageDelegate { // ignore current page scale. const clip = { ...documentRect, scale: viewportRect ? visualViewport.scale : 1 }; progress.throwIfAborted(); - const result = await this._mainFrameSession._client.send('Page.captureScreenshot', { format, quality, clip }); + const result = await this._mainFrameSession._client.send('Page.captureScreenshot', { format, quality, clip, captureBeyondViewport: !fitsViewport }); return Buffer.from(result.data, 'base64'); } - async resetViewport(): Promise { - await this._mainFrameSession._client.send('Emulation.setDeviceMetricsOverride', { mobile: false, width: 0, height: 0, deviceScaleFactor: 0 }); - } - async getContentFrame(handle: dom.ElementHandle): Promise { return this._sessionForHandle(handle)._getContentFrame(handle); } diff --git a/packages/playwright-core/src/server/firefox/ffPage.ts b/packages/playwright-core/src/server/firefox/ffPage.ts index e1f94d91ed..685b00f9fd 100644 --- a/packages/playwright-core/src/server/firefox/ffPage.ts +++ b/packages/playwright-core/src/server/firefox/ffPage.ts @@ -401,10 +401,6 @@ export class FFPage implements PageDelegate { await this._session.send('Page.close', { runBeforeUnload }); } - canScreenshotOutsideViewport(): boolean { - return true; - } - async setBackgroundColor(color?: { r: number; g: number; b: number; a: number; }): Promise { if (color) throw new Error('Not implemented'); @@ -430,10 +426,6 @@ export class FFPage implements PageDelegate { return Buffer.from(data, 'base64'); } - async resetViewport(): Promise { - assert(false, 'Should not be called'); - } - async getContentFrame(handle: dom.ElementHandle): Promise { const { contentFrameId } = await this._session.send('Page.describeNode', { frameId: handle._context.frame._id, diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts index b217c743cc..32b936af77 100644 --- a/packages/playwright-core/src/server/page.ts +++ b/packages/playwright-core/src/server/page.ts @@ -58,10 +58,8 @@ export interface PageDelegate { setFileChooserIntercepted(enabled: boolean): Promise; bringToFront(): Promise; - canScreenshotOutsideViewport(): boolean; - resetViewport(): Promise; // Only called if canScreenshotOutsideViewport() returns false. setBackgroundColor(color?: { r: number; g: number; b: number; a: number; }): Promise; - takeScreenshot(progress: Progress, format: string, documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined): Promise; + takeScreenshot(progress: Progress, format: string, documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined, fitsViewport: boolean | undefined): Promise; isElementHandle(remoteObject: any): boolean; adoptElementHandle(handle: dom.ElementHandle, to: dom.FrameExecutionContext): Promise>; diff --git a/packages/playwright-core/src/server/screenshotter.ts b/packages/playwright-core/src/server/screenshotter.ts index 058185b243..5fda7b6595 100644 --- a/packages/playwright-core/src/server/screenshotter.ts +++ b/packages/playwright-core/src/server/screenshotter.ts @@ -62,79 +62,50 @@ export class Screenshotter { async screenshotPage(progress: Progress, options: types.ScreenshotOptions): Promise { const format = validateScreenshotOptions(options); return this._queue.postTask(async () => { - const { viewportSize, originalViewportSize } = await this._originalViewportSize(progress); + const { viewportSize } = await this._originalViewportSize(progress); if (options.fullPage) { const fullPageSize = await this._fullPageSize(progress); let documentRect = { x: 0, y: 0, width: fullPageSize.width, height: fullPageSize.height }; - let overriddenViewportSize: types.Size | null = null; const fitsViewport = fullPageSize.width <= viewportSize.width && fullPageSize.height <= viewportSize.height; - if (!this._page._delegate.canScreenshotOutsideViewport() && !fitsViewport) { - overriddenViewportSize = fullPageSize; - progress.throwIfAborted(); // Avoid side effects. - await this._page.setViewportSize(overriddenViewportSize); - progress.cleanupWhenAborted(() => this._restoreViewport(originalViewportSize)); - } if (options.clip) documentRect = trimClipToSize(options.clip, documentRect); - const buffer = await this._screenshot(progress, format, documentRect, undefined, options); + const buffer = await this._screenshot(progress, format, documentRect, undefined, fitsViewport, options); progress.throwIfAborted(); // Avoid restoring after failure - should be done by cleanup. - if (overriddenViewportSize) - await this._restoreViewport(originalViewportSize); return buffer; } const viewportRect = options.clip ? trimClipToSize(options.clip, viewportSize) : { x: 0, y: 0, ...viewportSize }; - return await this._screenshot(progress, format, undefined, viewportRect, options); + return await this._screenshot(progress, format, undefined, viewportRect, true, options); }); } async screenshotElement(progress: Progress, handle: dom.ElementHandle, options: types.ElementScreenshotOptions = {}): Promise { const format = validateScreenshotOptions(options); return this._queue.postTask(async () => { - const { viewportSize, originalViewportSize } = await this._originalViewportSize(progress); + const { viewportSize } = await this._originalViewportSize(progress); await handle._waitAndScrollIntoViewIfNeeded(progress); progress.throwIfAborted(); // Do not do extra work. - let boundingBox = await handle.boundingBox(); + const boundingBox = await handle.boundingBox(); assert(boundingBox, 'Node is either not visible or not an HTMLElement'); assert(boundingBox.width !== 0, 'Node has 0 width.'); assert(boundingBox.height !== 0, 'Node has 0 height.'); - let overriddenViewportSize: types.Size | null = null; const fitsViewport = boundingBox.width <= viewportSize.width && boundingBox.height <= viewportSize.height; - if (!this._page._delegate.canScreenshotOutsideViewport() && !fitsViewport) { - overriddenViewportSize = helper.enclosingIntSize({ - width: Math.max(viewportSize.width, boundingBox.width), - height: Math.max(viewportSize.height, boundingBox.height), - }); - progress.throwIfAborted(); // Avoid side effects. - await this._page.setViewportSize(overriddenViewportSize); - progress.cleanupWhenAborted(() => this._restoreViewport(originalViewportSize)); - - progress.throwIfAborted(); // Avoid extra work. - await handle._waitAndScrollIntoViewIfNeeded(progress); - 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.'); - } - progress.throwIfAborted(); // Avoid extra work. const scrollOffset = await this._page.mainFrame().waitForFunctionValueInUtility(progress, () => ({ x: window.scrollX, y: window.scrollY })); const documentRect = { ...boundingBox }; documentRect.x += scrollOffset.x; documentRect.y += scrollOffset.y; - const buffer = await this._screenshot(progress, format, helper.enclosingIntRect(documentRect), undefined, options); + const buffer = await this._screenshot(progress, format, helper.enclosingIntRect(documentRect), undefined, fitsViewport, options); progress.throwIfAborted(); // Avoid restoring after failure - should be done by cleanup. - if (overriddenViewportSize) - await this._restoreViewport(originalViewportSize); return buffer; }); } - private async _screenshot(progress: Progress, format: 'png' | 'jpeg', documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, options: types.ElementScreenshotOptions): Promise { + private async _screenshot(progress: Progress, format: 'png' | 'jpeg', documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, fitsViewport: boolean | undefined, options: types.ElementScreenshotOptions): Promise { if ((options as any).__testHookBeforeScreenshot) await (options as any).__testHookBeforeScreenshot(); progress.throwIfAborted(); // Screenshotting is expensive - avoid extra work. @@ -144,7 +115,7 @@ export class Screenshotter { progress.cleanupWhenAborted(() => this._page._delegate.setBackgroundColor()); } progress.throwIfAborted(); // Avoid extra work. - const buffer = await this._page._delegate.takeScreenshot(progress, format, documentRect, viewportRect, options.quality); + 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. if (shouldSetDefaultBackground) await this._page._delegate.setBackgroundColor(); @@ -153,14 +124,6 @@ export class Screenshotter { await (options as any).__testHookAfterScreenshot(); return buffer; } - - private async _restoreViewport(originalViewportSize: types.Size | null) { - assert(!this._page._delegate.canScreenshotOutsideViewport()); - if (originalViewportSize) - await this._page.setViewportSize(originalViewportSize); - else - await this._page._delegate.resetViewport(); - } } class TaskQueue { diff --git a/packages/playwright-core/src/server/webkit/wkPage.ts b/packages/playwright-core/src/server/webkit/wkPage.ts index 754627301a..635ed59b9f 100644 --- a/packages/playwright-core/src/server/webkit/wkPage.ts +++ b/packages/playwright-core/src/server/webkit/wkPage.ts @@ -747,10 +747,6 @@ export class WKPage implements PageDelegate { }); } - canScreenshotOutsideViewport(): boolean { - return true; - } - async setBackgroundColor(color?: { r: number; g: number; b: number; a: number; }): Promise { await this._session.send('Page.setDefaultBackgroundColorOverride', { color }); } @@ -790,10 +786,6 @@ export class WKPage implements PageDelegate { return buffer; } - async resetViewport(): Promise { - assert(false, 'Should not be called'); - } - async getContentFrame(handle: dom.ElementHandle): Promise { const nodeInfo = await this._session.send('DOM.describeNode', { objectId: handle._objectId diff --git a/tests/screenshot.spec.ts b/tests/screenshot.spec.ts index a75d18f0dd..ee07521468 100644 --- a/tests/screenshot.spec.ts +++ b/tests/screenshot.spec.ts @@ -117,6 +117,38 @@ browserTest.describe('page screenshot', () => { expect(pixel(0, 8339).r).toBeLessThan(128); expect(pixel(0, 8339).b).toBeGreaterThan(128); }); + + browserTest('should handle vh units ', async ({ contextFactory }) => { + const context = await contextFactory(); + const page = await context.newPage(); + + await page.setViewportSize({ width: 800, height: 500 }); + await page.evaluate(() => { + document.body.style.margin = '0'; + document.body.style.padding = '0'; + document.documentElement.style.margin = '0'; + document.documentElement.style.padding = '0'; + const div = document.createElement('div'); + div.style.width = '100%'; + div.style.borderTop = '100vh solid red'; + div.style.borderBottom = '100vh solid blue'; + document.body.appendChild(div); + }); + const buffer = await page.screenshot({ fullPage: true }); + const decoded = PNG.sync.read(buffer); + + const pixel = (x: number, y: number) => { + const dst = new PNG({ width: 1, height: 1 }); + PNG.bitblt(decoded, dst, x, y, 1, 1); + const pixels = dst.data; + return { r: pixels[0], g: pixels[1], b: pixels[2], a: pixels[3] }; + }; + + expect(pixel(0, 0).r).toBeGreaterThan(128); + expect(pixel(0, 0).b).toBeLessThan(128); + expect(pixel(0, 999).r).toBeLessThan(128); + expect(pixel(0, 999).b).toBeGreaterThan(128); + }); }); browserTest.describe('element screenshot', () => { @@ -266,6 +298,35 @@ browserTest.describe('element screenshot', () => { await context.close(); }); + browserTest('element screenshots should handle vh units ', async ({ contextFactory }) => { + const context = await contextFactory(); + const page = await context.newPage(); + + await page.setViewportSize({ width: 800, height: 500 }); + await page.evaluate(() => { + const div = document.createElement('div'); + div.style.width = '100%'; + div.style.borderTop = '100vh solid red'; + div.style.borderBottom = '100vh solid blue'; + document.body.appendChild(div); + }); + const elementHandle = await page.$('div'); + const buffer = await elementHandle.screenshot(); + const decoded = PNG.sync.read(buffer); + + const pixel = (x: number, y: number) => { + const dst = new PNG({ width: 1, height: 1 }); + PNG.bitblt(decoded, dst, x, y, 1, 1); + const pixels = dst.data; + return { r: pixels[0], g: pixels[1], b: pixels[2], a: pixels[3] }; + }; + + expect(pixel(0, 0).r).toBeGreaterThan(128); + expect(pixel(0, 0).b).toBeLessThan(128); + expect(pixel(0, 999).r).toBeLessThan(128); + expect(pixel(0, 999).b).toBeGreaterThan(128); + }); + browserTest('should work if the main resource hangs', async ({ browser, browserName, mode, server }) => { browserTest.skip(mode !== 'default'); browserTest.fixme(browserName === 'chromium', 'https://github.com/microsoft/playwright/issues/9757');