无头浏览器PlayWright使用(可适配国产操作系统)

查询系统架构

uname -m

系统架构aarch64,arm都可以使用

1.安装Node.js

  • 可按照搜索安装进行安装(注意的是Node的版本不能太低,至少要18版本)
  • Node.js压缩包node-v20.18.0-linux-arm64.tar.xz
  • 解压并配置环境变量
tar -xf node-v20.18.0-linux-arm64.tar.xz -C /opt/
sudo mv /opt/node-v20.18.0-linux-arm64 /opt/node
echo 'export PATH=/opt/node/bin:$PATH' >> ~/.bashrc
source ~/.bashrc
node -v # 验证 v20.x
npm -v

2.下载 Playwright NPM 包及依赖

  • 忽略版本冲突,全局安装
npm install -g playwright --legacy-peer-deps
  • 全局安装
npm install -g playwright

在这里插入图片描述

  • 验证是否安装
playwright --version

3.下载二进制浏览器

npx playwright install --with-deps chromium firefox webkit
playwright install --with-deps chromium firefox webkit
playwright install chromium firefox webkit

在这里插入图片描述

  • 指定浏览器路径,指定 Playwright 浏览器二进制文件的下载位置
echo 'export PLAYWRIGHT_BROWSERS_PATH=$HOME/.cache/ms-playwright' >> ~/.bashrc
source ~/.bashrc
echo $PLAYWRIGHT_BROWSERS_PATH

4.下载系统依赖

  • Playwright 运行所需系统依赖
yum install -y libX11 libXcomposite libXdamage libXext libXi libXtst libXrandr \ alsa-lib atk at-spi2-atk at-spi2-core cairo cups-libs dbus-libs expat \ fontconfig freetype gdk-pixbuf2 glib2 gtk3 libdrm libffi libICE libSM \ libXcursor libXfixes libXinerama libXpm libXss libXv libXxf86vm libxcb \ libxkbcommon libxkbcommon-x11 mesa-libgbm nss pango wayland-libs-client \ wayland-libs-cursor wayland-libs-egl
yum install -y libX11 libXcomposite libXdamage libXext libXi libXtst libXrandr \ alsa-lib atk at-spi2-atk at-spi2-core cairo cups-libs dbus-libs expat \ fontconfig freetype gdk-pixbuf2 glib2 gtk3 libdrm libffi libICE libSM \ libXcursor libXfixes libXinerama libXpm libXss libXv libXxf86vm libxcb \ libxkbcommon libxkbcommon-x11 mesa-libgbm nss pango wayland-libs-client \ wayland-libs-cursor wayland-libs-egl

如遇到依赖包找不到 名称不匹配时 使用命令查询匹配的命令

yum search XXXX

如果是国产操作系统(麒麟系统),这几个包是找不到的

alsa-lib fontconfig libXcursor libXss libxkbcommon wayland-libs-client wayland-libs-cursor wayland-libs-egl

上述包在麒麟系统对应的包下载命令

yum install -y alsa-lib.aarch64 alsa-lib-devel.aarch64
yum install -y fontconfig.aarch64 fontconfig-devel.aarch64 fontconfig-help.noarch
yum install -y libXcursor.aarch64 libXcursor-devel.aarch64 libXcursor-help.noarch
yum install -y libXScrnSaver.aarch64
yum install -y libxkbcommon.aarch64 libxkbcommon-devel.aarch64
yum install -y libwayland-client libwayland-cursor libwayland-egl

5.配置index.js执行转换脚本

mkdir -p /opt/ultimate-solution
cd /opt/ultimate-solution
  • 搜索整个系统,忽略权限错误,只找可执行的 chrome;逐个验证找到的路径,直到找到能用的
find / -type f \( -name "chrome" -o -name "headless_shell" \) -executable 2>/dev/null | grep -E "playwright|ms-playwright"
  • 替换为你上面 find 到的第一个路径,验证版本
/root/.cache/ms-playwright/chromium-1097/chrome-linux/chrome --version
  • js脚本内容
const { chromium } = require('playwright');
const fs = require('fs');

