/* 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();
|
});
|
});
|