Playwright Python 教程:中级篇

目标

本篇将帮助你从"能用"到"用好",重点讲解如何处理真实测试场景中的复杂情况,让你的脚本更加稳定和高效。


一、高级等待策略:告别 time.sleep

在基础篇中,我们提到了要避免使用 page.wait_for_timeout()。这是因为固定等待时间非常不可靠(网络或机器速度可能导致元素加载或快或慢)。Playwright 提供了内置的自动等待和更智能的显式等待机制。

1. 内置的自动等待(Auto-waiting)
Playwright 的核心优势。在执行操作(如 click, fill)之前,它会自动执行一系列可操作性检查:

  • 元素是否存在(Attached)
  • 元素是否可见(Visible)
  • 元素是否稳定(Not animated, Stable)
  • 元素是否可交互(Enabled, Editable)
  • 元素是否接收到事件(Not obscured)

这意味着你通常不需要手动等待元素出现,直接操作即可。page.click(selector) 会一直等待该元素可点击(最多 30s,超时则报错)。

2. 显式等待(Explicit Waits)
有些情况下,你需要等待一个特定的条件发生,而不是某个元素可操作。这时就需要显式等待。

  • page.wait_for_selector(selector, state="visible", timeout=30000): 等待匹配选择器的元素达到特定状态(如 "visible", "hidden", "attached", "detached")。

    # 等待一个加载 spinner 消失
    page.wait_for_selector(".spinner", state="hidden")
    # 等待成功消息出现
    page.wait_for_selector("text=Operation successful", state="visible")
    
  • page.wait_for_url(url, timeout=30000): 等待页面导航到指定的 URL(支持通配符和正则表达式)。

    # 等待页面跳转到 dashboard 页
    page.wait_for_url("**/dashboard")
    
  • page.wait_for_function(js_function, arg=None, timeout=30000): 等待页面中的 JavaScript 函数返回真值。这是最灵活的等待方式。

    # 等待 window.isDataReady 变量变为 true
    page.wait_for_function("window.isDataReady === true")
    # 等待文档的高度超过 1000px
    page.wait_for_function("() => document.body.scrollHeight > 1000")
    
  • page.wait_for_timeout(milliseconds): 最后的手段。只有在没有其他更好方法时(例如等待一个非前端的后端处理),才使用固定等待。


二、处理复杂的 UI 组件

1. 下拉选择框(Select Dropdowns)
Playwright 为 <select> 元素提供了专用的 API,非常方便。

# 导航到有下拉框的页面,例如 https://demoqa.com/select-menu
page.goto("https://demoqa.com/select-menu")

# 通过标签选择 'Green'
page.select_option("#oldSelectMenu", "green")

# 通过值(value)选择 'Blue'
page.select_option("#oldSelectMenu", "blue")

# 或者通过索引选择 (索引从0开始)
page.select_option("#oldSelectMenu", index=3)

2. 文件上传(File Uploads)
处理 <input type="file"> 元素非常简单。

# 设置文件路径,一次选择一个文件
page.set_input_files('input[type="file"]', "path/to/myfile.pdf")

# 一次选择多个文件
page.set_input_files('input[type="file"]', ["file1.pdf", "file2.jpg"])

# 清空选择(将 input 的值设为空)
page.set_input_files('input[type="file"]', [])

3. 处理对话框(Dialogs: Alert, Confirm, Prompt)
Playwright 可以监听并响应 JavaScript 的原生对话框。

# 在点击触发对话框的按钮之前,先注册一个监听器
page.once("dialog", lambda dialog: dialog.accept())  # 默认点击 "OK"
# 或者,也可以获取对话框消息并做出选择
def handle_dialog(dialog):
    print(f"对话框消息: {dialog.message}")
    if "确认删除" in dialog.message:
        dialog.accept()  # 点击 "OK" 或 "Confirm"
    else:
        dialog.dismiss()  # 点击 "Cancel"

page.on("dialog", handle_dialog)

# 现在执行会触发对话框的操作,例如点击删除按钮
page.click("#delete-button")
# 操作完成后,最好移除监听器,以免影响后续操作
page.remove_listener("dialog", handle_dialog)

三、Frame 和 Iframe 处理

网页中的 iframe 就像一个"页面中的页面"。要操作 iframe 内的元素,必须先切换到对应的 frame 对象上。

