// Exercises the actual shared UTS logic as JS, not a duplicate implementation. // This does not replace Kotlin/Swift native compilation or device tests. 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 hx = process.env.HBUILDERX_HOME; assert.ok(hx, 'Set HBUILDERX_HOME to the HBuilderX installation directory'); const ts = require(path.join(hx, 'plugins/uniapp-uts-v1/node_modules/@dcloudio/uni-uts-v1/lib/typescript/lib/typescript.js')); const source = fs.readFileSync(path.join(__dirname, '../uni_modules/tb-douyin-pay/utssdk/common.uts'), 'utf8'); const compiled = ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ES2020, module: ts.ModuleKind.CommonJS }, }).outputText; const info = { appid: 'app', partnerid: 'merchant', prepayid: 'prepay', package: 'Sign=DYPay', noncestr: 'nonce', timestamp: '1780000000', sign: 'signed+value/==' }; function load(stringify = JSON.stringify) { const timers = new Map(); let timerId = 0; const exports = {}; const ctx = { exports, JSON: { stringify, parseObject(raw) { const value = JSON.parse(raw); return value == null ? null : { getString: key => typeof value[key] === 'string' ? value[key] : null }; } }, setTimeout(fn) { timers.set(++timerId, fn); return timerId; }, clearTimeout(id) { timers.delete(id); } }; vm.runInNewContext(compiled, ctx); assert.equal(exports.configure('app'), true); return { ...exports, timers }; } test('shared bridge keeps signed fields unchanged and handles default/explicit loading', () => { for (const showLoading of [undefined, null, true, false]) { const bridge = load(); const results = []; bridge.invoke({ payInfo: info, showLoading }, result => results.push(result), (payload, loading, done) => { assert.deepEqual(JSON.parse(payload), info); assert.equal(loading, showLoading ?? true); done('{"resultCode":"0","errorMsg":""}'); }); assert.equal(results.length, 1); assert.equal(results[0].resultCode, '0'); assert.equal(results[0].needsQuery, true); assert.equal(bridge.timers.size, 0); } }); test('missing/null/empty payment fields never reach native code or take a lock', () => { const invalid = [null, undefined, {}, { ...info, appid: 'wrong' }, { ...info, package: 'wrong' }, { ...info, timestamp: '1780000000000' }, { ...info, noncestr: 'n'.repeat(33) }]; for (const key of Object.keys(info)) { for (const value of [null, undefined, '', ' ']) invalid.push({ ...info, [key]: value }); } for (const payInfo of invalid) { const bridge = load(); const results = []; bridge.invoke({ payInfo }, r => results.push(r), () => assert.fail('Invalid native call')); assert.equal(results.length, 1); assert.equal(results[0].resultCode, '-1'); assert.equal(bridge.timers.size, 0); assert.equal(bridge.configure('app'), true); } }); test('null or failed serialization returns an error before the lock and native call', () => { for (const stringify of [() => null, () => undefined, () => '', () => { throw Error('serialize'); }]) { const bridge = load(stringify); bridge.invoke({ payInfo: info }, r => assert.equal(r.resultCode, '-1'), () => assert.fail('Invalid payload')); assert.equal(bridge.timers.size, 0); assert.equal(bridge.configure('app'), true); } }); test('reentrant calls, duplicate callbacks and late callbacks after reset are isolated', () => { const bridge = load(); const results = []; let oldDone; bridge.invoke({ payInfo: info }, r => results.push(r), (_, __, done) => { oldDone = done; }); bridge.invoke({ payInfo: info }, r => assert.equal(r.resultCode, '103'), () => assert.fail('Duplicate pay')); assert.equal(bridge.configure('other'), false); bridge.resetPending(); let newDone; bridge.invoke({ payInfo: info }, r => results.push(r), (_, __, done) => { newDone = done; }); oldDone('{"resultCode":"0"}'); assert.equal(results.length, 0); newDone('{"resultCode":"1"}'); newDone('{"resultCode":"0"}'); assert.equal(results.length, 1); assert.equal(results[0].resultCode, '1'); for (const timer of bridge.timers.values()) timer(); assert.equal(results.length, 1); }); test('timeout, malformed result and native exception remain query-required and unlock', () => { for (const mode of ['timeout', 'malformed', 'throw']) { const bridge = load(); const results = []; bridge.invoke({ payInfo: info }, r => results.push(r), (_, __, done) => { if (mode === 'throw') throw Error('native'); if (mode === 'malformed') done('{'); }); if (mode === 'timeout') for (const timer of bridge.timers.values()) timer(); assert.equal(results.length, 1); assert.notEqual(results[0].resultCode, '0'); assert.equal(results[0].needsQuery, true); assert.equal(bridge.configure('app'), true); } });