feat(observability): redesign +error.svelte with errorId display and copy button
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,13 +1,47 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
|
||||
let copied = $state(false);
|
||||
|
||||
function copyId() {
|
||||
const id = page.error?.errorId;
|
||||
if (!id) return;
|
||||
navigator.clipboard.writeText(id).then(() => {
|
||||
copied = true;
|
||||
setTimeout(() => (copied = false), 2000);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{m.page_title_error()}</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="px-4 py-12 text-center font-sans">
|
||||
<p class="font-sans text-6xl font-bold text-ink">{page.status}</p>
|
||||
<p class="mt-2 font-sans text-sm text-ink-2">{page.error?.message ?? 'Internal Error'}</p>
|
||||
</div>
|
||||
<main class="px-4 py-12 text-center font-sans">
|
||||
<h1 class="mb-2 font-serif text-2xl font-bold text-ink">{m.page_title_error()}</h1>
|
||||
<p class="mb-8 font-sans text-sm text-ink-2">
|
||||
{page.error?.message ?? m.error_internal_error()}
|
||||
</p>
|
||||
<p class="font-sans text-xs tracking-widest text-ink-3 uppercase">{page.status}</p>
|
||||
|
||||
{#if page.error?.errorId}
|
||||
<div class="mt-6 flex flex-col items-center gap-3">
|
||||
<p class="font-sans text-xs tracking-widest text-ink-2 uppercase">
|
||||
{m.error_page_id_label()}
|
||||
</p>
|
||||
<code
|
||||
class="rounded border border-line bg-surface px-3 py-1 font-mono text-sm text-ink select-all"
|
||||
>
|
||||
{page.error.errorId}
|
||||
</code>
|
||||
<button
|
||||
class="min-h-[44px] rounded-sm bg-brand-navy px-5 py-2 font-sans text-sm text-white transition-colors hover:opacity-90 focus-visible:ring-2 focus-visible:ring-brand-navy focus-visible:ring-offset-2"
|
||||
onclick={copyId}
|
||||
aria-label={m.error_copy_id_label()}
|
||||
>
|
||||
<span aria-live="polite">{copied ? m.error_copied() : m.error_copy_id_label()}</span>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</main>
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user