feat(phase-8-frontend): results loader UI + browsing list + 41 localization keys
* Pages/Results/ResultsList.razor — completed-events list with date range, sport/winner filter, search, footer count. * Pages/Results/ResultsLoader.razor — driver page with two modes (load all in range / load selected events), live progress reporting via IProgress<PullResultsProgress>, summary line, cancellable. * Replaces the Phase 5 Pages/Results.razor placeholder. Service layer: * IResultsBrowsingService + ResultsBrowsingService (Scoped, mirrors the Event/Anomaly browsing-service pattern). Reads IResultRepository + IEventRepository, projects to immutable view-model records. * UiServicesExtensions: registers ResultsBrowsingService; also fixes an unrelated localization resolver bug (drop ResourcesPath since SharedResource lives in the Marathon.UI.Resources namespace already). Localization: * 41 new Results.* keys (RU+EN parity) covering both pages, filter chips, loader modes, progress states, and footer copy. Tests: * ResultsListTests + ResultsLoaderTests — 22 new bUnit tests covering filter narrowing, mode switching, progress aggregation, and empty states. * FakeResultsBrowsingService support type for tests. * MarathonTestContext registers the fake; TestData adds factories for EventResult/EventResultListItem.
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
using Marathon.Application.Storage;
|
||||
using Marathon.UI.Services;
|
||||
|
||||
namespace Marathon.UI.Tests.Support;
|
||||
|
||||
/// <summary>
|
||||
/// In-memory <see cref="IResultsBrowsingService"/> for bUnit tests.
|
||||
/// Seed via <see cref="ResultItems"/> / <see cref="Candidates"/>.
|
||||
/// </summary>
|
||||
public sealed class FakeResultsBrowsingService : IResultsBrowsingService
|
||||
{
|
||||
public List<EventResultListItem> ResultItems { get; } = new();
|
||||
public List<EventResultCandidate> Candidates { get; } = new();
|
||||
public ResultsFilter? LastResultsFilter { get; private set; }
|
||||
public DateRange? LastCandidatesRange { get; private set; }
|
||||
|
||||
public Task<IReadOnlyList<EventResultListItem>> ListResultsAsync(
|
||||
ResultsFilter filter,
|
||||
CancellationToken ct)
|
||||
{
|
||||
LastResultsFilter = filter;
|
||||
IEnumerable<EventResultListItem> q = ResultItems;
|
||||
|
||||
if (filter.SportCodes is { Count: > 0 } sports)
|
||||
q = q.Where(r => sports.Contains(r.Sport.Value));
|
||||
|
||||
if (filter.WinnerSide is { } winner)
|
||||
q = q.Where(r => r.WinnerSide == winner);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(filter.SearchTerm))
|
||||
{
|
||||
var t = filter.SearchTerm.Trim();
|
||||
q = q.Where(r =>
|
||||
r.LeagueId.Contains(t, StringComparison.OrdinalIgnoreCase) ||
|
||||
r.Side1Name.Contains(t, StringComparison.OrdinalIgnoreCase) ||
|
||||
r.Side2Name.Contains(t, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
return Task.FromResult<IReadOnlyList<EventResultListItem>>(q.ToList());
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<EventResultCandidate>> ListLoadCandidatesAsync(
|
||||
DateRange range,
|
||||
CancellationToken ct)
|
||||
{
|
||||
LastCandidatesRange = range;
|
||||
return Task.FromResult<IReadOnlyList<EventResultCandidate>>(Candidates.ToList());
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ public abstract class MarathonTestContext : TestContext
|
||||
protected FakeEventBrowsingService Browsing { get; } = new();
|
||||
protected AnomalyBrowsingState AnomalyState { get; } = new();
|
||||
protected FakeAnomalyBrowsingService AnomalyBrowsing { get; } = new();
|
||||
protected FakeResultsBrowsingService Results { get; } = new();
|
||||
|
||||
protected MarathonTestContext()
|
||||
{
|
||||
@@ -34,6 +35,7 @@ public abstract class MarathonTestContext : TestContext
|
||||
Services.AddSingleton<IEventBrowsingService>(Browsing);
|
||||
Services.AddSingleton(AnomalyState);
|
||||
Services.AddSingleton<IAnomalyBrowsingService>(AnomalyBrowsing);
|
||||
Services.AddSingleton<IResultsBrowsingService>(Results);
|
||||
|
||||
Services.AddSingleton(typeof(IStringLocalizer<>), typeof(TestLocalizer<>));
|
||||
Services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>));
|
||||
|
||||
@@ -44,6 +44,48 @@ internal static class TestData
|
||||
return midnight.AddHours(hour);
|
||||
}
|
||||
|
||||
public static EventResultListItem ResultListItem(
|
||||
string id = "100001",
|
||||
int sport = 11,
|
||||
string country = "ENG",
|
||||
string league = "Premier League",
|
||||
string side1 = "Arsenal",
|
||||
string side2 = "Chelsea",
|
||||
int side1Score = 2,
|
||||
int side2Score = 1,
|
||||
Side winner = Side.Side1,
|
||||
DateTimeOffset? scheduled = null,
|
||||
DateTimeOffset? completed = null)
|
||||
=> new(
|
||||
new EventId(id),
|
||||
new SportCode(sport),
|
||||
country,
|
||||
league,
|
||||
side1,
|
||||
side2,
|
||||
scheduled ?? MoscowToday(20),
|
||||
side1Score,
|
||||
side2Score,
|
||||
winner,
|
||||
completed ?? MoscowToday(22));
|
||||
|
||||
public static EventResultCandidate ResultCandidate(
|
||||
string id = "100001",
|
||||
int sport = 11,
|
||||
string country = "ENG",
|
||||
string league = "Premier League",
|
||||
string side1 = "Arsenal",
|
||||
string side2 = "Chelsea",
|
||||
DateTimeOffset? scheduled = null)
|
||||
=> new(
|
||||
new EventId(id),
|
||||
new SportCode(sport),
|
||||
country,
|
||||
league,
|
||||
side1,
|
||||
side2,
|
||||
scheduled ?? MoscowToday(20));
|
||||
|
||||
public static EventDetail Detail(
|
||||
string id = "100001",
|
||||
params OddsTimelinePoint[] timeline)
|
||||
|
||||
Reference in New Issue
Block a user