Skip to content

Commit 7ea6042

Browse files
committed
fixed checkValidity()/reportValidity() for HtmlSelect
1 parent 7e52dbf commit 7ea6042

4 files changed

Lines changed: 323 additions & 4 deletions

File tree

src/changes/changes.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
</action>
2424
<action type="fix" dev="rbri">
2525
Fixed checkValidity()/reportValidity() and the CSS :valid/:invalid pseudo-classes
26-
for HtmlButtons, HtmlTextarea, HtmlInput's, HtmlObject, HtmlOutput.
26+
for HtmlButtons, HtmlTextarea, HtmlInput's, HtmlObject, HtmlOutput, HtmlSelect.
2727
</action>
2828
<action type="fix" dev="rbri">
2929
HtmlFileInput.reset(): a form reset no longer tries to reconstruct a fake File from the

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

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -756,6 +756,9 @@ public boolean isCustomErrorValidityState() {
756756
return !StringUtils.isEmptyOrNull(customValidity_);
757757
}
758758

759+
/**
760+
* {@inheritDoc}
761+
*/
759762
@Override
760763
public boolean isValidValidityState() {
761764
return !isCustomErrorValidityState()
@@ -767,7 +770,25 @@ public boolean isValidValidityState() {
767770
*/
768771
@Override
769772
public boolean isValueMissingValidityState() {
770-
return ATTRIBUTE_NOT_DEFINED != getAttributeDirect(ATTRIBUTE_REQUIRED)
771-
&& getSelectedOptions().isEmpty();
773+
if (ATTRIBUTE_NOT_DEFINED == getAttributeDirect(ATTRIBUTE_REQUIRED)) {
774+
return false;
775+
}
776+
777+
final List<HtmlOption> selected = getSelectedOptions();
778+
if (selected.isEmpty()) {
779+
return true;
780+
}
781+
782+
// per spec, this only applies to single-selection selects; a multi-select
783+
// or size>1 select with at least one option selected is never "missing"
784+
if (!isMultipleSelectEnabled() && getSize() <= 1) {
785+
final List<HtmlOption> options = getOptions();
786+
return !options.isEmpty()
787+
&& selected.size() == 1
788+
&& selected.get(0) == options.get(0)
789+
&& selected.get(0).getValueAttribute().isEmpty();
790+
}
791+
792+
return false;
772793
}
773794
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,8 @@ public HTMLFormElement getForm() {
400400
*/
401401
@JsxFunction
402402
public boolean checkValidity() {
403-
return getDomNodeOrDie().isValid();
403+
final HtmlSelect input = getDomNodeOrDie();
404+
return !input.willValidate() || input.isValid();
404405
}
405406

406407
/**

src/test/java/org/htmlunit/javascript/host/html/HTMLSelectElementTest.java

Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2907,4 +2907,301 @@ public void willValidate() throws Exception {
29072907

29082908
loadPageVerifyTitle2(html);
29092909
}
2910+
2911+
/**
2912+
* @throws Exception if an error occurs
2913+
*/
2914+
@Test
2915+
@Alerts({"true", "true", "true", "true", "true"})
2916+
public void checkValidityMirrorsWillValidateAcrossAllCases() throws Exception {
2917+
final String html = DOCTYPE_HTML
2918+
+ "<html><head>\n"
2919+
+ " <script>\n"
2920+
+ LOG_TITLE_FUNCTION
2921+
+ " function test() {\n"
2922+
+ " log(document.getElementById('i1').checkValidity());\n"
2923+
+ " log(document.getElementById('i2').checkValidity());\n"
2924+
+ " log(document.getElementById('i3').checkValidity());\n"
2925+
+ " log(document.getElementById('i4').checkValidity());\n"
2926+
+ " log(document.getElementById('i5').checkValidity());\n"
2927+
+ " }\n"
2928+
+ " </script>\n"
2929+
+ "</head>\n"
2930+
+ "<body onload='test()'>\n"
2931+
+ " <form>\n"
2932+
+ " <select id='i1'></select>"
2933+
+ " <select id='i2' disabled></select>"
2934+
+ " <select id='i3' hidden></select>"
2935+
+ " <select id='i4' readonly></select>"
2936+
+ " <select id='i5' style='display: none'></select>"
2937+
+ " </form>\n"
2938+
+ "</body></html>";
2939+
2940+
loadPageVerifyTitle2(html);
2941+
}
2942+
2943+
/**
2944+
* An editable select with a custom validity message must report
2945+
* checkValidity() false.
2946+
* @throws Exception if an error occurs
2947+
*/
2948+
@Test
2949+
@Alerts({"true", "true", "false", "false"})
2950+
public void setCustomValidityOnEditableSelect_isInvalid() throws Exception {
2951+
final String html = DOCTYPE_HTML
2952+
+ "<html><head>\n"
2953+
+ " <script>\n"
2954+
+ LOG_TITLE_FUNCTION
2955+
+ " function test() {\n"
2956+
+ " var s = document.getElementById('s');\n"
2957+
+ " s.setCustomValidity('some error');\n"
2958+
+ " log(s.willValidate);\n"
2959+
+ " log(s.validity.customError);\n"
2960+
+ " log(s.validity.valid);\n"
2961+
+ " log(s.checkValidity());\n"
2962+
+ " }\n"
2963+
+ " </script>\n"
2964+
+ "</head>\n"
2965+
+ "<body onload='test()'>\n"
2966+
+ " <form>\n"
2967+
+ " <select id='s'><option value='a'>a</option></select>"
2968+
+ " </form>\n"
2969+
+ "</body></html>";
2970+
2971+
loadPageVerifyTitle2(html);
2972+
}
2973+
2974+
/**
2975+
* A DISABLED select with a custom validity message must still
2976+
* report checkValidity() true -- disabled bars it from constraint
2977+
* validation entirely, so the custom error must not surface through
2978+
* checkValidity(), even though willValidate is already known to be false
2979+
* for this case.
2980+
* @throws Exception if an error occurs
2981+
*/
2982+
@Test
2983+
@Alerts({"false", "true", "false", "true"})
2984+
public void setCustomValidityOnDisabledSelect_notInvalid() throws Exception {
2985+
final String html = DOCTYPE_HTML
2986+
+ "<html><head>\n"
2987+
+ " <script>\n"
2988+
+ LOG_TITLE_FUNCTION
2989+
+ " function test() {\n"
2990+
+ " var s = document.getElementById('s');\n"
2991+
+ " s.setCustomValidity('some error');\n"
2992+
+ " log(s.willValidate);\n"
2993+
+ " log(s.validity.customError);\n"
2994+
+ " log(s.validity.valid);\n"
2995+
+ " log(s.checkValidity());\n"
2996+
+ " }\n"
2997+
+ " </script>\n"
2998+
+ "</head>\n"
2999+
+ "<body onload='test()'>\n"
3000+
+ " <form>\n"
3001+
+ " <select id='s' disabled><option value='a'>a</option></select>"
3002+
+ " </form>\n"
3003+
+ "</body></html>";
3004+
3005+
loadPageVerifyTitle2(html);
3006+
}
3007+
3008+
/**
3009+
* A required select with no option selected (valueMissing) is
3010+
* genuinely invalid when editable, but must report checkValidity() true if
3011+
* ALSO disabled -- confirms the disabled-barring check takes priority over
3012+
* an actual constraint violation, not just over a custom validity message.
3013+
* @throws Exception if an error occurs
3014+
*/
3015+
@Test
3016+
@Alerts({"false", "true"})
3017+
public void requiredNoSelectionDisabledSelect_checkValidityTrue() throws Exception {
3018+
final String html = DOCTYPE_HTML
3019+
+ "<html><head>\n"
3020+
+ " <script>\n"
3021+
+ LOG_TITLE_FUNCTION
3022+
+ " function test() {\n"
3023+
+ " var editable = document.getElementById('editable');\n"
3024+
+ " var disabled = document.getElementById('disabled');\n"
3025+
+ " log(editable.checkValidity());\n"
3026+
+ " log(disabled.checkValidity());\n"
3027+
+ " }\n"
3028+
+ " </script>\n"
3029+
+ "</head>\n"
3030+
+ "<body onload='test()'>\n"
3031+
+ " <form>\n"
3032+
+ " <select id='editable' required>"
3033+
+ " <option value=''></option>"
3034+
+ " <option value='a'>a</option>"
3035+
+ " </select>"
3036+
+ " <select id='disabled' required disabled>"
3037+
+ " <option value=''></option>"
3038+
+ " <option value='a'>a</option>"
3039+
+ " </select>"
3040+
+ " </form>\n"
3041+
+ "</body></html>";
3042+
3043+
loadPageVerifyTitle2(html);
3044+
}
3045+
3046+
/**
3047+
* A required-select validity without any custom validity involved:
3048+
* confirms .validity.valueMissing/.valid react correctly to selecting a
3049+
* real option vs. leaving the empty placeholder selected.
3050+
* @throws Exception if an error occurs
3051+
*/
3052+
@Test
3053+
@Alerts({"true", "false", "false", "true"})
3054+
public void validityValueMissingForRequiredSelectWithNoSelection() throws Exception {
3055+
final String html = DOCTYPE_HTML
3056+
+ "<html><head>\n"
3057+
+ " <script>\n"
3058+
+ LOG_TITLE_FUNCTION
3059+
+ " function test() {\n"
3060+
+ " var s = document.getElementById('s');\n"
3061+
+ " log(s.validity.valueMissing);\n"
3062+
+ " log(s.validity.valid);\n"
3063+
+ " s.value = 'a';\n"
3064+
+ " log(s.validity.valueMissing);\n"
3065+
+ " log(s.validity.valid);\n"
3066+
+ " }\n"
3067+
+ " </script>\n"
3068+
+ "</head>\n"
3069+
+ "<body onload='test()'>\n"
3070+
+ " <form>\n"
3071+
+ " <select id='s' required>"
3072+
+ " <option value=''></option>"
3073+
+ " <option value='a'>a</option>"
3074+
+ " </select>"
3075+
+ " </form>\n"
3076+
+ "</body></html>";
3077+
3078+
loadPageVerifyTitle2(html);
3079+
}
3080+
3081+
/**
3082+
* Clearing a previously-set custom validity message (empty string) must
3083+
* restore validity for an editable select -- confirms setCustomValidity is
3084+
* reversible, not just settable.
3085+
* @throws Exception if an error occurs
3086+
*/
3087+
@Test
3088+
@Alerts({"false", "false", "true"})
3089+
public void clearCustomValidity_restoresValid() throws Exception {
3090+
final String html = DOCTYPE_HTML
3091+
+ "<html><head>\n"
3092+
+ " <script>\n"
3093+
+ LOG_TITLE_FUNCTION
3094+
+ " function test() {\n"
3095+
+ " var s = document.getElementById('s');\n"
3096+
+ " s.setCustomValidity('some error');\n"
3097+
+ " log(s.validity.valid);\n"
3098+
+ " s.setCustomValidity('');\n"
3099+
+ " log(s.validity.customError);\n"
3100+
+ " log(s.validity.valid);\n"
3101+
+ " }\n"
3102+
+ " </script>\n"
3103+
+ "</head>\n"
3104+
+ "<body onload='test()'>\n"
3105+
+ " <form>\n"
3106+
+ " <select id='s'><option value='a'>a</option></select>"
3107+
+ " </form>\n"
3108+
+ "</body></html>";
3109+
3110+
loadPageVerifyTitle2(html);
3111+
}
3112+
3113+
// /**
3114+
// * The validationMessage should reflect the custom validity message for a
3115+
// * validation-participating (editable) select, and should be empty for a
3116+
// * barred-from-validation (disabled) one, regardless of a custom message
3117+
// * being set.
3118+
// * @throws Exception if an error occurs
3119+
// */
3120+
// @Test
3121+
// @Alerts({"editable error", ""})
3122+
// public void validationMessageReflectsCustomValidityWhereApplicable() throws Exception {
3123+
// final String html = DOCTYPE_HTML
3124+
// + "<html><head>\n"
3125+
// + " <script>\n"
3126+
// + LOG_TITLE_FUNCTION
3127+
// + " function test() {\n"
3128+
// + " var editable = document.getElementById('editable');\n"
3129+
// + " var disabled = document.getElementById('disabled');\n"
3130+
// + " editable.setCustomValidity('editable error');\n"
3131+
// + " disabled.setCustomValidity('disabled error');\n"
3132+
// + " log(editable.validationMessage);\n"
3133+
// + " log(disabled.validationMessage);\n"
3134+
// + " }\n"
3135+
// + " </script>\n"
3136+
// + "</head>\n"
3137+
// + "<body onload='test()'>\n"
3138+
// + " <form>\n"
3139+
// + " <select id='editable'><option value='a'>a</option></select>"
3140+
// + " <select id='disabled' disabled><option value='a'>a</option></select>"
3141+
// + " </form>\n"
3142+
// + "</body></html>";
3143+
//
3144+
// loadPageVerifyTitle2(html);
3145+
// }
3146+
3147+
/**
3148+
* @throws Exception if an error occurs
3149+
*/
3150+
@Test
3151+
@Alerts({"false", "false"})
3152+
public void reportValidityMatchesCheckValidity() throws Exception {
3153+
final String html = DOCTYPE_HTML
3154+
+ "<html><head>\n"
3155+
+ " <script>\n"
3156+
+ LOG_TITLE_FUNCTION
3157+
+ " function test() {\n"
3158+
+ " var s = document.getElementById('s');\n"
3159+
+ " s.setCustomValidity('some error');\n"
3160+
+ " log(s.checkValidity());\n"
3161+
+ " log(s.reportValidity());\n"
3162+
+ " }\n"
3163+
+ " </script>\n"
3164+
+ "</head>\n"
3165+
+ "<body onload='test()'>\n"
3166+
+ " <form>\n"
3167+
+ " <select id='s'><option value='a'>a</option></select>"
3168+
+ " </form>\n"
3169+
+ "</body></html>";
3170+
3171+
loadPageVerifyTitle2(html);
3172+
}
3173+
3174+
/**
3175+
* A DISABLED select inside a disabled fieldset (propagated disabling,
3176+
* rather than the select's own 'disabled' attribute) must also report
3177+
* checkValidity() true despite a custom validity message -- confirms the
3178+
* barring check consults isDisabled()'s ancestor-propagated result, not
3179+
* just the select's own attribute presence.
3180+
* @throws Exception if an error occurs
3181+
*/
3182+
@Test
3183+
@Alerts({"false", "true"})
3184+
public void setCustomValidityOnSelectDisabledViaFieldset_notInvalid() 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 s = document.getElementById('s');\n"
3191+
+ " s.setCustomValidity('some error');\n"
3192+
+ " log(s.willValidate);\n"
3193+
+ " log(s.checkValidity());\n"
3194+
+ " }\n"
3195+
+ " </script>\n"
3196+
+ "</head>\n"
3197+
+ "<body onload='test()'>\n"
3198+
+ " <form>\n"
3199+
+ " <fieldset disabled>\n"
3200+
+ " <select id='s'><option value='a'>a</option></select>\n"
3201+
+ " </fieldset>\n"
3202+
+ " </form>\n"
3203+
+ "</body></html>";
3204+
3205+
loadPageVerifyTitle2(html);
3206+
}
29103207
}

0 commit comments

Comments
 (0)