fix: align task due dates to the right
ci / gitleaks (push) Successful in 7s
ci / docker (push) Successful in 3m34s

This commit is contained in:
2026-09-10 17:01:24 +08:00
parent 0345fbe73c
commit 1366f302ec
8 changed files with 183 additions and 24 deletions
+59 -3
View File
@@ -32,12 +32,50 @@ describe('unified task due display', () => {
expect(overdue).not.toContain('swipe-bg')
})
it('uses semantic due colors and preserves task content and action tracks', () => {
it('places every due display in a right tail before stable actions', () => {
expect(app.match(/<span v-if="[^\"]+\.due_at" class="task-tail"><TaskDueDisplay/g)).toHaveLength(4)
expect(app).not.toContain('class="meta"><TaskDueDisplay')
expect(app).toContain('</div><span v-if="node.task.due_at" class="task-tail"><TaskDueDisplay')
expect(app).toContain('</span><span v-if="activeView===\'trash\'" class="task-actions">')
const ordinaryChildren = app.slice(app.indexOf('<article v-if="!collapsedTaskIds.has(node.task.id)"'), app.indexOf('</template>', app.indexOf('<article v-if="!collapsedTaskIds.has(node.task.id)"')))
expect(ordinaryChildren).toContain('v-if="activeView!==\'trash\'" class="drag-handle task-drag-handle"')
expect(ordinaryChildren).toContain('v-if="activeView!==\'trash\'" class="task-check"')
expect(ordinaryChildren).not.toContain('restoreTask(subtask)')
expect(ordinaryChildren).not.toContain('purgeTask(subtask)')
expect(ordinaryChildren).not.toContain('v-if="activeView===\'trash\'" class="task-actions"')
expect(app).toContain('<span v-else class="task-actions"><button class="icon ghost"')
expect(app).toContain('<span v-if="node.task.priority" class="priority"')
expect(app).toContain('v-if="node.subtasks.length" class="meta"')
expect(app).toContain('<span class="meta-item"><ListChecks/>')
})
it('uses a right-aligned desktop tail and a compact two-line mobile tail without empty slots', () => {
expect(css).toContain('.task-tail{flex:0 1 auto;min-width:0;max-width:')
expect(css).toContain('text-align:right;white-space:nowrap')
expect(css).toContain('.task-actions{display:flex;align-items:center;flex:0 0 auto}')
expect(css).toContain('.task-row>.drag-handle,.task-row>.task-check,.task-actions>*{flex-shrink:0}')
expect(css).toContain('@media(max-width:930px){.task-tail{flex:0 1 76px;max-width:76px;')
expect(css).toContain('.task-row:has(>.task-tail){height:66px;max-height:68px;}')
expect(css).toContain('.task-main{padding:4px 0}')
expect(css).toContain('.task-due__text{display:grid;justify-items:end;overflow:hidden;text-overflow:ellipsis;')
expect(css).toContain('.task-due__absolute,.task-due__relative{max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}')
expect(css).toContain('.task-due__separator{display:none}')
expect(css).not.toContain('.task-main .meta{display:flex;flex-wrap:wrap;')
expect(css).toContain('.task-check{width:44px;height:44px;')
expect(css).toContain('.icon,.ghost{min-width:44px;min-height:44px;')
expect(css).toMatch(/\.restore\{[^}]*min-height:44px/)
})
it('scopes content metadata and keeps date semantics without a visible calendar icon', () => {
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).toContain('.meta-item{display:flex;align-items:center;gap:3px}')
expect(css).not.toContain('.meta span{display:flex;')
expect(taskDueDisplay).not.toContain('CalendarDays')
expect(taskDueDisplay).toContain('<time')
expect(taskDueDisplay).toContain(':aria-label="presentation.fullText"')
expect(taskDueDisplay).toContain('class="task-due__separator"')
expect(css).not.toContain('.overdue-task .meta')
expect(css).not.toContain('.overdue-badge')
})
@@ -396,6 +434,24 @@ describe('task and habit row decoration', () => {
expect(css).toMatch(/\.restore\{[^}]*min-height:44px/)
})
it('reconciles only top-level Trash mutations and distinguishes refresh failure', () => {
const loadBlock = app.slice(app.indexOf('async function loadTrash()'), app.indexOf('async function switchView'))
const mutationBlock = app.slice(app.indexOf('async function mutateTrashTask'), app.indexOf('async function restoreTask'))
const restoreBlock = app.slice(app.indexOf('async function restoreTask'), app.indexOf('async function purgeTask'))
const purgeBlock = app.slice(app.indexOf('async function purgeTask'), app.indexOf('async function addSubtask'))
expect(loadBlock).toContain('return await runLatestRequest')
expect(mutationBlock).toContain('const result = await performTrashMutation(')
expect(mutationBlock).toContain('trash.value = trash.value.filter((item) => item.id !== task.id)')
expect(mutationBlock).toContain('totalTasks.value = nextTotalAfterLocalTaskRemoval(totalTasks.value)')
expect(mutationBlock).toContain('if (page.value > totalPages.value) page.value = totalPages.value')
expect(mutationBlock).toContain('if (!result.mutated)')
expect(mutationBlock).toContain('fail(result.error)')
expect(mutationBlock).toContain('if (!result.refreshed)')
expect(mutationBlock).toContain('请重试刷新')
expect(restoreBlock).toContain("await mutateTrashTask(task, () => api(`/tasks/${task.id}/restore`")
expect(purgeBlock).toContain("await mutateTrashTask(task, () => api(`/trash/${task.id}`")
})
it('supports pointer dragging for desktop task and subtask completion', () => {
expect(app.match(/@pointerdown="startTaskPointer/g)?.length).toBe(3)
expect(app.match(/@pointermove="moveTaskPointer/g)?.length).toBe(3)