feat: 支持知识库文档批量删除与分块内联编辑
This commit is contained in:
@@ -0,0 +1,348 @@
|
||||
import { flushPromises, mount } from '@vue/test-utils';
|
||||
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import DocumentTable from './DocumentTable.vue';
|
||||
|
||||
const sseMocks = vi.hoisted(() => ({
|
||||
abort: vi.fn(),
|
||||
post: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
const tableState = vi.hoisted(() => ({ rows: [] as any[] }));
|
||||
|
||||
vi.mock('element-plus', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('element-plus')>();
|
||||
const { defineComponent, h } = await import('vue');
|
||||
const ElTable = defineComponent({
|
||||
name: 'ElTable',
|
||||
props: ['data'],
|
||||
emits: ['selection-change'],
|
||||
setup(props, { expose, slots }) {
|
||||
expose({
|
||||
clearSelection: vi.fn(),
|
||||
toggleRowSelection: vi.fn(),
|
||||
});
|
||||
return () => {
|
||||
tableState.rows = props.data || [];
|
||||
return h('div', { class: 'table-stub' }, slots.default?.());
|
||||
};
|
||||
},
|
||||
});
|
||||
const ElTableColumn = defineComponent({
|
||||
name: 'ElTableColumn',
|
||||
props: ['label', 'reserveSelection', 'selectable', 'type'],
|
||||
setup(_props, { slots }) {
|
||||
return () =>
|
||||
h(
|
||||
'div',
|
||||
{ class: 'table-column-stub' },
|
||||
tableState.rows.map((row) => slots.default?.({ row })),
|
||||
);
|
||||
},
|
||||
});
|
||||
return { ...actual, ElTable, ElTableColumn };
|
||||
});
|
||||
|
||||
vi.mock('#/api/request', () => ({
|
||||
api: {},
|
||||
SseClient: class {
|
||||
abort = sseMocks.abort;
|
||||
post = sseMocks.post;
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@easyflow/locales', () => ({
|
||||
$t: (key: string, params?: Record<string, number>) =>
|
||||
params ? `${key}:${JSON.stringify(params)}` : key,
|
||||
}));
|
||||
|
||||
const rows = [
|
||||
{ id: '1', processStatus: 'COMPLETED', title: '一.docx' },
|
||||
{ id: '2', processStatus: 'PARSING', title: '处理中.docx' },
|
||||
{ id: '3', processStatus: 'COMPLETED', title: '三.pdf' },
|
||||
{ id: '4', processStatus: 'PARSE_FAILED', title: '四.xlsx' },
|
||||
{ id: '5', processStatus: 'COMPLETED', title: '五.docx' },
|
||||
{ id: '6', processStatus: 'COMPLETED', title: '六.pdf' },
|
||||
{ id: '7', processStatus: 'COMPLETED', title: '七.xlsx' },
|
||||
];
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('documentTable', () => {
|
||||
it('处理中行不可选,批量删除使用最多三个并发并汇总部分失败', async () => {
|
||||
let active = 0;
|
||||
let maxActive = 0;
|
||||
const post = vi.fn().mockImplementation(async (_url: string, body: any) => {
|
||||
active += 1;
|
||||
maxActive = Math.max(maxActive, active);
|
||||
await new Promise((resolve) => setTimeout(resolve, 5));
|
||||
active -= 1;
|
||||
return body.id === '4' || body.id === '6'
|
||||
? { data: false, errorCode: 0, message: '失败' }
|
||||
: { data: true, errorCode: 0 };
|
||||
});
|
||||
const get = vi.fn().mockResolvedValue({
|
||||
data: { records: rows, totalRow: rows.length },
|
||||
});
|
||||
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue('confirm' as never);
|
||||
|
||||
const wrapper = mount(DocumentTable, {
|
||||
global: { directives: { loading: {} } },
|
||||
props: {
|
||||
knowledgeId: '10',
|
||||
requestClient: { download: vi.fn(), get, post },
|
||||
},
|
||||
});
|
||||
await flushPromises();
|
||||
const warning = vi.spyOn(ElMessage, 'warning');
|
||||
|
||||
const table = wrapper.findComponent({ name: 'ElTable' });
|
||||
const selectionColumn = table
|
||||
.findAllComponents({ name: 'ElTableColumn' })
|
||||
.find((column) => column.props('type') === 'selection');
|
||||
expect(selectionColumn?.props('selectable')(rows[0])).toBe(true);
|
||||
expect(selectionColumn?.props('selectable')(rows[1])).toBe(false);
|
||||
expect(selectionColumn?.props('reserveSelection')).toBe(true);
|
||||
|
||||
table.vm.$emit('selection-change', [
|
||||
rows[0],
|
||||
rows[2],
|
||||
rows[3],
|
||||
rows[4],
|
||||
rows[5],
|
||||
rows[6],
|
||||
]);
|
||||
await wrapper.vm.$nextTick();
|
||||
const deleteButton = wrapper
|
||||
.findAll('.batch-toolbar button')
|
||||
.find((button) =>
|
||||
button.text().includes('documentCollection.batchDelete'),
|
||||
);
|
||||
if (!deleteButton) throw new Error('未找到批量删除按钮');
|
||||
await deleteButton.trigger('click');
|
||||
await new Promise((resolve) => setTimeout(resolve, 30));
|
||||
await flushPromises();
|
||||
|
||||
expect(post).toHaveBeenCalledTimes(6);
|
||||
expect(maxActive).toBe(3);
|
||||
expect(maxActive).toBeLessThanOrEqual(3);
|
||||
expect(warning).toHaveBeenCalledWith(
|
||||
'documentCollection.batchDeletePartial:{"failed":2,"success":4}',
|
||||
);
|
||||
expect(wrapper.find('.batch-toolbar').text()).toContain(
|
||||
'documentCollection.selectedCount:{"count":2}',
|
||||
);
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it('批量删除确认期间阻止重复确认和重复请求', async () => {
|
||||
let resolveConfirm!: (value: unknown) => void;
|
||||
const confirm = vi.spyOn(ElMessageBox, 'confirm').mockReturnValue(
|
||||
new Promise((resolve) => {
|
||||
resolveConfirm = resolve;
|
||||
}) as never,
|
||||
);
|
||||
const post = vi.fn().mockResolvedValue({ data: true, errorCode: 0 });
|
||||
const get = vi.fn().mockResolvedValue({
|
||||
data: { records: rows, totalRow: rows.length },
|
||||
});
|
||||
const wrapper = mount(DocumentTable, {
|
||||
global: { directives: { loading: {} } },
|
||||
props: {
|
||||
knowledgeId: '10',
|
||||
requestClient: { download: vi.fn(), get, post },
|
||||
},
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
wrapper
|
||||
.findComponent({ name: 'ElTable' })
|
||||
.vm.$emit('selection-change', [rows[0], rows[2]]);
|
||||
await wrapper.vm.$nextTick();
|
||||
const deleteButton = wrapper
|
||||
.findAll('.batch-toolbar button')
|
||||
.find((button) =>
|
||||
button.text().includes('documentCollection.batchDelete'),
|
||||
);
|
||||
if (!deleteButton) throw new Error('未找到批量删除按钮');
|
||||
await deleteButton.trigger('click');
|
||||
await deleteButton.trigger('click');
|
||||
|
||||
expect(confirm).toHaveBeenCalledTimes(1);
|
||||
expect(post).not.toHaveBeenCalled();
|
||||
|
||||
resolveConfirm('confirm');
|
||||
await flushPromises();
|
||||
expect(post).toHaveBeenCalledTimes(2);
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it('搜索和显式重载会清空当前页选择', async () => {
|
||||
const get = vi.fn().mockResolvedValue({
|
||||
data: { records: rows, totalRow: rows.length },
|
||||
});
|
||||
const wrapper = mount(DocumentTable, {
|
||||
global: { directives: { loading: {} } },
|
||||
props: {
|
||||
knowledgeId: '10',
|
||||
requestClient: { download: vi.fn(), get, post: vi.fn() },
|
||||
},
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
const table = wrapper.findComponent({ name: 'ElTable' });
|
||||
table.vm.$emit('selection-change', [rows[0]]);
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(wrapper.find('.batch-toolbar').exists()).toBe(true);
|
||||
|
||||
wrapper.vm.search('关键字');
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(wrapper.find('.batch-toolbar').exists()).toBe(false);
|
||||
|
||||
table.vm.$emit('selection-change', [rows[2]]);
|
||||
await wrapper.vm.$nextTick();
|
||||
wrapper.vm.reload();
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(wrapper.find('.batch-toolbar').exists()).toBe(false);
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it('后台状态更新只移除转为处理中的已选文档', async () => {
|
||||
const get = vi.fn().mockResolvedValue({
|
||||
data: { records: rows, totalRow: rows.length },
|
||||
});
|
||||
const wrapper = mount(DocumentTable, {
|
||||
global: { directives: { loading: {} } },
|
||||
props: {
|
||||
knowledgeId: '10',
|
||||
requestClient: { download: vi.fn(), get, post: vi.fn() },
|
||||
},
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
const table = wrapper.findComponent({ name: 'ElTable' });
|
||||
table.vm.$emit('selection-change', [rows[0], rows[2]]);
|
||||
await wrapper.vm.$nextTick();
|
||||
const streamOptions = sseMocks.post.mock.calls.at(-1)?.[2];
|
||||
streamOptions?.onMessage?.({
|
||||
data: JSON.stringify({
|
||||
documentId: String(rows[1]?.id || ''),
|
||||
knowledgeId: '10',
|
||||
processStatus: 'PARSING',
|
||||
type: 'document-status',
|
||||
}),
|
||||
event: 'document-status',
|
||||
});
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(wrapper.find('.batch-toolbar').text()).toContain(
|
||||
'documentCollection.selectedCount:{"count":2}',
|
||||
);
|
||||
|
||||
streamOptions?.onMessage?.({
|
||||
data: JSON.stringify({
|
||||
documentId: String(rows[2]?.id || ''),
|
||||
knowledgeId: '10',
|
||||
processStatus: 'INDEXING',
|
||||
type: 'document-status',
|
||||
}),
|
||||
event: 'document-status',
|
||||
});
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(wrapper.find('.batch-toolbar').text()).toContain(
|
||||
'documentCollection.selectedCount:{"count":1}',
|
||||
);
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it('后台静默重载按 ID 保留当前页有效选择', async () => {
|
||||
const get = vi.fn().mockResolvedValue({
|
||||
data: { records: rows, totalRow: rows.length },
|
||||
});
|
||||
const wrapper = mount(DocumentTable, {
|
||||
global: { directives: { loading: {} } },
|
||||
props: {
|
||||
knowledgeId: '10',
|
||||
requestClient: { download: vi.fn(), get, post: vi.fn() },
|
||||
},
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
const table = wrapper.findComponent({ name: 'ElTable' });
|
||||
table.vm.$emit('selection-change', [rows[0]]);
|
||||
await wrapper.vm.$nextTick();
|
||||
const pageData = wrapper.findComponent({ name: 'PageData' });
|
||||
await (pageData.vm as any).reload({ silent: true });
|
||||
await flushPromises();
|
||||
|
||||
expect(wrapper.find('.batch-toolbar').text()).toContain(
|
||||
'documentCollection.selectedCount:{"count":1}',
|
||||
);
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it('单条删除请求期间锁定行操作并阻止重复提交', async () => {
|
||||
let resolveDelete!: (value: unknown) => void;
|
||||
const post = vi.fn().mockReturnValue(
|
||||
new Promise((resolve) => {
|
||||
resolveDelete = resolve;
|
||||
}),
|
||||
);
|
||||
const get = vi.fn().mockResolvedValue({
|
||||
data: { records: rows, totalRow: rows.length },
|
||||
});
|
||||
const confirm = vi
|
||||
.spyOn(ElMessageBox, 'confirm')
|
||||
.mockResolvedValue('confirm' as never);
|
||||
const wrapper = mount(DocumentTable, {
|
||||
global: { directives: { loading: {} } },
|
||||
props: {
|
||||
knowledgeId: '10',
|
||||
requestClient: { download: vi.fn(), get, post },
|
||||
},
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
const deleteButton = wrapper.get('button[aria-label="button.delete"]');
|
||||
await deleteButton.trigger('click');
|
||||
await deleteButton.trigger('click');
|
||||
await flushPromises();
|
||||
|
||||
expect(confirm).toHaveBeenCalledTimes(1);
|
||||
expect(post).toHaveBeenCalledTimes(1);
|
||||
expect(
|
||||
wrapper.get('button[aria-label="button.delete"]').attributes(),
|
||||
).toHaveProperty('disabled');
|
||||
|
||||
resolveDelete({ data: true, errorCode: 0 });
|
||||
await flushPromises();
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it('只读权限不渲染选择列和批量入口', async () => {
|
||||
const get = vi.fn().mockResolvedValue({
|
||||
data: { records: rows, totalRow: rows.length },
|
||||
});
|
||||
const wrapper = mount(DocumentTable, {
|
||||
global: { directives: { loading: {} } },
|
||||
props: {
|
||||
knowledgeId: '10',
|
||||
permissions: { canDeleteContent: false },
|
||||
requestClient: { download: vi.fn(), get, post: vi.fn() },
|
||||
},
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
const columns = wrapper
|
||||
.findComponent({ name: 'ElTable' })
|
||||
.findAllComponents({ name: 'ElTableColumn' });
|
||||
expect(columns.some((column) => column.props('type') === 'selection')).toBe(
|
||||
false,
|
||||
);
|
||||
expect(wrapper.find('.batch-toolbar').exists()).toBe(false);
|
||||
wrapper.unmount();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user