119 lines
5.8 KiB
TypeScript
119 lines
5.8 KiB
TypeScript
import type { Page } from '@playwright/test'
|
|
import { expect, test } from './fixtures'
|
|
|
|
function bottomTab(page: Page, name: string) {
|
|
return page.getByRole('navigation', { name: '主要导航' }).getByRole('button', { name, exact: true })
|
|
}
|
|
|
|
async function openTaskComposer(page: Page) {
|
|
await page.getByRole('button', { name: '添加任务' }).click()
|
|
return page.getByRole('dialog', { name: /添加(?:今天)?任务/ })
|
|
}
|
|
|
|
test('settings are continuous, fit viewport, and controls are touch sized', async ({ page }) => {
|
|
await page.goto('/')
|
|
await bottomTab(page, '设置').click()
|
|
await expect(bottomTab(page, '设置')).toHaveAttribute('aria-current', 'page')
|
|
const groups = page.locator('.settings-group')
|
|
await expect(groups).toHaveCount(4)
|
|
const layout = await page.locator('.settings-sections').evaluate(element => {
|
|
const groups = [...element.querySelectorAll<HTMLElement>(':scope > .settings-group')]
|
|
return {
|
|
bodyOverflow: document.documentElement.scrollWidth - document.documentElement.clientWidth,
|
|
gaps: groups.slice(1).map((group, index) => group.getBoundingClientRect().top - groups[index].getBoundingClientRect().bottom),
|
|
}
|
|
})
|
|
expect(layout.bodyOverflow).toBe(0)
|
|
expect(layout.gaps.every(gap => gap >= 0 && gap <= 20)).toBeTruthy()
|
|
const sessionButtons = page.getByRole('button', { name: /撤销会话|撤销其他会话/ })
|
|
for (const target of await page.locator('.settings-row button, .settings-row .file-button, .settings-row select').all()) {
|
|
const box = await target.boundingBox()
|
|
expect(box).not.toBeNull()
|
|
expect(box!.width).toBeGreaterThanOrEqual(44)
|
|
expect(box!.height).toBeGreaterThanOrEqual(44)
|
|
}
|
|
for (const target of await sessionButtons.all()) {
|
|
const box = await target.boundingBox()
|
|
expect(box).not.toBeNull()
|
|
expect(box!.width).toBeGreaterThanOrEqual(44)
|
|
expect(box!.height).toBeGreaterThanOrEqual(44)
|
|
}
|
|
})
|
|
|
|
test('task, habit, countdown, memo, action and confirmation overlays share the modal contract', async ({ page }, testInfo) => {
|
|
await page.goto('/')
|
|
const assertModal = async (dialog: ReturnType<Page['getByRole']>) => {
|
|
await expect(dialog).toBeVisible()
|
|
await expect(page.locator('#app')).toHaveAttribute('inert', '')
|
|
const metrics = await dialog.evaluate(element => {
|
|
const panel = element as HTMLElement
|
|
const header = panel.querySelector<HTMLElement>('.app-sheet__header, header')
|
|
const footer = panel.querySelector<HTMLElement>('.app-sheet__footer, footer')
|
|
return {
|
|
horizontalOverflow: panel.scrollWidth - panel.clientWidth,
|
|
headerVisible: !header || header.getBoundingClientRect().top >= 0,
|
|
footerVisible: !footer || footer.getBoundingClientRect().bottom <= innerHeight + 1,
|
|
}
|
|
})
|
|
expect(metrics).toEqual({ horizontalOverflow: 0, headerVisible: true, footerVisible: true })
|
|
}
|
|
|
|
await openTaskComposer(page)
|
|
await assertModal(page.getByRole('dialog', { name: /添加(?:今天)?任务/ }))
|
|
await page.keyboard.press('Escape')
|
|
await bottomTab(page, '习惯').click()
|
|
await page.getByRole('button', { name: '添加习惯' }).click()
|
|
await assertModal(page.getByRole('dialog', { name: '添加习惯' }))
|
|
await page.keyboard.press('Escape')
|
|
await bottomTab(page, '倒数日').click()
|
|
await page.getByRole('button', { name: '添加倒数日' }).click()
|
|
await assertModal(page.getByRole('dialog', { name: '新建倒数日' }))
|
|
await page.keyboard.press('Escape')
|
|
|
|
await page.getByRole('button', { name: /展开菜单|收起菜单/ }).click()
|
|
await page.locator('.sidebar').getByRole('button', { name: '备忘录', exact: true }).click()
|
|
await page.getByRole('button', { name: '添加备忘录' }).click()
|
|
await assertModal(page.getByRole('dialog', { name: '备忘录详情' }))
|
|
await page.getByLabel('备忘录标题').fill(`未保存 ${testInfo.project.name}`)
|
|
await page.getByRole('button', { name: '关闭备忘录' }).click()
|
|
const confirmation = page.getByRole('dialog', { name: '放弃未保存的更改?' })
|
|
await assertModal(confirmation)
|
|
await confirmation.getByRole('button', { name: '取消' }).click()
|
|
await expect(page.getByRole('dialog', { name: '备忘录详情' })).toBeVisible()
|
|
await expect(page.getByLabel('备忘录标题')).toBeFocused()
|
|
})
|
|
|
|
test('AppSheet traps focus, Escape closes, and scrim owns outside hit testing', async ({ page }) => {
|
|
await page.goto('/')
|
|
const opener = page.getByRole('button', { name: '添加任务' })
|
|
await opener.focus()
|
|
const dialog = await openTaskComposer(page)
|
|
await expect(dialog).toBeVisible()
|
|
await expect(page.getByLabel('任务名称')).toBeFocused()
|
|
const background = page.locator('#app')
|
|
await expect(background).toHaveAttribute('aria-hidden', 'true')
|
|
await expect(background).toHaveAttribute('inert', '')
|
|
const geometry = await page.locator('.app-overlay').evaluate(element => {
|
|
const rect = element.getBoundingClientRect()
|
|
const hit = document.elementFromPoint(2, 2)
|
|
return { x: rect.x, y: rect.y, width: rect.width, height: rect.height, hitIsScrim: hit === element }
|
|
})
|
|
expect(geometry).toEqual({ x: 0, y: 0, width: page.viewportSize()!.width, height: page.viewportSize()!.height, hitIsScrim: true })
|
|
|
|
const focusable = dialog.locator('button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [href], [tabindex]:not([tabindex="-1"])').filter({ visible: true })
|
|
const first = focusable.first()
|
|
const last = focusable.last()
|
|
await first.focus()
|
|
await page.keyboard.press('Shift+Tab')
|
|
await expect(last).toBeFocused()
|
|
await page.keyboard.press('Tab')
|
|
await expect(first).toBeFocused()
|
|
await page.keyboard.press('Escape')
|
|
await expect(dialog).toBeHidden()
|
|
await expect(opener).toBeFocused()
|
|
|
|
await openTaskComposer(page)
|
|
await page.mouse.click(2, 2)
|
|
await expect(page.getByRole('dialog', { name: /添加(?:今天)?任务/ })).toBeHidden()
|
|
})
|