111
build / task (16.x, npm, macOS-latest) (push) Canceled after 0s
build / task (16.x, npm, ubuntu-latest) (push) Canceled after 0s
build / task (16.x, npm, windows-latest) (push) Canceled after 0s
build / task (16.x, pnpm, macOS-latest) (push) Canceled after 0s
build / task (16.x, pnpm, ubuntu-latest) (push) Canceled after 0s
build / task (16.x, pnpm, windows-latest) (push) Canceled after 0s
build / task (16.x, yarn, macOS-latest) (push) Canceled after 0s
build / task (16.x, yarn, ubuntu-latest) (push) Canceled after 0s
build / task (16.x, yarn, windows-latest) (push) Canceled after 0s
build / task (18.x, npm, macOS-latest) (push) Canceled after 0s
build / task (18.x, npm, ubuntu-latest) (push) Canceled after 0s
build / task (18.x, npm, windows-latest) (push) Canceled after 0s
build / task (18.x, pnpm, macOS-latest) (push) Canceled after 0s
build / task (18.x, pnpm, ubuntu-latest) (push) Canceled after 0s
build / task (18.x, pnpm, windows-latest) (push) Canceled after 0s
build / task (18.x, yarn, macOS-latest) (push) Canceled after 0s
build / task (18.x, yarn, ubuntu-latest) (push) Canceled after 0s
build / task (18.x, yarn, windows-latest) (push) Canceled after 0s
build / task (20.x, npm, macOS-latest) (push) Canceled after 0s
build / task (20.x, npm, ubuntu-latest) (push) Canceled after 0s
build / task (20.x, npm, windows-latest) (push) Canceled after 0s
build / task (20.x, pnpm, macOS-latest) (push) Canceled after 0s
build / task (20.x, pnpm, ubuntu-latest) (push) Canceled after 0s
build / task (20.x, pnpm, windows-latest) (push) Canceled after 0s
build / task (20.x, yarn, macOS-latest) (push) Canceled after 0s
build / task (20.x, yarn, ubuntu-latest) (push) Canceled after 0s
build / task (20.x, yarn, windows-latest) (push) Canceled after 0s

This commit is contained in:
dzc committed 2026-10-21 19:40:17 +08:00
1 parent cce677e3ff
commit 530cfcd939
42 files changed
+3951 -483

No files matched your search

