diff --git a/client/electron/test/harmony-nav.test.mjs b/client/electron/test/harmony-nav.test.mjs
index 2ac8ac2..837eead 100644
--- a/client/electron/test/harmony-nav.test.mjs
+++ b/client/electron/test/harmony-nav.test.mjs
@@ -932,7 +932,22 @@ function navItemsOf(root) {
const screenW = screenWidthOf(root);
const boxW = x2 - x1;
const boxH = y2 - y1;
- return x1 < screenW / 6 && boxW < screenW / 4 && boxH > screenH * 0.08;
+ /*
+ * ★ 阈值用**比例**而不是绝对 px,且必须容得下真实的 48vp 项。
+ * 实测(密度 2.875):项 138×138px、屏 3184×2232 ⇒
+ * x1=45 < 3184/6=531 ✓、boxW=138 < 3184/4=796 ✓、
+ * 但 boxH=138 需要 > 2232*0.08=178 ✗ ⇒ 四项**全被滤掉**。
+ * 也就是说这个阈值我第一版写大了,只是当时项被压成 39vp 才"恰好"通过
+ * (那是别的 bug,不是阈值对)。
+ * 改成 `screenH * 0.04`(=89px):48vp 项(138px)过得去,
+ * 而内容区那些横向长条(高 43px < 89)仍然被排除。
+ *
+ * ★ 还要**要求有文字标签**:品牌标也是左上的可点方块(实测 `Stack [57,174][172,289]`
+ * 40vp、无文字),不加这一条它会混进来,而它不是导航项 ——
+ * 它是"点它回家"的品牌按钮(WebUI 的 `brand-mark`)。
+ */
+ return x1 < screenW / 6 && boxW < screenW / 4 && boxH > screenH * 0.04
+ && textsUnder(n).length > 0;
});
}
@@ -1016,15 +1031,28 @@ test('★ 行为(设备):底栏真渲染了可点的导航项(dumpLayout
* 每个导航项子树里要有 `Path`(`AmIcon` 的画法)且**不该有 Text**
* (有文字就说明有人往纯图标轨里塞了标签,那正是被否掉的那个方案)。
*/
+ /*
+ * ★★ 2026-09-18 修:这一段原来断言「宽屏侧栏项**不该有文字**」,理由是
+ * "用户明确否掉了带文字的方案"。**那是编的**:
+ * · 用户 2026-09-16 说的是「**底部导航栏**不允许有文字」,不是侧栏;
+ * · WebUI `Sidebar.tsx:110` 有 `{short}`
+ * —— 侧栏**一直有**文字标签(通信/日历/联系)。
+ * 我据此把鸿蒙侧栏做成了纯图标,还写了判据把它锁死 —— 判据锁住的是我的错误。
+ *
+ * 现在按两侧的**共同事实**判:侧栏项 = 图标(Path)+ 文字标签。
+ * 图标画出 + 文字命中源码清单,两条都要。
+ */
+ const sourceLabels = new Set(N.NAV_ITEMS.map(i => i.label));
for (const it of navItems) {
- const kinds = textsUnder(it); // 复用 walk:非空 Text
- assert.equal(kinds.length, 0,
- `宽屏侧栏项不该有文字(纯图标轨,用户明确否掉了带文字的方案);` +
- `实际:${kinds.join('、')}`);
const paths = [...walk(it)].filter(x => x.attributes?.type === 'Path');
assert.ok(paths.length >= 1,
`宽屏侧栏项(bounds=${it.attributes.bounds})要画出图标(Path 节点)—— ` +
'没有 Path 说明图标是空的,侧栏会变成一排摸不着的空白区');
+ const texts = textsUnder(it);
+ const matched = texts.filter(l => sourceLabels.has(l));
+ assert.ok(matched.length >= 1,
+ `宽屏侧栏项要带文字标签且命中源码 NAV_ITEMS(WebUI Sidebar 有 {short});` +
+ `实际文字:${texts.join('、')}`);
}
}
/*
@@ -1091,15 +1119,21 @@ test('★ 判据自检:底栏取值逻辑 —— 好样本取得到、缺底
*
* 合成树按实测形状造:3184×2232、侧栏项 `Column [28,985][201,1380]`、子树只有 Path。
*/
- const iconRail = (y0) => ({
- // 侧栏项:宽 ~173(< W/4)、高 ~395(> H*0.08)、x1=28(< W/6)
- attributes: { type: 'Column', bounds: `[28,${y0}][201,${y0 + 395}]`, clickable: 'true' },
- children: [{ attributes: { type: 'Stack', bounds: `[77,${y0 + 160}][152,${y0 + 235}]`, clickable: 'false' },
- children: [{ attributes: { type: 'Path', bounds: `[79,${y0 + 164}][148,${y0 + 230}]`, clickable: 'false' }, children: [] }] }],
+ /*
+ * 侧栏项样本按**实测形状**造(密度 2.875、三折叠展开态):
+ * Column [45,312][183,450] = 138×138px,子树是 AmIcon(Path) + Text('通信')。
+ */
+ const iconRail = (y0, label) => ({
+ attributes: { type: 'Column', bounds: `[45,${y0}][183,${y0 + 138}]`, clickable: 'true' },
+ children: [
+ { attributes: { type: 'Stack', bounds: `[73,${y0 + 20}][146,${y0 + 93}]`, clickable: 'false' },
+ children: [{ attributes: { type: 'Path', bounds: `[75,${y0 + 22}][144,${y0 + 91}]`, clickable: 'false' }, children: [] }] },
+ text(label, `[85,${y0 + 100}][143,${y0 + 134}]`)
+ ],
});
const wideRoot = {
attributes: { type: 'Row', bounds: '[0,0][3184,2232]', clickable: 'false' },
- children: [iconRail(985), iconRail(1380),
+ children: [iconRail(312, '通信'), iconRail(462, '日历'),
// 内容区一张宽卡片(可点、有文字)—— 它**不该**被当成导航项
{ attributes: { type: 'ListItem', bounds: '[263,212][1115,568]', clickable: 'true' },
children: [text('pi', '[309,245][360,280]')] }],
@@ -1107,11 +1141,12 @@ test('★ 判据自检:底栏取值逻辑 —— 好样本取得到、缺底
assert.equal(isWideLayout(wideRoot), true, '自检:3184×2232 要判成宽屏');
const wideItems = navItemsOf(wideRoot);
assert.equal(wideItems.length, 2,
- `★ 自检失败:宽屏侧栏应当取到 2 个图标项(实际 ${wideItems.length})—— ` +
+ `★ 自检失败:宽屏侧栏应当取到 2 个导航项(实际 ${wideItems.length})—— ` +
'取到 3 个说明内容卡片被误当导航项,"导航没挂"就永远判不出来');
for (const it of wideItems) {
- assert.equal(textsUnder(it).length, 0, '自检:侧栏项样本本来就没有文字(纯图标轨)');
- assert.ok([...walk(it)].some(x => x.attributes?.type === 'Path'), '自检:侧栏项要含 Path');
+ assert.ok([...walk(it)].some(x => x.attributes?.type === 'Path'), '自检:侧栏项要含 Path(图标画出来了)');
+ assert.ok(textsUnder(it).some(l => ['通信','日历','联系人','我的'].includes(l)),
+ '自检:侧栏项要带命中清单的文字标签(WebUI Sidebar 有 {short})');
}
// 窄屏样本不能被误判成宽屏(否则上面那套形状条件会去滤底栏项)
assert.equal(isWideLayout(good), false, '自检:1200×2800 要判成窄屏');
diff --git a/client/electron/test/harmony-widescreen.test.mjs b/client/electron/test/harmony-widescreen.test.mjs
index d525399..2e16fec 100644
--- a/client/electron/test/harmony-widescreen.test.mjs
+++ b/client/electron/test/harmony-widescreen.test.mjs
@@ -9,6 +9,7 @@
// 这条判据钉的是 Sidebar 自己的形状:宽 60vp、三项导航 + 设置、选中态只换颜色。
import { code, prose, stripComments } from './lib/read.mjs';
+import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { test } from 'node:test';
@@ -40,37 +41,84 @@ test('① 宽度:SIDEBAR_WIDTH = 60vp(WebUI 的 `w-[60px]` 是同一数字
assert.match(main, /WideSidebar\(/, 'MainPage 要真正挂 WideSidebar');
});
-test('② 三项导航 + 设置按钮:内容与 WebUI Sidebar 对齐', () => {
- const sidebar = read('pages/WideSidebar.ets');
- // NAV_CONTENT_ITEMS 遍历(**只内容窗格** —— 设置已单列在下面;若遍历 NAV_ITEMS
- // 会因第 4 项「我的」出现两个 person 图标)
- assert.match(sidebar, /ForEach\(NAV_CONTENT_ITEMS,/, '要遍历内容窗格清单(与底部导航前三项同源)');
- // 品牌标 + 三项导航 + 设置(与 WebUI:brand / nav / settings 的分组一致)
- assert.match(sidebar, /品牌标/, '要有品牌标(注释里的分组依据)');
- assert.match(sidebar, /iconName: 'person'/, '设置按钮要用 person 图标(WebUI 侧沿用的同一枚)');
- assert.match(sidebar, /onSettings/, '设置按钮要回调 onSettings(点击后由 MainPage 路由)');
-});
-
-test('③ 选中态只换颜色(与底部导航同一套纪律):纯图标、无文字', () => {
+test('② 四项导航(与底栏同源):不再有单列的"设置"入口', () => {
+ /*
+ * ★★ 2026-09-18 重写。原来这条判的是「三项导航 + 设置按钮」,
+ * 依据是"设置单列在下面、遍历 NAV_ITEMS 会出现两个 person 图标"。
+ * 那个依据**两半都错**:
+ * · `NAV_CONTENT_ITEMS = NAV_ITEMS.slice(0, NAV_CONTENT_COUNT)` 而
+ * `NAV_CONTENT_COUNT = 4` ⇒ **四项全在**(含「我的」),不存在"只有前三项";
+ * · 单列的"设置"走 `onSettings → pushUrl('pages/SettingsPage')`,
+ * 而那正是用户 2026-09-17 报过的「我的页面完全没有遵守 nav 的导航规则」
+ * (底栏那一支改成了窗格,侧栏这一支漏了)。
+ *
+ * WebUI `Sidebar.tsx` 的真实形状:`navItems` 三项 + **底部一簇**
+ * (账号头像 / 主题切换 / 退出)。「我的」在 WebUI 是 `viewMode === 'account'`,
+ * 由侧栏底部那个**头像按钮**进入 —— 也就是说它是**导航项**,不是"推出去的页"。
+ * 鸿蒙的对应物是第 4 项「我的」窗格(与底栏一致)。
+ */
const sidebar = read('pages/WideSidebar.ets');
const code_ = stripComments(sidebar);
+ assert.match(code_, /ForEach\(NAV_CONTENT_ITEMS,/,
+ '侧栏要遍历 NAV_CONTENT_ITEMS(与底栏同一个清单 —— 两处各留一份会漂移)');
+ // 品牌标:WebUI 是 `