Overview
This page is a guide to working with HTML form controls through HtmlUnit's
plain Java API. It covers three things, in order:
the general shape of the form-control classes and the everyday operations you'll use
on them; the different, easily-confused ways to read and write a control's
value; and the full HTML Constraint Validation API as HtmlUnit implements it.
The form control class hierarchy
Every form control in a parsed page is represented by a subclass of
HtmlElement. The two most important base types are:
HtmlInput -- the base class for every <input>
element, regardless of its type attribute.
HtmlSelectableTextInput -- an HtmlInput subclass that
adds text-selection and simulated-typing support, used by every text-like input
type.
HtmlUnit creates a different concrete class per type attribute value, so
that each type can implement exactly the constraints and behavior real browsers give
it. The most commonly used ones:
HtmlTextInput (type="text", and the default when
type is absent or unrecognized)
HtmlNumberInput, HtmlEmailInput, HtmlUrlInput,
HtmlTelInput, HtmlSearchInput, HtmlPasswordInput
-- all text-like, each with its own additional constraint (see
Constraint validation below)
HtmlCheckBoxInput and HtmlRadioButtonInput -- boolean
"checked" controls rather than text controls
HtmlFileInput -- represents a file-upload control; see
File inputs below
HtmlSubmitInput, HtmlResetInput, HtmlButtonInput,
HtmlImageInput -- the various button-like input types
HtmlHiddenInput -- never a candidate for constraint validation and
never focusable, but otherwise a plain value holder
HtmlDateInput, HtmlTimeInput, HtmlColorInput,
HtmlRangeInput and the other less common HTML5 types each have their
own class as well.
Outside the <input> family, HtmlSelect,
HtmlTextArea and HtmlButton (the standalone
<button> element, not <input type="submit">) are
the other major form-control classes; both value handling and validation apply to
them too, and are covered later on this page.
If a script changes an input's type attribute at runtime, HtmlUnit
internally replaces the Java object backing that DOM node with an instance of the
correct class for the new type, carrying the old value across via
adjustValueAfterTypeChange() -- each subclass decides for itself what,
if anything, of the old value still makes sense under the new type (for example,
switching into type="file" always clears the value, since a
real browser can never carry a text value over into a file selection).
Finding form controls
The usual DOM-style lookups work as expected --
page.getHtmlElementById(String),
page.getElementByName(String), or the generic
DomElement traversal/query methods. HtmlForm additionally
offers form-scoped convenience lookups that are usually more direct when you already
know which form you're working with:
final HtmlForm form = page.getFormByName("login");
final HtmlTextInput username = form.getInputByName("username");
final List<HtmlInput> allChecked = form.getInputsByValue("on"); // e.g. matching checkboxes
final List<HtmlRadioButtonInput> options = form.getRadioButtonsByName("plan");
HtmlForm.getElements() / the JavaScript-visible elements
collection return every "listed" control associated with the form -- inputs, selects,
textareas, buttons, fieldsets and objects -- in document order, regardless of whether
they're physically nested inside the <form> tag or associated with
it purely via a form="..." attribute elsewhere in the page.
Clicking, typing, focus and selection
click() simulates a real mouse click, including whatever side effects a
real click has for that specific control -- a submit button submits its form, a reset
button resets it, a checkbox toggles its checked state, and so on. This is almost
always the right method to call to simulate user interaction with a button, rather
than calling a lower-level state-changing method directly.
type(String) (available on any HtmlSelectableTextInput or
HtmlTextArea) simulates real keystroke-by-keystroke typing, character by
character, including firing the same key and input events a real browser would, and
respecting whatever character-level restrictions the control type imposes (for
example, a number input silently ignores non-numeric keystrokes unless the relevant
browser-version feature says otherwise). This is different from -- and usually more
faithful to real-browser behavior than -- calling setValue(String),
which behaves like scripted assignment (comparable to JavaScript's .value =)
rather than user input. See Three ways to set a value below for the concrete
differences this can produce.
focus() and removeFocus() move keyboard focus to or away
from a control, the same way clicking into a field or tabbing away from it would.
select() (on selectable text controls) selects the entire current value,
and getSelectedText(), getSelectionStart()/
setSelectionStart(int) and getSelectionEnd()/
setSelectionEnd(int) give fine-grained access to the current text
selection/cursor position, mirroring the same-named JavaScript properties.
Checkboxes and radio buttons
HtmlCheckBoxInput and HtmlRadioButtonInput don't hold a
free-form text value the way other inputs do -- their state is a simple boolean,
accessed via isChecked() / setChecked(boolean). Calling
setChecked(true) on a radio button automatically unchecks every other
radio button in the same named group within the same form, exactly as a real browser
would.
isDefaultChecked() reflects the checked attribute as parsed
from markup -- the state the control reverts to on a form reset -- independently of
whatever the live isChecked() state currently is, the same relationship
getDefaultValue() has to getValue() for text controls.
final HtmlRadioButtonInput yearly = form.getRadioButtonsByName("plan").stream()
.filter(r -> "yearly".equals(r.getValueAttribute()))
.findFirst().orElseThrow();
yearly.setChecked(true);
File inputs
HtmlFileInput deliberately does not support setting its value
to an arbitrary string the way a text input does -- real browsers never allow a page
to pre-fill a file chooser's selection for security reasons, and HtmlUnit follows the
same rule. Instead, use one of:
setFiles(File...) -- point the control at one or more real files on
disk, as if the user had picked them in a native file-chooser dialog. Passing
more than one file requires the multiple attribute to be present, or
an IllegalStateException is thrown.
setDirectory(File) -- recursively selects every file in a directory,
for controls with the non-standard webkitdirectory attribute.
setData(byte[]) / getData() and
setContentType(String) / getContentType() -- an
HtmlUnit-only convenience with no browser equivalent at all: lets a test supply
the bytes (and MIME type) to actually submit, without needing a real file on
disk. When set, this in-memory data is used during form submission instead of
reading the selected file's real contents.
getValue() on a file input never returns the real file path -- it
returns the empty string if nothing is selected, or the same
C:\fakepath\filename-style placeholder real browsers use, for the same
privacy reason setting an arbitrary value isn't allowed in the first place.
Buttons and form submission
HtmlButton and the button-like HtmlInput subtypes
(HtmlSubmitInput, HtmlResetInput, HtmlButtonInput)
all respond to click() according to their effective type:
submit (the default if no type attribute is present)
submits the enclosing form, reset resets it, and button
does neither on its own -- it only runs whatever onclick handler script
the page attaches, if any.
Submitting a form runs the form's constraint validation first (see
checkValidity(), reportValidity() and the 'invalid' event below) unless the
form has the novalidate attribute, or the specific button that triggered
submission has formnovalidate. If validation fails, the submission never
happens -- no request is sent -- exactly as in a real browser.
Resetting a form (via a reset button's click(), or calling
HtmlForm.reset() directly) calls reset() on every one of the
form's associated controls in turn. Most controls restore their live value from their
current default value and clear their internal "dirty" state (see the next section);
a plain HtmlButton's own reset() is intentionally a no-op,
since a button has nothing to reset.
Value handling
The remainder of this page covers two topics in detail: how to read and write a
control's value correctly, and how HtmlUnit's constraint-validation API works. Both
apply across every text-like HtmlInput subtype, HtmlTextArea,
and (with its own specific rules, covered separately) HtmlSelect.
getValue(), getValueAttribute() and getRawValue()
These three methods are easy to confuse because they can return the same string in
the simplest cases -- but they answer three different questions, and they diverge as
soon as a script (or a real user) interacts with the field.
getValueAttribute() reads the literal value="..."
attribute as it currently stands in the HTML/DOM tree. This is the control's
default value -- what the field would reset back to on a form reset. It
never changes just because someone typed into the field or called
setValue(); it only changes if the attribute itself is modified (via
setValueAttribute(), or the equivalent defaultValue
property from JavaScript).
getRawValue() returns the control's current, unsanitized internal
value -- whatever is "really" stored right now, decoupled from the DOM attribute.
The first time a script calls setValue() or the user types into the
field, the control's dirty value flag is set, and from that point on
getRawValue() reflects the live edited content, independent of whatever
the value attribute says.
getValue() is what most code should actually call. For a plain text
input it is effectively the sanitized form of getRawValue(). For some
control types (see below for HtmlNumberInput) it additionally normalizes
or rejects content that getRawValue() would return unchanged.
public void value() throws Exception {
try (WebClient webClient = new WebClient()) {
final HtmlPage page = webClient.getPage("https://www.wetator.org/testform/");
final HtmlTextInput input = page.getHtmlElementById("project");
// before any edit, all three agree
System.out.println(input.getValueAttribute()); // "initial"
System.out.println(input.getRawValue()); // "initial"
System.out.println(input.getValue()); // "initial"
input.type("my fancy HtmlUnit project");
// the attribute never moved -- it is still the DEFAULT value
System.out.println(input.getValueAttribute()); // "initial"
// but the live value has
System.out.println(input.getRawValue()); // "edited"
System.out.println(input.getValue()); // "edited"
}
}
Calling reset() (the same method a form's reset algorithm calls on every
one of its controls) clears the dirty flag and re-derives the live value from the
current default value -- not necessarily the value the page had when it was
first loaded, since a script may have changed defaultValue/the
value attribute in the meantime.
input.setDefaultValue("changed-default");
input.reset();
System.out.println(input.getValue()); // "changed-default", not "initial"
Three ways to set a value
A text-like input's value can be changed through three genuinely different code
paths, and it's worth being explicit about them because they don't always apply the
same rules -- this matters most for HtmlNumberInput (see the next
section), but the general distinction applies more broadly:
- The initial
value attribute, parsed once when the
page loads.
setValue(String) (and the equivalent JavaScript
.value setter) -- scripted assignment, comparable to a program
directly overwriting the field, with no simulation of individual keystrokes.
type(String) -- simulated real typing, character by
character, which can expose different intermediate states than a single
setValue() call would, and respects per-keystroke input filtering
that setValue() does not.
For most control types these three paths end up agreeing on the final result. Where
they diverge, it's almost always because a specific control type applies extra
sanitization or leniency to one path but not another -- see the next section for a
concrete, fully worked-out example.
HtmlNumberInput: parsing and sanitization
HtmlNumberInput layers extra behavior on top of the three methods above,
because a number input's textual content has to be checked against the HTML
specification's own, fairly strict, floating-point-number grammar -- which is
not the same grammar as a Java or JavaScript numeric literal.
The grammar (implemented by the internal HtmlNumberParser helper) accepts:
- an optional single leading
- (a leading + is
not accepted -- this is a common surprise, since it differs from
JavaScript's own Number()/parseFloat()),
- one or more ASCII digits, optionally with a single decimal point and further
digits,
- an optional exponent (
e or E), which -- unlike the
number's own leading sign -- does accept both + and
-,
- nothing else: no whitespace anywhere, no thousands separators, no
NaN/Infinity, no hex/binary/octal notation.
getValue() on a number input returns the value trimmed and
locale-normalized if it parses successfully, or the empty string if it does not
(unless the rare JS_INPUT_NUMBER_ACCEPT_ALL browser feature is active for
the configured BrowserVersion, in which case the raw text is returned
even when unparseable). getRawValue() always returns the literal
characters that are currently in the field, valid or not.
final HtmlNumberInput number = page.getHtmlElementById("qty");
number.setValue("42");
System.out.println(number.getValue()); // "42"
number.setValue("not-a-number");
System.out.println(number.getValue()); // "" -- sanitized away
System.out.println(number.getRawValue()); // "not-a-number" -- untouched
number.setValue("+5");
System.out.println(number.getValue()); // "" -- leading '+' is never valid
This is the clearest example of the three value-setting paths genuinely disagreeing.
Take the string "1." -- a digit followed by a decimal point with nothing
after it:
- Set via the initial
value attribute, some configured
browser versions silently correct it to "1", while others reject it
outright to the empty string.
- Set via
setValue("1."), it is rejected outright to
the empty string, uniformly, with no browser-specific leniency at all.
- Reached via
type("1.") (simulating the user typing
1 then .), the field's live value after those two
keystrokes reports "1" -- the longest already-valid prefix of what
has been typed so far -- not the raw "1." and not the empty string.
A further keystroke continues from the full typed text, so typing 2
next correctly produces "1.2", not "12".
If your test needs to simulate a user filling in a number field, prefer
type(String) over setValue(String) whenever the exact,
keystroke-by-keystroke intermediate state matters -- for example, when asserting on
checkValidity() partway through typing, rather than only after the field
is fully filled in.
HtmlTextArea: the dirty value flag and reset
A <textarea>'s value works differently from an
<input>'s in one important respect: an input's value always lives
in a plain string attribute, but a textarea's default value is its child
text content -- the literal text between the opening and closing tags. HtmlUnit
exposes the live value through getText() / setText(String)
(mirroring the JavaScript value property), and the default through
getDefaultValue() / setDefaultValue(String) (mirroring
defaultValue), the same pairing as for HtmlInput.
The relationship between the two is governed by a dirty value flag, exactly
as the HTML specification defines it. While the flag is clean (the textarea has never
been edited by script or by the user), getText() is always computed
fresh from the current child text content -- so mutating the DOM children directly
(for example, via setDefaultValue(), or lower-level DOM manipulation)
is immediately reflected in getText(). The moment setText()
is called (or the user types into the field), the flag becomes dirty, and from then
on the live value is held independently -- child-content changes no longer affect it
at all, until a reset clears the flag again.
final HtmlTextArea comments = page.getHtmlElementById("comments"); // <textarea>seed</textarea>
System.out.println(comments.getText()); // "seed"
comments.setText("edited"); // dirties the flag
System.out.println(comments.getText()); // "edited"
// once dirty, further changes to the underlying default no longer
// affect the live value
comments.setDefaultValue("new-default");
System.out.println(comments.getText()); // still "edited"
System.out.println(comments.getDefaultValue()); // "new-default"
reset() clears the dirty flag and re-derives the live value from
whatever the child text content currently is -- not the page's originally
parsed text, and not necessarily whatever getDefaultValue() last
returned either, if the children were mutated some other way while the flag was
still dirty (in which case those mutations had no visible effect on
getText() until this exact moment, since they happened while the flag
was dirty and therefore ignored).
comments.reset();
System.out.println(comments.getText()); // "new-default" -- picks up the current child content
Like text-type inputs, setting the value to the value it already holds is treated as
a near no-op: the dirty flag still becomes (or stays) set, but the text entry cursor
is not moved, unlike a genuine change, which always moves the cursor to the
end of the new value. readonly is a real, spec-supported attribute for
<textarea> (unlike for <select> or
<button>, covered below) and bars the control from constraint
validation the same way disabled does.
Cloning a textarea (cloneNode(boolean)) correctly carries the dirty flag
and live value across to the clone as independent state -- a clone of a dirtied
textarea keeps the dirtied value, decoupled from its own (independently cloned) child
content, and typing into the original afterward has no effect on the clone or vice
versa.
Constraint validation
Every control that can participate in HTML's Constraint Validation API implements
ValidatableHtmlElement. The methods on that interface answer three
related but distinct questions:
- Does this control take part in validation at all? --
willValidate()
- If it does take part, does its current value satisfy its own
constraints? --
isValid() /
isValidValidityState() and the individual
isXxxValidityState()/hasXxxValidityState() methods
- Combining both of the above, is the control valid right now, and why
not if it isn't? --
checkValidity(),
reportValidity(), getValidationMessage()
It is important to understand that question 2 on its own deliberately
ignores question 1. A disabled, empty, required text input is "invalid" in the pure
constraint-checking sense (isValueMissingValidityState() returns
true), but it is also barred from validation entirely
(willValidate() returns false), and the two are combined
only at the higher-level methods. This is exactly how real browsers behave: a
disabled field never blocks form submission and never shows a validation message,
no matter how "wrong" its content looks.
willValidate(): is this control barred from validation?
willValidate() returns false if the control is barred from
constraint validation, and true otherwise. A control is barred when any
of the following apply:
- it has the
disabled attribute set, directly or inherited from an
enclosing <fieldset disabled> -- except for controls
that are themselves inside that fieldset's first <legend>
child, which are exempt from the fieldset's disabling,
- it is a text-like input or a textarea with the
readonly attribute
set (readonly on a <select> or <button> has
no such effect, since neither element supports a real readonly
attribute),
- it is an
<input type="hidden">,
- it is a
<button> with type="button" or
type="reset" (only type="submit" buttons -- the
default -- participate in validation),
- it is a
<fieldset> or <object> element --
these always report false, since neither is itself a candidate for
constraint validation, regardless of any attribute.
final HtmlTextInput required = page.getHtmlElementById("required");
final HtmlTextInput disabled = page.getHtmlElementById("disabledRequired");
System.out.println(required.willValidate()); // true
System.out.println(disabled.willValidate()); // false, even though it's also required+empty
isValid(), isValidValidityState() and the individual validity checks
isValid() (and the near-identical isValidValidityState())
answer the pure constraint question, without regard to
willValidate(). Internally each returns true only if none
of the following individual checks report a problem. Each check corresponds directly
to one property on the standard DOM ValidityState object, and each is
individually callable, which is useful for building a custom error message without
relying on the generic text from getValidationMessage():
isCustomErrorValidityState() -- true if
setCustomValidity(String) was called with a non-empty message and
it has not since been cleared
isValueMissingValidityState() -- true if the control
has a required attribute and no value (for <select>,
this specifically means the auto-selected first option has an empty value and
nothing else was explicitly selected)
hasPatternMismatchValidityState() -- value present but doesn't
match the pattern attribute
isTooLongValidityState() / isTooShortValidityState()
-- value longer/shorter than maxlength/minlength
(only once the control's value has actually been edited by the user or a
script -- an over-long initial attribute value alone does not trigger this)
hasTypeMismatchValidityState() -- value doesn't match the required
syntax for the control's type (e.g. email, url)
hasRangeOverflowValidityState() /
hasRangeUnderflowValidityState() -- numeric value outside
max/min (number/range inputs)
isStepMismatchValidityState() -- numeric value not aligned to the
step attribute, measured from min (or zero, if
min is absent)
hasBadInputValidityState() -- the user's input could not be
converted at all (for example, a number input whose text simply doesn't parse
as a number). This is distinct from a range or step violation: a value that
fails to parse can never be "too big" or "misaligned", it just isn't a number in
the first place.
final HtmlNumberInput qty = page.getHtmlElementById("qty");
qty.setValue("not-a-number");
System.out.println(qty.hasBadInputValidityState()); // true
System.out.println(qty.hasRangeOverflowValidityState()); // false -- there's no number to compare
System.out.println(qty.isStepMismatchValidityState()); // false -- same reason
System.out.println(qty.isValid()); // false, because of badInput
checkValidity(), reportValidity() and the 'invalid' event
These are the two methods application code (and HtmlUnit's own form-submission logic)
should normally call, rather than combining willValidate() and
isValid() by hand every time.
checkValidity() performs static validation: it returns
true if the control is either barred from validation or actually valid,
and false otherwise. If it returns false, a cancelable
invalid DOM event is fired on the control -- but no focus change happens.
reportValidity() does exactly the same static check and fires the same
event, but additionally moves focus to the control if it is invalid. This mirrors
the real-browser distinction between a script silently checking validity
(checkValidity()) and a user-facing "please fix this field"
interaction (reportValidity()).
A <form>'s own checkValidity()/
reportValidity() run this same check across every one of the form's
associated controls. Barred controls are skipped entirely -- neither counted as
passing nor failing -- and every genuinely invalid control (not just the first one
found) fires its own invalid event. reportValidity() at
the form level focuses only the first invalid control it finds.
final HtmlForm form = page.getHtmlElementById("myForm");
if (!form.checkValidity()) {
// at least one control is genuinely invalid; every failing control
// has already had its 'invalid' event fired
}
Custom validity and validation messages
setCustomValidity(String message) lets application code (or a test)
mark a control as invalid for a reason the built-in constraints don't express, by
supplying a non-empty message. Passing an empty string clears it again. A non-empty
custom message always wins over every other constraint -- but, like every other
constraint, it is still ignored entirely if the control is barred from validation
(willValidate() is false).
getCustomValidity() returns the raw string most recently passed to
setCustomValidity(), or the empty string if none is currently set.
getValidationMessage() returns a human-readable description of why the
control currently fails validation, or the empty string if it is valid or barred.
If a custom validity message is set, that exact string is returned unchanged.
Otherwise, the message describes whichever single built-in constraint is failing,
checked in this priority order: custom error, pattern mismatch, range overflow,
range underflow, step mismatch, too long, too short, type mismatch, bad input, value
missing. The wording for each built-in case is generic by design (for example,
"Please fill out this field." for a missing value) -- some control
types with control-specific data available (such as a number input's actual
min/max) may produce a more specific message that
interpolates the relevant value.
final HtmlTextInput email = page.getHtmlElementById("email");
email.setCustomValidity("We already have an account with this address.");
System.out.println(email.checkValidity()); // false
System.out.println(email.getValidationMessage()); // "We already have an account with this address."
email.setCustomValidity("");
System.out.println(email.checkValidity()); // back to whatever the built-in constraints say
HtmlButton: validation specifics
HtmlButton (the standalone <button> element)
participates in constraint validation, but its story is much simpler than a text
control's -- a button has no value the user edits, so none of the built-in
constraints that depend on a value (required, pattern,
minlength/maxlength, and so on) apply to it at all. The
only way a button ever becomes invalid is an explicit
setCustomValidity(String) call.
Whether a button is even a candidate for validation in the first place
depends entirely on its effective type: only type="submit"
(the default, if no type attribute is present) participates.
type="button" and type="reset" are both barred --
willValidate() is false for either, exactly as for a
disabled control, and for the same reason checkValidity() ignores
any custom validity message set on such a button.
final HtmlButton plain = page.getHtmlElementById("plainBtn"); // <button type="button">
plain.setCustomValidity("this will never surface");
System.out.println(plain.willValidate()); // false
System.out.println(plain.checkValidity()); // true -- barred, so the custom message is ignored
HtmlButton has no standard readonly attribute at all (per
spec, only text-like inputs and <textarea> support it) -- if
isReadOnly() reports true for a button in your markup,
treat that as informational only; it has no effect on willValidate() or
any other real browser behavior.
Because a button carries no editable value, reset(),
setDefaultValue(String) and getDefaultValue() are all
intentionally no-ops on HtmlButton -- this is by design, not a missing
feature, matching the fact that a real button genuinely has nothing to reset. A
button's value attribute (accessible via getValueAttribute()
/ setValueAttribute(String)) is submitted as part of the form data when
the button triggers submission, but it isn't something a user edits the way a text
input's value is, so there's no dirty-flag concept for it at all.
HtmlSelect: validation specifics
HtmlSelect has its own value-and-selection model, since a
<select> doesn't hold a single text value the way an
<input> does -- it holds a set of <option>
children, zero or more of which are selected.
getOptions(), getOptionSize(), getOption(int)
and getSelectedOptions() all search the entire descendant tree
of the select, not just its direct children -- so options grouped inside one or more
<optgroup> elements are found correctly, exactly as in a real
browser:
<select id="country">
<optgroup label="Europe">
<option value="de">Germany</option>
<option value="fr">France</option>
</optgroup>
<optgroup label="Asia">
<option value="jp">Japan</option>
</optgroup>
</select>
final HtmlSelect select = page.getHtmlElementById("country");
System.out.println(select.getOptionSize()); // 3 -- both optgroups are searched
getSelectedOptions() behaves differently depending on whether the
select allows multiple selections (isMultipleSelectEnabled()):
- in multiple-select mode, it returns every option whose
isSelected() is true, in document order;
- in single-select mode, if more than one option is (erroneously)
marked selected, only the last one encountered in document order is
returned; if nothing at all is currently selected, an empty list
is returned -- it does not fall back to "the first option" on its own.
That last point is worth calling out explicitly: a single-select <select>
with at least one option is normally never in a truly unselected state, because
HtmlUnit automatically selects the first option for you once, when the page finishes
loading (if nothing else was explicitly selected), and again automatically after
removing or replacing options via removeOption()/
replaceOption(). But if every option is deselected some other way (for
example, calling setSelectedIndex(-1) directly, or removing the
selected state from script), getSelectedOptions() will
genuinely return an empty list until something re-establishes a selection.
final HtmlSelect select = page.getHtmlElementById("country");
// right after page load, single-select mode: the first option is
// auto-selected if nothing else was
System.out.println(select.getSelectedOptions()); // [Germany]
select.setSelectedIndex(-1);
System.out.println(select.getSelectedOptions()); // [] -- empty, not [Germany]
select.setSelectedIndex(2);
System.out.println(select.getSelectedOptions()); // [Japan]
getSelectedIndex() / setSelectedIndex(int) give you the
same information as a plain integer index into getOptions() rather than
a list of HtmlOption objects; getSelectedIndex() returns
-1 when nothing is selected, matching the empty-list case above.
Setting a negative or out-of-range index simply results in nothing being selected.
HtmlSelect: the size attribute and default selection
getSize() returns the parsed size attribute as an
int, defaulting to 0 if the attribute is absent or isn't a
valid non-negative integer. This intentionally diverges from the HTML specification
itself, which defines the default rendered size as 1 for a
single select and 4 for a multiple select when the attribute is absent
-- getSize() is a raw attribute accessor, not a rendering computation, so
treat a return value of 0 as "no explicit size was set", not as a claim
about how the control would actually be drawn.
Whether a select gets a default selection at all depends on both
isMultipleSelectEnabled() and this size value: HtmlUnit auto-selects the
first option, once, when the page finishes loading, only if the select is in
single-select mode and its size is 1 or less (i.e. it renders as
a dropdown, not a scrollable list box) and nothing was already explicitly
selected in the markup.
HtmlSelect and required
A <select required> follows the same
isValueMissingValidityState() mechanism described earlier, but the rule
for what counts as "missing" is specific to how a select's default selection works.
It only applies to a single-select select with size 1
or less (the same rendering condition that triggers auto-selection, described above):
the value is considered missing if the option that ends up selected is the
first option in the list, and that first option's effective value is the
empty string, with nothing else explicitly selected instead.
<select required>
<option value=""></option>
<option value="a">Option A</option>
</select>
For the select above, with nothing explicitly selected, the auto-selected first
option has an empty value -- so isValid() is false and
checkValidity() reports a missing value, exactly like a browser's native
validation would show a "please select an item" message. Explicitly selecting the
second option (either in markup with selected, or via
setSelectedIndex(1)) clears the missing-value state immediately.
A multiple-select, or a single-select with size greater than
1, is only considered to have a missing value if literally nothing
is selected at all -- there is no "empty first option" special case for those, since
browsers don't auto-select anything for them in the first place.