Implemented Delete feature in DataGridsManager.py. There is still a bug as DBEngine.delete is not implemented
Improved readability for tests using matcher
This commit is contained in:
@@ -7,7 +7,7 @@ from fasthtml.components import *
|
||||
from myfasthtml.controls.Keyboard import Keyboard
|
||||
from myfasthtml.controls.TreeView import TreeView, TreeNode
|
||||
from myfasthtml.test.matcher import matches, TestObject, TestCommand, TestIcon, find_one, find, Contains, \
|
||||
DoesNotContain
|
||||
DoesNotContain, TestLabel
|
||||
from .conftest import root_instance
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ class TestTreeviewBehaviour:
|
||||
assert state.opened == []
|
||||
assert state.selected is None
|
||||
assert state.editing is None
|
||||
assert state.icon_config == {}
|
||||
assert state.icon_config == {'file': 'TreeViewFile', 'folder': 'TreeViewFolder'}
|
||||
|
||||
def test_i_can_create_empty_treeview(self, root_instance):
|
||||
"""Test creating an empty TreeView."""
|
||||
@@ -674,7 +674,7 @@ class TestTreeViewRender:
|
||||
Div(
|
||||
Div(
|
||||
TestIcon("chevron_right20_regular"), # Collapsed toggle icon
|
||||
Span("Parent"), # Label
|
||||
TestLabel("Parent", icon="folder20_regular"),
|
||||
Div( # Action buttons
|
||||
TestIcon("add_circle20_regular"),
|
||||
TestIcon("edit20_regular"),
|
||||
@@ -736,7 +736,7 @@ class TestTreeViewRender:
|
||||
expected_child_container = Div(
|
||||
Div(
|
||||
None, # No icon for leaf nodes
|
||||
Span("Child1"),
|
||||
TestLabel("Child1", icon="Document20Regular"),
|
||||
Div(), # action buttons
|
||||
cls=Contains("mf-treenode")
|
||||
),
|
||||
@@ -764,7 +764,7 @@ class TestTreeViewRender:
|
||||
expected = Div(
|
||||
Div(
|
||||
None, # No icon for leaf nodes
|
||||
Span("Leaf Node"), # Label
|
||||
TestLabel("Leaf Node", icon="Document20Regular"), # Label
|
||||
Div(), # Action buttons still present
|
||||
),
|
||||
cls=Contains("mf-treenode"),
|
||||
@@ -792,7 +792,7 @@ class TestTreeViewRender:
|
||||
expected = Div(
|
||||
Div(
|
||||
None, # No icon for leaf nodes
|
||||
Span("Selected Node"),
|
||||
TestLabel("Selected Node", icon="Document20Regular"),
|
||||
Div(), # Action buttons
|
||||
cls=Contains("mf-treenode", "selected")
|
||||
),
|
||||
@@ -872,7 +872,7 @@ class TestTreeViewRender:
|
||||
root_expected = Div(
|
||||
Div(
|
||||
TestIcon("chevron_down20_regular"), # Expanded icon
|
||||
Span("Root"),
|
||||
TestLabel("Root", icon="Folder20Regular"),
|
||||
Div(), # Action buttons
|
||||
cls=Contains("mf-treenode"),
|
||||
style=Contains("padding-left: 0px")
|
||||
@@ -887,10 +887,10 @@ class TestTreeViewRender:
|
||||
child_expected = Div(
|
||||
Div(
|
||||
TestIcon("chevron_down20_regular"), # Expanded icon
|
||||
Span("Child"),
|
||||
TestLabel("Child", icon="Folder20Regular"),
|
||||
Div(), # Action buttons
|
||||
cls=Contains("mf-treenode"),
|
||||
style=Contains("padding-left: 20px")
|
||||
style=Contains("padding-left: 45px")
|
||||
),
|
||||
cls="mf-treenode-container",
|
||||
data_node_id=child.id
|
||||
@@ -902,10 +902,10 @@ class TestTreeViewRender:
|
||||
grandchild_expected = Div(
|
||||
Div(
|
||||
None, # No icon for leaf nodes
|
||||
Span("Grandchild"),
|
||||
TestLabel("Grandchild", icon="Document20Regular"),
|
||||
Div(), # Action buttons
|
||||
cls=Contains("mf-treenode"),
|
||||
style=Contains("padding-left: 40px")
|
||||
style=Contains("padding-left: 90px")
|
||||
),
|
||||
cls="mf-treenode-container",
|
||||
data_node_id=grandchild.id
|
||||
@@ -1071,7 +1071,7 @@ class TestTreeViewRender:
|
||||
expected_root1 = Div(
|
||||
Div(
|
||||
None, # No icon for leaf nodes
|
||||
Span("Root 1"),
|
||||
TestLabel("Root 1", icon="Folder20Regular"),
|
||||
Div(), # Action buttons
|
||||
cls=Contains("mf-treenode")
|
||||
),
|
||||
@@ -1082,7 +1082,7 @@ class TestTreeViewRender:
|
||||
expected_root2 = Div(
|
||||
Div(
|
||||
None, # No icon for leaf nodes
|
||||
Span("Root 2"),
|
||||
TestLabel("Root 2", icon="Folder20Regular"),
|
||||
Div(), # Action buttons
|
||||
cls=Contains("mf-treenode")
|
||||
),
|
||||
|
||||
@@ -5,8 +5,9 @@ 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.commands import Command, CommandsManager, LambdaCommand, BoundCommand
|
||||
from myfasthtml.core.constants import ROUTE_ROOT, Routes
|
||||
from myfasthtml.core.instances import BaseInstance
|
||||
from myfasthtml.test.matcher import matches
|
||||
|
||||
|
||||
@@ -24,6 +25,20 @@ def reset_command_manager():
|
||||
CommandsManager.reset()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def session():
|
||||
"""Create a test session."""
|
||||
return {"user_info": {"id": "test-user-123"}}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def owner(session):
|
||||
"""Create a BaseInstance owner for testing bind_command."""
|
||||
res = BaseInstance(parent=None, session=session, _id="test-owner")
|
||||
res._bound_commands.clear()
|
||||
return res
|
||||
|
||||
|
||||
class TestCommandDefault:
|
||||
|
||||
def test_i_can_create_a_command_with_no_params(self):
|
||||
@@ -198,6 +213,187 @@ class TestCommandExecute:
|
||||
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
|
||||
|
||||
@pytest.mark.parametrize("when", ["before", "after"])
|
||||
def test_i_can_execute_bound_command_with_when(self, owner, when):
|
||||
"""Test that bound commands execute before or after main callback based on when parameter."""
|
||||
execution_order = []
|
||||
|
||||
def main_callback():
|
||||
execution_order.append("main")
|
||||
return Div(id="main")
|
||||
|
||||
def bound_callback():
|
||||
execution_order.append(when)
|
||||
return Div(id=when)
|
||||
|
||||
main_command = Command('main', 'Main command', owner, main_callback)
|
||||
bound_command = Command('bound', 'Bound command', owner, bound_callback)
|
||||
|
||||
owner.bind_command(main_command, bound_command, when=when)
|
||||
|
||||
res = main_command.execute()
|
||||
|
||||
if when == "before":
|
||||
assert execution_order == ["before", "main"]
|
||||
else:
|
||||
assert execution_order == ["main", "after"]
|
||||
|
||||
assert isinstance(res, list)
|
||||
|
||||
def test_i_can_execute_multiple_bound_commands_in_correct_order(self, owner):
|
||||
"""Test that multiple bound commands execute in correct order: before1, before2, main, after1, after2."""
|
||||
execution_order = []
|
||||
|
||||
def main_callback():
|
||||
execution_order.append("main")
|
||||
return Div(id="main")
|
||||
|
||||
def before1_callback():
|
||||
execution_order.append("before1")
|
||||
return Div(id="before1")
|
||||
|
||||
def before2_callback():
|
||||
execution_order.append("before2")
|
||||
return Div(id="before2")
|
||||
|
||||
def after1_callback():
|
||||
execution_order.append("after1")
|
||||
return Div(id="after1")
|
||||
|
||||
def after2_callback():
|
||||
execution_order.append("after2")
|
||||
return Div(id="after2")
|
||||
|
||||
main_command = Command('main', 'Main command', owner, main_callback)
|
||||
before1_command = Command('before1', 'Before 1 command', owner, before1_callback)
|
||||
before2_command = Command('before2', 'Before 2 command', owner, before2_callback)
|
||||
after1_command = Command('after1', 'After 1 command', owner, after1_callback)
|
||||
after2_command = Command('after2', 'After 2 command', owner, after2_callback)
|
||||
|
||||
owner.bind_command(main_command, before1_command, when="before")
|
||||
owner.bind_command(main_command, before2_command, when="before")
|
||||
owner.bind_command(main_command, after1_command, when="after")
|
||||
owner.bind_command(main_command, after2_command, when="after")
|
||||
|
||||
res = main_command.execute()
|
||||
|
||||
assert execution_order == ["before1", "before2", "main", "after1", "after2"]
|
||||
assert isinstance(res, list)
|
||||
|
||||
def test_main_callback_result_is_first_in_all_ret(self, owner):
|
||||
"""Test that main callback result is always all_ret[0] for HTMX target."""
|
||||
|
||||
def main_callback():
|
||||
return Div(id="main", cls="main-result")
|
||||
|
||||
def before_callback():
|
||||
return Div(id="before")
|
||||
|
||||
def after_callback():
|
||||
return Div(id="after")
|
||||
|
||||
main_command = Command('main', 'Main command', owner, main_callback)
|
||||
before_command = Command('before', 'Before command', owner, before_callback)
|
||||
after_command = Command('after', 'After command', owner, after_callback)
|
||||
|
||||
owner.bind_command(main_command, before_command, when="before")
|
||||
owner.bind_command(main_command, after_command, when="after")
|
||||
|
||||
res = main_command.execute()
|
||||
|
||||
assert isinstance(res, list)
|
||||
assert res[0].attrs["id"] == "main"
|
||||
assert res[0].attrs.get("class") == "main-result"
|
||||
|
||||
def test_hx_swap_oob_is_applied_to_bound_commands_results(self, owner):
|
||||
"""Test that hx-swap-oob is applied to bound commands results but not to main result."""
|
||||
|
||||
def main_callback():
|
||||
return Div(id="main")
|
||||
|
||||
def before_callback():
|
||||
return Div(id="before")
|
||||
|
||||
def after_callback():
|
||||
return Div(id="after")
|
||||
|
||||
main_command = Command('main', 'Main command', owner, main_callback)
|
||||
before_command = Command('before', 'Before command', owner, before_callback)
|
||||
after_command = Command('after', 'After command', owner, after_callback)
|
||||
|
||||
owner.bind_command(main_command, before_command, when="before")
|
||||
owner.bind_command(main_command, after_command, when="after")
|
||||
|
||||
res = main_command.execute()
|
||||
|
||||
assert isinstance(res, list)
|
||||
assert len(res) == 3
|
||||
# Main result should NOT have hx-swap-oob
|
||||
assert "hx-swap-oob" not in res[0].attrs
|
||||
# Bound commands results should have hx-swap-oob
|
||||
assert res[1].attrs["hx-swap-oob"] == "true"
|
||||
assert res[2].attrs["hx-swap-oob"] == "true"
|
||||
|
||||
def test_bound_commands_without_return_do_not_affect_main_result(self, owner):
|
||||
"""Test that bound commands without return value do not affect main result."""
|
||||
|
||||
def main_callback():
|
||||
return Div(id="main")
|
||||
|
||||
def before_callback():
|
||||
# No return value
|
||||
pass
|
||||
|
||||
def after_callback():
|
||||
# No return value
|
||||
pass
|
||||
|
||||
main_command = Command('main', 'Main command', owner, main_callback)
|
||||
before_command = Command('before', 'Before command', owner, before_callback)
|
||||
after_command = Command('after', 'After command', owner, after_callback)
|
||||
|
||||
owner.bind_command(main_command, before_command, when="before")
|
||||
owner.bind_command(main_command, after_command, when="after")
|
||||
|
||||
res = main_command.execute()
|
||||
|
||||
# When bound commands return None, they are still included in the result list
|
||||
# but the main callback result should still be first
|
||||
assert isinstance(res, list)
|
||||
assert res[0].attrs["id"] == "main"
|
||||
|
||||
def test_i_can_combine_bound_commands_with_observable_bindings(self, owner):
|
||||
"""Test that bound commands work correctly with observable bindings."""
|
||||
data = Data("initial")
|
||||
execution_order = []
|
||||
|
||||
def on_data_change(old, new):
|
||||
execution_order.append("observable")
|
||||
return Div(id="observable", cls="data-changed")
|
||||
|
||||
def main_callback():
|
||||
execution_order.append("main")
|
||||
data.value = "modified"
|
||||
return Div(id="main")
|
||||
|
||||
def before_callback():
|
||||
execution_order.append("before")
|
||||
return Div(id="before")
|
||||
|
||||
make_observable(data)
|
||||
bind(data, "value", on_data_change)
|
||||
|
||||
main_command = Command('main', 'Main command', owner, main_callback).bind(data)
|
||||
before_command = Command('before', 'Before command', owner, before_callback)
|
||||
|
||||
owner.bind_command(main_command, before_command, when="before")
|
||||
|
||||
res = main_command.execute()
|
||||
|
||||
# Execution order: before -> main -> observable change
|
||||
assert execution_order == ["before", "main", "observable"]
|
||||
assert isinstance(res, list)
|
||||
|
||||
|
||||
class TestLambaCommand:
|
||||
@@ -209,3 +405,66 @@ class TestLambaCommand:
|
||||
def test_by_default_target_is_none(self):
|
||||
command = LambdaCommand(None, lambda: "Hello World")
|
||||
assert command.get_htmx_params()["hx-swap"] == "none"
|
||||
|
||||
|
||||
class TestBoundCommand:
|
||||
"""Tests for BoundCommand dataclass."""
|
||||
|
||||
@pytest.mark.parametrize("when_param, expected_when", [
|
||||
(None, "after"), # default
|
||||
("before", "before"), # explicit before
|
||||
("after", "after"), # explicit after
|
||||
])
|
||||
def test_i_can_create_bound_command(self, when_param, expected_when):
|
||||
"""Test that BoundCommand can be created with different when values."""
|
||||
command = Command('test', 'Command description', None, callback)
|
||||
|
||||
if when_param is None:
|
||||
bound = BoundCommand(command=command)
|
||||
else:
|
||||
bound = BoundCommand(command=command, when=when_param)
|
||||
|
||||
assert bound.command is command
|
||||
assert bound.when == expected_when
|
||||
|
||||
|
||||
class TestCommandBindCommand:
|
||||
"""Tests for binding commands to other commands with when parameter."""
|
||||
|
||||
@pytest.mark.parametrize("when_param, expected_when", [
|
||||
(None, "after"), # default
|
||||
("before", "before"), # explicit before
|
||||
("after", "after"), # explicit after
|
||||
])
|
||||
def test_i_can_bind_command_with_when(self, owner, when_param, expected_when):
|
||||
"""Test that a command can be bound to another command with when parameter."""
|
||||
main_command = Command('main', 'Main command', owner, callback)
|
||||
bound_command = Command('bound', 'Bound command', owner, callback)
|
||||
|
||||
if when_param is None:
|
||||
owner.bind_command(main_command, bound_command)
|
||||
else:
|
||||
owner.bind_command(main_command, bound_command, when=when_param)
|
||||
|
||||
bound_commands = owner.get_bound_commands('main')
|
||||
|
||||
assert len(bound_commands) == 1
|
||||
assert bound_commands[0].command is bound_command
|
||||
assert bound_commands[0].when == expected_when
|
||||
|
||||
def test_i_can_bind_multiple_commands_with_different_when(self, owner):
|
||||
"""Test that multiple commands can be bound with different when values."""
|
||||
main_command = Command('main', 'Main command', owner, callback)
|
||||
before_command = Command('before', 'Before command', owner, callback)
|
||||
after_command = Command('after', 'After command', owner, callback)
|
||||
|
||||
owner.bind_command(main_command, before_command, when="before")
|
||||
owner.bind_command(main_command, after_command, when="after")
|
||||
|
||||
bound_commands = owner.get_bound_commands('main')
|
||||
|
||||
assert len(bound_commands) == 2
|
||||
assert bound_commands[0].command is before_command
|
||||
assert bound_commands[0].when == "before"
|
||||
assert bound_commands[1].command is after_command
|
||||
assert bound_commands[1].when == "after"
|
||||
|
||||
@@ -351,6 +351,7 @@ class TestErrorComparisonOutput:
|
||||
res = comparison_out.render()
|
||||
|
||||
assert "\n" + res == '''
|
||||
===== Actual ====== | ==== Expected =====
|
||||
(div "attr1"="value1" | (div "attr1"="value1"
|
||||
(p "id"="p_id") | (p "id"="p_id")
|
||||
) | )'''
|
||||
@@ -366,6 +367,7 @@ class TestErrorComparisonOutput:
|
||||
res = comparison_out.render()
|
||||
|
||||
assert "\n" + res == '''
|
||||
======= Actual ======== | ====== Expected =======
|
||||
(div "id"="div_id" ... | (div "id"="div_id" ...
|
||||
(span "class"="cls" ... | (span "class"="cls" ...
|
||||
(div "attr1"="value1" | (div "attr1"="value1"
|
||||
@@ -383,6 +385,7 @@ class TestErrorComparisonOutput:
|
||||
res = comparison_out.render()
|
||||
|
||||
assert "\n" + res == '''
|
||||
========= Actual ========= | ==== Expected =====
|
||||
(div "attr2"="** MISSING **" | (div "attr2"="value1"
|
||||
^^^^^^^^^^^^^^^^^^^^^^^ |
|
||||
(p "id"="p_id") | (p "id"="p_id")
|
||||
@@ -399,6 +402,7 @@ class TestErrorComparisonOutput:
|
||||
res = comparison_out.render()
|
||||
|
||||
assert "\n" + res == '''
|
||||
===== Actual ====== | ==== Expected =====
|
||||
(div "attr1"="value2" | (div "attr1"="value1"
|
||||
^^^^^^^^^^^^^^^^ |
|
||||
(p "id"="p_id") | (p "id"="p_id")
|
||||
@@ -415,6 +419,7 @@ class TestErrorComparisonOutput:
|
||||
res = comparison_out.render()
|
||||
|
||||
assert "\n" + res == '''
|
||||
===== Actual ====== | ==== Expected =====
|
||||
(div "attr1"="value1" | (div "attr1"="value1"
|
||||
(p "id"="p_id") | (span "id"="s_id")
|
||||
^ ^^^^^^^^^^^ |
|
||||
@@ -432,6 +437,7 @@ class TestErrorComparisonOutput:
|
||||
assert "\n" + debug_output == """
|
||||
Path : 'div'
|
||||
Error : The condition 'Contains(value2)' is not satisfied.
|
||||
====== Actual ====== | ========== Expected ==========
|
||||
(div "attr1"="value1") | (div "attr1"="Contains(value2)")
|
||||
^^^^^^^^^^^^^^^^ |"""
|
||||
|
||||
@@ -447,6 +453,7 @@ Error : The condition 'Contains(value2)' is not satisfied.
|
||||
res = comparison_out.render()
|
||||
|
||||
assert "\n" + res == '''
|
||||
============= Actual ============= | ============= Expected =============
|
||||
(div "attr1"="123" "attr2"="value2") | (Dummy "attr1"="123" "attr2"="value2")
|
||||
^^^ |'''
|
||||
|
||||
|
||||
Reference in New Issue
Block a user