From ae53d92c2e6d4bb1d31429fdf74ae38a0e307856 Mon Sep 17 00:00:00 2001 From: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com> Date: Thu, 1 Feb 2024 19:06:01 -0500 Subject: [PATCH] testing: add a way to test individual QML components This is a WIP and is intended to be adapted continuously to support more and more UI elements and reduce the time spent debugging components. Some components will require additional configuration (e.g. the conversation ID must be set), which may require additional changes. Change-Id: Iaa5d49693f874202439e746a274da4911adf7d15 --- src/app/ComponentTestWindow.qml | 65 ++++++++++++ src/app/LayoutManager.qml | 14 +++ src/app/MainApplicationWindow.qml | 26 +---- src/app/mainapplication.cpp | 100 ++++++++++++++---- src/app/mainapplication.h | 3 +- .../mainview/components/AccountComboBox.qml | 2 +- .../mainview/components/ChatViewHeader.qml | 4 +- src/app/mainview/components/MainOverlay.qml | 4 +- tests/qml/src/TestWrapper.qml | 22 +++- 9 files changed, 188 insertions(+), 52 deletions(-) create mode 100644 src/app/ComponentTestWindow.qml diff --git a/src/app/ComponentTestWindow.qml b/src/app/ComponentTestWindow.qml new file mode 100644 index 000000000..7729d5863 --- /dev/null +++ b/src/app/ComponentTestWindow.qml @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2024 Savoir-faire Linux Inc. + * + * This program 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 3 of the License, or + * (at your option) any later version. + * + * This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + */ + +import QtQuick +import QtQuick.Controls + +import net.jami.Adapters 1.1 + +// A window into which we can load a QML file for testing. +ApplicationWindow { + id: appWindow + visible: true + width: testWidth || loader.implicitWidth || 800 + height: testHeight || loader.implicitHeight || 600 + title: testComponentURI + + // WARNING: The following currently must be maintained in tandem with MainApplicationWindow.qml + // Used to manage full screen mode and save/restore window geometry. + readonly property bool useFrameless: false + property bool isRTL: UtilsAdapter.isRTL + LayoutMirroring.enabled: isRTL + LayoutMirroring.childrenInherit: isRTL + property LayoutManager layoutManager: LayoutManager { + appContainer: null + } + // Used to manage dynamic view loading and unloading. + property ViewManager viewManager: ViewManager {} + // Used to manage the view stack and the current view. + property ViewCoordinator viewCoordinator: ViewCoordinator {} + + Loader { + id: loader + source: Qt.resolvedUrl(testComponentURI) + onStatusChanged: { + console.log("Status changed to:", loader.status) + if (loader.status == Loader.Error || loader.status == Loader.Null) { + console.error("Couldn't load component:", source) + Qt.exit(1); + } else if (loader.status == Loader.Ready) { + console.info("Loaded component:", source); + // If any of the dimensions are not set, set them to the appWindow's dimensions + item.width = item.width || Qt.binding(() => appWindow.width); + item.height = item.height || Qt.binding(() => appWindow.height); + viewCoordinator.init(item); + } + } + } + + // Closing this window should always exit the application. + onClosing: Qt.quit() +} diff --git a/src/app/LayoutManager.qml b/src/app/LayoutManager.qml index f36b2b685..97017c240 100644 --- a/src/app/LayoutManager.qml +++ b/src/app/LayoutManager.qml @@ -41,6 +41,20 @@ QtObject { // Used to store if a OngoingCallPage component is fullscreened. property bool isCallFullscreen: false + // QWK: Provide spacing for widgets that may be occluded by the system buttons. + property QtObject qwkSystemButtonSpacing: QtObject { + id: qwkSystemButtonSpacing + readonly property bool isMacOS: Qt.platform.os.toString() === "osx" + // macOS buttons are on the left. + readonly property real left: { + appWindow.useFrameless && isMacOS && viewCoordinator.isInSinglePaneMode ? 80 : 0 + } + // Windows and Linux buttons are on the right. + readonly property real right: { + appWindow.useFrameless && !isMacOS && !root.isFullscreen ? sysBtnsLoader.width + 24 : 0 + } + } + // Restore a visible windowed mode. function restoreApp() { if (isHidden) { diff --git a/src/app/MainApplicationWindow.qml b/src/app/MainApplicationWindow.qml index 31b9dee72..b94475ad2 100644 --- a/src/app/MainApplicationWindow.qml +++ b/src/app/MainApplicationWindow.qml @@ -41,14 +41,11 @@ import QWindowKit ApplicationWindow { id: appWindow + readonly property bool useFrameless: UtilsAdapter.getAppValue(Settings.Key.UseFramelessWindow) property bool isRTL: UtilsAdapter.isRTL - LayoutMirroring.enabled: isRTL LayoutMirroring.childrenInherit: isRTL - // This needs to be set from the start. - readonly property bool useFrameless: UtilsAdapter.getAppValue(Settings.Key.UseFramelessWindow) - onActiveFocusItemChanged: { focusOverlay.margin = -5; if (activeFocusItem && ((activeFocusItem.focusReason === Qt.TabFocusReason) || (activeFocusItem.focusReason === Qt.BacktabFocusReason))) { @@ -94,16 +91,10 @@ ApplicationWindow { id: layoutManager appContainer: fullscreenContainer } - // Used to manage dynamic view loading and unloading. - ViewManager { - id: viewManager - } - + property ViewManager viewManager: ViewManager {} // Used to manage the view stack and the current view. - ViewCoordinator { - id: viewCoordinator - } + property ViewCoordinator viewCoordinator: ViewCoordinator {} // Used to prevent the window from being visible until the // window geometry has been restored and the view stack has @@ -234,17 +225,6 @@ ApplicationWindow { anchors.fill: parent } - // QWK: Provide spacing for widgets that may be occluded by the system buttons. - QtObject { - id: qwkSystemButtonSpacing - readonly property bool isMacOS: Qt.platform.os.toString() === "osx" - readonly property bool isFullscreen: layoutManager.isFullScreen - // macOS buttons are on the left. - readonly property real left: useFrameless && isMacOS && viewCoordinator.isInSinglePaneMode ? 80 : 0 - // Windows and Linux buttons are on the right. - readonly property real right: useFrameless && !isMacOS && !isFullscreen ? sysBtnsLoader.width + 24 : 0 - } - // QWK: Window Title bar Item { id: titleBar diff --git a/src/app/mainapplication.cpp b/src/app/mainapplication.cpp index 48b4fed51..8398bbf1b 100644 --- a/src/app/mainapplication.cpp +++ b/src/app/mainapplication.cpp @@ -335,49 +335,59 @@ MainApplication::parseArguments() } } - QCommandLineParser parser; - parser.addHelpOption(); - parser.addVersionOption(); + parser_.addHelpOption(); + parser_.addVersionOption(); QCommandLineOption webDebugOption(QStringList() << "remote-debugging-port", "Web debugging port.", "port"); - parser.addOption(webDebugOption); + parser_.addOption(webDebugOption); QCommandLineOption minimizedOption({"m", "minimized"}, "Start minimized."); - parser.addOption(minimizedOption); + parser_.addOption(minimizedOption); QCommandLineOption debugOption({"d", "debug"}, "Debug out."); - parser.addOption(debugOption); + parser_.addOption(debugOption); QCommandLineOption logFileOption({"f", "file"}, "Debug to <file>.", "file"); - parser.addOption(logFileOption); + parser_.addOption(logFileOption); #ifdef Q_OS_WINDOWS QCommandLineOption updateUrlOption({"u", "url"}, "<url> for debugging version queries.", "url"); - parser.addOption(updateUrlOption); + parser_.addOption(updateUrlOption); #endif QCommandLineOption terminateOption({"t", "term"}, "Terminate all instances."); - parser.addOption(terminateOption); + parser_.addOption(terminateOption); QCommandLineOption muteDaemonOption({"q", "quiet"}, "Mute daemon logging. (only if debug)"); - parser.addOption(muteDaemonOption); + parser_.addOption(muteDaemonOption); - parser.process(*this); +#ifdef QT_DEBUG + // In debug mode, add an option to test a specific QML component via its name. + // e.g. ./jami --test AccountComboBox + parser_.addOption(QCommandLineOption("test", "Test a QML component via its name.", "uri")); + // We may need to force the test window dimensions in the case that the component to test + // does not specify its own dimensions and is dependent on parent/sibling dimensions. + // e.g. ./jami --test AccountComboBox -w 200 + parser_.addOption(QCommandLineOption("width", "Width for the test window.", "width")); + parser_.addOption(QCommandLineOption("height", "Height for the test window.", "height")); +#endif - runOptions_[Option::StartMinimized] = parser.isSet(minimizedOption); - runOptions_[Option::Debug] = parser.isSet(debugOption); - if (parser.isSet(logFileOption)) { - auto logFileValue = parser.value(logFileOption); + parser_.process(*this); + + runOptions_[Option::StartMinimized] = parser_.isSet(minimizedOption); + runOptions_[Option::Debug] = parser_.isSet(debugOption); + if (parser_.isSet(logFileOption)) { + auto logFileValue = parser_.value(logFileOption); auto logFile = logFileValue.isEmpty() ? Utils::getDebugFilePath() : logFileValue; qputenv("JAMI_LOG_FILE", logFile.toStdString().c_str()); } #ifdef Q_OS_WINDOWS - runOptions_[Option::UpdateUrl] = parser.value(updateUrlOption); + runOptions_[Option::UpdateUrl] = parser_.value(updateUrlOption); #endif - runOptions_[Option::TerminationRequested] = parser.isSet(terminateOption); - runOptions_[Option::MuteDaemon] = parser.isSet(muteDaemonOption); + runOptions_[Option::TerminationRequested] = parser_.isSet(terminateOption); + runOptions_[Option::MuteDaemon] = parser_.isSet(muteDaemonOption); } void @@ -393,6 +403,35 @@ MainApplication::setApplicationFont() setFont(font); } +QString +findResource(const QString& targetBasename, const QString& basePath = ":/") +{ + QDir dir(basePath); + // List all entries in the directory excluding special entries '.' and '..' + QStringList entries = dir.entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot, + QDir::DirsFirst); + + Q_FOREACH (const QString& entry, entries) { + QString fullPath = basePath + "/" + entry; + QFileInfo fileInfo(fullPath); + + if (fileInfo.isDir()) { + // Recursively search in subdirectories + QString found = findResource(targetBasename, fullPath); + if (!found.isEmpty()) { + return found; // Return the first match found in any subdirectory + } + } else if (fileInfo.isFile() + && fileInfo.fileName().contains(targetBasename, Qt::CaseInsensitive)) { + // Match found, return the full path but remove the leading ":/". + return fileInfo.absoluteFilePath().mid(2); + } + } + + // No match found in this directory or its subdirectories + return QString(); +} + void MainApplication::initQmlLayer() { @@ -406,7 +445,30 @@ MainApplication::initQmlLayer() &screenInfo_, this); - engine_->load(QUrl(QStringLiteral("qrc:/MainApplicationWindow.qml"))); + QUrl url; + if (parser_.isSet("test")) { + // List the QML files in the project source tree. + const auto targetTestComponent = findResource(parser_.value("test")); + if (targetTestComponent.isEmpty()) { + C_FATAL << "Failed to find QML component:" << parser_.value("test"); + } + engine_->rootContext()->setContextProperty("testComponentURI", targetTestComponent); + // Log the width and height values for the test window. + const auto testWidth = parser_.isSet("width") ? parser_.value("width").toInt() : 0; + const auto testHeight = parser_.isSet("height") ? parser_.value("height").toInt() : 0; + engine_->rootContext()->setContextProperty("testWidth", testWidth); + engine_->rootContext()->setContextProperty("testHeight", testHeight); + url = u"qrc:/ComponentTestWindow.qml"_qs; + } else { + url = u"qrc:/MainApplicationWindow.qml"_qs; + } + QObject::connect( + engine_.get(), + &QQmlApplicationEngine::objectCreationFailed, + this, + [url]() { C_FATAL << "Failed to load QML component:" << url; }, + Qt::QueuedConnection); + engine_->load(url); // Report the render interface used. C_DBG << "Main window loaded using" << getRenderInterfaceString(); diff --git a/src/app/mainapplication.h b/src/app/mainapplication.h index d7117d687..e318ad73c 100644 --- a/src/app/mainapplication.h +++ b/src/app/mainapplication.h @@ -29,6 +29,7 @@ #include <QQmlEngine> #include <QScreen> #include <QWindow> +#include <QCommandLineParser> #include <memory> @@ -122,6 +123,6 @@ private: SystemTray* systemTray_; AppSettingsManager* settingsManager_; PreviewEngine* previewEngine_; - ScreenInfo screenInfo_; + QCommandLineParser parser_; }; diff --git a/src/app/mainview/components/AccountComboBox.qml b/src/app/mainview/components/AccountComboBox.qml index aa455bd20..c2f7c3b1c 100644 --- a/src/app/mainview/components/AccountComboBox.qml +++ b/src/app/mainview/components/AccountComboBox.qml @@ -29,7 +29,7 @@ Label { property alias popup: comboBoxPopup - width: parent ? parent.width : o + width: parent ? parent.width : 0 height: JamiTheme.accountListItemHeight property bool inSettings: viewCoordinator.currentViewName === "SettingsView" diff --git a/src/app/mainview/components/ChatViewHeader.qml b/src/app/mainview/components/ChatViewHeader.qml index d33ab24cc..69c02d7f7 100644 --- a/src/app/mainview/components/ChatViewHeader.qml +++ b/src/app/mainview/components/ChatViewHeader.qml @@ -75,8 +75,8 @@ Rectangle { anchors.fill: parent // QWK: spacing - anchors.leftMargin: qwkSystemButtonSpacing.left - anchors.rightMargin: 10 + qwkSystemButtonSpacing.right + anchors.leftMargin: layoutManager.qwkSystemButtonSpacing.left + anchors.rightMargin: 10 + layoutManager.qwkSystemButtonSpacing.right spacing: 16 JamiPushButton { QWKSetParentHitTestVisible {} diff --git a/src/app/mainview/components/MainOverlay.qml b/src/app/mainview/components/MainOverlay.qml index aabf85e54..455ecb347 100644 --- a/src/app/mainview/components/MainOverlay.qml +++ b/src/app/mainview/components/MainOverlay.qml @@ -131,8 +131,8 @@ Item { anchors.left: parent.left anchors.right: parent.right // QWK: spacing - anchors.leftMargin: qwkSystemButtonSpacing.left - anchors.rightMargin: qwkSystemButtonSpacing.right + anchors.leftMargin: layoutManager.qwkSystemButtonSpacing.left + anchors.rightMargin: layoutManager.qwkSystemButtonSpacing.right RowLayout { anchors.fill: parent diff --git a/tests/qml/src/TestWrapper.qml b/tests/qml/src/TestWrapper.qml index d8f4510d4..0f50f3f8a 100644 --- a/tests/qml/src/TestWrapper.qml +++ b/tests/qml/src/TestWrapper.qml @@ -19,6 +19,8 @@ import QtQuick import QtQuick.Controls import QtTest +import net.jami.Adapters 1.1 + import "../../../src/app/" // The purpose of this component is to fake the ApplicationWindow and prevent @@ -38,11 +40,23 @@ Item { Component.onCompleted: viewCoordinator.init(this) - // These are our fake app management objects. The caveat is that they - // must be maintained in sync with the actual objects in the app for now. - // The benefit, is that this should be the single place where we need to - // sync them. + property int visibility: 0 + Binding { + tw.visibility: uut.Window.window.visibility + when: QTestRootObject.windowShown + } + + // WARNING: The following currently must be maintained in tandem with MainApplicationWindow.qml + // Used to manage full screen mode and save/restore window geometry. + property bool isRTL: UtilsAdapter.isRTL + LayoutMirroring.enabled: isRTL + LayoutMirroring.childrenInherit: isRTL + property LayoutManager layoutManager: LayoutManager { + appContainer: null + } + // Used to manage dynamic view loading and unloading. property ViewManager viewManager: ViewManager {} + // Used to manage the view stack and the current view. property ViewCoordinator viewCoordinator: ViewCoordinator {} property QtObject appWindow: QtObject { property bool useFrameless: false -- GitLab