async function convertHtmlToPdf() {
  const htmlInputPath = process.argv[2];
  const pdfOutputPath = process.argv[3];

  if (!htmlInputPath || !pdfOutputPath) {
    console.error('Usage: node index.js <HTML_FILE_PATH> <PDF_OUTPUT_PATH>');
    process.exit(1);
  }

  // 启动 Playwright 浏览器
  const browser = await chromium.launch({
    // --------------------------
    // 这里必须替换为你步骤2验证成功的完整路径!!!
    // 测试环境:/root/.cache/ms-playwright/chromium-1208/chrome-linux/chrome
    executablePath: '/root/.cache/ms-playwright/chromium-1217/chrome-linux/chrome',
    headless: true,
    args: ['--no-sandbox', '--disable-gpu']
  });

  try {
    const page = await browser.newPage();
    
    // 读取 HTML 文件
    const htmlContent = fs.readFileSync(htmlInputPath, 'utf-8');
    
    // 加载 HTML 并等待渲染
    await page.setContent(htmlContent, { 
      waitUntil: 'networkidle', 
      timeout: 60000 
    });
    
    // 生成 PDF(A4 格式,支持背景)
    await page.pdf({ 
      path: pdfOutputPath, 
      format: 'A4', 
      printBackground: true,
      margin: { top: '1cm', bottom: '1cm', left: '1cm', right: '1cm' }
    });
    
    console.log('PDF generated successfully:', pdfOutputPath);
  } finally {
    // 确保浏览器关闭
    await browser.close();
  }
}

// 执行转换
convertHtmlToPdf().catch(err => {
  console.error('Error generating PDF:', err);
  process.exit(1);
});
  • 进行验证
node /opt/ultimate-solution/index.js

可能遇到的问题:
Node.js 的设计机制是:全局安装的包(-g)默认不会被本地代码引用。即使你全局装了 Playwright,你的项目文件夹里仍然需要一个本地的 node_modules

#1.进入项目目录 
cd /opt/ekp-ultimate-solution/ 
# 2. 执行本地安装(不要加 -g) 
npm install playwright

6.后端代码调用返回文件

public class HeadlessBrowserUtil implements AutoCloseable {

    private static final Logger logger = LoggerFactory.getLogger(HeadlessBrowserUtil.class);

    private static Config config;

    static {
        try {
            config = new Config();
        } catch (Exception e) {
            logger.error("配置初始化失败", e);
            config = null;
        }
    }

    /**
     *  常量定义(提升维护性)
     * */
    private static final long DEFAULT_TIMEOUT_SECONDS = 30;
    private static final String DEFAULT_PDF_PAGE_SIZE = "A4";
    private static final boolean DEFAULT_PRINT_BACKGROUND = true;
    private static final String DEFAULT_CHROME_DRIVER_PATH;
    private static final String DEFAULT_HEAD_LESS_SHELL_PATH;
    private static final String NEW_VERSION_HEAD_LESS_PATH;

    static {
        if (config != null) {
            DEFAULT_CHROME_DRIVER_PATH = config.getChromeDriverPath();
            DEFAULT_HEAD_LESS_SHELL_PATH = config.getChromeHeadlessShellPath();
            NEW_VERSION_HEAD_LESS_PATH = config.getNewVersionHeadlessPath();
        } else {
            DEFAULT_CHROME_DRIVER_PATH = "/usr/bin/chromedriver";
            DEFAULT_HEAD_LESS_SHELL_PATH = "/home/chroma/chrome-headless-shell-linux64/chrome-headless-shell";
            NEW_VERSION_HEAD_LESS_PATH = "/opt/ultimate-solution/index.js";
        }
    }


    private WebDriver driver;
    private WebDriverWait defaultWait;
    private final long defaultTimeoutSeconds = DEFAULT_TIMEOUT_SECONDS;

    // ========== 构造方法 ==========

    public HeadlessBrowserUtil() {}

//    public HeadlessBrowserUtil(String init) {
//        this(DEFAULT_TIMEOUT_SECONDS);
//
//    }




    public HeadlessBrowserUtil(long defaultTimeoutSeconds) {
//        this.defaultTimeoutSeconds = defaultTimeoutSeconds;
        initDriver();
    }

    /**
     * 自定义 ChromeOptions 的构造方法
     */
    public HeadlessBrowserUtil(ChromeOptions customOptions) {
        this(customOptions, DEFAULT_TIMEOUT_SECONDS);
    }

