from dataclasses import dataclass from typing import Any import pytest from fasthtml.components import Button, Div from myutils.observable import make_observable, bind from myfasthtml.core.commands import Command, CommandsManager, LambdaCommand from myfasthtml.core.constants import ROUTE_ROOT, Routes from myfasthtml.test.matcher import matches @dataclass class Data: value: Any def callback(): return "Hello World" @pytest.fixture(autouse=True) def reset_command_manager(): CommandsManager.reset() class TestCommandDefault: def test_i_can_create_a_command_with_no_params(self): command = Command('test', 'Command description', callback) assert command.id is not None assert command.name == 'test' assert command.description == 'Command description' assert command.execute() == "Hello World" def test_command_are_registered(self): command = Command('test', 'Command description', callback) assert CommandsManager.commands.get(str(command.id)) is command class TestCommandBind: def test_i_can_bind_a_command_to_an_element(self): command = Command('test', 'Command description', callback) elt = Button() updated = command.bind_ft(elt) expected = Button(hx_post=f"{ROUTE_ROOT}{Routes.Commands}") assert matches(updated, expected) def test_i_can_suppress_swapping_with_target_attr(self): command = Command('test', 'Command description', callback).htmx(target=None) elt = Button() updated = command.bind_ft(elt) expected = Button(hx_post=f"{ROUTE_ROOT}{Routes.Commands}", hx_swap="none") assert matches(updated, expected) def test_i_can_bind_a_command_to_an_observable(self): data = Data("hello") def on_data_change(old, new): return old, new def another_callback(): data.value = "new value" return "another callback result" make_observable(data) bind(data, "value", on_data_change) command = Command('test', 'Command description', another_callback).bind(data) res = command.execute() assert res == ["another callback result", ("hello", "new value")] def test_i_can_bind_a_command_to_an_observable_2(self): data = Data("hello") def on_data_change(old, new): return old, new def another_callback(): data.value = "new value" return ["another 1", "another 2"] make_observable(data) bind(data, "value", on_data_change) command = Command('test', 'Command description', another_callback).bind(data) res = command.execute() assert res == ["another 1", "another 2", ("hello", "new value")] def test_by_default_swap_is_set_to_outer_html(self): command = Command('test', 'Command description', callback) elt = Button() updated = command.bind_ft(elt) expected = Button(hx_post=f"{ROUTE_ROOT}{Routes.Commands}", hx_swap="outerHTML") assert matches(updated, expected) @pytest.mark.parametrize("return_values", [ [Div(), Div(id="id1"), "hello", Div(id="id2")], # list (Div(), Div(id="id1"), "hello", Div(id="id2")) # tuple ]) def test_swap_oob_is_automatically_set_when_multiple_elements_are_returned(self, return_values): """Test that hx-swap-oob is automatically set, but not for the first.""" def another_callback(): return return_values command = Command('test', 'Command description', another_callback) res = command.execute() assert "hx_swap_oob" not in res[0].attrs assert res[1].attrs["hx-swap-oob"] == "true" assert res[3].attrs["hx-swap-oob"] == "true" class TestCommandExecute: def test_i_can_create_a_command_with_no_params(self): command = Command('test', 'Command description', callback) assert command.id is not None assert command.name == 'test' assert command.description == 'Command description' assert command.execute() == "Hello World" def test_i_can_execute_a_command_with_closed_parameter(self): """The parameter is given when the command is created.""" def callback_with_param(param): return f"Hello {param}" command = Command('test', 'Command description', callback_with_param, "world") assert command.execute() == "Hello world" def test_i_can_execute_a_command_with_open_parameter(self): """The parameter is given by the browser, when the command is executed.""" def callback_with_param(name): return f"Hello {name}" command = Command('test', 'Command description', callback_with_param) assert command.execute(client_response={"name": "world"}) == "Hello world" def test_i_can_convert_arg_in_execute(self): """The parameter is given by the browser, when the command is executed.""" def callback_with_param(number: int): assert isinstance(number, int) command = Command('test', 'Command description', callback_with_param) command.execute(client_response={"number": "10"}) def test_swap_oob_is_added_when_multiple_elements_are_returned(self): """Test that hx-swap-oob is automatically set, but not for the first.""" def another_callback(): return Div(id="first"), Div(id="second"), "hello", Div(id="third") command = Command('test', 'Command description', another_callback) res = command.execute() assert "hx-swap-oob" not in res[0].attrs assert res[1].attrs["hx-swap-oob"] == "true" assert res[3].attrs["hx-swap-oob"] == "true" def test_swap_oob_is_not_added_when_there_no_id(self): """Test that hx-swap-oob is automatically set, but not for the first.""" def another_callback(): return Div(id="first"), Div(), "hello", Div() command = Command('test', 'Command description', another_callback) res = command.execute() assert "hx-swap-oob" not in res[0].attrs assert "hx-swap-oob" not in res[1].attrs assert "hx-swap-oob" not in res[3].attrs class TestLambaCommand: def test_i_can_create_a_command_from_lambda(self): command = LambdaCommand(lambda resp: "Hello World") assert command.execute() == "Hello World" def test_by_default_target_is_none(self): command = LambdaCommand(lambda resp: "Hello World") assert command.get_htmx_params()["hx-swap"] == "none"