Review caught a behavior divergence: the generic UiMenu auto-set its own Selected on any enabled pick, while the controller's EnabledProvider keeps the null-payload specials (Squelch / Tell-to-Selected) enabled/white like retail. So a special-item click set Selected=null and shifted the highlight onto the deferred placeholders — and the menu tests masked it by using a different (specials-disabled) gate than the controller ships. Fix: clean MVC contract mirroring retail UIElement_Menu::NewSelection — the widget REPORTS the pick via OnSelect; the controller OWNS Selected (it sets it only for talk-channel payloads). A special-item click now fires OnSelect(null), the controller ignores it, and the active channel + highlight stay put — observably identical to the pre-generalization widget, and extensible for when Squelch lands. Tests realigned to the controller's gate (specials white) and to the controller-owns-Selected contract. Full suite: 403 passed, 2 skipped, 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
178 lines
7.6 KiB
C#
178 lines
7.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's EnabledProvider: the null-payload
|
|
// specials (Squelch/Tell-to-Selected) are ENABLED/white like retail; only talk-CHANNEL
|
|
// items grey when unavailable. (The widget reports any enabled pick via OnSelect; the
|
|
// controller decides whether to update Selected, so specials are inert no-ops anyway.)
|
|
private static bool ChannelAvailable(object? p)
|
|
=> p is not ChatChannelKind ch
|
|
|| ch is ChatChannelKind.Say or ChatChannelKind.General
|
|
or ChatChannelKind.Trade or ChatChannelKind.Lfg;
|
|
|
|
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;
|
|
// Mirror the controller: the widget reports the pick, the controller sets Selected.
|
|
menu.OnSelect = p => { fired = p; if (p is ChatChannelKind) menu.Selected = 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;
|
|
// Mirror the controller: the widget reports the pick, the controller sets Selected.
|
|
menu.OnSelect = p => { fired = p; if (p is ChatChannelKind) menu.Selected = 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_FiresNull_LeavesSelectionUnchanged()
|
|
{
|
|
var menu = MakeMenu(); // Selected = Say
|
|
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 10, 5))); // open
|
|
|
|
// Mirror the controller: only channel payloads update Selected; the null-payload
|
|
// specials are deferred no-ops that leave the active channel + highlight unchanged.
|
|
bool fired = false; object? firedPayload = "sentinel";
|
|
menu.OnSelect = p => { fired = true; firedPayload = p; if (p is ChatChannelKind) menu.Selected = p; };
|
|
|
|
// "Squelch (ignore)" is index 0 = left col, row 0 (null payload), white/enabled.
|
|
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 10, -110)));
|
|
Assert.True(fired); // the pick IS reported...
|
|
Assert.Null(firedPayload); // ...with the special's null payload
|
|
Assert.Equal(ChatChannelKind.Say, menu.Selected); // ...but selection is unchanged (deferred no-op)
|
|
}
|
|
|
|
[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);
|
|
}
|
|
}
|