714f731ac9
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>
299 lines
12 KiB
TypeScript
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');
|
|
});
|
|
});
|