553db2bce3
Replaces PreMatch/Live placeholder pages with a shared EventListShell
(filter chips, date range, sortable virtualized-friendly table, debounced
search, live auto-refresh with odds-movement indicators) and adds a new
/events/{eventCode} detail page (asymmetric header lockup, dynamic
Match/Period tabs, Plotly.Blazor odds-over-time chart with accessible
data-table fallback, snapshot history, Excel export modal).
New primitives matching Phase 5's editorial-quant system:
- SportIcon: inline SVGs per sport (basketball=6, football=11,
tennis=22723, hockey=43658, generic fallback)
- OddsCell: tabular mono with ▲/▼/— delta + flash on change
(prefers-reduced-motion honored)
- OddsTimeline: Plotly.Blazor wrapper with theme-aware colors and
<details>/<summary> data-table screen-reader fallback
- ExportDialog: From/To pickers + ExportKind radio + Esc/Enter
keyboard, surfaces use-case errors inline
- EventListShell: shared section shell for PreMatch/Live cadence
State + service split keeps the RCL host-agnostic:
- IEventBrowsingService / EventBrowsingService — wraps repos, returns
view-model records (EventListItem, EventDetail, EventScopeBoard,
BetRow, OddsTimelinePoint, SnapshotHistoryEntry); pages never see
EF or domain entities directly.
- EventBrowsingState — singleton (per-circuit in BlazorWebView) holding
immutable PageFilter records for PreMatch and Live.
Plotly.Blazor 5.4.1 added (latest .NET 8 line; 7.x has breaking changes).
+59 RU/EN localization keys following the Phase 5 dot-segmented convention.
Tests: +26 bUnit tests (PreMatch/Live/Detail pages, OddsCell/SportIcon/
ExportDialog components, EventBrowsingState). Total 228/228 passing
(Domain 96 + Application 15 + Infrastructure 80 + UI 37; baseline 202).
Build clean (0/0).
PLAN.md: P2/P3/P5 top-level checkboxes ticked; P6 row marked Done.
64 lines
1.8 KiB
C#
64 lines
1.8 KiB
C#
using Bunit;
|
|
using Marathon.UI.Components;
|
|
using Marathon.UI.Tests.Support;
|
|
|
|
namespace Marathon.UI.Tests.Components;
|
|
|
|
public sealed class OddsCellTests : MarathonTestContext
|
|
{
|
|
[Fact]
|
|
public void Formats_decimal_to_two_places()
|
|
{
|
|
var cut = RenderComponent<OddsCell>(p => p.Add(c => c.Rate, 1.8m));
|
|
cut.Find(".m-odds__value").TextContent.Trim().Should().Be("1.80");
|
|
}
|
|
|
|
[Fact]
|
|
public void Renders_em_dash_when_rate_is_null()
|
|
{
|
|
var cut = RenderComponent<OddsCell>(p =>
|
|
{
|
|
p.Add(c => c.Rate, (decimal?)null);
|
|
p.Add(c => c.EmptyPlaceholder, "—");
|
|
});
|
|
cut.Find(".m-odds__value").TextContent.Trim().Should().Be("—");
|
|
}
|
|
|
|
[Fact]
|
|
public void Marks_up_when_rate_increased()
|
|
{
|
|
var cut = RenderComponent<OddsCell>(p =>
|
|
{
|
|
p.Add(c => c.Rate, 1.95m);
|
|
p.Add(c => c.Previous, 1.85m);
|
|
p.Add(c => c.ShowTrend, true);
|
|
});
|
|
cut.Find(".m-odds").GetAttribute("data-trend").Should().Be("rising");
|
|
cut.Find(".m-odds__delta").TextContent.Should().Contain("▲");
|
|
}
|
|
|
|
[Fact]
|
|
public void Marks_down_when_rate_decreased()
|
|
{
|
|
var cut = RenderComponent<OddsCell>(p =>
|
|
{
|
|
p.Add(c => c.Rate, 1.70m);
|
|
p.Add(c => c.Previous, 1.85m);
|
|
p.Add(c => c.ShowTrend, true);
|
|
});
|
|
cut.Find(".m-odds").GetAttribute("data-trend").Should().Be("falling");
|
|
cut.Find(".m-odds__delta").TextContent.Should().Contain("▼");
|
|
}
|
|
|
|
[Fact]
|
|
public void Hides_delta_glyph_when_show_trend_false()
|
|
{
|
|
var cut = RenderComponent<OddsCell>(p =>
|
|
{
|
|
p.Add(c => c.Rate, 1.85m);
|
|
p.Add(c => c.ShowTrend, false);
|
|
});
|
|
cut.FindAll(".m-odds__delta").Should().BeEmpty();
|
|
}
|
|
}
|