08.显示等待的自定义条件

Java+Selenium自动化测试 – 显示等待的自定义条件详解

目录

  1. 什么是自定义等待条件
  2. ExpectedCondition接口详解
  3. 创建自定义等待条件的方法
  4. 常见自定义等待条件实例
  5. 高级自定义等待条件
  6. 自定义条件的最佳实践
  7. 性能优化和调试技巧
  8. 常见问题和解决方案

什么是自定义等待条件

概念

自定义等待条件是当Selenium内置的ExpectedConditions无法满足特定需求时,开发者自己编写的等待逻辑。它允许我们定义复杂的、业务相关的等待条件。

为什么需要自定义等待条件?

虽然Selenium提供了丰富的内置等待条件,但在实际项目中,我们经常遇到以下场景:

  • 复杂的业务逻辑:等待特定的业务状态完成
  • 多条件组合:需要同时满足多个条件
  • 动态内容:等待动态生成的内容达到特定状态
  • 第三方组件:等待第三方插件或组件加载完成
  • 性能监控:等待页面性能指标达到预期

内置条件 vs 自定义条件对比

特性 内置ExpectedConditions 自定义等待条件
使用难度 简单 中等到复杂
灵活性 有限 完全可控
适用场景 通用场景 特定业务场景
维护成本 中等
性能控制 标准 可优化

ExpectedCondition接口详解

接口结构

public interface ExpectedCondition<T> extends Function<WebDriver, T> {
    T apply(WebDriver driver);
}

核心概念

  • 泛型T:等待条件返回的数据类型
  • apply方法:核心逻辑,返回null表示条件未满足,返回非null表示条件满足
  • Function接口:继承自Java 8的Function接口,支持Lambda表达式

返回值类型说明

// Boolean类型:条件是否满足
ExpectedCondition<Boolean> booleanCondition = driver -> {
    // 返回true表示条件满足,false表示不满足
    return someCondition;
};

// WebElement类型:返回找到的元素
ExpectedCondition<WebElement> elementCondition = driver -> {
    // 返回元素表示找到,null表示未找到
    return driver.findElement(By.id("myId"));
};

// String类型:返回特定的字符串值
ExpectedCondition<String> stringCondition = driver -> {
    // 返回字符串表示条件满足,null表示不满足
    return driver.getTitle().contains("期望标题") ? driver.getTitle() : null;
};

创建自定义等待条件的方法

方法1:匿名内部类(传统方式)

import org.openqa.selenium.support.ui.ExpectedCondition;
import org.openqa.selenium.WebDriver;

public class AnonymousClassExample {

    public void customWaitWithAnonymousClass() {
        WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));

        // 等待页面标题包含特定文本
        WebElement element = wait.until(new ExpectedCondition<WebElement>() {
            @Override
            public WebElement apply(WebDriver driver) {
                try {
                    WebElement elem = driver.findElement(By.id("myElement"));
                    if (elem.isDisplayed() && elem.isEnabled()) {
                        return elem;
                    }
                    return null;
                } catch (NoSuchElementException e) {
                    return null;
                }
            }

            @Override
            public String toString() {
                return "element to be visible and enabled";
            }
        });
    }
}

方法2:Lambda表达式(推荐)

public class LambdaExpressionExample {

    public void customWaitWithLambda() {
        WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));

        // 等待元素可见且可点击
        WebElement element = wait.until(driver -> {
            try {
                WebElement elem = driver.findElement(By.id("myElement"));
                return (elem.isDisplayed() && elem.isEnabled()) ? elem : null;
            } catch (NoSuchElementException e) {
                return null;
            }
        });

        // 等待页面加载完成
        wait.until(driver -> {
            JavascriptExecutor js = (JavascriptExecutor) driver;
            return js.executeScript("return document.readyState").equals("complete");
        });

        // 等待Ajax请求完成
        wait.until(driver -> {
            JavascriptExecutor js = (JavascriptExecutor) driver;
            return (Boolean) js.executeScript("return jQuery.active == 0");
        });
    }
}

方法3:静态方法封装(最佳实践)

public class CustomExpectedConditions {

