playwright使用

如题,AI收集整理,作为了解

Playwright 完整使用指南

Date: October 23, 2025
Code: https://github.com/microsoft/playwright


目录

  1. Playwright 概述

  2. 核心概念

  3. 环境搭建

  4. 基本用法

  5. 元素定位与操作

  6. 智能等待机制

  7. 网络请求处理

  8. 高级特性

  9. 最佳实践

  10. 实战案例

  11. 常见问题与解决方案


1. Playwright 概述

1.1 什么是 Playwright

Playwright 是由微软开发的一款开源自动化测试工具,专为现代 Web 应用程序设计。它提供了一套统一的 API,支持 Chromium、Firefox 和 WebKit 三大主流浏览器引擎,能够实现真正的跨浏览器测试。

1.2 核心优势

特性 传统工具(如 Selenium) Playwright 解决方案
跨浏览器支持 需分别配置不同驱动,兼容性问题多 原生支持三大浏览器,API 完全统一
元素等待机制 依赖 time.sleep() 或显式等待,脚本不稳定 所有操作内置智能等待,自动等待元素"可操作"状态
元素定位能力 主要依赖 CSS、XPath,对动态页面支持差 支持文本、组件、属性组合等 10+ 种定位方式
多场景模拟 多标签页、多用户场景配置复杂 基于"上下文隔离"机制,多场景独立运行
网络请求控制 需借助第三方工具,集成复杂 内置网络拦截、Mock、限速功能
移动端测试 需搭建额外环境,配置繁琐 一键模拟手机设备,支持 GPS 定位模拟

1.3 适用场景

  • 现代 Web 应用测试:尤其适合 React、Vue、Angular 等前端框架开发的 SPA 应用

  • 跨浏览器兼容性测试:需在 Chrome、Firefox、Safari 同时验证功能的项目

  • 自动化业务流程:用户登录、订单提交、数据导出等重复性业务操作

  • 异常场景测试:模拟接口报错、网络延迟、弱网环境的测试

  • 移动端响应式测试:无需真实设备,模拟不同手机型号的浏览器表现


2. 核心概念

2.1 浏览器(Browser)

浏览器实例代表一个具体的浏览器进程,可以是 Chromium、Firefox 或 WebKit。

1
2
3
4
5
6
7
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
# 启动不同浏览器
chromium_browser = p.chromium.launch()
firefox_browser = p.firefox.launch()
webkit_browser = p.webkit.launch()

2.2 浏览器上下文(Browser Context)

浏览器上下文是一个隔离的会话环境,相当于一个无痕浏览器窗口。它可以独立管理 cookies、localStorage 等状态信息。

1
2
3
4
5
6
7
# 创建浏览器上下文
context = browser.new_context(
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
viewport={"width": 1920, "height": 1080},
geolocation={"longitude": 12.492507, "latitude": 41.889938},
permissions=["geolocation"]
)

2.3 页面(Page)

页面代表浏览器中的一个标签页,是进行页面操作的主要对象。

1
2
3
4
5
6
7
8
# 创建新页面
page = context.new_page()

# 导航到网址
page.goto("https://playwright.dev")

# 获取页面标题
print(page.title())

2.4 元素定位器(Locator)

定位器是用于查找页面元素的对象,支持多种定位策略。

1
2
3
4
5
6
7
8
# 文本定位
page.locator("text=Get Started").click()

# CSS 选择器定位
page.locator("button#submit").click()

# XPath 定位
page.locator("//input[@name='username']").fill("admin")

3. 环境搭建

3.1 安装 Playwright

Python 环境

1
2
3
4
5
# 安装 Playwright 库
pip install playwright

# 安装浏览器驱动
playwright install

Node.js 环境

1
2
3
4
5
6
7
8
# 使用 npm
npm init playwright@latest

# 使用 yarn
yarn create playwright

# 使用 pnpm
pnpm create playwright

3.2 验证安装

创建一个简单的测试脚本验证环境是否正常:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from playwright.sync_api import sync_playwright

def test_installation():
with sync_playwright() as p:
# 启动浏览器(headless=False 显示浏览器窗口)
browser = p.chromium.launch(headless=False)
page = browser.new_page()

# 访问百度
page.goto("https://www.baidu.com")

