Playwright自动化测试实战指南

发布于:2025-06-27 ⋅ 阅读:(19) ⋅ 点赞:(0)

Playwright 自动化测试教程

1. Playwright 简介

Playwright 是一个现代化的端到端测试框架,支持 Chromium、Firefox 和 WebKit 浏览器。它提供跨平台的测试能力,支持 Windows、Linux 和 macOS 系统。

主要特点:

  • 支持多种浏览器
  • 跨平台测试
  • 支持无头(Headless)和有头模式:
    • 无头模式:浏览器在后台运行,不显示UI界面,适合自动化测试和CI/CD环境
    • 有头模式:浏览器显示完整UI界面,适合调试和开发阶段观察测试过程
  • 移动设备模拟

2. 安装与设置

Node.js 环境

npm init playwright@latest

Python 环境

pip install playwright
playwright install

3. 基础用法

启动浏览器

const { chromium } = require('playwright');

(async () => {
  const browser = await chromium.launch();
  const page = await browser.newPage();
  await page.goto('https://example.com');
  await browser.close();
})();

Python 同步模式

from playwright.sync_api import sync_playwright

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

4. 元素定位与交互

基本定位方法

// 通过文本定位
await page.getByText('Submit').click();

// 通过角色定位
await page.getByRole('button', { name: 'Sign in' }).click();

// 通过标签定位
await page.getByLabel('User Name').fill('John');

常见交互操作

// 点击
await page.locator('#submit').click();

// 输入文本
await page.locator('#search').fill('query');

// 拖放
await source.dragTo(target);

5. 断言与验证

基本断言

// 验证标题
await expect(page).toHaveTitle(/Playwright/);

// 验证元素可见
await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();

// 验证元素包含文本
await expect(page.locator('.status')).toContainText('Success');

6. 高级功能

截图

// 全屏截图
await page.screenshot({ path: 'screenshot.png' });

// 元素截图
await page.locator('.header').screenshot({ path: 'header.png' });

HTTP 认证

const context = await browser.newContext({
  httpCredentials: {
    username: 'bill',
    password: 'pa55w0rd'
  }
});

视觉回归测试

test('example test', async ({ page }) => {
  await page.goto('https://playwright.dev');
  await expect(page).toHaveScreenshot();
});

7. 测试框架集成

Playwright Test

import { test, expect } from '@playwright/test';

test('basic test', async ({ page }) => {
  await page.goto('https://playwright.dev/');
  const name = await page.innerText('.navbar__title');
  expect(name).toBe('Playwright');
});

Python pytest

import re
from playwright.sync_api import Page, expect

def test_has_title(page: Page):
    page.goto("https://playwright.dev/")
    expect(page).to_have_title(re.compile("Playwright"))

在这里插入图片描述

设置用户数据目录,保存用户状态

下次自动打开浏览器的时候,能保持用户数据和登陆状态

with sync_playwright() as p:
  user_data_dir = "user_data"  # 用户数据目录,用于保存cookie等
  context = p.chromium.launch_persistent_context(
      user_data_dir,
      headless=False
  )
  page = context.new_page()
  page.goto("https://www.xiaohongshu.com/explore")

8. 最佳实践

  1. 使用明确的定位器而非XPath
  2. 优先使用getByRole、getByText等语义化定位方法
  3. 合理使用waitFor系列方法
  4. 保持测试独立性和可重复性
  5. 使用Page Object模式组织测试代码

9. 常见问题

元素定位失败

  • 检查元素是否在iframe中
  • 确认元素已加载完成
  • 使用page.pause()调试

测试不稳定

  • 增加适当的等待
  • 使用可靠的定位策略
  • 避免依赖外部服务

10. 资源

11. Python Playwright 综合案例

下面是一个综合案例,展示了如何使用 Python 和 Playwright 进行网页自动化操作,包括网页截图、数据提取和表单交互等功能。

11.1 电商网站商品信息爬取

这个案例演示了如何使用 Playwright 爬取电商网站的商品信息,包括标题、价格和评分等数据。

import asyncio
from playwright.async_api import async_playwright
import pandas as pd
import time

