playwright-verify

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Skill: playwright-verify

Skill: playwright-verify

Đúc kết từ GH#96 — công cụ verify RẺ (< 5s/lần, không LLM) và là cách DUY NHẤT tương tác được với
localhost
/
file://
khi
claude-in-chrome
bị chặn 2 scheme này trên máy.
提炼自 GH#96——低成本验证工具(每次 < 5 秒,不使用 LLM),并且是在本机
claude-in-chrome
阻止访问这两个 scheme 时,唯一能够与
localhost
/
file://
交互的方式。

WHAT

WHAT

Purpose và context

Purpose 与 context

  • Purpose: verify code nhanh, một lần, bằng Playwright chạy qua standalone
    .mjs
    +
    node
    (không test runner): chụp ảnh
    localhost
    /
    file://
    , moi lỗi console/pageerror/http/requestfailed, đo phần tử thật, auth bypass; việc nhiều bước thì phiên bền
    connectOverCDP
    .
  • Trigger (when to use):
  • claude-in-chrome
    báo
    Frame with ID 0 is showing error page
    hoặc
    Can't interact with browser-internal or unparseable URLs
    trên
    localhost:*
    /
    file://
    .
  • Cần chẩn đoán phần tử UI "không thấy" / "không bấm được" — ảnh chụp không đủ, cần số đo thật (
    getBoundingClientRect
    ,
    getComputedStyle
    ).
  • Cần verify 1 giả thuyết code rồi vứt (không xứng tạo
    *.spec.ts
    + test runner).
  • Cần test qua dev-login/session giả thay vì đăng nhập SSO thật mỗi lần.
    • User nói 'verify bằng playwright', 'chụp ảnh trang', 'test nhanh UI bằng script', 'verify nhiều bước', 'giữ session giữa các lần chạy', 'không click được / không thấy phần tử', hoặc
      /playwright-verify
      .
  • Non-goals: không tạo
    *.spec.ts
    + test runner (
    npx playwright test
    ), không phải bộ test case UAT (→ uat-nonit-testcase), không thay claude-in-chrome cho trang web ngoài bình thường, không commit script vào repo đích.
  • Purpose: 快速、一次性验证代码,使用通过 standalone
    .mjs
    +
    node
    运行的 Playwright(不使用 test runner):截取
    localhost
    /
    file://
    截图,收集 console/pageerror/http/requestfailed 错误,测量真实元素,绕过 auth;多步骤任务则使用持久
    connectOverCDP
    session。
  • Trigger(何时使用):
  • claude-in-chrome
    在
    localhost:*
    /
    file://
    上报告
    Frame with ID 0 is showing error page
    或
    Can't interact with browser-internal or unparseable URLs
    。
  • 需要诊断 UI 元素“看不到”或“无法点击”的问题——仅凭截图不够,需要真实测量值(
    getBoundingClientRect
    、
    getComputedStyle
    )。
  • 需要验证一个代码假设,然后将其丢弃(无需创建
    *.spec.ts
    + test runner)。
  • 需要通过模拟 dev-login/session 进行测试,而不是每次都登录真实 SSO。
  • 用户说“用 Playwright 验证”“截取页面截图”“用脚本快速测试 UI”“进行多步骤验证”“在多次运行之间保留 session”“无法点击或看不到元素”,或使用
    /playwright-verify
    。
  • Non-goals: 不创建
    *.spec.ts
    + test runner(
    npx playwright test
    ),不用于 UAT 测试用例集(→
    uat-nonit-testcase
    ),不用于替代普通外部网站上的 claude-in-chrome,也不会将脚本提交到目标 repo。

Mental model

Mental model

giả thuyết cần verify → (đã cài?) → script .mjs một-phát: launch → gắn 4 kênh lỗi → goto (load + wait) → screenshot/đo → close → đọc log + ảnh → vứt
. Nhánh nhiều bước:
một Chrome CDP riêng (profile tạm) ← mỗi lượt step-N.mjs bám vào, state sống giữa các lượt → pkill khi xong
.
需要验证的假设 →(已安装?)→ 一次性 .mjs 脚本:launch → 挂接 4 个错误通道 → goto(load + wait)→ screenshot/测量 → close → 读取 log + 图像 → 丢弃
。多步骤分支:
一个独立的 Chrome CDP(临时 profile)← 每轮 step-N.mjs 接入,state 在各轮之间保持存活 → 完成后 pkill
。

