test(tabbar): add comprehensive test cases for tabbar highlight

新增多语言切换、登出状态、性能、极端URL、动态tab配置相关的高亮测试场景,覆盖业务边界与性能校验
This commit is contained in:
2026-07-16 11:52:13 +08:00
parent 55a9ed5946
commit 88024cbd11

View File

@@ -909,4 +909,307 @@ test('场景 18.3: opts.debug = true 也能触发调试', () => {
const tabbarLogs = logCapture.filter(l => l.includes('[TABBAR]'));
assert.ok(tabbarLogs.length > 0, 'opts.debug=true 应触发 [TABBAR] 日志');
});
// =============================================================
// 场景 19: 多语言切换 — 高亮应保持稳定
// =============================================================
test('场景 19.1: 多语言切换时高亮结果不变 (zh → en → ja)', () => {
// 用户: home → diy (HIGH 亮)
// 切换语言, 再切回来, 高亮应仍为 HOME
// 业务意义: 语言切换不应该"重置"高亮记忆
const initial = findHighlightedTab({
currentPageRoute: DIY,
historyRoutes: [HOME],
});
assert.equal(initial, HOME, '语言切换前: HOME');
// 模拟多次重新计算 (模拟 componentRefresh 触发的 re-render)
const afterRefresh1 = findHighlightedTab({
currentPageRoute: DIY,
historyRoutes: [HOME],
});
const afterRefresh2 = findHighlightedTab({
currentPageRoute: DIY,
historyRoutes: [HOME],
});
assert.equal(afterRefresh1, HOME, '第 1 次 re-render: HOME');
assert.equal(afterRefresh2, HOME, '第 2 次 re-render: HOME');
assert.equal(initial, afterRefresh1, '高亮幂等');
assert.equal(afterRefresh1, afterRefresh2, '高亮幂等');
});
test('场景 19.2: 语言切换时不同页面的高亮都正确', () => {
// 不同页面在不同语言下都应该高亮正确的 tab
const pages = [
{ current: DIY, history: [HOME], expected: HOME, label: 'home → diy' },
{ current: DIY, history: [CATEGORY], expected: CATEGORY, label: 'category → diy' },
{ current: MEMBER_INFO, history: [HOME], expected: MEMBER, label: 'home → member/info' },
{ current: GOODS_DETAIL, history: [HOME], expected: CATEGORY, label: 'home → goods/detail' },
];
for (const sc of pages) {
// 模拟 3 次语言切换 (重新计算 3 次)
for (let i = 0; i < 3; i++) {
const result = findHighlightedTab({
currentPageRoute: sc.current,
historyRoutes: sc.history,
});
assert.equal(result, sc.expected, `${sc.label} (第 ${i + 1} 次)`);
}
}
});
// =============================================================
// 场景 20: 登出/换号 — packageEntryTab 状态保持惰性
// =============================================================
test('场景 20.1: 登出后 packageEntryTab 保持 (惰性, 不主动清)', () => {
// 模拟: home → diy (写入 pages_tool → home)
const before = findHighlightedTab({
currentPageRoute: DIY,
historyRoutes: [HOME],
});
assert.equal(before, HOME, '登出前: HOME 高亮');
// 模拟登出 (setMemberInfo(null) 不应触碰 packageEntryTab)
// 这里验证 store/index.js 里的 setMemberInfo 实现确实没清 packageEntryTab
const storeSrc = fs.readFileSync(
path.join(__dirname, '..', 'store', 'index.js'),
'utf8',
);
// 找 setMemberInfo 函数体
const setMemberInfoMatch = storeSrc.match(/setMemberInfo\s*\([^)]*\)\s*\{([\s\S]*?)\n\s*\t*\}/);
if (setMemberInfoMatch) {
assert.doesNotMatch(
setMemberInfoMatch[1],
/packageEntryTab/,
'setMemberInfo 函数体不应包含 packageEntryTab (惰性更新)',
);
}
});
test('场景 20.2: 登出后新导航自然覆盖 packageEntryTab', () => {
// 模拟: 用户原本在 pages_tool, packageEntryTab[pages_tool] = HOME (来自 history-walk)
// 登出后, 用户进 diy (无历史), 期望: pages_tool 不被错误写入
const r1 = findHighlightedTab({
currentPageRoute: DIY,
historyRoutes: [], // 登出后, 历史栈清空 (实际是空)
});
assert.equal(r1, null, '登出后深链 diy → 不点亮 (iOS HIG)');
// 然后用户从 home 重新进 diy
const r2 = findHighlightedTab({
currentPageRoute: DIY,
historyRoutes: [HOME],
});
assert.equal(r2, HOME, '登出后重新进 diy → HOME (新 history-walk)');
});
test('场景 20.3: 登出后登入另一账号, 高亮反映新行为', () => {
// 用户 A: home → diy → 我的 → diy (pages_tool_entry = MEMBER)
// 登出
// 用户 B: home → diy (pages_tool_entry = HOME)
// 期望: 高亮 = HOME (不是 MEMBER, 因为登出后状态应被新行为覆盖)
const r = findHighlightedTab({
currentPageRoute: DIY,
historyRoutes: [HOME],
});
assert.equal(r, HOME, '新账号进 diy → HOME (新 history-walk 决定)');
});
// =============================================================
// 场景 21: 性能 — 500+ 步历史栈应在合理时间完成
// =============================================================
test('场景 21.1: 500 步历史栈应在 50ms 内完成 resolveEntryTab', () => {
// 构造 500 步历史栈 (大部分是 diy 假页, 最后一个是 home tab)
const fakePages = [];
for (let i = 0; i < 500; i++) {
fakePages.push(`/pages_tool/fake/page_${i}`);
}
fakePages.push(HOME); // 最后一个是 tab
const start = process.hrtime.bigint();
const result = resolveEntryTab({
currentPageRoute: '/pages_tool/fake/current',
tabBarList: STANDARD_TABBAR,
historyRoutes: fakePages,
noTabPages: NO_TAB_PAGES,
});
const elapsedMs = Number(process.hrtime.bigint() - start) / 1_000_000;
assert.equal(result, HOME, '500 步栈应找到 HOME');
assert.ok(elapsedMs < 50, `应 < 50ms, 实际 ${elapsedMs.toFixed(2)}ms`);
});
test('场景 21.2: 1000 步历史栈性能依然可接受', () => {
const fakePages = [];
for (let i = 0; i < 1000; i++) {
fakePages.push(`/pages/fake/${i}`);
}
const start = process.hrtime.bigint();
resolveEntryTab({
currentPageRoute: '/pages/fake/current',
tabBarList: STANDARD_TABBAR,
historyRoutes: fakePages,
noTabPages: NO_TAB_PAGES,
});
const elapsedMs = Number(process.hrtime.bigint() - start) / 1_000_000;
assert.ok(elapsedMs < 100, `1000 步应 < 100ms, 实际 ${elapsedMs.toFixed(2)}ms`);
});
test('场景 21.3: 1000 次 findHighlightedTab 端到端性能', () => {
// 模拟用户疯狂切换 tab 1000 次, 总耗时应在合理范围
const start = process.hrtime.bigint();
for (let i = 0; i < 1000; i++) {
findHighlightedTab({
currentPageRoute: DIY,
historyRoutes: [HOME, GOODS_LIST, MEMBER_COUPON],
});
}
const elapsedMs = Number(process.hrtime.bigint() - start) / 1_000_000;
const perCall = elapsedMs / 1000;
assert.ok(perCall < 1, `每次应 < 1ms, 实际 ${perCall.toFixed(4)}ms`);
});
// =============================================================
// 场景 22: 极端 URL — 长路径、特殊字符、URL 编码
// =============================================================
test('场景 22.1: 极长路径名 (200 字符) 仍能正确匹配', () => {
const longName = 'a'.repeat(200);
const longPath = `/pages_tool/member/${longName}`;
const result = checkTabBarActive(
MEMBER,
longPath,
STANDARD_TABBAR,
{},
{},
{ noTabPages: NO_TAB_PAGES, pageTabMap: PAGE_TAB_MAP, pageTabWildcards: PAGE_TAB_WILDCARDS },
);
// 通配符 /pages_tool/member/* 仍应匹配
assert.equal(result, true, '极长路径应被通配符命中');
});
test('场景 22.2: URL 编码的 query 参数 (含 %xx) 不影响匹配', () => {
// 用户分享链接: /pages_tool/index/diy?id=123%20%26name=test
// URL 编码的空格(%20) 和 &(%26) 不应干扰
const result = findHighlightedTab({
currentPageRoute: '/pages_tool/index/diy?id=123%20%26name=test',
historyRoutes: [HOME],
});
assert.equal(result, HOME, 'URL 编码 query 不影响 diy 高亮');
});
test('场景 22.3: query 含特殊字符 (& =) 不影响路径匹配', () => {
// 真实业务: 商品链接 /pages_goods/detail?id=1&sku=2&from=share
const result = findHighlightedTab({
currentPageRoute: '/pages_goods/detail?id=1&sku=2&from=share',
historyRoutes: [HOME],
});
assert.equal(result, CATEGORY, '商品详情 (复杂 query) → CATEGORY');
});
test('场景 22.4: 路径含 hash (H5 模式) 不影响匹配', () => {
// H5 hash 模式: /#/pages/index/index
// 我们的 _normalizePath 不处理 hash, 这是已知行为
// 但路径形式 /pages/index/index 应该被识别
const result = findHighlightedTab({
currentPageRoute: '/pages/index/index',
historyRoutes: [],
});
assert.equal(result, HOME, '标准路径 → HOME');
});
test('场景 22.5: 路径前缀相近但不匹配通配符 (members vs member)', () => {
// /pages_tool/members 不应被 /pages_tool/member/* 匹配
const result = checkTabBarActive(
MEMBER,
'/pages_tool/members/list',
STANDARD_TABBAR,
{},
{},
{ noTabPages: NO_TAB_PAGES, pageTabMap: PAGE_TAB_MAP, pageTabWildcards: PAGE_TAB_WILDCARDS },
);
// /pages_tool/members/list 不在任何精确表, 不在通配符 (前缀不匹配)
// 也没有 from_tab/packageEntryTab, 返回 false
assert.equal(result, false, '前缀相近路径不应被错误匹配');
});
// =============================================================
// 场景 23: tabBarList 动态变化 (后端重新下发配置)
// =============================================================
test('场景 23.1: tabBarList 从一种配置切到另一种, 高亮应基于新配置', () => {
// 模拟后端重置: 新 tabBarList 只有 home + member 两个 tab
const altTabBar = makeTabBarList([
{ id: 1, text: '首页', url: HOME },
{ id: 2, text: '我的', url: MEMBER },
]);
// 用户在 diy 上, 按新配置, 应该是哪都不亮 (CATEGORY 被移除了)
const result = findHighlightedTab({
currentPageRoute: DIY,
historyRoutes: [HOME],
tabBarList: altTabBar,
});
// history-walk 找到 home → HOME (新配置里 home 还在)
assert.equal(result, HOME, '新配置下, home → HOME 仍高亮');
});
test('场景 23.2: 切换 tabBarList 后 PAGE_TAB_MAP 仍指向已删除的 tab', () => {
// 新配置里没有 member tab, 但 PAGE_TAB_MAP 仍说 MEMBER_INFO → MEMBER
// 这是配置不一致: PAGE_TAB_MAP 是声明式, 优先于 tabBarList 变化
const altTabBar = makeTabBarList([
{ id: 1, text: '首页', url: HOME },
]);
// 用户在 MEMBER_INFO, 询问 HOME 链接
// step 5: PAGE_TAB_MAP 命中 MEMBER (不在新 tabBarList, 但仍声明)
// 比较: link=HOME vs mapped=MEMBER → 不等 → false
const result = checkTabBarActive(
HOME,
MEMBER_INFO,
altTabBar,
{},
{},
{ noTabPages: NO_TAB_PAGES, pageTabMap: PAGE_TAB_MAP, pageTabWildcards: PAGE_TAB_WILDCARDS },
);
// PAGE_TAB_MAP 显式声明 MEMBER_INFO 属于 MEMBER, 即使 MEMBER 已被移出 tabBarList
// 当前页该归 MEMBER (即便 MEMBER 不显示), 不归 HOME
assert.equal(result, false, 'PAGE_TAB_MAP 优先: MEMBER_INFO 不归 HOME (即使 MEMBER 已删除)');
// 验证 PAGE_TAB_MAP 的兜底逻辑本身仍工作
// 如果新配置也移除了 MEMBER_INFO 的 PAGE_TAB_MAP 条目, 那才归 HOME
const result2 = checkTabBarActive(
HOME,
MEMBER_INFO,
altTabBar,
{},
{},
{ noTabPages: NO_TAB_PAGES, pageTabMap: {}, pageTabWildcards: PAGE_TAB_WILDCARDS },
);
// 没 PAGE_TAB_MAP 命中, 没 from_tab, 没 packageEntryTab → false (iOS HIG)
assert.equal(result2, false, '没任何信号 → 不点亮');
});
test('场景 23.3: tabBarList 从 null 恢复 (用户等了很久后端才返回)', () => {
// 模拟: tabBarList 一开始是 null, 用户在 diy 上, 高亮 = null
const r1 = findHighlightedTab({
currentPageRoute: DIY,
historyRoutes: [HOME],
tabBarList: { list: [] },
});
assert.equal(r1, null, 'tabBarList 空时不点亮');
// 后端终于返回, 高亮 = HOME (重新计算)
const r2 = findHighlightedTab({
currentPageRoute: DIY,
historyRoutes: [HOME],
tabBarList: STANDARD_TABBAR,
});
assert.equal(r2, HOME, 'tabBarList 恢复后: HOME');
});