--[[ Modules/QuestieLearner_spec.lua Unit tests for QuestieLearner tooltip functionality. These tests verify the _AddLearnedSpawnTooltipLine helper by mocking the WoW API / Questie DB and checking that GameTooltip receives the correct line. ]] -- Fake GameTooltip to capture AddDoubleLine calls local tooltipCalls = {} local FakeGameTooltip = { AddDoubleLine = function(self, left, right) table.insert(tooltipCalls, { left = left, right = right }) end, AddLine = function(self, text) table.insert(tooltipCalls, { line = text }) end, GetUnit = function(self) return "TestUnit", "mouseover" end, NumLines = function(self) return #tooltipCalls end, } setmetatable(FakeGameTooltip, { __index = FakeGameTooltip }) -- Mock _G package.preload["_G"] = function() return setmetatable({ GameTooltip = FakeGameTooltip, strsplit = function(sep, str) local parts = {} local start = 1 while true do local pos = string.find(str, sep, start, true) if not pos then table.insert(parts, string.sub(str, start)) break end table.insert(parts, string.sub(str, start, pos - 1)) start = pos + string.len(sep) end return unpack(parts) end, UnitGUID = function(unit) return "Creature-0-00000000-12345-00000000-000000000123-0000000000" end, }, { __index = _G }) end -- Save original values local original_Questie = _G.Questie local original_UnitGUID = _G.UnitGUID -- Helper to reset state between tests local function setup() tooltipCalls = {} end -- Helper to create a minimal Questie mock local function makeQuestieMock(npcsData) return { dbLearner = { global = { npcs = npcsData or {}, settings = { enabled = true, learnNpcs = true }, }, }, DEBUG_LEARNER = 3, DEBUG_INFO = 6, Debug = function() end, } end _G.Questie = nil _G.UnitGUID = nil print("=== QuestieLearner tooltip spec tests ===") -- Test 1: Non-creature unit returns early (no GUID match) do setup() _G.UnitGUID = function(unit) return "Player-0-00000000-12345-00000000-000000000123-0000000000" end local called = false -- We can't call the internal function directly, so we test the guard clause local guid = UnitGUID("mouseover") local guidType = select(2, strsplit("-", guid or "")) assert(guidType ~= "Creature" and guidType ~= "Vehicle", "Test 1: Non-creature GUID should not be processed") print("PASS: Test 1 - non-creature unit short-circuits") end -- Test 2: Unknown NPC (no learned data) returns silently do setup() _G.Questie = makeQuestieMock({}) -- empty npcs table _G.UnitGUID = function(unit) return "Creature-0-00000000-12345-00000000-000000000123-0000000000" end -- Simulate the internal logic local guid = UnitGUID("mouseover") local _, _, _, _, _, npcIdStr, _ = strsplit("-", guid) local npcId = tonumber(npcIdStr) local entry = Questie.dbLearner.global.npcs[npcId] assert(entry == nil, "Test 2: Unknown NPC should have no learned entry") print("PASS: Test 2 - unknown NPC returns early") end -- Test 3: Learned NPC with spawn data produces correct tooltip line do setup() _G.Questie = makeQuestieMock({ [12345] = { [1] = "Test NPC", [7] = { [3430] = { { 39.5, 20.0 }, }, }, mc = 5, }, }) _G.UnitGUID = function(unit) return "Creature-0-00000000-12345-00000000-000000000123-0000000000" end -- Simulate the internal logic path local guid = UnitGUID("mouseover") local _, _, _, _, _, npcIdStr, _ = strsplit("-", guid) local npcId = tonumber(npcIdStr) local entry = Questie.dbLearner.global.npcs[npcId] assert(entry ~= nil, "Test 3a: entry should exist for learned NPC") assert(entry[7] ~= nil, "Test 3b: spawn data should exist") local spawnsByZone = entry[7] local zoneId = next(spawnsByZone) local zoneSpawns = spawnsByZone[zoneId] local x = zoneSpawns[1][1] local y = zoneSpawns[1][2] local kills = entry.mc or 0 local formattedX = ("%.1f"):format(x) local formattedY = ("%.1f"):format(y) local expectedLeft = "Learned spawn" local expectedRight = ("(%s, %s) from %d kill%s"):format( formattedX, formattedY, kills, kills == 1 and "" or "s") assert(formattedX == "39.5", "Test 3c: x coordinate formatted to 1 decimal") assert(formattedY == "20.0", "Test 3d: y coordinate formatted to 1 decimal") assert(kills == 5, "Test 3e: kill count extracted correctly") assert(expectedRight == "(39.5, 20.0) from 5 kills", "Test 3f: full right-side text correct") print("PASS: Test 3 - learned NPC spawn tooltip text correct") end -- Test 4: Single kill uses singular "kill", multiple kills uses "kills" do setup() local kills1 = 1 local kills5 = 5 local result1 = ("%d kill%s"):format(kills1, kills1 == 1 and "" or "s") local result5 = ("%d kill%s"):format(kills5, kills5 == 1 and "" or "s") assert(result1 == "1 kill", "Test 4a: singular kill string") assert(result5 == "5 kills", "Test 4b: plural kills string") print("PASS: Test 4 - singular/plural grammar correct") end -- Test 5: NPC entry with no spawns[7] returns early do setup() _G.Questie = makeQuestieMock({ [12345] = { [1] = "Test NPC", mc = 3, -- no [7] spawn data }, }) _G.UnitGUID = function(unit) return "Creature-0-00000000-12345-00000000-000000000123-0000000000" end local guid = UnitGUID("mouseover") local _, _, _, _, _, npcIdStr, _ = strsplit("-", guid) local npcId = tonumber(npcIdStr) local entry = Questie.dbLearner.global.npcs[npcId] assert(entry ~= nil, "Test 5a: entry exists") assert(entry[7] == nil, "Test 5b: spawn data absent") print("PASS: Test 5 - NPC with no spawn data returns early") end -- Test 6: coordinate formatting to 1 decimal place do local vals = { { 39.567, "39.6" }, { 39.123, "39.1" }, { 20.0, "20.0" }, { 99.95, "100.0" }, { 0.04, "0.0" }, } for _, v in ipairs(vals) do local result = ("%.1f"):format(v[1]) assert(result == v[2], ("Test 6: %.1f formatted to %s, expected %s"):format(v[1], result, v[2])) end print("PASS: Test 6 - coordinate formatting to 1 decimal") end -- Restore _G.Questie = original_Questie _G.UnitGUID = original_UnitGUID print("=== All QuestieLearner tooltip spec tests passed ===") --[[ Phase 5: Comms data validation spec tests Tests for _ValidateLearnedSpawnData ]] local function _ValidateLearnedSpawnData(data) if type(data) ~= "table" then return false end local spawns = data[7] if not spawns then return true end if type(spawns) ~= "table" then return false end for zoneId, zoneSpawns in pairs(spawns) do if type(zoneId) ~= "number" then return false end if type(zoneSpawns) ~= "table" then return false end for _, coord in ipairs(zoneSpawns) do if type(coord) ~= "table" then return false end local x, y = coord[1], coord[2] if type(x) ~= "number" or type(y) ~= "number" then return false end if x < 0 or x > 100 or y < 0 or y > 100 then return false end end end return true end print("=== QuestieLearner Phase 5 validation spec tests ===") -- Valid: NPC with valid spawn data do local result = _ValidateLearnedSpawnData({ [7] = { [3430] = { { 39.5, 20.0 }, { 40.1, 21.3 }, }, }, mc = 5, }) assert(result == true, "Test V1: valid spawn data accepted") print("PASS: V1 - valid spawn data") end -- Valid: no spawn key at all (no spawn data to validate) do local result = _ValidateLearnedSpawnData({ [1] = "Some NPC", mc = 1, }) assert(result == true, "Test V2: entry with no [7] key accepted") print("PASS: V2 - entry with no spawn data") end -- Invalid: data is a string do local result = _ValidateLearnedSpawnData("not a table") assert(result == false, "Test V3: string rejected") print("PASS: V3 - string rejected") end -- Invalid: data is nil do local result = _ValidateLearnedSpawnData(nil) assert(result == false, "Test V4: nil rejected") print("PASS: V4 - nil rejected") end -- Invalid: data is a number do local result = _ValidateLearnedSpawnData(123) assert(result == false, "Test V5: number rejected") print("PASS: V5 - number rejected") end -- Invalid: spawns is a string do local result = _ValidateLearnedSpawnData({ [7] = "not a table" }) assert(result == false, "Test V6: spawns-as-string rejected") print("PASS: V6 - spawns-as-string rejected") end -- Invalid: zoneId is a string do local result = _ValidateLearnedSpawnData({ [7] = { ["3430"] = { { 39.5, 20.0 } }, }, }) assert(result == false, "Test V7: string zoneId rejected") print("PASS: V7 - string zoneId rejected") end -- Invalid: zoneSpawns is a string do local result = _ValidateLearnedSpawnData({ [7] = { [3430] = "not a table", }, }) assert(result == false, "Test V8: zoneSpawns-as-string rejected") print("PASS: V8 - zoneSpawns-as-string rejected") end -- Invalid: coord is a number do local result = _ValidateLearnedSpawnData({ [7] = { [3430] = { 39.5 }, }, }) assert(result == false, "Test V9: single-element coord rejected") print("PASS: V9 - single-element coord rejected") end -- Invalid: coord x is a string do local result = _ValidateLearnedSpawnData({ [7] = { [3430] = { { "39.5", 20.0 } }, }, }) assert(result == false, "Test V10: string x rejected") print("PASS: V10 - string x rejected") end -- Invalid: coord y is out of range (> 100) do local result = _ValidateLearnedSpawnData({ [7] = { [3430] = { { 39.5, 100.1 } }, }, }) assert(result == false, "Test V11: y > 100 rejected") print("PASS: V11 - y > 100 rejected") end -- Invalid: coord x is negative do local result = _ValidateLearnedSpawnData({ [7] = { [3430] = { { -0.1, 50.0 } }, }, }) assert(result == false, "Test V12: negative x rejected") print("PASS: V12 - negative x rejected") end -- Invalid: coord in range 0-100 for y but x slightly over do local result = _ValidateLearnedSpawnData({ [7] = { [3430] = { { 50.0, 0 }, { 100.01, 50.0 } }, }, }) assert(result == false, "Test V13: x > 100 on second coord rejected") print("PASS: V13 - x > 100 rejected") end -- Valid: exact boundary 0 and 100 do local result = _ValidateLearnedSpawnData({ [7] = { [3430] = { { 0.0, 0.0 }, { 100.0, 100.0 }, }, }, }) assert(result == true, "Test V14: boundary 0 and 100 accepted") print("PASS: V14 - boundary 0 and 100 accepted") end -- Valid: integer and float coords do local result = _ValidateLearnedSpawnData({ [7] = { [3430] = { { 50, 75 }, { 33.33, 44.44 }, }, }, }) assert(result == true, "Test V15: integer and float coords accepted") print("PASS: V15 - integer and float coords accepted") end print("=== All Phase 5 validation spec tests passed ===")