diff --git a/src/components/undo_redo/components/UndoRedo.py b/src/components/undo_redo/components/UndoRedo.py index 56af2bd..edb00b4 100644 --- a/src/components/undo_redo/components/UndoRedo.py +++ b/src/components/undo_redo/components/UndoRedo.py @@ -1,6 +1,7 @@ import logging from dataclasses import dataclass +from fastcore.xml import FT from fasthtml.components import * from components.BaseComponent import BaseComponentSingleton @@ -8,6 +9,7 @@ from components.undo_redo.assets.icons import icon_redo, icon_undo from components.undo_redo.commands import UndoRedoCommandManager from components.undo_redo.constants import UNDO_REDO_INSTANCE_ID, UndoRedoAttrs from components_helpers import mk_icon, mk_tooltip +from core.settings_management import NoDefault logger = logging.getLogger("UndoRedoApp") @@ -64,13 +66,19 @@ class UndoRedo(BaseComponentSingleton): previous_state = self._settings_manager.load(self._session, None, digest=previous.digest) # reapply the state - current_state[current.key] = previous_state[current.key] + if previous_state is not NoDefault: + current_state[current.key] = previous_state[current.key] + else: + del current_state[current.key] self._settings_manager.save(self._session, current.entry, current_state) self.index -= 1 if current.attrs.on_undo is not None: - return self, current.attrs.on_undo() + ret = current.attrs.on_undo() + if isinstance(ret, FT) and 'id' in ret.attrs: + ret.attrs["hx-swap-oob"] = "true" + return self, ret else: return self @@ -87,13 +95,19 @@ class UndoRedo(BaseComponentSingleton): next_state = self._settings_manager.load(self._session, None, digest=next_.digest) # reapply the state - current_state[current.key] = next_state[current.key] + if current_state is not NoDefault: + current_state[current.key] = next_state[current.key] + else: + current_state = {current.key : next_state[current.key]} self._settings_manager.save(self._session, current.entry, current_state) self.index += 1 - if current.attrs.on_undo is not None: - return self, current.attrs.on_redo() + if current.attrs.on_redo is not None: + ret = current.attrs.on_undo() + if isinstance(ret, FT) and 'id' in ret.attrs: + ret.attrs["hx-swap-oob"] = "true" + return self, ret else: return self @@ -125,7 +139,7 @@ class UndoRedo(BaseComponentSingleton): def _mk_redo(self): if self._can_redo(): - command = self.history[self.index] + command = self.history[self.index + 1] return mk_tooltip(mk_icon(icon_redo, size=24, **self._commands.redo()), diff --git a/tests/test_undo_redo.py b/tests/test_undo_redo.py index 9082d62..a245e4a 100644 --- a/tests/test_undo_redo.py +++ b/tests/test_undo_redo.py @@ -1,20 +1,66 @@ +import os +import shutil + import pytest from fasthtml.components import Div from components.undo_redo.components.UndoRedo import UndoRedo +from components.undo_redo.constants import UndoRedoAttrs +from core.dbengine import DbEngine from core.settings_management import SettingsManager, MemoryDbEngine from helpers import matches, div_icon, Contains, DoesNotContain from my_mocks import tabs_manager +DB_ENGINE_ROOT = "undo_redo_test_db" +TEST_DB_ENTRY = "TestDbEntry" +TEST_DB_KEY = "TestDbKey" + + +class TestCommand: + def __init__(self, value): + self.value = value + + def __eq__(self, other): + if not isinstance(other, TestCommand): + return False + + return self.value == other.value + + def __hash__(self): + return hash(self.value) + + +@pytest.fixture() +def engine(session): + if os.path.exists(DB_ENGINE_ROOT): + shutil.rmtree(DB_ENGINE_ROOT) + + engine = DbEngine(DB_ENGINE_ROOT) + engine.init(session["user_id"]) + + yield engine + + shutil.rmtree(DB_ENGINE_ROOT) + + +@pytest.fixture() +def settings_manager(engine): + return SettingsManager(engine=engine) + @pytest.fixture -def undo_redo(session, tabs_manager): +def undo_redo(session, tabs_manager, settings_manager): return UndoRedo(session, UndoRedo.create_component_id(session), - settings_manager=SettingsManager(engine=MemoryDbEngine()), + settings_manager=settings_manager, tabs_manager=tabs_manager) +def init_command(session, settings_manager, undo_redo, value, on_undo=None): + settings_manager.save(session, TEST_DB_ENTRY, {TEST_DB_KEY: TestCommand(value)}) + undo_redo.snapshot(UndoRedoAttrs(f"Set value to {value}", on_undo=on_undo), TEST_DB_ENTRY, TEST_DB_KEY) + + def test_i_can_render(undo_redo): actual = undo_redo.__ft__() expected = Div( @@ -26,13 +72,13 @@ def test_i_can_render(undo_redo): assert matches(actual, expected) -def test_i_can_render_when_undoing_and_redoing(undo_redo): - undo_redo.push(UndoableCommand(0, 1)) - undo_redo.push(UndoableCommand(1, 2)) +def test_i_can_render_when_undoing_and_redoing(session, settings_manager, undo_redo): + init_command(session, settings_manager, undo_redo, "1") + init_command(session, settings_manager, undo_redo, "2") actual = undo_redo.__ft__() expected = Div( - Div(div_icon("undo", cls=DoesNotContain("mmt-btn-disabled")), data_tooltip="Undo 'Set new value'."), + Div(div_icon("undo", cls=DoesNotContain("mmt-btn-disabled")), data_tooltip="Undo 'Set value to 2'."), Div(div_icon("redo", cls=Contains("mmt-btn-disabled")), data_tooltip="Nothing to redo."), id=undo_redo.get_id(), ) @@ -41,8 +87,8 @@ def test_i_can_render_when_undoing_and_redoing(undo_redo): undo_redo.undo() # The command is now undone. We can redo it and undo the first command. actual = undo_redo.__ft__() expected = Div( - Div(div_icon("undo", cls=DoesNotContain("mmt-btn-disabled")), data_tooltip="Undo 'Set new value'."), - Div(div_icon("redo", cls=DoesNotContain("mmt-btn-disabled")), data_tooltip="Redo 'Set new value'."), + Div(div_icon("undo", cls=DoesNotContain("mmt-btn-disabled")), data_tooltip="Undo 'Set value to 1'."), + Div(div_icon("redo", cls=DoesNotContain("mmt-btn-disabled")), data_tooltip="Redo 'Set value to 2'."), id=undo_redo.get_id(), ) assert matches(actual, expected) @@ -75,26 +121,48 @@ def test_i_can_render_when_undoing_and_redoing(undo_redo): assert matches(actual, expected) -def test_i_can_undo_and_redo(undo_redo): - undo_redo.push(UndoableCommand(0, 1)) - undo_redo.push(UndoableCommand(1, 2)) +def test_values_are_correctly_reset(session, settings_manager, undo_redo): + # checks that the values are correctly returned + # Only checks that hx_swap_oob="true" is automatically put when id is present in the return + + def on_undo(): + current = settings_manager.get(session, TEST_DB_ENTRY, TEST_DB_KEY) + return Div(current.value, id='an_id') + + init_command(session, settings_manager, undo_redo, "1", on_undo=on_undo) + init_command(session, settings_manager, undo_redo, "2", on_undo=on_undo) self, res = undo_redo.undo() - expected = Div(1, hx_swap_oob="true") + expected = Div("1", id='an_id', hx_swap_oob="true") assert matches(res, expected) self, res = undo_redo.redo() - expected = Div(2, hx_swap_oob="true") + expected = Div("2", id='an_id', hx_swap_oob="true") assert matches(res, expected) -def test_history_is_rewritten_when_pushing_a_command(undo_redo): - undo_redo.push(UndoableCommand(0, 1)) - undo_redo.push(UndoableCommand(1, 2)) - undo_redo.push(UndoableCommand(2, 3)) +def test_i_can_manage_when_the_entry_was_not_present(session, settings_manager, undo_redo): + def on_undo(): + snapshot = settings_manager.load(session, TEST_DB_ENTRY) + if TEST_DB_KEY in snapshot: + return Div(snapshot[TEST_DB_KEY].value, id='an_id') + else: + return Div("**Not Found**", id='an_id') + + init_command(session, settings_manager, undo_redo, "1", on_undo=on_undo) + + self, res = undo_redo.undo() + expected = Div("**Not Found**", id='an_id', hx_swap_oob="true") + assert matches(res, expected) + + +def test_history_is_rewritten_when_pushing_a_command_after_undo(session, settings_manager, undo_redo): + init_command(session, settings_manager, undo_redo, "1") + init_command(session, settings_manager, undo_redo, "2") + init_command(session, settings_manager, undo_redo, "3") undo_redo.undo() undo_redo.undo() - undo_redo.push(UndoableCommand(1, 5)) + init_command(session, settings_manager, undo_redo, "5") - assert len(undo_redo.history) == 2 + assert len(undo_redo.history) == 3 # do not forget that history always has a default command with digest = None diff --git a/tests/test_workflow_designer.py b/tests/test_workflow_designer.py index fecd94b..88416c3 100644 --- a/tests/test_workflow_designer.py +++ b/tests/test_workflow_designer.py @@ -1,11 +1,15 @@ +from unittest.mock import MagicMock + import pytest from fastcore.basics import NotStr from fasthtml.components import * from fasthtml.xtend import Script +from components.undo_redo.components.UndoRedo import UndoRedo from components.workflows.components.WorkflowDesigner import WorkflowDesigner, COMPONENT_TYPES from components.workflows.constants import ProcessorTypes from components.workflows.db_management import WorkflowsDesignerSettings, WorkflowComponent, Connection +from core.instance_manager import InstanceManager from core.settings_management import SettingsManager, MemoryDbEngine from helpers import matches, Contains from my_mocks import tabs_manager @@ -13,6 +17,27 @@ from my_mocks import tabs_manager TEST_WORKFLOW_DESIGNER_ID = "workflow_designer_id" +@pytest.fixture(autouse=True) +def mock_undo_redo(session): + # Create a mock UndoRedo instance + undo_redo = MagicMock(spec=UndoRedo) + + # Store original get method + original_get = InstanceManager.get + + def mock_get(sess, instance_id, *args, **kwargs): + if instance_id == UndoRedo.create_component_id(sess): + return undo_redo + return original_get(sess, instance_id, *args, **kwargs) + + # Replace get method with our mock + InstanceManager.get = mock_get + + yield undo_redo + + # Restore original get method after test + InstanceManager.get = original_get + @pytest.fixture def designer(session, tabs_manager): return WorkflowDesigner(session=session, _id=TEST_WORKFLOW_DESIGNER_ID, diff --git a/tests/test_workflow_player.py b/tests/test_workflow_player.py index 1fc5e77..c146eb3 100644 --- a/tests/test_workflow_player.py +++ b/tests/test_workflow_player.py @@ -4,10 +4,12 @@ import pandas as pd import pytest from pandas.testing import assert_frame_equal +from components.undo_redo.components.UndoRedo import UndoRedo from components.workflows.components.WorkflowDesigner import COMPONENT_TYPES, WorkflowDesigner from components.workflows.components.WorkflowPlayer import WorkflowPlayer, WorkflowsPlayerError from components.workflows.constants import ProcessorTypes from components.workflows.db_management import WorkflowComponent, Connection, ComponentState, WorkflowsDesignerSettings +from core.instance_manager import InstanceManager from core.settings_management import SettingsManager, MemoryDbEngine from my_mocks import tabs_manager from workflow.engine import DataProcessorError @@ -16,6 +18,27 @@ TEST_WORKFLOW_DESIGNER_ID = "workflow_designer_id" TEST_WORKFLOW_PLAYER_ID = "workflow_player_id" +@pytest.fixture(autouse=True) +def mock_undo_redo(session): + # Create a mock UndoRedo instance + undo_redo = MagicMock(spec=UndoRedo) + + # Store original get method + original_get = InstanceManager.get + + def mock_get(sess, instance_id, *args, **kwargs): + if instance_id == UndoRedo.create_component_id(sess): + return undo_redo + return original_get(sess, instance_id, *args, **kwargs) + + # Replace get method with our mock + InstanceManager.get = mock_get + + yield undo_redo + + # Restore original get method after test + InstanceManager.get = original_get + @pytest.fixture def settings_manager(): return SettingsManager(MemoryDbEngine()) diff --git a/tests/test_workflows.py b/tests/test_workflows.py index 3b9b8f2..1587193 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -1,8 +1,12 @@ +from unittest.mock import MagicMock + import pytest from fasthtml.components import * from components.form.components.MyForm import FormField, MyForm +from components.undo_redo.components.UndoRedo import UndoRedo from components.workflows.components.Workflows import Workflows +from core.instance_manager import InstanceManager from core.settings_management import SettingsManager, MemoryDbEngine from helpers import matches, div_icon, search_elements_by_name, Contains from my_mocks import tabs_manager @@ -18,6 +22,28 @@ def workflows(session, tabs_manager): tabs_manager=tabs_manager) +@pytest.fixture(autouse=True) +def mock_undo_redo(session): + # Create a mock UndoRedo instance + undo_redo = MagicMock(spec=UndoRedo) + + # Store original get method + original_get = InstanceManager.get + + def mock_get(sess, instance_id, *args, **kwargs): + if instance_id == UndoRedo.create_component_id(sess): + return undo_redo + return original_get(sess, instance_id, *args, **kwargs) + + # Replace get method with our mock + InstanceManager.get = mock_get + + yield undo_redo + + # Restore original get method after test + InstanceManager.get = original_get + + def test_render_no_workflow(workflows): actual = workflows.__ft__() expected = Div(