---
name: bun-webview-test
description: 使用 Bun 1.4 内置的 Bun.WebView 无头浏览器对前端页面（Vue/Vite 等）做端到端冒烟与交互测试。当需要加载页面、真实点击、读取 DOM/应用状态、做断言、截图归档，或对本地开发服务器/静态站点做零依赖的浏览器级验证、可视化巡检、交互回归测试时，应使用本技能。
---

# Bun.WebView 前端测试

## Overview

本技能封装 **Bun 1.4 新增的 `Bun.WebView`** 无头浏览器能力，提供一套零依赖的前端端到端测试框架。它取代了 Puppeteer / Playwright 的安装开销，用单个 `bun` 命令即可驱动真实浏览器（macOS 用系统 WebKit，Windows/Linux 用已安装的 Chrome/Edge 经 CDP）完成：页面加载、真实用户输入、DOM/状态读取、断言与截图。

完整 API 细节见 `references/webview-api.md`，不要凭记忆猜测方法签名。

## When to use

- 对 `pnpm dev` 起的开发服务器（本项目默认 `http://localhost:1420`）做冒烟测试。
- 验证某个路由/组件是否正确挂载、渲染、无控制台报错。
- 模拟用户点击/输入/滚动，校验交互后的状态或路由变化。
- 需要截图归档供人工查看（可视化巡检、回归基线）。
- 任何"用浏览器跑一下确认没坏"的场景，且不想引入 Playwright 依赖。

## Prerequisites

1. **Bun >= 1.4**：运行 `bun --version` 确认；低于此版本无 `Bun.WebView`。
2. **目标前端可访问**：先用 `pnpm dev` 启动本项目开发服务器（或指向任意可访问 URL）。
3. **Windows / Linux 需安装浏览器**：`Bun.WebView` 在 Windows 默认走 Chrome 后端，需本机已装 Chrome/Chromium/Edge/Brave。找不到时设 `BUN_CHROME_PATH` 指向可执行文件，或装 Playwright 的 `chrome-headless-shell`。macOS 用系统 WebKit，无需安装。

## Workflow

### Quick Start（复用框架跑测试）

1. 确认开发服务器运行中：`pnpm dev` / `pnpm dd2`（保持 http://localhost:1420 可访问）。
2. **务必带 `?hideUpdateInfo=1` 打开页面**（见下方「启动弹窗」），否则会被数据包弹窗挡住。
3. 运行内置示例冒烟测试：
    ```bash
    bun .agents/skills/bun-webview-test/scripts/example.test.ts
    # 或指定地址
    bun .agents/skills/bun-webview-test/scripts/example.test.ts --url http://localhost:1420
    ```
4. 失败用例会自动留图（见 Guidelines 的 `.tmp` 约定），通过 `screenshots/home.png` 查看首屏。

注意点：

- 参数必须在**首次导航**的 URL 上；先打开无参数地址再 `router.push` 加参数不会生效。
- 该参数只关弹窗，**不会安装数据包**：依赖游戏数据的页面（如 `/char/:id` 构筑页）依然会因为
  `charBuild.calculateWeaponAttributes is not a function` 之类报错渲染失败。这类页面要在
  已装数据包的浏览器里人工验证，或给 `WebView` 传 `dataStore: "./profile"` 复用已装数据包的 profile。
- 判断弹窗是否还在：`document.querySelector("dialog.modal.modal-open")` 非空即被挡住。

### 编写新的测试文件

新建一个 `*.test.ts`，从框架导入能力，注册用例，末尾调用 `run()`：

```ts
import { test, expectVisible, expectEval, expectNoConsoleErrors, snapshot, run } from "./webview-test.ts"

test("标题区域可见", async ({ view }) => {
    await expectVisible(view, "header h1")
})

test("无控制台错误", async () => {
    await expectNoConsoleErrors()
})

run()
```

可用断言辅助（详见 `scripts/webview-test.ts`）：`expectText`、`expectContains`、`expectVisible`、`expectCount`、`expectCountAtLeast`、`expectEval`、`expectNoConsoleErrors`、`snapshot`、`evalExpr`。

直接在用例内也能调用底层 `view` 方法：`await view.click("button.submit")`、`await view.evaluate("app.store.xxx")`、`await view.scroll(0, 400)`、`await view.screenshot({ format: "png" })`。

### 运行参数

