#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.push_back(track); } struct LibraryScene { Library library; FakeDevice device; std::unique_ptr view; std::unique_ptr 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(library, device); view->rebuildTracks(); driver = std::make_unique(*view, Vec2F(1100.0f, 700.0f)); } ListRow& row(size_t index) { return view->tracks().row(index); } const Track& track(size_t index) { return *view->visibleTracks()[index]; } // The playing track's row leads with the play glyph. bool showsPlaying(size_t index) { const ListRow::Leading& leading = row(index).leading(); return leading.kind == ListRow::Leading::Kind::Icon && leading.icon.kind == Icon::Kind::Glyph && leading.icon.glyph == Glyph::Play; } }; } 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.track(0).mName); scene.driver->key(Key::A, KeyModifiers::controlOnly()); scene.driver->type("album 4"); CHECK_EQUAL(1u, scene.view->visibleTracks().size()); CHECK_EQUAL("GATTI", scene.track(0).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.track(0).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.track(1)); 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.showsPlaying(1)); CHECK(!scene.showsPlaying(0)); } 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(Key::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.tracks().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.tracks().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(LibraryKeyboard) { TEST(ArrowsSelectAndEnterPlays) { LibraryScene scene; scene.driver->click(scene.row(0)); scene.driver->key(Key::Down); CHECK(scene.view->selectedTrack() == &scene.track(1)); scene.driver->key(Key::Enter); CHECK_EQUAL(2u, scene.device.playing); CHECK(scene.showsPlaying(1)); } TEST(SelectionSurvivesFiltering) { LibraryScene scene; scene.driver->click(scene.row(3)); REQUIRE CHECK(scene.view->selectedTrack() != nullptr); const Track* selected = scene.view->selectedTrack(); scene.driver->click(scene.view->search); scene.driver->type("gat"); REQUIRE CHECK_EQUAL(1u, scene.view->visibleTracks().size()); CHECK(scene.view->selectedTrack() == selected); CHECK(scene.view->tracks().current() == std::optional(0)); } } 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.track(0)); CHECK_EQUAL("SELECTED", info.state.text()); CHECK_EQUAL("Jumpsuit", info.title.text()); CHECK_EQUAL("twenty one pilots", info.artist.text()); const auto value = [&info](const std::string& name) { return static_cast(info.details.editor(name))->text(); }; CHECK_EQUAL("Album 1", value("Album")); CHECK_EQUAL("3:20", value("Length")); CHECK_EQUAL("yes", value("Loved")); 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); } }