# HTML Deliverable QA

> 用无头 Chrome 对交付级 HTML（单文件工具/看板/追踪表/报告页）做自动化 QA 与视觉自检——断言渲染结果、截图查版式、注入 UI 状态查隐藏面板。适用于「做完了 HTML 但不确定有没有 bug」「截图看排版对不对」「展开态/弹窗态看不到怎么验」等场景。

- Skill: `paloma333/html-deliverable-qa` (Agent Skill)
- Install (CLI): `npx skillmds@latest add paloma333/html-deliverable-qa`
- Raw SKILL.md: https://api.skillmd.com/api/skills/paloma333/html-deliverable-qa/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: Paloma333 (https://skillmd.com/u/paloma333)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/paloma333/html-deliverable-qa

---


# 单文件 HTML 交付物的无头自检

做完 HTML 交付物（尤其是带交互逻辑的单文件工具）后，**不要直接交**。
本环境有 Chrome，可以在一秒内完成「逻辑断言 + 视觉检查」，比让用户截图反馈快得多。

## 0. 环境要点

- Chrome 路径：`/Applications/Google Chrome.app/Contents/MacOS/Google Chrome`
- **必须加 `--no-sandbox`**，否则在本沙箱里起不来
- 加 `--virtual-time-budget=4000` 让内联脚本跑完再输出
- 用 `file://` 绝对路径；若页面靠 `<script src="...">` 加载同目录文件，
  **临时副本必须放在原目录内**，否则相对路径 404（放 `/tmp` 会静默变成空数据）

## 1. 逻辑断言：`--dump-dom` + 正则

```bash
cd <项目目录> && "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
  --headless=new --no-sandbox --disable-gpu --virtual-time-budget=5000 \
  --dump-dom "file://$PWD/page.html" 2>/dev/null > /tmp/dom.html
```

然后用 Python 逐项断言关键标记（**用标记判断，不要靠肉眼读整份 DOM**）：

```python
import re
h = open('/tmp/dom.html', encoding='utf-8').read()
for pat, label in [(r'tr class="row', '表格行'),
                   (r'class="board"', '看板'),
                   (r'已载入 44 条', '数据载入')]:
    print(('✅' if re.search(pat, h) else '❌'), label)
# 统计数字、空状态、错误关键字
print('NaN:', h.count('NaN'), '| undefined:', h.count('undefined'))
```

> ⚠️ **坑**：`--dump-dom` 会把 `<script>` 源码也一起输出。
> 所以脚本里出现的字符串（模板片段、错误文案、`typeof x !== 'undefined'`）
> 都会被统计到 —— 计数 ≥1 不代表渲染出错，要**结合上下文或改用 DOM 结构标记**判断。

## 2. 视觉检查：`--screenshot` + PIL 局部放大

```bash
"$CH" --headless=new --no-sandbox --disable-gpu --hide-scrollbars \
  --window-size=1440,1150 --virtual-time-budget=4000 \
  --screenshot=/tmp/shot.png "file://$PWD/page.html"
```

整页截图看宏观版式；**具体某个组件要裁切放大**（不然细节看不清、会误判为 bug）：

```python
from PIL import Image
im = Image.open('/tmp/shot.png')
im.crop((20, 295, 1480, 355)).resize((1460, 180), Image.LANCZOS).save('/tmp/crop.png')
```

**裁切坐标怎么定位**：先看整图，量出组件在图中的大致 y 区间；
注意 Read 工具展示时可能被缩放（图宽 ≠ `--window-size` 宽度），
用「展示坐标 ÷ 展示宽 × 实际宽」换算回真实像素再裁。

## 3. 验隐藏状态：临时副本注入 UI 状态

展开面板、弹窗、看板、错误态 —— 默认截图都看不到。
复制一份到**同目录**、注入一行强制状态、截图、删掉：