Input và output contract

Input 与 output contract

FieldRequired?Ý nghĩa
InURL (
localhost:*
/
file://
/ khác)
cótrang cần verify
Inproject có
@playwright/test
trong
node_modules
cóscript phải chạy TỪ thư mục đó (hoặc
NODE_PATH
)
Incookie/localStorage dev-loginkhôngcho auth bypass
Outảnh chụp (vd
/tmp/check.png
) + log
[console]
/
[pageerror]
/
[http]
/
[reqfail]
cóbằng chứng; ảnh sạch một mình KHÔNG đủ
Outsố đo
getBoundingClientRect
/
getComputedStyle
khi chẩn đoán phần tửsố thật thay vì đoán từ ảnh
Out"xong"—giả thuyết xác nhận/bác bỏ bằng log + ảnh; script đã dọn; Chrome CDP (nếu có) đã
pkill
字段必填?含义
InURL(
localhost:*
/
file://
/ 其他)
是需要验证的页面
Inproject 的
node_modules
中包含
@playwright/test
是脚本必须从该目录运行(或设置
NODE_PATH
)
Incookie/localStorage dev-login否用于绕过 auth
Out截图(例如
/tmp/check.png
)+
[console]
/
[pageerror]
/
[http]
/
[reqfail]
日志
是证据;仅有干净截图不足以证明结果
Out
getBoundingClientRect
/
getComputedStyle
测量值
诊断元素时用真实数值代替根据截图猜测
Out“完成”—通过 log + 图像确认或否定假设;脚本已清理;Chrome CDP(如有)已执行
pkill

Rules và capabilities

