#include "TestFixtures.h" #include "core/NetworkModel.h" #include "scene/DiagramScene.h" #include "scene/EdgeItem.h" #include "scene/GridView.h" #include "scene/NodeItem.h" #include "scene/PortItem.h" #include #include #include #include using namespace diag; class TestScene : public QObject { Q_OBJECT private slots: void init() { fixtures::fillRegistry(m_reg); delete m_scene; delete m_model; m_model = new NetworkModel(&m_reg, this); m_undo = new QUndoStack(m_model); m_scene = new DiagramScene(m_model, m_undo, this); } void dropNode_createsSnappedItemAndUndoes() { const QUuid id = m_scene->dropNode(QStringLiteral("pump"), {107, 93}); QVERIFY(!id.isNull()); QVERIFY(m_scene->nodeItem(id)); // Center (107,93) minus half size (30,30) = (77,63), snapped to 20-grid. QCOMPARE(m_model->node(id)->pos, QPointF(80, 60)); QCOMPARE(m_scene->nodeItem(id)->pos(), QPointF(80, 60)); m_undo->undo(); QVERIFY(!m_scene->nodeItem(id)); } void nodeDrag_snapsAndIsUndoable() { const QUuid id = m_scene->dropNode(QStringLiteral("pump"), {100, 100}); NodeItem* item = m_scene->nodeItem(id); const QPointF before = item->pos(); // Simulate the drag protocol NodeItem uses. item->setSelected(true); m_scene->captureMoveOrigin(); item->setPos(before + QPointF(37, 42)); // snapped by itemChange m_scene->commitMove(); QCOMPARE(item->pos(), before + QPointF(40, 40)); QCOMPARE(m_model->node(id)->pos, before + QPointF(40, 40)); m_undo->undo(); QCOMPARE(m_model->node(id)->pos, before); QCOMPARE(item->pos(), before); } void ports_visibleOnlyWhenFocused() { const QUuid id = m_scene->dropNode(QStringLiteral("pump"), {100, 100}); NodeItem* item = m_scene->nodeItem(id); QCOMPARE(item->portItems().size(), 2); for (auto* p : item->portItems()) QVERIFY(!p->isVisible()); item->setSelected(true); for (auto* p : item->portItems()) QVERIFY(p->isVisible()); item->setSelected(false); for (auto* p : item->portItems()) QVERIFY(!p->isVisible()); } void interactiveConnect_compatiblePortsHighlighted() { const QUuid src = m_scene->dropNode(QStringLiteral("source"), {0, 0}); const QUuid pump = m_scene->dropNode(QStringLiteral("pump"), {300, 0}); const QUuid gen = m_scene->dropNode(QStringLiteral("generator"), {300, 200}); PortItem* srcOut = m_scene->nodeItem(src)->portItem(QStringLiteral("out")); PortItem* pumpIn = m_scene->nodeItem(pump)->portItem(QStringLiteral("in")); PortItem* pumpOut = m_scene->nodeItem(pump)->portItem(QStringLiteral("out")); PortItem* genOut = m_scene->nodeItem(gen)->portItem(QStringLiteral("out")); m_scene->startConnection(srcOut); QVERIFY(m_scene->isConnecting()); QCOMPARE(m_scene->connectStateFor(pumpIn), PortItem::ConnectState::Compatible); QCOMPARE(m_scene->connectStateFor(pumpOut), PortItem::ConnectState::Incompatible); QCOMPARE(m_scene->connectStateFor(genOut), PortItem::ConnectState::Incompatible); QVERIFY(pumpIn->isVisible()); // compatible ports shown during drag // Finish on the compatible port: edge created through the undo stack. QVERIFY(m_scene->finishConnection(pumpIn->scenePos())); QVERIFY(!m_scene->isConnecting()); QCOMPARE(m_model->edgeIds().size(), 1); m_undo->undo(); QCOMPARE(m_model->edgeIds().size(), 0); } void interactiveConnect_rejectsIncompatible() { const QUuid src = m_scene->dropNode(QStringLiteral("source"), {0, 0}); const QUuid gen = m_scene->dropNode(QStringLiteral("generator"), {300, 0}); PortItem* srcOut = m_scene->nodeItem(src)->portItem(QStringLiteral("out")); PortItem* genOut = m_scene->nodeItem(gen)->portItem(QStringLiteral("out")); QSignalSpy spy(m_scene, &DiagramScene::connectionMessage); m_scene->startConnection(srcOut); QVERIFY(!m_scene->finishConnection(genOut->scenePos())); QCOMPARE(m_model->edgeIds().size(), 0); QVERIFY(spy.count() >= 2); // start message + rejection reason } void edgeRoute_isOrthogonalBetweenPorts() { const QUuid src = m_scene->dropNode(QStringLiteral("source"), {0, 0}); const QUuid pump = m_scene->dropNode(QStringLiteral("pump"), {400, 200}); const QUuid e = m_model->addEdge(src, "out", pump, "in"); EdgeItem* item = m_scene->edgeItem(e); QVERIFY(item); const auto& poly = item->polyline(); QVERIFY(poly.size() >= 2); QCOMPARE(poly.first(), m_model->portScenePos(src, QStringLiteral("out"))); QCOMPARE(poly.last(), m_model->portScenePos(pump, QStringLiteral("in"))); for (int i = 0; i + 1 < poly.size(); ++i) QVERIFY(qAbs(poly[i].x() - poly[i + 1].x()) < 1e-6 || qAbs(poly[i].y() - poly[i + 1].y()) < 1e-6); } void edgesFollowNodeMove() { const QUuid src = m_scene->dropNode(QStringLiteral("source"), {0, 0}); const QUuid pump = m_scene->dropNode(QStringLiteral("pump"), {400, 0}); const QUuid e = m_model->addEdge(src, "out", pump, "in"); m_model->moveNode(pump, {400, 300}); QCOMPARE(m_scene->edgeItem(e)->polyline().last(), m_model->portScenePos(pump, QStringLiteral("in"))); } void copyPasteDelete_roundTrip() { const QUuid src = m_scene->dropNode(QStringLiteral("source"), {0, 0}); const QUuid pump = m_scene->dropNode(QStringLiteral("pump"), {300, 0}); m_model->addEdge(src, "out", pump, "in"); m_scene->nodeItem(src)->setSelected(true); m_scene->nodeItem(pump)->setSelected(true); m_scene->copySelection(); QVERIFY(m_scene->canPaste()); m_scene->paste(); QCOMPARE(m_model->nodeIds().size(), 4); QCOMPARE(m_model->edgeIds().size(), 2); // Pasted nodes become the new selection. QCOMPARE(m_scene->selectedItems().size(), 2); m_scene->selectAll(); m_scene->deleteSelection(); QCOMPARE(m_model->nodeIds().size(), 0); QCOMPARE(m_model->edgeIds().size(), 0); m_undo->undo(); QCOMPARE(m_model->nodeIds().size(), 4); QCOMPARE(m_model->edgeIds().size(), 2); } void crossingEdges_getArcWhenEnabled() { // Build two crossing water lines: (0,0)->(400,0) area and a vertical // crossing through them. const QUuid s1 = m_scene->dropNode(QStringLiteral("source"), {0, 100}); const QUuid c1 = m_scene->dropNode(QStringLiteral("consumer"), {400, 100}); const QUuid j1 = m_scene->dropNode(QStringLiteral("junction"), {200, 0}); const QUuid j2 = m_scene->dropNode(QStringLiteral("junction"), {200, 240}); m_model->addEdge(s1, "out", c1, "in"); const QUuid crossing = m_model->addEdge(j1, "s", j2, "n"); EdgeItem* item = m_scene->edgeItem(crossing); const double plainLen = Router::polylineLength(item->polyline()); // With arcs on (default), the painted path detours around the first edge. RouteConfig cfg = m_scene->routeConfig(); QVERIFY(cfg.arcOnIntersection); const auto hops = Router::crossings(item->polyline(), m_scene->edgeItem(m_model->edgeIds().first())->polyline()); QCOMPARE(hops.size(), 1); cfg.arcOnIntersection = false; m_scene->setRouteConfig(cfg); // still routes, no arcs — no crash, same polyline QCOMPARE(Router::polylineLength(m_scene->edgeItem(crossing)->polyline()), plainLen); } void gridView_dropMimeCreatesNode() { GridView view(m_scene); view.resize(600, 400); view.show(); QMimeData mime; mime.setData(QLatin1String(GridView::kNodeTypeMime), QByteArrayLiteral("pump")); QVERIFY(view.handleNodeTypeDrop(&mime, QPoint(300, 200))); QCOMPARE(m_model->nodeIds().size(), 1); QMimeData other; other.setData(QStringLiteral("text/plain"), QByteArrayLiteral("x")); QVERIFY(!view.handleNodeTypeDrop(&other, QPoint(300, 200))); QCOMPARE(m_model->nodeIds().size(), 1); } private: TypeRegistry m_reg; NetworkModel* m_model = nullptr; QUndoStack* m_undo = nullptr; DiagramScene* m_scene = nullptr; }; QTEST_MAIN(TestScene) #include "tst_scene.moc"