99 lines
4 KiB
TypeScript
99 lines
4 KiB
TypeScript
// 회의 내보내기 마크다운 렌더러(core) 회귀 테스트.
|
|
// 예전 정규식 변환은 이스케이프가 없어 `List<String>`·`<TBD>` 가 PDF에서 사라졌고,
|
|
// 한 줄짜리 줄들이 한 문단으로 붙었으며, 표가 파이프 그대로 찍혔다.
|
|
import { describe, expect, it } from 'vitest'
|
|
import { Paragraph, Table } from 'docx'
|
|
import {
|
|
escapeHtml,
|
|
markdownToSimpleHtml,
|
|
parseMarkdownBlocks,
|
|
renderMeetingPdfHtml,
|
|
} from '../src/utils/meeting-markdown'
|
|
import { blocksToDocxElements, markdownToDocx } from '../src/utils/markdown-to-docx'
|
|
|
|
describe('parseMarkdownBlocks', () => {
|
|
it('제목·불릿·체크박스(두 상태)·표·문단·구분선을 나눈다', () => {
|
|
const md = [
|
|
'## 결정',
|
|
'- 타입을 List<String> 으로 변경',
|
|
'- [ ] 문서화',
|
|
'- [x] 배포',
|
|
'',
|
|
'담당: <TBD>',
|
|
'줄1',
|
|
'---',
|
|
'| 시간 | 내용 |',
|
|
'|---|---|',
|
|
'| 00:01 | 시작 |',
|
|
].join('\n')
|
|
expect(parseMarkdownBlocks(md)).toEqual([
|
|
{ type: 'heading', level: 2, text: '결정' },
|
|
{ type: 'bullet', text: '타입을 List<String> 으로 변경' },
|
|
{ type: 'checkbox', checked: false, text: '문서화' },
|
|
{ type: 'checkbox', checked: true, text: '배포' },
|
|
{ type: 'paragraph', lines: ['담당: <TBD>', '줄1'] },
|
|
{ type: 'rule' },
|
|
{ type: 'table', header: ['시간', '내용'], rows: [['00:01', '시작']] },
|
|
])
|
|
})
|
|
|
|
it('빈 입력은 빈 목록', () => {
|
|
expect(parseMarkdownBlocks('')).toEqual([])
|
|
expect(parseMarkdownBlocks(null)).toEqual([])
|
|
})
|
|
})
|
|
|
|
describe('markdownToSimpleHtml', () => {
|
|
it('꺾쇠·앰퍼샌드·따옴표를 이스케이프해 PDF 에서 사라지지 않게 한다', () => {
|
|
const html = markdownToSimpleHtml('- List<String> & "q" <script>alert(1)</script>')
|
|
expect(html).toContain('List<String> & "q" <script>')
|
|
expect(html).not.toContain('<script>')
|
|
})
|
|
|
|
it('연속된 한 줄짜리 줄은 <br> 로 보존하고, 불릿은 <ul> 로 감싼다', () => {
|
|
expect(markdownToSimpleHtml('줄1\n줄2')).toBe('<p>줄1<br>줄2</p>')
|
|
expect(markdownToSimpleHtml('- a\n- [x] b')).toBe('<ul>\n<li>a</li>\n<li>☑ b</li>\n</ul>')
|
|
})
|
|
|
|
it('파이프 표를 <table> 로 그린다', () => {
|
|
const html = markdownToSimpleHtml('| 시간 | 내용 |\n|------|------|\n| 00:01 | <시작> |')
|
|
expect(html).toBe(
|
|
'<table><thead><tr><th>시간</th><th>내용</th></tr></thead><tbody><tr><td>00:01</td><td><시작></td></tr></tbody></table>',
|
|
)
|
|
})
|
|
|
|
it('**굵게** 는 이스케이프 뒤 <strong> 으로 바꾼다', () => {
|
|
expect(markdownToSimpleHtml('**중요** <b>')).toBe('<p><strong>중요</strong> <b></p>')
|
|
})
|
|
})
|
|
|
|
describe('renderMeetingPdfHtml', () => {
|
|
it('제목과 메타를 이스케이프하고 본문이 없으면 안내 문구를 쓴다', () => {
|
|
const html = renderMeetingPdfHtml({
|
|
title: '<img src=x onerror=alert(1)>',
|
|
meta: '일시: A & B',
|
|
markdown: null,
|
|
emptyMessage: '회의록이 생성되지 않았습니다.',
|
|
})
|
|
expect(html).toContain(`<h1>${escapeHtml('<img src=x onerror=alert(1)>')}</h1>`)
|
|
expect(html).not.toContain('<img')
|
|
expect(html).toContain('<p class="meta">일시: A & B</p>')
|
|
expect(html).toContain('<p>회의록이 생성되지 않았습니다.</p>')
|
|
})
|
|
})
|
|
|
|
describe('markdown-to-docx', () => {
|
|
it('블록을 문단·표로 매핑한다 (문단 줄마다 하나)', () => {
|
|
const elements = blocksToDocxElements(
|
|
parseMarkdownBlocks('# 제목\n- 항목\n- [x] 완료\n줄1\n줄2\n\n| a | b |\n|---|---|\n| 1 | 2 |'),
|
|
)
|
|
expect(elements.map((e) => (e instanceof Table ? 'table' : e instanceof Paragraph ? 'p' : '?'))).toEqual([
|
|
'p', 'p', 'p', 'p', 'p', 'table',
|
|
])
|
|
})
|
|
|
|
it('DOCX 버퍼를 만든다', async () => {
|
|
const buffer = await markdownToDocx('## 결정\n- A', '회의록')
|
|
expect(buffer.subarray(0, 2).toString('latin1')).toBe('PK')
|
|
})
|
|
})
|