重构跨平台参数编码逻辑: 1. 微信小程序/鸿蒙元服务:仅处理编码边界符和decode roundtrip,不主动编码,交由平台原生处理 2. H5/华为快应用:保留主动编码逻辑,适配浏览器query解析规则 3. 新增专项测试用例覆盖搜索关键词跨端流转场景,修复商品分类页跳转列表页关键词显示乱码的bug
481 lines
22 KiB
JavaScript
481 lines
22 KiB
JavaScript
/**
|
||
* 搜索关键词跨端编码测试
|
||
*
|
||
* 回归 Bug(2026-07-17):
|
||
* 商品分类页(/pages_goods/category)输入搜索关键词 → 点击搜索 → 跳到
|
||
* 商品列表页(/pages_goods/list),列表页搜索栏显示的关键词变成
|
||
* %E6%B2%99%E5%8F%91(对应"沙发")乱码。
|
||
*
|
||
* 根因: 微信小程序 / 鸿蒙元服务(mp-weixin / mp-harmony)下,
|
||
* uni.navigateTo / router.pushUrl 内部会自动对 url 整体做一次
|
||
* encodeURIComponent。util-navigate.js 的 `_safeEncodeForPlatform` 之前
|
||
* 在所有平台都主动 encode,导致微信/鸿蒙下 JS encode + 平台原生 encode
|
||
* = 双重编码。目标页 onLoad(options) 只 decode 一次后,拿到的还是
|
||
* `%E6%B2%99%E5%8F%91` 这种"已编码字符串",在 <input v-model="keyword">
|
||
* 上显示成乱码。
|
||
*
|
||
* 修复策略:
|
||
* - 微信小程序 + 鸿蒙元服务(mp-weixin / mp-harmony): `_safeEncodeForPlatform`
|
||
* 不做 encode,只做 decode roundtrip(防止上游重复 encode 入参)。让平台原生
|
||
* 去做唯一一次 encode。
|
||
* - H5 / 华为快应用: 仍然主动 encode 一次(防浏览器 query 错位)。
|
||
*
|
||
* 跑法:
|
||
* node --test tests/search-keyword-encoding.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。
|
||
* createNavigateApi({ platform }) 可强制注入平台到 globalThis。
|
||
*/
|
||
function loadNavInSandbox() {
|
||
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. _safeEncodeForPlatform 平台差异化
|
||
// =============================================================
|
||
test('_safeEncodeForPlatform: 鸿蒙元服务 → JS 侧不做 encode(让平台去做)', () => {
|
||
const nav = loadNavInSandbox();
|
||
const api = nav.createNavigateApi({ platform: 'mp-harmony' });
|
||
|
||
// 中文关键词原样返回(鸿蒙原生会再做一次 encode)
|
||
const r1 = api._safeEncodeForPlatform('沙发');
|
||
assert.equal(r1, '沙发', `mp-harmony 中文应原样返回,实际: ${r1}`);
|
||
|
||
// 含 & 的值原样返回(不能再 encode,否则双重编码)
|
||
const r2 = api._safeEncodeForPlatform('a&b=c');
|
||
assert.equal(r2, 'a&b=c', `mp-harmony 特殊字符应原样返回,实际: ${r2}`);
|
||
|
||
// 反引号 / 空格原样返回
|
||
const r3 = api._safeEncodeForPlatform('`hello world`');
|
||
assert.equal(r3, '`hello world`', `mp-harmony 反引号空格应原样返回,实际: ${r3}`);
|
||
});
|
||
|
||
test('_safeEncodeForPlatform: 鸿蒙下已编码入参 → decode 一次还原(避免双编码)', () => {
|
||
const nav = loadNavInSandbox();
|
||
const api = nav.createNavigateApi({ platform: 'mp-harmony' });
|
||
|
||
// 上游已 encode 过一次的入参(%E5%8F%91 是"发")
|
||
const r = api._safeEncodeForPlatform('%E5%8F%91');
|
||
assert.equal(r, '发', `mp-harmony 已编码入参应 decode 一次,实际: ${r}`);
|
||
});
|
||
|
||
test('_safeEncodeForPlatform: H5 → JS 侧主动 encode', () => {
|
||
const nav = loadNavInSandbox();
|
||
const api = nav.createNavigateApi({ platform: 'h5' });
|
||
|
||
const r1 = api._safeEncodeForPlatform('沙发');
|
||
assert.equal(r1, '%E6%B2%99%E5%8F%91', `h5 中文应 encode,实际: ${r1}`);
|
||
|
||
const r2 = api._safeEncodeForPlatform('a&b');
|
||
assert.equal(r2, 'a%26b', `h5 & 应 encode 成 %26,实际: ${r2}`);
|
||
});
|
||
|
||
test('_safeEncodeForPlatform: 微信小程序 → JS 只 encode 分隔符 (不 encode 中文)', () => {
|
||
// 2026-07-17 修正(二次): 微信小程序 split on '&',不 decode value。
|
||
// JS 只 encode `& = ? # \s`(防 query 边界被破坏),
|
||
// 中文 / 字母 / 数字 原样保留(防 onLoad 拿到 %xx 乱码)。
|
||
const nav = loadNavInSandbox();
|
||
const api = nav.createNavigateApi({ platform: 'mp-weixin' });
|
||
|
||
// 中文 原样保留
|
||
const r1 = api._safeEncodeForPlatform('沙发');
|
||
assert.equal(r1, '沙发', `mp-weixin 中文应原样,实际: ${r1}`);
|
||
|
||
// & encode 成 %26(防 query 边界被破坏)
|
||
const r2 = api._safeEncodeForPlatform('a&b');
|
||
assert.equal(r2, 'a%26b', `mp-weixin & 应 encode 成 %26,实际: ${r2}`);
|
||
|
||
// = 也 encode
|
||
const r3 = api._safeEncodeForPlatform('a=b');
|
||
assert.equal(r3, 'a%3Db', `mp-weixin = 应 encode 成 %3D,实际: ${r3}`);
|
||
|
||
// ? 也 encode
|
||
const r4 = api._safeEncodeForPlatform('a?b');
|
||
assert.equal(r4, 'a%3Fb', `mp-weixin ? 应 encode 成 %3F,实际: ${r4}`);
|
||
|
||
// URL 内的 & 和 = 在 mp-weixin 下会被 encode,但 :// 保留
|
||
const r5 = api._safeEncodeForPlatform('https://host.com?a=1&b=2');
|
||
assert.equal(r5, 'https://host.com%3Fa%3D1%26b%3D2', `mp-weixin URL 应 encode 分隔符,实际: ${r5}`);
|
||
});
|
||
|
||
test('_safeEncodeForPlatform: 华为快应用 → JS 侧主动 encode', () => {
|
||
const nav = loadNavInSandbox();
|
||
const api = nav.createNavigateApi({ platform: 'quickapp' });
|
||
|
||
const r1 = api._safeEncodeForPlatform('沙发');
|
||
assert.equal(r1, '%E6%B2%99%E5%8F%91', `quickapp 中文应 encode,实际: ${r1}`);
|
||
});
|
||
|
||
test('_safeEncodeForPlatform: 已编码字符串不双编码(非微信/非鸿蒙平台)', () => {
|
||
// 2026-07-17 修正: 微信小程序也属于"不做 JS encode"平台,跟鸿蒙一样
|
||
const nav = loadNavInSandbox();
|
||
// decode roundtrip + re-encode: 入参 %E8%A1%A8(="表")
|
||
// 还原后单次 encode 仍是 %E8%A1%A8
|
||
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 一次还原', () => {
|
||
// 上游已经 encode 过一次的入参(防御),防止平台原生 encode 后变成双编码
|
||
const nav = loadNavInSandbox();
|
||
const api = nav.createNavigateApi({ platform: 'mp-weixin' });
|
||
const r = api._safeEncodeForPlatform('%E5%8F%91'); // '发' 已编码
|
||
assert.equal(r, '发', `mp-weixin 已编码入参应 decode 一次,实际: ${r}`);
|
||
});
|
||
|
||
// =============================================================
|
||
// 2. 端到端模拟: 搜索关键词 → redirectTo → 模拟 onLoad(options)
|
||
// 关键: 验证 JS 编码一次 + 平台编码/解码 一次后,onLoad 拿到的 keyword 是原文
|
||
// =============================================================
|
||
|
||
/**
|
||
* 模拟 redirectTo 的拼装逻辑(同 util.js 中的实现)
|
||
* - 鸿蒙下用 safeEncodeForPlatform(原文返回)
|
||
* - 其他平台用 safeEncodeForPlatform(encode 一次)
|
||
*/
|
||
function buildRedirectUrl(to, param, encode) {
|
||
const queryIdx = to.indexOf('?');
|
||
const path = queryIdx === -1 ? to : to.substring(0, queryIdx);
|
||
const queryStr = Object.keys(param)
|
||
.map(k => encode(k) + '=' + encode(param[k]))
|
||
.join('&');
|
||
return path + (queryStr ? '?' + queryStr : '');
|
||
}
|
||
|
||
/**
|
||
* 模拟鸿蒙元服务对 url 的处理:
|
||
* - JS 传入"未编码"的 url,鸿蒙原生会先对整个 query 字符串做一次 encodeURIComponent
|
||
* (把 = & 这些分隔符也一起编码),然后解析 query 时再 decode 一次
|
||
* - 最终 onLoad(options) 拿到的 keyword 是原文
|
||
*
|
||
* 关键: 鸿蒙原生 encode 整段 query,不会把 = 和 & 当分隔符提前 split
|
||
* (这跟浏览器/H5 的 queryString 解析方式不同)
|
||
*/
|
||
function simulateHarmonyOnLoad(url) {
|
||
const qIdx = url.indexOf('?');
|
||
if (qIdx === -1) return { options: {} };
|
||
const queryStr = url.substring(qIdx + 1);
|
||
// 鸿蒙原生: 先对整段 query 字符串做 encodeURIComponent(= & 都会变 %xx)
|
||
const encodedQueryStr = encodeURIComponent(queryStr);
|
||
// 模拟解析 encoded 字符串: 把 %26 解码回 & 再 split
|
||
// 关键洞察: encodeURIComponent 后的字符串是安全的(没有裸 = &),所以可以
|
||
// 直接 decode 然后 split
|
||
const decodedBack = decodeURIComponent(encodedQueryStr);
|
||
const options = {};
|
||
decodedBack.split('&').forEach(kv => {
|
||
const eqIdx = kv.indexOf('=');
|
||
if (eqIdx === -1) return;
|
||
const k = kv.substring(0, eqIdx);
|
||
const v = kv.substring(eqIdx + 1);
|
||
options[k] = v;
|
||
});
|
||
return { options };
|
||
}
|
||
|
||
/**
|
||
* 模拟微信小程序对 url 的处理(2026-07-17 修正):
|
||
* - JS 只 encode URL 分隔符 (& = ? # \s),中文和字母数字原样保留
|
||
* - 微信小程序: 不 encode 整体 url, 只 split on '&' / '=',对 value 做 decodeURIComponent
|
||
* - 结果: 中文 keyword 原样<E58E9F><E6A0B7>到 onLoad(不会显示乱码),
|
||
* src 中的 & 已被 JS encode 成 %26,微信分解析后 onLoad 拿到 %26(而非原 &),
|
||
* 但 webview 只接接收 src 作为 URL 参数,浏览器 fetch 时会 decode %26 为 &,浏览器端解析正常
|
||
*/
|
||
function simulateWeixinOnLoad(url) {
|
||
const qIdx = url.indexOf('?');
|
||
if (qIdx === -1) return { options: {} };
|
||
const queryStr = url.substring(qIdx + 1);
|
||
const options = {};
|
||
// 微信小程序: 不 auto-encode, 只 split on '&' / '=',对 value decodeURIComponent
|
||
queryStr.split('&').forEach(kv => {
|
||
const eqIdx = kv.indexOf('=');
|
||
if (eqIdx === -1) return;
|
||
const rawK = kv.substring(0, eqIdx);
|
||
const rawV = kv.substring(eqIdx + 1);
|
||
try {
|
||
options[decodeURIComponent(rawK)] = decodeURIComponent(rawV);
|
||
} catch (e) {
|
||
options[rawK] = rawV;
|
||
}
|
||
});
|
||
return { options };
|
||
}
|
||
|
||
/**
|
||
* 模拟 H5 浏览器 / 华为快应用 Webview 对 url 的处理:
|
||
* - JS 已经主动 encode 过整段 query 字符串(关键步骤)
|
||
* - 浏览器解析 url 时 split on '&' 和 '=',然后对每段 value 做 decodeURIComponent
|
||
* - 最终 onLoad(options) 拿到的 keyword 是原文
|
||
*/
|
||
function simulateBrowserOnLoad(url) {
|
||
const qIdx = url.indexOf('?');
|
||
if (qIdx === -1) return { options: {} };
|
||
const queryStr = url.substring(qIdx + 1);
|
||
const options = {};
|
||
queryStr.split('&').forEach(kv => {
|
||
const eqIdx = kv.indexOf('=');
|
||
if (eqIdx === -1) return;
|
||
const rawK = kv.substring(0, eqIdx);
|
||
const rawV = kv.substring(eqIdx + 1);
|
||
// 浏览器 split 之后,对 value 做 decodeURIComponent
|
||
try {
|
||
options[decodeURIComponent(rawK)] = decodeURIComponent(rawV);
|
||
} catch (e) {
|
||
options[rawK] = rawV;
|
||
}
|
||
});
|
||
return { options };
|
||
}
|
||
|
||
// =============================================================
|
||
// 2.1 端到端:鸿蒙元服务(回归 Bug 的主要平台)
|
||
// =============================================================
|
||
test('端到端: 鸿蒙元服务 → 搜索关键词"沙发"在列表页 search 输入框显示原文(不乱码)', () => {
|
||
const nav = loadNavInSandbox();
|
||
const api = nav.createNavigateApi({ platform: 'mp-harmony' });
|
||
const encode = (v) => api._safeEncodeForPlatform(v);
|
||
|
||
// 模拟 /pages_tool/goods/search 中的 search() 方法:
|
||
// this.$util.redirectTo('/pages_goods/list', { keyword: '沙发' });
|
||
const url = buildRedirectUrl('/pages_goods/list', { keyword: '沙发' }, encode);
|
||
|
||
// 鸿蒙下 url 不应被双重编码: keyword= 应直接是中文(没 %)
|
||
assert.ok(!url.includes('%'),
|
||
`鸿蒙下 url 不应含 %xx(JS 不应预 encode),实际 url: ${url}`);
|
||
assert.ok(url.includes('keyword=沙发'),
|
||
`鸿蒙下 url 应含 keyword=沙发,实际: ${url}`);
|
||
|
||
// 鸿蒙原生自动 encode 一次,然后解析时 decode 一次,最终 options.keyword = '沙发'
|
||
const { options } = simulateHarmonyOnLoad(url);
|
||
assert.equal(options.keyword, '沙发',
|
||
`鸿蒙下 onLoad(options).keyword 应是原文,实际: ${options.keyword}`);
|
||
});
|
||
|
||
test('端到端: 鸿蒙元服务 → 含中文 + 反引号 + 空格的关键词在列表页 search 显示原文', () => {
|
||
// 注: 鸿蒙元服务下 JS 侧不做 encode(让鸿蒙原生 encode),
|
||
// 对于含 URL 分隔符 (& = ? #) 的关键词,需要业务方自己 encode,
|
||
// 否则会被 native 当成 query 分隔符破坏边界。
|
||
// 本测试覆盖用户实际场景: 中文 + 反引号 + 空格(鸿蒙原生都能正常 encode 处理)
|
||
const nav = loadNavInSandbox();
|
||
const api = nav.createNavigateApi({ platform: 'mp-harmony' });
|
||
const encode = (v) => api._safeEncodeForPlatform(v);
|
||
|
||
const kw = '`hello world` 沙发';
|
||
const url = buildRedirectUrl('/pages_goods/list', { keyword: kw }, encode);
|
||
assert.ok(url.includes('keyword=' + kw),
|
||
`鸿蒙下 url 应含原始 keyword,实际: ${url}`);
|
||
|
||
const { options } = simulateHarmonyOnLoad(url);
|
||
assert.equal(options.keyword, kw,
|
||
`鸿蒙下 onLoad(options).keyword 应是原文,实际: ${options.keyword}`);
|
||
});
|
||
|
||
// =============================================================
|
||
// 2.2 端到端:微信小程序(回归 Bug 的另一平台)
|
||
// =============================================================
|
||
test('端到端: 微信小程序 → 搜索关键词"沙发"在列表页 search 输入框显示原文(不乱码)', () => {
|
||
// 2026-07-17 修正: 微信小程序跟鸿蒙一样,JS 不做 encode,让平台原生去 encode
|
||
const nav = loadNavInSandbox();
|
||
const api = nav.createNavigateApi({ platform: 'mp-weixin' });
|
||
const encode = (v) => api._safeEncodeForPlatform(v);
|
||
|
||
const url = buildRedirectUrl('/pages_goods/list', { keyword: '沙发' }, encode);
|
||
|
||
// 微信下 url 不应被双重编码: keyword= 应直接是中文(没 %)
|
||
assert.ok(!url.includes('%'),
|
||
`微信下 url 不应含 %xx(JS 不应预 encode),实际 url: ${url}`);
|
||
assert.ok(url.includes('keyword=沙发'),
|
||
`微信下 url 应含 keyword=沙发,实际: ${url}`);
|
||
|
||
// 微信原生自动 encode 一次,然后解析时 decode 一次,最终 options.keyword = '沙发'
|
||
const { options } = simulateWeixinOnLoad(url);
|
||
assert.equal(options.keyword, '沙发',
|
||
`微信下 onLoad(options).keyword 应是原文,实际: ${options.keyword}`);
|
||
});
|
||
|
||
test('端到端: 微信小程序 → 含中文 + 反引号 + 空格的关键词在列表页 search 显示原文', () => {
|
||
const nav = loadNavInSandbox();
|
||
const api = nav.createNavigateApi({ platform: 'mp-weixin' });
|
||
const encode = (v) => api._safeEncodeForPlatform(v);
|
||
|
||
const kw = '`hello world` 沙发';
|
||
const url = buildRedirectUrl('/pages_goods/list', { keyword: kw }, encode);
|
||
// mp-weixin 下 JS 会 encode 空格(防 query 边界), 中文 / 反引号 原样
|
||
assert.ok(url.includes('keyword=' + encode('`hello world` 沙发')),
|
||
`微信下 url 应含 JS 编码后的 keyword(空格 → %20),实际: ${url}`);
|
||
|
||
const { options } = simulateWeixinOnLoad(url);
|
||
// decode 之后应还原 keyword(空格 回到)
|
||
assert.equal(options.keyword, kw,
|
||
`微信下 onLoad(options).keyword 应是 decode 后的原文,实际: ${options.keyword}`);
|
||
});
|
||
|
||
// =============================================================
|
||
// 2.3 端到端:H5
|
||
// =============================================================
|
||
test('端到端: H5 → 搜索关键词在列表页 search 输入框显示原文', () => {
|
||
const nav = loadNavInSandbox();
|
||
const api = nav.createNavigateApi({ platform: 'h5' });
|
||
const encode = (v) => api._safeEncodeForPlatform(v);
|
||
|
||
const url = buildRedirectUrl('/pages_goods/list', { keyword: '沙发' }, encode);
|
||
assert.ok(url.includes('keyword=%E6%B2%99%E5%8F%91'),
|
||
`H5 下 url 应含编码后的 keyword,实际: ${url}`);
|
||
|
||
// H5 浏览器从 query string 自动 decode
|
||
const { options } = simulateBrowserOnLoad(url);
|
||
assert.equal(options.keyword, '沙发',
|
||
`H5 下 browser decode 后 keyword 应是原文,实际: ${options.keyword}`);
|
||
});
|
||
|
||
// =============================================================
|
||
// 2.4 端到端:华为快应用
|
||
// =============================================================
|
||
test('端到端: 快应用 → 搜索关键词在列表页 search 输入框显示原文', () => {
|
||
const nav = loadNavInSandbox();
|
||
const api = nav.createNavigateApi({ platform: 'quickapp' });
|
||
const encode = (v) => api._safeEncodeForPlatform(v);
|
||
|
||
const url = buildRedirectUrl('/pages_goods/list', { keyword: '沙发' }, encode);
|
||
assert.ok(url.includes('keyword=%E6%B2%99%E5%8F%91'),
|
||
`快应用下 url 应含编码后的 keyword,实际: ${url}`);
|
||
|
||
const { options } = simulateBrowserOnLoad(url);
|
||
assert.equal(options.keyword, '沙发',
|
||
`快应用下 decode 后 keyword 应是原文,实际: ${options.keyword}`);
|
||
});
|
||
|
||
// =============================================================
|
||
// 3. 模拟完整的 bug 现场:商品分类 → 商品列表
|
||
// =============================================================
|
||
test('Bug 回归场景: 商品分类页输入"沙发" → 列表页搜索框显示"沙发"(4 平台)', () => {
|
||
const nav = loadNavInSandbox();
|
||
|
||
// 模拟 search.vue 中的 search() 方法
|
||
function searchClick(platform, keyword) {
|
||
const api = nav.createNavigateApi({ platform });
|
||
const encode = (v) => api._safeEncodeForPlatform(v);
|
||
// 模拟 redirectTo 拼装 url(纯函数,与 util.js redirectTo 同语义)
|
||
const url = buildRedirectUrl('/pages_goods/list', { keyword }, encode);
|
||
|
||
// 模拟 onLoad(options) 拿到的 keyword(按平台分派)
|
||
if (platform === 'mp-harmony') {
|
||
// 鸿蒙: 平台原生 encode 一次,parse 时 decode 一次
|
||
return simulateHarmonyOnLoad(url).options.keyword;
|
||
} else if (platform === 'mp-weixin') {
|
||
// 微信: 不 auto-encode, split on '&' / '=',对 value decodeURIComponent
|
||
return simulateWeixinOnLoad(url).options.keyword;
|
||
} else {
|
||
// H5 / 快应用: 浏览器/H5 风格 parse,decode 一次
|
||
return simulateBrowserOnLoad(url).options.keyword;
|
||
}
|
||
}
|
||
|
||
for (const p of ['h5', 'mp-weixin', 'mp-harmony', 'quickapp']) {
|
||
const keyword = searchClick(p, '沙发');
|
||
assert.equal(keyword, '沙发',
|
||
`${p} 平台:onLoad(options).keyword 应是"沙发",实际: ${keyword}`);
|
||
}
|
||
});
|
||
|
||
test('Bug 回归场景: 长关键词 + 特殊字符,4 平台都不应乱码', () => {
|
||
const nav = loadNavInSandbox();
|
||
|
||
function searchClick(platform, keyword) {
|
||
const api = nav.createNavigateApi({ platform });
|
||
const encode = (v) => api._safeEncodeForPlatform(v);
|
||
const url = buildRedirectUrl('/pages_goods/list', { keyword }, encode);
|
||
|
||
if (platform === 'mp-harmony') {
|
||
return simulateHarmonyOnLoad(url).options.keyword;
|
||
} else if (platform === 'mp-weixin') {
|
||
return simulateWeixinOnLoad(url).options.keyword;
|
||
} else {
|
||
return simulateBrowserOnLoad(url).options.keyword;
|
||
}
|
||
}
|
||
|
||
// 各种边界关键词
|
||
// 注: 不含 & = ? # 等 URL 分隔符(这些字符即使在鸿蒙下也应业务方自行 encode)
|
||
const keywords = [
|
||
'沙发', // 纯中文(本次 Bug 的核心场景)
|
||
'Nike 运动鞋', // 中英文 + 空格
|
||
'`反引号`', // 含反引号
|
||
'100%纯棉', // 含 %
|
||
'价格>100', // 含 >
|
||
'a b c', // 多空格
|
||
];
|
||
|
||
for (const p of ['h5', 'mp-weixin', 'mp-harmony', 'quickapp']) {
|
||
for (const kw of keywords) {
|
||
const got = searchClick(p, kw);
|
||
assert.equal(got, kw,
|
||
`${p} 平台: 关键词 "${kw}" 应原样传递,实际: "${got}"`);
|
||
}
|
||
}
|
||
});
|
||
|
||
// =============================================================
|
||
// 4. 防御:微信/鸿蒙下不能误伤已经预 encode 的入参
|
||
// =============================================================
|
||
test('防御: 鸿蒙下已 encode 的入参 → decode 一次还原(避免平台 encode 后变成双编码)', () => {
|
||
const nav = loadNavInSandbox();
|
||
const api = nav.createNavigateApi({ platform: 'mp-harmony' });
|
||
const encode = (v) => api._safeEncodeForPlatform(v);
|
||
|
||
// 上游已经 encode 过一次的入参(%E5%8F%91 是"发")
|
||
const url = buildRedirectUrl('/pages_goods/list', { keyword: '%E5%8F%91' }, encode);
|
||
// 应该 decode 还原成"发",而不是保留 %E5%8F%91
|
||
assert.ok(url.includes('keyword=发'),
|
||
`鸿蒙下已编码入参应被 decode 还原,实际 url: ${url}`);
|
||
|
||
// 然后鸿蒙原生 encode 一次,parse decode 一次,回到"发"
|
||
const { options } = simulateHarmonyOnLoad(url);
|
||
assert.equal(options.keyword, '发',
|
||
`鸿蒙下 decode-encode-decode 一次后应是原文,实际: ${options.keyword}`);
|
||
});
|
||
|
||
test('防御: 微信下已 encode 的入参 → decode 一次还原(避免平台 encode 后变成双编码)', () => {
|
||
// 2026-07-17 修正: 微信小程序同鸿蒙一样
|
||
const nav = loadNavInSandbox();
|
||
const api = nav.createNavigateApi({ platform: 'mp-weixin' });
|
||
const encode = (v) => api._safeEncodeForPlatform(v);
|
||
|
||
const url = buildRedirectUrl('/pages_goods/list', { keyword: '%E5%8F%91' }, encode);
|
||
assert.ok(url.includes('keyword=发'),
|
||
`微信下已编码入参应被 decode 还原,实际 url: ${url}`);
|
||
|
||
const { options } = simulateWeixinOnLoad(url);
|
||
assert.equal(options.keyword, '发',
|
||
`微信下 decode-encode-decode 一次后应是原文,实际: ${options.keyword}`);
|
||
}); |