自动化神器playwright

发布时间 2023-06-21 09:55:30作者: 桂木

一、前言
  一提到 WebUI 自动化测试工具首要推荐的必属是 Selenium,其优势在于跨平台、跨语言、完全开源、对商业用户也没有任何限制、支持分布式、拥有成熟的社区与学习文档等目前已经迭代更新到 4 版本。那么缺点也有比如环境配置、加载效率低、运行速度慢等。当然还有其他很多优秀的工具比如 Cypress、Robot Framework 等,现在介绍另一款最近几年微软推出的强大而易用的 UI 自动化测试工具-Playwright。

二、简介
微软开源自动化测试工具 Playwright,支持主流浏览器,包括:Chrome、Firefox、Safari 等,同时支持以无头模式、有头模式运行,并提供了同步、异步的 API,可以结合 Pytest 测试框架使用,并且支持浏览器端的自动化脚本录制等功能。

特点:

1、跨浏览器。Playwright 支持所有现代渲染引擎,包括Chromium、WebKit 和 Firefox。

2、跨平台。在 Windows、Linux 和 macOS 上进行本地或 CI、无头或有头测试。

3、跨语言。在 TypeScript、JavaScript、Python、.NET、Java 中使用Playwright API。

4、测试移动网络。适用于 Android 和 Mobile Safari 的 Google Chrome 原生移动仿真。相同的渲染引擎适用于桌面和云端。

官网地址:

https://playwright.dev

GitHub地址:

https://github.com/microsoft/playwright

三.安装

pip install playwright  # 安装playwright的python版本
playwright install  # 安装playwright自带的浏览器和ffmepg,此步骤耗时较长

 

四、交互模式
Playwright 支持交互模式,即运行单行代码或者代码块,能立即给出运行结果。

由于 Playwright 支持同步和异步的 API,那么应先了解一下什么是同步和异步?

同步,可以理解为在执行完一个函数或方法之后,一直等待系统返回值或消息,这时程序是出于阻塞的,只有接收到返回的值或消息后才往下执行其他的命令。

异步,执行完函数或方法后,不必阻塞性地等待返回值或消息,只需要向系统委托一个异步过程,那么当系统接收到返回值或消息时,系统会自动触发委托的异步过程,从而完成一个完整的流程。

下面以操作打开浏览器,访问百度首页,关闭浏览器为例。

1、同步命令

打开命令行,输入 python

进入到 Python 交互模式中,输入如下命令:

from playwright.sync_api import sync_playwright
playwright = sync_playwright().start()
browser = playwright.chromium.launch(headless=False)
page = browser.new_page()
page.goto("https://www.baidu.com/")
browser.close()
playwright.stop()

输入访问百度首页命令,浏览器页面会同时跳转到百度首页,同时命令行输出响应与请求的信息。

2、异步命令

打开命令行,输入 python -m asyncio

进入到 Python 交互模式中,输入如下命令:

from playwright.async_api import async_playwright
playwright = await async_playwright().start()
browser = await playwright.chromium.launch(headless=False)
page = await browser.new_page()
await page.goto("https://www.baidu.com/")
await browser.close()
await playwright.stop()

输入访问百度首页命令,浏览器页面也会同时跳转到百度首页,命令行也会输出响应与请求的信息。

 

五、录制模式
Playwright 带有命令行工具(录制功能),可用于记录用户交互并生成代码(Java、Python等)。其实就是类似于 Selenium IDE。

1、常规录制

打开命令行,输入

playwright codegen

自动打开浏览器和录制界面

通过操作(点击、输入等)浏览器页面,脚本也会自动增加操作的步骤。

此外,录制工具还可以获取元素的定位。点击停止录制,之后再点击 Explore 后,在页面点击想要定位的元素,即可获取到该元素定位的值。

作为演示,输入百度网址打开百度页面,搜索框中输入“playwright”,点击“百度一下”按钮,录制代码如下

from playwright.sync_api import sync_playwright

