Improved Command class management.

This commit is contained in:
2025-12-19 21:12:55 +01:00
parent b26abc4257
commit 1347f12618
23 changed files with 349 additions and 169 deletions

View File

@@ -45,7 +45,7 @@ def test_i_can_mk_button_with_attrs():
def test_i_can_mk_button_with_command(user, rt):
def new_value(value): return value
command = Command('test', 'TestingCommand', None, new_value, "this is my new value")
command = Command('test', 'TestingCommand', None, new_value, args=["this is my new value"])
@rt('/')
def get(): return mk.button('button', command)

View File

@@ -464,20 +464,20 @@ class TestTreeviewBehaviour:
def test_i_cannot_ensure_path_with_none(self, root_instance):
"""Test that ensure_path raises ValueError when path is None."""
tree_view = TreeView(root_instance)
with pytest.raises(ValueError, match="Invalid path.*None"):
tree_view.ensure_path(None)
def test_i_cannot_ensure_path_with_empty_string(self, root_instance):
"""Test that ensure_path raises ValueError for empty strings after stripping."""
tree_view = TreeView(root_instance)
with pytest.raises(ValueError, match="Invalid path.*empty"):
tree_view.ensure_path(" ")
with pytest.raises(ValueError, match="Invalid path.*empty"):
tree_view.ensure_path("")
with pytest.raises(ValueError, match="Invalid path.*empty"):
tree_view.ensure_path("...")
@@ -531,44 +531,57 @@ class TestTreeviewBehaviour:
def test_i_cannot_ensure_path_with_only_spaces_parts(self, root_instance):
"""Test that ensure_path raises ValueError for path parts with only spaces."""
tree_view = TreeView(root_instance)
with pytest.raises(ValueError, match="Invalid path"):
tree_view.ensure_path("folder1. .folder2")
def test_ensure_path_returns_last_node_id(self, root_instance):
"""Test that ensure_path returns the ID of the last node in the path."""
tree_view = TreeView(root_instance)
# Create a path and get the returned ID
returned_id = tree_view.ensure_path("folder1.folder2.folder3")
# Verify the returned ID is not None
assert returned_id is not None
# Verify the returned ID corresponds to folder3
assert returned_id in tree_view._state.items
assert tree_view._state.items[returned_id].label == "folder3"
# Verify we can use this ID to add a child
leaf = TreeNode(label="file.txt", type="file")
tree_view.add_node(leaf, parent_id=returned_id)
assert leaf.parent == returned_id
assert leaf.id in tree_view._state.items[returned_id].children
def test_ensure_path_returns_existing_node_id(self, root_instance):
"""Test that ensure_path returns ID even when path already exists."""
tree_view = TreeView(root_instance)
# Create initial path
first_id = tree_view.ensure_path("folder1.folder2")
# Ensure same path again
second_id = tree_view.ensure_path("folder1.folder2")
# Should return the same ID
assert first_id == second_id
assert tree_view._state.items[first_id].label == "folder2"
def test_i_can_add_the_same_node_id_twice(self, root_instance):
"""Test that adding a node with the same ID as an existing node raises ValueError."""
tree_view = TreeView(root_instance)
node1 = TreeNode(label="Node", type="folder", id="existing_id")
tree_view.add_node(node1)
node2 = TreeNode(label="Other Node", type="folder", id="existing_id")
tree_view.add_node(node2)
assert len(tree_view._state.items) == 1, "Node should not have been added to items"
assert tree_view._state.items[node1.id] == node2, "Node should not have been replaced"
class TestTreeViewRender:

View File

