Files
WoWee/tests/test_chat_markup_parser.cpp
Pavel Okhlopkov 42f1bb98ea refactor(chat): decompose into modular architecture, add GM commands, fix protocol
- Extract ChatPanel monolith into 15+ focused modules under ui/chat/
  (ChatInput, ChatTabManager, ChatTabCompleter, ChatMarkupParser,
  ChatMarkupRenderer, ChatCommandRegistry, ChatBubbleManager,
  ChatSettings, MacroEvaluator, GameStateAdapter, InputModifierAdapter)
- Split 2700-line chat_panel_commands.cpp into 11 command modules
- Add GM command handling: 190-command data table, dot-prefix interception,
  tab-completion, /gmhelp with category filter
- Fix ChatType enum to match WoW wire protocol (SAY=0x01 not 0x00);
  values 0x00-0x1B shared across Vanilla/TBC/WotLK
- Fix BG_SYSTEM_* values from 82-84 (UB in bitmask shifts) to 0x24-0x26
- Fix infinite Enter key loop after teleport (disable TOGGLE_CHAT repeat,
  add 2-frame input cooldown)
- Add tests: chat_markup_parser, chat_tab_completer, gm_commands,
  macro_evaluator

Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
2026-04-12 14:59:56 +03:00

208 lines
8.2 KiB
C++

