From 96ea7e681588e0efdd0f0cc9b33acedaf0b1a8af Mon Sep 17 00:00:00 2001 From: Marcel Date: Sun, 17 May 2026 09:44:32 +0200 Subject: [PATCH] feat(observability): redesign +error.svelte with errorId display and copy button Co-Authored-By: Claude Sonnet 4.6 --- frontend/messages/de.json | 33 +++------------ frontend/messages/en.json | 33 +++------------ frontend/messages/es.json | 33 +++------------ frontend/src/routes/+error.svelte | 42 +++++++++++++++++-- frontend/src/routes/error.svelte.test.ts | 53 ++++++++++++++++++++++-- 5 files changed, 102 insertions(+), 92 deletions(-) diff --git a/frontend/messages/de.json b/frontend/messages/de.json index 090c5725..38067459 100644 --- a/frontend/messages/de.json +++ b/frontend/messages/de.json @@ -473,7 +473,7 @@ "dashboard_reader_stats_persons_short": "Pers.", "dashboard_reader_stats_stories_short": "Gesch.", "dashboard_reader_draft_meta": "Entwurf · zuletzt bearbeitet {relative}", - "dashboard_resume_label": "Zuletzt geöffnet:", + "dashboard_resume_label": "Weiter, wo du aufgehört hast", "dashboard_resume_fallback": "Unbekanntes Dokument", "doc_status_placeholder": "Platzhalter", "doc_status_uploaded": "Hochgeladen", @@ -773,19 +773,15 @@ "admin_invite_created_title": "Einladung erstellt", "admin_invite_created_desc": "Teile diesen Link mit der einzuladenden Person:", "admin_invite_revoke_confirm": "Einladung wirklich widerrufen?", - "greeting_morning": "Guten Morgen, {name}.", "greeting_day": "Hallo, {name}.", "greeting_evening": "Guten Abend, {name}.", - - "dashboard_resume_label": "Weiter, wo du aufgehört hast", "dashboard_blocks": "{count} Abschnitte", "dashboard_resume_cta": "Weitertranskribieren", "dashboard_resume_other": "oder anderen Brief wählen", "dashboard_empty_title": "Noch kein Dokument begonnen", "dashboard_empty_body": "Wähle ein Dokument aus dem Archiv, um mit der Transkription zu beginnen.", "dashboard_empty_cta": "Zum Archiv", - "dashboard_mission_caption": "Offene Aufgaben", "queue_segment": "Segmentieren", "queue_segment_blurb": "Seiten aufteilen", @@ -795,7 +791,6 @@ "queue_review_blurb": "Texte kontrollieren", "queue_n_open": "{n} offen", "queue_show_all": "Alle anzeigen →", - "pulse_eyebrow": "Diese Woche", "pulse_headline": "Ihr habt {pages} Seiten bearbeitet.", "pulse_you": "Du selbst hast {pages} davon bearbeitet.", @@ -803,19 +798,15 @@ "pulse_transcribed": "Textstellen markiert", "pulse_reviewed": "Textstellen transkribiert", "pulse_uploaded": "Dokumente hochgeladen", - "feed_caption": "Kommentare & Aktivität", "feed_show_all": "Alle anzeigen", "feed_for_you": "für dich", - "audit_action_text_saved": "hat Text gespeichert in", "audit_action_file_uploaded": "hat eine Datei hochgeladen:", "audit_action_annotation_created": "hat eine Markierung erstellt in", "audit_action_comment_added": "hat kommentiert:", "audit_action_mention_created": "hat dich erwähnt in", - "dropzone_release": "Loslassen zum Hochladen", - "chronik_page_title": "Aktivitäten", "chronik_for_you_caption": "Für dich", "chronik_for_you_count": "{count} neu", @@ -859,9 +850,7 @@ "pagination_page_of": "Seite {page} von {total}", "pagination_nav_label": "Seitennavigation", "pagination_page_button": "Seite {page}", - "common_opens_new_tab": "(öffnet in neuem Tab)", - "transcribe_coach_title": "Erste Transkription?", "transcribe_coach_preamble": "Unser Kurrent-Erkenner lernt noch. Jede Transkription, die Sie zum Training freigeben, bringt ihm die Schrift bei — so funktioniert's:", "transcribe_coach_step_1_title": "Rahmen ziehen.", @@ -871,10 +860,8 @@ "transcribe_coach_step_3_title": "Speichert automatisch.", "transcribe_coach_footer_kurrent": "Hilfe zu Kurrent ↗", "transcribe_coach_footer_richtlinien": "Transkriptions-Richtlinien ↗", - "transcription_mode_help_label": "Lese- und Bearbeitungsmodus", "transcription_mode_help_body": "Lesen zeigt die Transkription als fließenden Text. Bearbeiten öffnet die Textfelder für jede Passage.", - "richtlinien_title": "Transkriptions-Richtlinien", "richtlinien_intro": "Damit alle Briefe einheitlich transkribiert werden — egal wer tippt — hier unsere Regeln. Die Seite wächst mit: sobald wir eine neue Konvention beschließen, landet sie hier.", "richtlinien_wiki_text": "Kurrent- und Sütterlin-Alphabete sind bei Wikipedia gut erklärt. Hier stehen nur unsere eigenen Vereinbarungen für dieses Archiv.", @@ -948,12 +935,9 @@ "bulk_edit_all_x_failed": "Filter konnte nicht abgerufen werden — bitte erneut versuchen.", "bulk_edit_topbar_title": "Massenbearbeitung", "bulk_edit_count_pill": "{count} werden bearbeitet", - "nav_stammbaum": "Stammbaum", "nav_geschichten": "Geschichten", - "error_geschichte_not_found": "Die Geschichte wurde nicht gefunden.", - "geschichten_index_title": "Geschichten", "geschichten_new_button": "Neue Geschichte", "geschichten_filter_all_pill": "Alle", @@ -973,7 +957,6 @@ "geschichten_card_attach_action": "+ Geschichte anhängen", "geschichten_card_show_all_for_person": "Alle Geschichten zu {name}", "geschichten_card_show_all": "Alle anzeigen", - "geschichte_editor_title_placeholder": "Titel der Geschichte", "geschichte_editor_body_placeholder": "Schreibe hier deine Geschichte…", "geschichte_editor_status_draft": "ENTWURF", @@ -1000,14 +983,11 @@ "geschichte_editor_toolbar_h3": "Unterüberschrift", "geschichte_editor_toolbar_ul": "Aufzählung", "geschichte_editor_toolbar_ol": "Nummerierte Liste", - "geschichte_delete_confirm_title": "Geschichte löschen?", "geschichte_delete_confirm_body": "Diese Aktion kann nicht rückgängig gemacht werden. Die Geschichte wird dauerhaft gelöscht und aus allen verlinkten Personen- und Dokumentseiten entfernt.", - "error_relationship_not_found": "Die Beziehung wurde nicht gefunden.", "error_circular_relationship": "Diese Beziehung würde einen Kreis erzeugen.", "error_duplicate_relationship": "Diese Beziehung gibt es bereits.", - "relation_parent_of": "Elternteil von", "relation_child_of": "Kind von", "relation_spouse_of": "Ehegatte", @@ -1018,7 +998,6 @@ "relation_doctor": "Arzt", "relation_neighbor": "Nachbar", "relation_other": "Sonstige", - "relation_inferred_parent": "Elternteil", "relation_inferred_child": "Kind", "relation_inferred_spouse": "Ehegatte", @@ -1036,9 +1015,7 @@ "relation_inferred_sibling_inlaw": "Schwager/Schwägerin", "relation_inferred_cousin_1": "Cousin/Cousine", "relation_inferred_distant": "Weitläufige Verwandtschaft", - "doc_details_field_relationship": "Verwandtschaft", - "stammbaum_empty_heading": "Noch keine Familienmitglieder", "stammbaum_empty_body": "Markiere Personen auf ihrer Bearbeitungsseite als Familienmitglied, damit sie hier erscheinen.", "stammbaum_empty_link": "→ Zur Personenliste", @@ -1050,7 +1027,6 @@ "stammbaum_zoom_in": "Vergrößern", "stammbaum_zoom_out": "Verkleinern", "stammbaum_generations": "Generationen", - "relation_error_duplicate": "Diese Beziehung gibt es bereits.", "relation_error_circular": "Diese Beziehung würde einen Kreis erzeugen.", "relation_error_self": "Eine Person kann nicht mit sich selbst verbunden werden.", @@ -1073,14 +1049,15 @@ "relation_form_field_from_year": "Von Jahr", "relation_form_field_to_year": "Bis Jahr", "relation_form_year_placeholder": "z.B. 1920", - "person_relationships_heading": "Beziehungen", "person_relationships_empty": "Noch keine Beziehungen bekannt.", - "timeline_aria_label": "Zeitachse Dokumentdichte", "timeline_clear_selection": "Auswahl zurücksetzen", "timeline_zoom_reset": "Zurück zur Übersicht", "timeline_bar_aria_singular": "{when}, 1 Dokument", "timeline_bar_aria_plural": "{when}, {count} Dokumente", - "timeline_dragging_aria_live": "Zeitraum {from} bis {to} ausgewählt" + "timeline_dragging_aria_live": "Zeitraum {from} bis {to} ausgewählt", + "error_page_id_label": "Fehler-ID", + "error_copy_id_label": "ID kopieren", + "error_copied": "Kopiert!" } diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 34c23200..2ecb565d 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -473,7 +473,7 @@ "dashboard_reader_stats_persons_short": "Pers.", "dashboard_reader_stats_stories_short": "Stor.", "dashboard_reader_draft_meta": "Draft · last edited {relative}", - "dashboard_resume_label": "Last opened:", + "dashboard_resume_label": "Continue where you left off", "dashboard_resume_fallback": "Unknown document", "doc_status_placeholder": "Placeholder", "doc_status_uploaded": "Uploaded", @@ -773,19 +773,15 @@ "admin_invite_created_title": "Invite created", "admin_invite_created_desc": "Share this link with the person you are inviting:", "admin_invite_revoke_confirm": "Really revoke this invite?", - "greeting_morning": "Good morning, {name}.", "greeting_day": "Hello, {name}.", "greeting_evening": "Good evening, {name}.", - - "dashboard_resume_label": "Continue where you left off", "dashboard_blocks": "{count} sections", "dashboard_resume_cta": "Continue transcribing", "dashboard_resume_other": "or choose another document", "dashboard_empty_title": "No document started yet", "dashboard_empty_body": "Choose a document from the archive to start transcribing.", "dashboard_empty_cta": "To the archive", - "dashboard_mission_caption": "Open tasks", "queue_segment": "Segment", "queue_segment_blurb": "Split pages", @@ -795,7 +791,6 @@ "queue_review_blurb": "Check texts", "queue_n_open": "{n} open", "queue_show_all": "Show all →", - "pulse_eyebrow": "This week", "pulse_headline": "You have worked on {pages} pages.", "pulse_you": "You personally worked on {pages} of them.", @@ -803,19 +798,15 @@ "pulse_transcribed": "Passages annotated", "pulse_reviewed": "Passages transcribed", "pulse_uploaded": "Documents uploaded", - "feed_caption": "Comments & activity", "feed_show_all": "Show all", "feed_for_you": "for you", - "audit_action_text_saved": "saved text in", "audit_action_file_uploaded": "uploaded a file:", "audit_action_annotation_created": "created an annotation in", "audit_action_comment_added": "commented:", "audit_action_mention_created": "mentioned you in", - "dropzone_release": "Release to upload", - "chronik_page_title": "Activity", "chronik_for_you_caption": "For you", "chronik_for_you_count": "{count} new", @@ -859,9 +850,7 @@ "pagination_page_of": "Page {page} of {total}", "pagination_nav_label": "Pagination", "pagination_page_button": "Page {page}", - "common_opens_new_tab": "(opens in new tab)", - "transcribe_coach_title": "First transcription?", "transcribe_coach_preamble": "Our Kurrent recogniser is still learning. Every transcription you release for training teaches it the handwriting — here's how it works:", "transcribe_coach_step_1_title": "Draw a frame.", @@ -871,10 +860,8 @@ "transcribe_coach_step_3_title": "Saves automatically.", "transcribe_coach_footer_kurrent": "Kurrent help ↗", "transcribe_coach_footer_richtlinien": "Transcription guidelines ↗", - "transcription_mode_help_label": "Read and edit mode", "transcription_mode_help_body": "Read shows the transcription as flowing text. Edit opens the text fields for each passage.", - "richtlinien_title": "Transcription Guidelines", "richtlinien_intro": "So every letter is transcribed consistently — no matter who types — here are our rules. The page grows with us: as soon as we agree a new convention, it lands here.", "richtlinien_wiki_text": "The Kurrent and Sütterlin alphabets are well explained on Wikipedia. Here you'll only find our own conventions for this archive.", @@ -948,12 +935,9 @@ "bulk_edit_all_x_failed": "Could not load filter results — please retry.", "bulk_edit_topbar_title": "Bulk edit", "bulk_edit_count_pill": "{count} will be edited", - "nav_stammbaum": "Family tree", "nav_geschichten": "Stories", - "error_geschichte_not_found": "The story was not found.", - "geschichten_index_title": "Stories", "geschichten_new_button": "New story", "geschichten_filter_all_pill": "All", @@ -973,7 +957,6 @@ "geschichten_card_attach_action": "+ Attach a story", "geschichten_card_show_all_for_person": "All stories about {name}", "geschichten_card_show_all": "Show all", - "geschichte_editor_title_placeholder": "Story title", "geschichte_editor_body_placeholder": "Write your story here…", "geschichte_editor_status_draft": "DRAFT", @@ -1000,14 +983,11 @@ "geschichte_editor_toolbar_h3": "Subheading", "geschichte_editor_toolbar_ul": "Bulleted list", "geschichte_editor_toolbar_ol": "Numbered list", - "geschichte_delete_confirm_title": "Delete story?", "geschichte_delete_confirm_body": "This action cannot be undone. The story will be permanently deleted and removed from all linked person and document pages.", - "error_relationship_not_found": "Relationship not found.", "error_circular_relationship": "This relationship would form a cycle.", "error_duplicate_relationship": "This relationship already exists.", - "relation_parent_of": "Parent of", "relation_child_of": "Child of", "relation_spouse_of": "Spouse", @@ -1018,7 +998,6 @@ "relation_doctor": "Doctor", "relation_neighbor": "Neighbour", "relation_other": "Other", - "relation_inferred_parent": "Parent", "relation_inferred_child": "Child", "relation_inferred_spouse": "Spouse", @@ -1036,9 +1015,7 @@ "relation_inferred_sibling_inlaw": "Sibling-in-law", "relation_inferred_cousin_1": "Cousin", "relation_inferred_distant": "Distant relative", - "doc_details_field_relationship": "Relationship", - "stammbaum_empty_heading": "No family members yet", "stammbaum_empty_body": "Mark a person as a family member on their edit page so they appear here.", "stammbaum_empty_link": "→ Go to person list", @@ -1050,7 +1027,6 @@ "stammbaum_zoom_in": "Zoom in", "stammbaum_zoom_out": "Zoom out", "stammbaum_generations": "Generations", - "relation_error_duplicate": "This relationship already exists.", "relation_error_circular": "This relationship would form a cycle.", "relation_error_self": "A person cannot be related to themselves.", @@ -1073,14 +1049,15 @@ "relation_form_field_from_year": "From year", "relation_form_field_to_year": "To year", "relation_form_year_placeholder": "e.g. 1920", - "person_relationships_heading": "Relationships", "person_relationships_empty": "No relationships known yet.", - "timeline_aria_label": "Document density timeline", "timeline_clear_selection": "Clear selection", "timeline_zoom_reset": "Reset zoom", "timeline_bar_aria_singular": "{when}, 1 document", "timeline_bar_aria_plural": "{when}, {count} documents", - "timeline_dragging_aria_live": "Range {from} to {to} selected" + "timeline_dragging_aria_live": "Range {from} to {to} selected", + "error_page_id_label": "Error ID", + "error_copy_id_label": "Copy ID", + "error_copied": "Copied!" } diff --git a/frontend/messages/es.json b/frontend/messages/es.json index 4e487ad8..a68b663d 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -473,7 +473,7 @@ "dashboard_reader_stats_persons_short": "Pers.", "dashboard_reader_stats_stories_short": "Hist.", "dashboard_reader_draft_meta": "Borrador · editado hace {relative}", - "dashboard_resume_label": "Último abierto:", + "dashboard_resume_label": "Continuar donde lo dejaste", "dashboard_resume_fallback": "Documento desconocido", "doc_status_placeholder": "Marcador", "doc_status_uploaded": "Cargado", @@ -773,19 +773,15 @@ "admin_invite_created_title": "Invitación creada", "admin_invite_created_desc": "Comparte este enlace con la persona invitada:", "admin_invite_revoke_confirm": "¿Realmente revocar esta invitación?", - "greeting_morning": "Buenos días, {name}.", "greeting_day": "Hola, {name}.", "greeting_evening": "Buenas noches, {name}.", - - "dashboard_resume_label": "Continuar donde lo dejaste", "dashboard_blocks": "{count} secciones", "dashboard_resume_cta": "Continuar transcripción", "dashboard_resume_other": "o elige otro documento", "dashboard_empty_title": "Aún no has comenzado ningún documento", "dashboard_empty_body": "Elige un documento del archivo para empezar a transcribir.", "dashboard_empty_cta": "Al archivo", - "dashboard_mission_caption": "Tareas pendientes", "queue_segment": "Segmentar", "queue_segment_blurb": "Dividir páginas", @@ -795,7 +791,6 @@ "queue_review_blurb": "Controlar textos", "queue_n_open": "{n} pendiente", "queue_show_all": "Ver todo →", - "pulse_eyebrow": "Esta semana", "pulse_headline": "Habéis trabajado {pages} páginas.", "pulse_you": "Tú mismo has trabajado {pages} de ellas.", @@ -803,19 +798,15 @@ "pulse_transcribed": "Fragmentos anotados", "pulse_reviewed": "Fragmentos transcritos", "pulse_uploaded": "Documentos subidos", - "feed_caption": "Comentarios y actividad", "feed_show_all": "Ver todo", "feed_for_you": "para ti", - "audit_action_text_saved": "guardó texto en", "audit_action_file_uploaded": "subió un archivo:", "audit_action_annotation_created": "creó una anotación en", "audit_action_comment_added": "comentó:", "audit_action_mention_created": "te mencionó en", - "dropzone_release": "Suelta para subir", - "chronik_page_title": "Actividades", "chronik_for_you_caption": "Para ti", "chronik_for_you_count": "{count} nuevas", @@ -859,9 +850,7 @@ "pagination_page_of": "Página {page} de {total}", "pagination_nav_label": "Paginación", "pagination_page_button": "Página {page}", - "common_opens_new_tab": "(abre en pestaña nueva)", - "transcribe_coach_title": "¿Primera transcripción?", "transcribe_coach_preamble": "Nuestro reconocedor de Kurrent aún está aprendiendo. Cada transcripción que libera para el entrenamiento le enseña la escritura — así funciona:", "transcribe_coach_step_1_title": "Dibujar un marco.", @@ -871,10 +860,8 @@ "transcribe_coach_step_3_title": "Se guarda automáticamente.", "transcribe_coach_footer_kurrent": "Ayuda sobre Kurrent ↗", "transcribe_coach_footer_richtlinien": "Normas de transcripción ↗", - "transcription_mode_help_label": "Modo lectura y edición", "transcription_mode_help_body": "Lectura muestra la transcripción como texto continuo. Edición abre los campos de texto para cada pasaje.", - "richtlinien_title": "Normas de transcripción", "richtlinien_intro": "Para que todas las cartas se transcriban de forma uniforme — sin importar quién transcriba — aquí están nuestras reglas. La página crece con nosotros.", "richtlinien_wiki_text": "Los alfabetos Kurrent y Sütterlin están bien explicados en Wikipedia. Aquí solo se recogen nuestros propios acuerdos para este archivo.", @@ -948,12 +935,9 @@ "bulk_edit_all_x_failed": "No se pudieron cargar los resultados del filtro; vuelve a intentarlo.", "bulk_edit_topbar_title": "Edición masiva", "bulk_edit_count_pill": "Se editarán {count}", - "nav_stammbaum": "Árbol genealógico", "nav_geschichten": "Historias", - "error_geschichte_not_found": "No se encontró la historia.", - "geschichten_index_title": "Historias", "geschichten_new_button": "Nueva historia", "geschichten_filter_all_pill": "Todas", @@ -973,7 +957,6 @@ "geschichten_card_attach_action": "+ Adjuntar historia", "geschichten_card_show_all_for_person": "Todas las historias sobre {name}", "geschichten_card_show_all": "Mostrar todas", - "geschichte_editor_title_placeholder": "Título de la historia", "geschichte_editor_body_placeholder": "Escribe tu historia aquí…", "geschichte_editor_status_draft": "BORRADOR", @@ -1000,14 +983,11 @@ "geschichte_editor_toolbar_h3": "Subencabezado", "geschichte_editor_toolbar_ul": "Lista con viñetas", "geschichte_editor_toolbar_ol": "Lista numerada", - "geschichte_delete_confirm_title": "¿Eliminar historia?", "geschichte_delete_confirm_body": "Esta acción no se puede deshacer. La historia se eliminará permanentemente y se quitará de todas las páginas de personas y documentos vinculados.", - "error_relationship_not_found": "La relación no fue encontrada.", "error_circular_relationship": "Esta relación crearía un ciclo.", "error_duplicate_relationship": "Esta relación ya existe.", - "relation_parent_of": "Progenitor de", "relation_child_of": "Hijo/a de", "relation_spouse_of": "Cónyuge", @@ -1018,7 +998,6 @@ "relation_doctor": "Médico", "relation_neighbor": "Vecino/a", "relation_other": "Otro", - "relation_inferred_parent": "Progenitor", "relation_inferred_child": "Hijo/a", "relation_inferred_spouse": "Cónyuge", @@ -1036,9 +1015,7 @@ "relation_inferred_sibling_inlaw": "Cuñado/a", "relation_inferred_cousin_1": "Primo/a", "relation_inferred_distant": "Pariente lejano", - "doc_details_field_relationship": "Parentesco", - "stammbaum_empty_heading": "Aún no hay miembros de la familia", "stammbaum_empty_body": "Marca a una persona como miembro de la familia en su página de edición para que aparezca aquí.", "stammbaum_empty_link": "→ Ir a la lista de personas", @@ -1050,7 +1027,6 @@ "stammbaum_zoom_in": "Acercar", "stammbaum_zoom_out": "Alejar", "stammbaum_generations": "Generaciones", - "relation_error_duplicate": "Esta relación ya existe.", "relation_error_circular": "Esta relación crearía un ciclo.", "relation_error_self": "Una persona no puede estar relacionada consigo misma.", @@ -1073,14 +1049,15 @@ "relation_form_field_from_year": "Desde año", "relation_form_field_to_year": "Hasta año", "relation_form_year_placeholder": "ej. 1920", - "person_relationships_heading": "Relaciones", "person_relationships_empty": "Aún no se conocen relaciones.", - "timeline_aria_label": "Cronología de densidad de documentos", "timeline_clear_selection": "Borrar selección", "timeline_zoom_reset": "Restablecer zoom", "timeline_bar_aria_singular": "{when}, 1 documento", "timeline_bar_aria_plural": "{when}, {count} documentos", - "timeline_dragging_aria_live": "Rango {from} a {to} seleccionado" + "timeline_dragging_aria_live": "Rango {from} a {to} seleccionado", + "error_page_id_label": "ID de error", + "error_copy_id_label": "Copiar ID", + "error_copied": "¡Copiado!" } diff --git a/frontend/src/routes/+error.svelte b/frontend/src/routes/+error.svelte index c64461e4..f910cb33 100644 --- a/frontend/src/routes/+error.svelte +++ b/frontend/src/routes/+error.svelte @@ -1,13 +1,47 @@ {m.page_title_error()} -
-