# 验证页面标题
assert "百度" in page.title()
print("环境搭建成功!")

browser.close()

if __name__ == "__main__":
test_installation()

3.3 浏览器配置

1
2
3
4
5
6
7
8
9
10
11
# 浏览器启动配置
browser = p.chromium.launch(
headless=False, # 是否无头模式
slow_mo=50, # 慢动作执行(毫秒)
args=[ # 浏览器启动参数
"--start-maximized",
"--disable-gpu",
"--no-sandbox"
],
devtools=True # 自动打开开发者工具
)

4. 基本用法

4.1 同步 API vs 异步 API

同步 API

1
2
3
4
5
6
7
8
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto("https://example.com")
print(page.title())
browser.close()

异步 API

1
2
3
4
5
6
7
8
9
10
11
12
import asyncio
from playwright.async_api import async_playwright

async def main():
async with async_playwright() as p:
browser = await p.chromium.launch()
page = await browser.new_page()
await page.goto("https://example.com")
print(await page.title())
await browser.close()

asyncio.run(main())

4.2 页面导航

1
2
3
4
5
6
7
8
9
10
11
12
13
# 基本导航
page.goto("https://example.com")

# 等待页面加载完成
page.goto("https://example.com", wait_until="networkidle")

# 前进后退
page.go_back()
page.go_forward()
page.reload()

# 获取当前 URL
current_url = page.url

4.3 页面操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 截图
page.screenshot(path="screenshot.png", full_page=True)

# 生成 PDF(仅 Chromium 支持)
page.pdf(path="page.pdf", format="A4")

# 滚动页面
page.scroll_by_amount(0, 500) # 向下滚动 500px

# 获取页面内容
content = page.content()

# 等待页面状态
page.wait_for_load_state("load") # 等待 load 事件
page.wait_for_load_state("domcontentloaded") # 等待 DOM 加载完成
page.wait_for_load_state("networkidle") # 等待网络空闲

5. 元素定位与操作

5.1 定位策略

文本定位

1
2
3
4
5
6
7
8
# 精确文本匹配
page.locator("text=Submit").click()

# 包含文本匹配
page.locator("text=Log in").click()

# 正则表达式匹配
page.locator("text=/^Submit$/").click()

CSS 选择器定位

1
2
3
4
5
6
7
8
9
10
11
# ID 选择器
page.locator("#username").fill("admin")

# Class 选择器
page.locator(".btn-primary").click()

# 属性选择器
page.locator("[name='email']").fill("test@example.com")

# 组合选择器
page.locator("form#login input[type='password']").fill("password")

XPath 定位

1
2
3
4
5
6
7
8
# 绝对 XPath
page.locator("/html/body/div[1]/form/input[2]").click()

# 相对 XPath
page.locator("//input[@id='username']").fill("admin")

# 文本 XPath
page.locator("//button[text()='Submit']").click()

ARIA 角色定位

1
2
3
4
5
6
7
8
# 按角色定位
page.locator("role=button", name="Submit").click()

# 按标签定位
page.locator("label=Username").fill("admin")

# 按占位符定位
page.locator("placeholder=Enter your email").fill("test@example.com")

5.2 元素操作

输入操作

1
2
3
4
5
6
7
8
9
10
11
12
# 填充文本
page.locator("#username").fill("admin")

# 清空文本
page.locator("#username").clear()

# 输入文本(模拟键盘输入)
page.locator("#username").type("admin", delay=100) # 延迟 100ms 输入

# 按下键盘
page.keyboard.press("Enter")
page.keyboard.type("Hello World!")

点击操作

1
2
3
4
5
6
7
8
9
10
11
# 基本点击
page.locator("button#submit").click()

# 双击
page.locator("div#item").dblclick()

# 右键点击
page.locator("div#menu").click(button="right")

# 按下 Shift 点击
page.locator("checkbox#select-all").click(modifiers=["Shift"])

表单操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 选择下拉框
page.locator("select#country").select_option("China")
page.locator("select#country").select_option(value="CN")

# 勾选复选框
page.locator("checkbox#agree").check()

# 取消勾选
page.locator("checkbox#agree").uncheck()

# 上传文件
page.locator("input[type='file']").set_input_files("document.pdf")

# 上传多个文件
page.locator("input[type='file']").set_input_files(["file1.txt", "file2.txt"])

