Files
maestro/scripts/validate-help-docs.test.ts
T
oss-sync 57702b4f34
CI / build-and-test (push) Canceled after 0s
sync: update from private repo (2336949a)
2026-09-17 11:29:31 +00:00

125 lines
4.6 KiB
TypeScript

// ヘルプの「飛び先の見出し」検証が本当に効くことを守るテスト (#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');
});
});