mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
Functional tests for KibanaErrorBoundary (#170569)
Part of https://github.com/elastic/kibana-team/issues/646 This PR adds an example plugin in `examples/error_boundary` that shows usage of KibanaErrorBoundary. The example plugin is used in a functional test to ensure errors are caught in the appropriate way, and error messages include a working Refresh button. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
c902f90a71
commit
09f4708de4
13 changed files with 262 additions and 11 deletions
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
@ -353,6 +353,7 @@ src/plugins/embeddable @elastic/kibana-presentation
|
||||||
x-pack/examples/embedded_lens_example @elastic/kibana-visualizations
|
x-pack/examples/embedded_lens_example @elastic/kibana-visualizations
|
||||||
x-pack/plugins/encrypted_saved_objects @elastic/kibana-security
|
x-pack/plugins/encrypted_saved_objects @elastic/kibana-security
|
||||||
x-pack/plugins/enterprise_search @elastic/enterprise-search-frontend
|
x-pack/plugins/enterprise_search @elastic/enterprise-search-frontend
|
||||||
|
examples/error_boundary @elastic/appex-sharedux
|
||||||
packages/kbn-es @elastic/kibana-operations
|
packages/kbn-es @elastic/kibana-operations
|
||||||
packages/kbn-es-archiver @elastic/kibana-operations @elastic/appex-qa
|
packages/kbn-es-archiver @elastic/kibana-operations @elastic/appex-qa
|
||||||
packages/kbn-es-errors @elastic/kibana-core
|
packages/kbn-es-errors @elastic/kibana-core
|
||||||
|
|
3
examples/error_boundary/README.md
Executable file
3
examples/error_boundary/README.md
Executable file
|
@ -0,0 +1,3 @@
|
||||||
|
## Error Boundary Example
|
||||||
|
|
||||||
|
A very simple example plugin for testing Kibana Error Boundary.
|
14
examples/error_boundary/kibana.jsonc
Normal file
14
examples/error_boundary/kibana.jsonc
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"type": "plugin",
|
||||||
|
"id": "@kbn/error-boundary-example-plugin",
|
||||||
|
"owner": "@elastic/appex-sharedux",
|
||||||
|
"description": "A plugin which exemplifes the KibanaErrorBoundary",
|
||||||
|
"plugin": {
|
||||||
|
"id": "error_boundary_example",
|
||||||
|
"server": false,
|
||||||
|
"browser": true,
|
||||||
|
"requiredPlugins": [
|
||||||
|
"developerExamples"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
12
examples/error_boundary/public/index.ts
Executable file
12
examples/error_boundary/public/index.ts
Executable file
|
@ -0,0 +1,12 @@
|
||||||
|
/*
|
||||||
|
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||||
|
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||||
|
* Side Public License, v 1.
|
||||||
|
*/
|
||||||
|
import { ErrorBoundaryExamplePlugin } from './plugin';
|
||||||
|
|
||||||
|
export function plugin() {
|
||||||
|
return new ErrorBoundaryExamplePlugin();
|
||||||
|
}
|
111
examples/error_boundary/public/plugin.tsx
Executable file
111
examples/error_boundary/public/plugin.tsx
Executable file
|
@ -0,0 +1,111 @@
|
||||||
|
/*
|
||||||
|
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||||
|
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||||
|
* Side Public License, v 1.
|
||||||
|
*/
|
||||||
|
import { EuiButton } from '@elastic/eui';
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
|
||||||
|
import { AppMountParameters, CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
|
||||||
|
import { DeveloperExamplesSetup } from '@kbn/developer-examples-plugin/public';
|
||||||
|
import { KibanaErrorBoundary, KibanaErrorBoundaryProvider } from '@kbn/shared-ux-error-boundary';
|
||||||
|
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
|
||||||
|
|
||||||
|
interface SetupDeps {
|
||||||
|
developerExamples: DeveloperExamplesSetup;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useErrors = () => {
|
||||||
|
return useState(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FatalComponent = () => {
|
||||||
|
const [hasError, setHasError] = useErrors();
|
||||||
|
|
||||||
|
if (hasError) {
|
||||||
|
const fatalError = new Error('Example of unknown error type');
|
||||||
|
throw fatalError;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EuiButton
|
||||||
|
onClick={() => {
|
||||||
|
setHasError(true);
|
||||||
|
}}
|
||||||
|
data-test-subj="fatalErrorBtn"
|
||||||
|
>
|
||||||
|
Click for fatal error
|
||||||
|
</EuiButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RecoverableComponent = () => {
|
||||||
|
const [hasError, setHasError] = useErrors();
|
||||||
|
|
||||||
|
if (hasError) {
|
||||||
|
// FIXME: use network interception to disable responses
|
||||||
|
// for chunk requests and attempt to lazy-load a component
|
||||||
|
// https://github.com/elastic/kibana/issues/170777
|
||||||
|
const upgradeError = new Error('ChunkLoadError');
|
||||||
|
upgradeError.name = 'ChunkLoadError';
|
||||||
|
throw upgradeError;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EuiButton
|
||||||
|
onClick={() => {
|
||||||
|
setHasError(true);
|
||||||
|
}}
|
||||||
|
data-test-subj="recoverableErrorBtn"
|
||||||
|
>
|
||||||
|
Click for recoverable error
|
||||||
|
</EuiButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ErrorBoundaryExamplePlugin implements Plugin<void, void, SetupDeps> {
|
||||||
|
public setup(core: CoreSetup, deps: SetupDeps) {
|
||||||
|
// Register an application into the side navigation menu
|
||||||
|
core.application.register({
|
||||||
|
id: 'errorBoundaryExample',
|
||||||
|
title: 'Error Boundary Example',
|
||||||
|
async mount({ element }: AppMountParameters) {
|
||||||
|
ReactDOM.render(
|
||||||
|
<KibanaErrorBoundaryProvider analytics={core.analytics}>
|
||||||
|
<KibanaErrorBoundary>
|
||||||
|
<KibanaPageTemplate>
|
||||||
|
<KibanaPageTemplate.Header
|
||||||
|
pageTitle="KibanaErrorBoundary example"
|
||||||
|
data-test-subj="errorBoundaryExampleHeader"
|
||||||
|
/>
|
||||||
|
<KibanaPageTemplate.Section grow={false}>
|
||||||
|
<FatalComponent />
|
||||||
|
</KibanaPageTemplate.Section>
|
||||||
|
<KibanaPageTemplate.Section>
|
||||||
|
<RecoverableComponent />
|
||||||
|
</KibanaPageTemplate.Section>
|
||||||
|
</KibanaPageTemplate>
|
||||||
|
</KibanaErrorBoundary>
|
||||||
|
</KibanaErrorBoundaryProvider>,
|
||||||
|
element
|
||||||
|
);
|
||||||
|
return () => ReactDOM.unmountComponentAtNode(element);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// This section is only needed to get this example plugin to show up in our Developer Examples.
|
||||||
|
deps.developerExamples.register({
|
||||||
|
appId: 'errorBoundaryExample',
|
||||||
|
title: 'Error Boundary Example Application',
|
||||||
|
description: `Build a plugin that registers an application that simply says "Error Boundary Example"`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
public start(_core: CoreStart) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
public stop() {}
|
||||||
|
}
|
22
examples/error_boundary/tsconfig.json
Normal file
22
examples/error_boundary/tsconfig.json
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "target/types"
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"index.ts",
|
||||||
|
"common/**/*.ts",
|
||||||
|
"public/**/*.ts",
|
||||||
|
"public/**/*.tsx",
|
||||||
|
"../../typings/**/*"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"target/**/*"
|
||||||
|
],
|
||||||
|
"kbn_references": [
|
||||||
|
"@kbn/core",
|
||||||
|
"@kbn/developer-examples-plugin",
|
||||||
|
"@kbn/shared-ux-error-boundary",
|
||||||
|
"@kbn/shared-ux-page-kibana-template"
|
||||||
|
]
|
||||||
|
}
|
|
@ -398,6 +398,7 @@
|
||||||
"@kbn/embedded-lens-example-plugin": "link:x-pack/examples/embedded_lens_example",
|
"@kbn/embedded-lens-example-plugin": "link:x-pack/examples/embedded_lens_example",
|
||||||
"@kbn/encrypted-saved-objects-plugin": "link:x-pack/plugins/encrypted_saved_objects",
|
"@kbn/encrypted-saved-objects-plugin": "link:x-pack/plugins/encrypted_saved_objects",
|
||||||
"@kbn/enterprise-search-plugin": "link:x-pack/plugins/enterprise_search",
|
"@kbn/enterprise-search-plugin": "link:x-pack/plugins/enterprise_search",
|
||||||
|
"@kbn/error-boundary-example-plugin": "link:examples/error_boundary",
|
||||||
"@kbn/es-errors": "link:packages/kbn-es-errors",
|
"@kbn/es-errors": "link:packages/kbn-es-errors",
|
||||||
"@kbn/es-query": "link:packages/kbn-es-query",
|
"@kbn/es-query": "link:packages/kbn-es-query",
|
||||||
"@kbn/es-types": "link:packages/kbn-es-types",
|
"@kbn/es-types": "link:packages/kbn-es-types",
|
||||||
|
|
|
@ -49,7 +49,7 @@ describe('<KibanaErrorBoundary>', () => {
|
||||||
expect(await findByText(strings.recoverable.callout.title())).toBeVisible();
|
expect(await findByText(strings.recoverable.callout.title())).toBeVisible();
|
||||||
expect(await findByText(strings.recoverable.callout.pageReloadButton())).toBeVisible();
|
expect(await findByText(strings.recoverable.callout.pageReloadButton())).toBeVisible();
|
||||||
|
|
||||||
(await findByTestId('recoverablePromptReloadBtn')).click();
|
(await findByTestId('errorBoundaryRecoverablePromptReloadBtn')).click();
|
||||||
|
|
||||||
expect(reloadSpy).toHaveBeenCalledTimes(1);
|
expect(reloadSpy).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
@ -69,7 +69,7 @@ describe('<KibanaErrorBoundary>', () => {
|
||||||
expect(await findByText(strings.fatal.callout.showDetailsButton())).toBeVisible();
|
expect(await findByText(strings.fatal.callout.showDetailsButton())).toBeVisible();
|
||||||
expect(await findByText(strings.fatal.callout.pageReloadButton())).toBeVisible();
|
expect(await findByText(strings.fatal.callout.pageReloadButton())).toBeVisible();
|
||||||
|
|
||||||
(await findByTestId('fatalPromptReloadBtn')).click();
|
(await findByTestId('errorBoundaryFatalPromptReloadBtn')).click();
|
||||||
|
|
||||||
expect(reloadSpy).toHaveBeenCalledTimes(1);
|
expect(reloadSpy).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
|
@ -55,7 +55,7 @@ const CodePanel: React.FC<ErrorCalloutProps & { onClose: () => void }> = (props)
|
||||||
</EuiPanel>
|
</EuiPanel>
|
||||||
</EuiFlyoutHeader>
|
</EuiFlyoutHeader>
|
||||||
<EuiFlyoutBody>
|
<EuiFlyoutBody>
|
||||||
<EuiCodeBlock>
|
<EuiCodeBlock data-test-subj="errorBoundaryFatalDetailsErrorString">
|
||||||
<p>{(error.stack ?? error.toString()) + '\n\n'}</p>
|
<p>{(error.stack ?? error.toString()) + '\n\n'}</p>
|
||||||
<p>
|
<p>
|
||||||
{errorName}
|
{errorName}
|
||||||
|
@ -93,25 +93,29 @@ export const FatalPrompt: React.FC<ErrorCalloutProps> = (props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EuiEmptyPrompt
|
<EuiEmptyPrompt
|
||||||
title={<h2>{strings.fatal.callout.title()}</h2>}
|
title={<h2 data-test-subj="errorBoundaryFatalHeader">{strings.fatal.callout.title()}</h2>}
|
||||||
color="danger"
|
color="danger"
|
||||||
iconType="error"
|
iconType="error"
|
||||||
body={
|
body={
|
||||||
<>
|
<>
|
||||||
<p>{strings.fatal.callout.body()}</p>
|
<p data-test-subj="errorBoundaryFatalPromptBody">{strings.fatal.callout.body()}</p>
|
||||||
<p>
|
<p>
|
||||||
<EuiButton
|
<EuiButton
|
||||||
color="danger"
|
color="danger"
|
||||||
iconType="refresh"
|
iconType="refresh"
|
||||||
fill={true}
|
fill={true}
|
||||||
onClick={onClickRefresh}
|
onClick={onClickRefresh}
|
||||||
data-test-subj="fatalPromptReloadBtn"
|
data-test-subj="errorBoundaryFatalPromptReloadBtn"
|
||||||
>
|
>
|
||||||
{strings.fatal.callout.pageReloadButton()}
|
{strings.fatal.callout.pageReloadButton()}
|
||||||
</EuiButton>
|
</EuiButton>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<EuiLink color="danger" onClick={() => setIsFlyoutVisible(true)}>
|
<EuiLink
|
||||||
|
color="danger"
|
||||||
|
onClick={() => setIsFlyoutVisible(true)}
|
||||||
|
data-test-subj="errorBoundaryFatalShowDetailsBtn"
|
||||||
|
>
|
||||||
{strings.fatal.callout.showDetailsButton()}
|
{strings.fatal.callout.showDetailsButton()}
|
||||||
</EuiLink>
|
</EuiLink>
|
||||||
{isFlyoutVisible ? (
|
{isFlyoutVisible ? (
|
||||||
|
@ -128,17 +132,25 @@ export const RecoverablePrompt = (props: ErrorCalloutProps) => {
|
||||||
const { onClickRefresh } = props;
|
const { onClickRefresh } = props;
|
||||||
return (
|
return (
|
||||||
<EuiEmptyPrompt
|
<EuiEmptyPrompt
|
||||||
iconType="warning"
|
title={
|
||||||
title={<h2>{strings.recoverable.callout.title()}</h2>}
|
<h2 data-test-subj="errorBoundaryRecoverableHeader">
|
||||||
body={<p>{strings.recoverable.callout.body()}</p>}
|
{strings.recoverable.callout.title()}
|
||||||
|
</h2>
|
||||||
|
}
|
||||||
color="warning"
|
color="warning"
|
||||||
|
iconType="warning"
|
||||||
|
body={
|
||||||
|
<p data-test-subj="errorBoundaryRecoverablePromptBody">
|
||||||
|
{strings.recoverable.callout.body()}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
actions={
|
actions={
|
||||||
<EuiButton
|
<EuiButton
|
||||||
color="warning"
|
color="warning"
|
||||||
iconType="refresh"
|
iconType="refresh"
|
||||||
fill={true}
|
fill={true}
|
||||||
onClick={onClickRefresh}
|
onClick={onClickRefresh}
|
||||||
data-test-subj="recoverablePromptReloadBtn"
|
data-test-subj="errorBoundaryRecoverablePromptReloadBtn"
|
||||||
>
|
>
|
||||||
{strings.recoverable.callout.pageReloadButton()}
|
{strings.recoverable.callout.pageReloadButton()}
|
||||||
</EuiButton>
|
</EuiButton>
|
||||||
|
|
|
@ -31,6 +31,7 @@ export default async function ({ readConfigFile }) {
|
||||||
require.resolve('./content_management'),
|
require.resolve('./content_management'),
|
||||||
require.resolve('./unified_field_list_examples'),
|
require.resolve('./unified_field_list_examples'),
|
||||||
require.resolve('./discover_customization_examples'),
|
require.resolve('./discover_customization_examples'),
|
||||||
|
require.resolve('./error_boundary'),
|
||||||
],
|
],
|
||||||
services: {
|
services: {
|
||||||
...functionalConfig.get('services'),
|
...functionalConfig.get('services'),
|
||||||
|
|
68
test/examples/error_boundary/index.ts
Normal file
68
test/examples/error_boundary/index.ts
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
/*
|
||||||
|
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||||
|
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||||
|
* Side Public License, v 1.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import expect from '@kbn/expect';
|
||||||
|
import { FtrProviderContext } from '../../functional/ftr_provider_context';
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||||
|
const retry = getService('retry');
|
||||||
|
const testSubjects = getService('testSubjects');
|
||||||
|
const PageObjects = getPageObjects(['common']);
|
||||||
|
const log = getService('log');
|
||||||
|
|
||||||
|
describe('Error Boundary Examples', () => {
|
||||||
|
before(async () => {
|
||||||
|
await PageObjects.common.navigateToApp('errorBoundaryExample');
|
||||||
|
await testSubjects.existOrFail('errorBoundaryExampleHeader');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fatal error', async () => {
|
||||||
|
log.debug('clicking button for fatal error');
|
||||||
|
await testSubjects.click('fatalErrorBtn');
|
||||||
|
const errorHeader = await testSubjects.getVisibleText('errorBoundaryFatalHeader');
|
||||||
|
expect(errorHeader).to.not.be(undefined);
|
||||||
|
|
||||||
|
log.debug('checking that the error has taken over the page');
|
||||||
|
await testSubjects.missingOrFail('errorBoundaryExampleHeader');
|
||||||
|
|
||||||
|
await testSubjects.click('errorBoundaryFatalShowDetailsBtn');
|
||||||
|
const errorString = await testSubjects.getVisibleText('errorBoundaryFatalDetailsErrorString');
|
||||||
|
expect(errorString).to.match(/Error: Example of unknown error type/);
|
||||||
|
|
||||||
|
log.debug('closing error flyout');
|
||||||
|
await testSubjects.click('euiFlyoutCloseButton');
|
||||||
|
|
||||||
|
log.debug('clicking page refresh');
|
||||||
|
await testSubjects.click('errorBoundaryFatalPromptReloadBtn');
|
||||||
|
|
||||||
|
await retry.try(async () => {
|
||||||
|
log.debug('checking for page refresh');
|
||||||
|
await testSubjects.existOrFail('errorBoundaryExampleHeader');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('recoverable error', async () => {
|
||||||
|
log.debug('clicking button for recoverable error');
|
||||||
|
await testSubjects.click('recoverableErrorBtn');
|
||||||
|
const errorHeader = await testSubjects.getVisibleText('errorBoundaryRecoverableHeader');
|
||||||
|
expect(errorHeader).to.not.be(undefined);
|
||||||
|
|
||||||
|
log.debug('checking that the error has taken over the page');
|
||||||
|
await testSubjects.missingOrFail('errorBoundaryExampleHeader');
|
||||||
|
|
||||||
|
log.debug('clicking page refresh');
|
||||||
|
await testSubjects.click('errorBoundaryRecoverablePromptReloadBtn');
|
||||||
|
|
||||||
|
await retry.try(async () => {
|
||||||
|
log.debug('checking for page refresh');
|
||||||
|
await testSubjects.existOrFail('errorBoundaryExampleHeader');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -700,6 +700,8 @@
|
||||||
"@kbn/encrypted-saved-objects-plugin/*": ["x-pack/plugins/encrypted_saved_objects/*"],
|
"@kbn/encrypted-saved-objects-plugin/*": ["x-pack/plugins/encrypted_saved_objects/*"],
|
||||||
"@kbn/enterprise-search-plugin": ["x-pack/plugins/enterprise_search"],
|
"@kbn/enterprise-search-plugin": ["x-pack/plugins/enterprise_search"],
|
||||||
"@kbn/enterprise-search-plugin/*": ["x-pack/plugins/enterprise_search/*"],
|
"@kbn/enterprise-search-plugin/*": ["x-pack/plugins/enterprise_search/*"],
|
||||||
|
"@kbn/error-boundary-example-plugin": ["examples/error_boundary"],
|
||||||
|
"@kbn/error-boundary-example-plugin/*": ["examples/error_boundary/*"],
|
||||||
"@kbn/es": ["packages/kbn-es"],
|
"@kbn/es": ["packages/kbn-es"],
|
||||||
"@kbn/es/*": ["packages/kbn-es/*"],
|
"@kbn/es/*": ["packages/kbn-es/*"],
|
||||||
"@kbn/es-archiver": ["packages/kbn-es-archiver"],
|
"@kbn/es-archiver": ["packages/kbn-es-archiver"],
|
||||||
|
|
|
@ -4304,6 +4304,10 @@
|
||||||
version "0.0.0"
|
version "0.0.0"
|
||||||
uid ""
|
uid ""
|
||||||
|
|
||||||
|
"@kbn/error-boundary-example-plugin@link:examples/error_boundary":
|
||||||
|
version "0.0.0"
|
||||||
|
uid ""
|
||||||
|
|
||||||
"@kbn/es-archiver@link:packages/kbn-es-archiver":
|
"@kbn/es-archiver@link:packages/kbn-es-archiver":
|
||||||
version "0.0.0"
|
version "0.0.0"
|
||||||
uid ""
|
uid ""
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue