// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
using System.Web;
using Aspire.Dashboard.Components.Controls;
using Aspire.Dashboard.Components.Pages;
using Aspire.Dashboard.Components.Resize;
using Aspire.Dashboard.Components.Tests.Shared;
using Aspire.Dashboard.Extensions;
using Aspire.Dashboard.Model;
using Aspire.Dashboard.Otlp.Model;
using Aspire.Dashboard.Otlp.Model.MetricValues;
using Aspire.Dashboard.Otlp.Storage;
using Aspire.Dashboard.Tests.Shared;
using Aspire.Dashboard.Utils;
using Bunit;
using Google.Protobuf.Collections;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Time.Testing;
using Microsoft.FluentUI.AspNetCore.Components;
using OpenTelemetry.Proto.Metrics.V1;
using Aspire.Tests;
using Xunit;
using static Aspire.Dashboard.Components.Pages.Metrics;
using static Aspire.Tests.Shared.Telemetry.TelemetryTestHelpers;
namespace Aspire.Dashboard.Components.Tests.Pages;
[UseCulture("en-US")]
public partial class MetricsTests : DashboardTestContext
{
private static readonly DateTime s_testTime = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
[Theory]
[InlineData(false, true)]
[InlineData(true, false)]
public async Task SignalActions_UseTelemetryRepositoryReadOnlyState(bool telemetryRepositoryIsReadOnly, bool dashboardClientIsReadOnly)
{
MetricsSetupHelpers.SetupMetricsPage(this);
Services.AddSingleton<IDashboardClient>(new TestDashboardClient(isReadOnly: dashboardClientIsReadOnly));
await FluentUISetupHelpers.ConfigureTelemetryRepository(this, telemetryRepositoryIsReadOnly, _ => Task.CompletedTask);
var cut = RenderComponent<Metrics>(builder => builder.AddCascadingValue(new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false, IsUltraLowWidth: false)));
Assert.Equal(telemetryRepositoryIsReadOnly, cut.FindComponent<PauseIncomingDataSwitch>().Instance.Disabled);
Assert.Equal(telemetryRepositoryIsReadOnly, cut.FindComponent<ClearSignalsButton>().FindComponent<AspireMenuButton>().Instance.Disabled);
}
[Fact]
public async Task ChangeResource_MeterAndInstrumentOnNewResource_InstrumentSet()
{
await ChangeResourceAndAssertInstrument(
app1InstrumentName: "test1",
app2InstrumentName: "test1",
expectedMeterNameAfterChange: "test-meter",
expectedInstrumentNameAfterChange: "test1");
}
[Fact]
public async Task ChangeResource_MeterAndInstrumentNotOnNewResources_InstrumentCleared()
{
await ChangeResourceAndAssertInstrument(
app1InstrumentName: "test1",
app2InstrumentName: "test2",
expectedMeterNameAfterChange: null,
expectedInstrumentNameAfterChange: null);
}
[Fact]
public async Task ChartContainer_ParametersAndActiveView_OnlyRefreshAndRenderWhenChanged()
{
JSInterop.Mode = JSRuntimeMode.Loose;
MetricsSetupHelpers.SetupMetricsPage(this);
var chartTimeProvider = new TestTimeProvider { UtcNow = DateTimeOffset.UtcNow };
Services.AddSingleton<BrowserTimeProvider>(chartTimeProvider);
var telemetryRepository = Services.GetRequiredService<SqliteTelemetryRepository>();
var metricTime = chartTimeProvider.GetUtcNow().UtcDateTime.AddMinutes(-1);
await telemetryRepository.AddMetricsAsync(new AddContext(), new RepeatedField<ResourceMetrics>
{
new ResourceMetrics
{
Resource = CreateResource(name: "TestApp"),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter"),
Metrics =
{
CreateSumMetric(
metricName: "test-instrument",
startTime: metricTime,
attributes: [new KeyValuePair<string, string>("http.method", "GET")])
}
}
}
}
});
var resource = telemetryRepository.GetResources().Single();
var cut = RenderComponent<ChartContainer>(builder =>
{
builder.Add(component => component.ResourceKey, resource.ResourceKey);
builder.Add(component => component.MeterName, "test-meter");
builder.Add(component => component.InstrumentName, "test-instrument");
builder.Add(component => component.Duration, TimeSpan.FromMinutes(5));
builder.Add(component => component.ActiveView, MetricViewKind.Graph);
builder.Add(component => component.OnViewChangedAsync, _ => Task.CompletedTask);
builder.Add(component => component.Resources, [resource]);
});
cut.WaitForAssertion(() =>
{
var dimensionFilter = Assert.Single(cut.Instance.DimensionFilters);
Assert.Equal("GET", Assert.Single(dimensionFilter.SelectedValues).Value);
Assert.Single(cut.FindComponents<PlotlyChart>());
Assert.Empty(cut.FindComponents<MetricTable>());
});
var dimensionFilters = cut.Instance.DimensionFilters;
var activities = new ConcurrentQueue<Activity>();
using var listener = ActivityListenerHelper.Create(telemetryRepository.SqlActivitySource, onActivityStopped: activities.Enqueue);
cut.SetParametersAndRender(builder => builder.Add(component => component.ActiveView, MetricViewKind.Table));
Assert.Empty(cut.FindComponents<PlotlyChart>());
Assert.Single(cut.FindComponents<MetricTable>());
if (cut.FindAll(".empty-content-cell") is [var emptyContentCell])
{
Assert.Equal("Loading...", emptyContentCell.TextContent.Trim());
}
cut.WaitForAssertion(() => Assert.Contains("1Value increased", cut.FindAll("[role='gridcell']").Select(cell => cell.TextContent.Trim())));
Assert.Empty(activities);
cut.SetParametersAndRender(builder => builder.Add(component => component.ActiveView, MetricViewKind.Graph));
Assert.Single(cut.FindComponents<PlotlyChart>());
Assert.Empty(cut.FindComponents<MetricTable>());
Assert.Empty(activities);
await telemetryRepository.AddMetricsAsync(new AddContext(), new RepeatedField<ResourceMetrics>
{
new ResourceMetrics
{
Resource = CreateResource(name: "TestApp"),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter"),
Metrics =
{
CreateSumMetric(
metricName: "test-instrument",
startTime: metricTime,
attributes: [new KeyValuePair<string, string>("http.method", "POST")])
}
}
}
}
});
activities.Clear();
cut.SetParametersAndRender(builder => builder.Add(component => component.Duration, TimeSpan.FromMinutes(5)));
Assert.Empty(activities);
cut.SetParametersAndRender(builder => builder.Add(component => component.Duration, TimeSpan.FromMinutes(1)));
cut.WaitForAssertion(() =>
{
var updatedFilter = Assert.Single(cut.Instance.DimensionFilters);
Assert.NotSame(dimensionFilters, cut.Instance.DimensionFilters);
Assert.Collection(
updatedFilter.SelectedValues.Select(value => value.Value).Order(),
value => Assert.Equal("GET", value),
value => Assert.Equal("POST", value));
var chart = cut.FindComponent<PlotlyChart>();
var dimensions = chart.Instance.InstrumentViewModel.MatchedDimensions!;
Assert.Equal(2, dimensions.Count);
Assert.All(dimensions, dimension => Assert.Equal(1, Assert.IsType<MetricValue<long>>(Assert.Single(dimension.Values)).Value));
Assert.Contains(activities, activity => ((string?)activity.GetTagItem("db.query.text"))?.Contains("telemetry_metric_points", StringComparison.Ordinal) == true);
});
}
[Fact]
public async Task ChartContainer_InstrumentChanged_TableUpdates()
{
JSInterop.Mode = JSRuntimeMode.Loose;
MetricsSetupHelpers.SetupMetricsPage(this);
await FluentUISetupHelpers.ConfigureTelemetryRepository(this, readOnly: true, telemetryRepository => telemetryRepository.AddMetricsAsync(new AddContext(), new RepeatedField<ResourceMetrics>
{
new ResourceMetrics
{
Resource = CreateResource(name: "TestApp"),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter"),
Metrics =
{
CreateSumMetric(metricName: "instrument-1", startTime: s_testTime.AddMinutes(1), value: 0),
CreateSumMetric(metricName: "instrument-1", startTime: s_testTime.AddMinutes(2), value: 1),
CreateSumMetric(metricName: "instrument-2", startTime: s_testTime.AddMinutes(1), value: 0),
CreateSumMetric(metricName: "instrument-2", startTime: s_testTime.AddMinutes(2), value: 2)
}
}
}
}
}));
var telemetryRepository = Services.GetRequiredService<SqliteTelemetryRepository>();
var resource = telemetryRepository.GetResources().Single();
var cut = RenderComponent<ChartContainer>(builder =>
{
builder.Add(component => component.ResourceKey, resource.ResourceKey);
builder.Add(component => component.MeterName, "test-meter");
builder.Add(component => component.InstrumentName, "instrument-1");
builder.Add(component => component.Duration, TimeSpan.FromMinutes(5));
builder.Add(component => component.ActiveView, MetricViewKind.Table);
builder.Add(component => component.OnViewChangedAsync, _ => Task.CompletedTask);
builder.Add(component => component.Resources, [resource]);
});
cut.WaitForState(() => cut.FindComponents<MetricTable>().Count == 1);
var table = cut.FindComponent<MetricTable>();
table.Render();
table.WaitForAssertion(() => Assert.Contains("1Value increased", table.FindAll("[role='gridcell']").Select(cell => cell.TextContent.Trim())));
cut.SetParametersAndRender(builder => builder.Add(component => component.InstrumentName, "instrument-2"));
cut.WaitForAssertion(() =>
{
var updatedTable = cut.FindComponent<MetricTable>();
Assert.Equal("instrument-2", updatedTable.Instance.InstrumentViewModel.Instrument?.Name);
Assert.NotEmpty(updatedTable.Instance.InstrumentViewModel.MatchedDimensions!);
});
cut.WaitForAssertion(() => Assert.Contains("2Value increased", cut.FindAll("[role='gridcell']").Select(cell => cell.TextContent.Trim())));
}
[Fact]
public async Task ChartContainer_TickUpdate_PreservesComponentCultureAndStopsOnDispose()
{
JSInterop.Mode = JSRuntimeMode.Loose;
MetricsSetupHelpers.SetupMetricsPage(this);
var timeProvider = new SignalingFakeTimeProvider();
Services.AddSingleton<TimeProvider>(timeProvider);
var telemetryRepository = Services.GetRequiredService<SqliteTelemetryRepository>();
var metric = CreateSumMetric(metricName: "test-instrument", startTime: DateTime.UtcNow);
// End in the current SQLite query window while spanning the fixed browser time used by the rendered chart.
metric.Sum.DataPoints.Single().StartTimeUnixNano = 0;
await telemetryRepository.AddMetricsAsync(new AddContext(), new RepeatedField<ResourceMetrics>
{
new ResourceMetrics
{
Resource = CreateResource(name: "TestApp"),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter"),
Metrics =
{
metric
}
}
}
}
});
var resource = telemetryRepository.GetResources().Single();
var activitySource = Services.GetRequiredService<DashboardActivitySource>();
var tickActivityCount = 0;
var activityStarted = new TaskCompletionSource<Activity>(TaskCreationOptions.RunContinuationsAsynchronously);
var activityStopped = new TaskCompletionSource<Activity>(TaskCreationOptions.RunContinuationsAsynchronously);
using var listener = ActivityListenerHelper.Create(
activitySource.ActivitySource,
onActivityStarted: activity =>
{
if (activity.OperationName == "Update metric chart data from tick")
{
Interlocked.Increment(ref tickActivityCount);
activityStarted.TrySetResult(activity);
}
},
onActivityStopped: activity =>
{
if (activity.OperationName == "Update metric chart data from tick")
{
activityStopped.TrySetResult(activity);
}
});
var componentCulture = (CultureInfo)CultureInfo.CurrentCulture.Clone();
componentCulture.DateTimeFormat.AMDesignator = "am";
var originalCulture = CultureInfo.CurrentCulture;
try
{
CultureInfo.CurrentCulture = componentCulture;
var cut = RenderComponent<ChartContainer>(builder =>
{
builder.Add(component => component.ResourceKey, resource.ResourceKey);
builder.Add(component => component.MeterName, "test-meter");
builder.Add(component => component.InstrumentName, "test-instrument");
builder.Add(component => component.Duration, TimeSpan.FromSeconds(1));
builder.Add(component => component.ActiveView, MetricViewKind.Table);
builder.Add(component => component.OnViewChangedAsync, _ => Task.CompletedTask);
builder.Add(component => component.Resources, [resource]);
});
cut.WaitForAssertion(() => Assert.Single(cut.FindComponents<MetricTable>()));
await timeProvider.TimerCreated.Task.DefaultTimeout();
timeProvider.Advance(TimeSpan.FromSeconds(1));
var started = await activityStarted.Task.WaitAsync(DefaultWaitTimeout);
var activity = await activityStopped.Task.WaitAsync(DefaultWaitTimeout);
Assert.Same(started, activity);
Assert.Equal(ActivityKind.Internal, activity.Kind);
Assert.Null(activity.ParentId);
cut.WaitForAssertion(() =>
{
Assert.Equal("12:59:57 am", cut.Find("[role='gridcell']").TextContent.Trim(), ignoreWhiteSpaceDifferences: true);
});
await cut.Instance.DisposeAsync().DefaultTimeout();
var activityCountAfterDispose = Volatile.Read(ref tickActivityCount);
timeProvider.Advance(TimeSpan.FromSeconds(10));
Assert.Equal(activityCountAfterDispose, Volatile.Read(ref tickActivityCount));
}
finally
{
CultureInfo.CurrentCulture = originalCulture;
}
}
[Fact]
public async Task InitialLoad_SingleResource_RedirectToResource()
{
// Arrange
MetricsSetupHelpers.SetupMetricsPage(this);
var navigationManager = Services.GetRequiredService<NavigationManager>();
var targetLocationTcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
using var changeHandler = navigationManager.RegisterLocationChangingHandler(c =>
{
targetLocationTcs.SetResult(c.TargetLocation);
return ValueTask.CompletedTask;
});
var telemetryRepository = Services.GetRequiredService<SqliteTelemetryRepository>();
await telemetryRepository.AddMetricsAsync(new AddContext(), new RepeatedField<ResourceMetrics>
{
new ResourceMetrics
{
Resource = CreateResource(name: "TestApp"),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter"),
Metrics =
{
CreateSumMetric(metricName: "test-instrument", startTime: s_testTime.AddMinutes(1))
}
}
}
}
});
// Act
var cut = RenderComponent<Metrics>(builder =>
{
builder.AddCascadingValue(new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false, IsUltraLowWidth: false));
});
// Assert
Assert.NotNull(targetLocationTcs);
Assert.Equal("/metrics/resource/TestApp?duration=5", await targetLocationTcs.Task.DefaultTimeout());
}
[Fact]
public async Task InitialLoad_HasSessionState_RedirectUsingState()
{
// Arrange
var testSessionStorage = new TestSessionStorage
{
OnGetAsync = key =>
{
if (key == BrowserStorageKeys.MetricsPageState)
{
var state = new MetricsPageState
{
ResourceName = "TestApp2",
MeterName = "test-meter",
InstrumentName = "test-instrument",
DurationMinutes = 720,
ViewKind = MetricViewKind.Table.ToString()
};
return (true, state);
}
else
{
throw new InvalidOperationException("Unexpected key: " + key);
}
}
};
MetricsSetupHelpers.SetupMetricsPage(this, sessionStorage: testSessionStorage);
var navigationManager = Services.GetRequiredService<NavigationManager>();
navigationManager.NavigateTo(DashboardUrls.MetricsUrl());
Uri? loadRedirect = null;
navigationManager.LocationChanged += (s, a) =>
{
loadRedirect = new Uri(a.Location);
};
var telemetryRepository = Services.GetRequiredService<SqliteTelemetryRepository>();
await telemetryRepository.AddMetricsAsync(new AddContext(), new RepeatedField<ResourceMetrics>
{
new ResourceMetrics
{
Resource = CreateResource(name: "TestApp"),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter"),
Metrics =
{
CreateSumMetric(metricName: "test-instrument", startTime: s_testTime.AddMinutes(1))
}
}
}
},
new ResourceMetrics
{
Resource = CreateResource(name: "TestApp2"),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter"),
Metrics =
{
CreateSumMetric(metricName: "test-instrument", startTime: s_testTime.AddMinutes(1))
}
}
}
}
});
// Act
var cut = RenderComponent<Metrics>(builder =>
{
builder.AddCascadingValue(new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false, IsUltraLowWidth: false));
});
// Assert
Assert.NotNull(loadRedirect);
Assert.Equal("/metrics/resource/TestApp2", loadRedirect.AbsolutePath);
var query = HttpUtility.ParseQueryString(loadRedirect.Query);
Assert.Equal("test-meter", query["meter"]);
Assert.Equal("test-instrument", query["instrument"]);
Assert.Equal("720", query["duration"]);
Assert.Equal(MetricViewKind.Table.ToString(), query["view"]);
}
[Fact]
public async Task PauseIncomingData_DisplaysPauseWarningInPageFooter()
{
MetricsSetupHelpers.SetupMetricsPage(this);
var telemetryRepository = Services.GetRequiredService<SqliteTelemetryRepository>();
await telemetryRepository.AddMetricsAsync(new AddContext(), new RepeatedField<ResourceMetrics>
{
new ResourceMetrics
{
Resource = CreateResource(name: "TestApp"),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter"),
Metrics =
{
CreateSumMetric(metricName: "test-instrument", startTime: s_testTime.AddMinutes(1))
}
}
}
}
});
var navigationManager = Services.GetRequiredService<NavigationManager>();
navigationManager.NavigateTo(DashboardUrls.MetricsUrl(resource: "TestApp", meter: "test-meter", instrument: "test-instrument"));
var viewport = new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false, IsUltraLowWidth: false);
var cut = RenderComponent<Metrics>(builder =>
{
builder.Add(m => m.ResourceName, "TestApp");
builder.AddCascadingValue(viewport);
});
cut.WaitForState(() => cut.Instance.PageViewModel.SelectedInstrument is not null);
var pauseManager = Services.GetRequiredService<PauseManager>();
var timeProvider = Services.GetRequiredService<BrowserTimeProvider>();
var loc = Services.GetRequiredService<IStringLocalizer<Resources.Metrics>>();
cut.FindComponent<PauseIncomingDataSwitch>().WaitForElement("fluent-button").Click();
cut.WaitForAssertion(() =>
{
Assert.True(pauseManager.AreMetricsPaused(out var pausedAt));
var pauseWarning = cut.FindComponent<PauseWarning>();
Assert.Equal(
string.Format(
CultureInfo.CurrentCulture,
loc[nameof(Resources.Metrics.PauseInProgressText)],
FormatHelpers.FormatTimeWithOptionalDate(timeProvider, pausedAt.Value)),
pauseWarning.Instance.PauseText);
Assert.Single(cut.Find("footer").QuerySelectorAll(".block-warning"));
});
cut.FindComponent<PauseIncomingDataSwitch>().WaitForElement("fluent-button").Click();
cut.WaitForAssertion(() =>
{
Assert.False(pauseManager.AreMetricsPaused(out _));
Assert.False(cut.HasComponent<PauseWarning>());
});
}
[Fact]
public async Task MetricsTree_MetricsAdded_TreeUpdated()
{
// Arrange
MetricsSetupHelpers.SetupMetricsPage(this);
var telemetryRepository = Services.GetRequiredService<SqliteTelemetryRepository>();
await telemetryRepository.AddMetricsAsync(new AddContext(), new RepeatedField<ResourceMetrics>
{
new ResourceMetrics
{
Resource = CreateResource(name: "TestApp"),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter1"),
Metrics =
{
CreateSumMetric(metricName: "test-instrument1-1", startTime: s_testTime.AddMinutes(1))
}
}
}
}
});
// Act 1
// Initial page load
var cut = RenderComponent<Metrics>(builder =>
{
builder.AddCascadingValue(new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false, IsUltraLowWidth: false));
builder.Add(m => m.ResourceName, "TestApp");
});
// Assert 2
cut.WaitForState(() => cut.Instance.PageViewModel.Instruments?.Count == 1);
cut.WaitForAssertion(() =>
{
// Assert in wait to make sure rendering has caught up to data update.
var tree1 = cut.FindComponent<FluentTreeView>();
var items1 = tree1.FindComponents<FluentTreeItem>();
foreach (var instrument in cut.Instance.PageViewModel.Instruments!)
{
Assert.Single(items1, i => i.Instance.Data as OtlpInstrumentSummary == instrument);
Assert.Single(items1, i => i.Instance.Data as string == instrument.Parent.Name);
}
});
// Act 2
// New instruments added
await telemetryRepository.AddMetricsAsync(new AddContext(), new RepeatedField<ResourceMetrics>
{
new ResourceMetrics
{
Resource = CreateResource(name: "TestApp"),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter1"),
Metrics =
{
CreateSumMetric(metricName: "test-instrument1-2", startTime: s_testTime.AddMinutes(1))
}
},
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter2"),
Metrics =
{
CreateSumMetric(metricName: "test-instrument2-1", startTime: s_testTime.AddMinutes(1))
}
}
}
}
});
// Assert 2
cut.WaitForState(() => cut.Instance.PageViewModel.Instruments?.Count == 3);
cut.WaitForAssertion(() =>
{
// Assert in wait to make sure rendering has caught up to data update.
var tree2 = cut.FindComponent<FluentTreeView>();
var items2 = tree2.FindComponents<FluentTreeItem>();
foreach (var instrument in cut.Instance.PageViewModel.Instruments!)
{
Assert.Single(items2, i => i.Instance.Data as OtlpInstrumentSummary == instrument);
Assert.Single(items2, i => i.Instance.Data as string == instrument.Parent.Name);
}
});
}
[Fact]
public async Task ReadOnly_ChartEndsAtLatestMetricTime()
{
MetricsSetupHelpers.SetupMetricsPage(this);
await FluentUISetupHelpers.ConfigureTelemetryRepository(this, readOnly: true, telemetryRepository => telemetryRepository.AddMetricsAsync(new AddContext(), new RepeatedField<ResourceMetrics>
{
new ResourceMetrics
{
Resource = CreateResource(name: "TestApp"),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter"),
Metrics =
{
CreateSumMetric(metricName: "test-instrument", startTime: s_testTime.AddMinutes(1)),
CreateSumMetric(metricName: "test-instrument", startTime: s_testTime.AddMinutes(2))
}
}
}
}
}));
Services.GetRequiredService<PauseManager>().SetMetricsPaused(true);
Services.GetRequiredService<NavigationManager>().NavigateTo(
DashboardUrls.MetricsUrl(resource: "TestApp", meter: "test-meter", instrument: "test-instrument", duration: 5, view: MetricViewKind.Graph.ToString()));
var cut = RenderComponent<Metrics>(builder =>
{
builder.Add(m => m.ResourceName, "TestApp");
builder.AddCascadingValue(new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false, IsUltraLowWidth: false));
});
cut.WaitForAssertion(() => Assert.Single(cut.FindComponents<PlotlyChart>()));
var chart = cut.FindComponent<PlotlyChart>().Instance;
var expectedEndTime = new DateTimeOffset(s_testTime.AddMinutes(2));
Assert.Equal(expectedEndTime, chart.DataEndTime);
cut.WaitForAssertion(() =>
{
var initializeChart = Assert.Single(JSInterop.Invocations, invocation => invocation.Identifier == "initializeChart");
Assert.Equal(chart.TimeProvider.ToLocal(expectedEndTime), Assert.IsType<DateTime>(initializeChart.Arguments[3]));
});
}
private async Task ChangeResourceAndAssertInstrument(string app1InstrumentName, string app2InstrumentName, string? expectedMeterNameAfterChange, string? expectedInstrumentNameAfterChange)
{
// Arrange
MetricsSetupHelpers.SetupMetricsPage(this);
var navigationManager = Services.GetRequiredService<NavigationManager>();
navigationManager.NavigateTo(DashboardUrls.MetricsUrl(resource: "TestApp", meter: "test-meter", instrument: app1InstrumentName, duration: 720, view: MetricViewKind.Table.ToString()));
var telemetryRepository = Services.GetRequiredService<SqliteTelemetryRepository>();
await telemetryRepository.AddMetricsAsync(new AddContext(), new RepeatedField<ResourceMetrics>
{
new ResourceMetrics
{
Resource = CreateResource(name: "TestApp"),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter"),
Metrics =
{
CreateSumMetric(metricName: app1InstrumentName, startTime: s_testTime.AddMinutes(1))
}
}
}
},
new ResourceMetrics
{
Resource = CreateResource(name: "TestApp2"),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter"),
Metrics =
{
CreateSumMetric(metricName: app2InstrumentName, startTime: s_testTime.AddMinutes(1))
}
}
}
}
});
// Act 1
var cut = RenderComponent<Metrics>(builder =>
{
builder.Add(m => m.ResourceName, "TestApp");
builder.AddCascadingValue(new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false, IsUltraLowWidth: false));
});
navigationManager.LocationChanged += (sender, e) =>
{
var expectedUrl = DashboardUrls.MetricsUrl(resource: "TestApp2", meter: expectedMeterNameAfterChange, instrument: expectedInstrumentNameAfterChange, duration: 720, view: "Table");
Assert.EndsWith(expectedUrl, e.Location);
cut.SetParametersAndRender(builder =>
{
builder.Add(m => m.ResourceName, "TestApp2");
});
};
var viewModel = cut.Instance.PageViewModel;
// Assert 1
Assert.Equal("test-meter", viewModel.SelectedMeter);
Assert.Equal(app1InstrumentName, viewModel.SelectedInstrument!.Name);
// Act 2
var resourceSelect = cut.FindComponent<ResourceSelect>();
var innerSelect = resourceSelect.Find("fluent-select");
innerSelect.Change("TestApp2");
cut.WaitForAssertion(() => Assert.Equal("TestApp2", viewModel.SelectedResource.Name));
Assert.Equal(expectedInstrumentNameAfterChange, viewModel.SelectedInstrument?.Name);
Assert.Equal(expectedMeterNameAfterChange, viewModel.SelectedMeter);
Assert.Equal(MetricViewKind.Table, viewModel.SelectedViewKind);
Assert.Equal(TimeSpan.FromMinutes(720), viewModel.SelectedDuration.Id);
}
private sealed class SignalingFakeTimeProvider : FakeTimeProvider
{
public TaskCompletionSource TimerCreated { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
public override ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period)
{
var timer = base.CreateTimer(callback, state, dueTime, period);
TimerCreated.TrySetResult();
return timer;
}
}
}