AddRuneTests.cs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. using System.Buffers;
  2. using System.Text;
  3. using UnitTests;
  4. using Xunit.Abstractions;
  5. namespace DriverTests;
  6. public class AddRuneTests (ITestOutputHelper output) : FakeDriverBase
  7. {
  8. [Fact]
  9. public void AddRune ()
  10. {
  11. IDriver driver = CreateFakeDriver ();
  12. driver.Rows = 25;
  13. driver.Cols = 80;
  14. driver.AddRune (new Rune ('a'));
  15. Assert.Equal ("a", driver.Contents? [0, 0].Grapheme);
  16. driver.Dispose ();
  17. }
  18. [Fact]
  19. public void AddRune_Accented_Letter_With_Three_Combining_Unicode_Chars ()
  20. {
  21. IDriver driver = CreateFakeDriver ();
  22. var expected = "ắ";
  23. var text = "\u1eaf";
  24. driver.AddStr (text);
  25. Assert.Equal (expected, driver.Contents! [0, 0].Grapheme);
  26. Assert.Equal (" ", driver.Contents [0, 1].Grapheme);
  27. driver.ClearContents ();
  28. driver.Move (0, 0);
  29. expected = "ắ";
  30. text = "\u0103\u0301";
  31. driver.AddStr (text);
  32. Assert.Equal (expected, driver.Contents [0, 0].Grapheme);
  33. Assert.Equal (" ", driver.Contents [0, 1].Grapheme);
  34. driver.ClearContents ();
  35. driver.Move (0, 0);
  36. expected = "ắ";
  37. text = "\u0061\u0306\u0301";
  38. driver.AddStr (text);
  39. Assert.Equal (expected, driver.Contents [0, 0].Grapheme);
  40. Assert.Equal (" ", driver.Contents [0, 1].Grapheme);
  41. driver.Dispose ();
  42. }
  43. [Fact]
  44. public void AddRune_InvalidLocation_DoesNothing ()
  45. {
  46. IDriver driver = CreateFakeDriver ();
  47. driver.Move (driver.Cols, driver.Rows);
  48. driver.AddRune ('a');
  49. for (var col = 0; col < driver.Cols; col++)
  50. {
  51. for (var row = 0; row < driver.Rows; row++)
  52. {
  53. Assert.Equal (" ", driver.Contents? [row, col].Grapheme);
  54. }
  55. }
  56. driver.Dispose ();
  57. }
  58. [Fact]
  59. public void AddRune_MovesToNextColumn ()
  60. {
  61. IDriver driver = CreateFakeDriver ();
  62. driver.AddRune ('a');
  63. Assert.Equal ("a", driver.Contents? [0, 0].Grapheme);
  64. Assert.Equal (0, driver.Row);
  65. Assert.Equal (1, driver.Col);
  66. driver.AddRune ('b');
  67. Assert.Equal ("b", driver.Contents? [0, 1].Grapheme);
  68. Assert.Equal (0, driver.Row);
  69. Assert.Equal (2, driver.Col);
  70. // Move to the last column of the first row
  71. int lastCol = driver.Cols - 1;
  72. driver.Move (lastCol, 0);
  73. Assert.Equal (0, driver.Row);
  74. Assert.Equal (lastCol, driver.Col);
  75. // Add a rune to the last column of the first row; should increment the row or col even though it's now invalid
  76. driver.AddRune ('c');
  77. Assert.Equal ("c", driver.Contents? [0, lastCol].Grapheme);
  78. Assert.Equal (lastCol + 1, driver.Col);
  79. // Add a rune; should succeed but do nothing as it's outside of Contents
  80. driver.AddRune ('d');
  81. Assert.Equal (lastCol + 2, driver.Col);
  82. for (var col = 0; col < driver.Cols; col++)
  83. {
  84. for (var row = 0; row < driver.Rows; row++)
  85. {
  86. Assert.NotEqual ("d", driver.Contents? [row, col].Grapheme);
  87. }
  88. }
  89. driver.Dispose ();
  90. }
  91. [Fact]
  92. public void AddRune_MovesToNextColumn_Wide ()
  93. {
  94. IDriver driver = CreateFakeDriver ();
  95. // 🍕 Slice of Pizza "\U0001F355"
  96. OperationStatus operationStatus = Rune.DecodeFromUtf16 ("\U0001F355", out Rune rune, out int charsConsumed);
  97. Assert.Equal (OperationStatus.Done, operationStatus);
  98. Assert.Equal (charsConsumed, rune.Utf16SequenceLength);
  99. Assert.Equal (2, rune.GetColumns ());
  100. driver.AddRune (rune);
  101. Assert.Equal (rune.ToString (), driver.Contents? [0, 0].Grapheme);
  102. Assert.Equal (0, driver.Row);
  103. Assert.Equal (2, driver.Col);
  104. driver.Dispose ();
  105. }
  106. [Fact]
  107. public void AddStr_Glyph_On_Second_Cell_Of_Wide_Glyph_Outputs_Correctly ()
  108. {
  109. IDriver? driver = CreateFakeDriver ();
  110. driver.SetScreenSize (6, 3);
  111. driver.GetOutputBuffer ().SetWideGlyphReplacement ((Rune)'①');
  112. driver.Clip = new (driver.Screen);
  113. driver.Move (1, 0);
  114. driver.AddStr ("┌");
  115. driver.Move (2, 0);
  116. driver.AddStr ("─");
  117. driver.Move (3, 0);
  118. driver.AddStr ("┐");
  119. driver.Clip.Exclude (new Region (new (1, 0, 3, 1)));
  120. driver.Move (0, 0);
  121. driver.AddStr ("🍎🍎🍎🍎");
  122. DriverAssert.AssertDriverContentsAre (
  123. """
  124. ①┌─┐🍎
  125. """,
  126. output,
  127. driver);
  128. driver.Refresh ();
  129. DriverAssert.AssertDriverOutputIs (@"\x1b[38;2;0;0;0m\x1b[48;2;0;0;0m①┌─┐🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m",
  130. output, driver);
  131. }
  132. [Fact]
  133. public void AddStr_WideGlyph_Second_Column_Attribute_Set_When_In_Clip ()
  134. {
  135. // This test verifies the fix for issue #4258
  136. // When a wide glyph is added and the second column is within the clip region,
  137. // the attribute for column N+1 should be set to match the current attribute.
  138. // See: OutputBufferImpl.cs line 194
  139. using IDriver driver = CreateFakeDriver ();
  140. driver.SetScreenSize (4, 2);
  141. // Set a specific attribute for the wide glyph
  142. Attribute wideGlyphAttr = new (Color.BrightRed, Color.BrightYellow);
  143. driver.CurrentAttribute = wideGlyphAttr;
  144. // Add a wide glyph at position (0, 0)
  145. driver.Move (0, 0);
  146. driver.AddStr ("🍎");
  147. // Verify the wide glyph is in column 0
  148. Assert.Equal ("🍎", driver.Contents! [0, 0].Grapheme);
  149. Assert.Equal (wideGlyphAttr, driver.Contents [0, 0].Attribute);
  150. // Verify column 1 (the second column of the wide glyph) has the correct attribute set
  151. // This is the fix: column N+1 should have CurrentAttribute set (line 194 in OutputBufferImpl.cs)
  152. Assert.Equal (wideGlyphAttr, driver.Contents [0, 1].Attribute);
  153. // Verify cursor moved to column 2
  154. Assert.Equal (2, driver.Col);
  155. }
  156. [Fact]
  157. public void AddStr_WideGlyph_Second_Column_Attribute_Not_Set_When_Outside_Clip ()
  158. {
  159. // This test verifies that when a wide glyph's second column is outside the clip,
  160. // the attribute for column N+1 is NOT modified
  161. using IDriver driver = CreateFakeDriver ();
  162. driver.SetScreenSize (4, 2);
  163. // Set initial attribute for the entire contents
  164. Attribute initialAttr = new (Color.White, Color.Black);
  165. driver.CurrentAttribute = initialAttr;
  166. driver.Move (0, 0);
  167. driver.AddStr (" ");
  168. driver.Move (0, 1);
  169. driver.AddStr (" ");
  170. // Create a clip that excludes column 1
  171. driver.Clip = new (new Rectangle (0, 0, 1, 2));
  172. // Set a different attribute for the wide glyph
  173. Attribute wideGlyphAttr = new (Color.BrightRed, Color.BrightYellow);
  174. driver.CurrentAttribute = wideGlyphAttr;
  175. // Try to add a wide glyph at position (0, 0)
  176. // Column 0 is in clip, but column 1 is NOT
  177. driver.Move (0, 0);
  178. driver.AddStr ("🍎");
  179. // Verify column 0 has the replacement character (can't fit wide glyph)
  180. Assert.NotEqual ("🍎", driver.Contents! [0, 0].Grapheme);
  181. // Verify column 1 still has the original attribute (NOT modified)
  182. Assert.Equal (initialAttr, driver.Contents [0, 1].Attribute);
  183. }
  184. [Fact]
  185. public void AddStr_WideGlyph_Second_Column_Attribute_Outputs_Correctly ()
  186. {
  187. // This test verifies the fix for issue #4258 by checking the actual driver output
  188. // This mimics what happens when TransparentShadow redraws a wide glyph from ScreenContents
  189. // WITHOUT line 194, column N+1's attribute doesn't get set, causing wrong colors in output
  190. // See: OutputBufferImpl.cs line ~196 (Contents [Row, Col].Attribute = CurrentAttribute;)
  191. using IDriver driver = CreateFakeDriver ();
  192. driver.SetScreenSize (3, 1);
  193. driver.Force16Colors = true;
  194. // Step 1: Draw initial content - a wide glyph at column 1 with white-on-black
  195. driver.CurrentAttribute = new Attribute (Color.White, Color.Black);
  196. driver.Move (1, 0);
  197. driver.AddStr ("🍎X"); // Wide glyph at columns 1-2, 'X' at column 3 doesn't exist (off-screen)
  198. // At this point:
  199. // - Column 0: space (default) with white-on-black
  200. // - Column 1: 🍎 with white-on-black
  201. // - Column 2: (part of 🍎) with white-on-black (from initial ClearContents)
  202. // Step 2: Now redraw the SAME wide glyph at column 1 but with a DIFFERENT attribute (red-on-yellow)
  203. // This simulates what transparent shadow does - it redraws what's underneath with a dimmed attribute
  204. driver.CurrentAttribute = new Attribute (Color.BrightRed, Color.BrightYellow);
  205. driver.Move (1, 0);
  206. driver.AddStr ("🍎");
  207. // Verify internal state
  208. Assert.Equal ("🍎", driver.Contents! [0, 1].Grapheme);
  209. Assert.Equal (new Attribute (Color.BrightRed, Color.BrightYellow), driver.Contents [0, 1].Attribute);
  210. // THIS is the critical assertion - column 2's attribute MUST be red-on-yellow
  211. // WITHOUT line 194: column 2 retains white-on-black
  212. // WITH line 194: column 2 gets red-on-yellow
  213. Assert.Equal (new Attribute (Color.BrightRed, Color.BrightYellow), driver.Contents [0, 2].Attribute);
  214. driver.Refresh ();
  215. // Expected output:
  216. // Column 0: space with white-on-black
  217. // Columns 1-2: 🍎 with red-on-yellow (both columns must have same attribute!)
  218. //
  219. // WITHOUT line 196, the output would be:
  220. // \x1b[97m\x1b[40m (white-on-black for column 0)
  221. // \x1b[91m\x1b[103m🍎 (red-on-yellow starts at column 1)
  222. // \x1b[97m\x1b[40m (WRONG! Attribute changes mid-glyph because column 2 still has white-on-black)
  223. //
  224. // WITH line 196, the output is:
  225. // \x1b[97m\x1b[40m (white-on-black for column 0)
  226. // \x1b[91m\x1b[103m🍎 (red-on-yellow for both columns 1 and 2)
  227. DriverAssert.AssertDriverOutputIs (
  228. "\x1b[97m\x1b[40m \x1b[91m\x1b[103m🍎",
  229. output,
  230. driver);
  231. }
  232. }