feat(popout): arm the auto-tell pop-out settings and add the pool self-test
This commit is contained in:
@@ -388,6 +388,7 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
new SelfTests.OnOpenMainUiRoutesMainWindowStep(this),
|
||||
new SelfTests.TypingIpcStateStep(this),
|
||||
new SelfTests.ConfigMigrationV23Step(this),
|
||||
new SelfTests.ChannelPopoutBindStep(this),
|
||||
new SelfTests.HoverSheenAllocStep(this),
|
||||
new SelfTests.HonorificHeaderRenderStep(this),
|
||||
new SelfTests.AboutIntegrationsStatusStep(this),
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
using System.Linq;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Plugin.SelfTest;
|
||||
|
||||
namespace HellionChat.SelfTests;
|
||||
|
||||
// Exercises the ChannelPopoutPool lifecycle in-game (a behavioural step, not a
|
||||
// non-null-handle check — feedback_hellion_chat_fontmanager_push_trap). Verifies
|
||||
// pre-alloc == MaxParallelPopouts, unique slot ids, a TryOpen->IsOpen->TryClose
|
||||
// round-trip, idempotent TryClose, and capacity-exceeded refusal (warn, no throw).
|
||||
// The pool is a LIVE DI singleton, so a tester may already have real pop-outs open
|
||||
// when /xlperf runs; the step tests against the FREE slots (not full capacity) and
|
||||
// only ever closes ids it opened, so it neither false-REDs on a non-empty pool nor
|
||||
// disturbs real pop-outs. The pure slot-map math is pinned by PopoutSlotMapTests
|
||||
// (Build-Suite); this step proves the live wiring on top of it. Every slot reserved
|
||||
// is released before RunStep returns, so no pop-out is left bound.
|
||||
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() { }
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
|
||||
namespace HellionChat.Ui.Components.Settings.Tabs;
|
||||
|
||||
@@ -45,12 +44,11 @@ internal sealed class ChannelsTab
|
||||
() => Plugin.Config.AutoTellTabsShowGreetedToggle,
|
||||
v => Plugin.Config.AutoTellTabsShowGreetedToggle = v
|
||||
);
|
||||
// Popout is a v1.8.0 teaser — render disabled, do NOT persist.
|
||||
using (ImRaii.Disabled(true))
|
||||
{
|
||||
var openAsPopout = Plugin.Config.AutoTellTabsOpenAsPopout;
|
||||
ImGui.Checkbox("Open as popout (lands in v1.8.0)", ref openAsPopout);
|
||||
}
|
||||
DrawToggle(
|
||||
"Open as popout",
|
||||
() => Plugin.Config.AutoTellTabsOpenAsPopout,
|
||||
v => Plugin.Config.AutoTellTabsOpenAsPopout = v
|
||||
);
|
||||
}
|
||||
|
||||
if (ImGui.CollapsingHeader("Tell auto-open mode", ImGuiTreeNodeFlags.DefaultOpen))
|
||||
@@ -75,7 +73,7 @@ internal sealed class ChannelsTab
|
||||
|
||||
private void DrawTellAutoOpenModeCombo()
|
||||
{
|
||||
var labels = new[] { "Off", "Sidebar", "Top tab", "Popout (lands in v1.8.0)" };
|
||||
var labels = new[] { "Off", "Sidebar", "Top tab", "Popout" };
|
||||
var values = Enum.GetValues<TellAutoOpenMode>();
|
||||
var current = Plugin.Config.TellAutoOpenMode;
|
||||
var selected = 0;
|
||||
@@ -91,9 +89,7 @@ internal sealed class ChannelsTab
|
||||
ImGui.SetNextItemWidth(220);
|
||||
if (ImGui.Combo("Tell auto-open mode", ref selected, labels, labels.Length))
|
||||
{
|
||||
// Popout (index 3) is a v1.8.0 teaser — revert to previous value
|
||||
// and skip SaveConfig.
|
||||
if (selected >= 0 && selected < values.Length && selected != 3)
|
||||
if (selected >= 0 && selected < values.Length)
|
||||
{
|
||||
Plugin.Config.TellAutoOpenMode = values[selected];
|
||||
_plugin.SaveConfig();
|
||||
|
||||
Reference in New Issue
Block a user