Werkbaum hat kein Backend, und das eigentlich Schwere an gemeinsamem Bearbeiten ist das Zusammenführen gleichzeitiger Änderungen — im Plan als `[!] Merging simultaneous edits` markiert. Etherpad hat das gelöst. Also geliehen statt nachgebaut: das Pad ist die Schreibfläche, Werkbaum die Ansicht. Nachgemessen an pad.hostsharing.net, bevor irgendwas gebaut wurde: - der Klartext-Export sendet `Access-Control-Allow-Origin: *`, - das kanonische Beispiel (SPEC §10) kommt byte-identisch zurück, - der HTML-Export zeigt kein Listen-Markup — Etherpad deutet `-` nicht zur Aufzählung um und behält die führenden Leerzeichen. Das war das Risiko, das die Idee hätte erledigen können. Nicht geprüft ist das Tippen im Etherpad-Editor selbst (Tab-Einrückung, mögliches Auto-Bullet); dafür braucht es einen echten Browser. Eigener Parameter statt `?sourceUrl=`: Die URL, die ein Mensch in der Hand hat, ist die Pad-URL — `/export/txt` hängt Werkbaum selbst an. Vor allem aber lizenziert der eigene Parameter das andere Verhalten, sodass D23 unangetastet bleibt: `sourceUrl` heißt weiter „statische Datei, einmal pro Laden geholt", bestehende Links bekommen kein Polling. Name und id sind die vollständige Pad-URL (Pad-Namen sind nur pro Instanz eindeutig). Textfeld schreibgeschützt, Knopf öffnet das Pad: ohne das verschwände getippter Text beim nächsten Abruf. Schrift bleibt Tinte statt grau — hier wird gelesen, der Plantext ist der Hauptinhalt. Drei Riegel im Takt, jeder aus einem echten Fehler: - `padBusy` — im Netzwerk-Mitschnitt stapelten sich die Abrufe, weil die Gegenseite langsamer war als der Takt; eine spät eintreffende alte Antwort hätte neueren Text überschrieben, - Abbruch nach 10 s — sonst bliebe der Riegel bei hängender Gegenseite für immer zu, - `visibilityState` + `visibilitychange` — nicht im Hintergrund abrufen, aber bei Rückkehr sofort. Der Stabilitätstakt übernimmt erst beim zweiten gleichen Abruf, sonst sieht man die anderen mitten im Tippen. Die Normalisierung der Pad-Adresse liegt headless in `remote.js`, damit sie testbar ist (23 neue Tests, 60 -> 83). Im Vorschau-Browser meldet `visibilityState` „hidden" und HMR lädt bei jeder Quelländerung neu — ein Reload sieht wie eine geglückte Übernahme aus; nachgewiesen wurde die Übernahme deshalb mit einem Marker auf `window`, der einen Reload nicht überlebt. SPEC §9 zuerst, dann D31, dann Code. Der Plan bekommt den Knoten nach D30 mit `[x]`, nicht `[^]`.
70 lines
2.7 KiB
JavaScript
70 lines
2.7 KiB
JavaScript
import { describe, it, expect } from 'vitest';
|
|
import { padUrls } from '../src/remote.js';
|
|
|
|
const PAD = 'https://pad.example.org/p/mein-plan';
|
|
|
|
describe('padUrls — Pad-Adresse normalisieren (D31)', () => {
|
|
it('hängt den Klartext-Export an', () => {
|
|
expect(padUrls(PAD)).toEqual({ pad: PAD, text: PAD + '/export/txt' });
|
|
});
|
|
|
|
/* Derselbe Pad soll genau EIN Dokument ergeben — die Identität leitet sich
|
|
aus `pad` ab, also müssen alle Schreibweisen darauf zusammenfallen. */
|
|
it.each([
|
|
['Schrägstrich am Ende', PAD + '/'],
|
|
['mehrere Schrägstriche', PAD + '///'],
|
|
['Export-Pfad mitgegeben', PAD + '/export/txt'],
|
|
['anderer Export', PAD + '/export/html'],
|
|
['Timeslider', PAD + '/timeslider'],
|
|
['Query dran', PAD + '?showChat=false'],
|
|
['Fragment dran', PAD + '#anker'],
|
|
['Query und Schrägstrich', PAD + '/?showControls=false'],
|
|
])('fällt auf dieselbe Pad-URL zusammen: %s', (_name, input) => {
|
|
expect(padUrls(input)).toEqual({ pad: PAD, text: PAD + '/export/txt' });
|
|
});
|
|
|
|
it('erlaubt eine Montage unter einem Unterpfad', () => {
|
|
const sub = 'https://example.org/etherpad/p/plan';
|
|
expect(padUrls(sub)).toEqual({ pad: sub, text: sub + '/export/txt' });
|
|
});
|
|
|
|
it('erlaubt http neben https', () => {
|
|
const h = 'http://pad.example.org/p/plan';
|
|
expect(padUrls(h).pad).toBe(h);
|
|
});
|
|
|
|
it('behält den Port', () => {
|
|
const h = 'https://pad.example.org:9001/p/plan';
|
|
expect(padUrls(h)).toEqual({ pad: h, text: h + '/export/txt' });
|
|
});
|
|
|
|
/* Zwei Pads gleichen Namens auf verschiedenen Hosts müssen unterscheidbar
|
|
bleiben — deshalb ist der Name die vollständige URL, nicht der Pad-Name. */
|
|
it('unterscheidet gleichnamige Pads verschiedener Hosts', () => {
|
|
expect(padUrls('https://a.example/p/plan').pad)
|
|
.not.toBe(padUrls('https://b.example/p/plan').pad);
|
|
});
|
|
|
|
it.each([
|
|
['kein /p/-Pfad', 'https://pad.example.org/mein-plan'],
|
|
['nur der Host', 'https://pad.example.org'],
|
|
['/p/ ohne Namen', 'https://pad.example.org/p/'],
|
|
['fremdes Schema', 'file:///tmp/plan.txt'],
|
|
['javascript:', 'javascript:alert(1)'],
|
|
['data:', 'data:text/plain,foo'],
|
|
['gar keine URL', 'nicht mal eine URL'],
|
|
['leer', ''],
|
|
])('weist ab: %s', (_name, input) => {
|
|
expect(padUrls(input)).toBeNull();
|
|
});
|
|
|
|
it('löst relative Angaben gegen die Seite auf, wenn eine Basis da ist', () => {
|
|
expect(padUrls('/p/plan', 'https://pad.example.org/x/y').pad)
|
|
.toBe('https://pad.example.org/p/plan');
|
|
});
|
|
|
|
it('ohne Basis bleibt eine relative Angabe unbrauchbar', () => {
|
|
expect(padUrls('/p/plan')).toBeNull();
|
|
});
|
|
});
|