Files
EasyFlow/easyflow-ui-admin/app/src/views/ai/documentCollection/DocumentTable.test.ts

349 lines
11 KiB
TypeScript

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();
});
});