    // 等待元素可见且包含特定文本
    public static ExpectedCondition<WebElement> elementToBeVisibleWithText(
            final By locator, final String expectedText) {
        return new ExpectedCondition<WebElement>() {
            @Override
            public WebElement apply(WebDriver driver) {
                try {
                    WebElement element = driver.findElement(locator);
                    if (element.isDisplayed() && element.getText().contains(expectedText)) {
                        return element;
                    }
                    return null;
                } catch (Exception e) {
                    return null;
                }
            }

            @Override
            public String toString() {
                return String.format("element located by %s to be visible with text '%s'", 
                                   locator, expectedText);
            }
        };
    }

    // 使用示例
    public void useCustomCondition() {
        WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
        WebElement element = wait.until(
            CustomExpectedConditions.elementToBeVisibleWithText(
                By.id("status"), "加载完成"
            )
        );
    }
}

常见自定义等待条件实例

1. 等待元素属性值变化

public class AttributeWaitConditions {

    // 等待元素的class属性包含特定值
    public static ExpectedCondition<Boolean> attributeToContain(
            final By locator, final String attribute, final String value) {
        return driver -> {
            try {
                WebElement element = driver.findElement(locator);
                String attributeValue = element.getAttribute(attribute);
                return attributeValue != null && attributeValue.contains(value);
            } catch (Exception e) {
                return false;
            }
        };
    }

    // 等待元素的style属性变化
    public static ExpectedCondition<Boolean> elementStyleToContain(
            final By locator, final String styleProperty, final String expectedValue) {
        return driver -> {
            try {
                WebElement element = driver.findElement(locator);
                String styleValue = element.getCssValue(styleProperty);
                return styleValue != null && styleValue.contains(expectedValue);
            } catch (Exception e) {
                return false;
            }
        };
    }

    // 使用示例
    public void waitForAttributeChanges() {
        WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));

        // 等待按钮的class包含"active"
        wait.until(attributeToContain(By.id("myButton"), "class", "active"));

        // 等待元素的display样式变为block
        wait.until(elementStyleToContain(By.id("myDiv"), "display", "block"));
    }
}

2. 等待文本内容变化

public class TextWaitConditions {

    // 等待元素文本不等于指定值
    public static ExpectedCondition<Boolean> textToBeNotEqual(
            final By locator, final String text) {
        return driver -> {
            try {
                WebElement element = driver.findElement(locator);
                return !element.getText().equals(text);
            } catch (Exception e) {
                return false;
            }
        };
    }

    // 等待元素文本长度达到指定值
    public static ExpectedCondition<Boolean> textLengthToBe(
            final By locator, final int expectedLength) {
        return driver -> {
            try {
                WebElement element = driver.findElement(locator);
                return element.getText().length() >= expectedLength;
            } catch (Exception e) {
                return false;
            }
        };
    }

    // 等待元素文本匹配正则表达式
    public static ExpectedCondition<Boolean> textToMatchPattern(
            final By locator, final String pattern) {
        return driver -> {
            try {
                WebElement element = driver.findElement(locator);
                return element.getText().matches(pattern);
            } catch (Exception e) {
                return false;
            }
        };
    }

    // 使用示例
    public void waitForTextChanges() {
        WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));

        // 等待状态文本不再是"加载中..."
        wait.until(textToBeNotEqual(By.id("status"), "加载中..."));

        // 等待描述文本长度至少50个字符
        wait.until(textLengthToBe(By.id("description"), 50));

        // 等待电话号码格式正确
        wait.until(textToMatchPattern(By.id("phone"), "\d{3}-\d{4}-\d{4}"));
    }
}

3. 等待元素数量变化

public class ElementCountConditions {

    // 等待元素数量等于指定值
    public static ExpectedCondition<Boolean> numberOfElementsToBe(
            final By locator, final int expectedCount) {
        return driver -> {
            try {
                List<WebElement> elements = driver.findElements(locator);
                return elements.size() == expectedCount;
            } catch (Exception e) {
                return false;
            }
        };
    }

