diff --git a/src/myfasthtml/assets/myfasthtml.css b/src/myfasthtml/assets/myfasthtml.css new file mode 100644 index 0000000..6d60374 --- /dev/null +++ b/src/myfasthtml/assets/myfasthtml.css @@ -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; +} \ No newline at end of file diff --git a/src/myfasthtml/controls/helpers.py b/src/myfasthtml/controls/helpers.py new file mode 100644 index 0000000..44655c4 --- /dev/null +++ b/src/myfasthtml/controls/helpers.py @@ -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 diff --git a/src/myfasthtml/docs/__init__.py b/src/myfasthtml/docs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/myfasthtml/docs/clickme.py b/src/myfasthtml/docs/clickme.py new file mode 100644 index 0000000..1f7322d --- /dev/null +++ b/src/myfasthtml/docs/clickme.py @@ -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) diff --git a/src/myfasthtml/docs/command_with_htmx_params.py b/src/myfasthtml/docs/command_with_htmx_params.py new file mode 100644 index 0000000..bf20174 --- /dev/null +++ b/src/myfasthtml/docs/command_with_htmx_params.py @@ -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) diff --git a/src/myfasthtml/docs/helloworld.py b/src/myfasthtml/docs/helloworld.py new file mode 100644 index 0000000..fe63326 --- /dev/null +++ b/src/myfasthtml/docs/helloworld.py @@ -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) diff --git a/src/myfasthtml/test/__init__.py b/src/myfasthtml/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/myfasthtml/test/testclient.py b/src/myfasthtml/test/testclient.py index 8aff99c..6fd1bfa 100644 --- a/src/myfasthtml/test/testclient.py +++ b/src/myfasthtml/test/testclient.py @@ -345,10 +345,6 @@ class TestableElement: elif options: 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 def _get_input_identifier(input_field, counter): """ @@ -432,14 +428,6 @@ class TestableElement: # Default to string return value - @staticmethod - def _get_element(html_fragment: str): - html_fragment = html_fragment.strip() - if (not html_fragment.startswith('" - return BeautifulSoup(html_fragment, 'html.parser').find() - @staticmethod def _parse(tag, html_fragment: str): elt = BeautifulSoup(html_fragment, 'html.parser') @@ -801,8 +789,8 @@ class TestableForm(TestableElement): # return value -class TestableInput(TestableElement): - def __init__(self, client, source): +class TestableControl(TestableElement): + def __init__(self, client, source, tag): super().__init__(client, source, "input") assert len(self.fields) <= 1 self._input_name = next(iter(self.fields)) @@ -815,12 +803,40 @@ class TestableInput(TestableElement): def value(self): 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): self.fields[self.name] = value - if self.name and self._support_htmx(): - return self._send_htmx_request(data={self.name: self.value}) - - return None + return self._send_value() + + +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): diff --git a/tests/controls/__init__.py b/tests/controls/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/controls/test_helpers.py b/tests/controls/test_helpers.py new file mode 100644 index 0000000..b5326df --- /dev/null +++ b/tests/controls/test_helpers.py @@ -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") diff --git a/tests/testclient/test_testable_checkbox.py b/tests/testclient/test_testable_checkbox.py new file mode 100644 index 0000000..4842a5f --- /dev/null +++ b/tests/testclient/test_testable_checkbox.py @@ -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", [ + ('', True), + ('', 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 = '''''' + + 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 = '''''' + + @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" diff --git a/tests/testclient/test_testable_input.py b/tests/testclient/test_testable_input.py new file mode 100644 index 0000000..2922ebe --- /dev/null +++ b/tests/testclient/test_testable_input.py @@ -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_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 = '''''' + + 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 = '''''' + + @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'"