Playwright 是一个强大的现代开源自动化库,专门为端到端测试而设计。它由微软的团队开发并维护,支持使用单一 API 来操控多种浏览器。

Playwright 之所以迅速流行,是因为它解决了许多传统自动化工具(如 Selenium)的痛点。

  1. 跨浏览器支持

    • 支持所有现代浏览器引擎:Chromium(Chrome, Edge)、FirefoxWebKit(Safari)。

    • 这意味着你可以在 Windows 上测试 Safari,确保了跨平台的一致性。

  2. 跨平台、多语言

    • 平台: 支持 Windows, macOS, Linux(包括在 Docker 容器中)。

    • 语言: 提供对 TypeScript/JavaScriptPython.NET (C#)Java 的一流支持,API 在不同语言间保持一致。

  3. 强大的自动化能力

    • 自动等待: Playwright 在执行操作前会自动等待元素可用的状态(如可点击、可见),无需手动添加 sleep 或复杂等待,大大提高了测试的稳定性和编写效率。

    • 网络拦截: 可以轻松地模拟和修改网络请求,例如拦截 API 调用、模拟慢速网络或离线状态。

    • 多标签页、多上下文: 能够处理多个浏览器标签页、甚至无痕浏览器上下文,这对于测试多用户场景或单点登录非常有用。

    • 文件下载与上传: 原生支持文件上传和下载操作的自动化。

    • Shadow DOM 支持: 对 Web Components 中的 Shadow DOM 有很好的支持,可以直接定位其中的元素。

  4. 速度快、可靠性高

    • 无头模式: 默认在无头模式下运行,速度极快。

    • 浏览器上下文: 通过创建独立的浏览器上下文,可以实现并行测试,且彼此隔离,互不干扰。

    • 追踪与调试: 内置强大的工具,可以记录测试执行过程的视频、截图和操作日志,方便调试失败的测试。

  5. 现代化的测试框架集成

    • 可以轻松与流行的测试运行器集成,如 JestMochaPlaywright Test(其自带的、功能丰富的测试运行器)、Pytest 等。

相关网址:

安装

pip install playwright

使用如下命令来安装playwright所需的引擎

playwright install

QuickStart

在我们的第一个脚本中,我们将导航到 https://playwright.nodejs.cn/ 并在 WebKit 中截取屏幕截图。你可以使用 Python 脚本运行 import Playwright,并启动 3 个浏览器(chromiumfirefoxwebkit)中的任意一个。

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    page.goto("http://www.baidu.com")
    page.screenshot(path="./example.png")
    browser.close()

默认情况下,Playwright 以无头模式运行浏览器。要查看浏览器 UI,请将 headless 选项设置为 False。你还可以使用 slow_mo 来减慢执行速度。在调试工具 section 中了解更多信息。

chromium.launch(headless=False, slow_mo=50)

Playwright 支持两种 API 变体:同步和异步。如果你的现代项目使用 asyncio,则应该使用异步 API:

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("http://www.baidu.com")
        print(await page.title())
        await browser.close()

asyncio.run(main())

浏览器操作

打开浏览器

with sync_playwright() as p:
    browser = p.chromium.launch() # 打开chrome
    browser = p.firefox.launch() # 打开firefox
    browser = p.webkit.launch() # 打开webkit

浏览器启动参数

p.chromium.launch()中可以通过设置启动参数改变浏览器的启动行为:

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(
        # 基本参数
        headless=False,           # 是否无头模式,False显示浏览器窗口
        executable_path="/path/to/browser",  # 指定浏览器可执行文件路径
        
        # 性能和控制参数
        slow_mo=1000,             # 操作间隔延迟(毫秒)
        timeout=30000,            # 操作超时时间(毫秒)
        
        # 设置浏览器参数
        args=[
            "--disable-web-security",      # 禁用web安全
            "--no-sandbox",                # 禁用沙盒
            "--disable-setuid-sandbox",    # 禁用setuid沙盒
            "--disable-dev-shm-usage",     # 禁用/dev/shm使用
            "--disable-accelerated-2d-canvas",
            "--disable-gpu",               # 禁用GPU硬件加速
            "--window-size=1920,1080",     # 窗口大小
        ],
        
        # 环境变量
        env={"ENV_VAR": "value"},
        
        # 代理设置
        proxy={
            "server": "http://proxy.example.com:8080",
            "username": "user",    # 可选
            "password": "pass"     # 可选
        },
        
        # 下载设置
        downloads_path="/path/to/downloads",  # 下载文件保存路径
        
        # 其他参数
        devtools=False,            # 是否自动打开开发者工具
        handle_sigint=True,        # 是否处理SIGINT信号
        handle_sigterm=True,       # 是否处理SIGTERM信号  
        handle_sighup=True,        # 是否处理SIGHUP信号
        
        # Chromium 特有但其他浏览器也兼容的参数
        ignore_default_args=False, # 是否忽略默认参数
        ignore_https_errors=False, # 是否忽略HTTPS错误
        
        # 高级参数
        firefox_user_prefs={},     # Firefox 用户首选项
        chromium_sandbox=False,    # 是否启用Chromium沙盒
    )

具体的浏览器参数

窗口和显示

common_args = [
    "--window-size=1920,1080",
    "--start-maximized",
    "--kiosk",
]

安全和权限

common_args = [
    "--disable-web-security",
    "--allow-running-insecure-content",
    "--disable-features=VizDisplayCompositor",
    "--allow-insecure-localhost",
]

性能和资源

common_args = [
    "--disable-background-timer-throttling",
    "--disable-backgrounding-occluded-windows",
    "--disable-renderer-backgrounding",
    "--disable-dev-shm-usage",
    "--disable-accelerated-2d-canvas",
    "--disable-gpu",
    "--no-sandbox",
    "--disable-setuid-sandbox",
    "--memory-pressure-off",
]

功能和扩展

common_args = [
    "--disable-extensions",
    "--disable-plugins",
    "--disable-translate",
    "--disable-sync",
    "--disable-default-apps",
    "--disable-component-extensions-with-background-pages",
]

自动化和测试

common_args = [
    "--disable-blink-features=AutomationControlled",
    "--enable-automation",
    "--remote-debugging-port=0",
    "--password-store=basic",
    "--use-mock-keychain",
]

用户界面

common_args = [
    "--hide-scrollbars",
    "--mute-audio",
    "--no-first-run",
    "--no-default-browser-check",
    "--disable-popup-blocking",
]

网络和代理

common_args = [
    "--proxy-bypass-list=*",
    "--proxy-server=http://proxy:8080",
]

语言和区域

common_args = [
    "--lang=zh-CN",
    "--accept-lang=zh-CN,zh;q=0.9,en;q=0.8",
]

Chromium/Chrome 特有参数

chromium_specific_args = [
    "--disable-features=TranslateUI",
    "--disable-ipc-flooding-protection",
    "--disable-hang-monitor",
    "--disable-back-forward-cache",
    "--disable-site-isolation-trials",
]

Firefox 特有参数

firefox_specific_args = [
    "--safe-mode",
    "--new-instance",
    "--width=1920",
    "--height=1080",
]

WebKit 特有参数

webkit_specific_args = [
    "--disable-web-security",
    "--allow-running-insecure-content",
]

关闭浏览器

with sync_playwright() as p:
    browser = p.chromium.launch(headless=False)
    browser.close() # 关闭浏览器

标签页管理

新建标签页

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=False)
    page = browser.new_page() # 新建标签页

