import assert from 'node:assert/strict' import test from 'node:test' import { effectScope, nextTick, ref } from 'vue' import { useAdminMarkdownTextareaFocus } from '../composables/useAdminMarkdownTextareaFocus.js' /** * 테스트용 textarea DOM 대역을 만든다. * @param {Object} options - textarea 옵션 * @returns {Object} textarea 대역 */ const createTextareaStub = (options = {}) => { const calls = { focus: 0, selections: [] } const textarea = { value: options.value ?? '첫 줄\n둘째 줄\n셋째 줄', selectionStart: options.selectionStart ?? 0, selectionEnd: options.selectionEnd ?? options.selectionStart ?? 0, scrollHeight: options.scrollHeight ?? 700, scrollTop: options.scrollTop ?? 0, clientWidth: options.clientWidth ?? 720, style: { height: '' }, focus: () => { calls.focus += 1 }, setSelectionRange: (start, end) => { calls.selections.push([start, end]) textarea.selectionStart = start textarea.selectionEnd = end }, getBoundingClientRect: () => ({ top: 0, bottom: 700, height: 700 }), calls } return textarea } /** * 테스트용 textarea 포커스 composable 상태를 만든다. * @param {Object} options - 테스트 옵션 * @returns {Object} 테스트 상태 */ const createTextareaFocusState = (options = {}) => { const scope = effectScope() const value = ref(options.value ?? '첫 줄\n둘째 줄\n셋째 줄') const activeMode = ref(options.activeMode ?? 'write') const writeModeContextValue = ref(null) const lastSelectionState = ref(options.lastSelectionState ?? { start: 0, end: 0 }) const activeLogicalLineIndex = ref(0) const gutterLineHeights = ref([28, 28, 28]) const editorRootRef = ref(null) const textarea = options.textarea ?? createTextareaStub({ value: value.value }) const calls = { syncBlockPanelState: 0, syncWriteSlashState: 0 } let focusState scope.run(() => { focusState = useAdminMarkdownTextareaFocus({ value, activeMode, writeModeContextValue, lastSelectionState, activeLogicalLineIndex, gutterLineHeights, editorRootRef, minTextareaHeight: 620, getWriteTextareaElement: () => textarea, getLineStartOffset: (lineIndex) => { const lines = String(value.value).split('\n') return lines.slice(0, lineIndex).join('\n').length + (lineIndex > 0 ? 1 : 0) }, syncBlockPanelState: () => { calls.syncBlockPanelState += 1 }, syncWriteSlashState: () => { calls.syncWriteSlashState += 1 } }) }) return { activeLogicalLineIndex, calls, focusState, lastSelectionState, textarea, value, writeModeContextValue, stop: () => scope.stop() } } /** * composable 내부 nextTick을 비운다. * @returns {Promise} */ const flushTextareaFocusTicks = async () => { await nextTick() await nextTick() await nextTick() } test('textarea 선택 영역을 마지막 선택 상태로 기억한다', () => { const textarea = createTextareaStub({ value: 'abc\ndef', selectionStart: 5, selectionEnd: 7 }) const state = createTextareaFocusState({ value: 'abc\ndef', textarea }) state.focusState.rememberTextareaSelection() assert.deepEqual(state.lastSelectionState.value, { start: 5, end: 7 }) state.stop() }) test('커서 위치 기준으로 작성 컨텍스트와 활성 줄을 갱신한다', async () => { const textarea = createTextareaStub({ value: '첫 줄\n둘째 줄\n셋째 줄', selectionStart: 8, selectionEnd: 8 }) const state = createTextareaFocusState({ textarea }) state.focusState.refreshCaretLogicalLine() await flushTextareaFocusTicks() assert.equal(state.writeModeContextValue.value, '첫 줄\n둘째 줄\n셋째 줄') assert.deepEqual(state.lastSelectionState.value, { start: 8, end: 8 }) assert.equal(state.activeLogicalLineIndex.value, 1) assert.equal(state.calls.syncBlockPanelState, 1) assert.equal(textarea.style.height, '700px') state.stop() }) test('지정 줄로 textarea 포커스와 커서를 이동한다', async () => { const textarea = createTextareaStub() const state = createTextareaFocusState({ textarea }) state.focusState.focusTextareaAtLine(2) await flushTextareaFocusTicks() assert.equal(textarea.calls.focus, 1) assert.deepEqual(textarea.calls.selections[0], [9, 9]) assert.equal(state.activeLogicalLineIndex.value, 2) state.stop() }) test('마지막 선택 상태로 textarea 포커스를 복원한다', async () => { const textarea = createTextareaStub() const state = createTextareaFocusState({ textarea, lastSelectionState: { start: 5, end: 8 } }) state.focusState.restoreTextareaFocus() await flushTextareaFocusTicks() assert.equal(textarea.calls.focus, 1) assert.deepEqual(textarea.calls.selections[0], [5, 8]) assert.equal(state.activeLogicalLineIndex.value, 1) state.stop() })