import { defineComponent, h } from 'vue' import { flushPromises, mount } from '@vue/test-utils' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { ApiResponseError } from '@/shared/api/http' import OutsourcedBusinessPhotosEditor from './OutsourcedBusinessPhotosEditor.vue' import { outsourcedBusinessFixture } from './OutsourcedBusinessFixtures' import type { OutsourcedBusinessPhoto } from '../outsourcedBusinessTypes' const calls=vi.hoisted(()=>({upload:vi.fn(),bind:vi.fn(),command:vi.fn(),detail:vi.fn(),content:vi.fn(),actor:{user:{id:'actor'}},canUpload:true})) vi.mock('../api/outsourcedBusiness',()=>({uploadOutsourcedBusinessPhoto:calls.upload,bindOutsourcedBusinessPhoto:calls.bind,getOutsourcedBusinessCommand:calls.command,getOutsourcedBusiness:calls.detail,getOutsourcedBusinessPhotoContent:calls.content})) vi.mock('@/core/auth/auth',()=>({useAuthStore:()=>calls.actor})) vi.mock('@/core/access/accessPreview',()=>({useAccessPreviewStore:()=>({can:()=>calls.canUpload})})) vi.mock('@/core/unsaved-work/useUnsavedWorkRegistration',()=>({useUnsavedWorkRegistration:()=>({captureBaseline:vi.fn()})})) vi.mock('element-plus',()=>({ElMessage:{success:vi.fn()},ElMessageBox:{confirm:vi.fn()}})) const Frame=defineComponent({props:['modelValue','busy','beforeClose'],setup(_props,{slots}){return()=>h('div',Object.values(slots).map(slot=>slot?.()))}}) const Button=defineComponent({props:['disabled'],template:''}) const global={stubs:{OutsourcedBusinessPhotosDialog:Frame,ElButton:Button,LoadErrorState:{props:['message'],template:'
{{message}}
'},ApiErrorMeta:true}} const photo:OutsourcedBusinessPhoto={slot:'LOADING_SCENE',category:'LOADING',fileId:'9007199254740993',fileName:'loading.png',contentType:'image/png',sizeBytes:'5',uploadedAt:'2026-10-20T10:00:00Z',savedAt:null} async function choose(wrapper:ReturnType){const input=wrapper.find('input[type="file"]');Object.defineProperty(input.element,'files',{configurable:true,value:[new File(['image'],'loading.png',{type:'image/png'})]});await input.trigger('change');await flushPromises()} beforeEach(()=>{ vi.clearAllMocks();sessionStorage.clear();calls.actor.user.id='actor';calls.canUpload=true calls.upload.mockResolvedValue(photo);calls.command.mockRejectedValue(new ApiResponseError('not found','A0404',404)) const current=outsourcedBusinessFixture();current.row.recordVersion='1';current.photos=[{...photo,savedAt:'2026-10-20T10:01:00Z'}] calls.bind.mockResolvedValue(current);calls.detail.mockResolvedValue(current) }) afterEach(() => vi.unstubAllGlobals()) describe('outsourced business per-slot photo save',()=>{ it('retains prepared file and original binding request through a failed bind without retransmitting the image',async()=>{ calls.bind.mockRejectedValueOnce(new TypeError('network interrupted')) const wrapper=mount(OutsourcedBusinessPhotosEditor,{global,props:{modelValue:true,detail:outsourcedBusinessFixture()}});await choose(wrapper) expect(calls.upload).toHaveBeenCalledTimes(1);expect(calls.bind).toHaveBeenCalledTimes(1) expect(wrapper.text()).toContain('尚未确认绑定保存');expect(wrapper.emitted('saved')).toBeUndefined() const original={...calls.bind.mock.calls[0]![2]} await wrapper.findAll('button').find(button=>button.text()==='核对原结果并继续绑定')!.trigger('click');await flushPromises() expect(calls.upload).toHaveBeenCalledTimes(1);expect(calls.bind).toHaveBeenCalledTimes(2) expect(calls.bind.mock.calls[1]![2]).toEqual(original);expect(original.fileId).toBe(photo.fileId);expect(original.recordVersion).toBe('0') expect(wrapper.emitted('saved')).toHaveLength(1);wrapper.unmount() }) it('does not offer a new upload after unknown upload or fabricate a saved photo',async()=>{ calls.upload.mockRejectedValue(new TypeError('network interrupted')) const wrapper=mount(OutsourcedBusinessPhotosEditor,{global,props:{modelValue:true,detail:outsourcedBusinessFixture()}});await choose(wrapper) await wrapper.findAll('button').find(button=>button.text()==='核对原结果并继续绑定')!.trigger('click');await flushPromises() expect(calls.upload).toHaveBeenCalledTimes(1);expect(calls.bind).not.toHaveBeenCalled();expect(wrapper.emitted('saved')).toBeUndefined() expect(wrapper.find('input[type="file"]').attributes('disabled')).toBeDefined();expect(wrapper.text()).toContain('禁止重复上传');wrapper.unmount() }) it('keeps saved metadata readable without upload permission and fetches image bytes only on explicit preview',async()=>{ calls.canUpload=false const detail=outsourcedBusinessFixture();detail.photos=[{...photo,savedAt:'2026-10-20T10:01:00Z'}] const wrapper=mount(OutsourcedBusinessPhotosEditor,{global,props:{modelValue:true,detail}});await flushPromises() expect(wrapper.find('input[type="file"]').exists()).toBe(false);expect(wrapper.text()).toContain('已保存');expect(calls.content).not.toHaveBeenCalled() wrapper.unmount() }) it('reads a saved loading photo only on click and releases its URL on closing',async()=>{ const create=vi.fn(()=> 'blob:outsourced-loading'),revoke=vi.fn() vi.stubGlobal('URL',class extends URL { static createObjectURL=create; static revokeObjectURL=revoke }) calls.canUpload=false;calls.content.mockResolvedValue(new Blob(['image'],{type:'image/png'})) const detail=outsourcedBusinessFixture();detail.photos=[{...photo,savedAt:'2026-10-20T10:01:00Z'}] const wrapper=mount(OutsourcedBusinessPhotosEditor,{global,props:{modelValue:true,detail}});await flushPromises() expect(calls.content).not.toHaveBeenCalled() await wrapper.findAll('button').find(button=>button.text()==='查看图片')!.trigger('click');await flushPromises() expect(calls.content).toHaveBeenCalledExactlyOnceWith('DS-OUT-1',detail.photos[0]);expect(wrapper.find('img').attributes('src')).toBe('blob:outsourced-loading') await wrapper.setProps({modelValue:false});expect(revoke).toHaveBeenCalledWith('blob:outsourced-loading');wrapper.unmount() }) })