import { flushPromises, mount, shallowMount } from '@vue/test-utils';
|
import { defineComponent, nextTick } from 'vue';
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import VersionDrawer from '../VersionDrawer.vue';
|
import drawerSource from '../VersionDrawer.vue?raw';
|
|
const apiMocks = vi.hoisted(() => ({
|
createFormulaVersion: vi.fn(),
|
getFormulaDetail: vi.fn(),
|
getFormulaFunctions: vi.fn(),
|
getFormulaTemplateReferences: vi.fn(),
|
preReleaseFormulaVersion: vi.fn(),
|
updateFormulaVersion: vi.fn(),
|
}));
|
const drawerState = vi.hoisted(() => ({ closeDrawer: vi.fn(), init: undefined as any }));
|
const tableState = vi.hoisted(() => ({ config: undefined as any, referenceConfig: undefined as any, referenceReload: vi.fn() }));
|
const InputSearch = defineComponent({
|
name: 'AInputSearch',
|
props: ['value'],
|
emits: ['search', 'update:value'],
|
template:
|
'<div><input data-test="reference-search-input" :value="value" @input="$emit(\'update:value\', $event.target.value)" /><button data-test="reference-search-submit" @click="$emit(\'search\', value)">查询</button></div>',
|
});
|
const layoutStubs = { 'a-button': true, 'a-input-search': true };
|
|
function deferred<T>() {
|
let resolve!: (value: T) => void;
|
const promise = new Promise<T>((promiseResolve) => {
|
resolve = promiseResolve;
|
});
|
return { promise, resolve };
|
}
|
|
vi.mock('#/api/x/eln/formulas', () => ({
|
cancelFormulaVersion: vi.fn(),
|
createFormulaVersion: apiMocks.createFormulaVersion,
|
getFormulaDetail: apiMocks.getFormulaDetail,
|
getFormulaFunctions: apiMocks.getFormulaFunctions,
|
getFormulaTemplateReferences: apiMocks.getFormulaTemplateReferences,
|
preReleaseFormulaVersion: apiMocks.preReleaseFormulaVersion,
|
publishFormulaVersion: vi.fn(),
|
switchCurrentFormulaVersion: vi.fn(),
|
updateFormulaVersion: apiMocks.updateFormulaVersion,
|
}));
|
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: drawerState.closeDrawer }];
|
},
|
}));
|
vi.mock('@jnpf/ui/modal', () => ({
|
BasicModal: {
|
name: 'BasicModal',
|
emits: ['heightChange', 'ok'],
|
props: ['afterClose', 'confirmLoading', 'height', 'minHeight', 'showOkBtn', 'title'],
|
template: '<div><slot /></div>',
|
},
|
useModal: () => [vi.fn(), { closeModal: vi.fn(), openModal: vi.fn() }],
|
}));
|
vi.mock('@jnpf/ui/vxeTable', () => ({
|
BasicVxeTable: {
|
name: 'BasicVxeTable',
|
props: ['dataSource'],
|
template: '<div><div v-for="record in dataSource"><slot :record="record" name="jieguoLeixing" /><slot :record="record" name="action" /></div></div>',
|
},
|
TableAction: {
|
name: 'TableAction',
|
props: ['actions'],
|
template:
|
'<button v-for="action in actions" :key="action.label" @click="action.onClick ? action.onClick() : action.modelConfirm?.onOk()">{{ action.label }}</button>',
|
},
|
useVxeTable: (config: any) => {
|
if (config.actionColumn) {
|
tableState.config = config;
|
return [vi.fn()];
|
}
|
tableState.referenceConfig = config;
|
tableState.referenceReload = vi.fn(async (options: any = {}) => {
|
const raw = { currentPage: options.page || 1, pageSize: 20, ...options.searchInfo };
|
const query = config.beforeFetch ? config.beforeFetch(raw) : raw;
|
return config.api?.(query);
|
});
|
return [vi.fn(), { reload: tableState.referenceReload }];
|
},
|
}));
|
|
describe('eLN formula version drawer', () => {
|
beforeEach(() => {
|
vi.clearAllMocks();
|
apiMocks.getFormulaDetail.mockResolvedValue({
|
data: {
|
bizEnabled: 'enabled',
|
gongshiBianma: 'hanliang_abc123',
|
id: 'f1',
|
mubanYinyongShu: 12,
|
templateReferences: [
|
{ bindingId: 'b1', bindingStatus: 'ACTIVE', formulaVersionId: 'v1', templateId: 't1', templateName: '含量模板', templateStatus: 'PUBLISHED' },
|
],
|
versions: [
|
{
|
banbenHao: 1,
|
bizStatus: 'PUBLISHED',
|
gongshiDingyiId: 'f1',
|
id: 'v1',
|
jieguoLeixing: 'TEXT',
|
parameters: [{ canshuBianma: 'a', canshuMingcheng: '样品量', paixu: 1, shifouBitian: 1, shujuLeixing: 'DECIMAL' }],
|
},
|
{
|
banbenHao: 2,
|
biaodashi: 'amount * 2',
|
bizStatus: 'DRAFT',
|
gongshiDingyiId: 'f1',
|
id: 'v2',
|
jieguoLeixing: 'DECIMAL',
|
parameters: [],
|
xiuyueMoshi: 'HALF_UP',
|
xiuyueXiaoshuWei: 2,
|
},
|
],
|
},
|
});
|
apiMocks.getFormulaFunctions.mockResolvedValue([]);
|
apiMocks.getFormulaTemplateReferences.mockResolvedValue({
|
data: {
|
list: [{ bindingId: 'b1', bindingStatus: 'ACTIVE', formulaVersionId: 'v1', templateId: 't1', templateName: '含量模板', templateStatus: 'PUBLISHED' }],
|
pagination: { currentPage: 1, pageSize: 20, total: 12 },
|
},
|
});
|
});
|
|
it('loads versions and template reference summaries from the detail endpoint', async () => {
|
const wrapper = mount(VersionDrawer, {
|
global: {
|
stubs: {
|
...layoutStubs,
|
'a-alert': true,
|
'a-descriptions': { template: '<div><slot /></div>' },
|
'a-descriptions-item': { template: '<span><slot /></span>' },
|
'a-form': true,
|
'a-form-item': true,
|
'a-input-number': true,
|
'a-select': true,
|
'a-tag': true,
|
'a-textarea': true,
|
ElnFormulaParameterTable: true,
|
},
|
},
|
});
|
await drawerState.init({ formulaId: 'f1' });
|
await flushPromises();
|
expect(apiMocks.getFormulaDetail).toHaveBeenCalledWith('f1');
|
expect(tableState.config.dataSource.value).toHaveLength(2);
|
expect(drawerSource).toContain('templateReferences');
|
expect(drawerSource).toContain('预发布,仅供模板验证');
|
expect(wrapper.text()).toContain('含量模板 · 已发布 · 已生效');
|
expect(wrapper.text()).toContain('文本');
|
expect(wrapper.text()).toContain('数值');
|
expect(wrapper.text()).not.toContain('DECIMAL');
|
expect(wrapper.text()).not.toContain('PUBLISHED · ACTIVE');
|
});
|
|
it('does not show the pre-release warning when the current version is published', async () => {
|
apiMocks.getFormulaDetail.mockResolvedValueOnce({
|
data: {
|
bizEnabled: 'enabled',
|
dangqianGongshiBanbenId: 'v1',
|
gongshiBianma: 'hanliang_abc123',
|
id: 'f1',
|
versions: [
|
{ banbenHao: 1, bizStatus: 'PUBLISHED', gongshiDingyiId: 'f1', id: 'v1', parameters: [] },
|
{ banbenHao: 2, bizStatus: 'PRE_RELEASE', gongshiDingyiId: 'f1', id: 'v2', parameters: [] },
|
],
|
},
|
});
|
const wrapper = mount(VersionDrawer, {
|
global: {
|
stubs: {
|
...layoutStubs,
|
'a-alert': { props: ['message'], template: '<div>{{ message }}</div>' },
|
'a-descriptions': { template: '<div><slot /></div>' },
|
'a-descriptions-item': { template: '<span><slot /></span>' },
|
'a-form': true,
|
'a-form-item': true,
|
'a-input-number': true,
|
'a-select': true,
|
'a-tag': true,
|
'a-textarea': true,
|
},
|
},
|
});
|
|
await drawerState.init({ formulaId: 'f1' });
|
await flushPromises();
|
|
expect(wrapper.find('.formula-summary .pre-release-warning').exists()).toBe(false);
|
});
|
|
it('uses the reference total and opens a paged view for summaries beyond the first five', async () => {
|
const wrapper = mount(VersionDrawer, {
|
global: {
|
stubs: {
|
...layoutStubs,
|
'a-alert': true,
|
'a-descriptions': { template: '<div><slot name="extra" /><slot /></div>' },
|
'a-descriptions-item': { template: '<span><slot /></span>' },
|
'a-form': true,
|
'a-form-item': true,
|
'a-input-number': true,
|
'a-input-search': InputSearch,
|
'a-select': true,
|
'a-tag': true,
|
'a-textarea': true,
|
},
|
},
|
});
|
await drawerState.init({ formulaId: 'f1' });
|
await flushPromises();
|
|
expect(wrapper.text()).toContain('12');
|
const viewAll = wrapper.find('[data-test="view-all-references"]');
|
expect(viewAll.exists()).toBe(true);
|
await viewAll.trigger('click');
|
await flushPromises();
|
|
expect(apiMocks.getFormulaTemplateReferences).toHaveBeenCalledWith('f1', { currentPage: 1, pageSize: 20 });
|
expect(tableState.referenceConfig.pagination).toMatchObject({ pageSize: 20 });
|
});
|
|
it('trims template reference searches and returns to the first page', async () => {
|
const wrapper = mount(VersionDrawer, {
|
global: {
|
stubs: {
|
...layoutStubs,
|
'a-alert': true,
|
'a-descriptions': { template: '<div><slot name="extra" /><slot /></div>' },
|
'a-descriptions-item': { template: '<span><slot /></span>' },
|
'a-form': true,
|
'a-form-item': true,
|
'a-input-number': true,
|
'a-input-search': InputSearch,
|
'a-select': true,
|
'a-tag': true,
|
'a-textarea': true,
|
},
|
},
|
});
|
await drawerState.init({ formulaId: 'f1' });
|
await flushPromises();
|
await wrapper.find('[data-test="view-all-references"]').trigger('click');
|
await wrapper.find('[data-test="reference-search-input"]').setValue(' 含量模板 ');
|
await nextTick();
|
await wrapper.find('[data-test="reference-search-submit"]').trigger('click');
|
await flushPromises();
|
|
expect(tableState.referenceReload).toHaveBeenLastCalledWith({ page: 1, searchInfo: { keyword: '含量模板' } });
|
expect(apiMocks.getFormulaTemplateReferences).toHaveBeenLastCalledWith('f1', {
|
currentPage: 1,
|
keyword: '含量模板',
|
pageSize: 20,
|
});
|
});
|
|
it('keeps the formula description summary away from the drawer edges', () => {
|
expect(drawerSource).toMatch(/\.formula-summary\s*\{[^}]*padding:\s*16px 24px;/);
|
});
|
|
it('keeps the template references away from the drawer edges', () => {
|
expect(drawerSource).toMatch(/\.formula-references\s*\{[^}]*padding:\s*12px 24px;/);
|
});
|
|
it('keeps the version table away from the drawer edges', () => {
|
expect(drawerSource).toMatch(/\.version-table\s*\{[^}]*padding:\s*12px 24px 0;/);
|
});
|
|
it('fills the drawer body so the version table can use the remaining height', () => {
|
expect(drawerSource).toMatch(/<BasicDrawer[^>]*class="full-drawer"/);
|
});
|
|
it('lets the creation-time column absorb remaining table width', () => {
|
const creationTimeColumn = tableState.config.columns.find((column: any) => column.dataIndex === 'chuangjianShijian');
|
expect(creationTimeColumn).toMatchObject({ minWidth: 165 });
|
expect(creationTimeColumn).not.toHaveProperty('width');
|
});
|
|
it('shows the last modification time in the version list', () => {
|
const lastModifiedColumn = tableState.config.columns.find((column: any) => column.dataIndex === 'gengxinShijian');
|
expect(lastModifiedColumn).toMatchObject({ format: 'date|YYYY-MM-DD HH:mm:ss', title: '最后修改时间', width: 165 });
|
});
|
|
it('keeps the newest formula detail when an older request finishes last', async () => {
|
const first = deferred<any>();
|
const second = deferred<any>();
|
apiMocks.getFormulaDetail.mockImplementationOnce(() => first.promise).mockImplementationOnce(() => second.promise);
|
shallowMount(VersionDrawer, {
|
global: {
|
stubs: {
|
...layoutStubs,
|
'a-alert': true,
|
'a-descriptions': { template: '<div><slot /></div>' },
|
'a-descriptions-item': { template: '<span><slot /></span>' },
|
'a-form': true,
|
'a-form-item': true,
|
'a-input-number': true,
|
'a-select': true,
|
'a-tag': true,
|
'a-textarea': true,
|
},
|
},
|
});
|
|
const firstInit = drawerState.init({ formulaId: 'formula-a' });
|
const secondInit = drawerState.init({ formulaId: 'formula-b' });
|
second.resolve({ data: { bizEnabled: 'enabled', gongshiBianma: 'formula_b', id: 'formula-b', versions: [{ id: 'version-b' }] } });
|
await secondInit;
|
await nextTick();
|
first.resolve({ data: { bizEnabled: 'enabled', gongshiBianma: 'formula_a', id: 'formula-a', versions: [{ id: 'version-a' }] } });
|
await firstInit;
|
await nextTick();
|
|
expect(tableState.config.dataSource.value.map((version: { id: string }) => version.id)).toEqual(['version-b']);
|
});
|
|
it('ignores a stale reload after the drawer has switched to another formula', async () => {
|
const staleReload = deferred<any>();
|
apiMocks.getFormulaDetail
|
.mockResolvedValueOnce({
|
data: {
|
bizEnabled: 'enabled',
|
gongshiBianma: 'formula_a',
|
id: 'formula-a',
|
versions: [{ banbenHao: 1, bizStatus: 'DRAFT', gongshiDingyiId: 'formula-a', id: 'version-a', parameters: [] }],
|
},
|
})
|
.mockImplementationOnce(() => staleReload.promise)
|
.mockResolvedValueOnce({ data: { bizEnabled: 'enabled', gongshiBianma: 'formula_b', id: 'formula-b', versions: [{ id: 'version-b' }] } });
|
const wrapper = mount(VersionDrawer, {
|
global: {
|
stubs: {
|
...layoutStubs,
|
'a-alert': true,
|
'a-descriptions': { template: '<div><slot /></div>' },
|
'a-descriptions-item': { template: '<span><slot /></span>' },
|
'a-form': true,
|
'a-form-item': true,
|
'a-input-number': true,
|
'a-select': true,
|
'a-tag': true,
|
'a-textarea': true,
|
},
|
},
|
});
|
await drawerState.init({ formulaId: 'formula-a' });
|
await nextTick();
|
const preReleaseButton = wrapper.findAll('button').find((button) => button.text() === '提交预发布');
|
expect(preReleaseButton).toBeDefined();
|
void preReleaseButton!.trigger('click');
|
await Promise.resolve();
|
|
await drawerState.init({ formulaId: 'formula-b' });
|
staleReload.resolve({ data: { bizEnabled: 'enabled', gongshiBianma: 'formula_a', id: 'formula-a', versions: [{ id: 'version-a' }] } });
|
await flushPromises();
|
|
expect(tableState.config.dataSource.value.map((version: { id: string }) => version.id)).toEqual(['version-b']);
|
expect(wrapper.emitted('reload')).toBeUndefined();
|
});
|
|
it('delegates copying to the backend by source version and keeps the drawer open on API failure', async () => {
|
apiMocks.createFormulaVersion.mockRejectedValue(new Error('network error'));
|
const wrapper = shallowMount(VersionDrawer, {
|
global: {
|
stubs: {
|
...layoutStubs,
|
'a-alert': true,
|
'a-descriptions': true,
|
'a-descriptions-item': true,
|
'a-form': true,
|
'a-form-item': true,
|
'a-input-number': true,
|
'a-select': true,
|
'a-tag': true,
|
'a-textarea': true,
|
},
|
},
|
});
|
await drawerState.init({ formulaId: 'f1' });
|
const copy = tableState.config.actionColumn;
|
expect(drawerSource).toContain('sourceVersionId: version.id');
|
expect(drawerSource).toContain('await createFormulaVersion');
|
expect(drawerSource).not.toContain('closeDrawer()');
|
expect(copy).toBeTruthy();
|
expect(wrapper.exists()).toBe(true);
|
expect(drawerState.closeDrawer).not.toHaveBeenCalled();
|
});
|
|
it('edits only supported result rules on a draft version', () => {
|
expect(drawerSource).not.toContain("{ fullName: '数值集合', id: 'DECIMAL_LIST' }");
|
expect(drawerSource).toContain('label="修约方式"');
|
expect(drawerSource).toContain("{ fullName: '四舍六入五成双', id: 'HALF_EVEN' }");
|
expect(drawerSource).not.toContain('银行家舍入');
|
expect(drawerSource).toContain(':max="12"');
|
});
|
|
it('provides placeholders for every standard draft-version control', () => {
|
expect(drawerSource).toContain('placeholder="请选择结果类型"');
|
expect(drawerSource).toContain('placeholder="请输入修约小数位"');
|
expect(drawerSource).toContain('placeholder="请选择修约方式"');
|
});
|
|
it('keeps bottom padding inside the draft-version modal content', () => {
|
expect(drawerSource).toContain('class="draft-version-content"');
|
expect(drawerSource).toMatch(/\.draft-version-content\s*\{[^}]*padding-bottom:\s*24px;/);
|
});
|
|
it('locks the measured collapsed height while parameter configuration toggles', async () => {
|
const wrapper = mount(VersionDrawer, {
|
global: {
|
stubs: {
|
...layoutStubs,
|
'a-alert': true,
|
'a-descriptions': { template: '<div><slot /></div>' },
|
'a-descriptions-item': { template: '<span><slot /></span>' },
|
'a-form': { template: '<form><slot /></form>' },
|
'a-form-item': { template: '<div><slot /></div>' },
|
'a-input-number': true,
|
'a-select': true,
|
'a-tag': true,
|
'a-textarea': true,
|
FormulaEditor: true,
|
FormulaPreview: true,
|
ParameterTable: true,
|
TrialPanel: true,
|
},
|
},
|
});
|
await drawerState.init({ formulaId: 'f1', openDraft: true });
|
await flushPromises();
|
|
const editModal = wrapper.findAllComponents({ name: 'BasicModal' })[0]!;
|
expect(editModal.props('height')).toBeUndefined();
|
expect(editModal.props('minHeight')).toBeUndefined();
|
|
editModal.vm.$emit('heightChange', 648);
|
await nextTick();
|
expect(editModal.props('height')).toBe(648);
|
expect(editModal.props('minHeight')).toBe(648);
|
|
await wrapper.find('[data-test="toggle-parameters"]').trigger('click');
|
await nextTick();
|
expect(editModal.props('height')).toBe(648);
|
expect(editModal.props('minHeight')).toBe(648);
|
});
|
|
it('uses a responsive two-pane workspace with a full-width collapsible parameter section', () => {
|
expect(drawerSource).toContain('width="min(1200px, calc(100vw - 48px))"');
|
expect(drawerSource).toContain('class="draft-version-workspace"');
|
expect(drawerSource).toContain('class="draft-version-editor"');
|
expect(drawerSource).toContain('class="draft-version-validation"');
|
expect(drawerSource).toContain('class="draft-version-preview"');
|
expect(drawerSource).toContain('class="draft-version-preview-title"');
|
expect(drawerSource).toContain('class="draft-version-trial"');
|
expect(drawerSource).toContain('class="draft-version-trial-title"');
|
expect(drawerSource).not.toContain('<a-tabs');
|
expect(drawerSource).toMatch(/\.draft-version-preview\s*\{[^}]*margin-top:\s*12px;[^}]*\}/);
|
expect(drawerSource).not.toMatch(/\.draft-version-preview\s*\{[^}]*border:/);
|
expect(drawerSource).not.toMatch(/\.draft-version-preview-title\s*\{[^}]*border:/);
|
expect(drawerSource).not.toMatch(/\.draft-version-trial-title\s*\{[^}]*border:/);
|
expect(drawerSource.indexOf('<FormulaPreview')).toBeLessThan(drawerSource.indexOf('class="draft-version-validation"'));
|
expect(drawerSource.indexOf('<TrialPanel')).toBeGreaterThan(drawerSource.indexOf('class="draft-version-validation"'));
|
expect(drawerSource).toContain('parametersExpanded');
|
expect(drawerSource).toContain('data-test="toggle-parameters"');
|
expect(drawerSource).toContain('DownOutlined v-if="parametersExpanded"');
|
expect(drawerSource).toContain('UpOutlined v-else');
|
expect(drawerSource.indexOf("parametersExpanded ? '收起配置' : '展开配置'")).toBeLessThan(drawerSource.indexOf('DownOutlined v-if="parametersExpanded"'));
|
expect(drawerSource).not.toMatch(/\.draft-version-content\s*\{[^}]*(?:height|max-height):/);
|
});
|
|
it('does not turn the formula editor column into a nested scroll container', () => {
|
expect(drawerSource).not.toMatch(/\.draft-version-editor\s*\{[^}]*overflow:\s*auto;/);
|
expect(drawerSource).not.toMatch(/\.draft-version-editor\s*\{[^}]*overflow:\s*hidden;/);
|
});
|
|
it('lets the expanded parameter table replace the entire modal workspace', async () => {
|
const wrapper = mount(VersionDrawer, {
|
global: {
|
stubs: {
|
...layoutStubs,
|
'a-alert': true,
|
'a-descriptions': { template: '<div><slot /></div>' },
|
'a-descriptions-item': { template: '<span><slot /></span>' },
|
'a-form': { template: '<form><slot /></form>' },
|
'a-form-item': { template: '<div><slot /></div>' },
|
'a-input-number': true,
|
'a-select': true,
|
'a-tag': true,
|
'a-textarea': true,
|
FormulaEditor: true,
|
FormulaPreview: true,
|
ParameterTable: {
|
name: 'ElnFormulaParameterTable',
|
props: { fullHeight: Boolean, modelValue: Array },
|
template: '<div data-test="parameter-table" />',
|
},
|
TrialPanel: true,
|
},
|
},
|
});
|
await drawerState.init({ formulaId: 'f1', openDraft: true });
|
await flushPromises();
|
|
expect(wrapper.find('.draft-version-workspace').exists()).toBe(true);
|
await wrapper.find('[data-test="toggle-parameters"]').trigger('click');
|
await nextTick();
|
|
expect(wrapper.find('.draft-version-workspace').exists()).toBe(false);
|
expect(wrapper.find('.draft-version-parameters.is-expanded').exists()).toBe(true);
|
expect(wrapper.findComponent({ name: 'ElnFormulaParameterTable' }).props('fullHeight')).toBe(true);
|
});
|
|
it('recreates the trial panel after the edit modal closes', async () => {
|
let trialPanelInstance = 0;
|
const TrialPanelStub = defineComponent({
|
name: 'ElnFormulaTrialPanel',
|
props: ['rule'],
|
setup() {
|
return { instance: ++trialPanelInstance };
|
},
|
template: '<div data-test="trial-panel">{{ instance }}</div>',
|
});
|
const wrapper = mount(VersionDrawer, {
|
global: {
|
stubs: {
|
...layoutStubs,
|
'a-alert': true,
|
'a-descriptions': { template: '<div><slot /></div>' },
|
'a-descriptions-item': { template: '<span><slot /></span>' },
|
'a-form': { template: '<form><slot /></form>' },
|
'a-form-item': { template: '<div><slot /></div>' },
|
'a-input-number': true,
|
'a-select': true,
|
'a-tag': true,
|
'a-textarea': true,
|
FormulaEditor: true,
|
FormulaPreview: true,
|
ParameterTable: true,
|
TrialPanel: TrialPanelStub,
|
},
|
},
|
});
|
await drawerState.init({ formulaId: 'f1', openDraft: true });
|
await flushPromises();
|
|
expect(wrapper.find('[data-test="trial-panel"]').text()).toBe('1');
|
const editModal = wrapper.findAllComponents({ name: 'BasicModal' })[0]!;
|
await editModal.props('afterClose')();
|
await nextTick();
|
|
expect(wrapper.find('[data-test="trial-panel"]').text()).toBe('2');
|
});
|
|
it('shows confirmation loading while a draft version is being saved', async () => {
|
const saveRequest = deferred<any>();
|
apiMocks.updateFormulaVersion.mockReturnValueOnce(saveRequest.promise);
|
const wrapper = mount(VersionDrawer, {
|
global: {
|
stubs: {
|
...layoutStubs,
|
'a-alert': true,
|
'a-descriptions': { template: '<div><slot /></div>' },
|
'a-descriptions-item': { template: '<span><slot /></span>' },
|
'a-form': { template: '<form><slot /></form>' },
|
'a-form-item': { template: '<div><slot /></div>' },
|
'a-input-number': true,
|
'a-select': true,
|
'a-tag': true,
|
'a-textarea': true,
|
FormulaEditor: true,
|
FormulaPreview: true,
|
ParameterTable: true,
|
TrialPanel: true,
|
},
|
},
|
});
|
await drawerState.init({ formulaId: 'f1', openDraft: true });
|
await flushPromises();
|
|
const editModal = wrapper.findAllComponents({ name: 'BasicModal' })[0]!;
|
editModal.vm.$emit('ok');
|
await nextTick();
|
expect(editModal.props('confirmLoading')).toBe(true);
|
|
saveRequest.resolve({ data: {} });
|
await flushPromises();
|
expect(editModal.props('confirmLoading')).toBe(false);
|
});
|
|
it('opens published versions in a readonly preview modal', async () => {
|
const validateEditor = vi.fn();
|
const FormulaEditorStub = defineComponent({
|
name: 'ElnFormulaEditor',
|
props: ['functions', 'modelValue', 'parameters', 'readonly', 'rule'],
|
setup(_, { expose }) {
|
expose({ validate: validateEditor });
|
return {};
|
},
|
template: '<div class="formula-editor-stub" />',
|
});
|
const wrapper = mount(VersionDrawer, {
|
global: {
|
stubs: {
|
...layoutStubs,
|
'a-alert': true,
|
'a-descriptions': { template: '<div><slot /></div>' },
|
'a-descriptions-item': { template: '<span><slot /></span>' },
|
'a-form': { template: '<form><slot /></form>' },
|
'a-form-item': { template: '<div><slot /></div>' },
|
'a-input-number': true,
|
'a-select': true,
|
'a-tag': true,
|
'a-textarea': true,
|
FormulaEditor: FormulaEditorStub,
|
FormulaPreview: true,
|
ParameterTable: true,
|
TrialPanel: true,
|
},
|
},
|
});
|
await drawerState.init({ formulaId: 'f1' });
|
await flushPromises();
|
|
const preview = wrapper.findAll('button').find((button) => button.text() === '预览');
|
expect(preview).toBeDefined();
|
await preview!.trigger('click');
|
await flushPromises();
|
|
const editor = wrapper.findComponent({ name: 'ElnFormulaEditor' });
|
const versionModal = wrapper.findAllComponents({ name: 'BasicModal' })[0]!;
|
expect(editor.props('readonly')).toBe(true);
|
expect(versionModal.props('title')).toBe('预览公式版本 - V1');
|
expect(versionModal.props('showOkBtn')).toBe(false);
|
expect(validateEditor).toHaveBeenCalledTimes(1);
|
});
|
|
it('loads function assistance and mounts the controlled editor for a draft version', async () => {
|
const validateEditor = vi.fn();
|
const FormulaEditorStub = defineComponent({
|
name: 'ElnFormulaEditor',
|
props: ['functions', 'modelValue', 'parameters', 'rule'],
|
setup(_, { expose }) {
|
expose({ validate: validateEditor });
|
return {};
|
},
|
template: '<div class="formula-editor-stub" />',
|
});
|
const wrapper = mount(VersionDrawer, {
|
global: {
|
stubs: {
|
...layoutStubs,
|
'a-alert': true,
|
'a-descriptions': { template: '<div><slot /></div>' },
|
'a-descriptions-item': { template: '<span><slot /></span>' },
|
'a-form': { template: '<form><slot /></form>' },
|
'a-form-item': { template: '<div><slot /></div>' },
|
'a-input-number': true,
|
'a-select': true,
|
'a-tag': true,
|
'a-textarea': true,
|
'a-button': true,
|
FormulaEditor: FormulaEditorStub,
|
FormulaPreview: true,
|
ParameterTable: true,
|
TrialPanel: true,
|
},
|
},
|
});
|
await drawerState.init({ formulaId: 'f1' });
|
await flushPromises();
|
|
const edit = wrapper.findAll('button').find((button) => button.text() === '编辑');
|
await edit!.trigger('click');
|
await flushPromises();
|
|
const editor = wrapper.findComponent({ name: 'ElnFormulaEditor' });
|
expect(wrapper.findAllComponents({ name: 'BasicModal' })[0]!.props('title')).toBe('编辑公式版本 - V2');
|
expect(apiMocks.getFormulaFunctions).toHaveBeenCalledTimes(1);
|
expect(editor.exists()).toBe(true);
|
expect(editor.props('parameters')).toEqual([]);
|
expect(editor.props('rule')).toEqual(expect.objectContaining({ biaodashi: 'amount * 2', jieguoLeixing: 'DECIMAL', parameters: [] }));
|
expect(validateEditor).toHaveBeenCalledTimes(1);
|
});
|
|
it('shows the target version in reason modal titles', async () => {
|
const wrapper = mount(VersionDrawer, {
|
global: {
|
stubs: {
|
...layoutStubs,
|
'a-alert': true,
|
'a-descriptions': { template: '<div><slot /></div>' },
|
'a-descriptions-item': { template: '<span><slot /></span>' },
|
'a-form': true,
|
'a-form-item': { template: '<div><slot /></div>' },
|
'a-input-number': true,
|
'a-select': true,
|
'a-tag': true,
|
'a-textarea': true,
|
FormulaEditor: true,
|
FormulaPreview: true,
|
ParameterTable: true,
|
TrialPanel: true,
|
},
|
},
|
});
|
await drawerState.init({ formulaId: 'f1' });
|
await flushPromises();
|
|
const reasonModal = wrapper.findAllComponents({ name: 'BasicModal' })[1]!;
|
const cancel = wrapper.findAll('button').find((button) => button.text() === '作废');
|
await cancel!.trigger('click');
|
expect(reasonModal.props('title')).toBe('作废公式版本 - V2');
|
|
const setCurrent = wrapper.findAll('button').find((button) => button.text() === '设为当前');
|
await setCurrent!.trigger('click');
|
expect(reasonModal.props('title')).toBe('切换当前版本 - V1');
|
});
|
|
it('opens the earliest draft editor immediately when initialized after draft creation', async () => {
|
apiMocks.getFormulaDetail.mockResolvedValueOnce({
|
data: {
|
bizEnabled: 'enabled',
|
gongshiBianma: 'hanliang_draft',
|
id: 'formula-draft-1',
|
versions: [
|
{
|
banbenHao: 1,
|
biaodashi: '',
|
bizStatus: 'DRAFT',
|
gongshiDingyiId: 'formula-draft-1',
|
id: 'version-draft-1',
|
jieguoLeixing: 'DECIMAL',
|
parameters: [],
|
},
|
],
|
},
|
});
|
const wrapper = mount(VersionDrawer, {
|
global: {
|
stubs: {
|
...layoutStubs,
|
'a-alert': true,
|
'a-descriptions': { template: '<div><slot /></div>' },
|
'a-descriptions-item': { template: '<span><slot /></span>' },
|
'a-form': { template: '<form><slot /></form>' },
|
'a-form-item': { template: '<div><slot /></div>' },
|
'a-input-number': true,
|
'a-select': true,
|
'a-tag': true,
|
'a-textarea': true,
|
FormulaEditor: { name: 'ElnFormulaEditor', props: ['functions', 'parameters', 'rule'], template: '<div class="formula-editor-stub" />' },
|
FormulaPreview: true,
|
ParameterTable: { props: ['modelValue'], template: '<div data-test="parameter-table">{{ modelValue.length }}</div>' },
|
TrialPanel: true,
|
},
|
},
|
});
|
|
await drawerState.init({ formulaId: 'formula-draft-1', openDraft: true });
|
await flushPromises();
|
|
expect(apiMocks.getFormulaFunctions).toHaveBeenCalledTimes(1);
|
expect(wrapper.findComponent({ name: 'ElnFormulaEditor' }).props('rule')).toEqual(
|
expect.objectContaining({ biaodashi: '', jieguoLeixing: 'DECIMAL', parameters: [], xiuyueMoshi: 'HALF_UP', xiuyueXiaoshuWei: 2 }),
|
);
|
expect(wrapper.find('.draft-version-preview').exists()).toBe(true);
|
expect(wrapper.find('.draft-version-trial').exists()).toBe(true);
|
expect(wrapper.text()).toContain('公式预览');
|
expect(wrapper.text()).toContain('临时试算');
|
expect(wrapper.find('[data-test="parameter-table"]').exists()).toBe(false);
|
|
await wrapper.find('[data-test="toggle-parameters"]').trigger('click');
|
await nextTick();
|
expect(wrapper.find('[data-test="parameter-table"]').exists()).toBe(true);
|
|
await drawerState.init({ formulaId: 'f1', openDraft: true });
|
await flushPromises();
|
expect(wrapper.find('.draft-version-preview').exists()).toBe(true);
|
expect(wrapper.find('.draft-version-trial').exists()).toBe(true);
|
expect(wrapper.find('[data-test="parameter-table"]').exists()).toBe(false);
|
});
|
|
it('clears and hides rounding settings for non-decimal results', async () => {
|
const Select = defineComponent({ name: 'ASelect', props: ['allowClear', 'fieldNames', 'options', 'value'], emits: ['update:value'], template: '<div />' });
|
const InputNumber = defineComponent({ name: 'AInputNumber', props: ['value'], template: '<input :value="value" />' });
|
const wrapper = mount(VersionDrawer, {
|
global: {
|
stubs: {
|
...layoutStubs,
|
'a-alert': true,
|
'a-descriptions': { template: '<div><slot /></div>' },
|
'a-descriptions-item': { template: '<span><slot /></span>' },
|
'a-form': { template: '<form><slot /></form>' },
|
'a-form-item': { template: '<div><slot /></div>' },
|
'a-input-number': InputNumber,
|
'a-select': Select,
|
'a-tag': true,
|
'a-textarea': true,
|
FormulaEditor: true,
|
FormulaPreview: true,
|
ParameterTable: true,
|
TrialPanel: true,
|
},
|
},
|
});
|
await drawerState.init({ formulaId: 'f1', openDraft: true });
|
await flushPromises();
|
|
const selects = wrapper.findAllComponents({ name: 'ASelect' });
|
expect(selects).toHaveLength(2);
|
expect(selects.every((select) => JSON.stringify(select.props('fieldNames')) === JSON.stringify({ label: 'fullName', value: 'id' }))).toBe(true);
|
await selects[0]!.vm.$emit('update:value', 'BOOLEAN');
|
await nextTick();
|
|
expect(wrapper.findAllComponents({ name: 'AInputNumber' })).toHaveLength(0);
|
expect(wrapper.findAllComponents({ name: 'ASelect' })).toHaveLength(1);
|
|
await wrapper.findComponent({ name: 'ASelect' }).vm.$emit('update:value', 'DECIMAL');
|
await nextTick();
|
expect(wrapper.findComponent({ name: 'AInputNumber' }).props('value')).toBe(2);
|
expect(wrapper.findAllComponents({ name: 'ASelect' }).at(1)!.props('value')).toBe('HALF_UP');
|
});
|
});
|