Clicking, Typing, and Form Interactions

8 min read

You can find an element. Now do something with it. This lesson covers the methods on WebElement that every Selenium test calls dozens of times: click, sendKeys, clear, getText, getAttribute, isDisplayed, isEnabled, isSelected. Each takes one line. Each has its own gotcha. By the end you'll know how to fill a real-world login form, read state from the page for assertions, and recognise the three exceptions Selenium throws when something goes sideways during interaction.

The interaction methods you'll use every test

When you call driver.findElement(...) you get back a WebElement. That object has a small, focused API:

WebElement emailInput = driver.findElement(By.id("email"));
WebElement passwordInput = driver.findElement(By.id("password"));
WebElement submitButton = driver.findElement(By.cssSelector("[data-testid='submit']"));
 
// Type text — appends to whatever is already in the field
emailInput.sendKeys("alice@test.com");
 
// Clear, then type — best practice when the field may have a value already
emailInput.clear();
emailInput.sendKeys("new-email@test.com");
 
// Click
submitButton.click();
 
// Submit a form (alternative — finds the surrounding <form> and submits it)
emailInput.submit();

A few things worth pinning down on day one:

  • sendKeys appends. If the input already contains "alice", calling sendKeys("@test.com") produces "alice@test.com". Use clear() first if you want to overwrite.
  • click() waits for the click event to fire but doesn't wait for whatever the click triggers. If the click loads a new page, you still need explicit waits before interacting with the next page (chapter 3).
  • submit() walks up to the surrounding <form> and submits it. Useful in a pinch when you don't want to find the submit button explicitly.

Special keys via the Keys enum

sendKeys accepts the Keys enum for non-printing characters:

import org.openqa.selenium.Keys;
 
emailInput.sendKeys(Keys.CONTROL, "a");          // select all
emailInput.sendKeys(Keys.BACK_SPACE);             // delete the selection
emailInput.sendKeys("alice@new.com", Keys.TAB);   // type, then tab to next field
emailInput.sendKeys(Keys.ENTER);                   // submit by pressing Enter
 
// Chord — Ctrl+A then Delete
emailInput.sendKeys(Keys.chord(Keys.CONTROL, "a"));
emailInput.sendKeys(Keys.DELETE);

Keys.chord(...) is how you express "hold these keys together." It's the most common way to do "select all then delete" if the field doesn't respond to a plain clear().

Reading state from the page

Just as critical as typing is reading. Every assertion in your suite ends with one of these:

// Visible text — what the user sees
String text = element.getText();
 
// Input value — what's currently typed
String value = element.getAttribute("value");
 
// Other useful attributes
String href = element.getAttribute("href");
String className = element.getAttribute("class");
String dataTestId = element.getAttribute("data-testid");
 
// Boolean state
boolean visible = element.isDisplayed();
boolean enabled = element.isEnabled();
boolean selected = element.isSelected();   // for checkboxes/radios/options
 
// Geometry
Dimension size = element.getSize();
Point location = element.getLocation();

Three of these warrant special attention:

  • getText() returns the rendered text — what's actually visible on screen. CSS display: none elements return an empty string, even if the underlying HTML has text. To read the raw HTML text regardless of CSS, use getAttribute("textContent").
  • getAttribute("value") is how you read what's currently typed in an input. getText() on an <input> returns an empty string — inputs don't have visible text in the DOM sense.
  • isSelected() only makes sense for checkboxes, radios, and <option> elements. Calling it on a button returns false and tells you nothing useful.

A complete login flow

Putting it together — fill a real form, click submit, read the result:

package com.mycompany.tests.tests;
 
import io.github.bonigarcia.wdm.WebDriverManager;
import org.openqa.selenium.By;
import org.openqa.selenium.Keys;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.testng.Assert;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
 
public class LoginFormTest {
 
    WebDriver driver;
 
    @BeforeMethod
    public void setup() {
        WebDriverManager.chromedriver().setup();
        driver = new ChromeDriver();
        driver.get("https://www.saucedemo.com");
    }
 
    @Test
    public void shouldLoginSuccessfully() {
        WebElement username = driver.findElement(By.id("user-name"));
        WebElement password = driver.findElement(By.id("password"));
        WebElement loginButton = driver.findElement(By.id("login-button"));
 
        username.sendKeys("standard_user");
        password.sendKeys("secret_sauce");
        loginButton.click();
 
        // Read the URL after login to assert success
        Assert.assertTrue(
            driver.getCurrentUrl().contains("/inventory.html"),
            "Should land on the inventory page after login"
        );
    }
 
    @Test
    public void shouldShowErrorOnEmptyCredentials() {
        driver.findElement(By.id("login-button")).click();
 
        WebElement error = driver.findElement(By.cssSelector("[data-test='error']"));
        Assert.assertTrue(error.isDisplayed());
        Assert.assertEquals(
            error.getText(),
            "Epic sadface: Username is required",
            "Sauce Demo's exact error message"
        );
    }
 
    @Test
    public void shouldClearAndTypeAgain() {
        WebElement username = driver.findElement(By.id("user-name"));
        username.sendKeys("wrong_user");
        username.clear();              // wipe what's there
        username.sendKeys("standard_user");
 
        Assert.assertEquals(
            username.getAttribute("value"),
            "standard_user",
            "Field should contain the second value, not the first"
        );
    }
 
