151 lines
7.3 KiB
TypeScript
151 lines
7.3 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 paper composition has no overlap or horizontal overflow', async ({ page }) => {
|
||
await page.goto('/')
|
||
await expect(await bottomTab(page, '今天')).toHaveAttribute('aria-current', 'page')
|
||
const board = page.locator('.today-board')
|
||
const environment = page.locator('.today-environment')
|
||
const tracks = board.locator('.today-track')
|
||
await expect(board).toBeVisible()
|
||
await expect(environment).toBeVisible()
|
||
await expect(tracks).toHaveCount(2)
|
||
|
||
const geometry = await board.evaluate((node) => {
|
||
const boardRect = node.getBoundingClientRect()
|
||
const environmentRect = node.querySelector('.today-environment')!.getBoundingClientRect()
|
||
const trackRects = [...node.querySelectorAll('.today-track')].map(item => item.getBoundingClientRect())
|
||
return {
|
||
boardRight: boardRect.right,
|
||
boardBottom: boardRect.bottom,
|
||
boardScrollWidth: node.scrollWidth,
|
||
boardClientWidth: node.clientWidth,
|
||
environmentBottom: environmentRect.bottom,
|
||
trackTops: trackRects.map(rect => rect.top),
|
||
trackRights: trackRects.map(rect => rect.right),
|
||
trackBottoms: trackRects.map(rect => rect.bottom),
|
||
}
|
||
})
|
||
expect(geometry.boardScrollWidth).toBeLessThanOrEqual(geometry.boardClientWidth)
|
||
expect(Math.min(...geometry.trackTops)).toBeGreaterThanOrEqual(geometry.environmentBottom - 1)
|
||
expect(Math.max(...geometry.trackRights)).toBeLessThanOrEqual(geometry.boardRight + 1)
|
||
expect(Math.max(...geometry.trackBottoms)).toBeLessThanOrEqual(geometry.boardBottom + 1)
|
||
|
||
for (const track of await tracks.all()) {
|
||
const head = track.locator('.today-track-head')
|
||
const labels = head.locator('strong, span')
|
||
await expect(labels).toHaveCount(2)
|
||
const boxes = await labels.evaluateAll(nodes => nodes.map(node => node.getBoundingClientRect()))
|
||
expect(Math.abs(boxes[0].top - boxes[1].top)).toBeLessThan(4)
|
||
}
|
||
})
|
||
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)
|
||
})
|