Skip to content

Commit e649584

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

5 files changed

Lines changed: 569 additions & 4 deletions

File tree

src/changes/changes.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
<body>
1010
<release version="5.4.0" date="August xx, 2026" description="Firefox 153, Bugfixes">
1111
<action type="fix" dev="rbri">
12-
Fixed checkValidity()/reportValidity() and the CSS :valid/:invalid pseudo-classes for HtmlButtons.
12+
Fixed checkValidity()/reportValidity() and the CSS :valid/:invalid pseudo-classes
13+
for HtmlButtons, HtmlTextarea.
1314
</action>
1415
<action type="fix" dev="rbri">
1516
HtmlFileInput.reset(): a form reset no longer tries to reconstruct a fake File from the

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,8 @@ public NodeList getLabels() {
374374
*/
375375
@JsxFunction
376376
public boolean checkValidity() {
377-
return getDomNodeOrDie().isValid();
377+
final HtmlTextArea textArea = getDomNodeOrDie();
378+
return !textArea.willValidate() || textArea.isValid();
378379
}
379380

380381
/**

src/test/java/org/htmlunit/html/HtmlFileInputTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1044,7 +1044,7 @@ public void valueWebkitdirectory() throws Exception {
10441044
* @throws Exception if the test fails
10451045
*/
10461046
@Test
1047-
@Alerts({"0-1"})
1047+
@Alerts("0-1")
10481048
public void resetClearsRealFileSelectionWithoutSpuriousChange() throws Exception {
10491049
final String html = DOCTYPE_HTML
10501050
+ "<html><body>\n"
@@ -1083,7 +1083,7 @@ public void resetClearsRealFileSelectionWithoutSpuriousChange() throws Exception
10831083
* @throws Exception if the test fails
10841084
*/
10851085
@Test
1086-
@Alerts({"0"})
1086+
@Alerts("0")
10871087
public void resetOnNeverTouchedFileInput_noChangeEventFired() throws Exception {
10881088
final String html = DOCTYPE_HTML
10891089
+ "<html><head>\n"

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

Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2878,4 +2878,328 @@ public void cssMatchingUpdatesWhenTypeChangesFromSubmitToButton() throws Excepti
28782878

28792879
loadPageVerifyTitle2(html);
28802880
}
2881+
2882+
/**
2883+
* A textarea with no 'required' and no custom validity issue
2884+
* matches :valid and not :invalid.
2885+
* @throws Exception if the test fails
2886+
*/
2887+
@Test
2888+
@Alerts({"true", "false"})
2889+
public void cssValidMatchesTextareaWithNoError() throws Exception {
2890+
final String html = DOCTYPE_HTML
2891+
+ "<html><head>\n"
2892+
+ "<script>\n"
2893+
+ LOG_TITLE_FUNCTION
2894+
+ " function test() {\n"
2895+
+ " var t = document.getElementById('t');\n"
2896+
+ " log(t.matches(':valid'));\n"
2897+
+ " log(t.matches(':invalid'));\n"
2898+
+ " }\n"
2899+
+ "</script></head>\n"
2900+
+ "<body onload='test()'>\n"
2901+
+ " <form>\n"
2902+
+ " <textarea id='t'>content</textarea>\n"
2903+
+ " </form>\n"
2904+
+ "</body></html>";
2905+
2906+
loadPageVerifyTitle2(html);
2907+
}
2908+
2909+
/**
2910+
* A textarea with a custom validity message matches :invalid and not
2911+
* :valid.
2912+
* @throws Exception if the test fails
2913+
*/
2914+
@Test
2915+
@Alerts({"false", "true"})
2916+
public void cssInvalidMatchesTextareaWithCustomValidity() throws Exception {
2917+
final String html = DOCTYPE_HTML
2918+
+ "<html><head>\n"
2919+
+ "<script>\n"
2920+
+ LOG_TITLE_FUNCTION
2921+
+ " function test() {\n"
2922+
+ " var t = document.getElementById('t');\n"
2923+
+ " t.setCustomValidity('some error');\n"
2924+
+ " log(t.matches(':valid'));\n"
2925+
+ " log(t.matches(':invalid'));\n"
2926+
+ " }\n"
2927+
+ "</script></head>\n"
2928+
+ "<body onload='test()'>\n"
2929+
+ " <form>\n"
2930+
+ " <textarea id='t'>content</textarea>\n"
2931+
+ " </form>\n"
2932+
+ "</body></html>";
2933+
2934+
loadPageVerifyTitle2(html);
2935+
}
2936+
2937+
/**
2938+
* A required, empty textarea matches :invalid (valueMissing).
2939+
* @throws Exception if the test fails
2940+
*/
2941+
@Test
2942+
@Alerts({"false", "true"})
2943+
public void cssInvalidMatchesRequiredEmptyTextarea() throws Exception {
2944+
final String html = DOCTYPE_HTML
2945+
+ "<html><head>\n"
2946+
+ "<script>\n"
2947+
+ LOG_TITLE_FUNCTION
2948+
+ " function test() {\n"
2949+
+ " var t = document.getElementById('t');\n"
2950+
+ " log(t.matches(':valid'));\n"
2951+
+ " log(t.matches(':invalid'));\n"
2952+
+ " }\n"
2953+
+ "</script></head>\n"
2954+
+ "<body onload='test()'>\n"
2955+
+ " <form>\n"
2956+
+ " <textarea id='t' required></textarea>\n"
2957+
+ " </form>\n"
2958+
+ "</body></html>";
2959+
2960+
loadPageVerifyTitle2(html);
2961+
}
2962+
2963+
/**
2964+
* A required, non-empty textarea matches :valid.
2965+
* @throws Exception if the test fails
2966+
*/
2967+
@Test
2968+
@Alerts({"true", "false"})
2969+
public void cssValidMatchesRequiredNonEmptyTextarea() throws Exception {
2970+
final String html = DOCTYPE_HTML
2971+
+ "<html><head>\n"
2972+
+ "<script>\n"
2973+
+ LOG_TITLE_FUNCTION
2974+
+ " function test() {\n"
2975+
+ " var t = document.getElementById('t');\n"
2976+
+ " log(t.matches(':valid'));\n"
2977+
+ " log(t.matches(':invalid'));\n"
2978+
+ " }\n"
2979+
+ "</script></head>\n"
2980+
+ "<body onload='test()'>\n"
2981+
+ " <form>\n"
2982+
+ " <textarea id='t' required>content</textarea>\n"
2983+
+ " </form>\n"
2984+
+ "</body></html>";
2985+
2986+
loadPageVerifyTitle2(html);
2987+
}
2988+
2989+
/**
2990+
* A DISABLED, required,
2991+
* empty textarea must match NEITHER :valid NOR :invalid -- disabled bars it
2992+
* from constraint validation entirely, regardless of the would-be
2993+
* valueMissing violation.
2994+
* @throws Exception if the test fails
2995+
*/
2996+
@Test
2997+
@Alerts({"false", "false"})
2998+
public void cssNeitherMatchesDisabledRequiredEmptyTextarea() throws Exception {
2999+
final String html = DOCTYPE_HTML
3000+
+ "<html><head>\n"
3001+
+ "<script>\n"
3002+
+ LOG_TITLE_FUNCTION
3003+
+ " function test() {\n"
3004+
+ " var t = document.getElementById('t');\n"
3005+
+ " log(t.matches(':valid'));\n"
3006+
+ " log(t.matches(':invalid'));\n"
3007+
+ " }\n"
3008+
+ "</script></head>\n"
3009+
+ "<body onload='test()'>\n"
3010+
+ " <form>\n"
3011+
+ " <textarea id='t' required disabled></textarea>\n"
3012+
+ " </form>\n"
3013+
+ "</body></html>";
3014+
3015+
loadPageVerifyTitle2(html);
3016+
}
3017+
3018+
/**
3019+
* A READONLY, required, empty textarea. Per spec, readonly bars an element
3020+
* from constraint validation the same way disabled does.
3021+
* @throws Exception if the test fails
3022+
*/
3023+
@Test
3024+
@Alerts({"false", "false"})
3025+
public void cssNeitherMatchesReadonlyRequiredEmptyTextarea() throws Exception {
3026+
final String html = DOCTYPE_HTML
3027+
+ "<html><head>\n"
3028+
+ "<script>\n"
3029+
+ LOG_TITLE_FUNCTION
3030+
+ " function test() {\n"
3031+
+ " var t = document.getElementById('t');\n"
3032+
+ " log(t.matches(':valid'));\n"
3033+
+ " log(t.matches(':invalid'));\n"
3034+
+ " }\n"
3035+
+ "</script></head>\n"
3036+
+ "<body onload='test()'>\n"
3037+
+ " <form>\n"
3038+
+ " <textarea id='t' required readonly></textarea>\n"
3039+
+ " </form>\n"
3040+
+ "</body></html>";
3041+
3042+
loadPageVerifyTitle2(html);
3043+
}
3044+
3045+
/**
3046+
* Same as above but with a custom validity message set as well -- readonly
3047+
* barring must take priority over the custom error too, same as disabled
3048+
* does.
3049+
* @throws Exception if the test fails
3050+
*/
3051+
@Test
3052+
@Alerts({"false", "false"})
3053+
public void cssNeitherMatchesReadonlyTextareaEvenWithCustomValidity() throws Exception {
3054+
final String html = DOCTYPE_HTML
3055+
+ "<html><head>\n"
3056+
+ "<script>\n"
3057+
+ LOG_TITLE_FUNCTION
3058+
+ " function test() {\n"
3059+
+ " var t = document.getElementById('t');\n"
3060+
+ " t.setCustomValidity('some error');\n"
3061+
+ " log(t.matches(':valid'));\n"
3062+
+ " log(t.matches(':invalid'));\n"
3063+
+ " }\n"
3064+
+ "</script></head>\n"
3065+
+ "<body onload='test()'>\n"
3066+
+ " <form>\n"
3067+
+ " <textarea id='t' readonly>content</textarea>\n"
3068+
+ " </form>\n"
3069+
+ "</body></html>";
3070+
3071+
loadPageVerifyTitle2(html);
3072+
}
3073+
3074+
/**
3075+
* Redundant-barring sanity check: disabled AND readonly AND required AND a
3076+
* custom validity message, all at once -- still neither pseudo-class should
3077+
* match.
3078+
* @throws Exception if the test fails
3079+
*/
3080+
@Test
3081+
@Alerts({"false", "false"})
3082+
public void cssNeitherMatchesDisabledAndReadonlyTextarea() throws Exception {
3083+
final String html = DOCTYPE_HTML
3084+
+ "<html><head>\n"
3085+
+ "<script>\n"
3086+
+ LOG_TITLE_FUNCTION
3087+
+ " function test() {\n"
3088+
+ " var t = document.getElementById('t');\n"
3089+
+ " t.setCustomValidity('some error');\n"
3090+
+ " log(t.matches(':valid'));\n"
3091+
+ " log(t.matches(':invalid'));\n"
3092+
+ " }\n"
3093+
+ "</script></head>\n"
3094+
+ "<body onload='test()'>\n"
3095+
+ " <form>\n"
3096+
+ " <textarea id='t' required disabled readonly></textarea>\n"
3097+
+ " </form>\n"
3098+
+ "</body></html>";
3099+
3100+
loadPageVerifyTitle2(html);
3101+
}
3102+
3103+
/**
3104+
* Dynamic transition: a required, empty textarea starts out correctly
3105+
* :invalid, then is made readonly at runtime -- must stop matching
3106+
* :invalid (or :valid) once barred, confirming the barred state is
3107+
* evaluated fresh rather than cached from page load.
3108+
* @throws Exception if the test fails
3109+
*/
3110+
@Test
3111+
@Alerts({"false", "true", "false", "false", "false", "true"})
3112+
public void cssMatchingUpdatesWhenReadonlyToggledAtRuntime() throws Exception {
3113+
final String html = DOCTYPE_HTML
3114+
+ "<html><head>\n"
3115+
+ "<script>\n"
3116+
+ LOG_TITLE_FUNCTION
3117+
+ " function test() {\n"
3118+
+ " var t = document.getElementById('t');\n"
3119+
+ " log(t.matches(':valid'));\n"
3120+
+ " log(t.matches(':invalid'));\n"
3121+
3122+
+ " t.readOnly = true;\n"
3123+
+ " log(t.matches(':valid'));\n"
3124+
+ " log(t.matches(':invalid'));\n"
3125+
3126+
+ " t.readOnly = false;\n"
3127+
+ " log(t.matches(':valid'));\n"
3128+
+ " log(t.matches(':invalid'));\n"
3129+
+ " }\n"
3130+
+ "</script></head>\n"
3131+
+ "<body onload='test()'>\n"
3132+
+ " <form>\n"
3133+
+ " <textarea id='t' required></textarea>\n"
3134+
+ " </form>\n"
3135+
+ "</body></html>";
3136+
3137+
loadPageVerifyTitle2(html);
3138+
}
3139+
3140+
/**
3141+
* Dynamic transition, disabled variant: same idea as above but toggling
3142+
* disabled instead of readonly.
3143+
* @throws Exception if the test fails
3144+
*/
3145+
@Test
3146+
@Alerts({"false", "true", "false", "false", "false", "true"})
3147+
public void cssMatchingUpdatesWhenDisabledToggledAtRuntime() throws Exception {
3148+
final String html = DOCTYPE_HTML
3149+
+ "<html><head>\n"
3150+
+ "<script>\n"
3151+
+ LOG_TITLE_FUNCTION
3152+
+ " function test() {\n"
3153+
+ " var t = document.getElementById('t');\n"
3154+
+ " log(t.matches(':valid'));\n"
3155+
+ " log(t.matches(':invalid'));\n"
3156+
3157+
+ " t.disabled = true;\n"
3158+
+ " log(t.matches(':valid'));\n"
3159+
+ " log(t.matches(':invalid'));\n"
3160+
3161+
+ " t.disabled = false;\n"
3162+
+ " log(t.matches(':valid'));\n"
3163+
+ " log(t.matches(':invalid'));\n"
3164+
+ " }\n"
3165+
+ "</script></head>\n"
3166+
+ "<body onload='test()'>\n"
3167+
+ " <form>\n"
3168+
+ " <textarea id='t' required></textarea>\n"
3169+
+ " </form>\n"
3170+
+ "</body></html>";
3171+
3172+
loadPageVerifyTitle2(html);
3173+
}
3174+
3175+
/**
3176+
* Disabled propagated via an enclosing &lt;fieldset disabled&gt; must bar
3177+
* the textarea from validation the same way an own 'disabled' attribute
3178+
* does -- exercises the isDisabled() parent-chain walk in combination with
3179+
* CSS matching, not just the element's own attribute.
3180+
* @throws Exception if the test fails
3181+
*/
3182+
@Test
3183+
@Alerts({"false", "false"})
3184+
public void cssNeitherMatchesTextareaInDisabledFieldset() throws Exception {
3185+
final String html = DOCTYPE_HTML
3186+
+ "<html><head>\n"
3187+
+ "<script>\n"
3188+
+ LOG_TITLE_FUNCTION
3189+
+ " function test() {\n"
3190+
+ " var t = document.getElementById('t');\n"
3191+
+ " log(t.matches(':valid'));\n"
3192+
+ " log(t.matches(':invalid'));\n"
3193+
+ " }\n"
3194+
+ "</script></head>\n"
3195+
+ "<body onload='test()'>\n"
3196+
+ " <form>\n"
3197+
+ " <fieldset disabled>\n"
3198+
+ " <textarea id='t' required></textarea>\n"
3199+
+ " </fieldset>\n"
3200+
+ " </form>\n"
3201+
+ "</body></html>";
3202+
3203+
loadPageVerifyTitle2(html);
3204+
}
28813205
}

0 commit comments

Comments
 (0)