Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
- Billing (13 tests): plan cards, prices, limits, usage bars, payment flow - ScheduledTasks (16 tests): CRUD table, schedule/target types, color tags - Knowledge (12 tests): 4 tabs, items/categories/search/analytics panels - Roles (12 tests): roles + permission templates tabs - ConfigSync (8 tests): sync log viewer with action labels Fix: Knowledge.tsx missing </Select> and </Modal> closing tags (JSX parse error) Fix: tests/setup.ts added ResizeObserver mock for ProTable compatibility
245 lines
8.0 KiB
TypeScript
245 lines
8.0 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
import { render, screen, waitFor } from '@testing-library/react'
|
|
import { http, HttpResponse } from 'msw'
|
|
import { setupServer } from 'msw/node'
|
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
|
|
import Billing from '@/pages/Billing'
|
|
|
|
// ── Mock data ──────────────────────────────────────────────────
|
|
|
|
const mockPlans = [
|
|
{
|
|
id: 'plan-free', name: 'free', display_name: '免费版',
|
|
description: '基础功能', price_cents: 0, currency: 'CNY',
|
|
interval: 'month',
|
|
features: {}, limits: { max_relay_requests_monthly: 100, max_hand_executions_monthly: 10, max_pipeline_runs_monthly: 5 },
|
|
is_default: true, sort_order: 0, status: 'active',
|
|
created_at: '2026-01-01T00:00:00Z', updated_at: '2026-01-01T00:00:00Z',
|
|
},
|
|
{
|
|
id: 'plan-pro', name: 'pro', display_name: '专业版',
|
|
description: '高级功能', price_cents: 9900, currency: 'CNY',
|
|
interval: 'month',
|
|
features: {}, limits: { max_relay_requests_monthly: 1000, max_hand_executions_monthly: 100, max_pipeline_runs_monthly: 50 },
|
|
is_default: false, sort_order: 1, status: 'active',
|
|
created_at: '2026-01-01T00:00:00Z', updated_at: '2026-01-01T00:00:00Z',
|
|
},
|
|
{
|
|
id: 'plan-team', name: 'team', display_name: '团队版',
|
|
description: '团队协作', price_cents: 29900, currency: 'CNY',
|
|
interval: 'month',
|
|
features: {}, limits: { max_relay_requests_monthly: 10000, max_hand_executions_monthly: 500, max_pipeline_runs_monthly: 200 },
|
|
is_default: false, sort_order: 2, status: 'active',
|
|
created_at: '2026-01-01T00:00:00Z', updated_at: '2026-01-01T00:00:00Z',
|
|
},
|
|
]
|
|
|
|
const mockSubscription = {
|
|
plan: mockPlans[0],
|
|
subscription: null,
|
|
usage: {
|
|
id: 'usage-001', account_id: 'acc-001',
|
|
period_start: '2026-04-01T00:00:00Z', period_end: '2026-04-30T23:59:59Z',
|
|
input_tokens: 5000, output_tokens: 12000,
|
|
relay_requests: 42, hand_executions: 3, pipeline_runs: 1,
|
|
max_input_tokens: null, max_output_tokens: null,
|
|
max_relay_requests: 100, max_hand_executions: 10, max_pipeline_runs: 5,
|
|
created_at: '2026-04-01T00:00:00Z', updated_at: '2026-04-07T12:00:00Z',
|
|
},
|
|
}
|
|
|
|
const server = setupServer()
|
|
|
|
beforeEach(() => {
|
|
server.listen({ onUnhandledRequest: 'bypass' })
|
|
})
|
|
|
|
afterEach(() => {
|
|
server.close()
|
|
})
|
|
|
|
function renderWithProviders(ui: React.ReactElement) {
|
|
const queryClient = new QueryClient({
|
|
defaultOptions: { queries: { retry: false } },
|
|
})
|
|
return render(
|
|
<QueryClientProvider client={queryClient}>
|
|
{ui}
|
|
</QueryClientProvider>,
|
|
)
|
|
}
|
|
|
|
function setupBillingHandlers(overrides: Record<string, unknown> = {}) {
|
|
server.use(
|
|
http.get('*/api/v1/billing/plans', () => {
|
|
return HttpResponse.json(overrides.plans ?? mockPlans)
|
|
}),
|
|
http.get('*/api/v1/billing/subscription', () => {
|
|
return HttpResponse.json(overrides.subscription ?? mockSubscription)
|
|
}),
|
|
)
|
|
}
|
|
|
|
describe('Billing', () => {
|
|
it('renders page title', () => {
|
|
setupBillingHandlers()
|
|
renderWithProviders(<Billing />)
|
|
expect(screen.getByText('计费管理')).toBeInTheDocument()
|
|
})
|
|
|
|
it('shows loading state', async () => {
|
|
server.use(
|
|
http.get('*/api/v1/billing/plans', async () => {
|
|
await new Promise(resolve => setTimeout(resolve, 500))
|
|
return HttpResponse.json(mockPlans)
|
|
}),
|
|
http.get('*/api/v1/billing/subscription', async () => {
|
|
await new Promise(resolve => setTimeout(resolve, 500))
|
|
return HttpResponse.json(mockSubscription)
|
|
}),
|
|
)
|
|
renderWithProviders(<Billing />)
|
|
expect(document.querySelector('.ant-spin')).toBeTruthy()
|
|
})
|
|
|
|
it('displays all three plan cards', async () => {
|
|
setupBillingHandlers()
|
|
renderWithProviders(<Billing />)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('免费版')).toBeInTheDocument()
|
|
})
|
|
expect(screen.getByText('专业版')).toBeInTheDocument()
|
|
expect(screen.getByText('团队版')).toBeInTheDocument()
|
|
})
|
|
|
|
it('displays plan prices', async () => {
|
|
setupBillingHandlers()
|
|
renderWithProviders(<Billing />)
|
|
|
|
await waitFor(() => {
|
|
// Free plan: ¥0
|
|
expect(screen.getByText('¥0')).toBeInTheDocument()
|
|
})
|
|
// Pro plan: ¥99, Team plan: ¥299
|
|
expect(screen.getByText('¥99')).toBeInTheDocument()
|
|
expect(screen.getByText('¥299')).toBeInTheDocument()
|
|
})
|
|
|
|
it('displays per-month interval', async () => {
|
|
setupBillingHandlers()
|
|
renderWithProviders(<Billing />)
|
|
|
|
await waitFor(() => {
|
|
// All plans are monthly, so "/月" should appear multiple times
|
|
const monthLabels = screen.getAllByText('/月')
|
|
expect(monthLabels.length).toBeGreaterThanOrEqual(3)
|
|
})
|
|
})
|
|
|
|
it('displays plan limits', async () => {
|
|
setupBillingHandlers()
|
|
renderWithProviders(<Billing />)
|
|
|
|
await waitFor(() => {
|
|
// Free plan limits
|
|
expect(screen.getByText('中转请求: 100 次/月')).toBeInTheDocument()
|
|
})
|
|
expect(screen.getByText('Hand 执行: 10 次/月')).toBeInTheDocument()
|
|
expect(screen.getByText('Pipeline 运行: 5 次/月')).toBeInTheDocument()
|
|
})
|
|
|
|
it('shows 当前计划 badge on free plan', async () => {
|
|
setupBillingHandlers()
|
|
renderWithProviders(<Billing />)
|
|
|
|
await waitFor(() => {
|
|
// "当前计划" appears on the badge AND the disabled button for free plan
|
|
const allCurrentPlan = screen.getAllByText('当前计划')
|
|
expect(allCurrentPlan.length).toBeGreaterThanOrEqual(1)
|
|
})
|
|
})
|
|
|
|
it('renders pro and team plan cards with buttons', async () => {
|
|
setupBillingHandlers()
|
|
renderWithProviders(<Billing />)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('专业版')).toBeInTheDocument()
|
|
})
|
|
// Non-current plans should have clickable buttons (not disabled "当前计划")
|
|
expect(screen.getByText('团队版')).toBeInTheDocument()
|
|
// Free plan is current → its button shows "当前计划" and is disabled
|
|
const allButtons = screen.getAllByRole('button')
|
|
const disabledButtons = allButtons.filter(btn => btn.hasAttribute('disabled'))
|
|
expect(disabledButtons.length).toBeGreaterThanOrEqual(1) // at least free plan button
|
|
})
|
|
|
|
it('shows 当前用量 section', async () => {
|
|
setupBillingHandlers()
|
|
renderWithProviders(<Billing />)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('当前用量')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('displays usage bars with correct values', async () => {
|
|
setupBillingHandlers()
|
|
renderWithProviders(<Billing />)
|
|
|
|
await waitFor(() => {
|
|
// relay_requests: 42 / 100
|
|
expect(screen.getByText('中转请求')).toBeInTheDocument()
|
|
})
|
|
expect(screen.getByText('Hand 执行')).toBeInTheDocument()
|
|
expect(screen.getByText('Pipeline 运行')).toBeInTheDocument()
|
|
})
|
|
|
|
it('shows error state on plans API failure', async () => {
|
|
server.use(
|
|
http.get('*/api/v1/billing/plans', () => {
|
|
return HttpResponse.json(
|
|
{ error: 'internal_error', message: '数据库错误' },
|
|
{ status: 500 },
|
|
)
|
|
}),
|
|
http.get('*/api/v1/billing/subscription', () => {
|
|
return HttpResponse.json(mockSubscription)
|
|
}),
|
|
)
|
|
renderWithProviders(<Billing />)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('加载失败')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('renders without subscription data', async () => {
|
|
setupBillingHandlers({
|
|
subscription: {
|
|
plan: null,
|
|
subscription: null,
|
|
usage: null,
|
|
},
|
|
})
|
|
renderWithProviders(<Billing />)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('免费版')).toBeInTheDocument()
|
|
})
|
|
// No usage section when usage is null
|
|
expect(screen.queryByText('当前用量')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('shows 选择计划 heading', async () => {
|
|
setupBillingHandlers()
|
|
renderWithProviders(<Billing />)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('选择计划')).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|