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