IListDataSourceTests.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513
  1. #nullable enable
  2. using System.Collections;
  3. using System.Collections.ObjectModel;
  4. using System.Collections.Specialized;
  5. using System.Text;
  6. using Xunit.Abstractions;
  7. // ReSharper disable InconsistentNaming
  8. namespace UnitTests_Parallelizable.ViewTests;
  9. public class IListDataSourceTests (ITestOutputHelper output)
  10. {
  11. private readonly ITestOutputHelper _output = output;
  12. #region Concurrent Modification Tests
  13. [Fact]
  14. public void ListWrapper_SuspendAndModify_NoEventsUntilResume ()
  15. {
  16. ObservableCollection<string> source = ["Item1"];
  17. ListWrapper<string> wrapper = new (source);
  18. var eventCount = 0;
  19. wrapper.CollectionChanged += (s, e) => eventCount++;
  20. wrapper.SuspendCollectionChangedEvent = true;
  21. source.Add ("Item2");
  22. source.Add ("Item3");
  23. source.RemoveAt (0);
  24. Assert.Equal (0, eventCount);
  25. wrapper.SuspendCollectionChangedEvent = false;
  26. // Should have adjusted marks for the removals that happened while suspended
  27. Assert.Equal (2, wrapper.Count);
  28. }
  29. #endregion
  30. /// <summary>
  31. /// Test implementation of IListDataSource for testing custom implementations
  32. /// </summary>
  33. private class TestListDataSource : IListDataSource
  34. {
  35. private readonly List<string> _items = ["Custom Item 00", "Custom Item 01", "Custom Item 02"];
  36. private readonly BitArray _marks = new (3);
  37. public event NotifyCollectionChangedEventHandler? CollectionChanged;
  38. public int Count => _items.Count;
  39. public int Length => _items.Any () ? _items.Max (s => s?.Length ?? 0) : 0;
  40. public bool SuspendCollectionChangedEvent { get; set; }
  41. public bool IsMarked (int item)
  42. {
  43. if (item < 0 || item >= _items.Count)
  44. {
  45. return false;
  46. }
  47. return _marks [item];
  48. }
  49. public void SetMark (int item, bool value)
  50. {
  51. if (item >= 0 && item < _items.Count)
  52. {
  53. _marks [item] = value;
  54. }
  55. }
  56. public void Render (ListView listView, bool selected, int item, int col, int line, int width, int viewportX = 0)
  57. {
  58. if (item < 0 || item >= _items.Count)
  59. {
  60. return;
  61. }
  62. listView.Move (col, line);
  63. string text = _items [item] ?? "";
  64. if (viewportX < text.Length)
  65. {
  66. text = text.Substring (viewportX);
  67. }
  68. else
  69. {
  70. text = "";
  71. }
  72. if (text.Length > width)
  73. {
  74. text = text.Substring (0, width);
  75. }
  76. listView.AddStr (text);
  77. // Fill remaining width
  78. for (int i = text.Length; i < width; i++)
  79. {
  80. listView.AddRune ((Rune)' ');
  81. }
  82. }
  83. public IList ToList () { return _items; }
  84. public void Dispose () { IsDisposed = true; }
  85. public void AddItem (string item)
  86. {
  87. _items.Add (item);
  88. // Resize marks
  89. var newMarks = new BitArray (_items.Count);
  90. for (var i = 0; i < Math.Min (_marks.Length, newMarks.Length); i++)
  91. {
  92. newMarks [i] = _marks [i];
  93. }
  94. if (!SuspendCollectionChangedEvent)
  95. {
  96. CollectionChanged?.Invoke (this, new (NotifyCollectionChangedAction.Add, item, _items.Count - 1));
  97. }
  98. }
  99. public bool IsDisposed { get; private set; }
  100. }
  101. #region ListWrapper<T> Render Tests
  102. [Fact]
  103. public void ListWrapper_Render_NullItem_RendersEmpty ()
  104. {
  105. ObservableCollection<string?> source = [null, "Item2"];
  106. ListWrapper<string?> wrapper = new (source);
  107. var listView = new ListView { Width = 20, Height = 2 };
  108. listView.BeginInit ();
  109. listView.EndInit ();
  110. // Render the null item (index 0)
  111. wrapper.Render (listView, false, 0, 0, 0, 20);
  112. // Should not throw and should render empty/spaces
  113. Assert.Equal (2, wrapper.Count);
  114. }
  115. [Fact]
  116. public void ListWrapper_Render_EmptyString_RendersSpaces ()
  117. {
  118. ObservableCollection<string> source = [""];
  119. ListWrapper<string> wrapper = new (source);
  120. var listView = new ListView { Width = 20, Height = 1 };
  121. listView.BeginInit ();
  122. listView.EndInit ();
  123. wrapper.Render (listView, false, 0, 0, 0, 20);
  124. Assert.Equal (1, wrapper.Count);
  125. Assert.Equal (0, wrapper.Length); // Empty string has zero length
  126. }
  127. [Fact]
  128. public void ListWrapper_Render_UnicodeText_CalculatesWidthCorrectly ()
  129. {
  130. ObservableCollection<string> source = ["Hello 你好", "Test"];
  131. ListWrapper<string> wrapper = new (source);
  132. // "Hello 你好" should be: "Hello " (6) + "你" (2) + "好" (2) = 10 columns
  133. Assert.True (wrapper.Length >= 10);
  134. }
  135. [Fact]
  136. public void ListWrapper_Render_LongString_ClipsToWidth ()
  137. {
  138. var longString = new string ('X', 100);
  139. ObservableCollection<string> source = [longString];
  140. ListWrapper<string> wrapper = new (source);
  141. var listView = new ListView { Width = 20, Height = 1 };
  142. listView.BeginInit ();
  143. listView.EndInit ();
  144. wrapper.Render (listView, false, 0, 0, 0, 10);
  145. Assert.Equal (100, wrapper.Length);
  146. }
  147. [Fact]
  148. public void ListWrapper_Render_WithViewportX_ScrollsHorizontally ()
  149. {
  150. ObservableCollection<string> source = ["0123456789ABCDEF"];
  151. ListWrapper<string> wrapper = new (source);
  152. var listView = new ListView { Width = 10, Height = 1 };
  153. listView.BeginInit ();
  154. listView.EndInit ();
  155. // Render with horizontal scroll offset of 5
  156. wrapper.Render (listView, false, 0, 0, 0, 10, 5);
  157. // Should render "56789ABCDE" (starting at position 5)
  158. Assert.Equal (16, wrapper.Length);
  159. }
  160. [Fact]
  161. public void ListWrapper_Render_ViewportXBeyondLength_RendersEmpty ()
  162. {
  163. ObservableCollection<string> source = ["Short"];
  164. ListWrapper<string> wrapper = new (source);
  165. var listView = new ListView { Width = 20, Height = 1 };
  166. listView.BeginInit ();
  167. listView.EndInit ();
  168. // Render with viewport beyond string length
  169. wrapper.Render (listView, false, 0, 0, 0, 10, 100);
  170. Assert.Equal (5, wrapper.Length);
  171. }
  172. [Fact]
  173. public void ListWrapper_Render_ColAndLine_PositionsCorrectly ()
  174. {
  175. ObservableCollection<string> source = ["Item1", "Item2"];
  176. ListWrapper<string> wrapper = new (source);
  177. var listView = new ListView { Width = 20, Height = 5 };
  178. listView.BeginInit ();
  179. listView.EndInit ();
  180. // Render at different positions
  181. wrapper.Render (listView, false, 0, 2, 1, 10); // col=2, line=1
  182. wrapper.Render (listView, false, 1, 0, 3, 10); // col=0, line=3
  183. Assert.Equal (2, wrapper.Count);
  184. }
  185. [Fact]
  186. public void ListWrapper_Render_WidthConstraint_FillsRemaining ()
  187. {
  188. ObservableCollection<string> source = ["Hi"];
  189. ListWrapper<string> wrapper = new (source);
  190. var listView = new ListView { Width = 20, Height = 1 };
  191. listView.BeginInit ();
  192. listView.EndInit ();
  193. // Render "Hi" in width of 10 - should fill remaining 8 with spaces
  194. wrapper.Render (listView, false, 0, 0, 0, 10);
  195. Assert.Equal (2, wrapper.Length);
  196. }
  197. [Fact]
  198. public void ListWrapper_Render_NonStringType_UsesToString ()
  199. {
  200. ObservableCollection<int> source = [42, 100, -5];
  201. ListWrapper<int> wrapper = new (source);
  202. var listView = new ListView { Width = 20, Height = 3 };
  203. listView.BeginInit ();
  204. listView.EndInit ();
  205. wrapper.Render (listView, false, 0, 0, 0, 10);
  206. wrapper.Render (listView, false, 1, 0, 1, 10);
  207. wrapper.Render (listView, false, 2, 0, 2, 10);
  208. Assert.Equal (3, wrapper.Count);
  209. Assert.True (wrapper.Length >= 2); // "42" is 2 chars, "100" is 3 chars
  210. }
  211. #endregion
  212. #region Custom IListDataSource Implementation Tests
  213. [Fact]
  214. public void CustomDataSource_AllMembers_WorkCorrectly ()
  215. {
  216. var customSource = new TestListDataSource ();
  217. var listView = new ListView { Source = customSource, Width = 20, Height = 5 };
  218. Assert.Equal (3, customSource.Count);
  219. Assert.Equal (14, customSource.Length); // "Custom Item 00" is 14 chars
  220. // Test marking
  221. Assert.False (customSource.IsMarked (0));
  222. customSource.SetMark (0, true);
  223. Assert.True (customSource.IsMarked (0));
  224. customSource.SetMark (0, false);
  225. Assert.False (customSource.IsMarked (0));
  226. // Test ToList
  227. IList list = customSource.ToList ();
  228. Assert.Equal (3, list.Count);
  229. Assert.Equal ("Custom Item 00", list [0]);
  230. // Test render doesn't throw
  231. listView.BeginInit ();
  232. listView.EndInit ();
  233. Exception ex = Record.Exception (() => customSource.Render (listView, false, 0, 0, 0, 20));
  234. Assert.Null (ex);
  235. }
  236. [Fact]
  237. public void CustomDataSource_CollectionChanged_RaisedOnModification ()
  238. {
  239. var customSource = new TestListDataSource ();
  240. var eventRaised = false;
  241. NotifyCollectionChangedAction? action = null;
  242. customSource.CollectionChanged += (s, e) =>
  243. {
  244. eventRaised = true;
  245. action = e.Action;
  246. };
  247. customSource.AddItem ("New Item");
  248. Assert.True (eventRaised);
  249. Assert.Equal (NotifyCollectionChangedAction.Add, action);
  250. Assert.Equal (4, customSource.Count);
  251. }
  252. [Fact]
  253. public void CustomDataSource_SuspendCollectionChanged_SuppressesEvents ()
  254. {
  255. var customSource = new TestListDataSource ();
  256. var eventCount = 0;
  257. customSource.CollectionChanged += (s, e) => eventCount++;
  258. customSource.SuspendCollectionChangedEvent = true;
  259. customSource.AddItem ("Item 1");
  260. customSource.AddItem ("Item 2");
  261. Assert.Equal (0, eventCount); // No events raised
  262. customSource.SuspendCollectionChangedEvent = false;
  263. customSource.AddItem ("Item 3");
  264. Assert.Equal (1, eventCount); // Event raised after resume
  265. }
  266. [Fact]
  267. public void CustomDataSource_Dispose_CleansUp ()
  268. {
  269. var customSource = new TestListDataSource ();
  270. customSource.Dispose ();
  271. // After dispose, adding should not raise events (if implemented correctly)
  272. customSource.AddItem ("New Item");
  273. // The test source doesn't unsubscribe in dispose, but this shows the pattern
  274. Assert.True (customSource.IsDisposed);
  275. }
  276. #endregion
  277. #region Edge Cases
  278. [Fact]
  279. public void ListWrapper_EmptyCollection_PropertiesReturnZero ()
  280. {
  281. ObservableCollection<string> source = [];
  282. ListWrapper<string> wrapper = new (source);
  283. Assert.Equal (0, wrapper.Count);
  284. Assert.Equal (0, wrapper.Length);
  285. }
  286. [Fact]
  287. public void ListWrapper_NullSource_HandledGracefully ()
  288. {
  289. ListWrapper<string> wrapper = new (null);
  290. Assert.Equal (0, wrapper.Count);
  291. Assert.Equal (0, wrapper.Length);
  292. // ToList should not throw
  293. IList list = wrapper.ToList ();
  294. Assert.Empty (list);
  295. }
  296. [Fact]
  297. public void ListWrapper_IsMarked_OutOfBounds_ReturnsFalse ()
  298. {
  299. ObservableCollection<string> source = ["Item1"];
  300. ListWrapper<string> wrapper = new (source);
  301. Assert.False (wrapper.IsMarked (-1));
  302. Assert.False (wrapper.IsMarked (1));
  303. Assert.False (wrapper.IsMarked (100));
  304. }
  305. [Fact]
  306. public void ListWrapper_SetMark_OutOfBounds_DoesNotThrow ()
  307. {
  308. ObservableCollection<string> source = ["Item1"];
  309. ListWrapper<string> wrapper = new (source);
  310. Exception ex = Record.Exception (() => wrapper.SetMark (-1, true));
  311. Assert.Null (ex);
  312. ex = Record.Exception (() => wrapper.SetMark (100, true));
  313. Assert.Null (ex);
  314. }
  315. [Fact]
  316. public void ListWrapper_CollectionShrinks_MarksAdjusted ()
  317. {
  318. ObservableCollection<string> source = ["Item1", "Item2", "Item3"];
  319. ListWrapper<string> wrapper = new (source);
  320. wrapper.SetMark (0, true);
  321. wrapper.SetMark (2, true);
  322. Assert.True (wrapper.IsMarked (0));
  323. Assert.True (wrapper.IsMarked (2));
  324. // Remove item 1 (middle item)
  325. source.RemoveAt (1);
  326. Assert.Equal (2, wrapper.Count);
  327. Assert.True (wrapper.IsMarked (0)); // Still marked
  328. // Item that was at index 2 is now at index 1
  329. }
  330. [Fact]
  331. public void ListWrapper_CollectionGrows_MarksPreserved ()
  332. {
  333. ObservableCollection<string> source = ["Item1"];
  334. ListWrapper<string> wrapper = new (source);
  335. wrapper.SetMark (0, true);
  336. Assert.True (wrapper.IsMarked (0));
  337. source.Add ("Item2");
  338. source.Add ("Item3");
  339. Assert.Equal (3, wrapper.Count);
  340. Assert.True (wrapper.IsMarked (0)); // Original mark preserved
  341. Assert.False (wrapper.IsMarked (1));
  342. Assert.False (wrapper.IsMarked (2));
  343. }
  344. [Fact]
  345. public void ListWrapper_StartsWith_EmptyString_ReturnsFirst ()
  346. {
  347. ObservableCollection<string> source = ["Apple", "Banana", "Cherry"];
  348. ListWrapper<string> wrapper = new (source);
  349. // Searching for empty string might return -1 or 0 depending on implementation
  350. int result = wrapper.StartsWith ("");
  351. Assert.True (result == -1 || result == 0);
  352. }
  353. [Fact]
  354. public void ListWrapper_StartsWith_NoMatch_ReturnsNegative ()
  355. {
  356. ObservableCollection<string> source = ["Apple", "Banana", "Cherry"];
  357. ListWrapper<string> wrapper = new (source);
  358. int result = wrapper.StartsWith ("Zebra");
  359. Assert.Equal (-1, result);
  360. }
  361. [Fact]
  362. public void ListWrapper_StartsWith_CaseInsensitive ()
  363. {
  364. ObservableCollection<string> source = ["Apple", "Banana", "Cherry"];
  365. ListWrapper<string> wrapper = new (source);
  366. Assert.Equal (0, wrapper.StartsWith ("app"));
  367. Assert.Equal (0, wrapper.StartsWith ("APP"));
  368. Assert.Equal (1, wrapper.StartsWith ("ban"));
  369. Assert.Equal (1, wrapper.StartsWith ("BAN"));
  370. }
  371. [Fact]
  372. public void ListWrapper_MaxLength_UpdatesOnCollectionChange ()
  373. {
  374. ObservableCollection<string> source = ["Hi"];
  375. ListWrapper<string> wrapper = new (source);
  376. Assert.Equal (2, wrapper.Length);
  377. source.Add ("Very Long String Indeed");
  378. Assert.Equal (23, wrapper.Length);
  379. source.Clear ();
  380. source.Add ("X");
  381. Assert.Equal (1, wrapper.Length);
  382. }
  383. [Fact]
  384. public void ListWrapper_Dispose_UnsubscribesFromCollectionChanged ()
  385. {
  386. ObservableCollection<string> source = ["Item1"];
  387. ListWrapper<string> wrapper = new (source);
  388. wrapper.CollectionChanged += (s, e) => { };
  389. wrapper.Dispose ();
  390. // After dispose, source changes should not raise wrapper events
  391. source.Add ("Item2");
  392. // The wrapper's event might still fire, but the wrapper won't propagate source events
  393. // This depends on implementation
  394. }
  395. #endregion
  396. }