laitimes

Technical Practice Dry Goods | Playwright, a web automation framework for Guanyuan Data, explains in detail

author:Flash Gene

1

background

At present, Selenium has the widest coverage of UI automation in the market, and Selenium is quick to get started, rich in materials, and has an active community. Prior to the discovery of Playwright, Guanyuan Data had been using Selenium as a web automation framework. However, we found some problems with Selenium in the process of writing automation use cases:

  • Rely on a variety of different drivers, and as the browser is constantly updated, these drivers need to be maintained continuously;
  • Selenium IDE 录屏代码不稳定;
  • The stability is not good enough;
  • ...

Then we found Playwright. Playwright is Microsoft's open-source web automation framework, which functions similarly to Selenium, Pyppeteer, etc., which can drive browsers to perform various automation operations, and provides support for mainstream browsers on the market. The API is simple and powerful, and it can be used to automate the operations of major browsers such as Chromium, Firefox, and WebKit with a single API, and it can be run in both headless and headless mode.

Below, we'll compare the pros and cons of Playwright and Selenium, as well as share some of our experiences based on Playwright.

2

The Stone Stone 和Selenium 对比

  • Supported Languages (all major languages)
    • Playwright:JavaScript & TypeScript\python\C#\Go\Java
    • Selenium:java\python\ruby\C#\C++\JavaScript
  • How to operate the browser and how it performs
    • Playwright: Interact with the browser through developer tools, simple installation, no need to install various drivers, and fast browser startup.
    • Selenium: You need to operate different browsers through various WebDrivers, and the startup speed of the browser is slow.
  • Browsers are supported
    • Playwright:支持所有主流浏览器,Chromium/WebKit/Firefox,不支持 IE11。
    • Selenium: Runs on all major browsers (excluding domestic skinning browsers).
  • Fast and reliable execution
    • Playwright: Auto-wait (wait for an element to appear/wait for an event to occur), Websocket-based (two-way communication) to automatically get the actual browser situation.
    • Selenium: It is necessary to add waits to the code, and even element state rotation judgment, increase the running time, and Selenium is based on the HTTP protocol (one-way communication).
  • Code Recording
    • Playwright: You can use common element positioning methods such as css, xpath, and text to record and generate code, which can greatly reduce the time it takes to write code, and at the same time, code stability can also be guaranteed.
    • Selenium:Selenium IDE 录制的代码是基于 coordinate 或者 DOM 层级结构,所以极其不稳定,也就导致 IDE 基本无人问津。
  • Asynchronous mode
    • Playwright supports asynchronous mode.
    • Selenium 不支持异步方式。
  • headless mode
    • Both Playwright and Selenium support headless mode, so UI automation can be run on Linux or in scenarios where there is a lack of display devices.
  • Mobile browsers
    • Both Playwright and Selenium support simulated testing of mobile browsers, but not physical testing.

3

Playwright environment

3.1 Prerequisite environment setup

JDK 8、Git、Maven、TestNg、jUnit等。

3.2 pom.xml添加代码

<dependency>
    <groupId>com.microsoft.playwright</groupId>
    <artifactId>playwright</artifactId>
    <version>1.17.0</version>
</dependency>
<dependency>
    <groupId>com.microsoft.playwright</groupId>
    <artifactId>assertions</artifactId>
    <version>1.17.2</version>
</dependency>           

3.3 Playwright Dependent Installation

#首先安装可以支持的浏览器
mvn exec:java -e -Dexec.mainClass=com.microsoft.playwright.CLI -Dexec.args="install"
#如果有特殊浏览器需要安装可以先查看
mvn exec:java -e -Dexec.mainClass=com.microsoft.playwright.CLI -Dexec.args="install --help"
#安装特殊浏览器
# Install WebKit
mvn exec:java -e -Dexec.mainClass=com.microsoft.playwright.CLI -Dexec.args="install webkit"
           

3.4 Code Recording

#打开浏览器进行录屏(保留已经验证的状态)
mvn exec:java -e -Dexec.mainClass=com.microsoft.playwright.CLI -Dexec.args="codegen --load-storage=auth.json https://www.guandata.com"
#如果要模仿手机操作,可以通过 playwright.devices 列表操作
# Emulate iPhone 11.
mvn exec:java -e -Dexec.mainClass=com.microsoft.playwright.CLI -Dexec.args='open --device="iPhone 11" https://www.guandata.com'
           

4

Practical operation

4.1 Screen Recording Example

Use playwright to record and playback the screen for a fixed scene and observe the screen recording effect.

