import assert from 'node:assert/strict' import test from 'node:test' import { addSlashSuppressedLine, clampSlashHighlightedIndex, createSlashCommandApplyPlan, createSlashCommandEditorEffectPlan, getMediaPickerKindFromSlashAction, getNextSlashHighlightedIndex, getSlashCommandFocusTarget, getSlashCommandTextareaSelection, getSlashMenuKeyboardAction, insertMarkdownLinesAtSlashTarget, isMediaSlashAction, parseSlashInput, removeSlashSuppressedLine, resolveSlashCommand, resolveSlashCommandFromInputValue, shouldCaptureSlashMenuKeyboardAction, shouldIgnorePendingMediaSlashCommit } from '../lib/markdown-slash-commands.js' test('슬래시 입력은 단일 줄 명령만 파싱한다', () => { assert.deepEqual(parseSlashInput('/image'), { query: 'image', raw: '/image' }) assert.equal(parseSlashInput(' /image'), null) assert.equal(parseSlashInput('/image\n다음 줄'), null) }) test('정확한 슬래시 명령과 미디어 액션을 판별한다', () => { assert.equal(resolveSlashCommand('image')?.action, 'media-image') assert.equal(resolveSlashCommand('img')?.id, 'image') assert.equal(resolveSlashCommandFromInputValue('/image')?.id, 'image') assert.equal(resolveSlashCommandFromInputValue('/ㄴㅇ'), null) assert.equal(resolveSlashCommandFromInputValue(' /image'), null) assert.equal(isMediaSlashAction('media-image'), true) assert.equal(isMediaSlashAction('lines'), false) assert.equal(getMediaPickerKindFromSlashAction('media-image'), 'image') assert.equal(getMediaPickerKindFromSlashAction('media-gallery'), 'gallery') assert.equal(getMediaPickerKindFromSlashAction('lines'), '') }) test('슬래시 명령 적용 계획은 미디어와 줄 삽입을 구분한다', () => { assert.deepEqual(createSlashCommandApplyPlan({ command: resolveSlashCommand('image'), sourceLine: 2 }), { kind: 'media', line: 2, mediaKind: 'image' }) const headingPlan = createSlashCommandApplyPlan({ command: resolveSlashCommand('h2'), sourceLine: 4 }) assert.deepEqual(headingPlan, { kind: 'lines', line: 4, lines: ['## '] }) assert.notEqual(headingPlan.lines, resolveSlashCommand('h2')?.lines) }) test('슬래시 명령 적용 계획은 적용할 수 없는 입력을 무시한다', () => { assert.deepEqual(createSlashCommandApplyPlan({ command: null, sourceLine: 1 }), { kind: 'none' }) assert.deepEqual(createSlashCommandApplyPlan({ command: resolveSlashCommand('image'), sourceLine: null }), { kind: 'none' }) assert.deepEqual(createSlashCommandApplyPlan({ command: { id: 'broken', label: '깨진 명령', description: '', keywords: [], action: 'unknown' }, sourceLine: 1 }), { kind: 'none' }) }) test('슬래시 명령 에디터 효과 계획은 미디어 모달 실행을 유지한다', () => { const applyPlan = createSlashCommandApplyPlan({ command: resolveSlashCommand('image'), sourceLine: 2 }) assert.deepEqual(createSlashCommandEditorEffectPlan({ plan: applyPlan, activeMode: 'preview' }), { kind: 'media', line: 2, mediaKind: 'image' }) }) test('슬래시 명령 에디터 효과 계획은 모드별 포커스 대상을 계산한다', () => { const applyPlan = createSlashCommandApplyPlan({ command: resolveSlashCommand('code'), sourceLine: 5 }) assert.deepEqual(createSlashCommandEditorEffectPlan({ plan: applyPlan, activeMode: 'write' }), { kind: 'lines', line: 5, lines: ['```', '', '```'], focusTarget: 'textarea' }) assert.deepEqual(createSlashCommandEditorEffectPlan({ plan: applyPlan, activeMode: 'preview' }), { kind: 'lines', line: 5, lines: ['```', '', '```'], focusTarget: 'preview' }) }) test('슬래시 명령 에디터 효과 계획은 무효 계획을 무시한다', () => { assert.deepEqual(createSlashCommandEditorEffectPlan({ plan: { kind: 'none' }, activeMode: 'write' }), { kind: 'none' }) assert.deepEqual(createSlashCommandEditorEffectPlan({ plan: null, activeMode: 'preview' }), { kind: 'none' }) }) test('미디어 슬래시 명령 지연 commit은 같은 줄 slash 원문일 때만 무시한다', () => { assert.equal(shouldIgnorePendingMediaSlashCommit({ pendingInsertLine: 3, startLine: 3, replacementLines: ['/image'] }), true) assert.equal(shouldIgnorePendingMediaSlashCommit({ pendingInsertLine: 3, startLine: 4, replacementLines: ['/image'] }), false) assert.equal(shouldIgnorePendingMediaSlashCommit({ pendingInsertLine: 3, startLine: 3, replacementLines: ['본문'] }), false) }) test('슬래시 제외 줄 Set은 기존 값을 보존한 새 Set으로 갱신한다', () => { const source = new Set([1, 2]) const added = addSlashSuppressedLine(source, 3) const removed = removeSlashSuppressedLine(added, 2) assert.deepEqual([...source], [1, 2]) assert.deepEqual([...added], [1, 2, 3]) assert.deepEqual([...removed], [1, 3]) assert.notEqual(added, source) assert.notEqual(removed, added) }) test('슬래시 제외 줄 Set은 유효하지 않은 줄 번호를 무시한다', () => { assert.deepEqual([...addSlashSuppressedLine(new Set([1]), Number.NaN)], [1]) assert.deepEqual([...removeSlashSuppressedLine(new Set([1]), Number.NaN)], [1]) }) test('슬래시 메뉴 하이라이트 인덱스는 명령 목록 범위 안으로 보정한다', () => { assert.equal(clampSlashHighlightedIndex(3, 5), 3) assert.equal(clampSlashHighlightedIndex(7, 5), 4) assert.equal(clampSlashHighlightedIndex(-2, 5), 0) assert.equal(clampSlashHighlightedIndex(2, 0), 0) assert.equal(clampSlashHighlightedIndex(Number.NaN, Number.NaN), 0) }) test('슬래시 메뉴 하이라이트 인덱스는 방향키로 순환한다', () => { assert.equal(getNextSlashHighlightedIndex({ currentIndex: 1, commandCount: 3, key: 'ArrowDown' }), 2) assert.equal(getNextSlashHighlightedIndex({ currentIndex: 2, commandCount: 3, key: 'ArrowDown' }), 0) assert.equal(getNextSlashHighlightedIndex({ currentIndex: 0, commandCount: 3, key: 'ArrowUp' }), 2) assert.equal(getNextSlashHighlightedIndex({ currentIndex: 8, commandCount: 3, key: 'ArrowUp' }), 1) assert.equal(getNextSlashHighlightedIndex({ currentIndex: 1, commandCount: 0, key: 'ArrowDown' }), 0) assert.equal(getNextSlashHighlightedIndex({ currentIndex: 1, commandCount: 3, key: 'Enter' }), 1) }) test('슬래시 메뉴 키보드 입력은 메뉴 동작으로 분류한다', () => { assert.equal(getSlashMenuKeyboardAction('ArrowDown'), 'navigate') assert.equal(getSlashMenuKeyboardAction('ArrowUp'), 'navigate') assert.equal(getSlashMenuKeyboardAction('Enter'), 'apply') assert.equal(getSlashMenuKeyboardAction('Escape'), 'cancel') assert.equal(getSlashMenuKeyboardAction('Tab'), '') }) test('슬래시 메뉴 키보드 입력은 적용 가능한 명령이 있을 때만 가로챈다', () => { assert.equal(shouldCaptureSlashMenuKeyboardAction({ action: 'navigate', commandCount: 2 }), true) assert.equal(shouldCaptureSlashMenuKeyboardAction({ action: 'apply', commandCount: 1 }), true) assert.equal(shouldCaptureSlashMenuKeyboardAction({ action: 'navigate', commandCount: 0 }), false) assert.equal(shouldCaptureSlashMenuKeyboardAction({ action: 'apply', commandCount: 0 }), false) assert.equal(shouldCaptureSlashMenuKeyboardAction({ action: 'cancel', commandCount: 0 }), true) assert.equal(shouldCaptureSlashMenuKeyboardAction({ action: '', commandCount: 1 }), false) }) test('슬래시 명령 줄에 미디어를 삽입할 때 원문 줄을 교체한다', () => { const markdown = [ '첫 줄', '/image', '마지막 줄' ].join('\n') assert.equal(insertMarkdownLinesAtSlashTarget(markdown, 1, ['![](/uploads/a.png)']), [ '첫 줄', '![](/uploads/a.png)', '마지막 줄' ].join('\n')) }) test('일반 줄에는 지정 위치 앞에 마크다운 줄을 삽입한다', () => { assert.equal(insertMarkdownLinesAtSlashTarget('첫 줄\n마지막 줄', 1, ['중간 줄']), '첫 줄\n중간 줄\n마지막 줄') }) test('슬래시 명령 포커스 대상은 단일 줄과 블록 삽입을 구분한다', () => { assert.deepEqual(getSlashCommandFocusTarget(2, ['## ']), { line: 2, offset: 3 }) assert.deepEqual(getSlashCommandFocusTarget(2, ['```', '', '```']), { line: 3, offset: 0 }) }) test('슬래시 명령 textarea 선택 위치는 줄 시작과 포커스 오프셋을 합산한다', () => { const markdown = [ '첫 줄', '둘째 줄', '## ', '마지막 줄' ].join('\n') assert.deepEqual(getSlashCommandTextareaSelection({ markdown, startLine: 2, lines: ['## '] }), { start: 12, end: 12 }) }) test('슬래시 명령 textarea 선택 위치는 여러 줄 블록의 내부 빈 줄을 가리킨다', () => { const markdown = [ '첫 줄', '```', '', '```', '마지막 줄' ].join('\n') assert.deepEqual(getSlashCommandTextareaSelection({ markdown, startLine: 1, lines: ['```', '', '```'] }), { start: 8, end: 8 }) })