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") const filename = path.join(root, "views/wallet/wallet-qb/index.vue") function load(source, mocks) { const compiled = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020, esModuleInterop: true }, }).outputText const mod = { exports: {} } const autoImports = ["ref", "reactive", "computed", "watch", "onMounted", "onBeforeUnmount"] new Function("module", "exports", "require", ...autoImports, compiled)( mod, mod.exports, id => id in mocks ? mocks[id] : require(id), ...autoImports.map(key => vue[key]), ) return mod.exports } function mount({ deferredLists = false } = {}) { const lists = [], statistics = [], errors = [] const request = options => { if (options.url.endsWith("/page-qb-users")) { if (deferredLists) return new Promise((resolve, reject) => lists.push({ ...options, resolve, reject })) lists.push(options) return Promise.resolve({ code: 0, data: { records: [], total: 100 } }) } assert.equal(options.url, "/wallet/wallet-manage/qb-users-total") return new Promise((resolve, reject) => statistics.push({ ...options, resolve, reject })) } const api = load(fs.readFileSync(path.join(root, "api/wallet/wallet-manage.ts"), "utf8"), { "/@/utils/request": request, "/@/api/asyncTask": {}, }) const hook = load(fs.readFileSync(path.join(root, "views/wallet/wallet-qb/useWalletQbUsers.ts"), "utf8"), { vue, "/@/api/wallet/wallet-manage": api, "/@/hooks/message": { useMessage: () => ({ error: message => errors.push(message) }) }, "/@/utils/formatTime": { formatDate: () => "2026-11" }, }) const renderer = vue.createRenderer({ createElement: () => ({}), createText: () => ({}), createComment: () => ({}), setText() {}, setElementText() {}, patchProp() {}, insert() {}, remove() {}, parentNode: () => null, nextSibling: () => null, }) let state const app = renderer.createApp({ setup() { state = hook.useWalletQbUsers(); return () => null } }) app.mount({}) return { state, lists, statistics, errors, close: () => app.unmount() } } const settle = () => new Promise(resolve => setImmediate(resolve)) const result = total => ({ code: 0, data: { total, eatTotal: total, payTotal: 0, topupTotal: 0, userCount: 4, walletCount: 7 } }) test("用户汇总 API 使用新路径并传递分页、排序和取消信号,旧接口继续兼容", async () => { const requests = [] const api = load(fs.readFileSync(path.join(root, "api/wallet/wallet-manage.ts"), "utf8"), { "/@/utils/request": options => { requests.push(options); return Promise.resolve({}) }, "/@/api/asyncTask": {}, }) const controller = new AbortController() const query = { current: 2, size: 20, ascs: "eat_balance", descs: "", walletRecordMonth: "2026-11", minBalance: 0 } await api.fetchQbUsers(query, controller.signal) assert.equal(requests[0].url, "/wallet/wallet-manage/page-qb-users") assert.equal(requests[0].method, "post") assert.deepEqual(requests[0].params, { current: 2, size: 20, ascs: "eat_balance", descs: undefined }) assert.equal(requests[0].data, query) assert.equal(requests[0].signal, controller.signal) await api.qbUsersTotal(query, controller.signal) assert.equal(requests[1].url, "/wallet/wallet-manage/qb-users-total") assert.equal(requests[1].method, "post") assert.equal(requests[1].signal, controller.signal) await api.qbTotal(query, controller.signal) await api.fetchListJoinQb(query) assert.equal(requests[2].url, "/wallet/wallet-manage/qb-total") assert.equal(requests[3].url, "/wallet/wallet-manage/page-join-qb") }) test("首次加载和翻页仅查询用户汇总列表,统计由用户单独触发", async () => { const page = mount() try { await settle() assert.equal(page.lists.length, 1) page.state.currentChangeHandle(2) await settle() page.state.sizeChangeHandle(20) await settle() assert.equal(page.lists.length, 3) assert.equal(page.statistics.length, 0) } finally { page.close() } }) test("合计余额区间以元输入精确转成分,包含零、负数及单边条件", async () => { const page = mount() try { await settle() for (const [minimum, maximum, expectedMin, expectedMax] of [ ["1000.10", "2000.99", 100010, 200099], ["-5.01", "0", -501, 0], ["90071992547409.01", "90071992547409.91", 9007199254740901, 9007199254740991], ["0", "", 0, undefined], ["", "2000", undefined, 200000], ["", "", undefined, undefined], ]) { Object.assign(page.state.state.queryForm, { minBalanceYuan: minimum, maxBalanceYuan: maximum }) await page.state.getDataList() assert.equal(page.lists.at(-1).data.minBalance, expectedMin) assert.equal(page.lists.at(-1).data.maxBalance, expectedMax) assert.equal(page.state.rangeError.value, "") } } finally { page.close() } }) test("反向区间、非法金额及超出分精度的输入拦截请求并保留已应用条件", async () => { const page = mount() try { await settle() const original = page.state.getAppliedQuery() for (const [minimum, maximum] of [["2000", "1000"], ["abc", ""], ["1.001", ""], ["9007199254740992", ""]]) { const before = page.lists.length Object.assign(page.state.state.queryForm, { minBalanceYuan: minimum, maxBalanceYuan: maximum }) await page.state.getDataList() assert.equal(page.lists.length, before) assert.ok(page.state.rangeError.value) assert.deepEqual(page.state.getAppliedQuery(), original) } } finally { page.close() } }) test("翻页、分项排序和统计沿用已应用快照,未提交的表单修改不会改变查询范围", async () => { const page = mount() try { await settle() Object.assign(page.state.state.queryForm, { deptIds: ["11"], walletTypeIdList: ["21"], isInclude: false, minBalanceYuan: "1000", maxBalanceYuan: "2000" }) await page.state.getDataList() const applied = page.state.getAppliedQuery() page.state.state.queryForm.deptIds.push("12") Object.assign(page.state.state.queryForm, { walletRecordMonth: "2026-10", minBalanceYuan: "0" }) page.state.currentChangeHandle(3) await settle() assert.equal(page.lists.at(-1).params.current, 3) assert.deepEqual(page.lists.at(-1).data.deptIds, ["11"]) assert.equal(page.lists.at(-1).data.minBalance, 100000) for (const [prop, order, ascending, descending] of [ ["eatBalance", "ascending", "eat_balance", undefined], ["payBalance", "descending", undefined, "pay_balance"], ["topupBalance", "ascending", "topup_balance", undefined], ["balance", "descending", undefined, "balance"], ]) { await page.state.handleBalanceSortChange({ prop, order }) assert.equal(page.lists.at(-1).params.current, 1) assert.equal(page.lists.at(-1).params.ascs, ascending) assert.equal(page.lists.at(-1).params.descs, descending) } await page.state.handleBalanceSortChange({ order: null }) assert.equal(page.lists.at(-1).params.ascs, undefined) assert.equal(page.lists.at(-1).params.descs, undefined) page.state.viewBalanceStatistics() page.state.viewBalanceStatistics() assert.equal(page.statistics.length, 1) const { ascs, descs, ...expectedStatisticsQuery } = applied assert.deepEqual(page.statistics[0].data, expectedStatisticsQuery) const snapshot = page.state.getAppliedQuery() snapshot.deptIds.push("99") assert.deepEqual(page.state.getAppliedQuery().deptIds, ["11"]) } finally { page.close() } }) test("新筛选取消旧统计,迟到的成功和失败均不覆盖新结果,空数据可明确统计为零", async () => { const page = mount() try { await settle() page.state.viewBalanceStatistics() Object.assign(page.state.state.queryForm, { minBalanceYuan: "1000" }) await page.state.getDataList() assert.equal(page.statistics[0].signal.aborted, true) assert.equal(page.state.balanceStatisticsLoaded.value, false) page.state.viewBalanceStatistics() page.statistics[0].reject(new Error("旧请求失败")) await settle() assert.equal(page.state.balanceStatisticsLoading.value, true) assert.deepEqual(page.errors, []) page.state.clearStatistics() page.state.viewBalanceStatistics() page.statistics[2].resolve(result(0)) await settle() page.statistics[1].resolve(result(1000)) await settle() assert.equal(page.state.balanceStatistics.total, 0) assert.equal(page.state.balanceStatisticsLoaded.value, true) assert.equal(page.state.balanceStatisticsLoading.value, false) } finally { page.close() } }) test("统计失败可以重试,卸载取消统计和列表,过期列表不会覆盖新页", async () => { const page = mount({ deferredLists: true }) try { await settle() Object.assign(page.state.state.queryForm, { minBalanceYuan: "1000" }) const newest = page.state.getDataList() assert.equal(page.lists[0].signal.aborted, true) page.lists[1].resolve({ code: 0, data: { records: [{ userId: "new" }], total: 1 } }) await newest page.lists[0].resolve({ code: 0, data: { records: [{ userId: "old" }], total: 100 } }) await settle() assert.equal(page.state.state.dataList[0].userId, "new") page.state.viewBalanceStatistics() page.statistics[0].reject(new Error("查询超时")) await settle() assert.equal(page.state.balanceStatisticsLoaded.value, false) assert.equal(page.state.balanceStatisticsLoading.value, false) assert.equal(page.state.balanceStatisticsError.value, "查询超时") page.state.viewBalanceStatistics() page.state.currentChangeHandle(2) page.close() assert.equal(page.statistics[1].signal.aborted, true) assert.equal(page.lists[2].signal.aborted, true) } finally { /* 此测试在验证卸载时已经关闭组件。 */ } }) test("真实页面的查询、统计和用户汇总表格模板可编译", () => { const descriptor = parse(fs.readFileSync(filename, "utf8"), { filename }).descriptor const script = compileScript(descriptor, { id: "wallet-qb-users-test" }) assert.deepEqual(compileTemplate({ source: descriptor.template.content, filename, id: "wallet-qb-users-test", compilerOptions: { bindingMetadata: script.bindings } }).errors, []) })