import assert from 'node:assert/strict' import test from 'node:test' import { nextTick, ref } from 'vue' import { useAdminMarkdownBlockActions } from '../composables/useAdminMarkdownBlockActions.js' /** * 테스트용 블록 액션 상태를 만든다. * @param {Object} options - 테스트 옵션 * @returns {Object} 테스트 상태 */ const createBlockActionsState = (options = {}) => { const calls = { applyPlans: [], ensure: 0, focusPreview: [], insertGallery: [], previewFocusRequests: [], textareaSelections: [] } const value = ref(options.value ?? '![기존](/old.png)') const activeMediaBlock = ref(options.activeMediaBlock ?? { kind: 'image', startLine: 0, endLine: 0, images: [{ alt: '기존', url: '/old.png' }] }) const activeBlockContext = ref(options.activeBlockContext ?? activeMediaBlock.value) const inlineColorSelectionContext = ref(options.inlineColorSelectionContext ?? null) const actions = useAdminMarkdownBlockActions({ value, activeMediaBlock, activeBlockContext, inlineColorSelectionContext, applyLineReplacementPlan: (plan, focusEditor = false) => { calls.applyPlans.push({ plan, focusEditor }) if (!plan) { return false } const lines = String(value.value || '').split('\n') lines.splice(plan.startLine, plan.endLine - plan.startLine + 1, ...plan.replacementLines) value.value = lines.join('\n') return true }, ensureBlockPanelEngaged: () => { calls.ensure += 1 }, insertGallery: (images) => { calls.insertGallery.push(images) }, setTextareaSelection: (...args) => { calls.textareaSelections.push(args) }, requestPreviewFocus: (focus) => { calls.previewFocusRequests.push(focus) }, focusPreviewEditableAtLine: (...args) => { calls.focusPreview.push(args) } }) return { actions, activeBlockContext, activeMediaBlock, calls, inlineColorSelectionContext, value } } test('이미지 표시 너비 변경을 현재 미디어 줄에 적용한다', () => { const state = createBlockActionsState() state.actions.updateActiveMediaImage(0, { pixelWidth: 240 }) assert.equal(state.value.value, '![](/old.png){size=240}') assert.equal(state.calls.ensure, 1) assert.equal(state.calls.applyPlans.at(-1).focusEditor, false) }) test('파일명 캡션 사용은 이미지 URL 기준 기본 캡션을 채운다', () => { const state = createBlockActionsState({ value: '![](/uploads/posts/2026/07/sample.png)', activeMediaBlock: { kind: 'image', startLine: 0, endLine: 0, images: [{ alt: '', caption: '', url: '/uploads/posts/2026/07/sample.png' }] } }) state.actions.setActiveMediaUseAlt(0, true) assert.equal(state.value.value, '![](/uploads/posts/2026/07/sample.png "sample.png")') assert.equal(state.calls.ensure, 1) }) test('활성 갤러리가 없으면 갤러리 삽입 함수로 위임한다', () => { const state = createBlockActionsState({ activeMediaBlock: null }) const images = [{ url: '/a.png', alt: 'A' }] state.actions.appendImagesToActiveGallery(images) assert.deepEqual(state.calls.insertGallery, [images]) assert.equal(state.calls.applyPlans.length, 0) }) test('소스 모드 인라인 색상 적용 후 textarea 선택 범위를 갱신한다', () => { const state = createBlockActionsState({ value: '강조 문장', inlineColorSelectionContext: { kind: 'inline-color', mode: 'source', start: 0, end: 2, text: '강조' } }) state.actions.updateActiveInlineColor('red') assert.equal(state.value.value, '::red[강조] 문장') assert.equal(state.inlineColorSelectionContext.value, null) assert.deepEqual(state.calls.textareaSelections, [[9, 9, { preserveScroll: true }]]) assert.equal(state.calls.ensure, 1) }) test('라이브 모드 인라인 색상 적용 후 미리보기 포커스를 예약한다', async () => { const state = createBlockActionsState({ value: '> 문장 테스트', inlineColorSelectionContext: { kind: 'inline-color', mode: 'live', sourceLine: 0, startOffset: 0, endOffset: 2, bodyText: '문장 테스트', text: '문장' } }) state.actions.updateActiveInlineColor('blue') await nextTick() assert.equal(state.value.value, '> ::blue[문장] 테스트') assert.equal(state.inlineColorSelectionContext.value, null) assert.deepEqual(state.calls.previewFocusRequests, [{ line: 0, offset: 2 }]) assert.deepEqual(state.calls.focusPreview, [[0, 0, 'auto', 2, 'nearest']]) })