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 <noreply@anthropic.com>
This commit is contained in:
@@ -44,7 +44,7 @@ public class BriefEndpointTests(TestWebApplicationFactory factory) : IClassFixtu
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[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();
|
var brief = await Get();
|
||||||
Assert.Equal("draft", brief.Status.Tag);
|
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");
|
var kern = brief.Sections.Single(s => s.SectionKey == "kern");
|
||||||
Assert.False(kern.Locked);
|
Assert.False(kern.Locked);
|
||||||
Assert.Empty(kern.Blocks);
|
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<BriefViewDto>("/api/v1/brief");
|
var view = await _client.GetFromJsonAsync<BriefViewDto>("/api/v1/brief");
|
||||||
// global passages + the arts-scoped one; no other-beroep passages leak in.
|
// global passages + the arts-scoped one; no other-beroep passages leak in.
|
||||||
Assert.Contains(view!.AvailablePassages, p => p.PassageId == "p-kern-arts");
|
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.
|
// 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 == "positief");
|
||||||
Assert.Contains(view.AvailablePassages, p => p.Besluit == "negatief" && p.Reason == "onvoldoende_scholing");
|
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<BriefViewDto>("/api/v1/brief");
|
||||||
// Case context is joined onto the screen DTO for the behandel scherm header.
|
// 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.
|
// 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.Equal("arts", view.CaseContext.Beroep);
|
||||||
Assert.False(string.IsNullOrWhiteSpace(view.CaseContext.ZorgverlenerNaam));
|
Assert.False(string.IsNullOrWhiteSpace(view.CaseContext.ZorgverlenerNaam));
|
||||||
Assert.False(string.IsNullOrWhiteSpace(view.CaseContext.AanvraagReferentie));
|
Assert.False(string.IsNullOrWhiteSpace(view.CaseContext.AanvraagReferentie));
|
||||||
@@ -125,12 +136,17 @@ public class BriefEndpointTests(TestWebApplicationFactory factory) : IClassFixtu
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[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();
|
await Get();
|
||||||
// Nothing filled yet → required sections empty → 409.
|
// Nothing filled yet → required sections empty → 409.
|
||||||
Assert.Equal(HttpStatusCode.Conflict, (await _client.SendAsync(Post("/api/v1/brief/submit"))).StatusCode);
|
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<BriefViewDto>("/api/v1/brief"))!.Brief;
|
var brief = (await _client.GetFromJsonAsync<BriefViewDto>("/api/v1/brief"))!.Brief;
|
||||||
await _client.PutAsJsonAsync("/api/v1/brief", FilledFrom(brief));
|
await _client.PutAsJsonAsync("/api/v1/brief", FilledFrom(brief));
|
||||||
|
|
||||||
@@ -156,7 +172,7 @@ public class BriefEndpointTests(TestWebApplicationFactory factory) : IClassFixtu
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Reject_returns_comments_and_editing_reopens_to_draft()
|
public async Task Reject_returns_comments()
|
||||||
{
|
{
|
||||||
var brief = await Get();
|
var brief = await Get();
|
||||||
await _client.PutAsJsonAsync("/api/v1/brief", FilledFrom(brief));
|
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<BriefViewDto>();
|
Post("/api/v1/brief/reject", role: "approver", body: new RejectBriefRequest("Graag aanvullen.")))).Content.ReadFromJsonAsync<BriefViewDto>();
|
||||||
Assert.Equal("rejected", rejected!.Brief.Status.Tag);
|
Assert.Equal("rejected", rejected!.Brief.Status.Tag);
|
||||||
Assert.Equal("Graag aanvullen.", rejected.Brief.Status.Comments);
|
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.
|
// A drafter save on a rejected letter reopens it to draft.
|
||||||
var reopened = await (await _client.PutAsJsonAsync("/api/v1/brief", FilledFrom(brief))).Content.ReadFromJsonAsync<BriefViewDto>();
|
var reopened = await (await _client.PutAsJsonAsync("/api/v1/brief", FilledFrom(brief))).Content.ReadFromJsonAsync<BriefViewDto>();
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ public class OrgTemplateEndpointTests(TestWebApplicationFactory factory) : IClas
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Publish_increments_version_appends_history_and_counts_unsent_briefs()
|
public async Task Publish_increments_the_version()
|
||||||
{
|
{
|
||||||
ResetStores();
|
ResetStores();
|
||||||
// One unsent brief for this sub-org (GetOrCreate on first read).
|
// One unsent brief for this sub-org (GetOrCreate on first read).
|
||||||
@@ -65,16 +65,42 @@ public class OrgTemplateEndpointTests(TestWebApplicationFactory factory) : IClas
|
|||||||
res.EnsureSuccessStatusCode();
|
res.EnsureSuccessStatusCode();
|
||||||
var published = await res.Content.ReadFromJsonAsync<PublishOrgTemplateResponse>();
|
var published = await res.Content.ReadFromJsonAsync<PublishOrgTemplateResponse>();
|
||||||
Assert.Equal(2, published!.Version);
|
Assert.Equal(2, published!.Version);
|
||||||
Assert.Equal(1, published.AffectedUnsentBriefs);
|
|
||||||
|
|
||||||
var view = await AdminView();
|
var view = await AdminView();
|
||||||
Assert.Equal(2, view.PublishedVersion);
|
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));
|
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<PublishOrgTemplateResponse>();
|
||||||
|
Assert.Equal(1, published!.AffectedUnsentBriefs);
|
||||||
|
|
||||||
|
var view = await AdminView();
|
||||||
Assert.Equal(1, view.UnsentBriefs);
|
Assert.Equal(1, view.UnsentBriefs);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Save_draft_validates_margins_and_round_trips()
|
public async Task Save_draft_validates_margins()
|
||||||
{
|
{
|
||||||
ResetStores();
|
ResetStores();
|
||||||
var draft = (await AdminView()).Draft;
|
var draft = (await AdminView()).Draft;
|
||||||
@@ -83,6 +109,13 @@ public class OrgTemplateEndpointTests(TestWebApplicationFactory factory) : IClas
|
|||||||
Assert.Equal(HttpStatusCode.BadRequest, (await _client.SendAsync(
|
Assert.Equal(HttpStatusCode.BadRequest, (await _client.SendAsync(
|
||||||
Req(HttpMethod.Put, $"/api/v1/admin/org-template/{Registers}", role: "admin",
|
Req(HttpMethod.Put, $"/api/v1/admin/org-template/{Registers}", role: "admin",
|
||||||
body: new SaveOrgTemplateRequest(invalid)))).StatusCode);
|
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 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",
|
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);
|
Req(HttpMethod.Post, $"/api/v1/admin/org-template/{Registers}/rollback/99", role: "admin"))).StatusCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
// Walk one brief all the way to sent under template v1, then republish the org
|
||||||
public async Task Sent_brief_keeps_its_pinned_template_while_an_unsent_brief_follows_a_republish()
|
// 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();
|
ResetStores();
|
||||||
// Walk one brief to sent under template v1.
|
|
||||||
var brief = (await _client.GetFromJsonAsync<BriefViewDto>("/api/v1/brief"))!.Brief;
|
var brief = (await _client.GetFromJsonAsync<BriefViewDto>("/api/v1/brief"))!.Brief;
|
||||||
var filled = brief.Sections
|
var filled = brief.Sections
|
||||||
.Select(s => new LetterSectionDto(s.SectionKey, s.Title, s.Required,
|
.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",
|
await _client.SendAsync(Req(HttpMethod.Put, $"/api/v1/admin/org-template/{Registers}", role: "admin",
|
||||||
body: new SaveOrgTemplateRequest(draft with { OrgName = "Hertitelde organisatie" })));
|
body: new SaveOrgTemplateRequest(draft with { OrgName = "Hertitelde organisatie" })));
|
||||||
await _client.SendAsync(Req(HttpMethod.Post, $"/api/v1/admin/org-template/{Registers}/publish", role: "admin"));
|
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<BriefViewDto>("/api/v1/brief");
|
var sentView = await _client.GetFromJsonAsync<BriefViewDto>("/api/v1/brief");
|
||||||
Assert.Equal(1, sentView!.OrgTemplate.Version);
|
Assert.Equal(1, sentView!.OrgTemplate.Version);
|
||||||
Assert.Equal("BIG-register", sentView.OrgTemplate.OrgName);
|
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<BriefViewDto>();
|
var freshView = await (await _client.SendAsync(Req(HttpMethod.Post, "/api/v1/brief/reset"))).Content.ReadFromJsonAsync<BriefViewDto>();
|
||||||
Assert.Equal(2, freshView!.OrgTemplate.Version);
|
Assert.Equal(2, freshView!.OrgTemplate.Version);
|
||||||
Assert.Equal("Hertitelde organisatie", freshView.OrgTemplate.OrgName);
|
Assert.Equal("Hertitelde organisatie", freshView.OrgTemplate.OrgName);
|
||||||
|
|||||||
@@ -81,12 +81,17 @@ public class PreviewEndpointTests(TestWebApplicationFactory factory) : IClassFix
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Proefbrief_is_admin_only_and_renders_the_draft_template()
|
public async Task Proefbrief_is_admin_only()
|
||||||
{
|
{
|
||||||
ResetStores();
|
ResetStores();
|
||||||
Assert.Equal(HttpStatusCode.Forbidden,
|
Assert.Equal(HttpStatusCode.Forbidden,
|
||||||
(await _client.GetAsync($"/api/v1/admin/org-template/{Registers}/preview")).StatusCode);
|
(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"));
|
var res = await _client.SendAsync(Req(HttpMethod.Get, $"/api/v1/admin/org-template/{Registers}/preview", role: "admin"));
|
||||||
res.EnsureSuccessStatusCode();
|
res.EnsureSuccessStatusCode();
|
||||||
var html = await res.Content.ReadAsStringAsync();
|
var html = await res.Content.ReadAsStringAsync();
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ function setup(adapter: Partial<BriefAdapter>): BriefStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('BriefStore action state (Idle | Busy | Failed)', () => {
|
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 = {
|
const approved: BriefView = {
|
||||||
...view,
|
...view,
|
||||||
brief: { ...brief, status: { tag: 'approved', approvedBy: 'u2', approvedAt: 't' } },
|
brief: { ...brief, status: { tag: 'approved', approvedBy: 'u2', approvedAt: 't' } },
|
||||||
@@ -70,7 +70,23 @@ describe('BriefStore action state (Idle | Busy | Failed)', () => {
|
|||||||
const pending = store.approve();
|
const pending = store.approve();
|
||||||
expect(store.busy()).toBe(true); // set synchronously, before any await resolves
|
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<Result<string, BriefView>> => Promise.resolve({ ok: true, value: view }),
|
||||||
|
save: (): Promise<Result<string, BriefView>> => Promise.resolve({ ok: true, value: view }),
|
||||||
|
approve: (): Promise<Result<string, BriefView>> =>
|
||||||
|
Promise.resolve({ ok: true, value: approved }),
|
||||||
|
});
|
||||||
|
await store.load();
|
||||||
|
|
||||||
|
await store.approve();
|
||||||
expect(store.busy()).toBe(false);
|
expect(store.busy()).toBe(false);
|
||||||
expect(store.lastError()).toBeNull();
|
expect(store.lastError()).toBeNull();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ const passageIds = (s: BriefState, key: string) =>
|
|||||||
.map((b) => (b.type === 'passage' ? b.sourcePassageId : ''));
|
.map((b) => (b.type === 'passage' ? b.sourcePassageId : ''));
|
||||||
|
|
||||||
describe('brief.machine reduce', () => {
|
describe('brief.machine reduce', () => {
|
||||||
it('BriefLoaded / BriefLoadFailed / Seed set state directly', () => {
|
it('BriefLoaded moves loading to loaded', () => {
|
||||||
expect(
|
expect(
|
||||||
reduce(initialLoading(), {
|
reduce(initialLoading(), {
|
||||||
tag: 'BriefLoaded',
|
tag: 'BriefLoaded',
|
||||||
@@ -92,10 +92,16 @@ describe('brief.machine reduce', () => {
|
|||||||
decisions,
|
decisions,
|
||||||
}).tag,
|
}).tag,
|
||||||
).toBe('loaded');
|
).toBe('loaded');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('BriefLoadFailed moves loading to failed with the reason', () => {
|
||||||
expect(reduce(initialLoading(), { tag: 'BriefLoadFailed', reason: 'x' })).toEqual({
|
expect(reduce(initialLoading(), { tag: 'BriefLoadFailed', reason: 'x' })).toEqual({
|
||||||
tag: 'failed',
|
tag: 'failed',
|
||||||
reason: 'x',
|
reason: 'x',
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Seed sets the state directly', () => {
|
||||||
const seeded = loaded();
|
const seeded = loaded();
|
||||||
expect(reduce(initialLoading(), { tag: 'Seed', state: seeded })).toBe(seeded);
|
expect(reduce(initialLoading(), { tag: 'Seed', state: seeded })).toBe(seeded);
|
||||||
});
|
});
|
||||||
@@ -150,10 +156,14 @@ describe('brief.machine reduce', () => {
|
|||||||
expect(block.content).toEqual(text('aangepast'));
|
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
|
let s = reduce(loaded(), besluit('positief')); // local-1 intro, local-2 pos
|
||||||
s = reduce(s, { tag: 'BlockMovedWithinSection', blockId: 'local-1', toIndex: 1 });
|
s = reduce(s, { tag: 'BlockMovedWithinSection', blockId: 'local-1', toIndex: 1 });
|
||||||
expect(sectionBlocks(s, 'kern').map((b) => b.blockId)).toEqual(['local-2', 'local-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' });
|
s = reduce(s, { tag: 'BlockRemoved', blockId: 'local-2' });
|
||||||
expect(sectionBlocks(s, 'kern').map((b) => b.blockId)).toEqual(['local-1']);
|
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' });
|
const submitted = loaded({ tag: 'submitted', submittedBy: 'u1', submittedAt: 't' });
|
||||||
// approve from draft is a no-op
|
// approve from draft is a no-op
|
||||||
expect(reduce(loaded(), { tag: 'Approved', by: 'u2', at: 't', decisions })).toEqual(loaded());
|
expect(reduce(loaded(), { tag: 'Approved', by: 'u2', at: 't', decisions })).toEqual(loaded());
|
||||||
@@ -226,7 +236,10 @@ describe('brief.machine reduce', () => {
|
|||||||
approvedBy: 'u2',
|
approvedBy: 'u2',
|
||||||
approvedAt: 't2',
|
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, {
|
const rejected = reduce(submitted, {
|
||||||
tag: 'Rejected',
|
tag: 'Rejected',
|
||||||
by: 'u2',
|
by: 'u2',
|
||||||
@@ -240,7 +253,12 @@ describe('brief.machine reduce', () => {
|
|||||||
rejectedAt: 't2',
|
rejectedAt: 't2',
|
||||||
comments: 'nee',
|
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);
|
expect(reduce(submitted, { tag: 'Sent', at: 't', decisions })).toBe(submitted);
|
||||||
const sent = reduce(approved, { tag: 'Sent', at: 't3', decisions });
|
const sent = reduce(approved, { tag: 'Sent', at: 't3', decisions });
|
||||||
expect(sent.tag === 'loaded' && sent.brief.status).toEqual({ tag: 'sent', sentAt: 't3' });
|
expect(sent.tag === 'loaded' && sent.brief.status).toEqual({ tag: 'sent', sentAt: 't3' });
|
||||||
|
|||||||
@@ -136,9 +136,13 @@ describe('submit', () => {
|
|||||||
expect((withScholing as any).data.punten).toBe(200);
|
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));
|
const submitting = submit(answering(complete));
|
||||||
expect(resolve(submitting, ok(undefined)).tag).toBe('Submitted');
|
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');
|
expect(resolve(submitting, err('boom')).tag).toBe('Failed');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -34,15 +34,27 @@ describe('change-request reduce', () => {
|
|||||||
expect((s as Extract<ChangeRequestState, { tag: 'Submitting' }>).data.postcode).toBe('2514 EA');
|
expect((s as Extract<ChangeRequestState, { tag: 'Submitting' }>).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' }), {
|
const submitting = reduce(editingWith({ straat: 'A 1', postcode: '2514 EA' }), {
|
||||||
tag: 'Submit',
|
tag: 'Submit',
|
||||||
});
|
});
|
||||||
const ok = reduce(submitting, { tag: 'SubmitConfirmed', referentie: 'BIG-2026-1' });
|
const ok = reduce(submitting, { tag: 'SubmitConfirmed', referentie: 'BIG-2026-1' });
|
||||||
expect(ok).toMatchObject({ tag: 'Submitted', 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' });
|
const failed = reduce(submitting, { tag: 'SubmitFailed', error: 'boom' });
|
||||||
expect(failed).toMatchObject({ tag: 'Failed', 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');
|
expect(reduce(failed, { tag: 'Retry' }).tag).toBe('Submitting');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -187,8 +187,11 @@ describe('manual diploma fallback', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('submit', () => {
|
describe('submit', () => {
|
||||||
it('reaches Indienen ONLY with a complete, valid draft', () => {
|
it('stays in Invullen when the draft is incomplete (no diploma)', () => {
|
||||||
expect(submit(invullen(validAdres)).tag).toBe('Invullen'); // 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));
|
const good = submit(invullen(validDraft));
|
||||||
expect(good.tag).toBe('Indienen');
|
expect(good.tag).toBe('Indienen');
|
||||||
expect((good as any).data.beroep).toBe('Arts');
|
expect((good as any).data.beroep).toBe('Arts');
|
||||||
@@ -196,11 +199,14 @@ describe('submit', () => {
|
|||||||
expect((good as any).data.adresHerkomst).toBe('brp');
|
expect((good as any).data.adresHerkomst).toBe('brp');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('resolve maps Indienen to Ingediend (with referentie) / Mislukt', () => {
|
it('resolve maps Indienen to Ingediend with the referentie', () => {
|
||||||
const indienen = submit(invullen(validDraft));
|
const ingediend = resolve(submit(invullen(validDraft)), ok('BIG-2026-001'));
|
||||||
expect(resolve(indienen, ok('BIG-2026-001')).tag).toBe('Ingediend');
|
expect(ingediend.tag).toBe('Ingediend');
|
||||||
expect((resolve(indienen, ok('BIG-2026-001')) as any).referentie).toBe('BIG-2026-001');
|
expect((ingediend as any).referentie).toBe('BIG-2026-001');
|
||||||
expect(resolve(indienen, err('boom')).tag).toBe('Mislukt');
|
});
|
||||||
|
|
||||||
|
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');
|
expect(s.tag).toBe('Ingediend');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('SubmitFailed then Retry returns to Indienen with the same data', () => {
|
it('SubmitFailed moves Indienen to Mislukt', () => {
|
||||||
let s = reduce(reduce(invullen(validDraft), { tag: 'Submit' }), {
|
const s = reduce(reduce(invullen(validDraft), { tag: 'Submit' }), {
|
||||||
tag: 'SubmitFailed',
|
tag: 'SubmitFailed',
|
||||||
error: 'boom',
|
error: 'boom',
|
||||||
});
|
});
|
||||||
expect(s.tag).toBe('Mislukt');
|
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.tag).toBe('Indienen');
|
||||||
expect((s as any).data.beroep).toBe('Arts');
|
expect((s as any).data.beroep).toBe('Arts');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -242,9 +242,13 @@ describe('satisfaction helpers', () => {
|
|||||||
expect(categorySatisfied(s, 'diploma')).toBe(true);
|
expect(categorySatisfied(s, 'diploma')).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('an active upload satisfies; a failed one does not', () => {
|
it('an active upload satisfies a required category', () => {
|
||||||
let s = select(stateWith([cat()]), 'diploma', 'u1');
|
const s = select(stateWith([cat()]), 'diploma', 'u1');
|
||||||
expect(categorySatisfied(s, 'diploma')).toBe(true);
|
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' });
|
s = reduceUpload(s, { type: 'UploadFailed', localId: 'u1', reason: 'x' });
|
||||||
expect(categorySatisfied(s, 'diploma')).toBe(false);
|
expect(categorySatisfied(s, 'diploma')).toBe(false);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user