Cover avatar crop input bounds and modal owner lifetime

This commit is contained in:
evgeny committed 2026-10-27 12:07:36 +01:00
1 parent c25ed7c243
commit bfc280310d
2 files changed
+120

No files matched your search

+5
View File
@@ -569,6 +569,11 @@ target_link_libraries(test_member_files_browser PRIVATE ${QT_LIBS} ${QT_TEST_LIB
add_test(NAME test_member_files_browser COMMAND test_member_files_browser)
set_tests_properties(test_member_files_browser PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen" TIMEOUT 30)
add_executable(test_avatar_crop tests/test_avatar_crop.cpp src/avatars/avatarcrop.cpp)
target_link_libraries(test_avatar_crop PRIVATE ${QT_LIBS} ${QT_TEST_LIB} utun pthread ${CMAKE_DL_LIBS})
add_test(NAME test_avatar_crop COMMAND test_avatar_crop)
set_tests_properties(test_avatar_crop PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen" TIMEOUT 20)
add_executable(test_avatars tests/test_avatars.cpp src/avatars/avatarcache.cpp src/avatars/avatarmanager.cpp
src/avatars/avatarcrop.cpp src/avatars/avatargallery.cpp src/imageviewer_window.cpp db/db_manager.cpp)
target_link_libraries(test_avatars PRIVATE ${QT_LIBS} ${QT_TEST_LIB} utun pthread)
+115
View File
@@ -0,0 +1,115 @@
/* Настоящий модальный редактор: ввод, минимальный квадрат и уничтожение владельца. */
#include "../src/avatars/avatarcrop.h"
#include <QtTest>
#include <QDialog>
#include <QPointer>
#include <QPushButton>
#include <QTemporaryDir>
#include <QWheelEvent>
#include <functional>
#include <cmath>
extern "C" {
#include "debug_config.h"
}
static void wheel(QWidget* view, QPoint angle, QPoint pixels = {}) {
const QPoint center = view->rect().center();
QWheelEvent event(center, view->mapToGlobal(center), pixels, angle, Qt::NoButton, Qt::NoModifier, Qt::NoScrollPhase, false);
QApplication::sendEvent(view, &event);
}
static bool edit(const QString& path, QRectF& crop, QWidget* parent,
const std::function<void(QDialog*, QWidget*, QPushButton*)>& action, bool waitForImage = true) {
QTimer poll, deadline; poll.setInterval(5); deadline.setSingleShot(true); bool timedOut = false;
QObject::connect(&poll, &QTimer::timeout, [&] {
auto* dialog = qobject_cast<QDialog*>(QApplication::activeModalWidget()); if (!dialog) return;
auto* view = dialog->findChild<QWidget*>("avatarCropView");
auto* upload = dialog->findChild<QPushButton*>("avatarCropUpload");
if (!view || !upload || (waitForImage && !upload->isEnabled())) return;
poll.stop(); action(dialog, view, upload);
});
QObject::connect(&deadline, &QTimer::timeout, [&] {
timedOut = true;
if (auto* dialog = qobject_cast<QDialog*>(QApplication::activeModalWidget())) dialog->reject();
});
poll.start(); deadline.start(3000);
const bool result = selectAvatarCrop(path, crop, parent);
if (timedOut) QTest::qFail("crop editor did not reach the requested state", __FILE__, __LINE__);
return result;
}
class AvatarCropTest : public QObject {
Q_OBJECT
private slots:
void initTestCase() { debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); }
void invalidAcceptancePreservesOutput() {
const QRectF before(.1, .2, .3, .4); QRectF crop = before;
QVERIFY(!edit(QStringLiteral("/nonexistent/avatar.png"), crop, nullptr,
[](QDialog* dialog, QWidget*, QPushButton*) { dialog->accept(); }, false));
QCOMPARE(crop, before);
}
void zeroAndHorizontalWheel_data() {
QTest::addColumn<QPoint>("angle"); QTest::addColumn<QPoint>("pixels");
QTest::newRow("zero") << QPoint() << QPoint();
QTest::newRow("horizontal-angle") << QPoint(120, 0) << QPoint();
QTest::newRow("horizontal-pixels") << QPoint() << QPoint(30, 0);
}
void zeroAndHorizontalWheel() {
QFETCH(QPoint, angle); QFETCH(QPoint, pixels);
QTemporaryDir directory; QVERIFY(directory.isValid());
const QString path = directory.filePath("source.png"); QImage image(800, 400, QImage::Format_RGB32); image.fill(Qt::blue);
QVERIFY(image.save(path)); QRectF crop;
QVERIFY(edit(path, crop, nullptr, [&](QDialog*, QWidget* view, QPushButton* upload) {
wheel(view, QPoint(0, 120)); wheel(view, angle, pixels); upload->click();
}));
QVERIFY(qAbs(crop.width() - .5 / 1.15) < 1e-8);
QVERIFY(qAbs(crop.height() - 1.0 / 1.15) < 1e-8);
}
void sourceGeometry_data() {
QTest::addColumn<QSize>("size"); QTest::addColumn<int>("zoomSteps");
QTest::newRow("wide") << QSize(800, 400) << 0;
QTest::newRow("portrait") << QSize(400, 800) << 0;
QTest::newRow("large-zoom") << QSize(800, 400) << 32;
QTest::newRow("tiny-zoom") << QSize(20, 2) << 32;
QTest::newRow("thin") << QSize(1, 10000) << 0;
}
void sourceGeometry() {
QFETCH(QSize, size); QFETCH(int, zoomSteps);
QTemporaryDir directory; QVERIFY(directory.isValid());
const QString path = directory.filePath("source.png"); QImage image(size, QImage::Format_RGB32); image.fill(Qt::green);
QVERIFY(image.save(path)); QRectF crop;
QVERIFY(edit(path, crop, nullptr, [&](QDialog*, QWidget* view, QPushButton* upload) {
for (int i = 0; i < zoomSteps; i++) wheel(view, QPoint(0, 120));
upload->click();
}));
QVERIFY(std::isfinite(crop.x()) && std::isfinite(crop.y()) && std::isfinite(crop.width()) && std::isfinite(crop.height()));
QVERIFY(crop.isValid()); QVERIFY(crop.x() >= -1e-8 && crop.y() >= -1e-8);
QVERIFY(crop.right() <= 1 + 1e-8 && crop.bottom() <= 1 + 1e-8);
const double pixelsWide = crop.width() * size.width(), pixelsHigh = crop.height() * size.height();
QVERIFY(qAbs(pixelsWide - pixelsHigh) < 1e-6);
QVERIFY(pixelsWide >= 1 - 1e-8);
if (zoomSteps == 32) QVERIFY(qAbs(pixelsWide - qMin(size.width(), size.height()) / qMin(16.0, double(qMin(size.width(), size.height())))) < 1e-6);
}
void cancelPreservesOutput() {
QTemporaryDir directory; QVERIFY(directory.isValid());
const QString path = directory.filePath("source.png"); QImage image(800, 400, QImage::Format_RGB32); image.fill(Qt::blue);
QVERIFY(image.save(path)); const QRectF before(.1, .2, .3, .4); QRectF crop = before;
QVERIFY(!edit(path, crop, nullptr, [](QDialog* dialog, QWidget*, QPushButton*) { dialog->reject(); }));
QCOMPARE(crop, before);
}
void parentCanDisappearDuringLoad() {
QPointer<QWidget> parent = new QWidget;
const QRectF before(.1, .2, .3, .4); QRectF crop = before;
QVERIFY(!edit(QStringLiteral("/nonexistent/avatar.png"), crop, parent,
[&](QDialog*, QWidget*, QPushButton*) { delete parent.data(); }, false));
QVERIFY(!parent); QCOMPARE(crop, before);
}
};
QTEST_MAIN(AvatarCropTest)
#include "test_avatar_crop.moc"