feat(accessibility): add native keyboard dialogs and semantic account links (U08)

This commit is contained in:
2026-11-02 19:24:21 +01:00
parent 86a73b862a
commit 62e2b7dee8
23 changed files with 218 additions and 65 deletions
@@ -15,6 +15,7 @@ public class BookingFormTests
public void Form_ExplainsMissingFields_AndDoesNotRoundInvalidInput()
{
using var context = new BunitContext();
context.JSInterop.Mode = JSRuntimeMode.Loose;
context.Services.AddSingleton<ToastService>();
context.Services.AddSingleton<IEntryService>(new EntryService(null!));
var dialog = context.Render<AddEntryDialog>(p => p.Add(x => x.AccountName, "Barkasse"));
@@ -40,6 +41,7 @@ public class BookingFormTests
public void Edit_ShowsFixedTypeAndCorrectionPath_AndRerenderKeepsInput()
{
using var context = new BunitContext();
context.JSInterop.Mode = JSRuntimeMode.Loose;
context.Services.AddSingleton<ToastService>();
context.Services.AddSingleton<IEntryService>(new EntryService(null!));
var entry = new EntryDto(1, 1, "2026-001", EntryType.Expense, new(2026, 1, 1), "Porto", 4.20m, false, false, null, null, null);
@@ -0,0 +1,44 @@
using Bunit;
using Duempelkas.App.Components.Dialogs;
using Duempelkas.App.Services;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
namespace Duempelkas.Tests;
public class ModalDialogTests
{
[Fact]
public async Task EscapeRequest_ClosesCleanDialog_ButProtectsDirtyInput()
{
using var context = new BunitContext();
context.JSInterop.Mode = JSRuntimeMode.Loose;
context.Services.AddSingleton<ToastService>();
var cancelled = 0;
var modal = context.Render<ModalDialog>(p => p.Add(x => x.Title, "Testdialog").Add(x => x.OnCancel, () => cancelled++));
Assert.NotNull(modal.Find("dialog").GetAttribute("aria-labelledby"));
await modal.InvokeAsync(() => modal.Instance.RequestCloseAsync());
Assert.Equal(1, cancelled);
modal.Render(p => p.Add(x => x.Dirty, true));
await modal.InvokeAsync(() => modal.Instance.RequestCloseAsync());
Assert.Equal(1, cancelled);
Assert.Contains("Ungespeicherte Eingaben", modal.Markup);
modal.Find("[data-discard-continue]").Click();
Assert.Empty(modal.FindAll("[role=alertdialog]"));
await modal.InvokeAsync(() => modal.Instance.RequestCloseAsync());
modal.Find("button.btn-outline-danger").Click();
Assert.Equal(2, cancelled);
}
[Fact]
public async Task BusyDialog_CannotClose()
{
using var context = new BunitContext();
context.JSInterop.Mode = JSRuntimeMode.Loose;
context.Services.AddSingleton<ToastService>();
var cancelled = false;
var modal = context.Render<ModalDialog>(p => p.Add(x => x.Busy, true).Add(x => x.OnCancel, () => cancelled = true));
await modal.InvokeAsync(() => modal.Instance.RequestCloseAsync());
Assert.False(cancelled);
}
}