Files
edentext/tests/unit/spell-recheck.test.ts
T
Steffen Becker 1a4b321b5a perf: the spell check re-reads the edited paragraphs, not the document
The dirty ranges ride the plugin state and are mapped through every
transaction; the check after a pause swaps only those textblocks'
squiggles, and the whole document is checked on a language or
dictionary change. The pause after a keystroke in a 124-page document
drops from 363 ms to 11 ms (Chromium) and from 1.4 s to 17 ms (Firefox).
A unit test drives the recheck; the dom run asserts the pause is free.
2026-09-08 18:07:41 +02:00

71 lines
2.7 KiB
TypeScript

// The check after an edit looks at the edited paragraph only; the others keep their
// squiggles. Drives a real body editor with a stub checker that flags xx-words.
import { describe, it, expect, vi, afterEach } from 'vitest';
import { Editor } from '@tiptap/core';
const { checks } = vi.hoisted(() => ({ checks: [] as string[] }));
vi.mock('../../src/lib/spell/controller', () => ({
spellController: {
isEnabled: () => true,
check: (w: string) => { checks.push(w); return !w.startsWith('xx'); },
suggest: () => [],
getLanguage: () => 'en-US',
addWord: () => {},
ignoreWord: () => {},
subscribe: () => () => {},
},
}));
const { extensions } = await import('../../src/lib/editor/extensions');
const { spellErrorAt } = await import('../../src/lib/editor/extensions/spellCheck');
const para = (text: string) => ({ type: 'paragraph', content: [{ type: 'text', text }] });
async function makeEditor() {
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] });
const el = document.createElement('div');
document.body.appendChild(el);
const editor = new Editor({
element: el,
extensions,
content: { type: 'doc', content: [para('alpha xxone beta'), para('gamma xxtwo delta')] },
});
await Promise.resolve(); // the mount's whole-document check
checks.length = 0;
return editor;
}
// 'alpha xxone beta' fills 1..17, so the second paragraph's text starts at 19.
const XXONE = 8;
const XXTWO = 26;
afterEach(() => vi.useRealTimers());
describe('spell recheck after an edit', () => {
it('checks the edited paragraph only and keeps the other squiggles', async () => {
const editor = await makeEditor();
expect(spellErrorAt(editor.state, XXONE)).toEqual({ from: 7, to: 12 });
editor.view.dispatch(editor.state.tr.insertText('q', 24)); // gamma → gammaq
vi.advanceTimersByTime(400);
expect(checks).toEqual(['gammaq', 'xxtwo', 'delta']);
expect(spellErrorAt(editor.state, XXONE)).toEqual({ from: 7, to: 12 });
expect(spellErrorAt(editor.state, XXTWO + 1)).toEqual({ from: 26, to: 31 });
editor.destroy();
});
it('drops a squiggle the edit fixed, and re-reads a paragraph a deletion emptied into', async () => {
const editor = await makeEditor();
editor.view.dispatch(editor.state.tr.insertText('two', 25, 30)); // xxtwo → two
vi.advanceTimersByTime(400);
expect(spellErrorAt(editor.state, XXTWO)).toBeNull();
expect(spellErrorAt(editor.state, XXONE)).toEqual({ from: 7, to: 12 });
checks.length = 0;
editor.view.dispatch(editor.state.tr.delete(19, 25)); // 'gamma ' gone: a point, not a range
vi.advanceTimersByTime(400);
expect(checks).toEqual(['two', 'delta']);
editor.destroy();
});
});