def run(playwright):
    browser = playwright.chromium.launch(headless=False)
    context = browser.new_context()

    # Open new page
    page = context.new_page()

    # Go to https://www.baidu.com/
    page.goto("https://www.baidu.com/")

    # Click input[name="wd"]
    page.click("input[name=\"wd\"]")

    # Fill input[name="wd"]
    page.fill("input[name=\"wd\"]", "playwright")

    # Click text=百度一下
    page.click("text=百度一下")
    # assert page.url == "https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&rsv_idx=1&tn=baidu&wd=playwright&fenlei=256&oq=playwright&rsv_pq=bf1abd6c000029f7&rsv_t=1937PYyfHvfyS6fay57V1zS1iCIiYC4%2B8I6srjLqYYkXrf8H9kce%2BLQKVzA&rqlang=cn&rsv_dl=tb&rsv_enter=0&rsv_btype=t&prefixsug=playwright&rsp=5"

    # Click text=百度一下
    # with page.expect_navigation(url="https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&rsv_idx=1&tn=baidu&wd=playwright&fenlei=256&oq=playwright&rsv_pq=a03207ba00008498&rsv_t=ecf2ko5wPyHrjSHwUBLAZZwxkyObcfsg5ge7apN1BeAdigW%2BzzxD%2F3CJI7k&rqlang=cn&rsv_dl=tb&rsv_enter=0&rsv_btype=t&prefixsug=playwright&rsp=5"):
    with page.expect_navigation():
        page.click("text=百度一下")

    # Click text=百度一下
    page.click("text=百度一下")
    # assert page.url == "https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&rsv_idx=1&tn=baidu&wd=playwright&fenlei=256&oq=playwright&rsv_pq=bf1abd6c000029f7&rsv_t=1937PYyfHvfyS6fay57V1zS1iCIiYC4%2B8I6srjLqYYkXrf8H9kce%2BLQKVzA&rqlang=cn&rsv_dl=tb&rsv_enter=0&rsv_btype=t&prefixsug=playwright&rsp=5"

    # ---------------------
    context.close()
    browser.close()

with sync_playwright() as playwright:
    run(playwright)

最后将录制的脚本复制出来,可做适当的调整。

调整后的脚本代码:

from playwright.sync_api import sync_playwright  # 导入playwright同步api

def run(playwright):  # 定义run方法
    browser = playwright.chromium.launch(headless=False)  # 创建chromium的browser对象,当前使用的是playwright安装的自带的chromium
    context = browser.new_context()  # 创建context对象,context之间是相互隔离的,可以理解为轻量级的浏览器实例

    page = context.new_page()  # 创建page对象,真正打开浏览器界面

    page.goto("https://www.baidu.com/")  # 跳转到百度url

    page.fill("input[name=\"wd\"]", "playwright")  # 通过css定位在搜索框中输入"playwright"

    with page.expect_navigation():  # 预期结果,点击"百度一下"按钮后会发生页面导航
        page.click("text=百度一下")  # 通过playwright自定义的文字定位器定位"百度一下"按钮并点击

    # ---------------------
    context.close()  # 关闭context
    browser.close()  # 关闭browser

with sync_playwright() as playwright:  # playwright使用入口,通过上下文方式
    run(playwright)  # 调用run方法,将playwright实例传入


2、模拟移动设备录制

open 可以模拟移动设备和平板设备

例如模拟 iPhone 14 访问博客园。

playwright open --device="iPhone 14" home.cnblogs.com/u/keima/

 

通过以上代码可以了解到:

  1. playwright支持同步和异步两种使用方法
  2. 不需要为每个浏览器下载webdriver
  3. 相比selenium多了一层context抽象
  4. 支持无头浏览器,且较为推荐(headless默认值为True)
  5. 可以使用传统定位方式(CSS,XPATH等),也有自定义的新的定位方式(如文字定位)
  6. 没有使用selenium的先定位元素,再进行操作的方式,而是在操作方法中传入了元素定位,定位和操作同时进行(其实也playwright也提供了单独的定位方法,作为可选)
  7. 很多方法使用了with的上下文语法

