I can bind select

This commit is contained in:
2025-11-08 19:58:47 +01:00
parent 6a05a84f0c
commit ad2823042c
22 changed files with 591 additions and 651 deletions

View File

@@ -2,8 +2,10 @@ import pytest
from fasthtml.fastapp import fast_app
from myfasthtml.auth.utils import create_auth_beforeware
from myfasthtml.core.utils import quoted_str
from myfasthtml.test.testclient import MyTestClient
def test_non_protected_route():
app, rt = fast_app()
user = MyTestClient(app)
@@ -31,3 +33,15 @@ def test_all_routes_are_protected():
user.open("/")
user.should_see("Sign In")
@pytest.mark.parametrize("actual,expected", [
("string", '"string"'),
("string with 'single quotes'", '''"string with 'single quotes'"'''),
('string with "double quotes"', """'string with "double quotes"'"""),
("""string with 'single' and "double" quotes""", '''"string with 'single' and \\"double\\" quotes"'''),
(None, "None"),
(123, "123"),
])
def test_i_can_quote_str(actual, expected):
assert quoted_str(actual) == expected

View File

@@ -24,7 +24,8 @@ from fasthtml.fastapp import fast_app
from myfasthtml.controls.helpers import mk
from myfasthtml.core.bindings import Binding, BooleanConverter
from myfasthtml.test.matcher import matches, AttributeForbidden
from myfasthtml.core.constants import Routes, ROUTE_ROOT
from myfasthtml.test.matcher import matches, AttributeForbidden, AnyValue
from myfasthtml.test.testclient import MyTestClient
@@ -67,7 +68,19 @@ def rt(user):
class TestBindingTextarea:
"""Tests for binding Textarea components."""
def test_i_can_bind_textarea(self, user, rt):
def test_i_can_bind_textarea(self):
data = Data("")
check_box = Textarea(name="textarea_name")
binding = Binding(data)
mk.manage_binding(check_box, binding)
# update the content
res = binding.update({"textarea_name": "Hello world !"})
expected = [Textarea("Hello world !", name="textarea_name", hx_swap_oob="true")]
assert matches(res, expected)
def test_i_can_bind_textarea_with_label(self, user, rt):
"""
Textarea should bind bidirectionally with data.
Value changes should update the label.
@@ -89,27 +102,6 @@ class TestBindingTextarea:
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.
@@ -156,6 +148,80 @@ class TestBindingTextarea:
class TestBindingSelect:
"""Tests for binding Select components (single selection)."""
def test_i_can_bind_select(self):
data = Data("")
select_elt = Select(
Option("Option 1", value="option1"),
Option("Option 2", value="option2"),
Option("Option 3", value="option3"),
name="select_name"
)
binding = Binding(data)
updated = mk.manage_binding(select_elt, binding)
expected = Select(
AttributeForbidden("hx_swap_oob"),
Option("Option 1", value="option1"),
Option("Option 2", value="option2"),
Option("Option 3", value="option3"),
name="select_name",
id=AnyValue(),
hx_post=f"{ROUTE_ROOT}{Routes.Bindings}",
)
assert matches(updated, expected)
def test_i_can_update_select(self):
data = Data("")
select_elt = Select(
Option("Option 1", value="option1"),
Option("Option 2", value="option2"),
Option("Option 3", value="option3"),
name="select_name"
)
binding = Binding(data)
mk.manage_binding(select_elt, binding)
res = binding.update({"select_name": "option2"})
expected = Select(
Option("Option 1", value="option1"),
Option("Option 2", value="option2", selected="true"),
Option("Option 3", value="option3"),
name="select_name",
id=AnyValue(),
hx_post=f"{ROUTE_ROOT}{Routes.Bindings}",
hx_swap_oob="true"
)
assert matches(res, [expected])
def test_i_can_change_selection(self):
data = Data("")
select_elt = Select(
Option("Option 1", value="option1"),
Option("Option 2", value="option2"),
Option("Option 3", value="option3"),
name="select_name"
)
binding = Binding(data)
mk.manage_binding(select_elt, binding)
binding.update({"select_name": "option2"})
res = binding.update({"select_name": "option1"})
expected = Select(
Option("Option 1", value="option1", selected="true"),
Option(AttributeForbidden("selected"), "Option 2", value="option2"),
Option(AttributeForbidden("selected"), "Option 3", value="option3"),
name="select_name",
id=AnyValue(),
hx_post=f"{ROUTE_ROOT}{Routes.Bindings}",
hx_swap_oob="true"
)
assert matches(res, [expected])
def test_i_can_bind_select_single(self, user, rt):
"""
Single select should bind with data.

