1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-17 22:22:54 +08:00
osu-lazer/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
2024-08-07 14:01:30 +02:00

1062 lines
50 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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Localisation;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Taiko.Mods;
using osu.Game.Screens.Footer;
using osu.Game.Tests.Mods;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.UserInterface
{
[TestFixture]
public partial class TestSceneModSelectOverlay : OsuManualInputManagerTestScene
{
protected override bool UseFreshStoragePerRun => true;
private RulesetStore rulesetStore = null!;
private TestModSelectOverlay modSelectOverlay = null!;
[Resolved]
private OsuConfigManager configManager { get; set; } = null!;
[BackgroundDependencyLoader]
private void load()
{
Dependencies.Cache(rulesetStore = new RealmRulesetStore(Realm));
Dependencies.Cache(Realm);
}
[SetUpSteps]
public void SetUpSteps()
{
AddStep("clear contents", Clear);
AddStep("reset ruleset", () => Ruleset.Value = rulesetStore.GetRuleset(0));
AddStep("reset mods", () => SelectedMods.SetDefault());
AddStep("reset config", () => configManager.SetValue(OsuSetting.ModSelectTextSearchStartsActive, true));
AddStep("reset mouse", () => InputManager.MoveMouseTo(Vector2.One));
AddStep("set beatmap", () => Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo));
AddStep("set up presets", () =>
{
Realm.Write(r =>
{
r.RemoveAll<ModPreset>();
r.Add(new ModPreset
{
Name = "AR0",
Description = "Too... many... circles...",
Ruleset = r.Find<RulesetInfo>(OsuRuleset.SHORT_NAME)!,
Mods = new[]
{
new OsuModDifficultyAdjust
{
ApproachRate = { Value = 0 }
}
}
});
r.Add(new ModPreset
{
Name = "Half Time 0.5x",
Description = "Very slow",
Ruleset = r.Find<RulesetInfo>(OsuRuleset.SHORT_NAME)!,
Mods = new[]
{
new OsuModHalfTime
{
SpeedChange = { Value = 0.5 }
}
}
});
});
});
}
private void createScreen()
{
AddStep("create screen", () =>
{
var receptor = new ScreenFooter.BackReceptor();
var footer = new ScreenFooter(receptor);
Child = new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies = new[] { (typeof(ScreenFooter), (object)footer) },
Children = new Drawable[]
{
receptor,
modSelectOverlay = new TestModSelectOverlay
{
RelativeSizeAxes = Axes.Both,
State = { Value = Visibility.Visible },
Beatmap = { Value = Beatmap.Value },
SelectedMods = { BindTarget = SelectedMods },
},
footer,
}
};
});
waitForColumnLoad();
}
[Test]
public void TestStateChange()
{
createScreen();
AddStep("toggle state", () => modSelectOverlay.ToggleVisibility());
}
[Test]
public void TestPreexistingSelection()
{
AddStep("set mods", () => SelectedMods.Value = new Mod[] { new OsuModAlternate(), new OsuModDaycore() });
createScreen();
AddUntilStep("two panels active", () => modSelectOverlay.ChildrenOfType<ModPanel>().Count(panel => panel.Active.Value) == 2);
AddAssert("mod multiplier correct", () =>
{
double multiplier = SelectedMods.Value.Aggregate(1d, (m, mod) => m * mod.ScoreMultiplier);
return Precision.AlmostEquals(multiplier, this.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value);
});
assertCustomisationToggleState(disabled: false, active: false);
AddAssert("setting items created", () => modSelectOverlay.ChildrenOfType<ISettingsItem>().Any());
}
[Test]
public void TestExternalSelection()
{
createScreen();
AddStep("set mods", () => SelectedMods.Value = new Mod[] { new OsuModAlternate(), new OsuModDaycore() });
AddUntilStep("two panels active", () => modSelectOverlay.ChildrenOfType<ModPanel>().Count(panel => panel.Active.Value) == 2);
AddAssert("mod multiplier correct", () =>
{
double multiplier = SelectedMods.Value.Aggregate(1d, (m, mod) => m * mod.ScoreMultiplier);
return Precision.AlmostEquals(multiplier, this.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value);
});
assertCustomisationToggleState(disabled: false, active: false);
AddAssert("setting items created", () => modSelectOverlay.ChildrenOfType<ISettingsItem>().Any());
}
[Test]
public void TestRulesetChange()
{
createScreen();
changeRuleset(0);
changeRuleset(1);
changeRuleset(2);
changeRuleset(3);
}
[Test]
public void TestIncompatibilityToggling()
{
createScreen();
changeRuleset(0);
AddStep("activate DT", () => getPanelForMod(typeof(OsuModDoubleTime)).TriggerClick());
AddAssert("DT active", () => SelectedMods.Value.Single().GetType() == typeof(OsuModDoubleTime));
AddAssert("DT panel active", () => getPanelForMod(typeof(OsuModDoubleTime)).Active.Value);
AddStep("activate NC", () => getPanelForMod(typeof(OsuModNightcore)).TriggerClick());
AddAssert("only NC active", () => SelectedMods.Value.Single().GetType() == typeof(OsuModNightcore));
AddAssert("DT panel not active", () => !getPanelForMod(typeof(OsuModDoubleTime)).Active.Value);
AddAssert("NC panel active", () => getPanelForMod(typeof(OsuModNightcore)).Active.Value);
AddStep("activate HR", () => getPanelForMod(typeof(OsuModHardRock)).TriggerClick());
AddAssert("NC+HR active", () => SelectedMods.Value.Any(mod => mod.GetType() == typeof(OsuModNightcore))
&& SelectedMods.Value.Any(mod => mod.GetType() == typeof(OsuModHardRock)));
AddAssert("NC panel active", () => getPanelForMod(typeof(OsuModNightcore)).Active.Value);
AddAssert("HR panel active", () => getPanelForMod(typeof(OsuModHardRock)).Active.Value);
AddStep("activate MR", () => getPanelForMod(typeof(OsuModMirror)).TriggerClick());
AddAssert("NC+MR active", () => SelectedMods.Value.Any(mod => mod.GetType() == typeof(OsuModNightcore))
&& SelectedMods.Value.Any(mod => mod.GetType() == typeof(OsuModMirror)));
AddAssert("NC panel active", () => getPanelForMod(typeof(OsuModNightcore)).Active.Value);
AddAssert("HR panel not active", () => !getPanelForMod(typeof(OsuModHardRock)).Active.Value);
AddAssert("MR panel active", () => getPanelForMod(typeof(OsuModMirror)).Active.Value);
}
[Test]
public void TestDimmedState()
{
createScreen();
changeRuleset(0);
AddUntilStep("any column dimmed", () => this.ChildrenOfType<ModColumn>().Any(column => !column.Active.Value));
ModSelectColumn lastColumn = null!;
AddAssert("last column dimmed", () => !this.ChildrenOfType<ModColumn>().Last().Active.Value);
AddStep("request scroll to last column", () =>
{
var lastDimContainer = this.ChildrenOfType<ModSelectOverlay.ColumnDimContainer>().Last();
lastColumn = lastDimContainer.Column;
lastDimContainer.RequestScroll?.Invoke(lastDimContainer);
});
AddUntilStep("column undimmed", () => lastColumn.Active.Value);
AddStep("click panel", () =>
{
InputManager.MoveMouseTo(lastColumn.ChildrenOfType<ModPanel>().First());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("panel selected", () => lastColumn.ChildrenOfType<ModPanel>().First().Active.Value);
}
[Test]
public void TestCustomisationToggleState()
{
createScreen();
assertCustomisationToggleState(disabled: true, active: false);
AddStep("select customisable mod", () => SelectedMods.Value = new[] { new OsuModDoubleTime() });
assertCustomisationToggleState(disabled: false, active: false);
AddStep("select mod requiring configuration externally", () => SelectedMods.Value = new[] { new OsuModDifficultyAdjust() });
assertCustomisationToggleState(disabled: false, active: false);
AddStep("reset mods", () => SelectedMods.SetDefault());
AddStep("select difficulty adjust via panel", () => getPanelForMod(typeof(OsuModDifficultyAdjust)).TriggerClick());
assertCustomisationToggleState(disabled: false, active: true);
AddStep("dismiss mod customisation via toggle", () =>
{
InputManager.MoveMouseTo(modSelectOverlay.ChildrenOfType<ModCustomisationHeader>().Single());
InputManager.Click(MouseButton.Left);
});
assertCustomisationToggleState(disabled: false, active: false);
AddStep("reset mods", () => SelectedMods.SetDefault());
AddStep("select difficulty adjust via panel", () => getPanelForMod(typeof(OsuModDifficultyAdjust)).TriggerClick());
assertCustomisationToggleState(disabled: false, active: true);
AddStep("dismiss mod customisation via keyboard", () => InputManager.Key(Key.Escape));
assertCustomisationToggleState(disabled: false, active: false);
AddStep("append another mod not requiring config", () => SelectedMods.Value = SelectedMods.Value.Append(new OsuModFlashlight()).ToArray());
assertCustomisationToggleState(disabled: false, active: false);
AddStep("select mod without configuration", () => SelectedMods.Value = new[] { new OsuModAutoplay() });
assertCustomisationToggleState(disabled: true, active: false);
AddStep("select difficulty adjust via panel", () => getPanelForMod(typeof(OsuModDifficultyAdjust)).TriggerClick());
assertCustomisationToggleState(disabled: false, active: true);
AddStep("select mod without configuration", () => SelectedMods.Value = new[] { new OsuModAutoplay() });
assertCustomisationToggleState(disabled: true, active: false); // config was dismissed without explicit user action.
AddStep("select mod preset with mod requiring configuration", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<ModPresetPanel>().First());
InputManager.Click(MouseButton.Left);
});
assertCustomisationToggleState(disabled: false, active: false);
}
[Test]
public void TestDismissCustomisationViaClickingAway()
{
createScreen();
assertCustomisationToggleState(disabled: true, active: false);
AddStep("select difficulty adjust via panel", () => getPanelForMod(typeof(OsuModDifficultyAdjust)).TriggerClick());
assertCustomisationToggleState(disabled: false, active: true);
AddStep("move mouse to search bar", () => InputManager.MoveMouseTo(modSelectOverlay.ChildrenOfType<ShearedSearchTextBox>().Single()));
AddStep("click", () => InputManager.Click(MouseButton.Left));
assertCustomisationToggleState(disabled: false, active: false);
}
[Test]
public void TestDismissCustomisationWhenHidingOverlay()
{
createScreen();
assertCustomisationToggleState(disabled: true, active: false);
AddStep("select difficulty adjust via panel", () => getPanelForMod(typeof(OsuModDifficultyAdjust)).TriggerClick());
assertCustomisationToggleState(disabled: false, active: true);
AddStep("hide overlay", () => modSelectOverlay.Hide());
AddStep("show overlay again", () => modSelectOverlay.Show());
assertCustomisationToggleState(disabled: false, active: false);
}
/// <summary>
/// Ensure that two mod overlays are not cross polluting via central settings instances.
/// </summary>
[Test]
public void TestSettingsNotCrossPolluting()
{
Bindable<IReadOnlyList<Mod>> selectedMods2 = null!;
ModSelectOverlay modSelectOverlay2 = null!;
createScreen();
AddStep("select difficulty adjust via panel", () => getPanelForMod(typeof(OsuModDifficultyAdjust)).TriggerClick());
AddStep("set setting", () => modSelectOverlay.ChildrenOfType<RoundedSliderBar<float>>().First().Current.Value = 8);
AddAssert("ensure setting is propagated", () => SelectedMods.Value.OfType<OsuModDifficultyAdjust>().Single().CircleSize.Value == 8);
AddStep("create second bindable", () => selectedMods2 = new Bindable<IReadOnlyList<Mod>>(new Mod[] { new OsuModDifficultyAdjust() }));
AddStep("create second overlay", () =>
{
Add(modSelectOverlay2 = new UserModSelectOverlay().With(d =>
{
d.Origin = Anchor.TopCentre;
d.Anchor = Anchor.TopCentre;
d.SelectedMods.BindTarget = selectedMods2;
}));
});
AddStep("show", () => modSelectOverlay2.Show());
AddAssert("ensure first is unchanged", () => SelectedMods.Value.OfType<OsuModDifficultyAdjust>().Single().CircleSize.Value == 8);
AddAssert("ensure second is default", () => selectedMods2.Value.OfType<OsuModDifficultyAdjust>().Single().CircleSize.Value == null);
}
[Test]
public void TestSettingsResetOnDeselection()
{
var osuModDoubleTime = new OsuModDoubleTime { SpeedChange = { Value = 1.2 } };
createScreen();
changeRuleset(0);
AddStep("set dt mod with custom rate", () => { SelectedMods.Value = new[] { osuModDoubleTime }; });
AddAssert("selected mod matches", () => (SelectedMods.Value.Single() as OsuModDoubleTime)?.SpeedChange.Value == 1.2);
AddStep("deselect", () => getPanelForMod(typeof(OsuModDoubleTime)).TriggerClick());
AddAssert("selected mods empty", () => SelectedMods.Value.Count == 0);
AddStep("reselect", () => getPanelForMod(typeof(OsuModDoubleTime)).TriggerClick());
AddAssert("selected mod has default value", () => (SelectedMods.Value.Single() as OsuModDoubleTime)?.SpeedChange.IsDefault == true);
}
[Test]
public void TestAnimationFlushOnClose()
{
createScreen();
changeRuleset(0);
AddStep("Select all difficulty-increase mods", () =>
{
modSelectOverlay.ChildrenOfType<ModColumn>()
.Single(c => c.ModType == ModType.DifficultyIncrease)
.SelectAll();
});
AddUntilStep("many mods selected", () => SelectedMods.Value.Count >= 5);
AddStep("trigger deselect and close overlay", () =>
{
modSelectOverlay.ChildrenOfType<ModColumn>()
.Single(c => c.ModType == ModType.DifficultyIncrease)
.DeselectAll();
modSelectOverlay.Hide();
});
AddAssert("all mods deselected", () => SelectedMods.Value.Count == 0);
}
[Test]
public void TestCommonModsMaintainedOnRulesetChange()
{
createScreen();
changeRuleset(0);
AddStep("select relax mod", () => SelectedMods.Value = new[] { Ruleset.Value.CreateInstance().CreateMod<ModRelax>() });
changeRuleset(0);
AddAssert("ensure mod still selected", () => SelectedMods.Value.SingleOrDefault() is OsuModRelax);
changeRuleset(2);
AddAssert("catch variant selected", () => SelectedMods.Value.SingleOrDefault() is CatchModRelax);
changeRuleset(3);
AddAssert("no mod selected", () => SelectedMods.Value.Count == 0);
}
[Test]
public void TestUncommonModsDiscardedOnRulesetChange()
{
createScreen();
changeRuleset(0);
AddStep("select single tap mod", () => SelectedMods.Value = new[] { new OsuModSingleTap() });
changeRuleset(0);
AddAssert("ensure mod still selected", () => SelectedMods.Value.SingleOrDefault() is OsuModSingleTap);
changeRuleset(3);
AddAssert("no mod selected", () => SelectedMods.Value.Count == 0);
}
[Test]
public void TestKeepSharedSettingsFromSimilarMods()
{
const float setting_change = 1.2f;
createScreen();
changeRuleset(0);
AddStep("select difficulty adjust mod", () => SelectedMods.Value = new[] { Ruleset.Value.CreateInstance().CreateMod<ModDifficultyAdjust>()! });
changeRuleset(0);
AddAssert("ensure mod still selected", () => SelectedMods.Value.SingleOrDefault() is OsuModDifficultyAdjust);
AddStep("change mod settings", () =>
{
var osuMod = getSelectedMod<OsuModDifficultyAdjust>();
osuMod.ExtendedLimits.Value = true;
osuMod.CircleSize.Value = setting_change;
osuMod.DrainRate.Value = setting_change;
osuMod.OverallDifficulty.Value = setting_change;
osuMod.ApproachRate.Value = setting_change;
});
changeRuleset(1);
AddAssert("taiko variant selected", () => SelectedMods.Value.SingleOrDefault() is TaikoModDifficultyAdjust);
AddAssert("shared settings preserved", () =>
{
var taikoMod = getSelectedMod<TaikoModDifficultyAdjust>();
return taikoMod.ExtendedLimits.Value &&
taikoMod.DrainRate.Value == setting_change &&
taikoMod.OverallDifficulty.Value == setting_change;
});
AddAssert("non-shared settings remain default", () =>
{
var taikoMod = getSelectedMod<TaikoModDifficultyAdjust>();
return taikoMod.ScrollSpeed.IsDefault;
});
}
[Test]
public void TestExternallySetCustomizedMod()
{
createScreen();
changeRuleset(0);
AddStep("set customized mod externally", () => SelectedMods.Value = new[] { new OsuModDoubleTime { SpeedChange = { Value = 1.01 } } });
AddAssert("ensure button is selected and customized accordingly", () =>
{
var button = getPanelForMod(SelectedMods.Value.Single().GetType());
return ((OsuModDoubleTime)button.Mod).SpeedChange.Value == 1.01;
});
}
[Test]
public void TestSettingsAreRetainedOnReload()
{
createScreen();
changeRuleset(0);
AddStep("set customized mod externally", () => SelectedMods.Value = new[] { new OsuModDoubleTime { SpeedChange = { Value = 1.01 } } });
AddAssert("setting remains", () => (SelectedMods.Value.SingleOrDefault() as OsuModDoubleTime)?.SpeedChange.Value == 1.01);
createScreen();
AddAssert("setting remains", () => (SelectedMods.Value.SingleOrDefault() as OsuModDoubleTime)?.SpeedChange.Value == 1.01);
}
[Test]
public void TestExternallySetModIsReplacedByOverlayInstance()
{
Mod external = new OsuModDoubleTime();
Mod overlayButtonMod = null!;
createScreen();
changeRuleset(0);
AddStep("set mod externally", () => { SelectedMods.Value = new[] { external }; });
AddAssert("ensure button is selected", () =>
{
var button = getPanelForMod(SelectedMods.Value.Single().GetType());
overlayButtonMod = button.Mod;
return button.Active.Value;
});
// Right now, when an external change occurs, the ModSelectOverlay will replace the global instance with its own
AddAssert("mod instance doesn't match", () => external != overlayButtonMod);
AddAssert("one mod present in global selected", () => SelectedMods.Value.Count == 1);
AddAssert("globally selected matches button's mod instance", () => SelectedMods.Value.Any(mod => ReferenceEquals(mod, overlayButtonMod)));
AddAssert("globally selected doesn't contain original external change", () => !SelectedMods.Value.Any(mod => ReferenceEquals(mod, external)));
}
[Test]
public void TestChangeIsValidChangesButtonVisibility()
{
createScreen();
changeRuleset(0);
AddAssert("double time visible", () => modSelectOverlay.ChildrenOfType<ModPanel>().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => panel.Visible));
AddStep("make double time invalid", () => modSelectOverlay.IsValidMod = m => !(m is OsuModDoubleTime));
AddUntilStep("double time not visible", () => modSelectOverlay.ChildrenOfType<ModPanel>().Where(panel => panel.Mod is OsuModDoubleTime).All(panel => !panel.Visible));
AddAssert("nightcore still visible", () => modSelectOverlay.ChildrenOfType<ModPanel>().Where(panel => panel.Mod is OsuModNightcore).Any(panel => panel.Visible));
AddStep("make double time valid again", () => modSelectOverlay.IsValidMod = _ => true);
AddUntilStep("double time visible", () => modSelectOverlay.ChildrenOfType<ModPanel>().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => panel.Visible));
AddAssert("nightcore still visible", () => modSelectOverlay.ChildrenOfType<ModPanel>().Where(b => b.Mod is OsuModNightcore).Any(panel => panel.Visible));
}
[Test]
public void TestChangeIsValidPreservesSelection()
{
createScreen();
changeRuleset(0);
AddStep("select DT + HD", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHidden() });
AddAssert("DT + HD selected", () => modSelectOverlay.ChildrenOfType<ModPanel>().Count(panel => panel.Active.Value) == 2);
AddStep("make NF invalid", () => modSelectOverlay.IsValidMod = m => !(m is ModNoFail));
AddAssert("DT + HD still selected", () => modSelectOverlay.ChildrenOfType<ModPanel>().Count(panel => panel.Active.Value) == 2);
}
[Test]
public void TestUnimplementedModIsUnselectable()
{
var testRuleset = new TestUnimplementedModOsuRuleset();
createScreen();
AddStep("set ruleset", () => Ruleset.Value = testRuleset.RulesetInfo);
waitForColumnLoad();
AddAssert("unimplemented mod panel is filtered", () => !getPanelForMod(typeof(TestUnimplementedMod)).Visible);
}
[Test]
public void TestFirstModSelectDeselect()
{
createScreen();
AddStep("apply search", () => modSelectOverlay.SearchTerm = "HD");
AddStep("press enter", () => InputManager.Key(Key.Enter));
AddAssert("hidden selected", () => getPanelForMod(typeof(OsuModHidden)).Active.Value);
AddAssert("all text selected in textbox", () =>
{
var textBox = modSelectOverlay.ChildrenOfType<SearchTextBox>().Single();
return textBox.SelectedText == textBox.Text;
});
AddStep("press enter again", () => InputManager.Key(Key.Enter));
AddAssert("hidden deselected", () => !getPanelForMod(typeof(OsuModHidden)).Active.Value);
AddStep("apply search matching nothing", () => modSelectOverlay.SearchTerm = "ZZZ");
AddStep("press enter", () => InputManager.Key(Key.Enter));
AddAssert("all text not selected in textbox", () =>
{
var textBox = modSelectOverlay.ChildrenOfType<SearchTextBox>().Single();
return textBox.SelectedText != textBox.Text;
});
AddStep("clear search", () => modSelectOverlay.SearchTerm = string.Empty);
AddStep("press enter", () => InputManager.Key(Key.Enter));
AddAssert("mod select hidden", () => modSelectOverlay.State.Value == Visibility.Hidden);
}
[Test]
public void TestSearchFocusChangeViaClick()
{
createScreen();
AddStep("click on search", navigateAndClick<ShearedSearchTextBox>);
AddAssert("focused", () => modSelectOverlay.SearchTextBox.HasFocus);
AddStep("click on mod column", navigateAndClick<ModColumn>);
AddAssert("lost focus", () => !modSelectOverlay.SearchTextBox.HasFocus);
void navigateAndClick<T>() where T : Drawable
{
InputManager.MoveMouseTo(modSelectOverlay.ChildrenOfType<T>().First());
InputManager.Click(MouseButton.Left);
}
}
[Test]
public void TestTextSearchActiveByDefault()
{
AddStep("text search starts active", () => configManager.SetValue(OsuSetting.ModSelectTextSearchStartsActive, true));
createScreen();
AddUntilStep("search text box focused", () => modSelectOverlay.SearchTextBox.HasFocus);
AddStep("press tab", () => InputManager.Key(Key.Tab));
AddAssert("search text box unfocused", () => !modSelectOverlay.SearchTextBox.HasFocus);
AddStep("press tab", () => InputManager.Key(Key.Tab));
AddAssert("search text box focused", () => modSelectOverlay.SearchTextBox.HasFocus);
}
[Test]
public void TestTextSearchNotActiveByDefault()
{
AddStep("text search does not start active", () => configManager.SetValue(OsuSetting.ModSelectTextSearchStartsActive, false));
createScreen();
AddUntilStep("search text box not focused", () => !modSelectOverlay.SearchTextBox.HasFocus);
AddStep("press tab", () => InputManager.Key(Key.Tab));
AddAssert("search text box focused", () => modSelectOverlay.SearchTextBox.HasFocus);
AddStep("press tab", () => InputManager.Key(Key.Tab));
AddAssert("search text box unfocused", () => !modSelectOverlay.SearchTextBox.HasFocus);
}
[Test]
public void TestSearchBoxFocusToggleRespondsToExternalChanges()
{
AddStep("text search does not start active", () => configManager.SetValue(OsuSetting.ModSelectTextSearchStartsActive, false));
createScreen();
AddUntilStep("search text box not focused", () => !modSelectOverlay.SearchTextBox.HasFocus);
AddStep("press tab", () => InputManager.Key(Key.Tab));
AddAssert("search text box focused", () => modSelectOverlay.SearchTextBox.HasFocus);
AddStep("unfocus search text box externally", () => ((IFocusManager)InputManager).ChangeFocus(null));
AddStep("press tab", () => InputManager.Key(Key.Tab));
AddAssert("search text box focused", () => modSelectOverlay.SearchTextBox.HasFocus);
}
[Test]
public void TestTextSearchDoesNotBlockCustomisationPanelKeyboardInteractions()
{
AddStep("text search starts active", () => configManager.SetValue(OsuSetting.ModSelectTextSearchStartsActive, true));
createScreen();
AddUntilStep("search text box focused", () => modSelectOverlay.SearchTextBox.HasFocus);
AddStep("select DT", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime() });
AddAssert("DT selected", () => modSelectOverlay.ChildrenOfType<ModPanel>().Count(panel => panel.Active.Value), () => Is.EqualTo(1));
AddStep("open customisation area", () => modSelectOverlay.ChildrenOfType<ModCustomisationHeader>().Single().TriggerClick());
assertCustomisationToggleState(disabled: false, active: true);
AddStep("hover over mod settings slider", () =>
{
var slider = modSelectOverlay.ChildrenOfType<ModCustomisationPanel>().Single().ChildrenOfType<OsuSliderBar<double>>().First();
InputManager.MoveMouseTo(slider);
});
AddStep("press right arrow", () => InputManager.PressKey(Key.Right));
AddAssert("DT speed changed", () => !SelectedMods.Value.OfType<OsuModDoubleTime>().Single().SpeedChange.IsDefault);
AddStep("close customisation area", () => InputManager.PressKey(Key.Escape));
AddUntilStep("search text box reacquired focus", () => modSelectOverlay.SearchTextBox.HasFocus);
}
[Test]
public void TestDeselectAllViaKey()
{
createScreen();
changeRuleset(0);
AddStep("kill search bar focus", () => modSelectOverlay.SearchTextBox.KillFocus());
AddStep("select DT + HD", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHidden() });
AddAssert("DT + HD selected", () => modSelectOverlay.ChildrenOfType<ModPanel>().Count(panel => panel.Active.Value) == 2);
AddStep("press backspace", () => InputManager.Key(Key.BackSpace));
AddUntilStep("all mods deselected", () => !SelectedMods.Value.Any());
}
[Test]
public void TestDeselectAllViaKey_WithSearchApplied()
{
createScreen();
changeRuleset(0);
AddStep("select DT + HD", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHidden() });
AddStep("focus on search", () => modSelectOverlay.SearchTextBox.TakeFocus());
AddStep("apply search", () => modSelectOverlay.SearchTerm = "Easy");
AddAssert("DT + HD selected and hidden", () => modSelectOverlay.ChildrenOfType<ModPanel>().Count(panel => !panel.Visible && panel.Active.Value) == 2);
AddStep("press backspace", () => InputManager.Key(Key.BackSpace));
AddAssert("DT + HD still selected", () => modSelectOverlay.ChildrenOfType<ModPanel>().Count(panel => panel.Active.Value) == 2);
AddAssert("search term changed", () => modSelectOverlay.SearchTerm == "Eas");
AddStep("kill focus", () => modSelectOverlay.SearchTextBox.KillFocus());
AddStep("press backspace", () => InputManager.Key(Key.BackSpace));
AddUntilStep("all mods deselected", () => !SelectedMods.Value.Any());
}
[Test]
public void TestDeselectAllViaButton()
{
createScreen();
changeRuleset(0);
AddAssert("deselect all button disabled", () => !this.ChildrenOfType<DeselectAllModsButton>().Single().Enabled.Value);
AddStep("select DT + HD", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHidden() });
AddAssert("DT + HD selected", () => modSelectOverlay.ChildrenOfType<ModPanel>().Count(panel => panel.Active.Value) == 2);
AddAssert("deselect all button enabled", () => this.ChildrenOfType<DeselectAllModsButton>().Single().Enabled.Value);
AddStep("click deselect all button", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<DeselectAllModsButton>().Single());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("all mods deselected", () => !SelectedMods.Value.Any());
AddAssert("deselect all button disabled", () => !this.ChildrenOfType<DeselectAllModsButton>().Single().Enabled.Value);
}
[Test]
public void TestDeselectAllViaButton_WithSearchApplied()
{
createScreen();
changeRuleset(0);
AddAssert("deselect all button disabled", () => !this.ChildrenOfType<DeselectAllModsButton>().Single().Enabled.Value);
AddStep("select DT + HD + RD", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHidden(), new OsuModRandom() });
AddAssert("DT + HD + RD selected", () => modSelectOverlay.ChildrenOfType<ModPanel>().Count(panel => panel.Active.Value) == 3);
AddAssert("deselect all button enabled", () => this.ChildrenOfType<DeselectAllModsButton>().Single().Enabled.Value);
AddStep("apply search", () => modSelectOverlay.SearchTerm = "Easy");
AddAssert("DT + HD + RD are hidden and selected", () => modSelectOverlay.ChildrenOfType<ModPanel>().Count(panel => !panel.Visible && panel.Active.Value) == 3);
AddAssert("deselect all button enabled", () => this.ChildrenOfType<DeselectAllModsButton>().Single().Enabled.Value);
AddStep("click deselect all button", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<DeselectAllModsButton>().Single());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("all mods deselected", () => !SelectedMods.Value.Any());
AddAssert("deselect all button disabled", () => !this.ChildrenOfType<DeselectAllModsButton>().Single().Enabled.Value);
}
[Test]
public void TestCloseViaBackButton()
{
createScreen();
changeRuleset(0);
AddStep("select difficulty adjust via panel", () => getPanelForMod(typeof(OsuModDifficultyAdjust)).TriggerClick());
assertCustomisationToggleState(disabled: false, active: true);
AddStep("dismiss customisation area", () => InputManager.Key(Key.Escape));
AddAssert("mod select still visible", () => modSelectOverlay.State.Value == Visibility.Visible);
AddStep("click back button", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<ScreenBackButton>().Single());
InputManager.Click(MouseButton.Left);
});
AddAssert("mod select hidden", () => modSelectOverlay.State.Value == Visibility.Hidden);
}
[Test]
public void TestCloseViaToggleModSelectionBinding()
{
createScreen();
changeRuleset(0);
AddStep("select difficulty adjust via panel", () => getPanelForMod(typeof(OsuModDifficultyAdjust)).TriggerClick());
assertCustomisationToggleState(disabled: false, active: true);
AddStep("press F1", () => InputManager.Key(Key.F1));
AddAssert("mod select hidden", () => modSelectOverlay.State.Value == Visibility.Hidden);
}
/// <summary>
/// Covers columns hiding/unhiding on changes of <see cref="ModSelectOverlay.IsValidMod"/>.
/// </summary>
[Test]
public void TestColumnHidingOnIsValidChange()
{
AddStep("create screen", () => Child = modSelectOverlay = new TestModSelectOverlay
{
RelativeSizeAxes = Axes.Both,
State = { Value = Visibility.Visible },
SelectedMods = { BindTarget = SelectedMods },
IsValidMod = mod => mod.Type == ModType.DifficultyIncrease || mod.Type == ModType.Conversion
});
waitForColumnLoad();
changeRuleset(0);
AddAssert("two columns visible", () => this.ChildrenOfType<ModColumn>().Count(col => col.IsPresent) == 2);
AddStep("unset filter", () => modSelectOverlay.IsValidMod = _ => true);
AddAssert("all columns visible", () => this.ChildrenOfType<ModColumn>().All(col => col.IsPresent));
AddStep("filter out everything", () => modSelectOverlay.IsValidMod = _ => false);
AddAssert("no columns visible", () => this.ChildrenOfType<ModColumn>().All(col => !col.IsPresent));
AddStep("hide", () => modSelectOverlay.Hide());
AddStep("set filter for 3 columns", () => modSelectOverlay.IsValidMod = mod => mod.Type == ModType.DifficultyReduction
|| mod.Type == ModType.Automation
|| mod.Type == ModType.Conversion);
AddStep("show", () => modSelectOverlay.Show());
AddUntilStep("3 columns visible", () => this.ChildrenOfType<ModColumn>().Count(col => col.IsPresent) == 3);
}
/// <summary>
/// Covers columns hiding/unhiding on changes of <see cref="ModSelectOverlay.SearchTerm"/>.
/// </summary>
[Test]
public void TestColumnHidingOnTextFilterChange()
{
AddStep("create screen", () => Child = modSelectOverlay = new TestModSelectOverlay
{
RelativeSizeAxes = Axes.Both,
State = { Value = Visibility.Visible },
SelectedMods = { BindTarget = SelectedMods }
});
waitForColumnLoad();
changeRuleset(0);
AddAssert("all columns visible", () => this.ChildrenOfType<ModColumn>().All(col => col.IsPresent));
AddStep("set search", () => modSelectOverlay.SearchTerm = "HD");
AddAssert("two columns visible", () => this.ChildrenOfType<ModColumn>().Count(col => col.IsPresent) == 2);
AddStep("filter out everything", () => modSelectOverlay.SearchTerm = "Some long search term with no matches");
AddAssert("no columns visible", () => this.ChildrenOfType<ModColumn>().All(col => !col.IsPresent));
AddStep("clear search bar", () => modSelectOverlay.SearchTerm = "");
AddAssert("all columns visible", () => this.ChildrenOfType<ModColumn>().All(col => col.IsPresent));
}
[Test]
public void TestHidingOverlayClearsTextSearch()
{
AddStep("create screen", () => Child = modSelectOverlay = new TestModSelectOverlay
{
RelativeSizeAxes = Axes.Both,
State = { Value = Visibility.Visible },
SelectedMods = { BindTarget = SelectedMods }
});
waitForColumnLoad();
changeRuleset(0);
AddAssert("all columns visible", () => this.ChildrenOfType<ModColumn>().All(col => col.IsPresent));
AddStep("set search", () => modSelectOverlay.SearchTerm = "fail");
AddAssert("one column visible", () => this.ChildrenOfType<ModColumn>().Count(col => col.IsPresent) == 1);
AddStep("hide", () => modSelectOverlay.Hide());
AddStep("show", () => modSelectOverlay.Show());
AddAssert("all columns visible", () => this.ChildrenOfType<ModColumn>().All(col => col.IsPresent));
}
[Test]
public void TestColumnHidingOnRulesetChange()
{
createScreen();
changeRuleset(0);
AddAssert("5 columns visible", () => this.ChildrenOfType<ModColumn>().Count(col => col.IsPresent) == 5);
AddStep("change to ruleset without all mod types", () => Ruleset.Value = TestCustomisableModRuleset.CreateTestRulesetInfo());
AddUntilStep("1 column visible", () => this.ChildrenOfType<ModColumn>().Count(col => col.IsPresent) == 1);
changeRuleset(0);
AddAssert("5 columns visible", () => this.ChildrenOfType<ModColumn>().Count(col => col.IsPresent) == 5);
}
[Test]
public void TestModMultiplierUpdates()
{
createScreen();
AddStep("select mod preset with half time", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<ModPresetPanel>().Single(preset => preset.Preset.Value.Name == "Half Time 0.5x"));
InputManager.Click(MouseButton.Left);
});
AddAssert("difficulty multiplier display shows correct value",
() => this.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value, () => Is.EqualTo(0.1).Within(Precision.DOUBLE_EPSILON));
// this is highly unorthodox in a test, but because the `ModSettingChangeTracker` machinery heavily leans on events and object disposal and re-creation,
// it is instrumental in the reproduction of the failure scenario that this test is supposed to cover.
AddStep("force collection", GC.Collect);
AddStep("open customisation area", () => modSelectOverlay.ChildrenOfType<ModCustomisationHeader>().Single().TriggerClick());
AddStep("reset half time speed to default", () => modSelectOverlay.ChildrenOfType<ModCustomisationPanel>().Single()
.ChildrenOfType<RevertToDefaultButton<double>>().Single().TriggerClick());
AddUntilStep("difficulty multiplier display shows correct value",
() => this.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value, () => Is.EqualTo(0.3).Within(Precision.DOUBLE_EPSILON));
}
[Test]
public void TestModSettingsOrder()
{
createScreen();
AddStep("select DT + HD + DF", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHidden(), new OsuModDeflate() });
AddStep("open customisation panel", () => this.ChildrenOfType<ModCustomisationHeader>().Single().TriggerClick());
AddAssert("mod settings order: DT, HD, DF", () =>
{
var columns = this.ChildrenOfType<ModCustomisationSection>();
return columns.ElementAt(0).Mod is OsuModDoubleTime &&
columns.ElementAt(1).Mod is OsuModHidden &&
columns.ElementAt(2).Mod is OsuModDeflate;
});
AddStep("replace DT with NC", () =>
{
SelectedMods.Value = SelectedMods.Value.Where(m => m is not ModDoubleTime).Append(new OsuModNightcore()).ToList();
this.ChildrenOfType<ModCustomisationHeader>().Single().TriggerClick();
});
AddAssert("mod settings order: NC, HD, DF", () =>
{
var columns = this.ChildrenOfType<ModCustomisationSection>();
return columns.ElementAt(0).Mod is OsuModNightcore &&
columns.ElementAt(1).Mod is OsuModHidden &&
columns.ElementAt(2).Mod is OsuModDeflate;
});
}
[Test]
public void TestOpeningCustomisationHidesPresetPopover()
{
createScreen();
AddStep("select DT", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime() });
AddStep("click new preset", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<AddPresetButton>().Single());
InputManager.Click(MouseButton.Left);
});
AddAssert("preset popover shown", () => this.ChildrenOfType<AddPresetPopover>().SingleOrDefault()?.IsPresent, () => Is.True);
AddStep("click customisation header", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<ModCustomisationHeader>().Single());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("preset popover hidden", () => this.ChildrenOfType<AddPresetPopover>().SingleOrDefault()?.IsPresent, () => Is.Not.True);
AddAssert("customisation panel shown", () => this.ChildrenOfType<ModCustomisationPanel>().Single().State.Value, () => Is.EqualTo(Visibility.Visible));
}
[Test]
public void TestCustomisationPanelAbsorbsInput([Values] bool textSearchStartsActive)
{
AddStep($"text search starts active = {textSearchStartsActive}", () => configManager.SetValue(OsuSetting.ModSelectTextSearchStartsActive, textSearchStartsActive));
createScreen();
AddStep("select DT", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime() });
AddStep("open customisation panel", () => this.ChildrenOfType<ModCustomisationHeader>().Single().TriggerClick());
AddAssert("search lost focus", () => !this.ChildrenOfType<ShearedSearchTextBox>().Single().HasFocus);
AddStep("press tab", () => InputManager.Key(Key.Tab));
AddAssert("search still not focused", () => !this.ChildrenOfType<ShearedSearchTextBox>().Single().HasFocus);
AddStep("press q", () => InputManager.Key(Key.Q));
AddAssert("easy not selected", () => SelectedMods.Value.Single() is OsuModDoubleTime);
// the "deselect all mods" action is intentionally disabled when customisation panel is open to not conflict with pressing backspace to delete characters in a textbox.
// this is supposed to be handled by the textbox itself especially since it's focused and thus prioritised in input queue,
// but it's not for some reason, and figuring out why is probably not going to be a pleasant experience (read TextBox.OnKeyDown for a head start).
AddStep("press backspace", () => InputManager.Key(Key.BackSpace));
AddAssert("mods not deselected", () => SelectedMods.Value.Single() is OsuModDoubleTime);
AddStep("move mouse to scroll bar", () => InputManager.MoveMouseTo(modSelectOverlay.ChildrenOfType<ModSelectOverlay.ColumnScrollContainer>().Single().ScreenSpaceDrawQuad.BottomLeft + new Vector2(10f, -5f)));
AddStep("scroll down", () => InputManager.ScrollVerticalBy(-10f));
AddAssert("column not scrolled", () => modSelectOverlay.ChildrenOfType<ModSelectOverlay.ColumnScrollContainer>().Single().IsScrolledToStart());
AddStep("press mouse", () => InputManager.PressButton(MouseButton.Left));
AddAssert("search still not focused", () => !this.ChildrenOfType<ShearedSearchTextBox>().Single().HasFocus);
AddStep("release mouse", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("customisation panel closed by click",
() => this.ChildrenOfType<ModCustomisationPanel>().Single().ExpandedState.Value,
() => Is.EqualTo(ModCustomisationPanel.ModCustomisationPanelState.Collapsed));
if (textSearchStartsActive)
AddAssert("search focused", () => this.ChildrenOfType<ShearedSearchTextBox>().Single().HasFocus);
else
AddAssert("search still not focused", () => !this.ChildrenOfType<ShearedSearchTextBox>().Single().HasFocus);
}
private void waitForColumnLoad() => AddUntilStep("all column content loaded", () =>
modSelectOverlay.ChildrenOfType<ModColumn>().Any()
&& modSelectOverlay.ChildrenOfType<ModColumn>().All(column => column.IsLoaded && column.ItemsLoaded)
&& modSelectOverlay.ChildrenOfType<ModPresetColumn>().Any()
&& modSelectOverlay.ChildrenOfType<ModPresetColumn>().All(column => column.IsLoaded));
private void changeRuleset(int id)
{
AddStep($"set ruleset to {id}", () => Ruleset.Value = rulesetStore.GetRuleset(id));
waitForColumnLoad();
}
private void assertCustomisationToggleState(bool disabled, bool active)
{
AddUntilStep($"customisation panel is {(disabled ? "" : "not ")}disabled", () => modSelectOverlay.ChildrenOfType<ModCustomisationPanel>().Single().Enabled.Value == !disabled);
AddAssert($"customisation panel is {(active ? "" : "not ")}active",
() => modSelectOverlay.ChildrenOfType<ModCustomisationPanel>().Single().ExpandedState.Value,
() => active ? Is.Not.EqualTo(ModCustomisationPanel.ModCustomisationPanelState.Collapsed) : Is.EqualTo(ModCustomisationPanel.ModCustomisationPanelState.Collapsed));
}
private T getSelectedMod<T>() where T : Mod => SelectedMods.Value.OfType<T>().Single();
private ModPanel getPanelForMod(Type modType)
=> modSelectOverlay.ChildrenOfType<ModPanel>().Single(panel => panel.Mod.GetType() == modType);
private partial class TestModSelectOverlay : UserModSelectOverlay
{
protected override bool ShowPresets => true;
}
private class TestUnimplementedMod : Mod
{
public override string Name => "Unimplemented mod";
public override string Acronym => "UM";
public override LocalisableString Description => "A mod that is not implemented.";
public override double ScoreMultiplier => 1;
public override ModType Type => ModType.Conversion;
}
private class TestUnimplementedModOsuRuleset : OsuRuleset
{
public override string ShortName => "unimplemented";
public override IEnumerable<Mod> GetModsFor(ModType type) =>
type == ModType.Conversion
? base.GetModsFor(type).Concat(new[] { new TestUnimplementedMod() })
: base.GetModsFor(type);
}
}
}