You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何扩展Selenium的FindsBy类实现元素递归查找(未找到时)

嘿,这个需求太常见了——不稳定的网站简直是Selenium自动化的噩梦,尤其是用页面对象模式的时候,原生的FindsBy根本扛不住偶尔的元素加载延迟。我给你一套完整的扩展方案,通过自定义属性+重试逻辑来解决这个问题:

自定义带重试功能的FindsBy扩展

第一步:创建自定义重试属性

先定义一个继承自FindsByAttribute的新属性,加上重试次数和间隔的配置参数,这样就能在注解里灵活指定重试规则:

using OpenQA.Selenium.Support.PageObjects;
using System;

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)]
public class RetryFindsByAttribute : FindsByAttribute
{
    // 默认重试3次
    public int RetryCount { get; set; } = 3;
    // 默认每次重试间隔500毫秒
    public int RetryIntervalMs { get; set; } = 500;
}

第二步:实现带重试的元素初始化逻辑

接下来需要重写页面对象的元素初始化逻辑,把重试逻辑嵌入到元素查找过程中。这里我们自定义一个RetryPageFactory工具类,替代原生的PageFactory:

using OpenQA.Selenium;
using OpenQA.Selenium.Support.PageObjects;
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading;

public static class RetryPageFactory
{
    public static void InitElements(ISearchContext driver, object page)
    {
        // 处理私有字段
        var fields = page.GetType().GetFields(BindingFlags.Instance | BindingFlags.NonPublic);
        foreach (var field in fields)
        {
            ProcessMemberWithRetry(driver, page, field);
        }

        // 处理属性
        var properties = page.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
        foreach (var prop in properties)
        {
            if (prop.CanWrite)
            {
                ProcessMemberWithRetry(driver, page, prop);
            }
        }
    }

    private static void ProcessMemberWithRetry(ISearchContext driver, object page, MemberInfo member)
    {
        var retryAttr = member.GetCustomAttribute<RetryFindsByAttribute>();
        if (retryAttr == null)
        {
            // 没有重试属性的成员,用原生初始化逻辑
            PageFactory.InitElements(driver, page);
            return;
        }

        var by = retryAttr.ToBy();
        Type memberType = member is FieldInfo field ? field.FieldType : ((PropertyInfo)member).PropertyType;

        if (typeof(IWebElement).IsAssignableFrom(memberType))
        {
            var element = RetryFindElement(driver, by, retryAttr.RetryCount, retryAttr.RetryIntervalMs);
            SetMemberValue(page, member, element);
        }
        else if (typeof(IList<IWebElement>).IsAssignableFrom(memberType))
        {
            var elements = RetryFindElements(driver, by, retryAttr.RetryCount, retryAttr.RetryIntervalMs);
            SetMemberValue(page, member, elements);
        }
    }

    private static IWebElement RetryFindElement(ISearchContext context, By by, int retryCount, int intervalMs)
    {
        WebDriverException lastException = null;
        for (int i = 0; i <= retryCount; i++)
        {
            try
            {
                return context.FindElement(by);
            }
            catch (NoSuchElementException ex)
            {
                lastException = ex;
                if (i < retryCount) Thread.Sleep(intervalMs);
            }
        }
        throw lastException;
    }

    private static IList<IWebElement> RetryFindElements(ISearchContext context, By by, int retryCount, int intervalMs)
    {
        for (int i = 0; i <= retryCount; i++)
        {
            var elements = context.FindElements(by);
            if (elements.Count > 0) return elements;
            if (i < retryCount) Thread.Sleep(intervalMs);
        }
        return new List<IWebElement>(); // 也可以选择抛出异常,根据测试需求调整
    }

    private static void SetMemberValue(object page, MemberInfo member, object value)
    {
        if (member is FieldInfo field)
        {
            field.SetValue(page, value);
        }
        else if (member is PropertyInfo prop)
        {
            prop.SetValue(page, value);
        }
    }
}

第三步:在页面对象中使用自定义属性

现在你就可以用RetryFindsBy替代原生的FindsBy,还能根据元素的稳定性自定义重试次数和间隔:

public class YourSettingsPage
{
    // 示例:针对不稳定的按钮,设置重试5次,每次间隔1秒
    [RetryFindsBy(How = How.CssSelector, Using = "li.config-btn button[class *= 'configure icon-settings']", RetryCount = 5, RetryIntervalMs = 1000)]
    private IList<IWebElement> SettingsButton;

    public YourSettingsPage(IWebDriver driver)
    {
        // 用自定义工厂初始化页面元素
        RetryPageFactory.InitElements(driver, this);
    }

    // 你的页面操作方法...
    public void ClickFirstSettingsButton()
    {
        SettingsButton.First().Click();
    }
}

额外优化建议

  • 可以把重试次数和间隔做成全局配置文件,不用每个元素单独设置;
  • 替换Thread.Sleep为WebDriverWait的忽略异常机制,更灵活高效:
    private static IWebElement RetryFindElement(ISearchContext context, By by, int retryCount, int intervalMs)
    {
        var wait = new WebDriverWait((IWebDriver)context, TimeSpan.FromMilliseconds(intervalMs * retryCount));
        wait.IgnoreExceptionTypes(typeof(NoSuchElementException));
        return wait.Until(d => d.FindElement(by));
    }
    
  • 添加日志记录,把每次重试的情况输出,方便调试不稳定的元素。

内容的提问来源于stack exchange,提问作者Shobhit Maheshwari

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.22 07:38:09