Fix: Umbrueche bleiben beim Bearbeiten von Legacy-PMs erhalten

Bestehende (importierte) Pressemitteilungen speichern ihren Text als
Plaintext mit \n-Umbruechen. Beim Laden in den HTML-Editor (flux:editor)
kollabierten diese zu einem Fliesssatz, weil HTML Whitespace faltet.

Die Plaintext->HTML-Konvertierung (Doppelumbruch = Absatz, Einzelumbruch
= <br>) lag bisher nur im Anzeige-Pfad (render()). Sie ist jetzt als
toEditorHtml() wiederverwendbar und wird beim Laden des Editors genutzt;
die Link-Policy (rel/target) bleibt dabei dem Anzeige-Pfad vorbehalten.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-06-18 12:57:45 +00:00
parent 624fca68a8
commit bcac326814
4 changed files with 93 additions and 4 deletions

View file

@ -59,6 +59,36 @@ test('editor shows the content score panel when a score exists', function () {
->assertSee('Noch 13 Punkte bis zur nächsten Stufe.');
});
test('mount converts legacy plain-text body to editable html so line breaks survive', function () {
/** @var TestCase $this */
['customer' => $customer, 'pr' => $pr] = makeCustomerWithPressRelease([
'text' => "Erster Absatz mit Inhalt.\nZweite Zeile desselben Absatzes.\n\nNeuer Absatz nach Leerzeile.",
]);
$this->actingAs($customer);
$component = LivewireVolt::test('customer.press-releases.edit', ['id' => $pr->id]);
$loaded = $component->get('text');
expect($loaded)->toContain('<p>');
expect($loaded)->toContain('Erster Absatz mit Inhalt.<br');
expect($loaded)->toContain('<p>Neuer Absatz nach Leerzeile.</p>');
});
test('mount leaves stored html body untouched in structure', function () {
/** @var TestCase $this */
['customer' => $customer, 'pr' => $pr] = makeCustomerWithPressRelease([
'text' => '<p>Absatz eins.</p><p>Absatz zwei.</p>',
]);
$this->actingAs($customer);
$loaded = LivewireVolt::test('customer.press-releases.edit', ['id' => $pr->id])->get('text');
expect($loaded)->toContain('<p>Absatz eins.</p>');
expect($loaded)->toContain('<p>Absatz zwei.</p>');
});
test('mount loads all Phase 7 fields and pivot contact', function () {
/** @var TestCase $this */
['customer' => $customer, 'pr' => $pr, 'contact' => $contact] = makeCustomerWithPressRelease([

View file

@ -107,6 +107,34 @@ test('render returns sanitized html for stored html content', function () {
expect($html)->not->toContain('<script');
});
test('toEditorHtml converts legacy plain text to paragraphs and br tags', function () {
$plain = "Zeile eins\nZeile zwei\n\nNeuer Absatz";
$html = sanitizer()->toEditorHtml($plain);
expect($html)->toContain('<p>');
expect($html)->toContain('Zeile eins<br');
expect($html)->toContain('Zeile zwei');
expect($html)->toContain('<p>Neuer Absatz</p>');
});
test('toEditorHtml keeps stored html but strips dangerous tags without baking in rel attributes', function () {
$stored = '<p>Hallo</p><p><a href="https://example.test">Link</a></p><script>bad()</script>';
$html = sanitizer()->toEditorHtml($stored);
expect($html)->toContain('<p>Hallo</p>');
expect($html)->not->toContain('<script');
// Link-Policy (sponsored/noopener) gehört nicht in den Editor — die wird
// erst beim Anzeigen über render() gesetzt, nicht in den Editor-Inhalt.
expect($html)->not->toContain('sponsored');
});
test('toEditorHtml returns empty string for null and whitespace', function () {
expect(sanitizer()->toEditorHtml(null))->toBe('');
expect(sanitizer()->toEditorHtml(' '))->toBe('');
});
test('plainTextLength counts chars without html noise', function () {
expect(sanitizer()->plainTextLength('<p>Hallo Welt</p>'))->toBe(10);
expect(sanitizer()->plainTextLength('<p>Eins</p> <p>Zwei</p>'))->toBe(9);