ci / build-test (push) Successful in 2m41s
Three things a reported Heizoel page got wrong at once. Its tank is dipped a few times a year and its burner read every few months, which is exactly the shape the coverage rules had not been walked through. "No data" for data that exists. A tank books nothing until the next dipstick closes the interval, so the stretch after the last dipstick is covered by no run at all, and a bucket no run covers was reported missing. The burner, whose run reaches into the window, said "only coarser data" -- the honest answer -- so one card claimed there was nothing while the coverage panel beside it listed years of data. A bucket that no run covers, no gap overlaps and no opening balance explains now reports the meter's resolution when its preceding coverage is within one interval of its own class: it is not silent, it is read rarely. A meter that does book its own buckets and stops -- a dead hourly source, a sheet asked about a later month -- still reads missing. Auto answering twelve months with one bar. Coarse only means "longer than a month", so a dipstick taken each autumn straddles a New Year as surely as a month start: coarsening the chart to years bought nothing and cost every point. The planning resolution now caps coarse at month when a run crosses a local year edge, and a series that cannot resolve the natural size no longer coarsens the whole chart -- it is drawn at that size with its buckets marked, which the chart and table already explain. A page contradicting itself. The comparison line above the ranking was fed the leading measure's matched coverage but worded as if it spoke for the page, directly above a burner row that did compare. It now names the figure it is about. Alongside: the "largest changes" ranking no longer drops a meter whose change is not comparable. It ranks what can be ranked, then lists the rest with their values and the reason -- the tank had been vanishing from its own energy type. And every dated table now reads newest first, as lists are read; charts stay chronological left to right, and the CSV export stays ascending for spreadsheets. A-41 to A-43 in the note record the three rules.
404 lines
21 KiB
C#
404 lines
21 KiB
C#
using System.Net;
|
||
using MeterVault.App;
|
||
using MeterVault.App.Analysis;
|
||
using MeterVault.App.Components.Shared.Analysis;
|
||
using MeterVault.App.Theme;
|
||
using MeterVault.Core.Analysis;
|
||
using MeterVault.Core.Analysis.Coverage;
|
||
using MeterVault.Infrastructure.Analysis;
|
||
using MeterVault.Infrastructure.Options;
|
||
using Microsoft.AspNetCore.Components;
|
||
using Microsoft.AspNetCore.Components.Web;
|
||
using Microsoft.Extensions.DependencyInjection;
|
||
using Microsoft.Extensions.Logging;
|
||
using Microsoft.Extensions.Logging.Abstractions;
|
||
using Microsoft.Extensions.Options;
|
||
using Microsoft.JSInterop;
|
||
using MudBlazor.Services;
|
||
using static MeterVault.Integration.Tests.Analysis.AnalysisUiTestData;
|
||
|
||
namespace MeterVault.Integration.Tests.Analysis;
|
||
|
||
/// <summary>
|
||
/// The shared analysis components rendered to HTML with the framework's static <see cref="HtmlRenderer"/> (no bUnit, no
|
||
/// browser): they render with real MudBlazor services, the title is the page's h1, the toolbar shows the effective
|
||
/// dates, the table says "—" and speaks statuses, the empty and error states offer their actions, attention items link
|
||
/// to their fix. Static rendering proves the markup, not the interactive chart or navigation — those are covered by the
|
||
/// pure model tests and the manual checklist.
|
||
/// </summary>
|
||
public sealed class AnalysisComponentRenderTests
|
||
{
|
||
[Fact]
|
||
public async Task The_page_header_is_the_pages_h1_with_breadcrumbs_carrying_the_period()
|
||
{
|
||
var ytd = AnalysisQuery.Default(AnalysisDefaults.History.ForScope(QueryScope.ForMeter(5))).WithPeriod(PeriodPreset.YearToDate);
|
||
RenderFragment crumbs = builder =>
|
||
{
|
||
builder.OpenComponent<AnalysisBreadcrumbs>(0);
|
||
builder.AddComponentParameter(1, nameof(AnalysisBreadcrumbs.Query), ytd);
|
||
builder.AddComponentParameter(2, nameof(AnalysisBreadcrumbs.EnergyTypeId), 1);
|
||
builder.AddComponentParameter(3, nameof(AnalysisBreadcrumbs.EnergyTypeName), "Strom");
|
||
builder.AddComponentParameter(4, nameof(AnalysisBreadcrumbs.MeterId), 5);
|
||
builder.AddComponentParameter(5, nameof(AnalysisBreadcrumbs.MeterName), "Wärmepumpe <Keller>");
|
||
builder.CloseComponent();
|
||
};
|
||
|
||
var raw = await RenderRawAsync<PageHeader>("en", new()
|
||
{
|
||
[nameof(PageHeader.Title)] = "Wärmepumpe <Keller>",
|
||
[nameof(PageHeader.Description)] = "Heat pump",
|
||
[nameof(PageHeader.Breadcrumbs)] = crumbs,
|
||
});
|
||
var html = WebUtility.HtmlDecode(raw);
|
||
|
||
// User data is text, never markup; the title is the page's one h1.
|
||
Assert.Contains("<Keller></h1>", raw, StringComparison.Ordinal);
|
||
Assert.Contains("<h1 class=\"mud-typography mud-typography-h4 mv-page-header__h1\">Wärmepumpe <Keller></h1>", html, StringComparison.Ordinal);
|
||
Assert.Contains("href=\"/?period=ytd\"", html, StringComparison.Ordinal);
|
||
Assert.Contains("href=\"/energy/1?period=ytd\"", html, StringComparison.Ordinal);
|
||
Assert.Contains("<nav aria-label=\"Breadcrumbs\"", html, StringComparison.Ordinal);
|
||
Assert.Contains("<span aria-current=\"page\">Wärmepumpe <Keller></span>", html, StringComparison.Ordinal);
|
||
Assert.Single(System.Text.RegularExpressions.Regex.Matches(html, "<h1"));
|
||
}
|
||
|
||
[Fact]
|
||
public async Task The_toolbar_shows_the_preset_with_its_effective_dates_and_a_refused_bucket()
|
||
{
|
||
var query = AnalysisQuery.Default(AnalysisDefaults.History).WithBucket(BucketSize.Day);
|
||
var period = query.WithPeriod(PeriodPreset.Last24Months).Resolve(Now, Berlin);
|
||
var plan = BucketPlanner.Plan(period, BucketSize.Day, maxPoints: AnalysisLimits.MaxPoints);
|
||
Assert.True(plan.Refused);
|
||
|
||
var html = await RenderAsync<PeriodToolbar>("de", new()
|
||
{
|
||
[nameof(PeriodToolbar.Query)] = query.WithPeriod(PeriodPreset.Last24Months),
|
||
[nameof(PeriodToolbar.Period)] = period,
|
||
[nameof(PeriodToolbar.Defaults)] = AnalysisDefaults.History,
|
||
[nameof(PeriodToolbar.Plan)] = plan,
|
||
[nameof(PeriodToolbar.ExportHref)] = "/export/analysis.csv?period=24m",
|
||
});
|
||
|
||
Assert.Contains("Letzte 24 Monate", html, StringComparison.Ordinal);
|
||
Assert.Contains(In("de", () => Format.PeriodRange(period)), html, StringComparison.Ordinal);
|
||
Assert.Contains("Das Intervall „Täglich“ bräuchte", html, StringComparison.Ordinal);
|
||
Assert.Contains("„Wöchentlich“ verwenden", html, StringComparison.Ordinal);
|
||
Assert.Contains("CSV exportieren", html, StringComparison.Ordinal);
|
||
Assert.Contains("Zurücksetzen", html, StringComparison.Ordinal);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task The_toolbar_shows_notices_for_what_the_address_got_wrong()
|
||
{
|
||
var query = AnalysisQuery.Parse("?period=fortnight&bucket=hourly", AnalysisDefaults.History);
|
||
|
||
var html = await RenderAsync<PeriodToolbar>("en", new() { [nameof(PeriodToolbar.Query)] = query });
|
||
|
||
Assert.Equal(2, query.Notices.Count);
|
||
foreach (var notice in query.Notices)
|
||
{
|
||
Assert.Contains(In("en", () => MeterVault.App.Localization.DisplayNames.Display(notice.Kind)), html, StringComparison.Ordinal);
|
||
}
|
||
|
||
Assert.DoesNotContain("fortnight", html, StringComparison.Ordinal);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task The_table_says_unknown_and_speaks_the_status()
|
||
{
|
||
var buckets = Buckets(D(2026, 1, 1), D(2026, 3, 31));
|
||
var series = In("en", () => AnalysisTableSeries.ForSeries(Series(1, "Haus", [Available(120), Missing(), Available(0)])));
|
||
|
||
var html = await RenderAsync<AnalysisTable>("en", new()
|
||
{
|
||
[nameof(AnalysisTable.Buckets)] = buckets,
|
||
[nameof(AnalysisTable.Series)] = new[] { series },
|
||
[nameof(AnalysisTable.DrillHref)] = (Func<AnalysisBucket, string?>)(b => "/drill/" + b.FirstDay.Month),
|
||
});
|
||
|
||
Assert.Contains("<th scope=\"row\" class=\"mv-row-label\">Feb</th>", html, StringComparison.Ordinal);
|
||
Assert.Contains(">0 kWh", html, StringComparison.Ordinal);
|
||
|
||
// A-42: the total above the rows, then March, February, January.
|
||
Assert.Equal(
|
||
["Total", "Mar", "Feb", "Jan"],
|
||
System.Text.RegularExpressions.Regex.Matches(html, "<th scope=\"row\" class=\"mv-row-label\">([^<]+)</th>")
|
||
.Select(m => m.Groups[1].Value));
|
||
Assert.Contains("mv-unknown\">—", html, StringComparison.Ordinal);
|
||
Assert.Contains("No data covers this period", html, StringComparison.Ordinal);
|
||
Assert.Contains("href=\"/drill/2\"", html, StringComparison.Ordinal);
|
||
Assert.Contains("aria-label=\"Details for Feb\"", html, StringComparison.Ordinal);
|
||
Assert.Contains("role=\"region\"", html, StringComparison.Ordinal);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task A_metric_card_shows_words_instead_of_a_fabricated_zero()
|
||
{
|
||
var unknown = await RenderAsync<MetricCard>("en", new()
|
||
{
|
||
[nameof(MetricCard.Title)] = "Consumption",
|
||
[nameof(MetricCard.Value)] = Missing(),
|
||
[nameof(MetricCard.Unit)] = "kWh",
|
||
});
|
||
Assert.Contains("No data", unknown, StringComparison.Ordinal);
|
||
Assert.DoesNotContain("0 kWh", unknown, StringComparison.Ordinal);
|
||
|
||
var partial = await RenderAsync<MetricCard>("en", new()
|
||
{
|
||
[nameof(MetricCard.Title)] = "Consumption",
|
||
[nameof(MetricCard.Value)] = Partial(12),
|
||
[nameof(MetricCard.Unit)] = "kWh",
|
||
[nameof(MetricCard.Change)] = Change.Between(12, 10),
|
||
[nameof(MetricCard.Polarity)] = ChangePolarity.HigherIsWorse,
|
||
[nameof(MetricCard.ChangeCaption)] = "vs. last year",
|
||
});
|
||
Assert.Contains("12 kWh", partial, StringComparison.Ordinal);
|
||
Assert.Contains("Partial", partial, StringComparison.Ordinal);
|
||
Assert.Contains("2.0 kWh more (+20.0 %)", partial, StringComparison.Ordinal);
|
||
Assert.Contains("mv-change-bad", partial, StringComparison.Ordinal);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task Empty_pending_and_error_states_offer_their_way_on()
|
||
{
|
||
var empty = await RenderAsync<EmptyPeriodState>("de", new()
|
||
{
|
||
[nameof(EmptyPeriodState.Availability)] = new AvailableRange(Now, Now, D(2020, 1, 1), D(2023, 3, 15)),
|
||
[nameof(EmptyPeriodState.LatestHref)] = "/meters/5?tab=analysis&from=2022-04-01&to=2023-03-31",
|
||
});
|
||
Assert.Contains("Keine Daten für diesen Zeitraum", empty, StringComparison.Ordinal);
|
||
Assert.Contains("Zu den neuesten Daten", empty, StringComparison.Ordinal);
|
||
Assert.Contains("from=2022-04-01", empty, StringComparison.Ordinal);
|
||
|
||
var nothing = await RenderAsync<EmptyPeriodState>("en", []);
|
||
Assert.Contains("No data yet", nothing, StringComparison.Ordinal);
|
||
Assert.DoesNotContain("Go to latest data", nothing, StringComparison.Ordinal);
|
||
|
||
var pending = await RenderAsync<PendingState>("en", []);
|
||
Assert.Contains("Analysis being prepared", pending, StringComparison.Ordinal);
|
||
|
||
var stale = await RenderAsync<PanelError>("en", new() { [nameof(PanelError.HasStaleValue)] = true });
|
||
Assert.Contains("The figures shown are from the previous selection.", stale, StringComparison.Ordinal);
|
||
|
||
var projection = await RenderAsync<ProjectionNote>("en", new() { [nameof(ProjectionNote.Days)] = 19, [nameof(ProjectionNote.ValueText)] = "320 kWh" });
|
||
Assert.Contains("Projection (straight-line from 19 days): ≈ 320 kWh", projection, StringComparison.Ordinal);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task A_value_status_names_the_reason_and_the_source_it_misses()
|
||
{
|
||
var value = new BucketValue(null, BucketStatus.Missing, Provenance.Derived, ValueIssue.MissingSource, null, [9, 5]);
|
||
|
||
var html = await RenderAsync<ValueStatus>("de", new()
|
||
{
|
||
[nameof(ValueStatus.Value)] = value,
|
||
[nameof(ValueStatus.Inline)] = true,
|
||
[nameof(ValueStatus.MeterName)] = (Func<int, string?>)(id => id == 5 ? "Solar 2" : null),
|
||
});
|
||
Assert.Contains("Keine Daten · Berechnet", html, StringComparison.Ordinal);
|
||
Assert.Contains("Einem Quellzähler fehlen hier Daten (Solar 2)", html, StringComparison.Ordinal);
|
||
|
||
// A plain complete value says nothing unless asked to.
|
||
Assert.Equal(string.Empty, (await RenderAsync<ValueStatus>("en", new() { [nameof(ValueStatus.Value)] = Available(3) })).Trim());
|
||
Assert.Contains("Complete · Measured", await RenderAsync<ValueStatus>("en", new()
|
||
{
|
||
[nameof(ValueStatus.Value)] = Available(3),
|
||
[nameof(ValueStatus.ShowWhenComplete)] = true,
|
||
}), StringComparison.Ordinal);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task A_comparison_summary_names_both_ranges_or_why_there_is_none()
|
||
{
|
||
var period = Range(D(2026, 1, 1), D(2026, 3, 31));
|
||
|
||
var shown = await RenderAsync<ComparisonSummary>("en", new()
|
||
{
|
||
[nameof(ComparisonSummary.Period)] = period,
|
||
[nameof(ComparisonSummary.Resolution)] = ComparisonResolver.Resolve(period, new ComparisonRequest(ComparisonKind.PreviousYear)),
|
||
[nameof(ComparisonSummary.Matched)] = MatchedCoverageResult.NotComparable,
|
||
});
|
||
Assert.Contains("Jan 1 – Mar 31, 2026 compared with Jan 1 – Mar 31, 2025", shown, StringComparison.Ordinal);
|
||
Assert.Contains("Not comparable", shown, StringComparison.Ordinal);
|
||
|
||
var all = await RenderAsync<ComparisonSummary>("en", new()
|
||
{
|
||
[nameof(ComparisonSummary.Period)] = period,
|
||
[nameof(ComparisonSummary.Resolution)] = new ComparisonResolution(ComparisonRequest.None, null, ComparisonUnavailableReason.AllHistory),
|
||
});
|
||
Assert.Contains("No comparison: All history has nothing before it to compare with.", all, StringComparison.Ordinal);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task Attention_items_render_with_their_actions_and_severity_words()
|
||
{
|
||
var html = await RenderAsync<AttentionList>("en", new()
|
||
{
|
||
[nameof(AttentionList.Problems)] = new[] { new AnalysisProblem(AnalysisProblemKind.StaleSource, 3) },
|
||
[nameof(AttentionList.Names)] = new AttentionNames(new Dictionary<int, string> { [3] = "Solar 1" }),
|
||
});
|
||
|
||
Assert.Contains("Needs attention", html, StringComparison.Ordinal);
|
||
Assert.Contains("Warning:", html, StringComparison.Ordinal);
|
||
Assert.Contains("Solar 1: the live source has stopped delivering.", html, StringComparison.Ordinal);
|
||
Assert.Contains("href=\"/meters/3?tab=sources\"", html, StringComparison.Ordinal);
|
||
|
||
Assert.Equal(string.Empty, (await RenderAsync<AttentionList>("en", [])).Trim());
|
||
}
|
||
|
||
[Fact]
|
||
public async Task Virtual_sources_show_the_formula_with_names_and_nested_sources_indented()
|
||
{
|
||
var nested = new SeriesContribution(7, "Solar 2", false, 1, [], [], Available(150), 150, [9, 8, 7], []);
|
||
var inner = new SeriesContribution(8, "Dach", true, 1, [], [], Available(150), 150, [9, 8], [nested]);
|
||
var first = new SeriesContribution(3, "Solar 1", false, 1, [], [], Available(100), 100, [9, 3], []);
|
||
var series = Series(9, "Summe Solar", [Available(250)], kind: QuantityKind.Generation) with
|
||
{
|
||
Basis = SeriesBasis.Virtual,
|
||
Contributions = [first, inner],
|
||
Virtual = new VirtualSeriesInfo(VirtualMeterStatus.Valid, "m3 + m8", Core.Analysis.Virtual.VirtualCostRule.SourceCosts, [3, 8], [3, 7], [], null, null),
|
||
};
|
||
|
||
var html = await RenderAsync<SeriesContributions>("de", new() { [nameof(SeriesContributions.Series)] = series });
|
||
|
||
Assert.Contains(">Solar 1</a>", html, StringComparison.Ordinal);
|
||
Assert.Contains("<code>m3</code>", html, StringComparison.Ordinal);
|
||
Assert.Contains("<code>m8</code>", html, StringComparison.Ordinal);
|
||
// The nested source is indented with an invariant CSS number, even for a German reader.
|
||
Assert.Contains("padding-left:2rem", html, StringComparison.Ordinal);
|
||
Assert.Contains("· berechnet", html, StringComparison.Ordinal);
|
||
Assert.Contains("href=\"/meters/7?tab=analysis\"", html, StringComparison.Ordinal);
|
||
Assert.Contains("Kosten: Summe der Kosten der Quellen", html, StringComparison.Ordinal);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task The_chart_renders_its_container_or_says_there_is_nothing()
|
||
{
|
||
var buckets = Buckets(D(2026, 1, 1), D(2026, 2, 28));
|
||
|
||
var none = await RenderAsync<AnalysisChart>("en", new()
|
||
{
|
||
[nameof(AnalysisChart.Buckets)] = buckets,
|
||
[nameof(AnalysisChart.Series)] = new[] { new AnalysisChartSeries("m1", "Haus", "kWh", [Missing(), Missing()]) },
|
||
});
|
||
Assert.Contains("No data in this range.", none, StringComparison.Ordinal);
|
||
|
||
var some = await RenderAsync<AnalysisChart>("en", new()
|
||
{
|
||
[nameof(AnalysisChart.Buckets)] = buckets,
|
||
[nameof(AnalysisChart.Series)] = new[] { new AnalysisChartSeries("m1", "Haus", "kWh", [Available(1), Partial(2)]) },
|
||
[nameof(AnalysisChart.Title)] = "Consumption of Haus",
|
||
});
|
||
Assert.Contains("aria-label=\"Chart: Consumption of Haus. The table lists the same values.\"", some, StringComparison.Ordinal);
|
||
Assert.Contains("* Partial, estimated or not fully priced", some, StringComparison.Ordinal);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task The_comparison_line_names_the_figure_it_is_about_when_a_page_shows_several()
|
||
{
|
||
// A-41: on one energy type page the tank's use shares no covered day with the year before while the burner
|
||
// beside it compares month by month. Unqualified, the line contradicts the card and the table under it.
|
||
var period = Range(D(2025, 10, 1), D(2026, 9, 30));
|
||
var resolution = ComparisonResolver.Resolve(period, new ComparisonRequest(ComparisonKind.PreviousYear), Now);
|
||
|
||
var alone = await RenderAsync<ComparisonSummary>("en", new()
|
||
{
|
||
[nameof(ComparisonSummary.Period)] = period,
|
||
[nameof(ComparisonSummary.Resolution)] = resolution,
|
||
[nameof(ComparisonSummary.Matched)] = MatchedCoverageResult.NotComparable,
|
||
});
|
||
Assert.Contains("Not comparable: the two periods share no covered days", alone, StringComparison.Ordinal);
|
||
|
||
var named = await RenderAsync<ComparisonSummary>("en", new()
|
||
{
|
||
[nameof(ComparisonSummary.Period)] = period,
|
||
[nameof(ComparisonSummary.Resolution)] = resolution,
|
||
[nameof(ComparisonSummary.Matched)] = MatchedCoverageResult.NotComparable,
|
||
[nameof(ComparisonSummary.Subject)] = "Total use",
|
||
});
|
||
Assert.Contains("Total use: not comparable — the two periods share no covered days", named, StringComparison.Ordinal);
|
||
Assert.DoesNotContain("Not comparable: the two", named, StringComparison.Ordinal);
|
||
|
||
var matched = await RenderAsync<ComparisonSummary>("de", new()
|
||
{
|
||
[nameof(ComparisonSummary.Period)] = period,
|
||
[nameof(ComparisonSummary.Resolution)] = resolution,
|
||
[nameof(ComparisonSummary.Matched)] = Matched(D(2025, 10, 1), D(2026, 5, 1)),
|
||
[nameof(ComparisonSummary.Subject)] = "Laufzeit",
|
||
});
|
||
Assert.Contains("Laufzeit: Veränderung über den gemeinsam abgedeckten Zeitraum – ", matched, StringComparison.Ordinal);
|
||
}
|
||
|
||
/// <summary>One matched stretch of the current period and the same stretch a year earlier.</summary>
|
||
private static MatchedCoverageResult Matched(DateOnly first, DateOnly last)
|
||
{
|
||
MatchedRange RangeOf(int years) => new(
|
||
new DateTimeOffset(first.AddYears(years).ToDateTime(TimeOnly.MinValue), TimeSpan.Zero),
|
||
new DateTimeOffset(last.AddYears(years).AddDays(1).ToDateTime(TimeOnly.MinValue), TimeSpan.Zero),
|
||
first.AddYears(years),
|
||
last.AddYears(years));
|
||
|
||
var piece = new MatchedPiece(RangeOf(0), RangeOf(-1));
|
||
return new MatchedCoverageResult(piece.Current, piece.Comparison, [piece]);
|
||
}
|
||
|
||
/// <summary>Renders a component and returns its HTML with entities decoded (the renderer encodes every non-ASCII letter).</summary>
|
||
private static async Task<string> RenderAsync<TComponent>(string culture, Dictionary<string, object?> parameters)
|
||
where TComponent : IComponent => WebUtility.HtmlDecode(await RenderRawAsync<TComponent>(culture, parameters));
|
||
|
||
private static async Task<string> RenderRawAsync<TComponent>(string culture, Dictionary<string, object?> parameters)
|
||
where TComponent : IComponent
|
||
{
|
||
await using var services = Services();
|
||
using var loggers = new NullLoggerFactory();
|
||
await using var renderer = new HtmlRenderer(services, loggers);
|
||
|
||
var previous = (System.Globalization.CultureInfo.CurrentCulture, System.Globalization.CultureInfo.CurrentUICulture);
|
||
try
|
||
{
|
||
System.Globalization.CultureInfo.CurrentCulture = System.Globalization.CultureInfo.GetCultureInfo(culture);
|
||
System.Globalization.CultureInfo.CurrentUICulture = System.Globalization.CultureInfo.GetCultureInfo(culture);
|
||
return await renderer.Dispatcher.InvokeAsync(async () =>
|
||
{
|
||
var output = await renderer.RenderComponentAsync<TComponent>(ParameterView.FromDictionary(parameters));
|
||
return output.ToHtmlString();
|
||
});
|
||
}
|
||
finally
|
||
{
|
||
System.Globalization.CultureInfo.CurrentCulture = previous.Item1;
|
||
System.Globalization.CultureInfo.CurrentUICulture = previous.Item2;
|
||
}
|
||
}
|
||
|
||
private static ServiceProvider Services()
|
||
{
|
||
var services = new ServiceCollection();
|
||
services.AddLogging();
|
||
services.AddMudServices();
|
||
services.AddSingleton<IJSRuntime, NoJs>();
|
||
services.AddSingleton<NavigationManager, TestNavigation>();
|
||
services.AddScoped<ThemeState>();
|
||
services.AddSingleton(Options.Create(new MeterVaultOptions()));
|
||
services.AddSingleton<InstanceCurrency>();
|
||
return services.BuildServiceProvider();
|
||
}
|
||
|
||
/// <summary>Static rendering runs no script; anything that asks gets nothing back.</summary>
|
||
private sealed class NoJs : IJSRuntime
|
||
{
|
||
public ValueTask<TValue> InvokeAsync<TValue>(string identifier, object?[]? args) => ValueTask.FromResult(default(TValue)!);
|
||
|
||
public ValueTask<TValue> InvokeAsync<TValue>(string identifier, CancellationToken cancellationToken, object?[]? args) =>
|
||
ValueTask.FromResult(default(TValue)!);
|
||
}
|
||
|
||
private sealed class TestNavigation : NavigationManager
|
||
{
|
||
public TestNavigation() => Initialize("http://localhost/", "http://localhost/meters/5?tab=analysis");
|
||
|
||
protected override void NavigateToCore(string uri, NavigationOptions options)
|
||
{
|
||
}
|
||
}
|
||
}
|