    // 等待元素数量大于指定值
    public static ExpectedCondition<Boolean> numberOfElementsToBeMoreThan(
            final By locator, final int expectedCount) {
        return driver -> {
            try {
                List<WebElement> elements = driver.findElements(locator);
                return elements.size() > expectedCount;
            } catch (Exception e) {
                return false;
            }
        };
    }

    // 等待列表中所有元素都可见
    public static ExpectedCondition<Boolean> allElementsToBeVisible(final By locator) {
        return driver -> {
            try {
                List<WebElement> elements = driver.findElements(locator);
                if (elements.isEmpty()) return false;

                for (WebElement element : elements) {
                    if (!element.isDisplayed()) {
                        return false;
                    }
                }
                return true;
            } catch (Exception e) {
                return false;
            }
        };
    }

    // 使用示例
    public void waitForElementCounts() {
        WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(15));

        // 等待表格有5行数据
        wait.until(numberOfElementsToBe(By.xpath("//table//tr"), 6)); // 包含表头

        // 等待搜索结果至少有10个
        wait.until(numberOfElementsToBeMoreThan(By.className("search-result"), 9));

        // 等待所有菜单项都可见
        wait.until(allElementsToBeVisible(By.className("menu-item")));
    }
}

4. 等待JavaScript执行完成

public class JavaScriptWaitConditions {

    // 等待页面完全加载
    public static ExpectedCondition<Boolean> pageToBeFullyLoaded() {
        return driver -> {
            JavascriptExecutor js = (JavascriptExecutor) driver;
            return js.executeScript("return document.readyState").equals("complete");
        };
    }

    // 等待jQuery加载完成
    public static ExpectedCondition<Boolean> jQueryToBeLoaded() {
        return driver -> {
            try {
                JavascriptExecutor js = (JavascriptExecutor) driver;
                return (Boolean) js.executeScript("return typeof jQuery !== 'undefined'");
            } catch (Exception e) {
                return false;
            }
        };
    }

    // 等待jQuery Ajax请求完成
    public static ExpectedCondition<Boolean> jQueryAjaxToBeComplete() {
        return driver -> {
            try {
                JavascriptExecutor js = (JavascriptExecutor) driver;
                return (Boolean) js.executeScript("return jQuery.active == 0");
            } catch (Exception e) {
                return false;
            }
        };
    }

    // 等待Angular应用加载完成
    public static ExpectedCondition<Boolean> angularToBeReady() {
        return driver -> {
            try {
                JavascriptExecutor js = (JavascriptExecutor) driver;
                return (Boolean) js.executeScript(
                    "return window.angular && " +
                    "angular.element(document).injector() && " +
                    "angular.element(document).injector().get('$http').pendingRequests.length === 0"
                );
            } catch (Exception e) {
                return false;
            }
        };
    }

    // 等待自定义JavaScript条件
    public static ExpectedCondition<Boolean> customJavaScriptCondition(final String script) {
        return driver -> {
            try {
                JavascriptExecutor js = (JavascriptExecutor) driver;
                Object result = js.executeScript(script);
                return result instanceof Boolean ? (Boolean) result : false;
            } catch (Exception e) {
                return false;
            }
        };
    }

    // 使用示例
    public void waitForJavaScriptConditions() {
        WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(30));

        // 等待页面完全加载
        wait.until(pageToBeFullyLoaded());

        // 等待jQuery加载
        wait.until(jQueryToBeLoaded());

        // 等待所有Ajax请求完成
        wait.until(jQueryAjaxToBeComplete());

        // 等待自定义条件:购物车数量大于0
        wait.until(customJavaScriptCondition(
            "return document.getElementById('cart-count').innerText > 0"
        ));
    }
}

高级自定义等待条件

1. 复合条件等待

public class CompositeWaitConditions {

    // 等待多个条件同时满足(AND逻辑)
    public static ExpectedCondition<Boolean> allConditionsToBeTrue(
            final ExpectedCondition<?>... conditions) {
        return driver -> {
            for (ExpectedCondition<?> condition : conditions) {
                try {
                    Object result = condition.apply(driver);
                    if (result == null || (result instanceof Boolean && !(Boolean) result)) {
                        return false;
                    }
                } catch (Exception e) {
                    return false;
                }
            }
            return true;
        };
    }

