From 51f2c4d1c06eda88df82307dbb3df504f96a1dfc Mon Sep 17 00:00:00 2001 From: Daniel Sage <24928223+dnsge@users.noreply.github.com> Date: Sun, 21 May 2023 06:10:49 -0400 Subject: [PATCH] Add input completion test suite (#4644) Co-authored-by: Rasmus Karlsson --- CHANGELOG.md | 1 + benchmarks/src/main.cpp | 2 +- mocks/include/mocks/EmptyApplication.hpp | 2 +- src/Application.cpp | 5 + src/Application.hpp | 8 +- src/common/CompletionModel.cpp | 31 +- src/common/CompletionModel.hpp | 6 + src/providers/bttv/BttvEmotes.cpp | 9 +- src/providers/bttv/BttvEmotes.hpp | 1 + src/providers/emoji/Emojis.cpp | 14 +- src/providers/emoji/Emojis.hpp | 22 +- src/providers/ffz/FfzEmotes.cpp | 9 +- src/providers/ffz/FfzEmotes.hpp | 1 + src/providers/seventv/SeventvEmotes.cpp | 10 +- src/providers/seventv/SeventvEmotes.hpp | 1 + src/providers/twitch/TwitchIrcServer.hpp | 22 +- src/providers/twitch/api/README.md | 2 +- src/singletons/Emotes.hpp | 6 + src/widgets/splits/InputCompletionPopup.cpp | 68 ++-- src/widgets/splits/InputCompletionPopup.hpp | 17 + tests/CMakeLists.txt | 1 + tests/src/HighlightController.cpp | 1 - tests/src/InputCompletion.cpp | 336 ++++++++++++++++++++ tests/src/TwitchMessageBuilder.cpp | 2 +- 24 files changed, 514 insertions(+), 63 deletions(-) create mode 100644 tests/src/InputCompletion.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index e6ca8d19e..bd82de959 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Minor: Added `/shoutout ` commands to shoutout specified user. (#4638) - Dev: Added command to set Qt's logging filter/rules at runtime (`/c2-set-logging-rules`). (#4637) - Dev: Added the ability to see & load custom themes from the Themes directory. No stable promises are made of this feature, changes might be made that breaks custom themes without notice. (#4570) +- Dev: Added test cases for emote and tab completion. (#4644) ## 2.4.4 diff --git a/benchmarks/src/main.cpp b/benchmarks/src/main.cpp index cc4491f22..5f4c967f8 100644 --- a/benchmarks/src/main.cpp +++ b/benchmarks/src/main.cpp @@ -13,7 +13,7 @@ int main(int argc, char **argv) ::benchmark::Initialize(&argc, argv); // Ensure settings are initialized before any tests are run - chatterino::Settings settings("/tmp/c2-empty-test"); + chatterino::Settings settings("/tmp/c2-empty-mock"); QtConcurrent::run([&app] { ::benchmark::RunSpecifiedBenchmarks(); diff --git a/mocks/include/mocks/EmptyApplication.hpp b/mocks/include/mocks/EmptyApplication.hpp index 846fd0aaa..4dabc4ffa 100644 --- a/mocks/include/mocks/EmptyApplication.hpp +++ b/mocks/include/mocks/EmptyApplication.hpp @@ -57,7 +57,7 @@ public: return nullptr; } - TwitchIrcServer *getTwitch() override + ITwitchIrcServer *getTwitch() override { return nullptr; } diff --git a/src/Application.cpp b/src/Application.cpp index b794a7966..248cb0d86 100644 --- a/src/Application.cpp +++ b/src/Application.cpp @@ -245,6 +245,11 @@ IUserDataController *Application::getUserData() return this->userData; } +ITwitchIrcServer *Application::getTwitch() +{ + return this->twitch; +} + void Application::save() { for (auto &singleton : this->singletons_) diff --git a/src/Application.hpp b/src/Application.hpp index 7c5525505..27f1c2f60 100644 --- a/src/Application.hpp +++ b/src/Application.hpp @@ -10,6 +10,7 @@ namespace chatterino { class TwitchIrcServer; +class ITwitchIrcServer; class PubSub; class CommandController; @@ -55,7 +56,7 @@ public: virtual CommandController *getCommands() = 0; virtual HighlightController *getHighlights() = 0; virtual NotificationController *getNotifications() = 0; - virtual TwitchIrcServer *getTwitch() = 0; + virtual ITwitchIrcServer *getTwitch() = 0; virtual ChatterinoBadges *getChatterinoBadges() = 0; virtual FfzBadges *getFfzBadges() = 0; virtual IUserDataController *getUserData() = 0; @@ -141,10 +142,7 @@ public: { return this->highlights; } - TwitchIrcServer *getTwitch() override - { - return this->twitch; - } + ITwitchIrcServer *getTwitch() override; ChatterinoBadges *getChatterinoBadges() override { return this->chatterinoBadges; diff --git a/src/common/CompletionModel.cpp b/src/common/CompletionModel.cpp index 9b123aa4c..61f1cc249 100644 --- a/src/common/CompletionModel.cpp +++ b/src/common/CompletionModel.cpp @@ -92,6 +92,7 @@ void CompletionModel::refresh(const QString &prefix, bool isFirstWord) return; } + auto *app = getIApp(); // Twitch channel auto *tc = dynamic_cast(&this->channel_); @@ -130,7 +131,7 @@ void CompletionModel::refresh(const QString &prefix, bool isFirstWord) } }; - if (auto account = getApp()->accounts->twitch.getCurrent()) + if (auto account = app->getAccounts()->twitch.getCurrent()) { // Twitch Emotes available globally for (const auto &emote : account->accessEmotes()->emotes) @@ -153,18 +154,18 @@ void CompletionModel::refresh(const QString &prefix, bool isFirstWord) // 7TV Global for (const auto &emote : - *getApp()->twitch->getSeventvEmotes().globalEmotes()) + *app->getTwitch()->getSeventvEmotes().globalEmotes()) { addString(emote.first.string, TaggedString::Type::SeventvGlobalEmote); } // Bttv Global - for (const auto &emote : *getApp()->twitch->getBttvEmotes().emotes()) + for (const auto &emote : *app->getTwitch()->getBttvEmotes().emotes()) { addString(emote.first.string, TaggedString::Type::BTTVChannelEmote); } // Ffz Global - for (const auto &emote : *getApp()->twitch->getFfzEmotes().emotes()) + for (const auto &emote : *app->getTwitch()->getFfzEmotes().emotes()) { addString(emote.first.string, TaggedString::Type::FFZChannelEmote); } @@ -172,7 +173,8 @@ void CompletionModel::refresh(const QString &prefix, bool isFirstWord) // Emojis if (prefix.startsWith(":")) { - const auto &emojiShortCodes = getApp()->emotes->emojis.shortCodes; + const auto &emojiShortCodes = + app->getEmotes()->getEmojis()->getShortCodes(); for (const auto &m : emojiShortCodes) { addString(QString(":%1:").arg(m), TaggedString::Type::Emoji); @@ -231,20 +233,20 @@ void CompletionModel::refresh(const QString &prefix, bool isFirstWord) addString(emote.first.string, TaggedString::Type::BTTVGlobalEmote); } #ifdef CHATTERINO_HAVE_PLUGINS - for (const auto &command : getApp()->commands->pluginCommands()) + for (const auto &command : app->getCommands()->pluginCommands()) { addString(command, TaggedString::PluginCommand); } #endif // Custom Chatterino commands - for (const auto &command : getApp()->commands->items) + for (const auto &command : app->getCommands()->items) { addString(command.name, TaggedString::CustomCommand); } // Default Chatterino commands for (const auto &command : - getApp()->commands->getDefaultChatterinoCommandList()) + app->getCommands()->getDefaultChatterinoCommandList()) { addString(command, TaggedString::ChatterinoCommand); } @@ -256,6 +258,19 @@ void CompletionModel::refresh(const QString &prefix, bool isFirstWord) } } +std::vector CompletionModel::allItems() const +{ + std::shared_lock lock(this->itemsMutex_); + + std::vector results; + results.reserve(this->items_.size()); + for (const auto &item : this->items_) + { + results.push_back(item.string); + } + return results; +} + bool CompletionModel::compareStrings(const QString &a, const QString &b) { // try comparing insensitively, if they are the same then senstively diff --git a/src/common/CompletionModel.hpp b/src/common/CompletionModel.hpp index 5b46fb2de..affbd3f10 100644 --- a/src/common/CompletionModel.hpp +++ b/src/common/CompletionModel.hpp @@ -6,6 +6,8 @@ #include #include +class InputCompletionTest; + namespace chatterino { class Channel; @@ -60,10 +62,14 @@ public: static bool compareStrings(const QString &a, const QString &b); private: + std::vector allItems() const; + mutable std::shared_mutex itemsMutex_; std::set items_; Channel &channel_; + + friend class ::InputCompletionTest; }; } // namespace chatterino diff --git a/src/providers/bttv/BttvEmotes.cpp b/src/providers/bttv/BttvEmotes.cpp index f214d4177..b1b6fb2e7 100644 --- a/src/providers/bttv/BttvEmotes.cpp +++ b/src/providers/bttv/BttvEmotes.cpp @@ -193,7 +193,7 @@ void BttvEmotes::loadEmotes() { if (!Settings::instance().enableBTTVGlobalEmotes) { - this->global_.set(EMPTY_EMOTE_MAP); + this->setEmotes(EMPTY_EMOTE_MAP); return; } @@ -203,13 +203,18 @@ void BttvEmotes::loadEmotes() auto emotes = this->global_.get(); auto pair = parseGlobalEmotes(result.parseJsonArray(), *emotes); if (pair.first) - this->global_.set( + this->setEmotes( std::make_shared(std::move(pair.second))); return pair.first; }) .execute(); } +void BttvEmotes::setEmotes(std::shared_ptr emotes) +{ + this->global_.set(std::move(emotes)); +} + void BttvEmotes::loadChannel(std::weak_ptr channel, const QString &channelId, const QString &channelDisplayName, diff --git a/src/providers/bttv/BttvEmotes.hpp b/src/providers/bttv/BttvEmotes.hpp index bca2d4b65..bbdcacccb 100644 --- a/src/providers/bttv/BttvEmotes.hpp +++ b/src/providers/bttv/BttvEmotes.hpp @@ -29,6 +29,7 @@ public: std::shared_ptr emotes() const; boost::optional emote(const EmoteName &name) const; void loadEmotes(); + void setEmotes(std::shared_ptr emotes); static void loadChannel(std::weak_ptr channel, const QString &channelId, const QString &channelDisplayName, diff --git a/src/providers/emoji/Emojis.cpp b/src/providers/emoji/Emojis.cpp index 72c429837..7872d6e68 100644 --- a/src/providers/emoji/Emojis.cpp +++ b/src/providers/emoji/Emojis.cpp @@ -265,7 +265,7 @@ void Emojis::loadEmojiSet() } std::vector> Emojis::parse( - const QString &text) + const QString &text) const { auto result = std::vector>(); int lastParsedEmojiEndIndex = 0; @@ -359,7 +359,7 @@ std::vector> Emojis::parse( return result; } -QString Emojis::replaceShortCodes(const QString &text) +QString Emojis::replaceShortCodes(const QString &text) const { QString ret(text); auto it = this->findShortCodesRegex_.globalMatch(text); @@ -393,4 +393,14 @@ QString Emojis::replaceShortCodes(const QString &text) return ret; } +const EmojiMap &Emojis::getEmojis() const +{ + return this->emojis; +} + +const std::vector &Emojis::getShortCodes() const +{ + return this->shortCodes; +} + } // namespace chatterino diff --git a/src/providers/emoji/Emojis.hpp b/src/providers/emoji/Emojis.hpp index 2f1679b6e..217aa1f4a 100644 --- a/src/providers/emoji/Emojis.hpp +++ b/src/providers/emoji/Emojis.hpp @@ -37,16 +37,32 @@ struct EmojiData { using EmojiMap = ConcurrentMap>; -class Emojis +class IEmojis +{ +public: + virtual ~IEmojis() = default; + + virtual std::vector> parse( + const QString &text) const = 0; + virtual const EmojiMap &getEmojis() const = 0; + virtual const std::vector &getShortCodes() const = 0; + virtual QString replaceShortCodes(const QString &text) const = 0; +}; + +class Emojis : public IEmojis { public: void initialize(); void load(); - std::vector> parse(const QString &text); + std::vector> parse( + const QString &text) const override; EmojiMap emojis; std::vector shortCodes; - QString replaceShortCodes(const QString &text); + QString replaceShortCodes(const QString &text) const override; + + const EmojiMap &getEmojis() const override; + const std::vector &getShortCodes() const override; private: void loadEmojis(); diff --git a/src/providers/ffz/FfzEmotes.cpp b/src/providers/ffz/FfzEmotes.cpp index 180628545..73be0b4dd 100644 --- a/src/providers/ffz/FfzEmotes.cpp +++ b/src/providers/ffz/FfzEmotes.cpp @@ -188,7 +188,7 @@ void FfzEmotes::loadEmotes() { if (!Settings::instance().enableFFZGlobalEmotes) { - this->global_.set(EMPTY_EMOTE_MAP); + this->setEmotes(EMPTY_EMOTE_MAP); return; } @@ -199,13 +199,18 @@ void FfzEmotes::loadEmotes() .timeout(30000) .onSuccess([this](auto result) -> Outcome { auto parsedSet = parseGlobalEmotes(result.parseJson()); - this->global_.set(std::make_shared(std::move(parsedSet))); + this->setEmotes(std::make_shared(std::move(parsedSet))); return Success; }) .execute(); } +void FfzEmotes::setEmotes(std::shared_ptr emotes) +{ + this->global_.set(std::move(emotes)); +} + void FfzEmotes::loadChannel( std::weak_ptr channel, const QString &channelID, std::function emoteCallback, diff --git a/src/providers/ffz/FfzEmotes.hpp b/src/providers/ffz/FfzEmotes.hpp index be0726f04..e2865fcb5 100644 --- a/src/providers/ffz/FfzEmotes.hpp +++ b/src/providers/ffz/FfzEmotes.hpp @@ -22,6 +22,7 @@ public: std::shared_ptr emotes() const; boost::optional emote(const EmoteName &name) const; void loadEmotes(); + void setEmotes(std::shared_ptr emotes); static void loadChannel( std::weak_ptr channel, const QString &channelId, std::function emoteCallback, diff --git a/src/providers/seventv/SeventvEmotes.cpp b/src/providers/seventv/SeventvEmotes.cpp index 2f7883abc..321e43a65 100644 --- a/src/providers/seventv/SeventvEmotes.cpp +++ b/src/providers/seventv/SeventvEmotes.cpp @@ -275,7 +275,7 @@ void SeventvEmotes::loadGlobalEmotes() { if (!Settings::instance().enableSevenTVGlobalEmotes) { - this->global_.set(EMPTY_EMOTE_MAP); + this->setGlobalEmotes(EMPTY_EMOTE_MAP); return; } @@ -289,7 +289,8 @@ void SeventvEmotes::loadGlobalEmotes() auto emoteMap = parseEmotes(parsedEmotes, true); qCDebug(chatterinoSeventv) << "Loaded" << emoteMap.size() << "7TV Global Emotes"; - this->global_.set(std::make_shared(std::move(emoteMap))); + this->setGlobalEmotes( + std::make_shared(std::move(emoteMap))); return Success; }) @@ -300,6 +301,11 @@ void SeventvEmotes::loadGlobalEmotes() .execute(); } +void SeventvEmotes::setGlobalEmotes(std::shared_ptr emotes) +{ + this->global_.set(std::move(emotes)); +} + void SeventvEmotes::loadChannelEmotes( const std::weak_ptr &channel, const QString &channelId, std::function callback, bool manualRefresh) diff --git a/src/providers/seventv/SeventvEmotes.hpp b/src/providers/seventv/SeventvEmotes.hpp index f978337be..7fea024bb 100644 --- a/src/providers/seventv/SeventvEmotes.hpp +++ b/src/providers/seventv/SeventvEmotes.hpp @@ -75,6 +75,7 @@ public: std::shared_ptr globalEmotes() const; boost::optional globalEmote(const EmoteName &name) const; void loadGlobalEmotes(); + void setGlobalEmotes(std::shared_ptr emotes); static void loadChannelEmotes( const std::weak_ptr &channel, const QString &channelId, std::function callback, diff --git a/src/providers/twitch/TwitchIrcServer.hpp b/src/providers/twitch/TwitchIrcServer.hpp index 9a9a22800..4593c48ba 100644 --- a/src/providers/twitch/TwitchIrcServer.hpp +++ b/src/providers/twitch/TwitchIrcServer.hpp @@ -23,7 +23,21 @@ class TwitchChannel; class BttvLiveUpdates; class SeventvEventAPI; -class TwitchIrcServer final : public AbstractIrcServer, public Singleton +class ITwitchIrcServer +{ +public: + virtual ~ITwitchIrcServer() = default; + + virtual const BttvEmotes &getBttvEmotes() const = 0; + virtual const FfzEmotes &getFfzEmotes() const = 0; + virtual const SeventvEmotes &getSeventvEmotes() const = 0; + + // Update this interface with TwitchIrcServer methods as needed +}; + +class TwitchIrcServer final : public AbstractIrcServer, + public Singleton, + public ITwitchIrcServer { public: TwitchIrcServer(); @@ -70,9 +84,9 @@ public: std::unique_ptr bttvLiveUpdates; std::unique_ptr seventvEventAPI; - const BttvEmotes &getBttvEmotes() const; - const FfzEmotes &getFfzEmotes() const; - const SeventvEmotes &getSeventvEmotes() const; + const BttvEmotes &getBttvEmotes() const override; + const FfzEmotes &getFfzEmotes() const override; + const SeventvEmotes &getSeventvEmotes() const override; protected: virtual void initializeConnection(IrcConnection *connection, diff --git a/src/providers/twitch/api/README.md b/src/providers/twitch/api/README.md index 83bc45e87..b031adea6 100644 --- a/src/providers/twitch/api/README.md +++ b/src/providers/twitch/api/README.md @@ -12,7 +12,7 @@ If you're adding support for a new endpoint, these are the things you should kno 1. Add a virtual function in the `IHelix` class. Naming should reflect the API name as best as possible. 1. Override the virtual function in the `Helix` class. -1. Mock the function in the `MockHelix` class in the `tests/src/HighlightController.cpp` file. +1. Mock the function in the `mock::Helix` class in the `mocks/include/mocks/Helix.hpp` file. 1. (Optional) Make a new error enum for the failure callback. For a simple example, see the `updateUserChatColor` function and its error enum `HelixUpdateUserChatColorError`. diff --git a/src/singletons/Emotes.hpp b/src/singletons/Emotes.hpp index 1a65a17d0..f74dab873 100644 --- a/src/singletons/Emotes.hpp +++ b/src/singletons/Emotes.hpp @@ -16,6 +16,7 @@ public: virtual ~IEmotes() = default; virtual ITwitchEmotes *getTwitchEmotes() = 0; + virtual IEmojis *getEmojis() = 0; }; class Emotes final : public IEmotes, public Singleton @@ -32,6 +33,11 @@ public: return &this->twitch; } + IEmojis *getEmojis() final + { + return &this->emojis; + } + TwitchEmotes twitch; Emojis emojis; diff --git a/src/widgets/splits/InputCompletionPopup.cpp b/src/widgets/splits/InputCompletionPopup.cpp index eb6a2ace7..28eb829ae 100644 --- a/src/widgets/splits/InputCompletionPopup.cpp +++ b/src/widgets/splits/InputCompletionPopup.cpp @@ -17,12 +17,7 @@ namespace { using namespace chatterino; - -struct CompletionEmote { - EmotePtr emote; - QString displayName; - QString providerName; -}; +using namespace chatterino::detail; void addEmotes(std::vector &out, const EmoteMap &map, const QString &text, const QString &providerName) @@ -53,33 +48,18 @@ void addEmojis(std::vector &out, const EmojiMap &map, } // namespace -namespace chatterino { +namespace chatterino::detail { -InputCompletionPopup::InputCompletionPopup(QWidget *parent) - : BasePopup({BasePopup::EnableCustomFrame, BasePopup::Frameless, - BasePopup::DontFocus, BaseWindow::DisableLayoutSave}, - parent) - , model_(this) -{ - this->initLayout(); - - QObject::connect(&this->redrawTimer_, &QTimer::timeout, this, [this] { - if (this->isVisible()) - { - this->ui_.listView->doItemsLayout(); - } - }); - this->redrawTimer_.setInterval(33); -} - -void InputCompletionPopup::updateEmotes(const QString &text, ChannelPtr channel) +std::vector buildCompletionEmoteList(const QString &text, + ChannelPtr channel) { std::vector emotes; + auto *app = getIApp(); auto *tc = dynamic_cast(channel.get()); // returns true also for special Twitch channels (/live, /mentions, /whispers, etc.) if (channel->isTwitchChannel()) { - if (auto user = getApp()->accounts->twitch.getCurrent()) + if (auto user = app->getAccounts()->twitch.getCurrent()) { // Twitch Emotes available globally auto emoteData = user->accessEmotes(); @@ -115,21 +95,21 @@ void InputCompletionPopup::updateEmotes(const QString &text, ChannelPtr channel) } } - if (auto bttvG = getApp()->twitch->getBttvEmotes().emotes()) + if (auto bttvG = app->getTwitch()->getBttvEmotes().emotes()) { addEmotes(emotes, *bttvG, text, "Global BetterTTV"); } - if (auto ffzG = getApp()->twitch->getFfzEmotes().emotes()) + if (auto ffzG = app->getTwitch()->getFfzEmotes().emotes()) { addEmotes(emotes, *ffzG, text, "Global FrankerFaceZ"); } - if (auto seventvG = getApp()->twitch->getSeventvEmotes().globalEmotes()) + if (auto seventvG = app->getTwitch()->getSeventvEmotes().globalEmotes()) { addEmotes(emotes, *seventvG, text, "Global 7TV"); } } - addEmojis(emotes, getApp()->emotes->emojis.emojis, text); + addEmojis(emotes, app->getEmotes()->getEmojis()->getEmojis(), text); // if there is an exact match, put that emote first for (size_t i = 1; i < emotes.size(); i++) @@ -147,6 +127,34 @@ void InputCompletionPopup::updateEmotes(const QString &text, ChannelPtr channel) } } + return emotes; +} + +} // namespace chatterino::detail + +namespace chatterino { + +InputCompletionPopup::InputCompletionPopup(QWidget *parent) + : BasePopup({BasePopup::EnableCustomFrame, BasePopup::Frameless, + BasePopup::DontFocus, BaseWindow::DisableLayoutSave}, + parent) + , model_(this) +{ + this->initLayout(); + + QObject::connect(&this->redrawTimer_, &QTimer::timeout, this, [this] { + if (this->isVisible()) + { + this->ui_.listView->doItemsLayout(); + } + }); + this->redrawTimer_.setInterval(33); +} + +void InputCompletionPopup::updateEmotes(const QString &text, ChannelPtr channel) +{ + auto emotes = detail::buildCompletionEmoteList(text, std::move(channel)); + this->model_.clear(); int count = 0; diff --git a/src/widgets/splits/InputCompletionPopup.hpp b/src/widgets/splits/InputCompletionPopup.hpp index a75b64563..9f36bb5ae 100644 --- a/src/widgets/splits/InputCompletionPopup.hpp +++ b/src/widgets/splits/InputCompletionPopup.hpp @@ -5,12 +5,29 @@ #include #include +#include namespace chatterino { class Channel; using ChannelPtr = std::shared_ptr; +struct Emote; +using EmotePtr = std::shared_ptr; + +namespace detail { + + struct CompletionEmote { + EmotePtr emote; + QString displayName; + QString providerName; + }; + + std::vector buildCompletionEmoteList(const QString &text, + ChannelPtr channel); + +} // namespace detail + class GenericListView; class InputCompletionPopup : public BasePopup diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 4f4cd2292..ce1677ef3 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -26,6 +26,7 @@ set(test_SOURCES ${CMAKE_CURRENT_LIST_DIR}/src/Updates.cpp ${CMAKE_CURRENT_LIST_DIR}/src/Filters.cpp ${CMAKE_CURRENT_LIST_DIR}/src/LinkParser.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/InputCompletion.cpp # Add your new file above this line! ) diff --git a/tests/src/HighlightController.cpp b/tests/src/HighlightController.cpp index b2f89e7a5..dc31aad00 100644 --- a/tests/src/HighlightController.cpp +++ b/tests/src/HighlightController.cpp @@ -1,6 +1,5 @@ #include "controllers/highlights/HighlightController.hpp" -#include "Application.hpp" #include "BaseSettings.hpp" #include "controllers/accounts/AccountController.hpp" #include "controllers/highlights/HighlightPhrase.hpp" diff --git a/tests/src/InputCompletion.cpp b/tests/src/InputCompletion.cpp new file mode 100644 index 000000000..e02edb145 --- /dev/null +++ b/tests/src/InputCompletion.cpp @@ -0,0 +1,336 @@ +#include "Application.hpp" +#include "BaseSettings.hpp" +#include "common/Aliases.hpp" +#include "common/CompletionModel.hpp" +#include "controllers/accounts/AccountController.hpp" +#include "messages/Emote.hpp" +#include "mocks/EmptyApplication.hpp" +#include "mocks/Helix.hpp" +#include "providers/twitch/TwitchIrcServer.hpp" +#include "singletons/Emotes.hpp" +#include "singletons/Paths.hpp" +#include "singletons/Settings.hpp" +#include "widgets/splits/InputCompletionPopup.hpp" + +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace chatterino; +using ::testing::Exactly; + +class MockTwitchIrcServer : public ITwitchIrcServer +{ +public: + const BttvEmotes &getBttvEmotes() const override + { + return this->bttv; + } + + const FfzEmotes &getFfzEmotes() const override + { + return this->ffz; + } + + const SeventvEmotes &getSeventvEmotes() const override + { + return this->seventv; + } + + BttvEmotes bttv; + FfzEmotes ffz; + SeventvEmotes seventv; +}; + +class MockApplication : mock::EmptyApplication +{ +public: + AccountController *getAccounts() override + { + return &this->accounts; + } + + ITwitchIrcServer *getTwitch() override + { + return &this->twitch; + } + + IEmotes *getEmotes() override + { + return &this->emotes; + } + + AccountController accounts; + MockTwitchIrcServer twitch; + Emotes emotes; +}; + +} // namespace + +namespace chatterino { + +class MockChannel : public Channel +{ +public: + MockChannel(const QString &name) + : Channel(name, Channel::Type::Twitch) + { + } +}; + +} // namespace chatterino + +EmotePtr namedEmote(const EmoteName &name) +{ + return std::shared_ptr(new Emote{ + .name{name}, + .images{}, + .tooltip{}, + .zeroWidth{}, + .id{}, + .author{}, + }); +} + +void addEmote(EmoteMap &map, const QString &name) +{ + EmoteName eName{.string{name}}; + map.insert(std::pair(eName, namedEmote(eName))); +} + +static QString DEFAULT_SETTINGS = R"!( +{ + "accounts": { + "uid117166826": { + "username": "testaccount_420", + "userID": "117166826", + "clientID": "abc", + "oauthToken": "def" + }, + "current": "testaccount_420" + } +})!"; + +class InputCompletionTest : public ::testing::Test +{ +protected: + void SetUp() override + { + // Write default settings to the mock settings json file + ASSERT_TRUE(QDir().mkpath("/tmp/c2-tests")); + + QFile settingsFile("/tmp/c2-tests/settings.json"); + ASSERT_TRUE(settingsFile.open(QIODevice::WriteOnly | QIODevice::Text)); + ASSERT_GT(settingsFile.write(DEFAULT_SETTINGS.toUtf8()), 0); + ASSERT_TRUE(settingsFile.flush()); + settingsFile.close(); + + // Initialize helix client + this->mockHelix = std::make_unique(); + initializeHelix(this->mockHelix.get()); + EXPECT_CALL(*this->mockHelix, loadBlocks).Times(Exactly(1)); + EXPECT_CALL(*this->mockHelix, update).Times(Exactly(1)); + + this->mockApplication = std::make_unique(); + this->settings = std::make_unique("/tmp/c2-tests"); + this->paths = std::make_unique(); + + this->mockApplication->accounts.initialize(*this->settings, + *this->paths); + this->mockApplication->emotes.initialize(*this->settings, *this->paths); + + this->channelPtr = std::make_shared("icelys"); + this->completionModel = + std::make_unique(*this->channelPtr); + + this->initializeEmotes(); + } + + void TearDown() override + { + ASSERT_TRUE(QDir("/tmp/c2-tests").removeRecursively()); + this->mockApplication.reset(); + this->settings.reset(); + this->paths.reset(); + this->mockHelix.reset(); + this->completionModel.reset(); + this->channelPtr.reset(); + } + + std::unique_ptr mockApplication; + std::unique_ptr settings; + std::unique_ptr paths; + std::unique_ptr mockHelix; + + ChannelPtr channelPtr; + std::unique_ptr completionModel; + +private: + void initializeEmotes() + { + auto bttvEmotes = std::make_shared(); + addEmote(*bttvEmotes, "FeelsGoodMan"); + addEmote(*bttvEmotes, "FeelsBadMan"); + addEmote(*bttvEmotes, "FeelsBirthdayMan"); + addEmote(*bttvEmotes, "Aware"); + addEmote(*bttvEmotes, "Clueless"); + addEmote(*bttvEmotes, "SaltyCorn"); + addEmote(*bttvEmotes, ":)"); + addEmote(*bttvEmotes, ":-)"); + addEmote(*bttvEmotes, "B-)"); + addEmote(*bttvEmotes, "Clap"); + this->mockApplication->twitch.bttv.setEmotes(std::move(bttvEmotes)); + + auto ffzEmotes = std::make_shared(); + addEmote(*ffzEmotes, "LilZ"); + addEmote(*ffzEmotes, "ManChicken"); + addEmote(*ffzEmotes, "CatBag"); + this->mockApplication->twitch.ffz.setEmotes(std::move(ffzEmotes)); + + auto seventvEmotes = std::make_shared(); + addEmote(*seventvEmotes, "Clap"); + addEmote(*seventvEmotes, "Clap2"); + this->mockApplication->twitch.seventv.setGlobalEmotes( + std::move(seventvEmotes)); + } + +protected: + auto queryEmoteCompletion(const QString &fullQuery) + { + // At the moment, buildCompletionEmoteList does not want the ':'. + QString normalizedQuery = fullQuery; + if (normalizedQuery.startsWith(':')) + { + normalizedQuery = normalizedQuery.mid(1); + } + + return chatterino::detail::buildCompletionEmoteList(normalizedQuery, + this->channelPtr); + } + + auto queryTabCompletion(const QString &fullQuery, bool isFirstWord) + { + this->completionModel->refresh(fullQuery, isFirstWord); + return this->completionModel->allItems(); + } +}; + +TEST_F(InputCompletionTest, EmoteNameFiltering) +{ + auto completion = queryEmoteCompletion(":feels"); + ASSERT_EQ(completion.size(), 3); + ASSERT_EQ(completion[0].displayName, "FeelsBirthdayMan"); + ASSERT_EQ(completion[1].displayName, "FeelsBadMan"); + ASSERT_EQ(completion[2].displayName, "FeelsGoodMan"); + + completion = queryEmoteCompletion(":)"); + ASSERT_EQ(completion.size(), 3); + ASSERT_EQ(completion[0].displayName, ":)"); // Exact match with : prefix + ASSERT_EQ(completion[1].displayName, ":-)"); + ASSERT_EQ(completion[2].displayName, "B-)"); + + completion = queryEmoteCompletion(":cat"); + ASSERT_TRUE(completion.size() >= 2); + // emoji exact match comes first + ASSERT_EQ(completion[0].displayName, "cat"); + // FFZ emote is prioritized over any other matching emojis + ASSERT_EQ(completion[1].displayName, "CatBag"); +} + +TEST_F(InputCompletionTest, EmoteExactNameMatching) +{ + auto completion = queryEmoteCompletion(":cat"); + ASSERT_TRUE(completion.size() >= 2); + // emoji exact match comes first + ASSERT_EQ(completion[0].displayName, "cat"); + // FFZ emote is prioritized over any other matching emojis + ASSERT_EQ(completion[1].displayName, "CatBag"); + + // not exactly "salt", SaltyCorn BTTV emote comes first + completion = queryEmoteCompletion(":sal"); + ASSERT_TRUE(completion.size() >= 3); + ASSERT_EQ(completion[0].displayName, "SaltyCorn"); + ASSERT_EQ(completion[1].displayName, "green_salad"); + ASSERT_EQ(completion[2].displayName, "salt"); + + // exactly "salt", emoji comes first + completion = queryEmoteCompletion(":salt"); + ASSERT_TRUE(completion.size() >= 2); + ASSERT_EQ(completion[0].displayName, "salt"); + ASSERT_EQ(completion[1].displayName, "SaltyCorn"); +} + +TEST_F(InputCompletionTest, EmoteProviderOrdering) +{ + auto completion = queryEmoteCompletion(":clap"); + // Current implementation leads to the exact first match being ignored when + // checking for exact matches. This is probably not intended behavior but + // this test is just verifying that the implementation stays the same. + // + // Initial ordering after filtering all available emotes: + // 1. Clap - BTTV + // 2. Clap - 7TV + // 3. Clap2 - 7TV + // 4. clapper - Emoji + // 5. clap - Emoji + // + // The 'exact match' starts looking at the second element and ends up swapping + // #2 with #1 despite #1 already being an exact match. + ASSERT_TRUE(completion.size() >= 5); + ASSERT_EQ(completion[0].displayName, "Clap"); + ASSERT_EQ(completion[0].providerName, "Global 7TV"); + ASSERT_EQ(completion[1].displayName, "Clap"); + ASSERT_EQ(completion[1].providerName, "Global BetterTTV"); + ASSERT_EQ(completion[2].displayName, "Clap2"); + ASSERT_EQ(completion[2].providerName, "Global 7TV"); + ASSERT_EQ(completion[3].displayName, "clapper"); + ASSERT_EQ(completion[3].providerName, "Emoji"); + ASSERT_EQ(completion[4].displayName, "clap"); + ASSERT_EQ(completion[4].providerName, "Emoji"); +} + +TEST_F(InputCompletionTest, TabCompletionEmote) +{ + auto completion = queryTabCompletion(":feels", false); + ASSERT_EQ(completion.size(), 0); // : prefix matters here + + // no : prefix defaults to emote completion + completion = queryTabCompletion("feels", false); + ASSERT_EQ(completion.size(), 3); + // note: different order from : menu + ASSERT_EQ(completion[0], "FeelsBadMan "); + ASSERT_EQ(completion[1], "FeelsBirthdayMan "); + ASSERT_EQ(completion[2], "FeelsGoodMan "); + + // no : prefix, emote completion. Duplicate Clap should be removed + completion = queryTabCompletion("cla", false); + ASSERT_EQ(completion.size(), 2); + ASSERT_EQ(completion[0], "Clap "); + ASSERT_EQ(completion[1], "Clap2 "); + + completion = queryTabCompletion("peepoHappy", false); + ASSERT_EQ(completion.size(), 0); // no peepoHappy emote + + completion = queryTabCompletion("Aware", false); + ASSERT_EQ(completion.size(), 1); + ASSERT_EQ(completion[0], "Aware "); // trailing space added +} + +TEST_F(InputCompletionTest, TabCompletionEmoji) +{ + auto completion = queryTabCompletion(":cla", false); + ASSERT_EQ(completion.size(), 8); + ASSERT_EQ(completion[0], ":clap: "); + ASSERT_EQ(completion[1], ":clap_tone1: "); + ASSERT_EQ(completion[2], ":clap_tone2: "); + ASSERT_EQ(completion[3], ":clap_tone3: "); + ASSERT_EQ(completion[4], ":clap_tone4: "); + ASSERT_EQ(completion[5], ":clap_tone5: "); + ASSERT_EQ(completion[6], ":clapper: "); + ASSERT_EQ(completion[7], ":classical_building: "); +} diff --git a/tests/src/TwitchMessageBuilder.cpp b/tests/src/TwitchMessageBuilder.cpp index e167a7773..db8675ca1 100644 --- a/tests/src/TwitchMessageBuilder.cpp +++ b/tests/src/TwitchMessageBuilder.cpp @@ -1,6 +1,5 @@ #include "providers/twitch/TwitchMessageBuilder.hpp" -#include "Application.hpp" #include "common/Channel.hpp" #include "messages/MessageBuilder.hpp" #include "mocks/EmptyApplication.hpp" @@ -27,6 +26,7 @@ public: { return &this->emotes; } + IUserDataController *getUserData() override { return &this->userData;