一些检查失败了
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
142 行
5.3 KiB
TypeScript
142 行
5.3 KiB
TypeScript
import { EditorSelection } from '@codemirror/state';
|
||
import createTestEditor from '../testing/createTestEditor';
|
||
import { matchWikiLinkTrigger, createWikiLinkCompletionSource } from './wikiLinkAutocompleteExtension';
|
||
|
||
describe('wikiLinkAutocompleteExtension', () => {
|
||
describe('matchWikiLinkTrigger', () => {
|
||
it('matches [[ at the end of line', () => {
|
||
const res = matchWikiLinkTrigger('Hello [[');
|
||
expect(res).toEqual({ query: '', matchLength: 2, trigger: '[[' });
|
||
});
|
||
|
||
it('matches full-width 【【 for Chinese input method', () => {
|
||
const res1 = matchWikiLinkTrigger('引用笔记:【【');
|
||
expect(res1).toEqual({ query: '', matchLength: 2, trigger: '[[' });
|
||
|
||
const res2 = matchWikiLinkTrigger('引用笔记:【【工作周报');
|
||
expect(res2).toEqual({ query: '工作周报', matchLength: 6, trigger: '[[' });
|
||
});
|
||
|
||
it('matches full-width @ for Chinese input method', () => {
|
||
const res1 = matchWikiLinkTrigger('@');
|
||
expect(res1).toEqual({ query: '', matchLength: 1, trigger: '@' });
|
||
|
||
const res2 = matchWikiLinkTrigger('查看 @今天');
|
||
expect(res2).toEqual({ query: '今天', matchLength: 3, trigger: '@' });
|
||
});
|
||
|
||
it('matches [[ with partial query', () => {
|
||
const res = matchWikiLinkTrigger('Ref: [[Meeting');
|
||
expect(res).toEqual({ query: 'Meeting', matchLength: 9, trigger: '[[' });
|
||
});
|
||
|
||
it('matches @ at start of line', () => {
|
||
const res = matchWikiLinkTrigger('@Today');
|
||
expect(res).toEqual({ query: 'Today', matchLength: 6, trigger: '@' });
|
||
});
|
||
|
||
it('matches @ after whitespace or open brackets', () => {
|
||
const res1 = matchWikiLinkTrigger('Check this @journal');
|
||
expect(res1).toEqual({ query: 'journal', matchLength: 8, trigger: '@' });
|
||
|
||
const res2 = matchWikiLinkTrigger('(see @note');
|
||
expect(res2).toEqual({ query: 'note', matchLength: 5, trigger: '@' });
|
||
});
|
||
|
||
it('does NOT match normal email addresses', () => {
|
||
expect(matchWikiLinkTrigger('user@domain.com')).toBeNull();
|
||
expect(matchWikiLinkTrigger('contact: support@joplinapp.org')).toBeNull();
|
||
});
|
||
|
||
it('returns null when no trigger is present', () => {
|
||
expect(matchWikiLinkTrigger('Normal text [1]')).toBeNull();
|
||
expect(matchWikiLinkTrigger('Just a single [')).toBeNull();
|
||
});
|
||
|
||
it('returns null across newlines', () => {
|
||
expect(matchWikiLinkTrigger('[[\nSecond line')).toBeNull();
|
||
expect(matchWikiLinkTrigger('@\nSecond line')).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe('createWikiLinkCompletionSource', () => {
|
||
it('returns completion options matching searchNotes for @ mention', async () => {
|
||
const mockNotes = [
|
||
{ id: 'note-today', title: '2026-11-01 星期日', parentTitle: '📅 今日日志' },
|
||
{ id: 'note-yesterday', title: '2026-10-31 星期六', parentTitle: '📅 昨日日志' },
|
||
];
|
||
const mockSearch = jest.fn().mockResolvedValue(mockNotes);
|
||
const source = createWikiLinkCompletionSource(mockSearch);
|
||
|
||
const initialText = 'Today log: @';
|
||
const editor = await createTestEditor(initialText, EditorSelection.cursor(initialText.length), []);
|
||
|
||
const result = await source({
|
||
state: editor.state,
|
||
pos: initialText.length,
|
||
explicit: true,
|
||
} as any);
|
||
|
||
expect(result).not.toBeNull();
|
||
expect(result?.from).toBe(initialText.indexOf('@'));
|
||
expect(result?.options.length).toBe(2);
|
||
expect(result?.options[0].label).toBe('2026-11-01 星期日');
|
||
expect(result?.options[0].detail).toBe('📅 今日日志');
|
||
});
|
||
|
||
it('applies replacement with standard Joplin markdown link [Title](:/id)', async () => {
|
||
const mockNotes = [
|
||
{ id: 'uuid-today', title: '2026-11-01 星期日' },
|
||
];
|
||
const mockSearch = jest.fn().mockResolvedValue(mockNotes);
|
||
const source = createWikiLinkCompletionSource(mockSearch);
|
||
|
||
const initialText = 'See @2026';
|
||
const editor = await createTestEditor(initialText, EditorSelection.cursor(initialText.length), []);
|
||
|
||
const result = await source({
|
||
state: editor.state,
|
||
pos: initialText.length,
|
||
explicit: true,
|
||
} as any);
|
||
|
||
expect(result).not.toBeNull();
|
||
const option = result!.options[0];
|
||
|
||
// Simulate apply
|
||
(option.apply as any)(editor, option, result!.from, initialText.length);
|
||
|
||
expect(editor.state.doc.toString()).toBe('See [2026-11-01 星期日](:/uuid-today)');
|
||
});
|
||
|
||
it('creates note on demand when selecting __CREATE_ item', async () => {
|
||
const mockNotes = [
|
||
{ id: '__CREATE_TODAY__', title: '2026-11-01 星期日', parentTitle: '📅 新建今日日志' },
|
||
];
|
||
const mockSearch = jest.fn().mockResolvedValue(mockNotes);
|
||
const mockCreate = jest.fn().mockResolvedValue({ id: 'brand-new-uuid', title: '2026-11-01 星期日' });
|
||
const source = createWikiLinkCompletionSource(mockSearch, mockCreate);
|
||
|
||
const initialText = 'Today: @今天';
|
||
const editor = await createTestEditor(initialText, EditorSelection.cursor(initialText.length), []);
|
||
|
||
const result = await source({
|
||
state: editor.state,
|
||
pos: initialText.length,
|
||
explicit: true,
|
||
} as any);
|
||
|
||
expect(result).not.toBeNull();
|
||
const option = result!.options[0];
|
||
|
||
(option.apply as any)(editor, option, result!.from, initialText.length);
|
||
|
||
// Wait for async createNote to resolve and dispatch
|
||
await new Promise(r => setTimeout(r, 20));
|
||
|
||
expect(mockCreate).toHaveBeenCalledWith('2026-11-01 星期日');
|
||
expect(editor.state.doc.toString()).toBe('Today: [2026-11-01 星期日](:/brand-new-uuid)');
|
||
});
|
||
});
|
||
});
|