六、编写模式
使用 IDE(如 PyCharm、Visual Studio Code 等) 进行编写代码并运行程序。

1、启动浏览器(无头模式)

Playwright 可以启动三种浏览器中的 chromium、firefox、webkit 任何一种。

示例操作如下,打开浏览器、跳转百度、屏幕截图、输出页面标题、关闭浏览器。

from playwright.sync_api import sync_playwright

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

 

2、启动浏览器(有头模式)

默认情况下,Playwright 以无头模式运行浏览器。要查看浏览器 UI(有头模式),请在启动浏览器时传递 headless=False 标志,还可以使用 slow_mo 来减慢执行速度。

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
browser = p.chromium.launch(headless=False, slow_mo=50)
page = browser.new_page()
page.goto("https://www.baidu.com/")
page.screenshot(path="example.png")
print(page.title())
browser.close()

3、异步

Playwright 支持 API 的两种变体:同步和异步。

支持异步,如果你的项目使用 asyncio,则应该使用 async 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("https://www.baidu.com/")
print(await page.title())
await browser.close()

asyncio.run(main())

4、context

Playwright 支持上下文管理:一个浏览器实例下可以有多个context,将浏览器分割成不同的上下文,以实现会话的分离。

如需要不同用户登录同一个网页,不需要创建多个浏览器实例,只需要创建多个context即可。

context1 = browser.new_context()
context2 = browser.new_context()

5、page

一个context下可以有多个page,一个page就代表一个浏览器的标签页或弹出窗口,用于进行页面操作。

page = context.new_page()

# 显式导航,类似于在浏览器中输入URL
page.goto('https://www.baidu.com/')
# 在输入框中输入字符
page.fill('#search', '百度')
# 点击提交按钮
page.click('#submit')
# 打印当前url
print(page.url)

6、browser

Playwright内置了很多支持的浏览器驱动,不用再像selenium那样去寻找对应匹配版本的浏览器驱动版本下载。

只需要创建相应的browser实例。

browser = playwright.chromium.launch()    #Chrome
browser = playwright.firefox.launch()    #火狐

7、frame
一个页面至少包含一个主frame,新的frame通过iframe标签定义,frame之间可以进行嵌套,只有先定位到frame才能对frame里面的元素进行定位和操作。playwright默认使用page进行的元素操作会重定向到主frame上。

# 通过名称获得frame
frame = page.frame('frame-login')

# 通过frame的url获得frame
frame = page.frame(url=r'.*domain.*')

# 通过选择器获得frame
frame_element_handle = page.query_selector('.frame-class')
frame = frame_element_handle.content_frame()

# 操作frame中的元素
frame.fill('#username-input', 'JoJo')

8、选择器
所有元素操作都需要使用选择器定位到要操作的元素,playwright同时支持css、xpath和自定义的选择器,使用时无需指定类型,playwright会自动进行判断。

# Using data-test-id= selector engine
page.click('data-test-id=foo')

# CSS and XPath selector engines are automatically detected
page.click('div')
page.click('//html/body/div')

# Find node by text substring
page.click('text=Hello w')

# Explicit CSS and XPath notation
page.click('css=div')
page.click('xpath=//html/body/div')

# Click an element with text 'Sign Up' inside of a #free-month-promo.
page.click('#free-month-promo >> text=Sign Up')

# Capture textContent of a section that contains an element with text 'Selectors'.
section_text = page.eval_on_selector('*css=section >> text=Selectors', 'e => e.textContent')

 

9、自动等待
像page.click(selector)、page.fill(selector, value)之类的元素操作会自动等待元素可见且可操作。

# Playwright 会等待 #search 元素出现在 DOM 中
page.fill('#search', 'query')

# Playwright 会等待元素停止动画并接受点击
page.click('#search')

