mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Reporting] Add new data-render-error
attribute (#114472)
* added new data-render-error attribute, read it and store it on job object * added data-render-error to the example app * added jest test * address pr feedback - make renderErrors optional in interfaces - create separate selectors for data render error selector/attr - Tidy up mergeMap behaviour * fix observable.test.ts snapshots and browser driver mock * updated jest snapshots Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
2afb43b869
commit
45e07af1fa
14 changed files with 191 additions and 4 deletions
|
@ -244,7 +244,12 @@ export const ReportingExampleApp = ({
|
|||
)}
|
||||
</EuiFlexItem>
|
||||
{logos.map((item, index) => (
|
||||
<EuiFlexItem key={index} data-shared-item>
|
||||
<EuiFlexItem
|
||||
key={index}
|
||||
data-shared-item
|
||||
data-shared-render-error
|
||||
data-render-error="This is an example error"
|
||||
>
|
||||
<EuiCard
|
||||
icon={<EuiIcon size="xxl" type={`logo${item}`} />}
|
||||
title={`Elastic ${item}`}
|
||||
|
|
|
@ -54,6 +54,9 @@ export async function generatePngObservableFactory(reporting: ReportingCore) {
|
|||
if (current.error) {
|
||||
found.push(current.error.message);
|
||||
}
|
||||
if (current.renderErrors) {
|
||||
found.push(...current.renderErrors);
|
||||
}
|
||||
return found;
|
||||
}, [] as string[]),
|
||||
})),
|
||||
|
|
|
@ -108,6 +108,9 @@ export async function generatePdfObservableFactory(reporting: ReportingCore) {
|
|||
if (current.error) {
|
||||
found.push(current.error.message);
|
||||
}
|
||||
if (current.renderErrors) {
|
||||
found.push(...current.renderErrors);
|
||||
}
|
||||
return found;
|
||||
}, [] as string[]),
|
||||
};
|
||||
|
|
|
@ -120,6 +120,9 @@ export async function generatePdfObservableFactory(reporting: ReportingCore) {
|
|||
if (current.error) {
|
||||
found.push(current.error.message);
|
||||
}
|
||||
if (current.renderErrors) {
|
||||
found.push(...current.renderErrors);
|
||||
}
|
||||
return found;
|
||||
}, [] as string[]),
|
||||
};
|
||||
|
|
|
@ -32,6 +32,8 @@ describe('Create Layout', () => {
|
|||
"selectors": Object {
|
||||
"itemsCountAttribute": "data-shared-items-count",
|
||||
"renderComplete": "[data-shared-item]",
|
||||
"renderError": "[data-render-error]",
|
||||
"renderErrorAttribute": "data-render-error",
|
||||
"screenshot": "[data-shared-items-container]",
|
||||
"timefilterDurationAttribute": "data-shared-timefilter-duration",
|
||||
},
|
||||
|
@ -63,6 +65,8 @@ describe('Create Layout', () => {
|
|||
"selectors": Object {
|
||||
"itemsCountAttribute": "data-shared-items-count",
|
||||
"renderComplete": "[data-shared-item]",
|
||||
"renderError": "[data-render-error]",
|
||||
"renderErrorAttribute": "data-render-error",
|
||||
"screenshot": "[data-shared-item]",
|
||||
"timefilterDurationAttribute": "data-shared-timefilter-duration",
|
||||
},
|
||||
|
@ -91,6 +95,8 @@ describe('Create Layout', () => {
|
|||
"selectors": Object {
|
||||
"itemsCountAttribute": "data-shared-items-count",
|
||||
"renderComplete": "[data-shared-item]",
|
||||
"renderError": "[data-render-error]",
|
||||
"renderErrorAttribute": "data-render-error",
|
||||
"screenshot": "[data-shared-items-container]",
|
||||
"timefilterDurationAttribute": "data-shared-timefilter-duration",
|
||||
},
|
||||
|
|
|
@ -13,6 +13,8 @@ import type { Layout } from './layout';
|
|||
export interface LayoutSelectorDictionary {
|
||||
screenshot: string;
|
||||
renderComplete: string;
|
||||
renderError: string;
|
||||
renderErrorAttribute: string;
|
||||
itemsCountAttribute: string;
|
||||
timefilterDurationAttribute: string;
|
||||
}
|
||||
|
@ -33,6 +35,8 @@ export const LayoutTypes = {
|
|||
export const getDefaultLayoutSelectors = (): LayoutSelectorDictionary => ({
|
||||
screenshot: '[data-shared-items-container]',
|
||||
renderComplete: '[data-shared-item]',
|
||||
renderError: '[data-render-error]',
|
||||
renderErrorAttribute: 'data-render-error',
|
||||
itemsCountAttribute: 'data-shared-items-count',
|
||||
timefilterDurationAttribute: 'data-shared-timefilter-duration',
|
||||
});
|
||||
|
|
|
@ -11,6 +11,7 @@ export const DEFAULT_PAGELOAD_SELECTOR = `.${APP_WRAPPER_CLASS}`;
|
|||
export const CONTEXT_GETNUMBEROFITEMS = 'GetNumberOfItems';
|
||||
export const CONTEXT_INJECTCSS = 'InjectCss';
|
||||
export const CONTEXT_WAITFORRENDER = 'WaitForRender';
|
||||
export const CONTEXT_GETRENDERERRORS = 'GetVisualisationsRenderErrors';
|
||||
export const CONTEXT_GETTIMERANGE = 'GetTimeRange';
|
||||
export const CONTEXT_ELEMENTATTRIBUTES = 'ElementPositionAndAttributes';
|
||||
export const CONTEXT_WAITFORELEMENTSTOBEINDOM = 'WaitForElementsToBeInDOM';
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* 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 { HeadlessChromiumDriver } from '../../browsers';
|
||||
import {
|
||||
createMockBrowserDriverFactory,
|
||||
createMockConfig,
|
||||
createMockConfigSchema,
|
||||
createMockLayoutInstance,
|
||||
createMockLevelLogger,
|
||||
createMockReportingCore,
|
||||
} from '../../test_helpers';
|
||||
import { CaptureConfig } from '../../types';
|
||||
import { LayoutInstance } from '../layouts';
|
||||
import { LevelLogger } from '../level_logger';
|
||||
import { getRenderErrors } from './get_render_errors';
|
||||
|
||||
describe('getRenderErrors', () => {
|
||||
let captureConfig: CaptureConfig;
|
||||
let layout: LayoutInstance;
|
||||
let logger: jest.Mocked<LevelLogger>;
|
||||
let browser: HeadlessChromiumDriver;
|
||||
|
||||
beforeEach(async () => {
|
||||
const schema = createMockConfigSchema();
|
||||
const config = createMockConfig(schema);
|
||||
const core = await createMockReportingCore(schema);
|
||||
|
||||
captureConfig = config.get('capture');
|
||||
layout = createMockLayoutInstance(captureConfig);
|
||||
logger = createMockLevelLogger();
|
||||
|
||||
await createMockBrowserDriverFactory(core, logger, {
|
||||
evaluate: jest.fn(
|
||||
async <T extends (...args: unknown[]) => unknown>({
|
||||
fn,
|
||||
args,
|
||||
}: {
|
||||
fn: T;
|
||||
args: Parameters<T>;
|
||||
}) => fn(...args)
|
||||
),
|
||||
getCreatePage: (driver) => {
|
||||
browser = driver;
|
||||
|
||||
return jest.fn();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
it('should extract the error messages', async () => {
|
||||
document.body.innerHTML = `
|
||||
<div dataRenderErrorSelector="a test error" />
|
||||
<div dataRenderErrorSelector="a test error" />
|
||||
<div dataRenderErrorSelector="a test error" />
|
||||
<div dataRenderErrorSelector="a test error" />
|
||||
`;
|
||||
|
||||
await expect(getRenderErrors(browser, layout, logger)).resolves.toEqual([
|
||||
'a test error',
|
||||
'a test error',
|
||||
'a test error',
|
||||
'a test error',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should extract the error messages, even when there are none', async () => {
|
||||
document.body.innerHTML = `
|
||||
<renderedSelector />
|
||||
`;
|
||||
|
||||
await expect(getRenderErrors(browser, layout, logger)).resolves.toEqual(undefined);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import type { HeadlessChromiumDriver } from '../../browsers';
|
||||
import type { LayoutInstance } from '../layouts';
|
||||
import { LevelLogger, startTrace } from '../';
|
||||
import { CONTEXT_GETRENDERERRORS } from './constants';
|
||||
|
||||
export const getRenderErrors = async (
|
||||
browser: HeadlessChromiumDriver,
|
||||
layout: LayoutInstance,
|
||||
logger: LevelLogger
|
||||
): Promise<undefined | string[]> => {
|
||||
const endTrace = startTrace('get_render_errors', 'read');
|
||||
logger.debug('reading render errors');
|
||||
const errorsFound: undefined | string[] = await browser.evaluate(
|
||||
{
|
||||
fn: (errorSelector, errorAttribute) => {
|
||||
const visualizations: Element[] = Array.from(document.querySelectorAll(errorSelector));
|
||||
const errors: string[] = [];
|
||||
|
||||
visualizations.forEach((visualization) => {
|
||||
const errorMessage = visualization.getAttribute(errorAttribute);
|
||||
if (errorMessage) {
|
||||
errors.push(errorMessage);
|
||||
}
|
||||
});
|
||||
|
||||
return errors.length ? errors : undefined;
|
||||
},
|
||||
args: [layout.selectors.renderError, layout.selectors.renderErrorAttribute],
|
||||
},
|
||||
{ context: CONTEXT_GETRENDERERRORS },
|
||||
logger
|
||||
);
|
||||
endTrace();
|
||||
|
||||
if (errorsFound?.length) {
|
||||
logger.warning(
|
||||
i18n.translate('xpack.reporting.screencapture.renderErrorsFound', {
|
||||
defaultMessage: 'Found {count} error messages. See report object for more information.',
|
||||
values: { count: errorsFound.length },
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return errorsFound;
|
||||
};
|
|
@ -53,5 +53,12 @@ export interface ScreenshotResults {
|
|||
timeRange: string | null;
|
||||
screenshots: Screenshot[];
|
||||
error?: Error;
|
||||
|
||||
/**
|
||||
* Individual visualizations might encounter errors at runtime. If there are any they are added to this
|
||||
* field. Any text captured here is intended to be shown to the user for debugging purposes, reporting
|
||||
* does no further sanitization on these strings.
|
||||
*/
|
||||
renderErrors?: string[];
|
||||
elementsPositionAndAttributes?: ElementsPositionAndAttribute[]; // NOTE: for testing
|
||||
}
|
||||
|
|
|
@ -98,6 +98,7 @@ describe('Screenshot Observable Pipeline', () => {
|
|||
},
|
||||
],
|
||||
"error": undefined,
|
||||
"renderErrors": undefined,
|
||||
"screenshots": Array [
|
||||
Object {
|
||||
"data": Object {
|
||||
|
@ -172,6 +173,7 @@ describe('Screenshot Observable Pipeline', () => {
|
|||
},
|
||||
],
|
||||
"error": undefined,
|
||||
"renderErrors": undefined,
|
||||
"screenshots": Array [
|
||||
Object {
|
||||
"data": Object {
|
||||
|
@ -223,6 +225,7 @@ describe('Screenshot Observable Pipeline', () => {
|
|||
},
|
||||
],
|
||||
"error": undefined,
|
||||
"renderErrors": undefined,
|
||||
"screenshots": Array [
|
||||
Object {
|
||||
"data": Object {
|
||||
|
@ -312,6 +315,7 @@ describe('Screenshot Observable Pipeline', () => {
|
|||
},
|
||||
],
|
||||
"error": [Error: An error occurred when trying to read the page for visualization panel info. You may need to increase 'xpack.reporting.capture.timeouts.waitForElements'. Error: Mock error!],
|
||||
"renderErrors": undefined,
|
||||
"screenshots": Array [
|
||||
Object {
|
||||
"data": Object {
|
||||
|
@ -354,6 +358,7 @@ describe('Screenshot Observable Pipeline', () => {
|
|||
},
|
||||
],
|
||||
"error": [Error: An error occurred when trying to read the page for visualization panel info. You may need to increase 'xpack.reporting.capture.timeouts.waitForElements'. Error: Mock error!],
|
||||
"renderErrors": undefined,
|
||||
"screenshots": Array [
|
||||
Object {
|
||||
"data": Object {
|
||||
|
@ -460,6 +465,7 @@ describe('Screenshot Observable Pipeline', () => {
|
|||
},
|
||||
],
|
||||
"error": undefined,
|
||||
"renderErrors": undefined,
|
||||
"screenshots": Array [
|
||||
Object {
|
||||
"data": Object {
|
||||
|
|
|
@ -17,6 +17,7 @@ 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 { getRenderErrors } from './get_render_errors';
|
||||
import { injectCustomCss } from './inject_css';
|
||||
import { openUrl } from './open_url';
|
||||
import { waitForRenderComplete } from './wait_for_render';
|
||||
|
@ -28,6 +29,7 @@ const DEFAULT_SCREENSHOT_CLIP_WIDTH = 1800;
|
|||
interface ScreenSetupData {
|
||||
elementsPositionAndAttributes: ElementsPositionAndAttribute[] | null;
|
||||
timeRange: string | null;
|
||||
renderErrors?: string[];
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
|
@ -98,16 +100,22 @@ export function getScreenshots$(
|
|||
return await Promise.all([
|
||||
getTimeRange(driver, layout, logger),
|
||||
getElementPositionAndAttributes(driver, layout, logger),
|
||||
]).then(([timeRange, elementsPositionAndAttributes]) => ({
|
||||
getRenderErrors(driver, layout, logger),
|
||||
]).then(([timeRange, elementsPositionAndAttributes, renderErrors]) => ({
|
||||
elementsPositionAndAttributes,
|
||||
timeRange,
|
||||
renderErrors,
|
||||
}));
|
||||
}),
|
||||
catchError((err) => {
|
||||
checkPageIsOpen(driver); // if browser has closed, throw a relevant error about it
|
||||
|
||||
logger.error(err);
|
||||
return Rx.of({ elementsPositionAndAttributes: null, timeRange: null, error: err });
|
||||
return Rx.of({
|
||||
elementsPositionAndAttributes: null,
|
||||
timeRange: null,
|
||||
error: err,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -120,11 +128,12 @@ export function getScreenshots$(
|
|||
? data.elementsPositionAndAttributes
|
||||
: getDefaultElementPosition(layout.getViewport(1));
|
||||
const screenshots = await getScreenshots(driver, elements, logger);
|
||||
const { timeRange, error: setupError } = data;
|
||||
const { timeRange, error: setupError, renderErrors } = data;
|
||||
return {
|
||||
timeRange,
|
||||
screenshots,
|
||||
error: setupError,
|
||||
renderErrors,
|
||||
elementsPositionAndAttributes: elements,
|
||||
};
|
||||
})
|
||||
|
|
|
@ -78,6 +78,9 @@ mockBrowserEvaluate.mockImplementation(() => {
|
|||
if (mockCall === contexts.CONTEXT_ELEMENTATTRIBUTES) {
|
||||
return Promise.resolve(getMockElementsPositionAndAttributes('Default Mock Title', 'Default '));
|
||||
}
|
||||
if (mockCall === contexts.CONTEXT_GETRENDERERRORS) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
throw new Error(mockCall);
|
||||
});
|
||||
const mockScreenshot = jest.fn(async () => Buffer.from('screenshot'));
|
||||
|
|
|
@ -18,6 +18,8 @@ export const createMockLayoutInstance = (captureConfig: CaptureConfig) => {
|
|||
renderComplete: 'renderedSelector',
|
||||
itemsCountAttribute: 'itemsSelector',
|
||||
screenshot: 'screenshotSelector',
|
||||
renderError: '[dataRenderErrorSelector]',
|
||||
renderErrorAttribute: 'dataRenderErrorSelector',
|
||||
timefilterDurationAttribute: 'timefilterDurationSelector',
|
||||
};
|
||||
return mockLayout;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue