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)
|
})
|