import { flushPromises, mount } from '@vue/test-utils' import ElementPlus, { ElMessage } from 'element-plus' import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import Stock from './Stock.vue' const stockStore = vi.hoisted(() => ({ stockList: [], warningList: [], logs: [], loading: false, stockPage: { currentPage: 1, pageSize: 20, total: 0 }, warningPage: { currentPage: 1, pageSize: 20, total: 0 }, logsPage: { currentPage: 1, pageSize: 20, total: 0 }, fetchStock: vi.fn(), fetchStockLogs: vi.fn(), fetchStockWarning: vi.fn(), updateWarningStock: vi.fn() })) const materialsStore = vi.hoisted(() => ({ materialOptions: [], historicalMaterialOptions: [], fetchMaterialOptions: vi.fn(), fetchHistoricalMaterialOptions: vi.fn() })) vi.mock('../stores/stock', () => ({ useStockStore: () => stockStore })) vi.mock('../stores/materials', () => ({ useMaterialsStore: () => materialsStore })) vi.mock('../stores/auth', () => ({ useAuthStore: () => ({ isAdmin: true }) })) vi.mock('../api/stock', () => ({ stockApi: { getInventoryBalanceDetails: vi.fn().mockResolvedValue({ data: [] }) } })) vi.mock('../api/masterData', () => ({ materialCategoriesApi: { list: vi.fn().mockResolvedValue({ data: [] }) } })) function findButton(wrapper, text) { return wrapper.findAll('button').find(button => button.text().includes(text)) } const TabPaneStub = { template: '
' } function mountStock() { return mount(Stock, { global: { plugins: [ElementPlus], // WarehouseSettings 及其依赖与本用例无关,直接替身掉; // ElTabPane 在 jsdom 下会让表格列渲染不出来,用直通替身绕开。 stubs: { teleport: true, WarehouseSettings: true, ElTabPane: TabPaneStub } } }) } describe('库存预警阈值校验', () => { let warnSpy beforeEach(() => { warnSpy = vi.spyOn(ElMessage, 'warning').mockImplementation(() => {}) stockStore.stockList = [{ material_id: 3, material_code: 'A010203', name: '测试物料', total_on_hand: '10.000000', warning_stock: '5.000000', warning_enabled: true, warning_configured: true, stock_status: 'normal' }] stockStore.fetchStock.mockReset() stockStore.fetchStockLogs.mockReset() stockStore.fetchStockWarning.mockReset() stockStore.updateWarningStock.mockReset().mockResolvedValue({ data: {} }) materialsStore.fetchMaterialOptions.mockReset() materialsStore.fetchHistoricalMaterialOptions.mockReset() }) afterEach(() => { // 只恢复 ElMessage 的 spy:restoreAllMocks 会把 vi.mock 工厂里的实现一并清掉, // 导致下一个用例中 api mock 返回 undefined。 warnSpy.mockRestore() }) test('阈值被清空时拦下,不提交 null', async () => { const wrapper = mountStock() await flushPromises() // el-table 的行内容在 jsdom 下渲染不出来(列宽依赖真实布局计算), // 于是跳过表格这一层,直接触发组件的打开逻辑。 wrapper.vm.$.setupState.openWarningDialog(stockStore.stockList[0]) await flushPromises() // el-input-number 清空后会向 v-model 写 null const threshold = wrapper.find('.el-input-number input') await threshold.setValue('') await threshold.trigger('blur') await flushPromises() await findButton(wrapper, '保存').trigger('click') await flushPromises() expect(stockStore.updateWarningStock).not.toHaveBeenCalled() expect(warnSpy).toHaveBeenCalledWith('请填写预警阈值') wrapper.unmount() }) test('阈值为 0 是合法配置,可以提交', async () => { const wrapper = mountStock() await flushPromises() wrapper.vm.$.setupState.openWarningDialog(stockStore.stockList[0]) await flushPromises() const threshold = wrapper.find('.el-input-number input') await threshold.setValue('0') await threshold.trigger('blur') await flushPromises() await findButton(wrapper, '保存').trigger('click') await flushPromises() expect(warnSpy).not.toHaveBeenCalled() expect(stockStore.updateWarningStock).toHaveBeenCalledTimes(1) wrapper.unmount() }) })