import type { StabilityAnalysisRow } from '#/api/x/lims/wendingxing';
|
|
import { describe, expect, it } from 'vitest';
|
|
import { buildStabilityChartModels, createStabilityChartOption, formatDisplayNumber, formatPeriodUnit } from '../stabilityAnalysis';
|
|
function row(overrides: Partial<StabilityAnalysisRow> = {}): StabilityAnalysisRow {
|
return {
|
batch_key: 'plan-a::250102',
|
biaozhun_id: 'standard-a',
|
exception_type: '',
|
jieguo_luru_riqi: '2026-01-01',
|
jihua_bianhao: 'STP-A',
|
jihua_id: 'plan-a',
|
numeric_result: 6.7,
|
pihao: '250102',
|
raw_result: '6.7',
|
shangxian: '7',
|
shangxian_value: 7,
|
shuju_leixing: '数值',
|
unit: '',
|
xiaxian: '5',
|
xiaxian_value: 5,
|
xiangmu_bianma: 'XM1899',
|
xiangmu_id: 'ph',
|
xiangmu_mingcheng: 'pH值',
|
yangpin_mingcheng: '脉络宁口服液',
|
zhouqi: '0',
|
zhouqi_danwei: '/',
|
zhouqi_order: 0,
|
...overrides,
|
};
|
}
|
|
describe('buildStabilityChartModels', () => {
|
it('keeps duplicate batch numbers separate and limits the axis to observed periods', () => {
|
const models = buildStabilityChartModels(
|
[
|
row(),
|
row({ numeric_result: 6, raw_result: '6', zhouqi: '1', zhouqi_danwei: '月', zhouqi_order: 30 }),
|
row({
|
batch_key: 'plan-b::250102',
|
jieguo_luru_riqi: '2026-01-02',
|
jihua_bianhao: 'STP-B',
|
jihua_id: 'plan-b',
|
numeric_result: 6.8,
|
}),
|
row({
|
batch_key: 'plan-b::250102',
|
jieguo_luru_riqi: '2026-01-09',
|
jihua_bianhao: 'STP-B',
|
jihua_id: 'plan-b',
|
numeric_result: 6.5,
|
pihao: '250102',
|
raw_result: '6.5',
|
zhouqi: '7',
|
zhouqi_danwei: '天',
|
zhouqi_order: 7,
|
}),
|
],
|
[{ xiangmu_id: 'ph', zhouqi: '3', zhouqi_danwei: '月', zhouqi_order: 90 }],
|
);
|
|
expect(models).toHaveLength(1);
|
expect(models[0]?.categories).toEqual(['0', '7天', '1月']);
|
expect(models[0]?.series.find((series) => series.id === 'plan-a::250102')).toMatchObject({
|
data: [6.7, null, 6],
|
name: 'STP-A / 250102',
|
});
|
expect(models[0]?.series.find((series) => series.id === 'plan-b::250102')?.name).toBe('STP-B / 250102');
|
expect(models[0]?.series.find((series) => series.id === 'upper-limit')?.data).toEqual([7, 7, 7]);
|
expect(models[0]?.series.find((series) => series.id === 'lower-limit')?.data).toEqual([5, 5, 5]);
|
expect(models[0]?.series.map((series) => series.id).slice(-2)).toEqual(['lower-limit', 'upper-limit']);
|
});
|
|
it('splits the same item when standard, limits, or units differ', () => {
|
const models = buildStabilityChartModels([
|
row(),
|
row({ biaozhun_id: 'standard-b', shangxian: '7.5', shangxian_value: 7.5 }),
|
row({ biaozhun_id: 'standard-c', unit: 'mg/ml' }),
|
]);
|
|
expect(models.filter((model) => model.item_id === 'ph')).toHaveLength(3);
|
expect(models[0]).toMatchObject({ lower_limit: '5', standard_id: 'standard-a', upper_limit: '7' });
|
});
|
|
it('uses the project code as the primary grouping key', () => {
|
const models = buildStabilityChartModels([row(), row({ xiangmu_id: 'ph-new-id' })]);
|
|
expect(models).toHaveLength(1);
|
});
|
|
it('keeps qualitative items as table-only models', () => {
|
const [model] = buildStabilityChartModels([
|
row({
|
biaozhun_id: 'standard-text',
|
numeric_result: null,
|
raw_result: '符合规定',
|
shangxian: '',
|
shangxian_value: null,
|
shuju_leixing: '文本',
|
xiaxian: '',
|
xiaxian_value: null,
|
xiangmu_bianma: 'XM-TEXT',
|
xiangmu_id: 'appearance',
|
xiangmu_mingcheng: '性状',
|
}),
|
]);
|
|
expect(model?.text_only).toBe(true);
|
expect(model?.series).toEqual([]);
|
});
|
|
it('retains exception metadata and ignores qualified numeric text', () => {
|
const [model] = buildStabilityChartModels([
|
row({ exception_type: 'oos' }),
|
row({
|
batch_key: 'plan-a::250103',
|
numeric_result: null,
|
pihao: '250103',
|
raw_result: '<0.01',
|
}),
|
]);
|
|
expect(model?.series.find((series) => series.id === 'plan-a::250102')?.points[0]?.exception_type).toBe('oos');
|
expect(model?.series.find((series) => series.id === 'plan-a::250103')?.data).toEqual([null]);
|
});
|
});
|
|
describe('createStabilityChartOption', () => {
|
it('renders independent dashed limits, a scroll legend, and disconnected record lines', () => {
|
const [model] = buildStabilityChartModels([row()]);
|
const option = createStabilityChartOption(model!, '稳定性趋势');
|
const series = option.series as Array<Record<string, any>>;
|
|
expect(option.legend).toMatchObject({ type: 'scroll' });
|
expect(series.find((item) => item.id === 'upper-limit')).toMatchObject({ lineStyle: { color: '#ef4444', type: 'dashed' }, name: '上限' });
|
expect(series.find((item) => item.id === 'lower-limit')).toMatchObject({ lineStyle: { color: '#f59e0b', type: 'dashed' }, name: '下限' });
|
expect(series.find((item) => item.id === 'plan-a::250102')?.connectNulls).toBe(false);
|
expect(option.yAxis).toMatchObject({ scale: true });
|
const tooltip = option.tooltip as { formatter: (params: unknown) => string };
|
expect(tooltip.formatter([{ axisValueLabel: '0', dataIndex: 0, marker: '', seriesId: 'plan-a::250102', seriesName: '250102', value: 6.7 }])).toContain(
|
'单位:-',
|
);
|
});
|
|
it('renders exception results as ordinary batch points', () => {
|
const [model] = buildStabilityChartModels([row({ exception_type: 'oos' })]);
|
const option = createStabilityChartOption(model!, '稳定性趋势');
|
const recordSeries = (option.series as Array<Record<string, any>>).find((item) => item.id === 'plan-a::250102');
|
|
expect(recordSeries?.data).toEqual([6.7]);
|
});
|
});
|
|
describe('table display formatting', () => {
|
it('removes database numeric trailing zeroes', () => {
|
expect(formatDisplayNumber('18.000000000000000')).toBe('18');
|
expect(formatDisplayNumber('0.050000000000000')).toBe('0.05');
|
expect(formatDisplayNumber('')).toBe('-');
|
});
|
|
it('translates stored period units for display', () => {
|
expect(formatPeriodUnit('month')).toBe('月');
|
expect(formatPeriodUnit('day')).toBe('天');
|
expect(formatPeriodUnit('/')).toBe('/');
|
});
|
});
|