test(selftest): pin the sidebar active-surface invariant

Drives the real Sidebar.Draw and reads the render counter, so a regression in
the draw path fails instead of a parallel calculation passing.

Three cases: one of two tabs active draws exactly one surface, a null active
tab draws zero, and icon-only mode still marks the active row.

Zero is a legitimate state, not a failure -- PickMainActiveTab returns null
when every tab is popped out, so the invariant is "at most one", not "exactly
one".
This commit is contained in:
2026-08-17 23:54:13 +02:00
parent 9936638bb3
commit 5caa266287
2 changed files with 92 additions and 0 deletions
+1
View File
@@ -454,6 +454,7 @@ public sealed class Plugin : IAsyncDalamudPlugin
new SelfTests.TellResetOnActivateStep(),
new SelfTests.CurrentTabCouplingStep(this),
new SelfTests.SidebarUnreadDotStep(this),
new SelfTests.SidebarActiveSurfaceStep(this),
new SelfTests.UnreadDecisionStep(),
new SelfTests.CurrentTabGuidedStep(this),
new SelfTests.CardClipPlanStep(this),
@@ -0,0 +1,91 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Plugin.SelfTest;
using HellionChat.Code;
namespace HellionChat.SelfTests;
// v1.10.0/C3: the active row gets a surface and an accent bar, so exactly the
// row the user is on must be marked -- and only that one. Drives the real
// Sidebar.Draw and reads the render-observability counter, so a regression in
// the draw path fails rather than a parallel calculation passing.
//
// "At most one", not "exactly one": PickMainActiveTab returns null when every
// tab is popped out, which is a legitimate state with zero marked rows.
internal sealed class SidebarActiveSurfaceStep : ISelfTestStep
{
private readonly Plugin _plugin;
public SidebarActiveSurfaceStep(Plugin plugin) => _plugin = plugin;
public string Name => "Hellion Chat - Sidebar active surface";
public SelfTestStepResult RunStep()
{
var sidebar = _plugin.MainWindow.GetSidebarForSelfTest();
if (sidebar is null)
{
ImGui.Text("Sidebar null");
SelfTestReport.Append(Name, "FAIL", new[] { "Sidebar null" });
return SelfTestStepResult.Fail;
}
var a = NewProbe("Surface Probe A@SelfTest");
var b = NewProbe("Surface Probe B@SelfTest");
var list = new List<Tab> { a, b };
var width = (float)Plugin.Config.SidebarAutoSwitchThresholdPx + 100f;
var savedWidth = Plugin.Config.SidebarWidth;
try
{
Plugin.Config.SidebarWidth = 220;
// (a) one of two tabs is active -> exactly one surface
Tab? active = a;
sidebar.Draw(width, list, ref active);
if (sidebar.LastRenderedActiveSurfaceCount != 1)
return Fail(
$"active tab drew {sidebar.LastRenderedActiveSurfaceCount}, expected 1"
);
// (b) no active tab (every tab popped out) -> zero, not a crash
active = null;
sidebar.Draw(width, list, ref active);
if (sidebar.LastRenderedActiveSurfaceCount != 0)
return Fail(
$"null active drew {sidebar.LastRenderedActiveSurfaceCount}, expected 0"
);
// (c) icon-only mode still marks the active row
active = b;
sidebar.Draw((float)Plugin.Config.SidebarAutoSwitchThresholdPx - 1f, list, ref active);
if (sidebar.LastRenderedActiveSurfaceCount != 1)
return Fail($"icon-only drew {sidebar.LastRenderedActiveSurfaceCount}, expected 1");
SelfTestReport.Append(Name, "PASS", new[] { "1 / 0 / 1 across the three cases" });
return SelfTestStepResult.Pass;
}
finally
{
Plugin.Config.SidebarWidth = savedWidth;
}
}
private static Tab NewProbe(string name) =>
new()
{
Name = name,
SelectedChannels = new Dictionary<ChatType, (ChatSource, ChatSource)>
{
[ChatType.Say] = (ChatSourceExt.All, ChatSourceExt.All),
},
};
private SelfTestStepResult Fail(string message)
{
ImGui.Text(message);
SelfTestReport.Append(Name, "FAIL", new[] { message });
return SelfTestStepResult.Fail;
}
public void CleanUp() { }
}