Files
Kanban/src/views/BoardView.spec.ts
T
chenshoulin 2d6e24dc91 feat(board): 支持批量添加与批量删除任务
- 批量添加面板:一行一条,整批共用目标列、优先级、标签与截止日期
- 快速新增支持粘贴多行文本,直接拆成多条任务
- 多选模式支持全选可见任务、整批删除,撤销时连原顺序一起还原
- 批量创建与删除走 store 的批量方法,一次落盘、一次广播
- 补充单元测试与 E2E 用例
2026-10-19 17:21:42 +08:00

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 个任务')
})
})