diff --git a/frontend/src/lib/geschichte/JourneyEditor.svelte.spec.ts b/frontend/src/lib/geschichte/JourneyEditor.svelte.spec.ts index a88330f3..01c121d7 100644 --- a/frontend/src/lib/geschichte/JourneyEditor.svelte.spec.ts +++ b/frontend/src/lib/geschichte/JourneyEditor.svelte.spec.ts @@ -192,27 +192,92 @@ describe('JourneyEditor — remove with rollback', () => { render(JourneyEditor, defaultProps({ geschichte: makeGeschichte({ items }) })); // Click remove (no note → direct remove) - await userEvent.click(page.getByRole('button', { name: 'Wirklich entfernen?' })); + await userEvent.click(page.getByRole('button', { name: 'Eintrag entfernen' })); await new Promise((r) => setTimeout(r, 50)); // Item should be restored after rollback await expect.element(page.getByText('Brief A')).toBeInTheDocument(); }); - it('item-add does NOT mark dirty (isDirty stays false)', async () => { + it('item-add enables publish button (isDirty stays false, canPublish becomes true)', async () => { const newItem = { id: 'i1', position: 0, note: 'Test' }; mockCsrfFetch(() => newItem); - const onSubmit = vi.fn().mockResolvedValue(undefined); - render(JourneyEditor, defaultProps({ onSubmit })); + render(JourneyEditor, defaultProps()); - // Add interlude (no unsaved warning should interfere) + // Publish should be disabled before adding item + await expect.element(page.getByRole('button', { name: /Veröffentlichen/ })).toBeDisabled(); + + // Add interlude await userEvent.click(page.getByText('Zwischentext hinzufügen')); await userEvent.fill(page.getByPlaceholder('Zwischentext eingeben…'), 'Test'); await userEvent.click(page.getByRole('button', { name: 'Hinzufügen', exact: true })); + await new Promise((r) => setTimeout(r, 50)); - // Saving (which requires non-empty title) — no unsaved warning dialog - await expect.element(page.getByRole('dialog')).not.toBeInTheDocument(); + // After item add, publish becomes enabled — item was added and state is correct + await expect.element(page.getByRole('button', { name: /Veröffentlichen/ })).not.toBeDisabled(); + }); +}); + +describe('JourneyEditor — reorder via move buttons', () => { + it('move-up calls PUT reorder with swapped IDs', async () => { + const items = [ + { id: 'i1', position: 0, document: docSummary('d1', 'Brief A') }, + { id: 'i2', position: 1, document: docSummary('d2', 'Brief B') } + ]; + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue({ + ok: true, + json: vi.fn().mockResolvedValue([ + { id: 'i2', position: 0, document: docSummary('d2', 'Brief B') }, + { id: 'i1', position: 1, document: docSummary('d1', 'Brief A') } + ]) + }) + ); + + render(JourneyEditor, defaultProps({ geschichte: makeGeschichte({ items }) })); + + await userEvent.click(page.getByRole('button', { name: /Brief B.*nach oben verschieben/ })); + await new Promise((r) => setTimeout(r, 50)); // handleMoveUp → handleReorder → csrfFetch: two await levels + + expect(globalThis.fetch).toHaveBeenCalledWith( + expect.stringContaining('/items/reorder'), + expect.objectContaining({ + method: 'PUT', + body: JSON.stringify({ itemIds: ['i2', 'i1'] }) + }) + ); + }); + + it('move-down calls PUT reorder with swapped IDs', async () => { + const items = [ + { id: 'i1', position: 0, document: docSummary('d1', 'Brief A') }, + { id: 'i2', position: 1, document: docSummary('d2', 'Brief B') } + ]; + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue({ + ok: true, + json: vi.fn().mockResolvedValue([ + { id: 'i2', position: 0, document: docSummary('d2', 'Brief B') }, + { id: 'i1', position: 1, document: docSummary('d1', 'Brief A') } + ]) + }) + ); + + render(JourneyEditor, defaultProps({ geschichte: makeGeschichte({ items }) })); + + await userEvent.click(page.getByRole('button', { name: /Brief A.*nach unten verschieben/ })); + await new Promise((r) => setTimeout(r, 50)); // handleMoveDown → handleReorder → csrfFetch: two await levels + + expect(globalThis.fetch).toHaveBeenCalledWith( + expect.stringContaining('/items/reorder'), + expect.objectContaining({ + method: 'PUT', + body: JSON.stringify({ itemIds: ['i2', 'i1'] }) + }) + ); }); });