更新了鸿蒙元服务的_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 原样��到 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}`);
|
|
}); |