test(ui): 候选收敛到 1/5 + 定名/端口/状态口径/动效成族的断言

第十一轮用户五条:去掉 etaHEN 状态显示、新项目起新名字与新端口、HTTP 与 SMB
服务也用「小绿灯」、demo 只留 1 和 5、把 demo1 进度条的闪光效果移植到 demo5
并在其他能加的地方也加。

拍板:项目定名 PS5 Nexus(仓库/ELF ps5-nexus),默认端口 2026。

- ui_demos_check.mjs:DEMOS 5 档 → 2 档(B/C/D 归档到工作区根的
  _retired-demos/),删掉对应断言 209 → 96;再加 3 条回归钉(demo1 三盏绿灯
  与地址都在吸顶导航条里且状态区不列打包发行版、demo5 状态区口径、demo5
  运行中进度条有扫光而静态容量条没有)→ 99 项全过。
- proposal_check.mjs:209 → 99,并新增 8 条命名/状态/动效关键词断言,另加
  一条「8888 只能作为『已改掉』的引述出现」的引述约束。

注:断言一律用渲染后的纯文本片段。textContent 会剥离标签,写
"<code>2026</code>" 这类关键词永远匹配不上,且不会抛错(假失败)。
This commit is contained in:
Songlx516 committed 2026-09-27 12:46:19 +08:00
1 parent 400784942e
commit 9c06fca17f
2 files changed
+50 -13

No files matched your search

