110 lines
5.1 KiB
TypeScript
110 lines
5.1 KiB
TypeScript
import type { APIRequestContext, Locator, Page } from '@playwright/test'
|
|
import { 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 expectTaskRowGeometry(row: Locator) {
|
|
const metrics = await row.evaluate((element: HTMLElement) => {
|
|
const controls = [...element.querySelectorAll<HTMLElement>('button')].map(control => {
|
|
const rect = control.getBoundingClientRect()
|
|
return { width: rect.width, height: rect.height }
|
|
})
|
|
const tail = element.querySelector<HTMLElement>('.task-tail')
|
|
const tailRect = tail?.getBoundingClientRect()
|
|
const rowRect = element.getBoundingClientRect()
|
|
const directChildrenInside = [...element.children].every(child => {
|
|
const rect = (child as HTMLElement).getBoundingClientRect()
|
|
return rect.left >= rowRect.left && rect.right <= rowRect.right && rect.top >= rowRect.top && rect.bottom <= rowRect.bottom
|
|
})
|
|
return {
|
|
height: rowRect.height,
|
|
clientWidth: element.clientWidth,
|
|
scrollWidth: element.scrollWidth,
|
|
controls,
|
|
tailInside: !tailRect || (tailRect.left >= rowRect.left && tailRect.right <= rowRect.right),
|
|
directChildrenInside,
|
|
}
|
|
})
|
|
expect(metrics.height).toBeGreaterThanOrEqual(64)
|
|
expect(metrics.height).toBeLessThanOrEqual(68)
|
|
expect(metrics.scrollWidth).toBeLessThanOrEqual(metrics.clientWidth)
|
|
expect(metrics.tailInside).toBe(true)
|
|
expect(metrics.directChildrenInside).toBe(true)
|
|
for (const control of metrics.controls) {
|
|
expect(control.width).toBeGreaterThanOrEqual(44)
|
|
expect(control.height).toBeGreaterThanOrEqual(44)
|
|
}
|
|
}
|
|
|
|
async function openInbox(page: Page) {
|
|
if ((await page.viewportSize())!.width <= 930) {
|
|
await page.locator('.topbar').getByRole('button', { name: /展开菜单|收起菜单/ }).click()
|
|
}
|
|
await page.locator('.sidebar').getByRole('button', { name: '收集箱', exact: true }).click()
|
|
}
|
|
|
|
test('task rows keep approved rhythm without clipping across desktop and mobile', async ({ page, request, baseURL }) => {
|
|
const suffix = `${test.info().project.name}-${Date.now()}`
|
|
const bootstrap = await request.get('/api/v1/bootstrap')
|
|
expect(bootstrap.ok()).toBeTruthy()
|
|
const inbox = (await bootstrap.json()).lists.find((item: { is_inbox: boolean }) => item.is_inbox)
|
|
|
|
const create = async (title: string, extra: Record<string, unknown> = {}) => {
|
|
const response = await mutate(request, baseURL!, '/api/v1/tasks', {
|
|
method: 'POST', data: { title, list_id: inbox.id, ...extra },
|
|
})
|
|
expect(response.ok(), await response.text()).toBeTruthy()
|
|
return response.json() as Promise<{ id: string }>
|
|
}
|
|
|
|
const ordinaryTitle = `普通任务-${suffix}`
|
|
const completedTitle = `完成任务-${suffix}`
|
|
const longTitle = `超长任务-${suffix}-` + '这是一段用于确认标题省略且不撑高任务行的连续文字'.repeat(5)
|
|
const dueTitle = `含截止日期-${suffix}`
|
|
const parentTitle = `含子任务-${suffix}`
|
|
const localDate = (offset: number) => {
|
|
const date = new Date()
|
|
date.setDate(date.getDate() + offset)
|
|
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
|
|
}
|
|
await create(ordinaryTitle)
|
|
await create(completedTitle)
|
|
await create(longTitle)
|
|
await create(dueTitle, { due_at: `${localDate(1)}T23:59:00`, due_has_time: true })
|
|
const parent = await create(parentTitle)
|
|
await create(`子任务-${suffix}`, { parent_id: parent.id })
|
|
|
|
await page.setViewportSize({ width: 1440, height: 900 })
|
|
await page.goto('/')
|
|
await openInbox(page)
|
|
|
|
const titles = [ordinaryTitle, completedTitle, longTitle, dueTitle, parentTitle]
|
|
for (const title of titles) {
|
|
const row = page.locator('.task-row').filter({ hasText: title })
|
|
await expect(row).toHaveCount(1)
|
|
await expectTaskRowGeometry(row)
|
|
}
|
|
const completedRow = page.locator('.task-row').filter({ hasText: completedTitle })
|
|
await completedRow.getByRole('button', { name: `完成${completedTitle}` }).click()
|
|
await expect(completedRow).toHaveClass(/done/)
|
|
await expectTaskRowGeometry(completedRow)
|
|
await expect(page.locator('.task-row').filter({ hasText: dueTitle }).locator('.task-tail')).toBeVisible()
|
|
await expect(page.locator('.task-row').filter({ hasText: parentTitle })).toContainText('0/1')
|
|
|
|
for (const viewport of [{ width: 390, height: 844 }, { width: 375, height: 667 }]) {
|
|
await page.setViewportSize(viewport)
|
|
for (const title of titles) {
|
|
await expectTaskRowGeometry(page.locator('.task-row').filter({ hasText: title }))
|
|
}
|
|
const pageMetrics = await page.evaluate(() => ({ clientWidth: document.documentElement.clientWidth, scrollWidth: document.documentElement.scrollWidth }))
|
|
expect(pageMetrics.scrollWidth).toBeLessThanOrEqual(pageMetrics.clientWidth)
|
|
}
|
|
})
|