test(admin-v2): Phase 2 frontend tests — 61 tests for 5 pages
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
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
This commit is contained in:
244
admin-v2/tests/pages/Billing.test.tsx
Normal file
244
admin-v2/tests/pages/Billing.test.tsx
Normal file
@@ -0,0 +1,244 @@
|
||||
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()
|
||||
})
|
||||
})
|
||||
})
|
||||
153
admin-v2/tests/pages/ConfigSync.test.tsx
Normal file
153
admin-v2/tests/pages/ConfigSync.test.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
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 ConfigSync from '@/pages/ConfigSync'
|
||||
|
||||
const mockSyncLogs = {
|
||||
items: [
|
||||
{
|
||||
id: 1,
|
||||
account_id: 'acc-001',
|
||||
client_fingerprint: 'fp-abc123def456',
|
||||
action: 'push',
|
||||
config_keys: 'model_config,prompt_config',
|
||||
client_values: '{"model":"gpt-4"}',
|
||||
saas_values: '{"model":"gpt-3.5"}',
|
||||
resolution: 'client_wins',
|
||||
created_at: '2026-04-07T10:30:00Z',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
account_id: 'acc-002',
|
||||
client_fingerprint: 'fp-xyz789',
|
||||
action: 'pull',
|
||||
config_keys: 'privacy_settings',
|
||||
client_values: null,
|
||||
saas_values: '{"analytics":true}',
|
||||
resolution: null,
|
||||
created_at: '2026-04-06T08:00:00Z',
|
||||
},
|
||||
],
|
||||
total: 2,
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
}
|
||||
|
||||
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>,
|
||||
)
|
||||
}
|
||||
|
||||
describe('ConfigSync', () => {
|
||||
it('renders page title', () => {
|
||||
server.use(
|
||||
http.get('*/api/v1/config/sync-logs', () => {
|
||||
return HttpResponse.json({ items: [], total: 0, page: 1, page_size: 20 })
|
||||
}),
|
||||
)
|
||||
renderWithProviders(<ConfigSync />)
|
||||
expect(screen.getByText('配置同步日志')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows loading state', async () => {
|
||||
server.use(
|
||||
http.get('*/api/v1/config/sync-logs', async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
return HttpResponse.json(mockSyncLogs)
|
||||
}),
|
||||
)
|
||||
renderWithProviders(<ConfigSync />)
|
||||
expect(document.querySelector('.ant-spin')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('displays sync logs with Chinese action labels', async () => {
|
||||
server.use(
|
||||
http.get('*/api/v1/config/sync-logs', () => {
|
||||
return HttpResponse.json(mockSyncLogs)
|
||||
}),
|
||||
)
|
||||
renderWithProviders(<ConfigSync />)
|
||||
|
||||
// Action labels are mapped to Chinese: push → 推送, pull → 拉取
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('推送')).toBeInTheDocument()
|
||||
})
|
||||
expect(screen.getByText('拉取')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('displays config keys for each log', async () => {
|
||||
server.use(
|
||||
http.get('*/api/v1/config/sync-logs', () => {
|
||||
return HttpResponse.json(mockSyncLogs)
|
||||
}),
|
||||
)
|
||||
renderWithProviders(<ConfigSync />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('model_config,prompt_config')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('displays resolution column', async () => {
|
||||
server.use(
|
||||
http.get('*/api/v1/config/sync-logs', () => {
|
||||
return HttpResponse.json(mockSyncLogs)
|
||||
}),
|
||||
)
|
||||
renderWithProviders(<ConfigSync />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('client_wins')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('color-codes action tags', async () => {
|
||||
server.use(
|
||||
http.get('*/api/v1/config/sync-logs', () => {
|
||||
return HttpResponse.json(mockSyncLogs)
|
||||
}),
|
||||
)
|
||||
renderWithProviders(<ConfigSync />)
|
||||
|
||||
await waitFor(() => {
|
||||
const pushTag = screen.getByText('推送').closest('.ant-tag')
|
||||
expect(pushTag?.className).toMatch(/blue/)
|
||||
})
|
||||
const pullTag = screen.getByText('拉取').closest('.ant-tag')
|
||||
expect(pullTag?.className).toMatch(/cyan/)
|
||||
})
|
||||
|
||||
it('renders table column headers', async () => {
|
||||
server.use(
|
||||
http.get('*/api/v1/config/sync-logs', () => {
|
||||
return HttpResponse.json(mockSyncLogs)
|
||||
}),
|
||||
)
|
||||
renderWithProviders(<ConfigSync />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('操作')).toBeInTheDocument()
|
||||
})
|
||||
expect(screen.getByText('客户端指纹')).toBeInTheDocument()
|
||||
expect(screen.getByText('配置键')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
299
admin-v2/tests/pages/Knowledge.test.tsx
Normal file
299
admin-v2/tests/pages/Knowledge.test.tsx
Normal file
@@ -0,0 +1,299 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||||
import { render, screen, waitFor, fireEvent, act } from '@testing-library/react'
|
||||
import { http, HttpResponse } from 'msw'
|
||||
import { setupServer } from 'msw/node'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
|
||||
import Knowledge from '@/pages/Knowledge'
|
||||
|
||||
// ── Mock data ──────────────────────────────────────────────────
|
||||
|
||||
const mockCategories = [
|
||||
{
|
||||
id: 'cat-001', name: '技术文档', description: '技术相关文档',
|
||||
parent_id: null, icon: '📚', sort_order: 0, item_count: 5,
|
||||
children: [], created_at: '2026-01-01T00:00:00Z', updated_at: '2026-01-01T00:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 'cat-002', name: '行业知识', description: '行业相关知识',
|
||||
parent_id: null, icon: '🏭', sort_order: 1, item_count: 3,
|
||||
children: [], created_at: '2026-01-15T00:00:00Z', updated_at: '2026-01-15T00:00:00Z',
|
||||
},
|
||||
]
|
||||
|
||||
const mockItems = {
|
||||
items: [
|
||||
{
|
||||
id: 'item-001', category_id: 'cat-001', title: 'API 认证指南',
|
||||
content: 'JWT 认证流程说明...', keywords: ['认证', 'JWT'],
|
||||
related_questions: [], priority: 5, status: 'active',
|
||||
version: 2, source: 'manual', tags: ['api', 'auth'],
|
||||
created_by: 'admin', created_at: '2026-02-01T00:00:00Z', updated_at: '2026-03-01T00:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 'item-002', category_id: 'cat-002', title: '玩具市场趋势 2026',
|
||||
content: '2026 年玩具行业趋势分析...', keywords: ['市场', '趋势'],
|
||||
related_questions: [], priority: 3, status: 'active',
|
||||
version: 1, source: 'import', tags: ['market'],
|
||||
created_by: 'admin', created_at: '2026-03-01T00:00:00Z', updated_at: '2026-03-01T00:00:00Z',
|
||||
},
|
||||
],
|
||||
total: 2, page: 1, page_size: 20,
|
||||
}
|
||||
|
||||
const mockOverview = {
|
||||
total_items: 8, active_items: 6, total_categories: 2,
|
||||
weekly_new_items: 1, total_references: 45, avg_reference_per_item: 5.6,
|
||||
hit_rate: 0.78, injection_rate: 0.65, positive_feedback_rate: 0.92,
|
||||
stale_items_count: 1,
|
||||
}
|
||||
|
||||
const mockTrends = { trends: [{ date: '2026-04-01', new_items: 2, references: 10, queries: 25 }] }
|
||||
const mockTopItems = { items: [] }
|
||||
const mockQuality = { metrics: [] }
|
||||
const mockGaps = { gaps: [] }
|
||||
|
||||
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 setupKnowledgeHandlers(overrides: Record<string, unknown> = {}) {
|
||||
server.use(
|
||||
http.get('*/api/v1/knowledge/categories', () => {
|
||||
return HttpResponse.json(overrides.categories ?? mockCategories)
|
||||
}),
|
||||
http.get('*/api/v1/knowledge/items', () => {
|
||||
return HttpResponse.json(overrides.items ?? mockItems)
|
||||
}),
|
||||
http.get('*/api/v1/knowledge/analytics/overview', () => {
|
||||
return HttpResponse.json(overrides.overview ?? mockOverview)
|
||||
}),
|
||||
http.get('*/api/v1/knowledge/analytics/trends', () => {
|
||||
return HttpResponse.json(overrides.trends ?? mockTrends)
|
||||
}),
|
||||
http.get('*/api/v1/knowledge/analytics/top-items', () => {
|
||||
return HttpResponse.json(overrides.topItems ?? mockTopItems)
|
||||
}),
|
||||
http.get('*/api/v1/knowledge/analytics/quality', () => {
|
||||
return HttpResponse.json(overrides.quality ?? mockQuality)
|
||||
}),
|
||||
http.get('*/api/v1/knowledge/analytics/gaps', () => {
|
||||
return HttpResponse.json(overrides.gaps ?? mockGaps)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
describe('Knowledge', () => {
|
||||
// ── Tab structure ─────────────────────────────────────────────
|
||||
|
||||
it('renders all tab labels', () => {
|
||||
setupKnowledgeHandlers()
|
||||
renderWithProviders(<Knowledge />)
|
||||
|
||||
expect(screen.getByText('知识条目')).toBeInTheDocument()
|
||||
expect(screen.getByText('分类管理')).toBeInTheDocument()
|
||||
expect(screen.getByText('搜索')).toBeInTheDocument()
|
||||
expect(screen.getByText('分析看板')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// ── Items Tab (default) ──────────────────────────────────────
|
||||
|
||||
it('displays items in default tab', async () => {
|
||||
setupKnowledgeHandlers()
|
||||
renderWithProviders(<Knowledge />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('API 认证指南')).toBeInTheDocument()
|
||||
})
|
||||
expect(screen.getByText('玩具市场趋势 2026')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('displays item status with Chinese labels', async () => {
|
||||
setupKnowledgeHandlers()
|
||||
renderWithProviders(<Knowledge />)
|
||||
|
||||
await waitFor(() => {
|
||||
// status "active" is displayed as "活跃" via statusLabels mapping
|
||||
const activeLabels = screen.getAllByText('活跃')
|
||||
expect(activeLabels.length).toBeGreaterThanOrEqual(2)
|
||||
})
|
||||
})
|
||||
|
||||
it('displays item version column', async () => {
|
||||
setupKnowledgeHandlers()
|
||||
renderWithProviders(<Knowledge />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('API 认证指南')).toBeInTheDocument()
|
||||
})
|
||||
// Version numbers in the table
|
||||
expect(screen.getByText('2')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows empty state when no items', async () => {
|
||||
setupKnowledgeHandlers({
|
||||
items: { items: [], total: 0, page: 1, page_size: 20 },
|
||||
})
|
||||
renderWithProviders(<Knowledge />)
|
||||
|
||||
await waitFor(() => {
|
||||
const empties = screen.getAllByText('暂无数据')
|
||||
expect(empties.length).toBeGreaterThanOrEqual(1)
|
||||
})
|
||||
})
|
||||
|
||||
// ── Categories Tab ───────────────────────────────────────────
|
||||
|
||||
it('switches to categories tab and displays categories', async () => {
|
||||
setupKnowledgeHandlers()
|
||||
renderWithProviders(<Knowledge />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('API 认证指南')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Find the tab button (not the panel heading) and click it
|
||||
const categoryTabs = screen.getAllByText('分类管理')
|
||||
await act(async () => {
|
||||
fireEvent.click(categoryTabs[0])
|
||||
})
|
||||
|
||||
// Wait for the categories panel to render its heading and tree
|
||||
await waitFor(() => {
|
||||
// "新建分类" button should appear in the CategoriesPanel toolbar
|
||||
expect(screen.getByText('新建分类')).toBeInTheDocument()
|
||||
}, { timeout: 3000 })
|
||||
|
||||
// Category names rendered via Tree component inside spans with icon prefix
|
||||
// Use stringContaining since the text includes icon emoji prefix
|
||||
expect(screen.getByText((content) => content.includes('技术文档'))).toBeInTheDocument()
|
||||
expect(screen.getByText((content) => content.includes('行业知识'))).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows empty categories state', async () => {
|
||||
setupKnowledgeHandlers({ categories: [] })
|
||||
renderWithProviders(<Knowledge />)
|
||||
|
||||
// Wait for items tab to load first
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('API 认证指南')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Switch to categories tab
|
||||
const categoryTabs = screen.getAllByText('分类管理')
|
||||
await act(async () => {
|
||||
fireEvent.click(categoryTabs[0])
|
||||
})
|
||||
|
||||
// The CategoriesPanel should show "暂无分类,请新建一个" for empty state
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('暂无分类,请新建一个')).toBeInTheDocument()
|
||||
}, { timeout: 3000 })
|
||||
})
|
||||
|
||||
// ── Analytics Tab ────────────────────────────────────────────
|
||||
|
||||
it('switches to analytics tab and shows overview stats', async () => {
|
||||
setupKnowledgeHandlers()
|
||||
renderWithProviders(<Knowledge />)
|
||||
|
||||
const analyticsTab = screen.getByText('分析看板')
|
||||
await act(async () => {
|
||||
fireEvent.click(analyticsTab)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('总条目数')).toBeInTheDocument()
|
||||
})
|
||||
expect(screen.getByText('活跃条目')).toBeInTheDocument()
|
||||
expect(screen.getByText('分类数')).toBeInTheDocument()
|
||||
expect(screen.getByText('本周新增')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('displays analytics numbers', async () => {
|
||||
setupKnowledgeHandlers()
|
||||
renderWithProviders(<Knowledge />)
|
||||
|
||||
const analyticsTab = screen.getByText('分析看板')
|
||||
await act(async () => {
|
||||
fireEvent.click(analyticsTab)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
// total_items: 8
|
||||
expect(screen.getByText('8')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ── Error Handling ───────────────────────────────────────────
|
||||
|
||||
it('shows empty on API failure', async () => {
|
||||
server.use(
|
||||
http.get('*/api/v1/knowledge/categories', () => {
|
||||
return HttpResponse.json(
|
||||
{ error: 'internal_error', message: '数据库错误' },
|
||||
{ status: 500 },
|
||||
)
|
||||
}),
|
||||
http.get('*/api/v1/knowledge/items', () => {
|
||||
return HttpResponse.json(
|
||||
{ error: 'internal_error', message: '数据库错误' },
|
||||
{ status: 500 },
|
||||
)
|
||||
}),
|
||||
)
|
||||
renderWithProviders(<Knowledge />)
|
||||
|
||||
await waitFor(() => {
|
||||
const empties = screen.getAllByText('暂无数据')
|
||||
expect(empties.length).toBeGreaterThanOrEqual(1)
|
||||
})
|
||||
})
|
||||
|
||||
// ── Loading State ────────────────────────────────────────────
|
||||
|
||||
it('shows loading spinner', async () => {
|
||||
server.use(
|
||||
http.get('*/api/v1/knowledge/categories', async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
return HttpResponse.json(mockCategories)
|
||||
}),
|
||||
http.get('*/api/v1/knowledge/items', async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
return HttpResponse.json(mockItems)
|
||||
}),
|
||||
)
|
||||
renderWithProviders(<Knowledge />)
|
||||
expect(document.querySelector('.ant-spin')).toBeTruthy()
|
||||
})
|
||||
|
||||
// ── Item Tags ────────────────────────────────────────────────
|
||||
|
||||
it('displays item tags in table', async () => {
|
||||
setupKnowledgeHandlers()
|
||||
renderWithProviders(<Knowledge />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('api')).toBeInTheDocument()
|
||||
})
|
||||
expect(screen.getByText('auth')).toBeInTheDocument()
|
||||
expect(screen.getByText('market')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
227
admin-v2/tests/pages/Roles.test.tsx
Normal file
227
admin-v2/tests/pages/Roles.test.tsx
Normal file
@@ -0,0 +1,227 @@
|
||||
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 Roles from '@/pages/Roles'
|
||||
|
||||
// ── Mock data ──────────────────────────────────────────────────
|
||||
|
||||
const mockRoles = [
|
||||
{
|
||||
id: 'role-admin', name: 'admin', description: '管理员',
|
||||
permissions: ['admin:full'], account_count: 2,
|
||||
created_at: '2026-01-01T00:00:00Z', updated_at: '2026-01-01T00:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 'role-user', name: 'user', description: '普通用户',
|
||||
permissions: ['model:read', 'relay:use', 'config:read', 'prompt:read'],
|
||||
account_count: 15,
|
||||
created_at: '2026-01-01T00:00:00Z', updated_at: '2026-01-01T00:00:00Z',
|
||||
},
|
||||
]
|
||||
|
||||
const mockTemplates = [
|
||||
{
|
||||
id: 'tpl-read-only', name: '只读模板', description: '仅查看权限',
|
||||
permissions: ['model:read', 'config:read'],
|
||||
created_at: '2026-02-01T00:00:00Z', updated_at: '2026-02-01T00:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 'tpl-operator', name: '操作员模板', description: '操作权限',
|
||||
permissions: ['model:read', 'relay:use', 'config:read', 'prompt:read', 'hand:use'],
|
||||
created_at: '2026-02-15T00:00:00Z', updated_at: '2026-02-15T00: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 setupRolesHandlers(overrides: Record<string, unknown> = {}) {
|
||||
server.use(
|
||||
http.get('*/api/v1/roles', () => {
|
||||
return HttpResponse.json(overrides.roles ?? mockRoles)
|
||||
}),
|
||||
http.get('*/api/v1/permission-templates', () => {
|
||||
return HttpResponse.json(overrides.templates ?? mockTemplates)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
describe('Roles', () => {
|
||||
it('renders page title', () => {
|
||||
setupRolesHandlers()
|
||||
renderWithProviders(<Roles />)
|
||||
expect(screen.getByText('角色与权限')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('displays tabs', () => {
|
||||
setupRolesHandlers()
|
||||
renderWithProviders(<Roles />)
|
||||
// Tabs use label spans with icons
|
||||
expect(screen.getByText('角色')).toBeInTheDocument()
|
||||
expect(screen.getByText('权限模板')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('displays roles in default tab', async () => {
|
||||
setupRolesHandlers()
|
||||
renderWithProviders(<Roles />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('admin')).toBeInTheDocument()
|
||||
})
|
||||
expect(screen.getByText('user')).toBeInTheDocument()
|
||||
expect(screen.getByText('管理员')).toBeInTheDocument()
|
||||
expect(screen.getByText('普通用户')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('displays permissions count tags', async () => {
|
||||
setupRolesHandlers()
|
||||
renderWithProviders(<Roles />)
|
||||
|
||||
await waitFor(() => {
|
||||
// "1 项" for admin (1 permission), "4 项" for user (4 permissions)
|
||||
expect(screen.getByText('1 项')).toBeInTheDocument()
|
||||
expect(screen.getByText('4 项')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('displays account count column', async () => {
|
||||
setupRolesHandlers()
|
||||
renderWithProviders(<Roles />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('admin')).toBeInTheDocument()
|
||||
})
|
||||
// account_count: admin=2, user=15
|
||||
expect(screen.getByText('2')).toBeInTheDocument()
|
||||
expect(screen.getByText('15')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('has 新建角色 button', async () => {
|
||||
setupRolesHandlers()
|
||||
renderWithProviders(<Roles />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('新建角色')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('renders 操作 column for role rows', async () => {
|
||||
setupRolesHandlers()
|
||||
renderWithProviders(<Roles />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('admin')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// 操作 column header should exist
|
||||
expect(screen.getByText('操作')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows empty roles state', async () => {
|
||||
setupRolesHandlers({ roles: [], templates: mockTemplates })
|
||||
renderWithProviders(<Roles />)
|
||||
|
||||
await waitFor(() => {
|
||||
const empties = screen.getAllByText('暂无数据')
|
||||
expect(empties.length).toBeGreaterThanOrEqual(1)
|
||||
})
|
||||
})
|
||||
|
||||
it('switches to templates tab and displays templates', async () => {
|
||||
setupRolesHandlers()
|
||||
renderWithProviders(<Roles />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('admin')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Click 权限模板 tab
|
||||
screen.getByText('权限模板').click()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('只读模板')).toBeInTheDocument()
|
||||
})
|
||||
expect(screen.getByText('操作员模板')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('displays template permission counts', async () => {
|
||||
setupRolesHandlers()
|
||||
renderWithProviders(<Roles />)
|
||||
|
||||
screen.getByText('权限模板').click()
|
||||
|
||||
await waitFor(() => {
|
||||
// read-only: 2 permissions, operator: 5 permissions
|
||||
expect(screen.getByText('2 项')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('shows empty templates state', async () => {
|
||||
setupRolesHandlers({ roles: mockRoles, templates: [] })
|
||||
renderWithProviders(<Roles />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('admin')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
screen.getByText('权限模板').click()
|
||||
|
||||
await waitFor(() => {
|
||||
const empties = screen.getAllByText('暂无数据')
|
||||
expect(empties.length).toBeGreaterThanOrEqual(1)
|
||||
})
|
||||
})
|
||||
|
||||
it('has 新建模板 button in templates tab', async () => {
|
||||
setupRolesHandlers()
|
||||
renderWithProviders(<Roles />)
|
||||
|
||||
screen.getByText('权限模板').click()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('新建模板')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('shows empty on roles API failure', async () => {
|
||||
server.use(
|
||||
http.get('*/api/v1/roles', () => {
|
||||
return HttpResponse.json(
|
||||
{ error: 'internal_error', message: '数据库错误' },
|
||||
{ status: 500 },
|
||||
)
|
||||
}),
|
||||
http.get('*/api/v1/permission-templates', () => {
|
||||
return HttpResponse.json(mockTemplates)
|
||||
}),
|
||||
)
|
||||
renderWithProviders(<Roles />)
|
||||
|
||||
await waitFor(() => {
|
||||
// ProTable shows 暂无数据 when data fetch fails
|
||||
const empties = screen.getAllByText('暂无数据')
|
||||
expect(empties.length).toBeGreaterThanOrEqual(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
268
admin-v2/tests/pages/ScheduledTasks.test.tsx
Normal file
268
admin-v2/tests/pages/ScheduledTasks.test.tsx
Normal file
@@ -0,0 +1,268 @@
|
||||
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 ScheduledTasks from '@/pages/ScheduledTasks'
|
||||
|
||||
// ── Mock data ──────────────────────────────────────────────────
|
||||
|
||||
const mockTasks = [
|
||||
{
|
||||
id: 'task-001',
|
||||
name: '每日早报',
|
||||
schedule: '0 8 * * *',
|
||||
schedule_type: 'cron',
|
||||
target: { type: 'agent', id: 'daily-news' },
|
||||
enabled: true,
|
||||
description: '每天早上8点推送新闻',
|
||||
last_run: '2026-04-07T08:00:00Z',
|
||||
next_run: '2026-04-08T08:00:00Z',
|
||||
run_count: 30,
|
||||
last_result: null,
|
||||
last_error: null,
|
||||
last_duration_ms: 1500,
|
||||
created_at: '2026-03-01T00:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 'task-002',
|
||||
name: '定时采集',
|
||||
schedule: '30m',
|
||||
schedule_type: 'interval',
|
||||
target: { type: 'hand', id: 'collector' },
|
||||
enabled: false,
|
||||
description: null,
|
||||
last_run: null,
|
||||
next_run: null,
|
||||
run_count: 0,
|
||||
last_result: null,
|
||||
last_error: null,
|
||||
last_duration_ms: null,
|
||||
created_at: '2026-04-01T00: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>,
|
||||
)
|
||||
}
|
||||
|
||||
describe('ScheduledTasks', () => {
|
||||
it('renders page header', () => {
|
||||
server.use(
|
||||
http.get('*/api/v1/scheduler/tasks', () => HttpResponse.json([])),
|
||||
)
|
||||
renderWithProviders(<ScheduledTasks />)
|
||||
expect(screen.getByText('定时任务')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows loading spinner', async () => {
|
||||
server.use(
|
||||
http.get('*/api/v1/scheduler/tasks', async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
return HttpResponse.json(mockTasks)
|
||||
}),
|
||||
)
|
||||
renderWithProviders(<ScheduledTasks />)
|
||||
expect(document.querySelector('.ant-spin')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('displays tasks in table', async () => {
|
||||
server.use(
|
||||
http.get('*/api/v1/scheduler/tasks', () => HttpResponse.json(mockTasks)),
|
||||
)
|
||||
renderWithProviders(<ScheduledTasks />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('每日早报')).toBeInTheDocument()
|
||||
})
|
||||
expect(screen.getByText('定时采集')).toBeInTheDocument()
|
||||
expect(screen.getByText('0 8 * * *')).toBeInTheDocument()
|
||||
expect(screen.getByText('30m')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('displays Chinese schedule type labels', async () => {
|
||||
server.use(
|
||||
http.get('*/api/v1/scheduler/tasks', () => HttpResponse.json(mockTasks)),
|
||||
)
|
||||
renderWithProviders(<ScheduledTasks />)
|
||||
|
||||
await waitFor(() => {
|
||||
// schedule_type labels: cron → "Cron", interval → "间隔"
|
||||
expect(screen.getByText('Cron')).toBeInTheDocument()
|
||||
})
|
||||
expect(screen.getByText('间隔')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('displays target type with English labels', async () => {
|
||||
server.use(
|
||||
http.get('*/api/v1/scheduler/tasks', () => HttpResponse.json(mockTasks)),
|
||||
)
|
||||
renderWithProviders(<ScheduledTasks />)
|
||||
|
||||
await waitFor(() => {
|
||||
// target type labels: agent → "Agent", hand → "Hand"
|
||||
expect(screen.getByText('Agent')).toBeInTheDocument()
|
||||
})
|
||||
expect(screen.getByText('Hand')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('displays target IDs', async () => {
|
||||
server.use(
|
||||
http.get('*/api/v1/scheduler/tasks', () => HttpResponse.json(mockTasks)),
|
||||
)
|
||||
renderWithProviders(<ScheduledTasks />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('daily-news')).toBeInTheDocument()
|
||||
})
|
||||
expect(screen.getByText('collector')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('displays enabled switches', async () => {
|
||||
server.use(
|
||||
http.get('*/api/v1/scheduler/tasks', () => HttpResponse.json(mockTasks)),
|
||||
)
|
||||
renderWithProviders(<ScheduledTasks />)
|
||||
|
||||
await waitFor(() => {
|
||||
const switches = document.querySelectorAll('.ant-switch')
|
||||
expect(switches.length).toBeGreaterThanOrEqual(2)
|
||||
})
|
||||
})
|
||||
|
||||
it('displays run count', async () => {
|
||||
server.use(
|
||||
http.get('*/api/v1/scheduler/tasks', () => HttpResponse.json(mockTasks)),
|
||||
)
|
||||
renderWithProviders(<ScheduledTasks />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('每日早报')).toBeInTheDocument()
|
||||
})
|
||||
// run_count: 30 is displayed in tabular-nums span
|
||||
expect(screen.getByText('30')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows last error in red when present', async () => {
|
||||
const tasksWithError = [{ ...mockTasks[0], last_error: 'Connection timeout' }]
|
||||
server.use(
|
||||
http.get('*/api/v1/scheduler/tasks', () => HttpResponse.json(tasksWithError)),
|
||||
)
|
||||
renderWithProviders(<ScheduledTasks />)
|
||||
|
||||
await waitFor(() => {
|
||||
const errorEl = screen.getByText('Connection timeout')
|
||||
expect(errorEl).toHaveClass('text-red-500')
|
||||
})
|
||||
})
|
||||
|
||||
it('shows dash for null last error', async () => {
|
||||
server.use(
|
||||
http.get('*/api/v1/scheduler/tasks', () => HttpResponse.json(mockTasks)),
|
||||
)
|
||||
renderWithProviders(<ScheduledTasks />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('每日早报')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('shows empty state when no tasks', async () => {
|
||||
server.use(
|
||||
http.get('*/api/v1/scheduler/tasks', () => HttpResponse.json([])),
|
||||
)
|
||||
renderWithProviders(<ScheduledTasks />)
|
||||
|
||||
await waitFor(() => {
|
||||
const empties = screen.getAllByText('暂无数据')
|
||||
expect(empties.length).toBeGreaterThanOrEqual(1)
|
||||
})
|
||||
})
|
||||
|
||||
it('shows error state on API failure', async () => {
|
||||
server.use(
|
||||
http.get('*/api/v1/scheduler/tasks', () => {
|
||||
return HttpResponse.json(
|
||||
{ error: 'internal_error', message: '数据库错误' },
|
||||
{ status: 500 },
|
||||
)
|
||||
}),
|
||||
)
|
||||
renderWithProviders(<ScheduledTasks />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('加载失败')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('has 新建任务 button', async () => {
|
||||
server.use(
|
||||
http.get('*/api/v1/scheduler/tasks', () => HttpResponse.json([])),
|
||||
)
|
||||
renderWithProviders(<ScheduledTasks />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('新建任务')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('renders action column with buttons', async () => {
|
||||
server.use(
|
||||
http.get('*/api/v1/scheduler/tasks', () => HttpResponse.json(mockTasks)),
|
||||
)
|
||||
renderWithProviders(<ScheduledTasks />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('每日早报')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// 操作 column header should be rendered
|
||||
expect(screen.getByText('操作')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('color-codes schedule type tags', async () => {
|
||||
server.use(
|
||||
http.get('*/api/v1/scheduler/tasks', () => HttpResponse.json(mockTasks)),
|
||||
)
|
||||
renderWithProviders(<ScheduledTasks />)
|
||||
|
||||
await waitFor(() => {
|
||||
const cronTag = screen.getByText('Cron').closest('.ant-tag')
|
||||
expect(cronTag?.className).toMatch(/blue/)
|
||||
})
|
||||
const intervalTag = screen.getByText('间隔').closest('.ant-tag')
|
||||
expect(intervalTag?.className).toMatch(/green/)
|
||||
})
|
||||
|
||||
it('color-codes target type tags', async () => {
|
||||
server.use(
|
||||
http.get('*/api/v1/scheduler/tasks', () => HttpResponse.json(mockTasks)),
|
||||
)
|
||||
renderWithProviders(<ScheduledTasks />)
|
||||
|
||||
await waitFor(() => {
|
||||
const agentTag = screen.getByText('Agent').closest('.ant-tag')
|
||||
expect(agentTag?.className).toMatch(/purple/)
|
||||
})
|
||||
const handTag = screen.getByText('Hand').closest('.ant-tag')
|
||||
expect(handTag?.className).toMatch(/cyan/)
|
||||
})
|
||||
})
|
||||
@@ -40,4 +40,11 @@ beforeAll(() => {
|
||||
return {} as CSSStyleDeclaration
|
||||
}
|
||||
}
|
||||
|
||||
// Ant Design ProTable / rc-virtual-list require ResizeObserver
|
||||
global.ResizeObserver = class ResizeObserver {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user