Kelsidavis-WoWee/tests/test_macro_evaluator.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

528 lines
17 KiB
C++

// Tests for MacroEvaluator — WoW macro conditional parsing and evaluation.
// Phase 4.5 of chat_panel_ref.md.
#include <catch_amalgamated.hpp>
#include "ui/chat/macro_evaluator.hpp"
#include "ui/chat/i_game_state.hpp"
#include "ui/chat/i_modifier_state.hpp"
#include <unordered_map>
#include <unordered_set>
using namespace wowee::ui;
// ── Mock IGameState ─────────────────────────────────────────
class MockGameState : public IGameState {
public:
// GUIDs
uint64_t playerGuid = 1;
uint64_t targetGuid = 2;
uint64_t focusGuid = 0;
uint64_t petGuid = 0;
uint64_t mouseoverGuid = 0;
// State flags
bool inCombat = false;
bool mounted = false;
bool swimming = false;
bool flying = false;
bool casting = false;
bool channeling = false;
bool stealthed = false;
bool pet = false;
bool inGroup = false;
bool inRaid = false;
bool indoors = false;
// Numeric
uint8_t talentSpec = 0;
uint32_t vehicleId = 0;
uint32_t castSpellId = 0;
std::string castSpellName;
// Entity states (guid → exists, dead, hostile)
struct EntityInfo { bool exists = true; bool dead = false; bool hostile = false; };
std::unordered_map<uint64_t, EntityInfo> entities;
// Form / aura
bool formAura = false;
// Aura checks: map of "spellname_debuff" → true
std::unordered_set<std::string> auras;
// IGameState implementation
uint64_t getPlayerGuid() const override { return playerGuid; }
uint64_t getTargetGuid() const override { return targetGuid; }
uint64_t getFocusGuid() const override { return focusGuid; }
uint64_t getPetGuid() const override { return petGuid; }
uint64_t getMouseoverGuid() const override { return mouseoverGuid; }
bool isInCombat() const override { return inCombat; }
bool isMounted() const override { return mounted; }
bool isSwimming() const override { return swimming; }
bool isFlying() const override { return flying; }
bool isCasting() const override { return casting; }
bool isChanneling() const override { return channeling; }
bool isStealthed() const override { return stealthed; }
bool hasPet() const override { return pet; }
bool isInGroup() const override { return inGroup; }
bool isInRaid() const override { return inRaid; }
bool isIndoors() const override { return indoors; }
uint8_t getActiveTalentSpec() const override { return talentSpec; }
uint32_t getVehicleId() const override { return vehicleId; }
uint32_t getCurrentCastSpellId() const override { return castSpellId; }
std::string getSpellName(uint32_t /*spellId*/) const override { return castSpellName; }
bool hasAuraByName(uint64_t /*targetGuid*/, const std::string& spellName,
bool wantDebuff) const override {
std::string key = spellName + (wantDebuff ? "_debuff" : "_buff");
// Lowercase for comparison
for (char& c : key) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
return auras.count(key) > 0;
}
bool hasFormAura() const override { return formAura; }
bool entityExists(uint64_t guid) const override {
if (guid == 0 || guid == static_cast<uint64_t>(-1)) return false;
auto it = entities.find(guid);
return it != entities.end() && it->second.exists;
}
bool entityIsDead(uint64_t guid) const override {
auto it = entities.find(guid);
return it != entities.end() && it->second.dead;
}
bool entityIsHostile(uint64_t guid) const override {
auto it = entities.find(guid);
return it != entities.end() && it->second.hostile;
}
private:
// Need these for unordered_set/map
struct StringHash { size_t operator()(const std::string& s) const { return std::hash<std::string>{}(s); } };
};
// ── Mock IModifierState ─────────────────────────────────────
class MockModState : public IModifierState {
public:
bool shift = false;
bool ctrl = false;
bool alt = false;
bool isShiftHeld() const override { return shift; }
bool isCtrlHeld() const override { return ctrl; }
bool isAltHeld() const override { return alt; }
};
// ── Helper ──────────────────────────────────────────────────
struct TestFixture {
MockGameState gs;
MockModState ms;
MacroEvaluator eval{gs, ms};
std::string run(const std::string& input) {
uint64_t tgt;
return eval.evaluate(input, tgt);
}
std::pair<std::string, uint64_t> runWithTarget(const std::string& input) {
uint64_t tgt;
std::string result = eval.evaluate(input, tgt);
return {result, tgt};
}
};
// ── Basic parsing tests ─────────────────────────────────────
TEST_CASE("No conditionals returns input as-is", "[macro_eval]") {
TestFixture f;
CHECK(f.run("Fireball") == "Fireball");
}
TEST_CASE("Empty string returns empty", "[macro_eval]") {
TestFixture f;
CHECK(f.run("") == "");
}
TEST_CASE("Whitespace-only returns empty", "[macro_eval]") {
TestFixture f;
CHECK(f.run(" ") == "");
}
TEST_CASE("Semicolon-separated alternatives: first matches", "[macro_eval]") {
TestFixture f;
CHECK(f.run("Fireball; Frostbolt") == "Fireball");
}
TEST_CASE("Default fallback after conditions", "[macro_eval]") {
TestFixture f;
f.gs.inCombat = false;
CHECK(f.run("[combat] Attack; Heal") == "Heal");
}
// ── Combat conditions ───────────────────────────────────────
TEST_CASE("[combat] true when in combat", "[macro_eval]") {
TestFixture f;
f.gs.inCombat = true;
CHECK(f.run("[combat] Attack; Heal") == "Attack");
}
TEST_CASE("[combat] false when not in combat", "[macro_eval]") {
TestFixture f;
f.gs.inCombat = false;
CHECK(f.run("[combat] Attack; Heal") == "Heal");
}
TEST_CASE("[nocombat] true when not in combat", "[macro_eval]") {
TestFixture f;
f.gs.inCombat = false;
CHECK(f.run("[nocombat] Heal; Attack") == "Heal");
}
TEST_CASE("[nocombat] false when in combat", "[macro_eval]") {
TestFixture f;
f.gs.inCombat = true;
CHECK(f.run("[nocombat] Heal; Attack") == "Attack");
}
// ── Modifier conditions ─────────────────────────────────────
TEST_CASE("[mod:shift] true when shift held", "[macro_eval]") {
TestFixture f;
f.ms.shift = true;
CHECK(f.run("[mod:shift] Polymorph; Fireball") == "Polymorph");
}
TEST_CASE("[mod:shift] false when shift not held", "[macro_eval]") {
TestFixture f;
f.ms.shift = false;
CHECK(f.run("[mod:shift] Polymorph; Fireball") == "Fireball");
}
TEST_CASE("[mod:ctrl] true when ctrl held", "[macro_eval]") {
TestFixture f;
f.ms.ctrl = true;
CHECK(f.run("[mod:ctrl] Decurse; Fireball") == "Decurse");
}
TEST_CASE("[mod:alt] true when alt held", "[macro_eval]") {
TestFixture f;
f.ms.alt = true;
CHECK(f.run("[mod:alt] Special; Normal") == "Special");
}
TEST_CASE("[nomod] true when no modifier held", "[macro_eval]") {
TestFixture f;
CHECK(f.run("[nomod] Normal; Modified") == "Normal");
}
TEST_CASE("[nomod] false when shift held", "[macro_eval]") {
TestFixture f;
f.ms.shift = true;
CHECK(f.run("[nomod] Normal; Modified") == "Modified");
}
// ── Target specifiers ───────────────────────────────────────
TEST_CASE("[@player] sets target override to player guid", "[macro_eval]") {
TestFixture f;
f.gs.playerGuid = 42;
auto [result, tgt] = f.runWithTarget("[@player] Heal");
CHECK(result == "Heal");
CHECK(tgt == 42);
}
TEST_CASE("[@focus] sets target override to focus guid", "[macro_eval]") {
TestFixture f;
f.gs.focusGuid = 99;
auto [result, tgt] = f.runWithTarget("[@focus] Heal");
CHECK(result == "Heal");
CHECK(tgt == 99);
}
TEST_CASE("[@pet] fails when no pet", "[macro_eval]") {
TestFixture f;
f.gs.petGuid = 0;
CHECK(f.run("[@pet] Mend Pet; Steady Shot") == "Steady Shot");
}
TEST_CASE("[@pet] succeeds when pet exists", "[macro_eval]") {
TestFixture f;
f.gs.petGuid = 50;
auto [result, tgt] = f.runWithTarget("[@pet] Mend Pet; Steady Shot");
CHECK(result == "Mend Pet");
CHECK(tgt == 50);
}
TEST_CASE("[@mouseover] fails when no mouseover", "[macro_eval]") {
TestFixture f;
f.gs.mouseoverGuid = 0;
CHECK(f.run("[@mouseover] Heal; Flash Heal") == "Flash Heal");
}
TEST_CASE("[@mouseover] succeeds when mouseover exists", "[macro_eval]") {
TestFixture f;
f.gs.mouseoverGuid = 77;
auto [result, tgt] = f.runWithTarget("[@mouseover] Heal; Flash Heal");
CHECK(result == "Heal");
CHECK(tgt == 77);
}
TEST_CASE("[target=focus] sets target override", "[macro_eval]") {
TestFixture f;
f.gs.focusGuid = 33;
auto [result, tgt] = f.runWithTarget("[target=focus] Polymorph");
CHECK(result == "Polymorph");
CHECK(tgt == 33);
}
// ── Entity conditions (exists/dead/help/harm) ───────────────
TEST_CASE("[exists] true when target entity exists", "[macro_eval]") {
TestFixture f;
f.gs.targetGuid = 10;
f.gs.entities[10] = {true, false, false};
CHECK(f.run("[exists] Attack; Buff") == "Attack");
}
TEST_CASE("[exists] false when no target", "[macro_eval]") {
TestFixture f;
f.gs.targetGuid = 0;
CHECK(f.run("[exists] Attack; Buff") == "Buff");
}
TEST_CASE("[dead] true when target is dead", "[macro_eval]") {
TestFixture f;
f.gs.targetGuid = 10;
f.gs.entities[10] = {true, true, false};
CHECK(f.run("[dead] Resurrect; Heal") == "Resurrect");
}
TEST_CASE("[nodead] true when target is alive", "[macro_eval]") {
TestFixture f;
f.gs.targetGuid = 10;
f.gs.entities[10] = {true, false, false};
CHECK(f.run("[nodead] Heal; Resurrect") == "Heal");
}
TEST_CASE("[harm] true when target is hostile", "[macro_eval]") {
TestFixture f;
f.gs.targetGuid = 10;
f.gs.entities[10] = {true, false, true};
CHECK(f.run("[harm] Attack; Heal") == "Attack");
}
TEST_CASE("[help] true when target is friendly", "[macro_eval]") {
TestFixture f;
f.gs.targetGuid = 10;
f.gs.entities[10] = {true, false, false};
CHECK(f.run("[help] Heal; Attack") == "Heal");
}
// ── Chained conditions ──────────────────────────────────────
TEST_CASE("Chained conditions all must pass", "[macro_eval]") {
TestFixture f;
f.gs.inCombat = true;
f.ms.shift = true;
CHECK(f.run("[combat,mod:shift] Special; Normal") == "Special");
}
TEST_CASE("Chained conditions: one fails → whole group fails", "[macro_eval]") {
TestFixture f;
f.gs.inCombat = true;
f.ms.shift = false;
CHECK(f.run("[combat,mod:shift] Special; Normal") == "Normal");
}
// ── State conditions ────────────────────────────────────────
TEST_CASE("[mounted] true when mounted", "[macro_eval]") {
TestFixture f;
f.gs.mounted = true;
CHECK(f.run("[mounted] Dismount; Mount") == "Dismount");
}
TEST_CASE("[swimming] true when swimming", "[macro_eval]") {
TestFixture f;
f.gs.swimming = true;
CHECK(f.run("[swimming] Swim; Walk") == "Swim");
}
TEST_CASE("[flying] true when flying", "[macro_eval]") {
TestFixture f;
f.gs.flying = true;
CHECK(f.run("[flying] Land; Fly") == "Land");
}
TEST_CASE("[stealthed] true when stealthed", "[macro_eval]") {
TestFixture f;
f.gs.stealthed = true;
CHECK(f.run("[stealthed] Ambush; Sinister Strike") == "Ambush");
}
TEST_CASE("[indoors] true when indoors", "[macro_eval]") {
TestFixture f;
f.gs.indoors = true;
CHECK(f.run("[indoors] Walk; Mount") == "Walk");
}
TEST_CASE("[outdoors] true when not indoors", "[macro_eval]") {
TestFixture f;
f.gs.indoors = false;
CHECK(f.run("[outdoors] Mount; Walk") == "Mount");
}
TEST_CASE("[group] true when in group", "[macro_eval]") {
TestFixture f;
f.gs.inGroup = true;
CHECK(f.run("[group] Party Spell; Solo Spell") == "Party Spell");
}
TEST_CASE("[nogroup] true when not in group", "[macro_eval]") {
TestFixture f;
f.gs.inGroup = false;
CHECK(f.run("[nogroup] Solo; Party") == "Solo");
}
TEST_CASE("[raid] true when in raid", "[macro_eval]") {
TestFixture f;
f.gs.inRaid = true;
CHECK(f.run("[raid] Raid Spell; Normal") == "Raid Spell");
}
TEST_CASE("[pet] true when has pet", "[macro_eval]") {
TestFixture f;
f.gs.pet = true;
CHECK(f.run("[pet] Mend Pet; Steady Shot") == "Mend Pet");
}
TEST_CASE("[nopet] true when no pet", "[macro_eval]") {
TestFixture f;
f.gs.pet = false;
CHECK(f.run("[nopet] Steady Shot; Mend Pet") == "Steady Shot");
}
// ── Spec conditions ─────────────────────────────────────────
TEST_CASE("[spec:1] matches primary spec (0-based index 0)", "[macro_eval]") {
TestFixture f;
f.gs.talentSpec = 0;
CHECK(f.run("[spec:1] Heal; DPS") == "Heal");
}
TEST_CASE("[spec:2] matches secondary spec (0-based index 1)", "[macro_eval]") {
TestFixture f;
f.gs.talentSpec = 1;
CHECK(f.run("[spec:2] DPS; Heal") == "DPS");
}
TEST_CASE("[spec:1] fails when spec is secondary", "[macro_eval]") {
TestFixture f;
f.gs.talentSpec = 1;
CHECK(f.run("[spec:1] Heal; DPS") == "DPS");
}
// ── Form / stance ───────────────────────────────────────────
TEST_CASE("[noform] true when no form aura", "[macro_eval]") {
TestFixture f;
f.gs.formAura = false;
CHECK(f.run("[noform] Cast; Shift") == "Cast");
}
TEST_CASE("[noform] false when in form", "[macro_eval]") {
TestFixture f;
f.gs.formAura = true;
CHECK(f.run("[noform] Cast; Shift") == "Shift");
}
// ── Vehicle ─────────────────────────────────────────────────
TEST_CASE("[vehicle] true when in vehicle", "[macro_eval]") {
TestFixture f;
f.gs.vehicleId = 100;
CHECK(f.run("[vehicle] Vehicle Spell; Normal") == "Vehicle Spell");
}
TEST_CASE("[novehicle] true when not in vehicle", "[macro_eval]") {
TestFixture f;
f.gs.vehicleId = 0;
CHECK(f.run("[novehicle] Normal; Vehicle Spell") == "Normal");
}
// ── Casting / channeling ────────────────────────────────────
TEST_CASE("[casting] true when casting", "[macro_eval]") {
TestFixture f;
f.gs.casting = true;
CHECK(f.run("[casting] Interrupt; Cast") == "Interrupt");
}
TEST_CASE("[channeling] requires both casting and channeling", "[macro_eval]") {
TestFixture f;
f.gs.casting = true;
f.gs.channeling = true;
CHECK(f.run("[channeling] Stop; Continue") == "Stop");
}
TEST_CASE("[channeling] false when not channeling", "[macro_eval]") {
TestFixture f;
f.gs.casting = true;
f.gs.channeling = false;
CHECK(f.run("[channeling] Stop; Continue") == "Continue");
}
// ── Multiple alternatives with conditions ───────────────────
TEST_CASE("Three alternatives: first condition fails, second matches", "[macro_eval]") {
TestFixture f;
f.gs.inCombat = false;
f.gs.mounted = true;
CHECK(f.run("[combat] Attack; [mounted] Dismount; Idle") == "Dismount");
}
TEST_CASE("Three alternatives: all conditions fail, default used", "[macro_eval]") {
TestFixture f;
f.gs.inCombat = false;
f.gs.mounted = false;
CHECK(f.run("[combat] Attack; [mounted] Dismount; Idle") == "Idle");
}
TEST_CASE("No matching conditions and no default returns empty", "[macro_eval]") {
TestFixture f;
f.gs.inCombat = false;
CHECK(f.run("[combat] Attack") == "");
}
// ── Unknown conditions are permissive ───────────────────────
TEST_CASE("Unknown condition passes (permissive)", "[macro_eval]") {
TestFixture f;
CHECK(f.run("[unknowncond] Spell") == "Spell");
}
// ── targetOverride is -1 when no target specifier ───────────
TEST_CASE("No target specifier leaves override as -1", "[macro_eval]") {
TestFixture f;
auto [result, tgt] = f.runWithTarget("[combat] Spell");
CHECK(tgt == static_cast<uint64_t>(-1));
}
// ── Malformed input ──────────────────────────────────────────
TEST_CASE("Missing closing bracket skips alternative", "[macro_eval]") {
TestFixture f;
// [combat without ] → skip, then "Fallback" matches
CHECK(f.run("[combat Spell; Fallback") == "Fallback");
}
TEST_CASE("Empty brackets match (empty condition = true)", "[macro_eval]") {
TestFixture f;
CHECK(f.run("[] Spell") == "Spell");
}