diff --git a/CMakeLists.txt b/CMakeLists.txt index 3c103ca..d5db7bb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,6 +36,7 @@ find_package(Qt6 6.5 REQUIRED COMPONENTS QuickControls2 Widgets DBus + Network ) # Find KDE Frameworks 6 diff --git a/appiumtests/test_settings.py b/appiumtests/test_settings.py index 9b7f224..e0feea1 100644 --- a/appiumtests/test_settings.py +++ b/appiumtests/test_settings.py @@ -32,3 +32,9 @@ def test_reset_dialog_opens(self, driver): self.click_by_name(driver, "Reset to Defaults") dialog = self.wait_for_element(driver, AppiumBy.NAME, "Reset Settings") assert dialog.is_displayed() + + def test_update_section_visible(self, driver): + self.navigate_to_settings(driver) + self.wait_for_element(driver, AppiumBy.ACCESSIBILITY_ID, "labelUpdateStatus") + self.wait_for_element(driver, AppiumBy.ACCESSIBILITY_ID, "btnCheckUpdates") + self.wait_for_element(driver, AppiumBy.ACCESSIBILITY_ID, "checkUpdateAutomatically") diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b33e66e..ba86f3b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -18,6 +18,7 @@ target_link_libraries(couchplay_core PUBLIC Qt6::QuickControls2 Qt6::Widgets Qt6::DBus + Qt6::Network KF6::I18n KF6::CoreAddons KF6::ConfigCore @@ -100,6 +101,8 @@ target_sources(couchplay PRIVATE core/SunshineConfig.h core/StreamManager.cpp core/StreamManager.h + core/UpdateChecker.cpp + core/UpdateChecker.h dbus/CouchPlayHelperClient.cpp dbus/CouchPlayHelperClient.h ) diff --git a/src/core/SettingsManager.cpp b/src/core/SettingsManager.cpp index b003b8b..cbe6ff9 100644 --- a/src/core/SettingsManager.cpp +++ b/src/core/SettingsManager.cpp @@ -23,6 +23,7 @@ void SettingsManager::loadSettings() m_hidePanels = general.readEntry(QStringLiteral("HidePanels"), true); m_killSteam = general.readEntry(QStringLiteral("KillSteam"), true); m_restoreSession = general.readEntry(QStringLiteral("RestoreSession"), false); + m_checkForUpdatesAutomatically = general.readEntry(QStringLiteral("CheckForUpdatesAutomatically"), true); m_ignoredDevices = general.readEntry(QStringLiteral("IgnoredDevices"), QStringList()); // Gamescope settings @@ -43,6 +44,7 @@ void SettingsManager::saveAllSettings() general.writeEntry(QStringLiteral("HidePanels"), m_hidePanels); general.writeEntry(QStringLiteral("KillSteam"), m_killSteam); general.writeEntry(QStringLiteral("RestoreSession"), m_restoreSession); + general.writeEntry(QStringLiteral("CheckForUpdatesAutomatically"), m_checkForUpdatesAutomatically); general.writeEntry(QStringLiteral("IgnoredDevices"), m_ignoredDevices); // Gamescope settings @@ -87,6 +89,17 @@ void SettingsManager::setRestoreSession(bool value) } } +void SettingsManager::setCheckForUpdatesAutomatically(bool value) +{ + if (m_checkForUpdatesAutomatically != value) { + m_checkForUpdatesAutomatically = value; + KSharedConfig::Ptr config = KSharedConfig::openConfig(QStringLiteral("couchplayrc")); + config->group(QStringLiteral("General")).writeEntry(QStringLiteral("CheckForUpdatesAutomatically"), m_checkForUpdatesAutomatically); + config->sync(); + Q_EMIT checkForUpdatesAutomaticallyChanged(); + } +} + void SettingsManager::setScalingMode(const QString &value) { if (m_scalingMode != value) { @@ -158,6 +171,7 @@ void SettingsManager::resetToDefaults() m_hidePanels = true; m_killSteam = true; m_restoreSession = false; + m_checkForUpdatesAutomatically = true; m_scalingMode = QStringLiteral("fit"); m_filterMode = QStringLiteral("linear"); m_borderlessWindows = true; @@ -168,6 +182,7 @@ void SettingsManager::resetToDefaults() Q_EMIT hidePanelsChanged(); Q_EMIT killSteamChanged(); Q_EMIT restoreSessionChanged(); + Q_EMIT checkForUpdatesAutomaticallyChanged(); Q_EMIT scalingModeChanged(); Q_EMIT filterModeChanged(); Q_EMIT borderlessWindowsChanged(); diff --git a/src/core/SettingsManager.h b/src/core/SettingsManager.h index 5656a80..45f3155 100644 --- a/src/core/SettingsManager.h +++ b/src/core/SettingsManager.h @@ -23,6 +23,7 @@ class SettingsManager : public QObject Q_PROPERTY(bool hidePanels READ hidePanels WRITE setHidePanels NOTIFY hidePanelsChanged) Q_PROPERTY(bool killSteam READ killSteam WRITE setKillSteam NOTIFY killSteamChanged) Q_PROPERTY(bool restoreSession READ restoreSession WRITE setRestoreSession NOTIFY restoreSessionChanged) + Q_PROPERTY(bool checkForUpdatesAutomatically READ checkForUpdatesAutomatically WRITE setCheckForUpdatesAutomatically NOTIFY checkForUpdatesAutomaticallyChanged) // Gamescope settings Q_PROPERTY(QString scalingMode READ scalingMode WRITE setScalingMode NOTIFY scalingModeChanged) @@ -55,6 +56,12 @@ class SettingsManager : public QObject } void setRestoreSession(bool value); + bool checkForUpdatesAutomatically() const + { + return m_checkForUpdatesAutomatically; + } + void setCheckForUpdatesAutomatically(bool value); + // Gamescope settings QString scalingMode() const { @@ -92,6 +99,7 @@ class SettingsManager : public QObject void hidePanelsChanged(); void killSteamChanged(); void restoreSessionChanged(); + void checkForUpdatesAutomaticallyChanged(); void scalingModeChanged(); void filterModeChanged(); void borderlessWindowsChanged(); @@ -105,6 +113,7 @@ class SettingsManager : public QObject bool m_hidePanels = true; bool m_killSteam = true; bool m_restoreSession = false; + bool m_checkForUpdatesAutomatically = true; // Gamescope settings QString m_scalingMode = QStringLiteral("fit"); diff --git a/src/core/UpdateChecker.cpp b/src/core/UpdateChecker.cpp new file mode 100644 index 0000000..68f34e0 --- /dev/null +++ b/src/core/UpdateChecker.cpp @@ -0,0 +1,204 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-FileCopyrightText: 2026 CouchPlay Contributors + +#include "UpdateChecker.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "couchplay-version.h" + +UpdateChecker::UpdateChecker(QObject *parent) + : QObject(parent) + , m_nam(new QNetworkAccessManager(this)) +{ + m_installMethod = detectInstallMethod(); +} + +void UpdateChecker::checkForUpdates() +{ + startCheck(false); +} + +void UpdateChecker::checkForUpdatesOnStartup() +{ + startCheck(true); +} + +void UpdateChecker::startCheck(bool automatic) +{ + if (m_state == Checking) { + return; + } + + m_automatic = automatic; + setState(Checking); + + QNetworkRequest request(QUrl(QStringLiteral("https://api.github.com/repos/hikaps/couchplay/releases/latest"))); + request.setHeader(QNetworkRequest::UserAgentHeader, QStringLiteral("couchplay/%1").arg(currentVersion())); + request.setRawHeader(QByteArrayLiteral("Accept"), QByteArrayLiteral("application/vnd.github+json")); + request.setTransferTimeout(10000); + + QNetworkReply *reply = m_nam->get(request); + connect(reply, &QNetworkReply::finished, this, [this, reply]() { + reply->deleteLater(); + + if (reply->error() != QNetworkReply::NoError) { + qWarning() << "UpdateChecker: request failed:" << reply->errorString(); + setState(Error); + return; + } + + const int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if (statusCode != 200) { + qWarning() << "UpdateChecker: unexpected HTTP status:" << statusCode; + setState(Error); + return; + } + + const QString tag = parseLatestTag(reply->readAll()); + if (tag.isEmpty()) { + qWarning() << "UpdateChecker: could not parse latest release response"; + setState(Error); + return; + } + + applyLatestTag(tag); + Q_EMIT checkFinished(m_automatic, m_updateAvailable, m_latestVersion); + }); +} + +void UpdateChecker::applyLatestTag(const QString &tag) +{ + const QString normalized = normalizeVersion(tag); + if (m_latestVersion != normalized) { + m_latestVersion = normalized; + Q_EMIT latestVersionChanged(); + } + + const bool available = compareVersions(m_latestVersion, currentVersion()) > 0; + if (m_updateAvailable != available) { + m_updateAvailable = available; + Q_EMIT updateAvailableChanged(); + } + + setState(available ? UpdateAvailable : UpToDate); + qDebug() << "UpdateChecker:" << (available ? "update available:" : "up to date") << m_latestVersion; +} + +QString UpdateChecker::normalizeVersion(const QString &version) +{ + QString normalized = version.trimmed(); + if (normalized.startsWith(QLatin1Char('v')) || normalized.startsWith(QLatin1Char('V'))) { + normalized.remove(0, 1); + } + return normalized; +} + +int UpdateChecker::compareVersions(const QString &a, const QString &b) +{ + const QStringList aParts = normalizeVersion(a).split(QLatin1Char('.')); + const QStringList bParts = normalizeVersion(b).split(QLatin1Char('.')); + + const int maxParts = qMax(aParts.size(), bParts.size()); + for (int i = 0; i < maxParts; ++i) { + const QString aPart = i < aParts.size() ? aParts.at(i) : QStringLiteral("0"); + const QString bPart = i < bParts.size() ? bParts.at(i) : QStringLiteral("0"); + + bool aOk = false; + bool bOk = false; + const qlonglong aNum = aPart.toLongLong(&aOk); + const qlonglong bNum = bPart.toLongLong(&bOk); + + int cmp = 0; + if (aOk && bOk) { + cmp = (aNum < bNum) ? -1 : (aNum > bNum) ? 1 : 0; + } else { + cmp = QString::compare(aPart, bPart, Qt::CaseInsensitive); + } + if (cmp != 0) { + return cmp; + } + } + return 0; +} + +QString UpdateChecker::parseLatestTag(const QByteArray &json) +{ + const QJsonDocument doc = QJsonDocument::fromJson(json); + if (!doc.isObject()) { + return QString(); + } + const QJsonValue tag = doc.object().value(QStringLiteral("tag_name")); + if (!tag.isString()) { + return QString(); + } + return tag.toString(); +} + +QString UpdateChecker::detectInstallMethod() +{ + if (QFile::exists(QStringLiteral("/.flatpak-info"))) { + return QStringLiteral("flatpak"); + } + + QFile osRelease(QStringLiteral("/etc/os-release")); + if (osRelease.open(QIODevice::ReadOnly | QIODevice::Text)) { + while (!osRelease.atEnd()) { + const QString line = QString::fromUtf8(osRelease.readLine()).trimmed(); + if (!line.startsWith(QStringLiteral("ID="))) { + continue; + } + QString value = line.mid(3); + if (value.startsWith(QLatin1Char('"')) && value.endsWith(QLatin1Char('"'))) { + value = value.mid(1, value.size() - 2); + } + if (value == QLatin1String("steamos")) { + return QStringLiteral("steamos"); + } + } + } + + return QStringLiteral("native"); +} + +UpdateChecker::State UpdateChecker::state() const +{ + return m_state; +} + +QString UpdateChecker::currentVersion() const +{ + return QStringLiteral(COUCHPLAY_VERSION_STRING); +} + +QString UpdateChecker::latestVersion() const +{ + return m_latestVersion; +} + +bool UpdateChecker::updateAvailable() const +{ + return m_updateAvailable; +} + +QString UpdateChecker::installMethod() const +{ + return m_installMethod; +} + +void UpdateChecker::setState(State state) +{ + if (m_state == state) { + return; + } + m_state = state; + Q_EMIT stateChanged(); +} diff --git a/src/core/UpdateChecker.h b/src/core/UpdateChecker.h new file mode 100644 index 0000000..4383010 --- /dev/null +++ b/src/core/UpdateChecker.h @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-FileCopyrightText: 2026 CouchPlay Contributors + +#pragma once + +#include +#include +#include +#include + +class QNetworkAccessManager; + +class UpdateChecker : public QObject +{ + Q_OBJECT + QML_ELEMENT + Q_PROPERTY(State state READ state NOTIFY stateChanged) + Q_PROPERTY(QString currentVersion READ currentVersion CONSTANT) + Q_PROPERTY(QString latestVersion READ latestVersion NOTIFY latestVersionChanged) + Q_PROPERTY(bool updateAvailable READ updateAvailable NOTIFY updateAvailableChanged) + Q_PROPERTY(QString installMethod READ installMethod CONSTANT) + Q_PROPERTY(QString releasesUrl READ releasesUrl CONSTANT) +public: + enum State { Idle, Checking, UpToDate, UpdateAvailable, Error }; // plain enum: Q_ENUM/QML-safe + Q_ENUM(State) + + explicit UpdateChecker(QObject *parent = nullptr); + + Q_INVOKABLE void checkForUpdates(); // manual — no passive notification + Q_INVOKABLE void checkForUpdatesOnStartup(); // automatic — Main.qml only + + // Pure helpers, public for unit tests: + static QString normalizeVersion(const QString &version); + static int compareVersions(const QString &a, const QString &b); + static QString parseLatestTag(const QByteArray &json); + static QString detectInstallMethod(); + + void applyLatestTag(const QString &tag); // public for unit tests; called by reply handler + + State state() const; + QString currentVersion() const; + QString latestVersion() const; + bool updateAvailable() const; + QString installMethod() const; + QString releasesUrl() const { return QStringLiteral("https://github.com/hikaps/couchplay/releases"); } + +Q_SIGNALS: + void stateChanged(); + void latestVersionChanged(); + void updateAvailableChanged(); + void checkFinished(bool automatic, bool updateAvailable, const QString &latestVersion); + +private: + void startCheck(bool automatic); + void setState(State state); + QNetworkAccessManager *m_nam = nullptr; + State m_state = Idle; + QString m_latestVersion; + bool m_updateAvailable = false; + bool m_automatic = false; + QString m_installMethod; +}; diff --git a/src/qml/Main.qml b/src/qml/Main.qml index 87a7c69..f102a08 100644 --- a/src/qml/Main.qml +++ b/src/qml/Main.qml @@ -129,6 +129,23 @@ Kirigami.ApplicationWindow { id: audioManager } + UpdateChecker { + id: updateChecker + + Component.onCompleted: { + if (settingsManager.checkForUpdatesAutomatically) { + updateChecker.checkForUpdatesOnStartup() + } + } + + onCheckFinished: (automatic, updateAvailable, latestVersion) => { + if (automatic && updateAvailable) { + applicationWindow().showPassiveNotification( + i18nc("@info", "CouchPlay %1 is available — update by re-running the install command from the README", latestVersion), "long") + } + } + } + globalDrawer: Kirigami.GlobalDrawer { id: drawer title: i18nc("@title", "CouchPlay") @@ -277,7 +294,8 @@ Kirigami.ApplicationWindow { steamConfigManager: steamConfigManager, settingsManager: settingsManager, heroicConfigManager: heroicConfigManager, - audioManager: audioManager + audioManager: audioManager, + updateChecker: updateChecker }) } } diff --git a/src/qml/pages/SettingsPage.qml b/src/qml/pages/SettingsPage.qml index 987f703..8d4ee91 100644 --- a/src/qml/pages/SettingsPage.qml +++ b/src/qml/pages/SettingsPage.qml @@ -29,6 +29,8 @@ Kirigami.ScrollablePage { property var audioManager: null + property var updateChecker: null + PresetManager { id: internalPresetManager } @@ -644,6 +646,121 @@ Kirigami.ScrollablePage { ] } + Kirigami.FormLayout { + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + wideMode: root.width > Kirigami.Units.gridUnit * 30 + + Kirigami.Separator { + Kirigami.FormData.isSection: true + Kirigami.FormData.label: i18nc("@title:group", "Software Update") + } + + RowLayout { + Kirigami.FormData.label: i18nc("@label", "Status") + spacing: Kirigami.Units.smallSpacing + + Kirigami.Icon { + source: { + let s = root.updateChecker ? root.updateChecker.state : -1 + if (s === UpdateChecker.Checking) return "view-refresh" + if (s === UpdateChecker.UpToDate) return "dialog-ok-apply" + if (s === UpdateChecker.Error) return "dialog-error" + return "dialog-information" + } + color: { + let s = root.updateChecker ? root.updateChecker.state : -1 + if (s === UpdateChecker.UpToDate) return Kirigami.Theme.positiveTextColor + if (s === UpdateChecker.Error) return Kirigami.Theme.negativeTextColor + return Kirigami.Theme.textColor + } + Layout.preferredWidth: Kirigami.Units.iconSizes.small + Layout.preferredHeight: Kirigami.Units.iconSizes.small + } + + Controls.Label { + objectName: "labelUpdateStatus" + text: { + if (!root.updateChecker || root.updateChecker.state === UpdateChecker.Idle) { + return i18nc("@info", "Not checked yet") + } + if (root.updateChecker.state === UpdateChecker.Checking) { + return i18nc("@info", "Checking…") + } + if (root.updateChecker.state === UpdateChecker.UpToDate) { + return i18nc("@info", "Up to date (%1)", root.updateChecker.currentVersion) + } + if (root.updateChecker.state === UpdateChecker.UpdateAvailable) { + return i18nc("@info", "Update available: %1", root.updateChecker.latestVersion) + } + return i18nc("@info", "Could not check for updates") + } + color: { + if (!root.updateChecker) return Kirigami.Theme.textColor + if (root.updateChecker.state === UpdateChecker.UpToDate) return Kirigami.Theme.positiveTextColor + if (root.updateChecker.state === UpdateChecker.Error) return Kirigami.Theme.negativeTextColor + return Kirigami.Theme.textColor + } + } + } + + Controls.Label { + Kirigami.FormData.label: i18nc("@label", "Latest release") + text: root.updateChecker && root.updateChecker.latestVersion !== "" ? root.updateChecker.latestVersion : "—" + } + + Controls.Button { + objectName: "btnCheckUpdates" + Kirigami.FormData.label: " " + Accessible.role: Accessible.Button + Accessible.name: text + Accessible.onPressAction: clicked() + text: i18nc("@action:button", "Check Now") + icon.name: "view-refresh" + enabled: root.updateChecker && root.updateChecker.state !== UpdateChecker.Checking + onClicked: root.updateChecker.checkForUpdates() + } + + Controls.CheckBox { + objectName: "checkUpdateAutomatically" + Kirigami.FormData.label: i18nc("@option:check", "Check for updates automatically") + Accessible.role: Accessible.CheckBox + Accessible.name: Kirigami.FormData.label + checked: root.settingsManager ? root.settingsManager.checkForUpdatesAutomatically : true + onToggled: if (root.settingsManager) root.settingsManager.checkForUpdatesAutomatically = checked + + Controls.ToolTip.text: i18nc("@info:tooltip", "Checks GitHub for a new release when CouchPlay starts. No data is sent beyond the request itself.") + Controls.ToolTip.visible: hovered + Controls.ToolTip.delay: 1000 + } + } + + Kirigami.InlineMessage { + Layout.fillWidth: true + visible: root.updateChecker !== null && root.updateChecker.updateAvailable + type: Kirigami.MessageType.Information + text: { + if (root.updateChecker.installMethod === "flatpak") { + return i18nc("@info", "Re-run the install command from the README to download the new version. Your settings and profiles are preserved.") + } + if (root.updateChecker.installMethod === "steamos") { + return i18nc("@info", "Re-run the install command from the README to update the app and its system extension. Stop any running session first.") + } + return i18nc("@info", "Re-run the install command from the README (add --tarball to keep the native binary), or download from the releases page.") + } + + actions: [ + Kirigami.Action { + objectName: "actionOpenReleases" + Accessible.role: Accessible.Button + Accessible.name: i18nc("@action:button", "Open Releases Page") + Accessible.onPressAction: triggered() + icon.name: "link" + text: i18nc("@action:button", "Open Releases Page") + onTriggered: Qt.openUrlExternally(root.updateChecker.releasesUrl) + } + ] + } + Kirigami.FormLayout { Layout.alignment: Qt.AlignLeft | Qt.AlignTop wideMode: root.width > Kirigami.Units.gridUnit * 30 diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 5b0d349..da9dcff 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -14,6 +14,7 @@ set(TEST_INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/../src/core ${CMAKE_CURRENT_SOURCE_DIR}/../src/dbus ${CMAKE_CURRENT_BINARY_DIR} + ${PROJECT_BINARY_DIR} ) # Common manager sources (for tests) @@ -51,6 +52,8 @@ set(TEST_MANAGER_SOURCES ../src/core/SunshineConfig.h ../src/core/StreamManager.cpp ../src/core/StreamManager.h + ../src/core/UpdateChecker.cpp + ../src/core/UpdateChecker.h ../src/dbus/CouchPlayHelperClient.cpp ../src/dbus/CouchPlayHelperClient.h ) @@ -78,6 +81,7 @@ function(add_couchplay_test TEST_NAME) Qt6::QuickControls2 Qt6::Widgets Qt6::DBus + Qt6::Network Qt6::Test KF6::I18n KF6::CoreAddons @@ -163,6 +167,7 @@ add_couchplay_test(test_streaming_config) add_couchplay_test(test_streaming_session) add_couchplay_test(test_streammanager) add_couchplay_test(test_sunshine_config) +add_couchplay_test(test_updatechecker) add_couchplay_test(test_usermanager) # Add helper tests diff --git a/tests/test_updatechecker.cpp b/tests/test_updatechecker.cpp new file mode 100644 index 0000000..fe0d914 --- /dev/null +++ b/tests/test_updatechecker.cpp @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-FileCopyrightText: 2026 CouchPlay Contributors + +#include +#include + +#include "SettingsManager.h" +#include "UpdateChecker.h" + +class TestUpdateChecker : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void testNormalizeVersion(); + void testCompareVersions(); + void testParseLatestTag(); + void testDetectInstallMethodMembership(); + void testApplyLatestTagUpdateAvailable(); + void testApplyLatestTagUpToDate(); + void testAutoCheckSettingDefault(); +}; + +void TestUpdateChecker::testNormalizeVersion() +{ + QCOMPARE(UpdateChecker::normalizeVersion(QStringLiteral("v0.4.1")), QStringLiteral("0.4.1")); + QCOMPARE(UpdateChecker::normalizeVersion(QStringLiteral("V0.4.1")), QStringLiteral("0.4.1")); + QCOMPARE(UpdateChecker::normalizeVersion(QStringLiteral("0.4.1")), QStringLiteral("0.4.1")); +} + +void TestUpdateChecker::testCompareVersions() +{ + QCOMPARE(UpdateChecker::compareVersions(QStringLiteral("0.4.0"), QStringLiteral("0.4.0")), 0); + QVERIFY(UpdateChecker::compareVersions(QStringLiteral("0.4.0"), QStringLiteral("0.4.1")) < 0); + QVERIFY(UpdateChecker::compareVersions(QStringLiteral("v0.5.0"), QStringLiteral("0.4.9")) > 0); + QCOMPARE(UpdateChecker::compareVersions(QStringLiteral("0.4"), QStringLiteral("0.4.0")), 0); + QVERIFY(UpdateChecker::compareVersions(QStringLiteral("0.4"), QStringLiteral("0.4.1")) < 0); + QVERIFY(UpdateChecker::compareVersions(QStringLiteral("1.0"), QStringLiteral("0.9")) > 0); +} + +void TestUpdateChecker::testParseLatestTag() +{ + QCOMPARE(UpdateChecker::parseLatestTag(QByteArrayLiteral("{\"tag_name\":\"v0.4.1\"}")), QStringLiteral("v0.4.1")); + QVERIFY(UpdateChecker::parseLatestTag(QByteArrayLiteral("{}")).isEmpty()); + QVERIFY(UpdateChecker::parseLatestTag(QByteArrayLiteral("not json")).isEmpty()); + QVERIFY(UpdateChecker::parseLatestTag(QByteArrayLiteral("{\"tag_name\":3}")).isEmpty()); +} + +void TestUpdateChecker::testDetectInstallMethodMembership() +{ + const QString method = UpdateChecker::detectInstallMethod(); + QVERIFY(method == QLatin1String("flatpak") || method == QLatin1String("steamos") || method == QLatin1String("native")); +} + +void TestUpdateChecker::testApplyLatestTagUpdateAvailable() +{ + UpdateChecker checker; + QSignalSpy stateSpy(&checker, &UpdateChecker::stateChanged); + + checker.applyLatestTag(QStringLiteral("v999.0.0")); + + QCOMPARE(checker.latestVersion(), QStringLiteral("999.0.0")); + QVERIFY(checker.updateAvailable()); + QCOMPARE(checker.state(), UpdateChecker::UpdateAvailable); + QVERIFY(stateSpy.count() >= 1); +} + +void TestUpdateChecker::testApplyLatestTagUpToDate() +{ + UpdateChecker checker; + + checker.applyLatestTag(QStringLiteral("v0.0.0")); + + QCOMPARE(checker.state(), UpdateChecker::UpToDate); + QVERIFY(!checker.updateAvailable()); +} + +void TestUpdateChecker::testAutoCheckSettingDefault() +{ + SettingsManager settings; + QVERIFY(settings.checkForUpdatesAutomatically()); +} + +QTEST_MAIN(TestUpdateChecker) +#include "test_updatechecker.moc"