+23 -1
View File
@@ -126,13 +126,35 @@ for (const key of [
"局域网地址而非回环", // 127.0.0.1 is useless on the OTHER device
"::after", // hit area is expanded separately from visual height
"IP:PORT", // the accepted shape
"209 项", // the acceptance count this round reached
"99 项", // acceptance count (was 209; B/C/D retired in round 11)
]) {
check(r.body.includes(key), `local-address rule present: ${key}`);
}
// the install-layer AuthID was mis-stated in section 6 until this round; it may only
// survive as a quoted correction, never as a live claim
quotedOnly("0x3800000000000010", /0 命中|作废|误记|误写|上一轮/, "no live '0x3800000000000010' AuthID survives");
// naming / port / status wording / motion family (2026-09-27 round 11). The project is
// `PS5 Nexus` on port 2026; the status strip lists ONLY running services (never a
// third-party bundle's install state -- we depend on the kstuff leaf, not on a packager);
// and motion is a deliberate FAMILY (running progress bar sweep / breathing LEDs / CTA
// sheen) rather than scattered decoration. Losing any of these silently re-opens
// decisions the user already made.
for (const key of [
"PS5 Nexus", // the project name replacing "文件与安装中心"
"ps5-nexus", // repo / ELF name
"默认 2026", // the new default port (was 8888).
// NB: assert on rendered text, not markup — `textContent` strips tags, so a key like
// "<code>2026</code>" can never match and would fail forever without anyone noticing.
"状态区只列「正在跑的服务」", // the status-strip rule
"打包发行版", // ...and what must NOT be listed there
"动效要克制", // the motion-family rule
"var(--pri-fg)", // inverted-button sheen must use the fg colour
"首尾同色", // seamless loop for the sweep gradient
]) {
check(r.body.includes(key), `naming/status/motion content present: ${key}`);
}
// 8888 may only survive as a quoted "we changed it" note, never as the live default
quotedOnly("默认 <code>8888</code>", /第六轮|改过来|避开|旧仓/, "no live '8888 is the default port' claim survives");
console.log(" sections: " + r.h2.join(" | "));
await page.screenshot({ path: OUT + "/proposal-wide.png", fullPage: true });
await page.close();
+27 -12
View File
@@ -1,7 +1,8 @@
// ============================================================
// UI 风格 demo 校验 + 截图 + 对比页生成
// 跑法: node .build/ui_demos_check.mjs
// 检查对象:工作区根的 ps5-ui-demo-{1..5}-*.html(这些 HTML 本身不入库)
// 检查对象:工作区根的 ps5-ui-demo-{1,5}-*.html(这些 HTML 本身不入库;
// 2026-09-27 起 B/C/D 三个候选已归档到工作区根的 _retired-demos/)
//
// 为什么这个脚本值得存在:
// ① 溢出:transform 移出视口的抽屉会撑开 documentElement 滚动区,
@@ -22,24 +23,19 @@ const ROOT = "C:/Users/songl/Desktop/Web File Manager";
const OUT = "C:/Users/songl/AppData/Local/Temp/wfm-ui";
fs.mkdirSync(OUT, { recursive: true });
// ⚠️ 2026-09-27 第五轮:候选收敛到 A 与 E 两条,B/C/D 已归档到工作区根的 _retired-demos/。
// 删掉它们的同时也删掉对应断言 —— 为已经出局的风格维持检查,成本远大于收益。
const DEMOS = [
{ key: "demo1", file: "ps5-ui-demo-1-console.html", name: "风格 A · 主机大厅" },
{ key: "demo2", file: "ps5-ui-demo-2-workbench.html", name: "风格 B · 双栏工作台" },
{ key: "demo3", file: "ps5-ui-demo-3-monitor.html", name: "风格 C · 终端监控台" },
{ key: "demo4", file: "ps5-ui-demo-4-bento.html", name: "风格 D · Bento 仪表盘" },
{ key: "demo5", file: "ps5-ui-demo-5-harness.html", name: "风格 E · Harness 开发者页" },
];
// 备用主题方向:demo4 默认亮 → 切暗;demo5 默认暗(忠于站点)→ 切亮。
// 两个方向都验,避免只测「亮→暗」一半。
const ALT_THEME = { demo4: "dark", demo5: "light" };
// 备用主题方向:只剩 demo5 是双主题(默认暗,忠于站点)→ 切亮再验一遍。
const ALT_THEME = { demo5: "light" };
// 每个 demo 各自的对比度采样点:正文 / 次要文字 / 强调文字 / 有底色的提示条
const CONTRAST_TARGETS = {
demo1: [["h1", "大标题"], [".crumb", "次要说明"], [".row .meta", "行内数字"], [".chip", "状态胶囊"], [".banner", "提示条"], [".ipchip .ipv", "本机地址"]],
demo2: [["h2", "标题"], [".rootbar", "路径条"], [".li .d", "行内次要"], [".bot", "底部状态条"], [".note.bad", "报错条"], [".ipchip .ipv", "本机地址"]],
demo3: [["h2", "面板标题"], [".kv div", "键值(左右混排)"], [".log .m", "日志正文"], [".sub", "辅助文字"], [".tag.q", "标签"], [".ipchip .ipv", "本机地址"]],
demo4: [["#sub", "副标题"], [".hero-stats span", "指标说明"], [".tile span", "卡片说明"], [".note.info", "信息条"], [".note.bad", "安全提示条"], [".ipchip .ipv", "本机地址"]],
// demo5 的采样点刻意跨了「页面底 / 卡片底 / 强调底」三种底色,
// 因为这套语言全靠近黑底 + 极低对比叠层,底色一变就容易掉出阈值。
// ⚠️ 六处全部落在**默认视图**内:视图化之后其余视图是 display:none,
@@ -55,6 +51,16 @@ const check = (ok, name) => { console.log((ok ? " PASS " : " FAIL ") + name)
"Attempting to serialize unexpected value"(本次真踩过)。 */
const EXTRA = {
demo1: [
["三盏状态绿灯与地址都在吸顶导航条里,且状态区不列第三方打包发行版",
`() => { const rail = document.querySelector(".toprail"); if (!rail) return false;
const r = rail.getBoundingClientRect();
const items = [...rail.querySelectorAll(".led, .chip")].filter(e => /kstuff|HTTP|SMB|\\d+\\.\\d+/.test(e.textContent));
if (items.length < 4) return false; // 三盏灯 + 地址胶囊
const t = rail.textContent;
if (!/kstuff/.test(t) || !/HTTP/.test(t) || !/SMB/.test(t)) return false;
if (/etaHEN/.test(t)) return false; // 状态区只列正在跑的服务
return items.every(e => { const b = e.getBoundingClientRect();
return b.top >= r.top - 1 && b.bottom <= r.bottom + 1; }); }`],
["导航改到顶部后触控目标仍 ≥44px(PS5 用触摸板光标)",
`() => [...document.querySelectorAll(".nav button")].every(b => b.getBoundingClientRect().height >= 44)`],
["导航不再产生左侧竖栏(横向空间全让给内容)",
@@ -62,6 +68,15 @@ const EXTRA = {
return r.width > 900 && r.height <= 80; }`],
],
demo5: [
["状态区只列正在跑的服务,不列第三方打包发行版",
`() => { const el = document.querySelector(".topbar"); if (!el) return false;
const t = el.textContent;
return /kstuff/.test(t) && /HTTP/.test(t) && /SMB/.test(t) && !/etaHEN|未安装/.test(t); }`],
["运行中的进度条有扫光,且静态容量条没有(在跑的才算)",
`() => { const p = document.querySelector(".track.pulse i");
if (!p || getComputedStyle(p).animationName === "none") return false;
const m = document.querySelector(".meter .bar i");
return !m || getComputedStyle(m).animationName === "none"; }`],
["游戏页有封面网格(≥6 张封面)",
`() => document.querySelectorAll("#view-library .gcard .cover").length >= 6`],
["封面有「抽不到 icon0.png」的回退态(一排卡片里不留空洞)",
@@ -272,7 +287,7 @@ for (const d of DEMOS) {
});
const foc = await p.evaluate(() => {
// 只挑「当前可见」的候选 —— 隐藏视图里的列表聚焦不上,
// 会给出 outline=0 shadow=0 的假失败(demo4 默认是总览视图,踩过)。
// 会给出 outline=0 shadow=0 的假失败(曾经有个候选默认停在总览视图,踩过)。
const vis = el => el && el.offsetParent !== null && el.getClientRects().length > 0;
const sel = [".row[tabindex]", ".li[tabindex]", ".lr[tabindex]", ".tabs button", ".seg button", ".nav button"]
.map(s => [...document.querySelectorAll(s)].find(vis)).find(Boolean);
@@ -409,7 +424,7 @@ for (const d of DEMOS) {
check(ps5.ipW > 0 && ps5.ipTop >= 0 && ps5.ipBottom <= 970,
`PS5 档本机地址在首屏内(top=${ps5.ipTop} bottom=${ps5.ipBottom})`);
/* 只有「视图化」页面(带 data-view 导航)才验「导航置顶且单行」。
风格 B/C/D 是单页长滚动、压根没有一级导航 —— 硬套只会造出假失败,
单页长滚动的候选压根没有一级导航 —— 硬套只会造出假失败,
而假失败和假通过一样有毒:它会让人开始忽略这一组断言。 */
if (ps5.n >= 2) {
check(ps5.n >= 4 && ps5.rows === 1, `PS5 档一级导航单行不折行(${ps5.n} 项 / ${ps5.rows} 行)`);