Skip to content

Commit 675b110

Browse files
authored
fix: Ensure ErrorBoundary and PanelErrorBoundary do not throw (#2345)
- If either were asked to render `undefined` children, they would throw. - They should be catching all errors, so we definitely do not want them to throw. They should be more robust. - Added unit tests.
1 parent 6e813fd commit 675b110

4 files changed

Lines changed: 128 additions & 2 deletions

File tree

packages/components/src/ErrorBoundary.test.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,9 @@ it('should render the fallback if there is an error', () => {
4040
expect(getByText('Fallback')).toBeInTheDocument();
4141
expect(onError).toHaveBeenCalledWith(error, expect.anything());
4242
});
43+
44+
it('should not throw if children are undefined', () => {
45+
expect(() =>
46+
render(<ErrorBoundary>{undefined}</ErrorBoundary>)
47+
).not.toThrow();
48+
});

packages/components/src/ErrorBoundary.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,10 @@ export class ErrorBoundary extends Component<
6363
</div>
6464
);
6565
}
66-
return children;
66+
67+
// We need to check for undefined children because React will throw an error if we return undefined from a render method
68+
// Note this behaviour was changed in React 18: https://github.com/reactwg/react-18/discussions/75
69+
return children ?? null;
6770
}
6871
}
6972

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import React from 'react';
2+
import { render, screen } from '@testing-library/react';
3+
import { LoadingOverlay } from '@deephaven/components';
4+
import type { Container, EventEmitter } from '@deephaven/golden-layout';
5+
import { TestUtils } from '@deephaven/test-utils';
6+
import PanelErrorBoundary from './PanelErrorBoundary';
7+
import PanelEvent from './PanelEvent';
8+
import LayoutUtils from './layout/LayoutUtils';
9+
10+
jest.mock('@deephaven/components', () => ({
11+
LoadingOverlay: jest.fn(() => null),
12+
}));
13+
14+
jest.mock('./layout/LayoutUtils', () => ({
15+
getIdFromContainer: jest.fn(),
16+
}));
17+
18+
describe('PanelErrorBoundary', () => {
19+
const mockPanelId = 'test-panel-id';
20+
const mockGlContainer = {
21+
// Add minimal container implementation
22+
on: jest.fn(),
23+
off: jest.fn(),
24+
} as unknown as Container;
25+
26+
const mockGlEventHub = {
27+
emit: jest.fn(),
28+
} as unknown as EventEmitter;
29+
30+
const TestChild = function TestChildComponent() {
31+
return <div>Test Child Content</div>;
32+
};
33+
const ErrorChild = () => {
34+
throw new Error('Test error');
35+
};
36+
37+
beforeAll(() => {
38+
TestUtils.disableConsoleOutput();
39+
});
40+
41+
beforeEach(() => {
42+
jest.clearAllMocks();
43+
(LayoutUtils.getIdFromContainer as jest.Mock).mockReturnValue(mockPanelId);
44+
});
45+
46+
afterAll(() => {
47+
jest.restoreAllMocks();
48+
});
49+
50+
it('renders children when there is no error', () => {
51+
render(
52+
<PanelErrorBoundary
53+
glContainer={mockGlContainer}
54+
glEventHub={mockGlEventHub}
55+
>
56+
<TestChild />
57+
</PanelErrorBoundary>
58+
);
59+
60+
expect(screen.getByText('Test Child Content')).toBeInTheDocument();
61+
});
62+
63+
it('renders error overlay when there is an error', () => {
64+
render(
65+
<PanelErrorBoundary
66+
glContainer={mockGlContainer}
67+
glEventHub={mockGlEventHub}
68+
>
69+
<ErrorChild />
70+
</PanelErrorBoundary>
71+
);
72+
73+
expect(LoadingOverlay).toHaveBeenCalledWith(
74+
expect.objectContaining({
75+
errorMessage: 'Error: Test error',
76+
isLoading: false,
77+
isLoaded: false,
78+
}),
79+
expect.any(Object)
80+
);
81+
});
82+
83+
it('emits CLOSED event on unmount', () => {
84+
const { unmount } = render(
85+
<PanelErrorBoundary
86+
glContainer={mockGlContainer}
87+
glEventHub={mockGlEventHub}
88+
>
89+
<TestChild />
90+
</PanelErrorBoundary>
91+
);
92+
93+
unmount();
94+
95+
expect(mockGlEventHub.emit).toHaveBeenCalledWith(
96+
PanelEvent.CLOSED,
97+
mockPanelId,
98+
mockGlContainer
99+
);
100+
});
101+
102+
it('should not throw if children are undefined', () => {
103+
expect(() =>
104+
render(
105+
<PanelErrorBoundary
106+
glContainer={mockGlContainer}
107+
glEventHub={mockGlEventHub}
108+
>
109+
{undefined}
110+
</PanelErrorBoundary>
111+
)
112+
).not.toThrow();
113+
});
114+
});

packages/dashboard/src/PanelErrorBoundary.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,10 @@ class PanelErrorBoundary extends Component<
5757
</div>
5858
);
5959
}
60-
return children;
60+
61+
// We need to check for undefined children because React will throw an error if we return undefined from a render method
62+
// Note this behaviour was changed in React 18: https://github.com/reactwg/react-18/discussions/75
63+
return children ?? null;
6164
}
6265
}
6366

0 commit comments

Comments
 (0)