web-corders-vrt 0.1.4 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,28 +2,40 @@
2
2
 
3
3
  Webページのビジュアルリグレッションテスト (VRT) を行うCLIツール。
4
4
 
5
- 本番環境と開発環境のスクリーンショットの差分を検出する。
6
- テスト結果は人間向け(HTMLレポート + diff画像)とLLM/Claude向け(JSONレポート)の両方で出力される。
5
+ 開発マシンに入っているブラウザを利用して、本番環境と開発/プレビュー環境のスクリーンショットの差分を検出する。
6
+ テスト結果は
7
+
8
+ - diff画像
9
+ - 人間向けHTMLレポート
10
+ - LLM/Claude向けJSONレポート
11
+
12
+ で出力される。
7
13
 
8
14
  ## セットアップ
9
15
 
10
- インストール不要。`npx` で直接実行できる。初回のみ Playwright のブラウザが必要:
16
+ 初回のみ Playwright のブラウザが必要:
17
+
18
+ ```bash
19
+ npx playwright install chromium #通常はインストール済みのChromeが使われるはず
20
+ ```
21
+
22
+ `init` コマンドで、Claude Codeのスキルファイル (`.claude/skills/web-corders-vrt/SKILL.md`) を自動生成する
11
23
 
12
24
  ```bash
13
- npx playwright install chromium
25
+ npx web-corders-vrt init --refDomain https://example.com
14
26
  ```
15
27
 
16
- ## 基本的な使い方
28
+ ## 手動での使い方
17
29
 
18
30
  ```bash
19
31
  npx web-corders-vrt run \
20
- --before https://example.com \
32
+ --reference https://production.com \
21
33
  --after http://localhost:3000 \
22
34
  --paths /,/about,/pricing
23
35
  ```
24
36
 
25
- - `--before` — 比較元URL(本番環境)
26
- - `--after` — 比較先URL(開発環境 / ステージング)
37
+ - `--reference` — リファレンスのプロトコル+ドメイン(本番環境)
38
+ - `--after` — 比較するプロトコル+ドメイン(開発 / プレビュー環境)
27
39
  - `--paths` — テスト対象のページパス(カンマ区切り)
28
40
 
29
41
  実行すると `./vrt-results/` 以下にタイムスタンプ付きのディレクトリが生成され、スクリーンショット・diff画像・レポートが保存される。
@@ -35,10 +47,10 @@ vrt-results/2026-02-28T10-00-00/
35
47
  ├── report.json # JSON レポート(Claude/LLM向け)
36
48
  ├── report.html # HTML レポート(人間向け・ブラウザで開く)
37
49
  └── screenshots/
38
- ├── top--sp--before.png
50
+ ├── top--sp--reference.png
39
51
  ├── top--sp--after.png
40
52
  ├── top--sp--diff.png
41
- ├── top--pc--before.png
53
+ ├── top--pc--reference.png
42
54
  ├── top--pc--after.png
43
55
  └── top--pc--diff.png
44
56
  ```
@@ -72,67 +84,58 @@ vrt-results/2026-02-28T10-00-00/
72
84
 
73
85
  | オプション | デフォルト | 説明 |
74
86
  | -------------------- | ---------- | ----------------------------------------------------------------------- |
75
- | `--before <url>` | (必須) | 比較元URL(本番環境) |
87
+ | `--reference <url>` | (必須) | リファレンスURL(本番環境) |
76
88
  | `--after <url>` | (必須) | 比較先URL(開発環境 / ステージング) |
77
89
  | `--paths <paths>` | (必須) | テスト対象のページパス(カンマ区切り) |
78
90
  | `--threshold <n>` | `0.1` | 差分許容率(%)。これ以下の差分はPASS扱い |
79
91
  | `--hide <selectors>` | — | 非表示にするCSSセレクタ(カンマ区切り)。例: `.ad-banner,.cookie-popup` |
80
- | `--no-html` | — | HTMLレポートを生成しない |
81
- | `--no-open` | — | HTMLレポートをブラウザで自動的に開かない |
82
92
 
83
93
  ビューポートはSP(375x812)とPC(1440x900)の2種類で固定。フルページスクリーンショットを取得し、結果は `./vrt-results/` に出力される。
84
94
 
95
+ ## Claude Code連携
96
+
97
+ ### スキルファイルの仕組み
98
+
99
+ `init`で生成される `.claude/skills/web-corders-vrt/SKILL.md` には以下の手順が記述される:
100
+
101
+ 1. 開発サーバーを起動
102
+ 2. VRTコマンドを実行
103
+ 3. `report.json` を読んで差分を確認
104
+ 4. diff画像を視覚的に確認
105
+ 5. diff画像からCSS/HTMLの修正箇所を特定
106
+ 6. コードを修正
107
+ 7. 再度VRTを実行して確認
108
+
109
+ 以降はCaludeに「VRTして」とか言うだけで使ってくれる はず。
110
+
85
111
  ## 実用的な例
86
112
 
87
113
  ### 動的要素を隠してテスト
88
114
 
89
- 広告バナーやクッキー同意バーなど、毎回変わる要素を非表示にする:
115
+ テスト対象外にしたい要素を非表示にする。
90
116
 
91
117
  ```bash
92
118
  npx web-corders-vrt run \
93
- --before https://example.com \
119
+ --reference https://example.com \
94
120
  --after http://localhost:3000 \
