Building a Rock-Solid BasePage in Selenium: 5 Smart Techniques for Reliable Test Automation

Amir EbrahimianAmir Ebrahimian
8 min read

"If you’ve ever struggled with flaky Selenium tests — clicks not working, sendKeys failing, or elements just refusing to load — you know how painful it can be. The good news? A well-designed BasePage can make all the difference."

1. Handling sendKeys Failures with a JavaScript Fallback

One of the most common frustrations in Selenium is when sendKeys just doesn’t work.
This can happen when:

  • The field is hidden behind overlays.

  • The app uses custom input components (React, Angular, etc.).

  • The browser blocks the keystroke simulation for some reason.

Instead of manually switching to JavaScript every time, we can make our BasePage smart enough to try sendKeys first — and if it fails, automatically fall back to JS injection.

Example Implementation:

public void type(By locator, String text) {
        WebElement element = find(locator);
        try {
            WebElement element = wait.until(ExpectedConditions.visibilityOfElementLocated(locator));
            element.clear();
            element.sendKeys(text);
        } catch (Exception e) {
            // Fallback to JavaScript if sendKeys fails
            JavascriptExecutor jsExecutor = (JavascriptExecutor) driver;
            jsExecutor.executeScript(
                    "const element = arguments[0];" +
                            "const value = arguments[1];" +
                            "const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;" +
                            "nativeInputValueSetter.call(element, value);" +
                            "element.dispatchEvent(new Event('input', { bubbles: true }));" +
                            "element.dispatchEvent(new Event('change', { bubbles: true }));" +
                            "element.dispatchEvent(new Event('blur', { bubbles: true }));",
                    element, text
            );
        }
    }

👉 With this approach, you don’t need to think about switching strategies — the BasePage takes care of it.

2. Safe Clicking with Waits and JS Backup

  • Clicking is another classic pain point in Selenium.
    You might see errors like:

    • ElementClickInterceptedException

    • ElementNotInteractableException

    • Or the button just doesn’t respond at all.

To fix this, we wrap all clicks inside a safeClick method that:

  1. Waits until the element is clickable.

  2. Tries the standard .click().

If it fails, falls back to JavaScript click.

Example Implementation:

public void safeClick(By locator) {
    try {
        WebElement element = wait.until(ExpectedConditions.elementToBeClickable(locator));
        element.click();
    } catch (Exception e) {
        // Fallback to JavaScript click
        WebElement element = driver.findElement(locator);
        JavascriptExecutor js = (JavascriptExecutor) driver;
        js.executeScript("arguments[0].click();", element);
    }
}

👉 Now your tests are much less flaky because clicks will always be handled one way or another.

3. Smart Waits: No More Hardcoded Sleeps

One of the biggest anti-patterns in Selenium is using Thread.sleep().
It slows tests down unnecessarily and still doesn’t guarantee stability.

Instead, we can build a smartWait utility in our BasePage:

  • Waits for conditions instead of fixed timeouts.

  • Can be reused across all tests.

  • Supports custom conditions (e.g., Angular finished loading, React idle state, etc.).

Example Implementation:

public WebElement waitForVisibility(By locator, int timeoutInSeconds) {
    WebDriverWait customWait = new WebDriverWait(driver, Duration.ofSeconds(timeoutInSeconds));
    return customWait.until(ExpectedConditions.visibilityOfElementLocated(locator));
}

public boolean waitForText(By locator, String text, int timeoutInSeconds) {
    WebDriverWait customWait = new WebDriverWait(driver, Duration.ofSeconds(timeoutInSeconds));
    return customWait.until(ExpectedConditions.textToBePresentInElementLocated(locator, text));
}

👉 This makes tests faster and more reliable because the script only waits as long as it really needs.

4. Centralized Error Handling & Logging

Debugging flaky tests is painful when all you see is NoSuchElementException.
Instead, we can create a centralized error handling & logging system:

  1. Capture a screenshot whenever an error occurs.

  2. Print useful debugging info (which locator failed, what action was attempted).

  3. Store logs in a structured way (could be console logs, a file, or even Allure reports).

Example Implementation:

public void logFailure(String action, By locator, Exception e) {
    System.err.println(" Failed to " + action + " on element: " + locator.toString());
    System.err.println("Reason: " + e.getMessage());
    takeScreenshot("Failure_" + System.currentTimeMillis());
}

