From ef3c51a4e225b5432c899f19b197fb3e4b68cf6b Mon Sep 17 00:00:00 2001 From: nerix Date: Sun, 29 Sep 2024 12:20:06 +0200 Subject: [PATCH] test: add more input completion tests (#5604) --- .github/workflows/test-macos.yml | 2 +- .github/workflows/test-windows.yml | 2 +- .github/workflows/test.yml | 2 +- CHANGELOG.md | 1 + tests/src/InputCompletion.cpp | 293 ++++++++++++++++++++++++++--- 5 files changed, 269 insertions(+), 31 deletions(-) diff --git a/.github/workflows/test-macos.yml b/.github/workflows/test-macos.yml index 9e76429fe..00e0bf10e 100644 --- a/.github/workflows/test-macos.yml +++ b/.github/workflows/test-macos.yml @@ -94,5 +94,5 @@ jobs: cd ../pubsub-server-test ./server 127.0.0.1:9050 & cd ../build-test - ctest --repeat until-pass:4 --output-on-failure --exclude-regex ClassicEmoteNameFiltering + ctest --repeat until-pass:4 --output-on-failure working-directory: build-test diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 7bdd43494..a81c69891 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -150,7 +150,7 @@ jobs: cd ..\pubsub-server-test .\server.exe 127.0.0.1:9050 & cd ..\build-test - ctest --repeat until-pass:4 --output-on-failure --exclude-regex ClassicEmoteNameFiltering + ctest --repeat until-pass:4 --output-on-failure working-directory: build-test - name: Clean Conan cache diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b83674e09..0ce528bd5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -88,7 +88,7 @@ jobs: cd ../pubsub-server-test ./server 127.0.0.1:9050 & cd ../build-test - ctest --repeat until-pass:4 --output-on-failure --exclude-regex ClassicEmoteNameFiltering + ctest --repeat until-pass:4 --output-on-failure working-directory: build-test - name: Upload coverage reports to Codecov diff --git a/CHANGELOG.md b/CHANGELOG.md index ef5464f9b..969cb8b8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -90,6 +90,7 @@ - Dev: The timer for `StreamerMode` is now destroyed on the correct thread. (#5571) - Dev: Cleanup some parts of the `magic_enum` adaptation for Qt. (#5587) - Dev: Refactored `static`s in headers to only be present once in the final app. (#5588) +- Dev: Added more tests for input completion. (#5604) - Dev: Refactored legacy Unicode zero-width-joiner replacement. (#5594) - Dev: The JSON output when copying a message (SHIFT + right-click) is now more extensive. (#5600) - Dev: Twitch messages are now sent using Twitch's Helix API instead of IRC by default. (#5607) diff --git a/tests/src/InputCompletion.cpp b/tests/src/InputCompletion.cpp index 1f219570c..a365be89d 100644 --- a/tests/src/InputCompletion.cpp +++ b/tests/src/InputCompletion.cpp @@ -3,6 +3,7 @@ #include "controllers/accounts/AccountController.hpp" #include "controllers/completion/strategies/ClassicEmoteStrategy.hpp" #include "controllers/completion/strategies/ClassicUserStrategy.hpp" +#include "controllers/completion/strategies/SmartEmoteStrategy.hpp" #include "controllers/completion/strategies/Strategy.hpp" #include "messages/Emote.hpp" #include "mocks/BaseApplication.hpp" @@ -84,7 +85,30 @@ public: SeventvEmotes seventvEmotes; }; -} // namespace +void containsRoughly(std::span span, const std::set &values) +{ + for (const auto &v : values) + { + bool found = false; + for (const auto &actualValue : span) + { + if (actualValue.displayName == v) + { + found = true; + break; + } + } + + ASSERT_TRUE(found) << v << " was not found in the span"; + } +} + +[[nodiscard]] bool allEmoji(std::span span) +{ + return std::ranges::all_of(span, [](const auto &it) { + return it.isEmoji && it.providerName == u"Emoji"; + }); +} EmotePtr namedEmote(const EmoteName &name) { @@ -104,7 +128,7 @@ void addEmote(EmoteMap &map, const QString &name) map.insert(std::pair(eName, namedEmote(eName))); } -static QString DEFAULT_SETTINGS = R"!( +const QString DEFAULT_SETTINGS = R"!( { "accounts": { "uid117166826": { @@ -117,6 +141,8 @@ static QString DEFAULT_SETTINGS = R"!( } })!"; +} // namespace + class InputCompletionTest : public ::testing::Test { protected: @@ -164,6 +190,7 @@ private: addEmote(*bttvEmotes, ":-)"); addEmote(*bttvEmotes, "B-)"); addEmote(*bttvEmotes, "Clap"); + addEmote(*bttvEmotes, ":tf:"); this->mockApplication->bttvEmotes.setEmotes(std::move(bttvEmotes)); auto ffzEmotes = std::make_shared(); @@ -175,50 +202,56 @@ private: auto seventvEmotes = std::make_shared(); addEmote(*seventvEmotes, "Clap"); addEmote(*seventvEmotes, "Clap2"); + addEmote(*seventvEmotes, "pajaW"); + addEmote(*seventvEmotes, "PAJAW"); this->mockApplication->seventvEmotes.setGlobalEmotes( std::move(seventvEmotes)); } protected: - auto queryClassicEmoteCompletion(const QString &fullQuery) + template + auto queryEmoteCompletion(const QString &fullQuery) { - EmoteSource source(this->channelPtr.get(), - std::make_unique()); + EmoteSource source(this->channelPtr.get(), std::make_unique()); source.update(fullQuery); std::vector out(source.output()); return out; } - auto queryClassicTabCompletion(const QString &fullQuery, bool isFirstWord) + template + auto queryTabCompletion(const QString &fullQuery, bool isFirstWord) { - EmoteSource source(this->channelPtr.get(), - std::make_unique()); + EmoteSource source(this->channelPtr.get(), std::make_unique()); source.update(fullQuery); QStringList m; source.addToStringList(m, 0, isFirstWord); return m; } -}; -void containsRoughly(std::span span, std::set values) -{ - for (const auto &v : values) + auto queryClassicEmoteCompletion(const QString &fullQuery) { - bool found = false; - for (const auto &actualValue : span) - { - if (actualValue.displayName == v) - { - found = true; - break; - } - } - - ASSERT_TRUE(found) << v << " was not found in the span"; + return queryEmoteCompletion(fullQuery); } -} + + auto queryClassicTabCompletion(const QString &fullQuery, bool isFirstWord) + { + return queryTabCompletion(fullQuery, + isFirstWord); + } + + auto querySmartEmoteCompletion(const QString &fullQuery) + { + return queryEmoteCompletion(fullQuery); + } + + auto querySmartTabCompletion(const QString &fullQuery, bool isFirstWord) + { + return queryTabCompletion(fullQuery, + isFirstWord); + } +}; TEST_F(InputCompletionTest, ClassicEmoteNameFiltering) { @@ -230,9 +263,9 @@ TEST_F(InputCompletionTest, ClassicEmoteNameFiltering) auto completion = queryClassicEmoteCompletion(":feels"); ASSERT_EQ(completion.size(), 3); // all these matches are BTTV global emotes - ASSERT_EQ(completion[0].displayName, "FeelsBirthdayMan"); - ASSERT_EQ(completion[1].displayName, "FeelsBadMan"); - ASSERT_EQ(completion[2].displayName, "FeelsGoodMan"); + // these are in no specific order + containsRoughly(completion, + {"FeelsBirthdayMan", "FeelsBadMan", "FeelsGoodMan"}); completion = queryClassicEmoteCompletion(":)"); ASSERT_EQ(completion.size(), 3); @@ -299,6 +332,30 @@ TEST_F(InputCompletionTest, ClassicEmoteProviderOrdering) ASSERT_EQ(completion[4].providerName, "Emoji"); } +TEST_F(InputCompletionTest, ClassicEmoteCase) +{ + auto completion = queryClassicEmoteCompletion(":pajaw"); + ASSERT_EQ(completion.size(), 2); + // there's no order here + containsRoughly(completion, {"pajaW", "PAJAW"}); + + completion = queryClassicEmoteCompletion(":PA"); + ASSERT_GT(completion.size(), 3); + containsRoughly({completion.begin(), 2}, {"pajaW", "PAJAW"}); + containsRoughly({completion.begin() + 2, completion.end()}, {"parking"}); + ASSERT_TRUE(allEmoji({completion.begin() + 2, completion.end()})); + + completion = queryClassicEmoteCompletion(":Pajaw"); + ASSERT_EQ(completion.size(), 2); + containsRoughly(completion, {"pajaW", "PAJAW"}); + + completion = queryClassicEmoteCompletion(":NOTHING"); + ASSERT_EQ(completion.size(), 0); + + completion = queryClassicEmoteCompletion(":nothing"); + ASSERT_EQ(completion.size(), 0); +} + TEST_F(InputCompletionTest, ClassicTabCompletionEmote) { auto completion = queryClassicTabCompletion(":feels", false); @@ -328,7 +385,13 @@ TEST_F(InputCompletionTest, ClassicTabCompletionEmote) TEST_F(InputCompletionTest, ClassicTabCompletionEmoji) { - auto completion = queryClassicTabCompletion(":cla", false); + auto completion = queryClassicTabCompletion(":tf", false); + ASSERT_EQ(completion.size(), 0); + + completion = queryClassicTabCompletion(":)", false); + ASSERT_EQ(completion.size(), 0); + + completion = queryClassicTabCompletion(":cla", false); ASSERT_EQ(completion.size(), 8); ASSERT_EQ(completion[0], ":clap: "); ASSERT_EQ(completion[1], ":clap_tone1: "); @@ -339,3 +402,177 @@ TEST_F(InputCompletionTest, ClassicTabCompletionEmoji) ASSERT_EQ(completion[6], ":clapper: "); ASSERT_EQ(completion[7], ":classical_building: "); } + +TEST_F(InputCompletionTest, ClassicTabCompletionCase) +{ + auto completion = queryClassicTabCompletion("pajaw", false); + ASSERT_EQ(completion.size(), 2); + ASSERT_EQ(completion[0], "pajaW "); + ASSERT_EQ(completion[1], "PAJAW "); + + completion = queryClassicTabCompletion("PA", false); + ASSERT_EQ(completion.size(), 2); + ASSERT_EQ(completion[0], "pajaW "); + ASSERT_EQ(completion[1], "PAJAW "); + + completion = queryClassicTabCompletion("Pajaw", false); + ASSERT_EQ(completion.size(), 2); + ASSERT_EQ(completion[0], "pajaW "); + ASSERT_EQ(completion[1], "PAJAW "); + + completion = queryClassicTabCompletion("NOTHING", false); + ASSERT_EQ(completion.size(), 0); + + completion = queryClassicTabCompletion("nothing", false); + ASSERT_EQ(completion.size(), 0); +} + +TEST_F(InputCompletionTest, SmartEmoteNameFiltering) +{ + auto completion = querySmartEmoteCompletion(":feels"); + ASSERT_EQ(completion.size(), 3); + ASSERT_EQ(completion[0].displayName, "FeelsBadMan"); + ASSERT_EQ(completion[1].displayName, "FeelsGoodMan"); + ASSERT_EQ(completion[2].displayName, "FeelsBirthdayMan"); + + completion = querySmartEmoteCompletion(":)"); + ASSERT_EQ(completion.size(), 3); + ASSERT_EQ(completion[0].displayName, ":)"); + ASSERT_EQ(completion[1].displayName, ":-)"); + ASSERT_EQ(completion[2].displayName, "B-)"); + + completion = querySmartEmoteCompletion(":cat"); + ASSERT_TRUE(completion.size() >= 4); + ASSERT_EQ(completion[0].displayName, "cat"); + ASSERT_EQ(completion[1].displayName, "cat2"); + ASSERT_EQ(completion[2].displayName, "CatBag"); + ASSERT_EQ(completion[3].displayName, "joy_cat"); +} + +TEST_F(InputCompletionTest, SmartEmoteExactNameMatching) +{ + auto completion = querySmartEmoteCompletion(":sal"); + ASSERT_TRUE(completion.size() >= 4); + ASSERT_EQ(completion[0].displayName, "salt"); + ASSERT_EQ(completion[1].displayName, "SaltyCorn"); + ASSERT_EQ(completion[2].displayName, "green_salad"); + ASSERT_EQ(completion[3].displayName, "saluting_face"); + + completion = querySmartEmoteCompletion(":salt"); + ASSERT_TRUE(completion.size() >= 2); + ASSERT_EQ(completion[0].displayName, "salt"); + ASSERT_EQ(completion[1].displayName, "SaltyCorn"); +} + +TEST_F(InputCompletionTest, SmartEmoteProviderOrdering) +{ + auto completion = querySmartEmoteCompletion(":clap"); + ASSERT_TRUE(completion.size() >= 6); + ASSERT_EQ(completion[0].displayName, "clap"); + ASSERT_EQ(completion[0].providerName, "Emoji"); + ASSERT_EQ(completion[1].displayName, "Clap"); + ASSERT_EQ(completion[1].providerName, "Global BetterTTV"); + ASSERT_EQ(completion[2].displayName, "Clap"); + ASSERT_EQ(completion[2].providerName, "Global 7TV"); + ASSERT_EQ(completion[3].displayName, "Clap2"); + ASSERT_EQ(completion[3].providerName, "Global 7TV"); + ASSERT_EQ(completion[4].displayName, "clapper"); + ASSERT_EQ(completion[4].providerName, "Emoji"); + ASSERT_EQ(completion[5].displayName, "clap_tone1"); + ASSERT_EQ(completion[5].providerName, "Emoji"); +} + +TEST_F(InputCompletionTest, SmartEmoteCase) +{ + auto completion = querySmartEmoteCompletion(":pajaw"); + ASSERT_EQ(completion.size(), 2); + ASSERT_EQ(completion[0].displayName, "pajaW"); + ASSERT_EQ(completion[1].displayName, "PAJAW"); + + completion = querySmartEmoteCompletion(":PA"); + ASSERT_EQ(completion.size(), 1); + ASSERT_EQ(completion[0].displayName, "PAJAW"); + + completion = querySmartEmoteCompletion(":Pajaw"); + ASSERT_EQ(completion.size(), 2); + ASSERT_EQ(completion[0].displayName, "PAJAW"); + ASSERT_EQ(completion[1].displayName, "pajaW"); + + completion = querySmartEmoteCompletion(":NOTHING"); + ASSERT_EQ(completion.size(), 0); + + completion = querySmartEmoteCompletion(":nothing"); + ASSERT_EQ(completion.size(), 0); +} + +TEST_F(InputCompletionTest, SmartTabCompletionEmote) +{ + auto completion = querySmartTabCompletion(":feels", false); + ASSERT_EQ(completion.size(), 0); // : prefix matters here + + // no : prefix defaults to emote completion + completion = querySmartTabCompletion("feels", false); + ASSERT_EQ(completion.size(), 3); + ASSERT_EQ(completion[0], "FeelsBadMan "); + ASSERT_EQ(completion[1], "FeelsGoodMan "); + ASSERT_EQ(completion[2], "FeelsBirthdayMan "); + + // no : prefix, emote completion. Duplicate Clap should be removed + completion = querySmartTabCompletion("cla", false); + ASSERT_EQ(completion.size(), 3); + ASSERT_EQ(completion[0], "Clap "); + ASSERT_EQ(completion[1], "Clap "); + ASSERT_EQ(completion[2], "Clap2 "); + + completion = querySmartTabCompletion("peepoHappy", false); + ASSERT_EQ(completion.size(), 0); + + completion = querySmartTabCompletion("Aware", false); + ASSERT_EQ(completion.size(), 1); + ASSERT_EQ(completion[0], "Aware "); +} + +TEST_F(InputCompletionTest, SmartTabCompletionEmoji) +{ + auto completion = querySmartTabCompletion(":tf", false); + ASSERT_EQ(completion.size(), 0); + // ASSERT_EQ(completion[0], ":tf: "); + + completion = querySmartTabCompletion(":)", false); + ASSERT_EQ(completion.size(), 0); + // ASSERT_EQ(completion[0], ":) "); + + completion = querySmartTabCompletion(":cla", false); + ASSERT_EQ(completion.size(), 8); + ASSERT_EQ(completion[0], ":clap: "); + ASSERT_EQ(completion[1], ":clapper: "); + ASSERT_EQ(completion[2], ":clap_tone1: "); + ASSERT_EQ(completion[3], ":clap_tone2: "); + ASSERT_EQ(completion[4], ":clap_tone3: "); + ASSERT_EQ(completion[5], ":clap_tone4: "); + ASSERT_EQ(completion[6], ":clap_tone5: "); + ASSERT_EQ(completion[7], ":classical_building: "); +} + +TEST_F(InputCompletionTest, SmartTabCompletionCase) +{ + auto completion = querySmartTabCompletion("pajaw", false); + ASSERT_EQ(completion.size(), 2); + ASSERT_EQ(completion[0], "pajaW "); + ASSERT_EQ(completion[1], "PAJAW "); + + completion = querySmartTabCompletion("PA", false); + ASSERT_EQ(completion.size(), 1); + ASSERT_EQ(completion[0], "PAJAW "); + + completion = querySmartTabCompletion("Pajaw", false); + ASSERT_EQ(completion.size(), 2); + ASSERT_EQ(completion[0], "PAJAW "); + ASSERT_EQ(completion[1], "pajaW "); + + completion = querySmartTabCompletion("NOTHING", false); + ASSERT_EQ(completion.size(), 0); + + completion = querySmartTabCompletion("nothing", false); + ASSERT_EQ(completion.size(), 0); +}