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