关于 browser
browser 是用于网页检查与自动化测试的技能,按任务在 agent-browser 和 Playwright 脚本间选择。它支持持久浏览器会话、页面结构检查及复合流程自动化。
主要功能
- 01浏览页面并执行交互
- 02读取 ARIA 页面结构
- 03比较操作前后的页面状态
- 04运行 Playwright 复合自动化
- 05保存并恢复浏览器会话
适用场景
- 01检查网页元素和表单
- 02验证交互后的页面变化
- 03跨视口生成响应式截图
- 04自动化多步骤网页测试
MCP 相关
该技能提及 Playwright MCP Bridge 扩展,可连接本地 Chrome 会话;没有说明其提供 MCP 服务。
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ inkeep/ team-skills --skill browser安装后在 Claude Code 中输入/browser确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
.env.exampleAPI_REFERENCE.mdREADME.mdpackage-lock.jsonpackage.jsonrun.jslib/helpers.jslocal-browser.jssession.jsreferences/cdp-tracing.mdlocal-browser.mdscripts/connect-local.jssession-server.js详细介绍
浏览器自动化
两种引擎:agent-browser(交互式工作的默认选择)和 Playwright scripts(用于复合操作/程序化操作)。具体使用哪种引擎,取决于你要完成的任务。
引擎选择
Agent-browser(交互式工作的默认选择)
需要以下操作时,请使用 agent-browser:
- 导航到 URL 并阅读/检查页面
- 截取屏幕截图(包括带注释的截图、整页截图)
- 点击、填写、选择表单元素
- 读取页面结构(带元素引用的 ARIA 快照)
- 比较操作前后的状态(
diff snapshot、diff screenshot) - 从元素中提取文本
- 基本的 Cookie/存储读取
- 快速视觉检查(“这个页面看起来对吗?”)
- 设备模拟、视口更改
- 生成 PDF
身份验证模式:
| 模式 | 命令 | 使用场景 |
|---|---|---|
| 无身份验证(全新会话) | agent-browser open <url> | 公共页面、开发服务器、CI/CD |
| 配置文件副本 | --profile Default | 快速访问用户的已登录状态(启动后可能过期) |
| 状态文件 | --state ./auth.json 或 state save/load | 跨会话持久保存身份验证状态,便于共享 |
| 会话名称 | --session-name myapp | 为重复任务自动保存/恢复会话 |
| 身份验证保管库 | auth save/login | 加密凭据存储 |
| 自动连接 | --auto-connect | 连接到正在运行的 Chrome 会话(需要 --remote-debugging-port=9222) |
可见性模式:
| 模式 | 方式 | 使用场景 |
|---|---|---|
| 无头模式(默认) | 默认行为 | CI/CD、自动化运行、后台任务 |
| 有头模式 | --headed | 用户希望观看、调试、手动介入(2FA/CAPTCHA) |
| 仪表板 | agent-browser dashboard | 无需显示窗口即可监控,支持实时流式传输 |
Playwright 脚本(用于复合操作和受能力限制的操作)
在以下两类操作中,请使用 Playwright 脚本:
A. 复合编排(始终使用 Playwright——架构上的必要选择):
- 在多步流程期间捕获控制台错误/警告(导航前注册事件监听器)
- 在特定操作期间捕获网络请求(实时事件筛选)
- 录制浏览器会话视频
- 运行无障碍审计(注入 axe-core + 生成结构化 WCAG 结果)
- 在多个断点下截取响应式屏幕截图(视口扫描)
- 根据屏幕截图序列生成 GIF
- 使用自定义状态码、标头和条件路由拦截/模拟网络请求
- 在流程中途将会话从无头模式切换到有头模式(
handoff()) - 在捕获前运行脚本(screengrabs 管道)
- 为覆盖层/模态框注册自动关闭处理程序(
addLocatorHandler)
B. 能力缺口(目前使用 Playwright;agent-browser 支持后可能迁移):
- 穿透 Shadow DOM 与 Web 组件交互
- 使用传递给函数的参数执行复杂 JS(
page.evaluate(fn, args)) - 完整的 iframe 上下文隔离(agent-browser 支持基本的 iframe 交互,但 eval/截图不会限定在 frame 内)
- 模拟慢速网络条件(agent-browser 仅支持离线模式,不支持延迟/带宽设置)
- 捕获 Web Vitals / Navigation Timing 性能指标
迁移关注列表: 关注 agent-browser 的版本更新,留意以下功能:Shadow DOM 选择器、支持传递参数的 eval、iframe 上下文范围控制、网络限速。支持后,请更新此路由树。
Playwright 脚本的身份验证模式:
| 模式 | 方式 | 使用场景 |
|---|---|---|
| 无身份验证(全新会话) | 默认 node run.js | 公共页面、开发服务器 |
| 状态文件 | helpers.loadAuthState(browser, authPath) | 从预先保存的 storageState 恢复 |
| 连接到 Chrome | node run.js --connect | 通过 MCP Bridge 扩展连接到实时会话(仅限开发机器) |
| 会话服务 | node run.js --session start | 在多次脚本调用之间保持浏览器会话 |
Agent-browser 快速参考
核心工作流——每次与 agent-browser 交互都遵循此模式:
# 1. Navigate
agent-browser open https://example.com
# 2. Snapshot — get element refs (@e1, @e2, ...)
agent-browser snapshot -i
# 3. Interact using refs
agent-browser fill @e1 "[email protected]"
agent-browser click @e3
# 4. Re-snapshot after DOM changes
agent-browser snapshot -i
批量执行——在一次调用中串联多个命令:
agent-browser batch "fill @e1 [email protected]" "fill @e2 password123" "click @e3"
差异比较——比较前后状态:
agent-browser snapshot -i # take baseline
# ... perform action ...
agent-browser diff snapshot # text diff of ARIA tree (+ additions, - removals)
agent-browser diff screenshot # pixel diff with mismatch %
完整命令参考(80 多个命令、身份验证模式、批量工作流、模板),请加载 agent-browser 技能(由 agent-browser Claude Code 插件提供——需要在设置时运行 claude plugin install agent-browser@agent-browser)。
Playwright 脚本——复合操作
重要——路径解析:
此技能通过插件系统安装。执行任何命令前,请根据加载此 SKILL.md 文件的位置确定技能目录,并在下方所有命令中使用该路径。将 $SKILL_DIR 替换为实际发现的路径。
预期插件路径:~/.claude/plugins/marketplaces/inkeep-team-skills/plugins/eng/skills/browser
Playwright 工作流
使用 Playwright 脚本进行通用浏览器自动化。针对任何复合自动化任务编写自定义 Playwright 代码,并通过通用执行器运行。
关键工作流——按顺序执行以下步骤:
-
启动会话——如果预计要运行多个脚本(调试、迭代、多步骤流程),请先启动持久浏览器会话。这是所有交互式工作的默认模式:
cd $SKILL_DIR && node run.js --session start脚本会自动检测会话,并通过 WebSocket(约 50 毫秒)连接,而不是启动新浏览器(约 2–3 秒)。登录状态、Cookie 和 localStorage 会在多次运行之间保留。只有在真正的一次性脚本或 CI/CD 环境中才跳过此步骤。
-
自动检测开发服务器——进行 localhost 测试时,运行服务器检测:
cd $SKILL_DIR && node -e "require('./lib/helpers').detectDevServers().then(servers => console.log(JSON.stringify(servers)))"- 如果找到 1 个服务器:自动使用并告知用户
- 如果找到多个服务器:询问用户要测试哪一个
- 如果未找到服务器:询问 URL,或主动协助启动开发服务器
-
将脚本写入 /tmp——切勿将测试文件写入技能目录;始终使用
/tmp/playwright-test-*.js -
参数化 URL——始终通过环境变量或脚本顶部的常量配置 URL
-
完成后停止会话——任务完成后,清理持久浏览器:
cd $SKILL_DIR && node run.js --session stop会话在闲置 10 分钟后也会自动停止。
工作原理
- 描述你想要测试或自动化的内容
- 启动会话(
--session start)——浏览器会保持运行,供后续脚本使用 - 自动检测正在运行的开发服务器(如果要测试外部网站,则询问 URL)
- 在
/tmp/playwright-test-*.js中编写自定义 Playwright 代码(不会使你的项目变得杂乱) - 通过以下命令运行:
cd $SKILL_DIR && node run.js /tmp/playwright-test-*.js——自动连接到会话 - 实时显示结果;登录状态和页面会在多次运行之间保留
- 完成后停止会话(
--session stop);测试文件由操作系统从 /tmp 中自动清理
本地浏览器模式(用户的 Chrome)
当用户要求你与其实际浏览器交互——使用其身份验证信息、Cookie 或扩展程序时,请使用本地浏览器连接器,而不是无头 Playwright。
**使用场景:**用户要求你代其在浏览器中执行操作,或你需要使用其已通过身份验证的会话。仅在用户的本地计算机上可用(Docker/沙盒环境中不可用)。
**先决条件:**Chrome 正在运行,且已安装 Playwright MCP Bridge 扩展程序。
执行: cd $SKILL_DIR && node scripts/connect-local.js /tmp/my-script.js 或 cd $SKILL_DIR && node run.js --connect /tmp/my-script.js
加载: references/local-browser.md,了解路由指南、限制和示例。
设置(首次使用)
cd $SKILL_DIR
npm run setup
这会安装 Playwright 和 Chromium 浏览器。只需安装一次。
执行模式
步骤 1:检测开发服务器(用于 localhost 测试)
cd $SKILL_DIR && node -e "require('./lib/helpers').detectDevServers().then(s => console.log(JSON.stringify(s)))"
步骤 2:将测试脚本写入 /tmp,并添加 URL 参数
// /tmp/playwright-test-page.js
const { chromium } = require('playwright');
// Parameterized URL (detected or user-provided)
const TARGET_URL = 'http://localhost:3001'; // <-- Auto-detected or from user
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.goto(TARGET_URL);
console.log('Page loaded:', await page.title());
await page.screenshot({ path: '/tmp/screenshot.png', fullPage: true });
console.log('Screenshot saved to /tmp/screenshot.png');
await browser.close();
})();
步骤 3:从技能目录执行
cd $SKILL_DIR && node run.js /tmp/playwright-test-page.js
状态检查:先查看 ARIA 快照,再查看截图
需要了解页面上的内容时,请使用 getPageStructure(),而不是 page.screenshot()。
| 需求 | 使用 | 原因 |
|---|---|---|
| 页面上有哪些元素? | getPageStructure(page) | 基于文本,内容精简,包含角色和选择器 |
| 页面看起来是什么样? | page.screenshot() | 展示视觉布局、颜色和间距——需要视觉处理 |
| 正确的元素是否已渲染? | getPageStructure(page, { root: '#my-section' }) | 限定范围的 ARIA 树——内容更少、速度更快 |
| 表单是否易于访问? | getPageStructure(page, { interactiveOnly: true }) | 仅包含交互元素——重点明确 |
| 我可以与哪些元素交互(按 ref ID)? | getPageStructureWithRefs(page) | 返回简短的 ref ID(e1、e2),以节省交互所需的 token |
| 能否在一个产物中同时查看视觉布局和交互元素? | annotatedScreenshot(page) | 为每个交互元素标注编号徽标的截图 |
| 操作后发生了哪些变化? | diffPageStructure(page, previousYaml) | ARIA 快照的统一差异——显示新增、移除或变更的元素 |
**原因:**ARIA 快照会生成约 200–400 个 token 的结构化文本。截图会生成约 2000–5000 个 token 的 Base64 图像数据,需要进行视觉处理。对于包含 10 个场景的 QA 运行,这种差异会显著累积。
仍需使用截图的情况:
- 视觉回归检查(布局、间距、对齐)
- 为 PR 正文采集证据
- 调试 CSS/样式问题
- 需要查看某物看起来如何,而非有哪些元素时
先观察,再编写(适用于多步骤流程)
对于包含 3 次以上交互的脚本(表单提交、多页面流程、复杂 UI),不要猜测选择器。先探索,再编写。
步骤 1:探索页面结构
// /tmp/playwright-explore.js
const { chromium } = require('playwright');
const helpers = require('./lib/helpers');
const TARGET_URL = 'http://localhost:3001/login';
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.goto(TARGET_URL);
// Get the actual page structure — roles, names, selectors
const structure = await helpers.getPageStructure(page, { interactiveOnly: true });
console.log('=== Interactive Elements ===');
console.log(JSON.stringify(structure.tree, null, 2));
console.log('\n=== Summary ===');
console.log(JSON.stringify(structure.summary));
await browser.close();
})();
步骤 2:使用观察到的选择器编写正式脚本
使用探索输出中的选择器和角色。不要猜测 input[name="..."] 或 button.class-name,而要使用你在 ARIA 树中实际看到的内容。
**适用情况:**多步骤表单、不熟悉的 UI、需要身份验证的流程,以及曾经有过一次选择器猜测失败的任何页面。
**可跳过的情况:**单步脚本(截图、检查标题、导航并验证)。
基于 ref 的交互(节省 token 的替代方式)
对于需要与许多元素交互的多步骤流程,请使用 getPageStructureWithRefs() + resolveRef(),而不是手动复制选择器字符串。ref ID(e1、e2……)更短,也更不容易出错。
// /tmp/playwright-ref-interact.js
const { chromium } = require('playwright');
const helpers = require('./lib/helpers');
const TARGET_URL = 'http://localhost:3001/login';
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.goto(TARGET_URL);
// Get structure with ref IDs
const { tree, refMap } = await helpers.getPageStructureWithRefs(page, { interactiveOnly: true });
console.log('Interactive elements:');
tree.forEach(el => console.log(` ${el.ref || '-'} ${el.role}: "${el.name}"`));
// e.g.: e1 textbox: "Email" e2 textbox: "Password" e3 button: "Sign in"
// Interact by ref (resolveRef is async — includes staleness check)
await (await helpers.resolveRef(page, refMap, 'e1')).fill('[email protected]');
await (await helpers.resolveRef(page, refMap, 'e2')).fill('password123');
await (await helpers.resolveRef(page, refMap, 'e3')).click();
await page.waitForURL('**/dashboard');
console.log('Login successful');
await browser.close();
})();
标注截图——在一张图片中查看页面及其交互元素:
// /tmp/playwright-annotated.js
const { chromium } = require('playwright');
const helpers = require('./lib/helpers');
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.goto('http://localhost:3001');
const result = await helpers.annotatedScreenshot(page, { path: '/tmp/annotated.png' });
console.log(`Annotated screenshot: ${result.path} (${result.elementCount} elements labeled)`);
console.log('Ref map:', result.refMap);
// Then: Read tool → /tmp/annotated.png to see the visual with numbered badges
await browser.close();
})();
**何时使用 ref,何时使用选择器:**如果需要在同一页面状态下与 3 个以上元素交互,ref 最适合多步骤流程。对于单次交互,或需要了解选择器(调试)时,请直接使用 getPageStructure()。
快照差异比较
比较操作前后的 ARIA 树快照,准确查看发生了哪些变化。无需手动比较原始 YAML,即可验证操作是否产生了预期效果。
// /tmp/playwright-diff.js
const { chromium } = require('playwright');
const helpers = require('./lib/helpers');
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.goto('http://localhost:3001/settings');
// Take a baseline snapshot
const { yaml: before } = await helpers.getPageStructure(page);
// Perform an action
await page.click('button.save');
await page.waitForLoadState('networkidle');
// Diff — see exactly what changed
const result = await helpers.diffPageStructure(page, before);
if (result.changed) {
console.log('Changes detected:\n', result.diff);
} else {
console.log('No changes — action had no effect');
}
// Chain: use currentYaml as next baseline
// const result2 = await helpers.diffPageStructure(page, result.currentYaml);
await browser.close();
})();
**适用情况:**表单提交、导航、AJAX 更新之后——以及任何需要验证页面是否发生变化的操作。逐行差异格式(+ 表示新增,- 表示移除, 表示未变)简洁易读。
身份验证
凭据
浏览器技能从环境变量读取凭据。在调用浏览器自动化之前,请先设置这些变量:
| 环境变量 | 用途 |
|---|---|
BROWSER_AUTH_USER | 默认用户名/邮箱 |
BROWSER_AUTH_PASS | 默认密码 |
BROWSER_AUTH_TOTP_SECRET | 用于 2FA 的 Base32 编码 TOTP 密钥 |
多站点凭据: 对于需要通过多个服务进行身份验证的工作流,请使用特定域名的环境变量,并在没有对应变量时回退到默认值:
| 环境变量模式 | 示例 |
|---|---|
BROWSER_AUTH_<DOMAIN>_USER | BROWSER_AUTH_GITHUB_USER |
BROWSER_AUTH_<DOMAIN>_PASS | BROWSER_AUTH_GOOGLE_PASS |
BROWSER_AUTH_<DOMAIN>_TOTP_SECRET | BROWSER_AUTH_GITHUB_TOTP_SECRET |
请根据要进行身份验证的域名匹配凭据集。如果不存在特定域名的变量,则回退到默认凭据集(BROWSER_AUTH_USER 等)。
身份验证拦截分类
当浏览器自动化遇到身份验证拦截时,请对其分类并采取相应措施。在认定需要人工介入前,请尝试所有可自动化的类别。 分类仅供参考,并非死板规则;如果某项操作看起来可以自动化,就尝试一下。只有在实际尝试失败后,才将其归类为“硬性拦截”。
| 类别 | 示例 | Agent 操作 | 置信度 |
|---|---|---|---|
| 可自动化 — 填写表单 | 用户名/密码登录、注册表单、密码重置 | 使用 helpers.authenticate(page, { username, password }) 填写字段,或通过 helpers.safeType() 手动填写。 | 高 |
| 可自动化 — TOTP | 登录后的 2FA 验证码输入 | 调用 helpers.generateTOTP(process.env.BROWSER_AUTH_TOTP_SECRET),填写验证码字段。 | 高(如果有密钥) |
| 可自动化 — OAuth/SSO | “使用 Google/GitHub/Microsoft 登录”按钮、SAML 重定向 | 点击 SSO 按钮,跟随重定向至身份提供商页面,在该页面填写凭据,然后跟随回调返回。 | 中(取决于身份提供商的机器人检测) |
| 可自动化 — 同意/横幅 | Cookie 同意、OAuth“授权”按钮、接受条款 | 点击接受/授权。处理 Cookie 时使用 helpers.handleCookieBanner()。 | 高 |
| 可自动化 — 邮件验证 | “点击邮件中的链接” | 仅当 Agent 可访问邮箱(邮箱 API)时尝试;如果无法访问,则归类为拦截。 | 低至中 |
| 尝试 — 可能需要人工介入 | reCAPTCHA v2 复选框、Cloudflare Turnstile、reCAPTCHA v3(基于评分) | 点击/交互。使用热启动配置文件可能通过。如果升级为图片挑战,则归类为拦截。 | 30-50% |
| 硬性拦截 — 需要人工介入 | hCaptcha、reCAPTCHA 图片挑战、短信/推送 MFA、WebAuthn/通行密钥、企业级反机器人系统(Akamai、PerimeterX、DataDome) | 在监督模式下:调用 helpers.handoff(page, { reason, successUrl })。在无头模式下:记录并跳过。 | 自动化成功率约为 0% |
交接(最后手段 — 仅限监督模式)
在监督模式下遇到硬性拦截时,helpers.handoff() 会打开有头浏览器,让人工解决拦截,然后恢复无头模式:
const { page: newPage, context: newCtx } = await helpers.handoff(page, {
reason: 'hCaptcha on login page',
successUrl: '/dashboard',
timeout: 120000
});
// newPage is authenticated — continue testing
交接功能会自动检测你处于会话模式(共享守护进程)还是独立模式,并进行相应处理。切勿在无头/CI 模式下调用 handoff——记录拦截情况,然后继续。
常见模式
测试页面(多个视口)
// /tmp/playwright-test-responsive.js
const { chromium } = require('playwright');
const TARGET_URL = 'http://localhost:3001'; // Auto-detected
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
// Desktop test
await page.setViewportSize({ width: 1920, height: 1080 });
await page.goto(TARGET_URL);
console.log('Desktop - Title:', await page.title());
await page.screenshot({ path: '/tmp/desktop.png', fullPage: true });
// Mobile test
await page.setViewportSize({ width: 375, height: 667 });
await page.screenshot({ path: '/tmp/mobile.png', fullPage: true });
await browser.close();
})();
测试登录流程
// /tmp/playwright-test-login.js
const { chromium } = require('playwright');
const TARGET_URL = 'http://localhost:3001'; // Auto-detected
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.goto(`${TARGET_URL}/login`);
await page.fill('input[name="email"]', '[email protected]');
await page.fill('input[name="password"]', 'password123');
await page.click('button[type="submit"]');
// Wait for redirect
await page.waitForURL('**/dashboard');
console.log('Login successful, redirected to dashboard');
await browser.close();
})();
测试已通过身份验证的页面
登录一次,保存会话,并在多次测试运行中重复使用。这样可以避免每次都重新登录。
步骤 1:登录并保存身份验证状态(运行一次)
// /tmp/playwright-auth-save.js
const { chromium } = require('playwright');
const helpers = require('./lib/helpers');
const TARGET_URL = 'http://localhost:3001/login';
(async () => {
const browser = await chromium.launch({ headless: true });
const context = await helpers.createContext(browser);
const page = await context.newPage();
await page.goto(TARGET_URL);
await helpers.authenticate(page, {
username: '[email protected]',
password: 'password123'
});
// Save session for reuse
const saved = await helpers.saveAuthState(context);
console.log('Auth state saved:', saved.path);
console.log(` ${saved.cookies} cookies, ${saved.origins} origins`);
// For Firebase/Supabase/modern auth that stores tokens in IndexedDB:
// const saved = await helpers.saveAuthState(context, '/tmp/auth.json', { indexedDB: true });
await browser.close();
})();
步骤 2:在后续测试中重复使用已保存的身份验证状态
// /tmp/playwright-test-dashboard.js
const { chromium } = require('playwright');
const helpers = require('./lib/helpers');
const TARGET_URL = 'http://localhost:3001/dashboard';
(async () => {
const browser = await chromium.launch({ headless: true });
// Load saved auth — skips login entirely
const context = await helpers.loadAuthState(browser);
const page = await context.newPage();
await page.goto(TARGET_URL);
console.log('Page title:', await page.title());
// You're now on the authenticated dashboard
await page.screenshot({ path: '/tmp/dashboard.png', fullPage: true });
await browser.close();
})();
从运行中的 Chrome 提取身份验证信息
当你需要测试已通过身份验证的页面,但不想在脚本中硬编码凭据时,或者应用使用了难以自动化的 SSO、OAuth 或 2FA 时使用。连接到已登录的 Chrome,提取会话,并在任何无头脚本中重复使用。
**需要:**Chrome 正在运行,并且已安装 Playwright MCP Bridge 扩展程序。
步骤 1:从正在运行的 Chrome 提取身份验证信息(运行一次)
// /tmp/playwright-extract-auth.js
const { connectToLocalBrowser, extractAuthState } = require('./lib/local-browser');
(async () => {
const conn = await connectToLocalBrowser();
// Extracts cookies + localStorage from Chrome's current session
await extractAuthState(conn.context, { path: '/tmp/auth-state.json' });
console.log('Auth extracted to /tmp/auth-state.json');
// For Firebase/Supabase/modern auth stored in IndexedDB:
// await extractAuthState(conn.context, { path: '/tmp/auth-state.json', indexedDB: true });
await conn.close();
})();
执行:cd $SKILL_DIR && node run.js --connect /tmp/playwright-extract-auth.js
步骤 2:在无头脚本中重复使用提取的身份验证信息
// /tmp/playwright-test-with-chrome-auth.js
const { chromium } = require('playwright');
const helpers = require('./lib/helpers');
const TARGET_URL = 'http://localhost:3001/dashboard';
(async () => {
const browser = await chromium.launch({ headless: true });
// Load Chrome's session — no credentials needed
const context = await helpers.loadAuthState(browser, '/tmp/auth-state.json');
const page = await context.newPage();
await page.goto(TARGET_URL);
console.log('Page title:', await page.title());
await browser.close();
})();
何时使用此方法,而不是“测试已通过身份验证的页面”:
| 场景 | 使用方法 |
|---|---|
| 你有凭据,并且想要一个独立脚本 | “测试已通过身份验证的页面”(登录 → saveAuthState) |
| 你已登录 Chrome,并且想要重复使用该会话 | 此模式(extractAuthState → loadAuthState) |
| 应用使用了难以编写脚本的 SSO、OAuth 或 2FA | 此模式 |
有关哪些身份验证状态可以可靠地转移(大多数会话 Cookie 和 JWT),以及哪些不能(Google DBSC、Cloudflare、WebAuthn),请参阅 references/local-browser.md。
填写并提交表单
// /tmp/playwright-test-form.js
const { chromium } = require('playwright');
const TARGET_URL = 'http://localhost:3001'; // Auto-detected
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.goto(`${TARGET_URL}/contact`);
await page.fill('input[name="name"]', 'John Doe');
await page.fill('input[name="email"]', '[email protected]');
await page.fill('textarea[name="message"]', 'Test message');
await page.click('button[type="submit"]');
// Verify submission
await page.waitForSelector('.success-message');
console.log('Form submitted successfully');
await browser.close();
})();
检查网络请求
// /tmp/playwright-test-network.js
const { chromium } = require('playwright');
const TARGET_URL = 'http://localhost:3001';
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
// Capture all API requests
const apiRequests = [];
page.on('request', request => {
if (request.url().includes('/api/')) {
apiRequests.push({
method: request.method(),
url: request.url(),
headers: request.headers()
});
}
});
page.on('response', response => {
if (response.url().includes('/api/')) {
console.log(`${response.status()} ${response.url()}`);
}
});
await page.goto(TARGET_URL);
await page.waitForLoadState('networkidle');
console.log('API requests captured:', JSON.stringify(apiRequests, null, 2));
await browser.close();
})();
注入 JavaScript
// /tmp/playwright-test-js-inject.js
const { chromium } = require('playwright');
const TARGET_URL = 'http://localhost:3001';
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.goto(TARGET_URL);
// Inject and execute JavaScript
const result = await page.evaluate(() => {
return {
title: document.title,
links: document.querySelectorAll('a').length,
meta: Array.from(document.querySelectorAll('meta')).map(m => ({
name: m.getAttribute('name'),
content: m.getAttribute('content')
})).filter(m => m.name),
localStorage: Object.keys(window.localStorage),
cookies: document.cookie
};
});
console.log('Page analysis:', JSON.stringify(result, null, 2));
await browser.close();
})();
检查失效链接
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.goto('http://localhost:3000');
const links = await page.locator('a[href^="http"]').all();
const results = { working: 0, broken: [] };
for (const link of links) {
const href = await link.getAttribute('href');
try {
const response = await page.request.head(href);
if (response.ok()) {
results.working++;
} else {
results.broken.push({ url: href, status: response.status() });
}
} catch (e) {
results.broken.push({ url: href, error: e.message });
}
}
console.log(`Working links: ${results.working}`);
console.log(`Broken links:`, results.broken);
await browser.close();
})();
截图并处理错误
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
try {
await page.goto('http://localhost:3000', {
waitUntil: 'networkidle',
timeout: 10000,
});
await page.screenshot({
path: '/tmp/screenshot.png',
fullPage: true,
});
console.log('Screenshot saved to /tmp/screenshot.png');
} catch (error) {
console.error('Error:', error.message);
} finally {
await browser.close();
}
})();
测试响应式设计
// /tmp/playwright-test-responsive-full.js
const { chromium } = require('playwright');
const TARGET_URL = 'http://localhost:3001'; // Auto-detected
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
const viewports = [
{ name: 'Desktop', width: 1920, height: 1080 },
{ name: 'Tablet', width: 768, height: 1024 },
{ name: 'Mobile', width: 375, height: 667 },
];
for (const viewport of viewports) {
console.log(
`Testing ${viewport.name} (${viewport.width}x${viewport.height})`,
);
await page.setViewportSize({
width: viewport.width,
height: viewport.height,
});
await page.goto(TARGET_URL);
await page.waitForTimeout(1000);
await page.screenshot({
path: `/tmp/${viewport.name.toLowerCase()}.png`,
fullPage: true,
});
}
console.log('All viewports tested');
await browser.close();
})();
在工作流期间监控控制台错误
用于验证 UI 工作流不会产生未显示的 JS 错误。
// /tmp/playwright-test-console.js
const { chromium } = require('playwright');
const helpers = require('./lib/helpers');
const TARGET_URL = 'http://localhost:3001';
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
// Start capturing BEFORE navigation
const consoleLogs = helpers.startConsoleCapture(page);
await page.goto(TARGET_URL);
await page.waitForLoadState('networkidle');
// Interact with the page
await page.click('button.submit').catch(() => {});
await page.waitForTimeout(1000);
// Check for errors
const errors = helpers.getConsoleErrors(consoleLogs);
if (errors.length > 0) {
console.log(`FAIL: ${errors.length} console error(s):`);
errors.forEach(e => console.log(` [${e.type}] ${e.text}`));
} else {
console.log('PASS: No console errors');
}
// Optionally filter for specific logs
const apiLogs = helpers.getConsoleLogs(consoleLogs, /api|fetch/i);
console.log(`API-related logs: ${apiLogs.length}`);
await browser.close();
})();
验证 UI 工作流期间的网络请求
用于检查是否触发了正确的 API 调用,以及状态代码是否正确。
// /tmp/playwright-test-network-verify.js
const { chromium } = require('playwright');
const helpers = require('./lib/helpers');
const TARGET_URL = 'http://localhost:3001';
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
// Capture only API requests
const network = helpers.startNetworkCapture(page, '/api/');
await page.goto(`${TARGET_URL}/dashboard`);
await page.waitForLoadState('networkidle');
// Check for failed API calls
const failed = helpers.getFailedRequests(network);
if (failed.length > 0) {
console.log(`FAIL: ${failed.length} failed API request(s):`);
failed.forEach(r => console.log(` ${r.method} ${r.url} -> ${r.status || r.failure}`));
} else {
console.log('PASS: All API requests succeeded');
}
// Review all captured requests
const all = helpers.getCapturedRequests(network);
console.log(`Total API requests: ${all.length}`);
all.forEach(r => console.log(` ${r.status} ${r.method} ${r.url}`));
await browser.close();
})();
录制工作流视频
当你需要录制多步骤浏览器交互时使用。
// /tmp/playwright-test-video.js
const { chromium } = require('playwright');
const helpers = require('./lib/helpers');
const TARGET_URL = 'http://localhost:3001';
(async () => {
const browser = await chromium.launch({ headless: true });
const context = await helpers.createVideoContext(browser, {
outputDir: '/tmp/playwright-videos'
});
const page = await context.newPage();
await page.goto(TARGET_URL);
await page.click('nav a:first-child');
await page.waitForTimeout(1000);
await page.click('button.submit').catch(() => {});
await page.waitForTimeout(1000);
// Video is saved when page closes
const videoPath = await page.video().path();
await page.close();
await context.close();
console.log(`Video saved: ${videoPath}`);
await browser.close();
})();
在修改后检查浏览器状态
用于验证 UI 操作是否正确保存了数据。
// /tmp/playwright-test-state.js
const { chromium } = require('playwright');
const helpers = require('./lib/helpers');
const TARGET_URL = 'http://localhost:3001';
(async () => {
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext();
const page = await context.newPage();
await page.goto(TARGET_URL);
// Check state before action
const storageBefore = await helpers.getLocalStorage(page);
console.log('localStorage before:', JSON.stringify(storageBefore));
const cookies = await helpers.getCookies(context);
console.log('Cookies:', cookies.map(c => `${c.name}=${c.value}`));
// Perform some action that should change state
await page.click('button.save-preferences').catch(() => {});
await page.waitForTimeout(500);
// Check state after action
const storageAfter = await helpers.getLocalStorage(page);
console.log('localStorage after:', JSON.stringify(storageAfter));
// Clean up for next test
await helpers.clearAllStorage(page);
await browser.close();
})();
探索页面结构
当你不了解页面的 DOM 结构时使用,例如第三方网站、需要身份验证的仪表板或陌生的 UI。获取 ARIA 快照,以便在编写交互代码前找到合适的选择器。
返回 yaml(保留层级结构的原始 ARIA 快照字符串)、tree(包含建议选择器的解析后节点)和 summary(按角色类型统计的数量)。
// /tmp/playwright-test-discover.js
const { chromium } = require('playwright');
const helpers = require('./lib/helpers');
const TARGET_URL = 'http://localhost:3001';
(async () => {
const browser = await chromium.launch({ headless: true });
const context = await helpers.createContext(browser);
const page = await context.newPage();
await page.goto(TARGET_URL, { waitUntil: 'networkidle' });
// Get full page structure
const structure = await helpers.getPageStructure(page);
console.log('Page:', structure.title);
console.log('Elements:', JSON.stringify(structure.summary));
// Raw YAML preserves nesting — useful for understanding page hierarchy
console.log('ARIA snapshot:\n', structure.yaml);
// Parsed tree has suggested selectors for each element
console.log('Interactive elements:');
structure.tree.filter(el =>
['button','link','textbox','checkbox','combobox'].includes(el.role)
).forEach(el => console.log(` ${el.role}: "${el.name}" → ${el.selector}`));
// Scope to a specific section
const formElements = await helpers.getPageStructure(page, {
interactiveOnly: true,
root: 'form'
});
console.log('Form inputs:', JSON.stringify(formElements.tree, null, 2));
await browser.close();
})();
目视检查(查看页面)
当你需要查看页面的实际样式时使用——例如在截取最终截图前、探索过程中、执行操作后,或验证 UI 状态时。这是为了帮助你了解页面,而非用于输出。
模式:截取临时截图,然后读取该截图。
// /tmp/playwright-test-inspect.js
const { chromium } = require('playwright');
const helpers = require('./lib/helpers');
const TARGET_URL = 'http://localhost:3001';
(async () => {
const browser = await chromium.launch({ headless: true });
const context = await helpers.createContext(browser);
const page = await context.newPage();
await page.goto(`${TARGET_URL}/dashboard`, { waitUntil: 'networkidle' });
// Take a quick screenshot to see the page
await page.screenshot({ path: '/tmp/inspect.png' });
// Inspect a specific section
const section = page.locator('.settings-panel');
await section.screenshot({ path: '/tmp/inspect-section.png' });
await browser.close();
})();
运行脚本后,读取图像文件以查看页面的实际样式:
Read tool → /tmp/inspect.png
Claude 会以可视化方式呈现 PNG 文件,因此你可以看到页面的实际布局、内容、弹出窗口、加载状态和任何问题。
何时使用此方法,而不是 getPageStructure():
| 需求 | 使用方法 |
|---|---|
| 查找选择器、了解 DOM 层级结构 | getPageStructure()(文本——速度更快、更精确) |
| 查看页面的实际样式 | 目视检查(截图——布局、颜色、叠加层、渲染效果) |
| 两者兼需——页面不熟悉 | 两者都用:先检查结构以查找选择器,再截图查看视觉效果 |
**提示:**进行迭代工作(探索页面、调试预脚本)时,请使用持久会话,这样就不必每次都重新启动浏览器。每次运行时都会覆盖截图文件。
为文档截取屏幕截图
当你需要为文档、帮助文章或 PR 截图撰写内容,并且需要运行中 UI 的一致、高质量图像时使用。
// /tmp/playwright-test-doc-screenshot.js
const { chromium } = require('playwright');
const TARGET_URL = 'http://localhost:3001';
(async () => {
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
viewport: { width: 1280, height: 720 },
deviceScaleFactor: 2, // Retina clarity
});
const page = await context.newPage();
await page.goto(`${TARGET_URL}/settings`);
await page.waitForLoadState('networkidle');
// Crop to the relevant section — avoid full-page captures with empty space
const section = page.locator('.api-keys-section');
await section.screenshot({
path: '/tmp/doc-settings-api-keys.png',
type: 'png',
});
// Full-page fallback when you need the whole view
await page.screenshot({
path: '/tmp/doc-settings-full.png',
type: 'png',
fullPage: false, // Viewport-only — keep it tight
});
console.log('Doc screenshots saved to /tmp/doc-*.png');
await browser.close();
})();
文档截图的关键设置:
viewport: { width: 1280, height: 720 }——标准文档宽度deviceScaleFactor: 2——视网膜分辨率,确保文字清晰type: 'png'——适用于 UI 截图的无损格式- 使用
element.screenshot()截取特定面板,而不是整页 - 每张图片控制在 <200KB——尽可能裁剪
媒体资源处理流水线
根据目标选择合适的预设和转换方式。预设会自动设置视口和 DPR,无需手动配置。
| 目标 | 预设 | 输出 | 最大大小 | 原因 |
|---|---|---|---|---|
| 文档站点截图 | docs-retina | 2560×1440 PNG | <500 KB | 针对 Next.js Image 呈现清晰的 Retina 效果 |
| GitHub PR 截图 | pr-standard | 1280×720 PNG | <200 KB | 在 GitHub 的 894px 显示宽度下清晰锐利。使用 /media-upload skill 上传,以获取 CDN URL |
| GitHub PR GIF | gif-compact | 800×450 动态 GIF | <10 MB | DPR 为 1——瓶颈在于 GIF 的 256 色调色板,而非像素密度。使用 /media-upload skill 上传,以获取 CDN URL |
| 视频(内部或面向客户) | video | 2560×1440 WebM → Bunny 或 Vimeo | — | 使用 /media-upload skill 上传。这两个平台都会转码为 ABR。视频使用 DPR 1 才正确 |
使用预设捕获适用于文档的高质量截图:
// /tmp/playwright-test-preset-screenshot.js
const { chromium } = require('playwright');
const helpers = require('./lib/helpers');
const TARGET_URL = 'http://localhost:3001';
(async () => {
const browser = await chromium.launch({ headless: true });
// Preset sets viewport 1280x720 + DPR 2 → 2560x1440 output
const context = await helpers.createPresetContext(browser, 'docs-retina');
const page = await context.newPage();
await page.goto(`${TARGET_URL}/settings`);
await page.waitForLoadState('networkidle');
// Element-level crop for tight framing
const section = page.locator('.api-keys-section');
await section.screenshot({ path: '/tmp/doc-api-keys.png', type: 'png' });
console.log('Docs screenshot: 2560x1440 Retina PNG');
await browser.close();
})();
为 PR 制作分步 GIF:
// /tmp/playwright-test-pr-gif.js
const { chromium } = require('playwright');
const helpers = require('./lib/helpers');
const TARGET_URL = 'http://localhost:3001';
(async () => {
const browser = await chromium.launch({ headless: true });
// gif-compact: 800x450 @ DPR 1 — optimized for GitHub's 10MB limit
const context = await helpers.createPresetContext(browser, 'gif-compact');
const page = await context.newPage();
const frames = [];
// Frame 1: Starting state
await page.goto(`${TARGET_URL}/settings`);
await page.waitForLoadState('networkidle');
frames.push(await page.screenshot({ type: 'png' }));
// Frame 2: Click action
await page.click('button.save');
await page.waitForTimeout(500);
frames.push(await page.screenshot({ type: 'png' }));
// Frame 3: Success state
await page.waitForSelector('.success-toast');
frames.push(await page.screenshot({ type: 'png' }));
// Assemble GIF — 3 frames at 2fps = 1.5s loop
const result = await helpers.screenshotsToGif(frames, '/tmp/pr-demo.gif', {
width: 800, height: 450, fps: 2
});
console.log(`GIF: ${result.path} (${result.sizeMB} MB, ${result.frames} frames)`);
await browser.close();
})();
添加点击指示和步骤标签的 GIF:
// /tmp/playwright-test-annotated-gif.js
const { chromium } = require('playwright');
const helpers = require('./lib/helpers');
const TARGET_URL = 'http://localhost:3001';
(async () => {
const browser = await chromium.launch({ headless: true });
const context = await helpers.createPresetContext(browser, 'gif-compact');
const page = await context.newPage();
const frames = [];
const annotations = [];
// Frame 1: Navigate to page
await page.goto(TARGET_URL);
frames.push(await page.screenshot({ type: 'png' }));
annotations.push({ label: 'Step 1: Open login page' });
// Frame 2: Click username field
await page.click('#username');
frames.push(await page.screenshot({ type: 'png' }));
annotations.push({ label: 'Step 2: Click username', click: { x: 640, y: 300 } });
// Frame 3: Type credentials
await page.fill('#username', 'admin');
frames.push(await page.screenshot({ type: 'png' }));
annotations.push({ label: 'Step 3: Enter username' });
// Frame 4: Click submit
await page.click('button[type="submit"]');
frames.push(await page.screenshot({ type: 'png' }));
annotations.push({ label: 'Step 4: Submit', click: { x: 640, y: 400 } });
const result = await helpers.screenshotsToGif(frames, '/tmp/login-demo.gif', {
width: 800, height: 450, fps: 2,
annotations
});
console.log(`Annotated GIF: ${result.path} (${result.sizeMB} MB, ${result.frames} frames)`);
await browser.close();
})();
运行无障碍审计
检查页面是否违反 WCAG 时使用。
// /tmp/playwright-test-a11y.js
const { chromium } = require('playwright');
const helpers = require('./lib/helpers');
const TARGET_URL = 'http://localhost:3001';
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.goto(TARGET_URL);
await page.waitForLoadState('networkidle');
const audit = await helpers.runAccessibilityAudit(page);
console.log(`Accessibility audit: ${audit.violationCount} violation(s), ${audit.passes} passes`);
if (audit.violationCount > 0) {
console.log('\nViolations:');
audit.summary.forEach(v => {
console.log(` [${v.impact}] ${v.id}: ${v.description} (${v.nodes} element(s))`);
console.log(` Help: ${v.helpUrl}`);
});
}
// Test keyboard focus order
const focusOrder = await helpers.checkFocusOrder(page, [
'a[href]:first-of-type',
'nav a:nth-child(2)',
'input[type="search"]'
]);
focusOrder.forEach(f => {
console.log(` Tab ${f.step}: expected ${f.expectedSelector} -> ${f.matches ? 'PASS' : 'FAIL'}`);
});
await browser.close();
})();
处理对话框和覆盖层
页面上有 alert()/confirm()/prompt() 对话框或阻止交互的覆盖层(Cookie 横幅、模态框)时使用。
// /tmp/playwright-test-dialogs.js
const { chromium } = require('playwright');
const helpers = require('./lib/helpers');
const TARGET_URL = 'http://localhost:3001';
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
// Auto-accept all dialogs (call BEFORE navigating)
const dialogLog = helpers.handleDialogs(page);
// Auto-dismiss cookie banners and common overlays
await helpers.dismissOverlays(page);
await page.goto(TARGET_URL);
await page.click('button.delete'); // triggers confirm()
// Check what dialogs appeared
console.log('Dialogs captured:', dialogLog.dialogs.length);
dialogLog.dialogs.forEach(d =>
console.log(` ${d.type}: "${d.message}"`)
);
// Custom overlay patterns (beyond the defaults)
await helpers.dismissOverlays(page, [
{ locator: '.onboarding-modal .close-btn', action: 'click' },
{ locator: '.promo-popup', action: 'remove' } // remove from DOM entirely
]);
await browser.close();
})();
使用追踪进行调试
当流程失败且需要准确了解发生了什么时使用——包括每一步的 DOM 状态、截图、网络和控制台信息。生成的 .zip 文件可在 Playwright Trace Viewer 中查看。
// /tmp/playwright-test-trace.js
const { chromium } = require('playwright');
const helpers = require('./lib/helpers');
const TARGET_URL = 'http://localhost:3001';
(async () => {
const browser = await chromium.launch({ headless: true });
const context = await helpers.createContext(browser);
// Start tracing BEFORE creating pages
await helpers.startTracing(context);
const page = await context.newPage();
await page.goto(TARGET_URL);
await page.click('button.submit');
await page.waitForSelector('.result');
// Stop and save trace
const trace = await helpers.stopTracing(context, '/tmp/trace.zip');
console.log(`Trace saved: ${trace.path}`);
console.log('View with: npx playwright show-trace /tmp/trace.zip');
await browser.close();
})();
生成 PDF
需要导出页面的 PDF 时使用,例如文档、报告或适合打印的输出。仅支持 Chromium headless 模式。
// /tmp/playwright-test-pdf.js
const { chromium } = require('playwright');
const helpers = require('./lib/helpers');
const TARGET_URL = 'http://localhost:3001/report';
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.goto(TARGET_URL, { waitUntil: 'networkidle' });
// Basic PDF
const result = await helpers.generatePdf(page, '/tmp/report.pdf');
console.log('PDF saved:', result.path);
// Accessible PDF with bookmarks
await helpers.generatePdf(page, '/tmp/report-accessible.pdf', {
tagged: true, // accessible/tagged PDF
outline: true, // document outline from headings
format: 'Letter',
margin: { top: '1cm', bottom: '1cm', left: '1cm', right: '1cm' }
});
await browser.close();
})();
下载文件
按钮或链接会触发文件下载,且你需要保存或检查文件时使用。
// /tmp/playwright-test-download.js
const { chromium } = require('playwright');
const helpers = require('./lib/helpers');
const TARGET_URL = 'http://localhost:3001/exports';
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.goto(TARGET_URL);
// Trigger download and save
const file = await helpers.waitForDownload(
page,
() => page.click('#export-csv'), // action that triggers the download
'/tmp/export.csv' // optional save path
);
console.log(`Downloaded: ${file.suggestedFilename} → ${file.path}`);
await browser.close();
})();
节省令牌模式(@playwright/cli)
对于高频 QA 运行(连续执行 5 个以上浏览器场景),使用 @playwright/cli 可减少令牌消耗。它会将快照、截图和日志写入磁盘,而不是写入 LLM 上下文——每项任务约消耗 27K 个令牌,而内联输出约需 114K(减少 4 倍)。
设置(一次性):
npm install -g @playwright/cli@latest
使用方式:
# Open a page (starts persistent session)
playwright-cli open http://localhost:3001
# Get page structure to disk (reads from .playwright-cli/ dir)
playwright-cli snapshot
# Interact
playwright-cli click "Login"
playwright-cli fill "Email" "[email protected]"
playwright-cli press Enter
# Take screenshot to disk
playwright-cli screenshot
# Read console/network to disk
playwright-cli console
playwright-cli network
之后按需从 .playwright-cli/ 读取保存的文件——只加载与当前断言相关的内容。
适用场景: 令牌预算有限的多场景 QA 运行。在委派模式下使用 10 个以上场景运行 /qa 时尤其有用。
适合使用普通模式的场景: 单个场景、调试(需要内联输出),以及需要以编程方式访问 Playwright API 的场景(复杂断言、自定义等待)。
用于测试多用户流程的命名会话:
playwright-cli -s=admin open http://localhost:3001
playwright-cli -s=user open http://localhost:3001
playwright-cli -s=admin click "Approve"
playwright-cli -s=user snapshot # verify the user sees the approval
注意: @playwright/cli 版本为 v0.1.1(Apache-2.0,尚未达到 1.0)。基于脚本的普通模式仍是主要执行方式;高频运行时可将此模式用作优化方案。
内联执行(简单任务)
对于快速的一次性任务,可以直接内联执行代码,无需创建文件:
# Take a quick screenshot
cd $SKILL_DIR && node run.js "
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.goto('http://localhost:3001');
await page.screenshot({ path: '/tmp/quick-screenshot.png', fullPage: true });
console.log('Screenshot saved');
await browser.close();
"
何时使用内联执行,何时使用文件:
- 内联执行:快速的一次性任务(截图、检查元素是否存在、获取页面标题)
- 文件:复杂测试、响应式设计检查,以及用户可能希望重新运行的任务
会话模式(持久浏览器)— 默认
对于所有交互式浏览器自动化,会话模式是推荐的默认模式。运行脚本前先启动会话——后续每个脚本都能在约 50 ms 内连接,而无需启动新浏览器(约需 2–3 s),并且登录状态会自动保留。
适用于会话模式的场景: 所有交互式工作,包括调试、测试、迭代流程、身份验证密集型页面、视频录制和多步骤自动化。这涵盖了绝大多数 Agent 使用场景。
仅在以下情况下使用 headless 模式(无会话): 真正的一次性脚本、CI/CD 流水线,或不适合运行持久守护进程的环境。
快速开始
# Start a session (do this first)
cd $SKILL_DIR && node run.js --session start
# Run scripts — they auto-connect to the session
cd $SKILL_DIR && node run.js /tmp/my-script.js
# Cookies, localStorage, and current URL all persist between runs
# Check session status
cd $SKILL_DIR && node run.js --session status
# Stop when done
cd $SKILL_DIR && node run.js --session stop
工作原理
--session start通过 Playwright 的launchServer()启动无头 Chromium- 浏览器作为后台守护进程运行(分离进程)
- 会话信息写入
/tmp/playwright-session.json - 运行脚本时,
run.js会自动检测会话并通过 WebSocket 连接 - 代码会获得预先配置好的
browser、context和page变量 - 脚本退出时,Cookie/localStorage/当前 URL 会保存到
/tmp/playwright-session-state.json - 下一个脚本会重新连接并恢复状态——认证状态和 URL 保持不变,可继续操作
代码可用的内容
在会话模式下,代码中预先定义了以下变量:
| 变量 | 说明 |
|---|---|
browser | 已连接的浏览器实例(在多次运行之间保持) |
context | 浏览器上下文,包含从上次运行恢复的 Cookie/localStorage |
page | 已导航至上次运行时最后一个 URL 的页面(首次运行时为空白) |
saveState | 退出前调用,以持久化 Cookie/localStorage/URL(包装器会自动调用) |
helpers | lib/helpers 中的所有辅助函数 |
chromium, devices | Playwright 导出项(用于创建其他上下文) |
会话模式与无头模式(无会话)对比
| 会话模式(默认) | 无头模式 — 无会话 | |
|---|---|---|
| 浏览器启动 | 一次(执行 --session start 时) | 每次执行脚本时 |
| 启动时间 | 约 50 ms(WebSocket 连接) | 约 2–3 秒(启动浏览器) |
| 登录状态 | 自动保留(运行之间保存 Cookie/localStorage) | 每次运行后丢失(使用 saveAuthState/loadAuthState) |
| 当前 URL | 从上次运行恢复 | 从 about:blank 开始 |
page.route() | 完全支持 | 完全支持 |
| Token 成本 | 最低(无需编写启动/关闭样板代码) | 较高(每个脚本都要启动并关闭) |
| 适用场景 | 所有交互式工作——调试、测试、迭代、认证流程 | CI/CD、隔离测试、一次性脚本 |
选项
# Start with headed browser (visible)
cd $SKILL_DIR && node run.js --session start --headless false
# Start with a resolution preset
cd $SKILL_DIR && node run.js --session start --preset video
自动清理
- 会话在闲置 10 分钟后自动停止(期间没有运行脚本)
- 如果会话进程崩溃,下一个脚本会检测到过期会话,并回退到全新的无头模式
- 会话文件和状态文件会自动清理
在会话模式下创建全新上下文
默认情况下会重用现有上下文(以持久化状态)。如果需要干净的上下文:
// Create an isolated context within the session
const freshContext = await browser.newContext();
const freshPage = await freshContext.newPage();
await freshPage.goto('https://example.com');
// This context has no cookies/localStorage from previous runs
可用辅助函数
所有辅助函数都位于 lib/helpers.js 中。在脚本中使用 const helpers = require('./lib/helpers');。这些函数按用途分类:
页面交互
| 辅助函数 | 使用时机 |
|---|---|
helpers.detectDevServers() | 至关重要 — 首先运行,用于 localhost 测试。返回检测到的服务 URL 数组。 |
helpers.createContext(browser, options?) | 使用默认值创建浏览器上下文:视口 1280x720、区域设置 en-US、时区 America/New_York。传入 { mobile: true } 可使用 iPhone UA。自动合并环境变量中的请求头。 |
helpers.waitForPageReady(page, options?) | 智能等待页面加载(默认使用 networkidle)。传入 { waitForSelector: '.loaded' } 可等待动态内容。 |
helpers.retryWithBackoff(fn, maxRetries?, initialDelay?) | 使用指数退避重试异步函数。默认:重试 3 次,初始延迟 1 秒。 |
helpers.safeClick(page, selector, { retries: 3 }) | 点击可能尚未立即显示或可点击的元素。自动重试。 |
helpers.safeType(page, selector, text) | 在输入框中输入内容。默认先清空字段。 |
helpers.extractTexts(page, selector) | 以数组形式获取多个匹配元素的文本。 |
helpers.scrollPage(page, 'down', 500) | 滚动页面。方向:'down'、'up'、'top'、'bottom'。 |
helpers.handleCookieBanner(page) | 关闭常见的 Cookie 同意横幅。尽早运行 — 清除阻挡交互的浮层。 |
helpers.authenticate(page, { username, password }) | 使用常见字段选择器执行登录流程。自动等待重定向。 |
helpers.generateTOTP(secret) | 根据 Base32 密钥生成 6 位 TOTP 验证码。采用 RFC 6238、SHA1、30 秒周期。在双重身份验证流程中与 BROWSER_AUTH_TOTP_SECRET 环境变量配合使用。 |
helpers.saveAuthState(context, path?, options?) | 身份验证后保存登录会话。默认路径:/tmp/playwright-auth.json。传入 { indexedDB: true } 可用于 Firebase/Supabase 身份验证。使用 loadAuthState 复用。 |
await helpers.handoff(page, { reason, successUrl?, successSelector?, timeout? }) | 仅限受监督模式。 最后的交接方案:关闭无头浏览器,为人工操作打开有头浏览器,轮询检查是否成功,然后恢复无头浏览器。自动检测会话模式或独立模式。返回 { page, context, completed } — 检查 completed 布尔值,以判断交接是成功还是超时。 |
helpers.loadAuthState(browser, path?, options?) | 使用保存的身份验证状态创建上下文。跳过重新登录。继承 createContext 的默认值。 |
helpers.getPageStructure(page, { interactiveOnly, root }) | 通过 ARIA 快照发现页面结构。返回 yaml(原始层级结构)、tree(带选择器的解析结果)和 summary(计数)。适用于不熟悉的页面。 |
helpers.getPageStructureWithRefs(page, options?) | 类似 getPageStructure,但会为交互元素分配简短的引用 ID(e1、e2 等)。另外返回 refMap 对象,其中包含结构化条目 { role, name, level?, nth? }。使用 nth 索引区分 role+name 相同的元素。可与 resolveRef() 配合,以节省令牌的方式进行多步交互。 |
await helpers.resolveRef(page, refMap, 'e3') | 异步。 将引用 ID 解析为 Playwright Locator。包含过期检测 — 如果元素已不在页面上,会立即(约 5 毫秒内)抛出错误,而不是等待 Playwright 的 30 秒超时。返回可调用 .click()、.fill() 等方法的 Locator。使用 (await helpers.resolveRef(...)).method() 语法。 |
await helpers.diffPageStructure(page, previousYaml, options?) | 将当前页面 ARIA 结构与之前的快照进行比较。返回 { diff, changed, currentYaml }。diff 是逐行差异字符串,带有 +/-/ 前缀。将 currentYaml 用作下一次比较的基准。需要 diff npm 包。 |
helpers.annotatedScreenshot(page, options?) | 截取带有交互元素编号标记的屏幕截图。返回 { path, refMap, tree, elementCount }。选项:path(保存位置)、root(范围)、fullPage。在同一产物中结合视觉布局和元素发现。 |
helpers.handleDialogs(page, options?) | 自动处理 alert/confirm/prompt 对话框。请在导航前调用。返回 { dialogs },供之后检查。 |
helpers.dismissOverlays(page, overlays?) | 使用 addLocatorHandler 自动关闭 Cookie 横幅、模态框和阻挡交互的浮层。传入自定义模式或使用默认值。 |
helpers.extractTableData(page, 'table.results') | 从 HTML 表格中提取结构化数据(表头 + 行)。 |
helpers.takeScreenshot(page, 'name') | 保存带时间戳的屏幕截图。 |
控制台监控 — 捕获静默 JS 错误
| 辅助函数 | 使用时机 |
|---|---|
helpers.startConsoleCapture(page) | 导航前调用。 返回一个收集器,用于累积所有控制台输出。 |
helpers.getConsoleErrors(collector) | 从收集器中获取仅包含错误级别消息和未捕获异常的内容。 |
helpers.getConsoleLogs(collector, filter?) | 获取所有日志,或按字符串/RegExp/函数筛选。 |
轻量替代方案(Playwright v1.56+): 如需快速检查且不使用收集器,可在事后使用 page.consoleMessages() 和 page.pageErrors() — 它们会返回自页面创建以来的所有消息/错误。
网络检查 — 验证 UI 工作流中的 API 调用
| 辅助函数 | 使用时机 |
|---|---|
helpers.startNetworkCapture(page, '/api/') | 导航前调用。 捕获请求/响应对。可选 URL 筛选条件。 |
helpers.getFailedRequests(collector) | 从收集器中获取 4xx、5xx 和连接失败请求。 |
helpers.getCapturedRequests(collector) | 获取所有已捕获的请求/响应条目。 |
helpers.waitForApiResponse(page, '/api/users', { status: 200 }) | 等待特定 API 调用完成。返回 { url, status, body, json }。 |
轻量替代方案(Playwright v1.56+): page.requests() 会返回自页面创建以来的所有请求 — 适合事后快速检查,无需设置收集器。
浏览器状态 — 检查存储和 Cookie
| 辅助函数 | 使用时机 |
|---|---|
helpers.getLocalStorage(page) | 获取所有 localStorage 条目。传入键可获取单个值。 |
helpers.getSessionStorage(page) | 获取所有 sessionStorage 条目。传入键可获取单个值。 |
helpers.getCookies(context) | 获取浏览器上下文中的所有 Cookie。 |
helpers.clearAllStorage(page) | 清除 localStorage + sessionStorage + Cookie。用于干净状态测试。 |
视频录制 — 录制浏览器交互
| 辅助函数 | 使用时机 |
|---|---|
helpers.createVideoContext(browser, { outputDir: '/tmp/videos' }) | 创建一个录制视频的上下文。页面/上下文关闭时保存视频。 |
媒体上传
加载 /media-upload 技能以上传视频或文件。它提供 uploadToVimeo()、uploadToBunnyStream() 和 uploadToBunnyStorage()。设置:./secrets/setup.sh --skill media-upload。
分辨率预设 — 为每个目标使用一致的尺寸
| 辅助函数 | 使用时机 |
|---|---|
helpers.RESOLUTION_PRESETS | 访问预设配置。键:docs-retina、pr-standard、gif-compact。每个预设都包含 viewport 和 deviceScaleFactor。 |
helpers.createPresetContext(browser, 'preset') | 创建使用预设 viewport + DPR 的上下文。替代手动配置 viewport/DPR。 |
媒体转换 — 将截图转换为 GIF
| 辅助函数 | 使用时机 |
|---|---|
helpers.screenshotsToGif(frames, path, opts) | 将 PNG 缓冲区转换为动画 GIF。选项:width、height、fps、quality、annotations(逐帧点击指示器 + 标签)。 |
无障碍 — WCAG 审计和键盘导航
| 辅助函数 | 使用时机 |
|---|---|
helpers.runAccessibilityAudit(page) | 注入 axe-core 并运行 WCAG 2.0 AA 审计。返回包含影响程度/描述的违规项。需要联网(CDN)。 |
helpers.checkFocusOrder(page, ['#first', '#second', '#third']) | 按 Tab 键遍历元素,并验证焦点是否按顺序落在预期选择器上。 |
性能指标 — 测量页面速度
| 辅助函数 | 使用时机 |
|---|---|
helpers.capturePerformanceMetrics(page) | 捕获导航计时(TTFB、DOM 交互时间)和 Web Vitals(FCP、LCP、CLS)。页面加载后调用。 |
响应式截图 — 多视口扫描
| 辅助函数 | 使用时机 |
|---|---|
helpers.captureResponsiveScreenshots(page, url) | 在手机/平板/桌面/宽屏断点下截取屏幕截图。可选自定义断点和输出目录。 |
网络模拟 — 测试网络状况不佳时的表现
| 辅助函数 | 使用场景 |
|---|---|
helpers.simulateSlowNetwork(page, 500) | 为所有请求添加人为延迟(毫秒)。 |
helpers.simulateOffline(context) | 将浏览器设为离线模式。 |
helpers.blockResources(page, ['image', 'font']) | 阻止特定类型的资源(图像、字体、样式表、脚本等)。 |
模拟特定故障: 使用 route.abort('connectionrefused') 模拟指定错误。错误类型:'connectionrefused'、'timedout'、'connectionreset'、'internetdisconnected'。
跟踪与调试
| 辅助函数 | 使用场景 |
|---|---|
helpers.startTracing(context, options?) | 开始录制跟踪(DOM 快照、屏幕截图、网络)。请在与页面交互前调用。 |
helpers.stopTracing(context, path?) | 停止跟踪并保存为 .zip。使用 npx playwright show-trace trace.zip 查看。 |
PDF 生成
| 辅助函数 | 使用场景 |
|---|---|
helpers.generatePdf(page, path?, options?) | 根据当前页面生成 PDF。选项:format、tagged(无障碍)、outline(书签)、margin。仅支持 Chromium 无头模式。 |
文件下载
| 辅助函数 | 使用场景 |
|---|---|
helpers.waitForDownload(page, triggerAction, savePath?) | 等待某个操作触发下载,然后保存文件。返回 { path, suggestedFilename, url }。 |
布局检查 — 验证元素位置
| 辅助函数 | 使用场景 |
|---|---|
helpers.getElementBounds(page, '.selector') | 获取边界框、可见性、是否位于视口内以及计算后的样式。选择器不存在时返回 null,元素隐藏时返回 { visible: false }。 |
页面结构内部工具 — 独立解析 ARIA 快照
| 辅助函数 | 使用场景 |
|---|---|
helpers.parseAriaSnapshot(yaml) | 将 Playwright ARIA 快照 YAML 字符串解析为结构化节点对象。每个节点都有 role、name,以及可选的 level、checked、disabled、expanded、selected。 |
helpers.suggestSelector(node) | 根据已解析的 ARIA 节点生成 getByRole(...) 选择器字符串。 |
helpers.INTERACTIVE_ROLES | 包含交互式 ARIA 角色的 Set(button、link、textbox、checkbox、radio、combobox、slider、switch、tab、menuitem、searchbox、spinbutton、option)。 |
本地浏览器 — 连接到用户的 Chrome
这些辅助函数位于 lib/local-browser.js 中。在脚本中使用 const { connectToLocalBrowser, getConnectedPage, extractAuthState } = require('./lib/local-browser');。完整文档请参阅 references/local-browser.md。
| 辅助函数 | 使用场景 |
|---|---|
connectToLocalBrowser(options?) | 通过扩展桥接连接到用户正在运行的 Chrome。返回 { browser, context, page, close() }。需要安装 Playwright MCP Bridge 扩展。设置 PLAYWRIGHT_MCP_EXTENSION_TOKEN 环境变量可跳过审批对话框。 |
getConnectedPage(context, url?) | 获取扩展公开的页面,并可选择导航到指定页面。注意:通过扩展桥接无法使用 context.newPage() — 请使用此函数或 connectToLocalBrowser() 返回的 page。 |
extractAuthState(context, options?) | 从用户的浏览器中提取 Cookie 和 localStorage(使用 { indexedDB: true } 时还会提取 IndexedDB)。使用 { path: '/tmp/auth.json' } 保存到文件,以便稍后通过 helpers.loadAuthState() 复用。 |
Shadow DOM — 与 Web 组件交互
| 助手 | 适用场景 |
|---|---|
helpers.pierceShadowDOM(page, hostSelector, fn, args?) | 在元素的开放 Shadow Root 内执行函数。适用于 Lit、Stencil、Radix、Shoelace、Salesforce Lightning、Angular Material CDK 以及任何 Web 组件。 |
hostSelector 可以是 CSS 选择器字符串,也可以是发现对象:
// CSS selector
await helpers.pierceShadowDOM(page, '#my-component', (sr) => {
return sr.querySelector('button')?.textContent;
});
// Dynamic discovery — find host whose id contains a substring
await helpers.pierceShadowDOM(page, { matchId: 'my-widget' }, (sr) => {
sr.querySelector('textarea')?.focus();
});
// Match by attribute
await helpers.pierceShadowDOM(page, { matchAttr: { name: 'data-widget', value: 'chat' } }, (sr) => {
return sr.querySelectorAll('button').length;
});
// Pass arguments
await helpers.pierceShadowDOM(page, '#player', (sr, vol) => {
sr.querySelector('input[type=range]').value = vol;
}, [75]);
存在的原因: 标准 Playwright 选择器(page.click、page.locator)无法穿透 Shadow DOM 边界。常见的变通方法是使用 page.evaluate() 手动扫描元素,但代码冗长且容易出错,尤其是在 Shadow Host ID 动态生成时(例如,Radix 会生成 inkeep-shadowradix-:r0:)。此助手封装了穿透并执行的模式。
DOM 稳定化 — 等待流式/实时内容
| 助手 | 适用场景 |
|---|---|
helpers.waitForDOMStabilization(page, options?) | 等待 DOM 区域停止变化。适用于触发流式 AI 响应、实时数据流、协作编辑更新或任何逐步到达的内容之后。 |
// Wait for chat response to finish streaming
const result = await helpers.waitForDOMStabilization(page, {
selector: '.chat-messages',
stableMs: 2000, // content unchanged for 2s = done
minContentLength: 100, // don't consider "stable" if still empty/loading
});
console.log(result.stable ? 'Streaming complete' : 'Timed out');
// Wait inside a shadow DOM host
await helpers.waitForDOMStabilization(page, {
inShadowDOM: { matchId: 'my-widget' },
selector: '[data-part="messages"]',
stableMs: 3000,
});
选项:selector(默认值:'body')、timeout(30s)、stableMs(3s)、pollInterval(500ms)、minContentLength(0)、inShadowDOM(null)。
返回 { stable: boolean, elapsed: number, finalLength: number }。
CDP 性能跟踪 — GPU 渲染指标
如需详细测量渲染流水线(栅格任务、绘制事件、合成开销),请加载参考文档:
加载: references/cdp-tracing.md
内容包括:使用正确类别的 CDP Tracing.start/end、Performance.getMetrics 增量、有头与无头模式(这一点至关重要——无头模式不会产生 GPU 事件)、用于高 DPI 压力测试的 DPR 配置,以及跟踪事件分析。
自定义 HTTP 标头
通过环境变量为所有 HTTP 请求配置自定义标头。适用于:
- 识别发往后端的自动化流量
- 获取针对 LLM 优化的响应(例如,纯文本错误,而非带样式的 HTML)
- 全局添加身份验证令牌
配置
单个标头(常见情况):
PW_HEADER_NAME=X-Automated-By PW_HEADER_VALUE=playwright-skill \
cd $SKILL_DIR && node run.js /tmp/my-script.js
多个标头(JSON 格式):
PW_EXTRA_HEADERS='{"X-Automated-By":"playwright-skill","X-Debug":"true"}' \
cd $SKILL_DIR && node run.js /tmp/my-script.js
工作原理
使用 helpers.createContext() 时会自动应用标头:
const context = await helpers.createContext(browser);
const page = await context.newPage();
// All requests from this page include your custom headers
对于使用原始 Playwright API 的脚本,请使用注入的 getContextOptionsWithHeaders():
const context = await browser.newContext(
getContextOptionsWithHeaders({ viewport: { width: 1920, height: 1080 } }),
);
高级用法
如需全面了解 Playwright API 文档,请参阅 API_REFERENCE.md:
- 选择器和定位器最佳实践
- 网络拦截和 API 模拟
- 身份验证和会话管理
- 视觉回归测试
- 移动设备模拟
- 性能测试
- 调试技巧
- CI/CD 集成
提示
- 关键:先检测服务——在为 localhost 测试编写测试代码前,务必先运行
detectDevServers() - 自定义标头——使用
PW_HEADER_NAME/PW_HEADER_VALUE环境变量,让后端识别自动化流量 - 使用 /tmp 存放测试文件——将文件写入
/tmp/playwright-test-*.js,不要写入 skill 目录或用户的项目 - 参数化 URL——在每个脚本顶部的
TARGET_URL常量中设置检测到或提供的 URL - 默认:无头浏览器——始终使用
headless: true,以兼容 Docker/CI - 有头模式——用户明确要求显示浏览器或在本地调试时,使用
headless: false - **等待策略:**使用
waitForURL、waitForSelector、waitForLoadState,而不是固定超时 - **错误处理:**始终使用 try-catch,确保自动化稳健运行
- **控制台输出:**使用
console.log()跟踪进度并展示当前操作 - **Docker:**辅助工具默认包含
--no-sandbox标志,以兼容容器 - **时间操控(Playwright v1.45+):**在测试中使用
page.clock控制时间——先运行await page.clock.install(),再运行await page.clock.fastForward('01:00')快进时间,或运行await page.clock.pauseAt(new Date('2025-01-01'))将时间冻结在指定时刻。适用于测试计时器、倒计时、会话过期和依赖时间的 UI。 - **WebSocket 拦截(Playwright v1.48+):**使用
page.routeWebSocket(url, handler)模拟或监控 WebSocket 连接。处理程序会接收一个WebSocketRoute,其中包含onMessage()、send()和close()。适用于在没有运行中服务的情况下测试实时功能(聊天、通知、实时更新)。 - **性能测试需要有头模式:**无头 Chrome 会跳过 GPU 合成——
Paint和CompositeLayers跟踪事件始终为零。进行任何渲染性能测量时都使用headless: false。请参阅references/cdp-tracing.md。 - **高 DPI 性能测试:**同时使用
--force-device-scale-factor=N(Chrome 标志,影响 GPU 光栅化)和deviceScaleFactor: N(Playwright 上下文,影响 CSS/屏幕截图)。二者的机制不同。DPR 1 会掩盖许多 GPU 瓶颈——使用 DPR 2-3 对渲染敏感路径进行压力测试。 - **Shadow DOM:**标准 Playwright 选择器无法穿透 Shadow 边界。对于使用 Web 组件(Lit、Stencil、Radix、Salesforce Lightning 等)的页面,请使用
helpers.pierceShadowDOM()。
故障排除
未安装 Playwright:
cd $SKILL_DIR && npm run setup
找不到模块:
确保通过 run.js 封装程序从 skill 目录运行
浏览器无法在 Docker 中启动:
确保已设置 --no-sandbox 和 --disable-setuid-sandbox 参数(辅助工具默认已包含)
找不到元素:
添加等待:await page.waitForSelector('.element', { timeout: 10000 })