diff --git a/test/termineIcs.test.js b/test/termineIcs.test.js index 8aa85c2..89b3f90 100644 --- a/test/termineIcs.test.js +++ b/test/termineIcs.test.js @@ -28,8 +28,17 @@ const termin = (extra = {}) => ({ }) const enc = new TextEncoder() -const dec = new TextDecoder('utf-8', { fatal: true }) const octets = (s) => enc.encode(s).length +/** + * Jede physische Zeile hoechstens 75 Oktette und wohlgeformtes UTF-16 (kein halbes Surrogatpaar). Ein Decode-Test + * (TextDecoder) taugt dafuer nicht: JS-Strings kodieren immer, einzelne Surrogate werden still zu U+FFFD. + */ +const pruefeZeilen = (text) => { + for (const zeile of text.split('\r\n')) { + assert.ok(octets(zeile) <= 75, `Zeile zu lang (${octets(zeile)}): ${zeile}`) + assert.ok(zeile.isWellFormed(), `Zeile mit halbem Surrogatpaar: ${JSON.stringify(zeile)}`) + } +} /** Zeilenfaltung rueckgaengig machen: CRLF + Leerzeichen faellt weg. */ const entfalte = (text) => text.replace(/\r\n /g, '') /** Logische Zeilen (entfaltet, ohne das abschliessende CRLF). */ @@ -80,16 +89,38 @@ test('Zeilenlaenge: jede Zeile hoechstens 75 Oktette, nie mitten in einem UTF-8- for (let n = 40; n < 130; n += 1) { const titel = baustein.repeat(n) const ics = icsTermin(termin({ titel, ort: titel, beschreibung: titel, organisator: { name: titel, email: 'info@salon-muster.de' }, teilnehmer: { name: titel, email: 'anna@example.com' } })) - for (const zeile of ics.split('\r\n')) { - assert.ok(octets(zeile) <= 75, `Zeile zu lang (${octets(zeile)}): ${zeile}`) - assert.doesNotThrow(() => dec.decode(enc.encode(zeile))) - } + pruefeZeilen(ics) for (const folge of ics.split('\r\n').filter((l) => l.startsWith(' '))) assert.ok(folge.length > 1, 'Folgezeile nicht leer') assert.equal(entescape(wert(ics, 'SUMMARY').slice('SUMMARY:'.length)), titel, 'Entfalten ergibt den Titel wieder') } } }) +test('Zeilenlaenge: Emoji (Surrogatpaare) werden nie zwischen ihren Haelften gefaltet, in Termin und Abo', () => { + let gefaltet = 0 + for (const vorsatz of ['', 'a', 'ab', 'abc']) { + for (let n = 1; n < 60; n += 1) { + const titel = vorsatz + '😀'.repeat(n) + const felder = { titel, ort: titel, beschreibung: titel, organisator: { name: titel, email: 'info@salon-muster.de' }, teilnehmer: { name: titel, email: 'anna@example.com' } } + const ics = icsTermin(termin(felder)) + pruefeZeilen(ics) + gefaltet += ics.split('\r\n').filter((l) => l.startsWith(' ')).length + assert.equal(entescape(wert(ics, 'SUMMARY').slice('SUMMARY:'.length)), titel) + assert.equal(entescape(wert(ics, 'DESCRIPTION').slice('DESCRIPTION:'.length)), titel) + const feed = icsKalender([termin(felder)], { name: titel, jetzt: JETZT }) + pruefeZeilen(feed) + assert.equal(entescape(wert(feed, 'X-WR-CALNAME').slice('X-WR-CALNAME:'.length)), titel) + } + } + assert.ok(gefaltet > 500, `der Test faltet wirklich (${gefaltet} Folgezeilen)`) + // Beispiel: DESCRIPTION: ist 12 Oktette lang, danach passen 15 Emoji (60 Oktette) in die erste Zeile, das 16. beginnt die Folgezeile + const beschreibung = '😀'.repeat(16) + const zeilen = icsTermin(termin({ beschreibung })).split('\r\n') + const i = zeilen.findIndex((l) => l.startsWith('DESCRIPTION:')) + assert.equal(zeilen[i], `DESCRIPTION:${'😀'.repeat(15)}`) + assert.equal(zeilen[i + 1], ' 😀') +}) + test('Zeilenlaenge: eine Zeile von genau 75 Oktetten bleibt ungefaltet, 76 wird gefaltet', () => { const zeileMit = (n) => icsTermin(termin({ titel: 'x'.repeat(n - 'SUMMARY:'.length) })).split('\r\n').find((l) => l.startsWith('SUMMARY:')) assert.equal(octets(zeileMit(75)), 75)