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

230 lines
11 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.
/**
* 作息报时器 · 老 Safari CSS 兼容性复现与验证(一次性调试脚本)
*
* 背景:iPhone 6s 上反馈两个现象:
* 1. 待机大屏的时间没有大字体
* 2. 到点循环播报了 5 次,但屏幕上没有「我知道了」按钮可点
*
* 假设:该机 Safari 版本较老,不支持 clamp() / dvh / inset 简写。
* 浏览器遇到「不支持的属性值」时会**只丢弃那一条声明**,同一条规则里的其它声明照常生效。
* → 于是 .standby-clock 的 font-size 被丢弃(字体退回默认 16px,但金色/粗体仍在)
* → .modal-overlay 的 inset:0 被丢弃(遮罩没有定位到四边,跑到文档静态位置,即视口外)
*
* 本脚本用请求拦截把这三处改成「浏览器不认识的值」,等同于老 Safari 的行为,
* 以此复现现象、验证因果;修完后同脚本应转为全部通过。
*
* 运行:node dev_test_scripts/debug/debug_schedule_reminder_legacy_safari.js
*/
const fs = require('fs')
const path = require('path')
const express = require('express')
const puppeteer = require('puppeteer')
const PORT = 8981
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)
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))
// ⚠️ 安全底线:主流程开始前快照已有日程 id,清理时只删"本次新建"的,
// 绝不删除数据库里原本就存在的用户数据(曾因无差别删除误删过用户真实日程)。
let PRE_EXISTING_IDS = new Set()
/**
* 把"老 Safari 不支持的写法"替换成浏览器不认识的值 → 该声明被丢弃。
* 这是对老浏览器行为的高保真模拟:只丢那一条,同规则其它声明不受影响。
*/
const degradeForLegacySafari = css => String(css)
.replace(/\bclamp\(/g, 'clampx(') // 不支持 clamp() → 整条 font-size 被丢弃
.replace(/\b(\d+)dvh\b/g, '$1dvhx') // 不支持 dvh 单位 → 整条 height 被丢弃
.replace(/inset:\s*0;/g, 'inset-legacy: 0;') // 不支持 inset 简写 → 整条被丢弃
const beijingHHmm = () => new Intl.DateTimeFormat('en-GB', {
timeZone: 'Asia/Shanghai', hour12: false, hour: '2-digit', minute: '2-digit',
}).format(new Date())
const buildServer = () => {
const app = express()
app.use(express.json())
app.use(express.static(path.join(process.cwd(), 'public')))
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 probe = async (browser, legacy) => {
const page = await browser.newPage()
await page.setViewport(VIEWPORT)
await page.setCacheEnabled(false)
const pageErrors = []
page.on('pageerror', e => pageErrors.push(String(e && e.message || e)))
await page.setRequestInterception(true)
page.on('request', async req => {
const url = req.url()
if (legacy && url.indexOf('order_box.css') >= 0) {
try {
const css = await (await fetch(url)).text()
return req.respond({ status: 200, contentType: 'text/css; charset=utf-8', body: degradeForLegacySafari(css) })
} catch (e) { return req.continue() }
}
return req.continue()
})
// 准备数据:当前分钟到点的日程
await fetch(`${BASE}/api/app_order_box/state/active-kit`, {
method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ kitId: '' }),
})
const targetTime = beijingHHmm()
await fetch(`${BASE}/api/app_order_box/kits/schedule_reminder/schedules`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: '兼容性验证日程', weekdays: [1, 2, 3, 4, 5, 6, 7],
items: [{ time: targetTime, name: '验证事项', repeatTimes: 5, intervalSec: 30 }],
}),
})
await page.evaluateOnNewDocument(() => {
try { localStorage.setItem('order_box_ui_mode', 'normal') } catch (e) { /* 静默 */ }
})
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(4000)
await page.evaluate(() => {
const modal = document.getElementById('dialogOverlay')
if (modal && window.getComputedStyle(modal).display !== 'none') {
const b = Array.from(modal.querySelectorAll('button')).find(x => /听到了|知道了|确定/.test(x.textContent || ''))
if (b) b.click()
}
})
await sleep(300)
// 进入待机 → 触发到点播报
await page.evaluate(() => { window.scrollTo(0, 0) })
await page.evaluate(() => {
const b = Array.from(document.querySelectorAll('#schedulesList button'))
.find(x => String(x.textContent || '').indexOf('用这套待机') >= 0)
if (b) b.click()
})
// 等播报弹窗出现
let alertAppeared = false
for (let i = 0; i < 24; i += 1) {
alertAppeared = await page.evaluate(() =>
window.getComputedStyle(document.getElementById('schedAlertModal')).display !== 'none')
if (alertAppeared) break
await sleep(500)
}
await sleep(600)
const result = await page.evaluate(() => {
const clock = document.getElementById('schedClock')
const modal = document.getElementById('schedAlertModal')
const ack = document.getElementById('btnSchedAck')
const overlay = window.getComputedStyle(modal)
const modalRect = modal.getBoundingClientRect()
const ackRect = ack.getBoundingClientRect()
const vh = window.innerHeight
const vw = window.innerWidth
// 遮罩是否真的覆盖了视口(老 Safari 丢 inset 后,遮罩会停在文档静态位置、跑出视口)
const modalOverlapsViewport = modalRect.bottom > 0 && modalRect.top < vh &&
modalRect.right > 0 && modalRect.left < vw
const ackVisible = ackRect.top >= 0 && ackRect.bottom <= vh && ackRect.width > 0
return {
clockFontSize: window.getComputedStyle(clock).fontSize,
clockText: clock.textContent,
modalDisplay: overlay.display,
modalRect: { top: Math.round(modalRect.top), bottom: Math.round(modalRect.bottom), h: Math.round(modalRect.height) },
ackRect: { top: Math.round(ackRect.top), bottom: Math.round(ackRect.bottom), w: Math.round(ackRect.width) },
viewport: { vw, vh },
docScrollH: document.documentElement.scrollHeight,
modalOverlapsViewport, ackVisible,
}
})
// 清理
await fetch(`${BASE}/api/app_order_box/kits/schedule_reminder/logs`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ itemId: 'x', itemName: 'x', status: 'fired' }),
}).catch(() => {})
// 清理:只删本次新建的日程,绝不删除数据库里原本就存在的用户数据
const list = await (await fetch(`${BASE}/api/app_order_box/kits/schedule_reminder/schedules`)).json()
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' })
}
await fetch(`${BASE}/api/app_order_box/state/active-kit`, {
method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ kitId: '' }),
})
await page.close()
return { result, pageErrors, targetTime }
}
;(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 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))
console.log('=== 老 Safari CSS 兼容性复现 / 验证 ===')
if (PRE_EXISTING_IDS.size) {
console.log(`(检测到已有 ${PRE_EXISTING_IDS.size} 套日程,本次不会删除它们)`)
}
console.log('')
console.log('[A] 模拟老 Safari(clamp / dvh / inset 均不被支持)')
const legacy = await probe(browser, true)
console.log(` 目标时刻 ${legacy.targetTime}`)
console.log(` 待机时钟字号 = ${legacy.result.clockFontSize}(文字 ${legacy.result.clockText})`)
console.log(` 播报弹窗 display=${legacy.result.modalDisplay},位置 ${JSON.stringify(legacy.result.modalRect)},视口高 ${legacy.result.viewport.vh}`)
console.log(` 「我知道了」按钮 ${JSON.stringify(legacy.result.ackRect)}`)
console.log(` 弹窗是否落在视口内 = ${legacy.result.modalOverlapsViewport},按钮是否可见 = ${legacy.result.ackVisible}`)
const clockSmall = parseFloat(legacy.result.clockFontSize) < 30
console.log(`\n → 现象1(时间没有大字体)${clockSmall ? '已复现 ✔' : '未复现 ✘'}`)
console.log(` → 现象2(没有「我知道了」按钮)${(!legacy.result.ackVisible || !legacy.result.modalOverlapsViewport) ? '已复现 ✔' : '未复现 ✘'}`)
console.log('\n[B] 正常模式(现代浏览器,不降级)')
const modern = await probe(browser, false)
console.log(` 待机时钟字号 = ${modern.result.clockFontSize}`)
console.log(` 弹窗落视口内 = ${modern.result.modalOverlapsViewport},按钮可见 = ${modern.result.ackVisible}`)
console.log(` → 与 iPhone 18 等新机表现一致:${parseFloat(modern.result.clockFontSize) >= 30 && modern.result.ackVisible ? '正常 ✔' : '异常 ✘'}`)
if (legacy.pageErrors.length || modern.pageErrors.length) {
console.log('\n页面报错:')
;[...legacy.pageErrors, ...modern.pageErrors].slice(0, 10).forEach(t => console.log(' ! ' + t))
}
console.log('\n判定标准:修完后,[A] 段的时钟字号应 >= 30px,且「我知道了」按钮应在视口内可见。')
console.log(`当前 [A] 是否已修复:${!clockSmall && legacy.result.ackVisible && legacy.result.modalOverlapsViewport ? '是 ✔' : '否 ✘(仍需修复)'}`)
await browser.close()
server.close(() => { cleanupDb(); process.exit(0) })
})()