Skip to content

Commit 32cc842

Browse files
audreyfeldroypydanny
authored andcommitted
Test required bool coercion, multi-bool forms, and widget rendering paths
Four tests that close the gaps identified during expert review: - Required bool with no default validates as False when unchecked (documents that the coercion makes bool validation errors impossible) - Two bool fields in one form, one checked and one unchecked (confirms the coercion loop handles each field independently) - Slider widget renders as range input with value attribute - Rich text widget renders as textarea element
1 parent 8213dff commit 32cc842

File tree

1 file changed

+58
-0
lines changed

1 file changed

+58
-0
lines changed

tests/test_form.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,22 @@ class WaiverForm(AirForm[WaiverModel]):
538538
assert form.data.accepted is False
539539

540540

541+
def test_validate_required_bool_unchecked_is_false() -> None:
542+
"""Required bool with no default still validates; unchecked means False."""
543+
544+
class AgreementModel(BaseModel):
545+
name: str
546+
agreed: bool
547+
548+
class AgreementForm(AirForm[AgreementModel]):
549+
pass
550+
551+
form = AgreementForm()
552+
form.validate({"name": "Audrey"})
553+
assert form.is_valid
554+
assert form.data.agreed is False
555+
556+
541557
def test_validate_checked_checkbox_is_true() -> None:
542558
"""Checked checkboxes send 'on'; Pydantic coerces to True."""
543559

@@ -595,6 +611,48 @@ class ConsentForm(AirForm[ConsentModel]):
595611
assert form.data.agreed is True
596612

597613

614+
def test_validate_two_bools_one_checked_one_unchecked() -> None:
615+
"""Each bool field is coerced independently."""
616+
617+
class PrefsModel(BaseModel):
618+
name: str
619+
newsletter: bool = AirField(default=False)
620+
terms: bool = AirField(default=False)
621+
622+
class PrefsForm(AirForm[PrefsModel]):
623+
pass
624+
625+
form = PrefsForm()
626+
form.validate({"name": "Audrey", "newsletter": "on"})
627+
assert form.is_valid
628+
assert form.data.newsletter is True
629+
assert form.data.terms is False
630+
631+
632+
def test_render_slider_widget() -> None:
633+
"""widget='slider' renders as a range input with value, not checked."""
634+
635+
class MixerModel(BaseModel):
636+
volume: int = AirField(default=50, widget="slider", label="Volume")
637+
638+
html = default_form_widget(model=MixerModel, data={"volume": 75})
639+
assert 'type="range"' in html
640+
assert 'value="75"' in html
641+
assert "checked" not in html
642+
643+
644+
def test_render_rich_text_widget_as_textarea() -> None:
645+
"""widget='rich_text' renders as a textarea element."""
646+
647+
class PostModel(BaseModel):
648+
body: str = AirField(widget="rich_text", label="Body")
649+
650+
html = default_form_widget(model=PostModel, data={"body": "Hello"})
651+
assert "<textarea" in html
652+
assert "Hello</textarea>" in html
653+
assert "<input" not in html
654+
655+
598656
def test_render_optional_not_required() -> None:
599657
class CompanionModel(BaseModel):
600658
catchphrase: str | None = None

0 commit comments

Comments
 (0)