How to Handle Radio Buttons and Checkboxes in Selenium and Playwright
Practice radio button and checkbox automation — select, deselect, assert state, handle groups, indeterminate state, and dynamic lists in Playwright, Selenium & Cypress.
Interactive Scenarios
No data-testid on inputs. Locate via starts-with(@name, "perm_").
| ID | Scenario | Type | Priority | |
|---|---|---|---|---|
| RC_001 | Verify checkbox can be checked Expected: isChecked() / isSelected() returns true after check(). ✅ positivehigh | positive | high | |
| RC_002 | Verify checkbox can be unchecked Expected: isChecked() returns false after uncheck(). ✅ positivehigh | positive | high | |
| RC_003 | Verify a radio button can be selected from a group Expected: The clicked radio is checked; others in the group become unchecked. ✅ positivehigh | positive | high | |
| RC_004 | Verify only one radio in a group can be selected at a time Expected: Selecting a second radio automatically deselects the first. ✅ positivehigh | positive | high | |
| RC_005 | Verify all checkboxes in a group can be checked Expected: All checkboxes with data-testid="chk-skill" are checked. ✅ positivemedium | positive | medium | |
| RC_006 | Verify pre-checked checkbox is checked on page load Expected: isChecked() returns true without any interaction. ✅ positivemedium | positive | medium | |
| RC_007 | Verify disabled checkbox cannot be interacted with Expected: isDisabled() returns true; check() throws or has no effect. ❌ negativehigh | negative | high | |
| RC_008 | Verify disabled radio button cannot be selected Expected: isDisabled() returns true for the disabled radio. ❌ negativehigh | negative | high | |
| RC_009 | Verify sibling-located checkbox via XPath preceding-sibling Expected: Checkbox is checked after XPath-based location. ✅ positivemedium | positive | medium | |
| RC_010 | Verify scoped plan card radio can be selected Expected: Radio inside the Enterprise plan card is checked after scoped selection. ✅ positivemedium | positive | medium | |
| RC_011 | Verify dynamic checkboxes with partial name attribute can be located Expected: Checkboxes with names starting with perm_ are located via XPath starts-with. ✅ positivemedium | positive | medium | |
| RC_012 | Verify checkbox result span updates after interaction Expected: The result span text changes to reflect the new checked state. ✅ positivemedium | positive | medium | |
| RC_013 | Verify checkbox is accessible via keyboard Tab and Space Expected: Checkbox can be focused and toggled using keyboard only. ✅ positivemedium | positive | medium | |
| RC_014 | Verify radio button group has correct name attribute for grouping Expected: All radios in the plan group share the same name attribute. ✅ positivelow | positive | low |
Checkboxes and radio buttons are the most common binary input controls. Checkboxes allow multi-selection; radio buttons enforce single selection within a group. Both expose an `isChecked` / `isSelected` state that automation frameworks can read and assert directly.
Use check() instead of click() — it is idempotent and only fires if the element is currently unchecked, making tests stable. Use uncheck() to deselect. Avoid asserting by visual appearance; always read the boolean checked state.
// Check
await page.getByTestId('chk-accept-terms').check();
// Uncheck
await page.getByTestId('chk-accept-terms').uncheck();
// Check all in a group
const skills = page.getByTestId('chk-skill');
for (const chk of await skills.all()) {
await chk.check();
}Radio buttons must be scoped to their group via the parent element or by the shared `name` attribute. Clicking one radio in a group automatically deselects the previously selected one — assert this mutual-exclusion behaviour in your tests.
// Select a radio by label (scoped to group)
const group = page.getByTestId('radio-plan-group');
await group.getByLabel('Pro').check();
// Assert mutual exclusion
await expect(group.getByLabel('Pro')).toBeChecked();
await expect(group.getByLabel('Starter')).not.toBeChecked();
// Select by value attribute
await page.locator('[name="plan"][value="enterprise"]').check();Assert the checked state directly rather than relying on class names or screenshots. All three frameworks expose a first-class boolean assertion for checked state.
// Assert checked
await expect(page.getByTestId('chk-newsletter')).toBeChecked();
// Assert unchecked
await expect(page.getByTestId('chk-accept-terms')).not.toBeChecked();
// Read boolean state
const checked = await page.getByTestId('chk-newsletter').isChecked();
console.log(checked); // trueDisabled controls must not be interactable. Assert isDisabled() before attempting any action. Some test runners throw on check() against a disabled element; others silently ignore it — always assert the state after.
// Assert disabled
await expect(page.getByTestId('chk-disabled')).toBeDisabled();
await expect(page.getByTestId('radio-disabled')).toBeDisabled();
// isDisabled() — boolean read
const disabled = await page.getByTestId('chk-disabled').isDisabled();
expect(disabled).toBe(true);| Action | Selenium | Playwright JS | Playwright PY | Cypress |
|---|---|---|---|---|
| Check | element.click() (if !isSelected()) | locator.check() | locator.check() | .check() |
| Uncheck | element.click() (if isSelected()) | locator.uncheck() | locator.uncheck() | .uncheck() |
| Assert checked | isSelected() | expect(locator).toBeChecked() | expect(locator).to_be_checked() | .should('be.checked') |
| Assert disabled | !isEnabled() | expect(locator).toBeDisabled() | expect(locator).to_be_disabled() | .should('be.disabled') |
| Check all in group | findElements() + loop | locator.all() + for…of | locator.all() + for…in | .check() (on multiple) |