mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-08-28 16:31:13 +08:00
200 lines
6.5 KiB
TypeScript
200 lines
6.5 KiB
TypeScript
/**
|
|
* Provider bodies rendered end-to-end.
|
|
*
|
|
* Bodies receive their class map as a prop and import no stylesheet, so unlike
|
|
* QuotaCard they can be rendered directly here — which is the only place the
|
|
* "absolute plus countdown" pairing is checked as actual markup rather than as
|
|
* a formatter's return value.
|
|
*/
|
|
|
|
import { beforeAll, describe, expect, test } from 'bun:test';
|
|
import { createElement } from 'react';
|
|
import { renderToStaticMarkup } from 'react-dom/server';
|
|
import i18n from '@/i18n';
|
|
import { CodexQuotaBody } from '@/features/quota/providers/codex/CodexQuotaBody';
|
|
import { ClaudeQuotaBody } from '@/features/quota/providers/claude/ClaudeQuotaBody';
|
|
import { KimiQuotaBody } from '@/features/quota/providers/kimi/KimiQuotaBody';
|
|
import { QUOTA_CLASS_KEYS, bindQuotaClasses } from '@/features/quota/types';
|
|
import { formatInstantShort } from '@/utils/quota';
|
|
import { DAY_MS, HOUR_MS } from '@/utils/time/durations';
|
|
import type { ClaudeQuotaState, CodexQuotaState, KimiQuotaState } from '@/types';
|
|
|
|
const classes = bindQuotaClasses(
|
|
Object.fromEntries(QUOTA_CLASS_KEYS.map((key) => [key, key])),
|
|
'test-host'
|
|
);
|
|
|
|
/**
|
|
* useNow() freezes to module-load time under renderToStaticMarkup (it reads
|
|
* getServerSnapshot), so instants are placed relative to the real clock.
|
|
*/
|
|
const now = Date.now();
|
|
|
|
// The i18n fallback is zh-CN; pin English so the countdown assertions read.
|
|
beforeAll(async () => {
|
|
await i18n.changeLanguage('en');
|
|
});
|
|
|
|
describe('CodexQuotaBody', () => {
|
|
const quota: CodexQuotaState = {
|
|
status: 'success',
|
|
planType: 'pro',
|
|
windows: [
|
|
{
|
|
id: 'primary',
|
|
label: '5-hour limit',
|
|
usedPercent: 38,
|
|
resetLabel: '08-02 18:00',
|
|
resetAtMs: now + 3 * HOUR_MS,
|
|
periodHours: 5,
|
|
},
|
|
],
|
|
rateLimitResetCredits: [
|
|
{
|
|
id: 'credit-1',
|
|
status: 'available',
|
|
grantedAt: new Date(now - DAY_MS).toISOString(),
|
|
expiresAt: new Date(now + 11 * DAY_MS).toISOString(),
|
|
},
|
|
],
|
|
rateLimitResetCreditsAvailableCount: 1,
|
|
};
|
|
|
|
test('renders a window reset as absolute plus countdown', () => {
|
|
const markup = renderToStaticMarkup(createElement(CodexQuotaBody, { quota, classes }));
|
|
|
|
expect(markup).toContain('08-02 18:00');
|
|
expect(markup).toContain('quotaResetRelative');
|
|
expect(markup).toMatch(/3 hours/);
|
|
});
|
|
|
|
test('renders reset-credit expiry in local time with a countdown', () => {
|
|
const markup = renderToStaticMarkup(createElement(CodexQuotaBody, { quota, classes }));
|
|
|
|
expect(markup).toContain(formatInstantShort(now + 11 * DAY_MS));
|
|
expect(markup).toMatch(/11 days/);
|
|
});
|
|
|
|
test('highlights a credit expiring within the final hour', () => {
|
|
const creditFirst: CodexQuotaState = {
|
|
...quota,
|
|
windows: [{ ...quota.windows[0], resetAtMs: now + 5 * DAY_MS }],
|
|
rateLimitResetCredits: [
|
|
{
|
|
id: 'credit-1',
|
|
status: 'available',
|
|
grantedAt: new Date(now - DAY_MS).toISOString(),
|
|
expiresAt: new Date(now + 30 * 60_000).toISOString(),
|
|
},
|
|
],
|
|
};
|
|
const markup = renderToStaticMarkup(
|
|
createElement(CodexQuotaBody, { quota: creditFirst, classes })
|
|
);
|
|
|
|
expect(markup).toContain('codexResetCreditRowSoon');
|
|
expect(markup).not.toContain('quotaRowSoon');
|
|
});
|
|
|
|
test('does not emphasize a reset countdown more than one hour away', () => {
|
|
const markup = renderToStaticMarkup(createElement(CodexQuotaBody, { quota, classes }));
|
|
|
|
expect(markup).not.toContain('quotaRowSoon');
|
|
expect(markup).not.toContain('quotaResetRelativeSoon');
|
|
expect(markup).not.toContain('codexResetCreditRowSoon');
|
|
});
|
|
|
|
test('emphasizes a reset countdown within the final hour', () => {
|
|
const urgent: CodexQuotaState = {
|
|
...quota,
|
|
windows: [{ ...quota.windows[0], resetAtMs: now + 30 * 60_000 }],
|
|
};
|
|
const markup = renderToStaticMarkup(createElement(CodexQuotaBody, { quota: urgent, classes }));
|
|
|
|
expect(markup).toContain('quotaResetRelativeSoon');
|
|
});
|
|
|
|
test('highlights nothing once every instant is in the past', () => {
|
|
const stale: CodexQuotaState = {
|
|
...quota,
|
|
windows: [{ ...quota.windows[0], resetAtMs: now - HOUR_MS }],
|
|
rateLimitResetCredits: [],
|
|
rateLimitResetCreditsAvailableCount: null,
|
|
};
|
|
const markup = renderToStaticMarkup(createElement(CodexQuotaBody, { quota: stale, classes }));
|
|
|
|
expect(markup).not.toContain('Soon');
|
|
});
|
|
|
|
test('keeps the baked label alone when the store entry predates resetAtMs', () => {
|
|
const stale: CodexQuotaState = {
|
|
...quota,
|
|
windows: [{ ...quota.windows[0], resetAtMs: undefined, periodHours: undefined }],
|
|
rateLimitResetCredits: [],
|
|
rateLimitResetCreditsAvailableCount: null,
|
|
};
|
|
const markup = renderToStaticMarkup(createElement(CodexQuotaBody, { quota: stale, classes }));
|
|
|
|
expect(markup).toContain('08-02 18:00');
|
|
expect(markup).not.toContain('quotaResetRelative');
|
|
});
|
|
});
|
|
|
|
describe('KimiQuotaBody', () => {
|
|
test('renders the concrete reset time alongside its countdown', () => {
|
|
const resetAtMs = now + 3 * HOUR_MS;
|
|
const quota: KimiQuotaState = {
|
|
status: 'success',
|
|
rows: [
|
|
{
|
|
id: 'summary',
|
|
label: 'Weekly limit',
|
|
used: 34,
|
|
limit: 100,
|
|
resetHint: '3h',
|
|
resetAtMs,
|
|
periodHours: 168,
|
|
},
|
|
],
|
|
};
|
|
const markup = renderToStaticMarkup(createElement(KimiQuotaBody, { quota, classes }));
|
|
|
|
expect(markup).toContain(formatInstantShort(resetAtMs));
|
|
expect(markup).toContain('quotaResetRelative');
|
|
expect(markup).toMatch(/3 hours/);
|
|
expect(markup).not.toContain('resets in 3h');
|
|
});
|
|
});
|
|
|
|
describe('ClaudeQuotaBody', () => {
|
|
test('pairs each window reset with a countdown', () => {
|
|
const quota: ClaudeQuotaState = {
|
|
status: 'success',
|
|
windows: [
|
|
{
|
|
id: 'five_hour',
|
|
label: '5-hour',
|
|
usedPercent: 12,
|
|
resetLabel: '08-02 17:00',
|
|
resetAtMs: now + 2 * HOUR_MS,
|
|
periodHours: 5,
|
|
},
|
|
{
|
|
id: 'seven_day',
|
|
label: '7-day',
|
|
usedPercent: 60,
|
|
resetLabel: '08-06 04:00',
|
|
resetAtMs: now + 4 * DAY_MS,
|
|
periodHours: 168,
|
|
},
|
|
],
|
|
};
|
|
const markup = renderToStaticMarkup(createElement(ClaudeQuotaBody, { quota, classes }));
|
|
|
|
expect(markup).toContain('08-02 17:00');
|
|
expect(markup).toContain('08-06 04:00');
|
|
expect(markup).toMatch(/2 hours/);
|
|
expect(markup).toMatch(/4 days/);
|
|
});
|
|
});
|