diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs index 123764f..293263e 100755 --- a/HellionChat/Plugin.cs +++ b/HellionChat/Plugin.cs @@ -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 diff --git a/HellionChat/SelfTests/ColorEditorBufferStep.cs b/HellionChat/SelfTests/ColorEditorBufferStep.cs new file mode 100644 index 0000000..f7cb8b0 --- /dev/null +++ b/HellionChat/SelfTests/ColorEditorBufferStep.cs @@ -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() { } +} diff --git a/HellionChat/SelfTests/ConfigMigrationV20Step.cs b/HellionChat/SelfTests/ConfigMigrationV20Step.cs new file mode 100644 index 0000000..d1f23d9 --- /dev/null +++ b/HellionChat/SelfTests/ConfigMigrationV20Step.cs @@ -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() { } +} diff --git a/HellionChat/SelfTests/HonorificHeaderRenderStep.cs b/HellionChat/SelfTests/HonorificHeaderRenderStep.cs new file mode 100644 index 0000000..3e57a4c --- /dev/null +++ b/HellionChat/SelfTests/HonorificHeaderRenderStep.cs @@ -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() { } +} diff --git a/HellionChat/SelfTests/HoverSheenAllocStep.cs b/HellionChat/SelfTests/HoverSheenAllocStep.cs new file mode 100644 index 0000000..12edffb --- /dev/null +++ b/HellionChat/SelfTests/HoverSheenAllocStep.cs @@ -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() { } +} diff --git a/HellionChat/SelfTests/PerformanceBaselineStep.cs b/HellionChat/SelfTests/PerformanceBaselineStep.cs new file mode 100644 index 0000000..3845b21 --- /dev/null +++ b/HellionChat/SelfTests/PerformanceBaselineStep.cs @@ -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() { } +} diff --git a/HellionChat/SelfTests/SidebarModeAutoSwitchStep.cs b/HellionChat/SelfTests/SidebarModeAutoSwitchStep.cs new file mode 100644 index 0000000..56f15bf --- /dev/null +++ b/HellionChat/SelfTests/SidebarModeAutoSwitchStep.cs @@ -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() { } +} diff --git a/HellionChat/Ui/Windows/MainWindow.cs b/HellionChat/Ui/Windows/MainWindow.cs index 33b6b19..0d16e9a 100644 --- a/HellionChat/Ui/Windows/MainWindow.cs +++ b/HellionChat/Ui/Windows/MainWindow.cs @@ -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()