AI真好玩系列-Agent Skill深度调研⑰playwright | AI驱动的浏览器自动化测试神器
@[TOC]( AI真好玩系列-Agent Skill深度调研⑰playwright | AI驱动的浏览器自动化测试神器)
开头碎碎念
宝宝们又见面啦~👋 今天给大家带来一个重量级的 Agent Skill——playwright!
你有没有经历过这种崩溃时刻:辛辛苦苦写了一堆 Selenium 脚本,本地跑得好好的,一到 CI 环境就各种超时、元素找不到、页面没加载完...你盯着那个红色的 FAILED 发呆,心想"明明昨天还能跑的啊" 😅
更惨的是,老板说"我们的应用要兼容 Chrome、Firefox 和 Safari",你看了看 Selenium 的跨浏览器配置,再看了看那一堆 driver 版本兼容问题,内心OS:我直接辞职行不行?😭
还有更离谱的——测试脚本里到处都是 time.sleep(3),你也不知道为什么要等3秒,可能是2秒不够,5秒又太慢,反正3秒"差不多"能跑过。结果有一天服务器慢了一点,3秒又不够了...这就是传说中的 flaky test!😤
现在有了 playwright Skill,AI Agent 可以直接帮你写测试、跑测试、截图对比、模拟移动设备——你说"测试一下登录功能",它就帮你写好完整的端到端测试;你说"截个图看看页面长啥样",它直接给你截图;你说"模拟 iPhone 看看响应式效果",它立马给你模拟出来!这才是浏览器自动化该有的样子!🚀
废话不多说,让我带你从原理到实战,彻底搞懂这个 AI 驱动的浏览器自动化测试神器~ 🎯
🌟 项目简介 | Project Introduction
playwright 是微软开源的浏览器自动化测试框架,作为 Agent Skill 使用时,它让 AI Agent 能够进行端到端测试、浏览器自动化操作、网页截图、网络拦截、移动设备模拟等。它支持 Chromium、Firefox 和 WebKit 三大浏览器引擎,内置自动等待机制,彻底告别 flaky 测试。
- 核心定位:AI Agent 的浏览器自动化能力引擎
- 开发团队:微软(Microsoft)
- 开源协议:Apache-2.0
- 支持语言:Python / JavaScript / TypeScript / Java / .NET
- 浏览器引擎:Chromium / Firefox / WebKit
- 适用场景:端到端测试、浏览器自动化、网页截图、爬虫、UI验证
核心特性一览
| 特性 | 说明 |
|---|---|
| 多浏览器引擎 | 一套代码跑 Chromium/Firefox/WebKit 三大引擎 |
| 自动等待 | 内置智能等待,无需手动 sleep |
| 网络拦截 | 拦截和修改网络请求,Mock API 响应 |
| 移动设备模拟 | 模拟 iPhone/iPad/Android 等设备 |
| 截图与录屏 | 页面截图、元素截图、录制测试视频 |
| 并行执行 | 多 Worker 并行跑测试,速度飞快 |
| 追踪查看器 | 测试失败时生成完整追踪,方便调试 |
| 代码生成 | 录制用户操作自动生成测试代码 |
| 选择器引擎 | CSS/XPath/text/role 等多种选择器 |
| 浏览器上下文 | 隔离的浏览器上下文,无需清理 Cookie |
🤔 为什么需要 playwright? | Why playwright?
痛点场景还原
痛点1:Selenium 的 flaky test 让人崩溃
你:测试脚本本地跑过了,提交代码!
CI:❌ FAILED - Element not found
你:??我本地明明能跑过啊
你:加个 sleep(3) 试试
CI:✅ PASSED
你(第二天):提交代码!
CI:❌ FAILED - Timeout after 30000ms
你:???3秒不够了?
你:改成 sleep(5) 吧...
你(内心):这不是测试,这是玄学 😭
痛点2:跨浏览器测试是噩梦
老板:我们的网站要兼容 Chrome、Firefox 和 Safari
你:好的,用 Selenium 配一下
你:ChromeDriver 版本要和 Chrome 版本匹配...
你:Firefox 要用 geckodriver...
你:Safari 要开启远程自动化...
你:三个浏览器的 Driver 版本还不统一...
你:CI 环境里还要单独安装...
你:我能不能只测 Chrome?😅
痛点3:测试数据隔离困难
你:测试用例A需要登录状态
你:测试用例B需要未登录状态
你:测试用例C需要特定用户数据
你:每个用例跑完要清理 Cookie、localStorage...
你:清理不干净就会影响下一个用例...
你:并行跑的时候更惨,互相干扰...
你:算了,串行跑吧,等1个小时 😭
痛点4:移动端测试成本高
老板:测试一下手机端的响应式效果
你:买个 iPhone 测一下?
你:买个 Android 测一下?
你:买个 iPad 测一下?
你:买个各种尺寸的设备...
你:我能不能用 Chrome DevTools 的模拟?
你:可以,但是不能自动化啊...
你:手动测一遍要2小时,每周都要测... 😭
对比表格
| 问题 | Selenium | Cypress | playwright Skill |
|---|---|---|---|
| 跨浏览器 | 需配置Driver | 仅Chromium | 三引擎开箱即用 ✔️ |
| 自动等待 | 手动sleep | 部分自动 | 全自动智能等待 ✔️ |
| 测试隔离 | 手动清理 | 有限 | 浏览器上下文隔离 ✔️ |
| 移动模拟 | 需Appium | 不支持 | 内置设备模拟 ✔️ |
| 网络拦截 | 需Proxy | 内置 | 内置Route API ✔️ |
| 执行速度 | 慢 | 中 | 快 ✔️ |
| 并行执行 | 需Grid | 内置 | 内置Worker ✔️ |
| 调试能力 | 有限 | Time Travel | Trace Viewer ✔️ |
| AI集成 | 无 | 无 | Agent Skill ✔️ |
| 学习成本 | 高 | 中 | 低(自然语言) ✔️ |
一句话总结:**playwright 让浏览器自动化从"玄学调试"变成"科学测试"!**🔬
📌 前提条件 | Prerequisites
- Node.js 环境:Node.js 18+(JavaScript/TypeScript 版本)或 Python 3.8+(Python 版本)
- 操作系统:Windows / macOS / Linux 均支持
- 浏览器引擎:playwright 安装时自动下载,无需手动配置
- Agent 框架:Claude Code / OpenClaw / Codex CLI 等任一平台
- 基础概念:了解 DOM 结构、CSS 选择器、HTTP 请求响应
🚀 核心技术栈 | Core Technologies
| 技术 | 版本 | 用途 | 链接 |
|---|---|---|---|
| playwright | 1.48+ | 浏览器自动化框架 | playwright.dev |
| Chromium | 最新 | Chrome/Edge 浏览器引擎 | chromium.org |
| Firefox | 最新 | Firefox 浏览器引擎 | mozilla.org |
| WebKit | 最新 | Safari 浏览器引擎 | webkit.org |
| pytest | 8.0+ | Python 测试框架 | pytest.org |
| pytest-playwright | 0.5+ | Playwright pytest 插件 | pypi.org/project/pyt… |
🧩 核心原理详解 | Core Principles
1. 多浏览器引擎架构 | Multi-Browser Engine Architecture
playwright 最核心的能力是统一了三大浏览器引擎的自动化接口。一套测试代码,可以在 Chromium、Firefox 和 WebKit 上无缝运行:
┌─────────────────────┐
│ Playwright API │
│ (统一自动化接口) │
└──────────┬──────────┘
│
┌──────────┴──────────┐
│ Playwright Core │
│ (跨引擎调度层) │
└──────────┬──────────┘
│
┌────────────────┼────────────────┐
│ │ │
┌─────────┴──────┐ ┌──────┴───────┐ ┌──────┴───────┐
│ Chromium │ │ Firefox │ │ WebKit │
│ (Chrome/Edge) │ │ (Firefox) │ │ (Safari) │
│ CDP协议 │ │ 自定义协议 │ │ 自定义协议 │
└────────────────┘ └──────────────┘ └──────────────┘
# 多浏览器引擎测试——一套代码跑三个浏览器
import pytest
from playwright.sync_api import Browser, Page, BrowserContext
# ===== 方式1:通过 pytest 参数化自动跑三个浏览器 =====
# pytest.ini 配置:
# [pytest]
# addopts = --browser chromium --browser firefox --browser webkit
# 测试文件无需修改,pytest-playwright 自动参数化
def test_login_on_all_browsers(page: Page):
"""在三个浏览器上测试登录功能"""
# 导航到登录页
page.goto("https://example.com/login")
# 填写用户名和密码
page.fill('input[name="username"]', "testuser")
page.fill('input[name="password"]', "password123")
# 点击登录按钮
page.click('button[type="submit"]')
# 验证登录成功——检查是否跳转到首页
page.wait_for_url("**/dashboard")
assert page.title() == "Dashboard - Example App"
# ===== 方式2:手动指定浏览器引擎 =====
from playwright.sync_api import sync_playwright
def test_cross_browser_manually():
"""手动控制三个浏览器引擎"""
with sync_playwright() as p:
# --- Chromium 引擎 ---
# 适用于:Chrome、Edge、Brave 等基于 Chromium 的浏览器
chromium = p.chromium.launch(headless=False)
chromium_page = chromium.new_page()
chromium_page.goto("https://example.com")
print(f"Chromium 标题: {chromium_page.title()}")
chromium_page.screenshot(path="chromium_screenshot.png")
chromium.close()
# --- Firefox 引擎 ---
# 适用于:Firefox 浏览器
firefox = p.firefox.launch(headless=False)
firefox_page = firefox.new_page()
firefox_page.goto("https://example.com")
print(f"Firefox 标题: {firefox_page.title()}")
firefox_page.screenshot(path="firefox_screenshot.png")
firefox.close()
# --- WebKit 引擎 ---
# 适用于:Safari 浏览器(macOS/iOS 的默认引擎)
webkit = p.webkit.launch(headless=False)
webkit_page = webkit.new_page()
webkit_page.goto("https://example.com")
print(f"WebKit 标题: {webkit_page.title()}")
webkit_page.screenshot(path="webkit_screenshot.png")
webkit.close()
# ===== 方式3:浏览器上下文隔离——同一浏览器多用户并行 =====
def test_browser_context_isolation():
"""演示浏览器上下文隔离——互不干扰"""
with sync_playwright() as p:
browser = p.chromium.launch()
# 用户A的上下文——已登录状态
context_a = browser.new_context()
page_a = context_a.new_page()
# 用户B的上下文——未登录状态
context_b = browser.new_context()
page_b = context_b.new_page()
# 用户A登录
page_a.goto("https://example.com/login")
page_a.fill('input[name="username"]', "user_a")
page_a.fill('input[name="password"]', "pass_a")
page_a.click('button[type="submit"]')
# 用户B访问同一网站——不会看到用户A的登录状态
page_b.goto("https://example.com")
# 用户B看到的是未登录的首页,而不是用户A的Dashboard
assert "Sign In" in page_b.text_content("body")
# 清理——每个上下文独立,互不影响
context_a.close()
context_b.close()
browser.close()
# ===== 方式4:连接已有浏览器实例 =====
def test_connect_existing_browser():
"""连接到已运行的浏览器——复用登录状态"""
with sync_playwright() as p:
# 连接到通过 CDP 暴露的 Chrome 实例
# 启动命令:chrome --remote-debugging-port=9222
browser = p.chromium.connect_over_cdp("http://localhost:9222")
# 获取已有上下文(包含你的登录状态)
context = browser.contexts[0]
page = context.new_page()
# 直接访问需要登录的页面——无需重新登录
page.goto("https://example.com/dashboard")
print(f"已登录页面标题: {page.title()}")
browser.close()
// JavaScript/TypeScript 版本——多浏览器引擎测试
const { test, expect } = require('@playwright/test');
// playwright.config.js 中配置多浏览器
// use: {
// browserName: ['chromium', 'firefox', 'webkit'],
// }
// 测试文件——自动在三个浏览器上运行
test('登录功能在所有浏览器上正常工作', async ({ page }) => {
// 导航到登录页
await page.goto('https://example.com/login');
// 填写表单
await page.fill('input[name="username"]', 'testuser');
await page.fill('input[name="password"]', 'password123');
// 点击登录
await page.click('button[type="submit"]');
// 验证跳转
await page.waitForURL('**/dashboard');
await expect(page).toHaveTitle(/Dashboard/);
});
// 手动指定浏览器引擎
test('Chromium 特有功能测试', async ({ browser }) => {
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('https://example.com');
// Chromium 特有的功能,如 CDP 会话
const cdpSession = await context.newCDPSession(page);
await cdpSession.send('Network.enable');
await context.close();
});
2. 自动等待机制 | Auto-Waiting Mechanism
playwright 最革命性的设计是内置了自动等待机制。在执行任何操作之前,playwright 会自动执行一系列可操作性检查(actionability checks),确保元素已经准备好被操作:
用户代码:page.click("#submit")
│
▼
┌───────────────────────┐
│ Actionability Checks │
│ (可操作性检查) │
└───────────┬───────────┘
│
┌───────────────┼───────────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌──────────┐ ┌──────────┐
│ 元素 │ │ 元素 │ │ 元素 │
│ 已附加 │ │ 可见 │ │ 稳定 │
│ 到DOM │ │ 不被遮挡 │ │ 不在动画 │
└────────┘ └──────────┘ └──────────┘
│ │ │
▼ ▼ ▼
┌────────┐ ┌──────────┐ ┌──────────┐
│ 元素 │ │ 元素 │ │ 元素 │
│ 已启用 │ │ 可接收 │ │ ...更多 │
│ 非禁用 │ │ 事件 │ │ 检查项 │
└────────┘ └──────────┘ └──────────┘
│
▼
┌───────────────┐
│ 执行点击操作 │
└───────────────┘
# 自动等待机制详解
from playwright.sync_api import sync_playwright, Page
# ===== 对比:Selenium vs Playwright =====
# Selenium 的做法——手动等待,容易出问题
"""
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
driver = webdriver.Chrome()
driver.get("https://example.com")
# 方式1:硬编码等待——不靠谱
import time
time.sleep(3) # 等多久?看运气...
driver.find_element(By.ID, "submit").click()
# 方式2:显式等待——好一点但很啰嗦
wait = WebDriverWait(driver, 10)
element = wait.until(EC.element_to_be_clickable((By.ID, "submit")))
element.click()
# 方式3:隐式等待——全局生效,不够灵活
driver.implicitly_wait(5)
"""
# Playwright 的做法——自动等待,零配置
def test_auto_waiting():
"""playwright 自动等待——无需手动 sleep"""
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto("https://example.com")
# playwright 自动等待:
# 1. 元素已附加到 DOM
# 2. 元素可见(不被遮挡)
# 3. 元素稳定(不在动画中)
# 4. 元素已启用(非 disabled)
# 5. 元素可接收事件
# 所有条件满足后才执行点击
page.click("#submit") # 就这么简单!无需手动等待
# 自动等待也适用于 fill 操作
page.fill("#username", "testuser") # 自动等待输入框可用
# 自动等待也适用于导航
page.click("a[href='/about']") # 自动等待链接可点击
# 点击后自动等待导航完成
browser.close()
# ===== 自定义等待条件 =====
def test_custom_waiting():
"""自定义等待条件——灵活控制等待逻辑"""
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto("https://example.com")
# 方式1:等待特定元素出现
page.wait_for_selector(".loaded", state="visible")
# 方式2:等待导航完成
page.wait_for_url("**/dashboard")
# 方式3:等待网络请求完成
page.wait_for_load_state("networkidle") # 网络空闲
# 方式4:等待自定义条件
page.wait_for_function("""
// 等待页面上的数据加载完成
() => document.querySelectorAll('.data-row').length > 0
""")
# 方式5:等待特定网络响应
with page.expect_response("**/api/data") as response_info:
page.click("#load-data")
response = response_info.value
assert response.status == 200
# 方式6:等待弹窗出现
with page.expect_popup() as popup_info:
page.click("#open-popup")
popup = popup_info.value
popup.wait_for_load_state()
browser.close()
# ===== 超时配置 =====
def test_timeout_configuration():
"""超时配置——灵活控制等待时间"""
with sync_playwright() as p:
# 全局超时配置
browser = p.chromium.launch()
context = browser.new_context()
page = context.new_page()
# 设置默认导航超时(毫秒)
page.set_default_navigation_timeout(30000) # 30秒
# 设置默认超时(毫秒)
page.set_default_timeout(10000) # 10秒
# 单次操作自定义超时
page.goto("https://example.com", timeout=60000) # 这次等60秒
page.click("#submit", timeout=5000) # 这次只等5秒
browser.close()
# ===== 重试机制 =====
def test_retry_mechanism():
"""playwright 的重试机制——自动重试失败的操作"""
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto("https://example.com")
# playwright 内部会自动重试操作
# 如果元素暂时不可用,会重试直到超时
# 比如元素正在动画中,会等动画结束后再操作
page.click(".animated-button") # 自动等待动画结束
# 对于断言,playwright 也提供了自动重试
from playwright.sync_api import expect
# expect 会自动重试断言,直到超时
# 不需要手动写 while 循环
expect(page.locator(".status")).to_have_text("Success", timeout=10000)
browser.close()
# 自动等待的完整可操作性检查列表
"""
playwright 在执行操作前自动执行的检查项:
1. Attached(已附加)
- 元素必须已附加到 DOM 树
- 不在 detached 状态
2. Visible(可见)
- 元素必须有非零尺寸
- 不被 display:none 隐藏
- 不被 visibility:hidden 隐藏
- 不被其他元素完全遮挡
3. Stable(稳定)
- 元素不在动画中
- 元素位置和尺寸不再变化
- 至少连续两帧保持不动
4. Enabled(已启用)
- 元素不是 disabled 状态
- button、input 等表单元素可交互
5. Receives Events(可接收事件)
- 元素是事件的目标
- 没有其他元素覆盖在上面拦截点击
- 比如透明的 overlay 不会阻止点击
6. Editable(可编辑)—— 仅 fill 操作
- 元素不是 readonly
- 元素不是 disabled
这些检查全部通过后,playwright 才会执行操作。
如果超时未通过,则抛出 TimeoutError。
"""
# 实际案例:对比 Selenium 和 Playwright 处理动态加载
def test_dynamic_loading_comparison():
"""对比 Selenium 和 Playwright 处理动态加载"""
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
# 场景:点击按钮后,数据通过 AJAX 加载
page.goto("https://example.com/data")
# 点击加载数据按钮
page.click("#load-data")
# Selenium 的做法:
# time.sleep(5) # 等多久?不知道...
# 或者
# wait.until(EC.presence_of_element_located((By.ID, "data-table")))
# Playwright 的做法:
# 自动等待数据表格出现
page.click("#data-table .first-row") # 自动等待表格出现并可点击
# 或者更精确地等待
page.wait_for_selector("#data-table", state="attached")
# state 选项:
# - "attached":元素已附加到 DOM(可能不可见)
# - "visible":元素可见
# - "hidden":元素隐藏或不存在
# - "detached":元素已从 DOM 移除
browser.close()
3. 网络拦截 | Network Interception
playwright 提供了强大的网络拦截能力,可以拦截、修改、Mock 网络请求和响应,这在测试中极为有用:
# 网络拦截详解
from playwright.sync_api import sync_playwright, Page, Route
# ===== 基础请求拦截 =====
def test_intercept_requests():
"""拦截和修改网络请求"""
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
# --- 拦截所有请求 ---
def handle_route(route: Route):
"""请求拦截处理器"""
# 打印请求信息
print(f"请求: {route.request.method} {route.request.url}")
# 继续发送请求(可以修改请求参数)
route.continue_()
# 注册拦截器——拦截所有请求
page.route("**/*", handle_route)
page.goto("https://example.com")
# --- 拦截特定请求 ---
# 只拦截 API 请求
page.route("**/api/**", lambda route: route.continue_())
# --- 修改请求头 ---
def add_auth_header(route: Route):
"""给请求添加认证头"""
headers = route.request.headers
headers["Authorization"] = "Bearer test-token-123"
route.continue_(headers=headers)
page.route("**/api/**", add_auth_header)
# --- 修改请求体 ---
def modify_request_body(route: Route):
"""修改 POST 请求体"""
if route.request.method == "POST":
# 获取原始请求体
original_body = route.request.post_data
print(f"原始请求体: {original_body}")
# 修改请求体
new_body = '{"username": "mock_user", "password": "mock_pass"}'
route.continue_(post_data=new_body)
else:
route.continue_()
page.route("**/api/login", modify_request_body)
# --- 阻止请求 ---
# 阻止加载图片——加速测试
page.route("**/*.{png,jpg,jpeg,gif,svg,webp}", lambda route: route.abort())
# 阻止加载字体
page.route("**/*.{woff,woff2,ttf,otf}", lambda route: route.abort())
# 阻止第三方追踪脚本
page.route("**/analytics/**", lambda route: route.abort())
page.route("**/tracking/**", lambda route: route.abort())
page.goto("https://example.com")
browser.close()
# ===== Mock API 响应 =====
def test_mock_api_responses():
"""Mock API 响应——无需真实后端"""
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
# --- Mock 登录 API ---
def mock_login(route: Route):
"""Mock 登录接口返回成功"""
if "login" in route.request.url:
route.fulfill(
status=200,
content_type="application/json",
body='{"success": true, "token": "mock-jwt-token", "user": {"name": "测试用户"}}',
)
else:
route.continue_()
page.route("**/api/auth/**", mock_login)
# --- Mock 用户数据 API ---
def mock_user_data(route: Route):
"""Mock 用户数据接口"""
route.fulfill(
status=200,
content_type="application/json",
body="""{
"users": [
{"id": 1, "name": "张三", "email": "zhangsan@example.com"},
{"id": 2, "name": "李四", "email": "lisi@example.com"},
{"id": 3, "name": "王五", "email": "wangwu@example.com"}
],
"total": 3,
"page": 1
}""",
)
page.route("**/api/users", mock_user_data)
# --- Mock 错误响应 ---
def mock_server_error(route: Route):
"""Mock 服务器错误——测试错误处理"""
route.fulfill(
status=500,
content_type="application/json",
body='{"error": "Internal Server Error", "message": "服务器开小差了"}',
)
page.route("**/api/error", mock_server_error)
# --- Mock 延迟响应 ---
def mock_slow_api(route: Route):
"""Mock 慢速 API——测试加载状态"""
import time
time.sleep(3) # 模拟3秒延迟
route.fulfill(
status=200,
content_type="application/json",
body='{"data": "slow response"}',
)
page.route("**/api/slow", mock_slow_api)
# --- Mock 文件下载 ---
def mock_file_download(route: Route):
"""Mock 文件下载"""
route.fulfill(
status=200,
content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": "attachment; filename=report.xlsx"},
body=b"mock-excel-file-content",
)
page.route("**/api/download/report", mock_file_download)
page.goto("https://example.com")
browser.close()
# ===== 高级网络拦截 =====
def test_advanced_network_interception():
"""高级网络拦截——请求修改、响应篡改"""
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
# --- 拦截并修改响应 ---
def modify_response(route: Route):
"""拦截真实响应并修改内容"""
# 先获取真实响应
response = route.fetch()
# 修改响应体
body = response.text()
# 替换页面中的某些文字
modified_body = body.replace("原价", "限时优惠价")
modified_body = modified_body.replace("¥999", "¥599")
# 返回修改后的响应
route.fulfill(
response=response,
body=modified_body,
)
page.route("**/product/**", modify_response)
# --- 录制网络请求 ---
requests_log = []
def log_requests(route: Route):
"""记录所有网络请求"""
requests_log.append({
"method": route.request.method,
"url": route.request.url,
"headers": dict(route.request.headers),
"post_data": route.request.post_data,
})
route.continue_()
page.route("**/*", log_requests)
# --- 条件性拦截 ---
def conditional_intercept(route: Route):
"""根据条件决定是否拦截"""
url = route.request.url
# 生产环境 API 转发到测试环境
if "api.production.com" in url:
new_url = url.replace("api.production.com", "api.staging.com")
route.continue_(url=new_url)
# 第三方追踪脚本直接阻止
elif any(domain in url for domain in ["google-analytics", "facebook.net", "doubleclick"]):
route.abort()
else:
route.continue_()
page.route("**/*", conditional_intercept)
page.goto("https://example.com")
# 查看录制的请求
for req in requests_log:
print(f"{req['method']} {req['url']}")
browser.close()
# ===== HAR 文件录制和回放 =====
def test_har_recording():
"""HAR 文件录制和回放——完美复现网络行为"""
with sync_playwright() as p:
browser = p.chromium.launch()
# --- 录制 HAR ---
context = browser.new_context(
record_har_path="network.har" # 录制到 HAR 文件
)
page = context.new_page()
page.goto("https://example.com")
page.click("#load-data")
context.close() # 关闭时自动保存 HAR 文件
# --- 回放 HAR ---
# 使用录制的 HAR 文件回放网络请求
context_replay = browser.new_context(
record_har_path=None,
)
page_replay = context_replay.new_page()
# 用录制的 HAR 响应替代真实网络请求
page_replay.route_from_har("network.har", not_found="abort")
page_replay.goto("https://example.com")
# 所有网络请求都从 HAR 文件中获取,无需真实网络
browser.close()
4. 移动设备模拟 | Mobile Device Emulation
playwright 内置了丰富的移动设备模拟能力,可以模拟各种手机和平板的屏幕尺寸、用户代理、触摸事件等:
# 移动设备模拟详解
from playwright.sync_api import sync_playwright, BrowserContext
# ===== 预置设备模拟 =====
def test_mobile_emulation():
"""使用预置设备配置模拟移动端"""
with sync_playwright() as p:
# --- iPhone 14 Pro ---
iphone = p.devices["iPhone 14 Pro"]
context = browser.new_context(**iphone)
page = context.new_page()
page.goto("https://example.com")
page.screenshot(path="iphone14pro.png")
context.close()
# --- iPhone 15 Pro Max ---
iphone_max = p.devices["iPhone 15 Pro Max"]
context = browser.new_context(**iphone_max)
page = context.new_page()
page.goto("https://example.com")
page.screenshot(path="iphone15promax.png")
context.close()
# --- iPad Pro ---
ipad = p.devices["iPad Pro 11"]
context = browser.new_context(**ipad)
page = context.new_page()
page.goto("https://example.com")
page.screenshot(path="ipadpro.png")
context.close()
# --- Pixel 7 ---
pixel = p.devices["Pixel 7"]
context = browser.new_context(**pixel)
page = context.new_page()
page.goto("https://example.com")
page.screenshot(path="pixel7.png")
context.close()
# --- Galaxy S9+ ---
galaxy = p.devices["Galaxy S9+"]
context = browser.new_context(**galaxy)
page = context.new_page()
page.goto("https://example.com")
page.screenshot(path="galaxy_s9.png")
context.close()
# ===== 自定义设备模拟 =====
def test_custom_device_emulation():
"""自定义设备模拟参数"""
with sync_playwright() as p:
browser = p.chromium.launch()
# --- 自定义移动设备 ---
context = browser.new_context(
# 屏幕尺寸
viewport={"width": 375, "height": 812},
# 设备像素比(Retina 屏幕为 2 或 3)
device_scale_factor=3,
# 是否模拟触摸
has_touch=True,
# 是否为移动端
is_mobile=True,
# 用户代理字符串
user_agent="Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15",
# 地理位置权限
permissions=["geolocation"],
geolocation={"latitude": 39.9042, "longitude": 116.4074}, # 北京
# 语言和时区
locale="zh-CN",
timezone_id="Asia/Shanghai",
# 颜色方案
color_scheme="dark", # 深色模式
# 减少动画(辅助功能)
reduced_motion="reduce",
)
page = context.new_page()
page.goto("https://example.com")
# 截图对比——移动端 vs 桌面端
page.screenshot(path="mobile_custom.png")
context.close()
browser.close()
# ===== 触摸事件模拟 =====
def test_touch_events():
"""模拟触摸事件——滑动、缩放、长按"""
with sync_playwright() as p:
iphone = p.devices["iPhone 14 Pro"]
browser = p.chromium.launch()
context = browser.new_context(**iphone)
page = context.new_page()
page.goto("https://example.com")
# --- 模拟滑动(下拉刷新) ---
# 从屏幕中间向下滑动
page.mouse.move(187, 400) # 移动到屏幕中间
page.mouse.down()
page.mouse.move(187, 600, steps=10) # 向下滑动200px
page.mouse.up()
# --- 模拟缩放(双指缩放) ---
# Playwright 不直接支持双指缩放,但可以通过 JS 注入实现
page.evaluate("""
// 模拟双指缩放事件
const element = document.querySelector('.zoomable');
const event = new WheelEvent('wheel', {
deltaY: -100, // 负值=放大
ctrlKey: true, // Ctrl+滚轮=缩放
bubbles: true,
});
element.dispatchEvent(event);
""")
# --- 模拟长按 ---
# 长按500毫秒
element = page.locator(".long-press-target")
element.click(delay=500) # 按住500毫秒后释放
# --- 模拟滑动手势(使用 touch 接口) ---
page.evaluate("""
// 模拟左滑手势
const target = document.querySelector('.swipeable');
const touchStart = new TouchEvent('touchstart', {
touches: [{ clientX: 300, clientY: 200 }],
bubbles: true,
});
const touchEnd = new TouchEvent('touchend', {
changedTouches: [{ clientX: 100, clientY: 200 }],
bubbles: true,
});
target.dispatchEvent(touchStart);
setTimeout(() => target.dispatchEvent(touchEnd), 200);
""")
context.close()
browser.close()
# ===== 响应式测试矩阵 =====
def test_responsive_matrix():
"""响应式测试矩阵——一次测试多个屏幕尺寸"""
with sync_playwright() as p:
browser = p.chromium.launch()
# 定义测试矩阵
devices_config = [
{"name": "iPhone SE", "width": 375, "height": 667, "scale": 2},
{"name": "iPhone 14", "width": 390, "height": 844, "scale": 3},
{"name": "iPad Mini", "width": 768, "height": 1024, "scale": 2},
{"name": "iPad Pro", "width": 1024, "height": 1366, "scale": 2},
{"name": "Desktop", "width": 1920, "height": 1080, "scale": 1},
]
for device in devices_config:
context = browser.new_context(
viewport={"width": device["width"], "height": device["height"]},
device_scale_factor=device["scale"],
is_mobile=device["width"] < 768,
)
page = context.new_page()
page.goto("https://example.com")
# 截图保存
filename = f"responsive_{device['name'].replace(' ', '_').lower()}.png"
page.screenshot(path=filename, full_page=True)
# 检查关键元素是否可见
nav = page.locator("nav")
assert nav.is_visible(), f"{device['name']}: 导航栏不可见"
# 检查移动端汉堡菜单
if device["width"] < 768:
hamburger = page.locator(".mobile-menu-toggle")
assert hamburger.is_visible(), f"{device['name']}: 移动端菜单按钮不可见"
context.close()
browser.close()
print("响应式测试矩阵完成!已生成5个设备的截图")
# ===== 列出所有预置设备 =====
def list_all_devices():
"""列出 playwright 所有预置设备"""
with sync_playwright() as p:
devices = p.devices
print(f"共 {len(devices)} 个预置设备:")
print()
# 按品牌分组
brands = {}
for name, config in devices.items():
brand = name.split()[0] if " " in name else "Other"
brands.setdefault(brand, []).append(name)
for brand, device_names in sorted(brands.items()):
print(f"【{brand}】({len(device_names)}个)")
for name in sorted(device_names):
print(f" - {name}")
print()
# 运行查看所有设备
list_all_devices()
🛠️ 安装与使用 | Installation & Usage
安装方式1:pip 安装(Python 版本)
# 安装 playwright Python 包
pip install playwright
# 安装浏览器引擎(首次使用必须执行)
playwright install
# 只安装特定浏览器引擎
playwright install chromium # 只安装 Chromium
playwright install firefox # 只安装 Firefox
playwright install webkit # 只安装 WebKit
# 安装 pytest 插件
pip install pytest-playwright
# 验证安装
python -c "from playwright.sync_api import sync_playwright; print('Playwright Python OK!')"
playwright --version
安装方式2:npm 安装(JavaScript/TypeScript 版本)
# 创建项目目录
mkdir playwright-tests && cd playwright-tests
# 初始化项目
npm init -y
# 安装 playwright
npm init playwright@latest
# 交互式配置:
# ? Do you want to use TypeScript or JavaScript? TypeScript
# ? Where to put your end-to-end tests? tests
# ? Add a GitHub Actions workflow? true
# ? Install Playwright browsers (can be done manually via 'npx playwright install')? true
# 手动安装
npm install @playwright/test
npx playwright install
# 验证安装
npx playwright --version
安装方式3:Docker 一键部署
# 拉取官方 Docker 镜像(已包含浏览器引擎)
docker pull mcr.microsoft.com/playwright/python:v1.48.0-noble
# 运行测试
docker run -it --rm \
-v $(pwd)/tests:/app/tests \
-v $(pwd)/test-results:/app/test-results \
mcr.microsoft.com/playwright/python:v1.48.0-noble \
pytest /app/tests/ --headed
# JavaScript 版本
docker pull mcr.microsoft.com/playwright:v1.48.0-noble
docker run -it --rm \
-v $(pwd)/tests:/app/tests \
mcr.microsoft.com/playwright:v1.48.0-noble \
npx playwright test
使用示例1:基础端到端测试 | Basic E2E Test
"""
示例1:基础端到端测试
场景:测试一个电商网站的登录和购物流程
"""
from playwright.sync_api import sync_playwright, expect
def test_shopping_flow():
"""完整的购物流程测试"""
with sync_playwright() as p:
# 启动浏览器(headless=False 可以看到浏览器操作)
browser = p.chromium.launch(headless=False, slow_mo=500)
page = browser.new_page()
# ===== 步骤1:访问首页 =====
page.goto("https://demo.playwright.dev/todomvc")
page.wait_for_load_state("networkidle")
# ===== 步骤2:添加待办事项 =====
# 使用 fill + press 模拟用户输入
page.fill(".new-todo", "买牛奶")
page.press(".new-todo", "Enter")
page.fill(".new-todo", "买面包")
page.press(".new-todo", "Enter")
page.fill(".new-todo", "买鸡蛋")
page.press(".new-todo", "Enter")
# ===== 步骤3:验证待办事项数量 =====
todo_items = page.locator(".todo-list li")
expect(todo_items).to_have_count(3)
# ===== 步骤4:完成一个待办事项 =====
page.locator(".todo-list li").nth(0).locator(".toggle").check()
# 验证已完成数量
completed = page.locator(".todo-list li.completed")
expect(completed).to_have_count(1)
# ===== 步骤5:筛选待办事项 =====
page.click("text=Active") # 只显示未完成
expect(page.locator(".todo-list li")).to_have_count(2)
page.click("text=Completed") # 只显示已完成
expect(page.locator(".todo-list li")).to_have_count(1)
# ===== 步骤6:截图保存 =====
page.screenshot(path="shopping_flow.png", full_page=True)
browser.close()
# 运行命令:pytest test_shopping.py --headed
使用示例2:网页截图与PDF生成 | Screenshot & PDF
"""
示例2:网页截图与PDF生成
场景:生成网页截图和PDF报告
"""
from playwright.sync_api import sync_playwright
def test_screenshot_and_pdf():
"""网页截图和PDF生成"""
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto("https://example.com", wait_until="networkidle")
# ===== 1. 全页面截图 =====
page.screenshot(
path="full_page.png",
full_page=True, # 截取完整页面(包括滚动区域)
animations="disabled", # 禁用动画,确保截图稳定
)
# ===== 2. 视口截图(只截可见区域) =====
page.screenshot(path="viewport.png") # 默认只截视口
# ===== 3. 元素截图 =====
# 截取特定元素的截图
header = page.locator("header")
header.screenshot(path="header.png")
# 截取特定区域
main_content = page.locator("main")
main_content.screenshot(path="main_content.png")
# ===== 4. 多设备截图对比 =====
devices = [
{"name": "desktop", "viewport": {"width": 1920, "height": 1080}},
{"name": "tablet", "viewport": {"width": 768, "height": 1024}},
{"name": "mobile", "viewport": {"width": 375, "height": 812}},
]
for device in devices:
context = browser.new_context(viewport=device["viewport"])
device_page = context.new_page()
device_page.goto("https://example.com", wait_until="networkidle")
device_page.screenshot(
path=f"screenshot_{device['name']}.png",
full_page=True,
)
context.close()
# ===== 5. 生成PDF(仅Chromium支持) =====
page.pdf(
path="page.pdf",
format="A4", # A4纸张
print_background=True, # 打印背景色
margin={
"top": "20mm",
"right": "15mm",
"bottom": "20mm",
"left": "15mm",
},
)
# ===== 6. 录制视频 =====
context = browser.new_context(
record_video_dir="videos/", # 视频保存目录
record_video_size={"width": 1280, "height": 720},
)
video_page = context.new_page()
video_page.goto("https://example.com")
video_page.click("a")
video_page.fill("input", "test")
context.close() # 关闭时自动保存视频
browser.close()
# ===== 批量截图工具 =====
def batch_screenshot(urls: list, output_dir: str = "screenshots"):
"""批量截图工具——给AI Agent用的截图能力"""
import os
os.makedirs(output_dir, exist_ok=True)
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page(viewport={"width": 1920, "height": 1080})
results = []
for url in urls:
try:
page.goto(url, wait_until="networkidle", timeout=30000)
# 生成安全的文件名
safe_name = url.replace("https://", "").replace("http://", "")
safe_name = safe_name.replace("/", "_").replace(".", "_")[:50]
filepath = os.path.join(output_dir, f"{safe_name}.png")
page.screenshot(path=filepath, full_page=True)
results.append({"url": url, "status": "success", "path": filepath})
print(f"✅ 截图成功: {url}")
except Exception as e:
results.append({"url": url, "status": "error", "error": str(e)})
print(f"❌ 截图失败: {url} - {e}")
browser.close()
return results
# 使用示例
urls = [
"https://playwright.dev",
"https://example.com",
"https://github.com/microsoft/playwright",
]
results = batch_screenshot(urls)
使用示例3:网络拦截与Mock | Network Mock
"""
示例3:网络拦截与Mock
场景:测试前端页面,Mock后端API响应
"""
from playwright.sync_api import sync_playwright, Route
import json
def test_with_mock_api():
"""使用 Mock API 测试前端页面"""
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
# ===== Mock 商品列表 API =====
def mock_products(route: Route):
"""Mock 商品列表接口"""
mock_data = {
"products": [
{"id": 1, "name": "iPhone 15 Pro", "price": 8999, "stock": 100},
{"id": 2, "name": "MacBook Pro", "price": 14999, "stock": 50},
{"id": 3, "name": "AirPods Pro", "price": 1899, "stock": 200},
],
"total": 3,
}
route.fulfill(
status=200,
content_type="application/json",
body=json.dumps(mock_data),
)
page.route("**/api/products", mock_products)
# ===== Mock 购物车 API =====
def mock_cart(route: Route):
"""Mock 购物车接口"""
if route.request.method == "GET":
mock_data = {
"items": [
{"product_id": 1, "quantity": 2, "price": 8999},
],
"total": 17998,
}
route.fulfill(
status=200,
content_type="application/json",
body=json.dumps(mock_data),
)
elif route.request.method == "POST":
# 添加到购物车
route.fulfill(
status=201,
content_type="application/json",
body='{"success": true, "message": "已添加到购物车"}',
)
page.route("**/api/cart/**", mock_cart)
# ===== Mock 支付 API =====
def mock_payment(route: Route):
"""Mock 支付接口"""
route.fulfill(
status=200,
content_type="application/json",
body='{"payment_id": "pay_mock_123", "status": "success", "redirect_url": "/order/success"}',
)
page.route("**/api/payment", mock_payment)
# ===== 阻止不必要的请求(加速测试) =====
page.route("**/*.{png,jpg,jpeg,gif,svg}", lambda route: route.abort())
page.route("**/analytics/**", lambda route: route.abort())
# 现在可以测试前端页面了,无需真实后端
page.goto("https://example-shop.com")
page.wait_for_load_state("networkidle")
# 验证商品列表已加载
products = page.locator(".product-item")
# 因为 Mock 返回了3个商品,所以应该有3个商品元素
browser.close()
# ===== API 测试工具 =====
def test_api_with_playwright():
"""使用 playwright 发送 API 请求"""
from playwright.sync_api import APIRequestContext
with sync_playwright() as p:
# 创建 API 请求上下文
api_context = p.request.new_context(
base_url="https://api.example.com",
extra_http_headers={"Authorization": "Bearer test-token"},
)
# GET 请求
response = api_context.get("/users")
assert response.ok
users = response.json()
print(f"获取到 {len(users)} 个用户")
# POST 请求
response = api_context.post("/users", data={
"name": "测试用户",
"email": "test@example.com",
})
assert response.ok
new_user = response.json()
print(f"创建用户: {new_user['id']}")
# PUT 请求
response = api_context.put(f"/users/{new_user['id']}", data={
"name": "更新后的用户名",
})
assert response.ok
# DELETE 请求
response = api_context.delete(f"/users/{new_user['id']}")
assert response.ok
api_context.dispose()
使用示例4:移动端测试 | Mobile Testing
"""
示例4:移动端测试
场景:测试移动端网页的响应式布局和触摸交互
"""
from playwright.sync_api import sync_playwright, expect
def test_mobile_responsive():
"""移动端响应式测试"""
with sync_playwright() as p:
# ===== iPhone 14 Pro 测试 =====
iphone_14 = p.devices["iPhone 14 Pro"]
browser = p.chromium.launch()
context = browser.new_context(**iphone_14)
page = context.new_page()
page.goto("https://example.com", wait_until="networkidle")
# 验证移动端导航菜单
# 桌面端的导航栏应该被隐藏
desktop_nav = page.locator(".desktop-nav")
expect(desktop_nav).to_be_hidden()
# 移动端汉堡菜单应该可见
mobile_menu = page.locator(".mobile-menu-toggle")
expect(mobile_menu).to_be_visible()
# 点击汉堡菜单
mobile_menu.click()
# 验证菜单展开
mobile_nav = page.locator(".mobile-nav")
expect(mobile_nav).to_be_visible()
# 截图
page.screenshot(path="mobile_iphone14.png")
context.close()
# ===== iPad 测试 =====
ipad = p.devices["iPad Pro 11"]
context = browser.new_context(**ipad)
page = context.new_page()
page.goto("https://example.com", wait_until="networkidle")
# iPad 应该显示桌面端导航
desktop_nav = page.locator(".desktop-nav")
expect(desktop_nav).to_be_visible()
page.screenshot(path="mobile_ipad.png")
context.close()
browser.close()
def test_mobile_touch_interactions():
"""移动端触摸交互测试"""
with sync_playwright() as p:
iphone = p.devices["iPhone 14 Pro"]
browser = p.chromium.launch()
context = browser.new_context(**iphone)
page = context.new_page()
page.goto("https://example.com", wait_until="networkidle")
# ===== 下拉刷新 =====
# 模拟从上往下的滑动手势
page.mouse.move(187, 100) # 移动到屏幕顶部
page.mouse.down()
page.mouse.move(187, 400, steps=20) # 向下滑动
page.mouse.up()
# 等待刷新完成
page.wait_for_selector(".refresh-indicator", state="hidden")
# ===== 滑动轮播图 =====
carousel = page.locator(".carousel")
box = carousel.bounding_box()
if box:
# 从右向左滑动
start_x = box["x"] + box["width"] * 0.8
end_x = box["x"] + box["width"] * 0.2
y = box["y"] + box["height"] / 2
page.mouse.move(start_x, y)
page.mouse.down()
page.mouse.move(end_x, y, steps=10)
page.mouse.up()
# ===== 地理位置测试 =====
# 授予地理位置权限
context.grant_permissions(["geolocation"])
context.set_geolocation({"latitude": 31.2304, "longitude": 121.4737}) # 上海
page.goto("https://example.com/nearby")
# 验证附近店铺列表已加载
expect(page.locator(".nearby-store").first).to_be_visible()
context.close()
browser.close()
使用示例5:视觉回归测试 | Visual Regression Testing
"""
示例5:视觉回归测试
场景:对比页面截图,检测UI变化
"""
from playwright.sync_api import sync_playwright, expect
def test_visual_regression():
"""视觉回归测试——检测UI变化"""
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page(viewport={"width": 1280, "height": 720})
page.goto("https://example.com", wait_until="networkidle")
# ===== 全页面截图对比 =====
expect(page).to_have_screenshot("homepage.png", {
"max_diff_pixels": 100, # 允许最多100个像素差异
"threshold": 0.2, # 差异阈值(0-1)
"animations": "disabled", # 禁用动画
})
# ===== 元素截图对比 =====
header = page.locator("header")
expect(header).to_have_screenshot("header.png", {
"max_diff_pixels": 50,
})
# ===== 特定区域截图对比 =====
hero_section = page.locator(".hero-section")
expect(hero_section).to_have_screenshot("hero.png")
browser.close()
# ===== 首次运行:生成基准截图 =====
# pytest test_visual.py --update-snapshots
# ===== 后续运行:对比基准截图 =====
# pytest test_visual.py
# ===== 如果有差异,会生成三张图片 =====
# - homepage.png (当前截图)
# - homepage-expected.png (期望截图)
# - homepage-diff.png (差异对比图,红色标记差异区域)
def test_visual_regression_with_mask():
"""带遮罩的视觉回归测试——忽略动态区域"""
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto("https://example.com", wait_until="networkidle")
# 忽略动态内容区域(如广告、时间戳、随机推荐)
expect(page).to_have_screenshot("homepage_masked.png", {
"mask": [
page.locator(".ad-banner"), # 广告横幅
page.locator(".timestamp"), # 时间戳
page.locator(".random-recommend"), # 随机推荐
],
"max_diff_pixels": 100,
})
browser.close()
# ===== 多页面视觉回归测试 =====
def test_multi_page_visual_regression():
"""多页面视觉回归测试"""
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page(viewport={"width": 1280, "height": 720})
# 定义需要测试的页面
pages_to_test = {
"homepage": "/",
"about": "/about",
"products": "/products",
"contact": "/contact",
"blog": "/blog",
}
for name, path in pages_to_test.items():
page.goto(f"https://example.com{path}", wait_until="networkidle")
expect(page).to_have_screenshot(f"{name}.png", {
"max_diff_pixels": 200,
"animations": "disabled",
})
print(f"✅ {name} 页面视觉测试通过")
browser.close()
使用示例6:Trace Viewer 调试 | Trace Debugging
"""
示例6:Trace Viewer 调试
场景:测试失败时生成追踪文件,方便调试
"""
from playwright.sync_api import sync_playwright
def test_with_trace():
"""生成 Trace 文件用于调试"""
with sync_playwright() as p:
browser = p.chromium.launch()
context = browser.new_context()
# ===== 开始追踪 =====
context.tracing.start(screenshots=True, snapshots=True, sources=True)
page = context.new_page()
page.goto("https://example.com")
# 执行一些操作
page.fill("#search", "playwright")
page.press("#search", "Enter")
# ===== 停止追踪并保存 =====
context.tracing.stop(path="trace.zip")
# 查看追踪文件:
# npx playwright show-trace trace.zip
# 会打开一个网页界面,展示:
# - 每一步操作的截图
# - DOM 快照
# - 网络请求日志
# - 控制台日志
# - 操作时间线
context.close()
browser.close()
# ===== pytest 配置自动追踪 =====
# playwright.config.js / pytest.ini 配置:
# [pytest]
# addopts = --tracing on # 测试失败时自动生成追踪
# 或者在 Python 中配置:
# pytest --tracing on --tracing retain-on-failure
# ===== 编程式查看追踪 =====
def test_programmatic_trace_analysis():
"""编程式分析追踪数据"""
import zipfile
import json
# 读取 trace.zip 文件
with zipfile.ZipFile("trace.zip", "r") as z:
# 列出追踪文件中的所有文件
print("追踪文件内容:")
for name in z.namelist():
print(f" - {name}")
# 读取追踪元数据
if "trace.trace" in z.namelist():
with z.open("trace.trace") as f:
trace_data = json.loads(f.read())
print(f"\n追踪时间: {trace_data.get('startTime', 'N/A')}")
print(f"操作数量: {len(trace_data.get('actions', []))}")
# ===== 录制测试视频 =====
def test_record_video():
"""录制测试视频"""
with sync_playwright() as p:
browser = p.chromium.launch()
# 配置视频录制
context = browser.new_context(
record_video_dir="videos/",
record_video_size={"width": 1280, "height": 720},
)
page = context.new_page()
page.goto("https://example.com")
page.fill("#search", "playwright")
page.press("#search", "Enter")
page.wait_for_load_state("networkidle")
# 关闭上下文时自动保存视频
context.close()
# 获取视频文件路径
video_path = page.video.path()
print(f"视频已保存到: {video_path}")
browser.close()
🔧 高级配置 | Advanced Configuration
playwright.config.ts 完整配置
// playwright.config.ts —— 完整配置文件
const { defineConfig, devices } = require('@playwright/test');
module.exports = defineConfig({
// ===== 测试目录 =====
testDir: './tests',
// ===== 全局超时 =====
timeout: 30000, // 每个测试最长30秒
expect: {
timeout: 5000, // 断言超时5秒
},
// ===== 并行执行 =====
fullyParallel: true, // 完全并行执行
workers: process.env.CI ? 4 : 2, // CI环境4个worker,本地2个
// ===== 失败重试 =====
retries: process.env.CI ? 2 : 0, // CI环境重试2次,本地不重试
// ===== 报告器 =====
reporter: [
['html', { open: 'never' }], // HTML报告
['json', { outputFile: 'test-results.json' }], // JSON报告
['list'], // 控制台列表
],
// ===== 全局配置 =====
use: {
// 基础URL
baseURL: 'http://localhost:3000',
// 浏览器配置
headless: true, // 无头模式
viewport: { width: 1280, height: 720 },
// 截图配置
screenshot: 'only-on-failure', // 仅失败时截图
// 视频配置
video: 'retain-on-failure', // 仅失败时保留视频
// 追踪配置
trace: 'retain-on-failure', // 仅失败时保留追踪
// 网络配置
offline: false,
navigationTimeout: 15000, // 导航超时15秒
actionTimeout: 10000, // 操作超时10秒
},
// ===== 多浏览器项目配置 =====
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 7'] },
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 14 Pro'] },
},
{
name: 'iPad',
use: { ...devices['iPad Pro 11'] },
},
],
// ===== 本地开发服务器 =====
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI, // CI环境不复用已有服务器
timeout: 120000,
},
});
pytest 配置
# pytest.ini —— Python 版本配置
# [pytest]
# addopts =
# --headed # 有头模式(显示浏览器)
# --browser chromium # 使用 Chromium
# --browser firefox # 使用 Firefox
# --browser webkit # 使用 WebKit
# --slowmo 100 # 每步操作间隔100ms
# --tracing on # 开启追踪
# --screenshot only-on-failure # 仅失败时截图
# --video retain-on-failure # 仅失败时保留视频
# --output test-results # 输出目录
# -v # 详细输出
# --tb=short # 简短的错误回溯
# ===== conftest.py —— 自定义 fixtures =====
import pytest
from playwright.sync_api import Page, BrowserContext
@pytest.fixture
def context_with_storage(browser):
"""带登录状态的浏览器上下文"""
# 从文件加载存储状态(Cookie、localStorage等)
context = browser.new_context(storage_state="auth/storage_state.json")
yield context
context.close()
@pytest.fixture
def logged_in_page(context_with_storage):
"""已登录的页面"""
page = context_with_storage.new_page()
page.goto("https://example.com/dashboard")
yield page
page.close()
@pytest.fixture
def mobile_context(browser):
"""移动端浏览器上下文"""
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
iphone = p.devices["iPhone 14 Pro"]
context = browser.new_context(**iphone)
yield context
context.close()
# ===== 使用自定义 fixtures =====
def test_dashboard_with_login(logged_in_page):
"""使用已登录状态的测试"""
# 无需登录,直接测试Dashboard
assert "Dashboard" in logged_in_page.title()
expect(logged_in_page.locator(".user-name")).to_have_text("测试用户")
def test_mobile_homepage(mobile_context):
"""使用移动端上下文的测试"""
page = mobile_context.new_page()
page.goto("https://example.com")
expect(page.locator(".mobile-menu-toggle")).to_be_visible()
Agent 框架集成配置
# ===== Claude Code / OpenClaw 集成 =====
# SKILL.md 配置文件
SKILL_MD = """
# Playwright Browser Automation Skill
## Description
Use Playwright to automate browser interactions, take screenshots, run E2E tests,
and emulate mobile devices.
## Commands
### Take a screenshot of a webpage
```bash
npx playwright screenshot --viewport-size="1280,720" --full-page <URL> <output.png>
Run a Playwright test
npx playwright test <test-file> --headed
Generate test code by recording
npx playwright codegen <URL>
Check page accessibility
npx playwright test --project=chromium accessibility.spec.ts
Examples
- "Take a screenshot of example.com"
- "Run the login E2E test"
- "Test the mobile responsive layout"
- "Check if the homepage has any accessibility issues" """
===== LangChain 集成 =====
from langchain.agents import AgentExecutor, create_openai_tools_agent from langchain.tools import tool from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate
@tool def take_screenshot(url: str, output_path: str = "screenshot.png") -> str: """ 对网页进行截图。 参数: url: 要截图的网页URL output_path: 截图保存路径 返回:截图文件路径 """ from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page(viewport={"width": 1280, "height": 720})
page.goto(url, wait_until="networkidle", timeout=30000)
page.screenshot(path=output_path, full_page=True)
browser.close()
return f"截图已保存到 {output_path}"
@tool def run_e2e_test(test_file: str) -> str: """ 运行 Playwright 端到端测试。 参数: test_file: 测试文件路径 返回:测试结果 """ import subprocess result = subprocess.run( ["npx", "playwright", "test", test_file, "--reporter=list"], capture_output=True, text=True, timeout=120, ) return result.stdout if result.returncode == 0 else f"测试失败:\n{result.stderr}"
@tool def check_mobile_responsive(url: str) -> str: """ 检查网页的移动端响应式效果。 参数: url: 要检查的网页URL 返回:各设备的截图路径列表 """ from playwright.sync_api import sync_playwright
results = []
with sync_playwright() as p:
browser = p.chromium.launch()
devices = [
("iPhone 14 Pro", p.devices["iPhone 14 Pro"]),
("iPad Pro 11", p.devices["iPad Pro 11"]),
("Desktop", {"viewport": {"width": 1920, "height": 1080}}),
]
for name, config in devices:
context = browser.new_context(**config)
page = context.new_page()
page.goto(url, wait_until="networkidle", timeout=30000)
safe_name = name.replace(" ", "_").lower()
filepath = f"responsive_{safe_name}.png"
page.screenshot(path=filepath, full_page=True)
results.append(f"{name}: {filepath}")
context.close()
browser.close()
return "\n".join(results)
创建 Agent
llm = ChatOpenAI(model="gpt-4o", temperature=0)
prompt = ChatPromptTemplate.from_messages([ ("system", """你是一个浏览器自动化测试助手。你可以使用以下工具: - take_screenshot: 对网页截图 - run_e2e_test: 运行端到端测试 - check_mobile_responsive: 检查移动端响应式效果
请根据用户需求选择合适的工具。"""),
("human", "{input}"),
("placeholder", "{agent_scratchpad}"),
])
tools = [take_screenshot, run_e2e_test, check_mobile_responsive] agent = create_openai_tools_agent(llm, tools, prompt) agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
使用示例
result = agent_executor.invoke({ "input": "帮我检查 example.com 在手机端和桌面端的显示效果" }) print(result['output'])
## 📊 效果对比 | Before & After
### 对比1:Selenium vs Playwright
| 维度 | Selenium | Playwright |
|------|----------|------------|
| 安装配置 | 需下载Driver,版本匹配 | 一条命令搞定 ✔️ |
| 自动等待 | 手动sleep或显式等待 | 内置智能等待 ✔️ |
| 跨浏览器 | 需配置不同Driver | 三引擎统一API ✔️ |
| 测试隔离 | 手动清理Cookie | 浏览器上下文隔离 ✔️ |
| 执行速度 | 慢(WebDriver协议开销) | 快(直接CDP通信) ✔️ |
| 网络拦截 | 需Proxy | 内置Route API ✔️ |
| 移动模拟 | 需Appium | 内置设备模拟 ✔️ |
| 调试能力 | 有限 | Trace Viewer ✔️ |
| 代码生成 | Selenium IDE | Playwright Codegen ✔️ |
| API设计 | 命令式 | 自动等待+声明式 ✔️ |
### 对比2:Cypress vs Playwright
| 维度 | Cypress | Playwright |
|------|---------|------------|
| 浏览器支持 | 仅Chromium系 | Chromium/Firefox/WebKit ✔️ |
| 测试语言 | 仅JavaScript | Python/JS/Java/.NET ✔️ |
| 并行执行 | 需付费Dashboard | 内置Worker并行 ✔️ |
| 多标签页 | 不支持 | 原生支持 ✔️ |
| 多域名 | 不支持 | 原生支持 ✔️ |
| 移动模拟 | 不支持 | 内置设备模拟 ✔️ |
| 执行速度 | 中 | 快 ✔️ |
| 社区生态 | 成熟 | 快速增长 ✔️ |
| 学习曲线 | 低 | 低 ✔️ |
| 开源免费 | 基础版免费 | 完全免费 ✔️ |
### 对比3:开发效率
| 任务 | Selenium | Cypress | Playwright |
|------|----------|---------|------------|
| 环境搭建 | 2小时 | 30分钟 | 10分钟 ✔️ |
| 简单登录测试 | 30分钟 | 15分钟 | 10分钟 ✔️ |
| 跨浏览器测试 | 1天 | 不支持 | 30分钟 ✔️ |
| 移动端测试 | 需Appium | 不支持 | 15分钟 ✔️ |
| 网络Mock | 需Proxy | 内置 | 内置 ✔️ |
| 视觉回归测试 | 需第三方 | 需第三方 | 内置 ✔️ |
| 调试失败测试 | 困难 | Time Travel | Trace Viewer ✔️ |
| CI集成 | 复杂 | 简单 | 简单 ✔️ |
## 🎨 定制项 | Customization Options
| 项目 | 修改方法 | 效果预览 |
|------|----------|----------|
| 浏览器引擎 | `browser: chromium/firefox/webkit` | 切换三大浏览器引擎 |
| 视口尺寸 | `viewport: {width: 1280, height: 720}` | 自定义窗口大小 |
| 设备像素比 | `deviceScaleFactor: 2` | Retina 屏幕模拟 |
| 深色模式 | `colorScheme: "dark"` | 模拟深色模式 |
| 地理位置 | `geolocation: {latitude, longitude}` | 模拟GPS定位 |
| 语言区域 | `locale: "zh-CN"` | 模拟中文环境 |
| 时区 | `timezoneId: "Asia/Shanghai"` | 模拟上海时区 |
| 权限 | `permissions: ["geolocation"]` | 授予/拒绝权限 |
| 用户代理 | `userAgent: "custom-ua"` | 自定义UA字符串 |
| HTTP认证 | `httpCredentials: {username, password}` | Basic Auth |
| 离线模式 | `offline: true` | 模拟断网环境 |
| 代理 | `proxy: {server, username, password}` | HTTP代理 |
| 存储状态 | `storageState: "state.json"` | 复用登录状态 |
| 录制视频 | `recordVideo: {dir, size}` | 录制测试视频 |
| 追踪 | `tracing: {screenshots, snapshots}` | 开启操作追踪 |
| 截图 | `screenshot: "only-on-failure"` | 失败时截图 |
| 超时 | `timeout: 30000` | 自定义超时时间 |
| 慢动作 | `slowMo: 500` | 每步间隔500ms |
| 无头模式 | `headless: false` | 显示浏览器窗口 |
## 🏗️ 架构对比 | Architecture Comparison
### vs Selenium
| 维度 | Selenium | Playwright |
|------|----------|------------|
| 架构 | WebDriver协议 | 自定义协议(CDP等) |
| 通信方式 | HTTP REST API | WebSocket长连接 ✔️ |
| 等待机制 | 手动/显式等待 | 自动智能等待 ✔️ |
| 浏览器支持 | 需各浏览器Driver | 内置三引擎 ✔️ |
| 执行速度 | 慢(HTTP开销) | 快(WebSocket) ✔️ |
| 测试隔离 | 手动管理 | 浏览器上下文 ✔️ |
| 网络控制 | 需Proxy | 内置Route API ✔️ |
| 调试能力 | 有限 | Trace Viewer ✔️ |
| 移动端 | 需Appium | 内置设备模拟 ✔️ |
| 社区 | 非常成熟 | 快速增长 |
| 类比 | 手动挡汽车 | 自动驾驶 🚗 |
### vs Cypress
| 维度 | Cypress | Playwright |
|------|---------|------------|
| 架构 | 注入浏览器内运行 | 进程外控制浏览器 |
| 浏览器支持 | 仅Chromium | 三引擎 ✔️ |
| 跨域支持 | 有限制 | 无限制 ✔️ |
| 多标签页 | 不支持 | 支持 ✔️ |
| 多浏览器实例 | 不支持 | 支持 ✔️ |
| 语言支持 | 仅JavaScript | Python/JS/Java/.NET ✔️ |
| 执行速度 | 中 | 快 ✔️ |
| 调试体验 | Time Travel | Trace Viewer ✔️ |
| 移动端 | 不支持 | 内置模拟 ✔️ |
| 开源免费 | 基础版免费 | 完全免费 ✔️ |
| 类比 | 单车道公路 | 多车道高速 🛣️ |
### vs Puppeteer
| 维度 | Puppeteer | Playwright |
|------|-----------|------------|
| 开发团队 | Google | Microsoft |
| 浏览器支持 | 仅Chromium | 三引擎 ✔️ |
| 自动等待 | 有限 | 完整 ✔️ |
| 测试框架 | 无内置 | 内置test runner ✔️ |
| 选择器 | CSS/XPath | CSS/XPath/text/role ✔️ |
| 网络拦截 | 有限 | 完整Route API ✔️ |
| 移动端 | 有限 | 完整设备模拟 ✔️ |
| 追踪调试 | 有限 | Trace Viewer ✔️ |
| 社区 | 大 | 快速增长 |
| 类比 | Chrome专用工具 | 全能浏览器工具箱 🧰 |
## 🐛 常见问题 | Troubleshooting
### 1. 浏览器安装失败?
```bash
# 问题:playwright install 下载浏览器失败
# 方案1:使用国内镜像
PLAYWRIGHT_DOWNLOAD_HOST=https://npmmirror.com/mirrors/playwright playwright install
# 方案2:只安装需要的浏览器
playwright install chromium # 只安装 Chromium
# 方案3:手动下载浏览器
# 设置环境变量指定浏览器路径
PLAYWRIGHT_BROWSERS_PATH=/path/to/browsers playwright install
# 方案4:Docker 环境中安装
docker run -it mcr.microsoft.com/playwright/python bash
playwright install --with-deps chromium # 同时安装系统依赖
2. 元素找不到?
# 问题:page.click("#submit") 报错 Element not found
# 方案1:使用更灵活的选择器
page.click("text=提交") # 文本选择器
page.click("[data-testid=submit]") # data-testid 选择器
page.click("button:has-text('提交')") # 组合选择器
page.get_by_role("button", name="提交") # 角色选择器(推荐)
page.get_by_text("提交") # 文本匹配
# 方案2:等待元素出现
page.wait_for_selector("#submit", state="visible")
page.click("#submit")
# 方案3:调试选择器
# 使用 Playwright Inspector 调试
# PWDEBUG=1 pytest test_file.py
# 方案4:使用 locator 链式调用
page.locator("form").locator("button").first.click()
3. 超时问题?
# 问题:测试经常超时
# 方案1:增加全局超时
page.set_default_timeout(30000) # 30秒
page.set_default_navigation_timeout(60000) # 导航60秒
# 方案2:单次操作自定义超时
page.goto("https://slow-site.com", timeout=60000)
page.click("#submit", timeout=15000)
# 方案3:等待特定条件而非固定时间
# 不要用 time.sleep
# 用 wait_for_* 系列方法
page.wait_for_load_state("networkidle")
page.wait_for_selector(".loaded")
page.wait_for_url("**/dashboard")
# 方案4:CI 环境增加超时
# pytest --timeout=120 # 每个测试最长120秒
4. CI 环境运行失败?
# GitHub Actions 配置示例
name: Playwright Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright/python:v1.48.0-noble
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: pip install -r requirements.txt
- name: Install Playwright browsers
run: playwright install --with-deps
- name: Run tests
run: pytest tests/ --headed
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: |
test-results/
trace.zip
5. 并行测试冲突?
# 问题:并行测试时数据互相干扰
# 方案1:使用浏览器上下文隔离
# 每个测试用独立的浏览器上下文
def test_isolated(browser):
context = browser.new_context() # 独立上下文
page = context.new_page()
page.goto("https://example.com")
# ... 测试 ...
context.close() # 关闭时自动清理
# 方案2:使用存储状态复用登录
# 先保存登录状态
def test_save_auth_state(browser):
context = browser.new_context()
page = context.new_page()
page.goto("https://example.com/login")
page.fill("#username", "testuser")
page.fill("#password", "password")
page.click("#submit")
context.storage_state(path="auth.json") # 保存登录状态
context.close()
# 后续测试复用登录状态
def test_with_saved_auth(browser):
context = browser.new_context(storage_state="auth.json")
page = context.new_page()
page.goto("https://example.com/dashboard")
# 已经是登录状态,无需重新登录
context.close()
# 方案3:使用 Worker 级别的 fixture
# conftest.py
import pytest
@pytest.fixture(scope="worker")
def worker_context(browser):
"""每个 Worker 独立的浏览器上下文"""
context = browser.new_context()
yield context
context.close()
6. 处理弹窗和对话框?
# 问题:如何处理 alert/confirm/prompt 弹窗
# 方案1:监听 dialog 事件
page.on("dialog", lambda dialog: dialog.accept())
# 方案2:带输入的弹窗
def handle_dialog(dialog):
print(f"弹窗类型: {dialog.type}")
print(f"弹窗消息: {dialog.message}")
dialog.accept("输入内容") # 接受并输入
page.on("dialog", handle_dialog)
page.click("#trigger-prompt")
# 方案3:取消弹窗
page.on("dialog", lambda dialog: dialog.dismiss())
# 方案4:文件下载
with page.expect_download() as download_info:
page.click("#download-button")
download = download_info.value
print(f"下载文件: {download.suggested_filename}")
download.save_as("downloaded_file.pdf")
📚 扩展学习资源 | Extended Resources
官方文档
- Playwright 官方文档 - 最全面的参考
- Playwright Python 文档 - Python 版本专用
- Playwright API 参考 - 完整 API 列表
- Playwright Test Runner - 测试运行器文档
- Playwright Trace Viewer - 追踪调试器
教程与指南
- Playwright 入门教程 - 编写第一个测试
- 最佳实践指南 - 测试编写最佳实践
- 选择器指南 - 选择器使用指南
- 调试指南 - 调试技巧大全
- CI 集成指南 - GitHub/GitLab CI 配置
社区资源
- Playwright GitHub - 源码和 Issue
- Playwright Discord - 社区讨论
- Awesome Playwright - 资源汇总
- Playwright 视频 - 官方视频教程
Agent 框架集成
- Claude Code 文档 - Claude Code 使用指南
- OpenClaw 文档 - OpenClaw Agent 框架
- LangChain Tools - LangChain 工具集成
- CrewAI 文档 - CrewAI Agent 框架
Conclusion | 结语
-
That's all for today~ - | 今天就写到这里啦~
-
Guys, ( ̄ω ̄( ̄ω ̄〃 ( ̄ω ̄〃)ゝ See you tomorrow~~ | 小伙伴们,( ̄ω ̄( ̄ω ̄〃 ( ̄ω ̄〃)ゝ我们明天再见啦~~
-
Everyone, be happy every day! 大家要天天开心哦
-
Welcome everyone to point out any mistakes in the article~ | 欢迎大家指出文章需要改正之处~
-
Learning has no end; win-win cooperation | 学无止境,合作共赢
-
Welcome all the passers-by, boys and girls, to offer better suggestions! ~~~ | 欢迎路过的小哥哥小姐姐们提出更好的意见哇~~