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");
}
}
}