/** * util-navigate.js 集成测试 * * 验证: * - util-navigate.js 作为独立 ESM 模块可被 vm.runInContext 加载 * - 所有导出的方法都存在且行为正确 * - util.js 通过 import 引用 util-navigate.js 的方法后,this.$util.xxx 仍然可用 * - getPlatform 注入(测试用 globalThis.__UTIL_NAVIGATE_TEST_PLATFORM__)能强制指定平台 * * 跑法: * node --test tests/util-navigate.test.js */ 'use strict'; 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 NAV_SRC = fs.readFileSync( path.join(__dirname, '..', 'common', 'js', 'util-navigate.js'), 'utf8', ); /** * 在沙箱里加载 util-navigate.js,把所有 export 提取到一个对象 */ function loadNavInSandbox() { // util-navigate.js 用了 ESM export, vm 不能直接跑 export // 我们用一段包装代码: 把 export 转成 module.exports const moduleObj = { exports: {} }; const wrapped = NAV_SRC .replace(/^export\s+function\s+/gm, 'function ') .replace(/^export\s+const\s+/gm, 'const ') .replace(/^export\s+let\s+/gm, 'let ') .replace(/^export\s+var\s+/gm, 'var ') + '\nmodule.exports = { _toParamValueString, parsePageParams, resolveLinkUrl, ' + 'cleanParamUrls, normalizeUrl, appendQueryParams, extractInternalAppPath, resolveNavMode, hasStaleUniacidInUrl, pickCurrentUniacid, getPlatform, shouldEncodeUrl, ' + '_safeEncodeForPlatform, _navigateTo, createNavigateApi };\n'; const sandbox = { console, globalThis: {}, module: moduleObj, exports: moduleObj.exports }; sandbox.globalThis = sandbox; vm.createContext(sandbox); vm.runInContext(wrapped, sandbox, { filename: 'util-navigate.js' }); return sandbox.module.exports; } // ============================================================= // 1. 模块可加载 // ============================================================= test('util-navigate.js: 能被 vm 加载,所有 export 都存在', () => { const nav = loadNavInSandbox(); const expected = [ '_toParamValueString', 'parsePageParams', 'resolveLinkUrl', 'cleanParamUrls', 'normalizeUrl', 'appendQueryParams', 'extractInternalAppPath', 'resolveNavMode', 'hasStaleUniacidInUrl', 'pickCurrentUniacid', 'getPlatform', 'shouldEncodeUrl', '_safeEncodeForPlatform', '_navigateTo', 'createNavigateApi', ]; for (const name of expected) { assert.equal(typeof nav[name], 'function', `${name} 应是 function,实际是 ${typeof nav[name]}`); } }); // ============================================================= // 2. createNavigateApi // ============================================================= test('createNavigateApi: 一次性拿到所有跳转方法', () => { const nav = loadNavInSandbox(); const api = nav.createNavigateApi(); assert.equal(typeof api.cleanParamUrls, 'function'); assert.equal(typeof api.normalizeUrl, 'function'); assert.equal(typeof api.appendQueryParams, 'function'); assert.equal(typeof api.getPlatform, 'function'); assert.equal(typeof api.shouldEncodeUrl, 'function'); assert.equal(typeof api._safeEncodeForPlatform, 'function'); assert.equal(typeof api._navigateTo, 'function'); }); // ============================================================= // 3. _toParamValueString / parsePageParams // ============================================================= test('_toParamValueString: 各种类型', () => { const { _toParamValueString } = loadNavInSandbox(); assert.equal(_toParamValueString(null), ''); assert.equal(_toParamValueString(undefined), ''); assert.equal(_toParamValueString('hello'), 'hello'); assert.equal(_toParamValueString(42), '42'); assert.equal(_toParamValueString(true), 'true'); assert.equal(_toParamValueString({ a: 1 }), '{"a":1}'); assert.equal(_toParamValueString([1, 2]), '[1,2]'); }); test('parsePageParams: JSON 字符串 / 对象 / null / 兜底', () => { const { parsePageParams } = loadNavInSandbox(); // 用 JSON.stringify 而非 deepEqual: vm 沙箱里返回的对象 prototype // 是沙箱的 Object,跟测试环境的 Object prototype 不同,deepEqual 会失败 const eq = (a, b) => assert.equal(JSON.stringify(a), JSON.stringify(b)); eq(parsePageParams('{"a":1}'), { a: 1 }); eq(parsePageParams({ a: 1 }), { a: 1 }); eq(parsePageParams(null), {}); eq(parsePageParams(''), {}); eq(parsePageParams('not json', { x: 1 }), { x: 1 }); eq(parsePageParams(undefined), {}); }); // ============================================================= // 4. resolveLinkUrl: 拆分 wap_url 的 query 进 page_params // ============================================================= test('resolveLinkUrl: 拆分 wap_url 的 query 进 page_params', () => { const { resolveLinkUrl } = loadNavInSandbox(); const link = { wap_url: 'https://host.com/path?id=1&uniacid=2' }; const url = resolveLinkUrl(link); assert.equal(url, 'https://host.com/path'); // 用 JSON.stringify 比较(避免 vm 沙箱 prototype 不一致) assert.equal(JSON.stringify(link.page_params), JSON.stringify({ id: '1', uniacid: '2' })); assert.equal(link.wap_url, 'https://host.com/path'); }); test('resolveLinkUrl: 无 wap_url 返回 null', () => { const { resolveLinkUrl } = loadNavInSandbox(); assert.equal(resolveLinkUrl({}), null); assert.equal(resolveLinkUrl(null), null); }); // ============================================================= // 5. cleanParamUrls // ============================================================= test('cleanParamUrls: 移除反引号和首尾空白', () => { const { cleanParamUrls } = loadNavInSandbox(); assert.equal( cleanParamUrls(' `https://host.com/x` '), 'https://host.com/x', ); }); test('cleanParamUrls: 任意 key / 任意嵌套层级', () => { const { cleanParamUrls } = loadNavInSandbox(); const out = cleanParamUrls({ a: { b: { c: { d: '`https://host.com/x`' } } }, }); assert.equal(out.a.b.c.d, 'https://host.com/x'); }); // ============================================================= // 6. normalizeUrl // ============================================================= test('normalizeUrl: 严格不补协议/不补 host', () => { const { normalizeUrl } = loadNavInSandbox(); assert.equal(normalizeUrl('/pages/index'), '/pages/index'); assert.equal(normalizeUrl('https://host.com/x'), 'https://host.com/x'); assert.equal(normalizeUrl('https:////host.com/x'), ''); }); test('normalizeUrl: host 小写 + 去默认端口', () => { const { normalizeUrl } = loadNavInSandbox(); assert.equal(normalizeUrl('https://HOST.com:443/x'), 'https://host.com/x'); assert.equal(normalizeUrl('http://host.com:80/x'), 'http://host.com/x'); assert.equal(normalizeUrl('https://host.com:8080/x'), 'https://host.com:8080/x'); }); // ============================================================= // 7. appendQueryParams // ============================================================= test('appendQueryParams: 基本功能', () => { const { appendQueryParams } = loadNavInSandbox(); assert.equal( appendQueryParams('https://host.com', { a: '1', b: '2' }), 'https://host.com?a=1&b=2', ); }); test('appendQueryParams: 已有 ? 时用 &', () => { const { appendQueryParams } = loadNavInSandbox(); assert.equal( appendQueryParams('https://host.com?x=1', { a: '2' }), 'https://host.com?x=1&a=2', ); }); test('appendQueryParams: 对象走 JSON.stringify', () => { const { appendQueryParams } = loadNavInSandbox(); const r = appendQueryParams('https://host.com', { obj: { x: 1 } }); assert.ok(r.includes('obj=')); }); // ============================================================= // 8. getPlatform + 测试平台注入 // ============================================================= test('getPlatform: createNavigateApi 注入 platform 强制指定', () => { const nav = loadNavInSandbox(); const api = nav.createNavigateApi({ platform: 'mp-harmony' }); assert.equal(api.getPlatform(), 'mp-harmony'); const api2 = nav.createNavigateApi({ platform: 'quickapp' }); assert.equal(api2.getPlatform(), 'quickapp'); const api3 = nav.createNavigateApi({ platform: 'h5' }); assert.equal(api3.getPlatform(), 'h5'); }); test('getPlatform: 运行时 platform 注入(模拟 systemInfo)', () => { const vm2 = require('node:vm'); // 模拟有 systemInfo 含 harmony const sb = { console }; sb.globalThis = sb; sb.systemInfo = { platform: 'harmonyos' }; // 这种方式 vm 沙箱不能直接注入,改用 process.env // 实际我们直接用 createNavigateApi 来测 const nav = loadNavInSandbox(); const api = nav.createNavigateApi(); // 默认兜底 'h5' // 注: getPlatform 的 systemInfo 注入依赖 globalThis, 沙箱里没 // createNavigateApi({ platform: 'xxx' }) 走的是 globalThis.__UTIL_NAVIGATE_TEST_PLATFORM__ const api2 = nav.createNavigateApi({ platform: 'mp-harmony' }); assert.equal(api2.getPlatform(), 'mp-harmony'); const api3 = nav.createNavigateApi({ platform: 'quickapp' }); assert.equal(api3.getPlatform(), 'quickapp'); }); // ============================================================= // 9. shouldEncodeUrl // ============================================================= test('shouldEncodeUrl: 4 平台都为 true', () => { const { shouldEncodeUrl } = loadNavInSandbox(); for (const p of ['h5', 'mp-weixin', 'mp-harmony', 'quickapp']) { assert.equal(shouldEncodeUrl(), true); } }); // ============================================================= // 10. _safeEncodeForPlatform // ============================================================= test('_safeEncodeForPlatform: H5 / 华为快应用 → 主动 encode', () => { // 2026-07-17 修正: H5 和 quickapp 平台 JS 侧必须主动 encode 一次 const nav = loadNavInSandbox(); for (const p of ['h5', 'quickapp']) { const api = nav.createNavigateApi({ platform: p }); const r = api._safeEncodeForPlatform('a&b'); assert.equal(r, 'a%26b', `${p}: & 必须 encode 成 %26,实际: ${r}`); } }); test('_safeEncodeForPlatform: 鸿蒙元服务 → JS 侧不做 encode', () => { // 2026-07-17 修正(二次): 鸿蒙整体 encode url, JS 完全不 encode const nav = loadNavInSandbox(); const api = nav.createNavigateApi({ platform: 'mp-harmony' }); const r = api._safeEncodeForPlatform('a&b'); assert.equal(r, 'a&b', `mp-harmony: JS 不应预 encode,实际: ${r}`); }); test('_safeEncodeForPlatform: 微信小程序 → JS 只 encode 分隔符', () => { // 2026-07-17 修正(二次): 微信 split on '&' 但不 decode value, JS 必须 encode `&` // 否则 query 边界被破坏。但中文 / 字母 / 数字原样保留(否则 onLoad 拿到 %xx 乱码) const nav = loadNavInSandbox(); const api = nav.createNavigateApi({ platform: 'mp-weixin' }); assert.equal(api._safeEncodeForPlatform('a&b'), 'a%26b', '& 应 encode 成 %26'); assert.equal(api._safeEncodeForPlatform('沙发'), '沙发', '中文不应 encode'); assert.equal(api._safeEncodeForPlatform('a=b'), 'a%3Db', '= 应 encode 成 %3D'); assert.equal(api._safeEncodeForPlatform('a?b'), 'a%3Fb', '? 应 encode 成 %3F'); }); test('_safeEncodeForPlatform: 已编码字符串不双编码(非微信/非鸿蒙平台)', () => { // decode roundtrip + re-encode: 入参 %E8%A1%A8(="表") // 还原后单次 encode 仍是 %E8%A1%A8 const nav = loadNavInSandbox(); for (const p of ['h5', 'quickapp']) { const api = nav.createNavigateApi({ platform: p }); const r = api._safeEncodeForPlatform('%E8%A1%A8'); assert.equal(r, '%E8%A1%A8', `${p} decode roundtrip + re-encode 不应双编码,实际: ${r}`); } }); test('_safeEncodeForPlatform: 微信/鸿蒙下已编码入参 → decode 一次还原', () => { const nav = loadNavInSandbox(); for (const p of ['mp-weixin', 'mp-harmony']) { const api = nav.createNavigateApi({ platform: p }); const r = api._safeEncodeForPlatform('%E8%A1%A8'); assert.equal(r, '表', `${p} 已编码入参应 decode 一次还原,实际: ${r}`); } }); // ============================================================= // 11. _navigateTo // ============================================================= test('_navigateTo: H5 用 window.location', () => { const nav = loadNavInSandbox(); const api = nav.createNavigateApi({ platform: 'h5' }); // H5 端 _navigateTo 内部: if (typeof window === 'undefined' || typeof window.location === 'undefined') // 所以需要传 window 全局(不是用 createNavigateApi 注入的) // 这里直接用 vm 沙箱跑 const vm2 = require('node:vm'); const moduleObj = { exports: {} }; const sb = { console, globalThis: {}, module: moduleObj, exports: moduleObj.exports }; sb.globalThis = sb; vm2.createContext(sb); vm2.runInContext(NAV_SRC.replace(/^export\s+/gm, '') + '\nmodule.exports = { getPlatform, _navigateTo };', sb); // 让 getPlatform 返回 h5 sb.globalThis.__UTIL_NAVIGATE_TEST_PLATFORM__ = 'h5'; // 重新拿方法(因为已经在上面拿过了,但 getPlatform 是在调用时才检查 globalThis) const _navigateTo = sb.module.exports._navigateTo; const calls = { navigateTo: [], redirectTo: [], reLaunch: [], switchTab: [] }; const uni = { navigateTo: (o) => calls.navigateTo.push(o), redirectTo: (o) => calls.redirectTo.push(o), reLaunch: (o) => calls.reLaunch.push(o), switchTab: (o) => calls.switchTab.push(o), }; const locationObj = { href: '', _replaced: '' }; locationObj.replace = function (u) { locationObj._replaced = u; locationObj.href = u; }; const window = { location: locationObj }; // _navigateTo 走的是 _navigateTo({ to, mode, platform }) 内部用 globalThis // 我们的 _navigateTo 实现里, h5 走 window.location.href = to // 但 util-navigate.js 里的 _navigateTo 内部是引用 'window' 全局 // 所以必须在沙箱里设 window sb.window = window; sb.uni = uni; // 直接调用 sandbox 里的 _navigateTo const r = _navigateTo({ to: '/pages/detail', mode: 'navigateTo' }); assert.equal(r.api, 'window.location'); assert.equal(locationObj.href, '/pages/detail'); }); test('_navigateTo: 微信小程序调 uni.navigateTo', () => { const calls = { navigateTo: [], redirectTo: [], reLaunch: [], switchTab: [] }; const uni = { navigateTo: (o) => calls.navigateTo.push(o), redirectTo: (o) => calls.redirectTo.push(o), reLaunch: (o) => calls.reLaunch.push(o), switchTab: (o) => calls.switchTab.push(o), }; // 微信小程序下 _navigateTo 用 globalThis.uni, 需要在 vm 沙箱里设 const vm2 = require('node:vm'); const moduleObj = { exports: {} }; const sb = { console, globalThis: {}, module: moduleObj, exports: moduleObj.exports }; sb.globalThis = sb; vm2.createContext(sb); vm2.runInContext(NAV_SRC.replace(/^export\s+/gm, '') + '\nmodule.exports = { _navigateTo };', sb); sb.globalThis.__UTIL_NAVIGATE_TEST_PLATFORM__ = 'mp-weixin'; sb.globalThis.uni = uni; sb.module.exports._navigateTo({ to: '/pages/detail', mode: 'navigateTo' }); assert.equal(calls.navigateTo.length, 1); }); test('_navigateTo: 鸿蒙元服务 tabBar 用 reLaunch 兜底', () => { const calls = { navigateTo: [], redirectTo: [], reLaunch: [], switchTab: [] }; const uni = { navigateTo: (o) => calls.navigateTo.push(o), redirectTo: (o) => calls.redirectTo.push(o), reLaunch: (o) => calls.reLaunch.push(o), switchTab: (o) => calls.switchTab.push(o), }; const vm2 = require('node:vm'); const moduleObj = { exports: {} }; const sb = { console, globalThis: {}, module: moduleObj, exports: moduleObj.exports }; sb.globalThis = sb; vm2.createContext(sb); vm2.runInContext(NAV_SRC.replace(/^export\s+/gm, '') + '\nmodule.exports = { _navigateTo };', sb); sb.globalThis.__UTIL_NAVIGATE_TEST_PLATFORM__ = 'mp-harmony'; sb.globalThis.uni = uni; sb.module.exports._navigateTo({ to: '/pages/index/index', mode: 'tabbar' }); assert.equal(calls.switchTab.length, 0, '鸿蒙不用 switchTab'); assert.equal(calls.reLaunch.length, 1, '鸿蒙用 reLaunch 兜底'); }); // ============================================================= // 12. util.js 引用 util-navigate.js 后, 方法仍可调 // ============================================================= test('util.js: 通过 import 引用 util-navigate 的方法,挂载到 default export', () => { // 我们直接验证 util.js 的 default export 里有这些方法 // (因为 util.js 是 ESM 且依赖 store/Config, vm 加载会失败, // 这里用 grep 简单检查 import 声明存在) const utilSrc = fs.readFileSync( path.join(__dirname, '..', 'common', 'js', 'util.js'), 'utf8', ); // 关键 import assert.ok(utilSrc.includes("from './util-navigate.js'"), 'util.js 应 import util-navigate.js'); assert.ok(utilSrc.includes('cleanParamUrls'), 'util.js 应 import cleanParamUrls'); assert.ok(utilSrc.includes('normalizeUrl as _normalizeUrlBase'), 'util.js 应 import normalizeUrl 别名 _normalizeUrlBase'); assert.ok(utilSrc.includes('extractInternalAppPath as _extractInternalAppPath'), 'util.js 应 import extractInternalAppPath 别名'); assert.ok(utilSrc.includes('resolveNavMode as _resolveNavMode'), 'util.js 应 import resolveNavMode 别名'); // default export 里的挂载(cleanParamUrls 直接挂, normalizeUrl 用包装函数委托) assert.ok(/cleanParamUrls,/.test(utilSrc), 'util.js 应把 cleanParamUrls 挂载到 default export'); assert.ok(utilSrc.includes('return _normalizeUrlBase(urlString)'), 'util.js 的 normalizeUrl 应委托给 _normalizeUrlBase'); }); // ============================================================= // 13. 老 API 兼容 — redirectTo / diyRedirectTo 还在 util.js // ============================================================= test('util.js: redirectTo / diyRedirectTo 仍在 util.js(没被搬走)', () => { const utilSrc = fs.readFileSync( path.join(__dirname, '..', 'common', 'js', 'util.js'), 'utf8', ); // redirectTo 函数定义 assert.ok(/^\s*redirectTo\s*\(\s*to,\s*param\s*=/m.test(utilSrc), 'redirectTo 应仍在 util.js'); // diyRedirectTo 函数定义 assert.ok(/^\s*diyRedirectTo\s*\(\s*link\s*\)\s*\{/m.test(utilSrc), 'diyRedirectTo 应仍在 util.js'); // redirectTo 内部调 _safeEncodeForPlatform(模块函数直调) assert.ok(utilSrc.includes('_safeEncodeForPlatform('), 'redirectTo 应调 _safeEncodeForPlatform'); // diyRedirectTo 内部调 this.cleanParamUrls 清理脏 link assert.ok(utilSrc.includes('this.cleanParamUrls('), 'diyRedirectTo 应调 this.cleanParamUrls'); // diyRedirectTo 内部调 _resolveNavMode + _extractInternalAppPath(内部页绝对 URL 还原) assert.ok(utilSrc.includes('_resolveNavMode('), 'diyRedirectTo 应调 _resolveNavMode'); assert.ok(utilSrc.includes('_extractInternalAppPath('), 'diyRedirectTo 应调 _extractInternalAppPath'); }); // ============================================================= // extractInternalAppPath — 内部页面绝对 URL 还原(双闸门) // 修复: 内部页被存成绝对 URL 时被误判外链 → 整页刷新 / web-view // ============================================================= test('extractInternalAppPath: 相对内部路径直接放行', () => { const nav = loadNavInSandbox(); assert.equal(nav.extractInternalAppPath('/pages_goods/detail?id=9'), '/pages_goods/detail?id=9'); assert.equal(nav.extractInternalAppPath('/pages/index/index'), '/pages/index/index'); }); test('extractInternalAppPath: 同域绝对 URL → 还原为相对路径', () => { const nav = loadNavInSandbox(); const H = ['xcx6.aigc-quickapp.com']; assert.equal( nav.extractInternalAppPath('https://xcx6.aigc-quickapp.com/hwappx/common/pages_tool/form/formdata?id=1', H), '/pages_tool/form/formdata?id=1', ); }); test('extractInternalAppPath: 同平台不同子域(域名不完全一致)→ null(严格模式)', () => { const nav = loadNavInSandbox(); const H = ['xcx6.aigc-quickapp.com']; // App 跑在 xcx6, 数据链接是 xcx3 → 视为外链 assert.equal( nav.extractInternalAppPath('https://xcx3.aigc-quickapp.com/hwappx/common/pages_tool/form/formdata', H), null, ); }); test('extractInternalAppPath: 传入完整 URL 作为白名单也能提取 host(完全一致才还原)', () => { const nav = loadNavInSandbox(); const H = ['https://xcx6.aigc-quickapp.com/']; // Config.h5Domain 形式 assert.equal( nav.extractInternalAppPath('https://xcx6.aigc-quickapp.com/hwappx/common/pages/index/index', H), '/pages/index/index', ); // host 不一致 → null assert.equal( nav.extractInternalAppPath('https://xcx3.aigc-quickapp.com/hwappx/common/pages/index/index', H), null, ); }); test('extractInternalAppPath: 外链域名不同(Shopify / Facebook)→ 不误伤, 返回 null', () => { const nav = loadNavInSandbox(); const H = ['xcx6.aigc-quickapp.com']; assert.equal(nav.extractInternalAppPath('https://xxx.myshopify.com/pages/about-us', H), null); assert.equal(nav.extractInternalAppPath('https://www.facebook.com/pages/Name/123', H), null); assert.equal(nav.extractInternalAppPath('https://example.com/pages_tool/x', H), null); }); test('extractInternalAppPath: 绝对 URL 但路径无 /pages 段 → null(真外链)', () => { const nav = loadNavInSandbox(); const H = ['xcx6.aigc-quickapp.com']; assert.equal(nav.extractInternalAppPath('https://xcx6.aigc-quickapp.com/activity/2026', H), null); }); test('extractInternalAppPath: 无白名单时绝对 URL 一律 null(不猜测)', () => { const nav = loadNavInSandbox(); assert.equal(nav.extractInternalAppPath('https://xcx3.aigc-quickapp.com/hwappx/common/pages/index/index'), null); assert.equal(nav.extractInternalAppPath('https://xcx3.aigc-quickapp.com/hwappx/common/pages/index/index', []), null); }); test('extractInternalAppPath: 空/非法输入安全返回 null', () => { const nav = loadNavInSandbox(); assert.equal(nav.extractInternalAppPath('', ['a.com']), null); assert.equal(nav.extractInternalAppPath(null, ['a.com']), null); assert.equal(nav.extractInternalAppPath(undefined), null); }); test('extractInternalAppPath: host 大小写不敏感 + 忽略端口', () => { const nav = loadNavInSandbox(); const H = ['xcx6.aigc-quickapp.com']; // host 大写 assert.equal( nav.extractInternalAppPath('https://XCX6.AIGC-QUICKAPP.COM/hwappx/common/pages/index/index', H), '/pages/index/index', ); // 带端口(白名单不带端口, 也应命中) assert.equal( nav.extractInternalAppPath('https://xcx6.aigc-quickapp.com:8443/hwappx/common/pages_goods/detail', H), '/pages_goods/detail', ); }); test('extractInternalAppPath: 各分包页面根都能识别 + 保留 hash', () => { const nav = loadNavInSandbox(); const H = ['h.com']; assert.equal(nav.extractInternalAppPath('https://h.com/base/pages_promotion/point/list', H), '/pages_promotion/point/list'); assert.equal(nav.extractInternalAppPath('https://h.com/base/pages_order/detail?id=2', H), '/pages_order/detail?id=2'); assert.equal(nav.extractInternalAppPath('/pages/index/index#top'), '/pages/index/index#top'); }); // ============================================================= // extractInternalAppPath: 强制内部(allowedHosts === true 跳过 host 校验) // ============================================================= test('extractInternalAppPath: allowedHosts=true → 跳过 host 校验(强制内部)', () => { const nav = loadNavInSandbox(); // 不同子域, 正常严格模式下会 null; 传 true 强制还原 assert.equal( nav.extractInternalAppPath('https://xcx3.aigc-quickapp.com/hwappx/common/pages_tool/form/formdata?id=1', true), '/pages_tool/form/formdata?id=1', ); // 完全陌生域名也能被强制还原(调用方已明确表示是内部页) assert.equal( nav.extractInternalAppPath('https://any-host.example.net/x/pages/index/index', true), '/pages/index/index', ); // 但仍要求路径带 /pages 段, 没有则 null assert.equal(nav.extractInternalAppPath('https://any-host.example.net/activity/2026', true), null); }); // ============================================================= // resolveNavMode — 统一开关 open_mode(internal/online/auto) // ============================================================= test('resolveNavMode: open_mode=internal/spa → internal', () => { const { resolveNavMode } = loadNavInSandbox(); assert.equal(resolveNavMode({ open_mode: 'internal' }), 'internal'); assert.equal(resolveNavMode({ open_mode: 'SPA' }), 'internal'); assert.equal(resolveNavMode({ openMode: 'internal' }), 'internal'); assert.equal(resolveNavMode({ page_params: { open_mode: 'internal' } }), 'internal'); }); test('resolveNavMode: open_mode=online/web → online', () => { const { resolveNavMode } = loadNavInSandbox(); assert.equal(resolveNavMode({ open_mode: 'online' }), 'online'); assert.equal(resolveNavMode({ open_mode: 'WEB' }), 'online'); assert.equal(resolveNavMode({ page_params: { open_mode: 'web' } }), 'online'); }); test('resolveNavMode: 未设置 / 未知值 → auto', () => { const { resolveNavMode } = loadNavInSandbox(); assert.equal(resolveNavMode({}), 'auto'); assert.equal(resolveNavMode({ open_mode: 'xxx' }), 'auto'); assert.equal(resolveNavMode(null), 'auto'); assert.equal(resolveNavMode(undefined), 'auto'); }); // ============================================================= // hasStaleUniacidInUrl — URL 原生 uniacid vs 系统当前 uniacid // 修复: 装修数据里硬编码的 uniacid 可能与系统不一致(脏数据 / 过期租户) // ============================================================= test('hasStaleUniacidInUrl: URL 无 uniacid → false', () => { const { hasStaleUniacidInUrl } = loadNavInSandbox(); assert.equal(hasStaleUniacidInUrl('https://partner.com/landing?id=130', 2482), false); assert.equal(hasStaleUniacidInUrl('https://partner.com/landing', 2482), false); }); test('hasStaleUniacidInUrl: URL uniacid 与当前一致 → false', () => { const { hasStaleUniacidInUrl } = loadNavInSandbox(); assert.equal(hasStaleUniacidInUrl('https://partner.com/landing?uniacid=2482', 2482), false); assert.equal(hasStaleUniacidInUrl('https://partner.com/landing?uniacid=2482', '2482'), false); // 字符串数字 assert.equal(hasStaleUniacidInUrl('https://partner.com/landing?uniacid=2482&id=130', 2482), false); }); test('hasStaleUniacidInUrl: URL uniacid 与当前不一致 → true(脏数据)', () => { const { hasStaleUniacidInUrl } = loadNavInSandbox(); assert.equal(hasStaleUniacidInUrl('https://partner.com/landing?uniacid=2482', 2483), true); assert.equal(hasStaleUniacidInUrl('https://partner.com/landing?uniacid=2482&id=130', 9999), true); }); test('hasStaleUniacidInUrl: 兼容 URL 编码 + 大小写 + 多个 query', () => { const { hasStaleUniacidInUrl } = loadNavInSandbox(); assert.equal(hasStaleUniacidInUrl('https://partner.com/x?UNIACID=2482', 2482), false); assert.equal(hasStaleUniacidInUrl('https://partner.com/x?uniacid=%32%34%38%32', 2482), false); // 全 URL 编码 assert.equal(hasStaleUniacidInUrl('https://partner.com/x?uniacid=2482&a=1&b=2', 2482), false); assert.equal(hasStaleUniacidInUrl('https://partner.com/x?a=1&uniacid=2482&b=2', 2482), false); }); test('hasStaleUniacidInUrl: currentUniacid 无效 → false(无法比较)', () => { const { hasStaleUniacidInUrl } = loadNavInSandbox(); assert.equal(hasStaleUniacidInUrl('https://partner.com/x?uniacid=2482', null), false); assert.equal(hasStaleUniacidInUrl('https://partner.com/x?uniacid=2482', undefined), false); assert.equal(hasStaleUniacidInUrl('https://partner.com/x?uniacid=2482', ''), false); assert.equal(hasStaleUniacidInUrl('https://partner.com/x?uniacid=2482', 0), false); }); test('hasStaleUniacidInUrl: 空 / 非法输入 → false(安全降级)', () => { const { hasStaleUniacidInUrl } = loadNavInSandbox(); assert.equal(hasStaleUniacidInUrl('', 2482), false); assert.equal(hasStaleUniacidInUrl(null, 2482), false); assert.equal(hasStaleUniacidInUrl(undefined, 2482), false); }); // ============================================================= // pickCurrentUniacid — 从两个候选中选 uniacid // 实际调用: (uni.getStorageSync('uniacid'), Config.uniacid) // 必须与 http.js 行 168: data.uniacid = uni.getStorageSync('uniacid') || Config.uniacid // 保持完全一致, 避免守卫/请求用不同真值 // 优先级: 第一参(运行时) > 第二参(编译期兜底) > 0 // ============================================================= test('pickCurrentUniacid: 第一参(运行时 storage)非 0 → 选第一参', () => { const { pickCurrentUniacid } = loadNavInSandbox(); assert.equal(pickCurrentUniacid(2483, 2482), 2483); assert.equal(pickCurrentUniacid('2483', 2482), 2483); // 字符串数字兼容 }); test('pickCurrentUniacid: 第一参无值 → 回退到第二参(Config 兜底)', () => { const { pickCurrentUniacid } = loadNavInSandbox(); assert.equal(pickCurrentUniacid(undefined, 2482), 2482); assert.equal(pickCurrentUniacid(null, 2482), 2482); assert.equal(pickCurrentUniacid('', 2482), 2482); // '' 视为无效 }); test('pickCurrentUniacid: 第一参为 0 → 回退到第二参(关键边界)', () => { const { pickCurrentUniacid } = loadNavInSandbox(); // 关键: storage 返回 0(空值)等价于"未设置", 必须回退到 Config.uniacid // 不能因为 storage 是 0 而把 0 当成"已设值"返回 assert.equal(pickCurrentUniacid(0, 2482), 2482); assert.equal(pickCurrentUniacid(0, '2482'), 2482); }); test('pickCurrentUniacid: 两个参都无值 → 0', () => { const { pickCurrentUniacid } = loadNavInSandbox(); assert.equal(pickCurrentUniacid(undefined, undefined), 0); assert.equal(pickCurrentUniacid(null, null), 0); assert.equal(pickCurrentUniacid(0, 0), 0); assert.equal(pickCurrentUniacid('', ''), 0); }); test('pickCurrentUniacid: 字符串数字自动 parseInt', () => { const { pickCurrentUniacid } = loadNavInSandbox(); assert.equal(pickCurrentUniacid('2482', undefined), 2482); assert.equal(pickCurrentUniacid('2482abc', undefined), 2482); // 解析到非数字停止 });