feat: collapse Today sections
ci / gitleaks (push) Successful in 8s
ci / docker (push) Successful in 6m1s

This commit is contained in:
2026-09-15 09:47:02 +08:00
parent 4e8045159f
commit f5a1521a4f
7 changed files with 172 additions and 17 deletions
+58
View File
@@ -0,0 +1,58 @@
import { readFileSync } from 'node:fs'
import { describe, expect, it } from 'vitest'
const app = readFileSync('src/App.vue', 'utf8')
const style = readFileSync('src/style.css', 'utf8')
const css = readFileSync('src/today-sections.css', 'utf8')
describe('Today independent collapsible sections', () => {
it('owns and persists one three-field collapse state in App', () => {
expect(app).toContain("const TODAY_SECTION_COLLAPSE_KEY = 'dodo.today-section-collapse.v1'")
expect(app).toContain('readTodaySectionCollapse(window.localStorage, TODAY_SECTION_COLLAPSE_KEY)')
expect(app).toContain('writeTodaySectionCollapse(window.localStorage, TODAY_SECTION_COLLAPSE_KEY, todaySectionCollapse.value)')
})
it('imports the section stylesheet from the global stylesheet', () => {
expect(style).toContain('@import "./today-sections.css";')
})
it('renders full-row accessible toggles and labelled regions for all three sections', () => {
for (const section of ['overdue', 'tasks', 'habits']) {
expect(app).toContain(`id="today-${section}-heading"`)
expect(app).toContain(`aria-controls="today-${section}"`)
expect(app).toContain(`:aria-expanded="!todaySectionCollapse.${section}"`)
if (section === 'tasks') {
expect(app).toContain(":id=\"activeView==='today' ? 'today-tasks' : undefined\"")
expect(app).toContain(":aria-labelledby=\"activeView==='today' ? 'today-tasks-heading' : undefined\"")
} else {
expect(app).toContain(`id="today-${section}"`)
expect(app).toContain(`aria-labelledby="today-${section}-heading"`)
}
}
expect(app.match(/class="today-section-toggle(?: today-section-anchor)?"/g)).toHaveLength(3)
expect(app).toContain('<span class="today-section-summary">{{totalTasks}} 项</span>')
expect(app).not.toContain('<span class="today-section-summary">{{taskTree.length}} 项</span>')
expect(app).toContain('<ChevronRight v-if="todaySectionCollapse.overdue"')
expect(app).toContain('<ChevronDown v-else')
expect(css).toMatch(/\.today-section-toggle\{[^}]*min-height:44px/)
})
it('keeps the habit panel mounted while collapsed and omits an empty overdue section', () => {
expect(app).toContain('<section v-if="overdueTaskTree.length" class="overdue-section today-collapsible-section">')
expect(app).toContain('<div v-show="!todaySectionCollapse.habits" id="today-habits"')
const habits = app.slice(app.indexOf('id="today-habits-heading"'), app.indexOf('</div>', app.indexOf('<MvpPanel ref="habitComposer" view="today-habits"')) + 6)
expect(habits).toContain('<MvpPanel ref="habitComposer" view="today-habits"')
expect(habits).not.toContain('v-if="!todaySectionCollapse.habits"')
})
it('expands a collapsed destination before next-tick scrolling and focus, respecting reduced motion', () => {
expect(app).toContain("async function navigateTodaySection(section: 'tasks' | 'habits')")
expect(app).toContain('todaySectionCollapse.value[section] = false')
expect(app).toContain('await nextTick()')
expect(app).toContain('await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()))')
expect(app).toContain('heading?.focus({ preventScroll: true })')
expect(app).toContain("window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth'")
expect(app).toContain("@click=\"navigateTodaySection('tasks')\"")
expect(app).toContain("@click=\"navigateTodaySection('habits')\"")
})
})