From 5696eac55a7209dcde2aefe12b17b8f0442647b8 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Mon, 17 Aug 2026 23:57:11 +0200 Subject: [PATCH] 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. --- HellionChat/Plugin.cs | 1 + HellionChat/SelfTests/TopTabUnderlineStep.cs | 76 ++++++++++++++++++++ HellionChat/Ui/Windows/MainWindow.cs | 2 + 3 files changed, 79 insertions(+) create mode 100644 HellionChat/SelfTests/TopTabUnderlineStep.cs diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs index 542a5d7..8ed0f49 100755 --- a/HellionChat/Plugin.cs +++ b/HellionChat/Plugin.cs @@ -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), diff --git a/HellionChat/SelfTests/TopTabUnderlineStep.cs b/HellionChat/SelfTests/TopTabUnderlineStep.cs new file mode 100644 index 0000000..1245d8e --- /dev/null +++ b/HellionChat/SelfTests/TopTabUnderlineStep.cs @@ -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 { 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.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() { } +} diff --git a/HellionChat/Ui/Windows/MainWindow.cs b/HellionChat/Ui/Windows/MainWindow.cs index a89982a..a65460e 100644 --- a/HellionChat/Ui/Windows/MainWindow.cs +++ b/HellionChat/Ui/Windows/MainWindow.cs @@ -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;