acdream/tests/AcDream.App.Tests/UI/UiMenuTests.cs
Erik 955f7a69a8 feat(D.2b): UiMenu (Type 6) — generic dropdown; channel knowledge moves to controller (widget-generalization Task 4)
UiChannelMenu → UiMenu: removed ChatChannelKind, the 14-item array, the
button-text map, and the availability default. Generic surface: MenuItem
(label + object? Payload), Selected (object?), OnSelect, EnabledProvider,
ButtonLabelProvider, RowsPerColumn/RowHeight/ColumnWidth (all settable).
All draw/event mechanics unchanged — same popup geometry, same click
coordinates, same 8-piece bevel, same 3-slice button face.

ChatWindowController gains ChannelItems[], ChannelButtonLabel(), and
ChannelAvailable() (verbatim from old widget), and populates the
factory-built Type-6 UiMenu via find-by-id rather than constructing a
replacement widget. The Menu property type is now UiMenu. OnChannelChanged
wrap replaced with the generic OnSelect wrap for the ReflowInputRow hook.

DatWidgetFactory registers Type 6 → new UiMenu().

Tests: UiChannelMenuTests → UiMenuTests (10 tests, all green); factory
Type6 test added; ChatWindowControllerTests updated to use OnSelect.
Divergence register: AP-42 added (flat item model vs retail nested-submenu
MakePopup @0x46d310 — latent, unreachable through the chat menu).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 17:18:27 +02:00

170 lines
6.6 KiB
C#

