测试前先侦察:让 Playwright 自动化真正可靠的决策树
很多人写 Playwright 脚本的方式是:打开文档,照着示例把 page.click('#submit') 填进去,跑一下,报错,改 selector,再跑,再报错——直到跑通。这个过程有点像蒙着眼睛开车,大方向没错,但每一步都在猜。
我后来总结出一个更可靠的框架,核心只有两点:先侦察,再行动;把复杂的服务器管理逻辑包进黑盒,不要让它污染主脚本。
决策树:在写第一行代码前先定位
不同的页面类型,需要截然不同的处理策略。在写任何 Playwright 代码之前,先回答两个问题:
第一问:这是静态 HTML 还是动态 Web 应用?
静态 HTML(本地文件、无 JS 渲染)可以直接读文件拿 selector,然后用 file:// URL 打开。这是最简单的路径,省去了起服务器的一切麻烦。
动态应用则需要真实的浏览器渲染才能看到最终 DOM,file:// 不够用,必须起服务器。
第二问:服务器是否已经在跑?
已在跑,直接指向端口即可,脚本只管自动化逻辑。
没在跑,就要考虑服务器生命周期管理——这是最容易把脚本搞乱的部分。
这张决策树把混在一起的问题分开了:「我该怎么访问」和「我该怎么操作」是两件事,不要混写。
侦察阶段:先截图,再找 selector
动态应用真正的坑是:你以为的 DOM 结构和渲染出来的不一样。JS 延迟挂载、骨架屏替换、异步数据填充——凡是没等 JS 执行完就去找 selector,就是在抓幻影。
networkidle 是最粗暴也最有效的等待策略:
page.goto('http://localhost:5173')
page.wait_for_load_state('networkidle') # 等网络空闲,JS 已执行
page.screenshot(path='/tmp/inspect.png', full_page=True)
content = page.content()先截图,看一眼当前真实渲染状态;再从 content() 或 locator().all() 里识别 selector。这个「先看,再动手」的习惯,能省掉大量反复调试的时间。
侦察阶段的目的不是验证功能,是建立「当前渲染状态的地图」。地图准了,后续的每一步操作才有坐标。
脚本黑盒原则:不要把工具逻辑吃进上下文
这是我在 AI agent 辅助开发里发现的一个反模式:当 agent 需要自动化测试时,它会忍不住先把 with_server.py 读进来,研究实现细节,然后再照着写一遍。结果就是:工具脚本的几百行实现细节全部进了上下文窗口,token 烧了,推理也被噪音干扰。
正确做法是把助手脚本当黑盒对待:--help 先跑一下,看接口,直接调用:
# 单服务器
python scripts/with_server.py --server "npm run dev" --port 5173 -- python your_test.py
# 前后端分离
python scripts/with_server.py \
--server "cd backend && python server.py" --port 3000 \
--server "cd frontend && npm run dev" --port 5173 \
-- python your_test.py主测试脚本只写业务逻辑,服务器的启动、健康检查、关闭全部委托给助手脚本处理。两件事分离,各自保持简单。
这个原则在 AI agent 自动化场景里尤其重要——agent 的上下文窗口是稀缺资源,能用接口解决的,不要用阅读源码解决。
取舍与边界
这套框架的适用范围主要是本地开发和 CI 测试,以及 AI agent 驱动的页面自动化探索。
它不适合替代专门的 E2E 框架设计——selector 稳定性、用例隔离、flaky 治理这些是另一套问题(我在另一篇笔记里单独聊过)。这里解决的是更前置的问题:在不确定目标页面结构的情况下,怎么让第一步操作落地,而不是碰运气。
一句话心法
自动化脚本可靠的前提,是在操作之前先知道自己面对的是什么——侦察不是可选步骤,是必须步骤。