From 4f6fe37b4dfa99859af49896449e8673487aedc1 Mon Sep 17 00:00:00 2001 From: Ilya Ashikhmin Date: Thu, 2 Jul 2026 23:37:18 +0200 Subject: [PATCH] test(app): add end-to-end scenarios against the real main window Bundled registry/sample loading, edge selection populating the grouped property panel, undoable title edit, solver + animation clock, and a headless render smoke check. Co-Authored-By: Claude Fable 5 --- tests/CMakeLists.txt | 6 ++ tests/tst_app.cpp | 138 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 tests/tst_app.cpp diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c719f4e..c763026 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -13,3 +13,9 @@ diag_add_test(tst_solver diagcore) diag_add_test(tst_json diagcore) diag_add_test(tst_undo diagscene) diag_add_test(tst_scene diagscene) + +qt_add_resources(TEST_RESOURCES ${CMAKE_SOURCE_DIR}/resources/resources.qrc) +add_executable(tst_app tst_app.cpp ${TEST_RESOURCES}) +target_link_libraries(tst_app PRIVATE diagapp Qt6::Test) +add_test(NAME tst_app COMMAND tst_app) +set_tests_properties(tst_app PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen") diff --git a/tests/tst_app.cpp b/tests/tst_app.cpp new file mode 100644 index 0000000..296e402 --- /dev/null +++ b/tests/tst_app.cpp @@ -0,0 +1,138 @@ +// End-to-end scenarios against the real MainWindow with bundled resources +// (registry config, SVGs, sample document), offscreen. + +#include "app/MainWindow.h" +#include "app/PropertyPanel.h" +#include "core/FlowSolver.h" +#include "core/NetworkModel.h" +#include "scene/DiagramScene.h" +#include "scene/EdgeItem.h" +#include "scene/NodeItem.h" + +#include +#include +#include +#include +#include +#include + +using namespace diag; + +class TestApp : public QObject { + Q_OBJECT +private slots: + void initTestCase() + { + Q_INIT_RESOURCE(resources); + } + + void registryLoads_fromBundledConfig() + { + MainWindow window; + const TypeRegistry* reg = window.model()->registry(); + QVERIFY(reg->nodeTypes().size() >= 9); + QVERIFY(reg->relation(QStringLiteral("water"))); + QVERIFY(reg->relation(QStringLiteral("power"))); + QVERIFY(reg->catalogue(QStringLiteral("pipe_series"))); + QCOMPARE(reg->nodeGroups(), + (QStringList{QStringLiteral("Water supply"), QStringLiteral("Power")})); + } + + void sampleLoads_andRoutes() + { + MainWindow window; + window.show(); + window.loadSample(); + QVERIFY(window.model()->nodeIds().size() >= 8); + QVERIFY(window.model()->edgeIds().size() >= 7); + // Every edge item has an orthogonal polyline attached to its ports. + for (QUuid id : window.model()->edgeIds()) { + EdgeItem* item = window.scene()->edgeItem(id); + QVERIFY(item); + QVERIFY(item->polyline().size() >= 2); + } + } + + void selectingEdge_populatesGroupedPropertyPanel() + { + MainWindow window; + window.show(); + window.loadSample(); + const QUuid edgeId = window.model()->edgeIds().first(); + window.scene()->clearSelection(); + window.scene()->edgeItem(edgeId)->setSelected(true); + + auto* panel = window.findChild(); + QVERIFY(panel); + const auto groups = panel->findChildren(); + QStringList titles; + for (auto* g : groups) + titles << g->title(); + QVERIFY(titles.contains(QStringLiteral("General"))); + QVERIFY(titles.contains(QStringLiteral("Presentation"))); + QVERIFY(titles.contains(QStringLiteral("Physics"))); + // Style enums and physics editors exist. + QVERIFY(panel->findChildren().size() >= 3); + QVERIFY(panel->findChildren().size() >= 2); + } + + void editingTitle_isUndoable() + { + MainWindow window; + window.show(); + window.loadSample(); + const QUuid nodeId = window.model()->nodeIds().first(); + window.scene()->clearSelection(); + window.scene()->nodeItem(nodeId)->setSelected(true); + + auto* panel = window.findChild(); + auto* titleEdit = panel->findChild(); + QVERIFY(titleEdit); + titleEdit->setText(QStringLiteral("Renamed")); + emit titleEdit->editingFinished(); + QCOMPARE(window.model()->nodeProperty(nodeId, "title").toString(), + QStringLiteral("Renamed")); + window.scene()->undoStack()->undo(); + QCOMPARE(window.model()->nodeProperty(nodeId, "title").toString(), + QStringLiteral("Intake")); + } + + void simulation_assignsFlowsAndAnimates() + { + MainWindow window; + window.show(); + window.loadSample(); + window.scene()->setFlowAnimationEnabled(true); + + // Solve both networks like the Simulation menu action does. + for (const Relation& rel : window.model()->registry()->relations()) + FlowSolver::apply(*window.model(), + FlowSolver::solve(*window.model(), rel.id)); + + // Water main from the intake carries the full balanced demand. + double maxWater = 0; + for (QUuid id : window.model()->edgeIds()) + if (window.model()->edge(id)->relation == QLatin1String("water")) + maxWater = qMax(maxWater, qAbs(window.model()->edge(id)->flowRate)); + QVERIFY(maxWater > 1.0); + QVERIFY(window.scene()->maxAbsFlow() >= maxWater); + + const double phase0 = window.scene()->animationPhase(); + QTRY_VERIFY_WITH_TIMEOUT(window.scene()->animationPhase() > phase0, 1000); + window.scene()->setFlowAnimationEnabled(false); + } + + void screenshot_rendersWithoutCrash() + { + MainWindow window; + window.resize(1000, 700); + window.show(); + window.loadSample(); + const QPixmap shot = window.grab(); + QVERIFY(!shot.isNull()); + QVERIFY(shot.width() > 0); + } +}; + +QTEST_MAIN(TestApp) +#include "tst_app.moc"