Skip to content

feat: add stepUp/stepDown to NumberFieldTester - #191

Merged
mcollovati merged 6 commits into
mainfrom
issues/179-numberfield-step-buttons
Sep 14, 2026
Merged

feat: add stepUp/stepDown to NumberFieldTester#191
mcollovati merged 6 commits into
mainfrom
issues/179-numberfield-step-buttons

Conversation

@totally-not-ai

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

Copy link
Copy Markdown
Contributor

Summary

NumberField and IntegerField can show +/- step buttons, but the tester had no way to click them. This adds stepUp() / stepDown() (with optional click counts) that simulate those clicks, plus an isValid() helper to check the field's current value against its constraints.

What changed

This change is purely additive: no existing method changes behaviour, so current tests keep working.

  • stepUp() / stepDown() and stepUp(int times) / stepDown(int times) on NumberFieldTester:
    • They require the step buttons to be visible (setStepButtonsVisible(true)) and the field to be usable, otherwise they throw IllegalStateException.
    • Each click sets the value on its own and is marked as coming from the client, so one value change event is fired per click, just like in the browser.
    • The new value is aligned with the step scale, measured from min when min is set and from zero otherwise. This mirrors the web component: stepping from a value that is off the scale moves to the next aligned value instead of adding a full step.
    • A click that would move outside the minmax range throws IllegalStateException, because the browser disables the button in that case. In a multi-click call, the clicks performed before the failing one are kept.
    • Stepping an empty field starts from zero, or from the closest boundary when zero is outside the range.
    • times must be positive; 0 or a negative number throws IllegalArgumentException.
  • isValid() on NumberFieldTester reports whether the field is currently valid: required, min, max and — when step is explicitly set — the step scale, and it also returns false when the field was marked invalid from the outside (for example by a Binder). setValue still does not check the step scale, since a value off the scale can also be committed from the browser; use isValid() to assert on that.

Fixes #179

Use case

A developer builds an order form with a quantity IntegerField that uses step buttons, with a minimum order of 2 and a step of 2. They want a unit test that the user can raise the quantity with the + button and that the order total follows.

IntegerField quantity = new IntegerField();
quantity.setStepButtonsVisible(true);
quantity.setMin(2);
quantity.setStep(2);

NumberFieldTester<IntegerField, Integer> quantity_ = test(quantity);

quantity_.stepUp(2); // two clicks on the + button, two value change events
Assertions.assertEquals(6, quantity.getValue());
Assertions.assertTrue(quantity_.isValid());
Assertions.assertEquals("60.00 €", view.total.getText());

API Changes

com.vaadin.flow.component.textfield.NumberFieldTester

// Added
public boolean isValid() // true when the field is not marked invalid and its value satisfies required, min, max and the explicitly set step
public void stepUp() // clicks the step up button once
public void stepUp(int times)
public void stepDown() // clicks the step down button once
public void stepDown(int times)

Test summary

# Status What the test verifies Why it matters
1 Stepping a field whose step buttons are not visible throws IllegalStateException The tester must not simulate a click on a button the user cannot see
2 Stepping a read-only field throws IllegalStateException A user cannot change a field that is not usable
3 A step moves to the next value aligned with the step scale, measured from min when set and from zero otherwise, also for values below the basis This is the core algorithm; a wrong result would silently not match the real component
4 A step that would leave the minmax range throws and leaves the value unchanged The browser disables the button there, so the tester must not produce an impossible value
5 In a multi-click call the failing click throws, earlier clicks are kept, and only performed clicks fire an event Pins the documented partial-stepping contract
6 Each click fires its own value change event, marked as coming from the client Apps listen to these events; one event per call would be wrong
7 Stepping an empty field starts from zero, or from the closest boundary when zero is outside the range Common case; an empty field must not stay empty or jump to a value out of range
8 times of 0 or a negative number throws IllegalArgumentException Clicking a button zero or minus one times is a test bug, not a no-op
9 Repeated decimal steps keep precision (0.1 → 0.2 → 0.4 → 0.1) Plain double arithmetic would accumulate rounding errors
10 isValid() is false for a value off the step scale, a value above max set on the server, an empty required field, and a field marked invalid from the outside This is the only way to assert on validation state, since setValue does not check the step scale
11 gap A non-positive step falls back to a step of 1, as the browser does Only reachable by explicitly setting a zero or negative step; low risk but untested
  • NumberFieldTesterTest.stepButtonsNotVisible_step_throws → 1
  • NumberFieldTesterTest.nonUsableField_step_throws → 2
  • NumberFieldTesterTest.unalignedValue_step_movesToTheClosestValueAlignedWithStep → 3
  • NumberFieldTesterTest.negativeValue_step_movesTowardsTheStepBasis → 3
  • NumberFieldTesterTest.stepWouldExceedBoundaries_throws_valueIsNotChanged → 4
  • NumberFieldTesterTest.stepMultipleTimes_aClickWouldExceedBoundaries_throws_earlierClicksAreKept → 4, 5, 6
  • NumberFieldTesterTest.integerField_stepMultipleTimes_oneEventPerClick → 6
  • NumberFieldTesterTest.emptyNumberField_step_oneStepIsSet_clientSideEventIsFired → 6, 7
  • NumberFieldTesterTest.emptyField_step_startsFromZeroOrTheClosestBoundary → 7
  • NumberFieldTesterTest.nonPositiveTimes_step_throws → 8
  • NumberFieldTesterTest.decimalStep_step_doesNotLosePrecision → 9
  • NumberFieldTesterTest.isValid_reportsConstraintViolationsAndExternalInvalidState → 10

