feat(quick-260916-dcz): CHANGELOG.md, Seite "Was ist neu" mit Kanalfilter, Versionszeile als Link, Bauzeit-Einbettung
- CHANGELOG.md (Wurzel): Unveroeffentlicht (bwo + dyv + diese Seite, 8 Punkte) und 1.0.0 – 2026-09-15 (11 Punkte aus den Handbuechern), Alltagssprache, echte Umlaute - next.config.ts liest CHANGELOG.md zur Bauzeit nach env.TESSERA_CHANGELOG_MD; .dockerignore-Ausnahme !CHANGELOG.md und COPY-Zeile im Web-Dockerfile - lib/changelog.ts: filterChangelogForChannel (live ohne Unveroeffentlicht, beta/dev mit "Noch nicht freigegeben (Beta)", leerer Abschnitt ausgeblendet), 10 Tests - Server-Seite /changelog (MDEditor.Markdown + rehype-sanitize, Hinweis auf Beta, Leer-Text), 3 Tests; Versionszeile ist Link mit aria-label, 2 Tests - de/en: sidebar.whatsNew und Namensraum changelog; Web-Suite 49/309, tsc 0 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018N9CD3ebPKm1b32bPpBknY
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
import { cleanup, render, screen } from '@testing-library/react';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
/**
|
||||
* changelog-page.test — Seite "Was ist neu" unter /changelog (quick-260916-dcz).
|
||||
*
|
||||
* Die Seite ist eine async Server-Komponente (Vorbild module-access-gate.test:
|
||||
* Funktion awaiten, Ergebnis rendern). `next-intl/server` bekommt eine
|
||||
* Handtabelle, `@/lib/app-version` einen veraenderbaren Kanal, `@/lib/changelog`
|
||||
* wird je Test per `vi.doMock` mit `importOriginal` (echte Filterfunktion,
|
||||
* eigener `changelogMarkdown`) bereitgestellt, `@uiw/react-md-editor` als
|
||||
* Modul mit `default.Markdown`, das die Quelle roh in ein <pre> schreibt.
|
||||
*/
|
||||
|
||||
vi.mock('next-intl/server', () => ({
|
||||
getTranslations: async () => (key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
title: 'Was ist neu',
|
||||
intro: 'Alle Änderungen an Tessera, sortiert nach Version – die aktuelle Version steht oben.',
|
||||
unreleasedHeading: 'Noch nicht freigegeben (Beta)',
|
||||
unreleasedHint:
|
||||
'Die Punkte unter „Noch nicht freigegeben“ sind in dieser Beta bereits enthalten, aber noch nicht als Version freigegeben.',
|
||||
empty: 'Noch keine Einträge vorhanden.',
|
||||
};
|
||||
return translations[key] ?? key;
|
||||
},
|
||||
}));
|
||||
|
||||
let mockChannel: 'beta' | 'live' | 'dev' = 'live';
|
||||
vi.mock('@/lib/app-version', () => ({
|
||||
get appVersion() {
|
||||
return { version: 'v1.2.3', channel: mockChannel, commit: 'abc1234' };
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@uiw/react-md-editor', () => ({
|
||||
default: {
|
||||
Markdown: ({ source }: { source?: string }) => <pre data-testid="md">{source}</pre>,
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('next-themes', () => ({
|
||||
useTheme: () => ({ resolvedTheme: 'light' }),
|
||||
}));
|
||||
|
||||
const SAMPLE = [
|
||||
'# Änderungen an Tessera',
|
||||
'',
|
||||
'Vorspann.',
|
||||
'',
|
||||
'## Unveröffentlicht',
|
||||
'',
|
||||
'### Neu',
|
||||
'',
|
||||
'- Punkt U1',
|
||||
'',
|
||||
'## 1.0.0 – 2026-09-15',
|
||||
'',
|
||||
'### Neu',
|
||||
'',
|
||||
'- Punkt A',
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
function mockChangelog(markdown: string) {
|
||||
vi.doMock('@/lib/changelog', async (importOriginal) => {
|
||||
const original = await importOriginal<typeof import('@/lib/changelog')>();
|
||||
return { ...original, changelogMarkdown: markdown };
|
||||
});
|
||||
}
|
||||
|
||||
async function renderPage() {
|
||||
const { default: ChangelogPage } = await import('./page');
|
||||
const element = await ChangelogPage();
|
||||
render(element);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.resetModules();
|
||||
vi.doUnmock('@/lib/changelog');
|
||||
mockChannel = 'live';
|
||||
});
|
||||
|
||||
describe('ChangelogPage /changelog (quick-260916-dcz)', () => {
|
||||
it('Test 1 (live): Titel sichtbar, kein Hinweis, Markdown ohne Unveröffentlicht', async () => {
|
||||
mockChannel = 'live';
|
||||
mockChangelog(SAMPLE);
|
||||
await renderPage();
|
||||
|
||||
expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('Was ist neu');
|
||||
expect(screen.queryByTestId('changelog-unreleased-hint')).not.toBeInTheDocument();
|
||||
const md = screen.getByTestId('md').textContent ?? '';
|
||||
expect(md).toContain('1.0.0');
|
||||
expect(md).not.toContain('Unveröffentlicht');
|
||||
expect(md).not.toContain('Noch nicht freigegeben');
|
||||
});
|
||||
|
||||
it('Test 2 (beta): Hinweis vorhanden, Markdown traegt "Noch nicht freigegeben (Beta)"', async () => {
|
||||
mockChannel = 'beta';
|
||||
mockChangelog(SAMPLE);
|
||||
await renderPage();
|
||||
|
||||
expect(screen.getByTestId('changelog-unreleased-hint')).toHaveTextContent(
|
||||
'Die Punkte unter „Noch nicht freigegeben“ sind in dieser Beta bereits enthalten, aber noch nicht als Version freigegeben.',
|
||||
);
|
||||
const md = screen.getByTestId('md').textContent ?? '';
|
||||
expect(md).toContain('Noch nicht freigegeben (Beta)');
|
||||
expect(md).toContain('- Punkt U1');
|
||||
});
|
||||
|
||||
it('Test 3 (leer): ohne Text erscheint der Leer-Hinweis und kein Markdown-Element', async () => {
|
||||
mockChannel = 'beta';
|
||||
mockChangelog('');
|
||||
await renderPage();
|
||||
|
||||
expect(screen.getByTestId('changelog-empty')).toHaveTextContent('Noch keine Einträge vorhanden.');
|
||||
expect(screen.queryByTestId('md')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import { ChangelogView } from '@/components/changelog/changelog-view';
|
||||
import { appVersion } from '@/lib/app-version';
|
||||
import { changelogMarkdown, filterChangelogForChannel } from '@/lib/changelog';
|
||||
|
||||
/**
|
||||
* Seite "Was ist neu" unter /changelog (quick-260916-dcz).
|
||||
*
|
||||
* Async Server-Komponente ohne 'use client' (Vorbild module-access-gate.tsx):
|
||||
* der Text stammt aus der Bauzeit-Variable `TESSERA_CHANGELOG_MD`
|
||||
* (next.config.ts liest CHANGELOG.md) und bleibt so im Server-Bundle. Die
|
||||
* Kanalregel liegt in `filterChangelogForChannel`: auf Live fehlt der
|
||||
* Abschnitt "Unveröffentlicht", auf Beta/Entwicklung erscheint er als "Noch
|
||||
* nicht freigegeben (Beta)" samt Hinweis. Zugriff nur angemeldet — die
|
||||
* bestehende middleware.ts schuetzt jede Route ausser /login und
|
||||
* /reset-password; /changelog ist keine oeffentliche Route.
|
||||
*/
|
||||
export default async function ChangelogPage() {
|
||||
const t = await getTranslations('changelog');
|
||||
const { markdown, hasUnreleased } = filterChangelogForChannel(changelogMarkdown, appVersion.channel, {
|
||||
unreleasedHeading: t('unreleasedHeading'),
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-3xl py-8 px-4">
|
||||
<h1 className="text-2xl font-bold text-foreground mb-2">{t('title')}</h1>
|
||||
<p className="text-sm text-muted-foreground mb-6">{t('intro')}</p>
|
||||
|
||||
{hasUnreleased && (
|
||||
<div
|
||||
data-testid="changelog-unreleased-hint"
|
||||
className="rounded-md bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 px-4 py-3 text-sm text-yellow-800 dark:text-yellow-200 mb-6"
|
||||
>
|
||||
{t('unreleasedHint')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{markdown.trim() === '' ? (
|
||||
<p data-testid="changelog-empty" className="text-sm text-muted-foreground">
|
||||
{t('empty')}
|
||||
</p>
|
||||
) : (
|
||||
<ChangelogView markdown={markdown} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user