import { mount } from '@vue/test-utils' import { createPinia, setActivePinia } from 'pinia' 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' function setup(status: ColumnId = 'todo') { const pinia = createPinia() setActivePinia(pinia) const store = useTaskStore() store.init({ repository: createMemoryTaskRepository(), crossTab: false }) const wrapper = mount(QuickAddTask, { props: { status }, global: { plugins: [pinia] }, attachTo: document.body, }) return { store, wrapper } } describe('QuickAddTask', () => { it('提交后创建任务到指定列并清空输入', async () => { const { store, wrapper } = setup('doing') const input = wrapper.find('input') await input.setValue('新任务') await wrapper.find('form').trigger('submit') expect(store.tasks.map((task) => task.title)).toEqual(['新任务']) expect(store.byColumn.doing.map((task) => task.title)).toEqual(['新任务']) expect((input.element as HTMLInputElement).value).toBe('') }) it('提交后保持聚焦,便于连续录入', async () => { const { wrapper } = setup() const input = wrapper.find('input') await input.setValue('第一条') await wrapper.find('form').trigger('submit') expect(document.activeElement).toBe(input.element) }) it('连续录入两条,后者位于列首', async () => { const { store, wrapper } = setup() const input = wrapper.find('input') await input.setValue('第一条') await wrapper.find('form').trigger('submit') await input.setValue('第二条') await wrapper.find('form').trigger('submit') expect(store.byColumn.todo.map((task) => task.title)).toEqual(['第二条', '第一条']) }) it('空白输入不创建任务', async () => { const { store, wrapper } = setup() const input = wrapper.find('input') await input.setValue(' ') await wrapper.find('form').trigger('submit') expect(store.tasks).toHaveLength(0) await wrapper.find('form').trigger('submit') expect(store.tasks).toHaveLength(0) }) it('输入法组合期间回车不提交(中文选词不误触发)', async () => { const { store, wrapper } = setup() const input = wrapper.find('input') await input.trigger('compositionstart') await input.setValue('zhongwen') await wrapper.find('form').trigger('submit') expect(store.tasks).toHaveLength(0) await input.trigger('compositionend') await input.setValue('中文') await wrapper.find('form').trigger('submit') expect(store.tasks.map((task) => task.title)).toEqual(['中文']) }) it('标题首尾空白被去除后入库', async () => { const { store, wrapper } = setup() await wrapper.find('input').setValue(' 写方案 ') await wrapper.find('form').trigger('submit') expect(store.tasks[0]?.title).toBe('写方案') }) it('输入框带可访问性标签', () => { const { wrapper } = setup() expect(wrapper.find('input').attributes('aria-label')).toBe('添加任务') }) }) /** * 粘贴多行文本 = 批量添加。 * 这里手工构造 paste 事件:jsdom 没有可用的 Clipboard API, * 而组件只依赖 `clipboardData.getData('text')` 这一小块接口。 */ function paste(wrapper: ReturnType['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.activeTasks).toHaveLength(0) }) })