feat(bulk-upload): add BulkDocumentEditLayout component with save handler
State-owner for the bulk upload flow: - N=0: full-panel BulkDropZone - N=1: title + shared metadata (no switcher/scope cards) - N≥2: FileSwitcherStrip + per-file ScopeCard + shared ScopeCard Save handler chunks files at 10/request, POSTs to /api/documents/quick-upload with typed metadata JSON part, tracks progress, redirects to /documents. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||
import { cleanup, render } from 'vitest-browser-svelte';
|
||||
import { page, userEvent } from 'vitest/browser';
|
||||
import BulkDocumentEditLayout from './BulkDocumentEditLayout.svelte';
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
function makeFile(name: string): File {
|
||||
return new File(['content'], name, { type: 'application/pdf' });
|
||||
}
|
||||
|
||||
async function addFilesViaInput(container: HTMLElement, files: File[]): Promise<void> {
|
||||
const input = container.querySelector('input[type="file"]') as HTMLInputElement;
|
||||
if (!input) throw new Error('No file input found — is BulkDropZone visible?');
|
||||
await userEvent.upload(input, files);
|
||||
}
|
||||
|
||||
describe('BulkDocumentEditLayout', () => {
|
||||
it('N=0: shows BulkDropZone', async () => {
|
||||
render(BulkDocumentEditLayout, {});
|
||||
await expect.element(page.getByTestId('bulk-drop-zone')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('N=1: file-switcher-strip and per-file scope card are absent', async () => {
|
||||
const { container } = render(BulkDocumentEditLayout, {});
|
||||
await addFilesViaInput(container, [makeFile('doc.pdf')]);
|
||||
expect(container.querySelector('[data-testid="file-switcher-strip"]')).toBeNull();
|
||||
expect(container.querySelector('[data-variant="per-file"]')).toBeNull();
|
||||
});
|
||||
|
||||
it('N=5: file-switcher-strip and per-file scope card are both present', async () => {
|
||||
const { container } = render(BulkDocumentEditLayout, {});
|
||||
await addFilesViaInput(container, [
|
||||
makeFile('a.pdf'),
|
||||
makeFile('b.pdf'),
|
||||
makeFile('c.pdf'),
|
||||
makeFile('d.pdf'),
|
||||
makeFile('e.pdf')
|
||||
]);
|
||||
expect(container.querySelector('[data-testid="file-switcher-strip"]')).not.toBeNull();
|
||||
expect(container.querySelector('[data-variant="per-file"]')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('removing middle file preserves order of remaining files', async () => {
|
||||
const { container } = render(BulkDocumentEditLayout, {});
|
||||
await addFilesViaInput(container, [
|
||||
makeFile('file0.pdf'),
|
||||
makeFile('file1.pdf'),
|
||||
makeFile('file2.pdf')
|
||||
]);
|
||||
|
||||
// Remove the chip for file1 via its "Entfernen" remove button (second × button)
|
||||
const removeButtons = container.querySelectorAll<HTMLButtonElement>(
|
||||
'[data-testid="file-switcher-strip"] button[aria-label="Entfernen"]'
|
||||
);
|
||||
expect(removeButtons.length).toBe(3);
|
||||
removeButtons[1].click(); // remove file1
|
||||
|
||||
// Wait for Svelte to flush the DOM update
|
||||
await vi.waitFor(
|
||||
() => {
|
||||
const chips = container.querySelectorAll(
|
||||
'[data-testid="file-switcher-strip"] [data-chip-id]'
|
||||
);
|
||||
expect(chips.length).toBe(2);
|
||||
expect(chips[0].textContent?.trim()).toContain('file0');
|
||||
expect(chips[1].textContent?.trim()).toContain('file2');
|
||||
},
|
||||
{ timeout: 1000 }
|
||||
);
|
||||
});
|
||||
|
||||
it('save calls fetch twice for 12 files (2 chunks of 10)', async () => {
|
||||
const mockFetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({ created: [], updated: [], errors: [] })
|
||||
});
|
||||
vi.stubGlobal('fetch', mockFetch);
|
||||
|
||||
// Also stub goto to prevent navigation errors in test
|
||||
vi.mock('$app/navigation', () => ({ goto: vi.fn() }));
|
||||
|
||||
const { container } = render(BulkDocumentEditLayout, {});
|
||||
const files = Array.from({ length: 12 }, (_, i) => makeFile(`f${i}.pdf`));
|
||||
await addFilesViaInput(container, files);
|
||||
|
||||
const saveBtn = container.querySelector(
|
||||
'button[data-testid="bulk-save-btn"]'
|
||||
) as HTMLButtonElement;
|
||||
expect(saveBtn).not.toBeNull();
|
||||
saveBtn.click();
|
||||
|
||||
// Wait for async save to complete
|
||||
await vi.waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(2), { timeout: 3000 });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user