import assert from 'node:assert/strict' import test from 'node:test' import { nextTick, ref } from 'vue' import { MARKDOWN_SLASH_COMMANDS } from '../lib/markdown-slash-commands.js' import { useAdminMarkdownSlashCommandActions } from '../composables/useAdminMarkdownSlashCommandActions.js' /** * 테스트용 슬래시 명령 실행 상태를 생성한다. * @param {Object} [options] - 테스트 옵션 * @returns {Object} 테스트 상태 */ const createSlashCommandActionsState = (options = {}) => { const calls = { clear: 0, focusPreview: [], media: [], release: [], replace: [], selection: [], suppress: [] } const activeMode = ref(options.activeMode ?? 'write') const markdownValue = ref(options.markdownValue ?? '/image\n아래') const liveSlashSourceLine = ref(options.sourceLine ?? 0) const liveSlashQuery = ref(options.query ?? '') const liveSlashInsertLine = ref(null) const actions = useAdminMarkdownSlashCommandActions({ activeMode, markdownValue, liveSlashSourceLine, liveSlashQuery, liveSlashInsertLine, clearLiveSlashState: () => { calls.clear += 1 liveSlashSourceLine.value = null }, releaseLiveSlashLine: (line) => calls.release.push(line), suppressLiveSlashLine: (line, value) => calls.suppress.push({ line, value }), replaceLineRange: (startLine, endLine, replacementLines, focusEditor = true) => { calls.replace.push({ startLine, endLine, replacementLines, focusEditor }) const lines = String(markdownValue.value || '').split('\n') lines.splice(startLine, endLine - startLine + 1, ...replacementLines) markdownValue.value = lines.join('\n') }, openMediaPicker: (mediaKind) => calls.media.push(mediaKind), setTextareaSelection: (start, end) => calls.selection.push({ start, end }), focusPreviewEditableAtLine: (...args) => calls.focusPreview.push(args), isClient: options.isClient ?? false }) return { actions, activeMode, calls, liveSlashInsertLine, liveSlashQuery, liveSlashSourceLine, markdownValue } } /** * 테스트용 프레임 콜백을 즉시 실행하도록 설치한다. * @returns {() => void} 복구 함수 */ const installFrameStub = () => { const previousFrame = globalThis.requestAnimationFrame globalThis.requestAnimationFrame = (callback) => { callback() return 1 } return () => { if (previousFrame === undefined) { delete globalThis.requestAnimationFrame return } globalThis.requestAnimationFrame = previousFrame } } test('관리자 Markdown 슬래시 미디어 명령은 원문 줄을 제거하고 미디어 모달을 연다', () => { const state = createSlashCommandActionsState() state.actions.applyLiveSlashCommand(MARKDOWN_SLASH_COMMANDS.find((command) => command.id === 'image')) assert.equal(state.markdownValue.value, '아래') assert.equal(state.liveSlashInsertLine.value, 0) assert.deepEqual(state.calls.replace, [{ startLine: 0, endLine: 0, replacementLines: [], focusEditor: false }]) assert.deepEqual(state.calls.media, ['image']) assert.equal(state.calls.clear, 1) }) test('관리자 Markdown 슬래시 줄 명령은 작성 모드에서 textarea 선택을 복원한다', () => { const state = createSlashCommandActionsState({ markdownValue: '/h2\n아래', query: 'h2' }) state.actions.applyLiveSlashCommand(MARKDOWN_SLASH_COMMANDS.find((command) => command.id === 'h2')) assert.equal(state.markdownValue.value, '## \n아래') assert.deepEqual(state.calls.release, [0]) assert.deepEqual(state.calls.selection, [{ start: 3, end: 3 }]) assert.equal(state.calls.clear, 1) }) test('관리자 Markdown 슬래시 줄 명령은 라이브 모드에서 미리보기 포커스를 예약한다', async () => { const restoreFrame = installFrameStub() const state = createSlashCommandActionsState({ activeMode: 'preview', markdownValue: '/code\n아래', query: 'code', isClient: true }) try { state.actions.applyLiveSlashCommand(MARKDOWN_SLASH_COMMANDS.find((command) => command.id === 'code')) await nextTick() await nextTick() assert.equal(state.markdownValue.value, ['```', '', '```', '아래'].join('\n')) assert.deepEqual(state.calls.focusPreview, [[1, 0, 'end', 0]]) } finally { restoreFrame() } }) test('관리자 Markdown 슬래시 적용 payload 후보가 없으면 입력 상태를 종료한다', () => { const state = createSlashCommandActionsState({ query: '없는명령', sourceLine: 2 }) state.actions.onLiveSlashApply({ sourceLine: 2, value: '/없는명령' }) assert.deepEqual(state.calls.release, [2]) assert.equal(state.calls.clear, 1) assert.deepEqual(state.calls.replace, []) }) test('관리자 Markdown 슬래시 취소는 현재 줄 내용을 기준으로 억제한다', () => { const state = createSlashCommandActionsState({ markdownValue: '첫 줄\n/gallery', sourceLine: 1 }) state.actions.cancelLiveSlashCommand() assert.deepEqual(state.calls.suppress, [{ line: 1, value: '/gallery' }]) assert.equal(state.calls.clear, 1) })