/* @vitest-environment happy-dom */
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
import { defHttp } from '#/api/request';
|
import { createDmsFilePrintTicket, getDmsFilePageCount, renderDmsPrintPdf } from '#/api/x/dms/print';
|
import { onlineUtils } from '#/utils/jnpf';
|
import { printManagedDmsDocument } from '../dmsHiprint';
|
import { prepareDmsPrintAttempt, type DmsPrintAttempt } from '../dmsPrintAttempt';
|
|
const pdf = vi.hoisted(() => ({ pages: 1 }));
|
vi.mock('#/api/request', () => ({ defHttp: { post: vi.fn() } }));
|
vi.mock('#/api/x/dms/print', () => ({ createDmsFilePrintTicket: vi.fn(), getDmsFilePageCount: vi.fn(), renderDmsPrintPdf: vi.fn() }));
|
vi.mock('#/utils/jnpf', () => ({ onlineUtils: { hiprint: vi.fn(), isHiprintClientReady: vi.fn() } }));
|
vi.mock('pdfjs-dist/build/pdf.worker.min.mjs?url', () => ({ default: 'worker' }));
|
vi.mock('pdfjs-dist', () => ({
|
GlobalWorkerOptions: {},
|
getDocument: () => ({
|
promise: Promise.resolve({
|
numPages: pdf.pages,
|
destroy: vi.fn(),
|
getPage: async () => ({ cleanup: vi.fn(), getViewport: () => ({ height: 842, width: 595 }), render: () => ({ promise: Promise.resolve() }) }),
|
}),
|
}),
|
}));
|
|
const config = { printTaskId: 'task', fileVersionId: 'version', copies: 1 };
|
let saved: DmsPrintAttempt;
|
let confirmations: number;
|
let rejectReceipt: boolean;
|
let rejectBeginResponse: boolean;
|
let deliveries: any[];
|
const snapshot = () => structuredClone(saved);
|
|
describe('durable DMS print recovery', () => {
|
beforeEach(() => {
|
vi.clearAllMocks();
|
const storage = new Map<string, string>();
|
vi.stubGlobal('localStorage', {
|
getItem: (k: string) => storage.get(k) ?? null,
|
setItem: (k: string, v: string) => storage.set(k, v),
|
removeItem: (k: string) => storage.delete(k),
|
get length() {
|
return storage.size;
|
},
|
});
|
pdf.pages = 1;
|
confirmations = 0;
|
rejectReceipt = false;
|
rejectBeginResponse = false;
|
deliveries = [];
|
saved = { id: 'attempt', tenantId: 'tenant', userId: 'user', taskId: 'task', fileVersionId: 'version', copies: 1, state: 'OPEN', numbersJson: JSON.stringify([{serialNumber:'unique-1',countSerialNumber:'20260001'}]), batchesJson: '[]' };
|
vi.mocked(getDmsFilePageCount).mockResolvedValue({ data: { pageCount: 1 } } as any);
|
vi.mocked(createDmsFilePrintTicket).mockResolvedValue({ data: { accessTicket: 'ticket', fileName: 'file.pdf', fileVersionId: 'version' } } as any);
|
vi.mocked(renderDmsPrintPdf).mockImplementation(async (request: any) => {
|
const pages = request.items[0].printPages.split('-').map(Number);
|
pdf.pages = (pages[1] ?? pages[0]) - pages[0] + 1;
|
return new Blob();
|
});
|
vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue({} as any);
|
vi.spyOn(HTMLCanvasElement.prototype, 'toDataURL').mockReturnValue('data:image/png;base64,x');
|
vi.mocked(onlineUtils.isHiprintClientReady).mockResolvedValue(true);
|
vi.mocked(onlineUtils.hiprint).mockImplementation(async (options) => {
|
deliveries.push(options);
|
options.onPrint?.({ mode: 'hiprint' });
|
return { mode: 'hiprint' };
|
});
|
vi.mocked(defHttp.post).mockImplementation(async ({ url, data }: any) => {
|
const batches = JSON.parse(saved.batchesJson);
|
const batch = batches.find((item: any) => item.id === data.batchId);
|
if (url.endsWith('/fasong-jihua') && !batches.length) saved.batchesJson = JSON.stringify(data.batches);
|
if (url.endsWith('/kaishi-pici')) {
|
if (batch.state !== 'READY') throw new Error('发送结果未知');
|
batch.state = 'SENDING';
|
batch.token = data.token;
|
saved.batchesJson = JSON.stringify(batches);
|
if (rejectBeginResponse) throw new Error('许可响应丢失');
|
}
|
if (url.endsWith('/queren-pici')) {
|
if (rejectReceipt) throw new Error('网络断开');
|
expect(batch.token).toBe(data.token);
|
batch.state = 'SUBMITTED';
|
saved.batchesJson = JSON.stringify(batches);
|
}
|
if (url.endsWith('/queren-fasong')) {
|
confirmations++;
|
saved.state = 'CONFIRMED';
|
}
|
return { code: 200, data: snapshot() } as any;
|
});
|
});
|
afterEach(() => {
|
vi.useRealTimers();
|
vi.restoreAllMocks();
|
vi.unstubAllGlobals();
|
});
|
|
it('prints two copies with fixed distinct serials and sends each exactly once', async () => {
|
saved.copies = 2;
|
saved.numbersJson = JSON.stringify([
|
{serialNumber:'unique-1',countSerialNumber:'20260001'},
|
{serialNumber:'unique-2',countSerialNumber:'20260002'},
|
]);
|
await printManagedDmsDocument('file.pdf', {...config,copies:2,watermarkBusinessTypes:['RECORD', 'REPORT']}, snapshot());
|
expect(deliveries).toHaveLength(2);
|
expect(deliveries.map(item => item.clientOptions.copies)).toEqual([1,1]);
|
const requests = vi.mocked(createDmsFilePrintTicket).mock.calls.map(call => call[0]);
|
expect(requests.every(item => item.requireSerialWatermark === true)).toBe(true);
|
expect(requests.map(item => item.watermarkBusinessTypes)).toEqual([['RECORD', 'REPORT'], ['RECORD', 'REPORT']]);
|
expect(requests.map(item => item.customBarcode)).toEqual([
|
[{barValue:'unique-1',barText:'打印流水号: unique-1 计数流水号: 20260001'}],
|
[{barValue:'unique-2',barText:'打印流水号: unique-2 计数流水号: 20260002'}],
|
]);
|
expect(confirmations).toBe(1);
|
await printManagedDmsDocument('file.pdf', config, snapshot());
|
expect(deliveries).toHaveLength(2);
|
});
|
|
it('applies online formatting separately to each frozen copy', async () => {
|
saved.copies = 2;
|
saved.numbersJson = JSON.stringify([{ serialNumber: 'unique-1', countSerialNumber: '20260001' }, { serialNumber: 'unique-2', countSerialNumber: '20260002' }]);
|
const builder = async ({ serialNumber, countSerialNumber }: any) => [{ barValue: serialNumber, barText: `${countSerialNumber} / ${serialNumber}` }];
|
await printManagedDmsDocument('file.pdf', { ...config, buildCustomBarcode: builder }, snapshot());
|
expect(vi.mocked(createDmsFilePrintTicket).mock.calls.map(([request]) => request.customBarcode)).toEqual([
|
[{ barValue: 'unique-1', barText: '20260001 / unique-1' }], [{ barValue: 'unique-2', barText: '20260002 / unique-2' }],
|
]);
|
});
|
it('validates every pending copy before any ticket or dispatch and allows retry after interface failure', async () => {
|
saved.copies = 2;
|
saved.numbersJson = JSON.stringify([{ serialNumber: 'unique-1', countSerialNumber: '20260001' }, { serialNumber: 'unique-2', countSerialNumber: '20260002' }]);
|
const builder = vi.fn(async ({ serialNumber }: any) => {
|
if (serialNumber === 'unique-2') throw new Error('格式接口不可用');
|
return [{ barValue: serialNumber, barText: serialNumber }];
|
});
|
await expect(printManagedDmsDocument('file.pdf', { ...config, buildCustomBarcode: builder }, snapshot())).rejects.toThrow('格式接口不可用');
|
expect(createDmsFilePrintTicket).not.toHaveBeenCalled();
|
expect(deliveries).toHaveLength(0);
|
builder.mockImplementation(async ({ serialNumber }) => [{ barValue: serialNumber, barText: serialNumber }]);
|
await printManagedDmsDocument('file.pdf', { ...config, buildCustomBarcode: builder }, snapshot());
|
expect(deliveries).toHaveLength(2);
|
});
|
it('reads each copy format once across page batches', async () => {
|
vi.mocked(getDmsFilePageCount).mockResolvedValue({ data: { pageCount: 101 } } as any);
|
const builder = vi.fn(async ({ serialNumber }: any) => [{ barValue: serialNumber, barText: '统一文字' }]);
|
await printManagedDmsDocument('file.pdf', { ...config, buildCustomBarcode: builder }, snapshot());
|
expect(builder).toHaveBeenCalledTimes(1);
|
expect(deliveries).toHaveLength(2);
|
expect(vi.mocked(createDmsFilePrintTicket).mock.calls.every(([request]) => request.customBarcode?.[0]?.barText === '统一文字')).toBe(true);
|
});
|
it('rejects a formatter that replaces the fixed barcode identity before sending', async () => {
|
await expect(printManagedDmsDocument('file.pdf', { ...config, buildCustomBarcode: () => [{ barValue: 'wrong', barText: 'text' }] }, snapshot())).rejects.toThrow('当前固定流水号');
|
expect(createDmsFilePrintTicket).not.toHaveBeenCalled();
|
expect(deliveries).toHaveLength(0);
|
});
|
it('late success after timeout is persisted and confirmed, and recovery never dispatches it again', async () => {
|
vi.useFakeTimers();
|
vi.mocked(onlineUtils.hiprint).mockImplementation(async (options) => {
|
deliveries.push(options);
|
return { mode: 'hiprint' };
|
});
|
const run = printManagedDmsDocument('file.pdf', config, snapshot());
|
const failure = expect(run).rejects.toThrow('结果未知');
|
await vi.advanceTimersByTimeAsync(60_001);
|
await failure;
|
expect(deliveries).toHaveLength(1);
|
expect(confirmations).toBe(0);
|
await expect(printManagedDmsDocument('file.pdf', config, snapshot())).rejects.toThrow('结果未知');
|
deliveries[0].onPrint({ mode: 'hiprint' });
|
await vi.advanceTimersByTimeAsync(0);
|
expect(confirmations).toBe(1);
|
expect(saved.state).toBe('CONFIRMED');
|
await printManagedDmsDocument('file.pdf', config, snapshot());
|
expect(deliveries).toHaveLength(1);
|
});
|
|
it('101 pages: failure of the second batch preserves the first 100 pages and resumes only page 101', async () => {
|
vi.mocked(getDmsFilePageCount).mockResolvedValue({ data: { pageCount: 101 } } as any);
|
const render = vi.mocked(renderDmsPrintPdf).getMockImplementation()!;
|
let failed = false;
|
vi.mocked(renderDmsPrintPdf).mockImplementation(async (request: any) => {
|
if (request.items[0].printPages === '101' && !failed) {
|
failed = true;
|
throw new Error('第二批转换失败');
|
}
|
return render(request);
|
});
|
await expect(printManagedDmsDocument('file.pdf', config, snapshot())).rejects.toThrow('第二批转换失败');
|
expect(deliveries).toHaveLength(1);
|
expect(confirmations).toBe(0);
|
expect(JSON.parse(saved.batchesJson).map((item: any) => item.state)).toEqual(['SUBMITTED', 'READY']);
|
const restored = await prepareDmsPrintAttempt(config, 1);
|
expect(restored.copies).toBe(1);
|
await printManagedDmsDocument('file.pdf', config, restored);
|
expect(deliveries).toHaveLength(2);
|
expect(confirmations).toBe(1);
|
expect(deliveries.map((item) => item.clientOptions.copies)).toEqual([1, 1]);
|
expect(vi.mocked(createDmsFilePrintTicket).mock.calls.map((call) => call[0].printPages)).toEqual(['1-100', '101', '101']);
|
});
|
|
it('lost dispatch-permission response blocks any send and is not automatically retried', async () => {
|
rejectBeginResponse = true;
|
await expect(printManagedDmsDocument('file.pdf', config, snapshot())).rejects.toThrow('许可响应丢失');
|
rejectBeginResponse = false;
|
await expect(printManagedDmsDocument('file.pdf', config, snapshot())).rejects.toThrow('结果未知');
|
expect(deliveries).toHaveLength(0);
|
});
|
|
it('success with failed receipt persistence is recovered after refresh from the scoped outbox', async () => {
|
rejectReceipt = true;
|
await expect(printManagedDmsDocument('file.pdf', config, snapshot())).rejects.toThrow('网络断开');
|
expect(deliveries).toHaveLength(1);
|
expect(localStorage.length).toBe(1);
|
const recovered = snapshot(); // 模拟丢弃原页面运行上下文。
|
rejectReceipt = false;
|
await printManagedDmsDocument('file.pdf', config, recovered);
|
expect(deliveries).toHaveLength(1);
|
expect(confirmations).toBeGreaterThan(0);
|
expect(localStorage.length).toBe(0);
|
});
|
|
it('another account cannot consume the original account local success receipt', async () => {
|
rejectReceipt = true;
|
await expect(printManagedDmsDocument('file.pdf', config, snapshot())).rejects.toThrow();
|
rejectReceipt = false;
|
const other = { ...snapshot(), userId: 'other' };
|
await expect(printManagedDmsDocument('file.pdf', config, other)).rejects.toThrow('结果未知');
|
expect(confirmations).toBe(0);
|
expect(deliveries).toHaveLength(1);
|
});
|
});
|