Files
Kanban/src/components/BatchAddDrawer.spec.ts
T
chenshoulin 65e98199ba feat: 回收站、批量编辑、合并导入与标签治理
- 数据:Task 增加 deletedAt,schema v2→v3;删除改为软删除,保留 30 天后自动清理
- 回收站:支持恢复、永久删除、二次确认清空;导航栏入口带数量角标
- 批量编辑:多选后统一移动列、改优先级、标签增删、设置截止日、归档
- 合并导入:按 id 匹配、updatedAt 新者胜;覆盖/合并二选一,执行前自动快照
- 标签治理:全局重命名、合并、删除;回收站任务不参与改写
- 看板语义读取统一改用 activeTasks,避免已删除任务泄漏到统计、今日与标签下拉
- UI:回收站图标与备份统一尺寸;批量编辑条统一控件高度并分组
- 测试:705 个单测通过,补充迁移、回收站、批量编辑、合并与标签用例
2026-10-19 18:32:42 +08:00

135 lines
4.6 KiB
TypeScript

import { enableAutoUnmount, mount } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import { afterEach, describe, expect, it } from 'vitest'
import { createMemoryTaskRepository } from '@/data/taskRepository'
import { useTaskStore } from '@/stores/tasks'
import { useToastStore } from '@/stores/toast'
import BatchAddDrawer from './BatchAddDrawer.vue'
enableAutoUnmount(afterEach)
function setup(open = true) {
const pinia = createPinia()
setActivePinia(pinia)
const store = useTaskStore()
store.init({ repository: createMemoryTaskRepository(), crossTab: false })
// 抽屉会被 Teleport 到 body,测试中改为就地渲染以便断言
const wrapper = mount(BatchAddDrawer, {
props: { open },
global: { plugins: [pinia], stubs: { teleport: true } },
attachTo: document.body,
})
return { store, wrapper }
}
function buttonByText(wrapper: ReturnType<typeof setup>['wrapper'], text: string) {
return wrapper.findAll('button').find((node) => node.text().includes(text))
}
describe('BatchAddDrawer 录入', () => {
it('打开后显示多行清单输入框', () => {
const { wrapper } = setup()
expect(wrapper.find('textarea[aria-label="批量任务清单"]').exists()).toBe(true)
})
it('按行实时统计将添加的任务数', async () => {
const { wrapper } = setup()
await wrapper.find('textarea').setValue('第一条\n\n- 第二条\n第三条')
expect(buttonByText(wrapper, '个任务')?.text()).toContain('添加 3 个任务')
})
it('清单为空时提交按钮不可用', () => {
const { wrapper } = setup()
const button = buttonByText(wrapper, '添加 0 个任务')
expect(button).toBeDefined()
expect(button?.attributes('disabled')).toBeDefined()
})
it('只有空行时同样不可提交', async () => {
const { wrapper } = setup()
await wrapper.find('textarea').setValue(' \n\n')
expect(buttonByText(wrapper, '添加 0 个任务')?.attributes('disabled')).toBeDefined()
})
})
describe('BatchAddDrawer 提交', () => {
it('按清单顺序创建任务,第一条在最上面', async () => {
const { store, wrapper } = setup()
await wrapper.find('textarea').setValue('第一条\n第二条\n第三条')
await buttonByText(wrapper, '添加 3 个任务')?.trigger('click')
expect(store.byColumn.todo.map((task) => task.title)).toEqual(['第一条', '第二条', '第三条'])
})
it('目标列、优先级、截止日期与标签整批共用', async () => {
const { store, wrapper } = setup()
await wrapper.find('textarea').setValue('第一条\n第二条')
await buttonByText(wrapper, '进行中')?.trigger('click')
await buttonByText(wrapper, '高')?.trigger('click')
await wrapper.find('input[type="date"]').setValue('2026-10-20')
const tagInput = wrapper.find('input[aria-label="添加标签"]')
await tagInput.setValue('前端')
await tagInput.trigger('keydown.enter')
await buttonByText(wrapper, '添加 2 个任务')?.trigger('click')
const created = store.byColumn.doing
expect(created.map((task) => task.title)).toEqual(['第一条', '第二条'])
expect(created[0]?.priority).toBe(3)
expect(created[0]?.dueDate).toBe('2026-10-20')
expect(created[0]?.tags).toEqual(['前端'])
expect(created[1]?.tags).toEqual(['前端'])
})
it('提交后请求关闭并提示数量', async () => {
const { wrapper } = setup()
await wrapper.find('textarea').setValue('A\nB')
await buttonByText(wrapper, '添加 2 个任务')?.trigger('click')
expect(wrapper.emitted('close')).toHaveLength(1)
expect(useToastStore().toasts.at(-1)?.message).toBe('已添加 2 个任务')
})
it('取消不创建任何任务', async () => {
const { store, wrapper } = setup()
await wrapper.find('textarea').setValue('A\nB')
await buttonByText(wrapper, '取消')?.trigger('click')
expect(store.tasks).toHaveLength(0)
expect(wrapper.emitted('close')).toHaveLength(1)
})
it('再次打开时清空上一次的残留', async () => {
const { wrapper } = setup()
await wrapper.find('textarea').setValue('上一次的清单')
await wrapper.setProps({ open: false })
await wrapper.setProps({ open: true })
expect((wrapper.find('textarea').element as HTMLTextAreaElement).value).toBe('')
})
it('误提交时可以整批撤销', async () => {
const { store, wrapper } = setup()
await wrapper.find('textarea').setValue('A\nB')
await buttonByText(wrapper, '添加 2 个任务')?.trigger('click')
const action = useToastStore().toasts.at(-1)?.action
expect(action?.label).toBe('撤销')
action?.run()
expect(store.activeTasks).toHaveLength(0)
})
})