const assert = require('assert'); const C = require('./caldav.js'); // --- unescapeText: single-pass, must survive adjacent backslashes --- assert.strictEqual(C.unescapeText('a\\,b'), 'a,b'); assert.strictEqual(C.unescapeText('a\\;b'), 'a;b'); assert.strictEqual(C.unescapeText('line1\\nline2'), 'line1\nline2'); assert.strictEqual(C.unescapeText('back\\\\slash'), 'back\\slash'); assert.strictEqual(C.unescapeText('lit\\\\nslash'), 'lit\\nslash'); // \\ then n => backslash + 'n', NOT newline // --- decodeXmlEntities --- assert.strictEqual(C.decodeXmlEntities('a"b&c '), 'a"b&c\r'); // --- unfold: CRLF or LF, strip exactly one WS, keep further spaces --- assert.deepStrictEqual(C.unfold('AB\r\n CD\r\nEF'), ['ABCD', 'EF']); assert.deepStrictEqual(C.unfold('AB\n\tCD'), ['ABCD']); assert.deepStrictEqual(C.unfold('A\r\n B'), ['A B']); // two leading spaces -> one kept // --- splitLine: first unquoted colon --- let p = C.splitLine('DTSTART;TZID="A:B":20260101T000000'); assert.strictEqual(p.name, 'DTSTART'); assert.strictEqual(p.value, '20260101T000000'); // --- parseIcs: timed UTC event --- const timedXml = '@@CAL /p/\t#5b8cff\tVEVENT\nBEGIN:VEVENT\r\nUID:u1\r\nSUMMARY:Picnic\r\nDTSTART:20260622T152500Z\r\nDTEND:20260622T171500Z\r\nEND:VEVENT'; let ev = C.parseIcs(timedXml)[0]; assert.strictEqual(ev.summary, 'Picnic'); assert.strictEqual(ev.cal, '/p/'); assert.strictEqual(ev.color, '#5b8cff'); assert.strictEqual(ev.allDay, false); assert.strictEqual(ev.startMs, Date.UTC(2026, 5, 22, 15, 25, 0)); assert.strictEqual(ev.endMs, Date.UTC(2026, 5, 22, 17, 15, 0)); // --- parseIcs: all-day event, exclusive DTEND --- const allDayXml = '@@CAL /p/\t#43c08a\tVEVENT\nBEGIN:VEVENT\nUID:u2\nSUMMARY:Office\nDTSTART;VALUE=DATE:20260602\nDTEND;VALUE=DATE:20260603\nEND:VEVENT'; let ad = C.parseIcs(allDayXml)[0]; assert.strictEqual(ad.allDay, true); assert.strictEqual(ad.date, '2026-06-02'); assert.strictEqual(ad.endDate, '2026-06-03'); assert.strictEqual(ad.startMs, null); // --- parseIcs: VTODO task --- const todoXml = '@@CAL /r/\t#e0a93c\tVTODO\nBEGIN:VTODO\nUID:t1\nSUMMARY:Pay rent\nDUE;VALUE=DATE:20260628\nSTATUS:NEEDS-ACTION\nEND:VTODO'; let td = C.parseIcs(todoXml)[0]; assert.strictEqual(td.kind, 'task'); assert.strictEqual(td.date, '2026-06-28'); assert.strictEqual(td.done, false); // --- parseIcs: folded SUMMARY + entity + escape end to end --- const foldXml = '@@CAL /p/\t#fff\tVEVENT\nBEGIN:VEVENT\r\nUID:u3\r\nSUMMARY:Team "sync"\\, part\r\n 2\r\nDTSTART:20260601T090000Z\r\nEND:VEVENT'; // folded: " 2" -> strip one fold-space -> " 2" let fe = C.parseIcs(foldXml)[0]; assert.strictEqual(fe.summary, 'Team "sync", part 2'); // --- parseIcs: defensive, never throws on garbage --- assert.deepStrictEqual(C.parseIcs('not xml at all'), []); assert.deepStrictEqual(C.parseIcs(''), []); // --- coveredDates: all-day exclusive end spans start..end-1 --- assert.deepStrictEqual( C.coveredDates({ allDay: true, date: '2026-06-10', endDate: '2026-06-13' }), ['2026-06-10', '2026-06-11', '2026-06-12']); // single all-day (no endDate) assert.deepStrictEqual(C.coveredDates({ allDay: true, date: '2026-06-02', endDate: null }), ['2026-06-02']); // pathological spans are capped, not allowed to loop forever assert.strictEqual(C.coveredDates({ allDay: true, date: '2026-01-01', endDate: '2028-01-01' }).length, C.MAX_EVENT_DAYS); // timed single-day assert.deepStrictEqual( C.coveredDates({ allDay: false, startMs: Date.UTC(2026,5,22,15,0), endMs: Date.UTC(2026,5,22,17,0) }).length, 1); assert.strictEqual(C.isMultiDay({ allDay: true, date: '2026-06-10', endDate: '2026-06-13' }), true); assert.strictEqual(C.isMultiDay({ allDay: true, date: '2026-06-02', endDate: '2026-06-03' }), false); // --- buildSegments: a Wed..Fri band in week 0 (grid starts Mon 2026-06-08) --- const segOut = C.buildSegments( [{ allDay: true, date: '2026-06-10', endDate: '2026-06-13', color: '#5b8cff', summary: 'Urlaub' }], '2026-06-08'); // Mon of week containing the 10th assert.strictEqual(segOut.laneCount, 1); assert.strictEqual(segOut.segments.length, 1); let s = segOut.segments[0]; assert.strictEqual(s.row, 0); assert.strictEqual(s.colStart, 2); // Wed assert.strictEqual(s.colEnd, 4); // Fri (covers 10,11,12) assert.strictEqual(s.lane, 0); assert.strictEqual(s.roundLeft, true); assert.strictEqual(s.roundRight, true); // --- buildSegments: week wrap Fri..Mon -> two segments, both inner ends square --- const wrap = C.buildSegments( [{ allDay: true, date: '2026-06-12', endDate: '2026-06-16', color: '#e06b9a', summary: 'Messe' }], '2026-06-08'); // covers 12,13,14,15 -> Fri,Sat,Sun (row0) + Mon (row1) assert.strictEqual(wrap.segments.length, 2); const r0 = wrap.segments.find(x => x.row === 0), r1 = wrap.segments.find(x => x.row === 1); assert.strictEqual(r0.colStart, 4); assert.strictEqual(r0.colEnd, 6); assert.strictEqual(r0.roundLeft, true); assert.strictEqual(r0.roundRight, false); assert.strictEqual(r1.colStart, 0); assert.strictEqual(r1.colEnd, 0); assert.strictEqual(r1.roundLeft, false); assert.strictEqual(r1.roundRight, true); // --- buildSegments: two overlapping events get distinct lanes --- const lanes = C.buildSegments([ { allDay: true, date: '2026-06-09', endDate: '2026-06-12', color: '#aaa', summary: 'A' }, { allDay: true, date: '2026-06-10', endDate: '2026-06-13', color: '#bbb', summary: 'B' } ], '2026-06-08'); assert.strictEqual(lanes.laneCount, 2); console.log('caldav.test.js: ALL PASS');