// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Linq; using System.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.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 class TestScenePlaylistsResultsScreen : ScreenTestScene { private const int scores_per_result = 10; private const int real_user_position = 200; private TestResultsScreen resultsScreen; private int currentScoreId; private bool requestComplete; private int totalCount; [SetUp] public void Setup() => Schedule(() => { currentScoreId = 1; requestComplete = false; totalCount = 0; bindHandler(); // 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() { ScoreInfo userScore = null; AddStep("bind user score info handler", () => { userScore = TestResources.CreateTestScoreInfo(); userScore.OnlineID = currentScoreId++; bindHandler(userScore: userScore); }); createResults(() => userScore); 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() { createResults(); AddAssert("top score selected", () => this.ChildrenOfType().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded); } [Test] public void TestShowUserScoreWithDelay() { ScoreInfo userScore = null; AddStep("bind user score info handler", () => { userScore = TestResources.CreateTestScoreInfo(); userScore.OnlineID = currentScoreId++; bindHandler(true, userScore); }); createResults(() => userScore); 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(); AddAssert("top score selected", () => this.ChildrenOfType().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded); } [Test] public void TestFetchWhenScrolledToTheRight() { createResults(); 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 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 TestFetchWhenScrolledToTheLeft() { ScoreInfo userScore = null; AddStep("bind user score info handler", () => { userScore = TestResources.CreateTestScoreInfo(); userScore.OnlineID = currentScoreId++; bindHandler(userScore: userScore); }); createResults(() => userScore); 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); } } private void createResults(Func getScore = null) { AddStep("load results", () => { LoadScreen(resultsScreen = new TestResultsScreen(getScore?.Invoke(), 1, new PlaylistItem { Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, Ruleset = { Value = new OsuRuleset().RulesetInfo } })); }); AddUntilStep("wait for screen to load", () => resultsScreen.IsLoaded); waitForDisplay(); } private void waitForDisplay() { AddUntilStep("wait for scores loaded", () => requestComplete && 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) => ((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)); 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 = (int)(userScore.OnlineID > 0 ? userScore.OnlineID : currentScoreId++), Accuracy = userScore.Accuracy, EndedAt = userScore.Date, Passed = userScore.Passed, Rank = userScore.Rank, Position = real_user_position, MaxCombo = userScore.MaxCombo, TotalScore = userScore.TotalScore, User = userScore.User, Statistics = userScore.Statistics, 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 = currentScoreId++, Accuracy = userScore.Accuracy, EndedAt = userScore.Date, Passed = true, Rank = userScore.Rank, MaxCombo = userScore.MaxCombo, TotalScore = userScore.TotalScore - i, User = new APIUser { Id = 2, Username = $"peppy{i}", CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", }, Statistics = userScore.Statistics }); multiplayerUserScore.ScoresAround.Higher.Scores.Add(new MultiplayerScore { ID = currentScoreId++, Accuracy = userScore.Accuracy, EndedAt = userScore.Date, Passed = true, Rank = userScore.Rank, MaxCombo = userScore.MaxCombo, TotalScore = userScore.TotalScore + i, User = new APIUser { Id = 2, Username = $"peppy{i}", CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", }, Statistics = userScore.Statistics }); totalCount += 2; } addCursor(multiplayerUserScore.ScoresAround.Lower); addCursor(multiplayerUserScore.ScoresAround.Higher); return multiplayerUserScore; } private IndexedMultiplayerScores createIndexResponse(IndexPlaylistScoresRequest req) { var result = new IndexedMultiplayerScores(); long startTotalScore = req.Cursor?.Properties["total_score"].ToObject() ?? 1000000; string sort = req.IndexParams?.Properties["sort"].ToObject() ?? "score_desc"; for (int i = 1; i <= scores_per_result; i++) { result.Scores.Add(new MultiplayerScore { ID = currentScoreId++, Accuracy = 1, EndedAt = DateTimeOffset.Now, Passed = true, Rank = ScoreRank.X, MaxCombo = 1000, TotalScore = startTotalScore + (sort == "score_asc" ? i : -i), User = new APIUser { Id = 2, Username = $"peppy{i}", CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", }, Statistics = new Dictionary { { HitResult.Miss, 1 }, { HitResult.Meh, 50 }, { HitResult.Good, 100 }, { HitResult.Great, 300 } } }); totalCount++; } addCursor(result); return result; } 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 { { "sort", JToken.FromObject(scores.Scores[^1].TotalScore > scores.Scores[^2].TotalScore ? "score_asc" : "score_desc") } } }; } private 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(ScoreInfo score, int roomId, PlaylistItem playlistItem, bool allowRetry = true) : base(score, roomId, playlistItem, allowRetry) { } } } }