docs(test): make Given/When/Then the default BDD structure (WP-71)
bdd.mdx previously banned "Given/When/Then ceremony" outright, which
directly contradicted WP-70's own acceptance tests (Acceptance/
BesluitLifecycleTests.cs already used // Given/When/Then comments) and
the backend's organically-evolved PascalCase_snake_sentence convention,
which the doc gave zero guidance for. Reverses that rule: every test is
now structured Given -> When -> Then, with a genuinely empty phase
omitted rather than faked; present-tense declarative naming and the
one-behaviour-per-test rule are unchanged. ADR-0006 gets a cross-reference
so both documents agree everywhere, not just in acceptance tests.
Also closes out the doc's other named-but-unenforced rules found by the
audit: fixes the 5 files asserting rendered $localize copy instead of
the underlying tag/message-id (the compliant pattern already existed in
werkvoorraad-item-view.spec.ts), splits the multi-behaviour titles the
doc itself calls a smell (";", "and", "/"), and fixes bdd.mdx's own false
citation of registratie-wizard.machine.spec.ts as "one transition per
test" by actually splitting that test into one-transition-per-test.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@ import { Result } from '@shared/kernel/fp';
|
||||
import { Brief, BriefDecisions, CaseContext, LetterBlock } from '@brief/domain/brief';
|
||||
import { OrgTemplate } from '@brief/domain/org-template';
|
||||
import { BriefAdapter, BriefView } from '@brief/infrastructure/brief.adapter';
|
||||
import { LetterPreviewAdapter } from '@brief/infrastructure/letter-preview.adapter';
|
||||
import { LetterPreviewAdapter, PREVIEW_FAILED } from '@brief/infrastructure/letter-preview.adapter';
|
||||
import { RevealBigNummerAdapter } from '@brief/infrastructure/reveal-bignummer.adapter';
|
||||
import { BriefStore } from './brief.store';
|
||||
|
||||
@@ -164,25 +164,64 @@ async function loadedStore(over: Partial<BriefAdapter> = {}): Promise<BriefStore
|
||||
}
|
||||
|
||||
describe('BriefStore undo/redo history', () => {
|
||||
it('records an edit, undoes and redoes it; buttons mirror; a no-op edit is not recorded', async () => {
|
||||
it('starts with nothing to undo', async () => {
|
||||
// Given a freshly loaded brief.
|
||||
// When no edit has happened yet...
|
||||
const store = await loadedStore();
|
||||
expect(store.canUndo()).toBe(false);
|
||||
|
||||
// Then there is nothing to undo.
|
||||
expect(store.canUndo()).toBe(false);
|
||||
});
|
||||
|
||||
it('records an edit and makes it undoable', async () => {
|
||||
// Given a loaded brief with one block.
|
||||
const store = await loadedStore();
|
||||
|
||||
// When a block is removed...
|
||||
store.edit({ tag: 'BlockRemoved', blockId: 'local-1' });
|
||||
|
||||
// Then the block is gone and the edit becomes undoable.
|
||||
expect(loadedBrief(store).sections[0].blocks.length).toBe(0);
|
||||
expect(store.canUndo()).toBe(true);
|
||||
});
|
||||
|
||||
it('undo reverts the edit and enables redo', async () => {
|
||||
// Given a brief with one recorded edit (a removed block).
|
||||
const store = await loadedStore();
|
||||
store.edit({ tag: 'BlockRemoved', blockId: 'local-1' });
|
||||
|
||||
// When the edit is undone...
|
||||
store.undo();
|
||||
|
||||
// Then the block is back, and redo becomes available.
|
||||
expect(loadedBrief(store).sections[0].blocks.length).toBe(1);
|
||||
expect(store.canRedo()).toBe(true);
|
||||
});
|
||||
|
||||
it('redo reapplies the undone edit', async () => {
|
||||
// Given an edit that was undone.
|
||||
const store = await loadedStore();
|
||||
store.edit({ tag: 'BlockRemoved', blockId: 'local-1' });
|
||||
store.undo();
|
||||
|
||||
// When it is redone...
|
||||
store.redo();
|
||||
expect(loadedBrief(store).sections[0].blocks.length).toBe(0);
|
||||
|
||||
// A no-op edit (unknown block) changes nothing → leaves no dead history step.
|
||||
// Then the edit is reapplied.
|
||||
expect(loadedBrief(store).sections[0].blocks.length).toBe(0);
|
||||
});
|
||||
|
||||
it('a no-op edit does not clear the redo future', async () => {
|
||||
// Given an undone edit, with redo available.
|
||||
const store = await loadedStore();
|
||||
store.edit({ tag: 'BlockRemoved', blockId: 'local-1' });
|
||||
store.undo(); // back to 1 block, redo available
|
||||
|
||||
// When an edit that changes nothing (an unknown block) is applied...
|
||||
store.edit({ tag: 'BlockRemoved', blockId: 'does-not-exist' });
|
||||
expect(store.canRedo()).toBe(true); // future NOT cleared by a no-op
|
||||
|
||||
// Then the no-op leaves no dead history step — redo is still available.
|
||||
expect(store.canRedo()).toBe(true);
|
||||
});
|
||||
|
||||
it('a new edit clears the redo future', async () => {
|
||||
@@ -268,12 +307,12 @@ describe('BriefStore.previewLetter', () => {
|
||||
const open = vi.spyOn(window, 'open').mockImplementation(() => null);
|
||||
vi.spyOn(TestBed.inject(LetterPreviewAdapter), 'preview').mockResolvedValue({
|
||||
ok: false,
|
||||
error: 'De voorvertoning kon niet worden geopend.',
|
||||
error: PREVIEW_FAILED,
|
||||
});
|
||||
|
||||
await store.previewLetter();
|
||||
expect(open).not.toHaveBeenCalled();
|
||||
expect(store.lastError()).toBe('De voorvertoning kon niet worden geopend.');
|
||||
expect(store.lastError()).toBe(PREVIEW_FAILED);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user