You can see the following screen recording process and screen recording code:

  • Playwright 支持无头浏览器模式,且较为推荐(headless 默认值为 True);
  • You can use traditional positioning methods (CSS, XPATH, etc.), and there are also custom new positioning methods (such as text positioning), and the element positioning and operation methods are simpler;
  • Element positioning is passed in the operation method, and the positioning and manipulation are carried out at the same time (playwright also provides a separate positioning method as an option) --selenium can only position the element first, and then operate;
  • There is no need to download webdriver for each browser, and you can switch between different browsers at will;
  • The way to record the screen is simple.

, duration 00:46

// 输入账号密码登录观远BI系统
// 进入数据中心,新建一个test2名称的目录
public class Example {
  public static void main(String[] args) {
    // 实例化page对象
    try (Playwright playwright = Playwright.create()) {
      Browser browser = playwright.chromium().launch(new BrowserType.LaunchOptions()
        .setHeadless(false));
      BrowserContext context = browser.newContext(new Browser.NewContextOptions()
        .setStorageStatePath(Paths.get("auth.json")));
      Page page = context.newPage();
      // 访问BI系统地址
      page.navigate("http://192.168.199.6:8087/auth/index");
      // 输入域名、账号、密码进行登录
      page.click("[placeholder=\"公司域\"]");
      page.fill("[placeholder=\"公司域\"]", "testing");
      page.click("[placeholder=\"邮箱地址\"]");
      page.fill("[placeholder=\"邮箱地址\"]", "[email protected]");
      page.click("[placeholder=\"密码\"]");
      page.fill("[placeholder=\"密码\"]", "123456");
      page.waitForNavigation(() -> {
        page.press("[placeholder=\"密码\"]", "Enter");
      });
      // 点击数据中心
      page.waitForNavigation(() -> {
        page.click("#rc-tabs-0-tab-datacenter div:has-text(\"数据中心\")");
      });
      // 点击新建文件夹进行文件夹创建
      page.click("button:has-text(\"新建文件夹\")");
      page.fill("text=文件夹名称文件夹位置:根目录 >> input[type=\"text\"]", "test-playwright");
      page.click("button:has-text(\"确定\")");
    }
  }
}
           

4.2 Toggle browser and headless settings

Playwright supports switching between different browsers, and compatibility testing of multiple browsers can be done in the actual automation process.

The day-to-day automation of use cases can be executed in headless mode (headless mode, the browser does not open the interface) to improve the efficiency of use case execution. When we develop use cases, we use the headless mode to facilitate code debugging (in headless mode, the browser will open the interface) to improve the efficiency of use case writing.

// 使用此Browser 对象,可以使用launch()方法启动浏览器实例
Playwright playwright = Playwright.create();
// 使用firefox浏览器,也可以改为webkit浏览器
Browser browser = playwright.firefox().launch();
// false为有头模式(有界面的浏览器),true为无头模式(没有界面的浏览器)
Browser browser = playwright.chromium().launch(new BrowserType.LaunchOptions().setHeadless(false));
           

4.3 Encapsulating Parent Methods

Encapsulate common methods in the parent class: Chromium by default, headless mode, setting browser width and height, default address of the environment, etc. When the parent class is referenced in the test class, the operation in the parent class is triggered when the test case is executed to avoid code rewriting.

public class UiAbstract {
    public static Page page;
    public static BrowserContext context;
    @BeforeSuite
    public void setUp() {
        Playwright playwright = Playwright.create();
        //设置浏览器为chromium,headless模式
        Browser browser = playwright.chromium().launch(new BrowserType.LaunchOptions().setHeadless(Env.HEADLESS));
        Browser.NewContextOptions contextOptions = new Browser.NewContextOptions();
        // 设置浏览器宽和高
        contextOptions.setViewportSize(1280, 720);
        //设置环境的默认地址
        contextOptions.setBaseURL(Env.TESTSERVER);
        context = browser.newContext(contextOptions);
    }
}
           

4.4 Common assertion methods

This section describes some commonly used methods of UI automation assertions, and the most commonly used in Guanyuan data is screenshot vs. assertion, see 4.5 for details.

Get the text content and assert:

String content = page.text_content(":nth-match(i, 2)");
Assert.assertEquals(content, expected, "[" + description + "]测试案例失败!");
           

Wait for the vanishing element to appear, then get the text content and assert:

// 等待元素出现
page.waitForSelector(":nth-match(i, 2)");
// 获取文本内容,进行断言
String content = page.text_content(":nth-match(i, 2)");
Assert.assertEquals(content, expected, "[" + description + "]测试案例失败!");
           

Get the internal text, make an assertion:

String text = page.inner_text(":nth-match(i, 2)");
Assert.assertEquals(text, expected, "[" + description + "]测试案例失败!");
           

Get the property value and assert it:

String attribute = page.getAttribute("#su", "value");
Assert.assertEquals(attribute, expected, "[" + description + "]测试案例失败!");
           

element visibility, to assert:

boolean visible = page.isVisible('#su');
Assert.assertTrue(visible, expected, description);
           

Enabled state (the element is clickable), to assert:

boolean enabled = page.isEnabled('#su');
Assert.assertTrue(visible, expected, description);
           

title, to assert:

String title = page.title();
Assert.assertEquals(title, expected, "[" + description + "]测试案例失败!");
           

url, to assert:

String url = page.url();
Assert.assertEquals(url, expected, "[" + description + "]测试案例失败!");
           

4.5 Screenshot vs. Assertion

Since the workload of assertion through text is also very large, Gu adds screenshots and base pixel comparison for assertion through secondary development, so as to improve the efficiency of UI automatic writing. Main process:

  1. When writing a use case, take a screenshot of the page that needs to be asserted, and upload the image to the corresponding path of the Samba server.
  2. In the test class, the screenshot assertion method is referenced (the screenshot can be positioned by the element or the screenshot can be taken on the full screen), and the pixel similarity (between 0-1) is set, and the generated screenshot result will be placed in the corresponding path of the Samba server.
  3. The screenshot assertion method will compare the pixels of two images, and draw the comparison results in a blank image, and the successful comparison will be gray, and the failed comparison will be red.
  4. Generate a test report containing actual screenshots, expected screenshots, and comparison screenshots, as shown below.
  5. If the assertion fails due to normal modification of the front-end, replace the actual screenshot with the expected screenshot through the developed gadget.
Technical Practice Dry Goods | Playwright, a web automation framework for Guanyuan Data, explains in detail

The screenshot asserts that the core code is as follows:

public class ImageDiff {
    public static double getDifferencePercent(String actual, String baseline, String diffOut, int threshold, double tolerance) throws IOException {
        // 读取实际图片和base图片的图像
        InputStream actualInputStream = new FileInputStream(actual);
        InputStream baseInputStream = new FileInputStream(baseline);
        try {
            BufferedImage img1 = ImageIO.read(actualInputStream);
            BufferedImage img2 = ImageIO.read(baseInputStream);
            // 获取图片的高和宽
            int width = img1.getWidth();
            int height = img1.getHeight();
            int width2 = img2.getWidth();
            int height2 = img2.getHeight();
            long diffCount = 0;
            // 遍历屏幕截图像素点数据
            for (int y = 0; y < height; y++) {
                for (int x = 0; x < width; x++) {
                    //使用getRGB(w, h)获取该点的颜色值是ARGB
                    //而在实际应用中使用的是RGB,所以需要将ARGB转化成RGB,即bufImg.getRGB(w, h) & 0xFFFFFF。
                    int i = pixelYIQDiff(img1.getRGB(x, y), img2.getRGB(x, y));
                    if (i > threshold) {
                        diffCount++;
                        img1.setRGB(x, y, 0xffff0000);
                    } else {
                        img1.setRGB(x, y, greyPixel(img1.getRGB(x, y)));
                    }
                }
            }
            long maxDiffCount = width * height;
            double diffPct = 100.0 * diffCount / maxDiffCount;
            // 不一致的图片大于精度,则使用图片标出
            if (diffPct > tolerance) {
                ImageIO.write(img1, "png", new File(diffOut));
            }
            return diffPct;
        } catch (FileNotFoundException e) {
            Log.warn(baseline + "文件不存在");
            DistributeController.uploadFile(baseline, actualInputStream);
            //            FileUtils.copyFile(actualFile, baselineFile);
            e.printStackTrace();
        }
        return 0;
    }
    // 分别获取出rgb中的r、g、b的值,并得到不一致数
    private static int pixelYIQDiff(int rgb1, int rgb2) {
         // 对整数直接进行计算得到rgb值
        int r1 = (rgb1 >> 16) & 0xff;
        int g1 = (rgb1 >> 8) & 0xff;
        int b1 = rgb1 & 0xff;
        int r2 = (rgb2 >> 16) & 0xff;
        int g2 = (rgb2 >> 8) & 0xff;
        int b2 = rgb2 & 0xff;

        double yDiff = rgb2y(r1, g1, b1) - rgb2y(r2, g2, b2);
        double iDiff = rgb2i(r1, g1, b1) - rgb2i(r2, g2, b2);
        double qDiff = rgb2q(r1, g1, b1) - rgb2q(r2, g2, b2);

        return (int) (0.5053 * yDiff * yDiff + 0.299 * iDiff * iDiff + 0.1957 * qDiff * qDiff);
    }

    private static double rgb2y(int r, int g, int b) {
        return r * 0.29889531 + g * 0.58662247 + b * 0.11448223;
    }