获取所有标签页

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    
    # 获取浏览器中的所有标签页
    all_pages = browser.contexts[0].pages  # 第一个上下文中的所有页面
    print(f"标签页数量: {len(all_pages)}")

    # 遍历所有标签页
    for i, page in enumerate(all_pages):
        print(f"标签页 {i}: {page.url}")

    # 获取特定上下文中的标签页
    browser.close()

获取标签页属性

# 基本属性
print(f"URL: {page.url}")
print(f"标题: {page.title()}")
print(f"页面HTML: {page.content()}")

# 页面状态
print(f"是否关闭: {page.is_closed()}")
print(f"页面加载状态: {page.evaluate('() => document.readyState')}")

# 视口信息
viewport = page.viewport_size
print(f"视口大小: {viewport}")

# 用户代理
user_agent = page.evaluate("() => navigator.userAgent")
print(f"User Agent: {user_agent}")

标签导航

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

# 获取导航历史
print(f"可以后退: {page.can_go_back()}")
print(f"可以前进: {page.can_go_forward()}")

标签页截图和PDF

# 截取整个页面
page.screenshot(path="fullpage.png", full_page=True)

# 截取可见区域
page.screenshot(path="viewport.png")

# 截取特定元素
element = page.locator(".header")
element.screenshot(path="header.png")

