Modules/LibraryViewer/tests/LibraryViewTests.cpp
Шурупов Илья Викторович 8842536836 Show song details in a docked Info panel in the library viewer
The library viewer is laid out in a dock area: the filter bar and
track list in the centre and an Info panel docked on the right, which
can be hidden, unpinned or floated like any dock panel and toggled from
View. It shows a cover art placeholder, whether the track is selected
or now playing, the title and artist, and album, genre, year, length,
plays, loved, file state and date added on separate lines.
2026-10-15 14:53:41 +03:00

293 lines
9.3 KiB
C++

#include "GUI.hpp"
#include "UiDriver.hpp"
#include "UnitTest++/UnitTest++.h"
using namespace tp;
using namespace tp::ui;
using namespace tp::ui::testing;
namespace {
class FakeDevice : public PlaybackDevice {
public:
bool start(SongId id) override {
if (id == unplayable) return false;
playing = id;
position = 0;
starts++;
return true;
}
[[nodiscard]] SongId playingId() const override { return playing; }
void resume() override { running = true; }
void pause() override { running = false; }
[[nodiscard]] halnf progress() const override { return position; }
void setProgress(halnf value) override { position = value; }
[[nodiscard]] halnf loaded() const override { return 1; }
void setVolume(halnf value) override { volume = value; }
SongId playing = 0;
SongId unplayable = 0;
halnf position = 0;
halnf volume = 1;
bool running = false;
int starts = 0;
};
void
addTrack(Library& library, SongId id, const std::string& name, const std::string& artist, bool loved, bool exists) {
Track track;
track.mId = id;
track.mName = name;
track.mArtist = artist;
track.mAlbum = "Album " + std::to_string(id);
track.mTotalTime = 200000;
track.mLoved = loved;
track.mExists = exists;
library.mTraks.append(track);
}
struct LibraryScene {
Library library;
FakeDevice device;
std::unique_ptr<LibraryWidget> view;
std::unique_ptr<UiDriver> driver;
LibraryScene() {
addTrack(library, 1, "Jumpsuit", "twenty one pilots", true, true);
addTrack(library, 2, "Mystery Lady", "Masego", false, true);
addTrack(library, 3, "No Idea", "Don Toliver", true, false);
addTrack(library, 4, "GATTI", "JACKBOYS", false, true);
view = std::make_unique<LibraryWidget>(library, device);
view->rebuildTracks();
driver = std::make_unique<UiDriver>(*view, Vec2F(1100.0f, 700.0f));
}
TrackWidget& row(size_t index) { return *view->visibleTracks()[index]; }
};
}
SUITE(LibraryFilters) {
TEST(SearchMatchesTitleArtistAndAlbumIgnoringCase) {
LibraryScene scene;
CHECK_EQUAL("4 of 4 tracks", scene.view->count.text());
scene.driver->click(scene.view->search);
scene.driver->type("don");
CHECK_EQUAL(1u, scene.view->visibleTracks().size());
CHECK_EQUAL("No Idea", scene.row(0).track().mName);
Modifiers control;
control.control = true;
scene.driver->key(InputID::A, control);
scene.driver->type("album 4");
CHECK_EQUAL(1u, scene.view->visibleTracks().size());
CHECK_EQUAL("GATTI", scene.row(0).track().mName);
}
TEST(LovedAndFileFiltersCombine) {
LibraryScene scene;
scene.driver->click(scene.view->lovedOnly);
CHECK_EQUAL(2u, scene.view->visibleTracks().size());
scene.driver->click(scene.view->files);
scene.driver->click(*scene.view->files.menu().items()[1]);
CHECK_EQUAL(1u, scene.view->visibleTracks().size());
CHECK_EQUAL("Jumpsuit", scene.row(0).track().mName);
CHECK_EQUAL("1 of 4 tracks", scene.view->count.text());
}
}
SUITE(LibraryPlayback) {
TEST(DoubleClickPlaysTrackInPlayerBar) {
LibraryScene scene;
scene.driver->click(scene.row(1));
CHECK(scene.view->selectedTrack() == &scene.row(1).track());
CHECK(scene.view->playingTrack() == nullptr);
const Vec2F center = scene.driver->centerOf(scene.row(1));
scene.driver->click(center);
scene.driver->click(center);
CHECK_EQUAL(2u, scene.device.playing);
CHECK(scene.device.running);
CHECK_EQUAL("Mystery Lady", scene.view->player.title.text());
CHECK_CLOSE(200, scene.view->player.seek.duration(), 0.01f);
CHECK(scene.view->player.isPlaying());
CHECK(scene.row(1).isPlaying());
CHECK(!scene.row(0).isPlaying());
}
TEST(PlayButtonStartsSelectionThenPauses) {
LibraryScene scene;
scene.driver->click(scene.row(2));
scene.driver->click(scene.view->player.playPause);
CHECK_EQUAL(3u, scene.device.playing);
scene.driver->click(scene.view->player.playPause);
CHECK(!scene.device.running);
CHECK(!scene.view->player.isPlaying());
scene.driver->key(InputID::SPACE);
CHECK(scene.device.running);
}
TEST(NextAndPreviousFollowVisibleOrderAndWrap) {
LibraryScene scene;
scene.driver->click(scene.view->player.next);
CHECK_EQUAL(1u, scene.device.playing);
scene.driver->click(scene.view->player.next);
CHECK_EQUAL(2u, scene.device.playing);
scene.driver->click(scene.view->player.previous);
scene.driver->click(scene.view->player.previous);
CHECK_EQUAL(4u, scene.device.playing);
}
TEST(ProgressSeekAndVolumeReachDevice) {
LibraryScene scene;
scene.driver->click(scene.view->player.playPause);
scene.device.position = 0.3f;
scene.driver->advance(32);
CHECK_CLOSE(0.3f, scene.view->player.seek.position(), 0.001f);
scene.driver->click(scene.view->player.seek.windowPointForPosition(0.75f));
CHECK_CLOSE(0.75f, scene.device.position, 0.01f);
scene.driver->click(scene.view->player.volume.muteButton());
CHECK_CLOSE(0, scene.device.volume, 0.001f);
}
TEST(FinishedTrackAdvancesToNext) {
LibraryScene scene;
scene.driver->click(scene.view->player.playPause);
CHECK_EQUAL(1u, scene.device.playing);
scene.device.position = 1;
scene.driver->advance(32);
CHECK_EQUAL(2u, scene.device.playing);
}
TEST(UnplayableTrackIsReported) {
LibraryScene scene;
scene.device.unplayable = 1;
scene.driver->click(scene.view->player.playPause);
CHECK(scene.view->playingTrack() == nullptr);
CHECK_EQUAL("Can't play \"Jumpsuit\"", scene.view->player.title.text());
}
TEST(MenusDrivePlayback) {
LibraryScene scene;
MenuBar bar;
scene.view->buildMenus(bar);
CHECK_EQUAL(3u, bar.titles().size());
scene.view->playbackMenu.items()[1]->activate();
CHECK_EQUAL(1u, scene.device.playing);
}
}
#include "ApplicationFrame.hpp"
SUITE(LibraryLayoutInFrame) {
TEST(ViewFillsFrameAndListTakesRemainingHeight) {
Library library;
FakeDevice device;
for (SongId id = 1; id <= 40; id++)
addTrack(library, id, "Track " + std::to_string(id), "Artist", id % 10 == 0, true);
LibraryWidget view(library, device);
view.rebuildTracks();
ApplicationFrame frame;
frame.setContent(&view);
UiDriver driver(frame, { 1500, 900 });
driver.settle();
const RectF bar = frame.menuBar.windowRect();
const RectF content = view.windowRect();
CHECK_CLOSE(1500, content.size.x, 0.5f);
CHECK_CLOSE(900, bar.size.y + content.size.y, 0.5f);
const RectF player = view.player.windowRect();
CHECK_CLOSE(900, player.pos.y + player.size.y, 0.5f);
CHECK_CLOSE(1500, player.size.x, 0.5f);
const RectF list = view.scroll().windowRect();
const RectF center = view.dock.centerRect();
CHECK_CLOSE(player.pos.y, list.pos.y + list.size.y, 0.5f);
CHECK_CLOSE(view.dock.windowPosition().x + center.pos.x + center.size.x, list.pos.x + list.size.x, 0.5f);
const RectF panel = view.infoPanel.windowRect();
CHECK(panel.pos.x > list.pos.x + list.size.x - 0.5f);
CHECK(panel.pos.x + panel.size.x > 1500 - 60);
}
TEST(ShortFilteredListStillLeavesPlayerAtBottom) {
Library library;
FakeDevice device;
for (SongId id = 1; id <= 40; id++)
addTrack(library, id, "Track " + std::to_string(id), "Artist", id % 10 == 0, true);
LibraryWidget view(library, device);
view.rebuildTracks();
ApplicationFrame frame;
frame.setContent(&view);
UiDriver driver(frame, { 1500, 900 });
driver.click(view.lovedOnly);
driver.settle();
CHECK_EQUAL(4u, view.visibleTracks().size());
const RectF player = view.player.windowRect();
const RectF list = view.scroll().windowRect();
CHECK_CLOSE(900, player.pos.y + player.size.y, 0.5f);
CHECK_CLOSE(1500, player.size.x, 0.5f);
CHECK_CLOSE(player.pos.y, list.pos.y + list.size.y, 0.5f);
}
}
SUITE(LibraryInfoPanel) {
TEST(InfoPanelIsDockedOpenOnTheRight) {
LibraryScene scene;
CHECK(scene.view->infoPanel.isOpen());
CHECK(scene.view->infoPanel.side() == DockSide::Right);
CHECK_EQUAL("Info", scene.view->infoPanel.title());
CHECK_EQUAL("No track selected", scene.view->info.title.text());
scene.driver->settle();
CHECK(scene.view->info.cover.size().x > 100);
}
TEST(SelectingShowsSongDetails) {
LibraryScene scene;
scene.driver->click(scene.row(0));
TrackInfoView& info = scene.view->info;
CHECK(info.track() == &scene.row(0).track());
CHECK_EQUAL("SELECTED", info.state.text());
CHECK_EQUAL("Jumpsuit", info.title.text());
CHECK_EQUAL("twenty one pilots", info.artist.text());
CHECK(info.details.text().find("Album: Album 1") != std::string::npos);
CHECK(info.details.text().find("Length: 3:20") != std::string::npos);
CHECK(info.details.text().find("Loved: yes") != std::string::npos);
CHECK(scene.driver->paint().hasText("Jumpsuit"));
}
TEST(PlayingTrackIsMarkedNowPlayingAndPauseClearsIt) {
LibraryScene scene;
scene.driver->click(scene.row(1));
scene.driver->click(scene.view->player.playPause);
CHECK_EQUAL("NOW PLAYING", scene.view->info.state.text());
scene.driver->click(scene.view->player.playPause);
CHECK_EQUAL("SELECTED", scene.view->info.state.text());
}
TEST(ViewMenuTogglesInfoPanel) {
LibraryScene scene;
MenuBar bar;
scene.view->buildMenus(bar);
scene.view->viewMenu.items()[0]->activate();
scene.driver->settle();
CHECK(!scene.view->infoPanel.isOpen());
scene.view->viewMenu.items()[0]->activate();
CHECK(scene.view->infoPanel.isOpen());
}
TEST(InfoPanelCanBeHiddenFromItsHeader) {
LibraryScene scene;
scene.driver->click(scene.view->infoPanel.hideButton());
CHECK(!scene.view->infoPanel.isOpen());
CHECK(scene.view->dock.stripeButton(scene.view->infoPanel) != nullptr);
}
}