Files
task_pilot/frontend/src/App.test.tsx
T
2026-10-18 22:52:18 +08:00

164 lines
4.6 KiB
TypeScript

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<Response> => {
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(
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<App />
</MemoryRouter>
</QueryClientProvider>,
)
}
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')
})
})