212 lines
7.1 KiB
JavaScript
212 lines
7.1 KiB
JavaScript
import { flushPromises, mount } from '@vue/test-utils'
|
||
import ElementPlus, { ElMessage } from 'element-plus'
|
||
import { beforeEach, describe, expect, test, vi } from 'vitest'
|
||
import Materials from './Materials.vue'
|
||
import { codeRulesApi, materialCategoriesApi, unitsApi } from '../api/masterData'
|
||
|
||
const materialsStore = vi.hoisted(() => ({
|
||
materials: [],
|
||
materialOptions: [],
|
||
loading: false,
|
||
pagination: { currentPage: 1, pageSize: 20, total: 0, totalPages: 0 },
|
||
fetchMaterials: vi.fn(),
|
||
fetchMaterialOptions: vi.fn(),
|
||
createMaterial: vi.fn(),
|
||
updateMaterial: vi.fn(),
|
||
obsoleteMaterial: vi.fn(),
|
||
restoreMaterial: vi.fn(),
|
||
getCategoryDisplay: vi.fn(() => '-')
|
||
}))
|
||
|
||
vi.mock('../stores/materials', () => ({ useMaterialsStore: () => materialsStore }))
|
||
vi.mock('../stores/auth', () => ({ useAuthStore: () => ({ isAdmin: true }) }))
|
||
vi.mock('../api/masterData', () => ({
|
||
materialCategoriesApi: { list: vi.fn() },
|
||
unitsApi: { list: vi.fn() },
|
||
codeRulesApi: { next: vi.fn() }
|
||
}))
|
||
|
||
// 真实的 ElSelect 在 jsdom 下会因自身更新报 "Maximum recursive updates exceeded",
|
||
// 这里用替身直接派发 change,模拟用户选中某个分类。
|
||
const SelectStub = {
|
||
props: ['modelValue', 'placeholder', 'filterable', 'filterMethod', 'noDataText', 'disabled', 'clearable'],
|
||
emits: ['update:modelValue', 'change'],
|
||
methods: {
|
||
pick(value) {
|
||
this.$emit('update:modelValue', value)
|
||
this.$emit('change', value)
|
||
}
|
||
},
|
||
template: '<div class="select-stub"></div>'
|
||
}
|
||
|
||
const OptionStub = { props: ['label', 'value'], template: '<span />' }
|
||
|
||
const categories = [
|
||
{ id: 1, code: 'A', name: '机械结构类', parent_id: null, status: 'active' },
|
||
{ id: 12, code: 'A01', name: '标准件', parent_id: 1, status: 'active' },
|
||
{ id: 13, code: 'A02', name: '成品件', parent_id: 1, status: 'active' }
|
||
]
|
||
|
||
function mountMaterials() {
|
||
return mount(Materials, {
|
||
global: {
|
||
plugins: [ElementPlus],
|
||
stubs: { teleport: true, ElSelect: SelectStub, ElOption: OptionStub }
|
||
}
|
||
})
|
||
}
|
||
|
||
function findButton(wrapper, text) {
|
||
return wrapper.findAll('button').find(button => button.text().includes(text))
|
||
}
|
||
|
||
// 页面上的物料搜索框也用了 ElSelect,弹窗里的三个依次是大类、小类、单位。
|
||
function formSelects(wrapper) {
|
||
const selects = wrapper.findAllComponents(SelectStub)
|
||
return { major: selects[1], minor: selects[2], unit: selects[3] }
|
||
}
|
||
|
||
function codeInput(wrapper) {
|
||
return wrapper.find('.material-code-field input')
|
||
}
|
||
|
||
async function openAddDialog(wrapper) {
|
||
await findButton(wrapper, '新增物料').trigger('click')
|
||
await flushPromises()
|
||
}
|
||
|
||
async function pickCategories(wrapper, { major = 1, minor = 12 } = {}) {
|
||
await formSelects(wrapper).major.vm.pick(major)
|
||
await formSelects(wrapper).minor.vm.pick(minor)
|
||
await flushPromises()
|
||
}
|
||
|
||
describe('新增物料时的编码联动', () => {
|
||
let warnSpy
|
||
|
||
beforeEach(() => {
|
||
materialCategoriesApi.list.mockResolvedValue({ data: categories })
|
||
unitsApi.list.mockResolvedValue({ data: [] })
|
||
codeRulesApi.next.mockReset()
|
||
materialsStore.createMaterial.mockReset()
|
||
materialsStore.updateMaterial.mockReset()
|
||
warnSpy = vi.spyOn(ElMessage, 'warning').mockImplementation(() => {})
|
||
})
|
||
|
||
test('还没选小类时,编码框提示先选分类', async () => {
|
||
codeRulesApi.next.mockResolvedValue({ data: { available: true, code: 'A010201' } })
|
||
|
||
const wrapper = mountMaterials()
|
||
await flushPromises()
|
||
await openAddDialog(wrapper)
|
||
|
||
expect(codeInput(wrapper).attributes('placeholder')).toBe('请先选择大类类型与小类类型')
|
||
|
||
await pickCategories(wrapper)
|
||
|
||
expect(codeInput(wrapper).attributes('placeholder')).toBe('')
|
||
wrapper.unmount()
|
||
})
|
||
|
||
test('选完小类后自动填入按规则算出的编码', async () => {
|
||
codeRulesApi.next.mockResolvedValue({ data: { available: true, code: 'A010201' } })
|
||
|
||
const wrapper = mountMaterials()
|
||
await flushPromises()
|
||
await openAddDialog(wrapper)
|
||
await pickCategories(wrapper)
|
||
|
||
expect(codeRulesApi.next).toHaveBeenCalledWith(12)
|
||
expect(codeInput(wrapper).element.value).toBe('A010201')
|
||
expect(wrapper.find('.material-code-hint').exists()).toBe(false)
|
||
wrapper.unmount()
|
||
})
|
||
|
||
test('小类没有规则时清空编码并提示手工填写', async () => {
|
||
codeRulesApi.next.mockResolvedValue({ data: { available: false, code: '' } })
|
||
|
||
const wrapper = mountMaterials()
|
||
await flushPromises()
|
||
await openAddDialog(wrapper)
|
||
await pickCategories(wrapper)
|
||
|
||
expect(codeInput(wrapper).element.value).toBe('')
|
||
expect(wrapper.find('.material-code-hint').text()).toContain('请手工填写')
|
||
wrapper.unmount()
|
||
})
|
||
|
||
test('没手改过编码时,换小类会按新分类的规则覆盖', async () => {
|
||
codeRulesApi.next
|
||
.mockResolvedValueOnce({ data: { available: true, code: 'A010201' } })
|
||
.mockResolvedValueOnce({ data: { available: true, code: 'A020202' } })
|
||
|
||
const wrapper = mountMaterials()
|
||
await flushPromises()
|
||
await openAddDialog(wrapper)
|
||
await pickCategories(wrapper)
|
||
expect(codeInput(wrapper).element.value).toBe('A010201')
|
||
|
||
await formSelects(wrapper).minor.vm.pick(13)
|
||
await flushPromises()
|
||
|
||
expect(codeRulesApi.next).toHaveBeenLastCalledWith(13)
|
||
expect(codeInput(wrapper).element.value).toBe('A020202')
|
||
wrapper.unmount()
|
||
})
|
||
|
||
test('手改过编码后,换小类保留用户填的值', async () => {
|
||
codeRulesApi.next.mockResolvedValue({ data: { available: true, code: 'A010201' } })
|
||
|
||
const wrapper = mountMaterials()
|
||
await flushPromises()
|
||
await openAddDialog(wrapper)
|
||
await pickCategories(wrapper)
|
||
await codeInput(wrapper).setValue('MY-001')
|
||
await flushPromises()
|
||
|
||
await formSelects(wrapper).minor.vm.pick(13)
|
||
await flushPromises()
|
||
|
||
expect(codeInput(wrapper).element.value).toBe('MY-001')
|
||
// 手改过就不再白白请求一次预览接口
|
||
expect(codeRulesApi.next).toHaveBeenCalledTimes(1)
|
||
wrapper.unmount()
|
||
})
|
||
|
||
test('点「重新生成」把编码交回当前分类的规则', async () => {
|
||
codeRulesApi.next
|
||
.mockResolvedValueOnce({ data: { available: true, code: 'A010201' } })
|
||
.mockResolvedValueOnce({ data: { available: true, code: 'A010299' } })
|
||
|
||
const wrapper = mountMaterials()
|
||
await flushPromises()
|
||
await openAddDialog(wrapper)
|
||
await pickCategories(wrapper)
|
||
await codeInput(wrapper).setValue('MY-001')
|
||
await flushPromises()
|
||
|
||
await findButton(wrapper, '重新生成').trigger('click')
|
||
await flushPromises()
|
||
|
||
expect(codeInput(wrapper).element.value).toBe('A010299')
|
||
wrapper.unmount()
|
||
})
|
||
|
||
test('小类没有规则且没填编码时,保存被拦下并提示补编码', async () => {
|
||
codeRulesApi.next.mockResolvedValue({ data: { available: false, code: '' } })
|
||
|
||
const wrapper = mountMaterials()
|
||
await flushPromises()
|
||
await openAddDialog(wrapper)
|
||
await pickCategories(wrapper)
|
||
|
||
await findButton(wrapper, '保存').trigger('click')
|
||
await flushPromises()
|
||
|
||
expect(materialsStore.createMaterial).not.toHaveBeenCalled()
|
||
expect(warnSpy).toHaveBeenCalledWith('请填写物料编码')
|
||
wrapper.unmount()
|
||
})
|
||
})
|