fix(journey-editor): unsaved-warning banner + save throws on failure
JourneyEditor now renders UnsavedWarningBanner when showUnsavedWarning is true. save() wraps onSubmit in try/catch so clearOnSuccess only fires on success. edit/+page.svelte handleSubmit throws instead of returning on non-ok responses so JourneyEditor sees the failure. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,9 @@ import { page, userEvent } from 'vitest/browser';
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
import JourneyEditor from './JourneyEditor.svelte';
|
||||
|
||||
vi.mock('$app/navigation', () => ({ beforeNavigate: vi.fn(), goto: vi.fn() }));
|
||||
import { beforeNavigate } from '$app/navigation';
|
||||
|
||||
const docSummary = (id: string, title: string) => ({
|
||||
id,
|
||||
title,
|
||||
@@ -612,6 +615,69 @@ describe('JourneyEditor — duplicate document aria-disabled', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('JourneyEditor — unsaved warning banner', () => {
|
||||
function triggerNavigationAttempt() {
|
||||
const calls = vi.mocked(beforeNavigate).mock.calls;
|
||||
if (calls.length === 0) return;
|
||||
const [callback] = calls[calls.length - 1];
|
||||
const cancel = vi.fn();
|
||||
(callback as (nav: { cancel: () => void; to: { url: URL } | null }) => void)({
|
||||
cancel,
|
||||
to: { url: new URL('http://localhost/geschichten') }
|
||||
});
|
||||
return cancel;
|
||||
}
|
||||
|
||||
it('banner is absent before any edit or navigation attempt', async () => {
|
||||
render(JourneyEditor, defaultProps());
|
||||
expect(document.querySelector('[class*="amber"]')).toBeNull();
|
||||
});
|
||||
|
||||
it('banner appears when dirty and a navigation is attempted', async () => {
|
||||
render(JourneyEditor, defaultProps());
|
||||
|
||||
// Mark dirty by editing the title
|
||||
const titleInput = page.getByPlaceholder(/Titel/);
|
||||
await titleInput.element().dispatchEvent(new InputEvent('input', { bubbles: true }));
|
||||
|
||||
// Simulate the user trying to navigate away
|
||||
const cancel = triggerNavigationAttempt();
|
||||
expect(cancel).toHaveBeenCalled();
|
||||
|
||||
await expect.element(page.getByText(m.admin_unsaved_warning())).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('banner stays after a failed save (clearOnSuccess not called when onSubmit throws)', async () => {
|
||||
const onSubmit = vi.fn().mockRejectedValue(new Error('server error'));
|
||||
render(
|
||||
JourneyEditor,
|
||||
defaultProps({
|
||||
onSubmit,
|
||||
geschichte: makeGeschichte({
|
||||
title: 'Titel',
|
||||
items: [{ id: 'i1', position: 0, document: docSummary('d1', 'Brief A') }]
|
||||
})
|
||||
})
|
||||
);
|
||||
|
||||
// Mark dirty
|
||||
const titleInput = page.getByPlaceholder(/Titel/);
|
||||
await titleInput.element().dispatchEvent(new InputEvent('input', { bubbles: true }));
|
||||
|
||||
// Trigger navigation → banner appears
|
||||
triggerNavigationAttempt();
|
||||
await expect.element(page.getByText(m.admin_unsaved_warning())).toBeInTheDocument();
|
||||
|
||||
// Attempt save — onSubmit throws
|
||||
await userEvent.click(page.getByRole('button', { name: m.geschichte_editor_save_draft() }));
|
||||
|
||||
// Banner must still be visible (isDirty was not cleared)
|
||||
await vi.waitFor(() => {
|
||||
expect(document.querySelector('[class*="amber"]')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('JourneyEditor — person chips from GeschichteView', () => {
|
||||
it('renders person names in the sidebar chips (PersonView carries no displayName)', async () => {
|
||||
render(
|
||||
|
||||
Reference in New Issue
Block a user