import { mkdirSync, writeFileSync } from 'node:fs' import { join } from 'node:path' import { fileURLToPath } from 'node:url' import { expect, test, type APIRequestContext } from '@playwright/test' const venvProject = fileURLToPath( new URL('../../.e2e-data/venv-project', import.meta.url), ) function createVenvFixture(): string { const scripts = join(venvProject, '.venv', 'Scripts') mkdirSync(scripts, { recursive: true }) writeFileSync(join(venvProject, '.venv', 'pyvenv.cfg'), 'home = C:\\Python\n') writeFileSync(join(scripts, 'python.exe'), '') return venvProject } const taskPayload = { id: 'e2e_echo', name: 'E2E echo', cron: '0 0 2 * * *', timezone: 'Asia/Shanghai', enabled: true, timeout_secs: 10, action: { type: 'script', working_dir: '.', interpreter: 'cmd.exe', script: '/C', args: ['echo', 'e2e-output'], env: {}, }, } async function authHeaders(request: APIRequestContext) { const response = await request.get('/api/v1/session') expect(response.ok()).toBeTruthy() const session = (await response.json()) as { token: string } return { 'X-TaskPilot-Token': session.token } } test('manages a task and reads a real run log', async ({ page, request }) => { const headers = await authHeaders(request) const existing = await request.get('/api/v1/tasks?page=1&page_size=100') expect(existing.ok()).toBeTruthy() const existingBody = (await existing.json()) as { items: Array<{ id: string }> } for (const task of existingBody.items) { await request.delete(`/api/v1/tasks/${task.id}`, { headers }) } const createResponse = await request.post('/api/v1/tasks', { data: taskPayload, headers, }) expect(createResponse.status()).toBe(201) await page.goto('/') await expect(page.getByText('E2E echo')).toBeVisible() await page.screenshot({ path: 'test-results/dashboard-desktop.png', fullPage: true, }) await page.getByRole('button', { name: '+ 新建任务' }).click() const newTaskDialog = page.getByRole('dialog', { name: '新建定时任务' }) await expect(newTaskDialog).toBeVisible() await expect(newTaskDialog.getByLabel('任务标识')).toBeFocused() await page.screenshot({ path: 'test-results/dashboard-task-editor.png', fullPage: true, }) const workingDir = createVenvFixture() await newTaskDialog.getByLabel(/项目工作目录/).fill(workingDir) await expect( newTaskDialog.locator('.env-status-badge.ok code'), ).toHaveText('.venv\\Scripts\\python.exe') await expect(newTaskDialog.getByText(/已关联虚拟环境/)).toBeVisible() await page.screenshot({ path: 'test-results/dashboard-venv-detected.png', fullPage: true, }) const addArgumentButton = newTaskDialog.getByRole('button', { name: '+ 添加参数', }) await addArgumentButton.focus() await page.keyboard.press('Enter') await newTaskDialog .getByPlaceholder('参数名 (如 --mode 或 -v)') .fill('--mode') await newTaskDialog .getByPlaceholder('参数值 (可选,无值可留空)') .fill('full') await newTaskDialog .getByLabel(/虚拟环境目录/) .fill('.venv') await newTaskDialog .getByText('调用预览 (Runtime Preview)') .scrollIntoViewIfNeeded() await expect( newTaskDialog.getByText( '.venv\\Scripts\\python.exe main.py --mode full', { exact: false }, ), ).toBeVisible() await page.screenshot({ path: 'test-results/dashboard-task-editor-runtime.png', fullPage: true, }) await page.keyboard.press('Escape') await expect(newTaskDialog).toBeHidden() await page.getByRole('button', { name: '运行' }).click() const drawer = page.getByRole('dialog', { name: 'E2E echo' }) await expect(drawer).toBeVisible() await expect(drawer.getByText('e2e-output')).toBeVisible() await expect(drawer.locator('.status-succeeded').first()).toBeVisible() await page.screenshot({ path: 'test-results/dashboard-log-drawer.png', fullPage: true, }) await drawer.getByRole('button', { name: '关闭运行日志' }).click() await page.getByRole('button', { name: '编辑' }).click() const editor = page.getByRole('dialog', { name: '编辑任务 [e2e_echo]', }) await expect(editor).toBeVisible() const nameInput = editor.getByLabel('名称') await nameInput.fill('E2E updated') await editor.getByRole('button', { name: '保存修改' }).click() await expect(page.getByText('E2E updated')).toBeVisible() await page.getByRole('button', { name: '停用' }).click() await expect( page .getByRole('row', { name: /E2E updated/ }) .locator('.status-disabled'), ).toBeVisible() await page.getByRole('button', { name: '删除' }).click() await page .getByRole('alertdialog') .getByRole('button', { name: '确认删除' }) .click() await expect(page.getByRole('heading', { name: '还没有任务' })).toBeVisible() }) test('keeps the dashboard usable on a narrow viewport', async ({ page, request, }) => { const headers = await authHeaders(request) await request.post('/api/v1/tasks', { headers, data: { ...taskPayload, id: 'e2e_mobile', name: 'Mobile task', }, }) await page.setViewportSize({ width: 390, height: 844 }) await page.goto('/') await expect(page.getByText('Mobile task')).toBeVisible() await expect(page.getByRole('button', { name: '+ 新建任务' })).toBeVisible() await page.screenshot({ path: 'test-results/dashboard-mobile.png', fullPage: true, }) }) test('cancels a running task from the log drawer', async ({ page, request, }) => { const headers = await authHeaders(request) const response = await request.post('/api/v1/tasks', { headers, data: { ...taskPayload, id: 'e2e_cancel', name: 'Cancel task', timeout_secs: 30, action: { ...taskPayload.action, args: ['ping', '-t', '127.0.0.1'], }, }, }) expect(response.status()).toBe(201) await page.goto('/') await page .getByRole('row', { name: /Cancel task/ }) .getByRole('button', { name: '运行' }) .click() const drawer = page.getByRole('dialog', { name: 'Cancel task' }) await expect(drawer).toBeVisible() await expect(drawer.getByText('SSE 实时连接')).toBeVisible() await drawer.getByRole('button', { name: '取消运行' }).click() await expect(drawer.locator('.status-cancelled').first()).toBeVisible() })