/* @vitest-environment happy-dom */
|
|
import { config, flushPromises, mount, shallowMount } from '@vue/test-utils';
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import FieldForm from '../FieldForm.vue';
|
|
config.global.stubs = {
|
AButton: { template: '<button><slot /></button>' },
|
AInput: { template: '<div><slot /><slot name="suffix" /></div>' },
|
ATooltip: { template: '<div><slot /></div>' },
|
};
|
|
function deferred<T>() {
|
let resolve!: (value: T) => void;
|
const promise = new Promise<T>((promiseResolve) => {
|
resolve = promiseResolve;
|
});
|
return { promise, resolve };
|
}
|
|
const apiMocks = vi.hoisted(() => ({ createField: vi.fn(), getProviderProperties: vi.fn(), suggestFieldCode: vi.fn() }));
|
const navigationMocks = vi.hoisted(() => ({ route: vi.fn() }));
|
const formState = vi.hoisted(() => ({
|
config: undefined as any,
|
resetFields: vi.fn(),
|
setFieldsValue: vi.fn(),
|
updateSchema: vi.fn(),
|
validate: vi.fn(),
|
}));
|
const modalState = vi.hoisted(() => ({ closeModal: vi.fn(), init: undefined as any, openCodeAssistModal: vi.fn() }));
|
|
vi.mock('#/api/x/eln/fields', () => ({
|
createField: apiMocks.createField,
|
getProviderProperties: apiMocks.getProviderProperties,
|
suggestFieldCode: apiMocks.suggestFieldCode,
|
}));
|
|
vi.mock('#/utils/jnpf', () => ({ getJnpfAppEnCode: () => '', onlineUtils: navigationMocks }));
|
|
vi.mock('@jnpf/hooks', () => ({ useMessage: () => ({ createMessage: { success: vi.fn() } }) }));
|
|
vi.mock('@jnpf/ui/form', () => ({
|
BasicForm: {
|
name: 'BasicForm',
|
template: `<div><slot name="recommendedCheckboxGroupCode"
|
:model="{ recommendedCheckboxGroupCode: 'original_group' }"
|
field="recommendedCheckboxGroupCode" /></div>`,
|
},
|
useForm: (config: any) => {
|
formState.config = config;
|
return [vi.fn(), formState];
|
},
|
}));
|
|
vi.mock('../CodeAssistModal.vue', () => ({
|
default: {
|
emits: ['confirm', 'register'],
|
name: 'CodeAssistModal',
|
template: '<div data-test="mock-code-assist-modal" />',
|
},
|
}));
|
|
vi.mock('@jnpf/ui/modal', () => ({
|
BasicModal: { name: 'BasicModal', template: '<div><slot /></div>' },
|
useModal: () => [vi.fn(), { openModal: modalState.openCodeAssistModal }],
|
useModalInner: (init: any) => {
|
modalState.init = init;
|
return [vi.fn(), { changeOkLoading: vi.fn(), closeModal: modalState.closeModal }];
|
},
|
}));
|
|
describe('eln global field form', () => {
|
beforeEach(() => {
|
vi.clearAllMocks();
|
apiMocks.createField.mockResolvedValue({ data: {}, msg: '创建成功' });
|
apiMocks.getProviderProperties.mockResolvedValue({
|
data: [{ dataType: 'TEXT', label: '检验依据', property: 'test.basis', readOnly: true, source: 'LIMS_TASK' }],
|
});
|
apiMocks.suggestFieldCode.mockResolvedValue({ data: { code: 'fuhe_jieguo' } });
|
formState.validate.mockResolvedValue({
|
danwei: '',
|
controlDateFormat: '',
|
controlDefaultChecked: false,
|
controlDefaultDate: '',
|
controlDefaultOption: undefined,
|
controlDefaultValue: '标准品',
|
controlOptionsText: '',
|
controlPlaceholder: '请输入供试品名称',
|
piciMoshi: 'RECORD_PRIVATE',
|
providerProperty: undefined,
|
readSource: 'NONE',
|
shifouBitian: true,
|
shujuLeixing: 'TEXT',
|
ziduanMingcheng: '样品编号',
|
});
|
});
|
|
it('does not expose editable identity code or scope fields', () => {
|
shallowMount(FieldForm);
|
const fields = formState.config.schemas.map((schema: any) => schema.field);
|
|
expect(fields).not.toContain('ziduanBianma');
|
expect(fields).not.toContain('zuoyongFanwei');
|
});
|
|
it('reserves enough label width for selection group recommendation labels', () => {
|
shallowMount(FieldForm);
|
|
expect(formState.config.labelWidth).toBe(130);
|
});
|
|
it('uses business fields instead of a raw JSON control configuration editor', () => {
|
shallowMount(FieldForm);
|
const fields = formState.config.schemas.map((schema: any) => schema.field);
|
|
expect(fields).not.toContain('kongjianPeizhiText');
|
expect(fields).toContain('controlPlaceholder');
|
expect(fields).toContain('controlDefaultValue');
|
expect(fields).toContain('controlOptionsText');
|
});
|
|
it('shows and submits a unit only for decimal fields', async () => {
|
formState.validate.mockResolvedValue({
|
danwei: 'mg',
|
piciMoshi: 'RECORD_PRIVATE',
|
readSource: 'NONE',
|
shifouBitian: false,
|
shujuLeixing: 'TEXT',
|
ziduanMingcheng: '样品名称',
|
});
|
const wrapper = shallowMount(FieldForm);
|
const unitSchema = formState.config.schemas.find((schema: any) => schema.field === 'danwei');
|
|
expect(unitSchema.ifShow({ model: { shujuLeixing: 'DECIMAL' } })).toBe(true);
|
expect(unitSchema.ifShow({ model: { shujuLeixing: 'TEXT' } })).toBe(false);
|
|
modalState.init({});
|
wrapper.findComponent({ name: 'BasicModal' }).vm.$emit('ok');
|
await flushPromises();
|
|
expect(apiMocks.createField).toHaveBeenCalledTimes(1);
|
expect(apiMocks.createField.mock.calls[0]?.[0]?.danwei).toBeUndefined();
|
});
|
|
it('shows only the control settings owned by the selected data type', () => {
|
shallowMount(FieldForm);
|
const typeOwnedFields = [
|
'danwei',
|
'controlPlaceholder',
|
'controlDefaultValue',
|
'controlOptionsText',
|
'controlDefaultOption',
|
'controlDateFormat',
|
'controlDefaultDate',
|
'controlDefaultChecked',
|
'recommendedCheckboxGroupCode',
|
'recommendedCheckboxMinSelected',
|
'checkboxGroupUnlimited',
|
'recommendedCheckboxMaxSelected',
|
];
|
const expectedVisibleFields: Record<string, string[]> = {
|
CHECKBOX: [
|
'controlDefaultChecked',
|
'recommendedCheckboxGroupCode',
|
'recommendedCheckboxMinSelected',
|
'checkboxGroupUnlimited',
|
'recommendedCheckboxMaxSelected',
|
],
|
DATE: ['controlDateFormat', 'controlDefaultDate'],
|
DATETIME: ['controlDateFormat', 'controlDefaultDate'],
|
DECIMAL: ['danwei', 'controlPlaceholder', 'controlDefaultValue'],
|
SELECT: ['controlPlaceholder', 'controlOptionsText', 'controlDefaultOption'],
|
TEXT: ['controlPlaceholder', 'controlDefaultValue'],
|
TEXTAREA: ['controlPlaceholder', 'controlDefaultValue'],
|
};
|
|
for (const [dataType, expectedFields] of Object.entries(expectedVisibleFields)) {
|
const model = {
|
checkboxGroupUnlimited: false,
|
readSource: 'NONE',
|
recommendedCheckboxGroupCode: 'review_result',
|
shujuLeixing: dataType,
|
};
|
const visibleFields = typeOwnedFields.filter((field) => {
|
const schema = formState.config.schemas.find((item: any) => item.field === field);
|
return schema.ifShow?.({ model }) !== false;
|
});
|
|
expect(visibleFields, dataType).toEqual(expectedFields);
|
}
|
});
|
|
it('hides provider property without an external source and validates it only on submit', async () => {
|
formState.validate.mockResolvedValue(false);
|
const wrapper = shallowMount(FieldForm);
|
const providerProperty = formState.config.schemas.find((schema: any) => schema.field === 'providerProperty');
|
|
expect(providerProperty.ifShow({ model: { readSource: 'NONE' } })).toBe(false);
|
expect(providerProperty.ifShow({ model: { readSource: 'LIMS_TASK' } })).toBe(true);
|
expect(providerProperty.dynamicRules({ model: { readSource: 'LIMS_TASK' } })).toEqual([]);
|
|
modalState.init({});
|
wrapper.findComponent({ name: 'BasicModal' }).vm.$emit('ok');
|
await flushPromises();
|
|
expect(providerProperty.dynamicRules({ model: { readSource: 'LIMS_TASK' } })).toEqual([
|
expect.objectContaining({ message: '请选择来源属性', required: true }),
|
]);
|
});
|
|
it('uses type-specific date pickers and Chinese-friendly display format options', () => {
|
shallowMount(FieldForm);
|
const dateFormat = formState.config.schemas.find((schema: any) => schema.field === 'controlDateFormat');
|
const defaultDate = formState.config.schemas.find((schema: any) => schema.field === 'controlDefaultDate');
|
|
expect(dateFormat.component).toBe('Select');
|
expect(dateFormat.componentProps({ formModel: { shujuLeixing: 'DATE' } }).options).toEqual([
|
expect.objectContaining({ id: 'yyyy-MM-dd' }),
|
expect.objectContaining({ id: 'yyyy/MM/dd' }),
|
expect.objectContaining({ id: 'yyyy年MM月dd日' }),
|
]);
|
expect(dateFormat.componentProps({ formModel: { shujuLeixing: 'DATETIME' } }).options).toEqual([
|
expect.objectContaining({ id: 'yyyy-MM-dd HH:mm' }),
|
expect.objectContaining({ id: 'yyyy-MM-dd HH:mm:ss' }),
|
expect.objectContaining({ id: 'yyyy/MM/dd HH:mm' }),
|
expect.objectContaining({ id: 'yyyy/MM/dd HH:mm:ss' }),
|
expect.objectContaining({ id: 'yyyy年MM月dd日 HH:mm:ss' }),
|
]);
|
|
expect(defaultDate.component).toBe('DatePicker');
|
expect(defaultDate.componentProps({ formModel: { controlDateFormat: 'yyyy-MM-dd', shujuLeixing: 'DATE' } })).toEqual(
|
expect.objectContaining({ format: 'YYYY-MM-DD', showTime: false, valueFormat: 'YYYY-MM-DD' }),
|
);
|
expect(
|
defaultDate.componentProps({
|
formModel: { controlDateFormat: 'yyyy-MM-dd HH:mm:ss', shujuLeixing: 'DATETIME' },
|
}),
|
).toEqual(
|
expect.objectContaining({
|
format: 'YYYY-MM-DD HH:mm:ss',
|
showTime: { format: 'HH:mm:ss' },
|
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
}),
|
);
|
});
|
|
it('keeps checkbox selection limits visible and enables them after a group code is entered', () => {
|
shallowMount(FieldForm);
|
const schemas = formState.config.schemas;
|
const minSelected = schemas.find((schema: any) => schema.field === 'recommendedCheckboxMinSelected');
|
const unlimited = schemas.find((schema: any) => schema.field === 'checkboxGroupUnlimited');
|
const maxSelected = schemas.find((schema: any) => schema.field === 'recommendedCheckboxMaxSelected');
|
const withoutCode = { checkboxGroupUnlimited: false, readSource: 'NONE', recommendedCheckboxGroupCode: '', shujuLeixing: 'CHECKBOX' };
|
const withCode = { ...withoutCode, recommendedCheckboxGroupCode: 'review_result' };
|
|
expect(minSelected.ifShow({ model: withoutCode })).toBe(true);
|
expect(unlimited.ifShow({ model: withoutCode })).toBe(true);
|
expect(maxSelected.ifShow({ model: withoutCode })).toBe(true);
|
expect(minSelected.dynamicDisabled({ model: withoutCode })).toBe(true);
|
expect(maxSelected.dynamicDisabled({ model: withoutCode })).toBe(true);
|
expect(minSelected.dynamicDisabled({ model: withCode })).toBe(false);
|
expect(maxSelected.dynamicDisabled({ model: withCode })).toBe(false);
|
});
|
|
it('creates a global field with a fixed ELN record write target', async () => {
|
const wrapper = shallowMount(FieldForm);
|
modalState.init({});
|
wrapper.findComponent({ name: 'BasicModal' }).vm.$emit('ok');
|
await flushPromises();
|
|
expect(apiMocks.createField).toHaveBeenCalledWith(
|
expect.objectContaining({
|
binding: { read: { source: 'NONE' }, write: { target: 'ELN_RECORD' } },
|
kongjianPeizhi: { defaultValue: '标准品', placeholder: '请输入供试品名称' },
|
shifouBitian: 1,
|
zuoyongFanwei: 'GLOBAL',
|
}),
|
);
|
expect(modalState.closeModal).toHaveBeenCalled();
|
});
|
|
it('configures an arbitrary selection group recommendation only for editable checkboxes', async () => {
|
formState.validate.mockResolvedValue({
|
binding: undefined,
|
checkboxGroupUnlimited: false,
|
controlDefaultChecked: false,
|
piciMoshi: 'RECORD_PRIVATE',
|
readSource: 'NONE',
|
recommendedCheckboxGroupCode: 'review_result',
|
recommendedCheckboxMaxSelected: 5,
|
recommendedCheckboxMinSelected: 2,
|
shifouBitian: false,
|
shujuLeixing: 'CHECKBOX',
|
ziduanMingcheng: '复核结论',
|
});
|
const wrapper = shallowMount(FieldForm);
|
const schemas = formState.config.schemas;
|
|
expect(
|
schemas
|
.find((schema: any) => schema.field === 'recommendedCheckboxGroupCode')
|
.ifShow({
|
model: { readSource: 'NONE', shujuLeixing: 'CHECKBOX' },
|
}),
|
).toBe(true);
|
expect(
|
schemas
|
.find((schema: any) => schema.field === 'recommendedCheckboxGroupCode')
|
.ifShow({
|
model: { readSource: 'NONE', shujuLeixing: 'TEXT' },
|
}),
|
).toBe(false);
|
|
modalState.init({});
|
wrapper.findComponent({ name: 'BasicModal' }).vm.$emit('ok');
|
await flushPromises();
|
|
expect(apiMocks.createField).toHaveBeenCalledWith(
|
expect.objectContaining({
|
recommendedCheckboxGroupCode: 'review_result',
|
recommendedCheckboxMaxSelected: 5,
|
recommendedCheckboxMinSelected: 2,
|
}),
|
);
|
});
|
|
it('offers code assistance for checkbox group recommendations and fills the confirmed code', async () => {
|
const wrapper = mount(FieldForm);
|
const groupCodeSchema = formState.config.schemas.find((schema: any) => schema.field === 'recommendedCheckboxGroupCode');
|
|
expect(groupCodeSchema.slot).toBe('recommendedCheckboxGroupCode');
|
expect(wrapper.get('[data-test="recommended-checkbox-code-assist"]').attributes('aria-label')).toBe('中文转拼音');
|
|
await wrapper.get('[data-test="recommended-checkbox-code-assist"]').trigger('click');
|
expect(modalState.openCodeAssistModal).toHaveBeenCalledWith(true);
|
|
wrapper.getComponent({ name: 'CodeAssistModal' }).vm.$emit('confirm', 'fuhe_jieguo');
|
|
expect(formState.setFieldsValue).toHaveBeenCalledWith({ recommendedCheckboxGroupCode: 'fuhe_jieguo' });
|
});
|
|
it('loads provider properties and forces record-private mode for external sources', async () => {
|
shallowMount(FieldForm);
|
const readSource = formState.config.schemas.find((schema: any) => schema.field === 'readSource');
|
await readSource.componentProps.onChange('LIMS_TASK');
|
|
expect(apiMocks.getProviderProperties).toHaveBeenCalledWith('LIMS_TASK');
|
expect(formState.setFieldsValue).toHaveBeenCalledWith({ piciMoshi: 'RECORD_PRIVATE', providerProperty: undefined });
|
expect(formState.updateSchema).toHaveBeenCalledWith(
|
expect.objectContaining({ field: 'providerProperty', componentProps: expect.objectContaining({ options: expect.any(Array) }) }),
|
);
|
});
|
|
it('keeps the newest provider options when an older request finishes last', async () => {
|
const limsResponse = deferred<any>();
|
apiMocks.getProviderProperties.mockImplementation((source: string) => {
|
if (source === 'LIMS_TASK') return limsResponse.promise;
|
return Promise.resolve({
|
data: [{ dataType: 'TEXT', label: '当前用户', property: 'user.name', readOnly: true, source: 'SYSTEM' }],
|
});
|
});
|
shallowMount(FieldForm);
|
const readSource = formState.config.schemas.find((schema: any) => schema.field === 'readSource');
|
|
const limsChange = readSource.componentProps.onChange('LIMS_TASK');
|
await Promise.resolve();
|
await Promise.resolve();
|
const systemChange = readSource.componentProps.onChange('SYSTEM');
|
await systemChange;
|
limsResponse.resolve({
|
data: [{ dataType: 'TEXT', label: '检验依据', property: 'test.basis', readOnly: true, source: 'LIMS_TASK' }],
|
});
|
await limsChange;
|
|
const providerUpdates = formState.updateSchema.mock.calls.map(([schema]) => schema).filter((schema) => schema.field === 'providerProperty');
|
expect(providerUpdates.at(-1).componentProps.options).toEqual([{ fullName: '当前用户', id: 'user.name' }]);
|
});
|
|
it('keeps safe empty provider options when loading fails', async () => {
|
apiMocks.getProviderProperties.mockRejectedValue(new Error('network error'));
|
shallowMount(FieldForm);
|
const readSource = formState.config.schemas.find((schema: any) => schema.field === 'readSource');
|
|
await expect(readSource.componentProps.onChange('LIMS_TASK')).resolves.toBeUndefined();
|
|
const providerUpdates = formState.updateSchema.mock.calls.map(([schema]) => schema).filter((schema) => schema.field === 'providerProperty');
|
expect(providerUpdates.at(-1).componentProps.options).toEqual([]);
|
});
|
|
it('creates a LIMS-bound field with the selected fixed property revision', async () => {
|
apiMocks.getProviderProperties.mockResolvedValue({
|
data: [
|
{
|
dataType: 'TEXT',
|
enabled: true,
|
label: '供试品名称',
|
property: 'verification.sample_name',
|
propertyRevision: 3,
|
readOnly: true,
|
source: 'LIMS_TASK',
|
},
|
],
|
});
|
formState.validate.mockResolvedValue({
|
piciMoshi: 'RECORD_PRIVATE',
|
providerProperty: 'verification.sample_name',
|
readSource: 'LIMS_TASK',
|
shifouBitian: false,
|
shujuLeixing: 'TEXT',
|
ziduanMingcheng: '供试品名称',
|
});
|
const wrapper = shallowMount(FieldForm);
|
modalState.init({});
|
const readSource = formState.config.schemas.find((schema: any) => schema.field === 'readSource');
|
await readSource.componentProps.onChange('LIMS_TASK');
|
const providerUpdates = formState.updateSchema.mock.calls.map(([schema]) => schema).filter((schema) => schema.field === 'providerProperty');
|
expect(providerUpdates.at(-1).componentProps.options).toEqual([{ fullName: '供试品名称(版本 3)', id: 'verification.sample_name' }]);
|
|
wrapper.findComponent({ name: 'BasicModal' }).vm.$emit('ok');
|
await flushPromises();
|
|
expect(apiMocks.createField).toHaveBeenCalledWith(
|
expect.objectContaining({
|
binding: {
|
read: { property: 'verification.sample_name', propertyRevision: 3, source: 'LIMS_TASK' },
|
write: { target: 'ELN_RECORD' },
|
},
|
}),
|
);
|
});
|
|
it('opens the independent LIMS source page instead of embedding the maintenance workspace', async () => {
|
const wrapper = mount(FieldForm);
|
modalState.init({});
|
const readSource = formState.config.schemas.find((schema: any) => schema.field === 'readSource');
|
await readSource.componentProps.onChange('LIMS_TASK');
|
await flushPromises();
|
|
await wrapper.get('[data-test="maintain-lims-source"]').trigger('click');
|
expect(navigationMocks.route).toHaveBeenCalledWith('/eln/sources', { title: 'LIMS 数据项' });
|
expect(wrapper.find('[data-test="lims-source-workspace"]').exists()).toBe(false);
|
expect(wrapper.findComponent({ name: 'SourceWorkspace' }).exists()).toBe(false);
|
});
|
});
|