2023-05-21 12:10:49 +02:00
|
|
|
#include "Application.hpp"
|
|
|
|
#include "common/Aliases.hpp"
|
|
|
|
#include "controllers/accounts/AccountController.hpp"
|
2023-09-24 14:17:17 +02:00
|
|
|
#include "controllers/completion/strategies/ClassicEmoteStrategy.hpp"
|
|
|
|
#include "controllers/completion/strategies/ClassicUserStrategy.hpp"
|
|
|
|
#include "controllers/completion/strategies/Strategy.hpp"
|
2023-05-21 12:10:49 +02:00
|
|
|
#include "messages/Emote.hpp"
|
2024-08-24 15:02:08 +02:00
|
|
|
#include "mocks/BaseApplication.hpp"
|
2023-10-13 17:41:23 +02:00
|
|
|
#include "mocks/Channel.hpp"
|
2023-05-21 12:10:49 +02:00
|
|
|
#include "mocks/Helix.hpp"
|
2024-09-14 12:17:31 +02:00
|
|
|
#include "mocks/Logging.hpp"
|
2023-10-13 17:41:23 +02:00
|
|
|
#include "mocks/TwitchIrcServer.hpp"
|
2023-05-21 12:10:49 +02:00
|
|
|
#include "singletons/Emotes.hpp"
|
|
|
|
#include "singletons/Paths.hpp"
|
|
|
|
#include "singletons/Settings.hpp"
|
2024-05-05 15:01:07 +02:00
|
|
|
#include "Test.hpp"
|
2023-05-21 12:10:49 +02:00
|
|
|
#include "widgets/splits/InputCompletionPopup.hpp"
|
|
|
|
|
|
|
|
#include <QDir>
|
|
|
|
#include <QFile>
|
|
|
|
#include <QModelIndex>
|
|
|
|
#include <QString>
|
2023-05-26 14:54:23 +02:00
|
|
|
#include <QTemporaryDir>
|
2023-05-21 12:10:49 +02:00
|
|
|
|
2023-08-27 13:11:59 +02:00
|
|
|
#include <span>
|
|
|
|
|
2023-10-13 17:41:23 +02:00
|
|
|
using namespace chatterino;
|
|
|
|
using chatterino::mock::MockChannel;
|
|
|
|
|
2023-05-21 12:10:49 +02:00
|
|
|
namespace {
|
|
|
|
|
2023-09-24 14:17:17 +02:00
|
|
|
using namespace chatterino::completion;
|
2023-05-21 12:10:49 +02:00
|
|
|
using ::testing::Exactly;
|
|
|
|
|
2024-08-24 15:02:08 +02:00
|
|
|
class MockApplication : public mock::BaseApplication
|
2023-05-21 12:10:49 +02:00
|
|
|
{
|
|
|
|
public:
|
2024-08-24 15:02:08 +02:00
|
|
|
explicit MockApplication(const QString &settingsData)
|
|
|
|
: BaseApplication(settingsData)
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
2023-05-21 12:10:49 +02:00
|
|
|
AccountController *getAccounts() override
|
|
|
|
{
|
|
|
|
return &this->accounts;
|
|
|
|
}
|
|
|
|
|
|
|
|
ITwitchIrcServer *getTwitch() override
|
|
|
|
{
|
|
|
|
return &this->twitch;
|
|
|
|
}
|
|
|
|
|
|
|
|
IEmotes *getEmotes() override
|
|
|
|
{
|
|
|
|
return &this->emotes;
|
|
|
|
}
|
|
|
|
|
2024-01-21 14:20:21 +01:00
|
|
|
BttvEmotes *getBttvEmotes() override
|
|
|
|
{
|
|
|
|
return &this->bttvEmotes;
|
|
|
|
}
|
|
|
|
|
|
|
|
FfzEmotes *getFfzEmotes() override
|
|
|
|
{
|
|
|
|
return &this->ffzEmotes;
|
|
|
|
}
|
|
|
|
|
|
|
|
SeventvEmotes *getSeventvEmotes() override
|
|
|
|
{
|
|
|
|
return &this->seventvEmotes;
|
|
|
|
}
|
|
|
|
|
2024-09-14 12:17:31 +02:00
|
|
|
ILogging *getChatLogger() override
|
|
|
|
{
|
|
|
|
return &this->logging;
|
|
|
|
}
|
|
|
|
|
|
|
|
mock::EmptyLogging logging;
|
2023-05-21 12:10:49 +02:00
|
|
|
AccountController accounts;
|
2023-10-13 17:41:23 +02:00
|
|
|
mock::MockTwitchIrcServer twitch;
|
2023-05-21 12:10:49 +02:00
|
|
|
Emotes emotes;
|
2024-01-21 14:20:21 +01:00
|
|
|
BttvEmotes bttvEmotes;
|
|
|
|
FfzEmotes ffzEmotes;
|
|
|
|
SeventvEmotes seventvEmotes;
|
2023-05-21 12:10:49 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
|
|
EmotePtr namedEmote(const EmoteName &name)
|
|
|
|
{
|
|
|
|
return std::shared_ptr<Emote>(new Emote{
|
|
|
|
.name{name},
|
|
|
|
.images{},
|
|
|
|
.tooltip{},
|
|
|
|
.zeroWidth{},
|
|
|
|
.id{},
|
|
|
|
.author{},
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
void addEmote(EmoteMap &map, const QString &name)
|
|
|
|
{
|
|
|
|
EmoteName eName{.string{name}};
|
|
|
|
map.insert(std::pair<EmoteName, EmotePtr>(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
|
|
|
|
{
|
|
|
|
// Initialize helix client
|
|
|
|
this->mockHelix = std::make_unique<mock::Helix>();
|
|
|
|
initializeHelix(this->mockHelix.get());
|
|
|
|
EXPECT_CALL(*this->mockHelix, loadBlocks).Times(Exactly(1));
|
|
|
|
EXPECT_CALL(*this->mockHelix, update).Times(Exactly(1));
|
|
|
|
|
2024-08-24 15:02:08 +02:00
|
|
|
this->mockApplication =
|
|
|
|
std::make_unique<MockApplication>(DEFAULT_SETTINGS);
|
2023-05-21 12:10:49 +02:00
|
|
|
|
2024-07-21 15:09:59 +02:00
|
|
|
this->mockApplication->accounts.load();
|
2023-05-21 12:10:49 +02:00
|
|
|
|
|
|
|
this->channelPtr = std::make_shared<MockChannel>("icelys");
|
|
|
|
|
|
|
|
this->initializeEmotes();
|
|
|
|
}
|
|
|
|
|
|
|
|
void TearDown() override
|
|
|
|
{
|
|
|
|
this->channelPtr.reset();
|
2024-09-14 12:17:31 +02:00
|
|
|
this->mockHelix.reset();
|
|
|
|
this->mockApplication.reset();
|
2023-05-21 12:10:49 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
std::unique_ptr<MockApplication> mockApplication;
|
|
|
|
std::unique_ptr<mock::Helix> mockHelix;
|
|
|
|
|
|
|
|
ChannelPtr channelPtr;
|
|
|
|
|
|
|
|
private:
|
|
|
|
void initializeEmotes()
|
|
|
|
{
|
|
|
|
auto bttvEmotes = std::make_shared<EmoteMap>();
|
|
|
|
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");
|
2024-01-21 14:20:21 +01:00
|
|
|
this->mockApplication->bttvEmotes.setEmotes(std::move(bttvEmotes));
|
2023-05-21 12:10:49 +02:00
|
|
|
|
|
|
|
auto ffzEmotes = std::make_shared<EmoteMap>();
|
|
|
|
addEmote(*ffzEmotes, "LilZ");
|
|
|
|
addEmote(*ffzEmotes, "ManChicken");
|
|
|
|
addEmote(*ffzEmotes, "CatBag");
|
2024-01-21 14:20:21 +01:00
|
|
|
this->mockApplication->ffzEmotes.setEmotes(std::move(ffzEmotes));
|
2023-05-21 12:10:49 +02:00
|
|
|
|
|
|
|
auto seventvEmotes = std::make_shared<EmoteMap>();
|
|
|
|
addEmote(*seventvEmotes, "Clap");
|
|
|
|
addEmote(*seventvEmotes, "Clap2");
|
2024-01-21 14:20:21 +01:00
|
|
|
this->mockApplication->seventvEmotes.setGlobalEmotes(
|
2023-05-21 12:10:49 +02:00
|
|
|
std::move(seventvEmotes));
|
|
|
|
}
|
|
|
|
|
|
|
|
protected:
|
2023-09-24 14:17:17 +02:00
|
|
|
auto queryClassicEmoteCompletion(const QString &fullQuery)
|
2023-05-21 12:10:49 +02:00
|
|
|
{
|
2023-09-24 14:17:17 +02:00
|
|
|
EmoteSource source(this->channelPtr.get(),
|
|
|
|
std::make_unique<ClassicEmoteStrategy>());
|
|
|
|
source.update(fullQuery);
|
2023-05-21 12:10:49 +02:00
|
|
|
|
2023-09-24 14:17:17 +02:00
|
|
|
std::vector<EmoteItem> out(source.output());
|
|
|
|
return out;
|
2023-05-21 12:10:49 +02:00
|
|
|
}
|
|
|
|
|
2023-09-24 14:17:17 +02:00
|
|
|
auto queryClassicTabCompletion(const QString &fullQuery, bool isFirstWord)
|
2023-05-21 12:10:49 +02:00
|
|
|
{
|
2023-09-24 14:17:17 +02:00
|
|
|
EmoteSource source(this->channelPtr.get(),
|
|
|
|
std::make_unique<ClassicTabEmoteStrategy>());
|
|
|
|
source.update(fullQuery);
|
|
|
|
|
|
|
|
QStringList m;
|
|
|
|
source.addToStringList(m, 0, isFirstWord);
|
|
|
|
return m;
|
2023-05-21 12:10:49 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2023-09-24 14:17:17 +02:00
|
|
|
void containsRoughly(std::span<EmoteItem> span, std::set<QString> values)
|
2023-08-27 13:11:59 +02:00
|
|
|
{
|
|
|
|
for (const auto &v : values)
|
|
|
|
{
|
|
|
|
bool found = false;
|
|
|
|
for (const auto &actualValue : span)
|
|
|
|
{
|
|
|
|
if (actualValue.displayName == v)
|
|
|
|
{
|
|
|
|
found = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-05-05 15:01:07 +02:00
|
|
|
ASSERT_TRUE(found) << v << " was not found in the span";
|
2023-08-27 13:11:59 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-24 14:17:17 +02:00
|
|
|
TEST_F(InputCompletionTest, ClassicEmoteNameFiltering)
|
2023-05-21 12:10:49 +02:00
|
|
|
{
|
2023-05-26 14:54:23 +02:00
|
|
|
// The completion doesn't guarantee an ordering for a specific category of emotes.
|
|
|
|
// This tests a specific implementation of the underlying std::unordered_map,
|
|
|
|
// so depending on the standard library used when compiling, this might yield
|
|
|
|
// different results.
|
|
|
|
|
2023-09-24 14:17:17 +02:00
|
|
|
auto completion = queryClassicEmoteCompletion(":feels");
|
2023-05-21 12:10:49 +02:00
|
|
|
ASSERT_EQ(completion.size(), 3);
|
2023-05-26 14:54:23 +02:00
|
|
|
// all these matches are BTTV global emotes
|
2023-05-21 12:10:49 +02:00
|
|
|
ASSERT_EQ(completion[0].displayName, "FeelsBirthdayMan");
|
|
|
|
ASSERT_EQ(completion[1].displayName, "FeelsBadMan");
|
|
|
|
ASSERT_EQ(completion[2].displayName, "FeelsGoodMan");
|
|
|
|
|
2023-09-24 14:17:17 +02:00
|
|
|
completion = queryClassicEmoteCompletion(":)");
|
2023-05-21 12:10:49 +02:00
|
|
|
ASSERT_EQ(completion.size(), 3);
|
|
|
|
ASSERT_EQ(completion[0].displayName, ":)"); // Exact match with : prefix
|
2023-08-27 13:11:59 +02:00
|
|
|
containsRoughly({completion.begin() + 1, 2}, {":-)", "B-)"});
|
2023-05-21 12:10:49 +02:00
|
|
|
|
2023-09-24 14:17:17 +02:00
|
|
|
completion = queryClassicEmoteCompletion(":cat");
|
2023-05-21 12:10:49 +02:00
|
|
|
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");
|
|
|
|
}
|
|
|
|
|
2023-09-24 14:17:17 +02:00
|
|
|
TEST_F(InputCompletionTest, ClassicEmoteExactNameMatching)
|
2023-05-21 12:10:49 +02:00
|
|
|
{
|
2023-09-24 14:17:17 +02:00
|
|
|
auto completion = queryClassicEmoteCompletion(":cat");
|
2023-05-21 12:10:49 +02:00
|
|
|
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
|
2023-09-24 14:17:17 +02:00
|
|
|
completion = queryClassicEmoteCompletion(":sal");
|
2023-05-21 12:10:49 +02:00
|
|
|
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
|
2023-09-24 14:17:17 +02:00
|
|
|
completion = queryClassicEmoteCompletion(":salt");
|
2023-05-21 12:10:49 +02:00
|
|
|
ASSERT_TRUE(completion.size() >= 2);
|
|
|
|
ASSERT_EQ(completion[0].displayName, "salt");
|
|
|
|
ASSERT_EQ(completion[1].displayName, "SaltyCorn");
|
|
|
|
}
|
|
|
|
|
2023-09-24 14:17:17 +02:00
|
|
|
TEST_F(InputCompletionTest, ClassicEmoteProviderOrdering)
|
2023-05-21 12:10:49 +02:00
|
|
|
{
|
2023-09-24 14:17:17 +02:00
|
|
|
auto completion = queryClassicEmoteCompletion(":clap");
|
2023-05-21 12:10:49 +02:00
|
|
|
// 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");
|
|
|
|
}
|
|
|
|
|
2023-09-24 14:17:17 +02:00
|
|
|
TEST_F(InputCompletionTest, ClassicTabCompletionEmote)
|
2023-05-21 12:10:49 +02:00
|
|
|
{
|
2023-09-24 14:17:17 +02:00
|
|
|
auto completion = queryClassicTabCompletion(":feels", false);
|
2023-05-21 12:10:49 +02:00
|
|
|
ASSERT_EQ(completion.size(), 0); // : prefix matters here
|
|
|
|
|
|
|
|
// no : prefix defaults to emote completion
|
2023-09-24 14:17:17 +02:00
|
|
|
completion = queryClassicTabCompletion("feels", false);
|
2023-05-21 12:10:49 +02:00
|
|
|
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
|
2023-09-24 14:17:17 +02:00
|
|
|
completion = queryClassicTabCompletion("cla", false);
|
2023-05-21 12:10:49 +02:00
|
|
|
ASSERT_EQ(completion.size(), 2);
|
|
|
|
ASSERT_EQ(completion[0], "Clap ");
|
|
|
|
ASSERT_EQ(completion[1], "Clap2 ");
|
|
|
|
|
2023-09-24 14:17:17 +02:00
|
|
|
completion = queryClassicTabCompletion("peepoHappy", false);
|
2023-05-21 12:10:49 +02:00
|
|
|
ASSERT_EQ(completion.size(), 0); // no peepoHappy emote
|
|
|
|
|
2023-09-24 14:17:17 +02:00
|
|
|
completion = queryClassicTabCompletion("Aware", false);
|
2023-05-21 12:10:49 +02:00
|
|
|
ASSERT_EQ(completion.size(), 1);
|
|
|
|
ASSERT_EQ(completion[0], "Aware "); // trailing space added
|
|
|
|
}
|
|
|
|
|
2023-09-24 14:17:17 +02:00
|
|
|
TEST_F(InputCompletionTest, ClassicTabCompletionEmoji)
|
2023-05-21 12:10:49 +02:00
|
|
|
{
|
2023-09-24 14:17:17 +02:00
|
|
|
auto completion = queryClassicTabCompletion(":cla", false);
|
2023-05-21 12:10:49 +02:00
|
|
|
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: ");
|
|
|
|
}
|