316 lines
9.8 KiB
JavaScript
316 lines
9.8 KiB
JavaScript
const { test } = require('node:test')
|
|
const assert = require('node:assert/strict')
|
|
const fs = require('node:fs')
|
|
const path = require('node:path')
|
|
const vm = require('node:vm')
|
|
const ts = require('typescript')
|
|
const { parse, compileTemplate } = require('vue/compiler-sfc')
|
|
const { ref, computed } = require('vue')
|
|
|
|
const source = fs.readFileSync(
|
|
path.join(__dirname, '../src/views/member/user/components/UserExportDialog.vue'),
|
|
'utf8'
|
|
)
|
|
const { descriptor } = parse(source)
|
|
const script = ts.transpileModule(descriptor.scriptSetup.content, {
|
|
compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS }
|
|
}).outputText
|
|
|
|
const waiting = (overrides = {}) => ({
|
|
taskId: 'task-1',
|
|
status: 'WAITING',
|
|
total: 0,
|
|
processed: 0,
|
|
progress: 0,
|
|
message: '等待处理',
|
|
fileName: '会员用户.xlsx',
|
|
existingTask: false,
|
|
retentionHours: 24,
|
|
...overrides
|
|
})
|
|
const finished = () => waiting({ status: 'SUCCESS', total: 3, processed: 3, progress: 100 })
|
|
const flush = async () => {
|
|
for (let index = 0; index < 8; index++) await Promise.resolve()
|
|
}
|
|
|
|
// 执行真实组件的 setup 脚本,以 Vue 响应式状态、可控计时器和 API 替身验证异步交互。
|
|
function createHarness(overrides = {}, savedTask) {
|
|
const lifecycle = {}
|
|
const timers = new Map()
|
|
const calls = { exports: [], polls: [], downloads: [], files: [] }
|
|
const storage = new Map(savedTask ? [['member-user-export:1:100', savedTask]] : [])
|
|
const api = {
|
|
exportUsers: async (params) => {
|
|
calls.exports.push(params)
|
|
return waiting()
|
|
},
|
|
getUserExportProgress: async (taskId) => {
|
|
calls.polls.push(taskId)
|
|
return waiting({ status: 'RUNNING' })
|
|
},
|
|
downloadUserExport: async (taskId) => {
|
|
calls.downloads.push(taskId)
|
|
return new Blob(['excel'])
|
|
},
|
|
...overrides
|
|
}
|
|
const context = {
|
|
exports: {},
|
|
ref,
|
|
computed,
|
|
Blob,
|
|
Object,
|
|
onMounted: (fn) => {
|
|
lifecycle.mounted = fn
|
|
},
|
|
onActivated: (fn) => {
|
|
lifecycle.activated = fn
|
|
},
|
|
onDeactivated: (fn) => {
|
|
lifecycle.deactivated = fn
|
|
},
|
|
onBeforeUnmount: (fn) => {
|
|
lifecycle.unmounted = fn
|
|
},
|
|
setTimeout: (fn, delay) => {
|
|
const id = Symbol()
|
|
timers.set(id, { fn, delay })
|
|
return id
|
|
},
|
|
clearTimeout: (id) => timers.delete(id),
|
|
sessionStorage: {
|
|
getItem: (key) => storage.get(key),
|
|
setItem: (key, value) => storage.set(key, value),
|
|
removeItem: (key) => storage.delete(key)
|
|
},
|
|
defineExpose: (exposed) => {
|
|
context.exposed = exposed
|
|
},
|
|
require: (name) => {
|
|
if (name === '@/api/member/user') return api
|
|
if (name === '@/utils/download')
|
|
return { default: { excel: (...args) => calls.files.push(args) } }
|
|
if (name === '@/utils/auth') return { getTenantId: () => '1' }
|
|
if (name === '@/store/modules/user') return { useUserStore: () => ({ getUser: { id: 100 } }) }
|
|
throw new Error(`Unexpected import: ${name}`)
|
|
}
|
|
}
|
|
vm.runInNewContext(
|
|
script +
|
|
'\nglobalThis.state = { task, errorMessage, pollFailed, existingTask, visible, downloadFile, startPolling, submitExport };',
|
|
context
|
|
)
|
|
return {
|
|
...context,
|
|
api,
|
|
calls,
|
|
lifecycle,
|
|
timers,
|
|
storage,
|
|
tick: async () => {
|
|
const next = timers.entries().next().value
|
|
assert.ok(next, '应存在下一次轮询')
|
|
timers.delete(next[0])
|
|
await next[1].fn()
|
|
await flush()
|
|
}
|
|
}
|
|
}
|
|
|
|
test('进度弹窗模板可编译', () => {
|
|
assert.deepEqual(
|
|
compileTemplate({
|
|
source: descriptor.template.content,
|
|
filename: 'UserExportDialog.vue',
|
|
id: 'export'
|
|
}).errors,
|
|
[]
|
|
)
|
|
})
|
|
|
|
test('提交表单全部条件的快照,忽略分页,保留零值和男女页面条件,并阻止重复点击', async () => {
|
|
let resolveSubmit
|
|
let captured
|
|
const h = createHarness({
|
|
exportUsers: (params) => {
|
|
captured = params
|
|
return new Promise((resolve) => {
|
|
resolveSubmit = resolve
|
|
})
|
|
}
|
|
})
|
|
const params = {
|
|
pageNo: 50,
|
|
pageSize: 10,
|
|
nickname: '会员',
|
|
userCode: '000001',
|
|
mobile: '139',
|
|
wechatNum: 'wx_test',
|
|
addressAreaId: 330100,
|
|
sex: 2,
|
|
status: 0,
|
|
memberRecentTagList: ['4', '5'],
|
|
createTime: ['2026-10-01 00:00:00', '2026-10-30 23:59:59'],
|
|
loginDate: ['2026-10-20 00:00:00', '2026-10-30 23:59:59'],
|
|
levelId: null,
|
|
groupId: null
|
|
}
|
|
const pending = h.exposed.open(params)
|
|
await h.exposed.open(params)
|
|
params.memberRecentTagList.push('6')
|
|
params.createTime[0] = 'changed'
|
|
assert.equal(captured.pageNo, undefined)
|
|
assert.equal(captured.pageSize, undefined)
|
|
assert.equal(captured.sex, 2)
|
|
assert.equal(captured.status, 0)
|
|
assert.deepEqual(Array.from(captured.memberRecentTagList), ['4', '5'])
|
|
assert.equal(captured.createTime[0], '2026-10-01 00:00:00')
|
|
assert.deepEqual(
|
|
Object.keys(captured).sort(),
|
|
Object.keys(params)
|
|
.filter((key) => !['pageNo', 'pageSize'].includes(key))
|
|
.sort()
|
|
)
|
|
resolveSubmit(waiting())
|
|
await pending
|
|
await flush()
|
|
assert.equal(h.exposed.isActive.value, true)
|
|
assert.equal(h.timers.size, 1)
|
|
assert.equal([...h.timers.values()][0].delay, 2000)
|
|
assert.equal(h.storage.get('member-user-export:1:100'), 'task-1')
|
|
})
|
|
|
|
test('完成后停止轮询并下载一次,允许手动再次下载', async () => {
|
|
const h = createHarness()
|
|
await h.exposed.open({ sex: 1 })
|
|
await flush()
|
|
h.api.getUserExportProgress = async () => finished()
|
|
await h.tick()
|
|
assert.equal(h.state.task.value.progress, 100)
|
|
assert.equal(h.exposed.isActive.value, false)
|
|
assert.equal(h.timers.size, 0)
|
|
assert.deepEqual(h.calls.downloads, ['task-1'])
|
|
assert.equal(h.calls.files.length, 1)
|
|
assert.equal(h.storage.size, 0)
|
|
await h.state.downloadFile()
|
|
assert.equal(h.calls.files.length, 2)
|
|
})
|
|
|
|
test('查询失败时保留任务并提供重试,不重复创建导出', async () => {
|
|
const h = createHarness({
|
|
getUserExportProgress: async () => {
|
|
throw new Error('network')
|
|
}
|
|
})
|
|
await h.exposed.open({ sex: 2 })
|
|
await flush()
|
|
assert.equal(h.state.pollFailed.value, true)
|
|
assert.equal(h.timers.size, 0)
|
|
h.api.getUserExportProgress = async () =>
|
|
waiting({ status: 'RUNNING', total: 10, processed: 5, progress: 50 })
|
|
h.state.startPolling()
|
|
await flush()
|
|
assert.equal(h.calls.exports.length, 1)
|
|
assert.equal(h.state.task.value.progress, 50)
|
|
assert.equal(h.state.pollFailed.value, false)
|
|
assert.equal(h.timers.size, 1)
|
|
})
|
|
|
|
test('任务失败停止轮询且不下载,可用原条件重试', async () => {
|
|
const h = createHarness({
|
|
getUserExportProgress: async () => waiting({ status: 'FAILURE', message: '导出失败' })
|
|
})
|
|
await h.exposed.open({ sex: 2, addressAreaId: 330100 })
|
|
await flush()
|
|
assert.equal(h.exposed.isActive.value, false)
|
|
assert.equal(h.timers.size, 0)
|
|
assert.equal(h.calls.downloads.length, 0)
|
|
await h.state.submitExport()
|
|
await flush()
|
|
assert.equal(h.calls.exports.length, 2)
|
|
assert.equal(h.calls.exports[1].addressAreaId, 330100)
|
|
})
|
|
|
|
test('KeepAlive 离开页面后忽略过时响应,返回时恢复轮询', async () => {
|
|
let resolvePoll
|
|
const h = createHarness({
|
|
getUserExportProgress: () =>
|
|
new Promise((resolve) => {
|
|
resolvePoll = resolve
|
|
})
|
|
})
|
|
await h.exposed.open({ sex: 1 })
|
|
h.lifecycle.deactivated()
|
|
resolvePoll(finished())
|
|
await flush()
|
|
assert.equal(h.state.task.value.status, 'WAITING')
|
|
assert.equal(h.calls.downloads.length, 0)
|
|
assert.equal(h.timers.size, 0)
|
|
h.api.getUserExportProgress = async () => finished()
|
|
h.lifecycle.activated()
|
|
await flush()
|
|
assert.equal(h.state.task.value.status, 'SUCCESS')
|
|
assert.equal(h.calls.downloads.length, 1)
|
|
h.lifecycle.unmounted()
|
|
assert.equal(h.timers.size, 0)
|
|
})
|
|
|
|
test('刷新后按用户和租户恢复任务,完成后下载并清除缓存', async () => {
|
|
const h = createHarness({ getUserExportProgress: async () => finished() }, 'task-1')
|
|
h.lifecycle.mounted()
|
|
await flush()
|
|
assert.equal(h.calls.exports.length, 0)
|
|
assert.equal(h.state.task.value.status, 'SUCCESS')
|
|
assert.equal(h.calls.downloads.length, 1)
|
|
assert.equal(h.storage.size, 0)
|
|
})
|
|
|
|
test('下载失败保持成功状态并打开弹窗,重试只下载文件', async () => {
|
|
const h = createHarness({
|
|
getUserExportProgress: async () => finished(),
|
|
downloadUserExport: async () => {
|
|
throw new Error('download interrupted')
|
|
}
|
|
})
|
|
await h.exposed.open({ status: 0 })
|
|
await flush()
|
|
assert.equal(h.state.task.value.status, 'SUCCESS')
|
|
assert.match(h.state.errorMessage.value, /下载失败/)
|
|
assert.equal(h.state.visible.value, true)
|
|
h.api.downloadUserExport = async () => new Blob(['excel'])
|
|
await h.state.downloadFile()
|
|
assert.equal(h.state.errorMessage.value, '')
|
|
assert.equal(h.calls.exports.length, 1)
|
|
assert.equal(h.calls.files.length, 1)
|
|
})
|
|
|
|
test('复用已有任务的提示在后续进度轮询后仍然保留', async () => {
|
|
const h = createHarness({ exportUsers: async () => waiting({ existingTask: true }) })
|
|
await h.exposed.open({ sex: 2 })
|
|
await flush()
|
|
assert.equal(h.state.existingTask.value, true)
|
|
await h.tick()
|
|
assert.equal(h.state.existingTask.value, true)
|
|
})
|
|
|
|
test('开始另一个任务不会改变正在下载的文件名', async () => {
|
|
let resolveDownload
|
|
const h = createHarness({
|
|
getUserExportProgress: async () => finished(),
|
|
downloadUserExport: () =>
|
|
new Promise((resolve) => {
|
|
resolveDownload = resolve
|
|
})
|
|
})
|
|
await h.exposed.open({ sex: 1 })
|
|
await flush()
|
|
h.api.exportUsers = async () => waiting({ taskId: 'task-2', fileName: '新的导出.xlsx' })
|
|
h.api.getUserExportProgress = async () => waiting({ taskId: 'task-2' })
|
|
await h.exposed.open({ sex: 2 })
|
|
await flush()
|
|
resolveDownload(new Blob(['first export']))
|
|
await flush()
|
|
assert.equal(h.calls.files[0][1], '会员用户.xlsx')
|
|
assert.equal(h.state.task.value.taskId, 'task-2')
|
|
})
|