如何让你的编程工具在浏览器中检测其运行效果
我受够了让编程机器人告诉我“前端工作已经完成”,但实际上根本没有人真正打开过前端界面来测试这些更改。 你们应该都见过这样的情景:机器人修改了一些代码,运行了格式化工具、类型检查工具和测试程序,所有结果都是正常的,然后它就会显示“已实现并验证通过”之类的信息。 但究竟是怎么验证的呢?通过测试确实能说明代码没有问题,但这并不等同于真正打开应用程序来使用那些刚刚被修改过的功能。 我遇到过很多这样的情况:代码本身没有问题,测试也通过了,但用户界面仍然存在一些明显的问题——比如某个控件与其他元素重叠了,某个选中的状态在数据存储中是存在的,但在屏幕上却没有高亮显示,某些操作在某种路径下可以正常执行,在另一
我受够了让编程机器人告诉我“前端工作已经完成”,但实际上根本没有人真正打开过前端界面来测试这些更改。
你们应该都见过这样的情景:机器人修改了一些代码,运行了格式化工具、类型检查工具和测试程序,所有结果都是正常的,然后它就会显示“已实现并验证通过”之类的信息。
但究竟是怎么验证的呢?通过测试确实能说明代码没有问题,但这并不等同于真正打开应用程序来使用那些刚刚被修改过的功能。
我遇到过很多这样的情况:代码本身没有问题,测试也通过了,但用户界面仍然存在一些明显的问题——比如某个控件与其他元素重叠了,某个选中的状态在数据存储中是存在的,但在屏幕上却没有高亮显示,某些操作在某种路径下可以正常执行,在另一种路径下却不行,某些过滤功能会更新数据,但页面上仍会保留旧的数据,保存后的界面看起来没什么问题,但一旦刷新页面,问题就会显现出来,还有地图图层的绘制顺序错误,弹出的模态窗口虽然技术上能够打开,但在屏幕上的显示位置却不合适等等。
机器人确实测试了它的代码,但没有人真正使用整个应用程序来进行测试。因此,我开始将浏览器测试作为任何面向用户的代码修改完成的标准之一。几周后,这个习惯就变成了一种实用的工具。关于这一点,我会在文章的最后部分详细说明。
在这篇文章中,你们将学习如何使用Playwright MCP为编程机器人提供一个真实的浏览器环境,以及如何编写相应的指令,让机器人去测试它刚刚做出的修改,而不仅仅是确认页面是否能够正常加载。
你们还会了解到如何让机器人以你们的项目本来使用的那种方式来启动应用程序,如何将这些指令分别放在`AGENTS.md`文件中或者一个可复用的技能配置文件中,为什么单独设置的审核机器人能够发现开发者遗漏的错误,以及如何确保机器人只清理它自己实际发起的操作所留下的痕迹。
在文章的最后部分,我会详细介绍这个工具后来发展成的开源项目“katalepsis”,包括它是如何判断某个检查是否真正通过的具体机制。
目录
先决条件
这并不是一份针对初学者的教程,因此以下准备是必要的:
需要具备使用支持MCP(模型上下文协议)的编码工具的经验,例如Claude Code、Codex或Cursor,并且要了解如何向这些工具发出指令。
需要一个可以通过JavaScript或TypeScript编写的开发脚本在本地运行的Web应用。示例中使用了基于
pnpm的Node项目,但实际使用过程中并不依赖任何特定的框架。需要具备基本的Playwright基础知识,了解测试、定位器以及断言的概念。在学习前半部分内容时,并不需要预先准备现有的测试套件。
如果想要使用Playwright的MCP功能,需要Node.js 20或更高版本;如果还想安装katalepsis插件,则需要Node.js 22.12或更高版本以及Git。
需要熟悉终端命令和Git操作,因为编码工具会自动启动进程并执行
git status等命令。
循环代理缺失的问题
编码工具之所以能迅速发挥重要作用,很大程度上是因为它们已经具备了完善的代码反馈机制:这些工具可以读取、编辑代码,运行格式化工具和测试用例,分析错误结果并重新尝试执行操作。因此,人们并不需要一次性就完成所有工作。
对于前端开发来说,目前明显缺失的是一个能够实现这种循环流程的工具。我理想中的循环流程应该是这样的:修改代码后运行测试,启动应用程序,使用新功能查看效果,发现问题后进行修复,然后再重新使用该功能。
最后这一步的重要性可能超乎你的想象。编码工具虽然可以告诉你“修复后的代码应该能够正常工作”,但我更希望它们能直接打开页面来验证这一结论是否正确。
Playwright的MCP功能正好可以实现这一点。我之前已经使用Playwright进行过端到端测试,而MCP服务器的另一个重要作用在于:它可以直接将浏览器交给编码工具使用。该服务器可以打开本地主机页面,查找控件元素,执行点击和输入操作,检查代码变化情况,并生成截图。
这与让编码工具运行pnpm playwright test是不同的。测试套件只会告诉你之前编写好的测试用例是否仍然能够通过,而MCP功能则会具体分析某项修改会对系统产生哪些影响。
如果编码工具对某些通用的地图层功能进行了重构,那么相关的测试用例可能会覆盖其中的一些辅助函数。但通过代码差异分析,还可以发现这些改动实际上会影响页面在初始渲染、元素选择、过滤以及重置等各个阶段的运行情况,因此编码工具会针对这些环节进行逐一测试。这种测试方式与我自己审查前端代码的方式非常相似。
设置过程相当简单,你只需要安装Node.js 20或更高版本,以及一个支持MCP功能的客户端工具即可:
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": ["@playwright/mcp@latest"]
}
}
}
一个简单的验证方法就是让编码工具打开Playwright的TodoMVC演示页面,添加一个名为“浏览器质量测试”的待办事项,然后确认这个待办事项确实能够正常显示。如果它能完成这个操作,那就说明它具备使用浏览器的能力。接下来要做的就是看看它如何利用这些浏览器功能了。
为什么单独使用的浏览器会存在局限性
仅仅为代理程序提供浏览器,并不能让它知道该查看什么内容、应该浏览到哪些位置,或者什么样的信息才能被视为有效的验证依据。如果没有这些指导,代理程序就会选择那些在技术上最简单的、只需要使用浏览器就能完成的任务。即使使用了Playwright工具,这样的代理程序仍然无法进行有效的质量测试。我就收到过这样的报告:
页面已成功加载。
浏览器验证已完成。
这份报告只是说明了页面已经打开,但并没有涉及到任何测试内容。其中的内容根本无法发现本文开头提到的那些问题。
因此,代理程序需要具体的操作指令才能完成相应的测试工作。下面是我制定的一些规则,用于指导代理程序如何检查自己的前端代码:应该测试哪些内容、需要检查什么细节,以及应该收集哪些证据作为验证结果。这些规则最初只是我在提示信息中反复重复的几句话,后来才发展成了你在后续章节中会看到的那些浏览器测试流程。
我确定的第一个规则是:测试内容必须与所进行的修改直接相关。如果修改了过滤功能,就需要使用这个新的过滤条件;如果修改了重置逻辑,就应该先改变某些状态然后再进行重置;如果涉及到了数据持久化存储,就需要刷新页面;如果修改了模态窗口的功能,就需要打开和关闭这个窗口;如果修改了响应式布局,就需要调整视口的大小。
当人们把这些规则说出来时,听起来似乎都很显而易见,但如果你让代理程序的指令仅仅是“验证应用程序是否能正常运行”,那么这些规则就根本不会被执行。
在实际操作中,这意味着代理程序在开始使用浏览器之前,会先读取代码变更内容,并将其转化为一个简短的测试计划。例如,对于过滤栏的修改,我期望看到的测试计划应该是这样的:
修改文件:src/features/projects/FilterBar.tsx, srcfeatures/projects/useFilters.ts
测试步骤:
1. 打开/project页面,查看当前显示了多少行数据。
2. 按“状态”字段进行过滤,所有行的状态都应该显示为“活跃”。
3> 在筛选结果中添加“我”作为所有者,此时列表应该会再次缩小。
4> 清除所有的筛选条件,页面上应该会恢复到最初的行数。
5> 再次应用筛选条件并刷新页面,由于筛选条件已经包含在URL中,所以这个设置应该能够保持有效。
然后,代理程序就会通过Playwright MCP提供的浏览器工具来执行这个测试计划。前几个步骤的具体操作代码如下:
browser_navigate { "url": "http://localhost:5173/projects" }
browser_snapshot {} → 页面中显示40行数据
browser_click { "element": "状态筛选按钮", "ref": "e14" }
browser_click { "element": "活跃选项", "ref": "e31" }
browser_snapshot {} → 页面中仅显示12行数据,所有行的状态都是“活跃”
browser_click { "element": "清除筛选条件", "ref": "e15" }
browser_snapshot {} → 页面中再次显示40行数据
其中ref值是从无障碍访问功能获取的,这样代理程序就可以准确地找到需要操作的控件,而无需去猜测CSS选择器的具体内容。
这种做法在某次修复交互式地图中光标无法正常退出悬停状态的问题时发挥了重要作用。虽然这次修改后的代码通过了原有的单元测试,但并不能证明这个修复措施确实有效。直到代理程序实际运行应用程序、在交互层面上进行各种操作后,我们才确认这个修复确实是有效的——因为光标的确能够按照预期的方式变化和恢复。第二条规则是检查最终结果,而不是交互过程本身。“我点击了‘重置’按钮,但什么变化也没有发生”这样的描述并不能提供太多有用的信息。你真正关心的是:重置操作是否真的把各项设置恢复到了原来的状态。保存设置也是如此——关键在于确认在更改设置并保存后,重新加载页面时这些设置仍然存在。
如果用测试用例来表述这一规则,那么代码会像这样写:其中最重要的一行是在页面重新加载之后的那部分代码:
import { test, expect } from "@playwright/test";
test("每周摘要设置在页面重新加载后仍然有效", async ({ page }) => {
await page.goto("/settings/notifications");
await page.getByRole("switch", { name: "Weekly digest" }).click();
await pageByID("button", { name: "Save" }).click();
await page.reload();
await expect(page.getByRole("switch", { name: "Weekly digest" }).toBeChecked());
});
代理程序不需要编写这样的测试用例来遵守这一规则。通过MCP,它也可以手动完成相同的操作:点击按钮、保存设置,然后重新加载页面,并从新的页面状态中判断开关选项是否仍然处于启用状态。
根据不同的需求,我也会要求提供不同类型的证据。例如,需要检查URL是否发生了变化、某个按钮是否被禁用,或者是否有对话框弹出——这些都需要通过分析DOM结构和浏览器状态来确定,而截图并不能满足这些需求。另外,如果需要判断某个对话框是否被截取了、两个元素是否重叠了,或者文本在手机屏幕上显示的效果如何,这时截图就非常有用。不过有时候,这两种方法都是必要的,因为有些模态窗口在DOM结构中是存在的,但在屏幕上可能无法正常显示。
对于布局上的变化来说,就需要调整页面的大小,并捕捉用户实际看到的界面效果:
browser.resize { "width": 375, "height": 812 }
browser.click { "element": "Export button", "ref": "e22" }
browser.take_screenshot { "filename": "export-dialog-375.png" }
browser.resize { "width": 1440, "height": 900 }
browser_take_screenshot { "filename": "export-dialog-1440.png" }
在测试过程中,代理程序还可以查看控制台日志;如果某个功能需要调用API或进行网络请求,那么也可以检查相关的网络请求记录。不过我只需要关注那些由这次变更所引发的错误而已。例如,如果一个已经存在了六个月的警告信息并不会影响整个测试流程;而对于仅对客户端生效的变更,则不需要进行网络性能测试。如果某个本应能够成功保存设置的操作实际上导致了“500 Internal Server Error”这样的错误,那我肯定需要知道这一点。
browser_console_messages和browser_network_requests这两个函数可以用来查看页面输出的信息以及发起的网络请求记录。因此,即使用户界面看起来没有问题,但如果保存操作失败了,也会在日志中显示相应的错误信息:
[PUT] http://localhost:5173/api/settings => [500] Internal Server Error
综合这些规则,就能把这一节开头那些模糊不清的测试要求转化为具体可执行的操作步骤了:
路由:/settings/notifications
操作步骤:开启“每周摘要”功能,点击“保存”按钮,然后重新加载页面。
检查结果:重新加载后开关选项仍然处于启用状态(aria-checked="true");API请求返回200码,控制台也没有出现新的错误信息。
截图记录:settings-375.png、settings-1440.png(在手机上显示时文本换行效果正常)
测试结果:pnpm test settings命令执行后,有14个测试用例通过了。
通过这些步骤,我就能够有效地验证这个功能是否按照预期工作了。
如何启动真实应用
在应用真正运行之前,代理程序根本无法进行任何测试,而正是在这种情况下,它最容易选择走捷径。
我并不想在每个代码仓库中都写“运行 `pnpm dev`”这样的指令,因此这套流程要求代理程序自行判断如何启动项目。它会首先检查 `package.json` 文件中的 `dev` 脚本,然后参考 `README` 文档中的说明;只有当其他方法都无法让应用正常运行时,才会选择使用现有的 Docker 配置来启动应用。
这套流程实际上就是 “浏览器评审” 功能中 “启动应用程序” 部分的具体实现方式,你将在下一节中看到完整的示例。在典型的 Vite 或 Next.js 项目中,按照这个流程操作,代码会看起来像这样:
# 1. 这个项目是如何启动的?
jq .scripts package.json
# {
# "dev": "vite",
# "build": "vite build",
# "test": "vitest run",
# "e2e": "playwright test"
# }
# 2. 在代理程序拥有的临时文件夹中,后台启动开发服务器,并记录其进程 ID,以便日后能精确地终止该进程。
QA_DIR=$(mktemp -d)
pnpm dev --port 5173 > "$QA_DIR/dev.log" 2>&1 & &
echo $! > "$QA_DIR/dev.pid"
# 3. 等待服务器启动成功后,再打开浏览器。
until curl -sf http://localhost:5173 > /dev/null; do sleep 1; done
如果项目中没有 `dev` 脚本,就会查看 `README` 文件;而如果 `README` 中说明应用需要通过 `docker compose up` 来启动 Postgres 和 Redis,代理程序就会使用该配置文件来创建相应的容器,并记录这些容器的信息。它不会重新编写配置文件。
我真正不希望看到的是代理程序为了方便测试而对应用程序进行任何修改:不允许使用临时身份验证绕过机制,不允许搭建虚假服务器,也不允许更改 `.env` 文件中的配置,更不允许因为无法确定正确的配置方式而创建一些临时路由。我们的目标应该是测试我实际要发布的软件本身。
比如下面这种修改,当登录页面成为测试障碍时,代理程序确实会倾向于这样做:
- if (!session) redirect("/login");
+ if (!session && process.env.NODE_ENV !== "development") redirect("/login");
如果代理程序使用项目文档中规定的测试账户或初始数据无法通过身份验证,正确的做法应该是立即停止操作,并报告该浏览器评审被阻断的原因。如实反映问题才是负责任的做法;而如果关闭了应用程序的身份验证机制再进行评审,那根本就不算是对真正应用的评估。
说明文件的存放位置
起初,我考虑的是继续在 `AGENTS.md` 文件中添加相关内容。这个文件已经包含了仓库中所有代理程序都应该了解的规则:代码结构、编码规范、测试理念、如何进行最小限度的修改,以及禁止编写无意义的注释。
我本来还打算再添加上百行内容,介绍如何启动应用、如何配置浏览器环境、需要点击哪些按钮、应该截图哪些界面、如何查看控制台错误信息以及如何关闭应用程序……但到了某个阶段,这个文件就会不再仅仅是指导说明,而变成了一篇冗长繁琐的文档。
了解这些文件各自的用途是非常有帮助的。AGENTS.md是一个普通的Markdown文件,像Codex这样的编码助手会在该仓库的每次会话开始时自动读取它;而Claude Code也会对CLAUDE.md做同样的事情。文件中的所有内容都会被应用到每个任务中,无论这个任务是修改CSS代码还是进行数据库迁移。因此,在每次运行时这些内容都会被加载,并且会被应用于所有的地方。
“技能”文件的处理方式则有所不同。这类文件是一个文件夹,其中包含一个以简短的名称和描述开头的SKILL.md文件。在会话开始时,助手只会看到这些名称和描述,每个技能只会有一两行相关内容。当某个任务与描述相符,或者有其他指令指定了需要使用某个技能时,助手才会读取文件的其余部分并按照其中的指示来执行操作。因此,你可以编写一份详细的长流程文件,而在实际需要的时候才将其加载出来。
我最终得出的结论是:将AGENTS.md视为“章程”,而将技能文件视为“操作手册”。章程内容简短,适用于所有情况,它规定了必须遵守的基本规则,但不会详细说明具体操作方法;而操作手册则是针对特定情况制定的逐步指导流程,只有当遇到这种情况时才会被使用。
在这种配置下,AGENTS.md明确规定:用户可见的修改内容必须在浏览器中经过验证后才能被视为完成;而“browser-review”技能文件则详细说明了如何进行这种验证:
/
├── AGENTS.md
└── .agents/
└── skills/
├── browser-review/
│ └── SKILL.md
├── pr-review/
│ └── SKILL.md
└── grill-me/
└── SKILL.md
AGENTS.md中的规则表述简洁明了,且始终处于可见状态:
## 浏览器验证
用户可见的修改内容必须在运行中的应用程序中经过验证后才能被视为完成。
对于涉及用户界面变更的任务,请参考`.agents/skills/browser-review/SKILL.md`文件。
成功的构建测试、类型检查、单元测试或端到端测试并不能替代浏览器验证。
请不要提交与浏览器验证相关的结果文件。在进行验证之前,务必停止所有相关操作并完成清理工作。
技能文件中包含了具体的操作步骤,只有在实际需要时才会被加载。我使用的是Codex和Claude Code,因此系统中有一个全局的AGENTS.md文件,Claude的CLAUDE.md会导入这个文件;同时还有一个共享的技能文件夹,这两个工具都会读取其中的内容。无论那天我使用哪个助手,它都会遵循相同的操作流程。
导入语句只需要一行代码,因为Claude Code允许CLAUDE.md通过@符号来引用另一个文件:
@~/.codex/AGENTS.md
以下是我最初编写的内容的简化版本:
---
名称:浏览器测试
描述:使用Playwright MCP来验证运行中的应用程序中那些对用户可见的变更。
---
# 浏览器测试
## 在开始测试之前
运行`git status --short`,记住之前存在哪些文件和目录。不要修改任何已存在的代码或配置。
仔细阅读测试任务和相关差异信息,弄清楚哪些路由受到了影响,数据流发生了怎样的变化,以及页面在加载、为空、出现错误、被选中或被禁用时的状态会发生什么变化;同时也要判断是否有某些内容需要在页面刷新后依然保持不变,还有哪些附近的功能可能会因此出现异常。
## 启动应用程序
首先使用仓库提供的开发脚本进行初始化设置,然后按照文档中的说明进行本地环境的配置,最后使用现有的Docker环境来运行应用程序。不要自行创建新的启动配置文件,也不要为了方便测试而修改应用程序的行为。请记录下你启动的所有进程和容器。
## 测试变更内容
页面加载完成后,不要立即停止测试流程。要完整地执行整个变更后的操作流程,并检查最终的结果。对于功能方面的验证,可以使用浏览器或DOM的状态来进行判断;而对于视觉方面的问题,则需要通过截图来确认。请及时报告任何新的错误信息或失败的请求,而不要忽略那些旧的警告信息。
如果发现某个功能出现了故障,请先修复导致问题的最小原因,然后重新运行相关的测试,并再次检查整个操作流程。千万不要想当然地认为修复后的代码就能正常工作。
## 清理测试环境
即使测试失败,也必须进行环境清理。关闭所有浏览器,停止服务器和容器的运行,并删除生成的截图、日志文件以及其他测试产生的残留文件,但请只删除你自己创建的这些文件。在完成清理工作之前,请再次运行`git status --short`命令。绝对不要执行`git add`、`git commit`或`git push`等操作。
## 提交测试结果
请详细说明你测试了哪些路由,进行了哪些操作,检查了哪些状态,运行了哪些测试,以及发现了什么问题并如何进行了修复。如果无法完成全部测试,请说明原因。这些做法其实都并不高明。关键在于,这种行为本身就是人们的默认预期,因此我并不指望代理程序能够自行判断是否应该打开浏览器来执行相关操作。
为什么审核者应该独立工作
明显的缺陷在于:负责判断代码是否完成工作的代理程序,恰恰就是编写该代码的同一個程序。虽然这种做法还是有一定价值的——毕竟我也会自己测试自己的代码——但问题在于,这种测试方式是基于开发者对代码实现原理的理解来进行的。
这里所说的“原理”,指的是开发者对自己所修改的代码的认知:他们知道修改了哪些代码,认为这些代码会带来什么影响,同样重要的是,也明白哪些代码没有受到影响。他们的测试计划正是基于这种认知制定的。因此,他们会只测试那些他们原本打算修改的部分,而完全不会去检查那些他们没有意识到会被影响的代码路径。这就是人们在自己测试代码时常常会犯的错误,而那些在审核过程中被遗漏的漏洞,往往就隐藏在这些被忽略的地方。
因此,当第二个代理程序来审核某个分支代码时,我会要求它自行确定测试范围。我现在制定的全局规则要求使用一个没有之前交流记录的新审核者:这个审核者只会收到原始请求、代码差异文件以及相关的审核工具,而不会了解到开发者认为代码为何能够正常工作。审核者需要自己判断这些修改可能会对代码产生哪些影响,并据此进行测试,而不能依赖开发者的截图、质量保证笔记或测试结果。
为这个审核者准备的提示信息被刻意设计得非常简短:
你正在审核一个并非你自己编写的代码分支。
原始请求:
需要将端到端应用的URL设置为可以通过E2E_BASE_URL来配置,这样测试就可以在任何端口上运行。
请执行`git diff main...HEAD`命令查看代码变更内容。按照“浏览器审核”指南进行操作,自行判断这些变更可能会对代码产生哪些影响,然后进行测试,并用证据支持你的判断结果。
迄今为止,最有效的发现来自于一个将端到端测试配置中的应用URL设置为可配置的Pull Request。审核者注意到,那些预先设置好的测试数据中,图像文件的URL仍然被硬编码为旧的端口地址,而在其他端口上运行应用程序后,这个问题确实得到了验证:有三条请求发送到了错误的端口,导致请求失败;而页面上则显示了备用图片。这个错误完全没有引起任何警报或异常。
简单来说,修改后的全局配置文件内容如下:
// e2e/global-setup.ts
const baseURL = process.env.E2E_BASE_URL ?? "http://localhost:8080";
await seedDatabase({
media: [
{ id: "m1", url: "http://localhost:8080/test-media/cover-1.jpg" },
{ id: "m2", url: "http://localhost:8080/test-media/cover-2.jpg" },
{ id: "m3", url: "http://localhost:8080/test-media/cover-3.jpg" },
],
});
开发者的理解是“应用URL现在是由E2E_BASE_URL决定的”,因此他运行的所有测试都使用了默认端口,而那些被硬编码的URL地址恰好也是正确的。但审核者并没有这种先入为主的观念。他仔细阅读了所有被修改过的配置文件内容,在8081端口上运行了应用程序,最终得出了不同的结论。
发现:在 e2e/global-setup.ts 文件中,那些用于初始化的媒体资源链接仍然指向 :8080 这个端口。
将 E2E_BASE_URL 设置为 http://localhost:8081 后运行应用程序,结果有 3 次请求到 http://localhost:8080/test-media/... 的操作失败了,错误原因是 net::ERR_CONNECTION_REFUSED。
在页面上显示时,图库显示的是 3 张备用图片,而不是实际的媒体文件(例如 gallery-8081.png)。
建议的解决方法是将这些初始化链接根据 E2E_BASE_URL 进行重新生成,或者让它们成为相对路径。
为了解决这个问题,我们只需要为每个链接添加一行代码即可:
{ id: "m1", url: new URL("/test-media/cover-1.jpg", baseURL).href },
进行测试后发现,无论采用哪种方法,检查这些卡片是否存在的结果都是正确的,因为备用图片同样属于“卡片”这一类别。只有通过查看页面以及网络浏览器的记录,才能发现这个差异——而这正是本文所要讨论的内容。
自己完成清理工作
一旦代理程序能够启动服务器和浏览器,那么进行清理工作就成为了设计流程中必不可少的一部分。否则,每次会话结束后都会留下一些残留物:比如未关闭的节点进程、Chromium 浏览器窗口、截图文件、Playwright 的运行记录、临时生成的认证文件,或者 Docker 容器等等。
不过,“把所有东西都清理干净”这种做法其实是错误的。pkill node 这条命令并不能算作真正的清理操作,它只会终止正在运行的节点进程;而 git clean -fd 则会删除那些与代理程序无关的、未被跟踪的文件。
正确的原则是:只清理你自己创建的东西,其他东西一律不要动。在清理前后使用 git status 命令来检查文件状态,这样就可以确保浏览器测试过程中产生的任何更改不会被无意中加入到代码库中。
# 不进行清理操作
pkill node
git clean -fd
# 进行清理操作
kill "$(cat "$QA_DIR/dev.pid")"
rm -rf "$QA_DIR"
在完成清理工作后,我期望看到的结果应该是这样的:
清理结果:
- 我启动的开发服务器已经停止运行(进程 ID 为 48213,使用命令 pnpm dev --port 5173 来终止它)。
- Playwright 的 MCP 浏览器也已经关闭。
- 文件 export-dialog-375.png 和 export-dialog-1440.png 已被删除。
- 目前仍在运行的进程是 pid 39102,它是通过 pnpm dev 在端口 :3000 上启动的,这个进程在我开始清理操作之前就已经在运行了,所以它并不属于我创建的进程。
两次使用 git status 命令检查后的结果是一样的,这说明在清理过程中没有任何错误发生,那些原本就存在的未跟踪文件也没有被删除。
这个规则也不是我凭空想出来的。在开发我即将介绍的这个工具时,用于测试该工具的代理程序自动执行了清理操作,它关闭了我机器上所有的 Playwright 浏览器窗口,而不仅仅是它自己使用的那个浏览器。公平地说,该代理程序在它的运行总结中确实提到了这一点。
当某项技能不再足够使用时
这个工具确实起到了预期的效果,因为代理程序确实会打开相应的应用程序。但我得到的结果仍然只是文字描述而已。代理程序会告诉我它已经打开了页面、按照流程进行了操作,一切看起来都正常,而我只能相信它的说法。
这其实就是我最初遇到的那个问题,只不过程度更严重了一些。我并不想要关于浏览器表现的漂亮描述,而是需要实实在在的证据:哪些操作成功了,哪些失败了,同时还需要有截图和运行记录作为支撑,这样我就可以把这些资料提交给审核人员。
于是我制定了一个开发计划,在一个周六花了一整天的时间来实现这个工具。在开发过程中,我通过设定里程碑以及进行独立审查来确保代码的质量。这个工具被命名为katalepsis——这个词在斯多葛哲学中意味着对某件事的理解如此清晰,以至于不可能出错,因此用它来命名这个工具似乎非常合适。
用户只需提供一个预览链接,这个工具就会生成一份报告:报告中包含HTML格式和Markdown格式的文字内容,这些资料可以直接添加到Pull Request中;同时还会提供JSON格式的数据文件。
这个工具具有两种功能。katalepsis run会使用你现有的Playwright测试用例来检测预览版本的功能是否正常,而Playwright给出的结果就是最终结果。katalepsis会对这些结果进行整理、编辑和打包,但绝不会重新解释任何测试结果。
katalepsis verify则会利用简单的英文描述来检查那些目前还没有人编写测试用例的功能。它会使用你自己的Anthropic、OpenAI或OpenRouter密钥,在真实的浏览器环境中执行这些测试。此外,还配备了MCP服务器,因此进行过修改的代理程序也可以自行运行这些测试并查看结果。
如何试用katalepsis
目前katalepsis还没有在npm上发布,因此用户需要先克隆它的代码仓库再进行安装。安装时需要Node 22.12或更高版本、Git以及ppnm 10(或者随Node一起提供的corepack):
curl -fsSL https://www.katalepsis.glamboyosa.xyz/install.sh | sh
安装完成后,katalepsis和katalepsis-mcp这两个工具会被添加到~/.local/bin目录中。
如果你想使用现有的测试用例来检测预览版本的功能,那么你的Playwright配置文件必须使用katalepsis提供的链接。同时,建议开启失败记录功能,这样报告中才会包含具体的失败信息:
// playwright.config.ts
use: {
baseURL: process.env.KATALEPSIS_BASE_URL ?? "http://localhost:3000",
screenshot: "only-on-failure",
video: "retain-on-failure",
trace: "retain-on-failure",
},
该代码仓库中包含了一个简单的演示应用程序,以及一个故意设置了错误的注册表单:提交空表单时不会显示错误信息,而是什么都不会发生。这个应用程序包含了三个Playwright测试用例。下面是实际运行这些测试的示例:
$ katalepsis run --cwd apps/demo-form --url http://127.0.0.1:4173
katalepsis: FAILED
2 passed, 1 failed, 0 blocked, 0 not tested
Preview commit: not reported, so unconfirmed
Report: artifacts/katalepsis/2026-10-19T08-21-25-288Z-3dba5a/report.html
Markdown: artifacts/katalepsis/2026-10-19T08-21-25-288Z-3dba5a/report.md
这份Markdown格式的报告是用于粘贴到Pull Request中的。它的顶部结构如下:
# katalepsis报告:测试失败
**在进行的3项检查中,2项通过,1项失败,0项被阻塞**。
- 预览地址:http://127.0.0.1:4173/
- 预览提交信息:未生成报告,因此结果未确认
- 所使用的工具版本:katalepsis 0.1.0、Playwright 1.63.0、Node 22.22.0
## 检查项目
| 状态 | 检查内容 | 项目名称 | 测试位置 | 执行时间 |
| --- | --- | --- | --- | --- |
| 失败 | “注册表单为空时,系统会显示必填字段错误” | chromium | signup.spec.ts:8 | 3.2秒 |
| 通过 | “填写有效的注册信息后,系统会显示确认提示” | chromium | signup.spec.ts:13 | 418毫秒 |
| 通过 | “输入无效的电子邮件地址时,系统会显示错误提示且不会提交表单” | chromium | signup.spec.ts:20 | 449毫秒 |
### 失败原因:“注册表单为空时,系统会显示必填字段错误”
- 失败类型:断言失败
- 证据:截图、视频记录、日志文件
在报告的下方,还会包含Playwright自身生成的失败信息(例如 `getByRole('alert') 方法未执行成功》),以及一些限制说明,比如预览功能无法显示该测试是基于哪个提交版本构建的。
对于那些没有对应测试用例的功能,你可以用简单的英语来描述这些检查规则。每条规则都会明确说明其目的(即需要执行什么操作)以及通过的标准(即在测试结束时哪些条件必须满足):
{
"checks": [
{
"id": "empty-form",
"name": "表单为空时的验证",
"intent": "不填写任何内容就提交注册表单",
"passCondition": "系统必须显示必填字段错误提示"
},
{
"id": "invalid-email",
"name": "无效电子邮件地址",
"intent": "填写姓名后输入无效的电子邮件地址再提交表单",
"passCondition": "系统必须显示电子邮件格式错误提示,并且不会显示成功确认信息"
}
]
}
接下来,你需要为katalepsis指定一个密钥来运行这些测试。这个密钥可以保存在操作系统的密钥链中,或者从环境变量中读取,但它绝对不能通过命令行传递:
$ katalepsis auth set openrouter
$ katalepsis verify --cwd apps/demo-form --url http://127.0.0.1:4173 --checks checks.json \
--provider openrouter --model anthropic/claude-haiku-5.5
katalepsis: 测试失败 (openrouter/anthropic/claude-haiku-5.5)
2项通过,1项失败,0项被阻塞
预览提交信息:未生成报告,因此结果未确认
报告文件路径:artifacts/katalepsis/2026-10-10T12-45-05-343Z-b51a0a/report.html
这个演示用例中的`checks.json`文件包含了三项检查规则,而这些检查能够发现与Playwright测试相同的错误——尽管实际上并没有人专门为这些功能编写测试用例。
最后,为了让编码代理能够自动完成所有这些测试工作,你需要注册一个MCP服务器。使用Codex工具,操作步骤如下:
codex mcp add katalepsis -- katalepsis-mcp --root /path/to/your-app
这个服务器会提供三个工具:`verification_run_tests`、`verification_check_browser`和`verification_get_report`,而且这些工具只能在你指定的文件夹内使用。在实际应用中,代理程序会收到这样的指令:“修改注册表单的内容后,运行`verification_check_browser`来检查更改是否有效,然后将生成的Markdown报告粘贴到总结报告中。”
katalepsis的内部工作原理
其中一些决策的制定正是基于本文中提到的问题。下面来看看这些机制在代码中的实现方式。
katalepsis是一个TypeScript单仓库项目,它由五个包组成:
core:结果数据结构、Playwright运行引擎以及报告生成工具agent:用于浏览器测试的工具及verify功能所需的证据验证规则credentials:存储密钥和配置参数mcp:MCP服务器cli:命令行工具
在那些需要处理错误、取消操作或依赖关系的场景中,katalepsis会使用Effect机制;而报告格式的生成则依赖于Effect Schema。verify模块中的模型循环会使用Vercel AI SDK,因此同样的代码可以用于Anthropic、OpenAI或OpenRouter等平台。
报告是一种数据结构,而非日志记录
无论测试是由Playwright执行还是由模型完成,在任何数据被写入之前,所有结果都会被转换成相同的VerificationRun结构。单个检查项的简化版本如下所示:
export const CheckStatus = Schema.Literals(["passed", "failed", "blocked"])
export const VerificationCheck = Schema.Struct({
id: Schema.String,
source: Schema.Literals["playwright-test", "agent"],
title: Schema.String,
status: CheckStatus,
flaky: Schema.Boolean,
failure: Schema.NullOr(Schema.Struct({ code: FailureCode, message: Schema.String })),
attempts: Schema.Array(Attempt),
evidence: Schema.Array(BundlePath),
});
那些从未被执行的测试会被记录在notTested列表中,并会附带相应的原因说明。因此,最终会出现四种结果:通过、失败、被阻塞或未测试。每份证据文件都会与其SHA-256哈希值一起被保存;同时,BundlePath格式不允许使用绝对路径或“..”符号,因此报告文件无法引用其所在文件夹之外的资源。
被阻塞并不等于失败
如果预览功能不可用,系统只会显示“被阻塞”的状态,而不会将这三个测试项都标记为失败。在启动Playwright之前,katalepsis会先尝试访问一次预览地址。相关的代码如下:
const UNAVAILABLE_STATUSES = new Set([502, 503, 504]);
const response = await fetch(url, {
redirect: "manual",
signal: AbortSignal.any([signal, AbortSignal.timeout(30_000)]),
});
if (UNAVAILABLE_STATUSES.has(response.status)) {
return { reachable: false, reason: `预览服务返回了HTTP ${response.status}状态码` };
}
如果发生网络错误或目标服务器处于不可用状态,所有选定的测试都会被阻塞,Playwright也就不会被启动。例如,当将演示程序指向一个没有任何服务的端口时,就会出现如下输出:
$ katalepsis run --cwd apps/demo-form --url http://127.0.0.1:4999
katalepsis: BLOCKED
0 passed, 0 failed, 3 blocked, 0 not tested
preview-unavailable: http://127.0.0.1:4999/ did not respond (connect ECONNREFUSED 127.0.0.1:4999). Playwright was not run.
剧作家的判断是最终的
run命令会使用--reporter=json选项启动项目自带的Playwright工具,并直接读取结果文件,而无需重新实现任何功能。Playwright在重试后仍认为通过的测试会将其标记为flaky,因此katalepsis也会保留这一标签,并记录所有的尝试过程。
它所增加的功能是对测试结果进行分类:如果失败信息中指出预览内容的来源无法被访问,那么该测试就会被标记为“阻塞”状态;而第三方API出现的故障则仍然会被视为失败,因为这确实是应用程序本身存在的问题。同样,如果浏览器无法正常启动,也会被标记为阻塞状态——我在使用安装了错误版本浏览器的机器上首次运行这篇文章的演示时,就遇到了这种情况。
模型无权判定某项操作是否通过
在verify函数中,模型虽然可以点击、输入内容并查看页面,但它无权自行判定某项操作是否通过。只有当模型指向的浏览器检测结果表明katalepsis执行的检查已经通过,并且检测结果显示的内容在模型操作之前确实不存在于页面上时,该测试才会被认定为通过。否则,它仍然会被标记为阻塞状态。
其具体运作机制如下:模型只能使用一组有限的工具,这些工具包括navigate、snapshot、click、fill、press、assert和finish。当模型调用assert时,它并不会直接返回自己的判断结果;katalepsis会在浏览器中实际执行这些操作,然后截取屏幕截图,并用像a3这样的编号来记录检测结果。当模型调用finish时,就必须引用这些编号。
finish: tool({
description: "一次性报告检测结果,需要引用诸如a1之类的编号。",
inputSchema: schema(
Schema.Struct({
verdict: Schema.Literals(["passed", "failed", "blocked"]),
summary: Schema.String,
assertionIds: Schema.Array(Schema.String),
}),
),
execute: (verdict) => {
onFinish(verdict);
return "检测结果已记录.";
},
}),
随后,一个名为decideOutcome的函数会将模型的判断结果与实际证据进行对比。以下是用于判断测试是否通过的代码片段:
if (!cited.every((assertion) => assertion.passed)) {
return "代理报告称测试通过,但其中某些检测项实际上并未通过。",
}
const shown = cited.filter((assertion) => assertion.expectation === "visible");
if (shown.length === 0) {
return "代理报告称测试通过,但所引用的检测项全部都是隐藏的或无法被访问的。",
}
if (!observeOnly && amp; shown.every((assertion) => assertion.trueAtStart === true)) {
return "代理报告称测试通过,但实际上在它开始操作之前,页面上就已经存在所有这些内容了。",
}
如果出现上述任何一种情况,检测结果都会被标记为“阻塞”,并且会使用no-evidence这个代码来说明原因。另外,如果引用了某个从未被实际执行的检测项,也会导致测试失败并被标记为阻塞状态。此外,如果在最后引用的检测项之后还有其他检测项失败,因为这些新的证据与之前的判断结果不一致,那么测试也会被视为失败。
trueAtStart这个条件的存在,是因为我测试过的某个模型试图通过引用电子邮件字段的标签来证明某个表单是有效的,而那个标签本来就存在于页面中。
trueAtStart: true的结果,但这样的结果本身并不足以证明整个测试是成功的。
一次只执行一个操作
在最初的测试过程中,我们还发现了并行执行的工具调用会相互干扰的情况,甚至还有数据被错误地输入到电子邮件字段中的现象。有些模型喜欢同时发送多个工具调用,比如两个fill调用会同时尝试向当前处于焦点状态的字段中输入数据。为了解决这个问题,我们引入了一个任务队列机制,确保每个浏览器操作都会按照顺序依次执行:
private exclusive(action: () => Promise): Promise {
const next = this.queue.then(action, action);
this.queue = next.catch(() => undefined);
return next;
}
每个操作都会等待前一个操作完成,无论它是成功还是失败;即使某个操作失败了,也不会影响后续操作的执行。
预算与限制条件
每个代理检查任务都在独立的浏览器环境中运行,并且会开启跟踪功能,因此生成的报告中总是包含可回放的跟踪信息。浏览范围仅限于预览页面的原始来源地址,以及通过--allow-origin选项允许访问的其他地址。该系统还设置了步骤数限制(,默认值为15)、令牌数量限制(--max-tokens,默认值为200,000)以及时间限制(--check-timeout,默认值为120秒);一旦这些限制被突破,系统就会报告“阻塞”错误,并会说明具体原因,而不会将其视为测试失败。报告中的URL地址中的查询字符串会被隐藏起来。
开发过程
在开始编写代码之前,我首先制定了一个分阶段的开发计划。M0阶段包括回顾我之前的两个项目是如何处理凭证和提供者的问题的,同时还确定了最初的架构设计方案以及相关的单仓库工具配置。M1阶段涉及Playwright运行时框架和报告功能的开发;M2阶段则是使用用户自己的密钥进行代理检查测试;M3阶段则专注于MCP服务器的实现。每个阶段结束后,都会使用全新的代理节点重新开始测试,确保之前的测试结果不会影响当前阶段的评估过程,这一做法与本文前面提到的“独立审核机制”是一致的。
实际上,这些回顾过程对最终开发的工具产生的影响比开发计划本身还要大。在M0阶段的评审中,我们发现了15个问题,其中最重要的一个规定是:只有那些无法访问的预览页面才会被认定为“阻塞”,而其他主机即使无法访问也会被视为测试失败。在M2阶段的评审中,我们发现有些模型会通过一些毫无实际意义的断言来通过测试,因此我们制定了更严格的通过标准,并且引入了未改变的页面副本作为验证依据。在M3阶段的评审中,我们发现有时客户端会在测试过程中突然退出,导致Playwright及其关联的浏览器继续运行;此外,我们还发现某些人为创建的符号链接会导致报告工具读取项目外部的文件。所有这些问题都已经被解决,并且都已经通过相应的测试进行了验证。
使用它后发生了什么
在向别人介绍这个工具之前,我先用自己的两个项目进行了测试。Conclave这套测试工具显示有95项测试通过了;而另一个更大的项目则有137项测试通过,但其中有一项测试失败了,原因是缩放比例的数值略有偏差。
其实这个失败的测试我已经存在了,但却没有注意到它的问题,因此katalepsis工具并不是发现了这个错误,而是让这个错误变得显而易见。我的这两个项目都硬编码了baseURL值,这使得它们的测试根本无法检测到我输入的正确URL。现在katalepsis会对此发出警告,因为如果在错误的服务器上运行测试,结果反而会比不进行任何测试更糟糕。
我在LinkedIn上发布了这个信息后,有人在评论中指出,你选择的那个提交版本并不一定就是用于生成预览版本的提交版本。他们说得对,因此现在的报告会同时记录这两个版本,并标明它们是“已确认匹配”、“不匹配”还是“未知”。如果这两个版本不匹配,那么原本应该通过的测试也会失败,因为如果在过时的预览版本上进行测试,根本无法了解代码中到底发生了什么变化。
预览版本会通过x-commit-sha响应头或标签来标识其所对应的提交版本,而用于检测可用性的工具也会读取这些信息。如果你的服务器不支持这两种方式,你可以手动指定--preview-commit参数。
我正在按照这篇文章中描述的方法亲自测试这个工具。现在,“browser-review”技能会先让我的自动化脚本运行katalepsis工具进行检测,而对于那些katalepsis还无法检查的内容,比如字段值、数据持久化情况、控制台日志、网络请求结果以及不同屏幕分辨率下的布局显示,就会转而使用普通的浏览器自动化测试方法。
第一次使用这个工具的团队,在尝试查找“katalepsis技能”时,因为我在AGENTS.md文件中描述得不够清楚,所以没有找到这个技能,最终还是回到了原来的浏览器审查流程。虽然这个错误很愚蠢,但回退机制确实起到了预期的作用。后来有人明确指出,katalepsis实际上是一个命令行工具,而“browser-review”才是真正的自动化检测技能。
几天后,我在一个实际的工作分支上使用了这个工具。我修改了代码,让GeoJSON成为主要的下载选项,而CSV和KML格式则被放在了下拉菜单中。自动化脚本在开始执行任何操作之前都会先停下来,因为verify命令会将页面内容发送给模型提供者,而我的规则要求在执行这个操作之前必须得到我的确认。这正是我想要的效果。
由于使用了预设的合成数据,并且我也明确给予了许可,所以测试结果是成功的:一个可见的主要下载按钮,以及在下拉菜单打开后才会显示的另外两种格式文件,报告中还包含了截图和调试日志。这个工具在无法使用katalepsis检测的情况下,仍然会使用传统的浏览器自动化方法来处理那些包含真实数据的页面。
最终的结果
并不是所有的任务都需要使用这些工具。例如,确认Markdown格式中的拼写错误是否已经得到修复,并不需要Playwright工具;而对于那些不会改变页面渲染结果的后端代码重构,也不需要Chromium浏览器。不过,对于组件、表单、导航菜单、对话框、客户端状态、地图显示、数据过滤、响应式布局设计、数据持久化功能,以及任何用户能够看到的错误修复工作来说,这些工具确实非常有用。
我的经验法则是:如果你要对用户界面在运行时的表现提出某种说法,那么运行时的用户界面本身就应该成为证明这些说法正确性的依据之一。
这些方法并不能替代有效的测试、代码审查,也不能取代我亲自检查那些重要的变更。它们只是为现有的流程补充了一个缺失的反馈环节而已。该工具已经具备了访问代码仓库和运行脚本的功能,也清楚自己做了哪些修改;通过浏览器,它可以检测自己构建的用户界面是否真的能够正常工作;而通过生成报告,它可以将结果以可视化的形式呈现给我,而不是仅仅口头告知我。如果它能够生成一个按钮,那么它就应该能够点击这个按钮,并在完成操作之前向我展示实际发生的情况。
katalepsis工具采用MIT许可证授权,可以在GitHub上找到它的代码。如果某个自动化检查工具给出的结果让你感到怀疑,我希望你能告诉我。
相关文章
如何阻止人工智能代理生成尼日利亚的邮政编码
2026年10月1日,尼日利亚的邮政编码从六位数字改为由11个字符组成的代码,这种编码能够唯一标识某栋建筑,例如 EK-01-A03-FK-01 。 在此之前训练的语言模型仍然会自信地使用旧格式来处理邮政编码。这会影响两种类型的程序:一种是负责生成验证代码的编程程序,另一种是在运行时从客户信息中读取邮政编码或填写配送表格的程序。 本文将介绍如何解决这个问题。对于那些生成验证代码的程序,你需要检测它们目前生成的代码是否存在错误,并为它们提供相应的规则来纠正这些错误;而对于在运行时处理邮政编码的程序,则需要为它们设定明确的边界:当模型提出一个邮政编码时,由特定的程序来判断该编码是否符合规范、是否已
阅读全文
代理们在三周内重构了30万行代码,而业内人士则都在思考:这究竟证明了什么?
CodeScene发布了一项案例研究:在三周的时间里,这些编程机器人使用大约4,000美元的代币费用,重新编写了30万行C语言代码,其工作过程通过逐帧回放工具进行了验证。在这些过程中,这些机器人还建立了一套针对特定代码库的编程流程指南。不过,业内人士对这项研究的范围、评估指标,以及最终结果在多大程度上受到所使用工具的影响提出了质疑。 作者:Steef-Jan Wiggers
阅读全文
如何使用TypeScript从零开始构建一个AI代理程序
许多关于AI代理的教程都是从语言模型开始讲起,为它提供一些工具,然后让它运行循环直到产生答案。这样的做法确实足以展示这些工具的使用方法,但却没有回答那些有趣的工程学问题。 当某个工具出现故障时会发生什么?谁来验证这些操作的结果?是什么机制能防止代理无限期地运行、执行不安全的命令,或者耗尽其上下文信息?又如何在不依赖模型不可预测的响应的情况下测试这些功能呢? 这些职责都属于 代理框架 ——也就是位于语言模型与其所运行的环境之间的那层软件。 在本次教程中,我们将使用TypeScript来构建一个小型且与具体工具提供商无关的代理框架,并以编码代理作为示例进行讲解。我们会从基本的执行循环开始,逐步添加
阅读全文
如何利用人工智能提示更快地构建网站用户界面
过去,创建网站的用户界面需要花费数小时的时间来手动编写HTML、CSS代码以及各种组件代码。但现在,你只需用简单的文字描述页面的结构,就可以让人工智能编码工具生成最初的代码版本。 不过,最终生成的代码质量取决于你提供的指令。如果指令过于简短或模糊,那么生成的代码可能只是包含随机颜色和重复代码的简单页面;而如果你能提供详细的说明——包括所使用的技术栈、文件结构以及设计规则,那么工具就能生成可实际使用的代码。 在本教程中,你将学习如何编写这样的指令。通过这些指令,你可以使用Next.js、Tailwind CSS以及shadcn/ui组件来构建一个带有固定导航栏的全屏页面。 完成本教程后,你将能够
阅读全文