chore(chromium): Capture off-screen content without resizing viewport (#10606)

This commit is contained in:
Henric Trotzig 2021-11-30 14:11:15 -08:00 committed by GitHub
parent bdfe92f8a7
commit 1bfc473bc8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 72 additions and 74 deletions

View file

@ -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<void> {
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<Buffer> {
async takeScreenshot(progress: Progress, format: 'png' | 'jpeg', documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined, fitsViewport: boolean | undefined): Promise<Buffer> {
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<void> {
await this._mainFrameSession._client.send('Emulation.setDeviceMetricsOverride', { mobile: false, width: 0, height: 0, deviceScaleFactor: 0 });
}
async getContentFrame(handle: dom.ElementHandle): Promise<frames.Frame | null> {
return this._sessionForHandle(handle)._getContentFrame(handle);
}

View file

@ -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<void> {
if (color)
throw new Error('Not implemented');
@ -430,10 +426,6 @@ export class FFPage implements PageDelegate {
return Buffer.from(data, 'base64');
}
async resetViewport(): Promise<void> {
assert(false, 'Should not be called');
}
async getContentFrame(handle: dom.ElementHandle): Promise<frames.Frame | null> {
const { contentFrameId } = await this._session.send('Page.describeNode', {
frameId: handle._context.frame._id,

View file

@ -58,10 +58,8 @@ export interface PageDelegate {
setFileChooserIntercepted(enabled: boolean): Promise<void>;
bringToFront(): Promise<void>;
canScreenshotOutsideViewport(): boolean;
resetViewport(): Promise<void>; // Only called if canScreenshotOutsideViewport() returns false.
setBackgroundColor(color?: { r: number; g: number; b: number; a: number; }): Promise<void>;
takeScreenshot(progress: Progress, format: string, documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined): Promise<Buffer>;
takeScreenshot(progress: Progress, format: string, documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined, fitsViewport: boolean | undefined): Promise<Buffer>;
isElementHandle(remoteObject: any): boolean;
adoptElementHandle<T extends Node>(handle: dom.ElementHandle<T>, to: dom.FrameExecutionContext): Promise<dom.ElementHandle<T>>;

View file

@ -62,79 +62,50 @@ export class Screenshotter {
async screenshotPage(progress: Progress, options: types.ScreenshotOptions): Promise<Buffer> {
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<Buffer> {
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<Buffer> {
private async _screenshot(progress: Progress, format: 'png' | 'jpeg', documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, fitsViewport: boolean | undefined, options: types.ElementScreenshotOptions): Promise<Buffer> {
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 {

View file

@ -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<void> {
await this._session.send('Page.setDefaultBackgroundColorOverride', { color });
}
@ -790,10 +786,6 @@ export class WKPage implements PageDelegate {
return buffer;
}
async resetViewport(): Promise<void> {
assert(false, 'Should not be called');
}
async getContentFrame(handle: dom.ElementHandle): Promise<frames.Frame | null> {
const nodeInfo = await this._session.send('DOM.describeNode', {
objectId: handle._objectId

View file

@ -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');