Playwright是微软推出的现代化浏览器自动化框架,它允许开发者通过代码控制真实浏览器完成各种操作。作为一名长期从事Web自动化测试的工程师,我发现Playwright相比传统工具(如Selenium)在稳定性和功能丰富度上都有显著提升。
智能自动等待机制是Playwright最令我印象深刻的功能。在实际项目中,我们不再需要手动添加各种sleep语句。例如,当执行click()操作时,Playwright会自动检查元素是否:
这种机制使得测试脚本更加稳定可靠。我曾经在一个电商项目中对比过,使用Playwright后测试用例的通过率从85%提升到了98%。
强大的选择器引擎提供了多种定位元素的方式:
python复制# 通过角色定位(最推荐)
page.get_by_role("button", name="提交")
# 通过标签文本定位
page.get_by_label("用户名")
# 通过占位符定位
page.get_by_placeholder("请输入手机号")
# 通过测试ID定位(需要开发配合)
page.get_by_test_id("login-submit")
在最近的一个金融项目中,我们使用角色定位方式,即使前端频繁修改DOM结构,测试脚本也无需经常调整。
对于Python项目,我推荐使用virtualenv创建隔离环境:
bash复制python -m venv playwright-env
source playwright-env/bin/activate # Linux/Mac
playwright-env\Scripts\activate # Windows
pip install playwright
playwright install chromium # 只安装Chromium浏览器
提示:在生产环境中,我通常会固定Playwright版本以避免意外升级带来的兼容性问题。例如:
pip install playwright==1.40.0
创建一个基本的测试脚本test_login.py:
python复制from playwright.sync_api import sync_playwright
def test_login():
with sync_playwright() as p:
# 启动浏览器
browser = p.chromium.launch(headless=False, slow_mo=100)
# 创建上下文和页面
context = browser.new_context()
page = context.new_page()
# 导航到测试页面
page.goto("https://example.com/login")
# 填写登录表单
page.get_by_label("用户名").fill("testuser")
page.get_by_label("密码").fill("password")
page.get_by_role("button", name="登录").click()
# 验证登录成功
assert page.url == "https://example.com/dashboard"
# 关闭浏览器
context.close()
browser.close()
if __name__ == "__main__":
test_login()
实际经验分享:
slow_mo参数在调试时非常有用,可以放慢操作速度便于观察browser.new_context(),这比创建多个浏览器实例更轻量headless=TruePlaywright强大的网络拦截功能可以帮助我们测试各种边界情况:
python复制def test_payment_with_mocked_api():
def handle_route(route):
if "/api/payment" in route.request.url:
route.fulfill(
status=200,
content_type="application/json",
body='{"status": "success"}'
)
else:
route.continue_()
page.route("**/*", handle_route)
# 执行支付操作
# 此时真实的支付API不会被调用,而是返回我们mock的响应
这个技巧在我们测试支付流程时非常有用,可以避免产生真实的交易记录。
测试文件下载功能时,可以使用以下模式:
python复制with page.expect_download() as download_info:
page.get_by_text("导出报表").click()
download = download_info.value
path = download.path() # 获取临时文件路径
file_name = download.suggested_filename # 获取建议文件名
# 将文件移动到指定位置
download.save_as(f"/tmp/{file_name}")
Playwright支持多浏览器测试,只需简单修改启动参数:
python复制browsers = {
"chromium": p.chromium,
"firefox": p.firefox,
"webkit": p.webkit
}
for name, browser_type in browsers.items():
browser = browser_type.launch()
page = browser.new_page()
# 执行测试逻辑
browser.close()
在实际项目中,我们会在CI流水线中并行运行这些测试,大幅缩短测试时间。
Playwright MCP(Model Context Protocol)是一种专为AI代理设计的浏览器自动化协议。与传统的Playwright API不同,MCP通过结构化数据交换实现浏览器控制,特别适合与LLM(大语言模型)集成。
核心设计思想:
在ClaudeCode环境中安装MCP服务:
bash复制# 添加Playwright MCP服务器
claude mcp add playwright -- cmd /c npx @playwright/mcp@latest
# 安装浏览器二进制文件
npx playwright install chromium
# 验证安装
claude mcp list
注意:在生产环境中,我建议固定MCP版本以避免意外升级带来的兼容性问题。例如:
npx @playwright/mcp@1.40.0
创建配置文件mcp.config.json:
json复制{
"browser": "chromium",
"headless": false,
"timeout": 30000,
"viewport": {
"width": 1280,
"height": 720
},
"proxy": {
"server": "http://proxy.example.com:8080",
"username": "proxyuser",
"password": "proxypass"
}
}
启动时指定配置文件:
bash复制claude mcp add playwright -- cmd /c npx @playwright/mcp@latest --config mcp.config.json
通过自然语言指令控制浏览器:
code复制使用playwright mcp打开https://example.com
在搜索框输入"Playwright测试"
点击搜索按钮
等待结果加载完成
获取前5个结果的标题和链接
MCP会将上述指令转换为结构化操作,并返回类似如下的响应:
json复制{
"status": "success",
"results": [
{
"title": "Playwright官方文档",
"url": "https://playwright.dev/docs/intro"
},
{
"title": "Playwright入门教程",
"url": "https://example.com/tutorials/playwright"
}
]
}
对于多步骤表单提交:
code复制使用playwright mcp打开https://example.com/register
填写表单字段:
- 姓名:张三
- 邮箱:zhangsan@example.com
- 电话:13800138000
选择"个人用户"单选按钮
勾选"同意条款"复选框
点击提交按钮
验证是否跳转到/register/success页面
经验分享:在实际项目中,我发现MCP处理动态表单特别高效。当表单字段发生变化时,只需调整自然语言指令,无需修改底层代码。
browser_type工具的典型使用场景:
json复制{
"tool": "browser_type",
"params": {
"element": "用户名输入框",
"ref": "e123",
"text": "testuser",
"submit": false
}
}
参数说明:
element:元素描述,用于LLM理解ref:元素引用ID,确保精准定位text:要输入的文本submit:是否在输入后提交表单browser_snapshot工具返回的可访问性树示例:
json复制{
"role": "document",
"name": "Example Domain",
"children": [
{
"role": "heading",
"name": "Example Domain",
"level": 1
},
{
"role": "paragraph",
"name": "This domain is for use in illustrative examples in documents."
}
]
}
这种结构化数据比截图更轻量,且便于LLM解析处理。
与Playwright MCP不同,Chrome DevTools MCP直接基于Chrome DevTools Protocol(CDP)实现,提供了更底层的浏览器控制能力。
架构差异:
| 特性 | Playwright MCP | Chrome DevTools MCP |
|---|---|---|
| 协议层 | Playwright高层API | 原始CDP协议 |
| 浏览器支持 | 多浏览器(Chromium/Firefox/WebKit) | 仅Chrome/Chromium |
| 性能开销 | 中等 | 较低 |
| 功能丰富度 | 优化过的常用操作 | 全部DevTools功能 |
| 上手难度 | 简单 | 较复杂 |
bash复制# 添加Chrome DevTools MCP服务器
claude mcp add chrome-devtools -- cmd /c npx chrome-devtools-mcp@latest
# 验证安装
claude mcp list
对于需要调试远程Chrome实例的场景:
bash复制# 启动Chrome并启用远程调试
chrome.exe --remote-debugging-port=9222
# 连接已有Chrome实例
claude mcp add chrome-devtools -- cmd /c npx chrome-devtools-mcp@latest --port 9222
json复制{
"tool": "performance_start_trace",
"params": {
"categories": ["loading", "scripting", "rendering", "painting"]
}
}
执行待测操作
停止追踪并获取结果:
json复制{
"tool": "performance_stop_trace",
"params": {}
}
典型的性能分析指令:
code复制分析最近一次性能追踪结果
识别加载时间超过500ms的资源
找出导致最大内容绘制(LCP)延迟的因素
列出执行时间超过100ms的长任务
MCP会返回结构化分析结果:
json复制{
"slowResources": [
{
"url": "https://example.com/large-image.jpg",
"loadTime": 1200,
"size": "1.2MB"
}
],
"lcpElement": {
"selector": "div.hero-image",
"loadTime": 1500
},
"longTasks": [
{
"duration": 320,
"details": "第三方广告脚本执行"
}
]
}
模拟慢速网络环境:
json复制{
"tool": "emulate_network",
"params": {
"networkCondition": "slow3G",
"latency": 1000,
"downloadThroughput": 500000,
"uploadThroughput": 500000
}
}
获取网络请求列表:
json复制{
"tool": "list_network_requests",
"params": {
"filter": "*.js"
}
}
典型响应:
json复制{
"requests": [
{
"url": "https://example.com/main.js",
"method": "GET",
"status": 200,
"size": "245KB",
"duration": 450,
"timings": {
"blocked": 10,
"dns": 20,
"connect": 30,
"ssl": 40,
"send": 5,
"wait": 200,
"receive": 145
}
}
]
}
@playwright/cli是微软专为AI代理设计的命令行工具,与传统Playwright CLI完全不同。它的核心设计目标是:
bash复制npm install -g @playwright/cli@latest
# 验证安装
playwright-cli --version
bash复制mkdir my-project && cd my-project
playwright-cli install
# 安装浏览器
playwright-cli install-browser chromium
# 安装技能文档
playwright-cli install --skills
初始化后会创建以下目录结构:
code复制.my-project/
├── .playwright-cli/
│ ├── browsers/ # 浏览器二进制
│ ├── sessions/ # 会话数据
│ ├── artifacts/ # 截图、PDF等
│ └── skills.md # AI技能文档
└── ...
启动新会话:
bash复制playwright-cli new --url https://example.com --headless false
命令响应:
json复制{
"sessionId": "s123",
"artifactsDir": "/path/to/.playwright-cli/sessions/s123"
}
定位页面元素:
bash复制playwright-cli locate --session s123 --selector "button:has-text('Submit')"
响应示例:
json复制{
"elementRef": "e456",
"elementInfo": {
"role": "button",
"name": "Submit",
"boundingBox": {"x": 100, "y": 200, "width": 80, "height": 40}
}
}
点击元素:
bash复制playwright-cli click --session s123 --element e456
创建script.pwcli文件:
code复制new --url https://example.com/login --headless false
locate --selector "input[name='username']" --as username
locate --selector "input[name='password']" --as password
locate --selector "button:has-text('Login')" --as login
type --element $username --text "testuser"
type --element $password --text "secret"
click --element $login
screenshot --path login_result.png
执行脚本:
bash复制playwright-cli batch --file script.pwcli
列出所有会话:
bash复制playwright-cli list-sessions
附加到现有会话:
bash复制playwright-cli attach --session s123
实时监控会话:
bash复制playwright-cli show --session s123
这个命令会打开一个监控窗口,实时显示AI代理的操作过程,非常适合调试复杂场景。
根据项目需求选择合适的工具:
| 考虑因素 | 选择CLI | 选择MCP |
|---|---|---|
| 执行环境 | 有文件系统访问权限 | 受限环境(如沙箱) |
| 操作复杂度 | 复杂多步骤场景 | 简单快速操作 |
| Token预算 | 需要高效利用 | 预算充足 |
| 调试需求 | 需要可视化监控 | 无需特殊调试 |
| 浏览器类型 | 需要多浏览器支持 | 仅需Chrome |
| 与现有测试框架集成 | 需要深度集成 | 独立使用 |
在实际项目中,我通常会这样搭配使用:
我们以电商网站为例,设计以下测试场景:
python复制import pytest
from playwright.sync_api import Page, expect
@pytest.fixture
def page(browser):
context = browser.new_context()
page = context.new_page()
yield page
context.close()
def test_login(page: Page):
page.goto("https://shop.example.com/login")
page.get_by_label("邮箱").fill("user@example.com")
page.get_by_label("密码").fill("password123")
page.get_by_role("button", name="登录").click()
expect(page).to_have_url("https://shop.example.com/dashboard")
def test_search(page: Page):
page.goto("https://shop.example.com")
page.get_by_placeholder("搜索商品").fill("Playwright")
page.get_by_role("button", name="搜索").click()
expect(page.get_by_text("搜索结果:Playwright")).to_be_visible()
创建page_objects.py:
python复制class LoginPage:
def __init__(self, page):
self.page = page
self.email = page.get_by_label("邮箱")
self.password = page.get_by_label("密码")
self.submit = page.get_by_role("button", name="登录")
def navigate(self):
self.page.goto("https://shop.example.com/login")
return self
def login(self, email, password):
self.email.fill(email)
self.password.fill(password)
self.submit.click()
更新测试脚本:
python复制def test_login_with_page_object(page: Page):
login_page = LoginPage(page).navigate()
login_page.login("user@example.com", "password123")
expect(page).to_have_url("https://shop.example.com/dashboard")
code复制使用playwright mcp打开https://shop.example.com
点击登录链接
在邮箱字段输入user@example.com
在密码字段输入password123
点击登录按钮
验证是否跳转到/dashboard页面
json复制{
"actions": [
{
"type": "navigate",
"url": "https://shop.example.com",
"status": "success"
},
{
"type": "click",
"element": "登录链接",
"status": "success"
},
{
"type": "fill",
"element": "邮箱字段",
"text": "user@example.com",
"status": "success"
},
{
"type": "fill",
"element": "密码字段",
"text": "password123",
"status": "success"
},
{
"type": "click",
"element": "登录按钮",
"status": "success"
},
{
"type": "verify",
"condition": "url_contains",
"value": "/dashboard",
"status": "success"
}
],
"duration": 4.2,
"status": "complete"
}
创建e2e_test.pwcli:
code复制new --url https://shop.example.com --headless false
click --selector "a:has-text('登录')"
type --selector "input[name='email']" --text "user@example.com"
type --selector "input[name='password']" --text "password123"
click --selector "button:has-text('登录')"
wait-for-url --contains "/dashboard"
screenshot --path login_success.png
bash复制playwright-cli batch --file e2e_test.pwcli --session e2e-test
playwright-cli show --session e2e-test
python复制import json
@pytest.fixture
def test_data():
with open("test_data.json") as f:
return json.load(f)
def test_checkout(page: Page, test_data):
login_page = LoginPage(page).navigate()
login_page.login(test_data["user"]["email"], test_data["user"]["password"])
# 继续其他测试步骤
python复制import csv
def load_test_cases():
with open("test_cases.csv") as f:
return list(csv.DictReader(f))
@pytest.mark.parametrize("case", load_test_cases())
def test_search(page: Page, case):
page.goto("https://shop.example.com")
page.get_by_placeholder("搜索商品").fill(case["query"])
page.get_by_role("button", name="搜索").click()
expect(page.get_by_text(case["expected"])).to_be_visible()
使用pytest-xdist插件实现并行测试:
bash复制pytest -n auto # 根据CPU核心数自动设置worker数量
python复制@pytest.fixture(scope="module")
def browser():
with sync_playwright() as p:
browser = p.chromium.launch()
yield browser
browser.close()
@pytest.fixture
def context(browser):
context = browser.new_context()
yield context
context.close()
@pytest.fixture
def page(context):
page = context.new_page()
yield page
page.close()
python复制@pytest.fixture
def auto_cleanup_page(browser):
context = browser.new_context()
page = context.new_page()
yield page
# 测试结束后自动清理
page.close()
context.close()
python复制@pytest.fixture
def page_with_recording(browser):
context = browser.new_context(record_video_dir="videos/")
page = context.new_page()
yield page
# 确保视频保存
page.close()
context.close()
python复制# 传统方式(不推荐)
page.wait_for_timeout(3000) # 固定等待
assert page.inner_text("#status") == "Success"
# Playwright推荐方式
expect(page.locator("#status")).to_have_text("Success", timeout=5000)
python复制def test_complex_validation(page: Page):
# 多个expect会自动等待
expect(page).to_have_url("https://example.com/dashboard")
expect(page.get_by_text("欢迎回来")).to_be_visible()
expect(page.get_by_role("button", name="登出")).to_be_enabled()
python复制browser = p.chromium.launch(
headless=False,
devtools=True, # 打开开发者工具
slow_mo=1000 # 放慢操作速度
)
python复制context = browser.new_context()
context.tracing.start(screenshots=True, snapshots=True)
# 执行测试操作
context.tracing.stop(path="trace.zip")
使用Trace Viewer分析:
bash复制playwright show-trace trace.zip
yaml复制name: Playwright Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
- name: Install dependencies
run: |
npm install -g @playwright/cli
pip install -r requirements.txt
playwright install
- name: Run tests
run: |
pytest --browser chromium --headed
- name: Upload artifacts
if: always()
uses: actions/upload-artifact@v2
with:
name: test-results
path: |
test-results/
videos/
traces/
python复制@pytest.mark.flaky(reruns=3, reruns_delay=2)
def test_flaky_scenario(page: Page):
# 这个测试如果失败会自动重试最多3次
...
问题现象:
code复制Error: locator.click: Timeout 30000ms exceeded.
=========================== logs ===========================
waiting for locator('button:has-text("Submit")')
解决方案:
python复制# 不推荐
page.locator("button.btn-primary").click()
# 推荐
page.get_by_role("button", name="Submit").click()
python复制# 切换到iframe内部
frame = page.frame_locator("iframe#payment")
frame.get_by_role("button", name="确认支付").click()
问题现象:测试有时通过有时失败
解决方案:
python复制page.locator("div.loading").wait_for(state="hidden")
python复制# 会自动等待元素可点击
page.get_by_role("button", name="动态加载").click()
问题现象:
code复制page.goto: Timeout 30000ms exceeded.
=========================== logs ===========================
navigating to "https://example.com", waiting until "load"
解决方案:
python复制page.goto("https://example.com", timeout=60000)
python复制browser = p.chromium.launch(proxy={
"server": "http://proxy.example.com:8080"
})
解决方案:
python复制page.route("**/*", lambda route: route.continue_())
问题现象:某些操作在Chrome正常但在Firefox失败
解决方案:
python复制if browser_type.name == "firefox":
page.keyboard.press("Enter")
else:
page.get_by_role("button", name="提交").click()
python复制# 这个API在所有浏览器中行为一致
page.get_by_role("button", name="提交").click()
解决方案:
python复制@pytest.fixture(scope="session")
def browser():
with sync_playwright() as p:
yield p.chromium.launch()
@pytest.fixture
def context(browser):
context = browser.new_context()
yield context
context.close()
bash复制pytest -n 4 # 使用4个worker并行执行
| 错误代码 | 含义 | 解决方案 |
|---|---|---|
| ERR_TIMEOUT | 操作超时 | 增加timeout参数或检查元素定位 |
| ERR_NETWORK | 网络问题 | 检查代理设置或mock网络请求 |
| ERR_ELEMENT_NOT_FOUND | 元素未找到 | 使用更健壮的选择器或检查iframe |
| ERR_INVALID_SELECTOR | 无效选择器 | 验证选择器语法 |
| ERR_NAVIGATION | 导航失败 | 检查目标URL是否可用 |
Playwright支持移动端模拟:
python复制iphone = p.devices["iPhone 12"]
browser = p.chromium.launch()
context = browser.new_context(**iphone)
集成Applitools进行视觉回归测试:
python复制from applitools.playwright import Eyes
def test_visual(page: Page):
eyes = Eyes()
eyes.open(page, "App Name", "Test Name")
eyes.check_window("Home Page")
eyes.close()
生成HTML测试报告:
bash复制pytest --html=report.html
结合MCP实现AI驱动的测试生成:
code复制分析https://example.com/login页面的测试场景
生成5个关键测试用例
包括正常登录、错误密码、空用户名等情况
在实际项目中保持对新特性的关注,但也要评估团队的实际需求和适应能力,避免盲目追求新技术。