import { describe, it, expect } from 'vitest'; import { CodeBlockValidator } from '../../../../src/domain/text/validators/CodeBlockValidator.js'; import { MarkdownParser } from '../../../../src/core/parser.js'; import type { ValidationContext } from '../../../../src/core/types.js'; describe('CodeBlockValidator', () => { const validator = new CodeBlockValidator(); const parser = new MarkdownParser(); function createContext(content: string): ValidationContext { return { tree: parser.parse(content), content, metadata: { filePath: '/test/sample.md', frontmatter: {}, }, }; } describe('基本プロパティ', () => { it('idが正しく設定されている', () => { expect(validator.id).toBe('text/code-block'); }); it('nameが正しく設定されている', () => { expect(validator.name).toBe('コードブロック検証'); }); }); describe('言語指定チェック', () => { it('言語指定がないコードブロックでwarningを返す', () => { const content = `# Test \`\`\` console.log("Hello"); \`\`\` `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/code-block/missing-language')).toBe(true); const result = results.find(r => r.ruleId === 'text/code-block/missing-language'); expect(result?.severity).toBe('warning'); expect(result?.message).toContain('言語指定'); }); it('言語指定があるコードブロックではwarningを返さない', () => { const content = `# Test \`\`\`typescript console.log("Hello"); \`\`\` `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/code-block/missing-language')).toBe(false); }); }); describe('プログラムコードのファイルパスチェック(step内)', () => { it('step内でプログラムコードブロックの前にファイルパスがない場合warningを返す', () => { const content = `# Test :::step 1. ステップ 説明文 \`\`\`typescript console.log("Hello"); \`\`\` ::: `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/code-block/missing-filepath')).toBe(true); const result = results.find(r => r.ruleId === 'text/code-block/missing-filepath'); expect(result?.severity).toBe('warning'); expect(result?.message).toContain('ファイルパス'); }); it('step内で_path/to/file_ 形式でファイルパスがある場合はwarningを返さない', () => { const content = `# Test :::step 1. ステップ 説明文 _src/index.ts_ \`\`\`typescript console.log("Hello"); \`\`\` ::: `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/code-block/missing-filepath')).toBe(false); }); it('step内でファイルパスがイタリック形式(_path_)でない場合はwarningを返す', () => { const content = `# Test :::step 1. ステップ 説明文 src/index.ts \`\`\`typescript console.log("Hello"); \`\`\` ::: `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/code-block/missing-filepath')).toBe(true); }); it('step内でファイルパスが説明文内に部分的に含まれている場合はwarningを返さない', () => { const content = `# Test :::step 1. **従来のReactアプリケーションの動作確認** Create React Appで作成したアプリケーションのソースコードを確認してみましょう。 _Create React Appで生成されたsrc/index.html_ \`\`\`html //highlightstart //highlightend //highlightstart
//highlightend \`\`\` ::: `; const context = createContext(content); const results = validator.validate(context); // ファイルパス(src/index.html)が説明文内に含まれているのでwarningを返さない expect(results.some(r => r.ruleId === 'text/code-block/missing-filepath')).toBe(false); }); it('step内でファイルパスのみが部分的に含まれていればwarningを返さない(例: package.json)', () => { const content = `# Test :::step 1. ステップ _設定ファイル package.json の編集_ \`\`\`json //addstart {"name": "my-app"} //addend \`\`\` ::: `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/code-block/missing-filepath')).toBe(false); }); it('step内で相対パス形式のファイルパスが含まれていればwarningを返さない', () => { const content = `# Test :::step 1. ステップ _./src/components/App.tsx を編集_ \`\`\`typescript //addstart export const App = () =>
Hello
; //addend \`\`\` ::: `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/code-block/missing-filepath')).toBe(false); }); it('step内で拡張子を含まないパスのみの場合はwarningを返す', () => { const content = `# Test :::step 1. ステップ _src/componentsディレクトリを作成_ \`\`\`typescript console.log("Hello"); \`\`\` ::: `; const context = createContext(content); const results = validator.validate(context); // 拡張子がない場合はファイルパスとして認識しない expect(results.some(r => r.ruleId === 'text/code-block/missing-filepath')).toBe(true); }); }); describe('bashコマンドのタイトルチェック(step内)', () => { it('step内でbashコードブロックの前に「コマンド実行」がない場合warningを返す', () => { const content = `# Test :::step 1. ステップ 説明文 \`\`\`bash npm install \`\`\` ::: `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/code-block/missing-command-title')).toBe(true); const result = results.find(r => r.ruleId === 'text/code-block/missing-command-title'); expect(result?.severity).toBe('warning'); expect(result?.message).toContain('コマンド実行'); }); it('step内で_コマンド実行_ がある場合はwarningを返さない', () => { const content = `# Test :::step 1. ステップ 説明文 _コマンド実行_ \`\`\`bash npm install \`\`\` ::: `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/code-block/missing-command-title')).toBe(false); }); it('step内で「コマンド実行」を含む文章がある場合はwarningを返さない(部分一致)', () => { const content = `# Test :::step 1. ステップ ターミナルで以下のコマンド実行してください。 \`\`\`bash npm install \`\`\` ::: `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/code-block/missing-command-title')).toBe(false); }); it('step内で「以下のコマンドを実行」を含む文章がある場合はwarningを返さない', () => { const content = `# Test :::step 1. ステップ 以下のコマンドを実行してください。 \`\`\`bash npm install \`\`\` ::: `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/code-block/missing-command-title')).toBe(false); }); it('step内でshコードブロックも同様にチェックする', () => { const content = `# Test :::step 1. ステップ 説明文 \`\`\`sh npm install \`\`\` ::: `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/code-block/missing-command-title')).toBe(true); }); it('step内でshellコードブロックも同様にチェックする', () => { const content = `# Test :::step 1. ステップ 説明文 \`\`\`shell npm install \`\`\` ::: `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/code-block/missing-command-title')).toBe(true); }); }); describe('複数のコードブロック(step内)', () => { it('step内で複数のコードブロックをそれぞれチェックする', () => { const content = `# Test :::step 1. ステップ 説明文 _src/index.ts_ \`\`\`typescript console.log("Hello"); \`\`\` \`\`\`javascript console.log("World"); \`\`\` ::: `; const context = createContext(content); const results = validator.validate(context); // 2番目のコードブロックにはファイルパスがない expect(results.some(r => r.ruleId === 'text/code-block/missing-filepath')).toBe(true); }); it('step内で全てのコードブロックに適切なタイトルとハイライトがある場合はwarningを返さない', () => { const content = `# Test :::step 1. ステップ 説明文 _src/index.ts_ \`\`\`typescript //addstart console.log("Hello"); //addend \`\`\` _コマンド実行_ \`\`\`bash npm install \`\`\` ::: `; const context = createContext(content); const results = validator.validate(context); expect(results.filter(r => r.ruleId.startsWith('text/code-block/')).length).toBe(0); }); }); describe('特殊なケース', () => { it('text言語のコードブロックはファイルパスチェックをスキップする', () => { const content = `# Test \`\`\`text これはテキストです \`\`\` `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/code-block/missing-filepath')).toBe(false); expect(results.some(r => r.ruleId === 'text/code-block/missing-command-title')).toBe(false); }); it('plaintext言語のコードブロックはファイルパスチェックをスキップする', () => { const content = `# Test \`\`\`plaintext これはテキストです \`\`\` `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/code-block/missing-filepath')).toBe(false); }); it('diff言語のコードブロックはファイルパスチェックをスキップする', () => { const content = `# Test \`\`\`diff - old + new \`\`\` `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/code-block/missing-filepath')).toBe(false); }); it('step内でjson言語のコードブロックはファイルパスをチェックする', () => { const content = `# Test :::step 1. ステップ 説明文 \`\`\`json {"key": "value"} \`\`\` ::: `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/code-block/missing-filepath')).toBe(true); }); }); describe('位置情報(step内)', () => { it('step内でエラー結果に正確な行番号が含まれる', () => { const content = `# Test :::step 1. ステップ 説明文 \`\`\`typescript console.log("Hello"); \`\`\` ::: `; const context = createContext(content); const results = validator.validate(context); const result = results.find(r => r.ruleId === 'text/code-block/missing-filepath'); // コードブロックは9行目から始まる(:::stepが3行目、空行2つ、番号1行、空行、説明1行、空行、コードブロック) expect(result?.location?.line).toBe(9); }); }); describe('コードハイライトチェック(step内)', () => { describe('ハイライトマーカーがある場合', () => { it('step内で//addstart〜//addendがあるコードブロックはwarningを返さない', () => { const content = `# Test :::step 1. ステップ 説明文 _src/index.html_ \`\`\`html //addstart

Hello

//addend \`\`\` ::: `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/code-block/missing-highlight')).toBe(false); }); it('step内で//delstart〜//delendがあるコードブロックはwarningを返さない', () => { const content = `# Test :::step 1. ステップ 説明文 _docker-compose.yml_ \`\`\`yaml //delstart services: meilisearch: image: getmeili/meilisearch:v1.9.1 //delend \`\`\` ::: `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/code-block/missing-highlight')).toBe(false); }); it('step内で//highlightstart〜//highlightendがあるコードブロックはwarningを返さない', () => { const content = `# Test :::step 1. ステップ 説明文 _docker-compose.yml_ \`\`\`yaml //highlightstart services: meilisearch: image: getmeili/meilisearch:v1.9.1 //highlightend \`\`\` ::: `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/code-block/missing-highlight')).toBe(false); }); it('step内で//errorstart〜//errorendがあるコードブロックはwarningを返さない', () => { const content = `# Test :::step 1. ステップ 説明文 _docker-compose.yml_ \`\`\`yaml services: meilisearch: //errorstart image: 12345 //errorend \`\`\` ::: `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/code-block/missing-highlight')).toBe(false); }); it('step内で//warningstart〜//warningendがあるコードブロックはwarningを返さない', () => { const content = `# Test :::step 1. ステップ 説明文 _docker-compose.yml_ \`\`\`yaml services: meilisearch: //warningstart restart: always //warningend \`\`\` ::: `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/code-block/missing-highlight')).toBe(false); }); }); describe('ハイライトマーカーがない場合', () => { it('step内でハイライトマーカーがないコードブロックはwarningを返す', () => { const content = `# Test :::step 1. ステップ 説明文 _src/index.ts_ \`\`\`typescript console.log("Hello"); \`\`\` ::: `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/code-block/missing-highlight')).toBe(true); const result = results.find(r => r.ruleId === 'text/code-block/missing-highlight'); expect(result?.severity).toBe('warning'); expect(result?.message).toContain('ハイライト'); }); it('step内でbashコマンドブロックはハイライトチェックをスキップする', () => { const content = `# Test :::step 1. ステップ 説明文 _コマンド実行_ \`\`\`bash npm install \`\`\` ::: `; const context = createContext(content); const results = validator.validate(context); // コマンドブロックはハイライトチェック対象外 expect(results.some(r => r.ruleId === 'text/code-block/missing-highlight')).toBe(false); }); }); describe('ハイライトチェックをスキップするケース', () => { it('step内で言語指定がないコードブロックはハイライトチェックをスキップする', () => { const content = `# Test :::step 1. ステップ 説明文 \`\`\` console.log("Hello"); \`\`\` ::: `; const context = createContext(content); const results = validator.validate(context); // 言語指定がないことのwarningは出るが、ハイライトチェックは出ない expect(results.some(r => r.ruleId === 'text/code-block/missing-language')).toBe(true); expect(results.some(r => r.ruleId === 'text/code-block/missing-highlight')).toBe(false); }); it('text言語のコードブロックはハイライトチェックをスキップする', () => { const content = `# Test \`\`\`text これはテキストです \`\`\` `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/code-block/missing-highlight')).toBe(false); }); it('plaintext言語のコードブロックはハイライトチェックをスキップする', () => { const content = `# Test \`\`\`plaintext これはテキストです \`\`\` `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/code-block/missing-highlight')).toBe(false); }); it('diff言語のコードブロックはハイライトチェックをスキップする', () => { const content = `# Test \`\`\`diff - old + new \`\`\` `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/code-block/missing-highlight')).toBe(false); }); }); describe('複数のハイライトマーカー', () => { it('step内で複数のハイライトマーカーがある場合もwarningを返さない', () => { const content = `# Test :::step 1. ステップ 説明文 _src/index.ts_ \`\`\`typescript //delstart const oldCode = "old"; //delend //addstart const newCode = "new"; //addend \`\`\` ::: `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/code-block/missing-highlight')).toBe(false); }); }); describe('不完全なハイライトマーカー', () => { it('step内で//addstartのみでaddendがない場合はerrorを返す', () => { const content = `# Test :::step 1. ステップ 説明文 _src/index.ts_ \`\`\`typescript //addstart console.log("Hello"); \`\`\` ::: `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/code-block/unclosed-highlight')).toBe(true); const result = results.find(r => r.ruleId === 'text/code-block/unclosed-highlight'); expect(result?.severity).toBe('error'); expect(result?.message).toContain('addend'); }); it('step内で//delstartのみでdelendがない場合はerrorを返す', () => { const content = `# Test :::step 1. ステップ 説明文 _src/index.ts_ \`\`\`typescript //delstart console.log("Hello"); \`\`\` ::: `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/code-block/unclosed-highlight')).toBe(true); }); it('step内で//highlightstartのみでhighlightendがない場合はerrorを返す', () => { const content = `# Test :::step 1. ステップ 説明文 _src/index.ts_ \`\`\`typescript //highlightstart console.log("Hello"); \`\`\` ::: `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/code-block/unclosed-highlight')).toBe(true); }); it('step内で//errorstartのみでerrorendがない場合はerrorを返す', () => { const content = `# Test :::step 1. ステップ 説明文 _src/index.ts_ \`\`\`typescript //errorstart console.log("Hello"); \`\`\` ::: `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/code-block/unclosed-highlight')).toBe(true); }); it('step内で//warningstartのみでwarningendがない場合はerrorを返す', () => { const content = `# Test :::step 1. ステップ 説明文 _src/index.ts_ \`\`\`typescript //warningstart console.log("Hello"); \`\`\` ::: `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/code-block/unclosed-highlight')).toBe(true); }); }); describe('位置情報', () => { it('step内でハイライトなしエラーに正確な行番号が含まれる', () => { const content = `# Test :::step 1. ステップ 説明文 _src/index.ts_ \`\`\`typescript console.log("Hello"); \`\`\` ::: `; const context = createContext(content); const results = validator.validate(context); const result = results.find(r => r.ruleId === 'text/code-block/missing-highlight'); // コードブロックは11行目から始まる expect(result?.location?.line).toBe(11); }); }); }); describe('step外のコードブロックチェック', () => { it('step外でタイトルがないコードブロックはwarningを返す', () => { // コードブロックの直前に空行のみがある場合(タイトルがない) const content = `# Test 前の段落。 \`\`\`typescript console.log("Hello"); \`\`\` `; const context = createContext(content); const results = validator.validate(context); // 「前の段落。」がタイトルとして認識されるので、このテストは別の方法で // タイトルがない状態を再現する必要がある // 実際のユースケースでは、コードブロックの直前に空行しかない状態 expect(results.some(r => r.ruleId === 'text/code-block/missing-title')).toBe(false); }); it('step外でコードブロックの直前に何もテキストがない場合はwarningを返す', () => { // ファイルの先頭にコードブロックがある場合 const content = `\`\`\`typescript console.log("Hello"); \`\`\` `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/code-block/missing-title')).toBe(true); const result = results.find(r => r.ruleId === 'text/code-block/missing-title'); expect(result?.severity).toBe('warning'); expect(result?.message).toContain('タイトルがありません'); }); it('step外で任意のタイトルがあればwarningを返さない', () => { const content = `# Test 設定ファイルの例 \`\`\`typescript console.log("Hello"); \`\`\` `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/code-block/missing-title')).toBe(false); }); it('step外ではファイルパス形式でなくてもOK', () => { const content = `# Test これはサンプルコードです \`\`\`typescript console.log("Hello"); \`\`\` `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/code-block/missing-filepath')).toBe(false); expect(results.some(r => r.ruleId === 'text/code-block/missing-title')).toBe(false); }); it('step外ではコマンド実行形式でなくてもOK', () => { const content = `# Test npmのインストールコマンド \`\`\`bash npm install \`\`\` `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/code-block/missing-command-title')).toBe(false); expect(results.some(r => r.ruleId === 'text/code-block/missing-title')).toBe(false); }); it('step外ではハイライトチェックをしない', () => { const content = `# Test サンプルコード \`\`\`typescript console.log("Hello"); \`\`\` `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/code-block/missing-highlight')).toBe(false); }); }); });