refactor: extract chat-input pure helpers for unit-testable submit + history math
ChatBox.SendMessage reads bytes from ValidateMessage so Encoding.UTF8.GetBytes runs once per send. ValidateMessage takes an injectable sanitiser so xUnit can exercise the length-equality gate without ClientStructs game memory. CompactInputSubmitter and CompactInputHistoryNavigator lift the deterministic parts of ChatInputBar's pop-out submit and history-up/down callback into POCO helpers under HellionChat/_Helpers/. The ImGui buffer splice (DeleteChars/InsertChars) stays at the call site because it needs the live callback data. Behavior is identical to the previous inline implementation; tests in the local Build Suite repo pin the contracts.
This commit is contained in:
@@ -16,6 +16,18 @@ public unsafe class ChatBox
|
||||
}
|
||||
|
||||
public static void SendMessage(string message)
|
||||
{
|
||||
var bytes = ValidateMessage(message);
|
||||
SendMessageUnsafe(bytes);
|
||||
}
|
||||
|
||||
// Validation split out so the deterministic checks (UTF-8 length, sanitise
|
||||
// round-trip) can run in xUnit without ClientStructs game memory. The
|
||||
// sanitiser is injectable so tests can pin throw behaviour without invoking
|
||||
// Utf8String->SanitizeString, which only resolves in-process. Returns the
|
||||
// already-encoded bytes so SendMessage doesn't pay GetBytes twice.
|
||||
// TEST-MIRROR: ../../../Hellion Build test/GameFunctions/ChatBoxTests.cs
|
||||
internal static byte[] ValidateMessage(string message, Func<string, string>? sanitiserOverride = null)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(message);
|
||||
if (bytes.Length == 0)
|
||||
@@ -24,10 +36,11 @@ public unsafe class ChatBox
|
||||
if (bytes.Length > 500)
|
||||
throw new ArgumentException(Language.ChatBox_Error_Too_Long, nameof(message));
|
||||
|
||||
if (message.Length != SanitiseText(message).Length)
|
||||
var sanitiser = sanitiserOverride ?? SanitiseText;
|
||||
if (message.Length != sanitiser(message).Length)
|
||||
throw new ArgumentException(Language.ChatBox_Error_Invalid, nameof(message));
|
||||
|
||||
SendMessageUnsafe(bytes);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static string SanitiseText(string text)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using HellionChat._Helpers;
|
||||
using HellionChat.Code;
|
||||
using HellionChat.Util;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
@@ -89,60 +90,42 @@ public sealed class ChatInputBar
|
||||
}
|
||||
}
|
||||
|
||||
private void SubmitCompact(Tab tab)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_state.Buffer))
|
||||
return;
|
||||
// TEST-MIRROR: ../_Helpers/CompactInputSubmitter.cs
|
||||
private void SubmitCompact(Tab tab) =>
|
||||
CompactInputSubmitter.TrySubmit(_state, tab, _host.SendChatBoxFromExternal);
|
||||
|
||||
var text = _state.Buffer;
|
||||
_state.Buffer = string.Empty;
|
||||
_state.HistoryCursor = -1;
|
||||
_host.SendChatBoxFromExternal(tab, text);
|
||||
}
|
||||
|
||||
// History-navigation callback for the compact input. Mirrors the main
|
||||
// window's logic but operates on _state.HistoryCursor and the shared
|
||||
// InputHistoryService. Index semantics match v0.5.x InputBacklog:
|
||||
// 0 = oldest, Count-1 = newest.
|
||||
// History-navigation callback for the compact input. Cursor math is
|
||||
// delegated to CompactInputHistoryNavigator; only the ImGui buffer
|
||||
// splice stays here because it needs the live callback data.
|
||||
// TEST-MIRROR: ../_Helpers/CompactInputHistoryNavigator.cs
|
||||
private int CompactCallback(scoped ref ImGuiInputTextCallbackData data)
|
||||
{
|
||||
if (data.EventFlag != ImGuiInputTextFlags.CallbackHistory)
|
||||
return 0;
|
||||
|
||||
var prev = _state.HistoryCursor;
|
||||
switch (data.EventKey)
|
||||
var direction = data.EventKey switch
|
||||
{
|
||||
case ImGuiKey.UpArrow:
|
||||
switch (_state.HistoryCursor)
|
||||
{
|
||||
case -1:
|
||||
var offset = 0;
|
||||
if (!string.IsNullOrWhiteSpace(_state.Buffer))
|
||||
{
|
||||
InputHistoryService.Push(_state.Buffer);
|
||||
offset = 1;
|
||||
}
|
||||
_state.HistoryCursor = InputHistoryService.Count - 1 - offset;
|
||||
break;
|
||||
case > 0:
|
||||
_state.HistoryCursor--;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case ImGuiKey.DownArrow:
|
||||
if (_state.HistoryCursor != -1)
|
||||
if (++_state.HistoryCursor >= InputHistoryService.Count)
|
||||
_state.HistoryCursor = -1;
|
||||
break;
|
||||
}
|
||||
|
||||
if (prev == _state.HistoryCursor)
|
||||
ImGuiKey.UpArrow => CompactInputHistoryNavigator.Direction.Up,
|
||||
ImGuiKey.DownArrow => CompactInputHistoryNavigator.Direction.Down,
|
||||
_ => (CompactInputHistoryNavigator.Direction?)null,
|
||||
};
|
||||
if (direction is null)
|
||||
return 0;
|
||||
|
||||
var historyStr = InputHistoryService.GetByCursor(_state.HistoryCursor) ?? string.Empty;
|
||||
data.DeleteChars(0, data.BufTextLen);
|
||||
data.InsertChars(0, historyStr);
|
||||
var (cursor, replacement) = CompactInputHistoryNavigator.Navigate(
|
||||
direction.Value,
|
||||
_state.HistoryCursor,
|
||||
_state.Buffer,
|
||||
() => InputHistoryService.Count,
|
||||
InputHistoryService.Push,
|
||||
InputHistoryService.GetByCursor);
|
||||
|
||||
_state.HistoryCursor = cursor;
|
||||
if (replacement is null)
|
||||
return 0;
|
||||
|
||||
data.DeleteChars(0, data.BufTextLen);
|
||||
data.InsertChars(0, replacement);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
using System;
|
||||
|
||||
namespace HellionChat._Helpers;
|
||||
|
||||
// Pure-helper mirror of the compact pop-out history-navigation cursor
|
||||
// math. The original CompactCallback was tangled with ImGuiInputTextCallbackData
|
||||
// (DeleteChars/InsertChars), which can't be exercised in xUnit. The
|
||||
// ImGui buffer mutation stays at the call site; only the deterministic
|
||||
// cursor-and-replacement decision lives here.
|
||||
//
|
||||
// Index semantics match InputHistoryService:
|
||||
// index 0 = oldest entry
|
||||
// index Count - 1 = newest entry
|
||||
// cursor == -1 = "not browsing history"
|
||||
//
|
||||
// TEST-MIRROR: ../../../Hellion Build test/Ui/CompactInputHistoryNavigatorTests.cs
|
||||
public static class CompactInputHistoryNavigator
|
||||
{
|
||||
public enum Direction { Up, Down }
|
||||
|
||||
// replacement == null means: caller must NOT touch the buffer. This
|
||||
// distinguishes "cursor unchanged, leave the user's typing alone"
|
||||
// from "cursor moved to an empty slot, clear the buffer".
|
||||
public static (int cursor, string? replacement) Navigate(
|
||||
Direction direction,
|
||||
int currentCursor,
|
||||
string currentBuffer,
|
||||
Func<int> getCount,
|
||||
Action<string> push,
|
||||
Func<int, string?> getByCursor)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(getCount);
|
||||
ArgumentNullException.ThrowIfNull(push);
|
||||
ArgumentNullException.ThrowIfNull(getByCursor);
|
||||
|
||||
var prev = currentCursor;
|
||||
var next = currentCursor;
|
||||
|
||||
switch (direction)
|
||||
{
|
||||
case Direction.Up:
|
||||
if (currentCursor == -1)
|
||||
{
|
||||
// First Up press from a fresh buffer: stash whatever
|
||||
// the user typed so they can recover it after browsing.
|
||||
var offset = 0;
|
||||
if (!string.IsNullOrWhiteSpace(currentBuffer))
|
||||
{
|
||||
push(currentBuffer);
|
||||
offset = 1;
|
||||
}
|
||||
next = getCount() - 1 - offset;
|
||||
}
|
||||
else if (currentCursor > 0)
|
||||
{
|
||||
next--;
|
||||
}
|
||||
break;
|
||||
case Direction.Down:
|
||||
if (currentCursor != -1)
|
||||
{
|
||||
next++;
|
||||
if (next >= getCount())
|
||||
next = -1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (prev == next)
|
||||
return (next, null);
|
||||
|
||||
var replacement = getByCursor(next) ?? string.Empty;
|
||||
return (next, replacement);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using HellionChat.Ui;
|
||||
|
||||
namespace HellionChat._Helpers;
|
||||
|
||||
// Pure-helper mirror of the compact pop-out submit flow. ChatInputBar's
|
||||
// SubmitCompact used to inline this against a sealed ChatLogWindow, which
|
||||
// blocks Moq-based isolation. Lifting the deterministic part into a POCO
|
||||
// keeps the production call site a one-liner while letting xUnit assert
|
||||
// the buffer/cursor reset and the sender contract directly.
|
||||
// TEST-MIRROR: ../../../Hellion Build test/Ui/CompactInputSubmitterTests.cs
|
||||
public static class CompactInputSubmitter
|
||||
{
|
||||
public static bool TrySubmit(InputState state, Tab tab, Action<Tab, string> sender)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(state);
|
||||
ArgumentNullException.ThrowIfNull(tab);
|
||||
ArgumentNullException.ThrowIfNull(sender);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(state.Buffer))
|
||||
return false;
|
||||
|
||||
var text = state.Buffer;
|
||||
state.Buffer = string.Empty;
|
||||
state.HistoryCursor = -1;
|
||||
sender(tab, text);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user