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( {ui} , ) } describe('ScheduledTasks', () => { it('renders page header', () => { server.use( http.get('*/api/v1/scheduler/tasks', () => HttpResponse.json([])), ) renderWithProviders() 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() expect(document.querySelector('.ant-spin')).toBeTruthy() }) it('displays tasks in table', async () => { server.use( http.get('*/api/v1/scheduler/tasks', () => HttpResponse.json(mockTasks)), ) renderWithProviders() 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() 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() 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() 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() 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() 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() 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() 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() 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() await waitFor(() => { expect(screen.getByText('加载失败')).toBeInTheDocument() }) }) it('has 新建任务 button', async () => { server.use( http.get('*/api/v1/scheduler/tasks', () => HttpResponse.json([])), ) renderWithProviders() 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() 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() 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() 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/) }) })