-- Mock WoW 1.12 client + scenario tests for ShardCap. Run with Lua 5.0. local ADDON_FILE = arg[1] or "ShardCap.lua" local fails, passes = 0, 0 local function check(cond, name) if cond then passes = passes + 1; print(" ok " .. name) else fails = fails + 1; print(" FAIL " .. name) end end local now, bags, bagItem, cursor, combat, frames, chat, maxStack local soulOK = {} local function isSoulBagItem(id) return id == 22243 or id == 99999 end local function fits(b, id) return not isSoulBagItem(bagItem[b]) or soulOK[id] end local itemNames = { [6265] = "Soul Shard", [16265] = "Some Other Item", [6948] = "Hearthstone" } local function reset(opts) opts = opts or {} now, cursor, combat, frames, chat = 0, nil, false, {}, {} maxStack = opts.maxStack or 3 soulOK = { [6265] = true } if opts.reagentsOK ~= false then soulOK[16583] = true; soulOK[5565] = true end familyKnown = opts.familyKnown bags, bagItem = {}, {} for b = 0, 4 do bags[b] = { size = 0, slots = {} } end ShardCapDB, SHARDCAP_CAP_VALUE, SHARDCAP_SPAM = nil, nil, nil C_Container, C_Item, GetBagItem, CLASSIC_API_VERSION = nil, nil, nil, nil bank = nil end local function setBag(b, size, itemID) bags[b].size = size; bagItem[b] = itemID end local function put(b, s, id, n) bags[b].slots[s] = { id = id, count = n or 1 } end -- vanilla API function GetTime() return now end function GetContainerNumSlots(b) return bags[b] and bags[b].size or 0 end function GetContainerItemLink(b, s) local it = bags[b].slots[s]; if not it then return nil end return "|cffffffff|Hitem:" .. it.id .. ":0:0:0|h[" .. (itemNames[it.id] or "?") .. "]|h|r" end function GetContainerItemInfo(b, s) local it = bags[b].slots[s]; if not it then return nil end return "tex", it.count, it.locked end function ContainerIDToInventoryID(b) return 19 + b end function GetInventoryItemLink(u, inv) local id = bagItem[inv - 19]; if not id then return nil end return "|Hitem:" .. id .. ":0:0:0|h[bag]|h" end function GetItemInfo(id) id = tonumber(id) if id == 6265 then return "Soul Shard", "item:6265", 1, 0, "Reagent", "Reagent", maxStack end if id == 16583 then return "Demonic Figurine", "item:16583", 1, 0, "Reagent", "Reagent", 10 end if id == 5565 then return "Infernal Stone", "item:5565", 1, 0, "Reagent", "Reagent", 10 end if id == 22243 then return "Small Soul Pouch", "item:22243", 2, 0, "Container", "Soul Bag", 1 end if id == 99999 then return "Custom Soul Bag", "item:99999", 2, 0, "Container", "Soul Bag", 1 end return "Bag", "item:" .. id, 1, 0, "Container", "Bag", 1 end function CursorHasItem() return cursor ~= nil end function SpellIsTargeting() return false end function PickupContainerItem(b, s) local it = bags[b].slots[s] if cursor then if not fits(b, cursor.id) then return end -- "That item doesn't go in that container." if it and it.id == cursor.id and it.count + cursor.count <= maxStack then it.count = it.count + cursor.count; cursor = nil elseif not it then bags[b].slots[s] = cursor; cursor = nil else bags[b].slots[s] = cursor; cursor = it end elseif it then bags[b].slots[s] = nil; cursor = it end end function SplitContainerItem(b, s, n) local it = bags[b].slots[s] if cursor or not it or n >= it.count then return end it.count = it.count - n; cursor = { id = it.id, count = n } end function DeleteCursorItem() cursor = nil end function UnitAffectingCombat() return combat end function IsAddOnLoaded() return false end DEFAULT_CHAT_FRAME = { AddMessage = function(self, m) table.insert(chat, m) end } SlashCmdList = {} -- Minimal widget mock: enough of Frame/CheckButton/Slider/EditBox/FontString for Options.lua local W = {} local function call(w, script) local fn = w.scripts[script] if fn then local old = this; this = w; fn(); this = old end end function W:RegisterEvent(e) self.events[e] = true end function W:SetScript(k, fn) self.scripts[k] = fn end function W:Show() self.shown = true end function W:Hide() self.shown = false end function W:GetName() return self.name end function W:SetText(t) self.text = t and tostring(t) or "" end function W:GetText() return self.text end function W:SetChecked(v) if v then self.checked = 1 else self.checked = nil end end function W:GetChecked() return self.checked end function W:Enable() self.enabled = true end function W:Disable() self.enabled = false end function W:SetMinMaxValues(lo, hi) self.lo, self.hi = lo, hi end function W:GetValue() return self.value end function W:SetValue(v) if self.lo and v < self.lo then v = self.lo end if self.hi and v > self.hi then v = self.hi end if v ~= self.value then self.value = v; call(self, "OnValueChanged") end end function W:SetFocus() self.focus = true; call(self, "OnEditFocusGained") end function W:ClearFocus() if self.focus then self.focus = false; call(self, "OnEditFocusLost") end end function W:CreateFontString() return newWidget("FontString") end for _, m in ipairs({ "SetPoint", "SetWidth", "SetHeight", "SetJustifyH", "SetTextColor", "SetValueStep", "SetAutoFocus", "SetNumeric", "SetMaxLetters", "SetOwner" }) do W[m] = function() end end local WMT = { __index = W } function newWidget(kind, name) local w = setmetatable({ kind = kind, name = name, scripts = {}, events = {}, shown = true, text = "", value = 0, enabled = true }, WMT) if name then _G[name] = w end table.insert(allWidgets, w) return w end allWidgets = {} function getglobal(n) return _G[n] end function CreateFrame(kind, name, parent, template) local f = newWidget(kind, name) if name and template then for _, sub in ipairs({ "Text", "Low", "High" }) do newWidget("FontString", name .. sub) end end table.insert(frames, f) return f end GameTooltip = newWidget("GameTooltip") NORMAL_FONT_COLOR = { r = 1, g = 0.82, b = 0 } GRAY_FONT_COLOR = { r = 0.5, g = 0.5, b = 0.5 } local F local function fire(e, a1) if not F.events[e] then return end event, arg1 = e, a1 this = F; F.scripts.OnEvent() end local function run(seconds) local stop = now + (seconds or 10) while now < stop do now = now + 0.05 if F.shown and F.scripts.OnUpdate then this = F; F.scripts.OnUpdate() end end end local OPTIONS_FILE = string.gsub(ADDON_FILE, "ShardCap%.lua$", "Options.lua") local categories, bank local function load(classic, withOptions) categories = {} if withOptions then InterfaceOptions_AddCategory = function(p) p:Hide(); table.insert(categories, p) end InterfaceOptionsFrame_OpenToCategory = function(p) p:Show(); p.refresh(p); categories.opened = p end else InterfaceOptions_AddCategory, InterfaceOptionsFrame_OpenToCategory = nil, nil end if classic then CLASSIC_API_VERSION = 10000 C_Container = { GetContainerItemInfo = function(b, s) local it = bags[b].slots[s]; if not it then return nil end return { itemID = it.id, stackCount = it.count, isLocked = it.locked and true or false } end, GetContainerNumFreeSlots = function(b) local id = bagItem[b] local t = (id == 22243 or id == 99999) and 4 or 0 return 0, t end, SwapItems = function(sb, ss, db_, ds) local a, b2 = bags[sb].slots[ss], bags[db_].slots[ds] if (a and not fits(db_, a.id)) or (b2 and not fits(sb, b2.id)) then return true end -- server refuses silently bags[sb].slots[ss], bags[db_].slots[ds] = b2, a; return true end, MoveItem = function(sb, ss, db_, ds, n) local src, dst = bags[sb].slots[ss], bags[db_].slots[ds] if not src or n > src.count then return false end if dst and (dst.id ~= src.id or dst.count + n > maxStack) then return false end if dst then dst.count = dst.count + n else bags[db_].slots[ds] = { id = src.id, count = n } end src.count = src.count - n; if src.count == 0 then bags[sb].slots[ss] = nil end return true end, } C_Item = { GetItemMaxStackSizeByID = function(id) return maxStack end, GetItemFamily = function(id) if not familyKnown then return 0 end -- family not reported: fall back to heuristics return soulOK[id] and 4 or 0 end } end if classic then C_Item.GetItemCount = function(id, includeBank) local t = 0 for b = 0, 4 do for s = 1, bags[b].size do local it = bags[b].slots[s]; if it and it.id == id then t = t + it.count end end end if includeBank and bank then t = t + (bank[id] or 0) end return t end end dofile(ADDON_FILE) dofile(OPTIONS_FILE) F = frames[1] fire("ADDON_LOADED", "ShardCap") fire("PLAYER_ENTERING_WORLD") end local function count(onlyBag) local t = 0 for b = 0, 4 do if not onlyBag or onlyBag == b then for s = 1, bags[b].size do local it = bags[b].slots[s] if it and it.id == 6265 then t = t + it.count end end end end return t end local function where(id) local t = {} for b = 0, 4 do for s = 1, bags[b].size do local it = bags[b].slots[s]; if it and it.id == id then table.insert(t, b) end end end return t end local function slotsUsed() local t = 0 for b = 0, 4 do for s = 1, bags[b].size do local it = bags[b].slots[s]; if it and it.id == 6265 then t = t + 1 end end end return t end for _, classic in ipairs({ false, true }) do local tag = classic and "[ClassicAPI] " or "[vanilla] " print(tag) -- 1. WoW 1.18.1 Clients stacks of 3: trims to exactly the cap, backpack first reset(); setBag(0, 16); setBag(4, 12, 22243) put(0, 2, 6265, 3); put(4, 1, 6265, 3); put(4, 2, 6265, 3); put(4, 3, 6265, 2) load(classic); ShardCapDB.cap = 6; ShardCapDB.sort = false; fire("BAG_UPDATE"); fire("BAG_UPDATE_DELAYED"); run() check(count() == 6, tag .. "trim 11 -> 6 with stacks of 3 (got " .. count() .. ")") check(count(0) == 0, tag .. "deleted from backpack before soul bag") check(cursor == nil, tag .. "cursor left empty") -- 2. Exactness: 13 shards, cap 12 -> deletes exactly 1 (ShardCapPlus deleted a whole stack of 3) reset(); setBag(0, 16); setBag(4, 12, 22243) put(0, 1, 6265, 1); for s = 1, 4 do put(4, s, 6265, 3) end load(classic); ShardCapDB.cap = 12; fire("BAG_UPDATE"); fire("BAG_UPDATE_DELAYED"); run() check(count() == 12, tag .. "13 -> exactly 12 (got " .. count() .. ")") reset(); setBag(0, 16); setBag(4, 12, 22243) put(0, 1, 6265, 3); for s = 1, 4 do put(4, s, 6265, 3) end load(classic); ShardCapDB.cap = 13; ShardCapDB.sort = false; fire("BAG_UPDATE"); fire("BAG_UPDATE_DELAYED"); run() check(count() == 13 and count(0) == 1, tag .. "15 -> 13 splits the backpack stack (got " .. count() .. ")") -- 3. No false positives: item 16265 is never touched reset(); setBag(0, 16) put(0, 1, 16265, 1); put(0, 2, 6265, 1); put(0, 3, 6265, 1) load(classic); ShardCapDB.cap = 1; fire("BAG_UPDATE"); fire("BAG_UPDATE_DELAYED"); run() check(bags[0].slots[1] and bags[0].slots[1].id == 16265, tag .. "item 16265 kept") check(count() == 1, tag .. "shards trimmed to 1") -- 4. Merge partial stacks into the soul bag stack reset(); setBag(0, 16); setBag(4, 12, 22243) put(0, 5, 6265, 1); put(4, 1, 6265, 2); put(4, 2, 6265, 1) load(classic); ShardCapDB.cap = 20; fire("BAG_UPDATE"); fire("BAG_UPDATE_DELAYED"); run() check(count() == 4, tag .. "merge keeps all 4 shards") check(slotsUsed() == 2, tag .. "4 shards in 2 slots after merging (got " .. slotsUsed() .. ")") check(count(0) == 0, tag .. "backpack emptied into soul bag") -- 5. Blizzlike (no stacking): move shards into free soul bag slots reset({ maxStack = 1 }); setBag(0, 16); setBag(2, 16, 22243) put(0, 1, 6265); put(0, 2, 6265); put(2, 1, 6265) load(classic); ShardCapDB.cap = 20; fire("BAG_UPDATE"); fire("BAG_UPDATE_DELAYED"); run() check(count(2) == 3 and count(0) == 0, tag .. "shards moved into soul bag in bag 2") -- 6. Combat: nothing deleted in combat, trimmed on leaving it reset(); setBag(0, 16) for s = 1, 5 do put(0, s, 6265, 1) end load(classic); run(); ShardCapDB.cap = 2 combat = true; fire("PLAYER_REGEN_DISABLED"); put(0, 6, 6265, 1); fire("BAG_UPDATE"); fire("BAG_UPDATE_DELAYED"); run() check(count() == 6, tag .. "no deletion during combat") combat = false; fire("PLAYER_REGEN_ENABLED"); run() check(count() == 2, tag .. "trimmed after combat (got " .. count() .. ")") -- 7. Manual /shardcap delete works in combat reset(); setBag(0, 16); for s = 1, 4 do put(0, s, 6265, 1) end load(classic); combat = true; fire("PLAYER_REGEN_DISABLED"); ShardCapDB.cap = 1 SlashCmdList.SHARDCAP("delete"); run() check(count() == 1, tag .. "manual delete in combat") -- 8. Holding an item on the cursor blocks deletion reset(); setBag(0, 16); for s = 1, 4 do put(0, s, 6265, 1) end cursor = { id = 6948, count = 1 } load(classic); ShardCapDB.cap = 1; fire("BAG_UPDATE"); fire("BAG_UPDATE_DELAYED"); run(2) check(count() == 4 and cursor.id == 6948, tag .. "waits while the cursor holds an item") cursor = nil; run() check(count() == 1, tag .. "proceeds once the cursor is free") -- 9. Locked stack is skipped until unlocked reset(); setBag(0, 16); put(0, 1, 6265, 1); put(0, 2, 6265, 1) bags[0].slots[2].locked = true; bags[0].slots[1].locked = true load(classic); ShardCapDB.cap = 0; fire("BAG_UPDATE"); fire("BAG_UPDATE_DELAYED"); run(1) check(count() == 2, tag .. "locked stacks untouched") bags[0].slots[2].locked = nil; bags[0].slots[1].locked = nil; run() check(count() == 0, tag .. "deleted after unlock") -- 10. Reagents go into the soul bag when there's room beyond the shard reserve reset({ familyKnown = true }); setBag(0, 16); setBag(4, 12, 22243) put(0, 1, 16583, 2); put(0, 2, 5565, 7); put(4, 1, 6265, 3) load(classic); ShardCapDB.cap = 12; fire("BAG_UPDATE"); fire("BAG_UPDATE_DELAYED"); run() check(where(16583)[1] == 4 and where(5565)[1] == 4, tag .. "figurine and infernal stone moved to soul bag") check(bags[4].slots[12] ~= nil and bags[4].slots[11] ~= nil, tag .. "reagents fill soul bag from the end") check(count() == 3, tag .. "shards untouched by reagent moves") -- 11. Reserve: cap 12 / 3 per slot = 4 slots kept for shards reset({ familyKnown = true }); setBag(0, 16); setBag(4, 5, 22243) put(0, 1, 16583, 1); put(0, 2, 5565, 1); put(4, 1, 6265, 1) load(classic); ShardCapDB.cap = 12; fire("BAG_UPDATE"); fire("BAG_UPDATE_DELAYED"); run() local moved = 0 if where(16583)[1] == 4 then moved = moved + 1 end if where(5565)[1] == 4 then moved = moved + 1 end check(moved == 1, tag .. "only 1 reagent moved into a 5-slot soul bag (4 kept for shards), got " .. moved) -- 12. Shards outside the soul bag get the free slots first reset({ familyKnown = true }); setBag(0, 16); setBag(4, 2, 22243) put(0, 1, 16583, 1); put(0, 2, 6265, 1); put(0, 3, 6265, 1) load(classic); ShardCapDB.cap = 6; ShardCapDB.sort = true; fire("BAG_UPDATE"); fire("BAG_UPDATE_DELAYED"); run() check(where(16583)[1] == 0, tag .. "reagent waits when shards need the space") -- 13. Server that doesn't allow reagents in soul bags: gives up, no endless retries reset({ reagentsOK = false }); setBag(0, 16); setBag(4, 12, 22243) put(0, 1, 16583, 1); put(0, 2, 5565, 1) load(classic); ShardCapDB.cap = 3; local before = table.getn(chat) fire("BAG_UPDATE"); fire("BAG_UPDATE_DELAYED"); run(20) check(where(16583)[1] == 0 and where(5565)[1] == 0 and cursor == nil, tag .. "refused reagents stay put, cursor empty") check(table.getn(chat) - before == 2, tag .. "one notice per refused item (got " .. (table.getn(chat) - before) .. ")") check(not F.shown, tag .. "scheduler goes idle after giving up") -- 14. Blizzlike server (shards don't stack): reagents left alone reset({ maxStack = 1, reagentsOK = false }); setBag(0, 16); setBag(4, 12, 22243) put(0, 1, 16583, 1) load(classic); fire("BAG_UPDATE"); fire("BAG_UPDATE_DELAYED"); run() check(where(16583)[1] == 0 and table.getn(chat) == 0, tag .. "no attempt on Blizzlike servers") -- 15. Toggle off reset({ familyKnown = true }); setBag(0, 16); setBag(4, 12, 22243); put(0, 1, 5565, 1) load(classic); SlashCmdList.SHARDCAP("reagents"); run() check(where(5565)[1] == 0 and ShardCapDB.reagents == false, tag .. "/shardcap reagents turns it off") end -- 10. Migration from ShardCap 1.x and slash parsing print("[misc]") reset(); setBag(0, 16) dofile(ADDON_FILE); F = frames[1] SHARDCAP_CAP_VALUE, SHARDCAP_SPAM = 7, true fire("ADDON_LOADED", "ShardCap") check(ShardCapDB.cap == 7 and ShardCapDB.notify == true, "migrated 1.x cap and spam setting") check(SHARDCAP_CAP_VALUE == nil, "old globals cleared") SlashCmdList.SHARDCAP("-3"); check(ShardCapDB.cap == 7, "negative cap rejected") SlashCmdList.SHARDCAP("2.5"); check(ShardCapDB.cap == 7, "fractional cap rejected") SlashCmdList.SHARDCAP(" 15 "); check(ShardCapDB.cap == 15, "cap set with whitespace") SlashCmdList.SHARDCAP("soulbag 3"); check(ShardCapDB.soulbags and ShardCapDB.soulbags[3], "manual soul bag") SlashCmdList.SHARDCAP("soulbag auto"); check(ShardCapDB.soulbags == nil, "back to auto") SlashCmdList.SHARDCAP("status"); SlashCmdList.SHARDCAP("help") check(table.getn(chat) > 5, "status/help print") reset(); setBag(0, 16); put(0, 1, 6265, 3); put(0, 2, 6265, 2) load(false); ShardCapDB.cap = 12; chat = {}; SlashCmdList.SHARDCAP("") check(string.find(chat[2] or "", "Demonic Figurines: |cffffffff0|r Infernal Stones: |cffffffff0|r", 1, true) ~= nil, "reagent line with none carried") reset(); setBag(0, 16); setBag(4, 12, 22243); put(0, 1, 6265, 3); put(0, 2, 6265, 2) put(0, 3, 16583, 2); put(4, 5, 16583, 1); put(0, 4, 5565, 7) load(false); ShardCapDB.cap = 12; ShardCapDB.reagents = false; chat = {}; SlashCmdList.SHARDCAP("") check(string.find(chat[2] or "", "Demonic Figurines: |cffffffff3|r Infernal Stones: |cffffffff7|r", 1, true) ~= nil, "reagents counted across all bags incl. soul bag (got: " .. (chat[2] or "nil") .. ")") check(string.find(chat[1] or "", "Soul Shards: |cffffffff5/12|r", 1, true) ~= nil, "/shardcap shows 5/12 (got: " .. (chat[1] or "nil") .. ")") -- 11. Unknown custom soul bag found through its "Soul Bag" subtype reset(); setBag(0, 16); setBag(3, 20, 99999); put(0, 1, 6265, 1) load(false); ShardCapDB.cap = 10; fire("BAG_UPDATE"); run() check(count(3) == 1, "soul bag detected by subtype") -- 12. Options window (ClassicAPI AddOn Options) print("[options]") local function findControl(page, label) for _, c in ipairs(page.controls) do local t = c.name and _G[c.name .. "Text"] if t and string.find(t.text, label, 1, true) then return c end end end local function click(c) if c.checked then c.checked = nil else c.checked = 1 end; call(c, "OnClick") end local function findKind(page, kind) for _, c in ipairs(page.controls) do if c.kind == kind then return c end end end reset(); setBag(0, 16); setBag(4, 12, 22243) put(0, 1, 6265, 3); put(0, 2, 6265, 2); put(4, 1, 16583, 2); put(0, 3, 5565, 4) bank = { [6265] = 4, [5565] = 10 } load(true, true); ShardCapDB.cap = 12 local main, bp = categories[1], categories[2] check(main and main.name == "ShardCap" and bp and bp.name == "Soul Bag" and bp.parent == "ShardCap", "two pages registered, Soul Bag under ShardCap") SlashCmdList.SHARDCAP("options") check(categories.opened == main and main.shown, "/shardcap options opens the ShardCap page") local texts = {} for _, f in ipairs(allWidgets) do if f.kind == "FontString" and f.text ~= "" then table.insert(texts, f.text) end end local joined = table.concat(texts, "\n") check(string.find(joined, "5/12", 1, true) ~= nil, "status shows 5/12") check(string.find(joined, "(+4 in bank)", 1, true) ~= nil, "status shows shards in bank") check(string.find(joined, "Infernal Stones: |cffffffff4|r |cffaaaaaa(+10 in bank)|r", 1, true) ~= nil, "status shows stones with bank count") local slider = findKind(main, "Slider") check(slider.value == 12, "slider shows the cap") slider:SetValue(30) check(ShardCapDB.cap == 30, "slider sets the cap") local box for _, f in ipairs(frames) do if f.kind == "EditBox" then box = f end end check(box.text == "30", "box follows the slider") box:SetFocus(); box:SetText("150"); call(box, "OnEnterPressed") check(ShardCapDB.cap == 150 and slider.value == 100, "box sets caps above the slider range") box:SetFocus(); box:SetText("250"); call(box, "OnEnterPressed") check(ShardCapDB.cap == 150 and box.text == "150", "out-of-range box value rejected") box:SetFocus(); box:SetText("7"); call(box, "OnEscapePressed") check(ShardCapDB.cap == 150 and box.text == "150", "Escape in the box discards the edit") local notify = findControl(main, "Chat notifications") click(notify); check(ShardCapDB.notify == true, "notifications checkbox") -- Cancel restores what was there when the window opened; Okay keeps changes main.cancel(main); bp.cancel(bp) check(ShardCapDB.cap == 12 and ShardCapDB.notify == false, "Cancel restores settings") SlashCmdList.SHARDCAP("options"); slider:SetValue(20); main.okay(main); bp.okay(bp) SlashCmdList.SHARDCAP("options"); main.cancel(main) check(ShardCapDB.cap == 20, "Okay keeps settings") -- Defaults reset only the page shown ShardCapDB.notify = true; ShardCapDB.reagents = false main.default(main) check(ShardCapDB.cap == 12 and ShardCapDB.notify == false and ShardCapDB.reagents == false, "Defaults on ShardCap page leave Soul Bag settings") bp.default(bp) check(ShardCapDB.reagents == true, "Defaults on Soul Bag page") -- Soul bag page bp:Show(); bp.refresh(bp) local auto = findControl(bp, "Detect soul bags automatically") local bag4 = findControl(bp, "Bag 4") local bag2 = findControl(bp, "Bag 2") check(auto.checked and bag4.checked and not bag4.enabled, "auto detection on; bag 4 shown as soul bag but locked") check(not bag2.enabled and string.find(_G[bag2.name .. "Text"].text, "empty slot", 1, true) ~= nil, "empty bag slot greyed out") click(auto) check(type(ShardCapDB.soulbags) == "table" and ShardCapDB.soulbags[4] and bag4.enabled, "manual mode seeded from detection") click(bag4) check(not ShardCapDB.soulbags[4], "bag checkbox clears a soul bag") click(auto) check(ShardCapDB.soulbags == nil, "back to auto") local reag = findControl(bp, "Demonic Figurines and Infernal Stones") click(reag); check(ShardCapDB.reagents == false, "reagents checkbox") -- Live refresh only while shown put(0, 4, 6265, 1) this = main; arg1 = 0.6; main.scripts.OnUpdate() texts = {} for _, f in ipairs(allWidgets) do if f.kind == "FontString" then table.insert(texts, f.text) end end check(string.find(table.concat(texts, "\n"), "6/12", 1, true) ~= nil, "status refreshes while open") -- Without ClassicAPI's options window reset(); setBag(0, 16) load(false, false); chat = {} SlashCmdList.SHARDCAP("options") check(table.getn(categories) == 0 and string.find(chat[1] or "", "needs ClassicAPI", 1, true) ~= nil, "no panel without ClassicAPI; slash explains") SlashCmdList.SHARDCAP("") check(string.find(chat[2] or "", "5/12", 1, true) == nil and string.find(chat[2] or "", "in bank", 1, true) == nil, "no bank text without ClassicAPI") -- New ClassicAPI buffer contract: empty slots leave stale buffer fields intact. reset(); setBag(0, 4); setBag(4, 4, 22243) put(0, 1, 6265, 3); put(0, 2, 16583, 2); put(4, 1, 5565, 1) load(true) local bufferSeen, bufferCalls = nil, 0 C_Container.GetContainerItemInfo = function(b, s, buf) check(type(buf) == "table" and (not bufferSeen or bufferSeen == buf), "one shared item-info buffer") bufferSeen = buf; bufferCalls = bufferCalls + 1 local it = bags[b].slots[s] if not it then return nil end buf.itemID, buf.stackCount, buf.isLocked = it.id, it.count, false return buf end local shards, reagents, soul = ShardCapAPI.StatusLines() check(bufferCalls == 8 and string.find(shards, "3/12", 1, true) ~= nil, "buffer scan ignores stale data in empty slots") check(string.find(reagents, "2|r", 1, true) ~= nil and string.find(reagents, "1|r", 1, true) ~= nil and string.find(soul, "3 free soul slots", 1, true) ~= nil, "count-only status includes reagents and free soul slots") -- Status refresh must not build action records (table.insert is used by Scan). local oldInsert = table.insert table.insert = function(t, value) if type(value) == "table" then error("status allocated an action record") end return oldInsert(t, value) end local statusOK = pcall(ShardCapAPI.StatusLines) table.insert = oldInsert check(statusOK, "status avoids sorting records") run() check(count() == 3 and count(0) == 0 and not F.shown, "action scans copy values from the shared buffer before reusing it") -- Refused sends must stop rather than notify, retry, or reject a reagent. for _, kind in ipairs({ "merge", "shard", "reagent", "shard MoveItem" }) do reset(); setBag(0, 4); setBag(4, 8, 22243) if kind == "merge" then put(0, 1, 6265, 1); put(4, 1, 6265, 1) elseif kind ~= "reagent" then put(0, 1, 6265, 3) else put(0, 1, 16583, 2) end load(true); ShardCapDB.notify = true if kind == "shard MoveItem" then C_Container.SwapItems = nil fire("ADDON_LOADED", "ShardCap") -- detect MoveItem without SwapItems end local calls = 0 local function refuse() calls = calls + 1; return false end local move, swap = C_Container.MoveItem, C_Container.SwapItems C_Container.MoveItem, C_Container.SwapItems = refuse, refuse chat = {}; run() check(calls == 1 and table.getn(chat) == 0 and not F.shown, kind .. " refused send stops without a success notice or polling") C_Container.MoveItem, C_Container.SwapItems = move, swap fire("BAG_UPDATE_DELAYED"); run() check(not F.shown and (kind == "reagent" and bags[0].slots[1] == nil or kind ~= "reagent" and count(0) == 0), kind .. " retries on a later bag event") end print(string.format("\n%d passed, %d failed", passes, fails)) if fails > 0 then os.exit(1) end