    public HeadlessBrowserUtil(ChromeOptions customOptions, long defaultTimeoutSeconds) {
//        this.defaultTimeoutSeconds = defaultTimeoutSeconds;
        try {
            this.driver = new ChromeDriver(customOptions);
            this.defaultWait = new WebDriverWait(driver, Duration.ofSeconds(defaultTimeoutSeconds));
            logger.info("ChromeDriver初始化成功(自定义Options)");
        } catch (Exception e) {
            logger.error("ChromeDriver初始化失败(自定义Options)", e);
            throw new RuntimeException("ChromeDriver初始化失败", e);
        }
    }

    // ========== 初始化 ==========
    // 先在类顶部新增常量(替换为你的ChromeDriver实际路径)
//    private static final String DEFAULT_HEAD_LESS_SHELL_PATH = "/home/fengxh/DevEnv/chroma/chrome-headless-shell-linux64/chrome-headless-shell";

    private void initDriver() {
        ChromeOptions options = new ChromeOptions();
        // 1. 显式指定ChromeDriver路径(关键!避免系统加载旧版本)
        System.setProperty("webdriver.chrome.driver", DEFAULT_CHROME_DRIVER_PATH);

        // 2. 适配144版本的无头参数(精简+兼容)
        options.setBinary(DEFAULT_HEAD_LESS_SHELL_PATH);
        options.addArguments("--headless=new");          // 144版本推荐的无头模式
        options.addArguments("--disable-gpu");           // 兼容Linux无GPU环境
        options.addArguments("--window-size=1920,1080");
        options.addArguments("--remote-allow-origins=*");// 解决跨域限制
        options.addArguments("--disable-dev-shm-usage"); // 关键!Linux下避免/dev/shm内存不足
        options.addArguments("--no-sandbox");            // 非root用户运行必需(Linux常见问题)
        options.addArguments("--disable-extensions");    // 禁用扩展,减少干扰
        // 以下参数144版本可保留,不影响兼容
        options.addArguments("--disable-blink-features=AutomationControlled");
        options.setExperimentalOption("excludeSwitches", new String[]{"enable-automation"});

        try {
            this.driver = new ChromeDriver(options);
            this.defaultWait = new WebDriverWait(driver, Duration.ofSeconds(defaultTimeoutSeconds));
            logger.info("ChromeDriver初始化成功,默认超时时间={}秒,ChromeDriver路径={}",
                    defaultTimeoutSeconds, DEFAULT_CHROME_DRIVER_PATH);
        } catch (Exception e) {
            // 补充详细错误日志(路径、版本、异常栈)
            logger.error("===== ChromeDriver初始化失败详情 =====", e);
            logger.error("1. ChromeDriver路径:{}", DEFAULT_CHROME_DRIVER_PATH);
            logger.error("2. Headless Shell路径:{}", DEFAULT_HEAD_LESS_SHELL_PATH);
            logger.error("3. 异常类型:{}", e.getClass().getName());
            logger.error("4. 异常信息:{}", e.getMessage());
            throw new RuntimeException("无头浏览器初始化失败,请检查日志中路径/版本/依赖", e);
        }
    }

    // ========== 核心新增:生成 PDF byte[] 方法 ==========
    /**
     * 生成当前页面的 PDF 字节数组(基于 CDP,无临时文件)
     * @return PDF 字节数组
     */
    public byte[] generatePdfAsBytes() {
        return generatePdfAsBytes(DEFAULT_PDF_PAGE_SIZE, DEFAULT_PRINT_BACKGROUND, false);
    }

