using AcDream.Core.Chat; using AcDream.UI.Abstractions.Panels.Chat; namespace AcDream.UI.Abstractions.Tests.Panels.Chat; /// /// Phase J Tier 3: must reserve footer /// space for the separator + input field so the input stays anchored /// at the bottom across window resizes (the user reported the input /// disappearing when the window shrank). The pattern is the standard /// ImGui chat-window layout: a scrollable child filling /// (0, -footerHeight), then the separator + input below it. /// public sealed class ChatPanelLayoutTests { private sealed class NoBus : ICommandBus { public void Publish(T command) where T : notnull { /* no-op */ } } [Fact] public void Render_OrderIs_Begin_BeginChild_EndChild_Separator_InputTextSubmit_End() { var log = new ChatLog(); log.OnSystemMessage("seed", chatType: 0); var vm = new ChatVM(log); var panel = new ChatPanel(vm); var renderer = new FakePanelRenderer(); panel.Render(new PanelContext(0.016f, new NoBus()), renderer); var methods = renderer.Calls.Select(c => c.Method).ToList(); int beginIdx = methods.IndexOf("Begin"); int beginChildIdx = methods.IndexOf("BeginChild"); int endChildIdx = methods.IndexOf("EndChild"); // L.0 follow-up: Copy-mode toggle adds a Separator above the // chat tail, so multiple Separators now exist. The footer // separator (the one we care about for input layout) is the // LAST one — between EndChild and the input field. int separatorIdx = methods.LastIndexOf("Separator"); int inputSubmitIdx = methods.IndexOf("InputTextSubmit"); int endIdx = methods.IndexOf("End"); // All present Assert.True(beginIdx >= 0, "Begin missing"); Assert.True(beginChildIdx >= 0, "BeginChild missing"); Assert.True(endChildIdx >= 0, "EndChild missing"); Assert.True(separatorIdx >= 0, "Separator missing"); Assert.True(inputSubmitIdx >= 0, "InputTextSubmit missing"); Assert.True(endIdx >= 0, "End missing"); // Order: Begin < BeginChild < EndChild < Separator < InputTextSubmit < End Assert.True(beginIdx < beginChildIdx); Assert.True(beginChildIdx < endChildIdx); Assert.True(endChildIdx < separatorIdx); Assert.True(separatorIdx < inputSubmitIdx); Assert.True(inputSubmitIdx < endIdx); } [Fact] public void Render_BeginChild_ReservesNegativeFooterFromFrameHeight() { var log = new ChatLog(); var vm = new ChatVM(log); var panel = new ChatPanel(vm); var renderer = new FakePanelRenderer { FrameHeightWithSpacingValue = 24f }; panel.Render(new PanelContext(0.016f, new NoBus()), renderer); var beginChildCall = renderer.Calls.Single(c => c.Method == "BeginChild"); var size = (System.Numerics.Vector2)beginChildCall.Args[1]!; // Width 0 = fill available; height < 0 = "fill minus this". // Reserved height should equal FrameHeightWithSpacing + a small // separator pad (~6f) so the input never visually clips the // last chat line. Assert.Equal(0f, size.X); Assert.True(size.Y < 0, $"expected negative reserve, got {size.Y}"); Assert.True(size.Y <= -24f, $"expected at least -24f reserve, got {size.Y}"); } [Fact] public void Render_NewEntries_ScrollsToBottom() { // First render establishes the baseline (no auto-scroll because // _lastRenderedCount == lines.Count == 0). Then a second render // after a new entry should fire SetScrollHereY(1.0f). var log = new ChatLog(); var vm = new ChatVM(log); var panel = new ChatPanel(vm); var renderer = new FakePanelRenderer(); var ctx = new PanelContext(0.016f, new NoBus()); panel.Render(ctx, renderer); Assert.DoesNotContain(renderer.Calls, c => c.Method == "SetScrollHereY"); // Append a new entry, render again — auto-scroll should fire. log.OnLocalSpeech("Caith", "hello", senderGuid: 0xAA, isRanged: false); renderer.Calls.Clear(); panel.Render(ctx, renderer); var scrollCall = renderer.Calls.Single(c => c.Method == "SetScrollHereY"); Assert.Equal(1.0f, (float)scrollCall.Args[0]!); } [Fact] public void Render_NoNewEntries_DoesNotForceScroll() { var log = new ChatLog(); log.OnSystemMessage("seed", chatType: 0); var vm = new ChatVM(log); var panel = new ChatPanel(vm); var renderer = new FakePanelRenderer(); var ctx = new PanelContext(0.016f, new NoBus()); // First render establishes count baseline (1 entry). The first // render auto-scrolls because lines.Count (1) > _lastRenderedCount // (0). Subsequent renders without new entries should NOT scroll. panel.Render(ctx, renderer); renderer.Calls.Clear(); panel.Render(ctx, renderer); Assert.DoesNotContain(renderer.Calls, c => c.Method == "SetScrollHereY"); } }