// Tests for ChatMarkupParser — WoW markup parsing into typed segments.
// Phase 2.3 of chat_panel_ref.md.
#include <catch_amalgamated.hpp>
#include "ui/chat/chat_markup_parser.hpp"
using namespace wowee::ui;
// ── Plain text ──────────────────────────────────────────────
TEST_CASE("Plain text produces single Text segment", "[chat_markup]") {
ChatMarkupParser parser;
auto segs = parser.parse("Hello world");
REQUIRE(segs.size() == 1);
CHECK(segs[0].type == SegmentType::Text);
CHECK(segs[0].text == "Hello world");
}
TEST_CASE("Empty string produces no segments", "[chat_markup]") {
ChatMarkupParser parser;
auto segs = parser.parse("");
CHECK(segs.empty());
}
// ── Color codes ─────────────────────────────────────────────
TEST_CASE("parseWowColor extracts AARRGGBB correctly", "[chat_markup]") {
// |cFF00FF00 → alpha=1.0, red=0.0, green=1.0, blue=0.0
std::string s = "|cFF00FF00some text|r";
ImVec4 c = ChatMarkupParser::parseWowColor(s, 0);
CHECK(c.x == Catch::Approx(0.0f).margin(0.01f)); // red
CHECK(c.y == Catch::Approx(1.0f).margin(0.01f)); // green
CHECK(c.z == Catch::Approx(0.0f).margin(0.01f)); // blue
CHECK(c.w == Catch::Approx(1.0f).margin(0.01f)); // alpha
}
TEST_CASE("parseWowColor with half values", "[chat_markup]") {
// |cFF808080 → gray (128/255 ≈ 0.502)
std::string s = "|cFF808080";
ImVec4 c = ChatMarkupParser::parseWowColor(s, 0);
CHECK(c.x == Catch::Approx(0.502f).margin(0.01f));
CHECK(c.y == Catch::Approx(0.502f).margin(0.01f));
CHECK(c.z == Catch::Approx(0.502f).margin(0.01f));
}
TEST_CASE("Colored text segment: |cAARRGGBB...text...|r", "[chat_markup]") {
ChatMarkupParser parser;
auto segs = parser.parse("|cFFFF0000Red text|r");
REQUIRE(segs.size() == 1);
CHECK(segs[0].type == SegmentType::ColoredText);
CHECK(segs[0].text == "Red text");
CHECK(segs[0].color.x == Catch::Approx(1.0f).margin(0.01f)); // red
CHECK(segs[0].color.y == Catch::Approx(0.0f).margin(0.01f)); // green
CHECK(segs[0].color.z == Catch::Approx(0.0f).margin(0.01f)); // blue
}
TEST_CASE("Colored text without |r includes rest of string", "[chat_markup]") {
ChatMarkupParser parser;
auto segs = parser.parse("|cFF00FF00Green forever");
REQUIRE(segs.size() == 1);
CHECK(segs[0].type == SegmentType::ColoredText);
CHECK(segs[0].text == "Green forever");
}
TEST_CASE("Mixed plain and colored text", "[chat_markup]") {
ChatMarkupParser parser;
auto segs = parser.parse("Hello |cFFFF0000world|r!");
REQUIRE(segs.size() == 3);
CHECK(segs[0].type == SegmentType::Text);
CHECK(segs[0].text == "Hello ");
CHECK(segs[1].type == SegmentType::ColoredText);
CHECK(segs[1].text == "world");
CHECK(segs[2].type == SegmentType::Text);
CHECK(segs[2].text == "!");
}
// ── Item links ──────────────────────────────────────────────
TEST_CASE("Item link: |cFFAARRGG|Hitem:ID:...|h[Name]|h|r", "[chat_markup]") {
ChatMarkupParser parser;
std::string raw = "|cff1eff00|Hitem:19019:0:0:0:0:0:0:0:80|h[Thunderfury]|h|r";
auto segs = parser.parse(raw);
REQUIRE(segs.size() == 1);
CHECK(segs[0].type == SegmentType::ItemLink);
CHECK(segs[0].text == "Thunderfury");
CHECK(segs[0].id == 19019);
CHECK_FALSE(segs[0].rawLink.empty());
}
TEST_CASE("Bare item link without color prefix", "[chat_markup]") {
ChatMarkupParser parser;
std::string raw = "|Hitem:12345:0:0:0|h[Some Item]|h";
auto segs = parser.parse(raw);
REQUIRE(segs.size() == 1);
CHECK(segs[0].type == SegmentType::ItemLink);
CHECK(segs[0].text == "Some Item");
CHECK(segs[0].id == 12345);
}
TEST_CASE("Item link with text before and after", "[chat_markup]") {
ChatMarkupParser parser;
std::string raw = "Check this: |cff0070dd|Hitem:50000:0:0:0|h[Cool Sword]|h|r nice!";
auto segs = parser.parse(raw);
REQUIRE(segs.size() == 3);
CHECK(segs[0].type == SegmentType::Text);
CHECK(segs[0].text == "Check this: ");
CHECK(segs[1].type == SegmentType::ItemLink);
CHECK(segs[1].text == "Cool Sword");
CHECK(segs[1].id == 50000);
CHECK(segs[2].type == SegmentType::Text);
CHECK(segs[2].text == " nice!");
}
// ── Spell links ─────────────────────────────────────────────
TEST_CASE("Spell link: |Hspell:ID:RANK|h[Name]|h", "[chat_markup]") {
ChatMarkupParser parser;
std::string raw = "|cff71d5ff|Hspell:48461:0|h[Wrath]|h|r";
auto segs = parser.parse(raw);
REQUIRE(segs.size() == 1);
CHECK(segs[0].type == SegmentType::SpellLink);
CHECK(segs[0].text == "Wrath");
CHECK(segs[0].id == 48461);
}
// ── Quest links ─────────────────────────────────────────────
TEST_CASE("Quest link with level extraction", "[chat_markup]") {
ChatMarkupParser parser;
std::string raw = "|cff808080|Hquest:9876:70|h[The Last Stand]|h|r";
auto segs = parser.parse(raw);
REQUIRE(segs.size() == 1);
CHECK(segs[0].type == SegmentType::QuestLink);
CHECK(segs[0].text == "The Last Stand");
CHECK(segs[0].id == 9876);
CHECK(segs[0].extra == 70); // quest level
}
// ── Achievement links ───────────────────────────────────────
TEST_CASE("Achievement link", "[chat_markup]") {
ChatMarkupParser parser;
std::string raw = "|cffffff00|Hachievement:2136:0:0:0:0:0:0:0:0:0|h[Glory of the Hero]|h|r";
auto segs = parser.parse(raw);
REQUIRE(segs.size() == 1);
CHECK(segs[0].type == SegmentType::AchievementLink);
CHECK(segs[0].text == "Glory of the Hero");
CHECK(segs[0].id == 2136);
}
// ── URLs ────────────────────────────────────────────────────
TEST_CASE("URL is detected as Url segment", "[chat_markup]") {
ChatMarkupParser parser;
auto segs = parser.parse("Visit https://example.com for info");
REQUIRE(segs.size() == 3);
CHECK(segs[0].type == SegmentType::Text);
CHECK(segs[0].text == "Visit ");
CHECK(segs[1].type == SegmentType::Url);
CHECK(segs[1].text == "https://example.com");
CHECK(segs[2].type == SegmentType::Text);
CHECK(segs[2].text == " for info");
}
TEST_CASE("URL at end of message", "[chat_markup]") {
ChatMarkupParser parser;
auto segs = parser.parse("Link: https://example.com/path?q=1");
REQUIRE(segs.size() == 2);
CHECK(segs[1].type == SegmentType::Url);
CHECK(segs[1].text == "https://example.com/path?q=1");
}
// ── Edge cases ──────────────────────────────────────────────
TEST_CASE("Short |c without enough hex digits treated as literal", "[chat_markup]") {
ChatMarkupParser parser;
auto segs = parser.parse("|c short");
REQUIRE(segs.size() == 2);
CHECK(segs[0].type == SegmentType::Text);
CHECK(segs[0].text == "|c");
CHECK(segs[1].type == SegmentType::Text);
CHECK(segs[1].text == " short");
}
TEST_CASE("Multiple item links in one message", "[chat_markup]") {
ChatMarkupParser parser;
std::string raw =
"|cff1eff00|Hitem:100:0|h[Sword]|h|r and |cff0070dd|Hitem:200:0|h[Shield]|h|r";
auto segs = parser.parse(raw);
REQUIRE(segs.size() == 3);
CHECK(segs[0].type == SegmentType::ItemLink);
CHECK(segs[0].text == "Sword");
CHECK(segs[0].id == 100);
CHECK(segs[1].type == SegmentType::Text);
CHECK(segs[1].text == " and ");
CHECK(segs[2].type == SegmentType::ItemLink);
CHECK(segs[2].text == "Shield");
CHECK(segs[2].id == 200);
}
TEST_CASE("parseWowColor with truncated string returns white", "[chat_markup]") {
ImVec4 c = ChatMarkupParser::parseWowColor("|cFF", 0);
CHECK(c.x == 1.0f);
CHECK(c.y == 1.0f);
CHECK(c.z == 1.0f);
CHECK(c.w == 1.0f);
}