557dab3caf
容器挂pos-<position>类+CSS联动规则,无效position归一化;静态测试锁定。 Co-Authored-By: Claude Code <noreply@anthropic.com>
102 lines
4.9 KiB
Python
102 lines
4.9 KiB
Python
"""邀请卡静态回归网:不变量"未展示 ⇒ 零占位、零拦截"。
|
|
|
|
背景(2026-09-01 bug):点 X 写入 permanent 后刷新,startInviteTimer 在
|
|
storage 守卫早退 ⇒ showInvite 永不执行 ⇒ invitePanel 引用恒为 null ⇒
|
|
hideInvite 提前返回 ⇒ .invite-panel 停留"display:block + opacity:0"的
|
|
隐形占位态,.invite-container(z-index:99998、无 pointer-events:none)
|
|
拦截页面右下角所有点击。修复 = 出厂即折叠(A)+ 容器点击穿透(C)。
|
|
"""
|
|
import re
|
|
from pathlib import Path
|
|
|
|
STATIC = Path(__file__).resolve().parent.parent / "static"
|
|
|
|
|
|
def _read(rel):
|
|
return (STATIC / rel).read_text(encoding="utf-8")
|
|
|
|
|
|
def _extract_function(src, name):
|
|
"""花括号计数提取函数源码(与 test_theme_static.py 同款)。"""
|
|
start = src.index("function " + name + "(")
|
|
i = src.index("{", start)
|
|
depth = 0
|
|
for j in range(i, len(src)):
|
|
if src[j] == "{":
|
|
depth += 1
|
|
elif src[j] == "}":
|
|
depth -= 1
|
|
if depth == 0:
|
|
return src[start:j + 1]
|
|
raise AssertionError("unbalanced braces in " + name)
|
|
|
|
|
|
def test_invite_panel_ships_hidden():
|
|
"""A:面板出厂即带 hide —— 任何绕过 showInvite 的路径都零占位。"""
|
|
src = _read("widget.js")
|
|
assert '<div class="invite-panel hide">' in src, \
|
|
"invite 模板必须出厂带 hide(未展示 ⇒ 无布局占位)"
|
|
|
|
|
|
def test_invite_container_click_through():
|
|
"""C:容器 pointer-events:none —— 卡片未展示时整棵子树点击穿透。"""
|
|
m = re.search(r"\.invite-container \{[^}]*\}", _read("widget.js"))
|
|
assert m, "widget.js 缺 .invite-container 规则"
|
|
assert "pointer-events:none" in m.group(0), \
|
|
".invite-container 必须 pointer-events:none(防隐形拦截点击)"
|
|
|
|
|
|
def test_show_invite_reflow_between_unhide_and_show():
|
|
"""入场动画:remove(hide) 与 add(show) 之间必须强制回流。
|
|
|
|
出厂带 hide 后,若 remove 与 add 同帧执行,display:none→block 与
|
|
opacity 变化合并进同一次样式重算,0.3s 淡入退化为瞬现。
|
|
"""
|
|
fn = _extract_function(_read("widget.js"), "showInvite")
|
|
for token in ('remove("hide")', "offsetWidth", 'add("show")'):
|
|
assert token in fn, f"showInvite 缺 {token}"
|
|
assert fn.index('remove("hide")') < fn.index("offsetWidth") < fn.index('add("show")'), \
|
|
"showInvite 须 remove(hide) → offsetWidth 回流 → add(show) 顺序执行"
|
|
|
|
|
|
def test_autoinvite_specialcase_removed_and_guard_kept():
|
|
"""A 落地后 !autoInvite 预折叠特判删除;startInviteTimer 的配置守卫保留。"""
|
|
src = _read("widget.js")
|
|
assert "disabledInvitePanel" not in src, \
|
|
"!autoInvite 预折叠特判应删除(出厂 hide 统一覆盖该不变量)"
|
|
fn = _extract_function(src, "startInviteTimer")
|
|
assert "if (!autoInvite) return;" in fn, \
|
|
"startInviteTimer 必须保留 data-auto-invite=false 永不自动弹的守卫"
|
|
|
|
|
|
def test_gdpr_privacy_notice_present():
|
|
"""GDPR 三入口结构锁(Task 9 质量审查): 邀请卡/聊天面板/独立页的
|
|
privacy 提示是纯模板字符串,无此断言则重构时会无声丢失。"""
|
|
wjs = _read("widget.js")
|
|
assert "invite-privacy" in wjs and "chat-footer" in wjs
|
|
assert 'href="https://hesupipe.com/privacy-policy/"' in wjs
|
|
assert 'target="_blank" rel="noopener"' in wjs
|
|
chat = _read("chat.html")
|
|
assert "privacy policy" in chat and "privacy-policy/" in chat
|
|
|
|
|
|
def test_position_class_panel_alignment():
|
|
"""位置联动(2026-09-30 线上bug): 面板对齐必须随 data-position 翻转。
|
|
根因: .chat-panel 硬编码 right:0(为 bottom-right 设计), positionStyles
|
|
只应用到按钮容器——bottom-left 时 400px 面板右缘贴 196px 容器右缘,
|
|
向左溢出屏幕 184px(线上实测 x=-183.78); top-* 模式面板 bottom:80px
|
|
同理向上溢出。修法: 容器挂 pos-<position> class + CSS 联动规则。"""
|
|
src = _read("widget.js")
|
|
# 1) 无效 position 归一化(防 "pos-垃圾" class 与查表错位)
|
|
assert "if (!positionStyles[position])" in src, "position 需归一化为合法值"
|
|
# 2) 容器挂载位置 class
|
|
assert '"widget-container pos-" + position' in src, "container 需挂 pos-<position> class"
|
|
# 3) CSS 联动规则: 左侧模式面板左对齐, 顶部模式面板从顶部向下弹
|
|
assert ".widget-container.pos-bottom-left .chat-panel { left:0; right:auto; }" in src
|
|
assert ".widget-container.pos-top-right .chat-panel { top:0; bottom:auto; }" in src
|
|
assert ".widget-container.pos-top-left .chat-panel { left:0; right:auto; top:0; bottom:auto; }" in src
|
|
# 4) pos 规则须在 mobile-host 规则之后注入(同特异性靠顺序决胜, 不得被 bottom:84px 覆盖打断)
|
|
mobile_rule = ".widget-container.mobile-host .chat-panel { bottom:84px; }"
|
|
pos_rule = ".widget-container.pos-bottom-left .chat-panel"
|
|
assert src.index(mobile_rule) < src.index(pos_rule), "pos 规则须在 mobile 规则之后"
|