362 lines
10 KiB
TypeScript
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();
|
|
});
|
|
});
|