from dataclasses import dataclass import pytest from fasthtml.components import Label, Input from myutils.observable import collect_return_values from myfasthtml.core.bindings import ( BindingsManager, Binding, DetectionMode, UpdateMode, BooleanConverter ) @dataclass class Data: value: str = "Hello World" @pytest.fixture(autouse=True) def reset_binding_manager(): BindingsManager.reset() @pytest.fixture() def data(): return Data() def test_i_can_register_a_binding(data): binding = Binding(data, "value") assert binding.id is not None assert binding.data is data assert binding.data_attr == 'value' def test_i_can_register_a_binding_with_default_attr(data): binding = Binding(data) assert binding.id is not None assert binding.data is data assert binding.data_attr == 'value' def test_i_can_retrieve_a_registered_binding(data): elt = Label("hello", id="label_id") binding = Binding(data).bind_ft(elt, name="label_name") assert BindingsManager.get_binding(binding.id) is binding def test_i_can_reset_bindings(data): elt = Label("hello", id="label_id") Binding(data).bind_ft(elt, name="label_name") assert len(BindingsManager.bindings) != 0 BindingsManager.reset() assert len(BindingsManager.bindings) == 0 def test_i_can_bind_an_element_to_a_binding(data): elt = Label("hello", id="label_id") Binding(data).bind_ft(elt, name="label_name") data.value = "new value" assert elt.children[0] == "new value" assert elt.attrs["hx-swap-oob"] == "true" assert elt.attrs["id"] == "label_id" def test_i_can_bind_an_element_attr_to_a_binding(data): elt = Input(value="some value", id="input_id") Binding(data).bind_ft(elt, name="input_name", attr="value") data.value = "new value" assert elt.attrs["value"] == "new value" assert elt.attrs["hx-swap-oob"] == "true" assert elt.attrs["id"] == "input_id" def test_bound_element_has_an_id(): elt = Label("hello") assert elt.attrs.get("id", None) is None Binding(Data()).bind_ft(elt, name="label_name") assert elt.attrs.get("id", None) is not None def test_i_can_collect_updates_values(data): elt = Label("hello") Binding(data).bind_ft(elt, name="label_name") data.value = "new value" collected = collect_return_values(data) assert collected == [elt] # a second time to ensure no side effect data.value = "another value" collected = collect_return_values(data) assert collected == [elt] def test_i_can_react_to_value_change(data): elt = Input(name="input_elt", value="hello") binding = Binding(data).bind_ft(elt, name="input_elt", attr="value") res = binding.update({"input_elt": "new value"}) assert len(res) == 1 def test_i_do_not_react_to_other_value_change(data): elt = Input(name="input_elt", value="hello") binding = Binding(data).bind_ft(elt, name="input_elt", attr="value") res = binding.update({"other_input_elt": "new value"}) assert res is None def test_i_can_react_to_attr_presence(data): elt = Input(name="input_elt", type="checkbox") binding = Binding(data).bind_ft( elt, name="input_elt", attr="checked", detection_mode=DetectionMode.AttributePresence ) res = binding.update({"checked": "true"}) assert len(res) == 1 def test_i_can_react_to_attr_non_presence(data): elt = Input(name="input_elt", type="checkbox") binding = Binding(data).bind_ft( elt, name="input_elt", attr="checked", detection_mode=DetectionMode.AttributePresence ) res = binding.update({}) assert len(res) == 1 def test_i_can_create_a_binding_without_activation(data): """ A binding created without calling bind_ft should not be active. """ binding = Binding(data, "value") assert binding._is_active is False assert binding.ft is None assert binding.ft_name is None assert binding.ft_attr is None assert BindingsManager.get_binding(binding.id) is None def test_i_can_activate_binding_via_bind_ft(data): """ Calling bind_ft should automatically activate the binding. """ elt = Label("hello", id="label_id") binding = Binding(data, "value") binding.bind_ft(elt, name="label_name") assert binding._is_active is True assert binding.ft is elt assert binding.ft_name == "label_name" assert BindingsManager.get_binding(binding.id) is binding def test_i_cannot_notify_when_not_active(data): """ A non-active binding should not update the UI when data changes. """ elt = Label("hello", id="label_id") binding = Binding(data, "value") binding.ft = elt binding.ft_name = "label_name" # Change data without activating the binding result = binding.notify("old", "new") assert result is None assert elt.children[0] == "hello" # Should not have changed def test_i_can_deactivate_a_binding(data): """ Deactivating a binding should clean up observers and unregister it. """ elt = Label("hello", id="label_id") binding = Binding(data, "value").bind_ft(elt, name="label_name") assert binding._is_active is True assert BindingsManager.get_binding(binding.id) is binding binding.deactivate() assert binding._is_active is False assert BindingsManager.get_binding(binding.id) is None def test_i_can_reactivate_a_binding(data): """ After deactivation, a binding can be reactivated by calling bind_ft again. """ elt1 = Label("hello", id="label_id_1") binding = Binding(data, "value").bind_ft(elt1, name="label_name_1") binding.deactivate() assert binding._is_active is False elt2 = Label("world", id="label_id_2") binding.bind_ft(elt2, name="label_name_2") assert binding._is_active is True assert binding.ft is elt2 assert binding.ft_name == "label_name_2" def test_bind_ft_deactivates_before_reconfiguring(data): """ Calling bind_ft on an active binding should deactivate it first, then reconfigure and reactivate. """ elt1 = Label("hello", id="label_id_1") elt2 = Label("world", id="label_id_2") binding = Binding(data, "value").bind_ft(elt1, name="label_name_1") # Change data to verify old binding works data.value = "updated" assert elt1.children[0] == "updated" # Reconfigure with new element binding.bind_ft(elt2, name="label_name_2") # Change data again data.value = "final" # Old element should not update assert elt1.children[0] == "updated" # New element should update assert elt2.children[0] == "final" def test_deactivate_can_be_called_multiple_times(data): """ Calling deactivate multiple times should be safe (idempotent). """ elt = Label("hello", id="label_id") binding = Binding(data, "value").bind_ft(elt, name="label_name") binding.deactivate() binding.deactivate() # Should not raise an error binding.deactivate() # Should not raise an error assert binding._is_active is False def test_i_cannot_activate_without_configuration(data): """ Calling activate directly without proper configuration should raise ValueError. """ binding = Binding(data, "value") with pytest.raises(ValueError, match="ft element is required"): binding.activate() def test_activation_validates_ft_name(data): """ Activation should fail if ft_name is not configured. """ elt = Label("hello", id="label_id") binding = Binding(data, "value") binding.ft = elt binding._detection = binding._factory(DetectionMode.ValueChange) binding._update = binding._factory(UpdateMode.ValueChange) with pytest.raises(ValueError, match="ft_name is required"): binding.activate() def test_activation_validates_strategies(data): """ Activation should fail if detection/update strategies are not initialized. """ elt = Label("hello", id="label_id") binding = Binding(data, "value") binding.ft = elt binding.ft_name = "label_name" with pytest.raises(ValueError, match="detection strategy not initialized"): binding.activate() def test_i_can_chain_bind_ft_calls(data): """ bind_ft should return self for method chaining. """ elt = Label("hello", id="label_id") binding = Binding(data, "value").bind_ft(elt, name="label_name") assert isinstance(binding, Binding) assert binding._is_active is True def test_bind_ft_updates_optional_parameters(data): """ bind_ft should update optional parameters if provided. """ elt = Input(name="input_elt", type="checkbox") binding = Binding(data, "value") binding.bind_ft( elt, name="input_elt", attr="checked", data_converter=BooleanConverter(), detection_mode=DetectionMode.AttributePresence, update_mode=UpdateMode.AttributePresence ) assert binding.detection_mode == DetectionMode.AttributePresence assert binding.update_mode == UpdateMode.AttributePresence assert isinstance(binding.data_converter, BooleanConverter) def test_deactivated_binding_does_not_update_on_data_change(data): """ After deactivation, changes to data should not update the UI element. """ elt = Label("hello", id="label_id") binding = Binding(data, "value").bind_ft(elt, name="label_name") # Verify it works when active data.value = "first update" assert elt.children[0] == "first update" # Deactivate binding.deactivate() # Change data - element should NOT update data.value = "second update" assert elt.children[0] == "first update" def test_multiple_bindings_can_coexist(data): """ Multiple bindings can be created and managed independently. """ elt1 = Label("hello", id="label_id_1") elt2 = Input(value="world", id="input_id_2") binding1 = Binding(data, "value").bind_ft(elt1, name="label_name") binding2 = Binding(data, "value").bind_ft(elt2, name="input_name", attr="value") assert len(BindingsManager.bindings) == 2 assert binding1._is_active is True assert binding2._is_active is True # Change data - both should update data.value = "updated" assert elt1.children[0] == "updated" assert elt2.attrs["value"] == "updated" # Deactivate one binding1.deactivate() assert len(BindingsManager.bindings) == 1 # Change data - only binding2 should update data.value = "final" assert elt1.children[0] == "updated" # Not changed assert elt2.attrs["value"] == "final" # Changed