From f94bf26f029654d27d39d8c066b8c7e9ceb60009 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Thu, 23 Apr 2026 18:42:22 -0400 Subject: [PATCH] feat(web): add ApiErrorBanner retry-capable error card --- web/src/lib/components/ApiErrorBanner.svelte | 22 +++++++++++++ web/src/lib/components/ApiErrorBanner.test.ts | 32 +++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 web/src/lib/components/ApiErrorBanner.svelte create mode 100644 web/src/lib/components/ApiErrorBanner.test.ts diff --git a/web/src/lib/components/ApiErrorBanner.svelte b/web/src/lib/components/ApiErrorBanner.svelte new file mode 100644 index 00000000..3bf9399b --- /dev/null +++ b/web/src/lib/components/ApiErrorBanner.svelte @@ -0,0 +1,22 @@ + + + diff --git a/web/src/lib/components/ApiErrorBanner.test.ts b/web/src/lib/components/ApiErrorBanner.test.ts new file mode 100644 index 00000000..299405b2 --- /dev/null +++ b/web/src/lib/components/ApiErrorBanner.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, test, vi } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/svelte'; +import ApiErrorBanner from './ApiErrorBanner.svelte'; + +describe('ApiErrorBanner', () => { + test('renders error.message when present', () => { + render(ApiErrorBanner, { + props: { + error: { code: 'server_error', message: 'database down', status: 500 }, + onRetry: () => {} + } + }); + expect(screen.getByRole('alert')).toHaveTextContent('database down'); + }); + + test('falls back to generic text when error lacks message', () => { + render(ApiErrorBanner, { props: { error: undefined, onRetry: () => {} } }); + expect(screen.getByRole('alert')).toHaveTextContent(/something went wrong/i); + }); + + test('clicking the button calls onRetry', async () => { + const onRetry = vi.fn(); + render(ApiErrorBanner, { + props: { + error: { code: 'server_error', message: 'x', status: 500 }, + onRetry + } + }); + await fireEvent.click(screen.getByRole('button', { name: /try again/i })); + expect(onRetry).toHaveBeenCalledTimes(1); + }); +});