    // 等待任一条件满足(OR逻辑)
    public static ExpectedCondition<Boolean> anyConditionToBeTrue(
            final ExpectedCondition<?>... conditions) {
        return driver -> {
            for (ExpectedCondition<?> condition : conditions) {
                try {
                    Object result = condition.apply(driver);
                    if (result != null && (!(result instanceof Boolean) || (Boolean) result)) {
                        return true;
                    }
                } catch (Exception e) {
                    // 继续检查下一个条件
                }
            }
            return false;
        };
    }

    // 使用示例
    public void waitForCompositeConditions() {
        WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(20));

        // 等待页面加载完成且特定元素可见
        wait.until(allConditionsToBeTrue(
            JavaScriptWaitConditions.pageToBeFullyLoaded(),
            ExpectedConditions.visibilityOfElementLocated(By.id("content"))
        ));

        // 等待成功或错误消息出现
        wait.until(anyConditionToBeTrue(
            ExpectedConditions.visibilityOfElementLocated(By.className("success-message")),
            ExpectedConditions.visibilityOfElementLocated(By.className("error-message"))
        ));
    }
}

2. 带超时重试的等待条件

public class RetryWaitConditions {

    // 带重试机制的等待条件
    public static <T> ExpectedCondition<T> withRetry(
            final ExpectedCondition<T> condition, final int maxRetries) {
        return driver -> {
            Exception lastException = null;
            for (int i = 0; i <= maxRetries; i++) {
                try {
                    T result = condition.apply(driver);
                    if (result != null) {
                        return result;
                    }
                } catch (Exception e) {
                    lastException = e;
                    if (i < maxRetries) {
                        try {
                            Thread.sleep(500); // 重试间隔
                        } catch (InterruptedException ie) {
                            Thread.currentThread().interrupt();
                            break;
                        }
                    }
                }
            }
            if (lastException != null) {
                throw new RuntimeException("条件在重试 " + maxRetries + " 次后仍未满足", lastException);
            }
            return null;
        };
    }

    // 使用示例
    public void waitWithRetry() {
        WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(30));

        // 带重试的元素查找
        WebElement element = wait.until(withRetry(
            ExpectedConditions.elementToBeClickable(By.id("unstable-button")), 
            3 // 最多重试3次
        ));
    }
}

3. 性能监控等待条件

public class PerformanceWaitConditions {

    // 等待页面加载时间小于指定值
    public static ExpectedCondition<Boolean> pageLoadTimeToBeUnder(final long maxLoadTimeMs) {
        return driver -> {
            JavascriptExecutor js = (JavascriptExecutor) driver;
            try {
                Long loadTime = (Long) js.executeScript(
                    "return performance.timing.loadEventEnd - performance.timing.navigationStart;"
                );
                return loadTime != null && loadTime > 0 && loadTime < maxLoadTimeMs;
            } catch (Exception e) {
                return false;
            }
        };
    }

    // 等待网络请求完成
    public static ExpectedCondition<Boolean> networkRequestsToBeComplete() {
        return driver -> {
            JavascriptExecutor js = (JavascriptExecutor) driver;
            try {
                // 检查是否有正在进行的fetch请求
                Boolean fetchComplete = (Boolean) js.executeScript(
                    "return window.fetch === undefined || " +
                    "!window.hasOwnProperty('activeFetchRequests') || " +
                    "window.activeFetchRequests === 0"
                );

                // 检查XMLHttpRequest
                Boolean xhrComplete = (Boolean) js.executeScript(
                    "return !window.XMLHttpRequest || " +
                    "window.XMLHttpRequest.OPENED === undefined || " +
                    "document.readyState === 'complete'"
                );

                return fetchComplete && xhrComplete;
            } catch (Exception e) {
                return false;
            }
        };
    }

    // 使用示例
    public void waitForPerformanceConditions() {
        WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(30));

        // 等待页面加载时间小于3秒
        wait.until(pageLoadTimeToBeUnder(3000));

