Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
b8d15ef
refactor: hardcoded values
FelipeDefensor Jul 26, 2025
27f7e75
chore: auto-format
FelipeDefensor Jul 27, 2025
914a0bd
refactor: remove unused code
FelipeDefensor Aug 14, 2025
6964fdd
fix: do not usable mutable default argument
FelipeDefensor Aug 26, 2025
7c3f233
fix: crash when creating score annotation
FelipeDefensor Sep 5, 2025
3be8cee
fix: crash when loading svg viewer without beat timeline
FelipeDefensor Nov 7, 2025
24576bd
fix: restore state with identical components but different ids
FelipeDefensor Nov 6, 2025
f07fd4d
test: record state after creating timelines with fixtures
FelipeDefensor Nov 13, 2025
974e715
refactor: rename Post.APP_RECORD_STATE to Post.APP_STATE_RECORD
FelipeDefensor Nov 13, 2025
467cb5e
test: set timeline visibility
FelipeDefensor Nov 7, 2025
4f4be13
fix: color validator
FelipeDefensor Nov 7, 2025
769e30b
refactor: start using {Timeline,TimelineUI}.subclasses instead of tim…
FelipeDefensor Aug 26, 2025
f13fa08
refactor: use Timeline.flags instead of individual class attributes
FelipeDefensor Aug 26, 2025
4c534a8
refactor: commands APIS [MAJOR]
FelipeDefensor Nov 25, 2025
63abd6b
fix: detection of empty timelines
FelipeDefensor Nov 12, 2025
d33d581
refactor: take "time" parameters to on_add method of point-like Timel…
FelipeDefensor Nov 12, 2025
1a31ad5
test: fix errors on test_dirs.py
FelipeDefensor Nov 25, 2025
ae05c63
fix: duplicated error message
FelipeDefensor Nov 26, 2025
7e64d1f
fix: error message wording
FelipeDefensor Nov 26, 2025
45dd1da
docs: add debug tip
FelipeDefensor Nov 26, 2025
0fae6ec
fix: undo/redo when importing
FelipeDefensor Nov 26, 2025
cfa4ab0
refactor: remove dead code
FelipeDefensor Nov 26, 2025
7074f76
fix: toggle timeline visibility
FelipeDefensor Feb 3, 2026
817a0ad
dev: clearer "Unregistered command" error output
FelipeDefensor Feb 5, 2026
b13eb27
fix: preserve timeline ids when opening files and undoing/redoing
FelipeDefensor Feb 5, 2026
cae09c6
fix: do not crash on race condition for creation of dirs.SITE_DATA_DIR
FelipeDefensor Feb 5, 2026
0b99b59
test: use deepdiff to print mismatching app states
FelipeDefensor Feb 5, 2026
6eb1f2a
fix: crash when using timeline context menus
FelipeDefensor Feb 5, 2026
b90e4a0
fix: crash when deleting beat timeline after score timeline has been …
FelipeDefensor Feb 5, 2026
1c5a8ad
fix: "//" in YouTube URL being replaced with "/" when saving
FelipeDefensor Feb 10, 2026
6d9a8a2
refactor: use patch_yes_or_no instead of Serve
FelipeDefensor Feb 17, 2026
1b6af29
fix: crash when timeline folder doesn't have the right structure
FelipeDefensor Feb 27, 2026
8500205
test: fix failing tests on Linux and macOS
FelipeDefensor Feb 27, 2026
c28cf26
test: fix failing tests on Linux and macOS
FelipeDefensor Feb 27, 2026
fb912be
refactor: remove unused posts
azfoo Mar 4, 2026
9db3142
refactor: actually call the post
azfoo Mar 4, 2026
7108397
refactor: remove duplicate
azfoo Mar 4, 2026
946d599
refactor: remove unused gets
azfoo Mar 4, 2026
9633a12
refactor: remove unused tl selectors
azfoo Mar 4, 2026
9bfb4c4
refactor: correct method name
azfoo Mar 4, 2026
305f6d5
fix: restore missing inspector value
azfoo Mar 4, 2026
66944b5
refactor: actually delete the file
azfoo Mar 6, 2026
609ee14
refactor: remove unused methods
azfoo Mar 12, 2026
ab51fd3
refactor: typing
azfoo Mar 12, 2026
bbaa5c7
docs: typo
azfoo Mar 12, 2026
e73be76
test: add helper functions for saving and opening tilia files
FelipeDefensor Mar 13, 2026
9b48a08
fix: duplicated IDs when using previously saved files
FelipeDefensor Mar 13, 2026
8b77a65
fix: restore deleted get
azfoo Mar 13, 2026
a87067d
refactor: boolean comparison
FelipeDefensor Mar 17, 2026
39f54f7
fix: error message on failed command with a partial for a callback
FelipeDefensor Dec 22, 2025
23c2eda
dev: improve unregistered command error message
FelipeDefensor Mar 17, 2026
df61c27
refactor: boolean comparison
FelipeDefensor Mar 17, 2026
eff94b9
dev: improve docstring
FelipeDefensor Mar 17, 2026
26ebdf7
perf: id generation
FelipeDefensor Mar 17, 2026
0793fd5
fix: crash when timeline command callback was not a function
FelipeDefensor Mar 17, 2026
a9654aa
fix: invalid return values for timeline commands not being detected
FelipeDefensor Mar 17, 2026
09d3d76
perf: import timeline subclasses only once
FelipeDefensor Mar 18, 2026
3251835
refactor: remove unused TimelineFlag
FelipeDefensor Mar 18, 2026
0b2fca4
refactor: always set TimelineFlags explicitly
FelipeDefensor Mar 18, 2026
72bcd07
fix: color value validator
FelipeDefensor Mar 18, 2026
42a7bf6
fix: menu accelerators not working on mac
FelipeDefensor Mar 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ The test suite is written in pytest. Below are some things to keep in my mind wh
Unfortunately, we can't simulate input to modal dialogs, as they block execution. To work around that, we can:
- Mock methods of the modal dialogs (e.g. `QInputDialog.getInt`). There are utility functions that do that in some cases (e.g. `tests.utils.patch_file_dialog`)
- If the dialog is called in response to a `Get` request, the `Serve` context manager can be used to mock the return value of the request. E.g.:

```python
with Serve(Get.FROM_USER_INT, (True, 150)):
user_actions.trigger(TiliaAction.TIMELINE_HEIGHT_SET)
commands.execute("timeline_height_set")
```

We should prefer the first option as it makes the test cover more code, but the second is more resilient to changes in implementation details.
Expand All @@ -31,10 +32,11 @@ def test_me(tlui, marker_tlui):
```

can be rewritten as:

```python
def test_me(marker_tlui, user_actions, tilia_state):
def test_me(marker_tlui, tilia_state):
tilia_state.current_time = 0
user_action.trigger(TiliaAction.MARKER_ADD)
commands.execute("marker_add")
assert not len(marker_tlui) == 1
```
You will find many examples of the former in the test suite, though. Refactors are welcome.
Expand Down
48 changes: 0 additions & 48 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
import tilia.settings as settings_module
from tilia.dirs import PROJECT_ROOT
from tilia.media.player.base import MediaTimeChangeReason
from tilia.ui import actions as tilia_actions_module
from tilia.app import App
from tilia.boot import setup_logic
from tilia.requests import (
Expand All @@ -25,7 +24,6 @@
get,
listen,
)
from tilia.ui.actions import TiliaAction, setup_actions
from tilia.ui.qtui import QtUI, TiliaMainWindow
from tilia.ui.cli.ui import CLI
from tilia.ui.windows import WindowKind
Expand Down Expand Up @@ -227,8 +225,6 @@ def qtui(tilia, cleanup_requests, qapplication, use_test_settings, use_test_logg
# noinspection PyProtectedMember
@pytest.fixture(scope="module")
def tilia(cleanup_requests):
mw = TiliaMainWindow()
setup_actions(mw)
tilia_ = setup_logic(autosaver=False)
tilia_.set_file_media_duration(100)
tilia_.reset_undo_manager()
Expand Down Expand Up @@ -278,50 +274,6 @@ def tlui(
}[request.param]


class UserActionManager:
"""
Class to simulate and mock user interaction with the GUI.
"""

def __init__(self):
self.action_to_trigger_count = {}
for action in tilia_actions_module.TiliaAction:
qaction = tilia_actions_module.get_qaction(action)
qaction.triggered.connect(
functools.partial(self._increment_trigger_count, action)
)

def trigger(self, action: TiliaAction):
tilia_actions_module.trigger(action)
self._increment_trigger_count(action)

def _increment_trigger_count(self, action):
if action not in self.action_to_trigger_count:
self.action_to_trigger_count[action] = 1
else:
self.action_to_trigger_count[action] += 1

def assert_triggered(self, action):
assert action in self.action_to_trigger_count

def assert_not_triggered(self, action):
assert action not in self.action_to_trigger_count


@pytest.fixture
def user_actions():
"""
Fixture to simulate and mock user interaction with the GUI.
This should be used with the <kind>_tlui fixtures
and NOT with <kind>_tl fixtures, as the latter do not
create corresponding TimelineUIS. Tests with <kind>_tl
may still pass if they use this fixture, provided they do
not require a TimelineUI.
"""
action_manager = UserActionManager()
yield action_manager


def parametrize_tl(func):
"""Adds a parameter 'tl' to a test that receives the name of a fixture that returns a component.
To get the timeline from within the test, add the `request` fixture to its arguments and
Expand Down
40 changes: 18 additions & 22 deletions tests/file/test_file_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from tilia.file.file_manager import FileManager
from unittest.mock import patch

from tilia.ui.actions import TiliaAction
from tilia.ui import commands


def get_empty_save_params():
Expand All @@ -25,23 +25,23 @@ def get_empty_save_params():
} | {"timelines_hash": ""}


class TestUserActions:
def test_save(self, tls, marker_tlui, tmp_path, user_actions):
class Tests:
def test_save(self, tls, marker_tlui, tmp_path):
marker_tlui.create_marker(0)
tmp_file_path = (tmp_path / "test_save.tla").resolve().__str__()
with patch_file_dialog(True, [tmp_file_path]):
user_actions.trigger(TiliaAction.FILE_SAVE_AS)
commands.execute("file.save_as")
marker_tlui.create_marker(1)
user_actions.trigger(TiliaAction.FILE_SAVE)
commands.execute("file.save")

with patch_yes_or_no_dialog(True):
user_actions.trigger(TiliaAction.TIMELINES_CLEAR)
commands.execute("timelines.clear_all")
assert marker_tlui.is_empty
with (
patch_file_dialog(True, [tmp_file_path]),
patch_yes_or_no_dialog(False), # do not save changes
):
user_actions.trigger(TiliaAction.FILE_OPEN)
commands.execute("file.open")
assert len(tls[0]) == 2


Expand Down Expand Up @@ -93,8 +93,8 @@ def test_is_file_modified_when_modified_timelines(
self, tilia, marker_tlui, tmp_path
):
with Serve(Get.FROM_USER_SAVE_PATH_TILIA, (True, tmp_path / "temp.tla")):
post(Post.FILE_SAVE)
post(Post.MARKER_ADD)
commands.execute("file.save")
commands.execute("timeline.marker.add")
assert tilia.file_manager.is_file_modified(get(Get.APP_STATE))

def test_is_file_modified_modified_tile(self, tilia):
Expand Down Expand Up @@ -144,13 +144,13 @@ def test_import_metadata_file_does_not_exist(self, tilia):
@pytest.mark.skipif(
not sys.platform.startswith("win"), reason="Windows specific test"
)
def test_is_saved_with_posix_paths(self, qtui, tilia, tmp_path, user_actions):
def test_is_saved_with_posix_paths(self, qtui, tilia, tmp_path):
file_path = tmp_path / "test.tla"
with patch_file_dialog(True, [str(EXAMPLE_MEDIA_PATH)]):
post(Post.UI_MEDIA_LOAD_LOCAL)
commands.execute("media.load.local")

with patch_file_dialog(True, [str(WindowsPath(file_path))]):
user_actions.trigger(TiliaAction.FILE_SAVE)
commands.execute("file.save")

with open(file_path, "r") as f:
data = json.load(f)
Expand All @@ -159,30 +159,26 @@ def test_is_saved_with_posix_paths(self, qtui, tilia, tmp_path, user_actions):
assert data["media_path"] == Path(EXAMPLE_MEDIA_PATH).as_posix()

def test_save_without_set_title_and_different_file_name(
self, qtui, tilia, tmp_path, user_actions
self, qtui, tilia, tmp_path
):
tla_path = tmp_path / "Some Title.tla"
with patch_file_dialog(True, [str(tla_path)]):
user_actions.trigger(TiliaAction.FILE_SAVE)
commands.execute("file.save")

assert tilia.file_manager.file.media_metadata["title"] == "Some Title"

def test_save_with_title_set_and_different_file_name(
self, qtui, tilia, tmp_path, user_actions
):
def test_save_with_title_set_and_different_file_name(self, qtui, tilia, tmp_path):
tilia.file_manager.file.media_metadata["title"] = "Title Already Set"
tla_path = tmp_path / "Some Title.tla"
with patch_file_dialog(True, [str(tla_path)]):
user_actions.trigger(TiliaAction.FILE_SAVE)
commands.execute("file.save")

assert tilia.file_manager.file.media_metadata["title"] == "Title Already Set"

def test_save_fail_reverts_to_original_name(
self, qtui, tilia, tmp_path, user_actions
):
def test_save_fail_reverts_to_original_name(self, qtui, tilia, tmp_path):
tla_path = tmp_path / "Non-existent Path" / "Some Other Title.tla"
with patch_file_dialog(True, [str(tla_path)]):
user_actions.trigger(TiliaAction.FILE_SAVE)
commands.execute("file.save")

assert tilia.file_manager.file.media_metadata[
"title"
Expand Down
50 changes: 0 additions & 50 deletions tests/mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,9 @@
from PyQt6.QtWidgets import QFileDialog, QMessageBox, QInputDialog

from tilia.requests import Get, Post, serve, server, stop_serving
from tilia.requests import get as get_original
from tilia.requests import post as post_original


class PatchGetMultiple:
def __init__(self, module: str, requests_to_return_values: dict[Get, Any]):
self.module = module
self.requests_to_return_values = requests_to_return_values
self.mock_request = None
self.mock = None

def __enter__(self):
self.patcher = patch(self.module + ".get", self.request_mock)
self.patcher.start()
self.mock = Mock()
return self.mock

def __exit__(self, exc_type, exc_val, exc_tb):
self.patcher.stop()

def request_mock(self, request: Get, *args, **kwargs):
if request in self.requests_to_return_values:
self.mock(request, *args, **kwargs)
return self.requests_to_return_values[request]
else:
return get_original(request, *args, **kwargs)


class Serve:
def __init__(self, request: Get, return_value: Any):
self.request = request
Expand Down Expand Up @@ -83,31 +58,6 @@ def _callback(self, *_, **__):
return return_value


class PatchGet:
def __init__(self, module: str, request_to_mock: Get, return_value: Any):
self.module = module
self.request_to_mock = request_to_mock
self.return_value = return_value
self.mock_request = None
self.mock = None

def __enter__(self):
self.patcher = patch(self.module + ".get", self.request_mock)
self.patcher.start()
self.mock = Mock()
return self.mock

def __exit__(self, exc_type, exc_val, exc_tb):
self.patcher.stop()

def request_mock(self, request: Get, *args, **kwargs):
if request == self.request_to_mock:
self.mock(request, *args, **kwargs)
return self.return_value
else:
return get_original(request, *args, **kwargs)


class PatchPost:
def __init__(self, module: str, request_to_mock: Post):
self.module = module
Expand Down
Loading
Loading