Source code for test_companion

"""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`` attached before interactive calls,
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()
        gl_mock.glVertex2f.assert_called()

Key codes
---------
Use ``Keys.ESCAPE``, ``Keys.RETURN``, etc. from ``wolfhece.plugins.abc``
instead of raw integer key codes::

    from wolfhece.plugins.abc import Keys
    kb = KeyboardSnapshot(key_code=Keys.ESCAPE)
"""
from __future__ import annotations

import sys
import types
from unittest.mock import MagicMock

import pytest

from wolfhece.plugins.abc import Keys
from wolfhece._viewer_plugin_handlers import KeyboardSnapshot, MouseContext

# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
# (use Keys.ESCAPE, Keys.RETURN, etc. — no raw integers needed)


# ---------------------------------------------------------------------------
# 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
[docs] StepTransition = _mod.StepTransition
[docs] def _new_companion(viewer): c = MyPluginCompanion() c.proxy.attach(viewer) return c
# =========================================================================== # 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 = _new_companion(viewer) assert c is not None
# -- start --------------------------------------------------------------
[docs] def test_start_registers_action(self, viewer): c = _new_companion(viewer) # Register the action directly (menu_build requires a wx.App) c.proxy.register_action(c.proxy.action_id('run'), rdown=c._rdown) assert len(c.proxy._registered_action_ids) > 0
[docs] def test_start_calls_viewer(self, viewer): c = _new_companion(viewer) c.start() viewer.start_action.assert_called()
# -- keyboard -----------------------------------------------------------
[docs] def test_esc_stops_action(self, viewer): c = _new_companion(viewer) c.start() consumed = c._key(_kb(Keys.ESCAPE)) assert consumed == StepTransition.CANCEL
[docs] def test_unknown_key_returns_false(self, viewer): c = _new_companion(viewer) c.start() consumed = c._key(_kb(ord('X'))) assert consumed is False
# -- paint --------------------------------------------------------------
[docs] def test_paint_does_not_crash(self, viewer, gl_mock): c = _new_companion(viewer) c.start() c._paint() # must not raise
[docs] def test_multistep_flow_next_then_finish(self, viewer): c = _new_companion(viewer) c.proxy.register_actions(c.actions_spec()) c.start() _, kwargs = viewer.register_action.call_args rdown_handler = kwargs['rdown_handler'] viewer.start_action.reset_mock() viewer.end_action.reset_mock() rdown_handler(viewer, _ctx(1.0, 2.0)) viewer.start_action.assert_called_once_with( 'myplugincompanion.run', 'Right-click again to finish — Esc to cancel', ) rdown_handler(viewer, _ctx(3.0, 4.0)) viewer.end_action.assert_called_once_with('Action finished')
# -- TODO: add tests for your own logic below --------------------------- # Example: # # def test_rdown_stores_point(self, viewer): # c = _new_companion(viewer) # c.start() # c._rdown(_ctx(10.0, 20.0)) # assert ... # check your state here