        // 等待所有网络请求完成
        wait.until(networkRequestsToBeComplete());
    }
}

自定义条件的最佳实践

1. 异常处理和容错性

public class RobustWaitConditions {

    // 健壮的元素等待条件
    public static ExpectedCondition<WebElement> robustElementToBeClickable(final By locator) {
        return new ExpectedCondition<WebElement>() {
            @Override
            public WebElement apply(WebDriver driver) {
                try {
                    WebElement element = driver.findElement(locator);

                    // 检查元素是否存在
                    if (element == null) {
                        return null;
                    }

                    // 检查元素是否可见
                    if (!element.isDisplayed()) {
                        return null;
                    }

                    // 检查元素是否启用
                    if (!element.isEnabled()) {
                        return null;
                    }

                    // 检查元素是否被其他元素遮挡
                    try {
                        element.click();
                        return element;
                    } catch (ElementClickInterceptedException e) {
                        return null;
                    }

                } catch (NoSuchElementException | StaleElementReferenceException e) {
                    return null;
                } catch (Exception e) {
                    // 记录意外异常但不中断等待
                    System.err.println("等待元素时发生意外异常: " + e.getMessage());
                    return null;
                }
            }

            @Override
            public String toString() {
                return "element located by " + locator + " to be clickable (robust check)";
            }
        };
    }
}

2. 可配置的等待条件

public class ConfigurableWaitConditions {

    public static class WaitConfig {
        private boolean checkVisibility = true;
        private boolean checkEnabled = true;
        private boolean checkClickable = false;
        private String expectedText = null;
        private String expectedAttribute = null;
        private String expectedAttributeValue = null;

        // Getter和Setter方法
        public WaitConfig checkVisibility(boolean check) {
            this.checkVisibility = check;
            return this;
        }

        public WaitConfig checkEnabled(boolean check) {
            this.checkEnabled = check;
            return this;
        }

        public WaitConfig checkClickable(boolean check) {
            this.checkClickable = check;
            return this;
        }

        public WaitConfig expectedText(String text) {
            this.expectedText = text;
            return this;
        }

        public WaitConfig expectedAttribute(String attribute, String value) {
            this.expectedAttribute = attribute;
            this.expectedAttributeValue = value;
            return this;
        }

        // Getter方法
        public boolean isCheckVisibility() { return checkVisibility; }
        public boolean isCheckEnabled() { return checkEnabled; }
        public boolean isCheckClickable() { return checkClickable; }
        public String getExpectedText() { return expectedText; }
        public String getExpectedAttribute() { return expectedAttribute; }
        public String getExpectedAttributeValue() { return expectedAttributeValue; }
    }

    // 可配置的元素等待条件
    public static ExpectedCondition<WebElement> configurableElementCondition(
            final By locator, final WaitConfig config) {
        return driver -> {
            try {
                WebElement element = driver.findElement(locator);

                // 检查可见性
                if (config.isCheckVisibility() && !element.isDisplayed()) {
                    return null;
                }

                // 检查是否启用
                if (config.isCheckEnabled() && !element.isEnabled()) {
                    return null;
                }

                // 检查文本
                if (config.getExpectedText() != null && 
                    !element.getText().contains(config.getExpectedText())) {
                    return null;
                }

                // 检查属性
                if (config.getExpectedAttribute() != null) {
                    String attrValue = element.getAttribute(config.getExpectedAttribute());
                    if (attrValue == null || !attrValue.equals(config.getExpectedAttributeValue())) {
                        return null;
                    }
                }

                // 检查可点击性
                if (config.isCheckClickable()) {
                    try {
                        element.click();
                    } catch (ElementClickInterceptedException e) {
                        return null;
                    }
                }

                return element;

            } catch (Exception e) {
                return null;
            }
        };
    }

    // 使用示例
    public void useConfigurableCondition() {
        WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));

        // 配置等待条件
        WaitConfig config = new WaitConfig()
            .checkVisibility(true)
            .checkEnabled(true)
            .expectedText("提交")
            .expectedAttribute("class", "btn-primary");

        WebElement element = wait.until(
            configurableElementCondition(By.id("submit-btn"), config)
        );
    }
}

