Files
HellionChat/HellionChat/SelfTests/CurrentTabCouplingStep.cs
T
JonKazama-Hellion 0ca8513065 fix(tell): couple CurrentTab to the active tab, retire LastTab
Plugin.CurrentTab now delegates to MainWindow.ActiveTab (fallback Tabs[0]) instead of the never-assigned LastTab index, so the game hooks, unread tracking, notification sounds, InputDisabled and Foray/Eureka paths all operate on the tab the user actually has selected. The dead LastTab/WantedTab fields and both WantedTab writes are removed.

A reference-based MainWindow.ResetActiveTabIfRemoved repairs the active-tab reference on eviction/logout (immune to the SaveConfig temp-tab strip window). The worker-thread eviction path marshals it onto the framework thread so the strip mutation serializes with Draw; logout is already framework-thread. The Draw-seed gains a lazy re-seed for a wholesale config swap. Adds CurrentTabCouplingStep (headless) and the interactive CurrentTabGuidedStep self-test (step count 30 -> 32).
2026-06-13 16:09:59 +02:00

122 lines
4.3 KiB
C#

using Dalamud.Bindings.ImGui;
using Dalamud.Plugin.SelfTest;
namespace HellionChat.SelfTests;
// F2: CurrentTab is coupled to MainWindow.ActiveTab (no longer the fixed index-0
// Tabs lookup). Asserts ReferenceEquals between the two, with false-green
// defenses: (1) empty-config exercises the getter's fallback; (2) null ActiveTab
// opens the window so the Draw-seed sets it and retries via Waiting (bounded so a
// never-drawn window cannot hang a batch); (3) a victim tab at index 0 makes a
// regressed index-0 getter return the victim (!= ActiveTab) and fail. Also checks
// the ResetActiveTabIfRemoved reference no-op branch.
internal sealed class CurrentTabCouplingStep : ISelfTestStep
{
private readonly Plugin _plugin;
private bool _forcedOpen;
private int _waitFrames;
public CurrentTabCouplingStep(Plugin plugin)
{
_plugin = plugin;
}
public string Name => "Hellion Chat - CurrentTab couples to active tab";
public SelfTestStepResult RunStep()
{
// Empty-config edge: actually exercise the getter's empty-fallback (it must
// return a fresh Tab, not null/throw) rather than an unconditional pass.
if (Plugin.Config.Tabs.Count == 0)
{
if (_plugin.CurrentTab is null)
{
ImGui.Text("Empty-config getter returned null instead of a fallback Tab.");
return SelfTestStepResult.Fail;
}
ImGui.Text("No tabs configured; getter returns the empty-fallback Tab.");
return SelfTestStepResult.Pass;
}
// /xlperf usually runs without the window drawn, so ActiveTab can be null
// on the first pass. Open the window so the Draw-seed sets it, retry next
// frame, and assert unconditionally once it is non-null. Bounded so a
// never-drawn window cannot hang a batch run.
if (_plugin.MainWindow.ActiveTab is null)
{
if (!_plugin.MainWindow.IsOpen)
{
_plugin.MainWindow.Toggle();
_forcedOpen = true;
}
if (++_waitFrames > 300)
{
RestoreWindow();
ImGui.Text(
"MainWindow never drew a seed within 300 frames; coupling not asserted."
);
return SelfTestStepResult.Pass;
}
ImGui.Text("Opening window so the draw-seed can set ActiveTab; retrying...");
return SelfTestStepResult.Waiting;
}
try
{
// Insert a victim at index 0: a regressed index-0 getter would return
// THIS instead of ActiveTab, so ReferenceEquals would catch it.
var victim = new Tab { Name = "selftest-coupling-victim" };
Plugin.Config.Tabs.Insert(0, victim);
try
{
if (!ReferenceEquals(_plugin.CurrentTab, _plugin.MainWindow.ActiveTab))
{
ImGui.Text("CurrentTab is not the same reference as ActiveTab");
return SelfTestStepResult.Fail;
}
if (ReferenceEquals(_plugin.CurrentTab, victim))
{
ImGui.Text("CurrentTab returned the index-0 victim (getter still index-based)");
return SelfTestStepResult.Fail;
}
// Reference no-op: resetting against a tab that is NOT the active
// one must leave the active reference untouched.
var activeBefore = _plugin.MainWindow.ActiveTab;
_plugin.MainWindow.ResetActiveTabIfRemoved(victim);
if (!ReferenceEquals(_plugin.MainWindow.ActiveTab, activeBefore))
{
ImGui.Text("ResetActiveTabIfRemoved changed the active tab on a non-match");
return SelfTestStepResult.Fail;
}
return SelfTestStepResult.Pass;
}
finally
{
Plugin.Config.Tabs.Remove(victim);
}
}
finally
{
RestoreWindow();
}
}
private void RestoreWindow()
{
if (_forcedOpen && _plugin.MainWindow.IsOpen)
_plugin.MainWindow.Toggle();
_forcedOpen = false;
}
public void CleanUp()
{
RestoreWindow();
_waitFrames = 0;
}
}