WebDriver异常捕获,放到framework底层

好的,我直接为您生成**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执行引擎**的代码框架或**任务调度器**的实现,请告知,我可以继续为您生成。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值