Files
lucky_shop/tests/util-navigate.test.js
ZF sun 1143ef7b72 fix(util-navigate): 完善鸿蒙元服务编码逻辑注释与说明
更新了util-navigate和对应测试文件的注释,补充鸿蒙原生编码的细节,说明双重编码的问题以及&符号的处理逻辑,修正了注释里的重复表述
2026-07-18 09:24:10 +08:00

647 lines
31 KiB
JavaScript

/**
* 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
// (再 encode 会双重编码; webview src 含 & 时,鸿蒙原生 encode 会自动变 %26,webview 能正常打开)
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); // 解析到非数字停止
});