如何扩展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
相关产品推荐
相关产品推荐

