feat: add local pomodoro focus timer
ci / gitleaks (push) Successful in 7s
ci / docker (push) Successful in 3m34s

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:
2026-09-22 09:10:05 +08:00
parent 12e4753aeb
commit a535f69cae
14 changed files with 513 additions and 25 deletions
+17 -9
View File
@@ -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')