5.3 元素状态检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 检查元素是否可见
if page.locator("div.success-message").is_visible():
print("操作成功")

# 检查元素是否存在
if page.locator("button#submit").is_enabled():
page.locator("button#submit").click()

# 等待元素出现
page.locator("div.loading").wait_for(state="hidden")

# 获取元素属性
value = page.locator("input#username").input_value()
text = page.locator("div.message").text_content()
attribute = page.locator("img.logo").get_attribute("src")

6. 智能等待机制

6.1 自动等待原理

Playwright 的所有操作都会自动等待元素满足以下条件:

  1. 元素已附加到 DOM(attached)

  2. 元素可见(visible)

  3. 元素稳定(stable,没有动画)

  4. 元素能够接收事件(not obscured)

  5. 元素已启用(enabled)

6.2 显式等待

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 等待元素可见
page.locator("div.result").wait_for(state="visible")

# 等待元素隐藏
page.locator("div.loading").wait_for(state="hidden")

# 等待元素包含特定文本
page.locator("div.message").wait_for(text="Success")

# 等待自定义条件
page.wait_for_function("() => document.title === 'Home Page'")

# 等待网络请求
with page.expect_request("**/api/data") as request_info:
page.click("button#load-data")
request = request_info.value

6.3 等待超时配置

1
2
3
4
5
6
7
8
# 全局超时配置
context = browser.new_context(timeout=30000) # 30 秒

# 单个操作超时
page.locator("button#submit").click(timeout=5000) # 5 秒

# 页面导航超时
page.goto("https://example.com", timeout=10000) # 10 秒

7. 网络请求处理

7.1 请求拦截与 Mock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 拦截所有请求
def handle_route(route):
if "api/user" in route.request.url:
# Mock API 响应
route.fulfill(
status=200,
content_type="application/json",
body='{"id": 123, "name": "Test User", "email": "test@example.com"}'
)
elif route.request.url.endswith(".png"):
# 阻止图片加载
route.abort()
else:
# 继续其他请求
route.continue_()

page.route("**/*", handle_route)

7.2 模拟网络错误

1
2
3
4
5
6
7
8
9
10
11
12
# 模拟 500 错误
page.route("**/api/payment", lambda route: route.fulfill(
status=500,
content_type="text/plain",
body="Internal Server Error"
))

# 模拟网络延迟
page.route("**/api/slow", lambda route: route.continue_(delay=3000)) # 延迟 3 秒

# 模拟网络中断
page.route("**/api/offline", lambda route: route.abort("connectionrefused"))

7.3 请求监控

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 监听所有请求
page.on("request", lambda request: print(f">> {request.method} {request.url}"))

# 监听所有响应
page.on("response", lambda response: print(f"<< {response.status} {response.url}"))

# 监听请求失败
page.on("requestfailed", lambda request: print(f"!! {request.url} {request.failure}"))

# 捕获特定请求
with page.expect_request("**/api/login") as request_info:
page.click("button#login")
login_request = request_info.value
print(f"Login request: {login_request.post_data}")

7.4 HTTP 认证

1
2
3
4
5
6
7
8
9
10
11
12
13
# 基本认证
context = browser.new_context(
http_credentials={"username": "user", "password": "pass"}
)

# 代理设置
context = browser.new_context(
proxy={
"server": "http://proxy.example.com:8080",
"username": "proxyuser",
"password": "proxypass"
}
)

8. 高级特性

8.1 多页面和弹出窗口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 处理新页面
with page.expect_popup() as popup_info:
page.click("a[target='_blank']")
popup_page = popup_info.value

# 处理下载
with page.expect_download() as download_info:
page.click("button#download")
download = download_info.value
download.save_as("file.pdf")

# 处理对话框
page.on("dialog", lambda dialog: dialog.accept()) # 接受对话框
page.on("dialog", lambda dialog: dialog.dismiss()) # 取消对话框
page.on("dialog", lambda dialog: dialog.accept("input text")) # 输入文本并接受

8.2 iframe 处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 获取 iframe
frame = page.frame(name="payment-form")
frame = page.frame(url="https://payment.example.com/form")

# 通过选择器获取 iframe
iframe_element = page.locator("iframe#checkout")
frame = iframe_element.content_frame()

