更新了鸿蒙元服务的_safeEncodeForPlatform实现和相关测试: 1. 修正此前错误的"鸿蒙整体encode url"假设,实际鸿蒙行为和微信一致,按&=分隔解析query 2. JS侧不再完全不编码,而是仅对&=?#和空格做encode,保留中文/字母数字等 3. 更新所有相关测试用例和文档注释,同步修复微信平台的编码逻辑说明
497 lines
23 KiB
JavaScript
497 lines
23 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">
|
||
* 上显示成乱码。
|
||
*
|
||
* 修复策略(2026-07-17 → 2026-07-18):
|
||
* - 微信小程序 + 鸿蒙元服务(mp-weixin / mp-harmony): `_safeEncodeForPlatform`
|
||
* 只 encode URL 分隔符 `& = ? # \s`(`_encodeUrlDelimiters`),中文 / 字母 /
|
||
* 数字 / 标点原样保留。鸿蒙之前的"完全交给平台 encode"假设是错的 —— 鸿蒙
|
||
* onLoad 行为跟微信一样, split on '&' / '=' 但不 decode value, 所以 JS
|
||
* 必须主动 encode 分隔符防 query 边界被破坏。
|
||
* - H5 / 华为快应用: 仍然主动 encode 一次(标准 encodeURIComponent, 防浏览器
|
||
* 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 分隔符(和 mp-weixin 一致)', () => {
|
||
// 2026-07-18 修正: 鸿蒙 onLoad 行为和微信一致, JS 必须 encode 分隔符。
|
||
// - 中文 / 反引号 / 其他标点 原样
|
||
// - & = ? # 空格 encode 成 %xx
|
||
const nav = loadNavInSandbox();
|
||
const api = nav.createNavigateApi({ platform: 'mp-harmony' });
|
||
|
||
// 中文原样
|
||
const r1 = api._safeEncodeForPlatform('沙发');
|
||
assert.equal(r1, '沙发', `mp-harmony 中文应原样,实际: ${r1}`);
|
||
|
||
// & = encode 成 %xx
|
||
const r2 = api._safeEncodeForPlatform('a&b=c');
|
||
assert.equal(r2, 'a%26b%3Dc', `mp-harmony 分隔符应 encode,实际: ${r2}`);
|
||
|
||
// 空格 encode 成 %20(防 query 边界被破坏)
|
||
const r3 = api._safeEncodeForPlatform('`hello world`');
|
||
assert.equal(r3, '`hello%20world`', `mp-harmony 空格应 encode,实际: ${r3}`);
|
||
});
|
||
|
||
test('_safeEncodeForPlatform: 鸿蒙下已编码入参 → decode 一次还原(避免双编码)', () => {
|
||
// 2026-07-18 仍成立: JS 侧 decode roundtrip 在所有平台都执行,
|
||
// 解码后只 encode 分隔符, 中文 / %xx 形式 都恢复成原文。
|
||
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-18 修正: 微信 / 鸿蒙走 _encodeUrlDelimiters 也会经过 decode roundtrip,
|
||
// 解码后只 encode 分隔符, 已编码的 %xx 形式(中文等)都恢复成原文, 不会双编码。
|
||
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 的处理(2026-07-18 修正):
|
||
* 鸿蒙 onLoad 行为跟微信一致 —— 不整体 encode url,只 split on '&' / '=',
|
||
* 对 value 做 decodeURIComponent。
|
||
*
|
||
* JS 侧 `_safeEncodeForPlatform` 已经把 `& = ? # \s` 编码成 %xx, 鸿蒙 split
|
||
* 之后直接拿到 key=value, 对 value decode 一次还原原文。
|
||
* 最终 onLoad(options) 拿到的 keyword 是原文。
|
||
*
|
||
* 关键: 鸿蒙 / 微信 都是 split-on-& + decode, 跟 H5 / 快应用(browser) 行为一致,
|
||
* 跟以前假设的"鸿蒙整体 encode"不同。
|
||
*/
|
||
function simulateHarmonyOnLoad(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 };
|
||
}
|
||
|
||
/**
|
||
* 模拟微信小程序对 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 输入框显示原文(不乱码)', () => {
|
||
// 2026-07-18 修正: 鸿蒙 onLoad 行为和微信一致, JS 只 encode 分隔符。
|
||
// 中文不在分隔符列表里, 所以 url 里中文原样保留; 鸿蒙 split-on-& 之后
|
||
// decode 一次, options.keyword 仍是原文。
|
||
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);
|
||
|
||
// 鸿蒙下中文不在 encode 列表里, url 应含 keyword=沙发
|
||
assert.ok(url.includes('keyword=沙发'),
|
||
`鸿蒙下 url 应含 keyword=沙发,实际: ${url}`);
|
||
|
||
// 鸿蒙 split-on-& 之后 decode 一次, 最终 options.keyword = '沙发'
|
||
const { options } = simulateHarmonyOnLoad(url);
|
||
assert.equal(options.keyword, '沙发',
|
||
`鸿蒙下 onLoad(options).keyword 应是原文,实际: ${options.keyword}`);
|
||
});
|
||
|
||
test('端到端: 鸿蒙元服务 → 含中文 + 反引号 + 空格的关键词在列表页 search 显示原文', () => {
|
||
// 2026-07-18 修正: 鸿蒙 / 微信一样, JS encode 空格 → %20(防 query 边界被破坏),
|
||
// 中文 / 反引号原样; 鸿蒙 split + decode 后还原成原文。
|
||
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);
|
||
// mp-harmony 下 JS 会 encode 空格(防 query 边界), 中文 / 反引号 原样
|
||
assert.ok(url.includes('keyword=' + encode(kw)),
|
||
`鸿蒙下 url 应含 JS 编码后的 keyword(空格 → %20),实际: ${url}`);
|
||
|
||
const { options } = simulateHarmonyOnLoad(url);
|
||
// decode 之后应还原 keyword
|
||
assert.equal(options.keyword, kw,
|
||
`鸿蒙下 onLoad(options).keyword 应是 decode 后的原文,实际: ${options.keyword}`);
|
||
});
|
||
|
||
// =============================================================
|
||
// 2.2 端到端:微信小程序(回归 Bug 的另一平台)
|
||
// =============================================================
|
||
test('端到端: 微信小程序 → 搜索关键词"沙发"在列表页 search 输入框显示原文(不乱码)', () => {
|
||
// 2026-07-18 修正: 微信 onLoad 是 split-on-& + decode 一次, JS 侧只 encode 分隔符,
|
||
// 中文原样保留, 最终 options.keyword 是原文。
|
||
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 应直接是中文(JS 不预 encode 中文)
|
||
assert.ok(!url.includes('%'),
|
||
`微信下 url 不应含 %xx(JS 不预 encode 中文),实际 url: ${url}`);
|
||
assert.ok(url.includes('keyword=沙发'),
|
||
`微信下 url 应含 keyword=沙发,实际: ${url}`);
|
||
|
||
// 微信 split-on-& + 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') {
|
||
// 鸿蒙: 不 auto-encode, split on '&' / '=',对 value decodeURIComponent
|
||
// (2026-07-18 修正: 跟微信行为一致)
|
||
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 一次还原(避免双编码)', () => {
|
||
// 2026-07-18 修正: 鸿蒙 / 微信 onLoad 行为一致, JS 侧 decode roundtrip
|
||
// 后只 encode 分隔符, 所以已编码入参还原成原文, 不会再被二次 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}`);
|
||
|
||
// 鸿蒙 split-on-& + decode 后回到"发"
|
||
const { options } = simulateHarmonyOnLoad(url);
|
||
assert.equal(options.keyword, '发',
|
||
`鸿蒙下 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}`);
|
||
}); |