Files
tessera-ctl/apps/web/src/components/admin/welcome-mail-template-editor.test.tsx
T
schalli 714f731ac9
Tessera CI/CD / Lint & Type Check (push) Successful in 51s
Tessera CI/CD / Tests (push) Successful in 1m29s
Tessera CI/CD / Desktop-Pakete bauen (push) Successful in 19s
Tessera CI/CD / Build & Publish Images (push) Successful in 4m14s
feat(admin): Anmeldehinweise der Willkommensmail in der Vorlage bearbeitbar
Neue Felder loginHintDirectory/loginHintLocal (Platzhalter, Pflicht, max. 1000),
Migration 20260930170000 (nullable, leer = Standard aus @tessera/shared).
Knopf Passwort festlegen und Gueltigkeitshinweis bleiben fest; local-no-link
wird nie erzeugt und bleibt fest. Vorschau springt beim Bearbeiten auf die
passende Kontoart. Lokal im Browser nachgewiesen.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 17:56:05 +02:00

299 lines
12 KiB
TypeScript

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<string, unknown>) => {
let val: unknown = de;
for (const part of `${ns}.${key}`.split('.')) {
val = (val as Record<string, unknown> | 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<typeof import('@/lib/welcome-mail-template-api')>();
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: `<html><body>Vorschau ${account}</body></html>`,
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(<WelcomeMailTemplateEditor />);
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(<WelcomeMailTemplateEditor />);
expect((await screen.findByRole('alert')).textContent).toContain('konnte nicht geladen werden');
});
});