feat(offline): bundle local assets and add safe UI error recovery (U16)
This commit is contained in:
@@ -37,7 +37,9 @@ public class CarryoverCorrectionTests
|
||||
var dialog = f.Context.Render<EditCarryoverDialog>(p => p.Add(x => x.CurrentAmount, 110m)
|
||||
.Add(x => x.InitialBalance, 100m).Add(x => x.Year, 2026).Add(x => x.AccountName, "Barkasse")
|
||||
.Add(x => x.OnSave, value => saved = value));
|
||||
Assert.Equal("110,00", dialog.Find("#carryover").GetAttribute("value"));
|
||||
dialog.Find("#carryover").Input("120,00");
|
||||
Assert.Equal("120,00", dialog.Find("#carryover").GetAttribute("value"));
|
||||
Assert.Contains("frühere und spätere Jahre", dialog.Markup);
|
||||
Assert.Contains("110,00", dialog.Find(".correction-preview").TextContent);
|
||||
Assert.True(dialog.Find("button.btn-success").HasAttribute("disabled"));
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Xunit;
|
||||
|
||||
namespace Duempelkas.Tests;
|
||||
|
||||
public class DesktopResourceTests
|
||||
{
|
||||
[Fact]
|
||||
public void StartupStylesAndFonts_AreDeliveredLocally()
|
||||
{
|
||||
var root = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
while (root != null && !File.Exists(Path.Combine(root.FullName, "Duempelkas.slnx"))) root = root.Parent;
|
||||
Assert.NotNull(root);
|
||||
var web = Path.Combine(root.FullName, "src", "Duempelkas.Desktop", "wwwroot");
|
||||
var html = File.ReadAllText(Path.Combine(web, "index.html"));
|
||||
Assert.DoesNotContain("https://", html);
|
||||
foreach (Match match in Regex.Matches(html, "(?:href|src)=\"([^\"]+)\""))
|
||||
{
|
||||
var path = match.Groups[1].Value;
|
||||
if (path.Length == 0 || path == "/" || path.StartsWith("_framework")) continue;
|
||||
Assert.True(File.Exists(Path.Combine(web, path)), path);
|
||||
}
|
||||
var icons = Path.Combine(web, "vendor", "bootstrap-icons");
|
||||
var css = File.ReadAllText(Path.Combine(icons, "bootstrap-icons.min.css"));
|
||||
foreach (Match match in Regex.Matches(css, "url\\([\"']?([^\"')]+)"))
|
||||
Assert.True(File.Exists(Path.Combine(icons, match.Groups[1].Value.Split('?')[0])));
|
||||
Assert.True(File.Exists(Path.Combine(icons, "LICENSE")));
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ public class EntryLifecycleUiTests
|
||||
Assert.Contains("zählt nicht zum Kontostand", page.Markup);
|
||||
page.Find(".toast-success .btn").Click();
|
||||
page.WaitForAssertion(() => Assert.Single(page.FindAll("tbody tr")));
|
||||
page.WaitForAssertion(() => Assert.Empty(page.FindAll(".deleted-status")));
|
||||
Assert.All(await entries.GetEntriesAsync(2, (int?)null), e => Assert.False(e.IsDeleted));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,15 @@ namespace Duempelkas.Tests;
|
||||
|
||||
public class EntryPresentationTests
|
||||
{
|
||||
[Fact]
|
||||
public void EmptyAccount_ShowsExplanationRatherThanParameterName()
|
||||
{
|
||||
using var f = new LedgerUiFixture();
|
||||
f.Service<Microsoft.AspNetCore.Components.NavigationManager>().NavigateTo("/accounts/2");
|
||||
var page = f.Context.Render<Duempelkas.App.Components.Routes>();
|
||||
page.WaitForAssertion(() => Assert.Contains("Noch keine Buchungen vorhanden.", page.Markup));
|
||||
Assert.DoesNotContain("emptyEntriesMessage", page.Markup);
|
||||
}
|
||||
[Fact]
|
||||
public void Actions_HaveVisibleTextAndBookingSpecificNames()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
using Bunit;
|
||||
using Duempelkas.App.Components;
|
||||
using Duempelkas.App.Services;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Xunit;
|
||||
|
||||
namespace Duempelkas.Tests;
|
||||
|
||||
public class ErrorRecoveryTests
|
||||
{
|
||||
[Fact]
|
||||
public void UnexpectedError_OffersRecoveryWithoutLeakingBookingData()
|
||||
{
|
||||
using var context = new BunitContext();
|
||||
context.Services.AddSingleton<ToastService>();
|
||||
var boundary = context.Render<AppErrorBoundary>(p => p.AddChildContent<Throws>());
|
||||
boundary.WaitForAssertion(() => Assert.Contains("nicht bestätigt", boundary.Markup));
|
||||
Assert.Contains("Zur Kontenübersicht", boundary.Markup);
|
||||
Assert.Contains("InvalidOperationException", boundary.Markup);
|
||||
Assert.DoesNotContain("SECRET_BOOKING_DATA", boundary.Markup);
|
||||
}
|
||||
private sealed class Throws : ComponentBase
|
||||
{
|
||||
protected override void BuildRenderTree(RenderTreeBuilder builder) => throw new InvalidOperationException("SECRET_BOOKING_DATA");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using Bunit;
|
||||
using Duempelkas.App.Components;
|
||||
using Duempelkas.App.Services;
|
||||
using Duempelkas.Domain.Enums;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Xunit;
|
||||
|
||||
namespace Duempelkas.Tests;
|
||||
|
||||
public class LayoutControlTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task TextScaleControl_ChangesPresentationWithoutChangingLedger()
|
||||
{
|
||||
using var f = new LedgerUiFixture();
|
||||
var entries = f.Service<IEntryService>();
|
||||
await entries.CreateEntryAsync(1, EntryType.Expense, DateTime.Today, "Porto mit sehr langer Beschreibung und Umlauten äöü", 4.20m);
|
||||
f.Service<NavigationManager>().NavigateTo("/accounts/1");
|
||||
var page = f.Context.Render<Routes>();
|
||||
page.WaitForElement("#text-scale").Change("200");
|
||||
Assert.Contains(f.Context.JSInterop.Invocations, i => i.Identifier == "duempelkasReading.set" && Equals(i.Arguments[0], 200));
|
||||
Assert.Equal(95.80m, (await f.Service<IBalanceQueryService>().GetAccountBalanceAsync(1)).TotalBalance);
|
||||
Capture("account", page.Markup);
|
||||
page.FindAll("button").Single(b => b.TextContent.Contains("Einnahme oder Ausgabe")).Click();
|
||||
page.WaitForElement("#entry-title");
|
||||
Capture("booking", page.Markup);
|
||||
}
|
||||
|
||||
private static void Capture(string name, string markup)
|
||||
{
|
||||
var output = Environment.GetEnvironmentVariable("DUEMPELKAS_LAYOUT_EVIDENCE");
|
||||
if (output == null) return;
|
||||
Directory.CreateDirectory(output);
|
||||
var root = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
while (root != null && !File.Exists(Path.Combine(root.FullName, "Duempelkas.slnx"))) root = root.Parent;
|
||||
var web = new Uri(Path.Combine(root!.FullName, "src", "Duempelkas.Desktop", "wwwroot") + Path.DirectorySeparatorChar).AbsoluteUri;
|
||||
File.WriteAllText(Path.Combine(output, name + ".html"), "<!doctype html><html lang='de'><head><meta charset='utf-8'><meta name='viewport' content='width=device-width, initial-scale=1'>" +
|
||||
$"<link rel='stylesheet' href='{web}vendor/bootstrap/bootstrap.min.css'><link rel='stylesheet' href='{web}vendor/bootstrap-icons/bootstrap-icons.min.css'><link rel='stylesheet' href='{web}css/app.css'>" +
|
||||
"</head><body>" + markup + $"<script src='{web}js/app.js'></script>" +
|
||||
"<script>document.querySelectorAll('dialog').forEach(d=>d.showModal());</script></body></html>");
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,9 @@ public class ModalDialogTests
|
||||
await modal.InvokeAsync(() => modal.Instance.RequestCloseAsync());
|
||||
Assert.Equal(1, cancelled);
|
||||
Assert.Contains("Ungespeicherte Eingaben", modal.Markup);
|
||||
await modal.InvokeAsync(() => modal.Instance.RequestCloseAsync());
|
||||
Assert.Empty(modal.FindAll("[role=alertdialog]"));
|
||||
await modal.InvokeAsync(() => modal.Instance.RequestCloseAsync());
|
||||
modal.Find("[data-discard-continue]").Click();
|
||||
Assert.Empty(modal.FindAll("[role=alertdialog]"));
|
||||
await modal.InvokeAsync(() => modal.Instance.RequestCloseAsync());
|
||||
|
||||
Reference in New Issue
Block a user