时间展示(ADR-008): - 详情抽屉新增只读「时间」区块:创建 / 更新常驻,完成时间缺失时标注「未完成」,归档任务追加归档时间;时间不进入草稿与 dirty 判定 - 已完成卡片在元信息行右侧显示「完成于 M月D日」,卡片根元素 title 汇总创建 / 完成 / 归档时间(精确到分钟) - 回收站条目补创建时间,删掉内联 formatTime,统一走 @/utils/date - date.ts 新增 formatDateTime(YYYY-MM-DD HH:mm)与 formatCompactDate(同年 M月D日 / 跨年带年份),空值与非法时间戳返回 null 逾期基准修复: - 新增 formatDueDateForTask:未完成任务仍以「今天」推算;已完成任务改以「完成日」推算,19 号截止、19 号当天完成的任务不会在 20 号显示「逾期 1 天」,完成时确实超期则定格为「逾期 N 天完成」,不再随日期增长 - 新增 localDateOf:completedAt 存 UTC 时间戳,按本地时区取日期,避免跨日偏差 - TaskCard 改用 dueLabel computed 取代模板内直接调用 formatDueDate;颜色仍由 isOverdue 决定,已完成卡片保持中性色 测试:date / TaskCard 补时间格式化与逾期基准用例(当天完成、提前完成、完成时超期定格、缺 completedAt、跨年),全量 728 单测通过;vue-tsc 与 eslint 干净。
119 lines
3.9 KiB
TypeScript
119 lines
3.9 KiB
TypeScript
import { mount } from '@vue/test-utils'
|
|
import type { VueWrapper } from '@vue/test-utils'
|
|
import { createPinia, setActivePinia } from 'pinia'
|
|
import { beforeEach, describe, expect, it } from 'vitest'
|
|
|
|
import { createMemoryTaskRepository } from '@/data/taskRepository'
|
|
import { useTaskStore } from '@/stores/tasks'
|
|
|
|
import TrashPanel from './TrashPanel.vue'
|
|
|
|
function setup(prepare?: (store: ReturnType<typeof useTaskStore>) => void) {
|
|
const pinia = createPinia()
|
|
setActivePinia(pinia)
|
|
const store = useTaskStore()
|
|
store.init({ repository: createMemoryTaskRepository(), crossTab: false })
|
|
prepare?.(store)
|
|
|
|
const wrapper = mount(TrashPanel, {
|
|
props: { open: true },
|
|
global: { plugins: [pinia], stubs: { teleport: true } },
|
|
})
|
|
|
|
return { store, wrapper }
|
|
}
|
|
|
|
function buttonByText(wrapper: VueWrapper, text: string) {
|
|
return wrapper.findAll('button').find((node) => node.text().includes(text))
|
|
}
|
|
|
|
describe('TrashPanel 展示', () => {
|
|
beforeEach(() => setActivePinia(createPinia()))
|
|
|
|
it('回收站为空时显示空状态', () => {
|
|
const { wrapper } = setup()
|
|
expect(wrapper.text()).toContain('回收站是空的')
|
|
})
|
|
|
|
it('列出标题、删除时间与原列,并统计数量', () => {
|
|
const { wrapper } = setup((store) => {
|
|
const task = store.createTask({ title: '被删除的任务', status: 'doing' })
|
|
store.removeTask(task?.id ?? '')
|
|
})
|
|
|
|
expect(wrapper.text()).toContain('被删除的任务')
|
|
expect(wrapper.text()).toContain('原列 进行中')
|
|
expect(wrapper.text()).toContain('删除于')
|
|
expect(wrapper.text()).toContain('创建于')
|
|
expect(wrapper.text()).toContain('共 1 个任务')
|
|
})
|
|
|
|
it('已归档的删除项带「归档」徽标', () => {
|
|
const { wrapper } = setup((store) => {
|
|
const task = store.createTask({ title: '归档任务', status: 'done' })
|
|
store.archiveTasks([task?.id ?? ''])
|
|
store.removeTask(task?.id ?? '')
|
|
})
|
|
|
|
expect(wrapper.text()).toContain('归档')
|
|
})
|
|
})
|
|
|
|
describe('TrashPanel 操作', () => {
|
|
beforeEach(() => setActivePinia(createPinia()))
|
|
|
|
it('恢复后回到看板,回收站变空', async () => {
|
|
const { store, wrapper } = setup((store) => {
|
|
const task = store.createTask({ title: '可恢复' })
|
|
store.removeTask(task?.id ?? '')
|
|
})
|
|
|
|
await buttonByText(wrapper, '恢复')?.trigger('click')
|
|
|
|
expect(store.activeTasks).toHaveLength(1)
|
|
expect(store.trashTasks).toHaveLength(0)
|
|
expect(wrapper.text()).toContain('回收站是空的')
|
|
})
|
|
|
|
it('永久删除后数据真正消失', async () => {
|
|
const { store, wrapper } = setup((store) => {
|
|
const task = store.createTask({ title: '永久删除' })
|
|
store.removeTask(task?.id ?? '')
|
|
})
|
|
|
|
await buttonByText(wrapper, '永久删除')?.trigger('click')
|
|
|
|
expect(store.tasks).toHaveLength(0)
|
|
expect(wrapper.text()).toContain('回收站是空的')
|
|
})
|
|
|
|
it('清空回收站需要二次确认', async () => {
|
|
const { store, wrapper } = setup((store) => {
|
|
const a = store.createTask({ title: 'A' })
|
|
const b = store.createTask({ title: 'B' })
|
|
store.removeTasks([a?.id ?? '', b?.id ?? ''])
|
|
})
|
|
|
|
await buttonByText(wrapper, '清空回收站')?.trigger('click')
|
|
expect(wrapper.text()).toContain('此操作不可撤销')
|
|
expect(store.tasks).toHaveLength(2)
|
|
|
|
await buttonByText(wrapper, '确认清空')?.trigger('click')
|
|
expect(store.tasks).toHaveLength(0)
|
|
expect(wrapper.text()).toContain('回收站是空的')
|
|
})
|
|
|
|
it('取消清空不会删除数据', async () => {
|
|
const { store, wrapper } = setup((store) => {
|
|
const task = store.createTask({ title: 'A' })
|
|
store.removeTask(task?.id ?? '')
|
|
})
|
|
|
|
await buttonByText(wrapper, '清空回收站')?.trigger('click')
|
|
await buttonByText(wrapper, '取消')?.trigger('click')
|
|
|
|
expect(store.trashTasks).toHaveLength(1)
|
|
expect(wrapper.text()).not.toContain('此操作不可撤销')
|
|
})
|
|
})
|