Files
task_pilot/frontend/e2e/dashboard.spec.ts
T
CNWei dd7612bc74 feat: 编辑器实时探测并显示已关联虚拟环境
fix: 按系统 ANSI 代码页解码子进程输出,修复中文日志乱码
2026-10-24 22:30:10 +08:00

206 lines
6.3 KiB
TypeScript

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()
})