feat: show task due distance
ci / gitleaks (push) Successful in 8s
ci / docker (push) Successful in 3m36s

This commit is contained in:
2026-09-10 16:14:09 +08:00
parent 3e80f2ecbb
commit 0345fbe73c
9 changed files with 604 additions and 18 deletions
+50 -5
View File
@@ -8,6 +8,40 @@ const mvpPanel = readFileSync('src/MvpPanel.vue', 'utf8')
const countdownPanel = readFileSync('src/CountdownPanel.vue', 'utf8')
const floatingAdd = readFileSync('src/components/FloatingAddButton.vue', 'utf8')
const completedFilterPill = readFileSync('src/components/CompletedFilterPill.vue', 'utf8')
const taskDueDisplay = readFileSync('src/components/TaskDueDisplay.vue', 'utf8')
describe('unified task due display', () => {
it('uses the shared display for overdue, ordinary parent, and child task rows', () => {
expect(app).toContain("import TaskDueDisplay from './components/TaskDueDisplay.vue'")
expect(app.match(/<TaskDueDisplay/g)).toHaveLength(4)
expect(app).toContain(':due-at="node.task.due_at"')
expect(app).toContain(':due-at="subtask.due_at"')
expect(app).not.toContain('formatDue(')
expect(app).not.toContain('overdue-badge')
expect(taskDueDisplay).not.toMatch(/setInterval|setTimeout/)
})
it('keeps overdue parent subtasks interactive without duplicating ordinary rows', () => {
const overdue = app.slice(app.indexOf('<section v-if="overdueTaskTree.length"'), app.indexOf('<h3 id="today-tasks"'))
expect(overdue).toContain('v-for="subtask in node.subtasks"')
expect(overdue).toContain('@click.stop="toggle(subtask)"')
expect(overdue).toContain('@click="selectTaskUnlessSwiped(subtask)"')
expect(overdue).not.toContain('class="drag-handle task-drag-handle"')
expect(overdue).not.toContain('startTaskReorder(subtask, $event)')
expect(overdue.match(/v-for="subtask in node.subtasks"/g)).toHaveLength(1)
expect(overdue).not.toContain('swipe-bg')
})
it('uses semantic due colors and preserves task content and action tracks', () => {
expect(css).toContain('.task-due--overdue{color:var(--danger)}')
expect(css).toContain('.task-due--neutral,.task-due--completed{color:var(--muted)}')
expect(css).toContain('.task-main{border:0;background:transparent;flex:1;min-width:0;')
expect(css).toContain('.icon,.ghost{min-width:44px;min-height:44px;')
expect(css).toContain('@media(max-width:930px){.task-main .meta{display:flex;flex-wrap:wrap;')
expect(css).not.toContain('.overdue-task .meta')
expect(css).not.toContain('.overdue-badge')
})
})
describe('mobile navigation styles', () => {
it('renames the bottom More tab to a direct Settings tab', () => {
@@ -351,11 +385,22 @@ describe('task and habit row decoration', () => {
expect(mvpPanel.match(/'--reorder-y': `\$\{habitReorder\?\.id === h\.id \? habitReorder\.offsetY : 0\}px`/g)?.length).toBe(1)
})
it('exposes complete task titles on every ellipsized parent and child title node', () => {
expect(app.match(/<strong :title="node\.task\.title">\{\{node\.task\.title\}\}<\/strong>/g)).toHaveLength(2)
expect(app.match(/<strong :title="subtask\.title">\{\{subtask\.title\}\}<\/strong>/g)).toHaveLength(2)
expect(app).not.toContain('<strong>{{node.task.title}}</strong>')
expect(app).not.toContain('<strong>{{subtask.title}}</strong>')
})
it('keeps the Trash restore action at least 44px tall', () => {
expect(css).toMatch(/\.restore\{[^}]*min-height:44px/)
})
it('supports pointer dragging for desktop task and subtask completion', () => {
expect(app.match(/@pointerdown="startTaskPointer/g)?.length).toBe(2)
expect(app.match(/@pointermove="moveTaskPointer/g)?.length).toBe(2)
expect(app.match(/@pointerup="finishTaskPointer/g)?.length).toBe(2)
expect(app.match(/@pointercancel="cancelTaskPointer/g)?.length).toBe(2)
expect(app.match(/@pointerdown="startTaskPointer/g)?.length).toBe(3)
expect(app.match(/@pointermove="moveTaskPointer/g)?.length).toBe(3)
expect(app.match(/@pointerup="finishTaskPointer/g)?.length).toBe(3)
expect(app.match(/@pointercancel="cancelTaskPointer/g)?.length).toBe(3)
})
it('supports pointer dragging for desktop habit rows in Today and Habits views', () => {
@@ -400,7 +445,7 @@ describe('task and habit row decoration', () => {
expect(app).not.toContain('class="sr-only" :aria-label="node.task.completed')
expect(app).not.toContain('class="sr-only" :aria-label="subtask.completed')
expect(app).toContain('class="task-check"')
expect(app.match(/class="task-check"/g)?.length).toBe(3)
expect(app.match(/class="task-check"/g)?.length).toBe(4)
expect(app).toContain('class="task-check-mark"')
expect(css).toContain('.task-check{')
expect(css).toContain('.task-check-mark{')