# 生成PDF
page.pdf(path="page.pdf", format="A4")

关闭标签页

page.close()

元素定位

使page.locator()可以进行元素定位

选择器语法

在介绍元素定位前,首先介绍一下CCS选择器语法。下面详细解释这些语法和其他常见的 CSS 选择器。

基础选择器

/* 元素选择器 */
div          /* 选择所有 div 元素 */
button       /* 选择所有 button 元素 */
span         /* 选择所有 span 元素 */

/* 类选择器 */
.submit      /* 选择 class 包含 submit 的元素 */
.btn.primary /* 选择同时有 btn 和 primary 类的元素 */

/* ID 选择器 */
#username    /* 选择 id 为 username 的元素 */
#main        /* 选择 id 为 main 的元素 */

/* 通配符选择器 */
*            /* 选择所有元素 */

/* 属性选择器 */
[type]       /* 选择有 type 属性的元素 */

组合选择器

/* 后代选择器(空格) */
div span           /* 选择 div 内的所有 span(任意嵌套) */
.form .input       /* 选择 .form 内的所有 .input */

/* 子元素选择器(>) */
div > span         /* 选择直接子元素 span */
ul > li            /* 选择 ul 的直接子元素 li */

/* 相邻兄弟选择器(+) */
h1 + p             /* 选择紧接在 h1 后面的 p 元素 */
.error + .input    /* 选择紧接在 .error 后面的 .input */

/* 通用兄弟选择器(~) */
h1 ~ p             /* 选择 h1 后面的所有 p 兄弟元素 */

属性选择器

/* 基础属性选择器 */
[type]              /* 选择有 type 属性的元素 */
[type="text"]       /* 选择 type="text" 的元素 */
[class~="btn"]      /* 选择 class 包含完整单词 "btn" 的元素 */

/* 子字符串匹配属性选择器 */
[class*="error"]    /* 选择 class 包含 "error" 的元素 */
[href^="https"]     /* 选择 href 以 "https" 开头的元素 */
[src$=".jpg"]       /* 选择 src 以 ".jpg" 结尾的元素 */
[class|="lang"]     /* 选择 class 以 "lang-" 开头的元素 */

/* 大小写敏感匹配 */
[type="submit" i]   /* 不区分大小写匹配 type 属性 */

伪类选择器

/* 状态伪类 */
:hover              /* 鼠标悬停时的元素 */
:focus              /* 获得焦点的元素 */
:active             /* 被激活的元素 */
:visited            /* 已访问的链接 */
:checked            /* 被选中的复选框/单选按钮 */
:disabled           /* 被禁用的元素 */
:enabled            /* 可用的元素 */

