Skip to content

Commit b84f9bf

Browse files
authored
feat: remove error boundary in favor of external boundary (#2580)
1 parent b6152da commit b84f9bf

File tree

11 files changed

+203
-173
lines changed

11 files changed

+203
-173
lines changed

e2e/helpers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ const groupsToSkip: Set<string> = new Set(['Components/Tooltip', 'Bullet Graph']
4444
*/
4545
const storiesToSkip: Map<string, string[]> = new Map(
4646
Object.entries({
47-
'Test Cases': ['noSeries'],
47+
'Test Cases': ['noSeries', 'errorBoundary'],
4848
Interactions: ['multiChartCursorSync'],
4949
'Metric (@alpha)': ['bodyContent'],
5050
}),
Loading

e2e/tests/test_cases_stories.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ import { eachRotation, pwEach } from '../helpers';
1313
import { common } from '../page_objects';
1414

1515
test.describe('Test cases stories', () => {
16+
test('should render EuiErrorBoundary when error is thrown is chart', async ({ page }) => {
17+
await common.expectElementAtUrlToMatchScreenshot(page)(
18+
'http://localhost:9001/?path=/story/test-cases--error-boundary&knob-throws error=true',
19+
'#story-root',
20+
);
21+
});
22+
1623
// See https://github.com/elastic/elastic-charts/issues/2456
1724
test('should render sunburst as full circle', async ({ page }) => {
1825
await common.expectChartAtUrlToMatchScreenshot(page)(

packages/charts/src/components/__snapshots__/chart.test.tsx.snap

Lines changed: 70 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -54,78 +54,76 @@ exports[`Chart should render the legend name test 1`] = `
5454
</div>
5555
</LegendComponent>
5656
</Connect(LegendComponent)>
57-
<ErrorBoundary>
58-
<Connect(SpecsParserComponent)>
59-
<SpecsParserComponent specParsed={[Function (anonymous)]} specUnmounted={[Function (anonymous)]}>
60-
<Settings debug={true} rendering="svg" showLegend={true} />
61-
<BarSeries id="test" data={{...}} xAccessor="x" yAccessors={{...}} />
62-
</SpecsParserComponent>
63-
</Connect(SpecsParserComponent)>
64-
<div className="echContainer">
65-
<Connect(ChartContainer) getChartContainerRef={[Function (anonymous)]} forwardStageRef={{...}}>
66-
<ChartContainer getChartContainerRef={[Function (anonymous)]} forwardStageRef={{...}} status="Initialized" initialized={true} tooltipState={{...}} isChartEmpty={false} canPinTooltip={false} pointerCursor={[undefined]} isBrushingAvailable={false} isBrushing={false} internalChartRenderer={[Function: chartRenderer]} settings={{...}} tooltip={{...}} disableInteractions={false} onPointerMove={[Function (anonymous)]} onMouseUp={[Function (anonymous)]} onMouseDown={[Function (anonymous)]} onKeyPress={[Function (anonymous)]} pinTooltip={[Function (anonymous)]}>
67-
<div className="echChartPointerContainer" style={{...}} onMouseMove={[Function (anonymous)]} onMouseLeave={[Function (anonymous)]} onMouseDown={[Function (anonymous)]} onMouseUp={[Function (anonymous)]} onContextMenu={[undefined]}>
68-
<Connect(CursorBand)>
69-
<CursorBand isBrushing={false} theme={{...}} chartRotation={0} cursorPosition={[undefined]} tooltipType="vertical" fromExternalEvent={[undefined]} tooltipState={{...}} dispatch={[Function: dispatch]} />
70-
</Connect(CursorBand)>
71-
<Connect(XYChart) forwardCanvasRef={{...}}>
72-
<XYChart forwardCanvasRef={{...}} locale="en-US" isRTL={false} initialized={true} isChartEmpty={false} debug={true} geometries={{...}} geometriesIndex={{...}} theme={{...}} chartContainerDimensions={{...}} highlightedLegendItem={[undefined]} hoveredAnnotationIds={{...}} rotation={0} renderingArea={{...}} chartTransform={{...}} axesSpecs={{...}} perPanelAxisGeoms={{...}} perPanelGridLines={{...}} axesStyles={{...}} annotationDimensions={{...}} annotationSpecs={{...}} panelGeoms={{...}} a11ySettings={{...}} onChartRendered={[Function (anonymous)]}>
73-
<figure aria-labelledby={[undefined]} aria-describedby="chart1--defaultSummary">
74-
<canvas dir="ltr" className="echCanvasRenderer" width={170} height={200} style={{...}} role="presentation" />
75-
</figure>
76-
<Connect(ScreenReaderSummaryComponent)>
77-
<ScreenReaderSummaryComponent chartTypeDescription="bar chart" a11ySettings={{...}} goalChartData={{...}} goalChartLabels={{...}} dispatch={[Function: dispatch]}>
78-
<div className="echScreenReaderOnly">
79-
<ScreenReaderLabel label={[undefined]} labelId={[undefined]} labelHeadingLevel="p" description={[undefined]} descriptionId="chart1--defaultSummary" defaultSummaryId="chart1--defaultSummary" tableCaption={[undefined]} goalChartLabels={{...}} />
80-
<ScreenReaderDescription label={[undefined]} labelId={[undefined]} labelHeadingLevel="p" description={[undefined]} descriptionId="chart1--defaultSummary" defaultSummaryId="chart1--defaultSummary" tableCaption={[undefined]} />
81-
<ScreenReaderTypes label={[undefined]} labelId={[undefined]} labelHeadingLevel="p" description={[undefined]} descriptionId="chart1--defaultSummary" defaultSummaryId="chart1--defaultSummary" tableCaption={[undefined]} chartTypeDescription="bar chart" goalChartData={{...}}>
82-
<dl>
83-
<dt>
84-
Chart type:
85-
</dt>
86-
<dd id="chart1--defaultSummary">
87-
bar chart
88-
</dd>
89-
</dl>
90-
</ScreenReaderTypes>
91-
</div>
92-
</ScreenReaderSummaryComponent>
93-
</Connect(ScreenReaderSummaryComponent)>
94-
</XYChart>
95-
</Connect(XYChart)>
96-
<Connect(CursorLine)>
97-
<CursorLine isBrushing={false} theme={{...}} chartRotation={0} cursorPosition={[undefined]} tooltipType="vertical" fromExternalEvent={[undefined]} isLine={false} tooltipState={{...}} dispatch={[Function: dispatch]} />
98-
</Connect(CursorLine)>
99-
<Connect(CursorCrossLine)>
100-
<CursorCrossLine theme={{...}} chartRotation={0} cursorCrossLinePosition={[undefined]} tooltipType="vertical" dispatch={[Function: dispatch]} />
101-
</Connect(CursorCrossLine)>
102-
<Connect(Tooltip) getChartContainerRef={[Function (anonymous)]}>
103-
<Tooltip getChartContainerRef={[Function (anonymous)]} tooltip={{...}} isExternal={false} isBrushing={false} zIndex={0} settings={{...}} tooltipTheme={{...}} rotation={0} chartId="chart1" backgroundColor="#FFF" visible={false} position={{...}} info={{...}} pinned={false} selected={{...}} canPinTooltip={false} onPointerMove={[Function (anonymous)]} toggleSelectedTooltipItem={[Function (anonymous)]} setSelectedTooltipItems={[Function (anonymous)]} pinTooltip={[Function (anonymous)]} />
104-
</Connect(Tooltip)>
105-
<Connect(Annotations) getChartContainerRef={[Function (anonymous)]} chartAreaRef={{...}}>
106-
<Annotations getChartContainerRef={[Function (anonymous)]} chartAreaRef={{...}} isChartEmpty={false} chartDimensions={{...}} sharedStyle={{...}} annotationDimensions={{...}} annotationSpecs={{...}} tooltipState={{...}} chartId="chart1" zIndex={0} hoveredAnnotationIds={{...}} clickable={false} onPointerMove={[Function (anonymous)]} onDOMElementLeave={[Function (anonymous)]} onDOMElementEnter={[Function (anonymous)]} onDOMElementClick={[Function (anonymous)]}>
107-
<AnnotationTooltip chartId="chart1" zIndex={0} state={{...}} chartRef={{...}} onScroll={[Function (anonymous)]} />
108-
</Annotations>
109-
</Connect(Annotations)>
110-
<Connect(Highlighter)>
111-
<Highlighter initialized={true} chartId="chart1" zIndex={0} isBrushing={false} highlightedGeometries={{...}} chartTransform={{...}} chartDimensions={{...}} chartRotation={0} style={{...}} dispatch={[Function: dispatch]}>
112-
<svg className="echHighlighter" style={{...}}>
113-
<defs>
114-
<clipPath id="echHighlighterClipPath__chart1">
115-
<rect x="0" y="0" width={170} height={200} />
116-
</clipPath>
117-
</defs>
118-
</svg>
119-
</Highlighter>
120-
</Connect(Highlighter)>
121-
<Connect(BrushTool)>
122-
<BrushTool initialized={true} projectionContainer={{...}} mainProjectionArea={{...}} isBrushAvailable={false} isBrushing={false} brushEvent={{...}} zIndex={0} dispatch={[Function: dispatch]} />
123-
</Connect(BrushTool)>
124-
</div>
125-
</ChartContainer>
126-
</Connect(ChartContainer)>
127-
</div>
128-
</ErrorBoundary>
57+
<Connect(SpecsParserComponent)>
58+
<SpecsParserComponent specParsed={[Function (anonymous)]} specUnmounted={[Function (anonymous)]}>
59+
<Settings debug={true} rendering="svg" showLegend={true} />
60+
<BarSeries id="test" data={{...}} xAccessor="x" yAccessors={{...}} />
61+
</SpecsParserComponent>
62+
</Connect(SpecsParserComponent)>
63+
<div className="echContainer">
64+
<Connect(ChartContainer) getChartContainerRef={[Function (anonymous)]} forwardStageRef={{...}}>
65+
<ChartContainer getChartContainerRef={[Function (anonymous)]} forwardStageRef={{...}} status="Initialized" initialized={true} tooltipState={{...}} isChartEmpty={false} canPinTooltip={false} pointerCursor={[undefined]} isBrushingAvailable={false} isBrushing={false} internalChartRenderer={[Function: chartRenderer]} settings={{...}} tooltip={{...}} disableInteractions={false} onPointerMove={[Function (anonymous)]} onMouseUp={[Function (anonymous)]} onMouseDown={[Function (anonymous)]} onKeyPress={[Function (anonymous)]} pinTooltip={[Function (anonymous)]}>
66+
<div className="echChartPointerContainer" style={{...}} onMouseMove={[Function (anonymous)]} onMouseLeave={[Function (anonymous)]} onMouseDown={[Function (anonymous)]} onMouseUp={[Function (anonymous)]} onContextMenu={[undefined]}>
67+
<Connect(CursorBand)>
68+
<CursorBand isBrushing={false} theme={{...}} chartRotation={0} cursorPosition={[undefined]} tooltipType="vertical" fromExternalEvent={[undefined]} tooltipState={{...}} dispatch={[Function: dispatch]} />
69+
</Connect(CursorBand)>
70+
<Connect(XYChart) forwardCanvasRef={{...}}>
71+
<XYChart forwardCanvasRef={{...}} locale="en-US" isRTL={false} initialized={true} isChartEmpty={false} debug={true} geometries={{...}} geometriesIndex={{...}} theme={{...}} chartContainerDimensions={{...}} highlightedLegendItem={[undefined]} hoveredAnnotationIds={{...}} rotation={0} renderingArea={{...}} chartTransform={{...}} axesSpecs={{...}} perPanelAxisGeoms={{...}} perPanelGridLines={{...}} axesStyles={{...}} annotationDimensions={{...}} annotationSpecs={{...}} panelGeoms={{...}} a11ySettings={{...}} onChartRendered={[Function (anonymous)]}>
72+
<figure aria-labelledby={[undefined]} aria-describedby="chart1--defaultSummary">
73+
<canvas dir="ltr" className="echCanvasRenderer" width={170} height={200} style={{...}} role="presentation" />
74+
</figure>
75+
<Connect(ScreenReaderSummaryComponent)>
76+
<ScreenReaderSummaryComponent chartTypeDescription="bar chart" a11ySettings={{...}} goalChartData={{...}} goalChartLabels={{...}} dispatch={[Function: dispatch]}>
77+
<div className="echScreenReaderOnly">
78+
<ScreenReaderLabel label={[undefined]} labelId={[undefined]} labelHeadingLevel="p" description={[undefined]} descriptionId="chart1--defaultSummary" defaultSummaryId="chart1--defaultSummary" tableCaption={[undefined]} goalChartLabels={{...}} />
79+
<ScreenReaderDescription label={[undefined]} labelId={[undefined]} labelHeadingLevel="p" description={[undefined]} descriptionId="chart1--defaultSummary" defaultSummaryId="chart1--defaultSummary" tableCaption={[undefined]} />
80+
<ScreenReaderTypes label={[undefined]} labelId={[undefined]} labelHeadingLevel="p" description={[undefined]} descriptionId="chart1--defaultSummary" defaultSummaryId="chart1--defaultSummary" tableCaption={[undefined]} chartTypeDescription="bar chart" goalChartData={{...}}>
81+
<dl>
82+
<dt>
83+
Chart type:
84+
</dt>
85+
<dd id="chart1--defaultSummary">
86+
bar chart
87+
</dd>
88+
</dl>
89+
</ScreenReaderTypes>
90+
</div>
91+
</ScreenReaderSummaryComponent>
92+
</Connect(ScreenReaderSummaryComponent)>
93+
</XYChart>
94+
</Connect(XYChart)>
95+
<Connect(CursorLine)>
96+
<CursorLine isBrushing={false} theme={{...}} chartRotation={0} cursorPosition={[undefined]} tooltipType="vertical" fromExternalEvent={[undefined]} isLine={false} tooltipState={{...}} dispatch={[Function: dispatch]} />
97+
</Connect(CursorLine)>
98+
<Connect(CursorCrossLine)>
99+
<CursorCrossLine theme={{...}} chartRotation={0} cursorCrossLinePosition={[undefined]} tooltipType="vertical" dispatch={[Function: dispatch]} />
100+
</Connect(CursorCrossLine)>
101+
<Connect(Tooltip) getChartContainerRef={[Function (anonymous)]}>
102+
<Tooltip getChartContainerRef={[Function (anonymous)]} tooltip={{...}} isExternal={false} isBrushing={false} zIndex={0} settings={{...}} tooltipTheme={{...}} rotation={0} chartId="chart1" backgroundColor="#FFF" visible={false} position={{...}} info={{...}} pinned={false} selected={{...}} canPinTooltip={false} onPointerMove={[Function (anonymous)]} toggleSelectedTooltipItem={[Function (anonymous)]} setSelectedTooltipItems={[Function (anonymous)]} pinTooltip={[Function (anonymous)]} />
103+
</Connect(Tooltip)>
104+
<Connect(Annotations) getChartContainerRef={[Function (anonymous)]} chartAreaRef={{...}}>
105+
<Annotations getChartContainerRef={[Function (anonymous)]} chartAreaRef={{...}} isChartEmpty={false} chartDimensions={{...}} sharedStyle={{...}} annotationDimensions={{...}} annotationSpecs={{...}} tooltipState={{...}} chartId="chart1" zIndex={0} hoveredAnnotationIds={{...}} clickable={false} onPointerMove={[Function (anonymous)]} onDOMElementLeave={[Function (anonymous)]} onDOMElementEnter={[Function (anonymous)]} onDOMElementClick={[Function (anonymous)]}>
106+
<AnnotationTooltip chartId="chart1" zIndex={0} state={{...}} chartRef={{...}} onScroll={[Function (anonymous)]} />
107+
</Annotations>
108+
</Connect(Annotations)>
109+
<Connect(Highlighter)>
110+
<Highlighter initialized={true} chartId="chart1" zIndex={0} isBrushing={false} highlightedGeometries={{...}} chartTransform={{...}} chartDimensions={{...}} chartRotation={0} style={{...}} dispatch={[Function: dispatch]}>
111+
<svg className="echHighlighter" style={{...}}>
112+
<defs>
113+
<clipPath id="echHighlighterClipPath__chart1">
114+
<rect x="0" y="0" width={170} height={200} />
115+
</clipPath>
116+
</defs>
117+
</svg>
118+
</Highlighter>
119+
</Connect(Highlighter)>
120+
<Connect(BrushTool)>
121+
<BrushTool initialized={true} projectionContainer={{...}} mainProjectionArea={{...}} isBrushAvailable={false} isBrushing={false} brushEvent={{...}} zIndex={0} dispatch={[Function: dispatch]} />
122+
</Connect(BrushTool)>
123+
</div>
124+
</ChartContainer>
125+
</Connect(ChartContainer)>
126+
</div>
129127
</div>
130128
</div>
131129
</Provider>

packages/charts/src/components/chart.tsx

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import { ChartBackground } from './chart_background';
1717
import { ChartContainer } from './chart_container';
1818
import { ChartResizer } from './chart_resizer';
1919
import { ChartStatus } from './chart_status';
20-
import { ErrorBoundary } from './error_boundary';
2120
import { Legend } from './legend/legend';
2221
import { getElementZIndex } from './portal/utils';
2322
import { Colors } from '../common/colors';
@@ -180,13 +179,10 @@ export class Chart extends React.Component<ChartProps, ChartState> {
180179
<ChartStatus />
181180
<ChartResizer />
182181
<Legend />
183-
{/* TODO: Add renderFn to error boundary */}
184-
<ErrorBoundary>
185-
<SpecsParser>{this.props.children}</SpecsParser>
186-
<div className="echContainer" ref={this.chartContainerRef}>
187-
<ChartContainer getChartContainerRef={this.getChartContainerRef} forwardStageRef={this.chartStageRef} />
188-
</div>
189-
</ErrorBoundary>
182+
<SpecsParser>{this.props.children}</SpecsParser>
183+
<div className="echContainer" ref={this.chartContainerRef}>
184+
<ChartContainer getChartContainerRef={this.getChartContainerRef} forwardStageRef={this.chartStageRef} />
185+
</div>
190186
</div>
191187
</div>
192188
</Provider>
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
import { mount } from 'enzyme';
10+
import React from 'react';
11+
12+
import { Chart } from './chart';
13+
14+
class SimpleErrorBoundary extends React.Component<{ onError?: (error: Error) => void }, { hasError: boolean }> {
15+
onError?: (error: Error) => void;
16+
constructor(props: { onError: (error: Error) => void }) {
17+
super(props);
18+
this.onError = props.onError;
19+
this.state = { hasError: false };
20+
}
21+
22+
static getDerivedStateFromError() {
23+
return { hasError: true };
24+
}
25+
26+
componentDidCatch(error: Error): void {
27+
this.onError?.(error);
28+
}
29+
30+
render() {
31+
if (this.state.hasError) {
32+
return <div id="simple-error">Error occurred</div>;
33+
}
34+
35+
return this.props.children;
36+
}
37+
}
38+
39+
describe('Error boundary', () => {
40+
it('should render error boundary when error thrown inside chart', () => {
41+
const onError = jest.fn();
42+
const Series = () => {
43+
throw new Error('What happened???');
44+
};
45+
46+
const wrapper = mount(
47+
<SimpleErrorBoundary onError={onError}>
48+
<Chart size={[100, 100]} id="chart1">
49+
<Series />
50+
</Chart>
51+
</SimpleErrorBoundary>,
52+
);
53+
const errorEl = wrapper.find('#simple-error');
54+
const chartEl = wrapper.find('.echChart');
55+
56+
expect(errorEl.exists()).toBe(true);
57+
expect(chartEl.exists()).toBe(false);
58+
expect(onError).toHaveBeenCalledWith(
59+
expect.objectContaining({
60+
message: 'What happened???',
61+
}),
62+
);
63+
});
64+
});

0 commit comments

Comments
 (0)