import { flushPromises, mount } from '@vue/test-utils';
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import StabilityAnalysisPage from '../../index.vue';
|
import pageSource from '../../index.vue?raw';
|
|
const apiMocks = vi.hoisted(() => ({
|
getData: vi.fn(),
|
getOptions: vi.fn(),
|
}));
|
const exportMocks = vi.hoisted(() => ({ download: vi.fn() }));
|
|
vi.mock('#/api/x/lims/wendingxing', () => ({
|
getStabilityAnalysisData: apiMocks.getData,
|
getStabilityAnalysisOptions: apiMocks.getOptions,
|
}));
|
|
vi.mock('../../helpers/stabilityExport', () => ({
|
downloadStabilityExport: exportMocks.download,
|
}));
|
|
vi.mock('@jnpf/hooks', () => ({
|
useMessage: () => ({ createMessage: { error: vi.fn(), warning: vi.fn() } }),
|
}));
|
|
vi.mock('@vben/plugins/echarts', () => ({
|
EchartsUI: { name: 'EchartsUI', template: '<div />' },
|
useEcharts: () => ({ renderEcharts: vi.fn() }),
|
}));
|
|
const PopupStub = {
|
name: 'AnalysisOptionPopup',
|
props: ['columns', 'disabled', 'mode', 'options', 'popupTitle', 'value'],
|
template: '<div class="popup-stub" />',
|
};
|
|
function mountPage() {
|
return mount(StabilityAnalysisPage, {
|
global: {
|
stubs: {
|
'a-alert': { template: '<div class="alert-stub"><slot /></div>' },
|
'a-button': { props: ['disabled'], template: '<button :disabled="disabled" @click="$emit(\'click\')"><slot /></button>' },
|
ASpin: { template: '<div><slot /></div>' },
|
'a-empty': { props: ['description'], template: '<div class="empty-stub">{{ description }}</div>' },
|
'a-table': {
|
name: 'ATable',
|
props: ['bordered', 'columns', 'dataSource', 'pagination', 'scroll', 'tableLayout'],
|
template: '<div class="analysis-table" />',
|
},
|
AnalysisOptionPopup: PopupStub,
|
},
|
},
|
});
|
}
|
|
async function selectFilters(wrapper: ReturnType<typeof mountPage>) {
|
await flushPromises();
|
const popup = (title: string) => wrapper.findAllComponents(PopupStub).find((item) => item.props('popupTitle') === title)!;
|
|
popup('选择分类').vm.$emit('update:value', 'CJ');
|
popup('选择分类').vm.$emit('change', 'CJ');
|
await flushPromises();
|
popup('选择品名').vm.$emit('update:value', 'product-1');
|
popup('选择品名').vm.$emit('change', 'product-1');
|
await flushPromises();
|
popup('选择检验项目').vm.$emit('update:value', ['item-ph']);
|
popup('选择检验项目').vm.$emit('change', ['item-ph']);
|
await flushPromises();
|
popup('选择稳定性考察编号').vm.$emit('update:value', ['plan-1']);
|
popup('选择稳定性考察编号').vm.$emit('change', ['plan-1']);
|
await flushPromises();
|
popup('选择批号').vm.$emit('update:value', ['plan-1::250102']);
|
popup('选择批号').vm.$emit('change', ['plan-1::250102']);
|
await wrapper.vm.$nextTick();
|
}
|
|
describe('stability analysis page', () => {
|
beforeEach(() => {
|
vi.clearAllMocks();
|
apiMocks.getOptions.mockResolvedValue({
|
data: {
|
fenlei_list: [{ code: 'CJ', id: 'category-1', key: 'CJ', name: '成品' }],
|
jihua_list: [{ code: 'STP-1', id: 'plan-1', key: 'plan-1', name: 'STP-1' }],
|
pihao_list: [
|
{
|
fangan_mingcheng: '长期稳定性方案',
|
jihua_bianhao: 'STP-1',
|
jihua_id: 'plan-1',
|
key: 'plan-1::250102',
|
name: '250102',
|
pihao: '250102',
|
},
|
],
|
xiangmu_list: [{ code: 'XM1899', id: 'item-ph', key: 'item-ph', name: 'pH值', shuju_leixing: '数值' }],
|
yangpin_list: [{ code: 'YP001', id: 'product-1', key: 'product-1', name: '脉络宁口服液' }],
|
},
|
});
|
apiMocks.getData.mockResolvedValue({
|
data: {
|
completeness: { audited_count: 1, expected_count: 3, missing_count: 1, pending_count: 1 },
|
expected_periods: [{ xiangmu_id: 'item-ph', zhouqi: '0', zhouqi_danwei: '/', zhouqi_order: 0 }],
|
rows: [
|
{
|
batch_key: 'plan-1::250102',
|
biaozhun_id: 'standard-1',
|
exception_type: '',
|
jieguo_luru_riqi: '2026-07-23',
|
jihua_bianhao: 'STP-1',
|
jihua_id: 'plan-1',
|
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: 'item-ph',
|
xiangmu_mingcheng: 'pH值',
|
yangpin_mingcheng: '脉络宁口服液',
|
zhouqi: '0',
|
zhouqi_danwei: '/',
|
zhouqi_order: 0,
|
},
|
],
|
},
|
});
|
});
|
|
it('uses a compact report layout with a dedicated result scroller', async () => {
|
const wrapper = mountPage();
|
await flushPromises();
|
|
expect(wrapper.find('.page-heading').exists()).toBe(false);
|
expect(wrapper.find('[data-scroll-container="true"]').exists()).toBe(true);
|
expect(wrapper.find('.filter-toolbar').exists()).toBe(true);
|
expect(wrapper.find('.table-section').exists()).toBe(true);
|
expect(wrapper.find('.table-section h2').text()).toBe('稳定性数据分析表');
|
expect(wrapper.find('.analysis-table').exists()).toBe(true);
|
expect(wrapper.findComponent({ name: 'ATable' }).props('dataSource')).toEqual([]);
|
expect(wrapper.find('.completeness-band').exists()).toBe(false);
|
expect(wrapper.find('.empty-stub').exists()).toBe(false);
|
});
|
|
it('keeps five equal filters and fixed-width actions on one row', () => {
|
expect(pageSource).toContain('grid-template-columns: repeat(5, minmax(0, 1fr)) 188px;');
|
expect(pageSource).toContain('width: 96px;');
|
expect(pageSource).toContain('width: 84px;');
|
expect(pageSource).not.toContain('.filter-toolbar {\n display: flex;');
|
});
|
|
it('shows only project identity and data type columns in the project popup', async () => {
|
const wrapper = mountPage();
|
await flushPromises();
|
const itemPopup = wrapper.findAllComponents(PopupStub).find((item) => item.props('popupTitle') === '选择检验项目')!;
|
const columnKeys = (itemPopup.props('columns') as Array<{ dataIndex: string }>).map((column) => column.dataIndex);
|
|
expect(columnKeys).toEqual(['sequence', 'code', 'name', 'shuju_leixing']);
|
expect(columnKeys).not.toContain('unit');
|
});
|
|
it('renders the full result table without pagination or horizontal scrolling', async () => {
|
const wrapper = mountPage();
|
await selectFilters(wrapper);
|
await wrapper.find('[data-action="search"]').trigger('click');
|
await flushPromises();
|
|
const table = wrapper.findComponent({ name: 'ATable' });
|
expect(table.props('bordered')).not.toBeUndefined();
|
expect(table.props('pagination')).toBe(false);
|
expect(table.props('scroll')).toBeUndefined();
|
expect(table.props('tableLayout')).toBe('fixed');
|
expect(wrapper.find('.export-implementation-note').exists()).toBe(false);
|
});
|
|
it('closes the bottom border of the empty table placeholder', () => {
|
expect(pageSource).toContain(':deep(.ant-table-tbody > .ant-table-placeholder > td)');
|
expect(pageSource).toContain('border-bottom: 1px solid #b9c2ce !important;');
|
});
|
|
it('renders the raw result without compliance annotations', async () => {
|
const defaultResponse = await apiMocks.getData();
|
apiMocks.getData.mockResolvedValueOnce({
|
data: {
|
completeness: { audited_count: 1, expected_count: 1, missing_count: 0, pending_count: 0 },
|
expected_periods: [],
|
rows: [
|
{
|
...defaultResponse.data.rows[0],
|
exception_type: 'non_compliant',
|
raw_result: '6.7',
|
},
|
],
|
},
|
});
|
const wrapper = mountPage();
|
await selectFilters(wrapper);
|
await wrapper.find('[data-action="search"]').trigger('click');
|
await flushPromises();
|
|
const table = wrapper.findComponent({ name: 'ATable' });
|
const resultColumn = (table.props('columns') as Array<Record<string, any>>).find((column) => column.dataIndex === 'raw_result');
|
const resultRow = (table.props('dataSource') as Array<Record<string, any>>)[0];
|
expect(resultColumn?.customRender({ record: resultRow })).toBe('6.7');
|
});
|
|
it('queries audited rows and exports the displayed rows in the browser', async () => {
|
const wrapper = mountPage();
|
await selectFilters(wrapper);
|
|
await wrapper.find('[data-action="search"]').trigger('click');
|
await flushPromises();
|
|
const expectedQuery = {
|
fenlei_bianma: 'CJ',
|
jihua_ids: ['plan-1'],
|
pihao_keys: [{ jihua_id: 'plan-1', pihao: '250102' }],
|
xiangmu_ids: ['item-ph'],
|
yangpin_id: 'product-1',
|
};
|
expect(apiMocks.getData).toHaveBeenCalledWith(expectedQuery);
|
expect(wrapper.find('.completeness-band').exists()).toBe(false);
|
expect(wrapper.find('.analysis-table').exists()).toBe(true);
|
expect(wrapper.find('.chart-block').exists()).toBe(true);
|
|
await wrapper.find('[data-action="export"]').trigger('click');
|
await flushPromises();
|
const queriedResponse = await apiMocks.getData.mock.results[0]?.value;
|
expect(exportMocks.download).toHaveBeenCalledWith(queriedResponse.data.rows);
|
});
|
|
it('keeps the last result and does not query automatically when the category changes', async () => {
|
const wrapper = mountPage();
|
await selectFilters(wrapper);
|
await wrapper.find('[data-action="search"]').trigger('click');
|
await flushPromises();
|
const queryCount = apiMocks.getData.mock.calls.length;
|
const popup = (title: string) => wrapper.findAllComponents(PopupStub).find((item) => item.props('popupTitle') === title)!;
|
|
popup('选择分类').vm.$emit('update:value', 'YL');
|
popup('选择分类').vm.$emit('change', 'YL');
|
await flushPromises();
|
|
expect(popup('选择品名').props('value')).toBeUndefined();
|
expect(popup('选择检验项目').props('value')).toEqual([]);
|
expect(popup('选择稳定性考察编号').props('value')).toEqual([]);
|
expect(popup('选择批号').props('value')).toEqual([]);
|
expect(apiMocks.getData).toHaveBeenCalledTimes(queryCount);
|
expect(wrapper.find('.analysis-table').exists()).toBe(true);
|
expect(wrapper.find('.completeness-band').exists()).toBe(false);
|
expect(wrapper.find<HTMLButtonElement>('[data-action="export"]').element.disabled).toBe(false);
|
|
await wrapper.find('[data-action="export"]').trigger('click');
|
await flushPromises();
|
const queriedResponse = await apiMocks.getData.mock.results[0]?.value;
|
expect(exportMocks.download).toHaveBeenLastCalledWith(queriedResponse.data.rows);
|
expect(apiMocks.getData).toHaveBeenCalledTimes(queryCount);
|
});
|
});
|