DazedTL/tests/test_rpgmaker_image_manager.py
DazedAnon d82895cc4d feat(rpgmaker): add scoped image patch preparation
- Patch highlighted editable images or all images when none are highlighted
- Skip unchanged images while preserving editable workspace copies
- Open the editable folder matching the current image or folder filter
2026-07-25 16:29:19 -05:00

301 lines
12 KiB
Python

import os
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from PIL import Image
from PyQt5.QtCore import QSettings, Qt
from PyQt5.QtTest import QTest
from PyQt5.QtWidgets import QApplication, QAbstractItemView, QMessageBox
from gui.rpgmaker_image_manager import RPGMakerImageManager, _PAGE_SIZE
class RPGMakerImageManagerSelectionTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.app = QApplication.instance() or QApplication([])
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
root = Path(self.temp.name)
self.game_root = root / "Game"
image_dir = self.game_root / "img" / "pictures"
data_dir = self.game_root / "data"
image_dir.mkdir(parents=True)
data_dir.mkdir(parents=True)
data_dir.joinpath("System.json").write_text(
'{"encryptionKey":"00112233445566778899aabbccddeeff"}',
encoding="utf-8",
)
for index in range(4):
Image.new("RGBA", (24, 24), (index * 40, 10, 20, 255)).save(
image_dir / f"image{index}.png"
)
self.settings_path = root / "settings.ini"
settings = QSettings(str(self.settings_path), QSettings.IniFormat)
self.manager = RPGMakerImageManager(self.game_root, settings=settings)
self.manager.resize(1100, 760)
self.manager.show()
self.manager._scan_worker.wait(5000)
self.app.processEvents()
def tearDown(self):
for worker in list(self.manager._thumbnail_workers):
worker.wait(5000)
self.manager.close()
self.app.processEvents()
self.temp.cleanup()
def _click(self, index: int, modifiers=Qt.NoModifier):
item = self.manager.image_list.item(index)
rect = self.manager.image_list.visualItemRect(item)
QTest.mouseClick(
self.manager.image_list.viewport(),
Qt.LeftButton,
modifiers,
rect.center(),
)
self.app.processEvents()
def test_ctrl_shift_and_ctrl_a_use_standard_extended_selection(self):
image_list = self.manager.image_list
self.assertEqual(
image_list.selectionMode(), QAbstractItemView.ExtendedSelection
)
self._click(0)
self._click(2, Qt.ControlModifier)
self.assertEqual(len(image_list.selectedItems()), 2)
self.assertEqual(len(self.manager.selected_ids), 2)
self._click(3, Qt.ShiftModifier)
selected_names = {item.text() for item in image_list.selectedItems()}
self.assertIn("image2.png", selected_names)
self.assertIn("image3.png", selected_names)
image_list.clearSelection()
image_list.setFocus()
QTest.keyClick(image_list, Qt.Key_A, Qt.ControlModifier)
self.app.processEvents()
self.assertEqual(len(image_list.selectedItems()), 4)
self.assertEqual(len(self.manager.selected_ids), 4)
def test_plain_click_replaces_selection_and_ctrl_click_adds(self):
self._click(0)
self._click(1)
self.assertEqual(len(self.manager.image_list.selectedItems()), 1)
self.assertEqual(len(self.manager.selected_ids), 1)
self._click(2, Qt.ControlModifier)
self.assertEqual(len(self.manager.image_list.selectedItems()), 2)
self.assertEqual(len(self.manager.selected_ids), 2)
def test_workspace_images_are_selected_after_manager_reopens(self):
self._click(0)
self._click(2, Qt.ControlModifier)
expected = set(self.manager.selected_ids)
for asset_id in expected:
asset = self.manager.assets_by_id[asset_id]
asset.plain_path.parent.mkdir(parents=True, exist_ok=True)
asset.plain_path.write_bytes(asset.runtime_plain_path.read_bytes())
manifest = self.game_root / ".dazedtl" / "image_selection.json"
manifest.write_text("not used", encoding="utf-8")
settings = QSettings(str(self.settings_path), QSettings.IniFormat)
reopened = RPGMakerImageManager(self.game_root, settings=settings)
reopened.resize(1100, 760)
reopened.show()
reopened._scan_worker.wait(5000)
self.app.processEvents()
try:
self.assertEqual(reopened.selected_ids, set())
self.assertEqual(reopened.image_list.selectedItems(), [])
self.assertEqual(manifest.read_text(encoding="utf-8"), "not used")
reopened.folder_combo.setCurrentIndex(
reopened.folder_combo.findData("img/pictures")
)
reopened.state_combo.setCurrentIndex(
reopened.state_combo.findData("editable")
)
self.app.processEvents()
self.assertEqual(reopened.image_list.count(), len(expected))
self.assertEqual(reopened.image_list.selectedItems(), [])
finally:
for worker in list(reopened._thumbnail_workers):
worker.wait(5000)
reopened.close()
self.app.processEvents()
def test_highlights_survive_filters_without_refreshing_editable_view(self):
self._click(1)
self._click(3, Qt.ControlModifier)
selected_before = set(self.manager.selected_ids)
self.assertEqual(len(selected_before), 2)
self.manager.search_edit.setText("image0")
self.app.processEvents()
self.manager.search_edit.clear()
self.app.processEvents()
self.assertEqual(self.manager.selected_ids, selected_before)
selected_ids = {
item.data(Qt.UserRole + 1)
for item in self.manager.image_list.selectedItems()
}
self.assertEqual(selected_ids, selected_before)
for asset_id in selected_before:
asset = self.manager.assets_by_id[asset_id]
asset.plain_path.parent.mkdir(parents=True, exist_ok=True)
asset.plain_path.write_bytes(asset.runtime_plain_path.read_bytes())
editable_filter = self.manager.state_combo.findData("editable")
self.manager.state_combo.setCurrentIndex(editable_filter)
QTest.qWait(20)
self.app.processEvents()
self.assertEqual(self.manager.image_list.count(), 2)
self.assertEqual(self.manager.selected_ids, selected_before)
removed_id = self.manager.image_list.item(0).data(Qt.UserRole + 1)
self._click(0, Qt.ControlModifier)
self.app.processEvents()
self.assertNotIn(removed_id, self.manager.selected_ids)
self.assertEqual(self.manager.image_list.count(), 2)
def test_delete_key_removes_only_highlighted_workspace_copy(self):
asset = self.manager.assets[0]
asset.plain_path.parent.mkdir(parents=True, exist_ok=True)
edited = asset.runtime_plain_path.read_bytes()
asset.plain_path.write_bytes(edited)
self.manager.state_combo.setCurrentIndex(
self.manager.state_combo.findData("editable")
)
self.app.processEvents()
self.assertEqual(self.manager.image_list.count(), 1)
self._click(0)
with (
patch.object(QMessageBox, "question", return_value=QMessageBox.Yes),
patch.object(QMessageBox, "information"),
):
QTest.keyClick(self.manager.image_list, Qt.Key_Delete)
action_worker = self.manager._action_worker
self.assertIsNotNone(action_worker)
action_worker.wait(5000)
self.app.processEvents()
self.manager._scan_worker.wait(5000)
self.app.processEvents()
self.assertFalse(asset.plain_path.exists())
self.assertTrue(asset.runtime_plain_path.exists())
self.assertEqual(self.manager.image_list.count(), 0)
self.assertEqual(self.manager.remove_button.text(), "Remove highlighted")
def test_prepare_uses_only_highlighted_editable_images(self):
highlighted = self.manager.assets[0]
not_highlighted = self.manager.assets[1]
for asset in (highlighted, not_highlighted):
asset.plain_path.parent.mkdir(parents=True, exist_ok=True)
asset.plain_path.write_bytes(asset.runtime_plain_path.read_bytes())
self._click(0)
with (
patch.object(QMessageBox, "question", return_value=QMessageBox.Yes),
patch.object(self.manager, "_start_action") as start_action,
):
self.manager._prepare_checked()
action, assets = start_action.call_args.args
self.assertEqual(action, "prepare")
self.assertEqual(assets, [highlighted])
self.assertTrue(highlighted.plain_path.exists())
self.assertTrue(not_highlighted.plain_path.exists())
self.assertEqual(
self.manager.prepare_button.text(), "Encrypt highlighted + patch"
)
def test_prepare_uses_all_editable_images_without_highlights(self):
editable = self.manager.assets[:2]
for asset in editable:
asset.plain_path.parent.mkdir(parents=True, exist_ok=True)
asset.plain_path.write_bytes(asset.runtime_plain_path.read_bytes())
with (
patch.object(QMessageBox, "question", return_value=QMessageBox.Yes),
patch.object(self.manager, "_start_action") as start_action,
):
self.manager._prepare_checked()
action, assets = start_action.call_args.args
self.assertEqual(action, "prepare")
self.assertEqual(assets, editable)
self.assertEqual(self.manager.prepare_button.text(), "Encrypt all + patch")
def test_bottom_controls_share_one_row_and_action_width(self):
action_buttons = (
self.manager.open_workspace_button,
self.manager.decrypt_selected_button,
self.manager.decrypt_all_button,
self.manager.remove_button,
self.manager.prepare_button,
)
widths = [button.width() for button in action_buttons]
tops = [button.geometry().top() for button in action_buttons]
self.assertLessEqual(max(widths) - min(widths), 1)
self.assertEqual(len(set(tops)), 1)
self.assertEqual(
self.manager.previous_button.geometry().top(),
self.manager.open_workspace_button.geometry().top(),
)
def test_open_folder_uses_highlighted_images_editable_parent(self):
self._click(0)
asset_id = self.manager.image_list.currentItem().data(Qt.UserRole + 1)
expected = self.manager.assets_by_id[asset_id].plain_path.parent
with patch(
"gui.rpgmaker_image_manager.QDesktopServices.openUrl",
return_value=True,
) as open_url:
self.manager._open_editable_folder()
opened_url = open_url.call_args.args[0]
self.assertEqual(Path(opened_url.toLocalFile()), expected)
self.assertTrue(expected.is_dir())
def test_thumbnail_batch_keeps_one_stable_tile_per_asset(self):
for worker in list(self.manager._thumbnail_workers):
worker.wait(5000)
self.app.processEvents()
image_list = self.manager.image_list
self.assertTrue(image_list.uniformItemSizes())
self.assertNotIn("border-bottom", image_list.styleSheet())
self.assertEqual(image_list.count(), 4)
self.assertTrue(
all(not image_list.item(index).icon().isNull() for index in range(4))
)
rects = [image_list.visualItemRect(image_list.item(index)) for index in range(4)]
self.assertEqual(len({(rect.x(), rect.y()) for rect in rects}), 4)
class RPGMakerImageManagerNavigationTests(unittest.TestCase):
def test_page_size_supports_large_image_sets(self):
self.assertEqual(_PAGE_SIZE, 1000)
def test_image_manager_is_a_dedicated_sidebar_page(self):
root = Path(__file__).resolve().parents[1]
main_source = (root / "gui" / "main.py").read_text(encoding="utf-8")
workflow_source = (root / "gui" / "workflow_tab.py").read_text(encoding="utf-8")
self.assertIn("PAGE_IMAGES = 2", main_source)
self.assertIn("self.image_manager_tab = RPGMakerImageManager", main_source)
self.assertIn('create_nav_button("🖼", "Images")', main_source)
self.assertNotIn("Open Image Manager", workflow_source)
if __name__ == "__main__":
unittest.main()