    /**
     * 自定义 PDF 参数生成字节数组
     * @param pageSize 页面大小(A4/Letter/Legal/A3 等)
     * @param printBackground 是否打印背景色/图片
     * @param removeMargins 是否移除打印边距
     * @return PDF 字节数组
     */
    public byte[] generatePdfAsBytes(String pageSize, boolean printBackground, boolean removeMargins) {
        try {
            // 1. 等待页面完全渲染(确保异步资源加载完成)
            waitForPageLoad();
            waitForAllImagesLoaded();
            logger.info("页面渲染完成,开始通过CDP生成PDF");

            // 2. 构造CDP打印参数
            Map<String, Object> printParams = new HashMap<>();
            printParams.put("paperWidth", getPageWidth(pageSize)); // 宽度(英寸)
            printParams.put("paperHeight", getPageHeight(pageSize)); // 高度(英寸)
            printParams.put("printBackground", printBackground); // 打印背景
            printParams.put("marginTop", removeMargins ? 0 : 0.4); // 上边距(英寸)
            printParams.put("marginBottom", removeMargins ? 0 : 0.4); // 下边距
            printParams.put("marginLeft", removeMargins ? 0 : 0.4); // 左边距
            printParams.put("marginRight", removeMargins ? 0 : 0.4); // 右边距
            printParams.put("preferCSSPageSize", true); // 优先使用CSS定义的页面大小
            printParams.put("transferMode", "ReturnAsBase64"); // 返回Base64格式

            // 3. 调用CDP的Page.printToPDF方法
            ChromeDriver chromeDriver = (ChromeDriver) this.driver;
            Map<String, Object> result = chromeDriver.executeCdpCommand("Page.printToPDF", printParams);
            String pdfBase64 = (String) result.get("data");

            // 4. Base64转byte[]
            byte[] pdfBytes = java.util.Base64.getDecoder().decode(pdfBase64);
            logger.info("PDF生成成功,字节数={}", pdfBytes.length);
            return pdfBytes;
        } catch (Exception e) {
            logger.error("生成PDF字节数组失败", e);
            throw new RuntimeException("PDF生成失败", e);
        }
    }

    /**
     * 注入HTML字符串并生成PDF字节数组(一站式调用)
     * @param htmlContent HTML字符串
     * @return PDF字节数组
     */
    public byte[] generatePdfFromHtml(String htmlContent) {
        // 注入HTML并等待渲染
        injectHtml(htmlContent);
        // 生成PDF
        return generatePdfAsBytes();
    }

    // ========== 核心PDF生成方法(新方案) ==========
    /**
    	* Playwright 转换
     * 通过HTML内容生成PDF(推荐使用)(适配国产麒麟操作系统新方案)
     */
    public byte[] generatePdfFromHtml(String htmlContent,String version) {
        Path tempHtml = null;
        Path tempPdf = null;
        try {
            // 1. 从配置读取工具路径
            String toolPath = NEW_VERSION_HEAD_LESS_PATH;
            if (StringUtil.isNull(toolPath)) {
                toolPath = "/opt/ultimate-solution/index.js";
                logger.warn("未配置pdfToolPath,使用默认路径: {}", toolPath);
            }

            // 2. 创建临时HTML文件
            tempHtml = Files.createTempFile("ekp-archive-", ".html");
            Files.write(tempHtml, htmlContent.getBytes(StandardCharsets.UTF_8));
            logger.info("创建临时HTML文件: {}", tempHtml.toAbsolutePath());

            // 3. 创建临时PDF文件路径
            tempPdf = Files.createTempFile("ekp-archive-", ".pdf");
            // 确保文件不存在
            Files.deleteIfExists(tempPdf);
            logger.info("指定PDF输出路径: {}", tempPdf.toAbsolutePath());

            // 4. 构造并执行命令
            List<String> command = Arrays.asList(
                    "node",
                    toolPath,
                    tempHtml.toAbsolutePath().toString(),
                    tempPdf.toAbsolutePath().toString()
            );
            logger.info("执行命令: {}", String.join(" ", command));

            ProcessBuilder pb = new ProcessBuilder(command);
            pb.redirectErrorStream(true);
            Process process = pb.start();

            // 5. 读取执行结果
            // 5. 读取执行结果
            String output = IOUtils.toString(process.getInputStream(), StandardCharsets.UTF_8);
            int exitCode = process.waitFor();
            logger.info("命令执行完成,退出码: {}, 输出: {}", exitCode, output);

            if (exitCode != 0) {
                throw new RuntimeException("PDF工具执行失败,退出码: " + exitCode + ",输出: " + output);
            }

            // 6. 检查并读取PDF
            if (!Files.exists(tempPdf) || Files.size(tempPdf) == 0) {
                throw new RuntimeException("PDF文件生成失败: " + tempPdf.toAbsolutePath());
            }
            byte[] pdfBytes = Files.readAllBytes(tempPdf);
            logger.info("PDF生成成功,大小: {} 字节", pdfBytes.length);
            return pdfBytes;

        } catch (Exception e) {
            logger.error("PDF生成失败", e);
            throw new RuntimeException("PDF生成失败: " + e.getMessage(), e);
        } finally {
            // 7. 清理临时文件
            if (tempHtml != null) {
                try { Files.deleteIfExists(tempHtml); } catch (Exception ex) { logger.error("清理HTML失败", ex); }
            }
            if (tempPdf != null) {
                try { Files.deleteIfExists(tempPdf); } catch (Exception ex) { logger.error("清理PDF失败", ex); }
            }
        }
    }