/* 结构伪类 */
:first-child        /* 父元素的第一个子元素 */
:last-child         /* 父元素的最后一个子元素 */
:nth-child(n)       /* 父元素的第 n 个子元素 */
:nth-last-child(n)  /* 父元素的倒数第 n 个子元素 */
:only-child         /* 父元素唯一的子元素 */

:first-of-type      /* 同类型中的第一个 */
:last-of-type       /* 同类型中的最后一个 */
:nth-of-type(n)     /* 同类型中的第 n 个 */
:only-of-type       /* 同类型中唯一的 */

/* 其他伪类 */
:not(selector)      /* 不匹配选择器的元素 */
:empty              /* 没有子元素的元素 */
:root               /* 文档的根元素 */

伪元素选择器

::before            /* 在元素内容前插入 */
::after             /* 在元素内容后插入 */
::first-line        /* 元素的第一行 */
::first-letter      /* 元素的第一个字母 */
::selection         /* 用户选中的内容 */

基础定位

CCS定位

element = page.locator("button.submit") # 选择所有 class 包含 submit 的 button 元素
element = page.locator("#login-btn") # 选择 id 为 login-btn 的元素
element = page.locator("div > span.highlight") # 选择所有直接父元素是 div 且 class 包含 highlight 的 span 元素

文本定位

element = page.locator("text=登录")
element = page.locator("text='Welcome'")
element = page.locator("text=/hello/i")  # 正则表达式

XPath 定位

element = page.locator("xpath=//button[@id='submit']")
element = page.locator("//div[@class='container']//a")

属性定位

element = page.locator("[data-testid='submit-button']")
element = page.locator("[type='email']")
element = page.locator("[placeholder*='search']")  # 包含

组合定位

element = page.locator("form.login >> input[name='username']") # 首先找到 class 为"login"的 form 元素,然后在该表单内查找 name 为"username"的 input 元素
element = page.locator(".modal >> .close-btn") # 首先找到 class 为"modal"的元素,然后在其内部查找 class 为"close-btn"的元素

高级定位策略

按位置定位

first_element = page.locator("button").first
last_element = page.locator("button").last
nth_element = page.locator("li").nth(2)  # 第三个元素

过滤定位

visible_elements = page.locator("button").filter(has_text="Submit")
hidden_elements = page.locator("div").filter(has=page.locator(".hidden"))

链式定位

element = (page.locator("form")
          .filter(has=page.locator("input[required]"))
          .locator("button"))

包含其他元素的定位

element = page.locator("div", has=page.locator("p.description"))
element = page.locator("li", has_text="Item").filter(
    has=page.locator(".badge")
)

相对定位

base_element = page.locator(".container")
child_element = base_element.locator(".item")
sibling_element = base_element.locator("xpath=following-sibling::div")

元素状态检查

可见性检查

is_visible = element.is_visible()
is_hidden = element.is_hidden()
is_enabled = element.is_enabled()
is_disabled = element.is_disabled()
is_editable = element.is_editable()

选中状态检查

is_checked = element.is_checked()

等待元素状态

element.wait_for(state="visible")        # 等待可见
element.wait_for(state="hidden")         # 等待隐藏
element.wait_for(state="attached")       # 等待附加到DOM
element.wait_for(state="detached")       # 等待从DOM分离

获取元素的属性和内容

获取文本内容

text = element.text_content()
inner_text = element.inner_text()
all_texts = element.all_text_contents()  # 多个匹配元素

获取属性

class_name = element.get_attribute("class")
data_value = element.get_attribute("data-value")
all_attributes = element.evaluate("el => el.attributes")

获取输入值

input_value = element.input_value()

获取内部HTML

html_content = element.inner_html()

获取元素的样式和尺寸

获取计算样式

color = element.evaluate("el => getComputedStyle(el).color")
font_size = element.evaluate("el => getComputedStyle(el).fontSize")

获取元素尺寸和位置

bounding_box = element.bounding_box()  # {x, y, width, height}
is_visible = bounding_box is not None

