otko/tests/gui/test_draw_node_tool.py
smillmorel 61b850bf46
Some checks failed
CI / lint (pull_request) Has been cancelled
CI / type (pull_request) Has been cancelled
CI / test-headless (macos-latest, 3.10) (pull_request) Has been cancelled
CI / test-headless (macos-latest, 3.11) (pull_request) Has been cancelled
CI / test-headless (macos-latest, 3.12) (pull_request) Has been cancelled
CI / test-headless (ubuntu-latest, 3.10) (pull_request) Has been cancelled
CI / test-headless (ubuntu-latest, 3.11) (pull_request) Has been cancelled
CI / test-headless (ubuntu-latest, 3.12) (pull_request) Has been cancelled
CI / test-headless (windows-latest, 3.10) (pull_request) Has been cancelled
CI / test-headless (windows-latest, 3.11) (pull_request) Has been cancelled
CI / test-headless (windows-latest, 3.12) (pull_request) Has been cancelled
CI / test-gui (pull_request) Has been cancelled
CI / test-integration (macos-latest) (pull_request) Has been cancelled
CI / test-integration (ubuntu-latest) (pull_request) Has been cancelled
CI / test-integration (windows-latest) (pull_request) Has been cancelled
style: resolve the remaining ruff findings
Auto-fixes (UP037, UP038, F401, I001, SIM105, RUF100) plus hand-fixes:
drop the now-unused `Union` imports, annotate mutable class attributes
with ClassVar, raise ValidationError instead of blind Exception in the
zero-length test, delete two dead locals, and bind the per-iteration
grid-builder closures to their loop variables (B023).
2026-09-16 13:20:36 -04:00

149 lines
5.3 KiB
Python

"""Unit tests for DrawNodeTool.
The tool only uses ``vm.project`` and ``vm.apply_command``; it does
not touch the canvas except through the stub selection interface.
"""
from __future__ import annotations
import pytest
pytest.importorskip("PySide6")
from otko.core import GridSystem, Node
from otko.viewmodels import ProjectViewModel
from otko.views.canvas3d.selection import SelectionState
from otko.views.tools.draw_node import (
DrawNodeTool,
_snap_to_grid,
)
class _CanvasStub:
def __init__(self) -> None:
self.selection = SelectionState()
def view_xy(self) -> None:
# No-op: real canvas switches camera; tool tests don't care.
pass
def set_snap_preview_enabled(self, _enabled: bool) -> None:
# Hover-snap preview is a real-canvas feature; tool tests ignore it.
pass
def _vm_with_grid() -> ProjectViewModel:
vm = ProjectViewModel()
vm.new_project()
vm.project.grid_system = GridSystem( # type: ignore[union-attr]
x_lines=[0.0, 3.0, 6.0],
y_lines=[0.0, 4.0],
z_lines=[0.0],
)
return vm
# ────────────────────────── snap helper ─────────────────────────────
def test_snap_to_grid_picks_nearest_lines() -> None:
x, y, z = _snap_to_grid(
2.2,
3.7,
0.6,
x_lines=[0.0, 3.0],
y_lines=[0.0, 4.0],
z_lines=[0.0, 3.0],
)
assert (x, y, z) == (3.0, 4.0, 0.0)
def test_snap_to_grid_identity_on_empty_axes() -> None:
x, y, z = _snap_to_grid(1.5, 2.5, 3.5, x_lines=[], y_lines=[], z_lines=[])
assert (x, y, z) == (1.5, 2.5, 3.5)
# ────────────────────────── tool behaviour ──────────────────────────
# NB: the canvas now does pixel-space snap BEFORE emitting emptyClicked;
# the tool receives only valid grid-intersection coordinates. These tests
# simulate that by passing exact intersection coords.
@pytest.mark.gui
def test_empty_click_creates_node_at_exact_intersection(qtbot) -> None: # type: ignore[no-untyped-def]
vm = _vm_with_grid()
tool = DrawNodeTool(_CanvasStub(), vm) # type: ignore[arg-type]
tool.activate()
# Canvas guarantees the coords ARE an intersection.
tool.on_empty_clicked(0.0, 0.0, 0.0)
assert len(vm.project.nodes) == 1 # type: ignore[union-attr]
assert vm.project.nodes[0].coords == (0.0, 0.0, 0.0)
tool.on_empty_clicked(3.0, 4.0, 0.0)
assert len(vm.project.nodes) == 2 # type: ignore[union-attr]
assert vm.project.nodes[1].coords == (3.0, 4.0, 0.0) # type: ignore[union-attr]
@pytest.mark.gui
def test_empty_click_duplicate_is_rejected(qtbot) -> None: # type: ignore[no-untyped-def]
"""Clicking exactly on an existing node's coords must not duplicate it."""
vm = _vm_with_grid()
vm.project.nodes.append(Node(id=42, coords=(3.0, 4.0, 0.0))) # type: ignore[union-attr]
tool = DrawNodeTool(_CanvasStub(), vm) # type: ignore[arg-type]
tool.activate()
status_msgs: list[str] = []
tool.statusChanged.connect(status_msgs.append)
tool.on_empty_clicked(3.0, 4.0, 0.0)
assert len(vm.project.nodes) == 1 # type: ignore[union-attr] (unchanged)
assert any("already exists" in m for m in status_msgs)
@pytest.mark.gui
def test_grid_less_canvas_never_emits_to_tool(qtbot) -> None: # type: ignore[no-untyped-def]
"""Documents the new contract: tool simply trusts canvas.
The canvas only emits emptyClicked when it has a valid snap
target. For a grid-less project, the canvas never emits, so the
tool is never invoked. We therefore don't test rejection here.
"""
vm = ProjectViewModel()
vm.new_project()
tool = DrawNodeTool(_CanvasStub(), vm) # type: ignore[arg-type]
tool.activate()
assert vm.project.nodes == [] # type: ignore[union-attr]
@pytest.mark.gui
def test_consecutive_clicks_allocate_unique_ids(qtbot) -> None: # type: ignore[no-untyped-def]
vm = _vm_with_grid()
tool = DrawNodeTool(_CanvasStub(), vm) # type: ignore[arg-type]
tool.activate()
tool.on_empty_clicked(0.0, 0.0, 0.0)
tool.on_empty_clicked(3.0, 0.0, 0.0)
tool.on_empty_clicked(6.0, 0.0, 0.0)
ids = [n.id for n in vm.project.nodes] # type: ignore[union-attr]
assert len(ids) == 3
assert len(set(ids)) == 3 # all unique
@pytest.mark.gui
def test_picking_existing_node_emits_status_only(qtbot) -> None: # type: ignore[no-untyped-def]
vm = _vm_with_grid()
vm.project.nodes.append(Node(id=1, coords=(0, 0, 0))) # type: ignore[union-attr]
tool = DrawNodeTool(_CanvasStub(), vm) # type: ignore[arg-type]
tool.activate()
status_msgs: list[str] = []
tool.statusChanged.connect(status_msgs.append)
tool.on_node_picked(1)
# No new nodes; user was told about the existing one.
assert len(vm.project.nodes) == 1 # type: ignore[union-attr]
assert any("already exists" in m for m in status_msgs)
@pytest.mark.gui
def test_tool_is_active_after_activate(qtbot) -> None: # type: ignore[no-untyped-def]
vm = _vm_with_grid()
tool = DrawNodeTool(_CanvasStub(), vm) # type: ignore[arg-type]
assert tool.is_active is False
tool.activate()
assert tool.is_active is True
tool.deactivate()
assert tool.is_active is False