Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .github/chat-meters-embed.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32 changes: 32 additions & 0 deletions .tools/chat-meters-tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Chat meters regression tests

From the repository root, with Python 3 and `lupa==2.8` installed:

```sh
python .tools/chat-meters-tests/run.py
```

The runner compiles each affected source file with Lua 5.1, then executes the
complete chat-host controller and the actual production placement, unlock,
visibility, sidebar and Combat Log functions against simulated WoW frames.
The runner extracts those closures because the surrounding modules initialize
the rest of the WoW UI. Their guards are not duplicated in the mock.

Covered behavior:

- No host frames, subscriptions, timers or hooks on a fresh disabled profile.
- Exactly two existing windows adopted; extra windows left standalone.
- Full header controls, transparent backgrounds and sidebar placement.
- All 32 combinations of edit/unlock/settings/resize flags and selected view.
- Native placement/size/mover writes cannot override embedding or saved geometry.
- Live geometry following without additional combat-data fetches.
- Native tab clicks, Combat Log restoration and instance/manual selection.
- Hidden Settings icon, hidden sidebar, unavailable host and chat passthrough.
- Partial/empty window rebuilds, replacements, profile opt-out and restoration.
- Disable removes event subscriptions/callbacks; re-enable reuses allocated frames.

The mocks reject writes to native chat frames/tabs and any controller timer or
method hook. Display-container alpha is exercised separately through the engine.
These tests cannot certify live-client taint, combat restrictions, rendering,
anchor resolution or performance. An in-game pass on Midnight 12.1 is still required.
The directory is excluded from release packages by the existing `.pkgmeta`.
179 changes: 179 additions & 0 deletions .tools/chat-meters-tests/mock_wow.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
-- Behavioral mocks, deliberately reject any write to a native chat frame/tab.
local F = {}
local function writable(f) assert(not f.native, "Attempt to modify native chat: " .. tostring(f.name)) end
function F:SetParent(v)
writable(self)
if self.parent~=v then self.parentChanges=(self.parentChanges or 0)+1 end
self.parent = v
end
function F:GetParent() return self.parent end
function F:GetName() return self.name end
function F:SetSize(w,h)
writable(self)
local changed=self.w~=w or self.h~=h
self.w,self.h = w,h
if changed and self.scripts.OnSizeChanged then self.scripts.OnSizeChanged(self,w,h) end
end
function F:SetWidth(v) writable(self); self.w = v end
function F:SetHeight(v) writable(self); self.h = v end
function F:GetWidth() return self.w end
function F:GetHeight() return self.h end
function F:SetScale(v) writable(self); self.scale = v end
function F:GetScale() return self.scale end
function F:GetEffectiveScale() return self.scale * (self.parent and self.parent:GetEffectiveScale() or 1) end
function F:SetFrameStrata(v) writable(self); self.strata = v end
function F:GetFrameStrata() return self.strata end
function F:SetFrameLevel(v) writable(self); self.level = v end
function F:GetFrameLevel() return self.level end
function F:SetAlpha(v) writable(self); self.alpha = v end
function F:GetAlpha() return self.alpha end
function F:SetClampedToScreen(v) writable(self); self.clamped = v end
function F:IsClampedToScreen() return self.clamped end
function F:IsProtected() return self.protected or false end
function F:SetPoint(...) writable(self); self.points[#self.points+1] = {...} end
function F:GetPoint(i) return unpack(self.points[i or 1] or {}) end
function F:GetNumPoints() return #self.points end
function F:ClearAllPoints() writable(self); self.points = {} end
function F:SetAllPoints() writable(self) end
function F:GetLeft() return self.left or 20 end
function F:GetRight() return self.right or (self:GetLeft()+self.w) end
function F:GetBottom() return self.bottom or 280 end
function F:Show() writable(self); self.shown = true end
function F:Hide()
writable(self)
local wasShown=self.shown
self.shown = false
if wasShown and self.scripts.OnHide then self.scripts.OnHide(self) end
end
function F:SetShown(v) if v then self:Show() else self:Hide() end end
function F:IsShown() return self.shown end
function F:IsVisible() return self.shown and (not self.parent or self.parent:IsVisible()) end
function F:EnableMouse() writable(self) end
function F:EnableMouseWheel() writable(self) end
function F:RegisterForClicks() writable(self) end
function F:SetScript(k,v) writable(self); self.scripts[k] = v end
function F:HookScript(k,v)
writable(self)
local old=self.scripts[k]
self.scripts[k]=function(...) if old then old(...) end; v(...) end
end
function F:GetScript(k) return self.scripts[k] end
function F:RegisterEvent(k) self.events[k] = true end
function F:SetText(v) writable(self); self.text = v end
function F:SetTextColor() writable(self) end
function F:SetColorTexture() writable(self) end
function F:SetFont() writable(self) end
function F:SetJustifyH() writable(self) end
function F:SetWordWrap() writable(self) end
function F:SetChecked(v) self.checked = v end
function F:GetChecked() return self.checked end
function F:CreateTexture() return CreateFrame("Texture",nil,self) end
function F:CreateFontString() return CreateFrame("FontString",nil,self) end
function F:SetOwner() end
function F:AddLine() end
FRAMES = {}
function CreateFrame(kind,name,parent,template)
local f = setmetatable({kind=kind,name=name,parent=parent,points={},scripts={},events={},
w=300,h=200,scale=1,strata="MEDIUM",level=1,alpha=1,shown=true,clamped=false}, {__index=F})
if name then _G[name] = f end
FRAMES[#FRAMES+1] = f
if template == "InterfaceOptionsCheckButtonTemplate" then f.Text=f:CreateFontString() end
return f
end
UIParent = CreateFrame("Frame","UIParent")
GameTooltip = CreateFrame("Frame")
ChatFrame1 = CreateFrame("Frame","ChatFrame1",UIParent)
ChatFrame1.native = true
ChatFrame2 = CreateFrame("Frame","ChatFrame2",UIParent)
ChatFrame2.native = true
ChatFrame1Tab = CreateFrame("Button","ChatFrame1Tab",UIParent)
ChatFrame1Tab.right = 120; ChatFrame1Tab.native = true
ChatFrame2Tab = CreateFrame("Button","ChatFrame2Tab",UIParent)
ChatFrame2Tab.right = 360; ChatFrame2Tab.native = true
GENERAL_CHAT_DOCK = { DOCKED_CHAT_FRAMES={ChatFrame1,ChatFrame2} }
SELECTED = ChatFrame1
function FCFDock_GetSelectedWindow() return SELECTED end
function FCFDock_SelectWindow() error("Must not select native chat") end
SECRET = {}
function issecretvalue(v) return v == SECRET end
function MouseIsOver(f) return f.mouseOver == true end
ZONE_KIND, ZONE_ID = "none", 0
function IsInInstance() return ZONE_KIND ~= "none", ZONE_KIND end
function GetInstanceInfo() return "Zone",ZONE_KIND,0,0,0,0,0,ZONE_ID end
Enum = { DamageMeterType = {DamageDone=0,HealingDone=2} }
SlashCmdList = {}
local pending = {}
C_Timer = {
After=function(_,f) pending[#pending+1]=f end,
NewTicker=function(_,f) return { callback=f, Cancel=function(s) s.cancelled=true end } end,
}
function Flush()
local q=pending; pending={}
for _,f in ipairs(q) do f() end
end
function Event(name,...)
for _,f in ipairs(FRAMES) do if f.events[name] then f.scripts.OnEvent(f,name,...) end end
Flush()
end
function hooksecurefunc(t,k,hook)
assert(type(t)=="table" and not t.native, "Native hook")
local old=t[k]
t[k]=function(...) local result=old(...); hook(...); return result end
end
Settings = {
RegisterCanvasLayoutCategory=function() return {GetID=function() return 123 end} end,
RegisterAddOnCategory=function() end,
OpenToCategory=function(id) OPENED_CATEGORY=id end,
}
CHAT_CONFIG = {enabled=true,tabHeight=24}
PANEL = CreateFrame("Frame",nil,UIParent); PANEL:SetSize(520,280)
CHAT_DATA = {bg=PANEL,_bgIns={b=-34}}
CHAT_DATA.sidebar=CreateFrame("Frame",nil,UIParent)
CHAT_DATA.portalBtn=CreateFrame("Button",nil,CHAT_DATA.sidebar)
CHAT_DATA.settingsBtn=CreateFrame("Button",nil,CHAT_DATA.sidebar)
CHAT_DATA.settingsBtn:SetSize(22,22)
CHAT_DATA.settingsBtn:SetPoint("TOP",CHAT_DATA.portalBtn,"BOTTOM",0,-10)
CHAT = { ECHAT={DB=function() return CHAT_CONFIG end, ResetIdleTimer=function() end} }
CHAT._chatWins={
[ChatFrame1]={smf=CreateFrame("Frame",nil,PANEL),track=CreateFrame("Frame",nil,PANEL)},
[ChatFrame2]={smf=CreateFrame("Frame",nil,PANEL),track=CreateFrame("Frame",nil,PANEL)},
}
ChatFrame2.FontStringContainer=CreateFrame("Frame",nil,UIParent)
ChatFrame2.ScrollBar={Track=CreateFrame("Frame",nil,UIParent)}
CombatLogQuickButtonFrame_Custom=CreateFrame("Frame",nil,UIParent)
DM = {_windows={}}
EllesmereUI = {
_ModuleNS={EllesmereUIChat=CHAT,EllesmereUIDamageMeters=DM},
_chatCFD=function() return CHAT_DATA end,
GetFontPath=function() return "font.ttf" end,
GetAccentColor=function() return 0,1,0 end,
}
SAVED = { width=375,height=180,position={x=1000,y=300} }
function NewWindow(kind)
local w={curDMType=kind,windowLocked=false}
w.frame=CreateFrame("Frame",nil,UIParent)
w.frame:SetSize(SAVED.width,SAVED.height)
w.frame:SetPoint("TOPLEFT",UIParent,"BOTTOMLEFT",1000,300)
w.frame:SetClampedToScreen(true)
w.header=CreateFrame("Frame",nil,w.frame)
w.frame._bg=w.frame:CreateTexture()
w.header._hdrBg=w.header:CreateTexture()
w.header:SetScript("OnMouseDown",function() w.dragAttempted=true end)
w.header:SetScript("OnMouseUp",function() w.homeOpened=true end)
w.titleText=w.header:CreateFontString()
w.timerText=w.header:CreateFontString()
w.hdrBtns={}
for i=1,5 do
w.hdrBtns[i]=CreateFrame("Button",nil,w.header)
w.hdrBtns[i]:SetPoint("RIGHT",w.header,"RIGHT",-i*22,0)
end
w.segmentBtn=w.hdrBtns[2]
w.resizeGrip=CreateFrame("Button",nil,w.frame)
w.lockBtn=CreateFrame("Button",nil,w.frame)
w.UpdateVisibility=function() if w.hideRule then w.frame:Hide() else w.frame:Show() end end
w.Refresh=function() w.refreshes=(w.refreshes or 0)+1 end
w.FitTitle=function() end
w.ApplyPosition=function() end
return w
end
DM._windows={NewWindow(0),NewWindow(2),NewWindow(5)}
47 changes: 47 additions & 0 deletions .tools/chat-meters-tests/run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""Run with Python 3 + lupa. Uses the actual Lua 5.1 controller and core closures."""
from pathlib import Path
from lupa.lua51 import LuaRuntime

here = Path(__file__).resolve().parent
root = here.parents[1]
lua = LuaRuntime(unpack_returned_tuples=True)
dm = (root / "EllesmereUIDamageMeters/EllesmereUIDamageMeters.lua").read_text(encoding="utf-8")
chat = (root / "EllesmereUIChat/EllesmereUIChat.lua").read_text(encoding="utf-8")
engine = (root / "EllesmereUIChat/EllesmereUIChat_Engine.lua").read_text(encoding="utf-8")

paths = ["EllesmereUIDamageMeters/EllesmereUIDamageMeters.lua",
"EllesmereUIDamageMeters/EllesmereUIDamageMeters_Chat.lua",
"EllesmereUIChat/EllesmereUIChat.lua",
"EllesmereUIChat/EllesmereUIChat_Engine.lua",
"EllesmereUIChat/EllesmereUIChat_Tabs.lua",
"EllesmereUIOptions/EUI_DamageMeters_Options.lua"]
for path in paths:
lua.execute("assert(loadstring(...))", (root / path).read_text(encoding="utf-8"))

def section(source, start, end):
return source[source.index(start):source.index(end, source.index(start))]

lua.execute((here / "mock_wow.lua").read_text())
lua.execute((here / "setup.lua").read_text())
# Compile original function bodies against minimal module locals; guards are not
# reimplemented in the mock. This catches regressions in the production writers.
lua.execute("local ns, EUI, _windows, MIN_W, MIN_H, MAX_WINDOWS = DM, EllesmereUI, DM._windows, 150, 50, 5\n"
"local function DB() return CONFIG end\nlocal function WinDB() return SAVED end\n"
+ section(dm, "ns.ApplyWinPosition = function", "-- Bookmarks are shared")
+ section(dm, "ns.RegisterDMUnlock = function", "local _combatEndTime")
+ section(dm, "ns.ApplyDMSize = function", "-- Standalone Combat Timer"))
lua.execute("local ns, EUI, ECHAT = CHAT, EllesmereUI, CHAT.ECHAT\nlocal CFD = EUI._chatCFD\n"
"local _sidebarFadeTarget, _sidebarFadeAlpha = 1, 1\nlocal function GetTabAreaHeight() return 24 end\n"
+ section(chat, "function ECHAT.ApplySidebarIconVisibility()", "-- Map of sidebar-icon visibility key"))
lua.execute("local ns, ECHAT, WINS = CHAT, CHAT.ECHAT, CHAT._chatWins\n"
"local function IsCombatLog(cf) return cf == ChatFrame2 end\n"
"local function SetBarMouse() end\nlocal function RebuildWindowFromBuffer() end\n"
+ section(engine, "function ECHAT.EngineUpdateCombatLogHost()", "-- Mirrors, on our own standalone event frame"))
lua.execute("function InstallWindowWriters(W)\nlocal frame, wdb, EUI, ns = W.frame, SAVED, EllesmereUI, DM\n"
"local function DB() return CONFIG end\n"
+ section(dm, " function W.UpdateVisibility()", " if EUI.RegisterVisibilityUpdater then EUI.RegisterVisibilityUpdater(W.UpdateVisibility)")
+ "\nW.ApplyPosition = function() DM.ApplyWinPosition(frame, wdb, W.idx) end\nend")
lua.execute("for i,w in ipairs(DM._windows) do w.idx=i; InstallWindowWriters(w) end")
lua.execute((root / paths[1]).read_text(), "EllesmereUIDamageMeters", lua.globals().DM)
lua.execute((here / "scenarios.lua").read_text())
print("PASS: Lua 5.1 syntax, lazy opt-in, native ownership, sidebar and lifecycle regressions")
Loading