mirror of
https://github.com/spalencsar/bearwave.git
synced 2026-07-06 22:24:17 +02:00
- Correct history/resume next-previous context and list index resync after reload - Abort stale RadioBrowser requests and suppress error banners when cache satisfies loads - Reset local search filter on page changes and debounce compact-mode API search - Move system tray to C++ (QSystemTrayIcon) for reliable Wayland context menus - Harden MPRIS registration and metadata updates for Plasma media widgets - Add playback and API race unit tests - Drop unused KF6 Kirigami, I18n, and CoreAddons build dependencies - Update packaging docs, CI, and PKGBUILD to match Qt-only requirements
306 lines
11 KiB
C++
306 lines
11 KiB
C++
// Copyright (c) 2026 Sebastian Palencsar
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
#include <QtTest>
|
|
#include <QApplication>
|
|
#include <QDir>
|
|
#include <QFile>
|
|
#include <QJsonDocument>
|
|
#include <QJsonObject>
|
|
#include <QJsonArray>
|
|
#include <QTemporaryDir>
|
|
#include <QCryptographicHash>
|
|
#include <QEventLoop>
|
|
#include <QTimer>
|
|
|
|
#include "radiobackend.h"
|
|
|
|
class RadioBackendPlaybackTest : public QObject
|
|
{
|
|
Q_OBJECT
|
|
|
|
private slots:
|
|
void initTestCase();
|
|
void init();
|
|
void history_next_navigates_older_entries();
|
|
void resume_disables_list_navigation();
|
|
void station_list_next_uses_filtered_index();
|
|
void history_next_does_not_jump_to_station_list();
|
|
void cached_load_keeps_error_clear_after_network_failure();
|
|
void list_reload_resyncs_index_for_playing_station();
|
|
void list_next_disabled_when_playing_station_not_in_new_list();
|
|
};
|
|
|
|
namespace {
|
|
QTemporaryDir *s_tempHome = nullptr;
|
|
|
|
QString cachePathForEndpoint(const QString &endpoint)
|
|
{
|
|
const QString cacheDir = QDir::homePath() + QStringLiteral("/.cache/bearwave/api_cache");
|
|
const QString hash = QString(QCryptographicHash::hash(endpoint.toUtf8(), QCryptographicHash::Md5).toHex());
|
|
return cacheDir + QStringLiteral("/") + hash + QStringLiteral(".json");
|
|
}
|
|
|
|
void writeStationCache(const QString &endpoint, const QString &name,
|
|
const QString &url = QString(), const QString &uuid = QString())
|
|
{
|
|
QDir().mkpath(QDir::homePath() + QStringLiteral("/.cache/bearwave/api_cache"));
|
|
|
|
QJsonObject station;
|
|
station.insert(QStringLiteral("name"), name);
|
|
station.insert(QStringLiteral("url_resolved"),
|
|
url.isEmpty() ? QStringLiteral("http://example.com/") + name : url);
|
|
station.insert(QStringLiteral("country"), QStringLiteral("Germany"));
|
|
station.insert(QStringLiteral("stationuuid"), uuid.isEmpty() ? name : uuid);
|
|
|
|
QJsonArray array;
|
|
array.append(station);
|
|
|
|
QFile file(cachePathForEndpoint(endpoint));
|
|
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
|
|
return;
|
|
}
|
|
file.write(QJsonDocument(array).toJson());
|
|
file.close();
|
|
}
|
|
|
|
void writeStationListCache(const QString &endpoint, const QJsonArray &stations)
|
|
{
|
|
QDir().mkpath(QDir::homePath() + QStringLiteral("/.cache/bearwave/api_cache"));
|
|
QFile file(cachePathForEndpoint(endpoint));
|
|
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
|
|
return;
|
|
}
|
|
file.write(QJsonDocument(stations).toJson());
|
|
file.close();
|
|
}
|
|
|
|
int visibleIndexForUrl(RadioBackend &backend, const QString &url)
|
|
{
|
|
const QList<QObject*> stations = backend.stations();
|
|
for (int i = 0; i < stations.size(); ++i) {
|
|
QObject *station = stations[i];
|
|
const QString resolved = station->property("urlResolved").toString();
|
|
if (resolved == url) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
void RadioBackendPlaybackTest::initTestCase()
|
|
{
|
|
s_tempHome = new QTemporaryDir();
|
|
QVERIFY(s_tempHome->isValid());
|
|
qputenv("HOME", s_tempHome->path().toUtf8());
|
|
|
|
const QString configDir = QDir::homePath() + QStringLiteral("/.config/bearwave");
|
|
QDir().mkpath(configDir);
|
|
|
|
QJsonObject state;
|
|
state.insert(QStringLiteral("lastStationName"), QStringLiteral("Station A"));
|
|
state.insert(QStringLiteral("lastStationUrl"), QStringLiteral("http://example.com/a"));
|
|
state.insert(QStringLiteral("volume"), 0.5);
|
|
|
|
QJsonArray recent;
|
|
recent.append(QJsonObject{
|
|
{QStringLiteral("name"), QStringLiteral("Station A")},
|
|
{QStringLiteral("uuid"), QStringLiteral("uuid-a")},
|
|
{QStringLiteral("urlResolved"), QStringLiteral("http://example.com/a")}
|
|
});
|
|
recent.append(QJsonObject{
|
|
{QStringLiteral("name"), QStringLiteral("Station B")},
|
|
{QStringLiteral("uuid"), QStringLiteral("uuid-b")},
|
|
{QStringLiteral("urlResolved"), QStringLiteral("http://example.com/b")}
|
|
});
|
|
recent.append(QJsonObject{
|
|
{QStringLiteral("name"), QStringLiteral("Station C")},
|
|
{QStringLiteral("uuid"), QStringLiteral("uuid-c")},
|
|
{QStringLiteral("urlResolved"), QStringLiteral("http://example.com/c")}
|
|
});
|
|
state.insert(QStringLiteral("recentStations"), recent);
|
|
|
|
QFile file(configDir + QStringLiteral("/state.json"));
|
|
QVERIFY(file.open(QIODevice::WriteOnly | QIODevice::Truncate));
|
|
file.write(QJsonDocument(state).toJson());
|
|
file.close();
|
|
}
|
|
|
|
void RadioBackendPlaybackTest::init()
|
|
{
|
|
initTestCase();
|
|
}
|
|
|
|
void RadioBackendPlaybackTest::history_next_navigates_older_entries()
|
|
{
|
|
RadioBackend backend;
|
|
|
|
QVERIFY(backend.playRecentByUuid(QStringLiteral("uuid-b"), QString()));
|
|
QCOMPARE(backend.currentStationUrl(), QStringLiteral("http://example.com/b"));
|
|
QVERIFY(backend.hasNextStation());
|
|
QVERIFY(!backend.hasPreviousStation());
|
|
|
|
backend.playNextStation();
|
|
QCOMPARE(backend.currentStationUrl(), QStringLiteral("http://example.com/a"));
|
|
QVERIFY(backend.hasPreviousStation());
|
|
}
|
|
|
|
void RadioBackendPlaybackTest::resume_disables_list_navigation()
|
|
{
|
|
RadioBackend backend;
|
|
|
|
backend.resumeLastStation();
|
|
QCOMPARE(backend.currentStationUrl(), QStringLiteral("http://example.com/a"));
|
|
QVERIFY(!backend.hasNextStation());
|
|
QVERIFY(!backend.hasPreviousStation());
|
|
|
|
backend.playNextStation();
|
|
QCOMPARE(backend.currentStationUrl(), QStringLiteral("http://example.com/a"));
|
|
}
|
|
|
|
void RadioBackendPlaybackTest::station_list_next_uses_filtered_index()
|
|
{
|
|
RadioBackend backend;
|
|
|
|
backend.addManualStation(QStringLiteral("Station One"), QStringLiteral("http://example.com/one"), QStringLiteral("DE"));
|
|
backend.addManualStation(QStringLiteral("Station Two"), QStringLiteral("http://example.com/two"), QStringLiteral("DE"));
|
|
|
|
backend.playStation(0);
|
|
QCOMPARE(backend.currentStationUrl(), QStringLiteral("http://example.com/two"));
|
|
QVERIFY(backend.hasNextStation());
|
|
|
|
backend.playNextStation();
|
|
QCOMPARE(backend.currentStationUrl(), QStringLiteral("http://example.com/one"));
|
|
}
|
|
|
|
void RadioBackendPlaybackTest::history_next_does_not_jump_to_station_list()
|
|
{
|
|
RadioBackend backend;
|
|
|
|
// Loaded station list present, but playback started from history (not playStation()).
|
|
backend.addManualStation(QStringLiteral("Station X"), QStringLiteral("http://example.com/x"), QStringLiteral("DE"));
|
|
backend.addManualStation(QStringLiteral("Station Y"), QStringLiteral("http://example.com/y"), QStringLiteral("DE"));
|
|
|
|
QVERIFY(backend.playRecentByUuid(QStringLiteral("uuid-c"), QString()));
|
|
QCOMPARE(backend.currentStationUrl(), QStringLiteral("http://example.com/c"));
|
|
|
|
backend.playNextStation();
|
|
QCOMPARE(backend.currentStationUrl(), QStringLiteral("http://example.com/a"));
|
|
QVERIFY(backend.currentStationUrl() != QStringLiteral("http://example.com/x"));
|
|
QVERIFY(backend.currentStationUrl() != QStringLiteral("http://example.com/y"));
|
|
}
|
|
|
|
void RadioBackendPlaybackTest::cached_load_keeps_error_clear_after_network_failure()
|
|
{
|
|
writeStationCache(QStringLiteral("/stations/topvote/100"), QStringLiteral("Cached Top Station"));
|
|
|
|
RadioBackend backend;
|
|
QVERIFY(QFile::exists(QDir::homePath() + QStringLiteral("/.cache/bearwave/api_cache")));
|
|
backend.loadTopStations();
|
|
|
|
QTRY_VERIFY_WITH_TIMEOUT(backend.stations().size() > 0, 2000);
|
|
QCOMPARE(backend.lastError(), QString());
|
|
|
|
QEventLoop loop;
|
|
QTimer::singleShot(11000, &loop, &QEventLoop::quit);
|
|
loop.exec();
|
|
|
|
QCOMPARE(backend.lastError(), QString());
|
|
QVERIFY(backend.stations().size() > 0);
|
|
}
|
|
|
|
void RadioBackendPlaybackTest::list_reload_resyncs_index_for_playing_station()
|
|
{
|
|
const QString topEndpoint = QStringLiteral("/stations/topvote/100");
|
|
const QString deEndpoint = QStringLiteral("/stations/bycountrycodeexact/DE?limit=50&order=votes&reverse=true");
|
|
const QString sharedUrl = QStringLiteral("http://example.com/shared");
|
|
|
|
QJsonArray topStations;
|
|
topStations.append(QJsonObject{
|
|
{QStringLiteral("name"), QStringLiteral("Alpha Top")},
|
|
{QStringLiteral("url_resolved"), QStringLiteral("http://example.com/alpha")},
|
|
{QStringLiteral("country"), QStringLiteral("Global")},
|
|
{QStringLiteral("stationuuid"), QStringLiteral("uuid-alpha")}
|
|
});
|
|
topStations.append(QJsonObject{
|
|
{QStringLiteral("name"), QStringLiteral("Shared Station")},
|
|
{QStringLiteral("url_resolved"), sharedUrl},
|
|
{QStringLiteral("country"), QStringLiteral("Global")},
|
|
{QStringLiteral("stationuuid"), QStringLiteral("uuid-shared")}
|
|
});
|
|
topStations.append(QJsonObject{
|
|
{QStringLiteral("name"), QStringLiteral("Zulu Top")},
|
|
{QStringLiteral("url_resolved"), QStringLiteral("http://example.com/zulu")},
|
|
{QStringLiteral("country"), QStringLiteral("Global")},
|
|
{QStringLiteral("stationuuid"), QStringLiteral("uuid-zulu")}
|
|
});
|
|
writeStationListCache(topEndpoint, topStations);
|
|
|
|
QJsonArray deStations;
|
|
deStations.append(QJsonObject{
|
|
{QStringLiteral("name"), QStringLiteral("DE Only")},
|
|
{QStringLiteral("url_resolved"), QStringLiteral("http://example.com/deonly")},
|
|
{QStringLiteral("country"), QStringLiteral("Germany")},
|
|
{QStringLiteral("stationuuid"), QStringLiteral("uuid-deonly")}
|
|
});
|
|
deStations.append(QJsonObject{
|
|
{QStringLiteral("name"), QStringLiteral("Shared Station")},
|
|
{QStringLiteral("url_resolved"), sharedUrl},
|
|
{QStringLiteral("country"), QStringLiteral("Germany")},
|
|
{QStringLiteral("stationuuid"), QStringLiteral("uuid-shared")}
|
|
});
|
|
writeStationListCache(deEndpoint, deStations);
|
|
|
|
RadioBackend backend;
|
|
backend.loadTopStations();
|
|
QTRY_VERIFY_WITH_TIMEOUT(backend.stations().size() == 3, 2000);
|
|
|
|
const int sharedTopIndex = visibleIndexForUrl(backend, sharedUrl);
|
|
QVERIFY(sharedTopIndex >= 0);
|
|
backend.playStation(sharedTopIndex);
|
|
QCOMPARE(backend.currentStationUrl(), sharedUrl);
|
|
QVERIFY(backend.hasNextStation());
|
|
|
|
backend.loadGermanStations();
|
|
QTRY_VERIFY_WITH_TIMEOUT(backend.stations().size() == 2, 2000);
|
|
QCOMPARE(backend.currentStationUrl(), sharedUrl);
|
|
QVERIFY(!backend.hasNextStation());
|
|
QVERIFY(backend.hasPreviousStation());
|
|
|
|
backend.playNextStation();
|
|
QCOMPARE(backend.currentStationUrl(), sharedUrl);
|
|
|
|
backend.playPreviousStation();
|
|
QCOMPARE(backend.currentStationUrl(), QStringLiteral("http://example.com/deonly"));
|
|
}
|
|
|
|
void RadioBackendPlaybackTest::list_next_disabled_when_playing_station_not_in_new_list()
|
|
{
|
|
const QString topEndpoint = QStringLiteral("/stations/topvote/100");
|
|
const QString deEndpoint = QStringLiteral("/stations/bycountrycodeexact/DE?limit=50&order=votes&reverse=true");
|
|
const QString topUrl = QStringLiteral("http://example.com/toponly");
|
|
|
|
writeStationCache(topEndpoint, QStringLiteral("Top Only"), topUrl, QStringLiteral("uuid-top"));
|
|
writeStationCache(deEndpoint, QStringLiteral("DE Only"), QStringLiteral("http://example.com/deonly"),
|
|
QStringLiteral("uuid-de"));
|
|
|
|
RadioBackend backend;
|
|
backend.loadTopStations();
|
|
QTRY_VERIFY_WITH_TIMEOUT(backend.stations().size() == 1, 2000);
|
|
backend.playStation(0);
|
|
QCOMPARE(backend.currentStationUrl(), topUrl);
|
|
QVERIFY(!backend.hasNextStation());
|
|
|
|
backend.loadGermanStations();
|
|
QTRY_VERIFY_WITH_TIMEOUT(backend.stations().size() == 1, 2000);
|
|
QCOMPARE(backend.currentStationUrl(), topUrl);
|
|
QVERIFY(!backend.hasNextStation());
|
|
QVERIFY(!backend.hasPreviousStation());
|
|
|
|
backend.playNextStation();
|
|
QCOMPARE(backend.currentStationUrl(), topUrl);
|
|
}
|
|
|
|
QTEST_MAIN(RadioBackendPlaybackTest)
|
|
#include "radiobackend_playback_test.moc" |