0

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.

2
  • you might just use a class that stores locators, and a few methods you can call to return the elements using the webdriverwaits.... or to perform certain actions. Commented Dec 3, 2024 at 20:29
  • Some people will just set an implicit wait. (it's global so if you can get away with it and you like the POM structure it can work in simple cases...) Commented Dec 3, 2024 at 20:37

1 Answer 1

3

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");
        }
    }
}
Sign up to request clarification or add additional context in comments.

2 Comments

I've gotten into the habit of returning a page model representing the next page or screen I expect the user to be navigated to. For example, the Login(username, password) method would return an instance of SecurePage. This enables method-chaining, and prevents initializing an object just to call one method on the same line. It would become new LoginPage(...).Login(...).Logout();
@GregBurghardt I'm not a big fan of chaining. I think it's easily abused making tests much harder to read and debug. And in intentional error cases where you need to validate an error instead of progressing through the expected flow, you end up having to write a bunch of new methods... one for the successful path and one for each of the error paths. I think the way I do it is much simpler, easier to read, and easier to debug.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.