From 5203ddea7220b940e5f3bb78a0356172f53aed29 Mon Sep 17 00:00:00 2001 From: cesnimda Date: Sat, 15 Aug 2026 14:09:51 +0200 Subject: [PATCH] feat(cv): harden multi-page builder Wrap pathological content, paginate oversized entries, measure A4 and Letter previews correctly, unify section ordering, and gate stored-output actions on saved state. --- JobTrackerApi.Tests/CvBuilderTests.cs | 46 ++++ .../JobApplicationsApplicationPackageTests.cs | 30 +++ JobTrackerApi/Services/CvRenderModel.cs | 53 ++-- JobTrackerApi/Services/CvTemplateRenderer.cs | 66 +++-- JobTrackerApi/Services/ThemedCvRenderer.cs | 53 ++-- docs/architecture/cv-builder.md | 11 +- docs/architecture/cv-theme-engine.md | 8 +- docs/audits/verification-log.md | 1 + docs/research/cv-builder-patterns.md | 65 ++--- docs/verification/career-002-cv-builder.md | 27 ++- docs/work-programmes/decisions.md | 10 + docs/work-programmes/master-progress.md | 8 +- docs/work-programmes/master-work-plan.md | 8 +- docs/work-programmes/session-handoff.md | 14 +- job-tracker-ui/src/cv-builder-page.test.tsx | 30 ++- job-tracker-ui/src/cvBuilder.test.ts | 16 +- job-tracker-ui/src/cvBuilder.ts | 29 +++ job-tracker-ui/src/views/CvBuilderEditor.tsx | 228 ++++++++++++------ job-tracker-ui/src/views/CvBuilderPage.tsx | 11 +- 19 files changed, 508 insertions(+), 206 deletions(-) diff --git a/JobTrackerApi.Tests/CvBuilderTests.cs b/JobTrackerApi.Tests/CvBuilderTests.cs index 9b97b71..f5cd432 100644 --- a/JobTrackerApi.Tests/CvBuilderTests.cs +++ b/JobTrackerApi.Tests/CvBuilderTests.cs @@ -96,6 +96,27 @@ public sealed class CvBuilderTests Assert.Contains(model.Sections, s => s.Title == "Volunteering" && s.Bullets.Contains("Coached juniors")); } + [Fact] + public void Resolver_places_custom_sections_in_the_shared_section_order() + { + var settings = new CvVariantSettings + { + Sections = + { + new CvSectionSetting { Key = "custom:vol" }, + new CvSectionSetting { Key = "summary" }, + }, + CustomSections = + { + new CvCustomSectionSetting { Key = "vol", Title = "Volunteering", Items = { "Coached juniors" } }, + }, + }; + + var model = CvVariantResolver.Build(Rich(), settings, "F", null); + Assert.True(model.Sections.FindIndex(section => section.Key == "custom:vol") + < model.Sections.FindIndex(section => section.Key == "summary")); + } + // ---- Renderer (one path, every theme is data) ---- [Fact] @@ -164,6 +185,31 @@ public sealed class CvBuilderTests Assert.False(CvThemeCatalog.Resolve("technical").AtsFriendly); // sidebar = not ATS-safe } + [Fact] + public void Long_content_wraps_and_can_flow_across_pages_without_shrinking_typography() + { + var profile = Rich(); + profile.Contact.FullName = new string('N', 180); + profile.Contact.Email = $"{new string('e', 180)}@example.com"; + profile.Jobs[0].Title = new string('T', 220); + profile.Jobs[0].Company = new string('C', 220); + profile.Jobs[0].Bullets = Enumerable.Range(1, 7) + .Select(index => index == 1 ? new string('x', 500) : $"Detailed achievement {index} with readable typography.") + .ToList(); + + var renderer = new ThemedCvRenderer(); + var model = CvVariantResolver.Build(profile, new CvVariantSettings(), "F", null); + var html = renderer.Render(model, CvThemeCatalog.Resolve("technical"), new CvVariantSettings { ThemeId = "technical" }).Html; + + Assert.Contains("class=\"entry entry-flow\"", html); + Assert.Contains("class=\"item-flow\"", html); + Assert.Contains("overflow-wrap:anywhere", html); + Assert.Contains("grid-template-columns:62mm minmax(0,1fr)", html); + Assert.Contains("overflow:visible", html); + Assert.Contains("white-space:normal", html); + Assert.DoesNotContain("transform:scale", html); + } + [Fact] public void Accent_override_reaches_the_css() { diff --git a/JobTrackerApi.Tests/JobApplicationsApplicationPackageTests.cs b/JobTrackerApi.Tests/JobApplicationsApplicationPackageTests.cs index a07f02e..ad17de0 100644 --- a/JobTrackerApi.Tests/JobApplicationsApplicationPackageTests.cs +++ b/JobTrackerApi.Tests/JobApplicationsApplicationPackageTests.cs @@ -449,6 +449,36 @@ public sealed class JobApplicationsApplicationPackageTests Assert.Contains("curved", edinburgh.Html, StringComparison.OrdinalIgnoreCase); } + [Fact] + public void Template_renderer_wraps_long_content_and_escapes_sidebar_values() + { + var document = TailoredCvDraftJson.Normalize(new TailoredCvDocument + { + Headline = new string('H', 180), + SelectedSkills = new List { $"{new string('s', 400)}" }, + Experience = new List + { + new() + { + Title = new string('T', 220), + Company = new string('C', 220), + Start = "2020", + End = "Present", + Bullets = Enumerable.Range(1, 7).Select(index => index == 1 ? new string('x', 500) : $"Achievement {index}").ToList(), + }, + }, + }); + + var html = new CvTemplateRenderer().Render(document, "auckland", new string('N', 180), "Engineer", "Acme", null).Html; + + Assert.Contains("class=\"entry entry-flow\"", html); + Assert.Contains("class=\"item-flow\"", html); + Assert.Contains("grid-template-columns:34% minmax(0,66%)", html); + Assert.Contains("overflow-wrap:anywhere", html); + Assert.Contains("<script>alert(1)</script>", html); + Assert.DoesNotContain("