🤖 Claude Code × 🎭 Stagehand × Playwright

Semantic E2E tests with stable, self-healing selectors 稳定、自愈 的选择器写语义化 E2E 测试

A custom Stagehand LLMClient that uses Claude Code to turn plain-language actions into stable, generalizable CSS selectors — cached to Git for zero-cost, deterministic CI replay. 一个由 Claude Code 驱动的 Stagehand 自定义 LLMClient:把自然语言操作转成稳定、可泛化的 CSS 选择器,并缓存进 Git,实现零成本、确定性的 CI 回放。

npm version node >= 18 MIT license 59 tests passing
The problem问题

Default LLM selectors are fragile默认 LLM 生成的选择器很脆弱

Stagehand's default model often emits XPath or hashed CSS classes that break on every frontend refactor. This package fixes selector quality, cost, and resilience at once. Stagehand 的默认模型常生成 XPath 或带 hash 的 CSS class,前端每次重构都会大面积失效。本包一次性解决选择器的质量、成本与韧性。

🎯

Better selectors更优的选择器

Claude Code + your skill docs prefer data-testid, aria-label and semantic tags that survive refactors.Claude Code + 你的 skill 文档,优先使用 data-testidaria-label 与语义标签,扛得住重构。

Zero-cost CICI 零成本

Selectors are cached on first run and committed to Git. CI is pure deterministic CDP replay — 0 LLM calls, $0.选择器首次运行即缓存并提交到 Git。CI 为纯确定性 CDP 回放 —— 零 LLM 调用、零成本。

🩹

Self-healing自愈能力

When a cached selector breaks, Claude Code regenerates it and can create a fix commit automatically.缓存选择器失效时,Claude Code 自动重新生成,并可自动创建修复 commit。

How it works工作原理

From plain language to a stable selector从自然语言到稳定选择器

You write a semantic action. Claude Code, guided by your e2e-skills/CLAUDE.md, generates a generalized selector and caches it. The example below is the real output captured from the MDN demo run. 你只写语义化操作。Claude Code 依据你的 e2e-skills/CLAUDE.md 生成泛化选择器并缓存。下面是 MDN 演示运行中真实捕获的输出。

You write a semantic action你写语义化操作

await stagehand.act("点击第一个博客卡片的 Read more 按钮", { page })

Claude Code generates & generalizes a selectorClaude Code 生成并泛化选择器

The skill doc steers it away from XPath toward short, semantic CSS.skill 文档引导它避开 XPath,产出简短的语义化 CSS。

The selector is cached + tracked in a manifest选择器被缓存并写入 manifest

Next run hits the cache — deterministic CDP replay, no LLM.下次运行直接命中缓存 —— 确定性 CDP 回放,无需 LLM。

Default LLM: fragile XPath默认 LLM:脆弱的 XPath breaks on refactor
xpath=/html[1]/body[1]/div[2]/div[1]/div[1]/section[1]/article[1]/footer[1]/a[1]
This package: generalized CSS本包:泛化的 CSS survives refactor
article:first-of-type footer a
Live Playwright demoPlaywright 真实演示

Real screenshots from a real run来自真实运行的截图

These are unedited screenshots captured by examples/mdn-blog/site-capture.spec.ts driving the live MDN Blog with Stagehand + Claude Code + Playwright over CDP. 以下为 examples/mdn-blog/site-capture.spec.ts 通过 CDP 驱动真实 MDN 博客 时,由 Stagehand + Claude Code + Playwright 截取的未经修饰的截图。

https://developer.mozilla.org/en-US/blog/
MDN blog list page driven by the test, showing blog cards
Step 1 extract() reads all blog-card titles from the a11y tree into a typed array. extract() 从可访问性树中读出所有博客卡片标题,返回类型化数组。
https://developer.mozilla.org/en-US/blog/.../
MDN article detail page after the semantic click navigated to the article
Step 2 act("click Read more") used the cached selector to click and navigate to the article. act("点击 Read more") 命中缓存选择器,完成点击并跳转到文章详情页。

The extract() above returned real, structured data: 上面的 extract() 返回了真实的结构化数据:

Introducing the MDN MCP server Under the hood of MDN's new frontend Image formats: Codecs and compression tools A beginner-friendly guide to view transitions in CSS Launching MDN's new front end Image formats: Pixel data …

…and the cached selector was recorded in .stagehand-cache/manifest.json: ……而缓存的选择器被记录进 .stagehand-cache/manifest.json

{
  "instruction": "点击第一个博客卡片的 Read more 按钮",
  "selector":    "article:first-of-type footer a",
  "url":         "https://developer.mozilla.org/en-US/blog/",
  "status":      "active",
  "generalized": true,
  "ttlSeconds":  2592000   // 30 days
}
Quick start快速开始

Up and running in a minute一分钟跑起来

Requires Node ≥ 18, the claude CLI in PATH, and @browserbasehq/stagehand ≥ 3.6. 需要 Node ≥ 18、PATH 中可用的 claude CLI,以及 @browserbasehq/stagehand ≥ 3.6。

# 1. install
npm install @tengxiaohtx/stagehand-cc-agent
// 2. use Claude Code as Stagehand's LLM engine
import { Stagehand } from "@browserbasehq/stagehand";
import { createClaudeCodeLLMClient } from "@tengxiaohtx/stagehand-cc-agent";

const stagehand = new Stagehand({
  env: "LOCAL",
  llmClient: createClaudeCodeLLMClient({
    cwd: "./e2e-skills",        // where your CLAUDE.md skill lives
    systemPromptEnhancement: "Prefer data-testid, then aria-label, then semantic tags.",
  }),
  cacheDir: "./.stagehand-cache",   // commit this to Git for zero-cost CI
});

await stagehand.init();
const page = stagehand.context.pages()[0];
await page.goto("https://app.example.com/login");

// semantic actions — no selectors written by hand
await stagehand.act("enter username test@example.com");
await stagehand.act("click the login button");

await stagehand.close();

Guide selector generation by dropping a CLAUDE.md skill file in your cwd: cwd 中放一个 CLAUDE.md skill 文件来指导选择器生成:

# e2e-skills/CLAUDE.md — selector priority ladder
1. [data-testid="..."]          # most stable
2. [aria-label] / [role] + name
3. semantic tags: article, nav, footer, h1–h3, time
4. stable BEM class (no hashes)
5. :first-of-type / :nth-of-type(n)   # positional fallback
CI integrationCI 集成

Deterministic, zero-cost CI replay确定性、零成本的 CI 回放

Commit .stagehand-cache/ to Git. In CI, runs hit the cache exclusively — no Claude Code calls, no cost, millisecond-level execution. .stagehand-cache/ 提交到 Git。CI 中运行完全命中缓存 —— 零 Claude Code 调用、零成本、毫秒级执行。

# .github/workflows/e2e.yml
- run: npx playwright install --with-deps chromium
- run: npx playwright test
  env: { CI: "true" }   # cache hit → no LLM, $0