fix: 物料图片上传纳入保存互斥
This commit is contained in:
parent
89368ba3f8
commit
dff5420a31
|
|
@ -26,6 +26,17 @@ vi.mock('../stores/auth', () => ({ useAuthStore: () => ({ isAdmin: true }) }))
|
||||||
vi.mock('../composables/useOrganizationSwitchGuard', () => ({
|
vi.mock('../composables/useOrganizationSwitchGuard', () => ({
|
||||||
useOrganizationSwitchGuard: vi.fn()
|
useOrganizationSwitchGuard: vi.fn()
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
// 图片上传等直接写请求走 useSavingGuard 的保存互斥;这里替身掉,避免测试环境需要 active Pinia。
|
||||||
|
const savingGuard = vi.hoisted(() => ({ runWithSaving: vi.fn(task => task()) }))
|
||||||
|
vi.mock('../composables/useSavingGuard', () => ({
|
||||||
|
useSavingGuard: () => savingGuard
|
||||||
|
}))
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// restoreAllMocks 会清掉 vi.fn 的实现,这里每个用例重新装上直通实现。
|
||||||
|
savingGuard.runWithSaving = vi.fn(task => task())
|
||||||
|
})
|
||||||
vi.mock('../api/masterData', () => ({
|
vi.mock('../api/masterData', () => ({
|
||||||
materialCategoriesApi: { list: vi.fn() },
|
materialCategoriesApi: { list: vi.fn() },
|
||||||
unitsApi: { list: vi.fn() },
|
unitsApi: { list: vi.fn() },
|
||||||
|
|
@ -556,3 +567,43 @@ describe('物料页无可用分类时的配置入口提示', () => {
|
||||||
wrapper.unmount()
|
wrapper.unmount()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('物料图片上传的保存互斥', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
materialCategoriesApi.list.mockResolvedValue({ data: categories })
|
||||||
|
unitsApi.list.mockResolvedValue({ data: [] })
|
||||||
|
codeRulesApi.next.mockReset()
|
||||||
|
codeRulesApi.next.mockResolvedValue({ data: { available: true, code: 'A010201' } })
|
||||||
|
materialsStore.createMaterial.mockReset().mockResolvedValue({ data: {} })
|
||||||
|
vi.spyOn(ElMessage, 'success').mockImplementation(() => {})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 上传绕开 axios、直接 fetch,是保存物料流程里的直接写请求。必须走保存互斥,
|
||||||
|
// 否则上传飞行期间组织 store 的 saving 仍为 false,能切公司,归属不明确。
|
||||||
|
test('带图片保存时,上传请求走保存互斥', async () => {
|
||||||
|
const wrapper = mountMaterials()
|
||||||
|
await flushPromises()
|
||||||
|
await openAddDialog(wrapper)
|
||||||
|
await pickCategories(wrapper)
|
||||||
|
setupState(wrapper).form.name = '带图物料'
|
||||||
|
setupState(wrapper).imageInput = { files: [new File(['x'], 'a.png', { type: 'image/png' })] }
|
||||||
|
|
||||||
|
const originalFetch = global.fetch
|
||||||
|
const fetchSpy = vi.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({ data: { url: '/uploads/a.png' } })
|
||||||
|
})
|
||||||
|
global.fetch = fetchSpy
|
||||||
|
|
||||||
|
try {
|
||||||
|
await setupState(wrapper).handleSave()
|
||||||
|
} finally {
|
||||||
|
global.fetch = originalFetch
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(fetchSpy).toHaveBeenCalled()
|
||||||
|
expect(materialsStore.createMaterial).toHaveBeenCalled()
|
||||||
|
expect(savingGuard.runWithSaving).toHaveBeenCalledTimes(1)
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -171,6 +171,7 @@ import { computed, ref, reactive, onMounted } from 'vue'
|
||||||
import { useMaterialsStore } from '../stores/materials'
|
import { useMaterialsStore } from '../stores/materials'
|
||||||
import { useAuthStore } from '../stores/auth'
|
import { useAuthStore } from '../stores/auth'
|
||||||
import { useOrganizationSwitchGuard } from '../composables/useOrganizationSwitchGuard'
|
import { useOrganizationSwitchGuard } from '../composables/useOrganizationSwitchGuard'
|
||||||
|
import { useSavingGuard } from '../composables/useSavingGuard'
|
||||||
import BaseTable from '../components/business/BaseTable.vue'
|
import BaseTable from '../components/business/BaseTable.vue'
|
||||||
import DeleteConfirm from '../components/business/DeleteConfirm.vue'
|
import DeleteConfirm from '../components/business/DeleteConfirm.vue'
|
||||||
import AlertTip from '../components/business/AlertTip.vue'
|
import AlertTip from '../components/business/AlertTip.vue'
|
||||||
|
|
@ -194,6 +195,9 @@ const MATERIAL_DATE_OPTIONS = {
|
||||||
|
|
||||||
const materialsStore = useMaterialsStore()
|
const materialsStore = useMaterialsStore()
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
|
// 图片上传是本页唯一直接发起的写请求(其余写请求都走 materialsStore 自身的保存互斥);
|
||||||
|
// 上传要用保存互斥包住,飞行期间禁止切换公司。
|
||||||
|
const { runWithSaving } = useSavingGuard()
|
||||||
|
|
||||||
const searchKeyword = ref('')
|
const searchKeyword = ref('')
|
||||||
const materialSearchText = ref('')
|
const materialSearchText = ref('')
|
||||||
|
|
@ -421,14 +425,16 @@ async function handleSave() {
|
||||||
uploadForm.append('image', file)
|
uploadForm.append('image', file)
|
||||||
// 这里绕开了 axios 拦截器,组织头得手工补上,否则图片上传会缺少 X-Organization-ID。
|
// 这里绕开了 axios 拦截器,组织头得手工补上,否则图片上传会缺少 X-Organization-ID。
|
||||||
const organizationId = getOrganizationId()
|
const organizationId = getOrganizationId()
|
||||||
const res = await fetch('/api/upload', {
|
// 上传是保存物料流程中的直接写请求:包进保存互斥,飞行期间禁止切换公司;
|
||||||
|
// runWithSaving 内部是 try/finally,上传失败也会复位,不会把 busy 永久卡住。
|
||||||
|
const res = await runWithSaving(() => fetch('/api/upload', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${authStore.token}`,
|
'Authorization': `Bearer ${authStore.token}`,
|
||||||
...(organizationId ? { 'X-Organization-ID': String(organizationId) } : {})
|
...(organizationId ? { 'X-Organization-ID': String(organizationId) } : {})
|
||||||
},
|
},
|
||||||
body: uploadForm
|
body: uploadForm
|
||||||
})
|
}))
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
ElMessage.error('图片上传失败')
|
ElMessage.error('图片上传失败')
|
||||||
return
|
return
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user