fix: unify completed item visibility
This commit is contained in:
+15
-4
@@ -77,6 +77,7 @@ const showCompleted = ref(readStoredBoolean(window.localStorage, SHOW_COMPLETED_
|
||||
const page = ref(1)
|
||||
const pageSize = 50
|
||||
const totalTasks = ref(0)
|
||||
const hiddenCompletedTaskCount = ref(0)
|
||||
const todayTaskTotal = ref(0)
|
||||
const todayTaskCompleted = ref(0)
|
||||
const todayHabitTotal = ref(0)
|
||||
@@ -475,6 +476,15 @@ async function loadTasksPage() {
|
||||
const data = await api(`/tasks?${params}`)
|
||||
tasks.value = data.items ?? []
|
||||
totalTasks.value = data.total ?? tasks.value.length
|
||||
hiddenCompletedTaskCount.value = 0
|
||||
if (!showCompleted.value && !query.value && activeView.value !== 'trash' && tasks.value.length === 0) {
|
||||
const completedParams = new URLSearchParams(params)
|
||||
completedParams.set('page', '1')
|
||||
completedParams.set('page_size', '1')
|
||||
completedParams.set('completed', 'true')
|
||||
const completedData = await api(`/tasks?${completedParams}`)
|
||||
hiddenCompletedTaskCount.value = Number(completedData.total ?? completedData.items?.length ?? 0)
|
||||
}
|
||||
}
|
||||
async function loadNavigation(force = false) {
|
||||
if (!force && navigationLoaded.value) return
|
||||
@@ -1137,10 +1147,11 @@ onUnmounted(() => {
|
||||
<label v-if="['tasks','today','upcoming','trash'].includes(activeView)" class="search"><Search/><input v-model="query" placeholder="搜索任务…" aria-label="搜索任务"><kbd>⌘ K</kbd></label>
|
||||
</header>
|
||||
<template v-if="['habits','settings'].includes(activeView)">
|
||||
<MvpPanel ref="habitComposer" :key="activeView" :view="activeView as 'habits'|'settings'" @changed="refreshAll" @notice="toast" />
|
||||
<MvpPanel ref="habitComposer" :key="activeView" :view="activeView as 'habits'|'settings'" :show-completed="showCompleted" @update:show-completed="showCompleted = $event" @changed="refreshAll" @notice="toast" />
|
||||
</template>
|
||||
<CountdownPanel ref="countdownComposer" v-else-if="activeView==='countdowns'" @notice="toast" />
|
||||
<template v-else>
|
||||
<div v-if="activeView==='today'" class="list-toolbar today-completed-toolbar"><label><input v-model="showCompleted" type="checkbox"> 显示已完成</label></div>
|
||||
<section v-if="activeView==='today'" class="today-board" aria-label="今日进度">
|
||||
<button class="today-track today-task-track" type="button" aria-controls="today-tasks" @click="scrollTodaySection('today-tasks')">
|
||||
<span class="today-track-head"><strong>任务 {{ todayTaskCompleted }} / {{ todayTaskTotal }}</strong></span>
|
||||
@@ -1162,7 +1173,7 @@ onUnmounted(() => {
|
||||
</section>
|
||||
<h3 id="today-tasks" class="section-heading today-section-anchor"><ListTodo/>任务</h3>
|
||||
</template>
|
||||
<div class="list-toolbar"><label v-if="activeView!=='trash'"><input v-model="showCompleted" type="checkbox"> 显示已完成</label><span v-if="activeView==='trash' || totalPages > 1 || totalTasks > 0">{{ totalPages > 1 ? `第 ${page} / ${totalPages} 页 · ` : '' }}共 {{totalTasks}} 项</span><button v-if="query" class="link" @click="query=''">清除搜索</button></div>
|
||||
<div v-if="activeView!=='today'" class="list-toolbar"><label v-if="activeView!=='trash'"><input v-model="showCompleted" type="checkbox"> 显示已完成</label><span v-if="activeView==='trash' || totalPages > 1 || totalTasks > 0">{{ totalPages > 1 ? `第 ${page} / ${totalPages} 页 · ` : '' }}共 {{totalTasks}} 项</span><button v-if="query" class="link" @click="query=''">清除搜索</button></div>
|
||||
<div v-if="activeView!=='trash' && totalPages > 1" class="pager"><button class="secondary" :disabled="page<=1 || loading" @click="previousPage">上一页</button><span>{{page}} / {{totalPages}}</span><button class="secondary" :disabled="page>=totalPages || loading" @click="nextPage">下一页</button></div>
|
||||
<section class="task-list" :class="{loading}">
|
||||
<template v-for="node in taskTree" :key="node.task.id">
|
||||
@@ -1177,11 +1188,11 @@ onUnmounted(() => {
|
||||
</article>
|
||||
<article v-if="!collapsedTaskIds.has(node.task.id)" v-for="subtask in node.subtasks" :key="subtask.id" :data-task-id="subtask.id" class="task-row subtask swipeable" :class="{done:subtask.completed,'just-completed': justCompletedTaskIds.has(subtask.id),ready:Math.abs(taskSwipeOffsets[subtask.id] ?? 0) >= 64,reordering:taskReorder?.id===subtask.id,'reorder-target':taskReorderTarget===subtask.id}" :style="{ '--swipe-x': `${taskSwipeOffsets[subtask.id] ?? 0}px`, '--reorder-y': `${taskReorder?.id === subtask.id ? taskReorder.offsetY : 0}px` }" @pointerdown="startTaskPointer(subtask, $event)" @pointermove="moveTaskPointer(subtask, $event)" @pointerup="finishTaskPointer(subtask, $event)" @pointercancel="cancelTaskPointer(subtask)" @touchstart.passive="startTaskSwipe(subtask, $event)" @touchmove.passive="moveTaskSwipe(subtask, $event)" @touchend="finishTaskSwipe(subtask, $event)" @touchcancel="cancelTaskSwipe(subtask)"><button class="drag-handle task-drag-handle" aria-label="上下拖动子任务排序" title="上下拖动排序" @pointerdown.stop="startTaskReorder(subtask, $event)" @pointermove.stop="moveTaskReorder(subtask, $event)" @pointerup.stop="finishTaskReorder(subtask, $event)" @pointercancel.stop="cancelTaskReorder"><GripVertical/></button><button class="task-check" :aria-label="subtask.completed ? `重新打开${subtask.title}` : `完成${subtask.title}`" :aria-pressed="subtask.completed" @click.stop="toggle(subtask)"><span class="task-check-mark" :class="`p${subtask.priority}`"><Check v-if="subtask.completed" /></span></button><div class="task-main" role="button" tabindex="0" @click="selectTaskUnlessSwiped(subtask)" @keydown.enter="selectTaskUnlessSwiped(subtask)" @keydown.space.prevent="selectTaskUnlessSwiped(subtask)"><strong>{{subtask.title}}</strong></div></article>
|
||||
</template>
|
||||
<div v-if="!visibleTasks.length&&!loading" class="empty"><ListTodo/><b>{{query?'没有匹配的任务':'这里还很安静'}}</b><span>{{query?'换个关键词试试':'写下第一件想完成的小事吧'}}</span><button v-if="activeView==='today' && !query" class="soft-button empty-action" @click="openTaskCompose"><Plus/>添加今天任务</button></div>
|
||||
<div v-if="!visibleTasks.length&&!loading" class="empty"><ListTodo/><b>{{ query ? '没有匹配的任务' : hiddenCompletedTaskCount > 0 ? '已完成任务已隐藏' : '这里还很安静' }}</b><span>{{query?'换个关键词试试':hiddenCompletedTaskCount > 0 ? '开启“显示已完成”即可查看。':'写下第一件想完成的小事吧'}}</span><button v-if="activeView==='today' && !query && hiddenCompletedTaskCount === 0" class="soft-button empty-action" @click="openTaskCompose"><Plus/>添加今天任务</button></div>
|
||||
</section>
|
||||
<div v-if="activeView==='today'" id="today-habits" class="today-section-anchor">
|
||||
<h3 class="section-heading"><Repeat2/>习惯</h3>
|
||||
<MvpPanel ref="habitComposer" view="today-habits" @changed="refreshAll" @notice="toast" @summary="updateTodayHabitSummary" />
|
||||
<MvpPanel ref="habitComposer" view="today-habits" :show-completed="showCompleted" @changed="refreshAll" @notice="toast" @summary="updateTodayHabitSummary" />
|
||||
</div>
|
||||
</template>
|
||||
</main>
|
||||
|
||||
+10
-13
@@ -2,7 +2,7 @@
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { Activity, ArchiveRestore, Check, Download, FileJson, GripVertical, LogOut, Pencil, Trash2, X } from 'lucide-vue-next'
|
||||
import { moveItemWithinScope } from './lib/task-utils'
|
||||
import { changedHabitFields, dateKey, formatHabitApiError, habitActionState, habitButtonNotice, habitButtonValue, isHabitComplete, isHabitScheduledToday, mergePage, nextHabitSwipeValue, previousHabitSwipeValue, readHabitGridCache, readStoredBoolean, shouldToggleRowSwipe, validateHabitForm, writeHabitGridCache, writeStoredBoolean, type HabitFormErrors, type HabitFormValues } from './lib/mvp-utils'
|
||||
import { changedHabitFields, dateKey, formatHabitApiError, habitActionState, habitButtonNotice, habitButtonValue, isHabitComplete, isHabitScheduledToday, mergePage, nextHabitSwipeValue, previousHabitSwipeValue, readHabitGridCache, shouldToggleRowSwipe, validateHabitForm, writeHabitGridCache, type HabitFormErrors, type HabitFormValues } from './lib/mvp-utils'
|
||||
import { csrfHeader } from './lib/csrf'
|
||||
import { createCompletionPulse } from './lib/completion-motion'
|
||||
|
||||
@@ -10,11 +10,12 @@ type View = 'habits' | 'today-habits' | 'settings'
|
||||
type HabitCell = { day: string; scheduled?: boolean; paused?: boolean; value: number | boolean }
|
||||
type Habit = { id: string; name: string; kind?: string; target?: number; max_value?: number | null; schedule_type?: HabitFormValues['schedule_type']; weekdays?: number[] | null; month_days?: number[] | null; interval_days?: number | null; archived_at?: string | null; unit?: string; cells?: HabitCell[]; stats?: Record<string, number> }
|
||||
type Session = { id: string; created_at?: string; last_seen_at?: string; current?: boolean; user_agent?: string }
|
||||
const props = defineProps<{ view: View }>()
|
||||
const props = defineProps<{ view: View; showCompleted: boolean }>()
|
||||
const emit = defineEmits<{
|
||||
changed: []
|
||||
notice: [message: string]
|
||||
summary: [value: { total: number; completed: number }]
|
||||
'update:showCompleted': [value: boolean]
|
||||
}>()
|
||||
const habits = ref<Habit[]>([])
|
||||
const archivedHabits = ref<Habit[]>([])
|
||||
@@ -61,17 +62,14 @@ const markHabitJustCompleted = createCompletionPulse(
|
||||
(id) => { justCompletedHabitIds.value = new Set(justCompletedHabitIds.value).add(id) },
|
||||
(id) => { const next = new Set(justCompletedHabitIds.value); next.delete(id); justCompletedHabitIds.value = next },
|
||||
)
|
||||
const HIDE_COMPLETED_HABITS_STORAGE_KEY = 'dodo.hide-completed-habits'
|
||||
const hideCompletedHabits = ref(readStoredBoolean(window.localStorage, HIDE_COMPLETED_HABITS_STORAGE_KEY, false))
|
||||
const todayHabits = computed(() => habits.value.filter((item) => isHabitScheduledToday(item, todayKey.value)))
|
||||
const visibleTodayHabits = computed(() => hideCompletedHabits.value ? todayHabits.value.filter((item) => !isDone(item, todayKey.value)) : todayHabits.value)
|
||||
const visibleHabits = computed(() => hideCompletedHabits.value ? habits.value.filter((item) => !isDone(item, todayKey.value)) : habits.value)
|
||||
const visibleTodayHabits = computed(() => props.showCompleted ? todayHabits.value : todayHabits.value.filter((item) => !isDone(item, todayKey.value)))
|
||||
const visibleHabits = computed(() => props.showCompleted ? habits.value : habits.value.filter((item) => !isDone(item, todayKey.value)))
|
||||
const todayHabitSummary = computed(() => ({
|
||||
total: todayHabits.value.length,
|
||||
completed: todayHabits.value.filter((item) => isDone(item, todayKey.value)).length,
|
||||
}))
|
||||
watch(todayHabitSummary, (value) => emit('summary', value), { immediate: true })
|
||||
watch(hideCompletedHabits, (value) => writeStoredBoolean(window.localStorage, HIDE_COMPLETED_HABITS_STORAGE_KEY, value))
|
||||
let dayRolloverTimer: ReturnType<typeof setInterval> | undefined
|
||||
|
||||
async function request(path: string, options: RequestInit = {}) {
|
||||
@@ -99,7 +97,7 @@ function isInteractiveTarget(target: EventTarget | null) {
|
||||
return target instanceof Element && Boolean(target.closest('button,input,select,textarea,a,label'))
|
||||
}
|
||||
function startHabitReorder(h: Habit, event: PointerEvent) {
|
||||
if (busy.value || hideCompletedHabits.value) return
|
||||
if (busy.value || !props.showCompleted) return
|
||||
habitReorder.value = { id: h.id, startY: event.clientY, offsetY: 0 }
|
||||
habitReorderTarget.value = h.id
|
||||
try { (event.currentTarget as Element).setPointerCapture(event.pointerId) } catch { /* synthetic events */ }
|
||||
@@ -526,12 +524,11 @@ onBeforeUnmount(() => {
|
||||
<template v-if="view === 'habits' || view === 'today-habits'">
|
||||
<header v-if="view === 'habits'" class="view-intro">
|
||||
<div><small>把想坚持的事,变成每天的日常</small></div>
|
||||
<div class="habit-toolbar"><label><input v-model="hideCompletedHabits" type="checkbox"> 隐藏已完成</label></div>
|
||||
<div class="habit-toolbar"><label><input :checked="showCompleted" type="checkbox" @change="emit('update:showCompleted', ($event.target as HTMLInputElement).checked)"> 显示已完成</label></div>
|
||||
</header>
|
||||
|
||||
<!-- 今日习惯:只展示“今天该做”的习惯,复用正式习惯行样式 -->
|
||||
<div v-if="view === 'today-habits' && (habits.length || !busy)" class="habit-list today-habit-list">
|
||||
<div class="habit-toolbar habit-toolbar-today"><label><input v-model="hideCompletedHabits" type="checkbox"> 隐藏已完成</label></div>
|
||||
<article v-for="h in visibleTodayHabits" :key="h.id" :data-habit-id="h.id" class="habit-row today-habit-row swipeable" :class="{ done: isDone(h, todayKey), 'just-completed': justCompletedHabitIds.has(h.id), ready: Math.abs(habitSwipeOffsets[h.id] ?? 0) >= 64 }" :style="{ '--swipe-x': `${habitSwipeOffsets[h.id] ?? 0}px` }" @pointerdown="startHabitPointer(h, $event)" @pointermove="moveHabitPointer(h, $event)" @pointerup="finishHabitPointer(h, $event)" @pointercancel="cancelHabitPointer(h)" @touchstart.passive="startHabitSwipe(h, $event)" @touchmove.passive="moveHabitSwipe(h, $event)" @touchend="finishHabitSwipe(h, $event)" @touchcancel="cancelHabitSwipe(h)">
|
||||
<button class="task-check habit-check" :disabled="!habitAction(h).writable" :aria-disabled="!habitAction(h).writable" :title="habitAction(h).reason" :aria-label="habitAction(h).reason || (isDone(h, todayKey) ? `减少${h.name}一次` : `完成${h.name}一次`)" :aria-pressed="isDone(h, todayKey)" @click.stop="toggleHabitFromButton(h)"><span class="task-check-mark"><Check v-if="isDone(h, todayKey)" /></span></button>
|
||||
<div class="habit-main">
|
||||
@@ -541,7 +538,7 @@ onBeforeUnmount(() => {
|
||||
<progress v-if="h.kind === 'numeric'" class="habit-progress-bar" :value="habitProgressValue(h)" :max="habitProgressMax(h)" :aria-label="`${h.name}进度:${habitProgressText(h)}`"></progress>
|
||||
</div>
|
||||
</article>
|
||||
<div v-if="!visibleTodayHabits.length && !busy" class="empty-panel today-empty-panel"><span>{{ hideCompletedHabits && todayHabits.length ? '已完成的习惯已隐藏。' : '今天没有安排习惯,轻松一下吧。' }}</span><button v-if="!hideCompletedHabits || !todayHabits.length" class="soft-button empty-action" @click="openHabitComposer"><Check/>添加习惯</button></div>
|
||||
<div v-if="!visibleTodayHabits.length && !busy" class="empty-panel today-empty-panel"><span>{{ !showCompleted && todayHabits.length ? '已完成的习惯已隐藏。' : '今天没有安排习惯,轻松一下吧。' }}</span><button v-if="showCompleted || !todayHabits.length" class="soft-button empty-action" @click="openHabitComposer"><Check/>添加习惯</button></div>
|
||||
</div>
|
||||
|
||||
<!-- 完整习惯列表 -->
|
||||
@@ -567,7 +564,7 @@ onBeforeUnmount(() => {
|
||||
<!-- 习惯列表支持整行滑动记录。 -->
|
||||
<div v-if="view === 'habits'" class="habit-list">
|
||||
<article v-for="h in visibleHabits" :key="h.id" :data-habit-id="h.id" class="habit-row swipeable" :class="{ done: isDone(h, todayKey), 'just-completed': justCompletedHabitIds.has(h.id), ready: Math.abs(habitSwipeOffsets[h.id] ?? 0) >= 64, reordering: habitReorder?.id === h.id, 'reorder-target': habitReorderTarget === h.id }" :style="{ '--swipe-x': `${habitSwipeOffsets[h.id] ?? 0}px`, '--reorder-y': `${habitReorder?.id === h.id ? habitReorder.offsetY : 0}px` }" @pointerdown="startHabitPointer(h, $event)" @pointermove="moveHabitPointer(h, $event)" @pointerup="finishHabitPointer(h, $event)" @pointercancel="cancelHabitPointer(h)" @touchstart.passive="startHabitSwipe(h, $event)" @touchmove.passive="moveHabitSwipe(h, $event)" @touchend="finishHabitSwipe(h, $event)" @touchcancel="cancelHabitSwipe(h)">
|
||||
<button class="drag-handle habit-drag-handle" :disabled="hideCompletedHabits" aria-label="上下拖动习惯排序" title="上下拖动排序" @pointerdown.stop="startHabitReorder(h, $event)" @pointermove.stop="moveHabitReorder(h, $event)" @pointerup.stop="finishHabitReorder(h, $event)" @pointercancel.stop="cancelHabitReorder"><GripVertical/></button>
|
||||
<button class="drag-handle habit-drag-handle" :disabled="!showCompleted" aria-label="上下拖动习惯排序" title="上下拖动排序" @pointerdown.stop="startHabitReorder(h, $event)" @pointermove.stop="moveHabitReorder(h, $event)" @pointerup.stop="finishHabitReorder(h, $event)" @pointercancel.stop="cancelHabitReorder"><GripVertical/></button>
|
||||
<button class="task-check habit-check" :disabled="!habitAction(h).writable" :aria-disabled="!habitAction(h).writable" :title="habitAction(h).reason" :aria-label="habitAction(h).reason || (isDone(h, todayKey) ? `减少${h.name}一次` : `完成${h.name}一次`)" :aria-pressed="isDone(h, todayKey)" @click.stop="toggleHabitFromButton(h)"><span class="task-check-mark"><Check v-if="isDone(h, todayKey)" /></span></button>
|
||||
<div class="habit-main" role="button" tabindex="0" :aria-label="`查看习惯详情:${h.name}`" @click="openHabitDetail(h, $event.currentTarget as HTMLElement)" @keydown.enter.prevent="openHabitDetail(h, $event.currentTarget as HTMLElement)" @keydown.space.prevent="openHabitDetail(h, $event.currentTarget as HTMLElement)">
|
||||
<span class="habit-name">{{ h.name }}</span>
|
||||
@@ -581,7 +578,7 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<div v-if="!habits.length && !busy" class="empty-panel">还没有习惯,从一件容易坚持的小事开始。</div>
|
||||
<div v-if="!visibleHabits.length && !busy" class="empty-panel">{{ !showCompleted && habits.length ? '已完成的习惯已隐藏。' : '还没有习惯,从一件容易坚持的小事开始。' }}</div>
|
||||
<button class="archived-toggle" type="button" @click="toggleArchivedHabits"><ArchiveRestore/>已归档({{ archivedHabits.length }})</button>
|
||||
<div v-if="showArchivedHabits" class="archived-habits">
|
||||
<button v-for="h in archivedHabits" :key="h.id" type="button" @click="openHabitDetail(h, $event.currentTarget as HTMLElement)"><span>{{ h.name }}</span><small>查看详情</small></button>
|
||||
|
||||
@@ -373,24 +373,44 @@ describe('task and habit row decoration', () => {
|
||||
expect(app).toContain('if (restoredNavigation.view === \'tasks\' && restoredNavigation.listId)')
|
||||
})
|
||||
|
||||
it('restores the completed-item visibility choices from local storage', () => {
|
||||
it('uses one positive completed-item preference across task and habit views', () => {
|
||||
expect(app).toContain("readStoredBoolean(window.localStorage, SHOW_COMPLETED_STORAGE_KEY, true)")
|
||||
expect(app).toContain("writeStoredBoolean(window.localStorage, SHOW_COMPLETED_STORAGE_KEY, value)")
|
||||
expect(mvpPanel).toContain("readStoredBoolean(window.localStorage, HIDE_COMPLETED_HABITS_STORAGE_KEY, false)")
|
||||
expect(mvpPanel).toContain("writeStoredBoolean(window.localStorage, HIDE_COMPLETED_HABITS_STORAGE_KEY, value)")
|
||||
expect(app).toContain(':show-completed="showCompleted"')
|
||||
expect(app).toContain('@update:show-completed="showCompleted = $event"')
|
||||
expect(mvpPanel).toContain('showCompleted: boolean')
|
||||
expect(mvpPanel).toContain("'update:showCompleted': [value: boolean]")
|
||||
expect(mvpPanel).not.toContain('HIDE_COMPLETED_HABITS_STORAGE_KEY')
|
||||
expect(mvpPanel).not.toContain('dodo.hide-completed-habits')
|
||||
})
|
||||
|
||||
it('can hide completed habits in both Today and full habit views', () => {
|
||||
expect(mvpPanel).toContain("const hideCompletedHabits = ref(readStoredBoolean(window.localStorage, HIDE_COMPLETED_HABITS_STORAGE_KEY, false))")
|
||||
expect(mvpPanel).toContain('const visibleTodayHabits = computed')
|
||||
expect(mvpPanel).toContain('const visibleHabits = computed')
|
||||
expect(mvpPanel).toContain('v-model="hideCompletedHabits"')
|
||||
expect(mvpPanel).toContain('隐藏已完成')
|
||||
it('shows one positive switch before Today content and no switch inside today habits', () => {
|
||||
expect(app).toContain('class="list-toolbar today-completed-toolbar"')
|
||||
expect(app.indexOf('class="list-toolbar today-completed-toolbar"')).toBeLessThan(app.indexOf('class="today-board"'))
|
||||
expect(app).toContain('<input v-model="showCompleted" type="checkbox"> 显示已完成')
|
||||
const todayHabits = mvpPanel.slice(mvpPanel.indexOf('<!-- 今日习惯'), mvpPanel.indexOf('<!-- 完整习惯列表'))
|
||||
expect(todayHabits).not.toContain('type="checkbox"')
|
||||
expect(todayHabits).not.toContain('显示已完成')
|
||||
expect(todayHabits).not.toContain('隐藏已完成')
|
||||
})
|
||||
|
||||
it('filters habits with the shared preference and disables sorting while completed items are hidden', () => {
|
||||
expect(mvpPanel).toContain('const visibleTodayHabits = computed(() => props.showCompleted ? todayHabits.value')
|
||||
expect(mvpPanel).toContain('const visibleHabits = computed(() => props.showCompleted ? habits.value')
|
||||
expect(mvpPanel).toContain('v-for="h in visibleTodayHabits"')
|
||||
expect(mvpPanel).toContain('v-for="h in visibleHabits"')
|
||||
expect(mvpPanel).toContain(':disabled="!showCompleted"')
|
||||
expect(mvpPanel).toContain('if (busy.value || !props.showCompleted) return')
|
||||
expect(css).toContain('.habit-toolbar{')
|
||||
})
|
||||
|
||||
it('distinguishes hidden completed items from true and search empty states', () => {
|
||||
expect(app).toContain('const hiddenCompletedTaskCount = ref(0)')
|
||||
expect(app).toContain("query ? '没有匹配的任务' : hiddenCompletedTaskCount > 0 ? '已完成任务已隐藏' : '这里还很安静'")
|
||||
expect(mvpPanel).toContain("!showCompleted && todayHabits.length ? '已完成的习惯已隐藏。'")
|
||||
expect(mvpPanel).toContain("!showCompleted && habits.length ? '已完成的习惯已隐藏。'")
|
||||
})
|
||||
|
||||
it('shows unfinished overdue tasks as a separate list inside Today', () => {
|
||||
expect(app).toContain('const overdueTasks = ref<Task[]>([])')
|
||||
expect(app).toContain("params.set('due_to', isoAtLocalDayOffset(0))")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from datetime import UTC, date, datetime, timedelta
|
||||
from datetime import date, timedelta
|
||||
|
||||
from tests.test_mvp_backend import boot
|
||||
from tests.test_mvp_backend import boot, local_today
|
||||
|
||||
|
||||
def create_habit(client, **overrides):
|
||||
@@ -97,7 +97,7 @@ def test_habit_update_rejects_non_finite_numeric_target_and_max_value(client):
|
||||
def test_habit_log_post_rejects_non_finite_value(client):
|
||||
boot(client)
|
||||
habit = create_habit(client).json()
|
||||
day = datetime.now(UTC).date().isoformat()
|
||||
day = local_today().isoformat()
|
||||
|
||||
for value in ("NaN", "Infinity", "-Infinity"):
|
||||
response = client.post(
|
||||
@@ -110,7 +110,7 @@ def test_habit_log_post_rejects_non_finite_value(client):
|
||||
def test_habit_log_put_rejects_non_finite_value(client):
|
||||
boot(client)
|
||||
habit = create_habit(client).json()
|
||||
day = datetime.now(UTC).date().isoformat()
|
||||
day = local_today().isoformat()
|
||||
|
||||
for value in ("NaN", "Infinity", "-Infinity"):
|
||||
response = client.put(
|
||||
@@ -147,7 +147,7 @@ def test_archived_habit_rejects_edit_logs_pause_and_active_permanent_delete(clie
|
||||
|
||||
habit = create_habit(client).json()
|
||||
hid = habit["id"]
|
||||
day = datetime.now(UTC).date().isoformat()
|
||||
day = local_today().isoformat()
|
||||
assert client.put(f"/api/v1/habits/{hid}/logs/{day}", json={"value": 1}).status_code == 200
|
||||
assert client.delete(f"/api/v1/habits/{hid}").status_code == 204
|
||||
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from datetime import datetime, timedelta
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from sqlalchemy import event
|
||||
|
||||
BUSINESS_TIME_ZONE = ZoneInfo("Asia/Shanghai")
|
||||
|
||||
|
||||
def local_today():
|
||||
return datetime.now(BUSINESS_TIME_ZONE).date()
|
||||
|
||||
|
||||
def boot(client):
|
||||
response = client.post(
|
||||
@@ -299,12 +306,12 @@ def test_habits_numeric_accumulation_pause_archive_grid_and_stats(client):
|
||||
)
|
||||
assert habit.status_code == 201
|
||||
habit_id = habit.json()["id"]
|
||||
today = datetime.now(UTC).date().isoformat()
|
||||
today = local_today().isoformat()
|
||||
for value in (6, 7):
|
||||
assert client.post(f"/api/v1/habits/{habit_id}/logs", json={"day": today, "value": value}).status_code == 200
|
||||
assert client.get(f"/api/v1/habits/{habit_id}/logs").json()[0]["value"] == 10
|
||||
assert client.put(f"/api/v1/habits/{habit_id}/logs/{today}", json={"value": 8}).json()["value"] == 8
|
||||
yesterday = (datetime.now(UTC).date() - timedelta(days=1)).isoformat()
|
||||
yesterday = (local_today() - timedelta(days=1)).isoformat()
|
||||
assert client.post(f"/api/v1/habits/{habit_id}/pauses", json={"start_date": yesterday, "end_date": today}).status_code == 201
|
||||
grid = client.get("/api/v1/habits/grid", params={"week": yesterday}).json()
|
||||
assert len(grid["days"]) == 7 and grid["habits"][0]["cells"]
|
||||
@@ -328,7 +335,7 @@ def test_habit_permanent_delete_removes_habit_and_history(client):
|
||||
json={"name": "待删除习惯", "kind": "numeric", "target": 3, "schedule_type": "daily"},
|
||||
)
|
||||
habit_id = created.json()["id"]
|
||||
today = datetime.now(UTC).date().isoformat()
|
||||
today = local_today().isoformat()
|
||||
assert client.put(f"/api/v1/habits/{habit_id}/logs/{today}", json={"value": 2}).status_code == 200
|
||||
|
||||
assert client.delete(f"/api/v1/habits/{habit_id}").status_code == 204
|
||||
@@ -346,7 +353,7 @@ def test_boolean_interval_habit_schedule(client):
|
||||
)
|
||||
assert habit.status_code == 201
|
||||
assert client.post(
|
||||
f"/api/v1/habits/{habit.json()['id']}/logs", json={"day": datetime.now(UTC).date().isoformat(), "value": 1}
|
||||
f"/api/v1/habits/{habit.json()['id']}/logs", json={"day": local_today().isoformat(), "value": 1}
|
||||
).json()["value"] == 1
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user