95
121
  --paths / \
96
122
  --hide ".cookie-banner,.ad-slot,[data-testid='live-chat']"
97
123
  ```
98
124
 
125
+ Next.jsツールバーはデフォルトで非表示。
126
+
99
127
  ### 差分許容率を緩めに設定
100
128
 
101
129
  フォントレンダリング差異などを無視したい場合:
102
130
 
103
131
  ```bash
104
132
  npx web-corders-vrt run \
105
- --before https://example.com \
133
+ --reference https://example.com \
106
134
  --after http://localhost:3000 \
107
135
  --paths / \
108
136
  --threshold 1
109
137
  ```
110
138
 
111
- ## Claude Code連携
112
-
113
- `vrt init` コマンドで、Claude Codeのスキルファイル (`.claude/commands/vrt.md`) を自動生成できる。
114
-
115
- ```bash
116
- npx web-corders-vrt init \
117
- --before https://example.com \
118
- --after http://localhost:3000 \
119
- --paths /,/about,/pricing
120
- ```
121
-
122
- 生成されたファイルをリポジトリにコミットすると、Claude Code上で `/vrt` コマンドを実行するだけで、VRTの実行 → 結果の読み取り → 差分箇所の特定 → コード修正 → 再テストまでを自律的に行えるようになる。
123
-
124
- ### スキルファイルの仕組み
125
-
126
- 生成される `.claude/commands/vrt.md` には以下の手順が記述される:
127
-
128
- 1. 開発サーバーを起動
129
- 2. VRTコマンドを実行
130
- 3. `report.json` を読んで差分を確認
131
- 4. diff画像を視覚的に確認
132
- 5. diff画像からCSS/HTMLの修正箇所を特定
133
- 6. コードを修正
134
- 7. 再度VRTを実行して確認
135
-
136
139
  ## 広告・計測系スクリプトのブロック
137
140
 
138
141
  以下のドメインへのリクエストは自動的にブロックされる。見た目のテストに不要であり、ページ読み込み速度を改善するため。
package/bin/vrt.ts CHANGED
@@ -14,25 +14,21 @@ program
14
14
  program
15
15
  .command("run")
16
16
  .description("Run visual regression tests")
17
- .requiredOption("--before <url>", "Baseline URL (production)")
17
+ .requiredOption("--reference <url>", "Reference URL (production)")
18
18
  .requiredOption("--after <url>", "Comparison URL (local/staging)")
19
19
  .requiredOption("--paths <paths>", "Page paths to compare (comma-separated)")
20
20
  .option("--threshold <n>", "Diff tolerance percentage", "0.1")
21
21
  .option("--hide <selectors>", "CSS selectors to hide (comma-separated)")
22
- .option("--no-html", "Skip HTML report generation")
23
- .option("--no-open", "Do not open HTML report in browser")
24
22
  .action(async (rawOptions) => {
25
23
  try {
26
24
  const parsed = cliOptionsSchema.parse(rawOptions);
27
25
 
28
26
  const options: ResolvedOptions = {
29
- beforeUrl: parsed.before,
27
+ referenceUrl: parsed.reference,
30
28
  afterUrl: parsed.after,
31
29
  paths: parsed.paths,
32
30
  threshold: parsed.threshold,
33
31
  hideSelectors: parsed.hide,
34
- html: parsed.html,
35
- open: parsed.open,
36
32
  };
37
33
 
38
34
  const report = await runVrt(options);
@@ -50,22 +46,12 @@ program
50
46
  program
51
47
  .command("init")
52
48
  .description(
53
- "Generate .claude/commands/vrt.md skill file for this repository",
49
+ "Generate .claude/skills/web-corders-vrt/SKILL.md for this repository",
54
50
  )
55
- .requiredOption("--before <url>", "Baseline URL (production)")
56
- .requiredOption("--after <url>", "Comparison URL (local/staging)")
57
- .requiredOption("--paths <paths>", "Page paths to compare (comma-separated)")
58
- .option("--threshold <n>", "Diff tolerance percentage", "0.1")
59
- .option("--hide <selectors>", "CSS selectors to hide (comma-separated)")
51
+ .requiredOption("--refDomain <domain>", "Reference domain URL (production)")
60
52
  .action(async (options) => {
61
53
  try {
62
- await runInit({
63
- before: options.before,
64
- after: options.after,
65
- paths: options.paths,
66
- threshold: parseFloat(options.threshold),
67
- hide: options.hide,
68
- });
54
+ await runInit({ refDomain: options.refDomain });
69
55
  } catch (error) {
70
56
  if (error instanceof Error) {
71
57
  console.error(`Error: ${error.message}`);
package/dist/bin/vrt.js CHANGED
@@ -6,13 +6,11 @@ import { Command } from "commander";
6
6
  // src/schemas.ts
7
7
  import { z } from "zod";
8
8
  var cliOptionsSchema = z.object({
9
- before: z.string().url("--before must be a valid URL"),
9
+ reference: z.string().url("--reference must be a valid URL"),
10
10
  after: z.string().url("--after must be a valid URL"),
11
11
  paths: z.string().transform((val) => val.split(",").map((p) => p.trim())),
12
12
  threshold: z.coerce.number().min(0).max(100).default(0.1),
13
- hide: z.string().optional().transform((val) => val ? val.split(",").map((s) => s.trim()) : []),
14
- html: z.boolean().default(true),
15
- open: z.boolean().default(true)
13
+ hide: z.string().optional().transform((val) => val ? val.split(",").map((s) => s.trim()) : [])
16
14
  });
17
15
 
18
16
  // src/commands/run.ts
@@ -193,16 +191,16 @@ var Screenshotter = class {
193
191
  // src/core/comparator.ts
194
192
  import pixelmatch from "pixelmatch";
195
193
  import { PNG } from "pngjs";
196
- function compareImages(beforeBuffer, afterBuffer, threshold = 0.1) {
197
- const before = PNG.sync.read(beforeBuffer);
194
+ function compareImages(referenceBuffer, afterBuffer, threshold = 0.1) {
195
+ const reference = PNG.sync.read(referenceBuffer);
198
196
  const after = PNG.sync.read(afterBuffer);
199
- const width = Math.max(before.width, after.width);
200
- const height = Math.max(before.height, after.height);
201
- const normalizedBefore = normalizeImage(before, width, height);
197
+ const width = Math.max(reference.width, after.width);
198
+ const height = Math.max(reference.height, after.height);
199
+ const normalizedReference = normalizeImage(reference, width, height);
202
200
  const normalizedAfter = normalizeImage(after, width, height);
203
201
  const diff = new PNG({ width, height });
204
202
  const diffCount = pixelmatch(
205
- normalizedBefore.data,
203
+ normalizedReference.data,
206
204
  normalizedAfter.data,
207
205
  diff.data,
208
206
  width,
@@ -226,7 +224,7 @@ function compareImages(beforeBuffer, afterBuffer, threshold = 0.1) {
226
224
  dimensions: {
227
225
  width,
228
226
  height,
229
- beforeHeight: before.height,
227
+ referenceHeight: reference.height,
230
228
  afterHeight: after.height
231
229
  }
232
230
  };
@@ -264,7 +262,7 @@ function printTerminalReport(report) {
264
262
  console.log("=".repeat(50));
265
263
  console.log("");
266
264
  console.log(
267
- `Comparing: ${chalk.cyan(meta.beforeUrl)} vs ${chalk.cyan(meta.afterUrl)}`
265
+ `Comparing: ${chalk.cyan(meta.referenceUrl)} vs ${chalk.cyan(meta.afterUrl)}`
268
266
  );
269
267
  console.log("");
270
268
  const groupedByPage = /* @__PURE__ */ new Map();
@@ -323,7 +321,7 @@ import { writeFile as writeFile2 } from "fs/promises";
323
321
  import { join as join2 } from "path";
324
322
 
325
323
  // src/templates/report.html
326
- var report_default = '<!doctype html>\n<html lang="ja">\n <head>\n <meta charset="UTF-8" />\n <meta name="viewport" content="width=device-width, initial-scale=1.0" />\n <title>VRT Report - {{TIMESTAMP}}</title>\n <style>\n {{CSS}}\n </style>\n </head>\n <body>\n <div class="header">\n <h1>web-corders-vrt</h1>\n <div class="meta">\n {{TIMESTAMP}} | Duration: {{DURATION}}s<br />\n Before: {{BEFORE_URL}} \u2192 After: {{AFTER_URL}}\n </div>\n </div>\n <div class="summary">\n <div class="stat total">{{TOTAL}} Total</div>\n <div class="stat pass">{{PASSED}} Passed</div>\n {{FAILED_STAT}}\n </div>\n <div class="results">\n {{RESULTS}}\n </div>\n </body>\n</html>\n';
324
+ var report_default = '<!doctype html>\n<html lang="ja">\n <head>\n <meta charset="UTF-8" />\n <meta name="viewport" content="width=device-width, initial-scale=1.0" />\n <title>VRT Report - {{TIMESTAMP}}</title>\n <style>\n {{CSS}}\n </style>\n </head>\n <body>\n <div class="header">\n <h1>web-corders-vrt</h1>\n <div class="meta">\n {{TIMESTAMP}} | Duration: {{DURATION}}s<br />\n Reference: {{REFERENCE_URL}} \u2192 After: {{AFTER_URL}}\n </div>\n </div>\n <div class="summary">\n <div class="stat total">{{TOTAL}} Total</div>\n <div class="stat pass">{{PASSED}} Passed</div>\n {{FAILED_STAT}}\n </div>\n <div class="results">\n {{RESULTS}}\n </div>\n </body>\n</html>\n';
327
325
 
328
326
  // src/templates/report.css
329
327
  var report_default2 = '* {\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n}\n\nbody {\n font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;\n background: #f5f5f5;\n color: #333;\n}\n\n.header {\n background: #1a1a2e;\n color: white;\n padding: 24px 32px;\n text-align: center;\n}\n\n.header h1 {\n font-size: 20px;\n margin-bottom: 8px;\n}\n\n.header .meta {\n font-size: 13px;\n color: #aaa;\n}\n\n.summary {\n display: flex;\n gap: 16px;\n padding: 16px 32px;\n background: white;\n border-bottom: 1px solid #e0e0e0;\n}\n\n.summary .stat {\n padding: 8px 16px;\n border-radius: 6px;\n font-size: 14px;\n font-weight: 600;\n}\n\n.stat.pass {\n background: #e8f5e9;\n color: #2e7d32;\n}\n\n.stat.fail {\n background: #ffebee;\n color: #c62828;\n}\n\n.stat.total {\n background: #e3f2fd;\n color: #1565c0;\n}\n\n.results {\n padding: 24px 32px;\n display: flex;\n flex-direction: column;\n gap: 24px;\n}\n\n.card {\n background: white;\n border-radius: 8px;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);\n overflow: hidden;\n}\n\n.card-header {\n padding: 16px 20px;\n border-bottom: 1px solid #eee;\n display: flex;\n justify-content: space-between;\n align-items: center;\n}\n\n.card-header h3 {\n font-size: 15px;\n}\n\n.badge {\n padding: 4px 10px;\n border-radius: 12px;\n font-size: 12px;\n font-weight: 600;\n}\n\n.badge.pass {\n background: #e8f5e9;\n color: #2e7d32;\n}\n\n.badge.fail {\n background: #ffebee;\n color: #c62828;\n}\n\n.badge.error {\n background: #fff3e0;\n color: #e65100;\n}\n\n.comparison {\n padding: 16px 20px;\n}\n\n.images {\n display: flex;\n gap: 8px;\n}\n\n.card.sp .img-container {\n flex: none;\n width: 375px;\n}\n\n.card.sp .img-container img {\n width: 375px;\n height: auto;\n border: 1px solid #eee;\n border-radius: 4px;\n}\n\n.card.pc .img-container {\n flex: 1;\n min-width: 0;\n}\n\n.card.pc .img-container img {\n width: 100%;\n height: auto;\n border: 1px solid #eee;\n border-radius: 4px;\n}\n\n.img-container .label {\n font-size: 11px;\n color: #888;\n margin-bottom: 4px;\n text-transform: uppercase;\n font-weight: 600;\n}\n\n.diff-info {\n padding: 8px 20px 16px;\n font-size: 13px;\n color: #666;\n}\n\n.diff-info .region {\n margin: 4px 0;\n padding-left: 16px;\n}\n\n/* Accordion for passed tests */\n.screenshot-accordion {\n border-top: 1px solid #eee;\n}\n\n.screenshot-accordion summary {\n padding: 12px 20px;\n cursor: pointer;\n font-size: 13px;\n font-weight: 600;\n color: #666;\n user-select: none;\n}\n\n.screenshot-accordion summary:hover {\n background: #fafafa;\n}\n';
@@ -339,7 +337,7 @@ function generateHtml(report) {
339
337
  const { meta, summary, results } = report;
340
338
  const resultCards = results.map((r) => generateResultCard(r)).join("\n");
341
339
  const failedStat = summary.failed > 0 ? `<div class="stat fail">${summary.failed} Failed</div>` : "";
342
- return report_default.replace("{{CSS}}", report_default2).replace(/\{\{TIMESTAMP\}\}/g, meta.timestamp).replace("{{DURATION}}", (meta.duration / 1e3).toFixed(1)).replace("{{BEFORE_URL}}", meta.beforeUrl).replace("{{AFTER_URL}}", meta.afterUrl).replace("{{TOTAL}}", String(summary.totalTests)).replace("{{PASSED}}", String(summary.passed)).replace("{{FAILED_STAT}}", failedStat).replace("{{RESULTS}}", resultCards);
340
+ return report_default.replace("{{CSS}}", report_default2).replace(/\{\{TIMESTAMP\}\}/g, meta.timestamp).replace("{{DURATION}}", (meta.duration / 1e3).toFixed(1)).replace("{{REFERENCE_URL}}", meta.referenceUrl).replace("{{AFTER_URL}}", meta.afterUrl).replace("{{TOTAL}}", String(summary.totalTests)).replace("{{PASSED}}", String(summary.passed)).replace("{{FAILED_STAT}}", failedStat).replace("{{RESULTS}}", resultCards);
343
341
  }
344
342
  function generateResultCard(result) {
345
343
  const { page, viewport, status, comparison, screenshots } = result;
@@ -347,9 +345,9 @@ function generateResultCard(result) {
347
345
  const imagesHtml = `
