mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -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/plugins/encrypted_saved_objects @elastic/kibana-security
|
||||
x-pack/plugins/enterprise_search @elastic/enterprise-search-frontend
|
||||
examples/error_boundary @elastic/appex-sharedux
|
||||
packages/kbn-es @elastic/kibana-operations
|
||||
packages/kbn-es-archiver @elastic/kibana-operations @elastic/appex-qa
|
||||
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/encrypted-saved-objects-plugin": "link:x-pack/plugins/encrypted_saved_objects",
|
||||
"@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-query": "link:packages/kbn-es-query",
|
||||
"@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.pageReloadButton())).toBeVisible();
|
||||
|
||||
(await findByTestId('recoverablePromptReloadBtn')).click();
|
||||
(await findByTestId('errorBoundaryRecoverablePromptReloadBtn')).click();
|
||||
|
||||
expect(reloadSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
@ -69,7 +69,7 @@ describe('<KibanaErrorBoundary>', () => {
|
|||
expect(await findByText(strings.fatal.callout.showDetailsButton())).toBeVisible();
|
||||
expect(await findByText(strings.fatal.callout.pageReloadButton())).toBeVisible();
|
||||
|
||||
(await findByTestId('fatalPromptReloadBtn')).click();
|
||||
(await findByTestId('errorBoundaryFatalPromptReloadBtn')).click();
|
||||
|
||||
expect(reloadSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
|
|
@ -55,7 +55,7 @@ const CodePanel: React.FC<ErrorCalloutProps & { onClose: () => void }> = (props)
|
|||
</EuiPanel>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<EuiCodeBlock>
|
||||
<EuiCodeBlock data-test-subj="errorBoundaryFatalDetailsErrorString">
|
||||
<p>{(error.stack ?? error.toString()) + '\n\n'}</p>
|
||||
<p>
|
||||
{errorName}
|
||||
|
@ -93,25 +93,29 @@ export const FatalPrompt: React.FC<ErrorCalloutProps> = (props) => {
|
|||
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
title={<h2>{strings.fatal.callout.title()}</h2>}
|
||||
title={<h2 data-test-subj="errorBoundaryFatalHeader">{strings.fatal.callout.title()}</h2>}
|
||||
color="danger"
|
||||
iconType="error"
|
||||
body={
|
||||
<>
|
||||
<p>{strings.fatal.callout.body()}</p>
|
||||
<p data-test-subj="errorBoundaryFatalPromptBody">{strings.fatal.callout.body()}</p>
|
||||
<p>
|
||||
<EuiButton
|
||||
color="danger"
|
||||
iconType="refresh"
|
||||
fill={true}
|
||||
onClick={onClickRefresh}
|
||||
data-test-subj="fatalPromptReloadBtn"
|
||||
data-test-subj="errorBoundaryFatalPromptReloadBtn"
|
||||
>
|
||||
{strings.fatal.callout.pageReloadButton()}
|
||||
</EuiButton>
|
||||
</p>
|
||||
<p>
|
||||
<EuiLink color="danger" onClick={() => setIsFlyoutVisible(true)}>
|
||||
<EuiLink
|
||||
color="danger"
|
||||
onClick={() => setIsFlyoutVisible(true)}
|
||||
data-test-subj="errorBoundaryFatalShowDetailsBtn"
|
||||
>
|
||||
{strings.fatal.callout.showDetailsButton()}
|
||||
</EuiLink>
|
||||
{isFlyoutVisible ? (
|
||||
|
@ -128,17 +132,25 @@ export const RecoverablePrompt = (props: ErrorCalloutProps) => {
|
|||
const { onClickRefresh } = props;
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
iconType="warning"
|
||||
title={<h2>{strings.recoverable.callout.title()}</h2>}
|
||||
body={<p>{strings.recoverable.callout.body()}</p>}
|
||||
title={
|
||||
<h2 data-test-subj="errorBoundaryRecoverableHeader">
|
||||
{strings.recoverable.callout.title()}
|
||||
</h2>
|
||||
}
|
||||
color="warning"
|
||||
iconType="warning"
|
||||
body={
|
||||
<p data-test-subj="errorBoundaryRecoverablePromptBody">
|
||||
{strings.recoverable.callout.body()}
|
||||
</p>
|
||||
}
|
||||
actions={
|
||||
<EuiButton
|
||||
color="warning"
|
||||
iconType="refresh"
|
||||
fill={true}
|
||||
onClick={onClickRefresh}
|
||||
data-test-subj="recoverablePromptReloadBtn"
|
||||
data-test-subj="errorBoundaryRecoverablePromptReloadBtn"
|
||||
>
|
||||
{strings.recoverable.callout.pageReloadButton()}
|
||||
</EuiButton>
|
||||
|
|
|
@ -31,6 +31,7 @@ export default async function ({ readConfigFile }) {
|
|||
require.resolve('./content_management'),
|
||||
require.resolve('./unified_field_list_examples'),
|
||||
require.resolve('./discover_customization_examples'),
|
||||
require.resolve('./error_boundary'),
|
||||
],
|
||||
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/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-archiver": ["packages/kbn-es-archiver"],
|
||||
|
|
|
@ -4304,6 +4304,10 @@
|
|||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/error-boundary-example-plugin@link:examples/error_boundary":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/es-archiver@link:packages/kbn-es-archiver":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue