Skip to content

Commit aff172e

Browse files
committed
frontend: add component tests for the alert component
1 parent ab45cd4 commit aff172e

File tree

2 files changed

+98
-6
lines changed

2 files changed

+98
-6
lines changed
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { render, screen } from '@testing-library/preact';
2+
import { fireEvent } from '@testing-library/preact';
3+
import { describe, it, expect, vi, beforeEach } from 'vitest';
4+
5+
// Helper to flush timers when using setTimeout
6+
const advanceTimers = async (ms: number) => {
7+
await vi.advanceTimersByTimeAsync(ms);
8+
};
9+
10+
describe('Alert component & showAlert', () => {
11+
beforeEach(() => {
12+
vi.useFakeTimers();
13+
window.scrollTo = vi.fn();
14+
});
15+
16+
it('renders an alert with heading and text', async () => {
17+
const real = await vi.importActual<typeof import('../Alert')>('../Alert');
18+
real.showAlert('Some text', 'danger', 'abc', 'Heading Text');
19+
render(<real.ErrorAlert />);
20+
expect(screen.getByText('Some text')).toBeTruthy();
21+
expect(screen.getByTestId('alert-heading')).toHaveTextContent('Heading Text');
22+
});
23+
24+
it('suppresses alert containing Failed to fetch', async () => {
25+
const real = await vi.importActual<typeof import('../Alert')>('../Alert');
26+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
27+
real.showAlert('Failed to fetch resource', 'danger');
28+
render(<real.ErrorAlert />);
29+
expect(screen.queryByText('Failed to fetch resource')).toBeNull();
30+
expect(warnSpy).toHaveBeenCalled();
31+
warnSpy.mockRestore();
32+
});
33+
34+
it('auto dismisses alert after timeout', async () => {
35+
const real = await vi.importActual<typeof import('../Alert')>('../Alert');
36+
real.showAlert('Autoclose', 'success', 'auto', 'Auto Heading', 2000);
37+
const { rerender } = render(<real.ErrorAlert />);
38+
expect(screen.getByText('Autoclose')).toBeTruthy();
39+
await advanceTimers(2000);
40+
rerender(<real.ErrorAlert />);
41+
expect(screen.queryByText('Autoclose')).toBeNull();
42+
});
43+
44+
it('replaces alert with same id and clears previous timeout', async () => {
45+
const real = await vi.importActual<typeof import('../Alert')>('../Alert');
46+
const clearSpy = vi.spyOn(window, 'clearTimeout');
47+
real.showAlert('First', 'warning', 'same', 'First Heading', 5000);
48+
const utils = render(<real.ErrorAlert />);
49+
expect(screen.getByText('First')).toBeTruthy();
50+
real.showAlert('Second', 'warning', 'same', 'Second Heading');
51+
utils.rerender(<real.ErrorAlert />);
52+
expect(screen.getByText('Second')).toBeTruthy();
53+
expect(screen.queryByText('First')).toBeNull();
54+
expect(clearSpy).toHaveBeenCalled();
55+
clearSpy.mockRestore();
56+
});
57+
58+
it('manual dismiss via close button triggers onClose and removes alert', async () => {
59+
const real = await vi.importActual<typeof import('../Alert')>('../Alert');
60+
real.showAlert('Dismiss me', 'danger', 'dismiss', 'Dismiss Heading');
61+
const { rerender } = render(<real.ErrorAlert />);
62+
expect(screen.getByText('Dismiss me')).toBeTruthy();
63+
const closeButtons = screen.getAllByTestId('close-alert');
64+
fireEvent.click(closeButtons[closeButtons.length - 1]);
65+
rerender(<real.ErrorAlert />);
66+
expect(screen.queryByText('Dismiss me')).toBeNull();
67+
});
68+
});

frontend/src/test-setup.ts

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ interface TabProps extends MockComponentProps {
6161

6262
interface AlertProps extends MockComponentProps {
6363
variant?: string;
64+
dismissible?: boolean;
65+
onClose?: () => void;
6466
}
6567

6668
interface ButtonProps extends MockComponentProps {
@@ -207,7 +209,16 @@ vi.mock('react-bootstrap', () => {
207209
h('div', { ...props, className: 'tab-pane' }, children);
208210

209211
const Alert = ({ children, variant, ...props }: AlertProps) =>
210-
h('div', { ...props, className: `alert alert-${variant || 'primary'}` }, children);
212+
h('div', { ...props, className: `alert alert-${variant || 'primary'}` }, [
213+
children,
214+
// Provide a close button when dismissible to simulate react-bootstrap behaviour
215+
props.dismissible && h('button', { 'data-testid': 'close-alert', onClick: props.onClose }, '×')
216+
]);
217+
218+
// Support <Alert.Heading> used by the component under test
219+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
220+
(Alert as any).Heading = ({ children, ...props }: MockComponentProps) =>
221+
h('h4', { ...props, 'data-testid': 'alert-heading' }, children);
211222

212223
const Spinner = ({ ...props }: MockComponentProps) =>
213224
h('div', { ...props, className: 'spinner' }, 'Loading...');
@@ -242,6 +253,18 @@ vi.mock('react-bootstrap', () => {
242253
};
243254
});
244255

256+
// Separate mock for direct 'react-bootstrap/Alert' import used in Alert component
257+
vi.mock('react-bootstrap/Alert', () => {
258+
const { h } = require('preact');
259+
const Alert = ({ children, variant, dismissible, onClose }: { children?: any; variant?: string; dismissible?: boolean; onClose?: () => void; }) =>
260+
h('div', { className: `alert alert-${variant || 'primary'}` }, [
261+
children,
262+
dismissible && h('button', { 'data-testid': 'close-alert', onClick: onClose }, '×')
263+
]);
264+
(Alert as any).Heading = ({ children }: { children?: any }) => h('h4', { 'data-testid': 'alert-heading' }, children);
265+
return { default: Alert };
266+
});
267+
245268
// Mock react-feather icons
246269
vi.mock('react-feather', () => ({
247270
ChevronDown: () => h('svg', { 'data-testid': 'chevron-down' }),
@@ -317,11 +340,6 @@ vi.mock('./utils', () => ({
317340
isDebugMode: { value: false },
318341
}));
319342

320-
// Mock Alert component
321-
vi.mock('./components/Alert', () => ({
322-
showAlert: vi.fn(),
323-
}));
324-
325343
// Mock preact-iso
326344
vi.mock('preact-iso', () => ({
327345
useLocation: () => ({
@@ -417,4 +435,10 @@ beforeAll(() => {
417435
},
418436
writable: true,
419437
});
438+
439+
window.scrollTo = vi.fn();
420440
});
441+
442+
vi.mock('./components/Alert', () => ({
443+
showAlert: vi.fn(),
444+
}));

0 commit comments

Comments
 (0)