diff --git a/src/app/events/Client.tsx b/src/app/events/Client.tsx index eb3ec21..ae2ffd2 100644 --- a/src/app/events/Client.tsx +++ b/src/app/events/Client.tsx @@ -202,9 +202,18 @@ export default function EventsClient() { {error && ( -
- {error} -
++ {error} +
+ +
diff --git a/src/app/events/page.test.tsx b/src/app/events/page.test.tsx
index fda6881..07dff0d 100644
--- a/src/app/events/page.test.tsx
+++ b/src/app/events/page.test.tsx
@@ -145,6 +145,36 @@ describe('EventsPage', () => {
});
});
+ it('renders a Retry button on error and refetches when clicked', async () => {
+ global.fetch = jest
+ .fn()
+ .mockRejectedValueOnce(new Error('Failed to load'))
+ .mockResolvedValueOnce({
+ ok: true,
+ text: async () =>
+ JSON.stringify({
+ items: [
+ {
+ id: 'evt-recovered',
+ ts: 1_782_460_000_000,
+ type: 'pair.registered',
+ payload: {},
+ },
+ ],
+ }),
+ } as unknown as Response);
+
+ renderPage();
+
+ const retryButton = await screen.findByRole('button', { name: /retry/i });
+ expect(retryButton).toBeInTheDocument();
+
+ fireEvent.click(retryButton);
+
+ expect(await screen.findByText('pair.registered')).toBeInTheDocument();
+ expect(screen.queryByRole('alert')).not.toBeInTheDocument();
+ });
+
it('has exactly one aria-live=polite region in the page content', async () => {
global.fetch = jest.fn().mockResolvedValueOnce({
ok: true,
@@ -1047,7 +1077,7 @@ describe('EventsPage', () => {
expect(screen.getAllByRole('listitem')).toHaveLength(1);
});
- it('hides the list when the filter matches nothing', async () => {
+ it('shows filter-empty state when the filter matches nothing', async () => {
await renderWithEvents();
fireEvent.change(screen.getByLabelText(/filter by event type/i), {
@@ -1055,11 +1085,41 @@ describe('EventsPage', () => {
});
expect(screen.queryByRole('listitem')).not.toBeInTheDocument();
+ expect(screen.getByText(/No events match the filter/i)).toBeInTheDocument();
expect(
screen.getByRole('button', { name: /clear filters/i })
).toBeEnabled();
});
+ it('keeps the filter-empty state announcement inside the polite live region', async () => {
+ await renderWithEvents();
+
+ fireEvent.change(screen.getByLabelText(/filter by event type/i), {
+ target: { value: 'no-such-type' },
+ });
+
+ const emptyMsg = screen.getByText(/No events match the filter/i);
+ const regions = document.querySelectorAll('main [aria-live=polite]');
+ expect(regions).toHaveLength(1);
+ expect(regions[0]).toContainElement(emptyMsg);
+ });
+
+ it('does not show filter-empty state when there are no events at all', async () => {
+ global.fetch = jest.fn().mockResolvedValueOnce({
+ ok: true,
+ text: async () => JSON.stringify({ items: [] }),
+ } as unknown as Response);
+
+ renderPage();
+ await waitFor(() => {
+ expect(screen.getByText(/No events yet/i)).toBeInTheDocument();
+ });
+
+ expect(
+ screen.queryByText(/No events match the filter/i)
+ ).not.toBeInTheDocument();
+ });
+
it('offers the filter controls before any events load', () => {
global.fetch = jest.fn(
() => new Promise(() => {})
diff --git a/src/app/pairs/Client.tsx b/src/app/pairs/Client.tsx
index 17eeed7..b8515a0 100644
--- a/src/app/pairs/Client.tsx
+++ b/src/app/pairs/Client.tsx
@@ -105,15 +105,14 @@ export default function PairsClient() {
+ {api.error} +
diff --git a/src/app/pairs/page.test.tsx b/src/app/pairs/page.test.tsx index 54c5a39..fb9dabd 100644 --- a/src/app/pairs/page.test.tsx +++ b/src/app/pairs/page.test.tsx @@ -267,22 +267,30 @@ describe('PairsPage', () => { ); }); - it('renders retry button on error and refetches on click', async () => { - mockFetchError('Network error'); + it('renders a Retry button on error and refetches when clicked', async () => { + global.fetch = jest + .fn() + .mockRejectedValueOnce(new Error('Network error')) + .mockResolvedValueOnce({ + ok: true, + status: 200, + text: async () => + JSON.stringify({ + pairs: [{ source: 'USDC', destination: 'EURC' }], + }), + } as unknown as Response); + render(