import { flushPromises, mount } from '@vue/test-utils';
|
import { defineComponent, h } from 'vue';
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import VersionDrawer from '../VersionDrawer.vue';
|
|
const apiMocks = vi.hoisted(() => ({
|
disableTemplate: vi.fn(),
|
enableTemplate: vi.fn(),
|
getTemplateDetail: vi.fn(),
|
getTemplateHealth: vi.fn(),
|
getTemplatePage: vi.fn(),
|
publishTemplate: vi.fn(),
|
switchCurrentTemplate: vi.fn(),
|
}));
|
const drawerState = vi.hoisted(() => ({ init: undefined as any }));
|
const healthOpenMock = vi.hoisted(() => vi.fn());
|
const stepSettingsOpenMock = vi.hoisted(() => vi.fn());
|
const drawerHookState = vi.hoisted(() => ({ calls: 0 }));
|
const localFormOpenMock = vi.hoisted(() => vi.fn());
|
const messageMocks = vi.hoisted(() => ({ error: vi.fn(), success: vi.fn(), warning: vi.fn() }));
|
const modalState = vi.hoisted(() => ({ close: vi.fn(), open: vi.fn(), setProps: vi.fn() }));
|
const officeMock = vi.hoisted(() => vi.fn());
|
const tableState = vi.hoisted(() => ({
|
actions: [] as any[],
|
config: undefined as any,
|
dropDownActions: [] as any[],
|
primaryActions: [] as any[],
|
reload: vi.fn(),
|
row: {
|
bizEnabled: 'enabled',
|
bizStatus: 'PUBLISHED',
|
id: 'template-v2',
|
mubanBanben: 'R02',
|
mubanBianhao: 'TMP-001',
|
shifouDangqian: false,
|
} as any,
|
}));
|
|
const TextareaStub = defineComponent({
|
name: 'ATextarea',
|
props: { value: String },
|
emits: ['update:value'],
|
setup(props, { emit }) {
|
return () => h('textarea', { value: props.value, onInput: (event: Event) => emit('update:value', (event.target as HTMLTextAreaElement).value) });
|
},
|
});
|
const FormItemStub = defineComponent({
|
name: 'AFormItem',
|
setup(_, { slots }) {
|
return () => h('div', slots.default?.());
|
},
|
});
|
|
vi.mock('#/api/x/eln/templates', () => ({
|
disableTemplate: apiMocks.disableTemplate,
|
enableTemplate: apiMocks.enableTemplate,
|
getTemplateDetail: apiMocks.getTemplateDetail,
|
getTemplateHealth: apiMocks.getTemplateHealth,
|
getTemplatePage: apiMocks.getTemplatePage,
|
publishTemplate: apiMocks.publishTemplate,
|
switchCurrentTemplate: apiMocks.switchCurrentTemplate,
|
}));
|
|
vi.mock('#/utils/jnpf', () => ({ onlineUtils: { openOfficeDocument: officeMock } }));
|
|
vi.mock('#/components/GlobalFormModal/index.vue', async () => {
|
const { defineComponent, h } = await import('vue');
|
return {
|
default: defineComponent({
|
name: 'GlobalFormModal',
|
props: { listenGlobal: Boolean },
|
setup(_, { expose }) {
|
expose({ open: localFormOpenMock });
|
return () => h('div', { 'data-test': 'drawer-local-form' });
|
},
|
}),
|
};
|
});
|
|
vi.mock('../HealthDrawer.vue', async () => {
|
const { defineComponent, h } = await import('vue');
|
return { default: defineComponent({ name: 'ElnTemplateHealthDrawer', setup: () => () => h('div') }) };
|
});
|
|
vi.mock('../StepSettingsDrawer.vue', async () => {
|
const { defineComponent, h } = await import('vue');
|
return { default: defineComponent({ name: 'ElnTemplateStepSettingsDrawer', setup: () => () => h('div') }) };
|
});
|
|
vi.mock('../VersionModal.vue', async () => {
|
const { defineComponent, h } = await import('vue');
|
return { default: defineComponent({ name: 'ElnTemplateVersionModal', setup: () => () => h('div') }) };
|
});
|
|
vi.mock('@jnpf/hooks', () => ({
|
useMessage: () => ({ createMessage: messageMocks }),
|
}));
|
|
vi.mock('@jnpf/ui/drawer', () => ({
|
BasicDrawer: { name: 'BasicDrawer', template: '<div><slot /></div>' },
|
useDrawer: () => [vi.fn(), { openDrawer: drawerHookState.calls++ % 2 === 0 ? healthOpenMock : stepSettingsOpenMock }],
|
useDrawerInner: (init: any) => {
|
drawerState.init = init;
|
return [vi.fn()];
|
},
|
}));
|
|
vi.mock('@jnpf/ui/modal', () => ({
|
BasicModal: { name: 'BasicModal', template: '<div><slot /></div>' },
|
useModal: () => [vi.fn(), { closeModal: modalState.close, openModal: modalState.open, setModalProps: modalState.setProps }],
|
}));
|
|
vi.mock('@jnpf/ui/vxeTable', async () => {
|
const { defineComponent, h } = await import('vue');
|
return {
|
BasicVxeTable: defineComponent({
|
name: 'BasicVxeTable',
|
setup(_, { slots }) {
|
return () => h('div', slots.action?.({ record: tableState.row }));
|
},
|
}),
|
TableAction: defineComponent({
|
name: 'TableAction',
|
props: { actions: Array, dropDownActions: Array },
|
setup(props) {
|
tableState.primaryActions = (props.actions || []) as any[];
|
tableState.dropDownActions = (props.dropDownActions || []) as any[];
|
tableState.actions = [...tableState.primaryActions, ...tableState.dropDownActions];
|
return () => h('div');
|
},
|
}),
|
useVxeTable: (config: any) => {
|
tableState.config = config;
|
return [vi.fn(), { reload: tableState.reload }];
|
},
|
};
|
});
|
|
describe('eLN template version drawer', () => {
|
beforeEach(() => {
|
vi.clearAllMocks();
|
tableState.actions = [];
|
tableState.dropDownActions = [];
|
tableState.primaryActions = [];
|
tableState.row = {
|
bizEnabled: 'enabled',
|
bizStatus: 'PUBLISHED',
|
id: 'template-v2',
|
mubanBanben: 'R02',
|
mubanBianhao: 'TMP-001',
|
shifouDangqian: false,
|
};
|
apiMocks.getTemplatePage.mockResolvedValue({
|
data: { list: [tableState.row], pagination: { currentPage: 1, pageSize: 20, total: 1 } },
|
});
|
apiMocks.getTemplateDetail.mockResolvedValue({ data: tableState.row });
|
apiMocks.getTemplateHealth.mockResolvedValue({ data: { diagnostics: [], fileId: 'file-1', templateId: 'template-v2' } });
|
apiMocks.disableTemplate.mockResolvedValue({ data: undefined });
|
apiMocks.enableTemplate.mockResolvedValue({ data: undefined });
|
apiMocks.publishTemplate.mockResolvedValue({ data: undefined });
|
apiMocks.switchCurrentTemplate.mockResolvedValue({ data: undefined });
|
});
|
|
it('按模板编号分页加载版本列表', async () => {
|
mount(VersionDrawer, {
|
global: { stubs: { 'a-alert': true, 'a-form-item': true, 'a-tag': true, 'a-textarea': true } },
|
});
|
|
await drawerState.init({ mubanBianhao: 'TMP-001' });
|
await tableState.config.api({ currentPage: 2, pageSize: 50 });
|
|
expect(tableState.reload).toHaveBeenCalledWith({ page: 1, searchInfo: {} });
|
expect(apiMocks.getTemplatePage).toHaveBeenCalledWith({ currentPage: 2, mubanBianhao: 'TMP-001', pageSize: 50, versionScope: 'ALL' });
|
});
|
|
it('版本列表显示创建时间', () => {
|
mount(VersionDrawer, {
|
global: { stubs: { 'a-alert': true, 'a-form-item': true, 'a-tag': true, 'a-textarea': true } },
|
});
|
|
const creationTimeColumn = tableState.config.columns.find((column: any) => column.dataIndex === 'chuangjianShijian');
|
expect(creationTimeColumn).toEqual(expect.objectContaining({ format: 'date|YYYY-MM-DD HH:mm:ss', title: '创建时间' }));
|
});
|
|
it('所有模板版本都可在抽屉内创建新版本', () => {
|
mount(VersionDrawer, {
|
global: { stubs: { 'a-alert': true, 'a-form-item': true, 'a-tag': true, 'a-textarea': true } },
|
});
|
|
const createVersion = tableState.actions.find((action) => action.label === '创建新版本');
|
expect(createVersion).toBeTruthy();
|
|
createVersion.onClick();
|
|
expect(modalState.open).toHaveBeenCalledWith(true, { sourceTemplateId: 'template-v2' });
|
});
|
|
it('行内保留高频操作并将生命周期操作收进更多菜单', () => {
|
mount(VersionDrawer, {
|
global: { stubs: { 'a-alert': true, 'a-form-item': true, 'a-tag': true, 'a-textarea': true } },
|
});
|
|
expect(tableState.primaryActions.map((action) => action.label)).toEqual(['详情', '查看', '体检', '创建新版本']);
|
expect(tableState.dropDownActions.map((action) => action.label)).toEqual(['步骤设置', '设为当前', '停用']);
|
});
|
|
it('允许将已发布且启用的非当前版本设为当前', async () => {
|
const wrapper = mount(VersionDrawer, {
|
global: { stubs: { 'a-alert': true, 'a-form-item': FormItemStub, 'a-tag': true, 'a-textarea': TextareaStub } },
|
});
|
|
const setCurrent = tableState.actions.find((action) => action.label === '设为当前');
|
expect(setCurrent).toBeTruthy();
|
setCurrent.onClick();
|
wrapper.findComponent(TextareaStub).vm.$emit('update:value', '切换业务模板');
|
await wrapper.vm.$nextTick();
|
wrapper.findComponent({ name: 'BasicModal' }).vm.$emit('ok');
|
await flushPromises();
|
|
expect(apiMocks.switchCurrentTemplate).toHaveBeenCalledWith('TMP-001', 'template-v2', '切换业务模板');
|
expect(messageMocks.success).toHaveBeenCalledWith('已将 R02 设为当前版本');
|
expect(tableState.reload).toHaveBeenCalled();
|
});
|
|
it.each([
|
['enabled', '停用', apiMocks.disableTemplate, '暂时停用模板', '模板版本已停用'],
|
['disabled', '启用', apiMocks.enableTemplate, '恢复使用模板', '模板版本已启用'],
|
])('%s版本可在抽屉内执行%s操作', async (bizEnabled, actionLabel, request, operationReason, successMessage) => {
|
tableState.row = { ...tableState.row, bizEnabled };
|
const wrapper = mount(VersionDrawer, {
|
global: { stubs: { 'a-alert': true, 'a-form-item': FormItemStub, 'a-tag': true, 'a-textarea': TextareaStub } },
|
});
|
|
tableState.actions.find((action) => action.label === actionLabel).onClick();
|
wrapper.findComponent(TextareaStub).vm.$emit('update:value', operationReason);
|
await wrapper.vm.$nextTick();
|
wrapper.findComponent({ name: 'BasicModal' }).vm.$emit('ok');
|
await flushPromises();
|
|
expect(request).toHaveBeenCalledWith('template-v2', operationReason);
|
expect(messageMocks.success).toHaveBeenCalledWith(successMessage);
|
expect(tableState.reload).toHaveBeenCalled();
|
expect(wrapper.emitted('reload')).toBeTruthy();
|
});
|
|
it('在版本抽屉内打开详情而不切换父页面', async () => {
|
const wrapper = mount(VersionDrawer, {
|
global: { stubs: { 'a-alert': true, 'a-form-item': true, 'a-tag': true, 'a-textarea': true } },
|
});
|
|
await tableState.actions.find((action) => action.label === '详情').onClick();
|
|
expect(apiMocks.getTemplateDetail).toHaveBeenCalledWith('template-v2');
|
expect(localFormOpenMock).toHaveBeenCalledWith(expect.objectContaining({ id: 'template-v2', mode: 'detail', type: 'modal' }));
|
expect(wrapper.find('[data-test="drawer-local-form"]').attributes('width')).toBe('1000px');
|
expect(wrapper.emitted('detail')).toBeUndefined();
|
});
|
|
it('草稿版本在抽屉内完成标注和体检', async () => {
|
tableState.row = { ...tableState.row, bizStatus: 'DRAFT', mubanFile: [{ fileExtension: 'docx', fileId: 'file-1', name: '原始模板.docx' }] };
|
apiMocks.getTemplateDetail.mockResolvedValue({ data: tableState.row });
|
mount(VersionDrawer, {
|
global: { stubs: { 'a-alert': true, 'a-form-item': true, 'a-tag': true, 'a-textarea': true } },
|
});
|
|
expect(tableState.actions.map((action) => action.label)).toEqual(expect.arrayContaining(['标注', '体检', '发布']));
|
|
await tableState.actions.find((action) => action.label === '标注').onClick();
|
expect(apiMocks.getTemplateDetail).toHaveBeenCalledWith('template-v2');
|
expect(officeMock).toHaveBeenCalledWith(
|
expect.objectContaining({
|
bizDataId: 'template-v2',
|
bizModule: 'eln.template',
|
bizScene: 'eln.template.annotate',
|
file: expect.objectContaining({ fileId: 'file-1' }),
|
mode: 'edit',
|
}),
|
);
|
|
await tableState.actions.find((action) => action.label === '体检').onClick();
|
expect(healthOpenMock).toHaveBeenCalledWith(true, {
|
bizStatus: 'DRAFT',
|
file: expect.objectContaining({ fileId: 'file-1' }),
|
templateCode: 'TMP-001',
|
templateId: 'template-v2',
|
});
|
});
|
|
it('草稿标注仅在手动保存后校验必备系统字段,关闭时不校验', async () => {
|
tableState.row = { ...tableState.row, bizStatus: 'DRAFT', mubanFile: [{ fileExtension: 'docx', fileId: 'file-1', name: '原始模板.docx' }] };
|
apiMocks.getTemplateDetail.mockResolvedValue({ data: tableState.row });
|
apiMocks.getTemplateHealth.mockResolvedValue({
|
data: {
|
diagnostics: [
|
{ code: 'TEST_RESULT_BINDING_REQUIRED', message: '请在文档中标注“检验结果”', severity: 'ERROR' },
|
{ code: 'TEST_CONCLUSION_BINDING_REQUIRED', message: '请在文档中标注“检验结论”', severity: 'ERROR' },
|
{ code: 'MISSING_CONTROL', message: '绑定存在但 DOCX 控件已缺失', severity: 'ERROR' },
|
],
|
fileId: 'file-1',
|
templateId: 'template-v2',
|
},
|
});
|
mount(VersionDrawer, {
|
global: { stubs: { 'a-alert': true, 'a-form-item': true, 'a-tag': true, 'a-textarea': true } },
|
});
|
|
await tableState.actions.find((action) => action.label === '标注').onClick();
|
const officeConfig = officeMock.mock.calls[0]?.[0];
|
|
expect(officeConfig.beforeClose).toBeUndefined();
|
expect(officeConfig.onManualSave).toBeTypeOf('function');
|
await expect(officeConfig.onManualSave()).rejects.toThrow('标注未完成:请在文档中标注“检验结果”;请在文档中标注“检验结论”');
|
expect(apiMocks.getTemplateHealth).toHaveBeenCalledWith('template-v2');
|
});
|
|
it('所有版本都可从版本抽屉打开步骤设置', () => {
|
mount(VersionDrawer, {
|
global: { stubs: { 'a-alert': true, 'a-form-item': true, 'a-tag': true, 'a-textarea': true } },
|
});
|
|
tableState.actions.find((action) => action.label === '步骤设置').onClick();
|
|
expect(stepSettingsOpenMock).toHaveBeenCalledWith(true, { version: tableState.row });
|
});
|
|
it.each([
|
['停用草稿', { bizEnabled: 'disabled', bizStatus: 'DRAFT' }],
|
['已发布版本', { bizEnabled: 'enabled', bizStatus: 'PUBLISHED' }],
|
])('%s使用由后端剔除标注插件的只读查看入口', async (_label, state) => {
|
tableState.row = {
|
...tableState.row,
|
...state,
|
mubanFile: [{ fileExtension: 'docx', fileId: 'file-1', name: '原始模板.docx' }],
|
};
|
apiMocks.getTemplateDetail.mockResolvedValue({ data: tableState.row });
|
mount(VersionDrawer, {
|
global: { stubs: { 'a-alert': true, 'a-form-item': true, 'a-tag': true, 'a-textarea': true } },
|
});
|
|
expect(tableState.actions.map((action) => action.label)).toContain('查看');
|
expect(tableState.actions.map((action) => action.label)).not.toContain('标注');
|
|
await tableState.actions.find((action) => action.label === '查看').onClick();
|
|
expect(officeMock).toHaveBeenCalledWith({
|
bizDataId: 'template-v2',
|
bizModule: 'eln.template',
|
bizScene: 'eln.template.annotate',
|
file: expect.objectContaining({ fileId: 'file-1' }),
|
mode: 'view',
|
});
|
});
|
|
it('草稿版本在抽屉内填写原因后发布', async () => {
|
tableState.row = { ...tableState.row, bizStatus: 'DRAFT' };
|
const wrapper = mount(VersionDrawer, {
|
global: { stubs: { 'a-alert': true, 'a-form-item': FormItemStub, 'a-tag': true, 'a-textarea': TextareaStub } },
|
});
|
|
tableState.actions.find((action) => action.label === '发布').onClick();
|
wrapper.findComponent(TextareaStub).vm.$emit('update:value', '版本验收完成');
|
await wrapper.vm.$nextTick();
|
wrapper.findComponent({ name: 'BasicModal' }).vm.$emit('ok');
|
await flushPromises();
|
|
expect(apiMocks.publishTemplate).toHaveBeenCalledWith('template-v2', '版本验收完成');
|
expect(messageMocks.success).toHaveBeenCalledWith('模板版本已发布');
|
expect(tableState.reload).toHaveBeenCalled();
|
expect(wrapper.emitted('reload')).toBeTruthy();
|
});
|
|
it('发布请求期间显示确定按钮 loading,并在完成后关闭', async () => {
|
let resolveRequest!: (value: unknown) => void;
|
apiMocks.publishTemplate.mockImplementationOnce(
|
() =>
|
new Promise((resolve) => {
|
resolveRequest = resolve;
|
}),
|
);
|
tableState.row = { ...tableState.row, bizStatus: 'DRAFT' };
|
const wrapper = mount(VersionDrawer, {
|
global: { stubs: { 'a-alert': true, 'a-form-item': FormItemStub, 'a-tag': true, 'a-textarea': TextareaStub } },
|
});
|
|
tableState.actions.find((action) => action.label === '发布').onClick();
|
wrapper.findComponent(TextareaStub).vm.$emit('update:value', '版本验收完成');
|
await wrapper.vm.$nextTick();
|
wrapper.findComponent({ name: 'BasicModal' }).vm.$emit('ok');
|
await vi.waitFor(() => expect(apiMocks.publishTemplate).toHaveBeenCalledOnce());
|
|
expect(modalState.setProps).toHaveBeenLastCalledWith({ confirmLoading: true });
|
resolveRequest({ data: undefined });
|
await vi.waitFor(() => expect(modalState.setProps).toHaveBeenLastCalledWith({ confirmLoading: false }));
|
});
|
|
it('发布失败后关闭确定按钮 loading 并保留弹窗', async () => {
|
apiMocks.publishTemplate.mockRejectedValueOnce(new Error('发布失败'));
|
tableState.row = { ...tableState.row, bizStatus: 'DRAFT' };
|
const wrapper = mount(VersionDrawer, {
|
global: { stubs: { 'a-alert': true, 'a-form-item': FormItemStub, 'a-tag': true, 'a-textarea': TextareaStub } },
|
});
|
|
tableState.actions.find((action) => action.label === '发布').onClick();
|
wrapper.findComponent(TextareaStub).vm.$emit('update:value', '版本验收完成');
|
await wrapper.vm.$nextTick();
|
wrapper.findComponent({ name: 'BasicModal' }).vm.$emit('ok');
|
await vi.waitFor(() => expect(messageMocks.error).toHaveBeenCalledWith('发布失败'));
|
|
expect(modalState.close).not.toHaveBeenCalled();
|
expect(modalState.setProps).toHaveBeenNthCalledWith(1, { confirmLoading: true });
|
expect(modalState.setProps).toHaveBeenLastCalledWith({ confirmLoading: false });
|
});
|
});
|