diff --git a/dashboard/reports/wallet-integration.json b/dashboard/reports/wallet-integration.json index 28eeafdf..bf64f317 100644 --- a/dashboard/reports/wallet-integration.json +++ b/dashboard/reports/wallet-integration.json @@ -1,6 +1,5 @@ { - "generatedAt": "2026-06-26T13:01:40.990Z", - "generatedAt": "2026-06-27T12:47:07.864Z", + "generatedAt": "2026-07-24T23:59:45.120Z", "total": 3, "passed": 3, "failed": 0, diff --git a/dashboard/src/components/ActivityFeed.tsx b/dashboard/src/components/ActivityFeed.tsx index 4ca686aa..14afb512 100644 --- a/dashboard/src/components/ActivityFeed.tsx +++ b/dashboard/src/components/ActivityFeed.tsx @@ -202,6 +202,9 @@ export function ActivityFeed() { Array.from({ length: 5 }).map((_, i) => ) ) : displayedEvents.length === 0 ? ( { + const { container } = render( + + ); + const results = await axe(container); + expect(results).toHaveNoViolations(); +}); + +test('renders the required message and optional title', () => { + render(); + expect(screen.getByText('No events found')).toBeInTheDocument(); + expect(screen.getByText('Update your filters to see results.')).toBeInTheDocument(); +}); + +test('omits the title when none is provided', () => { + render(); + expect(screen.queryByRole('heading')).not.toBeInTheDocument(); + expect(screen.getByText('Nothing here yet.')).toBeInTheDocument(); +}); + +test('renders an action button and fires its callback on click', () => { + const onClick = jest.fn(); + render( + + ); + fireEvent.click(screen.getByRole('button', { name: 'Create Template' })); + expect(onClick).toHaveBeenCalledTimes(1); +}); + +test('omits the action button when none is provided', () => { + render(); + expect(screen.queryByRole('button')).not.toBeInTheDocument(); +}); + +test('applies the default size class unless overridden', () => { + const { container, rerender } = render(); + expect(container.firstChild).toHaveClass('empty-state--default'); + + rerender(); + expect(container.firstChild).toHaveClass('empty-state--compact'); + + rerender(); + expect(container.firstChild).toHaveClass('empty-state--inline'); +}); + +test('renders a custom icon in place of the default one', () => { + render(} />); + expect(screen.getByTestId('custom-icon')).toBeInTheDocument(); +}); + +test('exposes a status role so screen readers announce the empty state', () => { + render(); + expect(screen.getByRole('status')).toHaveTextContent('No data available.'); +}); diff --git a/dashboard/src/components/EmptyState.tsx b/dashboard/src/components/EmptyState.tsx index 879a5e92..2a42c662 100644 --- a/dashboard/src/components/EmptyState.tsx +++ b/dashboard/src/components/EmptyState.tsx @@ -1,5 +1,72 @@ import type { ReactNode } from 'react'; +export interface EmptyStateAction { + label: string; + onClick: () => void; +} + +export interface EmptyStateProps { + /** Short heading. Omit for compact/inline placements that only need a message. */ + title?: string; + /** Helpful message guiding the user on what to do next. */ + message: string; + /** Custom icon/illustration. Falls back to a generic empty-tray icon. */ + icon?: ReactNode; + /** Optional call-to-action rendered below the message. */ + action?: EmptyStateAction; + /** + * Visual density: + * - "default": large dashed card for standalone page/section placeholders. + * - "compact": smaller dashed card for placeholders inside a page section. + * - "inline": no border/background — for placeholders already nested inside + * a bordered container (a panel, card, or table cell). + */ + size?: 'default' | 'compact' | 'inline'; + className?: string; +} + +function DefaultEmptyIcon() { + return ( + + ); +} + +/** + * Reusable placeholder for any screen or section with no data to show. + * Pairs an icon with a short title and a helpful message, and optionally + * a call-to-action, so empty screens always guide the user to a next step. + */ +export function EmptyState({ + title, + message, + icon, + action, + size = 'default', + className, +}: EmptyStateProps) { + const classes = ['empty-state', `empty-state--${size}`, className].filter(Boolean).join(' '); + + return ( +
+
{icon ?? }
+ {title &&

{title}

} +

{message}

+ {action && ( +
className="empty-state--compact" icon="📭" title="No history entries" @@ -206,6 +210,7 @@ export function NotificationTimelineView() { {/* Initial empty state — nothing searched yet */} {!loading && !timeline && !error && ( + ) : buckets.length === 0 ? ( +
+ +
) ) : pageItems.length === 0 ? ( + + 0 && ( + ) : ( + +
+

Start searching

+

Choose a type, delivery status, date range, or enter a query to find notifications.

+
icon="🔔" title="Search notifications" description="Choose a type, delivery status, date range, or enter a query to find notifications." @@ -430,6 +439,9 @@ export function NotificationSearchPage() { {!loading && !error && hasParams && response?.results.length === 0 && (