import { beforeEach, describe, expect, it, vi } from 'vitest';
|
const mocks = vi.hoisted(() => ({ post: vi.fn(), get: vi.fn(), print: vi.fn(), open: vi.fn(), emit: vi.fn(), preview: vi.fn() }));
|
vi.mock('#/api/request', () => ({ defHttp: { post: mocks.post, get: mocks.get } }));
|
vi.mock('#/utils/jnpf', () => ({ onlineUtils: { openFormModal: mocks.open, previewDmsFile: mocks.preview, getEmitter: () => ({ emit: mocks.emit }) } }));
|
vi.mock('../dmsHiprint', () => ({ printManagedDmsDocument: mocks.print }));
|
import { openDmsReprint, submitDmsReprint, sendDmsReprint, resumeDmsReprint, cancelDmsReprint, refreshDmsReprintLock, REPRINT_LOCK_EVENT } from '../dmsReprint';
|
import { configureReprintModal, reprintAuditSign, DMS_REPRINT_MODEL_ID } from '../dmsReprintMeasure';
|
let attempt: any;
|
let data: any;
|
let submit: any;
|
let context: any;
|
const receiptStore = new Map<string, string>();
|
beforeEach(() => {
|
vi.resetAllMocks();
|
receiptStore.clear();
|
vi.stubGlobal('localStorage', {
|
getItem: (k: string) => receiptStore.get(k),
|
setItem: (k: string, v: string) => receiptStore.set(k, v),
|
removeItem: (k: string) => receiptStore.delete(k),
|
});
|
attempt = {
|
id: 'request',
|
tenantId: '0',
|
userId: 'actor',
|
printRecordId: 'record',
|
fileVersionId: 'version',
|
reason: 'original reason',
|
bizSign: 'original-sign',
|
createdAt: 1700000000000,
|
state: 'OPEN',
|
numbersJson: JSON.stringify([{ serialNumber: 'original', countSerialNumber: '20260001' }]),
|
copies: 1,
|
batchesJson: '[]',
|
};
|
data = { chongda_fasong_id: 'request', biz_sign: 'new-sign', bupingheng_mingxi: [{ dayin_jilu_id: 'record', cuoshi: 'cuoshi3', yuanyin: 'new reason' }] };
|
submit = vi.fn((resolve: any) => {
|
attempt.documentId = 'doc';
|
resolve({ code: 200, data: { id: 'doc' } });
|
});
|
context = {
|
formData: data,
|
setFormData: (k: string, v: any) => {
|
data[k] = v;
|
},
|
formContext: { isEdit: false, submit, closeReprintForm: vi.fn().mockResolvedValue(undefined) },
|
};
|
mocks.post.mockImplementation(async ({ url }: any) => {
|
if (url.endsWith('jieshu-fasong')) attempt.state = 'CLOSED';
|
if (url.endsWith('queren-pici')) attempt.state = 'CONFIRMED';
|
return { code: 200, data: { ...attempt } };
|
});
|
mocks.print.mockImplementation(async () => {
|
attempt.state = 'CONFIRMED';
|
});
|
mocks.preview.mockResolvedValue(true);
|
});
|
describe('原生重打成功后登记及恢复', () => {
|
it('在线配置格式贯穿重打入口、原生表单、预览和实际发送,号码保持原流水', async () => {
|
const builder = async ({ serialNumber, countSerialNumber }: any) => [{ barValue: serialNumber, barText: `原件 ${serialNumber} / ${countSerialNumber || '历史无计数号'}` }];
|
mocks.get.mockResolvedValue({ code: 200, data: { locked: false, printRecordId: 'record' } });
|
await openDmsReprint('record', undefined, builder, ['RECORD', 'REPORT']);
|
context.params = mocks.open.mock.calls[0]![0].params;
|
expect(context.params.buildCustomBarcode).toBe(builder);
|
expect(context.params.watermarkBusinessTypes).toEqual(['RECORD', 'REPORT']);
|
await submitDmsReprint(context);
|
const preview = mocks.preview.mock.calls[0]![0];
|
expect(preview.watermarkType).toBe('print');
|
expect(preview.watermarkBusinessTypes).toEqual(['RECORD', 'REPORT']);
|
expect(preview.customBarcode).toEqual([{ barValue: 'original', barText: '原件 original / 20260001' }]);
|
await preview.reprint.send();
|
expect(mocks.print.mock.calls[0]![1].buildCustomBarcode).toBe(builder);
|
expect(mocks.print.mock.calls[0]![1].watermarkBusinessTypes).toEqual(['RECORD', 'REPORT']);
|
expect(mocks.print.mock.calls[0]![2].numbersJson).toBe(attempt.numbersJson);
|
});
|
it('共享格式接口失败时保留原重打任务,不打开预览、不发送、不提前登记', async () => {
|
const builder = async () => { throw new Error('格式接口不可用'); };
|
mocks.get.mockResolvedValue({ code: 200, data: { locked: false, printRecordId: 'record' } });
|
await openDmsReprint('record', undefined, builder);
|
context.params = mocks.open.mock.calls[0]![0].params;
|
const result = await submitDmsReprint(context);
|
expect(result.msg).toContain('格式接口不可用');
|
expect(mocks.preview).not.toHaveBeenCalled();
|
expect(mocks.print).not.toHaveBeenCalled();
|
expect(submit).not.toHaveBeenCalled();
|
});
|
it('历史无计数号重打和已保存登记恢复仍使用在线配置,不补号', async () => {
|
const builder = async ({ serialNumber, countSerialNumber }: any) => [{ barValue: serialNumber, barText: countSerialNumber || `历史 ${serialNumber}` }];
|
attempt.numbersJson = JSON.stringify([{ serialNumber: 'original' }]);
|
attempt.documentId = 'doc';
|
mocks.get.mockResolvedValue({ code: 200, data: attempt });
|
await resumeDmsReprint('record', 'request', undefined, builder);
|
const preview = mocks.preview.mock.calls[0]![0];
|
expect(preview.customBarcode).toEqual([{ barValue: 'original', barText: '历史 original' }]);
|
await preview.reprint.send();
|
expect(mocks.print.mock.calls[0]![1].buildCustomBarcode).toBe(builder);
|
expect(mocks.post.mock.calls.some(([req]: any) => req.url.endsWith('/yulan-canshu'))).toBe(false);
|
});
|
it('占用弹窗及未保存登记恢复均保留格式函数', async () => {
|
const builder = ({ serialNumber }: any) => [{ barValue: serialNumber, barText: serialNumber }];
|
mocks.get.mockResolvedValueOnce({ code: 200, data: { locked: true } }).mockResolvedValueOnce({ code: 200, data: attempt });
|
await openDmsReprint('record', undefined, builder);
|
expect(mocks.emit.mock.calls[0]![1].buildCustomBarcode).toBe(builder);
|
await resumeDmsReprint('record', 'request', undefined, builder);
|
expect(mocks.open.mock.calls[0]![0].params.buildCustomBarcode).toBe(builder);
|
});
|
it('统一弹窗预填一条固定措施,恢复保持原请求ID', async () => {
|
mocks.get.mockResolvedValue({ code: 200, data: attempt });
|
await resumeDmsReprint('record', 'request');
|
const config = mocks.open.mock.calls[0]![0];
|
expect(config.submitMode).toBe('form');
|
expect(config.params.requestId).toBe('request');
|
expect(config.params.bupingheng_mingxi).toEqual([{ dayin_jilu_id: 'record', cuoshi: 'cuoshi3', yuanyin: 'original reason' }]);
|
});
|
it('查询未锁定后才打开原生重打表单', async () => {
|
mocks.get.mockResolvedValue({ code: 200, data: { locked: false, printRecordId: 'record' } });
|
await openDmsReprint('record');
|
expect(mocks.get).toHaveBeenCalledWith({ url: expect.stringContaining('/record/lock-status') });
|
expect(mocks.open).toHaveBeenCalledTimes(1);
|
expect(mocks.emit).not.toHaveBeenCalled();
|
});
|
it('已有占用只打开状态弹窗,包括其他发起人的占用', async () => {
|
const status = { locked: true, printRecordId: 'record', requestId: 'request', owner: false, phase: 'READY' };
|
mocks.get.mockResolvedValue({ code: 200, data: status });
|
await openDmsReprint('record');
|
expect(mocks.open).not.toHaveBeenCalled();
|
expect(mocks.emit).toHaveBeenCalledWith(REPRINT_LOCK_EVENT, { status, onConfirm: undefined, buildCustomBarcode: undefined });
|
});
|
it('刷新状态只补交已保存回执,不打印也不自行登记', async () => {
|
attempt.state = 'OPEN';
|
attempt.batchesJson = JSON.stringify([{ id: 'batch', state: 'SENDING' }]);
|
receiptStore.set('dms-reprint-receipt:0:actor:request:batch', 'token');
|
mocks.get.mockResolvedValueOnce({ code: 200, data: attempt }).mockResolvedValueOnce({ code: 200, data: { locked: true, phase: 'REGISTRATION_PENDING' } });
|
const result = await refreshDmsReprintLock({ locked: true, printRecordId: 'record', requestId: 'request', owner: true });
|
expect(result.phase).toBe('REGISTRATION_PENDING');
|
expect(mocks.post).toHaveBeenCalledWith(
|
expect.objectContaining({ url: expect.stringContaining('queren-pici'), data: expect.objectContaining({ token: 'token' }) }),
|
expect.anything(),
|
);
|
expect(mocks.print).not.toHaveBeenCalled();
|
expect(submit).not.toHaveBeenCalled();
|
expect(receiptStore.size).toBe(0);
|
});
|
it('取消只使用原任务ID,后端拒绝时不打开新表单', async () => {
|
mocks.post.mockResolvedValue({ code: 400, msg: '任务已派发,不能取消' });
|
await expect(cancelDmsReprint('record', 'request')).rejects.toThrow('不能取消');
|
expect(mocks.open).not.toHaveBeenCalled();
|
expect(mocks.print).not.toHaveBeenCalled();
|
});
|
it('原任务已消失时恢复不偷偷创建新任务', async () => {
|
mocks.get.mockResolvedValue({ code: 200, data: null });
|
await expect(resumeDmsReprint('record', 'request')).rejects.toThrow('状态已变化');
|
expect(mocks.open).not.toHaveBeenCalled();
|
});
|
it('先暂存关闭表单再打开预览,手动发送成功后才原生登记', async () => {
|
await expect(submitDmsReprint(context)).resolves.toMatchObject({ code: 200, dmsReprintPrepared: true });
|
expect(submit).not.toHaveBeenCalled();
|
expect(data.biz_sign).toBe('original-sign');
|
expect(data.dengji_ren_id).toBe('actor');
|
expect(data.bupingheng_mingxi[0].yuanyin).toBe('original reason');
|
expect(context.formContext.closeReprintForm.mock.invocationCallOrder[0]!).toBeLessThan(mocks.preview.mock.invocationCallOrder[0]!);
|
expect(mocks.print).not.toHaveBeenCalled();
|
expect(mocks.preview.mock.calls[0]![0]).toMatchObject({
|
wenjianZhongxinId: 'version',
|
printOptions: { copies: 1 },
|
customBarcode: [{ barValue: 'original', barText: '打印流水号: original 计数流水号: 20260001' }],
|
});
|
await mocks.preview.mock.calls[0]![0].reprint.send();
|
expect(mocks.print).toHaveBeenCalledTimes(1);
|
expect(submit).toHaveBeenCalledTimes(1);
|
expect(mocks.print.mock.invocationCallOrder[0]!).toBeLessThan(submit.mock.invocationCallOrder[0]!);
|
expect(attempt.state).toBe('CLOSED');
|
});
|
it('打印成功登记失败保留确认状态,重试只补登记不再派发', async () => {
|
submit.mockImplementation((_: any, reject: any) => reject(new Error('save failed')));
|
await submitDmsReprint(context);
|
const send = mocks.preview.mock.calls[0]![0].reprint.send;
|
await expect(send()).rejects.toThrow('save failed');
|
expect(attempt.state).toBe('CONFIRMED');
|
expect(mocks.post.mock.calls.some(([r]: any[]) => r.url.endsWith('jieshu-fasong'))).toBe(false);
|
submit.mockImplementation((resolve: any) => {
|
attempt.documentId = 'doc';
|
resolve({ code: 200 });
|
});
|
await send();
|
expect(mocks.print).toHaveBeenCalledTimes(1);
|
expect(attempt.state).toBe('CLOSED');
|
});
|
it('发送成功后原生保存响应丢失按唯一关联恢复,不重复创建', async () => {
|
submit.mockImplementation((_: any, reject: any) => {
|
attempt.documentId = 'doc';
|
reject(new Error('network'));
|
});
|
await submitDmsReprint(context);
|
await mocks.preview.mock.calls[0]![0].reprint.send();
|
expect(submit).toHaveBeenCalledTimes(1);
|
expect(attempt.state).toBe('CLOSED');
|
});
|
it('原生非200响应不得标记登记完成,保留已发送事实', async () => {
|
submit.mockImplementation((resolve: any) => resolve({ code: 400, msg: 'save failed' }));
|
await submitDmsReprint(context);
|
await expect(mocks.preview.mock.calls[0]![0].reprint.send()).rejects.toThrow('save failed');
|
expect(attempt.state).toBe('CONFIRMED');
|
expect(mocks.emit).not.toHaveBeenCalledWith('DMS_REPRINT_COMPLETED', expect.anything());
|
});
|
it('预览打开失败保留技术任务,不生成不平衡登记', async () => {
|
mocks.preview.mockRejectedValue(new Error('preview failed'));
|
await expect(submitDmsReprint(context)).resolves.toMatchObject({ code: 200, dmsReprintPrepared: true, msg: expect.stringContaining('预览打开失败') });
|
expect(submit).not.toHaveBeenCalled();
|
expect(mocks.print).not.toHaveBeenCalled();
|
expect(attempt.state).toBe('OPEN');
|
});
|
it('未发送就取消只结束技术任务,不创建登记', async () => {
|
await submitDmsReprint(context);
|
mocks.post.mockImplementation(async () => {
|
attempt.state = 'CANCELLED';
|
return { code: 200, data: attempt };
|
});
|
await cancelDmsReprint('record', 'request');
|
await expect(mocks.preview.mock.calls[0]![0].reprint.send()).rejects.toThrow('已取消');
|
expect(submit).not.toHaveBeenCalled();
|
expect(mocks.print).not.toHaveBeenCalled();
|
});
|
it('已确认任务恢复表单后直接补登记,复用冻结签名且不打开预览', async () => {
|
attempt.state = 'CONFIRMED';
|
await expect(submitDmsReprint(context)).resolves.toMatchObject({ code: 200, data: { id: 'doc' } });
|
expect(data.biz_sign).toBe('original-sign');
|
expect(submit).toHaveBeenCalledTimes(1);
|
expect(mocks.preview).not.toHaveBeenCalled();
|
expect(mocks.print).not.toHaveBeenCalled();
|
});
|
it('迟到及并发成功回执只创建一次原生登记', async () => {
|
let transport: any;
|
mocks.print.mockImplementation(async (_: any, __: any, ___: any, t: any) => {
|
transport = t;
|
throw new Error('timeout');
|
});
|
await submitDmsReprint(context);
|
await expect(mocks.preview.mock.calls[0]![0].reprint.send()).rejects.toThrow('timeout');
|
expect(submit).not.toHaveBeenCalled();
|
const b = { id: 'batch', state: 'SENDING' };
|
await Promise.all([transport.submitted(attempt, b, 'token'), transport.submitted(attempt, b, 'token')]);
|
expect(submit).toHaveBeenCalledTimes(1);
|
expect(attempt.state).toBe('CLOSED');
|
});
|
it('暂存失败不关闭填写窗口,也不保存或打开预览', async () => {
|
mocks.post.mockRejectedValue(new Error('prepare failed'));
|
await expect(submitDmsReprint(context)).rejects.toThrow('prepare failed');
|
expect(context.formContext.closeReprintForm).not.toHaveBeenCalled();
|
expect(submit).not.toHaveBeenCalled();
|
expect(mocks.preview).not.toHaveBeenCalled();
|
});
|
it('部分回传或结果未知时不登记,全部成功才登记', async () => {
|
let transport: any;
|
mocks.print.mockImplementation(async (_: any, __: any, ___: any, t: any) => {
|
transport = t;
|
throw new Error('unknown');
|
});
|
await submitDmsReprint(context);
|
await expect(mocks.preview.mock.calls[0]![0].reprint.send()).rejects.toThrow('unknown');
|
mocks.post.mockImplementation(async () => ({ code: 200, data: { ...attempt } }));
|
await transport.submitted(attempt, { id: 'partial', state: 'SENDING' }, 'token');
|
expect(submit).not.toHaveBeenCalled();
|
attempt.state = 'CONFIRMED';
|
mocks.post.mockImplementation(async ({ url }: any) => {
|
if (url.endsWith('jieshu-fasong')) attempt.state = 'CLOSED';
|
return { code: 200, data: { ...attempt } };
|
});
|
await transport.submitted(attempt, { id: 'final', state: 'SENDING' }, 'token');
|
expect(submit).toHaveBeenCalledTimes(1);
|
expect(attempt.state).toBe('CLOSED');
|
});
|
it('已有登记恢复直接打开原任务预览,不再次签名登记或自动打印', async () => {
|
attempt.documentId = 'doc';
|
mocks.get.mockResolvedValue({ code: 200, data: attempt });
|
await resumeDmsReprint('record', 'request');
|
expect(mocks.open).not.toHaveBeenCalled();
|
expect(mocks.preview).toHaveBeenCalledTimes(1);
|
expect(submit).not.toHaveBeenCalled();
|
expect(mocks.print).not.toHaveBeenCalled();
|
});
|
it('同请求并发提交仅暂存并打开一次预览', async () => {
|
const first = submitDmsReprint(context);
|
const second = submitDmsReprint(context);
|
expect(first).toBe(second);
|
await first;
|
expect(submit).not.toHaveBeenCalled();
|
expect(mocks.preview).toHaveBeenCalledTimes(1);
|
});
|
it('无已保存登记时即使调用发送也不打印', async () => {
|
await expect(sendDmsReprint(attempt)).rejects.toThrow('恢复原生登记上下文');
|
expect(mocks.print).not.toHaveBeenCalled();
|
});
|
it('未知结果不能登记或结束任务', async () => {
|
attempt.documentId = 'doc';
|
mocks.print.mockRejectedValue(new Error('结果未知'));
|
await expect(sendDmsReprint(attempt)).rejects.toThrow('结果未知');
|
expect(submit).not.toHaveBeenCalled();
|
expect(mocks.post.mock.calls.some(([r]: any[]) => r.url.endsWith('jieshu-fasong'))).toBe(false);
|
});
|
it('旧流程已有登记的迟到回传复用原单据', async () => {
|
attempt.documentId = 'doc';
|
let transport: any;
|
mocks.print.mockImplementation(async (_: any, __: any, ___: any, t: any) => {
|
transport = t;
|
throw new Error('timeout');
|
});
|
await expect(sendDmsReprint(attempt)).rejects.toThrow('timeout');
|
const b = { id: 'batch', items: [0], pages: [1], state: 'SENDING' };
|
await transport.submitted(attempt, b, 'token');
|
expect(mocks.emit).toHaveBeenCalledWith('DMS_REPRINT_COMPLETED', { printRecordId: 'record', requestId: 'request' });
|
expect(submit).not.toHaveBeenCalled();
|
expect(receiptStore.size).toBe(0);
|
});
|
it('发送成功但更新执行失败,重试读取确认状态后仅更新执行', async () => {
|
attempt.documentId = 'doc';
|
mocks.post.mockImplementation(async ({ url }: any) => {
|
if (url.endsWith('jieshu-fasong')) throw new Error('finish failed');
|
return { code: 200, data: { ...attempt } };
|
});
|
await expect(sendDmsReprint(attempt)).rejects.toThrow('finish failed');
|
expect(attempt.state).toBe('CONFIRMED');
|
// 真实传输层对CONFIRMED直接返回;模拟此契约,不派发新批次。
|
mocks.print.mockImplementation(async (_: any, __: any, original: any) => {
|
expect(original.state).toBe('CONFIRMED');
|
});
|
mocks.post.mockImplementation(async ({ url }: any) => {
|
if (url.endsWith('jieshu-fasong')) attempt.state = 'CLOSED';
|
return { code: 200, data: { ...attempt } };
|
});
|
await sendDmsReprint(attempt);
|
expect(attempt.state).toBe('CLOSED');
|
expect(submit).not.toHaveBeenCalled();
|
});
|
});
|
describe('重打弹窗专属配置', () => {
|
it('仅目标入口隐藏子表增删按钮并锁恢复原因', () => {
|
const form = {
|
fields: [
|
{
|
__vModel__: 'bupingheng_mingxi',
|
columnBtnsList: [{ show: true }],
|
footerBtnsList: [{ show: true }],
|
__config__: { children: [{ __vModel__: 'cuoshi' }, { __vModel__: 'yuanyin' }] },
|
},
|
],
|
};
|
configureReprintModal('other', form, { dmsReprint: true });
|
expect(form.fields[0]!.columnBtnsList[0]!.show).toBe(true);
|
configureReprintModal(DMS_REPRINT_MODEL_ID, form, { dmsReprint: true, recovering: true });
|
expect(form.fields[0]!.columnBtnsList[0]!.show).toBe(false);
|
expect(form.fields[0]!.footerBtnsList[0]!.show).toBe(false);
|
expect((form.fields[0]!.__config__.children[1] as any).disabled).toBe(true);
|
});
|
it('仅已冻结任务恢复免再次签名,首次确认仍要求原生签名', () => {
|
const form = { confirmBtnConfig: { biz_sign_enabled: true }, fields: [] };
|
configureReprintModal(DMS_REPRINT_MODEL_ID, form, { dmsReprint: true });
|
expect(form.confirmBtnConfig.biz_sign_enabled).toBe(true);
|
configureReprintModal(DMS_REPRINT_MODEL_ID, form, { dmsReprint: true, recovering: true });
|
expect(form.confirmBtnConfig.biz_sign_enabled).toBe(false);
|
});
|
it('恢复使用冻结签名作为原生保存审计关联,其他业务不变', () => {
|
expect(reprintAuditSign(DMS_REPRINT_MODEL_ID, data, 'current-sign')).toBe('new-sign');
|
expect(reprintAuditSign('other', data, 'current-sign')).toBe('current-sign');
|
});
|
});
|