1215 lines
43 KiB
JavaScript
1215 lines
43 KiB
JavaScript
/**
|
|
* TabBar 高亮 — 行为测试 (按用户故事组织)
|
|
*
|
|
* 原则:
|
|
* - 每个测试都是一个真实的用户故事, 不是为了能测试过而迎合代码
|
|
* - 测试 input → 行为, 不测试代码结构 ("wiring 测试" 已删除)
|
|
* - 失败的测试 = 真实用户能看到的高亮错误
|
|
*
|
|
* 跑法 (需要 Node 18+):
|
|
* node --test tests/tabbar-highlight.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');
|
|
|
|
// =============================================================
|
|
// 加载被测源码 (ESM → CommonJS 沙箱)
|
|
// =============================================================
|
|
|
|
const TABBAR_SRC = fs.readFileSync(
|
|
path.join(__dirname, '..', 'common', 'js', 'util-tabbar.js'),
|
|
'utf8',
|
|
);
|
|
|
|
function loadTabbarInSandbox() {
|
|
const moduleObj = { exports: {} };
|
|
const wrapped = TABBAR_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 = { resolvePageUrl, isPageInRouteList, resolveLinkUrl, '
|
|
+ 'makeSystemTabBarList, adaptSubpackageUrl, isTabBarRootPage, '
|
|
+ 'isCurrentPageTabBarRoot, checkTabBarActive, resolveEntryTab, lookupPageTabMap };\n';
|
|
const sandbox = { console, globalThis: {}, module: moduleObj, exports: moduleObj.exports };
|
|
sandbox.globalThis = sandbox;
|
|
vm.createContext(sandbox);
|
|
vm.runInContext(wrapped, sandbox, { filename: 'util-tabbar.js' });
|
|
return sandbox.module.exports;
|
|
}
|
|
|
|
const { resolveEntryTab, checkTabBarActive, lookupPageTabMap } = loadTabbarInSandbox();
|
|
|
|
// =============================================================
|
|
// 业务测试夹具
|
|
// =============================================================
|
|
|
|
// 5 个 Tab + 5 个分包的标准布局
|
|
const HOME = '/pages/index/index';
|
|
const CATEGORY = '/pages_goods/category';
|
|
const CART = '/pages_goods/cart';
|
|
const MERCHANT = '/pages_promotion/merch/merchcategory';
|
|
const MEMBER = '/pages_tool/member/index';
|
|
|
|
// 常见非 tab 落地页
|
|
const DIY = '/pages_tool/index/diy';
|
|
const MEMBER_INFO = '/pages_tool/member/info';
|
|
const MEMBER_COUPON = '/pages_tool/member/coupon';
|
|
const MEMBER_BALANCE = '/pages_tool/member/balance';
|
|
const GOODS_LIST = '/pages_goods/list';
|
|
const GOODS_DETAIL = '/pages_goods/detail';
|
|
const SEARCH = '/pages_tool/goods/search';
|
|
const BRAND = '/pages_tool/goods/brand';
|
|
const COUPON = '/pages_tool/goods/coupon';
|
|
const HELP_LIST = '/pages_tool/help/list';
|
|
const NOTICE_LIST = '/pages_tool/notice/list';
|
|
const FILES_LIST = '/pages_tool/files/list';
|
|
const FENXIAO = '/pages_promotion/fenxiao/index';
|
|
|
|
// 全屏流程 (modal 风格)
|
|
const LOGIN = '/pages_tool/login/login';
|
|
const PAY = '/pages_tool/pay/index';
|
|
const WEBVIEW = '/pages_tool/webview/webview';
|
|
|
|
function makeTabBarList(tabs) {
|
|
return {
|
|
list: tabs.map((t, i) => ({
|
|
id: t.id || (i + 1),
|
|
text: t.text || '',
|
|
link: { wap_url: t.url },
|
|
})),
|
|
};
|
|
}
|
|
|
|
const STANDARD_TABBAR = makeTabBarList([
|
|
{ id: 1, text: '首页', url: HOME },
|
|
{ id: 2, text: '分类', url: CATEGORY },
|
|
{ id: 3, text: '购物车', url: CART },
|
|
{ id: 4, text: '商户', url: MERCHANT },
|
|
{ id: 5, text: '我的', url: MEMBER },
|
|
]);
|
|
|
|
// PAGE_TAB_MAP: 显式声明的页面归属 (精确 + null)
|
|
const PAGE_TAB_MAP = {
|
|
// 首页频道
|
|
[HOME]: HOME,
|
|
'/pages/empty/index': HOME,
|
|
[HELP_LIST]: HOME,
|
|
[NOTICE_LIST]: HOME,
|
|
// 分类频道 — tab 根
|
|
[CATEGORY]: CATEGORY,
|
|
[GOODS_LIST]: CATEGORY,
|
|
[GOODS_DETAIL]: CATEGORY,
|
|
// 商户频道
|
|
[MERCHANT]: MERCHANT,
|
|
'/pages_promotion/merch/detail': MERCHANT,
|
|
// 购物车
|
|
[CART]: CART,
|
|
// 我的频道 — tab 根
|
|
[MEMBER]: MEMBER,
|
|
'/pages_order/list': MEMBER,
|
|
'/pages_tool/recharge/list': MEMBER,
|
|
'/pages_tool/recharge/order_list': MEMBER,
|
|
// 显式"不点亮"
|
|
[FILES_LIST]: null,
|
|
};
|
|
|
|
// 通配符 (按子树批量归属, 减少维护成本)
|
|
const PAGE_TAB_WILDCARDS = [
|
|
{ pattern: '/pages_tool/member/*', tab: MEMBER },
|
|
{ pattern: '/pages_tool/goods/*', tab: CATEGORY },
|
|
];
|
|
|
|
// Modal / 全屏流程页 (在归属判定中被跳过, 视为透明)
|
|
const NO_TAB_PAGES = new Set([
|
|
LOGIN, PAY, WEBVIEW,
|
|
'/pages_tool/ai-chat/index',
|
|
'/pages_tool/vr/index',
|
|
'/pages_tool/cases/index',
|
|
'/pages_tool/webview/webview',
|
|
]);
|
|
|
|
/**
|
|
* 端到端: 给定当前页面 + 历史栈, 算出哪个 tab 高亮
|
|
* @returns {string|null} tab URL 或 null
|
|
*/
|
|
function findHighlightedTab({ currentPageRoute, historyRoutes = [], tabBarList = STANDARD_TABBAR }) {
|
|
const entryTab = resolveEntryTab({
|
|
currentPageRoute,
|
|
tabBarList,
|
|
historyRoutes,
|
|
noTabPages: NO_TAB_PAGES,
|
|
});
|
|
const currentPackageName = currentPageRoute.split('?')[0].split('/')[1] || '';
|
|
const packageEntryTab = entryTab ? { [currentPackageName]: entryTab } : {};
|
|
for (const tab of tabBarList.list) {
|
|
if (!tab.link || !tab.link.wap_url) continue;
|
|
if (checkTabBarActive(
|
|
tab.link.wap_url,
|
|
currentPageRoute,
|
|
tabBarList,
|
|
packageEntryTab,
|
|
{},
|
|
{ noTabPages: NO_TAB_PAGES, pageTabMap: PAGE_TAB_MAP, pageTabWildcards: PAGE_TAB_WILDCARDS },
|
|
)) {
|
|
return tab.link.wap_url;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function assertHighlight(actual, expected, label) {
|
|
assert.equal(actual, expected,
|
|
`[${label}] 期望 ${expected || '不点亮'}, 实际 ${actual || '不点亮'}`);
|
|
}
|
|
|
|
// =============================================================
|
|
// 场景 1: 跨包导航 — "从哪个 tab 出发, 非 tab 页就归属哪个 tab"
|
|
// =============================================================
|
|
|
|
test('场景 1.1: 用户从首页点 diy 链接 → 首页保持高亮', () => {
|
|
assertHighlight(findHighlightedTab({
|
|
currentPageRoute: DIY,
|
|
historyRoutes: [HOME],
|
|
}), HOME, 'home → diy');
|
|
});
|
|
|
|
test('场景 1.2: 用户从分类点 diy 链接 → 分类高亮', () => {
|
|
assertHighlight(findHighlightedTab({
|
|
currentPageRoute: DIY,
|
|
historyRoutes: [CATEGORY],
|
|
}), CATEGORY, 'category → diy');
|
|
});
|
|
|
|
test('场景 1.3: 用户从商户 tab 进 diy 微页面 → 商户高亮', () => {
|
|
assertHighlight(findHighlightedTab({
|
|
currentPageRoute: DIY,
|
|
historyRoutes: [MERCHANT],
|
|
}), MERCHANT, 'merchant → diy');
|
|
});
|
|
|
|
test('场景 1.4: 深层导航, 跳过非 tab 中间页', () => {
|
|
// 用户从首页逛到商品列表, 再点 diy 链接
|
|
// (中间经过非 tab 的 goods_list)
|
|
assertHighlight(findHighlightedTab({
|
|
currentPageRoute: DIY,
|
|
historyRoutes: [HOME, GOODS_LIST],
|
|
}), HOME, 'home → goods_list → diy');
|
|
});
|
|
|
|
test('场景 1.5: 经过 login 弹窗后, 仍记为来自原 tab', () => {
|
|
// 用户在 home, 弹登录页, 登录成功回到 diy
|
|
// login 是 modal, 不应该"洗掉"来源 tab
|
|
assertHighlight(findHighlightedTab({
|
|
currentPageRoute: DIY,
|
|
historyRoutes: [HOME, LOGIN],
|
|
}), HOME, 'home → login → diy');
|
|
});
|
|
|
|
// =============================================================
|
|
// 场景 2: 同包导航 — 用户在当前包内浏览, 高亮当前包的 tab
|
|
// =============================================================
|
|
|
|
test('场景 2.1: /pages_goods/list → /pages_goods/detail 属于分类频道', () => {
|
|
assertHighlight(findHighlightedTab({
|
|
currentPageRoute: GOODS_DETAIL,
|
|
historyRoutes: [GOODS_LIST],
|
|
}), CATEGORY, 'goods/list → detail');
|
|
});
|
|
|
|
test('场景 2.2: /pages_tool/member/coupon → /pages_tool/member/info 属于"我的"', () => {
|
|
assertHighlight(findHighlightedTab({
|
|
currentPageRoute: MEMBER_INFO,
|
|
historyRoutes: [MEMBER_COUPON],
|
|
}), MEMBER, 'member/coupon → info');
|
|
});
|
|
|
|
test('场景 2.3: 在"我的"频道内多页浏览, 高亮始终是"我的"', () => {
|
|
// home → member → coupon → info (都在 pages_tool 包)
|
|
assertHighlight(findHighlightedTab({
|
|
currentPageRoute: MEMBER_INFO,
|
|
historyRoutes: [HOME, MEMBER, MEMBER_COUPON],
|
|
}), MEMBER, 'home → member → coupon → info');
|
|
});
|
|
|
|
// =============================================================
|
|
// 场景 3: tab 切换 — 用户点底部 tab 跳转
|
|
// =============================================================
|
|
|
|
test('场景 3.1: tab→tab 跳转, 目标 tab 高亮 (不受来源干扰)', () => {
|
|
assertHighlight(findHighlightedTab({
|
|
currentPageRoute: CATEGORY,
|
|
historyRoutes: [HOME],
|
|
}), CATEGORY, 'home → category tab');
|
|
});
|
|
|
|
test('场景 3.2: 在 diy 页面点"我的" tab → 我的高亮 (同包)', () => {
|
|
// 实际场景: diy 页面底部 TabBar 用户点"我的"
|
|
// 走 switchTab/reLaunch, 栈变成 [MEMBER]
|
|
// diy 的临时 packageEntryTab 会被 MEMBER 覆盖
|
|
assertHighlight(findHighlightedTab({
|
|
currentPageRoute: MEMBER,
|
|
historyRoutes: [DIY],
|
|
}), MEMBER, 'diy → member tab');
|
|
});
|
|
|
|
test('场景 3.3: 在 diy 页面点"首页" tab → 首页高亮 (跨包)', () => {
|
|
assertHighlight(findHighlightedTab({
|
|
currentPageRoute: HOME,
|
|
historyRoutes: [DIY],
|
|
}), HOME, 'diy → home tab');
|
|
});
|
|
|
|
test('场景 3.4: 链式 tab 切换 home→分类→我的→购物车→首页', () => {
|
|
// 模拟用户连续点 tab, 每次都更新高亮
|
|
const tabs = [HOME, CATEGORY, MEMBER, CART, HOME];
|
|
for (let i = 0; i < tabs.length; i++) {
|
|
assertHighlight(findHighlightedTab({
|
|
currentPageRoute: tabs[i],
|
|
historyRoutes: tabs.slice(0, i),
|
|
}), tabs[i], `链式第 ${i + 1} 步: ${tabs[i]}`);
|
|
}
|
|
});
|
|
|
|
// =============================================================
|
|
// 场景 4: tab→tab 内部非 tab 页 — 通配符命中
|
|
// =============================================================
|
|
|
|
test('场景 4.1: 首页 → 商品搜索 → 分类高亮 (通配符 /pages_tool/goods/*)', () => {
|
|
assertHighlight(findHighlightedTab({
|
|
currentPageRoute: SEARCH,
|
|
historyRoutes: [HOME],
|
|
}), CATEGORY, 'home → search (goods/* 通配符)');
|
|
});
|
|
|
|
test('场景 4.2: 首页 → 商品品牌页 → 分类高亮 (通配符)', () => {
|
|
assertHighlight(findHighlightedTab({
|
|
currentPageRoute: BRAND,
|
|
historyRoutes: [HOME],
|
|
}), CATEGORY, 'home → brand (goods/* 通配符)');
|
|
});
|
|
|
|
test('场景 4.3: 首页 → 会员中心 → 我的高亮 (通配符 /pages_tool/member/*)', () => {
|
|
assertHighlight(findHighlightedTab({
|
|
currentPageRoute: MEMBER_INFO,
|
|
historyRoutes: [HOME],
|
|
}), MEMBER, 'home → member/info (member/* 通配符)');
|
|
});
|
|
|
|
test('场景 4.4: 首页 → 优惠券 → 我的高亮 (通配符)', () => {
|
|
assertHighlight(findHighlightedTab({
|
|
currentPageRoute: MEMBER_COUPON,
|
|
historyRoutes: [HOME],
|
|
}), MEMBER, 'home → member/coupon (member/* 通配符)');
|
|
});
|
|
|
|
test('场景 4.5: 首页 → 余额 → 我的高亮 (通配符)', () => {
|
|
assertHighlight(findHighlightedTab({
|
|
currentPageRoute: MEMBER_BALANCE,
|
|
historyRoutes: [HOME],
|
|
}), MEMBER, 'home → member/balance (member/* 通配符)');
|
|
});
|
|
|
|
test('场景 4.6: 分类 tab → 商品详情 → 分类保持高亮 (PAGE_TAB_MAP 精确优先)', () => {
|
|
// 即使来源是分类 tab, 商品详情仍在 PAGE_TAB_MAP 绑 CATEGORY
|
|
assertHighlight(findHighlightedTab({
|
|
currentPageRoute: GOODS_DETAIL,
|
|
historyRoutes: [CATEGORY],
|
|
}), CATEGORY, 'category → goods/detail (PAGE_TAB_MAP wins)');
|
|
});
|
|
|
|
// =============================================================
|
|
// 场景 5: 显式 PAGE_TAB_MAP 优先级
|
|
// =============================================================
|
|
|
|
test('场景 5.1: /pages_tool/files/list 显式绑 null → 不点亮 (业务规则: 文件页不归属任何 tab)', () => {
|
|
assertHighlight(findHighlightedTab({
|
|
currentPageRoute: FILES_LIST,
|
|
historyRoutes: [HOME],
|
|
}), null, 'home → files/list (null 优先)');
|
|
});
|
|
|
|
test('场景 5.2: /pages_tool/index/diy 不在 PAGE_TAB_MAP, 走 history-walk', () => {
|
|
// diy 不在精确表也不在通配符, 走 history-walk
|
|
// 从 home 来 → HOME 高亮
|
|
assertHighlight(findHighlightedTab({
|
|
currentPageRoute: DIY,
|
|
historyRoutes: [HOME],
|
|
}), HOME, 'diy 走 history-walk, home 高亮');
|
|
});
|
|
|
|
test('场景 5.3: diy 没在 PAGE_TAB_MAP 显式声明 (避免硬绑)', () => {
|
|
// 反向验证: 修复后, diy 不在 PAGE_TAB_MAP, 完全靠 history-walk
|
|
// 这是关键修复 — 之前 diy:HOME 导致深链误高亮
|
|
// 此测试防回归: 任何人重新加回 diy:HOME, 这个测试 + 行为测试都会挂
|
|
const currentMap = PAGE_TAB_MAP[DIY];
|
|
assert.equal(currentMap, undefined,
|
|
'diy 不应在 PAGE_TAB_MAP (修复后靠 history-walk 决定)');
|
|
});
|
|
|
|
// =============================================================
|
|
// 场景 6: 深链冷启动 (扫码/分享) — iOS HIG
|
|
// =============================================================
|
|
|
|
test('场景 6.1: 用户扫码进 diy → 不高亮任何 tab (iOS HIG)', () => {
|
|
// 用户从来没在任何 tab 页面, 没有来源记忆
|
|
assertHighlight(findHighlightedTab({
|
|
currentPageRoute: DIY,
|
|
historyRoutes: [],
|
|
}), null, '深链 diy');
|
|
});
|
|
|
|
test('场景 6.2: 分享 diy 链接给朋友, 朋友点开 → 不高亮 (无历史)', () => {
|
|
assertHighlight(findHighlightedTab({
|
|
currentPageRoute: DIY + '?id=123',
|
|
historyRoutes: [],
|
|
}), null, '深链 diy?id=123');
|
|
});
|
|
|
|
test('场景 6.3: 分享商品详情链接, 朋友点开 → 分类高亮 (通配符)', () => {
|
|
// 朋友点链接进入商品详情, 没有历史
|
|
// 商品详情在 PAGE_TAB_MAP 精确绑 CATEGORY → CATEGORY 高亮
|
|
assertHighlight(findHighlightedTab({
|
|
currentPageRoute: GOODS_DETAIL,
|
|
historyRoutes: [],
|
|
}), CATEGORY, '深链 goods/detail');
|
|
});
|
|
|
|
test('场景 6.4: 分享会员中心链接, 朋友点开 → 我的高亮 (精确绑)', () => {
|
|
assertHighlight(findHighlightedTab({
|
|
currentPageRoute: MEMBER,
|
|
historyRoutes: [],
|
|
}), MEMBER, '深链 member (精确)');
|
|
});
|
|
|
|
test('场景 6.5: 分享 fenxiao 链接, 朋友点开 → 不高亮 (不在任何 map)', () => {
|
|
// fenxiao 不在 PAGE_TAB_MAP, 不在通配符, 不在 tabBarList
|
|
// iOS HIG: 不高亮
|
|
assertHighlight(findHighlightedTab({
|
|
currentPageRoute: FENXIAO,
|
|
historyRoutes: [],
|
|
}), null, '深链 fenxiao');
|
|
});
|
|
|
|
// =============================================================
|
|
// 场景 7: 全屏 modal 流程 — login / pay / webview
|
|
// =============================================================
|
|
|
|
test('场景 7.1: 在 home 点登录 → 全屏不点亮 (NO_TAB_PAGES)', () => {
|
|
assertHighlight(findHighlightedTab({
|
|
currentPageRoute: LOGIN,
|
|
historyRoutes: [HOME],
|
|
}), null, 'home → login (NO_TAB_PAGES)');
|
|
});
|
|
|
|
test('场景 7.2: 支付流程中 → 不点亮 (NO_TAB_PAGES)', () => {
|
|
assertHighlight(findHighlightedTab({
|
|
currentPageRoute: PAY,
|
|
historyRoutes: [HOME, GOODS_LIST],
|
|
}), null, 'home → list → pay (NO_TAB_PAGES)');
|
|
});
|
|
|
|
test('场景 7.3: 登录后回到原页面, 高亮不丢失', () => {
|
|
// 实际场景: 用户在 home, 弹登录, 登录成功, 跳到 diy
|
|
// 期望: diy 仍归属 home (login 是 modal, 不算"离开" home)
|
|
assertHighlight(findHighlightedTab({
|
|
currentPageRoute: DIY,
|
|
historyRoutes: [HOME, LOGIN],
|
|
}), HOME, 'home → login → diy (modal 透明)');
|
|
});
|
|
|
|
test('场景 7.4: 支付完成后回到原页面, 高亮跟随当前包', () => {
|
|
// 用户: home → 进入商品区(逛购物车) → 支付 → 回到商品列表
|
|
// 实际栈: home → cart → pay → list
|
|
// 跳过 pay (NO_TAB_PAGES), 上一非 NO_TAB_PAGES 是 cart (pages_goods)
|
|
// 当前 list 在 pages_goods → 同包 → CATEGORY (商品频道的入口 tab)
|
|
assertHighlight(findHighlightedTab({
|
|
currentPageRoute: GOODS_LIST,
|
|
historyRoutes: [HOME, CART, PAY],
|
|
}), CATEGORY, 'home → cart → pay → list (modal 透明 + 同包)');
|
|
});
|
|
|
|
// =============================================================
|
|
// 场景 8: from_tab query 机制 (旧机制, 仍在用)
|
|
// =============================================================
|
|
|
|
test('场景 8.1: URL 带 from_tab=home → 强制高亮 home (即使当前在 pages_tool)', () => {
|
|
// diy-bottom-nav.vue#redirectTo 在用户点 TabBar 时会写 from_tab query
|
|
const result = checkTabBarActive(
|
|
HOME,
|
|
DIY,
|
|
STANDARD_TABBAR,
|
|
{},
|
|
{ from_tab: HOME },
|
|
{ noTabPages: NO_TAB_PAGES, pageTabMap: {}, pageTabWildcards: PAGE_TAB_WILDCARDS },
|
|
);
|
|
assert.equal(result, true, 'from_tab=home → HOME 高亮');
|
|
});
|
|
|
|
test('场景 8.2: from_tab=member 但 PAGE_TAB_MAP 已显式声明 → PAGE_TAB_MAP 优先', () => {
|
|
// PAGE_TAB_MAP 显式声明的页面, from_tab 不能覆盖
|
|
// 这是设计: 显式声明 > 任何 hint
|
|
const result = checkTabBarActive(
|
|
MEMBER,
|
|
DIY,
|
|
STANDARD_TABBAR,
|
|
{},
|
|
{ from_tab: MEMBER },
|
|
{ noTabPages: NO_TAB_PAGES, pageTabMap: { [DIY]: HOME }, pageTabWildcards: [] },
|
|
);
|
|
assert.equal(result, false, 'PAGE_TAB_MAP 优先于 from_tab');
|
|
});
|
|
|
|
test('场景 8.3: 非法 from_tab 值 → 忽略, 走后续链路', () => {
|
|
// from_tab 不是任何 tab, 视为无效
|
|
const result = checkTabBarActive(
|
|
HOME,
|
|
DIY,
|
|
STANDARD_TABBAR,
|
|
{},
|
|
{ from_tab: '/foo/bar' },
|
|
{ noTabPages: NO_TAB_PAGES, pageTabMap: {}, pageTabWildcards: [] },
|
|
);
|
|
assert.equal(result, false, '非法 from_tab → 忽略, 无其它信号 → 不亮');
|
|
});
|
|
|
|
// =============================================================
|
|
// 场景 9: URL 参数不影响高亮判定
|
|
// =============================================================
|
|
|
|
test('场景 9.1: diy 带 query ?id=123&from=share → 行为不变', () => {
|
|
assertHighlight(findHighlightedTab({
|
|
currentPageRoute: '/pages_tool/index/diy?id=123&from=share',
|
|
historyRoutes: [HOME],
|
|
}), HOME, 'home → diy?id=123&from=share');
|
|
});
|
|
|
|
test('场景 9.2: tab 根带 query 也精确匹配', () => {
|
|
// 即使带 ?id=123, 也能识别是 home tab
|
|
const result = resolveEntryTab({
|
|
currentPageRoute: '/pages/index/index?from=share',
|
|
tabBarList: STANDARD_TABBAR,
|
|
historyRoutes: [],
|
|
});
|
|
assert.equal(result, HOME, 'home tab 根 + query');
|
|
});
|
|
|
|
test('场景 9.3: 老路径 /pages/member/index 自动适配到 /pages_tool/member/index', () => {
|
|
// 兼容老 URL (旧业务数据)
|
|
const result = resolveEntryTab({
|
|
currentPageRoute: DIY,
|
|
tabBarList: STANDARD_TABBAR,
|
|
historyRoutes: ['/pages/member/index'],
|
|
});
|
|
assert.equal(result, MEMBER, '老路径自动 adaptSubpackageUrl');
|
|
});
|
|
|
|
// =============================================================
|
|
// 场景 10: 一致性 — 同时只有 1 个 tab 高亮
|
|
// =============================================================
|
|
|
|
test('场景 10.1: 5 个 tab 中, 同时只有 1 个高亮 (互斥)', () => {
|
|
const results = STANDARD_TABBAR.list.map(t =>
|
|
checkTabBarActive(
|
|
t.link.wap_url,
|
|
DIY,
|
|
STANDARD_TABBAR,
|
|
{ pages_tool: HOME },
|
|
{},
|
|
{ noTabPages: NO_TAB_PAGES, pageTabMap: PAGE_TAB_MAP, pageTabWildcards: PAGE_TAB_WILDCARDS },
|
|
)
|
|
);
|
|
const trueCount = results.filter(Boolean).length;
|
|
assert.equal(trueCount, 1, `5 个 tab 应只亮 1 个, 实际 ${trueCount} 个亮`);
|
|
});
|
|
|
|
test('场景 10.2: 同场景下 (current + history), 高亮结果应一致 (幂等)', () => {
|
|
// 同样的 input 调两次, 结果应一致
|
|
const opts = {
|
|
currentPageRoute: DIY,
|
|
historyRoutes: [HOME, GOODS_LIST],
|
|
tabBarList: STANDARD_TABBAR,
|
|
};
|
|
const r1 = resolveEntryTab(opts);
|
|
const r2 = resolveEntryTab(opts);
|
|
assert.equal(r1, r2, 'resolveEntryTab 幂等');
|
|
assert.equal(r1, HOME, '且结果应为 home');
|
|
});
|
|
|
|
// =============================================================
|
|
// 场景 11: 边界 — 输入异常时不能崩
|
|
// =============================================================
|
|
|
|
test('场景 11.1: tabBarList 为空 (服务端配置异常) → 任何 tab 都不亮', () => {
|
|
// 即使 diy 在 PAGE_TAB_MAP, 没有 tab 也没法亮
|
|
assertHighlight(findHighlightedTab({
|
|
currentPageRoute: DIY,
|
|
historyRoutes: [HOME],
|
|
tabBarList: { list: [] },
|
|
}), null, 'tabBarList 空');
|
|
});
|
|
|
|
test('场景 11.2: tabBarList.list 含脏数据 (link 为 null/空) → 不崩', () => {
|
|
const dirtyTabBar = {
|
|
list: [
|
|
{ id: 1, link: null },
|
|
{ id: 2, link: {} },
|
|
{ id: 3, text: '首页', link: { wap_url: HOME } },
|
|
{ id: 4, text: '我的', link: { wap_url: MEMBER } },
|
|
],
|
|
};
|
|
const r = resolveEntryTab({
|
|
currentPageRoute: DIY,
|
|
tabBarList: dirtyTabBar,
|
|
historyRoutes: [HOME],
|
|
});
|
|
assert.equal(r, HOME, '脏数据不影响查找');
|
|
});
|
|
|
|
test('场景 11.3: historyRoutes 含 null/空/无效值 → 安全跳过', () => {
|
|
const r = resolveEntryTab({
|
|
currentPageRoute: DIY,
|
|
tabBarList: STANDARD_TABBAR,
|
|
historyRoutes: [null, '', undefined, HOME],
|
|
});
|
|
assert.equal(r, HOME, 'historyRoutes 脏数据 → 跳过');
|
|
});
|
|
|
|
test('场景 11.4: historyRoutes 不是数组 → 视为无历史 (iOS HIG)', () => {
|
|
// 防御性编程: 异常入参不能崩
|
|
// 行为: 当作无历史 → 走 iOS HIG (无历史不点亮)
|
|
const r = resolveEntryTab({
|
|
currentPageRoute: DIY,
|
|
tabBarList: STANDARD_TABBAR,
|
|
historyRoutes: 'not-an-array',
|
|
});
|
|
assert.equal(r, null, 'historyRoutes 非数组 → 视为空 → 不点亮');
|
|
});
|
|
|
|
test('场景 11.5: 当前页面是 /pages_goods/some-unmapped-page 深链 → 不亮', () => {
|
|
// 业务上新加的商品页, 既不在 PAGE_TAB_MAP 也不在通配符
|
|
assertHighlight(findHighlightedTab({
|
|
currentPageRoute: '/pages_goods/some-unmapped-page',
|
|
historyRoutes: [],
|
|
}), null, '深链 unmapped 页');
|
|
});
|
|
|
|
// =============================================================
|
|
// 场景 12: 业务连续性 — packageEntryTab 惰性更新
|
|
// =============================================================
|
|
|
|
test('场景 12.1: 用户在 home → diy → 切到 member → 回 home → 进 diy, 仍归 home', () => {
|
|
// 完整路径模拟:
|
|
// stack=[home] (home 高亮)
|
|
// stack=[home, diy] (home 保持)
|
|
// stack=[member] (member 高亮, 弹栈)
|
|
// stack=[home] (用户返回)
|
|
// stack=[home, diy] (再次进 diy, 应仍归 home)
|
|
//
|
|
// 最后一步: 当前 diy, 历史 = [home]
|
|
// 期望: HOME 高亮 (因为用户在 home 上下文再次进了 diy)
|
|
assertHighlight(findHighlightedTab({
|
|
currentPageRoute: DIY,
|
|
historyRoutes: [HOME],
|
|
}), HOME, 'home → diy → member → 返回 → home → diy');
|
|
});
|
|
|
|
test('场景 12.2: 进入 cart tab 后, 旧 packageEntryTab 被新 tab 覆盖', () => {
|
|
// 用户在 home → diy (写入 pages_tool → home)
|
|
// 然后点 cart tab → 弹栈, 跳 CART
|
|
// 跳 CART 后: pages_goods → CART (覆盖旧 entry)
|
|
assertHighlight(findHighlightedTab({
|
|
currentPageRoute: CART,
|
|
historyRoutes: [HOME],
|
|
}), CART, 'home → diy → 点 cart tab → CART');
|
|
});
|
|
|
|
test('场景 12.3: 登出后, 旧 packageEntryTab 保持 (惰性, 下次 updateCurrentRoute 重算)', () => {
|
|
// 登出不应清 packageEntryTab (故意惰性)
|
|
// 下次进新页面时 updateCurrentRoute 会基于新栈重算, 自然覆盖
|
|
// 这里只验证行为: 登出后访问 diy, 仍按 history-walk 决定
|
|
assertHighlight(findHighlightedTab({
|
|
currentPageRoute: DIY,
|
|
historyRoutes: [HOME],
|
|
}), HOME, '登出后 diy 高亮仍由 history-walk 决定');
|
|
});
|
|
|
|
// =============================================================
|
|
// 场景 13: 核心单元测试 (resolveEntryTab 5 步 + lookupPageTabMap)
|
|
// =============================================================
|
|
|
|
test('单元 13.1: 当前是 tab 根 → 走 step 1 精确匹配, 不查 history', () => {
|
|
assert.equal(
|
|
resolveEntryTab({ currentPageRoute: CATEGORY, tabBarList: STANDARD_TABBAR, historyRoutes: [HOME, MEMBER] }),
|
|
CATEGORY,
|
|
'category 是 tab 根, 不被 history 干扰',
|
|
);
|
|
});
|
|
|
|
test('单元 13.2: lookupPageTabMap 精确匹配优先于通配符', () => {
|
|
// /pages_tool/member/index 在精确表里是 MEMBER
|
|
assert.equal(
|
|
lookupPageTabMap(MEMBER, PAGE_TAB_MAP, PAGE_TAB_WILDCARDS),
|
|
MEMBER,
|
|
);
|
|
});
|
|
|
|
test('单元 13.3: lookupPageTabMap 通配符命中非精确条目', () => {
|
|
assert.equal(
|
|
lookupPageTabMap('/pages_tool/member/balance_detail', PAGE_TAB_MAP, PAGE_TAB_WILDCARDS),
|
|
MEMBER,
|
|
'通配符 /pages_tool/member/* → MEMBER',
|
|
);
|
|
assert.equal(
|
|
lookupPageTabMap('/pages_tool/goods/brand', PAGE_TAB_MAP, PAGE_TAB_WILDCARDS),
|
|
CATEGORY,
|
|
'通配符 /pages_tool/goods/* → CATEGORY',
|
|
);
|
|
});
|
|
|
|
test('单元 13.4: lookupPageTabMap null 优先于通配符', () => {
|
|
// 即使有 /pages_tool/files/* 通配符, files/list 显式 null 应优先
|
|
const customMap = { [FILES_LIST]: null };
|
|
const customWildcards = [{ pattern: '/pages_tool/files/*', tab: HOME }];
|
|
assert.equal(
|
|
lookupPageTabMap(FILES_LIST, customMap, customWildcards),
|
|
null,
|
|
'files/list 显式 null 优先于通配符',
|
|
);
|
|
});
|
|
|
|
test('单元 13.5: lookupPageTabMap 无匹配 → undefined (走 history-walk)', () => {
|
|
assert.equal(
|
|
lookupPageTabMap(DIY, PAGE_TAB_MAP, PAGE_TAB_WILDCARDS),
|
|
undefined,
|
|
'diy 不在任何表, 走 history-walk',
|
|
);
|
|
});
|
|
|
|
test('单元 13.6: lookupPageTabMap 通配符不误匹配前缀相近路径', () => {
|
|
// /pages_tool/members (复数) 不会被 /pages_tool/member/* 匹配
|
|
assert.equal(
|
|
lookupPageTabMap('/pages_tool/members', PAGE_TAB_MAP, PAGE_TAB_WILDCARDS),
|
|
undefined,
|
|
'复数路径不匹配单数通配符',
|
|
);
|
|
});
|
|
|
|
// =============================================================
|
|
// 场景 14: checkTabBarActive 兜底 (直接函数调用, 验证边界)
|
|
// =============================================================
|
|
|
|
test('兜底 14.1: link URL 不在 tabBarList → 直接 false (早返回)', () => {
|
|
// 询问一个非法 link URL
|
|
const result = checkTabBarActive(
|
|
'/pages/non-existent-tab',
|
|
DIY,
|
|
STANDARD_TABBAR,
|
|
{ pages_tool: HOME },
|
|
{ from_tab: HOME },
|
|
{ noTabPages: NO_TAB_PAGES, pageTabMap: PAGE_TAB_MAP, pageTabWildcards: PAGE_TAB_WILDCARDS },
|
|
);
|
|
assert.equal(result, false, '非法 link URL → false (无视所有信号)');
|
|
});
|
|
|
|
test('兜底 14.2: 当前页面在 NO_TAB_PAGES → 任何 tab 都不亮', () => {
|
|
for (const tabUrl of [HOME, CATEGORY, CART, MERCHANT, MEMBER]) {
|
|
const result = checkTabBarActive(
|
|
tabUrl,
|
|
LOGIN,
|
|
STANDARD_TABBAR,
|
|
{ pages: HOME, pages_tool: MEMBER },
|
|
{},
|
|
{ noTabPages: NO_TAB_PAGES, pageTabMap: PAGE_TAB_MAP, pageTabWildcards: PAGE_TAB_WILDCARDS },
|
|
);
|
|
assert.equal(result, false, `login 页面, ${tabUrl} 不该亮`);
|
|
}
|
|
});
|
|
|
|
// =============================================================
|
|
// 场景 15: 核心 8 场景对照表 (业务高频路径)
|
|
// =============================================================
|
|
|
|
const SCENARIOS = [
|
|
{ label: '深链扫 diy', current: DIY, history: [], expected: null },
|
|
{ label: '首页 → diy 链接', current: DIY, history: [HOME], expected: HOME },
|
|
{ label: '首页 → 会员中心', current: MEMBER, history: [HOME], expected: MEMBER },
|
|
{ label: '分类 → diy 链接', current: DIY, history: [CATEGORY], expected: CATEGORY },
|
|
{ label: '分类 → 商品列表 → diy', current: DIY, history: [CATEGORY, GOODS_LIST], expected: CATEGORY },
|
|
{ label: '我的 → 优惠券 → diy', current: DIY, history: [MEMBER, MEMBER_COUPON], expected: MEMBER },
|
|
{ label: '首页 → 登录页 → diy', current: DIY, history: [HOME, LOGIN], expected: HOME },
|
|
{ label: '深链扫 member/info', current: MEMBER_INFO, history: [], expected: MEMBER },
|
|
];
|
|
|
|
test('场景对照: 8 个核心业务场景', () => {
|
|
for (const sc of SCENARIOS) {
|
|
assertHighlight(
|
|
findHighlightedTab({
|
|
currentPageRoute: sc.current,
|
|
historyRoutes: sc.history,
|
|
}),
|
|
sc.expected,
|
|
sc.label,
|
|
);
|
|
}
|
|
});
|
|
|
|
// =============================================================
|
|
// 场景 16: P0 修复验证 — 通用化 DIY_VIEW_INDEX
|
|
// =============================================================
|
|
|
|
test('场景 16.1: 在 home page (name=DIY_VIEW_INDEX), 自点 home tab → home 高亮', () => {
|
|
// 模拟: 用户在 home page, 点击 home tab
|
|
// verify 的 DIY_VIEW_INDEX 短路 + step 2 精确匹配, 都应高亮
|
|
// (实际测试中, 短路 vs 精确匹配谁先触发不重要, 结果都是 true)
|
|
const result = checkTabBarActive(
|
|
HOME,
|
|
HOME,
|
|
STANDARD_TABBAR,
|
|
{ pages: HOME },
|
|
{},
|
|
{ noTabPages: NO_TAB_PAGES, pageTabMap: PAGE_TAB_MAP, pageTabWildcards: PAGE_TAB_WILDCARDS },
|
|
);
|
|
assert.equal(result, true, '在 home 上点 home tab → 高亮');
|
|
});
|
|
|
|
test('场景 16.2: 在 home page, 点 cart tab → cart 不亮 (无来源记忆)', () => {
|
|
// 用户在 home, 没去过 cart, packageEntryTab 只有 pages
|
|
const result = checkTabBarActive(
|
|
CART,
|
|
HOME,
|
|
STANDARD_TABBAR,
|
|
{ pages: HOME },
|
|
{},
|
|
{ noTabPages: NO_TAB_PAGES, pageTabMap: PAGE_TAB_MAP, pageTabWildcards: PAGE_TAB_WILDCARDS },
|
|
);
|
|
// step 5 pageTabMap['/pages/index/index'] = HOME → 询问 cart 链接 → false
|
|
assert.equal(result, false, '在 home 上点 cart → cart 不该高亮');
|
|
});
|
|
|
|
test('场景 16.3: 在 home page, 点 merchant tab → 走 PAGE_TAB_MAP 兜底', () => {
|
|
// pageTabMap['/pages/index/index'] = HOME, 询问 merchant → false
|
|
const result = checkTabBarActive(
|
|
MERCHANT,
|
|
HOME,
|
|
STANDARD_TABBAR,
|
|
{ pages: HOME },
|
|
{},
|
|
{ noTabPages: NO_TAB_PAGES, pageTabMap: PAGE_TAB_MAP, pageTabWildcards: PAGE_TAB_WILDCARDS },
|
|
);
|
|
assert.equal(result, false, 'home → merchant: HOME 优先, merchant 不亮');
|
|
});
|
|
|
|
// =============================================================
|
|
// 场景 17: P0-2 修复 — tabBarList 异步加载竞态
|
|
// =============================================================
|
|
|
|
test('场景 17.1: tabBarList 异步加载场景 (文档化行为)', () => {
|
|
// 实际修复在 vue 组件里 (watch tabBarReady), 这里验证 resolveEntryTab 在 tabBarList 空时的行为
|
|
// 修复前: tabBarList 为空时, step 2 history-walk 会得到空 tabUrls, 走 fallback
|
|
// 修复后: resolveEntryTab 直接 return null, 不会用空 tabUrls 计算
|
|
const r1 = resolveEntryTab({
|
|
currentPageRoute: DIY,
|
|
tabBarList: { list: [] }, // 空
|
|
historyRoutes: [HOME],
|
|
noTabPages: NO_TAB_PAGES,
|
|
});
|
|
assert.equal(r1, null, 'tabBarList 空时, resolveEntryTab 应安全返回 null');
|
|
|
|
const r2 = resolveEntryTab({
|
|
currentPageRoute: DIY,
|
|
tabBarList: null, // null
|
|
historyRoutes: [HOME],
|
|
noTabPages: NO_TAB_PAGES,
|
|
});
|
|
assert.equal(r2, null, 'tabBarList null 时, resolveEntryTab 应安全返回 null');
|
|
});
|
|
|
|
// =============================================================
|
|
// 场景 18: P1-4 调试开关 (__TABBAR_DEBUG__)
|
|
// =============================================================
|
|
|
|
test('场景 18.1: __TABBAR_DEBUG__ 开启时, 决策路径有 console.log', () => {
|
|
// 在沙箱里, 临时设 globalThis.__TABBAR_DEBUG__ = true 并捕获 console.log
|
|
const logCapture = [];
|
|
const origLog = console.log;
|
|
const origGlobal = globalThis.__TABBAR_DEBUG__;
|
|
console.log = (...a) => logCapture.push(a.join(' '));
|
|
globalThis.__TABBAR_DEBUG__ = true;
|
|
|
|
try {
|
|
checkTabBarActive(
|
|
HOME,
|
|
DIY,
|
|
STANDARD_TABBAR,
|
|
{},
|
|
{},
|
|
{ noTabPages: NO_TAB_PAGES, pageTabMap: PAGE_TAB_MAP, pageTabWildcards: PAGE_TAB_WILDCARDS },
|
|
);
|
|
} finally {
|
|
console.log = origLog;
|
|
globalThis.__TABBAR_DEBUG__ = origGlobal;
|
|
}
|
|
|
|
assert.ok(logCapture.length > 0, `应至少有一次 console.log, 实际 ${logCapture.length} 次`);
|
|
assert.ok(logCapture.some(l => l.includes('[TABBAR]')), '日志应包含 [TABBAR] 前缀');
|
|
});
|
|
|
|
test('场景 18.2: __TABBAR_DEBUG__ 未开启时, 决策路径无 console.log', () => {
|
|
const logCapture = [];
|
|
const origLog = console.log;
|
|
const origGlobal = globalThis.__TABBAR_DEBUG__;
|
|
console.log = (...a) => logCapture.push(a.join(' '));
|
|
globalThis.__TABBAR_DEBUG__ = false;
|
|
|
|
try {
|
|
checkTabBarActive(
|
|
HOME,
|
|
DIY,
|
|
STANDARD_TABBAR,
|
|
{},
|
|
{},
|
|
{ noTabPages: NO_TAB_PAGES, pageTabMap: PAGE_TAB_MAP, pageTabWildcards: PAGE_TAB_WILDCARDS },
|
|
);
|
|
} finally {
|
|
console.log = origLog;
|
|
globalThis.__TABBAR_DEBUG__ = origGlobal;
|
|
}
|
|
|
|
const tabbarLogs = logCapture.filter(l => l.includes('[TABBAR]'));
|
|
assert.equal(tabbarLogs.length, 0, '__TABBAR_DEBUG__ 关闭时不应有 [TABBAR] 日志');
|
|
});
|
|
|
|
test('场景 18.3: opts.debug = true 也能触发调试', () => {
|
|
const logCapture = [];
|
|
const origLog = console.log;
|
|
const origGlobal = globalThis.__TABBAR_DEBUG__;
|
|
console.log = (...a) => logCapture.push(a.join(' '));
|
|
globalThis.__TABBAR_DEBUG__ = false;
|
|
|
|
try {
|
|
checkTabBarActive(
|
|
HOME,
|
|
DIY,
|
|
STANDARD_TABBAR,
|
|
{},
|
|
{},
|
|
{ noTabPages: NO_TAB_PAGES, pageTabMap: PAGE_TAB_MAP, pageTabWildcards: PAGE_TAB_WILDCARDS, debug: true },
|
|
);
|
|
} finally {
|
|
console.log = origLog;
|
|
globalThis.__TABBAR_DEBUG__ = origGlobal;
|
|
}
|
|
|
|
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');
|
|
}); |