Files
2026-03-22 23:14:16 +00:00

257 lines
9.0 KiB
TypeScript

import CodeMirror5Emulation from './CodeMirror5Emulation';
import { EditorView } from '@codemirror/view';
const makeCodeMirrorEmulation = (initialDocText: string) => {
const editorView = new EditorView({
doc: initialDocText,
});
return new CodeMirror5Emulation(editorView, ()=>{});
};
describe('CodeMirror5Emulation', () => {
it('getSearchCursor should support searching for strings', () => {
const codeMirror = makeCodeMirrorEmulation('testing --- this is a test.');
// Should find two matches for "test"
// Note that the CodeMirror documentation specifies that a search cursor
// should return a boolean when calling findNext/findPrevious. However,
// the codemirror-vim adapter returns just a truthy/falsy value.
const testCursor = codeMirror.getSearchCursor('test');
expect(testCursor.findNext()).toBeTruthy();
expect(testCursor.findNext()).toBeTruthy();
// Replace the second match
testCursor.replace('passing test');
expect(codeMirror.getValue()).toBe('testing --- this is a passing test.');
// Should also be able to find previous matches
expect(testCursor.findPrevious()).toBeTruthy();
// Should return a falsy value when attempting to search past the end of
// the document.
expect(testCursor.findPrevious()).toBeFalsy();
});
it('should fire update/change events on change', async () => {
const codeMirror = makeCodeMirrorEmulation('testing --- this is a test.');
const updateCallback = jest.fn();
const changeCallback = jest.fn();
codeMirror.on('update', updateCallback);
codeMirror.on('change', changeCallback);
expect(updateCallback).not.toHaveBeenCalled();
expect(changeCallback).not.toHaveBeenCalled();
jest.useFakeTimers();
// Inserting text should trigger the update and change events
codeMirror.editor.dispatch({
changes: { from: 0, to: 1, insert: 'Test: ' },
});
// Advance timers -- there may be a delay between the CM 6 event
// and the dispatched CM 5 event.
await jest.advanceTimersByTimeAsync(100);
expect(updateCallback).toHaveBeenCalled();
expect(changeCallback).toHaveBeenCalled();
// The change callback should be given two arguments:
// - the CodeMirror emulation object
// - a description of the changes
expect(changeCallback.mock.lastCall[0]).toBe(codeMirror);
expect(changeCallback.mock.lastCall[1]).toMatchObject({
from: { line: 0, ch: 0 },
to: { line: 0, ch: 1 },
// Arrays of lines
text: ['Test: '],
removed: ['t'],
});
});
it('defineOption should fire the option\'s update callback on change', () => {
const codeMirror = makeCodeMirrorEmulation('Test 1\nTest 2');
const onOptionUpdate = jest.fn();
codeMirror.defineOption('an-option!', 'test', onOptionUpdate);
const onOtherOptionUpdate = jest.fn();
codeMirror.defineOption('an-option 2', 1, onOtherOptionUpdate);
// onUpdate should be called once initially
expect(onOtherOptionUpdate).toHaveBeenCalledTimes(1);
expect(onOptionUpdate).toHaveBeenCalledTimes(1);
expect(onOptionUpdate).toHaveBeenLastCalledWith(
codeMirror,
// default value -- the new value
'test',
// the original value (none, so given CodeMirror.Init)
codeMirror.Init,
);
// onUpdate should be called each time the option changes
codeMirror.setOption('an-option!', 'test 2');
expect(onOptionUpdate).toHaveBeenCalledTimes(2);
expect(onOptionUpdate).toHaveBeenLastCalledWith(
codeMirror, 'test 2', 'test',
);
codeMirror.setOption('an-option!', 'test...');
expect(onOptionUpdate).toHaveBeenCalledTimes(3);
// The other update callback should not have been triggered
// additional times if its option hasn't updated.
expect(onOtherOptionUpdate).toHaveBeenCalledTimes(1);
});
it('should support running commands registered with defineExtension', () => {
const codeMirror = makeCodeMirrorEmulation('Test 1\nTest 2');
const testExtension = jest.fn((a: number) => `testing${a}`);
codeMirror.defineExtension('testExtension', testExtension);
expect(codeMirror.commandExists('testExtension')).toBe(true);
expect(codeMirror.execCommand('testExtension', 1)).toBe('testing1');
});
it('markText decorations should be removable', () => {
const codeMirror = makeCodeMirrorEmulation('Test 1\nTest 2');
const markDecoration = codeMirror.markText(
{ line: 0, ch: 0 },
{ line: 0, ch: 6 },
{ className: 'test-mark-decoration' },
);
const markDecoration2 = codeMirror.markText(
{ line: 1, ch: 0 },
{ line: 1, ch: 1 },
{ className: 'test-decoration-2' },
);
const editorDom = codeMirror.cm6.dom;
expect(editorDom.querySelectorAll('.test-mark-decoration')).toHaveLength(1);
expect(editorDom.querySelectorAll('.test-decoration-2')).toHaveLength(1);
codeMirror.setCursor(0, 2);
codeMirror.replaceSelection('!Test!');
// Editing the document shouldn't remove the mark
expect(codeMirror.editor.state.doc.toString()).toBe('Te!Test!st 1\nTest 2');
expect(editorDom.querySelectorAll('.test-mark-decoration')).toHaveLength(1);
// Clearing should remove only the decoration that was cleared.
markDecoration.clear();
expect(editorDom.querySelectorAll('.test-mark-decoration')).toHaveLength(0);
expect(editorDom.querySelectorAll('.test-decoration-2')).toHaveLength(1);
markDecoration2.clear();
expect(editorDom.querySelectorAll('.test-decoration-2')).toHaveLength(0);
});
it('defineExtension should override previous extensions with the same name', () => {
const codeMirror = makeCodeMirrorEmulation('Test...');
const testExtensionFn1 = jest.fn();
const testExtensionFn2 = jest.fn();
codeMirror.defineExtension('defineExtensionShouldOverride', testExtensionFn1);
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Accessing dynamic extension
(codeMirror as any).defineExtensionShouldOverride();
expect(testExtensionFn1).toHaveBeenCalledTimes(1);
expect(testExtensionFn2).toHaveBeenCalledTimes(0);
codeMirror.defineExtension('defineExtensionShouldOverride', testExtensionFn2);
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Accessing dynamic extension
(codeMirror as any).defineExtensionShouldOverride();
expect(testExtensionFn1).toHaveBeenCalledTimes(1);
expect(testExtensionFn2).toHaveBeenCalledTimes(1);
});
it('defineExtension should register an extension where this points to the editor', () => {
const codeMirror = makeCodeMirrorEmulation('Test...');
let lastThis = null;
codeMirror.defineExtension('testExtension', function() {
lastThis = this;
});
codeMirror.execCommand('testExtension');
expect(lastThis).toBe(codeMirror);
});
it('.markText should support specifying ranges outside the document', () => {
const codeMirror = makeCodeMirrorEmulation('Test...');
const testClassName = 'out-of-range-test-mark';
codeMirror.markText(
// In range
{ line: 0, ch: 4 },
// Out of range
{ line: 0, ch: 1002 },
{ className: testClassName },
);
const dom = codeMirror.editor.dom;
expect(dom.querySelectorAll(`.${testClassName}`)).toHaveLength(1);
});
it('.markText should throw when given non-integer boundaries', () => {
const codeMirror = makeCodeMirrorEmulation('Test...');
expect(() => codeMirror.markText(
{ line: 0, ch: 4.2 },
{ line: 0, ch: 5 },
)).toThrow(/is not an integer/i);
});
it('heightAtLine for a line past the document should be greater than for the last line', () => {
const codeMirror = makeCodeMirrorEmulation('line1\nline2\nline3');
// Mock lineBlockAt to return a block with non-zero height so that the
// distinction between top and top+height is observable in the test.
const mockLineBlock = { top: 900, bottom: 1000, height: 100, from: 0, to: 0 };
jest.spyOn(codeMirror.editor, 'lineBlockAt').mockReturnValue(mockLineBlock as never);
const lineCount = codeMirror.lineCount();
const heightAtLastLine = codeMirror.heightAtLine(lineCount - 1, 'local');
const heightPastEnd = codeMirror.heightAtLine(lineCount, 'local');
expect(heightPastEnd).toBeGreaterThan(heightAtLastLine);
});
it('heightAtLine for a line past the document should be greater than for the last line, given one long line', () => {
const singleLine = 'Very long line of text. '.repeat(400);
const codeMirror = makeCodeMirrorEmulation(`${singleLine}`);
// Mock lineBlockAt to return a block with non-zero height so that the
// distinction between top and top+height is observable in the test.
const mockLineBlock = { top: 900, bottom: 1000, height: 100, from: 0, to: 0 };
jest.spyOn(codeMirror.editor, 'lineBlockAt').mockReturnValue(mockLineBlock as never);
const lineCount = codeMirror.lineCount();
const heightAtLastLine = codeMirror.heightAtLine(lineCount - 1, 'local');
const heightPastEnd = codeMirror.heightAtLine(lineCount, 'local');
expect(heightPastEnd).toBeGreaterThan(heightAtLastLine);
});
it('heightAtLine should return a non-negative value for valid line numbers', () => {
const codeMirror = makeCodeMirrorEmulation('first\nsecond\nthird');
const lineCount = codeMirror.lineCount();
// Test all lines from top to bottom of document
for (let i = 0; i <= lineCount; i++) {
expect(codeMirror.heightAtLine(i, 'local')).toBeGreaterThanOrEqual(0);
}
});
});