Files
sori.studio/tests/markdown-editor-media.test.js

188 lines
6.6 KiB
JavaScript

import assert from 'node:assert/strict'
import test from 'node:test'
import {
createMediaBlockMarkdown,
findUploadSizeLimitError,
formatMediaFileSize,
getGallerySelectionOrder,
getMediaItemKind,
getMediaPickerPreviewUrl,
isMediaItemSelectableForTarget,
isUploadFileAllowedForMediaTarget,
resolveSelectedMediaItems,
resolveUploadFetchErrorMessage,
toggleMediaSelectionState
} from '../lib/markdown-editor-media.js'
import { buildDefaultUploadSizeLimits } from '../lib/upload-size-limit.js'
test('미디어 파일 크기를 표시 문자열로 변환한다', () => {
assert.equal(formatMediaFileSize(512), '512 B')
assert.equal(formatMediaFileSize(1536), '2 KB')
assert.equal(formatMediaFileSize(1024 * 1024 * 1.5), '1.5 MB')
assert.equal(formatMediaFileSize(0), '')
})
test('미디어 항목 kind가 없으면 확장자와 URL로 종류를 판별한다', () => {
assert.equal(getMediaItemKind({ url: '/uploads/a.webp' }), 'image')
assert.equal(getMediaItemKind({ url: '/uploads/a.mp4?x=1' }), 'video')
assert.equal(getMediaItemKind({ url: '/uploads/a.m4a' }), 'audio')
assert.equal(getMediaItemKind({ url: '/uploads/a.zip' }), 'file')
})
test('선택 대상별로 허용 미디어를 판별하고 파생 썸네일은 제외한다', () => {
assert.equal(isMediaItemSelectableForTarget({ url: '/uploads/posts/2026/07/a.png' }, 'image'), true)
assert.equal(isMediaItemSelectableForTarget({ url: '/uploads/posts/2026/07/a.mp4' }, 'image'), false)
assert.equal(isMediaItemSelectableForTarget({ url: '/uploads/posts/2026/07/a.mp4' }, 'video'), true)
assert.equal(isMediaItemSelectableForTarget({ url: '/uploads/posts/2026/07/thumbs/a-card.webp' }, 'image'), false)
assert.equal(isMediaItemSelectableForTarget({ url: '/uploads/posts/2026/07/thumbs/a-body-720.webp' }, 'gallery'), false)
})
test('미디어 선택 미리보기는 본문 파생 썸네일을 우선한다', () => {
assert.equal(getMediaPickerPreviewUrl({
url: '/uploads/posts/2026/07/a.png',
updatedAt: '2026-07-24T00:00:00.000Z',
size: 100,
bodyThumbnailStatus: {
hasThumbnail: true
}
}), '/uploads/posts/2026/07/thumbs/a-body-720.webp?v=2026-07-24T00%3A00%3A00.000Z-100')
})
test('업로드 파일은 선택 대상별 MIME 타입으로 허용 여부를 판별한다', () => {
assert.equal(isUploadFileAllowedForMediaTarget({ type: 'image/png' }, 'gallery'), true)
assert.equal(isUploadFileAllowedForMediaTarget({ type: 'video/mp4' }, 'video'), true)
assert.equal(isUploadFileAllowedForMediaTarget({ type: 'audio/mpeg' }, 'audio'), true)
assert.equal(isUploadFileAllowedForMediaTarget({ type: 'application/pdf' }, 'file'), true)
assert.equal(isUploadFileAllowedForMediaTarget({ type: 'image/png' }, 'file'), false)
})
test('미디어 항목으로 fenced 미디어 블록을 만든다', () => {
assert.deepEqual(createMediaBlockMarkdown('video', {
url: '/uploads/a.mp4',
title: '영상'
}), [':::video', 'url=/uploads/a.mp4', 'title=영상', 'caption=', ':::'])
assert.deepEqual(createMediaBlockMarkdown('file', {
url: '/uploads/a.pdf',
name: 'a.pdf',
size: 2048
}), [':::file', 'url=/uploads/a.pdf', 'title=a.pdf', 'description=', 'name=a.pdf', 'size=2 KB', ':::'])
})
test('갤러리 선택 순서를 선택 URL 순서로 반환한다', () => {
assert.equal(getGallerySelectionOrder(['/a.png', '/b.png'], '/b.png'), 2)
assert.equal(getGallerySelectionOrder(['/a.png'], '/b.png'), 0)
})
test('단일 미디어 선택은 기존 선택을 새 항목 하나로 교체한다', () => {
assert.deepEqual(toggleMediaSelectionState({
selectedUrls: ['/old.png'],
selectedItemMap: {
'/old.png': { url: '/old.png' }
},
mediaItem: { url: '/new.png', name: 'new' },
isGallery: false
}), {
selectedUrls: ['/new.png'],
selectedItemMap: {
'/new.png': { url: '/new.png', name: 'new' }
}
})
})
test('갤러리 미디어 선택은 선택 추가와 제거를 토글한다', () => {
const selected = toggleMediaSelectionState({
selectedUrls: ['/a.png'],
selectedItemMap: {
'/a.png': { url: '/a.png' }
},
mediaItem: { url: '/b.png' },
isGallery: true
})
assert.deepEqual(selected.selectedUrls, ['/a.png', '/b.png'])
assert.deepEqual(Object.keys(selected.selectedItemMap), ['/a.png', '/b.png'])
assert.deepEqual(toggleMediaSelectionState({
selectedUrls: selected.selectedUrls,
selectedItemMap: selected.selectedItemMap,
mediaItem: { url: '/a.png' },
isGallery: true
}), {
selectedUrls: ['/b.png'],
selectedItemMap: {
'/b.png': { url: '/b.png' }
}
})
})
test('선택된 URL 순서대로 미디어 항목을 해석한다', () => {
assert.deepEqual(resolveSelectedMediaItems(
['/a.png', '/b.png', '/missing.png'],
{
'/a.png': { url: '/a.png', source: 'map' }
},
[
{ url: '/b.png', source: 'list' }
]
), [
{ url: '/a.png', source: 'map' },
{ url: '/b.png', source: 'list' }
])
})
test('업로드 API 오류를 사용자 메시지로 변환한다', () => {
assert.equal(resolveUploadFetchErrorMessage({
data: {
message: ' 서버 오류 '
}
}), '서버 오류')
assert.equal(resolveUploadFetchErrorMessage({ statusCode: 413 }), '업로드 가능한 파일 크기를 초과했습니다.')
assert.equal(resolveUploadFetchErrorMessage(new Error('네트워크 오류')), '네트워크 오류')
assert.equal(resolveUploadFetchErrorMessage({}), '파일 업로드에 실패했습니다.')
})
test('업로드 파일 한도 초과 메시지를 계산한다', () => {
const uploadSizeLimits = buildDefaultUploadSizeLimits({
image: 100
})
assert.equal(findUploadSizeLimitError({
files: [
{ name: 'a.png', type: 'image/png', size: 10 },
{ name: 'b.png', type: 'image/png', size: 10 }
],
maxFileCount: 1,
maxTotalSize: 1000,
uploadSizeLimits
}), '한 번에 업로드할 수 있는 파일은 최대 1개입니다.')
assert.equal(findUploadSizeLimitError({
files: [
{ name: 'a.png', type: 'image/png', size: 60 },
{ name: 'b.png', type: 'image/png', size: 60 }
],
maxFileCount: 5,
maxTotalSize: 100,
uploadSizeLimits
}), '한 번에 업로드 가능한 전체 용량을 초과했습니다. (최대 100B)')
assert.equal(findUploadSizeLimitError({
files: [
{ name: 'a.png', type: 'image/png', size: 101 }
],
maxFileCount: 5,
maxTotalSize: 1000,
uploadSizeLimits
}), '파일 크기가 너무 큽니다. (최대 100B)')
assert.equal(findUploadSizeLimitError({
files: [
{ name: 'a.png', type: 'image/png', size: 100 }
],
maxFileCount: 5,
maxTotalSize: 1000,
uploadSizeLimits
}), null)
})