fix(journey): editor review round — labels, errors, pending state, a11y, tests
Addresses the remaining #792 review blockers and concerns in the journey editor cluster: - Interlude rows show 'Zwischentext' (dedicated key), not the add-button text - All four mutation handlers route the backend ErrorCode through getErrorMessage (a 409 duplicate no longer says 'bitte Seite neu laden') and console.error their failures so client-side errors leave a trace - Remove implements the spec'd pending state: row stays dimmed with an aria-live 'wird entfernt…' until the DELETE resolves; failure keeps the row - Move announcements fire after the reorder resolves (no false 'verschoben') - Touch targets ≥44px (remove ×, note links, create submit); focus moves to the new row after add, to a sensible neighbor after remove, back to × on confirm-cancel; drag handle is pointer-only; title/intro get aria-labels; publish-disabled reason is a visible hint, not a title tooltip - Amber warning styles use new --color-warning-* tokens with dark remaps - Blocked interlude-clear restores the draft instead of showing phantom text - useBlockDragDrop moves to $lib/shared/hooks — geschichte no longer imports another domain's internals - Test hardening: reorder-failure rollback (non-ok + reject), publish/ unpublish/empty-warning surface, destructive confirm path, maxlength assertions, JourneyCreate failure path, edit-page STORY/JOURNEY branch, fixture factory, m.* assertions, all fixed sleeps replaced with polling 67 component tests green across 6 spec files; transcription consumer of the moved hook re-verified (30 green). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -60,6 +60,7 @@ async function handleSubmit(e: SubmitEvent) {
|
||||
bind:value={title}
|
||||
onblur={() => (titleTouched = true)}
|
||||
placeholder={m.geschichte_editor_title_placeholder()}
|
||||
aria-label={m.journey_title_aria_label()}
|
||||
aria-invalid={showTitleError}
|
||||
class="block w-full rounded border px-3 py-2 font-serif text-lg text-ink placeholder:text-ink-3 focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring {showTitleError
|
||||
? 'border-danger'
|
||||
@@ -74,7 +75,7 @@ async function handleSubmit(e: SubmitEvent) {
|
||||
<button
|
||||
type="submit"
|
||||
disabled={submitting}
|
||||
class="rounded bg-brand-navy px-4 py-2 font-sans text-sm font-medium text-white hover:opacity-90 focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring disabled:opacity-50"
|
||||
class="inline-flex h-11 items-center rounded bg-brand-navy px-4 font-sans text-sm font-medium text-white hover:opacity-90 focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring disabled:opacity-50"
|
||||
>
|
||||
{m.journey_create_submit()}
|
||||
</button>
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { cleanup, render } from 'vitest-browser-svelte';
|
||||
import { page, userEvent } from 'vitest/browser';
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
import { getErrorMessage } from '$lib/shared/errors';
|
||||
|
||||
vi.mock('$app/navigation', () => ({
|
||||
goto: vi.fn()
|
||||
}));
|
||||
|
||||
const { default: JourneyCreate } = await import('./JourneyCreate.svelte');
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
describe('JourneyCreate — failure path', () => {
|
||||
it('renders the mapped error message when POST /api/geschichten fails with a code', async () => {
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
json: vi.fn().mockResolvedValue({ code: 'VALIDATION_ERROR' })
|
||||
})
|
||||
);
|
||||
|
||||
render(JourneyCreate, {});
|
||||
|
||||
await userEvent.fill(
|
||||
page.getByRole('textbox', { name: m.journey_title_aria_label() }),
|
||||
'Meine Lesereise'
|
||||
);
|
||||
await userEvent.click(page.getByRole('button', { name: m.journey_create_submit() }));
|
||||
|
||||
const alert = page.getByRole('alert');
|
||||
await expect.element(alert).toBeInTheDocument();
|
||||
await expect.element(alert).toHaveTextContent(getErrorMessage('VALIDATION_ERROR'));
|
||||
});
|
||||
|
||||
it('navigates to the edit page on success', async () => {
|
||||
const { goto } = await import('$app/navigation');
|
||||
vi.mocked(goto).mockClear();
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: vi.fn().mockResolvedValue({ id: 'g-new' })
|
||||
})
|
||||
);
|
||||
|
||||
render(JourneyCreate, {});
|
||||
|
||||
await userEvent.fill(
|
||||
page.getByRole('textbox', { name: m.journey_title_aria_label() }),
|
||||
'Meine Lesereise'
|
||||
);
|
||||
await userEvent.click(page.getByRole('button', { name: m.journey_create_submit() }));
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(goto).toHaveBeenCalledWith('/geschichten/g-new/edit');
|
||||
});
|
||||
});
|
||||
|
||||
it('has an accessible label on the title input', async () => {
|
||||
vi.stubGlobal('fetch', vi.fn());
|
||||
render(JourneyCreate, {});
|
||||
|
||||
await expect
|
||||
.element(page.getByRole('textbox', { name: m.journey_title_aria_label() }))
|
||||
.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user