Files
resolutionflow/frontend/src/components/layout/__tests__/TrialPill.test.tsx
Michael Chihlas 99343ab7a9 feat(dashboard): add TrialPill in AppLayout topbar
Mounts a billing-state pill in the topbar that reads useTrialBanner() and
renders the appropriate label / tone / CTA per spec:

- pristine / warning  → "Pro trial · Nd"   (info → warning amber as days drop)
- urgent              → "Pro trial · today" (warning amber, semibold)
- expired             → "Trial expired — pick a plan" → /account/billing/select-plan
- paid                → planBilling.display_name (quiet)
- complimentary       → "Complimentary Pro" (accent, no CTA)
- past_due            → "Payment failed — update card" → /account/billing
- canceled            → "Reactivate" → /account/billing/select-plan
- null                → hidden

Uses existing design-system tokens only (text-info/bg-info-dim,
text-warning/bg-warning-dim, text-danger/bg-danger-dim, text-accent/
bg-accent-dim, text-muted-foreground/bg-elevated). Clickable variants
render as react-router-dom <Link>s and are keyboard-focusable with an
accent focus-visible ring. Mobile collapses the label to a clock icon
with a title attribute carrying the full text.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 23:06:09 -04:00

156 lines
4.1 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { render, screen } from '@testing-library/react'
import { MemoryRouter } from 'react-router-dom'
import { TrialPill } from '../TrialPill'
import { useBillingStore } from '@/store/billingStore'
import type { SubscriptionState, PlanBillingState } from '@/types/billing'
const FROZEN_NOW = new Date('2026-05-06T12:00:00Z')
function renderPill() {
return render(
<MemoryRouter>
<TrialPill />
</MemoryRouter>,
)
}
function setBilling(opts: {
subscription: SubscriptionState | null
planBilling?: PlanBillingState | null
}) {
useBillingStore.setState({
subscription: opts.subscription,
planBilling: opts.planBilling ?? null,
planLimits: {},
enabledFeatures: {},
isLoading: false,
error: null,
})
}
function isoDaysFromNow(days: number): string {
const d = new Date(FROZEN_NOW.getTime() + days * 24 * 60 * 60 * 1000)
return d.toISOString()
}
describe('TrialPill', () => {
beforeEach(() => {
vi.useFakeTimers()
vi.setSystemTime(FROZEN_NOW)
useBillingStore.setState({
subscription: null,
planBilling: null,
planLimits: {},
enabledFeatures: {},
isLoading: false,
error: null,
})
})
afterEach(() => {
vi.useRealTimers()
})
it('renders Pro trial · Nd for pristine stage', () => {
setBilling({
subscription: {
status: 'trialing',
plan: 'pro',
current_period_start: FROZEN_NOW.toISOString(),
current_period_end: isoDaysFromNow(12),
cancel_at_period_end: false,
seat_limit: 5,
has_pro_entitlement: true,
is_paid: false,
},
})
renderPill()
const pill = screen.getByTestId('trial-pill')
expect(pill).toHaveTextContent(/Pro trial · 12d/)
// Pristine uses info tone tokens.
expect(pill.className).toContain('text-info')
expect(pill.className).toContain('bg-info-dim')
})
it('renders Trial expired CTA for expired stage', () => {
setBilling({
subscription: {
status: 'trialing',
plan: 'pro',
current_period_start: isoDaysFromNow(-14),
current_period_end: isoDaysFromNow(-1), // already past
cancel_at_period_end: false,
seat_limit: 5,
has_pro_entitlement: false,
is_paid: false,
},
})
renderPill()
const pill = screen.getByTestId('trial-pill')
expect(pill).toHaveTextContent(/Trial expired — pick a plan/)
// Clickable: rendered as anchor/link.
expect(pill.tagName).toBe('A')
expect(pill.getAttribute('href')).toBe('/account/billing/select-plan')
})
it('renders Complimentary Pro tag for complimentary subscription', () => {
setBilling({
subscription: {
status: 'complimentary',
plan: 'pro',
current_period_start: null,
current_period_end: null,
cancel_at_period_end: false,
seat_limit: null,
has_pro_entitlement: true,
is_paid: true,
},
})
renderPill()
const pill = screen.getByTestId('trial-pill')
expect(pill).toHaveTextContent(/Complimentary Pro/)
// Friendly tag, not clickable.
expect(pill.tagName).toBe('SPAN')
expect(pill.className).toContain('text-accent')
})
it('is hidden when subscription is null', () => {
setBilling({ subscription: null })
const { container } = renderPill()
expect(screen.queryByTestId('trial-pill')).not.toBeInTheDocument()
expect(container.firstChild).toBeNull()
})
it('past_due variant is clickable and links to /account/billing', () => {
setBilling({
subscription: {
status: 'past_due',
plan: 'pro',
current_period_start: isoDaysFromNow(-30),
current_period_end: isoDaysFromNow(-2),
cancel_at_period_end: false,
seat_limit: 5,
has_pro_entitlement: false,
is_paid: true,
},
})
renderPill()
const pill = screen.getByTestId('trial-pill')
expect(pill).toHaveTextContent(/Payment failed — update card/)
expect(pill.tagName).toBe('A')
expect(pill.getAttribute('href')).toBe('/account/billing')
})
})