```bash
python3 - <<'PY'
s = open('page.html', encoding='utf-8').read()
anchor = "var dirty = false;"          # 找一个注入锚点
s = s.replace(anchor, anchor + "\nsetTimeout(function(){ ui.open['app001']=true; renderBody(); }, 50);")
open('_tmp_probe.html','w',encoding='utf-8').write(s)
PY
"$CH" --headless=new --no-sandbox --disable-gpu --hide-scrollbars \
  --window-size=1440,1150 --virtual-time-budget=4000 \
  --screenshot=/tmp/shot_state.png "file://$PWD/_tmp_probe.html"
rm -f _tmp_probe.html
```

> 也可以给页面内置 `#hash` 视图路由（如 `#table` / `#board`），
> 这样直接 `file://.../page.html#board` 就能截图，且链接可分享 —— 比每次注入干净。

## 4. 静态部署后的联通性验证

```bash
for p in "/" "/data.js" "/download.xlsx"; do
  echo "$p -> $(curl -s -o /dev/null -w '%{http_code} %{size_download}' --max-time 25 "$URL$p")"
done
```
三个都要 200 且 size 非 0。**部署目录里避免中文文件名**（URL 编码容易出问题），
需要中文名就本地保留、部署副本改 ASCII。

## 5. 交付前 checklist

- [ ] `--dump-dom` 关键标记全过，无 `NaN` / 无空数据静默降级
- [ ] 数据加载失败时有**显式提示**，不是白屏或空表
- [ ] 截图确认：无横向溢出、无被裁切的列、sticky 表头生效
- [ ] 每个隐藏态（展开/看板/空状态）都截过图
- [ ] 手机宽度过一遍（**注意下面的 500px 限制**）
- [ ] 在线链接的每个静态资源都 200

### ⚠️ 两个会骗到你的坑

**（1）无头 Chrome 的视口宽度最小约 500px。**
`--window-size=390,844` 会被钳到 `innerWidth=500`，`--force-device-scale-factor=2`
也**不会**改变 CSS 视口。所以 390px 真机宽度**测不了** —— 别拿 414 的截图当 390 的结论
（看起来「被裁切」的区域，往往只是截图比实际视口窄）。
窄屏只能靠：① 加防御性 media query；② 用下面这段**量溢出**代替肉眼判断：

```js
var vw = innerWidth, bad = [];
document.querySelectorAll('body *').forEach(function(el){
  var r = el.getBoundingClientRect();
  if(r.right > vw + 1 && r.width > 0 && !el.closest('.tblwrap'))   // 排除有意内部滚动的容器
    bad.push(el.tagName + '.' + (el.className||'-') + '@' + Math.round(r.right));
});
document.title = 'VW=' + vw + ' SW=' + document.documentElement.scrollWidth + ' || ' + (bad.slice(0,10).join(' | ')||'clean');
```
`SW === VW` 且 `bad` 为空（或只剩预期滚动容器）= 没有真实溢出。

**（2）动态插入 DOM 后，`closest()` 的选择器可能匹配不到。**
「展开详情」这类额外插进去的行，若只给主行加了 `data-id`，
`el.closest('tr.row')` 在详情行里会返回 `null` —— 表现是**该区域所有编辑静默失效**，
不报错、不提示。稳妥做法：**所有可编辑容器统一带 `data-id`，统一用 `closest('[data-id]')`**。

## 6. 配套：单文件工具 + Excel 双形态

单文件 HTML 工具的 localStorage **是按浏览器隔离的，不是云同步**。
交付时务必同时给一份 xlsx（`openpyxl`）：
`DataValidation` 做下拉、`FormulaRule` 做条件格式（逾期红/临近黄/Offer 绿）、
`freeze_panes` + `auto_filter`、第二个 sheet 用 `COUNTIF/COUNTIFS/SUMPRODUCT` 做统计。
生成器脚本要写成**幂等可重跑**的，并把「本地中文名 + 部署用 ASCII 名」两份都输出，
这样页面里的下载链接本地也能点开。

## 7. 换设备/改数据后的同步纪律

`deploy-xxx/` 目录**永远是快照**。改完源文件必须：
`cp 源文件 → deploy-xxx/` → 重新部署 → `curl` 复验。
忘了同步 = 线上还是旧版，而且不会有任何报错。

