diff --git a/COMET.Web.Common.Tests/Components/LoginTestFixture.cs b/COMET.Web.Common.Tests/Components/LoginTestFixture.cs index 2f96cec9..7e195a70 100644 --- a/COMET.Web.Common.Tests/Components/LoginTestFixture.cs +++ b/COMET.Web.Common.Tests/Components/LoginTestFixture.cs @@ -37,6 +37,7 @@ namespace COMET.Web.Common.Tests.Components using COMET.Web.Common.ViewModels.Components; using Microsoft.AspNetCore.Components.Forms; + using Microsoft.AspNetCore.Components.Web; using Microsoft.Extensions.DependencyInjection; using Moq; @@ -71,6 +72,71 @@ public void Teardown() this.context.CleanContext(); } + [Test] + public async Task VerifyErrorsShown() + { + var renderer = this.context.RenderComponent(); + var errorsElement = renderer.Find(".validation-errors"); + var numberOfRequiredFieldsInFirstLoginTry = renderer.Instance.FieldsFocusedStatus.Count - 1; + + Assert.That(errorsElement.InnerHtml, Is.Empty); + + await renderer.Find("button").ClickAsync(new MouseEventArgs()); + Assert.That(errorsElement.ChildElementCount, Is.EqualTo(numberOfRequiredFieldsInFirstLoginTry)); + + // Username input field + await renderer.Find("input").FocusAsync(new FocusEventArgs()); + + Assert.Multiple(() => + { + Assert.That(renderer.Instance.FieldsFocusedStatus["UserName"], Is.True); + Assert.That(errorsElement.ChildElementCount, Is.EqualTo(numberOfRequiredFieldsInFirstLoginTry - 1)); + }); + + await renderer.Find("input").BlurAsync(new FocusEventArgs()); + + Assert.Multiple(() => + { + Assert.That(renderer.Instance.FieldsFocusedStatus["UserName"], Is.False); + Assert.That(errorsElement.ChildElementCount, Is.EqualTo(numberOfRequiredFieldsInFirstLoginTry)); + }); + } + + [Test] + public void VerifyFocusingAndBluring() + { + var renderer = this.context.RenderComponent(); + + Assert.That(renderer.Instance.FieldsFocusedStatus, Is.EqualTo(new Dictionary() + { + { "SourceAddress", false }, + { "UserName", false }, + { "Password", false } + })); + + const string fieldToFocusOn = "UserName"; + Assert.That(renderer.Instance.FieldsFocusedStatus[fieldToFocusOn], Is.False); + renderer.Instance.HandleFieldFocus(fieldToFocusOn); + + Assert.Multiple(()=> + { + foreach (var fieldStatus in renderer.Instance.FieldsFocusedStatus) + { + Assert.That(fieldStatus.Value, fieldStatus.Key == fieldToFocusOn ? Is.True : Is.False); + } + }); + + renderer.Instance.HandleFieldBlur(fieldToFocusOn); + + Assert.Multiple(() => + { + foreach (var fieldStatus in renderer.Instance.FieldsFocusedStatus) + { + Assert.That(fieldStatus.Value, Is.False); + } + }); + } + [Test] public async Task VerifyPerformLogin() { @@ -80,6 +146,13 @@ public async Task VerifyPerformLogin() this.authenticationService.Setup(x => x.Login(It.IsAny())) .ReturnsAsync(AuthenticationStateKind.ServerFail); + Assert.That(renderer.Instance.FieldsFocusedStatus, Is.EqualTo(new Dictionary() + { + { "SourceAddress", false }, + { "UserName", false }, + { "Password", false } + })); + await renderer.InvokeAsync(editForm.Instance.OnValidSubmit.InvokeAsync); Assert.Multiple(() => diff --git a/COMET.Web.Common/Components/Login.razor b/COMET.Web.Common/Components/Login.razor index 97588457..377bd1e6 100644 --- a/COMET.Web.Common/Components/Login.razor +++ b/COMET.Web.Common/Components/Login.razor @@ -24,35 +24,68 @@ - - @if (string.IsNullOrEmpty(this.ViewModel.serverConnectionService.ServerConfiguration.ServerAddress)) - { - - - - } - - - - - - - - + + @if (string.IsNullOrEmpty(this.ViewModel.serverConnectionService.ServerConfiguration.ServerAddress)) + { + + + + } + + + + + + + + +
    + @foreach (var fieldFocusedStatus in this.FieldsFocusedStatus) + { + if (fieldFocusedStatus.Value) + { + continue; + } + + @if (fieldFocusedStatus.Key == "SourceAddress" && !string.IsNullOrEmpty(editFormContext.GetValidationMessages(() => this.ViewModel.AuthenticationDto.SourceAddress).FirstOrDefault())) + { +
  • + } + + @if (fieldFocusedStatus.Key == "UserName" && !string.IsNullOrEmpty(editFormContext.GetValidationMessages(() => this.ViewModel.AuthenticationDto.UserName).FirstOrDefault())) + { +
  • + } + + @if (fieldFocusedStatus.Key == "Password" && !string.IsNullOrEmpty(editFormContext.GetValidationMessages(() => this.ViewModel.AuthenticationDto.Password).FirstOrDefault())) + { +
  • + } + } +
@if (!string.IsNullOrEmpty(this.ErrorMessage)) { diff --git a/COMET.Web.Common/Components/Login.razor.cs b/COMET.Web.Common/Components/Login.razor.cs index 2520c56c..dc402af2 100644 --- a/COMET.Web.Common/Components/Login.razor.cs +++ b/COMET.Web.Common/Components/Login.razor.cs @@ -29,6 +29,7 @@ namespace COMET.Web.Common.Components using COMET.Web.Common.ViewModels.Components; using Microsoft.AspNetCore.Components; + using Microsoft.AspNetCore.Components.Web; using ReactiveUI; @@ -64,6 +65,11 @@ public partial class Login /// public bool LoginEnabled { get; set; } = true; + /// + /// The dictionary of focus status from the form fields + /// + public Dictionary FieldsFocusedStatus { get; private set; } + /// /// Method invoked when the component is ready to start, having received its /// initial parameters from its parent in the render tree. @@ -72,6 +78,13 @@ protected override void OnInitialized() { base.OnInitialized(); + this.FieldsFocusedStatus = new Dictionary() + { + { "SourceAddress", false }, + { "UserName", false }, + { "Password", false } + }; + this.Disposables.Add(this.WhenAnyValue(x => x.ViewModel.AuthenticationState) .Subscribe(_ => this.ComputeDisplayProperties())); } @@ -120,5 +133,23 @@ private async Task ExecuteLogin() await this.ViewModel.ExecuteLogin(); this.LoginEnabled = true; } + + /// + /// Handles the focus event of the given fieldName + /// + /// Form field name, as indexed in + public void HandleFieldFocus(string fieldName) + { + this.FieldsFocusedStatus[fieldName] = true; // Set the field as focused + } + + /// + /// Handles the blur event of the given fieldName + /// + /// Form field name, as indexed in + public void HandleFieldBlur(string fieldName) + { + this.FieldsFocusedStatus[fieldName] = false; // Set the field as not focused when it loses focus + } } }