async def scrape_products():
    """爬取电商网站商品信息"""
    
    # 存储结果的列表
    products = []
    
    async with async_playwright() as p:
        # 启动浏览器(使用有头模式便于观察)
        browser = await p.chromium.launch(headless=False)
        
        # 创建新的上下文和页面
        context = await browser.new_context(
            viewport={"width": 1280, "height": 800},
            user_agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36"
        )
        page = await context.new_page()
        
        # 导航到目标网站
        await page.goto("https://demo.opencart.com/")
        
        # 等待页面加载完成
        await page.wait_for_load_state("networkidle")
        
        # 截图首页
        await page.screenshot(path="homepage.png", full_page=True)
        
        # 点击进入笔记本电脑分类
        await page.click("text=Laptops & Notebooks")
        await page.click("text=Show All Laptops & Notebooks")
        
        # 等待产品列表加载
        await page.wait_for_selector(".product-layout")
        
        # 提取产品信息
        product_cards = await page.query_selector_all(".product-layout")
        
        for card in product_cards:
            try:
                # 提取产品名称
                name_element = await card.query_selector("h4 a")
                name = await name_element.inner_text() if name_element else "未知产品"
                
                # 提取产品价格
                price_element = await card.query_selector(".price")
                price = await price_element.inner_text() if price_element else "价格未知"
                
                # 提取产品评分
                rating_element = await card.query_selector(".rating")
                rating_stars = await rating_element.query_selector_all("span.fa-stack") if rating_element else []
                rating = len([star for star in rating_stars if await star.get_attribute("class") and "fa-star" in await star.get_attribute("class")])
                
                # 获取产品链接
                link_element = await card.query_selector("h4 a")
                link = await link_element.get_attribute("href") if link_element else ""
                
                # 添加到产品列表
                products.append({
                    "name": name,
                    "price": price.split("\n")[0] if "\n" in price else price,  # 处理可能的特价显示
                    "rating": rating,
                    "link": link
                })
                
            except Exception as e:
                print(f"提取产品信息时出错: {e}")
        
        # 将结果保存为CSV
        df = pd.DataFrame(products)
        df.to_csv("products.csv", index=False, encoding="utf-8")
        
        # 关闭浏览器
        await browser.close()
        
        return products

# 运行爬虫
if __name__ == "__main__":
    products = asyncio.run(scrape_products())
    print(f"共爬取了 {len(products)} 个产品")
    for product in products:
        print(f"产品: {product['name']}, 价格: {product['price']}, 评分: {product['rating']}星")

11.2 自动化表单填写与提交

这个案例展示了如何使用 Playwright 自动填写表单并提交,包括处理不同类型的表单元素。

from playwright.sync_api import sync_playwright
import time

def automate_form_submission():
    """自动化表单填写与提交"""
    
    with sync_playwright() as p:
        # 启动浏览器
        browser = p.chromium.launch(headless=False)
        
        # 创建新的上下文和页面
        page = browser.new_page()
        
        # 导航到表单页面
        page.goto("https://demoqa.com/automation-practice-form")
        
        # 填写个人信息
        page.fill("#firstName", "张")
        page.fill("#lastName", "三")
        page.fill("#userEmail", "zhangsan@example.com")
        
        # 选择性别单选按钮
        page.click("label[for='gender-radio-1']")
        
        # 填写手机号码
        page.fill("#userNumber", "1381234567")
        
        # 选择出生日期
        page.click("#dateOfBirthInput")
        page.select_option(".react-datepicker__month-select", "3")  # 4月
        page.select_option(".react-datepicker__year-select", "1990")
        page.click(".react-datepicker__day--010")  # 选择10号
        
        # 填写学科
        page.click("#subjectsContainer")
        page.type("#subjectsInput", "Math")
        page.press("#subjectsInput", "Enter")
        page.type("#subjectsInput", "Computer")
        page.press("#subjectsInput", "Enter")
        
        # 选择爱好复选框
        page.click("label[for='hobbies-checkbox-1']")  # 运动
        page.click("label[for='hobbies-checkbox-3']")  # 音乐
        
        # 上传图片
        # 注意:需要提供一个实际存在的文件路径
        # page.set_input_files("#uploadPicture", "path/to/picture.jpg")
        
        # 填写当前地址
        page.fill("#currentAddress", "北京市海淀区中关村大街1号")
        
        # 选择州和城市
        page.click("#state")
        page.click("text=NCR")
        page.click("#city")
        page.click("text=Delhi")
        
        # 提交表单
        page.click("#submit")
        
        # 等待提交后的确认对话框
        page.wait_for_selector(".modal-content")
        
        # 截图确认结果
        page.screenshot(path="form_submission_result.png")
        
        # 验证提交成功
        success_text = page.inner_text(".modal-content")
        print("表单提交结果:", success_text)
        
        # 关闭浏览器
        browser.close()

if __name__ == "__main__":
    automate_form_submission()

11.3 多浏览器并行测试

这个案例展示了如何使用 Playwright 在多个浏览器上并行运行测试,比较不同浏览器的性能和兼容性。

import asyncio
from playwright.async_api import async_playwright
import time

