Files
Friendship-and-chat-pc/tests/member-user-export.test.cjs

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