import { mount, shallowMount } from '@vue/test-utils';
|
import { defineComponent, h } from 'vue';
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import HealthDrawer from '../HealthDrawer.vue';
|
import drawerSource from '../HealthDrawer.vue?raw';
|
|
const apiMocks = vi.hoisted(() => ({ confirmRemoveMissing: vi.fn(), getTemplateHealth: vi.fn() }));
|
const drawerState = vi.hoisted(() => {
|
const state = {
|
changeLoading: vi.fn((loading: boolean) => {
|
state.loading = loading;
|
}),
|
init: undefined as any,
|
loading: false,
|
};
|
return state;
|
});
|
const officeMock = vi.hoisted(() => vi.fn());
|
const modalState = vi.hoisted(() => ({ close: vi.fn(), open: vi.fn(), setProps: vi.fn() }));
|
const permissionState = vi.hoisted(() => ({ canEdit: true, requested: [] as string[] }));
|
|
const DiagnosticListStub = defineComponent({
|
name: 'DiagnosticListStub',
|
props: { dataSource: { default: () => [], type: Array } },
|
setup(props, { slots }) {
|
return () =>
|
h(
|
'div',
|
props.dataSource.map((item) => slots.renderItem?.({ item })),
|
);
|
},
|
});
|
const DiagnosticListItemStub = defineComponent({
|
name: 'DiagnosticListItemStub',
|
setup(_, { slots }) {
|
return () => h('div', [slots.default?.(), slots.actions?.()]);
|
},
|
});
|
const DiagnosticListItemMetaStub = defineComponent({
|
name: 'DiagnosticListItemMetaStub',
|
props: { description: String },
|
setup(props, { slots }) {
|
return () =>
|
h('div', [
|
h('div', { 'data-test': 'diagnostic-title' }, slots.title?.()),
|
props.description ? h('div', { 'data-test': 'diagnostic-description' }, props.description) : null,
|
]);
|
},
|
});
|
|
vi.mock('#/api/x/eln/templates', () => ({
|
confirmRemoveMissing: apiMocks.confirmRemoveMissing,
|
getTemplateHealth: apiMocks.getTemplateHealth,
|
}));
|
vi.mock('#/utils/jnpf', () => ({ onlineUtils: { openOfficeDocument: officeMock } }));
|
vi.mock('@jnpf/hooks', () => ({
|
useMessage: () => ({ createMessage: { success: vi.fn(), warning: vi.fn() } }),
|
usePermission: () => ({
|
hasBtnP: (code: string) => {
|
permissionState.requested.push(code);
|
return permissionState.canEdit && code === 'btn_edit';
|
},
|
}),
|
}));
|
vi.mock('@jnpf/ui/drawer', () => ({
|
BasicDrawer: { name: 'BasicDrawer', props: { title: String }, template: '<div><slot /></div>' },
|
useDrawerInner: (init: any) => {
|
drawerState.init = init;
|
return [vi.fn(), { changeLoading: drawerState.changeLoading }];
|
},
|
}));
|
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', () => ({ TableAction: { name: 'TableAction', template: '<div />' } }));
|
|
describe('eLN template health drawer', () => {
|
beforeEach(() => {
|
vi.clearAllMocks();
|
drawerState.loading = false;
|
permissionState.canEdit = true;
|
permissionState.requested = [];
|
apiMocks.getTemplateHealth.mockResolvedValue({
|
data: {
|
diagnostics: [
|
{ bindingId: 'b1', code: 'MISSING_CONTROL', expectedVersion: 3, message: '控件缺失', severity: 'ERROR' },
|
{ code: 'ORPHAN_CONTROL', controlTag: 'eln.field.orphan', message: '孤儿控件', severity: 'WARNING' },
|
{ code: 'UNSUPPORTED_REPEAT_CONTROL', message: '重复表格', severity: 'ERROR' },
|
],
|
fileId: 'file-1',
|
templateId: 'template-1',
|
},
|
});
|
});
|
|
it('在标题中显示模板编号', async () => {
|
const wrapper = shallowMount(HealthDrawer);
|
|
await drawerState.init({
|
bizStatus: 'DRAFT',
|
file: { fileId: 'file-1', name: '模板.docx' },
|
templateCode: 'TMP-001',
|
templateId: 'template-1',
|
});
|
|
expect(wrapper.findComponent({ name: 'BasicDrawer' }).props('title')).toBe('模板体检 - TMP-001');
|
});
|
|
it('诊断信息仅显示业务文案,并隐藏原始 code 和空辅助信息', async () => {
|
apiMocks.getTemplateHealth.mockResolvedValueOnce({
|
data: {
|
diagnostics: [{ code: 'TEST_RESULT_BINDING_REQUIRED', message: '请在文档中标注“检验结果”', severity: 'ERROR' }],
|
fileId: 'file-1',
|
templateId: 'template-1',
|
},
|
});
|
const wrapper = mount(HealthDrawer, {
|
global: {
|
stubs: {
|
'a-alert': true,
|
'a-button': true,
|
'a-form-item': true,
|
'a-tag': true,
|
'a-textarea': true,
|
Empty: true,
|
AList: DiagnosticListStub,
|
AListItem: DiagnosticListItemStub,
|
AListItemMeta: DiagnosticListItemMetaStub,
|
},
|
},
|
});
|
|
await drawerState.init({
|
bizStatus: 'DRAFT',
|
file: { fileId: 'file-1', name: '模板.docx' },
|
templateCode: 'TMP-001',
|
templateId: 'template-1',
|
});
|
|
expect(wrapper.find('[data-test="diagnostic-title"]').text()).toBe('请在文档中标注“检验结果”');
|
expect(wrapper.find('[data-test="diagnostic-description"]').exists()).toBe(false);
|
expect(wrapper.text()).not.toContain('TEST_RESULT_BINDING_REQUIRED');
|
});
|
|
it('groups diagnostics by severity and keeps repair actions evidence-specific', async () => {
|
const wrapper = shallowMount(HealthDrawer, {
|
global: {
|
stubs: {
|
'a-alert': true,
|
'a-button': true,
|
'a-empty': true,
|
'a-form-item': true,
|
'a-list': true,
|
'a-list-item': true,
|
'a-list-item-meta': true,
|
'a-tag': true,
|
'a-textarea': true,
|
},
|
},
|
});
|
await drawerState.init({ file: { fileId: 'file-1', name: '原始模板.docx' }, templateId: 'template-1' });
|
|
expect(apiMocks.getTemplateHealth).toHaveBeenCalledWith('template-1');
|
expect(drawerSource).toContain('groupedDiagnostics');
|
expect(drawerSource).toContain("action === 'repair'");
|
expect(drawerSource).toContain("action === 'confirmRemove'");
|
expect(drawerSource).toContain("action === 'locate'");
|
expect(drawerSource).not.toContain("code === 'UNSUPPORTED_REPEAT_CONTROL' && action === 'repair'");
|
expect(wrapper.findComponent({ name: 'BasicDrawer' }).exists()).toBe(true);
|
});
|
|
it('显式引入体检列表组件,避免真实页面把它们解析为未知元素', () => {
|
expect(drawerSource).toContain("import { Empty, List, ListItem, ListItemMeta } from 'ant-design-vue';");
|
expect(drawerSource).toContain('<Empty description="未发现模板绑定异常"');
|
expect(drawerSource).toContain('<List :data-source=');
|
expect(drawerSource).toContain('<ListItem>');
|
expect(drawerSource).toContain('<ListItemMeta');
|
});
|
|
it('无异常时在抽屉内容区垂直居中显示空状态', async () => {
|
apiMocks.getTemplateHealth.mockResolvedValueOnce({ data: { diagnostics: [], fileId: 'file-1', templateId: 'template-1' } });
|
const wrapper = mount(HealthDrawer, {
|
global: {
|
stubs: {
|
'a-alert': true,
|
'a-button': true,
|
'a-form-item': true,
|
'a-tag': true,
|
'a-textarea': true,
|
Empty: true,
|
List: true,
|
ListItem: true,
|
ListItemMeta: true,
|
},
|
},
|
});
|
|
await drawerState.init({ bizStatus: 'DRAFT', file: { fileId: 'file-1', name: '模板.docx' }, templateId: 'template-1' });
|
|
expect(wrapper.find('.health-content').exists()).toBe(true);
|
expect(wrapper.find('.health-empty').exists()).toBe(true);
|
expect(drawerSource).toContain('class="full-drawer"');
|
expect(drawerSource).toMatch(/\.health-content\s*\{[^}]*height:\s*100%/);
|
expect(drawerSource).toMatch(/\.health-empty\s*\{[^}]*align-items:\s*center[^}]*justify-content:\s*center/);
|
});
|
|
it('模板切换后旧体检响应不得覆盖新模板报告或关闭其 loading', async () => {
|
let resolveA!: (value: unknown) => void;
|
let resolveB!: (value: unknown) => void;
|
apiMocks.getTemplateHealth.mockImplementation(
|
(templateId: string) =>
|
new Promise((resolve) => {
|
if (templateId === 'template-a') resolveA = resolve;
|
else resolveB = resolve;
|
}),
|
);
|
const wrapper = shallowMount(HealthDrawer);
|
const loadA = drawerState.init({ file: { fileId: 'file-a', name: 'A.docx' }, templateId: 'template-a' });
|
await vi.waitFor(() => expect(apiMocks.getTemplateHealth).toHaveBeenCalledWith('template-a'));
|
const loadB = drawerState.init({ file: { fileId: 'file-b', name: 'B.docx' }, templateId: 'template-b' });
|
await vi.waitFor(() => expect(apiMocks.getTemplateHealth).toHaveBeenCalledWith('template-b'));
|
|
resolveB({ data: { diagnostics: [], fileId: 'file-b', templateId: 'template-b' } });
|
await loadB;
|
resolveA({ data: { diagnostics: [], fileId: 'file-a', templateId: 'template-a' } });
|
await loadA;
|
|
expect((wrapper.vm as any).report.templateId).toBe('template-b');
|
expect(drawerState.loading).toBe(false);
|
expect(drawerState.changeLoading).toHaveBeenCalledTimes(3);
|
});
|
|
it.each([
|
['定位', { actions: ['LOCATE'], code: 'ORPHAN_CONTROL', controlTag: 'eln.field.orphan' }],
|
['修复', { actions: ['REPAIR'], bindingId: 'binding-a', code: 'MISSING_CONTROL' }],
|
])('旧模板%s的保存回调不读取或干扰新模板体检', async (_label, diagnostic) => {
|
const wrapper = shallowMount(HealthDrawer);
|
await drawerState.init({ bizStatus: 'DRAFT', file: { fileId: 'file-a', name: 'A.docx' }, templateId: 'template-a' });
|
const [action] = (wrapper.vm as any).getDiagnosticActions(diagnostic);
|
if (_label === '修复') expect(permissionState.requested).toEqual([]);
|
action.onClick();
|
const onSave = officeMock.mock.calls[0]?.[0].onSave as () => Promise<void>;
|
let resolveB!: (value: unknown) => void;
|
let healthRequestCount = 0;
|
vi.clearAllMocks();
|
drawerState.loading = false;
|
apiMocks.getTemplateHealth.mockImplementation((templateId: string) => {
|
expect(templateId).toBe('template-b');
|
healthRequestCount += 1;
|
if (healthRequestCount > 1) return Promise.resolve({ data: { diagnostics: [], fileId: 'file-b', templateId: 'template-b' } });
|
return new Promise((resolve) => {
|
resolveB = resolve;
|
});
|
});
|
const loadB = drawerState.init({ bizStatus: 'DRAFT', file: { fileId: 'file-b', name: 'B.docx' }, templateId: 'template-b' });
|
await vi.waitFor(() => expect(apiMocks.getTemplateHealth).toHaveBeenCalledOnce());
|
|
await onSave();
|
|
expect(apiMocks.getTemplateHealth).toHaveBeenCalledOnce();
|
expect(drawerState.loading).toBe(true);
|
resolveB({ data: { diagnostics: [], fileId: 'file-b', templateId: 'template-b' } });
|
await loadB;
|
expect(drawerState.loading).toBe(false);
|
});
|
|
it('当前体检请求失败时仍关闭 loading 并保留错误传播', async () => {
|
apiMocks.getTemplateHealth.mockRejectedValueOnce(new Error('health failed'));
|
const wrapper = shallowMount(HealthDrawer);
|
|
await expect(drawerState.init({ file: { fileId: 'file-1', name: '模板.docx' }, templateId: 'template-1' })).rejects.toThrow('health failed');
|
|
expect(wrapper.exists()).toBe(true);
|
expect(drawerState.loading).toBe(false);
|
expect(drawerState.changeLoading).toHaveBeenCalledWith(false);
|
});
|
|
it('opens repair in the same office modal with the binding id', () => {
|
expect(drawerSource).toContain('repairBindingId: diagnostic.bindingId');
|
expect(drawerSource).toContain("bizModule: 'eln.template'");
|
expect(drawerSource).toContain("bizScene: 'eln.template.annotate'");
|
});
|
|
it('shows dependencies and requires a reason before confirming removal', () => {
|
expect(drawerSource).toContain('引用/公式依赖');
|
expect(drawerSource).toContain('pendingDiagnostic?.dependencies');
|
expect(drawerSource).toContain('reason.value.trim()');
|
expect(drawerSource).toContain('expectedVersion == null');
|
});
|
|
it('删除缺失绑定期间显示确定按钮 loading,并在完成后关闭', async () => {
|
let resolveRequest!: (value: unknown) => void;
|
apiMocks.confirmRemoveMissing.mockImplementationOnce(
|
() =>
|
new Promise((resolve) => {
|
resolveRequest = resolve;
|
}),
|
);
|
const wrapper = shallowMount(HealthDrawer);
|
await drawerState.init({ bizStatus: 'DRAFT', file: { fileId: 'file-1', name: '模板.docx' }, templateId: 'template-1' });
|
(wrapper.vm as any).requestRemove({ bindingId: 'b1', dependencies: [], expectedVersion: 3 });
|
(wrapper.vm as any).reason = '确认删除无效绑定';
|
|
const submit = (wrapper.vm as any).confirmRemove();
|
await vi.waitFor(() => expect(apiMocks.confirmRemoveMissing).toHaveBeenCalledOnce());
|
|
expect(modalState.setProps).toHaveBeenLastCalledWith({ confirmLoading: true });
|
resolveRequest({ data: undefined });
|
await submit;
|
expect(modalState.setProps).toHaveBeenLastCalledWith({ confirmLoading: false });
|
});
|
|
it('删除缺失绑定失败后关闭确定按钮 loading 并保留弹窗', async () => {
|
apiMocks.confirmRemoveMissing.mockRejectedValueOnce(new Error('删除失败'));
|
const wrapper = shallowMount(HealthDrawer);
|
await drawerState.init({ bizStatus: 'DRAFT', file: { fileId: 'file-1', name: '模板.docx' }, templateId: 'template-1' });
|
(wrapper.vm as any).requestRemove({ bindingId: 'b1', dependencies: [], expectedVersion: 3 });
|
(wrapper.vm as any).reason = '确认删除无效绑定';
|
|
await expect((wrapper.vm as any).confirmRemove()).rejects.toThrow('删除失败');
|
|
expect(modalState.close).not.toHaveBeenCalled();
|
expect(modalState.setProps).toHaveBeenNthCalledWith(1, { confirmLoading: true });
|
expect(modalState.setProps).toHaveBeenLastCalledWith({ confirmLoading: false });
|
});
|
|
it('草稿模板定位孤儿控件时使用只读模式和明确 Tag', async () => {
|
const wrapper = shallowMount(HealthDrawer);
|
await drawerState.init({ bizStatus: 'DRAFT', file: { fileId: 'file-1', name: '原始模板.docx' }, templateId: 'template-1' });
|
const [locate] = (wrapper.vm as any).getDiagnosticActions({ actions: ['LOCATE'], code: 'ORPHAN_CONTROL', controlTag: 'eln.field.orphan' });
|
|
locate.onClick();
|
|
expect(officeMock).toHaveBeenCalledWith(expect.objectContaining({ locateControlTag: 'eln.field.orphan', mode: 'view' }));
|
});
|
|
it('已发布模板体检隐藏修复和确认删除,只保留只读定位', async () => {
|
const wrapper = shallowMount(HealthDrawer);
|
await drawerState.init({ bizStatus: 'PUBLISHED', file: { fileId: 'file-1', name: '原始模板.docx' }, templateId: 'template-1' });
|
|
expect((wrapper.vm as any).getDiagnosticActions({ actions: ['REPAIR', 'CONFIRM_REMOVE'], bindingId: 'b1', code: 'MISSING_CONTROL' })).toEqual([]);
|
expect((wrapper.vm as any).getDiagnosticActions({ actions: ['LOCATE'], code: 'ROLE_MISMATCH', controlTag: 'eln.field.role-mismatch' })).toHaveLength(1);
|
});
|
|
it('仅显示后端授权且符合状态限制的诊断动作', async () => {
|
const wrapper = shallowMount(HealthDrawer);
|
await drawerState.init({ bizStatus: 'DRAFT', file: { fileId: 'file-1', name: '原始模板.docx' }, templateId: 'template-1' });
|
|
expect((wrapper.vm as any).getDiagnosticActions({ actions: ['LOCATE'], bindingId: 'b1', code: 'MISSING_CONTROL' })).toHaveLength(1);
|
expect((wrapper.vm as any).getDiagnosticActions({ actions: [], bindingId: 'b1', code: 'MISSING_CONTROL' })).toEqual([]);
|
});
|
|
it('不因按钮权限隐藏草稿模板的修复和确认删除', async () => {
|
permissionState.canEdit = false;
|
const wrapper = shallowMount(HealthDrawer);
|
await drawerState.init({ bizStatus: 'DRAFT', file: { fileId: 'file-1', name: '原始模板.docx' }, templateId: 'template-1' });
|
|
expect((wrapper.vm as any).getDiagnosticActions({ actions: ['REPAIR', 'CONFIRM_REMOVE'], bindingId: 'b1', code: 'MISSING_CONTROL' })).toHaveLength(2);
|
expect((wrapper.vm as any).getDiagnosticActions({ actions: ['LOCATE'], code: 'ROLE_MISMATCH', controlTag: 'eln.field.role-mismatch' })).toHaveLength(1);
|
});
|
});
|