refactor: unify task and habit list rows
ci / gitleaks (push) Successful in 10s
ci / docker (push) Successful in 3m45s

This commit is contained in:
2026-09-18 06:25:13 +08:00
parent e4e1d96b30
commit a4c1ac5978
8 changed files with 219 additions and 108 deletions
+77 -4
View File
@@ -10,6 +10,24 @@ async function mutate(request: APIRequestContext, baseURL: string, path: string,
return request.fetch(path, { ...options, headers: { ...options?.headers, 'x-csrf-token': await csrf(request), origin: baseURL } })
}
async function createHabit(request: APIRequestContext, baseURL: string, name: string, extra: Record<string, unknown> = {}) {
const response = await mutate(request, baseURL, '/api/v1/habits', {
method: 'POST',
data: { name, kind: 'boolean', schedule_type: 'daily', ...extra },
})
expect(response.ok(), await response.text()).toBeTruthy()
return response.json() as Promise<{ id: string }>
}
function shanghaiToday() {
const parts = new Intl.DateTimeFormat('en-CA', {
timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit', weekday: 'short',
}).formatToParts(new Date())
const value = (type: Intl.DateTimeFormatPartTypes) => parts.find(part => part.type === type)!.value
const weekdays: Record<string, number> = { Mon: 0, Tue: 1, Wed: 2, Thu: 3, Fri: 4, Sat: 5, Sun: 6 }
return { day: `${value('year')}-${value('month')}-${value('day')}`, weekday: weekdays[value('weekday')] }
}
function bottomTab(page: Page, name: string) {
return page.getByRole('navigation', { name: '主要导航' }).getByRole('button', { name, exact: true })
}
@@ -39,8 +57,55 @@ async function expectNotClipped(locator: Locator) {
expect(metrics.scrollHeight).toBeLessThanOrEqual(metrics.clientHeight)
}
test('boolean habits complete in place while paused and unscheduled rows explain why they are read-only', async ({ page, request, baseURL }, testInfo) => {
const suffix = `${testInfo.project.name}-${testInfo.workerIndex}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
const today = shanghaiToday()
const booleanName = `布尔习惯-${suffix}`
const pausedName = `暂停习惯-${suffix}`
const unscheduledName = `未安排习惯-${suffix}`
await createHabit(request, baseURL!, booleanName)
const paused = await createHabit(request, baseURL!, pausedName)
await createHabit(request, baseURL!, unscheduledName, { schedule_type: 'weekly', weekdays: [(today.weekday + 1) % 7] })
const pause = await mutate(request, baseURL!, `/api/v1/habits/${paused.id}/pauses`, {
method: 'POST', data: { start_date: today.day, end_date: today.day },
})
expect(pause.ok(), await pause.text()).toBeTruthy()
await page.goto('/')
await bottomTab(page, '习惯').click()
const booleanRow = page.locator('.habit-row').filter({ hasText: booleanName })
const booleanCheck = booleanRow.getByRole('button', { name: `完成${booleanName}一次` })
const checkGeometry = await booleanCheck.evaluate((element) => {
const control = element.getBoundingClientRect()
const row = element.closest('.habit-row')!.getBoundingClientRect()
return {
visibleWidth: control.width - Math.max(row.left - control.left, 0) - Math.max(control.right - row.right, 0),
height: control.height,
}
})
expect(checkGeometry.visibleWidth).toBeGreaterThanOrEqual(44)
expect(checkGeometry.height).toBeGreaterThanOrEqual(44)
await booleanCheck.click()
await expect(booleanRow).toHaveClass(/done/)
await expect(booleanRow.getByRole('button', { name: `减少${booleanName}一次` })).toHaveAttribute('aria-pressed', 'true')
await page.reload()
const persistedBooleanRow = page.locator('.habit-row').filter({ hasText: booleanName })
await expect(persistedBooleanRow).toHaveClass(/done/)
await persistedBooleanRow.locator('.habit-main').focus()
await persistedBooleanRow.locator('.habit-main').press('Enter')
await expect(page.getByRole('dialog', { name: booleanName })).toBeVisible()
await page.getByRole('button', { name: '关闭习惯详情' }).click()
const pausedRow = page.locator('.habit-row').filter({ hasText: pausedName })
await expect(pausedRow).toContainText('今天已暂停')
await expect(pausedRow.getByRole('button', { name: '今天已暂停' })).toBeDisabled()
const unscheduledRow = page.locator('.habit-row').filter({ hasText: unscheduledName })
await expect(unscheduledRow).toContainText('今天未安排')
await expect(unscheduledRow.getByRole('button', { name: '今天未安排' })).toBeDisabled()
})
test('approved polish keeps search state, dense rows, title-only memos, and unique detail titles', async ({ page, request, baseURL }, testInfo) => {
const suffix = testInfo.project.name
const suffix = `${testInfo.project.name}-${testInfo.workerIndex}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
const bootstrapResponse = await request.get('/api/v1/bootstrap')
expect(bootstrapResponse.ok()).toBeTruthy()
const inbox = (await bootstrapResponse.json()).lists.find((item: { is_inbox: boolean }) => item.is_inbox)
@@ -60,7 +125,7 @@ test('approved polish keeps search state, dense rows, title-only memos, and uniq
})
expect(memoResponse.ok(), await memoResponse.text()).toBeTruthy()
const day = new Date().toLocaleDateString('sv-SE')
const day = shanghaiToday().day
const countdownTitle = `紧凑倒数-${suffix}`
const countdownResponse = await mutate(request, baseURL!, '/api/v1/countdowns', {
method: 'POST',
@@ -78,8 +143,16 @@ test('approved polish keeps search state, dense rows, title-only memos, and uniq
await searchInput.fill(`搜索保留-${suffix}`)
const taskRow = page.locator('.task-row').filter({ hasText: taskTitle })
await expect(taskRow).toHaveCount(1)
await expectHeightInRange(taskRow, 64, 68)
await expectHeightInRange(taskRow, 57, 59)
await expectNotClipped(taskRow)
const transparent = 'rgba(0, 0, 0, 0)'
await expect(taskRow).toHaveCSS('background-color', transparent)
await taskRow.hover()
await expect(taskRow).toHaveCSS('background-color', transparent)
await taskRow.locator('.task-main').click()
await expect(taskRow).toHaveClass(/selected/)
await expect(taskRow).toHaveCSS('background-color', transparent)
await page.keyboard.press('Escape')
await searchInput.press('Escape')
const collapsedToggle = page.getByRole('button', { name: '展开搜索任务' })
await expect(collapsedToggle).toBeFocused()
@@ -96,7 +169,7 @@ test('approved polish keeps search state, dense rows, title-only memos, and uniq
await page.getByRole('button', { name: '添加习惯', exact: true }).click()
const habitRow = page.locator('.habit-row').filter({ hasText: habitName })
await expect(habitRow).toHaveCount(1)
await expectHeightInRange(habitRow, 84, 88)
await expectHeightInRange(habitRow, 57, 59)
await expectNotClipped(habitRow)
await habitRow.getByRole('button', { name: `查看习惯详情:${habitName}` }).click()
const habitDetail = page.getByRole('dialog', { name: habitName })