Added TestableCheckbox
This commit is contained in:
15
src/myfasthtml/assets/myfasthtml.css
Normal file
15
src/myfasthtml/assets/myfasthtml.css
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
.mf-icon-20 {
|
||||||
|
width: 20px;
|
||||||
|
min-width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
margin-top: auto;
|
||||||
|
margin-bottom: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mf-icon-16 {
|
||||||
|
width: 16px;
|
||||||
|
min-width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin-top: auto;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
33
src/myfasthtml/controls/helpers.py
Normal file
33
src/myfasthtml/controls/helpers.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
from fasthtml.components import *
|
||||||
|
|
||||||
|
from myfasthtml.core.commands import Command
|
||||||
|
from myfasthtml.core.utils import merge_classes
|
||||||
|
|
||||||
|
|
||||||
|
class mk:
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def button(element, command: Command = None, **kwargs):
|
||||||
|
return mk.manage_command(Button(element, **kwargs), command)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def icon(icon, size=20,
|
||||||
|
can_select=True,
|
||||||
|
can_hover=False,
|
||||||
|
cls='',
|
||||||
|
command: Command = None,
|
||||||
|
**kwargs):
|
||||||
|
merged_cls = merge_classes(f"mf-icon-{size}",
|
||||||
|
'icon-btn' if can_select else '',
|
||||||
|
'mmt-btn' if can_hover else '',
|
||||||
|
cls,
|
||||||
|
kwargs)
|
||||||
|
|
||||||
|
return mk.manage_command(Div(icon, cls=merged_cls, **kwargs), command)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def manage_command(ft, command: Command):
|
||||||
|
htmx = command.get_htmx_params() if command else {}
|
||||||
|
ft.attrs |= htmx
|
||||||
|
|
||||||
|
return ft
|
||||||
0
src/myfasthtml/docs/__init__.py
Normal file
0
src/myfasthtml/docs/__init__.py
Normal file
26
src/myfasthtml/docs/clickme.py
Normal file
26
src/myfasthtml/docs/clickme.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
from fasthtml import serve
|
||||||
|
|
||||||
|
from myfasthtml.controls.helpers import mk
|
||||||
|
from myfasthtml.core.commands import Command
|
||||||
|
from myfasthtml.myfastapp import create_app
|
||||||
|
|
||||||
|
|
||||||
|
# Define a simple command action
|
||||||
|
def say_hello():
|
||||||
|
return "Hello, FastHtml!"
|
||||||
|
|
||||||
|
|
||||||
|
# Create the command
|
||||||
|
hello_command = Command("say_hello", "Responds with a greeting", say_hello)
|
||||||
|
|
||||||
|
# Create the app
|
||||||
|
app, rt = create_app(protect_routes=False)
|
||||||
|
|
||||||
|
|
||||||
|
@rt("/")
|
||||||
|
def get_homepage():
|
||||||
|
return mk.button("Click Me!", command=hello_command)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
serve(port=5002)
|
||||||
25
src/myfasthtml/docs/command_with_htmx_params.py
Normal file
25
src/myfasthtml/docs/command_with_htmx_params.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
from fasthtml import serve
|
||||||
|
from fasthtml.components import *
|
||||||
|
|
||||||
|
from myfasthtml.controls.helpers import mk
|
||||||
|
from myfasthtml.core.commands import Command
|
||||||
|
from myfasthtml.icons.fa import icon_home
|
||||||
|
from myfasthtml.myfastapp import create_app
|
||||||
|
|
||||||
|
app, rt = create_app(protect_routes=False)
|
||||||
|
|
||||||
|
|
||||||
|
def change_text():
|
||||||
|
return "New text"
|
||||||
|
|
||||||
|
|
||||||
|
command = Command("change_text", "change the text", change_text).htmx(target="#text")
|
||||||
|
|
||||||
|
|
||||||
|
@rt("/")
|
||||||
|
def index():
|
||||||
|
return mk.button(Div(mk.icon(icon_home), Div("Hello World", id="text"), cls="flex"), command=command)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
serve(port=5002)
|
||||||
15
src/myfasthtml/docs/helloworld.py
Normal file
15
src/myfasthtml/docs/helloworld.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
from fasthtml import serve
|
||||||
|
from fasthtml.components import *
|
||||||
|
|
||||||
|
from myfasthtml.myfastapp import create_app
|
||||||
|
|
||||||
|
app, rt = create_app(protect_routes=False)
|
||||||
|
|
||||||
|
|
||||||
|
@rt("/")
|
||||||
|
def get_homepage():
|
||||||
|
return Div("Hello, FastHtml!")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
serve(port=5002)
|
||||||
0
src/myfasthtml/test/__init__.py
Normal file
0
src/myfasthtml/test/__init__.py
Normal file
@@ -345,10 +345,6 @@ class TestableElement:
|
|||||||
elif options:
|
elif options:
|
||||||
self.fields[name] = options[0]['value']
|
self.fields[name] = options[0]['value']
|
||||||
|
|
||||||
def _get_my_ft(self, element: Tag):
|
|
||||||
_inner = element.find(self.tag) if self.tag and self.tag != element.name else element
|
|
||||||
return MyFT(_inner.name, _inner.attrs)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_input_identifier(input_field, counter):
|
def _get_input_identifier(input_field, counter):
|
||||||
"""
|
"""
|
||||||
@@ -432,14 +428,6 @@ class TestableElement:
|
|||||||
# Default to string
|
# Default to string
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _get_element(html_fragment: str):
|
|
||||||
html_fragment = html_fragment.strip()
|
|
||||||
if (not html_fragment.startswith('<div') and
|
|
||||||
not html_fragment.startswith('<form')):
|
|
||||||
html_fragment = "<div>" + html_fragment + "</div>"
|
|
||||||
return BeautifulSoup(html_fragment, 'html.parser').find()
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _parse(tag, html_fragment: str):
|
def _parse(tag, html_fragment: str):
|
||||||
elt = BeautifulSoup(html_fragment, 'html.parser')
|
elt = BeautifulSoup(html_fragment, 'html.parser')
|
||||||
@@ -801,8 +789,8 @@ class TestableForm(TestableElement):
|
|||||||
# return value
|
# return value
|
||||||
|
|
||||||
|
|
||||||
class TestableInput(TestableElement):
|
class TestableControl(TestableElement):
|
||||||
def __init__(self, client, source):
|
def __init__(self, client, source, tag):
|
||||||
super().__init__(client, source, "input")
|
super().__init__(client, source, "input")
|
||||||
assert len(self.fields) <= 1
|
assert len(self.fields) <= 1
|
||||||
self._input_name = next(iter(self.fields))
|
self._input_name = next(iter(self.fields))
|
||||||
@@ -815,12 +803,40 @@ class TestableInput(TestableElement):
|
|||||||
def value(self):
|
def value(self):
|
||||||
return self.fields[self._input_name]
|
return self.fields[self._input_name]
|
||||||
|
|
||||||
|
def _send_value(self):
|
||||||
|
if self._input_name and self._support_htmx():
|
||||||
|
return self._send_htmx_request(data={self._input_name: self.value})
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class TestableInput(TestableControl):
|
||||||
|
def __init__(self, client, source):
|
||||||
|
super().__init__(client, source, "input")
|
||||||
|
|
||||||
def send(self, value):
|
def send(self, value):
|
||||||
self.fields[self.name] = value
|
self.fields[self.name] = value
|
||||||
if self.name and self._support_htmx():
|
return self._send_value()
|
||||||
return self._send_htmx_request(data={self.name: self.value})
|
|
||||||
|
|
||||||
return None
|
class TestableCheckbox(TestableControl):
|
||||||
|
def __init__(self, client, source):
|
||||||
|
super().__init__(client, source, "input")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_checked(self):
|
||||||
|
return self.fields[self._input_name] == True
|
||||||
|
|
||||||
|
def check(self):
|
||||||
|
self.fields[self._input_name] = True
|
||||||
|
return self._send_value()
|
||||||
|
|
||||||
|
def uncheck(self):
|
||||||
|
self.fields[self._input_name] = False
|
||||||
|
return self._send_value()
|
||||||
|
|
||||||
|
def toggle(self):
|
||||||
|
self.fields[self._input_name] = not self.fields[self._input_name]
|
||||||
|
return self._send_value()
|
||||||
|
|
||||||
|
|
||||||
# def get_value(tag):
|
# def get_value(tag):
|
||||||
|
|||||||
0
tests/controls/__init__.py
Normal file
0
tests/controls/__init__.py
Normal file
48
tests/controls/test_helpers.py
Normal file
48
tests/controls/test_helpers.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import pytest
|
||||||
|
from fasthtml.components import *
|
||||||
|
from fasthtml.fastapp import fast_app
|
||||||
|
|
||||||
|
from myfasthtml.controls.helpers import mk
|
||||||
|
from myfasthtml.core.commands import Command
|
||||||
|
from myfasthtml.test.matcher import matches
|
||||||
|
from myfasthtml.test.testclient import MyTestClient
|
||||||
|
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_can_mk_button():
|
||||||
|
button = mk.button('button')
|
||||||
|
expected = Button('button')
|
||||||
|
|
||||||
|
assert matches(button, expected)
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_can_mk_button_with_attrs():
|
||||||
|
button = mk.button('button', id="button_id", class_="button_class")
|
||||||
|
expected = Button('button', id="button_id", class_="button_class")
|
||||||
|
assert matches(button, expected)
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_can_mk_button_with_command(user, rt):
|
||||||
|
def new_value(value): return value
|
||||||
|
|
||||||
|
command = Command('test', 'TestingCommand', new_value, "this is my new value")
|
||||||
|
|
||||||
|
@rt('/')
|
||||||
|
def get(): return mk.button('button', command)
|
||||||
|
|
||||||
|
user.open("/")
|
||||||
|
user.should_see("button")
|
||||||
|
|
||||||
|
user.find_element("button").click()
|
||||||
|
user.should_see("this is my new value")
|
||||||
59
tests/testclient/test_testable_checkbox.py
Normal file
59
tests/testclient/test_testable_checkbox.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import pytest
|
||||||
|
from fasthtml.fastapp import fast_app
|
||||||
|
|
||||||
|
from myfasthtml.test.testclient import MyTestClient, TestableCheckbox
|
||||||
|
|
||||||
|
|
||||||
|
@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)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("html,expected_value", [
|
||||||
|
('<input type="checkbox" name="male" checked />', True),
|
||||||
|
('<input type="checkbox" name="male" />', False),
|
||||||
|
])
|
||||||
|
def test_i_can_read_input(test_client, html, expected_value):
|
||||||
|
input_elt = TestableCheckbox(test_client, html)
|
||||||
|
|
||||||
|
assert input_elt.name == "male"
|
||||||
|
assert input_elt.value == expected_value
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_can_read_input_with_label(test_client):
|
||||||
|
html = '''<label for="uid">Male</label><input id="uid" type="checkbox" name="male" checked />'''
|
||||||
|
|
||||||
|
input_elt = TestableCheckbox(test_client, html)
|
||||||
|
assert input_elt.fields_mapping == {"Male": "male"}
|
||||||
|
assert input_elt.name == "male"
|
||||||
|
assert input_elt.value == True
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_can_check_checkbox(test_client, rt):
|
||||||
|
html = '''<input type="checkbox" name="male" hx_post="/submit"/>'''
|
||||||
|
|
||||||
|
@rt('/submit')
|
||||||
|
def post(male: bool):
|
||||||
|
return f"Checkbox received {male=}"
|
||||||
|
|
||||||
|
input_elt = TestableCheckbox(test_client, html)
|
||||||
|
|
||||||
|
input_elt.check()
|
||||||
|
assert test_client.get_content() == "Checkbox received male=True"
|
||||||
|
|
||||||
|
input_elt.uncheck()
|
||||||
|
assert test_client.get_content() == "Checkbox received male=False"
|
||||||
|
|
||||||
|
input_elt.toggle()
|
||||||
|
assert test_client.get_content() == "Checkbox received male=True"
|
||||||
51
tests/testclient/test_testable_input.py
Normal file
51
tests/testclient/test_testable_input.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import pytest
|
||||||
|
from fasthtml.fastapp import fast_app
|
||||||
|
|
||||||
|
from myfasthtml.test.testclient import TestableInput, MyTestClient
|
||||||
|
|
||||||
|
|
||||||
|
@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 = '''<input type="text" name="username" value="john_doe" />'''
|
||||||
|
|
||||||
|
input_elt = TestableInput(test_client, html)
|
||||||
|
|
||||||
|
assert input_elt.name == "username"
|
||||||
|
assert input_elt.value == "john_doe"
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_can_read_input_with_label(test_client):
|
||||||
|
html = '''<label for="uid">Username</label><input id="uid" name="username" value="john_doe" />'''
|
||||||
|
|
||||||
|
input_elt = TestableInput(test_client, html)
|
||||||
|
assert input_elt.fields_mapping == {"Username": "username"}
|
||||||
|
assert input_elt.name == "username"
|
||||||
|
assert input_elt.value == "john_doe"
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_can_send_values(test_client, rt):
|
||||||
|
html = '''<input type="text" name="username" value="john_doe" hx_post="/submit"/>'''
|
||||||
|
|
||||||
|
@rt('/submit')
|
||||||
|
def post(username: str):
|
||||||
|
return f"Input received {username=}"
|
||||||
|
|
||||||
|
input_elt = TestableInput(test_client, html)
|
||||||
|
input_elt.send("another name")
|
||||||
|
|
||||||
|
assert test_client.get_content() == "Input received username='another name'"
|
||||||
Reference in New Issue
Block a user