Files
dodo/frontend/e2e/task-detail-paper-flow.spec.ts
T
bboysoul 8ab5ad05a0
ci / gitleaks (push) Successful in 6s
ci / docker (push) Successful in 3m32s
feat: add resizable desktop panes
2026-09-20 11:33:43 +08:00

104 lines
6.3 KiB
TypeScript

import type { APIRequestContext, Locator, Page } from '@playwright/test'
import { allowExpectedError, expect, test } from './fixtures'
async function csrf(request: APIRequestContext) {
const state = await request.storageState()
return state.cookies.find(cookie => cookie.name === 'dodo_csrf')?.value ?? ''
}
async function mutate(request: APIRequestContext, baseURL: string, path: string, options: Parameters<APIRequestContext['fetch']>[1]) {
return request.fetch(path, { ...options, headers: { ...options?.headers, 'x-csrf-token': await csrf(request), origin: baseURL } })
}
async function createFixture(request: APIRequestContext, baseURL: string, suffix: string) {
const bootstrap = await request.get('/api/v1/bootstrap')
expect(bootstrap.ok(), await bootstrap.text()).toBeTruthy()
const inbox = (await bootstrap.json()).lists.find((item: { is_inbox: boolean }) => item.is_inbox)
const title = `纸页顺流-${suffix}-${'很长的任务标题'.repeat(8)}`
const parentResponse = await mutate(request, baseURL, '/api/v1/tasks', {
method: 'POST', data: { title, description: `备注\n\n${'长内容 '.repeat(40)}`, list_id: inbox.id, due_at: '2030-06-18T08:30:00Z', due_has_time: true },
})
expect(parentResponse.ok(), await parentResponse.text()).toBeTruthy()
const parent = await parentResponse.json() as { id: string }
const childTitle = `子任务-${suffix}-/Users/example/${'unbroken-path/'.repeat(18)}file.txt`
const childResponse = await mutate(request, baseURL, '/api/v1/tasks', { method: 'POST', data: { title: childTitle, list_id: inbox.id, parent_id: parent.id } })
expect(childResponse.ok(), await childResponse.text()).toBeTruthy()
return { title, childTitle }
}
async function openDetail(page: Page, title: string) {
await page.goto('/')
const inbox = page.locator('.sidebar').getByRole('button', { name: '收集箱', exact: true })
if ((await page.viewportSize())!.width <= 930) await page.locator('.topbar').getByRole('button', { name: /展开菜单|收起菜单/ }).click()
await inbox.click()
const row = page.locator('.task-row').filter({ has: page.locator('strong', { hasText: title }) })
await expect(row).toHaveCount(1)
await row.locator('.task-main').click()
const detail = page.getByRole('dialog', { name: '任务详情' })
await expect(detail).toBeVisible()
await expect(detail.getByRole('button', { name: '保存更改' })).toBeEnabled()
return detail
}
async function rect(locator: Locator) {
const value = await locator.boundingBox()
expect(value).not.toBeNull()
return value!
}
test('task detail paper flow keeps approved responsive geometry and material', async ({ page, request, baseURL }, testInfo) => {
const fixture = await createFixture(request, baseURL!, `${testInfo.project.name}-${Date.now()}`)
const detail = await openDetail(page, fixture.title)
const viewport = page.viewportSize()!
const header = detail.locator('.detail-head')
const body = detail.locator('.detail-form')
const footer = detail.locator('.detail-actions')
const dateTime = detail.locator('.task-detail-date-time')
const nested = detail.locator('.subtask-detail,.after-completion-fields,.repeat-custom-fields')
const metrics = await detail.evaluate((element: HTMLElement) => {
const style = getComputedStyle(element)
const childrenInside = [...element.querySelectorAll<HTMLElement>('.detail-form > *')].every(child => child.getBoundingClientRect().right <= element.getBoundingClientRect().right + 1)
return { width: element.getBoundingClientRect().width, maxHeight: style.maxHeight, clientWidth: element.clientWidth, scrollWidth: element.scrollWidth, childrenInside }
})
expect(metrics.scrollWidth).toBeLessThanOrEqual(metrics.clientWidth)
expect(metrics.childrenInside).toBe(true)
expect((await rect(header)).height).toBeCloseTo(58, 0)
expect((await body.evaluate(el => parseFloat(getComputedStyle(el).paddingLeft)))).toBeCloseTo(18, 0)
const nestedStyles = await nested.evaluateAll(elements => elements.map(element => { const s = getComputedStyle(element); return { radius: s.borderRadius, shadow: s.boxShadow, background: s.backgroundColor } }))
for (const style of nestedStyles) { expect(style.radius).toBe('0px'); expect(style.shadow).toBe('none'); expect(style.background).toBe('rgba(0, 0, 0, 0)') }
for (const button of await detail.locator('button').all()) {
const box = await button.boundingBox()
expect(box?.height ?? 0).toBeGreaterThanOrEqual(44)
}
const footerBox = await rect(footer)
const detailBox = await rect(detail)
expect(Math.abs((footerBox.y + footerBox.height) - (detailBox.y + detailBox.height))).toBeLessThanOrEqual(2)
if (viewport.width >= 931) {
expect(metrics.width).toBeCloseTo(Number(await page.getByRole('separator', { name: '调整任务详情宽度' }).getAttribute('aria-valuenow')), 0)
const columns = await dateTime.locator('.task-detail-field').evaluateAll(elements => elements.map(element => element.getBoundingClientRect()))
expect(columns[0].top).toBeCloseTo(columns[1].top, 0)
expect(columns[1].left - columns[0].right).toBeCloseTo(10, 0)
} else {
expect(metrics.width).toBeCloseTo(viewport.width, 0)
expect(parseFloat(metrics.maxHeight)).toBeLessThanOrEqual(viewport.height * .88 + 1)
const columns = await dateTime.locator('.task-detail-field').evaluateAll(elements => elements.map(element => element.getBoundingClientRect()))
expect(columns[1].top).toBeGreaterThanOrEqual(columns[0].bottom)
expect(columns[0].width).toBeCloseTo(columns[1].width, 0)
}
})
test('subtask removal keeps the parent detail open and removes only the child', async ({ page, request, baseURL }, testInfo) => {
const fixture = await createFixture(request, baseURL!, `remove-${testInfo.project.name}-${Date.now()}`)
const detail = await openDetail(page, fixture.title)
const child = detail.locator('.subtask-detail').filter({ hasText: fixture.childTitle })
await expect(child).toHaveCount(1)
await child.getByRole('button', { name: `删除子任务${fixture.childTitle}` }).click()
const confirm = page.getByRole('dialog', { name: /删除子任务/ })
allowExpectedError(page, 'requestfailed: DELETE http://127.0.0.1:5173/api/v1/tasks/')
await confirm.getByRole('button', { name: '确认' }).click()
await expect(child).toHaveCount(0)
await expect(detail).toBeVisible()
await expect(detail.locator('textarea[aria-label="任务标题"]')).toHaveValue(fixture.title)
})