import { describe, expect, it } from 'vitest'; import { createRequire } from 'node:module'; import { WORDS_EMPTY_TEXT_ATTR, WORDS_NODE_ATTR, WORDS_PATH_ATTR, actionFromPaste, actionFromBeforeInput, applyHistoryCommand, applyWordsCommand, batchHistoryCommands, beginWordsComposition, createCollapsedSelection, createEmptyWordsDocument, createLink, createList, createListItem, createParagraph, createText, createWordsHistory, createWordsState, decodeWordsPath, documentPlainText, domTargetToWordsPoint, domTargetToWordsSelection, endWordsComposition, encodeWordsDomSelectionTarget, encodeWordsPath, normalizeDocument, parseWordsHtml, parseWordsJson, parseWordsMarkdown, parseWordsPlainText, redoWords, renderWordsDomHtml, renderWordsDocument, resolveDomPointFromTarget, restoreSelectionApiFromDomTarget, serializeWordsJson, serializeWordsHtml, serializeWordsMarkdown, selectionApiToWordsSelection, updateWordsComposition, undoWords, type WordsDocument, type WordsEditorState } from './index'; const require = createRequire(import.meta.url); const { JSDOM } = require('jsdom') as { readonly JSDOM: new (html: string) => { readonly window: Window }; }; function stateWithText(text: string): WordsEditorState { return createWordsState( { version: 1, children: [createParagraph([createText(text)])] }, createCollapsedSelection([0, 0], text.length) ); } describe('Words engine', () => { it('normalizes invalid and empty documents into one paragraph', () => { const result = normalizeDocument({ version: 99, children: [ { type: 'unknown' }, { type: 'heading', level: 9, children: [] }, { type: 'paragraph', children: [ { type: 'text', text: 'A', marks: ['bold', 'bogus', 'bold'] }, { type: 'text', text: 'B', marks: ['bold'] } ] } ] }); expect(result.document.version).toBe(1); expect(result.issues.map((issue) => issue.reason)).toContain('invalid-block'); expect(result.issues.map((issue) => issue.reason)).toContain('invalid-heading-level'); expect(result.issues.map((issue) => issue.reason)).toContain('invalid-mark'); expect(result.document.children).toHaveLength(2); expect(result.document.children[0]).toMatchObject({ type: 'heading', level: 1, children: [{ type: 'text', text: '' }] }); expect(result.document.children[1]).toMatchObject({ type: 'paragraph', children: [{ type: 'text', text: 'AB', marks: ['bold'] }] }); expect(normalizeDocument(null).document).toEqual(createEmptyWordsDocument()); }); it('rejects unsafe links but preserves their text', () => { const result = normalizeDocument({ version: 1, children: [ { type: 'paragraph', children: [ { type: 'link', href: 'javascript:alert(1)', children: [{ type: 'text', text: 'click me' }] } ] } ] }); expect(result.issues.map((issue) => issue.reason)).toContain('unsafe-url'); expect(result.document.children[0]).toMatchObject({ type: 'paragraph', children: [{ type: 'text', text: 'click me' }] }); }); it('flattens nested links during normalization and blocks inserting links inside links', () => { const result = normalizeDocument({ version: 1, children: [ createParagraph([ createLink('/outer', [createText('Outer '), createLink('/inner', [createText('Inner')])]) ]) ] }); expect(result.issues.map((issue) => issue.reason)).toContain('nested-link'); expect(result.document.children[0]).toMatchObject({ type: 'paragraph', children: [ { type: 'link', href: '/outer', children: [{ type: 'text', text: 'Outer Inner' }] } ] }); const state = createWordsState( result.document, createCollapsedSelection([0, 0, 0], 'Outer'.length) ); const nested = applyWordsCommand(state, { type: 'insertLink', href: '/nested' }); expect(nested.changed).toBe(false); expect(nested.state.document).toEqual(state.document); }); it('serializes and parses canonical JSON', () => { const doc: WordsDocument = { version: 1, children: [createParagraph([createText('Hello', ['italic', 'bold'])])] }; const json = serializeWordsJson(doc); expect(parseWordsJson(json)).toEqual({ version: 1, children: [createParagraph([createText('Hello', ['bold', 'italic'])])] }); }); it('applies text, mark and block commands', () => { let state = stateWithText('Hello'); let result = applyWordsCommand(state, { type: 'insertText', text: ' world' }); expect(result.changed).toBe(true); expect(documentPlainText(result.state.document)).toBe('Hello world'); expect(result.state.selection).toEqual(createCollapsedSelection([0, 0], 11)); state = result.state; result = applyWordsCommand(state, { type: 'toggleMark', mark: 'bold' }); expect(result.state.activeMarks).toEqual(['bold']); expect(result.state.document.children[0]).toMatchObject({ type: 'paragraph', children: [{ type: 'text', text: 'Hello world' }] }); result = applyWordsCommand(result.state, { type: 'insertText', text: '!' }); expect(result.state.document.children[0]).toMatchObject({ type: 'paragraph', children: [ { type: 'text', text: 'Hello world' }, { type: 'text', text: '!', marks: ['bold'] } ] }); result = applyWordsCommand(result.state, { type: 'setBlock', block: 'heading', level: 2 }); expect(result.state.document.children[0]).toMatchObject({ type: 'heading', level: 2 }); }); it('applies mark commands only to selected text ranges', () => { const state: WordsEditorState = { ...stateWithText('Alpha beta'), selection: { anchor: { path: [0, 0], offset: 6 }, focus: { path: [0, 0], offset: 10 } } }; const result = applyWordsCommand(state, { type: 'toggleMark', mark: 'italic' }); expect(result.state.document.children[0]).toMatchObject({ type: 'paragraph', children: [ { type: 'text', text: 'Alpha ' }, { type: 'text', text: 'beta', marks: ['italic'] } ] }); }); it('toggles lists and links', () => { let state = stateWithText('Alpha'); let result = applyWordsCommand(state, { type: 'toggleList', kind: 'check' }); expect(result.state.document.children[0]).toMatchObject({ type: 'list', kind: 'check', children: [{ type: 'list-item', children: [{ type: 'text', text: 'Alpha' }] }] }); result = applyWordsCommand(result.state, { type: 'toggleList', kind: 'check' }); expect(result.state.document.children[0]).toMatchObject({ type: 'paragraph', children: [{ type: 'text', text: 'Alpha' }] }); state = { ...result.state, selection: { anchor: { path: [0, 0], offset: 0 }, focus: { path: [0, 0], offset: 5 } } }; result = applyWordsCommand(state, { type: 'insertLink', href: 'https://active.dev' }); expect(result.state.document.children[0]).toMatchObject({ type: 'paragraph', children: [ { type: 'link', href: 'https://active.dev', children: [{ type: 'text', text: 'Alpha' }] } ] }); result = applyWordsCommand( { ...result.state, selection: createCollapsedSelection([0, 0], 0) }, { type: 'unlink' } ); expect(result.state.document.children[0]).toMatchObject({ type: 'paragraph', children: [{ type: 'text', text: 'Alpha' }] }); }); it('deletes by word or line instead of collapsing advanced beforeinput to one char', () => { let state = stateWithText('Alpha beta'); let result = applyWordsCommand(state, { type: 'deleteWordBackward' }); expect(documentPlainText(result.state.document)).toBe('Alpha '); expect(result.state.selection).toEqual(createCollapsedSelection([0, 0], 6)); state = stateWithText('Alpha beta'); result = applyWordsCommand( { ...state, selection: createCollapsedSelection([0, 0], 6) }, { type: 'deleteWordForward' } ); expect(documentPlainText(result.state.document)).toBe('Alpha '); result = applyWordsCommand(state, { type: 'deleteLineBackward' }); expect(documentPlainText(result.state.document)).toBe(''); expect(actionFromBeforeInput({ inputType: 'deleteWordBackward' })).toEqual({ type: 'command', command: { type: 'deleteWordBackward' }, batch: 'typing' }); expect(actionFromBeforeInput({ inputType: 'deleteHardLineForward' })).toEqual({ type: 'command', command: { type: 'deleteLineForward' }, batch: 'typing' }); }); it('tracks undo and redo history around changed commands only', () => { const initial = stateWithText('A'); let history = createWordsHistory(initial); history = applyHistoryCommand(history, { type: 'insertText', text: 'B' }); history = applyHistoryCommand(history, { type: 'insertText', text: '' }); history = applyHistoryCommand(history, { type: 'insertText', text: 'C' }); expect(history.past).toHaveLength(2); expect(documentPlainText(history.present.document)).toBe('ABC'); history = undoWords(history); expect(documentPlainText(history.present.document)).toBe('AB'); history = undoWords(history); expect(documentPlainText(history.present.document)).toBe('A'); history = redoWords(history); expect(documentPlainText(history.present.document)).toBe('AB'); }); it('edits text inside list items with deep paths', () => { let state = createWordsState( { version: 1, children: [createList('unordered', [createListItem([createText('Item')])])] }, createCollapsedSelection([0, 0, 0], 4) ); let result = applyWordsCommand(state, { type: 'insertText', text: '!' }); expect(result.changed).toBe(true); expect(result.state.document.children[0]).toMatchObject({ type: 'list', children: [{ children: [{ type: 'text', text: 'Item!' }] }] }); expect(result.state.selection).toEqual(createCollapsedSelection([0, 0, 0], 5)); state = { ...result.state, selection: { anchor: { path: [0, 0, 0], offset: 0 }, focus: { path: [0, 0, 0], offset: 4 } } }; result = applyWordsCommand(state, { type: 'toggleMark', mark: 'bold' }); expect(result.state.document.children[0]).toMatchObject({ type: 'list', children: [ { children: [ { type: 'text', text: 'Item', marks: ['bold'] }, { type: 'text', text: '!' } ] } ] }); }); it('keeps selection stable after normalization merges adjacent text', () => { const state: WordsEditorState = { document: { version: 1, children: [createParagraph([createText('AC', ['bold'])])] }, selection: createCollapsedSelection([0, 0], 1), activeMarks: ['bold'] }; const result = applyWordsCommand(state, { type: 'insertText', text: 'B' }); expect(result.state.document.children[0]).toMatchObject({ type: 'paragraph', children: [{ type: 'text', text: 'ABC', marks: ['bold'] }] }); expect(result.state.selection).toEqual(createCollapsedSelection([0, 0], 2)); }); it('replaces multi-block ranges with inserted text', () => { const state = createWordsState( { version: 1, children: [createParagraph([createText('Alpha')]), createParagraph([createText('Beta')])] }, { anchor: { path: [0, 0], offset: 2 }, focus: { path: [1, 0], offset: 2 } } ); const result = applyWordsCommand(state, { type: 'insertText', text: 'X' }); expect(documentPlainText(result.state.document)).toBe('AlXta'); expect(result.state.document.children).toHaveLength(1); expect(result.state.selection).toEqual(createCollapsedSelection([0, 0], 3)); }); it('splits paragraphs and list items for Enter and merges them for Backspace', () => { let state = stateWithText('Alpha'); let result = applyWordsCommand( { ...state, selection: createCollapsedSelection([0, 0], 2) }, { type: 'insertParagraph' } ); expect(result.state.document.children).toHaveLength(2); expect(result.state.document.children[0]).toMatchObject({ type: 'paragraph', children: [{ type: 'text', text: 'Al' }] }); expect(result.state.document.children[1]).toMatchObject({ type: 'paragraph', children: [{ type: 'text', text: 'pha' }] }); result = applyWordsCommand(result.state, { type: 'deleteBackward' }); expect(documentPlainText(result.state.document)).toBe('Alpha'); expect(result.state.document.children).toHaveLength(1); state = createWordsState( { version: 1, children: [createList('unordered', [createListItem([createText('Item')])])] }, createCollapsedSelection([0, 0, 0], 2) ); result = applyWordsCommand(state, { type: 'insertParagraph' }); expect(result.state.document.children[0]).toMatchObject({ type: 'list', children: [ { type: 'list-item', children: [{ type: 'text', text: 'It' }] }, { type: 'list-item', children: [{ type: 'text', text: 'em' }] } ] }); }); it('converts list items to blocks and lets users leave lists', () => { let state = createWordsState( { version: 1, children: [ createList('unordered', [ createListItem([createText('One')]), createListItem([createText('Two')]) ]) ] }, createCollapsedSelection([0, 1, 0], 1) ); let result = applyWordsCommand(state, { type: 'setBlock', block: 'paragraph' }); expect(result.state.document.children).toMatchObject([ { type: 'paragraph', children: [{ type: 'text', text: 'One' }] }, { type: 'paragraph', children: [{ type: 'text', text: 'Two' }] } ]); expect(result.state.selection).toEqual(createCollapsedSelection([1, 0], 1)); state = createWordsState( { version: 1, children: [ createList('unordered', [ createListItem([createText('Item')]), createListItem([createText('')]) ]) ] }, createCollapsedSelection([0, 1, 0], 0) ); result = applyWordsCommand(state, { type: 'insertParagraph' }); expect(result.state.document.children).toMatchObject([ { type: 'list', children: [{ children: [{ type: 'text', text: 'Item' }] }] }, { type: 'paragraph', children: [{ type: 'text', text: '' }] } ]); expect(result.state.selection).toEqual(createCollapsedSelection([1, 0], 0)); state = createWordsState( { version: 1, children: [ createList('check', [ createListItem([createText('First')], true), createListItem([createText('Second')]) ]) ] }, createCollapsedSelection([0, 0, 0], 0) ); result = applyWordsCommand(state, { type: 'deleteBackward' }); expect(result.state.document.children).toMatchObject([ { type: 'paragraph', children: [{ type: 'text', text: 'First' }] }, { type: 'list', kind: 'check', children: [{ children: [{ type: 'text', text: 'Second' }] }] } ]); expect(result.state.selection).toEqual(createCollapsedSelection([0, 0], 0)); }); it('unlinks from an inner text selection and allows relative URLs', () => { let state = createWordsState( { version: 1, children: [createParagraph([createText('Docs')])] }, { anchor: { path: [0, 0], offset: 0 }, focus: { path: [0, 0], offset: 4 } } ); let result = applyWordsCommand(state, { type: 'insertLink', href: '/docs' }); expect(result.state.document.children[0]).toMatchObject({ type: 'paragraph', children: [{ type: 'link', href: '/docs', children: [{ type: 'text', text: 'Docs' }] }] }); state = { ...result.state, selection: createCollapsedSelection([0, 0, 0], 2) }; result = applyWordsCommand(state, { type: 'unlink' }); expect(result.state.document.children[0]).toMatchObject({ type: 'paragraph', children: [{ type: 'text', text: 'Docs' }] }); }); it('does not normalize while serializing and batches history snapshots', () => { const doc: WordsDocument = { version: 1, children: [createParagraph([createText('A'), createText('B')])] }; expect(JSON.parse(serializeWordsJson(doc)).children[0].children).toHaveLength(2); const initial = stateWithText(''); let history = createWordsHistory(initial, { limit: 2 }); history = batchHistoryCommands(history, [ { type: 'insertText', text: 'A' }, { type: 'insertText', text: 'B' }, { type: 'insertText', text: 'C' } ]); expect(history.past).toHaveLength(1); expect(documentPlainText(history.present.document)).toBe('ABC'); expect(documentPlainText(undoWords(history).present.document)).toBe(''); history = applyHistoryCommand(history, { type: 'insertText', text: 'D' }); history = applyHistoryCommand(history, { type: 'insertText', text: 'E' }); history = applyHistoryCommand(history, { type: 'insertText', text: 'F' }); expect(history.past).toHaveLength(2); }); it('keeps DOM render metadata separate from clean export formats', () => { const doc: WordsDocument = { version: 1, children: [ createParagraph([ createText('A', ['bold']), createLink('/docs', [createText('Docs', ['italic'])]) ]), createList('check', [ createListItem([createText('Done')], true), createListItem([createText('')]) ]) ] }; const domHtml = renderWordsDomHtml(doc); expect(domHtml).toContain('data-words-path'); expect(domHtml).toContain('\u200b'); const cleanHtml = serializeWordsHtml(doc); expect(cleanHtml).toBe( '

ADocs

' ); expect(cleanHtml).not.toContain('data-words'); expect(cleanHtml).not.toContain('\u200b'); expect(parseWordsPlainText('A\r\nB\u0000')).toEqual({ version: 1, children: [createParagraph([createText('A')]), createParagraph([createText('B')])] }); }); it('imports and exports a conservative markdown format', () => { const doc: WordsDocument = { version: 1, children: [ { type: 'heading', level: 1, children: [createText('Title')] }, createParagraph([ createText('Read '), createLink('/docs', [createText('docs', ['bold'])]), createText(' and '), createText('ship', ['italic']) ]), createList('check', [ createListItem([createText('Done')], true), createListItem([createText('Todo')]) ]) ] }; expect(serializeWordsMarkdown(doc)).toBe( '# Title\n\nRead [**docs**](/docs) and _ship_\n\n- [x] Done\n- [ ] Todo' ); const imported = parseWordsMarkdown( '# Title\n\nRead [**docs**](/docs) and _ship_\n\n- [x] Done\n- [ ] Todo' ); expect(imported.children).toEqual([ { type: 'heading', level: 1, children: [createText('Title')] }, createParagraph([ createText('Read '), createLink('/docs', [createText('docs', ['bold'])]), createText(' and '), createText('ship', ['italic']) ]), createList('check', [ createListItem([createText('Done')], true), createListItem([createText('Todo')]) ]) ]); }); it('imports safe html while dropping unsupported scripts and unsafe links', () => { const imported = parseWordsHtml( [ '

Title

', '

Hello bold bad', 'Docs

', '' ].join('') ); expect(imported.children).toEqual([ { type: 'heading', level: 2, children: [createText('Title')] }, createParagraph([ createText('Hello '), createText('bold', ['bold']), createText(' bad'), createLink('/docs', [createText('Docs')], 'Docs') ]), createList('check', [ createListItem([createText('Done')], true), createListItem([createText('Todo')]) ]) ]); expect(documentPlainText(imported)).not.toContain('alert'); }); it('imports and exports content through explicit commands', () => { const initial = stateWithText(''); const imported = applyWordsCommand(initial, { type: 'importContent', format: 'text', content: 'Alpha\nBeta' }); expect(imported.changed).toBe(true); expect(documentPlainText(imported.state.document)).toBe('Alpha\nBeta'); const exportedHtml = applyWordsCommand(imported.state, { type: 'exportContent', format: 'html' }); expect(exportedHtml.changed).toBe(false); expect(exportedHtml.exportedContent).toBe('

Alpha

Beta

'); const exportedJson = applyWordsCommand(imported.state, { type: 'exportContent', format: 'json' }); expect(exportedJson.exportedContent).toBe(serializeWordsJson(imported.state.document)); const exportedMarkdown = applyWordsCommand(imported.state, { type: 'exportContent', format: 'markdown' }); expect(exportedMarkdown.exportedContent).toBe('Alpha\n\nBeta'); const importedMarkdown = applyWordsCommand(initial, { type: 'importContent', format: 'markdown', content: '## Notes\n\n- one\n- two' }); expect(importedMarkdown.changed).toBe(true); expect(documentPlainText(importedMarkdown.state.document)).toBe('Notes\none\ntwo'); const importedHtml = applyWordsCommand(initial, { type: 'importContent', format: 'html', content: '

Notes

  1. one
  2. two
' }); expect(importedHtml.changed).toBe(true); expect(documentPlainText(importedHtml.state.document)).toBe('Notes\none\ntwo'); const invalid = applyWordsCommand(imported.state, { type: 'importContent', format: 'json', content: '{' }); expect(invalid.changed).toBe(false); expect(invalid.state).toBe(imported.state); }); it('renders stable data-words metadata for DOM reconciliation', () => { const doc: WordsDocument = { version: 1, children: [ createParagraph([ createText('A', ['bold']), createLink('/docs', [createText('Docs', ['italic'])]) ]), createList('check', [createListItem([createText('')])]) ] }; const rendered = renderWordsDocument(doc); expect(rendered[0]).toMatchObject({ kind: 'element', tag: 'p', attrs: { [WORDS_NODE_ATTR]: 'block', [WORDS_PATH_ATTR]: '0', 'data-words-block': 'paragraph' } }); expect(rendered[0]?.children[0]).toMatchObject({ kind: 'element', tag: 'span', attrs: { [WORDS_NODE_ATTR]: 'text', [WORDS_PATH_ATTR]: '0.0', 'data-words-marks': 'bold' }, children: [{ kind: 'text', text: 'A' }] }); expect(rendered[0]?.children[1]).toMatchObject({ kind: 'element', tag: 'a', attrs: { [WORDS_NODE_ATTR]: 'link', [WORDS_PATH_ATTR]: '0.1', href: '/docs' } }); expect(rendered[1]).toMatchObject({ tag: 'ul', attrs: { [WORDS_NODE_ATTR]: 'list', [WORDS_PATH_ATTR]: '1', 'data-words-list-kind': 'check' } }); expect(rendered[1]?.children[0]).toMatchObject({ tag: 'li', attrs: { [WORDS_NODE_ATTR]: 'list-item', [WORDS_PATH_ATTR]: '1.0', 'data-words-checked': 'false' } }); expect( rendered[1]?.children[0]?.kind === 'element' ? rendered[1].children[0].children[0] : undefined ).toMatchObject({ kind: 'element', attrs: { [WORDS_NODE_ATTR]: 'text', [WORDS_PATH_ATTR]: '1.0.0', [WORDS_EMPTY_TEXT_ATTR]: '' }, children: [{ kind: 'text', text: '\u200b' }] }); }); it('maps encoded DOM targets back to clamped Words points and selections', () => { const doc: WordsDocument = { version: 1, children: [createParagraph([createText('Alpha')])] }; expect(encodeWordsPath([0, 2, 3])).toBe('0.2.3'); expect(decodeWordsPath('0.2.3')).toEqual([0, 2, 3]); expect(decodeWordsPath('0.x')).toBeUndefined(); expect(domTargetToWordsPoint(doc, { path: '0.0', offset: 99 })).toEqual({ path: [0, 0], offset: 5 }); expect(domTargetToWordsPoint(doc, { path: '1.0', offset: 0 })).toBeUndefined(); const selection = domTargetToWordsSelection(doc, { anchor: { path: '0.0', offset: 1 }, focus: { path: '0.0', offset: 4 } }); expect(selection).toEqual({ anchor: { path: [0, 0], offset: 1 }, focus: { path: [0, 0], offset: 4 } }); expect( encodeWordsDomSelectionTarget(doc, { anchor: { path: [0, 0], offset: 0 }, focus: { path: [0, 0], offset: 99 } }) ).toEqual({ anchor: { path: '0.0', offset: 0 }, focus: { path: '0.0', offset: 5 } }); }); it('translates beforeinput intents to engine commands without leaking composition', () => { expect(actionFromBeforeInput({ inputType: 'insertText', data: 'A' })).toEqual({ type: 'command', command: { type: 'insertText', text: 'A' }, batch: 'typing' }); expect( actionFromBeforeInput({ inputType: 'insertText', data: 'A', isComposing: true }) ).toEqual({ type: 'ignore', reason: 'composition' }); expect(actionFromBeforeInput({ inputType: 'insertParagraph' })).toEqual({ type: 'command', command: { type: 'insertParagraph' } }); expect(actionFromBeforeInput({ inputType: 'deleteContentBackward' })).toEqual({ type: 'command', command: { type: 'deleteBackward' }, batch: 'typing' }); expect(actionFromBeforeInput({ inputType: 'formatBold' })).toEqual({ type: 'command', command: { type: 'toggleMark', mark: 'bold' } }); expect(actionFromBeforeInput({ inputType: 'historyUndo' })).toEqual({ type: 'history', direction: 'undo' }); expect(actionFromBeforeInput({ inputType: 'insertFromPaste', data: 'x' })).toEqual({ type: 'ignore', reason: 'unsupported' }); }); it('maps browser Selection API ranges through data-words-path', () => { const doc: WordsDocument = { version: 1, children: [createParagraph([createText('Alpha'), createLink('/docs', [createText('Docs')])])] }; const dom = new JSDOM(`

Alpha Docs

`); const root = dom.window.document.getElementById('editor'); const alpha = root?.querySelector('[data-words-path="0.0"]')?.firstChild; const docs = root?.querySelector('[data-words-path="0.1.0"]')?.firstChild; const selection = dom.window.getSelection(); if (!root || !alpha || !docs || !selection) throw new Error('test-dom-missing'); const range = dom.window.document.createRange(); range.setStart(alpha, 2); range.setEnd(docs, 3); selection.removeAllRanges(); selection.addRange(range); expect(selectionApiToWordsSelection(doc, root, selection)).toEqual({ anchor: { path: [0, 0], offset: 2 }, focus: { path: [0, 1, 0], offset: 3 } }); }); it('restores browser Selection API ranges from encoded DOM targets', () => { const dom = new JSDOM(`

Alpha ​

`); const root = dom.window.document.getElementById('editor'); const selection = dom.window.getSelection(); if (!root || !selection) throw new Error('test-dom-missing'); const restored = restoreSelectionApiFromDomTarget( root, { anchor: { path: '0.0', offset: 1 }, focus: { path: '0.1', offset: 9 } }, selection ); expect(restored).toBe(true); expect(selection.anchorNode?.textContent).toBe('Alpha'); expect(selection.anchorOffset).toBe(1); expect(selection.focusNode?.textContent).toBe('\u200b'); expect(selection.focusOffset).toBe(0); expect(resolveDomPointFromTarget(root, { path: '0.0', offset: 99 })).toMatchObject({ offset: 5 }); }); it('normalizes paste and composition into explicit text commands', () => { expect(actionFromPaste({ plainText: 'A\r\nB\u0000' })).toEqual({ type: 'command', command: { type: 'insertText', text: 'A\nB' } }); expect(actionFromPaste({ html: 'A' })).toEqual({ type: 'ignore', reason: 'unsupported-html' }); let composition = beginWordsComposition('a'); composition = updateWordsComposition(composition, 'á'); expect(endWordsComposition(composition)).toEqual({ type: 'command', command: { type: 'insertText', text: 'á' } }); }); });