test(selftest): pin the top-tab underline invariant

TopTabBar had no observability at all -- no counter, no self-test reaching it.
It now exposes LastRenderedUnderlineCount and MainWindow hands the component
out the same way it already does for the sidebar.

Three cases: one of two tabs active draws exactly one underline, a null active
tab draws zero, and an active tab that is not in the list also draws zero. The
last one matters because the strip skips popped-out tabs, so the active tab
legitimately need not be among the drawn ones.
This commit is contained in:
2026-08-17 23:57:11 +02:00
parent e4659bc597
commit 5696eac55a
3 changed files with 79 additions and 0 deletions
+1
View File
@@ -455,6 +455,7 @@ public sealed class Plugin : IAsyncDalamudPlugin
new SelfTests.CurrentTabCouplingStep(this),
new SelfTests.SidebarUnreadDotStep(this),
new SelfTests.SidebarActiveSurfaceStep(this),
new SelfTests.TopTabUnderlineStep(this),
new SelfTests.UnreadDecisionStep(),
new SelfTests.CurrentTabGuidedStep(this),
new SelfTests.CardClipPlanStep(this),
@@ -0,0 +1,76 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Plugin.SelfTest;
using HellionChat.Code;
namespace HellionChat.SelfTests;
// v1.10.0/D1: the top-tab strip marks the active tab with a fill plus an accent
// underline. Drives the real TopTabBar.Draw and reads the render counter.
//
// "At most one", not "exactly one": the strip skips popped-out tabs, so zero
// underlines is a legitimate state.
internal sealed class TopTabUnderlineStep : ISelfTestStep
{
private readonly Plugin _plugin;
public TopTabUnderlineStep(Plugin plugin) => _plugin = plugin;
public string Name => "Hellion Chat - Top tab underline";
public SelfTestStepResult RunStep()
{
var strip = _plugin.MainWindow.GetTopTabsForSelfTest();
if (strip is null)
{
ImGui.Text("TopTabBar null");
SelfTestReport.Append(Name, "FAIL", new[] { "TopTabBar null" });
return SelfTestStepResult.Fail;
}
var a = NewProbe("Underline Probe A@SelfTest");
var b = NewProbe("Underline Probe B@SelfTest");
var list = new List<Tab> { a, b };
// (a) one of two tabs active -> exactly one underline
Tab? active = a;
strip.Draw(list, ref active);
if (strip.LastRenderedUnderlineCount != 1)
return Fail(
$"active tab drew {strip.LastRenderedUnderlineCount} underlines, expected 1"
);
// (b) no active tab -> zero, not a crash
active = null;
strip.Draw(list, ref active);
if (strip.LastRenderedUnderlineCount != 0)
return Fail($"null active drew {strip.LastRenderedUnderlineCount}, expected 0");
// (c) an active tab that is not in the list -> still zero
active = NewProbe("Absent@SelfTest");
strip.Draw(list, ref active);
if (strip.LastRenderedUnderlineCount != 0)
return Fail($"absent active drew {strip.LastRenderedUnderlineCount}, expected 0");
SelfTestReport.Append(Name, "PASS", new[] { "1 / 0 / 0 across the three cases" });
return SelfTestStepResult.Pass;
}
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() { }
}
+2
View File
@@ -204,6 +204,8 @@ internal sealed class MainWindow : Window, IFocusableChatWindow
internal Components.MessageList GetMessageListForSelfTest() => _messages;
internal Components.TopTabBar GetTopTabsForSelfTest() => _topTabs;
public override bool DrawConditions() => !_userHidden;
internal void UserHide() => _userHidden = true;