style: format the repo with prettier (green format:check)
`npm run format:check` (a CI gate) had drifted red across 44 files — pre-existing files plus recently-added ones committed without formatting. Ran `prettier --write .`; no logic changes. Also regenerates documentation.json (compodoc reflects the reformatted component sources). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -108,13 +108,13 @@ the next person doesn't spend an afternoon re-deciding. (Deliberate CIBG-specifi
|
||||
live in [CIBG gaps](?path=/docs/foundations-cibg-gap-register--docs); the FE⇄DS "same shape, different
|
||||
context" cases in [Domain-driven design](?path=/docs/foundations-domain-driven-design--docs).)
|
||||
|
||||
| Pair | Why kept separate |
|
||||
| --- | --- |
|
||||
| `choice-link` vs `application-link` | Share the same `to`/`clickable`/`activate` navigation triad, but bind **different vendored patterns** — CIBG _Keuzelijst_ (`.keuzelijst__link`, `.stretched-link`) vs _Aanvragen_ (`.dashboard-block.applications li a`) — with different list/host semantics (`app-choice-link` renders an inner `<li>`; `application-link` **is** the `<li>`). Merging would fight the vendored CSS. Extract the shared triad into a mixin only if it grows. |
|
||||
| `text-input` / `radio-group` / `checkbox` | Share only the standard Angular **ControlValueAccessor** boilerplate (the `writeValue`/`registerOn*`/`setDisabledState` block). They render genuinely different controls, so they stay three atoms. A base CVA class is the only DRY move — a refactor, not a component merge, and not worth it at three. |
|
||||
| `button variant="subtle"` (`.btn-link`) vs `app-link` | A subtle button _looks_ like a link but is an **action** (`<button>`, emits click); `app-link` is **navigation** (`<a routerLink>`). Different semantics and a11y roles → keep both. |
|
||||
| `shell` / `page-shell` / `wizard-shell` | Three distinct jobs that **compose**, not overlap: persistent app chrome (mounted once) → routed page body → the wizard form/step frame. |
|
||||
| Raw `<h3>` in `application-link` vs the `heading` atom | The vendored `.applications li a h3` chain styles the **bare `<h3>`**; wrapping it in the `app-heading` host element would sit between the anchor and the h3 and can break that selector. This is the one sanctioned raw-heading; everywhere else uses `<app-heading [level]>`. |
|
||||
| Pair | Why kept separate |
|
||||
| ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `choice-link` vs `application-link` | Share the same `to`/`clickable`/`activate` navigation triad, but bind **different vendored patterns** — CIBG _Keuzelijst_ (`.keuzelijst__link`, `.stretched-link`) vs _Aanvragen_ (`.dashboard-block.applications li a`) — with different list/host semantics (`app-choice-link` renders an inner `<li>`; `application-link` **is** the `<li>`). Merging would fight the vendored CSS. Extract the shared triad into a mixin only if it grows. |
|
||||
| `text-input` / `radio-group` / `checkbox` | Share only the standard Angular **ControlValueAccessor** boilerplate (the `writeValue`/`registerOn*`/`setDisabledState` block). They render genuinely different controls, so they stay three atoms. A base CVA class is the only DRY move — a refactor, not a component merge, and not worth it at three. |
|
||||
| `button variant="subtle"` (`.btn-link`) vs `app-link` | A subtle button _looks_ like a link but is an **action** (`<button>`, emits click); `app-link` is **navigation** (`<a routerLink>`). Different semantics and a11y roles → keep both. |
|
||||
| `shell` / `page-shell` / `wizard-shell` | Three distinct jobs that **compose**, not overlap: persistent app chrome (mounted once) → routed page body → the wizard form/step frame. |
|
||||
| Raw `<h3>` in `application-link` vs the `heading` atom | The vendored `.applications li a h3` chain styles the **bare `<h3>`**; wrapping it in the `app-heading` host element would sit between the anchor and the h3 and can break that selector. This is the one sanctioned raw-heading; everywhere else uses `<app-heading [level]>`. |
|
||||
|
||||
Single-consumer shared blocks (e.g. `placeholder-chip`, `rich-text-editor`, `checkbox`, the
|
||||
`task-list`/`choice-list`/`choice-link` family) currently have one consumer each. They stay in
|
||||
|
||||
+13
-13
@@ -5,10 +5,10 @@ import { Meta } from '@storybook/addon-docs/blocks';
|
||||
# Behaviour-driven tests
|
||||
|
||||
Tests here read as **specifications of behaviour**, not checks of implementation. A test
|
||||
says what the system *does* — in the domain's own words — so a failing test names a broken
|
||||
says what the system _does_ — in the domain's own words — so a failing test names a broken
|
||||
behaviour, and the suite doubles as living documentation. This is the BDD half of the
|
||||
[Testing strategy](?path=/docs/foundations-testing-strategy--docs) (which owns *what to
|
||||
test, by layer*); BDD owns *how each test is phrased and scoped*.
|
||||
[Testing strategy](?path=/docs/foundations-testing-strategy--docs) (which owns _what to
|
||||
test, by layer_); BDD owns _how each test is phrased and scoped_.
|
||||
|
||||
## Three rules
|
||||
|
||||
@@ -25,20 +25,20 @@ describe('parsePostcode', () => {
|
||||
});
|
||||
```
|
||||
|
||||
Read top-to-bottom it *is* the spec: "parsePostcode — normalises to 1234 AB; rejects
|
||||
Read top-to-bottom it _is_ the spec: "parsePostcode — normalises to 1234 AB; rejects
|
||||
malformed input."
|
||||
|
||||
### 2. One behaviour per test
|
||||
|
||||
A test asserts **one behaviour**, not one `expect()`. Several assertions that pin down the
|
||||
*same* behaviour belong together; assertions about *different* behaviours belong apart.
|
||||
_same_ behaviour belong together; assertions about _different_ behaviours belong apart.
|
||||
|
||||
| Keep together (one behaviour) | Split apart (separate behaviours) |
|
||||
| --- | --- |
|
||||
| A `Result`'s `.ok` then its `.value` | The `ok` branch **and** the `err` branch of a transition |
|
||||
| A whole-object `toEqual` | An invalid-input case **and** a valid-input case |
|
||||
| A loop asserting one rule over many inputs | Two independent state transitions |
|
||||
| A truth-table (`draft` → true, `approver` → false) of one rule | An authorization check **and** a rendering check |
|
||||
| Keep together (one behaviour) | Split apart (separate behaviours) |
|
||||
| -------------------------------------------------------------- | -------------------------------------------------------- |
|
||||
| A `Result`'s `.ok` then its `.value` | The `ok` branch **and** the `err` branch of a transition |
|
||||
| A whole-object `toEqual` | An invalid-input case **and** a valid-input case |
|
||||
| A loop asserting one rule over many inputs | Two independent state transitions |
|
||||
| A truth-table (`draft` → true, `approver` → false) of one rule | An authorization check **and** a rendering check |
|
||||
|
||||
A title that needs `/`, `;`, "then" or "and" to join two behaviours is the smell — split it,
|
||||
and each half gets its own present-tense name.
|
||||
@@ -46,8 +46,8 @@ and each half gets its own present-tense name.
|
||||
### 3. Speak the ubiquitous language (the DDD tie-in)
|
||||
|
||||
Test names use the **domain vocabulary**, not technical jargon — the same words as the
|
||||
[bounded contexts](?path=/docs/foundations-domain-driven-design--docs): a *behandelaar*
|
||||
drafts, a *beoordelaar* approves, a *herregistratie* is *ingediend*. The test name is
|
||||
[bounded contexts](?path=/docs/foundations-domain-driven-design--docs): a _behandelaar_
|
||||
drafts, a _beoordelaar_ approves, a _herregistratie_ is _ingediend_. The test name is
|
||||
readable by someone who knows the domain but not the code.
|
||||
|
||||
```ts
|
||||
|
||||
+8
-8
@@ -11,13 +11,13 @@ third-party i18n library. The source locale is **`nl`**; a second locale is a
|
||||
|
||||
## How it's wired
|
||||
|
||||
| Piece | Where | What |
|
||||
| --- | --- | --- |
|
||||
| Source locale | `angular.json` → `i18n.sourceLocale` | `nl` — the language the code is written in |
|
||||
| Locales | `angular.json` → `i18n.locales.en` | points at `src/locale/messages.en.xlf` |
|
||||
| Missing-translation policy | `angular.json` → `i18nMissingTranslation` | `error` — a missing `<target>` fails the build |
|
||||
| Runtime global | `angular.json` → `polyfills` | `@angular/localize/init` provides `$localize` |
|
||||
| English build/serve | `angular.json` → `configurations.en` | `ng build --configuration=en`, `ng serve --configuration=en` |
|
||||
| Piece | Where | What |
|
||||
| -------------------------- | ----------------------------------------- | ------------------------------------------------------------ |
|
||||
| Source locale | `angular.json` → `i18n.sourceLocale` | `nl` — the language the code is written in |
|
||||
| Locales | `angular.json` → `i18n.locales.en` | points at `src/locale/messages.en.xlf` |
|
||||
| Missing-translation policy | `angular.json` → `i18nMissingTranslation` | `error` — a missing `<target>` fails the build |
|
||||
| Runtime global | `angular.json` → `polyfills` | `@angular/localize/init` provides `$localize` |
|
||||
| English build/serve | `angular.json` → `configurations.en` | `ng build --configuration=en`, `ng serve --configuration=en` |
|
||||
|
||||
Locale switching is **build-time**, not runtime: each locale is its own bundle. There is
|
||||
no in-app language picker (out of scope for the POC).
|
||||
@@ -70,7 +70,7 @@ forgotten target breaks the `en` build rather than silently shipping Dutch.
|
||||
|
||||
**Rule: never assert on rendered copy.** Copy is the thing that changes per locale and per
|
||||
edit — a test that reads `"Voer een geldige postcode in"` breaks the moment a translator or
|
||||
a product owner touches the wording, in every locale. Assert on what's *invariant* instead:
|
||||
a product owner touches the wording, in every locale. Assert on what's _invariant_ instead:
|
||||
|
||||
- **Parsers / value objects** — assert on the `Result` discriminant and the parsed value,
|
||||
not the error message. This is the existing house pattern
|
||||
|
||||
+22
-22
@@ -6,7 +6,7 @@ import { Meta } from '@storybook/addon-docs/blocks';
|
||||
|
||||
A paced, hands-on route through this codebase for a developer who is a **strong
|
||||
programmer but new to frontend functional programming**. The [Overview](?path=/docs/foundations-overview--docs)
|
||||
is the *map* — every idea, cross-linked. This is the *route*: what to read first, what
|
||||
is the _map_ — every idea, cross-linked. This is the _route_: what to read first, what
|
||||
to **do** to make it stick, and how to know you understood it. Work through it over
|
||||
roughly three days.
|
||||
|
||||
@@ -33,7 +33,7 @@ below is a way to stop the compiler letting you build a state that can't actuall
|
||||
**Goal:** name the failure mode this whole architecture is designed to prevent.
|
||||
|
||||
Most UI bugs are not wrong algorithms — they're **impossible states that the types
|
||||
allowed anyway**. `isLoading` true *and* `error` set *and* `data` present: three
|
||||
allowed anyway**. `isLoading` true _and_ `error` set _and_ `data` present: three
|
||||
booleans give eight combinations, but only four are real. The extra four are bugs
|
||||
waiting to be rendered. The reflex this codebase trains: when you reach for a second or
|
||||
third boolean to track one thing, model a **discriminated union** instead, so the
|
||||
@@ -61,13 +61,13 @@ Code is organised first by **bounded context** — a business capability with it
|
||||
language: `shared`, `auth`, `registratie`, `herregistratie`, `brief`, `showcase`. Inside
|
||||
each context are five **layers**, and dependencies only ever point **inward**:
|
||||
|
||||
| Layer | Job | Angular? |
|
||||
| --- | --- | --- |
|
||||
| `domain/` | business rules + data types | **No — pure TS**, has `.spec.ts` |
|
||||
| `application/` | coordinate state/tasks (stores, commands) | yes (signals) |
|
||||
| `infrastructure/` | where data comes from (HTTP) | yes |
|
||||
| `contracts/` | wire DTOs (the FE⇄BE seam) | no |
|
||||
| `ui/` | how it looks | yes |
|
||||
| Layer | Job | Angular? |
|
||||
| ----------------- | ----------------------------------------- | -------------------------------- |
|
||||
| `domain/` | business rules + data types | **No — pure TS**, has `.spec.ts` |
|
||||
| `application/` | coordinate state/tasks (stores, commands) | yes (signals) |
|
||||
| `infrastructure/` | where data comes from (HTTP) | yes |
|
||||
| `contracts/` | wire DTOs (the FE⇄BE seam) | no |
|
||||
| `ui/` | how it looks | yes |
|
||||
|
||||
`ui → application → domain`, never the reverse; `ui` never touches `infrastructure`
|
||||
directly. Cross-context is one-directional too: `herregistratie → registratie → shared`,
|
||||
@@ -120,7 +120,7 @@ Four tools do the heavy lifting. **Pure functions:** output depends only on inpu
|
||||
side effects — trivially testable, no mocks. **Immutability:** you compute new values,
|
||||
you don't mutate old ones, so nothing changes under you. **Unidirectional flow:** data
|
||||
moves one way (state → view → message → new state), never a tangle of two-way bindings.
|
||||
**Sum and product types:** a *product* is "A and B" (a record); a *sum* is "A **or** B"
|
||||
**Sum and product types:** a _product_ is "A and B" (a record); a _sum_ is "A **or** B"
|
||||
(a discriminated union) — sums are how you make illegal states unrepresentable.
|
||||
|
||||
Put together: the **functional core** is pure logic (all of `domain/`, the reducers, the
|
||||
@@ -149,13 +149,13 @@ returns the next model, the view re-renders. All wiring goes through one idiom,
|
||||
|
||||
The rule that keeps `reduce` pure: **side effects live in commands, not the reducer.** A
|
||||
command (`application/submit-*.ts`) does the HTTP, then dispatches a message describing
|
||||
the *outcome*. Reducer = "what the new state is"; command = "go do it, then say what
|
||||
the _outcome_. Reducer = "what the new state is"; command = "go do it, then say what
|
||||
happened." And **derive, don't store** anything you can compute — e.g. a wizard's visible
|
||||
steps are `visibleSteps(answers)`, not a stored field.
|
||||
|
||||
A field's value lands in the Model on **every keystroke** (not on blur — blur only marks
|
||||
the field "touched"); a separate 600 ms debounce off the model snapshot autosaves the
|
||||
draft to the backend, an effect that lives *outside* the reducer. See
|
||||
draft to the backend, an effect that lives _outside_ the reducer. See
|
||||
`docs/reference/architecture/ARCHITECTURE.md` §2g.
|
||||
|
||||
**Do:** run `/form-machine` for a toy single field (say a "nickname" field with a max
|
||||
@@ -172,7 +172,7 @@ does?
|
||||
**Goal:** replace loading/error/empty booleans with one four-state value.
|
||||
|
||||
`RemoteData<E,T>` is a sum type with exactly four cases: `Loading | Empty |
|
||||
Failure{error} | Success{value}`. That's the four *real* states from lesson 1.1, and no
|
||||
Failure{error} | Success{value}`. That's the four _real_ states from lesson 1.1, and no
|
||||
others — you literally cannot construct "loading and error." Combine sources with
|
||||
`map` / `map2` / `andThen` (precedence: Failure > Loading > Empty > Success), and render
|
||||
it with the `<app-async>` molecule, which picks one of four mutually-exclusive templates
|
||||
@@ -202,12 +202,12 @@ boundary `parse*` adapters in `infrastructure/` (the FE⇄BE seam, where untrust
|
||||
becomes domain types). "Validate" scatters `if`-checks everywhere and forgets one;
|
||||
"parse" concentrates the check at the door and lets the compiler enforce the rest.
|
||||
|
||||
**Why "brand"?** TypeScript is *structurally* typed, so a bare `type Postcode = string`
|
||||
**Why "brand"?** TypeScript is _structurally_ typed, so a bare `type Postcode = string`
|
||||
would accept any string and lose all proof of validation. Intersecting a phantom marker —
|
||||
`string & { readonly __brand: 'Postcode' }` — makes the type **nominal**: no plain string
|
||||
satisfies it, so the only way to hold a `Postcode` is to go through the parser that stamps
|
||||
the brand. The brand is compile-time proof the value was validated (it exists only in the
|
||||
types, never at runtime). The DDD name for the concept is a *value object*; "brand" is just
|
||||
types, never at runtime). The DDD name for the concept is a _value object_; "brand" is just
|
||||
the TypeScript trick that makes it enforceable.
|
||||
|
||||
**Do:** run `/value-object` for a small field (e.g. a Dutch phone number). Read the parser
|
||||
@@ -223,7 +223,7 @@ re-validate its format? Why not?
|
||||
|
||||
You've now seen it twice: **small honest pieces, each only allowed to reach one level
|
||||
down, with illegal combinations forbidden by structure.** Atomic design applies it to
|
||||
*components* (atoms compose upward); The Elm Architecture applies it to *state* (pure
|
||||
_components_ (atoms compose upward); The Elm Architecture applies it to _state_ (pure
|
||||
`reduce` composes messages into models). They are the same principle at two scales — that
|
||||
is the thesis of this codebase. Read `docs/reference/fp-tea-atomic-design.md` Part 5; it's
|
||||
the "aha" that ties Day 1 and Day 2 together.
|
||||
@@ -258,11 +258,11 @@ which gets a Storybook story instead?
|
||||
|
||||
`describe` names the subject; each `it` states **one observable behaviour** in
|
||||
present tense — no `should`, no Given/When/Then ceremony. One behaviour per test means one
|
||||
*behaviour*, not one `expect`: assertions pinning down the same behaviour stay together
|
||||
_behaviour_, not one `expect`: assertions pinning down the same behaviour stay together
|
||||
(a `Result`'s `.ok` then its `.value`); assertions about different behaviours split apart
|
||||
(the ok branch **and** the err branch). If a title needs "and"/"then"/"/" to join two
|
||||
things, that's the smell — split it. And speak the **ubiquitous language**: a *behandelaar*
|
||||
drafts, a *beoordelaar* approves — the same words as the bounded contexts.
|
||||
things, that's the smell — split it. And speak the **ubiquitous language**: a _behandelaar_
|
||||
drafts, a _beoordelaar_ approves — the same words as the bounded contexts.
|
||||
|
||||
**Do:** read `registratie/domain/registratie-wizard.machine.spec.ts` — one transition per
|
||||
test, each named as a behaviour. (You saw this style get enforced when the specs were
|
||||
@@ -338,7 +338,7 @@ live swatches; then skim the [CIBG gap register](?path=/docs/foundations-cibg-ga
|
||||
Two framing ideas first. **BFF-lite + decision DTOs (ADR-0001):** each screen gets one
|
||||
screen-shaped endpoint returning a **decision-enriched** DTO — the backend computes the
|
||||
business rules, and **the FE renders decisions, it does not recompute them.** Per rule you
|
||||
pick a *decision flag* (server sends the boolean) or a *config value* (server sends the
|
||||
pick a _decision flag_ (server sends the boolean) or a _config value_ (server sends the
|
||||
threshold, FE applies it for instant feedback, server re-validates as authority). The FE
|
||||
keeps only **format** validation, never as authority.
|
||||
|
||||
@@ -358,8 +358,8 @@ skills in pipeline order:
|
||||
|
||||
`/new-feature` walks the whole pipeline if you'd rather do it in one guided pass.
|
||||
|
||||
**Check yourself:** for your slice, name for each business rule whether it's a *decision
|
||||
flag* or a *config value*, and which layer owns it. If a rule lives in two layers, which
|
||||
**Check yourself:** for your slice, name for each business rule whether it's a _decision
|
||||
flag_ or a _config value_, and which layer owns it. If a rule lives in two layers, which
|
||||
one is the **authority**?
|
||||
|
||||
**Go deeper:** `docs/reference/architecture/0001-bff-lite-decision-dtos.md`;
|
||||
|
||||
@@ -15,7 +15,7 @@ cross-linked take on one idea; the long-form source lives in `docs/reference/`
|
||||
## Architecture & domain
|
||||
|
||||
- [Domain-driven design](?path=/docs/foundations-domain-driven-design--docs) — bounded
|
||||
contexts + layers, dependencies pointing inward (the folder structure *is* the architecture).
|
||||
contexts + layers, dependencies pointing inward (the folder structure _is_ the architecture).
|
||||
- [Atomic design](?path=/docs/foundations-atomic-design--docs) — Atoms → Molecules →
|
||||
Organisms → Templates; a new page is composition, not new building blocks.
|
||||
|
||||
|
||||
+11
-11
@@ -6,21 +6,21 @@ import { Meta } from '@storybook/addon-docs/blocks';
|
||||
|
||||
Tests follow the same grain as the architecture: **push the logic down to where it's pure,
|
||||
test it there directly, and keep the layers above thin.** No single tool covers everything,
|
||||
so each layer gets the cheapest test that catches its class of bug. This page owns *what to
|
||||
test, by layer*; how each test is **phrased and scoped** — one behaviour, in the domain's
|
||||
so each layer gets the cheapest test that catches its class of bug. This page owns _what to
|
||||
test, by layer_; how each test is **phrased and scoped** — one behaviour, in the domain's
|
||||
language — is [BDD](?path=/docs/foundations-bdd--docs).
|
||||
|
||||
## What gets tested where
|
||||
|
||||
| Layer | Test kind | Tool | Rule |
|
||||
| --- | --- | --- | --- |
|
||||
| `domain/` | pure-function unit spec | Vitest | **Required.** Call the function directly — **no `TestBed`**. Reducers, combinators, `visibleSteps`, parsers, policies. |
|
||||
| `domain/value-objects/` | parser spec | Vitest | Happy path + normalisation + **each** rejection. Assert on the `Result`, never the message. |
|
||||
| `infrastructure/` `parse*` | trust-boundary spec | Vitest | Accept a valid DTO; **reject `null` / `{}` / malformed**. Name it `describe('… (trust boundary)')`. |
|
||||
| `application/` | store / command spec | Vitest | Reducer purity, optimistic begin→confirm/rollback, command `Result`. |
|
||||
| `ui/` | Storybook story | Storybook + a11y | Kept thin. Axe runs on every story; add a `play` only for wiring axe can't see. |
|
||||
| flows | e2e smoke | Playwright | One happy path + one error state per critical journey. |
|
||||
| backend | rule + endpoint + golden | xUnit | Mirror of the FE domain rules, plus `WebApplicationFactory` integration. |
|
||||
| Layer | Test kind | Tool | Rule |
|
||||
| -------------------------- | ------------------------ | ---------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
||||
| `domain/` | pure-function unit spec | Vitest | **Required.** Call the function directly — **no `TestBed`**. Reducers, combinators, `visibleSteps`, parsers, policies. |
|
||||
| `domain/value-objects/` | parser spec | Vitest | Happy path + normalisation + **each** rejection. Assert on the `Result`, never the message. |
|
||||
| `infrastructure/` `parse*` | trust-boundary spec | Vitest | Accept a valid DTO; **reject `null` / `{}` / malformed**. Name it `describe('… (trust boundary)')`. |
|
||||
| `application/` | store / command spec | Vitest | Reducer purity, optimistic begin→confirm/rollback, command `Result`. |
|
||||
| `ui/` | Storybook story | Storybook + a11y | Kept thin. Axe runs on every story; add a `play` only for wiring axe can't see. |
|
||||
| flows | e2e smoke | Playwright | One happy path + one error state per critical journey. |
|
||||
| backend | rule + endpoint + golden | xUnit | Mirror of the FE domain rules, plus `WebApplicationFactory` integration. |
|
||||
|
||||
## Tooling
|
||||
|
||||
|
||||
Reference in New Issue
Block a user