// ヘルプの「飛び先の見出し」検証が本当に効くことを守るテスト (#1274)。 // // 守る対象は「いま何本壊れているか」ではなく、**壊したら赤くなる経路が残って // いること**。`#` から後ろを見ない作りに戻したら、ここが緑のままになる——それを // 防ぐために、同じ本文で「合っている / 合っていない」を撮り比べる。 import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { spawnSync } from 'node:child_process'; import { mkdtempSync, writeFileSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '..'); const script = join(repoRoot, 'scripts', 'validate-help-docs.mjs'); let dir: string; function write(name: string, body: string) { writeFileSync(join(dir, name), body); } function run() { const r = spawnSync(process.execPath, [script, dir], { encoding: 'utf8' }); return { code: r.status, out: (r.stdout ?? '') + (r.stderr ?? '') }; } /** 飛び先になるページ。見出しの深さは 2 と 4 を混ぜてある(後述)。 */ const TARGET = `--- id: target title: 飛び先 category: basic order: 1 --- ## 接続を切ったとき、置き場の側はどうなるか 本文。 #### 深い見出しにも id は付きます 本文。 `; /** 同じページの中だけで完結する本文。見出しとそこへのリンクを 1 本ずつ持つ。 */ function selfPage(link: string) { return `--- id: self title: 同じページ category: basic order: 3 --- ## 書き換わったら、押さなくても切り替わります 続きは [上の節](${link}) を参照。 `; } function source(link: string) { return `--- id: source title: 出どころ category: basic order: 2 --- 案内は [こちら](${link}) にあります。 `; } describe('validate-help-docs.mjs の飛び先検証', () => { beforeEach(() => { dir = mkdtempSync(join(tmpdir(), 'help-docs-')); }); afterEach(() => { rmSync(dir, { recursive: true, force: true }); }); it('見出しと合っているリンクは通す', () => { write('01-target.md', TARGET); write('02-source.md', source('./01-target.md#接続を切ったとき-置き場の側はどうなるか')); const { code, out } = run(); expect(out).toContain('help-docs validation OK'); expect(code).toBe(0); }); it('見出しと合っていないリンクを、ファイル名・行番号・リンクの文字列つきで落とす', () => { write('01-target.md', TARGET); // 見出しの文言が変わったのにリンクが追いついていない、という壊れ方。 write('02-source.md', source('./01-target.md#接続を切ったとき置き場の側はどうなるか')); const { code, out } = run(); expect(code).toBe(1); expect(out).toContain('02-source.md:8:'); expect(out).toContain('](./01-target.md#接続を切ったとき置き場の側はどうなるか)'); expect(out).toContain("no heading '#接続を切ったとき置き場の側はどうなるか' in 01-target.md"); }); it('h2/h3 以外の見出しへのリンクも通す(画面は深さによらず id を付けるため)', () => { write('01-target.md', TARGET); write('02-source.md', source('./01-target.md#深い見出しにも-id-は付きます')); const { code } = run(); expect(code).toBe(0); }); it('同じページの中のリンクも、見出しと合っていれば通す', () => { write('03-self.md', selfPage('#書き換わったら-押さなくても切り替わります')); const { code, out } = run(); expect(out).toContain('help-docs validation OK'); expect(code).toBe(0); }); it('同じページの中のリンクも、合っていなければ名指しして落とす', () => { write('03-self.md', selfPage('#書き換わったら押さなくても切り替わります')); const { code, out } = run(); expect(code).toBe(1); expect(out).toContain('03-self.md:10:'); expect(out).toContain('](#書き換わったら押さなくても切り替わります)'); expect(out).toContain("no heading '#書き換わったら押さなくても切り替わります' in 03-self.md"); }); it('リンク先のファイルが無いときは、これまでどおり落とす', () => { write('01-target.md', TARGET); write('02-source.md', source('./99-missing.md#どこか')); const { code, out } = run(); expect(code).toBe(1); expect(out).toContain('target file not found'); }); });