feat: add paper-style mobile navigation
This commit is contained in:
@@ -10,6 +10,129 @@ async function openTaskComposer(page: Page) {
|
||||
return page.getByRole('dialog', { name: /添加(?:今天)?任务/ })
|
||||
}
|
||||
|
||||
test('bottom navigation uses the paper baseline geometry', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const metrics = await page.locator('.bottom').evaluate(element => {
|
||||
const nav = element as HTMLElement
|
||||
const rect = nav.getBoundingClientRect()
|
||||
const buttons = [...nav.querySelectorAll<HTMLElement>('button')]
|
||||
const active = nav.querySelector<HTMLElement>('button.active')!
|
||||
const activeStyle = getComputedStyle(active)
|
||||
const marker = getComputedStyle(active, '::before')
|
||||
const fab = document.querySelector<HTMLElement>('.unified-fab')!
|
||||
const fabRect = fab.getBoundingClientRect()
|
||||
return {
|
||||
viewportWidth: innerWidth,
|
||||
documentOverflow: document.documentElement.scrollWidth - document.documentElement.clientWidth,
|
||||
nav: { left: rect.left, right: rect.right, bottom: rect.bottom, width: rect.width, height: rect.height },
|
||||
buttonWidths: buttons.map(button => button.getBoundingClientRect().width),
|
||||
buttonHeights: buttons.map(button => button.getBoundingClientRect().height),
|
||||
background: getComputedStyle(nav).backgroundColor,
|
||||
borderTopWidth: getComputedStyle(nav).borderTopWidth,
|
||||
borderRadius: getComputedStyle(nav).borderRadius,
|
||||
boxShadow: getComputedStyle(nav).boxShadow,
|
||||
activeBackground: activeStyle.backgroundColor,
|
||||
activeColor: activeStyle.color,
|
||||
marker: { height: marker.height, background: marker.backgroundColor },
|
||||
fabGap: rect.top - fabRect.bottom,
|
||||
}
|
||||
})
|
||||
expect(metrics.documentOverflow).toBe(0)
|
||||
expect(metrics.nav).toEqual({ left: 0, right: metrics.viewportWidth, bottom: page.viewportSize()!.height, width: metrics.viewportWidth, height: 64 })
|
||||
expect(Math.max(...metrics.buttonWidths) - Math.min(...metrics.buttonWidths)).toBeLessThanOrEqual(1)
|
||||
expect(metrics.buttonHeights.every(height => height >= 44)).toBeTruthy()
|
||||
expect(metrics.background).toBe('rgb(255, 253, 248)')
|
||||
expect(metrics.borderTopWidth).toBe('1px')
|
||||
expect(metrics.borderRadius).toBe('0px')
|
||||
expect(metrics.boxShadow).toBe('none')
|
||||
expect(metrics.activeBackground).toBe('rgba(0, 0, 0, 0)')
|
||||
expect(metrics.activeColor).toBe('rgb(241, 90, 41)')
|
||||
expect(metrics.marker).toEqual({ height: '3px', background: 'rgb(241, 90, 41)' })
|
||||
expect(metrics.fabGap).toBeGreaterThanOrEqual(12)
|
||||
|
||||
const fab = page.locator('.unified-fab')
|
||||
const fabBox = await fab.boundingBox()
|
||||
expect(fabBox).not.toBeNull()
|
||||
await page.mouse.move(fabBox!.x + fabBox!.width / 2, fabBox!.y + fabBox!.height / 2)
|
||||
await page.mouse.down()
|
||||
await page.mouse.move(fabBox!.x + fabBox!.width / 2, page.viewportSize()!.height - 1, { steps: 8 })
|
||||
await page.mouse.up()
|
||||
const draggedGap = await page.evaluate(() => {
|
||||
const nav = document.querySelector<HTMLElement>('.bottom')!.getBoundingClientRect()
|
||||
const button = document.querySelector<HTMLElement>('.unified-fab')!.getBoundingClientRect()
|
||||
return nav.top - button.bottom
|
||||
})
|
||||
expect(draggedGap).toBeGreaterThanOrEqual(12)
|
||||
})
|
||||
|
||||
test('bottom navigation keeps its safe-area gap after dragging', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const nav = page.locator('.bottom')
|
||||
await expect(nav).toBeVisible()
|
||||
await page.locator('html').evaluate(element => element.style.setProperty('--safe-area-bottom', '34px', 'important'))
|
||||
await expect.poll(() => nav.evaluate(element => getComputedStyle(element).height)).toBe('98px')
|
||||
|
||||
const navBox = await nav.boundingBox()
|
||||
expect(navBox).not.toBeNull()
|
||||
expect(navBox!.height).toBe(98)
|
||||
|
||||
const fab = page.locator('.unified-fab')
|
||||
const fabBox = await fab.boundingBox()
|
||||
expect(fabBox).not.toBeNull()
|
||||
await page.mouse.move(fabBox!.x + fabBox!.width / 2, fabBox!.y + fabBox!.height / 2)
|
||||
await page.mouse.down()
|
||||
await page.mouse.move(fabBox!.x + fabBox!.width / 2, page.viewportSize()!.height - 1, { steps: 8 })
|
||||
await page.mouse.up()
|
||||
|
||||
const gap = await page.evaluate(() => {
|
||||
const navigation = document.querySelector<HTMLElement>('.bottom')!.getBoundingClientRect()
|
||||
const button = document.querySelector<HTMLElement>('.unified-fab')!.getBoundingClientRect()
|
||||
return navigation.top - button.bottom
|
||||
})
|
||||
expect(gap).toBeGreaterThanOrEqual(12)
|
||||
})
|
||||
|
||||
test('all bottom destinations expose one active page and desktop layout stays unchanged', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const navigation = page.getByRole('navigation', { name: '主要导航' })
|
||||
for (const label of ['今天', '习惯', '倒数日', '设置']) {
|
||||
await bottomTab(page, label).click()
|
||||
await expect(navigation.locator('[aria-current="page"]')).toHaveCount(1)
|
||||
await expect(bottomTab(page, label)).toHaveAttribute('aria-current', 'page')
|
||||
}
|
||||
|
||||
await bottomTab(page, '今天').click()
|
||||
await page.setViewportSize({ width: 1440, height: 900 })
|
||||
const desktop = await page.locator('.shell').evaluate(element => {
|
||||
const shell = getComputedStyle(element)
|
||||
const navigation = document.querySelector<HTMLElement>('.bottom')!
|
||||
const navStyle = getComputedStyle(navigation)
|
||||
const fab = document.querySelector<HTMLElement>('.unified-fab')!
|
||||
const fabStyle = getComputedStyle(fab)
|
||||
const fabRect = fab.getBoundingClientRect()
|
||||
return {
|
||||
shellDisplay: shell.display,
|
||||
shellColumns: shell.gridTemplateColumns,
|
||||
navDisplay: navStyle.display,
|
||||
fab: { position: fabStyle.position, width: fabRect.width, height: fabRect.height },
|
||||
}
|
||||
})
|
||||
expect(desktop.shellDisplay).toBe('grid')
|
||||
expect(desktop.shellColumns.split(' ').length).toBe(3)
|
||||
expect(desktop.navDisplay).toBe('none')
|
||||
expect(desktop.fab).toEqual({ position: 'fixed', width: 56, height: 56 })
|
||||
|
||||
const desktopFab = page.locator('.unified-fab')
|
||||
const desktopFabBox = await desktopFab.boundingBox()
|
||||
expect(desktopFabBox).not.toBeNull()
|
||||
await page.mouse.move(desktopFabBox!.x + desktopFabBox!.width / 2, desktopFabBox!.y + desktopFabBox!.height / 2)
|
||||
await page.mouse.down()
|
||||
await page.mouse.move(desktopFabBox!.x + desktopFabBox!.width / 2, 899, { steps: 8 })
|
||||
await page.mouse.up()
|
||||
const desktopBottomGap = await desktopFab.evaluate(element => innerHeight - element.getBoundingClientRect().bottom)
|
||||
expect(desktopBottomGap).toBe(84)
|
||||
})
|
||||
|
||||
test('settings are continuous, fit viewport, and controls are touch sized', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await bottomTab(page, '设置').click()
|
||||
|
||||
Reference in New Issue
Block a user