上个月,一个测试负责人跟我说了一件事。他们团队每周合入几十个PR,每次合入前都要求“跑一遍冒烟”。但实际操作是:QA手动打开测试环境,登录,点一遍主流程,截图,然后在PR下面回一句“冒烟通过”。这个流程有两个问题:第一,它不可复查。 三个月后回头看,没人知道那次“冒烟通过”到底点了什么、看了什么。第二,它会漏。 人重复点同一套流程,到第三十个PR的时候,注意力早就散了。
Playwright MCP给了一个不同的解法:让Agent在PR触发时,自己打开页面、自己探索、自己截图留证,最后由QA审查哪些动作值得沉淀成脚本。不是“AI替你测完”,而是“AI把冒烟变成一条可复查的工作流”。
一、先搞清楚:PR冒烟到底要让Agent做什么?
很多人一上来就想让Agent“自己判断这个PR有没有问题”。这个目标太模糊,Agent做不到,做出来也没人敢信。
PR冒烟的场景,真正适合Agent做的是三件事:
第一,探索和取证。 Agent收到一个PR,打开预览环境,按预设的主流程走一遍——登录、进入目标页面、执行关键操作。每一步截图、记录网络请求、保存页面快照。失败的地方保留完整现场。
第二,产出可审查的证据链。 不是一句“通过/失败”,而是“我点了什么、看到了什么、网络层返回了什么”。QA审查的是证据,不是结论。
第三,生成脚本草稿。 探索过程中稳定的步骤,Agent可以产出Playwright脚本草稿。但草稿不自动合入,由QA判断哪些值得进入冒烟集。
Agent负责“跑和记”,QA负责“判断和沉淀”。 这个分工是整条链路能成立的前提。
二、CI环境里跑Playwright MCP,和本地有什么不同?
本地跑MCP,浏览器是带界面的,你能看到Agent在点什么。CI环境没有显示器,必须切换到无头模式,而且需要额外的沙箱参数。
配置上有三个必须处理的地方。
第一,加--headless。 Playwright MCP默认是带界面模式,在CI的Linux runner上会直接失败。启动参数里必须显式声明无头模式。
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": ["@playwright/mcp@latest", "--headless", "--browser", "chromium"]
}
}
}
--headless也可以用环境变量PLAYWRIGHT_MCP_HEADLESS=true替代,两种方式等价。
第二,加--no-sandbox。 大多数CI容器以root用户运行,Chromium的沙箱机制会拒绝启动。Docker部署的官方示例里同样带了--no-sandbox参数。不加这个参数,你会看到一个很典型的错误:Failed to launch browser: No usable sandbox。
第三,固定版本,不要用@latest。 本地开发用@latest没问题,CI里必须锁版本。@latest在某次依赖更新后可能引入行为变化,导致昨天绿的冒烟今天莫名其妙红了。项目里放一个.mcp.json,版本写死,和代码一起提交。
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": ["@playwright/mcp@0.0.54", "--headless", "--no-sandbox", "--browser", "chromium"]
}
}
}
三、PR触发的GitHub Actions工作流怎么写?
核心逻辑是:PR创建或更新时,启动一个job,job里启动MCP Server,然后让Agent连接MCP、执行冒烟任务、上传证据。
下面是workflow的骨架,关键步骤都有注释。
name: AISmokeonPR
on:
pull_request:
types:[opened,synchronize,reopened]
jobs:
ai-smoke:
runs-on:ubuntu-latest
timeout-minutes:15
steps:
-uses:actions/checkout@v4
-uses:actions/setup-node@v4
with:
node-version:22
-name:Installdependencies
run:|
npm ci
npx playwright install chromium --with-deps
-name:StartPlaywrightMCP(headless)
run:|
npx @playwright/mcp@0.0.54 \
--headless --no-sandbox --browser chromium \
--port 8931 --host 0.0.0.0 &
npx wait-on http://localhost:8931/mcp --timeout 30000
-name:RunAIsmokeagent
env:
BASE_URL:${
{secrets.PR_PREVIEW_URL}}
OPENAI_API_KEY:${
{secrets.OPENAI_API_KEY}}
run:nodescripts/run-smoke-agent.js
-name:Uploadevidence
if:always()
uses:actions/upload-artifact@v4
with:
name:smoke-evidence
path:evidence/
retention-days:7
这里有几个设计决策值得单独说。
为什么用--port 8931而不是STDIO? STDIO模式适合本地客户端直接拉起Server,但在CI里,Agent脚本和MCP Server是两个独立进程。用HTTP transport把Server跑成长期服务,Agent通过http://localhost:8931/mcp连接,进程管理更清晰。
wait-on那一步不能省。 MCP Server启动需要时间,如果Agent在Server就绪之前就发起连接,会拿到ECONNREFUSED。wait-on等的是HTTP端点可访问,比固定sleep可靠。
if: always()上传证据。 冒烟失败的时候,截图和网络日志恰恰是最有价值的东西。如果只在成功时上传,失败现场就丢了。
四、Agent脚本里到底做什么?
scripts/run-smoke-agent.js不需要很复杂。它的职责是:连上MCP Server,按预设的冒烟目标执行,收集证据,退出时给出机器可读的结果。
核心逻辑是这样的:
const { Client } = require("@modelcontextprotocol/sdk/client/index.js");
const { StreamableHTTPClientTransport } = require("@modelcontextprotocol/sdk/client/streamableHttp.js");
asyncfunction main() {
const transport = new StreamableHTTPClientTransport(
new URL("http://localhost:8931/mcp")
);
const client = new Client({ name: "smoke-agent", version: "1.0" }, { capabilities: {} });
await client.connect(transport);
const baseUrl = process.env.BASE_URL;
const evidence = { steps: [], failed: false };
// Step 1: 导航
await client.callTool({ name: "browser_navigate", arguments: { url: ${baseUrl}/login } });
// Step 2: 快照 —— Agent先看页面,再决定操作
const snap = await client.callTool({ name: "browser_snapshot", arguments: {} });
evidence.steps.push({ phase: "snapshot_login", snapshot: snap.content });
// Step 3: 根据快照中的ref执行操作(示例:填登录表单)
// 实际项目中,ref从snapshot返回的结构化文本中解析
await client.callTool({
name: "browser_type",
arguments: { target: "e5", text: process.env.SMOKE_USER, submit: false }
});
await client.callTool({
name: "browser_type",
arguments: { target: "e6", text: process.env.SMOKE_PASS, submit: true }
});
// Step 4: 验证关键断言
const verify = await client.callTool({
name: "browser_verify_text_visible",
arguments: { text: "Dashboard" }
});
if (verify.isError) evidence.failed = true;
// Step 5: 落盘证据
require("fs").writeFileSync("evidence/result.json", JSON.stringify(evidence, null, 2));
await client.close();
process.exit(evidence.failed ? 1 : 0);
}
main().catch(err => {
console.error(err);
process.exit(1);
});
这段代码里最重要的设计是Step 2和Step 3之间的顺序:先browser_snapshot,再基于快照返回的ref执行操作。如果跳过快照、直接猜CSS选择器,脚本只能在当前页面结构下跑通,页面一改就废。
还有一个容易被忽略的细节:ref只在当前快照内有效。 每次导航或页面重新渲染后,必须重新browser_snapshot获取新的引用。Agent脚本里如果复用过期ref,会表现为“点错元素”或Element not found。
五、断言分三层,别让“工具成功”冒充“业务通过”
这是PR冒烟最危险的一层。
browser_verify_text_visible返回成功,只证明页面上的文字可见。它不证明接口返回了200,不证明订单真的创建了,不证明状态机流转正确。前端乐观更新、接口500但Toast提前弹出、页面显示了“成功”但数据库里什么都没有——这些“假通过”在手动冒烟里靠人的经验能兜住,在Agent脚本里必须用显式断言兜住。
PR冒烟的断言至少分三层:
第一层:工具层。 MCP工具调用没有返回isError。
第二层:页面层。 关键元素可见、关键文案出现。browser_verify_text_visible属于这一层。
第三层:业务层。 这是Agent脚本里最需要显式设计的一层。比如捕获关键网络请求并校验状态码,或者调用一个业务查询接口确认最终状态。如果冒烟目标涉及支付、订单、库存,第三层不能省。 没有第三层的冒烟,只是在验证“页面画出来了”,不是在验证“功能没坏”。
六、Agent跑出来的脚本草稿,要不要直接合入?
Jiten20的Agentic QA框架里有一个设计值得借鉴:Planner用MCP做浏览器探索,产出基于证据的测试计划;Generator只读取“经过QA审批的”测试场景,才生成Playwright代码;Healer分析失败,但修复方案需要人工批准才能合入。
这套流程的核心是人工审批门禁。Agent在PR里跑出来的冒烟结果和脚本草稿,不应该自动变成正式冒烟集的一部分。QA需要审查的是:这个步骤在业务上是不是关键路径?这个断言是不是太弱?这个选择器在未来UI改版时会不会碎?
Agent的价值是“把现场带回来”,不是“替你做决定”。
七、避坑指南
坑一:CI里忘了--headless。 带界面模式在无显示器的runner上直接失败,报错信息不一定直观。无头模式同时能省掉约200MB内存,对CI runner更友好。
坑二:用了@latest。 CI里版本必须锁死。@latest会在某次上游更新后让行为漂移,而且漂移的那一天,你正在排查一个和依赖更新无关的业务Bug。
坑三:跳过browser_snapshot直接操作。 Agent靠猜CSS选择器写的脚本,只能在当前构建上跑通。先observe再act,是Playwright MCP工作流的基础约束。
坑四:只断言页面文案。 页面说“成功”不等于业务成功。至少加一层网络请求校验,涉及资金和状态的冒烟必须加业务层断言。
坑五:让Agent自动合入脚本。 Agent生成的脚本草稿是“线索”,不是“资产”。草稿进正式冒烟集之前,必须有人判断它值不值得留下。
最后
PR冒烟这件事,过去的核心矛盾不是“跑得慢”,是“跑得不可复查” 。手动点一遍,截图存在某个人电脑里,结论写在PR评论里,三个月后什么都追溯不到。
Playwright MCP在CI/CD里的真正价值,不是让Agent“替人点按钮”,是把冒烟从一次性的手动动作,变成一条有快照、有截图、有网络日志、可回放、可审查的工作流。
Agent做机器的活:打开、操作、取证、落盘。人做人的活:判断哪些证据是有效的、哪些步骤值得沉淀、这个PR到底能不能合。
这条线画清楚了,PR冒烟才真正从“走过场”变成“有据可查”。
本文部分内容参考了霍格沃兹测试开发学社整理的相关技术资料,主要涉及软件测试、自动化测试、测试开发及 AI 测试等内容,侧重测试实践、工具应用与工程经验整理。