Skip to content

Commit 2a1e4e1

Browse files
committed
fixed checkValidity()/reportValidity() and the CSS :valid/:invalid pseudo-classes for HtmlButtons
1 parent ebdca9d commit 2a1e4e1

6 files changed

Lines changed: 335 additions & 14 deletions

File tree

src/changes/changes.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88

99
<body>
1010
<release version="5.4.0" date="August xx, 2026" description="Firefox 153, Bugfixes">
11+
<action type="fix" dev="rbri">
12+
Fixed checkValidity()/reportValidity() and the CSS :valid/:invalid pseudo-classes for HtmlButtons.
13+
</action>
1114
<action type="fix" dev="rbri">
1215
HtmlFileInput.reset(): a form reset no longer tries to reconstruct a fake File from the
1316
'value' attribute; it now simply clears the selected files, without firing a change event.

src/main/java/org/htmlunit/css/CssStyleSheet.java

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -918,14 +918,22 @@ private static boolean selectsPseudoClass(final BrowserVersion browserVersion,
918918
return true;
919919

920920
case "valid":
921-
if (element instanceof HtmlForm || element instanceof ValidatableElement) {
922-
return ((HtmlElement) element).isValid();
921+
if (element instanceof ValidatableElement validatable) {
922+
return validatable.willValidate()
923+
&& ((HtmlElement) validatable).isValid();
924+
}
925+
else if (element instanceof HtmlForm form) {
926+
return form.isValid();
923927
}
924928
return false;
925929

926930
case "invalid":
927-
if (element instanceof HtmlForm || element instanceof ValidatableElement) {
928-
return !((HtmlElement) element).isValid();
931+
if (element instanceof ValidatableElement validatable) {
932+
return validatable.willValidate()
933+
&& !((HtmlElement) validatable).isValid();
934+
}
935+
else if (element instanceof HtmlForm form) {
936+
return !form.isValid();
929937
}
930938
return false;
931939

src/main/java/org/htmlunit/html/HtmlButton.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -316,10 +316,6 @@ protected boolean isEmptyXmlTagExpanded() {
316316
*/
317317
@Override
318318
public boolean isValid() {
319-
if (TYPE_RESET.equals(getType()) || TYPE_BUTTON.equals(getType())) {
320-
return true;
321-
}
322-
323319
return super.isValid() && !isCustomErrorValidityState();
324320
}
325321

src/main/java/org/htmlunit/javascript/host/html/HTMLButtonElement.java

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ public void setType(final String newType) {
6666
*/
6767
@JsxGetter
6868
public String getType() {
69-
return ((HtmlButton) getDomNodeOrDie()).getType();
69+
return getDomNodeOrDie().getType();
7070
}
7171

7272
/**
@@ -150,7 +150,8 @@ public void setValue(final Object newValue) {
150150
*/
151151
@JsxFunction
152152
public boolean checkValidity() {
153-
return getDomNodeOrDie().isValid();
153+
final HtmlButton button = getDomNodeOrDie();
154+
return !button.willValidate() || button.isValid();
154155
}
155156

156157
/**
@@ -175,13 +176,18 @@ public ValidityState getValidity() {
175176
return validityState;
176177
}
177178

179+
@Override
180+
public HtmlButton getDomNodeOrDie() {
181+
return (HtmlButton) super.getDomNodeOrDie();
182+
}
183+
178184
/**
179185
* Returns whether this element will be validated when the form is submitted.
180186
* @return always {@code false}
181187
*/
182188
@JsxGetter
183189
public boolean isWillValidate() {
184-
return ((HtmlButton) getDomNodeOrDie()).willValidate();
190+
return getDomNodeOrDie().willValidate();
185191
}
186192

187193
/**
@@ -190,7 +196,7 @@ public boolean isWillValidate() {
190196
*/
191197
@JsxFunction
192198
public void setCustomValidity(final String message) {
193-
((HtmlButton) getDomNodeOrDie()).setCustomValidity(message);
199+
getDomNodeOrDie().setCustomValidity(message);
194200
}
195201

196202
/**
@@ -199,7 +205,7 @@ public void setCustomValidity(final String message) {
199205
*/
200206
@JsxGetter
201207
public boolean isFormNoValidate() {
202-
return ((HtmlButton) getDomNodeOrDie()).isFormNoValidate();
208+
return getDomNodeOrDie().isFormNoValidate();
203209
}
204210

205211
/**
@@ -208,6 +214,6 @@ public boolean isFormNoValidate() {
208214
*/
209215
@JsxSetter
210216
public void setFormNoValidate(final boolean value) {
211-
((HtmlButton) getDomNodeOrDie()).setFormNoValidate(value);
217+
getDomNodeOrDie().setFormNoValidate(value);
212218
}
213219
}

src/test/java/org/htmlunit/javascript/host/css/CSSSelectorTest.java

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2612,4 +2612,270 @@ public void buttonTypeInvalid() throws Exception {
26122612

26132613
loadPageVerifyTitle2(html);
26142614
}
2615+
2616+
/**
2617+
* Baseline: a default (type='submit') button with no custom validity issue
2618+
* matches :valid and not :invalid.
2619+
* @throws Exception if the test fails
2620+
*/
2621+
@Test
2622+
@Alerts({"true", "false"})
2623+
public void cssValidMatchesSubmitButtonWithNoError() throws Exception {
2624+
final String html = DOCTYPE_HTML
2625+
+ "<html><head>\n"
2626+
+ "<script>\n"
2627+
+ LOG_TITLE_FUNCTION
2628+
+ " function test() {\n"
2629+
+ " var b = document.getElementById('b');\n"
2630+
+ " log(b.matches(':valid'));\n"
2631+
+ " log(b.matches(':invalid'));\n"
2632+
+ " }\n"
2633+
+ "</script></head>\n"
2634+
+ "<body onload='test()'>\n"
2635+
+ " <form>\n"
2636+
+ " <button id='b' type='submit'>btn</button>\n"
2637+
+ " </form>\n"
2638+
+ "</body></html>";
2639+
2640+
loadPageVerifyTitle2(html);
2641+
}
2642+
2643+
/**
2644+
* A default (type='submit') button with a custom validity message matches
2645+
* :invalid and not :valid.
2646+
* @throws Exception if the test fails
2647+
*/
2648+
@Test
2649+
@Alerts({"false", "true"})
2650+
public void cssInvalidMatchesSubmitButtonWithCustomValidity() throws Exception {
2651+
final String html = DOCTYPE_HTML
2652+
+ "<html><head>\n"
2653+
+ "<script>\n"
2654+
+ LOG_TITLE_FUNCTION
2655+
+ " function test() {\n"
2656+
+ " var b = document.getElementById('b');\n"
2657+
+ " b.setCustomValidity('some error');\n"
2658+
+ " log(b.matches(':valid'));\n"
2659+
+ " log(b.matches(':invalid'));\n"
2660+
+ " }\n"
2661+
+ "</script></head>\n"
2662+
+ "<body onload='test()'>\n"
2663+
+ " <form>\n"
2664+
+ " <button id='b' type='submit'>btn</button>\n"
2665+
+ " </form>\n"
2666+
+ "</body></html>";
2667+
2668+
loadPageVerifyTitle2(html);
2669+
}
2670+
2671+
/**
2672+
* A type='button' element is barred from constraint validation --
2673+
* it must match NEITHER :valid NOR :invalid, even with no custom validity
2674+
* issue at all.
2675+
* @throws Exception if the test fails
2676+
*/
2677+
@Test
2678+
@Alerts({"false", "false"})
2679+
public void cssNeitherMatchesPlainButtonType() throws Exception {
2680+
final String html = DOCTYPE_HTML
2681+
+ "<html><head>\n"
2682+
+ "<script>\n"
2683+
+ LOG_TITLE_FUNCTION
2684+
+ " function test() {\n"
2685+
+ " var b = document.getElementById('b');\n"
2686+
+ " log(b.matches(':valid'));\n"
2687+
+ " log(b.matches(':invalid'));\n"
2688+
+ " }\n"
2689+
+ "</script></head>\n"
2690+
+ "<body onload='test()'>\n"
2691+
+ " <form>\n"
2692+
+ " <button id='b' type='button'>btn</button>\n"
2693+
+ " </form>\n"
2694+
+ "</body></html>";
2695+
2696+
loadPageVerifyTitle2(html);
2697+
}
2698+
2699+
/**
2700+
* Same as above, but ALSO with a custom validity message set -- the barring
2701+
* must take priority over the (otherwise would-be-invalid) custom error;
2702+
* neither pseudo-class should match.
2703+
* @throws Exception if the test fails
2704+
*/
2705+
@Test
2706+
@Alerts({"false", "false"})
2707+
public void cssNeitherMatchesPlainButtonTypeEvenWithCustomValidity() throws Exception {
2708+
final String html = DOCTYPE_HTML
2709+
+ "<html><head>\n"
2710+
+ "<script>\n"
2711+
+ LOG_TITLE_FUNCTION
2712+
+ " function test() {\n"
2713+
+ " var b = document.getElementById('b');\n"
2714+
+ " b.setCustomValidity('some error');\n"
2715+
+ " log(b.matches(':valid'));\n"
2716+
+ " log(b.matches(':invalid'));\n"
2717+
+ " }\n"
2718+
+ "</script></head>\n"
2719+
+ "<body onload='test()'>\n"
2720+
+ " <form>\n"
2721+
+ " <button id='b' type='button'>btn</button>\n"
2722+
+ " </form>\n"
2723+
+ "</body></html>";
2724+
2725+
loadPageVerifyTitle2(html);
2726+
}
2727+
2728+
/**
2729+
* A type='reset' is barred the same way as type='button' -- neither
2730+
* pseudo-class should match, custom validity or not.
2731+
* @throws Exception if the test fails
2732+
*/
2733+
@Test
2734+
@Alerts({"false", "false"})
2735+
public void cssNeitherMatchesResetButtonType() throws Exception {
2736+
final String html = DOCTYPE_HTML
2737+
+ "<html><head>\n"
2738+
+ "<script>\n"
2739+
+ LOG_TITLE_FUNCTION
2740+
+ " function test() {\n"
2741+
+ " var b = document.getElementById('b');\n"
2742+
+ " b.setCustomValidity('some error');\n"
2743+
+ " log(b.matches(':valid'));\n"
2744+
+ " log(b.matches(':invalid'));\n"
2745+
+ " }\n"
2746+
+ "</script></head>\n"
2747+
+ "<body onload='test()'>\n"
2748+
+ " <form>\n"
2749+
+ " <button id='b' type='reset'>btn</button>\n"
2750+
+ " </form>\n"
2751+
+ "</body></html>";
2752+
2753+
loadPageVerifyTitle2(html);
2754+
}
2755+
2756+
/**
2757+
* A DISABLED submit button is also barred from constraint
2758+
* validation -- neither pseudo-class should match, even with a custom
2759+
* validity message set.
2760+
* @throws Exception if the test fails
2761+
*/
2762+
@Test
2763+
@Alerts({"false", "false"})
2764+
public void cssNeitherMatchesDisabledSubmitButton() throws Exception {
2765+
final String html = DOCTYPE_HTML
2766+
+ "<html><head>\n"
2767+
+ "<script>\n"
2768+
+ LOG_TITLE_FUNCTION
2769+
+ " function test() {\n"
2770+
+ " var b = document.getElementById('b');\n"
2771+
+ " b.setCustomValidity('some error');\n"
2772+
+ " log(b.matches(':valid'));\n"
2773+
+ " log(b.matches(':invalid'));\n"
2774+
+ " }\n"
2775+
+ "</script></head>\n"
2776+
+ "<body onload='test()'>\n"
2777+
+ " <form>\n"
2778+
+ " <button id='b' type='submit' disabled>btn</button>\n"
2779+
+ " </form>\n"
2780+
+ "</body></html>";
2781+
2782+
loadPageVerifyTitle2(html);
2783+
}
2784+
2785+
/**
2786+
* Redundant-barring sanity check: type='button' AND disabled AND a custom
2787+
* validity message all at once -- still neither pseudo-class should match.
2788+
* @throws Exception if the test fails
2789+
*/
2790+
@Test
2791+
@Alerts({"false", "false"})
2792+
public void cssNeitherMatchesDisabledPlainButtonWithCustomValidity() throws Exception {
2793+
final String html = DOCTYPE_HTML
2794+
+ "<html><head>\n"
2795+
+ "<script>\n"
2796+
+ LOG_TITLE_FUNCTION
2797+
+ " function test() {\n"
2798+
+ " var b = document.getElementById('b');\n"
2799+
+ " b.setCustomValidity('some error');\n"
2800+
+ " log(b.matches(':valid'));\n"
2801+
+ " log(b.matches(':invalid'));\n"
2802+
+ " }\n"
2803+
+ "</script></head>\n"
2804+
+ "<body onload='test()'>\n"
2805+
+ " <form>\n"
2806+
+ " <button id='b' type='button' disabled>btn</button>\n"
2807+
+ " </form>\n"
2808+
+ "</body></html>";
2809+
2810+
loadPageVerifyTitle2(html);
2811+
}
2812+
2813+
/**
2814+
* Dynamic transition: a button barred via type='button' (with a leftover
2815+
* custom validity message set while barred) is switched to type='submit' at
2816+
* runtime. It must now correctly participate in validation and show
2817+
* :invalid -- confirms the barred/not-barred state is evaluated fresh each
2818+
* time, not cached from whatever it was when setCustomValidity() was
2819+
* originally called.
2820+
* @throws Exception if the test fails
2821+
*/
2822+
@Test
2823+
@Alerts({"false", "false", "false", "true"})
2824+
public void cssMatchingUpdatesWhenTypeChangesFromButtonToSubmit() throws Exception {
2825+
final String html = DOCTYPE_HTML
2826+
+ "<html><head>\n"
2827+
+ "<script>\n"
2828+
+ LOG_TITLE_FUNCTION
2829+
+ " function test() {\n"
2830+
+ " var b = document.getElementById('b');\n"
2831+
+ " b.setCustomValidity('some error');\n"
2832+
+ " log(b.matches(':valid'));\n"
2833+
+ " log(b.matches(':invalid'));\n"
2834+
2835+
+ " b.type = 'submit';\n"
2836+
+ " log(b.matches(':valid'));\n"
2837+
+ " log(b.matches(':invalid'));\n"
2838+
+ " }\n"
2839+
+ "</script></head>\n"
2840+
+ "<body onload='test()'>\n"
2841+
+ " <form>\n"
2842+
+ " <button id='b' type='button'>btn</button>\n"
2843+
+ " </form>\n"
2844+
+ "</body></html>";
2845+
2846+
loadPageVerifyTitle2(html);
2847+
}
2848+
2849+
/**
2850+
* Dynamic transition, the other direction: an initially-valid submit button
2851+
* is switched to type='button' at runtime -- it must stop matching :valid
2852+
* (or :invalid) entirely once barred, even though it was clean before the
2853+
* change.
2854+
* @throws Exception if the test fails
2855+
*/
2856+
@Test
2857+
@Alerts({"true", "false", "false", "false"})
2858+
public void cssMatchingUpdatesWhenTypeChangesFromSubmitToButton() throws Exception {
2859+
final String html = DOCTYPE_HTML
2860+
+ "<html><head>\n"
2861+
+ "<script>\n"
2862+
+ LOG_TITLE_FUNCTION
2863+
+ " function test() {\n"
2864+
+ " var b = document.getElementById('b');\n"
2865+
+ " log(b.matches(':valid'));\n"
2866+
+ " log(b.matches(':invalid'));\n"
2867+
2868+
+ " b.type = 'button';\n"
2869+
+ " log(b.matches(':valid'));\n"
2870+
+ " log(b.matches(':invalid'));\n"
2871+
+ " }\n"
2872+
+ "</script></head>\n"
2873+
+ "<body onload='test()'>\n"
2874+
+ " <form>\n"
2875+
+ " <button id='b' type='submit'>btn</button>\n"
2876+
+ " </form>\n"
2877+
+ "</body></html>";
2878+
2879+
loadPageVerifyTitle2(html);
2880+
}
26152881
}

0 commit comments

Comments
 (0)