Files
dodo/frontend/e2e/mobile-journeys.spec.ts
T
bboysoul 66589dd458
ci / gitleaks (push) Successful in 10s
ci / docker (push) Successful in 7m27s
feat: simplify today into plain list
2026-09-17 18:04:40 +08:00

156 lines
7.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { Locator, Page } from '@playwright/test'
import { allowExpectedError, expect, test } from './fixtures'
async function bottomTab(page: Page, name: string) {
return page.getByRole('navigation', { name: '主要导航' }).getByRole('button', { name, exact: true })
}
function taskRow(page: Page, title: string) {
return page.locator('.task-row').filter({ has: page.locator('strong', { hasText: title }) })
}
async function assertInsideViewport(locator: Locator, page: Page) {
const box = await locator.boundingBox()
const viewport = page.viewportSize()
expect(box).not.toBeNull()
expect(viewport).not.toBeNull()
expect(box!.x).toBeGreaterThanOrEqual(0)
expect(box!.y).toBeGreaterThanOrEqual(0)
expect(box!.x + box!.width).toBeLessThanOrEqual(viewport!.width + 1)
expect(box!.y + box!.height).toBeLessThanOrEqual(viewport!.height + 1)
}
test('Today plain-list composition has no overlap or horizontal overflow', async ({ page }) => {
await page.goto('/')
await expect(await bottomTab(page, '今天')).toHaveAttribute('aria-current', 'page')
const context = page.locator('.today-context')
const environment = page.locator('.today-environment')
const title = page.locator('.today-page-title')
const remaining = page.locator('.today-remaining')
const filter = page.locator('.today-inline-filter')
const firstSection = page.locator('.today-section-toggle').first()
for (const locator of [context, environment, title, remaining, filter, firstSection]) {
await expect(locator).toBeVisible()
}
const geometry = await page.locator('main').evaluate((main) => {
const selectors = ['.today-environment', '.today-page-title', '.today-remaining', '.today-inline-filter', '.today-section-toggle']
const rects = selectors.map(selector => main.querySelector(selector)!.getBoundingClientRect())
const context = main.querySelector('.today-context')!
const contextRect = context.getBoundingClientRect()
return {
mainScrollWidth: main.scrollWidth,
mainClientWidth: main.clientWidth,
contextScrollWidth: context.scrollWidth,
contextClientWidth: context.clientWidth,
contextLeft: contextRect.left,
contextRight: contextRect.right,
rects: rects.map(rect => ({ left: rect.left, right: rect.right, top: rect.top, bottom: rect.bottom, width: rect.width, height: rect.height })),
}
})
expect(geometry.mainScrollWidth).toBeLessThanOrEqual(geometry.mainClientWidth)
expect(geometry.contextScrollWidth).toBeLessThanOrEqual(geometry.contextClientWidth)
for (const rect of geometry.rects) {
expect(rect.width).toBeGreaterThan(0)
expect(rect.height).toBeGreaterThan(0)
expect(rect.left).toBeGreaterThanOrEqual(geometry.contextLeft - 1)
expect(rect.right).toBeLessThanOrEqual(geometry.contextRight + 1)
}
for (let index = 0; index < geometry.rects.length - 1; index += 1) {
expect(geometry.rects[index].bottom).toBeLessThanOrEqual(geometry.rects[index + 1].top + 1)
}
})
test('Today task persists through detail, completion and reopen', async ({ page }, testInfo) => {
const title = `E2E 今日任务 ${testInfo.project.name}`
await page.goto('/')
await expect(await bottomTab(page, '今天')).toHaveAttribute('aria-current', 'page')
await page.getByRole('button', { name: '添加任务' }).click()
await page.getByLabel('任务名称').fill(title)
await page.getByRole('button', { name: '添加任务', exact: true }).click()
const row = taskRow(page, title)
await expect(row).toHaveCount(1)
const rowMain = row.locator('.task-main')
await rowMain.click()
const detail = page.getByRole('dialog', { name: '任务详情' })
await expect(detail).toBeVisible()
await assertInsideViewport(detail, page)
await page.getByRole('button', { name: '关闭详情' }).click()
await expect(rowMain).toBeFocused()
await row.getByRole('button', { name: `完成${title}` }).click()
await expect(row).toHaveClass(/done/)
await page.reload()
const persisted = taskRow(page, title)
await expect(persisted).toHaveCount(1)
await expect(persisted.getByRole('button', { name: `重新打开${title}` })).toBeVisible()
await persisted.getByRole('button', { name: `重新打开${title}` }).click()
await expect(persisted.getByRole('button', { name: `完成${title}` })).toBeVisible()
})
test('numeric habit records history, edits target, and continues', async ({ page }, testInfo) => {
const name = `E2E 数量习惯 ${testInfo.project.name}`
await page.goto('/')
await (await bottomTab(page, '习惯')).click()
await expect(await bottomTab(page, '习惯')).toHaveAttribute('aria-current', 'page')
await page.getByRole('button', { name: '添加习惯' }).click()
await page.getByLabel('新习惯名称').fill(name)
await page.getByLabel('习惯类型').selectOption('numeric')
await page.getByLabel('目标值').fill('2')
await page.getByRole('button', { name: '添加习惯', exact: true }).click()
const row = page.locator('.habit-row').filter({ hasText: name })
await expect(row).toHaveCount(1)
const check = row.getByRole('button', { name: `完成${name}一次` })
await check.click()
await expect(row).toContainText('1 / 2')
await check.click()
await expect(row).toContainText('2 / 2')
await row.getByRole('button', { name: `查看习惯详情:${name}` }).click()
const detail = page.getByRole('dialog', { name: name })
await expect(detail.getByRole('heading', { name: '历史记录' })).toBeVisible()
await expect(detail.locator('.habit-history__row')).toContainText('2 / 2')
await detail.getByRole('button', { name: '编辑习惯' }).click()
await page.getByLabel('目标值').fill('3')
await page.getByRole('button', { name: '保存修改' }).click()
await expect(row).toContainText('2 / 3')
await row.getByRole('button', { name: `完成${name}一次` }).click()
await expect(row).toContainText('3 / 3')
await page.reload()
const persistedRow = page.locator('.habit-row').filter({ hasText: name })
await expect(persistedRow).toContainText('3 / 3')
await persistedRow.getByRole('button', { name: `查看习惯详情:${name}` }).click()
const persistedDetail = page.getByRole('dialog', { name })
await expect(persistedDetail.locator('.habit-history__row')).toContainText('3 / 3')
})
test('countdown archive and restore keeps one entity after refresh', async ({ page }, testInfo) => {
const title = `E2E 倒数日 ${testInfo.project.name}`
await page.goto('/')
await (await bottomTab(page, '倒数日')).click()
await page.getByRole('button', { name: '添加倒数日' }).click()
await page.getByLabel('倒数日名称').fill(title)
await page.getByRole('button', { name: '保存', exact: true }).click()
const item = page.getByRole('button').filter({ hasText: title })
await expect(item).toHaveCount(1)
await item.click()
const detail = page.getByRole('dialog', { name: title })
await expect(detail).toBeVisible()
// The UI intentionally aborts its DELETE fetch after the 204 response while closing the detail sheet.
allowExpectedError(page, 'requestfailed: DELETE http://127.0.0.1:5173/api/v1/countdowns/')
await Promise.all([
page.waitForResponse(response => response.url().includes(`/api/v1/countdowns/`) && response.request().method() === 'DELETE' && response.status() === 204),
detail.getByRole('button', { name: '归档' }).click(),
])
await page.getByRole('button', { name: /已归档(1/ }).click()
const archived = page.locator('.archived-countdowns article').filter({ hasText: title })
await expect(archived).toHaveCount(1)
await archived.getByRole('button', { name: '恢复' }).click()
await page.reload()
await expect(page.getByText(title, { exact: true })).toHaveCount(1)
})