Industry & PracticeTechniques & Tutorials
浏览 器 测试 不该 再看 图 点 坐标
Jev Browser 先给页面控件编号,模型只能在这份清单里做选择题。模型说完成不算通过,要通过还得看见页面上的证据。
In this piece
浏览器测试不该再看图点坐标
AI 把功能写完之后,真正难的不是再生成一段测试代码,而是合入前回答一个更窄的问题:这个界面有没有走到它声称的结果。
很多浏览器 Agent 的答案是把整页截图送给视觉模型,让它返回坐标或选择器,再点下去。这条回路有三笔固定成本。每一拍都要付视觉 token;布局一变,坐标就漂;模型一旦能写出 JavaScript、XPath 或 shell,执行器就不再是测试器,而是一个带着浏览器的代码解释器。
CodexQA 里的 codexqa-jev-browser(下文称 Jev Browser)换了一条约束更硬的路:页面先被收成编号索引,模型只能在这份索引上做选择题,动作打在节点上,通过与否看页面上还在不在的证据。它不是视觉 GUI Agent 的加速版,而是把「找控件」从模型侧挪回浏览器侧。
仓库自己写明:这里没有和视觉 GUI 模型的对照基准。网上能看到的「快几十倍、便宜几百倍」,来自 TypeSafe 对自己 System One / Jev 决策接口的公布数字,只覆盖决策调用,不包括打开页面和写报告。下文不把这些数字当成 CodexQA 的实测。
它在整条检查里站哪
CodexQA 是一组本地 Agent Skill,面向「代码已经写出来,合入前要不要信」这一段。需求缺口、用例、造数、架构图、变更影响面、缺陷扫描、评审页,各自是独立技能。Jev Browser 是闭环的最后一截:用例和数据有了之后,看界面是不是真走到了说好的证据。
技能目录的 SKILL.md 写了一句边界,值得先记住:它不是 browser-use/jev-ultrafast 的分支。决策可以走 TypeSafe 的 Jev /systemone,也可以退到兼容 OpenAI 的 chat/completions,还可以用一份写死的决策脚本把模型整个拿掉。宿主里的 Cursor / Claude / Codex 会话不是决策模型。CLI 自己发请求。
运行时是 Node.js 20+ 和 Playwright Chromium,默认有头。observe、run、explore 和 --decisions 不调用模型。只有现场的 auto 和 generate --goal 才需要 TYPESAFE_API_KEY 或 OPENAI_API_KEY。
先观测,再允许模型开口
观测脚本跑在页面里,入口是 collectSnapshot。它把 snapshot.dom.js 注进 Playwright 页面,默认最多收 250 个可见控件。
收集范围不是「所有 DOM」。选择器覆盖链接、按钮、输入、下拉、以及一组 ARIA 角色:button、link、textbox、searchbox、combobox、option、tab、switch 等。同一套遍历会走进 Shadow DOM。同源 iframe 用 contentDocument 再收一遍,并带上 frame 名;跨域 iframe 直接跳过,因为读不到文档。
可见性是几何加样式,不是「在 DOM 里」。display/visibility、hidden、aria-hidden、inert、零尺寸、视口外,都不会进索引。tabindex="-1" 的 input/textarea 也丢掉,避免把屏幕阅读器或框架藏起来的影子输入交给模型。
名字按无障碍顺序取:aria-labelledby、aria-label、label[for]、包住它的 label、图片 alt、可见文本,截到 80 字。输入框还会在上方一条窄带里找短标题,解决「框本身没名字、字在上面」这种表单。
有三类控件不是标准控件,脚本专门补过:
- 下拉、日历、建议层里的叶子节点。日期格会尽量拼上「2026年9月」这种月份标题,否则模型只能看见一个「15」。
- 搜索框末端的图标。没有文字的 svg/img,如果贴在搜索框右缘,会被收成一个名叫「搜索」的按钮。
- 密码以及名字像密钥的字段。值在出页面前就被清空,后续历史里也不会把敲进去的字发给模型。
每个元素有两个号,不要混。node 是这次浏览器会话里的稳定 id,写进 data-codexqa-jev-browser-id,用 WeakMap 记着,元素还在 DOM 里就不会变。index 是这一拍快照里的序号,从 1 重排,只活在这一次观测。模型看到的是 index。执行器点的是 node。
点之前还有一次 targetReady:重新确认节点还连着、没被挡住、elementFromPoint 打中的是它自己、iframe 的框也没被盖住。对不上就抛 StalePage,这一步不点。失败后 Agent 会再观测一次,按角色和名字把目标绑回去,绑不上就停。这是在对抗「决策发出去的那一拍,页面已经变了」。
动作空间是封闭的
buildSpace 只根据观测结果打开操作:有可点的才有 CLICK,有可输入的才有 TYPE,有原生 <select> 选项才有 SELECT。SCROLL_UP、SCROLL_DOWN、WAIT、DONE、BLOCKED 始终在菜单里。
模型回复进执行器之前过 validateDecision。操作必须在菜单里,目标 index 必须还在对应池子里。整段 JSON 里一旦出现 document.、javascript:、xpath、querySelector,直接拒绝,什么都不执行。页面文本在提示里被标成不可信数据,不能反过来当指令。
最近几步还会改菜单,而不是只靠提示词喊「别重复」:
- 刚输入过、值已经落在框里的字段,从可输入池里拿掉。
- 组合框刚输入完、选项层还开着,就暂时禁止再往别的输入框打字,也禁止再点开这个框本身。该点的是选项。
所以「点哪个」不是自由文本规划,是在一份当页清单里选题。清单变了,上一拍的 index 作废。
Jev 做的是一次多问,不是一段推理散文
配了 TYPESAFE_API_KEY 时,决策走 POST {TYPESAFE_BASE_URL}/systemone,默认模型名 jev-latest。一次请求里带上页面 URL、标题、最多 80 个控件的角色/名字/当前值/允许操作,以及最近 10 步。问题是并列的选择题:
| 问题 | 选项从哪来 |
|---|---|
operation | 当前动作空间 |
click_target / type_target / select_target | 对应池子里的 index |
text_<index> | 目标文本里已经写出的短语,最多 12 条,每条不超过 80 字;只挂到前 8 个可输入框上 |
输入什么字,Jev 不能现编。typeCandidates 只从目标里抠引号短语、搜索/输入/填写 后面的那一段,以及日期。搜索框还可以不经模型,直接用 searchQueryFromGoal 从目标里取出查询词。没有可选项、决策里也没有 text 时,这一步记为 skip,不打字。连续三次拿不到字,整段失败。
没有 Jev key 时,同一份动作空间走 chat/completions,temperature: 0,要求 JSON。打开浏览器之前的任务规划也走这条对话接口:拆步骤,并写下一句 doneWhen。规划发生在 goto 之前,避免页面干等。
第三条路是 --decisions。YAML/JSON 里按顺序写死操作和语义目标,ScriptedProvider 在当前页上把 {role, name} 解析成 index。这条路径不规划、不调用模型,用来把引擎和夹具跑通。
优先级是:决策脚本,然后 Jev,最后对话模型。
模型说做完了,不算通过
这是这套设计和普通 Agent 差得最大的地方。
DONE 只是一个候选操作。Agent 收到之后,还要用规划器写下的 doneWhen 再问一次:条件在这一页上是否已经可见。有 Jev 时走 jevConfirmDone,仍然是 /systemone 的 met / unmet 二选一,依据是 URL、标题、可见文本和控件,不要求条件原文逐字出现。没有 Jev 时走对话模型的 confirmDone。条件不成立,这一步失败,理由是 done condition is not visible。
单步有没有生效,是另一道门。用 Jev 跑 auto 时,动作之后会再调一次 jevAssert:把操作、输入值、动作前控件表、动作后控件表交给同一个选择题接口,只许答 pass 或 fail,并且明确「不要判断整个任务是否结束」。这次调用失败会被吃掉,退回本地规则 assessActionEffect:
type/select:期望文本是否出现在页面文本、标题、URL 或控件值里;- 点击和滚动:URL、标题或控件签名是否变化;
wait:直接算过。
本地规则判失败,或 Jev 断言判失败,步骤状态就是 fail,错误文案是 action had no visible effect。页面连续 3 次没有变化、连续 3 次 WAIT、连续 3 次陈旧重试,都会停。停之前如果 doneWhen 已经可见,仍可以记通过。步数默认有上限,走到顶还没出现 done 步骤,状态是 blocked,不是 pass。
回放模式更硬,不靠模型眼色。YAML 里的 assert 由 assertStep 做字符串比对:url_includes、url_equals、title_includes、visible_text、hidden_text,以及上一步 HTTP 的状态码和 JSONPath。断言没有写任何检查,这一步直接失败。断言失败后 teardown 仍会执行。
仓库写明:模型返回的 DONE 不是成功。离线 npm test 只说明引擎和夹具一致,不能说明真实站点或现场模型会成功。
四条命令,其实是一条数据流
observe → 编号快照
run → 回放 YAML / Markdown / API 用例
auto → 按自然语言目标走,不落用例
generate → 边走边把成功步骤编译成 YAML,可选 --verify 再跑一遍
explore → 不调模型,点可见控件,画出状态图回放用例的稳定形态是语义目标,不是选择器,也不是坐标:
- op: type
target: {role: searchbox, name: 搜索}
value: Pilot
- op: click
target: {role: link, name: Pilot 入门}
- op: assert
url_includes: docs.html
title_includes: Pilot 入门
visible_text: 入门这是仓库自带夹具 cases/examples/search-docs.yaml,对着 examples/app/index.html。${PASSWORD} 这类占位从环境变量展开,密钥不应该写进用例。op: http 可以在同一次运行里做接口准备或后端核对,并用 JSONPath 把返回值存下来给后面的步骤。
generate 不让人一边看页面一边手写步骤。每步成功后编译:点击、输入、选择保留语义目标;URL 变了就插入一条 url_includes 断言;结束步如果有标题,补一条 title_includes。--verify 再用 run 回放这份文件。编译会丢掉 skip、fail、blocked,也会丢掉纯 wait。所以生成物是「走过的成功轨迹」,不是「模型想过的全部计划」。
explore 不调用模型。它用角色、名字、值的哈希当状态键,默认最多 12 个状态、30 步、3 分钟。退出、删除、支付、结账、密码,中英文都在默认拒绝表里,除非 --allow 点名。产物是一张状态图,外加按点击轨迹编译出来的用例。这是覆盖侦察,不是业务验收。
每次运行写 reports/<run-id>/report.html,另有 report.json 和 report.md。HTML 里留步骤、带红框标记的截图、耗时和 token。截图是给人看的证据,不参与决策。用例失败时进程非零退出。
知识库比选择器更决定成功率
Jev 不知道百度结果里哪条是广告,也不知道登录弹窗出现时该停。这些写在 knowledge/<应用>/*.md,不是写回 src/policy.ts。
检索规则很简单。general: true 的笔记每次都带上,例如「出现登录弹窗就选 BLOCKED,除非目标明确要求登录」。其余笔记按启动 URL 的 host 加 2 分,目标或 URL 里命中的 keywords 逐条加分,取得分最高的最多 3 篇,拼进规划、决策和填字上下文。
笔记是参考,不是新操作。模型仍然只能选当前页上观测到的 index。站点特有的规则应该加一篇笔记,而不是改执行器。现场失败时,仓库的建议也是先看报告,写清该点哪个、不该点哪个,再补笔记,而不是先去造一个 CSS 选择器。
用之前要接受的边界
- 技能自己标的是实验性,目录版本
0.1.0。仓库许可证是 Apache-2.0;技能package.json写的是 MIT。以仓库根目录的LICENSE为准,两处不一致时不要猜。 - 跨域 iframe、文件选择框(
input[type=file]在角色识别里直接返回空)、验证码、强风控页面,观测阶段就进不来。索引里没有的控件,模型无法「看见图所以点一下」。 - 有头、同源、控件带可访问名字的页面,这套索引才站得住。名字来自占位热搜、图标按钮没有 alt、日历只剩一个数字,决策质量会先掉在观测上,而不是掉在模型上。
jevAssert和jevConfirmDone仍然是模型判断,只是输入从截图换成了控件表。它们回答的是「这份结构化页面是否呈现了效果」,不是形式化证明。调用失败时,单步效果会静默退回本地字符串规则。- 探索和自动模式在退出、删除、支付上默认要人确认。拒绝表是关键词,不是权限系统。
- 代理只用你自己设的
HTTPS_PROXY,CLI 不探测本机端口。不要把 key 写进聊天、用例或提交.env。
什么时候值得用
值得用的场合很具体:你已经有自然语言目标或一份用例,想在真实 Chromium 里留下可回放的 YAML,并且接受「控件必须能被编号」。回归应优先 run 已有用例,让断言而不是模型宣布结果。generate --verify 适合把一次走通固化下来。auto 适合还没有用例、但目标写得出「做完时页面上必须看见什么」。
不值得用的场合也同样具体:你需要视觉上核对像素、画布、验证码,或者页面根本不暴露可访问名字。那种任务继续用视觉模型或人工,比把 Jev 的选择题接口假装成眼睛更诚实。
CodexQA 把浏览器测试从「模型看着截图发挥」收成三件可以分开审计的事:这一拍页面上有哪些控件,模型在其中选了哪一个,选完之后证据还在不在。快不快是决策服务的事。这套引擎真正改掉的,是失败时你还能不能指出是观测漏了、选题错了,还是证据没出现。
参考
- 仓库:<https://github.com/openqa-cn/codexqa>,技能目录
skills/codexqa-jev-browser。本文核对的是 2026-09-24 的main公开树。 - 人读说明:该目录下的
README.zh-CN.md、HOW_IT_WORKS.zh-CN.md、KNOWN_LIMITATIONS.zh-CN.md、references/schema.md。 - 实现:
src/observe/snapshot.dom.js、src/policy.ts、src/jev.ts、src/agent.ts、src/act.ts、src/verify.ts、src/generate.ts、src/explore.ts、src/knowledge.ts。 - TypeSafe 公布的决策速度和价格,见 <https://typesafe.ai/> 与 <https://typesafe.ai/blog/introducing-system-one-models-and-jev>。那组数字不是本仓库的对照实验。
Found it useful? Pass it on
Scan with WeChat to open it on your phone and forward it.