import { describe, it, expect, beforeEach } from 'vitest'; import { StructureValidator } from '../../../../src/domain/text/validators/StructureValidator.js'; import { MarkdownParser } from '../../../../src/core/parser.js'; import type { ValidationContext, DocumentMetadata } from '../../../../src/core/types.js'; describe('StructureValidator', () => { let validator: StructureValidator; let parser: MarkdownParser; beforeEach(() => { validator = new StructureValidator(); parser = new MarkdownParser(); }); function createContext(content: string): ValidationContext { const tree = parser.parse(content); const frontmatter = parser.extractFrontmatter(content); const metadata: DocumentMetadata = { filePath: '/test/file.md', title: typeof frontmatter.title === 'string' ? frontmatter.title : undefined, frontmatter, }; return { tree, content, metadata }; } describe('Validatorインターフェース', () => { it('正しいIDを持つ', () => { expect(validator.id).toBe('text/structure'); }); it('正しい名前を持つ', () => { expect(validator.name).toBe('文書構造検証'); }); }); describe('### 学習目標 チェック', () => { it('学習目標セクションがない場合、エラーを返す', () => { const content = `--- title: テスト draft: false --- {{ toc }} ## 概要 本文です。 ## まとめ まとめです。`; const context = createContext(content); const results = validator.validate(context); const goalsError = results.find(r => r.ruleId === 'text/structure/missing-learning-goals'); expect(goalsError).toBeDefined(); expect(goalsError?.severity).toBe('error'); }); it('学習目標セクションがある場合、エラーを返さない', () => { const content = `--- title: テスト draft: false --- {{ toc }} ### 学習目標 この章では... ## まとめ まとめです。`; const context = createContext(content); const results = validator.validate(context); const goalsError = results.find(r => r.ruleId === 'text/structure/missing-learning-goals'); expect(goalsError).toBeUndefined(); }); }); describe('## まとめ チェック', () => { it('まとめセクションがない場合、エラーを返す', () => { const content = `--- title: テスト draft: false --- {{ toc }} ### 学習目標 この章では... ## 本文 本文です。`; const context = createContext(content); const results = validator.validate(context); const summaryError = results.find(r => r.ruleId === 'text/structure/missing-summary'); expect(summaryError).toBeDefined(); expect(summaryError?.severity).toBe('error'); }); it('まとめセクションがある場合、エラーを返さない', () => { const content = `--- title: テスト draft: false --- {{ toc }} ### 学習目標 この章では... ## まとめ まとめです。`; const context = createContext(content); const results = validator.validate(context); const summaryError = results.find(r => r.ruleId === 'text/structure/missing-summary'); expect(summaryError).toBeUndefined(); }); }); describe('まとめが最後のH2かチェック', () => { it('まとめの後にH2がある場合、警告を返す', () => { const content = `--- title: テスト draft: false --- {{ toc }} ### 学習目標 この章では... ## まとめ まとめです。 ## 追加セクション 追加内容です。`; const context = createContext(content); const results = validator.validate(context); const notLastError = results.find(r => r.ruleId === 'text/structure/summary-not-last'); expect(notLastError).toBeDefined(); expect(notLastError?.severity).toBe('warning'); }); it('まとめが最後のH2の場合、警告を返さない', () => { const content = `--- title: テスト draft: false --- {{ toc }} ### 学習目標 この章では... ## 本文 本文です。 ## まとめ まとめです。`; const context = createContext(content); const results = validator.validate(context); const notLastError = results.find(r => r.ruleId === 'text/structure/summary-not-last'); expect(notLastError).toBeUndefined(); }); }); describe('セクション順序チェック', () => { it('学習目標がまとめより後にある場合、エラーを返す', () => { const content = `--- title: テスト draft: false --- {{ toc }} ## まとめ まとめです。 ### 学習目標 この章では...`; const context = createContext(content); const results = validator.validate(context); const orderError = results.find(r => r.ruleId === 'text/structure/invalid-order'); expect(orderError).toBeDefined(); expect(orderError?.severity).toBe('error'); }); }); describe('正常ケース', () => { it('正しい構造の場合、エラーを返さない', () => { const content = `--- title: 正しいテキスト教材 draft: false --- {{ toc }} ## はじめに この章では以下を学びます。 ### 学習目標 この章の学習目標です。 :::note この章で学ぶこと - 項目1 - 項目2 ::: ## 学習項目1 学習内容です。 ## 学習項目2 学習内容です。 ## まとめ この章で学んだこと: - まとめ1 - まとめ2`; const context = createContext(content); const results = validator.validate(context); expect(results.length).toBe(0); }); }); describe('## はじめに チェック', () => { it('{{ toc }}の直後に「## はじめに」がない場合、エラーを返す', () => { const content = `--- title: テスト draft: false --- {{ toc }} これはテキストの概要です。 ### 学習目標 この章では... ## まとめ まとめです。`; const context = createContext(content); const results = validator.validate(context); const introError = results.find(r => r.ruleId === 'text/structure/missing-introduction'); expect(introError).toBeDefined(); expect(introError?.severity).toBe('error'); expect(introError?.message).toContain('はじめに'); }); it('{{ toc }}の直後に「## はじめに」がある場合、エラーを返さない', () => { const content = `--- title: テスト draft: false --- {{ toc }} ## はじめに これはテキストの概要です。 ### 学習目標 この章では... ## まとめ まとめです。`; const context = createContext(content); const results = validator.validate(context); const introError = results.find(r => r.ruleId === 'text/structure/missing-introduction'); expect(introError).toBeUndefined(); }); it('{{ toc }}がない場合でも「## はじめに」チェックはスキップする', () => { const content = `--- title: テスト draft: false --- ## はじめに これはテキストの概要です。 ### 学習目標 この章では... ## まとめ まとめです。`; const context = createContext(content); const results = validator.validate(context); // {{ toc }}がない場合はこのルールはチェックしない const introError = results.find(r => r.ruleId === 'text/structure/missing-introduction'); expect(introError).toBeUndefined(); }); it('{{ toc }}と「## はじめに」の間に他のH2がある場合、エラーを返す', () => { const content = `--- title: テスト draft: false --- {{ toc }} ## 別のセクション 内容 ## はじめに これはテキストの概要です。 ### 学習目標 この章では... ## まとめ まとめです。`; const context = createContext(content); const results = validator.validate(context); const introError = results.find(r => r.ruleId === 'text/structure/missing-introduction'); expect(introError).toBeDefined(); }); it('「## はじめに」のエラーに修正方法(suggestion)を含む', () => { const content = `--- title: テスト draft: false --- {{ toc }} これはテキストの概要です。 ### 学習目標 この章では... ## まとめ まとめです。`; const context = createContext(content); const results = validator.validate(context); const introError = results.find(r => r.ruleId === 'text/structure/missing-introduction'); expect(introError?.suggestion).toBeDefined(); expect(introError?.suggestion).toContain('## はじめに'); }); }); describe('H1見出し禁止チェック', () => { it('H1見出しがある場合、エラーを返す', () => { const content = `--- title: 第1章:Next.jsの概要とモダンWeb開発 draft: false --- {{ toc }} # 第1章:Next.jsの概要とモダンWeb開発 ## はじめに Next.jsは、ReactをベースにしたモダンなWebアプリケーションフレームワークです。 ### 学習目標 この章では... ## まとめ まとめです。`; const context = createContext(content); const results = validator.validate(context); const h1Error = results.find(r => r.ruleId === 'text/structure/h1-not-allowed'); expect(h1Error).toBeDefined(); expect(h1Error?.severity).toBe('error'); expect(h1Error?.message).toContain('H1見出し'); }); it('H1見出しがない場合、エラーを返さない', () => { const content = `--- title: 第1章:Next.jsの概要とモダンWeb開発 draft: false --- {{ toc }} ## はじめに Next.jsは、ReactをベースにしたモダンなWebアプリケーションフレームワークです。 ### 学習目標 この章では... ## まとめ まとめです。`; const context = createContext(content); const results = validator.validate(context); const h1Error = results.find(r => r.ruleId === 'text/structure/h1-not-allowed'); expect(h1Error).toBeUndefined(); }); it('複数のH1見出しがある場合、それぞれにエラーを返す', () => { const content = `--- title: テスト draft: false --- {{ toc }} # 見出し1 ## はじめに 内容 # 見出し2 ## まとめ まとめです。 ### 学習目標 この章では...`; const context = createContext(content); const results = validator.validate(context); const h1Errors = results.filter(r => r.ruleId === 'text/structure/h1-not-allowed'); expect(h1Errors.length).toBe(2); }); it('H1見出しエラーに位置情報を含む', () => { const content = `--- title: テスト draft: false --- {{ toc }} # 不要な見出し ## はじめに 内容 ### 学習目標 この章では... ## まとめ まとめです。`; const context = createContext(content); const results = validator.validate(context); const h1Error = results.find(r => r.ruleId === 'text/structure/h1-not-allowed'); expect(h1Error?.location).toBeDefined(); expect(h1Error?.location?.line).toBeGreaterThan(0); }); it('H1見出しエラーに修正方法(suggestion)を含む', () => { const content = `--- title: テスト draft: false --- {{ toc }} # 不要な見出し ## はじめに 内容 ### 学習目標 この章では... ## まとめ まとめです。`; const context = createContext(content); const results = validator.validate(context); const h1Error = results.find(r => r.ruleId === 'text/structure/h1-not-allowed'); expect(h1Error?.suggestion).toBeDefined(); expect(h1Error?.suggestion).toContain('削除'); }); }); describe('エラーメッセージの品質', () => { it('学習目標エラーに修正方法(suggestion)を含む', () => { const content = `--- title: テスト draft: false --- {{ toc }} ## まとめ まとめです。`; const context = createContext(content); const results = validator.validate(context); const goalsError = results.find(r => r.ruleId === 'text/structure/missing-learning-goals'); expect(goalsError?.suggestion).toBeDefined(); expect(goalsError?.suggestion).toContain('### 学習目標'); }); it('まとめエラーに修正方法(suggestion)を含む', () => { const content = `--- title: テスト draft: false --- {{ toc }} ### 学習目標 この章では...`; const context = createContext(content); const results = validator.validate(context); const summaryError = results.find(r => r.ruleId === 'text/structure/missing-summary'); expect(summaryError?.suggestion).toBeDefined(); expect(summaryError?.suggestion).toContain('## まとめ'); }); }); });