diff --git a/x-pack/legacy/plugins/reporting/export_types/common/layouts/layout.ts b/x-pack/legacy/plugins/reporting/export_types/common/layouts/layout.ts index 2c43517dbcaa9..5cd2f3e636a93 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/layouts/layout.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/layouts/layout.ts @@ -54,7 +54,7 @@ export abstract class Layout { public abstract getPdfPageSize(pageSizeParams: PageSizeParams): string | Size; - public abstract getViewport(itemsCount: number): ViewZoomWidthHeight; + public abstract getViewport(itemsCount: number): ViewZoomWidthHeight | null; public abstract getBrowserZoom(): number; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.test.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.test.ts index 75ac3dca4ffa0..68d660257a56d 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.test.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.test.ts @@ -22,6 +22,7 @@ import { LevelLogger } from '../../../../server/lib'; import { createMockBrowserDriverFactory, createMockLayoutInstance } from '../../../../test_helpers'; import { ConditionalHeaders, HeadlessChromiumDriver } from '../../../../types'; import { CaptureConfig } from '../../../../server/types'; +import * as contexts from './constants'; import { screenshotsObservableFactory } from './observable'; import { ElementsPositionAndAttribute } from './types'; @@ -57,10 +58,30 @@ describe('Screenshot Observable Pipeline', () => { expect(result).toMatchInlineSnapshot(` Array [ Object { + "elementsPositionAndAttributes": Array [ + Object { + "attributes": Object { + "description": "Default ", + "title": "Default Mock Title", + }, + "position": Object { + "boundingClientRect": Object { + "height": 600, + "left": 0, + "top": 0, + "width": 800, + }, + "scroll": Object { + "x": 0, + "y": 0, + }, + }, + }, + ], "error": undefined, "screenshots": Array [ Object { - "base64EncodedData": "allyourBase64 of boundingClientRect,scroll", + "base64EncodedData": "allyourBase64", "description": "Default ", "title": "Default Mock Title", }, @@ -95,6 +116,26 @@ describe('Screenshot Observable Pipeline', () => { expect(result).toMatchInlineSnapshot(` Array [ Object { + "elementsPositionAndAttributes": Array [ + Object { + "attributes": Object { + "description": "Default ", + "title": "Default Mock Title", + }, + "position": Object { + "boundingClientRect": Object { + "height": 600, + "left": 0, + "top": 0, + "width": 800, + }, + "scroll": Object { + "x": 0, + "y": 0, + }, + }, + }, + ], "error": undefined, "screenshots": Array [ Object { @@ -106,6 +147,26 @@ describe('Screenshot Observable Pipeline', () => { "timeRange": "Default GetTimeRange Result", }, Object { + "elementsPositionAndAttributes": Array [ + Object { + "attributes": Object { + "description": "Default ", + "title": "Default Mock Title", + }, + "position": Object { + "boundingClientRect": Object { + "height": 600, + "left": 0, + "top": 0, + "width": 800, + }, + "scroll": Object { + "x": 0, + "y": 0, + }, + }, + }, + ], "error": undefined, "screenshots": Array [ Object { @@ -150,10 +211,27 @@ describe('Screenshot Observable Pipeline', () => { await expect(getScreenshot()).resolves.toMatchInlineSnapshot(` Array [ Object { + "elementsPositionAndAttributes": Array [ + Object { + "attributes": Object {}, + "position": Object { + "boundingClientRect": Object { + "height": 200, + "left": 0, + "top": 0, + "width": 200, + }, + "scroll": Object { + "x": 0, + "y": 0, + }, + }, + }, + ], "error": [Error: An error occurred when trying to read the page for visualization panel info. You may need to increase 'xpack.reporting.capture.timeouts.waitForElements'. Error: Mock error!], "screenshots": Array [ Object { - "base64EncodedData": "allyourBase64 of boundingClientRect,scroll", + "base64EncodedData": "allyourBase64", "description": undefined, "title": undefined, }, @@ -161,10 +239,27 @@ describe('Screenshot Observable Pipeline', () => { "timeRange": null, }, Object { + "elementsPositionAndAttributes": Array [ + Object { + "attributes": Object {}, + "position": Object { + "boundingClientRect": Object { + "height": 200, + "left": 0, + "top": 0, + "width": 200, + }, + "scroll": Object { + "x": 0, + "y": 0, + }, + }, + }, + ], "error": [Error: An error occurred when trying to read the page for visualization panel info. You may need to increase 'xpack.reporting.capture.timeouts.waitForElements'. Error: Mock error!], "screenshots": Array [ Object { - "base64EncodedData": "allyourBase64 of boundingClientRect,scroll", + "base64EncodedData": "allyourBase64", "description": undefined, "title": undefined, }, @@ -208,10 +303,27 @@ describe('Screenshot Observable Pipeline', () => { await expect(getScreenshot()).resolves.toMatchInlineSnapshot(` Array [ Object { + "elementsPositionAndAttributes": Array [ + Object { + "attributes": Object {}, + "position": Object { + "boundingClientRect": Object { + "height": 200, + "left": 0, + "top": 0, + "width": 200, + }, + "scroll": Object { + "x": 0, + "y": 0, + }, + }, + }, + ], "error": "Instant timeout has fired!", "screenshots": Array [ Object { - "base64EncodedData": "allyourBase64 of boundingClientRect,scroll", + "base64EncodedData": "allyourBase64", "description": undefined, "title": undefined, }, @@ -221,5 +333,69 @@ describe('Screenshot Observable Pipeline', () => { ] `); }); + + it(`uses defaults for element positions and size when Kibana page is not ready`, async () => { + // mocks + const mockBrowserEvaluate = jest.fn(); + mockBrowserEvaluate.mockImplementation(() => { + const lastCallIndex = mockBrowserEvaluate.mock.calls.length - 1; + const { context: mockCall } = mockBrowserEvaluate.mock.calls[lastCallIndex][1]; + + if (mockCall === contexts.CONTEXT_ELEMENTATTRIBUTES) { + return Promise.resolve(null); + } else { + return Promise.resolve(); + } + }); + mockBrowserDriverFactory = await createMockBrowserDriverFactory(logger, { + evaluate: mockBrowserEvaluate, + }); + mockLayout.getViewport = () => null; + + // test + const getScreenshots$ = screenshotsObservableFactory(mockConfig, mockBrowserDriverFactory); + const getScreenshot = async () => { + return await getScreenshots$({ + logger, + urls: ['/welcome/home/start/index.php3?page=./home.php3'], + conditionalHeaders: {} as ConditionalHeaders, + layout: mockLayout, + browserTimezone: 'UTC', + }).toPromise(); + }; + + await expect(getScreenshot()).resolves.toMatchInlineSnapshot(` + Array [ + Object { + "elementsPositionAndAttributes": Array [ + Object { + "attributes": Object {}, + "position": Object { + "boundingClientRect": Object { + "height": 1200, + "left": 0, + "top": 0, + "width": 1800, + }, + "scroll": Object { + "x": 0, + "y": 0, + }, + }, + }, + ], + "error": undefined, + "screenshots": Array [ + Object { + "base64EncodedData": "allyourBase64", + "description": undefined, + "title": undefined, + }, + ], + "timeRange": undefined, + }, + ] + `); + }); }); }); diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.ts index 53a11c18abd79..519a3289395b9 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.ts @@ -18,6 +18,9 @@ import { ScreenSetupData, ScreenshotObservableOpts, ScreenshotResults } from './ import { waitForRenderComplete } from './wait_for_render'; import { waitForVisualizations } from './wait_for_visualizations'; +const DEFAULT_SCREENSHOT_CLIP_HEIGHT = 1200; +const DEFAULT_SCREENSHOT_CLIP_WIDTH = 1800; + export function screenshotsObservableFactory( captureConfig: CaptureConfig, browserDriverFactory: HeadlessChromiumDriverFactory @@ -42,7 +45,7 @@ export function screenshotsObservableFactory( mergeMap(() => openUrl(captureConfig, driver, url, conditionalHeaders, logger)), mergeMap(() => getNumberOfItems(captureConfig, driver, layout, logger)), mergeMap(async itemsCount => { - const viewport = layout.getViewport(itemsCount); + const viewport = layout.getViewport(itemsCount) || getDefaultViewPort(); await Promise.all([ driver.setViewport(viewport, logger), waitForVisualizations(captureConfig, driver, itemsCount, layout, logger), @@ -83,7 +86,12 @@ export function screenshotsObservableFactory( : getDefaultElementPosition(layout.getViewport(1)); const screenshots = await getScreenshots(driver, elements, logger); const { timeRange, error: setupError } = data; - return { timeRange, screenshots, error: setupError }; + return { + timeRange, + screenshots, + error: setupError, + elementsPositionAndAttributes: elements, + }; } ) ); @@ -97,17 +105,30 @@ export function screenshotsObservableFactory( }; } +/* + * If Kibana is showing a non-HTML error message, the viewport might not be + * provided by the browser. + */ +const getDefaultViewPort = () => ({ + height: DEFAULT_SCREENSHOT_CLIP_HEIGHT, + width: DEFAULT_SCREENSHOT_CLIP_WIDTH, + zoom: 1, +}); /* * If an error happens setting up the page, we don't know if there actually * are any visualizations showing. These defaults should help capture the page * enough for the user to see the error themselves */ -const getDefaultElementPosition = ({ height, width }: { height: number; width: number }) => [ - { +const getDefaultElementPosition = (dimensions: { height?: number; width?: number } | null) => { + const height = dimensions?.height || DEFAULT_SCREENSHOT_CLIP_HEIGHT; + const width = dimensions?.width || DEFAULT_SCREENSHOT_CLIP_WIDTH; + + const defaultObject = { position: { boundingClientRect: { top: 0, left: 0, height, width }, scroll: { x: 0, y: 0 }, }, attributes: {}, - }, -]; + }; + return [defaultObject]; +}; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/types.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/types.ts index 76613c2d631d6..e113a5d228cd7 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/types.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/types.ts @@ -45,4 +45,5 @@ export interface ScreenshotResults { timeRange: TimeRange | null; screenshots: Screenshot[]; error?: Error; + elementsPositionAndAttributes?: ElementsPositionAndAttribute[]; // NOTE: for testing } diff --git a/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts b/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts index 4b80e129c04da..dfaa87021c31c 100644 --- a/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts +++ b/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts @@ -190,19 +190,14 @@ export class HeadlessChromiumDriver { } public async screenshot(elementPosition: ElementPosition): Promise { - let clip; - if (elementPosition) { - const { boundingClientRect, scroll = { x: 0, y: 0 } } = elementPosition; - clip = { + const { boundingClientRect, scroll } = elementPosition; + const screenshot = await this.page.screenshot({ + clip: { x: boundingClientRect.left + scroll.x, y: boundingClientRect.top + scroll.y, height: boundingClientRect.height, width: boundingClientRect.width, - }; - } - - const screenshot = await this.page.screenshot({ - clip, + }, }); return screenshot.toString('base64'); diff --git a/x-pack/legacy/plugins/reporting/test_helpers/create_mock_browserdriverfactory.ts b/x-pack/legacy/plugins/reporting/test_helpers/create_mock_browserdriverfactory.ts index 6e95bed2ecf92..1be10f6a2056f 100644 --- a/x-pack/legacy/plugins/reporting/test_helpers/create_mock_browserdriverfactory.ts +++ b/x-pack/legacy/plugins/reporting/test_helpers/create_mock_browserdriverfactory.ts @@ -34,7 +34,7 @@ const getMockElementsPositionAndAttributes = ( ): ElementsPositionAndAttribute[] => [ { position: { - boundingClientRect: { top: 0, left: 0, width: 10, height: 11 }, + boundingClientRect: { top: 0, left: 0, width: 800, height: 600 }, scroll: { x: 0, y: 0 }, }, attributes: { title, description }, @@ -78,7 +78,7 @@ mockBrowserEvaluate.mockImplementation(() => { }); const mockScreenshot = jest.fn(); mockScreenshot.mockImplementation((item: ElementsPositionAndAttribute) => { - return Promise.resolve(`allyourBase64 of ${Object.keys(item)}`); + return Promise.resolve(`allyourBase64`); }); const getCreatePage = (driver: HeadlessChromiumDriver) => jest.fn().mockImplementation(() => Rx.of({ driver, exit$: Rx.never() })); @@ -94,31 +94,23 @@ export const createMockBrowserDriverFactory = async ( logger: Logger, opts: Partial = {} ): Promise => { - const captureConfig = { + const captureConfig: CaptureConfig = { timeouts: { openUrl: 30000, waitForElements: 30000, renderComplete: 30000 }, browser: { type: 'chromium', chromium: { inspect: false, disableSandbox: false, - userDataDir: '/usr/data/dir', - viewport: { width: 12, height: 12 }, proxy: { enabled: false, server: undefined, bypass: undefined }, }, autoDownload: false, - inspect: true, - userDataDir: '/usr/data/dir', - viewport: { width: 12, height: 12 }, - disableSandbox: false, - proxy: { enabled: false, server: undefined, bypass: undefined }, - maxScreenshotDimension: undefined, }, networkPolicy: { enabled: true, rules: [] }, viewport: { width: 800, height: 600 }, loadDelay: 2000, - zoom: 1, + zoom: 2, maxAttempts: 1, - } as CaptureConfig; + }; const binaryPath = '/usr/local/share/common/secure/'; const mockBrowserDriverFactory = await createDriverFactory(binaryPath, logger, captureConfig); diff --git a/x-pack/legacy/plugins/reporting/test_helpers/create_mock_layoutinstance.ts b/x-pack/legacy/plugins/reporting/test_helpers/create_mock_layoutinstance.ts index be60b56dcc0c1..81090e7616501 100644 --- a/x-pack/legacy/plugins/reporting/test_helpers/create_mock_layoutinstance.ts +++ b/x-pack/legacy/plugins/reporting/test_helpers/create_mock_layoutinstance.ts @@ -12,7 +12,7 @@ import { CaptureConfig } from '../server/types'; export const createMockLayoutInstance = (captureConfig: CaptureConfig) => { const mockLayout = createLayout(captureConfig, { id: LayoutTypes.PRESERVE_LAYOUT, - dimensions: { height: 12, width: 12 }, + dimensions: { height: 100, width: 100 }, }) as LayoutInstance; mockLayout.selectors = { renderComplete: 'renderedSelector',