- `--url <地址>`：覆盖基准地址（默认 `http://localhost:1420`）。
- 环境变量 `FRONTEND_TEST_URL`：同上，优先级低于 `--url`。
- 退出码：`0` 全部通过；`1` 有用例失败；`2` 无法导航到目标（通常是服务器没起）。

## Core Capabilities

1. **零依赖无头浏览器**：`new WebView({ width, height, console, dataStore })`，无需 npm 安装浏览器引擎。
2. **真实用户交互**：`click`/`type`/`press`/`scroll` 派发原生事件（`isTrusted === true`），选择器方法自动等待可操作。
3. **页面求值与状态读取**：`evaluate(expr)` 在页面上下文执行 JS 字符串并返回反序列化结果；`evalExpr` 封装了 `undefined` 安全处理。
4. **声明式断言**：文本 / 可见性 / 数量 / 任意表达式 / 控制台错误，断言失败即截图留证。
5. **截图归档**：`snapshot(ctx, name, format)` 保存到 `screenshots/`（相对 `process.cwd()`，建议以 `.tmp` 为工作目录），供人工或更严格的回归对比。

## Guidelines

- `evaluate` 接收**字符串表达式**；多语句用 `(function(){ ... })()` 包裹，顶层 `const/let` 会抛 `SyntaxError`。
- `console` 选项必须是**函数**（`console: (level, ...a) => {...}`），传 `true` 会抛 `ERR_INVALID_ARG_TYPE`。
- 断言失败不要靠"肉眼看截图"判断；优先用 `evaluate` 读取 DOM/状态做代码级断言，截图仅作人工辅助。
- **`navigate` 会重置页面**：注入到 `window` 上的钩子/状态（含 `localStorage` 之外的临时全局量）会随整页加载丢失，
  所以钩子只能在**最后一次 `navigate` 之后**装，且每个用例内自给自足，不要跨用例依赖注入态。
- **要断言「某个功能被触发」就 hook 构造函数**：例如确认「开关打开后立刻启动了索引 Worker」，
  可在页面里替换 `window.Worker` 记录 URL 再触发交互（`new Worker` 的参数是 `URL` 对象，`String()` 后含 `xxx.worker`）。
- **读模块内部状态**：页面上下文 `await import("/src/utils/xxx.ts")` 可直接调纯前端模块（裸包名解析不了），
  用来断言「语料/索引是否已建」这类不体现在 DOM 上的状态，比翻 UI 稳。
- 无头 profile 默认**深色主题且没有数据包**：依赖游戏数据的异步流程会静默失败，
  断言要挑不依赖数据的信号（DOM 结构、`localStorage`、Worker 计数），别把「数据没装」误判成功能坏了。
- 需要登录态跨用例保留时，给 `WebView` 传 `dataStore: "./profile"`。
- **导航 URL 一律追加 `?hideUpdateInfo=1`**，否则启动期的数据包弹窗会盖住页面（详见上方「启动弹窗」）。
- 截图与临时脚本放 `.tmp/`：框架按 `process.cwd()` 写 `screenshots/`，所以**以 `.tmp` 为工作目录运行**
  （`workdir: <repo>/.tmp`）即可让产物落在 `.tmp/screenshots/`，不要在仓库根留下 `screenshots/`。
- **点 reka-ui `Select` 只能坐标点可见项**：打开后全部 option 都在 DOM（本项目等级选择器有 240 个，不做虚拟化，`[role=option]` 可数），但**当前选中项会对齐触发器**，序号小于当前值的 option `getBoundingClientRect().top` 为负、点不到；`view.type` + `Enter` 的 typeahead 不生效。改值的做法：先 `evaluate` 取出一个**可见**（`top > 0 && bottom < innerHeight`）且 `innerText` 不等于当前值的 option，再用 `view.click(x, y)` 点它的中心。
- 每次变更前端后，建议把关键路由的冒烟测试纳入验证（配合 `pnpm lint` / `pnpm test`）。

## Resources

### scripts/

- `webview-test.ts` — 测试框架：注册器 `test`、断言辅助、截图、运行器 `run`。**测试文件应 import 它**。
- `example.test.ts` — 可直接运行的冒烟示例，针对本项目 `http://localhost:1420`。

### references/

- `webview-api.md` — `Bun.WebView` 完整 API 精简参考（构造、方法、后端、事件、注意事项）。
