import type { FormulaRule } from '#/api/x/eln/formulas';
|
|
import { flushPromises, mount } from '@vue/test-utils';
|
import { defineComponent, nextTick } from 'vue';
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import TrialPanel, { parseDecimalListInput } from '../TrialPanel.vue';
|
import panelSource from '../TrialPanel.vue?raw';
|
|
const apiMocks = vi.hoisted(() => ({ previewFormula: vi.fn() }));
|
|
vi.mock('#/api/x/eln/formulas', () => ({ previewFormula: apiMocks.previewFormula }));
|
|
const stubs = {
|
'a-alert': { props: ['message'], template: '<div>{{ message }}</div>' },
|
'a-button': defineComponent({
|
props: ['disabled', 'loading'],
|
emits: ['click'],
|
template: '<button :disabled="disabled" @click="$emit(\'click\')"><slot /></button>',
|
}),
|
'a-descriptions': { template: '<div><slot /></div>' },
|
'a-descriptions-item': { template: '<span><slot /></span>' },
|
'a-input': defineComponent({
|
props: ['value'],
|
emits: ['update:value'],
|
template: '<input class="trial-text" :value="value" @input="$emit(\'update:value\', $event.target.value)" />',
|
}),
|
'a-input-number': defineComponent({
|
props: ['value'],
|
emits: ['update:value'],
|
template: '<input class="trial-number" :value="value" @input="$emit(\'update:value\', Number($event.target.value))" />',
|
}),
|
'a-select': defineComponent({
|
name: 'ASelect',
|
props: ['fieldNames', 'options', 'value'],
|
emits: ['update:value'],
|
template: '<div class="trial-boolean">{{ value }}</div>',
|
}),
|
'a-textarea': defineComponent({
|
props: ['value'],
|
emits: ['update:value'],
|
template: '<textarea class="trial-list" :value="value" @input="$emit(\'update:value\', $event.target.value)" />',
|
}),
|
};
|
|
const rule: FormulaRule = {
|
biaodashi: 'amount + flag',
|
jieguoLeixing: 'DECIMAL',
|
parameters: [
|
{ canshuBianma: 'amount', canshuMingcheng: '含量', paixu: 1, shifouBitian: 1, shujuLeixing: 'DECIMAL' },
|
{ canshuBianma: 'replicates', canshuMingcheng: '重复值', paixu: 2, shifouBitian: 1, shujuLeixing: 'DECIMAL_LIST' },
|
{ canshuBianma: 'remark', canshuMingcheng: '备注', paixu: 3, shifouBitian: 0, shujuLeixing: 'TEXT' },
|
{ canshuBianma: 'flag', canshuMingcheng: '是否合格', paixu: 4, shifouBitian: 1, shujuLeixing: 'BOOLEAN' },
|
],
|
};
|
|
describe('eLN formula trial panel', () => {
|
beforeEach(() => {
|
vi.clearAllMocks();
|
});
|
|
it('parses decimal-list values line by line and rejects invalid values', () => {
|
expect(parseDecimalListInput('1.2\n-3\n')).toEqual(['1.2', '-3']);
|
expect(() => parseDecimalListInput('1\n1e3')).toThrow('第 2 行不是有效十进制数');
|
});
|
|
it('keeps typed inputs locally and calls the preview API with all slot types', async () => {
|
apiMocks.previewFormula.mockResolvedValue({ data: { diagnostics: [], rawResult: '3.0', roundedResult: '3.00', status: 'SUCCESS' } });
|
const wrapper = mount(TrialPanel, { props: { rule }, global: { stubs } });
|
|
await wrapper.find('.trial-number').setValue('1.25');
|
await wrapper.find('.trial-list').setValue('1\n2.5');
|
await wrapper.find('.trial-text').setValue('备注');
|
await wrapper.findComponent({ name: 'ASelect' }).vm.$emit('update:value', false);
|
await wrapper.find('[data-test="run-trial"]').trigger('click');
|
await flushPromises();
|
|
expect(apiMocks.previewFormula).toHaveBeenCalledWith({
|
...rule,
|
inputs: { amount: 1.25, flag: false, remark: '备注', replicates: ['1', '2.5'] },
|
});
|
expect(wrapper.text()).toContain('原始结果:3.0');
|
expect(wrapper.text()).toContain('修约结果:3.00');
|
expect(panelSource).toContain("slot.shujuLeixing === 'DECIMAL'");
|
expect(panelSource).toContain("slot.shujuLeixing === 'DECIMAL_LIST'");
|
expect(panelSource).toContain("slot.shujuLeixing === 'TEXT'");
|
expect(panelSource).toContain("slot.shujuLeixing === 'BOOLEAN'");
|
expect(panelSource).not.toContain('localStorage');
|
expect(panelSource).not.toContain('sessionStorage');
|
});
|
|
it('shows missing-input status and diagnostics while leaving results empty', async () => {
|
apiMocks.previewFormula.mockResolvedValue({
|
data: {
|
diagnostics: [{ end: 6, message: '缺少参数:amount', severity: 'ERROR', start: 0 }],
|
rawResult: null,
|
roundedResult: null,
|
status: 'MISSING_INPUT',
|
},
|
});
|
const wrapper = mount(TrialPanel, { props: { rule }, global: { stubs } });
|
|
await wrapper.find('[data-test="run-trial"]').trigger('click');
|
await flushPromises();
|
expect(apiMocks.previewFormula).toHaveBeenCalledTimes(1);
|
expect(wrapper.text()).toContain('MISSING_INPUT');
|
expect(wrapper.text()).toContain('缺少参数:amount');
|
expect(wrapper.text()).not.toContain('原始结果');
|
expect(wrapper.text()).not.toContain('修约结果');
|
});
|
|
it('submits an untouched boolean parameter as null instead of false', async () => {
|
apiMocks.previewFormula.mockResolvedValue({ data: { diagnostics: [], rawResult: null, roundedResult: null, status: 'MISSING_INPUT' } });
|
const wrapper = mount(TrialPanel, { props: { rule }, global: { stubs } });
|
|
await wrapper.find('[data-test="run-trial"]').trigger('click');
|
await flushPromises();
|
|
expect(apiMocks.previewFormula).toHaveBeenCalledWith(expect.objectContaining({ inputs: expect.objectContaining({ flag: null }) }));
|
});
|
|
it('shows error status and diagnostics while leaving results empty', async () => {
|
apiMocks.previewFormula.mockResolvedValue({
|
data: { diagnostics: [{ end: 4, message: '除数不能为零', severity: 'ERROR', start: 2 }], rawResult: null, roundedResult: null, status: 'ERROR' },
|
});
|
const wrapper = mount(TrialPanel, { props: { rule }, global: { stubs } });
|
|
await wrapper.find('[data-test="run-trial"]').trigger('click');
|
await flushPromises();
|
|
expect(wrapper.text()).toContain('ERROR');
|
expect(wrapper.text()).toContain('除数不能为零');
|
expect(wrapper.text()).not.toContain('原始结果');
|
expect(wrapper.text()).not.toContain('修约结果');
|
});
|
|
it('clears trial inputs and marks an existing result stale after rule changes', async () => {
|
apiMocks.previewFormula.mockResolvedValue({ data: { diagnostics: [], rawResult: '1.25', roundedResult: '1.25', status: 'SUCCESS' } });
|
const wrapper = mount(TrialPanel, { props: { rule }, global: { stubs } });
|
await wrapper.find('.trial-number').setValue('1.25');
|
await wrapper.find('[data-test="run-trial"]').trigger('click');
|
await flushPromises();
|
|
await wrapper.setProps({ rule: { ...rule, biaodashi: 'amount * 2' } });
|
await nextTick();
|
expect(wrapper.text()).toContain('结果已过期');
|
expect(wrapper.text()).toContain('原始结果:1.25');
|
expect(apiMocks.previewFormula).toHaveBeenCalledTimes(1);
|
|
await wrapper.find('[data-test="clear-trial-inputs"]').trigger('click');
|
expect((wrapper.find('.trial-number').element as HTMLInputElement).value).toBe('');
|
});
|
|
it('removes inputs for deleted parameters before the next trial', async () => {
|
apiMocks.previewFormula.mockResolvedValue({ data: { diagnostics: [], rawResult: null, roundedResult: null, status: 'MISSING_INPUT' } });
|
const wrapper = mount(TrialPanel, { props: { rule }, global: { stubs } });
|
await wrapper.find('.trial-text').setValue('将被删除');
|
await wrapper.setProps({ rule: { ...rule, parameters: rule.parameters.filter((slot) => slot.canshuBianma !== 'remark') } });
|
await nextTick();
|
await wrapper.find('[data-test="run-trial"]').trigger('click');
|
await flushPromises();
|
|
expect(apiMocks.previewFormula).toHaveBeenCalledWith(expect.objectContaining({ inputs: expect.not.objectContaining({ remark: expect.anything() }) }));
|
});
|
|
it('prevents duplicate submissions and keeps inputs when the request fails', async () => {
|
let rejectRequest!: (error: Error) => void;
|
apiMocks.previewFormula.mockReturnValue(
|
new Promise((_resolve, reject) => {
|
rejectRequest = reject;
|
}),
|
);
|
const wrapper = mount(TrialPanel, { props: { rule }, global: { stubs } });
|
await wrapper.find('.trial-number').setValue('2');
|
void wrapper.find('[data-test="run-trial"]').trigger('click');
|
await wrapper.find('[data-test="run-trial"]').trigger('click');
|
expect(apiMocks.previewFormula).toHaveBeenCalledTimes(1);
|
|
rejectRequest(new Error('network'));
|
await flushPromises();
|
expect(wrapper.text()).toContain('试算请求失败');
|
expect((wrapper.find('.trial-number').element as HTMLInputElement).value).toBe('2');
|
});
|
});
|