Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Screenshotting] Add captureBeyondViewport: false to workaround a res… #131877

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
21dfdd0
[Screenshotting] Add captureBeyondViewport: false to workaround a res…
tsullivan May 9, 2022
a2c1ccc
Squashed commit of the following:
tsullivan May 10, 2022
cbd1926
skip the visualize test
tsullivan May 10, 2022
5ab1168
revert extra logging that broke tests
tsullivan May 10, 2022
3fdfd6f
remove baseline file for non-working test
tsullivan May 10, 2022
14fdb7c
revert a messy change
tsullivan May 10, 2022
bb1eb1d
Merge remote-tracking branch 'elastic/main' into screenshot/fix-resiz…
tsullivan May 10, 2022
f94a107
move test file
tsullivan May 10, 2022
973bfd8
Merge remote-tracking branch 'elastic/main' into screenshot/fix-resiz…
tsullivan May 10, 2022
b86bcda
set viewport before navigating to url
tsullivan May 10, 2022
734d3bb
simplify createLayout call
tsullivan May 10, 2022
cebafbf
fix reports from TSVB editor
tsullivan May 10, 2022
fe3619a
unskip report from viz editor test
tsullivan May 10, 2022
66abad0
scrollTo is required
tsullivan May 10, 2022
0a94e1c
scroll the element into view
tsullivan May 10, 2022
d5a4848
polish
tsullivan May 10, 2022
9a0157f
update tests
tsullivan May 10, 2022
6371a3f
add test files
tsullivan May 10, 2022
2378b05
fit png test names to a pattern
tsullivan May 10, 2022
a12c31b
fix tests
tsullivan May 11, 2022
97784a7
simplify
tsullivan May 11, 2022
de2c8b8
no need to set the viewport before navigation
tsullivan May 11, 2022
6ddff84
Merge remote-tracking branch 'elastic/main' into screenshot/fix-resiz…
tsullivan May 11, 2022
cb59beb
fix tests
tsullivan May 11, 2022
40fdb9e
Merge remote-tracking branch 'elastic/main' into screenshot/fix-resiz…
tsullivan May 11, 2022
a3384c6
set viewport to element dimensions before capture
tsullivan May 11, 2022
e868fcc
unify resize logging
tsullivan May 11, 2022
6790d1a
always provide deviceScaleFactor to setViewport
tsullivan May 11, 2022
cc69062
do not resize horizontally
tsullivan May 11, 2022
1a24814
relocate an export
tsullivan May 12, 2022
d1176cc
always provide deviceScaleFactor
tsullivan May 12, 2022
307cf8e
Merge branch 'main' into screenshot/fix-resize-during-capture
tsullivan May 12, 2022
b6d505c
Merge branch 'main' into screenshot/fix-resize-during-capture
tsullivan May 12, 2022
f879440
Merge remote-tracking branch 'elastic/main' into screenshot/fix-resiz…
tsullivan May 12, 2022
91def4e
comment
tsullivan May 12, 2022
aecfeeb
Update x-pack/plugins/screenshotting/server/screenshots/get_screensho…
tsullivan May 12, 2022
d1edaaa
Merge branch 'screenshot/fix-resize-during-capture' of github.com:tsu…
tsullivan May 12, 2022
a0c7b63
no need to specify viewport at launch
tsullivan May 12, 2022
0b1d84a
revert move of DEFAULT_VIEWPORT
tsullivan May 13, 2022
55ab21b
partial revert a0c7b63f1a6
tsullivan May 13, 2022
9018483
Merge remote-tracking branch 'elastic/main' into screenshot/fix-resiz…
tsullivan May 13, 2022
d22353e
fix test
tsullivan May 13, 2022
01b3034
Merge branch 'main' into screenshot/fix-resize-during-capture
kibanamachine May 15, 2022
85ec2a1
Merge branch 'main' into screenshot/fix-resize-during-capture
kibanamachine May 16, 2022
2eaff1d
Merge remote-tracking branch 'elastic/main' into screenshot/fix-resiz…
tsullivan May 16, 2022
e1928ba
cleanup unnecessary async
tsullivan May 17, 2022
3543568
Merge branch 'main' into screenshot/fix-resize-during-capture
tsullivan May 17, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 8 additions & 9 deletions x-pack/plugins/screenshotting/server/browsers/chromium/driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,6 @@ export interface ElementPosition {
};
}

export interface Viewport {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not needed, and is confusing: puppeteer.Viewport is a similar but slightly different interface.

zoom: number;
width: number;
height: number;
}

