feat: 回收站、批量编辑、合并导入与标签治理
- 数据:Task 增加 deletedAt,schema v2→v3;删除改为软删除,保留 30 天后自动清理 - 回收站:支持恢复、永久删除、二次确认清空;导航栏入口带数量角标 - 批量编辑:多选后统一移动列、改优先级、标签增删、设置截止日、归档 - 合并导入:按 id 匹配、updatedAt 新者胜;覆盖/合并二选一,执行前自动快照 - 标签治理:全局重命名、合并、删除;回收站任务不参与改写 - 看板语义读取统一改用 activeTasks,避免已删除任务泄漏到统计、今日与标签下拉 - UI:回收站图标与备份统一尺寸;批量编辑条统一控件高度并分组 - 测试:705 个单测通过,补充迁移、回收站、批量编辑、合并与标签用例
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
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 TagManagerPanel from './TagManagerPanel.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(TagManagerPanel, {
|
||||
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() === text)
|
||||
}
|
||||
|
||||
describe('TagManagerPanel 展示', () => {
|
||||
beforeEach(() => setActivePinia(createPinia()))
|
||||
|
||||
it('没有标签时显示空状态', () => {
|
||||
const { wrapper } = setup()
|
||||
expect(wrapper.text()).toContain('还没有任何标签')
|
||||
})
|
||||
|
||||
it('列出标签与使用数量', () => {
|
||||
const { wrapper } = setup((store) => {
|
||||
store.createTask({ title: 'A', tags: ['前端'] })
|
||||
store.createTask({ title: 'B', tags: ['前端'] })
|
||||
})
|
||||
|
||||
expect(wrapper.text()).toContain('#前端')
|
||||
expect(wrapper.text()).toContain('2 个任务')
|
||||
})
|
||||
})
|
||||
|
||||
describe('TagManagerPanel 重命名', () => {
|
||||
beforeEach(() => setActivePinia(createPinia()))
|
||||
|
||||
it('重命名后所有任务同步', async () => {
|
||||
const { store, wrapper } = setup((store) => {
|
||||
store.createTask({ title: 'A', tags: ['前端'] })
|
||||
store.createTask({ title: 'B', tags: ['前端', '设计'] })
|
||||
})
|
||||
|
||||
const input = wrapper.find('input[aria-label="重命名标签 前端"]')
|
||||
await input.setValue('Frontend')
|
||||
await buttonByText(wrapper, '保存')?.trigger('click')
|
||||
|
||||
expect([...store.allTags].sort()).toEqual(['Frontend', '设计'].sort())
|
||||
expect(wrapper.text()).toContain('#Frontend')
|
||||
expect(wrapper.text()).not.toContain('#前端')
|
||||
})
|
||||
|
||||
it('清空输入后保存不改动标签', async () => {
|
||||
const { store, wrapper } = setup((store) => {
|
||||
store.createTask({ title: 'A', tags: ['前端'] })
|
||||
})
|
||||
|
||||
await wrapper.find('input[aria-label="重命名标签 前端"]').setValue(' ')
|
||||
await buttonByText(wrapper, '保存')?.trigger('click')
|
||||
|
||||
expect(store.allTags).toEqual(['前端'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('TagManagerPanel 合并与删除', () => {
|
||||
beforeEach(() => setActivePinia(createPinia()))
|
||||
|
||||
it('合并两个标签后去重', async () => {
|
||||
const { store, wrapper } = setup((store) => {
|
||||
store.createTask({ title: 'A', tags: ['前端', '设计'] })
|
||||
store.createTask({ title: 'B', tags: ['设计'] })
|
||||
})
|
||||
|
||||
await wrapper.find('select[aria-label="把标签 前端 合并到"]').setValue('设计')
|
||||
await buttonByText(wrapper, '合并')?.trigger('click')
|
||||
|
||||
expect(store.allTags).toEqual(['设计'])
|
||||
expect(store.activeTasks.every((task) => !task.tags.includes('前端'))).toBe(true)
|
||||
})
|
||||
|
||||
it('删除标签需要二次确认,且不删除任务', async () => {
|
||||
const { store, wrapper } = setup((store) => {
|
||||
store.createTask({ title: 'A', tags: ['前端'] })
|
||||
})
|
||||
|
||||
await buttonByText(wrapper, '删除')?.trigger('click')
|
||||
expect(store.allTags).toEqual(['前端'])
|
||||
|
||||
await buttonByText(wrapper, '确认删除')?.trigger('click')
|
||||
|
||||
expect(store.allTags).toEqual([])
|
||||
expect(store.activeTasks).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('取消删除保留标签', async () => {
|
||||
const { store, wrapper } = setup((store) => {
|
||||
store.createTask({ title: 'A', tags: ['前端'] })
|
||||
})
|
||||
|
||||
await buttonByText(wrapper, '删除')?.trigger('click')
|
||||
await buttonByText(wrapper, '取消')?.trigger('click')
|
||||
|
||||
expect(store.allTags).toEqual(['前端'])
|
||||
expect(wrapper.text()).not.toContain('确认删除')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user