# 等待 #search 出现在 DOM 中
page.wait_for_selector('#search', state='attached')
# 等待 #promo 可见, 例如具有 `visibility:visible`
page.wait_for_selector('#promo')

# 等待 #details 变得不可见, 例如通过 `display:none`.
page.wait_for_selector('#details', state='hidden')
# 等待 #promo 从 DOM 中移除
page.wait_for_selector('#promo', state='detached')

七、实例

1、网易云签到

import time
 
from playwright.sync_api import Playwright, sync_playwright, expect
 
 
def run(playwright: Playwright) -> None:
    browser = playwright.chromium.launch(headless=False)
    context = browser.new_context(viewport={'width': 1920, 'height': 1080})
 
    # Open new page
    page = context.new_page()
 
    # Go to https://music.163.com/
    page.goto("https://music.163.com/")
 
    # Click text=用户登录
    page.frame_locator("iframe[name=\"contentFrame\"]").locator("text=用户登录").click()
 
    # Click text=选择其他登录模式
    page.locator("text=选择其他登录模式").click()
 
    # Check input[type="checkbox"]
    page.locator("input[type=\"checkbox\"]").check()
 
    # Click text=QQ登录
    with page.expect_popup() as popup_info:
        page.locator("text=QQ登录").click()
    page1 = popup_info.value
 
    # Click #img_out_****
    page1.frame_locator("iframe[name=\"ptlogin_iframe\"]").locator("#img_out_****").click()
    # token用*代替
    page1.wait_for_url(f"https://music.163.com/back/sns?key=*")
 
    # Close page
    page1.close()
 
    # Click text=我的音乐
    page.locator("text=我的音乐").click()
    page.wait_for_url("https://music.163.com/#/my/m/music/playlist?id=152392364")
 
    time.sleep(3)
    # Click a:has-text("播放")
    page.frame_locator("iframe[name=\"contentFrame\"]").locator("a:has-text(\"播放\")").click()
    time.sleep(10)
 
    # Click text=网易云音乐
    page.locator("text=网易云音乐").click()
    page.wait_for_url("https://music.163.com/#")
 
    # Click a:has-text("签到")
    page.frame_locator("iframe[name=\"contentFrame\"]").locator('//*[@id="discover-module"]/div[2]/div[1]/div/div/div/div/a/i').click()
    time.sleep(10)
    # Close page
    page.close()
 
    # ---------------------
    context.close()
    browser.close()
    print('*******************************  签到完成  *******************************')
 
 
with sync_playwright() as playwright:
    run(playwright)

  

2、B站签到

from playwright.sync_api import Playwright, sync_playwright, expect
 
 
def run(playwright: Playwright) -> None:
    browser = playwright.chromium.launch(headless=False)
    context = browser.new_context(viewport={'width': 1920, 'height': 1080})
 
    # Open new page
    page = context.new_page()
 
    # Go to https://www.bilibili.com/
    page.goto("https://www.bilibili.com/")
 
    # Click span:has-text("登录")
    page.locator("span:has-text(\"登录\")").click()
 
    # Click text=QQ登录
    with page.expect_popup() as popup_info:
        page.locator("text=QQ登录").click()
    page1 = popup_info.value
 
    # Click text=****
    page1.frame_locator("iframe[name=\"ptlogin_iframe\"]").locator("text=1170527913 懿曲折扇情").click()
    page1.wait_for_url("https://www.bilibili.com/")
 
    # Click text=创作中心
    with page1.expect_popup() as popup_info:
        page1.locator("text=创作中心").click()
    page2 = popup_info.value
 
    # Click #canvas-wrap img >> nth=2
    page2.locator("#canvas-wrap img").nth(2).click()
 
    # Click #canvas-wrap img >> nth=2
    page2.locator("#canvas-wrap img").nth(2).click()
 
    # Click #canvas-wrap img >> nth=2
    page2.locator("#canvas-wrap img").nth(2).click()
 
    # Click #canvas-wrap img >> nth=1
    page2.locator("#canvas-wrap img").nth(1).click()
 
    page2.hover('//*[@id="app"]/div[1]/div/div[2]/span[1]/a/img')
    # Click text=直播中心
    with page2.expect_popup() as popup_info:
        page2.locator("text=直播中心").click()
    page3 = popup_info.value
 
    # Click text=签到 >> nth=0
    page3.locator("text=签到").first.click()
 
    # Click span:has-text("22") >> nth=0
    page3.locator('//div[@class="checkin-btn t-center pointer"]').first.click()
 
    # Close page
    page3.close()
 
    # Close page
    page2.close()
 
    # Close page
    page1.close()
 
    # Close page
    page.close()
 
    # ---------------------
    context.close()
    browser.close()
 
 
