mcpField
B
browser

browser

github.com/inkeep测试与质量49 次安装
代码仓库

关于 browser

browser 是用于网页检查与自动化测试的技能,按任务在 agent-browser 和 Playwright 脚本间选择。它支持持久浏览器会话、页面结构检查及复合流程自动化。

主要功能

  1. 01浏览页面并执行交互
  2. 02读取 ARIA 页面结构
  3. 03比较操作前后的页面状态
  4. 04运行 Playwright 复合自动化
  5. 05保存并恢复浏览器会话

适用场景

  1. 01检查网页元素和表单
  2. 02验证交互后的页面变化
  3. 03跨视口生成响应式截图
  4. 04自动化多步骤网页测试

MCP 相关

该技能提及 Playwright MCP Bridge 扩展,可连接本地 Chrome 会话;没有说明其提供 MCP 服务。

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/inkeep/team-skills --skill browser

安装后在 Claude Code 中输入/browser确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

.env.example
API_REFERENCE.md
README.md
package-lock.json
package.json
run.js
lib/
helpers.js
local-browser.js
session.js
references/
cdp-tracing.md
local-browser.md
scripts/
connect-local.js
session-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 恢复
连接到 Chromenode 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 代码,并通过通用执行器运行。

关键工作流——按顺序执行以下步骤:

  1. 启动会话——如果预计要运行多个脚本(调试、迭代、多步骤流程),请先启动持久浏览器会话。这是所有交互式工作的默认模式:

    cd $SKILL_DIR && node run.js --session start
    

    脚本会自动检测会话,并通过 WebSocket(约 50 毫秒)连接,而不是启动新浏览器(约 2–3 秒)。登录状态、Cookie 和 localStorage 会在多次运行之间保留。只有在真正的一次性脚本或 CI/CD 环境中才跳过此步骤。

  2. 自动检测开发服务器——进行 localhost 测试时,运行服务器检测:

    cd $SKILL_DIR && node -e "require('./lib/helpers').detectDevServers().then(servers => console.log(JSON.stringify(servers)))"
    
    • 如果找到 1 个服务器:自动使用并告知用户
    • 如果找到多个服务器:询问用户要测试哪一个
    • 如果未找到服务器:询问 URL,或主动协助启动开发服务器
  3. 将脚本写入 /tmp——切勿将测试文件写入技能目录;始终使用 /tmp/playwright-test-*.js

  4. 参数化 URL——始终通过环境变量或脚本顶部的常量配置 URL

  5. 完成后停止会话——任务完成后,清理持久浏览器:

    cd $SKILL_DIR && node run.js --session stop
    

    会话在闲置 10 分钟后也会自动停止。

工作原理

  1. 描述你想要测试或自动化的内容
  2. 启动会话(--session start)——浏览器会保持运行,供后续脚本使用
  3. 自动检测正在运行的开发服务器(如果要测试外部网站,则询问 URL)
  4. 在 /tmp/playwright-test-*.js 中编写自定义 Playwright 代码(不会使你的项目变得杂乱)
  5. 通过以下命令运行:cd $SKILL_DIR && node run.js /tmp/playwright-test-*.js——自动连接到会话
  6. 实时显示结果;登录状态和页面会在多次运行之间保留
  7. 完成后停止会话(--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>_USERBROWSER_AUTH_GITHUB_USER
BROWSER_AUTH_<DOMAIN>_PASSBROWSER_AUTH_GOOGLE_PASS
BROWSER_AUTH_<DOMAIN>_TOTP_SECRETBROWSER_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-retina2560×1440 PNG<500 KB针对 Next.js Image 呈现清晰的 Retina 效果
GitHub PR 截图pr-standard1280×720 PNG<200 KB在 GitHub 的 894px 显示宽度下清晰锐利。使用 /media-upload skill 上传,以获取 CDN URL
GitHub PR GIFgif-compact800×450 动态 GIF<10 MBDPR 为 1——瓶颈在于 GIF 的 256 色调色板,而非像素密度。使用 /media-upload skill 上传,以获取 CDN URL
视频(内部或面向客户)video2560×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
工作原理
  1. --session start 通过 Playwright 的 launchServer() 启动无头 Chromium
  2. 浏览器作为后台守护进程运行(分离进程)
  3. 会话信息写入 /tmp/playwright-session.json
  4. 运行脚本时,run.js 会自动检测会话并通过 WebSocket 连接
  5. 代码会获得预先配置好的 browser、context 和 page 变量
  6. 脚本退出时,Cookie/localStorage/当前 URL 会保存到 /tmp/playwright-session-state.json
  7. 下一个脚本会重新连接并恢复状态——认证状态和 URL 保持不变,可继续操作
代码可用的内容

在会话模式下,代码中预先定义了以下变量:

变量说明
browser已连接的浏览器实例(在多次运行之间保持)
context浏览器上下文,包含从上次运行恢复的 Cookie/localStorage
page已导航至上次运行时最后一个 URL 的页面(首次运行时为空白)
saveState退出前调用,以持久化 Cookie/localStorage/URL(包装器会自动调用)
helperslib/helpers 中的所有辅助函数
chromium, devicesPlaywright 导出项(用于创建其他上下文)
会话模式与无头模式(无会话)对比
会话模式(默认)无头模式 — 无会话
浏览器启动一次(执行 --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 })

评论

支持 Markdown