import { LogseqFormatter, ParsedBlock } from './logseq.formatter';
describe('LogseqFormatter', () => {
let formatter: LogseqFormatter;
beforeEach(() => {
formatter = new LogseqFormatter();
});
describe('parseToBlocks', () => {
it('should convert markdown headings to bold blocks', () => {
const content = '# 제목\n## 소제목';
const blocks = formatter.parseToBlocks(content);
expect(blocks).toHaveLength(2);
expect(blocks[0].content).toBe('**제목**');
expect(blocks[1].content).toBe('**소제목**');
});
it('should handle list items with indentation', () => {
const content = `제목
\t- 항목 1
\t- 항목 2
\t\t- 하위 항목`;
const blocks = formatter.parseToBlocks(content);
expect(blocks).toHaveLength(1);
expect(blocks[0].content).toBe('제목');
expect(blocks[0].children).toHaveLength(2);
expect(blocks[0].children![0].content).toBe('항목 1');
expect(blocks[0].children![1].content).toBe('항목 2');
expect(blocks[0].children![1].children).toHaveLength(1);
expect(blocks[0].children![1].children![0].content).toBe('하위 항목');
});
it('should convert checkboxes to DONE/TODO markers', () => {
const content = `[x] 완료된 작업
[ ] 미완료 작업`;
const blocks = formatter.parseToBlocks(content);
expect(blocks).toHaveLength(2);
expect(blocks[0].content).toBe('DONE 완료된 작업');
expect(blocks[1].content).toBe('TODO 미완료 작업');
});
it('should preserve Logseq properties (key:: value)', () => {
const content = `status:: done
priority:: high`;
const blocks = formatter.parseToBlocks(content);
expect(blocks).toHaveLength(2);
expect(blocks[0].content).toBe('status:: done');
expect(blocks[1].content).toBe('priority:: high');
});
it('should handle numbered lists as individual blocks', () => {
const content = `1. 첫 번째
2. 두 번째`;
const blocks = formatter.parseToBlocks(content);
// 번호 리스트는 각각 개별 최상위 블록
// (헤딩이 없으므로 섹션 없음)
expect(blocks).toHaveLength(2);
expect(blocks[0].content).toBe('첫 번째');
expect(blocks[1].content).toBe('두 번째');
});
it('should handle complex nested structure with tabs', () => {
// 탭 들여쓰기가 있는 볼드 텍스트는 별도 처리가 필요
// parseToBlocks는 헤딩/리스트에 최적화됨
// 이 케이스에서는 각 줄이 별도 블록으로 처리됨
const content = `# 📋 작업 요약
## 🔍 분석
- 항목 1
- 항목 2
## ✨ 구현
- DONE 완료된 작업`;
const blocks = formatter.parseToBlocks(content);
// 헤딩들은 최상위, 리스트는 바로 앞 헤딩의 자식
expect(blocks.length).toBeGreaterThanOrEqual(3);
expect(blocks[0].content).toBe('**📋 작업 요약**');
// 첫 번째 헤딩 다음 헤딩인 분석이 옴
expect(blocks[1].content).toBe('**🔍 분석**');
expect(blocks[1].children).toHaveLength(2);
expect(blocks[2].content).toBe('**✨ 구현**');
expect(blocks[2].children).toHaveLength(1);
});
// 🆕 Logseq 아웃라이너 호환성 테스트 - 헤딩 후 리스트
it('should make list items children of preceding heading', () => {
const content = `# 섹션 제목
- 항목 1
- 항목 2
- 항목 3`;
const blocks = formatter.parseToBlocks(content);
// 헤딩은 최상위 블록
expect(blocks).toHaveLength(1);
expect(blocks[0].content).toBe('**섹션 제목**');
// 리스트 항목들은 헤딩의 자식으로
expect(blocks[0].children).toHaveLength(3);
expect(blocks[0].children![0].content).toBe('항목 1');
expect(blocks[0].children![1].content).toBe('항목 2');
expect(blocks[0].children![2].content).toBe('항목 3');
});
// 🆕 여러 헤딩이 연속될 때 각각 개별 블록
it('should keep multiple headings as separate top-level blocks', () => {
const content = `## 🔴 필수 대응 사항
### 1. 첫 번째 항목
- 항목 내용
### 2. 두 번째 항목`;
const blocks = formatter.parseToBlocks(content);
// 3개의 헤딩이 최상위 블록, 리스트는 두 번째 헤딩의 자식
expect(blocks).toHaveLength(3);
expect(blocks[0].content).toBe('**🔴 필수 대응 사항**');
expect(blocks[1].content).toBe('**1. 첫 번째 항목**');
expect(blocks[1].children).toHaveLength(1);
expect(blocks[1].children![0].content).toBe('항목 내용');
expect(blocks[2].content).toBe('**2. 두 번째 항목**');
});
// 🆕 FE 협조사항 문서와 유사한 복잡한 구조 테스트
it('should handle FE request document style correctly', () => {
const content = `## 📋 개요
홈 피드 성능 최적화로 인해 FE에서 대응이 필요한 사항들입니다.
## 🔴 필수 대응 사항
### 1. Shorts 페이지네이션 구현 필요
- **변경 내용**: Shorts 조회 limit가 변경됨
- **영향**: 더보기/무한스크롤 시 Shorts가 50개까지만 표시됨
- **요청 사항**:
- [ ] 필터 적용 시 Shorts 무한스크롤 구현
- [ ] offset 파라미터 활용`;
const blocks = formatter.parseToBlocks(content);
// 최상위 블록: 개요, 필수 대응 사항, 1. Shorts...
expect(blocks.length).toBeGreaterThanOrEqual(3);
expect(blocks[0].content).toBe('**📋 개요**');
expect(blocks[0].children).toBeDefined();
expect(blocks[0].children![0].content).toContain('홈 피드 성능 최적화');
});
});
describe('optimizeForOutliner', () => {
it('should remove empty blocks and structure content', () => {
const content = `제목
내용`;
const blocks: ParsedBlock[] = formatter.optimizeForOutliner(content);
// 빈 줄로 구분되어도 일반 텍스트는 첫 번째가 섹션이 됨
// 현재 구현에서는 "내용"이 "제목"의 자식이 됨
expect(blocks).toHaveLength(1);
expect(blocks[0].content).toBe('제목');
expect(blocks[0].children).toBeDefined();
expect(blocks[0].children![0].content).toBe('내용');
});
it('should handle headings with content separately', () => {
const content = `# 제목
# 다른 제목`;
const blocks: ParsedBlock[] = formatter.optimizeForOutliner(content);
// 헤딩들은 각각 최상위 블록
expect(blocks).toHaveLength(2);
expect(blocks[0].content).toBe('**제목**');
expect(blocks[1].content).toBe('**다른 제목**');
});
});
describe('convertMarkdownTable', () => {
it('should convert table to hierarchical blocks', () => {
const table = `| 이름 | 상태 | 우선순위 |
|---|---|---|
| 작업1 | 완료 | 높음 |
| 작업2 | 진행중 | 중간 |`;
const blocks: ParsedBlock[] = formatter.convertMarkdownTable(table);
expect(blocks.length).toBeGreaterThan(0);
// 첫 번째는 테이블 제목
expect(blocks[0].content).toContain('📊');
});
});
describe('parseComplexDocument', () => {
it('should handle code blocks', () => {
const content = `설명
\`\`\`typescript
const x = 1;
\`\`\`
다음 내용`;
const blocks: ParsedBlock[] = formatter.parseComplexDocument(content);
expect(blocks.length).toBeGreaterThanOrEqual(2);
// 코드 블록이 하나의 블록으로 유지되는지 확인
const codeBlock = blocks.find((b: ParsedBlock) =>
b.content.includes('```'),
);
expect(codeBlock).toBeDefined();
expect(codeBlock!.content).toContain('const x = 1');
});
// 🆕 parseComplexDocument도 헤딩-리스트 구조 올바르게 처리
it('should make list items children of headings in complex documents', () => {
const content = `## 작업 목록
- 항목 1
- 항목 2
## 완료된 작업
- 완료 1`;
const blocks: ParsedBlock[] = formatter.parseComplexDocument(content);
expect(blocks.length).toBe(2); // 두 개의 섹션
expect(blocks[0].content).toBe('**작업 목록**');
expect(blocks[0].children).toHaveLength(2);
expect(blocks[1].content).toBe('**완료된 작업**');
expect(blocks[1].children).toHaveLength(1);
});
});
describe('analyzeLineType', () => {
it('should correctly identify line types', () => {
// private 메서드 테스트를 위해 parseToBlocks의 결과로 간접 확인
const headingContent = '## 헤딩';
const listContent = '- 리스트';
const checkboxContent = '[ ] 체크박스';
const propertyContent = 'key:: value';
const textContent = '일반 텍스트';
const headingBlocks = formatter.parseToBlocks(headingContent);
expect(headingBlocks[0].content).toBe('**헤딩**');
const listBlocks = formatter.parseToBlocks(listContent);
expect(listBlocks[0].content).toBe('리스트');
const checkboxBlocks = formatter.parseToBlocks(checkboxContent);
expect(checkboxBlocks[0].content).toBe('TODO 체크박스');
const propertyBlocks = formatter.parseToBlocks(propertyContent);
expect(propertyBlocks[0].content).toBe('key:: value');
const textBlocks = formatter.parseToBlocks(textContent);
expect(textBlocks[0].content).toBe('일반 텍스트');
});
});
/**
* 🆕 Logseq 호환성 검증 테스트
*
* Context7 Logseq 문서 근거:
* - "Full content is not displayed, Logseq doesn't support multiple unordered lists or headings in a block"
* - DB 버전 문서: "A block with multiple simple queries, multiple advanced queries,
* multiple embeds or multiple quotes only imports the first of these"
*
* 이 테스트들은 하나의 블록에 여러 리스트/헤딩이 포함되는 것을 방지하는 기능을 검증합니다.
*/
describe('validateBlockContent', () => {
it('should validate single-line content as valid', () => {
const result = formatter.validateBlockContent('단일 줄 내용');
expect(result.isValid).toBe(true);
expect(result.issues).toHaveLength(0);
});
it('should detect multiple unordered lists in a single block', () => {
// 이 경고 발생 케이스: "Logseq doesn't support multiple unordered lists"
const content = `- 항목 1
- 항목 2
- 항목 3`;
const result = formatter.validateBlockContent(content);
expect(result.isValid).toBe(false);
expect(result.issues.length).toBeGreaterThan(0);
expect(result.problematicPatterns).toContainEqual(
expect.objectContaining({ type: 'multiple-lists' }),
);
});
it('should detect multiple headings in a single block', () => {
// 이 경고 발생 케이스: "Logseq doesn't support multiple headings in a block"
const content = `## 첫 번째 헤딩
## 두 번째 헤딩`;
const result = formatter.validateBlockContent(content);
expect(result.isValid).toBe(false);
expect(result.problematicPatterns).toContainEqual(
expect.objectContaining({ type: 'multiple-headings' }),
);
});
it('should detect mixed heading and list content', () => {
const content = `## 헤딩
- 리스트 항목`;
const result = formatter.validateBlockContent(content);
expect(result.isValid).toBe(false);
expect(result.problematicPatterns).toContainEqual(
expect.objectContaining({ type: 'mixed-content' }),
);
});
it('should allow numbered lists with different markers', () => {
const content = `1. 첫 번째
2. 두 번째`;
const result = formatter.validateBlockContent(content);
// 번호 리스트도 여러 개면 문제
expect(result.isValid).toBe(false);
});
it('should validate empty content as valid', () => {
const result = formatter.validateBlockContent('');
expect(result.isValid).toBe(true);
});
it('should validate content with properties as valid', () => {
// 속성은 리스트 마커가 아님
const content = `status:: done
priority:: high`;
const result = formatter.validateBlockContent(content);
// 속성은 리스트가 아니므로 유효
expect(result.isValid).toBe(true);
});
});
describe('safeParseToBlocks', () => {
it('should automatically split invalid blocks with multiple lists', () => {
// 사용자가 여러 리스트 아이템을 한 번에 입력했을 때 자동 분리
const content = `- 항목 1
- 항목 2
- 항목 3`;
const blocks = formatter.safeParseToBlocks(content);
// 각 항목이 개별 블록으로 분리되어야 함
expect(blocks.length).toBe(3);
expect(blocks[0].content).toBe('항목 1');
expect(blocks[1].content).toBe('항목 2');
expect(blocks[2].content).toBe('항목 3');
});
it('should preserve valid block structure', () => {
const content = `## 제목
- 항목 1
- 항목 2`;
const blocks = formatter.safeParseToBlocks(content);
// 헤딩이 최상위, 리스트가 자식으로 - 이건 유효한 구조
expect(blocks.length).toBe(1);
expect(blocks[0].content).toBe('**제목**');
expect(blocks[0].children).toHaveLength(2);
});
it('should handle complex nested structures safely', () => {
const content = `## 작업 목록
- 작업 1
- 하위 작업 1-1
- 하위 작업 1-2
- 작업 2`;
const blocks = formatter.safeParseToBlocks(content);
// 구조가 유지되면서 각 블록은 Logseq 호환
expect(blocks.length).toBe(1);
expect(blocks[0].children).toBeDefined();
});
it('should split blocks containing multiple headings', () => {
const content = `## 첫 번째 섹션
## 두 번째 섹션`;
const blocks = formatter.safeParseToBlocks(content);
// 각 헤딩이 개별 블록
expect(blocks.length).toBe(2);
expect(blocks[0].content).toBe('**첫 번째 섹션**');
expect(blocks[1].content).toBe('**두 번째 섹션**');
});
});
describe('validateAndFixBlocks', () => {
it('should recursively validate and fix child blocks', () => {
const blocks: ParsedBlock[] = [
{
content: '**제목**',
children: [
{
content: `- 항목 1
- 항목 2`,
},
],
},
];
const fixed = formatter.validateAndFixBlocks(blocks);
// 부모는 유지되고 자식 중 잘못된 것은 수정됨
expect(fixed.length).toBe(1);
expect(fixed[0].content).toBe('**제목**');
// 자식은 분리되어야 함
expect(fixed[0].children!.length).toBeGreaterThanOrEqual(2);
});
});
});