Schreiben als E-Mail uebernehmen

Im Schreiben-Editor gibt es jetzt den Umschalter Brief/E-Mail. Die
E-Mail-Fassung wird aus dem - womoeglich bearbeiteten - Brieftext
abgeleitet: ohne Absenderblock, Anschrift und Datumszeile, aber mit dem
Bestellungsabsatz. Betreff und Text einzeln kopierbar.

Die offene Entscheidung aus dem Issue (mailto oder .eml) ist nach
Rueckfrage mit BEIDES beantwortet:

- mailto fuer kurze Texte. Der Knopf verschwindet, sobald der kodierte
  Link 1800 Zeichen uebersteigt - mailto uebergibt den Text in der
  Adresszeile, deren Laenge nirgends zugesichert ist, und ein halbierter
  Brief faellt erst beim Empfaenger auf. Die Grenze wird NACH der
  URL-Kodierung gerechnet; 900 Umlaute sind roh harmlos und kodiert
  weit drueber.
- .eml immer: RFC 5322 mit X-Unsent: 1, quoted-printable fuer den Rumpf,
  RFC-2047-Kodierung fuer Umlaute im Betreff. Bewusst ohne From-Kopf -
  ein erfundener Absender wuerde je nach Programm uebernommen.

Beide Knoepfe erscheinen nur mit E-Mail-Adresse (aus dem Kontakt
uebernommen oder von Hand). Ohne Adresse bleibt das Kopieren.

Die Ablage kennt den Unterschied: Eine E-Mail heisst in der Akte
"E-Mail: ...", und abgelegt wird die E-Mail-Fassung samt
Empfaengeradresse im Kopf der PDF - in der Akte soll stehen, was
tatsaechlich hinausging.

Sicherung in emailFassung(): Findet sich keine Anrede (frei umgebauter
Text), wird NICHTS entfernt. Lieber ein Briefkopf zu viel in der Mail
als stillschweigend verlorener Inhalt.

Geprueft: 20 Einheitstests, vier End-to-End-Tests auf beiden
Geraeteprofilen. 590 Unit-Tests, 81 e2e, null Typfehler.

Refs #5
This commit is contained in:
Florian Wilde
2026-08-23 11:02:00 +02:00
parent a52523fc97
commit b59d11c9b4
15 changed files with 877 additions and 37 deletions
+123
View File
@@ -0,0 +1,123 @@
import { expect, test } from '@playwright/test'
import { PrismaClient } from '@prisma/client'
import { anmelden } from './hilfen'
/**
* Schreiben als E-Mail — gegen die echte Anwendung.
*
* Geprüft wird der Weg, den die Einheitstests nicht sehen: Umschalten auf
* E-Mail, die abgeleitete Fassung ohne Briefkopf, der .eml-Download und
* die Ablage, die das Schreiben als E-Mail kennzeichnet.
*/
const KONTAKT = 'Prüfhausen Versicherung (E-Mail-Test)'
async function mitDb<T>(arbeit: (db: PrismaClient) => Promise<T>): Promise<T> {
const db = new PrismaClient()
try {
return await arbeit(db)
} finally {
await db.$disconnect()
}
}
async function aufraeumen(): Promise<void> {
await mitDb(async (db) => {
await db.dokument.deleteMany({ where: { empfaenger: { contains: 'Prüfhausen' } } })
await db.frist.deleteMany({ where: { titel: { contains: 'Antwort abwarten' } } })
await db.kontakt.deleteMany({ where: { name: { contains: 'Prüfhausen' } } })
})
}
test.describe('Schreiben als E-Mail', () => {
test.beforeAll(async () => {
await aufraeumen()
await mitDb(async (db) => {
const betreute = await db.betreute.findMany({
where: { geloeschtAm: null },
select: { id: true },
})
for (const person of betreute) {
await db.kontakt.create({
data: {
betreuteId: person.id,
name: KONTAKT,
organisation: KONTAKT,
rolle: 'SONSTIGES',
email: 'service@pruefhausen.example',
},
})
}
})
})
test.afterAll(async () => {
await aufraeumen()
})
test('leitet die E-Mail-Fassung ohne Briefkopf ab, mit Bestellungsabsatz', async ({
page,
}) => {
await anmelden(page)
await page.goto('/schreiben/neu?art=FREI')
// Kontakt wählen - bringt die E-Mail-Adresse mit.
await page.locator('select#kontaktAuswahl').selectOption({ label: `${KONTAKT}` })
await expect(page.locator('input#empfaengerEmail')).toHaveValue(
'service@pruefhausen.example',
)
await page.getByRole('button', { name: 'Als E-Mail' }).click()
const mailtext = await page.locator('textarea#emailText').inputValue()
// Beginnt mit der Anrede - kein Absenderblock, keine Datumszeile.
expect(mailtext.startsWith('Sehr geehrte')).toBe(true)
expect(mailtext).not.toMatch(/^\d{1,2}\.\d{1,2}\.\d{4}$/m)
// Der Nachweis der Bestellung bleibt drin.
expect(mailtext).toMatch(/Betreuungsgericht|bestellt/i)
})
test('lädt den .eml-Entwurf herunter', async ({ page }) => {
await anmelden(page)
await page.goto('/schreiben/neu?art=FREI')
await page.locator('select#kontaktAuswahl').selectOption({ label: `${KONTAKT}` })
await page.getByRole('button', { name: 'Als E-Mail' }).click()
const [download] = await Promise.all([
page.waitForEvent('download'),
page.getByRole('button', { name: 'Als E-Mail-Entwurf (.eml)' }).click(),
])
expect(download.suggestedFilename()).toMatch(/^\d{4}-\d{2}-\d{2}_.*\.eml$/)
})
test('zeigt ohne Adresse keine Mailprogramm-Knöpfe', async ({ page }) => {
await anmelden(page)
await page.goto('/schreiben/neu?art=FREI')
await page.getByRole('button', { name: 'Als E-Mail' }).click()
// Kopieren geht immer, die Programm-Knöpfe nicht.
await expect(page.getByRole('button', { name: 'Text kopieren' }).last()).toBeVisible()
await expect(page.getByRole('button', { name: 'Als E-Mail-Entwurf (.eml)' })).toHaveCount(0)
await expect(page.getByRole('link', { name: 'Im Mailprogramm öffnen' })).toHaveCount(0)
await expect(page.getByText(/Trag oben eine E-Mail-Adresse ein/)).toBeVisible()
})
test('legt die E-Mail als solche gekennzeichnet in die Akte', async ({ page }) => {
await anmelden(page)
await page.goto('/schreiben/neu?art=FREI')
await page.locator('select#kontaktAuswahl').selectOption({ label: `${KONTAKT}` })
await page.locator('input#betreff').fill('Prüfhausen Testanfrage')
await page.getByRole('button', { name: 'Als E-Mail' }).click()
await page.getByRole('button', { name: 'In die Akte legen' }).click()
await expect(page.getByText('Das Schreiben liegt in der Akte.')).toBeVisible({
timeout: 30_000,
})
// In der Akte als E-Mail erkennbar, nicht als Brief.
await page.goto('/dokumente?richtung=AUSGANG')
await expect(page.locator('body')).toContainText('E-Mail: Prüfhausen Testanfrage')
})
})