- SVG node items with snap-to-grid dragging; ports appear on focus/hover and highlight compatible targets during a connection drag - Orthogonally routed edge items with line styles, head/tail decorations (arrow, circle, diamond, bar), selection halo and title labels; later edges jump crossings with arcs when enabled - Grid background view with cursor-anchored zoom, middle-button pan, rubber-band selection and palette drag-and-drop - Undoable commands: add/move/delete/connect/property/paste with clipboard fragments; light/dark/high-contrast themes - Flow overlay: animated dashes, direction and density follow flow rate Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
138 lines
5.0 KiB
C++
138 lines
5.0 KiB
C++
#include "TestFixtures.h"
|
|
#include "core/NetworkModel.h"
|
|
#include "scene/Commands.h"
|
|
|
|
#include <QTest>
|
|
#include <QUndoStack>
|
|
|
|
using namespace diag;
|
|
|
|
class TestUndo : public QObject {
|
|
Q_OBJECT
|
|
private slots:
|
|
void init()
|
|
{
|
|
fixtures::fillRegistry(m_reg);
|
|
delete m_model;
|
|
m_model = new NetworkModel(&m_reg, this);
|
|
m_undo.clear();
|
|
}
|
|
|
|
void addNode_undoRedo()
|
|
{
|
|
auto* cmd = new AddNodeCommand(m_model, QStringLiteral("pump"), {100, 100});
|
|
m_undo.push(cmd);
|
|
const QUuid id = cmd->nodeId();
|
|
QVERIFY(m_model->node(id));
|
|
m_undo.undo();
|
|
QVERIFY(!m_model->node(id));
|
|
m_undo.redo();
|
|
QVERIFY(m_model->node(id)); // same id restored
|
|
}
|
|
|
|
void addEdge_undoRedo()
|
|
{
|
|
const QUuid src = m_model->addNode(QStringLiteral("source"), {0, 0});
|
|
const QUuid pump = m_model->addNode(QStringLiteral("pump"), {200, 0});
|
|
auto* cmd = new AddEdgeCommand(m_model, src, QStringLiteral("out"), pump,
|
|
QStringLiteral("in"));
|
|
m_undo.push(cmd);
|
|
QVERIFY(m_model->edge(cmd->edgeId()));
|
|
m_undo.undo();
|
|
QCOMPARE(m_model->edgeIds().size(), 0);
|
|
m_undo.redo();
|
|
QCOMPARE(m_model->edgeIds().size(), 1);
|
|
}
|
|
|
|
void moveNodes_undoRestoresPositions()
|
|
{
|
|
const QUuid a = m_model->addNode(QStringLiteral("pump"), {0, 0});
|
|
const QUuid b = m_model->addNode(QStringLiteral("pump"), {100, 0});
|
|
m_undo.push(new MoveNodesCommand(
|
|
m_model, {{a, {0, 0}, {40, 40}}, {b, {100, 0}, {140, 40}}}));
|
|
QCOMPARE(m_model->node(a)->pos, QPointF(40, 40));
|
|
m_undo.undo();
|
|
QCOMPARE(m_model->node(a)->pos, QPointF(0, 0));
|
|
QCOMPARE(m_model->node(b)->pos, QPointF(100, 0));
|
|
}
|
|
|
|
void removeItems_undoRestoresEdgesAndProps()
|
|
{
|
|
const QUuid src = m_model->addNode(QStringLiteral("source"), {0, 0});
|
|
const QUuid pump = m_model->addNode(QStringLiteral("pump"), {200, 0});
|
|
const QUuid c = m_model->addNode(QStringLiteral("consumer"), {400, 0});
|
|
const QUuid e1 = m_model->addEdge(src, "out", pump, "in");
|
|
const QUuid e2 = m_model->addEdge(pump, "out", c, "in");
|
|
m_model->setEdgeProperty(e1, "diameter", 150.0);
|
|
m_model->setNodeProperty(pump, "title", QStringLiteral("P-1"));
|
|
|
|
// Deleting the pump takes both attached edges with it.
|
|
m_undo.push(new RemoveItemsCommand(m_model, {pump}, {}));
|
|
QCOMPARE(m_model->nodeIds().size(), 2);
|
|
QCOMPARE(m_model->edgeIds().size(), 0);
|
|
|
|
m_undo.undo();
|
|
QCOMPARE(m_model->nodeIds().size(), 3);
|
|
QCOMPARE(m_model->edgeIds().size(), 2);
|
|
QVERIFY(m_model->edge(e2));
|
|
QCOMPARE(m_model->edgeProperty(e1, "diameter").toDouble(), 150.0);
|
|
QCOMPARE(m_model->nodeProperty(pump, "title").toString(), QStringLiteral("P-1"));
|
|
}
|
|
|
|
void setProperty_undoRedo()
|
|
{
|
|
const QUuid src = m_model->addNode(QStringLiteral("source"), {0, 0});
|
|
m_undo.push(new SetPropertyCommand(m_model, SetPropertyCommand::NodeTarget, src,
|
|
QStringLiteral("supply"), 25.0));
|
|
QCOMPARE(m_model->nodeProperty(src, "supply").toDouble(), 25.0);
|
|
m_undo.undo();
|
|
QCOMPARE(m_model->nodeProperty(src, "supply").toDouble(), 10.0); // spec default
|
|
}
|
|
|
|
void paste_remapsIdsAndOffsets()
|
|
{
|
|
const QUuid src = m_model->addNode(QStringLiteral("source"), {0, 0});
|
|
const QUuid pump = m_model->addNode(QStringLiteral("pump"), {200, 0});
|
|
m_model->addEdge(src, "out", pump, "in");
|
|
|
|
const QJsonObject frag = fragmentFromSelection(*m_model, {src, pump}, {});
|
|
auto* cmd = new PasteCommand(m_model, frag, {20, 20});
|
|
m_undo.push(cmd);
|
|
|
|
QCOMPARE(m_model->nodeIds().size(), 4);
|
|
QCOMPARE(m_model->edgeIds().size(), 2); // inner edge duplicated
|
|
const QList<QUuid> pasted = cmd->pastedNodes();
|
|
QCOMPARE(pasted.size(), 2);
|
|
QVERIFY(!pasted.contains(src));
|
|
QCOMPARE(m_model->node(pasted.first())->pos,
|
|
m_model->node(src)->pos + QPointF(20, 20));
|
|
|
|
m_undo.undo();
|
|
QCOMPARE(m_model->nodeIds().size(), 2);
|
|
QCOMPARE(m_model->edgeIds().size(), 1);
|
|
m_undo.redo();
|
|
QCOMPARE(m_model->nodeIds().size(), 4);
|
|
}
|
|
|
|
void copyFragment_skipsDanglingEdges()
|
|
{
|
|
const QUuid src = m_model->addNode(QStringLiteral("source"), {0, 0});
|
|
const QUuid pump = m_model->addNode(QStringLiteral("pump"), {200, 0});
|
|
m_model->addEdge(src, "out", pump, "in");
|
|
// Only the pump is copied: the edge to the source must not paste.
|
|
const QJsonObject frag = fragmentFromSelection(*m_model, {pump}, {});
|
|
auto* cmd = new PasteCommand(m_model, frag, {0, 40});
|
|
m_undo.push(cmd);
|
|
QCOMPARE(m_model->nodeIds().size(), 3);
|
|
QCOMPARE(m_model->edgeIds().size(), 1);
|
|
}
|
|
|
|
private:
|
|
TypeRegistry m_reg;
|
|
NetworkModel* m_model = nullptr;
|
|
QUndoStack m_undo;
|
|
};
|
|
|
|
QTEST_GUILESS_MAIN(TestUndo)
|
|
#include "tst_undo.moc"
|