148 lines
5.2 KiB
TypeScript
148 lines
5.2 KiB
TypeScript
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<typeof harness>) {
|
|
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()
|
|
}
|
|
})
|