test: cover recentFiles and the history log

The two framework-free 0%-coverage modules: the recent-files list
(dedupe by name, eight-entry cap, corrupt-localStorage tolerance) and
the labelled undo/redo mirror against a live jsdom editor (labels per
edit kind, undo/redo moves, redo branch discarded by a fresh edit).
This commit is contained in:
Steffen Becker committed 2026-08-15 23:16:08 +02:00
1 parent 82993aeda3
commit 32cf72a1d0
2 files changed
+126

No files matched your search

+77
View File
@@ -0,0 +1,77 @@
// The labelled undo/redo mirror (utils/historyLog.svelte.ts) against a live editor:
// labels per edit kind, entries moving between the stacks on undo/redo, and the
// redo branch being discarded by a fresh edit.
import { describe, it, expect, beforeEach } from 'vitest';
import { Editor } from '@tiptap/core';
import { closeHistory } from '@tiptap/pm/history';
import { extensions } from '../../src/lib/editor/extensions';
import { historyLog, recordTransaction, resetHistoryLog } from '../../src/lib/utils/historyLog.svelte';
function makeEditor() {
const el = document.createElement('div');
document.body.appendChild(el);
return new Editor({
element: el,
extensions,
content: { type: 'doc', content: [{ type: 'paragraph' }] },
onTransaction: ({ editor, transaction }) => recordTransaction(editor.state, transaction),
});
}
// Consecutive edits in one test tick share a timestamp and would merge into one
// undo group; an explicit group boundary keeps one log entry per edit.
const endGroup = (editor: Editor) =>
editor.view.dispatch(closeHistory(editor.state.tr));
const undoLabels = () => historyLog.undo.map((e) => e.label);
const redoLabels = () => historyLog.redo.map((e) => e.label);
describe('history log', () => {
beforeEach(resetHistoryLog);
it('labels typing, marks, structure and delete', () => {
const editor = makeEditor();
editor.commands.insertContent('Hello world');
endGroup(editor);
editor.chain().setTextSelection({ from: 1, to: 6 }).toggleBold().run();
endGroup(editor);
editor.commands.insertContentAt(editor.state.doc.content.size,
{ type: 'heading', attrs: { level: 2 }, content: [{ type: 'text', text: 'Title' }] });
endGroup(editor);
editor.chain().setTextSelection({ from: 7, to: 12 }).deleteSelection().run();
expect(undoLabels()).toEqual(['Typing: “Hello world”', 'Bold', 'Heading 2', 'Delete']);
expect(redoLabels()).toEqual([]);
editor.destroy();
});
it('moves entries across on undo/redo and keeps counts at the plugin depths', () => {
const editor = makeEditor();
editor.commands.insertContent('one');
endGroup(editor);
editor.chain().selectAll().toggleBold().run();
editor.commands.undo();
expect(undoLabels()).toEqual(['Typing: “one”']);
expect(redoLabels()).toEqual(['Bold']);
editor.commands.redo();
expect(undoLabels()).toEqual(['Typing: “one”', 'Bold']);
expect(redoLabels()).toEqual([]);
editor.destroy();
});
it('a fresh edit after undo discards the redo branch', () => {
const editor = makeEditor();
editor.commands.insertContent('one');
endGroup(editor);
editor.commands.insertContent(' two');
editor.commands.undo();
expect(redoLabels()).toHaveLength(1);
editor.commands.insertContent(' three');
expect(redoLabels()).toEqual([]);
expect(undoLabels()).toEqual(['Typing: “one”', 'Typing: “three”']);
editor.destroy();
});
});
+49
View File
@@ -0,0 +1,49 @@
// The recent-files list (storage/recentFiles.ts): dedupe by name, newest first,
// capped at eight, and tolerant of corrupt localStorage. jsdom has no IndexedDB,
// so the handle side no-ops — exactly the browser-without-picker path.
import { describe, it, expect, beforeEach } from 'vitest';
import { loadRecentFiles, rememberRecentFile, forgetRecentFiles } from '../../src/lib/storage/recentFiles';
const KEY = 'edentext-recent-files';
describe('recent files', () => {
beforeEach(() => localStorage.clear());
it('starts empty and survives corrupt or foreign values under its key', () => {
expect(loadRecentFiles()).toEqual([]);
localStorage.setItem(KEY, 'not json{');
expect(loadRecentFiles()).toEqual([]);
localStorage.setItem(KEY, '{"a":1}');
expect(loadRecentFiles()).toEqual([]);
localStorage.setItem(KEY, '[{"id":"x","name":"ok.odt","at":1},{"id":"y","at":2},null]');
expect(loadRecentFiles().map((f) => f.name)).toEqual(['ok.odt']);
});
it('remembers newest first and dedupes a handleless file by name, keeping its id', async () => {
await rememberRecentFile('a.odt', null);
await rememberRecentFile('b.odt', null);
expect(loadRecentFiles().map((f) => f.name)).toEqual(['b.odt', 'a.odt']);
const idA = loadRecentFiles()[1].id;
await rememberRecentFile('a.odt', null);
const list = loadRecentFiles();
expect(list.map((f) => f.name)).toEqual(['a.odt', 'b.odt']);
expect(list[0].id).toBe(idA);
});
it('keeps at most eight entries, dropping the oldest', async () => {
for (let i = 1; i <= 10; i++) await rememberRecentFile(`f${i}.odt`, null);
const names = loadRecentFiles().map((f) => f.name);
expect(names).toHaveLength(8);
expect(names[0]).toBe('f10.odt');
expect(names).not.toContain('f1.odt');
expect(names).not.toContain('f2.odt');
});
it('forgetRecentFiles clears the list and removes the key', async () => {
await rememberRecentFile('a.odt', null);
forgetRecentFiles();
expect(loadRecentFiles()).toEqual([]);
expect(localStorage.getItem(KEY)).toBeNull();
});
});