public void takeScreenshot(String fileName) {
    File srcFile = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
    try {
        FileUtils.copyFile(srcFile, new File("screenshots/" + fileName + ".png"));
    } catch (IOException io) {
        io.printStackTrace();
    }
}

👉 With this setup, when something fails you don’t just see an exception — you get:

  • The exact action that failed

  • The locator used

  • A screenshot of the page at failure

  • This massively reduces debugging time.

5. A Complete BasePage with Reusable Actions for DRY Automation

Repeating actions like login steps or form submissions across multiple Selenium tests leads to bloated, hard-to-maintain code. A well-designed BasePage class can centralize common actions, reduce duplication, and make your test suite more reliable and maintainable. By combining smart waits, error handling, and reusable methods, this BasePage becomes the backbone of your automation framework, embodying the DRY (Don’t Repeat Yourself) principle.

Why It Matters:

  • Eliminates repetitive code for actions like clicking, typing, or waiting for elements.

  • Promotes consistency across page classes, reducing errors.

  • Simplifies maintenance—update one method, and all tests benefit.

Example Implementation:

The following BasePage class provides reusable methods like click, type, and isDisplayed, which page-specific classes (e.g., LoginPage or SearchPage) can inherit to create concise, reusable actions.

[See the code above in the <xaiArtifact> tag]

How It Works:

  • Smart Waits: The waitForElement method adjusts timeouts dynamically (e.g., 30s for spinners, 15s for alerts, 10s for others), eliminating brittle Thread.sleep() calls.

  • Reusable Methods: Methods like click, type, and getText are defined once in BasePage and reused across all page classes, reducing code duplication.

  • Error Handling: The isDisplayed method gracefully handles TimeoutException, ensuring tests don’t fail unnecessarily.

  • Page-Specific Actions: Classes like LoginPage and SearchPage use BasePage methods to create higher-level actions (e.g., login or searchProduct) that encapsulate complex workflows.

Usage Example: With this BasePage, you can create clean page classes like these:

  • login("user", "pass") logs in a user in one line instead of repeating steps.

  • searchProduct("Laptop") performs a search without duplicating code.

  • loginWithInvalidCredentials checks for error messages, reusing isDisplayed for reliability.

Benefits:

  • Reduces code duplication

  • Improves readability

  • Easier to maintain (change in one place applies everywhere)

This follows the DRY principle: Don’t Repeat Yourself.

public class BasePage {
    protected WebDriver driver;
    protected WebDriverWait wait;

    public BasePage(WebDriver driver) {
        this.driver = driver;
        this.wait = new WebDriverWait(driver, Duration.ofSeconds(10)); // default wait
    }

    // ---------- Smart Wait ----------
    protected WebElement waitForElement(By locator) {
        String loc = locator.toString().toLowerCase();
        int timeout;

        if (loc.contains("spinner") || loc.contains("loader")) {
            timeout = 30;
        } else if (loc.contains("toast") || loc.contains("alert")) {
            timeout = 15;
        } else {
            timeout = 10;
        }

        return new WebDriverWait(driver, Duration.ofSeconds(timeout))
                .until(ExpectedConditions.visibilityOfElementLocated(locator));
    }

    // ---------- SendKeys with JS Fallback ----------
    public void type(By locator, String text) {
        try {
            WebElement element = wait.until(ExpectedConditions.visibilityOfElementLocated(locator));
            element.clear();
            element.sendKeys(text);
        } catch (Exception e) {
            JavascriptExecutor jsExecutor = (JavascriptExecutor) driver;
            WebElement element = driver.findElement(locator);
            jsExecutor.executeScript(
                    "const element = arguments[0];" +
                            "const value = arguments[1];" +
                            "const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;" +
                            "nativeInputValueSetter.call(element, value);" +
                            "element.dispatchEvent(new Event('input', { bubbles: true }));" +
                            "element.dispatchEvent(new Event('change', { bubbles: true }));" +
                            "element.dispatchEvent(new Event('blur', { bubbles: true }));",
                    element, text
            );
        }
    }

