import { describe, it, expect } from 'vitest'; import { StepDirectiveValidator } from '../../../../src/domain/text/validators/StepDirectiveValidator.js'; import { MarkdownParser } from '../../../../src/core/parser.js'; import type { ValidationContext } from '../../../../src/core/types.js'; describe('StepDirectiveValidator', () => { const validator = new StepDirectiveValidator(); 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/step'); }); it('nameが正しく設定されている', () => { expect(validator.name).toBe('ステップディレクティブ検証'); }); }); describe('インデントチェック', () => { it(':::step内の順序付きリスト後の説明文にインデントがある場合errorを返す', () => { const content = `# Test :::step 1. ファイルの作成 srcフォルダにindex.tsファイルを作成してください。 ::: `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/step/indented-content')).toBe(true); const result = results.find(r => r.ruleId === 'text/step/indented-content'); expect(result?.severity).toBe('error'); expect(result?.message).toContain('インデント'); }); it(':::step内のコードブロックにインデントがある場合errorを返す', () => { 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/step/indented-content')).toBe(true); }); it(':::step内でインデントなしの場合はerrorを返さない', () => { const content = `# Test :::step 1. ファイルの作成 srcフォルダにindex.tsファイルを作成してください。 2. コードの記述 src/index.tsファイルに以下のコードを記述してください。 _src/index.ts_ \`\`\`typescript console.log("Hello"); \`\`\` ::: `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/step/indented-content')).toBe(false); }); it('順序付きリスト直後の行にインデントがある場合errorを返す', () => { const content = `# Test :::step 1. ファイルの作成 説明文がインデントされています。 ::: `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/step/indented-content')).toBe(true); }); it('タブでインデントされている場合もerrorを返す', () => { const content = `# Test :::step 1. ファイルの作成 説明文がタブでインデントされています。 ::: `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/step/indented-content')).toBe(true); }); }); describe('複数のインデント違反', () => { it('複数箇所にインデントがある場合、それぞれerrorを返す', () => { const content = `# Test :::step 1. ファイルの作成 説明文1 2. コードの記述 説明文2 ::: `; const context = createContext(content); const results = validator.validate(context); const indentErrors = results.filter(r => r.ruleId === 'text/step/indented-content'); expect(indentErrors.length).toBeGreaterThanOrEqual(2); }); }); describe('コードブロック内のインデント', () => { it('コードブロック内のプログラムコードのインデントはエラーとしない', () => { const content = `# Test :::step 1. サーブレットの追加 説明文を追加します。 _src/SecondServlet.java_ \`\`\`java @WebServlet("/second") public class SecondServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) { PrintWriter out = resp.getWriter(); out.print("Hello"); } } \`\`\` ::: `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/step/indented-content')).toBe(false); }); it('コードブロックの開始マーカー(\`\`\`lang)にインデントがある場合はエラーを返す', () => { 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/step/indented-content')).toBe(true); }); it('コードブロックの終了マーカー(\`\`\`)にインデントがある場合はエラーを返す', () => { 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/step/indented-content')).toBe(true); }); it('複数のコードブロックがある場合もそれぞれの内部インデントは無視する', () => { const content = `# Test :::step 1. Javaコードの作成 Javaファイルを作成します。 _src/Main.java_ \`\`\`java public class Main { public static void main(String[] args) { System.out.println("Hello"); } } \`\`\` 2. コマンドの実行 コマンドを実行します。 _コマンド実行_ \`\`\`bash ./mvnw spring-boot:run \`\`\` ::: `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/step/indented-content')).toBe(false); }); }); describe(':::step外のコンテンツ', () => { it(':::step外のインデントはチェックしない', () => { const content = `# Test 通常のコンテンツ 1. 通常のリスト インデントされた説明 ## セクション `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/step/indented-content')).toBe(false); }); }); describe('空のstepブロック', () => { it(':::stepブロックが空の場合warningを返す', () => { const content = `# Test :::step ::: `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/step/empty')).toBe(true); const result = results.find(r => r.ruleId === 'text/step/empty'); expect(result?.severity).toBe('warning'); }); it(':::stepブロックに内容がある場合はwarningを返さない', () => { const content = `# Test :::step 1. 手順 説明 ::: `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/step/empty')).toBe(false); }); }); describe('番号付きリストのチェック', () => { it(':::step内に番号付きリストがない場合infoを返す', () => { const content = `# Test :::step 手順の説明だけ ::: `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/step/missing-numbered-list')).toBe(true); const result = results.find(r => r.ruleId === 'text/step/missing-numbered-list'); expect(result?.severity).toBe('info'); }); it(':::step内に番号付きリストがある場合はinfoを返さない', () => { const content = `# Test :::step 1. 手順1 説明1 2. 手順2 説明2 ::: `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/step/missing-numbered-list')).toBe(false); }); }); describe('位置情報', () => { it('エラー結果に正確な行番号が含まれる', () => { const content = `# Test :::step 1. ファイルの作成 インデントあり ::: `; const context = createContext(content); const results = validator.validate(context); const result = results.find(r => r.ruleId === 'text/step/indented-content'); expect(result?.location?.line).toBe(6); }); }); describe('ステップ説明文チェック', () => { it('順序付きリストの後に説明文がない場合warningを返す', () => { const content = `# Test :::step 1. VSCodeでプロジェクトを開く 2. サーブレットの追加 ::: `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/step/missing-description')).toBe(true); const result = results.find(r => r.ruleId === 'text/step/missing-description'); expect(result?.severity).toBe('warning'); expect(result?.message).toContain('説明'); }); it('順序付きリストの後にコードブロックのみの場合warningを返す', () => { const content = `# Test :::step 1. サーブレットの追加 _src/main/java/SecondServlet.java_ \`\`\`java public class SecondServlet {} \`\`\` 2. アプリケーションの再起動 _コマンド実行_ \`\`\`bash ./mvnw spring-boot:run \`\`\` ::: `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/step/missing-description')).toBe(true); const result = results.find(r => r.ruleId === 'text/step/missing-description'); expect(result?.message).toContain('説明'); }); it('順序付きリストの後に説明文とコードブロックがある場合はwarningを返さない', () => { const content = `# Test :::step 1. サーブレットの追加 \`src/main/java/com/example/servlet\`に\`SecondServlet.java\`を作成して下記コードを追加してください。 _src/main/java/SecondServlet.java_ \`\`\`java public class SecondServlet {} \`\`\` 2. アプリケーションの再起動 次のコマンドを実行してアプリケーションを再起動します。 \`\`\`bash ./mvnw spring-boot:run \`\`\` ::: `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/step/missing-description')).toBe(false); }); it('順序付きリストの後に説明文のみ(コードブロックなし)の場合はwarningを返さない', () => { const content = `# Test :::step 1. VSCodeでプロジェクトを開く 任意の場所でフォルダを作成し、VSCodeで開いてください。 2. ブラウザで動作確認 ブラウザを開き、\`http://localhost:8080/second\`にアクセスします。 ::: `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/step/missing-description')).toBe(false); }); it('ファイルパスタイトル(_path_)のみはコードブロックの一部として扱い説明文とみなさない', () => { 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/step/missing-description')).toBe(true); }); it('コマンド実行タイトル(_コマンド実行_)のみはコードブロックの一部として扱い説明文とみなさない', () => { const content = `# Test :::step 1. アプリケーションの起動 _コマンド実行_ \`\`\`bash npm start \`\`\` ::: `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/step/missing-description')).toBe(true); }); it('箇条書きリストも説明文として認める', () => { const content = `# Test :::step 1. サーブレットの追加 - \`@WebServlet("/second")\` はURLマッピング用アノテーションです - \`@Override\` はメソッドをオーバーライドするためのアノテーションです _src/SecondServlet.java_ \`\`\`java public class SecondServlet {} \`\`\` ::: `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/step/missing-description')).toBe(false); }); it('複数のステップで説明がないものだけwarningを返す', () => { const content = `# Test :::step 1. VSCodeでプロジェクトを開く 任意の場所でフォルダを作成してください。 2. サーブレットの追加 _src/SecondServlet.java_ \`\`\`java public class SecondServlet {} \`\`\` 3. ブラウザで確認 ブラウザでアクセスしてください。 ::: `; const context = createContext(content); const results = validator.validate(context); const descErrors = results.filter(r => r.ruleId === 'text/step/missing-description'); expect(descErrors.length).toBe(1); // ステップ2のみがエラー }); it('最後のステップに説明がない場合もwarningを返す', () => { const content = `# Test :::step 1. 手順1 説明があります。 2. 手順2 ::: `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/step/missing-description')).toBe(true); }); it('画像リンクも説明文として認める', () => { const content = `# Test :::step 1. ブラウザで動作確認 ![ブラウザでHello second page!!と表示されている画面キャプチャ](./images/screenshot.png) ::: `; const context = createContext(content); const results = validator.validate(context); expect(results.some(r => r.ruleId === 'text/step/missing-description')).toBe(false); }); }); });