1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-15 07:32:55 +08:00
osu-lazer/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
Dean Herbert a14dede036 Ensure TestBeatmapCarousel.Items only returns actually visible items (and in correct order)
Turns out that items could be in an order that isn't the same as how
things look on the carousel, so this change ensures that for testing
purposes they are sorted by Y position.

Also uses the `CarouselItem.Visible` flag to ensure that we don't have
to wait for drawable fade transitions after a filter operation.
2023-03-03 17:18:34 +09:00

1227 lines
45 KiB
C#

// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Taiko;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Carousel;
using osu.Game.Screens.Select.Filter;
using osu.Game.Tests.Resources;
using osuTK.Input;
namespace osu.Game.Tests.Visual.SongSelect
{
[TestFixture]
public partial class TestSceneBeatmapCarousel : OsuManualInputManagerTestScene
{
private TestBeatmapCarousel carousel;
private RulesetStore rulesets;
private readonly Stack<BeatmapSetInfo> selectedSets = new Stack<BeatmapSetInfo>();
private readonly HashSet<Guid> eagerSelectedIDs = new HashSet<Guid>();
private BeatmapInfo currentSelection => carousel.SelectedBeatmapInfo;
private const int set_count = 5;
[BackgroundDependencyLoader]
private void load(RulesetStore rulesets)
{
this.rulesets = rulesets;
}
[Test]
public void TestExternalRulesetChange()
{
createCarousel(new List<BeatmapSetInfo>());
AddStep("filter to ruleset 0", () => carousel.Filter(new FilterCriteria
{
Ruleset = rulesets.AvailableRulesets.ElementAt(0),
AllowConvertedBeatmaps = true,
}, false));
AddStep("add mixed ruleset beatmapset", () =>
{
var testMixed = TestResources.CreateTestBeatmapSetInfo(3);
for (int i = 0; i <= 2; i++)
{
testMixed.Beatmaps[i].Ruleset = rulesets.AvailableRulesets.ElementAt(i);
}
carousel.UpdateBeatmapSet(testMixed);
});
AddUntilStep("wait for filtered difficulties", () =>
{
var visibleBeatmapPanels = carousel.Items.OfType<DrawableCarouselBeatmap>().Where(p => p.IsPresent).ToArray();
return visibleBeatmapPanels.Length == 1
&& visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item)!.BeatmapInfo.Ruleset.OnlineID == 0) == 1;
});
AddStep("filter to ruleset 1", () => carousel.Filter(new FilterCriteria
{
Ruleset = rulesets.AvailableRulesets.ElementAt(1),
AllowConvertedBeatmaps = true,
}, false));
AddUntilStep("wait for filtered difficulties", () =>
{
var visibleBeatmapPanels = carousel.Items.OfType<DrawableCarouselBeatmap>().Where(p => p.IsPresent).ToArray();
return visibleBeatmapPanels.Length == 2
&& visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item)!.BeatmapInfo.Ruleset.OnlineID == 0) == 1
&& visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item)!.BeatmapInfo.Ruleset.OnlineID == 1) == 1;
});
AddStep("filter to ruleset 2", () => carousel.Filter(new FilterCriteria
{
Ruleset = rulesets.AvailableRulesets.ElementAt(2),
AllowConvertedBeatmaps = true,
}, false));
AddUntilStep("wait for filtered difficulties", () =>
{
var visibleBeatmapPanels = carousel.Items.OfType<DrawableCarouselBeatmap>().Where(p => p.IsPresent).ToArray();
return visibleBeatmapPanels.Length == 2
&& visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item!).BeatmapInfo.Ruleset.OnlineID == 0) == 1
&& visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item!).BeatmapInfo.Ruleset.OnlineID == 2) == 1;
});
}
[Test]
public void TestScrollPositionMaintainedOnAdd()
{
loadBeatmaps(count: 1, randomDifficulties: false);
for (int i = 0; i < 10; i++)
{
AddRepeatStep("Add some sets", () => carousel.UpdateBeatmapSet(TestResources.CreateTestBeatmapSetInfo()), 4);
checkSelectionIsCentered();
}
}
[Test]
public void TestDeletion()
{
loadBeatmaps(count: 5, randomDifficulties: true);
AddStep("remove first set", () => carousel.RemoveBeatmapSet(carousel.Items.Select(item => item.Item).OfType<CarouselBeatmapSet>().First().BeatmapSet));
AddUntilStep("4 beatmap sets visible", () => this.ChildrenOfType<DrawableCarouselBeatmapSet>().Count(set => set.Alpha > 0) == 4);
}
[Test]
public void TestScrollPositionMaintainedOnDelete()
{
loadBeatmaps(count: 50, randomDifficulties: false);
for (int i = 0; i < 10; i++)
{
AddRepeatStep("Remove some sets", () =>
carousel.RemoveBeatmapSet(carousel.Items.Select(item => item.Item)
.OfType<CarouselBeatmapSet>()
.OrderBy(item => item.GetHashCode())
.First(item => item.State.Value != CarouselItemState.Selected && item.Visible).BeatmapSet), 4);
checkSelectionIsCentered();
}
}
[Test]
public void TestManyPanels()
{
loadBeatmaps(count: 5000, randomDifficulties: true);
}
[Test]
public void TestKeyRepeat()
{
loadBeatmaps();
advanceSelection(false);
AddStep("press down arrow", () => InputManager.PressKey(Key.Down));
BeatmapInfo selection = null;
checkSelectionIterating(true);
AddStep("press up arrow", () => InputManager.PressKey(Key.Up));
checkSelectionIterating(true);
AddStep("release down arrow", () => InputManager.ReleaseKey(Key.Down));
checkSelectionIterating(true);
AddStep("release up arrow", () => InputManager.ReleaseKey(Key.Up));
checkSelectionIterating(false);
void checkSelectionIterating(bool isIterating)
{
for (int i = 0; i < 3; i++)
{
AddStep("store selection", () => selection = carousel.SelectedBeatmapInfo);
if (isIterating)
AddUntilStep("selection changed", () => !carousel.SelectedBeatmapInfo?.Equals(selection) == true);
else
AddUntilStep("selection not changed", () => carousel.SelectedBeatmapInfo?.Equals(selection) == true);
}
}
}
[Test]
public void TestRecommendedSelection()
{
loadBeatmaps(carouselAdjust: carousel => carousel.GetRecommendedBeatmap = beatmaps => beatmaps.LastOrDefault());
AddStep("select last", () => carousel.SelectBeatmap(carousel.BeatmapSets.Last().Beatmaps.Last()));
// check recommended was selected
advanceSelection(direction: 1, diff: false);
waitForSelection(1, 3);
// change away from recommended
advanceSelection(direction: -1, diff: true);
waitForSelection(1, 2);
// next set, check recommended
advanceSelection(direction: 1, diff: false);
waitForSelection(2, 3);
// next set, check recommended
advanceSelection(direction: 1, diff: false);
waitForSelection(3, 3);
// go back to first set and ensure user selection was retained
advanceSelection(direction: -1, diff: false);
advanceSelection(direction: -1, diff: false);
waitForSelection(1, 2);
}
/// <summary>
/// Test keyboard traversal
/// </summary>
[Test]
public void TestTraversal()
{
loadBeatmaps();
AddStep("select first", () => carousel.SelectBeatmap(carousel.BeatmapSets.First().Beatmaps.First()));
waitForSelection(1, 1);
advanceSelection(direction: 1, diff: true);
waitForSelection(1, 2);
advanceSelection(direction: -1, diff: false);
waitForSelection(set_count, 1);
advanceSelection(direction: -1, diff: true);
waitForSelection(set_count - 1, 3);
advanceSelection(diff: false);
advanceSelection(diff: false);
waitForSelection(1, 2);
advanceSelection(direction: -1, diff: true);
advanceSelection(direction: -1, diff: true);
waitForSelection(set_count, 3);
}
[TestCase(true)]
[TestCase(false)]
public void TestTraversalBeyondVisible(bool forwards)
{
var sets = new List<BeatmapSetInfo>();
const int total_set_count = 200;
AddStep("Populuate beatmap sets", () =>
{
sets.Clear();
for (int i = 0; i < total_set_count; i++)
sets.Add(TestResources.CreateTestBeatmapSetInfo());
});
loadBeatmaps(sets);
for (int i = 1; i < total_set_count; i += i)
selectNextAndAssert(i);
void selectNextAndAssert(int amount)
{
setSelected(forwards ? 1 : total_set_count, 1);
AddStep($"{(forwards ? "Next" : "Previous")} beatmap {amount} times", () =>
{
for (int i = 0; i < amount; i++)
{
carousel.SelectNext(forwards ? 1 : -1);
}
});
waitForSelection(forwards ? amount + 1 : total_set_count - amount);
}
}
[Test]
public void TestTraversalBeyondVisibleDifficulties()
{
var sets = new List<BeatmapSetInfo>();
const int total_set_count = 20;
AddStep("Populuate beatmap sets", () =>
{
sets.Clear();
for (int i = 0; i < total_set_count; i++)
sets.Add(TestResources.CreateTestBeatmapSetInfo(3));
});
loadBeatmaps(sets);
// Selects next set once, difficulty index doesn't change
selectNextAndAssert(3, true, 2, 1);
// Selects next set 16 times (50 \ 3 == 16), difficulty index changes twice (50 % 3 == 2)
selectNextAndAssert(50, true, 17, 3);
// Travels around the carousel thrice (200 \ 60 == 3)
// continues to select 20 times (200 \ 60 == 20)
// selects next set 6 times (20 \ 3 == 6)
// difficulty index changes twice (20 % 3 == 2)
selectNextAndAssert(200, true, 7, 3);
// All same but in reverse
selectNextAndAssert(3, false, 19, 3);
selectNextAndAssert(50, false, 4, 1);
selectNextAndAssert(200, false, 14, 1);
void selectNextAndAssert(int amount, bool forwards, int expectedSet, int expectedDiff)
{
// Select very first or very last difficulty
setSelected(forwards ? 1 : 20, forwards ? 1 : 3);
AddStep($"{(forwards ? "Next" : "Previous")} difficulty {amount} times", () =>
{
for (int i = 0; i < amount; i++)
carousel.SelectNext(forwards ? 1 : -1, false);
});
waitForSelection(expectedSet, expectedDiff);
}
}
/// <summary>
/// Test filtering
/// </summary>
[Test]
public void TestFiltering()
{
loadBeatmaps();
// basic filtering
setSelected(1, 1);
AddStep("Filter", () => carousel.Filter(new FilterCriteria { SearchText = carousel.BeatmapSets.ElementAt(2).Metadata.Title }, false));
checkVisibleItemCount(diff: false, count: 1);
checkVisibleItemCount(diff: true, count: 3);
waitForSelection(3, 1);
advanceSelection(diff: true, count: 4);
waitForSelection(3, 2);
AddStep("Un-filter (debounce)", () => carousel.Filter(new FilterCriteria()));
AddUntilStep("Wait for debounce", () => !carousel.PendingFilterTask);
checkVisibleItemCount(diff: false, count: set_count);
checkVisibleItemCount(diff: true, count: 3);
// test filtering some difficulties (and keeping current beatmap set selected).
setSelected(1, 2);
AddStep("Filter some difficulties", () => carousel.Filter(new FilterCriteria { SearchText = "Normal" }, false));
waitForSelection(1, 1);
AddStep("Un-filter", () => carousel.Filter(new FilterCriteria(), false));
waitForSelection(1, 1);
AddStep("Filter all", () => carousel.Filter(new FilterCriteria { SearchText = "Dingo" }, false));
checkVisibleItemCount(false, 0);
checkVisibleItemCount(true, 0);
AddAssert("Selection is null", () => currentSelection == null);
advanceSelection(true);
AddAssert("Selection is null", () => currentSelection == null);
advanceSelection(false);
AddAssert("Selection is null", () => currentSelection == null);
AddStep("Un-filter", () => carousel.Filter(new FilterCriteria(), false));
AddAssert("Selection is non-null", () => currentSelection != null);
setSelected(1, 3);
}
[Test]
public void TestFilterRange()
{
string searchText = null;
loadBeatmaps();
// buffer the selection
setSelected(3, 2);
AddStep("get search text", () => searchText = carousel.SelectedBeatmapSet!.Metadata.Title);
setSelected(1, 3);
AddStep("Apply a range filter", () => carousel.Filter(new FilterCriteria
{
SearchText = searchText,
StarDifficulty = new FilterCriteria.OptionalRange<double>
{
Min = 2,
Max = 5.5,
IsLowerInclusive = true
}
}, false));
// should reselect the buffered selection.
waitForSelection(3, 2);
}
/// <summary>
/// Test random non-repeating algorithm
/// </summary>
[Test]
public void TestRandom()
{
loadBeatmaps();
setSelected(1, 1);
nextRandom();
ensureRandomDidntRepeat();
nextRandom();
ensureRandomDidntRepeat();
nextRandom();
ensureRandomDidntRepeat();
prevRandom();
ensureRandomFetchSuccess();
prevRandom();
ensureRandomFetchSuccess();
nextRandom();
ensureRandomDidntRepeat();
nextRandom();
ensureRandomDidntRepeat();
nextRandom();
AddAssert("ensure repeat", () => selectedSets.Contains(carousel.SelectedBeatmapSet));
AddStep("Add set with 100 difficulties", () => carousel.UpdateBeatmapSet(TestResources.CreateTestBeatmapSetInfo(100, rulesets.AvailableRulesets.ToArray())));
AddStep("Filter Extra", () => carousel.Filter(new FilterCriteria { SearchText = "Extra 10" }, false));
checkInvisibleDifficultiesUnselectable();
checkInvisibleDifficultiesUnselectable();
checkInvisibleDifficultiesUnselectable();
checkInvisibleDifficultiesUnselectable();
checkInvisibleDifficultiesUnselectable();
AddStep("Un-filter", () => carousel.Filter(new FilterCriteria(), false));
}
/// <summary>
/// Test adding and removing beatmap sets
/// </summary>
[Test]
public void TestAddRemove()
{
loadBeatmaps();
var firstAdded = TestResources.CreateTestBeatmapSetInfo();
var secondAdded = TestResources.CreateTestBeatmapSetInfo();
AddStep("Add new set", () => carousel.UpdateBeatmapSet(firstAdded));
AddStep("Add new set", () => carousel.UpdateBeatmapSet(secondAdded));
checkVisibleItemCount(false, set_count + 2);
AddStep("Remove set", () => carousel.RemoveBeatmapSet(firstAdded));
checkVisibleItemCount(false, set_count + 1);
setSelected(set_count + 1, 1);
AddStep("Remove set", () => carousel.RemoveBeatmapSet(secondAdded));
checkVisibleItemCount(false, set_count);
waitForSelection(set_count);
}
[Test]
public void TestSelectionEnteringFromEmptyRuleset()
{
var sets = new List<BeatmapSetInfo>();
AddStep("Create beatmaps for taiko only", () =>
{
sets.Clear();
var rulesetBeatmapSet = TestResources.CreateTestBeatmapSetInfo(1);
var taikoRuleset = rulesets.AvailableRulesets.ElementAt(1);
rulesetBeatmapSet.Beatmaps.ForEach(b => b.Ruleset = taikoRuleset);
sets.Add(rulesetBeatmapSet);
});
loadBeatmaps(sets, () => new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(0) });
AddStep("Set non-empty mode filter", () =>
carousel.Filter(new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(1) }, false));
AddAssert("Something is selected", () => carousel.SelectedBeatmapInfo != null);
}
[Test]
public void TestSortingDateSubmitted()
{
var sets = new List<BeatmapSetInfo>();
const string zzz_string = "zzzzz";
AddStep("Populuate beatmap sets", () =>
{
sets.Clear();
for (int i = 0; i < 10; i++)
{
var set = TestResources.CreateTestBeatmapSetInfo(5);
// A total of 6 sets have date submitted (4 don't)
// A total of 5 sets have artist string (3 of which also have date submitted)
if (i >= 2 && i < 8) // i = 2, 3, 4, 5, 6, 7 have submitted date
set.DateSubmitted = DateTimeOffset.Now.AddMinutes(i);
if (i < 5) // i = 0, 1, 2, 3, 4 have matching string
set.Beatmaps.ForEach(b => b.Metadata.Artist = zzz_string);
set.Beatmaps.ForEach(b => b.Metadata.Title = $"submitted: {set.DateSubmitted}");
sets.Add(set);
}
});
loadBeatmaps(sets);
AddStep("Sort by date submitted", () => carousel.Filter(new FilterCriteria { Sort = SortMode.DateSubmitted }, false));
checkVisibleItemCount(diff: false, count: 10);
checkVisibleItemCount(diff: true, count: 5);
AddAssert("missing date are at end",
() => carousel.Items.OfType<DrawableCarouselBeatmapSet>().Reverse().TakeWhile(i => i.Item is CarouselBeatmapSet s && s.BeatmapSet.DateSubmitted == null).Count(), () => Is.EqualTo(4));
AddAssert("rest are at start", () => carousel.Items.OfType<DrawableCarouselBeatmapSet>().TakeWhile(i => i.Item is CarouselBeatmapSet s && s.BeatmapSet.DateSubmitted != null).Count(),
() => Is.EqualTo(6));
AddStep("Sort by date submitted and string", () => carousel.Filter(new FilterCriteria
{
Sort = SortMode.DateSubmitted,
SearchText = zzz_string
}, false));
checkVisibleItemCount(diff: false, count: 5);
checkVisibleItemCount(diff: true, count: 5);
AddAssert("missing date are at end",
() => carousel.Items.OfType<DrawableCarouselBeatmapSet>().Reverse().TakeWhile(i => i.Item is CarouselBeatmapSet s && s.BeatmapSet.DateSubmitted == null).Count(), () => Is.EqualTo(2));
AddAssert("rest are at start", () => carousel.Items.OfType<DrawableCarouselBeatmapSet>().TakeWhile(i => i.Item is CarouselBeatmapSet s && s.BeatmapSet.DateSubmitted != null).Count(),
() => Is.EqualTo(3));
}
[Test]
public void TestSorting()
{
var sets = new List<BeatmapSetInfo>();
const string zzz_string = "zzzzz";
AddStep("Populuate beatmap sets", () =>
{
sets.Clear();
for (int i = 0; i < 20; i++)
{
var set = TestResources.CreateTestBeatmapSetInfo();
if (i == 4)
set.Beatmaps.ForEach(b => b.Metadata.Artist = zzz_string);
if (i == 16)
set.Beatmaps.ForEach(b => b.Metadata.Author.Username = zzz_string);
sets.Add(set);
}
});
loadBeatmaps(sets);
AddStep("Sort by author", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Author }, false));
AddAssert($"Check {zzz_string} is at bottom", () => carousel.BeatmapSets.Last().Metadata.Author.Username == zzz_string);
AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false));
AddAssert($"Check {zzz_string} is at bottom", () => carousel.BeatmapSets.Last().Metadata.Artist == zzz_string);
}
/// <summary>
/// Ensures stability is maintained on different sort modes for items with equal properties.
/// </summary>
[Test]
public void TestSortingStabilityDateAdded()
{
var sets = new List<BeatmapSetInfo>();
AddStep("Populuate beatmap sets", () =>
{
sets.Clear();
for (int i = 0; i < 10; i++)
{
var set = TestResources.CreateTestBeatmapSetInfo();
set.DateAdded = DateTimeOffset.FromUnixTimeSeconds(i);
// only need to set the first as they are a shared reference.
var beatmap = set.Beatmaps.First();
beatmap.Metadata.Artist = "a";
beatmap.Metadata.Title = "b";
sets.Add(set);
}
});
loadBeatmaps(sets);
AddStep("Sort by title", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Title }, false));
AddAssert("Items remain in descending added order", () => carousel.BeatmapSets.Select(s => s.DateAdded), () => Is.Ordered.Descending);
AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false));
AddAssert("Items remain in descending added order", () => carousel.BeatmapSets.Select(s => s.DateAdded), () => Is.Ordered.Descending);
}
/// <summary>
/// Ensures stability is maintained on different sort modes while a new item is added to the carousel.
/// </summary>
[Test]
public void TestSortingStabilityWithRemovedAndReaddedItem()
{
List<BeatmapSetInfo> sets = new List<BeatmapSetInfo>();
AddStep("Populuate beatmap sets", () =>
{
sets.Clear();
for (int i = 0; i < 3; i++)
{
var set = TestResources.CreateTestBeatmapSetInfo(3);
// only need to set the first as they are a shared reference.
var beatmap = set.Beatmaps.First();
beatmap.Metadata.Artist = "same artist";
beatmap.Metadata.Title = "same title";
// testing the case where DateAdded happens to equal (quite rare).
set.DateAdded = DateTimeOffset.UnixEpoch;
sets.Add(set);
}
});
Guid[] originalOrder = null!;
loadBeatmaps(sets);
AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false));
AddAssert("Items in descending added order", () => carousel.BeatmapSets.Select(s => s.DateAdded), () => Is.Ordered.Descending);
AddStep("Save order", () => originalOrder = carousel.BeatmapSets.Select(s => s.ID).ToArray());
AddStep("Remove item", () => carousel.RemoveBeatmapSet(sets[1]));
AddStep("Re-add item", () => carousel.UpdateBeatmapSet(sets[1]));
AddAssert("Order didn't change", () => carousel.BeatmapSets.Select(s => s.ID), () => Is.EqualTo(originalOrder));
AddStep("Sort by title", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Title }, false));
AddAssert("Order didn't change", () => carousel.BeatmapSets.Select(s => s.ID), () => Is.EqualTo(originalOrder));
}
/// <summary>
/// Ensures stability is maintained on different sort modes while a new item is added to the carousel.
/// </summary>
[Test]
public void TestSortingStabilityWithNewItems()
{
List<BeatmapSetInfo> sets = new List<BeatmapSetInfo>();
AddStep("Populuate beatmap sets", () =>
{
sets.Clear();
for (int i = 0; i < 3; i++)
{
var set = TestResources.CreateTestBeatmapSetInfo(3);
// only need to set the first as they are a shared reference.
var beatmap = set.Beatmaps.First();
beatmap.Metadata.Artist = "same artist";
beatmap.Metadata.Title = "same title";
// testing the case where DateAdded happens to equal (quite rare).
set.DateAdded = DateTimeOffset.UnixEpoch;
sets.Add(set);
}
});
Guid[] originalOrder = null!;
loadBeatmaps(sets);
AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false));
AddAssert("Items in descending added order", () => carousel.BeatmapSets.Select(s => s.DateAdded), () => Is.Ordered.Descending);
AddStep("Save order", () => originalOrder = carousel.BeatmapSets.Select(s => s.ID).ToArray());
AddStep("Add new item", () =>
{
var set = TestResources.CreateTestBeatmapSetInfo();
// only need to set the first as they are a shared reference.
var beatmap = set.Beatmaps.First();
beatmap.Metadata.Artist = "same artist";
beatmap.Metadata.Title = "same title";
set.DateAdded = DateTimeOffset.FromUnixTimeSeconds(1);
carousel.UpdateBeatmapSet(set);
// add set to expected ordering
originalOrder = originalOrder.Prepend(set.ID).ToArray();
});
AddAssert("Order didn't change", () => carousel.BeatmapSets.Select(s => s.ID), () => Is.EqualTo(originalOrder));
AddStep("Sort by title", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Title }, false));
AddAssert("Order didn't change", () => carousel.BeatmapSets.Select(s => s.ID), () => Is.EqualTo(originalOrder));
}
[Test]
public void TestSortingWithFiltered()
{
List<BeatmapSetInfo> sets = new List<BeatmapSetInfo>();
AddStep("Populuate beatmap sets", () =>
{
sets.Clear();
for (int i = 0; i < 3; i++)
{
var set = TestResources.CreateTestBeatmapSetInfo(3);
set.Beatmaps[0].StarRating = 3 - i;
set.Beatmaps[2].StarRating = 6 + i;
sets.Add(set);
}
});
loadBeatmaps(sets);
AddStep("Filter to normal", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty, SearchText = "Normal" }, false));
AddAssert("Check first set at end", () => carousel.BeatmapSets.First().Equals(sets.Last()));
AddAssert("Check last set at start", () => carousel.BeatmapSets.Last().Equals(sets.First()));
AddStep("Filter to insane", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty, SearchText = "Insane" }, false));
AddAssert("Check first set at start", () => carousel.BeatmapSets.First().Equals(sets.First()));
AddAssert("Check last set at end", () => carousel.BeatmapSets.Last().Equals(sets.Last()));
}
[Test]
public void TestRemoveAll()
{
loadBeatmaps();
setSelected(2, 1);
AddAssert("Selection is non-null", () => currentSelection != null);
AddStep("Remove selected", () => carousel.RemoveBeatmapSet(carousel.SelectedBeatmapSet!));
waitForSelection(2);
AddStep("Remove first", () => carousel.RemoveBeatmapSet(carousel.BeatmapSets.First()));
AddStep("Remove first", () => carousel.RemoveBeatmapSet(carousel.BeatmapSets.First()));
waitForSelection(1);
AddUntilStep("Remove all", () =>
{
if (!carousel.BeatmapSets.Any()) return true;
carousel.RemoveBeatmapSet(carousel.BeatmapSets.Last());
return false;
});
checkNoSelection();
}
[Test]
public void TestEmptyTraversal()
{
loadBeatmaps(new List<BeatmapSetInfo>());
advanceSelection(direction: 1, diff: false);
checkNoSelection();
advanceSelection(direction: 1, diff: true);
checkNoSelection();
advanceSelection(direction: -1, diff: false);
checkNoSelection();
advanceSelection(direction: -1, diff: true);
checkNoSelection();
}
[Test]
public void TestHiding()
{
BeatmapSetInfo hidingSet = null;
List<BeatmapSetInfo> hiddenList = new List<BeatmapSetInfo>();
AddStep("create hidden set", () =>
{
hidingSet = TestResources.CreateTestBeatmapSetInfo(3);
hidingSet.Beatmaps[1].Hidden = true;
hiddenList.Clear();
hiddenList.Add(hidingSet);
});
loadBeatmaps(hiddenList);
setSelected(1, 1);
checkVisibleItemCount(true, 2);
advanceSelection(true);
waitForSelection(1, 3);
setHidden(3);
waitForSelection(1, 1);
setHidden(2, false);
advanceSelection(true);
waitForSelection(1, 2);
setHidden(1);
waitForSelection(1, 2);
setHidden(2);
checkNoSelection();
void setHidden(int diff, bool hidden = true)
{
AddStep((hidden ? "" : "un") + $"hide diff {diff}", () =>
{
hidingSet.Beatmaps[diff - 1].Hidden = hidden;
carousel.UpdateBeatmapSet(hidingSet);
});
}
}
[Test]
public void TestSelectingFilteredRuleset()
{
BeatmapSetInfo testMixed = null;
createCarousel(new List<BeatmapSetInfo>());
AddStep("add mixed ruleset beatmapset", () =>
{
testMixed = TestResources.CreateTestBeatmapSetInfo(3);
for (int i = 0; i <= 2; i++)
{
testMixed.Beatmaps[i].Ruleset = rulesets.AvailableRulesets.ElementAt(i);
}
carousel.UpdateBeatmapSet(testMixed);
});
AddStep("filter to ruleset 0", () =>
carousel.Filter(new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(0) }, false));
AddStep("select filtered map skipping filtered", () => carousel.SelectBeatmap(testMixed.Beatmaps[1], false));
AddAssert("unfiltered beatmap not selected", () => carousel.SelectedBeatmapInfo?.Ruleset.OnlineID == 0);
AddStep("remove mixed set", () =>
{
carousel.RemoveBeatmapSet(testMixed);
testMixed = null;
});
BeatmapSetInfo testSingle = null;
AddStep("add single ruleset beatmapset", () =>
{
testSingle = TestResources.CreateTestBeatmapSetInfo(3);
testSingle.Beatmaps.ForEach(b =>
{
b.Ruleset = rulesets.AvailableRulesets.ElementAt(1);
});
carousel.UpdateBeatmapSet(testSingle);
});
AddStep("select filtered map skipping filtered", () => carousel.SelectBeatmap(testSingle.Beatmaps[0], false));
checkNoSelection();
AddStep("remove single ruleset set", () => carousel.RemoveBeatmapSet(testSingle));
}
[Test]
public void TestCarouselRemembersSelection()
{
List<BeatmapSetInfo> manySets = new List<BeatmapSetInfo>();
AddStep("Populuate beatmap sets", () =>
{
manySets.Clear();
for (int i = 1; i <= 50; i++)
manySets.Add(TestResources.CreateTestBeatmapSetInfo(3));
});
loadBeatmaps(manySets);
advanceSelection(direction: 1, diff: false);
for (int i = 0; i < 5; i++)
{
AddStep("Toggle non-matching filter", () =>
{
carousel.Filter(new FilterCriteria { SearchText = Guid.NewGuid().ToString() }, false);
});
AddStep("Restore no filter", () =>
{
carousel.Filter(new FilterCriteria(), false);
eagerSelectedIDs.Add(carousel.SelectedBeatmapSet!.ID);
});
}
// always returns to same selection as long as it's available.
AddAssert("Selection was remembered", () => eagerSelectedIDs.Count == 1);
}
[Test]
public void TestFilteringByUserStarDifficulty()
{
BeatmapSetInfo set = null;
loadBeatmaps(new List<BeatmapSetInfo>());
AddStep("add mixed difficulty set", () =>
{
set = TestResources.CreateTestBeatmapSetInfo(1);
set.Beatmaps.Clear();
for (int i = 1; i <= 15; i++)
{
set.Beatmaps.Add(new BeatmapInfo(new OsuRuleset().RulesetInfo, new BeatmapDifficulty(), new BeatmapMetadata())
{
DifficultyName = $"Stars: {i}",
StarRating = i,
});
}
carousel.UpdateBeatmapSet(set);
});
AddStep("select added set", () => carousel.SelectBeatmap(set.Beatmaps[0], false));
AddStep("filter [5..]", () => carousel.Filter(new FilterCriteria { UserStarDifficulty = { Min = 5 } }));
AddUntilStep("Wait for debounce", () => !carousel.PendingFilterTask);
checkVisibleItemCount(true, 11);
AddStep("filter to [0..7]", () => carousel.Filter(new FilterCriteria { UserStarDifficulty = { Max = 7 } }));
AddUntilStep("Wait for debounce", () => !carousel.PendingFilterTask);
checkVisibleItemCount(true, 7);
AddStep("filter to [5..7]", () => carousel.Filter(new FilterCriteria { UserStarDifficulty = { Min = 5, Max = 7 } }));
AddUntilStep("Wait for debounce", () => !carousel.PendingFilterTask);
checkVisibleItemCount(true, 3);
AddStep("filter [2..2]", () => carousel.Filter(new FilterCriteria { UserStarDifficulty = { Min = 2, Max = 2 } }));
AddUntilStep("Wait for debounce", () => !carousel.PendingFilterTask);
checkVisibleItemCount(true, 1);
AddStep("filter to [0..]", () => carousel.Filter(new FilterCriteria { UserStarDifficulty = { Min = 0 } }));
AddUntilStep("Wait for debounce", () => !carousel.PendingFilterTask);
checkVisibleItemCount(true, 15);
}
[Test]
public void TestCarouselSelectsNextWhenPreviousIsFiltered()
{
List<BeatmapSetInfo> sets = new List<BeatmapSetInfo>();
// 10 sets that go osu! -> taiko -> catch -> osu! -> ...
for (int i = 0; i < 10; i++)
sets.Add(TestResources.CreateTestBeatmapSetInfo(5, new[] { getRuleset(i) }));
// Sort mode is important to keep the ruleset order
loadBeatmaps(sets, () => new FilterCriteria { Sort = SortMode.Title });
setSelected(1, 1);
for (int i = 1; i < 10; i++)
{
var rulesetInfo = getRuleset(i % 3);
AddStep($"Set ruleset to {rulesetInfo.ShortName}", () =>
{
carousel.Filter(new FilterCriteria { Ruleset = rulesetInfo, Sort = SortMode.Title }, false);
});
waitForSelection(i + 1, 1);
}
static RulesetInfo getRuleset(int index)
{
switch (index % 3)
{
default:
return new OsuRuleset().RulesetInfo;
case 1:
return new TaikoRuleset().RulesetInfo;
case 2:
return new CatchRuleset().RulesetInfo;
}
}
}
[Test]
public void TestCarouselSelectsBackwardsWhenDistanceIsShorter()
{
List<BeatmapSetInfo> sets = new List<BeatmapSetInfo>();
// 10 sets that go taiko, osu!, osu!, osu!, taiko, osu!, osu!, osu!, ...
for (int i = 0; i < 10; i++)
sets.Add(TestResources.CreateTestBeatmapSetInfo(5, new[] { getRuleset(i) }));
// Sort mode is important to keep the ruleset order
loadBeatmaps(sets, () => new FilterCriteria { Sort = SortMode.Title });
for (int i = 2; i < 10; i += 4)
{
setSelected(i, 1);
AddStep("Set ruleset to taiko", () =>
{
carousel.Filter(new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(1), Sort = SortMode.Title }, false);
});
waitForSelection(i - 1, 1);
AddStep("Remove ruleset filter", () =>
{
carousel.Filter(new FilterCriteria { Sort = SortMode.Title }, false);
});
}
static RulesetInfo getRuleset(int index)
{
switch (index % 4)
{
case 0:
return new TaikoRuleset().RulesetInfo;
default:
return new OsuRuleset().RulesetInfo;
}
}
}
private void loadBeatmaps(List<BeatmapSetInfo> beatmapSets = null, Func<FilterCriteria> initialCriteria = null, Action<BeatmapCarousel> carouselAdjust = null, int? count = null,
bool randomDifficulties = false)
{
bool changed = false;
if (beatmapSets == null)
{
beatmapSets = new List<BeatmapSetInfo>();
for (int i = 1; i <= (count ?? set_count); i++)
{
beatmapSets.Add(randomDifficulties
? TestResources.CreateTestBeatmapSetInfo()
: TestResources.CreateTestBeatmapSetInfo(3));
}
}
createCarousel(beatmapSets, c =>
{
carouselAdjust?.Invoke(c);
carousel.Filter(initialCriteria?.Invoke() ?? new FilterCriteria());
carousel.BeatmapSetsChanged = () => changed = true;
carousel.BeatmapSets = beatmapSets;
});
AddUntilStep("Wait for load", () => changed);
}
private void createCarousel(List<BeatmapSetInfo> beatmapSets, Action<BeatmapCarousel> carouselAdjust = null, Container target = null)
{
AddStep("Create carousel", () =>
{
selectedSets.Clear();
eagerSelectedIDs.Clear();
carousel = new TestBeatmapCarousel
{
RelativeSizeAxes = Axes.Both,
};
carouselAdjust?.Invoke(carousel);
carousel.BeatmapSets = beatmapSets;
(target ?? this).Child = carousel;
});
}
private void ensureRandomFetchSuccess() =>
AddAssert("ensure prev random fetch worked", () => selectedSets.Peek().Equals(carousel.SelectedBeatmapSet));
private void waitForSelection(int set, int? diff = null) =>
AddUntilStep($"selected is set{set}{(diff.HasValue ? $" diff{diff.Value}" : "")}", () =>
{
if (diff != null)
return carousel.SelectedBeatmapInfo?.Equals(carousel.BeatmapSets.Skip(set - 1).First().Beatmaps.Skip(diff.Value - 1).First()) == true;
return carousel.BeatmapSets.Skip(set - 1).First().Beatmaps.Contains(carousel.SelectedBeatmapInfo);
});
private void setSelected(int set, int diff) =>
AddStep($"select set{set} diff{diff}", () =>
carousel.SelectBeatmap(carousel.BeatmapSets.Skip(set - 1).First().Beatmaps.Skip(diff - 1).First()));
private void advanceSelection(bool diff, int direction = 1, int count = 1)
{
if (count == 1)
{
AddStep($"select {(direction > 0 ? "next" : "prev")} {(diff ? "diff" : "set")}", () =>
carousel.SelectNext(direction, !diff));
}
else
{
AddRepeatStep($"select {(direction > 0 ? "next" : "prev")} {(diff ? "diff" : "set")}", () =>
carousel.SelectNext(direction, !diff), count);
}
}
private void checkVisibleItemCount(bool diff, int count)
{
// until step required as we are querying against alive items, which are loaded asynchronously inside DrawableCarouselBeatmapSet.
AddUntilStep($"{count} {(diff ? "diffs" : "sets")} visible", () =>
carousel.Items.Count(s => (diff ? s.Item is CarouselBeatmap : s.Item is CarouselBeatmapSet) && s.Item.Visible), () => Is.EqualTo(count));
}
private void checkSelectionIsCentered()
{
AddAssert("Selected panel is centered", () =>
{
return Precision.AlmostEquals(
carousel.ScreenSpaceDrawQuad.Centre,
carousel.Items
.First(i => i.Item?.State.Value == CarouselItemState.Selected)
.ScreenSpaceDrawQuad.Centre, 100);
});
}
private void checkNoSelection() => AddAssert("Selection is null", () => currentSelection == null);
private void nextRandom() =>
AddStep("select random next", () =>
{
carousel.RandomAlgorithm.Value = RandomSelectAlgorithm.RandomPermutation;
if (!selectedSets.Any() && carousel.SelectedBeatmapInfo != null)
selectedSets.Push(carousel.SelectedBeatmapSet);
carousel.SelectNextRandom();
selectedSets.Push(carousel.SelectedBeatmapSet);
});
private void ensureRandomDidntRepeat() =>
AddAssert("ensure no repeats", () => selectedSets.Distinct().Count() == selectedSets.Count);
private void prevRandom() => AddStep("select random last", () =>
{
carousel.SelectPreviousRandom();
selectedSets.Pop();
});
private bool selectedBeatmapVisible()
{
var currentlySelected = carousel.Items.FirstOrDefault(s => s.Item is CarouselBeatmap && s.Item.State.Value == CarouselItemState.Selected);
if (currentlySelected == null)
return true;
return currentlySelected.Item!.Visible;
}
private void checkInvisibleDifficultiesUnselectable()
{
nextRandom();
AddAssert("Selection is visible", selectedBeatmapVisible);
}
private partial class TestBeatmapCarousel : BeatmapCarousel
{
public bool PendingFilterTask => PendingFilter != null;
public IEnumerable<DrawableCarouselItem> Items
{
get
{
foreach (var item in Scroll.Children.OrderBy(c => c.Y))
{
if (item.Item?.Visible != true)
continue;
yield return item;
if (item is DrawableCarouselBeatmapSet set)
{
foreach (var difficulty in set.DrawableBeatmaps)
yield return difficulty;
}
}
}
}
}
}
}