ToolTipTests.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Reactive;
  4. using System.Runtime.CompilerServices;
  5. using Avalonia.Controls.Primitives;
  6. using Avalonia.Data;
  7. using Avalonia.Input;
  8. using Avalonia.Input.Raw;
  9. using Avalonia.Platform;
  10. using Avalonia.Rendering;
  11. using Avalonia.Threading;
  12. using Avalonia.UnitTests;
  13. using Moq;
  14. using Xunit;
  15. namespace Avalonia.Controls.UnitTests
  16. {
  17. public class ToolTipTests_Popup : ToolTipTests
  18. {
  19. protected override TestServices ConfigureServices(TestServices baseServices) => baseServices;
  20. protected override void SetupWindowMock(Mock<IWindowImpl> windowImpl) { }
  21. protected override void VerifyToolTipType(Control control)
  22. {
  23. var toolTip = control.GetValue(ToolTip.ToolTipProperty);
  24. Assert.IsType<PopupRoot>(toolTip.PopupHost);
  25. Assert.Same(toolTip.VisualRoot, toolTip.PopupHost);
  26. }
  27. }
  28. public class ToolTipTests_Overlay : ToolTipTests, IDisposable
  29. {
  30. private readonly IDisposable _toolTipOpenSubscription;
  31. public ToolTipTests_Overlay()
  32. {
  33. _toolTipOpenSubscription = ToolTip.IsOpenProperty.Changed.Subscribe(new AnonymousObserver<AvaloniaPropertyChangedEventArgs<bool>>(e =>
  34. {
  35. if (e.Sender is Visual { VisualRoot: {} root } visual)
  36. OverlayLayer.GetOverlayLayer(visual).Measure(root.ClientSize);
  37. }));
  38. }
  39. public void Dispose()
  40. {
  41. _toolTipOpenSubscription.Dispose();
  42. }
  43. protected override TestServices ConfigureServices(TestServices baseServices) =>
  44. baseServices.With(windowingPlatform: new MockWindowingPlatform(popupImpl: window => null));
  45. protected override void SetupWindowMock(Mock<IWindowImpl> windowImpl)
  46. {
  47. windowImpl.Setup(x => x.CreatePopup()).Returns(default(IPopupImpl));
  48. }
  49. protected override void VerifyToolTipType(Control control)
  50. {
  51. var toolTip = control.GetValue(ToolTip.ToolTipProperty);
  52. Assert.IsType<OverlayPopupHost>(toolTip.PopupHost);
  53. Assert.Same(toolTip.VisualRoot, control.VisualRoot);
  54. }
  55. }
  56. public abstract class ToolTipTests : ScopedTestBase
  57. {
  58. protected abstract TestServices ConfigureServices(TestServices baseServices);
  59. protected abstract void SetupWindowMock(Mock<IWindowImpl> windowImpl);
  60. protected abstract void VerifyToolTipType(Control control);
  61. private void AssertToolTipOpen(Control control)
  62. {
  63. Assert.True(ToolTip.GetIsOpen(control));
  64. VerifyToolTipType(control);
  65. }
  66. private static readonly MouseDevice s_mouseDevice = new(new Pointer(0, PointerType.Mouse, true));
  67. [Fact]
  68. public void Should_Close_When_Control_Detaches()
  69. {
  70. using (UnitTestApplication.Start(ConfigureServices(TestServices.FocusableWindow)))
  71. {
  72. var panel = new Panel();
  73. var target = new Decorator()
  74. {
  75. [ToolTip.TipProperty] = "Tip",
  76. [ToolTip.ShowDelayProperty] = 0
  77. };
  78. panel.Children.Add(target);
  79. SetupWindowAndActivateToolTip(panel, target);
  80. AssertToolTipOpen(target);
  81. panel.Children.Remove(target);
  82. Assert.False(ToolTip.GetIsOpen(target));
  83. }
  84. }
  85. [Fact]
  86. public void Should_Close_When_Tip_Is_Opened_And_Detached_From_Visual_Tree()
  87. {
  88. using (UnitTestApplication.Start(ConfigureServices(TestServices.FocusableWindow)))
  89. {
  90. var target = new Decorator
  91. {
  92. [!ToolTip.TipProperty] = new Binding("Tip"),
  93. [ToolTip.ShowDelayProperty] = 0,
  94. };
  95. var panel = new Panel();
  96. panel.Children.Add(target);
  97. var mouseEnter = SetupWindowAndGetMouseEnterAction(panel);
  98. panel.DataContext = new ToolTipViewModel();
  99. mouseEnter(target);
  100. AssertToolTipOpen(target);
  101. panel.Children.Remove(target);
  102. Assert.False(ToolTip.GetIsOpen(target));
  103. }
  104. }
  105. [Fact]
  106. public void Should_Open_On_Pointer_Enter()
  107. {
  108. using (UnitTestApplication.Start(ConfigureServices(TestServices.FocusableWindow)))
  109. {
  110. var target = new Decorator()
  111. {
  112. [ToolTip.TipProperty] = "Tip",
  113. [ToolTip.ShowDelayProperty] = 0
  114. };
  115. SetupWindowAndActivateToolTip(target);
  116. AssertToolTipOpen(target);
  117. }
  118. }
  119. [Fact]
  120. public void Content_Should_Update_When_Tip_Property_Changes_And_Already_Open()
  121. {
  122. using (UnitTestApplication.Start(ConfigureServices(TestServices.FocusableWindow)))
  123. {
  124. var target = new Decorator()
  125. {
  126. [ToolTip.TipProperty] = "Tip",
  127. [ToolTip.ShowDelayProperty] = 0
  128. };
  129. SetupWindowAndActivateToolTip(target);
  130. AssertToolTipOpen(target);
  131. Assert.Equal("Tip", target.GetValue(ToolTip.ToolTipProperty).Content);
  132. ToolTip.SetTip(target, "Tip1");
  133. Assert.Equal("Tip1", target.GetValue(ToolTip.ToolTipProperty).Content);
  134. }
  135. }
  136. [Fact]
  137. public void Should_Open_On_Pointer_Enter_With_Delay()
  138. {
  139. using (UnitTestApplication.Start(ConfigureServices(TestServices.FocusableWindow)))
  140. {
  141. var target = new Decorator()
  142. {
  143. [ToolTip.TipProperty] = "Tip",
  144. [ToolTip.ShowDelayProperty] = 1
  145. };
  146. SetupWindowAndActivateToolTip(target);
  147. var timer = Assert.Single(Dispatcher.SnapshotTimersForUnitTests());
  148. Assert.Equal(TimeSpan.FromMilliseconds(1), timer.Interval);
  149. Assert.False(ToolTip.GetIsOpen(target));
  150. timer.ForceFire();
  151. AssertToolTipOpen(target);
  152. }
  153. }
  154. [Fact]
  155. public void Open_Class_Should_Not_Initially_Be_Added()
  156. {
  157. using (UnitTestApplication.Start(ConfigureServices(TestServices.StyledWindow)))
  158. {
  159. var toolTip = new ToolTip();
  160. var window = new Window();
  161. var decorator = new Decorator()
  162. {
  163. [ToolTip.TipProperty] = toolTip
  164. };
  165. window.Content = decorator;
  166. window.ApplyStyling();
  167. window.ApplyTemplate();
  168. window.Presenter.ApplyTemplate();
  169. Assert.Empty(toolTip.Classes);
  170. }
  171. }
  172. [Fact]
  173. public void Setting_IsOpen_Should_Add_Open_Class()
  174. {
  175. using (UnitTestApplication.Start(ConfigureServices(TestServices.StyledWindow)))
  176. {
  177. var toolTip = new ToolTip();
  178. var window = new Window();
  179. var decorator = new Decorator()
  180. {
  181. [ToolTip.TipProperty] = toolTip
  182. };
  183. window.Content = decorator;
  184. window.ApplyStyling();
  185. window.ApplyTemplate();
  186. window.Presenter.ApplyTemplate();
  187. ToolTip.SetIsOpen(decorator, true);
  188. Assert.Equal(new[] { ":open" }, toolTip.Classes);
  189. VerifyToolTipType(decorator);
  190. }
  191. }
  192. [Fact]
  193. public void Clearing_IsOpen_Should_Remove_Open_Class()
  194. {
  195. using (UnitTestApplication.Start(ConfigureServices(TestServices.StyledWindow)))
  196. {
  197. var toolTip = new ToolTip();
  198. var windowImpl = MockWindowingPlatform.CreateWindowMock();
  199. SetupWindowMock(windowImpl);
  200. var window = new Window(windowImpl.Object);
  201. var decorator = new Decorator()
  202. {
  203. [ToolTip.TipProperty] = toolTip
  204. };
  205. window.Content = decorator;
  206. window.ApplyStyling();
  207. window.ApplyTemplate();
  208. window.Presenter.ApplyTemplate();
  209. ToolTip.SetIsOpen(decorator, true);
  210. AssertToolTipOpen(decorator);
  211. ToolTip.SetIsOpen(decorator, false);
  212. Assert.Empty(toolTip.Classes);
  213. }
  214. }
  215. [Fact]
  216. public void Should_Close_On_Null_Tip()
  217. {
  218. using (UnitTestApplication.Start(ConfigureServices(TestServices.FocusableWindow)))
  219. {
  220. var target = new Decorator()
  221. {
  222. [ToolTip.TipProperty] = "Tip",
  223. [ToolTip.ShowDelayProperty] = 0
  224. };
  225. SetupWindowAndActivateToolTip(target);
  226. AssertToolTipOpen(target);
  227. target[ToolTip.TipProperty] = null;
  228. Assert.False(ToolTip.GetIsOpen(target));
  229. }
  230. }
  231. [Fact]
  232. public void Should_Not_Close_When_Pointer_Is_Moved_Over_ToolTip()
  233. {
  234. using (UnitTestApplication.Start(ConfigureServices(TestServices.FocusableWindow)))
  235. {
  236. var target = new Decorator()
  237. {
  238. [ToolTip.TipProperty] = "Tip",
  239. [ToolTip.ShowDelayProperty] = 0
  240. };
  241. var mouseEnter = SetupWindowAndGetMouseEnterAction(target);
  242. mouseEnter(target);
  243. AssertToolTipOpen(target);
  244. var tooltip = Assert.IsType<ToolTip>(target.GetValue(ToolTip.ToolTipProperty));
  245. mouseEnter(tooltip);
  246. AssertToolTipOpen(target);
  247. }
  248. }
  249. [Fact]
  250. public void Should_Not_Close_When_Pointer_Is_Moved_From_ToolTip_To_Original_Control()
  251. {
  252. using (UnitTestApplication.Start(ConfigureServices(TestServices.FocusableWindow)))
  253. {
  254. var target = new Decorator()
  255. {
  256. [ToolTip.TipProperty] = "Tip",
  257. [ToolTip.ShowDelayProperty] = 0
  258. };
  259. var mouseEnter = SetupWindowAndGetMouseEnterAction(target);
  260. mouseEnter(target);
  261. AssertToolTipOpen(target);
  262. var tooltip = Assert.IsType<ToolTip>(target.GetValue(ToolTip.ToolTipProperty));
  263. mouseEnter(tooltip);
  264. AssertToolTipOpen(target);
  265. mouseEnter(target);
  266. AssertToolTipOpen(target);
  267. }
  268. }
  269. [Fact]
  270. public void Should_Close_When_Pointer_Is_Moved_From_ToolTip_To_Another_Control()
  271. {
  272. using (UnitTestApplication.Start(ConfigureServices(TestServices.FocusableWindow)))
  273. {
  274. var target = new Decorator()
  275. {
  276. [ToolTip.TipProperty] = "Tip",
  277. [ToolTip.ShowDelayProperty] = 0
  278. };
  279. var other = new Decorator();
  280. var panel = new StackPanel
  281. {
  282. Children = { target, other }
  283. };
  284. var mouseEnter = SetupWindowAndGetMouseEnterAction(panel);
  285. mouseEnter(target);
  286. AssertToolTipOpen(target);
  287. var tooltip = Assert.IsType<ToolTip>(target.GetValue(ToolTip.ToolTipProperty));
  288. mouseEnter(tooltip);
  289. AssertToolTipOpen(target);
  290. mouseEnter(other);
  291. Assert.False(ToolTip.GetIsOpen(target));
  292. }
  293. }
  294. [Fact]
  295. public void New_ToolTip_Replaces_Other_ToolTip_Immediately()
  296. {
  297. using var app = UnitTestApplication.Start(ConfigureServices(TestServices.FocusableWindow));
  298. var target = new Decorator()
  299. {
  300. [ToolTip.TipProperty] = "Tip",
  301. [ToolTip.ShowDelayProperty] = 0
  302. };
  303. var other = new Decorator()
  304. {
  305. [ToolTip.TipProperty] = "Tip",
  306. [ToolTip.ShowDelayProperty] = (int) TimeSpan.FromHours(1).TotalMilliseconds,
  307. };
  308. var panel = new StackPanel
  309. {
  310. Children = { target, other }
  311. };
  312. var mouseEnter = SetupWindowAndGetMouseEnterAction(panel);
  313. mouseEnter(other);
  314. Assert.False(ToolTip.GetIsOpen(other)); // long delay
  315. mouseEnter(target);
  316. AssertToolTipOpen(target); // no delay
  317. mouseEnter(other);
  318. Assert.True(ToolTip.GetIsOpen(other)); // delay skipped, a tooltip was already open
  319. // Now disable the between-show system
  320. mouseEnter(target);
  321. AssertToolTipOpen(target);
  322. ToolTip.SetBetweenShowDelay(other, -1);
  323. mouseEnter(other);
  324. Assert.False(ToolTip.GetIsOpen(other));
  325. }
  326. [Fact]
  327. public void ToolTip_Events_Order_Is_Defined()
  328. {
  329. using var app = UnitTestApplication.Start(ConfigureServices(TestServices.FocusableWindow));
  330. var tip = new ToolTip() { Content = "Tip" };
  331. var target = new Decorator()
  332. {
  333. [ToolTip.TipProperty] = tip,
  334. [ToolTip.ShowDelayProperty] = 0
  335. };
  336. var eventsOrder = new List<(string eventName, object sender, object source)>();
  337. ToolTip.AddToolTipOpeningHandler(target,
  338. (sender, args) => eventsOrder.Add(("Opening", sender, args.Source)));
  339. ToolTip.AddToolTipClosingHandler(target,
  340. (sender, args) => eventsOrder.Add(("Closing", sender, args.Source)));
  341. SetupWindowAndActivateToolTip(target);
  342. AssertToolTipOpen(target);
  343. target[ToolTip.TipProperty] = null;
  344. Assert.False(ToolTip.GetIsOpen(target));
  345. Assert.Equal(
  346. new[]
  347. {
  348. ("Opening", (object)target, (object)target),
  349. ("Closing", target, target)
  350. },
  351. eventsOrder);
  352. }
  353. [Fact]
  354. public void ToolTip_Is_Not_Opened_If_Opening_Event_Handled()
  355. {
  356. using var app = UnitTestApplication.Start(ConfigureServices(TestServices.FocusableWindow));
  357. var tip = new ToolTip() { Content = "Tip" };
  358. var target = new Decorator()
  359. {
  360. [ToolTip.TipProperty] = tip,
  361. [ToolTip.ShowDelayProperty] = 0
  362. };
  363. ToolTip.AddToolTipOpeningHandler(target,
  364. (sender, args) => args.Cancel = true);
  365. SetupWindowAndActivateToolTip(target);
  366. Assert.False(ToolTip.GetIsOpen(target));
  367. }
  368. [Fact]
  369. public void ToolTip_Can_Be_Replaced_On_The_Fly_Via_Opening_Event()
  370. {
  371. using var app = UnitTestApplication.Start(ConfigureServices(TestServices.FocusableWindow));
  372. var tip1 = new ToolTip() { Content = "Hi" };
  373. var tip2 = new ToolTip() { Content = "Bye" };
  374. var target = new Decorator()
  375. {
  376. [ToolTip.TipProperty] = tip1,
  377. [ToolTip.ShowDelayProperty] = 0
  378. };
  379. ToolTip.AddToolTipOpeningHandler(target,
  380. (sender, args) => target[ToolTip.TipProperty] = tip2);
  381. SetupWindowAndActivateToolTip(target);
  382. AssertToolTipOpen(target);
  383. target[ToolTip.TipProperty] = null;
  384. Assert.False(ToolTip.GetIsOpen(target));
  385. }
  386. [Fact]
  387. public void Should_Close_When_Pointer_Leaves_Window()
  388. {
  389. using (UnitTestApplication.Start(TestServices.FocusableWindow))
  390. {
  391. var target = new Decorator()
  392. {
  393. [ToolTip.TipProperty] = "Tip",
  394. [ToolTip.ShowDelayProperty] = 0
  395. };
  396. var mouseEnter = SetupWindowAndGetMouseEnterAction(target);
  397. mouseEnter(target);
  398. AssertToolTipOpen(target);
  399. var topLevel = TopLevel.GetTopLevel(target);
  400. topLevel.PlatformImpl.Input(new RawPointerEventArgs(s_mouseDevice, (ulong)DateTime.Now.Ticks, topLevel,
  401. RawPointerEventType.LeaveWindow, default(RawPointerPoint), RawInputModifiers.None));
  402. Assert.False(ToolTip.GetIsOpen(target));
  403. }
  404. }
  405. private Action<Control> SetupWindowAndGetMouseEnterAction(Control windowContent, [CallerMemberName] string testName = null)
  406. {
  407. var windowImpl = MockWindowingPlatform.CreateWindowMock();
  408. SetupWindowMock(windowImpl);
  409. var hitTesterMock = new Mock<IHitTester>();
  410. var window = new Window(windowImpl.Object)
  411. {
  412. HitTesterOverride = hitTesterMock.Object,
  413. Content = windowContent,
  414. Title = testName,
  415. };
  416. window.ApplyStyling();
  417. window.ApplyTemplate();
  418. window.Presenter.ApplyTemplate();
  419. window.Show();
  420. Assert.True(windowContent.IsAttachedToVisualTree);
  421. Assert.True(windowContent.IsMeasureValid);
  422. Assert.True(windowContent.IsVisible);
  423. var controlIds = new Dictionary<Control, int>();
  424. IInputRoot lastRoot = null;
  425. return control =>
  426. {
  427. Point point;
  428. if (control == null)
  429. {
  430. point = default;
  431. }
  432. else
  433. {
  434. if (!controlIds.TryGetValue(control, out int id))
  435. {
  436. id = controlIds[control] = controlIds.Count;
  437. }
  438. point = new Point(id, int.MaxValue);
  439. }
  440. hitTesterMock.Setup(m => m.HitTestFirst(point, window, It.IsAny<Func<Visual, bool>>()))
  441. .Returns(control);
  442. var root = (IInputRoot)control?.VisualRoot ?? window;
  443. var timestamp = (ulong)DateTime.Now.Ticks;
  444. windowImpl.Object.Input(new RawPointerEventArgs(s_mouseDevice, timestamp, root,
  445. RawPointerEventType.Move, point, RawInputModifiers.None));
  446. if (lastRoot != null && lastRoot != root)
  447. {
  448. ((TopLevel)lastRoot).PlatformImpl?.Input(new RawPointerEventArgs(s_mouseDevice, timestamp,
  449. lastRoot, RawPointerEventType.LeaveWindow, new Point(-1,-1), RawInputModifiers.None));
  450. }
  451. lastRoot = root;
  452. Assert.True(control == null || control.IsPointerOver);
  453. };
  454. }
  455. private void SetupWindowAndActivateToolTip(Control windowContent, Control targetOverride = null, [CallerMemberName] string testName = null) =>
  456. SetupWindowAndGetMouseEnterAction(windowContent, testName)(targetOverride ?? windowContent);
  457. }
  458. internal class ToolTipViewModel
  459. {
  460. public string Tip => "Tip";
  461. }
  462. }