acdream/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/ChatPanelFocusTests.cs
Erik af74eac0c2 feat(input): #24 Phase K.2 - auto-enter player mode at login + MMB mouse-look + DebugPanel free-fly + Tab to chat-input focus
Five changes:

1. PlayerModeAutoEntry — testable guard class that fires once after
   EnterWorld + WorldSession.State.InWorld + player entity present +
   PlayerController.State == InWorld. GameWindow arms the entry
   after EnterWorld; per-frame Tick checks all four guards and
   invokes the same fly-to-player transition the Tab handler runs.
   User-initiated fly toggle (DebugPanel button) Cancel()s pending
   entry. Skip in offline mode (no ACDREAM_LIVE) — Holtburg orbit
   stays default for testing.

2. MouseLookState + KeyBindings.RetailDefaults() binds MMB Hold to
   InputAction.CameraInstantMouseLook. GameWindow subscribes:
   - Press: hide cursor, capture position, _mouseLookActive = true.
   - Release: restore cursor, deactivate.
   - WantCaptureMouse=true while held → suspend (release cursor).
   - MouseMove while active: combined drive — chase camera yaw +
     character heading move together (retail's signature mouse-look
     behavior). Camera Y still pitches camera-only.

3. DebugPanel "Toggle Free-Fly Mode" button via DebugVM.ToggleFlyMode
   action delegate — replaces the F-key as the primary discovery
   path for free-fly. Gated on DevToolsEnabled.

4. ChatPanel.FocusInput() one-shot + IPanelRenderer.SetKeyboardFocusHere
   primitive. GameWindow's ToggleChatEntry (Tab) subscriber calls
   _chatPanel.FocusInput() so Tab moves focus to the chat input
   field. Replaces the K.1c TODO stub.

5. WantCaptureMouse gating reinforcement on surviving mouse handlers
   (no new code; verified intact from K.1b).

21 new tests (8 PlayerModeAutoEntry, 10 MouseLookState, 3 ChatPanel
focus). 1183 total green. 0 warnings, 0 errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 09:20:17 +02:00

71 lines
3 KiB
C#

using AcDream.Core.Chat;
using AcDream.UI.Abstractions.Panels.Chat;
namespace AcDream.UI.Abstractions.Tests.Panels.Chat;
/// <summary>
/// Phase K.2 — Tab fires <see cref="AcDream.UI.Abstractions.Input.InputAction.ToggleChatEntry"/>,
/// which calls <see cref="ChatPanel.FocusInput"/>. The chat panel honors
/// the request on the very next <see cref="ChatPanel.Render"/> by emitting
/// a <c>SetKeyboardFocusHere</c> immediately before the input field. After
/// it fires once, subsequent renders without another <c>FocusInput</c>
/// call must not re-fire (one-shot semantics) — otherwise the chat field
/// would steal focus on every frame and the user could never click out.
/// </summary>
public sealed class ChatPanelFocusTests
{
private sealed class NullBus : AcDream.UI.Abstractions.ICommandBus
{
public void Publish<T>(T command) where T : notnull { }
}
[Fact]
public void FocusInput_NextRender_EmitsSetKeyboardFocusHereBeforeInput()
{
var panel = new ChatPanel(new ChatVM(new ChatLog()));
var renderer = new FakePanelRenderer();
panel.FocusInput();
panel.Render(new PanelContext(0.016f, new NullBus()), renderer);
// Find the SetKeyboardFocusHere call; it must come before the
// InputTextSubmit call so ImGui applies the focus to that widget.
int focusIdx = -1, inputIdx = -1;
for (int i = 0; i < renderer.Calls.Count; i++)
{
if (renderer.Calls[i].Method == "SetKeyboardFocusHere") focusIdx = i;
else if (renderer.Calls[i].Method == "InputTextSubmit") inputIdx = i;
}
Assert.True(focusIdx >= 0, "ChatPanel must call SetKeyboardFocusHere when FocusInput requested.");
Assert.True(inputIdx >= 0, "ChatPanel must still render the InputTextSubmit field.");
Assert.True(focusIdx < inputIdx, "SetKeyboardFocusHere must precede the InputTextSubmit it targets.");
}
[Fact]
public void Render_WithoutFocusInputCall_DoesNotEmitSetKeyboardFocusHere()
{
var panel = new ChatPanel(new ChatVM(new ChatLog()));
var renderer = new FakePanelRenderer();
panel.Render(new PanelContext(0.016f, new NullBus()), renderer);
Assert.DoesNotContain(renderer.Calls, c => c.Method == "SetKeyboardFocusHere");
}
[Fact]
public void FocusInput_OnlyAffectsTheNextRender_OneShot()
{
var panel = new ChatPanel(new ChatVM(new ChatLog()));
// Frame 1 — FocusInput requested → expect a SetKeyboardFocusHere.
var r1 = new FakePanelRenderer();
panel.FocusInput();
panel.Render(new PanelContext(0.016f, new NullBus()), r1);
Assert.Contains(r1.Calls, c => c.Method == "SetKeyboardFocusHere");
// Frame 2 — no further FocusInput call → must NOT re-fire.
var r2 = new FakePanelRenderer();
panel.Render(new PanelContext(0.016f, new NullBus()), r2);
Assert.DoesNotContain(r2.Calls, c => c.Method == "SetKeyboardFocusHere");
}
}