feat: keep subtasks in task details
This commit is contained in:
+39
-49
@@ -11,38 +11,31 @@ const completedFilterPill = readFileSync('src/components/CompletedFilterPill.vue
|
||||
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', () => {
|
||||
it('uses the shared display for overdue and ordinary parent task rows', () => {
|
||||
expect(app).toContain("import TaskDueDisplay from './components/TaskDueDisplay.vue'")
|
||||
expect(app.match(/<TaskDueDisplay/g)).toHaveLength(4)
|
||||
expect(app.match(/<TaskDueDisplay/g)).toHaveLength(2)
|
||||
expect(app).toContain(':due-at="node.task.due_at"')
|
||||
expect(app).toContain(':due-at="subtask.due_at"')
|
||||
expect(app).not.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', () => {
|
||||
it('keeps overdue subtasks out of the list while preserving their parent progress summary', () => {
|
||||
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')
|
||||
expect(overdue).not.toContain('v-for="subtask in node.subtasks"')
|
||||
expect(overdue).not.toContain('selectTaskUnlessSwiped(subtask)')
|
||||
expect(overdue).toContain('node.subtasks.filter(t=>t.completed).length')
|
||||
expect(overdue).toContain('{{node.subtasks.length}}')
|
||||
})
|
||||
|
||||
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)
|
||||
it('places every visible-list due display in a right tail before stable actions', () => {
|
||||
expect(app.match(/<span v-if="[^\"]+\.due_at" class="task-tail"><TaskDueDisplay/g)).toHaveLength(2)
|
||||
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).not.toContain('restoreTask(subtask)')
|
||||
expect(app).not.toContain('purgeTask(subtask)')
|
||||
expect(app).toContain('<span v-else class="task-actions"><button class="icon ghost task-detail-trigger"')
|
||||
expect(app).toContain('<span v-if="node.task.priority" class="priority"')
|
||||
expect(app).toContain('v-if="node.subtasks.length" class="meta"')
|
||||
@@ -215,11 +208,11 @@ describe('solid cream material system', () => {
|
||||
})
|
||||
|
||||
describe('approved UI detail direction', () => {
|
||||
it('uses one More detail entry for ordinary parent and child rows while preserving Trash and overdue actions', () => {
|
||||
it('uses one More detail entry for ordinary parent rows while preserving Trash and overdue actions', () => {
|
||||
const ordinaryRows = app.slice(app.indexOf('<section class="task-list"'), app.indexOf('</section>', app.indexOf('<section class="task-list"')))
|
||||
expect(ordinaryRows.match(/aria-label="打开任务详情"/g)).toHaveLength(2)
|
||||
expect(ordinaryRows.match(/aria-label="打开任务详情"/g)).toHaveLength(1)
|
||||
expect(ordinaryRows).toContain('@click.stop="selectTask(node.task)"><Ellipsis/>')
|
||||
expect(ordinaryRows).toContain('@click.stop="selectTask(subtask)"><Ellipsis/>')
|
||||
expect(ordinaryRows).not.toContain('selectTask(subtask)')
|
||||
expect(ordinaryRows).not.toContain('aria-label="删除任务"')
|
||||
expect(ordinaryRows).toContain('v-if="activeView===\'trash\'" class="task-actions"')
|
||||
expect(ordinaryRows).toContain('restoreTask(node.task)')
|
||||
@@ -228,9 +221,9 @@ describe('approved UI detail direction', () => {
|
||||
expect(overdue).not.toContain('aria-label="打开任务详情"')
|
||||
})
|
||||
|
||||
it('keeps the due tail immediately before the More detail entry', () => {
|
||||
it('keeps the parent due tail immediately before the More detail entry', () => {
|
||||
expect(app).toContain('</span><span v-else class="task-actions"><button class="icon ghost task-detail-trigger" aria-label="打开任务详情" @click.stop="selectTask(node.task)"><Ellipsis/></button></span>')
|
||||
expect(app).toContain('</span><span v-if="activeView!==\'trash\'" class="task-actions"><button class="icon ghost task-detail-trigger" aria-label="打开任务详情" @click.stop="selectTask(subtask)"><Ellipsis/></button></span>')
|
||||
expect(app).not.toContain('@click.stop="selectTask(subtask)"><Ellipsis/>')
|
||||
expect(css).toContain('.task-detail-trigger:hover{color:var(--text-primary);background:#fff7eb}')
|
||||
})
|
||||
|
||||
@@ -503,8 +496,8 @@ describe('task and habit row decoration', () => {
|
||||
expect(mvpPanel).not.toContain('RefreshCw,')
|
||||
})
|
||||
|
||||
it('supports vertical drag handles for reordering tasks and habits', () => {
|
||||
expect(app.match(/class="drag-handle task-drag-handle"/g)?.length).toBe(2)
|
||||
it('supports vertical drag handles for reordering visible parent tasks and habits', () => {
|
||||
expect(app.match(/class="drag-handle task-drag-handle"/g)?.length).toBe(1)
|
||||
expect(app).toContain('@pointerdown.stop="startTaskReorder')
|
||||
expect(app).toContain('@pointerup.stop="finishTaskReorder')
|
||||
expect(app).toContain("api('/tasks/reorder'")
|
||||
@@ -520,11 +513,10 @@ 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', () => {
|
||||
it('exposes complete titles on every ellipsized visible task 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 :title="subtask.title">{{subtask.title}}</strong>')
|
||||
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', () => {
|
||||
@@ -549,11 +541,11 @@ describe('task and habit row decoration', () => {
|
||||
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)
|
||||
expect(app.match(/@pointerup="finishTaskPointer/g)?.length).toBe(3)
|
||||
expect(app.match(/@pointercancel="cancelTaskPointer/g)?.length).toBe(3)
|
||||
it('supports pointer dragging for desktop visible task completion', () => {
|
||||
expect(app.match(/@pointerdown="startTaskPointer/g)?.length).toBe(1)
|
||||
expect(app.match(/@pointermove="moveTaskPointer/g)?.length).toBe(1)
|
||||
expect(app.match(/@pointerup="finishTaskPointer/g)?.length).toBe(1)
|
||||
expect(app.match(/@pointercancel="cancelTaskPointer/g)?.length).toBe(1)
|
||||
})
|
||||
|
||||
it('supports pointer dragging for desktop habit rows in Today and Habits views', () => {
|
||||
@@ -579,27 +571,25 @@ describe('task and habit row decoration', () => {
|
||||
expect(mvpPanel).toContain('shouldToggleRowSwipe')
|
||||
})
|
||||
|
||||
it('collapses and expands subtasks when the parent task is clicked', () => {
|
||||
expect(app).toContain('const collapsedTaskIds = ref(new Set<string>())')
|
||||
expect(app).toContain('function toggleTaskChildren(task: Task)')
|
||||
expect(app).toContain('selectTaskUnlessSwiped(node.task, true)')
|
||||
expect(app).toContain('v-if="!collapsedTaskIds.has(node.task.id)" v-for="subtask in node.subtasks"')
|
||||
expect(app).toContain(':aria-expanded="!collapsedTaskIds.has(node.task.id)"')
|
||||
it('shows subtasks only in task details while keeping the parent progress summary', () => {
|
||||
const overdue = app.slice(app.indexOf('<section v-if="overdueTaskTree.length"'), app.indexOf('<h3 id="today-tasks"'))
|
||||
const ordinaryRows = app.slice(app.indexOf('<section class="task-list"'), app.indexOf('</section>', app.indexOf('<section class="task-list"')))
|
||||
expect(overdue).not.toContain('v-for="subtask in node.subtasks"')
|
||||
expect(ordinaryRows).not.toContain('v-for="subtask in node.subtasks"')
|
||||
expect(app).not.toContain('collapsedTaskIds')
|
||||
expect(app).not.toContain('toggleTaskChildren')
|
||||
expect(app.match(/<span v-if="node\.subtasks\.length" class="meta">/g)).toHaveLength(2)
|
||||
expect(app).toContain('v-for="subtask in selectedTaskSubtasks"')
|
||||
expect(app).toContain('class="subtask-detail"')
|
||||
})
|
||||
|
||||
it('starts parent tasks collapsed when entering a task list or upcoming view', () => {
|
||||
expect(app).toContain('function collapseLoadedTaskChildren()')
|
||||
expect(app).toContain("else if (isTaskView(activeView.value)) {\n await loadAll()\n if (activeView.value === 'tasks' || activeView.value === 'upcoming') collapseLoadedTaskChildren()")
|
||||
expect(app).toContain("else {\n await loadAll()\n if (view === 'tasks' || view === 'upcoming') collapseLoadedTaskChildren()")
|
||||
expect(app).toContain('tasks.value.filter((task) => task.subtasks?.length).map((task) => task.id)')
|
||||
})
|
||||
|
||||
it('shows visible completion buttons for tasks and subtasks while keeping swipe shortcuts', () => {
|
||||
it('shows visible completion buttons for parent tasks while keeping subtask completion in details', () => {
|
||||
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(4)
|
||||
expect(app.match(/class="task-check"/g)?.length).toBe(2)
|
||||
expect(app).toContain('class="task-check-mark"')
|
||||
expect(app).toContain('class="subtask-detail" @click="toggle(subtask)"')
|
||||
expect(css).toContain('.task-check{')
|
||||
expect(css).toContain('.task-check-mark{')
|
||||
expect(css).toContain('.task-check:focus-visible{')
|
||||
|
||||
Reference in New Issue
Block a user