From 55a0a2d166866b859de4f5961252c61791b20bdd Mon Sep 17 00:00:00 2001 From: Edwin van den Houdt Date: Mon, 20 Jul 2026 20:33:25 +0200 Subject: [PATCH] test: split multi-assertion specs into single-behavior tests One behavior per test across FE machine/store specs and backend endpoint tests, so a failure names exactly what broke. Co-Authored-By: Claude Opus 4.8 --- .../BigRegister.Tests/BriefEndpointTests.cs | 34 ++++++++-- .../OrgTemplateEndpointTests.cs | 62 ++++++++++++++++--- .../BigRegister.Tests/PreviewEndpointTests.cs | 7 ++- docs/{ => project}/SHOWCASE-ROADMAP.md | 0 docs/{ => project}/backlog/README.md | 0 .../backlog/WP-01-axe-ci-gate.md | 0 .../backlog/WP-02-check-tokens.md | 0 .../backlog/WP-03-contracts-purity.md | 0 .../backlog/WP-04-ui-not-infrastructure.md | 0 .../backlog/WP-05-parse-boundaries.md | 0 .../backlog/WP-06-typed-async.md | 0 .../backlog/WP-07-brief-idioms.md | 0 .../backlog/WP-08-store-idiom.md | 0 .../{ => project}/backlog/WP-09-pure-logic.md | 0 .../backlog/WP-10-button-fidelity.md | 0 .../backlog/WP-11-markup-fidelity.md | 0 docs/{ => project}/backlog/WP-12-datablock.md | 0 .../backlog/WP-13-cibg-gap-register.md | 0 .../backlog/WP-14-storybook-taxonomy.md | 0 .../backlog/WP-15-missing-stories.md | 0 .../backlog/WP-16-component-a11y.md | 0 docs/{ => project}/backlog/WP-17-app-a11y.md | 0 .../backlog/WP-18-abac-capability-spine.md | 0 docs/{ => project}/backlog/WP-19-e2e-smoke.md | 0 .../backlog/WP-20-second-locale.md | 0 .../backlog/WP-21-resilience-seams.md | 0 .../backlog/WP-22-durable-persistence.md | 0 .../backlog/WP-23-org-template-backend.md | 0 .../backlog/WP-24-letter-canvas.md | 0 .../backlog/WP-25-letter-preview-html.md | 0 .../backlog/WP-26-org-template-editor.md | 0 .../backlog/WP-27-brief-ux-layer.md | 0 .../backlog/WP-28-brief-v2-demo-polish.md | 0 .../0001-mijn-aanvragen-en-wizardstatus.md | 0 .../0002-attribute-based-access-control.md | 0 .../0001-bff-lite-decision-dtos.md | 0 .../0002-user-groups-and-bounded-contexts.md | 0 .../architecture/0003-cibg-huisstijl.md | 0 .../architecture}/ARCHITECTURE.md | 0 docs/{ => reference}/fp-tea-atomic-design.md | 0 docs/{ => reference}/ui-ux-audit.md | 0 docs/{ => reference}/wcag-checklist.md | 0 src/app/brief/application/brief.store.spec.ts | 20 +++++- src/app/brief/domain/brief.machine.spec.ts | 28 +++++++-- .../domain/intake.machine.spec.ts | 6 +- .../domain/change-request.machine.spec.ts | 14 ++++- .../domain/registratie-wizard.machine.spec.ts | 33 +++++++--- src/app/shared/upload/upload.machine.spec.ts | 8 ++- 48 files changed, 178 insertions(+), 34 deletions(-) rename docs/{ => project}/SHOWCASE-ROADMAP.md (100%) rename docs/{ => project}/backlog/README.md (100%) rename docs/{ => project}/backlog/WP-01-axe-ci-gate.md (100%) rename docs/{ => project}/backlog/WP-02-check-tokens.md (100%) rename docs/{ => project}/backlog/WP-03-contracts-purity.md (100%) rename docs/{ => project}/backlog/WP-04-ui-not-infrastructure.md (100%) rename docs/{ => project}/backlog/WP-05-parse-boundaries.md (100%) rename docs/{ => project}/backlog/WP-06-typed-async.md (100%) rename docs/{ => project}/backlog/WP-07-brief-idioms.md (100%) rename docs/{ => project}/backlog/WP-08-store-idiom.md (100%) rename docs/{ => project}/backlog/WP-09-pure-logic.md (100%) rename docs/{ => project}/backlog/WP-10-button-fidelity.md (100%) rename docs/{ => project}/backlog/WP-11-markup-fidelity.md (100%) rename docs/{ => project}/backlog/WP-12-datablock.md (100%) rename docs/{ => project}/backlog/WP-13-cibg-gap-register.md (100%) rename docs/{ => project}/backlog/WP-14-storybook-taxonomy.md (100%) rename docs/{ => project}/backlog/WP-15-missing-stories.md (100%) rename docs/{ => project}/backlog/WP-16-component-a11y.md (100%) rename docs/{ => project}/backlog/WP-17-app-a11y.md (100%) rename docs/{ => project}/backlog/WP-18-abac-capability-spine.md (100%) rename docs/{ => project}/backlog/WP-19-e2e-smoke.md (100%) rename docs/{ => project}/backlog/WP-20-second-locale.md (100%) rename docs/{ => project}/backlog/WP-21-resilience-seams.md (100%) rename docs/{ => project}/backlog/WP-22-durable-persistence.md (100%) rename docs/{ => project}/backlog/WP-23-org-template-backend.md (100%) rename docs/{ => project}/backlog/WP-24-letter-canvas.md (100%) rename docs/{ => project}/backlog/WP-25-letter-preview-html.md (100%) rename docs/{ => project}/backlog/WP-26-org-template-editor.md (100%) rename docs/{ => project}/backlog/WP-27-brief-ux-layer.md (100%) rename docs/{ => project}/backlog/WP-28-brief-v2-demo-polish.md (100%) rename docs/{ => project}/prd/0001-mijn-aanvragen-en-wizardstatus.md (100%) rename docs/{ => project}/prd/0002-attribute-based-access-control.md (100%) rename docs/{ => reference}/architecture/0001-bff-lite-decision-dtos.md (100%) rename docs/{ => reference}/architecture/0002-user-groups-and-bounded-contexts.md (100%) rename docs/{ => reference}/architecture/0003-cibg-huisstijl.md (100%) rename docs/{ => reference/architecture}/ARCHITECTURE.md (100%) rename docs/{ => reference}/fp-tea-atomic-design.md (100%) rename docs/{ => reference}/ui-ux-audit.md (100%) rename docs/{ => reference}/wcag-checklist.md (100%) diff --git a/backend/tests/BigRegister.Tests/BriefEndpointTests.cs b/backend/tests/BigRegister.Tests/BriefEndpointTests.cs index 6125c88..8b46c25 100644 --- a/backend/tests/BigRegister.Tests/BriefEndpointTests.cs +++ b/backend/tests/BigRegister.Tests/BriefEndpointTests.cs @@ -44,7 +44,7 @@ public class BriefEndpointTests(TestWebApplicationFactory factory) : IClassFixtu } [Fact] - public async Task Get_creates_a_draft_from_the_template_with_scoped_passages() + public async Task Get_creates_a_draft_with_expected_sections_locked_and_empty() { var brief = await Get(); Assert.Equal("draft", brief.Status.Tag); @@ -57,7 +57,12 @@ public class BriefEndpointTests(TestWebApplicationFactory factory) : IClassFixtu var kern = brief.Sections.Single(s => s.SectionKey == "kern"); Assert.False(kern.Locked); Assert.Empty(kern.Blocks); + } + [Fact] + public async Task Get_offers_only_global_and_arts_scoped_besluit_tagged_passages() + { + await Get(); var view = await _client.GetFromJsonAsync("/api/v1/brief"); // global passages + the arts-scoped one; no other-beroep passages leak in. Assert.Contains(view!.AvailablePassages, p => p.PassageId == "p-kern-arts"); @@ -66,10 +71,16 @@ public class BriefEndpointTests(TestWebApplicationFactory factory) : IClassFixtu // Guided-drafting tags (WP-brief-v3): positief + negatief + reason-specific negatief. Assert.Contains(view.AvailablePassages, p => p.Besluit == "positief"); Assert.Contains(view.AvailablePassages, p => p.Besluit == "negatief" && p.Reason == "onvoldoende_scholing"); + } + [Fact] + public async Task Get_joins_the_case_context_with_the_BIG_nummer_masked() + { + await Get(); + var view = await _client.GetFromJsonAsync("/api/v1/brief"); // Case context is joined onto the screen DTO for the behandel scherm header. // The BIG-nummer ships MASKED by default (PRD-0002 ยง5c) โ€” reveal is a separate call. - Assert.Equal("********601", view.CaseContext.BigNummer); + Assert.Equal("********601", view!.CaseContext.BigNummer); Assert.Equal("arts", view.CaseContext.Beroep); Assert.False(string.IsNullOrWhiteSpace(view.CaseContext.ZorgverlenerNaam)); Assert.False(string.IsNullOrWhiteSpace(view.CaseContext.AanvraagReferentie)); @@ -125,12 +136,17 @@ public class BriefEndpointTests(TestWebApplicationFactory factory) : IClassFixtu } [Fact] - public async Task Submit_blocks_on_empty_required_section_then_succeeds_when_filled() + public async Task Submit_blocks_on_empty_required_section() { await Get(); // Nothing filled yet โ†’ required sections empty โ†’ 409. Assert.Equal(HttpStatusCode.Conflict, (await _client.SendAsync(Post("/api/v1/brief/submit"))).StatusCode); + } + [Fact] + public async Task Submit_succeeds_when_required_sections_filled() + { + await Get(); var brief = (await _client.GetFromJsonAsync("/api/v1/brief"))!.Brief; await _client.PutAsJsonAsync("/api/v1/brief", FilledFrom(brief)); @@ -156,7 +172,7 @@ public class BriefEndpointTests(TestWebApplicationFactory factory) : IClassFixtu } [Fact] - public async Task Reject_returns_comments_and_editing_reopens_to_draft() + public async Task Reject_returns_comments() { var brief = await Get(); await _client.PutAsJsonAsync("/api/v1/brief", FilledFrom(brief)); @@ -166,6 +182,16 @@ public class BriefEndpointTests(TestWebApplicationFactory factory) : IClassFixtu Post("/api/v1/brief/reject", role: "approver", body: new RejectBriefRequest("Graag aanvullen.")))).Content.ReadFromJsonAsync(); Assert.Equal("rejected", rejected!.Brief.Status.Tag); Assert.Equal("Graag aanvullen.", rejected.Brief.Status.Comments); + } + + [Fact] + public async Task Editing_a_rejected_letter_reopens_it_to_draft() + { + var brief = await Get(); + await _client.PutAsJsonAsync("/api/v1/brief", FilledFrom(brief)); + await _client.SendAsync(Post("/api/v1/brief/submit")); + await _client.SendAsync( + Post("/api/v1/brief/reject", role: "approver", body: new RejectBriefRequest("Graag aanvullen."))); // A drafter save on a rejected letter reopens it to draft. var reopened = await (await _client.PutAsJsonAsync("/api/v1/brief", FilledFrom(brief))).Content.ReadFromJsonAsync(); diff --git a/backend/tests/BigRegister.Tests/OrgTemplateEndpointTests.cs b/backend/tests/BigRegister.Tests/OrgTemplateEndpointTests.cs index 234dd83..cff2c06 100644 --- a/backend/tests/BigRegister.Tests/OrgTemplateEndpointTests.cs +++ b/backend/tests/BigRegister.Tests/OrgTemplateEndpointTests.cs @@ -55,7 +55,7 @@ public class OrgTemplateEndpointTests(TestWebApplicationFactory factory) : IClas } [Fact] - public async Task Publish_increments_version_appends_history_and_counts_unsent_briefs() + public async Task Publish_increments_the_version() { ResetStores(); // One unsent brief for this sub-org (GetOrCreate on first read). @@ -65,16 +65,42 @@ public class OrgTemplateEndpointTests(TestWebApplicationFactory factory) : IClas res.EnsureSuccessStatusCode(); var published = await res.Content.ReadFromJsonAsync(); Assert.Equal(2, published!.Version); - Assert.Equal(1, published.AffectedUnsentBriefs); var view = await AdminView(); Assert.Equal(2, view.PublishedVersion); + } + + [Fact] + public async Task Publish_appends_to_the_version_history() + { + ResetStores(); + await _client.GetAsync("/api/v1/brief"); + + var res = await _client.SendAsync(Req(HttpMethod.Post, $"/api/v1/admin/org-template/{Registers}/publish", role: "admin")); + res.EnsureSuccessStatusCode(); + + var view = await AdminView(); Assert.Equal(new[] { 1, 2 }, view.History.Select(h => h.Version)); + } + + [Fact] + public async Task Publish_counts_the_unsent_briefs_it_affects() + { + ResetStores(); + // One unsent brief for this sub-org (GetOrCreate on first read). + await _client.GetAsync("/api/v1/brief"); + + var res = await _client.SendAsync(Req(HttpMethod.Post, $"/api/v1/admin/org-template/{Registers}/publish", role: "admin")); + res.EnsureSuccessStatusCode(); + var published = await res.Content.ReadFromJsonAsync(); + Assert.Equal(1, published!.AffectedUnsentBriefs); + + var view = await AdminView(); Assert.Equal(1, view.UnsentBriefs); } [Fact] - public async Task Save_draft_validates_margins_and_round_trips() + public async Task Save_draft_validates_margins() { ResetStores(); var draft = (await AdminView()).Draft; @@ -83,6 +109,13 @@ public class OrgTemplateEndpointTests(TestWebApplicationFactory factory) : IClas Assert.Equal(HttpStatusCode.BadRequest, (await _client.SendAsync( Req(HttpMethod.Put, $"/api/v1/admin/org-template/{Registers}", role: "admin", body: new SaveOrgTemplateRequest(invalid)))).StatusCode); + } + + [Fact] + public async Task Save_draft_round_trips_the_edited_values() + { + ResetStores(); + var draft = (await AdminView()).Draft; var valid = draft with { OrgName = "BIG-register (nieuw)", Margins = new MarginsDto(30, 20, 20, 25) }; var res = await _client.SendAsync(Req(HttpMethod.Put, $"/api/v1/admin/org-template/{Registers}", role: "admin", @@ -115,11 +148,12 @@ public class OrgTemplateEndpointTests(TestWebApplicationFactory factory) : IClas Req(HttpMethod.Post, $"/api/v1/admin/org-template/{Registers}/rollback/99", role: "admin"))).StatusCode); } - [Fact] - public async Task Sent_brief_keeps_its_pinned_template_while_an_unsent_brief_follows_a_republish() + // Walk one brief all the way to sent under template v1, then republish the org + // template under a new name. Both invariant tests below share this exact + // precondition (the stores are process-global, so each sets it up). + private async Task WalkBriefToSentThenRepublish() { ResetStores(); - // Walk one brief to sent under template v1. var brief = (await _client.GetFromJsonAsync("/api/v1/brief"))!.Brief; var filled = brief.Sections .Select(s => new LetterSectionDto(s.SectionKey, s.Title, s.Required, @@ -138,13 +172,25 @@ public class OrgTemplateEndpointTests(TestWebApplicationFactory factory) : IClas await _client.SendAsync(Req(HttpMethod.Put, $"/api/v1/admin/org-template/{Registers}", role: "admin", body: new SaveOrgTemplateRequest(draft with { OrgName = "Hertitelde organisatie" }))); await _client.SendAsync(Req(HttpMethod.Post, $"/api/v1/admin/org-template/{Registers}/publish", role: "admin")); + } - // The sent brief still renders v1 with the old name (immutable)... + [Fact] + public async Task Sent_brief_keeps_its_pinned_template_after_a_republish() + { + await WalkBriefToSentThenRepublish(); + + // The sent brief still renders v1 with the old name (immutable). var sentView = await _client.GetFromJsonAsync("/api/v1/brief"); Assert.Equal(1, sentView!.OrgTemplate.Version); Assert.Equal("BIG-register", sentView.OrgTemplate.OrgName); + } - // ...while a fresh (unsent) brief follows the new published version. + [Fact] + public async Task Unsent_brief_follows_a_republish() + { + await WalkBriefToSentThenRepublish(); + + // A fresh (unsent) brief follows the new published version. var freshView = await (await _client.SendAsync(Req(HttpMethod.Post, "/api/v1/brief/reset"))).Content.ReadFromJsonAsync(); Assert.Equal(2, freshView!.OrgTemplate.Version); Assert.Equal("Hertitelde organisatie", freshView.OrgTemplate.OrgName); diff --git a/backend/tests/BigRegister.Tests/PreviewEndpointTests.cs b/backend/tests/BigRegister.Tests/PreviewEndpointTests.cs index 111b058..62369ec 100644 --- a/backend/tests/BigRegister.Tests/PreviewEndpointTests.cs +++ b/backend/tests/BigRegister.Tests/PreviewEndpointTests.cs @@ -81,12 +81,17 @@ public class PreviewEndpointTests(TestWebApplicationFactory factory) : IClassFix } [Fact] - public async Task Proefbrief_is_admin_only_and_renders_the_draft_template() + public async Task Proefbrief_is_admin_only() { ResetStores(); Assert.Equal(HttpStatusCode.Forbidden, (await _client.GetAsync($"/api/v1/admin/org-template/{Registers}/preview")).StatusCode); + } + [Fact] + public async Task Proefbrief_renders_the_draft_template_with_a_watermark() + { + ResetStores(); var res = await _client.SendAsync(Req(HttpMethod.Get, $"/api/v1/admin/org-template/{Registers}/preview", role: "admin")); res.EnsureSuccessStatusCode(); var html = await res.Content.ReadAsStringAsync(); diff --git a/docs/SHOWCASE-ROADMAP.md b/docs/project/SHOWCASE-ROADMAP.md similarity index 100% rename from docs/SHOWCASE-ROADMAP.md rename to docs/project/SHOWCASE-ROADMAP.md diff --git a/docs/backlog/README.md b/docs/project/backlog/README.md similarity index 100% rename from docs/backlog/README.md rename to docs/project/backlog/README.md diff --git a/docs/backlog/WP-01-axe-ci-gate.md b/docs/project/backlog/WP-01-axe-ci-gate.md similarity index 100% rename from docs/backlog/WP-01-axe-ci-gate.md rename to docs/project/backlog/WP-01-axe-ci-gate.md diff --git a/docs/backlog/WP-02-check-tokens.md b/docs/project/backlog/WP-02-check-tokens.md similarity index 100% rename from docs/backlog/WP-02-check-tokens.md rename to docs/project/backlog/WP-02-check-tokens.md diff --git a/docs/backlog/WP-03-contracts-purity.md b/docs/project/backlog/WP-03-contracts-purity.md similarity index 100% rename from docs/backlog/WP-03-contracts-purity.md rename to docs/project/backlog/WP-03-contracts-purity.md diff --git a/docs/backlog/WP-04-ui-not-infrastructure.md b/docs/project/backlog/WP-04-ui-not-infrastructure.md similarity index 100% rename from docs/backlog/WP-04-ui-not-infrastructure.md rename to docs/project/backlog/WP-04-ui-not-infrastructure.md diff --git a/docs/backlog/WP-05-parse-boundaries.md b/docs/project/backlog/WP-05-parse-boundaries.md similarity index 100% rename from docs/backlog/WP-05-parse-boundaries.md rename to docs/project/backlog/WP-05-parse-boundaries.md diff --git a/docs/backlog/WP-06-typed-async.md b/docs/project/backlog/WP-06-typed-async.md similarity index 100% rename from docs/backlog/WP-06-typed-async.md rename to docs/project/backlog/WP-06-typed-async.md diff --git a/docs/backlog/WP-07-brief-idioms.md b/docs/project/backlog/WP-07-brief-idioms.md similarity index 100% rename from docs/backlog/WP-07-brief-idioms.md rename to docs/project/backlog/WP-07-brief-idioms.md diff --git a/docs/backlog/WP-08-store-idiom.md b/docs/project/backlog/WP-08-store-idiom.md similarity index 100% rename from docs/backlog/WP-08-store-idiom.md rename to docs/project/backlog/WP-08-store-idiom.md diff --git a/docs/backlog/WP-09-pure-logic.md b/docs/project/backlog/WP-09-pure-logic.md similarity index 100% rename from docs/backlog/WP-09-pure-logic.md rename to docs/project/backlog/WP-09-pure-logic.md diff --git a/docs/backlog/WP-10-button-fidelity.md b/docs/project/backlog/WP-10-button-fidelity.md similarity index 100% rename from docs/backlog/WP-10-button-fidelity.md rename to docs/project/backlog/WP-10-button-fidelity.md diff --git a/docs/backlog/WP-11-markup-fidelity.md b/docs/project/backlog/WP-11-markup-fidelity.md similarity index 100% rename from docs/backlog/WP-11-markup-fidelity.md rename to docs/project/backlog/WP-11-markup-fidelity.md diff --git a/docs/backlog/WP-12-datablock.md b/docs/project/backlog/WP-12-datablock.md similarity index 100% rename from docs/backlog/WP-12-datablock.md rename to docs/project/backlog/WP-12-datablock.md diff --git a/docs/backlog/WP-13-cibg-gap-register.md b/docs/project/backlog/WP-13-cibg-gap-register.md similarity index 100% rename from docs/backlog/WP-13-cibg-gap-register.md rename to docs/project/backlog/WP-13-cibg-gap-register.md diff --git a/docs/backlog/WP-14-storybook-taxonomy.md b/docs/project/backlog/WP-14-storybook-taxonomy.md similarity index 100% rename from docs/backlog/WP-14-storybook-taxonomy.md rename to docs/project/backlog/WP-14-storybook-taxonomy.md diff --git a/docs/backlog/WP-15-missing-stories.md b/docs/project/backlog/WP-15-missing-stories.md similarity index 100% rename from docs/backlog/WP-15-missing-stories.md rename to docs/project/backlog/WP-15-missing-stories.md diff --git a/docs/backlog/WP-16-component-a11y.md b/docs/project/backlog/WP-16-component-a11y.md similarity index 100% rename from docs/backlog/WP-16-component-a11y.md rename to docs/project/backlog/WP-16-component-a11y.md diff --git a/docs/backlog/WP-17-app-a11y.md b/docs/project/backlog/WP-17-app-a11y.md similarity index 100% rename from docs/backlog/WP-17-app-a11y.md rename to docs/project/backlog/WP-17-app-a11y.md diff --git a/docs/backlog/WP-18-abac-capability-spine.md b/docs/project/backlog/WP-18-abac-capability-spine.md similarity index 100% rename from docs/backlog/WP-18-abac-capability-spine.md rename to docs/project/backlog/WP-18-abac-capability-spine.md diff --git a/docs/backlog/WP-19-e2e-smoke.md b/docs/project/backlog/WP-19-e2e-smoke.md similarity index 100% rename from docs/backlog/WP-19-e2e-smoke.md rename to docs/project/backlog/WP-19-e2e-smoke.md diff --git a/docs/backlog/WP-20-second-locale.md b/docs/project/backlog/WP-20-second-locale.md similarity index 100% rename from docs/backlog/WP-20-second-locale.md rename to docs/project/backlog/WP-20-second-locale.md diff --git a/docs/backlog/WP-21-resilience-seams.md b/docs/project/backlog/WP-21-resilience-seams.md similarity index 100% rename from docs/backlog/WP-21-resilience-seams.md rename to docs/project/backlog/WP-21-resilience-seams.md diff --git a/docs/backlog/WP-22-durable-persistence.md b/docs/project/backlog/WP-22-durable-persistence.md similarity index 100% rename from docs/backlog/WP-22-durable-persistence.md rename to docs/project/backlog/WP-22-durable-persistence.md diff --git a/docs/backlog/WP-23-org-template-backend.md b/docs/project/backlog/WP-23-org-template-backend.md similarity index 100% rename from docs/backlog/WP-23-org-template-backend.md rename to docs/project/backlog/WP-23-org-template-backend.md diff --git a/docs/backlog/WP-24-letter-canvas.md b/docs/project/backlog/WP-24-letter-canvas.md similarity index 100% rename from docs/backlog/WP-24-letter-canvas.md rename to docs/project/backlog/WP-24-letter-canvas.md diff --git a/docs/backlog/WP-25-letter-preview-html.md b/docs/project/backlog/WP-25-letter-preview-html.md similarity index 100% rename from docs/backlog/WP-25-letter-preview-html.md rename to docs/project/backlog/WP-25-letter-preview-html.md diff --git a/docs/backlog/WP-26-org-template-editor.md b/docs/project/backlog/WP-26-org-template-editor.md similarity index 100% rename from docs/backlog/WP-26-org-template-editor.md rename to docs/project/backlog/WP-26-org-template-editor.md diff --git a/docs/backlog/WP-27-brief-ux-layer.md b/docs/project/backlog/WP-27-brief-ux-layer.md similarity index 100% rename from docs/backlog/WP-27-brief-ux-layer.md rename to docs/project/backlog/WP-27-brief-ux-layer.md diff --git a/docs/backlog/WP-28-brief-v2-demo-polish.md b/docs/project/backlog/WP-28-brief-v2-demo-polish.md similarity index 100% rename from docs/backlog/WP-28-brief-v2-demo-polish.md rename to docs/project/backlog/WP-28-brief-v2-demo-polish.md diff --git a/docs/prd/0001-mijn-aanvragen-en-wizardstatus.md b/docs/project/prd/0001-mijn-aanvragen-en-wizardstatus.md similarity index 100% rename from docs/prd/0001-mijn-aanvragen-en-wizardstatus.md rename to docs/project/prd/0001-mijn-aanvragen-en-wizardstatus.md diff --git a/docs/prd/0002-attribute-based-access-control.md b/docs/project/prd/0002-attribute-based-access-control.md similarity index 100% rename from docs/prd/0002-attribute-based-access-control.md rename to docs/project/prd/0002-attribute-based-access-control.md diff --git a/docs/architecture/0001-bff-lite-decision-dtos.md b/docs/reference/architecture/0001-bff-lite-decision-dtos.md similarity index 100% rename from docs/architecture/0001-bff-lite-decision-dtos.md rename to docs/reference/architecture/0001-bff-lite-decision-dtos.md diff --git a/docs/architecture/0002-user-groups-and-bounded-contexts.md b/docs/reference/architecture/0002-user-groups-and-bounded-contexts.md similarity index 100% rename from docs/architecture/0002-user-groups-and-bounded-contexts.md rename to docs/reference/architecture/0002-user-groups-and-bounded-contexts.md diff --git a/docs/architecture/0003-cibg-huisstijl.md b/docs/reference/architecture/0003-cibg-huisstijl.md similarity index 100% rename from docs/architecture/0003-cibg-huisstijl.md rename to docs/reference/architecture/0003-cibg-huisstijl.md diff --git a/docs/ARCHITECTURE.md b/docs/reference/architecture/ARCHITECTURE.md similarity index 100% rename from docs/ARCHITECTURE.md rename to docs/reference/architecture/ARCHITECTURE.md diff --git a/docs/fp-tea-atomic-design.md b/docs/reference/fp-tea-atomic-design.md similarity index 100% rename from docs/fp-tea-atomic-design.md rename to docs/reference/fp-tea-atomic-design.md diff --git a/docs/ui-ux-audit.md b/docs/reference/ui-ux-audit.md similarity index 100% rename from docs/ui-ux-audit.md rename to docs/reference/ui-ux-audit.md diff --git a/docs/wcag-checklist.md b/docs/reference/wcag-checklist.md similarity index 100% rename from docs/wcag-checklist.md rename to docs/reference/wcag-checklist.md diff --git a/src/app/brief/application/brief.store.spec.ts b/src/app/brief/application/brief.store.spec.ts index 59ab573..c77c320 100644 --- a/src/app/brief/application/brief.store.spec.ts +++ b/src/app/brief/application/brief.store.spec.ts @@ -54,7 +54,7 @@ function setup(adapter: Partial): BriefStore { } describe('BriefStore action state (Idle | Busy | Failed)', () => { - it('is Busy synchronously once a transition starts, then Idle on success', async () => { + it('is Busy synchronously once a transition starts', async () => { const approved: BriefView = { ...view, brief: { ...brief, status: { tag: 'approved', approvedBy: 'u2', approvedAt: 't' } }, @@ -70,7 +70,23 @@ describe('BriefStore action state (Idle | Busy | Failed)', () => { const pending = store.approve(); expect(store.busy()).toBe(true); // set synchronously, before any await resolves - await pending; + await pending; // settle before the test ends + }); + + it('settles to Idle on a successful transition', async () => { + const approved: BriefView = { + ...view, + brief: { ...brief, status: { tag: 'approved', approvedBy: 'u2', approvedAt: 't' } }, + }; + const store = setup({ + load: (): Promise> => Promise.resolve({ ok: true, value: view }), + save: (): Promise> => Promise.resolve({ ok: true, value: view }), + approve: (): Promise> => + Promise.resolve({ ok: true, value: approved }), + }); + await store.load(); + + await store.approve(); expect(store.busy()).toBe(false); expect(store.lastError()).toBeNull(); }); diff --git a/src/app/brief/domain/brief.machine.spec.ts b/src/app/brief/domain/brief.machine.spec.ts index f8f9344..e37c46b 100644 --- a/src/app/brief/domain/brief.machine.spec.ts +++ b/src/app/brief/domain/brief.machine.spec.ts @@ -83,7 +83,7 @@ const passageIds = (s: BriefState, key: string) => .map((b) => (b.type === 'passage' ? b.sourcePassageId : '')); describe('brief.machine reduce', () => { - it('BriefLoaded / BriefLoadFailed / Seed set state directly', () => { + it('BriefLoaded moves loading to loaded', () => { expect( reduce(initialLoading(), { tag: 'BriefLoaded', @@ -92,10 +92,16 @@ describe('brief.machine reduce', () => { decisions, }).tag, ).toBe('loaded'); + }); + + it('BriefLoadFailed moves loading to failed with the reason', () => { expect(reduce(initialLoading(), { tag: 'BriefLoadFailed', reason: 'x' })).toEqual({ tag: 'failed', reason: 'x', }); + }); + + it('Seed sets the state directly', () => { const seeded = loaded(); expect(reduce(initialLoading(), { tag: 'Seed', state: seeded })).toBe(seeded); }); @@ -150,10 +156,14 @@ describe('brief.machine reduce', () => { expect(block.content).toEqual(text('aangepast')); }); - it('BlockRemoved and BlockMovedWithinSection reorder within a section', () => { + it('BlockMovedWithinSection reorders blocks within a section', () => { let s = reduce(loaded(), besluit('positief')); // local-1 intro, local-2 pos s = reduce(s, { tag: 'BlockMovedWithinSection', blockId: 'local-1', toIndex: 1 }); expect(sectionBlocks(s, 'kern').map((b) => b.blockId)).toEqual(['local-2', 'local-1']); + }); + + it('BlockRemoved drops a block from a section', () => { + let s = reduce(loaded(), besluit('positief')); // local-1 intro, local-2 pos s = reduce(s, { tag: 'BlockRemoved', blockId: 'local-2' }); expect(sectionBlocks(s, 'kern').map((b) => b.blockId)).toEqual(['local-1']); }); @@ -216,7 +226,7 @@ describe('brief.machine reduce', () => { }); }); - it('approve/reject fire only from submitted; send only from approved', () => { + it('approve fires only from submitted', () => { const submitted = loaded({ tag: 'submitted', submittedBy: 'u1', submittedAt: 't' }); // approve from draft is a no-op expect(reduce(loaded(), { tag: 'Approved', by: 'u2', at: 't', decisions })).toEqual(loaded()); @@ -226,7 +236,10 @@ describe('brief.machine reduce', () => { approvedBy: 'u2', approvedAt: 't2', }); - // reject carries comments + }); + + it('reject fires from submitted, carrying comments', () => { + const submitted = loaded({ tag: 'submitted', submittedBy: 'u1', submittedAt: 't' }); const rejected = reduce(submitted, { tag: 'Rejected', by: 'u2', @@ -240,7 +253,12 @@ describe('brief.machine reduce', () => { rejectedAt: 't2', comments: 'nee', }); - // send only from approved + }); + + it('send fires only from approved', () => { + const submitted = loaded({ tag: 'submitted', submittedBy: 'u1', submittedAt: 't' }); + const approved = reduce(submitted, { tag: 'Approved', by: 'u2', at: 't2', decisions }); + // send from submitted is a no-op expect(reduce(submitted, { tag: 'Sent', at: 't', decisions })).toBe(submitted); const sent = reduce(approved, { tag: 'Sent', at: 't3', decisions }); expect(sent.tag === 'loaded' && sent.brief.status).toEqual({ tag: 'sent', sentAt: 't3' }); diff --git a/src/app/herregistratie/domain/intake.machine.spec.ts b/src/app/herregistratie/domain/intake.machine.spec.ts index ded4ea8..86c4be9 100644 --- a/src/app/herregistratie/domain/intake.machine.spec.ts +++ b/src/app/herregistratie/domain/intake.machine.spec.ts @@ -136,9 +136,13 @@ describe('submit', () => { expect((withScholing as any).data.punten).toBe(200); }); - it('resolve maps Submitting to Submitted / Failed', () => { + it('resolve maps Submitting to Submitted on a successful submit', () => { const submitting = submit(answering(complete)); expect(resolve(submitting, ok(undefined)).tag).toBe('Submitted'); + }); + + it('resolve maps Submitting to Failed on a failed submit', () => { + const submitting = submit(answering(complete)); expect(resolve(submitting, err('boom')).tag).toBe('Failed'); }); }); diff --git a/src/app/registratie/domain/change-request.machine.spec.ts b/src/app/registratie/domain/change-request.machine.spec.ts index f44e497..1333067 100644 --- a/src/app/registratie/domain/change-request.machine.spec.ts +++ b/src/app/registratie/domain/change-request.machine.spec.ts @@ -34,15 +34,27 @@ describe('change-request reduce', () => { expect((s as Extract).data.postcode).toBe('2514 EA'); }); - it('confirms and fails only from Submitting; Retry re-submits a failure', () => { + it('SubmitConfirmed maps Submitting to Submitted with the referentie', () => { const submitting = reduce(editingWith({ straat: 'A 1', postcode: '2514 EA' }), { tag: 'Submit', }); const ok = reduce(submitting, { tag: 'SubmitConfirmed', referentie: 'BIG-2026-1' }); expect(ok).toMatchObject({ tag: 'Submitted', referentie: 'BIG-2026-1' }); + }); + it('SubmitFailed maps Submitting to Failed with the error', () => { + const submitting = reduce(editingWith({ straat: 'A 1', postcode: '2514 EA' }), { + tag: 'Submit', + }); const failed = reduce(submitting, { tag: 'SubmitFailed', error: 'boom' }); expect(failed).toMatchObject({ tag: 'Failed', error: 'boom' }); + }); + + it('Retry re-submits a failure', () => { + const submitting = reduce(editingWith({ straat: 'A 1', postcode: '2514 EA' }), { + tag: 'Submit', + }); + const failed = reduce(submitting, { tag: 'SubmitFailed', error: 'boom' }); expect(reduce(failed, { tag: 'Retry' }).tag).toBe('Submitting'); }); diff --git a/src/app/registratie/domain/registratie-wizard.machine.spec.ts b/src/app/registratie/domain/registratie-wizard.machine.spec.ts index d2cf6f6..a7a2209 100644 --- a/src/app/registratie/domain/registratie-wizard.machine.spec.ts +++ b/src/app/registratie/domain/registratie-wizard.machine.spec.ts @@ -187,8 +187,11 @@ describe('manual diploma fallback', () => { }); describe('submit', () => { - it('reaches Indienen ONLY with a complete, valid draft', () => { - expect(submit(invullen(validAdres)).tag).toBe('Invullen'); // no diploma + it('stays in Invullen when the draft is incomplete (no diploma)', () => { + expect(submit(invullen(validAdres)).tag).toBe('Invullen'); + }); + + it('reaches Indienen with a complete, valid draft, carrying its data', () => { const good = submit(invullen(validDraft)); expect(good.tag).toBe('Indienen'); expect((good as any).data.beroep).toBe('Arts'); @@ -196,11 +199,14 @@ describe('submit', () => { expect((good as any).data.adresHerkomst).toBe('brp'); }); - it('resolve maps Indienen to Ingediend (with referentie) / Mislukt', () => { - const indienen = submit(invullen(validDraft)); - expect(resolve(indienen, ok('BIG-2026-001')).tag).toBe('Ingediend'); - expect((resolve(indienen, ok('BIG-2026-001')) as any).referentie).toBe('BIG-2026-001'); - expect(resolve(indienen, err('boom')).tag).toBe('Mislukt'); + it('resolve maps Indienen to Ingediend with the referentie', () => { + const ingediend = resolve(submit(invullen(validDraft)), ok('BIG-2026-001')); + expect(ingediend.tag).toBe('Ingediend'); + expect((ingediend as any).referentie).toBe('BIG-2026-001'); + }); + + it('resolve maps Indienen to Mislukt on a failed submit', () => { + expect(resolve(submit(invullen(validDraft)), err('boom')).tag).toBe('Mislukt'); }); }); @@ -225,13 +231,20 @@ describe('reduce (message-driven happy path)', () => { expect(s.tag).toBe('Ingediend'); }); - it('SubmitFailed then Retry returns to Indienen with the same data', () => { - let s = reduce(reduce(invullen(validDraft), { tag: 'Submit' }), { + it('SubmitFailed moves Indienen to Mislukt', () => { + const s = reduce(reduce(invullen(validDraft), { tag: 'Submit' }), { tag: 'SubmitFailed', error: 'boom', }); expect(s.tag).toBe('Mislukt'); - s = reduce(s, { tag: 'Retry' }); + }); + + it('Retry returns Mislukt to Indienen with the same data', () => { + const mislukt = reduce(reduce(invullen(validDraft), { tag: 'Submit' }), { + tag: 'SubmitFailed', + error: 'boom', + }); + const s = reduce(mislukt, { tag: 'Retry' }); expect(s.tag).toBe('Indienen'); expect((s as any).data.beroep).toBe('Arts'); }); diff --git a/src/app/shared/upload/upload.machine.spec.ts b/src/app/shared/upload/upload.machine.spec.ts index 0268880..25542ba 100644 --- a/src/app/shared/upload/upload.machine.spec.ts +++ b/src/app/shared/upload/upload.machine.spec.ts @@ -242,9 +242,13 @@ describe('satisfaction helpers', () => { expect(categorySatisfied(s, 'diploma')).toBe(true); }); - it('an active upload satisfies; a failed one does not', () => { - let s = select(stateWith([cat()]), 'diploma', 'u1'); + it('an active upload satisfies a required category', () => { + const s = select(stateWith([cat()]), 'diploma', 'u1'); expect(categorySatisfied(s, 'diploma')).toBe(true); + }); + + it('a failed upload does not satisfy a required category', () => { + let s = select(stateWith([cat()]), 'diploma', 'u1'); s = reduceUpload(s, { type: 'UploadFailed', localId: 'u1', reason: 'x' }); expect(categorySatisfied(s, 'diploma')).toBe(false); });