From eab557d9af36b3cfc6abd60471eb9fb4a86ed41d Mon Sep 17 00:00:00 2001 From: Beat Hangartner Date: Fri, 9 Oct 2026 14:20:04 +0200 Subject: [PATCH] Test that Space on a picture turns the rotation its file saves The first test that runs a real QETApp, offscreen (qetapptest.h): it links qelectrotech_objects, keeps its settings, configuration and data in a temporary folder, and triggers the editor's own actions. tst_rotateselectionimage covers #1413: Space ("rotate_selection") turns a picture's rotationAngle, not QGraphicsItem's rotation, in place, and a picture read back from its XML has the angle; undo and redo, twice; and a group turn ("rotate_group_selection") moves each picture's pivot a quarter turn around the group's centre. All three fail with the code from before #1413. Co-Authored-By: Claude Opus 5.5 --- tests/qttest/CMakeLists.txt | 16 +++ tests/qttest/qetapptest.h | 112 ++++++++++++++++ tests/qttest/tst_rotateselectionimage.cpp | 152 ++++++++++++++++++++++ 3 files changed, 280 insertions(+) create mode 100644 tests/qttest/qetapptest.h create mode 100644 tests/qttest/tst_rotateselectionimage.cpp diff --git a/tests/qttest/CMakeLists.txt b/tests/qttest/CMakeLists.txt index 8653c86a4..1a5073f3c 100644 --- a/tests/qttest/CMakeLists.txt +++ b/tests/qttest/CMakeLists.txt @@ -235,6 +235,22 @@ set_tests_properties(tst_handlersize PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=off target_include_directories(tst_handlersize PRIVATE ${QET_DIR}/sources) target_link_libraries(tst_handlersize PRIVATE Qt::Test Qt::Widgets) +# Tests that need the application around the class they test -- a folio, +# its view, the editor window and its actions -- link the application's own +# code (the qelectrotech_objects library) and run a real QETApp offscreen, +# with settings and folders of their own (qetapptest.h). +function(qet_add_app_test name) + add_executable(${name} ${ARGN} qetapptest.h) + add_test(NAME ${name} COMMAND ${name}) + set_tests_properties(${name} PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen") + target_include_directories(${name} PRIVATE ${QET_DIR}/sources) + target_link_libraries(${name} PRIVATE qelectrotech_objects Qt::Test) +endfunction() + +# Space on a picture turns its own transform, which the file saves (#1413), +# alone and with a group. +qet_add_app_test(tst_rotateselectionimage tst_rotateselectionimage.cpp) + # TextResizeHandles: the corner handles that change the width of a text box, # on a plain QGraphicsTextItem with the "textWidth" property QET texts have. add_executable( diff --git a/tests/qttest/qetapptest.h b/tests/qttest/qetapptest.h new file mode 100644 index 000000000..6e93f8ea5 --- /dev/null +++ b/tests/qttest/qetapptest.h @@ -0,0 +1,112 @@ +/* + Copyright 2006-2026 The QElectroTech Team + This file is part of QElectroTech. + + QElectroTech is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + QElectroTech is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QElectroTech. If not, see . +*/ +#ifndef QETAPPTEST_H +#define QETAPPTEST_H + +/** + For tests that need the application around the class they test: a + folio, the view showing it, the editor window and its actions. They + link the application's own code (see qet_add_app_test() in + CMakeLists.txt) and run a real QETApp, offscreen, with its settings, + configuration and data in a folder of their own. +*/ + +#include "diagram.h" +#include "diagramview.h" +#include "qetapp.h" +#include "qetarguments.h" +#include "qetdiagrameditor.h" + +#include +#include +#include +#include +#include +#include + +namespace QetAppTest { + +/// The editor window QETApp opened at start. +inline QETDiagramEditor *editor() +{ + const QList editors = QETApp::diagramEditors(); + return editors.isEmpty() ? nullptr : editors.first(); +} + +/// The view of the folio shown in the editor, after a new project of one +/// folio if the editor has none open. +inline DiagramView *view() +{ + QETDiagramEditor *e = editor(); + if (!e) + return nullptr; + if (!e->findChild()) + e->newProject(); + return e->findChild(); +} + +inline Diagram *diagram() +{ + DiagramView *v = view(); + return v ? v->diagram() : nullptr; +} + +/// The editor's action whose data() is @p name, as the menus, the +/// toolbar and the keyboard shortcut trigger it ("rotate_selection" is +/// Space). +inline QAction *action(const QString &name) +{ + QETDiagramEditor *e = editor(); + if (!e) + return nullptr; + for (QAction *a : e->findChildren()) + if (a->data().toString() == name) + return a; + return nullptr; +} + +/// Run @p test inside a QETApp whose settings, configuration and data +/// live in a temporary folder, so that no test reads or changes the +/// user's own. +template +int run(int argc, char **argv) +{ + QApplication app(argc, argv); + QCoreApplication::setOrganizationName("QElectroTech"); + QCoreApplication::setApplicationName("QElectroTech"); + QTemporaryDir home; + if (!home.isValid()) + return 1; + QSettings::setDefaultFormat(QSettings::IniFormat); + QSettings::setPath(QSettings::IniFormat, QSettings::UserScope, home.filePath("settings")); + QSettings::setPath(QSettings::IniFormat, QSettings::SystemScope, home.filePath("settings")); + QETApp::applyDirectoryArguments(QETArguments(QList{ + "--config-dir=" + home.filePath("config"), + "--data-dir=" + home.filePath("data")})); + QETApp qetapp; + QCoreApplication::processEvents(); + Test test; + return QTest::qExec(&test, argc, argv); +} + +} // namespace QetAppTest + +#define QET_APP_TEST_MAIN(Test) \ + int main(int argc, char **argv) { return QetAppTest::run(argc, argv); } + +#endif diff --git a/tests/qttest/tst_rotateselectionimage.cpp b/tests/qttest/tst_rotateselectionimage.cpp new file mode 100644 index 000000000..1b23a0f60 --- /dev/null +++ b/tests/qttest/tst_rotateselectionimage.cpp @@ -0,0 +1,152 @@ +/* + Copyright 2006-2026 The QElectroTech Team + This file is part of QElectroTech. + + QElectroTech is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + QElectroTech is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QElectroTech. If not, see . +*/ +#include "qetapptest.h" +#include "qetgraphicsitem/diagramimageitem.h" + +#include + +/** + Space -- the editor's "rotate_selection" action -- on a picture turns + the picture's own transform (rotationAngle, around its pivot), the + rotation its file saves. Before #1413 it turned QGraphicsItem's plain + rotation, which toXml() does not save, so the picture came back + unrotated once the project was reopened. +*/ +class tst_rotateselectionimage : public QObject +{ + Q_OBJECT + + Diagram *m_diagram = nullptr; + + DiagramImageItem *addPicture(const QPointF &pos) + { + QPixmap pixmap(100, 60); + pixmap.fill(Qt::darkGreen); + auto *picture = new DiagramImageItem(pixmap); + m_diagram->addItem(picture); + picture->setPos(pos); + return picture; + } + + static QPointF pivotInScene(DiagramImageItem *picture) + { + return picture->mapToScene(picture->pivot()); + } + + /// The angle and position a file saves, read back by a new picture. + static DiagramImageItem *reloaded(DiagramImageItem *picture) + { + QDomDocument document; + auto *copy = new DiagramImageItem(); + copy->fromXml(picture->toXml(document)); + return copy; + } + + static void trigger(const char *name) + { + QAction *a = QetAppTest::action(QString::fromLatin1(name)); + QVERIFY2(a, name); + a->trigger(); + } + +private slots: + void initTestCase() + { + QVERIFY(QetAppTest::editor()); + m_diagram = QetAppTest::diagram(); + QVERIFY(m_diagram); + } + + void cleanup() + { + m_diagram->clearSelection(); + for (QGraphicsItem *item : m_diagram->items()) + if (item->type() == DiagramImageItem::Type) + delete item; + m_diagram->undoStack().clear(); + } + + void spaceTurnsThePictureAndTheFileKeepsIt() + { + DiagramImageItem *picture = addPicture(QPointF(200, 200)); + const QPointF pivot = pivotInScene(picture); + picture->setSelected(true); + + trigger("rotate_selection"); + trigger("rotate_selection"); + + QCOMPARE(picture->rotationAngle(), 180.0); + QCOMPARE(picture->rotation(), 0.0); // not QGraphicsItem's own rotation + QCOMPARE(pivotInScene(picture), pivot); // turned in place + + std::unique_ptr copy(reloaded(picture)); + QCOMPARE(copy->rotationAngle(), 180.0); + QCOMPARE(copy->pos(), picture->pos()); + } + + void undoAndRedo() + { + DiagramImageItem *picture = addPicture(QPointF(200, 200)); + const QPointF pos = picture->pos(); + picture->setSelected(true); + trigger("rotate_selection"); + + // RotateSelectionCommand animates undo and redo after the first + // time: the values arrive a moment later. + for (int round = 0; round < 2; ++round) + { + m_diagram->undoStack().undo(); + QTRY_COMPARE(picture->rotationAngle(), 0.0); + QCOMPARE(picture->pos(), pos); + m_diagram->undoStack().redo(); + QTRY_COMPARE(picture->rotationAngle(), 90.0); + QCOMPARE(picture->pos(), pos); + } + } + + // Turned as a group, each picture turns on itself and its pivot moves + // a quarter turn around the group's centre (the centre of the + // selection's bounding box, snapped to the grid). + void groupTurnMovesEachPivotAroundTheCentre() + { + DiagramImageItem *a = addPicture(QPointF(100, 100)); + DiagramImageItem *b = addPicture(QPointF(400, 160)); + a->setSelected(true); + b->setSelected(true); + const QPointF centre = Diagram::snapToGrid( + (a->sceneBoundingRect() | b->sceneBoundingRect()).center()); + const QTransform quarter = QTransform().translate(centre.x(), centre.y()) + .rotate(90).translate(-centre.x(), -centre.y()); + const QPointF expectedA = quarter.map(pivotInScene(a)); + const QPointF expectedB = quarter.map(pivotInScene(b)); + + trigger("rotate_group_selection"); + + QCOMPARE(a->rotationAngle(), 90.0); + QCOMPARE(b->rotationAngle(), 90.0); + QCOMPARE(pivotInScene(a), expectedA); + QCOMPARE(pivotInScene(b), expectedB); + + std::unique_ptr copy(reloaded(a)); + QCOMPARE(copy->rotationAngle(), 90.0); + QCOMPARE(pivotInScene(copy.get()) - copy->pos(), pivotInScene(a) - a->pos()); + } +}; + +QET_APP_TEST_MAIN(tst_rotateselectionimage) +#include "tst_rotateselectionimage.moc"