# 在 iframe 内操作
frame.locator("input#card-number").fill("4111 1111 1111 1111")
frame.locator("button#pay").click()

# 嵌套 iframe
parent_frame = page.frame(name="parent")
child_frame = parent_frame.frame(name="child")

8.3 设备模拟

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 模拟 iPhone 13
iphone_13 = playwright.devices["iPhone 13"]
context = browser.new_context(**iphone_13)

# 自定义设备模拟
context = browser.new_context(
viewport={"width": 375, "height": 667},
user_agent="Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15",
device_scale_factor=2,
is_mobile=True,
has_touch=True
)

# 模拟地理位置
context = browser.new_context(
geolocation={"longitude": 12.492507, "latitude": 41.889938},
permissions=["geolocation"]
)

# 模拟颜色方案
context = browser.new_context(color_scheme="dark") # "light", "dark", "no-preference"

8.4 视频录制和追踪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 录制视频
context = browser.new_context(record_video_dir="videos/")
page = context.new_page()
page.goto("https://example.com")
context.close() # 自动保存视频

# 追踪记录
context.tracing.start(screenshots=True, snapshots=True)
page.goto("https://example.com")
page.click("button#submit")
context.tracing.stop(path="trace.zip")

# 性能分析
metrics = page.evaluate("""() => {
const navigation = performance.getEntriesByType('navigation')[0];
return {
domContentLoaded: navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart,
loadComplete: navigation.loadEventEnd - navigation.loadEventStart,
totalTime: navigation.loadEventEnd - navigation.navigationStart
};
}""")

8.5 并行执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 多浏览器并行
from playwright.sync_api import sync_playwright
import concurrent.futures

def run_test(browser_type):
with sync_playwright() as p:
browser = getattr(p, browser_type).launch()
page = browser.new_page()
page.goto("https://example.com")
print(f"{browser_type}: {page.title()}")
browser.close()

with concurrent.futures.ThreadPoolExecutor() as executor:
executor.map(run_test, ["chromium", "firefox", "webkit"])

9. 最佳实践

9.1 测试设计原则

测试用户可见行为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# ✅ 好的实践:测试用户可见的行为
def test_login_success(page):
page.goto("/login")
page.locator("input#username").fill("admin")
page.locator("input#password").fill("password")
page.locator("button#submit").click()

# 验证用户看到的结果
assert page.is_visible("text=Welcome, admin!")
assert page.url.endswith("/dashboard")

# ❌ 不好的实践:依赖实现细节
def test_login_api_call(page):
with page.expect_request("**/api/login") as request_info:
page.click("button#submit")
request = request_info.value
assert request.post_data_json["username"] == "admin" # 依赖 API 实现细节

保持测试隔离

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 使用 beforeEach 确保测试隔离
def test_user_management(page, login):
# 每个测试都有独立的登录状态
page.goto("/users")
# 测试逻辑...

# 使用上下文隔离多用户场景
def test_multi_user_scenario():
with sync_playwright() as p:
browser = p.chromium.launch()

# 用户 1 上下文
user1_context = browser.new_context()
user1_page = user1_context.new_page()
login(user1_page, "user1", "pass1")

# 用户 2 上下文
user2_context = browser.new_context()
user2_page = user2_context.new_page()
login(user2_page, "user2", "pass2")

# 两个用户的操作完全隔离

9.2 代码组织模式

Page Object Model (POM)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# page_objects/login_page.py
class LoginPage:
def __init__(self, page):
self.page = page
self.username_input = page.locator("input#username")
self.password_input = page.locator("input#password")
self.submit_button = page.locator("button#submit")
self.error_message = page.locator("div.error")

def navigate(self):
self.page.goto("/login")

def login(self, username, password):
self.username_input.fill(username)
self.password_input.fill(password)
self.submit_button.click()

def get_error_message(self):
return self.error_message.text_content()

# tests/test_login.py
def test_successful_login(page):
login_page = LoginPage(page)
login_page.navigate()
login_page.login("admin", "password")

assert page.url.endswith("/dashboard")

def test_invalid_credentials(page):
login_page = LoginPage(page)
login_page.navigate()
login_page.login("admin", "wrong_password")

assert "Invalid credentials" in login_page.get_error_message()

