using AcDream.App.Spells; using AcDream.App.UI; using AcDream.App.UI.Layout; using AcDream.Core.Items; using AcDream.Core.Spells; using System.Numerics; namespace AcDream.App.Tests.UI.Layout; public sealed class SpellbookWindowControllerTests { private static readonly SpellbookRowStyle RowStyle = new( Width: 280f, Height: 32f, IconLeft: 0f, IconTop: 0f, IconWidth: 32f, IconHeight: 32f, LabelLeft: 42f, LabelWidth: 230f, FontDid: 0x40000001u, LabelColor: Vector4.One, BackgroundSprite: 0x06001396u, SelectedSprite: 0x06001397u); [Fact] public void RetailFixture_BindsTextTabs_AndPropagatesOpenClosedState() { ImportedLayout layout = FixtureLoader.LoadSpellbook(); var closed = new List(); using SpellbookWindowController? controller = Bind( layout, new Spellbook(), close: () => closed.Add(1u)); Assert.NotNull(controller); UiText spellTab = Assert.IsType(layout.FindElement(SpellbookWindowController.SpellTabId)); UiText componentTab = Assert.IsType(layout.FindElement(SpellbookWindowController.ComponentTabId)); UiElement spellPage = Assert.IsAssignableFrom(layout.FindElement(SpellbookWindowController.SpellPageId)); UiElement componentPage = Assert.IsAssignableFrom(layout.FindElement(SpellbookWindowController.ComponentPageId)); Assert.Equal(RetailUiStateIds.Open, spellTab.ActiveRetailStateId); Assert.Equal(RetailUiStateIds.Closed, componentTab.ActiveRetailStateId); Assert.True(spellPage.Visible); Assert.False(componentPage.Visible); Assert.Equal(3, spellTab.Children.Count); Assert.All(spellTab.Children, child => Assert.Equal(RetailUiStateIds.Open, Assert.IsAssignableFrom(child).ActiveRetailStateId)); componentTab.OnEvent(new UiEvent(0, componentTab, UiEventType.Click)); Assert.Equal(SpellbookWindowPage.Components, controller!.CurrentPage); Assert.Equal(RetailUiStateIds.Closed, spellTab.ActiveRetailStateId); Assert.Equal(RetailUiStateIds.Open, componentTab.ActiveRetailStateId); Assert.False(spellPage.Visible); Assert.True(componentPage.Visible); Assert.All(componentTab.Children, child => Assert.Equal(RetailUiStateIds.Open, Assert.IsAssignableFrom(child).ActiveRetailStateId)); Assert.Empty(closed); } [Fact] public void RetailFixture_BindsFilterIndicatorChildren_AndDeleteCaptionChild() { ImportedLayout layout = LayoutImporter.Build( FixtureLoader.LoadSpellbookInfos(), _ => (0u, 0, 0), datFont: null, stringResolve: value => value.StringId == 164868556u ? "Delete" : "Filter"); UiButton creature = Assert.IsType(layout.FindElement(0x10000298u)); Assert.Equal((0f, 1f, 13f, 13f), (creature.FaceLeft, creature.FaceTop, creature.FaceWidth, creature.FaceHeight)); Assert.Equal(UiButton.LabelAlignment.Left, creature.LabelAlign); Assert.Equal(17f, creature.LabelOffsetX); Assert.Equal("Filter", creature.Label); Assert.True(creature.ToggleBehavior); creature.Selected = true; Assert.Equal("Highlight", creature.ActiveState); Assert.Equal(UiButtonStateMachine.Highlight, creature.ActiveRetailStateId); UiButton levelSix = Assert.IsType(layout.FindElement(0x100002A1u)); Assert.Equal(UiButton.LabelAlignment.Left, levelSix.LabelAlign); Assert.Equal(17f, levelSix.LabelOffsetX); UiButton delete = Assert.IsType( layout.FindElement(SpellbookWindowController.DeleteButtonId)); Assert.Equal("Delete", delete.Label); Assert.Equal(UiButton.LabelAlignment.Center, delete.LabelAlign); Assert.Equal((187f, 61f, 100f, 32f), (delete.Left, delete.Top, delete.FaceWidth, delete.FaceHeight)); } [Fact] public void LearnedSpells_AreAuthoredRows_InDisplayOrder_WithSelectionAndDragPayload() { ImportedLayout layout = FixtureLoader.LoadSpellbook(); Spellbook book = CreateSpellbook(); book.OnSpellLearned(101u); book.OnSpellLearned(102u); book.OnSpellLearned(103u); var added = new List(); using SpellbookWindowController controller = Bind( layout, book, addFavorite: added.Add)!; UiItemList list = Assert.IsType( layout.FindElement(SpellbookWindowController.SpellListId)); UiScrollbar scrollbar = Assert.IsType( layout.FindElement(SpellbookWindowController.SpellScrollbarId)); Assert.Equal(1, list.Columns); Assert.Equal(280f, list.CellWidth); Assert.Equal(32f, list.CellHeight); Assert.Same(list.Scroll, scrollbar.Model); Assert.Equal(3, list.GetNumUIItems()); UiCatalogSlot first = Assert.IsType(list.GetItem(0)); UiCatalogSlot second = Assert.IsType(list.GetItem(1)); UiCatalogSlot third = Assert.IsType(list.GetItem(2)); Assert.Equal([102u, 101u, 103u], [first.EntryId, second.EntryId, third.EntryId]); Assert.Equal("Creature One", first.Label); Assert.True(first.ShowLabel); Assert.Equal(RowStyle.BackgroundSprite, first.BackgroundSprite); Assert.Equal(RowStyle.SelectedSprite, first.SelectedSprite); Assert.True(first.SelectionBehindContent); Assert.Equal(RowStyle.LabelLeft, first.LabelLeft); second.OnEvent(new UiEvent(0, second, UiEventType.Click)); Assert.True(second.Selected); Assert.False(first.Selected); first.OnEvent(new UiEvent( 0, first, UiEventType.DragBegin, Payload: new SpellbookShortcutDragPayload(first.EntryId))); Assert.True(first.Selected); Assert.IsType(first.GetDragPayload()); first.OnEvent(new UiEvent(0, first, UiEventType.DoubleClick)); Assert.Equal([102u], added); } [Fact] public void SchoolAndLevelFilters_RebuildFromAuthoritativeBitfield() { ImportedLayout layout = FixtureLoader.LoadSpellbook(); Spellbook book = CreateSpellbook(); book.OnSpellLearned(101u); // War I book.OnSpellLearned(102u); // Creature I book.OnSpellLearned(103u); // War VIII var sent = new List(); using SpellbookWindowController controller = Bind( layout, book, sendFilter: sent.Add)!; UiItemList list = Assert.IsType( layout.FindElement(SpellbookWindowController.SpellListId)); UiButton war = Assert.IsType(layout.FindElement(0x1000029Bu)); UiButton levelOne = Assert.IsType(layout.FindElement(0x1000029Cu)); Assert.True(war.Selected); Assert.Equal(3, list.GetNumUIItems()); war.OnEvent(new UiEvent(0, war, UiEventType.Click)); controller.Tick(); Assert.Equal([0x3FF7u], sent); Assert.Single(VisibleSpellIds(list)); Assert.Equal(102u, VisibleSpellIds(list)[0]); war.OnEvent(new UiEvent(0, war, UiEventType.Click)); levelOne.OnEvent(new UiEvent(0, levelOne, UiEventType.Click)); controller.Tick(); Assert.Equal([103u], VisibleSpellIds(list)); Assert.False(levelOne.Selected); } [Fact] public void Delete_UsesSharedConfirmation_AndWaitsForServerRemovalNotice() { ImportedLayout layout = FixtureLoader.LoadSpellbook(); Spellbook book = CreateSpellbook(); book.OnSpellLearned(101u); var removed = new List(); string? prompt = null; Action? complete = null; using SpellbookWindowController controller = Bind( layout, book, removeSpell: removed.Add, showConfirmation: (message, callback) => { prompt = message; complete = callback; })!; UiItemList list = Assert.IsType( layout.FindElement(SpellbookWindowController.SpellListId)); UiButton delete = Assert.IsType( layout.FindElement(SpellbookWindowController.DeleteButtonId)); delete.OnEvent(new UiEvent(0, delete, UiEventType.Click)); Assert.Null(prompt); UiCatalogSlot row = Assert.IsType(list.GetItem(0)); row.OnEvent(new UiEvent(0, row, UiEventType.Click)); delete.OnEvent(new UiEvent(0, delete, UiEventType.Click)); Assert.Equal( "Are you sure you want to remove War One from your spellbook? " + "You will no longer be able to cast this spell unless you learn it again!", prompt); Assert.Empty(removed); complete!(false); Assert.Empty(removed); Assert.True(book.Knows(101u)); delete.OnEvent(new UiEvent(0, delete, UiEventType.Click)); complete!(true); Assert.Equal([101u], removed); Assert.True(book.Knows(101u)); Assert.Single(VisibleSpellIds(list)); book.OnSpellForgotten(101u); controller.Tick(); Assert.False(book.Knows(101u)); Assert.Empty(VisibleSpellIds(list)); } private static SpellbookWindowController? Bind( ImportedLayout layout, Spellbook spellbook, Action? addFavorite = null, Action? sendFilter = null, Action? removeSpell = null, Action>? showConfirmation = null, Action? close = null) => SpellbookWindowController.Bind( layout, spellbook, new ClientObjectTable(), () => 1u, new Dictionary(), spellId => spellId + 1000u, iconId => iconId, spellId => spellId == 103u ? 8 : 1, _ => { }, addFavorite ?? (_ => { }), sendFilter ?? (_ => { }), removeSpell ?? (_ => { }), showConfirmation ?? ((_, _) => { }), (_, _) => { }, close ?? (() => { }), RowStyle, rowFont: null); private static Spellbook CreateSpellbook() { const string csv = "Spell ID,Name,School,IconId [Hex],SortKey\n" + "101,War One,War Magic,0x06000001,2\n" + "102,Creature One,Creature Enchantment,0x06000002,1\n" + "103,War Eight,War Magic,0x06000003,3\n"; return new Spellbook(SpellTable.LoadFromReader(new StringReader(csv))); } private static uint[] VisibleSpellIds(UiItemList list) => Enumerable.Range(0, list.GetNumUIItems()) .Select(index => Assert.IsType(list.GetItem(index)).EntryId) .ToArray(); }