    private static double rgb2i(int r, int g, int b) {
        return r * 0.59597799 - g * 0.27417610 - b * 0.32180189;
    }

    private static double rgb2q(int r, int g, int b) {
        return r * 0.21147017 - g * 0.52261711 + b * 0.31114694;
    }
    // 像素匹配上绘制灰色
    private static int greyPixel(int rgb1) {
        // 对整数直接进行计算得到rgb值
        int r1 = (rgb1 >> 16) & 0xff;
        int g1 = (rgb1 >> 8) & 0xff;
        int b1 = rgb1 & 0xff;
        int y = (int) rgb2y(r1, g1, b1);
        int gray = (int) (255 + (y - 255) * 0.2);

        return gray << 16 & 0x00ff0000 | gray << 8 & 0x0000ff00 | gray & 0x000000ff | 0xff000000;
    }
}
           

4.6 Other common operations

  • 下拉选择框:selectOpion
  • File uploads: setInputFiles, single file, multiple files, drag-and-drop uploads
  • 鼠标点击:mouse().click、mouse().dblclick
  • 鼠标拖动:mouse().down、mouse().up
  • 鼠标移动:mouse().move
  • Keyboard key: press
  • 截屏、录屏:screenshot、video
  • 浏览器滚动条滚动到元素位置:querySelector(element).scrollIntoViewIfNeeded()

5

Code in action

/**
 * @description: 测试权限控制功能对订阅计划中的内容是否生效
 * @date: 2021-12-29 18:33
 **/
public class SubscriptionReadTest extends UiAbstract {
    String description = "订阅-查看";
    // 测试类前实例化page对象
    @BeforeTest
    public void beforeTest(){
        // Open new page
        page = context.newPage();
    }
    @Test(dataProvider = "datapro")
    public void subscriptionReadTest(String role, String loginId) {
        page.navigate("/auth/index");
        // 输入域名、账号、密码点击登录
        page.click("[placeholder=\"公司域\"]");
        page.fill("[placeholder=\"公司域\"]", "rbac");
        page.click("[placeholder=\"邮箱地址\"]");
        page.fill("[placeholder=\"邮箱地址\"]", loginId);
        page.click("[placeholder=\"密码\"]");
        page.fill("[placeholder=\"密码\"]", "******");
        page.waitForNavigation(() -> {
            page.click("#loginBtn");
        });
        
        // 点击九宫格按钮
        page.click(":nth-match(i, 2)");
        
        // 点击订阅计划,根据元素截图
        page.click("text=订阅计划");
        page.waitForSelector("text=1");
        ScreenshotUtil.screenshotBySelector(page, "div[class='_1HjXw1zf']", description+'-'+role+"卡片订阅");

        // 点击合并订阅,根据元素截图
        page.click("text=合并订阅");
        page.waitForSelector("text=暂无相关数据");
        ScreenshotUtil.screenshotBySelector(page, "div[class='_1HjXw1zf']", description+'-'+role+"合并订阅");

        // 点击页面订阅,根据元素截图
        page.click("text=页面订阅");
        page.waitForSelector("text=1");
        ScreenshotUtil.screenshotBySelector(page, "div[class='_1HjXw1zf']", description+'-'+role+"页面订阅");

        // 点击数据集订阅,根据元素截图
        page.click("text=数据集订阅");
        page.waitForSelector("text=暂无相关数据");
        ScreenshotUtil.screenshotBySelector(page, "div[class='_1HjXw1zf']", description+'-'+role+"数据集订阅");
        
        // 对上述的测试截图进行断言
        ScreenshotUtil.ScreenshotAssert(description+'-'+role+"卡片订阅");
        ScreenshotUtil.ScreenshotAssert(description+'-'+role+"合并订阅");
        ScreenshotUtil.ScreenshotAssert(description+'-'+role+"页面订阅");
        ScreenshotUtil.ScreenshotAssert(description+'-'+role+"数据集订阅");
    }
    // 测试数据
    @DataProvider
    public Object[][] datapro() {
        return new Object[][]{
                {"普通用户","[email protected]"},
                {"只读用户","[email protected]"}
        };
    }
    // 测试完成后关闭page
    @AfterTest
    public void after(){
        // Close page
        page.close();
    }
}

           

Official website documents: https://playwright.dev/java/docs/intro

GitHub:https://github.com/microsoft/playwright-java

Author: Li Biao, senior test and development engineer of Guanyuan. He has rich working experience in business testing, big data testing, automated testing, performance testing and other fields.

Source-WeChat public account: Guanyuan Data Technical Team

Source: https://mp.weixin.qq.com/s/zmx5ZkEruGJ4EDdfAIzSkA

Dry

Read on