    // ========== 辅助方法 ==========
    /**
     * 注入HTML字符串到空白页(解决本地HTML渲染问题)
     */
    public HeadlessBrowserUtil injectHtml(String htmlContent) {
        try {
            // 打开空白页
            get("about:blank").waitForPageLoad();
            // 注入HTML并关闭文档流(避免渲染异常)
            executeScript("document.write(arguments[0]); document.close();", htmlContent);
            logger.info("HTML字符串注入完成,开始等待渲染");
            // 等待页面完全渲染(含图片/异步JS)
            waitForPageLoad();
            waitForAllImagesLoaded();
            logger.info("HTML渲染完成");
            return this;
        } catch (Exception e) {
            logger.error("HTML注入/渲染失败", e);
            throw new RuntimeException("HTML注入失败", e);
        }
    }

    /**
     * 等待所有图片加载完成(避免PDF缺图)
     * JDK8 适配点:
     * 1. 移除字符串文本块(JDK15+支持),替换为字符串拼接
     * 2. 确保 Lambda 泛型推导兼容 JDK8
     */
    /**
     * 等待所有图片加载完成(避免PDF缺图)
     * 修复点:
     * 1. JS脚本统一返回数值(0/1),避免布尔值导致类型转换异常
     * 2. Java端接收Number类型,兼容Long/Integer等数值类型
     * 3. 简化逻辑判断,直接基于数值判断是否加载完成
     */
    private void waitForAllImagesLoaded() {
        waitForCondition((Function<WebDriver, Boolean>) webDriver -> {
            // 修正JS脚本:统一返回数值(1=加载完成,0=未完成)
            Number result = (Number) ((JavascriptExecutor) webDriver).executeScript(
                    "var images = document.images;" +
                            "if (images.length === 0) return 1;" + // 无图片时返回1(完成)
                            "var loaded = 0;" +
                            "for (var i = 0; i < images.length; i++) {" +
                            "    if (images[i].complete && !images[i].error) loaded++;" +
                            "}" +
                            "return loaded === images.length ? 1 : 0;" // 有图片时,完成=1,未完成=0
            );
            // 数值转int,判断是否等于1(加载完成)
            return result.intValue() == 1;
        }, defaultTimeoutSeconds);
    }

    /**
     * 页面大小转英寸(CDP的printToPDF参数要求英寸单位)
     * JDK8 适配点:移除 switch 表达式(JDK14+支持),替换为 switch 语句
     */
    private double getPageWidth(String pageSize) {
        String upperPageSize = pageSize.toUpperCase();
        switch (upperPageSize) {
            case "A4":
                return 8.27;
            case "A3":
                return 11.69;
            case "LETTER":
                return 8.5;
            case "LEGAL":
                return 8.5;
            default:
                return 8.27; // 默认A4
        }
    }

    /**
     * 页面高度转英寸
     * JDK8 适配点:移除 switch 表达式(JDK14+支持),替换为 switch 语句
     */
    private double getPageHeight(String pageSize) {
        String upperPageSize = pageSize.toUpperCase();
        switch (upperPageSize) {
            case "A4":
                return 11.69;
            case "A3":
                return 16.54;
            case "LETTER":
                return 11.0;
            case "LEGAL":
                return 14.0;
            default:
                return 11.69; // 默认A4
        }
    }

