feat: refine task and countdown interactions
This commit is contained in:
@@ -246,6 +246,33 @@ describe('approved UI detail direction', () => {
|
||||
expect(css).toContain('.task-detail-field-input{width:190px!important;max-width:100%;justify-self:end}')
|
||||
})
|
||||
|
||||
it('opens Today habit bodies with keyboard parity while check clicks stay isolated', () => {
|
||||
const todayRows = mvpPanel.slice(mvpPanel.indexOf('class="habit-list today-habit-list"'), mvpPanel.indexOf('<!-- 完整习惯列表 -->'))
|
||||
expect(todayRows).toContain('role="button" tabindex="0"')
|
||||
expect(todayRows).toContain('@click="openHabitDetail(h, $event.currentTarget as HTMLElement)"')
|
||||
expect(todayRows).toContain('@keydown.enter.prevent="openHabitDetail(h, $event.currentTarget as HTMLElement)"')
|
||||
expect(todayRows).toContain('@keydown.space.prevent="openHabitDetail(h, $event.currentTarget as HTMLElement)"')
|
||||
expect(todayRows).toContain('@click.stop="toggleHabitFromButton(h)"')
|
||||
})
|
||||
|
||||
it('keeps custom recurrence controls at 44px without overflowing narrow screens', () => {
|
||||
expect(css).toContain('.repeat-custom-fields input,.repeat-custom-fields select{min-height:44px;')
|
||||
expect(css).toContain('.weekday-picker label{width:44px;height:44px;')
|
||||
expect(css).toContain('@media(max-width:930px){.repeat-custom-fields{padding:10px;gap:8px}')
|
||||
expect(css).toContain('.repeat-custom-fields>div,.repeat-custom-fields>label{min-width:0;flex-wrap:wrap;gap:6px}')
|
||||
})
|
||||
|
||||
it('compresses countdown focus without hiding its primary information', () => {
|
||||
expect(css).toMatch(/\.countdown-focus\{[^}]*min-height:140px/)
|
||||
expect(css).toMatch(/\.countdown-number strong\{[^}]*font-size:64px/)
|
||||
expect(countdownPanel).toContain('<h3>{{focusItem.title}}</h3>')
|
||||
expect(countdownPanel).toContain('<p>{{primaryDate(focusItem)}}</p>')
|
||||
})
|
||||
|
||||
it('uses the real Memo search state in its toggle name', () => {
|
||||
expect(readFileSync('src/MemoPanel.vue', 'utf8')).toContain(':aria-label="mobileSearchOpen ? \'收起搜索备忘录\' : \'展开搜索备忘录\'"')
|
||||
})
|
||||
|
||||
it('keeps refresh neutral and at least 44px', () => {
|
||||
expect(css).toContain('.topbar-refresh{width:44px;height:44px;min-width:44px;border-radius:50%;color:var(--text-secondary)}')
|
||||
expect(css).not.toContain('.topbar-refresh:hover:not(:disabled){background:#fff7eb;color:var(--accent)}')
|
||||
@@ -320,8 +347,13 @@ describe('completion feedback motion', () => {
|
||||
const saveTaskBlock = app.slice(app.indexOf('async function saveTask('), app.indexOf('async function saveSelectedTaskChanges('))
|
||||
expect(saveTaskBlock).not.toContain('/recurrences/')
|
||||
expect(saveTaskBlock).toContain('as Partial<Task>, false')
|
||||
expect(saveTaskBlock).toContain('selectedDueHasTime.value = Boolean(updated.due_has_time)')
|
||||
const unifiedSaveBlock = app.slice(app.indexOf('async function saveSelectedTaskChanges('), app.indexOf('async function removeTask('))
|
||||
expect(unifiedSaveBlock).toContain('const repeatRecurrence = taskSaved.due_at ? recurrence : null')
|
||||
expect(unifiedSaveBlock).toContain('if (!taskSaved.due_at) {')
|
||||
expect(unifiedSaveBlock).toContain('selectedTaskRecurrence.value = null')
|
||||
expect(unifiedSaveBlock).toContain("selectedTaskRepeat.value = 'none'")
|
||||
const dueRemovalBlock = unifiedSaveBlock.slice(unifiedSaveBlock.indexOf('if (!taskSaved.due_at) {'), unifiedSaveBlock.indexOf('} else {'))
|
||||
expect(dueRemovalBlock).not.toContain('saveRepeat(')
|
||||
})
|
||||
|
||||
it('keeps all task composer touch controls at least 44px on mobile without horizontal overflow', () => {
|
||||
@@ -1062,8 +1094,8 @@ describe('task detail layout', () => {
|
||||
})
|
||||
|
||||
it('keeps the list, due datetime and repeat controls equally compact', () => {
|
||||
expect(app).toContain('<label class="task-detail-due-row">截止时间')
|
||||
expect(app).toContain('class="task-detail-due-input task-detail-field-input"')
|
||||
expect(app).toContain('<div class="task-detail-due-row">')
|
||||
expect(app).toContain('v-model="selectedDueDate" class="task-detail-due-input task-detail-field-input"')
|
||||
expect(app.match(/class="task-detail-field-input"/g)).toHaveLength(2)
|
||||
expect(css).toContain('.task-detail-field-input{width:190px!important;max-width:100%;justify-self:end}')
|
||||
expect(css).toContain('.task-detail-due-input{padding-inline:9px!important}')
|
||||
@@ -1116,19 +1148,23 @@ describe('unified floating add interaction', () => {
|
||||
const saveBlock = app.slice(app.indexOf('async function saveSelectedTaskChanges()'), app.indexOf('async function removeTask'))
|
||||
expect(saveBlock).toContain("const taskId = selectedTask.value?.id")
|
||||
expect(saveBlock).toContain('const selectionToken = recurrenceLoadToken')
|
||||
expect(saveBlock).toContain("const repeatValue = selectedTask.value?.due_at ? selectedTaskRepeat.value : 'none'")
|
||||
expect(saveBlock).toContain("const repeatValue = selectedDueDate.value ? selectedTaskRepeat.value : 'none'")
|
||||
expect(saveBlock).toContain('structuredClone(selectedRepeatConfig.value)')
|
||||
expect(saveBlock).toContain("const taskSaved = await saveTask({ showSuccess: false, expectedTaskId: taskId, expectedSelectionToken: selectionToken })")
|
||||
expect(saveBlock).toContain("recurrenceLoadToken !== selectionToken")
|
||||
expect(saveBlock).toContain('const repeatRecurrence = taskSaved.due_at ? recurrence : null')
|
||||
expect(saveBlock).toContain('await saveRepeat(taskSaved, repeatValue, repeatConfig, afterCompletionDays, repeatRecurrence)')
|
||||
expect(saveBlock).toContain('if (!taskSaved.due_at) {')
|
||||
expect(saveBlock).toContain('selectedTaskRecurrence.value = null')
|
||||
expect(saveBlock).toContain("selectedTaskRepeat.value = 'none'")
|
||||
expect(saveBlock).toContain('await saveRepeat(taskSaved, repeatValue, repeatConfig, afterCompletionDays, recurrence)')
|
||||
const dueRemovalBlock = saveBlock.slice(saveBlock.indexOf('if (!taskSaved.due_at) {'), saveBlock.indexOf('} else {'))
|
||||
expect(dueRemovalBlock).not.toContain('saveRepeat(')
|
||||
expect(saveBlock).toContain("selectedRepeatError.value = ''")
|
||||
expect(saveBlock).toContain("selectedRepeatError.value = reason instanceof Error ? reason.message : '保存失败'")
|
||||
expect(saveBlock.indexOf("toast('已保存')")).toBeGreaterThan(saveBlock.indexOf('await saveRepeat(taskSaved, repeatValue, repeatConfig, afterCompletionDays, repeatRecurrence)'))
|
||||
expect(saveBlock.indexOf("toast('已保存')")).toBeGreaterThan(saveBlock.indexOf('await saveRepeat(taskSaved, repeatValue, repeatConfig, afterCompletionDays, recurrence)'))
|
||||
expect(saveBlock).toContain('if (savingSelectedTask.value || recurrenceLoading.value) return')
|
||||
expect(saveBlock).toContain('savingSelectedTask.value = true')
|
||||
expect(saveBlock).toContain('savingSelectedTask.value = false')
|
||||
expect(saveBlock).toContain("const repeatValue = selectedTask.value?.due_at ? selectedTaskRepeat.value : 'none'")
|
||||
expect(saveBlock).toContain("const repeatValue = selectedDueDate.value ? selectedTaskRepeat.value : 'none'")
|
||||
expect(app).toContain(':disabled="savingSelectedTask || recurrenceLoading" @click="saveSelectedTaskChanges"')
|
||||
expect(app).not.toContain('@blur="saveTask()"')
|
||||
expect(app).not.toContain('@change="saveTask()"')
|
||||
|
||||
Reference in New Issue
Block a user