feat(board): 支持批量添加与批量删除任务
- 批量添加面板:一行一条,整批共用目标列、优先级、标签与截止日期 - 快速新增支持粘贴多行文本,直接拆成多条任务 - 多选模式支持全选可见任务、整批删除,撤销时连原顺序一起还原 - 批量创建与删除走 store 的批量方法,一次落盘、一次广播 - 补充单元测试与 E2E 用例
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
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.tasks).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user