fix(journey-editor): note UX, error codes, list semantics, a11y labels
- handleNotePatch routes failures through failureMessage() so a backend
JOURNEY_NOTE_TOO_LONG renders its specific message in the row
- handleNoteBlur: '' vs undefined no-op guard (no spurious PATCH
{note:null}), empty blur collapses the textarea, clearing an existing
note collapses after the PATCH lands (spec LE-3)
- 'Notiz hinzufügen' toggle gets aria-expanded and moves focus into the
revealed textarea
- journey_remove_item_aria interpolates the item title (de/en/es); dead
journey_drag_aria_label key deleted from all locales
- editor item list is an <ol> (screen readers announce the ordering)
- editor title input gets maxlength=255 + border-danger error cue; intro
textarea gets maxlength=4000
- Briefmeta line (date · von X an Y) under document titles in the editor
row via the shared formatDocumentMetaLine (spec LE-2)
- new specs: successful save clears the unsaved warning; item add does
not arm the guard; server-confirmed order after successful reorder;
blur-without-typing no-op; focus hand-off into the note textarea
Review round 3: Sara (1-3), Felix, Elicit (LE-2/LE-3), Leonie.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -66,6 +66,35 @@ describe('JourneyItemRow — note textarea', () => {
|
||||
.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('blur without typing does not call onNotePatch and collapses the textarea', async () => {
|
||||
// '' (untouched draft) and undefined (no note) both mean "no note" — a
|
||||
// spurious PATCH {note: null} must not fire, and the empty textarea closes.
|
||||
const onNotePatch = vi.fn().mockResolvedValue(undefined);
|
||||
render(JourneyItemRow, { item: docItem(), ...defaultProps({ onNotePatch }) });
|
||||
|
||||
await userEvent.click(page.getByText(m.journey_note_add()));
|
||||
const textarea = page.getByRole('textbox', { name: /Kuratoren-Notiz für Brief von Karl/ });
|
||||
await textarea.element().dispatchEvent(new FocusEvent('blur'));
|
||||
|
||||
expect(onNotePatch).not.toHaveBeenCalled();
|
||||
await expect.element(page.getByText(m.journey_note_add())).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('moves focus into the textarea when "Notiz hinzufügen" opens it', async () => {
|
||||
render(JourneyItemRow, { item: docItem(), ...defaultProps() });
|
||||
|
||||
const toggle = page.getByText(m.journey_note_add());
|
||||
expect(toggle.element().getAttribute('aria-expanded')).toBe('false');
|
||||
await userEvent.click(toggle);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
const textarea = page
|
||||
.getByRole('textbox', { name: /Kuratoren-Notiz für Brief von Karl/ })
|
||||
.element();
|
||||
expect(document.activeElement).toBe(textarea);
|
||||
});
|
||||
});
|
||||
|
||||
it('calls onNotePatch on textarea blur with non-empty value', async () => {
|
||||
const onNotePatch = vi.fn().mockResolvedValue(undefined);
|
||||
render(JourneyItemRow, { item: docItem(), ...defaultProps({ onNotePatch }) });
|
||||
@@ -167,7 +196,9 @@ describe('JourneyItemRow — remove confirm', () => {
|
||||
});
|
||||
|
||||
// Click remove (x button)
|
||||
await userEvent.click(page.getByRole('button', { name: m.journey_remove_item_aria() }));
|
||||
await userEvent.click(
|
||||
page.getByRole('button', { name: m.journey_remove_item_aria({ title: 'Brief von Karl' }) })
|
||||
);
|
||||
|
||||
await expect.element(page.getByText(m.journey_remove_confirm())).toBeInTheDocument();
|
||||
await expect
|
||||
@@ -182,7 +213,9 @@ describe('JourneyItemRow — remove confirm', () => {
|
||||
...defaultProps({ onRemove })
|
||||
});
|
||||
|
||||
await userEvent.click(page.getByRole('button', { name: m.journey_remove_item_aria() }));
|
||||
await userEvent.click(
|
||||
page.getByRole('button', { name: m.journey_remove_item_aria({ title: 'Brief von Karl' }) })
|
||||
);
|
||||
await userEvent.click(page.getByRole('button', { name: m.journey_remove_confirm_yes() }));
|
||||
|
||||
expect(onRemove).toHaveBeenCalledTimes(1);
|
||||
@@ -195,13 +228,17 @@ describe('JourneyItemRow — remove confirm', () => {
|
||||
...defaultProps({ onRemove })
|
||||
});
|
||||
|
||||
await userEvent.click(page.getByRole('button', { name: m.journey_remove_item_aria() }));
|
||||
await userEvent.click(
|
||||
page.getByRole('button', { name: m.journey_remove_item_aria({ title: 'Brief von Karl' }) })
|
||||
);
|
||||
await userEvent.click(page.getByRole('button', { name: m.journey_remove_confirm_cancel() }));
|
||||
|
||||
expect(onRemove).not.toHaveBeenCalled();
|
||||
// The remove button should be back
|
||||
await expect
|
||||
.element(page.getByRole('button', { name: m.journey_remove_item_aria() }))
|
||||
.element(
|
||||
page.getByRole('button', { name: m.journey_remove_item_aria({ title: 'Brief von Karl' }) })
|
||||
)
|
||||
.toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -211,11 +248,15 @@ describe('JourneyItemRow — remove confirm', () => {
|
||||
...defaultProps()
|
||||
});
|
||||
|
||||
await userEvent.click(page.getByRole('button', { name: m.journey_remove_item_aria() }));
|
||||
await userEvent.click(
|
||||
page.getByRole('button', { name: m.journey_remove_item_aria({ title: 'Brief von Karl' }) })
|
||||
);
|
||||
await userEvent.click(page.getByRole('button', { name: m.journey_remove_confirm_cancel() }));
|
||||
|
||||
await vi.waitFor(() => {
|
||||
const removeBtn = page.getByRole('button', { name: m.journey_remove_item_aria() }).element();
|
||||
const removeBtn = page
|
||||
.getByRole('button', { name: m.journey_remove_item_aria({ title: 'Brief von Karl' }) })
|
||||
.element();
|
||||
expect(document.activeElement).toBe(removeBtn);
|
||||
});
|
||||
});
|
||||
@@ -228,7 +269,9 @@ describe('JourneyItemRow — remove confirm a11y', () => {
|
||||
...defaultProps()
|
||||
});
|
||||
|
||||
await userEvent.click(page.getByRole('button', { name: m.journey_remove_item_aria() }));
|
||||
await userEvent.click(
|
||||
page.getByRole('button', { name: m.journey_remove_item_aria({ title: 'Brief von Karl' }) })
|
||||
);
|
||||
|
||||
const group = document.querySelector('[role="group"]');
|
||||
expect(group).toBeTruthy();
|
||||
@@ -241,7 +284,9 @@ describe('JourneyItemRow — remove confirm a11y', () => {
|
||||
...defaultProps()
|
||||
});
|
||||
|
||||
await userEvent.click(page.getByRole('button', { name: m.journey_remove_item_aria() }));
|
||||
await userEvent.click(
|
||||
page.getByRole('button', { name: m.journey_remove_item_aria({ title: 'Brief von Karl' }) })
|
||||
);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
const cancelBtn = page
|
||||
@@ -257,7 +302,9 @@ describe('JourneyItemRow — remove confirm a11y', () => {
|
||||
...defaultProps()
|
||||
});
|
||||
|
||||
await userEvent.click(page.getByRole('button', { name: m.journey_remove_item_aria() }));
|
||||
await userEvent.click(
|
||||
page.getByRole('button', { name: m.journey_remove_item_aria({ title: 'Brief von Karl' }) })
|
||||
);
|
||||
await vi.waitFor(() => {
|
||||
const cancelBtn = page
|
||||
.getByRole('button', { name: m.journey_remove_confirm_cancel() })
|
||||
@@ -268,7 +315,9 @@ describe('JourneyItemRow — remove confirm a11y', () => {
|
||||
await userEvent.keyboard('{Escape}');
|
||||
|
||||
await vi.waitFor(() => {
|
||||
const removeBtn = page.getByRole('button', { name: m.journey_remove_item_aria() }).element();
|
||||
const removeBtn = page
|
||||
.getByRole('button', { name: m.journey_remove_item_aria({ title: 'Brief von Karl' }) })
|
||||
.element();
|
||||
expect(document.activeElement).toBe(removeBtn);
|
||||
});
|
||||
await expect.element(page.getByText(m.journey_remove_confirm())).not.toBeInTheDocument();
|
||||
@@ -284,7 +333,9 @@ describe('JourneyItemRow — pending remove state', () => {
|
||||
|
||||
await expect.element(page.getByText(m.journey_item_pending_remove())).toBeInTheDocument();
|
||||
await expect
|
||||
.element(page.getByRole('button', { name: m.journey_remove_item_aria() }))
|
||||
.element(
|
||||
page.getByRole('button', { name: m.journey_remove_item_aria({ title: 'Brief von Karl' }) })
|
||||
)
|
||||
.not.toBeInTheDocument();
|
||||
|
||||
const root = document.querySelector('[data-block-id="item-1"]')!;
|
||||
|
||||
Reference in New Issue
Block a user