import { flushPromises, mount } from '@vue/test-utils';
|
import { defineComponent, h } from 'vue';
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import StepSettingsDrawer from '../StepSettingsDrawer.vue';
|
|
const apiMocks = vi.hoisted(() => ({
|
getTemplateSteps: vi.fn(),
|
saveTemplateSteps: vi.fn(),
|
}));
|
const drawerState = vi.hoisted(() => ({ init: undefined as any }));
|
const drawerActions = vi.hoisted(() => ({
|
changeLoading: vi.fn(),
|
changeOkLoading: vi.fn(),
|
close: vi.fn(),
|
}));
|
const messageMocks = vi.hoisted(() => ({ error: vi.fn(), success: vi.fn(), warning: vi.fn() }));
|
const modalActions = vi.hoisted(() => ({ close: vi.fn(), open: vi.fn() }));
|
|
const configuration = {
|
singleStep: false,
|
steps: [
|
{ id: 'step-2', name: '记录结果', order: 2 },
|
{ id: 'step-1', name: '准备样品', order: 1 },
|
],
|
};
|
|
const InputStub = defineComponent({
|
name: 'AInput',
|
inheritAttrs: false,
|
props: { value: String },
|
emits: ['update:value'],
|
setup(props, { attrs, emit }) {
|
return () =>
|
h('input', {
|
...attrs,
|
value: props.value,
|
onInput: (event: Event) => emit('update:value', (event.target as HTMLInputElement).value),
|
});
|
},
|
});
|
|
const SelectStub = defineComponent({
|
name: 'ASelect',
|
inheritAttrs: false,
|
props: { options: { type: Array, default: () => [] }, value: String },
|
emits: ['update:value'],
|
setup(props, { attrs, emit }) {
|
return () =>
|
h(
|
'select',
|
{
|
...attrs,
|
value: props.value,
|
onChange: (event: Event) => emit('update:value', (event.target as HTMLSelectElement).value || undefined),
|
},
|
[h('option', { value: '' }), ...props.options.map((item: any) => h('option', { value: item.value }, item.label))],
|
);
|
},
|
});
|
|
const ButtonStub = defineComponent({
|
name: 'AButton',
|
inheritAttrs: false,
|
props: { disabled: Boolean },
|
emits: ['click'],
|
setup(props, { attrs, emit, slots }) {
|
return () => h('button', { ...attrs, disabled: props.disabled, onClick: () => emit('click') }, [slots.icon?.(), slots.default?.()]);
|
},
|
});
|
|
vi.mock('#/api/x/eln/templates', () => ({
|
getTemplateSteps: apiMocks.getTemplateSteps,
|
saveTemplateSteps: apiMocks.saveTemplateSteps,
|
}));
|
|
vi.mock('@jnpf/hooks', () => ({
|
useMessage: () => ({ createMessage: messageMocks }),
|
}));
|
|
vi.mock('@jnpf/ui/drawer', async () => {
|
const { defineComponent, h } = await import('vue');
|
return {
|
BasicDrawer: defineComponent({
|
name: 'BasicDrawer',
|
inheritAttrs: false,
|
props: { showFooter: Boolean, title: String },
|
emits: ['ok', 'register'],
|
setup(_, { slots }) {
|
return () => h('div', { 'data-test': 'basic-drawer' }, slots.default?.());
|
},
|
}),
|
useDrawerInner: (init: any) => {
|
drawerState.init = init;
|
return [
|
vi.fn(),
|
{
|
changeLoading: drawerActions.changeLoading,
|
changeOkLoading: drawerActions.changeOkLoading,
|
closeDrawer: drawerActions.close,
|
},
|
];
|
},
|
};
|
});
|
|
vi.mock('@jnpf/ui/modal', async () => {
|
const { defineComponent, h } = await import('vue');
|
return {
|
BasicModal: defineComponent({
|
name: 'BasicModal',
|
emits: ['ok', 'register'],
|
setup(_, { slots }) {
|
return () => h('div', { 'data-test': 'basic-modal' }, slots.default?.());
|
},
|
}),
|
useModal: () => [vi.fn(), { closeModal: modalActions.close, openModal: modalActions.open }],
|
};
|
});
|
|
vi.mock('vuedraggable', async () => {
|
const { defineComponent, h } = await import('vue');
|
return {
|
default: defineComponent({
|
name: 'DraggableStub',
|
inheritAttrs: false,
|
props: { disabled: Boolean, modelValue: { type: Array, default: () => [] } },
|
emits: ['update:modelValue'],
|
setup(props, { attrs, slots }) {
|
return () =>
|
h(
|
'div',
|
{ ...attrs, 'data-disabled': String(props.disabled) },
|
props.modelValue.map((element, index) => slots.item?.({ element, index })),
|
);
|
},
|
}),
|
};
|
});
|
|
function mountDrawer() {
|
return mount(StepSettingsDrawer, {
|
global: {
|
stubs: {
|
'a-button': ButtonStub,
|
'a-form-item': defineComponent({ name: 'AFormItem', template: '<div><slot /></div>' }),
|
'a-input': InputStub,
|
'a-select': SelectStub,
|
'a-tooltip': defineComponent({ name: 'ATooltip', template: '<span><slot /></span>' }),
|
},
|
},
|
});
|
}
|
|
describe('eLN template step settings drawer', () => {
|
beforeEach(() => {
|
vi.clearAllMocks();
|
apiMocks.getTemplateSteps.mockResolvedValue({ data: structuredClone(configuration) });
|
apiMocks.saveTemplateSteps.mockResolvedValue({ data: structuredClone(configuration) });
|
});
|
|
it('标题展示模板编号和模板版本', async () => {
|
const wrapper = mountDrawer();
|
|
await drawerState.init({
|
version: { bizEnabled: 'enabled', bizStatus: 'DRAFT', id: 'template-1', mubanBanben: 'R01', mubanBianhao: 'TMP-001' },
|
});
|
await wrapper.vm.$nextTick();
|
|
expect(wrapper.findComponent({ name: 'BasicDrawer' }).props('title')).toBe('步骤设置 - TMP-001 - R01');
|
});
|
|
it('加载已发布版本时按顺序只读展示', async () => {
|
const wrapper = mountDrawer();
|
|
await drawerState.init({
|
version: { bizEnabled: 'enabled', bizStatus: 'PUBLISHED', id: 'template-1', mubanBanben: 'R01', mubanBianhao: 'TMP-001' },
|
});
|
await wrapper.vm.$nextTick();
|
|
expect(apiMocks.getTemplateSteps).toHaveBeenCalledWith('template-1');
|
expect(drawerActions.changeLoading.mock.calls).toEqual([[true], [false]]);
|
expect(wrapper.findComponent({ name: 'BasicDrawer' }).props('showFooter')).toBe(false);
|
expect(wrapper.findComponent('[data-test="step-list"]').attributes('data-disabled')).toBe('true');
|
expect(wrapper.find('[data-test="step-list"]').text().indexOf('准备样品')).toBeLessThan(wrapper.find('[data-test="step-list"]').text().indexOf('记录结果'));
|
});
|
|
it('按拖动顺序保存步骤', async () => {
|
const wrapper = mountDrawer();
|
await drawerState.init({
|
version: { bizEnabled: 'enabled', bizStatus: 'DRAFT', id: 'template-1', mubanBanben: 'R01', mubanBianhao: 'TMP-001' },
|
});
|
await wrapper.vm.$nextTick();
|
|
const stepList = wrapper.findComponent({ name: 'DraggableStub' });
|
stepList.vm.$emit('update:modelValue', [...(stepList.props('modelValue') as any[])].reverse());
|
await wrapper.vm.$nextTick();
|
|
wrapper.findComponent({ name: 'BasicDrawer' }).vm.$emit('ok');
|
await flushPromises();
|
|
expect(apiMocks.saveTemplateSteps).toHaveBeenCalledWith('template-1', {
|
stepMigrations: {},
|
steps: [
|
{ id: 'step-2', name: '记录结果' },
|
{ id: 'step-1', name: '准备样品' },
|
],
|
});
|
expect(drawerActions.changeOkLoading.mock.calls).toEqual([[true], [false]]);
|
expect(drawerActions.close).toHaveBeenCalledOnce();
|
expect(wrapper.emitted('saved')).toEqual([['template-1']]);
|
});
|
|
it('删除已有步骤时记录迁移目标', async () => {
|
const wrapper = mountDrawer();
|
await drawerState.init({
|
version: { bizEnabled: 'enabled', bizStatus: 'DRAFT', id: 'template-1', mubanBanben: 'R01', mubanBianhao: 'TMP-001' },
|
});
|
await wrapper.vm.$nextTick();
|
|
await wrapper.find('[data-test="add-step"]').trigger('click');
|
await wrapper.find('[data-test="delete-step-step-1"]').trigger('click');
|
expect(modalActions.open).toHaveBeenCalledWith(true);
|
const migrationTarget = wrapper.findAllComponents(SelectStub).find((item) => item.attributes('data-test') === 'migration-target')!;
|
expect(migrationTarget.props('options')).toEqual([{ label: '记录结果', value: 'step-2' }]);
|
migrationTarget.vm.$emit('update:value', 'step-2');
|
await wrapper.vm.$nextTick();
|
wrapper.findComponent({ name: 'BasicModal' }).vm.$emit('ok');
|
await wrapper.vm.$nextTick();
|
await wrapper.find('[data-test="delete-step-local-step-1"]').trigger('click');
|
wrapper.findComponent({ name: 'BasicDrawer' }).vm.$emit('ok');
|
await flushPromises();
|
|
expect(apiMocks.saveTemplateSteps).toHaveBeenCalledWith(
|
'template-1',
|
expect.objectContaining({
|
stepMigrations: { 'step-1': 'step-2' },
|
steps: [{ id: 'step-2', name: '记录结果' }],
|
}),
|
);
|
});
|
|
it('保存失败后恢复按钮 loading 并保持抽屉打开', async () => {
|
apiMocks.saveTemplateSteps.mockRejectedValueOnce(new Error('保存失败'));
|
const wrapper = mountDrawer();
|
await drawerState.init({
|
version: { bizEnabled: 'enabled', bizStatus: 'DRAFT', id: 'template-1', mubanBanben: 'R01', mubanBianhao: 'TMP-001' },
|
});
|
await wrapper.vm.$nextTick();
|
|
wrapper.findComponent({ name: 'BasicDrawer' }).vm.$emit('ok');
|
await flushPromises();
|
|
expect(messageMocks.error).toHaveBeenCalledWith('保存失败');
|
expect(drawerActions.changeOkLoading.mock.calls).toEqual([[true], [false]]);
|
expect(drawerActions.close).not.toHaveBeenCalled();
|
});
|
});
|