Fixture 模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# conftest.py
import pytest
from playwright.sync_api import sync_playwright

@pytest.fixture(scope="session")
def playwright():
with sync_playwright() as p:
yield p

@pytest.fixture(scope="function")
def browser(playwright):
browser = playwright.chromium.launch(headless=True)
yield browser
browser.close()

@pytest.fixture(scope="function")
def page(browser):
page = browser.new_page()
yield page
page.close()

@pytest.fixture(scope="function")
def authenticated_page(page):
# 登录逻辑
page.goto("/login")
page.locator("input#username").fill("admin")
page.locator("input#password").fill("password")
page.locator("button#submit").click()
return page

9.3 性能优化

选择性截图

1
2
3
4
5
6
7
8
9
10
11
12
# 只在测试失败时截图
def test_critical_functionality(page, request):
try:
# 测试逻辑
page.goto("/critical-feature")
page.click("button#execute")
assert page.is_visible("text=Success")
except Exception as e:
# 失败时截图
screenshot_path = f"screenshots/{request.node.name}.png"
page.screenshot(path=screenshot_path, full_page=True)
raise e

网络优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 阻止不必要的资源加载
def optimize_network(page):
# 阻止图片加载
page.route("**/*.{png,jpg,jpeg,gif,svg}", lambda route: route.abort())

# 阻止第三方脚本
page.route("**/*.{js}", lambda route: route.abort() if "analytics" in route.request.url else route.continue_())

# Mock API 响应
page.route("**/api/slow-data", lambda route: route.fulfill(
status=200,
content_type="application/json",
body='{"data": []}'
))

9.4 调试技巧

可视化调试

1
2
3
4
5
6
7
8
# 慢动作执行
browser = p.chromium.launch(headless=False, slow_mo=100)

# 自动打开开发者工具
browser = p.chromium.launch(headless=False, devtools=True)

# 页面暂停
page.pause() # 会打开 Playwright Inspector

日志记录

1
2
3
4
5
6
7
8
9
10
11
12
13
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

def test_with_logging(page):
logger.info("Starting login test")
page.goto("/login")
logger.info(f"Current URL: {page.url}")

# 记录页面状态
page.on("request", lambda req: logger.debug(f"Request: {req.method} {req.url}"))
page.on("response", lambda res: logger.debug(f"Response: {res.status} {res.url}"))

10. 实战案例

10.1 电商网站测试案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
class ECommerceTest:
def __init__(self, page):
self.page = page
self.product_list = page.locator("div.product-item")
self.add_to_cart_button = page.locator("button.add-to-cart")
self.cart_icon = page.locator("div.cart-icon")
self.checkout_button = page.locator("button.checkout")

def search_product(self, keyword):
"""搜索商品"""
search_input = self.page.locator("input#search")
search_input.fill(keyword)
search_input.press("Enter")
self.page.wait_for_load_state("networkidle")

def add_product_to_cart(self, product_index=0):
"""添加商品到购物车"""
products = self.product_list.all()
if products:
product = products[product_index]
product.hover()
add_button = product.locator("button.add-to-cart")
add_button.click()

# 等待添加成功提示
self.page.locator("div.added-to-cart").wait_for(state="visible")

def go_to_cart(self):
"""进入购物车"""
self.cart_icon.click()
self.page.wait_for_url("**/cart")

def proceed_to_checkout(self):
"""进入结算页面"""
self.checkout_button.click()
self.page.wait_for_url("**/checkout")

def test_ecommerce_flow(page):
"""完整电商流程测试"""
ecommerce = ECommerceTest(page)

# 1. 访问首页
page.goto("https://example-ecommerce.com")

# 2. 搜索商品
ecommerce.search_product("laptop")

# 3. 添加商品到购物车
ecommerce.add_product_to_cart()

# 4. 验证购物车数量
cart_count = ecommerce.cart_icon.text_content()
assert cart_count == "1"

# 5. 进入购物车
ecommerce.go_to_cart()

# 6. 验证商品在购物车中
assert page.is_visible("text=Laptop")

# 7. 进入结算页面
ecommerce.proceed_to_checkout()

# 8. 填写结算信息
page.locator("input#shipping-address").fill("123 Test St")
page.locator("input#credit-card").fill("4111 1111 1111 1111")
page.locator("button#place-order").click()