Rules 与 capabilities

  • RULE-01 (MUST):
    import { chromium } from "@playwright/test"
    phải chạy TỪ TRONG thư mục project
    có
    @playwright/test
    trong
    node_modules
    — ESM resolve theo
    cwd
    , không theo vị trí file script. Script ở thư mục khác (vd
    /tmp
    ) →
    cp
    vào project trước khi chạy, hoặc trỏ
    NODE_PATH
    .
  • RULE-02 (MUST):
    waitUntil: "networkidle"
    TREO VÔ THỜI HẠN trên Next.js dev server
    — HMR/webpack dev giữ 1 kết nối long-poll/websocket sống liên tục nên network không bao giờ "idle". Dùng
    waitUntil: "load"
    +
    page.waitForTimeout(N)
    cố định.
  • RULE-03 (MUST): Auth bypass cho dev-login:
    context.addCookies([{url, name, value}])
    (ưu tiên
    url
    thay vì
    domain
    +
    path
    riêng lẻ — tránh cookie không match) +
    context.addInitScript(...)
    set
    localStorage
    — set state TRƯỚC khi gọi
    newPage()
    , không phải sau.
  • RULE-04 (MUST): 1
    context
    cho mỗi route độc lập
    nếu app có logic "gặp 401 ở API phụ → tự xoá session toàn cục" — dùng lại 1 context cho nhiều navigation khiến lỗi route N hỏng lây route N+1.
  • RULE-05 (MUST): Chẩn đoán "không thấy/không bấm được": đừng chỉ nhìn ảnh chụp — gọi
    page.evaluate(() => el.getBoundingClientRect())
    +
    getComputedStyle(el)
    lấy số đo thật;
    locator.click()
    tự báo lỗi rõ ("element is not visible") kèm log các bước thử lại, đọc log đó trước khi đoán nguyên nhân.
  • RULE-06 (MUST): Ảnh chụp sạch không chứng minh gì — luôn moi 4 kênh lỗi, không chỉ 2:
    console
    (type
    error
    ) ·
    pageerror
    ·
    response
    có
    status() >= 400
    ·
    requestfailed
    . Analytics crash, API 5xx, promise rejection không hiện lên pixel nào. Gắn vào
    page
    TRƯỚC
    goto
    :
    js
    page.on("response", r => { if (r.status() >= 400) console.log("[http]", r.status(), r.url()); });
    page.on("requestfailed", r => console.log("[reqfail]", r.failure()?.errorText, r.url()));
  • RULE-07 (MUST): Mode phiên bền: một Chrome riêng, một profile riêng (
    --user-data-dir
    tạm). Không bao giờ
    --remote-debugging-port
    lên Chrome cá nhân của user — macOS bật hộp thoại xin phép và script sẽ giẫm lên tab user đang làm.
  • RULE-08 (MUST): File script standalone không vào git — chạy từ scratchpad, không commit vào repo đích.
  • RULE-09 (MUST): Touch only what the task requires — no opportunistic changes.
  • Capabilities: chạy trình duyệt headless cục bộ + đọc/ghi file tạm; mạng ngoài chỉ khi cài binary (CDN), có timeout.
  • RULE-01 (MUST):
    import { chromium } from "@playwright/test"
    必须从包含
    @playwright/test
    且已安装于
    node_modules
    的 project 目录运行
    ——ESM 根据
    cwd
    解析,而不是根据脚本文件所在位置解析。如果脚本位于其他目录(例如
    /tmp
    ),应先将其
    cp
    到 project 后再运行,或者设置
    NODE_PATH
    。
  • RULE-02 (MUST):在 Next.js dev server 上使用
    waitUntil: "networkidle"
    会无限挂起
    ——HMR/webpack dev 会持续保持一个 long-poll/websocket 连接,因此网络永远不会进入“idle”状态。应改用
    waitUntil: "load"
    + 固定时长的
    page.waitForTimeout(N)
    。
  • RULE-03 (MUST):dev-login 的 auth bypass:使用
    context.addCookies([{url, name, value}])
    (优先使用
    url
    ,而不是分别设置
    domain
    +
    path
    ,以避免 cookie 无法匹配)+
    context.addInitScript(...)
    设置
    localStorage
    ——必须在调用
    newPage()
    之前设置 state,而不是之后。
  • RULE-04 (MUST):如果 app 存在“辅助 API 遇到 401 → 自动清除全局 session”的逻辑,则每个独立 route 使用 1 个
    context
    ——对多个 navigation 复用同一个 context,会导致 route N 的错误破坏 route N+1。
  • RULE-05 (MUST):诊断“看不到或无法点击”时:不要只查看截图——调用
    page.evaluate(() => el.getBoundingClientRect())
    +
    getComputedStyle(el)
    获取真实测量值;
    locator.click()
    会给出明确的错误信息(“element is not visible”)以及重试步骤日志,应先读取这些日志,再判断原因。
  • RULE-06 (MUST):干净截图无法证明任何事情——始终收集 4 个错误通道,而不只是 2 个:
    console
    (type 为
    error
    )·
    pageerror
    ·
    response
    的
    status() >= 400
    ·
    requestfailed
    。Analytics crash、API 5xx、promise rejection 都不会显示在任何像素上。必须在
    goto
    之前挂接到
    page
    :
    js
    page.on("response", r => { if (r.status() >= 400) console.log("[http]", r.status(), r.url()); });
    page.on("requestfailed", r => console.log("[reqfail]", r.failure()?.errorText, r.url()));
  • RULE-07 (MUST):持久 session 模式:使用一个独立 Chrome 和一个独立 profile(临时
    --user-data-dir
    )。绝不能对用户个人 Chrome 使用
    --remote-debugging-port
    ——macOS 会弹出权限请求对话框,而且脚本会干扰用户正在使用的 tab。
  • RULE-08 (MUST):standalone 脚本不得进入 git——从 scratchpad 运行,不要提交到目标 repo。
  • RULE-09 (MUST):仅修改任务要求的范围——不做顺手修改。
  • Capabilities:可运行本地 headless 浏览器并读取/写入临时文件;仅在安装 binary(CDN)时访问外部网络,并设置 timeout。

Failure boundaries

Failure boundaries

  • npx playwright install
    bị chặn mạng/treo → blocked, báo lỗi rõ (có timeout), không để lệnh treo vô hạn.
  • ESM không resolve
    @playwright/test
    (chạy sai cwd) → cp script vào project hoặc
    NODE_PATH
    , rồi chạy lại.
  • networkidle
    treo trên Next.js dev → đổi sang
    load
    +
    waitForTimeout
    (không phải lỗi app).
  • Phần tử không thấy/không bấm → đo số thật + đọc log
    locator.click()
    trước khi kết luận; không đoán từ ảnh.
  • npx playwright install
    被网络阻断或挂起 → blocked,明确报告错误并设置 timeout,不能让命令无限挂起。
  • ESM 无法解析
    @playwright/test
    (
    cwd
    错误)→ 将脚本
    cp
    到 project 或设置
    NODE_PATH
    ,然后重新运行。
  • networkidle
    在 Next.js dev 上挂起 → 改为
    load
    +
    waitForTimeout
    (这不是 app 错误)。
  • 元素看不到或无法点击 → 在下结论前测量真实数值并读取
    locator.click()
    日志;不要根据截图猜测。