    @Test
    public void shouldSubmitWithEnterKey() {
        driver.findElement(By.id("user-name")).sendKeys("standard_user");
        WebElement password = driver.findElement(By.id("password"));
        password.sendKeys("secret_sauce");
        password.sendKeys(Keys.ENTER);  // submit by Enter — no click needed
 
        Assert.assertTrue(driver.getCurrentUrl().contains("/inventory.html"));
    }
 
    @AfterMethod
    public void teardown() {
        if (driver != null) driver.quit();
    }
}

Four tests. Together they exercise every interaction method we've covered — sendKeys, click, clear, getText, getAttribute("value"), isDisplayed, Keys.ENTER, and the URL read.

The interaction loop, visualised

Step 1 of 5

Find

driver.findElement(By.cssSelector(...)) — locate the element. Throws NoSuchElementException if absent.

This pattern — find, prepare, act, read, assert — is what your tests will look like for the rest of your career. Get fluent at it now.

The three interaction-time exceptions

Three exceptions show up regularly during interaction; each tells you something specific:

NoSuchElementException — the locator didn't match. Either the locator is wrong, or the element hasn't loaded yet.

// Almost always — wait for the element first (chapter 3 covers explicit waits)
driver.findElement(By.id("does-not-exist"));   // → NoSuchElementException

ElementNotInteractableException — the element exists but can't be clicked or typed into. Usually because it's hidden, off-screen, disabled, or covered by another element (a modal, a sticky header, a loading spinner).

WebElement button = driver.findElement(By.id("submit"));
button.click();  // → ElementNotInteractableException if the button is hidden behind a spinner

StaleElementReferenceException — you found the element, but the DOM changed between finding it and using it. Single-page apps re-render constantly; an element that existed 200ms ago may have been replaced by a new (visually identical) element.

WebElement card = driver.findElement(By.cssSelector(".product-card"));
// ... something triggers a re-render ...
card.click();   // → StaleElementReferenceException — the old reference is dead

The fix is almost always to find the element again, immediately before using it:

driver.findElement(By.cssSelector(".product-card")).click();   // single line: find + click

We'll handle the timing dimension of these — the WebDriverWait machinery — in chapter 3.

Comparison with Cypress and Playwright

// Cypress
cy.get("[data-testid='email']").type("alice@test.com");
cy.get("[data-testid='submit']").click();
 
// Playwright
await page.getByTestId("email").fill("alice@test.com");
await page.getByTestId("submit").click();

Both modern frameworks have auto-waiting baked into every interaction. Selenium does notfindElement and click do not retry. That's why chapter 3 exists. Once you've built the explicit-wait pattern, your code reads only slightly heavier than the JS frameworks.

The Selenium tool entry on qa.codes lists every WebElement method, and the XPath & CSS selectors cheat sheet covers the locator side.

⚠️ Common mistakes

  • sendKeys without clear() on a pre-filled field. Inputs that the app pre-fills (a remembered email, a default value) get the new text appended, producing strings like alice@test.comnew@test.com. Call clear() first whenever you can't be sure the field is empty. Note: clear() itself can sometimes fail on rich text editors — for those, fall back to Keys.chord(Keys.CONTROL, "a") followed by Keys.DELETE.
  • Reading input value with getText(). <input> elements have no rendered text content — getText() returns an empty string. Use getAttribute("value") instead. The number of suites that have at least one getText() call on an input that should be getAttribute("value") is depressingly high.
  • Catching StaleElementReferenceException with a tight retry loop. If you see staleness, the answer isn't try { click } catch { try again }. The answer is "don't hold WebElement references across DOM mutations" — find immediately before use, or use Page Factory's @CacheLookup = false. A retry-on-stale catch hides a design problem and produces flaky tests.

🎯 Practice task

Build a complete form-interaction test on a real site. 30–40 minutes.

  1. Add LoginFormTest from this lesson to your project. Run all four tests; all should pass.
  2. Add a fifth test: lock-out flow. Use the credentials locked_out_user / secret_sauce. Sauce Demo refuses login. Assert the error message via error.getText(), and assert the URL did not change.
  3. Read every state. On the inventory page (after a successful login), find the cart icon. Use:
    • isDisplayed() to check it's visible
    • getAttribute("class") to dump the classes
    • getText() to read its visible text (initially empty)
    • getSize() and getLocation() just to see what they return Print each. Useful exercise — you'll be surprised how often you'll want exactly these reads in real tests.
  4. Make StaleElementReferenceException happen. On the inventory page, find any product card. Click "Add to cart" on a different card. Now try to use the first card's reference. Read the exception. Then refactor the test to find the element fresh each time, and watch it pass.
  5. Use Tab and Enter. Write a test that fills the username and password using only sendKeys and Keys.TAB to move between fields, then Keys.ENTER to submit. No .click() calls. This is how a keyboard user navigates the page — and great a11y signal too.
  6. Stretch — the wait you don't yet have. Add a test that immediately tries to find an element on the inventory page right after loginButton.click(). On a fast machine it works; on a slow CI runner it would intermittently fail. Don't fix it yet — just feel the timing problem. Chapter 3 starts there.

Next lesson: the form elements we haven't covered yet — dropdowns (the <select> element and the dedicated Select class), checkboxes, and radio buttons. Each has its own subtle behaviour that catches beginners.

// tip to track lessons you complete and pick up where you left off across devices.