缘由

老板让搞下线上业务的 ui 监控,之前是写 selenium 然后引入一个代理工具来搞,在测 ui 自动化的同时,来监测接口的信息,
也可以达到现在使用的工具:playwright 差不多的效果,但使用下来,明显 playwright 更好用些

pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>org.example</groupId>
  <artifactId>examples</artifactId>
  <version>0.1-SNAPSHOT</version>
  <name>Playwright Client Examples</name>
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>
  <dependencies>
    <dependency>
      <groupId>com.microsoft.playwright</groupId>
      <artifactId>playwright</artifactId>
      <version>1.28.0</version>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.10.1</version>
      </plugin>
    </plugins>
  </build>
</project>
package org.example;

import com.microsoft.playwright.*;

public class App {
    public static void main(String[] args) {
        try (Playwright playwright = Playwright.create()) {
            Browser browser = playwright.chromium().launch();
            Page page = browser.newPage();
            page.navigate("http://playwright.dev");
            System.out.println(page.title());
        }
    }
}

安装

运行这个 maven 工程的时候,会自动安装 playwright 的,如果没有,也可以使用命令来运行 maven 工程

mvn exec:java -e -Dexec.mainClass=com.microsoft.playwright.CLI -Dexec.args="install"

# 一般不需要执行这个指令,至少我这边的环境没有用到下面个命令,可能在linux的机器上会需要吧
mvn exec:java -e -Dexec.mainClass=com.microsoft.playwright.CLI -Dexec.args="install-deps"


命令行工具

mvn exec:java -e -Dexec.mainClass=com.microsoft.playwright.CLI

录制工具

# 个人感觉这个录制工具还挺好用的,就是debug java文件好像没那么好使,官方有例子是js的
mvn exec:java -e -Dexec.mainClass=com.microsoft.playwright.CLI -Dexec.args="codegen"

全局配置

Playwright playwright = null;
        Browser browser = null;
        BrowserContext context = null;
        Page page = null;
        Browser.NewContextOptions newContextOptions = new Browser.NewContextOptions().setViewportSize(1440, 875);

        try {
            playwright = Playwright.create();
            browser = playwright.chromium().launch(new BrowserType.LaunchOptions()
//                .setHeadless(false)
//                .setDevtools(true)
                    .setSlowMo(1800));
            // 不同的context的配置,理论上可能是一样的,例如浏览器的尺寸
            context = browser.newContext(newContextOptions);
            page = context.newPage();
            page.setDefaultTimeout(60000);

打开页面

public enum WaitUntilState {
    LOAD,
    DOMCONTENTLOADED,
    NETWORKIDLE,
    COMMIT;

    private WaitUntilState() {
    }
}

页面加载分 4 个阶段,依次是 COMMIT, DOMCONTENTLOADED, LOAD, NETWORKIDLE
设置方法:

Page.NavigateOptions options = new Page.NavigateOptions();
            options.setWaitUntil(WaitUntilState.LOAD);
                page.navigate("https://flowmore.pingpongx.com/entrance/signin", options);

截图

page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get("/tmp/ui-monitor/" + getFormattedTime() + ".png")));

保存视频

BrowserContext context = browser.newContext(new Browser.NewContextOptions()
  .setRecordVideoDir(Paths.get("videos/"))
  .setRecordVideoSize(640, 480));

定位(支持通过 js 方法自定义定位符)

page.locator("#payment-link").getByText("外贸收款").click();
page.getByRole(AriaRole.TEXTBOX, new Page.GetByRoleOptions().setName("付款人名称/收款账户")).click();
page.getByText("提现", new Page.GetByTextOptions().setExact(true)).first().hover();

上传文件

// Select one file
page.getByLabel("Upload file").setInputFiles(Paths.get("myfile.pdf"));

// Select multiple files
page.getByLabel("Upload files").setInputFiles(new Path[] {Paths.get("file1.txt"), Paths.get("file2.txt")});

// Remove all the selected files
page.getByLabel("Upload file").setInputFiles(new Path[0]);

// Upload buffer from memory
page.getByLabel("Upload file").setInputFiles(new FilePayload(
  "file.txt", "text/plain", "this is test".getBytes(StandardCharsets.UTF_8)));

下载文件

Download download = page.waitForDownload(() -> {
    // Perform the action that initiates download
    page.locator("button#delayed-download").click();
});
// Wait for the download process to complete
Path path = download.path();
System.out.println(download.path());
// Save downloaded file somewhere
download.saveAs(Paths.get("/path/to/save/download/at.txt"));

page.onDownload(download -> System.out.println(download.path()));


执行 js

// A primitive value.
page.evaluate("num => num", 42);

// An array.
page.evaluate("array => array.length", Arrays.asList(1, 2, 3));

// An object.
Map<String, Object> obj = new HashMap<>();
obj.put("foo", "bar");
page.evaluate("object => object.foo", obj);

