fix: harden async task and habit mutations
This commit is contained in:
+28
-24
@@ -363,9 +363,12 @@ describe('approved UI detail direction', () => {
|
||||
for (const [, attributes] of buttons) expect(attributes).toMatch(/\btype="(?:button|submit)"/)
|
||||
const removeBlock = app.slice(app.indexOf('async function removeSubtask('), app.indexOf('function closeTaskDetail('))
|
||||
expect(removeBlock).toContain('if (taskDetailBusy.value) return')
|
||||
expect(removeBlock).toContain("await api(`/tasks/${subtask.id}`, { method: 'DELETE' })")
|
||||
expect(removeBlock).toContain('parent.subtasks = (parent.subtasks ?? []).filter')
|
||||
expect(removeBlock).toContain('tasks.value = tasks.value.filter')
|
||||
expect(removeBlock).toContain("() => api(`/tasks/${subtask.id}`, { method: 'DELETE' })")
|
||||
expect(removeBlock).toContain('await taskMutationReconciler.run(')
|
||||
expect(removeBlock).toContain('{ affectsTaskView: true }')
|
||||
expect(removeBlock).not.toContain('affectsTrash: true')
|
||||
expect(removeBlock).toContain('selectedTask.value.subtasks = (selectedTask.value.subtasks ?? []).filter')
|
||||
expect(removeBlock).not.toContain('tasks.value = tasks.value.filter')
|
||||
expect(removeBlock).not.toContain('selectedTask.value = null')
|
||||
})
|
||||
|
||||
@@ -655,7 +658,7 @@ describe('mobile list row language', () => {
|
||||
expect(css).toContain('.habit-detail-sheet{width:min(500px,100%);max-height:min(88dvh,760px);overflow:hidden')
|
||||
expect(mvpPanel).toContain("habitHistoryNextTo.value = ''")
|
||||
expect(mvpPanel).toContain('habitHistoryHasMore.value = false')
|
||||
expect(mvpPanel).toContain('syncHabitHistoryToday(h)')
|
||||
expect(mvpPanel).toContain('syncHabitHistoryToday(h, day)')
|
||||
expect(mvpPanel).toContain('@click="openHabitDetail(h, $event.currentTarget as HTMLElement)"')
|
||||
expect(mvpPanel).toContain('@keydown.enter.prevent="openHabitDetail(h, $event.currentTarget as HTMLElement)"')
|
||||
expect(mvpPanel).toContain('initial-focus="button[aria-label=\'关闭习惯详情\']"')
|
||||
@@ -801,16 +804,14 @@ describe('task and habit row decoration', () => {
|
||||
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(mutationBlock).toContain('await taskMutationReconciler.run(')
|
||||
expect(mutationBlock).toContain('{ affectsTrash: true, affectsTaskView }')
|
||||
expect(mutationBlock).not.toContain('performTrashMutation(')
|
||||
expect(mutationBlock).not.toContain('loadTrash')
|
||||
expect(restoreBlock).toContain("await mutateTrashTask(task, () => api(`/tasks/${task.id}/restore`")
|
||||
expect(restoreBlock).toContain("'任务已恢复', true")
|
||||
expect(purgeBlock).toContain("await mutateTrashTask(task, () => api(`/trash/${task.id}`")
|
||||
expect(purgeBlock).toContain("'任务已永久删除', false")
|
||||
})
|
||||
|
||||
it('supports pointer dragging for desktop task completion in regular and overdue rows', () => {
|
||||
@@ -828,11 +829,12 @@ describe('task and habit row decoration', () => {
|
||||
})
|
||||
|
||||
it('updates habit rows locally after swiping instead of refreshing the whole Today section', () => {
|
||||
expect(mvpPanel).toContain('setLocalHabitValue(h, next)')
|
||||
const applyBlock = mvpPanel.slice(mvpPanel.indexOf('async function applyHabitSwipe'), mvpPanel.indexOf('async function finishHabitSwipe'))
|
||||
const mutationBlock = mvpPanel.slice(mvpPanel.indexOf('async function mutateHabitValue'), mvpPanel.indexOf('function suppressHabitDetailClick'))
|
||||
const applyBlock = mvpPanel.slice(mvpPanel.indexOf('async function applyHabitSwipe'), mvpPanel.indexOf('function suppressHabitDetailClick'))
|
||||
expect(mutationBlock).toContain('setLocalHabitValue(h, next, day)')
|
||||
expect(mutationBlock).toContain('setLocalHabitValue(h, rollback, day)')
|
||||
expect(applyBlock).not.toContain('await loadHabits()')
|
||||
expect(applyBlock).toContain('setLocalHabitValue(h, next)')
|
||||
expect(applyBlock).toContain('setLocalHabitValue(h, previous)')
|
||||
expect(applyBlock).toContain('mutateHabitValue(h, next')
|
||||
})
|
||||
|
||||
it('keeps swipe behavior without rendering the swipe background layer', () => {
|
||||
@@ -882,10 +884,10 @@ describe('task and habit row decoration', () => {
|
||||
|
||||
it('reconciles concurrent completion mutations without suppressing their feedback', () => {
|
||||
const toggleBlock = app.slice(app.indexOf('async function toggle(task: Task)'), app.indexOf('function isInteractiveTarget'))
|
||||
expect(toggleBlock).toContain('const completing = !task.completed')
|
||||
expect(toggleBlock).toContain('await taskMutationReconciler.run(')
|
||||
expect(toggleBlock).toContain("() => toast(completing ? '完成啦' : '已重新打开')")
|
||||
expect(toggleBlock).toContain('applyTaskUpdate(task, updated)')
|
||||
expect(toggleBlock).toContain('const completing = updated.completed')
|
||||
expect(toggleBlock).toContain('await taskToggleCoordinator.toggle(')
|
||||
expect(toggleBlock).toContain("toast(completing ? '完成啦' : '已重新打开')")
|
||||
expect(toggleBlock).toContain('applyTaskUpdate(task, updated, !preserveSelectedDraft)')
|
||||
expect(toggleBlock).not.toContain("beginLatestRequest('tasks')")
|
||||
})
|
||||
|
||||
@@ -897,8 +899,7 @@ describe('task and habit row decoration', () => {
|
||||
expect(css).toContain('.habit-check{')
|
||||
expect(mvpPanel).toContain('@pointerdown="startHabitPointer')
|
||||
const toggleBlock = mvpPanel.slice(mvpPanel.indexOf('async function toggleHabitFromButton'), mvpPanel.indexOf('function currentHabitForm'))
|
||||
expect(toggleBlock).toContain('setLocalHabitValue(h, next)')
|
||||
expect(toggleBlock).toContain('setLocalHabitValue(h, previous)')
|
||||
expect(toggleBlock).toContain('mutateHabitValue(h, next')
|
||||
expect(toggleBlock).not.toContain('await loadHabits()')
|
||||
})
|
||||
|
||||
@@ -1038,7 +1039,10 @@ describe('task and habit row decoration', () => {
|
||||
expect(app).toContain('return Boolean(dueToday || completedToday)')
|
||||
expect(app).toContain("params.set('completed_from', isoAtLocalDayOffset(0))")
|
||||
expect(app).toContain("params.set('completed_to', isoAtLocalDayOffset(1))")
|
||||
expect(app).toContain("if (activeView.value === 'today' && showCompleted.value) await loadAll()")
|
||||
const toggleBlock = app.slice(app.indexOf('async function toggle(task: Task)'), app.indexOf('function isInteractiveTarget'))
|
||||
expect(toggleBlock).toContain('reconcile: async (confirmed, ownership) =>')
|
||||
expect(toggleBlock).toContain('await reconcileCurrentViewAfterTaskMutation()')
|
||||
expect(toggleBlock).not.toContain('await loadAll()')
|
||||
})
|
||||
|
||||
it('reclassifies Today tasks after due edits without toggling page loading', () => {
|
||||
@@ -1203,7 +1207,7 @@ describe('approved habit safety and U2 title hierarchy', () => {
|
||||
expect(mvpPanel).toContain('habitAction(h)')
|
||||
expect(mvpPanel).toContain(':disabled="!habitAction(h).writable"')
|
||||
expect(mvpPanel).toContain(':aria-disabled="!habitAction(h).writable"')
|
||||
expect(mvpPanel).toContain('setLocalHabitValue(h, previous)')
|
||||
expect(mvpPanel).toContain('setLocalHabitValue(h, rollback, day)')
|
||||
expect(mvpPanel).toContain('formatHabitApiError')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user