feat(document): explain auto-generated title under the edit title field (#726)
Adds the FR-TITLE-005 helper line under the title input in DescriptionSection, shown only on the single-document edit form via a new showTitleHelp prop (off for the new-document and bulk-edit forms). It is wired to the input with aria-describedby and uses text-ink-3 (WCAG AA on bg-surface). New Paraglide key form_helper_title_autogenerated in de/en/es. Adds a component test for the helper + aria wiring and an end-to-end pass: create an auto-titled doc, edit its date, and see the title follow on the detail page. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
36
frontend/e2e/document-title-autosync.spec.ts
Normal file
36
frontend/e2e/document-title-autosync.spec.ts
Normal file
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -56,6 +56,7 @@
|
|||||||
"form_label_sender": "Absender",
|
"form_label_sender": "Absender",
|
||||||
"form_label_receivers": "Empfänger",
|
"form_label_receivers": "Empfänger",
|
||||||
"form_label_title": "Titel",
|
"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_tags": "Schlagworte",
|
||||||
"form_label_content": "Inhalt",
|
"form_label_content": "Inhalt",
|
||||||
"form_placeholder_content": "Kurze Beschreibung des Inhalts…",
|
"form_placeholder_content": "Kurze Beschreibung des Inhalts…",
|
||||||
|
|||||||
@@ -56,6 +56,7 @@
|
|||||||
"form_label_sender": "Sender",
|
"form_label_sender": "Sender",
|
||||||
"form_label_receivers": "Recipients",
|
"form_label_receivers": "Recipients",
|
||||||
"form_label_title": "Title",
|
"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_tags": "Tags",
|
||||||
"form_label_content": "Content",
|
"form_label_content": "Content",
|
||||||
"form_placeholder_content": "Brief description of the content…",
|
"form_placeholder_content": "Brief description of the content…",
|
||||||
|
|||||||
@@ -56,6 +56,7 @@
|
|||||||
"form_label_sender": "Remitente",
|
"form_label_sender": "Remitente",
|
||||||
"form_label_receivers": "Destinatarios",
|
"form_label_receivers": "Destinatarios",
|
||||||
"form_label_title": "Título",
|
"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_tags": "Etiquetas",
|
||||||
"form_label_content": "Contenido",
|
"form_label_content": "Contenido",
|
||||||
"form_placeholder_content": "Breve descripción del contenido…",
|
"form_placeholder_content": "Breve descripción del contenido…",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ let {
|
|||||||
titleRequired = false,
|
titleRequired = false,
|
||||||
suggestedTitle = '',
|
suggestedTitle = '',
|
||||||
hideTitle = false,
|
hideTitle = false,
|
||||||
|
showTitleHelp = false,
|
||||||
editMode = false
|
editMode = false
|
||||||
}: {
|
}: {
|
||||||
tags?: Tag[];
|
tags?: Tag[];
|
||||||
@@ -31,6 +32,7 @@ let {
|
|||||||
titleRequired?: boolean;
|
titleRequired?: boolean;
|
||||||
suggestedTitle?: string;
|
suggestedTitle?: string;
|
||||||
hideTitle?: boolean;
|
hideTitle?: boolean;
|
||||||
|
showTitleHelp?: boolean;
|
||||||
editMode?: boolean;
|
editMode?: boolean;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
@@ -72,8 +74,14 @@ const titleValue = $derived(titleDirty ? currentTitle : suggestedTitle || curren
|
|||||||
titleDirty = true;
|
titleDirty = true;
|
||||||
}}
|
}}
|
||||||
required={titleRequired}
|
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"
|
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}
|
||||||
|
<p id="title-help" class="mt-1 text-xs text-ink-3">
|
||||||
|
{m.form_helper_title_autogenerated()}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|||||||
@@ -55,3 +55,26 @@ describe('DescriptionSection — onMount seeding (Felix B1/B2 fix regression fen
|
|||||||
expect(input.value).toBe('Parent Value');
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -221,6 +221,7 @@ async function handleReplaceFile(e: Event) {
|
|||||||
initialArchiveFolder={doc.archiveFolder ?? ''}
|
initialArchiveFolder={doc.archiveFolder ?? ''}
|
||||||
initialSummary={doc.summary ?? ''}
|
initialSummary={doc.summary ?? ''}
|
||||||
titleRequired={true}
|
titleRequired={true}
|
||||||
|
showTitleHelp={true}
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user