3. 日志和调试支持

public class DebuggableWaitConditions {

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

    // 带调试信息的等待条件
    public static ExpectedCondition<WebElement> debuggableElementWait(
            final By locator, final String conditionName) {
        return new ExpectedCondition<WebElement>() {
            private int attemptCount = 0;

            @Override
            public WebElement apply(WebDriver driver) {
                attemptCount++;
                logger.debug("等待条件 '{}' 第 {} 次检查", conditionName, attemptCount);

                try {
                    WebElement element = driver.findElement(locator);

                    if (!element.isDisplayed()) {
                        logger.debug("元素存在但不可见: {}", locator);
                        return null;
                    }

                    if (!element.isEnabled()) {
                        logger.debug("元素可见但不可用: {}", locator);
                        return null;
                    }

                    logger.debug("等待条件 '{}' 在第 {} 次检查时满足", conditionName, attemptCount);
                    return element;

                } catch (NoSuchElementException e) {
                    logger.debug("元素未找到: {} (第 {} 次检查)", locator, attemptCount);
                    return null;
                } catch (Exception e) {
                    logger.warn("等待条件检查时发生异常: {}", e.getMessage());
                    return null;
                }
            }

            @Override
            public String toString() {
                return String.format("debuggable condition '%s' for element %s (attempts: %d)", 
                                   conditionName, locator, attemptCount);
            }
        };
    }

    // 使用示例
    public void useDebuggableCondition() {
        WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));

        WebElement element = wait.until(
            debuggableElementWait(By.id("complex-element"), "复杂元素可点击检查")
        );
    }
}

性能优化和调试技巧

1. 轮询间隔优化

public class PerformanceOptimizedWaits {

    // 自适应轮询间隔
    public static ExpectedCondition<WebElement> adaptivePollingElementWait(final By locator) {
        return new ExpectedCondition<WebElement>() {
            private int checkCount = 0;
            private long startTime = System.currentTimeMillis();

            @Override
            public WebElement apply(WebDriver driver) {
                checkCount++;
                long elapsed = System.currentTimeMillis() - startTime;

                try {
                    WebElement element = driver.findElement(locator);
                    if (element.isDisplayed() && element.isEnabled()) {
                        return element;
                    }

                    // 根据已等待时间调整检查频率
                    if (elapsed < 2000) {
                        // 前2秒快速检查
                        Thread.sleep(100);
                    } else if (elapsed < 5000) {
                        // 2-5秒中等频率
                        Thread.sleep(300);
                    } else {
                        // 5秒后降低频率
                        Thread.sleep(500);
                    }

                    return null;

                } catch (Exception e) {
                    return null;
                }
            }

            @Override
            public String toString() {
                return String.format("adaptive polling for %s (checks: %d)", locator, checkCount);
            }
        };
    }
}

2. 条件缓存优化

public class CachedWaitConditions {

    private static final Map<String, Object> conditionCache = new ConcurrentHashMap<>();

    // 带缓存的JavaScript条件
    public static ExpectedCondition<Boolean> cachedJavaScriptCondition(
            final String script, final String cacheKey, final long cacheTimeMs) {
        return driver -> {
            String fullCacheKey = cacheKey + "_" + script.hashCode();
            CacheEntry entry = (CacheEntry) conditionCache.get(fullCacheKey);

            // 检查缓存
            if (entry != null && (System.currentTimeMillis() - entry.timestamp) < cacheTimeMs) {
                return entry.result;
            }

            // 执行脚本
            try {
                JavascriptExecutor js = (JavascriptExecutor) driver;
                Boolean result = (Boolean) js.executeScript(script);

                // 更新缓存
                conditionCache.put(fullCacheKey, new CacheEntry(result, System.currentTimeMillis()));

                return result;
            } catch (Exception e) {
                return false;
            }
        };
    }

    private static class CacheEntry {
        final Boolean result;
        final long timestamp;

        CacheEntry(Boolean result, long timestamp) {
            this.result = result;
            this.timestamp = timestamp;
        }
    }

