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} +

+ +
)}
)} + {filteredItems && + filteredItems.length === 0 && + items && + items.length > 0 && ( + + )} {filteredItems && filteredItems.length > 0 && ( <>

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.status === 'error' && ( -

- {api.error} +
+

+ {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(); - await waitFor(() => { - expect(screen.getByRole('alert')).toBeInTheDocument(); - }); - const retryBtn = screen.getByRole('button', { name: 'Retry' }); - expect(retryBtn).toBeInTheDocument(); + const retryButton = await screen.findByRole('button', { name: /retry/i }); + expect(retryButton).toBeInTheDocument(); - mockFetch([{ source: 'USDC', destination: 'XLM' }]); - fireEvent.click(retryBtn); + fireEvent.click(retryButton); await waitFor(() => { - expect(screen.getByText('USDC/XLM')).toBeInTheDocument(); + expect(screen.getByText('1 pair')).toBeInTheDocument(); }); + expect(screen.queryByRole('alert')).not.toBeInTheDocument(); }); it('has exactly one aria-live=polite region', async () => {