diagrams-opus-qt/tests/test_scene.py
Ilya af66debcde feat(view): graphics scene, node/port/edge items, undoable controller
Add the QGraphicsView layer: NodeItem (SVG symbol + title + draggable,
snap-to-grid), PortItem (relation-colored anchors with compatibility
hints), EdgeItem (orthogonal routing, arc hops on crossings, line styles,
head/tail decorations, animated flow dashes), and DiagramScene binding the
document to items with edge-drawing interaction and a grid background.
Add EditorController with a QUndoStack (add/connect/delete/move/edit) and
scene interaction tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 23:20:09 +02:00

100 lines
3.3 KiB
Python

"""Scene/interaction tests: node items, port connect, move reroute, delete, undo."""
import pytest
from pipeline_editor.config import AppConfig
from pipeline_editor.controller import EditorController
from pipeline_editor.model.document import DiagramDocument
from pipeline_editor.model.node_library import default_library
from pipeline_editor.view.node_item import NodeItem
from pipeline_editor.view.edge_item import EdgeItem
from pipeline_editor.view.scene import DiagramScene
@pytest.fixture
def env(qapp):
doc = DiagramDocument()
lib = default_library()
ctrl = EditorController(doc, lib)
cfg = AppConfig()
scene = DiagramScene(doc, cfg, controller=ctrl)
return doc, lib, ctrl, cfg, scene
def test_add_node_creates_item(env):
doc, lib, ctrl, cfg, scene = env
node = scene.create_node("pump", 100, 100)
assert node is not None
assert node.node_id in scene.node_items
assert isinstance(scene.node_items[node.node_id], NodeItem)
def test_connect_ports_creates_edge_item(env):
doc, lib, ctrl, cfg, scene = env
a = ctrl.add_node("source", 0, 100)
b = ctrl.add_node("pump", 200, 100)
src_port = scene.node_items[a.node_id].port_item("out")
dst_port = scene.node_items[b.node_id].port_item("in")
scene._begin_edge(src_port)
scene._finish_edge(dst_port)
assert len(scene.edge_items) == 1
edge = next(doc.edges())
assert edge.source_node == a.node_id and edge.target_node == b.node_id
def test_incompatible_ports_do_not_connect(env):
doc, lib, ctrl, cfg, scene = env
a = ctrl.add_node("source", 0, 100) # water
t = ctrl.add_node("transformer", 200, 100) # power
scene._begin_edge(scene.node_items[a.node_id].port_item("out"))
scene._finish_edge(scene.node_items[t.node_id].port_item("hv"))
assert len(scene.edge_items) == 0
def test_move_node_reroutes_edge(env):
doc, lib, ctrl, cfg, scene = env
a = ctrl.add_node("source", 0, 100)
b = ctrl.add_node("pump", 200, 100)
ctrl.connect(a.node_id, "out", b.node_id, "in")
edge_item = next(iter(scene.edge_items.values()))
before = edge_item.path_points()[-1]
# move target node down; edge end should follow
scene.node_items[b.node_id].setPos(200, 300)
after = edge_item.path_points()[-1]
assert after != before
assert after[1] > before[1]
def test_selection_emits_model(env):
doc, lib, ctrl, cfg, scene = env
a = ctrl.add_node("pump", 0, 0)
received = []
scene.selection_changed.connect(lambda m: received.append(m))
scene.node_items[a.node_id].setSelected(True)
assert received and received[-1] is a
def test_delete_and_undo_roundtrip(env):
doc, lib, ctrl, cfg, scene = env
a = ctrl.add_node("source", 0, 100)
b = ctrl.add_node("pump", 200, 100)
ctrl.connect(a.node_id, "out", b.node_id, "in")
assert len(scene.edge_items) == 1
ctrl.delete(node_ids=[a.node_id])
assert a.node_id not in scene.node_items
assert len(scene.edge_items) == 0 # cascaded
ctrl.undo()
assert a.node_id in scene.node_items
assert len(scene.edge_items) == 1 # edge restored
def test_move_undo(env):
doc, lib, ctrl, cfg, scene = env
a = ctrl.add_node("pump", 40, 40)
ctrl.record_move(a.node_id, (40, 40), (120, 200))
assert (a.x, a.y) == (120, 200)
ctrl.undo()
assert (a.x, a.y) == (40, 40)
assert scene.node_items[a.node_id].pos().x() == 40