diff --git a/frontend/e2e/document-title-autosync.spec.ts b/frontend/e2e/document-title-autosync.spec.ts new file mode 100644 index 00000000..2f1bcb91 --- /dev/null +++ b/frontend/e2e/document-title-autosync.spec.ts @@ -0,0 +1,36 @@ +import { expect, test } from '@playwright/test'; + +/** + * Auto-title sync, full-stack happy path (#726). A document whose stored title equals its + * machine-generated auto-title must follow a date correction forward on save; a hand-edit would + * be kept. The exhaustive permutations live in the backend unit/integration suites — this is the + * single end-to-end pass, and it also asserts the FR-005 helper line is present on the edit form. + */ +test.describe('Document auto-title sync (#726)', () => { + test('editing the date rebuilds the auto-title, and the edit form explains it', async ({ + page + }) => { + // 1. Create a document with no date/location, so its stored title == its auto-title + // (originalFilename only). createDocument derives originalFilename from the title. + await page.goto('/documents/new'); + await page.waitForSelector('[data-hydrated]'); + await page.getByLabel('Titel').fill('E2E Auto-Titel Sync'); + await page.getByRole('button', { name: 'Speichern', exact: true }).click(); + await expect(page).toHaveURL(/\/documents\/[^/]+$/); + const detailUrl = page.url(); + + // 2. The edit form carries the FR-005 helper explaining the auto-generated title. + await page.goto(`${detailUrl}/edit`); + await page.waitForSelector('[data-hydrated]'); + await expect(page.locator('#title-help')).toBeVisible(); + + // 3. Add a YEAR-precision date WITHOUT touching the title, then save. + await page.locator('#documentDate').fill('15.01.1928'); + await page.locator('#metaDatePrecision').selectOption('YEAR'); + await page.getByRole('button', { name: 'Speichern', exact: true }).click(); + + // 4. The detail page shows the regenerated title carrying the new year. + await expect(page).toHaveURL(/\/documents\/[^/]+$/); + await expect(page.getByRole('heading', { name: /E2E Auto-Titel Sync.*1928/ })).toBeVisible(); + }); +}); diff --git a/frontend/messages/de.json b/frontend/messages/de.json index 7601d996..25bc0fa0 100644 --- a/frontend/messages/de.json +++ b/frontend/messages/de.json @@ -56,6 +56,7 @@ "form_label_sender": "Absender", "form_label_receivers": "Empfänger", "form_label_title": "Titel", + "form_helper_title_autogenerated": "Wird automatisch aus Datum und Ort gebildet — sobald du den Titel änderst, bleibt deine Version erhalten.", "form_label_tags": "Schlagworte", "form_label_content": "Inhalt", "form_placeholder_content": "Kurze Beschreibung des Inhalts…", diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 722bac6b..bd71f0ba 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -56,6 +56,7 @@ "form_label_sender": "Sender", "form_label_receivers": "Recipients", "form_label_title": "Title", + "form_helper_title_autogenerated": "Generated automatically from the date and place — as soon as you edit the title, your version is kept.", "form_label_tags": "Tags", "form_label_content": "Content", "form_placeholder_content": "Brief description of the content…", diff --git a/frontend/messages/es.json b/frontend/messages/es.json index 746f4fc7..07cd1087 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -56,6 +56,7 @@ "form_label_sender": "Remitente", "form_label_receivers": "Destinatarios", "form_label_title": "Título", + "form_helper_title_autogenerated": "Se genera automáticamente a partir de la fecha y el lugar; en cuanto edites el título, se conservará tu versión.", "form_label_tags": "Etiquetas", "form_label_content": "Contenido", "form_placeholder_content": "Breve descripción del contenido…", diff --git a/frontend/src/lib/document/DescriptionSection.svelte b/frontend/src/lib/document/DescriptionSection.svelte index b18cc16a..3e4f12b3 100644 --- a/frontend/src/lib/document/DescriptionSection.svelte +++ b/frontend/src/lib/document/DescriptionSection.svelte @@ -17,6 +17,7 @@ let { titleRequired = false, suggestedTitle = '', hideTitle = false, + showTitleHelp = false, editMode = false }: { tags?: Tag[]; @@ -31,6 +32,7 @@ let { titleRequired?: boolean; suggestedTitle?: string; hideTitle?: boolean; + showTitleHelp?: boolean; editMode?: boolean; } = $props(); @@ -72,8 +74,14 @@ const titleValue = $derived(titleDirty ? currentTitle : suggestedTitle || curren titleDirty = true; }} required={titleRequired} + aria-describedby={showTitleHelp ? 'title-help' : undefined} class="block w-full rounded border border-line p-2 text-sm shadow-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring" /> + {#if showTitleHelp} +
+ {m.form_helper_title_autogenerated()} +
+ {/if} {/if} diff --git a/frontend/src/lib/document/DescriptionSection.svelte.spec.ts b/frontend/src/lib/document/DescriptionSection.svelte.spec.ts index f0c05599..bc47a98a 100644 --- a/frontend/src/lib/document/DescriptionSection.svelte.spec.ts +++ b/frontend/src/lib/document/DescriptionSection.svelte.spec.ts @@ -55,3 +55,26 @@ describe('DescriptionSection — onMount seeding (Felix B1/B2 fix regression fen expect(input.value).toBe('Parent Value'); }); }); + +describe('DescriptionSection — auto-generated title helper (FR-TITLE-005)', () => { + it('shows the helper and wires aria-describedby when showTitleHelp is set', async () => { + render(DescriptionSection, { showTitleHelp: true }); + const help = document.querySelector('#title-help') as HTMLElement; + expect(help).not.toBeNull(); + expect(help.textContent?.trim().length ?? 0).toBeGreaterThan(0); + const titleInput = document.querySelector('input#title') as HTMLInputElement; + expect(titleInput.getAttribute('aria-describedby')).toBe('title-help'); + }); + + it('omits the helper by default (e.g. the new-document form)', async () => { + render(DescriptionSection, {}); + expect(document.querySelector('#title-help')).toBeNull(); + const titleInput = document.querySelector('input#title') as HTMLInputElement; + expect(titleInput.getAttribute('aria-describedby')).toBeNull(); + }); + + it('omits the helper when the title field is hidden (bulk edit)', async () => { + render(DescriptionSection, { showTitleHelp: true, hideTitle: true }); + expect(document.querySelector('#title-help')).toBeNull(); + }); +}); diff --git a/frontend/src/lib/document/DocumentEditLayout.svelte b/frontend/src/lib/document/DocumentEditLayout.svelte index 2b72e58c..e0867517 100644 --- a/frontend/src/lib/document/DocumentEditLayout.svelte +++ b/frontend/src/lib/document/DocumentEditLayout.svelte @@ -221,6 +221,7 @@ async function handleReplaceFile(e: Event) { initialArchiveFolder={doc.archiveFolder ?? ''} initialSummary={doc.summary ?? ''} titleRequired={true} + showTitleHelp={true} />