刘光辉
6 小时以前 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
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
/* eslint-disable vue/one-component-per-file -- 模拟表单解析器和三种弹窗外壳,验证真实宿主提交链路。 */
import { flushPromises, mount } from '@vue/test-utils';
 
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
 
import GlobalFormModal from './index.vue';
 
const mocks = vi.hoisted(() => ({
  events: new Map<string, (config: any) => Promise<void>>(),
  getConfigData: vi.fn(),
  getModelInfo: vi.fn(),
  createModel: vi.fn(),
  updateModel: vi.fn(),
  request: vi.fn(),
  error: vi.fn(),
}));
vi.mock('@jnpf/hooks', () => ({ useMessage: () => ({ createMessage: { error: mocks.error, success: vi.fn(), warning: vi.fn() } }) }));
vi.mock('@vben/stores', () => ({ useUserStore: () => ({ getUserInfo: {} }) }));
vi.mock('#/api/onlineDev/visualDev', () => mocks);
vi.mock('#/api/request', () => ({ defHttp: { get: vi.fn(), post: vi.fn() } }));
vi.mock('#/store', () => ({ useBaseStore: () => ({ getSysConfig: {} }) }));
vi.mock('#/utils/jnpf', () => ({
  onlineUtils: {
    request: mocks.request,
    getEmitter: () => ({ on: (name, handler) => mocks.events.set(name, handler), off: (name) => mocks.events.delete(name) }),
  },
}));
vi.mock('#/locales', () => ({ $t: (key, fallback) => fallback || key }));
vi.mock('#/components/FormExtraPanel/index.vue', () => ({ default: { render: () => null } }));
vi.mock('#/directives/disablePasswordAutofill', () => ({ vDisablePasswordAutofill: {} }));
vi.mock('@jnpf/ui/modal', async () => {
  const { defineComponent, h } = await import('vue');
  return {
    BasicModal: defineComponent({
      name: 'ModalShell',
      props: { open: Boolean, confirmLoading: Boolean, closeFunc: { type: Function, default: undefined } },
      setup:
        (_, { slots }) =>
        () =>
          h('div', slots.default?.()),
    }),
  };
});
vi.mock('@jnpf/ui/popup', async () => {
  const { BasicModal } = await import('@jnpf/ui/modal');
  return { BasicPopup: BasicModal };
});
vi.mock('@jnpf/ui/drawer', async () => {
  const { BasicModal } = await import('@jnpf/ui/modal');
  return { BasicDrawer: BasicModal };
});
vi.mock('@jnpf/utils', async () => {
  const { defineComponent, h } = await import('vue');
  const { getScriptFunc } = await import('../../../../../packages/jnpf/utils/src/helpers/common');
  return {
    getScriptFunc,
    getDateTimeUnit: () => 'day',
    createAsyncComponent: () => defineComponent({ name: 'ParserStub', emits: ['submit'], setup: () => () => h('div') }),
  };
});
 
let wrapper: ReturnType<typeof mount>;
const hook = `({ formData, formContext, onlineUtils }) => onlineUtils.request(
  '/api/dms/biz/wenjian/fenfa/dayin-jilu/' + formContext.id + '/qianshou-fafang',
  'post', { bizSign: formData.biz_sign, bizReviewSign: formData.biz_review_sign })`;
async function open(submitMode: string = 'form', funcs?: any, type = 'modal') {
  funcs ??= { customSubmit: true, customSubmitFunc: hook };
  mocks.getConfigData.mockResolvedValue({ data: { webType: 2, formData: JSON.stringify({ fields: [], funcs, popupType: type }) } });
  const config = { modelId: 'handover', id: 'print-1', type, submitMode, onConfirm: vi.fn(), onCancel: vi.fn(), onSubmit: vi.fn() };
  await mocks.events.get('OPEN_FORM_MODAL')!(config);
  await flushPromises();
  return config;
}
function submit(data?: any, auditBizSign = 'issuer-sign') {
  data ??= { biz_sign: 'issuer-sign', biz_review_sign: 'recipient-sign' };
  const callback = vi.fn();
  const setFormData = (key, value) => {
    data[key] = value;
  };
  wrapper.findComponent({ name: 'ParserStub' }).vm.$emit(
    'submit',
    data,
    callback,
    {
      formData: data,
      setFormData,
      onlineUtils: { request: mocks.request },
    },
    [],
    () => [],
    auditBizSign,
  );
  return callback;
}
beforeEach(() => {
  vi.clearAllMocks();
  mocks.getModelInfo.mockResolvedValue({ data: { id: 'print-1', data: '{}' } });
  mocks.updateModel.mockResolvedValue({ code: 200, data: { id: 'print-1' } });
  mocks.request.mockResolvedValue({ code: 200, msg: '发放成功', data: { id: 'print-1' } });
  wrapper = mount(GlobalFormModal, { global: { stubs: { AButton: true } } });
});
afterEach(() => wrapper.unmount());
 
