feat: redesign settings as paper ledger
This commit is contained in:
@@ -5,6 +5,21 @@ function bottomTab(page: Page, name: string) {
|
||||
return page.getByRole('navigation', { name: '主要导航' }).getByRole('button', { name, exact: true })
|
||||
}
|
||||
|
||||
async function openSettings(page: Page) {
|
||||
const mobileTab = bottomTab(page, '设置')
|
||||
if (await mobileTab.isVisible()) {
|
||||
await mobileTab.click()
|
||||
return
|
||||
}
|
||||
const desktopSettings = page.getByRole('navigation', { name: '管理' }).getByRole('button', { name: '设置', exact: true })
|
||||
const box = await desktopSettings.boundingBox()
|
||||
if (box && box.x + box.width > 0 && box.y + box.height > 0 && box.x < (await page.viewportSize())!.width) await desktopSettings.click()
|
||||
else {
|
||||
await page.getByRole('button', { name: /^(展开|收起)菜单$/ }).click()
|
||||
await desktopSettings.click()
|
||||
}
|
||||
}
|
||||
|
||||
async function openTaskComposer(page: Page) {
|
||||
await page.getByRole('button', { name: '添加任务' }).click()
|
||||
return page.getByRole('dialog', { name: /添加(?:今天)?任务/ })
|
||||
@@ -133,29 +148,57 @@ test('all bottom destinations expose one active page and desktop layout stays un
|
||||
expect(desktopBottomGap).toBe(84)
|
||||
})
|
||||
|
||||
test('settings are continuous, fit viewport, and controls are touch sized', async ({ page }) => {
|
||||
test('settings match the approved paper-ledger geometry and action hierarchy', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await bottomTab(page, '设置').click()
|
||||
await expect(bottomTab(page, '设置')).toHaveAttribute('aria-current', 'page')
|
||||
await openSettings(page)
|
||||
if ((await page.viewportSize())!.width <= 720) 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')]
|
||||
const page = element as HTMLElement
|
||||
const main = page.closest('main') as HTMLElement
|
||||
const heading = page.querySelector<HTMLElement>('.settings-heading h1')!
|
||||
const sectionHeaders = [...page.querySelectorAll<HTMLElement>('.settings-group > header')]
|
||||
const rows = [...page.querySelectorAll<HTMLElement>('.settings-row')]
|
||||
const menu = main.querySelector<HTMLElement>('.topbar > .icon')!.getBoundingClientRect()
|
||||
const refresh = main.querySelector<HTMLElement>('.settings-refresh')!.getBoundingClientRect()
|
||||
const rect = page.getBoundingClientRect()
|
||||
const mainRect = main.getBoundingClientRect()
|
||||
return {
|
||||
bodyOverflow: document.documentElement.scrollWidth - document.documentElement.clientWidth,
|
||||
gaps: groups.slice(1).map((group, index) => group.getBoundingClientRect().top - groups[index].getBoundingClientRect().bottom),
|
||||
leftPadding: rect.left - mainRect.left,
|
||||
rightPadding: mainRect.right - rect.right,
|
||||
headingSize: getComputedStyle(heading).fontSize,
|
||||
sectionHeights: sectionHeaders.map(header => header.getBoundingClientRect().height),
|
||||
rowHeights: rows.map(row => row.getBoundingClientRect().height),
|
||||
controlsOverlap: !(menu.right <= refresh.left || refresh.right <= menu.left || menu.bottom <= refresh.top || refresh.bottom <= menu.top),
|
||||
}
|
||||
})
|
||||
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)
|
||||
const isMobileContract = (await page.viewportSize())!.width <= 720
|
||||
if (isMobileContract) {
|
||||
expect(layout.leftPadding).toBeCloseTo(29, 0)
|
||||
expect(layout.rightPadding).toBeCloseTo(29, 0)
|
||||
expect(layout.headingSize).toBe('24px')
|
||||
} else {
|
||||
const pageWidth = await page.locator('.settings-sections').evaluate(element => element.getBoundingClientRect().width)
|
||||
expect(pageWidth).toBeLessThanOrEqual(900)
|
||||
expect(layout.leftPadding).toBeCloseTo(layout.rightPadding, 0)
|
||||
expect(layout.headingSize).toBe('34px')
|
||||
}
|
||||
for (const target of await sessionButtons.all()) {
|
||||
expect(layout.sectionHeights.every(height => height === 44)).toBeTruthy()
|
||||
expect(layout.rowHeights.every(height => height >= (isMobileContract ? 64 : 62))).toBeTruthy()
|
||||
expect(layout.controlsOverlap).toBe(false)
|
||||
|
||||
const exportButton = page.getByRole('button', { name: '导出 ZIP' })
|
||||
const fileInput = page.locator('input[type=file]')
|
||||
await fileInput.setInputFiles({ name: 'settings-geometry.zip', mimeType: 'application/zip', buffer: Buffer.from('zip-placeholder') })
|
||||
const preflightButton = page.getByRole('button', { name: '开始预检' })
|
||||
const actionColors = await Promise.all([exportButton, preflightButton].map(control => control.evaluate(element => ({ background: getComputedStyle(element).backgroundColor, color: getComputedStyle(element).color }))))
|
||||
expect(actionColors[0]).toEqual({ background: 'rgb(241, 90, 41)', color: 'rgb(255, 255, 255)' })
|
||||
expect(actionColors[1].background).not.toBe('rgb(241, 90, 41)')
|
||||
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user