刘光辉
3 小时以前 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
/* eslint-disable vue/one-component-per-file -- 测试中分别模拟弹窗容器和表单解析器。 */
import { flushPromises, mount } from '@vue/test-utils';
 
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
 
import FormPopup from './FormPopup.vue';
 
const mocks = vi.hoisted(() => ({
  capture: vi.fn(),
  confirm: vi.fn(),
  createModel: vi.fn(),
  customFormInit: vi.fn(),
  error: vi.fn(),
  getDataInterfaceRes: vi.fn(),
  getLaunchFlowParamList: vi.fn(),
  getParamList: vi.fn(),
  init: undefined as ((data: any) => void) | undefined,
  launchFlow: vi.fn(),
  reset: vi.fn(),
  runtime: {} as any,
  selectedRows: vi.fn(),
  sign: vi.fn(),
  signedUtils: vi.fn(),
  submit: vi.fn(),
  validate: vi.fn(),
  warning: vi.fn(),
}));
 
vi.mock('@jnpf/hooks', () => ({
  useMessage: () => ({ createConfirm: mocks.confirm, createMessage: { error: mocks.error, success: vi.fn(), warning: mocks.warning } }),
}));
vi.mock('@vben/stores', () => ({ useUserStore: () => ({ getUserInfo: {} }) }));
vi.mock('#/api/onlineDev/visualDev', () => ({ createModel: mocks.createModel, launchFlow: mocks.launchFlow }));
vi.mock('#/api/systemData/dataInterface', () => ({ getDataInterfaceRes: mocks.getDataInterfaceRes }));
vi.mock('#/api/onlineDev/auditHeaders', () => ({ createAuditCorrelationId: () => 'audit-id' }));
vi.mock('#/locales', () => ({ $t: (value: string, fallback?: string) => fallback || value }));
vi.mock('#/utils/jnpf', () => ({
  getLaunchFlowParamList: mocks.getLaunchFlowParamList,
  getParamList: mocks.getParamList,
  onlineUtils: { capture: mocks.capture, sign: mocks.sign },
}));
vi.mock('#/components/FormGenerator/src/helper/auditDisplay', () => ({
  createAuditRequestOnlineUtils: (utils: any) => utils,
  createSignedAuditRequestOnlineUtils: mocks.signedUtils,
}));
vi.mock('../list/CustomForm.vue', async () => {
  const { defineComponent, h } = await import('vue');
  return {
    default: defineComponent({
      setup(_, { expose }) {
        expose({ init: mocks.customFormInit });
        return () => h('div');
      },
    }),
  };
});
vi.mock('@jnpf/ui/popup', async () => {
  const { defineComponent, h } = await import('vue');
  return {
    BasicPopup: defineComponent({
      setup:
        (_, { slots }) =>
        () =>
          h('div', [h('header', slots.insertToolbar?.()), slots.default?.()]),
    }),
    usePopupInner: (init: (data: any) => void) => {
      mocks.init = init;
      return [vi.fn(), { changeLoading: vi.fn(), setPopupProps: vi.fn() }];
    },
  };
});
vi.mock('@jnpf/utils', async () => {
  const { defineComponent, h, inject } = await import('vue');
  const { getScriptFunc } = await import('../../../../../../../packages/jnpf/utils/src/helpers/common');
  return {
    createAsyncComponent: () =>
      defineComponent({
        name: 'ParserStub',
        emits: ['runtimeChange', 'submit'],
        setup(_, { expose }) {
          const onlineUtils = inject('onlineUtils');
          expose({
            getRuntimeParameter: () => ({ ...mocks.runtime, getTableSelectedRows: mocks.selectedRows, onlineUtils }),
            handleReset: mocks.reset,
            handleSubmit: mocks.submit,
            handleValidate: mocks.validate,
          });
          return () => h('div');
        },
      }),
    getDateTimeUnit: () => 'day',
    getScriptFunc,
  };
});
 
