VirtualizingCarouselPanelTests.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  1. using System;
  2. using System.Collections;
  3. using System.Collections.ObjectModel;
  4. using System.Linq;
  5. using System.Threading;
  6. using System.Threading.Tasks;
  7. using Avalonia.Animation;
  8. using Avalonia.Collections;
  9. using Avalonia.Controls.Presenters;
  10. using Avalonia.Controls.Primitives;
  11. using Avalonia.Controls.Templates;
  12. using Avalonia.Layout;
  13. using Avalonia.Threading;
  14. using Avalonia.UnitTests;
  15. using Avalonia.VisualTree;
  16. using Moq;
  17. using Xunit;
  18. #nullable enable
  19. namespace Avalonia.Controls.UnitTests
  20. {
  21. public class VirtualizingCarouselPanelTests : ScopedTestBase
  22. {
  23. [Fact]
  24. public void Initial_Item_Is_Displayed()
  25. {
  26. using var app = Start();
  27. var items = new[] { "foo", "bar" };
  28. var (target, _) = CreateTarget(items);
  29. Assert.Single(target.Children);
  30. var container = Assert.IsType<ContentPresenter>(target.Children[0]);
  31. Assert.Equal("foo", container.Content);
  32. }
  33. [Fact]
  34. public void Displays_Next_Item()
  35. {
  36. using var app = Start();
  37. var items = new[] { "foo", "bar" };
  38. var (target, carousel) = CreateTarget(items);
  39. carousel.SelectedIndex = 1;
  40. Layout(target);
  41. Assert.Single(target.Children);
  42. var container = Assert.IsType<ContentPresenter>(target.Children[0]);
  43. Assert.Equal("bar", container.Content);
  44. }
  45. [Fact]
  46. public void Handles_Inserted_Item()
  47. {
  48. using var app = Start();
  49. var items = new ObservableCollection<string> { "foo", "bar" };
  50. var (target, carousel) = CreateTarget(items);
  51. var container = Assert.IsType<ContentPresenter>(target.Children[0]);
  52. items.Insert(0, "baz");
  53. Layout(target);
  54. Assert.Single(target.Children);
  55. Assert.Same(container, target.Children[0]);
  56. Assert.Equal("foo", container.Content);
  57. }
  58. [Fact]
  59. public void Handles_Removed_Item()
  60. {
  61. using var app = Start();
  62. var items = new ObservableCollection<string> { "foo", "bar" };
  63. var (target, carousel) = CreateTarget(items);
  64. var container = Assert.IsType<ContentPresenter>(target.Children[0]);
  65. items.RemoveAt(0);
  66. Layout(target);
  67. Assert.Single(target.Children);
  68. Assert.Same(container, target.Children[0]);
  69. Assert.Equal("bar", container.Content);
  70. }
  71. [Fact]
  72. public void Handles_Replaced_Item()
  73. {
  74. using var app = Start();
  75. var items = new ObservableCollection<string> { "foo", "bar" };
  76. var (target, carousel) = CreateTarget(items);
  77. var container = Assert.IsType<ContentPresenter>(target.Children[0]);
  78. items[0] = "baz";
  79. Layout(target);
  80. Assert.Single(target.Children);
  81. Assert.Same(container, target.Children[0]);
  82. Assert.Equal("baz", container.Content);
  83. }
  84. [Fact]
  85. public void Handles_Moved_Item()
  86. {
  87. using var app = Start();
  88. var items = new ObservableCollection<string> { "foo", "bar" };
  89. var (target, carousel) = CreateTarget(items);
  90. var container = Assert.IsType<ContentPresenter>(target.Children[0]);
  91. items.Move(0, 1);
  92. Layout(target);
  93. Assert.Single(target.Children);
  94. Assert.Same(container, target.Children[0]);
  95. Assert.Equal("bar", container.Content);
  96. }
  97. [Fact]
  98. public void Handles_Moved_Item_Range()
  99. {
  100. using var app = Start();
  101. AvaloniaList<string> items = ["foo", "bar", "baz", "qux", "quux"];
  102. var (target, carousel) = CreateTarget(items);
  103. var container = Assert.IsType<ContentPresenter>(target.Children[0]);
  104. carousel.SelectedIndex = 3;
  105. Layout(target);
  106. items.MoveRange(0, 2, 4);
  107. Layout(target);
  108. Assert.Multiple(() =>
  109. {
  110. Assert.Single(target.Children);
  111. Assert.Same(container, target.Children[0]);
  112. Assert.Equal("qux", container.Content);
  113. Assert.Equal(1, carousel.SelectedIndex);
  114. });
  115. }
  116. public class Transitions : ScopedTestBase
  117. {
  118. [Fact]
  119. public void Initial_Item_Does_Not_Start_Transition()
  120. {
  121. using var app = Start();
  122. var items = new Control[] { new Button(), new Canvas() };
  123. var transition = new Mock<IPageTransition>();
  124. var (target, _) = CreateTarget(items, transition.Object);
  125. transition.Verify(x => x.Start(
  126. It.IsAny<Visual>(),
  127. It.IsAny<Visual>(),
  128. It.IsAny<bool>(),
  129. It.IsAny<CancellationToken>()),
  130. Times.Never);
  131. }
  132. [Fact]
  133. public void Changing_SelectedIndex_Starts_Transition()
  134. {
  135. using var app = Start();
  136. var items = new Control[] { new Button(), new Canvas() };
  137. var transition = new Mock<IPageTransition>();
  138. var (target, carousel) = CreateTarget(items, transition.Object);
  139. carousel.SelectedIndex = 1;
  140. Layout(target);
  141. transition.Verify(x => x.Start(
  142. items[0],
  143. items[1],
  144. true,
  145. It.IsAny<CancellationToken>()),
  146. Times.Once);
  147. }
  148. [Fact]
  149. public void Changing_SelectedIndex_transitions_forward_cycle()
  150. {
  151. using var app = Start();
  152. Dispatcher.UIThread.Invoke(() => // This sets up a proper sync context
  153. {
  154. var items = new Control[] { new Button(), new Canvas(), new Label() };
  155. var transition = new Mock<IPageTransition>();
  156. var (target, carousel) = CreateTarget(items, transition.Object);
  157. var cycleindexes = new[] { 1, 2, 0 };
  158. for (int cycleIndex = 0; cycleIndex < cycleindexes.Length; cycleIndex++)
  159. {
  160. carousel.SelectedIndex = cycleindexes[cycleIndex];
  161. Layout(target);
  162. Dispatcher.UIThread.RunJobs();
  163. var index = cycleIndex;
  164. transition.Verify(x => x.Start(
  165. index > 0 ? items[cycleindexes[index - 1]] : items[0],
  166. items[cycleindexes[index]],
  167. true,
  168. It.IsAny<CancellationToken>()),
  169. Times.Once);
  170. }
  171. });
  172. }
  173. [Fact]
  174. public void Changing_SelectedIndex_transitions_backward_cycle()
  175. {
  176. using var app = Start();
  177. Dispatcher.UIThread.Invoke(() => // This sets up a proper sync context
  178. {
  179. var items = new Control[] { new Button(), new Canvas(), new Label() };
  180. var transition = new Mock<IPageTransition>();
  181. var (target, carousel) = CreateTarget(items, transition.Object);
  182. var cycleindexes = new[] { 2, 1, 0 };
  183. for (int cycleIndex = 0; cycleIndex < cycleindexes.Length; cycleIndex++)
  184. {
  185. carousel.SelectedIndex = cycleindexes[cycleIndex];
  186. Layout(target);
  187. Dispatcher.UIThread.RunJobs();
  188. var index = cycleIndex;
  189. transition.Verify(x => x.Start(
  190. index > 0 ? items[cycleindexes[index - 1]] : items[0],
  191. items[cycleindexes[index]],
  192. false,
  193. It.IsAny<CancellationToken>()),
  194. Times.Once);
  195. }
  196. });
  197. }
  198. [Fact]
  199. public void TransitionFrom_Control_Is_Recycled_When_Transition_Completes()
  200. {
  201. using var app = Start();
  202. using var sync = UnitTestSynchronizationContext.Begin();
  203. var items = new Control[] { new Button(), new Canvas() };
  204. var transition = new Mock<IPageTransition>();
  205. var (target, carousel) = CreateTarget(items, transition.Object);
  206. var transitionTask = new TaskCompletionSource();
  207. transition.Setup(x => x.Start(
  208. items[0],
  209. items[1],
  210. true,
  211. It.IsAny<CancellationToken>()))
  212. .Returns(() => transitionTask.Task);
  213. carousel.SelectedIndex = 1;
  214. Layout(target);
  215. Assert.Equal(items, target.Children);
  216. Assert.All(items, x => Assert.True(x.IsVisible));
  217. transitionTask.SetResult();
  218. sync.ExecutePostedCallbacks();
  219. Assert.Equal(items, target.Children);
  220. Assert.False(items[0].IsVisible);
  221. Assert.True(items[1].IsVisible);
  222. }
  223. [Fact]
  224. public void Existing_Transition_Is_Canceled_If_Interrupted()
  225. {
  226. using var app = Start();
  227. using var sync = UnitTestSynchronizationContext.Begin();
  228. var items = new Control[] { new Button(), new Canvas() };
  229. var transition = new Mock<IPageTransition>();
  230. var (target, carousel) = CreateTarget(items, transition.Object);
  231. var transitionTask = new TaskCompletionSource();
  232. CancellationToken? cancelationToken = null;
  233. transition.Setup(x => x.Start(
  234. items[0],
  235. items[1],
  236. true,
  237. It.IsAny<CancellationToken>()))
  238. .Callback<Visual, Visual, bool, CancellationToken>((_, _, _, c) => cancelationToken = c)
  239. .Returns(() => transitionTask.Task);
  240. carousel.SelectedIndex = 1;
  241. Layout(target);
  242. Assert.NotNull(cancelationToken);
  243. Assert.False(cancelationToken!.Value.IsCancellationRequested);
  244. carousel.SelectedIndex = 0;
  245. Layout(target);
  246. Assert.True(cancelationToken!.Value.IsCancellationRequested);
  247. }
  248. }
  249. private static IDisposable Start() => UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);
  250. private static (VirtualizingCarouselPanel, Carousel) CreateTarget(
  251. IEnumerable items,
  252. IPageTransition? transition = null)
  253. {
  254. var carousel = new Carousel
  255. {
  256. ItemsSource = items,
  257. Template = CarouselTemplate(),
  258. PageTransition = transition,
  259. };
  260. var root = new TestRoot(carousel);
  261. root.LayoutManager.ExecuteInitialLayoutPass();
  262. return ((VirtualizingCarouselPanel)carousel.Presenter!.Panel!, carousel);
  263. }
  264. private static IControlTemplate CarouselTemplate()
  265. {
  266. return new FuncControlTemplate((c, ns) =>
  267. new ScrollViewer
  268. {
  269. Name = "PART_ScrollViewer",
  270. Template = ScrollViewerTemplate(),
  271. HorizontalScrollBarVisibility = ScrollBarVisibility.Hidden,
  272. VerticalScrollBarVisibility = ScrollBarVisibility.Hidden,
  273. Content = new ItemsPresenter
  274. {
  275. Name = "PART_ItemsPresenter",
  276. [~ItemsPresenter.ItemsPanelProperty] = c[~ItemsControl.ItemsPanelProperty],
  277. }.RegisterInNameScope(ns)
  278. }.RegisterInNameScope(ns));
  279. }
  280. private static FuncControlTemplate ScrollViewerTemplate()
  281. {
  282. return new FuncControlTemplate<ScrollViewer>((parent, scope) =>
  283. new Panel
  284. {
  285. Children =
  286. {
  287. new ScrollContentPresenter
  288. {
  289. Name = "PART_ContentPresenter",
  290. }.RegisterInNameScope(scope),
  291. }
  292. });
  293. }
  294. private static void Layout(Control c) => ((ILayoutRoot)c.GetVisualRoot()!).LayoutManager.ExecuteLayoutPass();
  295. }
  296. }