Skip to content

Commit

Permalink
[Reporting] Unit test for screenshot observable (#57638)
Browse files Browse the repository at this point in the history
[Reporting] test the screenshot observable
  • Loading branch information
tsullivan authored Feb 18, 2020
1 parent e8994ab commit c47612f
Show file tree
Hide file tree
Showing 23 changed files with 684 additions and 247 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -75,7 +75,7 @@ export class PrintLayout extends Layout {
args: [this.selectors.screenshot, elementSize.height, elementSize.width],
};

await browser.evaluate(evalOptions);
await browser.evaluate(evalOptions, { context: 'PositionElements' }, logger);
}

public getPdfImageSize() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@

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';
import { CONTEXT_CHECKFORTOASTMESSAGE } from './constants';

export const checkForToastMessage = async (
browser: HeadlessBrowser,
Expand All @@ -20,13 +21,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: CONTEXT_CHECKFORTOASTMESSAGE },
logger
);

// Log an error to track the event in kibana server logs
logger.error(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* 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.
*/

export const CONTEXT_GETNUMBEROFITEMS = 'GetNumberOfItems';
export const CONTEXT_INJECTCSS = 'InjectCss';
export const CONTEXT_WAITFORRENDER = 'WaitForRender';
export const CONTEXT_GETTIMERANGE = 'GetTimeRange';
export const CONTEXT_ELEMENTATTRIBUTES = 'ElementPositionAndAttributes';
export const CONTEXT_CHECKFORTOASTMESSAGE = 'CheckForToastMessage';
export const CONTEXT_WAITFORELEMENTSTOBEINDOM = 'WaitForElementsToBeInDOM';
export const CONTEXT_SKIPTELEMETRY = 'SkipTelemetry';
Original file line number Diff line number Diff line change
Expand Up @@ -4,48 +4,55 @@
* 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';
import { CONTEXT_ELEMENTATTRIBUTES } from './constants';

export const getElementPositionAndAttributes = async (
browser: HeadlessBrowser,
layout: LayoutInstance
layout: LayoutInstance,
logger: Logger
): Promise<ElementsPositionAndAttribute[]> => {
const elementsPositionAndAttributes: ElementsPositionAndAttribute[] = await browser.evaluate({
fn: (selector, attributes) => {
const elements: NodeListOf<Element> = document.querySelectorAll(selector);
const elementsPositionAndAttributes: ElementsPositionAndAttribute[] = await browser.evaluate(
{
fn: (selector: string, attributes: any) => {
const elements: NodeListOf<Element> = 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: CONTEXT_ELEMENTATTRIBUTES },
logger
);

if (elementsPositionAndAttributes.length === 0) {
throw new Error(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
* 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 { CONTEXT_GETNUMBEROFITEMS } from './constants';

export const getNumberOfItems = async (
browser: HeadlessBrowser,
Expand All @@ -17,20 +18,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: CONTEXT_GETNUMBEROFITEMS },
logger
);

return itemsCount;
};
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
* 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 { CONTEXT_GETTIMERANGE } from './constants';
import { TimeRange } from './types';

export const getTimeRange = async (
Expand All @@ -16,23 +17,27 @@ export const getTimeRange = async (
): Promise<TimeRange | null> => {
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: CONTEXT_GETTIMERANGE },
logger
);

if (timeRange) {
logger.info(`timeRange: ${timeRange.duration}`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ScreenshotResults[]> {
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<ScreenshotResults> => {
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';
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
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';
import { CONTEXT_INJECTCSS } from './constants';

const fsp = { readFile: promisify(fs.readFile) };

Expand All @@ -21,13 +22,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: CONTEXT_INJECTCSS },
logger
);
};
Loading

0 comments on commit c47612f

Please sign in to comment.