第一次接触Playwright是在一个电商项目的测试需求中,当时我们需要在Chrome、Firefox和Safari三个浏览器上验证支付流程。用传统工具写三套测试脚本简直让人崩溃,直到发现了这个支持多浏览器统一API的神器。Playwright最让我惊艳的是它的"跨浏览器一致性"——同样的脚本稍作修改就能跑在不同浏览器上,这对需要兼容性测试的团队简直是福音。
和Selenium相比,Playwright有几个杀手级优势:首先是速度快,实测同样的测试用例能快30%以上;其次是自动等待机制,再也不用写满屏的time.sleep了;最重要的是原生支持无头模式,在CI/CD流水线里跑测试特别顺畅。我团队现在做UI自动化测试,首选方案一定是Playwright。
建议使用Python 3.8+版本,太老的Python可能会遇到依赖问题。我习惯用virtualenv创建隔离环境,避免包冲突:
bash复制python -m venv playwright_env
source playwright_env/bin/activate # Linux/Mac
playwright_env\Scripts\activate # Windows
安装核心库只需要一行命令:
bash复制pip install playwright
这里有个小技巧:如果网络环境不好,可以单独安装需要的浏览器。比如我们项目主要用Chromium和Firefox:
bash复制playwright install chromium
playwright install firefox
第一次安装会下载约500MB的浏览器二进制文件(Chromium 180MB,Firefox 120MB,WebKit 200MB)。我在阿里云服务器上实测,完整安装所有浏览器大约需要5分钟。
推荐用这个组合命令验证:
bash复制python -m playwright codegen https://baidu.com
看到浏览器弹出并开始录制操作,说明环境配置正确。有个常见坑点是防火墙拦截浏览器下载,如果遇到启动报错,可以尝试手动下载浏览器驱动。
这是我在项目中常用的浏览器启动模板:
python复制from playwright.sync_api import sync_playwright
def run_test(browser_type):
with sync_playwright() as p:
# 配置启动参数
browser = p[browser_type].launch(
headless=False,
slow_mo=100, # 操作延迟100ms方便观察
args=['--start-maximized']
)
page = browser.new_page()
page.goto('https://example.com')
print(f"{browser_type}标题:", page.title())
browser.close()
# 同时测试三个浏览器
for browser in ['chromium', 'firefox', 'webkit']:
run_test(browser)
特别注意几个实用参数:
slow_mo:放慢操作速度便于调试args:传递浏览器启动参数viewport:设置初始窗口大小当测试用例很多时,串行执行太耗时。这是我的并行测试方案:
python复制import threading
def thread_task(url, browser_type):
with sync_playwright() as p:
browser = p[browser_type].launch()
page = browser.new_page()
page.goto(url)
# 实际测试操作...
browser.close()
threads = []
for i in range(3): # 同时开3个浏览器
t = threading.Thread(
target=thread_task,
args=('https://example.com', ['chromium','firefox','webkit'][i])
)
threads.append(t)
t.start()
for t in threads:
t.join()
在8核机器上实测,50个测试用例的并行执行比串行快4倍。注意要控制并发数,太多线程会导致浏览器崩溃。
无头模式默认就是开启的,但我们可以进一步优化:
python复制browser = p.chromium.launch(
headless=True,
args=[
'--disable-gpu',
'--no-sandbox',
'--disable-dev-shm-usage'
]
)
这些参数能显著提升稳定性,特别是在Docker环境中。我在CI中跑测试时,加了这些参数后失败率从15%降到了3%。
无头模式最大的痛点是不方便调试。我的解决方案是:
python复制page.goto(url)
try:
page.click("button.submit")
except Exception as e:
page.screenshot(path=f"error_{browser_type}.png")
raise e
python复制context = browser.new_context(
record_video_dir="videos/",
record_video_size={"width": 1280, "height": 720}
)
python复制page.on("console", lambda msg: print(f"CONSOLE: {msg.text}"))
以电商登录为例,演示完整测试流程:
python复制def test_login():
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
context = browser.new_context()
page = context.new_page()
# 1. 访问登录页
page.goto("https://shop.example.com/login")
# 2. 填写表单
page.fill("#username", "testuser")
page.fill("#password", "Test1234")
# 3. 提交表单
page.click("button[type='submit']")
# 4. 验证结果
assert page.url == "https://shop.example.com/dashboard"
assert page.is_visible("text=欢迎回来")
# 5. 清理
context.close()
browser.close()
这个案例检查关键功能在三大浏览器的表现:
python复制def check_compatibility():
results = {}
for browser_type in ['chromium', 'firefox', 'webkit']:
with sync_playwright() as p:
browser = p[browser_type].launch()
page = browser.new_page()
page.goto("https://shop.example.com/search?q=playwright")
# 检查搜索框
results[f"{browser_type}_search"] = page.is_visible("#search-box")
# 检查结果加载
page.wait_for_selector(".product-item", timeout=5000)
results[f"{browser_type}_items"] = page.locator(".product-item").count()
browser.close()
# 生成兼容性报告
assert results["chromium_items"] == results["firefox_items"] == results["webkit_items"]
print("兼容性测试通过:", results)
启动录制模式有个实用技巧:添加--save-storage参数保存登录状态
bash复制playwright codegen https://shop.example.com --target python --save-storage=auth.json
这样下次测试可以直接加载认证状态:
python复制context = browser.new_context(storage_state="auth.json")
自动生成的代码通常需要优化:
python复制# 原始生成的
page.click("text=Add to cart")
# 优化为
page.click("button[data-testid='add-to-cart']")
python复制# 原始
page.click("#submit")
# 优化后
page.locator("#submit").wait_for()
page.click("#submit")
python复制from playwright.sync_api import TimeoutError
retry = 3
while retry > 0:
try:
page.click("#flaky-button")
break
except TimeoutError:
retry -= 1
page.reload()
模拟慢速网络测试加载状态:
python复制def test_slow_network():
with sync_playwright() as p:
browser = p.chromium.launch()
context = browser.new_context()
# 设置网络限速
context.set_default_timeout(10000) # 10秒超时
page = context.new_page()
# 开始记录请求
page.route("**/*", lambda route: route.continue_())
page.on("request", lambda request: print(">>", request.method, request.url))
page.on("response", lambda response: print("<<", response.status, response.url))
page.goto("https://example.com")
browser.close()
测试移动端适配性:
python复制def test_mobile():
with sync_playwright() as p:
iphone = p.devices["iPhone 12"]
browser = p.chromium.launch()
context = browser.new_context(**iphone)
page = context.new_page()
page.goto("https://m.example.com")
assert page.viewport_size["width"] == 390
page.screenshot(path="mobile.png")
browser.close()
我总结的元素定位三板斧:
page.pause()进入调试模式python复制# 文本定位
page.click("text=Submit")
# CSS定位
page.click("#submit-btn")
# XPath定位
page.click("//button[contains(@class, 'primary')]")
python复制page.locator(".dynamic-element").wait_for()
处理iframe的黄金法则:
python复制# 1. 定位iframe
frame = page.frame(name="payment-iframe")
# 2. 在iframe内操作
frame.fill("#card-number", "4242424242424242")
# 3. 返回主页面
page.click("#confirm-payment")
可靠的文件上传方案:
python复制# 不要用传统的input点击
# 而是直接设置文件路径
page.set_input_files("#file-upload", [
"/path/to/file1.pdf",
"/path/to/file2.jpg"
])
# 处理上传进度
with page.expect_event("filechooser") as fc:
page.click("#upload-button")
file_chooser = fc.value
file_chooser.set_files("/path/to/file.pdf")
这是我的标准工作流配置:
yaml复制name: Playwright Tests
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: '3.9'
- run: pip install playwright
- run: playwright install
- run: playwright install-deps
- run: pytest tests/
- uses: actions/upload-artifact@v2
if: always()
with:
name: playwright-report
path: test-results/
集成Allure报告:
python复制# conftest.py
import allure
from playwright.sync_api import Page
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
outcome = yield
report = outcome.get_result()
if report.when == "call" and hasattr(item, "funcargs"):
page = item.funcargs.get("page")
if page:
allure.attach(
page.screenshot(full_page=True),
name="screenshot",
attachment_type=allure.attachment_type.PNG
)
这是我推荐的项目结构:
code复制tests/
├── pages/
│ ├── login_page.py
│ ├── product_page.py
│ └── cart_page.py
├── fixtures/
│ └── browser.py
├── test_login.py
└── test_checkout.py
示例页面对象:
python复制# pages/login_page.py
class LoginPage:
def __init__(self, page):
self.page = page
self.username = page.locator("#username")
self.password = page.locator("#password")
self.submit = page.locator("#submit")
def navigate(self):
self.page.goto("https://example.com/login")
def login(self, user, pwd):
self.username.fill(user)
self.password.fill(pwd)
self.submit.click()
我常用的数据驱动方案:
python复制import json
import pytest
def load_test_data():
with open("test_data.json") as f:
return json.load(f)
@pytest.mark.parametrize("user_data", load_test_data()["users"])
def test_login(user_data):
login_page = LoginPage(page)
login_page.navigate()
login_page.login(user_data["username"], user_data["password"])
assert page.url == user_data["expected_url"]
记录关键性能指标:
python复制def test_performance():
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
# 启动性能监控
page.goto("https://example.com", wait_until="networkidle")
# 获取性能指标
metrics = page.evaluate("""() => ({
ttfb: performance.timing.responseStart - performance.timing.requestStart,
domLoad: performance.timing.domComplete - performance.timing.domLoading,
pageLoad: performance.timing.loadEventEnd - performance.timing.navigationStart
})""")
print(f"TTFB: {metrics['ttfb']}ms")
print(f"DOM加载: {metrics['domLoad']}ms")
print(f"页面加载: {metrics['pageLoad']}ms")
assert metrics["pageLoad"] < 3000 # 3秒内完成加载
browser.close()
模拟多用户并发:
python复制import asyncio
from playwright.async_api import async_playwright
async def simulate_user(user_id):
async with async_playwright() as p:
browser = await p.chromium.launch()
page = await browser.new_page()
await page.goto(f"https://example.com?user={user_id}")
# 执行用户操作...
await browser.close()
async def stress_test(user_count=100):
tasks = [simulate_user(i) for i in range(user_count)]
await asyncio.gather(*tasks)
asyncio.run(stress_test())
连接Android真机调试:
bash复制# 1. 启用设备调试模式
adb devices
# 2. 通过Playwright连接
playwright open --device="Pixel 5" chrome
精确控制触摸操作:
python复制def test_swipe():
with sync_playwright() as p:
browser = p.chromium.launch()
context = browser.new_context(**p.devices["iPhone 12"])
page = context.new_page()
# 滑动操作
page.dispatch_event(".carousel", "touchstart", {"x": 100, "y": 100})
page.dispatch_event(".carousel", "touchmove", {"x": 50, "y": 100})
page.dispatch_event(".carousel", "touchend")
# 验证滑动效果
assert page.is_visible(".slide-2")
browser.close()
使用pixelmatch进行视觉对比:
python复制from PIL import Image
import pixelmatch
def test_visual_regression():
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto("https://example.com")
# 获取截图
screenshot = page.screenshot(full_page=True)
with open("current.png", "wb") as f:
f.write(screenshot)
# 与基线对比
img1 = Image.open("baseline.png")
img2 = Image.open("current.png")
diff = Image.new("RGBA", img1.size)
mismatch = pixelmatch.pixelmatch(
img1.load(), img2.load(),
diff.load(),
img1.width, img1.height,
threshold=0.1
)
assert mismatch < 100 # 允许100个像素差异
browser.close()
忽略动态区域的对比:
python复制def test_ignore_dynamic():
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto("https://example.com")
# 隐藏动态元素
page.evaluate("""() => {
document.querySelector(".banner").style.visibility = "hidden";
document.querySelector(".timer").style.display = "none";
}""")
# 现在可以安全截图对比了
screenshot = page.screenshot()
# ...对比逻辑...
browser.close()
自动化检测常见漏洞:
python复制def test_xss():
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
# 测试搜索框
page.goto("https://example.com/search")
page.fill("#search", "<script>alert(1)</script>")
page.click("#submit")
# 检查是否弹出alert
def handle_dialog(dialog):
assert False, f"发现XSS漏洞: {dialog.message}"
page.on("dialog", handle_dialog)
page.wait_for_timeout(1000) # 等待可能的弹窗
browser.close()
检查安全头信息:
python复制def test_security_headers():
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
response = page.goto("https://example.com")
headers = response.headers
assert "x-frame-options" in headers
assert "DENY" in headers["x-frame-options"]
assert "strict-transport-security" in headers
assert "content-security-policy" in headers
browser.close()
自定义Playwright插件:
python复制# conftest.py
import pytest
from playwright.sync_api import sync_playwright
@pytest.fixture(scope="session")
def browser():
with sync_playwright() as p:
browser = p.chromium.launch()
yield browser
browser.close()
@pytest.fixture
def page(browser):
context = browser.new_context()
page = context.new_page()
yield page
context.close()
# 测试用例
def test_homepage(page):
page.goto("https://example.com")
assert page.title() == "Example Domain"
扩展断言功能:
python复制# assertions.py
from playwright.sync_api import Page
class PlaywrightAssertions:
def __init__(self, page: Page):
self.page = page
def to_have_title(self, expected):
assert self.page.title() == expected
def to_contain_text(self, selector, text):
element = self.page.locator(selector)
assert text in element.text_content()
# 使用示例
def test_custom_assert(page):
assert_obj = PlaywrightAssertions(page)
page.goto("https://example.com")
assert_obj.to_have_title("Example Domain")
assert_obj.to_contain_text("body", "illustrative examples")
在云平台运行测试:
python复制def test_on_browserstack():
caps = {
'browser': 'chrome',
'os': 'Windows',
'os_version': '10',
'name': 'Playwright Test',
'build': 'playwright-python-1',
'browserstack.username': 'YOUR_USERNAME',
'browserstack.accessKey': 'YOUR_ACCESS_KEY'
}
with sync_playwright() as p:
browser = p.chromium.connect(
f"wss://cdp.browserstack.com/playwright?caps={json.dumps(caps)}"
)
page = browser.new_page()
page.goto("https://example.com")
assert page.title() == "Example Domain"
browser.close()
另一种云方案:
python复制def test_on_lambdatest():
caps = {
"platform": "Windows 10",
"browserName": "Chrome",
"version": "latest",
"build": "Playwright Python",
"name": "Playwright Test",
"network": True,
"video": True,
"console": True
}
with sync_playwright() as p:
browser = p.chromium.connect(
f"wss://cdp.lambdatest.com/playwright?capabilities={json.dumps(caps)}"
)
page = browser.new_page()
page.goto("https://example.com")
page.screenshot(path="lambda_test.png")
browser.close()
生成可视化报告:
python复制def generate_html_report(test_results):
html = """
<html>
<head><title>测试报告</title></head>
<body>
<h1>Playwright测试报告</h1>
<table border="1">
<tr>
<th>测试用例</th>
<th>状态</th>
<th>截图</th>
</tr>
"""
for result in test_results:
status = "通过" if result["passed"] else "失败"
html += f"""
<tr>
<td>{result['name']}</td>
<td>{status}</td>
<td><img src="{result['screenshot']}" width="200"></td>
</tr>
"""
html += """
</table>
</body>
</html>
"""
with open("report.html", "w") as f:
f.write(html)
集成Prometheus监控:
python复制from prometheus_client import start_http_server, Gauge
# 创建指标
TEST_DURATION = Gauge('test_duration_seconds', '测试执行耗时')
TEST_SUCCESS = Gauge('test_success', '测试通过情况', ['test_name'])
def test_with_metrics():
start_time = time.time()
try:
# 执行测试...
TEST_SUCCESS.labels("example_test").set(1)
except:
TEST_SUCCESS.labels("example_test").set(0)
raise
finally:
TEST_DURATION.set(time.time() - start_time)
# 启动指标服务器
start_http_server(8000)
使用Faker创建测试数据:
python复制from faker import Faker
def test_with_fake_data():
fake = Faker()
test_user = {
"name": fake.name(),
"email": fake.email(),
"address": fake.address()
}
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto("https://example.com/register")
page.fill("#name", test_user["name"])
page.fill("#email", test_user["email"])
page.click("#submit")
assert page.is_visible("text=注册成功")
browser.close()
集成数据库操作:
python复制import sqlite3
@pytest.fixture
def db_setup():
conn = sqlite3.connect(":memory:")
cursor = conn.cursor()
cursor.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
cursor.execute("INSERT INTO users (name) VALUES ('测试用户')")
conn.commit()
yield conn
conn.close()
def test_with_db(db_setup):
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto("https://example.com/admin")
# 从数据库获取测试数据
user = db_setup.execute("SELECT * FROM users LIMIT 1").fetchone()
page.fill("#search", user[1])
page.click("#search-btn")
assert page.is_visible(f"text={user[1]}")
browser.close()
我推荐的三层测试结构:
python复制# 示例UI测试筛选标准
UI_TEST_CASES = [
("关键业务流程", True),
("核心用户旅程", True),
("边缘场景", False),
("视觉回归", True)
]
def should_run_ui_test(test_case):
return any(tag in test_case.tags for tag in ["critical", "smoke"])
基于变更的测试选择:
python复制def select_tests_based_on_changes(changed_files):
tests_to_run = set()
# 映射文件到测试用例
file_test_map = {
"frontend/login.js": ["test_login.py", "test_auth.py"],
"frontend/checkout.js": ["test_checkout.py"]
}
for file in changed_files:
if file in file_test_map:
tests_to_run.update(file_test_map[file])
return list(tests_to_run) or ["test_smoke.py"]
结合OCR识别验证码:
python复制import pytesseract
from PIL import Image
def test_with_ocr():
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto("https://example.com/captcha")
# 获取验证码图片
captcha = page.locator("#captcha-image")
captcha.screenshot(path="captcha.png")
# OCR识别
image = Image.open("captcha.png")
text = pytesseract.image_to_string(image).strip()
# 输入验证码
page.fill("#captcha-input", text)
page.click("#submit")
assert page.is_visible("text=验证成功")
browser.close()
使用OpenCV验证UI元素:
python复制import cv2
import numpy as np
def test_ui_with_cv():
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto("https://example.com")
# 获取截图
page.screenshot(path="page.png")
# 加载模板图片
template = cv2.imread("search-icon.png", 0)
screenshot = cv2.imread("page.png", 0)
# 模板匹配
res = cv2.matchTemplate(screenshot, template, cv2.TM_CCOEFF_NORMED)
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)
assert max_val > 0.8 # 相似度阈值
browser.close()