Files
atomic-design-poc/apps/ssp/src/app/registratie/application/admin-cases.store.spec.ts
T
ehoandClaude Opus 5 7def4a7552 fix(ssp): route cancel/delete through runSubmit, surface the error (RB-20)
ApplicationsStore.cancel and AdminCasesStore.delete rolled an optimistic
write back on failure but showed no message — a bare catch with no
Result and no error channel (CQ-002). Both now call runSubmit and set a
lastError signal on failure, mirroring createSubmitChangeRequest in the
same folder. Each page renders the error with the existing app-alert
atom, the same pattern brief.page.ts already uses for lastError.

Added a spec file for ApplicationsStore (none existed) and extended
AdminCasesStore's spec, each asserting the rollback AND the surfaced
error. Verified both new assertions fail without the fix (an Edit
undo/redo of the store method, not git checkout).

Regenerated libs/shared/docs/behaviour-spec.mdx (gen:behaviour-spec) to
pick up the new/renamed test names. Marked RB-20 done in 99-backlog.md
and recorded the change in implementation/rb-20.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 18:32:51 +02:00

79 lines
2.8 KiB
TypeScript

import { TestBed } from '@angular/core/testing';
import { describe, it, expect, vi } from 'vitest';
import { SUBMIT_FAILED } from '@shared/application/submit';
import { ApplicationsAdapter } from '@registratie/infrastructure/applications.adapter';
import { AdminCasesStore } from './admin-cases.store';
const summary = (id: string) => ({
id,
type: 'registratie',
status: { tag: 'Concept', stepIndex: 0, stepCount: 3 },
documentIds: [],
createdAt: '2026-07-23T10:00:00Z',
updatedAt: '2026-07-23T10:00:00Z',
owner: '19012345601',
});
function setup(adapter: Partial<ApplicationsAdapter>): AdminCasesStore {
TestBed.configureTestingModule({
providers: [{ provide: ApplicationsAdapter, useValue: adapter }],
});
return TestBed.inject(AdminCasesStore);
}
describe('AdminCasesStore', () => {
it('loads and parses the cross-owner list', async () => {
const store = setup({ listAll: () => Promise.resolve([summary('a'), summary('b')]) });
await store.load();
const s = store.cases();
expect(s.tag).toBe('Success');
expect(s.tag === 'Success' && s.value.map((c) => c.id)).toEqual(['a', 'b']);
});
it('deletes optimistically and confirms via the admin endpoint', async () => {
const deleteAny = vi.fn().mockResolvedValue(undefined);
const store = setup({
listAll: () => Promise.resolve([summary('a'), summary('b')]),
deleteAny,
});
await store.load();
await store.delete('a');
expect(deleteAny).toHaveBeenCalledWith('a');
const s = store.cases();
expect(s.tag === 'Success' && s.value.map((c) => c.id)).toEqual(['b']);
});
// RB-20: a failed delete must not be silent — the row rolls back AND the store
// surfaces the error the page renders. Before RB-20 this only rolled back
// (bare `catch { this.state.set(before) }`), so `lastError()` stayed null forever.
it('rolls back the removal and surfaces the error when the delete fails', async () => {
const deleteAny = vi.fn().mockRejectedValue(new Error('boom'));
const store = setup({ listAll: () => Promise.resolve([summary('a')]), deleteAny });
await store.load();
await store.delete('a');
const s = store.cases();
expect(s.tag === 'Success' && s.value.map((c) => c.id)).toEqual(['a']); // reappears
expect(store.lastError()).toBe(SUBMIT_FAILED);
});
it('clears a stale error on the next delete attempt', async () => {
const deleteAny = vi
.fn()
.mockRejectedValueOnce(new Error('boom'))
.mockResolvedValueOnce(undefined);
const store = setup({
listAll: () => Promise.resolve([summary('a'), summary('b')]),
deleteAny,
});
await store.load();
await store.delete('a');
expect(store.lastError()).toBe(SUBMIT_FAILED);
await store.delete('b');
expect(store.lastError()).toBeNull();
});
});