"""Unit tests for MyPluginCompanion.
This file is copied verbatim (with ``MyPluginCompanion`` replaced by your
class name) when you create a new plugin with *New plugin from template…*.
Quick-start
-----------
Run from the project root::
pytest <your_plugin_dir>/tests/ -v
Mocking strategy
----------------
The companion needs a ``WolfMapViewer`` at construction time, but tests must
work **without a display** (CI / headless servers).
Two fixtures are provided:
``viewer``
A :class:`~unittest.mock.MagicMock` that satisfies every attribute and
method call. We explicitly set ``xmin`` / ``xmax`` so that helper
methods that compute sizes relative to the viewport return sensible
values instead of zero.
``gl_mock``
A fake ``OpenGL.GL`` module injected into ``sys.modules`` via
``monkeypatch``. Without it, any call to ``_paint()`` would fail with an
``ImportError``. The fixture exposes the mock so you can assert on
specific OpenGL calls::
c._paint(viewer)
gl_mock.glVertex2f.assert_called()
Key codes
---------
Import ``KeyboardSnapshot`` and pass integer key codes. Common values::
_WXK_ESCAPE = 27
_WXK_RETURN = 13
"""
from __future__ import annotations
import sys
import types
from unittest.mock import MagicMock
import pytest
from wolfhece._viewer_plugin_handlers import KeyboardSnapshot, MouseContext
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture
[docs]
def viewer():
"""Minimal WolfMapViewer stand-in (no wx, no display required)."""
v = MagicMock()
v.xmin = 0.0
v.xmax = 1000.0
return v
@pytest.fixture
[docs]
def gl_mock(monkeypatch):
"""Inject a headless OpenGL.GL module so paint methods do not crash."""
gl = types.ModuleType('OpenGL.GL')
gl.glBegin = MagicMock()
gl.glEnd = MagicMock()
gl.glVertex2f = MagicMock()
gl.glColor4f = MagicMock()
gl.glLineWidth = MagicMock()
gl.glPointSize = MagicMock()
gl.GL_LINES = 1
gl.GL_LINE_STRIP = 3
gl.GL_LINE_LOOP = 2
gl.GL_POINTS = 0
opengl_pkg = types.ModuleType('OpenGL')
opengl_pkg.GL = gl
monkeypatch.setitem(sys.modules, 'OpenGL', opengl_pkg)
monkeypatch.setitem(sys.modules, 'OpenGL.GL', gl)
return gl
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
[docs]
def _ctx(x: float = 0.0, y: float = 0.0) -> MouseContext:
"""Build a right-click context at world coordinates (x, y)."""
return MouseContext(x=x, y=y, x_snap=x, y_snap=y, x_pixel=0, y_pixel=0)
[docs]
def _kb(key_code: int, ctrl: bool = False) -> KeyboardSnapshot:
"""Build a keyboard event."""
return KeyboardSnapshot(key_code=key_code, ctrl=ctrl)
# ---------------------------------------------------------------------------
# Import the class under test via importlib to avoid sys.path collisions when
# multiple plugins run in the same pytest session.
# ---------------------------------------------------------------------------
import importlib.util as _ilu
from pathlib import Path as _Path
[docs]
_COMPANION_FILE = _Path(__file__).parent.parent / 'companion.py'
[docs]
_spec = _ilu.spec_from_file_location('_template_companion', _COMPANION_FILE)
[docs]
_mod = _ilu.module_from_spec(_spec)
_spec.loader.exec_module(_mod) # type: ignore[union-attr]
[docs]
MyPluginCompanion = _mod.MyPluginCompanion
# ===========================================================================
# Tests — add your own below
# ===========================================================================
[docs]
class TestMyPluginCompanion:
"""Behaviour tests — pure Python, no wx, no display."""
# -- construction -------------------------------------------------------
[docs]
def test_instantiation(self, viewer):
"""The companion can be created without errors."""
c = MyPluginCompanion(viewer)
assert c is not None
# -- start --------------------------------------------------------------
[docs]
def test_start_registers_action(self, viewer):
c = MyPluginCompanion(viewer)
# Register the action directly (menu_build requires a wx.App)
c._register_action(c._action_id('run'), rdown=c._rdown)
assert len(c._registered_action_ids) > 0
[docs]
def test_start_calls_viewer(self, viewer):
c = MyPluginCompanion(viewer)
c.start()
viewer.start_action.assert_called()
# -- keyboard -----------------------------------------------------------
[docs]
def test_esc_stops_action(self, viewer):
c = MyPluginCompanion(viewer)
c.start()
consumed = c._key(viewer, _kb(_WXK_ESCAPE))
assert consumed is True
# stop() → _end_action() → viewer.end_action()
viewer.end_action.assert_called()
[docs]
def test_unknown_key_returns_false(self, viewer):
c = MyPluginCompanion(viewer)
c.start()
consumed = c._key(viewer, _kb(ord('X')))
assert consumed is False
# -- paint --------------------------------------------------------------
[docs]
def test_paint_does_not_crash(self, viewer, gl_mock):
c = MyPluginCompanion(viewer)
c.start()
c._paint(viewer) # must not raise
# -- TODO: add tests for your own logic below ---------------------------
# Example:
#
# def test_rdown_stores_point(self, viewer):
# c = MyPluginCompanion(viewer)
# c.start()
# c._rdown(viewer, _ctx(10.0, 20.0))
# assert ... # check your state here