// Run after HBuilderX MP-WEIXIN compilation; no network or real account access. const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const vm = require('node:vm'); const repo = path.resolve(__dirname, '..'); const built = path.resolve(process.argv[2] || path.join(repo, 'unpackage/dist/dev/mp-weixin')); const app = JSON.parse(fs.readFileSync(path.join(built, 'app.json'), 'utf8')); const roots = app.subPackages.map(item => item.root.replace(/\/$/, '') + '/'); const packageOf = file => roots.find(root => file.startsWith(root)) || ''; let checks = 0; function test(name, action) { action(); checks++; console.log('PASS ' + name); } function jsFiles(dir) { return fs.readdirSync(dir, { withFileTypes: true }).flatMap(item => { const file = path.join(dir, item.name); return item.isDirectory() ? jsFiles(file) : item.name.endsWith('.js') ? [file] : []; }); } test('all generated static requires stay within main package or their own subpackage', () => { const violations = []; for (const file of jsFiles(built)) { const relative = path.relative(built, file).split(path.sep).join('/'); for (const match of fs.readFileSync(file, 'utf8').matchAll(/\brequire\(["']([^"']+)["']\)/g)) { if (!match[1].startsWith('.')) continue; const target = path.posix.normalize(path.posix.join(path.posix.dirname(relative), match[1])); assert.ok(fs.existsSync(path.join(built, target)), relative + ': missing module ' + target); if (packageOf(target) && packageOf(target) !== packageOf(relative)) violations.push(relative + ' -> ' + target); } } assert.deepEqual(violations, [], 'Unavailable subpackage dependencies: ' + violations.join(', ')); }); test('route, selection and login output is freshly compiled', () => { for (const file of ['services/match-route.uts', 'packages/finance/services/compare.uts', 'packages/account/login/login.uvue']) { const output = path.join(built, file.replace(/\.(uts|uvue)$/, '.js')); assert.ok(fs.statSync(output).mtimeMs >= fs.statSync(path.join(repo, file)).mtimeMs, 'Recompile MP-WEIXIN: ' + file); } }); // Model a cold account-package load: finance/content modules are not registered. const loaded = new Map(); let page, onLoad, restoredPath; class JsonObject { constructor(value) { Object.assign(this, value); } } class UtsType { static initProps(options) { return options; } } const vendor = { UTSJSONObject: JsonObject, UTS: { UTSType: UtsType }, defineComponent: component => component, ref: value => ({ value }), watch: () => {}, onMounted: () => {}, onUnmounted: () => {}, onShow: () => {}, onHide: () => {}, onUnload: () => {}, onLoad: callback => { onLoad = callback; }, index: { getWindowInfo: () => ({ windowWidth: 390, windowHeight: 844, statusBarHeight: 24 }), getMenuButtonBoundingClientRect: () => ({ width: 0, height: 0, top: 0, left: 0 }), onWindowResize: () => {}, offWindowResize: () => {} } }; function load(relative) { assert.ok(!packageOf(relative) || packageOf(relative) === 'packages/account/', 'module is not registered: ' + relative); if (relative === 'common/vendor.js') return vendor; if (loaded.has(relative)) return loaded.get(relative); const exports = {}; loaded.set(relative, exports); vm.runInNewContext(fs.readFileSync(path.join(built, relative), 'utf8'), { exports, setInterval, clearInterval, wx: { createPage: component => { page = component; } }, require: module => load(path.posix.normalize(path.posix.join(path.posix.dirname(relative), module))) }, { filename: relative }); return exports; } test('login registers and initializes without loading the finance subpackage', () => { load('packages/account/login/login.js'); assert.equal(typeof page.setup, 'function'); page.setup({}); assert.equal(typeof onLoad, 'function'); assert.ok([...loaded.keys()].every(file => !file.startsWith('packages/finance/'))); }); const route = load('services/match-route.js'); const realReturnPath = route.matchReturnPath; route.matchReturnPath = (...args) => { restoredPath = realReturnPath(...args); return restoredPath; }; test('account login restores match context using the shared route module', () => { onLoad({ resumeMatch: '1', matchPage: 'product', assessmentId: '42', productIds: '3,7', revisionId: '9', productId: '7', tab: 'details', from: 'compare' }); assert.equal(restoredPath, '/packages/finance/product/product?assessmentId=42&productIds=3,7&revisionId=9&productId=7&tab=details&from=compare'); }); const cases = [ ['matches with selected products and revision', ['matches', '42', '3,7', '9'], '/packages/finance/matches/matches?assessmentId=42&productIds=3,7&revisionId=9&group=eligible'], ['matches without a selection', ['matches', '42', '', '', '', 'unknown'], '/packages/finance/matches/matches?assessmentId=42&productIds=&group=unknown'], ['compare selection order', ['compare', '42', '7,3,8', '9'], '/packages/finance/compare/compare?assessmentId=42&productIds=7,3,8&revisionId=9'], ['context product', ['product', '42', '3', '', '3'], '/packages/finance/product/product?assessmentId=42&productIds=3&productId=3&tab=explanation&from='], ['public product details', ['product', '', '', '', '3', 'eligible', 'details'], '/packages/finance/product/product?productId=3&tab=details&from='], ['public product conditions', ['product', '', '', '', '3', 'eligible', 'conditions', 'archive'], '/packages/finance/product/product?productId=3&tab=conditions&from=archive'], ['external return URL', ['https://example.invalid', '42', '', ''], ''], ['undeclared page', ['login', '42', '', ''], ''], ['missing assessment for matches', ['matches', '', '', ''], ''], ['missing assessment for compare', ['compare', '', '3', ''], ''], ['invalid assessment', ['compare', '42&x=1', '3', ''], ''], ['zero assessment', ['matches', '0', '', ''], ''], ['invalid revision', ['matches', '42', '', '0'], ''], ['duplicate products', ['compare', '42', '3,3', ''], ''], ['too many products', ['compare', '42', '1,2,3,4', ''], ''], ['invalid product list', ['compare', '42', '3,x', ''], ''], ['empty list member', ['compare', '42', '3,', ''], ''], ['invalid matches group', ['matches', '42', '', '', '', 'other'], ''], ['missing product', ['product', '42', '', '', ''], ''], ['invalid product', ['product', '42', '', '', '3&x=1'], ''], ['invalid product tab', ['product', '42', '', '', '3', 'eligible', 'other'], ''], ['invalid product source', ['product', '42', '', '', '3', 'eligible', 'details', '//evil.invalid'], ''], ['public product cannot restore an explanation', ['product', '', '', '', '3'], ''], ['public product cannot carry a selection', ['product', '', '3', '', '3', 'eligible', 'details'], ''], ['public product cannot carry a revision', ['product', '', '', '9', '3', 'eligible', 'details'], ''] ]; for (const [name, args, expected] of cases) test(name, () => assert.equal(route.matchReturnPath(...args), expected)); test('selection parser keeps the existing limits', () => { assert.deepEqual(Array.from(route.parseCompareIds('')), []); assert.deepEqual(Array.from(route.parseCompareIds('7,3,8')), ['7', '3', '8']); for (const raw of ['1,2,3,4', '3,3', '0', '03', '-3', '3,', '3, 7', '12345678901', '3&tab=details']) assert.throws(() => route.parseCompareIds(raw)); }); test('finance selection still uses shared validation and isolates session/assessment/revision', () => { let session = { userId: 'synthetic-user', token: 'synthetic-token' }; const exports = {}; vm.runInNewContext(fs.readFileSync(path.join(built, 'packages/finance/services/compare.js'), 'utf8'), { exports, require(module) { if (module === '../../../services/match-route.js') return route; if (module === './matches.js') return {}; if (module === '../../../services/api.js') return { getSession: () => session && { getString: key => session[key] } }; throw Error('Unexpected finance dependency: ' + module); } }); exports.saveCompareSelection('42', ['7', '3'], '9'); assert.deepEqual(Array.from(exports.compareSelection('42', '9')), ['7', '3']); assert.throws(() => exports.saveCompareSelection('42', ['7', '7'], '9')); assert.deepEqual(Array.from(exports.compareSelection('42', '9')), ['7', '3']); for (const change of [() => { session.token = 'another-token'; }, () => { session.userId = 'another-user'; }]) { change(); assert.deepEqual(Array.from(exports.compareSelection('42', '9')), []); exports.saveCompareSelection('42', ['3'], '9'); } assert.deepEqual(Array.from(exports.compareSelection('43', '9')), []); exports.saveCompareSelection('42', ['3'], '9'); assert.deepEqual(Array.from(exports.compareSelection('42', '10')), []); exports.saveCompareSelection('42', ['3'], '9'); session = null; assert.deepEqual(Array.from(exports.compareSelection('42', '9')), []); }); console.log(checks + ' checks passed; no network calls.');