|
| 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