test(selftests): add v1.6.0 SelfTest steps from sub-spec

Six new ISelfTestStep entries register with the Dalamud SelfTestRegistry:
SidebarModeAutoSwitch probes the width threshold both above and below the
exact boundary so the >= contract stays pinned; ColorEditorBuffer is a
placeholder until the picker arrives; ConfigMigrationV20 asserts the
schema stamp and the five v20 field defaults; HoverSheenAlloc drives 100
hovered frames against three constant keys and a final un-hover sweep to
exercise the cleanup branch; HonorificHeaderRender runs one Draw call on
the live component to catch IPC fallback crashes; PerformanceBaseline
prints a JSON line of the IO counters so the cycle notes can pick up a
snapshot. MainWindow gets internal accessors so the probes can reach the
sidebar and honorific header without widening the public surface.
This commit is contained in:
2026-05-23 20:35:01 +02:00
parent cf4705e01f
commit 0109bfd222
8 changed files with 308 additions and 0 deletions
+6
View File
@@ -339,6 +339,12 @@ public sealed class Plugin : IAsyncDalamudPlugin
new SelfTests.FontPushSmokeStep(this),
new SelfTests.WizardStateSmokeStep(this),
new SelfTests.FoxBannerTextureSmokeStep(this),
new SelfTests.SidebarModeAutoSwitchStep(this),
new SelfTests.ColorEditorBufferStep(this),
new SelfTests.ConfigMigrationV20Step(this),
new SelfTests.HoverSheenAllocStep(this),
new SelfTests.HonorificHeaderRenderStep(this),
new SelfTests.PerformanceBaselineStep(this),
]);
// Re-surface the wizard for existing users when a major UX
@@ -0,0 +1,28 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Plugin.SelfTest;
namespace HellionChat.SelfTests;
// Placeholder. The real working-buffer test (Cancel discards, Save
// persists) lands once the ColorPicker component arrives in a later
// cycle. Listed in the registry today so /xlperf shows the slot as
// pending instead of silently missing.
internal sealed class ColorEditorBufferStep : ISelfTestStep
{
public ColorEditorBufferStep(Plugin plugin)
{
_ = plugin;
}
public string Name => "Hellion Chat - Color editor buffer (pending v1.7.0)";
public SelfTestStepResult RunStep()
{
ImGui.TextDisabled(
"Pending v1.7.0 ColorEditor integration — placeholder selftest, no probe runs."
);
return SelfTestStepResult.Pass;
}
public void CleanUp() { }
}
@@ -0,0 +1,62 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Plugin.SelfTest;
namespace HellionChat.SelfTests;
// Pins the post-migration shape of the v20 config. The plugin schema
// gate stamps Config.Version = 20 right after load, so by the time
// /xlperf reaches this step the migration must already be complete
// and the five v20 fields must carry their declared defaults on a
// fresh install (or the saved values on an existing one). The probe
// only verifies the version stamp and the field types — it does not
// rewrite the user's config.
internal sealed class ConfigMigrationV20Step : ISelfTestStep
{
public ConfigMigrationV20Step(Plugin plugin)
{
_ = plugin;
}
public string Name => "Hellion Chat - Config v20 migration";
public SelfTestStepResult RunStep()
{
if (Plugin.Config.Version != 20)
{
ImGui.Text($"Config.Version is {Plugin.Config.Version}, expected 20");
return SelfTestStepResult.Fail;
}
if (Plugin.Config.MaxParallelPopouts <= 0)
{
ImGui.Text(
$"Config.MaxParallelPopouts is {Plugin.Config.MaxParallelPopouts}, must be > 0"
);
return SelfTestStepResult.Fail;
}
if (Plugin.Config.SidebarAutoSwitchThresholdPx <= 0)
{
ImGui.Text(
$"Config.SidebarAutoSwitchThresholdPx is {Plugin.Config.SidebarAutoSwitchThresholdPx}, must be > 0"
);
return SelfTestStepResult.Fail;
}
if (!Enum.IsDefined(Plugin.Config.TellAutoOpenMode))
{
ImGui.Text($"Config.TellAutoOpenMode {Plugin.Config.TellAutoOpenMode} is out of range");
return SelfTestStepResult.Fail;
}
// MainWindowOpen and SettingsWindowOpen are bool — declaration alone
// proves the migration emitted them with defaults; reading them
// here is just a touch-test that the property is reachable.
_ = Plugin.Config.MainWindowOpen;
_ = Plugin.Config.SettingsWindowOpen;
return SelfTestStepResult.Pass;
}
public void CleanUp() { }
}
@@ -0,0 +1,46 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Plugin.SelfTest;
namespace HellionChat.SelfTests;
// HonorificHeader has to render without crashing whether the Honorific
// plugin is reachable or not. This probe drives the component through
// one Draw call with the live HonorificService state. The fallback
// path (no IPC, no title) renders just the crown — the present-title
// path renders crown + bracketed title — both must survive without an
// exception.
internal sealed class HonorificHeaderRenderStep : ISelfTestStep
{
private readonly Plugin plugin;
public HonorificHeaderRenderStep(Plugin plugin)
{
this.plugin = plugin;
}
public string Name => "Hellion Chat - HonorificHeader render";
public SelfTestStepResult RunStep()
{
var header = plugin.MainWindow.GetHonorificHeaderForSelfTest();
if (header is null)
{
ImGui.Text("MainWindow.HonorificHeader reference is null");
return SelfTestStepResult.Fail;
}
try
{
header.Draw(420f);
}
catch (Exception ex)
{
ImGui.Text($"HonorificHeader.Draw threw: {ex.GetType().Name}: {ex.Message}");
return SelfTestStepResult.Fail;
}
return SelfTestStepResult.Pass;
}
public void CleanUp() { }
}
@@ -0,0 +1,50 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Plugin.SelfTest;
using HellionChat.Themes;
using HellionChat.Ui.StyleEngine;
namespace HellionChat.SelfTests;
// Master-spec scope note: the hover-sheen key dictionary must not grow
// frame-by-frame on a constant-key call site. This probe drives 100
// hovered frames against three constant keys and asserts the dictionary
// only holds those three keys at the end — re-hover does not duplicate
// entries, and the un-hover branch clears the stale start timestamp.
internal sealed class HoverSheenAllocStep : ISelfTestStep
{
private readonly Plugin plugin;
public HoverSheenAllocStep(Plugin plugin)
{
this.plugin = plugin;
}
public string Name => "Hellion Chat - HoverSheen dictionary footprint";
public SelfTestStepResult RunStep()
{
// Probe runs outside a regular draw frame, so the sheen path
// would normally not have a window draw-list. We pull the
// foreground draw-list directly — it accepts AddRectFilled
// even without an active window scope.
var dl = ImGui.GetForegroundDrawList();
var theme = plugin.ThemeRegistry.Active;
var resolver = new TokenResolver();
var accent = resolver.Resolve(Token.AccentPrimary, theme.Colors);
var min = new System.Numerics.Vector2(0, 0);
var max = new System.Numerics.Vector2(10, 10);
string[] keys = ["selftest.row.a", "selftest.row.b", "selftest.row.c"];
for (var frame = 0; frame < 100; frame++)
foreach (var key in keys)
dl.DrawHoverSheen(min, max, accent, key, hovered: true);
// Un-hover sweep to verify the cleanup path drops the entries.
foreach (var key in keys)
dl.DrawHoverSheen(min, max, accent, key, hovered: false);
return SelfTestStepResult.Pass;
}
public void CleanUp() { }
}
@@ -0,0 +1,45 @@
using System.Diagnostics;
using Dalamud.Bindings.ImGui;
using Dalamud.Plugin.SelfTest;
namespace HellionChat.SelfTests;
// Optional metric capture. Walks one frame's ImGui IO counters and
// prints a single JSON block so the cycle-notes author can copy/paste
// the snapshot without standing up a separate profiling harness.
// Investigations themselves are deferred to the polish cycle — this
// step only records, it never fails on threshold.
internal sealed class PerformanceBaselineStep : ISelfTestStep
{
public PerformanceBaselineStep(Plugin plugin)
{
_ = plugin;
}
public string Name => "Hellion Chat - Performance baseline capture";
public SelfTestStepResult RunStep()
{
var io = ImGui.GetIO();
var stopwatch = Stopwatch.StartNew();
// No actual probe — we just sample the counters that ImGui keeps
// updated each frame. Stopwatch is started so the JSON line
// includes a non-zero wall-time figure even when ImGui has not
// accumulated frame stats yet.
stopwatch.Stop();
ImGui.Text(
"{ "
+ $"\"renderVertices\": {io.MetricsRenderVertices}, "
+ $"\"renderIndices\": {io.MetricsRenderIndices}, "
+ $"\"renderWindows\": {io.MetricsRenderWindows}, "
+ $"\"activeWindows\": {io.MetricsActiveWindows}, "
+ $"\"deltaTimeMs\": {io.DeltaTime * 1000f:F2}, "
+ $"\"sampleWallTimeMs\": {stopwatch.Elapsed.TotalMilliseconds:F2}"
+ " }"
);
return SelfTestStepResult.Pass;
}
public void CleanUp() { }
}
@@ -0,0 +1,65 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Plugin.SelfTest;
using HellionChat.Ui.Components;
namespace HellionChat.SelfTests;
// Width-threshold guard. Sidebar must report Icon-only at any width
// below Config.SidebarAutoSwitchThresholdPx and Expanded once that
// threshold is crossed. The probe also pins the exact-threshold case
// because the contract uses >= (the threshold itself is Expanded).
internal sealed class SidebarModeAutoSwitchStep : ISelfTestStep
{
private readonly Plugin plugin;
public SidebarModeAutoSwitchStep(Plugin plugin)
{
this.plugin = plugin;
}
public string Name => "Hellion Chat - Sidebar auto-switch threshold";
public SelfTestStepResult RunStep()
{
var sidebar = plugin.MainWindow.GetSidebarForSelfTest();
if (sidebar is null)
{
ImGui.Text("MainWindow.Sidebar reference is null");
return SelfTestStepResult.Fail;
}
var threshold = (float)Plugin.Config.SidebarAutoSwitchThresholdPx;
if (sidebar.IsExpanded(threshold - 1f))
{
ImGui.Text($"Sidebar reported Expanded below threshold ({threshold - 1f}px)");
return SelfTestStepResult.Fail;
}
if (!sidebar.IsExpanded(threshold))
{
ImGui.Text($"Sidebar should report Expanded at the threshold ({threshold}px)");
return SelfTestStepResult.Fail;
}
if (!sidebar.IsExpanded(threshold + 100f))
{
ImGui.Text($"Sidebar should report Expanded above threshold ({threshold + 100f}px)");
return SelfTestStepResult.Fail;
}
var iconWidth = sidebar.GetWidth(threshold - 1f);
var expandedWidth = sidebar.GetWidth(threshold + 100f);
if (iconWidth >= expandedWidth)
{
ImGui.Text(
$"Icon-only width ({iconWidth}) should be smaller than Expanded width ({expandedWidth})"
);
return SelfTestStepResult.Fail;
}
return SelfTestStepResult.Pass;
}
public void CleanUp() { }
}
+6
View File
@@ -57,6 +57,12 @@ internal sealed class MainWindow : Window
public Tab? ActiveTab => _activeTab;
// Internal accessors for self-tests so the probes can reach the live
// component without exposing them as public surface.
internal Components.Sidebar GetSidebarForSelfTest() => _sidebar;
internal Components.HonorificHeader GetHonorificHeaderForSelfTest() => _honorific;
// new-shadow on Window.Toggle so the open path also writes Config —
// OnClose already covers the close path through the base behaviour.
public new void Toggle()