    // ========== 备选方案:临时文件方式(兼容低版本ChromeDriver) ==========
    /**
     * 临时文件方式生成PDF byte[](若CDP调用失败时使用)
     * @param tempPdfPath 临时文件路径
     * @return PDF字节数组
     */
    public byte[] generatePdfByTempFile(String tempPdfPath) {
        // 1. 构造带打印参数的ChromeOptions
        ChromeOptions options = new ChromeOptions();
        options.addArguments("--headless=new");
        options.addArguments("--disable-gpu");
        options.addArguments("--print-to-pdf=" + tempPdfPath);
        options.addArguments("--print-to-pdf-page-size=" + DEFAULT_PDF_PAGE_SIZE);
        options.addArguments("--print-backgrounds=" + DEFAULT_PRINT_BACKGROUND);
        options.addArguments("--no-margins");

        // 2. 重新初始化Driver(必须重新创建才能生效打印参数)
        WebDriver tempDriver = new ChromeDriver(options);
        try {
            // 3. 复制当前页面的URL/HTML(保持上下文)
            tempDriver.get(this.driver.getCurrentUrl());
            WebDriverWait tempWait = new WebDriverWait(tempDriver, Duration.ofSeconds(defaultTimeoutSeconds));
            tempWait.until(webDriver -> ((JavascriptExecutor) webDriver)
                    .executeScript("return document.readyState").equals("complete"));

            // 4. 读取临时文件并转byte[]
            File pdfFile = new File(tempPdfPath);
            if (!pdfFile.exists() || pdfFile.length() == 0) {
                throw new RuntimeException("临时PDF文件生成失败:" + tempPdfPath);
            }
            byte[] pdfBytes = Files.readAllBytes(pdfFile.toPath());
            logger.info("临时文件方式生成PDF成功,字节数={}", pdfBytes.length);
            return pdfBytes;
        } catch (Exception e) {
            logger.error("临时文件方式生成PDF失败", e);
            throw new RuntimeException("PDF生成失败", e);
        } finally {
            // 5. 清理临时文件+关闭临时Driver
            tempDriver.quit();
            File pdfFile = new File(tempPdfPath);
            if (pdfFile.exists() && !pdfFile.delete()) {
                logger.warn("临时PDF文件清理失败:{}", tempPdfPath);
            }
        }
    }

    // ========== 原有方法保留 + 少量优化 ==========
    public HeadlessBrowserUtil get(String url) {
        try {
            driver.get(url);
            logger.debug("访问URL:{}", url);
            return this;
        } catch (Exception e) {
            logger.error("访问URL失败:{}", url, e);
            throw new RuntimeException("访问URL失败", e);
        }
    }

    public Object executeScript(String script, Object... args) {
        try {
            return ((JavascriptExecutor) driver).executeScript(script, args);
        } catch (Exception e) {
            logger.error("执行JS脚本失败:{}", script.substring(0, Math.min(script.length(), 100)), e);
            throw new RuntimeException("JS脚本执行失败", e);
        }
    }

    public Object executeAsyncScript(String script, Object... args) {
        try {
            return ((JavascriptExecutor) driver).executeAsyncScript(script, args);
        } catch (Exception e) {
            logger.error("执行异步JS脚本失败", e);
            throw new RuntimeException("异步JS脚本执行失败", e);
        }
    }

    public WebElement waitForElement(By locator) {
        return defaultWait.until(ExpectedConditions.presenceOfElementLocated(locator));
    }

    public WebElement waitForElement(By locator, long timeoutSeconds) {
        return new WebDriverWait(driver, Duration.ofSeconds(timeoutSeconds))
                .until(ExpectedConditions.presenceOfElementLocated(locator));
    }

    public WebElement waitForElementVisible(By locator) {
        return defaultWait.until(ExpectedConditions.visibilityOfElementLocated(locator));
    }

    public WebElement waitForElementClickable(By locator) {
        return defaultWait.until(ExpectedConditions.elementToBeClickable(locator));
    }

    public boolean waitForTextContains(By locator, String text) {
        return defaultWait.until(ExpectedConditions.textToBePresentInElementLocated(locator, text));
    }

    public boolean waitForAttributeValue(By locator, String attribute, String value) {
        return defaultWait.until(ExpectedConditions.attributeToBe(locator, attribute, value));
    }

    public boolean waitForTitleContains(String title) {
        return defaultWait.until(ExpectedConditions.titleContains(title));
    }

    public boolean waitForTitleIs(String title) {
        return defaultWait.until(ExpectedConditions.titleIs(title));
    }

    public WebElement waitForJsCompleteMarker(String markerId) {
        return waitForElement(By.id(markerId));
    }

    public int[] waitForJsCompleteWithResult(String markerId, String successAttr, String failAttr) {
        WebElement marker = waitForElement(By.id(markerId));
        String success = marker.getAttribute(successAttr);
        String fail = marker.getAttribute(failAttr);
        return new int[]{
                Integer.parseInt(success != null ? success : "0"),
                Integer.parseInt(fail != null ? fail : "0")
        };
    }

