diff --git a/HellionChat/Integrations/HonorificService.cs b/HellionChat/Integrations/HonorificService.cs index 6a37588..5f139c6 100644 --- a/HellionChat/Integrations/HonorificService.cs +++ b/HellionChat/Integrations/HonorificService.cs @@ -195,4 +195,23 @@ internal sealed class HonorificService : IDisposable return false; return true; } + + // Test seam: the three status fields are private-set and IPC-driven, which a + // headless /xlperf run can't reach (Honorific is usually absent in tests). + // Callers MUST snapshot the prior values and restore them in CleanUp, and + // MUST drive Set -> Draw -> Assert within ONE synchronous RunStep (never + // Waiting between Set and Assert) — a between-frame OnReady/OnTitleChanged + // would otherwise clobber this state and a CleanUp restore can't un-corrupt a + // mid-flight assertion. (A FontsReady precondition gate returning Waiting + // BEFORE the snapshot/Set is fine — nothing is mutated yet.) + internal void TestOnly_SetState( + bool isAvailable, + (uint Major, uint Minor)? detectedApiVersion, + HonorificTitleData? title + ) + { + IsAvailable = isAvailable; + DetectedApiVersion = detectedApiVersion; + CurrentTitle = title; + } } diff --git a/HellionChat/SelfTests/HonorificHeaderRenderStep.cs b/HellionChat/SelfTests/HonorificHeaderRenderStep.cs index 3e57a4c..8d7d704 100644 --- a/HellionChat/SelfTests/HonorificHeaderRenderStep.cs +++ b/HellionChat/SelfTests/HonorificHeaderRenderStep.cs @@ -1,5 +1,6 @@ using Dalamud.Bindings.ImGui; using Dalamud.Plugin.SelfTest; +using HellionChat.Integrations; namespace HellionChat.SelfTests; @@ -20,8 +21,26 @@ internal sealed class HonorificHeaderRenderStep : ISelfTestStep public string Name => "Hellion Chat - HonorificHeader render"; + private HonorificService? _svc; + private bool _prevAvailable; + private (uint Major, uint Minor)? _prevVersion; + private HonorificTitleData? _prevTitle; + private bool _prevToggle; + private bool _snapshotted; + public SelfTestStepResult RunStep() { + // HonorificHeader.Draw early-returns on !FontsReady (HonorificHeader.cs:40-44) + // and never reaches the gated title branch, which would make assert (a) a + // false FAIL during a font-atlas rebuild. Return Waiting BEFORE any + // snapshot/mutation so the runner re-polls cleanly and no seam state leaks + // (precedent: FoxBannerTextureSmokeStep). This is a pre-Set precondition + // gate, not a mid-test Waiting — the Set->Draw->Assert window stays synchronous. + if (!plugin.FontManager.FontsReady) + { + return SelfTestStepResult.Waiting; + } + var header = plugin.MainWindow.GetHonorificHeaderForSelfTest(); if (header is null) { @@ -29,9 +48,48 @@ internal sealed class HonorificHeaderRenderStep : ISelfTestStep return SelfTestStepResult.Fail; } + _svc = header.GetServiceForSelfTest(); + _prevAvailable = _svc.IsAvailable; + _prevVersion = _svc.DetectedApiVersion; + _prevTitle = _svc.CurrentTitle; + _prevToggle = Plugin.Config.ShowHonorificTitleInHeader; + _snapshotted = true; + + var valid = new HonorificTitleData("Champion", false, false, null, null, null, null, null); + var original = new HonorificTitleData("Champion", false, true, null, null, null, null, null); + + // Draw at a deliberately wide 420px so the title never hits the truncation + // clamp — LastTitleRendered then reflects the GATE outcome, not the width. try { + // (a) available + valid title + toggle on -> title renders + Plugin.Config.ShowHonorificTitleInHeader = true; + _svc.TestOnly_SetState(true, (3, 1), valid); header.Draw(420f); + if (!header.LastTitleRendered) + { + ImGui.Text("Gate failed: valid title did not render"); + return SelfTestStepResult.Fail; + } + + // (b) toggle off -> title suppressed (crown stays, untestable headless) + Plugin.Config.ShowHonorificTitleInHeader = false; + header.Draw(420f); + if (header.LastTitleRendered) + { + ImGui.Text("Gate failed: title rendered with toggle off"); + return SelfTestStepResult.Fail; + } + + // (c) IsOriginal title -> suppressed even with toggle on + Plugin.Config.ShowHonorificTitleInHeader = true; + _svc.TestOnly_SetState(true, (3, 1), original); + header.Draw(420f); + if (header.LastTitleRendered) + { + ImGui.Text("Gate failed: original title rendered"); + return SelfTestStepResult.Fail; + } } catch (Exception ex) { @@ -42,5 +100,12 @@ internal sealed class HonorificHeaderRenderStep : ISelfTestStep return SelfTestStepResult.Pass; } - public void CleanUp() { } + public void CleanUp() + { + if (!_snapshotted || _svc is null) + return; + Plugin.Config.ShowHonorificTitleInHeader = _prevToggle; + _svc.TestOnly_SetState(_prevAvailable, _prevVersion, _prevTitle); + _snapshotted = false; + } } diff --git a/HellionChat/SelfTests/README.md b/HellionChat/SelfTests/README.md index e499afb..6f06e4f 100644 --- a/HellionChat/SelfTests/README.md +++ b/HellionChat/SelfTests/README.md @@ -37,9 +37,12 @@ step explicitly as smoke-only instead of faking a headless pass. ## Anti-pattern of record -`HonorificService.ShouldRenderSlot` had zero production callers and was green -only because the test called it directly — a test passing on a path the game -never runs. That is the failure this standard prevents. +`HonorificService.ShouldRenderSlot` once had zero production callers and was +green only because the test called it directly — a test passing on a path the +game never runs. v1.8.7 retired it: the gate is now wired into the real +`HonorificHeader.Draw` and asserted through it via +`HonorificHeader.LastTitleRendered` (see `HonorificHeaderRenderStep`). Kept here +as the canonical example of the failure this standard prevents. ## Step classification