Search code examples
c#selenium-webdriverautomated-testswebdriverwait

Selenium Automation with C# POM Elements vs Element Methods


So I have been playing around with Selenium's POM (Page Object Model) specifically the FindsBy annotation.

[FindsBy(How = How.XPath, Using = "//a[@attribute='some_value']")]
public IWebElement ElementDescription;

But I ran into troubles needing to wait for objects and having to call the WebDriverWait function with the same locator used in the FindsBy annotation:

WebDriverWait wait = new WebDriverWait(driver, TimeSpan.FromSeconds(30));
wait.Until(SeleniumExtras.WaitHelpers.ExpectedConditions.ElementToBeClickable(By.XPath("//a[@attribute='some_value']")));

This is not very maintainable as you would then have to update multiple locators is the object's locator changes.

My question is, would a Method for each element work better if the method itself waits for the object before using it ?

 public IWebElement ElementDescription(int seconds = 30)
 {
     const string locator = "//a[@attribute='some_value']";

     // Wait for the element to be visible before returning it
     WebDriverWait wait = new WebDriverWait(driver, TimeSpan.FromSeconds(seconds));
     IWebElement element = wait.Until(SeleniumExtras.WaitHelpers.ExpectedConditions.ElementToBeClickable(By.XPath(locator)));


     // Return the element
     return element;
 }

Many of the elements I work with are dynamically produced depending on different choices you make in the app, so I have to wait for data to load or elements to be visible.


Solution

  • You're confusing PageFactory with the Page Object Model (POM). FindsBy, etc. are part of PageFactory. POM is not a part of Selenium, it's a way to organize and structure classes that represent pages (or partial pages) of a website.

    The Selenium lead and devs recommend that you NOT use PageFactory, see this video. On the other hand, it is highly recommended to use POM.

    The issue you bring up about PageFactory is one of the main reasons I stopped using it years ago before I learned that the Selenium team discouraged its use.

    The way I solved this problem is by creating a BasePage class that holds a bunch of convenience wrappers around common Selenium methods like .FindElement(), .Click(), .SendKeys(), etc.

    Here's my BasePage with one sample method, .Click(). You can see that it takes in a locator and timeout, if desired. It handles waiting for clickable, etc.

    BasePage.cs

    using OpenQA.Selenium;
    using OpenQA.Selenium.Support.UI;
    using ExpectedConditions = SeleniumExtras.WaitHelpers.ExpectedConditions;
    
    namespace SeleniumFramework.PageObjects
    {
        public class BasePage
        {
            protected IWebDriver Driver;
    
            public BasePage(IWebDriver driver)
            {
                Driver = driver;
            }
    
            /// <summary>
            /// Clicks the element.
            /// </summary>
            /// <param name="locator">The By locator for the desired element.</param>
            /// <param name="timeOutSeconds">[Optional] How long to wait for the element. The default is 10.</param>
            public void Click(By locator, int timeOutSeconds = 10)
            {
                DateTime expire = DateTime.Now.AddSeconds(timeOutSeconds);
                while (DateTime.Now < expire)
                {
                    try
                    {
                        new WebDriverWait(Driver, TimeSpan.FromSeconds(timeOutSeconds)).Until(ExpectedConditions.ElementToBeClickable(locator)).Click();
    
                        return;
                    }
                    catch (Exception e) when (e is ElementClickInterceptedException || e is StaleElementReferenceException)
                    {
                        // do nothing, loop again
                    }
                }
    
                throw new Exception($"Not able to click element <{locator}> within {timeOutSeconds}s.");
            }
        }
    }
    

    Here's a sample LoginPage page object that shows the use of BasePage methods.

    LoginPage.cs

    using OpenQA.Selenium;
    
    namespace SeleniumFramework.PageObjects.TheInternet
    {
        class LoginPage : BasePage
        {
            private readonly By _loginButtonLocator = By.CssSelector("button");
            private readonly By _passwordLocator = By.Id("password");
            private readonly By _usernameLocator = By.Id("username");
    
            public LoginPage(IWebDriver driver) : base(driver)
            {
            }
    
            /// <summary>
            /// Logs in with the provided username and password.
            /// </summary>
            /// <param name="username">The username.</param>
            /// <param name="password">The password.</param>
            public void Login(string username, string password)
            {
                SendKeys(_usernameLocator, username);
                SendKeys(_passwordLocator, password);
                Click(_loginButtonLocator);
            }
        }
    }
    

    Finally, here's a sample test that uses LoginPage.

    using SeleniumFramework.PageObjects.TheInternet;
    
    namespace SeleniumFramework.Tests.TheInternet
    {
        public class LoginTest : BaseTest
        {
            [Test]
            [Category("Login")]
            public void Login()
            {
                string username = TestData["username"];
                string password = TestData["password"];
    
                new LoginPage(Driver.Value!).Login(username, password);
    
                new SecurePage(Driver.Value!).Logout();
    
                Assert.That(Driver.Value!.Url, Is.EqualTo(Url), "Verify login URL");
            }
        }
    }