import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import test from 'node:test' import { computed, effectScope, nextTick, reactive, ref, watch } from 'vue' import { parse } from '@vue/compiler-sfc' import ts from 'typescript' import { buildAttachmentUploadFormData } from '../src/utils/crm-upload' const file = new URL('../src/views/m/crm/customer/CustomerAttachments.vue', import.meta.url) const { descriptor } = parse(readFileSync(file, 'utf8')) const ast = ts.createSourceFile( 'CustomerAttachments.ts', descriptor.scriptSetup!.content, ts.ScriptTarget.Latest, true ) // Run every setup statement, replacing only external imports and SFC macros. const source = ast.statements .filter((node) => !ts.isImportDeclaration(node)) .map((node) => node.getText(ast)) .join('\n') function harness() { let resolve!: (value: unknown) => void let reject!: (error: Error) => void const response = new Promise((done, fail) => { resolve = done reject = fail }) const requests: FormData[] = [] const events: unknown[][] = [] const notices: string[] = [] const previews: unknown[] = [] let exposed: { hasUploading: () => boolean } const scope = effectScope() const bindings = { computed, nextTick, reactive, ref, watch, defineProps: () => ({}), withDefaults: (props: object, defaults: object) => ({ ...defaults, ...props }), defineEmits: () => (...args: unknown[]) => events.push(args), defineExpose: (value: typeof exposed) => { exposed = value }, showToast: (message: string) => notices.push(message), showFailToast: (message: string) => notices.push(message), buildAttachmentUploadFormData, fetchUploadAttachment: (data: FormData) => { requests.push(data) return response }, // Only external URL resolution and navigation are stubbed; Vue state and FormData stay real. parseCrmAttachmentField: (value: string) => value.split(',').filter(Boolean), resolveCrmAttachmentFiles: (urls: string[]) => urls.map((url) => ({ url, name: url.split('/').pop() })), resolveCrmAttachmentUrl: (url: string) => `https://files.example/${url}`, isCrmImageFile: ({ name }: { name: string }) => name.endsWith('.png'), openCrmAttachmentPreviewInNewTab: (item: unknown) => previews.push(item), downloadCrmAttachment: () => undefined } const code = ts.transpile(source, { target: ts.ScriptTarget.ESNext }) const instance = scope.run(() => new Function( ...Object.keys(bindings), `${code}\nreturn { onPick, gridItems, fileList, uploading, onPreview }` )(...Object.values(bindings)) ) return { ...instance, resolve, reject, requests, events, notices, previews, hasUploading: () => exposed.hasUploading(), stop: () => scope.stop() } } async function startUpload(h: ReturnType) { const raw = new File(['fixture'], '客户附件.png', { type: 'image/png' }) const input = { files: [raw], value: 'selected' } const pending = h.onPick({ target: input }) await nextTick() // Prime the computed cache while transport is pending, just as the template does. assert.equal(h.gridItems.value[0].uploading, true) assert.equal(h.gridItems.value[0].url, '') assert.equal(h.uploading.value, 1) assert.equal(h.hasUploading(), true) assert.equal(input.value, '') assert.equal(h.requests.length, 1) assert.equal(h.requests[0].get('bizType'), 'crm_customer_certificate') assert.equal(h.requests[0].get('folder'), 'crm/customer') assert.equal((h.requests[0].get('file') as File).name, raw.name) return { pending } } test('pending customer upload refreshes the cached grid and enables preview on success', async () => { const h = harness() try { const { pending } = await startUpload(h) h.resolve({ data: { fileUrl: 'customer/saved.png' } }) await pending await nextTick() assert.equal(h.gridItems.value[0].uploading, false) assert.equal(h.gridItems.value[0].url, 'https://files.example/customer/saved.png') assert.equal(h.fileList.value[0].status, 'done') assert.equal(h.uploading.value, 0) assert.equal(h.hasUploading(), false) assert.equal(h.requests.length, 1) assert.deepEqual(h.events, [['update:modelValue', 'customer/saved.png']]) h.onPreview(h.gridItems.value[0]) assert.deepEqual(h.previews, [{ ossId: 'customer/saved.png', name: '客户附件.png' }]) assert.deepEqual(h.notices, []) } finally { h.stop() } }) test('rejected customer upload clears the cached grid uploading state without emitting a URL', async () => { const h = harness() try { const { pending } = await startUpload(h) h.reject(new Error('fixture upload failure')) await pending await nextTick() assert.equal(h.gridItems.value[0].uploading, false) assert.equal(h.gridItems.value[0].url, '') assert.equal(h.fileList.value[0].status, 'failed') assert.equal(h.uploading.value, 0) assert.equal(h.hasUploading(), false) assert.equal(h.requests.length, 1) assert.deepEqual(h.events, []) assert.deepEqual(h.notices, ['fixture upload failure']) h.onPreview(h.gridItems.value[0]) assert.deepEqual(h.previews, []) } finally { h.stop() } })