{page.status}

-

{page.error?.message ?? 'Internal Error'}

-
+
+

{m.page_title_error()}

+

+ {page.error?.message ?? m.error_internal_error()} +

+

{page.status}

+ + {#if page.error?.errorId} +
+

+ {m.error_page_id_label()} +

+ + {page.error.errorId} + + +
+ {/if} +
diff --git a/frontend/src/routes/error.svelte.test.ts b/frontend/src/routes/error.svelte.test.ts index 097dc3f6..7ed53214 100644 --- a/frontend/src/routes/error.svelte.test.ts +++ b/frontend/src/routes/error.svelte.test.ts @@ -4,7 +4,10 @@ import { page as browserPage } from 'vitest/browser'; const mockPage = { status: 500, - error: { message: 'Internal Error' } as { message: string } | null + error: { message: 'Internal Error', errorId: undefined } as { + message: string; + errorId?: string; + } | null }; vi.mock('$app/state', () => ({ @@ -13,6 +16,16 @@ vi.mock('$app/state', () => ({ } })); +vi.mock('$lib/paraglide/messages.js', () => ({ + m: { + page_title_error: () => 'Es ist etwas schiefgelaufen.', + error_internal_error: () => 'Ein unerwarteter Fehler ist aufgetreten.', + error_page_id_label: () => 'Fehler-ID', + error_copy_id_label: () => 'ID kopieren', + error_copied: () => 'Kopiert!' + } +})); + afterEach(cleanup); async function loadComponent() { @@ -20,7 +33,7 @@ async function loadComponent() { } describe('+error.svelte', () => { - it('renders the page status code prominently', async () => { + it('renders the page status code', async () => { mockPage.status = 404; mockPage.error = { message: 'Not Found' }; @@ -40,13 +53,45 @@ describe('+error.svelte', () => { await expect.element(browserPage.getByText('Database unavailable')).toBeVisible(); }); - it('falls back to the literal "Internal Error" when page.error is null', async () => { + it('falls back to error_internal_error message when page.error is null', async () => { mockPage.status = 500; mockPage.error = null; const ErrorPage = await loadComponent(); render(ErrorPage); - await expect.element(browserPage.getByText('Internal Error')).toBeVisible(); + await expect + .element(browserPage.getByText('Ein unerwarteter Fehler ist aufgetreten.')) + .toBeVisible(); + }); + + it('shows errorId when page.error.errorId is set', async () => { + mockPage.status = 500; + mockPage.error = { message: 'Something broke', errorId: 'abc-123-def' }; + + const ErrorPage = await loadComponent(); + render(ErrorPage); + + await expect.element(browserPage.getByText('abc-123-def')).toBeVisible(); + }); + + it('shows copy button when errorId is present', async () => { + mockPage.status = 500; + mockPage.error = { message: 'Something broke', errorId: 'abc-123-def' }; + + const ErrorPage = await loadComponent(); + render(ErrorPage); + + await expect.element(browserPage.getByRole('button', { name: 'ID kopieren' })).toBeVisible(); + }); + + it('does not render errorId section when errorId is absent', async () => { + mockPage.status = 500; + mockPage.error = { message: 'Something broke' }; + + const ErrorPage = await loadComponent(); + render(ErrorPage); + + await expect.element(browserPage.getByText('Fehler-ID')).not.toBeInTheDocument(); }); });