with sync_playwright() as playwright:
    run(playwright)

八、自动等待Options

 等待API

element_handle.is_checked()
element_handle.is_disabled()
element_handle.is_editable()
element_handle.is_enabled()
element_handle.is_hidden()
element_handle.is_visible()
page.is_checked(selector, **kwargs)
page.is_disabled(selector, **kwargs)
page.is_editable(selector, **kwargs)
page.is_enabled(selector, **kwargs)
page.is_hidden(selector, **kwargs)
page.is_visible(selector, **kwargs)
locator.is_checked(**kwargs)
locator.is_disabled(**kwargs)
locator.is_editable(**kwargs)
locator.is_enabled(**kwargs)
locator.is_hidden(**kwargs)
locator.is_visible(**kwargs)

九、断言

expect(locator).not_to_be_checked(**kwargs)
expect(locator).not_to_be_disabled(**kwargs)
expect(locator).not_to_be_editable(**kwargs)
expect(locator).not_to_be_empty(**kwargs)
expect(locator).not_to_be_enabled(**kwargs)
expect(locator).not_to_be_focused(**kwargs)
expect(locator).not_to_be_hidden(**kwargs)
expect(locator).not_to_be_visible(**kwargs)
expect(locator).not_to_contain_text(expected, **kwargs)
expect(locator).not_to_have_attribute(name, value, **kwargs)
expect(locator).not_to_have_class(expected, **kwargs)
expect(locator).not_to_have_count(count, **kwargs)
expect(locator).not_to_have_css(name, value, **kwargs)
expect(locator).not_to_have_id(id, **kwargs)
expect(locator).not_to_have_js_property(name, value, **kwargs)
expect(locator).not_to_have_text(expected, **kwargs)
expect(locator).not_to_have_value(value, **kwargs)
expect(locator).not_to_have_values(values, **kwargs)
expect(locator).to_be_checked(**kwargs)
expect(locator).to_be_disabled(**kwargs)
expect(locator).to_be_editable(**kwargs)
expect(locator).to_be_empty(**kwargs)
expect(locator).to_be_enabled(**kwargs)
expect(locator).to_be_focused(**kwargs)
expect(locator).to_be_hidden(**kwargs)
expect(locator).to_be_visible(**kwargs)
expect(locator).to_contain_text(expected, **kwargs)
expect(locator).to_have_attribute(name, value, **kwargs)
expect(locator).to_have_class(expected, **kwargs)
expect(locator).to_have_count(count, **kwargs)
expect(locator).to_have_css(name, value, **kwargs)
expect(locator).to_have_id(id, **kwargs)
expect(locator).to_have_js_property(name, value, **kwargs)
expect(locator).to_have_text(expected, **kwargs)
expect(locator).to_have_value(value, **kwargs)
expect(locator).to_have_values(values, **kwargs)
expect(page).not_to_have_title(title_or_reg_exp, **kwargs)
expect(page).not_to_have_url(url_or_reg_exp, **kwargs)
expect(page).to_have_title(title_or_reg_exp, **kwargs)
expect(page).to_have_url(url_or_reg_exp, **kwargs)
expect(api_response).not_to_be_ok()
expect(api_response).to_be_ok()

使用

page.locator("#submit-button").click()
    expect(page.locator(".status")).to_have_text("Submitted")