    // 清理缓存
    public static void clearCache() {
        conditionCache.clear();
    }
}

3. 性能监控

public class PerformanceMonitoredWaits {

    // 带性能监控的等待条件
    public static <T> ExpectedCondition<T> monitoredCondition(
            final ExpectedCondition<T> condition, final String conditionName) {
        return new ExpectedCondition<T>() {
            private long startTime = System.currentTimeMillis();
            private int checkCount = 0;

            @Override
            public T apply(WebDriver driver) {
                checkCount++;
                long checkStartTime = System.currentTimeMillis();

                try {
                    T result = condition.apply(driver);
                    long checkDuration = System.currentTimeMillis() - checkStartTime;

                    if (result != null) {
                        long totalDuration = System.currentTimeMillis() - startTime;
                        logger.info("等待条件 '{}' 完成: 总时间={}ms, 检查次数={}, 平均检查时间={}ms", 
                                  conditionName, totalDuration, checkCount, 
                                  checkCount > 0 ? totalDuration / checkCount : 0);
                    }

                    return result;
                } catch (Exception e) {
                    long checkDuration = System.currentTimeMillis() - checkStartTime;
                    logger.debug("等待条件 '{}' 第{}次检查异常: {}ms", conditionName, checkCount, checkDuration);
                    throw e;
                }
            }

            @Override
            public String toString() {
                return String.format("monitored condition '%s'", conditionName);
            }
        };
    }
}

常见问题和解决方案

1. 内存泄漏问题

public class MemoryEfficientWaits {

    // 避免内存泄漏的等待条件
    public static ExpectedCondition<WebElement> memoryEfficientElementWait(final By locator) {
        return driver -> {
            try {
                // 使用局部变量,避免持有不必要的引用
                WebElement element = driver.findElement(locator);

                // 立即检查条件,不保存中间状态
                boolean isReady = element.isDisplayed() && element.isEnabled();

                // 清理可能的大对象引用
                if (!isReady) {
                    element = null; // 帮助GC
                    return null;
                }

                return element;

            } catch (Exception e) {
                // 不保存异常对象,避免内存泄漏
                return null;
            }
        };
    }
}

2. 线程安全问题

public class ThreadSafeWaitConditions {

    private static final ThreadLocal<Map<String, Object>> threadLocalCache = 
        ThreadLocal.withInitial(HashMap::new);

    // 线程安全的缓存等待条件
    public static ExpectedCondition<Boolean> threadSafeCachedCondition(
            final String script, final String cacheKey) {
        return driver -> {
            Map<String, Object> cache = threadLocalCache.get();

            // 检查线程本地缓存
            Boolean cachedResult = (Boolean) cache.get(cacheKey);
            if (cachedResult != null) {
                return cachedResult;
            }

            try {
                JavascriptExecutor js = (JavascriptExecutor) driver;
                Boolean result = (Boolean) js.executeScript(script);

                // 存储到线程本地缓存
                cache.put(cacheKey, result);

                return result;
            } catch (Exception e) {
                return false;
            }
        };
    }

    // 清理线程本地缓存
    public static void clearThreadLocalCache() {
        threadLocalCache.get().clear();
    }
}

3. 超时处理优化

public class TimeoutOptimizedWaits {

    // 分阶段超时等待
    public static ExpectedCondition<WebElement> phasedTimeoutElementWait(final By locator) {
        return new ExpectedCondition<WebElement>() {
            private long startTime = System.currentTimeMillis();

            @Override
            public WebElement apply(WebDriver driver) {
                long elapsed = System.currentTimeMillis() - startTime;

                try {
                    // 第一阶段:快速查找(前3秒)
                    if (elapsed < 3000) {
                        WebElement element = driver.findElement(locator);
                        if (element.isDisplayed()) {
                            return element;
                        }
                        return null;
                    }

                    // 第二阶段:等待可见(3-8秒)
                    if (elapsed < 8000) {
                        WebElement element = driver.findElement(locator);
                        if (element.isDisplayed() && element.isEnabled()) {
                            return element;
                        }
                        return null;
                    }

                    // 第三阶段:强制等待(8秒后)
                    WebElement element = driver.findElement(locator);
                    // 即使不完全满足条件也返回,让调用者决定
                    return element.isDisplayed() ? element : null;

                } catch (Exception e) {
                    // 根据阶段决定是否继续等待
                    return elapsed > 10000 ? null : null;
                }
            }

            @Override
            public String toString() {
                long elapsed = System.currentTimeMillis() - startTime;
                return String.format("phased timeout wait for %s (elapsed: %dms)", locator, elapsed);
            }
        };
    }
}

