feat: unify task and habit pages with Today
ci / gitleaks (push) Successful in 8s
ci / docker (push) Successful in 6m49s

This commit is contained in:
2026-09-18 10:51:57 +08:00
parent 6d568273ef
commit 978c4b87ed
9 changed files with 204 additions and 20 deletions
+86
View File
@@ -1,5 +1,6 @@
import { readFileSync } from 'node:fs'
import { describe, expect, it } from 'vitest'
import { loadTaskOpenTotal } from './lib/task-open-total'
const app = readFileSync('src/App.vue', 'utf8')
const countdown = readFileSync('src/CountdownPanel.vue', 'utf8')
@@ -9,6 +10,44 @@ const memoCss = readFileSync('src/memo.css', 'utf8')
const memoPanel = readFileSync('src/MemoPanel.vue', 'utf8')
const memoRow = readFileSync('src/components/MemoRow.vue', 'utf8')
describe('task-list open summary reconciliation', () => {
it('returns primary data without waiting for the open-total request', async () => {
let resolveSummary!: (value: { total: number }) => void
const summary = new Promise<{ total: number }>((resolve) => { resolveSummary = resolve })
const committed: Array<number | null> = []
const result = await loadTaskOpenTotal(
async () => summary,
() => true,
value => committed.push(value),
)
expect(result).toBeUndefined()
expect(committed).toEqual([])
resolveSummary({ total: 4 })
await summary
await Promise.resolve()
expect(committed).toEqual([4])
})
it('keeps null on failure and ignores stale list responses', async () => {
const failed: Array<number | null> = []
loadTaskOpenTotal(async () => { throw new Error('summary failed') }, () => true, value => failed.push(value))
await Promise.resolve()
await Promise.resolve()
expect(failed).toEqual([])
let current = true
let resolveSummary!: (value: { total: number }) => void
const summary = new Promise<{ total: number }>((resolve) => { resolveSummary = resolve })
const committed: Array<number | null> = []
loadTaskOpenTotal(async () => summary, () => current, value => committed.push(value))
current = false
resolveSummary({ total: 9 })
await summary
await Promise.resolve()
expect(committed).toEqual([])
})
})
describe('approved five-detail polish', () => {
it('uses an independent accessible task-search panel toggle', () => {
expect(app).toContain(":aria-label=\"mobileSearchOpen ? '收起搜索任务' : '展开搜索任务'\"")
@@ -34,6 +73,53 @@ describe('approved five-detail polish', () => {
expect(css).toMatch(/@media\(max-width:930px\)\{[\s\S]*?\.countdown-focus\{[^}]*height:140px/)
})
it('uses approved Today-sourced page headers for task lists without changing Trash IA', () => {
expect(app).toContain('v-if="activeView===\'tasks\'" class="list-page-context"')
expect(app).toContain('<h1 class="list-page-title" :title="activeName">{{ activeName }}</h1>')
expect(app).toContain('<p class="list-page-summary">{{ taskOpenTotal === null ? \'待完成统计暂不可用\' : `还有 ${taskOpenTotal} 项待完成` }}</p>')
expect(app).toContain('<CompletedFilterPill v-model="showCompleted" class="list-inline-filter" />')
expect(app).toContain('id="task-list-heading" class="list-section-heading"')
expect(app).toContain('<span id="task-list-title" class="list-section-title">任务</span>')
expect(app).toContain('<span class="list-section-count">{{ totalTasks }}</span>')
expect(app).toContain("v-if=\"activeView==='tasks' && totalPages > 1\" class=\"list-page-meta\"")
expect(app).toContain('v-if="query" class="list-search-clear"')
expect(app).not.toContain("activeView==='tasks' && (totalPages > 1 || totalTasks > 0)")
expect(app).toContain("activeView==='tasks' ? 'task-list-title' : undefined")
const taskTopbar = app.slice(app.indexOf('<header class="topbar"'), app.indexOf('</header>'))
expect(taskTopbar).not.toContain('CompletedFilterPill v-if="activeView!==\'today\'"')
expect(app).toContain('v-if="activeView!==\'today\' && activeView!==\'tasks\'" class="list-toolbar"')
expect(css).toContain('.list-page-context{width:min(100%,630px);margin:0 auto 0;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:start;gap:0 16px}')
expect(css).toContain('.list-page-title{margin:0;font-size:34px;line-height:1.15;font-weight:700;letter-spacing:-.035em}')
expect(css).toContain('.list-page-summary{margin:8px 0 18px;color:var(--muted);font-size:13px}')
expect(css).toContain('@media(min-width:1440px){main.list-main>.list-page-context')
expect(css).toContain('@media(max-width:720px){main.list-main{padding-left:29px;padding-right:29px}main.list-main>.list-page-context,main.list-main>.list-search-reveal,main.list-main>.list-section-heading,main.list-main>.task-list,main.list-main>.list-page-meta,main.list-main>.pager,main.list-main>.mvp-view{width:100%}')
expect(css).toContain('@media(min-width:721px) and (max-width:1439px){main.list-main>.list-page-context,main.list-main>.list-search-reveal,main.list-main>.list-section-heading,main.list-main>.task-list,main.list-main>.list-page-meta,main.list-main>.pager,main.list-main>.mvp-view{width:min(100%,630px)}')
expect(css).toContain('.list-search-reveal{position:relative;min-height:44px;margin-bottom:0;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center}')
expect(css).toContain('@media(max-width:720px){')
expect(css).not.toContain('top:-62px')
expect(css).toContain('.list-inline-filter .completed-filter-pill__track,.habit-inline-filter .completed-filter-pill__track{width:31px;height:18px;background:#d8d0c5}')
expect(css).toContain('.list-inline-filter .completed-filter-pill__thumb,.habit-inline-filter .completed-filter-pill__thumb{width:14px;height:14px}')
expect(css).toContain('.list-section-heading{width:min(100%,630px);min-height:44px;margin:0 auto;display:flex;align-items:center;border-bottom:1px solid #e8e0d5}')
})
it('uses approved Today-sourced page headers for full Habits and keeps archive lazy section', () => {
expect(habits).toContain('v-if="view === \'habits\'" class="habit-page-context"')
expect(habits).toContain('<h1 class="list-page-title">习惯</h1>')
expect(habits).toContain('<p class="list-page-summary">今天还有 {{ todayHabitSummary.total - todayHabitSummary.completed }} 项</p>')
expect(habits).toContain('class="completed-filter-pill habit-inline-filter"')
expect(habits).toContain('@update:model-value="$emit(\'update:showCompleted\', $event)"')
expect(habits).toContain('class="list-section-heading habit-section-heading"')
expect(habits).toContain('<span class="list-section-title">今日习惯</span>')
expect(habits).toContain('<span class="list-section-count">{{ todayHabitSummary.total }}</span>')
expect(habits).not.toContain('<span class="list-section-count">{{ visibleHabits.length }}</span>')
expect(habits).not.toContain('今日完成 {{')
expect(habits).toContain('class="habit-archive-section"')
expect(css).toContain('.habit-page-context{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:start;gap:0 16px}')
expect(css).toContain('.habit-section-heading{margin-top:0}')
expect(css).toContain('main.list-main>.mvp-view{gap:0}')
expect(css).toContain('main.list-main>.mvp-view>.habit-archive-section{border-top:1px solid #e8e0d5}')
})
it('uses one 58px plain-list contract for active task and habit rows', () => {
expect(app).toContain("class=\"task-list plain-list\"")
expect(app).toContain("'task-row--trash':activeView==='trash'")