重构跨平台参数编码逻辑: 1. 微信小程序/鸿蒙元服务:仅处理编码边界符和decode roundtrip,不主动编码,交由平台原生处理 2. H5/华为快应用:保留主动编码逻辑,适配浏览器query解析规则 3. 新增专项测试用例覆盖搜索关键词跨端流转场景,修复商品分类页跳转列表页关键词显示乱码的bug
646 lines
31 KiB
JavaScript
646 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
|
|
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); // 解析到非数字停止
|
|
});
|