- 批量添加面板:一行一条,整批共用目标列、优先级、标签与截止日期 - 快速新增支持粘贴多行文本,直接拆成多条任务 - 多选模式支持全选可见任务、整批删除,撤销时连原顺序一起还原 - 批量创建与删除走 store 的批量方法,一次落盘、一次广播 - 补充单元测试与 E2E 用例
783 lines
26 KiB
TypeScript
783 lines
26 KiB
TypeScript
import { enableAutoUnmount, mount } from '@vue/test-utils'
|
|
import dayjs from 'dayjs'
|
|
import { createPinia, setActivePinia } from 'pinia'
|
|
import { nextTick } from 'vue'
|
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
|
|
import BoardColumn from '@/components/BoardColumn.vue'
|
|
import { createMemoryTaskRepository } from '@/data/taskRepository'
|
|
import { useFilterStore } from '@/stores/filters'
|
|
import { useTaskStore } from '@/stores/tasks'
|
|
import { useToastStore } from '@/stores/toast'
|
|
import { useUiStore } from '@/stores/ui'
|
|
|
|
import BoardView from './BoardView.vue'
|
|
|
|
// BoardView 会在 document 上注册快捷键监听,必须逐个卸载避免相互干扰
|
|
enableAutoUnmount(afterEach)
|
|
|
|
function setup() {
|
|
const pinia = createPinia()
|
|
setActivePinia(pinia)
|
|
const store = useTaskStore()
|
|
store.init({ repository: createMemoryTaskRepository(), crossTab: false })
|
|
return { pinia, store }
|
|
}
|
|
|
|
function mountBoard(pinia: ReturnType<typeof createPinia>) {
|
|
// 抽屉内容会被 Teleport 到 body,测试中改为就地渲染以便断言
|
|
return mount(BoardView, {
|
|
global: { plugins: [pinia], stubs: { teleport: true } },
|
|
attachTo: document.body,
|
|
})
|
|
}
|
|
|
|
describe('BoardView 结构', () => {
|
|
it('渲染三个固定列', () => {
|
|
const { pinia } = setup()
|
|
const headings = mountBoard(pinia)
|
|
.findAll('h2')
|
|
.map((node) => node.text())
|
|
expect(headings).toEqual(['待办', '进行中', '已完成'])
|
|
})
|
|
|
|
it('空看板显示空状态', () => {
|
|
const { pinia } = setup()
|
|
const wrapper = mountBoard(pinia)
|
|
expect(wrapper.findAll('article')).toHaveLength(0)
|
|
expect(wrapper.text()).toContain('暂无任务')
|
|
})
|
|
|
|
it('每列显示任务计数', () => {
|
|
const { pinia, store } = setup()
|
|
store.createTask({ title: '待办事项' })
|
|
store.createTask({ title: '进行中事项', status: 'doing' })
|
|
|
|
const counters = mountBoard(pinia)
|
|
.findAll('[data-role="column-count"]')
|
|
.map((node) => node.text())
|
|
expect(counters).toEqual(['1', '1', '0'])
|
|
})
|
|
|
|
it('渲染任务标题并显示总数', () => {
|
|
const { pinia, store } = setup()
|
|
store.createTask({ title: '写方案' })
|
|
store.createTask({ title: '做原型', status: 'doing' })
|
|
|
|
const wrapper = mountBoard(pinia)
|
|
expect(wrapper.text()).toContain('写方案')
|
|
expect(wrapper.text()).toContain('做原型')
|
|
expect(wrapper.text()).toContain('共 2 个任务')
|
|
})
|
|
|
|
it('任务按列归位', () => {
|
|
const { pinia, store } = setup()
|
|
store.createTask({ title: '待办事项' })
|
|
store.createTask({ title: '进行中事项', status: 'doing' })
|
|
store.createTask({ title: '已完成事项', status: 'done' })
|
|
|
|
const sections = mountBoard(pinia).findAll('section')
|
|
expect(sections[0]?.text()).toContain('待办事项')
|
|
expect(sections[0]?.text()).not.toContain('进行中事项')
|
|
expect(sections[1]?.text()).toContain('进行中事项')
|
|
expect(sections[2]?.text()).toContain('已完成事项')
|
|
})
|
|
|
|
it('存在存储提示时由 App 统一展示(看板不再重复渲染)', () => {
|
|
const { pinia, store } = setup()
|
|
store.notice = '本地存储不可用'
|
|
// 提示已提升到 App 层,BoardView 自身不应再渲染一遍
|
|
expect(mountBoard(pinia).text()).not.toContain('本地存储不可用')
|
|
})
|
|
})
|
|
|
|
describe('BoardView 端到端交互', () => {
|
|
it('在界面上输入并回车即可新增任务', async () => {
|
|
const { pinia, store } = setup()
|
|
const wrapper = mountBoard(pinia)
|
|
|
|
const firstColumnInput = wrapper.findAll('section')[0]?.find('input')
|
|
expect(firstColumnInput).toBeDefined()
|
|
|
|
await firstColumnInput?.setValue('界面新增的任务')
|
|
await wrapper.findAll('form')[0]?.trigger('submit')
|
|
|
|
expect(store.byColumn.todo.map((task) => task.title)).toEqual(['界面新增的任务'])
|
|
expect(wrapper.text()).toContain('界面新增的任务')
|
|
})
|
|
|
|
it('点击完成按钮后卡片移动到已完成列', async () => {
|
|
const { pinia, store } = setup()
|
|
store.createTask({ title: '待办任务' })
|
|
const wrapper = mountBoard(pinia)
|
|
|
|
await wrapper.find('article button').trigger('click')
|
|
|
|
expect(store.byColumn.todo).toHaveLength(0)
|
|
expect(store.byColumn.done.map((task) => task.title)).toEqual(['待办任务'])
|
|
expect(wrapper.findAll('section')[2]?.text()).toContain('待办任务')
|
|
})
|
|
|
|
it('再次点击可撤销完成', async () => {
|
|
const { pinia, store } = setup()
|
|
store.createTask({ title: '待办任务' })
|
|
const wrapper = mountBoard(pinia)
|
|
|
|
await wrapper.find('article button').trigger('click')
|
|
await wrapper.find('article button').trigger('click')
|
|
|
|
expect(store.byColumn.todo.map((task) => task.title)).toEqual(['待办任务'])
|
|
expect(store.byColumn.done).toHaveLength(0)
|
|
})
|
|
})
|
|
|
|
describe('BoardView 详情抽屉', () => {
|
|
it('初始不渲染详情表单', () => {
|
|
const { pinia, store } = setup()
|
|
store.createTask({ title: '待办任务' })
|
|
expect(mountBoard(pinia).find('input[aria-label="标题"]').exists()).toBe(false)
|
|
})
|
|
|
|
it('点击卡片打开抽屉并回填标题', async () => {
|
|
const { pinia, store } = setup()
|
|
store.createTask({ title: '待办任务' })
|
|
const wrapper = mountBoard(pinia)
|
|
|
|
await wrapper.find('article').trigger('click')
|
|
await nextTick()
|
|
|
|
expect((wrapper.find('input[aria-label="标题"]').element as HTMLInputElement).value).toBe(
|
|
'待办任务',
|
|
)
|
|
})
|
|
|
|
it('保存后看板同步更新', async () => {
|
|
const { pinia, store } = setup()
|
|
store.createTask({ title: '旧标题' })
|
|
const wrapper = mountBoard(pinia)
|
|
|
|
await wrapper.find('article').trigger('click')
|
|
await nextTick()
|
|
await wrapper.find('input[aria-label="标题"]').setValue('新标题')
|
|
|
|
const saveButton = wrapper.findAll('button').find((node) => node.text() === '保存')
|
|
await saveButton?.trigger('click')
|
|
await nextTick()
|
|
|
|
expect(wrapper.text()).toContain('新标题')
|
|
expect(store.tasks[0]?.title).toBe('新标题')
|
|
})
|
|
|
|
it('有未保存修改时取消先确认,放弃后抽屉关闭且数据不变', async () => {
|
|
const { pinia, store } = setup()
|
|
store.createTask({ title: '原标题' })
|
|
const wrapper = mountBoard(pinia)
|
|
|
|
await wrapper.find('article').trigger('click')
|
|
await nextTick()
|
|
await wrapper.find('input[aria-label="标题"]').setValue('改了一半')
|
|
|
|
const cancelButton = wrapper.findAll('button').find((node) => node.text() === '取消')
|
|
await cancelButton?.trigger('click')
|
|
await nextTick()
|
|
|
|
// 先给出确认,抽屉保持打开
|
|
expect(wrapper.text()).toContain('有未保存的修改')
|
|
expect(wrapper.find('input[aria-label="标题"]').exists()).toBe(true)
|
|
|
|
const discardButton = wrapper.findAll('button').find((node) => node.text() === '放弃修改')
|
|
await discardButton?.trigger('click')
|
|
await nextTick()
|
|
|
|
expect(wrapper.find('input[aria-label="标题"]').exists()).toBe(false)
|
|
expect(store.tasks[0]?.title).toBe('原标题')
|
|
})
|
|
|
|
it('删除后抽屉关闭且任务从看板消失', async () => {
|
|
const { pinia, store } = setup()
|
|
store.createTask({ title: '要删除的任务' })
|
|
const wrapper = mountBoard(pinia)
|
|
|
|
await wrapper.find('article').trigger('click')
|
|
await nextTick()
|
|
|
|
const deleteButton = wrapper.findAll('button').find((node) => node.text() === '删除')
|
|
await deleteButton?.trigger('click')
|
|
await nextTick()
|
|
|
|
expect(store.tasks).toHaveLength(0)
|
|
expect(wrapper.find('input[aria-label="标题"]').exists()).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('BoardView 拖拽接线', () => {
|
|
it('列组件 emit move 时驱动 store 改变状态', async () => {
|
|
const { pinia, store } = setup()
|
|
const task = store.createTask({ title: 'A' })
|
|
const wrapper = mountBoard(pinia)
|
|
|
|
const column = wrapper.findAllComponents(BoardColumn)[0]
|
|
column?.vm.$emit('move', { id: task?.id ?? '', toColumn: 'doing', toIndex: 0 })
|
|
await nextTick()
|
|
|
|
expect(store.byColumn.todo).toHaveLength(0)
|
|
expect(store.byColumn.doing.map((item) => item.title)).toEqual(['A'])
|
|
})
|
|
|
|
it('列内移动事件驱动顺序变化', async () => {
|
|
const { pinia, store } = setup()
|
|
const a = store.createTask({ title: 'A' })
|
|
store.createTask({ title: 'B' })
|
|
store.createTask({ title: 'C' })
|
|
expect(store.byColumn.todo.map((item) => item.title)).toEqual(['C', 'B', 'A'])
|
|
|
|
const wrapper = mountBoard(pinia)
|
|
const column = wrapper.findAllComponents(BoardColumn)[0]
|
|
column?.vm.$emit('move', { id: a?.id ?? '', toColumn: 'todo', toIndex: 0 })
|
|
await nextTick()
|
|
|
|
expect(store.byColumn.todo.map((item) => item.title)).toEqual(['A', 'C', 'B'])
|
|
expect(wrapper.findAll('section')[0]?.text()).toContain('A')
|
|
})
|
|
})
|
|
|
|
describe('BoardView 筛选', () => {
|
|
it('按优先级筛选后只显示匹配卡片,列计数同步变化', async () => {
|
|
const { pinia, store } = setup()
|
|
store.createTask({ title: '高优先级', priority: 3 })
|
|
store.createTask({ title: '低优先级', priority: 1 })
|
|
const wrapper = mountBoard(pinia)
|
|
expect(wrapper.findAll('article')).toHaveLength(2)
|
|
|
|
useFilterStore().togglePriority(3)
|
|
await nextTick()
|
|
|
|
expect(wrapper.findAll('article')).toHaveLength(1)
|
|
expect(wrapper.text()).toContain('高优先级')
|
|
expect(wrapper.text()).not.toContain('低优先级')
|
|
expect(wrapper.findAll('[data-role="column-count"]')[0]?.text()).toBe('1')
|
|
})
|
|
|
|
it('显示命中数量提示', async () => {
|
|
const { pinia, store } = setup()
|
|
store.createTask({ title: 'A', priority: 3 })
|
|
store.createTask({ title: 'B', priority: 1 })
|
|
const wrapper = mountBoard(pinia)
|
|
|
|
useFilterStore().togglePriority(3)
|
|
await nextTick()
|
|
|
|
expect(wrapper.text()).toContain('已隐藏 1 个')
|
|
})
|
|
|
|
it('清空筛选后恢复全部卡片', async () => {
|
|
const { pinia, store } = setup()
|
|
store.createTask({ title: 'A', priority: 3 })
|
|
store.createTask({ title: 'B', priority: 1 })
|
|
const wrapper = mountBoard(pinia)
|
|
|
|
const filters = useFilterStore()
|
|
filters.togglePriority(3)
|
|
await nextTick()
|
|
filters.clear()
|
|
await nextTick()
|
|
|
|
expect(wrapper.findAll('article')).toHaveLength(2)
|
|
expect(wrapper.text()).not.toContain('已隐藏')
|
|
})
|
|
|
|
it('点击「逾期」只显示逾期任务', async () => {
|
|
const { pinia, store } = setup()
|
|
store.createTask({
|
|
title: '逾期任务',
|
|
dueDate: dayjs().subtract(1, 'day').format('YYYY-MM-DD'),
|
|
})
|
|
store.createTask({ title: '未来任务', dueDate: dayjs().add(3, 'day').format('YYYY-MM-DD') })
|
|
const wrapper = mountBoard(pinia)
|
|
|
|
const overdueButton = wrapper.findAll('button').find((node) => node.text().includes('逾期'))
|
|
await overdueButton?.trigger('click')
|
|
await nextTick()
|
|
|
|
expect(wrapper.text()).toContain('逾期任务')
|
|
expect(wrapper.text()).not.toContain('未来任务')
|
|
})
|
|
|
|
it('列内渲染子集时给列组件传 sortDisabled(拖拽保护)', async () => {
|
|
const { pinia, store } = setup()
|
|
store.createTask({ title: 'A' })
|
|
const wrapper = mountBoard(pinia)
|
|
|
|
expect(wrapper.findAllComponents(BoardColumn)[0]?.props('sortDisabled')).toBe(false)
|
|
|
|
useFilterStore().togglePriority(3)
|
|
await nextTick()
|
|
expect(wrapper.findAllComponents(BoardColumn)[0]?.props('sortDisabled')).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('BoardView 归档', () => {
|
|
function archiveButton(wrapper: ReturnType<typeof mountBoard>) {
|
|
return wrapper.findAll('button').find((node) => node.text().includes('个已完成'))
|
|
}
|
|
|
|
async function createArchivable(store: ReturnType<typeof useTaskStore>) {
|
|
const task = store.createTask({ title: '很久前完成的任务', status: 'done' })
|
|
store.updateTask(task?.id ?? '', {
|
|
completedAt: dayjs().subtract(30, 'day').toISOString(),
|
|
})
|
|
return task
|
|
}
|
|
|
|
it('默认不显示归档任务', async () => {
|
|
const { pinia, store } = setup()
|
|
const task = await createArchivable(store)
|
|
store.archiveTasks([task?.id ?? ''])
|
|
|
|
const wrapper = mountBoard(pinia)
|
|
|
|
expect(wrapper.findAll('article')).toHaveLength(0)
|
|
expect(wrapper.text()).toContain('已隐藏 1 个')
|
|
// 提示里要说明是归档造成的,否则用户不知道列内排序为什么拖不动
|
|
expect(wrapper.text()).toContain('归档 1')
|
|
})
|
|
|
|
it('开启「显示归档」后归档任务重新出现', async () => {
|
|
const { pinia, store } = setup()
|
|
const task = await createArchivable(store)
|
|
store.archiveTasks([task?.id ?? ''])
|
|
|
|
const wrapper = mountBoard(pinia)
|
|
const toggle = wrapper.findAll('button').find((node) => node.text() === '显示归档')
|
|
await toggle?.trigger('click')
|
|
await nextTick()
|
|
|
|
expect(wrapper.findAll('article')).toHaveLength(1)
|
|
expect(wrapper.text()).toContain('很久前完成的任务')
|
|
})
|
|
|
|
it('归档后给出可撤销提示,撤销后任务回到看板', async () => {
|
|
const { pinia, store } = setup()
|
|
await createArchivable(store)
|
|
const wrapper = mountBoard(pinia)
|
|
|
|
await archiveButton(wrapper)?.trigger('click')
|
|
await nextTick()
|
|
|
|
expect(store.tasks[0]?.archivedAt).not.toBeNull()
|
|
expect(wrapper.findAll('article')).toHaveLength(0)
|
|
|
|
const toast = useToastStore()
|
|
expect(toast.toasts[0]?.action?.label).toBe('撤销')
|
|
|
|
toast.runAction(toast.toasts[0]?.id ?? '')
|
|
await nextTick()
|
|
|
|
expect(store.tasks[0]?.archivedAt).toBeNull()
|
|
expect(wrapper.findAll('article')).toHaveLength(1)
|
|
})
|
|
|
|
it('隐藏归档时「已完成」列禁用列内排序', async () => {
|
|
const { pinia, store } = setup()
|
|
const task = await createArchivable(store)
|
|
store.archiveTasks([task?.id ?? ''])
|
|
|
|
const wrapper = mountBoard(pinia)
|
|
// 归档任务只可能在「已完成」列
|
|
expect(wrapper.findAllComponents(BoardColumn)[2]?.props('sortDisabled')).toBe(true)
|
|
expect(wrapper.findAllComponents(BoardColumn)[0]?.props('sortDisabled')).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('BoardView 快捷键', () => {
|
|
function pressKey(key: string, target: EventTarget = document): void {
|
|
target.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true, cancelable: true }))
|
|
}
|
|
|
|
it('按 N 聚焦待办列的快速新增输入框', async () => {
|
|
const { pinia } = setup()
|
|
const wrapper = mountBoard(pinia)
|
|
await nextTick()
|
|
|
|
pressKey('n')
|
|
expect(document.activeElement).toBe(wrapper.find('input[aria-label="添加任务"]').element)
|
|
})
|
|
|
|
it('按 / 聚焦搜索框', async () => {
|
|
const { pinia } = setup()
|
|
const wrapper = mountBoard(pinia)
|
|
await nextTick()
|
|
|
|
pressKey('/')
|
|
expect(document.activeElement).toBe(wrapper.find('input[aria-label="搜索任务"]').element)
|
|
})
|
|
|
|
it('在输入框内按 N 不触发快捷键', async () => {
|
|
const { pinia } = setup()
|
|
const wrapper = mountBoard(pinia)
|
|
await nextTick()
|
|
|
|
const search = wrapper.find('input[aria-label="搜索任务"]')
|
|
pressKey('n', search.element)
|
|
|
|
expect(document.activeElement).not.toBe(wrapper.find('input[aria-label="添加任务"]').element)
|
|
})
|
|
|
|
it('应用级面板打开时不响应快捷键', async () => {
|
|
const { pinia } = setup()
|
|
const wrapper = mountBoard(pinia)
|
|
await nextTick()
|
|
|
|
// 帮助面板由 App 渲染,这里只验证看板会让出快捷键
|
|
useUiStore().openHelp()
|
|
await nextTick()
|
|
pressKey('n')
|
|
|
|
expect(document.activeElement).not.toBe(wrapper.find('input[aria-label="添加任务"]').element)
|
|
})
|
|
})
|
|
|
|
describe('BoardView 键盘移动', () => {
|
|
type Direction = 'left' | 'right' | 'up' | 'down'
|
|
|
|
/** 模拟卡片发出 keymove(任意一列转发都会到达 BoardView 的处理函数) */
|
|
function emitKeyMove(
|
|
wrapper: ReturnType<typeof mountBoard>,
|
|
id: string,
|
|
direction: Direction,
|
|
): void {
|
|
wrapper.findAllComponents(BoardColumn)[0]?.vm.$emit('keymove', { id, direction })
|
|
}
|
|
|
|
it('右移把任务移到下一列末尾', async () => {
|
|
const { pinia, store } = setup()
|
|
const task = store.createTask({ title: 'A' })
|
|
const wrapper = mountBoard(pinia)
|
|
|
|
emitKeyMove(wrapper, task?.id ?? '', 'right')
|
|
await nextTick()
|
|
|
|
expect(store.byColumn.todo).toHaveLength(0)
|
|
expect(store.byColumn.doing.map((item) => item.title)).toEqual(['A'])
|
|
})
|
|
|
|
it('左移把任务移回上一列', async () => {
|
|
const { pinia, store } = setup()
|
|
const task = store.createTask({ title: 'A', status: 'doing' })
|
|
const wrapper = mountBoard(pinia)
|
|
|
|
emitKeyMove(wrapper, task?.id ?? '', 'left')
|
|
await nextTick()
|
|
|
|
expect(store.byColumn.doing).toHaveLength(0)
|
|
expect(store.byColumn.todo.map((item) => item.title)).toEqual(['A'])
|
|
})
|
|
|
|
it('最左列再左移无效果', async () => {
|
|
const { pinia, store } = setup()
|
|
const task = store.createTask({ title: 'A' })
|
|
const wrapper = mountBoard(pinia)
|
|
|
|
emitKeyMove(wrapper, task?.id ?? '', 'left')
|
|
await nextTick()
|
|
|
|
expect(store.byColumn.todo.map((item) => item.title)).toEqual(['A'])
|
|
})
|
|
|
|
it('最右列再右移无效果', async () => {
|
|
const { pinia, store } = setup()
|
|
const task = store.createTask({ title: 'A', status: 'done' })
|
|
const wrapper = mountBoard(pinia)
|
|
|
|
emitKeyMove(wrapper, task?.id ?? '', 'right')
|
|
await nextTick()
|
|
|
|
expect(store.byColumn.done.map((item) => item.title)).toEqual(['A'])
|
|
})
|
|
|
|
it('上移调整列内顺序', async () => {
|
|
const { pinia, store } = setup()
|
|
const a = store.createTask({ title: 'A' })
|
|
store.createTask({ title: 'B' })
|
|
store.createTask({ title: 'C' })
|
|
// 快速新增插入列首,当前顺序为 C, B, A
|
|
const wrapper = mountBoard(pinia)
|
|
|
|
emitKeyMove(wrapper, a?.id ?? '', 'up')
|
|
await nextTick()
|
|
|
|
expect(store.byColumn.todo.map((item) => item.title)).toEqual(['C', 'A', 'B'])
|
|
})
|
|
|
|
it('下移调整列内顺序', async () => {
|
|
const { pinia, store } = setup()
|
|
store.createTask({ title: 'A' })
|
|
store.createTask({ title: 'B' })
|
|
const c = store.createTask({ title: 'C' })
|
|
// 当前顺序 C, B, A
|
|
const wrapper = mountBoard(pinia)
|
|
|
|
emitKeyMove(wrapper, c?.id ?? '', 'down')
|
|
await nextTick()
|
|
|
|
expect(store.byColumn.todo.map((item) => item.title)).toEqual(['B', 'C', 'A'])
|
|
})
|
|
|
|
it('已在列首时上移无效果', async () => {
|
|
const { pinia, store } = setup()
|
|
store.createTask({ title: 'A' })
|
|
const first = store.createTask({ title: 'B' })
|
|
const wrapper = mountBoard(pinia)
|
|
|
|
emitKeyMove(wrapper, first?.id ?? '', 'up')
|
|
await nextTick()
|
|
|
|
expect(store.byColumn.todo.map((item) => item.title)).toEqual(['B', 'A'])
|
|
})
|
|
|
|
it('列内渲染子集时禁止上下移动(避免写坏全量顺序)', async () => {
|
|
const { pinia, store } = setup()
|
|
const a = store.createTask({ title: 'A', priority: 3 })
|
|
store.createTask({ title: 'B', priority: 1 })
|
|
const wrapper = mountBoard(pinia)
|
|
|
|
useFilterStore().togglePriority(3)
|
|
await nextTick()
|
|
|
|
emitKeyMove(wrapper, a?.id ?? '', 'down')
|
|
await nextTick()
|
|
|
|
// 顺序保持原样
|
|
expect(store.byColumn.todo.map((item) => item.title)).toEqual(['B', 'A'])
|
|
})
|
|
|
|
it('跨列移动在筛选态下依然可用', async () => {
|
|
const { pinia, store } = setup()
|
|
const task = store.createTask({ title: 'A', priority: 3 })
|
|
const wrapper = mountBoard(pinia)
|
|
|
|
useFilterStore().togglePriority(3)
|
|
await nextTick()
|
|
|
|
emitKeyMove(wrapper, task?.id ?? '', 'right')
|
|
await nextTick()
|
|
|
|
expect(store.byColumn.doing.map((item) => item.title)).toEqual(['A'])
|
|
})
|
|
})
|
|
|
|
describe('BoardView 批量添加', () => {
|
|
function buttonByText(wrapper: ReturnType<typeof mountBoard>, text: string) {
|
|
return wrapper.findAll('button').find((node) => node.text().includes(text))
|
|
}
|
|
|
|
it('点击工具栏「批量添加」打开面板', async () => {
|
|
const { pinia } = setup()
|
|
const wrapper = mountBoard(pinia)
|
|
|
|
expect(wrapper.find('textarea[aria-label="批量任务清单"]').exists()).toBe(false)
|
|
|
|
await buttonByText(wrapper, '批量添加')?.trigger('click')
|
|
await nextTick()
|
|
|
|
expect(wrapper.find('textarea[aria-label="批量任务清单"]').exists()).toBe(true)
|
|
})
|
|
|
|
it('批量添加后卡片出现在看板上', async () => {
|
|
const { pinia, store } = setup()
|
|
const wrapper = mountBoard(pinia)
|
|
|
|
await buttonByText(wrapper, '批量添加')?.trigger('click')
|
|
await nextTick()
|
|
|
|
await wrapper.find('textarea[aria-label="批量任务清单"]').setValue('写周报\n评审 PR')
|
|
await buttonByText(wrapper, '添加 2 个任务')?.trigger('click')
|
|
await nextTick()
|
|
|
|
expect(store.byColumn.todo.map((item) => item.title)).toEqual(['写周报', '评审 PR'])
|
|
expect(wrapper.text()).toContain('写周报')
|
|
})
|
|
})
|
|
|
|
describe('BoardView 多选与批量删除', () => {
|
|
/** 文案精确匹配:「多选」是「退出多选」的子串 */
|
|
function exactButton(wrapper: ReturnType<typeof mountBoard>, text: string) {
|
|
return wrapper.findAll('button').find((node) => node.text() === text)
|
|
}
|
|
|
|
async function enterSelection(wrapper: ReturnType<typeof mountBoard>) {
|
|
await exactButton(wrapper, '多选')?.trigger('click')
|
|
await nextTick()
|
|
}
|
|
|
|
it('点击「多选」后卡片变为可勾选,并出现操作条', async () => {
|
|
const { pinia, store } = setup()
|
|
store.createTask({ title: 'A' })
|
|
const wrapper = mountBoard(pinia)
|
|
|
|
expect(wrapper.text()).not.toContain('已选')
|
|
|
|
await enterSelection(wrapper)
|
|
|
|
expect(wrapper.text()).toContain('已选 0 个任务')
|
|
expect(wrapper.find('article').attributes('role')).toBe('checkbox')
|
|
})
|
|
|
|
it('点击卡片即选中,计数同步', async () => {
|
|
const { pinia, store } = setup()
|
|
store.createTasks([{ title: 'A' }, { title: 'B' }])
|
|
const wrapper = mountBoard(pinia)
|
|
await enterSelection(wrapper)
|
|
|
|
await wrapper.find('[data-task-id]').trigger('click')
|
|
await nextTick()
|
|
|
|
expect(wrapper.text()).toContain('已选 1 个任务')
|
|
expect(wrapper.find('article').attributes('aria-checked')).toBe('true')
|
|
})
|
|
|
|
it('多选时点击卡片不会打开详情抽屉', async () => {
|
|
const { pinia, store } = setup()
|
|
store.createTask({ title: 'A' })
|
|
const wrapper = mountBoard(pinia)
|
|
await enterSelection(wrapper)
|
|
|
|
await wrapper.find('article').trigger('click')
|
|
await nextTick()
|
|
|
|
expect(wrapper.find('[role="dialog"]').exists()).toBe(false)
|
|
})
|
|
|
|
it('全选选中当前可见的全部任务', async () => {
|
|
const { pinia, store } = setup()
|
|
store.createTasks([{ title: 'A' }, { title: 'B', status: 'doing' }, { title: 'C' }])
|
|
const wrapper = mountBoard(pinia)
|
|
await enterSelection(wrapper)
|
|
|
|
await wrapper
|
|
.findAll('button')
|
|
.find((node) => node.text().includes('全选'))
|
|
?.trigger('click')
|
|
await nextTick()
|
|
|
|
expect(wrapper.text()).toContain('已选 3 个任务')
|
|
})
|
|
|
|
it('筛选后全选只覆盖可见任务', async () => {
|
|
const { pinia, store } = setup()
|
|
store.createTasks([
|
|
{ title: '高优先', priority: 3 },
|
|
{ title: '普通', priority: 1 },
|
|
])
|
|
const wrapper = mountBoard(pinia)
|
|
await enterSelection(wrapper)
|
|
|
|
useFilterStore().togglePriority(3)
|
|
await nextTick()
|
|
|
|
await wrapper
|
|
.findAll('button')
|
|
.find((node) => node.text().includes('全选'))
|
|
?.trigger('click')
|
|
await nextTick()
|
|
|
|
expect(wrapper.text()).toContain('已选 1 个任务')
|
|
})
|
|
|
|
it('删除选中任务后弹出可撤销的提示,撤销连原顺序一起还原', async () => {
|
|
const { pinia, store } = setup()
|
|
store.createTasks([{ title: 'A' }, { title: 'B' }])
|
|
const wrapper = mountBoard(pinia)
|
|
await enterSelection(wrapper)
|
|
|
|
await wrapper.find('[data-task-id]').trigger('click')
|
|
await nextTick()
|
|
await wrapper
|
|
.findAll('button')
|
|
.find((node) => node.text().includes('删除'))
|
|
?.trigger('click')
|
|
await nextTick()
|
|
|
|
expect(store.tasks.map((item) => item.title)).toEqual(['B'])
|
|
// 删除后自动退出多选
|
|
expect(wrapper.text()).not.toContain('已选')
|
|
|
|
const toast = useToastStore()
|
|
expect(toast.toasts.at(-1)?.message).toBe('已删除 1 个任务')
|
|
toast.toasts.at(-1)?.action?.run()
|
|
|
|
expect(store.byColumn.todo.map((item) => item.title)).toEqual(['A', 'B'])
|
|
})
|
|
|
|
it('没有选中任务时删除按钮不可用', async () => {
|
|
const { pinia, store } = setup()
|
|
store.createTask({ title: 'A' })
|
|
const wrapper = mountBoard(pinia)
|
|
await enterSelection(wrapper)
|
|
|
|
const removeButton = wrapper.findAll('button').find((node) => node.text().includes('删除 0 个'))
|
|
expect(removeButton?.attributes('disabled')).toBeDefined()
|
|
})
|
|
|
|
it('筛选条件变化会清空选择,避免删掉看不见的任务', async () => {
|
|
const { pinia, store } = setup()
|
|
store.createTasks([
|
|
{ title: 'A', priority: 3 },
|
|
{ title: 'B', priority: 1 },
|
|
])
|
|
const wrapper = mountBoard(pinia)
|
|
await enterSelection(wrapper)
|
|
await wrapper.find('[data-task-id]').trigger('click')
|
|
await nextTick()
|
|
expect(wrapper.text()).toContain('已选 1 个任务')
|
|
|
|
useFilterStore().togglePriority(1)
|
|
await nextTick()
|
|
|
|
expect(wrapper.text()).toContain('已选 0 个任务')
|
|
})
|
|
|
|
it('退出多选后清空选择并恢复卡片的常规语义', async () => {
|
|
const { pinia, store } = setup()
|
|
store.createTask({ title: 'A' })
|
|
const wrapper = mountBoard(pinia)
|
|
await enterSelection(wrapper)
|
|
await wrapper.find('article').trigger('click')
|
|
await nextTick()
|
|
|
|
await exactButton(wrapper, '退出多选')?.trigger('click')
|
|
await nextTick()
|
|
|
|
expect(wrapper.text()).not.toContain('已选')
|
|
expect(wrapper.find('article').attributes('role')).toBeUndefined()
|
|
|
|
// 恢复后点击卡片重新等于「打开详情」
|
|
await wrapper.find('article').trigger('click')
|
|
await nextTick()
|
|
expect(wrapper.find('[role="dialog"]').exists()).toBe(true)
|
|
})
|
|
|
|
it('归档后清空选择,避免选中已不可见的卡片', async () => {
|
|
const { pinia, store } = setup()
|
|
const task = store.createTask({ title: '很久前完成', status: 'done' })
|
|
store.updateTask(task?.id ?? '', { completedAt: dayjs().subtract(30, 'day').toISOString() })
|
|
const wrapper = mountBoard(pinia)
|
|
await enterSelection(wrapper)
|
|
|
|
await wrapper
|
|
.findAll('button')
|
|
.find((node) => node.text().includes('全选'))
|
|
?.trigger('click')
|
|
await nextTick()
|
|
expect(wrapper.text()).toContain('已选 1 个任务')
|
|
|
|
await wrapper
|
|
.findAll('button')
|
|
.find((node) => node.text().includes('个已完成'))
|
|
?.trigger('click')
|
|
await nextTick()
|
|
|
|
expect(wrapper.text()).toContain('已选 0 个任务')
|
|
})
|
|
})
|