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

362 lines
10 KiB
TypeScript

import { flushPromises, mount } from '@vue/test-utils';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import DocumentImportBatchStatus from './DocumentImportBatchStatus.vue';
const apiMocks = vi.hoisted(() => ({
get: vi.fn(),
post: vi.fn(),
}));
vi.mock('#/api/request', () => ({ api: apiMocks }));
vi.mock('@easyflow/locales', () => ({
$t: (key: string) => key,
}));
function createBatch(status: 'COMPLETED' | 'RUNNING', completedCount: number) {
return {
batchId: 'batch-1',
completedCount,
failedCount: 0,
importMode: 'AUTO',
pendingCount: status === 'RUNNING' ? 1 : 0,
processingCount: 0,
progressPercent: status === 'COMPLETED' ? 100 : 50,
skippedCount: 0,
status,
totalCount: 2,
};
}
describe('documentImportBatchStatus', () => {
beforeEach(() => {
localStorage.clear();
});
afterEach(() => {
vi.clearAllMocks();
vi.useRealTimers();
});
it('页面首次进入时不恢复已完成批次', async () => {
apiMocks.get.mockResolvedValue({
data: createBatch('COMPLETED', 2),
errorCode: 0,
});
const wrapper = mount(DocumentImportBatchStatus, {
props: { knowledgeId: 'knowledge-1' },
});
await flushPromises();
expect(wrapper.find('.batch-status').exists()).toBe(false);
wrapper.unmount();
});
it('当前页面跟踪的批次完成后移除提示', async () => {
vi.useFakeTimers();
apiMocks.get
.mockResolvedValueOnce({
data: createBatch('RUNNING', 1),
errorCode: 0,
})
.mockResolvedValueOnce({
data: createBatch('COMPLETED', 2),
errorCode: 0,
});
const wrapper = mount(DocumentImportBatchStatus, {
props: { knowledgeId: 'knowledge-1' },
});
await flushPromises();
expect(wrapper.find('.batch-status').exists()).toBe(true);
await vi.advanceTimersByTimeAsync(3000);
await flushPromises();
expect(wrapper.find('.batch-status').exists()).toBe(false);
wrapper.unmount();
});
it('关闭当前批次提示后停止轮询且不影响导入接口', async () => {
vi.useFakeTimers();
apiMocks.get.mockResolvedValue({
data: createBatch('RUNNING', 1),
errorCode: 0,
});
const wrapper = mount(DocumentImportBatchStatus, {
props: { knowledgeId: 'knowledge-1' },
});
await flushPromises();
const closeButton = wrapper.find('.batch-status__close');
expect(closeButton.attributes('aria-label')).toBe(
'documentCollection.importDoc.dismissBatchStatus',
);
await closeButton.trigger('click');
expect(wrapper.find('.batch-status').exists()).toBe(false);
await vi.advanceTimersByTimeAsync(30_000);
expect(apiMocks.get).toHaveBeenCalledTimes(1);
expect(apiMocks.post).not.toHaveBeenCalled();
wrapper.unmount();
});
it('当前批次关闭状态在本地保留且新批次仍会展示', async () => {
apiMocks.get
.mockResolvedValueOnce({
data: createBatch('RUNNING', 1),
errorCode: 0,
})
.mockResolvedValueOnce({
data: createBatch('RUNNING', 1),
errorCode: 0,
})
.mockResolvedValueOnce({
data: { ...createBatch('RUNNING', 0), batchId: 'batch-2' },
errorCode: 0,
});
const firstWrapper = mount(DocumentImportBatchStatus, {
props: { knowledgeId: 'knowledge-1' },
});
await flushPromises();
await firstWrapper.find('.batch-status__close').trigger('click');
firstWrapper.unmount();
const secondWrapper = mount(DocumentImportBatchStatus, {
props: { knowledgeId: 'knowledge-1' },
});
await flushPromises();
expect(secondWrapper.find('.batch-status').exists()).toBe(false);
await secondWrapper.setProps({ refreshKey: 1 });
await flushPromises();
expect(secondWrapper.find('.batch-status').exists()).toBe(true);
secondWrapper.unmount();
});
it('失败批次关联任务实际完成后不再展示提示', async () => {
apiMocks.get.mockResolvedValue({
data: {
...createBatch('RUNNING', 0),
actualCompleted: true,
failedCount: 2,
pendingCount: 0,
status: 'PARTIAL_SUCCEEDED',
},
errorCode: 0,
});
const wrapper = mount(DocumentImportBatchStatus, {
props: { knowledgeId: 'knowledge-1' },
});
await flushPromises();
expect(wrapper.find('.batch-status').exists()).toBe(false);
wrapper.unmount();
});
it('失败批次低频复查后在关联任务完成时移除提示', async () => {
vi.useFakeTimers();
const failedBatch = {
...createBatch('RUNNING', 0),
actualCompleted: false,
failedCount: 2,
pendingCount: 0,
status: 'PARTIAL_SUCCEEDED',
};
apiMocks.get
.mockResolvedValueOnce({
data: failedBatch,
errorCode: 0,
})
.mockResolvedValueOnce({
data: { ...failedBatch, actualCompleted: true },
errorCode: 0,
});
const wrapper = mount(DocumentImportBatchStatus, {
props: { knowledgeId: 'knowledge-1' },
});
await flushPromises();
expect(wrapper.find('.batch-status').exists()).toBe(true);
await vi.advanceTimersByTimeAsync(15_000);
await flushPromises();
expect(apiMocks.get).toHaveBeenCalledTimes(2);
expect(wrapper.find('.batch-status').exists()).toBe(false);
wrapper.unmount();
});
it('部分失败批次按 failedCount 继续并发送统一批次参数', async () => {
const failedBatch = {
batchId: 'batch-1',
completedCount: 1,
failedCount: 1,
importMode: 'AUTO',
pendingCount: 0,
processingCount: 0,
progressPercent: 100,
retryableFailedCount: 0,
skippedCount: 0,
status: 'PARTIAL_SUCCEEDED',
totalCount: 2,
};
apiMocks.get.mockResolvedValue({
data: failedBatch,
errorCode: 0,
});
apiMocks.post.mockResolvedValue({
data: {
...failedBatch,
failedCount: 0,
pendingCount: 1,
progressPercent: 50,
status: 'RUNNING',
},
errorCode: 0,
});
const wrapper = mount(DocumentImportBatchStatus, {
props: {
knowledgeId: 'knowledge-1',
manageable: true,
},
});
await flushPromises();
const continueButton = wrapper.find('.batch-status__continue');
expect(continueButton.exists()).toBe(true);
await continueButton.trigger('click');
await flushPromises();
expect(apiMocks.post).toHaveBeenCalledWith(
'/api/v1/document/import/batch/continue',
{
batchId: 'batch-1',
knowledgeId: 'knowledge-1',
},
);
wrapper.unmount();
});
it('没有失败项时不使用旧兼容计数显示继续按钮', async () => {
apiMocks.get.mockResolvedValue({
data: {
...createBatch('COMPLETED', 2),
retryableFailedCount: 1,
status: 'PARTIAL_SUCCEEDED',
},
errorCode: 0,
});
const wrapper = mount(DocumentImportBatchStatus, {
props: {
knowledgeId: 'knowledge-1',
manageable: true,
},
});
await flushPromises();
expect(wrapper.find('.batch-status__continue').exists()).toBe(false);
wrapper.unmount();
});
it('中断批次展示异常原因、错误标识和继续入口', async () => {
apiMocks.get.mockResolvedValue({
data: {
...createBatch('RUNNING', 0),
failedCount: 2,
interruptCode: 'redis_unavailable',
interruptedAt: '2026-08-07T09:25:28+08:00',
interruptMessage: '缓存与消息服务异常,自动导入已中断',
pendingCount: 0,
status: 'INTERRUPTED',
},
errorCode: 0,
});
const wrapper = mount(DocumentImportBatchStatus, {
props: {
knowledgeId: 'knowledge-1',
manageable: true,
},
});
await flushPromises();
const alert = wrapper.find('.batch-status__alert');
expect(alert.exists()).toBe(true);
expect(alert.attributes('role')).toBe('alert');
expect(alert.text()).toContain('缓存与消息服务异常,自动导入已中断');
expect(alert.text()).toContain('redis_unavailable');
expect(wrapper.find('.batch-status__count').text()).toContain('2 / 2');
expect(wrapper.find('.batch-status__continue').exists()).toBe(true);
wrapper.unmount();
});
it('状态接口暂时失败时展示错误并自动退避恢复轮询', async () => {
vi.useFakeTimers();
apiMocks.get
.mockRejectedValueOnce(new Error('network unavailable'))
.mockResolvedValueOnce({
data: createBatch('RUNNING', 1),
errorCode: 0,
});
const wrapper = mount(DocumentImportBatchStatus, {
props: { knowledgeId: 'knowledge-1' },
});
await flushPromises();
expect(wrapper.find('.batch-status--error').exists()).toBe(true);
await vi.advanceTimersByTimeAsync(3000);
await flushPromises();
expect(apiMocks.get).toHaveBeenCalledTimes(2);
expect(wrapper.find('.batch-status--error').exists()).toBe(false);
expect(wrapper.find('.batch-status').exists()).toBe(true);
wrapper.unmount();
});
it('运行批次轮询失败时保留旧状态、展示错误并自动恢复', async () => {
vi.useFakeTimers();
apiMocks.get
.mockResolvedValueOnce({
data: createBatch('RUNNING', 1),
errorCode: 0,
})
.mockRejectedValueOnce(new Error('network unavailable'))
.mockResolvedValueOnce({
data: createBatch('COMPLETED', 2),
errorCode: 0,
});
const wrapper = mount(DocumentImportBatchStatus, {
props: { knowledgeId: 'knowledge-1' },
});
await flushPromises();
await vi.advanceTimersByTimeAsync(3000);
await flushPromises();
expect(wrapper.find('.batch-status__summary').exists()).toBe(true);
expect(wrapper.find('.batch-status__load-error').exists()).toBe(true);
await vi.advanceTimersByTimeAsync(3000);
await flushPromises();
expect(apiMocks.get).toHaveBeenCalledTimes(3);
expect(wrapper.find('.batch-status__load-error').exists()).toBe(false);
expect(wrapper.find('.batch-status').exists()).toBe(false);
wrapper.unmount();
});
});