A game framework written with osu! in mind.
at master 403 lines 19 kB view raw
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.Linq; 6using NUnit.Framework; 7using osu.Framework.Bindables; 8using osu.Framework.Graphics; 9using osu.Framework.Graphics.UserInterface; 10using osu.Framework.Input; 11using osu.Framework.Input.Events; 12using osu.Framework.Input.States; 13using osu.Framework.Testing; 14using osuTK; 15using osuTK.Input; 16 17namespace osu.Framework.Tests.Visual.UserInterface 18{ 19 public class TestSceneDropdown : ManualInputManagerTestScene 20 { 21 private const int items_to_add = 10; 22 private const float explicit_height = 100; 23 private float calculatedHeight; 24 private readonly TestDropdown testDropdown, testDropdownMenu, bindableDropdown, emptyDropdown, disabledDropdown; 25 private readonly PlatformActionContainer platformActionContainerKeyboardSelection, platformActionContainerKeyboardPreselection, platformActionContainerEmptyDropdown; 26 private readonly BindableList<TestModel> bindableList = new BindableList<TestModel>(); 27 28 private int previousIndex; 29 private int lastVisibleIndexOnTheCurrentPage, lastVisibleIndexOnTheNextPage; 30 private int firstVisibleIndexOnTheCurrentPage, firstVisibleIndexOnThePreviousPage; 31 32 public TestSceneDropdown() 33 { 34 var testItems = new TestModel[10]; 35 int i = 0; 36 while (i < items_to_add) 37 testItems[i] = @"test " + i++; 38 39 Add(platformActionContainerKeyboardSelection = new PlatformActionContainer 40 { 41 Child = testDropdown = new TestDropdown 42 { 43 Width = 150, 44 Position = new Vector2(50, 50), 45 Items = testItems 46 } 47 }); 48 49 Add(platformActionContainerKeyboardPreselection = new PlatformActionContainer 50 { 51 Child = testDropdownMenu = new TestDropdown 52 { 53 Width = 150, 54 Position = new Vector2(250, 50), 55 Items = testItems 56 } 57 }); 58 testDropdownMenu.Menu.MaxHeight = explicit_height; 59 60 Add(bindableDropdown = new TestDropdown 61 { 62 Width = 150, 63 Position = new Vector2(450, 50), 64 ItemSource = bindableList 65 }); 66 67 Add(platformActionContainerEmptyDropdown = new PlatformActionContainer 68 { 69 Child = emptyDropdown = new TestDropdown 70 { 71 Width = 150, 72 Position = new Vector2(650, 50), 73 } 74 }); 75 76 Add(disabledDropdown = new TestDropdown 77 { 78 Width = 150, 79 Position = new Vector2(50, 350), 80 Items = testItems, 81 Current = 82 { 83 Value = testItems[3], 84 Disabled = true 85 } 86 }); 87 } 88 89 [Test] 90 public void TestExternalBindableChangeKeepsSelection() 91 { 92 toggleDropdownViaClick(testDropdown, "dropdown1"); 93 AddStep("click item 4", () => testDropdown.SelectItem(testDropdown.Menu.Items[4])); 94 95 AddAssert("item 4 is selected", () => testDropdown.Current.Value.Identifier == "test 4"); 96 97 AddStep("replace items", () => 98 { 99 testDropdown.Items = testDropdown.Items.Select(i => new TestModel(i.ToString())).ToArray(); 100 }); 101 102 AddAssert("item 4 is selected", () => testDropdown.Current.Value.Identifier == "test 4"); 103 } 104 105 [Test] 106 public void TestBasic() 107 { 108 var i = items_to_add; 109 110 toggleDropdownViaClick(testDropdown, "dropdown1"); 111 AddAssert("dropdown is open", () => testDropdown.Menu.State == MenuState.Open); 112 113 AddRepeatStep("add item", () => testDropdown.AddDropdownItem("test " + i++), items_to_add); 114 AddAssert("item count is correct", () => testDropdown.Items.Count() == items_to_add * 2); 115 116 AddStep($"Set dropdown1 height to {explicit_height}", () => 117 { 118 calculatedHeight = testDropdown.Menu.Height; 119 testDropdown.Menu.MaxHeight = explicit_height; 120 }); 121 AddAssert($"dropdown1 height is {explicit_height}", () => testDropdown.Menu.Height == explicit_height); 122 123 AddStep($"Set dropdown1 height to {float.PositiveInfinity}", () => testDropdown.Menu.MaxHeight = float.PositiveInfinity); 124 AddAssert("dropdown1 height is calculated automatically", () => testDropdown.Menu.Height == calculatedHeight); 125 126 AddStep("click item 13", () => testDropdown.SelectItem(testDropdown.Menu.Items[13])); 127 128 AddAssert("dropdown1 is closed", () => testDropdown.Menu.State == MenuState.Closed); 129 AddAssert("item 13 is selected", () => testDropdown.Current.Value.Equals(testDropdown.Items.ElementAt(13))); 130 131 AddStep("select item 15", () => testDropdown.Current.Value = testDropdown.Items.ElementAt(15)); 132 AddAssert("item 15 is selected", () => testDropdown.Current.Value.Equals(testDropdown.Items.ElementAt(15))); 133 134 toggleDropdownViaClick(testDropdown, "dropdown1"); 135 AddAssert("dropdown1 is open", () => testDropdown.Menu.State == MenuState.Open); 136 137 toggleDropdownViaClick(testDropdownMenu, "dropdown2"); 138 139 AddAssert("dropdown1 is closed", () => testDropdown.Menu.State == MenuState.Closed); 140 AddAssert("dropdown2 is open", () => testDropdownMenu.Menu.State == MenuState.Open); 141 142 AddStep("select 'invalid'", () => testDropdown.Current.Value = "invalid"); 143 144 AddAssert("'invalid' is selected", () => testDropdown.Current.Value.Identifier == "invalid"); 145 AddAssert("label shows 'invalid'", () => testDropdown.Header.Label.ToString() == "invalid"); 146 147 AddStep("select item 2", () => testDropdown.Current.Value = testDropdown.Items.ElementAt(2)); 148 AddAssert("item 2 is selected", () => testDropdown.Current.Value.Equals(testDropdown.Items.ElementAt(2))); 149 150 AddStep("clear bindable list", () => bindableList.Clear()); 151 toggleDropdownViaClick(bindableDropdown, "dropdown3"); 152 AddAssert("no elements in bindable dropdown", () => !bindableDropdown.Items.Any()); 153 154 AddStep("add items to bindable", () => bindableList.AddRange(new[] { "one", "two", "three" }.Select(s => new TestModel(s)))); 155 AddStep("select three", () => bindableDropdown.Current.Value = "three"); 156 AddStep("remove first item from bindable", () => bindableList.RemoveAt(0)); 157 AddAssert("two items in dropdown", () => bindableDropdown.Items.Count() == 2); 158 AddAssert("current value still three", () => bindableDropdown.Current.Value.Identifier == "three"); 159 160 AddStep("remove three", () => bindableList.Remove("three")); 161 AddAssert("current value should be two", () => bindableDropdown.Current.Value.Identifier == "two"); 162 } 163 164 private void performKeypress(Drawable drawable, Key key) 165 { 166 drawable.TriggerEvent(new KeyDownEvent(new InputState(), key)); 167 drawable.TriggerEvent(new KeyUpEvent(new InputState(), key)); 168 } 169 170 private void performPlatformAction(PlatformAction action, PlatformActionContainer platformActionContainer, Drawable drawable) 171 { 172 var tempIsHovered = drawable.IsHovered; 173 var tempHasFocus = drawable.HasFocus; 174 175 drawable.IsHovered = true; 176 drawable.HasFocus = true; 177 178 platformActionContainer.TriggerPressed(action); 179 platformActionContainer.TriggerReleased(action); 180 181 drawable.IsHovered = tempIsHovered; 182 drawable.HasFocus = tempHasFocus; 183 } 184 185 [TestCase(false)] 186 [TestCase(true)] 187 public void TestKeyboardSelection(bool cleanSelection) 188 { 189 if (cleanSelection) 190 AddStep("Clean selection", () => testDropdown.Current.Value = null); 191 192 AddStep("Select next item", () => 193 { 194 previousIndex = testDropdown.SelectedIndex; 195 performKeypress(testDropdown.Header, Key.Down); 196 }); 197 AddAssert("Next item is selected", () => testDropdown.SelectedIndex == previousIndex + 1); 198 199 AddStep("Select previous item", () => 200 { 201 previousIndex = testDropdown.SelectedIndex; 202 performKeypress(testDropdown.Header, Key.Up); 203 }); 204 AddAssert("Previous item is selected", () => testDropdown.SelectedIndex == Math.Max(0, previousIndex - 1)); 205 206 AddStep("Select last item", 207 () => performPlatformAction(PlatformAction.MoveToListEnd, platformActionContainerKeyboardSelection, testDropdown.Header)); 208 AddAssert("Last item selected", () => testDropdown.SelectedItem == testDropdown.Menu.DrawableMenuItems.Last().Item); 209 210 AddStep("Select first item", 211 () => performPlatformAction(PlatformAction.MoveToListStart, platformActionContainerKeyboardSelection, testDropdown.Header)); 212 AddAssert("First item selected", () => testDropdown.SelectedItem == testDropdown.Menu.DrawableMenuItems.First().Item); 213 214 AddStep("Select next item when empty", () => performKeypress(emptyDropdown.Header, Key.Up)); 215 AddStep("Select previous item when empty", () => performKeypress(emptyDropdown.Header, Key.Down)); 216 AddStep("Select last item when empty", () => performKeypress(emptyDropdown.Header, Key.PageUp)); 217 AddStep("Select first item when empty", () => performKeypress(emptyDropdown.Header, Key.PageDown)); 218 } 219 220 [TestCase(false)] 221 [TestCase(true)] 222 public void TestKeyboardPreselection(bool cleanSelection) 223 { 224 if (cleanSelection) 225 AddStep("Clean selection", () => testDropdownMenu.Current.Value = null); 226 227 toggleDropdownViaClick(testDropdownMenu); 228 assertDropdownIsOpen(testDropdownMenu); 229 230 AddStep("Preselect next item", () => 231 { 232 previousIndex = testDropdownMenu.PreselectedIndex; 233 performKeypress(testDropdownMenu.Menu, Key.Down); 234 }); 235 AddAssert("Next item is preselected", () => testDropdownMenu.PreselectedIndex == previousIndex + 1); 236 237 AddStep("Preselect previous item", () => 238 { 239 previousIndex = testDropdownMenu.PreselectedIndex; 240 performKeypress(testDropdownMenu.Menu, Key.Up); 241 }); 242 AddAssert("Previous item is preselected", () => testDropdownMenu.PreselectedIndex == Math.Max(0, previousIndex - 1)); 243 244 AddStep("Preselect last visible item", () => 245 { 246 lastVisibleIndexOnTheCurrentPage = testDropdownMenu.Menu.DrawableMenuItems.ToList().IndexOf(testDropdownMenu.Menu.VisibleMenuItems.Last()); 247 performKeypress(testDropdownMenu.Menu, Key.PageDown); 248 }); 249 AddAssert("Last visible item preselected", () => testDropdownMenu.PreselectedIndex == lastVisibleIndexOnTheCurrentPage); 250 251 AddStep("Preselect last visible item on the next page", () => 252 { 253 lastVisibleIndexOnTheNextPage = 254 Math.Clamp(lastVisibleIndexOnTheCurrentPage + testDropdownMenu.Menu.VisibleMenuItems.Count(), 0, testDropdownMenu.Menu.Items.Count - 1); 255 256 performKeypress(testDropdownMenu.Menu, Key.PageDown); 257 }); 258 AddAssert("Last visible item on the next page preselected", () => testDropdownMenu.PreselectedIndex == lastVisibleIndexOnTheNextPage); 259 260 AddStep("Preselect first visible item", () => 261 { 262 firstVisibleIndexOnTheCurrentPage = testDropdownMenu.Menu.DrawableMenuItems.ToList().IndexOf(testDropdownMenu.Menu.VisibleMenuItems.First()); 263 performKeypress(testDropdownMenu.Menu, Key.PageUp); 264 }); 265 AddAssert("First visible item preselected", () => testDropdownMenu.PreselectedIndex == firstVisibleIndexOnTheCurrentPage); 266 267 AddStep("Preselect first visible item on the previous page", () => 268 { 269 firstVisibleIndexOnThePreviousPage = Math.Clamp(firstVisibleIndexOnTheCurrentPage - testDropdownMenu.Menu.VisibleMenuItems.Count(), 0, 270 testDropdownMenu.Menu.Items.Count - 1); 271 performKeypress(testDropdownMenu.Menu, Key.PageUp); 272 }); 273 AddAssert("First visible item on the previous page selected", () => testDropdownMenu.PreselectedIndex == firstVisibleIndexOnThePreviousPage); 274 AddAssert("First item is preselected", () => testDropdownMenu.Menu.PreselectedItem.Item == testDropdownMenu.Menu.DrawableMenuItems.First().Item); 275 276 AddStep("Preselect last item", 277 () => performPlatformAction(PlatformAction.MoveToListEnd, platformActionContainerKeyboardPreselection, testDropdownMenu)); 278 AddAssert("Last item preselected", () => testDropdownMenu.Menu.PreselectedItem.Item == testDropdownMenu.Menu.DrawableMenuItems.Last().Item); 279 280 AddStep("Finalize selection", () => performKeypress(testDropdownMenu.Menu, Key.Enter)); 281 assertLastItemSelected(); 282 assertDropdownIsClosed(testDropdownMenu); 283 284 toggleDropdownViaClick(testDropdownMenu); 285 assertDropdownIsOpen(testDropdownMenu); 286 287 AddStep("Preselect first item", 288 () => performPlatformAction(PlatformAction.MoveToListStart, platformActionContainerKeyboardPreselection, testDropdownMenu)); 289 AddAssert("First item preselected", () => testDropdownMenu.Menu.PreselectedItem.Item == testDropdownMenu.Menu.DrawableMenuItems.First().Item); 290 291 AddStep("Discard preselection", () => performKeypress(testDropdownMenu.Menu, Key.Escape)); 292 assertDropdownIsClosed(testDropdownMenu); 293 assertLastItemSelected(); 294 295 toggleDropdownViaClick(emptyDropdown, "empty dropdown"); 296 AddStep("Preselect next item when empty", () => performKeypress(emptyDropdown.Menu, Key.Down)); 297 AddStep("Preselect previous item when empty", () => performKeypress(emptyDropdown.Menu, Key.Up)); 298 AddStep("Preselect first visible item when empty", () => performKeypress(emptyDropdown.Menu, Key.PageUp)); 299 AddStep("Preselect last visible item when empty", () => performKeypress(emptyDropdown.Menu, Key.PageDown)); 300 AddStep("Preselect first item when empty", 301 () => performPlatformAction(PlatformAction.MoveToListStart, platformActionContainerEmptyDropdown, emptyDropdown)); 302 AddStep("Preselect last item when empty", 303 () => performPlatformAction(PlatformAction.MoveToListEnd, platformActionContainerEmptyDropdown, emptyDropdown)); 304 305 void assertLastItemSelected() => AddAssert("Last item selected", () => testDropdownMenu.SelectedItem == testDropdownMenu.Menu.DrawableMenuItems.Last().Item); 306 } 307 308 [Test] 309 public void TestSelectNull() 310 { 311 AddStep("select item 1", () => testDropdown.Current.Value = testDropdown.Items.ElementAt(1)); 312 AddAssert("item 1 is selected", () => testDropdown.Current.Value.Equals(testDropdown.Items.ElementAt(1))); 313 AddStep("select item null", () => testDropdown.Current.Value = null); 314 AddAssert("null is selected", () => testDropdown.Current.Value == null); 315 } 316 317 [Test] 318 public void TestDisabledCurrent() 319 { 320 TestModel originalValue = null; 321 322 AddStep("store original value", () => originalValue = disabledDropdown.Current.Value); 323 324 toggleDropdownViaClick(disabledDropdown); 325 assertDropdownIsClosed(disabledDropdown); 326 327 AddStep("attempt to select next", () => performKeypress(disabledDropdown, Key.Down)); 328 valueIsUnchanged(); 329 330 AddStep("attempt to select previous", () => performKeypress(disabledDropdown, Key.Up)); 331 valueIsUnchanged(); 332 333 AddStep("attempt to select first", () => InputManager.Keys(PlatformAction.MoveToListStart)); 334 valueIsUnchanged(); 335 336 AddStep("attempt to select last", () => InputManager.Keys(PlatformAction.MoveToListEnd)); 337 valueIsUnchanged(); 338 339 AddStep("enable current", () => disabledDropdown.Current.Disabled = false); 340 toggleDropdownViaClick(disabledDropdown); 341 assertDropdownIsOpen(disabledDropdown); 342 343 AddStep("disable current", () => disabledDropdown.Current.Disabled = true); 344 assertDropdownIsClosed(disabledDropdown); 345 346 void valueIsUnchanged() => AddAssert("value is unchanged", () => disabledDropdown.Current.Value.Equals(originalValue)); 347 } 348 349 private void toggleDropdownViaClick(TestDropdown dropdown, string dropdownName = null) => AddStep($"click {dropdownName ?? "dropdown"}", () => 350 { 351 InputManager.MoveMouseTo(dropdown.Header); 352 InputManager.Click(MouseButton.Left); 353 }); 354 355 private void assertDropdownIsOpen(TestDropdown dropdown) => AddAssert("dropdown is open", () => dropdown.Menu.State == MenuState.Open); 356 357 private void assertDropdownIsClosed(TestDropdown dropdown) => AddAssert("dropdown is closed", () => dropdown.Menu.State == MenuState.Closed); 358 359 private class TestModel : IEquatable<TestModel> 360 { 361 public readonly string Identifier; 362 363 public TestModel(string identifier) 364 { 365 Identifier = identifier; 366 } 367 368 public bool Equals(TestModel other) 369 { 370 if (other == null) 371 return false; 372 373 return other.Identifier == Identifier; 374 } 375 376 public override string ToString() => Identifier; 377 378 public static implicit operator TestModel(string str) => new TestModel(str); 379 } 380 381 private class TestDropdown : BasicDropdown<TestModel> 382 { 383 public new DropdownMenu Menu => base.Menu; 384 385 protected override DropdownMenu CreateMenu() => new TestDropdownMenu(); 386 387 protected override DropdownHeader CreateHeader() => new BasicDropdownHeader(); 388 389 public void SelectItem(MenuItem item) => ((TestDropdownMenu)Menu).SelectItem(item); 390 391 private class TestDropdownMenu : BasicDropdownMenu 392 { 393 public void SelectItem(MenuItem item) => Children.FirstOrDefault(c => c.Item == item)? 394 .TriggerEvent(new ClickEvent(GetContainingInputManager().CurrentState, MouseButton.Left)); 395 } 396 397 internal new DropdownMenuItem<TestModel> SelectedItem => base.SelectedItem; 398 399 public int SelectedIndex => Menu.DrawableMenuItems.Select(d => d.Item).ToList().IndexOf(SelectedItem); 400 public int PreselectedIndex => Menu.DrawableMenuItems.ToList().IndexOf(Menu.PreselectedItem); 401 } 402 } 403}