1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
import test from 'node:test'
import assert from 'node:assert/strict'
import fs from 'node:fs'
import vm from 'node:vm'
import { parse as parseVue } from '@vue/compiler-sfc'
import { parse as parseJs } from 'acorn'
 
const backend = 'https://lims.example/dev-api'
const read = path => fs.readFileSync(new URL(path, import.meta.url), 'utf8')
 
function withoutImports(source) {
  const imports = parseJs(source, { sourceType: 'module', ecmaVersion: 'latest' }).body
    .filter(node => node.type === 'ImportDeclaration')
  for (const node of imports.reverse()) source = source.slice(0, node.start) + source.slice(node.end)
  return source
}
 
function downloader() {
  const saved = [], requests = [], errors = []
  const state = { data: new Blob(['original bytes']), error: null, missingUrls: [] }
  const context = vm.createContext({
    URL, Blob, window: { location: { origin: 'https://lims.example' } },
    getJavaApi: () => backend, getToken: () => 'test-token',
    saveAs: (...args) => saved.push(args),
    ElMessage: { error: message => errors.push(message) },
    axios: { get: async (url, options) => {
      requests.push({ url, ...options })
      if (state.missingUrls.includes(url)) throw { response: { status: 404 } }
      if (state.error) throw state.error
      return { data: state.data }
    } },
  })
  const helper = withoutImports(read('../src/utils/attachment.js')).replace(/^export /mg, '')
  vm.runInContext(helper, context)
  const plugin = withoutImports(read('../src/plugins/download.js'))
    .replace('import.meta.env.VITE_APP_BASE_API', "'/dev-api'")
    .replace('export default', 'globalThis.download =')
  vm.runInContext(plugin, context)
  return { context, saved, requests, errors, state, download: context.download }
}
 
test('文件名补充文档/图片目录,并优先遵循附件的存储类型', () => {
  const { context: c } = downloader()
  for (const [file, type, directory] of [
    ['report.docx', undefined, 'word'], ['report.pdf', 2, 'word'],
    ['photo.PNG', undefined, 'img'], ['photo.webp', undefined, 'img'],
    ['scan.bin', 1, 'img'], ['photo.png', 2, 'word'], ['photo.bmp', 0, 'img'],
  ]) assert.equal(c.resolveAttachmentUrl(file, type), `${backend}/${directory}/${file}`)
})
 
test('已有静态路径与代理前缀不会重复拼接', () => {
  const { context: c } = downloader()
  for (const path of ['word/a.docx', '/word/a.docx', '/dev-api/word/a.docx', `${backend}/word/a.docx`]) {
    assert.equal(c.resolveAttachmentUrl(path), `${backend}/word/a.docx`)
  }
  assert.equal(c.resolveAttachmentUrl('/profile/upload/2026/a.pdf'), `${backend}/profile/upload/2026/a.pdf`)
  assert.equal(c.resolveAttachmentUrl('word\\folder\\a.pdf'), `${backend}/word/folder/a.pdf`)
})
 
test('中文、空格、#、?、百分号和已有编码均按真实文件名下载', () => {
  const { context: c } = downloader()
  for (const name of ['委托附件 #1?.docx', '浓度100%.pdf', 'a+b&c.pdf']) {
    const expected = `${backend}/word/${encodeURIComponent(name)}`
    assert.equal(c.resolveAttachmentUrl(name), expected)
    assert.equal(c.resolveAttachmentUrl(expected), expected)
  }
  assert.equal(c.resolveAttachmentUrl('/word/folder/a%20b.pdf'), `${backend}/word/folder/a%20b.pdf`)
})
 
test('保留外部地址查询参数,拒绝空地址及无效协议', () => {
  const { context: c } = downloader()
  const external = 'https://files.example/a.pdf?signature=abc#page=1'
  assert.equal(c.resolveAttachmentUrl(external), external)
  for (const value of [null, '', ' ', 'javascript:alert(1)']) assert.throws(() => c.resolveAttachmentUrl(value))
})
 
test('下载名保留原始扩展名,无名称时使用存储文件名', () => {
  const { context: c } = downloader()
  assert.equal(c.attachmentFileName(`${backend}/word/test.pdf`, '检验附件'), '检验附件.pdf')
  assert.equal(c.attachmentFileName(`${backend}/word/test.docx`, '原始记录.docx'), '原始记录.docx')
  assert.equal(c.attachmentFileName(`${backend}/word/a%20b.pdf`), 'a b.pdf')
})
 
