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:
@@ -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() { }
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user