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 <noreply@anthropic.com>
This commit is contained in:
Beat Hangartner
2026-10-09 14:20:04 +02:00
parent 3dd977a040
commit eab557d9af
3 changed files with 280 additions and 0 deletions
+16
View File
@@ -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(
+112
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#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 <QAction>
#include <QApplication>
#include <QCoreApplication>
#include <QSettings>
#include <QTemporaryDir>
#include <QtTest>
namespace QetAppTest {
/// The editor window QETApp opened at start.
inline QETDiagramEditor *editor()
{
const QList<QETDiagramEditor *> 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<DiagramView *>())
e->newProject();
return e->findChild<DiagramView *>();
}
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<QAction *>())
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 <typename Test>
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<QString>{
"--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<Test>(argc, argv); }
#endif
+152
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include "qetapptest.h"
#include "qetgraphicsitem/diagramimageitem.h"
#include <QDomDocument>
/**
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<DiagramImageItem> 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<DiagramImageItem> 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"