检查元素是否包含其他元素

contains_child = element.locator(".child").count() > 0

元素等待

等待元素出现

element = page.locator(".dynamic-content")
element.wait_for()  # 等待元素附加到DOM

等待元素可见

element.wait_for(state="visible")

等待元素满足条件

element.wait_for(
    state="visible",
    timeout=10000  # 10秒超时
)

自定义等待条件

page.wait_for_function("""
    () => document.querySelector('.result').textContent.includes('成功')
""")

元素交互

点击

基础点击

page.click("button#submit")
page.locator(".btn").click()

点击选项

page.click("button", 
    button="right",      # 右键点击
    delay=1000,          # 按下和释放之间的延迟
    force=True,          # 强制点击,即使元素不可点击
    no_wait_after=True,  # 点击后不等待导航
    modifiers=["Shift"]  # 修饰键
)

双击

page.dblclick(".item")

输入

基础输入

page.fill("#username", "myusername")
page.locator("input[type='password']").fill("mypassword")

类型输入(模拟键盘)

page.type("#search", "keyword", delay=100)  # 带延迟的输入

清空

page.fill("#input", "")  # 方法1
page.locator("#input").clear()  # 方法2

文件上传

page.set_input_files("#file-input", "path/to/file.txt")
page.set_input_files("#multi-file", ["file1.txt", "file2.jpg"])

通过如下方式清除文件

page.set_input_files("#file-input", [])

选择和拖拽

下拉选择框

page.select_option("#country", "china")  # 按值
page.select_option("#colors", label="Red")  # 按标签
page.select_option("#multi-select", value=["opt1", "opt2"])  # 多选

复选框和单选框

page.check("#agree-terms")
page.uncheck("#newsletter")
page.set_checked("#option1", True)

拖拽

page.drag_and_drop("#source", "#target")

通过如下方式进行手动拖拽

source = page.locator("#draggable")
target = page.locator("#droppable")
source.drag_to(target)

键盘模拟

在元素上操作键盘

element = page.locator("#input")
element.press("Enter")
element.press("Control+A")  # 全选

页面级键盘操作

page.keyboard.type("Hello World!")
page.keyboard.press("ArrowDown")
page.keyboard.down("Shift")  # 按住Shift
page.keyboard.up("Shift")    # 释放Shift

组合键

page.keyboard.press("Control+A")
page.keyboard.press("Control+C")

鼠标模拟

悬停

page.hover(".menu-item")
page.locator(".dropdown").hover()

精确鼠标控制

page.mouse.move(100, 200)      # 移动到坐标
page.mouse.click(150, 250)     # 点击坐标
page.mouse.dblclick(300, 400)  # 双击坐标
page.mouse.down()              # 按下鼠标
page.mouse.up()                # 释放鼠标

滚轮

page.mouse.wheel(0, 100)  # 向下滚动100px

元素的截图和焦点

元素截图

element.screenshot(path="element.png")

带选项的截图

element.screenshot(
    path="highlighted.png",
    type="jpeg",
    quality=80,
    omit_background=True
)

焦点

element.focus()
element.blur()

检查焦点

has_focus = element.evaluate("el => el === document.activeElement") # 通过javascript来获取焦点

执行 JavaScript

在页面上下文中执行

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=False)
    page = browser.new_page()
    page.goto("https://example.com")
    
    # 执行简单的 JavaScript 并返回值
    title = page.evaluate("() => document.title")
    print(f"页面标题: {title}")
    
    # 执行带参数的 JavaScript
    result = page.evaluate("(a, b) => a + b", 5, 3)
    print(f"5 + 3 = {result}")
    
    # 执行复杂的 JavaScript 代码块
    page_info = page.evaluate("""
        () => {
            return {
                title: document.title,
                url: window.location.href,
                userAgent: navigator.userAgent,
                viewport: {
                    width: window.innerWidth,
                    height: window.innerHeight
                }
            };
        }
    """)
    print(f"页面信息: {page_info}")
    
    browser.close()

