A game framework written with osu! in mind.
at master 367 lines 13 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.Collections.Generic; 6using System.Linq; 7using System.Threading; 8using NUnit.Framework; 9using osu.Framework.Allocation; 10using osu.Framework.Bindables; 11using osu.Framework.Graphics; 12using osu.Framework.Graphics.Containers; 13using osu.Framework.Graphics.UserInterface; 14using osu.Framework.Testing; 15using osuTK; 16using osuTK.Input; 17 18namespace osu.Framework.Tests.Visual.UserInterface 19{ 20 public class TestSceneRearrangeableListContainer : ManualInputManagerTestScene 21 { 22 private TestRearrangeableList list; 23 24 private Container listContainer; 25 26 [SetUp] 27 public void Setup() => Schedule(() => 28 { 29 Child = listContainer = new Container 30 { 31 Anchor = Anchor.Centre, 32 Origin = Anchor.Centre, 33 Size = new Vector2(500, 300), 34 Child = list = new TestRearrangeableList { RelativeSizeAxes = Axes.Both } 35 }; 36 }); 37 38 [Test] 39 public void TestAddItem() 40 { 41 for (int i = 0; i < 5; i++) 42 { 43 int localI = i; 44 45 addItems(1); 46 AddAssert($"last item is \"{i}\"", () => list.ChildrenOfType<RearrangeableListItem<int>>().Last().Model == localI); 47 } 48 } 49 50 [Test] 51 public void TestBindBeforeLoad() 52 { 53 AddStep("create list", () => list = new TestRearrangeableList { RelativeSizeAxes = Axes.Both }); 54 AddStep("bind list to items", () => list.Items.BindTo(new BindableList<int>(new[] { 1, 2, 3 }))); 55 AddStep("add list to hierarchy", () => listContainer.Add(list)); 56 } 57 58 [Test] 59 public void TestAddDuplicateItemsFails() 60 { 61 const int item = 1; 62 63 AddStep("add item 1", () => list.Items.Add(item)); 64 65 AddAssert("add same item throws", () => 66 { 67 try 68 { 69 list.Items.Add(item); 70 return false; 71 } 72 catch (InvalidOperationException) 73 { 74 return true; 75 } 76 }); 77 } 78 79 [Test] 80 public void TestRemoveItem() 81 { 82 addItems(5); 83 84 for (int i = 0; i < 5; i++) 85 { 86 int localI = i; 87 88 AddStep($"remove item \"{i}\"", () => list.Items.Remove(localI)); 89 AddAssert($"first item is not \"{i}\"", () => list.ChildrenOfType<RearrangeableListItem<int>>().FirstOrDefault()?.Model != localI); 90 } 91 } 92 93 [Test] 94 public void TestClearItems() 95 { 96 addItems(5); 97 98 AddStep("clear items", () => list.Items.Clear()); 99 100 AddAssert("no items contained", () => !list.ChildrenOfType<RearrangeableListItem<string>>().Any()); 101 } 102 103 [Test] 104 public void TestRearrangeByDrag() 105 { 106 addItems(5); 107 108 addDragSteps(1, 4, new[] { 0, 2, 3, 4, 1 }); 109 addDragSteps(1, 3, new[] { 0, 2, 1, 3, 4 }); 110 addDragSteps(0, 3, new[] { 2, 1, 3, 0, 4 }); 111 addDragSteps(3, 4, new[] { 2, 1, 0, 4, 3 }); 112 addDragSteps(4, 2, new[] { 4, 2, 1, 0, 3 }); 113 addDragSteps(2, 4, new[] { 2, 4, 1, 0, 3 }); 114 } 115 116 [Test] 117 public void TestRearrangeByDragWithHiddenItems() 118 { 119 addItems(6); 120 121 AddStep("hide item zero", () => list.ListContainer.First(i => i.Model == 0).Hide()); 122 123 addDragSteps(2, 5, new[] { 0, 1, 3, 4, 5, 2 }); 124 addDragSteps(2, 4, new[] { 0, 1, 3, 2, 4, 5 }); 125 addDragSteps(1, 4, new[] { 0, 3, 2, 4, 1, 5 }); 126 addDragSteps(4, 5, new[] { 0, 3, 2, 1, 5, 4 }); 127 addDragSteps(5, 3, new[] { 0, 5, 3, 2, 1, 4 }); 128 addDragSteps(3, 5, new[] { 0, 3, 5, 2, 1, 4 }); 129 } 130 131 [Test] 132 public void TestRearrangeByDragAfterRemoval() 133 { 134 addItems(5); 135 136 addDragSteps(0, 4, new[] { 1, 2, 3, 4, 0 }); 137 addDragSteps(1, 4, new[] { 2, 3, 4, 1, 0 }); 138 addDragSteps(2, 4, new[] { 3, 4, 2, 1, 0 }); 139 addDragSteps(3, 4, new[] { 4, 3, 2, 1, 0 }); 140 141 AddStep("remove 3 and 2", () => 142 { 143 list.Items.Remove(3); 144 list.Items.Remove(2); 145 }); 146 147 addDragSteps(4, 0, new[] { 1, 0, 4 }); 148 addDragSteps(0, 1, new[] { 0, 1, 4 }); 149 addDragSteps(4, 0, new[] { 4, 0, 1 }); 150 } 151 152 [Test] 153 public void TestRemoveAfterDragScrollThenTryRearrange() 154 { 155 addItems(5); 156 157 // Scroll 158 AddStep("move mouse to first item", () => InputManager.MoveMouseTo(getItem(0))); 159 AddStep("begin a drag", () => InputManager.PressButton(MouseButton.Left)); 160 AddStep("move the mouse", () => InputManager.MoveMouseTo(getItem(0), new Vector2(0, 30))); 161 AddStep("end the drag", () => InputManager.ReleaseButton(MouseButton.Left)); 162 163 AddStep("remove all but one item", () => 164 { 165 for (int i = 0; i < 4; i++) 166 list.Items.Remove(getItem(i).Model); 167 }); 168 169 // Drag 170 AddStep("move mouse to first dragger", () => InputManager.MoveMouseTo(getDragger(4))); 171 AddStep("begin a drag", () => InputManager.PressButton(MouseButton.Left)); 172 AddStep("move the mouse", () => InputManager.MoveMouseTo(getDragger(4), new Vector2(0, 30))); 173 AddStep("end the drag", () => InputManager.ReleaseButton(MouseButton.Left)); 174 } 175 176 [Test] 177 public void TestScrolledWhenDraggedToBoundaries() 178 { 179 addItems(100); 180 181 AddStep("scroll to item 50", () => list.ScrollTo(50)); 182 183 float scrollPosition = 0; 184 AddStep("get scroll position", () => scrollPosition = list.ScrollPosition); 185 186 AddStep("move to 52", () => 187 { 188 InputManager.MoveMouseTo(getDragger(52)); 189 InputManager.PressButton(MouseButton.Left); 190 }); 191 192 AddStep("drag to 0", () => InputManager.MoveMouseTo(getDragger(0), new Vector2(0, -1))); 193 194 AddUntilStep("scrolling up", () => list.ScrollPosition < scrollPosition); 195 AddUntilStep("52 is the first item", () => list.Items.First() == 52); 196 197 AddStep("drag to 99", () => InputManager.MoveMouseTo(getDragger(99), new Vector2(0, 1))); 198 199 AddUntilStep("scrolling down", () => list.ScrollPosition > scrollPosition); 200 AddUntilStep("52 is the last item", () => list.Items.Last() == 52); 201 } 202 203 [Test] 204 public void TestRearrangeWhileAddingItems() 205 { 206 addItems(2); 207 208 AddStep("grab item 0", () => 209 { 210 InputManager.MoveMouseTo(getDragger(0)); 211 InputManager.PressButton(MouseButton.Left); 212 }); 213 214 AddStep("move to bottom", () => InputManager.MoveMouseTo(list.ToScreenSpace(list.LayoutRectangle.BottomLeft) + new Vector2(0, 10))); 215 216 addItems(10); 217 218 AddUntilStep("0 is the last item", () => list.Items.Last() == 0); 219 } 220 221 [Test] 222 public void TestRearrangeWhileRemovingItems() 223 { 224 addItems(50); 225 226 AddStep("grab item 0", () => 227 { 228 InputManager.MoveMouseTo(getDragger(0)); 229 InputManager.PressButton(MouseButton.Left); 230 }); 231 232 AddStep("move to bottom", () => InputManager.MoveMouseTo(list.ToScreenSpace(list.LayoutRectangle.BottomLeft) + new Vector2(0, 20))); 233 234 int lastItem = 49; 235 236 AddRepeatStep("remove item", () => 237 { 238 list.Items.Remove(lastItem--); 239 }, 25); 240 241 AddUntilStep("0 is the last item", () => list.Items.Last() == 0); 242 243 AddRepeatStep("remove item", () => 244 { 245 list.Items.Remove(lastItem--); 246 }, 25); 247 248 AddStep("release button", () => InputManager.ReleaseButton(MouseButton.Left)); 249 } 250 251 [Test] 252 public void TestNotScrolledToTopOnRemove() 253 { 254 addItems(100); 255 256 float scrollPosition = 0; 257 AddStep("scroll to item 50", () => 258 { 259 list.ScrollTo(50); 260 scrollPosition = list.ScrollPosition; 261 }); 262 263 AddStep("remove item 50", () => list.Items.Remove(50)); 264 265 AddAssert("scroll hasn't changed", () => list.ScrollPosition == scrollPosition); 266 } 267 268 [Test] 269 public void TestRemoveDuringLoadAndReAdd() 270 { 271 TestDelayedLoadRearrangeableList delayedList = null; 272 273 AddStep("create list", () => Child = delayedList = new TestDelayedLoadRearrangeableList()); 274 275 AddStep("add item 1", () => delayedList.Items.Add(1)); 276 AddStep("remove item 1", () => delayedList.Items.Remove(1)); 277 AddStep("add item 1", () => delayedList.Items.Add(1)); 278 AddStep("allow load", () => delayedList.AllowLoad.Release(100)); 279 280 AddUntilStep("only one item", () => delayedList.ChildrenOfType<BasicRearrangeableListItem<int>>().Count() == 1); 281 } 282 283 private void addDragSteps(int from, int to, int[] expectedSequence) 284 { 285 AddStep($"move to {from}", () => 286 { 287 InputManager.MoveMouseTo(getDragger(from)); 288 InputManager.PressButton(MouseButton.Left); 289 }); 290 291 AddStep($"drag to {to}", () => 292 { 293 var fromDragger = getDragger(from); 294 var toDragger = getDragger(to); 295 296 InputManager.MoveMouseTo(getDragger(to), fromDragger.ScreenSpaceDrawQuad.TopLeft.Y < toDragger.ScreenSpaceDrawQuad.TopLeft.Y ? new Vector2(0, 1) : new Vector2(0, -1)); 297 }); 298 299 assertSequence(expectedSequence); 300 301 AddStep("release button", () => InputManager.ReleaseButton(MouseButton.Left)); 302 } 303 304 private void assertSequence(params int[] sequence) 305 { 306 AddAssert($"sequence is {string.Join(", ", sequence)}", 307 () => list.Items.SequenceEqual(sequence.Select(value => value))); 308 } 309 310 private void addItems(int count) 311 { 312 AddStep($"add {count} item(s)", () => 313 { 314 int startId = list.Items.Count == 0 ? 0 : list.Items.Max() + 1; 315 316 for (int i = 0; i < count; i++) 317 list.Items.Add(startId + i); 318 }); 319 320 AddUntilStep("wait for items to load", () => list.ItemMap.Values.All(i => i.IsLoaded)); 321 } 322 323 private RearrangeableListItem<int> getItem(int index) 324 => list.ChildrenOfType<RearrangeableListItem<int>>().First(i => i.Model == index); 325 326 private BasicRearrangeableListItem<int>.Button getDragger(int index) 327 => list.ChildrenOfType<BasicRearrangeableListItem<int>>().First(i => i.Model == index) 328 .ChildrenOfType<BasicRearrangeableListItem<int>.Button>().First(); 329 330 private class TestRearrangeableList : BasicRearrangeableListContainer<int> 331 { 332 public float ScrollPosition => ScrollContainer.Current; 333 334 public new IReadOnlyDictionary<int, RearrangeableListItem<int>> ItemMap => base.ItemMap; 335 336 public new FillFlowContainer<RearrangeableListItem<int>> ListContainer => base.ListContainer; 337 338 public void ScrollTo(int item) 339 => ScrollContainer.ScrollTo(this.ChildrenOfType<BasicRearrangeableListItem<int>>().First(i => i.Model == item), false); 340 } 341 342 private class TestDelayedLoadRearrangeableList : BasicRearrangeableListContainer<int> 343 { 344 public readonly SemaphoreSlim AllowLoad = new SemaphoreSlim(0, 100); 345 346 protected override BasicRearrangeableListItem<int> CreateBasicItem(int item) => new TestRearrangeableListItem(item, AllowLoad); 347 348 private class TestRearrangeableListItem : BasicRearrangeableListItem<int> 349 { 350 private readonly SemaphoreSlim allowLoad; 351 352 public TestRearrangeableListItem(int item, SemaphoreSlim allowLoad) 353 : base(item, false) 354 { 355 this.allowLoad = allowLoad; 356 } 357 358 [BackgroundDependencyLoader] 359 private void load() 360 { 361 if (!allowLoad.Wait(TimeSpan.FromSeconds(10))) 362 throw new TimeoutException(); 363 } 364 } 365 } 366 } 367}