1. 通过 Name/URL 或选择器定位 Frame

# 通过 frame 的 name 属性或 URL 匹配
frame = page.frame(name="my-frame")
frame = page.frame(url="**/login.html")

# 通过 CSS/XPath 选择器定位 iframe 元素,然后获取其对应的 frame 对象
iframe_element = page.query_selector(".my-iframe")
frame = iframe_element.content_frame

2. 在 Frame 内操作元素
获取到 frame 对象后,其 API 和 page 对象几乎一样。

# 在 frame 内点击、填写
frame.click("button")
frame.fill("#username", "myuser")

# 也可以使用 with 语句进行上下文切换,这是一种更清晰的方式
with page.frame("my-frame"):
    page.click("button")  # 这里的 page 操作会自动限定在 frame 内

四、管理多个 Page 和 Tab

点击一个链接有时会打开新标签页或新窗口。Playwright 可以轻松管理多个页面。

# 在点击之前,监听新的 page 事件
with page.expect_popup() as popup_info:
    page.click('a[target="_blank"]')  # 点击一个会打开新窗口的链接

# 获取新打开的 page 对象
new_page = popup_info.value

# 等待新页面加载完成
new_page.wait_for_load_state()

# 在新页面上进行操作
print(new_page.title())

# 操作完成后,关闭新页面
new_page.close()

# 切回原始页面继续操作
page.bring_to_front()  # 将原始页面提到最前(激活状态)
page.click("#back-to-original")

五、实战演练:一个综合案例

让我们模拟一个稍复杂的场景:登录一个需要处理 iframe 和等待的网站。

from playwright.sync_api import sync_playwright

def test_complex_login():
    with sync_playwright() as p:
        # 启动浏览器,设置视口大小
        browser = p.chromium.launch(headless=False)
        context = browser.new_context(viewport={"width": 1920, "height": 1080})
        page = context.new_page()

        # 1. 导航到登录页
        page.goto("https://example.com/login")

        # 2. 处理登录表单(假设它在 iframe 里)
        login_frame = page.frame(url="**/login-iframe")
        assert login_frame, "Login iframe not found"

        # 在 iframe 内操作
        login_frame.fill("#username", "testuser")
        login_frame.fill("#password", "testpass")

        # 3. 点击登录按钮,等待导航到仪表盘(Dashboard)
        with page.expect_navigation():  # 等待主页面发生导航
            login_frame.click("#submit-btn")

        # 4. 等待仪表盘上的某个关键元素出现
        page.wait_for_selector("text=Welcome, testuser", state="visible")
        page.wait_for_url("**/dashboard")

        # 5. 进行一些操作,例如上传头像
        # 假设点击一个按钮会触发文件选择对话框
        page.click("#upload-avatar")
        page.set_input_files('input[type="file"]', "avatar.jpg")

        # 6. 等待上传成功的 Toast 消息出现并消失
        page.wait_for_selector("text=Upload successful", state="visible")
        page.wait_for_selector("text=Upload successful", state="hidden")

        # 7. 断言头像图片的 src 属性已更新
        avatar_src = page.get_attribute("#user-avatar", "src")
        assert avatar_src and "avatar" in avatar_src, "Avatar was not updated"

        print("✅ 综合测试通过!")
        browser.close()

if __name__ == "__main__":
    test_complex_login()

中级篇总结

你现在已经掌握了处理真实世界 Web 应用自动化测试的中级技能:

  1. 智能等待:使用 wait_for_selector, wait_for_function 等替代固定等待,让脚本更稳定。
  2. 复杂组件交互:熟练处理下拉框文件上传JavaScript 对话框
  3. Frame 处理:能够定位 frame 对象并在其上下文内进行操作。
  4. 多页面管理:使用 expect_popup 来捕获和控制新打开的标签页。

下一步(高级篇预览)
在高级篇,我们将探索真正强大的功能,迈向专家水平:

  • 网络拦截与模拟:mock API 响应,修改请求头,实现"无依赖测试"。
  • 认证与持久化状态:如何保存登录状态,实现一次登录多次测试。
  • 高级浏览器配置:代理设置、语言、地理位置模拟。
  • 录制视频与追踪:生成测试执行的视频和可视化报告。
  • 与 Pytest 集成:如何组织测试用例、夹具(fixtures)和生成报告。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