Selenium 4 现代化实战——WebDriver Manager、相对定位、CDP 协议
Selenium 4 现代化实战——WebDriver Manager、相对定位、CDP 协议
适读人群:Selenium 存量用户 / 需要了解 Selenium 4 新特性的工程师 | 阅读时长:约 14 分钟 | 核心价值:掌握 Selenium 4 的三大核心新特性,让老框架焕发新活力
那个每次更新 Chrome 就崩的 CI 流水线
我有个前同事叫阿峰,他们公司的 E2E 测试用 Selenium 3 + ChromeDriver,一切运行正常——直到 Chrome 自动升级。
每次 Chrome 大版本更新,他们的 CI 流水线就必然崩一次,报错:
SessionNotCreatedException: Message: session not created:
This version of ChromeDriver only supports Chrome version 110
Current browser version is 114.0.5735.90阿峰的解决方案是每次 Chrome 更新后,手动下载对应版本的 ChromeDriver,更新配置,重新跑流水线。
后来他升级到 Selenium 4,用上了 WebDriver Manager,这个问题彻底消失了。
Selenium 4 的三大核心新特性
Selenium 4 不是简单的版本迭代,它带来了三个根本性的改变:
- WebDriver Manager——自动管理浏览器驱动版本
- 相对定位器——基于元素位置关系定位
- Chrome DevTools Protocol (CDP) 支持——直接操控浏览器底层能力
我们逐一深入。
特性一:WebDriver Manager
Selenium 4.6+ 内置了驱动管理功能,不再需要手动下载和配置 ChromeDriver。
Maven 依赖
<dependencies>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>4.16.1</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.1</version>
<scope>test</scope>
</dependency>
</dependencies>新的初始化方式
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.edge.EdgeDriver;
public class DriverFactory {
public static WebDriver createDriver(String browserName) {
return switch (browserName.toLowerCase()) {
case "chrome" -> {
ChromeOptions options = new ChromeOptions();
options.addArguments("--headless=new"); // Chrome 112+ 的新 headless 模式
options.addArguments("--no-sandbox");
options.addArguments("--disable-dev-shm-usage");
options.addArguments("--window-size=1920,1080");
// Selenium 4.6+ 会自动下载匹配当前 Chrome 版本的 ChromeDriver
yield new ChromeDriver(options);
}
case "firefox" -> {
// 同样自动管理 GeckoDriver
yield new FirefoxDriver();
}
case "edge" -> {
// 自动管理 EdgeDriver
yield new EdgeDriver();
}
default -> throw new IllegalArgumentException("不支持的浏览器: " + browserName);
};
}
}如果需要更精细的控制,可以用独立的 WebDriverManager 库(io.github.bonigarcia):
<dependency>
<groupId>io.github.bonigarcia</groupId>
<artifactId>webdrivermanager</artifactId>
<version>5.6.3</version>
<scope>test</scope>
</dependency>// 使用 WebDriverManager 库
WebDriverManager.chromedriver().setup();
WebDriver driver = new ChromeDriver();
// 指定特定版本
WebDriverManager.chromedriver().driverVersion("114.0.5735.90").setup();
WebDriver driver = new ChromeDriver();
// 自动检测当前安装的 Chrome 版本并下载匹配的驱动
WebDriverManager.chromedriver().browserVersion("latest").setup();特性二:相对定位器(Relative Locators)
相对定位器允许基于已知元素的位置关系来定位其他元素,适合那些没有合适 ID 或 class 的元素。
import org.openqa.selenium.support.locators.RelativeLocator;
// 假设有这样的 HTML 结构:
// <label for="username">用户名</label>
// <input id="username" type="text">
// <label for="password">密码</label>
// <input id="password" type="password">
// <button type="submit">登录</button>
WebDriver driver = new ChromeDriver();
driver.get("https://your-app.example.com/login");
// 定位用户名 label 右边的输入框
WebElement usernameInput = driver.findElement(
RelativeLocator.with(By.tagName("input")).toRightOf(By.xpath("//label[@for='username']"))
);
// 定位密码输入框(在密码 label 的下方)
WebElement passwordInput = driver.findElement(
RelativeLocator.with(By.tagName("input")).below(By.cssSelector("label[for='username']"))
);
// 定位提交按钮(在密码输入框的下方)
WebElement submitButton = driver.findElement(
RelativeLocator.with(By.tagName("button")).below(By.id("password"))
);
// 组合多个相对关系
WebElement targetCell = driver.findElement(
RelativeLocator.with(By.tagName("td"))
.toRightOf(By.xpath("//td[text()='订单金额']"))
.above(By.xpath("//td[text()='税费']"))
);相对定位器支持的方向:
| 方法 | 含义 |
|---|---|
above(locator) | 在指定元素上方 |
below(locator) | 在指定元素下方 |
toLeftOf(locator) | 在指定元素左侧 |
toRightOf(locator) | 在指定元素右侧 |
near(locator) | 在指定元素附近(默认 50px 范围) |
near(locator, distance) | 在指定像素范围内 |
适用场景: 表单中的输入框缺乏 ID,表格中的数据单元格,复杂布局中没有明确标识的元素。
注意: 相对定位器基于元素在视口中的像素坐标,如果页面响应式布局导致元素位置变化,定位可能失效。建议只在没有其他定位手段时使用。
特性三:CDP 协议支持
Chrome DevTools Protocol 是 Chrome 浏览器暴露的底层控制接口,Selenium 4 通过 CDP 让 Java 测试代码能做很多之前不可能完成的事情。
3.1 网络拦截和修改
import org.openqa.selenium.devtools.DevTools;
import org.openqa.selenium.devtools.v119.network.Network;
import org.openqa.selenium.devtools.v119.network.model.*;
ChromeDriver driver = new ChromeDriver();
DevTools devTools = driver.getDevTools();
devTools.createSession();
// 启用网络监控
devTools.send(Network.enable(
Optional.empty(),
Optional.empty(),
Optional.empty()
));
// 监听请求
devTools.addListener(Network.requestWillBeSent(), request -> {
System.out.println("请求URL: " + request.getRequest().getUrl());
System.out.println("请求方法: " + request.getRequest().getMethod());
});
// 监听响应
devTools.addListener(Network.responseReceived(), response -> {
System.out.println("响应状态: " + response.getResponse().getStatus());
System.out.println("响应URL: " + response.getResponse().getUrl());
});
driver.get("https://your-app.example.com");3.2 模拟地理位置
import org.openqa.selenium.devtools.v119.emulation.Emulation;
DevTools devTools = driver.getDevTools();
devTools.createSession();
// 设置地理位置(北京)
devTools.send(Emulation.setGeolocationOverride(
Optional.of(39.9042), // 纬度
Optional.of(116.4074), // 经度
Optional.of(0.0) // 精度(米)
));
driver.get("https://your-app.example.com/nearby-stores");
// 现在应用会认为用户在北京3.3 模拟网络条件
import org.openqa.selenium.devtools.v119.network.model.ConnectionType;
// 模拟 3G 网络(测试弱网场景)
devTools.send(Network.emulateNetworkConditions(
false, // 是否离线
100, // 延迟(毫秒)
1500 * 1024, // 下载速度(字节/秒)1.5 Mbps
750 * 1024, // 上传速度(字节/秒)0.75 Mbps
Optional.of(ConnectionType.CELLULAR3G)
));
driver.get("https://your-app.example.com");
// 现在所有网络请求都会有 3G 网络的延迟和速度限制
// 模拟离线
devTools.send(Network.emulateNetworkConditions(
true, // 离线
0, 0, 0,
Optional.empty()
));3.4 拦截并修改请求(Request Interception)
// 设置请求拦截
devTools.send(Network.setRequestInterception(
List.of(new RequestPattern(
Optional.of("*/api/user*"), // URL 模式
Optional.empty(),
Optional.of(InterceptionStage.REQUEST)
))
));
// 处理被拦截的请求
devTools.addListener(Network.requestIntercepted(), request -> {
String url = request.getRequest().getUrl();
if (url.contains("/api/user/profile")) {
// 返回 Mock 数据
devTools.send(Network.continueInterceptedRequest(
request.getInterceptionId(),
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.of("{\"name\":\"Mock用户\",\"role\":\"admin\"}"),
Optional.of(Map.of("Content-Type", "application/json")),
Optional.empty(),
Optional.empty()
));
} else {
// 放行正常请求
devTools.send(Network.continueInterceptedRequest(
request.getInterceptionId(),
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty()
));
}
});3.5 Console 日志捕获
import org.openqa.selenium.devtools.v119.log.Log;
import java.util.ArrayList;
import java.util.List;
List<String> consoleErrors = new ArrayList<>();
devTools.send(Log.enable());
devTools.addListener(Log.entryAdded(), entry -> {
if ("error".equals(entry.getLevel().toString())) {
consoleErrors.add(entry.getText());
}
});
driver.get("https://your-app.example.com");
// 执行测试操作...
// 断言没有控制台错误
assertTrue("存在 Console 错误: " + consoleErrors, consoleErrors.isEmpty());踩坑实录
坑一:CDP API 版本号写死导致跨 Chrome 版本不兼容
现象: 代码里 import 的是 org.openqa.selenium.devtools.v119.network.Network,Chrome 升级到 120 后,编译或运行时报错。
原因: Selenium 的 CDP 绑定是按 Chrome 版本编号的,不同版本的 API 包名不同。
解法: 升级 Selenium 版本以获取新 Chrome 版本的 CDP 绑定,或者使用版本无关的 HasDevTools 接口:
// 尽量用高版本 selenium-java,它会包含多个 CDP 版本
// 如果 v119 的 import 失败,改成 v120、v121 等最新版本
// 更稳健的做法是封装一层,隔离 CDP 版本
public class ChromeDevToolsHelper {
private final DevTools devTools;
public ChromeDevToolsHelper(ChromeDriver driver) {
this.devTools = driver.getDevTools();
this.devTools.createSession();
}
// 封装具体 CDP 调用,版本相关的 import 只在这一个类里
}坑二:相对定位器找到错误的元素
现象: 使用相对定位器定位表格中的数据单元格,有时找到正确的,有时找到相邻行的。
原因: 相对定位器的"below/above"判断基于元素中心点的纵坐标距离,如果多个元素纵向坐标接近,可能选中错误的。
解法: 增加更多约束条件:
// 只用 below 可能不够精确
WebElement cell = driver.findElement(
RelativeLocator.with(By.tagName("td")).below(By.xpath("//td[text()='订单金额']"))
);
// 同时加上 toRightOf 约束,更精确
WebElement cell = driver.findElement(
RelativeLocator.with(By.tagName("td"))
.below(By.xpath("//th[text()='金额']"))
.toRightOf(By.xpath("//td[text()='2024-01-01']"))
);坑三:Headless 模式下 JavaScript 对话框处理失效
现象: 非 headless 模式下 driver.switchTo().alert().accept() 正常,headless 模式下无效或超时。
原因: Chrome 新 Headless 模式(--headless=new)与旧 headless 模式行为差异,以及某些 JS 对话框在 headless 下的渲染问题。
解法:
// 提前注册对话框处理器
driver.register(new Alert() {
// ...
});
// 或者用 CDP 提前处理
devTools.addListener(Page.javascriptDialogOpening(), dialog -> {
System.out.println("对话框: " + dialog.getMessage());
devTools.send(Page.handleJavaScriptDialog(true, Optional.of("")));
});Selenium 4 的 W3C BiDi 协议(未来方向)
Selenium 4 开始引入 W3C WebDriver BiDi 协议,这是比 CDP 更标准化的浏览器自动化协议,未来会取代 CDP 并支持所有浏览器(不只 Chrome)。
import org.openqa.selenium.bidi.module.LogInspector;
import org.openqa.selenium.bidi.log.ConsoleLogEntry;
// 使用 BiDi 监听 Console 日志(所有浏览器通用)
try (LogInspector logInspector = new LogInspector(driver)) {
CompletableFuture<ConsoleLogEntry> future = new CompletableFuture<>();
logInspector.onConsoleEntry(future::complete);
driver.get("https://your-app.example.com");
ConsoleLogEntry logEntry = future.get(5, TimeUnit.SECONDS);
System.out.println("Console 日志: " + logEntry.getText());
}BiDi 目前还在快速发展中,API 可能变化,生产环境谨慎使用,但值得关注。
小结
Selenium 4 的三大新特性解决了三个长期痛点:
- WebDriver Manager 解决了驱动版本管理的噩梦
- 相对定位器 提供了一种新的定位思路,对复杂布局有帮助
- CDP 支持 让 Selenium 在网络拦截、弱网模拟、地理位置伪造等场景有了真正的能力
如果你的团队还在用 Selenium 3,升级到 4 是值得的。如果在做新项目,还是建议评估 Playwright。
