// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. #nullable disable using System; using System.IO; using System.Linq; using Newtonsoft.Json.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Configuration; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Collections; using osu.Game.Configuration; using osu.Game.Extensions; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.Leaderboards; using osu.Game.Online.Notifications.WebSocket; using osu.Game.Online.Notifications.WebSocket.Events; using osu.Game.Overlays; using osu.Game.Overlays.BeatmapListing; using osu.Game.Overlays.Mods; using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Toolbar; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Scoring; using osu.Game.Screens.Menu; using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Ranking; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Carousel; using osu.Game.Screens.Select.Leaderboards; using osu.Game.Screens.Select.Options; using osu.Game.Tests.Beatmaps.IO; using osuTK; using osuTK.Input; using SharpCompress; namespace osu.Game.Tests.Visual.Navigation { public partial class TestSceneScreenNavigation : OsuGameTestScene { private const float click_padding = 25; private Vector2 backButtonPosition => Game.ToScreenSpace(new Vector2(click_padding, Game.LayoutRectangle.Bottom - click_padding)); private Vector2 optionsButtonPosition => Game.ToScreenSpace(new Vector2(click_padding, click_padding)); [TestCase(false)] [TestCase(true)] public void TestConfirmationRequiredToDiscardPlaylist(bool withPlaylistItemAdded) { Screens.OnlinePlay.Playlists.Playlists playlistScreen = null; AddUntilStep("wait for dialog overlay", () => Game.ChildrenOfType().SingleOrDefault() != null); PushAndConfirm(() => playlistScreen = new Screens.OnlinePlay.Playlists.Playlists()); AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); AddStep("open create screen", () => { InputManager.MoveMouseTo(playlistScreen.ChildrenOfType().Single()); InputManager.Click(MouseButton.Left); }); if (withPlaylistItemAdded) { AddUntilStep("wait for settings displayed", () => (playlistScreen.CurrentSubScreen as PlaylistsRoomSubScreen)?.ChildrenOfType().SingleOrDefault()?.State.Value == Visibility.Visible); AddStep("edit playlist", () => InputManager.Key(Key.Enter)); AddUntilStep("wait for song select", () => (playlistScreen.CurrentSubScreen as PlaylistsSongSelect)?.BeatmapSetsLoaded == true); AddUntilStep("wait for selection", () => !Game.Beatmap.IsDefault); AddStep("add item", () => InputManager.Key(Key.Enter)); AddUntilStep("wait for return to playlist screen", () => playlistScreen.CurrentSubScreen is PlaylistsRoomSubScreen); AddStep("go back to song select", () => { InputManager.MoveMouseTo(playlistScreen.ChildrenOfType().Single(b => b.Text == "Edit playlist")); InputManager.Click(MouseButton.Left); }); AddUntilStep("wait for song select", () => (playlistScreen.CurrentSubScreen as PlaylistsSongSelect)?.BeatmapSetsLoaded == true); AddStep("press home button", () => { InputManager.MoveMouseTo(Game.Toolbar.ChildrenOfType().Single()); InputManager.Click(MouseButton.Left); }); AddAssert("confirmation dialog shown", () => Game.ChildrenOfType().Single().CurrentDialog is not null); pushEscape(); pushEscape(); AddAssert("confirmation dialog shown", () => Game.ChildrenOfType().Single().CurrentDialog is not null); AddStep("confirm exit", () => InputManager.Key(Key.Enter)); AddAssert("dialog dismissed", () => Game.ChildrenOfType().Single().CurrentDialog == null); exitViaEscapeAndConfirm(); } else { pushEscape(); AddAssert("confirmation dialog not shown", () => Game.ChildrenOfType().Single().CurrentDialog == null); exitViaEscapeAndConfirm(); } } [Test] public void TestExitSongSelectWithEscape() { TestPlaySongSelect songSelect = null; PushAndConfirm(() => songSelect = new TestPlaySongSelect()); AddStep("Show mods overlay", () => songSelect.ModSelectOverlay.Show()); AddAssert("Overlay was shown", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible); pushEscape(); AddAssert("Overlay was hidden", () => songSelect.ModSelectOverlay.State.Value == Visibility.Hidden); exitViaEscapeAndConfirm(); } [Test] public void TestSongSelectBackActionHandling() { TestPlaySongSelect songSelect = null; PushAndConfirm(() => songSelect = new TestPlaySongSelect()); AddStep("set filter", () => filterControlTextBox().Current.Value = "test"); AddStep("press back", () => InputManager.Click(MouseButton.Button1)); AddAssert("still at song select", () => Game.ScreenStack.CurrentScreen == songSelect); AddAssert("filter cleared", () => string.IsNullOrEmpty(filterControlTextBox().Current.Value)); AddStep("set filter again", () => filterControlTextBox().Current.Value = "test"); AddStep("open collections dropdown", () => { InputManager.MoveMouseTo(songSelect.ChildrenOfType().Single()); InputManager.Click(MouseButton.Left); }); AddStep("press back once", () => InputManager.Click(MouseButton.Button1)); AddAssert("still at song select", () => Game.ScreenStack.CurrentScreen == songSelect); AddAssert("collections dropdown closed", () => songSelect .ChildrenOfType().Single() .ChildrenOfType.DropdownMenu>().Single().State == MenuState.Closed); AddStep("press back a second time", () => InputManager.Click(MouseButton.Button1)); AddAssert("filter cleared", () => string.IsNullOrEmpty(filterControlTextBox().Current.Value)); AddStep("press back a third time", () => InputManager.Click(MouseButton.Button1)); ConfirmAtMainMenu(); TextBox filterControlTextBox() => songSelect.ChildrenOfType().Single(); } [Test] public void TestSongSelectScrollHandling() { TestPlaySongSelect songSelect = null; double scrollPosition = 0; AddStep("set game volume to max", () => Game.Dependencies.Get().SetValue(FrameworkSetting.VolumeUniversal, 1d)); AddUntilStep("wait for volume overlay to hide", () => Game.ChildrenOfType().SingleOrDefault()?.State.Value, () => Is.EqualTo(Visibility.Hidden)); PushAndConfirm(() => songSelect = new TestPlaySongSelect()); AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); AddStep("store scroll position", () => scrollPosition = getCarouselScrollPosition()); AddStep("move to left side", () => InputManager.MoveMouseTo( songSelect.ChildrenOfType().Single().ScreenSpaceDrawQuad.TopLeft + new Vector2(1))); AddStep("scroll down", () => InputManager.ScrollVerticalBy(-1)); AddAssert("carousel didn't move", getCarouselScrollPosition, () => Is.EqualTo(scrollPosition)); AddRepeatStep("alt-scroll down", () => { InputManager.PressKey(Key.AltLeft); InputManager.ScrollVerticalBy(-1); InputManager.ReleaseKey(Key.AltLeft); }, 5); AddAssert("game volume decreased", () => Game.Dependencies.Get().Get(FrameworkSetting.VolumeUniversal), () => Is.LessThan(1)); AddStep("move to carousel", () => InputManager.MoveMouseTo(songSelect.ChildrenOfType().Single())); AddStep("scroll down", () => InputManager.ScrollVerticalBy(-1)); AddAssert("carousel moved", getCarouselScrollPosition, () => Is.Not.EqualTo(scrollPosition)); double getCarouselScrollPosition() => Game.ChildrenOfType>().Single().Current; } /// /// This tests that the F1 key will open the mod select overlay, and not be handled / blocked by the music controller (which has the same default binding /// but should be handled *after* song select). /// [Test] public void TestOpenModSelectOverlayUsingAction() { TestPlaySongSelect songSelect = null; PushAndConfirm(() => songSelect = new TestPlaySongSelect()); AddStep("Show mods overlay", () => InputManager.Key(Key.F1)); AddAssert("Overlay was shown", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible); } [Test] public void TestAttemptPlayBeatmapWrongHashFails() { Screens.Select.SongSelect songSelect = null; AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).GetResultSafely()); PushAndConfirm(() => songSelect = new TestPlaySongSelect()); AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); AddStep("change beatmap files", () => { foreach (var file in Game.Beatmap.Value.BeatmapSetInfo.Files.Where(f => Path.GetExtension(f.Filename) == ".osu")) { using (var stream = Game.Storage.GetStream(Path.Combine("files", file.File.GetStoragePath()), FileAccess.ReadWrite)) stream.WriteByte(0); } }); AddStep("invalidate cache", () => { ((IWorkingBeatmapCache)Game.BeatmapManager).Invalidate(Game.Beatmap.Value.BeatmapSetInfo); }); AddStep("select next difficulty", () => InputManager.Key(Key.Down)); AddStep("press enter", () => InputManager.Key(Key.Enter)); AddUntilStep("wait for player loader", () => Game.ScreenStack.CurrentScreen is PlayerLoader); AddUntilStep("wait for song select", () => songSelect.IsCurrentScreen()); } [Test] public void TestAttemptPlayBeatmapMissingFails() { Screens.Select.SongSelect songSelect = null; AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).GetResultSafely()); PushAndConfirm(() => songSelect = new TestPlaySongSelect()); AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); AddStep("delete beatmap files", () => { foreach (var file in Game.Beatmap.Value.BeatmapSetInfo.Files.Where(f => Path.GetExtension(f.Filename) == ".osu")) Game.Storage.Delete(Path.Combine("files", file.File.GetStoragePath())); }); AddStep("invalidate cache", () => { ((IWorkingBeatmapCache)Game.BeatmapManager).Invalidate(Game.Beatmap.Value.BeatmapSetInfo); }); AddStep("select next difficulty", () => InputManager.Key(Key.Down)); AddStep("press enter", () => InputManager.Key(Key.Enter)); AddUntilStep("wait for player loader", () => Game.ScreenStack.CurrentScreen is PlayerLoader); AddUntilStep("wait for song select", () => songSelect.IsCurrentScreen()); } [Test] public void TestRetryCountIncrements() { Player player = null; Screens.Select.SongSelect songSelect = null; PushAndConfirm(() => songSelect = new TestPlaySongSelect()); AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); AddStep("press enter", () => InputManager.Key(Key.Enter)); AddUntilStep("wait for player", () => { DismissAnyNotifications(); player = Game.ScreenStack.CurrentScreen as Player; return player?.IsLoaded == true; }); AddAssert("retry count is 0", () => player.RestartCount == 0); // todo: see https://github.com/ppy/osu/issues/22220 // tests are supposed to be immune to this edge case by the logic in TestPlayer, // but we're running a full game instance here, so we have to work around it manually. AddStep("end spectator before retry", () => Game.SpectatorClient.EndPlaying(player.GameplayState)); AddStep("attempt to retry", () => player.ChildrenOfType().First().Action()); AddAssert("old player score marked failed", () => player.Score.ScoreInfo.Rank, () => Is.EqualTo(ScoreRank.F)); AddUntilStep("wait for old player gone", () => Game.ScreenStack.CurrentScreen != player); AddUntilStep("get new player", () => (player = Game.ScreenStack.CurrentScreen as Player) != null); AddAssert("retry count is 1", () => player.RestartCount == 1); } [Test] public void TestRetryImmediatelyAfterCompletion() { var getOriginalPlayer = playToCompletion(); AddStep("attempt to retry", () => getOriginalPlayer().ChildrenOfType().First().Action()); AddAssert("original play isn't failed", () => getOriginalPlayer().Score.ScoreInfo.Rank, () => Is.Not.EqualTo(ScoreRank.F)); AddUntilStep("wait for player", () => Game.ScreenStack.CurrentScreen != getOriginalPlayer() && Game.ScreenStack.CurrentScreen is Player); } [Test] public void TestExitImmediatelyAfterCompletion() { var player = playToCompletion(); AddStep("attempt to exit", () => player().ChildrenOfType().First().Action()); AddUntilStep("wait for results", () => Game.ScreenStack.CurrentScreen is ResultsScreen); } [Test] public void TestShowMedalAtResults() { playToResults(); AddStep("award medal", () => ((DummyAPIAccess)API).NotificationsClient.Receive(new SocketMessage { Event = @"new", Data = JObject.FromObject(new NewPrivateNotificationEvent { Name = @"user_achievement_unlock", Details = JObject.FromObject(new UserAchievementUnlock { Title = "Time And A Half", Description = "Having a right ol' time. One and a half of them, almost.", Slug = @"all-intro-doubletime" }) }) })); AddUntilStep("medal overlay shown", () => Game.ChildrenOfType().Single().State.Value, () => Is.EqualTo(Visibility.Visible)); } [Test] public void TestRetryFromResults() { var getOriginalPlayer = playToResults(); AddStep("attempt to retry", () => ((ResultsScreen)Game.ScreenStack.CurrentScreen).ChildrenOfType().First().Action()); AddUntilStep("wait for player", () => Game.ScreenStack.CurrentScreen != getOriginalPlayer() && Game.ScreenStack.CurrentScreen is Player); } [Test] public void TestDeleteAllScoresAfterPlaying() { playToResults(); ScoreInfo score = null; LeaderboardScore scorePanel = null; AddStep("get score", () => score = ((ResultsScreen)Game.ScreenStack.CurrentScreen).Score); AddAssert("ensure score is databased", () => Game.Realm.Run(r => r.Find(score.ID)?.DeletePending == false)); AddStep("press back button", () => Game.ChildrenOfType().First().Action()); AddStep("show local scores", () => Game.ChildrenOfType().First().Current.Value = new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Local)); AddUntilStep("wait for score displayed", () => (scorePanel = Game.ChildrenOfType().FirstOrDefault(s => s.Score.Equals(score))) != null); AddStep("open options", () => InputManager.Key(Key.F3)); AddStep("choose clear all scores", () => InputManager.Key(Key.Number4)); AddUntilStep("wait for dialog display", () => ((Drawable)Game.Dependencies.Get()).IsLoaded); AddUntilStep("wait for dialog", () => Game.Dependencies.Get().CurrentDialog != null); AddStep("confirm deletion", () => InputManager.Key(Key.Number1)); AddUntilStep("wait for dialog dismissed", () => Game.Dependencies.Get().CurrentDialog == null); AddUntilStep("ensure score is pending deletion", () => Game.Realm.Run(r => r.Find(score.ID)?.DeletePending == true)); AddUntilStep("wait for score panel removal", () => scorePanel.Parent == null); } [Test] public void TestDeleteScoreAfterPlaying() { playToResults(); ScoreInfo score = null; LeaderboardScore scorePanel = null; AddStep("get score", () => score = ((ResultsScreen)Game.ScreenStack.CurrentScreen).Score); AddAssert("ensure score is databased", () => Game.Realm.Run(r => r.Find(score.ID)?.DeletePending == false)); AddStep("press back button", () => Game.ChildrenOfType().First().Action()); AddStep("show local scores", () => Game.ChildrenOfType().First().Current.Value = new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Local)); AddUntilStep("wait for score displayed", () => (scorePanel = Game.ChildrenOfType().FirstOrDefault(s => s.Score.Equals(score))) != null); AddStep("right click panel", () => { InputManager.MoveMouseTo(scorePanel); InputManager.Click(MouseButton.Right); }); AddStep("click delete", () => { var dropdownItem = Game .ChildrenOfType().First() .ChildrenOfType().First() .ChildrenOfType().First(i => i.Item.Text.ToString() == "Delete"); InputManager.MoveMouseTo(dropdownItem); InputManager.Click(MouseButton.Left); }); AddUntilStep("wait for dialog display", () => ((Drawable)Game.Dependencies.Get()).IsLoaded); AddUntilStep("wait for dialog", () => Game.Dependencies.Get().CurrentDialog != null); AddStep("confirm deletion", () => InputManager.Key(Key.Number1)); AddUntilStep("wait for dialog dismissed", () => Game.Dependencies.Get().CurrentDialog == null); AddUntilStep("ensure score is pending deletion", () => Game.Realm.Run(r => r.Find(score.ID)?.DeletePending == true)); AddUntilStep("wait for score panel removal", () => scorePanel.Parent == null); } [TestCase(true)] [TestCase(false)] public void TestSongContinuesAfterExitPlayer(bool withUserPause) { Player player = null; IWorkingBeatmap beatmap() => Game.Beatmap.Value; Screens.Select.SongSelect songSelect = null; PushAndConfirm(() => songSelect = new TestPlaySongSelect()); AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); AddStep("import beatmap", () => BeatmapImportHelper.LoadOszIntoOsu(Game, virtualTrack: true).WaitSafely()); AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); if (withUserPause) AddStep("pause", () => Game.Dependencies.Get().Stop(true)); AddStep("press enter", () => InputManager.Key(Key.Enter)); AddUntilStep("wait for player", () => { DismissAnyNotifications(); return (player = Game.ScreenStack.CurrentScreen as Player) != null; }); AddUntilStep("wait for fail", () => player.GameplayState.HasFailed); AddUntilStep("wait for track stop", () => !Game.MusicController.IsPlaying); AddAssert("Ensure time before preview point", () => Game.MusicController.CurrentTrack.CurrentTime < beatmap().BeatmapInfo.Metadata.PreviewTime); pushEscape(); AddUntilStep("wait for track playing", () => Game.MusicController.IsPlaying); AddAssert("Ensure time wasn't reset to preview point", () => Game.MusicController.CurrentTrack.CurrentTime < beatmap().BeatmapInfo.Metadata.PreviewTime); } [Test] public void TestMenuMakesMusic() { TestPlaySongSelect songSelect = null; PushAndConfirm(() => songSelect = new TestPlaySongSelect()); AddUntilStep("wait for no track", () => Game.MusicController.CurrentTrack.IsDummyDevice); AddStep("return to menu", () => songSelect.Exit()); AddUntilStep("wait for track", () => !Game.MusicController.CurrentTrack.IsDummyDevice && Game.MusicController.IsPlaying); } [Test] public void TestPushSongSelectAndPressBackButtonImmediately() { AddStep("push song select", () => Game.ScreenStack.Push(new TestPlaySongSelect())); AddStep("press back button", () => Game.ChildrenOfType().First().Action()); AddWaitStep("wait two frames", 2); } [Test] public void TestExitSongSelectWithClick() { TestPlaySongSelect songSelect = null; PushAndConfirm(() => songSelect = new TestPlaySongSelect()); AddStep("Show mods overlay", () => songSelect.ModSelectOverlay.Show()); AddAssert("Overlay was shown", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible); AddStep("Move mouse to dimmed area", () => InputManager.MoveMouseTo(new Vector2( songSelect.ScreenSpaceDrawQuad.TopLeft.X + 1, songSelect.ScreenSpaceDrawQuad.TopLeft.Y + songSelect.ScreenSpaceDrawQuad.Height / 2))); AddStep("Click left mouse button", () => InputManager.Click(MouseButton.Left)); AddUntilStep("Overlay was hidden", () => songSelect.ModSelectOverlay.State.Value == Visibility.Hidden); exitViaBackButtonAndConfirm(); } [Test] public void TestModsResetOnEnteringMultiplayer() { var osuAutomationMod = new OsuModAutoplay(); AddStep("Enable autoplay", () => { Game.SelectedMods.Value = new[] { osuAutomationMod }; }); PushAndConfirm(() => new Screens.OnlinePlay.Multiplayer.Multiplayer()); AddUntilStep("Mods are removed", () => Game.SelectedMods.Value.Count == 0); AddStep("Return to menu", () => Game.ScreenStack.CurrentScreen.Exit()); AddUntilStep("Mods are restored", () => Game.SelectedMods.Value.Contains(osuAutomationMod)); } [Test] public void TestExitMultiWithEscape() { PushAndConfirm(() => new Screens.OnlinePlay.Playlists.Playlists()); exitViaEscapeAndConfirm(); } [Test] public void TestExitMultiWithBackButton() { PushAndConfirm(() => new Screens.OnlinePlay.Playlists.Playlists()); exitViaBackButtonAndConfirm(); } [Test] public void TestOpenOptionsAndExitWithEscape() { AddUntilStep("Wait for options to load", () => Game.Settings.IsLoaded); AddStep("Enter menu", () => InputManager.Key(Key.Enter)); AddStep("Move mouse to options overlay", () => InputManager.MoveMouseTo(optionsButtonPosition)); AddStep("Click options overlay", () => InputManager.Click(MouseButton.Left)); AddAssert("Options overlay was opened", () => Game.Settings.State.Value == Visibility.Visible); AddStep("Hide options overlay using escape", () => InputManager.Key(Key.Escape)); AddAssert("Options overlay was closed", () => Game.Settings.State.Value == Visibility.Hidden); } [Test] public void TestWaitForNextTrackInMenu() { bool trackCompleted = false; AddUntilStep("Wait for music controller", () => Game.MusicController.IsLoaded); AddStep("Seek close to end", () => { Game.MusicController.SeekTo(Game.MusicController.CurrentTrack.Length - 1000); Game.MusicController.CurrentTrack.Completed += () => trackCompleted = true; }); AddUntilStep("Track was completed", () => trackCompleted); AddUntilStep("Track was restarted", () => Game.MusicController.IsPlaying); } [Test] public void TestModSelectInput() { AddUntilStep("Wait for toolbar to load", () => Game.Toolbar.IsLoaded); TestPlaySongSelect songSelect = null; PushAndConfirm(() => songSelect = new TestPlaySongSelect()); AddStep("Show mods overlay", () => songSelect.ModSelectOverlay.Show()); AddStep("Change ruleset to osu!taiko", () => { InputManager.PressKey(Key.ControlLeft); InputManager.Key(Key.Number2); InputManager.ReleaseKey(Key.ControlLeft); }); AddAssert("Ruleset changed to osu!taiko", () => Game.Toolbar.ChildrenOfType().Single().Current.Value.OnlineID == 1); AddAssert("Mods overlay still visible", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible); } [Test] public void TestBeatmapOptionsInput() { AddUntilStep("Wait for toolbar to load", () => Game.Toolbar.IsLoaded); AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); TestPlaySongSelect songSelect = null; PushAndConfirm(() => songSelect = new TestPlaySongSelect()); AddStep("Show options overlay", () => songSelect.BeatmapOptionsOverlay.Show()); AddStep("Change ruleset to osu!taiko", () => { InputManager.PressKey(Key.ControlLeft); InputManager.Key(Key.Number2); InputManager.ReleaseKey(Key.ControlLeft); }); AddAssert("Ruleset changed to osu!taiko", () => Game.Toolbar.ChildrenOfType().Single().Current.Value.OnlineID == 1); AddAssert("Options overlay still visible", () => songSelect.BeatmapOptionsOverlay.State.Value == Visibility.Visible); } [Test] public void TestSettingsViaHotkeyFromMainMenu() { AddUntilStep("Wait for toolbar to load", () => Game.Toolbar.IsLoaded); AddAssert("toolbar not displayed", () => Game.Toolbar.State.Value == Visibility.Hidden); AddStep("press settings hotkey", () => { InputManager.PressKey(Key.ControlLeft); InputManager.Key(Key.O); InputManager.ReleaseKey(Key.ControlLeft); }); AddUntilStep("settings displayed", () => Game.Settings.State.Value == Visibility.Visible); } [Test] public void TestToolbarHiddenByUser() { AddUntilStep("Wait for toolbar to load", () => Game.Toolbar.IsLoaded); AddStep("Enter menu", () => InputManager.Key(Key.Enter)); AddUntilStep("Toolbar is visible", () => Game.Toolbar.State.Value == Visibility.Visible); AddStep("Hide toolbar", () => { InputManager.PressKey(Key.ControlLeft); InputManager.Key(Key.T); InputManager.ReleaseKey(Key.ControlLeft); }); pushEscape(); AddStep("Enter menu", () => InputManager.Key(Key.Enter)); AddAssert("Toolbar is hidden", () => Game.Toolbar.State.Value == Visibility.Hidden); AddStep("Enter song select", () => { InputManager.Key(Key.Enter); InputManager.Key(Key.Enter); }); AddAssert("Toolbar is hidden", () => Game.Toolbar.State.Value == Visibility.Hidden); } [Test] public void TestPushMatchSubScreenAndPressBackButtonImmediately() { TestMultiplayerComponents multiplayerComponents = null; PushAndConfirm(() => multiplayerComponents = new TestMultiplayerComponents()); AddUntilStep("wait for lounge", () => multiplayerComponents.ChildrenOfType().SingleOrDefault()?.IsLoaded == true); AddStep("open room", () => multiplayerComponents.ChildrenOfType().Single().Open()); AddStep("press back button", () => Game.ChildrenOfType().First().Action()); AddWaitStep("wait two frames", 2); AddStep("exit lounge", () => Game.ScreenStack.Exit()); // `TestMultiplayerComponents` registers a request handler in its BDL, but never unregisters it. // to prevent the handler living for longer than it should be, clean up manually. AddStep("clean up multiplayer request handler", () => ((DummyAPIAccess)API).HandleRequest = null); } [Test] public void TestFeaturedArtistDisclaimerDialog() { BeatmapListingOverlay getBeatmapListingOverlay() => Game.ChildrenOfType().FirstOrDefault(); AddStep("Wait for notifications to load", () => Game.SearchBeatmapSet(string.Empty)); AddUntilStep("wait for dialog overlay", () => Game.ChildrenOfType().SingleOrDefault() != null); AddUntilStep("Wait for beatmap overlay to load", () => getBeatmapListingOverlay()?.State.Value == Visibility.Visible); AddAssert("featured artist filter is on", () => getBeatmapListingOverlay().ChildrenOfType().First().Current.Contains(SearchGeneral.FeaturedArtists)); AddStep("toggle featured artist filter", () => getBeatmapListingOverlay().ChildrenOfType>().First(i => i.Value == SearchGeneral.FeaturedArtists).TriggerClick()); AddAssert("disclaimer dialog is shown", () => Game.ChildrenOfType().Single().CurrentDialog != null); AddAssert("featured artist filter is still on", () => getBeatmapListingOverlay().ChildrenOfType().First().Current.Contains(SearchGeneral.FeaturedArtists)); AddStep("confirm", () => InputManager.Key(Key.Enter)); AddAssert("dialog dismissed", () => Game.ChildrenOfType().Single().CurrentDialog == null); AddUntilStep("featured artist filter is off", () => !getBeatmapListingOverlay().ChildrenOfType().First().Current.Contains(SearchGeneral.FeaturedArtists)); } [Test] public void TestBeatmapListingLinkSearchOnInitialOpen() { BeatmapListingOverlay getBeatmapListingOverlay() => Game.ChildrenOfType().FirstOrDefault(); AddStep("open beatmap overlay with test query", () => Game.SearchBeatmapSet("test")); AddUntilStep("wait for beatmap overlay to load", () => getBeatmapListingOverlay()?.State.Value == Visibility.Visible); AddAssert("beatmap overlay sorted by relevance", () => getBeatmapListingOverlay().ChildrenOfType().Single().Current.Value == SortCriteria.Relevance); } [Test] public void TestMainOverlaysClosesNotificationOverlay() { ChangelogOverlay getChangelogOverlay() => Game.ChildrenOfType().FirstOrDefault(); AddUntilStep("Wait for notifications to load", () => Game.Notifications.IsLoaded); AddStep("Show notifications", () => Game.Notifications.Show()); AddUntilStep("wait for notifications shown", () => Game.Notifications.IsPresent && Game.Notifications.State.Value == Visibility.Visible); AddStep("Show changelog listing", () => Game.ShowChangelogListing()); AddUntilStep("wait for changelog shown", () => getChangelogOverlay()?.IsPresent == true && getChangelogOverlay()?.State.Value == Visibility.Visible); AddAssert("Notifications is hidden", () => Game.Notifications.State.Value == Visibility.Hidden); AddStep("Show notifications", () => Game.Notifications.Show()); AddUntilStep("wait for notifications shown", () => Game.Notifications.State.Value == Visibility.Visible); AddUntilStep("changelog still visible", () => getChangelogOverlay().State.Value == Visibility.Visible); } [Test] public void TestMainOverlaysClosesSettingsOverlay() { ChangelogOverlay getChangelogOverlay() => Game.ChildrenOfType().FirstOrDefault(); AddUntilStep("Wait for settings to load", () => Game.Settings.IsLoaded); AddStep("Show settings", () => Game.Settings.Show()); AddUntilStep("wait for settings shown", () => Game.Settings.IsPresent && Game.Settings.State.Value == Visibility.Visible); AddStep("Show changelog listing", () => Game.ShowChangelogListing()); AddUntilStep("wait for changelog shown", () => getChangelogOverlay()?.IsPresent == true && getChangelogOverlay()?.State.Value == Visibility.Visible); AddAssert("Settings is hidden", () => Game.Settings.State.Value == Visibility.Hidden); AddStep("Show settings", () => Game.Settings.Show()); AddUntilStep("wait for settings shown", () => Game.Settings.State.Value == Visibility.Visible); AddUntilStep("changelog still visible", () => getChangelogOverlay().State.Value == Visibility.Visible); } [Test] public void TestOverlayClosing() { // use now playing overlay for "overlay -> background" drag case // since most overlays use a scroll container that absorbs on mouse down NowPlayingOverlay nowPlayingOverlay = null; AddUntilStep("Wait for now playing load", () => (nowPlayingOverlay = Game.ChildrenOfType().FirstOrDefault()) != null); AddStep("enter menu", () => InputManager.Key(Key.Enter)); AddUntilStep("toolbar displayed", () => Game.Toolbar.State.Value == Visibility.Visible); AddStep("open now playing", () => InputManager.Key(Key.F6)); AddUntilStep("now playing is visible", () => nowPlayingOverlay.State.Value == Visibility.Visible); // drag tests // background -> toolbar AddStep("move cursor to background", () => InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.BottomRight)); AddStep("press left mouse button", () => InputManager.PressButton(MouseButton.Left)); AddStep("move cursor to toolbar", () => InputManager.MoveMouseTo(Game.Toolbar.ScreenSpaceDrawQuad.Centre)); AddStep("release left mouse button", () => InputManager.ReleaseButton(MouseButton.Left)); AddAssert("now playing is hidden", () => nowPlayingOverlay.State.Value == Visibility.Hidden); AddStep("press now playing hotkey", () => InputManager.Key(Key.F6)); // toolbar -> background AddStep("press left mouse button", () => InputManager.PressButton(MouseButton.Left)); AddStep("move cursor to background", () => InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.BottomRight)); AddStep("release left mouse button", () => InputManager.ReleaseButton(MouseButton.Left)); AddAssert("now playing is still visible", () => nowPlayingOverlay.State.Value == Visibility.Visible); // background -> overlay AddStep("press left mouse button", () => InputManager.PressButton(MouseButton.Left)); AddStep("move cursor to now playing overlay", () => InputManager.MoveMouseTo(nowPlayingOverlay.ScreenSpaceDrawQuad.Centre)); AddStep("release left mouse button", () => InputManager.ReleaseButton(MouseButton.Left)); AddAssert("now playing is still visible", () => nowPlayingOverlay.State.Value == Visibility.Visible); // overlay -> background AddStep("press left mouse button", () => InputManager.PressButton(MouseButton.Left)); AddStep("move cursor to background", () => InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.BottomRight)); AddStep("release left mouse button", () => InputManager.ReleaseButton(MouseButton.Left)); AddAssert("now playing is still visible", () => nowPlayingOverlay.State.Value == Visibility.Visible); // background -> background AddStep("press left mouse button", () => InputManager.PressButton(MouseButton.Left)); AddStep("move cursor to left", () => InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.BottomLeft)); AddStep("release left mouse button", () => InputManager.ReleaseButton(MouseButton.Left)); AddAssert("now playing is hidden", () => nowPlayingOverlay.State.Value == Visibility.Hidden); AddStep("press now playing hotkey", () => InputManager.Key(Key.F6)); // click tests // toolbar AddStep("move cursor to toolbar", () => InputManager.MoveMouseTo(Game.Toolbar.ScreenSpaceDrawQuad.Centre)); AddStep("click left mouse button", () => InputManager.Click(MouseButton.Left)); AddAssert("now playing is still visible", () => nowPlayingOverlay.State.Value == Visibility.Visible); // background AddStep("move cursor to background", () => InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.BottomRight)); AddStep("click left mouse button", () => InputManager.Click(MouseButton.Left)); AddAssert("now playing is hidden", () => nowPlayingOverlay.State.Value == Visibility.Hidden); // move the mouse firmly inside game bounds to avoid interfering with other tests. AddStep("center cursor", () => InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.Centre)); } [Test] public void TestExitWithOperationInProgress() { int x = 0; AddUntilStep("wait for dialog overlay", () => { x = 0; return Game.ChildrenOfType().SingleOrDefault() != null; }); AddRepeatStep("start ongoing operation", () => { Game.Notifications.Post(new ProgressNotification { Text = $"Something is still running #{++x}", Progress = 0.5f, State = ProgressNotificationState.Active, }); }, 15); AddAssert("all notifications = 15", () => Game.Notifications.AllNotifications.Count(), () => Is.EqualTo(15)); AddStep("Hold escape", () => InputManager.PressKey(Key.Escape)); AddUntilStep("confirmation dialog shown", () => Game.ChildrenOfType().Single().CurrentDialog is ConfirmExitDialog); AddStep("Release escape", () => InputManager.ReleaseKey(Key.Escape)); AddStep("cancel exit", () => InputManager.Key(Key.Escape)); AddAssert("dialog dismissed", () => Game.ChildrenOfType().Single().CurrentDialog == null); AddStep("complete operation", () => { this.ChildrenOfType().ForEach(n => { n.Progress = 100; n.State = ProgressNotificationState.Completed; }); }); AddStep("Hold escape", () => InputManager.PressKey(Key.Escape)); AddUntilStep("Wait for intro", () => Game.ScreenStack.CurrentScreen is IntroScreen); AddStep("Release escape", () => InputManager.ReleaseKey(Key.Escape)); AddUntilStep("Wait for game exit", () => Game.ScreenStack.CurrentScreen == null); } [Test] public void TestForceExitWithOperationInProgress() { AddStep("set hold delay to 0", () => Game.LocalConfig.SetValue(OsuSetting.UIHoldActivationDelay, 0.0)); AddUntilStep("wait for dialog overlay", () => Game.ChildrenOfType().SingleOrDefault() != null); AddRepeatStep("start ongoing operation", () => { Game.Notifications.Post(new ProgressNotification { Text = "Something is still running", Progress = 0.5f, State = ProgressNotificationState.Active, }); }, 15); AddRepeatStep("attempt force exit", () => Game.ScreenStack.CurrentScreen.Exit(), 2); AddUntilStep("stopped at exit confirm", () => Game.ChildrenOfType().Single().CurrentDialog is ConfirmExitDialog); } [Test] public void TestExitGameFromSongSelect() { PushAndConfirm(() => new TestPlaySongSelect()); exitViaEscapeAndConfirm(); pushEscape(); // returns to osu! logo AddStep("Hold escape", () => InputManager.PressKey(Key.Escape)); AddUntilStep("Wait for intro", () => Game.ScreenStack.CurrentScreen is IntroScreen); AddStep("Release escape", () => InputManager.ReleaseKey(Key.Escape)); AddUntilStep("Wait for game exit", () => Game.ScreenStack.CurrentScreen == null); AddStep("test dispose doesn't crash", () => Game.Dispose()); } [Test] public void TestExitWithHoldDisabled() { AddStep("set hold delay to 0", () => Game.LocalConfig.SetValue(OsuSetting.UIHoldActivationDelay, 0.0)); AddStep("press escape twice rapidly", () => { InputManager.Key(Key.Escape); Schedule(InputManager.Key, Key.Escape); }); pushEscape(); AddAssert("exit dialog is shown", () => Game.Dependencies.Get().CurrentDialog is ConfirmExitDialog); } [Test] public void TestQuickSkinEditorDoesntNukeSkin() { AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); AddStep("open", () => InputManager.Key(Key.Space)); AddStep("skin", () => InputManager.Key(Key.E)); AddStep("editor", () => InputManager.Key(Key.S)); AddStep("and close immediately", () => InputManager.Key(Key.Escape)); AddStep("open again", () => InputManager.Key(Key.S)); Player player = null; AddUntilStep("wait for player", () => (player = Game.ScreenStack.CurrentScreen as Player) != null); AddUntilStep("wait for gameplay still has health bar", () => player.ChildrenOfType().Any()); } [Test] public void TestTouchScreenDetectionAtSongSelect() { AddStep("touch logo", () => { var button = Game.ChildrenOfType().Single(); var touch = new Touch(TouchSource.Touch1, button.ScreenSpaceDrawQuad.Centre); InputManager.BeginTouch(touch); InputManager.EndTouch(touch); }); AddAssert("touch screen detected active", () => Game.Dependencies.Get().Get(Static.TouchInputActive), () => Is.True); AddStep("click settings button", () => { var button = Game.ChildrenOfType().Last(); InputManager.MoveMouseTo(button); InputManager.Click(MouseButton.Left); }); AddAssert("touch screen detected inactive", () => Game.Dependencies.Get().Get(Static.TouchInputActive), () => Is.False); AddStep("close settings sidebar", () => InputManager.Key(Key.Escape)); Screens.Select.SongSelect songSelect = null; AddRepeatStep("go to solo", () => InputManager.Key(Key.P), 3); AddUntilStep("wait for song select", () => (songSelect = Game.ScreenStack.CurrentScreen as Screens.Select.SongSelect) != null); AddUntilStep("wait for beatmap sets loaded", () => songSelect.BeatmapSetsLoaded); AddStep("switch to osu! ruleset", () => { InputManager.PressKey(Key.LControl); InputManager.Key(Key.Number1); InputManager.ReleaseKey(Key.LControl); }); AddStep("touch beatmap wedge", () => { var wedge = Game.ChildrenOfType().Single(); var touch = new Touch(TouchSource.Touch2, wedge.ScreenSpaceDrawQuad.Centre); InputManager.BeginTouch(touch); InputManager.EndTouch(touch); }); AddUntilStep("touch device mod activated", () => Game.SelectedMods.Value, () => Has.One.InstanceOf()); AddStep("switch to mania ruleset", () => { InputManager.PressKey(Key.LControl); InputManager.Key(Key.Number4); InputManager.ReleaseKey(Key.LControl); }); AddUntilStep("touch device mod not activated", () => Game.SelectedMods.Value, () => Has.None.InstanceOf()); AddStep("touch beatmap wedge", () => { var wedge = Game.ChildrenOfType().Single(); var touch = new Touch(TouchSource.Touch2, wedge.ScreenSpaceDrawQuad.Centre); InputManager.BeginTouch(touch); InputManager.EndTouch(touch); }); AddUntilStep("touch device mod not activated", () => Game.SelectedMods.Value, () => Has.None.InstanceOf()); AddStep("switch to osu! ruleset", () => { InputManager.PressKey(Key.LControl); InputManager.Key(Key.Number1); InputManager.ReleaseKey(Key.LControl); }); AddUntilStep("touch device mod activated", () => Game.SelectedMods.Value, () => Has.One.InstanceOf()); AddStep("click beatmap wedge", () => { InputManager.MoveMouseTo(Game.ChildrenOfType().Single()); InputManager.Click(MouseButton.Left); }); AddUntilStep("touch device mod not activated", () => Game.SelectedMods.Value, () => Has.None.InstanceOf()); } [Test] public void TestTouchScreenDetectionInGame() { PushAndConfirm(() => new TestPlaySongSelect()); AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); AddStep("select", () => InputManager.Key(Key.Enter)); Player player = null; AddUntilStep("wait for player", () => { DismissAnyNotifications(); return (player = Game.ScreenStack.CurrentScreen as Player) != null; }); AddUntilStep("wait for track playing", () => Game.Beatmap.Value.Track.IsRunning); AddStep("touch", () => { var touch = new Touch(TouchSource.Touch2, Game.ScreenSpaceDrawQuad.Centre); InputManager.BeginTouch(touch); InputManager.EndTouch(touch); }); AddUntilStep("touch device mod added to score", () => player.Score.ScoreInfo.Mods, () => Has.One.InstanceOf()); AddStep("exit player", () => player.Exit()); AddUntilStep("touch device mod still active", () => Game.SelectedMods.Value, () => Has.One.InstanceOf()); } [Test] public void TestExitSongSelectAndImmediatelyClickLogo() { Screens.Select.SongSelect songSelect = null; PushAndConfirm(() => songSelect = new TestPlaySongSelect()); AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); AddStep("press escape and then click logo immediately", () => { InputManager.Key(Key.Escape); clickLogoWhenNotCurrent(); }); void clickLogoWhenNotCurrent() { if (songSelect.IsCurrentScreen()) Scheduler.AddOnce(clickLogoWhenNotCurrent); else { InputManager.MoveMouseTo(Game.ChildrenOfType().Single()); InputManager.Click(MouseButton.Left); } } } [Test] public void TestPresentBeatmapAfterDeletion() { BeatmapSetInfo beatmap = null; Screens.Select.SongSelect songSelect = null; PushAndConfirm(() => songSelect = new TestPlaySongSelect()); AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); AddStep("delete selected beatmap", () => { beatmap = Game.Beatmap.Value.BeatmapSetInfo; Game.BeatmapManager.Delete(Game.Beatmap.Value.BeatmapSetInfo); }); AddUntilStep("nothing selected", () => Game.Beatmap.IsDefault); AddStep("present deleted beatmap", () => Game.PresentBeatmap(beatmap)); AddAssert("still nothing selected", () => Game.Beatmap.IsDefault); } private Func playToResults() { var player = playToCompletion(); AddUntilStep("wait for results", () => (Game.ScreenStack.CurrentScreen as ResultsScreen)?.IsLoaded == true); return player; } private Func playToCompletion() { Player player = null; IWorkingBeatmap beatmap() => Game.Beatmap.Value; Screens.Select.SongSelect songSelect = null; PushAndConfirm(() => songSelect = new TestPlaySongSelect()); AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); AddStep("set mods", () => Game.SelectedMods.Value = new Mod[] { new OsuModNoFail(), new OsuModDoubleTime { SpeedChange = { Value = 2 } } }); AddStep("press enter", () => InputManager.Key(Key.Enter)); AddUntilStep("wait for player", () => { DismissAnyNotifications(); return (player = Game.ScreenStack.CurrentScreen as Player) != null; }); AddUntilStep("wait for track playing", () => beatmap().Track.IsRunning); AddStep("seek to near end", () => player.ChildrenOfType().First().Seek(beatmap().Beatmap.HitObjects[^1].StartTime - 1000)); AddUntilStep("wait for complete", () => player.GameplayState.HasPassed); return () => player; } private void pushEscape() => AddStep("Press escape", () => InputManager.Key(Key.Escape)); private void exitViaEscapeAndConfirm() { pushEscape(); ConfirmAtMainMenu(); } private void exitViaBackButtonAndConfirm() { AddStep("Move mouse to backButton", () => InputManager.MoveMouseTo(backButtonPosition)); AddStep("Click back button", () => InputManager.Click(MouseButton.Left)); ConfirmAtMainMenu(); } public partial class TestPlaySongSelect : PlaySongSelect { public ModSelectOverlay ModSelectOverlay => ModSelect; public BeatmapOptionsOverlay BeatmapOptionsOverlay => BeatmapOptions; } } }