// SPDX-FileCopyrightText: 2026 Reion Wong // SPDX-License-Identifier: BSD-3-Clause #include #include #include #include #include #include #include #include #include "base/dbus.h" #include "base/event_loop.h" #include "base/settings.h" #include "pages/language/language.h" #include "pages/language/preferredlanguagelist.h" #include "ui/application.h" #include "ui/keys.h" #include "ui/layout.h" #include "ui/test_input.h" #include "ui/window.h" using namespace cutefish; using namespace cutefish::ui; using namespace settings; using namespace std::chrono_literals; namespace { // Runs the loop until the condition holds or five seconds pass. bool waitFor(const std::function& condition) { base::EventLoop& loop = Application::instance()->loop(); const auto deadline = std::chrono::steady_clock::now() + 5s; while (!condition()) { if (std::chrono::steady_clock::now() > deadline) return false; const int timer = loop.startTimer(5ms, false, [&loop] { loop.quit(); }); loop.run(); loop.stopTimer(timer); } return true; } // cutefish-services' settings with the given languages, in a directory of its own. class LanguageConfig { public: explicit LanguageConfig(const std::vector& codes) { char pattern[] = "/tmp/settings-language-XXXXXX"; m_directory = mkdtemp(pattern); path = m_directory + "/services.conf"; base::Settings settings(path); settings.setValue("Language/language", codes.front()); settings.setValue("Language/preferredLanguages", codes); } ~LanguageConfig() { std::filesystem::remove_all(m_directory); } std::string path; private: std::string m_directory; }; std::vector codes(const std::vector& entries) { std::vector result; for (const LanguageEntry& entry : entries) result.push_back(entry.code); return result; } } // namespace TEST(Language, OpeningThePageDoesNotLoadTheCatalog) { LanguageConfig config({"en_US", "zh_Hans_CN"}); Language language(config.path); EXPECT_EQ(codes(language.preferredLanguages()), (std::vector{"en_US", "zh_Hans_CN"})); EXPECT_TRUE(language.availableLanguages().empty()); EXPECT_FALSE(language.isLoading()); } TEST(Language, CatalogAndSearch) { LanguageConfig config({"en_US", "zh_Hans_CN"}); Language language(config.path); language.loadLanguages(); ASSERT_TRUE(waitFor([&] { return !language.isLoading() && !language.availableLanguages().empty(); })); std::vector available = codes(language.availableLanguages()); ASSERT_EQ(available.size(), 40u); EXPECT_EQ(available.front(), "en_GB"); EXPECT_EQ(available[available.size() - 2], "zh_Hant_HK"); EXPECT_EQ(available.back(), "zh_Hant_TW"); // Preferred languages are left out even when searched for. language.search("zh_Hans_CN"); EXPECT_TRUE(language.availableLanguages().empty()); // A language's own name, its code, ignoring case and the space around it. language.search("Deutsch"); EXPECT_EQ(codes(language.availableLanguages()), std::vector{"de_DE"}); language.search("it_IT"); EXPECT_EQ(language.availableLanguages().size(), 1u); language.search("no-such-language"); EXPECT_TRUE(language.availableLanguages().empty()); language.search(" ID_id "); EXPECT_EQ(codes(language.availableLanguages()), std::vector{"id_ID"}); language.search(""); EXPECT_EQ(language.availableLanguages().size(), 40u); } TEST(Language, ClosingWhileLoadingLeavesTheCatalogForTheNextPage) { LanguageConfig config({"en_US", "zh_Hans_CN"}); auto language = std::make_unique(config.path); language->loadLanguages(); language.reset(); Language reopened(config.path); reopened.loadLanguages(); ASSERT_TRUE(waitFor([&] { return !reopened.isLoading() && reopened.availableLanguages().size() == 40; })); } TEST(Language, SavingWithoutTheServiceReportsAnError) { // Never against the live session, whose languages this would change. const base::dbus::Proxy daemon(base::dbus::Connection::session(), "org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus"); if (daemon.call("NameHasOwner", std::string("org.cutefish.Services")).value().value_or(false)) GTEST_SKIP() << "Needs a private session bus: run the tests in dbus-run-session"; LanguageConfig config({"en_US", "de_DE"}); Language language(config.path); language.moveLanguage(1, 0); ASSERT_TRUE(waitFor([&] { return !language.isSaving(); })); EXPECT_EQ(language.errorMessage(), "Could not save preferred languages. Please try again."); EXPECT_EQ(codes(language.preferredLanguages()), (std::vector{"en_US", "de_DE"})); } namespace { // The preferred language list in a window at 150%, its moves recorded. struct DragFixture : ::testing::Test { explicit DragFixture(std::vector languages = {"en_US", "ja_JP", "de_DE"}) : config(languages) , language(config.path) { window.setScale(1.5); auto root = std::make_unique(); root->setMargins({20, 20, 20, 20}); list = root->addChild(language); list->onMoveRequested = [this](int from, int to) { moves.emplace_back(from, to); }; window.setContentView(std::move(root)); window.resize({520, 440}); window.grab(); } // The middle of a row's drag handle, in window coordinates. PointF handle(int index) const { const RectF rect = list->geometry(); return {rect.right() - 16 + 9, rect.y + PreferredLanguageList::kRowHeight * (index + 0.5)}; } double rowY(double rows) const { return list->geometry().y + PreferredLanguageList::kRowHeight * rows; } PointF startDrag(int index) { const PointF point = handle(index); TestInput::movePointer(window, point); TestInput::press(window, point); TestInput::movePointer(window, {point.x, point.y + 16}); EXPECT_TRUE(list->isDragging()); return point; } LanguageConfig config; Language language; Window window; PreferredLanguageList* list; std::vector> moves; }; struct LongDragFixture : DragFixture { LongDragFixture() : DragFixture({"en_US", "en_GB", "ja_JP", "ko_KR", "fr_FR", "de_DE", "es_ES", "pt_BR", "ru_RU", "it_IT", "id_ID", "vi_VN"}) { } }; } // namespace TEST_F(DragFixture, MovesDown) { const PointF point = startDrag(0); TestInput::movePointer(window, {point.x, rowY(2.5)}); EXPECT_EQ(list->targetIndex(), 2); EXPECT_TRUE(moves.empty()); TestInput::release(window, {point.x, rowY(2.5)}); EXPECT_EQ(moves, (std::vector>{{0, 2}})); EXPECT_FALSE(list->isDragging()); } TEST_F(DragFixture, MovesUp) { const PointF point = startDrag(2); TestInput::movePointer(window, {point.x, rowY(0.5)}); TestInput::release(window, {point.x, rowY(0.5)}); EXPECT_EQ(moves, (std::vector>{{2, 0}})); } TEST_F(DragFixture, EscapeCancels) { const PointF point = startDrag(0); TestInput::movePointer(window, {point.x, rowY(2.5)}); TestInput::typeKey(window, key::Escape); EXPECT_FALSE(list->isDragging()); TestInput::release(window, {point.x, rowY(2.5)}); EXPECT_TRUE(moves.empty()); } TEST_F(DragFixture, ReleasingOutsideCancels) { const PointF point = startDrag(0); TestInput::movePointer(window, {list->geometry().x - 10, rowY(2.5)}); TestInput::release(window, {list->geometry().x - 10, rowY(2.5)}); EXPECT_TRUE(moves.empty()); } TEST_F(DragFixture, DroppingInPlaceSavesNothing) { const PointF point = startDrag(0); TestInput::movePointer(window, point); TestInput::release(window, point); EXPECT_TRUE(moves.empty()); } TEST_F(DragFixture, KeysMoveTheFocusedRow) { TestInput::click(window, handle(1)); TestInput::typeKey(window, key::Up); TestInput::typeKey(window, key::Down); EXPECT_EQ(moves, (std::vector>{{1, 0}, {1, 2}})); } TEST_F(LongDragFixture, ScrollsAtTheEdge) { // Seven rows show; the rest scroll. EXPECT_EQ(list->geometry().height, PreferredLanguageList::kRowHeight * 7); const PointF point = startDrag(0); const PointF bottom{point.x, list->geometry().bottom() - 1}; TestInput::movePointer(window, bottom); ASSERT_TRUE(waitFor([&] { return list->scrollY() > PreferredLanguageList::kRowHeight; })); TestInput::release(window, bottom); ASSERT_EQ(moves.size(), 1u); EXPECT_EQ(moves.front().first, 0); EXPECT_GT(moves.front().second, 0); } TEST_F(DragFixture, ReleasingAboveMovesToTheTop) { const PointF point = startDrag(2); TestInput::movePointer(window, {point.x, rowY(0.5)}); TestInput::movePointer(window, {point.x, list->geometry().y - 20}); EXPECT_EQ(list->targetIndex(), 0); TestInput::release(window, {point.x, list->geometry().y - 20}); EXPECT_EQ(moves, (std::vector>{{2, 0}})); } TEST_F(DragFixture, ReleasingBelowMovesToTheBottom) { const PointF point = startDrag(0); const double below = list->geometry().bottom() + 20; TestInput::movePointer(window, {point.x, below}); EXPECT_EQ(list->targetIndex(), list->count() - 1); TestInput::release(window, {point.x, below}); EXPECT_EQ(moves, (std::vector>{{0, list->count() - 1}})); }