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);
});
});
});