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

This commit is contained in:
Tim Sullivan 2022-05-17 11:36:44 -07:00 committed by GitHub
parent 6375d90683
commit 8cf334468e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 615 additions and 144 deletions

View file

@ -35,12 +35,6 @@ export interface ElementPosition {
};
}
export interface Viewport {
zoom: number;
width: number;
height: number;
}
interface OpenOptions {
context?: Context;
headers: Headers;
@ -203,7 +197,7 @@ export class HeadlessChromiumDriver {
}
/*
* Call Page.screenshot and return a base64-encoded string of the image
* Receive a PNG buffer of the page screenshot from Chromium
*/
async screenshot(elementPosition: ElementPosition): Promise<Buffer | undefined> {
const { boundingClientRect, scroll } = elementPosition;
@ -214,6 +208,7 @@ export class HeadlessChromiumDriver {
height: boundingClientRect.height,
width: boundingClientRect.width,
},
captureBeyondViewport: false, // workaround for an internal resize. See: https://github.com/puppeteer/puppeteer/issues/7043
});
if (Buffer.isBuffer(screenshot)) {
@ -263,14 +258,18 @@ export class HeadlessChromiumDriver {
await this.page.waitForFunction(fn, { timeout, polling: WAIT_FOR_DELAY_MS }, ...args);
}
/**
* Setting the viewport is required to ensure that all capture elements are visible: anything not in the
* viewport can not be captured.
*/
async setViewport(
{ width: _width, height: _height, zoom }: Viewport,
{ width: _width, height: _height, zoom }: { zoom: number; width: number; height: number },
logger: Logger
): Promise<void> {
const width = Math.floor(_width);
const height = Math.floor(_height);
logger.debug(`Setting viewport to: width=${width} height=${height} zoom=${zoom}`);
logger.debug(`Setting viewport to: width=${width} height=${height} scaleFactor=${zoom}`);
await this.page.setViewport({
width,

View file

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

View file

@ -5,31 +5,30 @@
* 2.0.
*/
import type { Logger } from '@kbn/core/server';
import type { ScreenshotModePluginSetup } from '@kbn/screenshot-mode-plugin/server';
import { getDataPath } from '@kbn/utils';
import { spawn } from 'child_process';
import _ from 'lodash';
import del from 'del';
import fs from 'fs';
import { uniq } from 'lodash';
import path from 'path';
import puppeteer, { Browser, ConsoleMessage, HTTPRequest, Page } from 'puppeteer';
import puppeteer, { Browser, ConsoleMessage, HTTPRequest, Page, Viewport } from 'puppeteer';
import { createInterface } from 'readline';
import * as Rx from 'rxjs';
import {
catchError,
concatMap,
ignoreElements,
map,
concatMap,
mergeMap,
reduce,
takeUntil,
tap,
} from 'rxjs/operators';
import type { Logger } from '@kbn/core/server';
import type { ScreenshotModePluginSetup } from '@kbn/screenshot-mode-plugin/server';
import { ConfigType } from '../../../config';
import { errors } from '../../../../common';
import { getChromiumDisconnectedError } from '..';
import { errors } from '../../../../common';
import { ConfigType } from '../../../config';
import { safeChildProcess } from '../../safe_child_process';
import { HeadlessChromiumDriver } from '../driver';
import { args } from './args';
@ -37,12 +36,7 @@ import { getMetrics, PerformanceMetrics } from './metrics';
interface CreatePageOptions {
browserTimezone?: string;
defaultViewport: {
/** Size in pixels */
width?: number;
/** Size in pixels */
height?: number;
};
defaultViewport: { width?: number };
openUrlTimeout: number;
}
@ -63,10 +57,16 @@ interface ClosePageResult {
metrics?: PerformanceMetrics;
}
export const DEFAULT_VIEWPORT = {
width: 1950,
height: 1200,
};
/**
* Size of the desired initial viewport. This is needed to render the app before elements load into their
* layout. Once the elements are positioned, the viewport must be *resized* to include the entire element container.
*/
export const DEFAULT_VIEWPORT: Required<Pick<Viewport, 'width' | 'height' | 'deviceScaleFactor'>> =
{
width: 1950,
height: 1200,
deviceScaleFactor: 1,
};
// Default args used by pptr
// https://github.com/puppeteer/puppeteer/blob/13ea347/src/node/Launcher.ts#L168
@ -138,6 +138,19 @@ export class HeadlessChromiumDriverFactory {
const chromiumArgs = this.getChromiumArgs();
logger.debug(`Chromium launch args set to: ${chromiumArgs}`);
// We set the viewport width using the client-side layout info to reduce the chances of
// browser reflow. Only the window height is expected to be adjusted dramatically
// before taking a screenshot, to ensure the elements to capture are contained in the viewport.
const viewport = {
...DEFAULT_VIEWPORT,
width: defaultViewport.width ?? DEFAULT_VIEWPORT.width,
};
logger.debug(
`Launching with viewport: width=${viewport.width} height=${viewport.height} scaleFactor=${viewport.deviceScaleFactor}`
);
(async () => {
let browser: Browser | undefined;
try {
@ -148,13 +161,7 @@ export class HeadlessChromiumDriverFactory {
ignoreHTTPSErrors: true,
handleSIGHUP: false,
args: chromiumArgs,
// We optionally set this at page creation to reduce the chances of
// browser reflow. In most cases only the height needs to be adjusted
// before taking a screenshot.
// NOTE: _.defaults assigns to the target object, so we copy it.
// NOTE NOTE: _.defaults is not the same as { ...DEFAULT_VIEWPORT, ...defaultViewport }
defaultViewport: _.defaults({ ...defaultViewport }, DEFAULT_VIEWPORT),
defaultViewport: viewport,
env: {
TZ: browserTimezone,
},

View file

@ -6,16 +6,17 @@
*/
import { NEVER, of } from 'rxjs';
import type { HeadlessChromiumDriver, HeadlessChromiumDriverFactory } from './chromium';
import {
CONTEXT_SKIPTELEMETRY,
CONTEXT_GETNUMBEROFITEMS,
CONTEXT_INJECTCSS,
CONTEXT_WAITFORRENDER,
CONTEXT_GETTIMERANGE,
CONTEXT_DEBUG,
CONTEXT_ELEMENTATTRIBUTES,
CONTEXT_GETNUMBEROFITEMS,
CONTEXT_GETRENDERERRORS,
CONTEXT_GETTIMERANGE,
CONTEXT_INJECTCSS,
CONTEXT_SKIPTELEMETRY,
CONTEXT_WAITFORRENDER,
} from '../screenshots/constants';
import type { HeadlessChromiumDriver, HeadlessChromiumDriverFactory } from './chromium';
const selectors = {
renderComplete: 'renderedSelector',
@ -40,6 +41,7 @@ function getElementsPositionAndAttributes(title: string, description: string) {
export function createMockBrowserDriver(): jest.Mocked<HeadlessChromiumDriver> {
const evaluate = jest.fn(async (_, { context }) => {
switch (context) {
case CONTEXT_DEBUG:
case CONTEXT_SKIPTELEMETRY:
case CONTEXT_INJECTCSS:
case CONTEXT_WAITFORRENDER:

View file

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

View file

@ -61,6 +61,7 @@ describe('Create Layout', () => {
},
"useReportingBranding": true,
"viewport": Object {
"deviceScaleFactor": 1,
"height": 1200,
"width": 1950,
},

View file

@ -6,10 +6,10 @@
*/
import { PageOrientation, PredefinedPageSize } from 'pdfmake/interfaces';
import type { LayoutParams, LayoutSelectorDictionary } from '../../common/layout';
import { LayoutTypes } from '../../common';
import type { Layout } from '.';
import { DEFAULT_SELECTORS } from '.';
import { LayoutTypes } from '../../common';
import type { LayoutParams, LayoutSelectorDictionary } from '../../common/layout';
import { DEFAULT_VIEWPORT } from '../browsers';
import { BaseLayout } from './base_layout';
@ -40,7 +40,7 @@ export class PrintLayout extends BaseLayout implements Layout {
return this.zoom;
}
public getViewport(itemsCount: number) {
public getViewport(itemsCount = 1) {
return {
zoom: this.zoom,
width: this.viewport.width,

View file

@ -8,6 +8,7 @@
import { APP_WRAPPER_CLASS } from '@kbn/core/server';
export const DEFAULT_PAGELOAD_SELECTOR = `.${APP_WRAPPER_CLASS}`;
// FIXME: cleanup: remove this file and use the EventLogger's Actions enum instead
export const CONTEXT_GETNUMBEROFITEMS = 'GetNumberOfItems';
export const CONTEXT_INJECTCSS = 'InjectCss';
export const CONTEXT_WAITFORRENDER = 'WaitForRender';
@ -17,3 +18,4 @@ export const CONTEXT_ELEMENTATTRIBUTES = 'ElementPositionAndAttributes';
export const CONTEXT_WAITFORELEMENTSTOBEINDOM = 'WaitForElementsToBeInDOM';
export const CONTEXT_SKIPTELEMETRY = 'SkipTelemetry';
export const CONTEXT_READMETADATA = 'ReadVisualizationsMetadata';
export const CONTEXT_DEBUG = 'Debug';

View file

@ -32,11 +32,12 @@ describe('getElementPositionAndAttributes', () => {
elements.forEach((element) =>
Object.assign(element, {
scrollIntoView: () => {},
getBoundingClientRect: () => ({
width: parseFloat(element.style.width),
height: parseFloat(element.style.height),
top: parseFloat(element.style.top),
left: parseFloat(element.style.left),
y: parseFloat(element.style.top),
x: parseFloat(element.style.left),
}),
})
);

View file

@ -60,9 +60,8 @@ export const getElementPositionAndAttributes = async (
results.push({
position: {
boundingClientRect: {
// modern browsers support x/y, but older ones don't
top: boundingClientRect.y || boundingClientRect.top,
left: boundingClientRect.x || boundingClientRect.left,
top: boundingClientRect.y,
left: boundingClientRect.x,
width: boundingClientRect.width,
height: boundingClientRect.height,
},

View file

@ -8,6 +8,8 @@
import { loggingSystemMock } from '@kbn/core/server/mocks';
import { createMockBrowserDriver } from '../browsers/mock';
import { ConfigType } from '../config';
import { Layout } from '../layouts';
import { createMockLayout } from '../layouts/mock';
import { EventLogger } from './event_logger';
import { getScreenshots } from './get_screenshots';
@ -31,12 +33,14 @@ describe('getScreenshots', () => {
let browser: ReturnType<typeof createMockBrowserDriver>;
let eventLogger: EventLogger;
let config = {} as ConfigType;
let layout: Layout;
beforeEach(async () => {
browser = createMockBrowserDriver();
config = { capture: { zoom: 2 } } as ConfigType;
eventLogger = new EventLogger(loggingSystemMock.createLogger(), config);
browser.evaluate.mockImplementation(({ fn, args }) => (fn as Function)(...args));
layout = createMockLayout();
});
afterEach(() => {
@ -44,8 +48,8 @@ describe('getScreenshots', () => {
});
it('should return screenshots', async () => {
await expect(getScreenshots(browser, eventLogger, elementsPositionAndAttributes)).resolves
.toMatchInlineSnapshot(`
await expect(getScreenshots(browser, eventLogger, elementsPositionAndAttributes, layout))
.resolves.toMatchInlineSnapshot(`
Array [
Object {
"data": Object {
@ -90,7 +94,7 @@ describe('getScreenshots', () => {
});
it('should forward elements positions', async () => {
await getScreenshots(browser, eventLogger, elementsPositionAndAttributes);
await getScreenshots(browser, eventLogger, elementsPositionAndAttributes, layout);
expect(browser.screenshot).toHaveBeenCalledTimes(2);
expect(browser.screenshot).toHaveBeenNthCalledWith(
@ -107,7 +111,7 @@ describe('getScreenshots', () => {
browser.screenshot.mockResolvedValue(Buffer.from(''));
await expect(
getScreenshots(browser, eventLogger, elementsPositionAndAttributes)
getScreenshots(browser, eventLogger, elementsPositionAndAttributes, layout)
).rejects.toBeInstanceOf(Error);
});
});

View file

@ -5,15 +5,59 @@
* 2.0.
*/
import type { HeadlessChromiumDriver } from '../browsers';
import { Logger } from '@kbn/logging';
import { HeadlessChromiumDriver } from '../browsers';
import { Layout } from '../layouts';
import { Actions, EventLogger } from './event_logger';
import type { ElementsPositionAndAttribute } from './get_element_position_data';
import type { Screenshot } from './types';
/**
* Resize the viewport to contain the element to capture.
*
* @async
* @param {HeadlessChromiumDriver} browser - used for its methods to control the page
* @param {ElementsPositionAndAttribute['position']} position - position data for the element to capture
* @param {Layout} layout - used for client-side layout data from the job params
* @param {Logger} logger
*/
const resizeViewport = async (
browser: HeadlessChromiumDriver,
position: ElementsPositionAndAttribute['position'],
layout: Layout,
logger: Logger
) => {
const { boundingClientRect, scroll } = position;
// Using width from the layout is preferred, it avoids the elements moving around horizontally,
// which would invalidate the position data that was passed in.
const width = layout.width || boundingClientRect.left + scroll.x + boundingClientRect.width;
await browser.setViewport(
{
width,
height: boundingClientRect.top + scroll.y + boundingClientRect.height,
zoom: layout.getBrowserZoom(),
},
logger
);
};
/**
* Get screenshots of multiple areas of the page
*
* @async
* @param {HeadlessChromiumDriver} browser - used for its methods to control the page
* @param {EventLogger} eventLogger
* @param {ElementsPositionAndAttribute[]} elements[] - position data about all the elements to capture
* @param {Layout} layout - used for client-side layout data from the job params
* @returns {Promise<Screenshot[]>}
*/
export const getScreenshots = async (
browser: HeadlessChromiumDriver,
eventLogger: EventLogger,
elementsPositionAndAttributes: ElementsPositionAndAttribute[]
elements: ElementsPositionAndAttribute[],
layout: Layout
): Promise<Screenshot[]> => {
const { kbnLogger } = eventLogger;
kbnLogger.info(`taking screenshots`);
@ -21,16 +65,20 @@ export const getScreenshots = async (
const screenshots: Screenshot[] = [];
try {
for (let i = 0; i < elementsPositionAndAttributes.length; i++) {
const item = elementsPositionAndAttributes[i];
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
const { position, attributes } = element;
await resizeViewport(browser, position, layout, eventLogger.kbnLogger);
const endScreenshot = eventLogger.logScreenshottingEvent(
'screenshot capture',
Actions.GET_SCREENSHOT,
'read',
eventLogger.getPixelsFromElementPosition(item.position)
eventLogger.getPixelsFromElementPosition(position)
);
const data = await browser.screenshot(item.position);
const data = await browser.screenshot(position);
if (!data?.byteLength) {
throw new Error(`Failure in getScreenshots! Screenshot data is void`);
@ -38,8 +86,8 @@ export const getScreenshots = async (
screenshots.push({
data,
title: item.attributes.title,
description: item.attributes.description,
title: attributes.title,
description: attributes.description,
});
endScreenshot({ byte_length: data.byteLength });

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import type { CloudSetup } from '@kbn/cloud-plugin/server';
import type { HttpServiceSetup, KibanaRequest, Logger, PackageInfo } from '@kbn/core/server';
import type { ExpressionAstExpression } from '@kbn/expressions-plugin/common';
import type { Optional } from '@kbn/utility-types';
@ -22,16 +23,15 @@ import {
tap,
toArray,
} from 'rxjs/operators';
import type { CloudSetup } from '@kbn/cloud-plugin/server';
import {
errors,
LayoutParams,
SCREENSHOTTING_APP_ID,
SCREENSHOTTING_EXPRESSION,
SCREENSHOTTING_EXPRESSION_INPUT,
errors,
} from '../../common';
import { HeadlessChromiumDriverFactory, PerformanceMetrics } from '../browsers';
import { systemHasInsufficientMemory } from '../cloud';
import type { HeadlessChromiumDriverFactory, PerformanceMetrics } from '../browsers';
import type { ConfigType } from '../config';
import { durationToNumber } from '../config';
import {
@ -42,8 +42,7 @@ import {
toPdf,
toPng,
} from '../formats';
import type { Layout } from '../layouts';
import { createLayout } from '../layouts';
import { createLayout, Layout } from '../layouts';
import { EventLogger, Transactions } from './event_logger';
import type { ScreenshotObservableOptions, ScreenshotObservableResult } from './observable';
import { ScreenshotObservableHandler, UrlOrUrlWithContext } from './observable';
@ -109,13 +108,6 @@ export class Screenshots {
this.semaphore = new Semaphore(config.poolSize);
}
private createLayout(options: CaptureOptions): Layout {
const layout = createLayout(options.layout ?? {});
this.logger.debug(`Layout: width=${layout.width} height=${layout.height}`);
return layout;
}
private captureScreenshots(
eventLogger: EventLogger,
layout: Layout,
@ -128,7 +120,7 @@ export class Screenshots {
{
browserTimezone,
openUrlTimeout: durationToNumber(this.config.capture.timeouts.openUrl),
defaultViewport: { height: layout.height, width: layout.width },
defaultViewport: { width: layout.width },
},
this.logger
)
@ -233,7 +225,7 @@ export class Screenshots {
const eventLogger = new EventLogger(this.logger, this.config);
const transactionEnd = eventLogger.startTransaction(Transactions.SCREENSHOTTING);
const layout = this.createLayout(options);
const layout = createLayout(options.layout ?? {});
const captureOptions = this.getCaptureOptions(options);
return this.captureScreenshots(eventLogger, layout, captureOptions).pipe(

View file

@ -9,24 +9,28 @@ import type { Headers } from '@kbn/core/server';
import { defer, forkJoin, Observable, throwError } from 'rxjs';
import { catchError, mergeMap, switchMapTo, timeoutWith } from 'rxjs/operators';
import { errors, LayoutTypes } from '../../common';
import type { Context, HeadlessChromiumDriver } from '../browsers';
import { DEFAULT_VIEWPORT, getChromiumDisconnectedError } from '../browsers';
import {
Context,
DEFAULT_VIEWPORT,
getChromiumDisconnectedError,
HeadlessChromiumDriver,
} from '../browsers';
import { ConfigType, durationToNumber as toNumber } from '../config';
import type { Layout } from '../layouts';
import type { PdfScreenshotOptions } from '../formats';
import { Layout } from '../layouts';
import { Actions, EventLogger } from './event_logger';
import type { ElementsPositionAndAttribute } from './get_element_position_data';
import { getElementPositionAndAttributes } from './get_element_position_data';
import { getNumberOfItems } from './get_number_of_items';
import { getRenderErrors } from './get_render_errors';
import type { Screenshot } from './types';
import { getScreenshots } from './get_screenshots';
import { getPdf } from './get_pdf';
import { getRenderErrors } from './get_render_errors';
import { getScreenshots } from './get_screenshots';
import { getTimeRange } from './get_time_range';
import { injectCustomCss } from './inject_css';
import { openUrl } from './open_url';
import type { Screenshot } from './types';
import { waitForRenderComplete } from './wait_for_render';
import { waitForVisualizations } from './wait_for_visualizations';
import type { PdfScreenshotOptions } from '../formats';
type CaptureTimeouts = ConfigType['capture']['timeouts'];
export interface PhaseTimeouts extends CaptureTimeouts {
@ -109,15 +113,6 @@ const getDefaultElementPosition = (
];
};
/*
* If Kibana is showing a non-HTML error message, the viewport might not be
* provided by the browser.
*/
const getDefaultViewPort = () => ({
...DEFAULT_VIEWPORT,
zoom: 1,
});
export class ScreenshotObservableHandler {
constructor(
private readonly driver: HeadlessChromiumDriver,
@ -173,15 +168,9 @@ export class ScreenshotObservableHandler {
const waitTimeout = toNumber(this.config.capture.timeouts.waitForElements);
return defer(() => getNumberOfItems(driver, this.eventLogger, waitTimeout, this.layout)).pipe(
mergeMap(async (itemsCount) => {
// set the viewport to the dimensions from the job, to allow elements to flow into the expected layout
const viewport = this.layout.getViewport(itemsCount) || getDefaultViewPort();
// Set the viewport allowing time for the browser to handle reflow and redraw
// before checking for readiness of visualizations.
await driver.setViewport(viewport, this.eventLogger.kbnLogger);
await waitForVisualizations(driver, this.eventLogger, waitTimeout, itemsCount, this.layout);
}),
mergeMap((itemsCount) =>
waitForVisualizations(driver, this.eventLogger, waitTimeout, itemsCount, this.layout)
),
this.waitUntil(waitTimeout, 'wait for elements')
);
}
@ -266,7 +255,7 @@ export class ScreenshotObservableHandler {
this.checkPageIsOpen(); // fail the report job if the browser has closed
const elements =
data.elementsPositionAndAttributes ??
getDefaultElementPosition(this.layout.getViewport(1));
getDefaultElementPosition(this.layout.getViewport());
let screenshots: Screenshot[] = [];
try {
screenshots = this.shouldCapturePdf()
@ -276,7 +265,7 @@ export class ScreenshotObservableHandler {
this.getTitle(data.timeRange),
(this.options as PdfScreenshotOptions).logo
)
: await getScreenshots(this.driver, this.eventLogger, elements);
: await getScreenshots(this.driver, this.eventLogger, elements, this.layout);
} catch (e) {
throw new errors.FailedToCaptureScreenshot(e.message);
}

View file

@ -7,7 +7,7 @@
import type { Headers } from '@kbn/core/server';
import type { Context, HeadlessChromiumDriver } from '../browsers';
import { DEFAULT_PAGELOAD_SELECTOR } from './constants';
import { CONTEXT_DEBUG, DEFAULT_PAGELOAD_SELECTOR } from './constants';
import { Actions, EventLogger } from './event_logger';
export const openUrl = async (
@ -29,6 +29,27 @@ export const openUrl = async (
try {
await browser.open(url, { context, headers, waitForSelector, timeout }, kbnLogger);
// Debug logging for viewport size and resizing
await browser.evaluate(
{
fn() {
// eslint-disable-next-line no-console
console.log(
`Navigating URL with viewport size: width=${window.innerWidth} height=${window.innerHeight} scaleFactor:${window.devicePixelRatio}`
);
window.addEventListener('resize', () => {
// eslint-disable-next-line no-console
console.log(
`Detected a viewport resize: width=${window.innerWidth} height=${window.innerHeight} scaleFactor:${window.devicePixelRatio}`
);
});
},
args: [],
},
{ context: CONTEXT_DEBUG },
kbnLogger
);
} catch (err) {
kbnLogger.error(err);

View file

@ -24,7 +24,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
};
describe('Captures', () => {
it('PNG that matches the baseline', async () => {
it('PNG file matches the baseline image', async () => {
await PageObjects.common.navigateToApp(appId);
await (await testSubjects.find('shareButton')).click();

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View file

@ -24,14 +24,21 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
const reporting = getService('reporting');
const ecommerceSOPath = 'x-pack/test/functional/fixtures/kbn_archiver/reporting/ecommerce.json';
const loadEcommerce = async () => {
await esArchiver.load('x-pack/test/functional/es_archives/reporting/ecommerce');
await kibanaServer.importExport.load(ecommerceSOPath);
await kibanaServer.uiSettings.replace({
defaultIndex: '5193f870-d861-11e9-a311-0fa548c5f953',
});
};
const unloadEcommerce = async () => {
await esArchiver.unload('x-pack/test/functional/es_archives/reporting/ecommerce');
await kibanaServer.importExport.unload(ecommerceSOPath);
};
describe('Dashboard Reporting Screenshots', () => {
before('initialize tests', async () => {
await kibanaServer.uiSettings.replace({
defaultIndex: '5193f870-d861-11e9-a311-0fa548c5f953',
});
await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/reporting/ecommerce');
await kibanaServer.importExport.load(ecommerceSOPath);
await loadEcommerce();
await browser.setWindowSize(1600, 850);
await security.role.create('test_dashboard_user', {
@ -39,7 +46,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
cluster: [],
indices: [
{
names: ['ecommerce'],
names: ['ecommerce', 'kibana_sample_data_ecommerce'],
privileges: ['read'],
field_security: { grant: ['*'], except: [] },
},
@ -61,8 +68,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
]);
});
after('clean up archives', async () => {
await esArchiver.unload('x-pack/test/functional/es_archives/reporting/ecommerce');
await kibanaServer.importExport.unload(ecommerceSOPath);
await unloadEcommerce();
await es.deleteByQuery({
index: '.reporting-*',
refresh: true,
@ -88,6 +94,13 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
});
describe('Print Layout', () => {
before(async () => {
await loadEcommerce();
});
after(async () => {
await unloadEcommerce();
});
it('downloads a PDF file', async function () {
// Generating and then comparing reports can take longer than the default 60s timeout because the comparePngs
// function is taking about 15 seconds per comparison in jenkins.
@ -107,6 +120,13 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
});
describe('Print PNG button', () => {
before(async () => {
await loadEcommerce();
});
after(async () => {
await unloadEcommerce();
});
it('is available if new', async () => {
await PageObjects.common.navigateToApp('dashboard');
await PageObjects.dashboard.clickNewDashboard();
@ -123,7 +143,14 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
});
describe('PNG Layout', () => {
it('downloads a PNG file: small dashboard', async function () {
before(async () => {
await loadEcommerce();
});
after(async () => {
await unloadEcommerce();
});
it('PNG file matches the baseline: small dashboard', async function () {
this.timeout(300000);
await PageObjects.common.navigateToApp('dashboard');
@ -152,7 +179,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
expect(percentDiff).to.be.lessThan(0.09);
});
it('downloads a PNG file: large dashboard', async function () {
it('PNG file matches the baseline: large dashboard', async function () {
this.timeout(300000);
await PageObjects.common.navigateToApp('dashboard');
@ -183,6 +210,13 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
});
describe('Preserve Layout', () => {
before(async () => {
await loadEcommerce();
});
after(async () => {
await unloadEcommerce();
});
it('downloads a PDF file: small dashboard', async function () {
this.timeout(300000);
await PageObjects.common.navigateToApp('dashboard');
@ -227,5 +261,57 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
await kibanaServer.uiSettings.replace({});
});
});
describe('Sample data from Kibana 7.6', () => {
const reportFileName = 'sample_data_ecommerce_76';
let sessionReportPath: string;
before(async () => {
await kibanaServer.uiSettings.replace({
defaultIndex: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
});
await esArchiver.load('x-pack/test/functional/es_archives/reporting/ecommerce_76');
await kibanaServer.importExport.load(
'x-pack/test/functional/fixtures/kbn_archiver/reporting/ecommerce_76.json'
);
await PageObjects.common.navigateToApp('dashboard');
await PageObjects.dashboard.loadSavedDashboard('[K7.6-eCommerce] Revenue Dashboard');
await PageObjects.reporting.openPngReportingPanel();
await PageObjects.reporting.forceSharedItemsContainerSize({ width: 1405 });
await PageObjects.reporting.clickGenerateReportButton();
await PageObjects.reporting.removeForceSharedItemsContainerSize();
const url = await PageObjects.reporting.getReportURL(60000);
const reportData = await PageObjects.reporting.getRawPdfReportData(url);
sessionReportPath = await PageObjects.reporting.writeSessionReport(
reportFileName,
'png',
reportData,
REPORTS_FOLDER
);
});
after(async () => {
await esArchiver.unload('x-pack/test/functional/es_archives/reporting/ecommerce_76');
await kibanaServer.importExport.unload(
'x-pack/test/functional/fixtures/kbn_archiver/reporting/ecommerce_76.json'
);
});
it('PNG file matches the baseline image', async function () {
this.timeout(300000);
const percentDiff = await reporting.checkIfPngsMatch(
sessionReportPath,
PageObjects.reporting.getBaselineReportPath(reportFileName, 'png', REPORTS_FOLDER),
config.get('screenshots.directory'),
log
);
expect(percentDiff).to.be.lessThan(0.09);
});
});
});
}

View file

@ -16,7 +16,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
const log = getService('log');
const reporting = getService('reporting');
describe('dashboard reporting', () => {
describe('dashboard reporting: creates a map report', () => {
// helper function to check the difference between the new image and the baseline
const measurePngDifference = async (fileName: string) => {
const url = await PageObjects.reporting.getReportURL(60000);
@ -43,7 +43,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
await reporting.deleteAllReports();
});
it('creates a map report using sample geo data', async function () {
it('PNG file matches the baseline image, using sample geo data', async function () {
await reporting.initEcommerce();
await PageObjects.common.navigateToApp('dashboard');
@ -57,7 +57,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
await reporting.teardownEcommerce();
});
it('creates a map report using embeddable example', async function () {
it('PNG file matches the baseline image, using embeddable example', async function () {
await PageObjects.common.navigateToApp('dashboard');
await PageObjects.dashboard.loadSavedDashboard('map embeddable example');
await PageObjects.reporting.openPngReportingPanel();

View file

@ -6,15 +6,19 @@
*/
import expect from '@kbn/expect';
import path from 'path';
import { FtrProviderContext } from '../../ftr_provider_context';
const REPORTS_FOLDER = path.resolve(__dirname, 'reports');
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const es = getService('es');
const esArchiver = getService('esArchiver');
const browser = getService('browser');
const log = getService('log');
const config = getService('config');
const kibanaServer = getService('kibanaServer');
const ecommerceSOPath = 'x-pack/test/functional/fixtures/kbn_archiver/reporting/ecommerce.json';
const reporting = getService('reporting');
const PageObjects = getPageObjects([
'reporting',
@ -27,28 +31,42 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
describe('Visualize Reporting Screenshots', function () {
this.tags(['smoke']);
before('initialize tests', async () => {
log.debug('ReportingPage:initTests');
await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/reporting/ecommerce');
await kibanaServer.importExport.load(ecommerceSOPath);
before(async () => {
await browser.setWindowSize(1600, 850);
await kibanaServer.uiSettings.replace({
'timepicker:timeDefaults':
'{ "from": "2019-04-27T23:56:51.374Z", "to": "2019-08-23T16:18:51.821Z"}',
});
});
after('clean up archives', async () => {
await esArchiver.unload('x-pack/test/functional/es_archives/reporting/ecommerce');
await kibanaServer.importExport.unload(ecommerceSOPath);
after(async () => {
await es.deleteByQuery({
index: '.reporting-*',
refresh: true,
body: { query: { match_all: {} } },
});
await kibanaServer.uiSettings.unset('timepicker:timeDefaults');
});
describe('Print PDF button', () => {
const ecommerceSOPath =
'x-pack/test/functional/fixtures/kbn_archiver/reporting/ecommerce.json';
before('initialize tests', async () => {
log.debug('ReportingPage:initTests');
await esArchiver.load('x-pack/test/functional/es_archives/reporting/ecommerce');
await kibanaServer.importExport.load(ecommerceSOPath);
await kibanaServer.uiSettings.replace({
'timepicker:timeDefaults':
'{ "from": "2019-04-27T23:56:51.374Z", "to": "2019-08-23T16:18:51.821Z"}',
defaultIndex: '5193f870-d861-11e9-a311-0fa548c5f953',
});
});
after('clean up archives', async () => {
await esArchiver.unload('x-pack/test/functional/es_archives/reporting/ecommerce');
await kibanaServer.importExport.unload(ecommerceSOPath);
await es.deleteByQuery({
index: '.reporting-*',
refresh: true,
body: { query: { match_all: {} } },
});
await kibanaServer.uiSettings.unset('timepicker:timeDefaults');
});
it('is available if new', async () => {
await PageObjects.common.navigateToUrl('visualize', 'new', { useActualUrl: true });
await PageObjects.visualize.clickAggBasedVisualizations();
@ -66,21 +84,69 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.reporting.openPdfReportingPanel();
expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be(null);
});
});
it('downloaded PDF has OK status', async function () {
// Generating and then comparing reports can take longer than the default 60s timeout
this.timeout(180000);
describe('PNG reports: sample data created in 7.6', () => {
const reportFileName = 'tsvb';
await PageObjects.common.navigateToApp('dashboard');
await PageObjects.dashboard.loadSavedDashboard('Ecom Dashboard');
await PageObjects.reporting.openPdfReportingPanel();
before(async () => {
await kibanaServer.uiSettings.replace({
defaultIndex: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
});
await esArchiver.load('x-pack/test/functional/es_archives/reporting/ecommerce_76');
await kibanaServer.importExport.load(
'x-pack/test/functional/fixtures/kbn_archiver/reporting/ecommerce_76.json'
);
log.debug('navigate to visualize');
await PageObjects.common.navigateToApp('visualize');
});
after(async () => {
await esArchiver.unload('x-pack/test/functional/es_archives/reporting/ecommerce_76');
await kibanaServer.importExport.unload(
'x-pack/test/functional/fixtures/kbn_archiver/reporting/ecommerce_76.json'
);
});
it('TSVB Gauge: PNG file matches the baseline image', async function () {
log.debug('load saved visualization');
await PageObjects.visualize.loadSavedVisualization(
'[K7.6-eCommerce] Sold Products per Day',
{ navigateToVisualize: false }
);
log.debug('set time range');
await PageObjects.timePicker.setAbsoluteRange(
'Apr 15, 2022 @ 00:00:00.000',
'May 22, 2022 @ 00:00:00.000'
);
log.debug('open png reporting panel');
await PageObjects.reporting.openPngReportingPanel();
log.debug('click generate report button');
await PageObjects.reporting.clickGenerateReportButton();
log.debug('get the report download URL');
const url = await PageObjects.reporting.getReportURL(60000);
const res = await PageObjects.reporting.getResponse(url);
log.debug('download the report');
const reportData = await PageObjects.reporting.getRawPdfReportData(url);
const sessionReportPath = await PageObjects.reporting.writeSessionReport(
reportFileName,
'png',
reportData,
REPORTS_FOLDER
);
expect(res.status).to.equal(200);
expect(res.get('content-type')).to.equal('application/pdf');
// check the file
const percentDiff = await reporting.checkIfPngsMatch(
sessionReportPath,
PageObjects.reporting.getBaselineReportPath(reportFileName, 'png', REPORTS_FOLDER),
config.get('screenshots.directory'),
log
);
expect(percentDiff).to.be.lessThan(0.09);
});
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View file

@ -0,0 +1,219 @@
{
"type": "index",
"value": {
"aliases": {
},
"index": "kibana_sample_data_ecommerce",
"mappings": {
"properties": {
"category": {
"fields": {
"keyword": {
"type": "keyword"
}
},
"type": "text"
},
"currency": {
"type": "keyword"
},
"customer_birth_date": {
"type": "date"
},
"customer_first_name": {
"fields": {
"keyword": {
"ignore_above": 256,
"type": "keyword"
}
},
"type": "text"
},
"customer_full_name": {
"fields": {
"keyword": {
"ignore_above": 256,
"type": "keyword"
}
},
"type": "text"
},
"customer_gender": {
"type": "keyword"
},
"customer_id": {
"type": "keyword"
},
"customer_last_name": {
"fields": {
"keyword": {
"ignore_above": 256,
"type": "keyword"
}
},
"type": "text"
},
"customer_phone": {
"type": "keyword"
},
"day_of_week": {
"type": "keyword"
},
"day_of_week_i": {
"type": "integer"
},
"email": {
"type": "keyword"
},
"event": {
"properties": {
"dataset": {
"type": "keyword"
}
}
},
"geoip": {
"properties": {
"city_name": {
"type": "keyword"
},
"continent_name": {
"type": "keyword"
},
"country_iso_code": {
"type": "keyword"
},
"location": {
"type": "geo_point"
},
"region_name": {
"type": "keyword"
}
}
},
"manufacturer": {
"fields": {
"keyword": {
"type": "keyword"
}
},
"type": "text"
},
"order_date": {
"type": "date"
},
"order_id": {
"type": "keyword"
},
"products": {
"properties": {
"_id": {
"fields": {
"keyword": {
"ignore_above": 256,
"type": "keyword"
}
},
"type": "text"
},
"base_price": {
"type": "half_float"
},
"base_unit_price": {
"type": "half_float"
},
"category": {
"fields": {
"keyword": {
"type": "keyword"
}
},
"type": "text"
},
"created_on": {
"type": "date"
},
"discount_amount": {
"type": "half_float"
},
"discount_percentage": {
"type": "half_float"
},
"manufacturer": {
"fields": {
"keyword": {
"type": "keyword"
}
},
"type": "text"
},
"min_price": {
"type": "half_float"
},
"price": {
"type": "half_float"
},
"product_id": {
"type": "long"
},
"product_name": {
"analyzer": "english",
"fields": {
"keyword": {
"type": "keyword"
}
},
"type": "text"
},
"quantity": {
"type": "integer"
},
"sku": {
"type": "keyword"
},
"tax_amount": {
"type": "half_float"
},
"taxful_price": {
"type": "half_float"
},
"taxless_price": {
"type": "half_float"
},
"unit_discount_amount": {
"type": "half_float"
}
}
},
"sku": {
"type": "keyword"
},
"taxful_total_price": {
"type": "half_float"
},
"taxless_total_price": {
"type": "half_float"
},
"total_quantity": {
"type": "integer"
},
"total_unique_products": {
"type": "integer"
},
"type": {
"type": "keyword"
},
"user": {
"type": "keyword"
}
}
},
"settings": {
"index": {
"auto_expand_replicas": "0-1",
"number_of_replicas": "0",
"number_of_shards": "1"
}
}
}
}

File diff suppressed because one or more lines are too long