/* @vitest-environment happy-dom */
|
|
import { config, flushPromises, mount, shallowMount } from '@vue/test-utils';
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import VersionDrawer from '../VersionDrawer.vue';
|
import drawerSource from '../VersionDrawer.vue?raw';
|
|
config.global.stubs = { 'a-button': { template: '<button><slot /></button>' } };
|
|
function deferred<T>() {
|
let resolve!: (value: T) => void;
|
const promise = new Promise<T>((promiseResolve) => {
|
resolve = promiseResolve;
|
});
|
return { promise, resolve };
|
}
|
|
const apiMocks = vi.hoisted(() => ({ getFieldDetail: vi.fn(), getProviderProperties: vi.fn(), updateFieldVersion: vi.fn() }));
|
const sourceApiMocks = vi.hoisted(() => ({ getSourceReference: vi.fn() }));
|
const navigationMocks = vi.hoisted(() => ({ route: vi.fn() }));
|
const drawerState = vi.hoisted(() => ({ init: undefined as any }));
|
const formState = vi.hoisted(() => ({ config: undefined as any, setFieldsValue: vi.fn(), updateSchema: vi.fn(), validate: vi.fn() }));
|
const tableState = vi.hoisted(() => ({ config: undefined as any }));
|
|
vi.mock('#/api/x/eln/fields', () => ({
|
cancelFieldVersion: vi.fn(),
|
createFieldVersion: vi.fn(),
|
getFieldDetail: apiMocks.getFieldDetail,
|
getProviderProperties: apiMocks.getProviderProperties,
|
preReleaseFieldVersion: vi.fn(),
|
publishFieldVersion: vi.fn(),
|
switchCurrentFieldVersion: vi.fn(),
|
updateFieldVersion: apiMocks.updateFieldVersion,
|
}));
|
|
vi.mock('#/api/x/eln/sources', () => ({ getSourceReference: sourceApiMocks.getSourceReference }));
|
vi.mock('#/utils/jnpf', () => ({ getJnpfAppEnCode: () => '', onlineUtils: navigationMocks }));
|
|
vi.mock('@jnpf/hooks', () => ({
|
useMessage: () => ({ createMessage: { success: vi.fn(), warning: vi.fn() } }),
|
}));
|
|
vi.mock('@jnpf/ui/drawer', () => ({
|
BasicDrawer: { name: 'BasicDrawer', template: '<div><slot /></div>' },
|
useDrawerInner: (init: any) => {
|
drawerState.init = init;
|
return [vi.fn(), { changeLoading: vi.fn(), closeDrawer: vi.fn() }];
|
},
|
}));
|
|
vi.mock('@jnpf/ui/form', () => ({
|
BasicForm: { name: 'BasicForm', template: '<div />' },
|
useForm: (config: any) => {
|
formState.config = config;
|
return [vi.fn(), { resetFields: vi.fn(), setFieldsValue: formState.setFieldsValue, updateSchema: formState.updateSchema, validate: formState.validate }];
|
},
|
}));
|
|
vi.mock('@jnpf/ui/modal', () => ({
|
BasicModal: { name: 'BasicModal', props: ['title'], template: '<div :data-title="title"><slot /></div>' },
|
useModal: () => [vi.fn(), { closeModal: vi.fn(), openModal: vi.fn() }],
|
}));
|
|
vi.mock('@jnpf/ui/vxeTable', () => ({
|
BasicVxeTable: {
|
name: 'BasicVxeTable',
|
props: ['dataSource'],
|
template:
|
'<div><slot v-for="record in dataSource" :record="record" name="shujuLeixing" /><slot v-for="record in dataSource" :record="record" name="action" /></div>',
|
},
|
TableAction: { name: 'TableAction', props: ['actions'], template: '<div>{{ actions.map((action) => action.label).join(" ") }}</div>' },
|
useVxeTable: (config: any) => {
|
tableState.config = config;
|
return [vi.fn()];
|
},
|
}));
|
|
describe('eLN field version drawer', () => {
|
beforeEach(() => {
|
vi.clearAllMocks();
|
apiMocks.getProviderProperties.mockResolvedValue({ data: [] });
|
sourceApiMocks.getSourceReference.mockResolvedValue({ data: undefined });
|
apiMocks.updateFieldVersion.mockResolvedValue({ data: {} });
|
apiMocks.getFieldDetail.mockResolvedValue({
|
data: {
|
bizEnabled: 'enabled',
|
dangqianZiduanBanbenId: 'v1',
|
id: 'field-1',
|
versions: [
|
{
|
banbenHao: 1,
|
binding: { read: { source: 'LIMS_TASK' } },
|
bizStatus: 'PUBLISHED',
|
id: 'v1',
|
shujuLeixing: 'TEXT',
|
ziduanDingyiId: 'field-1',
|
},
|
{
|
banbenHao: 2,
|
binding: { read: { source: 'NONE' } },
|
bizStatus: 'DRAFT',
|
id: 'v2',
|
shujuLeixing: 'DECIMAL',
|
ziduanDingyiId: 'field-1',
|
},
|
],
|
ziduanBianma: 'sample_code_ab12cd34',
|
zuoyongFanwei: 'GLOBAL',
|
},
|
});
|
});
|
|
it('loads and saves arbitrary checkbox selection group recommendations', async () => {
|
apiMocks.getFieldDetail.mockResolvedValue({
|
data: {
|
bizEnabled: 'enabled',
|
id: 'field-1',
|
versions: [
|
{
|
banbenHao: 1,
|
binding: { read: { source: 'NONE' } },
|
bizStatus: 'DRAFT',
|
id: 'v1',
|
recommendedCheckboxGroupCode: 'review_result',
|
recommendedCheckboxMaxSelected: null,
|
recommendedCheckboxMinSelected: 2,
|
shujuLeixing: 'CHECKBOX',
|
ziduanDingyiId: 'field-1',
|
ziduanMingcheng: '复核结论',
|
},
|
],
|
ziduanBianma: 'review_result_ab12cd34',
|
zuoyongFanwei: 'GLOBAL',
|
},
|
});
|
formState.validate.mockResolvedValue({
|
checkboxGroupUnlimited: false,
|
piciMoshi: 'RECORD_PRIVATE',
|
readSource: 'NONE',
|
recommendedCheckboxGroupCode: 'review_result',
|
recommendedCheckboxMaxSelected: 8,
|
recommendedCheckboxMinSelected: 3,
|
shifouBitian: false,
|
shujuLeixing: 'CHECKBOX',
|
ziduanMingcheng: '复核结论',
|
});
|
const wrapper = mount(VersionDrawer, {
|
global: { stubs: { 'a-alert': true, 'a-descriptions': true, 'a-descriptions-item': true, 'a-form-item': true, 'a-tag': true, 'a-textarea': true } },
|
});
|
await drawerState.init({ fieldId: 'field-1' });
|
await flushPromises();
|
const actions = wrapper.findComponent({ name: 'TableAction' }).props('actions') as any[];
|
await actions.find((action) => action.label === '编辑').onClick();
|
await flushPromises();
|
|
expect(formState.setFieldsValue).toHaveBeenCalledWith(
|
expect.objectContaining({
|
checkboxGroupUnlimited: true,
|
recommendedCheckboxGroupCode: 'review_result',
|
recommendedCheckboxMinSelected: 2,
|
}),
|
);
|
wrapper.findAllComponents({ name: 'BasicModal' })[0]!.vm.$emit('ok');
|
await flushPromises();
|
expect(apiMocks.updateFieldVersion).toHaveBeenCalledWith(
|
'v1',
|
expect.objectContaining({
|
recommendedCheckboxGroupCode: 'review_result',
|
recommendedCheckboxMaxSelected: 8,
|
recommendedCheckboxMinSelected: 3,
|
}),
|
);
|
});
|
|
it('shows field data types in Chinese', async () => {
|
const wrapper = mount(VersionDrawer, {
|
global: {
|
stubs: {
|
'a-alert': true,
|
'a-descriptions': true,
|
'a-descriptions-item': true,
|
'a-form-item': true,
|
'a-tag': true,
|
'a-textarea': true,
|
},
|
},
|
});
|
await drawerState.init({ fieldId: 'field-1' });
|
await flushPromises();
|
|
expect(wrapper.text()).toContain('单行文本');
|
expect(wrapper.text()).toContain('数值');
|
expect(wrapper.text()).not.toContain('TEXT');
|
expect(wrapper.text()).not.toContain('DECIMAL');
|
});
|
|
it('loads independent V1/V2 history into the platform table', async () => {
|
shallowMount(VersionDrawer, {
|
global: {
|
stubs: {
|
'a-alert': true,
|
'a-descriptions': true,
|
'a-descriptions-item': true,
|
'a-form-item': true,
|
'a-tag': true,
|
'a-textarea': true,
|
},
|
},
|
});
|
await drawerState.init({ fieldId: 'field-1' });
|
await flushPromises();
|
|
expect(apiMocks.getFieldDetail).toHaveBeenCalledWith('field-1');
|
expect(tableState.config.dataSource.value).toHaveLength(2);
|
expect(tableState.config.pagination).toBe(false);
|
});
|
|
it('allows published versions to be viewed without exposing a save action', async () => {
|
const wrapper = mount(VersionDrawer, {
|
global: {
|
stubs: {
|
'a-alert': true,
|
'a-descriptions': true,
|
'a-descriptions-item': true,
|
'a-form-item': true,
|
'a-tag': true,
|
'a-textarea': true,
|
},
|
},
|
});
|
await drawerState.init({ fieldId: 'field-1' });
|
await flushPromises();
|
|
expect(wrapper.text()).toContain('查看');
|
expect(drawerSource).toContain(':disabled="isViewingVersion"');
|
expect(drawerSource).toContain(':show-ok-btn="!isViewingVersion"');
|
});
|
|
it('keeps every field read-only and shows the version number when viewing a published version', async () => {
|
const wrapper = mount(VersionDrawer, {
|
global: {
|
stubs: {
|
'a-alert': true,
|
'a-descriptions': true,
|
'a-descriptions-item': true,
|
'a-form-item': true,
|
'a-tag': true,
|
'a-textarea': true,
|
},
|
},
|
});
|
await drawerState.init({ fieldId: 'field-1' });
|
await flushPromises();
|
|
const publishedVersion = tableState.config.dataSource.value[0];
|
const actions = wrapper.findComponent({ name: 'TableAction' }).props('actions') as any[];
|
await actions.find((action) => action.label === '查看').onClick();
|
await flushPromises();
|
|
const providerProperty = formState.config.schemas.find((schema: any) => schema.field === 'providerProperty');
|
expect(providerProperty.dynamicDisabled({ model: { readSource: 'LIMS_TASK' } })).toBe(true);
|
expect(wrapper.findAll('[data-title]').map((element) => element.attributes('data-title'))).toContain(`查看字段版本 - V${publishedVersion.banbenHao}`);
|
});
|
|
it('does not validate a source property before an external source is selected', () => {
|
shallowMount(VersionDrawer, {
|
global: {
|
stubs: {
|
'a-alert': true,
|
'a-descriptions': true,
|
'a-descriptions-item': true,
|
'a-form-item': true,
|
'a-tag': true,
|
'a-textarea': true,
|
},
|
},
|
});
|
const providerProperty = formState.config.schemas.find((schema: any) => schema.field === 'providerProperty');
|
|
expect(providerProperty.dynamicRules({ model: { readSource: undefined } })).toEqual([]);
|
expect(providerProperty.dynamicRules({ model: { readSource: 'NONE' } })).toEqual([]);
|
expect(providerProperty.dynamicRules({ model: { readSource: 'LIMS_TASK' } })).toHaveLength(1);
|
expect(providerProperty.dynamicRules({ model: { readSource: 'SYSTEM' } })).toHaveLength(1);
|
});
|
|
it('uses shared status actions and keeps template fields away from external sources', () => {
|
expect(drawerSource).toContain('versionActions({');
|
expect(drawerSource).toContain("field.value?.zuoyongFanwei === 'GLOBAL'");
|
expect(drawerSource).toContain('READ_SOURCE_OPTIONS');
|
expect(drawerSource).toContain('NONE_READ_SOURCE_OPTIONS');
|
});
|
|
it('presents field ownership and remark in the detail summary', () => {
|
expect(drawerSource).toContain('字段详情');
|
expect(drawerSource).toContain('field.mubanBianhao');
|
expect(drawerSource).toContain('field.beizhu');
|
});
|
|
it('keeps the summary and version table away from the drawer edges', () => {
|
expect(drawerSource).toContain('padding: 16px 24px;');
|
expect(drawerSource).toContain('padding: 12px 24px 0;');
|
});
|
|
it('shows both current and target version numbers in switch confirmation', () => {
|
expect(drawerSource).toContain(`V\${currentVersion.value.banbenHao}`);
|
expect(drawerSource).toContain(`V\${version.banbenHao}`);
|
expect(drawerSource).toContain('switchCurrentFieldVersion');
|
});
|
|
it('keeps the draft form mounted between openings so it can be reset before showing', () => {
|
expect(drawerSource).not.toMatch(/registerVersionModal[^>]*destroy-on-close/);
|
expect(drawerSource).toMatch(/registerVersionModal[^>]*force-render/);
|
});
|
|
it('fills the drawer body so the version table can use the remaining height', () => {
|
expect(drawerSource).toMatch(/<BasicDrawer[^>]*class="full-drawer"/);
|
});
|
|
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(VersionDrawer, {
|
global: {
|
stubs: {
|
'a-alert': true,
|
'a-descriptions': true,
|
'a-descriptions-item': true,
|
'a-form-item': true,
|
'a-tag': true,
|
'a-textarea': true,
|
},
|
},
|
});
|
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(VersionDrawer, {
|
global: {
|
stubs: {
|
'a-alert': true,
|
'a-descriptions': true,
|
'a-descriptions-item': true,
|
'a-form-item': true,
|
'a-tag': true,
|
'a-textarea': true,
|
},
|
},
|
});
|
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('shows and saves the fixed revision of a configured LIMS property', 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 = mount(VersionDrawer, {
|
global: {
|
stubs: {
|
'a-alert': true,
|
'a-descriptions': true,
|
'a-descriptions-item': true,
|
'a-form-item': true,
|
'a-tag': true,
|
'a-textarea': true,
|
},
|
},
|
});
|
await drawerState.init({ fieldId: 'field-1' });
|
await flushPromises();
|
const actions = wrapper.findAllComponents({ name: 'TableAction' })[1]!.props('actions') as any[];
|
await actions.find((action) => action.label === '编辑').onClick();
|
const readSource = formState.config.schemas.find((schema: any) => schema.field === 'readSource');
|
await readSource.componentProps.onChange('LIMS_TASK');
|
await flushPromises();
|
|
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.findAllComponents({ name: 'BasicModal' })[0]!.vm.$emit('ok');
|
await flushPromises();
|
expect(apiMocks.updateFieldVersion).toHaveBeenCalledWith(
|
'v2',
|
expect.objectContaining({
|
binding: {
|
read: { property: 'verification.sample_name', propertyRevision: 3, source: 'LIMS_TASK' },
|
write: { target: 'ELN_RECORD' },
|
},
|
}),
|
);
|
});
|
|
it('opens the independent LIMS source page from draft field editing', async () => {
|
const wrapper = mount(VersionDrawer, {
|
global: {
|
stubs: {
|
'a-alert': true,
|
'a-descriptions': true,
|
'a-descriptions-item': true,
|
'a-form-item': true,
|
'a-tag': true,
|
'a-textarea': true,
|
},
|
},
|
});
|
await drawerState.init({ fieldId: 'field-1' });
|
await flushPromises();
|
const actions = wrapper.findAllComponents({ name: 'TableAction' })[1]!.props('actions') as any[];
|
await actions.find((action) => action.label === '编辑').onClick();
|
const readSource = formState.config.schemas.find((schema: any) => schema.field === 'readSource');
|
|
expect(wrapper.find('[data-test="maintain-lims-source"]').exists()).toBe(false);
|
await readSource.componentProps.onChange('LIMS_TASK');
|
await flushPromises();
|
expect(wrapper.get('[data-test="maintain-lims-source"]').text()).toContain('维护 LIMS 数据项');
|
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);
|
|
await readSource.componentProps.onChange('SYSTEM');
|
await flushPromises();
|
expect(wrapper.find('[data-test="maintain-lims-source"]').exists()).toBe(false);
|
});
|
|
it('preserves a pinned property revision when the current catalog has moved on', async () => {
|
apiMocks.getFieldDetail.mockResolvedValue({
|
data: {
|
bizEnabled: 'enabled',
|
id: 'field-1',
|
versions: [
|
{
|
banbenHao: 2,
|
binding: { read: { property: 'verification.sample_name', propertyRevision: 3, source: 'LIMS_TASK' } },
|
bizStatus: 'DRAFT',
|
id: 'v2',
|
piciMoshi: 'RECORD_PRIVATE',
|
shifouBitian: 0,
|
shujuLeixing: 'TEXT',
|
ziduanDingyiId: 'field-1',
|
ziduanMingcheng: '供试品名称',
|
},
|
],
|
ziduanBianma: 'sample_name_ab12cd34',
|
zuoyongFanwei: 'GLOBAL',
|
},
|
});
|
apiMocks.getProviderProperties.mockResolvedValue({
|
data: [
|
{
|
dataType: 'TEXT',
|
enabled: true,
|
label: '供试品名称(新规则)',
|
property: 'verification.sample_name',
|
propertyRevision: 4,
|
readOnly: true,
|
source: 'LIMS_TASK',
|
},
|
],
|
});
|
sourceApiMocks.getSourceReference.mockResolvedValue({
|
data: { dataType: 'TEXT', displayName: '供试品名称', enabled: false, propertyCode: 'verification.sample_name', revision: 3 },
|
});
|
formState.validate.mockResolvedValue({
|
piciMoshi: 'RECORD_PRIVATE',
|
providerProperty: 'verification.sample_name',
|
readSource: 'LIMS_TASK',
|
shifouBitian: false,
|
shujuLeixing: 'TEXT',
|
ziduanMingcheng: '供试品名称',
|
});
|
const wrapper = mount(VersionDrawer, {
|
global: {
|
stubs: {
|
'a-alert': true,
|
'a-button': true,
|
'a-descriptions': true,
|
'a-descriptions-item': true,
|
'a-form-item': true,
|
'a-tag': true,
|
'a-textarea': true,
|
},
|
},
|
});
|
await drawerState.init({ fieldId: 'field-1' });
|
await flushPromises();
|
const actions = wrapper.findComponent({ name: 'TableAction' }).props('actions') as any[];
|
await actions.find((action) => action.label === '编辑').onClick();
|
await flushPromises();
|
|
expect(sourceApiMocks.getSourceReference).toHaveBeenCalledWith('verification.sample_name', 3);
|
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.findAllComponents({ name: 'BasicModal' })[0]!.vm.$emit('ok');
|
await flushPromises();
|
expect(apiMocks.updateFieldVersion).toHaveBeenCalledWith(
|
'v2',
|
expect.objectContaining({
|
binding: expect.objectContaining({
|
read: { property: 'verification.sample_name', propertyRevision: 3, source: 'LIMS_TASK' },
|
}),
|
}),
|
);
|
});
|
});
|