From 709976ec92c2f22dc0161f4e6c2b569efd02ad33 Mon Sep 17 00:00:00 2001 From: LockedTerminal Date: Sat, 29 Aug 2026 15:14:51 +0000 Subject: [PATCH] fix: add distinct OnHold visual state to StatusBadge Closes #258 - Add OnHold to InvoiceStatus enum in comebackhere-frontend types - Add badge--on-hold CSS class to App.css (visually distinct from badge--pending via a solid amber border, same warm background tone) - Add statusLabels map so OnHold renders as 'On Hold' (human-readable) - Update aria-label to use the human-readable label - Add StatusBadge.test.tsx covering all 7 status variants with explicit assertions for the new OnHold class, label text, aria-label, and that OnHold className differs from Pending --- comebackhere-frontend/src/App.css | 8 ++ .../src/components/StatusBadge.tsx | 15 +++- .../src/tests/StatusBadge.test.tsx | 77 +++++++++++++++++++ comebackhere-frontend/src/types/index.ts | 2 + 4 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 comebackhere-frontend/src/tests/StatusBadge.test.tsx diff --git a/comebackhere-frontend/src/App.css b/comebackhere-frontend/src/App.css index ba7c6d7..9f667dc 100644 --- a/comebackhere-frontend/src/App.css +++ b/comebackhere-frontend/src/App.css @@ -328,6 +328,14 @@ h3 { color: var(--color-success); } +/* OnHold — visually distinct from Pending so payers understand the invoice + is blocked by an external hold rather than simply awaiting payment. */ +.badge--on-hold { + background: var(--color-warning-bg); + color: var(--color-warning-text); + border: 1px solid #fcd34d; +} + .message { padding: 12px 16px; border-radius: var(--radius); diff --git a/comebackhere-frontend/src/components/StatusBadge.tsx b/comebackhere-frontend/src/components/StatusBadge.tsx index 3ad8e9d..5c40e8d 100644 --- a/comebackhere-frontend/src/components/StatusBadge.tsx +++ b/comebackhere-frontend/src/components/StatusBadge.tsx @@ -11,10 +11,23 @@ const statusColors: Record = { Cancelled: "badge badge--cancelled", RefundRequested: "badge badge--refund-requested", Released: "badge badge--released", + OnHold: "badge badge--on-hold", +} + +/** Human-readable label for each status, so "OnHold" shows as "On Hold". */ +const statusLabels: Record = { + Pending: "Pending", + Paid: "Paid", + Expired: "Expired", + Cancelled: "Cancelled", + RefundRequested: "Refund Requested", + Released: "Released", + OnHold: "On Hold", } export function StatusBadge({ status }: StatusBadgeProps) { + const label = statusLabels[status] ?? status return ( - {status} + {label} ) } diff --git a/comebackhere-frontend/src/tests/StatusBadge.test.tsx b/comebackhere-frontend/src/tests/StatusBadge.test.tsx new file mode 100644 index 0000000..a8bee69 --- /dev/null +++ b/comebackhere-frontend/src/tests/StatusBadge.test.tsx @@ -0,0 +1,77 @@ +import { describe, it, expect } from 'vitest' +import '@testing-library/jest-dom' +import { render, screen } from '@testing-library/react' +import { StatusBadge } from '../components/StatusBadge' +import { InvoiceStatus } from '../types' + +describe('StatusBadge', () => { + it('renders Pending badge', () => { + render() + const badge = screen.getByRole('status') + expect(badge).toHaveTextContent('Pending') + expect(badge).toHaveClass('badge--pending') + }) + + it('renders Paid badge', () => { + render() + const badge = screen.getByRole('status') + expect(badge).toHaveTextContent('Paid') + expect(badge).toHaveClass('badge--paid') + }) + + it('renders Expired badge', () => { + render() + const badge = screen.getByRole('status') + expect(badge).toHaveTextContent('Expired') + expect(badge).toHaveClass('badge--expired') + }) + + it('renders Cancelled badge', () => { + render() + const badge = screen.getByRole('status') + expect(badge).toHaveTextContent('Cancelled') + expect(badge).toHaveClass('badge--cancelled') + }) + + it('renders RefundRequested badge', () => { + render() + const badge = screen.getByRole('status') + expect(badge).toHaveTextContent('Refund Requested') + expect(badge).toHaveClass('badge--refund-requested') + }) + + it('renders Released badge', () => { + render() + const badge = screen.getByRole('status') + expect(badge).toHaveTextContent('Released') + expect(badge).toHaveClass('badge--released') + }) + + describe('OnHold variant (issue #258)', () => { + it('renders OnHold badge with distinct css class', () => { + render() + const badge = screen.getByRole('status') + expect(badge).toHaveClass('badge--on-hold') + }) + + it('renders "On Hold" human-readable label, not "OnHold"', () => { + render() + expect(screen.getByRole('status')).toHaveTextContent('On Hold') + }) + + it('OnHold and Pending use different CSS classes', () => { + const { rerender } = render() + const pendingClass = screen.getByRole('status').className + + rerender() + const onHoldClass = screen.getByRole('status').className + + expect(onHoldClass).not.toBe(pendingClass) + }) + + it('has an accessible aria-label for screen readers', () => { + render() + expect(screen.getByRole('status')).toHaveAttribute('aria-label', 'Invoice status: On Hold') + }) + }) +}) diff --git a/comebackhere-frontend/src/types/index.ts b/comebackhere-frontend/src/types/index.ts index 37b7da2..fdb788b 100644 --- a/comebackhere-frontend/src/types/index.ts +++ b/comebackhere-frontend/src/types/index.ts @@ -5,6 +5,8 @@ export enum InvoiceStatus { Cancelled = "Cancelled", RefundRequested = "RefundRequested", Released = "Released", + /** Invoice is blocked because its underlying settlement is on hold. */ + OnHold = "OnHold", } export type TransactionEventType =