A game framework written with osu! in mind.
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2// See the LICENCE file in the repository root for full licence text.
3
4using System;
5using System.Collections.Generic;
6using System.Linq;
7using NUnit.Framework;
8using osu.Framework.Graphics;
9using osu.Framework.Graphics.UserInterface;
10using osu.Framework.Testing;
11using osuTK;
12using osuTK.Input;
13
14namespace osu.Framework.Tests.Visual.UserInterface
15{
16 public class TestSceneNestedMenus : MenuTestScene
17 {
18 private const int max_depth = 5;
19 private const int max_count = 5;
20
21 private Random rng;
22
23 [SetUp]
24 public new void SetUp() => rng = new Random(1337);
25
26 protected override Menu CreateMenu() => new ClickOpenMenu(TimePerAction)
27 {
28 Anchor = Anchor.Centre,
29 Origin = Anchor.Centre,
30 Items = new[]
31 {
32 generateRandomMenuItem("First"),
33 generateRandomMenuItem("Second"),
34 generateRandomMenuItem("Third"),
35 }
36 };
37
38 private class ClickOpenMenu : BasicMenu
39 {
40 protected override Menu CreateSubMenu() => new ClickOpenMenu(HoverOpenDelay, false);
41
42 public ClickOpenMenu(double timePerAction, bool topLevel = true)
43 : base(Direction.Vertical, topLevel)
44 {
45 HoverOpenDelay = timePerAction;
46 }
47 }
48
49 #region Test Cases
50
51 /// <summary>
52 /// Tests if the <see cref="Menu"/> respects <see cref="Menu.TopLevelMenu"/> = true, by not alowing it to be closed
53 /// when a click happens outside the <see cref="Menu"/>.
54 /// </summary>
55 [Test]
56 public void TestAlwaysOpen()
57 {
58 AddStep("Click outside", () => InputManager.Click(MouseButton.Left));
59 AddAssert("Check AlwaysOpen = true", () => Menus.GetSubMenu(0).State == MenuState.Open);
60 }
61
62 /// <summary>
63 /// Tests if the hover state on <see cref="Menu.DrawableMenuItem"/>s is valid.
64 /// </summary>
65 [Test]
66 public void TestHoverState()
67 {
68 AddAssert("Check submenu closed", () => Menus.GetSubMenu(1)?.State != MenuState.Open);
69 AddStep("Hover item", () => InputManager.MoveMouseTo(Menus.GetMenuItems()[0]));
70 AddAssert("Check item hovered", () => Menus.GetMenuItems()[0].IsHovered);
71 }
72
73 /// <summary>
74 /// Tests if the <see cref="Menu"/> respects <see cref="Menu.TopLevelMenu"/> = true.
75 /// </summary>
76 [Test]
77 public void TestTopLevelMenu()
78 {
79 AddStep("Hover item", () => InputManager.MoveMouseTo(Menus.GetSubStructure(0).GetMenuItems()[0]));
80 AddAssert("Check closed", () => Menus.GetSubMenu(1)?.State != MenuState.Open);
81 AddAssert("Check closed", () => Menus.GetSubMenu(1)?.State != MenuState.Open);
82 AddStep("Click item", () => InputManager.Click(MouseButton.Left));
83 AddAssert("Check open", () => Menus.GetSubMenu(1).State == MenuState.Open);
84 }
85
86 /// <summary>
87 /// Tests if clicking once on a menu that has <see cref="Menu.TopLevelMenu"/> opens it, and clicking a second time
88 /// closes it.
89 /// </summary>
90 [Test]
91 public void TestDoubleClick()
92 {
93 AddStep("Click item", () => ClickItem(0, 0));
94 AddAssert("Check open", () => Menus.GetSubMenu(1).State == MenuState.Open);
95 AddStep("Click item", () => ClickItem(0, 0));
96 AddAssert("Check closed", () => Menus.GetSubMenu(1)?.State != MenuState.Open);
97 }
98
99 /// <summary>
100 /// Tests whether click on <see cref="Menu.DrawableMenuItem"/>s causes sub-menus to instantly appear.
101 /// </summary>
102 [Test]
103 public void TestInstantOpen()
104 {
105 AddStep("Click item", () => ClickItem(0, 1));
106 AddAssert("Check open", () => Menus.GetSubMenu(1).State == MenuState.Open);
107 AddStep("Click item", () => ClickItem(1, 0));
108 AddAssert("Check open", () => Menus.GetSubMenu(2).State == MenuState.Open);
109 }
110
111 /// <summary>
112 /// Tests if clicking on an item that has no sub-menu causes the menu to close.
113 /// </summary>
114 [Test]
115 public void TestActionClick()
116 {
117 AddStep("Click item", () => ClickItem(0, 0));
118 AddStep("Click item", () => ClickItem(1, 0));
119 AddAssert("Check closed", () => Menus.GetSubMenu(1)?.State != MenuState.Open);
120 }
121
122 /// <summary>
123 /// Tests if hovering over menu items respects the <see cref="Menu.HoverOpenDelay"/>.
124 /// </summary>
125 [Test]
126 public void TestHoverOpen()
127 {
128 AddStep("Click item", () => ClickItem(0, 1));
129 AddStep("Hover item", () => InputManager.MoveMouseTo(Menus.GetSubStructure(1).GetMenuItems()[0]));
130 AddAssert("Check closed", () => Menus.GetSubMenu(2)?.State != MenuState.Open);
131 AddAssert("Check open", () => Menus.GetSubMenu(2).State == MenuState.Open);
132 AddStep("Hover item", () => InputManager.MoveMouseTo(Menus.GetSubStructure(2).GetMenuItems()[0]));
133 AddAssert("Check closed", () => Menus.GetSubMenu(3)?.State != MenuState.Open);
134 AddAssert("Check open", () => Menus.GetSubMenu(3).State == MenuState.Open);
135 }
136
137 /// <summary>
138 /// Tests if hovering over a different item on the main <see cref="Menu"/> will instantly open another menu
139 /// and correctly changes the sub-menu items to the new items from the hovered item.
140 /// </summary>
141 [Test]
142 public void TestHoverChange()
143 {
144 IReadOnlyList<MenuItem> currentItems = null;
145 AddStep("Click item", () => { ClickItem(0, 0); });
146
147 AddStep("Get items", () => { currentItems = Menus.GetSubMenu(1).Items; });
148
149 AddAssert("Check open", () => Menus.GetSubMenu(1).State == MenuState.Open);
150 AddStep("Hover item", () => InputManager.MoveMouseTo(Menus.GetSubStructure(0).GetMenuItems()[1]));
151 AddAssert("Check open", () => Menus.GetSubMenu(1).State == MenuState.Open);
152
153 AddAssert("Check new items", () => !Menus.GetSubMenu(1).Items.SequenceEqual(currentItems));
154 AddAssert("Check closed", () =>
155 {
156 int currentSubMenu = 3;
157
158 while (true)
159 {
160 var subMenu = Menus.GetSubMenu(currentSubMenu);
161 if (subMenu == null)
162 break;
163
164 if (subMenu.State == MenuState.Open)
165 return false;
166
167 currentSubMenu++;
168 }
169
170 return true;
171 });
172 }
173
174 /// <summary>
175 /// Tests whether hovering over a different item on a sub-menu opens a new sub-menu in a delayed fashion
176 /// and correctly changes the sub-menu items to the new items from the hovered item.
177 /// </summary>
178 [Test]
179 public void TestDelayedHoverChange()
180 {
181 AddStep("Click item", () => ClickItem(0, 2));
182 AddStep("Hover item", () => InputManager.MoveMouseTo(Menus.GetSubStructure(1).GetMenuItems()[0]));
183 AddAssert("Check closed", () => Menus.GetSubMenu(2)?.State != MenuState.Open);
184 AddAssert("Check closed", () => Menus.GetSubMenu(2)?.State != MenuState.Open);
185
186 AddStep("Hover item", () => { InputManager.MoveMouseTo(Menus.GetSubStructure(1).GetMenuItems()[1]); });
187
188 AddAssert("Check closed", () => Menus.GetSubMenu(2)?.State != MenuState.Open);
189 AddAssert("Check open", () => Menus.GetSubMenu(2).State == MenuState.Open);
190
191 AddAssert("Check closed", () =>
192 {
193 int currentSubMenu = 3;
194
195 while (true)
196 {
197 var subMenu = Menus.GetSubMenu(currentSubMenu);
198 if (subMenu == null)
199 break;
200
201 if (subMenu.State == MenuState.Open)
202 return false;
203
204 currentSubMenu++;
205 }
206
207 return true;
208 });
209 }
210
211 /// <summary>
212 /// Tests whether clicking on <see cref="Menu"/>s that have opened sub-menus don't close the sub-menus.
213 /// Then tests hovering in reverse order to make sure only the lower level menus close.
214 /// </summary>
215 [Test]
216 public void TestMenuClicksDontClose()
217 {
218 AddStep("Click item", () => ClickItem(0, 1));
219 AddStep("Click item", () => ClickItem(1, 0));
220 AddStep("Click item", () => ClickItem(2, 0));
221 AddStep("Click item", () => ClickItem(3, 0));
222
223 for (int i = 3; i >= 1; i--)
224 {
225 int menuIndex = i;
226 AddStep("Hover item", () => InputManager.MoveMouseTo(Menus.GetSubStructure(menuIndex).GetMenuItems()[0]));
227 AddAssert("Check submenu open", () => Menus.GetSubMenu(menuIndex + 1).State == MenuState.Open);
228 AddStep("Click item", () => InputManager.Click(MouseButton.Left));
229 AddAssert("Check all open", () =>
230 {
231 for (int j = 0; j <= menuIndex; j++)
232 {
233 int menuIndex2 = j;
234 if (Menus.GetSubMenu(menuIndex2)?.State != MenuState.Open)
235 return false;
236 }
237
238 return true;
239 });
240 }
241 }
242
243 /// <summary>
244 /// Tests whether clicking on the <see cref="Menu"/> that has <see cref="Menu.TopLevelMenu"/> closes all sub menus.
245 /// </summary>
246 [Test]
247 public void TestMenuClickClosesSubMenus()
248 {
249 AddStep("Click item", () => ClickItem(0, 1));
250 AddStep("Click item", () => ClickItem(1, 0));
251 AddStep("Click item", () => ClickItem(2, 0));
252 AddStep("Click item", () => ClickItem(3, 0));
253 AddStep("Click item", () => ClickItem(0, 1));
254
255 AddAssert("Check submenus closed", () =>
256 {
257 for (int j = 1; j <= 3; j++)
258 {
259 int menuIndex2 = j;
260 if (Menus.GetSubMenu(menuIndex2).State == MenuState.Open)
261 return false;
262 }
263
264 return true;
265 });
266 }
267
268 /// <summary>
269 /// Tests whether clicking on an action in a sub-menu closes all <see cref="Menu"/>s.
270 /// </summary>
271 [Test]
272 public void TestActionClickClosesMenus()
273 {
274 AddStep("Click item", () => ClickItem(0, 1));
275 AddStep("Click item", () => ClickItem(1, 0));
276 AddStep("Click item", () => ClickItem(2, 0));
277 AddStep("Click item", () => ClickItem(3, 0));
278 AddStep("Click item", () => ClickItem(4, 0));
279
280 AddAssert("Check submenus closed", () =>
281 {
282 for (int j = 1; j <= 3; j++)
283 {
284 int menuIndex2 = j;
285 if (Menus.GetSubMenu(menuIndex2).State == MenuState.Open)
286 return false;
287 }
288
289 return true;
290 });
291 }
292
293 /// <summary>
294 /// Tests whether clicking outside the <see cref="Menu"/> structure closes all sub-menus.
295 /// </summary>
296 /// <param name="hoverPrevious">Whether the previous menu should first be hovered before clicking outside.</param>
297 [TestCase(false)]
298 [TestCase(true)]
299 public void TestClickingOutsideClosesMenus(bool hoverPrevious)
300 {
301 for (int i = 0; i <= 3; i++)
302 {
303 int i2 = i;
304
305 for (int j = 0; j <= i; j++)
306 {
307 int menuToOpen = j;
308 int itemToOpen = menuToOpen == 0 ? 1 : 0;
309 AddStep("Click item", () => ClickItem(menuToOpen, itemToOpen));
310 }
311
312 if (hoverPrevious && i > 0)
313 AddStep("Hover previous", () => InputManager.MoveMouseTo(Menus.GetSubStructure(i2 - 1).GetMenuItems()[i2 > 1 ? 0 : 1]));
314
315 AddStep("Remove hover", () => InputManager.MoveMouseTo(Vector2.Zero));
316 AddStep("Click outside", () => InputManager.Click(MouseButton.Left));
317 AddAssert("Check submenus closed", () =>
318 {
319 for (int j = 1; j <= i2 + 1; j++)
320 {
321 int menuIndex2 = j;
322 if (Menus.GetSubMenu(menuIndex2).State == MenuState.Open)
323 return false;
324 }
325
326 return true;
327 });
328 }
329 }
330
331 /// <summary>
332 /// Opens some menus and then changes the selected item.
333 /// </summary>
334 [Test]
335 public void TestSelectedState()
336 {
337 AddStep("Click item", () => ClickItem(0, 2));
338 AddAssert("Check open", () => Menus.GetSubMenu(1).State == MenuState.Open);
339
340 AddStep("Hover item", () => InputManager.MoveMouseTo(Menus.GetSubStructure(1).GetMenuItems()[1]));
341 AddAssert("Check closed 1", () => Menus.GetSubMenu(2)?.State != MenuState.Open);
342 AddAssert("Check open", () => Menus.GetSubMenu(2).State == MenuState.Open);
343 AddAssert("Check selected index 1", () => Menus.GetSubStructure(1).GetSelectedIndex() == 1);
344
345 AddStep("Change selection", () => Menus.GetSubStructure(1).SetSelectedState(0, MenuItemState.Selected));
346 AddAssert("Check selected index", () => Menus.GetSubStructure(1).GetSelectedIndex() == 0);
347
348 AddStep("Change selection", () => Menus.GetSubStructure(1).SetSelectedState(2, MenuItemState.Selected));
349 AddAssert("Check selected index 2", () => Menus.GetSubStructure(1).GetSelectedIndex() == 2);
350
351 AddStep("Close menus", () => Menus.GetSubMenu(0).Close());
352 AddAssert("Check selected index 4", () => Menus.GetSubStructure(1).GetSelectedIndex() == -1);
353 }
354
355 #endregion
356
357 private MenuItem generateRandomMenuItem(string name = "Menu Item", int currDepth = 1)
358 {
359 var item = new MenuItem(name);
360
361 if (currDepth == max_depth)
362 return item;
363
364 int subCount = rng.Next(0, max_count);
365 var subItems = new List<MenuItem>();
366 for (int i = 0; i < subCount; i++)
367 subItems.Add(generateRandomMenuItem(item.Text + $" #{i + 1}", currDepth + 1));
368
369 item.Items = subItems;
370 return item;
371 }
372 }
373}