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);
+}