openthebox/tests/OpenTheBox.Tests/RendererTests.cs
2026-03-18 19:04:06 +01:00

937 lines
29 KiB
C#

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 ──────────────────────────────────────────────────────────────
/// <summary>
/// Renders an IRenderable to string using a local AnsiConsole. No shared state.
/// </summary>
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();
}
}
/// <summary>
/// Captures AnsiConsole.Console and Console.Out output for renderer tests.
/// MUST NOT be used in parallel — use [Collection("ConsoleTests")].
/// </summary>
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<object[]> AllHairStyles() =>
Enum.GetValues<HairStyle>().Select(v => new object[] { v });
public static IEnumerable<object[]> AllEyeStyles() =>
Enum.GetValues<EyeStyle>().Select(v => new object[] { v });
public static IEnumerable<object[]> AllBodyStyles() =>
Enum.GetValues<BodyStyle>().Select(v => new object[] { v });
public static IEnumerable<object[]> AllLegStyles() =>
Enum.GetValues<LegStyle>().Select(v => new object[] { v });
public static IEnumerable<object[]> AllArmStyles() =>
Enum.GetValues<ArmStyle>().Select(v => new object[] { v });
public static IEnumerable<object[]> AllTintColors() =>
Enum.GetValues<TintColor>().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<ResourceType>())
{
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<object[]> AllResourceTypes() =>
Enum.GetValues<ResourceType>().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<StatType>())
{
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<object[]> AllStatTypes() =>
Enum.GetValues<StatType>().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<object[]> AllUIFeatures() =>
Enum.GetValues<UIFeature>().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<BasicRenderer>(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<SpectreRenderer>(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<BasicRenderer>(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<UIFeature>())
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);
}
}