1
0
Fork 0
mirror of https://git.jami.net/savoirfairelinux/jami-client-qt.git synced 2025-09-10 03:53:23 +02:00

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
This commit is contained in:
Andreas Traczyk 2024-02-01 19:06:01 -05:00
parent 97e477416a
commit ae53d92c2e
9 changed files with 188 additions and 52 deletions

View file

@ -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()
}

View file

@ -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) {

View file

@ -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

View file

@ -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();

View file

@ -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_;
};

View file

@ -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"

View file

@ -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 {}

View file

@ -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

View file

@ -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