尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Java+Selenium打印自动化实战:CDP生成PDF与断言指南

发布时间:2026/9/26 7:00:58

资讯中心
01
ARTICLE

Java+Selenium打印自动化实战:CDP生成PDF与断言指南

Java+Selenium打印自动化实战:CDP生成PDF与断言指南
做Web自动化的人多半都碰过这个需求业务方丢过来一句“帮我把浏览器里的打印功能也测一下”。Java Selenium实现浏览器打印功能听起来不过就是driver点一下“打印”按钮的事但真跑到那一步脚本十有八九卡死在一个不受WebDriver控制的系统对话框上。这篇文章就围绕这个老难题把我在实践中趟过的几条技术路线——CDP、Robot、前端PDF、静默打印——掰开揉碎讲清楚尽量还原一个从“打印预览弹不出来”到“稳定产出PDF并能自动断言内容”的完整过程。适合正卡在这个需求上的测试开发也适合想搞懂Selenium能力边界的人。1. 打印对话框那道“跨不过去的屏障”WebDriver与浏览器原生UI的边界1.1 为什么Selenium点不掉那个“打印”按钮很多人第一次写打印自动化用例时都是这么想的定位到页面上的“打印”按钮click()然后看着浏览器弹出打印预览框再让脚本去操作预览框里的“保存”按钮。结果click一执行脚本就悬在那了——打印预览框确实弹出来了但WebDriver的findElement去定位预览框里的元素无论怎么等都定位不到最终超时。这个问题的根源在于打印预览对话框不属于DOM。Chrome的打印预览虽然长得像页面但它其实是浏览器UI层的东西就像你点浏览器右上角的“三个点”弹出的菜单一样WebDriver协议压根不负责操作这层UI。Selenium能控制的是一个App里面的内容而打印对话框是浏览器这个“容器外壳”的一部分它不属于你正在测试的那个页面。用遥控器看电视打个比方你能控制电视频道、音量但电视外壳上的实体电源键遥控器是够不着的——除非你的遥控器恰好有红外学习功能。Chrome、Firefox在这个问题上表现各异但结论一致原生打印对话框都无法用WebDriver直接定位和操作。Chrome的预览框是一个内置页面理论上可以通过调试接口拿到底层信息但Selenium没有暴露这样的封装Firefox的打印预览更传统完全是一个系统模态窗口Edge因为和Chrome同内核表现和Chrome一模一样。更麻烦的是当你调用window.print()触发打印预览后有的浏览器会阻塞页面脚本执行导致WebDriverWait等待页面状态的时候一起卡住整个用例就那么挂到超时。1.2 常见误判window.print()能否被断言或重写遇到弹框第一反应往往是“我在JS层面能不能绕一下”。比如重写window.print function() {}把弹窗吞掉这确实能让用例不卡死但问题也来了你只是把弹窗屏蔽了打印行为本身没有被验证。业务要的是“打印出来的PDF正确”而不是“点击打印按钮不报错”。用一个空函数替换打印逻辑等于你只是在验证按钮的click事件绑定了打印链路完全是断的。还有一个更根本的限制window.print()本身没有返回值也不提供任何回调通知。它的设计初衷就是告诉浏览器“用户想打印了”至于打印预览有没有弹出、用户最后有没有点保存、PDF有没有生成JS层面一概不知。所以你没法用JavaScript去断言打印成功与否。写自动化踩过几次坑之后我的结论是不要试图在对话框层面和它硬碰硬而是要想办法绕开它去验证对话框背后真正有价值的那个东西——打印的结果也就是那份PDF。2. 四条技术路线横评CDP、Robot、前端PDF、静默打印2.1 先把四条路都摆出来要绕开原生打印对话框业界常见的有四条路。我一个个试过先把它们的核心逻辑和适用场景摆出来对比技术方案核心原理可控性断言能力维护成本典型场景CDP Page.printToPDF通过Chrome DevTools协议调用浏览器底层打印引擎直接拿到PDF数据高纸张、边距、背景都能精确控制强拿到的是一份完整PDF可以做文本、页数、路径断言中自动化回归、打印内容校验Robot模拟CtrlP用Robot类模拟键盘快捷键触发系统打印流程低依赖窗口焦点和坐标系弱打印结果难以回传高非常脆本机临时手工辅助jsPDF html2canvas在页面里截取DOM渲染成图片再拼成PDF绕过浏览器打印机制高样式完全自己控制强PDF直接在JS里生成可回传数据中业务本身就是“生成PDF供下载”浏览器静默打印参数启动时加--kiosk-printing让window.print()直接送打印机不弹窗中只能选默认打印机弱文件是否生成难以自动感知低CI环境配合虚拟PDF打印机表格看完你就发现了没有一条路是“点原生对话框”的全是绕。绕的方式不一样后续的断言能力和稳定性天差地别。2.2 CDP这条路为什么值得深入四条路里我对CDP Page.printToPDF的偏爱是极其明显的。原因不复杂它是Chrome专门为程序化打印设计的一个命令它不是模拟而是直接调用浏览器真实的打印引擎。你传一个JSON参数给它它把当前页面渲染后返回一段Base64编码的PDF数据干净利落没有任何UI。对自动化来说这份PDF数据就是宝贝你可以落盘可以用PDFBox去解析里面的文本可以数页数可以做关键字断言——所有测试人员想要的验证手段它都接得上。另一个让CDP方案胜出的原因是Selenium 4把这个门槛拉低了很多。在Selenium 3时代想走CDP得自己去连ChromeDriver暴露的WebSocket端口自己手动拼协议请求还要处理消息路由光是握手就要写一堆样板代码。Selenium 4直接内置了DevTools接口driver.getDevTools().createSession()之后就可以往浏览器发CDP命令。Java Selenium CDP这套组合现在已经相当顺滑。当然CDP也不是银弹它只适用于Chromium系浏览器Firefox那边就没有对等的官方API这么方便。但绝大多数面向B端的管理系统都是“Chrome内核预定”先把Chrome这条路走扎实对大部分团队都够用了。3. Java Selenium 4实操用CDP把打印变成可控API3.1 环境与依赖准备先交代一下我跑通这套方案时的环境方便你复现时对标JDK 8或11任何能跑Java项目的版本都行Selenium Java版本4.6以上建议直接上4.15或更新Chrome浏览器版本不用太纠结Selenium Manager会自动匹配chromeDriverMaven或Gradle引入selenium-java和pdfboxMaven依赖这样写dependency groupIdorg.seleniumhq.selenium/groupId artifactIdselenium-java/artifactId version4.15.0/version /dependency dependency groupIdorg.apache.pdfbox/groupId artifactIdpdfbox/artifactId version2.0.29/version /dependency一个小提醒Selenium 4.6开始内置了Selenium Manager第一次跑的时候会自动去下载匹配版本的chromeDriver。如果你们是内网环境Selenium Manager下载不了driver就需要手动指定webdriver.chrome.driver系统属性并把Chrome和driver版本严格对应上否则会报session not created异常。3.2 核心代码从DevTools会话到PDF文件直接看完整代码我跑了无数次这段是可以直接粘到项目里的。import org.openqa.selenium.chrome.ChromeDriver; import org.openqa.selenium.chrome.ChromeOptions; import org.openqa.selenium.devtools.DevTools; import org.openqa.selenium.devtools.Command; import java.io.FileOutputStream; import java.util.Base64; import java.util.HashMap; import java.util.Map; public class PrintToPdfDemo { public static void main(String[] args) throws Exception { ChromeOptions options new ChromeOptions(); // 无头模式跑打印更稳定建议直接用 options.addArguments(--headlessnew); ChromeDriver driver new ChromeDriver(options); // 1. 创建DevTools会话 DevTools devTools driver.getDevTools(); devTools.createSession(); // 2. 开启Page域printToPDF属于这个域 devTools.send(new Command(Page.enable)); // 3. 打开目标页面 driver.get(https://example.com/order/10086); // 4. 这里值得多说一句一定要等页面渲染完再打印 // 不然PDF里会出现空白页或字体丢失下面第五节还会细讲 waitForPageReady(driver); // 5. 组装printToPDF参数 MapString, Object params new HashMap(); params.put(printBackground, true); // 打印背景色和背景图 params.put(paperWidth, 8.27); // A4宽度单位英寸 params.put(paperHeight, 11.69); // A4高度 params.put(marginTop, 0.4); params.put(marginBottom, 0.4); params.put(marginLeft, 0.4); params.put(marginRight, 0.4); params.put(scale, 1.0); // 缩放比例 params.put(preferCSSPageSize, false); // 是否优先使用CSS page尺寸 // 6. 发送命令拿到Base64编码的PDF MapString, Object result devTools.send(new Command(Page.printToPDF, params)); String base64Pdf (String) result.get(data); // 7. 解码并落盘 byte[] pdfBytes Base64.getDecoder().decode(base64Pdf); String fileName print_ System.currentTimeMillis() .pdf; try (FileOutputStream fos new FileOutputStream(fileName)) { fos.write(pdfBytes); } System.out.println(PDF已保存: fileName); driver.quit(); } }这里的核心就三步获取DevTools会话发送Page.printToPDF命令解析返回的Base64数据。你不需要理解WebSocket层的东西Selenium 4把协议细节都封住了。关于参数我再补充几个用得多的printBackground控制的是background-color和background-image是否打出来默认false经常有同学忘了开导致PDF上一片白还以为是页面样式问题scale是全局缩放如果你发现内容太挤可以调成0.8preferCSSPageSize这个参数很容易和页面里的CSS冲突后面踩坑部分专门说。3.3 很关键的一步让页面打印样式与CDP打印参数协同很多人拿到CDP方案后都有个困惑我打印出来的PDF为什么和页面上看到的不一样布局乱了、导航栏出来了、表格被截断。这个问题的根源往往不在CDP而在页面的打印样式没有写好。浏览器打印引擎有一个规则当存在media print样式时它会以这份样式为准来渲染打印内容。也就是说CDP的printToPDF参数控制的是“打印机设置”——纸张、边距、缩放而页面CSS控制的是“打印版式”——哪些元素显示、哪些隐藏、字体多大。一个典型的页面需要这样设计打印样式media print { /* 页面上不需要出现的元素打印时隐藏 */ .no-print, .sidebar, .header, .footer { display: none !important; } /* 主内容区占满打印宽度 */ .print-area { width: 100%; margin: 0; padding: 0; } /* 避免表格内容被截断时出现难看的跨页样式 */ table { break-inside: avoid; } }我的经验是凡是打印类用例先把页面的media print样式核对一遍再调CDP不然你会在“PDF内容不对”这个坑里绕很久。很多前端开发压根没写过media print默认样式直接用于打印结果导航栏、侧边栏全印上去了页面宽度又超过纸张右边直接被切掉。这一点解决了后面CDP参数才能真正稳定。4. 从按钮到PDF落盘一个订单打印场景的完整落地过程4.1 先想清楚被测对象打印按钮和打印结果实际业务里的打印按钮样式和触发方式五花八门但底子基本都是window.print()。自动化要验证的不是弹窗本身而是“打印结果”正确。所以我在项目里设了一个默认规则自动化环境不触发原生打印预览直接用CDP打印当前页面然后对PDF做断言。这个规则看似绕过了按钮实际上没有偏离业务。因为按钮点下去之后浏览器的打印引擎拿到的也是这同一份渲染好的页面走同一套media print样式。我们的断言点从“对话框弹出来”平移到了“打印结果是什么样”后者恰恰是业务真正关心的。一个订单打印用例的断言清单一般是这样的PDF文件确实生成且大小大于某个阈值比如10KBPDF总页数等于预期订单摘要页一般就是一页PDF文本内容包含订单号、收货人、金额等关键字段如果需要可以进一步校验表格里的明细条目数4.2 代码落位等待渲染、触发打印、按规则命名等待页面渲染这件事我强烈建议不要用死等Thread.sleep。一个订单页面可能有图片、有字体、有动态加载的数据sleep时间短了容易白页长了拖慢整个用例集。我常用的是一个组合条件document.readyState等于complete同时关键图片元素或某个数据绑定元素的加载状态符合预期。简单可靠的方法是先用Selenium的JavascriptExecutor轮询判断public static void waitForPageReady(ChromeDriver driver) { new WebDriverWait(driver, 15).until( webDriver - ((JavascriptExecutor) webDriver) .executeScript(return document.readyState) .equals(complete) ); // 再给字体加载一个缓冲如果页面用了自定义字体 new WebDriverWait(driver, 10).until( webDriver - ((JavascriptExecutor) webDriver) .executeScript(return document.fonts document.fonts.ready) .toString().contains(fulfilled) ); }文件名命名我建议带上业务标识和时间戳比如order_10086_20250115_103000.pdf。做回归的时候在CI报告里直接就能根据用例关联到对应的PDF文件排查问题省很多事。4.3 用PDFBox做内容断言拿到PDF之后断言方式就多了。我的默认选择是PDFBox因为文本提取稳定、速度快而且对中文支持也够用。下面是去掉包装后的核心代码import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.text.PDFTextStripper; public static void assertPdfContent(String pdfPath, String expectedText) throws Exception { try (PDDocument doc PDDocument.load(new File(pdfPath))) { // 断言页数比如订单摘要是一页 int pageCount doc.getNumberOfPages(); assertTrue(页数异常: pageCount, pageCount 1); // 提取文本 PDFTextStripper stripper new PDFTextStripper(); String text stripper.getText(doc); // 断言关键内容 assertTrue(PDF中未找到期望文本: expectedText, text.contains(expectedText)); } }这里有一个实际测试中的心得如果你发现PDFBox抽出来的中文是乱码先别急着怀疑PDFBoxChrome生成的PDF在文本提取层通常是没问题的乱码往往意味着font subset信息缺失需要检查页面字体加载完成情况。这也是为什么我在4.2坚持要等document.fonts.ready之后再打印那一步就是防这个的。4.4 输出目录、清理与并发隔离PDF文件是测试产物落盘位置和清理策略得提前定好不然CI跑一个月下来磁盘会被填满。我一般每个测试类都建一个独立子目录放在target/print-pdfs/{类名}/下面这样并发跑多个用例不会互相覆盖文件。测试结束后保留最新一轮的结果用于排查再往前的历史文件统一清理。特别提醒一点如果你的回归任务被设计成可以并发跑文件名里不带类名或线程标识的话两个用例同时写一个文件轻则断言互相干扰重则报文件占用异常。这个坑我在CI上踩过一次从那以后文件名里一律带上了用例标识和时间戳。5. 实战踩坑记录printToPDF的五个“坑你没商量”的细节5.1 参数类型别乱传布尔、数字、字符串的区别第一次用Selenium 4的DevTools发送printToPDF命令时我在参数类型上栽了个跟头。当时参考了一段Python代码里面printBackground传的是字符串true我照搬到Java里结果PDF的背景图死活打不出来也不报错。后来翻了CDP协议定义才发现printBackground在协议里是boolean类型Java这边要用Boolean.TRUE数字参数比如paperWidth、marginTop要传Double类型。Selenium的DevTools在序列化时是严格按类型处理的传字符串true和传布尔true序列化结果在JSON层面就不一样Chrome解析时按协议类型去取取不到就用了默认值。正确写法开头代码里已经给了这里再强调一遍布尔字段传Boolean数字字段传Double不要偷懒写成字符串。你在网上看到很多Python示例传字符串是能跑的因为Python的字典类型到了JSON序列化环节会自动适配但Java里Map的泛型是Object类型全看你自己怎么塞。5.2 preferCSSPageSize与CSS page打架时谁说了算这个坑相当阴险。页面某天加了一行CSSpage { size: A5 landscape; }然后你发现CDP打印出来的PDF突然全部变成A5横版了你明明在参数里传了A4纸张尺寸。原因就是你设了preferCSSPageSize为true或者某次升级后它的默认行为发生了变化。协议文档里写得很清楚当preferCSSPageSize为true时printToPDF完全采用CSS里page指定的尺寸忽略paperWidth和paperHeight参数。所以团队里最好有个明确约定纸张尺寸统一由CDP参数控制那就在参数里固定preferCSSPageSizefalse并且要求所有页面都不要写page尺寸规则反过来如果业务确实需要页面自己去定义不同纸张比如快递单是热敏纸订单是A4那就用preferCSSPageSizetrue在页面CSS里统一维护一套page规则。最怕的就是两边都在调测试看PDF不对折腾半天才发现是CSS里藏了一个page。5.3 字体图片没加载完PDF出白页PDF生成出来多一页内容还缺了一片这种问题我统计过绝大多数是时序问题。Chrome的printToPDF是“快照”当前渲染状态如果调用命令那一刻图片还在加载、字体还在网络请求中快照里就是空白占位。尤其是一些大报表页面表格几千行、图表引用外部数据加载时间差个几百毫秒PDF内容就差了十万八千里。解决方案不是猛加sleep而是做有意义的等待。我推荐两个条件document.fonts.ready返回完成态以及networkidle状态——后者可以通过Chrome DevTools的Network域监听或者简单点用JavaScript轮询performance.getEntriesByType(resource)看最后一个关键资源的时间戳。等项目跑稳了再把这些等待条件固化成工具方法不要每次写死。5.4 Robot方案在CI上为什么总翻车Robot模拟CtrlP这套我最早期的项目里用过后来彻底放弃了。问题出在它太依赖运行环境。本机调试的时候窗口焦点在浏览器上模拟按键没问题一上CI机器同时跑着好几个任务焦点的归属完全不可控Enter按下去可能点到了别的窗口上。再加上高分屏的DPI缩放屏幕坐标系和物理坐标对不上坐标点击就是灾难。更要命的是无头模式下Robot完全无效因为根本没有可视窗口可以拿焦点。而无头模式恰恰是CI最常用的运行方式。所以我现在的态度很明确Robot只能作为开发环境里手动“代点一下”的辅助手段任何需要稳定运行的自动化流水线都不要碰这条方案。5.5 无头模式和有头模式打印结果居然不一样最后这个坑更隐蔽。Chrome的--headlessnew模式视口的默认尺寸和渲染方式跟有头模式不一样这直接导致打印PDF的分页点不同。同样的页面无头模式打印出来是1页有头模式可能变2页或者表格的行在某处断开的位置不同。所以打印类用例要么统一在无头模式跑要么统一在有头模式跑别混着来。我一开始没注意开发环境手工验证用有头CI流水线是无头两边结论对不上排查了很久才发现是这个原因。另外建议在ChromeOptions里显式设置window-size参数把视口宽度固定下来比如1280x800分页逻辑就稳定很多。6. 方案选型建议不同业务诉求下我应该用哪一招6.1 一张决策清单直接套看完前面的对比和踩坑你可能已经能拍板了。为了更直观我整理了一个决策清单遇到打印类需求直接对号入座业务诉求推荐方案核心理由不建议的原因回归测试要验证打印内容正确CDP printToPDF PDFBox可控性强、结果可断言、稳定Robot方案在CI上不可靠用户要一个“下载PDF”的功能按钮jsPDF html2canvasPDF在页面内生成本身就是业务功能CDP是测试专用不适合生产环境调用批量单据要直接送打印机--kiosk-printing静默打印省去弹窗链路最短预览对话框本来就该由用户操作自动化难以介入需要验证打印预览弹窗本身的UI样式手动测试 截图对比原生对话框无法WebDriver定位只能从视觉层验证CDP返回的是PDF不是弹窗服务端要生成PDF供其他系统消费Java后端PDF库如iText与浏览器无关稳定可控浏览器方案是为前端渲染设计的链路重、性能差6.2 三个稳定性的底层原则这套打印自动化踩了这么多坑沉淀下来其实就三条原则。第一永远不要操作原生打印对话框。不管用什么方案遇到打印预览框默认它无法被WebDriver操作直接绕到它背后去。这个认知能帮你省下大量排查时间。第二断言一定要落在PDF内容上而不是落在“弹窗有没有出来”上。前面的链路设计已经说明了PDF文本、页数、文件大小这些才能支撑起有效的回归断言。第三把打印能力封装成一个独立工具类。不要在每个用例里重新写一遍DevTools会话、参数组装、等待逻辑。我项目里的封装大致是这样的public class PrintPdfUtil { public static byte[] printCurrentPageToPdf(ChromeDriver driver) { DevTools devTools driver.getDevTools(); devTools.createSession(); devTools.send(new Command(Page.enable)); waitForReady(driver); MapString, Object params defaultParams(); MapString, Object result devTools.send(new Command(Page.printToPDF, params)); return Base64.getDecoder().decode((String) result.get(data)); } }封装之后用例代码里就是一行调用团队统一改参数也方便。6.3 跨浏览器兼容怎么取舍最后说说Firefox。Firefox的打印预览和Chrome差异很大Selenium官方对Firefox的打印支持也远没有Chrome的DevTools接口这么顺手。如果你们的产品明确是多浏览器兼容我的建议是把“打印内容验证”定位成Chrome专项用例在Firefox上只做按钮可点击、页面不报错这一层的基础验证。不是技术不能做而是投入产出比太低。多数B端后台系统都深度绑定了Chrome的兼容性在这个前提下先把Chrome的打印链路打磨到极致比每个浏览器都糊一层更值钱。这个需求我自己前后做过三版方案。第一版用Robot被CI的焦点问题折磨到放弃第二版用CDP参数和CSS打架PDF页数忽多忽少查了整整一天第三版才把参数、等待条件、输出目录全部固化下来才算真正稳定。现在的项目里凡是涉及打印的用例我默认都走CDP PDFBox这条链路已经稳定跑了大半年。最后再分享一个我个人的习惯如果页面里有图表又有表格打印前一定等图表渲染完成再调printToPDF否则图表区域经常在PDF里留白。先把CDP跑通再回头串业务按钮是最不会绕远路的一条路径。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。