刘光辉
5 小时以前 cf11e63b659afcc9792ab3e395f1b45d844431a8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
/* @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);
  });
});