Add the design foundation: self-hosted Nunito, type scale, new tokens and an Icon component

This commit is contained in:
tleininger committed 2026-10-01 12:03:39 +02:00
1 parent 3530167cc3
commit 33fa1e4e93
9 files changed
+401 -10

No files matched your search

+30
View File
@@ -0,0 +1,30 @@
using Elternbeirat.Web.Shared;
namespace Elternbeirat.Web.Tests;
/// <summary>
/// Unit tests for the icon lookup behind the Icon component. Pure logic, no
/// PocketBase needed.
/// </summary>
public class IconSetTests
{
[Theory]
[InlineData("calendar")]
[InlineData("map-pin")]
[InlineData("help-circle")] // kept under its old name although Lucide renamed it
public void Known_name_returns_svg_markup(string name)
{
var markup = IconSet.Find(name);
markup.ShouldNotBeNullOrWhiteSpace();
markup.ShouldStartWith("<"); // SVG child elements, ready to inline
}
[Theory]
[InlineData("does-not-exist")]
[InlineData("Calendar")] // lookup is case-sensitive, names are code identifiers
[InlineData("")]
[InlineData(null)]
public void Unknown_name_returns_null(string? name) =>
IconSet.Find(name).ShouldBeNull();
}
+44 -1
View File
@@ -1,4 +1,5 @@
using System.Net;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Mvc.Testing;
namespace Elternbeirat.Web.Tests;
@@ -15,7 +16,7 @@ namespace Elternbeirat.Web.Tests;
/// </para>
/// </summary>
[Collection(PocketBaseTestGroup.Name)]
public sealed class RouteSmokeTests : IDisposable
public sealed partial class RouteSmokeTests : IDisposable
{
// WithWebHostBuilder returns a new factory that wraps the base one; both are
// disposable, so both are held and disposed to avoid leaking either.
@@ -158,6 +159,39 @@ public sealed class RouteSmokeTests : IDisposable
events.ShouldContain("Sitzungen und Veranstaltungen auf einen Blick."); // the page body
}
[Theory]
[InlineData("/app.css")]
[InlineData("/")]
public async Task No_resource_is_loaded_from_a_foreign_host(string route)
{
// Privacy rule (docs/recht.md): nothing may make the visitor's browser contact
// a third-party host -- no Google Fonts, no CDN. Check the stylesheet and the
// rendered home page for anything that LOADS from an absolute URL (src,
// srcset, <link href>, CSS url() and @import). Plain <a href> links are left
// alone on purpose: an editor may link to another site, and a link loads
// nothing until the visitor clicks it.
var client = _factory.CreateClient();
var body = await client.GetStringAsync(new Uri(route, UriKind.Relative));
ForeignResource().Matches(body).Select(match => match.Value).ShouldBeEmpty();
}
[Fact]
public async Task Site_font_is_self_hosted()
{
// The @font-face must point at our own wwwroot with a relative URL, and the
// file must actually be served -- otherwise every visitor silently falls back
// to the system font.
var client = _factory.CreateClient();
var css = await client.GetStringAsync(new Uri("/app.css", UriKind.Relative));
css.ShouldContain("url(\"fonts/nunito-latin-wght.woff2\")");
var font = await client.GetAsync(new Uri("/fonts/nunito-latin-wght.woff2", UriKind.Relative));
font.StatusCode.ShouldBe(HttpStatusCode.OK);
}
[Fact]
public async Task Health_returns_200_when_PocketBase_is_reachable()
{
@@ -187,4 +221,13 @@ public sealed class RouteSmokeTests : IDisposable
response.StatusCode.ShouldBe(HttpStatusCode.ServiceUnavailable);
}
/// <summary>
/// Matches a resource reference that points at an absolute URL, with or without
/// scheme (<c>https://</c>, <c>http://</c> or protocol-relative <c>//</c>): the
/// <c>src</c>/<c>srcset</c> attributes, a <c>&lt;link&gt;</c>'s <c>href</c>, CSS
/// <c>url()</c> and <c>@import</c>.
/// </summary>
[GeneratedRegex("""(?:\b(?:src|srcset)\s*=\s*|<link\b[^>]*\bhref\s*=\s*|url\(\s*|@import\s+)["']?\s*(?:https?:)?//""", RegexOptions.IgnoreCase)]
private static partial Regex ForeignResource();
}