describe('pure form toolbar and save', () => {
  let wrapper: ReturnType<typeof mount>;
 
  beforeEach(() => {
    vi.resetAllMocks();
    mocks.runtime = { formData: { id: '', status: 'ready' } };
    mocks.selectedRows.mockReturnValue([]);
    mocks.validate.mockResolvedValue(true);
    mocks.getDataInterfaceRes.mockResolvedValue({ msg: 'done' });
    mocks.launchFlow.mockResolvedValue({ msg: 'done' });
    mocks.signedUtils.mockImplementation((utils, getSign) => ({ ...utils, credential: getSign() }));
  });
 
  afterEach(() => {
    wrapper?.unmount();
    wrapper = undefined as any;
  });
 
  async function open(customSubmit = false, config: any = {}, isPreview = false) {
    if (!wrapper) wrapper = mount(FormPopup, { global: { stubs: { AButton: { template: '<button><slot /></button>' } } } });
    mocks.init?.({
      modelId: 'model',
      isPreview,
      formData: JSON.stringify({
        fields: [],
        funcs: { customSubmit, customSubmitFunc: '({ formContext }) => new Promise((resolve, reject) => formContext.submit(resolve, reject))' },
        ...config,
      }),
    });
    await flushPromises();
  }
 
  function customButton(actionConfig: any = {}, extra: any = {}) {
    return {
      value: 'action',
      label: '业务操作',
      show: true,
      actionConfig: { btnType: 2, func: '({ onlineUtils }) => onlineUtils.capture()', ...actionConfig },
      ...extra,
    };
  }
 
  async function click(label = '业务操作') {
    const button = wrapper.findAll('button').find((item) => item.text() === label);
    expect(button, `button ${label}`).toBeDefined();
    await button!.trigger('click');
    await flushPromises();
  }
 
  it.each([undefined, true, false])('hides confirm only for explicit false (%s)', async (hasConfirmBtn) => {
    await open(false, { hasConfirmBtn });
    expect(wrapper.findAll('button').some((button) => button.text() === 'common.okText')).toBe(hasConfirmBtn !== false);
    expect(wrapper.findAll('button').some((button) => button.text() === 'common.resetText')).toBe(true);
    if (hasConfirmBtn !== false) {
      await click('common.okText');
      expect(mocks.submit).toHaveBeenCalledOnce();
    }
  });
 
  it('runs a button with latest selected tuples without validating or submitting the scan form', async () => {
    await open(false, {
      hasConfirmBtn: false,
      formCustomBtns: [
        customButton({
          enableFormValidate: false,
          func: '({ formData, data, getTableSelectedRows, onlineUtils }) => onlineUtils.capture(formData, data, getTableSelectedRows("items"))',
        }),
      ],
    });
    mocks.runtime.formData = { id: '', status: 'changed' };
    const rows = [[2, { wenjian_id: 'doc-2' }]];
    mocks.selectedRows.mockReturnValue(rows);
    await click();
    expect(mocks.capture).toHaveBeenCalledExactlyOnceWith(mocks.runtime.formData, mocks.runtime.formData, rows);
    expect(mocks.selectedRows).toHaveBeenCalledWith('items');
    expect(mocks.validate).not.toHaveBeenCalled();
    expect(mocks.submit).not.toHaveBeenCalled();
    expect(mocks.createModel).not.toHaveBeenCalled();
    expect(mocks.reset).not.toHaveBeenCalled();
  });
 
  it('updates enable rules when runtime data changes and omits hidden buttons', async () => {
    await open(false, {
      formCustomBtns: [
        customButton({ enableFunc: '({ data }) => data.status === "ready"' }),
        customButton({}, { value: 'hidden', label: '隐藏按钮', show: false }),
      ],
    });
    expect(wrapper.text()).toContain('业务操作');
    expect(wrapper.text()).not.toContain('隐藏按钮');
    mocks.runtime.formData.status = 'blocked';
    wrapper.findComponent({ name: 'ParserStub' }).vm.$emit('runtimeChange');
    await flushPromises();
    expect(wrapper.text()).not.toContain('业务操作');
  });
 
  it.each([true, false])('validates by default and executes only on success (%s)', async (valid) => {
    mocks.validate.mockResolvedValue(valid);
    await open(false, { formCustomBtns: [customButton()] });
    await click();
    expect(mocks.validate).toHaveBeenCalledOnce();
    expect(mocks.capture).toHaveBeenCalledTimes(valid ? 1 : 0);
    expect(mocks.createModel).not.toHaveBeenCalled();
  });
 
  it('discards an old action when the form is reopened during validation', async () => {
    let resolveValidate!: (value: boolean) => void;
    mocks.validate.mockReturnValue(new Promise((resolve) => (resolveValidate = resolve)));
    await open(false, { formCustomBtns: [customButton()] });
    await click();
    await open(false);
    resolveValidate(true);
    await flushPromises();
    expect(mocks.capture).not.toHaveBeenCalled();
  });
 
  it('lets scripts validate before explicitly requesting a signature', async () => {
    await open(false, {
      formCustomBtns: [
        customButton({
          biz_sign_enabled: true,
          func: '({ formData, sign }) => { if (formData.status !== "ready") return; return sign(({ bizSign, onlineUtils }) => onlineUtils.capture(bizSign, onlineUtils.credential)); }',
        }),
      ],
    });
    mocks.runtime.formData.status = 'blocked';
    await click();
    expect(mocks.sign).not.toHaveBeenCalled();
    mocks.runtime.formData.status = 'ready';
    await click();
    expect(mocks.sign).toHaveBeenCalledOnce();
    expect(mocks.capture).not.toHaveBeenCalled();
    const options = mocks.sign.mock.calls[0]![0];
    expect(options.metaData).toMatchObject({ biz_form_id: 'model', biz_button: '业务操作', biz_data: [mocks.runtime.formData] });
    options.onCancel();
    expect(mocks.capture).not.toHaveBeenCalled();
    options.onSubmit({ biz_sign: 'sign-id' });
    expect(mocks.capture).toHaveBeenCalledExactlyOnceWith('sign-id', 'sign-id');
  });
 
  it('does not run a pending signed callback after reset', async () => {
    await open(false, { formCustomBtns: [customButton({ biz_sign_enabled: true, func: '({ sign }) => sign(({ onlineUtils }) => onlineUtils.capture())' })] });
    await click();
    const options = mocks.sign.mock.calls[0]![0];
    await click('common.resetText');
    options.onSubmit({ biz_sign: 'old-sign' });
    expect(mocks.capture).not.toHaveBeenCalled();
  });
 
  it('calls sign callbacks directly when electronic signing is disabled', async () => {
    await open(false, { formCustomBtns: [customButton({ func: '({ sign }) => sign(({ bizSign, onlineUtils }) => onlineUtils.capture(bizSign))' })] });
    await click();
    expect(mocks.capture).toHaveBeenCalledExactlyOnceWith(undefined);
    expect(mocks.sign).not.toHaveBeenCalled();
  });
 
  it('opens an associated form with the current record', async () => {
    await open(false, { formCustomBtns: [customButton({ btnType: 1, modelId: 'target' })] });
    await click();
    expect(mocks.customFormInit).toHaveBeenCalledWith(expect.objectContaining({ menuId: 'target', record: mocks.runtime.formData, recordModelId: 'model' }));
    expect(mocks.createModel).not.toHaveBeenCalled();
  });
 
  it('runs an interface only after its signature and confirmation', async () => {
    mocks.getParamList.mockReturnValue([{ field: 'status', defaultValue: 'ready' }]);
    await open(false, { formCustomBtns: [customButton({ btnType: 3, interfaceId: 'interface', templateJson: [], useConfirm: true, biz_sign_enabled: true })] });
    await click();
    expect(mocks.confirm).not.toHaveBeenCalled();
    mocks.sign.mock.calls[0]![0].onSubmit({ biz_sign: 'sign-id' });
    expect(mocks.confirm).toHaveBeenCalledOnce();
    expect(mocks.getDataInterfaceRes).not.toHaveBeenCalled();
    await mocks.confirm.mock.calls[0]![0].onOk();
    expect(mocks.getParamList).toHaveBeenCalledWith([], mocks.runtime.formData);
    expect(mocks.getDataInterfaceRes).toHaveBeenCalledExactlyOnceWith('interface', { paramList: [{ field: 'status', defaultValue: 'ready' }] });
  });
 
  it('launches a configured flow with mapped form data', async () => {
    mocks.getLaunchFlowParamList.mockReturnValue({ status: 'ready' });
    await open(false, { formCustomBtns: [customButton({ btnType: 4, launchFlow: { flowId: 'flow', transferList: [], currentUser: true } })] });
    await click();
    expect(mocks.getLaunchFlowParamList).toHaveBeenCalledWith([], mocks.runtime.formData);
    expect(mocks.launchFlow).toHaveBeenCalledWith('model', expect.objectContaining({ template: 'flow', btnCode: 'action', dataList: [{ status: 'ready' }] }));
  });
 
  it('prevents custom actions in preview', async () => {
    await open(false, { formCustomBtns: [customButton()] }, true);
    await click();
    expect(mocks.warning).toHaveBeenCalled();
    expect(mocks.validate).not.toHaveBeenCalled();
    expect(mocks.capture).not.toHaveBeenCalled();
  });
 
  it('reports rejected script promises without saving the form', async () => {
    await open(false, { formCustomBtns: [customButton({ func: '() => Promise.reject(new Error("业务失败"))' })] });
    await click();
    expect(mocks.error).toHaveBeenCalledWith('业务失败');
    expect(mocks.createModel).not.toHaveBeenCalled();
  });
 
  it.each([false, true])('passes the saved ID before resetting (customSubmit=%s)', async (customSubmit) => {
    await open(customSubmit);
    let resolveSave!: (response: any) => void;
    mocks.createModel.mockReturnValue(new Promise((resolve) => (resolveSave = resolve)));
    const callback = vi.fn(() => expect(mocks.reset).not.toHaveBeenCalled());
    wrapper.findComponent({ name: 'ParserStub' }).vm.$emit('submit', { id: '', name: 'new' }, callback);
    await flushPromises();
    expect(callback).not.toHaveBeenCalled();
 
    resolveSave({ data: { id: 'saved-id' }, msg: 'saved' });
    await flushPromises();
    expect(callback).toHaveBeenCalledExactlyOnceWith('saved-id');
    expect(mocks.reset).toHaveBeenCalledOnce();
  });
 
  it.each([false, true])('does not call afterSubmit or reset on failed save (customSubmit=%s)', async (customSubmit) => {
    await open(customSubmit);
    mocks.createModel.mockRejectedValue(new Error('save failed'));
    const callback = vi.fn();
    wrapper.findComponent({ name: 'ParserStub' }).vm.$emit('submit', { name: 'new' }, callback);
    await flushPromises();
    expect(callback).not.toHaveBeenCalled();
    expect(mocks.reset).not.toHaveBeenCalled();
  });
});