# 9. 验证订单成功
assert page.is_visible("text=Order placed successfully")
assert page.url.endswith("/order-confirmation")

10.2 数据抓取案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
import csv
import asyncio
from playwright.async_api import async_playwright

class DataScraper:
def __init__(self, browser):
self.browser = browser

async def scrape_page(self, url):
"""抓取单个页面数据"""
page = await self.browser.new_page()
try:
await page.goto(url)
await page.wait_for_load_state("networkidle")

# 抓取数据
products = []
product_elements = await page.query_selector_all("div.product-item")

for element in product_elements:
product = {
"name": await element.locator("h3.product-name").text_content(),
"price": await element.locator("span.price").text_content(),
"rating": await element.locator("div.rating").text_content(),
"image_url": await element.locator("img.product-image").get_attribute("src")
}
products.append(product)

return products
finally:
await page.close()

async def scrape_multiple_pages(self, base_url, total_pages):
"""抓取多页数据"""
all_products = []

for page_num in range(1, total_pages + 1):
url = f"{base_url}?page={page_num}"
print(f"Scraping page {page_num}: {url}")

page_data = await self.scrape_page(url)
all_products.extend(page_data)

# 随机延迟避免被封
await asyncio.sleep(1)

return all_products

def save_to_csv(self, data, filename):
"""保存数据到 CSV 文件"""
if not data:
return

with open(filename, mode='w', newline='', encoding='utf-8') as file:
writer = csv.DictWriter(file, fieldnames=data[0].keys())
writer.writeheader()
writer.writerows(data)

async def main():
async with async_playwright() as p:
# 启动浏览器(无头模式)
browser = await p.chromium.launch(headless=True)

scraper = DataScraper(browser)

# 抓取数据
base_url = "https://example-ecommerce.com/products"
products = await scraper.scrape_multiple_pages(base_url, total_pages=5)

# 保存数据
scraper.save_to_csv(products, "products.csv")

print(f"Scraped {len(products)} products successfully!")

await browser.close()

if __name__ == "__main__":
asyncio.run(main())

10.3 API 测试集成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import pytest
import requests
from playwright.sync_api import sync_playwright

class APITester:
def __init__(self, base_url):
self.base_url = base_url
self.session = requests.Session()

def get_user(self, user_id):
"""获取用户信息"""
response = self.session.get(f"{self.base_url}/api/users/{user_id}")
response.raise_for_status()
return response.json()

def create_user(self, user_data):
"""创建用户"""
response = self.session.post(f"{self.base_url}/api/users", json=user_data)
response.raise_for_status()
return response.json()

def update_user(self, user_id, user_data):
"""更新用户信息"""
response = self.session.put(f"{self.base_url}/api/users/{user_id}", json=user_data)
response.raise_for_status()
return response.json()

@pytest.fixture(scope="module")
def api_tester():
"""API 测试器 fixture"""
return APITester("https://api.example.com")

def test_api_and_ui_integration(api_tester, page):
"""API 和 UI 集成测试"""
# 1. 通过 API 创建测试用户
user_data = {
"name": "Test User",
"email": "test@example.com",
"password": "password123"
}
created_user = api_tester.create_user(user_data)
user_id = created_user["id"]

try:
# 2. 通过 UI 登录
page.goto("https://example.com/login")
page.locator("input#email").fill(user_data["email"])
page.locator("input#password").fill(user_data["password"])
page.locator("button#login").click()

# 3. 验证 UI 显示正确的用户信息
page.goto("https://example.com/profile")
assert page.locator("h1.username").text_content() == user_data["name"]
assert page.locator("div.email").text_content() == user_data["email"]

# 4. 通过 UI 更新用户信息
page.locator("button#edit-profile").click()
page.locator("input#name").fill("Updated Name")
page.locator("button#save").click()

# 5. 通过 API 验证更新结果
updated_user = api_tester.get_user(user_id)
assert updated_user["name"] == "Updated Name"

finally:
# 清理测试数据
api_tester.session.delete(f"{api_tester.base_url}/api/users/{user_id}")

11. 常见问题与解决方案

11.1 元素定位问题

问题:元素定位不稳定

1
2
3
4
5
6
7
# ❌ 不稳定的定位方式
page.locator("div:nth-child(3) > button").click() # 依赖页面结构

