Skip to content

Commit d1a5045

Browse files
hyqzzclaude
andcommitted
fix: directory panel scroll (#58) and time warp stuck negative (#59)
- #58: desktop #directory had no max-height so a long directory grew past the viewport and #dir-body overflow never activated; add viewport-bound max-height (calc(100dvh - 124px)) plus min-height:0 on the flex child - #59: warpUp() incremented the ladder index regardless of sign, so once negative, ] pushed the rate MORE negative; now ] always steps toward positive (descend ladder while negative, flip -1x -> +1x, then ascend) - add tools/repro-issues-58-59-60.mjs: puppeteer end-to-end verification for all three issues (16 checks, includes official-domain ICP gating) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 0bcf6c8 commit d1a5045

3 files changed

Lines changed: 164 additions & 2 deletions

File tree

src/style.css

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,9 @@ html, body {
114114
#directory {
115115
position: fixed; top: 110px; left: 14px; z-index: 20;
116116
width: 260px;
117+
/* 面板必须限制在视口内,#dir-body 的 overflow 才会生效(issue #58) */
118+
max-height: calc(100vh - 124px);
119+
max-height: calc(100dvh - 124px);
117120
background: rgba(8, 16, 28, 0.82); border: 1px solid rgba(110, 160, 210, 0.28);
118121
border-radius: 10px; backdrop-filter: blur(8px);
119122
display: flex; flex-direction: column;
@@ -128,7 +131,8 @@ html, body {
128131
touch-action: manipulation;
129132
}
130133
.dir-head:hover { background: rgba(60, 110, 170, 0.15); border-radius: 10px 10px 0 0; }
131-
#dir-body { overflow-y: auto; padding: 6px 8px; flex: 1; touch-action: pan-y; }
134+
/* min-height:0:flex 子项默认 min-height:auto 会阻止收缩,导致滚动条永不出现(issue #58) */
135+
#dir-body { overflow-y: auto; padding: 6px 8px; flex: 1; min-height: 0; touch-action: pan-y; }
132136
#dir-body details { margin-bottom: 4px; }
133137
#dir-body summary {
134138
cursor: pointer; padding: 6px 8px; font-size: 13px; color: #9fc6ea;

src/ui/hud.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,16 @@ export class HUD {
3232
warpRate(paused) {
3333
return paused ? 0 : this.warpSign * WARP_LADDER[this.warpIndex];
3434
}
35-
warpUp() { this.warpIndex = Math.min(WARP_LADDER.length - 1, this.warpIndex + 1); }
35+
// 倍率阶梯按符号对称:… −10x ← −1x ← +1x → +10x …
36+
// ] 恒向正方向走(负倍率时先降档,到 −1x 再按翻回 +1x);[ 恒向负方向走
37+
warpUp() {
38+
if (this.warpSign < 0) {
39+
if (this.warpIndex === 0) this.warpSign = 1;
40+
else this.warpIndex--;
41+
} else {
42+
this.warpIndex = Math.min(WARP_LADDER.length - 1, this.warpIndex + 1);
43+
}
44+
}
3645
warpDown() {
3746
if (this.warpIndex === 0 && this.warpSign > 0) this.warpSign = -1;
3847
else if (this.warpSign < 0) this.warpIndex = Math.min(WARP_LADDER.length - 1, this.warpIndex + 1);

tools/repro-issues-58-59-60.mjs

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
// Issues #58/#59/#60 复现+验证(需 dev server: npm run dev)
2+
// #58 左侧天体目录内容超高时无法滚动 —— 桌面端 #directory 缺 max-height、#dir-body 缺 min-height:0
3+
// #59 时间倍率进入负数后按 ] 无法回到正数 —— warpUp 未处理负号方向
4+
// #60 备案号硬编码 —— 自部署域名也显示官方 ICP 号;改为仅 *.icodestar.net 显示
5+
// 用法:node tools/repro-issues-58-59-60.mjs [port]
6+
7+
import puppeteer from 'puppeteer';
8+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
9+
const PORT = process.argv[2] || '5173';
10+
11+
let pass = 0, fail = 0;
12+
const check = (name, cond, detail = '') => {
13+
if (cond) { pass++; console.log(` ✅ ${name}`); }
14+
else { fail++; console.log(` ❌ ${name} —— ${detail}`); }
15+
};
16+
17+
// 视口须 >1024 以避开移动端判定(quality.js smallScreen 分支,触屏机型上会切成底部抽屉布局)。
18+
// 目录全展开后内容 ~2300px,1100px 视口下仍必然溢出,可复现 #58 场景。
19+
const browser = await puppeteer.launch({
20+
headless: true,
21+
args: ['--no-sandbox', '--use-angle=swiftshader', '--enable-unsafe-swiftshader', '--window-size=1280,1100', '--hide-scrollbars'],
22+
defaultViewport: { width: 1280, height: 1100 },
23+
});
24+
const page = await browser.newPage();
25+
// 触屏开发机上 headless 会继承 pointer:coarse / maxTouchPoints —— 伪装成纯桌面设备,
26+
// 否则 quality.js 判定为移动端、目录切成底部抽屉,无法测桌面路径
27+
await page.evaluateOnNewDocument(() => {
28+
Object.defineProperty(navigator, 'maxTouchPoints', { get: () => 0 });
29+
const origMM = window.matchMedia.bind(window);
30+
window.matchMedia = (q) => {
31+
if (/pointer:\s*coarse/.test(q)) {
32+
return { matches: false, media: q, addEventListener() {}, removeEventListener() {}, addListener() {}, removeListener() {} };
33+
}
34+
return origMM(q);
35+
};
36+
});
37+
const errors = [];
38+
page.on('pageerror', (e) => errors.push(e.message));
39+
await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'networkidle2', timeout: 120000 });
40+
await page.waitForSelector('#start-btn', { visible: true, timeout: 120000 });
41+
42+
// ---- #60 备案号:非官方域名(localhost)不得显示 ----
43+
console.log('\n[Issue-60] 备案号仅官方域名显示');
44+
const beian = await page.evaluate(() => {
45+
const el = document.getElementById('beian');
46+
return { exists: !!el, hidden: el?.hidden, text: el?.textContent ?? '', visible: el ? getComputedStyle(el).display !== 'none' && !el.hidden && el.textContent.length > 0 : false };
47+
});
48+
check('localhost 下 #beian 不显示(hidden 且无文本)', beian.exists && beian.hidden && beian.text === '',
49+
JSON.stringify(beian));
50+
// 域名门控正则本身的正反用例
51+
const re = /(^|\.)icodestar\.net$/;
52+
check('正则匹配官方域名 sw.icodestar.net / icodestar.net',
53+
re.test('sw.icodestar.net') && re.test('icodestar.net'));
54+
check('正则拒绝自部署域名(example.com / evil-icodestar.net / hyqzz.github.io)',
55+
!re.test('example.com') && !re.test('evil-icodestar.net') && !re.test('hyqzz.github.io'));
56+
57+
// ---- 进入应用 ----
58+
await page.click('#start-btn');
59+
await sleep(3000);
60+
61+
// ---- #58 目录滚动 ----
62+
console.log('\n[Issue-58] 天体目录超高时可滚动');
63+
const isTouchLayout = await page.evaluate(() => document.documentElement.classList.contains('touch'));
64+
check('桌面布局生效(非移动端抽屉)', !isTouchLayout, 'html.touch 被激活,需增大视口');
65+
const dir = await page.evaluate(() => {
66+
// 展开全部分组,最大化内容高度(复现截图中的长目录)
67+
document.querySelectorAll('#dir-body details').forEach((d) => { d.open = true; });
68+
const panel = document.getElementById('directory');
69+
const body = document.getElementById('dir-body');
70+
const pr = panel.getBoundingClientRect();
71+
const overflow = body.scrollHeight > body.clientHeight;
72+
const before = body.scrollTop;
73+
body.scrollTop = 99999;
74+
const after = body.scrollTop;
75+
body.scrollTop = 0;
76+
return {
77+
panelBottom: pr.bottom, viewportH: innerHeight,
78+
scrollH: body.scrollHeight, clientH: body.clientHeight,
79+
overflow, scrolled: after > before,
80+
};
81+
});
82+
check('复现:内容高于可视区(scrollHeight > clientHeight)', dir.overflow,
83+
`scrollH=${dir.scrollH}, clientH=${dir.clientH}(未复现说明视口不够矮或目录太短)`);
84+
check('面板底边不超出视口(max-height 生效)', dir.panelBottom <= dir.viewportH + 1,
85+
`panelBottom=${dir.panelBottom.toFixed(0)}, viewport=${dir.viewportH}`);
86+
check('#dir-body 可实际滚动(scrollTop 可变)', dir.scrolled,
87+
`scrollTop 设置后=${dir.scrolled}`);
88+
89+
// 静态断言:桌面基础规则(media query 之外)必须带视口内 max-height —— 纯桌面
90+
// fine-pointer 机器不落入 @media (pointer:coarse) 分支,全靠这条规则修复 #58
91+
const cssRules = await page.evaluate(() => {
92+
const found = { dirMaxHeight: null, bodyMinHeight: null };
93+
for (const sheet of document.styleSheets) {
94+
let rules;
95+
try { rules = sheet.cssRules; } catch { continue; }
96+
for (const r of rules) {
97+
if (r.type !== CSSRule.STYLE_RULE) continue; // 只看顶层规则,排除 @media 内
98+
if (r.selectorText === '#directory' && r.style.maxHeight) found.dirMaxHeight = r.style.maxHeight;
99+
if (r.selectorText === '#dir-body' && r.style.minHeight) found.bodyMinHeight = r.style.minHeight;
100+
}
101+
}
102+
return found;
103+
});
104+
// CSSOM 可能把 calc(100dvh - 124px) 序列化为 calc(-124px + 100dvh)
105+
check('桌面基础规则 #directory 带 max-height(calc 视口内)',
106+
/calc\((100d?vh - 124px|-124px \+ 100d?vh)\)/.test(cssRules.dirMaxHeight ?? ''), `maxHeight=${cssRules.dirMaxHeight}`);
107+
check('桌面基础规则 #dir-body 带 min-height:0(flex 收缩前提)',
108+
cssRules.bodyMinHeight === '0px', `minHeight=${cssRules.bodyMinHeight}`);
109+
110+
// ---- #59 时间倍率:负 → 正可恢复 ----
111+
console.log('\n[Issue-59] 时间倍率负数后可回到正数');
112+
const rate = () => page.evaluate(() => window.__game.simClock.rate);
113+
// 每次按键后等倍率实际变化(帧循环消费 tapped 有延迟,固定 sleep 会丢按键)
114+
const press = async (code, n = 1) => {
115+
for (let i = 0; i < n; i++) {
116+
const before = await rate();
117+
await page.keyboard.press(code, { delay: 60 });
118+
const t0 = Date.now();
119+
while (Date.now() - t0 < 1500 && (await rate()) === before) await sleep(50);
120+
}
121+
};
122+
const r0 = await rate();
123+
check('初始倍率 = 1', r0 === 1, `rate=${r0}`);
124+
125+
await press('BracketLeft', 3); // 1 → −1 → −10 → −60
126+
const rNeg = await rate();
127+
check('按 [ ×3 进入负倍率(−60)', rNeg === -60, `rate=${rNeg}`);
128+
129+
await press('BracketRight', 3); // −60 → −10 → −1 → +1
130+
const rBack = await rate();
131+
check('按 ] ×3 回到 +1(修复点:负倍率下 ] 向正方向走)', rBack === 1, `rate=${rBack}`);
132+
133+
await press('BracketRight', 1);
134+
const rUp = await rate();
135+
check('继续按 ] 正常加速到 +10', rUp === 10, `rate=${rUp}`);
136+
137+
// 负方向仍正常:[ 从 +1 翻到 −1
138+
await press('BracketLeft', 2); // 10 → 1 → −1
139+
const rFlip = await rate();
140+
check('按 [ 从 +1 翻到 −1(原有行为未破坏)', rFlip === -1, `rate=${rFlip}`);
141+
await press('BracketRight', 1); // −1 → +1
142+
const rFlip2 = await rate();
143+
check('−1 时按 ] 翻回 +1', rFlip2 === 1, `rate=${rFlip2}`);
144+
145+
check('全程无页面运行时错误', errors.length === 0, errors.join(' | '));
146+
147+
await browser.close();
148+
console.log(`\n结果: ${pass} 通过, ${fail} 失败`);
149+
process.exit(fail ? 1 : 0);

0 commit comments

Comments
 (0)