interface OpenOptions {
context?: Context;
headers: Headers;
Expand Down Expand Up @@ -203,7 +197,7 @@ export class HeadlessChromiumDriver {
}

/*
* Call Page.screenshot and return a base64-encoded string of the image
* Receive a PNG buffer of the page screenshot from Chromium
*/
async screenshot(elementPosition: ElementPosition): Promise<Buffer | undefined> {
const { boundingClientRect, scroll } = elementPosition;
Expand All @@ -214,6 +208,7 @@ export class HeadlessChromiumDriver {
height: boundingClientRect.height,
width: boundingClientRect.width,
},
captureBeyondViewport: false, // workaround for an internal resize. See: https://github.com/puppeteer/puppeteer/issues/7043
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixes the blank visualizations

});

if (Buffer.isBuffer(screenshot)) {
Expand Down Expand Up @@ -263,14 +258,18 @@ export class HeadlessChromiumDriver {
await this.page.waitForFunction(fn, { timeout, polling: WAIT_FOR_DELAY_MS }, ...args);
}

/**
* Setting the viewport is required to ensure that all capture elements are visible: anything not in the
* viewport can not be captured.
*/
async setViewport(
{ width: _width, height: _height, zoom }: Viewport,
{ width: _width, height: _height, zoom }: { zoom: number; width: number; height: number },
logger: Logger
): Promise<void> {
const width = Math.floor(_width);
const height = Math.floor(_height);

logger.debug(`Setting viewport to: width=${width} height=${height} zoom=${zoom}`);
logger.debug(`Setting viewport to: width=${width} height=${height} scaleFactor=${zoom}`);

await this.page.setViewport({
width,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
* 2.0.
*/

import type { Logger } from '@kbn/core/server';
import type { ScreenshotModePluginSetup } from '@kbn/screenshot-mode-plugin/server';
import puppeteer from 'puppeteer';
import * as Rx from 'rxjs';
import { mergeMap, take } from 'rxjs/operators';
import type { Logger } from '@kbn/core/server';
import type { ScreenshotModePluginSetup } from '@kbn/screenshot-mode-plugin/server';
import { DEFAULT_VIEWPORT, HeadlessChromiumDriverFactory } from '.';
import { ConfigType } from '../../../config';
import { HeadlessChromiumDriverFactory, DEFAULT_VIEWPORT } from '.';

jest.mock('puppeteer');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,44 +5,38 @@
* 2.0.
*/

import type { Logger } from '@kbn/core/server';
import type { ScreenshotModePluginSetup } from '@kbn/screenshot-mode-plugin/server';
import { getDataPath } from '@kbn/utils';
import { spawn } from 'child_process';
import _ from 'lodash';
import del from 'del';
import fs from 'fs';
import { uniq } from 'lodash';
import path from 'path';
import puppeteer, { Browser, ConsoleMessage, HTTPRequest, Page } from 'puppeteer';
import puppeteer, { Browser, ConsoleMessage, HTTPRequest, Page, Viewport } from 'puppeteer';
import { createInterface } from 'readline';
import * as Rx from 'rxjs';
import {
catchError,
concatMap,
ignoreElements,
map,
concatMap,
mergeMap,
reduce,
takeUntil,
tap,
} from 'rxjs/operators';
import type { Logger } from '@kbn/core/server';
import type { ScreenshotModePluginSetup } from '@kbn/screenshot-mode-plugin/server';
import { ConfigType } from '../../../config';
import { errors } from '../../../../common';
import { getChromiumDisconnectedError } from '..';
import { errors } from '../../../../common';
import { ConfigType } from '../../../config';
import { safeChildProcess } from '../../safe_child_process';
import { HeadlessChromiumDriver } from '../driver';
import { args } from './args';
import { getMetrics, PerformanceMetrics } from './metrics';

interface CreatePageOptions {
browserTimezone?: string;
defaultViewport: {
/** Size in pixels */
width?: number;
/** Size in pixels */
height?: number;
};
defaultViewport: { width?: number };
openUrlTimeout: number;
}

Expand All @@ -63,10 +57,16 @@ interface ClosePageResult {
metrics?: PerformanceMetrics;
}

export const DEFAULT_VIEWPORT = {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I refined this object more to align to what Puppeteer expects, and moved it to the layouts directory of this plugin

width: 1950,
height: 1200,
};
/**
* Size of the desired initial viewport. This is needed to render the app before elements load into their
* layout. Once the elements are positioned, the viewport must be *resized* to include the entire element container.
*/
export const DEFAULT_VIEWPORT: Required<Pick<Viewport, 'width' | 'height' | 'deviceScaleFactor'>> =
{
width: 1950,
height: 1200,
deviceScaleFactor: 1,
};

// Default args used by pptr
// https://github.com/puppeteer/puppeteer/blob/13ea347/src/node/Launcher.ts#L168
Expand Down Expand Up @@ -138,6 +138,19 @@ export class HeadlessChromiumDriverFactory {

const chromiumArgs = this.getChromiumArgs();
logger.debug(`Chromium launch args set to: ${chromiumArgs}`);

// We set the viewport width using the client-side layout info to reduce the chances of
// browser reflow. Only the window height is expected to be adjusted dramatically
// before taking a screenshot, to ensure the elements to capture are contained in the viewport.
const viewport = {
...DEFAULT_VIEWPORT,
width: defaultViewport.width ?? DEFAULT_VIEWPORT.width,
};

logger.debug(
`Launching with viewport: width=${viewport.width} height=${viewport.height} scaleFactor=${viewport.deviceScaleFactor}`
);

(async () => {
let browser: Browser | undefined;
try {
Expand All @@ -148,13 +161,7 @@ export class HeadlessChromiumDriverFactory {
ignoreHTTPSErrors: true,
handleSIGHUP: false,
args: chromiumArgs,

// We optionally set this at page creation to reduce the chances of
// browser reflow. In most cases only the height needs to be adjusted
// before taking a screenshot.
// NOTE: _.defaults assigns to the target object, so we copy it.
// NOTE NOTE: _.defaults is not the same as { ...DEFAULT_VIEWPORT, ...defaultViewport }
defaultViewport: _.defaults({ ...defaultViewport }, DEFAULT_VIEWPORT),
defaultViewport: viewport,
env: {
TZ: browserTimezone,
},
Expand Down
12 changes: 7 additions & 5 deletions x-pack/plugins/screenshotting/server/browsers/mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,17 @@
*/

import { NEVER, of } from 'rxjs';
import type { HeadlessChromiumDriver, HeadlessChromiumDriverFactory } from './chromium';
import {
CONTEXT_SKIPTELEMETRY,
CONTEXT_DEBUG,
CONTEXT_ELEMENTATTRIBUTES,
CONTEXT_GETNUMBEROFITEMS,
CONTEXT_GETRENDERERRORS,
CONTEXT_GETTIMERANGE,
CONTEXT_INJECTCSS,
CONTEXT_SKIPTELEMETRY,
CONTEXT_WAITFORRENDER,
CONTEXT_GETTIMERANGE,
CONTEXT_ELEMENTATTRIBUTES,
CONTEXT_GETRENDERERRORS,
} from '../screenshots/constants';
import type { HeadlessChromiumDriver, HeadlessChromiumDriverFactory } from './chromium';

const selectors = {
renderComplete: 'renderedSelector',
Expand All @@ -40,6 +41,7 @@ function getElementsPositionAndAttributes(title: string, description: string) {
export function createMockBrowserDriver(): jest.Mocked<HeadlessChromiumDriver> {
const evaluate = jest.fn(async (_, { context }) => {
switch (context) {
case CONTEXT_DEBUG:
case CONTEXT_SKIPTELEMETRY:
case CONTEXT_INJECTCSS:
case CONTEXT_WAITFORRENDER:
Expand Down
13 changes: 10 additions & 3 deletions x-pack/plugins/screenshotting/server/layouts/base_layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,16 @@ export abstract class BaseLayout {
pageSizeParams: PageSizeParams
): CustomPageSize | PredefinedPageSize;

// Return the dimensions unscaled dimensions (before multiplying the zoom factor)
// driver.setViewport() Adds a top and left margin to the viewport, and then multiplies by the scaling factor
public abstract getViewport(itemsCount: number): ViewZoomWidthHeight | null;
/**
* Return the unscaled dimensions (before multiplying the zoom factor)
*
* `itemsCount` is only needed for the `print` layout implementation, where the number of items to capture
* affects the viewport size
*
* @param {number} [itemsCount=1] - The number of items to capture. Default is 1.
* @returns ViewZoomWidthHeight - Viewport data
*/
public abstract getViewport(itemsCount?: number): ViewZoomWidthHeight | null;

public abstract getBrowserZoom(): number;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ describe('Create Layout', () => {
},
"useReportingBranding": true,
"viewport": Object {
"deviceScaleFactor": 1,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hardened the types in this PR, to make sure the scale factor is always part of the viewport info

"height": 1200,
"width": 1950,
},
Expand Down
6 changes: 3 additions & 3 deletions x-pack/plugins/screenshotting/server/layouts/print_layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
*/

import { PageOrientation, PredefinedPageSize } from 'pdfmake/interfaces';
import type { LayoutParams, LayoutSelectorDictionary } from '../../common/layout';
import { LayoutTypes } from '../../common';
import type { Layout } from '.';
import { DEFAULT_SELECTORS } from '.';
import { LayoutTypes } from '../../common';
import type { LayoutParams, LayoutSelectorDictionary } from '../../common/layout';
import { DEFAULT_VIEWPORT } from '../browsers';
import { BaseLayout } from './base_layout';

Expand Down Expand Up @@ -40,7 +40,7 @@ export class PrintLayout extends BaseLayout implements Layout {
return this.zoom;
}

public getViewport(itemsCount: number) {
public getViewport(itemsCount = 1) {
return {
zoom: this.zoom,
width: this.viewport.width,
Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/screenshotting/server/screenshots/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { APP_WRAPPER_CLASS } from '@kbn/core/server';
export const DEFAULT_PAGELOAD_SELECTOR = `.${APP_WRAPPER_CLASS}`;

// FIXME: cleanup: remove this file and use the EventLogger's Actions enum instead
export const CONTEXT_GETNUMBEROFITEMS = 'GetNumberOfItems';
export const CONTEXT_INJECTCSS = 'InjectCss';
export const CONTEXT_WAITFORRENDER = 'WaitForRender';
Expand All @@ -17,3 +18,4 @@ export const CONTEXT_ELEMENTATTRIBUTES = 'ElementPositionAndAttributes';
export const CONTEXT_WAITFORELEMENTSTOBEINDOM = 'WaitForElementsToBeInDOM';
export const CONTEXT_SKIPTELEMETRY = 'SkipTelemetry';
export const CONTEXT_READMETADATA = 'ReadVisualizationsMetadata';
export const CONTEXT_DEBUG = 'Debug';
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,12 @@ describe('getElementPositionAndAttributes', () => {

elements.forEach((element) =>
Object.assign(element, {
scrollIntoView: () => {},
getBoundingClientRect: () => ({
width: parseFloat(element.style.width),
height: parseFloat(element.style.height),
top: parseFloat(element.style.top),
left: parseFloat(element.style.left),
y: parseFloat(element.style.top),
x: parseFloat(element.style.left),
}),
})
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,8 @@ export const getElementPositionAndAttributes = async (
results.push({
position: {
boundingClientRect: {
// modern browsers support x/y, but older ones don't
top: boundingClientRect.y || boundingClientRect.top,
left: boundingClientRect.x || boundingClientRect.left,
top: boundingClientRect.y,
left: boundingClientRect.x,
width: boundingClientRect.width,
height: boundingClientRect.height,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import { loggingSystemMock } from '@kbn/core/server/mocks';
import { createMockBrowserDriver } from '../browsers/mock';
import { ConfigType } from '../config';
import { Layout } from '../layouts';
import { createMockLayout } from '../layouts/mock';
import { EventLogger } from './event_logger';
import { getScreenshots } from './get_screenshots';

Expand All @@ -31,21 +33,23 @@ describe('getScreenshots', () => {
let browser: ReturnType<typeof createMockBrowserDriver>;
let eventLogger: EventLogger;
let config = {} as ConfigType;
let layout: Layout;

beforeEach(async () => {
browser = createMockBrowserDriver();
config = { capture: { zoom: 2 } } as ConfigType;
eventLogger = new EventLogger(loggingSystemMock.createLogger(), config);
browser.evaluate.mockImplementation(({ fn, args }) => (fn as Function)(...args));
layout = createMockLayout();
});

afterEach(() => {
jest.clearAllMocks();
});

it('should return screenshots', async () => {
await expect(getScreenshots(browser, eventLogger, elementsPositionAndAttributes)).resolves
.toMatchInlineSnapshot(`
await expect(getScreenshots(browser, eventLogger, elementsPositionAndAttributes, layout))
.resolves.toMatchInlineSnapshot(`
Array [
Object {
"data": Object {
Expand Down Expand Up @@ -90,7 +94,7 @@ describe('getScreenshots', () => {
});

it('should forward elements positions', async () => {
await getScreenshots(browser, eventLogger, elementsPositionAndAttributes);
await getScreenshots(browser, eventLogger, elementsPositionAndAttributes, layout);

expect(browser.screenshot).toHaveBeenCalledTimes(2);
expect(browser.screenshot).toHaveBeenNthCalledWith(
Expand All @@ -107,7 +111,7 @@ describe('getScreenshots', () => {
browser.screenshot.mockResolvedValue(Buffer.from(''));

await expect(
getScreenshots(browser, eventLogger, elementsPositionAndAttributes)
getScreenshots(browser, eventLogger, elementsPositionAndAttributes, layout)
).rejects.toBeInstanceOf(Error);
});
});
Loading