feat(ui): constrain decimal input and add stacked timed toasts with uniform actions

This commit is contained in:
2026-11-02 19:19:07 +01:00
parent 22e509d982
commit 86a73b862a
24 changed files with 315 additions and 103 deletions
+3 -1
View File
@@ -15,6 +15,7 @@ public class BookingFormTests
public void Form_ExplainsMissingFields_AndDoesNotRoundInvalidInput()
{
using var context = new BunitContext();
context.Services.AddSingleton<ToastService>();
context.Services.AddSingleton<IEntryService>(new EntryService(null!));
var dialog = context.Render<AddEntryDialog>(p => p.Add(x => x.AccountName, "Barkasse"));
Assert.Contains("Barkasse", dialog.Find("h5").TextContent);
@@ -22,7 +23,7 @@ public class BookingFormTests
Assert.True(dialog.Find("button.btn-success").HasAttribute("disabled"));
dialog.Find("#entry-title").Input("Porto");
dialog.Find("#entry-amount").Input("12,501");
Assert.Equal("12,501", dialog.Find("#entry-amount").GetAttribute("value"));
Assert.Equal("", dialog.Find("#entry-amount").GetAttribute("value"));
Assert.True(dialog.Find("button.btn-success").HasAttribute("disabled"));
dialog.Find("#entry-amount").Input("-12,50");
Assert.True(dialog.Find("button.btn-success").HasAttribute("disabled"));
@@ -39,6 +40,7 @@ public class BookingFormTests
public void Edit_ShowsFixedTypeAndCorrectionPath_AndRerenderKeepsInput()
{
using var context = new BunitContext();
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);
var dialog = context.Render<AddEntryDialog>(p => p.Add(x => x.EditEntry, entry));
+3 -3
View File
@@ -37,7 +37,7 @@ public class BookingSaveTests
Assert.Equal(1, fixture.Entries.WriteCalls);
delayed.SetResult();
await pending;
page.WaitForAssertion(() => Assert.Contains("gespeichert", page.Find(".alert-success").TextContent));
page.WaitForAssertion(() => Assert.Contains("gespeichert", page.Find(".toast-success").TextContent));
Assert.Single(await fixture.Entries.GetEntriesAsync(1, (int?)null));
Assert.Single(page.FindAll("tr.entry-recent"));
if (transfer) Assert.Single(await fixture.Entries.GetEntriesAsync(2, (int?)null));
@@ -62,7 +62,7 @@ public class BookingSaveTests
fixture.Entries.BeforeWrite = () => Task.FromException(new IOException("Simulierter Speicherfehler"));
await page.Find("form").TriggerEventAsync("onsubmit", EventArgs.Empty);
Assert.Contains("Eingaben bleiben erhalten", page.Markup);
Assert.Empty(page.FindAll(".alert-success"));
Assert.Empty(page.FindAll(".toast-success"));
Assert.Equal("Testbuchung", page.Find(transfer ? "#transfer-title" : "#entry-title").GetAttribute("value"));
Assert.Equal("12,50", page.Find(transfer ? "#transfer-amount" : "#entry-amount").GetAttribute("value"));
Assert.Empty(await fixture.Entries.GetEntriesAsync(1, (int?)null));
@@ -86,7 +86,7 @@ public class BookingSaveTests
dialog.Find("#entry-title").Input("Testbuchung");
dialog.Find("#entry-amount").Input("12,50");
await dialog.Find("form").TriggerEventAsync("onsubmit", EventArgs.Empty);
Assert.Contains("Die Buchung ist gespeichert", dialog.Markup);
Assert.Contains(fixture.Context.Services.GetRequiredService<ToastService>().Messages, t => t.Message.Contains("Die Buchung ist gespeichert"));
await dialog.Find("form").TriggerEventAsync("onsubmit", EventArgs.Empty);
Assert.Equal(1, fixture.Entries.WriteCalls);
Assert.Equal(2, notifications);
+43
View File
@@ -0,0 +1,43 @@
using Bunit;
using Duempelkas.App.Components;
using Duempelkas.App.Services;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
namespace Duempelkas.Tests;
public class ToastTests
{
[Fact]
public async Task ToastsStack_ExpireIndependently_AndPauseDuringInteraction()
{
using var context = new BunitContext();
context.Services.AddSingleton<ToastService>();
var host = context.Render<ToastHost>();
var service = context.Services.GetRequiredService<ToastService>();
var first = service.Show("Erste Meldung", duration: TimeSpan.FromMilliseconds(100));
service.Pause(first);
service.Show("Zweite Meldung", ToastKind.Success, persistent: true);
host.WaitForAssertion(() => Assert.Equal(2, host.FindAll(".app-toast").Count));
await Task.Delay(200);
Assert.Equal(2, service.Messages.Count);
service.Resume(first);
host.WaitForAssertion(() => Assert.Single(host.FindAll(".app-toast")));
Assert.Contains("Zweite Meldung", host.Markup);
host.Find(".toast-close").Click();
Assert.Empty(service.Messages);
}
[Fact]
public void AmountControl_RejectsWholePasteWithoutRounding()
{
using var context = new BunitContext();
string amount = "12,50";
var control = context.Render<AmountInput>(p => p.Add(x => x.Value, amount).Add(x => x.ValueChanged, value => amount = value));
control.Find("input").Input("12,501");
Assert.Equal("12,50", amount);
Assert.Contains("nicht übernommen", control.Markup);
control.Find("input").Input("12.25");
Assert.Equal("12.25", amount);
}
}