diff --git a/Tests/UnitTestsParallelizable/Views/SpinnerStyleTests.cs b/Tests/UnitTestsParallelizable/Views/SpinnerStyleTests.cs new file mode 100644 index 000000000..d230ff962 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Views/SpinnerStyleTests.cs @@ -0,0 +1,522 @@ +#nullable enable +namespace UnitTests_Parallelizable.ViewsTests; + +/// +/// Parallelizable tests for and its concrete implementations. +/// Tests the abstract properties and behavior of all spinner style variants. +/// +public class SpinnerStyleTests +{ + #region Abstract Properties Tests + + [Fact] + public void Custom_HasExpectedDefaults () + { + SpinnerStyle style = new SpinnerStyle.Custom (); + + Assert.False (style.HasSpecialCharacters); + Assert.Empty (style.Sequence); + Assert.False (style.SpinBounce); + Assert.Equal (80, style.SpinDelay); + } + + [Fact] + public void Dots_HasExpectedProperties () + { + SpinnerStyle style = new SpinnerStyle.Dots (); + + Assert.False (style.HasSpecialCharacters); + Assert.Equal (10, style.Sequence.Length); + Assert.False (style.SpinBounce); + Assert.Equal (80, style.SpinDelay); + Assert.Equal ("⠋", style.Sequence [0]); + } + + [Fact] + public void Line_HasExpectedProperties () + { + SpinnerStyle style = new SpinnerStyle.Line (); + + Assert.False (style.HasSpecialCharacters); + Assert.Equal (4, style.Sequence.Length); + Assert.False (style.SpinBounce); + Assert.Equal (130, style.SpinDelay); + Assert.Equal (["-", @"\", "|", "/"], style.Sequence); + } + + #endregion + + #region SpinBounce Tests + + [Theory] + [InlineData (typeof (SpinnerStyle.Dots4), true)] + [InlineData (typeof (SpinnerStyle.Dots6), true)] + [InlineData (typeof (SpinnerStyle.Dots7), true)] + [InlineData (typeof (SpinnerStyle.GrowVertical), true)] + [InlineData (typeof (SpinnerStyle.GrowHorizontal), true)] + [InlineData (typeof (SpinnerStyle.Balloon2), true)] + [InlineData (typeof (SpinnerStyle.Bounce), true)] + [InlineData (typeof (SpinnerStyle.BouncingBar), true)] + [InlineData (typeof (SpinnerStyle.BouncingBall), true)] + [InlineData (typeof (SpinnerStyle.Pong), true)] + [InlineData (typeof (SpinnerStyle.SoccerHeader), true)] + [InlineData (typeof (SpinnerStyle.Speaker), true)] + [InlineData (typeof (SpinnerStyle.Dots), false)] + [InlineData (typeof (SpinnerStyle.Line), false)] + [InlineData (typeof (SpinnerStyle.SimpleDots), false)] + public void SpinBounce_ReturnsExpectedValue (Type styleType, bool expectedBounce) + { + SpinnerStyle? style = Activator.CreateInstance (styleType) as SpinnerStyle; + + Assert.NotNull (style); + Assert.Equal (expectedBounce, style.SpinBounce); + } + + #endregion + + #region HasSpecialCharacters Tests + + [Theory] + [InlineData (typeof (SpinnerStyle.Arrow2), true)] + [InlineData (typeof (SpinnerStyle.Smiley), true)] + [InlineData (typeof (SpinnerStyle.Monkey), true)] + [InlineData (typeof (SpinnerStyle.Hearts), true)] + [InlineData (typeof (SpinnerStyle.Clock), true)] + [InlineData (typeof (SpinnerStyle.Earth), true)] + [InlineData (typeof (SpinnerStyle.Moon), true)] + [InlineData (typeof (SpinnerStyle.Runner), true)] + [InlineData (typeof (SpinnerStyle.Weather), true)] + [InlineData (typeof (SpinnerStyle.Christmas), true)] + [InlineData (typeof (SpinnerStyle.Grenade), true)] + [InlineData (typeof (SpinnerStyle.FingerDance), true)] + [InlineData (typeof (SpinnerStyle.FistBump), true)] + [InlineData (typeof (SpinnerStyle.SoccerHeader), true)] + [InlineData (typeof (SpinnerStyle.MindBlown), true)] + [InlineData (typeof (SpinnerStyle.Speaker), true)] + [InlineData (typeof (SpinnerStyle.OrangePulse), true)] + [InlineData (typeof (SpinnerStyle.BluePulse), true)] + [InlineData (typeof (SpinnerStyle.OrangeBluePulse), true)] + [InlineData (typeof (SpinnerStyle.TimeTravelClock), true)] + [InlineData (typeof (SpinnerStyle.Dots), false)] + [InlineData (typeof (SpinnerStyle.Line), false)] + [InlineData (typeof (SpinnerStyle.SimpleDots), false)] + [InlineData (typeof (SpinnerStyle.Star), false)] + [InlineData (typeof (SpinnerStyle.Arc), false)] + public void HasSpecialCharacters_ReturnsExpectedValue (Type styleType, bool expectedHasSpecial) + { + SpinnerStyle? style = Activator.CreateInstance (styleType) as SpinnerStyle; + + Assert.NotNull (style); + Assert.Equal (expectedHasSpecial, style.HasSpecialCharacters); + } + + #endregion + + #region Sequence Tests + + [Fact] + public void Sequence_AllStyles_ReturnsNonEmptyArray () + { + Type [] allStyles = typeof (SpinnerStyle) + .GetNestedTypes () + .Where (t => !t.IsAbstract && t.IsSubclassOf (typeof (SpinnerStyle))) + .ToArray (); + + foreach (Type styleType in allStyles) + { + if (styleType == typeof (SpinnerStyle.Custom)) + { + continue; // Custom has empty sequence by design + } + + SpinnerStyle? style = Activator.CreateInstance (styleType) as SpinnerStyle; + Assert.NotNull (style); + Assert.NotEmpty (style.Sequence); + } + } + + [Fact] + public void Sequence_AllStyles_ContainsOnlyNonNullStrings () + { + Type [] allStyles = typeof (SpinnerStyle) + .GetNestedTypes () + .Where (t => !t.IsAbstract && t.IsSubclassOf (typeof (SpinnerStyle))) + .ToArray (); + + foreach (Type styleType in allStyles) + { + SpinnerStyle? style = Activator.CreateInstance (styleType) as SpinnerStyle; + Assert.NotNull (style); + Assert.All (style.Sequence, frame => Assert.NotNull (frame)); + } + } + + [Theory] + [InlineData (typeof (SpinnerStyle.Dots), 10)] + [InlineData (typeof (SpinnerStyle.Dots2), 8)] + [InlineData (typeof (SpinnerStyle.Line), 4)] + [InlineData (typeof (SpinnerStyle.SimpleDots), 4)] + [InlineData (typeof (SpinnerStyle.Star), 6)] + [InlineData (typeof (SpinnerStyle.Toggle), 2)] + [InlineData (typeof (SpinnerStyle.Arrow), 8)] + [InlineData (typeof (SpinnerStyle.Smiley), 2)] + [InlineData (typeof (SpinnerStyle.Hearts), 5)] + [InlineData (typeof (SpinnerStyle.Clock), 12)] + [InlineData (typeof (SpinnerStyle.Earth), 3)] + public void Sequence_SpecificStyles_HasExpectedLength (Type styleType, int expectedLength) + { + SpinnerStyle? style = Activator.CreateInstance (styleType) as SpinnerStyle; + + Assert.NotNull (style); + Assert.Equal (expectedLength, style.Sequence.Length); + } + + [Fact] + public void Sequence_Dots8Bit_Has256Frames () + { + SpinnerStyle style = new SpinnerStyle.Dots8Bit (); + + Assert.Equal (256, style.Sequence.Length); + } + + [Fact] + public void Sequence_Material_HasProgressBarFrames () + { + SpinnerStyle style = new SpinnerStyle.Material (); + + Assert.NotEmpty (style.Sequence); + + // Material style uses both filled (█) and empty (▁) block characters + // to create a progress bar animation + Assert.All (style.Sequence, frame => + Assert.True ( + frame.Contains ("█") || frame.Contains ("▁"), + $"Frame should contain either filled (█) or empty (▁) blocks, but was: {frame}" + ) + ); + + // Verify that at least some frames contain the filled block + Assert.Contains (style.Sequence, frame => frame.Contains ("█")); + + // Verify that the sequence shows progression (some frames are all empty at the end) + Assert.Contains (style.Sequence, frame => frame == "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁"); + } + + #endregion + + #region SpinDelay Tests + + [Theory] + [InlineData (typeof (SpinnerStyle.Custom), 80)] + [InlineData (typeof (SpinnerStyle.Dots), 80)] + [InlineData (typeof (SpinnerStyle.Line), 130)] + [InlineData (typeof (SpinnerStyle.SimpleDots), 400)] + [InlineData (typeof (SpinnerStyle.Star), 70)] + [InlineData (typeof (SpinnerStyle.GrowVertical), 120)] + [InlineData (typeof (SpinnerStyle.Balloon), 140)] + [InlineData (typeof (SpinnerStyle.Triangle), 50)] + [InlineData (typeof (SpinnerStyle.Arc), 100)] + [InlineData (typeof (SpinnerStyle.Material), 17)] + public void SpinDelay_SpecificStyles_HasExpectedValue (Type styleType, int expectedDelay) + { + SpinnerStyle? style = Activator.CreateInstance (styleType) as SpinnerStyle; + + Assert.NotNull (style); + Assert.Equal (expectedDelay, style.SpinDelay); + } + + [Fact] + public void SpinDelay_AllStyles_IsPositive () + { + Type [] allStyles = typeof (SpinnerStyle) + .GetNestedTypes () + .Where (t => !t.IsAbstract && t.IsSubclassOf (typeof (SpinnerStyle))) + .ToArray (); + + foreach (Type styleType in allStyles) + { + SpinnerStyle? style = Activator.CreateInstance (styleType) as SpinnerStyle; + Assert.NotNull (style); + Assert.True (style.SpinDelay > 0, $"{styleType.Name} should have positive SpinDelay"); + } + } + + #endregion + + #region Specific Style Behavior Tests + + [Fact] + public void SimpleDots_SequenceStartsWithDot () + { + SpinnerStyle style = new SpinnerStyle.SimpleDots (); + + Assert.StartsWith (".", style.Sequence [0]); + } + + [Fact] + public void SimpleDotsScrolling_SequenceShowsProgression () + { + SpinnerStyle style = new SpinnerStyle.SimpleDotsScrolling (); + + Assert.Equal ([". ", ".. ", "...", " ..", " .", " "], style.Sequence); + } + + [Fact] + public void Arrow_SequenceContainsAllDirections () + { + SpinnerStyle style = new SpinnerStyle.Arrow (); + + Assert.Contains ("←", style.Sequence); + Assert.Contains ("↑", style.Sequence); + Assert.Contains ("→", style.Sequence); + Assert.Contains ("↓", style.Sequence); + } + + [Fact] + public void BouncingBar_SequenceContainsBrackets () + { + SpinnerStyle style = new SpinnerStyle.BouncingBar (); + + Assert.All (style.Sequence, frame => Assert.Contains ("[", frame)); + Assert.All (style.Sequence, frame => Assert.Contains ("]", frame)); + } + + [Fact] + public void BouncingBall_SequenceContainsParentheses () + { + SpinnerStyle style = new SpinnerStyle.BouncingBall (); + + Assert.All (style.Sequence, frame => Assert.Contains ("(", frame)); + Assert.All (style.Sequence, frame => Assert.Contains (")", frame)); + Assert.All (style.Sequence, frame => Assert.Contains ("●", frame)); + } + + [Fact] + public void Pong_SequenceContainsVerticalBars () + { + SpinnerStyle style = new SpinnerStyle.Pong (); + + Assert.All (style.Sequence, frame => Assert.StartsWith ("▐", frame)); + Assert.All (style.Sequence, frame => Assert.EndsWith ("▌", frame)); + } + + [Fact] + public void Clock_SequenceContains12Frames () + { + SpinnerStyle style = new SpinnerStyle.Clock (); + + Assert.Equal (12, style.Sequence.Length); + + // Verify it contains the 12 hour positions (on-the-hour clock faces) + Assert.Contains ("🕛 ", style.Sequence); // 12 o'clock + Assert.Contains ("🕐 ", style.Sequence); // 1 o'clock + Assert.Contains ("🕑 ", style.Sequence); // 2 o'clock + Assert.Contains ("🕒 ", style.Sequence); // 3 o'clock + Assert.Contains ("🕓 ", style.Sequence); // 4 o'clock + Assert.Contains ("🕔 ", style.Sequence); // 5 o'clock + Assert.Contains ("🕕 ", style.Sequence); // 6 o'clock + Assert.Contains ("🕖 ", style.Sequence); // 7 o'clock + Assert.Contains ("🕗 ", style.Sequence); // 8 o'clock + Assert.Contains ("🕘 ", style.Sequence); // 9 o'clock + Assert.Contains ("🕙 ", style.Sequence); // 10 o'clock + Assert.Contains ("🕚 ", style.Sequence); // 11 o'clock + } + + [Fact] + public void Earth_SequenceContainsGlobeEmojis () + { + SpinnerStyle style = new SpinnerStyle.Earth (); + + Assert.Contains ("🌍 ", style.Sequence); + Assert.Contains ("🌎 ", style.Sequence); + Assert.Contains ("🌏 ", style.Sequence); + } + + [Fact] + public void Weather_SequenceShowsWeatherProgression () + { + SpinnerStyle style = new SpinnerStyle.Weather (); + + Assert.Contains ("☀️ ", style.Sequence); + Assert.Contains ("⛅️ ", style.Sequence); + Assert.Contains ("🌧 ", style.Sequence); + Assert.Contains ("⛈ ", style.Sequence); + } + + [Fact] + public void Shark_SequenceShowsSharkAnimation () + { + SpinnerStyle style = new SpinnerStyle.Shark (); + + // Check that frames show movement + Assert.All (style.Sequence, frame => Assert.Contains ("|", frame)); + Assert.All (style.Sequence, frame => Assert.Contains ("_", frame)); + } + + [Fact] + public void Christmas_SequenceContainsTreeEmojis () + { + SpinnerStyle style = new SpinnerStyle.Christmas (); + + Assert.Contains ("🌲", style.Sequence); + Assert.Contains ("🎄", style.Sequence); + } + + [Fact] + public void MindBlown_SequenceShowsProgressiveExpression () + { + SpinnerStyle style = new SpinnerStyle.MindBlown (); + + Assert.Contains ("😐 ", style.Sequence); + Assert.Contains ("😮 ", style.Sequence); + Assert.Contains ("🤯 ", style.Sequence); + Assert.Contains ("💥 ", style.Sequence); + } + #endregion + + #region Toggle Style Tests + + [Theory] + [InlineData (typeof (SpinnerStyle.Toggle), 2)] + [InlineData (typeof (SpinnerStyle.Toggle2), 2)] + [InlineData (typeof (SpinnerStyle.Toggle3), 2)] + [InlineData (typeof (SpinnerStyle.Toggle4), 4)] + [InlineData (typeof (SpinnerStyle.Toggle5), 2)] + [InlineData (typeof (SpinnerStyle.Toggle6), 2)] + [InlineData (typeof (SpinnerStyle.Toggle7), 2)] + [InlineData (typeof (SpinnerStyle.Toggle8), 2)] + [InlineData (typeof (SpinnerStyle.Toggle9), 2)] + [InlineData (typeof (SpinnerStyle.Toggle10), 3)] + [InlineData (typeof (SpinnerStyle.Toggle11), 2)] + [InlineData (typeof (SpinnerStyle.Toggle12), 2)] + [InlineData (typeof (SpinnerStyle.Toggle13), 3)] + public void ToggleStyles_HaveExpectedFrameCount (Type styleType, int expectedFrames) + { + SpinnerStyle? style = Activator.CreateInstance (styleType) as SpinnerStyle; + + Assert.NotNull (style); + Assert.Equal (expectedFrames, style.Sequence.Length); + } + + #endregion + + #region Dots Style Variant Tests + + [Theory] + [InlineData (typeof (SpinnerStyle.Dots))] + [InlineData (typeof (SpinnerStyle.Dots2))] + [InlineData (typeof (SpinnerStyle.Dots3))] + [InlineData (typeof (SpinnerStyle.Dots4))] + [InlineData (typeof (SpinnerStyle.Dots5))] + [InlineData (typeof (SpinnerStyle.Dots6))] + [InlineData (typeof (SpinnerStyle.Dots7))] + [InlineData (typeof (SpinnerStyle.Dots8))] + [InlineData (typeof (SpinnerStyle.Dots9))] + [InlineData (typeof (SpinnerStyle.Dots10))] + [InlineData (typeof (SpinnerStyle.Dots11))] + [InlineData (typeof (SpinnerStyle.Dots12))] + public void DotsStyles_DoNotHaveSpecialCharacters (Type styleType) + { + SpinnerStyle? style = Activator.CreateInstance (styleType) as SpinnerStyle; + + Assert.NotNull (style); + Assert.False (style.HasSpecialCharacters); + } + + [Fact] + public void Dots8Bit_SequenceCoversAllBrailleCharacters () + { + SpinnerStyle style = new SpinnerStyle.Dots8Bit (); + + // Braille patterns from U+2800 to U+28FF (256 characters) + Assert.Equal (256, style.Sequence.Length); + + // Should start with blank braille + Assert.Equal ("⠀", style.Sequence [0]); + + // Should end with full braille + Assert.Equal ("⣿", style.Sequence [^1]); + } + + #endregion + + #region Pulse Style Tests + + [Fact] + public void OrangePulse_SequenceShowsPulseEffect () + { + SpinnerStyle style = new SpinnerStyle.OrangePulse (); + + Assert.Contains ("🔸 ", style.Sequence); + Assert.Contains ("🔶 ", style.Sequence); + Assert.Contains ("🟠 ", style.Sequence); + } + + [Fact] + public void BluePulse_SequenceShowsPulseEffect () + { + SpinnerStyle style = new SpinnerStyle.BluePulse (); + + Assert.Contains ("🔹 ", style.Sequence); + Assert.Contains ("🔷 ", style.Sequence); + Assert.Contains ("🔵 ", style.Sequence); + } + + [Fact] + public void OrangeBluePulse_CombinesBothPulses () + { + SpinnerStyle style = new SpinnerStyle.OrangeBluePulse (); + + // Should contain frames from both orange and blue pulses + Assert.Contains ("🔸 ", style.Sequence); + Assert.Contains ("🟠 ", style.Sequence); + Assert.Contains ("🔹 ", style.Sequence); + Assert.Contains ("🔵 ", style.Sequence); + } + + #endregion + + #region Edge Cases + + [Fact] + public void AllStyles_CanBeInstantiated () + { + Type [] allStyles = typeof (SpinnerStyle) + .GetNestedTypes () + .Where (t => !t.IsAbstract && t.IsSubclassOf (typeof (SpinnerStyle))) + .ToArray (); + + foreach (Type styleType in allStyles) + { + Exception? exception = Record.Exception (() => Activator.CreateInstance (styleType)); + + Assert.Null (exception); + } + } + + [Fact] + public void AllStyles_PropertiesAreImmutable () + { + Type [] allStyles = typeof (SpinnerStyle) + .GetNestedTypes () + .Where (t => !t.IsAbstract && t.IsSubclassOf (typeof (SpinnerStyle))) + .ToArray (); + + foreach (Type styleType in allStyles) + { + SpinnerStyle? style1 = Activator.CreateInstance (styleType) as SpinnerStyle; + SpinnerStyle? style2 = Activator.CreateInstance (styleType) as SpinnerStyle; + + Assert.NotNull (style1); + Assert.NotNull (style2); + + // Same type should have same property values + Assert.Equal (style1.HasSpecialCharacters, style2.HasSpecialCharacters); + Assert.Equal (style1.SpinBounce, style2.SpinBounce); + Assert.Equal (style1.SpinDelay, style2.SpinDelay); + Assert.Equal (style1.Sequence, style2.Sequence); + } + } + + #endregion +} \ No newline at end of file diff --git a/codecov.yml b/codecov.yml index 55daf4f6a..615664dbb 100644 --- a/codecov.yml +++ b/codecov.yml @@ -15,6 +15,9 @@ coverage: threshold: 1% # Allow 1% decrease without failing base: auto # Compare against base branch (v2_develop) if_ci_failed: error # Fail if CI fails + # Only measure Terminal.Gui library + paths: + - "Terminal.Gui/" # Per-patch (PR) coverage patch: @@ -23,50 +26,44 @@ coverage: threshold: 5% # Allow 5% variance base: auto if_ci_failed: error + # Only measure Terminal.Gui library + paths: + - "Terminal.Gui/" # Comment configuration for PRs comment: layout: "diff, files, footer" behavior: default # Update existing comment require_changes: false # Always comment - require_base: true # Require base coverage to compare - require_head: true # Require head coverage - branches: - - v2_develop - - v2_release + require_base: true # Require base coverage to compare + require_head: true # Require head coverage # Flag-based coverage grouping (matches your workflow flags) flags: unittests-nonparallel: carryforward: false # Don't carry forward if missing paths: - - Terminal.Gui/ - + - "Terminal.Gui/" # Only Terminal.Gui library unittests-parallel: carryforward: false paths: - - Terminal.Gui/ - + - "Terminal.Gui/" # Only Terminal.Gui library integrationtests: carryforward: false paths: - - Terminal.Gui/ + - "Terminal.Gui/" # Only Terminal.Gui library -# Files and paths to ignore +# Files and paths to ignore (belt-and-suspenders approach) ignore: - - "Examples/**/*" # Example projects - - "Tests/**/*" # Test projects themselves - - "UnitTests/**/*" # Legacy test location - - "docfx/**/*" # Documentation - - "Scripts/**/*" # PowerShell scripts + - "Examples/**" # Example projects (no trailing /*) + - "Tests/**" # Test projects themselves + - "UnitTests/**" # Legacy test location + - "docfx/**" # Documentation + - "Scripts/**" # PowerShell scripts - "**/*.Designer.cs" # Auto-generated files - - "**/obj/**/*" # Build artifacts - - "**/bin/**/*" # Build artifacts - -# # Path fixes (normalize Windows paths to Unix) -# fixes: -# - "C:/Users/runner/work/Terminal.Gui/Terminal.Gui/::" # Windows CI paths -# - "D:/a/Terminal.Gui/Terminal.Gui/::" # Alternative Windows paths + - "**/obj/**" # Build artifacts + - "**/bin/**" # Build artifacts + - "**/*.AssemblyInfo.cs" # Assembly info files # Only process coverage from these branches branches: