From cb327b8073c8b38a0c6eafcbfc3eb1c175919005 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Fri, 8 May 2026 08:21:21 +0200 Subject: [PATCH] feat: add ThemeSwitchSelfTestStep + ISelfTestRegistry wiring Registers a single SelfTestStep that exercises Plugin.ThemeRegistry.Switch through the live theme list. Verified in-game via /xldev SelfTest tab on 2026-05-08; Plugin loads cleanly with the RegisterTestSteps call and the step runs the theme cycle as expected. Folder is HellionChat/SelfTest/ (singular). Future steps may rename to SelfTests/ to match the local Plan v4 convention. --- HellionChat/Plugin.cs | 7 ++ .../SelfTest/ThemeSwitchSelfTestStep.cs | 85 +++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 HellionChat/SelfTest/ThemeSwitchSelfTestStep.cs diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs index b4bd86f..91f258a 100755 --- a/HellionChat/Plugin.cs +++ b/HellionChat/Plugin.cs @@ -41,6 +41,7 @@ public sealed class Plugin : IDalamudPlugin [PluginService] public static IAddonLifecycle AddonLifecycle { get; private set; } = null!; [PluginService] public static IPlayerState PlayerState { get; private set; } = null!; [PluginService] public static ISeStringEvaluator Evaluator { get; private set; } = null!; + [PluginService] public static ISelfTestRegistry SelfTestRegistry { get; private set; } = null!; public static Configuration Config = null!; public static FileDialogManager FileDialogManager { get; private set; } = null!; @@ -452,6 +453,12 @@ public sealed class Plugin : IDalamudPlugin ThemeRegistry = new Themes.ThemeRegistry(customThemesDir); ThemeRegistry.Switch(Config.Theme); + // SelfTest hooks live alongside the live registry — the steps + // poll Active per frame and need the registry already wired. + SelfTestRegistry.RegisterTestSteps([ + new SelfTest.ThemeSwitchSelfTestStep(this), + ]); + // Plugin integrations register their IPC subscribers up-front so // Ready/Disposing events from the target plugins are caught from // the very first frame, even if the user's Honorific reloads diff --git a/HellionChat/SelfTest/ThemeSwitchSelfTestStep.cs b/HellionChat/SelfTest/ThemeSwitchSelfTestStep.cs new file mode 100644 index 0000000..49e4d33 --- /dev/null +++ b/HellionChat/SelfTest/ThemeSwitchSelfTestStep.cs @@ -0,0 +1,85 @@ +using Dalamud.Bindings.ImGui; +using Dalamud.Plugin.SelfTest; +using HellionChat.Themes; + +namespace HellionChat.SelfTest; + +// Validates the runtime theme-switch contract from the user side. The +// caller toggles the active theme via Settings -> Theme & Layout, the +// step polls ThemeRegistry.Active per frame and only passes once the +// slug has moved away from the initial value and back. The ABGR cache +// is sanity-checked on every frame: a freshly switched theme must carry +// a populated cache, otherwise Switch() forgot the recompute and the UI +// would still draw, just with all-transparent slots. +internal sealed class ThemeSwitchSelfTestStep : ISelfTestStep +{ + private readonly Plugin plugin; + private string? initialSlug; + private bool switchedAway; + + public ThemeSwitchSelfTestStep(Plugin plugin) + { + this.plugin = plugin; + } + + public string Name => "Hellion Chat - Theme switch"; + + public SelfTestStepResult RunStep() + { + var registry = this.plugin.ThemeRegistry; + if (registry is null) + return SelfTestStepResult.Fail; + + var active = registry.Active; + if (active is null) + return SelfTestStepResult.Fail; + + if (!HasPopulatedCache(active)) + return SelfTestStepResult.Fail; + + if (this.initialSlug is null) + { + this.initialSlug = active.Slug; + ImGui.Text($"Initial theme: \"{this.initialSlug}\". Open Settings -> Theme & Layout and pick a different theme."); + return SelfTestStepResult.Waiting; + } + + if (!this.switchedAway) + { + if (!string.Equals(active.Slug, this.initialSlug, StringComparison.OrdinalIgnoreCase)) + { + this.switchedAway = true; + return SelfTestStepResult.Waiting; + } + + ImGui.Text($"Switch the active theme away from \"{this.initialSlug}\"."); + return SelfTestStepResult.Waiting; + } + + if (!string.Equals(active.Slug, this.initialSlug, StringComparison.OrdinalIgnoreCase)) + { + ImGui.Text($"Switch back to \"{this.initialSlug}\" to finish the test."); + return SelfTestStepResult.Waiting; + } + + return SelfTestStepResult.Pass; + } + + public void CleanUp() + { + this.initialSlug = null; + this.switchedAway = false; + } + + // Any non-zero slot proves the cache was actually recomputed for the + // current theme. We don't compare against a reference, because custom + // themes can legitimately share slot values with a built-in. + private static bool HasPopulatedCache(Theme theme) + { + var cache = theme.AbgrCache; + return (cache.Primary + | cache.WindowBg + | cache.TextPrimary + | cache.Border) != 0u; + } +}