describe('目标表单提交及成功回调', () => {
  it('原流水重打恢复通过原生createModel保存,并使用冻结签名作为审计关联', async () => {
    mocks.createModel.mockResolvedValue({ code: 200, data: { id: 'reprint-doc' } });
    mocks.getConfigData.mockResolvedValue({ data: { webType: 2, formData: JSON.stringify({ fields: [], funcs: {
      customSubmit: true,
      customSubmitFunc: `({setFormData,formContext}) => {
        setFormData('biz_sign','frozen-sign');
        return new Promise((resolve,reject)=>formContext.submit(resolve,reject));
      }`,
    } }) } });
    const config = { modelId: '37a318b30d7c4daf9670d141b33ed1b7', type: 'modal', submitMode: 'form', params: { dmsReprint: true }, onConfirm: vi.fn() };
    await mocks.events.get('OPEN_FORM_MODAL')!(config); await flushPromises();
    submit({ biz_sign: 'new-sign', chongda_fasong_id: 'request', bupingheng_mingxi: [{ dayin_jilu_id: 'copy', cuoshi: 'cuoshi3', yuanyin: 'reason' }] }, 'new-sign');
    await flushPromises();
    expect(mocks.createModel).toHaveBeenCalledWith(config.modelId, expect.objectContaining({ data: expect.stringContaining('frozen-sign') }), 'frozen-sign');
    expect(mocks.updateModel).not.toHaveBeenCalled();
    expect(config.onConfirm).toHaveBeenCalledWith({ success: true, data: { id: 'reprint-doc' } });
  });
 
  it.each(['default', 'custom'])('%s 保留打开失败时的原回调契约', async (mode) => {
    mocks.getConfigData.mockRejectedValueOnce(new Error('配置加载失败'));
    const config = await open(mode);
    expect(config.onCancel).not.toHaveBeenCalled();
    expect(config.onConfirm).not.toHaveBeenCalled();
  });
 
  it.each(['modal', 'fullScreen', 'drawer'])('%s 确认前取消不提交、不调用成功回调', async (type) => {
    const config = await open('form', undefined, type);
    const close = wrapper.findComponent({ name: 'ModalShell' }).props('closeFunc') as () => Promise<boolean>;
    expect(await close()).toBe(true);
    expect(config.onCancel).toHaveBeenCalledTimes(1);
    expect(config.onConfirm).not.toHaveBeenCalled();
    expect(mocks.request).not.toHaveBeenCalled();
    expect(mocks.updateModel).not.toHaveBeenCalled();
    expect(mocks.createModel).not.toHaveBeenCalled();
  });
 
  it.each(['modal', 'fullScreen', 'drawer'])('%s 等待发放请求成功并转发签名,进行中重复提交只执行一次', async (type) => {
    let resolve;
    mocks.request.mockImplementation(
      () =>
        new Promise((done) => {
          resolve = done;
        }),
    );
    const config = await open('form', undefined, type);
    const callback = submit(undefined, 'current-event-sign');
    submit();
    await flushPromises();
    expect(mocks.request).toHaveBeenCalledTimes(1);
    expect(mocks.request).toHaveBeenCalledWith(
      expect.stringContaining('/print-1/qianshou-fafang'),
      'post',
      { bizSign: 'issuer-sign', bizReviewSign: 'recipient-sign' },
      expect.objectContaining({ 'X-Audit-Biz-Sign': 'current-event-sign' }),
    );
    expect(mocks.updateModel).not.toHaveBeenCalled();
    expect(mocks.createModel).not.toHaveBeenCalled();
    expect(callback).not.toHaveBeenCalled();
    expect(config.onConfirm).not.toHaveBeenCalled();
    resolve({ code: 200, data: { id: 'print-1' } });
    await flushPromises();
    expect(callback).toHaveBeenCalledWith('print-1');
    expect(config.onConfirm).toHaveBeenCalledExactlyOnceWith({ success: true, data: { id: 'print-1' } });
  });
 
  it.each(['reject', 'code'])('接口失败(%s)不触发成功回调,允许同一表单重试', async (failure) => {
    const config = await open();
    if (failure === 'reject') mocks.request.mockRejectedValueOnce(new Error('发放失败'));
    else mocks.request.mockResolvedValueOnce({ code: 500, msg: '发放失败' });
    const callback = submit();
    await flushPromises();
    expect(config.onConfirm).not.toHaveBeenCalled();
    expect(callback).not.toHaveBeenCalled();
    expect(mocks.error).toHaveBeenCalledWith('发放失败');
    submit();
    await flushPromises();
    expect(config.onConfirm).toHaveBeenCalledTimes(1);
    expect(mocks.updateModel).not.toHaveBeenCalled();
  });
 
  it('目标脚本无效时不退回通用保存', async () => {
    const config = await open('form', { customSubmit: true, customSubmitFunc: '' });
    submit();
    await flushPromises();
    expect(config.onConfirm).not.toHaveBeenCalled();
    expect(mocks.updateModel).not.toHaveBeenCalled();
    expect(mocks.error).toHaveBeenCalled();
  });
 
  it('目标脚本可以显式调用原生保存,携带最新字段和签名', async () => {
    const config = await open('form', {
      customSubmit: true,
      customSubmitFunc: `({setFormData, formContext}) => {
      setFormData('beizhu', '修改后的备注');
      return new Promise((resolve, reject) => formContext.submit(resolve, reject));
    }`,
    });
    submit();
    await flushPromises();
    expect(mocks.updateModel).toHaveBeenCalledExactlyOnceWith(
      'handover',
      expect.objectContaining({
        onlineUtilsOpen: true,
        data: expect.stringContaining('修改后的备注'),
      }),
      'issuer-sign',
    );
    expect(config.onConfirm).toHaveBeenCalledTimes(1);
  });
 
  it.each(['default', 'form'])('%s 未启用自定义提交时仍走原生保存并返回 ID', async (mode) => {
    const config = await open(mode, { customSubmit: false });
    const callback = submit();
    await flushPromises();
    expect(mocks.updateModel).toHaveBeenCalledTimes(1);
    expect(callback).toHaveBeenCalledWith('print-1');
    expect(config.onConfirm).toHaveBeenCalledTimes(1);
    expect(mocks.request).not.toHaveBeenCalled();
  });
 
  it('未选择 form 模式时保留原默认保存语义', async () => {
    const config = await open('default');
    submit();
    await flushPromises();
    expect(mocks.updateModel).toHaveBeenCalledTimes(1);
    expect(mocks.request).not.toHaveBeenCalled();
    expect(config.onConfirm).toHaveBeenCalledTimes(1);
  });
 
  it.each(['modal', 'fullScreen', 'drawer'])('%s 旧默认模式仍不转发新增审计参数', async (type) => {
    const config = await open('default', undefined, type);
    delete config.submitMode; // 未传 submitMode 的旧调用也是默认模式。
    submit(undefined, 'must-not-forward');
    await flushPromises();
    expect(mocks.updateModel.mock.calls[0]?.[2]).toBeUndefined();
    expect(mocks.request).not.toHaveBeenCalled();
    expect(config.onConfirm).toHaveBeenCalledTimes(1);
  });
 
  it.each(['default', 'custom'])('%s 原提交失败保留弹窗,不调用成功或取消回调', async (mode) => {
    const config = await open(mode);
    if (mode === 'default') mocks.updateModel.mockRejectedValueOnce(new Error('save failed'));
    else config.onSubmit.mockRejectedValueOnce(new Error('custom failed'));
    const callback = submit();
    await flushPromises();
    expect(config.onConfirm).not.toHaveBeenCalled();
    expect(config.onCancel).not.toHaveBeenCalled();
    expect(callback).not.toHaveBeenCalled();
    expect(wrapper.findComponent({ name: 'ParserStub' }).exists()).toBe(true);
    submit();
    await flushPromises();
    expect(config.onConfirm).toHaveBeenCalledTimes(1);
    expect(mocks.request).not.toHaveBeenCalled();
  });
 
  it('调用方 custom 模式等待原 onSubmit Promise,仍不调用目标脚本或 afterSubmit', async () => {
    let resolve;
    const config = await open('custom');
    config.onSubmit.mockImplementation(
      () =>
        new Promise((done) => {
          resolve = done;
        }),
    );
    const callback = submit();
    await flushPromises();
    expect(config.onConfirm).not.toHaveBeenCalled();
    resolve();
    await flushPromises();
    expect(config.onConfirm).toHaveBeenCalledTimes(1);
    expect(callback).not.toHaveBeenCalled();
    expect(mocks.request).not.toHaveBeenCalled();
    expect(mocks.updateModel).not.toHaveBeenCalled();
  });
 
  it('调用方 custom 模式继续由 onSubmit 处理', async () => {
    const config = await open('custom');
    submit();
    await flushPromises();
    expect(config.onSubmit).toHaveBeenCalledTimes(1);
    expect(config.onConfirm).toHaveBeenCalledTimes(1);
    expect(mocks.updateModel).not.toHaveBeenCalled();
    expect(mocks.request).not.toHaveBeenCalled();
  });
});
 
 
describe('重打运行时属性与先保存收尾', () => {
  async function openReprint() {
    mocks.getConfigData.mockResolvedValue({ data: { webType: 2, formData: JSON.stringify({
      popupType: 'fullScreen',
      fields: [{ __vModel__: 'bupingheng_mingxi', __config__: { jnpfKey: 'table', children: [
        { __vModel__: '', controlKey: 'wenjian_mingcheng', __config__: { isVirtualField: true } },
      ] } }],
      funcs: { customSubmit: true, customSubmitFunc: `({ formData, setFormData, formContext, onlineUtils }) => {
        setFormData('bupingheng_mingxi', [{...formData.bupingheng_mingxi[0], yuanyin: 'frozen'}]);
        return onlineUtils.request('/reprint-test', 'post', {formData, formContext});
      }` },
    }) } });
    const config = { modelId: '37a318b30d7c4daf9670d141b33ed1b7', submitMode: 'form', params: { dmsReprint: true }, onConfirm: vi.fn() };
    await mocks.events.get('OPEN_FORM_MODAL')!(config);
    await flushPromises();
    return config;
  }
  it('回填保留文件展示属性,但原生保存仍剔除虚拟字段', async () => {
    let resolve!: (value: any) => void;
    mocks.request.mockReturnValue(new Promise(r => { resolve = r; }));
    const config = await openReprint();
    const runtime = { bupingheng_mingxi: [{ dayin_jilu_id: 'record', cuoshi: 'cuoshi3', wenjian_mingcheng: '原文件', yuanyin: 'reason' }] };
    submit(runtime);
    await flushPromises();
    expect(runtime.bupingheng_mingxi[0]).toMatchObject({ wenjian_mingcheng: '原文件', yuanyin: 'frozen' });
    const ctx = mocks.request.mock.calls[0]![2].formContext;
    mocks.createModel.mockResolvedValue({ code: 200, data: { id: 'doc' } });
    await new Promise((resolve, reject) => ctx.submit(resolve, reject));
    const saved = JSON.parse(mocks.createModel.mock.calls[0]![1].data);
    expect(saved.bupingheng_mingxi[0]).not.toHaveProperty('wenjian_mingcheng');
    expect(saved.bupingheng_mingxi[0].dayin_jilu_id).toBe('record');
    resolve({ code: 200, data: { id: 'doc' } });
    await flushPromises();
    expect(config.onConfirm).toHaveBeenCalledTimes(1);
    expect(wrapper.findComponent({ name: 'ModalShell' }).props('open')).toBe(false);
  });
  it('暂存关闭不触发登记成功回调,后续原生保存使用关闭前快照', async () => {
    let captured: any;
    mocks.request.mockImplementation(async (_url, _method, { formContext }) => {
      captured = formContext;
      await formContext.closeReprintForm();
      return { code: 200, dmsReprintPrepared: true };
    });
    const config = await openReprint();
    const runtime = { biz_sign: 'frozen-sign', chongda_fasong_id: 'request', bupingheng_mingxi: [{ dayin_jilu_id: 'record', cuoshi: 'cuoshi3', wenjian_mingcheng: '原文件', yuanyin: 'reason' }] };
    const callback = submit(runtime);
    await flushPromises();
    expect(mocks.createModel).not.toHaveBeenCalled();
    expect(config.onConfirm).not.toHaveBeenCalled();
    expect(callback).not.toHaveBeenCalled();
    expect(wrapper.findComponent({ name: 'ModalShell' }).props('open')).toBe(false);
    runtime.bupingheng_mingxi.length = 0;
    runtime.biz_sign = 'changed';
    mocks.createModel.mockResolvedValue({ code: 200, data: { id: 'doc' } });
    await new Promise((resolve, reject) => captured.submit(resolve, reject));
    const saved = JSON.parse(mocks.createModel.mock.calls[0]![1].data);
    expect(saved.bupingheng_mingxi[0]).toMatchObject({ dayin_jilu_id: 'record', yuanyin: 'frozen' });
    expect(saved.bupingheng_mingxi[0]).not.toHaveProperty('wenjian_mingcheng');
    expect(mocks.createModel.mock.calls[0]![2]).toBe('frozen-sign');
  });
  it('登记保存失败停止loading并保留原表单供重试', async () => {
    mocks.request.mockRejectedValue(new Error('登记失败'));
    const config = await openReprint();
    submit({ bupingheng_mingxi: [{ dayin_jilu_id: 'record', cuoshi: 'cuoshi3' }] });
    await flushPromises();
    expect(wrapper.findComponent({ name: 'ModalShell' }).props('confirmLoading')).toBe(false);
    expect(wrapper.findComponent({ name: 'ModalShell' }).props('open')).toBe(true);
    expect(config.onConfirm).not.toHaveBeenCalled();
  });
});