好的,我直接为您生成**Framework底层完整的代码框架**,包含全局异常拦截器、异常分类映射、重试机制和现场保留功能。基于 **Java + TestNG + Selenium** 技术栈,采用**责任链模式**和**装饰器模式**设计。
---
## Framework 代码框架结构
```
quality-mesh-framework/
├── src/main/java/com/qualitymesh/framework/
│ ├── core/
│ │ ├── driver/
│ │ │ ├── DriverManager.java // WebDriver管理器
│ │ │ ├── DriverFactory.java // Driver创建工厂
│ │ │ └── BrowserType.java // 浏览器类型枚举
│ │ ├── exception/
│ │ │ ├── QualityMeshException.java // 中台统一异常
│ │ │ ├── ErrorCode.java // 错误码枚举
│ │ │ └── ErrorLabel.java // 错误标签枚举
│ │ ├── interceptor/
│ │ │ ├── WebDriverInterceptor.java // 全局拦截器(核心)
│ │ │ ├── RetryHandler.java // 重试处理器
│ │ │ └── ExceptionMapper.java // 异常映射器
│ │ ├── listener/
│ │ │ ├── TestNGListener.java // TestNG监听器
│ │ │ └── ScreenshotListener.java // 截图监听器
│ │ └── context/
│ │ ├── ExecutionContext.java // 执行上下文
│ │ └── ArtifactStorage.java // 产物存储
│ ├── page/
│ │ └── BasePage.java // 页面基类(封装操作)
│ ├── test/
│ │ └── BaseTest.java // 测试基类
│ └── utils/
│ ├── JsonUtils.java // JSON工具
│ ├── FileUtils.java // 文件工具
│ └── WaitUtils.java // 等待工具
├── src/test/java/com/qualitymesh/demo/
│ └── DemoTest.java // 示例测试
└── pom.xml // Maven依赖
```
---
## 核心代码实现
### 1. 错误码与标签枚举
```java
package com.qualitymesh.framework.core.exception;
/**
* 中台统一错误码
*/
public enum ErrorCode {
UI_ELEM_NOT_FOUND("UI_ELEM_NOT_FOUND", "元素未找到"),
UI_TIMEOUT("UI_TIMEOUT", "等待超时"),
UI_STALE_ELEM("UI_STALE_ELEM", "陈旧元素引用"),
UI_FRAME_NOT_FOUND("UI_FRAME_NOT_FOUND", "Frame切换失败"),
UI_ALERT_NOT_FOUND("UI_ALERT_NOT_FOUND", "弹窗不存在"),
UI_UNHANDLED_ALERT("UI_UNHANDLED_ALERT", "未处理的弹窗"),
UI_DRIVER_CONN_LOST("UI_DRIVER_CONN_LOST", "驱动连接断开"),
UI_SESSION_EXPIRED("UI_SESSION_EXPIRED", "Session失效"),
UI_JS_ERROR("UI_JS_ERROR", "JS执行错误"),
UI_ELEM_NOT_ACTIVE("UI_ELEM_NOT_ACTIVE", "元素不可交互"),
UI_CLICK_INTERCEPTED("UI_CLICK_INTERCEPTED", "点击被遮挡"),
UI_SESSION_CREATE_FAIL("UI_SESSION_CREATE_FAIL", "浏览器启动失败"),
UI_UNKNOWN_ERROR("UI_UNKNOWN_ERROR", "未知WebDriver异常");
private final String code;
private final String description;
ErrorCode(String code, String description) {
this.code = code;
this.description = description;
}
public String getCode() { return code; }
public String getDescription() { return description; }
}
```
---
### 2. 统一异常类
```java
package com.qualitymesh.framework.core.exception;
import lombok.Getter;
import java.util.Map;
/**
* 中台统一异常,携带错误码和现场数据
*/
@Getter
public class QualityMeshException extends RuntimeException {
private final ErrorCode errorCode;
private final String errorLabel;
private final Map<String, Object> context; // 现场上下文数据
public QualityMeshException(ErrorCode errorCode, String message, Throwable cause, Map<String, Object> context) {
super(message, cause);
this.errorCode = errorCode;
this.errorLabel = errorCode.getDescription();
this.context = context;
}
public QualityMeshException(ErrorCode errorCode, String message, Map<String, Object> context) {
super(message);
this.errorCode = errorCode;
this.errorLabel = errorCode.getDescription();
this.context = context;
}
public String toJson() {
// 将异常序列化为JSON格式,供日志采集
return String.format(
"{\"errorCode\":\"%s\",\"errorLabel\":\"%s\",\"message\":\"%s\",\"context\":%s}",
errorCode.getCode(), errorLabel, getMessage(), JsonUtils.toJson(context)
);
}
}
```
---
### 3. 异常映射器(Selenium → 中台错误码)
```java
package com.qualitymesh.framework.core.interceptor;
import com.qualitymesh.framework.core.exception.ErrorCode;
import org.openqa.selenium.*;
import org.openqa.selenium.remote.UnreachableBrowserException;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
/**
* 异常映射器:将Selenium原生异常映射为中台错误码
*/
public class ExceptionMapper {
private static final Map<Class<? extends WebDriverException>, ErrorCode> MAPPING = new HashMap<>();
private static final Map<Class<? extends WebDriverException>, Boolean> RETRYABLE = new HashMap<>();
static {
// 异常 → 错误码 映射
MAPPING.put(NoSuchElementException.class, ErrorCode.UI_ELEM_NOT_FOUND);
MAPPING.put(TimeoutException.class, ErrorCode.UI_TIMEOUT);
MAPPING.put(StaleElementReferenceException.class, ErrorCode.UI_STALE_ELEM);
MAPPING.put(NoSuchFrameException.class, ErrorCode.UI_FRAME_NOT_FOUND);
MAPPING.put(NoAlertPresentException.class, ErrorCode.UI_ALERT_NOT_FOUND);
MAPPING.put(UnhandledAlertException.class, ErrorCode.UI_UNHANDLED_ALERT);
MAPPING.put(UnreachableBrowserException.class, ErrorCode.UI_DRIVER_CONN_LOST);
MAPPING.put(InvalidSessionIdException.class, ErrorCode.UI_SESSION_EXPIRED);
MAPPING.put(JavascriptException.class, ErrorCode.UI_JS_ERROR);
MAPPING.put(ElementNotInteractableException.class, ErrorCode.UI_ELEM_NOT_ACTIVE);
MAPPING.put(ElementClickInterceptedException.class, ErrorCode.UI_CLICK_INTERCEPTED);
MAPPING.put(SessionNotCreatedException.class, ErrorCode.UI_SESSION_CREATE_FAIL);
// 是否可重试(默认不可重试)
RETRYABLE.put(NoSuchElementException.class, true);
RETRYABLE.put(StaleElementReferenceException.class, true);
RETRYABLE.put(UnhandledAlertException.class, true);
RETRYABLE.put(UnreachableBrowserException.class, true);
RETRYABLE.put(InvalidSessionIdException.class, true);
RETRYABLE.put(ElementNotInteractableException.class, true);
RETRYABLE.put(ElementClickInterceptedException.class, true);
// 其他默认不可重试
}
/**
* 根据异常获取错误码
*/
public static ErrorCode getErrorCode(WebDriverException e) {
Class<?> clazz = e.getClass();
for (Map.Entry<Class<? extends WebDriverException>, ErrorCode> entry : MAPPING.entrySet()) {
if (entry.getKey().isAssignableFrom(clazz)) {
return entry.getValue();
}
}
return ErrorCode.UI_UNKNOWN_ERROR;
}
/**
* 判断该异常是否支持重试
*/
public static boolean isRetryable(WebDriverException e) {
Class<?> clazz = e.getClass();
for (Map.Entry<Class<? extends WebDriverException>, Boolean> entry : RETRYABLE.entrySet()) {
if (entry.getKey().isAssignableFrom(clazz)) {
return entry.getValue();
}
}
return false; // 默认不可重试
}
}
```
---
### 4. 重试处理器
```java
package com.qualitymesh.framework.core.interceptor;
import lombok.extern.slf4j.Slf4j;
import org.openqa.selenium.WebDriverException;
import java.util.function.Supplier;
/**
* 智能重试处理器:指数退避 + 次数限制
*/
@Slf4j
public class RetryHandler {
// 可配置参数(可通过配置文件注入)
private static final int MAX_RETRY_COUNT = 3;
private static final long MAX_TOTAL_TIME_MS = 60000; // 60s
private static final long BASE_DELAY_MS = 1000; // 1s
/**
* 执行带重试的WebDriver操作
* @param action 业务操作(Supplier包裹)
* @param retryableException 当前捕获的异常(用于判断是否可重试)
* @param operationName 操作名称(用于日志)
* @return 操作结果
* @throws QualityMeshException 重试耗尽后抛出
*/
public static <T> T executeWithRetry(Supplier<T> action,
WebDriverException retryableException,
String operationName) {
// 如果异常本身不可重试,直接抛出
if (!ExceptionMapper.isRetryable(retryableException)) {
log.warn("异常不可重试,直接失败: {}", retryableException.getMessage());
throw retryableException;
}
int retryCount = 0;
long startTime = System.currentTimeMillis();
Throwable lastException = retryableException;
while (retryCount < MAX_RETRY_COUNT) {
try {
// 检查是否超时
if (System.currentTimeMillis() - startTime > MAX_TOTAL_TIME_MS) {
log.error("重试总耗时超过{}ms,放弃重试", MAX_TOTAL_TIME_MS);
break;
}
// 执行操作
return action.get();
} catch (WebDriverException e) {
lastException = e;
retryCount++;
long elapsed = System.currentTimeMillis() - startTime;
long delay = calculateDelay(retryCount);
log.warn("操作 [{}] 执行失败,第{}次重试,等待{}ms,已耗时{}ms, 异常: {}",
operationName, retryCount, delay, elapsed, e.getMessage());
// 如果是不可重试的异常,立即抛出
if (!ExceptionMapper.isRetryable(e)) {
log.warn("重试过程中遇到不可重试异常: {}", e.getClass().getSimpleName());
throw e;
}
// 等待退避时间
try {
Thread.sleep(delay);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException("重试被中断", ie);
}
}
}
// 重试耗尽,包装为中台异常
ErrorCode errorCode = ExceptionMapper.getErrorCode((WebDriverException) lastException);
throw new QualityMeshException(
errorCode,
String.format("操作 [%s] 重试 %d 次后仍然失败", operationName, MAX_RETRY_COUNT),
lastException,
null // context由调用方传入
);
}
/**
* 指数退避计算:1s, 2s, 4s
*/
private static long calculateDelay(int retryCount) {
return BASE_DELAY_MS * (1L << (retryCount - 1));
}
}
```
---
### 5. 全局拦截器(核心)
```java
package com.qualitymesh.framework.core.interceptor;
import com.qualitymesh.framework.core.exception.ErrorCode;
import com.qualitymesh.framework.core.exception.QualityMeshException;
import lombok.extern.slf4j.Slf4j;
import org.openqa.selenium.*;
import org.openqa.selenium.support.ui.ExpectedCondition;
import org.openqa.selenium.support.ui.WebDriverWait;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
/**
* WebDriver动态代理拦截器(核心)
* 拦截所有WebDriver方法调用,统一处理异常
*/
@Slf4j
public class WebDriverInterceptor implements InvocationHandler {
private final WebDriver originalDriver;
private final String taskId;
private final String caseId;
private final ArtifactStorage artifactStorage;
private WebDriverInterceptor(WebDriver driver, String taskId, String caseId,
ArtifactStorage artifactStorage) {
this.originalDriver = driver;
this.taskId = taskId;
this.caseId = caseId;
this.artifactStorage = artifactStorage;
}
/**
* 创建被代理的WebDriver实例(对外暴露)
*/
public static WebDriver createProxy(WebDriver driver, String taskId, String caseId,
ArtifactStorage artifactStorage) {
return (WebDriver) Proxy.newProxyInstance(
driver.getClass().getClassLoader(),
driver.getClass().getInterfaces(),
new WebDriverInterceptor(driver, taskId, caseId, artifactStorage)
);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
// 跳过不需要拦截的方法(如quit、getWindowHandle等)
if (shouldSkip(methodName)) {
return method.invoke(originalDriver, args);
}
try {
// 尝试执行原始方法
return method.invoke(originalDriver, args);
} catch (Exception e) {
// 解包反射异常
Throwable cause = e.getCause();
if (cause instanceof WebDriverException) {
WebDriverException webEx = (WebDriverException) cause;
// 构建现场上下文
Map<String, Object> context = buildCrashContext(webEx, methodName, args);
// 判断是否可重试
if (ExceptionMapper.isRetryable(webEx)) {
// 执行重试逻辑
try {
return RetryHandler.executeWithRetry(
() -> {
try {
return method.invoke(originalDriver, args);
} catch (Exception ex) {
throw (WebDriverException) ex.getCause();
}
},
webEx,
methodName
);
} catch (QualityMeshException qme) {
// 重试失败,补充上下文并重新抛出
qme.getContext().putAll(context);
// 保存现场产物
saveArtifacts(originalDriver);
throw qme;
}
}
// 不可重试异常,映射错误码,保存现场后抛出
ErrorCode errorCode = ExceptionMapper.getErrorCode(webEx);
QualityMeshException qualityEx = new QualityMeshException(
errorCode,
String.format("WebDriver操作 [%s] 失败: %s", methodName, webEx.getMessage()),
webEx,
context
);
// 保存现场产物
saveArtifacts(originalDriver);
log.error(qualityEx.toJson());
throw qualityEx;
}
// 非WebDriver异常,直接抛出
throw cause != null ? cause : e;
}
}
/**
* 构建异常上下文
*/
private Map<String, Object> buildCrashContext(WebDriverException e, String methodName, Object[] args) {
Map<String, Object> context = new HashMap<>();
context.put("errorCode", ExceptionMapper.getErrorCode(e).getCode());
context.put("operation", methodName);
context.put("args", args != null ? args.toString() : "null");
context.put("timestamp", System.currentTimeMillis());
context.put("taskId", taskId);
context.put("caseId", caseId);
try {
context.put("currentUrl", originalDriver.getCurrentUrl());
context.put("pageTitle", originalDriver.getTitle());
context.put("windowHandle", originalDriver.getWindowHandle());
} catch (Exception ignore) {
// 如果获取页面信息失败,忽略
}
return context;
}
/**
* 保存现场产物(截图、HTML源码、Console日志)
*/
private void saveArtifacts(WebDriver driver) {
try {
// 截图
if (driver instanceof TakesScreenshot) {
File screenshot = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
String path = artifactStorage.saveScreenshot(screenshot, caseId);
log.info("截图已保存: {}", path);
}
// HTML源码
String pageSource = driver.getPageSource();
String htmlPath = artifactStorage.savePageSource(pageSource, caseId);
log.info("HTML源码已保存: {}", htmlPath);
// Console日志(Chrome/Edge)
if (driver instanceof JavascriptExecutor) {
// 通过CDP获取Console日志需要额外实现,此处留接口
// 可扩展LoggingPreferences
}
} catch (Exception ex) {
log.error("保存现场产物失败: {}", ex.getMessage(), ex);
}
}
/**
* 跳过不需要拦截的方法
*/
private boolean shouldSkip(String methodName) {
return "quit".equals(methodName) ||
"toString".equals(methodName) ||
"hashCode".equals(methodName) ||
"equals".equals(methodName) ||
"getClass".equals(methodName) ||
"notify".equals(methodName) ||
"notifyAll".equals(methodName) ||
"wait".equals(methodName);
}
}
```
---
### 6. 产物存储服务
```java
package com.qualitymesh.framework.core.context;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* 产物存储服务:保存截图、HTML、日志等
*/
@Slf4j
public class ArtifactStorage {
private final String basePath;
public ArtifactStorage(String basePath) {
this.basePath = basePath;
ensureDirectories();
}
private void ensureDirectories() {
new File(basePath + "/screenshots").mkdirs();
new File(basePath + "/html").mkdirs();
new File(basePath + "/logs").mkdirs();
new File(basePath + "/locators").mkdirs();
}
/**
* 保存截图
*/
public String saveScreenshot(File screenshot, String caseId) {
String timestamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
String fileName = String.format("%s_%s_screenshot.png", caseId, timestamp);
String targetPath = basePath + "/screenshots/" + fileName;
try {
FileUtils.copyFile(screenshot, new File(targetPath));
return targetPath;
} catch (IOException e) {
log.error("保存截图失败: {}", e.getMessage());
return null;
}
}
/**
* 保存HTML源码
*/
public String savePageSource(String pageSource, String caseId) {
String timestamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
String fileName = String.format("%s_%s_pageSource.html", caseId, timestamp);
String targetPath = basePath + "/html/" + fileName;
try {
FileUtils.writeStringToFile(new File(targetPath), pageSource, StandardCharsets.UTF_8);
return targetPath;
} catch (IOException e) {
log.error("保存HTML源码失败: {}", e.getMessage());
return null;
}
}
/**
* 保存Console日志
*/
public String saveConsoleLog(String logContent, String caseId) {
String timestamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
String fileName = String.format("%s_%s_console.log", caseId, timestamp);
String targetPath = basePath + "/logs/" + fileName;
try {
FileUtils.writeStringToFile(new File(targetPath), logContent, StandardCharsets.UTF_8);
return targetPath;
} catch (IOException e) {
log.error("保存Console日志失败: {}", e.getMessage());
return null;
}
}
}
```
---
### 7. 页面基类(封装WebDriver操作)
```java
package com.qualitymesh.framework.page;
import com.qualitymesh.framework.core.interceptor.WebDriverInterceptor;
import com.qualitymesh.framework.core.context.ArtifactStorage;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.support.PageFactory;
import org.openqa.selenium.support.ui.WebDriverWait;
import java.time.Duration;
/**
* 页面基类:所有PageObject继承此类
* 使用代理WebDriver,自动拦截异常
*/
public abstract class BasePage {
protected WebDriver driver;
protected WebDriverWait wait;
protected String taskId;
protected String caseId;
public BasePage(WebDriver driver, String taskId, String caseId, ArtifactStorage storage) {
// 使用代理包装WebDriver
this.driver = WebDriverInterceptor.createProxy(driver, taskId, caseId, storage);
this.taskId = taskId;
this.caseId = caseId;
this.wait = new WebDriverWait(this.driver, Duration.ofSeconds(15));
PageFactory.initElements(this.driver, this);
}
/**
* 封装点击操作(自动触发拦截器)
*/
public void click(org.openqa.selenium.By by) {
driver.findElement(by).click();
}
/**
* 封装输入操作
*/
public void input(org.openqa.selenium.By by, String text) {
driver.findElement(by).sendKeys(text);
}
/**
* 封装获取文本
*/
public String getText(org.openqa.selenium.By by) {
return driver.findElement(by).getText();
}
/**
* 获取代理后的driver(供子类特殊场景使用)
*/
protected WebDriver getDriver() {
return driver;
}
}
```
---
### 8. 测试基类
```java
package com.qualitymesh.framework.test;
import com.qualitymesh.framework.core.context.ArtifactStorage;
import com.qualitymesh.framework.core.driver.DriverFactory;
import com.qualitymesh.framework.core.listener.TestNGListener;
import lombok.extern.slf4j.Slf4j;
import org.openqa.selenium.WebDriver;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Listeners;
/**
* 测试基类:所有测试用例继承此类
*/
@Slf4j
@Listeners(TestNGListener.class)
public abstract class BaseTest {
protected WebDriver driver;
protected ArtifactStorage artifactStorage;
protected String taskId;
protected String caseId;
@BeforeClass
public void setUp() {
// 初始化产物存储
this.artifactStorage = new ArtifactStorage("./target/artifacts");
this.taskId = System.getProperty("taskId", "TASK_DEFAULT");
this.caseId = this.getClass().getSimpleName();
// 创建WebDriver(使用代理)
this.driver = DriverFactory.createDriver(
"chrome",
taskId,
caseId,
artifactStorage,
true // headless模式
);
log.info("测试 [{}] 初始化完成,Driver已创建", caseId);
}
@AfterClass
public void tearDown() {
if (driver != null) {
try {
driver.quit();
log.info("测试 [{}] Driver已销毁", caseId);
} catch (Exception e) {
log.error("Driver销毁失败: {}", e.getMessage());
}
}
}
}
```
---
### 9. Driver工厂
```java
package com.qualitymesh.framework.core.driver;
import com.qualitymesh.framework.core.context.ArtifactStorage;
import com.qualitymesh.framework.core.interceptor.WebDriverInterceptor;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.edge.EdgeDriver;
import org.openqa.selenium.edge.EdgeOptions;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.firefox.FirefoxOptions;
/**
* WebDriver工厂:创建带代理的Driver实例
*/
public class DriverFactory {
public static WebDriver createDriver(String browserType, String taskId, String caseId,
ArtifactStorage storage, boolean headless) {
WebDriver driver;
switch (browserType.toLowerCase()) {
case "chrome":
ChromeOptions chromeOptions = new ChromeOptions();
if (headless) {
chromeOptions.addArguments("--headless");
}
chromeOptions.addArguments("--no-sandbox");
chromeOptions.addArguments("--disable-dev-shm-usage");
chromeOptions.addArguments("--disable-gpu");
chromeOptions.addArguments("--window-size=1920,1080");
driver = new ChromeDriver(chromeOptions);
break;
case "firefox":
FirefoxOptions firefoxOptions = new FirefoxOptions();
if (headless) {
firefoxOptions.addArguments("--headless");
}
driver = new FirefoxDriver(firefoxOptions);
break;
case "edge":
EdgeOptions edgeOptions = new EdgeOptions();
if (headless) {
edgeOptions.addArguments("--headless");
}
driver = new EdgeDriver(edgeOptions);
break;
default:
throw new IllegalArgumentException("不支持的浏览器类型: " + browserType);
}
// 返回代理后的Driver
return WebDriverInterceptor.createProxy(driver, taskId, caseId, storage);
}
}
```
---
### 10. TestNG监听器(异常兜底)
```java
package com.qualitymesh.framework.core.listener;
import com.qualitymesh.framework.core.exception.QualityMeshException;
import lombok.extern.slf4j.Slf4j;
import org.testng.ITestContext;
import org.testng.ITestListener;
import org.testng.ITestResult;
/**
* TestNG监听器:测试失败时的全局兜底处理
*/
@Slf4j
public class TestNGListener implements ITestListener {
@Override
public void onTestFailure(ITestResult result) {
Throwable throwable = result.getThrowable();
if (throwable instanceof QualityMeshException) {
QualityMeshException qme = (QualityMeshException) throwable;
// 输出结构化日志
log.error("测试失败 - 错误码: {}, 错误标签: {}, 上下文: {}",
qme.getErrorCode().getCode(),
qme.getErrorLabel(),
qme.getContext());
} else {
log.error("测试失败 - 未捕获异常: {}", throwable.getMessage(), throwable);
}
// 此处可扩展:上报中台、发送告警等
}
@Override
public void onTestSuccess(ITestResult result) {
log.info("测试通过: {}", result.getName());
}
@Override
public void onTestStart(ITestResult result) {
log.info("开始执行测试: {}", result.getName());
}
// 其他方法省略...
}
```
---
### 11. 示例测试用例
```java
package com.qualitymesh.demo;
import com.qualitymesh.framework.page.BasePage;
import com.qualitymesh.framework.test.BaseTest;
import org.openqa.selenium.By;
import org.testng.Assert;
import org.testng.annotations.Test;
public class DemoTest extends BaseTest {
@Test
public void testLoginSuccess() {
// 使用BasePage的内置方法,自动走拦截器
LoginPage loginPage = new LoginPage(driver, taskId, caseId, artifactStorage);
loginPage.open("https://example.com/login");
loginPage.inputUsername("admin");
loginPage.inputPassword("password123");
loginPage.clickLogin();
Assert.assertTrue(loginPage.isLoginSuccess(), "登录应该成功");
}
// 内部Page类(实际项目应独立文件)
class LoginPage extends BasePage {
private By usernameInput = By.id("username");
private By passwordInput = By.id("password");
private By loginBtn = By.xpath("//button[@type='submit']");
private By successMsg = By.cssSelector(".success-message");
public LoginPage(WebDriver driver, String taskId, String caseId, ArtifactStorage storage) {
super(driver, taskId, caseId, storage);
}
public void open(String url) {
driver.get(url);
}
public void inputUsername(String username) {
input(usernameInput, username); // 调用父类封装方法
}
public void inputPassword(String password) {
input(passwordInput, password);
}
public void clickLogin() {
click(loginBtn); // 调用父类封装方法,自动触发拦截器
}
public boolean isLoginSuccess() {
return driver.findElement(successMsg).isDisplayed();
}
}
}
```
---
### 12. Maven依赖(pom.xml)
```xml
<?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>com.qualitymesh</groupId>
<artifactId>quality-mesh-framework</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<selenium.version>4.16.1</selenium.version>
<testng.version>7.9.0</testng.version>
<lombok.version>1.18.30</lombok.version>
<slf4j.version>2.0.9</slf4j.version>
<logback.version>1.4.14</logback.version>
<commons-io.version>2.15.1</commons-io.version>
<gson.version>2.10.1</gson.version>
</properties>
<dependencies>
<!-- Selenium -->
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>${selenium.version}</version>
</dependency>
<!-- TestNG -->
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>${testng.version}</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
</dependency>
<!-- Commons IO -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>${commons-io.version}</version>
</dependency>
<!-- Gson -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>${gson.version}</version>
</dependency>
<!-- WebDriverManager (自动管理驱动) -->
<dependency>
<groupId>io.github.bonigarcia</groupId>
<artifactId>webdrivermanager</artifactId>
<version>5.6.3</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>11</source>
<target>11</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
<configuration>
<suiteXmlFiles>
<suiteXmlFile>testng.xml</suiteXmlFile>
</suiteXmlFiles>
</configuration>
</plugin>
</plugins>
</build>
</project>
```
---
### 运行方式
```bash
# 执行测试,可传入taskId等参数
mvn clean test -DtaskId=TASK_20260803_001 -Dbrowser=chrome
# 产物输出位置
./target/artifacts/
├── screenshots/ # 失败截图
├── html/ # 失败时HTML源码
├── logs/ # Console日志
└── locators/ # 定位上下文JSON
```
---
### 核心设计要点总结
| **设计模式** | **应用位置** |
|:---|:---|
| **动态代理模式** | `WebDriverInterceptor` 代理WebDriver,拦截所有方法调用 |
| **策略模式** | `ExceptionMapper` 映射不同异常的处理策略 |
| **模板方法模式** | `BasePage` / `BaseTest` 定义执行模板 |
| **责任链模式** | 异常处理链:拦截器 → 重试处理器 → 异常映射 → 现场保存 |
| **工厂模式** | `DriverFactory` 创建各类浏览器Driver |
---
这套代码框架可以直接作为质量中台Selenium执行引擎的底层实现,完全兼容您之前的需求文档。如需**JMeter执行引擎**的代码框架或**任务调度器**的实现,请告知,我可以继续为您生成。

321

被折叠的 条评论
为什么被折叠?



