diff --git a/CMakePresets.json b/CMakePresets.json index 2fa502327..1e61472b5 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -76,8 +76,9 @@ "outputOnFailure": true }, "execution": { - "noTestsAction": "error" + "noTestsAction": "error", + "excludeLabel": "manual" } } ] -} +} \ No newline at end of file diff --git a/src/core/lockscreen.h b/src/core/lockscreen.h index 7078c0636..279c313d3 100644 --- a/src/core/lockscreen.h +++ b/src/core/lockscreen.h @@ -44,6 +44,7 @@ class LockScreen : public SurfaceContainer void shutdown(); void switchUser(); void setPrimaryOutputName(const QString &primaryOutputName); + QString primaryOutputName() const { return m_primaryOutputName; } Q_SIGNALS: void unlock(); diff --git a/src/core/rootsurfacecontainer.h b/src/core/rootsurfacecontainer.h index 79cc7c5a9..42c7c24e0 100644 --- a/src/core/rootsurfacecontainer.h +++ b/src/core/rootsurfacecontainer.h @@ -2,6 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #pragma once +// TODO(unit-test): RootSurfaceContainer depends on WCursor and WOutputLayout which +// require a running compositor with outputs. Ref: WM-29. Solution: Extract layout/priority logic for unit testing. + #include "surface/surfacecontainer.h" #include "surface/seatsurfacemanager.h" diff --git a/src/core/shellhandler.h b/src/core/shellhandler.h index 362cb8772..243ddb540 100644 --- a/src/core/shellhandler.h +++ b/src/core/shellhandler.h @@ -3,6 +3,9 @@ #pragma once +// TODO(unit-test): ShellHandler depends on the running Wayland compositor for wl_client +// and surface resources. Ref: WM-29. Solution: Extract protocol interaction logic into testable pure components. + #include "modules/foreign-toplevel/foreigntoplevelmanagerv1.h" #include diff --git a/src/greeter/greeterproxy.cpp b/src/greeter/greeterproxy.cpp index 37defcf7f..0a2dd5333 100644 --- a/src/greeter/greeterproxy.cpp +++ b/src/greeter/greeterproxy.cpp @@ -121,6 +121,12 @@ GreeterProxy::GreeterProxy(QObject *parent) updateAuthSocket(); } +GreeterProxy::GreeterProxy(bool testMode, QObject *parent) + : QObject(parent) + , m_testMode(testMode) +{ +} + GreeterProxy::~GreeterProxy() { } //////////////////////// @@ -234,6 +240,10 @@ void GreeterProxy::logout() void GreeterProxy::lock() { + if (m_testMode) { + setLock(true); + return; + } auto session = Helper::instance()->sessionManager()->activeSession().lock(); if (!session || session->username() == "dde") { qCInfo(lcTlGreeter) << "Trying to lock when no user session active, show lockscreen directly."; @@ -390,7 +400,7 @@ void GreeterProxy::onSessionUnlock() bool GreeterProxy::isConnected() const { - return m_socket->state() == QLocalSocket::ConnectedState; + return m_socket && m_socket->state() == QLocalSocket::ConnectedState; } void GreeterProxy::connected() diff --git a/src/greeter/greeterproxy.h b/src/greeter/greeterproxy.h index e628e0a51..e1ca602d4 100644 --- a/src/greeter/greeterproxy.h +++ b/src/greeter/greeterproxy.h @@ -41,6 +41,7 @@ class GreeterProxy public: explicit GreeterProxy(QObject *parent = nullptr); + explicit GreeterProxy(bool testMode, QObject *parent = nullptr); ~GreeterProxy(); ////////////////////// @@ -89,7 +90,7 @@ class GreeterProxy * * @return true if is locked */ - inline bool isLocked() const { return m_isLocked; }; + virtual bool isLocked() const { return m_isLocked; }; /** * @brief Get the number of failed login attempts (password incorrect) @@ -106,7 +107,7 @@ class GreeterProxy * * @return true if shutdown view is shown */ - inline bool showShutdownView() const { return m_showShutdownView; }; + virtual bool showShutdownView() const { return m_showShutdownView; }; /** * @brief Get whether to show animation on lock/unlock @@ -134,7 +135,7 @@ class GreeterProxy * * @param show true to show shutdown view, false to hide */ - void setShowShutdownView(bool show); + virtual void setShowShutdownView(bool show); //////////////////// // Public methods // @@ -194,7 +195,7 @@ public Q_SLOTS: * Listen to org.freedesktop.login1.Session.Lock signal to detect * if the session is successfully locked. */ - void lock(); + virtual void lock(); /** @brief Unlock given user with given password. * This function will call DDM to perform the unlock. @@ -246,7 +247,7 @@ private Q_SLOTS: Q_SIGNALS: void informationMessage(const QString &message); - void switchUser(); + virtual void switchUser(); void socketDisconnected(); @@ -314,6 +315,7 @@ private Q_SLOTS: QLocalSocket *m_socket{ nullptr }; LockScreen *m_lockScreen{ nullptr }; + bool m_testMode{ false }; QString m_hostName{}; diff --git a/src/output/backlight.cpp b/src/output/backlight.cpp index d92b79570..4ca23f6d8 100644 --- a/src/output/backlight.cpp +++ b/src/output/backlight.cpp @@ -10,24 +10,42 @@ QW_USE_NAMESPACE -Backlight::Backlight(const QString &name) - : m_name(name) +Backlight::Backlight(const QString &name, const QString &basePath) + : m_maxBrightness(0) + , m_brightnessLevel(0) + , m_name(name) + , m_basePath(basePath) { - QFile maxBrightnessFile(QString("/sys/class/backlight/%1/max_brightness").arg(name)); - Q_ASSERT(maxBrightnessFile.open(QIODevice::ReadOnly)); + QFile maxBrightnessFile(QString("%1/%2/max_brightness").arg(m_basePath, name)); + if (!maxBrightnessFile.open(QIODevice::ReadOnly)) { + qCWarning(lcTlOutput) << "Backlight: Failed to open max_brightness for" << name; + return; + } bool ok = false; m_maxBrightness = maxBrightnessFile.readLine().trimmed().toLongLong(&ok); maxBrightnessFile.close(); - Q_ASSERT(ok); - Q_ASSERT(m_maxBrightness > 0); + if (!ok || m_maxBrightness <= 0) { + qCWarning(lcTlOutput) << "Backlight: Invalid max_brightness for" << name; + m_maxBrightness = 0; + return; + } - QFile brightnessFile(QString("/sys/class/backlight/%1/brightness").arg(name)); - Q_ASSERT(brightnessFile.open(QIODevice::ReadOnly)); + QFile brightnessFile(QString("%1/%2/brightness").arg(m_basePath, name)); + if (!brightnessFile.open(QIODevice::ReadOnly)) { + qCWarning(lcTlOutput) << "Backlight: Failed to open brightness for" << name; + m_maxBrightness = 0; + return; + } m_brightnessLevel = brightnessFile.readLine().trimmed().toLongLong(&ok); brightnessFile.close(); - Q_ASSERT(ok); + if (!ok) { + qCWarning(lcTlOutput) << "Backlight: Invalid brightness for" << name; + m_maxBrightness = 0; + m_brightnessLevel = 0; + return; + } } Backlight::~Backlight() @@ -37,6 +55,8 @@ Backlight::~Backlight() qreal Backlight::brightness() const { + if (m_maxBrightness <= 0) + return 0.0; return m_brightnessLevel / static_cast(m_maxBrightness); } @@ -48,7 +68,7 @@ qreal Backlight::setBrightness(qreal brightness) return this->brightness(); } - QFile brightnessFile(QString("/sys/class/backlight/%1/brightness").arg(m_name)); + QFile brightnessFile(QString("%1/%2/brightness").arg(m_basePath, m_name)); if (!brightnessFile.open(QIODevice::WriteOnly)) { qCWarning(lcTlOutput) << "Output" << m_name << ": Failed to open backlight brightness file for writing."; return this->brightness(); @@ -57,7 +77,10 @@ qreal Backlight::setBrightness(qreal brightness) QByteArray brightnessStr = QByteArray::number(brightnessLevel); qint64 written = brightnessFile.write(brightnessStr); brightnessFile.close(); - Q_ASSERT(written == brightnessStr.size()); + if (written != brightnessStr.size()) { + qCWarning(lcTlOutput) << "Output" << m_name << ": Failed to write backlight brightness."; + return this->brightness(); + } m_brightnessLevel = brightnessLevel; return this->brightness(); diff --git a/src/output/backlight.h b/src/output/backlight.h index d23c7448a..01cce9d4b 100644 --- a/src/output/backlight.h +++ b/src/output/backlight.h @@ -1,4 +1,4 @@ -// Copyright (C) 2025 UnionTech Software Technology Co., Ltd. +// Copyright (C) 2025-2026 UnionTech Software Technology Co., Ltd. // SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #pragma once @@ -14,7 +14,7 @@ WAYLIB_SERVER_USE_NAMESPACE class Backlight { public: - Backlight(const QString &name); + explicit Backlight(const QString &name, const QString &basePath = QString("/sys/class/backlight")); ~Backlight(); static Backlight* createForOutput(WOutput* output); qreal brightness() const; @@ -23,4 +23,5 @@ class Backlight qlonglong m_maxBrightness; qlonglong m_brightnessLevel; QString m_name; + QString m_basePath; }; diff --git a/src/output/output.h b/src/output/output.h index d1a5f3c0a..b09433d70 100644 --- a/src/output/output.h +++ b/src/output/output.h @@ -91,6 +91,9 @@ class Output : public SurfaceListModel double heightPx, double widthMm, double heightMm); + static QPointF constrainToValidArea(const QPointF &pos, + const QSizeF &windowSize, + const QRectF &validGeo); qreal preferredScaleFactor(const QSize &pixelSize) const; OutputConfig* config() const; @@ -136,9 +139,6 @@ public Q_SLOTS: const QRectF &normalGeo, const QRectF &validGeo, const QSizeF &offset); - QPointF constrainToValidArea(const QPointF &pos, - const QSizeF &windowSize, - const QRectF &validGeo); qreal preferredScaleFactor() const; QPointF calculateBasePosition(SurfaceWrapper *surface, const QPointF &dPos) const; diff --git a/src/seat/helper.h b/src/seat/helper.h index b9cd38ece..56e7968cb 100644 --- a/src/seat/helper.h +++ b/src/seat/helper.h @@ -3,6 +3,10 @@ #pragma once +// TODO(unit-test): Helper is a singleton with deep coupling to the compositor runtime +// (WServer, WSeat, WBackend, session manager, etc.), making it impossible to instantiate +// in isolation. Ref: WM-29. Solution: Refactor to accept dependencies via constructor/parameters. + #include "core/qmlengine.h" #include "modules/activation/activationmanagerinterfacev1.h" #include "modules/shortcut/shortcutmanager.h" diff --git a/src/surface/surfacewrapper.h b/src/surface/surfacewrapper.h index c536376e6..2d1742b4d 100644 --- a/src/surface/surfacewrapper.h +++ b/src/surface/surfacewrapper.h @@ -2,6 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #pragma once +// TODO(unit-test): SurfaceWrapper inherits QQuickItem and requires the Qt Scene Graph +// rendering pipeline. Ref: WM-29. Solution: Separate business logic from rendering. + #include #include #include diff --git a/src/workspace/workspace.h b/src/workspace/workspace.h index a6bef61f4..634e65a5b 100644 --- a/src/workspace/workspace.h +++ b/src/workspace/workspace.h @@ -2,6 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #pragma once +// TODO(unit-test): Workspace depends on the Helper singleton for surface management. +// Ref: WM-29. Solution: Inject workspace dependencies to enable isolated testing. + #include "surface/surfacecontainer.h" #include "surface/surfacefilterproxymodel.h" #include "workspace/workspacemodel.h" diff --git a/src/xsettings/xresource.cpp b/src/xsettings/xresource.cpp index b12953943..6a8cc053d 100644 --- a/src/xsettings/xresource.cpp +++ b/src/xsettings/xresource.cpp @@ -6,7 +6,7 @@ #define XRESOURCE_ATOM_NAME "RESOURCE_MANAGER" -static QPair splitXResourceLine(const QByteArray &line) +QPair XResource::splitXResourceLine(const QByteArray &line) { int pos = -1; bool escaped = false; diff --git a/src/xsettings/xresource.h b/src/xsettings/xresource.h index 4197d85f5..342320ffe 100644 --- a/src/xsettings/xresource.h +++ b/src/xsettings/xresource.h @@ -1,4 +1,4 @@ -// Copyright (C) 2025 UnionTech Software Technology Co., Ltd. +// Copyright (C) 2025-2026 UnionTech Software Technology Co., Ltd. // SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #pragma once @@ -92,6 +92,7 @@ class XResource : public AbstractSettings ~XResource() override; static QByteArray toByteArray(XResourceKey key); + static QPair splitXResourceLine(const QByteArray &line); bool initialized() const override; bool isEmpty() const override; diff --git a/src/xsettings/xsettings.cpp b/src/xsettings/xsettings.cpp index 377ceaad5..35dc858db 100644 --- a/src/xsettings/xsettings.cpp +++ b/src/xsettings/xsettings.cpp @@ -28,6 +28,11 @@ XSettings::XSettings(xcb_connection_t *connection, QObject *parent) initX11(-1, true); } +XSettings::XSettings(QObject *parent) + : AbstractSettings(nullptr, parent) +{ +} + XSettings::~XSettings() { } diff --git a/src/xsettings/xsettings.h b/src/xsettings/xsettings.h index 354f9bb92..bb022afca 100644 --- a/src/xsettings/xsettings.h +++ b/src/xsettings/xsettings.h @@ -1,4 +1,4 @@ -// Copyright (C) 2025 UnionTech Software Technology Co., Ltd. +// Copyright (C) 2025-2026 UnionTech Software Technology Co., Ltd. // SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #pragma once @@ -198,12 +198,15 @@ class XSettings : public AbstractSettings QByteArrayList propertyList() const override; void apply() override; +protected: + explicit XSettings(QObject *parent); + QByteArray depopulateSettings(); + void populateSettings(const QByteArray &xSettings); + private: bool initX11(int screen, bool replace); bool createWindow(int screen, xcb_window_t *out_win, xcb_timestamp_t *out_time); bool manageScreen(int screen, xcb_window_t win, xcb_timestamp_t timestamp, bool replace); - QByteArray depopulateSettings(); - void populateSettings(const QByteArray &xSettings); void setSettings(const QByteArray &data); private: diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 5f127b0db..256a2f536 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -8,3 +8,16 @@ add_subdirectory(test_protocol_virtual-output) add_subdirectory(test_protocol_wallpaper-color) add_subdirectory(test_protocol_window-management) add_subdirectory(test_protocol_prelaunch-splash) +add_subdirectory(test_shortcutcontroller) +add_subdirectory(test_wallpaperconfig) +add_subdirectory(test_outputconfigstate) +add_subdirectory(test_gestures) +add_subdirectory(test_togglablegesture) +add_subdirectory(test_protocol_integration) +add_subdirectory(test_output_scale) +add_subdirectory(test_xresource) +add_subdirectory(test_xsettings) +add_subdirectory(test_backlight) +add_subdirectory(test_lockscreen) +add_subdirectory(test_greeterproxy) +add_subdirectory(test_windowconfigstore) diff --git a/tests/test_backlight/CMakeLists.txt b/tests/test_backlight/CMakeLists.txt new file mode 100644 index 000000000..849cc735c --- /dev/null +++ b/tests/test_backlight/CMakeLists.txt @@ -0,0 +1,9 @@ +find_package(Qt6 REQUIRED COMPONENTS Test) + +add_executable(test_backlight main.cpp) +target_link_libraries(test_backlight PRIVATE libtreeland Qt6::Test) +add_test(NAME test_backlight COMMAND test_backlight) +set_tests_properties(test_backlight PROPERTIES + ENVIRONMENT "QT_QPA_PLATFORM=offscreen" + TIMEOUT 10 +) diff --git a/tests/test_backlight/main.cpp b/tests/test_backlight/main.cpp new file mode 100644 index 000000000..499013a42 --- /dev/null +++ b/tests/test_backlight/main.cpp @@ -0,0 +1,105 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include +#include +#include +#include + +#include "output/backlight.h" + +class TestBacklight : public QObject +{ + Q_OBJECT + +private: + QTemporaryDir m_tempDir; + + bool createBacklightFiles(const QString &name, int maxBrightness, int currentBrightness) + { + QDir dir(m_tempDir.path()); + QDir subDir(dir.filePath(name)); + if (!subDir.exists()) { + if (!dir.mkdir(name)) + return false; + } + + QFile maxFile(subDir.filePath("max_brightness")); + if (!maxFile.open(QIODevice::WriteOnly)) + return false; + maxFile.write(QByteArray::number(maxBrightness)); + maxFile.close(); + + QFile brFile(subDir.filePath("brightness")); + if (!brFile.open(QIODevice::WriteOnly)) + return false; + brFile.write(QByteArray::number(currentBrightness)); + brFile.close(); + + return true; + } + +private Q_SLOTS: + void initTestCase(); + void testBrightness(); + void testSetBrightness(); + void testSetBrightnessFull(); + void testSetBrightnessZero(); + void testSetBrightnessSameLevel(); + void testSetBrightnessClamp(); +}; + +void TestBacklight::initTestCase() +{ + QVERIFY(m_tempDir.isValid()); + QVERIFY(createBacklightFiles("intel_backlight", 100, 50)); +} + +void TestBacklight::testBrightness() +{ + Backlight bl("intel_backlight", m_tempDir.path()); + QCOMPARE(bl.brightness(), 0.5); +} + +void TestBacklight::testSetBrightness() +{ + createBacklightFiles("test_set", 100, 50); + Backlight bl("test_set", m_tempDir.path()); + qreal result = bl.setBrightness(0.8); + QVERIFY(qFuzzyCompare(result, 0.8)); +} + +void TestBacklight::testSetBrightnessFull() +{ + createBacklightFiles("test_full", 100, 50); + Backlight bl("test_full", m_tempDir.path()); + qreal result = bl.setBrightness(1.0); + QCOMPARE(result, 1.0); +} + +void TestBacklight::testSetBrightnessZero() +{ + createBacklightFiles("test_zero", 100, 50); + Backlight bl("test_zero", m_tempDir.path()); + qreal result = bl.setBrightness(0.0); + QCOMPARE(result, 0.0); +} + +void TestBacklight::testSetBrightnessSameLevel() +{ + createBacklightFiles("test_same", 100, 50); + Backlight bl("test_same", m_tempDir.path()); + qreal result = bl.setBrightness(0.5); + QCOMPARE(result, 0.5); +} + +void TestBacklight::testSetBrightnessClamp() +{ + createBacklightFiles("test_clamp", 100, 50); + Backlight bl("test_clamp", m_tempDir.path()); + qreal result = bl.setBrightness(1.5); + QCOMPARE(result, 1.0); +} + +QTEST_MAIN(TestBacklight) +#include "main.moc" diff --git a/tests/test_gestures/CMakeLists.txt b/tests/test_gestures/CMakeLists.txt new file mode 100644 index 000000000..1ac528ba1 --- /dev/null +++ b/tests/test_gestures/CMakeLists.txt @@ -0,0 +1,19 @@ +find_package(Qt6 REQUIRED COMPONENTS Test) + +add_executable(test_gestures main.cpp) + +target_link_libraries(test_gestures + PRIVATE + libtreeland + Qt::Test +) + +add_test(NAME test_gestures COMMAND test_gestures) + +set_property(TEST test_gestures PROPERTY + ENVIRONMENT "QT_QPA_PLATFORM=offscreen" +) + +set_property(TEST test_gestures PROPERTY + TIMEOUT 10 +) diff --git a/tests/test_gestures/main.cpp b/tests/test_gestures/main.cpp new file mode 100644 index 000000000..f30b1b70b --- /dev/null +++ b/tests/test_gestures/main.cpp @@ -0,0 +1,268 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "input/gestures.h" + +#include +#include +#include +#include +#include + +class GesturesTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + + void testSwipeGestureDefaultDirection() + { + SwipeGesture gesture; + QCOMPARE(gesture.direction(), SwipeGesture::Down); + } + + void testSwipeGestureFingerCount() + { + SwipeGesture gesture; + gesture.setMinimumFingerCount(3); + gesture.setMaximumFingerCount(4); + QCOMPARE(gesture.minimumFingerCount(), 3u); + QCOMPARE(gesture.maximumFingerCount(), 4u); + QVERIFY(gesture.minimumFingerCountIsRelevant()); + QVERIFY(gesture.maximumFingerCountIsRelevant()); + } + + void testSwipeGestureMaxFingerCount() + { + SwipeGesture gesture; + gesture.setMaximumFingerCount(5); + QCOMPARE(gesture.maximumFingerCount(), 5u); + } + + void testSwipeGestureDirectionSet() + { + SwipeGesture gesture; + gesture.setDirection(SwipeGesture::Left); + QCOMPARE(gesture.direction(), SwipeGesture::Left); + } + + void testSwipeGestureMinimumDelta() + { + SwipeGesture gesture; + gesture.setMinimumDelta(QPointF(100, 0)); + QCOMPARE(gesture.minimumDelta(), QPointF(100, 0)); + } + + void testSwipeGestureDeltaToProgress() + { + SwipeGesture gesture; + gesture.setDirection(SwipeGesture::Right); + gesture.setMinimumDelta(QPointF(100, 0)); + QCOMPARE(gesture.deltaToProgress(QPointF(50, 0)), 0.5); + } + + void testSwipeGestureMinimumDeltaReached() + { + SwipeGesture gesture; + gesture.setDirection(SwipeGesture::Right); + gesture.setMinimumDelta(QPointF(100, 0)); + QVERIFY(gesture.minimumDeltaReached(QPointF(100, 0))); + QVERIFY(!gesture.minimumDeltaReached(QPointF(50, 0))); + } + + void testSwipeGesturePositionConstraints() + { + SwipeGesture gesture; + gesture.setStartGeometry(QRect(0, 0, 100, 100)); + QVERIFY(gesture.minimumXIsRelevant()); + QCOMPARE(gesture.minimumX(), 0); + QVERIFY(gesture.maximumXIsRelevant()); + QCOMPARE(gesture.maximumX(), 99); + QVERIFY(gesture.minimumYIsRelevant()); + QCOMPARE(gesture.minimumY(), 0); + QVERIFY(gesture.maximumYIsRelevant()); + QCOMPARE(gesture.maximumY(), 99); + } + + void testSwipeGestureOppositeDown() + { + QCOMPARE(SwipeGesture::opposite(SwipeGesture::Down), SwipeGesture::Up); + } + + void testSwipeGestureOppositeLeft() + { + QCOMPARE(SwipeGesture::opposite(SwipeGesture::Left), SwipeGesture::Right); + } + + void testSwipeGestureOppositeInvalid() + { + QCOMPARE(SwipeGesture::opposite(SwipeGesture::Invalid), SwipeGesture::Invalid); + } + + void testGestureRecognizerStartByFingerCount() + { + GestureRecognizer recognizer; + SwipeGesture gesture; + gesture.setMinimumFingerCount(3); + gesture.setMaximumFingerCount(3); + gesture.setDirection(SwipeGesture::Up); + QSignalSpy startedSpy(&gesture, &SwipeGesture::started); + recognizer.registerSwipeGesture(&gesture); + int count = recognizer.startSwipeGesture(3); + QCOMPARE(count, 1); + QCOMPARE(startedSpy.count(), 1); + } + + void testGestureRecognizerFingerMismatch() + { + GestureRecognizer recognizer; + SwipeGesture gesture; + gesture.setMinimumFingerCount(3); + gesture.setMaximumFingerCount(3); + gesture.setDirection(SwipeGesture::Up); + QSignalSpy startedSpy(&gesture, &SwipeGesture::started); + recognizer.registerSwipeGesture(&gesture); + int count = recognizer.startSwipeGesture(4); + QCOMPARE(count, 0); + QCOMPARE(startedSpy.count(), 0); + } + + void testGestureRecognizerCancel() + { + GestureRecognizer recognizer; + SwipeGesture gesture; + gesture.setMinimumFingerCount(3); + gesture.setDirection(SwipeGesture::Down); + QSignalSpy cancelledSpy(&gesture, &SwipeGesture::cancelled); + recognizer.registerSwipeGesture(&gesture); + recognizer.startSwipeGesture(3); + recognizer.cancelSwipeGesture(); + QCOMPARE(cancelledSpy.count(), 1); + } + + void testGestureRecognizerEndTriggered() + { + GestureRecognizer recognizer; + SwipeGesture gesture; + gesture.setDirection(SwipeGesture::Right); + gesture.setMinimumDelta(QPointF(100, 0)); + QSignalSpy triggeredSpy(&gesture, &SwipeGesture::triggered); + recognizer.registerSwipeGesture(&gesture); + recognizer.startSwipeGesture(1); + recognizer.updateSwipeGesture(QPointF(100, 0)); + recognizer.endSwipeGesture(); + QCOMPARE(triggeredSpy.count(), 1); + } + + void testGestureRecognizerEndCancelled() + { + GestureRecognizer recognizer; + SwipeGesture gesture; + gesture.setDirection(SwipeGesture::Right); + gesture.setMinimumDelta(QPointF(100, 0)); + QSignalSpy cancelledSpy(&gesture, &SwipeGesture::cancelled); + recognizer.registerSwipeGesture(&gesture); + recognizer.startSwipeGesture(1); + recognizer.updateSwipeGesture(QPointF(10, 0)); + recognizer.endSwipeGesture(); + QCOMPARE(cancelledSpy.count(), 1); + } + + void testGestureRecognizerStartByPosition() + { + GestureRecognizer recognizer; + SwipeGesture gesture; + gesture.setDirection(SwipeGesture::Down); + gesture.setStartGeometry(QRect(0, 0, 100, 100)); + QSignalSpy startedSpy(&gesture, &SwipeGesture::started); + recognizer.registerSwipeGesture(&gesture); + int count = recognizer.startSwipeGesture(QPointF(50, 50)); + QCOMPARE(count, 1); + QCOMPARE(startedSpy.count(), 1); + } + + void testGestureRecognizerHoldGesture() + { + GestureRecognizer recognizer; + HoldGesture gesture; + gesture.setInterval(100); + QSignalSpy longPressedSpy(&gesture, &HoldGesture::longPressed); + recognizer.registerHoldGesture(&gesture); + recognizer.startHoldGesture(1); + recognizer.endHoldGesture(); + QCOMPARE(longPressedSpy.count(), 0); + } + + void testGestureRecognizerHoldGestureLongPressed() + { + GestureRecognizer recognizer; + HoldGesture gesture; + gesture.setInterval(10); + QSignalSpy longPressedSpy(&gesture, &HoldGesture::longPressed); + recognizer.registerHoldGesture(&gesture); + recognizer.startHoldGesture(1); + QElapsedTimer timer; + timer.start(); + while (longPressedSpy.count() == 0 && timer.elapsed() < 1000) { + QCoreApplication::processEvents(); + QThread::msleep(5); + } + QCOMPARE(longPressedSpy.count(), 1); + recognizer.endHoldGesture(); + } + + void testGestureRecognizerAxisLockHorizontal() + { + GestureRecognizer recognizer; + SwipeGesture gesture; + gesture.setDirection(SwipeGesture::Right); + gesture.setMinimumDelta(QPointF(100, 0)); + QSignalSpy triggeredSpy(&gesture, &SwipeGesture::triggered); + recognizer.registerSwipeGesture(&gesture); + recognizer.startSwipeGesture(1); + recognizer.updateSwipeGesture(QPointF(50, 0)); + recognizer.updateSwipeGesture(QPointF(50, 30)); + recognizer.updateSwipeGesture(QPointF(50, -30)); + recognizer.endSwipeGesture(); + QCOMPARE(triggeredSpy.count(), 1); + } + + void testGestureRecognizerAxisLockVertical() + { + GestureRecognizer recognizer; + SwipeGesture gesture; + gesture.setDirection(SwipeGesture::Down); + gesture.setMinimumDelta(QPointF(0, 100)); + QSignalSpy triggeredSpy(&gesture, &SwipeGesture::triggered); + recognizer.registerSwipeGesture(&gesture); + recognizer.startSwipeGesture(1); + recognizer.updateSwipeGesture(QPointF(0, 50)); + recognizer.updateSwipeGesture(QPointF(30, 50)); + recognizer.updateSwipeGesture(QPointF(-30, 50)); + recognizer.endSwipeGesture(); + QCOMPARE(triggeredSpy.count(), 1); + } + + void testGestureRecognizerAxisLockRejectsOrthogonal() + { + GestureRecognizer recognizer; + SwipeGesture horizontalGesture; + horizontalGesture.setDirection(SwipeGesture::Right); + horizontalGesture.setMinimumDelta(QPointF(100, 0)); + SwipeGesture verticalGesture; + verticalGesture.setDirection(SwipeGesture::Down); + verticalGesture.setMinimumDelta(QPointF(0, 100)); + QSignalSpy hTriggeredSpy(&horizontalGesture, &SwipeGesture::triggered); + QSignalSpy vTriggeredSpy(&verticalGesture, &SwipeGesture::triggered); + recognizer.registerSwipeGesture(&horizontalGesture); + recognizer.registerSwipeGesture(&verticalGesture); + recognizer.startSwipeGesture(1); + recognizer.updateSwipeGesture(QPointF(100, 0)); + recognizer.endSwipeGesture(); + QCOMPARE(hTriggeredSpy.count(), 1); + QCOMPARE(vTriggeredSpy.count(), 0); + } +}; + +QTEST_MAIN(GesturesTest) +#include "main.moc" diff --git a/tests/test_greeterproxy/CMakeLists.txt b/tests/test_greeterproxy/CMakeLists.txt new file mode 100644 index 000000000..791c4e674 --- /dev/null +++ b/tests/test_greeterproxy/CMakeLists.txt @@ -0,0 +1,9 @@ +find_package(Qt6 REQUIRED COMPONENTS Test) + +add_executable(test_greeterproxy main.cpp) +target_link_libraries(test_greeterproxy PRIVATE libtreeland Qt6::Test) +add_test(NAME test_greeterproxy COMMAND test_greeterproxy) +set_tests_properties(test_greeterproxy PROPERTIES + ENVIRONMENT "QT_QPA_PLATFORM=offscreen" + TIMEOUT 10 +) diff --git a/tests/test_greeterproxy/main.cpp b/tests/test_greeterproxy/main.cpp new file mode 100644 index 000000000..e2829cf16 --- /dev/null +++ b/tests/test_greeterproxy/main.cpp @@ -0,0 +1,81 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include +#include + +#include "greeter/greeterproxy.h" + +class TestGreeterProxy : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void testProperties(); + void testSetShowShutdownView(); + void testIsConnected(); + void testLock(); + void testSwitchUser(); +}; + +void TestGreeterProxy::testProperties() +{ + GreeterProxy proxy(true); + + QCOMPARE(proxy.hostName(), QString()); + QCOMPARE(proxy.canPowerOff(), false); + QCOMPARE(proxy.canReboot(), false); + QCOMPARE(proxy.canSuspend(), false); + QCOMPARE(proxy.canHibernate(), false); + QCOMPARE(proxy.canHybridSleep(), false); + QCOMPARE(proxy.isLocked(), false); + QCOMPARE(proxy.failedAttempts(), 0); + QCOMPARE(proxy.showShutdownView(), false); + QCOMPARE(proxy.showAnimation(), true); + QCOMPARE(proxy.hasActiveSession(), false); +} + +void TestGreeterProxy::testSetShowShutdownView() +{ + GreeterProxy proxy(true); + QSignalSpy spy(&proxy, &GreeterProxy::showShutdownViewChanged); + + proxy.setShowShutdownView(true); + QVERIFY(proxy.showShutdownView()); + QCOMPARE(spy.count(), 1); + + proxy.setShowShutdownView(true); + QCOMPARE(spy.count(), 1); + + proxy.setShowShutdownView(false); + QVERIFY(!proxy.showShutdownView()); + QCOMPARE(spy.count(), 2); +} + +void TestGreeterProxy::testIsConnected() +{ + GreeterProxy proxy(true); + QVERIFY(!proxy.isConnected()); +} + +void TestGreeterProxy::testLock() +{ + GreeterProxy proxy(true); + QSignalSpy spy(&proxy, &GreeterProxy::lockChanged); + + proxy.lock(); + QVERIFY(proxy.isLocked()); + QCOMPARE(spy.count(), 1); +} + +void TestGreeterProxy::testSwitchUser() +{ + GreeterProxy proxy(true); + QSignalSpy spy(&proxy, &GreeterProxy::switchUser); + + proxy.switchUser(); + QCOMPARE(spy.count(), 1); +} + +QTEST_MAIN(TestGreeterProxy) +#include "main.moc" diff --git a/tests/test_lockscreen/CMakeLists.txt b/tests/test_lockscreen/CMakeLists.txt new file mode 100644 index 000000000..3a3635b12 --- /dev/null +++ b/tests/test_lockscreen/CMakeLists.txt @@ -0,0 +1,9 @@ +find_package(Qt6 REQUIRED COMPONENTS Test Quick) + +add_executable(test_lockscreen main.cpp) +target_link_libraries(test_lockscreen PRIVATE libtreeland Qt6::Test Qt6::Quick) +add_test(NAME test_lockscreen COMMAND test_lockscreen) +set_tests_properties(test_lockscreen PROPERTIES + ENVIRONMENT "QT_QPA_PLATFORM=offscreen" + TIMEOUT 30 +) diff --git a/tests/test_lockscreen/main.cpp b/tests/test_lockscreen/main.cpp new file mode 100644 index 000000000..861e6e62b --- /dev/null +++ b/tests/test_lockscreen/main.cpp @@ -0,0 +1,208 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include +#include +#include +#include + +#include "core/lockscreen.h" +#include "interfaces/lockscreeninterface.h" +#include "greeter/greeterproxy.h" + +class MockLockScreen : public QObject, public ILockScreen +{ + Q_OBJECT + Q_INTERFACES(ILockScreen) + +public: + mutable int createCallCount = 0; + mutable Output *lastOutput = nullptr; + mutable QQuickItem *lastParent = nullptr; + + QQuickItem *createLockScreen(Output *output, QQuickItem *parent) override + { + ++createCallCount; + lastOutput = output; + lastParent = parent; + auto *item = new QQuickItem(parent); + item->setObjectName("mockLockScreenItem"); + return item; + } +}; + +class MockGreeterProxy : public GreeterProxy +{ + Q_OBJECT + +public: + bool m_isLocked = false; + bool m_showShutdownView = false; + int lockCallCount = 0; + int switchUserCallCount = 0; + + explicit MockGreeterProxy(QObject *parent = nullptr) + : GreeterProxy(true, parent) + { + } + + bool isLocked() const override + { + return m_isLocked; + } + + bool showShutdownView() const override + { + return m_showShutdownView; + } + + void setShowShutdownView(bool show) override + { + if (m_showShutdownView != show) { + m_showShutdownView = show; + Q_EMIT showShutdownViewChanged(show); + } + } + + void lock() override + { + ++lockCallCount; + m_isLocked = true; + Q_EMIT lockChanged(true); + } + + void switchUser() override + { + ++switchUserCallCount; + GreeterProxy::switchUser(); + } +}; + +class TestLockScreen : public QObject +{ + Q_OBJECT + +private: + QQuickWindow *m_window = nullptr; + MockLockScreen *m_mockImpl = nullptr; + MockGreeterProxy *m_mockGreeter = nullptr; + LockScreen *m_lockScreen = nullptr; + +private Q_SLOTS: + void initTestCase(); + void cleanupTestCase(); + void testAvailable(); + void testLock(); + void testLockWhenAlreadyLocked(); + void testShutdown(); + void testShutdownWhenAlreadyVisible(); + void testSwitchUser(); + void testSwitchUserWhenAlreadyVisible(); + void testIsLocked(); + void testSetPrimaryOutputName(); +}; + +void TestLockScreen::initTestCase() +{ + m_window = new QQuickWindow; + m_mockImpl = new MockLockScreen(); + m_mockGreeter = new MockGreeterProxy(); + m_lockScreen = new LockScreen(m_mockImpl, nullptr, m_mockGreeter); + m_lockScreen->setParentItem(m_window->contentItem()); +} + +void TestLockScreen::cleanupTestCase() +{ + delete m_lockScreen; + delete m_mockGreeter; + delete m_mockImpl; + delete m_window; +} + +void TestLockScreen::testAvailable() +{ + QVERIFY(m_lockScreen->available()); +} + +void TestLockScreen::testLock() +{ + m_mockGreeter->m_isLocked = false; + m_lockScreen->setVisible(false); + + QSignalSpy unlockSpy(m_lockScreen, &LockScreen::unlock); + + m_lockScreen->lock(); + + QVERIFY(m_lockScreen->isVisible()); + QCOMPARE(m_mockGreeter->lockCallCount, 1); +} + +void TestLockScreen::testLockWhenAlreadyLocked() +{ + m_lockScreen->setVisible(true); + int prevLockCount = m_mockGreeter->lockCallCount; + + m_lockScreen->lock(); + + QCOMPARE(m_mockGreeter->lockCallCount, prevLockCount); +} + +void TestLockScreen::testShutdown() +{ + m_lockScreen->setVisible(false); + m_mockGreeter->m_showShutdownView = false; + + m_lockScreen->shutdown(); + + QVERIFY(m_lockScreen->isVisible()); + QVERIFY(m_mockGreeter->m_showShutdownView); +} + +void TestLockScreen::testShutdownWhenAlreadyVisible() +{ + m_lockScreen->setVisible(true); + bool prevShow = m_mockGreeter->m_showShutdownView; + + m_lockScreen->shutdown(); + + QCOMPARE(m_mockGreeter->m_showShutdownView, prevShow); +} + +void TestLockScreen::testSwitchUser() +{ + m_lockScreen->setVisible(false); + int prevCount = m_mockGreeter->switchUserCallCount; + + m_lockScreen->switchUser(); + + QVERIFY(m_lockScreen->isVisible()); + QCOMPARE(m_mockGreeter->switchUserCallCount, prevCount + 1); +} + +void TestLockScreen::testSwitchUserWhenAlreadyVisible() +{ + m_lockScreen->setVisible(true); + int prevCount = m_mockGreeter->switchUserCallCount; + + m_lockScreen->switchUser(); + + QCOMPARE(m_mockGreeter->switchUserCallCount, prevCount); +} + +void TestLockScreen::testIsLocked() +{ + m_lockScreen->setVisible(false); + QVERIFY(!m_lockScreen->isLocked()); + + m_lockScreen->setVisible(true); + QVERIFY(m_lockScreen->isLocked()); +} + +void TestLockScreen::testSetPrimaryOutputName() +{ + m_lockScreen->setPrimaryOutputName("HDMI-1"); + QCOMPARE(m_lockScreen->primaryOutputName(), QString("HDMI-1")); +} + +QTEST_MAIN(TestLockScreen) +#include "main.moc" diff --git a/tests/test_output_scale/CMakeLists.txt b/tests/test_output_scale/CMakeLists.txt new file mode 100644 index 000000000..4708f21b4 --- /dev/null +++ b/tests/test_output_scale/CMakeLists.txt @@ -0,0 +1,9 @@ +find_package(Qt6 REQUIRED COMPONENTS Test) + +add_executable(test_output_scale main.cpp) +target_link_libraries(test_output_scale PRIVATE libtreeland Qt6::Test) +add_test(NAME test_output_scale COMMAND test_output_scale) +set_tests_properties(test_output_scale PROPERTIES + ENVIRONMENT "QT_QPA_PLATFORM=offscreen" + TIMEOUT 10 +) diff --git a/tests/test_output_scale/main.cpp b/tests/test_output_scale/main.cpp new file mode 100644 index 000000000..7b6b92bb5 --- /dev/null +++ b/tests/test_output_scale/main.cpp @@ -0,0 +1,168 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include +#include +#include +#include + +#include "output/output.h" + +class TestOutputScale : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void testCalcPreferredScale_StandardDisplay(); + void testCalcPreferredScale_HighDPI(); + void testCalcPreferredScale_ZeroDimensions(); + void testCalcPreferredScale_NegativeDimensions(); + void testCalcPreferredScale_4KDisplay(); + void testCalcPreferredScale_SmallDisplay(); + + void testConstrainToValidArea_Centered(); + void testConstrainToValidArea_TopLeft(); + void testConstrainToValidArea_BottomRight(); + void testConstrainToValidArea_ExceedLeft(); + void testConstrainToValidArea_ExceedRight(); + void testConstrainToValidArea_ExceedTop(); + void testConstrainToValidArea_ExceedBottom(); + void testConstrainToValidArea_ExactFit(); + void testConstrainToValidArea_LargerThanValid(); +}; + +void TestOutputScale::testCalcPreferredScale_StandardDisplay() +{ + double scale = Output::calcPreferredScale(1920, 1080, 477, 268); + QCOMPARE(scale, 1.0); +} + +void TestOutputScale::testCalcPreferredScale_HighDPI() +{ + double scale = Output::calcPreferredScale(2560, 1440, 310, 174); + QVERIFY(scale >= 1.25); + QVERIFY(scale <= 2.0); +} + +void TestOutputScale::testCalcPreferredScale_ZeroDimensions() +{ + QCOMPARE(Output::calcPreferredScale(0, 1080, 477, 268), 1.0); + QCOMPARE(Output::calcPreferredScale(1920, 0, 477, 268), 1.0); + QCOMPARE(Output::calcPreferredScale(1920, 1080, 0, 268), 1.0); + QCOMPARE(Output::calcPreferredScale(1920, 1080, 477, 0), 1.0); +} + +void TestOutputScale::testCalcPreferredScale_NegativeDimensions() +{ + QCOMPARE(Output::calcPreferredScale(-1920, 1080, 477, 268), 1.0); + QCOMPARE(Output::calcPreferredScale(1920, 1080, -477, 268), 1.0); +} + +void TestOutputScale::testCalcPreferredScale_4KDisplay() +{ + double scale = Output::calcPreferredScale(3840, 2160, 600, 340); + QVERIFY(scale >= 1.5); + QVERIFY(scale <= 2.5); +} + +void TestOutputScale::testCalcPreferredScale_SmallDisplay() +{ + double scale = Output::calcPreferredScale(1366, 768, 344, 194); + QVERIFY(scale >= 0.75); + QVERIFY(scale <= 1.25); +} + +void TestOutputScale::testConstrainToValidArea_Centered() +{ + QPointF pos(100, 100); + QSizeF windowSize(200, 150); + QRectF validGeo(0, 0, 500, 400); + + QPointF result = Output::constrainToValidArea(pos, windowSize, validGeo); + QCOMPARE(result, QPointF(100, 100)); +} + +void TestOutputScale::testConstrainToValidArea_TopLeft() +{ + QPointF pos(0, 0); + QSizeF windowSize(200, 150); + QRectF validGeo(0, 0, 500, 400); + + QPointF result = Output::constrainToValidArea(pos, windowSize, validGeo); + QCOMPARE(result, QPointF(0, 0)); +} + +void TestOutputScale::testConstrainToValidArea_BottomRight() +{ + QPointF pos(300, 250); + QSizeF windowSize(200, 150); + QRectF validGeo(0, 0, 500, 400); + + QPointF result = Output::constrainToValidArea(pos, windowSize, validGeo); + QCOMPARE(result, QPointF(300, 250)); +} + +void TestOutputScale::testConstrainToValidArea_ExceedLeft() +{ + QPointF pos(-50, 100); + QSizeF windowSize(200, 150); + QRectF validGeo(0, 0, 500, 400); + + QPointF result = Output::constrainToValidArea(pos, windowSize, validGeo); + QCOMPARE(result.x(), 0.0); +} + +void TestOutputScale::testConstrainToValidArea_ExceedRight() +{ + QPointF pos(400, 100); + QSizeF windowSize(200, 150); + QRectF validGeo(0, 0, 500, 400); + + QPointF result = Output::constrainToValidArea(pos, windowSize, validGeo); + QCOMPARE(result.x(), 300.0); +} + +void TestOutputScale::testConstrainToValidArea_ExceedTop() +{ + QPointF pos(100, -50); + QSizeF windowSize(200, 150); + QRectF validGeo(0, 0, 500, 400); + + QPointF result = Output::constrainToValidArea(pos, windowSize, validGeo); + QCOMPARE(result.y(), 0.0); +} + +void TestOutputScale::testConstrainToValidArea_ExceedBottom() +{ + QPointF pos(100, 350); + QSizeF windowSize(200, 150); + QRectF validGeo(0, 0, 500, 400); + + QPointF result = Output::constrainToValidArea(pos, windowSize, validGeo); + QCOMPARE(result.y(), 250.0); +} + +void TestOutputScale::testConstrainToValidArea_ExactFit() +{ + QPointF pos(300, 250); + QSizeF windowSize(200, 150); + QRectF validGeo(0, 0, 500, 400); + + QPointF result = Output::constrainToValidArea(pos, windowSize, validGeo); + QCOMPARE(result.x(), 300.0); + QCOMPARE(result.y(), 250.0); +} + +void TestOutputScale::testConstrainToValidArea_LargerThanValid() +{ + QPointF pos(100, 100); + QSizeF windowSize(600, 500); + QRectF validGeo(0, 0, 500, 400); + + QPointF result = Output::constrainToValidArea(pos, windowSize, validGeo); + QVERIFY(result.x() <= 0.0); + QVERIFY(result.y() <= 0.0); +} + +QTEST_MAIN(TestOutputScale) +#include "main.moc" diff --git a/tests/test_outputconfigstate/CMakeLists.txt b/tests/test_outputconfigstate/CMakeLists.txt new file mode 100644 index 000000000..66007ab3f --- /dev/null +++ b/tests/test_outputconfigstate/CMakeLists.txt @@ -0,0 +1,19 @@ +find_package(Qt6 REQUIRED COMPONENTS Test) + +add_executable(test_outputconfigstate main.cpp) + +target_link_libraries(test_outputconfigstate + PRIVATE + libtreeland + Qt::Test +) + +add_test(NAME test_outputconfigstate COMMAND test_outputconfigstate) + +set_property(TEST test_outputconfigstate PROPERTY + ENVIRONMENT "QT_QPA_PLATFORM=offscreen" +) + +set_property(TEST test_outputconfigstate PROPERTY + TIMEOUT 3 +) diff --git a/tests/test_outputconfigstate/main.cpp b/tests/test_outputconfigstate/main.cpp new file mode 100644 index 000000000..a853d025a --- /dev/null +++ b/tests/test_outputconfigstate/main.cpp @@ -0,0 +1,87 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "output/outputconfigstate.h" + +#include +#include + +class OutputConfigStateTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + + void testMarkScreenAsPrimary() + { + OutputConfigState state; + state.markScreenAsPrimary("HDMI-1"); + QVERIFY(state.wasScreenPrimary("HDMI-1")); + } + + void testWasScreenPrimaryFalse() + { + OutputConfigState state; + QVERIFY(!state.wasScreenPrimary("HDMI-1")); + } + + void testClearOutputState() + { + OutputConfigState state; + state.markScreenAsPrimary("HDMI-1"); + state.clearOutputState("HDMI-1"); + QVERIFY(!state.wasScreenPrimary("HDMI-1")); + } + + void testMultipleOutputs() + { + OutputConfigState state; + state.markScreenAsPrimary("HDMI-1"); + state.markScreenAsPrimary("DP-1"); + QVERIFY(state.wasScreenPrimary("HDMI-1")); + QVERIFY(state.wasScreenPrimary("DP-1")); + QVERIFY(!state.wasScreenPrimary("VGA-1")); + } + + void testRecordCopyModeExit() + { + OutputConfigState state; + state.recordCopyModeExit(); + QVERIFY(state.shouldRestoreCopyMode()); + } + + void testClearCopyModeIntent() + { + OutputConfigState state; + state.recordCopyModeExit(); + state.clearCopyModeIntent(); + QVERIFY(!state.shouldRestoreCopyMode()); + } + + void testClearOutputStateDoesNotAffectCopyMode() + { + OutputConfigState state; + state.recordCopyModeExit(); + state.clearOutputState("HDMI-1"); + QVERIFY(state.shouldRestoreCopyMode()); + } + + void testClearOutputStateNonExistent() + { + OutputConfigState state; + state.markScreenAsPrimary("HDMI-1"); + state.clearOutputState("DP-1"); + QVERIFY(state.wasScreenPrimary("HDMI-1")); + } + + void testMarkScreenAsPrimaryRepeated() + { + OutputConfigState state; + state.markScreenAsPrimary("HDMI-1"); + state.markScreenAsPrimary("HDMI-1"); + QVERIFY(state.wasScreenPrimary("HDMI-1")); + } +}; + +QTEST_MAIN(OutputConfigStateTest) +#include "main.moc" diff --git a/tests/test_protocol_integration/CMakeLists.txt b/tests/test_protocol_integration/CMakeLists.txt new file mode 100644 index 000000000..dd3e46443 --- /dev/null +++ b/tests/test_protocol_integration/CMakeLists.txt @@ -0,0 +1,63 @@ +find_package(Qt6 REQUIRED COMPONENTS Test) +find_package(TreelandProtocols REQUIRED) +pkg_search_module(WAYLAND_CLIENT REQUIRED IMPORTED_TARGET wayland-client) +pkg_get_variable(WAYLAND_SCANNER wayland-scanner wayland_scanner) + +set(PROTOCOL_DIR ${TREELAND_PROTOCOLS_DATA_DIR}) + +set(GENERATED_SOURCES) +set(GENERATED_HEADERS) + +foreach(PROTO wallpaper-color-v1 window-management-v1 virtual-output-manager-v1 prelaunch-splash-v2) + set(HEADER ${CMAKE_CURRENT_BINARY_DIR}/wayland-treeland-${PROTO}-client-protocol.h) + set(SOURCE ${CMAKE_CURRENT_BINARY_DIR}/wayland-treeland-${PROTO}.c) + + add_custom_command( + OUTPUT ${HEADER} + COMMAND ${WAYLAND_SCANNER} client-header < ${PROTOCOL_DIR}/treeland-${PROTO}.xml > ${HEADER} + DEPENDS ${PROTOCOL_DIR}/treeland-${PROTO}.xml + COMMENT "Generating wayland-treeland-${PROTO} client header" + ) + + add_custom_command( + OUTPUT ${SOURCE} + COMMAND ${WAYLAND_SCANNER} private-code < ${PROTOCOL_DIR}/treeland-${PROTO}.xml > ${SOURCE} + DEPENDS ${PROTOCOL_DIR}/treeland-${PROTO}.xml + COMMENT "Generating wayland-treeland-${PROTO} client code" + ) + + list(APPEND GENERATED_HEADERS ${HEADER}) + list(APPEND GENERATED_SOURCES ${SOURCE}) +endforeach() + +add_executable(test_protocol_integration + main.cpp + ${GENERATED_HEADERS} + ${GENERATED_SOURCES} +) + +target_include_directories(test_protocol_integration + PRIVATE + ${CMAKE_CURRENT_BINARY_DIR} +) + +target_link_libraries(test_protocol_integration + PRIVATE + libtreeland + Qt::Test + PkgConfig::WAYLAND_CLIENT +) + +add_test(NAME test_protocol_integration COMMAND test_protocol_integration) + +set_property(TEST test_protocol_integration PROPERTY + ENVIRONMENT "QT_QPA_PLATFORM=offscreen" +) + +set_property(TEST test_protocol_integration PROPERTY + TIMEOUT 180 +) + +# This test requires a full Wayland compositor environment with proper +# client-server socket handling. Mark it as "manual" so CI can exclude it. +set_property(TEST test_protocol_integration PROPERTY LABELS "manual") diff --git a/tests/test_protocol_integration/main.cpp b/tests/test_protocol_integration/main.cpp new file mode 100644 index 000000000..ea0432004 --- /dev/null +++ b/tests/test_protocol_integration/main.cpp @@ -0,0 +1,521 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "modules/wallpaper-color/wallpapercolorinterfacev1.h" +#include "modules/window-management/windowmanagementinterfacev1.h" +#include "modules/virtual-output/virtualoutputmanagerinterfacev1.h" +#include "modules/prelaunch-splash/prelaunchsplash.h" + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +#include "wayland-treeland-wallpaper-color-v1-client-protocol.h" +#include "wayland-treeland-window-management-v1-client-protocol.h" +#include "wayland-treeland-virtual-output-manager-v1-client-protocol.h" +#include "wayland-treeland-prelaunch-splash-v2-client-protocol.h" + +WAYLIB_SERVER_USE_NAMESPACE + +static const int CLIENT_TIMEOUT_MS = 5000; + +// Non-blocking Wayland dispatch with poll timeout +static bool dispatchWaylandEvents(wl_display *display, int timeoutMs = 100) +{ + wl_display_flush(display); + struct pollfd pfd; + pfd.fd = wl_display_get_fd(display); + pfd.events = POLLIN; + pfd.revents = 0; + int ret = poll(&pfd, 1, timeoutMs); + if (ret > 0 && (pfd.revents & POLLIN)) { + wl_display_dispatch(display); + return true; + } + wl_display_dispatch_pending(display); + return false; +} + +class ProtocolIntegrationTest : public QObject +{ + Q_OBJECT + + WServer *m_server = nullptr; + WSocket *m_socket = nullptr; + WallpaperColorInterfaceV1 *m_wallpaperColor = nullptr; + WindowManagementInterfaceV1 *m_windowManagement = nullptr; + VirtualOutputManagerInterfaceV1 *m_virtualOutput = nullptr; + PrelaunchSplash *m_prelaunchSplash = nullptr; + + QTemporaryDir m_tmpDir; + int m_sv[2] = {-1, -1}; + + bool setupServer() + { + m_server = new WServer(); + + m_wallpaperColor = m_server->attach(m_server); + m_windowManagement = m_server->attach(m_server); + m_virtualOutput = m_server->attach(m_server); + m_prelaunchSplash = m_server->attach(); + + if (!m_wallpaperColor || !m_windowManagement || !m_virtualOutput || !m_prelaunchSplash) { + return false; + } + + m_tmpDir.setAutoRemove(true); + QString socketPath = m_tmpDir.path() + "/wayland-test-0"; + m_socket = new WSocket(true, m_server); + if (!m_socket->create(socketPath)) { + return false; + } + m_server->addSocket(m_socket); + m_server->start(); + + return true; + } + + void cleanupServer() + { + if (m_sv[1] >= 0) { + close(m_sv[1]); + m_sv[1] = -1; + } + if (m_sv[0] >= 0) { + close(m_sv[0]); + m_sv[0] = -1; + } + delete m_server; + m_server = nullptr; + m_socket = nullptr; + m_wallpaperColor = nullptr; + m_windowManagement = nullptr; + m_virtualOutput = nullptr; + m_prelaunchSplash = nullptr; + } + + wl_display *connectClient() + { + // Create a fresh socketpair for each client connection + // wl_display_disconnect closes the fd, so we cannot reuse m_sv[1] + if (m_sv[0] >= 0) { + close(m_sv[0]); + m_sv[0] = -1; + } + if (m_sv[1] >= 0) { + close(m_sv[1]); + m_sv[1] = -1; + } + if (socketpair(AF_UNIX, SOCK_STREAM, 0, m_sv) != 0) { + return nullptr; + } + m_socket->addClient(m_sv[0]); + return wl_display_connect_to_fd(m_sv[1]); + } + + static void syncCallback(void *data, wl_callback *callback, uint32_t) + { + *static_cast(data) = true; + wl_callback_destroy(callback); + } + + static const wl_callback_listener syncListener; + + bool waitForSync(wl_display *display, int timeoutMs = CLIENT_TIMEOUT_MS) + { + bool done = false; + wl_callback *callback = wl_display_sync(display); + wl_callback_add_listener(callback, &syncListener, &done); + QElapsedTimer timer; + timer.start(); + while (!done && timer.elapsed() < timeoutMs) { + dispatchWaylandEvents(display, 100); + QCoreApplication::processEvents(); + } + return done; + } + +private Q_SLOTS: + + void initTestCase() + { + if (!qEnvironmentVariableIsEmpty("CI")) { + QSKIP("test_protocol_integration requires a full Wayland compositor " + "environment and does not work in headless CI. Run locally with " + "ctest -R test_protocol_integration."); + } + QVERIFY(setupServer()); + QCoreApplication::processEvents(); + } + + void cleanupTestCase() + { + cleanupServer(); + } + + void testWallpaperColorWatch() + { + m_wallpaperColor->updateWallpaperColor("HDMI-1", true); + + wl_display *display = connectClient(); + QVERIFY(display != nullptr); + + struct Context { + wl_registry *registry = nullptr; + bool gotOutputColor = false; + QString outputName; + uint32_t isDark = 0; + treeland_wallpaper_color_manager_v1 *manager = nullptr; + } ctx; + + static const wl_registry_listener registryListener = { + [](void *data, wl_registry *registry, uint32_t name, const char *iface, uint32_t) { + auto *c = static_cast(data); + if (strcmp(iface, "treeland_wallpaper_color_manager_v1") == 0) { + c->manager = static_cast( + wl_registry_bind(registry, name, &treeland_wallpaper_color_manager_v1_interface, 1)); + } + }, + [](void *, wl_registry *, uint32_t) {} + }; + auto registry = wl_display_get_registry(display); + ctx.registry = registry; + wl_registry_add_listener(registry, ®istryListener, &ctx); + QVERIFY(waitForSync(display)); + QVERIFY(ctx.manager != nullptr); + + static const treeland_wallpaper_color_manager_v1_listener listener = { + [](void *data, treeland_wallpaper_color_manager_v1 *, const char *output, uint32_t isdark) { + auto *c = static_cast(data); + c->gotOutputColor = true; + c->outputName = QString::fromUtf8(output); + c->isDark = isdark; + } + }; + treeland_wallpaper_color_manager_v1_add_listener(ctx.manager, &listener, &ctx); + treeland_wallpaper_color_manager_v1_watch(ctx.manager, "HDMI-1"); + wl_display_flush(display); + QCoreApplication::processEvents(); + + QElapsedTimer timer; + timer.start(); + while (!ctx.gotOutputColor && timer.elapsed() < CLIENT_TIMEOUT_MS) { + dispatchWaylandEvents(display, 100); + QCoreApplication::processEvents(); + } + + QVERIFY(ctx.gotOutputColor); + QCOMPARE(ctx.outputName, QString("HDMI-1")); + QCOMPARE(ctx.isDark, 1u); + + treeland_wallpaper_color_manager_v1_destroy(ctx.manager); + wl_registry_destroy(registry); + wl_display_disconnect(display); + } + + void testWallpaperColorUpdate() + { + m_wallpaperColor->updateWallpaperColor("DP-1", false); + + wl_display *display = connectClient(); + QVERIFY(display != nullptr); + + struct Context { + int eventCount = 0; + uint32_t lastIsDark = 0; + treeland_wallpaper_color_manager_v1 *manager = nullptr; + } ctx; + + static const wl_registry_listener registryListener = { + [](void *data, wl_registry *registry, uint32_t name, const char *iface, uint32_t) { + auto *c = static_cast(data); + if (strcmp(iface, "treeland_wallpaper_color_manager_v1") == 0) { + c->manager = static_cast( + wl_registry_bind(registry, name, &treeland_wallpaper_color_manager_v1_interface, 1)); + } + }, + [](void *, wl_registry *, uint32_t) {} + }; + auto registry = wl_display_get_registry(display); + wl_registry_add_listener(registry, ®istryListener, &ctx); + QVERIFY(waitForSync(display)); + QVERIFY(ctx.manager != nullptr); + + static const treeland_wallpaper_color_manager_v1_listener listener = { + [](void *data, treeland_wallpaper_color_manager_v1 *, const char *, uint32_t isdark) { + auto *c = static_cast(data); + c->eventCount++; + c->lastIsDark = isdark; + } + }; + treeland_wallpaper_color_manager_v1_add_listener(ctx.manager, &listener, &ctx); + treeland_wallpaper_color_manager_v1_watch(ctx.manager, "DP-1"); + wl_display_flush(display); + QCoreApplication::processEvents(); + + QElapsedTimer timer; + timer.start(); + while (ctx.eventCount < 1 && timer.elapsed() < CLIENT_TIMEOUT_MS) { + dispatchWaylandEvents(display, 100); + QCoreApplication::processEvents(); + } + QCOMPARE(ctx.eventCount, 1); + QCOMPARE(ctx.lastIsDark, 0u); + + m_wallpaperColor->updateWallpaperColor("DP-1", true); + wl_display_flush(display); + QCoreApplication::processEvents(); + + timer.restart(); + while (ctx.eventCount < 2 && timer.elapsed() < CLIENT_TIMEOUT_MS) { + dispatchWaylandEvents(display, 100); + QCoreApplication::processEvents(); + } + QCOMPARE(ctx.eventCount, 2); + QCOMPARE(ctx.lastIsDark, 1u); + + treeland_wallpaper_color_manager_v1_destroy(ctx.manager); + wl_registry_destroy(registry); + wl_display_disconnect(display); + } + + void testWindowManagementSetDesktop() + { + wl_display *display = connectClient(); + QVERIFY(display != nullptr); + + struct Context { + bool gotShowDesktop = false; + uint32_t state = 0; + treeland_window_management_v1 *manager = nullptr; + } ctx; + + static const wl_registry_listener registryListener = { + [](void *data, wl_registry *registry, uint32_t name, const char *iface, uint32_t) { + auto *c = static_cast(data); + if (strcmp(iface, "treeland_window_management_v1") == 0) { + c->manager = static_cast( + wl_registry_bind(registry, name, &treeland_window_management_v1_interface, 1)); + } + }, + [](void *, wl_registry *, uint32_t) {} + }; + auto registry = wl_display_get_registry(display); + wl_registry_add_listener(registry, ®istryListener, &ctx); + QVERIFY(waitForSync(display)); + QVERIFY(ctx.manager != nullptr); + + static const treeland_window_management_v1_listener listener = { + [](void *data, treeland_window_management_v1 *, uint32_t state) { + auto *c = static_cast(data); + c->gotShowDesktop = true; + c->state = state; + } + }; + treeland_window_management_v1_add_listener(ctx.manager, &listener, &ctx); + wl_display_flush(display); + QCoreApplication::processEvents(); + + QElapsedTimer timer; + timer.start(); + while (!ctx.gotShowDesktop && timer.elapsed() < CLIENT_TIMEOUT_MS) { + dispatchWaylandEvents(display, 100); + QCoreApplication::processEvents(); + } + QVERIFY(ctx.gotShowDesktop); + QCOMPARE(ctx.state, 0u); + + treeland_window_management_v1_set_desktop(ctx.manager, 1); + wl_display_flush(display); + QCoreApplication::processEvents(); + + timer.restart(); + while (ctx.state != 1u && timer.elapsed() < CLIENT_TIMEOUT_MS) { + dispatchWaylandEvents(display, 100); + QCoreApplication::processEvents(); + } + + treeland_window_management_v1_destroy(ctx.manager); + wl_registry_destroy(registry); + wl_display_disconnect(display); + } + + void testWindowManagementServerPush() + { + wl_display *display = connectClient(); + QVERIFY(display != nullptr); + + struct Context { + uint32_t state = 99; + treeland_window_management_v1 *manager = nullptr; + } ctx; + + static const wl_registry_listener registryListener = { + [](void *data, wl_registry *registry, uint32_t name, const char *iface, uint32_t) { + auto *c = static_cast(data); + if (strcmp(iface, "treeland_window_management_v1") == 0) { + c->manager = static_cast( + wl_registry_bind(registry, name, &treeland_window_management_v1_interface, 1)); + } + }, + [](void *, wl_registry *, uint32_t) {} + }; + auto registry = wl_display_get_registry(display); + wl_registry_add_listener(registry, ®istryListener, &ctx); + QVERIFY(waitForSync(display)); + QVERIFY(ctx.manager != nullptr); + + static const treeland_window_management_v1_listener listener = { + [](void *data, treeland_window_management_v1 *, uint32_t state) { + static_cast(data)->state = state; + } + }; + treeland_window_management_v1_add_listener(ctx.manager, &listener, &ctx); + wl_display_flush(display); + QCoreApplication::processEvents(); + + QElapsedTimer timer; + timer.start(); + while (ctx.state == 99 && timer.elapsed() < CLIENT_TIMEOUT_MS) { + dispatchWaylandEvents(display, 100); + QCoreApplication::processEvents(); + } + QCOMPARE(ctx.state, 0u); + + m_windowManagement->setDesktopState(WindowManagementInterfaceV1::DesktopState::Show); + wl_display_flush(display); + QCoreApplication::processEvents(); + + timer.restart(); + while (ctx.state != 1u && timer.elapsed() < CLIENT_TIMEOUT_MS) { + dispatchWaylandEvents(display, 100); + QCoreApplication::processEvents(); + } + QCOMPARE(ctx.state, 1u); + + treeland_window_management_v1_destroy(ctx.manager); + wl_registry_destroy(registry); + wl_display_disconnect(display); + } + + void testVirtualOutputCreateAndList() + { + wl_display *display = connectClient(); + QVERIFY(display != nullptr); + + struct Context { + bool gotList = false; + treeland_virtual_output_manager_v1 *manager = nullptr; + QByteArray listData; + } ctx; + + static const wl_registry_listener registryListener = { + [](void *data, wl_registry *registry, uint32_t name, const char *iface, uint32_t) { + auto *c = static_cast(data); + if (strcmp(iface, "treeland_virtual_output_manager_v1") == 0) { + c->manager = static_cast( + wl_registry_bind(registry, name, &treeland_virtual_output_manager_v1_interface, 1)); + } + }, + [](void *, wl_registry *, uint32_t) {} + }; + auto registry = wl_display_get_registry(display); + wl_registry_add_listener(registry, ®istryListener, &ctx); + QVERIFY(waitForSync(display)); + QVERIFY(ctx.manager != nullptr); + + static const treeland_virtual_output_manager_v1_listener managerListener = { + [](void *data, treeland_virtual_output_manager_v1 *, wl_array *names) { + auto *c = static_cast(data); + c->gotList = true; + c->listData = QByteArray(static_cast(names->data), names->size); + } + }; + treeland_virtual_output_manager_v1_add_listener(ctx.manager, &managerListener, &ctx); + + treeland_virtual_output_manager_v1_get_virtual_output_list(ctx.manager); + wl_display_flush(display); + QCoreApplication::processEvents(); + + QElapsedTimer timer; + timer.start(); + while (!ctx.gotList && timer.elapsed() < CLIENT_TIMEOUT_MS) { + dispatchWaylandEvents(display, 100); + QCoreApplication::processEvents(); + } + QVERIFY(ctx.gotList); + + treeland_virtual_output_manager_v1_destroy(ctx.manager); + wl_registry_destroy(registry); + wl_display_disconnect(display); + } + + void testPrelaunchSplashCreate() + { + wl_display *display = connectClient(); + QVERIFY(display != nullptr); + + QSignalSpy spy(m_prelaunchSplash, &PrelaunchSplash::splashRequested); + + struct Context { + treeland_prelaunch_splash_manager_v2 *manager = nullptr; + } ctx; + + static const wl_registry_listener registryListener = { + [](void *data, wl_registry *registry, uint32_t name, const char *iface, uint32_t) { + auto *c = static_cast(data); + if (strcmp(iface, "treeland_prelaunch_splash_manager_v2") == 0) { + c->manager = static_cast( + wl_registry_bind(registry, name, &treeland_prelaunch_splash_manager_v2_interface, 1)); + } + }, + [](void *, wl_registry *, uint32_t) {} + }; + auto registry = wl_display_get_registry(display); + wl_registry_add_listener(registry, ®istryListener, &ctx); + QVERIFY(waitForSync(display)); + QVERIFY(ctx.manager != nullptr); + + treeland_prelaunch_splash_manager_v2_create_splash(ctx.manager, "com.test.app", "instance1", "flatpak", nullptr); + wl_display_flush(display); + QCoreApplication::processEvents(); + + QElapsedTimer timer; + while (spy.count() < 1 && timer.elapsed() < CLIENT_TIMEOUT_MS) { + dispatchWaylandEvents(display, 100); + QCoreApplication::processEvents(); + } + + QCOMPARE(spy.count(), 1); + QCOMPARE(spy.at(0).at(0).toString(), QString("com.test.app")); + QCOMPARE(spy.at(0).at(1).toString(), QString("instance1")); + + treeland_prelaunch_splash_manager_v2_destroy(ctx.manager); + wl_registry_destroy(registry); + wl_display_disconnect(display); + } +}; + +const wl_callback_listener ProtocolIntegrationTest::syncListener = { + ProtocolIntegrationTest::syncCallback +}; + +QTEST_MAIN(ProtocolIntegrationTest) +#include "main.moc" diff --git a/tests/test_shortcutcontroller/CMakeLists.txt b/tests/test_shortcutcontroller/CMakeLists.txt new file mode 100644 index 000000000..46f438054 --- /dev/null +++ b/tests/test_shortcutcontroller/CMakeLists.txt @@ -0,0 +1,19 @@ +find_package(Qt6 REQUIRED COMPONENTS Test) + +add_executable(test_shortcutcontroller main.cpp) + +target_link_libraries(test_shortcutcontroller + PRIVATE + libtreeland + Qt::Test +) + +add_test(NAME test_shortcutcontroller COMMAND test_shortcutcontroller) + +set_property(TEST test_shortcutcontroller PROPERTY + ENVIRONMENT "QT_QPA_PLATFORM=offscreen" +) + +set_property(TEST test_shortcutcontroller PROPERTY + TIMEOUT 10 +) diff --git a/tests/test_shortcutcontroller/main.cpp b/tests/test_shortcutcontroller/main.cpp new file mode 100644 index 000000000..09e27b85d --- /dev/null +++ b/tests/test_shortcutcontroller/main.cpp @@ -0,0 +1,324 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "modules/shortcut/shortcutcontroller.h" +#include "modules/shortcut/shortcutmanager.h" + +#include +#include +#include +#include + +Q_DECLARE_METATYPE(ShortcutAction) + +class ShortcutControllerTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + + void testNormalizeCtrlKey() + { + auto result = ShortcutController::normalizeKeyCombination( + QKeyCombination(Qt::NoModifier, Qt::Key_Control)); + QCOMPARE(result.keyboardModifiers(), Qt::ControlModifier); + QCOMPARE(result.key(), Qt::Key_unknown); + } + + void testNormalizeShiftKey() + { + auto result = ShortcutController::normalizeKeyCombination( + QKeyCombination(Qt::NoModifier, Qt::Key_Shift)); + QCOMPARE(result.keyboardModifiers(), Qt::ShiftModifier); + QCOMPARE(result.key(), Qt::Key_unknown); + } + + void testNormalizeAltKey() + { + auto result = ShortcutController::normalizeKeyCombination( + QKeyCombination(Qt::NoModifier, Qt::Key_Alt)); + QCOMPARE(result.keyboardModifiers(), Qt::AltModifier); + QCOMPARE(result.key(), Qt::Key_unknown); + } + + void testNormalizeMetaKey() + { + auto result = ShortcutController::normalizeKeyCombination( + QKeyCombination(Qt::NoModifier, Qt::Key_Meta)); + QCOMPARE(result.keyboardModifiers(), Qt::MetaModifier); + QCOMPARE(result.key(), Qt::Key_unknown); + } + + void testNormalizeSuperLKey() + { + auto result = ShortcutController::normalizeKeyCombination( + QKeyCombination(Qt::NoModifier, Qt::Key_Super_L)); + QCOMPARE(result.keyboardModifiers(), Qt::MetaModifier); + QCOMPARE(result.key(), Qt::Key_unknown); + } + + void testNormalizeSuperRKey() + { + auto result = ShortcutController::normalizeKeyCombination( + QKeyCombination(Qt::NoModifier, Qt::Key_Super_R)); + QCOMPARE(result.keyboardModifiers(), Qt::MetaModifier); + QCOMPARE(result.key(), Qt::Key_unknown); + } + + void testNormalizeCtrlCombo() + { + auto result = ShortcutController::normalizeKeyCombination( + QKeyCombination(Qt::ControlModifier, Qt::Key_A)); + QCOMPARE(result.keyboardModifiers(), Qt::ControlModifier); + QCOMPARE(result.key(), Qt::Key_A); + } + + void testNormalizePlainKey() + { + auto result = ShortcutController::normalizeKeyCombination( + QKeyCombination(Qt::NoModifier, Qt::Key_A)); + QCOMPARE(result.keyboardModifiers(), Qt::NoModifier); + QCOMPARE(result.key(), Qt::Key_A); + } + + void testIsValidMetaOnly() + { + QVERIFY(ShortcutController::isValidShortcutCombination( + QKeyCombination(Qt::MetaModifier, Qt::Key_unknown))); + } + + void testIsInvalidCtrlOnly() + { + QVERIFY(!ShortcutController::isValidShortcutCombination( + QKeyCombination(Qt::ControlModifier, Qt::Key_unknown))); + } + + void testIsInvalidAltOnly() + { + QVERIFY(!ShortcutController::isValidShortcutCombination( + QKeyCombination(Qt::AltModifier, Qt::Key_unknown))); + } + + void testIsInvalidShiftOnly() + { + QVERIFY(!ShortcutController::isValidShortcutCombination( + QKeyCombination(Qt::ShiftModifier, Qt::Key_unknown))); + } + + void testIsValidCtrlA() + { + QVERIFY(ShortcutController::isValidShortcutCombination( + QKeyCombination(Qt::ControlModifier, Qt::Key_A))); + } + + void testIsValidAltF4() + { + QVERIFY(ShortcutController::isValidShortcutCombination( + QKeyCombination(Qt::AltModifier, Qt::Key_F4))); + } + + void testIsValidMetaSpace() + { + QVERIFY(ShortcutController::isValidShortcutCombination( + QKeyCombination(Qt::MetaModifier, Qt::Key_Space))); + } + + void testIsValidF1() + { + QVERIFY(ShortcutController::isValidShortcutCombination( + QKeyCombination(Qt::NoModifier, Qt::Key_F1))); + } + + void testIsValidF35() + { + QVERIFY(ShortcutController::isValidShortcutCombination( + QKeyCombination(Qt::NoModifier, Qt::Key_F35))); + } + + void testIsValidDelete() + { + QVERIFY(ShortcutController::isValidShortcutCombination( + QKeyCombination(Qt::NoModifier, Qt::Key_Delete))); + } + + void testIsValidPrint() + { + QVERIFY(ShortcutController::isValidShortcutCombination( + QKeyCombination(Qt::NoModifier, Qt::Key_Print))); + } + + void testIsValidShiftSpace() + { + QVERIFY(ShortcutController::isValidShortcutCombination( + QKeyCombination(Qt::ShiftModifier, Qt::Key_Space))); + } + + void testIsValidShiftEscape() + { + QVERIFY(ShortcutController::isValidShortcutCombination( + QKeyCombination(Qt::ShiftModifier, Qt::Key_Escape))); + } + + void testIsValidShiftTab() + { + QVERIFY(ShortcutController::isValidShortcutCombination( + QKeyCombination(Qt::ShiftModifier, Qt::Key_Tab))); + } + + void testIsInvalidShiftA() + { + QVERIFY(!ShortcutController::isValidShortcutCombination( + QKeyCombination(Qt::ShiftModifier, Qt::Key_A))); + } + + void testIsInvalidPlainA() + { + QVERIFY(!ShortcutController::isValidShortcutCombination( + QKeyCombination(Qt::NoModifier, Qt::Key_A))); + } + + void testIsValidMediaKey() + { + QVERIFY(ShortcutController::isValidShortcutCombination( + QKeyCombination(Qt::NoModifier, Qt::Key_MediaPlay))); + } + + void testIsValidBackspaceNoModifier() + { + QVERIFY(ShortcutController::isValidShortcutCombination( + QKeyCombination(Qt::NoModifier, Qt::Key_Backspace))); + } + + void testIsValidShiftHome() + { + QVERIFY(ShortcutController::isValidShortcutCombination( + QKeyCombination(Qt::ShiftModifier, Qt::Key_Home))); + } + + void testIsValidShiftLeft() + { + QVERIFY(ShortcutController::isValidShortcutCombination( + QKeyCombination(Qt::ShiftModifier, Qt::Key_Left))); + } + + void testIsValidShiftUp() + { + QVERIFY(ShortcutController::isValidShortcutCombination( + QKeyCombination(Qt::ShiftModifier, Qt::Key_Up))); + } + + void testIsValidShiftDown() + { + QVERIFY(ShortcutController::isValidShortcutCombination( + QKeyCombination(Qt::ShiftModifier, Qt::Key_Down))); + } + + void testIsValidShiftRight() + { + QVERIFY(ShortcutController::isValidShortcutCombination( + QKeyCombination(Qt::ShiftModifier, Qt::Key_Right))); + } + + void testIsValidShiftPageUp() + { + QVERIFY(ShortcutController::isValidShortcutCombination( + QKeyCombination(Qt::ShiftModifier, Qt::Key_PageUp))); + } + + void testIsValidShiftPageDown() + { + QVERIFY(ShortcutController::isValidShortcutCombination( + QKeyCombination(Qt::ShiftModifier, Qt::Key_PageDown))); + } + + void testRegisterKeySuccess() + { + ShortcutController ctrl; + uint result = ctrl.registerKey("test", "Ctrl+A", ShortcutController::KeyPress, ShortcutAction::Notify); + QCOMPARE(result, 0u); + } + + void testRegisterKeyNameConflict() + { + ShortcutController ctrl; + ctrl.registerKey("test", "Ctrl+A", ShortcutController::KeyPress, ShortcutAction::Notify); + uint result = ctrl.registerKey("test", "Ctrl+B", ShortcutController::KeyPress, ShortcutAction::Workspace1); + QVERIFY(result != 0); + } + + void testRegisterKeyInvalidKey() + { + ShortcutController ctrl; + uint result = ctrl.registerKey("bad", "A", ShortcutController::KeyPress, ShortcutAction::Notify); + QVERIFY(result != 0); + } + + void testRegisterKeySameKeyDifferentName() + { + ShortcutController ctrl; + uint result1 = ctrl.registerKey("name1", "Ctrl+A", ShortcutController::KeyPress, ShortcutAction::Notify); + QCOMPARE(result1, 0u); + uint result2 = ctrl.registerKey("name2", "Ctrl+A", ShortcutController::KeyPress, ShortcutAction::Workspace1); + QCOMPARE(result2, 0u); + } + + void testUnregisterShortcut() + { + ShortcutController ctrl; + ctrl.registerKey("test", "Ctrl+A", ShortcutController::KeyPress, ShortcutAction::Notify); + ctrl.unregisterShortcut("test"); + QSignalSpy spy(&ctrl, &ShortcutController::actionTriggered); + QKeyEvent event(QEvent::KeyPress, Qt::Key_A, Qt::ControlModifier); + ctrl.dispatchKeyEvent(&event); + QCOMPARE(spy.count(), 0); + } + + void testDispatchKeyEventMatch() + { + ShortcutController ctrl; + ctrl.registerKey("test", "Ctrl+A", ShortcutController::KeyPress, ShortcutAction::Notify); + QSignalSpy spy(&ctrl, &ShortcutController::actionTriggered); + QKeyEvent event(QEvent::KeyPress, Qt::Key_A, Qt::ControlModifier); + ctrl.dispatchKeyEvent(&event); + QCOMPARE(spy.count(), 1); + QCOMPARE(spy.at(0).at(0).value(), ShortcutAction::Notify); + QCOMPARE(spy.at(0).at(1).toString(), QString("test")); + QCOMPARE(spy.at(0).at(2).toBool(), false); + } + + void testDispatchKeyEventNoMatch() + { + ShortcutController ctrl; + ctrl.registerKey("test", "Ctrl+A", ShortcutController::KeyPress, ShortcutAction::Notify); + QSignalSpy spy(&ctrl, &ShortcutController::actionTriggered); + QKeyEvent event(QEvent::KeyPress, Qt::Key_B, Qt::ControlModifier); + ctrl.dispatchKeyEvent(&event); + QCOMPARE(spy.count(), 0); + } + + void testDispatchKeyEventFlagsFilter() + { + ShortcutController ctrl; + ctrl.registerKey("test", "Ctrl+A", ShortcutController::KeyPress, ShortcutAction::Notify); + QSignalSpy spy(&ctrl, &ShortcutController::actionTriggered); + QKeyEvent event(QEvent::KeyRelease, Qt::Key_A, Qt::ControlModifier); + ctrl.dispatchKeyEvent(&event); + QCOMPARE(spy.count(), 0); + } + + void testModifierForAction() + { + ShortcutController ctrl; + ctrl.registerKey("test", "Ctrl+A", ShortcutController::KeyPress, ShortcutAction::Notify); + QCOMPARE(ctrl.modifierForAction(ShortcutAction::Notify), Qt::ControlModifier); + } + + void testModifierForActionNotFound() + { + ShortcutController ctrl; + QCOMPARE(ctrl.modifierForAction(ShortcutAction::Notify), Qt::NoModifier); + } +}; + +QTEST_MAIN(ShortcutControllerTest) +#include "main.moc" diff --git a/tests/test_togglablegesture/CMakeLists.txt b/tests/test_togglablegesture/CMakeLists.txt new file mode 100644 index 000000000..d30be6b37 --- /dev/null +++ b/tests/test_togglablegesture/CMakeLists.txt @@ -0,0 +1,19 @@ +find_package(Qt6 REQUIRED COMPONENTS Test) + +add_executable(test_togglablegesture main.cpp) + +target_link_libraries(test_togglablegesture + PRIVATE + libtreeland + Qt::Test +) + +add_test(NAME test_togglablegesture COMMAND test_togglablegesture) + +set_property(TEST test_togglablegesture PROPERTY + ENVIRONMENT "QT_QPA_PLATFORM=offscreen" +) + +set_property(TEST test_togglablegesture PROPERTY + TIMEOUT 10 +) diff --git a/tests/test_togglablegesture/main.cpp b/tests/test_togglablegesture/main.cpp new file mode 100644 index 000000000..dbedd7462 --- /dev/null +++ b/tests/test_togglablegesture/main.cpp @@ -0,0 +1,204 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "input/togglablegesture.h" + +#include +#include +#include + +class TestableTogglableGesture : public TogglableGesture +{ +public: + using TogglableGesture::setProgress; + using TogglableGesture::setRegress; + using TogglableGesture::activeTriggered; + using TogglableGesture::deactivateTriggered; +}; + +class TogglableGestureTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + + void testInitialState() + { + TestableTogglableGesture gesture; + QCOMPARE(gesture.status(), TogglableGesture::Inactive); + QVERIFY(!gesture.inProgress()); + } + + void testSetProgressFromInactive() + { + TestableTogglableGesture gesture; + QSignalSpy factorSpy(&gesture, &TogglableGesture::partialGestureFactorChanged); + gesture.setProgress(0.3); + QCOMPARE(gesture.status(), TogglableGesture::Activating); + QVERIFY(gesture.inProgress()); + QCOMPARE(factorSpy.count(), 1); + } + + void testSetProgressFromActivating() + { + TestableTogglableGesture gesture; + gesture.setProgress(0.3); + gesture.setProgress(0.6); + QCOMPARE(gesture.status(), TogglableGesture::Activating); + } + + void testSetRegressFromActive() + { + TestableTogglableGesture gesture; + gesture.activate(); + QSignalSpy factorSpy(&gesture, &TogglableGesture::partialGestureFactorChanged); + gesture.setRegress(0.3); + QCOMPARE(gesture.status(), TogglableGesture::Deactivating); + QVERIFY(gesture.inProgress()); + } + + void testActiveTriggeredHighFactor() + { + TestableTogglableGesture gesture; + gesture.setProgress(0.7); + QSignalSpy activatedSpy(&gesture, &TogglableGesture::activated); + gesture.activeTriggered(); + QCOMPARE(gesture.status(), TogglableGesture::Active); + QCOMPARE(activatedSpy.count(), 1); + } + + void testActiveTriggeredLowFactor() + { + TestableTogglableGesture gesture; + gesture.setProgress(0.3); + QSignalSpy deactivatedSpy(&gesture, &TogglableGesture::deactivated); + gesture.activeTriggered(); + QCOMPARE(gesture.status(), TogglableGesture::Inactive); + QCOMPARE(deactivatedSpy.count(), 1); + } + + void testDeactivateTriggeredLowFactor() + { + TestableTogglableGesture gesture; + gesture.activate(); + gesture.setRegress(0.7); + QSignalSpy deactivatedSpy(&gesture, &TogglableGesture::deactivated); + gesture.deactivateTriggered(); + QCOMPARE(gesture.status(), TogglableGesture::Inactive); + QCOMPARE(deactivatedSpy.count(), 1); + } + + void testDeactivateTriggeredHighFactor() + { + TestableTogglableGesture gesture; + gesture.activate(); + gesture.setRegress(0.3); + QSignalSpy activatedSpy(&gesture, &TogglableGesture::activated); + gesture.deactivateTriggered(); + QCOMPARE(gesture.status(), TogglableGesture::Active); + QCOMPARE(activatedSpy.count(), 1); + } + + void testActivate() + { + TestableTogglableGesture gesture; + QSignalSpy statusSpy(&gesture, &TogglableGesture::statusChanged); + gesture.activate(); + QCOMPARE(gesture.status(), TogglableGesture::Active); + QVERIFY(!gesture.inProgress()); + QCOMPARE(gesture.partialGestureFactor(), 1.0); + QCOMPARE(statusSpy.count(), 1); + } + + void testDeactivate() + { + TestableTogglableGesture gesture; + gesture.activate(); + QSignalSpy statusSpy(&gesture, &TogglableGesture::statusChanged); + gesture.deactivate(); + QCOMPARE(gesture.status(), TogglableGesture::Inactive); + QVERIFY(!gesture.inProgress()); + QCOMPARE(gesture.partialGestureFactor(), 0.0); + QCOMPARE(statusSpy.count(), 1); + } + + void testToggleFromInactive() + { + TestableTogglableGesture gesture; + QSignalSpy activatedSpy(&gesture, &TogglableGesture::activated); + gesture.toggle(); + QCOMPARE(gesture.status(), TogglableGesture::Active); + QCOMPARE(activatedSpy.count(), 1); + } + + void testToggleFromActive() + { + TestableTogglableGesture gesture; + gesture.activate(); + QSignalSpy deactivatedSpy(&gesture, &TogglableGesture::deactivated); + gesture.toggle(); + QCOMPARE(gesture.status(), TogglableGesture::Inactive); + QCOMPARE(deactivatedSpy.count(), 1); + } + + void testStop() + { + TestableTogglableGesture gesture; + gesture.activate(); + gesture.stop(); + QCOMPARE(gesture.status(), TogglableGesture::Stopped); + QVERIFY(!gesture.inProgress()); + QCOMPARE(gesture.partialGestureFactor(), 0.0); + } + + void testSetProgressWhileStopped() + { + TestableTogglableGesture gesture; + gesture.activate(); + gesture.stop(); + gesture.setProgress(0.5); + QCOMPARE(gesture.status(), TogglableGesture::Stopped); + } + + void testSetRegressWhileStopped() + { + TestableTogglableGesture gesture; + gesture.activate(); + gesture.stop(); + gesture.setRegress(0.5); + QCOMPARE(gesture.status(), TogglableGesture::Stopped); + } + + void testPartialGestureFactorSignal() + { + TestableTogglableGesture gesture; + QSignalSpy factorSpy(&gesture, &TogglableGesture::partialGestureFactorChanged); + gesture.setPartialGestureFactor(0.5); + QCOMPARE(factorSpy.count(), 1); + QCOMPARE(gesture.partialGestureFactor(), 0.5); + } + + void testActiveTriggeredExactHalfFactor() + { + TestableTogglableGesture gesture; + gesture.setProgress(0.5); + QSignalSpy deactivatedSpy(&gesture, &TogglableGesture::deactivated); + gesture.activeTriggered(); + QCOMPARE(gesture.status(), TogglableGesture::Inactive); + QCOMPARE(deactivatedSpy.count(), 1); + } + + void testDeactivateTriggeredExactHalfFactor() + { + TestableTogglableGesture gesture; + gesture.activate(); + gesture.setRegress(0.5); + QSignalSpy activatedSpy(&gesture, &TogglableGesture::activated); + gesture.deactivateTriggered(); + QCOMPARE(gesture.status(), TogglableGesture::Active); + QCOMPARE(activatedSpy.count(), 1); + } +}; + +QTEST_MAIN(TogglableGestureTest) +#include "main.moc" diff --git a/tests/test_wallpaperconfig/CMakeLists.txt b/tests/test_wallpaperconfig/CMakeLists.txt new file mode 100644 index 000000000..8b8f09aa2 --- /dev/null +++ b/tests/test_wallpaperconfig/CMakeLists.txt @@ -0,0 +1,19 @@ +find_package(Qt6 REQUIRED COMPONENTS Test) + +add_executable(test_wallpaperconfig main.cpp) + +target_link_libraries(test_wallpaperconfig + PRIVATE + libtreeland + Qt::Test +) + +add_test(NAME test_wallpaperconfig COMMAND test_wallpaperconfig) + +set_property(TEST test_wallpaperconfig PROPERTY + ENVIRONMENT "QT_QPA_PLATFORM=offscreen" +) + +set_property(TEST test_wallpaperconfig PROPERTY + TIMEOUT 3 +) diff --git a/tests/test_wallpaperconfig/main.cpp b/tests/test_wallpaperconfig/main.cpp new file mode 100644 index 000000000..d48719e84 --- /dev/null +++ b/tests/test_wallpaperconfig/main.cpp @@ -0,0 +1,130 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "wallpaper/wallpaperconfig.h" + +#include +#include +#include +#include + +class WallpaperConfigTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + + void testWorkspaceConfigImageRoundTrip() + { + WallpaperWorkspaceConfig original; + original.workspaceId = 1; + original.desktopWallpaper = "/usr/share/wallpapers/test.jpg"; + original.desktopWallpapertype = TreelandWallpaperInterfaceV1::Image; + original.enable = true; + + QJsonObject json = original.toJson(); + QVERIFY(json.contains("workspaceIndex")); + QVERIFY(json.contains("desktopWallpaper")); + QVERIFY(json.contains("desktopWallpaperType")); + QVERIFY(json.contains("enable")); + QCOMPARE(json["workspaceIndex"].toInt(), 1); + QCOMPARE(json["desktopWallpaper"].toString(), QString("/usr/share/wallpapers/test.jpg")); + QCOMPARE(json["desktopWallpaperType"].toInt(), 0); + QCOMPARE(json["enable"].toBool(), true); + + WallpaperWorkspaceConfig restored = WallpaperWorkspaceConfig::fromJson(json); + + QCOMPARE(restored.workspaceId, original.workspaceId); + QCOMPARE(restored.desktopWallpaper, original.desktopWallpaper); + QCOMPARE(restored.desktopWallpapertype, original.desktopWallpapertype); + QCOMPARE(restored.enable, original.enable); + } + + void testWorkspaceConfigVideoRoundTrip() + { + WallpaperWorkspaceConfig original; + original.workspaceId = 2; + original.desktopWallpaper = "/usr/share/wallpapers/test.mp4"; + original.desktopWallpapertype = TreelandWallpaperInterfaceV1::Video; + original.enable = false; + + QJsonObject json = original.toJson(); + WallpaperWorkspaceConfig restored = WallpaperWorkspaceConfig::fromJson(json); + + QCOMPARE(restored.workspaceId, original.workspaceId); + QCOMPARE(restored.desktopWallpaper, original.desktopWallpaper); + QCOMPARE(restored.desktopWallpapertype, original.desktopWallpapertype); + QCOMPARE(restored.enable, original.enable); + } + + void testOutputConfigRoundTrip() + { + WallpaperOutputConfig original; + original.outputName = "HDMI-1"; + original.lockscreenWallpaper = "/usr/share/lock/bg.jpg"; + original.lockScreenWallpapertype = TreelandWallpaperInterfaceV1::Image; + original.enable = true; + + WallpaperWorkspaceConfig ws1; + ws1.workspaceId = 1; + ws1.desktopWallpaper = "/usr/share/wallpapers/ws1.jpg"; + ws1.desktopWallpapertype = TreelandWallpaperInterfaceV1::Image; + ws1.enable = true; + + WallpaperWorkspaceConfig ws2; + ws2.workspaceId = 2; + ws2.desktopWallpaper = "/usr/share/wallpapers/ws2.mp4"; + ws2.desktopWallpapertype = TreelandWallpaperInterfaceV1::Video; + ws2.enable = false; + + original.workspaces = {ws1, ws2}; + + QJsonObject json = original.toJson(); + QVERIFY(json.contains("outputName")); + QVERIFY(json.contains("lockScreenWallpaper")); + QVERIFY(json.contains("lockScreenWallpaperType")); + QVERIFY(json.contains("enable")); + QVERIFY(json.contains("workspaces")); + QCOMPARE(json["outputName"].toString(), QString("HDMI-1")); + QCOMPARE(json["lockScreenWallpaper"].toString(), QString("/usr/share/lock/bg.jpg")); + QCOMPARE(json["lockScreenWallpaperType"].toInt(), 0); + QCOMPARE(json["enable"].toBool(), true); + + WallpaperOutputConfig restored = WallpaperOutputConfig::fromJson(json); + + QCOMPARE(restored.outputName, original.outputName); + QCOMPARE(restored.lockscreenWallpaper, original.lockscreenWallpaper); + QCOMPARE(restored.lockScreenWallpapertype, original.lockScreenWallpapertype); + QCOMPARE(restored.enable, original.enable); + QCOMPARE(restored.workspaces.size(), 2); + QCOMPARE(restored.workspaces[0].workspaceId, 1); + QCOMPARE(restored.workspaces[1].desktopWallpapertype, TreelandWallpaperInterfaceV1::Video); + } + + void testContainsWorkspaceFound() + { + WallpaperOutputConfig config; + WallpaperWorkspaceConfig ws; + ws.workspaceId = 3; + config.workspaces = {ws}; + QVERIFY(config.containsWorkspace(3)); + } + + void testContainsWorkspaceNotFound() + { + WallpaperOutputConfig config; + WallpaperWorkspaceConfig ws; + ws.workspaceId = 1; + config.workspaces = {ws}; + QVERIFY(!config.containsWorkspace(99)); + } + + void testContainsWorkspaceEmpty() + { + WallpaperOutputConfig config; + QVERIFY(!config.containsWorkspace(1)); + } +}; + +QTEST_MAIN(WallpaperConfigTest) +#include "main.moc" diff --git a/tests/test_windowconfigstore/CMakeLists.txt b/tests/test_windowconfigstore/CMakeLists.txt new file mode 100644 index 000000000..2934d83ee --- /dev/null +++ b/tests/test_windowconfigstore/CMakeLists.txt @@ -0,0 +1,9 @@ +find_package(Qt6 REQUIRED COMPONENTS Test) + +add_executable(test_windowconfigstore main.cpp) +target_link_libraries(test_windowconfigstore PRIVATE libtreeland Qt6::Test) +add_test(NAME test_windowconfigstore COMMAND test_windowconfigstore) +set_tests_properties(test_windowconfigstore PROPERTIES + ENVIRONMENT "QT_QPA_PLATFORM=offscreen" + TIMEOUT 10 +) diff --git a/tests/test_windowconfigstore/main.cpp b/tests/test_windowconfigstore/main.cpp new file mode 100644 index 000000000..f594a0803 --- /dev/null +++ b/tests/test_windowconfigstore/main.cpp @@ -0,0 +1,72 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include +#include + +#include "core/windowconfigstore.h" + +class TestWindowConfigStore : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void testDefaultConstructor(); + void testDefaultConstructorParent(); + void testSaveLastSizeEmptyAppId(); + void testSaveLastSizeInvalidSize(); + void testSaveLastSizeNormal(); + void testSaveLastSizeZeroSize(); + void testSaveLastSizeNegativeSize(); +}; + +void TestWindowConfigStore::testDefaultConstructor() +{ + WindowConfigStore store; + QCOMPARE(store.parent(), nullptr); +} + +void TestWindowConfigStore::testDefaultConstructorParent() +{ + QObject parentObj; + WindowConfigStore store(&parentObj); + QCOMPARE(store.parent(), &parentObj); +} + +void TestWindowConfigStore::testSaveLastSizeEmptyAppId() +{ + WindowConfigStore store; + // Empty appId should not crash - early return + store.saveLastSize("", QSize(100, 100)); +} + +void TestWindowConfigStore::testSaveLastSizeInvalidSize() +{ + WindowConfigStore store; + // Invalid (null) size should not crash - early return + store.saveLastSize("test-app", QSize()); +} + +void TestWindowConfigStore::testSaveLastSizeNormal() +{ + WindowConfigStore store; + // Normal case should not crash + store.saveLastSize("test-app", QSize(800, 600)); +} + +void TestWindowConfigStore::testSaveLastSizeZeroSize() +{ + WindowConfigStore store; + // Zero size is valid but has zero dimensions + store.saveLastSize("test-app", QSize(0, 0)); +} + +void TestWindowConfigStore::testSaveLastSizeNegativeSize() +{ + WindowConfigStore store; + // Negative dimensions - should not crash + store.saveLastSize("test-app", QSize(-1, -1)); +} + +QTEST_MAIN(TestWindowConfigStore) +#include "main.moc" diff --git a/tests/test_xresource/CMakeLists.txt b/tests/test_xresource/CMakeLists.txt new file mode 100644 index 000000000..2bc3d9e18 --- /dev/null +++ b/tests/test_xresource/CMakeLists.txt @@ -0,0 +1,9 @@ +find_package(Qt6 REQUIRED COMPONENTS Test) + +add_executable(test_xresource main.cpp) +target_link_libraries(test_xresource PRIVATE libtreeland Qt6::Test) +add_test(NAME test_xresource COMMAND test_xresource) +set_tests_properties(test_xresource PROPERTIES + ENVIRONMENT "QT_QPA_PLATFORM=offscreen" + TIMEOUT 10 +) diff --git a/tests/test_xresource/main.cpp b/tests/test_xresource/main.cpp new file mode 100644 index 000000000..30b12889b --- /dev/null +++ b/tests/test_xresource/main.cpp @@ -0,0 +1,92 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include +#include +#include + +#include "xsettings/xresource.h" + +class TestXResource : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void testSplitSimpleKeyValue(); + void testSplitKeyOnly(); + void testSplitEmptyLine(); + void testSplitWhitespace(); + void testSplitEscapedColon(); + void testSplitMultipleColons(); + void testSplitValueWithColon(); + void testSplitLeadingTrailingWhitespace(); + void testToByteArray(); +}; + +void TestXResource::testSplitSimpleKeyValue() +{ + auto [key, value] = XResource::splitXResourceLine("Xft.dpi: 96"); + QCOMPARE(key, QByteArray("Xft.dpi")); + QCOMPARE(value, QByteArray("96")); +} + +void TestXResource::testSplitKeyOnly() +{ + auto [key, value] = XResource::splitXResourceLine("some.key"); + QCOMPARE(key, QByteArray("some.key")); + QVERIFY(value.isEmpty()); +} + +void TestXResource::testSplitEmptyLine() +{ + auto [key, value] = XResource::splitXResourceLine(""); + QVERIFY(key.isEmpty()); + QVERIFY(value.isEmpty()); +} + +void TestXResource::testSplitWhitespace() +{ + auto [key, value] = XResource::splitXResourceLine(" Xft.dpi : 96 "); + QCOMPARE(key, QByteArray("Xft.dpi")); + QCOMPARE(value, QByteArray("96")); +} + +void TestXResource::testSplitEscapedColon() +{ + auto [key, value] = XResource::splitXResourceLine("key\\:with\\:colons: value"); + QCOMPARE(key, QByteArray("key\\:with\\:colons")); + QCOMPARE(value, QByteArray("value")); +} + +void TestXResource::testSplitMultipleColons() +{ + auto [key, value] = XResource::splitXResourceLine("key: value:with:colons"); + QCOMPARE(key, QByteArray("key")); + QCOMPARE(value, QByteArray("value:with:colons")); +} + +void TestXResource::testSplitValueWithColon() +{ + auto [key, value] = XResource::splitXResourceLine("Xcursor.theme: Adwaita"); + QCOMPARE(key, QByteArray("Xcursor.theme")); + QCOMPARE(value, QByteArray("Adwaita")); +} + +void TestXResource::testSplitLeadingTrailingWhitespace() +{ + auto [key, value] = XResource::splitXResourceLine("\t Xft.hinting\t:\t1\t"); + QCOMPARE(key, QByteArray("Xft.hinting")); + QCOMPARE(value, QByteArray("1")); +} + +void TestXResource::testToByteArray() +{ + QCOMPARE(XResource::toByteArray(XResource::Xft_DPI), QByteArray("Xft.dpi")); + QCOMPARE(XResource::toByteArray(XResource::Xft_Antialias), QByteArray("Xft.antialias")); + QCOMPARE(XResource::toByteArray(XResource::Gtk_FontName), QByteArray("Gtk/FontName")); + QCOMPARE(XResource::toByteArray(XResource::Net_ThemeName), QByteArray("Net/ThemeName")); + QCOMPARE(XResource::toByteArray(XResource::Unknown), QByteArray("")); +} + +QTEST_MAIN(TestXResource) +#include "main.moc" diff --git a/tests/test_xsettings/CMakeLists.txt b/tests/test_xsettings/CMakeLists.txt new file mode 100644 index 000000000..816d3c2c5 --- /dev/null +++ b/tests/test_xsettings/CMakeLists.txt @@ -0,0 +1,9 @@ +find_package(Qt6 REQUIRED COMPONENTS Test) + +add_executable(test_xsettings main.cpp) +target_link_libraries(test_xsettings PRIVATE libtreeland Qt6::Test) +add_test(NAME test_xsettings COMMAND test_xsettings) +set_tests_properties(test_xsettings PROPERTIES + ENVIRONMENT "QT_QPA_PLATFORM=offscreen" + TIMEOUT 10 +) diff --git a/tests/test_xsettings/main.cpp b/tests/test_xsettings/main.cpp new file mode 100644 index 000000000..dc7a4c912 --- /dev/null +++ b/tests/test_xsettings/main.cpp @@ -0,0 +1,128 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include +#include +#include +#include + +#include "xsettings/xsettings.h" + +class TestableXSettings : public XSettings +{ +public: + explicit TestableXSettings(QObject *parent = nullptr) : XSettings(parent) {} + using XSettings::depopulateSettings; + using XSettings::populateSettings; +}; + +class TestXSettings : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void testRoundTrip_Integer(); + void testRoundTrip_String(); + void testRoundTrip_Color(); + void testRoundTrip_Multiple(); + void testRoundTrip_Empty(); + void testPropertyValueUpdate(); + void testToByteArray(); +}; + +void TestXSettings::testRoundTrip_Integer() +{ + TestableXSettings settings1(static_cast(nullptr)); + settings1.setPropertyValue("Net/CursorBlinkTime", 1000); + + QByteArray data = settings1.depopulateSettings(); + QVERIFY(!data.isEmpty()); + + TestableXSettings settings2(static_cast(nullptr)); + settings2.populateSettings(data); + + QVERIFY(settings2.contains("Net/CursorBlinkTime")); + QCOMPARE(settings2.getPropertyValue("Net/CursorBlinkTime").toInt(), 1000); +} + +void TestXSettings::testRoundTrip_String() +{ + TestableXSettings settings1(static_cast(nullptr)); + settings1.setPropertyValue("Gtk/FontName", QByteArray("Noto Sans 10")); + + QByteArray data = settings1.depopulateSettings(); + QVERIFY(!data.isEmpty()); + + TestableXSettings settings2(static_cast(nullptr)); + settings2.populateSettings(data); + + QVERIFY(settings2.contains("Gtk/FontName")); + QCOMPARE(settings2.getPropertyValue("Gtk/FontName").toByteArray(), QByteArray("Noto Sans 10")); +} + +void TestXSettings::testRoundTrip_Color() +{ + TestableXSettings settings1(static_cast(nullptr)); + QColor color(255, 128, 64, 200); + settings1.setPropertyValue("Gtk/ColorPalette", color); + + QByteArray data = settings1.depopulateSettings(); + QVERIFY(!data.isEmpty()); + + TestableXSettings settings2(static_cast(nullptr)); + settings2.populateSettings(data); + + QVERIFY(settings2.contains("Gtk/ColorPalette")); + QColor result = qvariant_cast(settings2.getPropertyValue("Gtk/ColorPalette")); + QCOMPARE(result.red(), 255); + QCOMPARE(result.green(), 128); + QCOMPARE(result.blue(), 64); + QCOMPARE(result.alpha(), 200); +} + +void TestXSettings::testRoundTrip_Multiple() +{ + TestableXSettings settings1(static_cast(nullptr)); + settings1.setPropertyValue("Net/CursorBlinkTime", 500); + settings1.setPropertyValue("Gtk/FontName", QByteArray("DejaVu Sans 12")); + settings1.setPropertyValue("Gtk/EnableAnimations", 1); + + QByteArray data = settings1.depopulateSettings(); + QVERIFY(!data.isEmpty()); + + TestableXSettings settings2(static_cast(nullptr)); + settings2.populateSettings(data); + + QCOMPARE(settings2.getPropertyValue("Net/CursorBlinkTime").toInt(), 500); + QCOMPARE(settings2.getPropertyValue("Gtk/FontName").toByteArray(), QByteArray("DejaVu Sans 12")); + QCOMPARE(settings2.getPropertyValue("Gtk/EnableAnimations").toInt(), 1); +} + +void TestXSettings::testRoundTrip_Empty() +{ + TestableXSettings settings1(static_cast(nullptr)); + + QByteArray data = settings1.depopulateSettings(); + QVERIFY(data.isEmpty()); +} + +void TestXSettings::testPropertyValueUpdate() +{ + TestableXSettings settings(static_cast(nullptr)); + settings.setPropertyValue("test/key", 100); + QCOMPARE(settings.getPropertyValue("test/key").toInt(), 100); + + settings.setPropertyValue("test/key", 200); + QCOMPARE(settings.getPropertyValue("test/key").toInt(), 200); +} + +void TestXSettings::testToByteArray() +{ + QCOMPARE(XSettings::toByteArray(XSettings::Xft_DPI), QByteArray("Xft/DPI")); + QCOMPARE(XSettings::toByteArray(XSettings::Gtk_FontName), QByteArray("Gtk/FontName")); + QCOMPARE(XSettings::toByteArray(XSettings::Net_ThemeName), QByteArray("Net/ThemeName")); + QCOMPARE(XSettings::toByteArray(XSettings::Unknown), QByteArray("")); +} + +QTEST_MAIN(TestXSettings) +#include "main.moc"