4. 调试和故障排除

public class DebuggingWaitConditions {

    // 详细调试信息的等待条件
    public static ExpectedCondition<WebElement> debugElementWait(final By locator) {
        return new ExpectedCondition<WebElement>() {
            private final List<String> debugLog = new ArrayList<>();

            @Override
            public WebElement apply(WebDriver driver) {
                try {
                    debugLog.add("开始查找元素: " + locator);

                    WebElement element = driver.findElement(locator);
                    debugLog.add("元素已找到");

                    // 检查元素状态
                    boolean isDisplayed = element.isDisplayed();
                    boolean isEnabled = element.isEnabled();
                    String tagName = element.getTagName();
                    String text = element.getText();

                    debugLog.add(String.format("元素状态: displayed=%s, enabled=%s, tag=%s, text='%s'", 
                               isDisplayed, isEnabled, tagName, text));

                    if (isDisplayed && isEnabled) {
                        debugLog.add("元素满足条件");
                        return element;
                    } else {
                        debugLog.add("元素不满足条件");
                        return null;
                    }

                } catch (NoSuchElementException e) {
                    debugLog.add("元素未找到: " + e.getMessage());
                    return null;
                } catch (Exception e) {
                    debugLog.add("发生异常: " + e.getClass().getSimpleName() + " - " + e.getMessage());
                    return null;
                }
            }

            @Override
            public String toString() {
                StringBuilder sb = new StringBuilder();
                sb.append("Debug wait for ").append(locator).append("n");
                for (String log : debugLog) {
                    sb.append("  ").append(log).append("n");
                }
                return sb.toString();
            }
        };
    }
}

总结

关键要点

  1. 选择合适的实现方式:Lambda表达式简洁,静态方法封装便于复用
  2. 异常处理:始终处理可能的异常,返回null而不是抛出异常
  3. 性能考虑:合理设置轮询间隔,避免过度检查
  4. 可维护性:提供清晰的toString()方法,便于调试
  5. 线程安全:在多线程环境中注意线程安全问题

最佳实践总结

public class CustomWaitBestPractices {

    // 推荐的自定义等待条件模板
    public static ExpectedCondition<WebElement> bestPracticeElementWait(final By locator) {
        return new ExpectedCondition<WebElement>() {
            @Override
            public WebElement apply(WebDriver driver) {
                try {
                    WebElement element = driver.findElement(locator);

                    // 检查所有必要条件
                    if (element.isDisplayed() && element.isEnabled()) {
                        return element;
                    }

                    return null; // 条件不满足时返回null

                } catch (NoSuchElementException | StaleElementReferenceException e) {
                    return null; // 预期异常,返回null继续等待
                } catch (Exception e) {
                    // 记录意外异常但不中断等待
                    logger.warn("等待元素时发生意外异常: {}", e.getMessage());
                    return null;
                }
            }

            @Override
            public String toString() {
                return "element located by " + locator + " to be visible and enabled";
            }
        };
    }
}

使用建议

  1. 优先使用内置条件:如果内置的ExpectedConditions能满足需求,优先使用
  2. 封装常用条件:将项目中常用的自定义条件封装成工具方法
  3. 添加日志支持:在复杂的等待条件中添加适当的日志
  4. 性能测试:对自定义等待条件进行性能测试,确保不会影响测试执行速度
  5. 文档化:为自定义等待条件编写清晰的文档和使用示例

通过掌握自定义等待条件的编写技巧,您可以处理各种复杂的等待场景,编写更加稳定和高效的Selenium自动化测试脚本!

发表评论