Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions src/app/events/Client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -202,9 +202,18 @@ export default function EventsClient() {
</div>
</fieldset>
{error && (
<p role="alert" className="text-sm text-rose-600">
{error}
</p>
<div role="alert" className="flex flex-wrap items-center gap-3 rounded-lg border border-rose-200 bg-rose-50 p-4 dark:border-rose-900 dark:bg-rose-950">
<p className="text-sm text-rose-600 dark:text-rose-400">
{error}
</p>
<button
type="button"
onClick={refetchEvents}
className="rounded-full bg-rose-600 px-4 py-1.5 text-xs font-medium text-white hover:bg-rose-700 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500"
>
Retry
</button>
</div>
)}
<section
aria-labelledby="events-log-heading"
Expand All @@ -225,6 +234,15 @@ export default function EventsClient() {
description="Router events will appear here once activity starts."
/>
)}
{filteredItems &&
filteredItems.length === 0 &&
items &&
items.length > 0 && (
<EmptyState
title="No events match the filter"
description="Try a different event type or clear the filter to see all events."
/>
)}
{filteredItems && filteredItems.length > 0 && (
<>
<p className="text-sm text-neutral-600 dark:text-neutral-400">
Expand Down
62 changes: 61 additions & 1 deletion src/app/events/page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -1047,19 +1077,49 @@ 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), {
target: { value: 'no-such-type' },
});

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(() => {})
Expand Down
11 changes: 5 additions & 6 deletions src/app/pairs/Client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,15 +105,14 @@ export default function PairsClient() {
<ColumnVisibilityToggle visibility={visibility} onToggle={toggle} />
</div>
{api.status === 'error' && (
<div
role="alert"
className="flex items-center justify-between rounded-lg border border-rose-200 bg-rose-50 p-4 text-sm text-rose-600 dark:border-rose-900/50 dark:bg-rose-950/30"
>
<span>{api.error}</span>
<div role="alert" className="flex flex-wrap items-center gap-3 rounded-lg border border-rose-200 bg-rose-50 p-4 dark:border-rose-900 dark:bg-rose-950">
<p className="text-sm text-rose-600 dark:text-rose-400">
{api.error}
</p>
<button
type="button"
onClick={api.refetch}
className="rounded border border-rose-300 px-3 py-1 text-xs font-medium hover:bg-rose-100 dark:border-rose-700 dark:hover:bg-rose-900/40"
className="rounded-full bg-rose-600 px-4 py-1.5 text-xs font-medium text-white hover:bg-rose-700 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500"
>
Retry
</button>
Expand Down
28 changes: 18 additions & 10 deletions src/app/pairs/page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<PairsPage />);
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 () => {
Expand Down
Loading