diff --git a/src/requirements.txt b/src/requirements.txt index c0e094c..0c335d9 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -33,3 +33,5 @@ python-multipart orjson argon2-cffi toga-core>=0.5.2 +pillow +toga-dummy>=0.5.2 # for headless GUI tests diff --git a/src/seedpass_gui/__init__.py b/src/seedpass_gui/__init__.py index f473abc..4ef96bc 100644 --- a/src/seedpass_gui/__init__.py +++ b/src/seedpass_gui/__init__.py @@ -1,11 +1,11 @@ """Graphical user interface for SeedPass.""" -from .app import SeedPassApp +from .app import SeedPassApp, build def main() -> None: """Launch the GUI application.""" - SeedPassApp().main_loop() + build().main_loop() __all__ = ["SeedPassApp", "main"] diff --git a/src/seedpass_gui/app.py b/src/seedpass_gui/app.py index b3b84cb..f1578f1 100644 --- a/src/seedpass_gui/app.py +++ b/src/seedpass_gui/app.py @@ -16,10 +16,12 @@ class LockScreenWindow(toga.Window): """Window prompting for the master password.""" def __init__( - self, app: SeedPassApp, vault: VaultService, entries: EntryService + self, controller: SeedPassApp, vault: VaultService, entries: EntryService ) -> None: super().__init__("Unlock Vault") - self.app = app + # Store a reference to the SeedPass application instance separately from + # the ``toga`` ``Window.app`` attribute to avoid conflicts. + self.controller = controller self.vault = vault self.entries = entries @@ -43,8 +45,8 @@ class LockScreenWindow(toga.Window): except Exception as exc: # pragma: no cover - GUI error handling self.message.text = str(exc) return - main = MainWindow(self.app, self.vault, self.entries) - self.app.main_window = main + main = MainWindow(self.controller, self.vault, self.entries) + self.controller.main_window = main main.show() self.close() @@ -53,10 +55,12 @@ class MainWindow(toga.Window): """Main application window showing vault entries.""" def __init__( - self, app: SeedPassApp, vault: VaultService, entries: EntryService + self, controller: SeedPassApp, vault: VaultService, entries: EntryService ) -> None: super().__init__("SeedPass") - self.app = app + # ``Window.app`` is reserved for the Toga ``App`` instance. Store the + # SeedPass application reference separately. + self.controller = controller self.vault = vault self.entries = entries @@ -115,7 +119,7 @@ class EntryDialog(toga.Window): self.username_input = toga.TextInput(style=Pack(flex=1)) self.url_input = toga.TextInput(style=Pack(flex=1)) self.length_input = toga.NumberInput( - min_value=8, max_value=128, style=Pack(width=80), value=16 + min=8, max=128, style=Pack(width=80), value=16 ) save_button = toga.Button( @@ -184,7 +188,8 @@ class SearchDialog(toga.Window): def build() -> SeedPassApp: - return SeedPassApp() + """Return a configured :class:`SeedPassApp` instance.""" + return SeedPassApp(formal_name="SeedPass", app_id="org.seedpass.gui") class SeedPassApp(toga.App): @@ -201,4 +206,4 @@ class SeedPassApp(toga.App): def main() -> None: # pragma: no cover - GUI bootstrap """Run the BeeWare application.""" - SeedPassApp().main_loop() + build().main_loop() diff --git a/src/tests/test_gui_headless.py b/src/tests/test_gui_headless.py new file mode 100644 index 0000000..67b5557 --- /dev/null +++ b/src/tests/test_gui_headless.py @@ -0,0 +1,69 @@ +import os +from types import SimpleNamespace + +import toga + +from seedpass_gui.app import LockScreenWindow, MainWindow, EntryDialog + + +class FakeVault: + def __init__(self): + self.called = False + + def unlock(self, request): + self.called = True + + +class FakeEntries: + def __init__(self): + self.added = [] + self.modified = [] + + def list_entries(self): + return [] + + def search_entries(self, query): + return [] + + def add_entry(self, label, length, username=None, url=None): + self.added.append((label, length, username, url)) + return 1 + + def modify_entry(self, entry_id, username=None, url=None, label=None): + self.modified.append((entry_id, username, url, label)) + + +def setup_module(module): + os.environ["TOGA_BACKEND"] = "toga_dummy" + import asyncio + + asyncio.set_event_loop(asyncio.new_event_loop()) + + +def test_unlock_creates_main_window(): + app = toga.App("Test", "org.example") + controller = SimpleNamespace(main_window=None) + vault = FakeVault() + entries = FakeEntries() + + win = LockScreenWindow(controller, vault, entries) + win.password_input.value = "pw" + win.handle_unlock(None) + + assert vault.called + assert isinstance(controller.main_window, MainWindow) + + +def test_entrydialog_add_calls_service(): + toga.App("Test2", "org.example2") + entries = FakeEntries() + main = SimpleNamespace(entries=entries, refresh_entries=lambda: None) + + dlg = EntryDialog(main, None) + dlg.label_input.value = "L" + dlg.username_input.value = "u" + dlg.url_input.value = "x" + dlg.length_input.value = 12 + dlg.save(None) + + assert entries.added == [("L", 12, "u", "x")]