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.
89 lines
3.2 KiB
TypeScript
89 lines
3.2 KiB
TypeScript
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();
|
|
});
|
|
});
|