mirror of
https://github.com/ppy/osu.git
synced 2026-05-19 15:40:14 +08:00
Add test coverage for song select filtering
This commit is contained in:
@@ -0,0 +1,336 @@
|
||||
// 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.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Toolbar;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mania.Mods;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Screens;
|
||||
using osu.Game.Screens.Footer;
|
||||
using osu.Game.Screens.Menu;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
using osu.Game.Tests.Resources;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
public partial class TestSceneSongSelectFiltering : ScreenTestScene
|
||||
{
|
||||
private BeatmapManager manager = null!;
|
||||
private RulesetStore rulesets = null!;
|
||||
private MusicController music = null!;
|
||||
private OsuConfigManager config = null!;
|
||||
|
||||
private SoloSongSelect songSelect = null!;
|
||||
private BeatmapCarousel carousel => songSelect.ChildrenOfType<BeatmapCarousel>().Single();
|
||||
|
||||
private FilterControl filter => songSelect.ChildrenOfType<FilterControl>().Single();
|
||||
private ShearedFilterTextBox filterTextBox => songSelect.ChildrenOfType<ShearedFilterTextBox>().Single();
|
||||
private int filterOperationsCount;
|
||||
|
||||
[Cached]
|
||||
private readonly ScreenFooter screenFooter;
|
||||
|
||||
[Cached]
|
||||
private readonly OsuLogo logo;
|
||||
|
||||
[Cached(typeof(INotificationOverlay))]
|
||||
private readonly INotificationOverlay notificationOverlay = new NotificationOverlay();
|
||||
|
||||
public TestSceneSongSelectFiltering()
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new PopoverContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Toolbar
|
||||
{
|
||||
State = { Value = Visibility.Visible },
|
||||
},
|
||||
screenFooter = new ScreenFooter
|
||||
{
|
||||
OnBack = () => Stack.CurrentScreen.Exit(),
|
||||
},
|
||||
logo = new OsuLogo
|
||||
{
|
||||
Alpha = 0f,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
Stack.Padding = new MarginPadding { Top = Toolbar.HEIGHT };
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(GameHost host)
|
||||
{
|
||||
RealmDetachedBeatmapStore beatmapStore;
|
||||
|
||||
// These DI caches are required to ensure for interactive runs this test scene doesn't nuke all user beatmaps in the local install.
|
||||
// At a point we have isolated interactive test runs enough, this can likely be removed.
|
||||
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
|
||||
Dependencies.Cache(Realm);
|
||||
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, null, Audio, Resources, host, Beatmap.Default));
|
||||
Dependencies.CacheAs<BeatmapStore>(beatmapStore = new RealmDetachedBeatmapStore());
|
||||
|
||||
Dependencies.Cache(music = new MusicController());
|
||||
|
||||
// required to get bindables attached
|
||||
Add(music);
|
||||
Add(beatmapStore);
|
||||
|
||||
Dependencies.Cache(config = new OsuConfigManager(LocalStorage));
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Stack.ScreenPushed += updateFooter;
|
||||
Stack.ScreenExited += updateFooter;
|
||||
}
|
||||
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("reset defaults", () =>
|
||||
{
|
||||
Ruleset.Value = new OsuRuleset().RulesetInfo;
|
||||
|
||||
Beatmap.SetDefault();
|
||||
SelectedMods.SetDefault();
|
||||
|
||||
config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Title);
|
||||
config.SetValue(OsuSetting.SongSelectGroupingMode, GroupMode.All);
|
||||
|
||||
songSelect = null!;
|
||||
filterOperationsCount = 0;
|
||||
});
|
||||
|
||||
AddStep("delete all beatmaps", () => manager.Delete());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSingleFilterOnEnter()
|
||||
{
|
||||
importBeatmapForRuleset(0);
|
||||
importBeatmapForRuleset(0);
|
||||
|
||||
loadSongSelect();
|
||||
|
||||
AddAssert("filter count is 0", () => filterOperationsCount, () => Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNoFilterOnSimpleResume()
|
||||
{
|
||||
importBeatmapForRuleset(0);
|
||||
importBeatmapForRuleset(0);
|
||||
|
||||
loadSongSelect();
|
||||
|
||||
AddStep("push child screen", () => Stack.Push(new TestSceneOsuScreenStack.TestScreen("test child")));
|
||||
waitForSuspension();
|
||||
|
||||
AddStep("return", () => songSelect.MakeCurrent());
|
||||
AddUntilStep("wait for current", () => songSelect.IsCurrentScreen());
|
||||
AddAssert("filter count is 0", () => filterOperationsCount, () => Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFilterOnResumeAfterChange()
|
||||
{
|
||||
importBeatmapForRuleset(0);
|
||||
importBeatmapForRuleset(0);
|
||||
|
||||
AddStep("change convert setting", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, false));
|
||||
|
||||
loadSongSelect();
|
||||
|
||||
AddStep("push child screen", () => Stack.Push(new TestSceneOsuScreenStack.TestScreen("test child")));
|
||||
waitForSuspension();
|
||||
|
||||
AddStep("change convert setting", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, true));
|
||||
|
||||
AddStep("return", () => songSelect.MakeCurrent());
|
||||
AddUntilStep("wait for current", () => songSelect.IsCurrentScreen());
|
||||
AddAssert("filter count is 1", () => filterOperationsCount, () => Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSorting()
|
||||
{
|
||||
loadSongSelect();
|
||||
addManyTestMaps();
|
||||
|
||||
// TODO: old test has this step, but there doesn't seem to be any purpose for it.
|
||||
// AddUntilStep("random map selected", () => Beatmap.Value != defaultBeatmap);
|
||||
|
||||
AddStep(@"Sort by Artist", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Artist));
|
||||
AddStep(@"Sort by Title", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Title));
|
||||
AddStep(@"Sort by Author", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Author));
|
||||
AddStep(@"Sort by DateAdded", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.DateAdded));
|
||||
AddStep(@"Sort by BPM", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.BPM));
|
||||
AddStep(@"Sort by Length", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Length));
|
||||
AddStep(@"Sort by Difficulty", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Difficulty));
|
||||
AddStep(@"Sort by Source", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Source));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCutInFilterTextBox()
|
||||
{
|
||||
loadSongSelect();
|
||||
|
||||
AddStep("set filter text", () => filterTextBox.Current.Value = "nonono");
|
||||
AddStep("select all", () => InputManager.Keys(PlatformAction.SelectAll));
|
||||
AddStep("press ctrl/cmd-x", () => InputManager.Keys(PlatformAction.Cut));
|
||||
|
||||
AddAssert("filter text cleared", () => filterTextBox.Current.Value, () => Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNonFilterableModChange()
|
||||
{
|
||||
importBeatmapForRuleset(0);
|
||||
|
||||
loadSongSelect();
|
||||
|
||||
// Mod that is guaranteed to never re-filter.
|
||||
AddStep("add non-filterable mod", () => SelectedMods.Value = new Mod[] { new OsuModCinema() });
|
||||
AddAssert("filter count is 0", () => filterOperationsCount, () => Is.EqualTo(0));
|
||||
|
||||
// Removing the mod should still not re-filter.
|
||||
AddStep("remove non-filterable mod", () => SelectedMods.Value = Array.Empty<Mod>());
|
||||
AddAssert("filter count is 0", () => filterOperationsCount, () => Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFilterableModChange()
|
||||
{
|
||||
importBeatmapForRuleset(3);
|
||||
|
||||
loadSongSelect();
|
||||
|
||||
// Change to mania ruleset.
|
||||
AddStep("filter to mania ruleset", () => Ruleset.Value = rulesets.AvailableRulesets.First(r => r.OnlineID == 3));
|
||||
AddAssert("filter count is 1", () => filterOperationsCount, () => Is.EqualTo(1));
|
||||
|
||||
// Apply a mod, but this should NOT re-filter because there's no search text.
|
||||
AddStep("add filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModKey3() });
|
||||
AddAssert("filter count is 1", () => filterOperationsCount, () => Is.EqualTo(1));
|
||||
|
||||
// Set search text. Should re-filter.
|
||||
AddStep("set search text to match mods", () => filterTextBox.Current.Value = "keys=3");
|
||||
AddAssert("filter count is 2", () => filterOperationsCount, () => Is.EqualTo(2));
|
||||
|
||||
// Change filterable mod. Should re-filter.
|
||||
AddStep("change new filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModKey5() });
|
||||
AddAssert("filter count is 3", () => filterOperationsCount, () => Is.EqualTo(3));
|
||||
|
||||
// Add non-filterable mod. Should NOT re-filter.
|
||||
AddStep("apply non-filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModNoFail(), new ManiaModKey5() });
|
||||
AddAssert("filter count is 3", () => filterOperationsCount, () => Is.EqualTo(3));
|
||||
|
||||
// Remove filterable mod. Should re-filter.
|
||||
AddStep("remove filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModNoFail() });
|
||||
AddAssert("filter count is 4", () => filterOperationsCount, () => Is.EqualTo(4));
|
||||
|
||||
// Remove non-filterable mod. Should NOT re-filter.
|
||||
AddStep("remove non-filterable mod", () => SelectedMods.Value = Array.Empty<Mod>());
|
||||
AddAssert("filter count is 4", () => filterOperationsCount, () => Is.EqualTo(4));
|
||||
|
||||
// Add filterable mod. Should re-filter.
|
||||
AddStep("add filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModKey3() });
|
||||
AddAssert("filter count is 5", () => filterOperationsCount, () => Is.EqualTo(5));
|
||||
}
|
||||
|
||||
private void loadSongSelect()
|
||||
{
|
||||
AddStep("load screen", () => Stack.Push(songSelect = new SoloSongSelect()));
|
||||
AddUntilStep("wait for load", () => Stack.CurrentScreen == songSelect && songSelect.IsLoaded);
|
||||
AddStep("hook events", () =>
|
||||
{
|
||||
filterOperationsCount = 0;
|
||||
filter.CriteriaChanged += _ => filterOperationsCount++;
|
||||
});
|
||||
}
|
||||
|
||||
private void importBeatmapForRuleset(int rulesetId)
|
||||
{
|
||||
int beatmapsCount = 0;
|
||||
|
||||
AddStep($"import test map for ruleset {rulesetId}", () =>
|
||||
{
|
||||
beatmapsCount = songSelect.IsNull() ? 0 : carousel.Filters.OfType<BeatmapCarouselFilterGrouping>().Single().SetItems.Count;
|
||||
manager.Import(TestResources.CreateTestBeatmapSetInfo(3, rulesets.AvailableRulesets.Where(r => r.OnlineID == rulesetId).ToArray()));
|
||||
});
|
||||
|
||||
// This is specifically for cases where the add is happening post song select load.
|
||||
// For cases where song select is null, the assertions are provided by the load checks.
|
||||
AddUntilStep("wait for imported to arrive in carousel", () => songSelect.IsNull() || carousel.Filters.OfType<BeatmapCarouselFilterGrouping>().Single().SetItems.Count > beatmapsCount);
|
||||
}
|
||||
|
||||
private void changeRuleset(int rulesetId)
|
||||
{
|
||||
AddStep($"change ruleset to {rulesetId}", () => Ruleset.Value = rulesets.AvailableRulesets.First(r => r.OnlineID == rulesetId));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Imports test beatmap sets to show in the carousel.
|
||||
/// </summary>
|
||||
/// <param name="difficultyCountPerSet">
|
||||
/// The exact count of difficulties to create for each beatmap set.
|
||||
/// A <see langword="null"/> value causes the count of difficulties to be selected randomly.
|
||||
/// </param>
|
||||
private void addManyTestMaps(int? difficultyCountPerSet = null)
|
||||
{
|
||||
AddStep("import test maps", () =>
|
||||
{
|
||||
var usableRulesets = rulesets.AvailableRulesets.Where(r => r.OnlineID != 2).ToArray();
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
manager.Import(TestResources.CreateTestBeatmapSetInfo(difficultyCountPerSet, usableRulesets));
|
||||
});
|
||||
}
|
||||
|
||||
private void waitForSuspension() => AddUntilStep("wait for not current", () => !songSelect.AsNonNull().IsCurrentScreen());
|
||||
|
||||
private void updateFooter(IScreen? _, IScreen? newScreen)
|
||||
{
|
||||
if (newScreen is IOsuScreen osuScreen && osuScreen.ShowFooter)
|
||||
{
|
||||
screenFooter.Show();
|
||||
screenFooter.SetButtons(osuScreen.CreateFooterButtons());
|
||||
}
|
||||
else
|
||||
{
|
||||
screenFooter.Hide();
|
||||
screenFooter.SetButtons(Array.Empty<ScreenFooterButton>());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -68,7 +68,7 @@ namespace osu.Game.Graphics.Carousel
|
||||
public int ItemsTracked => Items.Count;
|
||||
|
||||
/// <summary>
|
||||
/// The number of carousel items currently in rotation for display.
|
||||
/// The items currently in rotation for display.
|
||||
/// </summary>
|
||||
public int DisplayableItems => carouselItems?.Count ?? 0;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user