feat: redesign task detail paper flow
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
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(350, 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)
|
||||
})
|
||||
@@ -166,7 +166,10 @@ test('approved polish keeps search state, dense rows, title-only memos, and uniq
|
||||
await taskRow.locator('.task-main').click()
|
||||
await expect(taskRow).toHaveClass(/selected/)
|
||||
await expect(taskRow).toHaveCSS('background-color', transparent)
|
||||
const taskDetail = page.getByRole('dialog', { name: '任务详情' })
|
||||
await expect(taskDetail.getByRole('button', { name: '保存更改' })).toBeEnabled()
|
||||
await page.keyboard.press('Escape')
|
||||
await expect(taskDetail).toBeHidden()
|
||||
await searchInput.press('Escape')
|
||||
const collapsedToggle = page.getByRole('button', { name: '展开搜索任务' })
|
||||
await expect(collapsedToggle).toBeFocused()
|
||||
|
||||
Reference in New Issue
Block a user