Reporting/fix visual warning test (#164383)

## Summary

Closes https://github.com/elastic/kibana/issues/135309

This PR eliminates a skipped functional test by replacing the test
coverage with unit tests.

*
`x-pack/plugins/screenshotting/server/screenshots/screenshots.test.ts`:
ensures that waiting too long for the URL to open will return the
expected error message
*
`x-pack/plugins/screenshotting/server/browsers/chromium/driver.test.ts`:
ensures that when the screenshot capture method is passed an error
message, that error message is injected into the screenshot

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Tim Sullivan 2023-08-22 09:20:40 -07:00 committed by GitHub
parent 7c896218dd
commit deb64c19cf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 549 additions and 343 deletions

View file

@ -353,7 +353,6 @@ enabled:
- x-pack/test/reporting_functional/reporting_and_deprecated_security.config.ts
- x-pack/test/reporting_functional/reporting_and_security.config.ts
- x-pack/test/reporting_functional/reporting_without_security.config.ts
- x-pack/test/reporting_functional/reporting_and_timeout.config.ts
- x-pack/test/rule_registry/security_and_spaces/config_basic.ts
- x-pack/test/rule_registry/security_and_spaces/config_trial.ts
- x-pack/test/rule_registry/spaces_only/config_basic.ts

View file

@ -0,0 +1,47 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
const stubDevTools = {
send: jest.fn(),
};
const stubTarget = {
createCDPSession: jest.fn(() => {
return stubDevTools;
}),
};
const stubPage = {
target: jest.fn(() => {
return stubTarget;
}),
emulateTimezone: jest.fn(),
setDefaultTimeout: jest.fn(),
isClosed: jest.fn(),
setViewport: jest.fn(),
evaluate: jest.fn(),
screenshot: jest.fn().mockResolvedValue(`you won't believe this one weird screenshot`),
evaluateOnNewDocument: jest.fn(),
setRequestInterception: jest.fn(),
_client: jest.fn(() => ({ on: jest.fn() })),
on: jest.fn(),
goto: jest.fn(),
waitForSelector: jest.fn().mockResolvedValue(true),
waitForFunction: jest.fn(),
};
const stubBrowser = {
newPage: jest.fn(() => {
return stubPage;
}),
};
const puppeteer = {
launch: jest.fn(() => {
return stubBrowser;
}),
};
// eslint-disable-next-line import/no-default-export
export default puppeteer;

View file

@ -0,0 +1,107 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { Logger } from '@kbn/logging';
import { ScreenshotModePluginSetup } from '@kbn/screenshot-mode-plugin/server';
import * as puppeteer from 'puppeteer';
import { Size } from '../../../common/layout';
import { ConfigType } from '../../config';
import { PreserveLayout } from '../../layouts/preserve_layout';
import { HeadlessChromiumDriver } from './driver';
describe('chromium driver', () => {
let mockConfig: ConfigType;
let mockLogger: Logger;
let mockScreenshotModeSetup: ScreenshotModePluginSetup;
let mockPage: puppeteer.Page;
const mockBasePath = '/kibanaTest1';
beforeEach(() => {
mockLogger = { debug: jest.fn(), error: jest.fn(), info: jest.fn() } as unknown as Logger;
mockLogger.get = () => mockLogger;
mockConfig = {
networkPolicy: {
enabled: false,
rules: [],
},
browser: {
autoDownload: false,
chromium: { proxy: { enabled: false } },
},
capture: {
timeouts: {
openUrl: 60000,
waitForElements: 60000,
renderComplete: 60000,
},
zoom: 2,
},
poolSize: 1,
};
mockPage = {
screenshot: jest.fn().mockResolvedValue(`you won't believe this one weird screenshot`),
evaluate: jest.fn(),
} as unknown as puppeteer.Page;
mockScreenshotModeSetup = {
setScreenshotContext: jest.fn(),
setScreenshotModeEnabled: jest.fn(),
isScreenshotMode: jest.fn(),
};
});
it('return screenshot with preserve layout option', async () => {
const driver = new HeadlessChromiumDriver(
mockScreenshotModeSetup,
mockConfig,
mockBasePath,
mockPage
);
const result = await driver.screenshot({
elementPosition: {
boundingClientRect: { top: 200, left: 10, height: 10, width: 100 },
scroll: { x: 100, y: 300 },
},
layout: new PreserveLayout({ width: 16, height: 16 }),
});
expect(result).toEqual(Buffer.from(`you won't believe this one weird screenshot`, 'base64'));
});
it('add error to screenshot contents', async () => {
const driver = new HeadlessChromiumDriver(
mockScreenshotModeSetup,
mockConfig,
mockBasePath,
mockPage
);
// @ts-expect-error spy on non-public class method
const testSpy = jest.spyOn(driver, 'injectScreenshottingErrorHeader');
const result = await driver.screenshot({
elementPosition: {
boundingClientRect: { top: 200, left: 10, height: 10, width: 100 },
scroll: { x: 100, y: 300 },
},
layout: new PreserveLayout({} as Size),
error: new Error(`Here's the fake error!`),
});
expect(testSpy.mock.lastCall).toMatchInlineSnapshot(`
Array [
[Error: Here's the fake error!],
"[data-shared-items-container]",
]
`);
expect(result).toEqual(Buffer.from(`you won't believe this one weird screenshot`, 'base64'));
});
});

View file

@ -6,6 +6,7 @@
*/
import type { Logger } from '@kbn/core/server';
import { loggerMock } from '@kbn/logging-mocks';
import type { ScreenshotModePluginSetup } from '@kbn/screenshot-mode-plugin/server';
import * as puppeteer from 'puppeteer';
import * as Rx from 'rxjs';
@ -24,22 +25,18 @@ describe('HeadlessChromiumDriverFactory', () => {
},
},
} as ConfigType;
let logger: jest.Mocked<Logger>;
let screenshotMode: jest.Mocked<ScreenshotModePluginSetup>;
let logger: Logger;
let screenshotMode: ScreenshotModePluginSetup;
let factory: HeadlessChromiumDriverFactory;
let mockBrowser: jest.Mocked<puppeteer.Browser>;
let mockBrowser: puppeteer.Browser;
beforeEach(async () => {
logger = {
debug: jest.fn(),
error: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
get: jest.fn(() => logger),
} as unknown as typeof logger;
screenshotMode = {} as unknown as typeof screenshotMode;
logger = loggerMock.create();
screenshotMode = {} as unknown as ScreenshotModePluginSetup;
let pageClosed = false;
mockBrowser = {
newPage: jest.fn().mockResolvedValue({
target: jest.fn(() => ({
@ -57,9 +54,8 @@ describe('HeadlessChromiumDriverFactory', () => {
pageClosed = true;
}),
process: jest.fn(),
} as unknown as jest.Mocked<puppeteer.Browser>;
(puppeteer as jest.Mocked<typeof puppeteer>).launch.mockResolvedValue(mockBrowser);
} as unknown as puppeteer.Browser;
jest.spyOn(puppeteer, 'launch').mockResolvedValue(mockBrowser);
factory = new HeadlessChromiumDriverFactory(screenshotMode, config, logger, path, '');
jest.spyOn(factory, 'getBrowserLogger').mockReturnValue(Rx.EMPTY);
@ -84,9 +80,8 @@ describe('HeadlessChromiumDriverFactory', () => {
});
it('rejects if Puppeteer launch fails', async () => {
(puppeteer as jest.Mocked<typeof puppeteer>).launch.mockRejectedValue(
`Puppeteer Launch mock fail.`
);
jest.spyOn(puppeteer, 'launch').mockRejectedValue(`Puppeteer Launch mock fail.`);
expect(() =>
factory
.createPage({ openUrlTimeout: 0, defaultViewport: DEFAULT_VIEWPORT })
@ -99,9 +94,8 @@ describe('HeadlessChromiumDriverFactory', () => {
describe('close behaviour', () => {
it('does not allow close to be called on the browse more than once', async () => {
await factory
.createPage({ openUrlTimeout: 0, defaultViewport: DEFAULT_VIEWPORT })
.pipe(
await Rx.firstValueFrom(
factory.createPage({ openUrlTimeout: 0, defaultViewport: DEFAULT_VIEWPORT }).pipe(
take(1),
mergeMap(async ({ close }) => {
expect(mockBrowser.close).not.toHaveBeenCalled();
@ -110,7 +104,7 @@ describe('HeadlessChromiumDriverFactory', () => {
expect(mockBrowser.close).toHaveBeenCalledTimes(1);
})
)
.toPromise();
);
// Check again, after the observable completes
expect(mockBrowser.close).toHaveBeenCalledTimes(1);
});

View file

@ -5,48 +5,18 @@
* 2.0.
*/
import type { CloudSetup } from '@kbn/cloud-plugin/server';
import type { HttpServiceSetup, KibanaRequest, Logger, PackageInfo } from '@kbn/core/server';
import type { KibanaRequest } from '@kbn/core/server';
import type { ExpressionAstExpression } from '@kbn/expressions-plugin/common';
import type { Optional } from '@kbn/utility-types';
import { Semaphore } from '@kbn/std';
import ipaddr from 'ipaddr.js';
import { defaultsDeep, sum } from 'lodash';
import { from, Observable, of, throwError } from 'rxjs';
import {
catchError,
concatMap,
first,
map,
mergeMap,
take,
takeUntil,
tap,
toArray,
} from 'rxjs/operators';
import {
errors,
LayoutParams,
SCREENSHOTTING_APP_ID,
SCREENSHOTTING_EXPRESSION,
SCREENSHOTTING_EXPRESSION_INPUT,
} from '../../common';
import { HeadlessChromiumDriverFactory, PerformanceMetrics } from '../browsers';
import { systemHasInsufficientMemory } from '../cloud';
import type { ConfigType } from '../config';
import { durationToNumber } from '../config';
import { LayoutParams } from '../../common';
import { PerformanceMetrics } from '../browsers';
import {
PdfScreenshotOptions,
PdfScreenshotResult,
PngScreenshotOptions,
PngScreenshotResult,
toPdf,
toPng,
} from '../formats';
import { createLayout, Layout } from '../layouts';
import { EventLogger, Transactions } from './event_logger';
import type { ScreenshotObservableOptions, ScreenshotObservableResult } from './observable';
import { ScreenshotObservableHandler, UrlOrUrlWithContext } from './observable';
export type { ScreenshotObservableResult, UrlOrUrlWithContext } from './observable';
@ -55,17 +25,14 @@ export interface CaptureOptions extends Optional<ScreenshotObservableOptions, 'u
* Expression to render. Mutually exclusive with `urls`.
*/
expression?: string | ExpressionAstExpression;
/**
* Expression input.
*/
input?: unknown;
/**
* Layout parameters.
*/
layout?: LayoutParams;
/**
* Source Kibana request object from where the headers will be extracted.
*/
@ -79,7 +46,6 @@ export interface CaptureResult {
* Collected performance metrics during the screenshotting session.
*/
metrics?: CaptureMetrics;
/**
* Screenshotting results.
*/
@ -89,163 +55,4 @@ export interface CaptureResult {
export type ScreenshotOptions = PdfScreenshotOptions | PngScreenshotOptions;
export type ScreenshotResult = PdfScreenshotResult | PngScreenshotResult;
const DEFAULT_SETUP_RESULT = {
elementsPositionAndAttributes: null,
timeRange: null,
};
export class Screenshots {
private semaphore: Semaphore;
constructor(
private readonly browserDriverFactory: HeadlessChromiumDriverFactory,
private readonly logger: Logger,
private readonly packageInfo: PackageInfo,
private readonly http: HttpServiceSetup,
private readonly config: ConfigType,
private readonly cloud?: CloudSetup
) {
this.semaphore = new Semaphore(config.poolSize);
}
private captureScreenshots(
eventLogger: EventLogger,
layout: Layout,
options: ScreenshotObservableOptions
): Observable<CaptureResult> {
const { browserTimezone } = options;
return this.browserDriverFactory
.createPage(
{
browserTimezone,
openUrlTimeout: durationToNumber(this.config.capture.timeouts.openUrl),
defaultViewport: { width: layout.width, deviceScaleFactor: layout.getBrowserZoom() },
},
this.logger
)
.pipe(
this.semaphore.acquire(),
mergeMap(({ driver, error$, close }) => {
const screen: ScreenshotObservableHandler = new ScreenshotObservableHandler(
driver,
this.config,
eventLogger,
layout,
options
);
return from(options.urls).pipe(
concatMap((url, index) =>
screen.setupPage(index, url).pipe(
catchError((error) => {
screen.checkPageIsOpen(); // this fails the job if the browser has closed
this.logger.error(error);
eventLogger.error(error, Transactions.SCREENSHOTTING);
return of({ ...DEFAULT_SETUP_RESULT, error }); // allow "as-is" screenshot with injected warning message
}),
takeUntil(error$),
screen.getScreenshots()
)
),
take(options.urls.length),
toArray(),
mergeMap((results) =>
// At this point we no longer need the page, close it and send out the results
close().pipe(map(({ metrics }) => ({ metrics, results })))
)
);
}),
first()
);
}
private getScreenshottingAppUrl() {
const info = this.http.getServerInfo();
const { protocol, port } = info;
let { hostname } = info;
if (ipaddr.isValid(hostname) && !sum(ipaddr.parse(hostname).toByteArray())) {
hostname = 'localhost';
}
return `${protocol}://${hostname}:${port}${this.http.basePath.serverBasePath}/app/${SCREENSHOTTING_APP_ID}`;
}
private getCaptureOptions({
expression,
input,
request,
...options
}: ScreenshotOptions): ScreenshotObservableOptions {
const headers = { ...(request?.headers ?? {}), ...(options.headers ?? {}) };
const urls = expression
? [
[
this.getScreenshottingAppUrl(),
{
[SCREENSHOTTING_EXPRESSION]: expression,
[SCREENSHOTTING_EXPRESSION_INPUT]: input,
},
] as UrlOrUrlWithContext,
]
: options.urls;
return defaultsDeep(
{
...options,
headers,
urls,
},
{
timeouts: {
openUrl: 60000,
waitForElements: 60000,
renderComplete: 120000,
},
urls: [],
}
);
}
systemHasInsufficientMemory(): boolean {
return systemHasInsufficientMemory(this.cloud, this.logger.get('cloud'));
}
getScreenshots(options: PngScreenshotOptions): Observable<PngScreenshotResult>;
getScreenshots(options: PdfScreenshotOptions): Observable<PdfScreenshotResult>;
getScreenshots(options: ScreenshotOptions): Observable<ScreenshotResult>;
getScreenshots(options: ScreenshotOptions): Observable<ScreenshotResult> {
if (this.systemHasInsufficientMemory()) {
return throwError(() => new errors.InsufficientMemoryAvailableOnCloudError());
}
const eventLogger = new EventLogger(this.logger, this.config);
const transactionEnd = eventLogger.startTransaction(Transactions.SCREENSHOTTING);
const layout = createLayout(options.layout ?? {});
const captureOptions = this.getCaptureOptions(options);
return this.captureScreenshots(eventLogger, layout, captureOptions).pipe(
tap(({ results, metrics }) => {
transactionEnd({
labels: {
cpu: metrics?.cpu,
memory: metrics?.memory,
memory_mb: metrics?.memoryInMegabytes,
...eventLogger.getByteLengthFromCaptureResults(results),
},
});
}),
mergeMap((result) => {
switch (options.format) {
case 'pdf':
return toPdf(eventLogger, this.packageInfo, layout, options, result);
default:
return toPng(result);
}
})
);
}
}
export { Screenshots } from './screenshots';

View file

@ -0,0 +1,166 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { CloudSetup } from '@kbn/cloud-plugin/server';
import type { HttpServiceSetup } from '@kbn/core-http-server';
import type { PackageInfo } from '@kbn/core/server';
import type { Logger } from '@kbn/logging';
import { loggerMock } from '@kbn/logging-mocks';
import type { ScreenshotModePluginSetup } from '@kbn/screenshot-mode-plugin/server';
import puppeteer from 'puppeteer';
import * as Rx from 'rxjs';
import { firstValueFrom } from 'rxjs';
import type { PngScreenshotOptions } from '..';
import { HeadlessChromiumDriverFactory } from '../browsers';
import type { ConfigType } from '../config';
import { Screenshots } from './screenshots';
jest.mock('puppeteer');
describe('class Screenshots', () => {
let mockConfig: ConfigType;
let browserDriverFactory: HeadlessChromiumDriverFactory;
let mockPackageInfo: PackageInfo;
let mockHttpSetup: HttpServiceSetup;
let mockCloudSetup: CloudSetup;
let mockLogger: Logger;
let mockScreenshotModeSetup: ScreenshotModePluginSetup;
const mockBinaryPath = '/kibana/x-pack/plugins/screenshotting/chromium/linux/headless_shell';
const mockBasePath = '/kibanaTest1';
beforeEach(() => {
mockLogger = loggerMock.create();
mockConfig = {
networkPolicy: {
enabled: false,
rules: [],
},
browser: {
autoDownload: false,
chromium: { proxy: { enabled: false } },
},
capture: {
timeouts: {
openUrl: 60000,
waitForElements: 60000,
renderComplete: 60000,
},
zoom: 2,
},
poolSize: 1,
};
mockScreenshotModeSetup = {} as unknown as ScreenshotModePluginSetup;
browserDriverFactory = new HeadlessChromiumDriverFactory(
mockScreenshotModeSetup,
mockConfig,
mockLogger,
mockBinaryPath,
mockBasePath
);
mockCloudSetup = { isCloudEnabled: true, instanceSizeMb: 8000 } as unknown as CloudSetup;
});
const getScreenshotsInstance = () =>
new Screenshots(
browserDriverFactory,
mockLogger,
mockPackageInfo,
mockHttpSetup,
mockConfig,
mockCloudSetup
);
it('detects sufficient memory from cloud plugin', () => {
const screenshotsInstance = getScreenshotsInstance();
const hasInsufficient = screenshotsInstance.systemHasInsufficientMemory();
expect(hasInsufficient).toBe(false);
});
it('detects insufficient memory from cloud plugin', () => {
mockCloudSetup = { isCloudEnabled: true, instanceSizeMb: 1000 } as unknown as CloudSetup;
const screenshotsInstance = getScreenshotsInstance();
const hasInsufficient = screenshotsInstance.systemHasInsufficientMemory();
expect(hasInsufficient).toBe(true);
});
it('ignores insufficient memory if cloud is not enabled', () => {
mockCloudSetup = { isCloudEnabled: false, instanceSizeMb: 1000 } as unknown as CloudSetup;
const screenshotsInstance = getScreenshotsInstance();
const hasInsufficient = screenshotsInstance.systemHasInsufficientMemory();
expect(hasInsufficient).toBe(false);
});
describe('getScreenshots', () => {
beforeAll(() => {
jest.mock('puppeteer'); // see __mocks__/puppeteer.ts
});
beforeEach(() => {
jest.spyOn(browserDriverFactory, 'getBrowserLogger').mockReturnValue(Rx.EMPTY);
jest.spyOn(browserDriverFactory, 'getProcessLogger').mockReturnValue(Rx.EMPTY);
jest.spyOn(browserDriverFactory, 'getPageExit').mockReturnValue(Rx.EMPTY);
});
it('getScreenshots with PngScreenshotOptions', async () => {
const screenshotsInstance = getScreenshotsInstance();
const options: PngScreenshotOptions = {
format: 'png',
layout: { id: 'preserve_layout' },
urls: ['/app/home/test'],
};
const observe = screenshotsInstance.getScreenshots(options);
await firstValueFrom(observe).then((captureResult) => {
expect(captureResult.results[0].screenshots[0].data).toEqual(
Buffer.from(`you won't believe this one weird screenshot`, 'base64')
);
expect(captureResult.results[0].renderErrors).toBe(undefined);
expect(captureResult.results[0].error).toBe(undefined);
});
});
it('adds warning to the screenshot in case of openUrl timeout', async () => {
// @ts-expect-error should not assign new value to read-only property
mockConfig.capture.timeouts.openUrl = 10; // must be a small amount of milliseconds
// mock override
const browser = await puppeteer.launch();
const page = await browser.newPage(); // should be stubPage
const pageGotoSpy = jest.spyOn(page, 'goto');
pageGotoSpy.mockImplementation(
() =>
new Promise((resolve) => {
setTimeout(resolve, 100); // must be larger than 10
})
);
const screenshotsInstance = getScreenshotsInstance();
const options: PngScreenshotOptions = {
format: 'png',
layout: { id: 'preserve_layout' },
urls: ['/app/home/test'],
};
const observe = screenshotsInstance.getScreenshots(options);
await firstValueFrom(observe).then((captureResult) => {
expect(captureResult.results[0].error).toEqual(
new Error(
`Screenshotting encountered a timeout error: "open URL" took longer than 0.01 seconds.` +
` You may need to increase "xpack.screenshotting.capture.timeouts.openUrl" in kibana.yml.`
)
);
});
});
});
});

View file

@ -0,0 +1,208 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { CloudSetup } from '@kbn/cloud-plugin/server';
import type { HttpServiceSetup, Logger, PackageInfo } from '@kbn/core/server';
import { Semaphore } from '@kbn/std';
import ipaddr from 'ipaddr.js';
import { defaultsDeep, sum } from 'lodash';
import { from, Observable, of, throwError } from 'rxjs';
import {
catchError,
concatMap,
first,
map,
mergeMap,
take,
takeUntil,
tap,
toArray,
} from 'rxjs/operators';
import { CaptureResult, ScreenshotOptions, ScreenshotResult } from '.';
import {
errors,
SCREENSHOTTING_APP_ID,
SCREENSHOTTING_EXPRESSION,
SCREENSHOTTING_EXPRESSION_INPUT,
} from '../../common';
import { HeadlessChromiumDriverFactory } from '../browsers';
import { systemHasInsufficientMemory } from '../cloud';
import type { ConfigType } from '../config';
import { durationToNumber } from '../config';
import {
PdfScreenshotOptions,
PdfScreenshotResult,
PngScreenshotOptions,
PngScreenshotResult,
toPdf,
toPng,
} from '../formats';
import { createLayout, Layout } from '../layouts';
import { EventLogger, Transactions } from './event_logger';
import type { ScreenshotObservableOptions } from './observable';
import { ScreenshotObservableHandler, UrlOrUrlWithContext } from './observable';
const DEFAULT_SETUP_RESULT = {
elementsPositionAndAttributes: null,
timeRange: null,
};
export class Screenshots {
private semaphore: Semaphore;
constructor(
private readonly browserDriverFactory: HeadlessChromiumDriverFactory,
private readonly logger: Logger,
private readonly packageInfo: PackageInfo,
private readonly http: HttpServiceSetup,
private readonly config: ConfigType,
private readonly cloud?: CloudSetup
) {
this.semaphore = new Semaphore(config.poolSize);
}
private captureScreenshots(
eventLogger: EventLogger,
layout: Layout,
options: ScreenshotObservableOptions
): Observable<CaptureResult> {
const { browserTimezone } = options;
return this.browserDriverFactory
.createPage(
{
browserTimezone,
openUrlTimeout: durationToNumber(this.config.capture.timeouts.openUrl),
defaultViewport: { width: layout.width, deviceScaleFactor: layout.getBrowserZoom() },
},
this.logger
)
.pipe(
this.semaphore.acquire(),
mergeMap(({ driver, error$, close }) => {
const screen: ScreenshotObservableHandler = new ScreenshotObservableHandler(
driver,
this.config,
eventLogger,
layout,
options
);
return from(options.urls).pipe(
concatMap((url, index) =>
screen.setupPage(index, url).pipe(
catchError((error) => {
screen.checkPageIsOpen(); // this fails the job if the browser has closed
this.logger.error(error);
eventLogger.error(error, Transactions.SCREENSHOTTING);
return of({ ...DEFAULT_SETUP_RESULT, error }); // allow "as-is" screenshot with injected warning message
}),
takeUntil(error$),
screen.getScreenshots()
)
),
take(options.urls.length),
toArray(),
mergeMap((results) =>
// At this point we no longer need the page, close it and send out the results
close().pipe(map(({ metrics }) => ({ metrics, results })))
)
);
}),
first()
);
}
private getScreenshottingAppUrl() {
const info = this.http.getServerInfo();
const { protocol, port } = info;
let { hostname } = info;
if (ipaddr.isValid(hostname) && !sum(ipaddr.parse(hostname).toByteArray())) {
hostname = 'localhost';
}
return `${protocol}://${hostname}:${port}${this.http.basePath.serverBasePath}/app/${SCREENSHOTTING_APP_ID}`;
}
private getCaptureOptions({
expression,
input,
request,
...options
}: ScreenshotOptions): ScreenshotObservableOptions {
const headers = { ...(request?.headers ?? {}), ...(options.headers ?? {}) };
const urls = expression
? [
[
this.getScreenshottingAppUrl(),
{
[SCREENSHOTTING_EXPRESSION]: expression,
[SCREENSHOTTING_EXPRESSION_INPUT]: input,
},
] as UrlOrUrlWithContext,
]
: options.urls;
return defaultsDeep(
{
...options,
headers,
urls,
},
{
timeouts: {
openUrl: 60000,
waitForElements: 60000,
renderComplete: 120000,
},
urls: [],
}
);
}
systemHasInsufficientMemory(): boolean {
return systemHasInsufficientMemory(this.cloud, this.logger.get('cloud'));
}
getScreenshots(options: PngScreenshotOptions): Observable<PngScreenshotResult>;
getScreenshots(options: PdfScreenshotOptions): Observable<PdfScreenshotResult>;
getScreenshots(options: ScreenshotOptions): Observable<ScreenshotResult>;
getScreenshots(options: ScreenshotOptions): Observable<ScreenshotResult> {
if (this.systemHasInsufficientMemory()) {
return throwError(() => new errors.InsufficientMemoryAvailableOnCloudError());
}
const eventLogger = new EventLogger(this.logger, this.config);
const transactionEnd = eventLogger.startTransaction(Transactions.SCREENSHOTTING);
const layout = createLayout(options.layout ?? {});
const captureOptions = this.getCaptureOptions(options);
return this.captureScreenshots(eventLogger, layout, captureOptions).pipe(
tap(({ results, metrics }) => {
transactionEnd({
labels: {
cpu: metrics?.cpu,
memory: metrics?.memory,
memory_mb: metrics?.memoryInMegabytes,
...eventLogger.getByteLengthFromCaptureResults(results),
},
});
}),
mergeMap((result) => {
switch (options.format) {
case 'pdf':
return toPdf(eventLogger, this.packageInfo, layout, options, result);
default:
return toPng(result);
}
})
);
}
}

View file

@ -23,6 +23,8 @@
"@kbn/utils",
"@kbn/safer-lodash-set",
"@kbn/core-logging-server-mocks",
"@kbn/logging-mocks",
"@kbn/core-http-server",
],
"exclude": [
"target/**/*",

View file

@ -1,26 +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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { FtrConfigProviderContext } from '@kbn/test';
import { resolve } from 'path';
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
const functionalConfig = await readConfigFile(require.resolve('./reporting_and_security.config'));
return {
...functionalConfig.getAll(),
junit: { reportName: 'X-Pack Reporting Functional Tests: Reports and Timeout Handling' },
testFiles: [resolve(__dirname, './reporting_and_timeout')],
kbnTestServer: {
...functionalConfig.get('kbnTestServer'),
serverArgs: [
...functionalConfig.get('kbnTestServer.serverArgs'),
`--xpack.reporting.capture.timeouts.waitForElements=1s`,
],
},
};
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

View file

@ -1,98 +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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import expect from '@kbn/expect';
import fs from 'fs';
import path from 'path';
import { PNG } from 'pngjs';
import { FtrProviderContext } from '../ftr_provider_context';
// eslint-disable-next-line import/no-default-export
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const PageObjects = getPageObjects(['common', 'reporting', 'dashboard']);
const log = getService('log');
const reportingAPI = getService('reportingAPI');
const reportingFunctional = getService('reportingFunctional');
const browser = getService('browser');
const png = getService('png');
const config = getService('config');
const screenshotDir = config.get('screenshots.directory');
// FLAKY: https://github.com/elastic/kibana/issues/135309
describe.skip('Reporting Functional Tests with forced timeout', function () {
const dashboardTitle = 'Ecom Dashboard Hidden Panel Titles';
const sessionPngFullPage = 'warnings_capture_session_a';
const sessionPngCropped = 'warnings_capture_session_b';
const baselinePng = path.resolve(__dirname, 'fixtures/baseline/warnings_capture_b.png');
let url: string;
before(async () => {
await reportingAPI.logTaskManagerHealth();
await reportingFunctional.initEcommerce();
await PageObjects.common.navigateToApp('dashboard');
await PageObjects.dashboard.loadSavedDashboard(dashboardTitle);
await PageObjects.reporting.setTimepickerInEcommerceDataRange();
await browser.setWindowSize(800, 850);
await PageObjects.reporting.openPngReportingPanel();
await PageObjects.reporting.clickGenerateReportButton();
url = await PageObjects.reporting.getReportURL(60000);
const res = await PageObjects.reporting.getResponse(url);
expect(res.status).to.equal(200);
expect(res.get('content-type')).to.equal('image/png');
});
after(async () => {
await reportingFunctional.teardownEcommerce();
});
it('adds a visual warning in the report output', async () => {
const captureData = await PageObjects.reporting.getRawPdfReportData(url);
const sessionReport = await PageObjects.reporting.writeSessionReport(
sessionPngFullPage,
'png',
captureData,
screenshotDir
);
const region = { height: 320, width: 1540, srcX: 20, srcY: 10 };
const dstPath = path.resolve(screenshotDir, sessionPngCropped + '.png');
const dst = new PNG({ width: region.width, height: region.height });
const pngSessionFilePath = await new Promise<string>((resolve) => {
fs.createReadStream(sessionReport)
.pipe(new PNG())
.on('parsed', function () {
log.info(`cropping report to the visual warning area`);
this.bitblt(dst, region.srcX, region.srcY, region.width, region.height, 0, 0);
dst.pack().pipe(fs.createWriteStream(dstPath));
resolve(dstPath);
});
});
log.info(`saved cropped file to ${dstPath}`);
expect(
await png.checkIfPngsMatch(pngSessionFilePath, baselinePng, screenshotDir)
).to.be.lessThan(0.09);
/**
* This test may fail when styling differences affect the result. To update the snapshot:
*
* 1. Run the functional test, to generate new temporary files for screenshot comparison.
* 2. Save the screenshot as the new baseline file:
* cp \
* x-pack/test/functional/screenshots/session/warnings_capture_session_b_actual.png \
* x-pack/test/reporting_functional/reporting_and_timeout/fixtures/baseline/warnings_capture_b.png
* 3. Commit the changes to the .png file
*/
});
});
}