    public String waitForJsCompleteSimple(String inputId) {
        WebElement input = waitForElement(By.id(inputId));
        new WebDriverWait(driver, Duration.ofSeconds(defaultTimeoutSeconds))
                .until((Function<WebDriver, Boolean>) d -> {
                    String val = input.getAttribute("value");
                    return val != null && !val.isEmpty();
                });
        return input.getAttribute("value");
    }

    public boolean waitForBodyDataAttribute(String attributeName, String expectedValue) {
        String fullAttr = "data-" + attributeName;
        return defaultWait.until(ExpectedConditions.attributeToBe(By.tagName("body"), fullAttr, expectedValue));
    }

    public <T> T waitForCondition(Function<WebDriver, T> condition, long timeoutSeconds) {
        return new FluentWait<>(driver)
                .withTimeout(Duration.ofSeconds(timeoutSeconds))
                .pollingEvery(Duration.ofMillis(500))
                .ignoring(NoSuchElementException.class, StaleElementReferenceException.class) // 增加异常忽略
                .until(condition);
    }

    public WebElement findElement(By locator) {
        return driver.findElement(locator);
    }

    public List<WebElement> findElements(By locator) {
        return driver.findElements(locator);
    }

    public HeadlessBrowserUtil click(By locator) {
        waitForElementClickable(locator).click();
        return this;
    }

    public HeadlessBrowserUtil sendKeys(By locator, String text) {
        WebElement element = waitForElementVisible(locator);
        element.clear();
        element.sendKeys(text);
        return this;
    }

    public String getText(By locator) {
        return waitForElementVisible(locator).getText();
    }

    public String getPageSource() {
        return driver.getPageSource();
    }

    public String getCurrentUrl() {
        return driver.getCurrentUrl();
    }

    public String getTitle() {
        return driver.getTitle();
    }

    public HeadlessBrowserUtil refresh() {
        driver.navigate().refresh();
        return this;
    }

    public HeadlessBrowserUtil back() {
        driver.navigate().back();
        return this;
    }

    public HeadlessBrowserUtil forward() {
        driver.navigate().forward();
        return this;
    }

    public byte[] takeScreenshot() {
        return ((TakesScreenshot) driver).getScreenshotAs(OutputType.BYTES);
    }

    public File takeScreenshotAsFile() {
        return ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
    }

    public HeadlessBrowserUtil switchToFrame(By locator) {
        driver.switchTo().frame(waitForElement(locator));
        return this;
    }

    public HeadlessBrowserUtil switchToDefaultContent() {
        driver.switchTo().defaultContent();
        return this;
    }

    /**
     * 【过时】无头模式下无效,建议使用 generatePdfAsBytes()
     */
    @Deprecated
    public HeadlessBrowserUtil triggerPrint() {
        logger.warn("triggerPrint()方法在无头模式下无效,请使用generatePdfAsBytes()生成PDF");
        Actions actions = new Actions(driver);
        actions.keyDown(Keys.CONTROL).sendKeys("p").keyUp(Keys.CONTROL).perform();
        return this;
    }

    public HeadlessBrowserUtil sleep(long seconds) {
        try {
            Thread.sleep(seconds * 1000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return this;
    }

    public HeadlessBrowserUtil waitForPageLoad() {
        defaultWait.until(webDriver -> ((JavascriptExecutor) webDriver)
                .executeScript("return document.readyState").equals("complete"));
        return this;
    }

    public HeadlessBrowserUtil waitForLibraryLoad(String libraryName) {
        defaultWait.until(webDriver -> ((JavascriptExecutor) webDriver)
                .executeScript("return typeof " + libraryName + " !== 'undefined'").equals(true));
        return this;
    }

    @Override
    public void close() {
        if (driver != null) {
            try {
                driver.quit();
                logger.info("ChromeDriver已正常关闭");
            } catch (Exception e) {
                logger.error("ChromeDriver关闭失败", e);
            } finally {
                driver = null;
            }
        }
    }

    public WebDriver getDriver() {
        return driver;
    }

    public HeadlessBrowserUtil setDefaultTimeout(long seconds) {
        this.defaultWait = new WebDriverWait(driver, Duration.ofSeconds(seconds));
        return this;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

唐不是营养物质

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值