async def run_performance_test():
    """在多个浏览器上并行测试网站性能"""
    
    async with async_playwright() as p:
        # 定义要测试的浏览器
        browsers = [
            {"name": "Chromium", "browser_type": p.chromium},
            {"name": "Firefox", "browser_type": p.firefox},
            {"name": "WebKit", "browser_type": p.webkit}
        ]
        
        # 定义测试网站
        test_sites = [
            {"name": "Google", "url": "https://www.google.com"},
            {"name": "GitHub", "url": "https://github.com"},
            {"name": "Stack Overflow", "url": "https://stackoverflow.com"}
        ]
        
        results = []
        
        # 创建测试任务
        tasks = []
        for browser in browsers:
            for site in test_sites:
                tasks.append(test_site_performance(browser, site))
        
        # 并行运行所有测试
        results = await asyncio.gather(*tasks)
        
        # 打印结果
        print("\n性能测试结果:")
        print("-" * 80)
        print(f"{'浏览器':<10} {'网站':<15} {'加载时间(ms)':<15} {'内存使用(MB)':<15}")
        print("-" * 80)
        
        for result in results:
            print(f"{result['browser']:<10} {result['site']:<15} {result['load_time']:<15.2f} {result['memory']:<15.2f}")
        
        return results

async def test_site_performance(browser_info, site_info):
    """测试特定浏览器和网站的性能"""
    
    browser_name = browser_info["name"]
    site_name = site_info["name"]
    site_url = site_info["url"]
    
    # 启动浏览器
    browser = await browser_info["browser_type"].launch()
    
    try:
        # 创建新的上下文和页面
        context = await browser.new_context()
        page = await context.new_page()
        
        # 测量页面加载时间
        start_time = time.time()
        
        # 导航到网站
        response = await page.goto(site_url)
        
        # 等待页面完全加载
        await page.wait_for_load_state("networkidle")
        
        # 计算加载时间(毫秒)
        load_time = (time.time() - start_time) * 1000
        
        # 获取内存使用情况
        client = await page.context.new_cdp_session(page)
        performance_stats = await client.send("Performance.getMetrics")
        memory_usage = next((metric["value"] for metric in performance_stats["metrics"] 
                            if metric["name"] == "JSHeapUsedSize"), 0) / (1024 * 1024)  # 转换为MB
        
        # 截图
        await page.screenshot(path=f"{browser_name}_{site_name}.png")
        
        return {
            "browser": browser_name,
            "site": site_name,
            "load_time": load_time,
            "memory": memory_usage
        }
    
    finally:
        # 关闭浏览器
        await browser.close()

# 运行测试
if __name__ == "__main__":
    asyncio.run(run_performance_test())

11.4 网页监控与自动化报告

这个案例展示了如何使用 Playwright 定期监控网站状态,并生成报告。

import asyncio
from playwright.async_api import async_playwright
import datetime
import pandas as pd
import matplotlib.pyplot as plt
import os
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.image import MIMEImage

# 监控配置
MONITORING_INTERVAL = 3600  # 每小时检查一次
WEBSITES = [
    {"name": "Google", "url": "https://www.google.com"},
    {"name": "Baidu", "url": "https://www.baidu.com"},
    {"name": "GitHub", "url": "https://github.com"}
]
REPORT_DIR = "monitoring_reports"

# 确保报告目录存在
os.makedirs(REPORT_DIR, exist_ok=True)

async def monitor_website(website):
    """监控单个网站的状态和性能"""
    
    name = website["name"]
    url = website["url"]
    timestamp = datetime.datetime.now()
    
    async with async_playwright() as p:
        try:
            # 启动浏览器
            browser = await p.chromium.launch()
            context = await browser.new_context()
            page = await context.new_page()
            
            # 记录开始时间
            start_time = time.time()
            
            # 导航到网站
            response = await page.goto(url, timeout=30000)
            status = response.status if response else 0
            
            # 等待页面加载完成
            await page.wait_for_load_state("networkidle")
            
            # 计算加载时间
            load_time = (time.time() - start_time) * 1000  # 毫秒
            
            # 截图
            screenshot_path = f"{REPORT_DIR}/{name}_{timestamp.strftime('%Y%m%d_%H%M%S')}.png"
            await page.screenshot(path=screenshot_path, full_page=True)
            
            # 关闭浏览器
            await browser.close()
            
            return {
                "name": name,
                "url": url,
                "timestamp": timestamp,
                "status": status,
                "load_time": load_time,
                "screenshot_path": screenshot_path,
                "success": True,
                "error": None
            }
            
        except Exception as e:
            # 处理错误
            return {
                "name": name,
                "url": url,
                "timestamp": timestamp,
                "status": 0,
                "load_time": 0,
                "screenshot_path": None,
                "success": False,
                "error": str(e)
            }

async def run_monitoring_cycle():
    """运行一次完整的监控周期"""
    
    print(f"开始监控周期: {datetime.datetime.now()}")
    
    # 并行监控所有网站
    tasks = [monitor_website(website) for website in WEBSITES]
    results = await asyncio.gather(*tasks)
    
    # 保存结果到CSV
    timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
    df = pd.DataFrame(results)
    csv_path = f"{REPORT_DIR}/monitoring_{timestamp}.csv"
    df.to_csv(csv_path, index=False)
    
    # 生成报告
    generate_report(results, timestamp)
    
    print(f"监控周期完成: {datetime.datetime.now()}")
    return results


网站公告

今日签到

点亮在社区的每一天
去签到