#include #include #include #include #include "core/task.hpp" #include "support/fixed_metrics.hpp" #include "ui/application.hpp" #include "ui/context.hpp" #include "ui/box.hpp" #include "ui/video_source.hpp" #include "ui/reveal.hpp" #include "ui/widgets.hpp" using namespace playfin; using platform::Action; using platform::Event; namespace { Event press(Action action) { Event event; event.type = Event::Type::ActionDown; event.action = action; return event; } Event typed(const std::string& text) { Event event; event.type = Event::Type::Text; event.text = text; return event; } struct ButtonsScreen : ui::Screen { ui::Button* first = nullptr; ui::Button* second = nullptr; int presses = 0; ButtonsScreen() { auto* row = add(ui::Axis::Row); first = row->add("One", [this] { ++presses; }); second = row->add("Two", [this] { ++presses; }); } }; struct Rig { core::WorkerPool pool{1}; core::MainQueue mainQueue; test::FixedMetrics metrics; std::unique_ptr app; Rig() { app = std::make_unique(ui::Application::Services{pool, mainQueue, metrics, nullptr}); } void frames(int count = 1) { for (int i = 0; i < count; ++i) { app->update(1000.0f / 60.0f); } } }; } // namespace TEST_CASE("a pushed screen gets the focus; Confirm presses the focused button") { Rig rig; auto* screen = static_cast(rig.app->push(std::make_unique())); rig.frames(); CHECK(rig.app->focus().focused() == screen->first); rig.app->handle(press(Action::Right)); CHECK(rig.app->focus().focused() == screen->second); rig.app->handle(press(Action::Confirm)); CHECK(screen->presses == 1); } TEST_CASE("Back pops screens and restores the previous focus") { Rig rig; auto* home = static_cast(rig.app->push(std::make_unique())); rig.frames(); rig.app->handle(press(Action::Right)); rig.app->push(std::make_unique()); rig.frames(); CHECK(rig.app->depth() == 2); rig.app->handle(press(Action::Back)); rig.frames(); CHECK(rig.app->depth() == 1); CHECK(rig.app->focus().focused() == home->second); rig.app->handle(press(Action::Back)); // the last screen stays CHECK(rig.app->depth() == 1); } TEST_CASE("a modal dialog keeps the focus until closed") { Rig rig; auto* home = static_cast(rig.app->push(std::make_unique())); rig.frames(); int confirmed = 0; rig.app->showModal(ui::makeDialog("Title", "Message", {{"OK", [&] { ++confirmed; }}, {"Cancel", [] {}}})); rig.frames(); CHECK(rig.app->hasModal()); rig.app->handle(press(Action::Down)); rig.app->handle(press(Action::Up)); CHECK_FALSE(home->isAncestorOf(rig.app->focus().focused())); rig.app->handle(press(Action::Confirm)); rig.frames(); CHECK(confirmed == 1); CHECK_FALSE(rig.app->hasModal()); CHECK(rig.app->focus().focused() == home->first); rig.app->showModal(ui::makeDialog("Title", "Message", {{"OK", [] {}}})); rig.frames(); rig.app->handle(press(Action::Back)); rig.frames(); CHECK_FALSE(rig.app->hasModal()); } TEST_CASE("the dialogs of a screen close when the screen goes, the ones below stay") { Rig rig; rig.app->push(std::make_unique()); rig.app->showModal(ui::makeDialog("Below", "", {{"OK", [] {}}})); rig.app->closeModal(); rig.app->push(std::make_unique()); rig.app->showModal(ui::makeDialog("Player", "", {{"OK", [] {}}})); rig.app->pop(); CHECK_FALSE(rig.app->hasModal()); rig.app->showModal(ui::makeDialog("Kept", "", {{"OK", [] {}}})); rig.app->push(std::make_unique()); rig.app->showModal(ui::makeDialog("Top", "", {{"OK", [] {}}})); rig.app->replaceTop(std::make_unique()); CHECK(rig.app->hasModal()); // "Kept" belongs to the screen below rig.app->closeModal(); CHECK_FALSE(rig.app->hasModal()); rig.app->showModal(ui::makeDialog("Any", "", {{"OK", [] {}}})); rig.app->replace(std::make_unique()); CHECK_FALSE(rig.app->hasModal()); } TEST_CASE("closing the screens hides and destroys them all, with their dialogs") { struct Watched : ButtonsScreen { int* hidden; bool* destroyed; Watched(int* h, bool* d) : hidden(h), destroyed(d) {} ~Watched() override { *destroyed = true; } void onHide() override { ++*hidden; } }; Rig rig; int hidden = 0; bool first = false; bool second = false; rig.app->push(std::make_unique(&hidden, &first)); rig.app->push(std::make_unique(&hidden, &second)); rig.app->showModal(ui::makeDialog("Open", "", {{"OK", [] {}}})); rig.app->closeScreens(); CHECK(rig.app->depth() == 0); CHECK_FALSE(rig.app->hasModal()); CHECK(first); CHECK(second); CHECK(hidden >= 2); } TEST_CASE("text fields edit with the PC keyboard on Linux") { Rig rig; struct FieldScreen : ui::Screen { ui::TextField* field = nullptr; std::string changed; FieldScreen() { platform::TextInputRequest request; request.title = "Name"; field = add("Name", request, [this](const std::string& value) { changed = value; }); } }; auto* screen = static_cast(rig.app->push(std::make_unique())); rig.frames(); rig.app->handle(press(Action::Confirm)); rig.frames(); CHECK(rig.app->hasModal()); rig.app->handle(typed("Ana")); rig.app->handle(typed("\xC3\xA9")); Event erase; erase.type = Event::Type::EditDelete; rig.app->handle(erase); rig.app->handle(typed("s")); rig.app->handle(press(Action::Confirm)); rig.frames(); CHECK_FALSE(rig.app->hasModal()); CHECK(screen->field->value() == "Anas"); CHECK(screen->changed == "Anas"); } namespace { int gFieldChanges = 0; struct CountingFieldScreen : ui::Screen { ui::TextField* field = nullptr; CountingFieldScreen() { platform::TextInputRequest request; request.title = "Name"; field = add("Name", request, [](const std::string&) { ++gFieldChanges; }); } }; } // namespace TEST_CASE("a text field destroyed while it is edited never gets the result") { Rig rig; gFieldChanges = 0; rig.app->push(std::make_unique()); rig.frames(); rig.app->handle(press(Action::Confirm)); rig.frames(); REQUIRE(rig.app->hasModal()); rig.app->replace(std::make_unique()); // e.g. a network reply moves on rig.frames(); rig.app->handle(typed("x")); rig.app->handle(press(Action::Confirm)); rig.frames(); CHECK(gFieldChanges == 0); CHECK_FALSE(rig.app->hasModal()); } TEST_CASE("an editor closed another way does not block the next one") { Rig rig; rig.app->push(std::make_unique()); rig.frames(); rig.app->handle(press(Action::Confirm)); rig.frames(); REQUIRE(rig.app->hasModal()); rig.app->closeModal(); rig.frames(); REQUIRE_FALSE(rig.app->hasModal()); rig.app->handle(press(Action::Confirm)); rig.frames(); CHECK(rig.app->hasModal()); } TEST_CASE("dialog buttons stay inside the card, however many there are") { Rig rig; rig.app->push(std::make_unique()); rig.app->showModal(ui::makeDialog("Menu", "Test Server", {{"Switch the current user", [] {}}, {"Switch to another server", [] {}}, {"Sign out of this server", [] {}}, {"Cancel", [] {}}})); rig.frames(2); ui::View* overlay = rig.app->root().children().back().get(); ui::View* card = overlay->children().front().get(); const ui::Rect box = card->absoluteFrame(); int buttons = 0; std::function visit = [&](ui::View& view) { if (dynamic_cast(&view) != nullptr) { ++buttons; const ui::Rect frame = view.absoluteFrame(); CHECK(frame.x >= box.x); CHECK(frame.x + frame.width <= box.x + box.width); } for (const auto& child : view.children()) { visit(*child); } }; visit(*card); CHECK(buttons == 4); } TEST_CASE("work posted to the main queue runs during update") { Rig rig; rig.app->push(std::make_unique()); bool ran = false; rig.mainQueue.post([&] { ran = true; }); rig.frames(); CHECK(ran); } namespace { // runs frames, drawing when asked, until the application settles (focus ring glide, first layout) void settle(Rig& rig) { for (int i = 0; i < 120; ++i) { rig.app->update(1000.0f / 60.0f); if (rig.app->needsDraw()) { rig.app->markDrawn(); } } } } // namespace TEST_CASE("an idle application stops asking to draw; input, jobs and spinners ask again") { Rig rig; rig.app->push(std::make_unique()); settle(rig); rig.app->update(1000.0f / 60.0f); CHECK_FALSE(rig.app->needsDraw()); rig.app->handle(press(Action::Right)); rig.app->update(1000.0f / 60.0f); CHECK(rig.app->needsDraw()); settle(rig); CHECK_FALSE(rig.app->needsDraw()); rig.mainQueue.post([] {}); rig.app->update(1000.0f / 60.0f); CHECK(rig.app->needsDraw()); settle(rig); rig.app->toast("hello", 100); CHECK(rig.app->needsDraw()); settle(rig); CHECK_FALSE(rig.app->needsDraw()); ui::context().redrawNextFrame = true; // what a spinner does while it is drawn rig.app->markDrawn(); CHECK(rig.app->needsDraw()); rig.app->markDrawn(); CHECK_FALSE(rig.app->needsDraw()); } namespace { struct StillVideo : ui::VideoSource { bool renderVideo() override { return false; } platform::VideoLayer videoLayer() const override { return {}; } }; } // namespace TEST_CASE("the application keeps the video source a screen registers") { Rig rig; StillVideo video; CHECK(rig.app->videoSource() == nullptr); rig.app->setVideoSource(&video); CHECK(rig.app->videoSource() == &video); CHECK(rig.app->root().background.a == 0); // the video shows through rig.app->setVideoSource(nullptr); CHECK(rig.app->videoSource() == nullptr); CHECK(rig.app->root().background.a > 0); } TEST_CASE("the halo background shows under the screens, never under a video") { Rig rig; CHECK_FALSE(rig.app->backgroundShown()); rig.app->setBackgroundImage(7, 1920, 1080); CHECK(rig.app->backgroundShown()); StillVideo video; rig.app->setVideoSource(&video); CHECK_FALSE(rig.app->backgroundShown()); CHECK(rig.app->clearColor().a == 0); rig.app->setVideoSource(nullptr); CHECK(rig.app->backgroundShown()); CHECK(rig.app->clearColor().r == ui::theme().background.r); CHECK(rig.app->clearColor().a == 255); } TEST_CASE("a pushed screen fades in and rises; the screen revealed by pop fades back in") { Rig rig; ui::Screen* first = rig.app->push(std::make_unique()); rig.frames(30); ui::Screen* second = rig.app->push(std::make_unique()); CHECK(second->opacity == doctest::Approx(0)); CHECK(second->shiftY == doctest::Approx(24)); rig.frames(22); // 367 ms CHECK(second->opacity == doctest::Approx(1)); CHECK(second->shiftY == doctest::Approx(0)); rig.app->pop(); CHECK(first->opacity == doctest::Approx(0)); rig.frames(8); // 133 ms CHECK(first->opacity > 0.5f); CHECK(first->opacity < 1.0f); rig.frames(8); CHECK(first->opacity == doctest::Approx(1)); } TEST_CASE("a replaced screen fades in without rising") { Rig rig; rig.app->push(std::make_unique()); ui::Screen* tab = rig.app->replace(std::make_unique()); CHECK(tab->opacity == doctest::Approx(0)); CHECK(tab->shiftY == doctest::Approx(0)); rig.frames(16); // 267 ms CHECK(tab->opacity == doctest::Approx(1)); } TEST_CASE("a screen can ask for a longer rise") { Rig rig; auto screen = std::make_unique(); screen->enterRise = 40; ui::Screen* pushed = rig.app->push(std::move(screen)); CHECK(pushed->shiftY == doctest::Approx(40)); } TEST_CASE("without transitions screens show at once") { Rig rig; rig.app->setTransitions(false); CHECK_FALSE(rig.app->transitions()); ui::Screen* screen = rig.app->push(std::make_unique()); CHECK(screen->opacity == doctest::Approx(1)); CHECK(screen->shiftY == doctest::Approx(0)); } TEST_CASE("rows arrive one after the other, at most twelve of them animated") { Rig rig; auto* screen = rig.app->push(std::make_unique()); auto* column = screen->add(ui::Axis::Column); for (int i = 0; i < 15; ++i) { column->add(ui::Axis::Row); } rig.frames(30); ui::revealChildren(*column, 1); const auto& rows = column->children(); CHECK(rows[0]->opacity == doctest::Approx(1)); // before `first`: untouched CHECK(rows[1]->opacity == doctest::Approx(0)); CHECK(rows[1]->shiftY == doctest::Approx(30)); CHECK(rows[13]->opacity == doctest::Approx(1)); // the 13th revealed row shows at once rig.frames(3); // 50 ms CHECK(rows[1]->opacity > 0); CHECK(rows[3]->opacity == doctest::Approx(0)); // starts 70 ms later rig.frames(42); // 750 ms: the 12th started at 385 ms CHECK(rows[12]->opacity == doctest::Approx(1)); CHECK(rows[12]->shiftY == doctest::Approx(0)); } TEST_CASE("without transitions rows show at once") { Rig rig; rig.app->setTransitions(false); auto* screen = rig.app->push(std::make_unique()); auto* row = screen->add(ui::Axis::Row); ui::revealChildren(*screen, 0); CHECK(row->opacity == doctest::Approx(1)); }