    // ---------- Safe Click with JS Fallback ----------
    public void safeClick(By locator) {
        try {
            WebElement element = wait.until(ExpectedConditions.elementToBeClickable(locator));
            element.click();
        } catch (Exception e) {
            WebElement element = driver.findElement(locator);
            JavascriptExecutor js = (JavascriptExecutor) driver;
            js.executeScript("arguments[0].click();", element);
        }
    }

    // ---------- Custom Waits ----------
    public WebElement waitForVisibility(By locator, int timeoutInSeconds) {
        WebDriverWait customWait = new WebDriverWait(driver, Duration.ofSeconds(timeoutInSeconds));
        return customWait.until(ExpectedConditions.visibilityOfElementLocated(locator));
    }

    public boolean waitForText(By locator, String text, int timeoutInSeconds) {
        WebDriverWait customWait = new WebDriverWait(driver, Duration.ofSeconds(timeoutInSeconds));
        return customWait.until(ExpectedConditions.textToBePresentInElementLocated(locator, text));
    }

    // ---------- Logging & Screenshot ----------
    public void logFailure(String action, By locator, Exception e) {
        System.err.println("❌ Failed to " + action + " on element: " + locator.toString());
        System.err.println("Reason: " + e.getMessage());
        takeScreenshot("Failure_" + System.currentTimeMillis());
    }

    public void takeScreenshot(String fileName) {
        File srcFile = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
        try {
            FileUtils.copyFile(srcFile, new File("screenshots/" + fileName + ".png"));
        } catch (IOException io) {
            io.printStackTrace();
        }
    }

    // ---------- Helper Getters ----------
    public String getText(By locator) {
        return waitForElement(locator).getText();
    }

    public boolean isDisplayed(By locator) {
        try {
            return waitForElement(locator).isDisplayed();
        } catch (TimeoutException e) {
            return false;
        }
    }
}

Usage Example: With this BasePage, you can create clean page classes like these:

  • login("user", "pass") logs in a user in one line instead of repeating steps.

  • searchProduct("Laptop") performs a search without duplicating code.

  • loginWithInvalidCredentials checks for error messages, reusing isDisplayed for reliability.

class LoginPage extends BasePage {
    private final By usernameField = By.id("username");
    private final By passwordField = By.id("password");
    private final By loginButton = By.id("loginBtn");
    private final By errorMessage = By.className("error-message");

    public LoginPage(WebDriver driver) {
        super(driver);
    }

    public void login(String username, String password) {
        try {
            type(usernameField, username);
            type(passwordField, password);
            safeClick(loginButton);
        } catch (Exception e) {
            logFailure("login", loginButton, e);
        }
    }

    public boolean loginWithInvalidCredentials(String username, String password) {
        try {
            type(usernameField, username);
            type(passwordField, password);
            safeClick(loginButton);
            return isDisplayed(errorMessage);
        } catch (Exception e) {
            logFailure("loginWithInvalidCredentials", errorMessage, e);
            return false;
        }
    }
}

class SearchPage extends BasePage {
    private final By searchBox = By.name("searchBox");
    private final By searchButton = By.cssSelector("button.search");

    public SearchPage(WebDriver driver) {
        super(driver);
    }

    public void searchProduct(String productName) {
        try {
            type(searchBox, productName);
            safeClick(searchButton);
        } catch (Exception e) {
            logFailure("searchProduct", searchButton, e);
        }
    }
}

👉 This BasePage ensures your tests are concise, reliable, and easy to maintain. By centralizing common actions and following the DRY principle, you can focus on writing test scenarios instead of managing repetitive code. Try it in your next project to see how it streamlines your automation workflow!

Benefits:

  • Reduced Duplication: Common actions are defined once and reused everywhere.

  • Improved Readability: Page classes are concise and focused on page-specific logic.

  • Easier Maintenance: Update a method in BasePage, and all page classes inherit the change.

  • Scalability: Works for small projects and scales to large test suites with minimal overhead.

Final Note

"These are just a few examples of how you can make your Selenium tests smarter and more reliable. There are countless other improvements you can add — from advanced logging, retry mechanisms, smart waits, to full-blown reporting systems. The key is to start small, automate what wastes your time the most, and keep evolving your BasePage over time."

0
Subscribe to my newsletter

Read articles from Amir Ebrahimian directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Amir Ebrahimian
Amir Ebrahimian

Software Quality Assurance professional with 4+ years of experience in ensuring software reliability and performance