test(frontend): replay real messages instead of hand-built state literals (WP-70)
Every machine spec redefined its own throwaway fixture helper (editing1/2/3,
editingWith), hardcoding fields like errors: {} that assert against shapes
the reducer may never actually produce. given(reduce, initial)(...msgs)
(libs/shared/src/testing/machine.ts) replaces them by replaying real Msgs
through the real reduce, so a fixture is provably reachable. Adds the same
idiom for value objects (unwrapOk) and RemoteData (loading/success/failure),
plus intake.acceptance.spec.ts as a worked full-journey example.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { machineRemoteData } from './machine-remote-data';
|
||||
import { loading, success } from '../testing/remote-data';
|
||||
|
||||
describe('machineRemoteData', () => {
|
||||
it('maps loading → Loading', () => {
|
||||
expect(machineRemoteData({ tag: 'loading' })).toEqual({ tag: 'Loading' });
|
||||
expect(machineRemoteData({ tag: 'loading' })).toEqual(loading());
|
||||
});
|
||||
|
||||
it('maps failed → Failure carrying an Error with the reason', () => {
|
||||
@@ -14,6 +15,6 @@ describe('machineRemoteData', () => {
|
||||
|
||||
it('maps loaded → Success carrying the whole loaded state', () => {
|
||||
const loaded = { tag: 'loaded', foo: 42 } as const;
|
||||
expect(machineRemoteData(loaded)).toEqual({ tag: 'Success', value: loaded });
|
||||
expect(machineRemoteData(loaded)).toEqual(success(loaded));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { RemoteData, map2, map } from './remote-data';
|
||||
import { loading, failure, success } from '../testing/remote-data';
|
||||
|
||||
const loading: RemoteData<string, number> = { tag: 'Loading' };
|
||||
const failure: RemoteData<string, number> = { tag: 'Failure', error: 'x' };
|
||||
const ok = (n: number): RemoteData<string, number> => ({ tag: 'Success', value: n });
|
||||
const loadingRd: RemoteData<string, number> = loading();
|
||||
const failureRd: RemoteData<string, number> = failure('x');
|
||||
const ok = (n: number): RemoteData<string, number> => success(n);
|
||||
|
||||
describe('RemoteData combinators', () => {
|
||||
it('map only touches Success', () => {
|
||||
const times10 = (n: number) => n * 10;
|
||||
expect(map(ok(2), times10)).toEqual({ tag: 'Success', value: 20 });
|
||||
expect(map(loading, times10)).toEqual(loading);
|
||||
expect(map(loadingRd, times10)).toEqual(loadingRd);
|
||||
});
|
||||
|
||||
it('map2 precedence: Failure > Loading > Success', () => {
|
||||
const add = (a: number, b: number) => a + b;
|
||||
expect(map2(failure, ok(1), add)).toEqual(failure); // a failed
|
||||
expect(map2(ok(1), failure, add)).toEqual(failure); // b failed
|
||||
expect(map2(loading, ok(1), add)).toEqual({ tag: 'Loading' });
|
||||
expect(map2(failureRd, ok(1), add)).toEqual(failureRd); // a failed
|
||||
expect(map2(ok(1), failureRd, add)).toEqual(failureRd); // b failed
|
||||
expect(map2(loadingRd, ok(1), add)).toEqual({ tag: 'Loading' });
|
||||
expect(map2(ok(2), ok(3), add)).toEqual({ tag: 'Success', value: 5 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Test-only DSL for the Elm-store idiom (CLAUDE.md #3). A fixture is built by
|
||||
* replaying real `Msg`s through the real `reduce` — never by hand-assembling a
|
||||
* state object field-by-field. That closes off illegal states the reducer would
|
||||
* never actually produce: if a spec can't reach a state via messages, it can't
|
||||
* assert on it either.
|
||||
*
|
||||
* `given(reduce, initial)` partially applies a machine's reducer + starting
|
||||
* state; the result is a variadic replay function a spec calls with the exact
|
||||
* message sequence a real user/flow would send.
|
||||
*/
|
||||
export const given =
|
||||
<S, M>(reduce: (s: S, m: M) => S, initial: S) =>
|
||||
(...msgs: M[]): S =>
|
||||
msgs.reduce(reduce, initial);
|
||||
@@ -0,0 +1,13 @@
|
||||
import { RemoteData } from '@shared/application/remote-data';
|
||||
|
||||
/**
|
||||
* Test-only constructors for `RemoteData` — one line per variant, so a spec
|
||||
* builds fixtures through the same tagged-union shape production code renders
|
||||
* (`foldRemote`/`<app-async>`), never a hand-rolled literal that could drift
|
||||
* from the real type.
|
||||
*/
|
||||
export const loading = <E = never, T = never>(): RemoteData<E, T> => ({ tag: 'Loading' });
|
||||
|
||||
export const success = <T, E = never>(value: T): RemoteData<E, T> => ({ tag: 'Success', value });
|
||||
|
||||
export const failure = <E, T = never>(error: E): RemoteData<E, T> => ({ tag: 'Failure', error });
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Result } from '@shared/kernel/fp';
|
||||
|
||||
/**
|
||||
* Unwrap a `Result` produced by a REAL `parse*` value-object parser, throwing
|
||||
* if it isn't `ok`. This is the only sanctioned way for a spec to obtain a
|
||||
* branded value-object type — it closes off the `'garbage' as Postcode` cast
|
||||
* route, since the only door to the branded type is the parser itself.
|
||||
*/
|
||||
export function unwrapOk<E, T>(result: Result<E, T>): T {
|
||||
if (!result.ok) {
|
||||
throw new Error(`unwrapOk: expected ok, got error: ${JSON.stringify(result.error)}`);
|
||||
}
|
||||
return result.value;
|
||||
}
|
||||
Reference in New Issue
Block a user