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

[Reporting] Unit test for screenshot observable #57638

Merged
merged 9 commits into from
Feb 18, 2020
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
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
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,13 @@
/*
* 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';
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';
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 one, and the next 3 files have a lot of whitespace changes enforced by prettier. It would help to ignore whitespace changes in this PR review :)

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