// A single handle.
ElementHandle button = page.evaluate("window.button");
page.evaluate("button => button.textContent", button);

// Alternative notation using elementHandle.evaluate.
button.evaluate("(button, from) => button.textContent.substring(from)", 5);

// Object with multiple handles.
ElementHandle button1 = page.evaluate("window.button1");
ElementHandle button2 = page.evaluate("window.button2");
Map<String, ElementHandle> arg = new HashMap<>();
arg.put("button1", button1);
arg.put("button2", button2);
page.evaluate("o => o.button1.textContent + o.button2.textContent", arg);

// Object destructuring works. Note that property names must match
// between the destructured object and the argument.
// Also note the required parenthesis.
Map<String, ElementHandle> arg = new HashMap<>();
arg.put("button1", button1);
arg.put("button2", button2);
page.evaluate("({ button1, button2 }) => button1.textContent + button2.textContent", arg);

// Array works as well. Arbitrary names can be used for destructuring.
// Note the required parenthesis.
page.evaluate(
  "([b1, b2]) => b1.textContent + b2.textContent",
  Arrays.asList(button1, button2));

// Any non-cyclic mix of serializables and handles works.
Map<String, Object> arg = new HashMap<>();
arg.put("button1", button1);
arg.put("list", Arrays.asList(button2));
arg.put("foo", 0);
page.evaluate(
  "x => x.button1.textContent + x.list[0].textContent + String(x.foo)",
  arg);

frame

Locator username = page.frameLocator(".frame-class").getByLabel("User Name");

多页面切换

BrowserContext userContext = browser.newContext();
BrowserContext adminContext = browser.newContext();

发送请求

apiRequestContext = playwright.request().newContext(new APIRequest.NewContextOptions()
//                // All requests we send go to this API endpoint.
                .setBaseURL("https://xxx.com"));
Map<String, String> data = new HashMap<>();
        data.put("appName", "ui-monitor");
        apiRequestContext.post("/v2/alert/***", RequestOptions.create().setData(data));

过滤请求

page.route("**/*", route -> {
    if ("image".equals(route.request().resourceType()))
        route.abort();
    else
        route.resume();
});

mock 或修改服务端的返回数据

page.route("**/api/fetch_data", route -> route.fulfill(new Route.FulfillOptions()
  .setStatus(200)
  .setBody(testData)));
page.navigate("https://example.com");

page.route("**/title.html", route -> {
  // Fetch original response.
  APIResponse response = page.request().fetch(route.request());
  // Add a prefix to the title.
  String body = response.text();
  body = body.replace("<title>", "<title>My prefix:");
  Map<String, String> headers = response.headers();
  headers.put("content-type": "text/html");
  route.fulfill(new Route.FulfillOptions()
    // Pass all fields from the response.
    .setResponse(response)
    // Override response body.
    .setBody(body)
    // Force content type to be html.
    .setHeaders(headers));
});

打印接口

Consumer<Request> listener = request -> {
                if (request.url().contains("https://flowmore.pingpongx.com") && request.url().contains("api") && request.response() != null) {
                    System.out.println("Request "+ request.url() + " finished, res: " + new String(request.response().body()));

                }
            };
            page.onRequestFinished(listener);

websocket

page.onWebSocket(ws -> {
  log("WebSocket opened: " + ws.url());
  ws.onFrameSent(frameData -> log(frameData.text()));
  ws.onFrameReceived(frameData -> log(frameData.text()));
  ws.onClose(ws1 -> log("WebSocket closed"));
});

trace(可以先录制,然后如果脚本执行成功了,再将它删掉)

Browser browser = chromium.launch();
BrowserContext context = browser.newContext();
context.tracing().start(new Tracing.StartOptions()
  .setScreenshots(true)
  .setSnapshots(true));
Page page = context.newPage();
page.navigate("https://playwright.dev");
context.tracing().stop(new Tracing.StopOptions()
  .setPath(Paths.get("trace.zip")));

问题

  1. 发现一个前端的问题:页面上的一个按钮有时展示,有时不展示 ==> 开始还以为是自己脚本的问题,后来看到了界面,是因为接口响应时间不稳定导致的,让前端同学优化了
  2. 可能是打开方式不对的问题,开始是想要复用 playwright,只在一开始创建 brower, context 对象,然后一段时间后(在这期间一直在解析接口的响应数据)发生了内存泄露 ==>一开始是使用的 512M,以为是内存小了,因为本地执行没发生(本地用的是 2G),可能不是不发生,只是需要的时间可能比较久,让运维加到了 1024M,还是不行,主要是 String 对象一直拷贝导致的,更改成每次都重新创建 brower, context, page 了,下面是修改后,-Xmx128m 的内存使用情况,应该是 Ok 了

参考

官方文档之 java 版本: https://playwright.dev/java/docs/cli


↙↙↙阅读原文可查看相关链接,并与作者交流