Files

170 lines
10 KiB
Python
Raw Permalink 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.
#!/usr/bin/env python3
"""前端「契约」自测(离线,纯标准库,不起服务)。
锁三件容易在后续改动里被悄悄改回去的事:
1. **首屏不能预置某一屏的文案**。以前 index.html 里直接写着「设置后台账号」,
于是已经登录的人打开面板会先看到一屏登录/注册页,等 /api/auth/status 回来
才跳走(用户报的「先闪一秒登录页」)。现在 index.html 里只允许有中性的
「正在连接面板…」,具体那一屏必须由 app.js 判定之后再画。
2. **默认排序是「已配置优先」**,卡片一行 4 个、列表一行 2 个。
3. localStorage 一律走 lsGet/lsSet —— 隐私模式下它会抛异常,
不能因为读不到一个本地偏好就把整个面板搞崩。
4. **同排卡片必须等高**。meta 那行文案长短差很多(B 站会多出「普通会员 |
硬币 1223 | 等级 5」这种尾巴),一旦允许它换行,那一张就会比同排高出一截
(用户报的「第一个站点卡片比其他的高」)。所以 meta 固定单行 + 写死高度,
完整文案挂到 title 上。
5. **站点配置是弹窗,不是在卡片里就地展开**。就地展开那张卡会横跨两列,把一排
四个的节奏打乱、右边空出一大片(用户就是这么反馈的)。点卡片 = 打开
`#siteDlg`,卡片本身只有「logo + 站名 + 分类 + 状态 + 一行概况」。
跑法:python3 tools/test_panel_static.py
"""
import os
import re
import sys
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
STATIC = os.path.join(ROOT, "panel", "static")
passed, failed = [], []
def check(name, cond, detail=""):
(passed if cond else failed).append(name)
print(f" [{'OK' if cond else 'FAIL'}] {name}" + (f" :: {detail}" if not cond else ""))
def read(name):
with open(os.path.join(STATIC, name), encoding="utf-8") as f:
return f.read()
html = read("index.html")
js = read("app.js")
css = read("style.css")
print("=" * 66)
print("1) 首屏:index.html 不许预置登录/设置屏")
gate = re.search(r'<div class="[^"]*"\s+id="gate"[^>]*>(.*?)<!-- =+ 主界面', html, re.S)
check("index.html 里有 gate 区块", bool(gate))
if gate:
body = gate.group(1)
check("gate 里没有「设置后台账号」", "设置后台账号" not in body)
check("gate 里没有「登录后台」", "登录后台" not in body)
check("gate 里没有「登录」按钮文案", ">登录<" not in body)
check("gate 默认是中性文案「正在连接面板…」", "正在连接面板" in body)
check("gate 的表单默认带 hidden", re.search(r'id="gateForm"[^>]*class="hidden"', body) is not None)
check("gate 自己不默认隐藏(否则慢网络下是一片空白)",
re.search(r'class="gate"\s+id="gate"', body) is None and 'class="gate"' in gate.group(0),
gate.group(0)[:80])
check("app.js 的 showGate 会先把表单放出来", 'gateForm").classList.remove("hidden")' in js)
check("app.js 里有上一次登录状态的本地提示", "dc_auth_hint" in js and "readHint" in js and "writeHint" in js)
check("启动时按提示先画主界面(不等接口)", "readHint().l === true" in js and "hideGate()" in js)
check("gateCheck 会把结论写回本地", js.count("writeHint(") >= 4, js.count("writeHint("))
print("2) localStorage 走安全封装(隐私模式会抛异常)")
bare = len(re.findall(r"\blocalStorage\.(get|set|remove)Item", js))
check("除封装本身外没有裸调用", bare == 2, f"发现 {bare} 处")
check("有 lsGet / lsSet 封装", "const lsGet" in js and "const lsSet" in js)
check("读历史视图偏好走封装", 'lsGet("dc_view")' in js)
print("3) 站点排序:默认已配置优先")
check("app.js 定义了排序取值", "SORT_MODES" in js)
check("默认取值是 configured", re.search(r'SORT_MODES\.includes\(lsGet\("dc_sort"\)\)\s*\?\s*lsGet\("dc_sort"\)\s*:\s*"configured"', js) is not None)
check("过滤之后统一过一遍排序", "return sortSites(currentSites().filter(" in js)
check("「已配置优先」用的是 configured 标志", "Number(!!b.configured) - Number(!!a.configured)" in js)
check("index.html 有排序下拉且默认项是「已配置优先」",
re.search(r'<select id="sortSelect"[\s\S]*?<option value="configured">已配置优先</option>', html) is not None)
check("排序选择会被记下来", 'lsSet("dc_sort", S.sort)' in js)
print("4) 密度:卡片一行 4 个、列表一行 2 个")
def rule(selector):
m = re.search(re.escape(selector) + r"\s*\{([^}]*)\}", css)
return m.group(1) if m else ""
def media_block(width):
"""取某个断点的整段 @media 内容(到下一个 @media 为止)。"""
i = css.find(f"@media (max-width: {width}px)")
if i < 0:
return ""
j = css.find("@media ", i + 1)
return css[i: j if j > 0 else len(css)]
grid = rule(".grid")
lst = rule(".list")
check("卡片一行 4 列", "repeat(4, minmax(0, 1fr))" in grid, grid.strip()[:90])
check("网格顶部对齐(否则展开一张会把同排其它卡片拉高)", "align-items: start" in grid, grid.strip()[:90])
check("列表一行 2 列", "repeat(2, minmax(0, 1fr))" in lst, lst.strip()[:90])
check("卡片有 3 / 2 / 1 列的降级",
"repeat(3, minmax(0, 1fr))" in media_block(1560)
and "repeat(2, minmax(0, 1fr))" in media_block(1180)
and ".grid { grid-template-columns: 1fr; }" in media_block(820))
check("列表窄屏回单列", ".list { grid-template-columns: 1fr; }" in media_block(1180))
check("卡片不再就地展开(没有 .card.open 跨列)", ".card.open" not in css)
print("5) 同排卡片等高:meta 固定单行")
meta_css = rule(".card-meta")
check("meta 不换行", "white-space: nowrap" in meta_css, meta_css.strip()[:110])
check("meta 溢出用省略号", "overflow: hidden" in meta_css and "text-overflow: ellipsis" in meta_css, meta_css.strip()[:110])
mh = re.search(r"height:\s*(\d+(?:\.\d+)?)px", meta_css)
mlh = re.search(r"line-height:\s*(\d+(?:\.\d+)?)px", meta_css)
mpb = re.search(r"padding:\s*0\s+\d+px\s+(\d+(?:\.\d+)?)px", meta_css)
check("meta 高度写死了(不是靠内容撑)", bool(mh), meta_css.strip()[:110])
check("height 正好 = 行高 + 下间距(改 padding 时这里会红,提醒一起改)",
bool(mh and mlh and mpb) and float(mh.group(1)) == float(mlh.group(1)) + float(mpb.group(1)),
f"height={mh and mh.group(1)} line-height={mlh and mlh.group(1)} padding-bottom={mpb and mpb.group(1)}")
check("完整 meta 挂到 title 上(截断的部分还能看到)", 'class="card-meta" title=' in js)
check("展开/收起后 meta 的 title 也跟着更新", "el.title = el.textContent" in js)
print("6) 视觉基建:统计条 / 图标 / 状态点")
check("顶栏统计收成一条 .statbar,不再是堆 .pill", 'class="statbar"' in html and 'class="pill' not in html)
check("统计项之间有分隔线", ".stat + .stat { border-left" in css)
check("状态徽章带前置圆点", ".badge::before" in css)
check("展开箭头是自绘 SVG 而不是字符 ▶", "▶" not in js and "const CHEVRON" in js)
check("顶栏按钮带内联图标", html.count("<button") >= 4 and 'id="btnRunAll"' in html and html.count("<svg") >= 6)
check("下拉框去掉了原生箭头(自绘 chevron)", "appearance: none" in css and "background-position: right 9px center" in css)
check("开关是自绘滑块", ".switch input::after" in css)
check("统计数字用等宽数字", "font-variant-numeric: tabular-nums" in css)
print("7) 站点配置走弹窗,不再就地展开卡片")
check("index.html 里有站点配置弹窗", re.search(r'<dialog id="siteDlg"', html) is not None)
check("弹窗内容区带 data-key(表单事件靠它认站点)", re.search(r'id="siteDlgBody"[^>]*data-key', html) is not None)
check("弹窗底栏有 添加 / 立即签到 / 保存 三个动作",
all(f'id="{i}"' in html for i in ("siteDlgAdd", "siteDlgRun", "siteDlgSave")))
check("卡片整块可点(data-action=open + role=button,键盘也能进)",
'data-action="open" role="button" tabindex="0"' in js)
check("列表行的「配置」按钮还在", 'data-action="open" data-key="${site.key}"' in js)
check("就地展开的痕迹清干净了(.card.open / .card-body / S.expanded / toggle)",
".card-body" not in css and "S.expanded" not in js and 'data-action="toggle"' not in js)
check("工具条去掉了「展开全部 / 收起全部」", "btnExpandAll" not in html and "btnCollapseAll" not in html)
check("用 showModal 打开、close 事件统一收尾",
"dlg.showModal()" in js and '.addEventListener("close", onSiteDialogClosed)' in js)
check("弹窗的三段式布局必须限定 [open](无条件 display:flex 会盖掉浏览器默认的 dialog:not([open]){display:none},弹窗关不掉)",
re.search(r"#siteDlg\[open\]\s*\{[^}]*display:\s*flex", css) is not None)
check("打开弹窗不再强制切回卡片视图(列表视图里也能直接配)", 'S.view = "card";' not in js)
check("操作按钮在弹窗底栏而不是滚动的编辑区里(改 cookie 时「保存」始终看得见)",
re.search(r'class="dlg-foot">[\s\S]{0,600}?data-action="save"', html) is not None)
check("提示会搬进打开的弹窗里(原生 dialog 在顶层,页面上的提示会被盖住)",
'document.querySelector("dialog[open]")' in js)
check("「立即签到」先关弹窗再开日志抽屉(抽屉排在弹窗下面,不关就看不见)",
re.search(r'action === "run"\)\s*\{\s*closeSite\(\)', js) is not None)
check("卡片右侧箭头表示「进入」而不是「展开」(没有旋转态)", ".chev.open" not in css)
check("表单类站点的字段直接摊开(不再藏进「其它参数(可选)」折叠条里,"
"否则新建账号后连账号密码框都找不到)",
re.search(r'site\.credential === "cookie"\s*\?\s*`<details class="extras"', js) is not None)
check("cookie 类站点的可选参数仍然收在折叠条里",
"其它参数(${(site.fields || []).length} 项,可选)" in js)
print("=" * 66)
print(f"通过 {len(passed)} 失败 {len(failed)}")
for one in failed:
print(" ✗", one)
sys.exit(1 if failed else 0)