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:
eho
2026-07-20 20:33:25 +02:00
co-authored by Claude Opus 4.8
parent 5cae44f163
commit 55a0a2d166
48 changed files with 178 additions and 34 deletions
+18 -2
View File
@@ -54,7 +54,7 @@ function setup(adapter: Partial<BriefAdapter>): 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<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.lastError()).toBeNull();
});
+23 -5
View File
@@ -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' });
@@ -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');
});
});
@@ -34,15 +34,27 @@ describe('change-request reduce', () => {
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' }), {
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');
});
@@ -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');
});
+6 -2
View File
@@ -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);
});