feat(fp): WP-07 — brief on the shared idioms + RemoteData MDX
Collapse brief.store's busy signal + nullable lastError into one Idle | Busy | Failed union (saveState gets matching tag-object style), and route brief.page's load through RemoteData + <app-async> instead of a hand-rolled @switch, via a BriefStore.remoteData projection of the machine's existing loading/failed tags -- the machine keeps owning the letter's own status lifecycle untouched. New brief.store.spec.ts covers the Busy->Idle/Failed transitions; new Foundations/RemoteData & Async MDX page documents the pattern and the WP-06 typed-loaded-slot fallback. Deviation from the original plan recorded in the WP file.
This commit is contained in:
88
src/app/brief/application/brief.store.spec.ts
Normal file
88
src/app/brief/application/brief.store.spec.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { Result } from '@shared/kernel/fp';
|
||||
import { Brief, BriefDecisions } from '@brief/domain/brief';
|
||||
import { BriefAdapter, BriefView } from '@brief/infrastructure/brief.adapter';
|
||||
import { BriefStore } from './brief.store';
|
||||
|
||||
const decisions: BriefDecisions = {
|
||||
canEdit: true,
|
||||
canApprove: true,
|
||||
canReject: true,
|
||||
canSend: true,
|
||||
};
|
||||
|
||||
const brief: Brief = {
|
||||
briefId: 'b1',
|
||||
beroep: 'arts',
|
||||
templateId: 't1',
|
||||
placeholders: [],
|
||||
sections: [],
|
||||
status: { tag: 'draft' },
|
||||
drafterId: 'u1',
|
||||
};
|
||||
|
||||
const view: BriefView = { brief, availablePassages: [], decisions };
|
||||
|
||||
function setup(adapter: Partial<BriefAdapter>): BriefStore {
|
||||
TestBed.configureTestingModule({ providers: [{ provide: BriefAdapter, useValue: adapter }] });
|
||||
return TestBed.inject(BriefStore);
|
||||
}
|
||||
|
||||
describe('BriefStore action state (Idle | Busy | Failed)', () => {
|
||||
it('is Busy synchronously once a transition starts, then Idle on success', 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();
|
||||
|
||||
const pending = store.approve();
|
||||
expect(store.busy()).toBe(true); // set synchronously, before any await resolves
|
||||
|
||||
await pending;
|
||||
expect(store.busy()).toBe(false);
|
||||
expect(store.lastError()).toBeNull();
|
||||
});
|
||||
|
||||
it('goes Busy then Failed on a failing transition, surfacing the error', async () => {
|
||||
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: false, error: 'niet toegestaan' }),
|
||||
});
|
||||
await store.load();
|
||||
|
||||
await store.approve();
|
||||
expect(store.busy()).toBe(false);
|
||||
expect(store.lastError()).toBe('niet toegestaan');
|
||||
});
|
||||
|
||||
it('a subsequent successful transition clears a prior Failed state', async () => {
|
||||
let approveResult: Result<string, BriefView> = { ok: false, error: 'eerste poging mislukt' };
|
||||
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(approveResult),
|
||||
});
|
||||
await store.load();
|
||||
|
||||
await store.approve();
|
||||
expect(store.lastError()).toBe('eerste poging mislukt');
|
||||
|
||||
approveResult = {
|
||||
ok: true,
|
||||
value: { ...view, brief: { ...brief, status: { tag: 'approved', approvedBy: 'u2', approvedAt: 't' } } },
|
||||
};
|
||||
await store.approve();
|
||||
expect(store.busy()).toBe(false);
|
||||
expect(store.lastError()).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user