feat: add local pomodoro focus timer
Frontend-only 25m focus / 5m break timer with pause-resume-reset, deadline-based absolute timing, versioned localStorage persistence, cross-tab conflict protection, midnight-completion attribution, accessible stage controls, and reduced-motion support. Adds 专注 entry to desktop sidebar; mobile bottom nav unchanged; no notifications, task association, history, or cross-device sync. Also fixes mobile E2E task isolation and the stale calendar 390px overflow assertion.
This commit is contained in:
@@ -223,7 +223,6 @@ test('calendar week focus keeps seven usable day controls without page overflow'
|
||||
mainOverflow: main.scrollWidth - main.clientWidth,
|
||||
calendarOverflow: calendar.scrollWidth - calendar.clientWidth,
|
||||
filters: { clientWidth: filters.clientWidth, scrollWidth: filters.scrollWidth, overflowX: getComputedStyle(filters).overflowX },
|
||||
stripOverflow: element.scrollWidth > element.clientWidth,
|
||||
buttons: [...element.querySelectorAll<HTMLElement>('.calendar-week-day')].map(button => ({ width: button.getBoundingClientRect().width, height: button.getBoundingClientRect().height })),
|
||||
}
|
||||
})
|
||||
@@ -240,7 +239,6 @@ test('calendar week focus keeps seven usable day controls without page overflow'
|
||||
return { background: style.backgroundColor, color: style.color }
|
||||
})
|
||||
expect(selectedColors).toEqual({ background: 'rgb(241, 90, 41)', color: 'rgb(255, 255, 255)' })
|
||||
if ((await page.viewportSize())!.width <= 390) expect(metrics.stripOverflow).toBeTruthy()
|
||||
})
|
||||
|
||||
test('settings match the approved paper-ledger geometry and action hierarchy', async ({ page }) => {
|
||||
|
||||
@@ -14,26 +14,34 @@ async function createTask(request: APIRequestContext, baseURL: string, listId: s
|
||||
expect(response.ok(), await response.text()).toBeTruthy()
|
||||
}
|
||||
|
||||
async function openInbox(page: Page) {
|
||||
const inbox = page.locator('.sidebar').getByRole('button', { name: '收集箱', exact: true })
|
||||
async function createList(request: APIRequestContext, baseURL: string, name: string) {
|
||||
const response = await request.post('/api/v1/lists', {
|
||||
data: { name },
|
||||
headers: { 'x-csrf-token': await csrf(request), origin: baseURL },
|
||||
})
|
||||
expect(response.ok(), await response.text()).toBeTruthy()
|
||||
return response.json() as Promise<{ id: string }>
|
||||
}
|
||||
|
||||
async function openList(page: Page, name: string) {
|
||||
const list = page.locator('.sidebar').getByRole('button', { name, exact: true })
|
||||
if (await page.evaluate(() => window.innerWidth <= 930)) {
|
||||
await page.locator('main .topbar > button').first().click()
|
||||
}
|
||||
await inbox.click()
|
||||
await list.click()
|
||||
}
|
||||
|
||||
test('task pagination stays below the list and returns to the list start after navigation', async ({ page, request, baseURL }) => {
|
||||
const bootstrapResponse = await request.get('/api/v1/bootstrap')
|
||||
expect(bootstrapResponse.ok()).toBeTruthy()
|
||||
const inbox = (await bootstrapResponse.json()).lists.find((item: { is_inbox: boolean }) => item.is_inbox)
|
||||
expect(inbox).toBeTruthy()
|
||||
const nonce = crypto.randomUUID()
|
||||
const listName = `分页验收清单 ${nonce}`
|
||||
const list = await createList(request, baseURL!, listName)
|
||||
|
||||
for (let index = 1; index <= 51; index += 1) {
|
||||
await createTask(request, baseURL!, inbox.id, `分页验收任务 ${String(index).padStart(2, '0')}`)
|
||||
await createTask(request, baseURL!, list.id, `分页验收任务 ${nonce} ${String(index).padStart(2, '0')}`)
|
||||
}
|
||||
|
||||
await page.goto('/')
|
||||
await openInbox(page)
|
||||
await openList(page, listName)
|
||||
|
||||
const taskList = page.locator('.task-list')
|
||||
const pager = page.locator('.pager')
|
||||
|
||||
@@ -14,27 +14,34 @@ async function createTask(request: APIRequestContext, baseURL: string, title: st
|
||||
expect(response.ok(), await response.text()).toBeTruthy()
|
||||
}
|
||||
|
||||
async function openInbox(page: Page) {
|
||||
async function createList(request: APIRequestContext, baseURL: string, name: string) {
|
||||
const response = await request.post('/api/v1/lists', {
|
||||
data: { name },
|
||||
headers: { 'x-csrf-token': await csrf(request), origin: baseURL },
|
||||
})
|
||||
expect(response.ok(), await response.text()).toBeTruthy()
|
||||
return response.json() as Promise<{ id: string }>
|
||||
}
|
||||
|
||||
async function openList(page: Page, name: string) {
|
||||
if ((await page.viewportSize())!.width <= 930) {
|
||||
await page.locator('.topbar').getByRole('button', { name: /展开菜单|收起菜单/ }).click()
|
||||
}
|
||||
await page.locator('.sidebar').getByRole('button', { name: '收集箱', exact: true }).click()
|
||||
await page.locator('.sidebar').getByRole('button', { name, exact: true }).click()
|
||||
}
|
||||
|
||||
test('inbox and task composer match approved visuals', async ({ page, request, baseURL }, testInfo) => {
|
||||
test.skip(testInfo.project.name !== 'mobile-390', 'approved visual baselines are maintained for mobile-390 only')
|
||||
await page.clock.install({ time: new Date('2026-09-19T05:00:00.000Z') })
|
||||
|
||||
const bootstrap = await request.get('/api/v1/bootstrap')
|
||||
expect(bootstrap.ok()).toBeTruthy()
|
||||
const inbox = (await bootstrap.json()).lists.find((item: { is_inbox: boolean }) => item.is_inbox)
|
||||
expect(inbox).toBeTruthy()
|
||||
const listName = '视觉回归清单'
|
||||
const list = await createList(request, baseURL!, listName)
|
||||
|
||||
const seededTitles = ['整理本周工作记录', '检查 Dodo 备份', '周末买水果和牛奶'] as const
|
||||
for (const title of seededTitles) await createTask(request, baseURL!, title, inbox.id)
|
||||
for (const title of seededTitles) await createTask(request, baseURL!, title, list.id)
|
||||
|
||||
await page.goto('/')
|
||||
await openInbox(page)
|
||||
await openList(page, listName)
|
||||
const seededRows = page.locator('.task-row').filter({ hasText: /整理本周工作记录|检查 Dodo 备份|周末买水果和牛奶/ })
|
||||
await expect(seededRows).toHaveCount(3)
|
||||
await expect(page).toHaveScreenshot('inbox.png', { fullPage: true })
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 43 KiB |
Reference in New Issue
Block a user