- 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)
116 lines
4.3 KiB
C#
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() { }
|
|
}
|