[plasma/union] /: autotests: Extract common testing code into its own header

Arjen Hiemstra <[email protected]> Wed, 5 Aug 2026 10:33:24 +0000 (UTC)
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit 8d7cb9dc2a1d2b99d490af4a4dd6179e8d6bab17 by Arjen Hiemstra.
Committed on 05/08/2026 at 10:24.
Pushed by ahiemstra into branch 'master'.

autotests: Extract common testing code into its own header

We need this in multiple cases, so let's keep a common implementation
around.

A  +191  -0    autotests/TestInputPlugin.h     [License: LGPL(3+eV) LGPL(v3.0) LGPL(v2.1)]
M  +2    -119  autotests/TestSelector.cpp
M  +4    -13   autotests/TestStyle.cpp
M  +5    -0    src/StyleRule.cpp
M  +2    -0    src/StyleRule.h

https://invent.kde.org/plasma/union/-/commit/8d7cb9dc2a1d2b99d490af4a4dd6179e8d6bab17

diff --git a/autotests/TestInputPlugin.h b/autotests/TestInputPlugin.h
new file mode 100644
index 00000000..4dd09bca
--- /dev/null
+++ b/autotests/TestInputPlugin.h
@@ -0,0 +1,191 @@
+// SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+// SPDX-FileCopyrightText: 2026 Arjen Hiemstra <[email protected]>
+
+#pragma once
+
+#include <QFile>
+#include <QJsonArray>
+
+#include <InputPlugin.h>
+#include <Style.h>
+#include <StyleLoader.h>
+#include <StylePackage.h>
+
+using namespace Union;
+using namespace Qt::StringLiterals;
+
+// Helper functions and classes for testing Style functionality.
+
+// Convenience wrapper around QFINDTESTDATA that returns a path rather than a string.
+#define FINDTESTPATH(relative) std::filesystem::path(QFINDTESTDATA(relative).toStdString())
+
+Element::State stateFromString(const QString &string)
+{
+    const auto metaEnum = QMetaEnum::fromType<Element::States>();
+
+    QByteArray value = string.toUtf8();
+
+    auto count = metaEnum.keyCount();
+    for (int i = 0; i < count; ++i) {
+        if (qstrnicmp(metaEnum.key(i), value.data(), value.size()) == 0) {
+            return Element::State(metaEnum.value(i));
+        }
+    }
+
+    return Element::State::None;
+}
+
+Union::SelectorList jsonToSelectorList(const QJsonArray &json)
+{
+    Union::SelectorList result;
+    for (const auto &entry : json) {
+        if (entry[u"universal"].toBool()) {
+            result.append(Selector::create<SelectorType::AnyElement>());
+            continue;
+        }
+
+        if (entry[u"child"].toBool()) {
+            result.append(Selector::create<SelectorType::ChildCombinator>());
+            continue;
+        }
+
+        if (entry[u"descendant"].toBool()) {
+            result.append(Selector::create<SelectorType::DescendantCombinator>());
+            continue;
+        }
+
+        if (auto type = entry[u"type"].toString(); !type.isEmpty()) {
+            result.append(Selector::create<SelectorType::Type>(type));
+        }
+
+        if (auto id = entry[u"id"].toString(); !id.isEmpty()) {
+            result.append(Selector::create<SelectorType::Id>(id));
+        }
+
+        if (auto states = entry[u"states"]; !states.isUndefined()) {
+            QJsonArray statesArray;
+            if (states.isArray()) {
+                statesArray = states.toArray();
+            } else {
+                statesArray = QJsonArray{states};
+            }
+
+            for (const auto &state : statesArray) {
+                result.append(Selector::create<SelectorType::State>(stateFromString(state.toString())));
+            }
+        }
+
+        for (const auto &hint : entry[u"hints"].toArray()) {
+            result.append(Selector::create<SelectorType::Hint>(hint.toString()));
+        }
+
+        for (const auto &exists : entry[u"attributes_exists"].toArray()) {
+            result.append(Selector::create<SelectorType::AttributeExists>(exists.toString()));
+        }
+
+        for (const auto &[key, value] : entry[u"attributes_equals"].toObject().asKeyValueRange()) {
+            result.append(Selector::create<SelectorType::AttributeEquals>(std::make_pair(key.toString(), value.toVariant())));
+        }
+
+        for (const auto &[key, value] : entry[u"attributes_substring"].toObject().asKeyValueRange()) {
+            result.append(Selector::create<SelectorType::AttributeSubstringMatch>(std::make_pair(key.toString(), value.toString())));
+        }
+    }
+
+    return result;
+}
+
+Union::Color variantToColor(const QVariant &variant)
+{
+    auto qcolor = variant.value<QColor>();
+    return Union::Color::rgba(qcolor.red(), qcolor.green(), qcolor.blue(), qcolor.alpha());
+}
+
+static const QStringList KnownProperties{
+    u"background-color"_s,
+};
+
+void setRuleProperty(const std::shared_ptr<Union::StyleRule> &rule, const QString &key, const QVariant &value)
+{
+    if (!KnownProperties.contains(key)) {
+        return;
+    }
+
+    if (!rule->properties()) {
+        rule->setProperties(std::make_unique<Union::Properties::StylePropertyGroup>());
+    }
+    auto properties = rule->properties();
+
+    if (key.startsWith(u"background")) {
+        if (!properties->background()) {
+            properties->setBackground(std::make_unique<Union::Properties::BackgroundPropertyGroup>());
+        }
+        auto background = properties->background();
+
+        if (key == u"background-color") {
+            background->setColor(variantToColor(value));
+        }
+    }
+}
+
+struct TestStyleLoader : public Union::StyleLoader {
+    bool load(std::shared_ptr<Union::Style> style) override
+    {
+        auto stylePath = style->path() / "contents" / "test" / "style.json";
+
+        QFile jsonFile{stylePath};
+        if (!jsonFile.open(QIODevice::ReadOnly)) {
+            qWarning() << "Failed reading test style data from" << stylePath.string() << jsonFile.errorString();
+            return false;
+        }
+
+        const auto json = QJsonDocument::fromJson(jsonFile.readAll())[u"rules"].toArray();
+        if (json.isEmpty()) {
+            qWarning() << stylePath.string() << "does not contain valid JSON data";
+            return false;
+        }
+
+        for (const auto &jsonRule : json) {
+            auto rule = Union::StyleRule::create();
+
+            for (const auto &[key, value] : jsonRule.toObject().asKeyValueRange()) {
+                if (key == u"selectors") {
+                    rule->setSelectors(jsonToSelectorList(value.toArray()));
+                } else {
+                    setRuleProperty(rule, key.toString(), value.toVariant());
+                }
+            }
+
+            if (!rule->isEmpty()) {
+                style->insert(rule);
+            }
+        }
+
+        return true;
+    }
+};
+
+struct TestInputPlugin : public Union::InputPlugin {
+    Union::StylePackage::Error validatePackage([[maybe_unused]] const Union::StylePackage &package) override
+    {
+        if (!std::filesystem::exists(package.path() / "contents" / "test")) {
+            return Union::StylePackage::Error::MissingFiles;
+        }
+
+        if (!std::filesystem::exists(package.path() / "contents" / "test" / "style.json")) {
+            return Union::StylePackage::Error::MissingFiles;
+        }
+
+        return Union::StylePackage::Error::None;
+    }
+
+    Union::PackageHandler::Error createPackage([[maybe_unused]] const Union::StylePackage &package) override
+    {
+        return Union::PackageHandler::Error::None;
+    }
+
+    std::shared_ptr<Union::Style> createStyle(const Union::StylePackage &package) const override
+    {
+        return Union::Style::create(package.path(), std::make_unique<TestStyleLoader>());
+    }
+};
diff --git a/autotests/TestSelector.cpp b/autotests/TestSelector.cpp
index 4b3d1592..df79cf7a 100644
--- a/autotests/TestSelector.cpp
+++ b/autotests/TestSelector.cpp
@@ -8,25 +8,11 @@
 #include <Element.h>
 #include <Selector.h>
 
