chore(chromium): Capture off-screen content without resizing viewport (#10606)
This commit is contained in:
parent
bdfe92f8a7
commit
1bfc473bc8
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>>;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
Loading…
Reference in a new issue