HOW

HOW

Main workflow

Main workflow

StepTypeInputsActionOutputs/exitFailure/next
W01deterministicmáyKiểm đã cài (
ls ~/Library/Caches/ms-playwright
); chưa có thì cài
binary sẵnCDN bị chặn → blocked
W02judgmentgiả thuyết + URLViết 1 file
.mjs
chạy thẳng bằng
node
, gắn 4 kênh lỗi trước
goto
script—
W03effectscriptChạy từ đúng thư mục (
node check.mjs
), đọc log + ảnh; dọn script
log + ảnhresolve lỗi → cp/NODE_PATH, lặp W03
W04effectviệc nhiều bướcMode phiên bền
connectOverCDP
(B01)
state giữ giữa lượtxong →
pkill
Chi tiết từng bước (nguồn chân lý cho W01–W04):
  1. Kiểm tra đã cài chưa trước khi cài lại:
    ls ~/Library/Caches/ms-playwright
    (macOS) — binary cache dùng chung mọi project, không cần cài lại per-repo.
    • Chưa có:
      pnpm add -D @playwright/test
      (bỏ qua nếu project đã có sẵn trong
      devDependencies
      ) rồi
      npx playwright install chromium
      .
    • npx playwright install
      tải binary từ CDN ngoài (
      cdn.playwright.dev
      ) — sandbox/CI có allowlist mạng sẽ treo hoặc fail. Set timeout khi gọi tự động; nếu bị chặn, báo lỗi rõ thay vì để lệnh treo vô thời hạn.
  2. Viết 1 file
    .mjs
    chạy thẳng bằng
    node
    (không qua
    npx playwright test
    ) — nặng hơn cho việc verify-rồi-vứt:
    js
    import { chromium } from "@playwright/test"; // export chromium dùng được như package "playwright"
    
    const browser = await chromium.launch();
    const page = await browser.newPage({ viewport: { width: 1400, height: 900 } });
    page.on("console", m => console.log("[console]", m.type(), m.text()));
    page.on("pageerror", e => console.log("[pageerror]", e.message));
    
    await page.goto("http://localhost:3000/some-page", { waitUntil: "load" }); // KHÔNG "networkidle" — xem Rules #2
    await page.waitForTimeout(1500);
    await page.screenshot({ path: "/tmp/check.png", fullPage: true });
    
    await browser.close();
  3. Chạy từ đúng thư mục (xem Rules #1),
    node check.mjs
    , đọc console/pageerror log + ảnh chụp. Dọn file
    .mjs
    sau khi xong nếu chạy trong 1 project cụ thể (không cần xoá gì khỏi git — script không nằm trong repo nếu chạy từ scratch dir).
  4. Việc NHIỀU BƯỚC có điều kiện → mode phiên bền (học từ browser-use/browser-harness, 09/2026). Script một-phát bắt agent viết mù cả kịch bản; sai một bước là chạy lại từ đăng nhập. Thay vào đó: dựng MỘT Chrome có cổng CDP, mỗi lượt là một script nhỏ bám vào — tab, cookie,
    localStorage
    , scroll giữ nguyên giữa các lượt, agent nhìn rồi mới quyết bước kế.
    bash
    # một lần — Chrome riêng, profile riêng; KHÔNG bám vào Chrome cá nhân của user
    CH="$HOME/Library/Caches/ms-playwright/chromium-1243/chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing"
    "$CH" --remote-debugging-port=9333 --user-data-dir=/tmp/pw-prof --headless=new --no-first-run about:blank &
    js
    // mỗi lượt — step-N.mjs
    import { chromium } from "@playwright/test";
    const browser = await chromium.connectOverCDP("http://localhost:9333");
    const ctx = browser.contexts()[0];
    const page = ctx.pages()[0] ?? await ctx.newPage();   // bám tab cũ, KHÔNG launch mới
    // ... một hành động + quan sát; state sống sang lượt sau ...
    await browser.close();   // chỉ ngắt kết nối, Chrome vẫn chạy
    Xong việc:
    pkill -f "remote-debugging-port=9333"
    . Đo 09/09/2026 (python-playwright, cùng trang
    file://
    ): launch mới 1.1–1.9s/lượt, mất state;
    connectOverCDP
    0.65–0.80s/lượt,
    window.__marker
    sống qua tiến trình mới (3/3). Bản
    .mjs
    trên chưa chạy bằng node trong phiên đo — API đồng nhất với bản python đã chạy.
步骤类型输入操作输出/退出失败/下一步
W01确定性机器检查是否已安装(
ls ~/Library/Caches/ms-playwright
);未安装则进行安装
binary 就绪CDN 被阻断 → blocked
W02判断假设 + URL编写 1 个直接通过
node
运行的
.mjs
文件,在
goto
前挂接 4 个错误通道
脚本—
W03执行脚本从正确目录运行(
node check.mjs
),读取 log + 图像;清理脚本
log + 图像解决错误 → cp/NODE_PATH,重复 W03
W04执行多步骤任务持久 session 模式
connectOverCDP
(B01)
state 在各轮之间保留完成后 → pkill
各步骤详情(W01–W04 的事实来源):
  1. 重新安装前先检查是否已经安装(
    ls ~/Library/Caches/ms-playwright
    ,macOS)——binary cache 为所有 project 共享,不需要针对每个 repo 重复安装。
    • 尚未安装:执行
      pnpm add -D @playwright/test
      (如果 project 的
      devDependencies
      中已经存在则跳过),然后执行
      npx playwright install chromium
      。
    • npx playwright install
      会从外部 CDN(
      cdn.playwright.dev
      )下载 binary——sandbox/CI 的网络 allowlist 可能导致其挂起或失败。自动调用时应设置 timeout;如果被阻断,应明确报告错误,而不是让命令无限挂起。
  2. 编写 1 个直接通过
    node
    运行的
    .mjs
    文件
    (不经过
    npx playwright test
    )——对于“验证后即丢弃”的场景,这种方式更轻量:
    js
    import { chromium } from "@playwright/test"; // export chromium dùng được như package "playwright"
    
    const browser = await chromium.launch();
    const page = await browser.newPage({ viewport: { width: 1400, height: 900 } });
    page.on("console", m => console.log("[console]", m.type(), m.text()));
    page.on("pageerror", e => console.log("[pageerror]", e.message));
    
    await page.goto("http://localhost:3000/some-page", { waitUntil: "load" }); // KHÔNG "networkidle" — xem Rules #2
    await page.waitForTimeout(1500);
    await page.screenshot({ path: "/tmp/check.png", fullPage: true });
    
    await browser.close();
  3. 从正确目录运行(参见 Rules #1),执行
    node check.mjs
    ,读取 console/pageerror log + 截图。如果在特定 project 中运行,完成后清理
    .mjs
    文件(如果从 scratch 目录运行,则无需从 git 中删除任何内容,因为脚本不在 repo 中)。
  4. 有条件的多步骤任务 → 持久 session 模式(借鉴自 browser-use/browser-harness,09/2026)。一次性脚本会迫使 agent 盲目编写整个场景;只要某个步骤出错,就必须从登录步骤重新运行。替代方案是:启动一个带 CDP 端口的 Chrome,每轮运行一个小型脚本进行连接——tab、cookie、
    localStorage
    、scroll 状态会在各轮之间保留,agent 观察当前状态后再决定下一步。
    bash
    # một lần — Chrome riêng, profile riêng; KHÔNG bám vào Chrome cá nhân của user
    CH="$HOME/Library/Caches/ms-playwright/chromium-1243/chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing"
    "$CH" --remote-debugging-port=9333 --user-data-dir=/tmp/pw-prof --headless=new --no-first-run about:blank &
    js
    // mỗi lượt — step-N.mjs
    import { chromium } from "@playwright/test";
    const browser = await chromium.connectOverCDP("http://localhost:9333");
    const ctx = browser.contexts()[0];
    const page = ctx.pages()[0] ?? await ctx.newPage();   // bám tab cũ, KHÔNG launch mới
    // ... một hành động + quan sát; state sống sang lượt sau ...
    await browser.close();   // chỉ ngắt kết nối, Chrome vẫn chạy
    工作完成后:执行
    pkill -f "remote-debugging-port=9333"
    。 09/09/2026 的测量结果(python-playwright,同一个
    file://
    页面):每次新 launch 需要 1.1–1.9 秒且会丢失 state;
    connectOverCDP
    每次需要 0.65–0.80 秒,
    window.__marker
    可跨新进程存活(3/3)。测量期间,上面的
    .mjs
    版本尚未通过 node 运行——其 API 与已经运行过的 python 版本一致。

Branches

Branches

IDKindGuardHành viSkip / failureRejoin
B01conditional_requiredviệc NHIỀU BƯỚC có điều kiện (cần nhìn rồi mới quyết bước kế)một Chrome riêng
--remote-debugging-port=9333
+ profile tạm; mỗi lượt
step-N.mjs
connectOverCDP
việc một-phát → skip; không bao giờ bám Chrome cá nhânkết thúc bằng
pkill -f "remote-debugging-port=9333"
B02user_optionaltrang cần dev-login/session giả
context.addCookies
+
context.addInitScript
TRƯỚC
newPage()
; 1 context/route nếu app tự xoá session khi 401
—W03
B03recoveryphần tử "không thấy/không bấm được"
page.evaluate(() => el.getBoundingClientRect())
+
getComputedStyle(el)
, đọc log retry của
locator.click()
—W03
ID类型Guard行为跳过/失败汇合
B01conditional_required有条件的多步骤任务(需要先观察,再决定下一步)一个独立 Chrome,
--remote-debugging-port=9333
+ 临时 profile;每轮通过
step-N.mjs
执行
connectOverCDP
一次性任务 → skip;绝不能连接用户的个人 Chrome结束时执行
pkill -f "remote-debugging-port=9333"
B02user_optional页面需要模拟 dev-login/session在
newPage()
之前执行
context.addCookies
+
context.addInitScript
;如果 app 会在 401 时清除 session,则每个 route 使用 1 个 context
—W03
B03recovery元素“看不到或无法点击”
page.evaluate(() => el.getBoundingClientRect())
+
getComputedStyle(el)
,读取
locator.click()
的重试日志
—W03

Validation và stopping

Validation 与 stopping

Kết luận dựa trên 4 kênh lỗi + ảnh + số đo, không chỉ ảnh. Mỗi lượt chạy < 5s, không LLM. Dừng khi giả thuyết đã có bằng chứng; dọn script +
pkill
Chrome CDP.
结论必须基于 4 个错误通道 + 图像 + 测量值,而不能只依赖图像。每次运行 < 5 秒,不使用 LLM。当假设已经有证据时停止;清理脚本并对 Chrome CDP 执行
pkill
。

Examples

Examples

  • Positive: claude-in-chrome báo
    Frame with ID 0 is showing error page
    trên
    http://localhost:3000/some-page
    → cp
    check.mjs
    vào project →
    node check.mjs
    →
    /tmp/check.png
    + log sạch cả
    [console]
    /
    [pageerror]
    /
    [http]
    /
    [reqfail]
    → xác nhận trang render, xoá script.
  • Boundary/failure: script dùng
    waitUntil: "networkidle"
    trên Next.js dev → treo vô hạn; sửa thành
    waitUntil: "load"
    +
    page.waitForTimeout(1500)
    → chạy xong. Ảnh sạch nhưng log có
    [http] 500 /api/x
    → KHÔNG báo pass.
  • Positive: claude-in-chrome 在
    http://localhost:3000/some-page
    上报告
    Frame with ID 0 is showing error page
    → 将
    check.mjs
    cp
    到 project → 执行
    node check.mjs
    → 得到
    /tmp/check.png
    ,且
    [console]
    /
    [pageerror]
    /
    [http]
    /
    [reqfail]
    日志均干净 → 确认页面正常渲染,删除脚本。
  • Boundary/failure: 脚本在 Next.js dev 上使用
    waitUntil: "networkidle"
    → 无限挂起;改为
    waitUntil: "load"
    +
    page.waitForTimeout(1500)
    → 顺利运行。图像干净但日志包含
    [http] 500 /api/x
    → 不得报告为 pass。