feat(bulk-upload): guard save() against concurrent invocations

Adds a saving $state flag that blocks re-entry while a chunk upload is
in flight. The UploadSaveBar save button is disabled via a new disabled
prop while saving is true. Tested: clicking Save twice fires fetch only
once.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-25 11:03:58 +02:00
committed by marcel
parent d32e671e9d
commit 899508f9ca
3 changed files with 38 additions and 3 deletions

View File

@@ -206,6 +206,34 @@ describe('BulkDocumentEditLayout', () => {
);
});
it('save() does not call fetch a second time when already saving', async () => {
let resolveFirst: (() => void) | undefined;
const mockFetch = vi.fn().mockImplementation(
() =>
new Promise<Response>((resolve) => {
resolveFirst = () =>
resolve({
ok: true,
json: async () => ({ created: [], updated: [], errors: [] })
} as Response);
})
);
vi.stubGlobal('fetch', mockFetch);
const { container } = render(BulkDocumentEditLayout, {});
await addFilesViaInput(container, [makeFile('a.pdf')]);
const saveBtn = container.querySelector(
'button[data-testid="bulk-save-btn"]'
) as HTMLButtonElement;
saveBtn.click(); // first click — fetch is in-flight
saveBtn.click(); // second click — should be a no-op
resolveFirst?.();
await vi.waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(1), { timeout: 3000 });
expect(mockFetch).toHaveBeenCalledTimes(1);
});
it('discard-all resets to N=0 state and shows drop zone', async () => {
const { container } = render(BulkDocumentEditLayout, {});
await addFilesViaInput(container, [makeFile('a.pdf'), makeFile('b.pdf')]);