Files
Toolbox/dev_test_scripts/debug/debug_schedule_reminder_iphone6s_layout.js
T
yangxiangyuan a3e8911deb feat: 新增作息报时器功能并替换短链接前缀为/to/
- 新增作息报时器独立模块,包含完整的日程管理、语音播报和待机功能
- 短链接正式前缀由/l/改为/to/,解决手机小屏下与I、i、1字形混淆问题
- 保留旧前缀/l/、/L/、/I/、/i/兼容已分发的旧短链
- 新增短码大小写兜底匹配,仅唯一匹配时生效避免歧义
- 新增防爆紧急替换功能,可轮换首页鉴权并替换短链接
- 新增多个调试和集成测试脚本,保障数据安全和功能回归
2026-10-09 14:05:36 +08:00

320 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 作息报时器 · iPhone 6s(375×667)布局体检(一次性调试脚本)
*
* 目标:在目标设备尺寸下量化检查布局是否真的有问题,而不是凭截图猜。
* 检查项:
* 1. 是否存在横向溢出(页面左右被撑破)
* 2. 滚到底时,卡片操作按钮是否被底部固定 Tab 栏遮挡
* 3. 待机大屏 / 播报全屏 是否有元素超出容器宽度
* 4. 内容是否真的能滚到底(有无被裁掉、滚不到的区域)
*
* 运行:node dev_test_scripts/debug/debug_schedule_reminder_iphone6s_layout.js
*/
const fs = require('fs')
const path = require('path')
const express = require('express')
const puppeteer = require('puppeteer')
const PORT = 8980
const BASE = `http://127.0.0.1:${PORT}`
const DB_FILE = path.join(process.cwd(), 'data', 'app_order_box_schedule_reminder.db')
const DB_EXISTED_BEFORE = fs.existsSync(DB_FILE)
// iPhone 6s:CSS 视口 375 × 667,DPR 2
const VIEWPORT = { width: 375, height: 667, deviceScaleFactor: 2, isMobile: true, hasTouch: true }
const CHROME_CANDIDATES = [
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe',
]
const findChrome = () => CHROME_CANDIDATES.find(p => fs.existsSync(p)) || undefined
const sleep = ms => new Promise(r => setTimeout(r, ms))
let problems = 0
// 脚本启动时已存在的日程 id(要保护的数据,清理阶段一律跳过)
let PRE_EXISTING_IDS = new Set()
const report = (name, ok, detail) => {
if (!ok) problems += 1
console.log(` ${ok ? '✔' : '✘'} ${name}${detail ? '\n ' + detail : ''}`)
}
const buildServer = () => {
const app = express()
app.use(express.json())
const pub = path.join(process.cwd(), 'public')
app.use(express.static(pub))
const { bindRoutes } = require('../../src/server/app_order_box')
bindRoutes(app)
return app.listen(PORT)
}
const cleanupDb = () => {
if (DB_EXISTED_BEFORE) return
;['', '-wal', '-shm'].forEach(suffix => {
const p = DB_FILE + suffix
try { if (fs.existsSync(p)) fs.unlinkSync(p) } catch (_) { /* 静默 */ }
})
}
const run = async (page) => {
console.log(`=== 布局体检:iPhone 6s ${VIEWPORT.width}×${VIEWPORT.height} ===\n`)
// ⚠️ 安全底线:先快照已有日程 id,清理时只删"本次新建"的,
// 绝不删除数据库里原本就存在的用户数据(曾因无差别删除误删过用户真实日程)。
const existing = await (await fetch(`${BASE}/api/app_order_box/kits/schedule_reminder/schedules`)).json()
PRE_EXISTING_IDS = new Set((existing.data || []).map(s => s.id))
if (PRE_EXISTING_IDS.size) {
console.log(`(已有 ${PRE_EXISTING_IDS.size} 套日程,本次不会删除它们)\n`)
}
await fetch(`${BASE}/api/app_order_box/state/active-kit`, {
method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ kitId: '' }),
})
// 造一套 6 个时刻的日程,保证列表足够长(复现截图里的场景)
await fetch(`${BASE}/api/app_order_box/kits/schedule_reminder/schedules`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: '体检日程(周三在公司)', weekdays: [3],
items: [
{ time: '15:00', name: '看下股票收盘' },
{ time: '15:50', name: '起来走动下' },
{ time: '16:29', name: '看下钉钉,有没有审批单' },
{ time: '17:30', name: '看下 hermes,自动任务执行情况' },
{ time: '17:50', name: '准备下班,上个厕所' },
{ time: '18:30', name: '下班回家路上' },
],
}),
})
await page.goto(`${BASE}/tools/web_order_box/index.html`, { waitUntil: 'networkidle2', timeout: 30000 })
await page.waitForFunction(() => {
const b = document.getElementById('btnEnterSchedule')
return b && !b.disabled
}, { timeout: 15000 })
await page.click('#btnEnterSchedule')
await sleep(4200)
// ---------- 1. 横向溢出 ----------
console.log('[1] 横向溢出检查')
const overflow = await page.evaluate(() => {
const de = document.documentElement
const offenders = []
document.querySelectorAll('#viewSchedule *').forEach(el => {
const r = el.getBoundingClientRect()
if (r.width === 0) return
if (r.right > de.clientWidth + 0.5 || r.left < -0.5) {
offenders.push({
cls: (el.className && String(el.className)) || el.tagName,
left: Math.round(r.left), right: Math.round(r.right),
})
}
})
return { clientWidth: de.clientWidth, scrollWidth: de.scrollWidth, offenders: offenders.slice(0, 8) }
})
report('页面无横向滚动', overflow.scrollWidth <= overflow.clientWidth, `clientWidth=${overflow.clientWidth} scrollWidth=${overflow.scrollWidth}`)
report('无元素超出右边界', overflow.offenders.length === 0, JSON.stringify(overflow.offenders))
// ---------- 2. 大字时钟是否溢出 ----------
console.log('\n[2] 待机大屏检查')
const clockInfo = await page.evaluate(() => {
const clock = document.getElementById('schedClock')
const panel = document.getElementById('schedStandbyPanel')
const next = document.getElementById('schedNext')
const cs = window.getComputedStyle(clock)
return {
clockFontSize: cs.fontSize,
clockScrollW: clock.scrollWidth,
clockClientW: clock.clientWidth,
panelH: Math.round(panel.getBoundingClientRect().height),
nextH: Math.round(next.getBoundingClientRect().height),
nextText: next.textContent,
viewportH: window.innerHeight,
}
})
report('时钟文字未被裁切', clockInfo.clockScrollW <= clockInfo.clockClientW + 1,
`font=${clockInfo.clockFontSize} scrollW=${clockInfo.clockScrollW} clientW=${clockInfo.clockClientW}`)
console.log(` 待机面板高 ${clockInfo.panelH}px / 视口高 ${clockInfo.viewportH}px(占 ${Math.round(clockInfo.panelH / clockInfo.viewportH * 100)}%)`)
console.log(` "下一条"文案高 ${clockInfo.nextH}px:${clockInfo.nextText}`)
// ---------- 3. 底部 Tab 栏是否遮挡内容 ----------
console.log('\n[3] 待机中状态 + 底部 Tab 栏遮挡检查')
// 先点"用这套待机",复现截图里的真实状态(大屏显示"下一条…"倒计时)
await page.evaluate(() => {
window.scrollTo(0, 0)
const b = Array.from(document.querySelectorAll('#schedulesList button'))
.find(x => String(x.textContent || '').indexOf('用这套待机') >= 0)
if (b) b.click()
})
await sleep(1200)
const nextInfo = await page.evaluate(() => {
const el = document.getElementById('schedNext')
const cs = window.getComputedStyle(el)
const lineH = parseFloat(cs.lineHeight) || 25
const h = el.getBoundingClientRect().height
return {
text: el.textContent,
height: Math.round(h),
lines: Math.max(1, Math.round(h / lineH)),
status: document.getElementById('schedStatus').textContent,
}
})
console.log(` "下一条"占 ${nextInfo.lines} 行:${nextInfo.text}`)
console.log(` 状态栏:${nextInfo.status}`)
report('"下一条"文案 2 行以内(短屏不会被撑太高)', nextInfo.lines <= 2, `实际 ${nextInfo.lines} 行`)
await page.evaluate(() => { window.scrollTo(0, document.body.scrollHeight) })
await sleep(400)
// 把内部滚动容器也拉到底
await page.evaluate(() => {
document.querySelectorAll('.motion-body').forEach(el => { el.scrollTop = el.scrollHeight })
})
await sleep(400)
const overlap = await page.evaluate(() => {
const tabBar = document.getElementById('schedTabBar')
const tabTop = tabBar.getBoundingClientRect().top
const lastCard = document.querySelector('#schedulesList .card-item:last-child')
const actions = lastCard ? lastCard.querySelector('.card-actions') : null
const body = document.querySelector('#viewSchedule .motion-body')
const cs = body ? window.getComputedStyle(body) : null
return {
tabTop: Math.round(tabTop),
tabH: Math.round(tabBar.getBoundingClientRect().height),
actionsBottom: actions ? Math.round(actions.getBoundingClientRect().bottom) : null,
bodyPadBottom: cs ? cs.paddingBottom : null,
bodyScrollTop: body ? Math.round(body.scrollTop) : null,
bodyScrollH: body ? body.scrollHeight : null,
bodyClientH: body ? body.clientHeight : null,
docScrollTop: Math.round(window.scrollY),
docScrollH: document.documentElement.scrollHeight,
docClientH: document.documentElement.clientHeight,
}
})
console.log(' ' + JSON.stringify(overlap))
report('滚到底后,卡片操作按钮完整露出(不被 Tab 栏遮挡)',
overlap.actionsBottom !== null && overlap.actionsBottom <= overlap.tabTop,
`按钮底 ${overlap.actionsBottom}px vs Tab栏顶 ${overlap.tabTop}px(Tab栏高 ${overlap.tabH}px,body padding-bottom ${overlap.bodyPadBottom})`)
// ---------- 4. 播报全屏检查 ----------
console.log('\n[4] 播报全屏弹窗检查')
await page.evaluate(() => {
const modal = document.getElementById('schedAlertModal')
modal.style.display = 'flex'
document.getElementById('schedAlertTime').textContent = '18:30'
document.getElementById('schedAlertName').textContent = '下班回家路上'
document.getElementById('schedAlertRepeat').textContent = '第 1 / 5 次播报 · 每 30 秒一次'
})
await sleep(300)
const alertInfo = await page.evaluate(() => {
const container = document.querySelector('.sched-alert-container')
const ack = document.getElementById('btnSchedAck')
const time = document.getElementById('schedAlertTime')
const r = ack.getBoundingClientRect()
return {
containerH: Math.round(container.getBoundingClientRect().height),
viewportH: window.innerHeight,
ackBottom: Math.round(r.bottom),
ackVisible: r.bottom <= window.innerHeight + 1 && r.top >= 0,
timeScrollW: time.scrollWidth,
timeClientW: time.clientWidth,
}
})
report('「我知道了」按钮在视口内可见(不用滚动)', alertInfo.ackVisible,
`按钮底 ${alertInfo.ackBottom}px / 视口高 ${alertInfo.viewportH}px,容器高 ${alertInfo.containerH}px`)
report('播报时刻大字未被裁切', alertInfo.timeScrollW <= alertInfo.timeClientW + 1,
`scrollW=${alertInfo.timeScrollW} clientW=${alertInfo.timeClientW}`)
// ---------- 5. 编辑器检查 ----------
console.log('\n[5] 日程编辑器检查')
await page.evaluate(() => {
document.getElementById('schedAlertModal').style.display = 'none'
window.scrollTo(0, 0)
document.getElementById('btnNewSchedule').click()
})
await sleep(500)
const editorOpened = await page.evaluate(() =>
window.getComputedStyle(document.getElementById('schedEditorModal')).display !== 'none')
report('编辑器已打开(测试前置条件)', editorOpened)
const editorInfo = await page.evaluate(() => {
const sheet = document.querySelector('#schedEditorModal .modal-sheet')
const saveBtn = document.getElementById('btnSchedEditorSave')
const bodyEl = document.getElementById('schedEditorBody')
const chips = Array.from(document.querySelectorAll('.weekday-chip'))
const rows = new Set(chips.map(c => Math.round(c.getBoundingClientRect().top)))
// 关键:带 √ 的 chip 文字是否比 chip 自身还宽(会被 nowrap 撑破、压到邻居)
const chipDetails = chips.map(c => {
const r = c.getBoundingClientRect()
return {
text: String(c.textContent || '').trim(),
boxW: Math.round(r.width),
// scrollWidth > clientWidth 说明内容被裁/溢出
overflow: c.scrollWidth - c.clientWidth,
}
})
return {
sheetH: Math.round(sheet.getBoundingClientRect().height),
viewportH: window.innerHeight,
weekdayRows: rows.size,
chipDetails: chipDetails,
overflowChips: chipDetails.filter(c => c.overflow > 1),
saveBtnBottom: saveBtn ? Math.round(saveBtn.getBoundingClientRect().bottom) : null,
bodyScrollH: bodyEl.scrollHeight,
bodyClientH: bodyEl.clientHeight,
docScrollW: document.documentElement.scrollWidth,
docClientW: document.documentElement.clientWidth,
}
})
report('星期 chip 单行显示', editorInfo.weekdayRows === 1, `实际占用 ${editorInfo.weekdayRows} 行`)
report('星期 chip 文字未溢出自身边框(带 √ 也放得下)', editorInfo.overflowChips.length === 0,
JSON.stringify(editorInfo.chipDetails.map(c => c.text + '=' + c.boxW + 'px,溢出' + c.overflow)))
report('编辑器打开后页面仍无横向滚动', editorInfo.docScrollW <= editorInfo.docClientW,
`scrollWidth=${editorInfo.docScrollW} clientWidth=${editorInfo.docClientW}`)
console.log(` 编辑器面板高 ${editorInfo.sheetH}px(视口 ${editorInfo.viewportH}px),内容可滚动 ${editorInfo.bodyScrollH} > ${editorInfo.bodyClientH}`)
await page.evaluate(() => { document.getElementById('schedEditorModal').style.display = 'none' })
// ---------- 6. 清理(只删本脚本新建的,绝不碰已有数据) ----------
const list = await (await fetch(`${BASE}/api/app_order_box/kits/schedule_reminder/schedules`)).json()
let removed = 0
for (const s of (list.data || [])) {
if (PRE_EXISTING_IDS.has(s.id)) continue
await fetch(`${BASE}/api/app_order_box/kits/schedule_reminder/schedules?id=${encodeURIComponent(s.id)}`, { method: 'DELETE' })
removed += 1
}
console.log(`\n[6] 清理:删除本次新建的 ${removed} 套日程,保留原有 ${PRE_EXISTING_IDS.size} 套`)
await fetch(`${BASE}/api/app_order_box/state/active-kit`, {
method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ kitId: '' }),
})
console.log(`\n=== 体检完成:发现 ${problems} 处问题 ===`)
}
;(async () => {
const server = buildServer()
await sleep(500)
const browser = await puppeteer.launch({
headless: true, executablePath: findChrome(),
args: ['--no-sandbox', '--disable-setuid-sandbox', '--mute-audio'],
})
const page = await browser.newPage()
await page.setViewport(VIEWPORT)
const pageErrors = []
page.on('pageerror', e => pageErrors.push(String(e && e.message || e)))
await page.evaluateOnNewDocument(() => {
try { localStorage.setItem('order_box_ui_mode', 'normal') } catch (e) { /* 静默 */ }
})
try {
await run(page)
} catch (e) {
console.log('\n✘ 体检异常:' + String((e && e.stack) || e))
problems += 1
}
if (pageErrors.length) console.log('\n页面报错:\n ' + pageErrors.join('\n '))
await browser.close()
server.close(() => { cleanupDb(); process.exit(0) })
})()