chore: untrack Tests/ and stray Modules spec file (dev-only)
Tests are for local dev only. .gitignore already excludes Tests/ and Modules/*_spec.lua from new adds, but the files were still tracked from prior commits. Untrack them with --cached so the working copies stay on disk for development but no longer ship in the live repo.
This commit is contained in:
@@ -1,401 +0,0 @@
|
||||
--[[
|
||||
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 ===")
|
||||
@@ -1,386 +0,0 @@
|
||||
-- Audit verification suite for workflow/performance-audit-2026-06-03-FULL.md (Pass 8).
|
||||
--
|
||||
-- These are STATIC source assertions: each test reads the actual file and checks
|
||||
-- that the audit's claim still matches the source. They run in desktop Lua (no
|
||||
-- WoW mock needed) and are re-runnable.
|
||||
--
|
||||
-- Tests in "false positives" and "structural facts" should ALWAYS pass.
|
||||
-- Tests in "confirmed bugs"/"confirmed perf" are a SNAPSHOT at HEAD 581634d:
|
||||
-- they pass while the finding is unfixed. When you land a fix, invert or remove
|
||||
-- the matching assertion (each is tagged with its Pass-8 ID, e.g. [B1]).
|
||||
|
||||
local function read(path)
|
||||
local f = assert(io.open(path, "r"), "cannot open " .. path)
|
||||
local c = f:read("*a")
|
||||
f:close()
|
||||
return c
|
||||
end
|
||||
|
||||
-- plain (non-pattern) substring search
|
||||
local function has(content, needle)
|
||||
return string.find(content, needle, 1, true) ~= nil
|
||||
end
|
||||
|
||||
-- count plain (non-pattern) occurrences
|
||||
local function count(content, needle)
|
||||
local n, pos = 0, 1
|
||||
while true do
|
||||
local s, e = string.find(content, needle, pos, true)
|
||||
if not s then break end
|
||||
n = n + 1
|
||||
pos = e + 1
|
||||
end
|
||||
return n
|
||||
end
|
||||
|
||||
local function startsWithBOM(path)
|
||||
local f = assert(io.open(path, "rb"), "cannot open " .. path)
|
||||
local head = f:read(3)
|
||||
f:close()
|
||||
return head == "\239\187\191"
|
||||
end
|
||||
|
||||
describe("Audit Pass 8.1 - Lua 5.0 incompatibility surface", function()
|
||||
it("[8.1] raw # length operator is used in TOC-loaded files (5.0 parse error)", function()
|
||||
-- The dominant 5.0 blocker the pass-6/7 scan missed entirely.
|
||||
assert.is_true(has(read("Localization/l10n.lua"), "#args"))
|
||||
assert.is_true(has(read("Modules/Map/QuestieMap.lua"), "#mapDrawQueue"))
|
||||
end)
|
||||
|
||||
it("[8.1] QuestieLib.tpack uses the ... expression (real 5.0 parse error)", function()
|
||||
assert.is_true(has(read("Modules/Libs/QuestieLib.lua"), 'n = select("#", ...), ...'))
|
||||
end)
|
||||
|
||||
it("[8.1] % is NOT used as a modulo operator (codebase uses math.mod)", function()
|
||||
-- math.mod shim exists; raw % only appears in format strings.
|
||||
assert.is_true(has(read("Modules/Libs/QuestieLoader.lua"), "math.mod"))
|
||||
end)
|
||||
|
||||
it("[8.1] no goto / bit32 / string.pack / utf8 in core runtime", function()
|
||||
local db = read("Database/QuestieDB.lua")
|
||||
assert.is_false(has(db, "goto "))
|
||||
assert.is_false(has(db, "bit32."))
|
||||
assert.is_false(has(db, "string.pack"))
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("Audit Pass 8.2 - corrected FALSE POSITIVES (must always pass)", function()
|
||||
it("[FP2] UnitFactionGroup('Player') capital-P is an established working pattern", function()
|
||||
-- Used across Corrections DB files that gate real entries; proves tokens
|
||||
-- are case-insensitive, so QuestieMenu.lua:114 is NOT a bug.
|
||||
assert.is_true(has(read("Database/Corrections/classicItemFixes.lua"),
|
||||
'UnitFactionGroup("Player")'))
|
||||
assert.is_true(has(read("Modules/QuestieMenu/QuestieMenu.lua"),
|
||||
'UnitFactionGroup("Player")'))
|
||||
end)
|
||||
|
||||
it("[FP1] Ascension_IsScalingEnabled is declared no-arg but called with questId (harmless lint)", function()
|
||||
local lib = read("Modules/Libs/QuestieLib.lua")
|
||||
assert.is_true(has(lib, "local function Ascension_IsScalingEnabled()"))
|
||||
assert.is_true(has(lib, "Ascension_IsScalingEnabled(questId)"))
|
||||
-- Lua discards extra args; this changes no behavior. Lint only.
|
||||
end)
|
||||
|
||||
it("[FP3] the select() shim exists, proving Lua 5.0 lacks select (pass-3 was wrong)", function()
|
||||
local loader = read("Modules/Libs/QuestieLoader.lua")
|
||||
assert.is_true(has(loader, "if not select then"))
|
||||
assert.is_true(has(loader, "select = function(index, ...)"))
|
||||
end)
|
||||
|
||||
it("[FP5] TaskQueue is wired (NOT dead code)", function()
|
||||
assert.is_true(has(read("Questie-X.toc"), "TaskQueue.lua"))
|
||||
assert.is_nil(io.open("Questie-X-Turtle.toc", "r"))
|
||||
local qq = read("Modules/Quest/QuestieQuest.lua")
|
||||
assert.is_true(has(qq, 'ImportModule("TaskQueue")'))
|
||||
assert.is_true(has(qq, "TaskQueue:Queue("))
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("Audit Pass 8.3 - confirmed BUGS (snapshot at HEAD; invert on fix)", function()
|
||||
it("[B1] IsComplete calls GetQuest(questId) twice in one expression", function()
|
||||
assert.is_true(has(read("Database/QuestieDB.lua"),
|
||||
"QuestieDB.GetQuest(questId) and QuestieDB.GetQuest(questId).ObjectiveData"))
|
||||
end)
|
||||
|
||||
it("[B2] QuestieOptionsTracker calls :Cancel() on the number fadeTickerValue", function()
|
||||
local content = read("Modules/Options/TrackerTab/QuestieOptionsTracker.lua")
|
||||
assert.is_true(count(content, "fadeTickerValue:Cancel()") >= 3)
|
||||
end)
|
||||
|
||||
it("[B3] alreadySentBandaid is declared once and never wiped (unbounded)", function()
|
||||
local content = read("Modules/QuestieAnnounce.lua")
|
||||
assert.is_true(has(content, "local alreadySentBandaid = {}"))
|
||||
assert.is_false(has(content, "wipe(alreadySentBandaid)"))
|
||||
assert.equals(1, count(content, "alreadySentBandaid = {}"))
|
||||
end)
|
||||
|
||||
it("[B4] factionReactions reads UnitFactionGroup at module load time", function()
|
||||
assert.is_true(has(read("Database/QuestieDB.lua"),
|
||||
'local playerFaction = UnitFactionGroup("player")'))
|
||||
end)
|
||||
|
||||
it("[B5] Questie-X.toc lists QuestieSlash.lua twice", function()
|
||||
assert.equals(2, count(read("Questie-X.toc"), "QuestieSlash.lua"))
|
||||
end)
|
||||
|
||||
it("[B6] correction files begin with a UTF-8 BOM", function()
|
||||
assert.is_true(startsWithBOM("Database/Corrections/tbcQuestFixes.lua"))
|
||||
assert.is_true(startsWithBOM("Database/Corrections/wotlkItemFixes.lua"))
|
||||
assert.is_true(startsWithBOM("Database/Corrections/wotlkQuestFixes.lua"))
|
||||
end)
|
||||
|
||||
it("[B7] _Qframe.BaseOnUpdate is referenced but never defined (dead glow ticker)", function()
|
||||
local frame = read("Modules/FramePool/QuestieFrame.lua")
|
||||
local pool = read("Modules/FramePool/QuestieFramePool.lua")
|
||||
assert.is_true(has(frame, "_Qframe.BaseOnUpdate")) -- assigned from
|
||||
assert.is_true(has(pool, "returnFrame.BaseOnUpdate")) -- gated on
|
||||
-- never defined anywhere:
|
||||
assert.is_false(has(frame, "function _Qframe.BaseOnUpdate"))
|
||||
assert.is_false(has(frame, "function _Qframe:BaseOnUpdate"))
|
||||
assert.is_false(has(frame, "_Qframe.BaseOnUpdate = function"))
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("Audit Pass 8.4 - confirmed PERFORMANCE findings (snapshot)", function()
|
||||
it("[P1] QuestieComms no longer re-serializes the accumulating list inside the loop", function()
|
||||
local comms = read("Modules/Network/QuestieComms.lua")
|
||||
assert.is_false(has(comms, "string.len(QuestieSerializer:Serialize(rawQuestList)) > 200"))
|
||||
assert.is_true(has(comms, "GetSerializedPacketSize(quest)"))
|
||||
end)
|
||||
|
||||
it("[P3] QuestieComms no longer front-removes its broadcast queues", function()
|
||||
local comms = read("Modules/Network/QuestieComms.lua")
|
||||
assert.is_false(has(comms, "tremove(blocks, 1)"))
|
||||
assert.is_false(has(comms, "tremove(_QuestieComms._nextBroadcastData, 1)"))
|
||||
assert.is_false(has(comms, "tremove(_QuestieComms._nextBroadcastDataV2, 1)"))
|
||||
assert.is_true(has(comms, "local function QueuePop(queue, queueState)"))
|
||||
end)
|
||||
|
||||
it("[P4] QuestieLib.tunpack is recursive (slow vararg unpack)", function()
|
||||
local lib = read("Modules/Libs/QuestieLib.lua")
|
||||
assert.is_true(has(lib, "local function recursion(i)"))
|
||||
assert.is_true(has(lib, "return tbl[i], recursion(i + 1)"))
|
||||
assert.is_false(has(lib, "return unpack(tbl, 1, tbl.n)")) -- the proposed fix
|
||||
end)
|
||||
|
||||
it("[P6] O(n) front-insert tinsert(t, 1, x) is used in the tooltip path", function()
|
||||
assert.is_true(has(read("Modules/Tooltips/Tooltip.lua"),
|
||||
"tinsert(tempObjectives, 1, objectiveInfo.text)"))
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("Audit Pass 9 - file-by-file findings (snapshot at HEAD)", function()
|
||||
it("[N1] bit library is used UNGUARDED in TOC files (1.12 runtime risk)", function()
|
||||
assert.is_true(has(read("Questie.lua"), "local band = bit.band"))
|
||||
assert.is_true(has(read("Database/QuestieDB.lua"), "local bitband = bit.band"))
|
||||
-- no bit shim in the compat layer:
|
||||
assert.is_false(has(read("Modules/QuestieCompat.lua"), "bit ="))
|
||||
-- ...while the vendored XXH lib DOES guard it (the contrast):
|
||||
assert.is_true(has(read("Libs/XXH_Lua_Lib/XXH_Lua_Lib.lua"), "bit and bit.band"))
|
||||
end)
|
||||
|
||||
it("[N2] strsplit is used in runtime but only shimmed in a test mock", function()
|
||||
assert.is_true(has(read("Modules/Network/QuestieComms.lua"), "strsplit"))
|
||||
assert.is_false(has(read("Modules/QuestieCompat.lua"), "strsplit"))
|
||||
end)
|
||||
|
||||
it("[N3] Options files use { ... } where {} was intended (5.0 parse error)", function()
|
||||
assert.is_true(has(read("Modules/Options/QuestieOptions.lua"), "tabs = { ... }"))
|
||||
assert.is_true(has(read("Modules/Options/ArrowTab/QuestieOptionsArrow.lua"), "= { ... }"))
|
||||
end)
|
||||
|
||||
it("[N4] QuestieCommsData indexes GetNPC/GetObject result with no nil-check", function()
|
||||
local d = read("Modules/Network/QuestieCommsData.lua")
|
||||
assert.is_true(has(d, "QuestieDB:GetNPC(objective.id).name"))
|
||||
assert.is_true(has(d, "QuestieDB:GetObject(objective.id).name"))
|
||||
-- the item branch right below DOES guard, proving the inconsistency:
|
||||
assert.is_true(has(d, "if(dbItem and dbItem.name and (not dbItem.Hidden)) then"))
|
||||
end)
|
||||
|
||||
it("[N5] explicit unpack replaced select(8, ...) in live quest/instance lookups", function()
|
||||
local player = read("Modules/QuestiePlayer.lua")
|
||||
local handler = read("Modules/Quest/QuestEventHandler.lua")
|
||||
local tracker = read("Modules/Tracker/QuestieTracker.lua")
|
||||
|
||||
assert.is_true(has(player, "local _, _, _, _, _, _, _, instanceMapID = GetInstanceInfo()"))
|
||||
assert.is_false(has(player, "select(8, GetInstanceInfo())"))
|
||||
|
||||
assert.is_true(has(handler, "local _, _, _, _, _, _, _, questLogQuestId = GetQuestLogTitle(questLogIndex)"))
|
||||
assert.is_false(has(handler, "select(8, GetQuestLogTitle(questLogIndex))"))
|
||||
|
||||
assert.is_true(has(tracker, "local _, _, _, _, _, _, _, questId = GetQuestLogTitle(questIndex)"))
|
||||
assert.is_true(has(tracker, "local _, _, _, _, _, _, _, questId = GetQuestLogTitle(index)"))
|
||||
assert.is_false(has(tracker, "select(8, GetQuestLogTitle(questIndex))"))
|
||||
assert.is_false(has(tracker, "select(8, GetQuestLogTitle(index))"))
|
||||
end)
|
||||
|
||||
it("[9.1->11.1] CORRECTED: % modulo IS used (the 'avoided' claim was wrong)", function()
|
||||
-- Pass 8-10 wrongly said % modulo = 0. Real modulo operators exist in
|
||||
-- Turtle-TOC files and are 5.0 parse errors that math.mod cannot rescue.
|
||||
assert.is_true(has(read("Modules/QuestieStream.lua"), "mod(val, 256)"))
|
||||
assert.is_true(has(read("Modules/QuestiePlayer.lua"), "% playerRaceFlagX2"))
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("Audit Pass 11 - gap-fill findings (snapshot at HEAD)", function()
|
||||
it("[11.1] % modulo operator is present in multiple Turtle-TOC files", function()
|
||||
assert.is_true(has(read("Database/QuestieDB.lua"), " % "))
|
||||
assert.is_true(has(read("Modules/QuestieLearner.lua"), " % "))
|
||||
end)
|
||||
|
||||
it("[G1] QuestieNameplate:UpdateNameplate re-splits the GUID and early-returns in-loop", function()
|
||||
local np = read("Modules/QuestieNameplate.lua")
|
||||
-- re-derives npcId from the guid on every update (cacheable):
|
||||
assert.is_true(has(np, 'strsplit("-", guid)'))
|
||||
-- the early return aborts the whole loop on a missing unit:
|
||||
assert.is_true(has(np, "if (not unitName) or (not npcId) then\n return"))
|
||||
end)
|
||||
|
||||
it("[G2] QuestieValidateGameCache has the unreachable isQuestLogGood guard", function()
|
||||
local v = read("Modules/QuestieValidateGameCache.lua")
|
||||
assert.is_true(has(v, "local isQuestLogGood = true"))
|
||||
assert.is_false(has(v, "isQuestLogGood = false")) -- never set false => guard is dead
|
||||
end)
|
||||
|
||||
it("[G3] QuestieCompat shims neither bit nor strsplit (N1/N2 gaps stand)", function()
|
||||
local c = read("Modules/QuestieCompat.lua")
|
||||
assert.is_false(has(c, "strsplit ="))
|
||||
assert.is_false(has(c, "bit ="))
|
||||
assert.is_true(has(c, "QuestieCompat.C_Timer")) -- but C_Timer IS polyfilled
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("Audit Pass 10 - additional performance findings (snapshot)", function()
|
||||
it("[PP1] a batch Query(id, keys) API exists that IsDoable does not use", function()
|
||||
local compiler = read("Database/compiler.lua")
|
||||
assert.is_true(has(compiler, "handle.Query = function(id, keys)")) -- batch reader exists
|
||||
local db = read("Database/QuestieDB.lua")
|
||||
-- IsDoable issues many single-key reads instead of one batch read:
|
||||
assert.is_true(count(db, "QueryQuestSingle(questId,") >= 8)
|
||||
end)
|
||||
|
||||
it("[PP2] hot non-fragile files repeat Questie.db.profile chains", function()
|
||||
assert.is_true(count(read("Modules/Tooltips/MapIconTooltip.lua"), "Questie.db.profile.") >= 8)
|
||||
assert.is_true(count(read("Modules/Map/QuestieMap.lua"), "Questie.db.profile.") >= 8)
|
||||
end)
|
||||
|
||||
it("[PP3] arrow target sort no longer allocates an inline comparator per refresh", function()
|
||||
assert.is_true(has(read("Modules/Arrow/QuestieArrow.lua"),
|
||||
"local function _SortTargetByDistance(a, b)"))
|
||||
assert.is_true(has(read("Modules/Arrow/QuestieArrow.lua"),
|
||||
"table.sort(sortedTargets, _SortTargetByDistance)"))
|
||||
assert.is_true(count(read("Modules/Network/QuestieComms.lua"), "table.sort(") >= 3)
|
||||
end)
|
||||
|
||||
it("[PP4] arrow avoids re-setting unchanged distance text every throttled tick", function()
|
||||
assert.is_true(has(read("Modules/Arrow/QuestieArrow.lua"),
|
||||
"objectiveFrame._lastDistanceText ~= distanceText"))
|
||||
end)
|
||||
|
||||
it("[PP7] arrow performance throttles are profile-backed and exposed in Arrow options", function()
|
||||
local arrow = read("Modules/Arrow/QuestieArrow.lua")
|
||||
local arrowOptions = read("Modules/Options/ArrowTab/QuestieOptionsArrow.lua")
|
||||
local advancedOptions = read("Modules/Options/AdvancedTab/QuestieOptionsAdvanced.lua")
|
||||
local defaults = read("Modules/Options/QuestieOptionsDefaults.lua")
|
||||
|
||||
assert.is_true(has(arrow, 'return _GetProfileNumber("arrowUpdateThrottle"'))
|
||||
assert.is_true(has(arrow, 'return _GetProfileNumber("arrowRecalcInterval"'))
|
||||
assert.is_true(has(arrow, 'return _GetProfileNumber("arrowTrackerRefreshThrottle"'))
|
||||
assert.is_false(has(arrowOptions, "arrowPerformanceHeader"))
|
||||
assert.is_false(has(arrowOptions, "arrowUpdateThrottle"))
|
||||
assert.is_true(has(advancedOptions, "arrowPerformanceHeader"))
|
||||
assert.is_true(has(advancedOptions, "arrowUpdateThrottle"))
|
||||
assert.is_true(has(advancedOptions, "arrowRecalcInterval"))
|
||||
assert.is_true(has(advancedOptions, "arrowTrackerRefreshThrottle"))
|
||||
assert.is_true(has(defaults, "arrowUpdateThrottle = 0.05"))
|
||||
assert.is_true(has(defaults, "arrowRecalcInterval = 1.0"))
|
||||
assert.is_true(has(defaults, "arrowTrackerRefreshThrottle = 0.5"))
|
||||
end)
|
||||
|
||||
it("[L9] learner performance presets keep comms UI and broadcast gate synchronized", function()
|
||||
local advancedOptions = read("Modules/Options/AdvancedTab/QuestieOptionsAdvanced.lua")
|
||||
assert.is_true(has(advancedOptions, 'settings.learnerCommsIntensity = "fast"'))
|
||||
assert.is_true(has(advancedOptions, 'settings.learnerCommsIntensity = "normal"'))
|
||||
assert.is_true(has(advancedOptions, 'settings.learnerCommsIntensity = "low"'))
|
||||
assert.is_true(has(advancedOptions, "Questie.db.profile.learnerBroadcast = true"))
|
||||
end)
|
||||
|
||||
it("[C1] QuestieComms full quest-list throttles are profile-backed", function()
|
||||
local comms = read("Modules/Network/QuestieComms.lua")
|
||||
local defaults = read("Modules/Options/QuestieOptionsDefaults.lua")
|
||||
local broadcastQuestUpdatePos = comms:find("function _QuestieComms:BroadcastQuestUpdate", 1, true)
|
||||
local isQuestieCommsEnabledPos = comms:find("local IsQuestieCommsEnabled", 1, true)
|
||||
|
||||
assert.is_true(has(comms, 'GetProfileNumber("questieCommsQuestListPacketSize"'))
|
||||
assert.is_true(has(comms, 'GetProfileNumber("questieCommsQuestListInitialJitter"'))
|
||||
assert.is_true(has(comms, 'GetProfileNumber("questieCommsQuestListBlockInterval"'))
|
||||
assert.is_true(has(comms, "IsQuestieCommsEnabled = function()"))
|
||||
assert.is_true(isQuestieCommsEnabledPos ~= nil)
|
||||
assert.is_true(broadcastQuestUpdatePos ~= nil)
|
||||
assert.is_true(isQuestieCommsEnabledPos < broadcastQuestUpdatePos)
|
||||
assert.is_true(has(comms, "GetQuestListPacketSizeLimit()"))
|
||||
assert.is_true(has(comms, "GetQuestListInitialJitter()"))
|
||||
assert.is_true(has(comms, "GetQuestListBlockInterval()"))
|
||||
assert.is_true(has(defaults, "questieCommsEnabled = true"))
|
||||
assert.is_true(has(defaults, "questieCommsQuestListPacketSize = 200"))
|
||||
assert.is_true(has(defaults, "questieCommsQuestListInitialJitter = 3"))
|
||||
assert.is_true(has(defaults, "questieCommsQuestListBlockInterval = 3"))
|
||||
end)
|
||||
|
||||
it("[C2] QuestieComms performance controls are exposed in Advanced options", function()
|
||||
local advancedOptions = read("Modules/Options/AdvancedTab/QuestieOptionsAdvanced.lua")
|
||||
|
||||
assert.is_true(has(advancedOptions, "questieCommsPerformanceHeader"))
|
||||
assert.is_true(has(advancedOptions, "questieCommsEnabled"))
|
||||
assert.is_true(has(advancedOptions, "questieCommsQuestListPacketSize"))
|
||||
assert.is_true(has(advancedOptions, "questieCommsQuestListInitialJitter"))
|
||||
assert.is_true(has(advancedOptions, "questieCommsQuestListBlockInterval"))
|
||||
assert.is_true(has(advancedOptions, "Questie.db.profile.questieCommsEnabled == false"))
|
||||
end)
|
||||
|
||||
it("[L50] loader, serializer, and stream are wired for Lua 5.0 compatibility", function()
|
||||
local loader = read("Modules/Libs/QuestieLoader.lua")
|
||||
local serializer = read("Modules/Libs/QuestieSerializer.lua")
|
||||
local stream = read("Modules/QuestieStream.lua")
|
||||
|
||||
assert.is_true(has(loader, "bitlib.band = bitlib.band or band32"))
|
||||
assert.is_true(has(loader, "strsplit = function(separator, text, max)"))
|
||||
|
||||
assert.is_true(has(serializer, "local mod = math.mod"))
|
||||
assert.is_true(has(serializer, "mod(expo, 0x2)"))
|
||||
assert.is_true(has(serializer, "mod(b1, 0x80)"))
|
||||
assert.is_false(has(serializer, "expo % 0x2"))
|
||||
|
||||
assert.is_true(has(stream, "local mod = math.mod"))
|
||||
assert.is_true(has(stream, "table.getn(self._bin)"))
|
||||
assert.is_true(has(stream, "mod(val1, 256)"))
|
||||
assert.is_true(has(stream, "mod(val2, 256)"))
|
||||
assert.is_false(has(stream, "val1 % 256"))
|
||||
assert.is_false(has(stream, "val2 % 256"))
|
||||
end)
|
||||
|
||||
it("[L10] UNIT_DIED dedupe cannot suppress a later PARTY_KILL learner update", function()
|
||||
local learner = read("Modules/QuestieLearner.lua")
|
||||
|
||||
assert.is_true(has(learner, 'local lastEventType = type(last) == "table" and last.eventType or nil'))
|
||||
assert.is_true(has(learner, 'if eventType ~= "PARTY_KILL" or lastEventType == "PARTY_KILL" then'))
|
||||
assert.is_true(has(learner, '_Learner.killDebounce[dstGUID] = { ts = now, eventType = eventType }'))
|
||||
end)
|
||||
|
||||
it("[L11] object capture traces and gameobject learning are wired into the learner", function()
|
||||
local learner = read("Modules/QuestieLearner.lua")
|
||||
|
||||
assert.is_true(has(learner, 'local function TraceLearnerEntity(source, guid, unitType, entityId, name)'))
|
||||
assert.is_true(has(learner, 'TraceLearnerEntity("mouseover", guid, unitType, entityId, name)'))
|
||||
assert.is_true(has(learner, 'TraceLearnerEntity("target", guid, unitType, entityId, name)'))
|
||||
assert.is_true(has(learner, 'TraceLearnerEntity("loot_target", targetGuid, targetType, targetId, UnitName("target"))'))
|
||||
assert.is_true(has(learner, 'GetLootSourceInfo then'))
|
||||
assert.is_true(has(learner, 'TraceLearnerEntity("loot_source", sourceGuid, nil, sourceQty, lootName)'))
|
||||
assert.is_true(has(learner, 'function QuestieLearner:OnGameObjectUsed(objectId)'))
|
||||
assert.is_true(has(learner, 'frame:RegisterEvent("GAMEOBJECT_USED")'))
|
||||
assert.is_true(has(learner, 'self:OnGameObjectUsed(arg1)'))
|
||||
assert.is_true(has(learner, 'TraceLearnerEntity("gossip", npcGuid, unitType, id, name)'))
|
||||
assert.is_true(has(learner, 'self:LearnObject(entityId, name)'))
|
||||
assert.is_true(has(learner, 'if unitType == "GameObject" then'))
|
||||
end)
|
||||
end)
|
||||
@@ -1,16 +0,0 @@
|
||||
local function read(path)
|
||||
local f = assert(io.open(path, "r"), "cannot open " .. path)
|
||||
local c = f:read("*a")
|
||||
f:close()
|
||||
return c
|
||||
end
|
||||
|
||||
describe("Questie available quests guard", function()
|
||||
it("skips unavailable quests before touching tagInfoWasCached", function()
|
||||
local content = read("Modules/Quest/AvailableQuests.lua")
|
||||
assert.is_true(content:find("if not quest then", 1, true) ~= nil)
|
||||
assert.is_true(content:find("Skipping unavailable quest during draw", 1, true) ~= nil)
|
||||
assert.is_true(content:find("unavailableQuestLogged[questId]", 1, true) ~= nil)
|
||||
assert.is_true(content:find("quest.tagInfoWasCached = true", 1, true) ~= nil)
|
||||
end)
|
||||
end)
|
||||
@@ -1,86 +0,0 @@
|
||||
describe("Questie debug throttle", function()
|
||||
local originalPrint
|
||||
local originalGetTime
|
||||
local originalImport
|
||||
|
||||
before_each(function()
|
||||
dofile("Tests/wow_api_mock.lua")
|
||||
Questie.Print = nil
|
||||
_G.IsLoggedIn = function()
|
||||
return false
|
||||
end
|
||||
_G.C_Timer = _G.C_Timer or {}
|
||||
_G.C_Timer.After = function()
|
||||
end
|
||||
originalImport = QuestieLoader.ImportModule
|
||||
QuestieLoader.ImportModule = function(self, name)
|
||||
if name == "QuestieLib" then
|
||||
return { AddonPath = "" }
|
||||
elseif name == "QuestieValidateGameCache" then
|
||||
return { StartCheck = function() end }
|
||||
end
|
||||
return originalImport(self, name)
|
||||
end
|
||||
dofile("Questie.lua")
|
||||
|
||||
originalPrint = _G.print
|
||||
originalGetTime = _G.GetTime
|
||||
|
||||
_G._questie_debug_lines = {}
|
||||
_G.print = function(...)
|
||||
local parts = {}
|
||||
for i = 1, select("#", ...) do
|
||||
parts[#parts + 1] = tostring(select(i, ...))
|
||||
end
|
||||
table.insert(_G._questie_debug_lines, table.concat(parts, " "))
|
||||
end
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
_G.print = originalPrint
|
||||
_G.GetTime = originalGetTime
|
||||
QuestieLoader.ImportModule = originalImport
|
||||
_G._questie_debug_lines = nil
|
||||
end)
|
||||
|
||||
it("suppresses rapid debug lines until the throttle window reopens", function()
|
||||
local now = 0
|
||||
_G.GetTime = function()
|
||||
return now
|
||||
end
|
||||
|
||||
Questie.db.profile.debugEnabled = true
|
||||
Questie.db.profile.debugEnabledPrint = true
|
||||
Questie.db.profile.debugLevel = Questie.DEBUG_INFO
|
||||
Questie.db.profile.debugMessageThrottle = 0.5
|
||||
|
||||
Questie:Debug(Questie.DEBUG_INFO, "alpha")
|
||||
now = 0.1
|
||||
Questie:Debug(Questie.DEBUG_INFO, "beta")
|
||||
now = 0.6
|
||||
Questie:Debug(Questie.DEBUG_INFO, "gamma")
|
||||
|
||||
assert.equals(2, table.getn(_G._questie_debug_lines))
|
||||
assert.is_true(string.find(_G._questie_debug_lines[1], "alpha", 1, true) ~= nil)
|
||||
assert.is_true(string.find(_G._questie_debug_lines[2], "gamma", 1, true) ~= nil)
|
||||
end)
|
||||
|
||||
it("throttles critical messages too because fatal output is separate", function()
|
||||
local now = 0
|
||||
_G.GetTime = function()
|
||||
return now
|
||||
end
|
||||
|
||||
Questie.db.profile.debugEnabled = true
|
||||
Questie.db.profile.debugEnabledPrint = true
|
||||
Questie.db.profile.debugLevel = Questie.DEBUG_INFO + Questie.DEBUG_CRITICAL
|
||||
Questie.db.profile.debugMessageThrottle = 0.5
|
||||
|
||||
Questie:Debug(Questie.DEBUG_INFO, "alpha")
|
||||
now = 0.1
|
||||
Questie:Debug(Questie.DEBUG_CRITICAL, "boom")
|
||||
|
||||
assert.equals(1, table.getn(_G._questie_debug_lines))
|
||||
assert.is_true(string.find(_G._questie_debug_lines[1], "alpha", 1, true) ~= nil)
|
||||
end)
|
||||
end)
|
||||
@@ -1,65 +0,0 @@
|
||||
local function read_file(path)
|
||||
local handle = assert(io.open(path, "r"))
|
||||
local content = handle:read("*a")
|
||||
handle:close()
|
||||
return content
|
||||
end
|
||||
|
||||
local function find_lines(content, needle)
|
||||
local lines = {}
|
||||
for line in content:gmatch("[^\r\n]+") do
|
||||
if line:find(needle, 1, true) then
|
||||
lines[#lines + 1] = line
|
||||
end
|
||||
end
|
||||
return lines
|
||||
end
|
||||
|
||||
local function count_occurrences(content, needle)
|
||||
local count = 0
|
||||
local start = 1
|
||||
while true do
|
||||
local first, last = content:find(needle, start, true)
|
||||
if not first then
|
||||
break
|
||||
end
|
||||
count = count + 1
|
||||
start = last + 1
|
||||
end
|
||||
return count
|
||||
end
|
||||
|
||||
describe("Questie error policy", function()
|
||||
it("routes Questie:Error through debug critical and keeps a fatal printer", function()
|
||||
local questie = read_file("Questie.lua")
|
||||
|
||||
assert.is.truthy(questie:find("function Questie:Error(...)", 1, true))
|
||||
assert.is.truthy(questie:find("Questie:Debug(Questie.DEBUG_CRITICAL", 1, true))
|
||||
assert.is.truthy(questie:find("function Questie:Fatal(...)", 1, true))
|
||||
assert.is.truthy(questie:find("[FATAL]", 1, true))
|
||||
end)
|
||||
|
||||
it("routes missing quest spam through debug critical", function()
|
||||
local quest = read_file("Modules/Quest/QuestieQuest.lua")
|
||||
local comms = read_file("Modules/Network/QuestieComms.lua")
|
||||
assert.are.equal(2, count_occurrences(quest, "Questie:Debug(Questie.DEBUG_CRITICAL, l10n("))
|
||||
assert.are.equal(2, count_occurrences(comms, "Questie:Debug(Questie.DEBUG_CRITICAL, l10n("))
|
||||
end)
|
||||
|
||||
it("keeps startup-breaking conditions on the fatal path", function()
|
||||
local versionCheck = read_file("Modules/VersionCheck.lua")
|
||||
local init = read_file("Modules/QuestieInit.lua")
|
||||
local eventHandler = read_file("Modules/QuestieEventHandler.lua")
|
||||
local versionLines = find_lines(versionCheck, "ERROR inside NewAddon")
|
||||
local initLines = find_lines(init, "Module not loaded correctly")
|
||||
local eventLines = find_lines(eventHandler, "Config DB from saved variables")
|
||||
|
||||
assert.are.equal(1, #versionLines)
|
||||
assert.are.equal(1, #initLines)
|
||||
assert.are.equal(1, #eventLines)
|
||||
|
||||
assert.is.truthy(versionLines[1]:find("Questie:Fatal", 1, true))
|
||||
assert.is.truthy(initLines[1]:find("Questie:Fatal", 1, true))
|
||||
assert.is.truthy(eventLines[1]:find("Questie:Fatal", 1, true))
|
||||
end)
|
||||
end)
|
||||
@@ -1,33 +0,0 @@
|
||||
describe("Questie item name safety", function()
|
||||
local function read(path)
|
||||
local f = assert(io.open(path, "r"), "cannot open " .. path)
|
||||
local c = f:read("*a")
|
||||
f:close()
|
||||
return c
|
||||
end
|
||||
|
||||
before_each(function()
|
||||
dofile("Tests/wow_api_mock.lua")
|
||||
GetItemInfo = function()
|
||||
error("GetItemInfo should not be called for invalid item ids")
|
||||
end
|
||||
dofile("Database/QuestieDB.lua")
|
||||
end)
|
||||
|
||||
it("returns a placeholder instead of calling GetItemInfo for invalid item ids", function()
|
||||
local item = Item:CreateFromItemID(nil)
|
||||
assert.equals("item:nil", item:GetItemName())
|
||||
end)
|
||||
|
||||
it("also handles non-numeric item ids safely", function()
|
||||
local item = Item:CreateFromItemID("bad-id")
|
||||
assert.equals("item:bad-id", item:GetItemName())
|
||||
end)
|
||||
|
||||
it("skips malformed item objectives without an item id", function()
|
||||
local lib = read("Modules/Libs/QuestieLib.lua")
|
||||
assert.is_true(string.find(lib, "local itemId = objectiveDB.Id", 1, true) ~= nil)
|
||||
assert.is_true(string.find(lib, "if not itemId then", 1, true) ~= nil)
|
||||
assert.is_true(string.find(lib, "QuestieDB.itemDataOverrides[itemId]", 1, true) ~= nil)
|
||||
end)
|
||||
end)
|
||||
@@ -1,869 +0,0 @@
|
||||
local function read(path)
|
||||
local f = assert(io.open(path, "r"), "cannot open " .. path)
|
||||
local c = f:read("*a")
|
||||
f:close()
|
||||
return c
|
||||
end
|
||||
|
||||
local function has(content, needle)
|
||||
return string.find(content, needle, 1, true) ~= nil
|
||||
end
|
||||
|
||||
describe("QuestieLearner data source mode", function()
|
||||
it("adds a mode selector and explicit fallback options in the database tab", function()
|
||||
local dbOptions = read("Modules/Options/DatabaseTab/QuestieOptionsDatabase.lua")
|
||||
local advanced = read("Modules/Options/AdvancedTab/QuestieOptionsAdvanced.lua")
|
||||
assert.is_true(has(dbOptions, "Data Source Mode"))
|
||||
assert.is_true(has(dbOptions, "auto = l10n(\"Auto (current behavior)\")"))
|
||||
assert.is_true(has(dbOptions, "learner = l10n(\"Learner Only\")"))
|
||||
assert.is_true(has(dbOptions, "static = l10n(\"Static Only\")"))
|
||||
assert.is_true(has(dbOptions, "none = l10n(\"Neither (base DB only)\")"))
|
||||
assert.is_true(has(dbOptions, "local function GetLearnerSelectedMode()"))
|
||||
assert.is_true(has(dbOptions, "get = function() return GetLearnerSelectedMode() end"))
|
||||
assert.is_true(has(dbOptions, "Runtime mode:"))
|
||||
assert.is_true(has(advanced, "local function RefreshLearnerRuntime()"))
|
||||
assert.is_true(has(advanced, "RefreshLearnerRuntime()"))
|
||||
end)
|
||||
|
||||
it("defaults the learner mode to auto and exposes the live refresh hook", function()
|
||||
local learner = read("Modules/QuestieLearner.lua")
|
||||
local defaults = read("Modules/Options/QuestieOptionsDefaults.lua")
|
||||
assert.is_true(has(defaults, "dataSourceMode = \"auto\""))
|
||||
assert.is_true(has(learner, "function QuestieLearner:GetDataSourceMode()"))
|
||||
assert.is_true(has(learner, "function QuestieLearner:IsLearnerLiveEnabled()"))
|
||||
assert.is_true(has(learner, "function QuestieLearner:ApplyDataSourceMode()"))
|
||||
assert.is_true(has(learner, "function QuestieLearner:RefreshLiveState()"))
|
||||
end)
|
||||
|
||||
it("gates static suppression and tooltip fallback on the selected mode", function()
|
||||
local quest = read("Modules/Quest/QuestieQuest.lua")
|
||||
local priv = read("Modules/Quest/QuestieQuestPrivates.lua")
|
||||
local tip = read("Modules/Tooltips/Tooltip.lua")
|
||||
assert.is_true(has(quest, "dataSourceMode == \"auto\" or dataSourceMode == \"learner\""))
|
||||
assert.is_true(has(priv, "dataSourceMode == \"none\""))
|
||||
assert.is_true(has(tip, "mode ~= \"static\" and mode ~= \"none\""))
|
||||
end)
|
||||
|
||||
it("exposes learner tooltip controls and tooltip resize support", function()
|
||||
local general = read("Modules/Options/GeneralTab/QuestieOptionsGeneral.lua")
|
||||
local defaults = read("Modules/Options/QuestieOptionsDefaults.lua")
|
||||
local advanced = read("Modules/Options/AdvancedTab/QuestieOptionsAdvanced.lua")
|
||||
local tip = read("Modules/Tooltips/Tooltip.lua")
|
||||
local handler = read("Modules/Tooltips/TooltipHandler.lua")
|
||||
local learner = read("Modules/QuestieLearner.lua")
|
||||
assert.is_true(has(defaults, "learnerTooltips = true"))
|
||||
assert.is_true(has(defaults, "learnerTooltipShowSpawn = true"))
|
||||
assert.is_true(has(defaults, "learnerTooltipShowConfidence = true"))
|
||||
assert.is_true(has(defaults, "learnerTooltipShowTotalSpawns = true"))
|
||||
assert.is_true(has(defaults, "learnerTooltipAutoResize = true"))
|
||||
assert.is_true(has(defaults, "learnerTooltipUseSecondary = false"))
|
||||
assert.is_true(has(general, "Learner Tooltips"))
|
||||
assert.is_true(has(general, "Enable learner tooltips"))
|
||||
assert.is_true(has(general, "Show total spawns learned"))
|
||||
assert.is_true(has(general, "Use secondary learner tooltip"))
|
||||
assert.is_true(has(tip, "function QuestieTooltips:ResizeTooltip(tooltip)"))
|
||||
assert.is_true(has(handler, "QuestieTooltips:ResizeTooltip(GameTooltip)"))
|
||||
assert.is_true(has(handler, "QuestieTooltips:ResizeTooltip(self)"))
|
||||
assert.is_true(has(learner, "learnerTooltipShowSpawn"))
|
||||
assert.is_true(has(learner, "_CountLearnedNpcSpawns(entry)"))
|
||||
assert.is_true(has(learner, "_ShowLearnerTooltipFrame(GameTooltip, rendered)"))
|
||||
assert.is_true(has(advanced, "QuestieOptionsUtils:Delay(0.05, QuestieQuest.SmoothReset"))
|
||||
assert.is_true(has(advanced, "QuestieOptionsUtils:Delay(0.05, QuestieOptions.ClusterRedraw"))
|
||||
end)
|
||||
|
||||
it("allows learner mode to draw pins from a single learned spawn when needed", function()
|
||||
local priv = read("Modules/Quest/QuestieQuestPrivates.lua")
|
||||
assert.is_true(has(priv, "local staticHasSpawns = spawns and next(spawns) ~= nil"))
|
||||
assert.is_true(has(priv, "local canUseLearnerSpawns = dataSourceMode == \"learner\""))
|
||||
assert.is_true(has(priv, "or not staticHasSpawns"))
|
||||
end)
|
||||
|
||||
it("keeps learner-only pin builders off the static DB lookup path", function()
|
||||
local priv = read("Modules/Quest/QuestieQuestPrivates.lua")
|
||||
assert.is_true(has(priv, "local npcData = QuestieDB:GetNPC(npcId)"))
|
||||
assert.is_true(has(priv, "local name = npcData and npcData.name or nil"))
|
||||
assert.is_true(has(priv, "local spawns = npcData and npcData.spawns or {}"))
|
||||
assert.is_true(has(priv, "local rank = npcData and npcData.rank"))
|
||||
assert.is_true(has(priv, "local objectData = QuestieDB:GetObject(objectId)"))
|
||||
assert.is_true(has(priv, "local name = objectData and objectData.name or nil"))
|
||||
assert.is_true(has(priv, "local spawns = objectData and objectData.spawns or {}"))
|
||||
end)
|
||||
|
||||
it("maps object objectives from learner object captures before refresh", function()
|
||||
local learner = read("Modules/QuestieLearner.lua")
|
||||
assert.is_true(has(learner, "_Learner.recentObjects = _Learner.recentObjects or {}"))
|
||||
assert.is_true(has(learner, "function QuestieLearner:LearnQuestObjectiveObject(questId, objectId, objText, objectiveIndex)"))
|
||||
assert.is_true(has(learner, "objType == \"object\""))
|
||||
assert.is_true(has(learner, "self:LearnQuestObjectiveObject(questId, objectId, objText, j)"))
|
||||
assert.is_true(has(learner, "_Learner.recentObjects[objectId]"))
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("QuestieLearner missing base DB fallback", function()
|
||||
local QuestieLearner
|
||||
|
||||
before_each(function()
|
||||
dofile("Tests/wow_api_mock.lua")
|
||||
Questie.dbLearner.global.settings.enabled = false
|
||||
Questie.dbLearner.global.settings.dataSourceMode = "static"
|
||||
QuestieDB.baseDatabaseMissing = true
|
||||
QuestieDB.IsBaseDatabaseMissing = function()
|
||||
return true
|
||||
end
|
||||
QuestieLearner = dofile("Modules/QuestieLearner.lua")
|
||||
end)
|
||||
|
||||
it("forces learner mode and live recording when the base DB is missing", function()
|
||||
assert.equals("learner", QuestieLearner:GetDataSourceMode())
|
||||
assert.is_true(QuestieLearner:IsEnabled())
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("QuestieLearner learner mode activation", function()
|
||||
local QuestieLearner
|
||||
|
||||
before_each(function()
|
||||
dofile("Tests/wow_api_mock.lua")
|
||||
Questie.dbLearner.global.settings.enabled = false
|
||||
Questie.dbLearner.global.settings.dataSourceMode = "learner"
|
||||
QuestieDB.baseDatabaseMissing = false
|
||||
QuestieDB.IsBaseDatabaseMissing = function()
|
||||
return false
|
||||
end
|
||||
QuestieLearner = dofile("Modules/QuestieLearner.lua")
|
||||
end)
|
||||
|
||||
it("re-enables learner recording when learner mode is applied", function()
|
||||
QuestieLearner:ApplyDataSourceMode()
|
||||
assert.is_true(Questie.dbLearner.global.settings.enabled)
|
||||
assert.is_true(QuestieLearner:IsEnabled())
|
||||
end)
|
||||
|
||||
it("refreshes live learner caches and quest pins when runtime settings change", function()
|
||||
local clearCount = 0
|
||||
local resetCount = 0
|
||||
|
||||
QuestieDB.ClearModeCaches = function()
|
||||
clearCount = clearCount + 1
|
||||
end
|
||||
QuestieQuest.SmoothReset = function()
|
||||
resetCount = resetCount + 1
|
||||
end
|
||||
|
||||
QuestieLearner:RefreshLiveState()
|
||||
|
||||
assert.is_true(clearCount >= 1)
|
||||
assert.is_true(resetCount >= 1)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("QuestieDB learner mode merges learner overrides", function()
|
||||
before_each(function()
|
||||
dofile("Tests/wow_api_mock.lua")
|
||||
dofile("Database/QuestieDB.lua")
|
||||
dofile("Database/npcDB.lua")
|
||||
Questie.dbLearner.global.settings.enabled = true
|
||||
Questie.dbLearner.global.settings.dataSourceMode = "learner"
|
||||
Questie.dbLearner.global.npcs = {
|
||||
[7001] = {
|
||||
[1] = "Learner NPC",
|
||||
},
|
||||
}
|
||||
QuestieDB.npcDataOverrides = {
|
||||
[7001] = {
|
||||
[1] = "Learner NPC",
|
||||
[7] = {
|
||||
[44] = {
|
||||
{ 12.5, 34.5 },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
QuestieDB.private = QuestieDB.private or {}
|
||||
QuestieDB.private.npcCache = {}
|
||||
end)
|
||||
|
||||
it("prefers the learner override spawn table when raw learner data is incomplete", function()
|
||||
local npc = QuestieDB:GetNPC(7001)
|
||||
assert.is_table(npc)
|
||||
assert.is_table(npc.spawns)
|
||||
assert.is_table(npc.spawns[44])
|
||||
assert.equals(1, table.getn(npc.spawns[44]))
|
||||
assert.equals(12.5, npc.spawns[44][1][1])
|
||||
assert.equals(34.5, npc.spawns[44][1][2])
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("QuestieDB quest build guards missing objective tables", function()
|
||||
local originalImport
|
||||
local originalQueryItemSingle
|
||||
|
||||
before_each(function()
|
||||
dofile("Tests/wow_api_mock.lua")
|
||||
|
||||
local questieLib = {
|
||||
GetTbcLevel = function()
|
||||
return 1, 1
|
||||
end,
|
||||
}
|
||||
local questieCorrections = {
|
||||
hiddenQuests = {},
|
||||
killCreditObjectiveFirst = {},
|
||||
}
|
||||
|
||||
originalImport = QuestieLoader.ImportModule
|
||||
QuestieLoader.ImportModule = function(self, name)
|
||||
if name == "QuestieLib" then
|
||||
return questieLib
|
||||
end
|
||||
if name == "QuestieCorrections" then
|
||||
return questieCorrections
|
||||
end
|
||||
return originalImport(self, name)
|
||||
end
|
||||
|
||||
dofile("Database/QuestieDB.lua")
|
||||
dofile("Database/questDB.lua")
|
||||
|
||||
Questie.dbLearner.global.settings.enabled = true
|
||||
Questie.dbLearner.global.settings.dataSourceMode = "learner"
|
||||
QuestieDB.private.questCache = {}
|
||||
QuestieDB.questDataOverrides = {}
|
||||
|
||||
originalQueryItemSingle = QuestieDB.QueryItemSingle
|
||||
QuestieDB.QueryItemSingle = function(itemId, field)
|
||||
if field == "name" then
|
||||
return "Test Item " .. tostring(itemId)
|
||||
end
|
||||
return originalQueryItemSingle(itemId, field)
|
||||
end
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
if originalQueryItemSingle then
|
||||
QuestieDB.QueryItemSingle = originalQueryItemSingle
|
||||
end
|
||||
if originalImport then
|
||||
QuestieLoader.ImportModule = originalImport
|
||||
end
|
||||
end)
|
||||
|
||||
it("does not crash when required source items exist but objectives are missing", function()
|
||||
local questId = 900001
|
||||
QuestieDB.questDataOverrides[questId] = {
|
||||
[1] = "Source Item Quest",
|
||||
[21] = {
|
||||
[1] = 20482,
|
||||
},
|
||||
}
|
||||
|
||||
local ok, quest = pcall(function()
|
||||
return QuestieDB:GetQuest(questId)
|
||||
end)
|
||||
|
||||
assert.is_true(ok)
|
||||
assert.is_table(quest)
|
||||
assert.is_table(quest.SpecialObjectives)
|
||||
assert.is_table(quest.SpecialObjectives[20482])
|
||||
assert.equals("Test Item 20482", quest.SpecialObjectives[20482].Description)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("QuestieDB missing quest logs are deduplicated", function()
|
||||
local originalImport
|
||||
local originalDebug
|
||||
local originalQueryQuest
|
||||
|
||||
before_each(function()
|
||||
dofile("Tests/wow_api_mock.lua")
|
||||
|
||||
local questieLib = {
|
||||
GetTbcLevel = function()
|
||||
return 1, 1
|
||||
end,
|
||||
}
|
||||
local questieCorrections = {
|
||||
hiddenQuests = {},
|
||||
killCreditObjectiveFirst = {},
|
||||
}
|
||||
|
||||
originalImport = QuestieLoader.ImportModule
|
||||
QuestieLoader.ImportModule = function(self, name)
|
||||
if name == "QuestieLib" then
|
||||
return questieLib
|
||||
end
|
||||
if name == "QuestieCorrections" then
|
||||
return questieCorrections
|
||||
end
|
||||
return originalImport(self, name)
|
||||
end
|
||||
|
||||
dofile("Database/QuestieDB.lua")
|
||||
dofile("Database/questDB.lua")
|
||||
|
||||
QuestieDB.private.questCache = {}
|
||||
QuestieDB.private.missingQuestLog = {}
|
||||
|
||||
originalDebug = Questie.Debug
|
||||
Questie.Debug = function(self, level, ...)
|
||||
_G._questie_debug_calls = (_G._questie_debug_calls or 0) + 1
|
||||
end
|
||||
|
||||
originalQueryQuest = QuestieDB.QueryQuest
|
||||
QuestieDB.QueryQuest = function()
|
||||
return nil
|
||||
end
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
if originalQueryQuest then
|
||||
QuestieDB.QueryQuest = originalQueryQuest
|
||||
end
|
||||
if originalDebug then
|
||||
Questie.Debug = originalDebug
|
||||
end
|
||||
if originalImport then
|
||||
QuestieLoader.ImportModule = originalImport
|
||||
end
|
||||
_G._questie_debug_calls = nil
|
||||
end)
|
||||
|
||||
it("logs a missing quest only once per quest id", function()
|
||||
local questId = 900002
|
||||
|
||||
assert.is_nil(QuestieDB:GetQuest(questId))
|
||||
assert.is_nil(QuestieDB:GetQuest(questId))
|
||||
assert.equals(1, _G._questie_debug_calls)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("QuestieLearner quest accept resolution", function()
|
||||
local QuestieLearner
|
||||
local originalGetNumQuestLogEntries
|
||||
local originalGetQuestLogSelection
|
||||
local originalGetQuestLogTitle
|
||||
local originalGetQuestIDFromLogIndex
|
||||
local originalGetQuestLogIndexByID
|
||||
local originalLearnQuest
|
||||
local originalUnitGUID
|
||||
|
||||
before_each(function()
|
||||
dofile("Tests/wow_api_mock.lua")
|
||||
Questie.dbLearner.global.settings.enabled = true
|
||||
Questie.dbLearner.global.settings.dataSourceMode = "learner"
|
||||
|
||||
originalGetNumQuestLogEntries = _G.GetNumQuestLogEntries
|
||||
originalGetQuestLogSelection = QuestieCompat.GetQuestLogSelection
|
||||
originalGetQuestLogTitle = QuestieCompat.GetQuestLogTitle
|
||||
originalGetQuestIDFromLogIndex = QuestieCompat.GetQuestIDFromLogIndex
|
||||
originalGetQuestLogIndexByID = QuestieCompat.GetQuestLogIndexByID
|
||||
originalUnitGUID = _G.UnitGUID
|
||||
|
||||
_G.GetNumQuestLogEntries = function()
|
||||
return 1
|
||||
end
|
||||
_G.UnitGUID = function()
|
||||
return nil
|
||||
end
|
||||
|
||||
QuestieCompat.GetQuestLogSelection = function()
|
||||
return 1
|
||||
end
|
||||
|
||||
QuestieCompat.GetQuestLogTitle = function(index)
|
||||
if index == 1 then
|
||||
return "Real Quest", 10, nil, false, nil, nil, nil, 4321
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
QuestieCompat.GetQuestIDFromLogIndex = function(index)
|
||||
if index == 1 then
|
||||
return 4321
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
QuestieCompat.GetQuestLogIndexByID = function(questId)
|
||||
if questId == 4321 then
|
||||
return 1
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
QuestieLearner = dofile("Modules/QuestieLearner.lua")
|
||||
originalLearnQuest = QuestieLearner.LearnQuest
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
QuestieLearner.LearnQuest = originalLearnQuest
|
||||
_G.GetNumQuestLogEntries = originalGetNumQuestLogEntries
|
||||
_G.UnitGUID = originalUnitGUID
|
||||
QuestieCompat.GetQuestLogSelection = originalGetQuestLogSelection
|
||||
QuestieCompat.GetQuestLogTitle = originalGetQuestLogTitle
|
||||
QuestieCompat.GetQuestIDFromLogIndex = originalGetQuestIDFromLogIndex
|
||||
QuestieCompat.GetQuestLogIndexByID = originalGetQuestLogIndexByID
|
||||
end)
|
||||
|
||||
it("uses a real quest log entry instead of raw accepted event args", function()
|
||||
local capturedQuestId = nil
|
||||
QuestieLearner.LearnQuest = function(self, questId, data)
|
||||
capturedQuestId = questId
|
||||
end
|
||||
|
||||
QuestieLearner:OnQuestAccepted(615514513, nil)
|
||||
|
||||
assert.equals(4321, capturedQuestId)
|
||||
end)
|
||||
|
||||
it("refuses impossible quest ids when they do not resolve to the quest log", function()
|
||||
local capturedQuestId = nil
|
||||
QuestieCompat.GetQuestLogSelection = function()
|
||||
return nil
|
||||
end
|
||||
QuestieCompat.GetQuestLogTitle = function()
|
||||
return nil
|
||||
end
|
||||
QuestieCompat.GetQuestIDFromLogIndex = function()
|
||||
return nil
|
||||
end
|
||||
QuestieCompat.GetQuestLogIndexByID = function()
|
||||
return nil
|
||||
end
|
||||
QuestieLearner.LearnQuest = function(self, questId, data)
|
||||
capturedQuestId = questId
|
||||
end
|
||||
|
||||
QuestieLearner:OnQuestAccepted(615514513, nil)
|
||||
|
||||
assert.is_nil(capturedQuestId)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("QuestieLearner quest turn-in resolution", function()
|
||||
local QuestieLearner
|
||||
local originalLearnQuest
|
||||
local originalGetQuestID
|
||||
local originalGetRewardText
|
||||
local originalUnitGUID
|
||||
|
||||
before_each(function()
|
||||
dofile("Tests/wow_api_mock.lua")
|
||||
|
||||
originalGetQuestID = _G.GetQuestID
|
||||
originalGetRewardText = _G.GetRewardText
|
||||
originalUnitGUID = _G.UnitGUID
|
||||
|
||||
Questie.dbLearner.global.settings.enabled = true
|
||||
Questie.dbLearner.global.settings.dataSourceMode = "learner"
|
||||
|
||||
_G.GetQuestID = function()
|
||||
return 4321
|
||||
end
|
||||
_G.GetRewardText = function()
|
||||
return "Reward text"
|
||||
end
|
||||
_G.UnitGUID = function()
|
||||
return nil
|
||||
end
|
||||
|
||||
QuestieLearner = dofile("Modules/QuestieLearner.lua")
|
||||
originalLearnQuest = QuestieLearner.LearnQuest
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
QuestieLearner.LearnQuest = originalLearnQuest
|
||||
_G.GetQuestID = originalGetQuestID
|
||||
_G.GetRewardText = originalGetRewardText
|
||||
_G.UnitGUID = originalUnitGUID
|
||||
end)
|
||||
|
||||
it("rejects malformed turn-in quest ids unless they resolve to a recent completion", function()
|
||||
local capturedQuestId = nil
|
||||
QuestieLearner.LearnQuest = function(self, questId, data)
|
||||
capturedQuestId = questId
|
||||
end
|
||||
|
||||
QuestieLearner:OnQuestTurnedIn(545915281, nil, nil)
|
||||
|
||||
assert.is_nil(capturedQuestId)
|
||||
end)
|
||||
|
||||
it("uses the recent quest-complete cache when the raw turn-in quest id is malformed", function()
|
||||
local capturedQuestId = nil
|
||||
QuestieLearner.LearnQuest = function(self, questId, data)
|
||||
capturedQuestId = questId
|
||||
end
|
||||
|
||||
QuestieLearner:OnQuestComplete()
|
||||
QuestieLearner:OnQuestTurnedIn(545915281, nil, nil)
|
||||
|
||||
assert.equals(4321, capturedQuestId)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("QuestieLearner GUID and loot learning", function()
|
||||
local QuestieLearner
|
||||
local originalGetNPC
|
||||
local originalGetItemInfo
|
||||
local originalLearnItem
|
||||
local originalLearnItemDrop
|
||||
local originalGetLootSourceInfo
|
||||
local originalGetNumLootItems
|
||||
local originalGetLootSlotInfo
|
||||
local originalGetLootSlotLink
|
||||
|
||||
before_each(function()
|
||||
dofile("Tests/wow_api_mock.lua")
|
||||
Questie.dbLearner.global.settings.enabled = true
|
||||
Questie.dbLearner.global.settings.dataSourceMode = "learner"
|
||||
Questie.dbLearner.global.settings.learnItems = true
|
||||
Questie.dbLearner.global.settings.learnNpcs = true
|
||||
|
||||
QuestieDB.npcData = {
|
||||
[15297] = { [1] = "Arcanist Helion" },
|
||||
}
|
||||
originalGetNPC = QuestieDB.GetNPC
|
||||
QuestieDB.GetNPC = function(self, id)
|
||||
if id == 168 then
|
||||
return { name = "Something Else" }
|
||||
end
|
||||
return originalGetNPC and originalGetNPC(self, id) or nil
|
||||
end
|
||||
|
||||
originalGetItemInfo = _G.GetItemInfo
|
||||
_G.GetItemInfo = function(link)
|
||||
if link == "item:20470" then
|
||||
return "Quest Token", nil, nil, 1, 1, 3, 1, nil, nil, nil, nil, 1, 0
|
||||
end
|
||||
return nil
|
||||
end
|
||||
originalGetLootSourceInfo = _G.GetLootSourceInfo
|
||||
_G.GetLootSourceInfo = function()
|
||||
return "Creature-0-0-0-0-15297-0000000000", 1
|
||||
end
|
||||
originalGetNumLootItems = _G.GetNumLootItems
|
||||
_G.GetNumLootItems = function()
|
||||
return 1
|
||||
end
|
||||
originalGetLootSlotInfo = _G.GetLootSlotInfo
|
||||
_G.GetLootSlotInfo = function()
|
||||
return nil, "Quest Token", nil, nil, 1
|
||||
end
|
||||
originalGetLootSlotLink = _G.GetLootSlotLink
|
||||
_G.GetLootSlotLink = function()
|
||||
return "item:20470"
|
||||
end
|
||||
dofile("Modules/QuestieLearner.lua")
|
||||
QuestieLearner = _G.QuestieLearner
|
||||
originalLearnItem = QuestieLearner.LearnItem
|
||||
originalLearnItemDrop = QuestieLearner.LearnItemDrop
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
QuestieDB.GetNPC = originalGetNPC
|
||||
_G.GetItemInfo = originalGetItemInfo
|
||||
_G.GetLootSourceInfo = originalGetLootSourceInfo
|
||||
_G.GetNumLootItems = originalGetNumLootItems
|
||||
_G.GetLootSlotInfo = originalGetLootSlotInfo
|
||||
_G.GetLootSlotLink = originalGetLootSlotLink
|
||||
if QuestieLearner then
|
||||
QuestieLearner.LearnItem = originalLearnItem
|
||||
QuestieLearner.LearnItemDrop = originalLearnItemDrop
|
||||
end
|
||||
end)
|
||||
|
||||
it("prefers the exact NPC name over a mismatched GUID entry id", function()
|
||||
local resolvedId, unitType = QuestieLearner:ResolveNpcIdFromGuidAndName("Creature-0-0-0-0-168-0000000000", "Arcanist Helion")
|
||||
assert.equals(15297, resolvedId)
|
||||
assert.equals("Creature", unitType)
|
||||
end)
|
||||
|
||||
it("ignores non-quest loot items and only learns quest-item drops", function()
|
||||
local learnedItemId = nil
|
||||
local dropItemId = nil
|
||||
local dropNpcId = nil
|
||||
|
||||
QuestieLearner.LearnItem = function(self, itemId, name, itemLevel, requiredLevel, itemClassId, itemSubClassId)
|
||||
if itemClassId == 12 then
|
||||
learnedItemId = itemId
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
QuestieLearner.LearnItemDrop = function(self, itemId, npcId)
|
||||
dropItemId = itemId
|
||||
dropNpcId = npcId
|
||||
end
|
||||
|
||||
local originalGetItemInfo = _G.GetItemInfo
|
||||
_G.GetItemInfo = function(link)
|
||||
if link == "item:20470" then
|
||||
return "Quest Token", nil, nil, 1, 1, nil, nil, nil, nil, nil, nil, 1, 0
|
||||
end
|
||||
return originalGetItemInfo(link)
|
||||
end
|
||||
|
||||
QuestieLearner:OnLootOpened()
|
||||
|
||||
assert.is_nil(learnedItemId)
|
||||
assert.is_nil(dropItemId)
|
||||
assert.is_nil(dropNpcId)
|
||||
|
||||
_G.GetItemInfo = originalGetItemInfo
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("QuestieDB learner source fallback", function()
|
||||
before_each(function()
|
||||
dofile("Tests/wow_api_mock.lua")
|
||||
dofile("Database/QuestieDB.lua")
|
||||
dofile("Database/npcDB.lua")
|
||||
dofile("Database/objectDB.lua")
|
||||
dofile("Database/questDB.lua")
|
||||
dofile("Database/itemDB.lua")
|
||||
|
||||
Questie.dbLearner.global.settings.enabled = true
|
||||
Questie.dbLearner.global.settings.dataSourceMode = "learner"
|
||||
Questie.dbLearner.global.npcs = {
|
||||
[9001] = {
|
||||
[1] = "Learner Whelp",
|
||||
[7] = {
|
||||
[44] = {
|
||||
{ 12.3, 45.6 },
|
||||
},
|
||||
},
|
||||
[8] = {
|
||||
[44] = {
|
||||
{ 13.3, 46.6 },
|
||||
},
|
||||
},
|
||||
[9] = 44,
|
||||
},
|
||||
}
|
||||
Questie.dbLearner.global.objects = {
|
||||
[9002] = {
|
||||
[1] = "Learner Cache",
|
||||
[4] = {
|
||||
[44] = {
|
||||
{ 11.1, 22.2 },
|
||||
},
|
||||
},
|
||||
[5] = 44,
|
||||
},
|
||||
}
|
||||
|
||||
QuestieDB.QueryNPC = function() return nil end
|
||||
QuestieDB.QueryObject = function() return nil end
|
||||
QuestieDB.QueryQuest = function() return nil end
|
||||
QuestieDB.QueryItem = function() return nil end
|
||||
QuestieDB.private.npcCache = {}
|
||||
QuestieDB.private.objectCache = {}
|
||||
QuestieDB.private.questCache = {}
|
||||
QuestieDB.private.itemCache = {}
|
||||
end)
|
||||
|
||||
it("returns learner NPC data when static queries are unavailable", function()
|
||||
local npc = QuestieDB:GetNPC(9001)
|
||||
assert.is_table(npc)
|
||||
assert.equals("Learner Whelp", npc.name)
|
||||
assert.is_table(npc.spawns)
|
||||
assert.is_table(npc.waypoints)
|
||||
assert.equals(44, npc.zoneID)
|
||||
end)
|
||||
|
||||
it("turns learner kill evidence into NPC spawn coordinates immediately in learner mode", function()
|
||||
Questie.dbLearner.global.npcs[9003] = {
|
||||
[1] = "Learner Kill",
|
||||
[8] = {
|
||||
[101] = {
|
||||
zoneId = 44,
|
||||
x = 18.5,
|
||||
y = 27.25,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
local npc = QuestieDB:GetNPC(9003)
|
||||
assert.is_table(npc)
|
||||
assert.is_table(npc.spawns)
|
||||
assert.is_table(npc.spawns[44])
|
||||
assert.equals(1, #npc.spawns[44])
|
||||
assert.equals(18.5, npc.spawns[44][1][1])
|
||||
assert.equals(27.25, npc.spawns[44][1][2])
|
||||
end)
|
||||
|
||||
it("collapses many nearby kills (distinct GUIDs) into one spawn pin", function()
|
||||
-- Five kills of respawns at the same spot: distinct GUID keys, slightly
|
||||
-- drifting player coords. This must render as ONE pin, not five.
|
||||
Questie.dbLearner.global.npcs[9005] = {
|
||||
[1] = "Respawning Boar",
|
||||
[8] = {
|
||||
[201] = { zoneId = 44, x = 50.0, y = 50.0 },
|
||||
[202] = { zoneId = 44, x = 50.4, y = 50.3 },
|
||||
[203] = { zoneId = 44, x = 49.7, y = 50.6 },
|
||||
[204] = { zoneId = 44, x = 50.9, y = 49.8 },
|
||||
[205] = { zoneId = 44, x = 50.2, y = 50.1 },
|
||||
},
|
||||
}
|
||||
|
||||
local npc = QuestieDB:GetNPC(9005)
|
||||
assert.is_table(npc)
|
||||
assert.is_table(npc.spawns[44])
|
||||
assert.equals(1, #npc.spawns[44])
|
||||
end)
|
||||
|
||||
it("keeps genuinely separate spawn locations as distinct pins", function()
|
||||
Questie.dbLearner.global.npcs[9006] = {
|
||||
[1] = "Field Boars",
|
||||
[8] = {
|
||||
[301] = { zoneId = 44, x = 20.0, y = 20.0 },
|
||||
[302] = { zoneId = 44, x = 20.3, y = 20.2 }, -- same spot as 301
|
||||
[303] = { zoneId = 44, x = 70.0, y = 65.0 }, -- far corner
|
||||
},
|
||||
}
|
||||
|
||||
local npc = QuestieDB:GetNPC(9006)
|
||||
assert.is_table(npc)
|
||||
assert.is_table(npc.spawns[44])
|
||||
assert.equals(2, #npc.spawns[44])
|
||||
end)
|
||||
|
||||
it("honors the spawn dedup radius knob (0 disables proximity merge)", function()
|
||||
Questie.dbLearner.global.settings.spawnDedupRadius = 0
|
||||
Questie.dbLearner.global.npcs[9007] = {
|
||||
[1] = "Drifting Kills",
|
||||
[8] = {
|
||||
[401] = { zoneId = 44, x = 50.0, y = 50.0 },
|
||||
[402] = { zoneId = 44, x = 50.4, y = 50.3 },
|
||||
[403] = { zoneId = 44, x = 49.7, y = 50.6 },
|
||||
[404] = { zoneId = 44, x = 50.9, y = 49.8 },
|
||||
[405] = { zoneId = 44, x = 50.2, y = 50.1 },
|
||||
},
|
||||
}
|
||||
|
||||
local npc = QuestieDB:GetNPC(9007)
|
||||
assert.is_table(npc)
|
||||
assert.is_table(npc.spawns[44])
|
||||
-- With merging off, each distinct kill coordinate stays its own pin.
|
||||
assert.equals(5, #npc.spawns[44])
|
||||
end)
|
||||
|
||||
it("widens merging when the dedup radius is increased", function()
|
||||
Questie.dbLearner.global.settings.spawnDedupRadius = 12
|
||||
Questie.dbLearner.global.npcs[9008] = {
|
||||
[1] = "Loose Cluster",
|
||||
[8] = {
|
||||
[501] = { zoneId = 44, x = 40.0, y = 40.0 },
|
||||
[502] = { zoneId = 44, x = 48.0, y = 46.0 }, -- ~10 away: merges at radius 12
|
||||
},
|
||||
}
|
||||
|
||||
local npc = QuestieDB:GetNPC(9008)
|
||||
assert.is_table(npc)
|
||||
assert.is_table(npc.spawns[44])
|
||||
assert.equals(1, #npc.spawns[44])
|
||||
end)
|
||||
|
||||
it("returns learner object data when static queries are unavailable", function()
|
||||
local obj = QuestieDB:GetObject(9002)
|
||||
assert.is_table(obj)
|
||||
assert.equals("Learner Cache", obj.name)
|
||||
assert.is_table(obj.spawns)
|
||||
assert.equals(44, obj.zoneID)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("QuestieDB partial base DB missing", function()
|
||||
before_each(function()
|
||||
dofile("Tests/wow_api_mock.lua")
|
||||
dofile("Database/QuestieDB.lua")
|
||||
end)
|
||||
|
||||
it("does not report the base DB missing when only one store failed", function()
|
||||
QuestieDB.baseDatabaseMissing = true
|
||||
QuestieDB.baseDatabaseMissingKeys = { itemData = true }
|
||||
assert.is_false(QuestieDB:IsBaseDatabaseMissing())
|
||||
assert.is_true(QuestieDB:IsStoreMissing("itemData"))
|
||||
assert.is_false(QuestieDB:IsStoreMissing("npcData"))
|
||||
end)
|
||||
|
||||
it("reports the base DB missing only when every core store failed", function()
|
||||
QuestieDB.baseDatabaseMissing = true
|
||||
QuestieDB.baseDatabaseMissingKeys = {
|
||||
npcData = true, objectData = true, questData = true, itemData = true,
|
||||
}
|
||||
assert.is_true(QuestieDB:IsBaseDatabaseMissing())
|
||||
end)
|
||||
|
||||
it("honors the static selection for a present store even when another is missing", function()
|
||||
QuestieDB.baseDatabaseMissing = true
|
||||
QuestieDB.baseDatabaseMissingKeys = { itemData = true }
|
||||
Questie.dbLearner.global.settings.dataSourceMode = "static"
|
||||
Questie.dbLearner.global.npcs = {
|
||||
[9100] = { [1] = "Should Not Win", [7] = { [44] = { { 1, 2 } } }, [9] = 44 },
|
||||
}
|
||||
QuestieDB.private.npcCache = {}
|
||||
local queried = false
|
||||
QuestieDB.QueryNPC = function() queried = true; return nil end
|
||||
|
||||
pcall(function() QuestieDB:GetNPC(9100) end)
|
||||
assert.is_true(queried)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("QuestieDB mode cohesion", function()
|
||||
before_each(function()
|
||||
dofile("Tests/wow_api_mock.lua")
|
||||
dofile("Database/QuestieDB.lua")
|
||||
dofile("Database/npcDB.lua")
|
||||
Questie.dbLearner.global.settings.enabled = true
|
||||
Questie.dbLearner.global.npcs = {
|
||||
[9200] = { [1] = "Learner Only NPC", [7] = { [44] = { { 5, 6 } } }, [9] = 44 },
|
||||
}
|
||||
QuestieDB.QueryNPC = function() return nil end
|
||||
QuestieDB.baseDatabaseMissing = false
|
||||
QuestieDB.baseDatabaseMissingKeys = {}
|
||||
QuestieDB.private.npcCache = {}
|
||||
end)
|
||||
|
||||
it("does NOT leak learner data into static mode when the store is present", function()
|
||||
Questie.dbLearner.global.settings.dataSourceMode = "static"
|
||||
QuestieDB.private.npcCache = {}
|
||||
assert.is_nil(QuestieDB:GetNPC(9200))
|
||||
end)
|
||||
|
||||
it("does NOT leak learner data into none mode when the store is present", function()
|
||||
Questie.dbLearner.global.settings.dataSourceMode = "none"
|
||||
QuestieDB.private.npcCache = {}
|
||||
assert.is_nil(QuestieDB:GetNPC(9200))
|
||||
end)
|
||||
|
||||
it("DOES overlay learner data in auto mode when the static DB lacks it", function()
|
||||
Questie.dbLearner.global.settings.dataSourceMode = "auto"
|
||||
QuestieDB.private.npcCache = {}
|
||||
local npc = QuestieDB:GetNPC(9200)
|
||||
assert.is_table(npc)
|
||||
assert.equals("Learner Only NPC", npc.name)
|
||||
end)
|
||||
|
||||
it("clears every cache including the zone cache on mode switch", function()
|
||||
QuestieDB.private.questCache[1] = {}
|
||||
QuestieDB.private.zoneCache[1] = {}
|
||||
QuestieDB:ClearModeCaches()
|
||||
assert.is_nil(QuestieDB.private.questCache[1])
|
||||
assert.is_nil(QuestieDB.private.zoneCache[1])
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("QuestieLearner mode switch redraw wiring", function()
|
||||
it("drives a full real-time refresh via SmoothReset, not the mis-named event handler", function()
|
||||
local function read(path)
|
||||
local f = assert(io.open(path, "r")); local c = f:read("*a"); f:close(); return c
|
||||
end
|
||||
local dbOptions = read("Modules/Options/DatabaseTab/QuestieOptionsDatabase.lua")
|
||||
assert.is_true(string.find(dbOptions, "QuestieQuest:SmoothReset()", 1, true) ~= nil)
|
||||
-- The old import name resolved to nil and silently skipped the redraw.
|
||||
assert.is_nil(string.find(dbOptions, "ImportModule(\"QuestieEventHandler\")", 1, true))
|
||||
end)
|
||||
end)
|
||||
@@ -1,67 +0,0 @@
|
||||
local function countKeys(tbl)
|
||||
local n = 0
|
||||
for _ in pairs(tbl or {}) do
|
||||
n = n + 1
|
||||
end
|
||||
return n
|
||||
end
|
||||
|
||||
describe("QuestieLearner reset all learned data", function()
|
||||
local QuestieLearner
|
||||
|
||||
before_each(function()
|
||||
dofile("Tests/wow_api_mock.lua")
|
||||
|
||||
Questie.dbLearner.global = {
|
||||
settings = {
|
||||
enabled = false,
|
||||
prioritizeMyData = true,
|
||||
dataSourceMode = "auto",
|
||||
learnNpcs = true,
|
||||
learnQuests = true,
|
||||
learnItems = true,
|
||||
learnObjects = true,
|
||||
},
|
||||
npcs = {
|
||||
[1001] = { [1] = "Boar", mc = 2 },
|
||||
},
|
||||
quests = {
|
||||
[2001] = { [1] = "Quest", mc = 1 },
|
||||
},
|
||||
items = {
|
||||
[3001] = { [1] = "Item", mc = 1 },
|
||||
},
|
||||
objects = {
|
||||
[4001] = { [1] = "Object", mc = 1 },
|
||||
},
|
||||
Ascension = {
|
||||
npcs = { [9001] = { [1] = "Bucket NPC" } },
|
||||
quests = { [9002] = { [1] = "Bucket Quest" } },
|
||||
items = { [9003] = { [1] = "Bucket Item" } },
|
||||
objects = { [9004] = { [1] = "Bucket Object" } },
|
||||
},
|
||||
}
|
||||
|
||||
dofile("Modules/QuestieLearner.lua")
|
||||
QuestieLearner = _G.QuestieLearner
|
||||
end)
|
||||
|
||||
it("clears all learned data buckets while preserving settings", function()
|
||||
QuestieLearner:ClearAllData()
|
||||
|
||||
assert.is_table(Questie.dbLearner.global.settings)
|
||||
assert.equals("auto", Questie.dbLearner.global.settings.dataSourceMode)
|
||||
assert.is_true(Questie.dbLearner.global.settings.learnObjects)
|
||||
|
||||
local npcCount, questCount, itemCount, objectCount = QuestieLearner:GetStats()
|
||||
assert.equals(0, npcCount)
|
||||
assert.equals(0, questCount)
|
||||
assert.equals(0, itemCount)
|
||||
assert.equals(0, objectCount)
|
||||
assert.equals(0, countKeys(Questie.dbLearner.global.npcs))
|
||||
assert.equals(0, countKeys(Questie.dbLearner.global.quests))
|
||||
assert.equals(0, countKeys(Questie.dbLearner.global.items))
|
||||
assert.equals(0, countKeys(Questie.dbLearner.global.objects))
|
||||
assert.is_nil(Questie.dbLearner.global.Ascension)
|
||||
end)
|
||||
end)
|
||||
@@ -1,870 +0,0 @@
|
||||
describe("QuestieLearner kill-path batching", function()
|
||||
local queuedTimers
|
||||
local broadcasts
|
||||
local QuestieLearner
|
||||
local simulatedTime
|
||||
|
||||
local function read(path)
|
||||
local f = assert(io.open(path, "r"))
|
||||
local content = f:read("*a")
|
||||
f:close()
|
||||
return content
|
||||
end
|
||||
|
||||
local function has(text, needle)
|
||||
return text and text:find(needle, 1, true) ~= nil
|
||||
end
|
||||
|
||||
local function drainQueuedTimers()
|
||||
while next(queuedTimers) do
|
||||
local currentQueue = queuedTimers
|
||||
queuedTimers = {}
|
||||
for i = 1, table.getn(currentQueue) do
|
||||
local fn = currentQueue[i]
|
||||
if fn then
|
||||
simulatedTime = simulatedTime + 1
|
||||
fn()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
before_each(function()
|
||||
dofile("Tests/wow_api_mock.lua")
|
||||
|
||||
queuedTimers = {}
|
||||
broadcasts = {}
|
||||
simulatedTime = 1000
|
||||
_G.GetTime = function()
|
||||
return simulatedTime
|
||||
end
|
||||
QuestieCompat.C_Timer.After = function(_, fn)
|
||||
queuedTimers[table.getn(queuedTimers) + 1] = fn
|
||||
end
|
||||
|
||||
local originalImportModule = QuestieLoader.ImportModule
|
||||
QuestieLoader.ImportModule = function(self, name)
|
||||
if name == "QuestieLearnerComms" then
|
||||
return {
|
||||
BroadcastLearnedData = function(_, op, typ, id, data)
|
||||
broadcasts[table.getn(broadcasts) + 1] = {
|
||||
op = op,
|
||||
typ = typ,
|
||||
id = id,
|
||||
data = data,
|
||||
}
|
||||
end,
|
||||
}
|
||||
end
|
||||
return originalImportModule(self, name)
|
||||
end
|
||||
|
||||
Questie.dbLearner.global.settings.enabled = true
|
||||
Questie.dbLearner.global.settings.learnNpcs = true
|
||||
Questie.dbLearner.global.settings.learnQuests = true
|
||||
Questie.dbLearner.global.settings.learnItems = true
|
||||
Questie.dbLearner.global.settings.learnObjects = true
|
||||
Questie.dbLearner.global.settings.minConfidencePins = 1
|
||||
Questie.dbLearner.global.npcs = {}
|
||||
Questie.dbLearner.global.quests = {}
|
||||
Questie.dbLearner.global.items = {}
|
||||
Questie.dbLearner.global.objects = {}
|
||||
QuestieDB.npcDataOverrides = {}
|
||||
QuestieDB.questDataOverrides = {}
|
||||
QuestieDB.itemDataOverrides = {}
|
||||
QuestieDB.objectDataOverrides = {}
|
||||
QuestieDB.private = {
|
||||
npcCache = {
|
||||
[1001] = { name = "Cached Boar" },
|
||||
},
|
||||
itemCache = {},
|
||||
}
|
||||
QuestiePlayer.currentQuestlog = {}
|
||||
|
||||
QuestieLearner = dofile("Modules/QuestieLearner.lua")
|
||||
end)
|
||||
|
||||
it("coalesces repeated NPC live updates instead of invalidating DB cache per kill", function()
|
||||
QuestieLearner:LearnNPC(1001, "Laggy Boar", nil, nil, nil, nil, 41.25, 52.5, 44)
|
||||
QuestieLearner:LearnNPC(1001, "Laggy Boar", nil, nil, nil, nil, 41.35, 52.6, 44)
|
||||
|
||||
assert.is_nil(QuestieDB.npcDataOverrides[1001])
|
||||
assert.is_table(QuestieDB.private.npcCache[1001])
|
||||
assert.equals(2, table.getn(queuedTimers))
|
||||
|
||||
drainQueuedTimers()
|
||||
|
||||
assert.is_table(QuestieDB.npcDataOverrides[1001])
|
||||
assert.is_nil(QuestieDB.private.npcCache[1001])
|
||||
assert.is_table(QuestieDB.npcDataOverrides[1001][7])
|
||||
|
||||
local flushedSpawnCount = table.getn(QuestieDB.npcDataOverrides[1001][7][44])
|
||||
|
||||
QuestieLearner:LearnNPC(1001, "Laggy Boar", nil, nil, nil, nil, 80.0, 80.0, 44)
|
||||
|
||||
assert.equals(flushedSpawnCount, table.getn(QuestieDB.npcDataOverrides[1001][7][44]))
|
||||
assert.is_true(table.getn(queuedTimers) >= 2)
|
||||
end)
|
||||
|
||||
it("heals repeated same-bucket npc coordinates without duplicating the learned spawn", function()
|
||||
QuestieLearner:LearnNPC(2001, "Healing Boar", nil, nil, nil, nil, 40.10, 40.10, 44)
|
||||
QuestieLearner:LearnNPC(2001, "Healing Boar", nil, nil, nil, nil, 40.30, 40.30, 44)
|
||||
|
||||
local spawns = Questie.dbLearner.global.npcs[2001][7][44]
|
||||
assert.is_table(spawns)
|
||||
assert.equals(1, table.getn(spawns))
|
||||
assert.equals(2, spawns[1][3])
|
||||
assert.is_true(math.abs(spawns[1][1] - 40.20) < 0.001)
|
||||
assert.is_true(math.abs(spawns[1][2] - 40.20) < 0.001)
|
||||
end)
|
||||
|
||||
it("keeps explicit learned npc spawns available for learner arrows", function()
|
||||
Questie.dbLearner.global.settings.dataSourceMode = "learner"
|
||||
QuestieLearner:LearnNPC(3001, "Arrow Boar", nil, nil, nil, nil, 22.10, 33.20, 44)
|
||||
QuestieLearner:InjectLearnedData()
|
||||
|
||||
assert.equals("explicit", Questie.dbLearner.global.npcs[3001].spawnSource)
|
||||
assert.is_table(Questie.dbLearner.global.npcs[3001][7])
|
||||
assert.is_table(Questie.dbLearner.global.npcs[3001][7][44])
|
||||
assert.equals(1, table.getn(Questie.dbLearner.global.npcs[3001][7][44]))
|
||||
assert.equals(22.10, Questie.dbLearner.global.npcs[3001][7][44][1][1])
|
||||
assert.equals(33.20, Questie.dbLearner.global.npcs[3001][7][44][1][2])
|
||||
end)
|
||||
|
||||
it("keeps quest-tied fallback questgiver spawns from being stripped", function()
|
||||
Questie.dbLearner.global.settings.dataSourceMode = "learner"
|
||||
QuestieLearner:LearnNPC(4001, "Quest Giver", nil, nil, nil, nil, 10.0, 20.0, 44)
|
||||
QuestieLearner:LearnQuestGiver(6004, 4001, 1, true)
|
||||
|
||||
assert.equals("explicit", Questie.dbLearner.global.npcs[4001].spawnSource)
|
||||
assert.is_table(Questie.dbLearner.global.npcs[4001][7])
|
||||
assert.is_table(Questie.dbLearner.global.npcs[4001][7][44])
|
||||
assert.equals(1, table.getn(Questie.dbLearner.global.npcs[4001][7][44]))
|
||||
end)
|
||||
|
||||
it("keeps quest-related protected npc spawns available for turn-in arrows", function()
|
||||
Questie.dbLearner.global.settings.dataSourceMode = "auto"
|
||||
QuestieDB.ascensionOverrideKeys = QuestieDB.ascensionOverrideKeys or {}
|
||||
QuestieDB.ascensionOverrideKeys.NPC = QuestieDB.ascensionOverrideKeys.NPC or {}
|
||||
QuestieDB.ascensionOverrideKeys.NPC[5001] = { [7] = true }
|
||||
|
||||
QuestieLearner:LearnQuestGiver(6007, 5001, 1, false)
|
||||
QuestieLearner:LearnNPC(5001, "Protected Turn-In", nil, nil, nil, nil, 15.5, 25.5, 44)
|
||||
drainQueuedTimers()
|
||||
|
||||
assert.is_table(QuestieDB.npcDataOverrides[5001])
|
||||
assert.is_table(QuestieDB.npcDataOverrides[5001][7])
|
||||
assert.is_table(QuestieDB.npcDataOverrides[5001][7][44])
|
||||
assert.equals(1, table.getn(QuestieDB.npcDataOverrides[5001][7][44]))
|
||||
end)
|
||||
|
||||
it("ignores non-quest loot items so they do not pollute learner state", function()
|
||||
Questie.dbLearner.global.settings.dataSourceMode = "learner"
|
||||
QuestieDB.private.itemCache = {}
|
||||
|
||||
local learned = QuestieLearner:LearnItem(2301, "Quest Shard", 1, 1, 1, 0)
|
||||
|
||||
assert.is_false(learned)
|
||||
assert.is_nil(Questie.dbLearner.global.items[2301])
|
||||
assert.is_nil(QuestieDB.private.itemCache[2301])
|
||||
end)
|
||||
|
||||
it("keeps quest item drop sources when the item is a quest item", function()
|
||||
Questie.dbLearner.global.settings.dataSourceMode = "learner"
|
||||
local learned = QuestieLearner:LearnItem(2302, "Quest Shard", 1, 1, 12, 0)
|
||||
assert.is_true(learned)
|
||||
assert.is_true(Questie.dbLearner.global.items[2302].questRelevant)
|
||||
|
||||
QuestieLearner:LearnItemDrop(2302, 7301)
|
||||
|
||||
assert.is_table(Questie.dbLearner.global.items[2302][2])
|
||||
assert.equals(7301, Questie.dbLearner.global.items[2302][2][1])
|
||||
end)
|
||||
|
||||
it("ignores non-quest objects until they are explicitly quest-related", function()
|
||||
Questie.dbLearner.global.settings.dataSourceMode = "learner"
|
||||
|
||||
local learned = QuestieLearner:LearnObject(3301, "Random Object", 11.1, 22.2, 44)
|
||||
|
||||
assert.is_false(learned)
|
||||
assert.is_nil(Questie.dbLearner.global.objects[3301])
|
||||
end)
|
||||
|
||||
it("stores quest-related object coordinates when promoted", function()
|
||||
Questie.dbLearner.global.settings.dataSourceMode = "learner"
|
||||
Questie.dbLearner.global.quests[6005] = {
|
||||
[10] = {
|
||||
[2] = {
|
||||
{ 3302, "Quest Object" },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
local learned = QuestieLearner:LearnObject(3302, "Quest Object", 11.1, 22.2, 44, true)
|
||||
|
||||
assert.is_true(learned)
|
||||
assert.is_table(Questie.dbLearner.global.objects[3302])
|
||||
assert.is_true(Questie.dbLearner.global.objects[3302].questRelevant)
|
||||
assert.is_table(Questie.dbLearner.global.objects[3302][4])
|
||||
assert.is_table(Questie.dbLearner.global.objects[3302][4][44])
|
||||
assert.equals(1, table.getn(Questie.dbLearner.global.objects[3302][4][44]))
|
||||
assert.equals(11.1, Questie.dbLearner.global.objects[3302][4][44][1][1])
|
||||
assert.equals(22.2, Questie.dbLearner.global.objects[3302][4][44][1][2])
|
||||
end)
|
||||
|
||||
it("keeps quest-related protected object spawns available for turn-in arrows", function()
|
||||
Questie.dbLearner.global.settings.dataSourceMode = "auto"
|
||||
QuestieDB.ascensionOverrideKeys = QuestieDB.ascensionOverrideKeys or {}
|
||||
QuestieDB.ascensionOverrideKeys.OBJECT = QuestieDB.ascensionOverrideKeys.OBJECT or {}
|
||||
QuestieDB.ascensionOverrideKeys.OBJECT[5002] = { [4] = true }
|
||||
|
||||
QuestieLearner:LearnQuestGiver(6008, 5002, 2, false)
|
||||
local learned = QuestieLearner:LearnObject(5002, "Protected Object", 21.5, 31.5, 44, true)
|
||||
|
||||
assert.is_true(learned)
|
||||
assert.is_table(QuestieDB.objectDataOverrides[5002])
|
||||
assert.is_table(QuestieDB.objectDataOverrides[5002][4])
|
||||
assert.is_table(QuestieDB.objectDataOverrides[5002][4][44])
|
||||
assert.equals(1, table.getn(QuestieDB.objectDataOverrides[5002][4][44]))
|
||||
end)
|
||||
|
||||
it("rejects non-quest item network merges without quest references", function()
|
||||
Questie.dbLearner.global.settings.dataSourceMode = "learner"
|
||||
local changed = QuestieLearner:_ApplyIncomingNetworkMerge("ITEM", 2401, { [1] = "Arcane Sliver" }, "NEW")
|
||||
|
||||
assert.is_false(changed)
|
||||
assert.is_nil(Questie.dbLearner.global.items[2401])
|
||||
end)
|
||||
|
||||
it("rejects non-quest object network merges without quest references", function()
|
||||
Questie.dbLearner.global.settings.dataSourceMode = "learner"
|
||||
local changed = QuestieLearner:_ApplyIncomingNetworkMerge("OBJECT", 3401, { [1] = "Random Object", [4] = { [44] = { { 11.1, 22.2 } } } }, "NEW")
|
||||
|
||||
assert.is_false(changed)
|
||||
assert.is_nil(Questie.dbLearner.global.objects[3401])
|
||||
end)
|
||||
|
||||
it("clears cached quest data when learner adds questgiver links", function()
|
||||
Questie.dbLearner.global.settings.dataSourceMode = "learner"
|
||||
QuestieDB.private.questCache = {
|
||||
[6004] = { cached = true },
|
||||
}
|
||||
|
||||
QuestieLearner:LearnQuestGiver(6004, 4001, 1, true)
|
||||
|
||||
assert.is_table(Questie.dbLearner.global.quests[6004])
|
||||
assert.is_nil(QuestieDB.private.questCache[6004])
|
||||
end)
|
||||
|
||||
it("force-flushes active quest pins within the NPC live-update flush (no second debounce)", function()
|
||||
local updateCount = 0
|
||||
local originalUpdateQuest = QuestieQuest.UpdateQuest
|
||||
QuestieQuest.UpdateQuest = function(_, questId)
|
||||
updateCount = updateCount + 1
|
||||
end
|
||||
|
||||
QuestiePlayer.currentQuestlog = { [5500] = true }
|
||||
local originalGetQuest = QuestieDB.GetQuest
|
||||
QuestieDB.GetQuest = function(questId)
|
||||
if questId == 5500 then
|
||||
return { Objectives = { [1] = { Id = 7500, spawnList = { [7500] = {} } } } }
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
QuestieLearner:LearnNPC(7500, "Quest Boar", nil, nil, nil, nil, 41.0, 52.0, 44)
|
||||
|
||||
-- Drain exactly ONE timer round (the NPC live-update flush). The pin refresh
|
||||
-- must happen inside that same flush, not in a later pinRefreshDelay cycle.
|
||||
local firstRound = queuedTimers
|
||||
queuedTimers = {}
|
||||
for i = 1, table.getn(firstRound) do
|
||||
simulatedTime = simulatedTime + 1
|
||||
firstRound[i]()
|
||||
end
|
||||
|
||||
assert.is_true(updateCount >= 1)
|
||||
|
||||
-- And draining the rest must terminate (no infinite self-re-arming timer).
|
||||
drainQueuedTimers()
|
||||
|
||||
QuestieDB.GetQuest = originalGetQuest
|
||||
QuestieQuest.UpdateQuest = originalUpdateQuest
|
||||
end)
|
||||
|
||||
it("coalesces repeated quest-pin refreshes into one flush", function()
|
||||
local updateCount = 0
|
||||
local originalUpdateQuest = QuestieQuest.UpdateQuest
|
||||
QuestieQuest.UpdateQuest = function(_, questId)
|
||||
updateCount = updateCount + 1
|
||||
end
|
||||
|
||||
QuestiePlayer.currentQuestlog = { [5001] = true }
|
||||
Questie.dbLearner.global.quests = {}
|
||||
QuestieDB.questDataOverrides = {}
|
||||
|
||||
QuestieLearner:LearnQuestObjectiveNPC(5001, 7001, "Pin Boar slain", 1)
|
||||
QuestieLearner:LearnQuestObjectiveNPC(5001, 7001, "Pin Boar slain", 1)
|
||||
|
||||
assert.is_true(table.getn(queuedTimers) >= 1)
|
||||
|
||||
for i = 1, table.getn(queuedTimers) do
|
||||
queuedTimers[i]()
|
||||
end
|
||||
|
||||
assert.is_table(Questie.dbLearner.global.quests[5001])
|
||||
assert.is_table(QuestieDB.questDataOverrides[5001])
|
||||
|
||||
QuestieQuest.UpdateQuest = originalUpdateQuest
|
||||
end)
|
||||
|
||||
it("suppresses an already queued pin refresh after switching to manual mode", function()
|
||||
local updateCount = 0
|
||||
local originalUpdateQuest = QuestieQuest.UpdateQuest
|
||||
QuestieQuest.UpdateQuest = function(_, questId)
|
||||
updateCount = updateCount + 1
|
||||
end
|
||||
|
||||
QuestiePlayer.currentQuestlog = { [5003] = true }
|
||||
Questie.dbLearner.global.quests = {}
|
||||
QuestieDB.questDataOverrides = {}
|
||||
|
||||
QuestieLearner:LearnQuestObjectiveNPC(5003, 7003, "Manual Boar slain", 1)
|
||||
|
||||
assert.is_true(table.getn(queuedTimers) >= 1)
|
||||
|
||||
Questie.dbLearner.global.settings.pinRefreshMode = "manual"
|
||||
drainQueuedTimers()
|
||||
|
||||
assert.equals(0, updateCount)
|
||||
|
||||
QuestieQuest.UpdateQuest = originalUpdateQuest
|
||||
end)
|
||||
|
||||
it("keeps bystander UNIT_DIED eligible for learning in the source path", function()
|
||||
local learner = read("Modules/QuestieLearner.lua")
|
||||
|
||||
assert.is_true(has(learner, "Unconditionally map the spawn position for Ascension DB building"))
|
||||
assert.is_true(has(learner, "self:LearnNPC(npcId, name, nil, nil, nil, nil, px, py, zoneId)"))
|
||||
assert.is_false(has(learner, 'if eventType ~= "PARTY_KILL" and not credited then'))
|
||||
end)
|
||||
|
||||
it("still learns and batches local PARTY_KILL combat-log events", function()
|
||||
QuestieLearner:OnCombatLogEvent(
|
||||
1234,
|
||||
"PARTY_KILL",
|
||||
UnitGUID("player"),
|
||||
UnitName("player"),
|
||||
nil,
|
||||
"Creature-0-0-0-0-7003-0000000001",
|
||||
"Tagged Boar",
|
||||
nil
|
||||
)
|
||||
|
||||
assert.is_table(Questie.dbLearner.global.npcs[7003])
|
||||
assert.equals("Tagged Boar", Questie.dbLearner.global.npcs[7003][1])
|
||||
assert.equals(2, table.getn(queuedTimers))
|
||||
end)
|
||||
|
||||
it("only announces combat-log learning once per unique NPC id", function()
|
||||
local debugMessages = {}
|
||||
local originalDebug = Questie.Debug
|
||||
Questie.Debug = function(self, level, ...)
|
||||
local parts = { ... }
|
||||
for i = 1, table.getn(parts) do
|
||||
if parts[i] == "[QuestieLearner] Combat-log learned NPC:" then
|
||||
debugMessages[table.getn(debugMessages) + 1] = level
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
QuestieLearner:OnCombatLogEvent(
|
||||
1234,
|
||||
"PARTY_KILL",
|
||||
UnitGUID("player"),
|
||||
UnitName("player"),
|
||||
nil,
|
||||
"Creature-0-0-0-0-7010-0000000001",
|
||||
"Unique Boar",
|
||||
nil
|
||||
)
|
||||
|
||||
QuestieLearner:OnCombatLogEvent(
|
||||
1235,
|
||||
"PARTY_KILL",
|
||||
UnitGUID("player"),
|
||||
UnitName("player"),
|
||||
nil,
|
||||
"Creature-0-0-0-0-7010-0000000002",
|
||||
"Unique Boar",
|
||||
nil
|
||||
)
|
||||
|
||||
assert.is_table(Questie.dbLearner.global.npcs[7010])
|
||||
assert.equals(1, table.getn(debugMessages))
|
||||
|
||||
Questie.Debug = originalDebug
|
||||
end)
|
||||
|
||||
it("still learns credited UNIT_DIED combat-log events when PARTY_KILL is absent", function()
|
||||
QuestieLearner:OnCombatLogEvent(
|
||||
1234,
|
||||
"UNIT_DIED",
|
||||
UnitGUID("player"),
|
||||
UnitName("player"),
|
||||
nil,
|
||||
"Creature-0-0-0-0-7004-0000000001",
|
||||
"Credited Boar",
|
||||
nil
|
||||
)
|
||||
|
||||
assert.is_table(Questie.dbLearner.global.npcs[7004])
|
||||
assert.equals("Credited Boar", Questie.dbLearner.global.npcs[7004][1])
|
||||
assert.equals(2, table.getn(queuedTimers))
|
||||
end)
|
||||
|
||||
it("cross-links quest giver NPCs and objects learned after the quest without duplicate pin flushes", function()
|
||||
local updateCount = 0
|
||||
local originalUpdateQuest = QuestieQuest.UpdateQuest
|
||||
QuestieQuest.UpdateQuest = function(_, questId)
|
||||
updateCount = updateCount + 1
|
||||
end
|
||||
Questie.db.profile.learnerBroadcast = false
|
||||
QuestiePlayer.currentQuestlog = { [6001] = true }
|
||||
|
||||
QuestieLearner:LearnQuest(6001, {
|
||||
[1] = "Cross Link Givers",
|
||||
[2] = { [1] = { 7101 }, [2] = { 8101 } },
|
||||
[3] = { [1] = { 7102 }, [2] = { 8102 } },
|
||||
})
|
||||
|
||||
queuedTimers = {}
|
||||
|
||||
QuestieLearner:LearnNPC(7101, "Starter Guard", nil, nil, nil, nil, 10, 20, 44)
|
||||
QuestieLearner:LearnNPC(7102, "Finisher Guard", nil, nil, nil, nil, 30, 40, 44)
|
||||
QuestieLearner:LearnObject(8101, "Starter Chest")
|
||||
QuestieLearner:LearnObject(8102, "Finisher Chest")
|
||||
|
||||
assert.same({ 6001 }, Questie.dbLearner.global.npcs[7101][10])
|
||||
assert.same({ 6001 }, Questie.dbLearner.global.npcs[7102][11])
|
||||
assert.same({ 6001 }, Questie.dbLearner.global.objects[8101][2])
|
||||
assert.same({ 6001 }, Questie.dbLearner.global.objects[8102][3])
|
||||
assert.same({ 6001 }, QuestieDB.npcDataOverrides[7101][10])
|
||||
assert.same({ 6001 }, QuestieDB.npcDataOverrides[7102][11])
|
||||
assert.same({ 6001 }, QuestieDB.objectDataOverrides[8101][2])
|
||||
assert.same({ 6001 }, QuestieDB.objectDataOverrides[8102][3])
|
||||
|
||||
drainQueuedTimers()
|
||||
|
||||
assert.is_table(Questie.dbLearner.global.quests[6001])
|
||||
assert.is_table(QuestieDB.questDataOverrides[6001])
|
||||
|
||||
QuestieQuest.UpdateQuest = originalUpdateQuest
|
||||
end)
|
||||
|
||||
it("cross-links item objective drop NPCs into quest creature objectives", function()
|
||||
Questie.db.profile.learnerBroadcast = false
|
||||
QuestiePlayer.currentQuestlog = { [6002] = true }
|
||||
|
||||
QuestieLearner:LearnQuest(6002, {
|
||||
[1] = "Cross Link Drops",
|
||||
[10] = {
|
||||
[3] = {
|
||||
{ 2201, 0, 1, "Collect one tusk" },
|
||||
},
|
||||
},
|
||||
})
|
||||
QuestieLearner:LearnItem(2201, "Quest Tusk", 1, 1, 12, 0)
|
||||
QuestieLearner:LearnItemDrop(2201, 7201)
|
||||
|
||||
assert.equals(7201, Questie.dbLearner.global.quests[6002][10][1][1][1])
|
||||
assert.equals(7201, QuestieDB.questDataOverrides[6002][10][1][1][1])
|
||||
|
||||
QuestieLearner:LearnItemDrop(2201, 7201)
|
||||
|
||||
assert.equals(1, table.getn(Questie.dbLearner.global.quests[6002][10][1]))
|
||||
assert.equals(1, table.getn(QuestieDB.questDataOverrides[6002][10][1]))
|
||||
end)
|
||||
|
||||
it("defers learner objective tooltip registration to protected AscensionDB quest data", function()
|
||||
Questie.db.profile.learnerBroadcast = false
|
||||
QuestiePlayer.currentQuestlog = {}
|
||||
QuestieDB.ascensionOverrideKeys = {
|
||||
QUEST = {
|
||||
[8334] = {
|
||||
[10] = true,
|
||||
},
|
||||
},
|
||||
}
|
||||
_G._lastRegisteredTooltip = nil
|
||||
|
||||
QuestieLearner:LearnQuestObjectiveNPC(8334, 15271, "Tender slain", 1)
|
||||
|
||||
assert.is_table(Questie.dbLearner.global.quests[8334])
|
||||
assert.is_nil(_G._lastRegisteredTooltip)
|
||||
end)
|
||||
|
||||
it("coalesces repeated learner broadcasts into one latest update", function()
|
||||
QuestieLearner:LearnItem(2001, "Quest Tusk", 1, 1, 12, 0)
|
||||
QuestieLearner:LearnItem(2001, "Quest Tusk", 1, 1, 12, 0)
|
||||
|
||||
assert.equals(0, table.getn(broadcasts))
|
||||
assert.equals(1, table.getn(queuedTimers))
|
||||
|
||||
drainQueuedTimers()
|
||||
|
||||
assert.equals(1, table.getn(broadcasts))
|
||||
assert.equals("NEW", broadcasts[1].op)
|
||||
assert.equals("ITEM", broadcasts[1].typ)
|
||||
assert.equals(2001, broadcasts[1].id)
|
||||
end)
|
||||
|
||||
it("coalesces repeated inbound network merges into one inject pass", function()
|
||||
local injectCount = 0
|
||||
local originalInject = QuestieLearner.InjectLearnedData
|
||||
QuestieLearner.InjectLearnedData = function(self)
|
||||
injectCount = injectCount + 1
|
||||
QuestieLearner.data = Questie.dbLearner.global
|
||||
end
|
||||
|
||||
QuestieLearner:HandleNetworkData("NPC", 3001, { [1] = "Net Boar", [7] = { [44] = { { 11, 22 } } } }, "NEW")
|
||||
QuestieLearner:HandleNetworkData("NPC", 3001, { [1] = "Net Boar", [7] = { [44] = { { 11, 22 }, { 33, 44 } } } }, "UPDATE")
|
||||
|
||||
assert.equals(0, injectCount)
|
||||
assert.equals(1, table.getn(queuedTimers))
|
||||
|
||||
drainQueuedTimers()
|
||||
|
||||
assert.equals(1, injectCount)
|
||||
assert.equals("Net Boar", Questie.dbLearner.global.npcs[3001][1])
|
||||
assert.equals(2, table.getn(Questie.dbLearner.global.npcs[3001][7][44]))
|
||||
|
||||
QuestieLearner.InjectLearnedData = originalInject
|
||||
end)
|
||||
|
||||
it("forwards OnEvent payload arguments to learner handlers", function()
|
||||
local originalCreateFrame = _G.CreateFrame
|
||||
local originalStrsplit = _G.strsplit
|
||||
local createdFrame = nil
|
||||
local captured = {
|
||||
questTurnedIn = nil,
|
||||
questAccepted = nil,
|
||||
combatLog = nil,
|
||||
itemInfoReceived = nil,
|
||||
questTrackingCleared = nil,
|
||||
}
|
||||
|
||||
_G.CreateFrame = function()
|
||||
createdFrame = {
|
||||
RegisterEvent = function() end,
|
||||
SetScript = function(self, scriptName, fn)
|
||||
if scriptName == "OnEvent" then
|
||||
self._onEvent = fn
|
||||
end
|
||||
end,
|
||||
}
|
||||
return createdFrame
|
||||
end
|
||||
_G.strsplit = function(sep, str)
|
||||
local parts = {}
|
||||
local pattern = string.format("([^%s]+)", sep)
|
||||
for part in string.gmatch(str, pattern) do
|
||||
parts[#parts + 1] = part
|
||||
end
|
||||
return unpack(parts)
|
||||
end
|
||||
|
||||
QuestieLearner = dofile("Modules/QuestieLearner.lua")
|
||||
local originalOnQuestTurnedIn = QuestieLearner.OnQuestTurnedIn
|
||||
local originalOnQuestAccepted = QuestieLearner.OnQuestAccepted
|
||||
local originalOnCombatLogEvent = QuestieLearner.OnCombatLogEvent
|
||||
local originalOnGetItemInfoReceived = QuestieLearner.OnGetItemInfoReceived
|
||||
local originalClearQuestObjectiveTracking = QuestieLearner.ClearQuestObjectiveTracking
|
||||
|
||||
QuestieLearner.OnQuestTurnedIn = function(self, questId, npcId, questFlags)
|
||||
captured.questTurnedIn = { questId, npcId, questFlags }
|
||||
end
|
||||
QuestieLearner.OnQuestAccepted = function(self, questId, questGiver)
|
||||
captured.questAccepted = { questId, questGiver }
|
||||
end
|
||||
QuestieLearner.OnCombatLogEvent = function(self, timestamp, eventType, srcGUID, srcName, srcFlags, dstGUID, dstName, dstFlags, spellId, spellName)
|
||||
captured.combatLog = { timestamp, eventType, srcGUID, srcName, srcFlags, dstGUID, dstName, dstFlags, spellId, spellName }
|
||||
end
|
||||
QuestieLearner.OnGetItemInfoReceived = function(self, itemId)
|
||||
captured.itemInfoReceived = { itemId }
|
||||
end
|
||||
QuestieLearner.ClearQuestObjectiveTracking = function(self, questId)
|
||||
captured.questTrackingCleared = { questId }
|
||||
end
|
||||
|
||||
QuestieLearner:RegisterEvents()
|
||||
|
||||
local frame = createdFrame
|
||||
frame._onEvent(frame, "QUEST_TURNED_IN", 101, 202, 303)
|
||||
frame._onEvent(frame, "QUEST_ACCEPTED", 404, 505)
|
||||
frame._onEvent(frame, "COMBAT_LOG_EVENT_UNFILTERED", 1, "SPELL_DAMAGE", "src-guid", "Src", 2, "dst-guid", "Dst", 4, 777, "Fireball")
|
||||
frame._onEvent(frame, "GET_ITEM_INFO_RECEIVED", 888)
|
||||
frame._onEvent(frame, "QUEST_REMOVED", 999)
|
||||
|
||||
assert.same({ 101, 202, 303 }, captured.questTurnedIn)
|
||||
assert.same({ 404, 505 }, captured.questAccepted)
|
||||
assert.same({ 1, "SPELL_DAMAGE", "src-guid", "Src", 2, "dst-guid", "Dst", 4, 777, "Fireball" }, captured.combatLog)
|
||||
assert.same({ 888 }, captured.itemInfoReceived)
|
||||
assert.same({ 999 }, captured.questTrackingCleared)
|
||||
|
||||
QuestieLearner.OnQuestTurnedIn = originalOnQuestTurnedIn
|
||||
QuestieLearner.OnQuestAccepted = originalOnQuestAccepted
|
||||
QuestieLearner.OnCombatLogEvent = originalOnCombatLogEvent
|
||||
QuestieLearner.OnGetItemInfoReceived = originalOnGetItemInfoReceived
|
||||
QuestieLearner.ClearQuestObjectiveTracking = originalClearQuestObjectiveTracking
|
||||
_G.CreateFrame = originalCreateFrame
|
||||
_G.strsplit = originalStrsplit
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("QuestieLearnerComms queue draining", function()
|
||||
local sentMessages
|
||||
local processedMessages
|
||||
local QuestieLearnerComms
|
||||
local currentTime
|
||||
|
||||
before_each(function()
|
||||
dofile("Tests/wow_api_mock.lua")
|
||||
|
||||
sentMessages = {}
|
||||
processedMessages = {}
|
||||
currentTime = 1000
|
||||
|
||||
local originalImportModule = QuestieLoader.ImportModule
|
||||
QuestieLoader.ImportModule = function(self, name)
|
||||
if name == "QuestieLearner" then
|
||||
return _G.QuestieLearner
|
||||
end
|
||||
return originalImportModule(self, name)
|
||||
end
|
||||
QuestieLoader.CreateModule = function(self, name)
|
||||
_G[name] = { private = {} }
|
||||
return _G[name]
|
||||
end
|
||||
|
||||
_G.LibStub = function(name, silent)
|
||||
if name == "AceComm-3.0" then
|
||||
return { RegisterComm = function() end }
|
||||
elseif name == "LibDeflate" then
|
||||
return {
|
||||
CompressDeflate = function(_, s) return s end,
|
||||
EncodeForPrint = function(_, s) return s end,
|
||||
DecodeForPrint = function(_, s) return s end,
|
||||
DecompressDeflate = function(_, s) return s end,
|
||||
}
|
||||
elseif name == "AceSerializer-3.0" then
|
||||
return {
|
||||
Serialize = function(_, payload)
|
||||
return payload.op .. ":" .. tostring(payload.id)
|
||||
end,
|
||||
Deserialize = function(_, serialized)
|
||||
local op, id = string.match(serialized, "([^:]+):(.+)")
|
||||
return true, {
|
||||
_ver = 2,
|
||||
op = op,
|
||||
typ = "NPC",
|
||||
id = tonumber(id) or id,
|
||||
d = { [1] = "net" },
|
||||
}
|
||||
end,
|
||||
}
|
||||
elseif name == "XXH_Lua_Lib" then
|
||||
return nil
|
||||
elseif name == "HereBeDragonsQuestie-2.0" then
|
||||
return {}
|
||||
end
|
||||
return {}
|
||||
end
|
||||
|
||||
_G.QuestieLearner = {
|
||||
HandleNetworkData = function(_, typ, id, d, op)
|
||||
processedMessages[table.getn(processedMessages) + 1] = {
|
||||
typ = typ,
|
||||
id = id,
|
||||
op = op,
|
||||
}
|
||||
end,
|
||||
}
|
||||
|
||||
_G.GetChannelName = function() return 1 end
|
||||
_G.JoinPermanentChannel = function() end
|
||||
_G.ChatFrame_RemoveChannel = function() end
|
||||
_G.DEFAULT_CHAT_FRAME = { GetID = function() return 1 end }
|
||||
_G.SendChatMessage = function(msg, mode, nilarg, channel)
|
||||
sentMessages[table.getn(sentMessages) + 1] = msg
|
||||
end
|
||||
_G.InCombatLockdown = function() return false end
|
||||
_G.random = function() return 0 end
|
||||
_G.GetTime = function()
|
||||
return currentTime
|
||||
end
|
||||
QuestieCompat.C_Timer.NewTicker = function(delay, fn)
|
||||
return { Cancel = function() end }
|
||||
end
|
||||
|
||||
dofile("Modules/Network/QuestieLearnerComms.lua")
|
||||
QuestieLearnerComms = _G.QuestieLearnerComms
|
||||
QuestieLearnerComms:Initialize()
|
||||
end)
|
||||
|
||||
it("drains outgoing and incoming queues in FIFO order without front-removal", function()
|
||||
QuestieLearnerComms:BroadcastLearnedData("NEW", "NPC", 101, { foo = "a" })
|
||||
QuestieLearnerComms:BroadcastLearnedData("UPDATE", "NPC", 102, { foo = "b" })
|
||||
|
||||
QuestieLearnerComms:OnCommReceived("QuestieLearner", "NEW:201", "CHANNEL", "Alice")
|
||||
QuestieLearnerComms:OnCommReceived("QuestieLearner", "UPDATE:202", "CHANNEL", "Bob")
|
||||
|
||||
QuestieLearnerComms.private:ProcessQueues()
|
||||
currentTime = currentTime + 4
|
||||
QuestieLearnerComms.private:ProcessQueues()
|
||||
currentTime = currentTime + 4
|
||||
QuestieLearnerComms.private:ProcessQueues()
|
||||
|
||||
assert.equals(2, table.getn(sentMessages))
|
||||
assert.equals("NEW:101", sentMessages[1])
|
||||
assert.equals("UPDATE:102", sentMessages[2])
|
||||
assert.equals(2, table.getn(processedMessages))
|
||||
assert.equals(201, processedMessages[1].id)
|
||||
assert.equals(202, processedMessages[2].id)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("QuestieComms packet sizing", function()
|
||||
local serializeCount
|
||||
local sendCount
|
||||
local QuestieComms
|
||||
|
||||
before_each(function()
|
||||
dofile("Tests/wow_api_mock.lua")
|
||||
|
||||
serializeCount = 0
|
||||
sendCount = 0
|
||||
|
||||
local originalCreateModule = QuestieLoader.CreateModule
|
||||
local originalImportModule = QuestieLoader.ImportModule
|
||||
QuestieLoader.CreateModule = function(self, name)
|
||||
_G[name] = { private = {} }
|
||||
return _G[name]
|
||||
end
|
||||
QuestieLoader.ImportModule = function(self, name)
|
||||
if name == "QuestieSerializer" then
|
||||
return {
|
||||
Serialize = function(_, payload)
|
||||
serializeCount = serializeCount + 1
|
||||
return "packet:" .. tostring(payload.id or payload[1] or "x")
|
||||
end,
|
||||
Deserialize = function() return true, {} end,
|
||||
}
|
||||
end
|
||||
return originalImportModule(self, name)
|
||||
end
|
||||
|
||||
_G.LibStub = function(name, silent)
|
||||
if name == "AceComm-3.0" then
|
||||
return { RegisterComm = function() end }
|
||||
elseif name == "LibDeflate" then
|
||||
return {
|
||||
CompressDeflate = function(_, s) return s end,
|
||||
EncodeForPrint = function(_, s) return s end,
|
||||
DecodeForPrint = function(_, s) return s end,
|
||||
DecompressDeflate = function(_, s) return s end,
|
||||
}
|
||||
elseif name == "AceSerializer-3.0" then
|
||||
return {
|
||||
Serialize = function(_, payload) return "packet:" .. tostring(payload.id or payload[1] or "x") end,
|
||||
Deserialize = function() return true, {} end,
|
||||
}
|
||||
elseif name == "XXH_Lua_Lib" then
|
||||
return nil
|
||||
elseif name == "HereBeDragonsQuestie-2.0" then
|
||||
return {}
|
||||
end
|
||||
return {}
|
||||
end
|
||||
|
||||
_G.QuestiePlayer.GetGroupType = function() return "party" end
|
||||
_G.QuestiePlayer.numberOfGroupMembers = 5
|
||||
_G.QuestieDB.QuestPointers = {
|
||||
[101] = true,
|
||||
[102] = true,
|
||||
[103] = true,
|
||||
}
|
||||
_G.QuestieDB.QueryQuestSingle = function()
|
||||
return 0
|
||||
end
|
||||
_G.QuestieDB.GetQuest = function(questId)
|
||||
return {
|
||||
Objectives = {
|
||||
[1] = { Id = questId * 10 },
|
||||
},
|
||||
}
|
||||
end
|
||||
_G.QuestLogCache.questLog_DO_NOT_MODIFY = {
|
||||
[101] = { questTag = "Normal" },
|
||||
[102] = { questTag = "Normal" },
|
||||
[103] = { questTag = "Normal" },
|
||||
}
|
||||
_G.QuestLogCache.GetQuestObjectives = function()
|
||||
return {
|
||||
{ type = "monster", numFulfilled = 0, numRequired = 1 },
|
||||
}
|
||||
end
|
||||
_G.ZoneDB.GetUiMapIdByAreaId = function()
|
||||
return 1
|
||||
end
|
||||
_G.HBD = {
|
||||
GetZoneDistance = function() return 1 end,
|
||||
GetPlayerZone = function() return 1 end,
|
||||
}
|
||||
_G.GetChannelName = function() return 1 end
|
||||
_G.SendChatMessage = function() end
|
||||
_G.Questie.SendCommMessage = function()
|
||||
sendCount = sendCount + 1
|
||||
end
|
||||
_G.UnitInBattleground = function() return false end
|
||||
_G.random = function() return 0 end
|
||||
_G.GetTime = function() return 0 end
|
||||
_G.tinsert = table.insert
|
||||
QuestieCompat.C_Timer.After = function(_, fn) end
|
||||
QuestieCompat.C_Timer.NewTicker = function(_, fn)
|
||||
return { Cancel = function() end }
|
||||
end
|
||||
|
||||
QuestieComms = dofile("Modules/Network/QuestieComms.lua")
|
||||
QuestieComms.private.CreatePacket = function()
|
||||
return {
|
||||
data = {},
|
||||
write = function() end,
|
||||
}
|
||||
end
|
||||
|
||||
serializeCount = 0
|
||||
end)
|
||||
|
||||
it("serializes each quest once while packing broadcast blocks", function()
|
||||
QuestieComms.private:BroadcastQuestLog("QC_ID_BROADCAST_FULL_QUESTLIST", "WHISPER", "Tester")
|
||||
assert.equals(3, serializeCount)
|
||||
|
||||
serializeCount = 0
|
||||
QuestieComms.private:BroadcastQuestLogV2("QC_ID_BROADCAST_FULL_QUESTLIST", "WHISPER", "Tester")
|
||||
assert.equals(3, serializeCount)
|
||||
end)
|
||||
|
||||
it("suppresses outgoing QuestieComms immediately when disabled", function()
|
||||
Questie.db.profile.questieCommsEnabled = false
|
||||
|
||||
local packet = QuestieComms.private:CreatePacket(QuestieComms.private.QC_ID_BROADCAST_QUEST_REMOVE)
|
||||
packet.data.writeMode = QuestieComms.private.QC_WRITE_ALLGROUP
|
||||
packet.data.priority = "NORMAL"
|
||||
packet.data.id = 101
|
||||
packet:write()
|
||||
|
||||
assert.equals(0, sendCount)
|
||||
end)
|
||||
end)
|
||||
@@ -1,23 +0,0 @@
|
||||
describe("QuestieQuest tooltip fallback", function()
|
||||
local function read(path)
|
||||
local f = assert(io.open(path, "r"), "cannot open " .. path)
|
||||
local c = f:read("*a")
|
||||
f:close()
|
||||
return c
|
||||
end
|
||||
|
||||
local function has(content, needle)
|
||||
return string.find(content, needle, 1, true) ~= nil
|
||||
end
|
||||
|
||||
it("registers direct objective tooltips when special objectives have ids but no spawnList", function()
|
||||
local questieQuest = read("Modules/Quest/QuestieQuest.lua")
|
||||
|
||||
assert.is_true(has(questieQuest, 'tooltipKey = "m_" .. objective.Id'))
|
||||
assert.is_true(has(questieQuest, 'tooltipKey = "o_" .. objective.Id'))
|
||||
assert.is_true(has(questieQuest, 'tooltipKey = "i_" .. objective.Id'))
|
||||
assert.is_true(has(questieQuest, 'elseif objective.Type == "killcredit" then'))
|
||||
assert.is_true(has(questieQuest, 'QuestieTooltips:RegisterObjectiveTooltip(questId, "m_" .. id, objective)'))
|
||||
assert.is_true(has(questieQuest, "objective.registeredItemTooltips = true"))
|
||||
end)
|
||||
end)
|
||||
@@ -1,66 +0,0 @@
|
||||
describe("Questie tooltip precedence", function()
|
||||
local function contains(lines, needle)
|
||||
for _, line in ipairs(lines or {}) do
|
||||
if string.find(line, needle, 1, true) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
before_each(function()
|
||||
dofile("Tests/wow_api_mock.lua")
|
||||
|
||||
local originalImportModule = QuestieLoader.ImportModule
|
||||
QuestieLoader.ImportModule = function(self, name)
|
||||
if name == "QuestieLib" then
|
||||
return {
|
||||
GetColoredQuestName = function(_, questId)
|
||||
return "Quest " .. tostring(questId)
|
||||
end,
|
||||
Colorize = function(_, text)
|
||||
return text
|
||||
end,
|
||||
}
|
||||
end
|
||||
return originalImportModule(self, name)
|
||||
end
|
||||
|
||||
Questie.db.profile.showQuestsInNpcTooltip = true
|
||||
Questie.db.profile.enableTooltipsQuestLevel = false
|
||||
Questie.db.profile.enableTooltipsNPCID = false
|
||||
QuestiePlayer.numberOfGroupMembers = 0
|
||||
QuestieCompat.IsInGroup = function() return false end
|
||||
QuestieCompat.UnitInParty = function() return false end
|
||||
|
||||
QuestieDB.GetQuest = function(_, questId)
|
||||
if questId ~= 8334 then
|
||||
return nil
|
||||
end
|
||||
|
||||
return {
|
||||
ObjectiveData = {
|
||||
[1] = { Type = "monster", Id = 15271, Text = "Tender slain" },
|
||||
[2] = { Type = "monster", Id = 15294, Text = "Feral Tender slain" },
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
dofile("Modules/Tooltips/Tooltip.lua")
|
||||
QuestieTooltips.lookupByKey["m_15271"] = {
|
||||
["8334 Tender 15271"] = {
|
||||
questId = 8334,
|
||||
name = "Tender",
|
||||
starterId = 15271,
|
||||
},
|
||||
}
|
||||
end)
|
||||
|
||||
it("adds AscensionDB objective text under quest titles when no objective tooltip is registered", function()
|
||||
local lines = QuestieTooltips:GetTooltip("m_15271")
|
||||
|
||||
assert.is_true(contains(lines, "Quest 8334"))
|
||||
assert.is_true(contains(lines, "Tender slain"))
|
||||
assert.is_true(contains(lines, "Feral Tender slain"))
|
||||
end)
|
||||
end)
|
||||
Reference in New Issue
Block a user