using OpenTheBox.Core; using OpenTheBox.Core.Characters; using OpenTheBox.Core.Enums; using OpenTheBox.Core.Items; using OpenTheBox.Localization; using OpenTheBox.Rendering; using OpenTheBox.Rendering.Panels; using Spectre.Console; using Spectre.Console.Rendering; namespace OpenTheBox.Tests; // ── Helpers ────────────────────────────────────────────────────────────── /// /// Renders an IRenderable to string using a local AnsiConsole. No shared state. /// static class RenderHelper { public static string RenderToString(IRenderable renderable) { var writer = new StringWriter(); var console = AnsiConsole.Create(new AnsiConsoleSettings { Out = new AnsiConsoleOutput(writer), Ansi = AnsiSupport.Yes, ColorSystem = ColorSystemSupport.TrueColor }); console.Write(renderable); return writer.ToString(); } } /// /// Captures AnsiConsole.Console and Console.Out output for renderer tests. /// MUST NOT be used in parallel — use [Collection("ConsoleTests")]. /// sealed class ConsoleCapture : IDisposable { private readonly IAnsiConsole _origAnsi; private readonly TextWriter _origOut; private readonly StringWriter _ansiWriter = new(); private readonly StringWriter _consoleWriter = new(); public ConsoleCapture() { _origAnsi = AnsiConsole.Console; _origOut = Console.Out; AnsiConsole.Console = AnsiConsole.Create(new AnsiConsoleSettings { Out = new AnsiConsoleOutput(_ansiWriter), Interactive = InteractionSupport.No, Ansi = AnsiSupport.Yes, ColorSystem = ColorSystemSupport.TrueColor }); Console.SetOut(_consoleWriter); } public string Output => _ansiWriter.ToString() + _consoleWriter.ToString(); public void Dispose() { AnsiConsole.Console = _origAnsi; Console.SetOut(_origOut); } } // ── Panel Tests ────────────────────────────────────────────────────────── public class PortraitPanelTests { [Fact] public void Render_DefaultAppearance_DoesNotThrow() { var result = RenderHelper.RenderToString(PortraitPanel.Render(new PlayerAppearance())); Assert.NotEmpty(result); } [Fact] public void Render_FullyEquipped_DoesNotThrow() { var appearance = new PlayerAppearance { HairStyle = HairStyle.Cyberpunk, HairTint = TintColor.Neon, EyeStyle = EyeStyle.CyberneticEyes, BodyStyle = BodyStyle.Armored, BodyTint = TintColor.Gold, LegStyle = LegStyle.RocketBoots, ArmStyle = ArmStyle.Wings }; var result = RenderHelper.RenderToString(PortraitPanel.Render(appearance)); Assert.NotEmpty(result); } [Theory] [MemberData(nameof(AllHairStyles))] public void Render_EachHairStyle_DoesNotThrow(HairStyle style) { var result = RenderHelper.RenderToString( PortraitPanel.Render(new PlayerAppearance { HairStyle = style })); Assert.NotEmpty(result); } [Theory] [MemberData(nameof(AllEyeStyles))] public void Render_EachEyeStyle_DoesNotThrow(EyeStyle style) { var result = RenderHelper.RenderToString( PortraitPanel.Render(new PlayerAppearance { EyeStyle = style })); Assert.NotEmpty(result); } [Theory] [MemberData(nameof(AllBodyStyles))] public void Render_EachBodyStyle_DoesNotThrow(BodyStyle style) { var result = RenderHelper.RenderToString( PortraitPanel.Render(new PlayerAppearance { BodyStyle = style })); Assert.NotEmpty(result); } [Theory] [MemberData(nameof(AllLegStyles))] public void Render_EachLegStyle_DoesNotThrow(LegStyle style) { var result = RenderHelper.RenderToString( PortraitPanel.Render(new PlayerAppearance { LegStyle = style })); Assert.NotEmpty(result); } [Theory] [MemberData(nameof(AllArmStyles))] public void Render_EachArmStyle_DoesNotThrow(ArmStyle style) { var result = RenderHelper.RenderToString( PortraitPanel.Render(new PlayerAppearance { ArmStyle = style })); Assert.NotEmpty(result); } [Theory] [MemberData(nameof(AllTintColors))] public void Render_EachHairTint_DoesNotThrow(TintColor tint) { var result = RenderHelper.RenderToString( PortraitPanel.Render(new PlayerAppearance { HairTint = tint })); Assert.NotEmpty(result); } [Theory] [MemberData(nameof(AllTintColors))] public void Render_EachBodyTint_DoesNotThrow(TintColor tint) { var result = RenderHelper.RenderToString( PortraitPanel.Render(new PlayerAppearance { BodyTint = tint })); Assert.NotEmpty(result); } public static IEnumerable AllHairStyles() => Enum.GetValues().Select(v => new object[] { v }); public static IEnumerable AllEyeStyles() => Enum.GetValues().Select(v => new object[] { v }); public static IEnumerable AllBodyStyles() => Enum.GetValues().Select(v => new object[] { v }); public static IEnumerable AllLegStyles() => Enum.GetValues().Select(v => new object[] { v }); public static IEnumerable AllArmStyles() => Enum.GetValues().Select(v => new object[] { v }); public static IEnumerable AllTintColors() => Enum.GetValues().Select(v => new object[] { v }); } public class ResourcePanelTests { [Fact] public void Render_Empty_DoesNotThrow() { var state = GameState.Create("Test", Locale.EN); var result = RenderHelper.RenderToString(ResourcePanel.Render(state)); Assert.NotEmpty(result); } [Theory] [MemberData(nameof(AllResourceTypes))] public void Render_SingleResource_DoesNotThrow(ResourceType type) { var state = GameState.Create("Test", Locale.EN); state.VisibleResources.Add(type); state.Resources[type] = new ResourceState(50, 100); var result = RenderHelper.RenderToString(ResourcePanel.Render(state)); Assert.NotEmpty(result); } [Fact] public void Render_AllResources_DoesNotThrow() { var state = GameState.Create("Test", Locale.EN); foreach (var rt in Enum.GetValues()) { state.VisibleResources.Add(rt); state.Resources[rt] = new ResourceState(30 + (int)rt * 5, 100); } var result = RenderHelper.RenderToString(ResourcePanel.Render(state)); Assert.NotEmpty(result); } [Fact] public void Render_ZeroCurrent_DoesNotThrow() { var state = GameState.Create("Test", Locale.EN); state.VisibleResources.Add(ResourceType.Gold); state.Resources[ResourceType.Gold] = new ResourceState(0, 100); var result = RenderHelper.RenderToString(ResourcePanel.Render(state)); Assert.NotEmpty(result); } [Fact] public void Render_ZeroMax_DoesNotThrow() { var state = GameState.Create("Test", Locale.EN); state.VisibleResources.Add(ResourceType.Ink); state.Resources[ResourceType.Ink] = new ResourceState(0, 0); var result = RenderHelper.RenderToString(ResourcePanel.Render(state)); Assert.NotEmpty(result); } [Fact] public void Render_FullResource_DoesNotThrow() { var state = GameState.Create("Test", Locale.EN); state.VisibleResources.Add(ResourceType.Gold); state.Resources[ResourceType.Gold] = new ResourceState(100, 100); var result = RenderHelper.RenderToString(ResourcePanel.Render(state)); Assert.NotEmpty(result); } [Fact] public void Render_VisibleButNotInDict_Skipped() { var state = GameState.Create("Test", Locale.EN); state.VisibleResources.Add(ResourceType.Ink); // Not adding to Resources dict → should skip without crash var result = RenderHelper.RenderToString(ResourcePanel.Render(state)); Assert.NotEmpty(result); } public static IEnumerable AllResourceTypes() => Enum.GetValues().Select(v => new object[] { v }); } public class StatsPanelTests { [Fact] public void Render_Empty_DoesNotThrow() { var state = GameState.Create("Test", Locale.EN); var result = RenderHelper.RenderToString(StatsPanel.Render(state)); Assert.NotEmpty(result); } [Theory] [MemberData(nameof(AllStatTypes))] public void Render_SingleStat_DoesNotThrow(StatType type) { var state = GameState.Create("Test", Locale.EN); state.VisibleStats.Add(type); state.Stats[type] = 42; var result = RenderHelper.RenderToString(StatsPanel.Render(state)); Assert.NotEmpty(result); } [Fact] public void Render_AllStats_DoesNotThrow() { var state = GameState.Create("Test", Locale.EN); foreach (var st in Enum.GetValues()) { state.VisibleStats.Add(st); state.Stats[st] = 10 + (int)st * 3; } state.TotalBoxesOpened = 999; var result = RenderHelper.RenderToString(StatsPanel.Render(state)); Assert.NotEmpty(result); } [Fact] public void Render_HighBoxCount_DoesNotThrow() { var state = GameState.Create("Test", Locale.EN); state.TotalBoxesOpened = 999_999; var result = RenderHelper.RenderToString(StatsPanel.Render(state)); Assert.NotEmpty(result); } public static IEnumerable AllStatTypes() => Enum.GetValues().Select(v => new object[] { v }); } public class InventoryPanelTests { [Fact] public void Render_Empty_DoesNotThrow() { var state = GameState.Create("Test", Locale.EN); var result = RenderHelper.RenderToString(InventoryPanel.Render(state)); Assert.NotEmpty(result); } [Fact] public void Render_SingleItem_DoesNotThrow() { var state = GameState.Create("Test", Locale.EN); state.AddItem(ItemInstance.Create("health_potion_small")); var result = RenderHelper.RenderToString(InventoryPanel.Render(state)); Assert.NotEmpty(result); } [Fact] public void Render_GroupedItems_DoesNotThrow() { var state = GameState.Create("Test", Locale.EN); state.AddItem(ItemInstance.Create("health_potion_small")); state.AddItem(ItemInstance.Create("health_potion_small")); state.AddItem(ItemInstance.Create("mana_crystal_small")); var result = RenderHelper.RenderToString(InventoryPanel.Render(state)); Assert.NotEmpty(result); } [Fact] public void Render_WithLocalization_DoesNotThrow() { var state = GameState.Create("Test", Locale.EN); state.AddItem(ItemInstance.Create("health_potion_small")); var loc = new LocalizationManager(Locale.EN); var result = RenderHelper.RenderToString(InventoryPanel.Render(state, loc: loc)); Assert.NotEmpty(result); } [Fact] public void Render_ManyItems_DoesNotThrow() { var state = GameState.Create("Test", Locale.EN); for (int i = 0; i < 50; i++) state.AddItem(ItemInstance.Create($"item_{i}")); var result = RenderHelper.RenderToString(InventoryPanel.Render(state)); Assert.NotEmpty(result); } } // ── RenderContext + RendererFactory Tests ──────────────────────────────── public class RenderContextTests { [Theory] [MemberData(nameof(AllUIFeatures))] public void Unlock_SetsFeature(UIFeature feature) { var ctx = new RenderContext(); Assert.False(ctx.Has(feature)); ctx.Unlock(feature); Assert.True(ctx.Has(feature)); } [Fact] public void CompletionPercent_DefaultsToZero() { var ctx = new RenderContext(); Assert.Equal(0, ctx.CompletionPercent); } [Fact] public void CompletionPercent_CanBeSet() { var ctx = new RenderContext(); ctx.CompletionPercent = 75; Assert.Equal(75, ctx.CompletionPercent); } [Fact] public void FromGameState_MirrorsUnlockedFeatures() { var state = GameState.Create("Test", Locale.EN); state.UnlockedUIFeatures.Add(UIFeature.TextColors); state.UnlockedUIFeatures.Add(UIFeature.ArrowKeySelection); var ctx = RenderContext.FromGameState(state); Assert.True(ctx.HasColors); Assert.True(ctx.HasArrowSelection); Assert.False(ctx.HasInventoryPanel); } [Fact] public void FromGameState_Empty_AllFalse() { var state = GameState.Create("Test", Locale.EN); var ctx = RenderContext.FromGameState(state); Assert.False(ctx.HasColors); Assert.False(ctx.HasExtendedColors); Assert.False(ctx.HasArrowSelection); Assert.False(ctx.HasInventoryPanel); Assert.False(ctx.HasResourcePanel); Assert.False(ctx.HasStatsPanel); Assert.False(ctx.HasPortraitPanel); Assert.False(ctx.HasFullLayout); Assert.False(ctx.HasKeyboardShortcuts); Assert.False(ctx.HasBoxAnimation); Assert.False(ctx.HasCraftingPanel); Assert.False(ctx.HasCompletionTracker); } public static IEnumerable AllUIFeatures() => Enum.GetValues().Select(v => new object[] { v }); } public class RendererFactoryTests { [Fact] public void Create_NoFeatures_ReturnsBasicRenderer() { var ctx = new RenderContext(); var loc = new LocalizationManager(Locale.EN); var renderer = RendererFactory.Create(ctx, loc); Assert.IsType(renderer); } [Theory] [InlineData(UIFeature.TextColors)] [InlineData(UIFeature.ExtendedColors)] [InlineData(UIFeature.ArrowKeySelection)] [InlineData(UIFeature.InventoryPanel)] [InlineData(UIFeature.ResourcePanel)] [InlineData(UIFeature.StatsPanel)] [InlineData(UIFeature.PortraitPanel)] [InlineData(UIFeature.FullLayout)] [InlineData(UIFeature.KeyboardShortcuts)] [InlineData(UIFeature.BoxAnimation)] [InlineData(UIFeature.CraftingPanel)] public void Create_WithSpectreFeature_ReturnsSpectreRenderer(UIFeature feature) { var ctx = new RenderContext(); ctx.Unlock(feature); var loc = new LocalizationManager(Locale.EN); var renderer = RendererFactory.Create(ctx, loc); Assert.IsType(renderer); } [Fact] public void Create_CompletionTrackerOnly_ReturnsBasicRenderer() { // CompletionTracker alone doesn't require Spectre features var ctx = new RenderContext(); ctx.Unlock(UIFeature.CompletionTracker); var loc = new LocalizationManager(Locale.EN); var renderer = RendererFactory.Create(ctx, loc); Assert.IsType(renderer); } } // ── SpectreRenderer Output Tests ──────────────────────────────────────── [Collection("ConsoleTests")] public class SpectreRendererOutputTests : IDisposable { private readonly ConsoleCapture _capture = new(); private readonly LocalizationManager _loc = new(Locale.EN); public void Dispose() => _capture.Dispose(); private SpectreRenderer CreateRenderer(params UIFeature[] features) { var ctx = new RenderContext(); foreach (var f in features) ctx.Unlock(f); return new SpectreRenderer(ctx, _loc); } // ── ShowMessage ── [Fact] public void ShowMessage_NoFeatures_DoesNotThrow() { var r = CreateRenderer(); r.ShowMessage("Hello world"); Assert.NotEmpty(_capture.Output); } [Fact] public void ShowMessage_HasColors_DoesNotThrow() { var r = CreateRenderer(UIFeature.TextColors); r.ShowMessage("Hello world"); Assert.NotEmpty(_capture.Output); } [Fact] public void ShowMessage_WithBrackets_DoesNotThrow() { var r = CreateRenderer(UIFeature.TextColors); r.ShowMessage("This has [red] brackets [/] in it"); Assert.NotEmpty(_capture.Output); } // ── ShowError ── [Fact] public void ShowError_NoFeatures_DoesNotThrow() { var r = CreateRenderer(); r.ShowError("Something went wrong"); Assert.NotEmpty(_capture.Output); } [Fact] public void ShowError_HasColors_DoesNotThrow() { var r = CreateRenderer(UIFeature.TextColors); r.ShowError("Something went wrong"); Assert.NotEmpty(_capture.Output); } // ── ShowBoxOpening (no animation) ── [Fact] public void ShowBoxOpening_NoFeatures_DoesNotThrow() { var r = CreateRenderer(); r.ShowBoxOpening("Test Box", "Common"); Assert.NotEmpty(_capture.Output); } [Fact] public void ShowBoxOpening_HasColors_DoesNotThrow() { var r = CreateRenderer(UIFeature.TextColors); r.ShowBoxOpening("Cool Box", "Rare"); Assert.NotEmpty(_capture.Output); } [Theory] [InlineData("Common")] [InlineData("Uncommon")] [InlineData("Rare")] [InlineData("Epic")] [InlineData("Legendary")] [InlineData("Mythic")] [InlineData("Unknown")] public void ShowBoxOpening_AllRarities_DoesNotThrow(string rarity) { var r = CreateRenderer(UIFeature.TextColors); r.ShowBoxOpening("Box", rarity); Assert.NotEmpty(_capture.Output); } // ── ShowLootReveal ── private static List<(string, string, string)> SampleLoot() => [ ("Health Potion", "Common", "Consumable"), ("Epic Sword", "Epic", "Equipment"), ("Box of Boxes", "Uncommon", "Box") ]; [Fact] public void ShowLootReveal_NoFeatures_DoesNotThrow() { var r = CreateRenderer(); r.ShowLootReveal(SampleLoot()); Assert.NotEmpty(_capture.Output); } [Fact] public void ShowLootReveal_HasColors_DoesNotThrow() { var r = CreateRenderer(UIFeature.TextColors); r.ShowLootReveal(SampleLoot()); Assert.NotEmpty(_capture.Output); } [Fact] public void ShowLootReveal_HasInventoryPanel_DoesNotThrow() { var r = CreateRenderer(UIFeature.TextColors, UIFeature.InventoryPanel); r.ShowLootReveal(SampleLoot()); Assert.NotEmpty(_capture.Output); } [Fact] public void ShowLootReveal_EmptyList_DoesNotThrow() { var r = CreateRenderer(UIFeature.TextColors, UIFeature.InventoryPanel); r.ShowLootReveal([]); // Empty is OK, no crash } [Fact] public void ShowLootReveal_ItemNameWithBrackets_DoesNotThrow() { var r = CreateRenderer(UIFeature.TextColors); r.ShowLootReveal([("[Special] Item", "Rare", "Box")]); Assert.NotEmpty(_capture.Output); } // ── ShowAdventureDialogue ── [Fact] public void ShowAdventureDialogue_WithCharacter_NoFeatures_DoesNotThrow() { var r = CreateRenderer(); r.ShowAdventureDialogue("Guard", "Halt!"); Assert.NotEmpty(_capture.Output); } [Fact] public void ShowAdventureDialogue_WithoutCharacter_NoFeatures_DoesNotThrow() { var r = CreateRenderer(); r.ShowAdventureDialogue(null, "The wind howls."); Assert.NotEmpty(_capture.Output); } [Fact] public void ShowAdventureDialogue_WithCharacter_HasColors_DoesNotThrow() { var r = CreateRenderer(UIFeature.TextColors); r.ShowAdventureDialogue("Dragon", "Prepare yourself!"); Assert.NotEmpty(_capture.Output); } [Fact] public void ShowAdventureDialogue_WithoutCharacter_HasColors_DoesNotThrow() { var r = CreateRenderer(UIFeature.TextColors); r.ShowAdventureDialogue(null, "A mysterious voice echoes."); Assert.NotEmpty(_capture.Output); } // ── ShowUIFeatureUnlocked ── [Fact] public void ShowUIFeatureUnlocked_NoFeatures_DoesNotThrow() { var r = CreateRenderer(); r.ShowUIFeatureUnlocked("Text Colors"); Assert.NotEmpty(_capture.Output); } [Fact] public void ShowUIFeatureUnlocked_HasColors_DoesNotThrow() { var r = CreateRenderer(UIFeature.TextColors); r.ShowUIFeatureUnlocked("Inventory Panel"); Assert.NotEmpty(_capture.Output); } // ── ShowInteraction ── [Fact] public void ShowInteraction_NoFeatures_DoesNotThrow() { var r = CreateRenderer(); r.ShowInteraction("You found something interesting."); Assert.NotEmpty(_capture.Output); } [Fact] public void ShowInteraction_HasColors_DoesNotThrow() { var r = CreateRenderer(UIFeature.TextColors); r.ShowInteraction("A secret passage opens."); Assert.NotEmpty(_capture.Output); } // ── ShowGameState ── [Fact] public void ShowGameState_NoFeatures_DoesNotThrow() { var state = GameState.Create("Test", Locale.EN); var ctx = new RenderContext(); var r = new SpectreRenderer(ctx, _loc); r.ShowGameState(state, ctx); // No panels → minimal or no output, but no crash } [Fact] public void ShowGameState_SequentialPanels_DoesNotThrow() { var state = CreateFullState(); var ctx = new RenderContext(); ctx.Unlock(UIFeature.TextColors); ctx.Unlock(UIFeature.PortraitPanel); ctx.Unlock(UIFeature.StatsPanel); ctx.Unlock(UIFeature.ResourcePanel); ctx.Unlock(UIFeature.InventoryPanel); ctx.Unlock(UIFeature.CompletionTracker); ctx.CompletionPercent = 42; var r = new SpectreRenderer(ctx, _loc); r.ShowGameState(state, ctx); Assert.NotEmpty(_capture.Output); } [Fact] public void ShowGameState_FullLayout_DoesNotThrow() { var state = CreateFullState(); var ctx = new RenderContext(); foreach (var f in Enum.GetValues()) ctx.Unlock(f); ctx.CompletionPercent = 88; var r = new SpectreRenderer(ctx, _loc); r.ShowGameState(state, ctx); Assert.NotEmpty(_capture.Output); } [Fact] public void ShowGameState_FullLayout_PartialPanels_DoesNotThrow() { var state = GameState.Create("Test", Locale.EN); var ctx = new RenderContext(); ctx.Unlock(UIFeature.TextColors); ctx.Unlock(UIFeature.FullLayout); // FullLayout unlocked but individual panels are NOT → shows placeholder "???" var r = new SpectreRenderer(ctx, _loc); r.ShowGameState(state, ctx); Assert.NotEmpty(_capture.Output); } // ── Clear ── [Fact] public void Clear_DoesNotThrow() { var r = CreateRenderer(UIFeature.TextColors); r.Clear(); // May throw IOException internally, should be caught } // ── Helpers ── private static GameState CreateFullState() { var state = GameState.Create("Test", Locale.EN); state.Appearance = new PlayerAppearance { HairStyle = HairStyle.Short, EyeStyle = EyeStyle.Blue, BodyStyle = BodyStyle.RegularTShirt, LegStyle = LegStyle.Short, ArmStyle = ArmStyle.Regular }; state.AddItem(ItemInstance.Create("gold_pouch")); state.AddItem(ItemInstance.Create("box_of_boxes")); state.VisibleResources.Add(ResourceType.Gold); state.Resources[ResourceType.Gold] = new ResourceState(75, 100); state.VisibleStats.Add(StatType.Strength); state.Stats[StatType.Strength] = 15; state.TotalBoxesOpened = 42; return state; } } // ── BasicRenderer Output Tests ────────────────────────────────────────── [Collection("ConsoleTests")] public class BasicRendererOutputTests : IDisposable { private readonly ConsoleCapture _capture = new(); private readonly LocalizationManager _loc = new(Locale.EN); private readonly BasicRenderer _renderer; public BasicRendererOutputTests() { _renderer = new BasicRenderer(_loc); } public void Dispose() => _capture.Dispose(); [Fact] public void ShowMessage_WritesToConsole() { _renderer.ShowMessage("Test message"); Assert.Contains("Test message", _capture.Output); } [Fact] public void ShowError_WritesWithPrefix() { _renderer.ShowError("Bad thing"); Assert.Contains("ERROR", _capture.Output); } [Fact] public void ShowBoxOpening_DoesNotThrow() { _renderer.ShowBoxOpening("Test Box", "Rare"); Assert.NotEmpty(_capture.Output); } [Fact] public void ShowLootReveal_WritesItems() { _renderer.ShowLootReveal([("Sword", "Epic", "Equipment")]); Assert.Contains("Sword", _capture.Output); } [Fact] public void ShowLootReveal_EmptyList_DoesNotThrow() { _renderer.ShowLootReveal([]); // No items → just the header line } [Fact] public void ShowGameState_WithCompletionTracker_ShowsPercent() { var state = GameState.Create("Test", Locale.EN); var ctx = new RenderContext(); ctx.Unlock(UIFeature.CompletionTracker); ctx.CompletionPercent = 55; _renderer.ShowGameState(state, ctx); Assert.Contains("55", _capture.Output); } [Fact] public void ShowGameState_WithoutCompletionTracker_ShowsBoxCount() { var state = GameState.Create("Test", Locale.EN); var ctx = new RenderContext(); _renderer.ShowGameState(state, ctx); // Early-game: shows box counter even without panels unlocked Assert.Contains("Boxes Opened", _capture.Output); } [Fact] public void ShowAdventureDialogue_WithCharacter_DoesNotThrow() { _renderer.ShowAdventureDialogue("NPC", "Hello!"); Assert.Contains("NPC", _capture.Output); } [Fact] public void ShowAdventureDialogue_WithoutCharacter_DoesNotThrow() { _renderer.ShowAdventureDialogue(null, "Narration text."); Assert.Contains("Narration", _capture.Output); } [Fact] public void ShowUIFeatureUnlocked_DoesNotThrow() { _renderer.ShowUIFeatureUnlocked("Text Colors"); Assert.Contains("Text Colors", _capture.Output); } [Fact] public void ShowInteraction_DoesNotThrow() { _renderer.ShowInteraction("Something happens"); Assert.Contains("Something happens", _capture.Output); } [Fact] public void Clear_DoesNotThrow() { _renderer.Clear(); } } // ── Input Method Tests ────────────────────────────────────────────────── [Collection("ConsoleTests")] public class RendererInputTests : IDisposable { private readonly ConsoleCapture _capture = new(); private readonly LocalizationManager _loc = new(Locale.EN); private readonly TextReader _origIn; public RendererInputTests() { _origIn = Console.In; } public void Dispose() { Console.SetIn(_origIn); _capture.Dispose(); } [Fact] public void BasicRenderer_ShowSelection_ReturnsCorrectIndex() { Console.SetIn(new StringReader("2\n")); var r = new BasicRenderer(_loc); int result = r.ShowSelection("Choose:", ["A", "B", "C"]); Assert.Equal(1, result); } [Fact] public void BasicRenderer_ShowTextInput_ReturnsInput() { Console.SetIn(new StringReader("Hello\n")); var r = new BasicRenderer(_loc); string result = r.ShowTextInput("Name"); Assert.Equal("Hello", result); } [Fact] public void BasicRenderer_ShowAdventureChoice_ReturnsIndex() { Console.SetIn(new StringReader("1\n")); var r = new BasicRenderer(_loc); int result = r.ShowAdventureChoice(["Fight", "Run"]); Assert.Equal(0, result); } [Fact] public void SpectreRenderer_ShowSelection_NoArrow_ReturnsIndex() { Console.SetIn(new StringReader("3\n")); var ctx = new RenderContext(); // No arrow selection → falls through to number input path var r = new SpectreRenderer(ctx, _loc); int result = r.ShowSelection("Pick:", ["X", "Y", "Z"]); Assert.Equal(2, result); } [Fact] public void SpectreRenderer_ShowSelection_HasColorsNoArrow_ReturnsIndex() { Console.SetIn(new StringReader("1\n")); var ctx = new RenderContext(); ctx.Unlock(UIFeature.TextColors); var r = new SpectreRenderer(ctx, _loc); int result = r.ShowSelection("Pick:", ["Only"]); Assert.Equal(0, result); } }