348
346
  <div class="comparison">
349
347
  <div class="images">
350
- <div class="img-container img-before">
351
- <div class="label">Before</div>
352
- <img src="${screenshots.before}" alt="Before" loading="lazy">
348
+ <div class="img-container img-reference">
349
+ <div class="label">Reference</div>
350
+ <img src="${screenshots.reference}" alt="Reference" loading="lazy">
353
351
  </div>
354
352
  <div class="img-container img-after">
355
353
  <div class="label">After</div>
@@ -384,9 +382,9 @@ async function runVrt(options) {
384
382
  const spinner = ora("Initializing browser...").start();
385
383
  try {
386
384
  await screenshotter.initialize();
387
- spinner.text = `Taking screenshots of ${chalk2.cyan(options.beforeUrl)}...`;
388
- const beforeScreenshots = await screenshotter.captureAll(
389
- options.beforeUrl,
385
+ spinner.text = `Taking screenshots of ${chalk2.cyan(options.referenceUrl)}...`;
386
+ const referenceScreenshots = await screenshotter.captureAll(
387
+ options.referenceUrl,
390
388
  options.paths,
391
389
  VIEWPORTS,
392
390
  options.hideSelectors
@@ -400,15 +398,15 @@ async function runVrt(options) {
400
398
  );
401
399
  spinner.text = "Comparing screenshots...";
402
400
  const results = [];
403
- for (const beforeShot of beforeScreenshots) {
401
+ for (const referenceShot of referenceScreenshots) {
404
402
  const afterShot = afterScreenshots.find(
405
- (a) => a.pagePath === beforeShot.pagePath && a.viewportType === beforeShot.viewportType
403
+ (a) => a.pagePath === referenceShot.pagePath && a.viewportType === referenceShot.viewportType
406
404
  );
407
405
  if (!afterShot) {
408
406
  results.push(
409
407
  createErrorResult(
410
- beforeShot.pagePath,
411
- beforeShot.viewportType,
408
+ referenceShot.pagePath,
409
+ referenceShot.viewportType,
412
410
  options,
413
411
  "After screenshot not found"
414
412
  )
@@ -417,33 +415,33 @@ async function runVrt(options) {
417
415
  }
418
416
  try {
419
417
  const comparison = compareImages(
420
- beforeShot.buffer,
418
+ referenceShot.buffer,
421
419
  afterShot.buffer,
422
420
  options.threshold
423
421
  );
424
- const filePrefix = `${beforeShot.pageName}--${beforeShot.viewportType}`;
425
- const beforePath = join3("screenshots", `${filePrefix}--before.png`);
422
+ const filePrefix = `${referenceShot.pageName}--${referenceShot.viewportType}`;
423
+ const referencePath = join3("screenshots", `${filePrefix}--reference.png`);
426
424
  const afterPath = join3("screenshots", `${filePrefix}--after.png`);
427
425
  const diffPath = join3("screenshots", `${filePrefix}--diff.png`);
428
- await writeFile3(join3(runDir, beforePath), beforeShot.buffer);
426
+ await writeFile3(join3(runDir, referencePath), referenceShot.buffer);
429
427
  await writeFile3(join3(runDir, afterPath), afterShot.buffer);
430
428
  await writeFile3(join3(runDir, diffPath), comparison.diffImage);
431
- const pageName = beforeShot.pagePath === "/" ? "\u30C8\u30C3\u30D7\u30DA\u30FC\u30B8" : beforeShot.pagePath.replace(/^\//, "");
432
- const viewport = getViewportDimensions(beforeShot.viewportType);
429
+ const pageName = referenceShot.pagePath === "/" ? "\u30C8\u30C3\u30D7\u30DA\u30FC\u30B8" : referenceShot.pagePath.replace(/^\//, "");
430
+ const viewport = getViewportDimensions(referenceShot.viewportType);
433
431
  results.push({
434
432
  page: {
435
- path: beforeShot.pagePath,
433
+ path: referenceShot.pagePath,
436
434
  name: pageName,
437
435
  url: {
438
- before: new URL(
439
- beforeShot.pagePath,
440
- options.beforeUrl
436
+ reference: new URL(
437
+ referenceShot.pagePath,
438
+ options.referenceUrl
441
439
  ).toString(),
442
- after: new URL(beforeShot.pagePath, options.afterUrl).toString()
440
+ after: new URL(referenceShot.pagePath, options.afterUrl).toString()
443
441
  }
444
442
  },
445
443
  viewport: {
446
- type: beforeShot.viewportType,
444
+ type: referenceShot.viewportType,
447
445
  ...viewport
448
446
  },
449
447
  status: comparison.passed ? "pass" : "fail",
@@ -455,7 +453,7 @@ async function runVrt(options) {
455
453
  dimensions: comparison.dimensions
456
454
  },
457
455
  screenshots: {
458
- before: beforePath,
456
+ reference: referencePath,
459
457
  after: afterPath,
460
458
  diff: diffPath
461
459
  }
@@ -463,8 +461,8 @@ async function runVrt(options) {
463
461
  } catch (error) {
464
462
  results.push(
465
463
  createErrorResult(
466
- beforeShot.pagePath,
467
- beforeShot.viewportType,
464
+ referenceShot.pagePath,
465
+ referenceShot.viewportType,
468
466
  options,
469
467
  error instanceof Error ? error.message : String(error)
470
468
  )
@@ -479,7 +477,7 @@ async function runVrt(options) {
479
477
  version: "1.0",
480
478
  meta: {
481
479
  timestamp,
482
- beforeUrl: options.beforeUrl,
480
+ referenceUrl: options.referenceUrl,
483
481
  afterUrl: options.afterUrl,
484
482
  duration,
485
483
  command: buildCommandString(options)
@@ -497,14 +495,10 @@ async function runVrt(options) {
497
495
  printTerminalReport(report);
498
496
  const jsonPath = await writeJsonReport(report, runDir);
499
497
  console.log(`JSON report: ${chalk2.dim(jsonPath)}`);
500
- if (options.html) {
501
- const htmlPath = await writeHtmlReport(report, runDir);
502
- console.log(`HTML report: ${chalk2.dim(htmlPath)}`);
503
- if (options.open) {
504
- const { exec } = await import("child_process");
505
- exec(`open "${htmlPath}"`);
506
- }
507
- }
498
+ const htmlPath = await writeHtmlReport(report, runDir);
499
+ console.log(`HTML report: ${chalk2.dim(htmlPath)}`);
500
+ const { exec } = await import("child_process");
501
+ exec(`open "${htmlPath}"`);
508
502
  console.log(`Output dir: ${chalk2.dim(runDir)}`);
509
503
  return report;
510
504
  } finally {
@@ -522,7 +516,7 @@ function createErrorResult(pagePath, viewportType, options, error) {
522
516
  path: pagePath,
523
517
  name: pageName,
524
518
  url: {
525
- before: new URL(pagePath, options.beforeUrl).toString(),
519
+ reference: new URL(pagePath, options.referenceUrl).toString(),
526
520
  after: new URL(pagePath, options.afterUrl).toString()
527
521
  }
528
522
  },
@@ -533,15 +527,15 @@ function createErrorResult(pagePath, viewportType, options, error) {
533
527
  diffPixelCount: 0,
534
528
  totalPixels: 0,
535
529
  threshold: options.threshold,
536
- dimensions: { width: 0, height: 0, beforeHeight: 0, afterHeight: 0 }
530
+ dimensions: { width: 0, height: 0, referenceHeight: 0, afterHeight: 0 }
537
531
  },
538
- screenshots: { before: "", after: "", diff: "" },
532
+ screenshots: { reference: "", after: "", diff: "" },
539
533
  error
540
534
  };
541
535
  }
542
536
  function buildCommandString(options) {
543
537
  const parts = ["npx web-corders-vrt run"];
544
- parts.push(`--before ${options.beforeUrl}`);
538
+ parts.push(`--reference ${options.referenceUrl}`);
545
539
  parts.push(`--after ${options.afterUrl}`);
546
540
  parts.push(`--paths ${options.paths.join(",")}`);
547
541
  if (options.threshold !== DEFAULT_THRESHOLD) {
@@ -554,76 +548,68 @@ function buildCommandString(options) {
554
548
  }
555
549
 
556
550
  // src/commands/init.ts
557
- import { mkdir as mkdir2, writeFile as writeFile4, access } from "fs/promises";
551
+ import { mkdir as mkdir2, writeFile as writeFile4, readFile, access } from "fs/promises";
558
552
  import { join as join4 } from "path";
559
553
  import chalk3 from "chalk";
554
+
555
+ // src/templates/SKILL-TEMPLATE.md
556
+ var SKILL_TEMPLATE_default = '---\nname: web-corders-vrt\ndescription: WEB\u7528VRT(Visual Regression Test)\u30C4\u30FC\u30EB\u3002\u672C\u756A\u30C9\u30E1\u30A4\u30F3\u3068\u30ED\u30FC\u30AB\u30EB\u958B\u767A\u307E\u305F\u306F\u30D7\u30EC\u30D3\u30E5\u30FC\u30C9\u30E1\u30A4\u30F3\u306E\u9593\u3067\u3001\u7279\u5B9A\u30D1\u30B9\u306E\u30D3\u30B8\u30E5\u30A2\u30EB\u5DEE\u5206\u3092\u691C\u51FA\u3059\u308B\u3002\n---\n\n## \u4F7F\u3044\u65B9\n\n```bash\nnpx web-corders-vrt run \\\n --reference ${referenceDomain} \\\n --after <\u958B\u767A\u74B0\u5883\u306E\u30C9\u30E1\u30A4\u30F3|\u30E6\u30FC\u30B6\u30FC\u304B\u3089\u6307\u793A\u304C\u306A\u3044\u5834\u5408\u306Fhttp://localhost:3000> \\\n --paths <\u30C6\u30B9\u30C8\u5BFE\u8C61\u306E\u30D1\u30B9\uFF08\u30AB\u30F3\u30DE\u533A\u5207\u308A\uFF09> \\\n```\n\n## \u624B\u9806\n\n1. \u30E6\u30FC\u30B6\u30FC\u304B\u3089\u6BD4\u8F03\u5BFE\u8C61\u306E\u30C9\u30E1\u30A4\u30F3\u3092\u6307\u5B9A\u3055\u308C\u305F\u5834\u5408\u3001\u305D\u308C\u306B\u5F93\u3046\u3002\u6307\u793A\u304C\u306A\u3044\u5834\u5408\u3001after\u306F\u958B\u767A\u30B5\u30FC\u30D0\u30FC\u3068\u3057\u3001\u8D77\u52D5\u3057\u3066\u3044\u306A\u3051\u308C\u3070 `npm run dev` \u3067\u8D77\u52D5\u3057\u3066\u5F85\u6A5F\u3059\u308B\u3002\u3053\u3053\u3067\u8D77\u52D5\u3057\u305F\u30C9\u30E1\u30A4\u30F3\u3068\u30D7\u30ED\u30C8\u30B3\u30EB\u3092after\u306E\u30C9\u30E1\u30A4\u30F3\u3068\u3059\u308B\u3002\n2. \u4E0A\u8A18\u306E\u30B3\u30DE\u30F3\u30C9\u3067VRT\u3092\u5B9F\u884C\u3059\u308B\uFF08`--paths` \u306F\u30BF\u30B9\u30AF\u306B\u5FDC\u3058\u3066\u8A2D\u5B9A\u3059\u308B\uFF09\n3. `./vrt-results/` \u5185\u306E\u6700\u65B0\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u306B\u3042\u308B `report.json` \u3092\u8AAD\u3080\n4. `status: "fail"` \u306E\u30C6\u30B9\u30C8\u7D50\u679C\u306B\u6CE8\u76EE\u3059\u308B\n5. \u8A72\u5F53\u3059\u308Bdiff\u753B\u50CF\uFF08`*--diff.png`\uFF09\u3092Read\u30C4\u30FC\u30EB\u3067\u8996\u899A\u7684\u306B\u78BA\u8A8D\u3059\u308B\n6. diff\u753B\u50CF\u304B\u3089\u4FEE\u6B63\u3059\u3079\u304DCSS\u3084HTML\u306E\u5834\u6240\u3092\u7279\u5B9A\u3067\u304D\u308B\u306A\u3089\u4FEE\u6B63\u306B\u306F\u3044\u308B\u3002\u518D\u5EA6VRT\u3092\u5B9F\u884C\u3057\u3066\u4FEE\u6B63\u304C\u53CD\u6620\u3055\u308C\u305F\u3053\u3068\u3092\u78BA\u8A8D\u3059\u308B\n7. \u4FEE\u6B63\u304C\u4E0D\u53EF\u80FD\u306A\u3089\u30E6\u30FC\u30B6\u30FC\u306B\u305D\u306E\u65E8\u3092\u4F1D\u3048\u3001\u30EC\u30DD\u30FC\u30C8\u304C\u51FA\u6765\u305F\u3053\u3068\u3092\u4F1D\u3048\u308B\n\n## \u30AA\u30D7\u30B7\u30E7\u30F3\n\n| \u30AA\u30D7\u30B7\u30E7\u30F3 | \u8AAC\u660E |\n| -------------------- | ------------------------------------------- |\n| `--reference <url>` | \u30EA\u30D5\u30A1\u30EC\u30F3\u30B9\u30D7\u30ED\u30C8\u30B3\u30EB+\u30C9\u30E1\u30A4\u30F3\uFF08\u672C\u756A\u74B0\u5883\uFF09 |\n| `--after <url>` | \u6BD4\u8F03\u5148U\u30D7\u30ED\u30C8\u30B3\u30EB+\u30C9\u30E1\u30A4\u30F3\uFF08\u958B\u767A\u74B0\u5883\uFF09 |\n| `--paths <paths>` | \u30C6\u30B9\u30C8\u5BFE\u8C61\u306E\u30DA\u30FC\u30B8\u30D1\u30B9\uFF08\u30AB\u30F3\u30DE\u533A\u5207\u308A\uFF09 |\n| `--threshold <n>` | \u5DEE\u5206\u8A31\u5BB9\u7387\uFF08%\uFF09\u3002\u30C7\u30D5\u30A9\u30EB\u30C8: 0.1 |\n| `--hide <selectors>` | \u975E\u8868\u793A\u306B\u3059\u308BCSS\u30BB\u30EC\u30AF\u30BF\uFF08\u30AB\u30F3\u30DE\u533A\u5207\u308A\uFF09 |\n| `--no-open` | HTML\u30EC\u30DD\u30FC\u30C8\u3092\u30D6\u30E9\u30A6\u30B6\u3067\u958B\u304B\u306A\u3044 |\n';
557
+
558
+ // src/commands/init.ts
560
559
  async function runInit(options) {
561
- const outDir = join4(process.cwd(), ".claude", "commands");
562
- const outFile = join4(outDir, "vrt.md");
560
+ const outDir = join4(process.cwd(), ".claude", "skills", "web-corders-vrt");
561
+ const outFile = join4(outDir, "SKILL.md");
563
562
  try {
564
563
  await access(outFile);
565
564
  console.log(chalk3.yellow(`\u26A0 ${outFile} already exists. Overwriting...`));
566
565
  } catch {
567
566
  }
568
567
  await mkdir2(outDir, { recursive: true });
569
- const content = generateSkillFile(options);
568
+ const content = SKILL_TEMPLATE_default.replace(
569
+ /\$\{referenceDomain\}/g,
570
+ options.refDomain
571
+ );
570
572
  await writeFile4(outFile, content, "utf-8");
571
573
  console.log(chalk3.green(`\u2705 Created ${outFile}`));
574
+ await ensureGitignore(process.cwd());
572
575
  console.log("");
573
576
  console.log(
574
- "This skill file allows Claude Code to run VRT with the /vrt command."
575
- );
576
- console.log(
577
- "Review and customize the file, then commit it to your repository."
577
+ "Review and customize the skill file, then commit it to your repository."
578
578
  );
579
579
  }
580
- function generateSkillFile(options) {
581
- const cmdParts = ["npx web-corders-vrt run"];
582
- cmdParts.push(` --before ${options.before}`);
583
- cmdParts.push(` --after ${options.after}`);
584
- cmdParts.push(` --paths ${options.paths}`);
585
- if (options.threshold !== void 0 && options.threshold !== DEFAULT_THRESHOLD) {
586
- cmdParts.push(` --threshold ${options.threshold}`);
580
+ var VRT_RESULTS_PATTERN = "vrt-results/";
581
+ async function ensureGitignore(cwd) {
582
+ const gitignorePath = join4(cwd, ".gitignore");
583
+ let content = "";
584
+ try {
585
+ content = await readFile(gitignorePath, "utf-8");
586
+ } catch {
587
587
  }
588
- if (options.hide) {
589
- cmdParts.push(` --hide "${options.hide}"`);
588
+ const lines = content.split("\n");
589
+ if (lines.some((line) => line.trim() === VRT_RESULTS_PATTERN)) {
590
+ return;
590
591
  }
591
- cmdParts.push(" --no-open");
592
- const command = cmdParts.join(" \\\n");
593
- return `VRT\u3092\u5B9F\u884C\u3057\u3066\u672C\u756A\u74B0\u5883\u3068\u306E\u30D3\u30B8\u30E5\u30A2\u30EB\u5DEE\u5206\u3092\u691C\u51FA\u3059\u308B\u3002
594
-
595
- ## \u624B\u9806
596
-
597
- 1. \u958B\u767A\u30B5\u30FC\u30D0\u30FC\u304C\u8D77\u52D5\u3057\u3066\u3044\u306A\u3051\u308C\u3070 \`npm run dev\` \u3067\u8D77\u52D5\u3057\u3066\u5F85\u6A5F\u3059\u308B
598
- 2. \u4EE5\u4E0B\u306E\u30B3\u30DE\u30F3\u30C9\u3067VRT\u3092\u5B9F\u884C:
599
-
600
- \`\`\`bash
601
- ${command}
602
- \`\`\`
603
-
604
- 3. \`./vrt-results/\` \u5185\u306E\u6700\u65B0\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u306B\u3042\u308B \`report.json\` \u3092\u8AAD\u3080
605
- 4. \`status: "fail"\` \u306E\u30C6\u30B9\u30C8\u7D50\u679C\u306B\u6CE8\u76EE\u3059\u308B
606
- 5. \u8A72\u5F53\u3059\u308Bdiff\u753B\u50CF\uFF08\`*--diff.png\`\uFF09\u3092Read\u30C4\u30FC\u30EB\u3067\u8996\u899A\u7684\u306B\u78BA\u8A8D\u3059\u308B
607
- 6. diff\u753B\u50CF\u304B\u3089\u4FEE\u6B63\u3059\u3079\u304DCSS\u3084HTML\u306E\u5834\u6240\u3092\u7279\u5B9A\u3059\u308B
608
- 7. \u30BD\u30FC\u30B9\u30B3\u30FC\u30C9\u3092\u4FEE\u6B63\u3059\u308B
609
- 8. \u518D\u5EA6VRT\u3092\u5B9F\u884C\u3057\u3066\u4FEE\u6B63\u304C\u53CD\u6620\u3055\u308C\u305F\u3053\u3068\u3092\u78BA\u8A8D\u3059\u308B
610
- `;
592
+ const separator = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
593
+ await writeFile4(
594
+ gitignorePath,
595
+ content + separator + VRT_RESULTS_PATTERN + "\n",
596
+ "utf-8"
597
+ );
598
+ console.log(chalk3.green(`\u2705 Added '${VRT_RESULTS_PATTERN}' to .gitignore`));
611
599
  }
612
600
 
613
601
  // bin/vrt.ts
614
602
  var program = new Command();
615
603
  program.name("vrt").description("Visual Regression Testing CLI - Compare web pages visually").version("0.1.0");
616
- program.command("run").description("Run visual regression tests").requiredOption("--before <url>", "Baseline URL (production)").requiredOption("--after <url>", "Comparison URL (local/staging)").requiredOption("--paths <paths>", "Page paths to compare (comma-separated)").option("--threshold <n>", "Diff tolerance percentage", "0.1").option("--hide <selectors>", "CSS selectors to hide (comma-separated)").option("--no-html", "Skip HTML report generation").option("--no-open", "Do not open HTML report in browser").action(async (rawOptions) => {
604
+ program.command("run").description("Run visual regression tests").requiredOption("--reference <url>", "Reference URL (production)").requiredOption("--after <url>", "Comparison URL (local/staging)").requiredOption("--paths <paths>", "Page paths to compare (comma-separated)").option("--threshold <n>", "Diff tolerance percentage", "0.1").option("--hide <selectors>", "CSS selectors to hide (comma-separated)").action(async (rawOptions) => {
617
605
  try {
618
606
  const parsed = cliOptionsSchema.parse(rawOptions);
619
607
  const options = {
620
- beforeUrl: parsed.before,
608
+ referenceUrl: parsed.reference,
621
609
  afterUrl: parsed.after,
622
610
  paths: parsed.paths,
623
611
  threshold: parsed.threshold,
624
- hideSelectors: parsed.hide,
625
- html: parsed.html,
626
- open: parsed.open
612
+ hideSelectors: parsed.hide
627
613
  };
628
614
  const report = await runVrt(options);
629
615
  process.exit(report.summary.overallStatus === "pass" ? 0 : 1);
@@ -637,16 +623,10 @@ program.command("run").description("Run visual regression tests").requiredOption
637
623
  }
638
624
  });
639
625
  program.command("init").description(
640
- "Generate .claude/commands/vrt.md skill file for this repository"
641
- ).requiredOption("--before <url>", "Baseline URL (production)").requiredOption("--after <url>", "Comparison URL (local/staging)").requiredOption("--paths <paths>", "Page paths to compare (comma-separated)").option("--threshold <n>", "Diff tolerance percentage", "0.1").option("--hide <selectors>", "CSS selectors to hide (comma-separated)").action(async (options) => {
626
+ "Generate .claude/skills/web-corders-vrt/SKILL.md for this repository"
627
+ ).requiredOption("--refDomain <domain>", "Reference domain URL (production)").action(async (options) => {
642
628
  try {
643
- await runInit({
644
- before: options.before,
645
- after: options.after,
646
- paths: options.paths,
647
- threshold: parseFloat(options.threshold),
648
- hide: options.hide
649
- });
629
+ await runInit({ refDomain: options.refDomain });
650
630
  } catch (error) {
651
631
  if (error instanceof Error) {
652
632
  console.error(`Error: ${error.message}`);