// 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.Collections.Generic; using System.Linq; using System.Net; using JetBrains.Annotations; using Newtonsoft.Json.Linq; using NUnit.Framework; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Placeholders; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Screens.Ranking; using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Resources; namespace osu.Game.Tests.Visual.Playlists { public partial class TestScenePlaylistsResultsScreen : ScreenTestScene { private const int scores_per_result = 10; private const int real_user_position = 200; private TestResultsScreen resultsScreen; private int lowestScoreId; // Score ID of the lowest score in the list. private int highestScoreId; // Score ID of the highest score in the list. private bool requestComplete; private int totalCount; private ScoreInfo userScore; [SetUpSteps] public override void SetUpSteps() { base.SetUpSteps(); // Previous test instances of the results screen may still exist at this point so wait for // those screens to be cleaned up by the base SetUpSteps before re-initialising test state. // The screen also holds a leased Beatmap bindable so reassigning it must happen after // the screen has been exited. AddStep("initialise user scores and beatmap", () => { lowestScoreId = 1; highestScoreId = 1; requestComplete = false; totalCount = 0; userScore = TestResources.CreateTestScoreInfo(); userScore.TotalScore = 0; userScore.Statistics = new Dictionary(); userScore.MaximumStatistics = new Dictionary(); // Beatmap is required to be an actual beatmap so the scores can get their scores correctly // calculated for standardised scoring, else the tests that rely on ordering will fall over. Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value); }); } [Test] public void TestShowWithUserScore() { AddStep("bind user score info handler", () => bindHandler(userScore: userScore)); createResults(() => userScore); waitForDisplay(); AddAssert("user score selected", () => this.ChildrenOfType().Single(p => p.Score.OnlineID == userScore.OnlineID).State == PanelState.Expanded); AddAssert($"score panel position is {real_user_position}", () => this.ChildrenOfType().Single(p => p.Score.OnlineID == userScore.OnlineID).ScorePosition.Value == real_user_position); } [Test] public void TestShowNullUserScore() { AddStep("bind user score info handler", () => bindHandler()); createResults(); waitForDisplay(); AddAssert("top score selected", () => this.ChildrenOfType().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded); } [Test] public void TestShowUserScoreWithDelay() { AddStep("bind user score info handler", () => bindHandler(true, userScore)); createResults(() => userScore); waitForDisplay(); AddAssert("more than 1 panel displayed", () => this.ChildrenOfType().Count() > 1); AddAssert("user score selected", () => this.ChildrenOfType().Single(p => p.Score.OnlineID == userScore.OnlineID).State == PanelState.Expanded); } [Test] public void TestShowNullUserScoreWithDelay() { AddStep("bind delayed handler", () => bindHandler(true)); createResults(); waitForDisplay(); AddAssert("top score selected", () => this.ChildrenOfType().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded); } [Test] public void TestFetchWhenScrolledToTheRight() { AddStep("bind delayed handler", () => bindHandler(true)); createResults(); waitForDisplay(); for (int i = 0; i < 2; i++) { int beforePanelCount = 0; AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType().Count()); AddStep("scroll to right", () => resultsScreen.ScorePanelList.ChildrenOfType().Single().ScrollToEnd(false)); AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible); waitForDisplay(); AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType().Count() >= beforePanelCount + scores_per_result); AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden); } } [Test] public void TestNoMoreScoresToTheRight() { AddStep("bind delayed handler with scores", () => bindHandler(delayed: true)); createResults(); waitForDisplay(); int beforePanelCount = 0; AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType().Count()); AddStep("scroll to right", () => resultsScreen.ScorePanelList.ChildrenOfType().Single().ScrollToEnd(false)); AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible); waitForDisplay(); AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType().Count() >= beforePanelCount + scores_per_result); AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden); AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType().Count()); AddStep("bind delayed handler with no scores", () => bindHandler(delayed: true, noScores: true)); AddStep("scroll to right", () => resultsScreen.ScorePanelList.ChildrenOfType().Single().ScrollToEnd(false)); AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible); waitForDisplay(); AddAssert("count not increased", () => this.ChildrenOfType().Count() == beforePanelCount); AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden); AddAssert("no placeholders shown", () => this.ChildrenOfType().Count(), () => Is.Zero); } [Test] public void TestFetchWhenScrolledToTheLeft() { AddStep("bind user score info handler", () => bindHandler(userScore: userScore)); createResults(() => userScore); waitForDisplay(); AddStep("bind delayed handler", () => bindHandler(true)); for (int i = 0; i < 2; i++) { int beforePanelCount = 0; AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType().Count()); AddStep("scroll to left", () => resultsScreen.ScorePanelList.ChildrenOfType().Single().ScrollToStart(false)); AddAssert("left loading spinner shown", () => resultsScreen.LeftSpinner.State.Value == Visibility.Visible); waitForDisplay(); AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType().Count() >= beforePanelCount + scores_per_result); AddAssert("left loading spinner hidden", () => resultsScreen.LeftSpinner.State.Value == Visibility.Hidden); } } [Test] public void TestShowWithNoScores() { AddStep("bind user score info handler", () => bindHandler(noScores: true)); createResults(); AddAssert("no scores visible", () => !resultsScreen.ScorePanelList.GetScorePanels().Any()); AddAssert("placeholder shown", () => this.ChildrenOfType().Count(), () => Is.EqualTo(1)); } private void createResults(Func getScore = null) { AddStep("load results", () => { LoadScreen(resultsScreen = new TestResultsScreen(getScore?.Invoke(), 1, new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) { RulesetID = new OsuRuleset().RulesetInfo.OnlineID })); }); AddUntilStep("wait for screen to load", () => resultsScreen.IsLoaded); } private void waitForDisplay() { AddUntilStep("wait for scores loaded", () => requestComplete // request handler may need to fire more than once to get scores. && totalCount > 0 && resultsScreen.ScorePanelList.GetScorePanels().Count() == totalCount && resultsScreen.ScorePanelList.AllPanelsVisible); AddWaitStep("wait for display", 5); } private void bindHandler(bool delayed = false, ScoreInfo userScore = null, bool failRequests = false, bool noScores = false) => ((DummyAPIAccess)API).HandleRequest = request => { // pre-check for requests we should be handling (as they are scheduled below). switch (request) { case ShowPlaylistUserScoreRequest: case IndexPlaylistScoresRequest: break; default: return false; } requestComplete = false; double delay = delayed ? 3000 : 0; Scheduler.AddDelayed(() => { if (failRequests) { triggerFail(request); return; } switch (request) { case ShowPlaylistUserScoreRequest s: if (userScore == null) triggerFail(s); else triggerSuccess(s, createUserResponse(userScore)); break; case IndexPlaylistScoresRequest i: triggerSuccess(i, createIndexResponse(i, noScores)); break; } }, delay); return true; }; private void triggerSuccess(APIRequest req, T result) where T : class { requestComplete = true; req.TriggerSuccess(result); } private void triggerFail(APIRequest req) { requestComplete = true; req.TriggerFailure(new WebException("Failed.")); } private MultiplayerScore createUserResponse([NotNull] ScoreInfo userScore) { var multiplayerUserScore = new MultiplayerScore { ID = highestScoreId, Accuracy = userScore.Accuracy, Passed = userScore.Passed, Rank = userScore.Rank, Position = real_user_position, MaxCombo = userScore.MaxCombo, User = userScore.User, ScoresAround = new MultiplayerScoresAround { Higher = new MultiplayerScores(), Lower = new MultiplayerScores() } }; totalCount++; for (int i = 1; i <= scores_per_result; i++) { multiplayerUserScore.ScoresAround.Lower.Scores.Add(new MultiplayerScore { ID = getNextLowestScoreId(), Accuracy = userScore.Accuracy, Passed = true, Rank = userScore.Rank, MaxCombo = userScore.MaxCombo, User = new APIUser { Id = 2, Username = $"peppy{i}", CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", }, }); multiplayerUserScore.ScoresAround.Higher.Scores.Add(new MultiplayerScore { ID = getNextHighestScoreId(), Accuracy = userScore.Accuracy, Passed = true, Rank = userScore.Rank, MaxCombo = userScore.MaxCombo, User = new APIUser { Id = 2, Username = $"peppy{i}", CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", }, }); totalCount += 2; } addCursor(multiplayerUserScore.ScoresAround.Lower); addCursor(multiplayerUserScore.ScoresAround.Higher); return multiplayerUserScore; } private IndexedMultiplayerScores createIndexResponse(IndexPlaylistScoresRequest req, bool noScores = false) { var result = new IndexedMultiplayerScores(); if (noScores) return result; string sort = req.IndexParams?.Properties["sort"].ToObject() ?? "score_desc"; for (int i = 1; i <= scores_per_result; i++) { result.Scores.Add(new MultiplayerScore { ID = sort == "score_asc" ? getNextHighestScoreId() : getNextLowestScoreId(), Accuracy = 1, Passed = true, Rank = ScoreRank.X, MaxCombo = 1000, User = new APIUser { Id = 2, Username = $"peppy{i}", CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", }, }); totalCount++; } addCursor(result); return result; } /// /// The next highest score ID to appear at the left of the list. Monotonically decreasing. /// private int getNextHighestScoreId() => --highestScoreId; /// /// The next lowest score ID to appear at the right of the list. Monotonically increasing. /// /// private int getNextLowestScoreId() => ++lowestScoreId; private void addCursor(MultiplayerScores scores) { scores.Cursor = new Cursor { Properties = new Dictionary { { "total_score", JToken.FromObject(scores.Scores[^1].TotalScore) }, { "score_id", JToken.FromObject(scores.Scores[^1].ID) }, } }; scores.Params = new IndexScoresParams { Properties = new Dictionary { // [ 1, 2, 3, ... ] => score_desc (will be added to the right of the list) // [ 3, 2, 1, ... ] => score_asc (will be added to the left of the list) { "sort", JToken.FromObject(scores.Scores[^1].ID > scores.Scores[^2].ID ? "score_desc" : "score_asc") } } }; } private partial class TestResultsScreen : PlaylistsResultsScreen { public new LoadingSpinner LeftSpinner => base.LeftSpinner; public new LoadingSpinner CentreSpinner => base.CentreSpinner; public new LoadingSpinner RightSpinner => base.RightSpinner; public new ScorePanelList ScorePanelList => base.ScorePanelList; public TestResultsScreen([CanBeNull] ScoreInfo score, int roomId, PlaylistItem playlistItem, bool allowRetry = true) : base(score, roomId, playlistItem, allowRetry) { } } } }