Improving functionalities and adding unit tests
This commit is contained in:
@@ -7,7 +7,7 @@ from components.BaseComponent import BaseComponentSingleton
|
|||||||
from components.undo_redo.assets.icons import icon_redo, icon_undo
|
from components.undo_redo.assets.icons import icon_redo, icon_undo
|
||||||
from components.undo_redo.commands import UndoRedoCommandManager
|
from components.undo_redo.commands import UndoRedoCommandManager
|
||||||
from components.undo_redo.constants import UNDO_REDO_INSTANCE_ID
|
from components.undo_redo.constants import UNDO_REDO_INSTANCE_ID
|
||||||
from components_helpers import mk_icon
|
from components_helpers import mk_icon, mk_tooltip
|
||||||
|
|
||||||
logger = logging.getLogger("UndoRedoApp")
|
logger = logging.getLogger("UndoRedoApp")
|
||||||
|
|
||||||
@@ -40,50 +40,67 @@ class UndoRedo(BaseComponentSingleton):
|
|||||||
self.index += 1
|
self.index += 1
|
||||||
|
|
||||||
def undo(self):
|
def undo(self):
|
||||||
logger.info(f"Undo command")
|
logger.debug(f"Undo command")
|
||||||
return self
|
if self.index == 0:
|
||||||
|
logger.debug(f" No command to undo.")
|
||||||
|
return self
|
||||||
|
|
||||||
|
self.index -= 1
|
||||||
|
command = self.history[self.index]
|
||||||
|
logger.debug(f" Undoing command {command.name} ({command.desc})")
|
||||||
|
res = command.undo()
|
||||||
|
return self, res
|
||||||
|
|
||||||
def redo(self):
|
def redo(self):
|
||||||
logger.info("Redo command")
|
logger.debug("Redo command")
|
||||||
if self.index > 0:
|
if self.index >= len(self.history):
|
||||||
self.history.clear()
|
logger.debug(f" No command to redo.")
|
||||||
self.index = 0
|
return self
|
||||||
else:
|
|
||||||
self.push("something")
|
command = self.history[self.index]
|
||||||
return self
|
logger.debug(f" Redoing command {command.name} ({command.desc})")
|
||||||
|
res = command.redo()
|
||||||
|
self.index += 1
|
||||||
|
return self, res
|
||||||
|
|
||||||
def __ft__(self):
|
def __ft__(self, oob=False):
|
||||||
return Div(
|
return Div(
|
||||||
self._mk_undo(),
|
self._mk_undo(),
|
||||||
self._mk_redo(),
|
self._mk_redo(),
|
||||||
id=self._id,
|
id=self._id,
|
||||||
cls="flex"
|
cls="flex",
|
||||||
|
hx_swap_oob="true" if oob else None
|
||||||
)
|
)
|
||||||
|
|
||||||
def _mk_undo(self):
|
def _mk_undo(self):
|
||||||
if self._can_undo():
|
if self._can_undo():
|
||||||
return mk_icon(icon_undo,
|
command = self.history[self.index - 1]
|
||||||
size=24,
|
return mk_tooltip(mk_icon(icon_undo,
|
||||||
**self._commands.undo())
|
size=24,
|
||||||
|
**self._commands.undo()),
|
||||||
|
"Undo")
|
||||||
else:
|
else:
|
||||||
return mk_icon(icon_undo,
|
return mk_tooltip(mk_icon(icon_undo,
|
||||||
size=24,
|
size=24,
|
||||||
can_select=False,
|
can_select=False,
|
||||||
cls="mmt-btn-disabled")
|
cls="mmt-btn-disabled"),
|
||||||
|
"Nothing to undo")
|
||||||
|
|
||||||
def _mk_redo(self):
|
def _mk_redo(self):
|
||||||
if self._can_redo():
|
if self._can_redo():
|
||||||
return mk_icon(icon_redo,
|
return mk_tooltip(mk_icon(icon_redo,
|
||||||
size=24,
|
size=24,
|
||||||
**self._commands.redo())
|
**self._commands.redo()),
|
||||||
|
"Redo")
|
||||||
else:
|
else:
|
||||||
return mk_icon(icon_redo,
|
return mk_tooltip(mk_icon(icon_redo,
|
||||||
size=24,
|
size=24,
|
||||||
can_select=False,
|
can_select=False,
|
||||||
cls="mmt-btn-disabled")
|
cls="mmt-btn-disabled"),
|
||||||
|
"Nothing to redo")
|
||||||
|
|
||||||
def _can_undo(self):
|
def _can_undo(self):
|
||||||
return self.index > 0
|
return self.index > 0
|
||||||
|
|
||||||
def _can_redo(self):
|
def _can_redo(self):
|
||||||
return self.index < len(self.history) - 1
|
return self.index < len(self.history)
|
||||||
|
|||||||
@@ -43,6 +43,12 @@ class Contains:
|
|||||||
"""
|
"""
|
||||||
s: str
|
s: str
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class DoesNotContain:
|
||||||
|
"""
|
||||||
|
To check if the attribute does not contain a specific value
|
||||||
|
"""
|
||||||
|
s: str
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class JsonViewerNode:
|
class JsonViewerNode:
|
||||||
@@ -449,6 +455,11 @@ def matches(actual, expected, path=""):
|
|||||||
elif isinstance(expected.attrs[expected_attr], Contains):
|
elif isinstance(expected.attrs[expected_attr], Contains):
|
||||||
assert expected.attrs[expected_attr].s in actual.attrs[expected_attr], \
|
assert expected.attrs[expected_attr].s in actual.attrs[expected_attr], \
|
||||||
f"{print_path(path)}Attribute '{expected_attr}' does not contain '{expected.attrs[expected_attr].s}': actual='{actual.attrs[expected_attr]}', expected ='{expected.attrs[expected_attr].s}'."
|
f"{print_path(path)}Attribute '{expected_attr}' does not contain '{expected.attrs[expected_attr].s}': actual='{actual.attrs[expected_attr]}', expected ='{expected.attrs[expected_attr].s}'."
|
||||||
|
|
||||||
|
elif isinstance(expected.attrs[expected_attr], DoesNotContain):
|
||||||
|
assert expected.attrs[expected_attr].s not in actual.attrs[expected_attr], \
|
||||||
|
f"{print_path(path)}Attribute '{expected_attr}' does contain '{expected.attrs[expected_attr].s}' while it must not: actual='{actual.attrs[expected_attr]}'."
|
||||||
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
assert actual.attrs[expected_attr] == expected.attrs[expected_attr], \
|
assert actual.attrs[expected_attr] == expected.attrs[expected_attr], \
|
||||||
@@ -750,13 +761,14 @@ def icon(name: str):
|
|||||||
return NotStr(f'<svg name="{name}"')
|
return NotStr(f'<svg name="{name}"')
|
||||||
|
|
||||||
|
|
||||||
def div_icon(name: str):
|
def div_icon(name: str, cls=None):
|
||||||
"""
|
"""
|
||||||
Test if an element is an icon wrapped in a div
|
Test if an element is an icon wrapped in a div
|
||||||
:param name:
|
:param name:
|
||||||
|
:param cls:
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return Div(NotStr(f'<svg name="{name}"'))
|
return Div(NotStr(f'<svg name="{name}"'), cls=cls)
|
||||||
|
|
||||||
|
|
||||||
def span_icon(name: str):
|
def span_icon(name: str):
|
||||||
|
|||||||
@@ -57,7 +57,8 @@ def sample_structure():
|
|||||||
"Path 'div[class=a long attr]':\n\tAttribute 'class' does not start with 'different start': actual='a long attr', expected ='different start'."),
|
"Path 'div[class=a long attr]':\n\tAttribute 'class' does not start with 'different start': actual='a long attr', expected ='different start'."),
|
||||||
(Div(cls="a long attr"), Div(cls=Contains("not included")),
|
(Div(cls="a long attr"), Div(cls=Contains("not included")),
|
||||||
"Path 'div[class=a long attr]':\n\tAttribute 'class' does not contain 'not included': actual='a long attr', expected ='not included'."),
|
"Path 'div[class=a long attr]':\n\tAttribute 'class' does not contain 'not included': actual='a long attr', expected ='not included'."),
|
||||||
|
(Div(cls="a long attr"), Div(cls=DoesNotContain("long attr")),
|
||||||
|
"Path 'div[class=a long attr]':\n\tAttribute 'class' does contain 'long attr' while it must not: actual='a long attr'."),
|
||||||
])
|
])
|
||||||
def test_matches_error_expected(value, expected, expected_error):
|
def test_matches_error_expected(value, expected, expected_error):
|
||||||
with pytest.raises(AssertionError) as error:
|
with pytest.raises(AssertionError) as error:
|
||||||
@@ -75,6 +76,7 @@ def test_matches_error_expected(value, expected, expected_error):
|
|||||||
(Div(), Div(Empty)),
|
(Div(), Div(Empty)),
|
||||||
(Div(cls="a long attr"), Div(cls=StartsWith("a long"))),
|
(Div(cls="a long attr"), Div(cls=StartsWith("a long"))),
|
||||||
(Div(cls="a long attr"), Div(cls=Contains("long"))),
|
(Div(cls="a long attr"), Div(cls=Contains("long"))),
|
||||||
|
(Div(cls="a long attr"), Div(cls=DoesNotContain("xxxx"))),
|
||||||
])
|
])
|
||||||
def test_matches_success_expected(value, expected):
|
def test_matches_success_expected(value, expected):
|
||||||
assert matches(value, expected)
|
assert matches(value, expected)
|
||||||
|
|||||||
@@ -1,7 +1,24 @@
|
|||||||
from components.undo_redo.components.UndoRedo import UndoRedo
|
import pytest
|
||||||
from core.settings_management import SettingsManager, MemoryDbEngine
|
from fasthtml.components import Div
|
||||||
|
|
||||||
|
from components.undo_redo.components.UndoRedo import UndoRedo, CommandHistory
|
||||||
|
from core.settings_management import SettingsManager, MemoryDbEngine
|
||||||
|
from helpers import matches, div_icon, Contains, DoesNotContain
|
||||||
|
from my_mocks import tabs_manager
|
||||||
|
|
||||||
|
|
||||||
|
class UndoableCommand(CommandHistory):
|
||||||
|
def __init__(self, old_value=0, new_value=0):
|
||||||
|
super().__init__("command", f"The new value is {new_value}")
|
||||||
|
self.old_value = old_value
|
||||||
|
self.new_value = new_value
|
||||||
|
|
||||||
|
def undo(self):
|
||||||
|
return Div(self.old_value, hx_swap_oob="true")
|
||||||
|
|
||||||
|
def redo(self):
|
||||||
|
return Div(self.new_value, hx_swap_oob="true")
|
||||||
|
|
||||||
TEST_UNDO_REDO_INSTANCE_ID = "test_undo_redo_instance_id"
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def undo_redo(session, tabs_manager):
|
def undo_redo(session, tabs_manager):
|
||||||
@@ -11,5 +28,74 @@ def undo_redo(session, tabs_manager):
|
|||||||
tabs_manager=tabs_manager)
|
tabs_manager=tabs_manager)
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_can_render(undo_redo):
|
||||||
|
actual = undo_redo.__ft__()
|
||||||
|
expected = Div(
|
||||||
|
Div(div_icon("undo", cls=Contains("mmt-btn-disabled")), data_tooltip="Nothing to undo"),
|
||||||
|
Div(div_icon("redo", cls=Contains("mmt-btn-disabled")), data_tooltip="Nothing to redo"),
|
||||||
|
id=undo_redo.get_id(),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert matches(actual, expected)
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_can_render_when_undoing_and_redoing(undo_redo):
|
||||||
|
undo_redo.push(UndoableCommand())
|
||||||
|
undo_redo.push(UndoableCommand())
|
||||||
|
|
||||||
|
actual = undo_redo.__ft__()
|
||||||
|
expected = Div(
|
||||||
|
Div(div_icon("undo", cls=DoesNotContain("mmt-btn-disabled")), data_tooltip="Undo "),
|
||||||
|
Div(div_icon("redo", cls=Contains("mmt-btn-disabled")), data_tooltip=""),
|
||||||
|
id=undo_redo.get_id(),
|
||||||
|
)
|
||||||
|
assert matches(actual, expected)
|
||||||
|
|
||||||
|
undo_redo.undo() # The command is now undone. We can redo it and undo the first command.
|
||||||
|
actual = undo_redo.__ft__()
|
||||||
|
expected = Div(
|
||||||
|
div_icon("undo", cls=DoesNotContain("mmt-btn-disabled")),
|
||||||
|
div_icon("redo", cls=DoesNotContain("mmt-btn-disabled")),
|
||||||
|
id=undo_redo.get_id(),
|
||||||
|
)
|
||||||
|
assert matches(actual, expected)
|
||||||
|
|
||||||
|
undo_redo.undo() # Undo again, I cannot undo anymore.
|
||||||
|
actual = undo_redo.__ft__()
|
||||||
|
expected = Div(
|
||||||
|
div_icon("undo", cls=Contains("mmt-btn-disabled")),
|
||||||
|
div_icon("redo", cls=DoesNotContain("mmt-btn-disabled")),
|
||||||
|
id=undo_redo.get_id(),
|
||||||
|
)
|
||||||
|
assert matches(actual, expected)
|
||||||
|
|
||||||
|
undo_redo.redo() # Redo once.
|
||||||
|
actual = undo_redo.__ft__()
|
||||||
|
expected = Div(
|
||||||
|
div_icon("undo", cls=DoesNotContain("mmt-btn-disabled")),
|
||||||
|
div_icon("redo", cls=DoesNotContain("mmt-btn-disabled")),
|
||||||
|
id=undo_redo.get_id(),
|
||||||
|
)
|
||||||
|
assert matches(actual, expected)
|
||||||
|
|
||||||
|
undo_redo.redo() # Redo a second time.
|
||||||
|
actual = undo_redo.__ft__()
|
||||||
|
expected = Div(
|
||||||
|
div_icon("undo", cls=DoesNotContain("mmt-btn-disabled")),
|
||||||
|
div_icon("redo", cls=Contains("mmt-btn-disabled")),
|
||||||
|
id=undo_redo.get_id(),
|
||||||
|
)
|
||||||
|
assert matches(actual, expected)
|
||||||
|
|
||||||
|
|
||||||
def test_i_can_undo_and_redo(undo_redo):
|
def test_i_can_undo_and_redo(undo_redo):
|
||||||
pass
|
undo_redo.push(UndoableCommand(0, 1))
|
||||||
|
undo_redo.push(UndoableCommand(1, 2))
|
||||||
|
|
||||||
|
self, res = undo_redo.undo()
|
||||||
|
expected = Div(1, hx_swap_oob="true")
|
||||||
|
assert matches(res, expected)
|
||||||
|
|
||||||
|
self, res = undo_redo.redo()
|
||||||
|
expected = Div(2, hx_swap_oob="true")
|
||||||
|
assert matches(res, expected)
|
||||||
|
|||||||
Reference in New Issue
Block a user