+163
View File
@@ -0,0 +1,163 @@
const { test } = require("node:test")
const assert = require("node:assert/strict")
const fs = require("node:fs")
const path = require("node:path")
const ts = require("typescript")
const vue = require("vue")
const { parse, compileScript } = require("@vue/compiler-sfc")
const root = path.join(__dirname, "../src")
const flush = () => new Promise(setImmediate)
const deferred = () => { let resolve, reject; const promise = new Promise((a, b) => { resolve = a; reject = b }); return { promise, resolve, reject } }
function load(source, mocks, globals = {}) {
const compiled = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2021 } }).outputText
const module = { exports: {} }
const names = Object.keys(globals)
new Function("module", "exports", "require", ...names, compiled)(module, module.exports, key => key in mocks ? mocks[key] : require(key), ...Object.values(globals))
return module.exports
}
function component(query) {
let unmount
let now = 0
let timerId = 0
const timers = new Map()
const events = []
const requests = []
let exposed
const source = compileScript(parse(fs.readFileSync(path.join(root, "components/AsyncTaskProgress.vue"), "utf8")).descriptor, { id: "test" }).content
const mod = load(source, {
vue: { ...vue, onBeforeUnmount: fn => { unmount = fn } },
"/@/api/asyncTask": { getAsyncTaskProgress: async (url, params) => { requests.push({ url, params }); return query(requests.length) } },
"/@/utils/exportDownload": { downloadExportFile() {} },
"/@/hooks/message": { useMessage: () => ({ error() {} }) },
}, {
setTimeout: fn => { timers.set(++timerId, fn); return timerId },
clearTimeout: id => timers.delete(id),
Date: { now: () => now },
})
const state = mod.default.setup({ config: { progressUrl: "/wallet/async-task/progress", redisKey: "wallet:export:xf", pollTimeoutMs: 1000 } }, {
expose: value => { exposed = value }, emit: (...args) => events.push(args),
})
return { state, exposed, events, requests, timers, unmount: () => unmount(), setNow: value => { now = value },
async tick() { const entry = timers.entries().next().value; assert.ok(entry, "下一次轮询已安排"); timers.delete(entry[0]); entry[1](); await flush() } }
}
const initial = { taskId: "a".repeat(32), redisKey: "wallet:export:xf", taskName: "消费记录", status: "PENDING", total: 1, completed: 0, percentage: 0, message: "等待执行", finished: false, success: false }
const success = { ...initial, status: "SUCCESS", completed: 1, percentage: 100, finished: true, success: true, resultFileUrl: "/admin/sys-file/oss/file?fileName=test.xlsx", resultFileName: "消费记录.xlsx" }
test("提交互斥且冻结嵌套筛选,列表分页不限制导出", async () => {
const submitted = deferred()
const calls = []
const ui = []
const downloads = []
const mod = load(fs.readFileSync(path.join(root, "hooks/batchExport.ts"), "utf8"), {
vue,
"/@/components/AsyncTaskProgress.vue": {},
"/@/hooks/message": { useMessage: () => ({ error: value => ui.push(value) }) },
"/@/utils/exportDownload": { downloadExportFile: (...args) => downloads.push(args) },
})
const task = mod.useBatchExport({ redisKey: initial.redisKey, progressUrl: "/wallet/async-task/progress", taskName: "导出", fileName: "回退.xlsx", submitTask: async query => { calls.push(query); return submitted.promise } })
task.exportProgressRef.value = { startSubmitting: () => ui.push("submit"), start: () => ui.push("start"), show: () => ui.push("show") }
const query = vue.reactive({ current: 8, size: 10, userIds: ["1"], range: { from: "2026-01-01" }, descs: ["create_time"] })
const pending = task.submitExport(query)
await task.submitExport(query)
query.userIds.push("2"); query.range.from = "2026-02-01"
assert.equal(calls.length, 1)
assert.deepEqual(calls[0], { userIds: ["1"], range: { from: "2026-01-01" }, descs: ["create_time"] })
assert.deepEqual(ui, ["submit", "show"])
submitted.resolve({ data: initial }); await pending
assert.equal(task.exportSubmitting.value, true)
task.handleExportSuccess(success)
assert.equal(task.exportSubmitting.value, false)
assert.deepEqual(downloads[0], [success.resultFileUrl, success.resultFileName])
})
test("提交失败恢复按钮状态并展示受控错误", async () => {
const errors = []
const mod = load(fs.readFileSync(path.join(root, "hooks/batchExport.ts"), "utf8"), {
vue, "/@/components/AsyncTaskProgress.vue": {},
"/@/hooks/message": { useMessage: () => ({ error: message => errors.push(message) }) },
"/@/utils/exportDownload": {},
})
const task = mod.useBatchExport({ submitTask: async () => { throw { msg: "任务繁忙" } } })
task.exportProgressRef.value = { startSubmitting() {}, submissionFailed: message => errors.push(message) }
await task.submitExport({})
assert.equal(task.exportSubmitting.value, false)
assert.deepEqual(errors, ["任务繁忙", "任务繁忙"])
})
test("慢查询不重叠,成功只通知一次且停止轮询", async () => {
const pending = deferred()
const task = component(async count => count === 1 ? pending.promise : { data: success })
task.exposed.startSubmitting("导出")
assert.equal(task.state.canClose.value, false)
task.exposed.start(initial)
await flush()
assert.equal(task.requests.length, 1)
assert.equal(task.timers.size, 0)
pending.resolve({ data: { ...initial, status: "RUNNING", total: 10, completed: 1, percentage: 10 } }); await flush()
assert.equal(task.timers.size, 1)
await task.tick()
assert.equal(task.events.length, 1)
assert.equal(task.events[0][0], "success")
assert.equal(task.timers.size, 0)
assert.equal(task.state.canClose.value, true)
})
test("连续五次查询失败暂停,重新查询原任务后仍可下载", async () => {
const task = component(async count => { if (count <= 5) throw { msg: "网络中断" }; return { data: success } })
task.exposed.start(initial); await flush()
for (let i = 0; i < 4; i++) await task.tick()
assert.equal(task.requests.length, 5)
assert.equal(task.state.state.polling, false)
assert.equal(task.state.state.queryError, "网络中断")
task.state.state.visible = false; task.exposed.show()
assert.equal(task.state.state.visible, true)
task.state.retry(); await flush()
assert.equal(task.requests.length, 6)
assert.ok(task.requests.every(item => item.params.taskId === initial.taskId))
assert.equal(task.events[0][0], "success")
})
test("卸载或新任务替换后忽略旧请求响应", async () => {
const pending = deferred()
const task = component(async () => pending.promise)
task.exposed.start(initial); await flush(); task.unmount()
pending.resolve({ data: success }); await flush()
assert.equal(task.timers.size, 0)
assert.equal(task.events.length, 0)
const old = deferred()
const replacement = component(async count => count === 1 ? old.promise : { data: { ...initial, taskId: "b".repeat(32) } })
replacement.exposed.start(initial)
replacement.exposed.start({ ...initial, taskId: "b".repeat(32) }); await flush()
old.resolve({ data: success }); await flush()
assert.equal(replacement.state.state.progress.taskId, "b".repeat(32))
assert.equal(replacement.events.length, 0)
})
test("轮询超时只暂停跟踪,后台失败才发出 failure", async () => {
const task = component(async () => ({ data: initial }))
task.exposed.start(initial); await flush(); task.setNow(1001)
await task.tick()
assert.equal(task.requests.length, 1)
assert.match(task.state.state.queryError, /后台任务可能仍在运行/)
assert.equal(task.events.length, 0)
const failed = component(async () => ({ data: { ...initial, finished: true, success: false, status: "FAILED", message: "导出失败" } }))
failed.exposed.start(initial); await flush()
assert.equal(failed.events[0][0], "failure")
assert.equal(failed.state.canDownload.value, false)
})
test("大文件下载使用原生附件地址并适配单体路径,不创建 Blob", () => {
const links = []
const source = fs.readFileSync(path.join(root, "utils/exportDownload.ts"), "utf8").replaceAll("import.meta.env", '({ VITE_API_URL: "/api" })')
const mod = load(source, { "/@/utils/other": { default: { adaptationUrl: path => path.replace("/hard/", "/admin/") } } }, {
document: { createElement: () => { const link = { style: {}, click() {}, remove() {} }; links.push(link); return link }, body: { appendChild() {} } },
Blob: class { constructor() { throw new Error("不能将整个大文件读入 Blob") } },
})
mod.downloadExportFile("/hard/file?id=large", "充值/扣款.xlsx")
assert.equal(links[0].href, "/api/admin/file?id=large")
assert.equal(links[0].download, "充值_扣款.xlsx")
mod.downloadExportFile("https://files.example.com/test", "test.xlsx")
assert.equal(links[1].href, "https://files.example.com/test")
})
+56
View File
@@ -0,0 +1,56 @@
const { test } = require("node:test")
const assert = require("node:assert/strict")
const fs = require("node:fs")
const path = require("node:path")
const ts = require("typescript")
// 项目尚未配置前端测试框架,使用现有 TypeScript 与 Node 测试业务状态判断。
const source = fs.readFileSync(path.join(__dirname, "../src/views/hard/hard-third-order/order.ts"), "utf8")
const compiled = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS } }).outputText
const loaded = { exports: {} }
new Function("module", "exports", compiled)(loaded, loaded.exports)
const { orderState, refundState, refundDisabledReason, remainingPrice, apiResult, refundTime } = loaded.exports
const paid = {
id: "90071992547409931",
orderType: "6",
orderStatus: "5",
payType: "3",
walletId: "123",
payTime: "2026-10-21 10:00:00",
payPrice: 1000,
refundPrice: 0,
}
test("共享退款状态码仍区分部分退款与全额退款", () => {
const partial = { ...paid, orderStatus: "10", refundPrice: 200 }
const full = { ...partial, refundPrice: 1000 }
assert.equal(orderState(partial).label, "已部分退款")
assert.equal(orderState(full).label, "已全额退款")
assert.equal(remainingPrice(partial), 800)
assert.equal(refundDisabledReason(partial), "")
assert.equal(refundDisabledReason(full), "订单已全额退款")
})
test("待支付订单和其他业务订单不提供钱包退款", () => {
assert.equal(remainingPrice({ ...paid, orderStatus: "1", payTime: undefined }), 0)
assert.equal(refundDisabledReason({ ...paid, orderStatus: "1", payTime: undefined }), "订单尚未完成支付")
assert.equal(refundDisabledReason({ ...paid, orderType: "1" }), "仅支持第三方应用支付底单")
assert.equal(refundDisabledReason({ ...paid, payType: "10" }), "该支付方式不支持底单钱包退款")
})
test("售后申请中即使底单状态尚未同步也不能重复退款", () => {
assert.equal(refundDisabledReason(paid, [{ id: "456", status: "10" }]), "存在处理中的退款")
assert.equal(refundState({ ...paid, refundStatus: "10", refundPrice: 200 }).label, "退款处理中")
})
test("历史售后无退款时间时使用审核时间", () => {
assert.equal(refundTime({ status: "50", auditTime: "2026-10-21 10:02:00", createTime: "2026-10-21 10:01:00" }), "2026-10-21 10:02:00")
assert.equal(refundTime({ status: "10", auditTime: "2026-10-21 10:02:00", createTime: "2026-10-21 10:01:00" }), "2026-10-21 10:01:00")
})
test("接口 HTTP 成功与业务支付成功分别判断,坏日志不误报成功", () => {
assert.equal(apiResult({ res: '{"code":200}' }).type, "success")
assert.equal(apiResult({ res: '{"code":505,"message":"用户余额不足"}' }).label, "用户余额不足")
assert.equal(apiResult({ error: "timeout", res: '{"code":200}' }).type, "danger")
assert.equal(apiResult({ res: "invalid-json" }).label, "查看响应")
})
+68
View File
@@ -0,0 +1,68 @@
const { test } = require("node:test")
const assert = require("node:assert/strict")
const fs = require("node:fs")
const path = require("node:path")
const ts = require("typescript")
const vue = require("vue")
const { parse, compileScript, compileTemplate } = require("@vue/compiler-sfc")
const root = path.join(__dirname, "../src")
function load(source, mocks, globals = {}) {
const compiled = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2021 } }).outputText
const mod = { exports: {} }
new Function("module", "exports", "require", ...Object.keys(globals), compiled)(mod, mod.exports,
key => key in mocks ? mocks[key] : require(key), ...Object.values(globals))
return mod.exports
}
for (const scene of ["cz", "jz", "qb"]) {
test(`${scene} 页面提交对应后台任务,固定筛选并保留进度和下载流程`, async () => {
const requests = [], progress = [], downloads = []
const submitted = { taskId: "a".repeat(32), redisKey: `wallet:export:${scene}`, status: "PENDING" }
const request = options => { requests.push(options); return Promise.resolve({ data: submitted }) }
const tasks = load(fs.readFileSync(path.join(root, "api/asyncTask.ts"), "utf8"), { "/@/utils/request": { default: request } })
const apis = {}
for (const api of ["wallet-manage", "wallet-transaction"]) {
apis[`/@/api/wallet/${api}`] = load(fs.readFileSync(path.join(root, `api/wallet/${api}.ts`), "utf8"), {
"/@/utils/request": { default: request }, "/@/api/asyncTask": tasks,
})
}
const hook = load(fs.readFileSync(path.join(root, "hooks/batchExport.ts"), "utf8"), {
vue, "/@/components/AsyncTaskProgress.vue": {},
"/@/hooks/message": { useMessage: () => ({ error: message => assert.fail(message) }) },
"/@/utils/exportDownload": { downloadExportFile: (...args) => downloads.push(args) },
})
const descriptor = parse(fs.readFileSync(path.join(root, `views/wallet/wallet-${scene}/index.vue`), "utf8")).descriptor
assert.deepEqual(compileTemplate({ source: descriptor.template.content, filename: `${scene}.vue`, id: scene }).errors, [])
const page = load(compileScript(descriptor, { id: scene }).content, {
...apis, vue, dayjs: { default: require("dayjs") },
"/@/components/AsyncTaskProgress.vue": {}, "/@/hooks/batchExport": hook,
"/@/hooks/table": { useTable: state => {
state.pagination = { current: 8, size: 10 }
return { getDataList() {}, tableStyle: {} }
} },
"/@/hooks/message": {}, "/@/hooks/dict": { useDict: () => ({}) },
"/@/utils/formatMoney": {}, "/@/api/admin/dept": {},
"/@/utils/formatTime": { formatDate: () => "2026-09" },
}, { ref: vue.ref, reactive: vue.reactive, defineAsyncComponent: vue.defineAsyncComponent, useRouter: () => ({ push() {} }) })
const state = page.default.setup({}, { expose() {} })
state.exportProgressRef.value = { startSubmitting: name => progress.push(name), start: value => progress.push(value), submissionFailed: message => assert.fail(message) }
Object.assign(state.state.queryForm, { current: 8, size: 10, userId: "123", deptIds: ["456"], walletTypeIdList: ["789"], isInclude: false })
if (scene !== "qb") Object.assign(state.state.queryForm, { bizType: "3", createTimeList: ["2026-09-01", "2026-09-30"], tagList: ["7"] })
const before = JSON.parse(JSON.stringify(state.state.queryForm))
const expected = { ...before }; delete expected.current; delete expected.size
await state.exportExcel()
assert.equal(requests.length, 1)
assert.equal(requests[0].url, scene === "qb" ? "/wallet/wallet-manage/export-qb-async" : `/wallet/wallet-transaction/export-${scene}-async`)
assert.equal(requests[0].method, "post")
assert.deepEqual(requests[0].data, expected)
assert.deepEqual(JSON.parse(JSON.stringify(state.state.queryForm)), before)
assert.equal(state.exportTaskConfig.progressUrl, "/wallet/async-task/progress")
assert.equal(state.exportTaskConfig.redisKey, submitted.redisKey)
assert.deepEqual(progress[1], submitted)
state.state.queryForm.deptIds.push("999")
assert.deepEqual(requests[0].data.deptIds, ["456"])
state.handleExportSuccess({ ...submitted, status: "SUCCESS", resultFileUrl: "https://files.example.com/export.xlsx", resultFileName: "原导出文件.xlsx" })
assert.deepEqual(downloads, [["https://files.example.com/export.xlsx", "原导出文件.xlsx"]])
})
}
+59
View File
@@ -0,0 +1,59 @@
const { test } = require("node:test")
const assert = require("node:assert/strict")
const fs = require("node:fs")
const path = require("node:path")
const ts = require("typescript")
const vue = require("vue")
const { parse, compileScript, compileTemplate } = require("@vue/compiler-sfc")
const root = path.join(__dirname, "../src")
function load(source, mocks, globals = {}) {
const compiled = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2021 } }).outputText
const mod = { exports: {} }
new Function("module", "exports", "require", ...Object.keys(globals), compiled)(mod, mod.exports,
key => key in mocks ? mocks[key] : require(key), ...Object.values(globals))
return mod.exports
}
test("电子钱包页面提交后台任务,保留筛选快照且不受当前列表页限制", async () => {
const requests = []
const progress = []
const submitted = { taskId: "a".repeat(32), redisKey: "wallet:export:manage", status: "PENDING" }
const request = options => { requests.push(options); return Promise.resolve({ data: submitted }) }
const tasks = load(fs.readFileSync(path.join(root, "api/asyncTask.ts"), "utf8"), { "/@/utils/request": { default: request } })
const api = load(fs.readFileSync(path.join(root, "api/wallet/wallet-manage.ts"), "utf8"), {
"/@/utils/request": { default: request }, "/@/api/asyncTask": tasks,
})
const hook = load(fs.readFileSync(path.join(root, "hooks/batchExport.ts"), "utf8"), {
vue, "/@/components/AsyncTaskProgress.vue": {},
"/@/hooks/message": { useMessage: () => ({ error: message => assert.fail(message) }) },
"/@/utils/exportDownload": {},
})
const descriptor = parse(fs.readFileSync(path.join(root, "views/wallet/wallet-manage/index.vue"), "utf8")).descriptor
assert.deepEqual(compileTemplate({ source: descriptor.template.content, filename: "wallet-manage.vue", id: "wallet-manage" }).errors, [])
const page = load(compileScript(descriptor, { id: "wallet-manage" }).content, {
vue, "/@/api/wallet/wallet-manage": api,
"/@/components/AsyncTaskProgress.vue": {}, "/@/hooks/batchExport": hook,
"/@/hooks/table": { useTable: state => {
state.pagination = { current: 3, size: 10 }
return { getDataList() {}, tableStyle: {} }
} },
"/@/hooks/message": {}, "/@/hooks/dict": { useDict: () => ({}) },
"/@/utils/formatMoney": {},
}, { ref: vue.ref, reactive: vue.reactive, defineAsyncComponent: vue.defineAsyncComponent })
const state = page.default.setup({}, { expose() {} })
state.exportProgressRef.value = { startSubmitting: name => progress.push(name), start: value => progress.push(value), submissionFailed: message => assert.fail(message) }
Object.assign(state.state.queryForm, { current: 3, size: 10, userId: "123", deptIds: ["456"], walletTypeId: "789", isInclude: false, leBalance: 100 })
const original = JSON.parse(JSON.stringify(state.state.queryForm))
await state.exportExcel()
assert.equal(requests.length, 1)
assert.equal(requests[0].url, "/wallet/wallet-manage/export-async")
assert.equal(requests[0].method, "post")
assert.deepEqual(requests[0].data, { userId: "123", deptIds: ["456"], walletTypeId: "789", isInclude: false, leBalance: 100 })
assert.deepEqual(JSON.parse(JSON.stringify(state.state.queryForm)), original)
assert.equal(state.exportTaskConfig.progressUrl, "/wallet/async-task/progress")
assert.equal(state.exportTaskConfig.redisKey, "wallet:export:manage")
assert.deepEqual(progress, ["电子钱包导出", submitted])
state.state.queryForm.deptIds.push("999")
assert.deepEqual(requests[0].data.deptIds, ["456"])
})