Generate navigation and render page embeds from PocketBase
This commit is contained in:
1 parent
6b5d9dda55
commit
3fd3796d73
26 files changed
+544
-114
No files matched your search
@@ -0,0 +1,68 @@
|
||||
using Elternbeirat.Contracts;
|
||||
using Elternbeirat.Web.Features.Layout;
|
||||
|
||||
namespace Elternbeirat.Web.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for the navigation builder. Pure logic, no PocketBase needed.
|
||||
/// </summary>
|
||||
public class NavBuilderTests
|
||||
{
|
||||
private static Page Page(string slug, string location, double order, string title) =>
|
||||
new() { Slug = slug, Location = location, Order = order, Title = title };
|
||||
|
||||
[Fact]
|
||||
public void Header_keeps_only_header_pages_ordered_by_order()
|
||||
{
|
||||
var pages = new[]
|
||||
{
|
||||
Page("board", "header", 2, "Vorstandsteam"),
|
||||
Page("home", "header", 1, "Start"),
|
||||
Page("imprint", "footer", 1, "Impressum"),
|
||||
Page("hidden", "", 1, "Unterseite"),
|
||||
};
|
||||
|
||||
var header = NavBuilder.Header(pages);
|
||||
|
||||
header.Select(link => link.Title).ShouldBe(["Start", "Vorstandsteam"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Footer_keeps_only_footer_pages_ordered_by_order()
|
||||
{
|
||||
var pages = new[]
|
||||
{
|
||||
Page("privacy", "footer", 2, "Datenschutz"),
|
||||
Page("imprint", "footer", 1, "Impressum"),
|
||||
Page("home", "header", 1, "Start"),
|
||||
};
|
||||
|
||||
var footer = NavBuilder.Footer(pages);
|
||||
|
||||
footer.Select(link => link.Title).ShouldBe(["Impressum", "Datenschutz"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Home_page_maps_to_root_every_other_slug_to_its_path()
|
||||
{
|
||||
var pages = new[]
|
||||
{
|
||||
Page("home", "header", 1, "Start"),
|
||||
Page("board", "header", 2, "Vorstandsteam"),
|
||||
};
|
||||
|
||||
var header = NavBuilder.Header(pages);
|
||||
|
||||
header[0].Href.ShouldBe("/");
|
||||
header[1].Href.ShouldBe("/board");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pages_without_a_location_never_appear_in_a_menu()
|
||||
{
|
||||
var pages = new[] { Page("hidden", "", 1, "Unterseite") };
|
||||
|
||||
NavBuilder.Header(pages).ShouldBeEmpty();
|
||||
NavBuilder.Footer(pages).ShouldBeEmpty();
|
||||
}
|
||||
}
|
||||
@@ -257,8 +257,10 @@ public sealed class PocketBaseFixture : IAsyncLifetime
|
||||
});
|
||||
await CreateRecordAsync(http, "pages", new
|
||||
{
|
||||
// Carries an embed so the ContentPage embed path is covered: /patrons
|
||||
// renders its body plus the FAQ teaser.
|
||||
title = "Förderverein", body = "# Förderverein", location = "header",
|
||||
order = 3, slug = "patrons", embed = Array.Empty<string>(), @public = true,
|
||||
order = 3, slug = "patrons", embed = new[] { "faqs" }, @public = true,
|
||||
});
|
||||
// Nav placeholder for the FAQ list (route /faqs is served by FaqList,
|
||||
// which shadows this page). Slug matches production's "faqs" nav entry.
|
||||
|
||||
@@ -98,4 +98,47 @@ public sealed class RouteSmokeTests : IDisposable
|
||||
html.ShouldContain("Wann gibt es Mittagessen?");
|
||||
html.ShouldContain("Mensa und Mittagessen"); // German topic heading
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Layout_navigation_is_built_from_the_pages()
|
||||
{
|
||||
// The header and footer menus are generated from the "pages" records, not
|
||||
// hard-coded. Assert a header link, a footer link, and that the non-public
|
||||
// draft page never leaks into the menu.
|
||||
var client = _factory.CreateClient();
|
||||
|
||||
var html = await client.GetStringAsync(new Uri("/", UriKind.Relative));
|
||||
|
||||
html.ShouldContain("href=\"/board\""); // header page
|
||||
html.ShouldContain("href=\"/imprint\""); // footer page
|
||||
html.ShouldNotContain("href=\"/draft\""); // public=false, filtered out
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Home_renders_the_embeds_its_page_opts_into()
|
||||
{
|
||||
// The home page's embed field is ["posts","events"], so the root route
|
||||
// renders the posts teaser and the events teaser below the intro.
|
||||
var client = _factory.CreateClient();
|
||||
|
||||
var html = await client.GetStringAsync(new Uri("/", UriKind.Relative));
|
||||
|
||||
html.ShouldContain("Aktuelle Beiträge"); // posts embed heading
|
||||
html.ShouldContain("Neuer Vorstand"); // a seeded post title
|
||||
html.ShouldContain("Kommende Termine"); // events embed heading
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Content_page_renders_the_embed_its_page_opts_into()
|
||||
{
|
||||
// /patrons is a catch-all content page whose embed field is ["faqs"], so it
|
||||
// shows its body plus the FAQ teaser -- proving the ContentPage embed path.
|
||||
var client = _factory.CreateClient();
|
||||
|
||||
var html = await client.GetStringAsync(new Uri("/patrons", UriKind.Relative));
|
||||
|
||||
html.ShouldContain("Förderverein"); // the page body
|
||||
html.ShouldContain("Häufige Fragen"); // faqs embed heading
|
||||
html.ShouldContain("Wann gibt es Mittagessen?"); // a seeded question
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user