mirror of
https://github.com/ppy/osu.git
synced 2026-05-16 09:42:53 +08:00
485 lines
21 KiB
C#
485 lines
21 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.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using NUnit.Framework;
|
|
using osu.Framework.Allocation;
|
|
using osu.Framework.Bindables;
|
|
using osu.Framework.Extensions;
|
|
using osu.Framework.Extensions.ObjectExtensions;
|
|
using osu.Framework.Graphics;
|
|
using osu.Framework.Graphics.Containers;
|
|
using osu.Framework.Graphics.Shapes;
|
|
using osu.Framework.Testing;
|
|
using osu.Framework.Utils;
|
|
using osu.Game.Beatmaps;
|
|
using osu.Game.Collections;
|
|
using osu.Game.Database;
|
|
using osu.Game.Graphics;
|
|
using osu.Game.Graphics.Carousel;
|
|
using osu.Game.Graphics.Containers;
|
|
using osu.Game.Overlays;
|
|
using osu.Game.Scoring;
|
|
using osu.Game.Screens.Select;
|
|
using osu.Game.Screens.Select.Filter;
|
|
using osu.Game.Screens.SelectV2;
|
|
using osu.Game.Tests.Beatmaps;
|
|
using osu.Game.Tests.Resources;
|
|
using osuTK;
|
|
using osuTK.Graphics;
|
|
using osuTK.Input;
|
|
using BeatmapCarousel = osu.Game.Screens.SelectV2.BeatmapCarousel;
|
|
|
|
namespace osu.Game.Tests.Visual.SongSelectV2
|
|
{
|
|
public abstract partial class BeatmapCarouselTestScene : OsuManualInputManagerTestScene
|
|
{
|
|
protected readonly Stack<BeatmapSetInfo> BeatmapSetRequestedSelections = new Stack<BeatmapSetInfo>();
|
|
protected readonly Stack<BeatmapInfo> BeatmapRequestedSelections = new Stack<BeatmapInfo>();
|
|
|
|
protected readonly BindableList<BeatmapSetInfo> BeatmapSets = new BindableList<BeatmapSetInfo>();
|
|
|
|
protected TestBeatmapCarousel Carousel = null!;
|
|
|
|
protected OsuScrollContainer<Drawable> Scroll => Carousel.ChildrenOfType<OsuScrollContainer<Drawable>>().Single();
|
|
|
|
[Cached(typeof(BeatmapStore))]
|
|
private BeatmapStore store;
|
|
|
|
[Cached]
|
|
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
|
|
|
|
public Func<IEnumerable<BeatmapInfo>, BeatmapInfo>? BeatmapRecommendationFunction { get; set; }
|
|
|
|
private OsuTextFlowContainer stats = null!;
|
|
|
|
private int beatmapCount;
|
|
|
|
protected int NewItemsPresentedInvocationCount;
|
|
|
|
protected BeatmapCarouselTestScene()
|
|
{
|
|
store = new TestBeatmapStore
|
|
{
|
|
BeatmapSets = { BindTarget = BeatmapSets }
|
|
};
|
|
|
|
BeatmapSets.BindCollectionChanged((_, _) => beatmapCount = BeatmapSets.Sum(s => s.Beatmaps.Count));
|
|
|
|
Scheduler.AddDelayed(updateStats, 100, true);
|
|
}
|
|
|
|
[BackgroundDependencyLoader]
|
|
private void load()
|
|
{
|
|
Dependencies.Cache(Realm);
|
|
}
|
|
|
|
protected void CreateCarousel()
|
|
{
|
|
AddStep("create components", () =>
|
|
{
|
|
BeatmapRequestedSelections.Clear();
|
|
BeatmapSetRequestedSelections.Clear();
|
|
BeatmapRecommendationFunction = null;
|
|
NewItemsPresentedInvocationCount = 0;
|
|
|
|
Box topBox;
|
|
Children = new Drawable[]
|
|
{
|
|
new GridContainer
|
|
{
|
|
RelativeSizeAxes = Axes.Both,
|
|
ColumnDimensions = new[]
|
|
{
|
|
new Dimension(GridSizeMode.Relative, 1),
|
|
},
|
|
RowDimensions = new[]
|
|
{
|
|
new Dimension(GridSizeMode.Absolute, 200),
|
|
new Dimension(),
|
|
new Dimension(GridSizeMode.Absolute, 200),
|
|
},
|
|
Content = new[]
|
|
{
|
|
new Drawable[]
|
|
{
|
|
topBox = new Box
|
|
{
|
|
Anchor = Anchor.Centre,
|
|
Origin = Anchor.Centre,
|
|
Colour = Color4.Cyan,
|
|
RelativeSizeAxes = Axes.Both,
|
|
Alpha = 0.4f,
|
|
},
|
|
},
|
|
new Drawable[]
|
|
{
|
|
Carousel = new TestBeatmapCarousel
|
|
{
|
|
NewItemsPresented = _ => NewItemsPresentedInvocationCount++,
|
|
RequestSelection = b =>
|
|
{
|
|
BeatmapRequestedSelections.Push(b.Beatmap);
|
|
Carousel.CurrentGroupedBeatmap = b;
|
|
},
|
|
RequestRecommendedSelection = groupedBeatmaps =>
|
|
{
|
|
var recommendedBeatmap = BeatmapRecommendationFunction?.Invoke(groupedBeatmaps.Select(gb => gb.Beatmap)) ?? groupedBeatmaps.First().Beatmap;
|
|
var recommendedGroupedBeatmap = groupedBeatmaps.First(gb => gb.Beatmap.Equals(recommendedBeatmap));
|
|
BeatmapSetRequestedSelections.Push(recommendedBeatmap.BeatmapSet!);
|
|
Carousel.CurrentGroupedBeatmap = recommendedGroupedBeatmap;
|
|
},
|
|
BleedTop = 50,
|
|
BleedBottom = 50,
|
|
Anchor = Anchor.Centre,
|
|
Origin = Anchor.Centre,
|
|
Width = 800,
|
|
RelativeSizeAxes = Axes.Y,
|
|
},
|
|
},
|
|
new[]
|
|
{
|
|
new Box
|
|
{
|
|
Anchor = Anchor.Centre,
|
|
Origin = Anchor.Centre,
|
|
Colour = Color4.Cyan,
|
|
RelativeSizeAxes = Axes.Both,
|
|
Alpha = 0.4f,
|
|
},
|
|
topBox.CreateProxy(),
|
|
}
|
|
}
|
|
},
|
|
stats = new OsuTextFlowContainer
|
|
{
|
|
AutoSizeAxes = Axes.Both,
|
|
Padding = new MarginPadding(10),
|
|
TextAnchor = Anchor.CentreLeft,
|
|
},
|
|
};
|
|
|
|
// Prefer title sorting so that order of carousel panels match order of BeatmapSets bindable.
|
|
Carousel.Filter(new FilterCriteria { Sort = SortMode.Title });
|
|
});
|
|
}
|
|
|
|
protected void SortBy(SortMode mode) => ApplyToFilterAndWaitForFilter($"sort by {mode.GetDescription().ToLowerInvariant()}", c => c.Sort = mode);
|
|
|
|
protected void GroupBy(GroupMode mode) => ApplyToFilterAndWaitForFilter($"group by {mode.GetDescription().ToLowerInvariant()}", c => c.Group = mode);
|
|
|
|
protected void SortAndGroupBy(SortMode sort, GroupMode group)
|
|
{
|
|
ApplyToFilterAndWaitForFilter($"sort by {sort.GetDescription().ToLowerInvariant()} & group by {group.GetDescription().ToLowerInvariant()}", c =>
|
|
{
|
|
c.Sort = sort;
|
|
c.Group = group;
|
|
});
|
|
}
|
|
|
|
protected void ApplyToFilterAndWaitForFilter(string description, Action<FilterCriteria>? apply)
|
|
{
|
|
AddStep(description, () =>
|
|
{
|
|
var criteria = Carousel.Criteria ?? new FilterCriteria();
|
|
apply?.Invoke(criteria);
|
|
Carousel.Filter(criteria);
|
|
});
|
|
|
|
WaitForFiltering();
|
|
}
|
|
|
|
protected void WaitForDrawablePanels() => AddUntilStep("drawable panels loaded", () => Carousel.ChildrenOfType<ICarouselPanel>().Count(), () => Is.GreaterThan(0));
|
|
protected void WaitForFiltering() => AddUntilStep("filtering finished", () => Carousel.IsFiltering, () => Is.False);
|
|
protected void WaitForScrolling() => AddUntilStep("scroll finished", () => Scroll.Current, () => Is.EqualTo(Scroll.Target));
|
|
|
|
protected void ToggleGroupCollapse() => AddStep("toggle group collapse", () =>
|
|
{
|
|
InputManager.PressKey(Key.ShiftLeft);
|
|
InputManager.Key(Key.Enter);
|
|
InputManager.ReleaseKey(Key.ShiftLeft);
|
|
});
|
|
|
|
protected void SelectNextGroup() => AddStep("select next group", () =>
|
|
{
|
|
InputManager.PressKey(Key.ShiftLeft);
|
|
InputManager.Key(Key.Right);
|
|
InputManager.ReleaseKey(Key.ShiftLeft);
|
|
});
|
|
|
|
protected void SelectPrevGroup() => AddStep("select prev group", () =>
|
|
{
|
|
InputManager.PressKey(Key.ShiftLeft);
|
|
InputManager.Key(Key.Left);
|
|
InputManager.ReleaseKey(Key.ShiftLeft);
|
|
});
|
|
|
|
protected void SelectNextPanel() => AddStep("select next panel", () => InputManager.Key(Key.Down));
|
|
protected void SelectPrevPanel() => AddStep("select prev panel", () => InputManager.Key(Key.Up));
|
|
protected void SelectNextSet() => AddStep("select next set", () => InputManager.Key(Key.Right));
|
|
protected void SelectPrevSet() => AddStep("select prev set", () => InputManager.Key(Key.Left));
|
|
|
|
protected void Select() => AddStep("select", () => InputManager.Key(Key.Enter));
|
|
|
|
protected void CheckNoSelection() => AddAssert("has no selection", () => Carousel.CurrentGroupedBeatmap, () => Is.Null);
|
|
protected void CheckHasSelection() => AddAssert("has selection", () => Carousel.CurrentGroupedBeatmap, () => Is.Not.Null);
|
|
|
|
protected void CheckRequestPresentCount(int expected) =>
|
|
AddAssert($"check present count is {expected}", () => Carousel.RequestPresentBeatmapCount, () => Is.EqualTo(expected));
|
|
|
|
protected void CheckActivationCount(int expected) =>
|
|
AddAssert($"check activation count is {expected}", () => Carousel.ActivationCount, () => Is.EqualTo(expected));
|
|
|
|
protected void CheckDisplayedBeatmapsCount(int expected)
|
|
{
|
|
AddAssert($"{expected} diffs displayed", () => Carousel.MatchedBeatmapsCount, () => Is.EqualTo(expected));
|
|
}
|
|
|
|
protected void CheckDisplayedBeatmapSetsCount(int expected)
|
|
{
|
|
AddAssert($"{expected} sets displayed", () =>
|
|
{
|
|
var groupingFilter = Carousel.Filters.OfType<BeatmapCarouselFilterGrouping>().Single();
|
|
|
|
// Using groupingFilter.SetItems.Count alone doesn't work.
|
|
// When sorting by difficulty, there can be more than one set panel for the same set displayed.
|
|
return groupingFilter.SetItems.Sum(s => s.Value.Count(i => i.Model is GroupedBeatmapSet));
|
|
}, () => Is.EqualTo(expected));
|
|
}
|
|
|
|
protected void CheckDisplayedGroupsCount(int expected)
|
|
{
|
|
AddAssert($"{expected} groups displayed", () =>
|
|
{
|
|
var groupingFilter = Carousel.Filters.OfType<BeatmapCarouselFilterGrouping>().Single();
|
|
return groupingFilter.GroupItems.Count;
|
|
}, () => Is.EqualTo(expected));
|
|
}
|
|
|
|
protected ICarouselPanel? GetSelectedPanel() => Carousel.ChildrenOfType<ICarouselPanel>().SingleOrDefault(p => p.Selected.Value);
|
|
protected ICarouselPanel? GetKeyboardSelectedPanel() => Carousel.ChildrenOfType<ICarouselPanel>().SingleOrDefault(p => p.KeyboardSelected.Value);
|
|
|
|
protected void WaitForExpandedGroup(int group)
|
|
{
|
|
AddUntilStep($"group {group} is expanded", () =>
|
|
{
|
|
var groupingFilter = Carousel.Filters.OfType<BeatmapCarouselFilterGrouping>().Single();
|
|
|
|
GroupDefinition g = groupingFilter.GroupItems.Keys.ElementAt(group);
|
|
// offset by one because the group itself is included in the items list.
|
|
CarouselItem item = groupingFilter.GroupItems[g].ElementAt(0);
|
|
|
|
return item.Model is GroupDefinition def && def == Carousel.ExpandedGroup;
|
|
});
|
|
}
|
|
|
|
protected void WaitForBeatmapSelection(int group, int panel)
|
|
{
|
|
AddUntilStep($"selected is group{group} panel{panel}", () =>
|
|
{
|
|
var groupingFilter = Carousel.Filters.OfType<BeatmapCarouselFilterGrouping>().Single();
|
|
|
|
GroupDefinition? groupDefinition = groupingFilter.GroupItems.Keys.ElementAtOrDefault(group);
|
|
|
|
if (groupDefinition == null)
|
|
return false;
|
|
|
|
// offset by one because the group itself is included in the items list.
|
|
CarouselItem item = groupingFilter.GroupItems[groupDefinition].ElementAt(panel + 1);
|
|
|
|
return Carousel.CurrentGroupedBeatmap?.Equals(item.Model as GroupedBeatmap) == true;
|
|
});
|
|
}
|
|
|
|
protected void WaitForSetSelection(int set, int? diff = null)
|
|
{
|
|
if (diff != null)
|
|
{
|
|
AddUntilStep($"selected is set{set} diff{diff.Value}",
|
|
() => Carousel.CurrentBeatmap,
|
|
() => Is.EqualTo(BeatmapSets[set].Beatmaps[diff.Value]));
|
|
}
|
|
else
|
|
{
|
|
AddUntilStep($"selected is set{set}", () => BeatmapSets[set].Beatmaps.Contains(Carousel.CurrentBeatmap!));
|
|
}
|
|
}
|
|
|
|
protected IEnumerable<T> GetVisiblePanels<T>()
|
|
where T : Drawable
|
|
{
|
|
return Carousel.ChildrenOfType<UserTrackingScrollContainer>().Single()
|
|
.ChildrenOfType<T>()
|
|
.Where(p => ((ICarouselPanel)p).Item?.IsVisible == true)
|
|
.OrderBy(p => p.Y);
|
|
}
|
|
|
|
protected void ClickVisiblePanel<T>(int index)
|
|
where T : Drawable
|
|
{
|
|
AddStep($"click panel at index {index}", () =>
|
|
{
|
|
Carousel.ChildrenOfType<UserTrackingScrollContainer>().Single()
|
|
.ChildrenOfType<T>()
|
|
.Where(p => ((ICarouselPanel)p).Item?.IsVisible == true)
|
|
.OrderBy(p => p.Y)
|
|
.ElementAt(index)
|
|
.ChildrenOfType<Panel>().Single()
|
|
.TriggerClick();
|
|
});
|
|
}
|
|
|
|
protected void ClickVisiblePanelWithOffset<T>(int index, Vector2 positionOffsetFromCentre)
|
|
where T : Drawable
|
|
{
|
|
AddStep($"move mouse to panel {index} with offset {positionOffsetFromCentre}", () =>
|
|
{
|
|
var panel = Carousel.ChildrenOfType<UserTrackingScrollContainer>().Single()
|
|
.ChildrenOfType<T>()
|
|
.Where(p => ((ICarouselPanel)p).Item?.IsVisible == true)
|
|
.OrderBy(p => p.Y)
|
|
.ElementAt(index);
|
|
|
|
InputManager.MoveMouseTo(panel.ScreenSpaceDrawQuad.Centre + panel.ToScreenSpace(positionOffsetFromCentre) - panel.ToScreenSpace(Vector2.Zero));
|
|
});
|
|
|
|
AddStep("click", () => InputManager.Click(MouseButton.Left));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add requested beatmap sets count to list.
|
|
/// </summary>
|
|
/// <param name="count">The count of beatmap sets to add.</param>
|
|
/// <param name="fixedDifficultiesPerSet">If not null, the number of difficulties per set. If null, randomised difficulty count will be used.</param>
|
|
/// <param name="randomMetadata">Whether to randomise the metadata to make groupings more uniform.</param>
|
|
protected void AddBeatmaps(int count, int? fixedDifficultiesPerSet = null, bool randomMetadata = false) => AddStep($"add {count} beatmaps{(randomMetadata ? " with random data" : "")}", () =>
|
|
{
|
|
var beatmaps = new List<BeatmapSetInfo>();
|
|
|
|
for (int i = 0; i < count; i++)
|
|
beatmaps.Add(CreateTestBeatmapSetInfo(fixedDifficultiesPerSet, randomMetadata));
|
|
|
|
BeatmapSets.AddRange(beatmaps);
|
|
});
|
|
|
|
protected static BeatmapSetInfo CreateTestBeatmapSetInfo(int? fixedDifficultiesPerSet, bool randomMetadata)
|
|
{
|
|
var beatmapSetInfo = TestResources.CreateTestBeatmapSetInfo(fixedDifficultiesPerSet ?? RNG.Next(1, 4));
|
|
|
|
if (randomMetadata)
|
|
{
|
|
char randomCharacter = getRandomCharacter();
|
|
|
|
var metadata = new BeatmapMetadata
|
|
{
|
|
// Create random metadata, then we can check if sorting works based on these
|
|
Artist = $"{randomCharacter}ome Artist " + RNG.Next(0, 9),
|
|
Title = $"{randomCharacter}ome Song (set id {beatmapSetInfo.OnlineID:000}) {Guid.NewGuid()}",
|
|
Author = { Username = $"{randomCharacter}ome Guy " + RNG.Next(0, 9) },
|
|
};
|
|
|
|
foreach (var beatmap in beatmapSetInfo.Beatmaps)
|
|
beatmap.Metadata = metadata.DeepClone();
|
|
}
|
|
|
|
return beatmapSetInfo;
|
|
}
|
|
|
|
private static long randomCharPointer;
|
|
|
|
private static char getRandomCharacter()
|
|
{
|
|
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz*";
|
|
return chars[(int)((randomCharPointer++ / 2) % chars.Length)];
|
|
}
|
|
|
|
protected void RemoveAllBeatmaps() => AddStep("clear all beatmaps", () => BeatmapSets.Clear());
|
|
|
|
protected void RemoveFirstBeatmap() =>
|
|
AddStep("remove first beatmap", () =>
|
|
{
|
|
if (BeatmapSets.Count == 0) return;
|
|
|
|
BeatmapSets.Remove(BeatmapSets.First());
|
|
});
|
|
|
|
private void updateStats()
|
|
{
|
|
if (Carousel.IsNull())
|
|
return;
|
|
|
|
stats.Clear();
|
|
createHeader("beatmap store");
|
|
stats.AddParagraph($"""
|
|
sets: {BeatmapSets.Count}
|
|
beatmaps: {beatmapCount}
|
|
""");
|
|
createHeader("carousel");
|
|
stats.AddParagraph($"""
|
|
filtering: {Carousel.IsFiltering} (total {Carousel.FilterCount} times)
|
|
tracked: {Carousel.ItemsTracked}
|
|
displayable: {Carousel.DisplayableItems}
|
|
displayed: {Carousel.VisibleItems}
|
|
selected: {Carousel.CurrentGroupedBeatmap}
|
|
""");
|
|
|
|
void createHeader(string text)
|
|
{
|
|
stats.AddParagraph(string.Empty);
|
|
stats.AddParagraph(text, cp =>
|
|
{
|
|
cp.Font = cp.Font.With(size: 18, weight: FontWeight.Bold);
|
|
});
|
|
}
|
|
}
|
|
|
|
public partial class TestBeatmapCarousel : BeatmapCarousel
|
|
{
|
|
public int ActivationCount { get; private set; }
|
|
public int RequestPresentBeatmapCount { get; private set; }
|
|
|
|
public int FilterDelay = 0;
|
|
|
|
public IEnumerable<BeatmapInfo> PostFilterBeatmaps = null!;
|
|
|
|
public BeatmapInfo? SelectedBeatmapInfo => (CurrentSelection as GroupedBeatmap)?.Beatmap;
|
|
public BeatmapSetInfo? SelectedBeatmapSet => SelectedBeatmapInfo?.BeatmapSet;
|
|
|
|
public new GroupedBeatmapSet? ExpandedBeatmapSet => base.ExpandedBeatmapSet;
|
|
public new GroupDefinition? ExpandedGroup => base.ExpandedGroup;
|
|
|
|
public Func<List<BeatmapCollection>> AllCollections { get; set; } = () => [];
|
|
public Func<FilterCriteria, Dictionary<Guid, ScoreRank>> BeatmapInfoGuidToTopRankMapping { get; set; } = _ => new Dictionary<Guid, ScoreRank>();
|
|
|
|
public TestBeatmapCarousel()
|
|
{
|
|
RequestPresentBeatmap = _ => RequestPresentBeatmapCount++;
|
|
}
|
|
|
|
protected override void HandleItemActivated(CarouselItem item)
|
|
{
|
|
ActivationCount++;
|
|
base.HandleItemActivated(item);
|
|
}
|
|
|
|
protected override async Task<IEnumerable<CarouselItem>> FilterAsync(bool clearExistingPanels = false)
|
|
{
|
|
var items = await base.FilterAsync(clearExistingPanels).ConfigureAwait(true);
|
|
|
|
if (FilterDelay != 0)
|
|
await Task.Delay(FilterDelay).ConfigureAwait(true);
|
|
|
|
PostFilterBeatmaps = items.Select(i => i.Model).OfType<GroupedBeatmap>().Select(i => i.Beatmap);
|
|
return items;
|
|
}
|
|
|
|
protected override List<BeatmapCollection> GetAllCollections() => AllCollections.Invoke();
|
|
protected override Dictionary<Guid, ScoreRank> GetBeatmapInfoGuidToTopRankMapping(FilterCriteria criteria) => BeatmapInfoGuidToTopRankMapping.Invoke(criteria);
|
|
}
|
|
}
|
|
}
|