test('图片和文档下载原始 Blob,并仅向业务服务器发送登录凭据', async () => {
  const { download, saved, requests, state } = downloader()
  assert.equal(await download.attachment({ fileUrl: '原图.jpg', type: 1 }, '原图.jpg'), true)
  assert.equal(saved[0][0], state.data)
  assert.equal(saved[0][1], '原图.jpg')
  assert.equal(requests[0].url, `${backend}/img/${encodeURIComponent('原图.jpg')}`)
  assert.equal(requests[0].responseType, 'blob')
  assert.equal(requests[0].headers.Authorization, 'Bearer test-token')
  await download.saveAs('https://files.example/a.docx?signature=abc', '附件.docx')
  assert.equal(requests.length, 1)
  assert.equal(saved[1][0], 'https://files.example/a.docx?signature=abc')
})
 
test('接口错误与错误网页不保存成附件,合法 JSON/HTML 附件可正常下载', async () => {
  const { download, saved, errors, state } = downloader()
  state.data = new Blob(['{"code":500,"msg":"附件不存在"}'], { type: 'application/json' })
  assert.equal(await download.saveAs('missing.docx', 'missing.docx'), false)
  assert.equal(errors.pop(), '附件不存在')
  state.data = new Blob(['<!doctype html><html>Not found</html>'], { type: 'text/html' })
  assert.equal(await download.saveAs('missing.pdf'), false)
  assert.equal(saved.length, 0)
  state.data = new Blob(['{"hello":"world"}'], { type: 'application/json' })
  assert.equal(await download.saveAs('data.json'), true)
  state.data = new Blob(['<!doctype html><html>Document</html>'], { type: 'text/html' })
  assert.equal(await download.saveAs('document.html'), true)
})
 
test('404、登录失效及空文件地址提示失败,不生成下载文件', async () => {
  const { download, saved, errors, requests, state } = downloader()
  for (const status of [404, 401, 403]) {
    state.error = { response: { status } }
    assert.equal(await download.saveAs('a.pdf'), false)
  }
  const count = requests.length
  assert.equal(await download.attachment({ fileUrl: null }), false)
  assert.equal(requests.length, count)
  assert.equal(saved.length, 0)
  assert.equal(errors.length, 4)
})
 
test('现有导出接口传入 Blob 时直接保存,不额外发起请求', () => {
  const { download, saved, requests } = downloader()
  const data = new Blob(['exported bytes'])
  download.saveAs(data, '导出.xlsx')
  assert.equal(saved[0][0], data)
  assert.equal(saved[0][1], '导出.xlsx')
  assert.equal(requests.length, 0)
})
 
test('兼容历史图片保存目录差异,明确指定存储类型的附件不猜测其他目录', async () => {
  const { download, saved, requests, state } = downloader()
  state.missingUrls = [`${backend}/img/scan.bmp`]
  assert.equal(await download.saveAs('scan.bmp', '原扫描件.bmp'), true)
  assert.equal(requests[1].url, `${backend}/word/scan.bmp`)
  assert.equal(saved[0][1], '原扫描件.bmp')
  const count = requests.length
  assert.equal(await download.attachment({ fileUrl: 'scan.bmp', type: 1 }), false)
  assert.equal(requests.length, count + 1)
})
 
function orderComponent(getFileList) {
  const { descriptor } = parseVue(read('../src/views/business/productOrder/index.vue'))
  let source = descriptor.script.content
  const ast = parseJs(source, { sourceType: 'module', ecmaVersion: 'latest' })
  const edits = ast.body.filter(n => n.type === 'ImportDeclaration' || n.type === 'ExportDefaultDeclaration')
    .map(n => n.type === 'ImportDeclaration' ? { start: n.start, end: n.end, text: '' }
      : { start: n.start, end: n.declaration.start, text: 'globalThis.component = ' })
  for (const edit of edits.sort((a, b) => b.start - a.start)) source = source.slice(0, edit.start) + edit.text + source.slice(edit.end)
  const context = vm.createContext({ vueQr: {}, PrintDialog: {}, ReportPreview: {}, limsTable: {}, mapGetters: () => ({}), getFileList })
  vm.runInContext(source, context)
  return { ...context.component.data(), ...context.component.methods }
}
 
test('下单附件翻页保留委托ID,切换委托时重置页码和旧列表', async () => {
  const requests = []
  const instance = orderComponent(params => {
    requests.push(params)
    return Promise.resolve({ code: 200, data: { records: [], total: 30 } })
  })
  instance.handleFileLook({ id: 123 })
  instance.paginationFile({ page: 2, limit: 10 })
  assert.equal(requests[1].insOrderId, 123)
  assert.equal(requests[1].current, 2)
  assert.equal(requests[1].size, 10)
  instance.handleFileLook({ id: 456 })
  assert.equal(requests[2].insOrderId, 456)
  assert.equal(requests[2].current, 1)
  assert.equal(instance.tableDataFile.length, 0)
})