使page.evaluate_handle()返回 JSHandle 对象

# 返回 JSHandle,可以传递到其他 evaluate 调用中
element_handle = page.evaluate_handle("() => document.body")
text_content = page.evaluate("(body) => body.textContent", element_handle)

# JSHandle 可以调用方法
bounding_box = element_handle.bounding_box()

在元素上下文中执行

# 获取元素并在其上下文中执行 JavaScript
element = page.locator(".content")

# 获取元素的文本内容
text = element.evaluate("(el) => el.textContent")

# 获取元素属性
class_name = element.evaluate("(el) => el.className")

# 修改元素样式
element.evaluate("""
    (el) => {
        el.style.backgroundColor = 'yellow';
        el.style.padding = '10px';
    }
""")

# 执行复杂的元素操作
element.evaluate("""
    (el) => {
        // 获取计算样式
        const style = window.getComputedStyle(el);
        return {
            tagName: el.tagName,
            className: el.className,
            fontSize: style.fontSize,
            color: style.color,
            isVisible: el.offsetParent !== null
        };
    }
""")

使element.evaluate_all()在多个元素上执行

# 在多个匹配的元素上执行 JavaScript
all_links = page.locator("a")

# 获取所有链接的 href 和文本
link_data = all_links.evaluate_all("""
    (elements) => {
        return elements.map(el => ({
            text: el.textContent.trim(),
            href: el.href,
            target: el.target
        }));
    }
""")

print(f"找到 {len(link_data)} 个链接")
for link in link_data:
    print(f"链接: {link['text']} -> {link['href']}")

操作 Cookie

cookie_template = {
    'name': 'cookie_name',           # 必需: Cookie 名称
    'value': 'cookie_value',         # 必需: Cookie 值
    
    # 以下三选一(指定 Cookie 的作用范围)
    'url': 'https://example.com',    # 方式1: 完整 URL
    'domain': '.example.com',        # 方式2: 域名(包含子域名用 . 开头)
    'path': '/',                     # 方式3: 路径(需要与 domain 一起使用)
    
    # 可选属性
    'expires': 1698765432,           # 过期时间(Unix 时间戳)
    'httpOnly': True,                # 是否仅 HTTP 可访问
    'secure': True,                  # 是否仅 HTTPS 传输
    'sameSite': 'Strict',            # SameSite 策略: 'Strict', 'Lax', 'None'
}

# 注意:必须提供 url 或 domain+path 来指定 Cookie 的作用范围
cookies = browser.cookies()
site_cookies = browser.cookies("https://example.com")

使用如下方式添加单个 Cookie

# 添加基本的 Cookie
browser.add_cookies([{
    'name': 'user_preference',
    'value': 'dark_mode',
    'url': 'https://example.com'
}])

# 添加完整的 Cookie(包含所有属性)
browser.add_cookies([{
    'name': 'session_id',
    'value': 'abc123xyz',
    'domain': '.example.com',
    'path': '/',
    'expires': int((datetime.now() + timedelta(days=7)).timestamp()),  # 7天后过期
    'httpOnly': True,
    'secure': True,
    'sameSite': 'Lax'
}])

当想批量添加 Cookie 的时候,如下:

# 批量添加 Cookie
cookies_to_add = [
    {
        'name': 'user_id',
        'value': '12345',
        'url': 'https://example.com'
    },
    {
        'name': 'theme',
        'value': 'dark',
        'url': 'https://example.com'
    },
    {
        'name': 'language',
        'value': 'zh-CN', 
        'url': 'https://example.com'
    }
]

browser.add_cookies(cookies_to_add)
browser.clear_cookies()

文章作者: Vsoapmac
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 soap的会员制餐厅
python 自动化测试 第三方库
喜欢就支持一下吧