feat: Initial clean release of CHM
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
这个提交包含在:
@@ -0,0 +1,256 @@
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户