# ✅ 稳定的定位方式
page.locator("button#submit").click() # 使用 ID
page.locator("button[type='submit']").click() # 使用属性
page.locator("text=Submit").click() # 使用文本

解决方案:

  1. 优先使用 ID 和 name 属性:最稳定的定位方式

  2. 使用文本定位:对于用户可见的元素

  3. 组合定位策略:提高定位的唯一性

  4. 等待元素稳定:使用 wait_for() 方法

11.2 异步加载问题

问题:页面内容动态加载导致元素找不到

1
2
3
4
5
6
7
8
9
10
11
# ❌ 直接操作,没有等待
page.goto("/products")
page.locator("div.product").click() # 可能还没加载完成

# ✅ 等待元素加载
page.goto("/products")
page.locator("div.product").wait_for(state="visible")
page.locator("div.product").click()

# ✅ 等待网络空闲
page.goto("/products", wait_until="networkidle")

解决方案:

  1. 使用 wait_until 参数:确保页面完全加载

  2. 等待特定元素:使用 wait_for() 方法

  3. 监听网络请求:确保关键 API 调用完成

  4. 使用 page.wait_for_function():等待自定义条件

11.3 测试稳定性问题

问题:测试时有时无地失败(Flaky Tests)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# ❌ 不稳定的测试
def test_flaky(page):
page.goto("/form")
page.locator("input#name").fill("Test")
page.locator("button#submit").click()
assert page.is_visible("text=Success") # 可能还没显示

# ✅ 稳定的测试
def test_stable(page):
page.goto("/form")
page.locator("input#name").fill("Test")

# 使用 with 语句等待导航完成
with page.expect_navigation():
page.locator("button#submit").click()

# 等待成功消息显示
success_message = page.locator("text=Success")
success_message.wait_for(state="visible")
assert success_message.is_visible()

解决方案:

  1. 使用 expect_navigation():等待页面跳转完成

  2. 明确等待条件:不要依赖隐式等待

  3. 增加重试机制:对不稳定的测试增加重试

  4. 使用 tracing:记录测试执行过程便于分析

11.4 性能优化问题

问题:测试执行速度慢

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# ❌ 每次测试都启动新浏览器
def test_slow_1():
with sync_playwright() as p:
browser = p.chromium.launch()
# 测试逻辑...

# ✅ 复用浏览器实例
@pytest.fixture(scope="session")
def browser():
with sync_playwright() as p:
browser = p.chromium.launch()
yield browser
browser.close()

# ✅ 使用无头模式
browser = p.chromium.launch(headless=True)

# ✅ 阻止不必要的资源
page.route("**/*.{png,jpg,jpeg,gif}", lambda route: route.abort())

解决方案:

  1. 复用浏览器实例:减少启动时间

  2. 使用无头模式:提高执行速度

  3. 网络优化:阻止不必要的资源加载

  4. 并行执行:利用多核 CPU 资源

11.5 跨浏览器兼容性问题

问题:在不同浏览器中表现不一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 解决方案:参数化测试不同浏览器
import pytest

@pytest.mark.parametrize("browser_type", ["chromium", "firefox", "webkit"])
def test_cross_browser(browser_type, playwright):
browser = getattr(playwright, browser_type).launch()
page = browser.new_page()

try:
page.goto("https://example.com")
# 执行测试逻辑
assert "Example Domain" in page.title()
finally:
browser.close()

解决方案:

  1. 参数化测试:在多个浏览器中运行相同测试

  2. 统一选择器策略:避免浏览器特定的选择器

  3. 处理浏览器差异:对特定浏览器添加兼容代码

  4. 使用设备模拟:测试移动端兼容性

11.6 调试技巧

使用 Playwright Inspector

1
2
# 启动 Inspector
PWDEBUG=1 python your_script.py

生成测试报告

1
2
3
4
5
6
# 使用 pytest-html 生成报告
pytest --html=report.html --self-contained-html

# 使用 allure 生成美观的报告
pytest --alluredir=allure-results
allure serve allure-results

录制和回放

1
2
3
4
5
# 录制用户操作生成代码
playwright codegen https://example.com

# 录制网络请求
playwright codegen --save-storage=auth.json https://example.com/login