Skip to content
Open
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
119 changes: 119 additions & 0 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -5719,6 +5719,125 @@ code,
}
}

/* ─── BillingHistory ─────────────────────────────────────────────────────────
Hover-triggered / keyboard-accessible billing history rows.
GrantFox FWC26 campaign (Issue #692) — matches DashboardOverview pattern.
────────────────────────────────────────────────────────────────────────── */

.billing-history {
max-width: 960px;
margin: 0 auto;
}

.billing-history__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 24px;
flex-wrap: wrap;
}

.billing-history__title {
margin: 0;
font-size: clamp(1.25rem, 2.5vw, 1.5rem);
color: var(--text);
}

.billing-history__subtitle {
margin: 4px 0 0;
font-size: 0.85rem;
color: var(--muted);
}

.billing-history__list {
display: grid;
gap: 8px;
list-style: none;
padding: 0;
margin: 0;
}

.billing-history__item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 14px 18px;
border-radius: var(--radius-lg, 12px);
background: var(--surface-soft);
border: 1px solid var(--line);
transition: border-color 160ms ease, box-shadow 160ms ease;
}

.billing-history__item:hover {
border-color: var(--accent, #6366f1);
box-shadow: var(--focus-ring);
}

.billing-history__item-left {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}

.billing-history__item-title {
font-weight: 600;
font-size: 0.9rem;
color: var(--text);
line-height: 1.3;
}

.billing-history__item-meta {
font-size: 0.78rem;
color: var(--muted);
display: flex;
flex-wrap: wrap;
gap: 8px;
}

.billing-history__item-right {
display: flex;
align-items: center;
gap: 12px;
flex-shrink: 0;
}

.billing-history__item-amount {
font-weight: 700;
font-variant-numeric: tabular-nums;
font-size: 0.9rem;
color: var(--text);
}

.billing-history__item-amount--credit {
color: var(--success, #10b981);
}

.billing-history__item-amount--debit {
color: var(--text);
}

.billing-history__empty {
text-align: center;
padding: 48px 24px;
color: var(--muted);
font-size: 0.9rem;
}

@media (max-width: 640px) {
.billing-history__item {
flex-direction: column;
align-items: flex-start;
}

.billing-history__item-right {
width: 100%;
justify-content: space-between;
}
}

/* ─── ApiDetailStickyTOC ────────────────────────────────────────────────────
Sticky right-rail TOC inside the documentation tab. Tokens only — no
hardcoded hex values. Transition respects prefers-reduced-motion.
Expand Down
171 changes: 171 additions & 0 deletions src/pages/BillingHistory.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
// @vitest-environment jsdom

import { cleanup, fireEvent, render, screen } from '@testing-library/react';
import { afterEach, describe, expect, it } from 'vitest';
import BillingHistory, { type BillingRecord } from './BillingHistory';

const MOCK_RECORDS: BillingRecord[] = [
{
id: 'bill-1',
type: 'invoice',
title: 'Monthly API Usage',
description: 'Aggregated charges for all API calls in February 2026.',
amount: 4200.0,
date: '2026-02-28',
status: 'success',
invoiceNumber: 'INV-2026-0028',
category: 'API Usage',
method: 'Vault',
},
{
id: 'bill-2',
type: 'payment',
title: 'USDC Vault Deposit',
description: 'Deposit from connected Freighter wallet.',
amount: 500.0,
date: '2026-02-15',
status: 'operational',
method: 'Freighter Wallet',
},
{
id: 'bill-3',
type: 'charge',
title: 'WeatherSim API Calls',
description: 'Per-request charges for WeatherSim /v1/forecast endpoint.',
amount: 12.5,
date: '2026-02-27',
status: 'operational',
category: 'Weather & Climate',
},
];

describe('BillingHistory Page Component (#692)', () => {
afterEach(cleanup);

it('renders the section heading and subtitle', () => {
render(<BillingHistory records={MOCK_RECORDS} />);

expect(screen.getByText('Billing History')).toBeTruthy();
expect(
screen.getByText('Invoices, payments, and usage charges with preview details on hover or focus.'),
).toBeTruthy();
});

it('renders all billing records as list items', () => {
render(<BillingHistory records={MOCK_RECORDS} />);

expect(screen.getByText('Monthly API Usage')).toBeTruthy();
expect(screen.getByText('USDC Vault Deposit')).toBeTruthy();
expect(screen.getByText('WeatherSim API Calls')).toBeTruthy();
});

it('renders invoice numbers and dates for records that have them', () => {
render(<BillingHistory records={MOCK_RECORDS} />);

expect(screen.getByText('INV-2026-0028')).toBeTruthy();
expect(screen.getByText('2026-02-28')).toBeTruthy();
});

it('renders formatted amounts with correct sign (credit vs debit)', () => {
render(<BillingHistory records={MOCK_RECORDS} />);

// Invoice (debit): -USDC 4,200.00
expect(screen.getByText(/-USDC 4,200\.00/)).toBeTruthy();
// Payment (credit): +USDC 500.00
expect(screen.getByText(/\+USDC 500\.00/)).toBeTruthy();
});

it('shows empty state when records array is empty', () => {
render(<BillingHistory records={[]} />);

expect(screen.getByText('No billing records yet.')).toBeTruthy();
});

it('wraps each billing item in a PreviewCard trigger', () => {
render(<BillingHistory records={MOCK_RECORDS} />);

const trigger1 = screen.getByRole('button', {
name: /preview details for monthly api usage/i,
});
expect(trigger1).toBeTruthy();

const trigger2 = screen.getByRole('button', {
name: /preview details for usdc vault deposit/i,
});
expect(trigger2).toBeTruthy();
});

it('opens preview panel on focus and shows record details', () => {
render(<BillingHistory records={MOCK_RECORDS} />);

const trigger = screen.getByRole('button', {
name: /preview details for monthly api usage/i,
});

fireEvent.focus(trigger);

const tooltip = screen.getByRole('tooltip');
expect(tooltip).toBeTruthy();
expect(tooltip).toHaveTextContent('Monthly API Usage');
expect(tooltip).toHaveTextContent('Invoice');
expect(tooltip).toHaveTextContent('Aggregated charges for all API calls in February 2026.');
});

it('opens preview panel on hover and shows correct metrics', () => {
const { container } = render(<BillingHistory records={MOCK_RECORDS} />);

const wrapper = container.querySelector('.preview-card__wrapper') as HTMLElement;
expect(wrapper).toBeTruthy();

fireEvent.mouseEnter(wrapper);

const tooltip = screen.getByRole('tooltip');
expect(tooltip).toBeTruthy();
expect(tooltip).toHaveTextContent('Amount');
expect(tooltip).toHaveTextContent('Date');
});

it('closes preview panel on Escape key', () => {
render(<BillingHistory records={MOCK_RECORDS} />);

const trigger = screen.getByRole('button', {
name: /preview details for monthly api usage/i,
});

fireEvent.focus(trigger);
expect(screen.getByRole('tooltip')).toBeTruthy();

fireEvent.keyDown(trigger, { key: 'Escape' });
expect(screen.queryByRole('tooltip')).toBeNull();
});

it('links trigger to preview panel via aria-describedby while open', () => {
render(<BillingHistory records={MOCK_RECORDS} />);

const trigger = screen.getByRole('button', {
name: /preview details for monthly api usage/i,
});

fireEvent.focus(trigger);

const panel = screen.getByRole('tooltip');
expect(trigger.getAttribute('aria-describedby')).toBe(panel.id);
});

it('renders refund type as credit with positive sign', () => {
const refundRecord: BillingRecord = {
id: 'bill-refund',
type: 'refund',
title: 'API Overcharge Refund',
description: 'Refund issued for duplicate charge on WeatherSim API.',
amount: 25.0,
date: '2026-03-01',
status: 'success',
invoiceNumber: 'RFND-001',
};

render(<BillingHistory records={[refundRecord]} />);

expect(screen.getByText(/\+USDC 25\.00/)).toBeTruthy();
});
});
Loading
Loading