Files
desktop-planer/dev/tests/run.js
2026-10-05 21:15:41 +02:00

105 lines
7.1 KiB
JavaScript

// Tests der reinen Module: gjs -m dev/tests/run.js
import * as D from '../../desktop@marha.de/dates.js';
import {continueList, renderMarkdown} from '../../desktop@marha.de/markdown.js';
import {batches, concat, fileStamp, planPull, planPush, stampToSeconds, tailHash, uploadItem} from '../../desktop@marha.de/files.js';
let failed = 0;
function eq(actual, expected, name) {
const a = JSON.stringify(actual), e = JSON.stringify(expected);
if (a === e)
print(`ok ${name}`);
else {
failed++;
print(`FAIL ${name}\n got ${a}\n expected ${e}`);
}
}
eq(D.toDateString(new Date(2026, 8, 17)), '2026-09-17', 'toDateString');
eq(D.addDays('2026-09-30', 1), '2026-10-01', 'addDays über Monatsgrenze');
eq(D.addDays('2026-01-01', -1), '2025-12-31', 'addDays rückwärts über Jahr');
eq(D.startOfWeek('2026-09-17'), '2026-09-14', 'startOfWeek Do → Mo');
eq(D.startOfWeek('2026-09-20'), '2026-09-14', 'startOfWeek So → Mo');
eq(D.startOfWeek('2026-09-14'), '2026-09-14', 'startOfWeek Mo bleibt');
eq(D.weekDays('2026-09-17').length, 7, 'weekDays 7 Tage');
const grid = D.monthGrid(2026, 8);
eq(grid[0], '2026-08-31', 'monthGrid startet Montag davor');
eq(grid.length, 42, 'monthGrid 42 Zellen');
eq(D.monthGrid(2026, 5)[0], '2026-06-01', 'monthGrid Monat beginnt Montag');
eq(D.isoWeek('2026-09-17'), 38, 'isoWeek');
eq(D.isoWeek('2027-01-01'), 53, 'isoWeek Jahreswechsel');
eq(D.formatMonth(2026, 8), 'September 2026', 'formatMonth');
eq(D.formatDate('2026-09-17'), 'Do, 17.09.2026', 'formatDate');
eq(D.formatDayMonth('2026-09-17'), '17.09.', 'formatDayMonth');
eq(D.parseDateInput('17.09.2026'), '2026-09-17', 'parseDateInput dd.mm.yyyy');
eq(D.parseDateInput('1.9.26'), '2026-09-01', 'parseDateInput kurz');
eq(D.parseDateInput('2026-09-17'), '2026-09-17', 'parseDateInput iso');
eq(D.parseDateInput('31.02.2026'), null, 'parseDateInput ungültig');
eq(D.parseDateInput('abc'), null, 'parseDateInput Müll');
eq(D.parseTimeInput('9:05'), '09:05', 'parseTimeInput h:mm');
eq(D.parseTimeInput('14'), '14:00', 'parseTimeInput nur Stunde');
eq(D.parseTimeInput('9.30'), '09:30', 'parseTimeInput Punkt');
eq(D.parseTimeInput('25:00'), null, 'parseTimeInput ungültig');
eq(D.timeToMinutes('01:30'), 90, 'timeToMinutes');
eq(D.minutesToTime(90), '01:30', 'minutesToTime');
eq(renderMarkdown('# Titel'), '<span size="18pt" weight="bold">Titel</span>', 'md h1');
eq(renderMarkdown('## A **b** c'), '<span size="15pt" weight="bold">A <b>b</b> c</span>', 'md h2 bold');
eq(renderMarkdown('a *kursiv* b'), 'a <i>kursiv</i> b', 'md italic');
eq(renderMarkdown('snake_case_name'), 'snake_case_name', 'md underscore im Wort bleibt');
eq(renderMarkdown('2 * 3 * 4'), '2 * 3 * 4', 'md einzelne Sterne bleiben');
eq(renderMarkdown('- eins\n - zwei'), ' • eins\n • zwei', 'md verschachtelte Liste');
eq(renderMarkdown('1. a\n2. b'), ' 1. a\n 2. b', 'md nummeriert');
eq(renderMarkdown('- [ ] offen\n- [x] fertig'),
' ☐ offen\n ☑ <span foreground="#888888"><s>fertig</s></span>', 'md checkbox');
eq(renderMarkdown('a `x < y` b'), 'a <span font_family="monospace" background="#2b2b2b">x &lt; y</span> b', 'md code escaped');
eq(renderMarkdown('`**nicht fett**`'), '<span font_family="monospace" background="#2b2b2b">**nicht fett**</span>', 'md code schützt Sterne');
eq(renderMarkdown('```\nlet a = 1 & 2;\n```'), '<span font_family="monospace" background="#2b2b2b">let a = 1 &amp; 2;</span>', 'md codeblock');
eq(renderMarkdown('[Link](http://x)'), '<u><span foreground="#8ab4f8">Link</span></u>', 'md link');
eq(renderMarkdown('[[Wiki]]'), '<u><span foreground="#8ab4f8">Wiki</span></u>', 'md wikilink');
eq(renderMarkdown('![Bild](a.png)'), '[Bild: Bild]', 'md image');
eq(renderMarkdown('> zitat'), '<span foreground="#b0b0b0">▎ <i>zitat</i></span>', 'md quote');
eq(renderMarkdown('---'), '<span foreground="#666666">────────────────────────────</span>', 'md hr');
eq(renderMarkdown('~~weg~~'), '<s>weg</s>', 'md strike');
eq(renderMarkdown('a\n\nb'), 'a\n\nb', 'md leere Zeile bleibt');
eq(continueList('- eins'), '- ', 'Liste: nächster Punkt');
eq(continueList(' * eins'), ' * ', 'Liste: Einrückung und Marker bleiben');
eq(continueList('- [x] fertig'), '- [ ] ', 'Liste: Checkbox leer weiter');
eq(continueList('3. drei'), '4. ', 'Liste: Nummer zählt hoch');
eq(continueList('- '), '', 'Liste: leerer Punkt beendet');
eq(continueList('2. '), '', 'Liste: leerer nummerierter Punkt beendet');
eq(continueList('nur Text'), null, 'Liste: kein Marker');
eq(continueList('---'), null, 'Liste: Trennlinie ist keine Liste');
const H = 3600, NOW = 1_800_000_000;
eq(fileStamp(NOW), '2027-01-15T08:00:00.000Z', 'fileStamp');
eq(stampToSeconds('2027-01-15T08:00:00.000Z'), NOW, 'stampToSeconds');
const local = new Map([['a/s.jsonl', NOW - 2 * H], ['a/frisch.jsonl', NOW - 10], ['m/MEMORY.md', NOW - 3 * H]]);
eq(planPush(local, {'m/MEMORY.md': {updated: fileStamp(NOW - 3 * H), size: 5, hash: 'x'}, 'weg.md': fileStamp(NOW - 5 * H), 'alt.md': null}, NOW, H),
{upload: [{id: 'a/s.jsonl', updated: fileStamp(NOW - 2 * H)}], remove: ['weg.md']},
'planPush: nur ruhende, geänderte Dateien hoch, Verschwundene als Tombstone, Gemeldete nicht nochmal');
const enc = s => new TextEncoder().encode(s);
eq(tailHash(enc('abc')), 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad', 'tailHash = SHA-256 bei kleinen Dateien');
eq(tailHash(enc('x'.repeat(70000) + 'abc')), tailHash(enc('y'.repeat(70000) + 'x'.repeat(65533) + 'abc')), 'tailHash sieht nur die letzten 64 KB');
const known = {updated: 'u', size: 4, hash: tailHash(enc('a\nb\n'))};
eq(uploadItem('s', 'v', enc('a\nb\nc\n'), known), {id: 's', updated: 'v', base: {size: 4, hash: known.hash}, append: 'c\n'}, 'uploadItem: nur der Rest, wenn der Anfang stimmt');
eq(uploadItem('s', 'v', enc('x\nb\nc\n'), known), {id: 's', updated: 'v', content: 'x\nb\nc\n'}, 'uploadItem: ganze Datei bei anderem Anfang');
eq(uploadItem('s', 'v', enc('a\n'), known), {id: 's', updated: 'v', content: 'a\n'}, 'uploadItem: ganze Datei, wenn kürzer');
eq(uploadItem('s', 'v', enc('a\nb\nc\n'), 'u'), {id: 's', updated: 'v', content: 'a\nb\nc\n'}, 'uploadItem: alter Stand ohne Größe → ganz');
eq(uploadItem('s', 'v', new Uint8Array([0xff, 0xfe]), null), null, 'uploadItem: kein UTF-8 → nichts');
eq(Array.from(concat(new Uint8Array([1, 2]), new Uint8Array([3]))), [1, 2, 3], 'concat');
eq(planPull([
{id: 'a/s.jsonl', updated: fileStamp(NOW - 1 * H), content: 'x'},
{id: 'a/s.jsonl', updated: fileStamp(NOW - 3 * H), content: 'x'},
{id: 'neu.md', updated: fileStamp(NOW), content: 'y'},
{id: 'm/MEMORY.md', updated: fileStamp(NOW), deleted: true},
{id: 'nie.md', updated: fileStamp(NOW), deleted: true},
], local).map(p => `${p.file.id}:${p.action}`),
['a/s.jsonl:write', 'a/s.jsonl:skip', 'neu.md:write', 'm/MEMORY.md:delete', 'nie.md:skip'], 'planPull: neuer schreibt, älter überspringt, Tombstone löscht');
eq(batches([1, 2, 3, 4], n => n, 5), [[1, 2], [3], [4]], 'batches nach Größe');
eq(batches([], n => n, 5), [], 'batches leer');
print(failed ? `\n${failed} FEHLER` : '\nalle Tests ok');
if (failed)
imports.system.exit(1);