+#include "TestInputPlugin.h"
+
 using namespace Union;
 using namespace Qt::StringLiterals;
 
-Element::State stateFromString(const QString &string)
-{
-    const auto metaEnum = QMetaEnum::fromType<Element::States>();
-
-    QByteArray value = string.toUtf8();
-
-    auto count = metaEnum.keyCount();
-    for (int i = 0; i < count; ++i) {
-        if (qstrnicmp(metaEnum.key(i), value.data(), value.size()) == 0) {
-            return Element::State(metaEnum.value(i));
-        }
-    }
-
-    return Element::State::None;
-}
-
 struct ElementProperties {
     QString type;
     QString id;
@@ -97,109 +83,6 @@ ElementList structureFromJson(const QJsonArray &json)
     return result;
 }
 
-struct SelectorProperties {
-    ElementProperties element;
-    bool universal = false;
-    bool child = false;
-    bool descendant = false;
-    QVariantList attributes_exists;
-    QVariantMap attributes_equals;
-    QVariantMap attributes_substring;
-};
-
-SelectorProperties jsonToSelectorProperties(const QJsonObject &json)
-{
-    SelectorProperties result;
-
-    result.element = jsonToElementProperties(json);
-
-    if (json.contains(u"universal")) {
-        result.universal = json.value(u"universal").toBool();
-    }
-
-    if (json.contains(u"child")) {
-        result.child = json.value(u"child").toBool();
-    }
-
-    if (json.contains(u"descendant")) {
-        result.descendant = json.value(u"descendant").toBool();
-    }
-
-    if (json.contains(u"attributes_exists")) {
-        result.attributes_exists = json.value(u"attributes_exists").toArray().toVariantList();
-    }
-
-    if (json.contains(u"attributes_equals")) {
-        result.attributes_equals = json.value(u"attributes_equals").toObject().toVariantMap();
-    }
-
-    if (json.contains(u"attribute_substring")) {
-        result.attributes_substring = json.value(u"attributes_substring").toObject().toVariantMap();
-    }
-
-    return result;
-}
-
-SelectorList jsonToSelectorList(const QJsonArray &json)
-{
-    SelectorList result;
-    for (auto entry : json) {
-        auto properties = jsonToSelectorProperties(entry.toObject());
-
-        if (properties.universal) {
-            result.append(Selector::create<SelectorType::AnyElement>());
-            continue;
-        }
-
-        if (properties.child) {
-            result.append(Selector::create<SelectorType::ChildCombinator>());
-            continue;
-        }
-
-        if (properties.descendant) {
-            result.append(Selector::create<SelectorType::DescendantCombinator>());
-            continue;
-        }
-
-        if (!properties.element.type.isEmpty()) {
-            result.append(Selector::create<SelectorType::Type>(properties.element.type));
-        }
-
-        if (!properties.element.id.isEmpty()) {
-            result.append(Selector::create<SelectorType::Id>(properties.element.id));
-        }
-
-        if (properties.element.states != 0) {
-            QMetaEnum statesEnum = QMetaEnum::fromType<Element::States>();
-            const auto count = statesEnum.keyCount();
-            for (int i = 0; i < count; ++i) {
-                auto value = statesEnum.value(i);
-                if (properties.element.states & value) {
-                    result.append(Selector::create<SelectorType::State>(Element::State(value)));
-                }
-            }
-        }
-
-        for (const auto &hint : properties.element.hints) {
-            result.append(Selector::create<SelectorType::Hint>(hint));
-        }
-
-        for (const auto &exists : properties.attributes_exists) {
-            result.append(Selector::create<SelectorType::AttributeExists>(exists.toString()));
-        }
-
-        for (auto [key, value] : properties.attributes_equals.asKeyValueRange()) {
-            result.append(Selector::create<SelectorType::AttributeEquals>(std::make_pair(key, value)));
-        }
-
-        for (auto [key, value] : properties.attributes_substring.asKeyValueRange()) {
-            result.append(Selector::create<SelectorType::AttributeSubstringMatch>(std::make_pair(key, value.toString())));
-        }
-    }
-
-    return result;
-}
-
 struct TestData {
     QString name;
     SelectorList selectors;
diff --git a/autotests/TestStyle.cpp b/autotests/TestStyle.cpp
index 1c8a9de8..568b2b5e 100644
--- a/autotests/TestStyle.cpp
+++ b/autotests/TestStyle.cpp
@@ -6,27 +6,18 @@
 #include <Style.h>
 #include <StyleLoader.h>
 
+#include "TestInputPlugin.h"
+
 using namespace Union;
 using namespace Qt::StringLiterals;
 
-struct TestLoader : public StyleLoader {
-    bool load(std::shared_ptr<Style> style) override
-    {
-        auto testRule = StyleRule::create();
-        testRule->setSelectors({Selector::create<SelectorType::Id>(u"test"_s)});
-        style->insert(testRule);
-
-        return true;
-    }
-};
-
 class TestStyle : public QObject
 {
     Q_OBJECT
 private Q_SLOTS:
     void testLoad()
     {
-        auto style = Style::create(std::filesystem::path(), std::make_unique<TestLoader>());
+        auto style = Style::create(std::filesystem::path(), std::make_unique<TestStyleLoader>());
 
         QVERIFY(style->load());
 
@@ -37,7 +28,7 @@ private Q_SLOTS:
 
     void testMatches()
     {
-        auto style = Style::create(std::filesystem::path(), std::make_unique<TestLoader>());
+        auto style = Style::create(std::filesystem::path(), std::make_unique<TestStyleLoader>());
         QVERIFY(style->load());
 
         QList<Element::Ptr> elements;
diff --git a/src/StyleRule.cpp b/src/StyleRule.cpp
index 89eda4ce..e948e6df 100644
--- a/src/StyleRule.cpp
+++ b/src/StyleRule.cpp
@@ -22,6 +22,11 @@ StyleRule::StyleRule(std::unique_ptr<StyleRulePrivate> &&d)
 
 StyleRule::~StyleRule() = default;
 
+bool StyleRule::isEmpty() const
+{
+    return d->selectors.isEmpty() && d->properties->isEmpty();
+}
+
 SelectorList StyleRule::selectors() const
 {
     return d->selectors;
diff --git a/src/StyleRule.h b/src/StyleRule.h
index d4a4e63d..fb056b17 100644
--- a/src/StyleRule.h
+++ b/src/StyleRule.h
@@ -39,6 +39,8 @@ public:
     StyleRule(std::unique_ptr<StyleRulePrivate> &&d);
     ~StyleRule() override;
 
+    bool isEmpty() const;
+
     SelectorList selectors() const;
     void setSelectors(const SelectorList &selectors);