125 lines
4.6 KiB
TypeScript
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');
|
|
});
|
|
});
|