ConnectedAnimationTests.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585
  1. using System;
  2. using Avalonia.Animation;
  3. using Avalonia.Animation.Easings;
  4. using Avalonia.Controls;
  5. using Avalonia.UnitTests;
  6. using Xunit;
  7. namespace Avalonia.Controls.UnitTests;
  8. public class ConnectedAnimationConfigurationTests : ScopedTestBase
  9. {
  10. [Fact]
  11. public void GravityConfig_IsShadowEnabled_DefaultIsTrue()
  12. {
  13. var config = new GravityConnectedAnimationConfiguration();
  14. Assert.True(config.IsShadowEnabled);
  15. }
  16. [Fact]
  17. public void GravityConfig_IsShadowEnabled_CanBeSetFalse()
  18. {
  19. var config = new GravityConnectedAnimationConfiguration { IsShadowEnabled = false };
  20. Assert.False(config.IsShadowEnabled);
  21. }
  22. [Fact]
  23. public void GravityConfig_IsShadowEnabled_RoundTrips()
  24. {
  25. var config = new GravityConnectedAnimationConfiguration { IsShadowEnabled = true };
  26. Assert.True(config.IsShadowEnabled);
  27. config.IsShadowEnabled = false;
  28. Assert.False(config.IsShadowEnabled);
  29. }
  30. [Fact]
  31. public void GravityConfig_IsConnectedAnimationConfiguration()
  32. {
  33. var config = new GravityConnectedAnimationConfiguration();
  34. Assert.IsAssignableFrom<ConnectedAnimationConfiguration>(config);
  35. }
  36. [Fact]
  37. public void DirectConfig_Duration_DefaultIsNull()
  38. {
  39. var config = new DirectConnectedAnimationConfiguration();
  40. Assert.Null(config.Duration);
  41. }
  42. [Fact]
  43. public void DirectConfig_Duration_RoundTrips()
  44. {
  45. var d = TimeSpan.FromMilliseconds(200);
  46. var config = new DirectConnectedAnimationConfiguration { Duration = d };
  47. Assert.Equal(d, config.Duration);
  48. }
  49. [Fact]
  50. public void DirectConfig_Duration_CanBeSetToNull()
  51. {
  52. var config = new DirectConnectedAnimationConfiguration { Duration = TimeSpan.FromMilliseconds(100) };
  53. config.Duration = null;
  54. Assert.Null(config.Duration);
  55. }
  56. [Fact]
  57. public void DirectConfig_IsConnectedAnimationConfiguration()
  58. {
  59. var config = new DirectConnectedAnimationConfiguration();
  60. Assert.IsAssignableFrom<ConnectedAnimationConfiguration>(config);
  61. }
  62. [Fact]
  63. public void BasicConfig_IsConnectedAnimationConfiguration()
  64. {
  65. var config = new BasicConnectedAnimationConfiguration();
  66. Assert.IsAssignableFrom<ConnectedAnimationConfiguration>(config);
  67. }
  68. [Fact]
  69. public void BasicConfig_IsInstantiable()
  70. {
  71. var config = new BasicConnectedAnimationConfiguration();
  72. Assert.NotNull(config);
  73. }
  74. }
  75. public class ConnectedAnimationServiceTests : ScopedTestBase
  76. {
  77. private static ConnectedAnimationService CreateService() => new ConnectedAnimationService();
  78. [Fact]
  79. public void GetForTopLevel_NullTopLevel_ThrowsArgumentNullException()
  80. {
  81. Assert.Throws<ArgumentNullException>(() =>
  82. ConnectedAnimationService.GetForTopLevel(null!));
  83. }
  84. [Fact]
  85. public void DefaultDuration_InitiallyIs300ms()
  86. {
  87. var service = CreateService();
  88. Assert.Equal(TimeSpan.FromMilliseconds(300), service.DefaultDuration);
  89. }
  90. [Fact]
  91. public void DefaultEasingFunction_InitiallyNull()
  92. {
  93. var service = CreateService();
  94. Assert.Null(service.DefaultEasingFunction);
  95. }
  96. [Fact]
  97. public void DefaultDuration_CanBeSet()
  98. {
  99. var service = CreateService();
  100. var d = TimeSpan.FromMilliseconds(500);
  101. service.DefaultDuration = d;
  102. Assert.Equal(d, service.DefaultDuration);
  103. }
  104. [Fact]
  105. public void DefaultEasingFunction_CanBeSet()
  106. {
  107. var service = CreateService();
  108. var easing = new LinearEasing();
  109. service.DefaultEasingFunction = easing;
  110. Assert.Same(easing, service.DefaultEasingFunction);
  111. }
  112. [Fact]
  113. public void GetAnimation_UnknownKey_ReturnsNull()
  114. {
  115. var service = CreateService();
  116. Assert.Null(service.GetAnimation("nonexistent"));
  117. }
  118. [Fact]
  119. public void PrepareToAnimate_NullKey_ThrowsArgumentNullException()
  120. {
  121. var service = CreateService();
  122. var source = new Border();
  123. Assert.Throws<ArgumentNullException>(() =>
  124. service.PrepareToAnimate(null!, source));
  125. }
  126. [Fact]
  127. public void PrepareToAnimate_EmptyKey_ThrowsArgumentException()
  128. {
  129. var service = CreateService();
  130. var source = new Border();
  131. Assert.Throws<ArgumentException>(() =>
  132. service.PrepareToAnimate(string.Empty, source));
  133. }
  134. [Fact]
  135. public void PrepareToAnimate_NullSource_ThrowsArgumentNullException()
  136. {
  137. var service = CreateService();
  138. Assert.Throws<ArgumentNullException>(() =>
  139. service.PrepareToAnimate("key", null!));
  140. }
  141. [Fact]
  142. public void PrepareToAnimate_ReturnsAnimation_WithMatchingKey()
  143. {
  144. var service = CreateService();
  145. var animation = service.PrepareToAnimate("hero", new Border());
  146. Assert.Equal("hero", animation.Key);
  147. }
  148. [Fact]
  149. public void PrepareToAnimate_AnimationInitiallyNotConsumed()
  150. {
  151. var service = CreateService();
  152. var animation = service.PrepareToAnimate("hero", new Border());
  153. Assert.False(animation.IsConsumed);
  154. }
  155. [Fact]
  156. public void GetAnimation_AfterPrepare_ReturnsAnimation()
  157. {
  158. var service = CreateService();
  159. var animation = service.PrepareToAnimate("hero", new Border());
  160. Assert.Same(animation, service.GetAnimation("hero"));
  161. }
  162. [Fact]
  163. public void GetAnimation_ReturnsSameInstance_ForSameKey()
  164. {
  165. var service = CreateService();
  166. service.PrepareToAnimate("hero", new Border());
  167. var a1 = service.GetAnimation("hero");
  168. var a2 = service.GetAnimation("hero");
  169. Assert.Same(a1, a2);
  170. }
  171. [Fact]
  172. public void PrepareToAnimate_SameKey_ReplacesOldAnimation()
  173. {
  174. var service = CreateService();
  175. var first = service.PrepareToAnimate("hero", new Border());
  176. var second = service.PrepareToAnimate("hero", new Border());
  177. Assert.NotSame(first, second);
  178. Assert.True(first.IsConsumed || first.IsDisposed);
  179. Assert.Same(second, service.GetAnimation("hero"));
  180. }
  181. [Fact]
  182. public void PrepareToAnimate_DifferentKeys_BothInService()
  183. {
  184. var service = CreateService();
  185. var a1 = service.PrepareToAnimate("key1", new Border());
  186. var a2 = service.PrepareToAnimate("key2", new Border());
  187. Assert.Same(a1, service.GetAnimation("key1"));
  188. Assert.Same(a2, service.GetAnimation("key2"));
  189. }
  190. }
  191. public class ConnectedAnimationTests : ScopedTestBase
  192. {
  193. private static ConnectedAnimationService CreateService() => new ConnectedAnimationService();
  194. [Fact]
  195. public void Key_MatchesPreparedKey()
  196. {
  197. var service = CreateService();
  198. var animation = service.PrepareToAnimate("myKey", new Border());
  199. Assert.Equal("myKey", animation.Key);
  200. }
  201. [Fact]
  202. public void IsConsumed_InitiallyFalse()
  203. {
  204. var service = CreateService();
  205. var animation = service.PrepareToAnimate("hero", new Border());
  206. Assert.False(animation.IsConsumed);
  207. }
  208. [Fact]
  209. public void Configuration_InitiallyNull()
  210. {
  211. var service = CreateService();
  212. var animation = service.PrepareToAnimate("hero", new Border());
  213. Assert.Null(animation.Configuration);
  214. }
  215. [Fact]
  216. public void Configuration_GravityRoundTrips()
  217. {
  218. var service = CreateService();
  219. var animation = service.PrepareToAnimate("hero", new Border());
  220. var config = new GravityConnectedAnimationConfiguration { IsShadowEnabled = false };
  221. animation.Configuration = config;
  222. Assert.Same(config, animation.Configuration);
  223. }
  224. [Fact]
  225. public void Configuration_DirectRoundTrips()
  226. {
  227. var service = CreateService();
  228. var animation = service.PrepareToAnimate("hero", new Border());
  229. var config = new DirectConnectedAnimationConfiguration { Duration = TimeSpan.FromMilliseconds(100) };
  230. animation.Configuration = config;
  231. Assert.Same(config, animation.Configuration);
  232. }
  233. [Fact]
  234. public void Configuration_BasicRoundTrips()
  235. {
  236. var service = CreateService();
  237. var animation = service.PrepareToAnimate("hero", new Border());
  238. var config = new BasicConnectedAnimationConfiguration();
  239. animation.Configuration = config;
  240. Assert.Same(config, animation.Configuration);
  241. }
  242. [Fact]
  243. public void TryStart_ReturnsTrue_WhenNotConsumed()
  244. {
  245. var service = CreateService();
  246. var animation = service.PrepareToAnimate("hero", new Border());
  247. var result = animation.TryStart(new Border());
  248. Assert.True(result);
  249. }
  250. [Fact]
  251. public void TryStart_ConsumesAnimation()
  252. {
  253. var service = CreateService();
  254. var animation = service.PrepareToAnimate("hero", new Border());
  255. animation.TryStart(new Border());
  256. Assert.True(animation.IsConsumed);
  257. }
  258. [Fact]
  259. public void TryStart_SecondCall_ReturnsFalse()
  260. {
  261. var service = CreateService();
  262. var animation = service.PrepareToAnimate("hero", new Border());
  263. animation.TryStart(new Border());
  264. var second = animation.TryStart(new Border());
  265. Assert.False(second);
  266. }
  267. [Fact]
  268. public void TryStart_ReturnsFalse_WhenDisposed()
  269. {
  270. var service = CreateService();
  271. var animation = service.PrepareToAnimate("hero", new Border());
  272. animation.Dispose();
  273. var result = animation.TryStart(new Border());
  274. Assert.False(result);
  275. }
  276. [Fact]
  277. public void TryStart_WithEmptyCoordinatedElements_ReturnsTrue()
  278. {
  279. var service = CreateService();
  280. var animation = service.PrepareToAnimate("hero", new Border());
  281. var result = animation.TryStart(new Border(), Array.Empty<Visual>());
  282. Assert.True(result);
  283. }
  284. [Fact]
  285. public void TryStart_WhenNoTopLevel_FiresCompleted()
  286. {
  287. var service = CreateService();
  288. var animation = service.PrepareToAnimate("hero", new Border());
  289. ConnectedAnimationCompletedEventArgs? received = null;
  290. animation.Completed += (_, e) => received = e;
  291. animation.TryStart(new Border()); // no TopLevel ancestor
  292. Assert.NotNull(received);
  293. }
  294. [Fact]
  295. public void TryStart_WhenNoTopLevel_Completed_NotCancelled()
  296. {
  297. var service = CreateService();
  298. var animation = service.PrepareToAnimate("hero", new Border());
  299. bool? cancelled = null;
  300. animation.Completed += (_, e) => cancelled = e.Cancelled;
  301. animation.TryStart(new Border());
  302. Assert.False(cancelled);
  303. }
  304. [Fact]
  305. public void TryStart_WhenNoTopLevel_RemovesFromService()
  306. {
  307. var service = CreateService();
  308. var animation = service.PrepareToAnimate("hero", new Border());
  309. animation.TryStart(new Border());
  310. Assert.Null(service.GetAnimation("hero"));
  311. }
  312. [Fact]
  313. public void TryStart_WithCoordinatedElements_WhenNoTopLevel_FiresCompleted()
  314. {
  315. var service = CreateService();
  316. var animation = service.PrepareToAnimate("hero", new Border());
  317. var coordinated = new Visual[] { new Border(), new Border() };
  318. bool fired = false;
  319. animation.Completed += (_, _) => fired = true;
  320. animation.TryStart(new Border(), coordinated);
  321. Assert.True(fired);
  322. }
  323. [Fact]
  324. public void Dispose_RemovesFromService()
  325. {
  326. var service = CreateService();
  327. service.PrepareToAnimate("hero", new Border());
  328. Assert.NotNull(service.GetAnimation("hero"));
  329. service.GetAnimation("hero")!.Dispose();
  330. Assert.Null(service.GetAnimation("hero"));
  331. }
  332. [Fact]
  333. public void Dispose_CalledTwice_IsNoOp()
  334. {
  335. var service = CreateService();
  336. var animation = service.PrepareToAnimate("hero", new Border());
  337. animation.Dispose();
  338. animation.Dispose(); // must not throw
  339. }
  340. [Fact]
  341. public void Dispose_WhenNotMidFlight_DoesNotFireCompleted()
  342. {
  343. var service = CreateService();
  344. var animation = service.PrepareToAnimate("hero", new Border());
  345. bool fired = false;
  346. animation.Completed += (_, _) => fired = true;
  347. animation.Dispose();
  348. Assert.False(fired);
  349. }
  350. [Fact]
  351. public void GetAnimation_ReturnsNull_AfterTryStart()
  352. {
  353. var service = CreateService();
  354. service.PrepareToAnimate("hero", new Border());
  355. service.GetAnimation("hero")!.TryStart(new Border());
  356. Assert.Null(service.GetAnimation("hero"));
  357. }
  358. [Fact]
  359. public void GetAnimation_ReturnsNull_AfterDispose()
  360. {
  361. var service = CreateService();
  362. service.PrepareToAnimate("hero", new Border());
  363. service.GetAnimation("hero")!.Dispose();
  364. Assert.Null(service.GetAnimation("hero"));
  365. }
  366. [Fact]
  367. public void GetAnimation_ReturnsNull_WhenAnimationIsConsumed()
  368. {
  369. var service = CreateService();
  370. var animation = service.PrepareToAnimate("hero", new Border());
  371. animation.TryStart(new Border());
  372. Assert.Null(service.GetAnimation("hero"));
  373. }
  374. [Fact]
  375. public void MultipleAnimations_IndependentLifecycles()
  376. {
  377. var service = CreateService();
  378. var a1 = service.PrepareToAnimate("anim1", new Border());
  379. var a2 = service.PrepareToAnimate("anim2", new Border());
  380. a1.TryStart(new Border());
  381. Assert.True(a1.IsConsumed);
  382. Assert.False(a2.IsConsumed);
  383. Assert.Null(service.GetAnimation("anim1"));
  384. Assert.NotNull(service.GetAnimation("anim2"));
  385. }
  386. [Fact]
  387. public void PrepareToAnimate_AfterDispose_CanPrepareAgain()
  388. {
  389. var service = CreateService();
  390. var first = service.PrepareToAnimate("hero", new Border());
  391. first.Dispose();
  392. var second = service.PrepareToAnimate("hero", new Border());
  393. Assert.NotSame(first, second);
  394. Assert.Same(second, service.GetAnimation("hero"));
  395. }
  396. // ResolveTimingAndEasing tests — no reflection needed, method is internal.
  397. [Fact]
  398. public void Configuration_DirectWithDuration_UsesProvidedDuration()
  399. {
  400. var service = CreateService();
  401. var animation = service.PrepareToAnimate("hero", new Border());
  402. animation.Configuration = new DirectConnectedAnimationConfiguration
  403. {
  404. Duration = TimeSpan.FromMilliseconds(250)
  405. };
  406. animation.ResolveTimingAndEasing(service, out var resolvedDuration, out _, out _, out _);
  407. Assert.Equal(TimeSpan.FromMilliseconds(250), resolvedDuration);
  408. }
  409. [Fact]
  410. public void Configuration_DirectWithNullDuration_UsesServiceDefaultDuration()
  411. {
  412. var service = CreateService();
  413. service.DefaultDuration = TimeSpan.FromMilliseconds(400);
  414. var animation = service.PrepareToAnimate("hero", new Border());
  415. animation.Configuration = new DirectConnectedAnimationConfiguration { Duration = null };
  416. animation.ResolveTimingAndEasing(service, out var resolvedDuration, out _, out _, out _);
  417. Assert.Equal(TimeSpan.FromMilliseconds(400), resolvedDuration);
  418. }
  419. [Fact]
  420. public void Configuration_Basic_UsesServiceDefaultDuration()
  421. {
  422. var service = CreateService();
  423. service.DefaultDuration = TimeSpan.FromMilliseconds(400);
  424. var animation = service.PrepareToAnimate("hero", new Border());
  425. animation.Configuration = new BasicConnectedAnimationConfiguration();
  426. animation.ResolveTimingAndEasing(service, out var resolvedDuration, out _, out _, out _);
  427. Assert.Equal(TimeSpan.FromMilliseconds(400), resolvedDuration);
  428. }
  429. [Fact]
  430. public void Configuration_Gravity_UsesServiceDefaultDuration()
  431. {
  432. var service = CreateService();
  433. service.DefaultDuration = TimeSpan.FromMilliseconds(350);
  434. var animation = service.PrepareToAnimate("hero", new Border());
  435. animation.Configuration = new GravityConnectedAnimationConfiguration();
  436. animation.ResolveTimingAndEasing(service, out var resolvedDuration, out _, out _, out _);
  437. Assert.Equal(TimeSpan.FromMilliseconds(350), resolvedDuration);
  438. }
  439. [Fact]
  440. public void Configuration_Gravity_UseGravityDip_IsTrue()
  441. {
  442. var service = CreateService();
  443. var animation = service.PrepareToAnimate("hero", new Border());
  444. animation.Configuration = new GravityConnectedAnimationConfiguration();
  445. animation.ResolveTimingAndEasing(service, out _, out _, out var useGravityDip, out _);
  446. Assert.True(useGravityDip);
  447. }
  448. [Fact]
  449. public void Configuration_Direct_UseGravityDip_IsFalse()
  450. {
  451. var service = CreateService();
  452. var animation = service.PrepareToAnimate("hero", new Border());
  453. animation.Configuration = new DirectConnectedAnimationConfiguration();
  454. animation.ResolveTimingAndEasing(service, out _, out _, out var useGravityDip, out _);
  455. Assert.False(useGravityDip);
  456. }
  457. [Fact]
  458. public void Configuration_Basic_UseGravityDip_IsFalse()
  459. {
  460. var service = CreateService();
  461. var animation = service.PrepareToAnimate("hero", new Border());
  462. animation.Configuration = new BasicConnectedAnimationConfiguration();
  463. animation.ResolveTimingAndEasing(service, out _, out _, out var useGravityDip, out _);
  464. Assert.False(useGravityDip);
  465. }
  466. [Fact]
  467. public void Configuration_Gravity_IsShadowEnabled_False_UseShadow_IsFalse()
  468. {
  469. var service = CreateService();
  470. var animation = service.PrepareToAnimate("hero", new Border());
  471. animation.Configuration = new GravityConnectedAnimationConfiguration { IsShadowEnabled = false };
  472. animation.ResolveTimingAndEasing(service, out _, out _, out _, out var useShadow);
  473. Assert.False(useShadow);
  474. }
  475. [Fact]
  476. public void Configuration_Gravity_IsShadowEnabled_True_UseShadow_IsTrue()
  477. {
  478. var service = CreateService();
  479. var animation = service.PrepareToAnimate("hero", new Border());
  480. animation.Configuration = new GravityConnectedAnimationConfiguration { IsShadowEnabled = true };
  481. animation.ResolveTimingAndEasing(service, out _, out _, out _, out var useShadow);
  482. Assert.True(useShadow);
  483. }
  484. }