View File

@@ -3,7 +3,7 @@ from fastcore.basics import NotStr
from fasthtml.components import *
from myfasthtml.test.matcher import matches, StartsWith, Contains, DoesNotContain, Empty, DoNotCheck, ErrorOutput, \
ErrorComparisonOutput, AttributeForbidden
ErrorComparisonOutput, AttributeForbidden, AnyValue
from myfasthtml.test.testclient import MyFT
@@ -17,6 +17,7 @@ from myfasthtml.test.testclient import MyFT
(Div(attr1="valueXXX", attr2="value"), Div(attr1=StartsWith("value"))),
(Div(attr1="before value after", attr2="value"), Div(attr1=Contains("value"))),
(Div(attr1="before after", attr2="value"), Div(attr1=DoesNotContain("value"))),
(Div(attr1="value"), Div(attr1=AnyValue())),
(None, DoNotCheck()),
(123, DoNotCheck()),
(Div(), DoNotCheck()),
@@ -49,6 +50,8 @@ def test_i_can_match(actual, expected):
(Div(attr1="value1"), Div(attr1=StartsWith("value2")), "The condition 'StartsWith(value2)' is not satisfied"),
(Div(attr1="value1"), Div(attr1=Contains("value2")), "The condition 'Contains(value2)' is not satisfied"),
(Div(attr1="value1 value2"), Div(attr1=DoesNotContain("value2")), "The condition 'DoesNotContain(value2)'"),
(Div(attr1=None), Div(attr1=AnyValue()), "'attr1' is not found in Actual"),
(Div(), Div(attr1=AnyValue()), "'attr1' is not found in Actual"),
(NotStr("456"), NotStr("123"), "Notstr values are different"),
(Div(attr="value"), Div(Empty()), "The condition 'Empty()' is not satisfied"),
(Div(120), Div(Empty()), "The condition 'Empty()' is not satisfied"),
@@ -176,6 +179,7 @@ def test_i_can_output_error_when_predicate():
def test_i_can_output_error_when_predicate_wrong_value():
"""I can display error when the condition predicate is not satisfied."""
elt = "before after"
expected = Contains("value")
path = ""
@@ -186,6 +190,7 @@ def test_i_can_output_error_when_predicate_wrong_value():
def test_i_can_output_error_child_element():
"""I can display error when the element has children"""
elt = Div(P(id="p_id"), Div(id="child_1"), Div(id="child_2"), attr1="value1")
expected = elt
path = ""
@@ -198,6 +203,19 @@ def test_i_can_output_error_child_element():
')',
]
def test_i_can_output_error_child_element_text():
"""I can display error when the children is not a FT"""
elt = Div("Hello world", Div(id="child_1"), Div(id="child_2"), attr1="value1")
expected = elt
path = ""
error_output = ErrorOutput(path, elt, expected)
error_output.compute()
assert error_output.output == ['(div "attr1"="value1"',
' "Hello world"',
' (div "id"="child_1")',
' (div "id"="child_2")',
')',
]
def test_i_can_output_error_child_element_indicating_sub_children():
elt = Div(P(id="p_id"), Div(Div(id="child_2"), id="child_1"), attr1="value1")

View File

@@ -481,4 +481,3 @@ class TestMyTestClientFindForm:
error_message = str(exc_info.value)
assert "Found 2 forms (with the specified fields). Expected exactly 1." in error_message

View File

