Files
HellionChat/HellionChat/SelfTests/ChannelPopoutBindStep.cs
T
JonKazama-Hellion 6f71b09331 fix(closeout): address closure-review findings
- gate keybind pill-sync on IsChannelOrExistingLinkshell so an empty
  linkshell slot no longer desyncs the pill from the real send channel
- close manually-popped pop-out windows on logout via an IsOpen filter
  instead of the PopOut flag (which manual pops never set)
- read the router's tell-tab lookup through a lock-wrapped accessor so the
  framework thread cannot enumerate Config.Tabs mid worker-thread mutation
- add a "switch on every tell" toggle (default on) and make the auto-open
  mode pick the matching layout, so Sidebar vs Top-tab are distinct
- comment corrections (stale/contradictory text, TEST-MIRROR path depth)
2026-06-16 09:04:18 +02:00

116 lines
4.3 KiB
C#

using System.Linq;
using Dalamud.Bindings.ImGui;
using Dalamud.Plugin.SelfTest;
namespace HellionChat.SelfTests;
// In-game behavioural check of the ChannelPopoutPool lifecycle (not a non-null-handle
// check — feedback_hellion_chat_fontmanager_push_trap): pre-alloc count, unique slot
// ids, a TryOpen->IsOpen->TryClose round-trip, idempotent close, and capacity refusal.
// The pool is a live DI singleton, so the step works against the FREE slots (not full
// capacity) and only closes ids it opened — it neither false-REDs on a non-empty pool
// nor disturbs real pop-outs. Pure slot-map math is pinned by PopoutSlotMapTests.
internal sealed class ChannelPopoutBindStep : ISelfTestStep
{
private readonly Plugin _plugin;
public ChannelPopoutBindStep(Plugin plugin)
{
_plugin = plugin;
}
public string Name => "Hellion Chat - Channel popout pool lifecycle";
public SelfTestStepResult RunStep()
{
var pool = _plugin.ChannelPopoutPool;
var capacity = Plugin.Config.MaxParallelPopouts;
if (pool.Instances.Count != capacity)
{
ImGui.Text(
$"Expected {capacity} pre-allocated pop-out windows, found {pool.Instances.Count}."
);
return SelfTestStepResult.Fail;
}
if (pool.Instances.Select(w => w.SlotIndex).Distinct().Count() != pool.Instances.Count)
{
ImGui.Text("Pop-out windows do not have unique slot indices.");
return SelfTestStepResult.Fail;
}
// Free slots right now = capacity minus whatever real pop-outs are already
// bound. Testing against this (not capacity) keeps the step state-independent.
var free = capacity - pool.Instances.Count(w => w.Bound is not null);
// Round-trip on a throwaway tab, only when there's a slot to take. A bare Tab
// has CurrentChannel.Channel == Invalid + an empty SelectedChannels, so the
// pool's OnTabActivated strip is a no-op (no NRE), and the live active tab is
// passed only as `previous`, so it is never mutated. We close before
// returning, so the bound window never reaches a Draw frame.
if (free > 0)
{
var probe = new Tab { Name = "##selftest-popout-probe" };
if (pool.IsOpen(probe.Identifier))
{
ImGui.Text("Probe tab already open before TryOpen.");
return SelfTestStepResult.Fail;
}
if (!pool.TryOpen(probe))
{
ImGui.Text("TryOpen returned false with a free slot.");
return SelfTestStepResult.Fail;
}
if (!pool.IsOpen(probe.Identifier))
{
ImGui.Text("IsOpen is false right after a successful TryOpen.");
pool.TryClose(probe.Identifier);
return SelfTestStepResult.Fail;
}
pool.TryClose(probe.Identifier);
if (pool.IsOpen(probe.Identifier))
{
ImGui.Text("IsOpen is still true after TryClose.");
return SelfTestStepResult.Fail;
}
// Idempotent: closing an already-closed id is a silent no-op.
pool.TryClose(probe.Identifier);
}
// Capacity guard: fill the remaining free slots, then one more open must be
// refused (warn, no throw). Release everything we opened before reporting.
var fillers = Enumerable
.Range(0, free)
.Select(_ => new Tab { Name = "##selftest-fill" })
.ToList();
var opened = fillers.Count(pool.TryOpen);
var overflow = new Tab { Name = "##selftest-overflow" };
var overflowRejected = !pool.TryOpen(overflow);
foreach (var filler in fillers)
pool.TryClose(filler.Identifier);
pool.TryClose(overflow.Identifier);
if (opened != free)
{
ImGui.Text($"Filled only {opened}/{free} free slots before TryOpen refused.");
return SelfTestStepResult.Fail;
}
if (!overflowRejected)
{
ImGui.Text("Pool accepted an open beyond capacity instead of refusing.");
return SelfTestStepResult.Fail;
}
return SelfTestStepResult.Pass;
}
public void CleanUp() { }
}