From 24ea6b3ee1583ecd7a41cb2f561b8a10018588a9 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Fri, 14 Feb 2020 14:17:08 -0700 Subject: [PATCH] [Reporting] test the screenshot observable commit 1c6a6b0c40ca7ddcd69ff3a2fb489b8c3acb759f Author: Timothy Sullivan Date: Thu Feb 13 14:10:40 2020 -0700 fix tests commit a3afefd0eb842060186bc29e649fadd846f895fb Merge: facd94bd61 6e4efdfa7c Author: Timothy Sullivan Date: Thu Feb 13 13:56:24 2020 -0700 Merge branch 'master' into reporting/np-server-uisettings commit facd94bd61a13235c01627e6234a5c222af6e7d7 Author: Timothy Sullivan Date: Thu Feb 13 12:16:51 2020 -0700 extract internals access to separate core class commit 2f12495121c4317391bcc3a7635cddf9c46b9b21 Merge: dd0fd40aaf 3f3969dedf Author: Timothy Sullivan Date: Thu Feb 13 09:57:07 2020 -0700 Merge branch 'master' into reporting/np-server-uisettings commit dd0fd40aaf724919d4e7353b4bd4e0828c73d515 Author: Timothy Sullivan Date: Wed Feb 12 14:44:03 2020 -0700 add more tests commit 25dc761bb0ac25a245a5fd995c37c9b483f504a3 Author: Timothy Sullivan Date: Wed Feb 12 13:54:47 2020 -0700 simplify reporting usage collector setup commit e8aa02da67ef2f0c17fedee1bcd2398bf0ef972c Author: Timothy Sullivan Date: Wed Feb 12 13:44:13 2020 -0700 consistent name for reportingPlugin commit d235b16f322f8251aa67f95abcf3938064b338b7 Author: Timothy Sullivan Date: Wed Feb 12 13:52:07 2020 -0700 Prettier changes commit e8ddc7bcaa5f28af67751b8fbffeea186a01b067 Author: Timothy Sullivan Date: Wed Feb 12 13:28:35 2020 -0700 [Reporting/New Platform] Provide async access to server-side --- .../common/layouts/print_layout.ts | 2 +- .../common/lib/screenshots/check_for_toast.ts | 18 ++- .../screenshots/get_element_position_data.ts | 72 +++++---- .../lib/screenshots/get_number_of_items.ts | 28 ++-- .../common/lib/screenshots/get_screenshots.ts | 2 +- .../common/lib/screenshots/get_time_range.ts | 32 ++-- .../common/lib/screenshots/index.ts | 90 +---------- .../common/lib/screenshots/inject_css.ts | 22 +-- .../common/lib/screenshots/observable.test.ts | 153 ++++++++++++++++++ .../common/lib/screenshots/observable.ts | 94 +++++++++++ .../common/lib/screenshots/open_url.ts | 2 +- .../common/lib/screenshots/scan_page.ts | 2 +- .../common/lib/screenshots/skip_telemetry.ts | 29 ---- .../lib/screenshots/wait_for_dom_elements.ts | 18 ++- .../common/lib/screenshots/wait_for_render.ts | 74 +++++---- .../chromium/driver/chromium_driver.ts | 53 ++++-- .../reporting/server/browsers/index.ts | 3 + .../plugins/reporting/server/types.d.ts | 9 +- .../create_mock_browserdriverfactory.ts | 126 +++++++++++++++ .../create_mock_layoutinstance.ts | 25 +++ .../plugins/reporting/test_helpers/index.ts | 2 + x-pack/legacy/plugins/reporting/types.d.ts | 5 +- 22 files changed, 597 insertions(+), 264 deletions(-) create mode 100644 x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.test.ts create mode 100644 x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.ts delete mode 100644 x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/skip_telemetry.ts create mode 100644 x-pack/legacy/plugins/reporting/test_helpers/create_mock_browserdriverfactory.ts create mode 100644 x-pack/legacy/plugins/reporting/test_helpers/create_mock_layoutinstance.ts diff --git a/x-pack/legacy/plugins/reporting/export_types/common/layouts/print_layout.ts b/x-pack/legacy/plugins/reporting/export_types/common/layouts/print_layout.ts index 44361181e3262..075a7c22969cb 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/layouts/print_layout.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/layouts/print_layout.ts @@ -6,7 +6,7 @@ import path from 'path'; import { EvaluateFn, SerializableOrJSHandle } from 'puppeteer'; import { LevelLogger } from '../../../server/lib'; -import { HeadlessChromiumDriver } from '../../../server/browsers/chromium/driver'; +import { HeadlessChromiumDriver } from '../../../server/browsers'; import { ServerFacade } from '../../../types'; import { LayoutTypes } from '../constants'; import { getDefaultLayoutSelectors, Layout, LayoutSelectorDictionary, Size } from './layout'; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/check_for_toast.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/check_for_toast.ts index 50599a927ec67..b957bb7c3eeb6 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/check_for_toast.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/check_for_toast.ts @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import { ElementHandle } from 'puppeteer'; -import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers/chromium/driver'; +import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers'; import { LevelLogger } from '../../../../server/lib'; import { LayoutInstance } from '../../layouts/layout'; @@ -20,13 +20,17 @@ export const checkForToastMessage = async ( .then(async () => { // Check for a toast message on the page. If there is one, capture the // message and throw an error, to fail the screenshot. - const toastHeaderText: string = await browser.evaluate({ - fn: selector => { - const nodeList = document.querySelectorAll(selector); - return nodeList.item(0).innerText; + const toastHeaderText: string = await browser.evaluate( + { + fn: selector => { + const nodeList = document.querySelectorAll(selector); + return nodeList.item(0).innerText; + }, + args: [layout.selectors.toastHeader], }, - args: [layout.selectors.toastHeader], - }); + { context: 'CheckForToastMessage' }, + logger + ); // Log an error to track the event in kibana server logs logger.error( diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_element_position_data.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_element_position_data.ts index ce51dc2317c79..2c30953f502df 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_element_position_data.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_element_position_data.ts @@ -4,48 +4,54 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers/chromium/driver'; +import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers'; import { LayoutInstance } from '../../layouts/layout'; import { AttributesMap, ElementsPositionAndAttribute } from './types'; +import { Logger } from '../../../../types'; export const getElementPositionAndAttributes = async ( browser: HeadlessBrowser, - layout: LayoutInstance + layout: LayoutInstance, + logger: Logger ): Promise => { - const elementsPositionAndAttributes: ElementsPositionAndAttribute[] = await browser.evaluate({ - fn: (selector, attributes) => { - const elements: NodeListOf = document.querySelectorAll(selector); + const elementsPositionAndAttributes: ElementsPositionAndAttribute[] = await browser.evaluate( + { + fn: (selector: string, attributes: any) => { + const elements: NodeListOf = document.querySelectorAll(selector); - // NodeList isn't an array, just an iterator, unable to use .map/.forEach - const results: ElementsPositionAndAttribute[] = []; - for (let i = 0; i < elements.length; i++) { - const element = elements[i]; - const boundingClientRect = element.getBoundingClientRect() as DOMRect; - results.push({ - position: { - boundingClientRect: { - // modern browsers support x/y, but older ones don't - top: boundingClientRect.y || boundingClientRect.top, - left: boundingClientRect.x || boundingClientRect.left, - width: boundingClientRect.width, - height: boundingClientRect.height, + // NodeList isn't an array, just an iterator, unable to use .map/.forEach + const results: ElementsPositionAndAttribute[] = []; + for (let i = 0; i < elements.length; i++) { + const element = elements[i]; + const boundingClientRect = element.getBoundingClientRect() as DOMRect; + results.push({ + position: { + boundingClientRect: { + // modern browsers support x/y, but older ones don't + top: boundingClientRect.y || boundingClientRect.top, + left: boundingClientRect.x || boundingClientRect.left, + width: boundingClientRect.width, + height: boundingClientRect.height, + }, + scroll: { + x: window.scrollX, + y: window.scrollY, + }, }, - scroll: { - x: window.scrollX, - y: window.scrollY, - }, - }, - attributes: Object.keys(attributes).reduce((result: AttributesMap, key) => { - const attribute = attributes[key]; - result[key] = element.getAttribute(attribute); - return result; - }, {}), - }); - } - return results; + attributes: Object.keys(attributes).reduce((result: AttributesMap, key) => { + const attribute = attributes[key]; + (result as any)[key] = element.getAttribute(attribute); + return result; + }, {} as AttributesMap), + }); + } + return results; + }, + args: [layout.selectors.screenshot, { title: 'data-title', description: 'data-description' }], }, - args: [layout.selectors.screenshot, { title: 'data-title', description: 'data-description' }], - }); + { context: 'ElementPositionAndAttributes' }, + logger + ); if (elementsPositionAndAttributes.length === 0) { throw new Error( diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_number_of_items.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_number_of_items.ts index 166d57f972a5c..ff2a26010306f 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_number_of_items.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_number_of_items.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers/chromium/driver'; +import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers'; import { LevelLogger } from '../../../../server/lib'; import { LayoutInstance } from '../../layouts/layout'; @@ -17,20 +17,24 @@ export const getNumberOfItems = async ( // returns the value of the `itemsCountAttribute` if it's there, otherwise // we just count the number of `itemSelector` - const itemsCount: number = await browser.evaluate({ - fn: (selector, countAttribute) => { - const elementWithCount = document.querySelector(`[${countAttribute}]`); - if (elementWithCount && elementWithCount != null) { - const count = elementWithCount.getAttribute(countAttribute); - if (count && count != null) { - return parseInt(count, 10); + const itemsCount: number = await browser.evaluate( + { + fn: (selector, countAttribute) => { + const elementWithCount = document.querySelector(`[${countAttribute}]`); + if (elementWithCount && elementWithCount != null) { + const count = elementWithCount.getAttribute(countAttribute); + if (count && count != null) { + return parseInt(count, 10); + } } - } - return document.querySelectorAll(selector).length; + return document.querySelectorAll(selector).length; + }, + args: [layout.selectors.renderComplete, layout.selectors.itemsCountAttribute], }, - args: [layout.selectors.renderComplete, layout.selectors.itemsCountAttribute], - }); + { context: 'GetNumberOfItems' }, + logger + ); return itemsCount; }; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_screenshots.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_screenshots.ts index ff9f4549c0d4f..b21d1e752ba3f 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_screenshots.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_screenshots.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers/chromium/driver'; +import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers'; import { LevelLogger } from '../../../../server/lib'; import { Screenshot, ElementsPositionAndAttribute } from './types'; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_time_range.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_time_range.ts index db63748c534d5..81a5cad04808b 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_time_range.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_time_range.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers/chromium/driver'; +import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers'; import { LevelLogger } from '../../../../server/lib'; import { LayoutInstance } from '../../layouts/layout'; import { TimeRange } from './types'; @@ -16,23 +16,27 @@ export const getTimeRange = async ( ): Promise => { logger.debug('getting timeRange'); - const timeRange: TimeRange | null = await browser.evaluate({ - fn: durationAttribute => { - const durationElement = document.querySelector(`[${durationAttribute}]`); + const timeRange: TimeRange | null = await browser.evaluate( + { + fn: durationAttribute => { + const durationElement = document.querySelector(`[${durationAttribute}]`); - if (!durationElement) { - return null; - } + if (!durationElement) { + return null; + } - const duration = durationElement.getAttribute(durationAttribute); - if (!duration) { - return null; - } + const duration = durationElement.getAttribute(durationAttribute); + if (!duration) { + return null; + } - return { duration }; + return { duration }; + }, + args: [layout.selectors.timefilterDurationAttribute], }, - args: [layout.selectors.timefilterDurationAttribute], - }); + { context: 'GetTimeRange' }, + logger + ); if (timeRange) { logger.info(`timeRange: ${timeRange.duration}`); diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/index.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/index.ts index 62b5e29e88ecf..5a04f1a497abf 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/index.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/index.ts @@ -4,92 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as Rx from 'rxjs'; -import { concatMap, first, mergeMap, take, toArray } from 'rxjs/operators'; -import { CaptureConfig, HeadlessChromiumDriverFactory, ServerFacade } from '../../../../types'; -import { getElementPositionAndAttributes } from './get_element_position_data'; -import { getNumberOfItems } from './get_number_of_items'; -import { getScreenshots } from './get_screenshots'; -import { getTimeRange } from './get_time_range'; -import { injectCustomCss } from './inject_css'; -import { openUrl } from './open_url'; -import { scanPage } from './scan_page'; -import { skipTelemetry } from './skip_telemetry'; -import { ScreenshotObservableOpts, ScreenshotResults } from './types'; -import { waitForElementsToBeInDOM } from './wait_for_dom_elements'; -import { waitForRenderComplete } from './wait_for_render'; - -export function screenshotsObservableFactory( - server: ServerFacade, - browserDriverFactory: HeadlessChromiumDriverFactory -) { - const config = server.config(); - const captureConfig: CaptureConfig = config.get('xpack.reporting.capture'); - - return function screenshotsObservable({ - logger, - urls, - conditionalHeaders, - layout, - browserTimezone, - }: ScreenshotObservableOpts): Rx.Observable { - const create$ = browserDriverFactory.createPage( - { viewport: layout.getBrowserViewport(), browserTimezone }, - logger - ); - return Rx.from(urls).pipe( - concatMap(url => { - return create$.pipe( - mergeMap(({ driver, exit$ }) => { - const screenshot$ = Rx.of(1).pipe( - mergeMap(() => openUrl(driver, url, conditionalHeaders, logger)), - mergeMap(() => skipTelemetry(driver, logger)), - mergeMap(() => scanPage(driver, layout, logger)), - mergeMap(() => getNumberOfItems(driver, layout, logger)), - mergeMap(async itemsCount => { - const viewport = layout.getViewport(itemsCount); - await Promise.all([ - driver.setViewport(viewport, logger), - waitForElementsToBeInDOM(driver, itemsCount, layout, logger), - ]); - }), - mergeMap(async () => { - // Waiting till _after_ elements have rendered before injecting our CSS - // allows for them to be displayed properly in many cases - await injectCustomCss(driver, layout, logger); - - if (layout.positionElements) { - // position panel elements for print layout - await layout.positionElements(driver, logger); - } - - await waitForRenderComplete(captureConfig, driver, layout, logger); - }), - mergeMap(() => getTimeRange(driver, layout, logger)), - mergeMap( - async (timeRange): Promise => { - const elementsPositionAndAttributes = await getElementPositionAndAttributes( - driver, - layout - ); - const screenshots = await getScreenshots({ - browser: driver, - elementsPositionAndAttributes, - logger, - }); - - return { timeRange, screenshots }; - } - ) - ); - - return Rx.race(screenshot$, exit$); - }), - first() - ); - }), - take(urls.length), - toArray() - ); - }; -} +export { screenshotsObservableFactory } from './observable'; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/inject_css.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/inject_css.ts index d27b6d0752cf9..b70892fb79009 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/inject_css.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/inject_css.ts @@ -7,7 +7,7 @@ import fs from 'fs'; import { promisify } from 'util'; import { LevelLogger } from '../../../../server/lib'; -import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers/chromium/driver'; +import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers'; import { Layout } from '../../layouts/layout'; const fsp = { readFile: promisify(fs.readFile) }; @@ -21,13 +21,17 @@ export const injectCustomCss = async ( const filePath = layout.getCssOverridesPath(); const buffer = await fsp.readFile(filePath); - await browser.evaluate({ - fn: css => { - const node = document.createElement('style'); - node.type = 'text/css'; - node.innerHTML = css; // eslint-disable-line no-unsanitized/property - document.getElementsByTagName('head')[0].appendChild(node); + await browser.evaluate( + { + fn: css => { + const node = document.createElement('style'); + node.type = 'text/css'; + node.innerHTML = css; // eslint-disable-line no-unsanitized/property + document.getElementsByTagName('head')[0].appendChild(node); + }, + args: [buffer.toString()], }, - args: [buffer.toString()], - }); + { context: 'InjectCss' }, + logger + ); }; 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 new file mode 100644 index 0000000000000..1f7291e39b8fc --- /dev/null +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.test.ts @@ -0,0 +1,153 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +jest.mock('../../../../server/browsers/chromium/puppeteer', () => ({ + puppeteerLaunch: () => ({ + // Fixme needs event emitters + newPage: () => ({ + setDefaultTimeout: jest.fn(), + }), + process: jest.fn(), + close: jest.fn(), + }), +})); + +import * as Rx from 'rxjs'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { loggingServiceMock } from '../../../../../../../../src/core/server/mocks'; +import { LevelLogger } from '../../../../server/lib'; +import { + createMockBrowserDriverFactory, + createMockLayoutInstance, + createMockServer, + mockSelectors, +} from '../../../../test_helpers'; +import { ConditionalHeaders } from '../../../../types'; +import { screenshotsObservableFactory } from './observable'; +import { ElementsPositionAndAttribute } from './types'; + +/* + * Mocks + */ +const mockLogger = jest.fn(loggingServiceMock.create); +const logger = new LevelLogger(mockLogger()); + +const __LEGACY = createMockServer({ settings: { 'xpack.reporting.capture': { loadDelay: 13 } } }); +const mockLayout = createMockLayoutInstance(__LEGACY); + +describe('Screenshot Observable Pipeline', () => { + let mockBrowserDriverFactory: any; + + beforeEach(async () => { + mockBrowserDriverFactory = await createMockBrowserDriverFactory(logger, {}); + }); + + it('pipelines a single url into screenshot and timeRange', async () => { + const getScreenshots$ = screenshotsObservableFactory(__LEGACY, mockBrowserDriverFactory); + const result = await getScreenshots$({ + logger, + urls: ['/welcome/home/start/index.htm'], + conditionalHeaders: {} as ConditionalHeaders, + layout: mockLayout, + browserTimezone: 'UTC', + }).toPromise(); + + expect(result).toMatchInlineSnapshot(` + Array [ + Object { + "screenshots": Array [ + Object { + "base64EncodedData": "allyourBase64 of boundingClientRect,scroll", + "description": "Default ", + "title": "Default Mock Title", + }, + ], + "timeRange": "Default GetTimeRange Result", + }, + ] + `); + }); + + it('pipelines multiple urls into', async () => { + // mock implementations + const mockScreenshot = jest.fn().mockImplementation((item: ElementsPositionAndAttribute) => { + return Promise.resolve(`allyourBase64 screenshots`); + }); + + // mocks + mockBrowserDriverFactory = await createMockBrowserDriverFactory(logger, { + screenshot: mockScreenshot, + }); + + // test + const getScreenshots$ = screenshotsObservableFactory(__LEGACY, mockBrowserDriverFactory); + const result = await getScreenshots$({ + logger, + urls: ['/welcome/home/start/index2.htm', '/welcome/home/start/index.php3?page=./home.php'], + conditionalHeaders: {} as ConditionalHeaders, + layout: mockLayout, + browserTimezone: 'UTC', + }).toPromise(); + + expect(result).toMatchInlineSnapshot(` + Array [ + Object { + "screenshots": Array [ + Object { + "base64EncodedData": "allyourBase64 screenshots", + "description": "Default ", + "title": "Default Mock Title", + }, + ], + "timeRange": "Default GetTimeRange Result", + }, + Object { + "screenshots": Array [ + Object { + "base64EncodedData": "allyourBase64 screenshots", + "description": "Default ", + "title": "Default Mock Title", + }, + ], + "timeRange": "Default GetTimeRange Result", + }, + ] + `); + }); + + it('fails if error toast message is found', async () => { + // mock implementations + const mockWaitForSelector = jest.fn().mockImplementation((selectorArg: string) => { + const { toastHeader } = mockSelectors; + if (selectorArg === toastHeader) { + return Promise.resolve(true); + } + // make the error toast message get found before anything else + return Rx.interval(100).toPromise(); + }); + + // mocks + mockBrowserDriverFactory = await createMockBrowserDriverFactory(logger, { + waitForSelector: mockWaitForSelector, + }); + + // test + const getScreenshots$ = screenshotsObservableFactory(__LEGACY, mockBrowserDriverFactory); + const getScreenshot = async () => { + return await getScreenshots$({ + logger, + urls: ['/welcome/home/start/index2.htm', '/welcome/home/start/index.php3?page=./home.php'], + conditionalHeaders: {} as ConditionalHeaders, + layout: mockLayout, + browserTimezone: 'UTC', + }).toPromise(); + }; + + await expect(getScreenshot()).rejects.toMatchInlineSnapshot( + `[Error: Encountered an unexpected message on the page: Toast Message]` + ); + }); +}); 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 new file mode 100644 index 0000000000000..3161af6d2bea1 --- /dev/null +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.ts @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as Rx from 'rxjs'; +import { concatMap, first, mergeMap, take, toArray } from 'rxjs/operators'; +import { CaptureConfig, HeadlessChromiumDriverFactory, ServerFacade } from '../../../../types'; +import { getElementPositionAndAttributes } from './get_element_position_data'; +import { getNumberOfItems } from './get_number_of_items'; +import { getScreenshots } from './get_screenshots'; +import { getTimeRange } from './get_time_range'; +import { injectCustomCss } from './inject_css'; +import { openUrl } from './open_url'; +import { scanPage } from './scan_page'; +import { ScreenshotObservableOpts, ScreenshotResults } from './types'; +import { waitForElementsToBeInDOM } from './wait_for_dom_elements'; +import { waitForRenderComplete } from './wait_for_render'; + +export function screenshotsObservableFactory( + server: ServerFacade, + browserDriverFactory: HeadlessChromiumDriverFactory +) { + const config = server.config(); + const captureConfig: CaptureConfig = config.get('xpack.reporting.capture'); + + return function screenshotsObservable({ + logger, + urls, + conditionalHeaders, + layout, + browserTimezone, + }: ScreenshotObservableOpts): Rx.Observable { + const create$ = browserDriverFactory.createPage( + { viewport: layout.getBrowserViewport(), browserTimezone }, + logger + ); + return Rx.from(urls).pipe( + concatMap(url => { + return create$.pipe( + mergeMap(({ driver, exit$ }) => { + const screenshot$ = Rx.of(1).pipe( + mergeMap(() => openUrl(driver, url, conditionalHeaders, logger)), + mergeMap(() => scanPage(driver, layout, logger)), + mergeMap(() => getNumberOfItems(driver, layout, logger)), + mergeMap(async itemsCount => { + const viewport = layout.getViewport(itemsCount); + await Promise.all([ + driver.setViewport(viewport, logger), + waitForElementsToBeInDOM(driver, itemsCount, layout, logger), + ]); + }), + mergeMap(async () => { + // Waiting till _after_ elements have rendered before injecting our CSS + // allows for them to be displayed properly in many cases + await injectCustomCss(driver, layout, logger); + + if (layout.positionElements) { + // position panel elements for print layout + await layout.positionElements(driver, logger); + } + + await waitForRenderComplete(captureConfig, driver, layout, logger); + }), + mergeMap(() => getTimeRange(driver, layout, logger)), + mergeMap( + async (timeRange): Promise => { + const elementsPositionAndAttributes = await getElementPositionAndAttributes( + driver, + layout, + logger + ); + const screenshots = await getScreenshots({ + browser: driver, + elementsPositionAndAttributes, + logger, + }); + + return { timeRange, screenshots }; + } + ) + ); + + return Rx.race(screenshot$, exit$); + }), + first() + ); + }), + take(urls.length), + toArray() + ); + }; +} diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/open_url.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/open_url.ts index 288e8b81acdc9..e465499f839f9 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/open_url.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/open_url.ts @@ -6,7 +6,7 @@ import { ConditionalHeaders } from '../../../../types'; import { LevelLogger } from '../../../../server/lib'; -import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers/chromium/driver'; +import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers'; import { WAITFOR_SELECTOR } from '../../constants'; export const openUrl = async ( diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/scan_page.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/scan_page.ts index 81ff01bb204b8..010ffe8f23afc 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/scan_page.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/scan_page.ts @@ -5,7 +5,7 @@ */ import * as Rx from 'rxjs'; -import { HeadlessChromiumDriver } from '../../../../server/browsers/chromium/driver'; +import { HeadlessChromiumDriver } from '../../../../server/browsers'; import { LevelLogger } from '../../../../server/lib'; import { LayoutInstance } from '../../layouts/layout'; import { checkForToastMessage } from './check_for_toast'; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/skip_telemetry.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/skip_telemetry.ts deleted file mode 100644 index 367354032a843..0000000000000 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/skip_telemetry.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers/chromium/driver'; -import { LevelLogger } from '../../../../server/lib'; - -const LAST_REPORT_STORAGE_KEY = 'xpack.data'; - -export async function skipTelemetry(browser: HeadlessBrowser, logger: LevelLogger) { - const storageData = await browser.evaluate({ - fn: storageKey => { - // set something - const optOutJSON = JSON.stringify({ lastReport: Date.now() }); - localStorage.setItem(storageKey, optOutJSON); - - // get it - const session = localStorage.getItem(storageKey); - - // return it - return session; - }, - args: [LAST_REPORT_STORAGE_KEY], - }); - - logger.debug(`added data to localStorage to skip telmetry: ${storageData}`); -} diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_dom_elements.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_dom_elements.ts index 3e9498179e407..e133f5c00abce 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_dom_elements.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_dom_elements.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers/chromium/driver'; +import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers'; import { LevelLogger } from '../../../../server/lib'; import { LayoutInstance } from '../../layouts/layout'; @@ -16,13 +16,17 @@ export const waitForElementsToBeInDOM = async ( ): Promise => { logger.debug(`waiting for ${itemsCount} rendered elements to be in the DOM`); - await browser.waitFor({ - fn: selector => { - return document.querySelectorAll(selector).length; + await browser.waitFor( + { + fn: selector => { + return document.querySelectorAll(selector).length; + }, + args: [layout.selectors.renderComplete], + toEqual: itemsCount, }, - args: [layout.selectors.renderComplete], - toEqual: itemsCount, - }); + { context: 'WaitForElementsToBeInDom' }, + logger + ); logger.info(`found ${itemsCount} rendered elements in the DOM`); return itemsCount; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_render.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_render.ts index df0d591ff913c..9c3267f1819f9 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_render.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_render.ts @@ -5,7 +5,7 @@ */ import { CaptureConfig } from '../../../../types'; -import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers/chromium/driver'; +import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers'; import { LevelLogger } from '../../../../server/lib'; import { LayoutInstance } from '../../layouts/layout'; @@ -18,48 +18,52 @@ export const waitForRenderComplete = async ( logger.debug('waiting for rendering to complete'); return await browser - .evaluate({ - fn: (selector, visLoadDelay) => { - // wait for visualizations to finish loading - const visualizations: NodeListOf = document.querySelectorAll(selector); - const visCount = visualizations.length; - const renderedTasks = []; + .evaluate( + { + fn: (selector, visLoadDelay) => { + // wait for visualizations to finish loading + const visualizations: NodeListOf = document.querySelectorAll(selector); + const visCount = visualizations.length; + const renderedTasks = []; - function waitForRender(visualization: Element) { - return new Promise(resolve => { - visualization.addEventListener('renderComplete', () => resolve()); - }); - } + function waitForRender(visualization: Element) { + return new Promise(resolve => { + visualization.addEventListener('renderComplete', () => resolve()); + }); + } - function waitForRenderDelay() { - return new Promise(resolve => { - setTimeout(resolve, visLoadDelay); - }); - } + function waitForRenderDelay() { + return new Promise(resolve => { + setTimeout(resolve, visLoadDelay); + }); + } - for (let i = 0; i < visCount; i++) { - const visualization = visualizations[i]; - const isRendered = visualization.getAttribute('data-render-complete'); + for (let i = 0; i < visCount; i++) { + const visualization = visualizations[i]; + const isRendered = visualization.getAttribute('data-render-complete'); - if (isRendered === 'disabled') { - renderedTasks.push(waitForRenderDelay()); - } else if (isRendered === 'false') { - renderedTasks.push(waitForRender(visualization)); + if (isRendered === 'disabled') { + renderedTasks.push(waitForRenderDelay()); + } else if (isRendered === 'false') { + renderedTasks.push(waitForRender(visualization)); + } } - } - // The renderComplete fires before the visualizations are in the DOM, so - // we wait for the event loop to flush before telling reporting to continue. This - // seems to correct a timing issue that was causing reporting to occasionally - // capture the first visualization before it was actually in the DOM. - // Note: 100 proved too short, see https://github.com/elastic/kibana/issues/22581, - // bumping to 250. - const hackyWaitForVisualizations = () => new Promise(r => setTimeout(r, 250)); + // The renderComplete fires before the visualizations are in the DOM, so + // we wait for the event loop to flush before telling reporting to continue. This + // seems to correct a timing issue that was causing reporting to occasionally + // capture the first visualization before it was actually in the DOM. + // Note: 100 proved too short, see https://github.com/elastic/kibana/issues/22581, + // bumping to 250. + const hackyWaitForVisualizations = () => new Promise(r => setTimeout(r, 250)); - return Promise.all(renderedTasks).then(hackyWaitForVisualizations); + return Promise.all(renderedTasks).then(hackyWaitForVisualizations); + }, + args: [layout.selectors.renderComplete, captureConfig.loadDelay], }, - args: [layout.selectors.renderComplete, captureConfig.loadDelay], - }) + { context: 'WaitForRender' }, + logger + ) .then(() => { logger.debug('rendering is complete'); }); 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 de8449ff29132..0592124b9897b 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 @@ -28,6 +28,15 @@ interface WaitForSelectorOpts { silent?: boolean; } +interface EvaluateOpts { + fn: EvaluateFn; + args: SerializableOrJSHandle[]; +} + +interface EvaluateMetaOpts { + context: string; +} + const WAIT_FOR_DELAY_MS: number = 100; export class HeadlessChromiumDriver { @@ -158,11 +167,15 @@ export class HeadlessChromiumDriver { return screenshot.toString('base64'); } - public async evaluate({ fn, args = [] }: { fn: EvaluateFn; args: SerializableOrJSHandle[] }) { + public async evaluate( + { fn, args = [] }: EvaluateOpts, + meta: EvaluateMetaOpts, + logger: LevelLogger + ) { + logger.debug(`evaluate ${meta.context}`); const result = await this.page.evaluate(fn, ...args); return result; } - public async waitForSelector( selector: string, opts: WaitForSelectorOpts = {}, @@ -179,10 +192,14 @@ export class HeadlessChromiumDriver { // Provide some troubleshooting info to see if we're on the login page, // "Kibana could not load correctly", etc logger.error(`waitForSelector ${selector} failed on ${this.page.url()}`); - const pageText = await this.evaluate({ - fn: () => document.querySelector('body')!.innerText, - args: [], - }); + const pageText = await this.evaluate( + { + fn: () => document.querySelector('body')!.innerText, + args: [], + }, + { context: `waitForSelector${selector}` }, + logger + ); logger.debug(`Page plain text: ${pageText.replace(/\n/g, '\\n')}`); // replace newline with escaped for single log line } throw err; @@ -192,17 +209,21 @@ export class HeadlessChromiumDriver { return resp; } - public async waitFor({ - fn, - args, - toEqual, - }: { - fn: EvaluateFn; - args: SerializableOrJSHandle[]; - toEqual: T; - }) { + public async waitFor( + { + fn, + args, + toEqual, + }: { + fn: EvaluateFn; + args: SerializableOrJSHandle[]; + toEqual: T; + }, + context: EvaluateMetaOpts, + logger: LevelLogger + ) { while (true) { - const result = await this.evaluate({ fn, args }); + const result = await this.evaluate({ fn, args }, context, logger); if (result === toEqual) { return; } diff --git a/x-pack/legacy/plugins/reporting/server/browsers/index.ts b/x-pack/legacy/plugins/reporting/server/browsers/index.ts index 402fabea56c84..1e42e2736962e 100644 --- a/x-pack/legacy/plugins/reporting/server/browsers/index.ts +++ b/x-pack/legacy/plugins/reporting/server/browsers/index.ts @@ -10,6 +10,9 @@ export { ensureAllBrowsersDownloaded } from './download'; export { createBrowserDriverFactory } from './create_browser_driver_factory'; export { getDefaultChromiumSandboxDisabled } from './default_chromium_sandbox_disabled'; +export { HeadlessChromiumDriver } from './chromium/driver'; +export { HeadlessChromiumDriverFactory } from './chromium/driver_factory'; + export const chromium = { paths: chromiumDefinition.paths, createDriverFactory: chromiumDefinition.createDriverFactory, diff --git a/x-pack/legacy/plugins/reporting/server/types.d.ts b/x-pack/legacy/plugins/reporting/server/types.d.ts index 20673423aa448..59b7bc2020ad9 100644 --- a/x-pack/legacy/plugins/reporting/server/types.d.ts +++ b/x-pack/legacy/plugins/reporting/server/types.d.ts @@ -5,17 +5,12 @@ */ import { Legacy } from 'kibana'; -import { - ElasticsearchServiceSetup, - SavedObjectsServiceStart, - UiSettingsServiceStart, -} from 'src/core/server'; +import { ElasticsearchServiceSetup } from 'src/core/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { PluginStart as DataPluginStart } from '../../../../../src/plugins/data/server'; import { SecurityPluginSetup } from '../../../../plugins/security/server'; import { XPackMainPlugin } from '../../xpack_main/server/xpack_main'; -import { EnqueueJobFn, ESQueueInstance, ReportingPluginSpecOptions } from '../types'; -import { HeadlessChromiumDriverFactory } from './browsers/chromium/driver_factory'; +import { ReportingPluginSpecOptions } from '../types'; export interface ReportingSetupDeps { elasticsearch: ElasticsearchServiceSetup; 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 new file mode 100644 index 0000000000000..b921412e43c10 --- /dev/null +++ b/x-pack/legacy/plugins/reporting/test_helpers/create_mock_browserdriverfactory.ts @@ -0,0 +1,126 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Page } from 'puppeteer'; +import * as Rx from 'rxjs'; +import { ElementsPositionAndAttribute } from '../export_types/common/lib/screenshots/types'; +import { HeadlessChromiumDriver, HeadlessChromiumDriverFactory } from '../server/browsers'; +import { createDriverFactory } from '../server/browsers/chromium'; +import { BrowserConfig, Logger, NetworkPolicy } from '../types'; + +interface CreateMockBrowserDriverFactoryOpts { + evaluate: jest.Mock, any[]>; + waitForSelector: jest.Mock, any[]>; + screenshot: jest.Mock, any[]>; + getCreatePage: (driver: HeadlessChromiumDriver) => jest.Mock; +} + +export const mockSelectors = { + renderComplete: 'renderedSelector', + itemsCountAttribute: 'itemsSelector', + screenshot: 'screenshotSelector', + timefilterDurationAttribute: 'timefilterDurationSelector', + toastHeader: 'toastHeaderSelector', +}; + +const getMockElementsPositionAndAttributes = ( + title: string, + description: string +): ElementsPositionAndAttribute[] => [ + { + position: { + boundingClientRect: { top: 0, left: 0, width: 10, height: 11 }, + scroll: { x: 0, y: 0 }, + }, + attributes: { title, description }, + }, +]; + +const mockWaitForSelector = jest.fn(); +mockWaitForSelector.mockImplementation((selectorArg: string) => { + const { renderComplete, itemsCountAttribute, toastHeader } = mockSelectors; + if (selectorArg === `${renderComplete},[${itemsCountAttribute}]`) { + return Promise.resolve(true); + } else if (selectorArg === toastHeader) { + return Rx.never().toPromise(); + } + throw new Error(selectorArg); +}); +const mockBrowserEvaluate = jest.fn(); +mockBrowserEvaluate.mockImplementation(() => { + const lastCallIndex = mockBrowserEvaluate.mock.calls.length - 1; + const { context } = mockBrowserEvaluate.mock.calls[lastCallIndex][1]; + + if (context === 'GetNumberOfItems') { + return Promise.resolve(1); + } + if (context === 'InjectCss') { + return Promise.resolve('injected css'); + } + if (context === 'WaitForRender') { + return Promise.resolve('waited render'); + } + if (context === 'GetTimeRange') { + return Promise.resolve('Default GetTimeRange Result'); + } + if (context === 'ElementPositionAndAttributes') { + return Promise.resolve(getMockElementsPositionAndAttributes('Default Mock Title', 'Default ')); + } + if (context === 'CheckForToastMessage') { + return Promise.resolve('Toast Message'); + } + throw new Error(context); +}); +const mockScreenshot = jest.fn(); +mockScreenshot.mockImplementation((item: ElementsPositionAndAttribute) => { + return Promise.resolve(`allyourBase64 of ${Object.keys(item)}`); +}); +const getCreatePage = (driver: HeadlessChromiumDriver) => + jest.fn().mockImplementation(() => Rx.of({ driver, exit$: Rx.never() })); + +const defaultOpts: CreateMockBrowserDriverFactoryOpts = { + evaluate: mockBrowserEvaluate, + waitForSelector: mockWaitForSelector, + screenshot: mockScreenshot, + getCreatePage, +}; + +export const createMockBrowserDriverFactory = async ( + logger: Logger, + opts: Partial +): Promise => { + const browserConfig = { + inspect: true, + userDataDir: '/usr/data/dir', + viewport: { width: 12, height: 12 }, + disableSandbox: false, + proxy: { enabled: false }, + } as BrowserConfig; + + const binaryPath = '/usr/local/share/common/secure/'; + const queueTimeout = 55; + const networkPolicy = {} as NetworkPolicy; + + const mockBrowserDriverFactory = await createDriverFactory( + binaryPath, + logger, + browserConfig, + queueTimeout, + networkPolicy + ); + + const mockPage = {} as Page; + const mockBrowserDriver = new HeadlessChromiumDriver(mockPage, { inspect: true, networkPolicy }); + + // mock the driver methods as either default mocks or passed-in + mockBrowserDriver.waitForSelector = opts.waitForSelector ? opts.waitForSelector : defaultOpts.waitForSelector; // prettier-ignore + mockBrowserDriver.evaluate = opts.evaluate ? opts.evaluate : defaultOpts.evaluate; + mockBrowserDriver.screenshot = opts.screenshot ? opts.screenshot : defaultOpts.screenshot; + + mockBrowserDriverFactory.createPage = getCreatePage(mockBrowserDriver); + + return mockBrowserDriverFactory; +}; 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 new file mode 100644 index 0000000000000..a2eb03c3fe300 --- /dev/null +++ b/x-pack/legacy/plugins/reporting/test_helpers/create_mock_layoutinstance.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createLayout } from '../export_types/common/layouts'; +import { LayoutTypes } from '../export_types/common/constants'; +import { LayoutInstance } from '../export_types/common/layouts/layout'; +import { ServerFacade } from '../types'; + +export const createMockLayoutInstance = (__LEGACY: ServerFacade) => { + const mockLayout = createLayout(__LEGACY, { + id: LayoutTypes.PRESERVE_LAYOUT, + dimensions: { height: 12, width: 12 }, + }) as LayoutInstance; + mockLayout.selectors = { + renderComplete: 'renderedSelector', + itemsCountAttribute: 'itemsSelector', + screenshot: 'screenshotSelector', + timefilterDurationAttribute: 'timefilterDurationSelector', + toastHeader: 'toastHeaderSelector', + }; + return mockLayout; +}; diff --git a/x-pack/legacy/plugins/reporting/test_helpers/index.ts b/x-pack/legacy/plugins/reporting/test_helpers/index.ts index 7fbc5661d5211..91c348ba1db3d 100644 --- a/x-pack/legacy/plugins/reporting/test_helpers/index.ts +++ b/x-pack/legacy/plugins/reporting/test_helpers/index.ts @@ -6,3 +6,5 @@ export { createMockServer } from './create_mock_server'; export { createMockReportingCore } from './create_mock_reportingplugin'; +export { createMockBrowserDriverFactory, mockSelectors } from './create_mock_browserdriverfactory'; +export { createMockLayoutInstance } from './create_mock_layoutinstance'; diff --git a/x-pack/legacy/plugins/reporting/types.d.ts b/x-pack/legacy/plugins/reporting/types.d.ts index 1549c173b3d6e..38406186c8173 100644 --- a/x-pack/legacy/plugins/reporting/types.d.ts +++ b/x-pack/legacy/plugins/reporting/types.d.ts @@ -311,8 +311,9 @@ export interface ExportTypeDefinition< } export { CancellationToken } from './common/cancellation_token'; -export { HeadlessChromiumDriver } from './server/browsers/chromium/driver'; -export { HeadlessChromiumDriverFactory } from './server/browsers/chromium/driver_factory'; + +export { HeadlessChromiumDriver, HeadlessChromiumDriverFactory } from './server/browsers'; + export { ExportTypesRegistry } from './server/lib/export_types_registry'; // Prefer to import this type using: `import { LevelLogger } from 'relative/path/server/lib';` export { LevelLogger as Logger };