@@ -1,18 +1,3 @@
"""
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

View File

@@ -54,5 +54,5 @@ def test_i_can_send_values(test_client, rt):
def i_can_find_input_by_name(test_client):
html = '''<label for="uid">Username</label><input id="uid" name="username" value="john_doe" />'''
test_client.find_input("username")
element = test_client.find_input("Username")
assert False

View File

@@ -1,191 +1,63 @@
"""
Comprehensive binding tests for all bindable FastHTML components.
import pytest
from fasthtml.fastapp import fast_app
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
from fasthtml.components import (
Label, Select, Option
)
from myfasthtml.controls.helpers import mk
from myfasthtml.core.bindings import Binding
from myfasthtml.test.testclient import TestableSelect, MyTestClient
@dataclass
class Data:
value: str = "hello world"
@pytest.fixture
def test_app():
test_app, rt = fast_app(default_hdrs=False)
return test_app
@dataclass
class NumericData:
value: int = 50
@pytest.fixture
def rt(test_app):
return test_app.route
@dataclass
class BoolData:
value: bool = True
@pytest.fixture
def test_client(test_app):
return MyTestClient(test_app)
@dataclass
class ListData:
value: list = None
def __post_init__(self):
if self.value is None:
self.value = []
def test_i_can_read_select(test_client):
html = '''<select name="select_name">
<option value="option1">Option 1</option>
<option value="option2">Option 2</option>
<option value="option3">Option 3</option>
</select>
'''
select_elt = TestableSelect(test_client, html)
assert select_elt.name == "select_name"
assert select_elt.value == "option1" # if no selected found, the first option is selected by default
assert select_elt.options == [{'text': 'Option 1', 'value': 'option1'},
{'text': 'Option 2', 'value': 'option2'},
{'text': 'Option 3', 'value': 'option3'}]
assert select_elt.select_fields == {'select_name': [{'text': 'Option 1', 'value': 'option1'},
{'text': 'Option 2', 'value': 'option2'},
{'text': 'Option 3', 'value': 'option3'}]}
assert select_elt.is_multiple is False
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")
def test_i_can_select_option(test_client):
html = '''<select name="select_name">
<option value="option1">Option 1</option>
<option value="option2">Option 2</option>
<option value="option3">Option 3</option>
</select>
'''
select_elt = TestableSelect(test_client, html)
select_elt.select("option2")
assert select_elt.value == "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']")
def test_i_can_select_by_text(test_client):
html = '''<select name="select_name">
<option value="option1">Option 1</option>
<option value="option2">Option 2</option>
<option value="option3">Option 3</option>
</select>
'''
select_elt = TestableSelect(test_client, html)
select_elt.select_by_text("Option 3")
assert select_elt.value == "option3"

View File

@@ -1,136 +1,46 @@
"""
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
from fasthtml.components import (
Label, Textarea
)
from myfasthtml.controls.helpers import mk
from myfasthtml.core.bindings import Binding
@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 = []
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")
from dataclasses import dataclass
import pytest
from fasthtml.fastapp import fast_app
from myfasthtml.test.testclient import MyTestClient, TestableTextarea
@dataclass
class Data:
value: str = "hello world"
@pytest.fixture
def test_app():
test_app, rt = fast_app(default_hdrs=False)
return test_app
@pytest.fixture
def rt(test_app):
return test_app.route
@pytest.fixture
def test_client(test_app):
return MyTestClient(test_app)
def test_i_can_read_input(test_client):
html = '''<textarea name="textarea_name">Lorem ipsum</textarea>'''
input_elt = TestableTextarea(test_client, html)
assert input_elt.name == "textarea_name"
assert input_elt.value == "Lorem ipsum"
@pytest.mark.skip("To update later")
def test_i_can_read_input_with_label(test_client):
html = '''<label for="uid">Text Area</label><textarea id="uid" name="textarea_name">Lorem ipsum</textarea>'''
input_elt = TestableTextarea(test_client, html)
assert input_elt.fields_mapping == {"Text Area": "textarea_name"}
assert input_elt.name == "textarea_name"
assert input_elt.value == "Lorem ipsum"