@@ -33,9 +33,15 @@ class TestCommandDefault:
assert command.description == 'Command description'
assert command.execute() == "Hello World"
def test_command_are_registered(self):
def test_commands_are_registered(self):
command = Command('test', 'Command description', None, callback)
assert CommandsManager.commands.get(str(command.id)) is command
def test_commands_with_the_same_key_share_the_same_id(self):
command1 = Command('test', 'Command description', None, None, key="test_key")
command2 = Command('test', 'Command description', None, None, key="test_key")
assert command1.id is command2.id
class TestCommandBind:
@@ -74,7 +80,7 @@ class TestCommandBind:
res = command.execute()
assert res == ["another callback result", ("hello", "new value")]
assert res == ["another callback result", "hello", "new value"]
def test_i_can_bind_a_command_to_an_observable_2(self):
data = Data("hello")
@@ -92,7 +98,7 @@ class TestCommandBind:
res = command.execute()
assert res == ["another 1", "another 2", ("hello", "new value")]
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', None, callback)
@@ -120,6 +126,15 @@ class TestCommandBind:
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_i_can_send_parameters(self):
command = Command('test', 'Command description', None, None, kwargs={"param": "value"}) # callback is not important
elt = Button()
updated = command.bind_ft(elt)
hx_vals = updated.attrs["hx-vals"]
assert 'param' in hx_vals
assert hx_vals['param'] == 'value'
class TestCommandExecute:
@@ -137,7 +152,7 @@ class TestCommandExecute:
def callback_with_param(param):
return f"Hello {param}"
command = Command('test', 'Command description', None, callback_with_param, "world")
command = Command('test', 'Command description', None, callback_with_param, args=["world"])
assert command.execute() == "Hello world"
def test_i_can_execute_a_command_with_open_parameter(self):
@@ -188,9 +203,9 @@ class TestCommandExecute:
class TestLambaCommand:
def test_i_can_create_a_command_from_lambda(self):
command = LambdaCommand(None, lambda resp: "Hello World")
command = LambdaCommand(None, lambda: "Hello World")
assert command.execute() == "Hello World"
def test_by_default_target_is_none(self):
command = LambdaCommand(None, lambda resp: "Hello World")
command = LambdaCommand(None, lambda: "Hello World")
assert command.get_htmx_params()["hx-swap"] == "none"

58
tests/core/test_utils.py Normal file
View File

@@ -0,0 +1,58 @@
import pytest
from myfasthtml.core.utils import flatten
@pytest.mark.parametrize("input_args,expected,test_description", [
# Simple list without nesting
(([1, 2, 3],), [1, 2, 3], "simple list"),
# Nested list (one level)
(([1, [2, 3], 4],), [1, 2, 3, 4], "nested list one level"),
# Nested tuple
(((1, (2, 3), 4),), [1, 2, 3, 4], "nested tuple"),
# Mixed list and tuple
(([1, (2, 3), [4, 5]],), [1, 2, 3, 4, 5], "mixed list and tuple"),
# Deeply nested structure
(([1, [2, [3, [4, 5]]]],), [1, 2, 3, 4, 5], "deeply nested structure"),
# Empty list
(([],), [], "empty list"),
# Empty nested lists
(([1, [], [2, []], 3],), [1, 2, 3], "empty nested lists"),
# Preserves order
(([[3, 1], [4, 2]],), [3, 1, 4, 2], "preserves order"),
# Strings (should not be iterated)
((["hello", ["world"]],), ["hello", "world"], "strings not iterated"),
# Mixed types
(([1, "text", [2.5, True], None],), [1, "text", 2.5, True, None], "mixed types"),
# Multiple arguments with lists
(([1, 2], [3, 4], 5), [1, 2, 3, 4, 5], "multiple arguments with lists"),
# Scalar values only
((1, 2, 3), [1, 2, 3], "scalar values only"),
# Mixed scalars and lists
((1, [2, 3], 4, [5, 6]), [1, 2, 3, 4, 5, 6], "mixed scalars and lists"),
# Multiple nested arguments
(([1, [2]], [3, [4]], 5), [1, 2, 3, 4, 5], "multiple nested arguments"),
# No arguments
((), [], "no arguments"),
# Complex real-world example
(([1, [2, 3], [[4, 5], [6, 7]], [[[8, 9]]], 10],), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], "complex nesting"),
])
def test_i_can_flatten(input_args, expected, test_description):
"""Test that flatten correctly handles various nested structures and arguments."""
result = flatten(*input_args)
assert result == expected, f"Failed for test case: {test_description}"

View File

@@ -34,13 +34,13 @@ def rt(user):
class TestingCommand:
def test_i_can_trigger_a_command(self, user):
command = Command('test', 'TestingCommand', None, new_value, "this is my new value")
command = Command('test', 'TestingCommand', None, new_value, args=["this is my new value"])
testable = TestableElement(user, mk.button('button', command))
testable.click()
assert user.get_content() == "this is my new value"
def test_error_is_raised_when_command_is_not_found(self, user):
command = Command('test', 'TestingCommand', None, new_value, "this is my new value")
command = Command('test', 'TestingCommand', None, new_value, args=["this is my new value"])
CommandsManager.reset()
testable = TestableElement(user, mk.button('button', command))
@@ -50,7 +50,7 @@ class TestingCommand:
assert "not found." in str(exc_info.value)
def test_i_can_play_a_complex_scenario(self, user, rt):
command = Command('test', 'TestingCommand', None, new_value, "this is my new value")
command = Command('test', 'TestingCommand', None, new_value, args=["this is my new value"])
@rt('/')
def get(): return mk.button('button', command)