import { DEFAULT_WELCOME_MAIL_TEXTS } from '@tessera/shared'; import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import de from '@/messages/de.json'; // Namensraumfaehiger next-intl-Mock gegen die echten deutschen Texte: // Punktschluessel werden aufgeloest, {param} wird eingesetzt. vi.mock('next-intl', () => ({ useLocale: () => 'de', useTranslations: (ns: string) => (key: string, params?: Record) => { let val: unknown = de; for (const part of `${ns}.${key}`.split('.')) { val = (val as Record | undefined)?.[part]; } if (typeof val !== 'string') return `${ns}.${key}`; return params ? val.replace(/\{(\w+)\}/g, (_m, name: string) => String(params[name] ?? '')) : val; }, })); const api = vi.hoisted(() => ({ fetchWelcomeMailTemplate: vi.fn(), saveWelcomeMailTemplate: vi.fn(), resetWelcomeMailTemplate: vi.fn(), previewWelcomeMail: vi.fn(), sendWelcomeMailTest: vi.fn(), })); vi.mock('@/lib/welcome-mail-template-api', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, ...api }; }); import { WelcomeMailTemplateRequestError } from '@/lib/welcome-mail-template-api'; import { WelcomeMailTemplateEditor } from './welcome-mail-template-editor'; const defaults = { ...DEFAULT_WELCOME_MAIL_TEXTS }; const custom = { subject: 'Hallo {{vorname}}', heading: 'Willkommen, {{name}}!', intro: 'Eigene Einleitung', loginHintDirectory: 'Eigener Verzeichnis-Hinweis', loginHintLocal: 'Eigener Lokal-Hinweis', closing: 'Ihr IT-Team', }; function state(isCustom: boolean) { return { custom: isCustom, texts: isCustom ? { ...custom } : { ...defaults }, defaults: { ...defaults }, updatedAt: isCustom ? '2026-09-30T10:00:00.000Z' : null, updatedBy: isCustom ? 'ada' : null, }; } beforeEach(() => { vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) => { cb(0); return 0; }); api.fetchWelcomeMailTemplate.mockResolvedValue(state(false)); api.previewWelcomeMail.mockImplementation(async (texts: typeof defaults, account: string) => ({ subject: texts.subject, html: `Vorschau ${account}`, text: '', })); api.saveWelcomeMailTemplate.mockImplementation(async (texts: typeof defaults) => ({ ...state(true), texts: { ...texts }, })); api.resetWelcomeMailTemplate.mockResolvedValue(state(false)); api.sendWelcomeMailTest.mockResolvedValue({ success: true, to: 'ada@example.invalid' }); }); afterEach(() => { cleanup(); vi.clearAllMocks(); vi.unstubAllGlobals(); }); async function renderLoaded() { render(); return (await screen.findByLabelText('Betreff')) as HTMLInputElement; } describe('WelcomeMailTemplateEditor', () => { it('laedt die Standardtexte ins Formular und zeigt Hinweis auf die festen Bausteine', async () => { const subject = await renderLoaded(); expect(subject.value).toBe(defaults.subject); expect((screen.getByLabelText('Überschrift') as HTMLInputElement).value).toBe(defaults.heading); expect((screen.getByLabelText('Einleitungstext') as HTMLTextAreaElement).value).toBe( defaults.intro, ); expect((screen.getByLabelText('Abschlusstext') as HTMLTextAreaElement).value).toBe( defaults.closing, ); expect(screen.getByTestId('welcome-template-status').textContent).toContain( 'Zurzeit gelten die Standardtexte.', ); expect( screen.getByText(/So kann eine eigene Vorlage die Anmeldung nicht kaputt machen/), ).toBeTruthy(); }); it('Hinweisbox Platzhalter: Tabelle mit allen sechs Platzhaltern, Bedeutung und Beispiel', async () => { await renderLoaded(); const box = screen.getByRole('region', { name: 'Platzhalter' }); const rows = within(box).getAllByRole('row'); expect(rows).toHaveLength(7); // Kopf + 6 for (const token of [ '{{name}}', '{{vorname}}', '{{benutzername}}', '{{email}}', '{{adresse}}', '{{firma}}', ]) { expect(within(box).getByRole('button', { name: `${token} einfügen` })).toBeTruthy(); } expect(within(box).getByText('Name Ihres Mandanten')).toBeTruthy(); expect(within(box).getByText('max.mustermann@example.com')).toBeTruthy(); }); it('Klick auf einen Platzhalter fuegt ihn an der Cursorposition des zuletzt fokussierten Feldes ein', async () => { const subject = await renderLoaded(); fireEvent.change(subject, { target: { value: 'Hallo bei uns' } }); subject.focus(); subject.setSelectionRange(6, 6); fireEvent.click(screen.getByRole('button', { name: '{{vorname}} einfügen' })); expect(subject.value).toBe('Hallo {{vorname}} bei uns'); }); it('unbekannter Platzhalter wird benannt, Speichern und Testmail sind gesperrt', async () => { const user = userEvent.setup(); await renderLoaded(); const heading = screen.getByLabelText('Überschrift'); await user.clear(heading); fireEvent.change(heading, { target: { value: 'Hallo {{xyz}}' } }); expect(await screen.findByText('Unbekannter Platzhalter {{xyz}}')).toBeTruthy(); expect((screen.getByRole('button', { name: 'Speichern' }) as HTMLButtonElement).disabled).toBe( true, ); expect( (screen.getByRole('button', { name: 'Testmail an mich senden' }) as HTMLButtonElement) .disabled, ).toBe(true); }); it('Anmeldehinweise: beide Felder nach dem Einleitungstext, mit Standardtext bzw. eigener Vorlage vorbelegt', async () => { await renderLoaded(); const dir = screen.getByLabelText( 'Anmeldehinweis für Verzeichniskonten', ) as HTMLTextAreaElement; const local = screen.getByLabelText('Anmeldehinweis für lokale Konten') as HTMLTextAreaElement; expect(dir.value).toBe( 'Melden Sie sich mit Ihrem Benutzernamen und Ihrem gewohnten Windows-Passwort an.', ); expect(local.value).toBe( 'Bevor Sie sich zum ersten Mal anmelden, legen Sie bitte Ihr persönliches Passwort fest.', ); // Reihenfolge: Einleitung → Verzeichnis → lokal → Abschluss const intro = screen.getByLabelText('Einleitungstext'); const closing = screen.getByLabelText('Abschlusstext'); const follows = (a: Element, b: Element) => Boolean(a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING); expect(follows(intro, dir)).toBe(true); expect(follows(dir, local)).toBe(true); expect(follows(local, closing)).toBe(true); expect(screen.getByText(/direkt vor dem Knopf „Passwort festlegen“/)).toBeTruthy(); cleanup(); api.fetchWelcomeMailTemplate.mockResolvedValue(state(true)); await renderLoaded(); expect( (screen.getByLabelText('Anmeldehinweis für Verzeichniskonten') as HTMLTextAreaElement).value, ).toBe(custom.loginHintDirectory); expect( (screen.getByLabelText('Anmeldehinweis für lokale Konten') as HTMLTextAreaElement).value, ).toBe(custom.loginHintLocal); }); it('Anmeldehinweis: Pflichtfeld, Platzhalter-Klick fuegt ein, Fokus schaltet die Vorschau auf die Kontoart', async () => { await renderLoaded(); const local = screen.getByLabelText('Anmeldehinweis für lokale Konten') as HTMLTextAreaElement; fireEvent.change(local, { target: { value: 'Hallo ' } }); local.focus(); local.setSelectionRange(6, 6); fireEvent.click(screen.getByRole('button', { name: '{{vorname}} einfügen' })); expect(local.value).toBe('Hallo {{vorname}}'); expect(screen.getByRole('button', { name: 'Lokales Konto' }).getAttribute('aria-pressed')).toBe( 'true', ); fireEvent.change(local, { target: { value: ' ' } }); expect(await screen.findByText('Bitte füllen Sie dieses Feld aus.')).toBeTruthy(); expect((screen.getByRole('button', { name: 'Speichern' }) as HTMLButtonElement).disabled).toBe( true, ); }); it('Speichern schickt alle sechs Texte inkl. Anmeldehinweise', async () => { const user = userEvent.setup(); await renderLoaded(); fireEvent.change(screen.getByLabelText('Anmeldehinweis für Verzeichniskonten'), { target: { value: 'Mit dem Firmenkennwort anmelden, {{vorname}}.' }, }); fireEvent.change(screen.getByLabelText('Anmeldehinweis für lokale Konten'), { target: { value: 'Erst Passwort setzen.' }, }); await user.click(screen.getByRole('button', { name: 'Speichern' })); expect(api.saveWelcomeMailTemplate).toHaveBeenCalledWith({ ...defaults, loginHintDirectory: 'Mit dem Firmenkennwort anmelden, {{vorname}}.', loginHintLocal: 'Erst Passwort setzen.', }); }); it('Speichern schickt die Texte und meldet Erfolg; Status zeigt die eigene Vorlage', async () => { const user = userEvent.setup(); const subject = await renderLoaded(); fireEvent.change(subject, { target: { value: 'Neu für {{firma}}' } }); await user.click(screen.getByRole('button', { name: 'Speichern' })); expect(api.saveWelcomeMailTemplate).toHaveBeenCalledWith({ ...defaults, subject: 'Neu für {{firma}}', }); expect((await screen.findByRole('status')).textContent).toContain( 'Die Vorlage wurde gespeichert.', ); expect(screen.getByTestId('welcome-template-status').textContent).toContain( 'Eigene Vorlage aktiv', ); expect(screen.getByTestId('welcome-template-status').textContent).toContain('von ada'); }); it('Fehler der API beim Speichern wird angezeigt (400 mit Meldung)', async () => { const user = userEvent.setup(); api.saveWelcomeMailTemplate.mockRejectedValueOnce( new WelcomeMailTemplateRequestError(400, ['Betreff: Unbekannter Platzhalter {{abc}}']), ); await renderLoaded(); await user.click(screen.getByRole('button', { name: 'Speichern' })); expect((await screen.findByRole('alert')).textContent).toBe( 'Betreff: Unbekannter Platzhalter {{abc}}', ); }); it('Auf Standard zuruecksetzen: Rueckfrage, DELETE, danach Standardtexte im Formular', async () => { const user = userEvent.setup(); api.fetchWelcomeMailTemplate.mockResolvedValue(state(true)); const subject = await renderLoaded(); expect(subject.value).toBe(custom.subject); await user.click(screen.getByRole('button', { name: 'Auf Standard zurücksetzen' })); const dialog = screen.getByRole('alertdialog'); expect(api.resetWelcomeMailTemplate).not.toHaveBeenCalled(); await user.click(within(dialog).getByRole('button', { name: 'Zurücksetzen' })); expect(api.resetWelcomeMailTemplate).toHaveBeenCalledTimes(1); await waitFor(() => expect(subject.value).toBe(defaults.subject)); expect(screen.getByTestId('welcome-template-status').textContent).toContain('Standardtexte'); expect(screen.queryByRole('alertdialog')).toBeNull(); }); it('Vorschau: iframe mit dem von der API gerenderten HTML, Umschalter lokales Konto', async () => { const user = userEvent.setup(); await renderLoaded(); const frame = screen.getByTitle('Vorschau der Willkommensmail') as HTMLIFrameElement; await waitFor(() => expect(frame.getAttribute('srcdoc')).toContain('Vorschau directory')); expect(frame.getAttribute('sandbox')).toBe(''); await user.click(screen.getByRole('button', { name: 'Lokales Konto' })); await waitFor(() => expect(frame.getAttribute('srcdoc')).toContain('Vorschau local')); expect(api.previewWelcomeMail).toHaveBeenLastCalledWith(defaults, 'local', expect.anything()); expect(screen.getByTestId('welcome-preview-subject').textContent).toBe(defaults.subject); }); it('Testmail an mich: sendet den aktuellen (ungespeicherten) Inhalt und nennt die Adresse', async () => { const user = userEvent.setup(); const subject = await renderLoaded(); fireEvent.change(subject, { target: { value: 'Ungespeichert' } }); await user.click(screen.getByRole('button', { name: 'Testmail an mich senden' })); expect(api.sendWelcomeMailTest).toHaveBeenCalledWith({ ...defaults, subject: 'Ungespeichert' }); expect(api.saveWelcomeMailTemplate).not.toHaveBeenCalled(); expect((await screen.findByRole('status')).textContent).toBe( 'Die Testmail wurde an ada@example.invalid gesendet.', ); }); it('Ladefehler wird angezeigt', async () => { api.fetchWelcomeMailTemplate.mockRejectedValueOnce(new Error('down')); render(); expect((await screen.findByRole('alert')).textContent).toContain('konnte nicht geladen werden'); }); });