using System.Collections.Generic;
using System.Linq;
using AcDream.App.UI;
using AcDream.UI.Abstractions;
namespace AcDream.App.Tests.UI;
public class UiMenuTests
{
// PopupH = RowsPerColumn(7) * RowHeight(17) = 119; popup opens upward so top = -119.
// Item idx -> col = idx/7, row = idx%7; row band y in [top+row*17, top+(row+1)*17).
// Right column needs lx >= ColumnWidth(191) + Border(5) = lx >= 196 after bevel offset,
// but the original tests used lx=200 which maps ix=195 -> col=(int)(195/191)=1. OK.
// The 14 channel items verbatim (matches ChannelItems in ChatWindowController).
private static readonly UiMenu.MenuItem[] ChannelItems =
{
new("Squelch (ignore)", (object?)null),
new("Tell to Selected", (object?)null),
new("Chat to All", (object?)ChatChannelKind.Say),
new("Tell to Fellows", (object?)ChatChannelKind.Fellowship),
new("Tell to General Chat", (object?)ChatChannelKind.General),
new("Tell to LFG Chat", (object?)ChatChannelKind.Lfg),
new("Tell to Society Chat", (object?)ChatChannelKind.Society),
new("Tell to Monarch", (object?)ChatChannelKind.Monarch),
new("Tell to Patron", (object?)ChatChannelKind.Patron),
new("Tell to Vassals", (object?)ChatChannelKind.Vassals),
new("Tell to Allegiance", (object?)ChatChannelKind.Allegiance),
new("Tell to Trade Chat", (object?)ChatChannelKind.Trade),
new("Tell to Roleplay Chat", (object?)ChatChannelKind.Roleplay),
new("Tell to Olthoi Chat", (object?)ChatChannelKind.Olthoi),
};
// Availability gate identical to ChatWindowController.ChannelAvailable.
private static bool ChannelAvailable(object? p)
=> p is ChatChannelKind ch
? ch is ChatChannelKind.Say or ChatChannelKind.General
or ChatChannelKind.Trade or ChatChannelKind.Lfg
: false; // null-payload (Squelch/Tell-to-Selected) = inert
private UiMenu MakeMenu() => new UiMenu
{
Width = 80f, Height = 18f,
Items = ChannelItems,
Selected = (object?)ChatChannelKind.Say,
EnabledProvider = ChannelAvailable,
};
[Fact]
public void Items_HasExpected14Entries()
{
Assert.Equal(14, ChannelItems.Length);
}
[Fact]
public void Items_FirstEntry_IsSquelch_Special()
{
Assert.Equal("Squelch (ignore)", ChannelItems[0].Label);
Assert.Null(ChannelItems[0].Payload);
}
[Fact]
public void Items_LastEntry_IsOlthoi()
{
var last = ChannelItems[^1];
Assert.Equal("Tell to Olthoi Chat", last.Label);
Assert.Equal(ChatChannelKind.Olthoi, last.Payload);
}
[Fact]
public void Items_ContainAll12ChannelKinds()
{
var kinds = new HashSet<ChatChannelKind>(
ChannelItems.Where(i => i.Payload is ChatChannelKind).Select(i => (ChatChannelKind)i.Payload!));
foreach (var k in new[]
{
ChatChannelKind.Say, ChatChannelKind.General, ChatChannelKind.Trade, ChatChannelKind.Lfg,
ChatChannelKind.Fellowship, ChatChannelKind.Allegiance, ChatChannelKind.Patron,
ChatChannelKind.Vassals, ChatChannelKind.Monarch, ChatChannelKind.Roleplay,
ChatChannelKind.Society, ChatChannelKind.Olthoi,
})
Assert.Contains(k, kinds);
}
[Fact]
public void DefaultSelected_IsNull_OnBlankMenu()
{
// A freshly constructed UiMenu has no Selected by default (controller sets it).
Assert.Null(new UiMenu().Selected);
}
[Fact]
public void Select_AvailableLeftColumnItem_FiresOnSelect()
{
var menu = MakeMenu();
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 10, 5))); // open
object? fired = null;
menu.OnSelect = p => fired = p;
// "Chat to All" (Say) is index 2 = left col, row 2: y in [-85,-68). Say is available.
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 10, -76)));
Assert.Equal(ChatChannelKind.Say, fired);
Assert.Equal(ChatChannelKind.Say, menu.Selected);
}
[Fact]
public void Select_AvailableRightColumnItem_FiresOnSelect()
{
var menu = MakeMenu();
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 10, 5))); // open
object? fired = null;
menu.OnSelect = p => fired = p;
// "Tell to Trade Chat" (Trade) is index 11 = right col (lx>=191), row 4: y in [-51,-34).
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 200, -42)));
Assert.Equal(ChatChannelKind.Trade, fired);
Assert.Equal(ChatChannelKind.Trade, menu.Selected);
}
[Fact]
public void Select_SpecialItem_DoesNotFire()
{
var menu = MakeMenu();
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 10, 5))); // open
int fired = 0;
menu.OnSelect = _ => fired++;
// "Squelch (ignore)" is index 0 = left col, row 0 (null payload): y in [-119,-102).
// null payload → ChannelAvailable returns false → inert.
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 10, -110)));
Assert.Equal(0, fired);
}
[Fact]
public void Select_UnavailableChannel_DoesNotFire()
{
var menu = MakeMenu();
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 10, 5))); // open
int fired = 0;
menu.OnSelect = _ => fired++;
// "Tell to Fellows" (Fellowship) is index 3 = left col, row 3: y in [-68,-51).
// Fellowship is unavailable by the default static gate, so the click is inert.
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 10, -60)));
Assert.Equal(0, fired);
}
[Fact]
public void EnabledProvider_Overrides_DefaultGate()
{
// Override: all items enabled (even Fellowship which is normally greyed).
var menu = new UiMenu
{
Width = 80f, Height = 18f,
Items = ChannelItems,
Selected = (object?)ChatChannelKind.Say,
EnabledProvider = _ => true,
};
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 10, 5))); // open
object? fired = null;
menu.OnSelect = p => fired = p;
// With every item enabled, "Tell to Fellows" (idx 3, row 3) now fires.
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 10, -60)));
Assert.Equal(ChatChannelKind.Fellowship, fired);
}
}