TimePickerTests.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  1. using System;
  2. using System.Linq;
  3. using System.Reactive.Subjects;
  4. using Avalonia.Controls.Primitives;
  5. using Avalonia.Controls.Shapes;
  6. using Avalonia.Controls.Templates;
  7. using Avalonia.Data;
  8. using Avalonia.Headless;
  9. using Avalonia.Platform;
  10. using Avalonia.Threading;
  11. using Avalonia.UnitTests;
  12. using Avalonia.VisualTree;
  13. using Moq;
  14. using Xunit;
  15. namespace Avalonia.Controls.UnitTests
  16. {
  17. public class TimePickerTests : ScopedTestBase
  18. {
  19. [Fact]
  20. public void SelectedTimeChanged_Should_Fire_When_SelectedTime_Set()
  21. {
  22. using (UnitTestApplication.Start(Services))
  23. {
  24. bool handled = false;
  25. TimePicker timePicker = new TimePicker();
  26. timePicker.SelectedTimeChanged += (s, e) =>
  27. {
  28. handled = true;
  29. };
  30. TimeSpan value = TimeSpan.FromHours(10);
  31. timePicker.SelectedTime = value;
  32. Threading.Dispatcher.UIThread.RunJobs();
  33. Assert.True(handled);
  34. }
  35. }
  36. [Fact]
  37. public void Using_24HourClock_Should_Hide_Period()
  38. {
  39. using (UnitTestApplication.Start(Services))
  40. {
  41. TimePicker timePicker = new TimePicker()
  42. {
  43. ClockIdentifier = "12HourClock",
  44. Template = CreateTemplate()
  45. };
  46. timePicker.ApplyTemplate();
  47. var desc = timePicker.GetVisualDescendants();
  48. Assert.True(desc.Count() > 1);//Should be layoutroot grid & button
  49. Grid container = null;
  50. Assert.True(desc.ElementAt(1) is Button);
  51. container = (desc.ElementAt(1) as Button).Content as Grid;
  52. Assert.True(container != null);
  53. var periodTextHost = container.Children[6] as Border;
  54. Assert.True(periodTextHost != null);
  55. Assert.True(periodTextHost.IsVisible);
  56. timePicker.ClockIdentifier = "24HourClock";
  57. Assert.False(periodTextHost.IsVisible);
  58. }
  59. }
  60. [Fact]
  61. public void UseSeconds_Equals_False_Should_Hide_Seconds()
  62. {
  63. using (UnitTestApplication.Start(Services))
  64. {
  65. TimePicker timePicker = new TimePicker()
  66. {
  67. UseSeconds = true,
  68. Template = CreateTemplate()
  69. };
  70. timePicker.ApplyTemplate();
  71. var desc = timePicker.GetVisualDescendants();
  72. Assert.True(desc.Count() > 1);//Should be layoutroot grid & button
  73. Grid container = null;
  74. Assert.True(desc.ElementAt(1) is Button);
  75. container = (desc.ElementAt(1) as Button).Content as Grid;
  76. Assert.True(container != null);
  77. var periodTextHost = container.Children[4] as Border;
  78. Assert.True(periodTextHost != null);
  79. Assert.True(periodTextHost.IsVisible);
  80. timePicker.UseSeconds = false;
  81. Assert.False(periodTextHost.IsVisible);
  82. }
  83. }
  84. [Fact]
  85. public void UseSeconds_Equals_False_Should_Have_Zero_Seconds()
  86. {
  87. using (UnitTestApplication.Start(Services))
  88. {
  89. TimePicker timePicker = new TimePicker()
  90. {
  91. UseSeconds = false,
  92. Template = CreateTemplate(includePopup: true)
  93. };
  94. timePicker.ApplyTemplate();
  95. var desc = timePicker.GetVisualDescendants();
  96. Assert.True(desc.Count() > 2);
  97. // find button
  98. Assert.True(desc.ElementAt(1) is Button);
  99. var btn = (Button)desc.ElementAt(1);
  100. Assert.True(desc.ElementAt(2) is Popup);
  101. var popup = (Popup)desc.ElementAt(2);
  102. Assert.True(popup.Child is TimePickerPresenter);
  103. var timePickerPresenter = (TimePickerPresenter)popup.Child;
  104. var panel = (Panel)timePickerPresenter.VisualChildren[0];
  105. var acceptBtn = (Button)panel.VisualChildren[0];
  106. Assert.False(popup.IsOpen);
  107. btn.PerformClick();
  108. Assert.True(popup.IsOpen);
  109. Assert.False(timePickerPresenter.UseSeconds);
  110. acceptBtn.PerformClick();
  111. Assert.Equal(0, timePickerPresenter.Time.Seconds);
  112. Assert.Equal(0, timePicker.SelectedTime?.Seconds);
  113. }
  114. }
  115. [Fact]
  116. public void TimePickerPresenter_UseSeconds_Equals_False_Should_Have_Zero_Seconds()
  117. {
  118. using (UnitTestApplication.Start(Services))
  119. {
  120. TimePickerPresenter timePickerPresenter = new TimePickerPresenter()
  121. {
  122. UseSeconds = false,
  123. Template = CreatePickerTemplate(),
  124. };
  125. timePickerPresenter.ApplyTemplate();
  126. var panel = (Panel)timePickerPresenter.VisualChildren[0];
  127. var acceptBtn = (Button)panel.VisualChildren[0];
  128. acceptBtn.PerformClick();
  129. Assert.Equal(0, timePickerPresenter.Time.Seconds);
  130. }
  131. }
  132. [Fact]
  133. public void SelectedTime_null_Should_Use_Placeholders()
  134. {
  135. using (UnitTestApplication.Start(Services))
  136. {
  137. TimePicker timePicker = new TimePicker()
  138. {
  139. Template = CreateTemplate()
  140. };
  141. timePicker.ApplyTemplate();
  142. var desc = timePicker.GetVisualDescendants();
  143. Assert.True(desc.Count() > 1);//Should be layoutroot grid & button
  144. Grid container = null;
  145. Assert.True(desc.ElementAt(1) is Button);
  146. container = (desc.ElementAt(1) as Button).Content as Grid;
  147. Assert.True(container != null);
  148. var hourTextHost = container.Children[0] as Border;
  149. Assert.True(hourTextHost != null);
  150. var hourText = hourTextHost.Child as TextBlock;
  151. var minuteTextHost = container.Children[2] as Border;
  152. Assert.True(minuteTextHost != null);
  153. var minuteText = minuteTextHost.Child as TextBlock;
  154. var secondTextHost = container.Children[4] as Border;
  155. Assert.True(secondTextHost != null);
  156. var secondText = secondTextHost.Child as TextBlock;
  157. TimeSpan ts = TimeSpan.FromHours(10);
  158. timePicker.SelectedTime = ts;
  159. Assert.NotNull(hourText.Text);
  160. Assert.NotNull(minuteText.Text);
  161. Assert.NotNull(secondText.Text);
  162. timePicker.SelectedTime = null;
  163. Assert.Null(hourText.Text);
  164. Assert.Null(minuteText.Text);
  165. Assert.Null(secondText.Text);
  166. }
  167. }
  168. [Fact]
  169. [UseEmptyDesignatorCulture]
  170. public void Using_12HourClock_On_Culture_With_Empty_Period_Should_Show_Period()
  171. {
  172. using (UnitTestApplication.Start(Services))
  173. {
  174. TimePicker timePicker = new TimePicker()
  175. {
  176. Template = CreateTemplate(), ClockIdentifier = "12HourClock",
  177. };
  178. timePicker.ApplyTemplate();
  179. var desc = timePicker.GetVisualDescendants();
  180. Assert.True(desc.Count() > 1); //Should be layoutroot grid & button
  181. Assert.True(desc.ElementAt(1) is Button);
  182. var container = (desc.ElementAt(1) as Button).Content as Grid;
  183. Assert.True(container != null);
  184. var periodTextHost = container.Children[6] as Border;
  185. Assert.NotNull(periodTextHost);
  186. var periodText = periodTextHost.Child as TextBlock;
  187. Assert.NotNull(periodTextHost);
  188. TimeSpan ts = TimeSpan.FromHours(10);
  189. timePicker.SelectedTime = ts;
  190. Assert.False(string.IsNullOrEmpty(periodText.Text));
  191. timePicker.SelectedTime = null;
  192. Assert.False(string.IsNullOrEmpty(periodText.Text));
  193. }
  194. }
  195. [Fact]
  196. public void SelectedTime_EnableDataValidation()
  197. {
  198. using (UnitTestApplication.Start(Services))
  199. {
  200. var handled = false;
  201. var timePicker = new TimePicker();
  202. timePicker.SelectedTimeChanged += (s, e) =>
  203. {
  204. var minTime = new TimeSpan(10, 0, 0);
  205. var maxTime = new TimeSpan(15, 0, 0);
  206. if (e.NewTime < minTime)
  207. throw new DataValidationException($"time is less than {maxTime}");
  208. if (e.NewTime > maxTime)
  209. throw new DataValidationException($"time is over {maxTime}");
  210. handled = true;
  211. };
  212. // time is less than
  213. Assert.Throws<DataValidationException>(() => timePicker.SelectedTime = new TimeSpan(1, 2, 3));
  214. // time is over
  215. Assert.Throws<DataValidationException>(() => timePicker.SelectedTime = new TimeSpan(21, 22, 23));
  216. var exception = new DataValidationException("failed validation");
  217. var observable =
  218. new BehaviorSubject<BindingNotification>(new BindingNotification(exception,
  219. BindingErrorType.DataValidationError));
  220. timePicker.Bind(TimePicker.SelectedTimeProperty, observable);
  221. Assert.True(DataValidationErrors.GetHasErrors(timePicker));
  222. Dispatcher.UIThread.RunJobs();
  223. timePicker.SelectedTime = new TimeSpan(11, 12, 13);
  224. Assert.True(handled);
  225. }
  226. }
  227. [Theory]
  228. [InlineData("PART_HourSelector")]
  229. [InlineData("PART_MinuteSelector")]
  230. [InlineData("PART_SecondSelector")]
  231. public void Selector_ScrollUp_Should_Work(string selectorName)
  232. => TestSelectorScrolling(selectorName, panel => panel.ScrollUp());
  233. [Theory]
  234. [InlineData("PART_HourSelector")]
  235. [InlineData("PART_MinuteSelector")]
  236. [InlineData("PART_SecondSelector")]
  237. public void Selector_ScrollDown_Should_Work(string selectorName)
  238. => TestSelectorScrolling(selectorName, panel => panel.ScrollDown());
  239. private static void TestSelectorScrolling(string selectorName, Action<DateTimePickerPanel> scroll)
  240. {
  241. using var app = UnitTestApplication.Start(Services);
  242. var presenter = new TimePickerPresenter { Template = CreatePickerTemplate() };
  243. presenter.ApplyTemplate();
  244. presenter.Measure(new Size(1000, 1000));
  245. var panel = presenter
  246. .GetVisualDescendants()
  247. .OfType<DateTimePickerPanel>()
  248. .FirstOrDefault(panel => panel.Name == selectorName);
  249. Assert.NotNull(panel);
  250. var previousOffset = panel.Offset;
  251. scroll(panel);
  252. Assert.NotEqual(previousOffset, panel.Offset);
  253. }
  254. private static TestServices Services => TestServices.MockThreadingInterface.With(
  255. fontManagerImpl: new HeadlessFontManagerStub(),
  256. standardCursorFactory: Mock.Of<ICursorFactory>(),
  257. textShaperImpl: new HeadlessTextShaperStub(),
  258. renderInterface: new HeadlessPlatformRenderInterface());
  259. private static IControlTemplate CreateTemplate(bool includePopup = false)
  260. {
  261. return new FuncControlTemplate((control, scope) =>
  262. {
  263. var layoutRoot = new Grid
  264. {
  265. Name = "LayoutRoot"
  266. }.RegisterInNameScope(scope);
  267. //Skip contentpresenter
  268. var flyoutButton = new Button
  269. {
  270. Name = "PART_FlyoutButton"
  271. }.RegisterInNameScope(scope);
  272. var contentGrid = new Grid
  273. {
  274. Name = "PART_FlyoutButtonContentGrid"
  275. }.RegisterInNameScope(scope);
  276. var firstPickerHost = new Border
  277. {
  278. Name = "PART_FirstPickerHost",
  279. Child = new TextBlock
  280. {
  281. Name = "PART_HourTextBlock"
  282. }.RegisterInNameScope(scope)
  283. }.RegisterInNameScope(scope);
  284. Grid.SetColumn(firstPickerHost, 0);
  285. var secondPickerHost = new Border
  286. {
  287. Name = "PART_SecondPickerHost",
  288. Child = new TextBlock
  289. {
  290. Name = "PART_MinuteTextBlock"
  291. }.RegisterInNameScope(scope)
  292. }.RegisterInNameScope(scope);
  293. Grid.SetColumn(secondPickerHost, 2);
  294. var thirdPickerHost = new Border
  295. {
  296. Name = "PART_ThirdPickerHost",
  297. Child = new TextBlock
  298. {
  299. Name = "PART_SecondTextBlock"
  300. }.RegisterInNameScope(scope)
  301. }.RegisterInNameScope(scope);
  302. Grid.SetColumn(thirdPickerHost, 4);
  303. var fourthPickerHost = new Border
  304. {
  305. Name = "PART_FourthPickerHost",
  306. Child = new TextBlock
  307. {
  308. Name = "PART_PeriodTextBlock"
  309. }.RegisterInNameScope(scope)
  310. }.RegisterInNameScope(scope);
  311. Grid.SetColumn(fourthPickerHost, 6);
  312. var firstSpacer = new Rectangle
  313. {
  314. Name = "PART_FirstColumnDivider"
  315. }.RegisterInNameScope(scope);
  316. Grid.SetColumn(firstSpacer, 1);
  317. var secondSpacer = new Rectangle
  318. {
  319. Name = "PART_SecondColumnDivider"
  320. }.RegisterInNameScope(scope);
  321. Grid.SetColumn(secondSpacer, 3);
  322. var thirdSpacer = new Rectangle
  323. {
  324. Name = "PART_ThirdColumnDivider"
  325. }.RegisterInNameScope(scope);
  326. Grid.SetColumn(thirdSpacer, 5);
  327. contentGrid.Children.AddRange(new Control[] { firstPickerHost, firstSpacer, secondPickerHost, secondSpacer, thirdPickerHost, thirdSpacer, fourthPickerHost });
  328. flyoutButton.Content = contentGrid;
  329. layoutRoot.Children.Add(flyoutButton);
  330. if (includePopup)
  331. {
  332. var popup = new Popup
  333. {
  334. Name = "PART_Popup"
  335. }.RegisterInNameScope(scope);
  336. var pickerPresenter = new TimePickerPresenter
  337. {
  338. Name = "PART_PickerPresenter",
  339. Template = CreatePickerTemplate()
  340. }.RegisterInNameScope(scope);
  341. pickerPresenter.ApplyTemplate();
  342. popup.Child = pickerPresenter;
  343. layoutRoot.Children.Add(popup);
  344. }
  345. return layoutRoot;
  346. });
  347. }
  348. private static IControlTemplate CreatePickerTemplate()
  349. {
  350. return new FuncControlTemplate((control, scope) =>
  351. {
  352. var acceptButton = new Button
  353. {
  354. Name = "PART_AcceptButton"
  355. }.RegisterInNameScope(scope);
  356. var hourSelector = new DateTimePickerPanel
  357. {
  358. Name = "PART_HourSelector",
  359. PanelType = DateTimePickerPanelType.Hour,
  360. ShouldLoop = true
  361. }.RegisterInNameScope(scope);
  362. var minuteSelector = new DateTimePickerPanel
  363. {
  364. Name = "PART_MinuteSelector",
  365. PanelType = DateTimePickerPanelType.Minute,
  366. ShouldLoop = true
  367. }.RegisterInNameScope(scope);
  368. var secondHost = new Panel
  369. {
  370. Name = "PART_SecondHost"
  371. }.RegisterInNameScope(scope);
  372. var secondSelector = new DateTimePickerPanel
  373. {
  374. Name = "PART_SecondSelector",
  375. PanelType = DateTimePickerPanelType.Second,
  376. ShouldLoop = true
  377. }.RegisterInNameScope(scope);
  378. var periodHost = new Panel
  379. {
  380. Name = "PART_PeriodHost"
  381. }.RegisterInNameScope(scope);
  382. var periodSelector = new DateTimePickerPanel
  383. {
  384. Name = "PART_PeriodSelector",
  385. PanelType = DateTimePickerPanelType.TimePeriod,
  386. }.RegisterInNameScope(scope);
  387. var pickerContainer = new Grid
  388. {
  389. Name = "PART_PickerContainer"
  390. }.RegisterInNameScope(scope);
  391. var secondSpacer = new Rectangle
  392. {
  393. Name = "PART_SecondSpacer"
  394. }.RegisterInNameScope(scope);
  395. var thirdSpacer = new Rectangle
  396. {
  397. Name = "PART_ThirdSpacer"
  398. }.RegisterInNameScope(scope);
  399. var contentPanel = new Panel();
  400. contentPanel.Children.AddRange(new Control[] { acceptButton, hourSelector, minuteSelector, secondHost, secondSelector, periodHost, periodSelector, pickerContainer, secondSpacer, thirdSpacer });
  401. return contentPanel;
  402. });
  403. }
  404. }
  405. }