import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { MemoryRouter } from 'react-router-dom' import { describe, expect, it, vi } from 'vitest' import type { Task, TaskListResponse } from './api/client' import App from './App' const task: Task = { id: 'echo_task', name: 'Echo task', cron: '0 0 2 * * *', timezone: 'Asia/Shanghai', enabled: true, timeout_secs: 300, concurrency_policy: 'skip', retry_policy: { max_attempts: 0, delay_secs: 0, }, action: { type: 'script', working_dir: 'D:\\Projects\\echo', interpreter: 'python.exe', script: 'main.py', args: [], env: {}, }, created_at: '2026-10-18T10:00:00Z', updated_at: '2026-10-18T10:00:00Z', } const taskList: TaskListResponse = { items: [task], page: 1, page_size: 20, total: 1, } function jsonResponse(body: unknown, status = 200): Response { return new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' }, }) } function createFetchMock() { return vi.fn( async (input: RequestInfo | URL, init?: RequestInit): Promise => { const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url if (url.endsWith('/api/v1/health')) { return jsonResponse({ status: 'ok', service: 'task-pilot-backend', version: '0.1.0', }) } if (url.endsWith('/api/v1/session')) { return jsonResponse({ token: 'test-token' }) } if (url.endsWith('/api/v1/cron/preview')) { return jsonResponse({ timezone: 'Asia/Shanghai', runs: [ '2026-10-19 02:00:00', '2026-10-20 02:00:00', ], }) } if (url.includes('/api/v1/tasks') && init?.method === 'POST') { return jsonResponse( { ...task, ...JSON.parse(String(init.body)), }, 201, ) } if (url.includes('/api/v1/tasks')) { return jsonResponse(taskList) } return jsonResponse({ code: 'NOT_FOUND', message: 'not found', details: {} }, 404) }, ) } function renderApp() { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, }, }, }) return render( , ) } describe('TaskDashboard', () => { it('renders tasks returned by the Rust API', async () => { vi.stubGlobal('fetch', createFetchMock()) renderApp() expect(await screen.findByText('Echo task')).toBeInTheDocument() expect(screen.getByText('echo_task')).toBeInTheDocument() expect(screen.getByText('0 0 2 * * *')).toBeInTheDocument() expect(await screen.findByText('在线')).toBeInTheDocument() }) it('creates a script task with the visible form values', async () => { const fetchMock = createFetchMock() vi.stubGlobal('fetch', fetchMock) const user = userEvent.setup() renderApp() await user.click(screen.getByRole('button', { name: '+ 新建任务' })) await user.click(screen.getByRole('button', { name: '+ 添加参数' })) expect( screen.getByPlaceholderText('参数名 (如 --mode 或 -v)'), ).toBeInTheDocument() expect( screen.getByPlaceholderText('参数值 (可选,无值可留空)'), ).toBeInTheDocument() await user.clear(screen.getByLabelText(/任务标识/)) await user.type(screen.getByLabelText(/任务标识/), 'new_task') await user.type(screen.getByLabelText('名称'), 'New task') await user.type(screen.getByLabelText('工作目录'), 'D:\\Projects\\new') await user.click( screen.getByRole('button', { name: '保存并热刷新' }), ) await waitFor(() => { const createCall = fetchMock.mock.calls.find( ([input, init]) => String(input).endsWith('/api/v1/tasks') && init?.method === 'POST', ) expect(createCall).toBeDefined() }) const createCall = fetchMock.mock.calls.find( ([input, init]) => String(input).endsWith('/api/v1/tasks') && init?.method === 'POST', ) const payload = JSON.parse(String(createCall?.[1]?.body)) expect(payload.id).toBe('new_task') expect(payload.name).toBe('New task') expect(payload.action.type).toBe('script') expect(payload.action.working_dir).toBe('D:\\Projects\\new') }) })