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)'); }); }); });