The basic interactions you've learned — click, sendKeys, clear — handle text inputs and ordinary buttons. The form elements that need specialised treatment are dropdowns, checkboxes, and radio buttons. Each has its own quirks. Native <select> elements get a dedicated Select class with its own API. Custom dropdowns built from divs and lists need plain clicks. Checkboxes need state-aware logic because click() toggles them rather than setting them. This lesson covers all three with runnable Java code.
Native dropdowns — the Select class
The native HTML <select> element is an old part of the platform. Selenium ships a dedicated Select helper that wraps the WebElement and gives you the verbs you'd expect:
import org.openqa.selenium.support.ui.Select;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
WebElement countryEl = driver.findElement(By.id("country"));
Select country = new Select(countryEl);
// Three ways to pick an option
country.selectByVisibleText("United Kingdom");
country.selectByValue("uk"); // matches <option value="uk">
country.selectByIndex(2); // 0-based; the third <option>
// Read the current selection
String selected = country.getFirstSelectedOption().getText();
// Read every option
List<WebElement> options = country.getOptions();
for (WebElement option : options) {
System.out.println(option.getText());
}
// Multi-select (only if the underlying <select multiple>)
if (country.isMultiple()) {
country.selectByVisibleText("Red");
country.selectByVisibleText("Blue");
country.deselectByVisibleText("Red");
country.deselectAll();
}A few non-obvious rules:
- The
Selectclass only works on real<select>elements. Wrapping a<div role="listbox">inSelectthrowsUnexpectedTagNameException. The first thing the constructor does is verify the tag. getFirstSelectedOption()is what you assert on. CallinggetText()on the wrapped WebElement returns the entire dropdown's option-list as one big string — not what you want.selectByVisibleTextis whitespace-sensitive."United Kingdom "(trailing space) won't match"United Kingdom". Inspect the option text in DevTools if you're unsure.
Custom dropdowns — just click
Most modern web apps don't use native <select>. They build dropdowns out of <div>, <button>, and <li> so they can style them. Those need plain clicks, not the Select class:
// Open the dropdown
driver.findElement(By.cssSelector("[data-testid='country-dropdown']")).click();
// Wait for the option list to render (chapter 3 — for now assume it's instant)
// Click the option
driver.findElement(By.xpath("//li[normalize-space()='United Kingdom']")).click();
// Verify by reading the trigger's text
String selected = driver.findElement(By.cssSelector("[data-testid='country-dropdown'] .selected-label"))
.getText();The pattern is always: click to open, click the option, read the trigger to assert. Custom dropdowns often need an explicit wait between opening and clicking (chapter 3) — the option list usually renders asynchronously.
Checkboxes — state-aware clicks
Checkboxes are HTML's most counter-intuitive element to automate. Selenium's click() toggles the checkbox, it doesn't set it. So if you "want it checked" but it's already checked, clicking unchecks it. Always check isSelected() first:
WebElement termsCheckbox = driver.findElement(By.id("terms"));
// Make sure it's checked, regardless of starting state
if (!termsCheckbox.isSelected()) {
termsCheckbox.click();
}
// Make sure it's unchecked
if (termsCheckbox.isSelected()) {
termsCheckbox.click();
}
// Verify
Assert.assertTrue(termsCheckbox.isSelected(), "Terms checkbox should be checked");For Page Object code, factor this into a helper:
public void setCheckbox(WebElement el, boolean checked) {
if (el.isSelected() != checked) {
el.click();
}
}Cypress and Playwright handle this differently — cy.check() and page.check() are idempotent (checking an already-checked box does nothing). Selenium leaves the responsibility to you.
Radio buttons — click and verify
Radio buttons are easier than checkboxes — clicking a radio is idempotent (clicking the already-selected radio does nothing visible). But within a group, clicking one radio also unchecks any sibling radio in the same group:
WebElement standardShipping = driver.findElement(By.id("shipping-standard"));
WebElement expressShipping = driver.findElement(By.id("shipping-express"));
standardShipping.click();
Assert.assertTrue(standardShipping.isSelected());
Assert.assertFalse(expressShipping.isSelected());
expressShipping.click();
Assert.assertTrue(expressShipping.isSelected());
Assert.assertFalse(standardShipping.isSelected()); // automatically deselectedPicking a radio from a group dynamically — say, by value attribute:
import java.util.List;
List<WebElement> radios = driver.findElements(By.name("shipping-method"));
for (WebElement radio : radios) {
if ("express".equals(radio.getAttribute("value"))) {
radio.click();
break;
}
}This pattern shows up whenever the test data drives which radio to pick — chapter 7 (data-driven testing) leans on this heavily.
Quick reference — the form-element matrix
Selenium calls per form-element type
| Set value | Read state | Notes | |
|---|---|---|---|
| Native <select> | new Select(el).selectByVisibleText("UK") | select.getFirstSelectedOption().getText() | Use the dedicated Select class — only works on real <select> |
| Custom dropdown | trigger.click() then option.click() | trigger.findElement(...).getText() | Plain clicks. Often needs an explicit wait between open and pick |
| Checkbox | if (!checkbox.isSelected()) checkbox.click() | checkbox.isSelected() | click() toggles — guard with isSelected() to make set/unset idempotent |
| Radio button | radio.click() — siblings auto-deselect | radio.isSelected() | Within a name-group, only one is selected at a time |
Pin this to your monitor for your first month. The patterns are stable; the muscle memory will follow.
A complete shipping-form test
Putting all four element types into one test against a hypothetical shipping form. We'll use https://practice.expandtesting.com/inputs as a stand-in for "any form with a select"; in your own work you'll target your own app.
package com.mycompany.tests.tests;
import io.github.bonigarcia.wdm.WebDriverManager;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.support.ui.Select;
import org.testng.Assert;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import java.util.List;
public class FormElementsTest {
WebDriver driver;
@BeforeMethod
public void setup() {
WebDriverManager.chromedriver().setup();
driver = new ChromeDriver();
driver.get("https://practice.expandtesting.com/dropdown");
}
@Test
public void shouldSelectFromNativeDropdown() {
WebElement dropdownEl = driver.findElement(By.id("dropdown"));
Select dropdown = new Select(dropdownEl);
dropdown.selectByVisibleText("Option 2");
Assert.assertEquals(
dropdown.getFirstSelectedOption().getText(),
"Option 2"
);
}
@Test
public void shouldListAllDropdownOptions() {
Select dropdown = new Select(driver.findElement(By.id("dropdown")));
List<WebElement> options = dropdown.getOptions();
// First "option" on this site is the placeholder; assert at least 3 real options
Assert.assertTrue(options.size() >= 3,
"Should have at least three options including the placeholder");
}
@Test
public void shouldToggleCheckboxIdempotently() {
// Switch to the checkboxes page
driver.get("https://practice.expandtesting.com/checkboxes");
// First checkbox starts unchecked on this site
WebElement checkbox = driver.findElement(By.cssSelector("input[type='checkbox']"));
// Make it checked, no matter the starting state
if (!checkbox.isSelected()) checkbox.click();
Assert.assertTrue(checkbox.isSelected());
// Make it unchecked
if (checkbox.isSelected()) checkbox.click();
Assert.assertFalse(checkbox.isSelected());
}
@AfterMethod
public void teardown() {
if (driver != null) driver.quit();
}
}Three tests across two pages. Each one demonstrates the right tool for the right element type.
Comparison with Cypress and Playwright
// Cypress
cy.get("#country").select("United Kingdom");
cy.get("#terms").check(); // idempotent — won't toggle if already checked
cy.get("#shipping-express").check();
// Playwright
await page.locator("#country").selectOption("United Kingdom");
await page.locator("#terms").check(); // idempotent
await page.locator("#shipping-express").check();Cypress's .check() and Playwright's .check() are both idempotent — call them on an already-checked box and nothing happens. Selenium's .click() always toggles. That's why every Selenium codebase has a setCheckbox(el, checked) helper somewhere; the modern frameworks don't need one.
For the canonical reference of every method covered here, see the Selenium tool entry on qa.codes.
⚠️ Common mistakes
- Wrapping a custom dropdown in
Select.new Select(driver.findElement(By.cssSelector("[data-testid='country-dropdown']")))on a<div>-based dropdown throwsUnexpectedTagNameExceptionimmediately. TheSelectclass is only for native<select>elements. For custom dropdowns, click the trigger and click the option like any other elements. - Calling
click()on a checkbox without checking state. A test that starts withcheckbox.click()works on first run but breaks on retry — if the previous test left the checkbox checked, this one unchecks it. Always guard:if (!checkbox.isSelected()) checkbox.click();. Or write asetCheckbox(el, true)helper and use it everywhere. - Asserting on a radio's
getAttribute("checked")instead ofisSelected(). Thecheckedattribute reflects only the initial HTML state; it doesn't update when the user clicks.isSelected()reports the current DOM state, which is what you actually care about. Same trap exists for checkboxes.
🎯 Practice task
Drive every form-element type. 30–40 minutes.
- Add
FormElementsTestfrom this lesson to your project. Run it. All three tests should pass. - Multi-select practice. Visit https://practice.expandtesting.com/dropdown and find the multi-select element if available, or pick another public site with a
<select multiple>. Write a test that selects three options, assertsgetAllSelectedOptions().size() == 3, thendeselectAll()and asserts the size is back to zero. - Custom dropdown. On a real site you use (Gmail, your own app, any modern e-commerce site), find a styled dropdown that's not a native
<select>. Use DevTools to confirm the underlying tag is<div>or<button>, then write a test that clicks-to-open and clicks-to-select. Notice you didn't reach forSelect. - Checkbox guard helper. Write a static method
Forms.setCheckbox(WebElement el, boolean checked)that uses theif (el.isSelected() != checked) el.click();pattern. Refactor yourshouldToggleCheckboxIdempotentlytest to call it —Forms.setCheckbox(cb, true),Forms.setCheckbox(cb, false). Run twice; the test should be just as green on the second run as the first. - Radio group iteration. Find any radio group on a public page (https://practice.expandtesting.com/radio-buttons or your own app). Write a parameterised test (you can use a simple loop for now,
@DataProvideris in chapter 5) that:- Iterates through every radio
- Clicks it
- Asserts only that radio is selected and all others are not
- Stretch — auto-detect. Write a helper
selectFromAnyDropdown(WebDriver driver, By trigger, String optionText)that detects whether the underlying element is a<select>(useSelect) or a custom dropdown (click-then-click). Test it on both a native and a custom dropdown. This is the kind of utility that earns its keep across a 200-page-object framework.
Chapter 2 is done. You've now got the full locator toolkit and the interaction methods to drive any form on any page. Chapter 3 is where Selenium's biggest tool lives: synchronisation. Every test you write from here on will rely on WebDriverWait to handle the asynchronous nature of modern web apps — the difference between the suite that's flaky once a week and the suite you can trust.