Repository: cal-itp/benefits

Issue:
Form field validation: update copy to be more specific on the problem

![image](https://github.com/cal-itp/benefits/assets/25497886/239f1560-3fb5-4031-91f2-944b736620b3)

![image](https://github.com/cal-itp/benefits/assets/25497886/e3ad07da-d36a-40de-9c9c-399d510b722f)


## Acceptance Criteria

- [x] Configure the browser validation message - https://developer.mozilla.org/en-US/docs/Web/HTML/Constraint_validation#controlling_the_text_of_constraint_violation
- [x] Test with different browser locales
- [x] Add custom copy for both Eligibility Index (radio buttons) and Eligibility Confirm (text fields)



Discussion:

Resolving patch:
diff --git a/benefits/eligibility/forms.py b/benefits/eligibility/forms.py
--- a/benefits/eligibility/forms.py
+++ b/benefits/eligibility/forms.py
@@ -30,8 +30,11 @@ def __init__(self, agency: models.TransitAgency, *args, **kwargs):
         self.classes = "col-lg-8"
         # second element is not used since we render the whole label using selection_label_template,
         # therefore set to None
-        self.fields["verifier"].choices = [(v.id, None) for v in verifiers]
-        self.fields["verifier"].widget.selection_label_templates = {v.id: v.selection_label_template for v in verifiers}
+        verifier_field = self.fields["verifier"]
+        verifier_field.choices = [(v.id, None) for v in verifiers]
+        verifier_field.widget.selection_label_templates = {v.id: v.selection_label_template for v in verifiers}
+        verifier_field.widget.attrs.update({"data-custom-validity": _("Please choose a transit benefit.")})
+        self.use_custom_validity = True
 
     def clean(self):
         if not recaptcha.verify(self.data):
@@ -48,11 +51,6 @@ class EligibilityVerificationForm(forms.Form):
     submit_value = _("Find my record")
     submitting_value = _("Checking")
 
-    _error_messages = {
-        "invalid": _("Check your input. The format looks wrong."),
-        "missing": _("This field is required."),
-    }
-
     def __init__(
         self,
         title,
@@ -68,6 +66,8 @@ def __init__(
         sub_input_mode=None,
         sub_max_length=None,
         sub_pattern=None,
+        sub_custom_validity=None,
+        name_custom_validity=None,
         *args,
         **kwargs,
     ):
@@ -117,6 +117,9 @@ def __init__(
             sub_widget.attrs.update({"inputmode": sub_input_mode})
         if sub_max_length:
             sub_widget.attrs.update({"maxlength": sub_max_length})
+        if sub_custom_validity:
+            sub_widget.attrs.update({"data-custom-validity": sub_custom_validity})
+            self.use_custom_validity = True
 
         self.fields["sub"] = forms.CharField(
             label=sub_label,
@@ -127,6 +130,9 @@ def __init__(
         name_widget = widgets.FormControlTextInput(placeholder=name_placeholder)
         if name_max_length:
             name_widget.attrs.update({"maxlength": name_max_length})
+        if name_custom_validity:
+            name_widget.attrs.update({"data-custom-validity": name_custom_validity})
+            self.use_custom_validity = True
 
         self.fields["name"] = forms.CharField(label=name_label, widget=name_widget, help_text=name_help_text)
 
@@ -157,6 +163,8 @@ def __init__(self, *args, **kwargs):
             sub_input_mode="numeric",
             sub_max_length=5,
             sub_pattern=r"\d{5}",
+            sub_custom_validity=_("Please enter a 5-digit number."),
+            name_custom_validity=_("Please enter your last name."),
             *args,
             **kwargs,
         )
@@ -185,6 +193,8 @@ def __init__(self, *args, **kwargs):
             sub_input_mode="numeric",
             sub_max_length=4,
             sub_pattern=r"\d{4}",
+            sub_custom_validity=_("Please enter a 4-digit number."),
+            name_custom_validity=_("Please enter your last name."),
             *args,
             **kwargs,
         )


Test patch:
diff --git a/tests/pytest/eligibility/test_forms.py b/tests/pytest/eligibility/test_forms.py
new file mode 100644
--- /dev/null
+++ b/tests/pytest/eligibility/test_forms.py
@@ -0,0 +1,37 @@
+from benefits.eligibility.forms import MSTCourtesyCard, SBMTDMobilityPass
+
+
+def test_MSTCourtesyCard():
+    form = MSTCourtesyCard(data={"sub": "12345", "name": "Gonzalez"})
+
+    assert form.is_valid()
+
+    sub_attrs = form.fields["sub"].widget.attrs
+    assert sub_attrs["pattern"] == r"\d{5}"
+    assert sub_attrs["inputmode"] == "numeric"
+    assert sub_attrs["maxlength"] == 5
+    assert sub_attrs["data-custom-validity"] == "Please enter a 5-digit number."
+
+    name_attrs = form.fields["name"].widget.attrs
+    assert name_attrs["maxlength"] == 255
+    assert name_attrs["data-custom-validity"] == "Please enter your last name."
+
+    assert form.use_custom_validity
+
+
+def test_SBMTDMobilityPass():
+    form = SBMTDMobilityPass(data={"sub": "1234", "name": "Barbara"})
+
+    assert form.is_valid()
+
+    sub_attrs = form.fields["sub"].widget.attrs
+    assert sub_attrs["pattern"] == r"\d{4}"
+    assert sub_attrs["maxlength"] == 4
+    assert sub_attrs["inputmode"] == "numeric"
+    assert sub_attrs["data-custom-validity"] == "Please enter a 4-digit number."
+
+    name_attrs = form.fields["name"].widget.attrs
+    assert name_attrs["maxlength"] == 255
+    assert name_attrs["data-custom-validity"] == "Please enter your last name."
+
+    assert form.use_custom_validity