// @vitest-environment jsdom // BalanceBadge presentation: polls an injected snapshot callback and renders // 余额 in both widths, hiding unknown figures behind the empty placeholder, // and re-reads through the injected refresh callback when the user clicks it. import { act, cleanup, fireEvent, render, screen } from '@testing-library/react' import { afterEach, describe, expect, it, vi } from 'vitest' import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime' import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts' import type { OpenRouterBalance } from '@deepseek-ai/dsh-openrouter-usage/client' import { BALANCE_POLL_MS, BalanceBadge, type BalanceBadgeProps } from '../src/client/BalanceBadge.tsx' import { zh } from '../src/client/locales.ts' afterEach(() => { vi.useRealTimers() cleanup() }) const t = makeTranslate(zh, commonZh) function makeBalance(over: Partial = {}): OpenRouterBalance { return { balanceUsd: 10.5, label: 'prod-key', usageTokens: 1000, limitTokens: 100000, isFreeTier: false, updatedAt: Date.now(), currency: 'USD', ...over, } } function makeProps(over: Partial = {}) { return { wide: true, snapshot: vi.fn(async () => makeBalance()), refresh: vi.fn(async () => makeBalance()), t, ...over, } as unknown as Parameters[0] } describe('BalanceBadge', () => { it('renders the balance label with the USD figure in wide mode', async () => { render() expect(await screen.findByLabelText('余额 $10.50')).toBeTruthy() expect(screen.getByText('余额 $10.50')).toBeTruthy() }) it('polls the snapshot callback on the balance interval', async () => { vi.useFakeTimers() const snapshot = vi.fn(async () => makeBalance()) render() await act(async () => { await Promise.resolve() }) expect(snapshot).toHaveBeenCalledTimes(1) await act(async () => { vi.advanceTimersByTime(BALANCE_POLL_MS) }) await act(async () => { await Promise.resolve() }) expect(snapshot).toHaveBeenCalledTimes(2) }) it('renders only the rail marker in the collapsed sidebar', async () => { render() expect(await screen.findByLabelText('余额 $10.50')).toBeTruthy() expect(screen.queryByText('余额 $10.50')).toBeNull() }) it('shows the empty placeholder when no balance is known yet', async () => { render( makeBalance({ balanceUsd: null })) })} />) expect(await screen.findByLabelText('余额 ——')).toBeTruthy() }) it('re-reads through refresh on click and renders the fetched figure', async () => { const refresh = vi.fn(async () => makeBalance({ balanceUsd: 42.25 })) render() const badge = await screen.findByLabelText('余额 $10.50') await act(async () => { fireEvent.click(badge) }) expect(refresh).toHaveBeenCalledOnce() expect(await screen.findByLabelText('余额 $42.25')).toBeTruthy() }) it('ignores a click while a refresh is still outstanding', async () => { let release: (value: OpenRouterBalance) => void = () => {} const refresh = vi.fn(() => new Promise((resolve) => { release = resolve })) render() const badge = await screen.findByLabelText('余额 $10.50') await act(async () => { fireEvent.click(badge) }) expect(badge.getAttribute('aria-busy')).toBe('true') await act(async () => { fireEvent.click(badge) }) expect(refresh).toHaveBeenCalledOnce() await act(async () => { release(makeBalance({ balanceUsd: 7 })) }) expect(badge.getAttribute('aria-busy')).toBe('false') await act(async () => { fireEvent.click(badge) }) expect(refresh).toHaveBeenCalledTimes(2) }) it('keeps the last-known figure when a refresh fails', async () => { const refresh = vi.fn(async () => { throw new Error('boom') }) render() const badge = await screen.findByLabelText('余额 $10.50') await act(async () => { fireEvent.click(badge) }) expect(await screen.findByLabelText('余额 $10.50')).toBeTruthy() expect(badge.getAttribute('aria-busy')).toBe('false') }) })