156 lines
7.6 KiB
TypeScript
156 lines
7.6 KiB
TypeScript
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)
|
||
})
|