Implements Task G2: binds the imported chat LayoutDesc (0x21000006) to live behavior, the acdream analogue of retail ChatInterface + gmMainChatUI::PostInit. - UiDatElement: add OnClick hook + OnEvent override so Send/max-min buttons can be wired by a controller without needing a dedicated widget type. - ChatWindowController.Bind: reads transcript (0x10000011) and input (0x10000016) rects from the raw ElementInfo tree (factory skips them as Type-12/no-media), places UiChatView under the transcript panel and UiChatInput under the input bar; replaces the imported scrollbar track (0x10000012) with UiChatScrollbar driving UiChatView.Scroll; replaces the channel menu placeholder (0x10000014) with UiChannelMenu; wires Send button and max/min toggle via the new OnClick hook. ChatCommandRouter.Submit routes all input through the existing pipeline. - 6 smoke tests: Bind returns non-null, Transcript is child of panel, Input is child of bar, Input.OnSubmit publishes SendChatCmd, channel change updates submit channel, returns null when panels missing. Build: 0 errors. Test suite: 392 passed / 1 skipped / 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
209 lines
7.8 KiB
C#
209 lines
7.8 KiB
C#
using System.Collections.Generic;
|
|
using AcDream.App.UI;
|
|
using AcDream.App.UI.Layout;
|
|
using AcDream.Core.Chat;
|
|
using AcDream.UI.Abstractions;
|
|
using AcDream.UI.Abstractions.Panels.Chat;
|
|
|
|
namespace AcDream.App.Tests.UI.Layout;
|
|
|
|
/// <summary>
|
|
/// Smoke tests for <see cref="ChatWindowController.Bind"/> — no dats, no GL.
|
|
///
|
|
/// Building the Type-12 "skipped" elements via the pure <see cref="LayoutImporter"/>
|
|
/// path is the correct approach: we build a synthetic info tree that reflects the
|
|
/// real chat layout hierarchy (root → transcript panel + input bar as Type-3
|
|
/// containers, with Type-12 children for transcript + input, plus a Type-3 track
|
|
/// and menu), call <see cref="LayoutImporter.Build"/> to get the widget tree
|
|
/// (Type-12 children skipped, Type-3 parents created), then call
|
|
/// <see cref="ChatWindowController.Bind"/> which reads rects from the info tree
|
|
/// and places behavioral widgets under the parent containers.
|
|
/// </summary>
|
|
public class ChatWindowControllerTests
|
|
{
|
|
// ── Null-resolve helper (no GL needed) ─────────────────────────────────
|
|
private static (uint, int, int) NoTex(uint _) => (0u, 0, 0);
|
|
|
|
// ── Capture bus — records every Publish call ────────────────────────────
|
|
private sealed class CaptureBus : ICommandBus
|
|
{
|
|
public readonly List<object> Published = new();
|
|
public void Publish<T>(T cmd) where T : notnull => Published.Add(cmd!);
|
|
}
|
|
|
|
// ── Synthetic element tree matching the real chat layout topology ────────
|
|
|
|
/// <summary>
|
|
/// Build a minimal synthetic ElementInfo tree that mirrors the real chat
|
|
/// layout (0x21000006) with enough fidelity for Bind to succeed:
|
|
/// root (Type-3)
|
|
/// transcriptPanel (Type-3) [0x10000010]
|
|
/// transcript (Type-12, no media) [0x10000011] ← skipped by factory
|
|
/// track (Type-3) [0x10000012]
|
|
/// inputBar (Type-3) [0x10000013]
|
|
/// menu (Type-3) [0x10000014]
|
|
/// input (Type-12, no media) [0x10000016] ← skipped by factory
|
|
/// send (Type-3) [0x10000019]
|
|
/// maxmin (Type-3) [0x1000046F]
|
|
/// </summary>
|
|
private static (ElementInfo rootInfo, ImportedLayout layout, ChatVM vm) BuildTestTree()
|
|
{
|
|
var transcriptNode = new ElementInfo
|
|
{
|
|
Id = 0x10000011u, Type = 12, // Type-12, no media → skipped by factory
|
|
X = 16, Y = 0, Width = 458, Height = 74,
|
|
};
|
|
var trackNode = new ElementInfo
|
|
{
|
|
Id = 0x10000012u, Type = 3,
|
|
X = 474, Y = 6, Width = 16, Height = 68,
|
|
};
|
|
var transcriptPanel = new ElementInfo
|
|
{
|
|
Id = 0x10000010u, Type = 3, X = 0, Y = 9, Width = 490, Height = 74,
|
|
};
|
|
transcriptPanel.Children.Add(transcriptNode);
|
|
transcriptPanel.Children.Add(trackNode);
|
|
|
|
var menuNode = new ElementInfo
|
|
{
|
|
Id = 0x10000014u, Type = 3, X = 0, Y = 0, Width = 46, Height = 17,
|
|
};
|
|
var inputNode = new ElementInfo
|
|
{
|
|
Id = 0x10000016u, Type = 12, // Type-12, no media → skipped by factory
|
|
X = 46, Y = 0, Width = 398, Height = 17,
|
|
};
|
|
var sendNode = new ElementInfo
|
|
{
|
|
Id = 0x10000019u, Type = 3, X = 444, Y = 0, Width = 46, Height = 17,
|
|
};
|
|
var inputBar = new ElementInfo
|
|
{
|
|
Id = 0x10000013u, Type = 3, X = 0, Y = 83, Width = 490, Height = 17,
|
|
};
|
|
inputBar.Children.Add(menuNode);
|
|
inputBar.Children.Add(inputNode);
|
|
inputBar.Children.Add(sendNode);
|
|
|
|
var maxMinNode = new ElementInfo
|
|
{
|
|
Id = 0x1000046Fu, Type = 3, X = 474, Y = 0, Width = 16, Height = 16,
|
|
};
|
|
|
|
var root = new ElementInfo
|
|
{
|
|
Id = 0x1000000Eu, Type = 3, Width = 490, Height = 100,
|
|
};
|
|
root.Children.Add(transcriptPanel);
|
|
root.Children.Add(inputBar);
|
|
root.Children.Add(maxMinNode);
|
|
|
|
var layout = LayoutImporter.Build(root, NoTex, null);
|
|
var vm = new ChatVM(new ChatLog());
|
|
return (root, layout, vm);
|
|
}
|
|
|
|
// ── Test 1: Bind returns non-null with the minimal tree ──────────────────
|
|
|
|
[Fact]
|
|
public void Bind_Returns_NonNull_OnValidTree()
|
|
{
|
|
var (rootInfo, layout, vm) = BuildTestTree();
|
|
var bus = new CaptureBus();
|
|
|
|
var ctrl = ChatWindowController.Bind(rootInfo, layout, vm, bus, null, null, NoTex);
|
|
|
|
Assert.NotNull(ctrl);
|
|
}
|
|
|
|
// ── Test 2: Transcript is placed as a child of the transcript panel ──────
|
|
|
|
[Fact]
|
|
public void Bind_Transcript_IsChildOfTranscriptPanel()
|
|
{
|
|
var (rootInfo, layout, vm) = BuildTestTree();
|
|
var bus = new CaptureBus();
|
|
|
|
var ctrl = ChatWindowController.Bind(rootInfo, layout, vm, bus, null, null, NoTex);
|
|
|
|
Assert.NotNull(ctrl);
|
|
var panel = layout.FindElement(0x10000010u);
|
|
Assert.NotNull(panel);
|
|
// The transcript widget must be a child of the transcript panel.
|
|
Assert.Contains(ctrl!.Transcript, panel!.Children);
|
|
}
|
|
|
|
// ── Test 3: Input is placed as a child of the input bar ─────────────────
|
|
|
|
[Fact]
|
|
public void Bind_Input_IsChildOfInputBar()
|
|
{
|
|
var (rootInfo, layout, vm) = BuildTestTree();
|
|
var bus = new CaptureBus();
|
|
|
|
var ctrl = ChatWindowController.Bind(rootInfo, layout, vm, bus, null, null, NoTex);
|
|
|
|
Assert.NotNull(ctrl);
|
|
var bar = layout.FindElement(0x10000013u);
|
|
Assert.NotNull(bar);
|
|
Assert.Contains(ctrl!.Input, bar!.Children);
|
|
}
|
|
|
|
// ── Test 4: Input.OnSubmit publishes SendChatCmd via the capture bus ─────
|
|
|
|
[Fact]
|
|
public void Bind_InputSubmit_PublishesSendChatCmd()
|
|
{
|
|
var (rootInfo, layout, vm) = BuildTestTree();
|
|
var bus = new CaptureBus();
|
|
|
|
var ctrl = ChatWindowController.Bind(rootInfo, layout, vm, bus, null, null, NoTex);
|
|
|
|
Assert.NotNull(ctrl);
|
|
ctrl!.Input.OnSubmit!.Invoke("hello world");
|
|
|
|
// ChatCommandRouter.Submit should have published a SendChatCmd.
|
|
Assert.Single(bus.Published);
|
|
var cmd = Assert.IsType<SendChatCmd>(bus.Published[0]);
|
|
Assert.Equal("hello world", cmd.Text);
|
|
}
|
|
|
|
// ── Test 5: Channel change updates the channel used by subsequent submits ─
|
|
|
|
[Fact]
|
|
public void Bind_ChannelChange_UpdatesSubmitChannel()
|
|
{
|
|
var (rootInfo, layout, vm) = BuildTestTree();
|
|
var bus = new CaptureBus();
|
|
|
|
var ctrl = ChatWindowController.Bind(rootInfo, layout, vm, bus, null, null, NoTex);
|
|
|
|
Assert.NotNull(ctrl);
|
|
// Switch channel to General.
|
|
ctrl!.Menu.OnChannelChanged!.Invoke(ChatChannelKind.General);
|
|
ctrl.Input.OnSubmit!.Invoke("hey all");
|
|
|
|
Assert.Single(bus.Published);
|
|
var cmd = Assert.IsType<SendChatCmd>(bus.Published[0]);
|
|
Assert.Equal(ChatChannelKind.General, cmd.Channel);
|
|
}
|
|
|
|
// ── Test 6: Bind returns null when required elements are absent ──────────
|
|
|
|
[Fact]
|
|
public void Bind_Returns_Null_WhenTranscriptPanelMissing()
|
|
{
|
|
// Build a layout that is missing the transcript panel entirely.
|
|
var root = new ElementInfo { Id = 0x1000000Eu, Type = 3, Width = 490, Height = 100 };
|
|
// No children → TranscriptPanelId and InputBarId are absent from the widget tree.
|
|
|
|
var layout = LayoutImporter.Build(root, NoTex, null);
|
|
var vm = new ChatVM(new ChatLog());
|
|
var bus = new CaptureBus();
|
|
|
|
var ctrl = ChatWindowController.Bind(root, layout, vm, bus, null, null, NoTex);
|
|
|
|
Assert.Null(ctrl);
|
|
}
|
|
}
|