1. Neu laden fragt bei URL-Dokumenten nicht mehr (F5 verwirft ohnehin still, D23); bei Datei-Dokumenten bleibt die Rückfrage. Dabei gefunden und behoben: Der Knopf erschien auch bei Server-Dokumenten und hätte die JSON-Antwort der API als Text an alle gepusht. 2. Die Rückfrage beim letzten Dokument sagt, dass danach wieder das Beispiel dasteht (docDeleteLastConfirm, 9 Sprachen). 3. Ein zweiter Tab, der in dieselbe Ablage schreibt, wird per storage-Ereignis erkannt und gemeldet (tabConflict, isDocKey in docstore.js) — kein Sync, aber kein stiller Verlust mehr. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
120 lines
4.7 KiB
JavaScript
120 lines
4.7 KiB
JavaScript
import { describe, it, expect } from 'vitest';
|
|
import { readDocs, storeDocs, storeDocText, docTextKey, isDocKey,
|
|
LS_DOCS, LS_SRC, DOC_TEXT_PREFIX } from '../src/docstore.js';
|
|
|
|
/* Storage-Attrappe mit Zähler — die Vergleich-vor-Schreiben-Regel ist nur
|
|
über die Zahl der setItem-Aufrufe zu beweisen. */
|
|
function mem(init){
|
|
const m = new Map(Object.entries(init || {}));
|
|
let writes = 0;
|
|
return {
|
|
getItem: k => (m.has(k) ? m.get(k) : null),
|
|
setItem: (k, v) => { writes++; m.set(k, String(v)); },
|
|
removeItem: k => { m.delete(k); },
|
|
keys: () => [...m.keys()],
|
|
writes: () => writes,
|
|
};
|
|
}
|
|
|
|
const A = {id: 'example', name: 'Example', text: '- a'};
|
|
const B = {id: 'k1', name: 'Sprint 15', text: '- b', source: 'https://x.example/p'};
|
|
|
|
describe('docstore — Index + Text je Dokument (D83)', () => {
|
|
it('Rundreise: storeDocs schreibt, readDocs liest dasselbe zurück', () => {
|
|
const s = mem();
|
|
storeDocs(s, [A, B], s.keys());
|
|
expect(readDocs(s)).toEqual({docs: [A, B], legacy: false});
|
|
});
|
|
|
|
it('der Index trägt KEINEN Text — der liegt je Dokument unter eigenem Schlüssel', () => {
|
|
const s = mem();
|
|
storeDocs(s, [A], s.keys());
|
|
expect(s.getItem(LS_DOCS)).not.toContain('- a');
|
|
expect(s.getItem(docTextKey('example'))).toBe('- a');
|
|
});
|
|
|
|
it('Altformat (Texte im Array) wird gelesen und als legacy gemeldet', () => {
|
|
const s = mem({[LS_DOCS]: JSON.stringify([A, B])});
|
|
const r = readDocs(s);
|
|
expect(r.legacy).toBe(true);
|
|
expect(r.docs).toEqual([A, B]);
|
|
});
|
|
|
|
it('ein fehlender Text-Schlüssel kostet nur diesen einen Text, nicht die Liste', () => {
|
|
const s = mem();
|
|
storeDocs(s, [A, B], s.keys());
|
|
s.removeItem(docTextKey('k1'));
|
|
const r = readDocs(s);
|
|
expect(r.docs.map(d => d.id)).toEqual(['example', 'k1']);
|
|
expect(r.docs[1].text).toBe('');
|
|
expect(r.docs[0].text).toBe('- a');
|
|
});
|
|
|
|
it('kaputter oder unbrauchbarer Index -> null (Beispiel-Pfad des Aufrufers)', () => {
|
|
expect(readDocs(mem({[LS_DOCS]: '{kaputt'}))).toBe(null);
|
|
expect(readDocs(mem({[LS_DOCS]: '"kein Array"'}))).toBe(null);
|
|
expect(readDocs(mem({[LS_DOCS]: '[]'}))).toBe(null);
|
|
expect(readDocs(mem({[LS_DOCS]: JSON.stringify([{name: 'ohne id'}])}))).toBe(null);
|
|
expect(readDocs(mem())).toBe(null);
|
|
});
|
|
|
|
it('unveränderte Schlüssel werden nicht neu geschrieben', () => {
|
|
const s = mem();
|
|
storeDocs(s, [A, B], s.keys());
|
|
const vorher = s.writes();
|
|
storeDocs(s, [A, B], s.keys()); /* nichts geändert */
|
|
expect(s.writes()).toBe(vorher);
|
|
storeDocs(s, [A, {...B, text: '- b2'}], s.keys()); /* ein Text geändert */
|
|
expect(s.writes()).toBe(vorher + 1);
|
|
});
|
|
|
|
it('Texte gelöschter Dokumente werden abgeräumt, fremde Schlüssel bleiben', () => {
|
|
const s = mem({'werkbaum-ui': '{}'});
|
|
storeDocs(s, [A, B], s.keys());
|
|
storeDocs(s, [A], s.keys()); /* B gelöscht */
|
|
expect(s.getItem(docTextKey('k1'))).toBe(null);
|
|
expect(s.getItem('werkbaum-ui')).toBe('{}');
|
|
});
|
|
|
|
it('storeDocText schreibt Text-Schlüssel UND Spiegel — und nur bei Änderung', () => {
|
|
const s = mem();
|
|
storeDocText(s, 'example', '- neu');
|
|
expect(s.getItem(docTextKey('example'))).toBe('- neu');
|
|
expect(s.getItem(LS_SRC)).toBe('- neu');
|
|
const vorher = s.writes();
|
|
storeDocText(s, 'example', '- neu');
|
|
expect(s.writes()).toBe(vorher);
|
|
});
|
|
|
|
it('ids mit Doppelpunkten und URLs (live:/url:) tragen als Schlüssel', () => {
|
|
const live = {id: 'live:https://w.example/api/v1/documents/abc', name: 'Plan', text: '- x'};
|
|
const s = mem();
|
|
storeDocs(s, [live], s.keys());
|
|
expect(readDocs(s).docs).toEqual([live]);
|
|
expect(s.getItem(DOC_TEXT_PREFIX + live.id)).toBe('- x');
|
|
});
|
|
|
|
it('fehlender Name fällt auf die id zurück, Quota-Fehler laufen zum Aufrufer', () => {
|
|
const s = mem({[LS_DOCS]: JSON.stringify([{id: 'x'}])});
|
|
expect(readDocs(s).docs[0].name).toBe('x');
|
|
const voll = {getItem: () => null, setItem: () => { throw new Error('QuotaExceeded'); }, removeItem: () => {}};
|
|
expect(() => storeDocs(voll, [A], [])).toThrow();
|
|
});
|
|
});
|
|
|
|
describe('isDocKey — Fremd-Tab-Erkennung (D84)', () => {
|
|
it('erkennt Index, aktives Dokument, Spiegel und Text-Schlüssel', () => {
|
|
expect(isDocKey(LS_DOCS)).toBe(true);
|
|
expect(isDocKey('werkbaum-active')).toBe(true);
|
|
expect(isDocKey(LS_SRC)).toBe(true);
|
|
expect(isDocKey(DOC_TEXT_PREFIX + 'live:https://x/api/v1/documents/a')).toBe(true);
|
|
});
|
|
|
|
it('fremde Schlüssel und Ansichts-Zustand sind keine Dokument-Schlüssel', () => {
|
|
expect(isDocKey('werkbaum-ui')).toBe(false);
|
|
expect(isDocKey('werkbaum-lang')).toBe(false);
|
|
expect(isDocKey('werkbaum-snaps')).toBe(false);
|
|
expect(isDocKey(null)).toBe(false);
|
|
});
|
|
});
|