diff --git a/src/Aspire.Dashboard/Components/Pages/Login.razor.cs b/src/Aspire.Dashboard/Components/Pages/Login.razor.cs index 155d58409f6..5f2f7264ca3 100644 --- a/src/Aspire.Dashboard/Components/Pages/Login.razor.cs +++ b/src/Aspire.Dashboard/Components/Pages/Login.razor.cs @@ -38,6 +38,15 @@ public partial class Login : IAsyncDisposable protected override async Task OnInitializedAsync() { + // Create EditContext before awaiting. This is required to prevent an await in OnInitializedAsync + // triggering parameters being set on EditForm before EditContext is created. + // If that happens then EditForm errors that it requires an EditContext. + _formModel = new TokenFormModel(); + EditContext = new EditContext(_formModel); + _messageStore = new(EditContext); + EditContext.OnValidationRequested += (s, e) => _messageStore.Clear(); + EditContext.OnFieldChanged += (s, e) => _messageStore.Clear(e.FieldIdentifier); + // If the browser is already authenticated then redirect to the app. if (AuthenticationState is { } authStateTask) { @@ -48,12 +57,6 @@ protected override async Task OnInitializedAsync() return; } } - - _formModel = new TokenFormModel(); - EditContext = new EditContext(_formModel); - _messageStore = new(EditContext); - EditContext.OnValidationRequested += (s, e) => _messageStore.Clear(); - EditContext.OnFieldChanged += (s, e) => _messageStore.Clear(e.FieldIdentifier); } protected override async Task OnAfterRenderAsync(bool firstRender) diff --git a/tests/Aspire.Dashboard.Components.Tests/Pages/LoginTests.cs b/tests/Aspire.Dashboard.Components.Tests/Pages/LoginTests.cs new file mode 100644 index 00000000000..7ea32dd6a0c --- /dev/null +++ b/tests/Aspire.Dashboard.Components.Tests/Pages/LoginTests.cs @@ -0,0 +1,73 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Dashboard.Components.Tests.Shared; +using Aspire.Dashboard.Model; +using Bunit; +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Logging; +using Microsoft.FluentUI.AspNetCore.Components; +using Xunit; +using Xunit.Abstractions; + +namespace Aspire.Dashboard.Components.Tests.Pages; + +[UseCulture("en-US")] +public partial class LoginTests : TestContext +{ + private readonly ITestOutputHelper _testOutputHelper; + + public LoginTests(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + } + + [Fact] + public void Initialize_LongRunningAuthStateFunc_EditContextSet() + { + // Arrange + SetupLoginServices(); + + // This represents a long running auth state task. + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + // Act + var cut = RenderComponent(builder => + { + builder.Add(p => p.AuthenticationState, tcs.Task); + }); + + var instance = cut.Instance; + var logger = Services.GetRequiredService>(); + var loc = Services.GetRequiredService>(); + + cut.WaitForState(() => instance.EditContext != null); + } + + private void SetupLoginServices() + { + var version = typeof(FluentMain).Assembly.GetName().Version!; + + JSInterop.SetupModule("/Components/Pages/Login.razor.js"); + + JSInterop.SetupModule(GetFluentFile("./_content/Microsoft.FluentUI.AspNetCore.Components/Components/Anchor/FluentAnchor.razor.js", version)); + + var textboxModule = JSInterop.SetupModule(GetFluentFile("./_content/Microsoft.FluentUI.AspNetCore.Components/Components/TextField/FluentTextField.razor.js", version)); + textboxModule.SetupVoid("setControlAttribute", _ => true); + + var loggerFactory = IntegrationTestHelpers.CreateLoggerFactory(_testOutputHelper); + + Services.AddLocalization(); + Services.AddSingleton(loggerFactory); + Services.AddSingleton(new TestDashboardClient()); + Services.AddSingleton(); + Services.AddSingleton(); + } + + private static string GetFluentFile(string filePath, Version version) + { + return $"{filePath}?v={version}"; + } +}