feat(board): 支持批量添加与批量删除任务
- 批量添加面板:一行一条,整批共用目标列、优先级、标签与截止日期 - 快速新增支持粘贴多行文本,直接拆成多条任务 - 多选模式支持全选可见任务、整批删除,撤销时连原顺序一起还原 - 批量创建与删除走 store 的批量方法,一次落盘、一次广播 - 补充单元测试与 E2E 用例
This commit is contained in:
@@ -4,6 +4,7 @@ import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { createMemoryTaskRepository } from '@/data/taskRepository'
|
||||
import { useTaskStore } from '@/stores/tasks'
|
||||
import { useToastStore } from '@/stores/toast'
|
||||
import type { ColumnId } from '@/types/task'
|
||||
|
||||
import QuickAddTask from './QuickAddTask.vue'
|
||||
@@ -98,3 +99,104 @@ describe('QuickAddTask', () => {
|
||||
expect(wrapper.find('input').attributes('aria-label')).toBe('添加任务')
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* 粘贴多行文本 = 批量添加。
|
||||
* 这里手工构造 paste 事件:jsdom 没有可用的 Clipboard API,
|
||||
* 而组件只依赖 `clipboardData.getData('text')` 这一小块接口。
|
||||
*/
|
||||
function paste(wrapper: ReturnType<typeof setup>['wrapper'], text: string): boolean {
|
||||
const event = new Event('paste', { bubbles: true, cancelable: true })
|
||||
Object.defineProperty(event, 'clipboardData', { value: { getData: () => text } })
|
||||
wrapper.find('input').element.dispatchEvent(event)
|
||||
return event.defaultPrevented
|
||||
}
|
||||
|
||||
describe('QuickAddTask 批量粘贴', () => {
|
||||
it('粘贴多行文本创建多条任务,并保持粘贴顺序', () => {
|
||||
const { store, wrapper } = setup()
|
||||
|
||||
expect(paste(wrapper, '写周报\n评审 PR\n回归测试')).toBe(true)
|
||||
|
||||
expect(store.byColumn.todo.map((task) => task.title)).toEqual(['写周报', '评审 PR', '回归测试'])
|
||||
})
|
||||
|
||||
it('粘贴的多条任务位于已有任务之前', () => {
|
||||
const { store, wrapper } = setup()
|
||||
store.createTask({ title: '已有任务' })
|
||||
|
||||
paste(wrapper, '第一条\n第二条')
|
||||
|
||||
expect(store.byColumn.todo.map((task) => task.title)).toEqual(['第一条', '第二条', '已有任务'])
|
||||
})
|
||||
|
||||
it('粘贴到指定列(进行中)', () => {
|
||||
const { store, wrapper } = setup('doing')
|
||||
|
||||
paste(wrapper, 'A\nB')
|
||||
|
||||
expect(store.byColumn.doing.map((task) => task.title)).toEqual(['A', 'B'])
|
||||
expect(store.byColumn.todo).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('单行粘贴不拦截,交回浏览器默认行为', () => {
|
||||
const { store, wrapper } = setup()
|
||||
|
||||
expect(paste(wrapper, '单个任务')).toBe(false)
|
||||
expect(store.tasks).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('只有空行时不创建任务,也不拦截粘贴', () => {
|
||||
const { store, wrapper } = setup()
|
||||
|
||||
expect(paste(wrapper, ' \n\n')).toBe(false)
|
||||
expect(store.tasks).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('粘贴时忽略空行与列表符号', () => {
|
||||
const { store, wrapper } = setup()
|
||||
|
||||
paste(wrapper, '- 写周报\n\n- 评审 PR\n')
|
||||
|
||||
expect(store.byColumn.todo.map((task) => task.title)).toEqual(['写周报', '评审 PR'])
|
||||
})
|
||||
|
||||
it('粘贴前已输入的草稿并入第一条,不丢失也不残留', async () => {
|
||||
const { store, wrapper } = setup()
|
||||
const input = wrapper.find('input')
|
||||
await input.setValue('手写的一条')
|
||||
|
||||
paste(wrapper, '粘贴一\n粘贴二')
|
||||
|
||||
expect(store.byColumn.todo.map((task) => task.title)).toEqual([
|
||||
'手写的一条',
|
||||
'粘贴一',
|
||||
'粘贴二',
|
||||
])
|
||||
|
||||
// v-model 的写回要等一轮视图更新
|
||||
await wrapper.vm.$nextTick()
|
||||
expect((input.element as HTMLInputElement).value).toBe('')
|
||||
})
|
||||
|
||||
it('批量粘贴后弹出提示并保持聚焦,便于继续录入', () => {
|
||||
const { wrapper } = setup()
|
||||
|
||||
paste(wrapper, 'A\nB')
|
||||
|
||||
const toast = useToastStore()
|
||||
expect(toast.toasts.at(-1)?.message).toBe('已批量添加 2 个任务')
|
||||
expect(document.activeElement).toBe(wrapper.find('input').element)
|
||||
})
|
||||
|
||||
it('误粘贴时可以整批撤销', () => {
|
||||
const { store, wrapper } = setup()
|
||||
paste(wrapper, 'A\nB')
|
||||
|
||||
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