From a190c45988d5fb2f4d097d95b477bf37f9a91177 Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Tue, 5 Aug 2025 19:17:37 -0400 Subject: [PATCH] Proof-of-concept QML test WIP, do not review yet --- CMakeLists.txt | 2 +- autotests/CMakeLists.txt | 26 +++++++++ autotests/qmltest.cpp | 61 +++++++++++++++++++++ autotests/server.cpp | 16 ++++++ autotests/tst_media.qml | 73 ++++++++++++++++++++++++++ src/messagecontent/ImageComponent.qml | 2 + src/messagecontent/mediasizehelper.cpp | 4 ++ 7 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 autotests/qmltest.cpp create mode 100644 autotests/tst_media.qml diff --git a/CMakeLists.txt b/CMakeLists.txt index adf609310..a5a4c1682 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -55,7 +55,7 @@ ecm_setup_version(${PROJECT_VERSION} VERSION_HEADER ${CMAKE_CURRENT_BINARY_DIR}/neochat-version.h ) -find_package(Qt6 ${QT_MIN_VERSION} NO_MODULE COMPONENTS Core Quick Gui QuickControls2 Multimedia Svg TextToSpeech WebView) +find_package(Qt6 ${QT_MIN_VERSION} NO_MODULE COMPONENTS Core Quick Gui QuickControls2 Multimedia Svg TextToSpeech WebView QuickTest) set_package_properties(Qt6 PROPERTIES TYPE REQUIRED PURPOSE "Basic application components" diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index a5a8a0259..903ed0843 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -9,6 +9,21 @@ target_link_libraries(neochat_server PUBLIC Qt::HttpServer QuotientQt6) add_definitions(-DDATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/data" ) +macro(add_qml_tests) + if (WIN32) + set(_extra_args -platform offscreen) + endif() + + foreach(test ${ARGV}) + add_test(NAME ${test} + COMMAND qmltest + ${_extra_args} + -input ${test} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + endforeach() +endmacro() + ecm_add_test( neochatroomtest.cpp LINK_LIBRARIES neochat Qt::Test @@ -104,3 +119,14 @@ ecm_add_test( LINK_LIBRARIES neochat Qt::Test neochat_server TEST_NAME roommanagertest ) + +add_executable(qmltest qmltest.cpp) +target_link_libraries(qmltest + PRIVATE + Qt6::Qml + Qt6::QuickTest + neochat_server + neochat + neochatplugin) + +add_qml_tests(tst_media.qml) diff --git a/autotests/qmltest.cpp b/autotests/qmltest.cpp new file mode 100644 index 000000000..0e61b6d57 --- /dev/null +++ b/autotests/qmltest.cpp @@ -0,0 +1,61 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "accountmanager.h" +#include "server.h" + +using namespace Quotient; +using namespace Qt::StringLiterals; + +class NetworkAccessManagerFactory : public QQmlNetworkAccessManagerFactory +{ + QNetworkAccessManager *create(QObject *) override + { + auto nam = NetworkAccessManager::instance(); + QObject::connect(nam, &QNetworkAccessManager::sslErrors, nam, [](auto reply, auto errors) { + reply->ignoreSslErrors(errors); + }); + nam->setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy); + + return nam; + } +}; + +class Setup : public QObject +{ + Q_OBJECT + +public: + Setup() = default; + + Server server; + +public Q_SLOTS: + void qmlEngineAvailable(QQmlEngine *engine) + { + KLocalizedString::setApplicationDomain(QByteArrayLiteral("neochat")); + + engine->setNetworkAccessManagerFactory(new NetworkAccessManagerFactory()); + engine->rootContext()->setContextObject(new KLocalizedContext(engine)); + + server.start(); + Q_UNUSED(new AccountManager(true)); + } +}; + +QUICK_TEST_MAIN_WITH_SETUP(NeoChat, Setup) + +#include "qmltest.moc" diff --git a/autotests/server.cpp b/autotests/server.cpp index 4ac650c58..2e08fad7e 100644 --- a/autotests/server.cpp +++ b/autotests/server.cpp @@ -12,6 +12,8 @@ #include #include +#include +#include #include using namespace Qt::Literals::StringLiterals; @@ -116,6 +118,20 @@ void Server::start() }); m_server.route(u"/_matrix/client/r0/sync"_s, QHttpServerRequest::Method::Get, this, &Server::sync); + m_server.route(u"/_matrix/client/v1/media/download//"_s, + QHttpServerRequest::Method::Get, + [](const QString &serverName, const QString &mediaId, QHttpServerResponder &responder) { + qInfo() << serverName << mediaId; + QImage image(128, 128, QImage::Format::Format_RGB32); + image.fill(Qt::white); + + QByteArray bytes; + QBuffer buffer(&bytes); + buffer.open(QIODevice::WriteOnly); + image.save(&buffer, "PNG"); + + responder.write(&buffer, "image/png", QHttpServerResponder::StatusCode::Ok); + }); QSslConfiguration config; QFile key(QStringLiteral(DATA_DIR) + u"/localhost.key"_s); diff --git a/autotests/tst_media.qml b/autotests/tst_media.qml new file mode 100644 index 000000000..d9f45d2a8 --- /dev/null +++ b/autotests/tst_media.qml @@ -0,0 +1,73 @@ +/* + * SPDX-FileCopyrightText: 2025 Joshua Goins + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +import QtQuick +import QtQuick.Controls as QQC2 +import QtTest + +import org.kde.kirigami as Kirigami +import org.kde.neochat.messagecontent + +TestCase { + id: root + + name: "ImageComponentTest" + + width: 300 + height: 300 + + // Base component to not re-initialize the same variables over and over + component BaseImageComponent: ImageComponent { + eventId: "dummyevent" + display: "dummytext" + fileTransferInfo: null + } + + Component { + id: invalidMxcUrlComponent + BaseImageComponent { + componentAttributes: QtObject { + property bool isSticker: false + property bool animated: false + // Missing user_id, which is required in libQuotient + property string source: "mxc://localhost:1234/AAAAAAAAAAAAAAAAAAAAAAAA?room_id=!AjYwbldYDmSVfGrVHV:localhost&event_id=$vJfWLoXK02im0M3rlFWLosiHojrwWSknLb0JXveEE1o" + } + } + } + + Component { + id: validMxcUrlComponent + BaseImageComponent { + componentAttributes: QtObject { + property bool isSticker: false + property bool animated: false + property string source: "mxc://localhost:1234/AAAAAAAAAAAAAAAAAAAAAAAA?user_id=@user:localhost:1234&room_id=!AjYwbldYDmSVfGrVHV:localhost&event_id=$vJfWLoXK02im0M3rlFWLosiHojrwWSknLb0JXveEE1o" + } + } + } + + function test_invalid() { + wait(1000); // Wait for Quotient to grab the right capability + + ignoreWarning("No connection specified, cannot convert mxc request"); + + const item = createTemporaryObject(invalidMxcUrlComponent, this); + verify(item); + compare(item._private.imageItem.status, Image.Loading); + + // It should fail if we didn't specify the connection + tryCompare(item._private.imageItem, "status", Image.Error); + } + + function test_basic() { + wait(1000); // Wait for Quotient to grab the right capability + + const item = createTemporaryObject(validMxcUrlComponent, this); + verify(item); + compare(item._private.imageItem.status, Image.Loading); // initial load + tryCompare(item._private.imageItem, "status", Image.Error); + } +} diff --git a/src/messagecontent/ImageComponent.qml b/src/messagecontent/ImageComponent.qml index 22e38b4da..dbe601985 100644 --- a/src/messagecontent/ImageComponent.qml +++ b/src/messagecontent/ImageComponent.qml @@ -43,6 +43,8 @@ Item { */ property var contentMaxHeight: undefined + readonly property alias _private: _private + implicitWidth: mediaSizeHelper.currentSize.width implicitHeight: mediaSizeHelper.currentSize.height diff --git a/src/messagecontent/mediasizehelper.cpp b/src/messagecontent/mediasizehelper.cpp index 1726c15e5..b33c24338 100644 --- a/src/messagecontent/mediasizehelper.cpp +++ b/src/messagecontent/mediasizehelper.cpp @@ -150,6 +150,10 @@ QSize MediaSizeHelper::currentSize() const if (height > heightLimit()) { return QSize(qRound(heightLimit() * aspectRatio()), qRound(heightLimit())); } + // TODO: NO + if (qIsNaN(width) || qIsNaN(height)) { + return {}; + } return QSize(qRound(width), qRound(height)); } else { qreal height = std::min(heightLimit(), resolvedMediaHeight());