mobile-customer-attachments.test.ts 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. import assert from 'node:assert/strict'
  2. import { readFileSync } from 'node:fs'
  3. import test from 'node:test'
  4. import { computed, effectScope, nextTick, reactive, ref, watch } from 'vue'
  5. import { parse } from '@vue/compiler-sfc'
  6. import ts from 'typescript'
  7. import { buildAttachmentUploadFormData } from '../src/utils/crm-upload'
  8. const file = new URL('../src/views/m/crm/customer/CustomerAttachments.vue', import.meta.url)
  9. const { descriptor } = parse(readFileSync(file, 'utf8'))
  10. const ast = ts.createSourceFile(
  11. 'CustomerAttachments.ts',
  12. descriptor.scriptSetup!.content,
  13. ts.ScriptTarget.Latest,
  14. true
  15. )
  16. // Run every setup statement, replacing only external imports and SFC macros.
  17. const source = ast.statements
  18. .filter((node) => !ts.isImportDeclaration(node))
  19. .map((node) => node.getText(ast))
  20. .join('\n')
  21. function harness() {
  22. let resolve!: (value: unknown) => void
  23. let reject!: (error: Error) => void
  24. const response = new Promise((done, fail) => {
  25. resolve = done
  26. reject = fail
  27. })
  28. const requests: FormData[] = []
  29. const events: unknown[][] = []
  30. const notices: string[] = []
  31. const previews: unknown[] = []
  32. let exposed: { hasUploading: () => boolean }
  33. const scope = effectScope()
  34. const bindings = {
  35. computed,
  36. nextTick,
  37. reactive,
  38. ref,
  39. watch,
  40. defineProps: () => ({}),
  41. withDefaults: (props: object, defaults: object) => ({ ...defaults, ...props }),
  42. defineEmits:
  43. () =>
  44. (...args: unknown[]) =>
  45. events.push(args),
  46. defineExpose: (value: typeof exposed) => {
  47. exposed = value
  48. },
  49. showToast: (message: string) => notices.push(message),
  50. showFailToast: (message: string) => notices.push(message),
  51. buildAttachmentUploadFormData,
  52. fetchUploadAttachment: (data: FormData) => {
  53. requests.push(data)
  54. return response
  55. },
  56. // Only external URL resolution and navigation are stubbed; Vue state and FormData stay real.
  57. parseCrmAttachmentField: (value: string) => value.split(',').filter(Boolean),
  58. resolveCrmAttachmentFiles: (urls: string[]) =>
  59. urls.map((url) => ({ url, name: url.split('/').pop() })),
  60. resolveCrmAttachmentUrl: (url: string) => `https://files.example/${url}`,
  61. isCrmImageFile: ({ name }: { name: string }) => name.endsWith('.png'),
  62. openCrmAttachmentPreviewInNewTab: (item: unknown) => previews.push(item),
  63. downloadCrmAttachment: () => undefined
  64. }
  65. const code = ts.transpile(source, { target: ts.ScriptTarget.ESNext })
  66. const instance = scope.run(() =>
  67. new Function(
  68. ...Object.keys(bindings),
  69. `${code}\nreturn { onPick, gridItems, fileList, uploading, onPreview }`
  70. )(...Object.values(bindings))
  71. )
  72. return {
  73. ...instance,
  74. resolve,
  75. reject,
  76. requests,
  77. events,
  78. notices,
  79. previews,
  80. hasUploading: () => exposed.hasUploading(),
  81. stop: () => scope.stop()
  82. }
  83. }
  84. async function startUpload(h: ReturnType<typeof harness>) {
  85. const raw = new File(['fixture'], '客户附件.png', { type: 'image/png' })
  86. const input = { files: [raw], value: 'selected' }
  87. const pending = h.onPick({ target: input })
  88. await nextTick()
  89. // Prime the computed cache while transport is pending, just as the template does.
  90. assert.equal(h.gridItems.value[0].uploading, true)
  91. assert.equal(h.gridItems.value[0].url, '')
  92. assert.equal(h.uploading.value, 1)
  93. assert.equal(h.hasUploading(), true)
  94. assert.equal(input.value, '')
  95. assert.equal(h.requests.length, 1)
  96. assert.equal(h.requests[0].get('bizType'), 'crm_customer_certificate')
  97. assert.equal(h.requests[0].get('folder'), 'crm/customer')
  98. assert.equal((h.requests[0].get('file') as File).name, raw.name)
  99. return { pending }
  100. }
  101. test('pending customer upload refreshes the cached grid and enables preview on success', async () => {
  102. const h = harness()
  103. try {
  104. const { pending } = await startUpload(h)
  105. h.resolve({ data: { fileUrl: 'customer/saved.png' } })
  106. await pending
  107. await nextTick()
  108. assert.equal(h.gridItems.value[0].uploading, false)
  109. assert.equal(h.gridItems.value[0].url, 'https://files.example/customer/saved.png')
  110. assert.equal(h.fileList.value[0].status, 'done')
  111. assert.equal(h.uploading.value, 0)
  112. assert.equal(h.hasUploading(), false)
  113. assert.equal(h.requests.length, 1)
  114. assert.deepEqual(h.events, [['update:modelValue', 'customer/saved.png']])
  115. h.onPreview(h.gridItems.value[0])
  116. assert.deepEqual(h.previews, [{ ossId: 'customer/saved.png', name: '客户附件.png' }])
  117. assert.deepEqual(h.notices, [])
  118. } finally {
  119. h.stop()
  120. }
  121. })
  122. test('rejected customer upload clears the cached grid uploading state without emitting a URL', async () => {
  123. const h = harness()
  124. try {
  125. const { pending } = await startUpload(h)
  126. h.reject(new Error('fixture upload failure'))
  127. await pending
  128. await nextTick()
  129. assert.equal(h.gridItems.value[0].uploading, false)
  130. assert.equal(h.gridItems.value[0].url, '')
  131. assert.equal(h.fileList.value[0].status, 'failed')
  132. assert.equal(h.uploading.value, 0)
  133. assert.equal(h.hasUploading(), false)
  134. assert.equal(h.requests.length, 1)
  135. assert.deepEqual(h.events, [])
  136. assert.deepEqual(h.notices, ['fixture upload failure'])
  137. h.onPreview(h.gridItems.value[0])
  138. assert.deepEqual(h.previews, [])
  139. } finally {
  140. h.stop()
  141. }
  142. })