Why: make material, background work, confirmations and files accessible to nontechnical users across desktop and mobile. Evidence: Dev SELF receipts for Frontend tsc, 190 tests, knip baseline, build, nine fixture browser cases and two real HTTP/browser smoke cases using MockRunner. Impact: existing API and permissions remain authoritative; stale events/queries and duplicate effects are guarded, while one file preview and reading state are preserved. Refs: work-companion-ui design r0.2 and plan r1 U2-U5; UI01-UI13.
73 lines
4.3 KiB
TypeScript
73 lines
4.3 KiB
TypeScript
import { test, expect, type Page, type APIRequestContext } from '@playwright/test'
|
|
|
|
test.skip(!process.env.WORK_COMPANION_LIVE_BROWSER, 'Requires the task-owned backend, SQLite data and MockRunner')
|
|
|
|
async function openRealConversation(page: Page, request: APIRequestContext) {
|
|
const projectResponse = await request.post('/api/projects', { data: { name: '工作台验收 ' + Date.now() } })
|
|
expect(projectResponse.ok()).toBe(true)
|
|
const project = await projectResponse.json()
|
|
const conversationResponse = await request.post(`/api/projects/${project.id}/conversations`, { data: { title: '真实接口验收' } })
|
|
expect(conversationResponse.ok()).toBe(true)
|
|
const conversation = await conversationResponse.json()
|
|
await page.addInitScript(({ projectId, conversationId }) => {
|
|
localStorage.setItem('as.conv', conversationId)
|
|
localStorage.setItem('as.ui', JSON.stringify({ theme: 'light', rightCollapsed: true, lastProjectId: projectId }))
|
|
}, { projectId: project.id, conversationId: conversation.id })
|
|
await page.goto('/')
|
|
await expect(page.locator('.chat-title')).toHaveText('真实接口验收')
|
|
return { project, conversation }
|
|
}
|
|
|
|
test('real upload/send/reload preserves three actual attachments', async ({ page, request }) => {
|
|
const { project, conversation } = await openRealConversation(page, request)
|
|
const errors: string[] = []
|
|
page.on('pageerror', error => errors.push(error.message))
|
|
await page.locator('input[type=file]').first().setInputFiles([
|
|
{ name: '会议纪要.txt', mimeType: 'text/plain', buffer: Buffer.from('会议决定:先明确交付日期') },
|
|
{ name: '需求说明.txt', mimeType: 'text/plain', buffer: Buffer.from('需求:整理材料并生成摘要') },
|
|
{ name: '评审意见.txt', mimeType: 'text/plain', buffer: Buffer.from('评审意见:补充验收标准') },
|
|
])
|
|
await expect(page.locator('.attach-chip')).toHaveCount(3)
|
|
const artifacts = await request.get(`/api/projects/${project.id}/artifacts`).then(r => r.json())
|
|
expect(artifacts).toHaveLength(3)
|
|
await page.getByRole('button', { name: '发送', exact: true }).click()
|
|
await expect.poll(async () => {
|
|
const current = await request.get(`/api/conversations/${conversation.id}`).then(r => r.json())
|
|
return current.runs.some((run: { kind: string; status: string }) => run.kind === 'conversation' && run.status === 'completed')
|
|
}).toBe(true)
|
|
await page.reload()
|
|
await expect(page.locator('.chat-title')).toHaveText('真实接口验收')
|
|
await page.getByRole('button', { name: '打开文件', exact: true }).click()
|
|
await expect(page.locator('[data-artifact-row]')).toHaveCount(3)
|
|
await page.locator('[data-artifact-row]').first().click()
|
|
await expect(page.locator('.preview-pane-name')).toBeVisible()
|
|
expect(errors).toEqual([])
|
|
})
|
|
|
|
test('real background card and HITL decision survive query reconciliation', async ({ page, request }) => {
|
|
const { conversation } = await openRealConversation(page, request)
|
|
const errors: string[] = []
|
|
page.on('pageerror', error => errors.push(error.message))
|
|
const input = page.getByRole('textbox', { name: '工作目标或消息', exact: true })
|
|
// Backend's deterministic runner exercises the real run/card/event lifecycle;
|
|
// this is not a live-model quality or stage-findings test.
|
|
await input.fill('#background')
|
|
await page.getByRole('button', { name: '发送', exact: true }).click()
|
|
await expect.poll(async () => {
|
|
const out = await request.get(`/api/conversations/${conversation.id}/cards`).then(r => r.json())
|
|
return out.cards?.some((card: { kind: string; status: string }) => card.kind === 'progress' && ['done', 'failed'].includes(card.status))
|
|
}, { timeout: 15000 }).toBe(true)
|
|
await expect(page.locator('[data-conversation-work]')).toBeVisible()
|
|
await expect(page.getByRole('button', { name: '发送', exact: true })).toBeVisible()
|
|
await input.fill('#hitl')
|
|
await page.getByRole('button', { name: '发送', exact: true }).click()
|
|
await expect(page.locator('.hitl-card')).toBeVisible()
|
|
await page.locator('.hitl-card').getByRole('button', { name: /Markdown 文档/ }).click()
|
|
await expect(page.locator('.hitl-card')).toHaveCount(0)
|
|
await expect.poll(async () => {
|
|
const current = await request.get(`/api/conversations/${conversation.id}`).then(r => r.json())
|
|
return current.activeHitl.length
|
|
}).toBe(0)
|
|
expect(errors).toEqual([])
|
|
})
|