feat(admin): eigene Vorlage fuer die Willkommensmail mit Platzhaltern, Vorschau und Testmail
Administrator -> Willkommensmail: Betreff, Ueberschrift, Einleitung, Abschluss je
Mandant (Tabelle WelcomeMailTemplate, RLS je Mandant, Migration 20260930150000);
Platzhalter {{name}} {{vorname}} {{benutzername}} {{email}} {{adresse}} {{firma}},
unbekannte -> 400 bzw. Hinweis beim Tippen; Werte escaped, Vorlage reiner Text.
Live-Vorschau per API gerendert, Testmail an die eigene Adresse ohne Token,
Zuruecksetzen auf Standard. Feste Bausteine (Kopf, Zugangsdaten, Anmeldehinweis,
Knoepfe, Fusszeile) bleiben immer drin.
Kopf: Wellenzelle dunkel statt weiss, Streifen 600x40, Inhalt 24 px naeher –
keine weisse Luecke, wenn OWA das CID-Bild nicht zeigt.
Lokal nachgewiesen: Hinweis/Sperre bei {{xyz}}, Speichern, Testmail (Link nur
/login), echte Mail mit eigener Vorlage und 7-Tage-Link.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
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',
|
||||
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('Speichern schickt die vier 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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user