Deliberately untested: the conversion of a stepped value back to Integer for IntegerField, which is covered indirectly by the integer stepping test, and the exact wording of the exception messages.

NumberField and IntegerField render +/- step buttons when
setStepButtonsVisible(true) is enabled, and there was no tester method
for clicking them.

stepUp()/stepDown() (and their stepUp(times)/stepDown(times) variants)
simulate those clicks: they require the step buttons to be visible,
apply the step aligned with the step scale measured from min, and refuse
to move outside the min-max range, the same way the web component
disables the button. The value is set as coming from the client.

Fixes #179
Pin the step alignment for values below the step basis, where both step
directions move towards the basis, the same as the web component's
remainder-based _getIncrement. Also cover stepping an empty field down
and clicking a step button zero times.

Share the alignment margin between stepAligned and the empty-field
boundary case, and document why the element property is used to detect
an unset min.
for (int i = 1; i < times; i++) {
value = stepOnce(value, up);
}
setValueAsUser(toValue(value));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setting the value only for the last value seems incorrect when times > 1. A real application would fire on a value change event for every step click

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, changed it. step now sets the value once per click instead of computing the end value first, so stepUp(3) fires three value change events, and if a click cannot be performed the value keeps the steps applied before it — same as in the browser. integerField_stepMultipleTimes_oneEventPerClick asserts the full sequence of values seen by the listener.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be a test that verifies that stepUp(N) throws an exception if the number of steps makes the value exceed min or max

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Covered now by stepMultipleTimes_aClickWouldExceedBoundaries_throws_earlierClicksAreKept, in both directions: with min=0, max=10, step=3, stepUp(3) from 4 throws on the third click, and stepDown(3) from 5 throws on the third click.

It also pins what the value is left at, which is worth stating explicitly since the implementation changed after your earlier comment: each click now sets the value on its own, so a failing click leaves the earlier ones applied — 4 → 6 → 9 → throw, and 5 → 3 → 0 → throw — and only the performed clicks fire a value change event.

Comment thread shared/src/main/java/com/vaadin/flow/component/textfield/NumberFieldTester.java Outdated
Clicking a step button commits the value in the browser, so stepUp and
stepDown now set the value once per click instead of once per call, and
clicking zero times is rejected like a negative count.

isValid() exposes the constraints the field applies to its current
value, including the step scale, which setValue deliberately does not
check: a value off the step scale can be committed from the browser too.
…d-step-buttons

# Conflicts:
#	junit6/src/test/java/com/vaadin/flow/component/textfield/NumberFieldTesterTest.java
#	shared/src/main/java/com/vaadin/flow/component/textfield/NumberFieldTester.java
A stepUp/stepDown call that hits the range limit part way through keeps
the clicks it already performed, which was only promised in the Javadoc.

isValid() now also reports a field that is marked invalid from the
outside, as a Binder does, and its test covers a value set on the server
outside the min-max range.
@totally-not-ai

Copy link
Copy Markdown
Contributor Author

@mcollovati all five points are addressed in the latest commits. The branch also carries a merge of main, which had moved on to the uniform clear() / clickClearButton() work — NumberFieldTester now has both those methods and the new step methods, and NumberFieldTesterTest implements the RefusesEmptyValueContract / ClearButtonContract from main alongside the step tests.

API added on top of main: NumberFieldTester.isValid(), stepUp(), stepUp(int), stepDown(), stepDown(int), all of which the generated NumberFieldLocator and IntegerFieldLocator pick up.

The multi-click boundary test only exercised the max side; it now steps
down past min as well, asserting that the failing click throws and the
clicks before it are kept.
@mcollovati
mcollovati merged commit b60a65f into main Sep 14, 2026
6 checks passed
@mcollovati
mcollovati deleted the issues/179-numberfield-step-buttons branch September 14, 2026 13:32
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.

NumberFieldTester has no stepUp()/stepDown() for the step buttons

1 participant