mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[User Experience] Add error boundary to prevent UX dashboard from crashing the application (#117583) (#117912)
* wrap UX dashboard into an error boundary (fixes #117543) * refactor APM root app tests to reuse coreMock Before this change, the tests for the root application component of the APM app were manually mocking the `coreStart` objects required to render the component. After this change, these tests will now reuse the relevant `coreMock` methods. * refactor: fix typo on createAppMountParameters test utility Co-authored-by: Lucas Fernandes da Costa <lucas.fernandesdacosta@elastic.co> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Lucas F. da Costa <lucas@lucasfcosta.com> Co-authored-by: Lucas Fernandes da Costa <lucas.fernandesdacosta@elastic.co>
This commit is contained in:
parent
e671b459b0
commit
79cb1913d3
7 changed files with 105 additions and 89 deletions
|
@ -458,7 +458,7 @@ describe('Plugin', () => {
|
|||
const [coreStartMock, startDepsMock] = await coreSetup.getStartServices();
|
||||
const unmountMock = jest.fn();
|
||||
renderAppMock.mockReturnValue(unmountMock);
|
||||
const params = coreMock.createAppMountParamters('/fake/base/path');
|
||||
const params = coreMock.createAppMountParameters('/fake/base/path');
|
||||
|
||||
new Plugin(coreMock.createPluginInitializerContext()).setup(coreSetup);
|
||||
// Grab registered mount function
|
||||
|
@ -528,7 +528,7 @@ import { renderApp } from './application';
|
|||
|
||||
describe('renderApp', () => {
|
||||
it('mounts and unmounts UI', () => {
|
||||
const params = coreMock.createAppMountParamters('/fake/base/path');
|
||||
const params = coreMock.createAppMountParameters('/fake/base/path');
|
||||
const core = coreMock.createStart();
|
||||
|
||||
// Verify some expected DOM element is rendered into the element
|
||||
|
@ -540,7 +540,7 @@ describe('renderApp', () => {
|
|||
});
|
||||
|
||||
it('unsubscribes from uiSettings', () => {
|
||||
const params = coreMock.createAppMountParamters('/fake/base/path');
|
||||
const params = coreMock.createAppMountParameters('/fake/base/path');
|
||||
const core = coreMock.createStart();
|
||||
// Create a fake Subject you can use to monitor observers
|
||||
const settings$ = new Subject();
|
||||
|
@ -555,7 +555,7 @@ describe('renderApp', () => {
|
|||
});
|
||||
|
||||
it('resets chrome visibility', () => {
|
||||
const params = coreMock.createAppMountParamters('/fake/base/path');
|
||||
const params = coreMock.createAppMountParameters('/fake/base/path');
|
||||
const core = coreMock.createStart();
|
||||
|
||||
// Verify stateful Core API was called on mount
|
||||
|
|
|
@ -169,5 +169,5 @@ export const coreMock = {
|
|||
createStart: createCoreStartMock,
|
||||
createPluginInitializerContext: pluginInitializerContextMock,
|
||||
createStorage: createStorageMock,
|
||||
createAppMountParamters: createAppMountParametersMock,
|
||||
createAppMountParameters: createAppMountParametersMock,
|
||||
};
|
||||
|
|
|
@ -7,24 +7,31 @@
|
|||
|
||||
import React from 'react';
|
||||
import { act } from '@testing-library/react';
|
||||
import { EuiErrorBoundary } from '@elastic/eui';
|
||||
import { mount } from 'enzyme';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { Observable } from 'rxjs';
|
||||
import { CoreStart, DocLinksStart, HttpStart } from 'src/core/public';
|
||||
import { AppMountParameters, DocLinksStart, HttpStart } from 'src/core/public';
|
||||
import { mockApmPluginContextValue } from '../context/apm_plugin/mock_apm_plugin_context';
|
||||
import { createCallApmApi } from '../services/rest/createCallApmApi';
|
||||
import { renderApp } from './';
|
||||
import { renderApp as renderApmApp } from './';
|
||||
import { UXAppRoot } from './uxApp';
|
||||
import { disableConsoleWarning } from '../utils/testHelpers';
|
||||
import { dataPluginMock } from 'src/plugins/data/public/mocks';
|
||||
import { embeddablePluginMock } from 'src/plugins/embeddable/public/mocks';
|
||||
import { ApmPluginStartDeps } from '../plugin';
|
||||
import { ApmPluginSetupDeps, ApmPluginStartDeps } from '../plugin';
|
||||
import { RumHome } from '../components/app/RumDashboard/RumHome';
|
||||
|
||||
jest.mock('../services/rest/data_view', () => ({
|
||||
createStaticDataView: () => Promise.resolve(undefined),
|
||||
}));
|
||||
|
||||
describe('renderApp', () => {
|
||||
let mockConsole: jest.SpyInstance;
|
||||
jest.mock('../components/app/RumDashboard/RumHome', () => ({
|
||||
RumHome: () => <p>Home Mock</p>,
|
||||
}));
|
||||
|
||||
describe('renderApp (APM)', () => {
|
||||
let mockConsole: jest.SpyInstance;
|
||||
beforeAll(() => {
|
||||
// The RUM agent logs an unnecessary message here. There's a couple open
|
||||
// issues need to be fixed to get the ability to turn off all of the logging:
|
||||
|
@ -40,11 +47,15 @@ describe('renderApp', () => {
|
|||
mockConsole.mockRestore();
|
||||
});
|
||||
|
||||
it('renders the app', () => {
|
||||
const { core, config, observabilityRuleTypeRegistry } =
|
||||
mockApmPluginContextValue;
|
||||
const getApmMountProps = () => {
|
||||
const {
|
||||
core: coreStart,
|
||||
config,
|
||||
observabilityRuleTypeRegistry,
|
||||
corePlugins,
|
||||
} = mockApmPluginContextValue;
|
||||
|
||||
const plugins = {
|
||||
const pluginsSetup = {
|
||||
licensing: { license$: new Observable() },
|
||||
triggersActionsUi: { actionTypeRegistry: {}, ruleTypeRegistry: {} },
|
||||
data: {
|
||||
|
@ -99,7 +110,7 @@ describe('renderApp', () => {
|
|||
} as unknown as ApmPluginStartDeps;
|
||||
|
||||
jest.spyOn(window, 'scrollTo').mockReturnValueOnce(undefined);
|
||||
createCallApmApi(core as unknown as CoreStart);
|
||||
createCallApmApi(coreStart);
|
||||
|
||||
jest
|
||||
.spyOn(window.console, 'warn')
|
||||
|
@ -111,17 +122,24 @@ describe('renderApp', () => {
|
|||
}
|
||||
});
|
||||
|
||||
return {
|
||||
coreStart,
|
||||
pluginsSetup: pluginsSetup as unknown as ApmPluginSetupDeps,
|
||||
appMountParameters: appMountParameters as unknown as AppMountParameters,
|
||||
pluginsStart,
|
||||
config,
|
||||
observabilityRuleTypeRegistry,
|
||||
corePlugins,
|
||||
};
|
||||
};
|
||||
|
||||
it('renders the app', () => {
|
||||
const mountProps = getApmMountProps();
|
||||
|
||||
let unmount: () => void;
|
||||
|
||||
act(() => {
|
||||
unmount = renderApp({
|
||||
coreStart: core as any,
|
||||
pluginsSetup: plugins as any,
|
||||
appMountParameters: appMountParameters as any,
|
||||
pluginsStart,
|
||||
config,
|
||||
observabilityRuleTypeRegistry,
|
||||
});
|
||||
unmount = renderApmApp(mountProps);
|
||||
});
|
||||
|
||||
expect(() => {
|
||||
|
@ -129,3 +147,21 @@ describe('renderApp', () => {
|
|||
}).not.toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('renderUxApp', () => {
|
||||
it('has an error boundary for the UXAppRoot', async () => {
|
||||
const uxMountProps = mockApmPluginContextValue;
|
||||
|
||||
const wrapper = mount(<UXAppRoot {...(uxMountProps as any)} />);
|
||||
|
||||
wrapper
|
||||
.find(RumHome)
|
||||
.simulateError(new Error('Oh no, an unexpected error!'));
|
||||
|
||||
expect(wrapper.find(RumHome)).toHaveLength(0);
|
||||
expect(wrapper.find(EuiErrorBoundary)).toHaveLength(1);
|
||||
expect(wrapper.find(EuiErrorBoundary).text()).toMatch(
|
||||
/Error: Oh no, an unexpected error!/
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { euiLightVars, euiDarkVars } from '@kbn/ui-shared-deps-src/theme';
|
||||
import { EuiErrorBoundary } from '@elastic/eui';
|
||||
import { AppMountParameters, CoreStart } from 'kibana/public';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
@ -132,7 +133,9 @@ export function UXAppRoot({
|
|||
<RouterProvider history={history} router={uxRouter}>
|
||||
<InspectorContextProvider>
|
||||
<UrlParamsProvider>
|
||||
<UxApp />
|
||||
<EuiErrorBoundary>
|
||||
<UxApp />
|
||||
</EuiErrorBoundary>
|
||||
<UXActionMenu appMountParameters={appMountParameters} />
|
||||
</UrlParamsProvider>
|
||||
</InspectorContextProvider>
|
||||
|
|
|
@ -6,11 +6,11 @@
|
|||
*/
|
||||
|
||||
import React, { ReactNode, useMemo } from 'react';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { RouterProvider } from '@kbn/typed-react-router-config';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { createMemoryHistory, History } from 'history';
|
||||
import { merge } from 'lodash';
|
||||
import { coreMock } from '../../../../../../src/core/public/mocks';
|
||||
import { UrlService } from '../../../../../../src/plugins/share/common/url_service';
|
||||
import { createObservabilityRuleTypeRegistryMock } from '../../../../observability/public';
|
||||
import { ApmPluginContext, ApmPluginContextValue } from './apm_plugin_context';
|
||||
|
@ -20,72 +20,43 @@ import { createCallApmApi } from '../../services/rest/createCallApmApi';
|
|||
import { apmRouter } from '../../components/routing/apm_route_config';
|
||||
import { MlLocatorDefinition } from '../../../../ml/public';
|
||||
|
||||
const uiSettings: Record<string, unknown> = {
|
||||
[UI_SETTINGS.TIMEPICKER_QUICK_RANGES]: [
|
||||
{
|
||||
from: 'now/d',
|
||||
to: 'now/d',
|
||||
display: 'Today',
|
||||
},
|
||||
{
|
||||
from: 'now/w',
|
||||
to: 'now/w',
|
||||
display: 'This week',
|
||||
},
|
||||
],
|
||||
[UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS]: {
|
||||
from: 'now-15m',
|
||||
to: 'now',
|
||||
},
|
||||
[UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS]: {
|
||||
pause: false,
|
||||
value: 100000,
|
||||
},
|
||||
};
|
||||
const coreStart = coreMock.createStart({ basePath: '/basepath' });
|
||||
|
||||
const mockCore = {
|
||||
const mockCore = merge({}, coreStart, {
|
||||
application: {
|
||||
capabilities: {
|
||||
apm: {},
|
||||
ml: {},
|
||||
},
|
||||
currentAppId$: new Observable(),
|
||||
getUrlForApp: (appId: string) => '',
|
||||
navigateToUrl: (url: string) => {},
|
||||
},
|
||||
chrome: {
|
||||
docTitle: { change: () => {} },
|
||||
setBreadcrumbs: () => {},
|
||||
setHelpExtension: () => {},
|
||||
setBadge: () => {},
|
||||
},
|
||||
docLinks: {
|
||||
DOC_LINK_VERSION: '0',
|
||||
ELASTIC_WEBSITE_URL: 'https://www.elastic.co/',
|
||||
links: {
|
||||
apm: {},
|
||||
},
|
||||
},
|
||||
http: {
|
||||
basePath: {
|
||||
prepend: (path: string) => `/basepath${path}`,
|
||||
get: () => `/basepath`,
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
Context: ({ children }: { children: ReactNode }) => children,
|
||||
},
|
||||
notifications: {
|
||||
toasts: {
|
||||
addWarning: () => {},
|
||||
addDanger: () => {},
|
||||
},
|
||||
},
|
||||
uiSettings: {
|
||||
get: (key: string) => uiSettings[key],
|
||||
get$: (key: string) => of(mockCore.uiSettings.get(key)),
|
||||
get: (key: string) => {
|
||||
const uiSettings: Record<string, unknown> = {
|
||||
[UI_SETTINGS.TIMEPICKER_QUICK_RANGES]: [
|
||||
{
|
||||
from: 'now/d',
|
||||
to: 'now/d',
|
||||
display: 'Today',
|
||||
},
|
||||
{
|
||||
from: 'now/w',
|
||||
to: 'now/w',
|
||||
display: 'This week',
|
||||
},
|
||||
],
|
||||
[UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS]: {
|
||||
from: 'now-15m',
|
||||
to: 'now',
|
||||
},
|
||||
[UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS]: {
|
||||
pause: false,
|
||||
value: 100000,
|
||||
},
|
||||
};
|
||||
return uiSettings[key];
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const mockConfig: ConfigSchema = {
|
||||
serviceMapEnabled: true,
|
||||
|
@ -118,16 +89,22 @@ const mockPlugin = {
|
|||
},
|
||||
};
|
||||
|
||||
const mockAppMountParameters = {
|
||||
setHeaderActionMenu: () => {},
|
||||
const mockCorePlugins = {
|
||||
embeddable: {},
|
||||
inspector: {},
|
||||
maps: {},
|
||||
observability: {},
|
||||
data: {},
|
||||
};
|
||||
|
||||
export const mockApmPluginContextValue = {
|
||||
appMountParameters: mockAppMountParameters,
|
||||
appMountParameters: coreMock.createAppMountParameters('/basepath'),
|
||||
config: mockConfig,
|
||||
core: mockCore,
|
||||
plugins: mockPlugin,
|
||||
observabilityRuleTypeRegistry: createObservabilityRuleTypeRegistryMock(),
|
||||
corePlugins: mockCorePlugins,
|
||||
deps: {},
|
||||
};
|
||||
|
||||
export function MockApmPluginContextWrapper({
|
||||
|
@ -135,7 +112,7 @@ export function MockApmPluginContextWrapper({
|
|||
value = {} as ApmPluginContextValue,
|
||||
history,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
children?: ReactNode;
|
||||
value?: ApmPluginContextValue;
|
||||
history?: History;
|
||||
}) {
|
||||
|
|
|
@ -23,7 +23,7 @@ import { renderApp, renderHeaderActions } from './';
|
|||
|
||||
describe('renderApp', () => {
|
||||
const kibanaDeps = {
|
||||
params: coreMock.createAppMountParamters(),
|
||||
params: coreMock.createAppMountParameters(),
|
||||
core: coreMock.createStart(),
|
||||
plugins: {
|
||||
licensing: licensingMock.createStart(),
|
||||
|
|
|
@ -59,7 +59,7 @@ describe('captureURLApp', () => {
|
|||
captureURLApp.create(coreSetupMock);
|
||||
|
||||
const [[{ mount }]] = coreSetupMock.application.register.mock.calls;
|
||||
await mount(coreMock.createAppMountParamters());
|
||||
await mount(coreMock.createAppMountParameters());
|
||||
|
||||
expect(mockLocationReplace).toHaveBeenCalledTimes(1);
|
||||
expect(mockLocationReplace).toHaveBeenCalledWith(
|
||||
|
@ -77,7 +77,7 @@ describe('captureURLApp', () => {
|
|||
captureURLApp.create(coreSetupMock);
|
||||
|
||||
const [[{ mount }]] = coreSetupMock.application.register.mock.calls;
|
||||
await mount(coreMock.createAppMountParamters());
|
||||
await mount(coreMock.createAppMountParameters());
|
||||
|
||||
expect(mockLocationReplace).toHaveBeenCalledTimes(1);
|
||||
expect(mockLocationReplace).toHaveBeenCalledWith(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue