/* @vitest-environment happy-dom */
|
|
import type { FieldDataType } from '#/api/x/eln/fields';
|
|
import { flushPromises, mount } from '@vue/test-utils';
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import SourceWorkspace from '../SourceWorkspace.vue';
|
|
const apiMocks = vi.hoisted(() => ({
|
createSourceDataItem: vi.fn(),
|
createSourcePath: vi.fn(),
|
getSourceDataItems: vi.fn(),
|
getSourceObjectFields: vi.fn(),
|
getSourceObjects: vi.fn(),
|
getSourcePaths: vi.fn(),
|
getSourcePreviewSamples: vi.fn(),
|
previewSourceDataItem: vi.fn(),
|
publishSourceDataItem: vi.fn(),
|
saveSourceDataItem: vi.fn(),
|
setSourcePropertyEnabled: vi.fn(),
|
suggestFieldCode: vi.fn(),
|
}));
|
const navigationMocks = vi.hoisted(() => ({ route: vi.fn() }));
|
|
vi.mock('#/api/x/eln/fields', () => ({ suggestFieldCode: apiMocks.suggestFieldCode }));
|
vi.mock('#/api/x/eln/sources', () => apiMocks);
|
vi.mock('#/utils/jnpf', () => ({ onlineUtils: navigationMocks }));
|
vi.mock('@jnpf/ui/drawer', () => ({
|
BasicDrawer: { name: 'BasicDrawer', template: '<aside><slot /><slot name="footer" /></aside>' },
|
useDrawer: () => [vi.fn(), { closeDrawer: vi.fn(), openDrawer: vi.fn() }],
|
}));
|
vi.mock('@jnpf/ui/form', () => ({
|
BasicForm: { name: 'BasicForm', template: '<form />' },
|
useForm: () => [vi.fn(), { getFieldsValue: vi.fn(() => ({})), resetFields: vi.fn() }],
|
}));
|
vi.mock('@jnpf/ui/modal', () => ({
|
BasicModal: { name: 'BasicModal', template: '<section><slot /></section>' },
|
useModal: () => [vi.fn(), { openModal: vi.fn() }],
|
}));
|
vi.mock('@jnpf/ui/vxeTable', () => ({
|
BasicVxeTable: {
|
name: 'BasicVxeTable',
|
props: { dataSource: { default: () => [], type: Array } },
|
template:
|
'<div><slot name="tableTitle" /><div v-for="record in dataSource" :key="record.id"><span>{{ record.displayName }}</span><span>{{ record.sourcePath || record.path }}</span><slot name="dataType" :record="record" /><slot name="status" :record="record" /><slot name="action" :record="record" /></div></div>',
|
},
|
TableAction: {
|
name: 'TableAction',
|
props: ['actions'],
|
template: '<span><button v-for="action in actions" :key="action.label" @click="action.onClick">{{ action.label }}</button></span>',
|
},
|
useVxeTable: () => [vi.fn(), { setLoading: vi.fn() }],
|
}));
|
|
const publishedItem = {
|
dataType: 'DATE',
|
displayName: '生产日期',
|
editVersion: 1,
|
enabled: true,
|
id: 'item-1',
|
outputKey: 'production_date',
|
outputName: '生产日期',
|
propertyCode: 'yangpin_shengchan_riqi',
|
returnContent: '原值',
|
revision: 1,
|
scope: '全部检验项',
|
sourcePath: '检验项 > 请验单 > 生产日期',
|
sourcePathId: 'path-1',
|
status: 'PUBLISHED',
|
};
|
|
function mountWorkspace(fieldDataType?: FieldDataType) {
|
return mount(SourceWorkspace, {
|
props: fieldDataType ? { fieldDataType } : {},
|
global: {
|
stubs: {
|
'a-alert': { template: '<div><slot /><slot name="action" /></div>' },
|
'a-button': {
|
emits: ['click'],
|
props: ['disabled'],
|
template: '<button :disabled="disabled" @click="$emit(\'click\')"><slot name="icon" /><slot /></button>',
|
},
|
'a-input': {
|
emits: ['blur', 'update:value'],
|
props: ['disabled', 'value'],
|
template: '<input :disabled="disabled" :value="value" @blur="$emit(\'blur\')" @input="$emit(\'update:value\', $event.target.value)" />',
|
},
|
'a-select': {
|
emits: ['change', 'update:value'],
|
props: ['options', 'value'],
|
template:
|
'<select :value="value" @change="$emit(\'update:value\', $event.target.value); $emit(\'change\', $event.target.value)"><option value=""></option><option v-for="item in options" :key="item.value" :value="item.value">{{ item.label }}</option></select>',
|
},
|
'a-tag': { template: '<span><slot /></span>' },
|
'a-tooltip': { template: '<span><slot /></span>' },
|
},
|
},
|
});
|
}
|
|
describe('lIMS 数据项工作台', () => {
|
beforeEach(() => {
|
vi.clearAllMocks();
|
apiMocks.getSourceDataItems.mockResolvedValue({ data: [publishedItem] });
|
apiMocks.getSourcePaths.mockResolvedValue({
|
data: [
|
{
|
code: 'request',
|
displayName: '所属请验单',
|
editVersion: 1,
|
id: 'path-1',
|
path: '检验项 > 请验单 > 生产日期',
|
outputs: [{ dataType: 'DATE', displayName: '生产日期', key: 'production_date', path: '检验项 > 请验单 > 生产日期', returnContent: '原值' }],
|
revision: 1,
|
scope: '全部检验项',
|
status: 'PUBLISHED',
|
},
|
],
|
});
|
apiMocks.getSourcePreviewSamples.mockResolvedValue({ data: [{ batchNo: 'B001', id: 'task-1', sampleName: '样品 A', testName: '含量', testNo: 'JY001' }] });
|
apiMocks.suggestFieldCode.mockResolvedValue({ data: { code: 'yangpin_shengchan_riqi' } });
|
apiMocks.createSourceDataItem.mockResolvedValue({ data: { ...publishedItem, editVersion: 0, id: 'draft-1', status: 'DRAFT' } });
|
apiMocks.saveSourceDataItem.mockResolvedValue({ data: { ...publishedItem, editVersion: 0, id: 'draft-2', revision: 2, status: 'DRAFT' } });
|
apiMocks.previewSourceDataItem.mockResolvedValue({ data: { value: '2026-09-07' } });
|
apiMocks.publishSourceDataItem.mockResolvedValue({ data: publishedItem });
|
apiMocks.setSourcePropertyEnabled.mockResolvedValue({ data: false });
|
});
|
|
it('按业务语言展示并可选用已发布数据项', async () => {
|
const wrapper = mountWorkspace('DATE');
|
await flushPromises();
|
|
expect(wrapper.text()).toContain('新增数据项');
|
expect(wrapper.text()).toContain('检验项 > 请验单 > 生产日期');
|
expect(wrapper.text()).not.toContain('创建新版本');
|
await wrapper
|
.findAll('button')
|
.find((button) => button.text() === '选用')!
|
.trigger('click');
|
expect(wrapper.emitted('select')?.[0]?.[0]).toMatchObject({ property: 'yangpin_shengchan_riqi', propertyRevision: 1 });
|
});
|
|
it('输入中文业务名称后自动生成只读标识并保存草稿', async () => {
|
const wrapper = mountWorkspace();
|
await flushPromises();
|
await wrapper.get('[data-test="new-source-property"]').trigger('click');
|
const displayName = wrapper.get('[data-test="source-display-name"]');
|
await displayName.setValue('样品生产日期');
|
await displayName.trigger('blur');
|
await flushPromises();
|
|
expect(apiMocks.suggestFieldCode).toHaveBeenCalledWith('样品生产日期');
|
const propertyCode = wrapper.get('[data-test="source-property-code"]');
|
expect(propertyCode.attributes('disabled')).toBeDefined();
|
expect((propertyCode.element as HTMLInputElement).value).toBe('yangpin_shengchan_riqi');
|
await wrapper.get('[data-test="source-path-select"]').setValue('path-1:production_date');
|
await wrapper
|
.findAll('button')
|
.find((button) => button.text() === '保存草稿')!
|
.trigger('click');
|
await flushPromises();
|
expect(apiMocks.createSourceDataItem).toHaveBeenCalledWith(
|
expect.objectContaining({
|
displayName: '样品生产日期',
|
propertyCode: 'yangpin_shengchan_riqi',
|
sourcePathId: 'path-1',
|
outputKey: 'production_date',
|
}),
|
);
|
});
|
|
it('编辑已发布项时由后端自动创建草稿版本', async () => {
|
const wrapper = mountWorkspace();
|
await flushPromises();
|
await wrapper
|
.findAll('button')
|
.find((button) => button.text() === '编辑')!
|
.trigger('click');
|
await wrapper
|
.findAll('button')
|
.find((button) => button.text() === '保存草稿')!
|
.trigger('click');
|
await flushPromises();
|
expect(apiMocks.saveSourceDataItem).toHaveBeenCalledWith('item-1', expect.objectContaining({ expectedEditVersion: 1 }));
|
});
|
|
it('通过业务选择器预览,不要求手填检验项 ID', async () => {
|
const wrapper = mountWorkspace();
|
await flushPromises();
|
await wrapper
|
.findAll('button')
|
.find((button) => button.text() === '编辑')!
|
.trigger('click');
|
await flushPromises();
|
await wrapper.get('[data-test="source-sample-select"]').setValue('task-1');
|
await wrapper
|
.findAll('button')
|
.find((button) => button.text() === '预览')!
|
.trigger('click');
|
await flushPromises();
|
expect(apiMocks.previewSourceDataItem).toHaveBeenCalledWith('draft-2', 'task-1');
|
expect(wrapper.text()).toContain('2026-09-07');
|
});
|
|
it('从方案 A 主界面进入来源路径管理', async () => {
|
const wrapper = mountWorkspace();
|
await flushPromises();
|
await wrapper.get('[data-test="manage-source-paths"]').trigger('click');
|
expect(navigationMocks.route).toHaveBeenCalledWith('/eln/source-paths', { title: '来源路径管理' });
|
});
|
|
it('重新打开数据项编辑时刷新可用来源路径', async () => {
|
const wrapper = mountWorkspace();
|
await flushPromises();
|
apiMocks.getSourcePaths.mockResolvedValueOnce({
|
data: [
|
{
|
code: 'sample',
|
displayName: '所属样品',
|
editVersion: 1,
|
id: 'path-2',
|
outputs: [{ dataType: 'TEXT', displayName: '批号', key: 'batch_no', path: '检验项 > 样品 > 批号', returnContent: '原值' }],
|
revision: 1,
|
scope: '全部检验项',
|
status: 'PUBLISHED',
|
},
|
],
|
});
|
|
await wrapper.get('[data-test="new-source-property"]').trigger('click');
|
await flushPromises();
|
|
expect(apiMocks.getSourcePaths).toHaveBeenCalledTimes(2);
|
expect(wrapper.text()).toContain('检验项 > 样品 > 批号');
|
});
|
|
it('预览样例加载失败时仍显示数据项和来源路径', async () => {
|
apiMocks.getSourcePreviewSamples.mockRejectedValue(new Error('preview unavailable'));
|
const wrapper = mountWorkspace();
|
await flushPromises();
|
|
expect(wrapper.text()).toContain('生产日期');
|
expect(wrapper.text()).toContain('检验项 > 请验单 > 生产日期');
|
expect(wrapper.text()).not.toContain('LIMS 数据项加载失败');
|
expect(apiMocks.getSourcePreviewSamples).not.toHaveBeenCalled();
|
|
await wrapper.get('[data-test="new-source-property"]').trigger('click');
|
await flushPromises();
|
expect(apiMocks.getSourcePreviewSamples).toHaveBeenCalledTimes(1);
|
});
|
});
|