""" Comprehensive binding tests for all bindable FastHTML components. This test suite covers: - Input (text) - already tested - Checkbox - already tested - Textarea - Select (single) - Select (multiple) - Range (slider) - Radio buttons - Button - Input with Datalist (combobox) """ from dataclasses import dataclass import pytest from fasthtml.components import ( Input, Label, Textarea, Select, Option, Button, Datalist ) from fasthtml.fastapp import fast_app from myfasthtml.controls.helpers import mk from myfasthtml.core.bindings import Binding from myfasthtml.test.matcher import matches, AttributeForbidden from myfasthtml.test.testclient import MyTestClient @dataclass class Data: value: str = "hello world" @dataclass class NumericData: value: int = 50 @dataclass class BoolData: value: bool = True @dataclass class ListData: value: list = None def __post_init__(self): if self.value is None: self.value = [] @pytest.fixture() def user(): test_app, rt = fast_app(default_hdrs=False) user = MyTestClient(test_app) return user @pytest.fixture() def rt(user): return user.app.route class TestBindingTextarea: """Tests for binding Textarea components.""" def test_i_can_bind_textarea(self, user, rt): """ Textarea should bind bidirectionally with data. Value changes should update the label. """ @rt("/") def index(): data = Data("Initial text") textarea_elt = Textarea(name="textarea_name") label_elt = Label() mk.manage_binding(textarea_elt, Binding(data)) mk.manage_binding(label_elt, Binding(data)) return textarea_elt, label_elt user.open("/") user.should_see("Initial text") testable_textarea = user.find_element("textarea") testable_textarea.send("New multiline\ntext content") user.should_see("New multiline\ntext content") def test_i_can_bind_textarea_with_empty_initial_value(self, user, rt): """ Textarea with empty initial value should update correctly. """ @rt("/") def index(): data = Data("") textarea_elt = Textarea(name="textarea_name") label_elt = Label() mk.manage_binding(textarea_elt, Binding(data)) mk.manage_binding(label_elt, Binding(data)) return textarea_elt, label_elt user.open("/") user.should_see("") # Empty initially testable_textarea = user.find_element("textarea") testable_textarea.send("First content") user.should_see("First content") def test_textarea_append_works_with_binding(self, user, rt): """ Appending text to textarea should trigger binding update. """ @rt("/") def index(): data = Data("Start") textarea_elt = Textarea(name="textarea_name") label_elt = Label() mk.manage_binding(textarea_elt, Binding(data)) mk.manage_binding(label_elt, Binding(data)) return textarea_elt, label_elt user.open("/") user.should_see("Start") testable_textarea = user.find_element("textarea") testable_textarea.append(" + More") user.should_see("Start + More") def test_textarea_clear_works_with_binding(self, user, rt): """ Clearing textarea should update binding to empty string. """ @rt("/") def index(): data = Data("Content to clear") textarea_elt = Textarea(name="textarea_name") label_elt = Label() mk.manage_binding(textarea_elt, Binding(data)) mk.manage_binding(label_elt, Binding(data)) return textarea_elt, label_elt user.open("/") user.should_see("Content to clear") testable_textarea = user.find_element("textarea") testable_textarea.clear() user.should_not_see("Content to clear") class TestBindingSelect: """Tests for binding Select components (single selection).""" def test_i_can_bind_select_single(self, user, rt): """ Single select should bind with data. Selecting an option should update the label. """ @rt("/") def index(): data = Data("option1") select_elt = Select( Option("Option 1", value="option1"), Option("Option 2", value="option2"), Option("Option 3", value="option3"), name="select_name" ) label_elt = Label() mk.manage_binding(select_elt, Binding(data)) mk.manage_binding(label_elt, Binding(data)) return select_elt, label_elt user.open("/") user.should_see("option1") testable_select = user.find_element("select") testable_select.select("option2") user.should_see("option2") testable_select.select("option3") user.should_see("option3") def test_i_can_bind_select_by_text(self, user, rt): """ Selecting by visible text should work with binding. """ @rt("/") def index(): data = Data("opt1") select_elt = Select( Option("First Option", value="opt1"), Option("Second Option", value="opt2"), Option("Third Option", value="opt3"), name="select_name" ) label_elt = Label() mk.manage_binding(select_elt, Binding(data)) mk.manage_binding(label_elt, Binding(data)) return select_elt, label_elt user.open("/") user.should_see("opt1") testable_select = user.find_element("select") testable_select.select_by_text("Second Option") user.should_see("opt2") def test_select_with_default_selected_option(self, user, rt): """ Select with a pre-selected option should initialize correctly. """ @rt("/") def index(): data = Data("option2") select_elt = Select( Option("Option 1", value="option1"), Option("Option 2", value="option2", selected=True), Option("Option 3", value="option3"), name="select_name" ) label_elt = Label() mk.manage_binding(select_elt, Binding(data)) mk.manage_binding(label_elt, Binding(data)) return select_elt, label_elt user.open("/") user.should_see("option2") class TestBindingSelectMultiple: """Tests for binding Select components with multiple selection.""" def test_i_can_bind_select_multiple(self, user, rt): """ Multiple select should bind with list data. Selecting multiple options should update the label. """ @rt("/") def index(): data = ListData(["option1"]) select_elt = Select( Option("Option 1", value="option1"), Option("Option 2", value="option2"), Option("Option 3", value="option3"), name="select_name", multiple=True ) label_elt = Label() mk.manage_binding(select_elt, Binding(data)) mk.manage_binding(label_elt, Binding(data)) return select_elt, label_elt user.open("/") user.should_see("['option1']") testable_select = user.find_element("select") testable_select.select("option2") user.should_see("['option1', 'option2']") testable_select.select("option3") user.should_see("['option1', 'option2', 'option3']") def test_i_can_deselect_from_multiple_select(self, user, rt): """ Deselecting options from multiple select should update binding. """ @rt("/") def index(): data = ListData(["option1", "option2"]) select_elt = Select( Option("Option 1", value="option1"), Option("Option 2", value="option2"), Option("Option 3", value="option3"), name="select_name", multiple=True ) label_elt = Label() mk.manage_binding(select_elt, Binding(data)) mk.manage_binding(label_elt, Binding(data)) return select_elt, label_elt user.open("/") user.should_see("['option1', 'option2']") testable_select = user.find_element("select") testable_select.deselect("option1") user.should_see("['option2']") class TestBindingRange: """Tests for binding Range (slider) components.""" def test_i_can_bind_range(self, user, rt): """ Range input should bind with numeric data. Changing the slider should update the label. """ @rt("/") def index(): data = NumericData(50) range_elt = Input( type="range", name="range_name", min="0", max="100", value="50" ) label_elt = Label() mk.manage_binding(range_elt, Binding(data)) mk.manage_binding(label_elt, Binding(data)) return range_elt, label_elt user.open("/") user.should_see("50") testable_range = user.find_element("input[type='range']") testable_range.set(75) user.should_see("75") testable_range.set(25) user.should_see("25") def test_range_increase_decrease(self, user, rt): """ Increasing and decreasing range should update binding. """ @rt("/") def index(): data = NumericData(50) range_elt = Input( type="range", name="range_name", min="0", max="100", step="10", value="50" ) label_elt = Label() mk.manage_binding(range_elt, Binding(data)) mk.manage_binding(label_elt, Binding(data)) return range_elt, label_elt user.open("/") user.should_see("50") testable_range = user.find_element("input[type='range']") testable_range.increase() user.should_see("60") testable_range.increase() user.should_see("70") testable_range.decrease() user.should_see("60") def test_range_clamping_to_min_max(self, user, rt): """ Range values should be clamped to min/max bounds. """ @rt("/") def index(): data = NumericData(50) range_elt = Input( type="range", name="range_name", min="0", max="100", value="50" ) label_elt = Label() mk.manage_binding(range_elt, Binding(data)) mk.manage_binding(label_elt, Binding(data)) return range_elt, label_elt user.open("/") testable_range = user.find_element("input[type='range']") testable_range.set(150) # Above max user.should_see("100") testable_range.set(-10) # Below min user.should_see("0") class TestBindingRadio: """Tests for binding Radio button components.""" def test_i_can_bind_radio_buttons(self): data = Data() radio1 = Input(type="radio", name="radio_name", value="option1") radio2 = Input(type="radio", name="radio_name", value="option2") radio3 = Input(type="radio", name="radio_name", value="option3") binding = Binding(data) mk.manage_binding(radio1, binding) mk.manage_binding(radio2, Binding(data)) mk.manage_binding(radio3, Binding(data)) res = binding.update({"radio_name": "option1"}) # option1 is selected expected = [ Input(type="radio", name="radio_name", value="option1", checked="true"), Input(AttributeForbidden("checked"), type="radio", name="radio_name", value="option2"), Input(AttributeForbidden("checked"), type="radio", name="radio_name", value="option3"), ] assert matches(res, expected) def test_i_can_bind_radio_buttons_and_label(self, user, rt): """ Radio buttons should bind with data. Selecting a radio should update the label. """ @rt("/") def index(): data = Data() radio1 = Input(type="radio", name="radio_name", value="option1", checked="true") radio2 = Input(type="radio", name="radio_name", value="option2") radio3 = Input(type="radio", name="radio_name", value="option3") label_elt = Label() mk.manage_binding(radio1, Binding(data)) mk.manage_binding(radio2, Binding(data)) mk.manage_binding(radio3, Binding(data)) mk.manage_binding(label_elt, Binding(data)) return radio1, radio2, radio3, label_elt user.open("/") # Select second radio testable_radio2 = user.find_element("input[value='option2']") testable_radio2.select() user.should_see("option2") # Select third radio testable_radio3 = user.find_element("input[value='option3']") testable_radio3.select() user.should_see("option3") def test_radio_initial_state(self, user, rt): """ Radio buttons should initialize with correct checked state. """ @rt("/") def index(): data = Data("option2") radio1 = Input(type="radio", name="radio_name", value="option1") radio2 = Input(type="radio", name="radio_name", value="option2", checked=True) radio3 = Input(type="radio", name="radio_name", value="option3") label_elt = Label() mk.manage_binding(radio1, Binding(data)) mk.manage_binding(radio2, Binding(data)) mk.manage_binding(radio3, Binding(data)) mk.manage_binding(label_elt, Binding(data)) return radio1, radio2, radio3, label_elt user.open("/") user.should_see("option2") class TestBindingButton: """Tests for binding Button components.""" def test_i_can_click_button_with_binding(self, user, rt): """ Clicking a button with HTMX should trigger binding updates. """ @rt("/") def index(): data = Data("initial") button_elt = Button("Click me", hx_post="/update", hx_vals='{"action": "clicked"}') label_elt = Label() mk.manage_binding(button_elt, Binding(data)) mk.manage_binding(label_elt, Binding(data)) return button_elt, label_elt @rt("/update") def update(action: str): data = Data("button clicked") label_elt = Label() mk.manage_binding(label_elt, Binding(data)) return label_elt user.open("/") user.should_see("initial") testable_button = user.find_element("button") testable_button.click() user.should_see("button clicked") def test_button_without_htmx_does_nothing(self, user, rt): """ Button without HTMX should not trigger updates. """ @rt("/") def index(): data = Data("initial") button_elt = Button("Plain button") # No HTMX label_elt = Label() mk.manage_binding(button_elt, Binding(data)) mk.manage_binding(label_elt, Binding(data)) return button_elt, label_elt user.open("/") user.should_see("initial") testable_button = user.find_element("button") result = testable_button.click() assert result is None # No HTMX, no response class TestBindingDatalist: """Tests for binding Input with Datalist (combobox).""" def test_i_can_bind_input_with_datalist(self, user, rt): """ Input with datalist should allow both free text and suggestions. """ @rt("/") def index(): data = Data("") datalist = Datalist( Option(value="suggestion1"), Option(value="suggestion2"), Option(value="suggestion3"), id="suggestions" ) input_elt = Input( name="input_name", list="suggestions" ) label_elt = Label() mk.manage_binding(input_elt, Binding(data)) mk.manage_binding(label_elt, Binding(data)) return input_elt, datalist, label_elt user.open("/") user.should_see("") testable_input = user.find_element("input[list='suggestions']") # Can type free text testable_input.send("custom value") user.should_see("custom value") # Can select from suggestions testable_input.select_suggestion("suggestion2") user.should_see("suggestion2") def test_datalist_suggestions_are_available(self, user, rt): """ Datalist suggestions should be accessible for validation. """ @rt("/") def index(): data = Data("") datalist = Datalist( Option(value="apple"), Option(value="banana"), Option(value="cherry"), id="fruits" ) input_elt = Input( name="input_name", list="fruits" ) label_elt = Label() mk.manage_binding(input_elt, Binding(data)) mk.manage_binding(label_elt, Binding(data)) return input_elt, datalist, label_elt user.open("/") testable_input = user.find_element("input[list='fruits']") # Check that suggestions are available suggestions = testable_input.suggestions assert "apple" in suggestions assert "banana" in suggestions assert "cherry" in suggestions class TestBindingEdgeCases: """Tests for edge cases and special scenarios.""" def test_multiple_components_bind_to_same_data(self, user, rt): """ Multiple different components can bind to the same data object. """ @rt("/") def index(): data = Data("synchronized") input_elt = Input(name="input_name") textarea_elt = Textarea(name="textarea_name") label_elt = Label() mk.manage_binding(input_elt, Binding(data)) mk.manage_binding(textarea_elt, Binding(data)) mk.manage_binding(label_elt, Binding(data)) return input_elt, textarea_elt, label_elt user.open("/") user.should_see("synchronized") # Change via input testable_input = user.find_element("input") testable_input.send("changed via input") user.should_see("changed via input") # Change via textarea testable_textarea = user.find_element("textarea") testable_textarea.send("changed via textarea") user.should_see("changed via textarea") def test_component_without_name_attribute(self, user, rt): """ Component without name attribute should handle gracefully. """ @rt("/") def index(): data = Data("test") # Input without name - should not crash input_elt = Input() # No name attribute label_elt = Label() mk.manage_binding(label_elt, Binding(data)) return input_elt, label_elt user.open("/") user.should_see("test") def test_binding_with_initial_empty_string(self, user, rt): """ Binding should work correctly with empty string initial values. """ @rt("/") def index(): data = Data("") input_elt = Input(name="input_name") label_elt = Label() mk.manage_binding(input_elt, Binding(data)) mk.manage_binding(label_elt, Binding(data)) return input_elt, label_elt user.open("/") testable_input = user.find_element("input") testable_input.send("now has value") user.should_see("now has value") def test_binding_with_special_characters(self, user, rt): """ Binding should handle special characters correctly. """ @rt("/") def index(): data = Data("Hello") input_elt = Input(name="input_name") label_elt = Label() mk.manage_binding(input_elt, Binding(data)) mk.manage_binding(label_elt, Binding(data)) return input_elt, label_elt user.open("/") testable_input = user.find_element("input") testable_input.send("Special: <>&\"'") user.should_see("Special: <>&\"'")