Skip to content

feat: let CheckboxTester and SwitchTester set a state, not just toggle - #185

Merged
mcollovati merged 4 commits into
mainfrom
issues/177-checkbox-switch-set-state
Sep 16, 2026
Merged

mcollovati merged 4 commits into
mainfrom
issues/177-checkbox-switch-set-state

Conversation

@totally-not-ai

@totally-not-ai totally-not-ai Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Summary

CheckboxTester and SwitchTester could only click(), which flips the value. This adds check() / uncheck() and switchOn() / switchOff(), so a test can put the component into the state it needs without first asking what the state is.

What changed

  • CheckboxTester gains check(), uncheck() and isChecked().
  • SwitchTester gains switchOn() and switchOff() (it already had isOn()).
  • Each of the four gesture methods clicks only when the component is not already in the wanted state, so no extra value change event is fired. The value change still comes from the client, exactly like click().
  • The usability check runs in every case: calling them on a disabled checkbox or a read-only switch throws IllegalStateException, even when the call would otherwise do nothing.
  • Because the methods are public on the testers, the annotation processor also copies them onto the generated CheckboxLocator and SwitchLocator.

This change is purely additive — no existing method changed its behaviour, so nothing breaks.

Use case

You test a signup view with an "I accept the terms" checkbox and a settings view with a "dark mode" switch. You want the test to say what the user did ("accept the terms"), and you do not want it to break if the default value of the field changes later — a plain click() would then flip the value the wrong way.

@Test
void signup_requiresAcceptedTerms() {
    SignupView view = navigateTo(SignupView.class);

    test(view.email).setValue("jane@example.com");
    test(view.acceptTerms).check();          // no-op if already checked
    test(view.submit).click();

    Assertions.assertTrue(test(view.acceptTerms).isChecked());
    Assertions.assertEquals("Welcome!", test(view.message).getText());
}

@Test
void darkMode_canBeTurnedOff() {
    SettingsView view = navigateTo(SettingsView.class);

    test(view.darkMode).switchOff();         // leaves it off if it was off
    Assertions.assertFalse(view.darkMode.getValue());
}

API Changes

com.vaadin.flow.component.checkbox.CheckboxTester

// Added
public boolean isChecked()
public void check() // clicks only if not already checked
public void uncheck() // clicks only if not already unchecked

com.vaadin.flow.component.checkbox.SwitchTester

// Added
public void switchOn() // clicks only if not already on
public void switchOff() // clicks only if not already off

Test summary

# Status What the test verifies Why it matters
1 check() / uncheck() reach the wanted state, fire exactly one value change with isFromClient() == true, and fire nothing when already in that state The core feature; a stray click would flip the value back and a server-side event would not match the browser
2 switchOn() / switchOff() behave the same way on Switch Same contract for the switch tester
3 A disabled checkbox and a read-only switch throw IllegalStateException, even when the call is a no-op Tests must not silently "succeed" on a component the user cannot touch
4 isChecked() matches the checkbox value before and after a click It is the state check the new methods and user tests rely on
5 check(), uncheck() and isChecked() work through the generated CheckboxLocator Confirms the processor really exposes the new methods on the locator API
  • CheckboxTesterTest.checkAndUncheck_onlyClickWhenNeeded — row 1
  • SwitchTesterTest.switchOnAndOff_onlyClickWhenNeeded — row 2
  • CheckboxTesterTest.check_notUsableAndAlreadyChecked_throws, SwitchTesterTest.switchOn_notUsableAndAlreadyOn_throws — row 3
  • CheckboxTesterTest.isChecked_reflectsValue — row 4
  • LocatorApiTest.checkbox_checkUncheckAndIsChecked_reachableFromLocator — row 5

Not tested on purpose: switchOn() / switchOff() through the generated SwitchLocator. It is the same generated delegation that row 5 already pins for the checkbox.

totally-not-ai Bot and others added 2 commits September 8, 2026 08:01
CheckboxTester and SwitchTester exposed only click(), so putting a
checkbox or switch into a known state meant reading the component value
first. Add CheckboxTester.setChecked(boolean)/isChecked() and
SwitchTester.setOn(boolean), mirroring TestBench's CheckboxElement.

Setting the state routes through click(), so the value change is seen as
coming from the client, and is a no-op when the component already is in
the requested state. The usability guard runs in either case, so acting
on a disabled or read-only component still throws.

Fixes #177
@mcollovati

Copy link
Copy Markdown
Contributor

The change looks good, but setChecked and setOn doesn't seem proper names for the methods.
I would see them as a force set of the value, potentially always firing events server side, not as a user gesture.
Let's think about some more descriptive names

setChecked(boolean)/setOn(boolean) read like a forced value set. Rename
them to the gesture the user performs: CheckboxTester.check()/uncheck()
and SwitchTester.switchOn()/switchOff(), matching the imperative naming
the other testers already use (openDetails(), closeDetails(), select()).

Behaviour is unchanged: each one clicks only when the component is not
already in the target state, and the usability guard runs either way.
switchOff() promises to do nothing when the switch is already off, and
uncheck() is declared on the tester so the generated locator picks it up
- neither branch was exercised.
@totally-not-ai

Copy link
Copy Markdown
Contributor Author

@mcollovati Agreed — setChecked/setOn read like a forced value set, which is exactly what these are not. Renamed them to the gesture the user performs:

  • CheckboxTester: check() / uncheck()
  • SwitchTester: switchOn() / switchOff()

This also lines up with the imperative naming the other testers already use (openDetails(), closeDetails(), select()), and keeps set* reserved for the component-level API that really does force a value. check()/uncheck() are the same names Playwright uses for this operation, with the same semantics. isChecked() / isOn() stay as they are, being plain read accessors.

Behaviour is unchanged: each method clicks only when the component is not already in the target state, and the usability guard runs either way, so a disabled or read-only component still throws even on the no-op path. Javadoc now says why the no-op happens ("a user would not click it in that case") rather than describing it as setting a state.

Full build is green.

@mcollovati
mcollovati merged commit 1b7ad46 into main Sep 16, 2026
6 checks passed
@mcollovati
mcollovati deleted the issues/177-checkbox-switch-set-state branch September 16, 2026 10:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants