feat(usability): explain booking fields and validate decimal input (U06)
This commit is contained in:
@@ -35,3 +35,7 @@ Gemeinsame explizite Jahresauswahl bleibt beim Wechsel Übersicht/Konto/Zurück
|
||||
## U05 – Alltagssprache
|
||||
|
||||
Aktionsleisten benennen Erfassen, Kontenverschieben, PDF-Speichern und Sicherung ausführlich. Flexible Breiten und Zeilenumbruch ersetzen die feste 75-px-Breite. Rückspielen liegt weiterhin auffindbar unter „Weitere Aktionen“ und erklärt Umfang und Rückfallsicherung. „Kontostand“ und „Änderung im Jahr“ ersetzen Fachbegriffe. Umbuchung erklärt ausdrücklich, dass keine Banküberweisung ausgelöst wird. Anrede in den betroffenen Abläufen: du. Prüfung: 21/21 Debug-Tests; native Sichtprüfung folgt im Abschluss. Ein Nutzertest mit der Zielgruppe ist dadurch nicht ersetzt.
|
||||
|
||||
## U06 – Verständliche Eingabe
|
||||
|
||||
Konto im Dialogtitel, sichtbare Art beim Bearbeiten und erklärter Korrekturweg: abbrechen, falsche Buchung löschen, neu erfassen. Keine Änderung der Buchungsart bestehender Daten. Feldnahe, technisch zugeordnete Hinweise für Beschreibung, Datum und Betrag. Komma/Punkt sowie eingefügte einfache Beträge mit Rand-Leerzeichen werden akzeptiert; Minus, Tausendertrenner, Währungszeichen, Überlauf und mehr als zwei Nachkommastellen abgewiesen, ohne Rundung. Dienste prüfen dieselben fachlichen Mindestregeln. Nach erneutem Rendern bleiben geänderte Eingaben erhalten. Prüfung: 35/35 Debug-Tests, darunter Parsing-Grenzfälle und gerenderte Formularprüfung mit Korrektur ungültiger Eingaben. Längere Buchungsdialoge können innerhalb des Fensters scrollen.
|
||||
|
||||
@@ -1,30 +1,45 @@
|
||||
@inject IEntryService EntryService
|
||||
|
||||
<div class="dialog-backdrop">
|
||||
<div class="dialog-content" @onclick:stopPropagation="true">
|
||||
<h5>@(EditEntry != null ? "Buchung bearbeiten" : "Neue Buchung")</h5>
|
||||
<div class="dialog-content booking-dialog" role="dialog" aria-modal="true" aria-labelledby="entry-dialog-title">
|
||||
<h5 id="entry-dialog-title">@(EditEntry != null ? "Buchung bearbeiten" : "Einnahme oder Ausgabe erfassen") – @AccountName</h5>
|
||||
|
||||
<div class="form-container">
|
||||
<label class="form-label" for="entry-type">Art</label>
|
||||
@if (EditEntry == null)
|
||||
{
|
||||
<label class="form-label">Art</label>
|
||||
<select @bind="entryType">
|
||||
<select id="entry-type" @bind="entryType">
|
||||
<option value="@EntryType.Income">Einnahme</option>
|
||||
<option value="@EntryType.Expense">Ausgabe</option>
|
||||
</select>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div><strong>@(entryType == EntryType.Income ? "Einnahme" : "Ausgabe")</strong>
|
||||
<p class="text-muted">Falsche Art? Diese Buchung abbrechen, in der Kontoliste löschen und mit der richtigen Art neu erfassen.</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
<label class="form-label">Datum</label>
|
||||
<input type="date" @bind="date" />
|
||||
|
||||
<label class="form-label">Bezeichnung</label>
|
||||
<input type="text" class="form-control" @bind="title" placeholder="Beschreibung" />
|
||||
|
||||
<label class="form-label">Betrag (€)</label>
|
||||
<input type="number" class="form-control no-spinner" @bind="amount" step="0.01" min="0.01" />
|
||||
<label class="form-label" for="entry-date">Datum</label>
|
||||
<div>
|
||||
<input id="entry-date" type="date" value="@dateText" @oninput="e => dateText = e.Value?.ToString() ?? string.Empty" aria-describedby="entry-date-help" aria-invalid="@(date == default ? "true" : "false")" />
|
||||
@if (date == default) { <p id="entry-date-help" class="text-danger">Bitte ein gültiges Datum eingeben.</p> }
|
||||
</div>
|
||||
|
||||
@if (OutsideYear)
|
||||
<label class="form-label" for="entry-title">Beschreibung</label>
|
||||
<div>
|
||||
<input id="entry-title" type="text" class="form-control" @bind="title" @bind:event="oninput" placeholder="z. B. Mitgliedsbeitrag oder Porto" aria-describedby="entry-title-help" aria-invalid="@(string.IsNullOrWhiteSpace(title) ? "true" : "false")" />
|
||||
@if (string.IsNullOrWhiteSpace(title)) { <p id="entry-title-help" class="text-danger">Bitte beschreiben, wofür das Geld eingenommen oder ausgegeben wurde.</p> }
|
||||
</div>
|
||||
|
||||
<label class="form-label" for="entry-amount">Betrag (€)</label>
|
||||
<div>
|
||||
<input id="entry-amount" type="text" inputmode="decimal" class="form-control" @bind="amountText" @bind:event="oninput" placeholder="12,50" aria-describedby="entry-amount-help" aria-invalid="@(!ValidAmount ? "true" : "false")" />
|
||||
<p id="entry-amount-help" class="@(!ValidAmount ? "text-danger" : "text-muted")">Betrag ohne Minuszeichen, größer als 0. Komma oder Punkt, höchstens zwei Nachkommastellen. Keine Tausendertrenner oder Währungszeichen, auch beim Einfügen. Es wird nicht gerundet.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (OutsideYear && date != default)
|
||||
{
|
||||
<div class="alert alert-warning" role="status">
|
||||
Datum @date.ToString("dd.MM.yyyy"): Diese Buchung liegt außerhalb von Jahr @ContextYear und wird dort nicht angezeigt.
|
||||
@@ -33,12 +48,10 @@
|
||||
}
|
||||
|
||||
<div class="d-flex justify-content-end gap-2">
|
||||
<button class="btn btn-success" @onclick="Save"
|
||||
disabled="@(string.IsNullOrWhiteSpace(title) || amount <= 0 || (OutsideYear && (!confirmDifferentYear || confirmedYear != date.Year)))">
|
||||
<i class="bi bi-check-lg"></i> @(EditEntry != null ? "Speichern" : "Hinzufügen")
|
||||
<button class="btn btn-success" @onclick="Save" disabled="@(!CanSave)">
|
||||
<i class="bi bi-check-lg"></i> Speichern
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" @onclick="Cancel"><i class="bi bi-x-lg"></i> Abbrechen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -27,12 +27,20 @@ public partial class AddEntryDialog
|
||||
private int? confirmedYear;
|
||||
private bool OutsideYear => ContextYear.HasValue && date.Year != ContextYear.Value;
|
||||
|
||||
[Parameter] public string AccountName { get; set; } = string.Empty;
|
||||
private bool CanSave => !string.IsNullOrWhiteSpace(title) && ValidAmount && date != default && (!OutsideYear || (confirmDifferentYear && confirmedYear == date.Year));
|
||||
|
||||
#region Fields
|
||||
|
||||
private EntryType entryType = EntryType.Income;
|
||||
private DateTime date = DateTime.Today;
|
||||
private string dateText = DateTime.Today.ToString("yyyy-MM-dd");
|
||||
private DateTime date => Duempelkas.App.Services.BookingInput.Date(dateText);
|
||||
private string title = string.Empty;
|
||||
private decimal amount;
|
||||
private string amountText = string.Empty;
|
||||
private bool ValidAmount => Duempelkas.App.Services.BookingInput.TryAmount(amountText, out _);
|
||||
private decimal amount => Duempelkas.App.Services.BookingInput.TryAmount(amountText, out var value) ? value : 0;
|
||||
private bool initialized;
|
||||
private int? initializedEntryId;
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -40,12 +48,15 @@ public partial class AddEntryDialog
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
if (initialized && initializedEntryId == EditEntry?.Id) return;
|
||||
initialized = true;
|
||||
initializedEntryId = EditEntry?.Id;
|
||||
if (EditEntry != null)
|
||||
{
|
||||
entryType = EditEntry.Type;
|
||||
date = EditEntry.Date;
|
||||
dateText = EditEntry.Date.ToString("yyyy-MM-dd");
|
||||
title = EditEntry.Title;
|
||||
amount = EditEntry.Amount;
|
||||
amountText = EditEntry.Amount.ToString("0.00", System.Globalization.CultureInfo.GetCultureInfo("de-DE"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +66,7 @@ public partial class AddEntryDialog
|
||||
|
||||
private async Task Save()
|
||||
{
|
||||
if (OutsideYear && (!confirmDifferentYear || confirmedYear != date.Year)) return;
|
||||
if (!CanSave) return;
|
||||
if (EditEntry != null)
|
||||
{
|
||||
await EntryService.UpdateEntryAsync(EditEntry.Id, date, title.Trim(), amount);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
@inject IAccountService AccountService
|
||||
|
||||
<div class="dialog-backdrop">
|
||||
<div class="dialog-content" @onclick:stopPropagation="true">
|
||||
<div class="dialog-content booking-dialog" @onclick:stopPropagation="true">
|
||||
<h5 class="mb-3">@(EditEntry != null ? "Umbuchung bearbeiten" : "Neue Umbuchung")</h5>
|
||||
|
||||
<p class="text-muted">Erfasst nur die Buchung. Führt keine Banküberweisung aus.</p>
|
||||
@@ -18,13 +18,14 @@
|
||||
</select>
|
||||
|
||||
<label class="form-label">Datum</label>
|
||||
<input type="date" class="form-control" @bind="date" />
|
||||
<input type="date" class="form-control" value="@dateText" @oninput="e => dateText = e.Value?.ToString() ?? string.Empty" />
|
||||
|
||||
<label class="form-label">Bezeichnung</label>
|
||||
<input type="text" class="form-control" @bind="title" placeholder="Beschreibung der Umbuchung" />
|
||||
<input type="text" class="form-control" @bind="title" @bind:event="oninput" placeholder="Beschreibung der Umbuchung" />
|
||||
|
||||
<label class="form-label">Betrag (€)</label>
|
||||
<input type="number" class="form-control no-spinner" @bind="amount" step="0.01" min="0.01" />
|
||||
<div><input type="text" inputmode="decimal" class="form-control" @bind="amountText" @bind:event="oninput" aria-describedby="transfer-amount-help" />
|
||||
<p id="transfer-amount-help" class="@(!ValidAmount ? "text-danger" : "text-muted")">Betrag ohne Minuszeichen, größer als 0; Komma oder Punkt, höchstens zwei Nachkommastellen. Keine Tausendertrenner oder Währungszeichen; keine Rundung.</p></div>
|
||||
</div>
|
||||
|
||||
@if (OutsideYear)
|
||||
|
||||
@@ -30,15 +30,20 @@ public partial class AddTransferDialog
|
||||
|
||||
private List<AccountSummaryDto> accounts = new();
|
||||
private int targetAccountId;
|
||||
private DateTime date = DateTime.Today;
|
||||
private string dateText = DateTime.Today.ToString("yyyy-MM-dd");
|
||||
private DateTime date => Duempelkas.App.Services.BookingInput.Date(dateText);
|
||||
private string title = string.Empty;
|
||||
private decimal amount;
|
||||
private string amountText = string.Empty;
|
||||
private bool ValidAmount => Duempelkas.App.Services.BookingInput.TryAmount(amountText, out _);
|
||||
private decimal amount => Duempelkas.App.Services.BookingInput.TryAmount(amountText, out var value) ? value : 0;
|
||||
private bool initialized;
|
||||
private int? initializedEntryId;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
|
||||
private bool CanSave => targetAccountId > 0 && !string.IsNullOrWhiteSpace(title) && amount > 0;
|
||||
private bool CanSave => targetAccountId > 0 && targetAccountId != SourceAccountId && !string.IsNullOrWhiteSpace(title) && ValidAmount && date != default && (!OutsideYear || (confirmDifferentYear && confirmedYear == date.Year));
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -51,12 +56,15 @@ public partial class AddTransferDialog
|
||||
accounts = await AccountService.GetAllAccountsAsync();
|
||||
}
|
||||
|
||||
if (initialized && initializedEntryId == EditEntry?.Id) return;
|
||||
initialized = true;
|
||||
initializedEntryId = EditEntry?.Id;
|
||||
if (EditEntry != null)
|
||||
{
|
||||
targetAccountId = EditEntry.LinkedAccountId ?? 0;
|
||||
date = EditEntry.Date;
|
||||
dateText = EditEntry.Date.ToString("yyyy-MM-dd");
|
||||
title = EditEntry.Title;
|
||||
amount = EditEntry.Amount;
|
||||
amountText = EditEntry.Amount.ToString("0.00", System.Globalization.CultureInfo.GetCultureInfo("de-DE"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +74,7 @@ public partial class AddTransferDialog
|
||||
|
||||
private async Task Save()
|
||||
{
|
||||
if (OutsideYear && (!confirmDifferentYear || confirmedYear != date.Year)) return;
|
||||
if (!CanSave) return;
|
||||
if (EditEntry != null)
|
||||
{
|
||||
await EntryService.UpdateTransferAsync(EditEntry.Id, targetAccountId, date, title.Trim(), amount);
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
|
||||
@if (showAddEntry)
|
||||
{
|
||||
<AddEntryDialog AccountId="AccountId" ContextYear="selectedYear"
|
||||
<AddEntryDialog AccountName="@(account?.Name ?? "")" AccountId="AccountId" ContextYear="selectedYear"
|
||||
OnSave="HandleEntryCreated"
|
||||
OnCancel="() => showAddEntry = false" />
|
||||
}
|
||||
@@ -175,7 +175,7 @@
|
||||
|
||||
@if (editingEntry != null)
|
||||
{
|
||||
<AddEntryDialog AccountId="AccountId" ContextYear="selectedYear"
|
||||
<AddEntryDialog AccountName="@(account?.Name ?? "")" AccountId="AccountId" ContextYear="selectedYear"
|
||||
EditEntry="editingEntry"
|
||||
OnSave="HandleEntryEdited"
|
||||
OnCancel="() => editingEntry = null" />
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Duempelkas.App.Services;
|
||||
|
||||
public static partial class BookingInput
|
||||
{
|
||||
[GeneratedRegex(@"^\d+([,.]\d{1,2})?$", RegexOptions.CultureInvariant)]
|
||||
private static partial Regex AmountPattern();
|
||||
|
||||
public static bool TryAmount(string text, out decimal amount)
|
||||
{
|
||||
amount = 0;
|
||||
var plain = text.Trim();
|
||||
return AmountPattern().IsMatch(plain)
|
||||
&& decimal.TryParse(plain.Replace(',', '.'), NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out amount)
|
||||
&& amount > 0;
|
||||
}
|
||||
|
||||
public static DateTime Date(string text) => DateTime.TryParseExact(text, "yyyy-MM-dd",
|
||||
CultureInfo.InvariantCulture, DateTimeStyles.None, out var date) ? date : default;
|
||||
|
||||
public static void Validate(DateTime date, string title, decimal amount)
|
||||
{
|
||||
if (date == default) throw new ArgumentException("Bitte ein gültiges Datum eingeben.");
|
||||
if (string.IsNullOrWhiteSpace(title)) throw new ArgumentException("Bitte eine Beschreibung eingeben.");
|
||||
if (amount <= 0 || decimal.Round(amount, 2) != amount)
|
||||
throw new ArgumentException("Betrag ohne Minuszeichen, größer als 0 und mit höchstens zwei Nachkommastellen eingeben.");
|
||||
}
|
||||
}
|
||||
@@ -597,3 +597,8 @@ input[type="number"].no-spinner {
|
||||
|
||||
.more-actions { align-self: center; }
|
||||
.more-actions[open] { padding: .5rem; border: 1px solid var(--color-border); border-radius: .5rem; }
|
||||
|
||||
.form-container > div { min-width: 0; }
|
||||
.form-container p { margin: .35rem 0 0; font-size: .9rem; }
|
||||
|
||||
.booking-dialog { max-height: calc(100vh - 2rem); overflow-y: auto; }
|
||||
|
||||
@@ -99,6 +99,8 @@ public class EntryService : IEntryService
|
||||
|
||||
public async Task<EntryDto> CreateEntryAsync(int accountId, EntryType type, DateTime date, string title, decimal amount)
|
||||
{
|
||||
if (!Enum.IsDefined(type)) throw new ArgumentException("Ungültige Buchungsart.");
|
||||
BookingInput.Validate(date, title, amount);
|
||||
using var lease = await DatabaseLease.AcquireAsync(_location);
|
||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||
|
||||
@@ -110,7 +112,7 @@ public class EntryService : IEntryService
|
||||
DisplayId = displayId,
|
||||
Type = type,
|
||||
Date = date,
|
||||
Title = title,
|
||||
Title = title.Trim(),
|
||||
Amount = amount
|
||||
};
|
||||
db.Entries.Add(entry);
|
||||
@@ -121,6 +123,7 @@ public class EntryService : IEntryService
|
||||
|
||||
public async Task CreateTransferAsync(int sourceAccountId, int targetAccountId, DateTime date, string title, decimal amount)
|
||||
{
|
||||
BookingInput.Validate(date, title, amount);
|
||||
if (sourceAccountId == targetAccountId)
|
||||
throw new InvalidOperationException("Umbuchung innerhalb desselben Kontos ist nicht möglich.");
|
||||
|
||||
@@ -136,7 +139,7 @@ public class EntryService : IEntryService
|
||||
DisplayId = transferDisplayId,
|
||||
Type = EntryType.Expense,
|
||||
Date = date,
|
||||
Title = title,
|
||||
Title = title.Trim(),
|
||||
Amount = amount
|
||||
};
|
||||
|
||||
@@ -149,7 +152,7 @@ public class EntryService : IEntryService
|
||||
DisplayId = transferDisplayId,
|
||||
Type = EntryType.Income,
|
||||
Date = date,
|
||||
Title = title,
|
||||
Title = title.Trim(),
|
||||
Amount = amount
|
||||
};
|
||||
|
||||
@@ -220,6 +223,7 @@ public class EntryService : IEntryService
|
||||
|
||||
public async Task UpdateEntryAsync(int entryId, DateTime date, string title, decimal amount)
|
||||
{
|
||||
BookingInput.Validate(date, title, amount);
|
||||
using var lease = await DatabaseLease.AcquireAsync(_location);
|
||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||
|
||||
@@ -230,7 +234,7 @@ public class EntryService : IEntryService
|
||||
.FirstOrDefaultAsync(tl => tl.SourceEntryId == entryId || tl.TargetEntryId == entryId);
|
||||
|
||||
entry.Date = date;
|
||||
entry.Title = title;
|
||||
entry.Title = title.Trim();
|
||||
entry.Amount = amount;
|
||||
|
||||
if (link != null)
|
||||
@@ -240,7 +244,7 @@ public class EntryService : IEntryService
|
||||
if (otherEntry != null)
|
||||
{
|
||||
otherEntry.Date = date;
|
||||
otherEntry.Title = title;
|
||||
otherEntry.Title = title.Trim();
|
||||
otherEntry.Amount = amount;
|
||||
}
|
||||
}
|
||||
@@ -250,6 +254,7 @@ public class EntryService : IEntryService
|
||||
|
||||
public async Task UpdateTransferAsync(int entryId, int newLinkedAccountId, DateTime date, string title, decimal amount)
|
||||
{
|
||||
BookingInput.Validate(date, title, amount);
|
||||
using var lease = await DatabaseLease.AcquireAsync(_location);
|
||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||
|
||||
@@ -266,10 +271,10 @@ public class EntryService : IEntryService
|
||||
|
||||
// Update date, title, amount on both sides
|
||||
entry.Date = date;
|
||||
entry.Title = title;
|
||||
entry.Title = title.Trim();
|
||||
entry.Amount = amount;
|
||||
otherEntry.Date = date;
|
||||
otherEntry.Title = title;
|
||||
otherEntry.Title = title.Trim();
|
||||
otherEntry.Amount = amount;
|
||||
|
||||
// If the linked account has changed, move the other entry to the new account
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
using Bunit;
|
||||
using Duempelkas.App.Components.Dialogs;
|
||||
using Duempelkas.App.Services;
|
||||
using Duempelkas.App.Services.Models;
|
||||
using Duempelkas.Domain.Enums;
|
||||
using Duempelkas.Infrastructure.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Xunit;
|
||||
|
||||
namespace Duempelkas.Tests;
|
||||
|
||||
public class BookingFormTests
|
||||
{
|
||||
[Fact]
|
||||
public void Form_ExplainsMissingFields_AndDoesNotRoundInvalidInput()
|
||||
{
|
||||
using var context = new BunitContext();
|
||||
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);
|
||||
Assert.Contains("Bitte beschreiben", dialog.Markup);
|
||||
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.True(dialog.Find("button.btn-success").HasAttribute("disabled"));
|
||||
dialog.Find("#entry-amount").Input("-12,50");
|
||||
Assert.True(dialog.Find("button.btn-success").HasAttribute("disabled"));
|
||||
dialog.Find("#entry-amount").Input("12,50");
|
||||
Assert.False(dialog.Find("button.btn-success").HasAttribute("disabled"));
|
||||
dialog.Find("#entry-date").Input("");
|
||||
Assert.True(dialog.Find("button.btn-success").HasAttribute("disabled"));
|
||||
Assert.Contains("Bitte ein gültiges Datum", dialog.Markup);
|
||||
dialog.Find("#entry-date").Input("2026-01-02");
|
||||
Assert.False(dialog.Find("button.btn-success").HasAttribute("disabled"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Edit_ShowsFixedTypeAndCorrectionPath_AndRerenderKeepsInput()
|
||||
{
|
||||
using var context = new BunitContext();
|
||||
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));
|
||||
Assert.Contains("Ausgabe", dialog.Markup);
|
||||
Assert.Contains("mit der richtigen Art neu erfassen", dialog.Markup);
|
||||
dialog.Find("#entry-title").Input("Korrigierter Text");
|
||||
dialog.Render(p => p.Add(x => x.EditEntry, entry));
|
||||
Assert.Equal("Korrigierter Text", dialog.Find("#entry-title").GetAttribute("value"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using Duempelkas.App.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace Duempelkas.Tests;
|
||||
|
||||
public class BookingInputTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("12,50", true)]
|
||||
[InlineData("12.50", true)]
|
||||
[InlineData(" 12,50 ", true)]
|
||||
[InlineData("12,500", false)]
|
||||
[InlineData("-12,50", false)]
|
||||
[InlineData("1.234,56", false)]
|
||||
[InlineData("1,234.56", false)]
|
||||
[InlineData("12,50 €", false)]
|
||||
[InlineData("0", false)]
|
||||
[InlineData("", false)]
|
||||
[InlineData("9999999999999999999999999999999999999", false)]
|
||||
public void DecimalInput_HasExplicitRules(string text, bool valid)
|
||||
{
|
||||
Assert.Equal(valid, BookingInput.TryAmount(text, out var amount));
|
||||
if (valid) Assert.Equal(12.50m, amount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ServiceValidation_RejectsInvalidData()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => BookingInput.Validate(default, "Test", 1m));
|
||||
Assert.Throws<ArgumentException>(() => BookingInput.Validate(DateTime.Today, " ", 1m));
|
||||
Assert.Throws<ArgumentException>(() => BookingInput.Validate(DateTime.Today, "Test", -1m));
|
||||
Assert.Throws<ArgumentException>(() => BookingInput.Validate(DateTime.Today, "Test", 1.001m));
|
||||
Assert.Equal(default, BookingInput.Date("2026-02-30"));
|
||||
}
|
||||
}
|
||||
@@ -42,9 +42,9 @@ public class YearNavigationTests
|
||||
page.WaitForElement(".summary-section");
|
||||
page.FindAll("button").Single(b => b.TextContent.Contains("Einnahme oder Ausgabe erfassen")).Click();
|
||||
var dialog = page.FindComponent<AddEntryDialog>();
|
||||
dialog.Find("input[type=date]").Change("2026-01-02");
|
||||
dialog.Find("input[type=text]").Change("Außerhalb");
|
||||
dialog.Find("input[type=number]").Change("12.50");
|
||||
dialog.Find("input[type=date]").Input("2026-01-02");
|
||||
dialog.Find("#entry-title").Input("Außerhalb");
|
||||
dialog.Find("#entry-amount").Input("12.50");
|
||||
Assert.Contains("außerhalb", dialog.Markup);
|
||||
Assert.True(dialog.Find("button.btn-success").HasAttribute("disabled"));
|
||||
dialog.Find("input[type=checkbox]").Change(true);
|
||||
|
||||
Reference in New Issue
Block a user