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()
})
})